Initial publish
This commit is contained in:
+23
@@ -0,0 +1,23 @@
|
||||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
@@ -0,0 +1,9 @@
|
||||
# Package Managers
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
bun.lock
|
||||
bun.lockb
|
||||
|
||||
# Miscellaneous
|
||||
/static/
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# litewriter
|
||||
|
||||
A (not so) lightweight Markdown editor.
|
||||
|
||||
Focuses on allowing exports to various formats and theming capabilities.
|
||||
|
||||
Currently supports exporting to HTML, PDF, and OpenDocument Text (ODT).
|
||||
Also plans to support DOCX in the future.
|
||||
|
||||
## Performance
|
||||
|
||||
HTML and PDF exporters are pretty fast. Both uses the Marked library to
|
||||
convert Markdown to HTML and the PDF converter uses the browser's print
|
||||
dialog to save as PDF.
|
||||
|
||||
However, the ODT converter requires a lot of verbose template XMLs, so it
|
||||
may be slower than others, still, it won't slow the entire page down as it's
|
||||
lazily loaded.
|
||||
|
||||
## ODT Converter
|
||||
|
||||
The ODT converter is based on XML content generated from LibreOffice and ONLYOFFICE.
|
||||
Though, It is most compatible with LibreOffice.
|
||||
|
||||
There's also one known limitation, Block quotes can't be used at the moment.
|
||||
@@ -0,0 +1,40 @@
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
import js from '@eslint/js';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import globals from 'globals';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import ts from 'typescript-eslint';
|
||||
import svelteConfig from './svelte.config.js';
|
||||
|
||||
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||
|
||||
export default ts.config(
|
||||
includeIgnoreFile(gitignorePath),
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs.recommended,
|
||||
prettier,
|
||||
...svelte.configs.prettier,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: { ...globals.browser, ...globals.node }
|
||||
},
|
||||
rules: {
|
||||
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
||||
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||
'no-undef': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
extraFileExtensions: ['.svelte'],
|
||||
parser: ts.parser,
|
||||
svelteConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "litewriter",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.5",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/kit": "^2.22.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@types/html2pdf.js": "^0.10.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-svelte": "^3.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.20.0",
|
||||
"vite": "^7.0.4"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@tailwindcss/oxide",
|
||||
"core-js",
|
||||
"esbuild"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@10.15.1+sha512.34e538c329b5553014ca8e8f4535997f96180a1d0f614339357449935350d924e22f8614682191264ec33d1462ac21561aff97f6bb18065351c162c7e8f6de67",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-markdown": "^6.3.4",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.38.1",
|
||||
"dompurify": "^3.2.6",
|
||||
"jszip": "^3.10.1",
|
||||
"marked": "^16.2.1",
|
||||
"svelte-codemirror-editor": "^1.4.1",
|
||||
"thememirror": "^2.0.1"
|
||||
}
|
||||
}
|
||||
Generated
+2898
File diff suppressed because it is too large
Load Diff
+63
@@ -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%;
|
||||
}
|
||||
}
|
||||
Vendored
+13
@@ -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 {};
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
@@ -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"/>`;
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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?.()}
|
||||
@@ -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"
|
||||
};
|
||||
};
|
||||
@@ -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]} />
|
||||
@@ -0,0 +1,3 @@
|
||||
# allow crawling everything by default
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,18 @@
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://svelte.dev/docs/kit/integrations
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
});
|
||||
Reference in New Issue
Block a user