diff --git a/.github/workflows/playground-pages.yml b/.github/workflows/playground-pages.yml
new file mode 100644
index 0000000..6c15d23
--- /dev/null
+++ b/.github/workflows/playground-pages.yml
@@ -0,0 +1,57 @@
+name: Playground Pages
+
+on:
+ workflow_run:
+ workflows:
+ - Publish to JSR
+ types:
+ - completed
+ workflow_dispatch:
+ inputs:
+ ref:
+ description: Git ref to deploy (branch, tag, or commit SHA)
+ required: false
+ type: string
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: true
+
+jobs:
+ deploy:
+ # Skip when the publish failed: the pinned docxml version would not exist on JSR.
+ if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
+ 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.event.workflow_run.head_sha || github.ref }}
+
+ - name: Setup Deno
+ uses: denoland/setup-deno@v2
+ with:
+ deno-version: 2.9.5
+
+ - 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 dab5248..ba1dbcc 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 07045d9..ab2e1d5 100644
--- a/deno.lock
+++ b/deno.lock
@@ -16,7 +16,8 @@
"npm:eslint@10.9.0": "10.9.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": {
@@ -483,6 +484,10 @@
"prelude-ls"
]
},
+ "typescript@6.0.3": {
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "bin": true
+ },
"uri-js@4.4.1": {
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dependencies": [
@@ -524,7 +529,8 @@
"npm:eslint@10.9.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 db3f215..2350442 100644
--- a/imports.json
+++ b/imports.json
@@ -15,7 +15,7 @@
"std/path/posix/relative": "jsr:@std/path@1.1.6/posix/relative",
"std/path/posix/resolve": "jsr:@std/path@1.1.6/posix/resolve",
"std/testing/bdd": "jsr:@std/testing@1.0.20/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..7bdc72c
--- /dev/null
+++ b/playground/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "docxml-playground",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "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..bb2aca4
--- /dev/null
+++ b/playground/scripts/generate-docxml-dts.ts
@@ -0,0 +1,76 @@
+///
+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);
+
+ // Semantic checks are meaningless here: `noLib` hides the globals and bare
+ // specifiers are resolved by Deno's import map, not by tsc.
+ const errors = program.getSyntacticDiagnostics();
+
+ if (errors.length > 0) {
+ throw new Error(
+ errors
+ .map((d) =>
+ ts.flattenDiagnosticMessageText(d.messageText, '\n')
+ )
+ .join('\n')
+ );
+ }
+
+ 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..121eb5f
--- /dev/null
+++ b/playground/src/App.tsx
@@ -0,0 +1,314 @@
+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';
+import { readPermalink, writePermalink } from './permalink.ts';
+
+// Pinned so the executed code matches the types shipped with this build.
+const DOCXML_RUNTIME_URL = `https://esm.sh/jsr/@fontoxml/docxml@${__DOCXML_VERSION__}?bundle`;
+const DOCXML_TYPES_URL = new URL('../docxml/docxml.d.ts', import.meta.url);
+
+// Matches the module specifier of `from 'docxml'`, `import('docxml')` and friends.
+const DOCXML_SPECIFIER = /(\bfrom\s*|\bimport\s*\(\s*)(['"])docxml\2/g;
+
+const INITIAL_SOURCE = examples[0]?.source ?? ''; // The "Hello world" example.
+const MODEL_URI = monaco.Uri.parse('file:///playground/main.ts');
+
+// 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' }
+ );
+ },
+};
+
+// docxml reads Deno.cwd() even in the browser.
+if (!(globalThis as unknown as { Deno?: unknown }).Deno) {
+ (globalThis as unknown as { Deno: unknown }).Deno = { cwd: () => '/' };
+}
+
+async function fetchText(url: URL | string): Promise {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`${response.status} ${response.statusText}`);
+ }
+ return response.text();
+}
+
+/** Feeds the declaration bundle generated at build time to Monaco. */
+async function loadDocxmlTypes() {
+ const bundledDts = await fetchText(DOCXML_TYPES_URL);
+ 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.setEagerModelSync(true);
+ tsDefaults.addExtraLib(bundledDts, 'file:///docxml/docxml-bundle.d.ts');
+}
+
+/** Returns the messages Monaco reports for the editor contents. */
+async function getDiagnostics(): Promise {
+ const worker = await (
+ await monaco.languages.typescript.getTypeScriptWorker()
+ )(MODEL_URI);
+ const uri = MODEL_URI.toString();
+
+ const diagnostics = [
+ ...(await worker.getSyntacticDiagnostics(uri)),
+ ...(await worker.getSemanticDiagnostics(uri)),
+ ];
+
+ return diagnostics.map((diagnostic) =>
+ typeof diagnostic.messageText === 'string'
+ ? diagnostic.messageText
+ : diagnostic.messageText.messageText
+ );
+}
+
+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 {
+ asUint8Array?: () => Uint8Array;
+ toArchive?: () => Promise<{ asUint8Array: () => Uint8Array }>;
+ };
+ if (typeof obj?.asUint8Array === 'function') return obj.asUint8Array();
+ if (typeof obj?.toArchive === 'function') {
+ return (await obj.toArchive()).asUint8Array();
+ }
+
+ throw new Error(
+ 'Result must be Docx, Archive, Uint8Array, Blob, or ArrayBuffer.'
+ );
+}
+
+function download(blob: Blob, fileName: string) {
+ const url = URL.createObjectURL(blob);
+ const anchor = document.createElement('a');
+ anchor.href = url;
+ anchor.download = fileName;
+ document.body.append(anchor);
+ anchor.click();
+ anchor.remove();
+ // Safari aborts the download when the URL is revoked synchronously.
+ setTimeout(() => URL.revokeObjectURL(url));
+}
+
+function messageOf(err: unknown): string {
+ return err instanceof Error ? err.message : String(err);
+}
+
+export function App() {
+ const editorRef = useRef(null);
+ const instanceRef = useRef(
+ null
+ );
+ const [status, setStatus] = useState('Loading types...');
+ const [hasError, setHasError] = useState(false);
+ const [isBusy, setIsBusy] = useState(false);
+
+ function report(message: string, isFailure = false) {
+ setStatus(message);
+ setHasError(isFailure);
+ }
+
+ useEffect(() => {
+ if (!editorRef.current) return;
+
+ const model = monaco.editor.createModel(
+ INITIAL_SOURCE,
+ 'typescript',
+ MODEL_URI
+ );
+ const editor = monaco.editor.create(editorRef.current, {
+ model,
+ theme: 'vs',
+ automaticLayout: true,
+ minimap: { enabled: false },
+ fontSize: 13,
+ });
+ instanceRef.current = editor;
+
+ let disposed = false;
+ (async () => {
+ try {
+ await loadDocxmlTypes();
+ const shared = await readPermalink();
+ if (disposed) return;
+ if (shared) model.setValue(shared);
+ report('Ready.');
+ } catch (err) {
+ if (!disposed) report(`Types failed: ${messageOf(err)}`, true);
+ }
+ })();
+
+ return () => {
+ disposed = true;
+ model.dispose();
+ editor.dispose();
+ instanceRef.current = null;
+ };
+ }, []);
+
+ async function generateDocx() {
+ const source = instanceRef.current?.getValue();
+ if (!source) return;
+
+ setIsBusy(true);
+ report('Compiling...');
+
+ try {
+ const [firstError] = await getDiagnostics();
+ if (firstError) throw new Error(firstError);
+
+ const jsSource = ts
+ .transpileModule(source, {
+ compilerOptions: {
+ target: ts.ScriptTarget.ES2020,
+ module: ts.ModuleKind.ES2022,
+ },
+ })
+ .outputText.replace(
+ DOCXML_SPECIFIER,
+ `$1$2${DOCXML_RUNTIME_URL}$2`
+ );
+
+ const url = URL.createObjectURL(
+ new Blob([jsSource], { type: 'text/javascript' })
+ );
+
+ try {
+ const mod = await import(/* @vite-ignore */ url);
+ if (typeof mod.default !== 'function') {
+ throw new Error('Module must export a default function.');
+ }
+ report('Generating DOCX...');
+ const data = await normalizeResult(await mod.default());
+ download(
+ new Blob([Uint8Array.from(data)], {
+ type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ }),
+ 'playground-output.docx'
+ );
+ report(`Done (${data.byteLength} bytes).`);
+ } finally {
+ URL.revokeObjectURL(url);
+ }
+ } catch (err) {
+ report(messageOf(err), true);
+ } finally {
+ setIsBusy(false);
+ }
+ }
+
+ async function copyPermalink() {
+ const source = instanceRef.current?.getValue();
+ if (!source) return;
+
+ try {
+ await navigator.clipboard.writeText(await writePermalink(source));
+ report('Permalink copied.');
+ } catch (err) {
+ report(messageOf(err), true);
+ }
+ }
+
+ function loadExample(id: string) {
+ const example = examples.find((entry) => entry.id === id);
+ if (example) instanceRef.current?.setValue(example.source);
+ }
+
+ return (
+
+
+
+
+
+
+
{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/permalink.ts b/playground/src/permalink.ts
new file mode 100644
index 0000000..d2a71bb
--- /dev/null
+++ b/playground/src/permalink.ts
@@ -0,0 +1,89 @@
+const PERMALINK_KEY = 'code';
+const PERMALINK_PREFIX_GZIP = 'gz:';
+
+// Stays well below the ~2000 character URL length that every browser and proxy handles.
+const MAX_PERMALINK_URL_LENGTH = 1800;
+
+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;
+}
+
+function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
+ return bytes.buffer.slice(
+ bytes.byteOffset,
+ bytes.byteOffset + bytes.byteLength
+ ) as ArrayBuffer;
+}
+
+async function pipe(bytes: Uint8Array, stream: ReadableWritablePair) {
+ const piped = new Blob([toArrayBuffer(bytes)]).stream().pipeThrough(stream);
+ return new Uint8Array(await new Response(piped).arrayBuffer());
+}
+
+async function encode(source: string): Promise {
+ const compressed = await pipe(
+ new TextEncoder().encode(source),
+ new CompressionStream('gzip')
+ );
+ return PERMALINK_PREFIX_GZIP + bytesToBase64(compressed);
+}
+
+async function decode(encoded: string): Promise {
+ if (!encoded.startsWith(PERMALINK_PREFIX_GZIP)) {
+ throw new Error('Unsupported permalink format.');
+ }
+ const uncompressed = await pipe(
+ base64ToBytes(encoded.slice(PERMALINK_PREFIX_GZIP.length)),
+ new DecompressionStream('gzip')
+ );
+ return new TextDecoder().decode(uncompressed);
+}
+
+/** Reads the source stored in the current URL hash, or null when there is none. */
+export async function readPermalink(): Promise {
+ const encoded = new URLSearchParams(
+ globalThis.location.hash.replace(/^#/, '')
+ ).get(PERMALINK_KEY);
+
+ if (!encoded) return null;
+
+ try {
+ return await decode(encoded);
+ } catch {
+ return null;
+ }
+}
+
+/** Stores the source in the URL hash and returns the shareable URL. */
+export async function writePermalink(source: string): Promise {
+ if (!('CompressionStream' in globalThis)) {
+ throw new Error('This browser does not support permalinks.');
+ }
+
+ const url = new URL(globalThis.location.href);
+ url.hash = new URLSearchParams({
+ [PERMALINK_KEY]: await encode(source),
+ }).toString();
+
+ if (url.href.length > MAX_PERMALINK_URL_LENGTH) {
+ throw new Error(
+ 'Code is too long to share as a link, even compressed.'
+ );
+ }
+
+ history.replaceState(null, '', url);
+ return url.href;
+}
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()),
+ },
+});