Initial publish

This commit is contained in:
2025-09-04 15:49:04 +07:00
commit 1f7e1825ba
28 changed files with 4079 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
@import 'tailwindcss';
@plugin '@tailwindcss/typography';
@custom-variant dark (&:where(.dark, .dark *));
body {
--theme-background: #f0f0f0;
--theme-background-darker: #e0e0e0;
--theme-foreground: #202020;
--theme-floating: #f0f0f0;
background-color: var(--theme-background);
color: var(--theme-foreground);
}
@utility bg-theme {
background-color: var(--theme-background);
}
@utility bg-theme-darker {
background-color: var(--theme-background-darker);
}
@utility bg-theme-floating {
background-color: var(--theme-floating);
}
@utility text-color-theme {
color: var(--theme-foreground);
}
.btn {
@apply px-2 py-1 cursor-pointer;
}
.btn-generic {
@apply bg-gray-400/40 hover:bg-gray-300/40 dark:hover:bg-gray-500/40 rounded transition-colors;
}
.generic-select {
@apply border py-1 px-2 rounded;
}
.printarea {
visibility: hidden;
display: none;
}
@media print {
body {
visibility: hidden;
}
.printarea {
display: block;
visibility: visible;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
}
+13
View File
@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+27
View File
@@ -0,0 +1,27 @@
.h1 {
font-size: 2em;
}
.ͼ5 {
color: gray;
}
.h2 {
font-size: 1.5em;
}
.h3 {
font-size: 1.17em;
}
.h4 {
font-size: 1em;
}
.h5 {
font-size: 0.83em;
}
.h6 {
font-size: 0.67em;
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+82
View File
@@ -0,0 +1,82 @@
<script lang="ts">
import { download } from '$lib/download';
import DOMPurify from 'dompurify';
import { marked } from 'marked';
const { content }: { content: string } = $props();
let exportDialog: HTMLDialogElement;
let format = $state('html');
let processing = $state(false);
let printContent: string = $state('');
export function showModal() {
exportDialog.showModal();
}
async function exportHtml() {
const html = await marked.parse(content);
const blob = new Blob([html], {
type: 'text/html'
});
download(blob, 'litewriter-document.html');
}
export async function exportPdf() {
printContent = DOMPurify.sanitize(await marked.parse(content));
setTimeout(() => {
window.print();
printContent = '';
}, 1000);
}
async function exportBtn() {
switch (format) {
case 'html':
await exportHtml();
break;
case 'pdf':
await exportPdf();
break;
case 'odt':
await (await import('../opendocument')).exportOdt(content);
break;
}
exportDialog.close();
}
</script>
<div class="printarea prose">
{@html printContent}
</div>
<dialog
bind:this={exportDialog}
class="m-auto py-4 px-5 bg-theme-floating text-color-theme border border-gray-500 rounded"
>
<h1 class="text-2xl">Export</h1>
<div class="grid grid-cols-2 my-4">
<label class="flex items-center" for="formatSelect">Format</label>
<div>
<select name="formatSelect" id="formatSelect" class="generic-select" bind:value={format}>
<option value="html">HTML</option>
<option value="pdf">PDF</option>
<option value="odt">OpenDocument Text (ODT)</option>
</select>
</div>
</div>
{#if processing}
<div class="font-bold">Please wait while your document is being processed...</div>
{/if}
<div class="flex justify-end gap-2">
<button class="btn-generic btn" onclick={() => exportDialog.close()} disabled={processing}>
Cancel
</button>
<button class="btn-generic btn" onclick={exportBtn} disabled={processing}> Export </button>
</div>
</dialog>
+14
View File
@@ -0,0 +1,14 @@
<script lang="ts">
let { children, mouseOver, blurEvent, buttonClicked, name, shownMenu } = $props();
</script>
<button
class={`hover:bg-gray-500/40 btn rounded transition-colors ${shownMenu === name ? 'bg-gray-500/40' : ''}`}
aria-haspopup="menu"
onmouseover={() => mouseOver(name)}
onfocus={() => mouseOver(name)}
onblur={(e) => blurEvent(e)}
onclick={() => buttonClicked(name)}
>
{@render children()}
</button>
+10
View File
@@ -0,0 +1,10 @@
export function download(blob: Blob, fileName: string) {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
link.click();
URL.revokeObjectURL(url);
}
+1
View File
@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.
+124
View File
@@ -0,0 +1,124 @@
export const bulletListContentStyle = `
<text:list-style style:name="L1">
<text:list-level-style-bullet text:level="1" text:style-name="BulletSymbols" loext:num-list-format="%1%" text:bullet-char="•">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="0.5in" fo:text-indent="-0.25in" fo:margin-left="0.5in"/>
</style:list-level-properties>
<style:text-properties fo:font-family="OpenSymbol"/>
</text:list-level-style-bullet>
<text:list-level-style-bullet text:level="2" text:style-name="BulletSymbols" loext:num-list-format="%2%" text:bullet-char="◦">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="0.75in" fo:text-indent="-0.25in" fo:margin-left="0.75in"/>
</style:list-level-properties>
<style:text-properties fo:font-family="OpenSymbol"/>
</text:list-level-style-bullet>
<text:list-level-style-bullet text:level="3" text:style-name="BulletSymbols" loext:num-list-format="%3%" text:bullet-char="▪">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="1in" fo:text-indent="-0.25in" fo:margin-left="1in"/>
</style:list-level-properties>
<style:text-properties fo:font-family="OpenSymbol"/>
</text:list-level-style-bullet>
<text:list-level-style-bullet text:level="4" text:style-name="BulletSymbols" loext:num-list-format="%4%" text:bullet-char="•">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="1.25in" fo:text-indent="-0.25in" fo:margin-left="1.25in"/>
</style:list-level-properties>
<style:text-properties fo:font-family="OpenSymbol"/>
</text:list-level-style-bullet>
<text:list-level-style-bullet text:level="5" text:style-name="BulletSymbols" loext:num-list-format="%5%" text:bullet-char="◦">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="1.5in" fo:text-indent="-0.25in" fo:margin-left="1.5in"/>
</style:list-level-properties>
<style:text-properties fo:font-family="OpenSymbol"/>
</text:list-level-style-bullet>
<text:list-level-style-bullet text:level="6" text:style-name="BulletSymbols" loext:num-list-format="%6%" text:bullet-char="▪">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="1.75in" fo:text-indent="-0.25in" fo:margin-left="1.75in"/>
</style:list-level-properties>
<style:text-properties fo:font-family="OpenSymbol"/>
</text:list-level-style-bullet>
<text:list-level-style-bullet text:level="7" text:style-name="BulletSymbols" loext:num-list-format="%7%" text:bullet-char="•">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="2in" fo:text-indent="-0.25in" fo:margin-left="2in"/>
</style:list-level-properties>
<style:text-properties fo:font-family="OpenSymbol"/>
</text:list-level-style-bullet>
<text:list-level-style-bullet text:level="8" text:style-name="BulletSymbols" loext:num-list-format="%8%" text:bullet-char="◦">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="2.25in" fo:text-indent="-0.25in" fo:margin-left="2.25in"/>
</style:list-level-properties>
<style:text-properties fo:font-family="OpenSymbol"/>
</text:list-level-style-bullet>
<text:list-level-style-bullet text:level="9" text:style-name="BulletSymbols" loext:num-list-format="%9%" text:bullet-char="▪">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="2.5in" fo:text-indent="-0.25in" fo:margin-left="2.5in"/>
</style:list-level-properties>
<style:text-properties fo:font-family="OpenSymbol"/>
</text:list-level-style-bullet>
<text:list-level-style-bullet text:level="10" text:style-name="BulletSymbols" loext:num-list-format="%10%" text:bullet-char="•">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="2.75in" fo:text-indent="-0.25in" fo:margin-left="2.75in"/>
</style:list-level-properties>
<style:text-properties fo:font-family="OpenSymbol"/>
</text:list-level-style-bullet>
</text:list-style>`;
export const bulletListStyle = `
<style:style style:name="BulletSymbols" style:display-name="Bullet Symbols" style:family="text">
<style:text-properties style:font-name="OpenSymbol" fo:font-family="OpenSymbol" style:font-charset="x-symbol" style:font-name-asian="OpenSymbol" style:font-family-asian="OpenSymbol" style:font-charset-asian="x-symbol" style:font-name-complex="OpenSymbol" style:font-family-complex="OpenSymbol" style:font-charset-complex="x-symbol"/>
</style:style>`;
export const orderedListContentStyle = `
<text:list-style style:name="L2">
<text:list-level-style-number text:level="1" text:style-name="NumberingSymbols" loext:num-list-format="%1%." style:num-suffix="." style:num-format="1">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="0.5in" fo:text-indent="-0.25in" fo:margin-left="0.5in"/>
</style:list-level-properties>
</text:list-level-style-number>
<text:list-level-style-number text:level="2" text:style-name="NumberingSymbols" loext:num-list-format="%2%." style:num-suffix="." style:num-format="1">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="0.75in" fo:text-indent="-0.25in" fo:margin-left="0.75in"/>
</style:list-level-properties>
</text:list-level-style-number>
<text:list-level-style-number text:level="3" text:style-name="NumberingSymbols" loext:num-list-format="%3%." style:num-suffix="." style:num-format="1">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="1in" fo:text-indent="-0.25in" fo:margin-left="1in"/>
</style:list-level-properties>
</text:list-level-style-number>
<text:list-level-style-number text:level="4" text:style-name="NumberingSymbols" loext:num-list-format="%4%." style:num-suffix="." style:num-format="1">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="1.25in" fo:text-indent="-0.25in" fo:margin-left="1.25in"/>
</style:list-level-properties>
</text:list-level-style-number>
<text:list-level-style-number text:level="5" text:style-name="NumberingSymbols" loext:num-list-format="%5%." style:num-suffix="." style:num-format="1">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="1.5in" fo:text-indent="-0.25in" fo:margin-left="1.5in"/>
</style:list-level-properties>
</text:list-level-style-number>
<text:list-level-style-number text:level="6" text:style-name="NumberingSymbols" loext:num-list-format="%6%." style:num-suffix="." style:num-format="1">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="1.75in" fo:text-indent="-0.25in" fo:margin-left="1.75in"/>
</style:list-level-properties>
</text:list-level-style-number>
<text:list-level-style-number text:level="7" text:style-name="NumberingSymbols" loext:num-list-format="%7%." style:num-suffix="." style:num-format="1">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="2in" fo:text-indent="-0.25in" fo:margin-left="2in"/>
</style:list-level-properties>
</text:list-level-style-number>
<text:list-level-style-number text:level="8" text:style-name="NumberingSymbols" loext:num-list-format="%8%." style:num-suffix="." style:num-format="1">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="2.25in" fo:text-indent="-0.25in" fo:margin-left="2.25in"/>
</style:list-level-properties>
</text:list-level-style-number>
<text:list-level-style-number text:level="9" text:style-name="NumberingSymbols" loext:num-list-format="%9%." style:num-suffix="." style:num-format="1">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="2.5in" fo:text-indent="-0.25in" fo:margin-left="2.5in"/>
</style:list-level-properties>
</text:list-level-style-number>
<text:list-level-style-number text:level="10" text:style-name="NumberingSymbols" loext:num-list-format="%10%." style:num-suffix="." style:num-format="1">
<style:list-level-properties text:list-level-position-and-space-mode="label-alignment">
<style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="2.75in" fo:text-indent="-0.25in" fo:margin-left="2.75in"/>
</style:list-level-properties>
</text:list-level-style-number>
</text:list-style>`;
export const orderedListStyle = `<style:style style:name="NumberingSymbols" style:display-name="Numbering Symbols" style:family="text"/>`;
+292
View File
@@ -0,0 +1,292 @@
import { marked, options, type Token, type Tokens, type TokensList } from "marked";
import JSZip from "jszip";
import { download } from "./download";
interface StylesRequired {
bulletList?: boolean;
orderedList?: boolean;
}
const contentTemplate = (content: string, customStyles: string) => `<?xml version="1.0" encoding="UTF-8"?>
<office:document-content
xmlns:css3t="http://www.w3.org/TR/css3-text/"
xmlns:grddl="http://www.w3.org/2003/g/data-view#"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xforms="http://www.w3.org/2002/xforms"
xmlns:dom="http://www.w3.org/2001/xml-events"
xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0"
xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0"
xmlns:math="http://www.w3.org/1998/Math/MathML"
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
xmlns:ooo="http://openoffice.org/2004/office"
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
xmlns:ooow="http://openoffice.org/2004/writer"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:drawooo="http://openoffice.org/2010/draw"
xmlns:oooc="http://openoffice.org/2004/calc"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0"
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2"
xmlns:tableooo="http://openoffice.org/2009/table"
xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"
xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0"
xmlns:rpt="http://openoffice.org/2005/report"
xmlns:formx="urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0"
xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"
xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0"
xmlns:officeooo="http://openoffice.org/2009/office"
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"
xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0"
xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0"
xmlns:field="urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0"
office:version="1.4">
<office:scripts/>
<office:font-face-decls/>
<office:automatic-styles>
<style:style style:name="P1" style:family="paragraph" style:parent-style-name="Heading1">
<style:paragraph-properties />
<style:text-properties officeooo:rsid="0005075b" officeooo:paragraph-rsid="0005075b"/>
</style:style>
<style:style style:family="text" style:name="TextBold">
<style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"/>
</style:style>
<style:style style:family="text" style:name="TextItalic">
<style:text-properties fo:font-style="italic" style:font-style-asian="italic" style:font-style-complex="italic"/>
</style:style>${customStyles}
</office:automatic-styles>
<office:body>
<office:text>
<text:sequence-decls>
<text:sequence-decl text:display-outline-level="0" text:name="Illustration"/>
<text:sequence-decl text:display-outline-level="0" text:name="Table"/>
<text:sequence-decl text:display-outline-level="0" text:name="Text"/>
<text:sequence-decl text:display-outline-level="0" text:name="Drawing"/>
<text:sequence-decl text:display-outline-level="0" text:name="Figure"/>
</text:sequence-decls>
${content}
</office:text>
</office:body>
</office:document-content>`;
const styleTemplate = (customStyles: string) => `<?xml version="1.0" encoding="UTF-8"?>
<office:document-styles
xmlns:css3t="http://www.w3.org/TR/css3-text/"
xmlns:grddl="http://www.w3.org/2003/g/data-view#"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:dom="http://www.w3.org/2001/xml-events"
xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0"
xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0"
xmlns:math="http://www.w3.org/1998/Math/MathML"
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
xmlns:ooo="http://openoffice.org/2004/office"
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
xmlns:ooow="http://openoffice.org/2004/writer"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:drawooo="http://openoffice.org/2010/draw"
xmlns:oooc="http://openoffice.org/2004/calc"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0"
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2"
xmlns:tableooo="http://openoffice.org/2009/table"
xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"
xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0"
xmlns:rpt="http://openoffice.org/2005/report"
xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"
xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0"
xmlns:officeooo="http://openoffice.org/2009/office"
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"
xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0"
xmlns:number="urn:oasis:names:tc:opendocument:
xmlns:datastyle:1.0"
xmlns:field="urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0"
office:version="1.4">
<office:font-face-decls/>
<office:styles>
<style:style style:class="default" style:display-name="Normal" style:family="paragraph" style:name="Normal">
<style:paragraph-properties fo:line-height="115%" fo:margin-top="0pt" fo:margin-bottom="10pt" />
<style:text-properties style:font-name="Noto Sans" fo:font-family="'Noto Sans'" style:font-style-name="Regular" style:font-family-generic="swiss" style:font-pitch="variable" />
</style:style>
<style:style style:name="Heading" style:family="paragraph" style:parent-style-name="Normal" style:class="chapter">
<style:paragraph-properties fo:margin-top="0.1665in" fo:margin-bottom="0.0835in" style:contextual-spacing="false" fo:keep-with-next="always"/>
<style:text-properties style:font-name="Noto Sans" fo:font-family="'Noto Sans'" style:font-style-name="Regular" style:font-family-generic="swiss" style:font-pitch="variable" fo:font-size="16pt" style:font-name-asian="Noto Sans CJK SC" style:font-family-asian="'Noto Sans CJK SC'" style:font-family-generic-asian="system" style:font-pitch-asian="variable" style:font-size-asian="14pt" style:font-name-complex="Noto Sans Thai1" style:font-family-complex="'Noto Sans Thai'" style:font-family-generic-complex="system" style:font-pitch-complex="variable" style:font-size-complex="18pt"/>
</style:style>
<style:style style:name="Heading1" style:display-name="Heading 1" style:family="paragraph" style:parent-style-name="Heading" style:default-outline-level="1" style:class="chapter">
<style:paragraph-properties fo:margin-top="0.1665in" fo:margin-bottom="0.0835in" style:contextual-spacing="false"/>
<style:text-properties fo:font-size="32pt" fo:font-weight="bold" style:font-size-asian="32pt" style:font-weight-asian="bold" style:font-size-complex="32pt" style:font-weight-complex="bold"/>
</style:style>
<style:style style:name="Heading2" style:display-name="Heading 2" style:family="paragraph" style:parent-style-name="Heading" style:default-outline-level="2" style:class="chapter">
<style:paragraph-properties fo:margin-top="0.1665in" fo:margin-bottom="0.0835in" style:contextual-spacing="false"/>
<style:text-properties fo:font-size="24pt" fo:font-weight="bold" style:font-size-asian="24pt" style:font-weight-asian="bold" style:font-size-complex="24pt" style:font-weight-complex="bold"/>
</style:style>
<style:style style:name="Heading3" style:display-name="Heading 3" style:family="paragraph" style:parent-style-name="Heading" style:default-outline-level="3" style:class="chapter">
<style:paragraph-properties fo:margin-top="0.1665in" fo:margin-bottom="0.0835in" style:contextual-spacing="false"/>
<style:text-properties fo:font-size="18.72pt" fo:font-weight="bold" style:font-size-asian="18.72pt" style:font-weight-asian="bold" style:font-size-complex="18.72pt" style:font-weight-complex="bold"/>
</style:style>
<style:style style:name="Code" style:family="paragraph" style:parent-style-name="Normal" style:class="default">
<style:text-properties style:font-name="JetBrains Mono" fo:font-family="'JetBrains Mono'" style:font-style-name="Regular" style:font-family-generic="swiss" style:font-pitch="variable" fo:font-size="10pt" style:font-name-asian="Noto Sans CJK SC" style:font-family-asian="'Noto Sans CJK SC'" style:font-family-generic-asian="system" style:font-pitch-asian="variable" style:font-size-asian="14pt" style:font-name-complex="Noto Sans Thai1" style:font-family-complex="'Noto Sans Thai'" style:font-family-generic-complex="system" style:font-pitch-complex="variable" style:font-size-complex="18pt"/>
</style:style>
<style:style style:name="Codespan" style:family="text" style:parent-style-name="Normal" style:class="default">
<style:text-properties style:font-name="JetBrains Mono" fo:font-family="'JetBrains Mono'" style:font-style-name="Regular" style:font-family-generic="swiss" style:font-pitch="variable" fo:font-size="10pt" style:font-name-asian="Noto Sans CJK SC" style:font-family-asian="'Noto Sans CJK SC'" style:font-family-generic-asian="system" style:font-pitch-asian="variable" style:font-size-asian="14pt" style:font-name-complex="Noto Sans Thai1" style:font-family-complex="'Noto Sans Thai'" style:font-family-generic-complex="system" style:font-pitch-complex="variable" style:font-size-complex="18pt"/>
</style:style>${customStyles}
</office:styles>
<office:automatic-styles>
<style:page-layout style:name="Mpm1">
<style:page-layout-properties fo:page-width="8.3in" fo:page-height="11.7in" style:num-format="1" style:print-orientation="portrait" fo:margin-top="0.7874in" fo:margin-bottom="0.7874in" fo:margin-left="0.7874in" fo:margin-right="0.7874in" style:writing-mode="lr-tb" style:footnote-max-height="0in" loext:margin-gutter="0in">
<style:footnote-sep style:width="0.0071in" style:distance-before-sep="0.0398in" style:distance-after-sep="0.0398in" style:line-style="solid" style:adjustment="left" style:rel-width="25%" style:color="#000000"/>
</style:page-layout-properties>
<style:header-style/>
<style:footer-style/>
</style:page-layout>
</office:automatic-styles>
<office:master-styles>
<style:master-page style:name="Standard" style:page-layout-name="Mpm1"/>
</office:master-styles>
</office:document-styles>`;
const manifestRdf = `<?xml version="1.0" encoding="utf-8"?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about="styles.xml">
<rdf:type rdf:resource="http://docs.oasis-open.org/ns/office/1.2/meta/odf#StylesFile"/>
</rdf:Description>
<rdf:Description rdf:about="">
<ns0:hasPart xmlns:ns0="http://docs.oasis-open.org/ns/office/1.2/meta/pkg#" rdf:resource="styles.xml"/>
</rdf:Description>
<rdf:Description rdf:about="content.xml">
<rdf:type rdf:resource="http://docs.oasis-open.org/ns/office/1.2/meta/odf#ContentFile"/>
</rdf:Description>
<rdf:Description rdf:about="">
<ns0:hasPart xmlns:ns0="http://docs.oasis-open.org/ns/office/1.2/meta/pkg#" rdf:resource="content.xml"/>
</rdf:Description>
<rdf:Description rdf:about="">
<rdf:type rdf:resource="http://docs.oasis-open.org/ns/office/1.2/meta/pkg#Document"/>
</rdf:Description>
</rdf:RDF>`;
const manifestInf = `<?xml version="1.0" encoding="UTF-8"?>
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.4" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0">
<manifest:file-entry manifest:full-path="/" manifest:version="1.4" manifest:media-type="application/vnd.oasis.opendocument.text"/>
<manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
<manifest:file-entry manifest:full-path="manifest.rdf" manifest:media-type="application/rdf+xml"/>
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
</manifest:manifest>`;
let stylesRequired: StylesRequired = {};
async function getCustomStyles() {
let contentStyle = "";
let style = "";
if (stylesRequired.bulletList || stylesRequired.orderedList) {
const listInfo = await import("./odt/list");
if (stylesRequired.bulletList) {
contentStyle += listInfo.bulletListContentStyle;
style += listInfo.bulletListStyle;
}
if (stylesRequired.orderedList) {
contentStyle += listInfo.orderedListContentStyle;
style += listInfo.orderedListStyle;
}
}
return {
content: contentStyle,
style: style
};
}
function parseTokens(tokens: TokensList | Token[]) {
let outputText = "";
for (const tok of tokens) {
switch (tok.type) {
case "heading":
outputText += `<text:h text:style-name="Heading${tok.depth}" text:outline-level="${tok.depth}">${parseTokens(tok.tokens ?? [])}</text:h>\n`;
break;
case "paragraph":
outputText += `<text:p text:style-name="Normal">${parseTokens(tok.tokens ?? [])}</text:p>`;
break;
case "strong":
outputText += `<text:span text:style-name="TextBold">${parseTokens(tok.tokens ?? [])}</text:span>`;
break;
case "em":
outputText += `<text:span text:style-name="TextItalic">${parseTokens(tok.tokens ?? [])}</text:span>`;
break;
case "codespan":
outputText += `<text:span text:style-name="Codespan">${tok.text}</text:span>`;
break;
case "code":
outputText += `<text:p text:style-name="Code">${(tok.text as string).replaceAll("\n", "<text:line-break/>")}</text:p>`;
break;
case "list":
if (tok.ordered) {
stylesRequired.orderedList = true;
} else {
stylesRequired.bulletList = true;
}
outputText += `<text:list text:style-name="${tok.ordered ? "L2" : "L1"}">
${parseTokens(tok.items)}</text:list>`;
break;
case "list_item":
outputText += "<text:list-item>";
const labelIndex = tok.tokens?.findIndex((t) => t.type === "text") ?? -1;
let remainingTokens = tok.tokens ?? [];
if (labelIndex !== -1) {
const label = tok.tokens?.[labelIndex];
outputText += "<text:p>";
if (tok.task) {
outputText += `[${tok.checked ? "x" : " "}] `;
}
outputText += (label as Tokens.Text).text;
outputText += "</text:p>";
remainingTokens.splice(labelIndex, 1);
}
outputText += `${parseTokens(remainingTokens)}</text:list-item>\n`;
break;
case "text":
outputText += tok.text;
break;
}
}
return outputText;
}
export async function exportOdt(content: string) {
stylesRequired = {};
const tokens = marked.lexer(content);
console.log(tokens);
const zip = new JSZip();
const xmlContent = parseTokens(tokens);
const customStyles = await getCustomStyles();
zip.file("content.xml", contentTemplate(xmlContent, customStyles.content));
zip.file("styles.xml", styleTemplate(customStyles.style));
zip.file("mimetype", "application/vnd.oasis.opendocument.text");
zip.file("manifest.rdf", manifestRdf);
const metaInf = zip.folder("META-INF");
metaInf?.file("manifest.xml", manifestInf);
const zipContent = await zip.generateAsync({ type: "blob", mimeType: "application/vnd.oasis.opendocument.text" });
download(zipContent, "litewriter-document.odt");
}
+11
View File
@@ -0,0 +1,11 @@
import type { PageTheme } from "./theme";
import { dracula as draculaCode } from "thememirror";
export const dracula: PageTheme = {
background: "#282A36",
backgroundDark: "#21222C",
floatingControls: "#343746",
foreground: "#F8F8F2",
codemirror: draculaCode,
dark: true,
};
+10
View File
@@ -0,0 +1,10 @@
import type { Extension } from "@codemirror/state";
export interface PageTheme {
background: string;
backgroundDark?: string;
floatingControls?: string;
foreground: string;
dark: boolean;
codemirror: Extension;
}
+12
View File
@@ -0,0 +1,12 @@
<script lang="ts">
import '../app.css';
import favicon from '$lib/assets/favicon.svg';
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
{@render children?.()}
+21
View File
@@ -0,0 +1,21 @@
import { fail, type Actions } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
export const actions = {
changeTheme: async ({ cookies, request }) => {
const data = await request.formData();
const theme = data.get("theme");
if (theme === null || typeof (theme) !== "string") {
return fail(400);
}
cookies.set("lwThemeName", theme, { path: "/" });
}
} satisfies Actions;
export const load: PageServerLoad = async ({ cookies }) => {
return {
themeName: cookies.get("lwThemeName") ?? "Default"
};
};
+272
View File
@@ -0,0 +1,272 @@
<script lang="ts">
import { markdown } from '@codemirror/lang-markdown';
import CodeMirror from 'svelte-codemirror-editor';
import '../css/codemirror.css';
import { Decoration, MatchDecorator, ViewPlugin } from '@codemirror/view';
import type { PageTheme } from '$lib/themes/theme';
import { dracula } from '$lib/themes/dracula';
import { enhance } from '$app/forms';
import { onMount } from 'svelte';
import MenuButton from '$lib/components/MenuButton.svelte';
import ExportDialog from '$lib/components/ExportDialog.svelte';
let { data } = $props();
let { themeName } = data;
let content = $state(`# Heading 1
## Heading 2
### Heading 3
**bold** *italic*`);
let changeThemeForm: HTMLFormElement;
const themeOptions: { [key: string]: PageTheme | null } = {
Default: null,
Dracula: dracula
};
let cmTheme = themeOptions[themeName]?.codemirror ?? null;
let headingDecorations: { [key: string]: Decoration } = {
'#': Decoration.mark({ class: 'h1' }),
'##': Decoration.mark({ class: 'h2' }),
'###': Decoration.mark({ class: 'h3' }),
'####': Decoration.mark({ class: 'h4' }),
'#####': Decoration.mark({ class: 'h5' }),
'######': Decoration.mark({ class: 'h6' })
};
let decorator = new MatchDecorator({
regexp: /^#{1,6} .*/gm,
decoration(match, view, pos) {
return headingDecorations[match[0].split(' ')[0]];
}
});
const headingPlugin = ViewPlugin.define(
(view) => ({
decorations: decorator.createDeco(view),
update(u) {
this.decorations = decorator.updateDeco(u, this.decorations);
}
}),
{
decorations: (v) => v.decorations
}
);
onMount(() => {
const pageTheme = themeOptions[themeName];
if (pageTheme?.dark) {
document.body.classList.add('dark');
}
document.body.style.setProperty('--theme-background', pageTheme?.background ?? null);
document.body.style.setProperty('--theme-background-darker', pageTheme?.backgroundDark ?? null);
document.body.style.setProperty('--theme-floating', pageTheme?.floatingControls ?? null);
document.body.style.setProperty('--theme-foreground', pageTheme?.foreground ?? null);
});
let shownMenu = $state<string | null>(null);
let exportDialog: ExportDialog;
function menuButtonMouseOver(menuName: string) {
if (shownMenu === null) return;
shownMenu = menuName;
}
function menuButtonClicked(menuName: string) {
if (shownMenu === menuName) {
shownMenu = null;
menuTriggeredWithAlt = false;
return;
}
shownMenu = menuName;
}
function menuButtonBlur(e: FocusEvent) {
if (
e.relatedTarget !== null &&
(e.relatedTarget as HTMLElement).classList.contains('top-menu-button')
) {
return;
}
shownMenu = null;
}
function menuActionComplete() {
shownMenu = null;
}
function newFileBtn() {
content = '';
menuActionComplete();
}
function aboutBtn() {
alert(`litewriter - Open-source online Markdown editor for writing documents`);
menuActionComplete();
}
function exportFileBtn() {
exportDialog.showModal();
menuActionComplete();
}
function printFileBtn() {
exportDialog.exportPdf();
menuActionComplete();
}
let altPressed = $state(false);
let menuTriggeredWithAlt = $state(false);
const altMenuMapping: { [key: string]: string } = {
f: 'file',
h: 'help'
};
const altSubmenuMapping: { [menu: string]: { [key: string]: Function } } = {
file: {
n: newFileBtn,
e: exportFileBtn,
p: printFileBtn
},
help: {
a: aboutBtn
}
};
function keyDownListener(ev: KeyboardEvent) {
if (menuTriggeredWithAlt && shownMenu !== null && altSubmenuMapping[shownMenu]?.[ev.key]) {
ev.preventDefault();
ev.stopPropagation();
altSubmenuMapping[shownMenu][ev.key]();
menuTriggeredWithAlt = false;
shownMenu = null;
}
if (ev.altKey) {
const menu = altMenuMapping[ev.key];
if (menu) {
ev.preventDefault();
ev.stopPropagation();
menuTriggeredWithAlt = true;
menuButtonClicked(menu);
return;
}
if (ev.key === 'Alt') {
ev.preventDefault();
ev.stopPropagation();
altPressed = true;
menuTriggeredWithAlt = false;
}
return;
}
}
function keyUpListener(ev: KeyboardEvent) {
if (ev.key === 'Alt') {
altPressed = false;
}
}
</script>
<svelte:body onkeydown={keyDownListener} onkeyup={keyUpListener} />
<header class="p-4 flex justify-between bg-theme-darker">
<ul class="flex" role="list">
<li>
<MenuButton
{shownMenu}
name="file"
mouseOver={menuButtonMouseOver}
buttonClicked={menuButtonClicked}
blurEvent={menuButtonBlur}
>
<span class={altPressed ? 'underline' : ''}>F</span>ile
</MenuButton>
<div
class={`z-10 w-44 absolute bg-theme-floating rounded ${shownMenu === 'file' ? '' : 'hidden'} shadow`}
role="menu"
>
<ul>
<li class="flex">
<button
class="hover:bg-gray-300/40 dark:hover:bg-gray-500/40 btn rounded transition-colors flex-1 text-left top-menu-button"
onclick={newFileBtn}
>
<span class={menuTriggeredWithAlt ? 'underline' : ''}>N</span>ew File
</button>
</li>
<li class="flex">
<button
class="hover:bg-gray-300/40 dark:hover:bg-gray-500/40 btn rounded transition-colors flex-1 text-left top-menu-button"
onclick={exportFileBtn}
>
<span class={menuTriggeredWithAlt ? 'underline' : ''}>E</span>xport...
</button>
</li>
<li class="flex">
<button
class="hover:bg-gray-300/40 dark:hover:bg-gray-500/40 btn rounded transition-colors flex-1 text-left top-menu-button"
onclick={printFileBtn}
>
<span class={menuTriggeredWithAlt ? 'underline' : ''}>P</span>rint...
</button>
</li>
</ul>
</div>
</li>
<li>
<MenuButton
{shownMenu}
name="help"
mouseOver={menuButtonMouseOver}
buttonClicked={menuButtonClicked}
blurEvent={menuButtonBlur}
>
<span class={altPressed ? 'underline' : ''}>H</span>elp
</MenuButton>
<div
class={`z-10 w-44 absolute bg-theme-floating rounded ${shownMenu === 'help' ? '' : 'hidden'} shadow`}
role="menu"
>
<ul>
<li class="flex">
<button
class="hover:bg-gray-300/40 dark:hover:bg-gray-500/40 btn rounded transition-colors flex-1 text-left top-menu-button"
>
<span class={menuTriggeredWithAlt ? 'underline' : ''}>A</span>bout
</button>
</li>
</ul>
</div>
</li>
</ul>
<form method="POST" use:enhance action="?/changeTheme" bind:this={changeThemeForm}>
<select
oninput={() => changeThemeForm.submit()}
value={themeName}
name="theme"
id="themeSelector"
class="generic-select"
>
<option value="Default">Default (Light)</option>
<option value="Dracula">Dracula</option>
</select>
</form>
</header>
<ExportDialog bind:this={exportDialog} {content} />
<CodeMirror bind:value={content} theme={cmTheme} lang={markdown()} extensions={[headingPlugin]} />