diff --git a/.github/workflows/playground-pages.yml b/.github/workflows/playground-pages.yml new file mode 100644 index 0000000..7aca6c6 --- /dev/null +++ b/.github/workflows/playground-pages.yml @@ -0,0 +1,51 @@ +name: Playground Pages + +on: + push: + branches: + - playground + release: + types: + - published + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref || github.ref }} + + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: 2.8.0 + + - name: Build playground + run: deno task playground:build + + - name: Setup Pages + uses: actions/configure-pages@v6 + + - name: Upload playground artifact + uses: actions/upload-pages-artifact@v5 + with: + path: playground/dist + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/README.md b/README.md index 1c6520d..4ef35a0 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ an existing `.docx` or `.dotx` template. You could use `docxml` to: -- Create an MS Word file without ever opening MS Word -- Create a parameterized template file and render it to `.docx` with your data parameters -- Write JSON, XML or other data structures to `.docx` -- Parse content from an existing `.docx` file -- Extract style information from a `.docx` or `.dotx` file +- Create an MS Word file without ever opening MS Word +- Create a parameterized template file and render it to `.docx` with your data parameters +- Write JSON, XML or other data structures to `.docx` +- Parse content from an existing `.docx` file +- Extract style information from a `.docx` or `.dotx` file This documentation for this lib is available at various locations: @@ -18,6 +18,10 @@ This documentation for this lib is available at various locations: [👉 Deno mirror](http://jsr.io/@fontoxml/docxml)
[👉 npm mirror](http://npmjs.org/package/docxml) +#### Try it now! + +**[Docxml playground](https://fontoxml.github.io/docxml/)** + #### For Deno [👉 Main article](https://github.com/fontoxml/docxml/wiki/Deno-or-Node) @@ -85,68 +89,68 @@ yet -- they are not available, but hopefully soon. **API features:** -- [x] 100% typed -- [x] Asynchronous components -- [x] Component composition +- [x] 100% typed +- [x] Asynchronous components +- [x] Component composition **Custom styles:** -- [x] Font size and color -- [x] Bold, italic, underline styles, strike-through -- [x] Subscript, superscript, small caps -- [x] Paragraph spacing and indentation -- [x] Left/right/center/justified alignment -- [x] Numbering -- [ ] Aligning text on tabs -- [x] Font family -- [ ] Embed TTF in the DOCX file +- [x] Font size and color +- [x] Bold, italic, underline styles, strike-through +- [x] Subscript, superscript, small caps +- [x] Paragraph spacing and indentation +- [x] Left/right/center/justified alignment +- [x] Numbering +- [ ] Aligning text on tabs +- [x] Font family +- [ ] Embed TTF in the DOCX file **References:** -- [x] Cross references -- [ ] Table of contents +- [x] Cross references +- [ ] Table of contents **Tables:** -- [x] Colspans and rowspans -- [x] Cell borders -- [x] [Table borders](http://officeopenxml.com/WPtableBorders.php) -- [x] [Conditional formatting](http://officeopenxml.com/WPtblLook.php) +- [x] Colspans and rowspans +- [x] Cell borders +- [x] [Table borders](http://officeopenxml.com/WPtableBorders.php) +- [x] [Conditional formatting](http://officeopenxml.com/WPtblLook.php) **Images:** -- [x] From any `UInt8Array` source -- [x] Alternative and title text -- [x] Width and height +- [x] From any `UInt8Array` source +- [x] Alternative and title text +- [x] Width and height **Sections:** -- [x] Width and height -- [x] Orientation -- [x] Page headers & footers +- [x] Width and height +- [x] Orientation +- [x] Page headers & footers **Comments:** -- [x] Point comment -- [x] Range comment -- [ ] Comment reply +- [x] Point comment +- [x] Range comment +- [ ] Comment reply **Change tracking:** -- [x] Text additions and deletions -- [x] Style changes -- [x] Table row additions and deletions +- [x] Text additions and deletions +- [x] Style changes +- [x] Table row additions and deletions #### Differences with actual MS Word DOCX Obviously `docxml` is a TypeScript project, which is already very different from how you would normally interact with a `.docx` file. More meaningfully however, `docxml` is meant to make writing Word document files _easier_ than going straight to OOXML. For example: -- All sizes are of type `Length`, which means it doesn't matter wether you input them as points, centimeters, inches, 1/2, 1/8th or 1/20th points, English Metric Units, and so on. -- The JSX pragma will try to correct components that would lead to invalid XML structures, by splitting the parents of invalidly placed components recursively until the new position is valid. Moreover, string content in unexpected places is automatically wrapped in `` when using JSX. This makes the configuration of a new DOCX a little more forgiving. -- Using the `` or `` components will automatically create all required relationships etc. -- Some of the words have changed, generally speaking `docxml` is more verbose than the DOCX verbiage. -- Generally speaking `docxml` prefers formal (JS) references over references-by-identifier. In those cases the identifiers are randomly generated for you when the `.docx` file is written. -- Especially in tables and images, a lot of formatting details are automatically applied. In a lot of cases there is no API _yet_ to change them. +- All sizes are of type `Length`, which means it doesn't matter wether you input them as points, centimeters, inches, 1/2, 1/8th or 1/20th points, English Metric Units, and so on. +- The JSX pragma will try to correct components that would lead to invalid XML structures, by splitting the parents of invalidly placed components recursively until the new position is valid. Moreover, string content in unexpected places is automatically wrapped in `` when using JSX. This makes the configuration of a new DOCX a little more forgiving. +- Using the `` or `` components will automatically create all required relationships etc. +- Some of the words have changed, generally speaking `docxml` is more verbose than the DOCX verbiage. +- Generally speaking `docxml` prefers formal (JS) references over references-by-identifier. In those cases the identifiers are randomly generated for you when the `.docx` file is written. +- Especially in tables and images, a lot of formatting details are automatically applied. In a lot of cases there is no API _yet_ to change them. #### For contributors diff --git a/deno.json b/deno.json index 080ec9f..e81bdc8 100644 --- a/deno.json +++ b/deno.json @@ -49,9 +49,12 @@ }, "importMap": "imports.json", "tasks": { - "check": "deno check .", - "lint": "deno lint", + "check": "deno check mod.ts inspect.ts $(find examples lib vendor -type f \\( -name '*.ts' -o -name '*.tsx' \\))", + "lint": "deno lint --ignore=playground/dist", "test": "deno task lint && deno task check && deno test -A lib", + "playground:dev": "cd playground && deno task dev", + "playground:build": "cd playground && deno task build", + "playground:preview": "cd playground && deno task preview", "test:coverage": "deno task test --coverage=coverage && deno coverage --exclude='utils/xml-validation/fontoxml' --exclude='vendor' --lcov --output=coverage/cov.lcov coverage && genhtml --ignore-errors inconsistent --output-directory ./coverage/html_cov ./coverage/cov.lcov && open ./coverage/html_cov/index.html", "precommit": "deno task lint && deno task test" } diff --git a/deno.lock b/deno.lock index 8287051..8ab7c98 100644 --- a/deno.lock +++ b/deno.lock @@ -12,7 +12,8 @@ "npm:eslint@10.4.0": "10.4.0", "npm:fontoxpath@3.34.0": "3.34.0", "npm:jszip@3.10.1": "3.10.1", - "npm:slimdom@4.3.5": "4.3.5" + "npm:slimdom@4.3.5": "4.3.5", + "npm:typescript@6.0.3": "6.0.3" }, "jsr": { "@std/assert@1.0.19": { @@ -463,6 +464,10 @@ "prelude-ls" ] }, + "typescript@6.0.3": { + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "bin": true + }, "uri-js@4.4.1": { "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dependencies": [ @@ -504,7 +509,8 @@ "npm:eslint@10.4.0", "npm:fontoxpath@3.34.0", "npm:jszip@3.10.1", - "npm:slimdom@4.3.5" + "npm:slimdom@4.3.5", + "npm:typescript@6.0.3" ] } } diff --git a/imports.json b/imports.json index 67c5570..32b503b 100644 --- a/imports.json +++ b/imports.json @@ -15,6 +15,7 @@ "std/path/posix/relative": "jsr:@std/path@1.1.4/posix/relative", "std/path/posix/resolve": "jsr:@std/path@1.1.4/posix/resolve", "std/testing/bdd": "jsr:@std/testing@1.0.18/bdd", + "typescript": "npm:typescript@6.0.3", "xml_renderer": "./vendor/xml_renderer/mod.ts" } diff --git a/playground/.gitignore b/playground/.gitignore new file mode 100644 index 0000000..9a18220 --- /dev/null +++ b/playground/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +public/ \ No newline at end of file diff --git a/playground/deno.json b/playground/deno.json new file mode 100644 index 0000000..b0b3c1c --- /dev/null +++ b/playground/deno.json @@ -0,0 +1,16 @@ +{ + "tasks": { + "types": "deno run -A scripts/generate-docxml-dts.ts", + "dev": "deno task types && deno run -A --node-modules-dir=auto npm:vite", + "build": "deno task types && deno run -A --node-modules-dir=auto npm:vite build", + "preview": "deno run -A --node-modules-dir=auto npm:vite preview --host --port 4173" + }, + "nodeModulesDir": "auto", + "compilerOptions": { + "lib": ["dom", "dom.iterable", "es2021"], + "jsx": "react-jsx", + "jsxImportSource": "preact", + "types": ["vite/client"] + }, + "exclude": ["dist"] +} diff --git a/playground/deno.lock b/playground/deno.lock new file mode 100644 index 0000000..04353bb --- /dev/null +++ b/playground/deno.lock @@ -0,0 +1,808 @@ +{ + "version": "5", + "specifiers": { + "npm:@preact/preset-vite@2.10.5": "2.10.5_@babel+core@7.29.0_vite@5.4.21_preact@10.29.2", + "npm:monaco-editor@0.53.0": "0.53.0", + "npm:preact@10.29.2": "10.29.2", + "npm:typescript@*": "5.9.3", + "npm:typescript@5.9.3": "5.9.3", + "npm:vite@*": "5.4.21", + "npm:vite@5.4.21": "5.4.21" + }, + "npm": { + "@babel/code-frame@7.29.0": { + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dependencies": [ + "@babel/helper-validator-identifier", + "js-tokens", + "picocolors" + ] + }, + "@babel/compat-data@7.29.3": { + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==" + }, + "@babel/core@7.29.0": { + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dependencies": [ + "@babel/code-frame", + "@babel/generator", + "@babel/helper-compilation-targets", + "@babel/helper-module-transforms", + "@babel/helpers", + "@babel/parser", + "@babel/template", + "@babel/traverse", + "@babel/types", + "@jridgewell/remapping", + "convert-source-map", + "debug", + "gensync", + "json5", + "semver" + ] + }, + "@babel/generator@7.29.1": { + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dependencies": [ + "@babel/parser", + "@babel/types", + "@jridgewell/gen-mapping", + "@jridgewell/trace-mapping", + "jsesc" + ] + }, + "@babel/helper-annotate-as-pure@7.27.3": { + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dependencies": [ + "@babel/types" + ] + }, + "@babel/helper-compilation-targets@7.28.6": { + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dependencies": [ + "@babel/compat-data", + "@babel/helper-validator-option", + "browserslist", + "lru-cache", + "semver" + ] + }, + "@babel/helper-globals@7.28.0": { + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==" + }, + "@babel/helper-module-imports@7.28.6": { + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dependencies": [ + "@babel/traverse", + "@babel/types" + ] + }, + "@babel/helper-module-transforms@7.28.6_@babel+core@7.29.0": { + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dependencies": [ + "@babel/core", + "@babel/helper-module-imports", + "@babel/helper-validator-identifier", + "@babel/traverse" + ] + }, + "@babel/helper-plugin-utils@7.28.6": { + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==" + }, + "@babel/helper-string-parser@7.27.1": { + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" + }, + "@babel/helper-validator-identifier@7.28.5": { + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==" + }, + "@babel/helper-validator-option@7.27.1": { + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==" + }, + "@babel/helpers@7.29.2": { + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dependencies": [ + "@babel/template", + "@babel/types" + ] + }, + "@babel/parser@7.29.3": { + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dependencies": [ + "@babel/types" + ], + "bin": true + }, + "@babel/plugin-syntax-jsx@7.28.6_@babel+core@7.29.0": { + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dependencies": [ + "@babel/core", + "@babel/helper-plugin-utils" + ] + }, + "@babel/plugin-transform-react-jsx-development@7.27.1_@babel+core@7.29.0": { + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "dependencies": [ + "@babel/core", + "@babel/plugin-transform-react-jsx" + ] + }, + "@babel/plugin-transform-react-jsx@7.28.6_@babel+core@7.29.0": { + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "dependencies": [ + "@babel/core", + "@babel/helper-annotate-as-pure", + "@babel/helper-module-imports", + "@babel/helper-plugin-utils", + "@babel/plugin-syntax-jsx", + "@babel/types" + ] + }, + "@babel/template@7.28.6": { + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dependencies": [ + "@babel/code-frame", + "@babel/parser", + "@babel/types" + ] + }, + "@babel/traverse@7.29.0": { + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dependencies": [ + "@babel/code-frame", + "@babel/generator", + "@babel/helper-globals", + "@babel/parser", + "@babel/template", + "@babel/types", + "debug" + ] + }, + "@babel/types@7.29.0": { + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dependencies": [ + "@babel/helper-string-parser", + "@babel/helper-validator-identifier" + ] + }, + "@esbuild/aix-ppc64@0.21.5": { + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "os": ["aix"], + "cpu": ["ppc64"] + }, + "@esbuild/android-arm64@0.21.5": { + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@esbuild/android-arm@0.21.5": { + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "os": ["android"], + "cpu": ["arm"] + }, + "@esbuild/android-x64@0.21.5": { + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "os": ["android"], + "cpu": ["x64"] + }, + "@esbuild/darwin-arm64@0.21.5": { + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@esbuild/darwin-x64@0.21.5": { + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@esbuild/freebsd-arm64@0.21.5": { + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@esbuild/freebsd-x64@0.21.5": { + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@esbuild/linux-arm64@0.21.5": { + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@esbuild/linux-arm@0.21.5": { + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@esbuild/linux-ia32@0.21.5": { + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "os": ["linux"], + "cpu": ["ia32"] + }, + "@esbuild/linux-loong64@0.21.5": { + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@esbuild/linux-mips64el@0.21.5": { + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "os": ["linux"], + "cpu": ["mips64el"] + }, + "@esbuild/linux-ppc64@0.21.5": { + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@esbuild/linux-riscv64@0.21.5": { + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@esbuild/linux-s390x@0.21.5": { + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@esbuild/linux-x64@0.21.5": { + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@esbuild/netbsd-x64@0.21.5": { + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "os": ["netbsd"], + "cpu": ["x64"] + }, + "@esbuild/openbsd-x64@0.21.5": { + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@esbuild/sunos-x64@0.21.5": { + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "os": ["sunos"], + "cpu": ["x64"] + }, + "@esbuild/win32-arm64@0.21.5": { + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@esbuild/win32-ia32@0.21.5": { + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@esbuild/win32-x64@0.21.5": { + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@jridgewell/gen-mapping@0.3.13": { + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dependencies": [ + "@jridgewell/sourcemap-codec", + "@jridgewell/trace-mapping" + ] + }, + "@jridgewell/remapping@2.3.5": { + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dependencies": [ + "@jridgewell/gen-mapping", + "@jridgewell/trace-mapping" + ] + }, + "@jridgewell/resolve-uri@3.1.2": { + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + }, + "@jridgewell/sourcemap-codec@1.5.5": { + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "@jridgewell/trace-mapping@0.3.31": { + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dependencies": [ + "@jridgewell/resolve-uri", + "@jridgewell/sourcemap-codec" + ] + }, + "@preact/preset-vite@2.10.5_@babel+core@7.29.0_vite@5.4.21_preact@10.29.2": { + "integrity": "sha512-p0vJpxiVO7KWWazWny3LUZ+saXyZKWv6Ju0bYMWNJRp2YveufRPgSUB1C4MTqGJfz07EehMgfN+AJNwQy+w6Iw==", + "dependencies": [ + "@babel/core", + "@babel/plugin-transform-react-jsx", + "@babel/plugin-transform-react-jsx-development", + "@prefresh/vite", + "@rollup/pluginutils@5.3.0", + "babel-plugin-transform-hook-names", + "debug", + "magic-string", + "picocolors", + "vite", + "vite-prerender-plugin", + "zimmerframe" + ] + }, + "@prefresh/babel-plugin@0.5.3": { + "integrity": "sha512-57LX2SHs4BX2s1IwCjNzTE2OJeEepRCNf1VTEpbNcUyHfMO68eeOWGDIt4ob9aYlW6PEWZ1SuwNikuoIXANDtQ==" + }, + "@prefresh/core@1.5.10_preact@10.29.2": { + "integrity": "sha512-7yPTFbG56sutaFu8krp3B4a200KOFUvrtlllKWRuLjsYXo9UUucHOZRcer+gtgMkFTpv6ob8TGcTwA32bSwa1w==", + "dependencies": [ + "preact" + ] + }, + "@prefresh/utils@1.2.1": { + "integrity": "sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==" + }, + "@prefresh/vite@2.4.12_preact@10.29.2_vite@5.4.21": { + "integrity": "sha512-FY1fzXpUjiuosznMV0YM7XAOPZjB5FIdWS0W24+XnlxYkt9hNAwwsiKYn+cuTEoMtD/ZVazS5QVssBr9YhpCQA==", + "dependencies": [ + "@babel/core", + "@prefresh/babel-plugin", + "@prefresh/core", + "@prefresh/utils", + "@rollup/pluginutils@4.2.1", + "preact", + "vite" + ] + }, + "@rollup/pluginutils@4.2.1": { + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dependencies": [ + "estree-walker", + "picomatch@2.3.2" + ] + }, + "@rollup/pluginutils@5.3.0": { + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dependencies": [ + "@types/estree", + "estree-walker", + "picomatch@4.0.4" + ] + }, + "@rollup/rollup-android-arm-eabi@4.60.4": { + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "os": ["android"], + "cpu": ["arm"] + }, + "@rollup/rollup-android-arm64@4.60.4": { + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@rollup/rollup-darwin-arm64@4.60.4": { + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@rollup/rollup-darwin-x64@4.60.4": { + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@rollup/rollup-freebsd-arm64@4.60.4": { + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@rollup/rollup-freebsd-x64@4.60.4": { + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@rollup/rollup-linux-arm-gnueabihf@4.60.4": { + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@rollup/rollup-linux-arm-musleabihf@4.60.4": { + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@rollup/rollup-linux-arm64-gnu@4.60.4": { + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@rollup/rollup-linux-arm64-musl@4.60.4": { + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@rollup/rollup-linux-loong64-gnu@4.60.4": { + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@rollup/rollup-linux-loong64-musl@4.60.4": { + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@rollup/rollup-linux-ppc64-gnu@4.60.4": { + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@rollup/rollup-linux-ppc64-musl@4.60.4": { + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@rollup/rollup-linux-riscv64-gnu@4.60.4": { + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@rollup/rollup-linux-riscv64-musl@4.60.4": { + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@rollup/rollup-linux-s390x-gnu@4.60.4": { + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@rollup/rollup-linux-x64-gnu@4.60.4": { + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@rollup/rollup-linux-x64-musl@4.60.4": { + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@rollup/rollup-openbsd-x64@4.60.4": { + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@rollup/rollup-openharmony-arm64@4.60.4": { + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "os": ["openharmony"], + "cpu": ["arm64"] + }, + "@rollup/rollup-win32-arm64-msvc@4.60.4": { + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@rollup/rollup-win32-ia32-msvc@4.60.4": { + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@rollup/rollup-win32-x64-gnu@4.60.4": { + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@rollup/rollup-win32-x64-msvc@4.60.4": { + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@types/estree@1.0.8": { + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" + }, + "@types/trusted-types@1.0.6": { + "integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==" + }, + "babel-plugin-transform-hook-names@1.0.2_@babel+core@7.29.0": { + "integrity": "sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==", + "dependencies": [ + "@babel/core" + ] + }, + "baseline-browser-mapping@2.10.31": { + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", + "bin": true + }, + "boolbase@1.0.0": { + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "browserslist@4.28.2": { + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dependencies": [ + "baseline-browser-mapping", + "caniuse-lite", + "electron-to-chromium", + "node-releases", + "update-browserslist-db" + ], + "bin": true + }, + "caniuse-lite@1.0.30001793": { + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==" + }, + "convert-source-map@2.0.0": { + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "css-select@5.2.2": { + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dependencies": [ + "boolbase", + "css-what", + "domhandler", + "domutils", + "nth-check" + ] + }, + "css-what@6.2.2": { + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==" + }, + "debug@4.4.3": { + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": [ + "ms" + ] + }, + "dom-serializer@2.0.0": { + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": [ + "domelementtype", + "domhandler", + "entities" + ] + }, + "domelementtype@2.3.0": { + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, + "domhandler@5.0.3": { + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": [ + "domelementtype" + ] + }, + "domutils@3.2.2": { + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dependencies": [ + "dom-serializer", + "domelementtype", + "domhandler" + ] + }, + "electron-to-chromium@1.5.359": { + "integrity": "sha512-8lPELWuYZIWk7NDvCNthtmMw/7Q5Wu25NpM4djFMHBmk8DubPAtL4YTOp7ou0e7HyJtwkVlWv8XMLURnrtgJQw==" + }, + "entities@4.5.0": { + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "esbuild@0.21.5": { + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "optionalDependencies": [ + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-x64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" + ], + "scripts": true, + "bin": true + }, + "escalade@3.2.0": { + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" + }, + "estree-walker@2.0.2": { + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "fsevents@2.3.3": { + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "os": ["darwin"], + "scripts": true + }, + "gensync@1.0.0-beta.2": { + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + }, + "he@1.2.0": { + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": true + }, + "js-tokens@4.0.0": { + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "jsesc@3.1.0": { + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "bin": true + }, + "json5@2.2.3": { + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": true + }, + "kolorist@1.8.0": { + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==" + }, + "lru-cache@5.1.1": { + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": [ + "yallist" + ] + }, + "magic-string@0.30.21": { + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dependencies": [ + "@jridgewell/sourcemap-codec" + ] + }, + "monaco-editor@0.53.0": { + "integrity": "sha512-0WNThgC6CMWNXXBxTbaYYcunj08iB5rnx4/G56UOPeL9UVIUGGHA1GR0EWIh9Ebabj7NpCRawQ5b0hfN1jQmYQ==", + "dependencies": [ + "@types/trusted-types" + ] + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nanoid@3.3.12": { + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "bin": true + }, + "node-html-parser@6.1.13": { + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "dependencies": [ + "css-select", + "he" + ] + }, + "node-releases@2.0.44": { + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==" + }, + "nth-check@2.1.1": { + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": [ + "boolbase" + ] + }, + "picocolors@1.1.1": { + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch@2.3.2": { + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==" + }, + "picomatch@4.0.4": { + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==" + }, + "postcss@8.5.15": { + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dependencies": [ + "nanoid", + "picocolors", + "source-map-js" + ] + }, + "preact@10.29.2": { + "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==" + }, + "rollup@4.60.4": { + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dependencies": [ + "@types/estree" + ], + "optionalDependencies": [ + "@rollup/rollup-android-arm-eabi", + "@rollup/rollup-android-arm64", + "@rollup/rollup-darwin-arm64", + "@rollup/rollup-darwin-x64", + "@rollup/rollup-freebsd-arm64", + "@rollup/rollup-freebsd-x64", + "@rollup/rollup-linux-arm-gnueabihf", + "@rollup/rollup-linux-arm-musleabihf", + "@rollup/rollup-linux-arm64-gnu", + "@rollup/rollup-linux-arm64-musl", + "@rollup/rollup-linux-loong64-gnu", + "@rollup/rollup-linux-loong64-musl", + "@rollup/rollup-linux-ppc64-gnu", + "@rollup/rollup-linux-ppc64-musl", + "@rollup/rollup-linux-riscv64-gnu", + "@rollup/rollup-linux-riscv64-musl", + "@rollup/rollup-linux-s390x-gnu", + "@rollup/rollup-linux-x64-gnu", + "@rollup/rollup-linux-x64-musl", + "@rollup/rollup-openbsd-x64", + "@rollup/rollup-openharmony-arm64", + "@rollup/rollup-win32-arm64-msvc", + "@rollup/rollup-win32-ia32-msvc", + "@rollup/rollup-win32-x64-gnu", + "@rollup/rollup-win32-x64-msvc", + "fsevents" + ], + "bin": true + }, + "semver@6.3.1": { + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": true + }, + "simple-code-frame@1.3.0": { + "integrity": "sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==", + "dependencies": [ + "kolorist" + ] + }, + "source-map-js@1.2.1": { + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "source-map@0.7.6": { + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==" + }, + "stack-trace@1.0.0": { + "integrity": "sha512-H6D7134xi6qONvh7ZHKgviXf+rd3vhGBSvebPZCaUkd8zvQ+7PtDw6CljPTe4cXWNf2IKZGNqw6VJXSb9IgBpA==" + }, + "typescript@5.9.3": { + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "bin": true + }, + "update-browserslist-db@1.2.3_browserslist@4.28.2": { + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dependencies": [ + "browserslist", + "escalade", + "picocolors" + ], + "bin": true + }, + "vite-prerender-plugin@0.5.13_vite@5.4.21": { + "integrity": "sha512-IKSpYkzDBsKAxa05naRbj7GvNVMSdww/Z/E89oO3xndz+gWnOBOKOAbEXv7qDhktY/j3vHgJmoV1pPzqU2tx9g==", + "dependencies": [ + "kolorist", + "magic-string", + "node-html-parser", + "simple-code-frame", + "source-map", + "stack-trace", + "vite" + ] + }, + "vite@5.4.21": { + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dependencies": [ + "esbuild", + "postcss", + "rollup" + ], + "optionalDependencies": [ + "fsevents" + ], + "bin": true + }, + "yallist@3.1.1": { + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "zimmerframe@1.1.4": { + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==" + } + }, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:@preact/preset-vite@2.10.5", + "npm:monaco-editor@0.53.0", + "npm:preact@10.29.2", + "npm:typescript@5.9.3", + "npm:vite@5.4.21" + ] + } + } +} diff --git a/playground/index.html b/playground/index.html new file mode 100644 index 0000000..a56d4ed --- /dev/null +++ b/playground/index.html @@ -0,0 +1,12 @@ + + + + + + Docxml Playground + + +
+ + + diff --git a/playground/package.json b/playground/package.json new file mode 100644 index 0000000..78ff10b --- /dev/null +++ b/playground/package.json @@ -0,0 +1,20 @@ +{ + "name": "docxml-playground", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "monaco-editor": "0.53.0", + "preact": "10.29.2", + "typescript": "5.9.3" + }, + "devDependencies": { + "@preact/preset-vite": "2.10.5", + "vite": "5.4.21" + } +} diff --git a/playground/scripts/generate-docxml-dts.ts b/playground/scripts/generate-docxml-dts.ts new file mode 100644 index 0000000..e1774ae --- /dev/null +++ b/playground/scripts/generate-docxml-dts.ts @@ -0,0 +1,62 @@ +/// +import ts from 'typescript'; + +const outDir = new URL('../public/docxml/', import.meta.url); +const outDtsPath = new URL('docxml.d.ts', outDir); +const entryFilePath = new URL('../../mod.ts', import.meta.url).pathname; + +function emitSingleDts(): string { + const outputPath = '/out/docxml.d.ts'; + + const options: ts.CompilerOptions = { + declaration: true, + emitDeclarationOnly: true, + module: ts.ModuleKind.System, + outFile: outputPath, + target: ts.ScriptTarget.ES2020, + moduleResolution: ts.ModuleResolutionKind.NodeNext, + allowImportingTsExtensions: true, + skipLibCheck: true, + noLib: true, + }; + + const host = ts.createCompilerHost(options); + + let outDts = ''; + host.writeFile = (fileName, text) => { + if (fileName === outputPath) { + outDts = text; + } + }; + + const program = ts.createProgram([entryFilePath], options, host); + + program.emit(); + + if (!outDts) { + throw new Error('No declaration output was generated.'); + } + + return [ + '// Generated file. Do not edit manually.', + outDts, + "declare module 'docxml' {", + " export * from 'mod';", + " export { default } from 'mod';", + '}', + '', + ].join('\n'); +} + +async function main() { + await Deno.mkdir(outDir, { recursive: true }); + + console.log('Generating single docxml.d.ts from local sources...'); + const dts = emitSingleDts(); + + await Deno.writeTextFile(outDtsPath, dts); + + console.log(`Generated ${outDtsPath.pathname} (${dts.length} chars)`); +} + +await main(); diff --git a/playground/src/App.tsx b/playground/src/App.tsx new file mode 100644 index 0000000..48f9424 --- /dev/null +++ b/playground/src/App.tsx @@ -0,0 +1,434 @@ +import 'monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution'; +import 'monaco-editor/esm/vs/editor/editor.all'; +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import 'monaco-editor/esm/vs/language/typescript/monaco.contribution'; +import { useEffect, useRef, useState } from 'preact/hooks'; +import ts from 'typescript'; +import { examples } from './examples.ts'; + +const DOCXML_RUNTIME_URL = 'https://esm.sh/jsr/@fontoxml/docxml?bundle'; +const DOCXML_TYPES_URL = new URL('../docxml/docxml.d.ts', import.meta.url); + +const INITIAL_SOURCE = examples[0]?.source ?? ''; // The "Hello world" example. + +const PERMALINK_KEY = 'code'; +const MAX_PERMALINK_HASH_LENGTH = 1500; +const MAX_PERMALINK_URL_LENGTH = 1800; +const PERMALINK_PREFIX_GZIP = 'gz:'; + +// Workers for Monaco +(self as unknown as { MonacoEnvironment: unknown }).MonacoEnvironment = { + getWorker(_workerId: string, label: string) { + if (label === 'typescript' || label === 'javascript') { + return new Worker( + new URL('./workers/ts.worker.ts', import.meta.url), + { type: 'module' } + ); + } + return new Worker( + new URL('./workers/editor.worker.ts', import.meta.url), + { type: 'module' } + ); + }, +}; + +// Polyfill Deno.cwd for docxml browser compat +if (!(globalThis as unknown as { Deno?: unknown }).Deno) { + (globalThis as unknown as { Deno: unknown }).Deno = { cwd: () => '/' }; +} + +/** + * Loads the precompiled docxml declaration bundle generated at build time + * and feeds it to Monaco's TypeScript language service. + */ +async function loadDocxmlTypes() { + const bundledDts = await fetch(DOCXML_TYPES_URL).then((r) => r.text()); + + // Configure TS compiler options in Monaco + const tsDefaults = monaco.languages.typescript.typescriptDefaults; + + tsDefaults.setCompilerOptions({ + target: monaco.languages.typescript.ScriptTarget.ES2020, + module: monaco.languages.typescript.ModuleKind.ESNext, + moduleResolution: + monaco.languages.typescript.ModuleResolutionKind.NodeJs, + allowSyntheticDefaultImports: true, + esModuleInterop: true, + allowImportingTsExtensions: true, + strict: true, + baseUrl: 'file:///', + }); + + tsDefaults.setDiagnosticsOptions({ + noSemanticValidation: false, + noSyntaxValidation: false, + }); + + tsDefaults.setEagerModelSync(true); + + tsDefaults.addExtraLib(bundledDts, 'file:///docxml/docxml-bundle.d.ts'); +} + +async function normalizeResult(result: unknown): Promise { + if (result instanceof Uint8Array) return result; + if (result instanceof ArrayBuffer) return new Uint8Array(result); + if (result instanceof Blob) + return new Uint8Array(await result.arrayBuffer()); + + const obj = result as Record; + if (typeof obj?.asUint8Array === 'function') { + return (obj as { asUint8Array: () => Uint8Array }).asUint8Array(); + } + if (typeof obj?.toArchive === 'function') { + const archive = await ( + obj as { + toArchive: () => Promise<{ asUint8Array: () => Uint8Array }>; + } + ).toArchive(); + return archive.asUint8Array(); + } + + throw new Error( + 'Result must be Docx, Archive, Uint8Array, Blob, or ArrayBuffer.' + ); +} + +function downloadBlob(blob: Blob, fileName: string) { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + a.click(); + URL.revokeObjectURL(url); +} + +function downloadDocx(data: Uint8Array, fileName: string) { + downloadBlob( + new Blob([Uint8Array.from(data)], { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }), + fileName + ); +} + +function bytesToBase64(bytes: Uint8Array): string { + let binary = ''; + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + return btoa(binary); +} + +function base64ToBytes(encoded: string): Uint8Array { + const binary = atob(encoded); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} + +async function encodePermalinkSource(source: string): Promise { + const bytes = new TextEncoder().encode(source); + const byteBuffer = bytes.buffer.slice( + bytes.byteOffset, + bytes.byteOffset + bytes.byteLength + ) as ArrayBuffer; + + if (!('CompressionStream' in globalThis)) { + throw new Error('CompressionStream unavailable'); + } + + const compressed = await new Response( + new Blob([byteBuffer]) + .stream() + .pipeThrough(new CompressionStream('gzip')) + ).arrayBuffer(); + + return PERMALINK_PREFIX_GZIP + bytesToBase64(new Uint8Array(compressed)); +} + +async function decodePermalinkSource(encoded: string): Promise { + if (encoded.startsWith(PERMALINK_PREFIX_GZIP)) { + const compressed = base64ToBytes( + encoded.slice(PERMALINK_PREFIX_GZIP.length) + ); + const compressedBuffer = compressed.buffer.slice( + compressed.byteOffset, + compressed.byteOffset + compressed.byteLength + ) as ArrayBuffer; + + if (!('DecompressionStream' in globalThis)) { + throw new Error('DecompressionStream unavailable'); + } + + const uncompressed = await new Response( + new Blob([compressedBuffer]) + .stream() + .pipeThrough(new DecompressionStream('gzip')) + ).arrayBuffer(); + + return new TextDecoder().decode(uncompressed); + } + + return new TextDecoder().decode(base64ToBytes(encoded)); +} + +async function readPermalinkSource(): Promise { + const hash = globalThis.location.hash.startsWith('#') + ? globalThis.location.hash.slice(1) + : globalThis.location.hash; + if (!hash) return null; + + const params = new URLSearchParams(hash); + const encoded = params.get(PERMALINK_KEY); + if (!encoded) return null; + + try { + return await decodePermalinkSource(encoded); + } catch { + return null; + } +} + +async function writePermalinkSource( + source: string +): Promise<{ ok: true } | { ok: false; reason: string }> { + const url = new URL(globalThis.location.href); + + if (source === INITIAL_SOURCE || source.trim() === '') { + url.hash = ''; + history.replaceState(null, '', url); + return { ok: true }; + } else { + const params = new URLSearchParams(url.hash.slice(1)); + let encodedSource = ''; + try { + encodedSource = await encodePermalinkSource(source); + } catch { + return { + ok: false, + reason: 'This browser does not support permalink compression.', + }; + } + + params.set(PERMALINK_KEY, encodedSource); + const nextHash = params.toString(); + if (nextHash.length > MAX_PERMALINK_HASH_LENGTH) { + return { + ok: false, + reason: 'Code is too long for a safe permalink URL, even compressed.', + }; + } + url.hash = nextHash; + + if (url.toString().length > MAX_PERMALINK_URL_LENGTH) { + return { + ok: false, + reason: 'Code is too long for a safe permalink URL, even compressed.', + }; + } + } + + history.replaceState(null, '', url); + return { ok: true }; +} + +export function App() { + const editorRef = useRef(null); + const instanceRef = useRef( + null + ); + const [status, setStatus] = useState('Loading types...'); + const [hasError, setHasError] = useState(false); + const [isGenerating, setIsGenerating] = useState(false); + + useEffect(() => { + if (!editorRef.current) return; + + const model = monaco.editor.createModel( + INITIAL_SOURCE, + 'typescript', + monaco.Uri.parse('file:///playground/main.ts') + ); + + instanceRef.current = monaco.editor.create(editorRef.current, { + model, + theme: 'vs', + automaticLayout: true, + minimap: { enabled: false }, + fontSize: 13, + }); + + loadDocxmlTypes() + .then(() => { + setStatus('Ready.'); + setHasError(false); + }) + .catch((err) => { + setStatus( + `Types failed: ${err instanceof Error ? err.message : err}` + ); + setHasError(true); + }); + + void readPermalinkSource().then((permalinkSource) => { + if ( + permalinkSource != null && + model.getValue() === INITIAL_SOURCE + ) { + model.setValue(permalinkSource); + } + }); + + return () => { + model.dispose(); + instanceRef.current?.dispose(); + }; + }, []); + + async function generateDocx() { + const editor = instanceRef.current; + if (!editor) return; + + setIsGenerating(true); + setHasError(false); + setStatus('Compiling...'); + + try { + const source = editor.getValue(); + const jsSource = ts + .transpileModule(source, { + compilerOptions: { + target: ts.ScriptTarget.ES2020, + module: ts.ModuleKind.ES2022, + strict: true, + }, + }) + .outputText.replaceAll( + `from 'docxml'`, + `from '${DOCXML_RUNTIME_URL}'` + ) + .replaceAll(`from "docxml"`, `from "${DOCXML_RUNTIME_URL}"`); + + const blob = new Blob([jsSource], { type: 'text/javascript' }); + const url = URL.createObjectURL(blob); + + try { + const mod = await import(/* @vite-ignore */ url); + if (typeof mod.default !== 'function') { + throw new Error('Module must export a default function.'); + } + setStatus('Generating DOCX...'); + const result = await mod.default(); + const data = await normalizeResult(result); + downloadDocx(data, 'playground-output.docx'); + setStatus(`Done (${data.byteLength} bytes).`); + setHasError(false); + } finally { + URL.revokeObjectURL(url); + } + } catch (err) { + setStatus(err instanceof Error ? err.message : String(err)); + setHasError(true); + } finally { + setIsGenerating(false); + } + } + + function loadExample(id: string) { + const example = examples.find((e) => e.id === id); + if (example && instanceRef.current) { + instanceRef.current.getModel()?.setValue(example.source); + } + } + + return ( +
+
+

Docxml Playground

+ +
+ +
+ +
+

{status}

+ + + +
+ + +
+ ); +} diff --git a/playground/src/examples.ts b/playground/src/examples.ts new file mode 100644 index 0000000..8544159 --- /dev/null +++ b/playground/src/examples.ts @@ -0,0 +1,434 @@ +export type Example = { id: string; label: string; source: string }; + +export const examples: Example[] = [ + { + id: 'hello', + label: 'Hello World', + source: `import Docx, { Paragraph, Text } from 'docxml'; + +export default function build() { + const docx = Docx.fromNothing(); + + docx.document.set( + new Paragraph({}, new Text({}, 'Hello from the docxml playground.')) + ); + + return docx; +} +`, + }, + { + id: 'styled-text', + label: 'Styled Text', + source: `import Docx, { Paragraph, Text, pt } from 'docxml'; + +export default function build() { + const docx = Docx.fromNothing(); + + docx.document.set( + new Paragraph({ alignment: 'center' }, + new Text({ isBold: true, fontSize: pt(28) }, 'Bold & Centered'), + new Text({}, ' \\u2014 '), + new Text({ isItalic: true, color: '0066CC' }, 'Italic & Blue') + ) + ); + + return docx; +} +`, + }, + { + id: 'multiple-paragraphs', + label: 'Multiple Paragraphs', + source: `import Docx, { Paragraph, Text, pt } from 'docxml'; + +export default function build() { + const docx = Docx.fromNothing(); + + docx.document.set([ + new Paragraph({}, new Text({ isBold: true, fontSize: pt(24) }, 'Document Title')), + new Paragraph({}, new Text({}, 'First paragraph of content.')), + new Paragraph({}, new Text({}, 'Second paragraph of content.')), + new Paragraph({}, + new Text({}, 'Mixed: '), + new Text({ isBold: true }, 'bold'), + new Text({}, ', '), + new Text({ isItalic: true }, 'italic'), + new Text({}, ', and '), + new Text({ isBold: true, isItalic: true }, 'both'), + new Text({}, '.') + ), + ]); + + return docx; +} +`, + }, + { + id: 'table', + label: 'Table', + source: `import Docx, { Table, Row, Cell, Paragraph, Text, cm, pt } from 'docxml'; + +export default function build() { + const docx = Docx.fromNothing(); + + docx.document.set( + new Table( + { + columnWidths: [cm(3), cm(2.5), cm(2.5), cm(2.5), cm(2.5)], + cellPadding: { top: pt(6), bottom: pt(6), start: pt(6), end: pt(6) }, + borders: { + top: { type: 'single', width: pt(1), color: '666666' }, + bottom: { type: 'single', width: pt(1), color: '666666' }, + start: { type: 'single', width: pt(1), color: '666666' }, + end: { type: 'single', width: pt(1), color: '666666' }, + insideH: { type: 'dashed', width: pt(1), color: 'CCCCCC' }, + insideV: { type: 'dashed', width: pt(1), color: 'CCCCCC' }, + }, + }, + new Row({ isHeaderRow: true }, + new Cell({}, new Paragraph({}, new Text({ isBold: true }, 'Year'))), + new Cell({}, new Paragraph({}, new Text({ isBold: true }, 'Q1'))), + new Cell({}, new Paragraph({}, new Text({ isBold: true }, 'Q2'))), + new Cell({}, new Paragraph({}, new Text({ isBold: true }, 'Q3'))), + new Cell({}, new Paragraph({}, new Text({ isBold: true }, 'Q4'))) + ), + new Row({}, + new Cell({}, new Paragraph({}, new Text({}, '2023'))), + new Cell({}, new Paragraph({}, new Text({}, '120'))), + new Cell({}, new Paragraph({}, new Text({}, '145'))), + new Cell({}, new Paragraph({}, new Text({}, '132'))), + new Cell({}, new Paragraph({}, new Text({}, '158'))) + ), + new Row({}, + new Cell({}, new Paragraph({}, new Text({}, '2024'))), + new Cell({}, new Paragraph({}, new Text({}, '165'))), + new Cell({}, new Paragraph({}, new Text({}, '170'))), + new Cell({}, new Paragraph({}, new Text({}, '155'))), + new Cell({}, new Paragraph({}, new Text({}, '190'))) + ) + ) + ); + + return docx; +} +`, + }, + { + id: 'sections', + label: 'Sections & Page Sizes', + source: `import Docx, { Section, Paragraph, Text, cm } from 'docxml'; + +export default function build() { + const docx = Docx.fromNothing(); + + docx.document.set([ + new Section({ pageWidth: cm(21), pageHeight: cm(29.7) }, + new Paragraph({}, new Text({}, 'This is an A4 portrait page.')) + ), + new Section({ pageWidth: cm(29.7), pageHeight: cm(21), pageOrientation: 'landscape' }, + new Paragraph({}, new Text({}, 'This is an A4 landscape page.')) + ), + new Section({ pageWidth: cm(20), pageHeight: cm(20) }, + new Paragraph({}, new Text({}, 'This is a square page.')) + ), + ]); + + return docx; +} +`, + }, + { + id: 'hyperlinks', + label: 'Hyperlinks & Bookmarks', + source: `import Docx, { BookmarkRangeStart, BookmarkRangeEnd, Hyperlink, Paragraph, Section, Text } from 'docxml'; + +export default function build() { + const docx = Docx.fromNothing(); + + const bookmark = docx.bookmarks.create(); + + docx.document.set([ + new Section({}, + new Paragraph({}, + new Hyperlink({ bookmark }, + new Text({ color: '0563C1', isUnderlined: true }, 'Jump to bookmarked section') + ) + ) + ), + new Section({}, + new BookmarkRangeStart({ bookmark }), + new Paragraph({}, + new Hyperlink({ url: 'https://github.com/fontoxml/docxml' }, + new Text({ color: '0563C1', isUnderlined: true }, 'Visit docxml on GitHub') + ) + ), + new BookmarkRangeEnd({ bookmark }) + ), + ]); + + return docx; +} +`, + }, + { + id: 'comments', + label: 'Comments', + source: `import Docx, { Comment, CommentRangeStart, CommentRangeEnd, Paragraph, Text } from 'docxml'; + +export default function build() { + const docx = Docx.fromNothing(); + + docx.document.styles.add({ + id: 'CommentReference', + type: 'character', + paragraph: {}, + }); + + const comment = docx.document.comments.add( + { author: 'Reviewer', date: new Date(), initials: 'R' }, + [new Paragraph({}, new Text({}, 'This needs revision.'))] + ); + + const reply = docx.document.comments.add( + { author: 'Author', date: new Date(), parentId: comment }, + [new Paragraph({}, new Text({}, 'I agree, will fix.'))] + ); + + docx.document.set( + new Paragraph({}, + new Text({}, 'NSYNC is the '), + new CommentRangeStart({ id: comment }), + new CommentRangeStart({ id: reply }), + new Text({}, 'greatest'), + new Comment({ id: comment }), + new Comment({ id: reply }), + new CommentRangeEnd({ id: comment }), + new CommentRangeEnd({ id: reply }), + new Text({}, ' band in history.') + ) + ); + + return docx; +} +`, + }, + { + id: 'fields', + label: 'Fields (Hyperlink)', + source: `import Docx, { FieldDefinition, FieldNames, FieldRangeEnd, FieldRangeInstruction, FieldRangeSeparator, FieldRangeStart, Paragraph, Section, Text } from 'docxml'; + +export default function build() { + const docx = Docx.fromNothing(); + + docx.document.set( + new Section({}, + new Paragraph({}, + new FieldRangeStart({}), + new Text({}, + new FieldRangeInstruction({}, + new FieldDefinition({ name: FieldNames.HYPERLINK, value: 'http://www.google.com' }) + ), + new FieldRangeSeparator({}), + 'Click here to visit Google' + ), + new FieldRangeEnd({}) + ) + ) + ); + + return docx; +} +`, + }, + { + id: 'headers-footers', + label: 'Headers & Footers', + source: `import Docx, { Paragraph, Section, Text } from 'docxml'; + +export default function build() { + const docx = Docx.fromNothing(); + + const header = docx.document.headers.add('word/header1.xml', [ + new Paragraph({}, new Text({ isBold: true }, 'Document Header')), + ]); + + const footer = docx.document.footers.add( + 'word/footer1.xml', + new Paragraph({}, new Text({ color: '888888' }, 'Page footer — docxml playground')) + ); + + docx.document.set( + new Section({ headers: header, footers: footer }, + new Paragraph({}, new Text({}, 'This page has a header and a footer.')), + new Paragraph({}, new Text({}, 'Check the top and bottom of the page in Word.')) + ) + ); + + return docx; +} +`, + }, + { + id: 'protected', + label: 'Protected Document', + source: `import Docx, { Paragraph, Section, Text, cm } from 'docxml'; + +export default function build() { + const docx = Docx.fromNothing(); + + docx.document.settings.set('documentProtection', { + edit: 'readOnly', + enforcement: true, + }); + + docx.document.set( + new Section({ pageWidth: cm(21), pageHeight: cm(29.7) }, + new Paragraph({}, new Text({}, 'This document is read-only protected.')), + new Paragraph({}, new Text({}, 'You cannot edit this in Word without removing protection.')) + ) + ); + + return docx; +} +`, + }, + { + id: 'track-changes-insertion', + label: 'Track Changes: Insertion', + source: `import Docx, { Insertion, Paragraph, Section, Text } from 'docxml'; + +export default function build() { + const docx = Docx.fromNothing(); + + const date = new Date(); + const author = 'Editor'; + + docx.document.set( + new Section({}, + new Paragraph( + { pilcrow: { insertion: { id: 1, author, date } } }, + new Insertion({ id: 2, author, date }, + new Text({}, 'This entire paragraph was inserted.') + ) + ), + new Paragraph({}, + new Text({}, 'This text was already here. '), + new Insertion({ id: 3, author, date }, + new Text({}, 'This part was added later.') + ) + ) + ) + ); + + return docx; +} +`, + }, + { + id: 'track-changes-deletion', + label: 'Track Changes: Deletion', + source: `import Docx, { DeletedText, Deletion, Paragraph, Section, Text } from 'docxml'; + +export default function build() { + const docx = Docx.fromNothing(); + + const date = new Date(); + const author = 'Editor'; + + docx.document.set( + new Section({}, + new Paragraph( + { pilcrow: { deletion: { id: 1, author, date } } }, + new Deletion({ id: 2, author, date }, + new DeletedText({}, 'This paragraph was deleted.') + ) + ), + new Paragraph({}, + new Text({}, 'Existing text. '), + new Deletion({ id: 3, author, date }, + new DeletedText({}, 'This part was removed.') + ) + ) + ) + ); + + return docx; +} +`, + }, + { + id: 'track-changes-formatting', + label: 'Track Changes: Formatting', + source: `import Docx, { Paragraph, Section, Text } from 'docxml'; + +export default function build() { + const docx = Docx.fromNothing(); + + docx.document.set( + new Section({}, + new Paragraph({}, + new Text( + { + isBold: true, + change: { author: 'Editor', id: 1, date: new Date() }, + }, + 'This text was changed to bold.' + ) + ), + new Paragraph({}, + new Text( + { + isItalic: true, + change: { author: 'Editor', id: 2, date: new Date(), isBold: true }, + }, + 'This was bold, changed to italic only.' + ) + ) + ) + ); + + return docx; +} +`, + }, + { + id: 'track-changes-move', + label: 'Track Changes: Move', + source: `import Docx, { MoveFrom, MoveFromRangeStart, MoveFromRangeEnd, MoveTo, MoveToRangeStart, MoveToRangeEnd, Paragraph, Section, Text } from 'docxml'; + +export default function build() { + const docx = Docx.fromNothing(); + + const date = new Date(); + const author = 'Editor'; + + docx.document.set( + new Section({}, + new Paragraph( + { pilcrow: { moveFrom: { id: 0, author, date } } }, + new MoveFromRangeStart({ id: 1, name: 'move_0', author, date }), + new MoveFrom({ id: 2, author, date }, + new Text({}, 'This paragraph was moved from here.') + ), + new MoveFromRangeEnd({ id: 1 }) + ), + new Paragraph({}, + new Text({}, 'This paragraph stays in place.') + ), + new Paragraph( + { pilcrow: { moveTo: { id: 3, author, date } } }, + new MoveToRangeStart({ id: 4, name: 'move_0', author, date }), + new MoveTo({ id: 5, author, date }, + new Text({}, 'This paragraph was moved from here.') + ), + new MoveToRangeEnd({ id: 4 }) + ) + ) + ); + + return docx; +} +`, + }, +]; diff --git a/playground/src/globals.d.ts b/playground/src/globals.d.ts new file mode 100644 index 0000000..ba02fb8 --- /dev/null +++ b/playground/src/globals.d.ts @@ -0,0 +1 @@ +declare const __DOCXML_VERSION__: string; diff --git a/playground/src/main.tsx b/playground/src/main.tsx new file mode 100644 index 0000000..e9c94bc --- /dev/null +++ b/playground/src/main.tsx @@ -0,0 +1,11 @@ +import { render } from 'preact'; +import { App } from './App.tsx'; +import './style.css'; + +const root = document.querySelector('#app'); + +if (!(root instanceof HTMLElement)) { + throw new Error('Could not find #app root.'); +} + +render(, root); diff --git a/playground/src/style.css b/playground/src/style.css new file mode 100644 index 0000000..5b02b1c --- /dev/null +++ b/playground/src/style.css @@ -0,0 +1,170 @@ +:root { + --bg: #f4f5f2; + --surface: #ffffff; + --text: #111827; + --muted: #4b5563; + --line: #d1d5db; + --accent: #0f766e; + --danger: #b91c1c; +} + +* { + box-sizing: border-box; +} + +html, +body, +#app { + margin: 0; + height: 100%; + font-family: 'IBM Plex Sans', system-ui, sans-serif; + background: var(--bg); + color: var(--text); +} + +#app { + padding: 1rem; + height: 100%; +} + +.layout { + width: 100%; + max-width: 1400px; + height: 100%; + margin: 0 auto; + display: grid; + grid-template-rows: auto 1fr auto auto; + gap: 0.75rem; +} + +.header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1.2rem; + flex-wrap: wrap; + padding: 1rem 1.1rem; + border: 1px solid var(--line); + border-radius: 14px; + background: + radial-gradient( + circle at top right, + #c8f3ea 0%, + rgba(200, 243, 234, 0) 55% + ), + linear-gradient(135deg, #ffffff 0%, #f7faf9 100%); + box-shadow: 0 8px 24px rgba(15, 118, 110, 0.08); +} + +.headerTitle { + margin: 0; + font-size: 2rem; + line-height: 1.05; + color: var(--accent); +} + +.exampleSelect { + border: 1px solid var(--line); + border-radius: 8px; + padding: 0.4rem 0.6rem; + font: inherit; + font-size: 0.9rem; + background: var(--surface); + color: var(--text); + margin-left: auto; +} + +.editor { + min-height: 300px; + border: 1px solid var(--line); + background: var(--surface); + overflow: hidden; +} + +.statusRow { + display: flex; + align-items: center; + gap: 0.65rem; +} + +button { + border: 1px solid var(--accent); + border-radius: 10px; + padding: 0.5rem 0.9rem; + font: inherit; + background: var(--accent); + color: #fff; + cursor: pointer; + font-weight: 600; +} + +button:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +button.secondary { + background: var(--surface); + color: var(--accent); +} + +.status { + margin: 0; + flex: 1; + padding: 0.65rem 0.75rem; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--surface); + font-family: ui-monospace, Menlo, Monaco, monospace; + font-size: 0.85rem; + color: var(--muted); +} + +.status.error { + color: var(--danger); +} + +.footer { + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0; + font-size: 0.8rem; + color: var(--muted); +} + +.footer a { + color: var(--accent); + text-decoration: none; +} + +.footer a:hover { + text-decoration: underline; +} + +.footerVersion { + font-family: ui-monospace, Menlo, Monaco, monospace; + color: var(--muted); + opacity: 0.7; +} + +@media (max-width: 700px) { + .header { + padding: 0.85rem; + } + + .headerTitle { + font-size: 1.65rem; + } + + .exampleSelect { + width: 100%; + margin-left: 0; + } + + .statusRow { + flex-direction: column; + align-items: stretch; + } +} diff --git a/playground/src/workers/editor.worker.ts b/playground/src/workers/editor.worker.ts new file mode 100644 index 0000000..dc3c5fb --- /dev/null +++ b/playground/src/workers/editor.worker.ts @@ -0,0 +1 @@ +import 'monaco-editor/esm/vs/editor/editor.worker'; diff --git a/playground/src/workers/ts.worker.ts b/playground/src/workers/ts.worker.ts new file mode 100644 index 0000000..bed56fc --- /dev/null +++ b/playground/src/workers/ts.worker.ts @@ -0,0 +1 @@ +import 'monaco-editor/esm/vs/language/typescript/ts.worker'; diff --git a/playground/vite.config.ts b/playground/vite.config.ts new file mode 100644 index 0000000..af6fb6b --- /dev/null +++ b/playground/vite.config.ts @@ -0,0 +1,23 @@ +import preact from '@preact/preset-vite'; +import { readFileSync } from 'node:fs'; +import { defineConfig } from 'vite'; + +function getVersion(): string { + try { + const denoJsonPath = new URL('../deno.json', import.meta.url); + const denoJson = JSON.parse(readFileSync(denoJsonPath, 'utf8')) as { + version?: string; + }; + return denoJson.version || 'dev'; + } catch { + return 'dev'; + } +} + +export default defineConfig({ + base: './', + plugins: [preact()], + define: { + __DOCXML_VERSION__: JSON.stringify(getVersion()), + }, +});