From 2766f72959c59df7f9ca90301e209d82c543a32d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20G=C3=B3mez?= Date: Tue, 19 May 2026 12:11:00 +0200 Subject: [PATCH 01/23] playground --- .github/workflows/playground-pages.yml | 47 ++ deno.json | 7 +- imports.json | 1 + playground/.gitignore | 2 + playground/README.md | 30 + playground/deno.json | 10 + playground/deno.lock | 807 +++++++++++++++++++++++++ playground/index.html | 12 + playground/package.json | 20 + playground/src/App.tsx | 603 ++++++++++++++++++ playground/src/examples.ts | 26 + playground/src/main.tsx | 11 + playground/src/style.css | 158 +++++ playground/src/vite-workers.d.ts | 13 + playground/tsconfig.json | 13 + playground/vite.config.ts | 7 + 16 files changed, 1765 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/playground-pages.yml create mode 100644 playground/.gitignore create mode 100644 playground/README.md create mode 100644 playground/deno.json create mode 100644 playground/deno.lock create mode 100644 playground/index.html create mode 100644 playground/package.json create mode 100644 playground/src/App.tsx create mode 100644 playground/src/examples.ts create mode 100644 playground/src/main.tsx create mode 100644 playground/src/style.css create mode 100644 playground/src/vite-workers.d.ts create mode 100644 playground/tsconfig.json create mode 100644 playground/vite.config.ts diff --git a/.github/workflows/playground-pages.yml b/.github/workflows/playground-pages.yml new file mode 100644 index 0000000..1c03c2b --- /dev/null +++ b/.github/workflows/playground-pages.yml @@ -0,0 +1,47 @@ +name: Playground Pages + +on: + 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@v4 + + - name: Setup Deno + uses: denoland/setup-deno@v1 + with: + deno-version: v2.x + + - name: Build playground + run: deno task playground:build + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload playground artifact + uses: actions/upload-pages-artifact@v3 + with: + path: playground/dist + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/deno.json b/deno.json index 080ec9f..7714447 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 run -A --node-modules-dir=auto npm:vite", + "playground:build": "cd playground && deno run -A --node-modules-dir=auto npm:vite build", + "playground:preview": "cd playground && deno run -A --node-modules-dir=auto npm:vite preview --host --port 4173", "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/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..763301f --- /dev/null +++ b/playground/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ \ No newline at end of file diff --git a/playground/README.md b/playground/README.md new file mode 100644 index 0000000..f91e6b0 --- /dev/null +++ b/playground/README.md @@ -0,0 +1,30 @@ +# Docxml Playground + +Vite + Preact playground to try `docxml` in the browser. + +The app bundles Monaco statically at build time (including workers), and still loads the latest published `docxml` release from JSR at runtime. +Type information for Monaco is loaded from the latest JSR source files in-memory and is not written to disk. + +## Run locally with Deno + +From the repository root: + +```sh +deno task playground:dev +``` + +Then open `http://localhost:5173`. + +## Build static output + +From the repository root: + +```sh +deno task playground:build +``` + +The generated static site is written to `playground/dist`. + +## Deploy + +The `.github/workflows/playground-pages.yml` workflow builds the playground and publishes `playground/dist` to GitHub Pages whenever a new release is published. diff --git a/playground/deno.json b/playground/deno.json new file mode 100644 index 0000000..8e6bcd0 --- /dev/null +++ b/playground/deno.json @@ -0,0 +1,10 @@ +{ + "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..f19d2e2 --- /dev/null +++ b/playground/deno.lock @@ -0,0 +1,807 @@ +{ + "version": "5", + "specifiers": { + "npm:@preact/preset-vite@^2.10.1": "2.10.5_@babel+core@7.29.0_vite@5.4.21_preact@10.29.2", + "npm:monaco-editor@0.53": "0.53.0", + "npm:preact@^10.26.9": "10.29.2", + "npm:typescript@^5.8.3": "5.9.3", + "npm:vite@*": "5.4.21", + "npm:vite@^5.4.10": "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.1", + "npm:monaco-editor@0.53", + "npm:preact@^10.26.9", + "npm:typescript@^5.8.3", + "npm:vite@^5.4.10" + ] + } + } +} 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..c74e99d --- /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.26.9", + "typescript": "^5.8.3" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.1", + "vite": "^5.4.10" + } +} diff --git a/playground/src/App.tsx b/playground/src/App.tsx new file mode 100644 index 0000000..2e423fa --- /dev/null +++ b/playground/src/App.tsx @@ -0,0 +1,603 @@ +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'; +// @ts-ignore Vite resolves worker query imports during bundling. +import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; +// @ts-ignore Vite resolves worker query imports during bundling. +import TypeScriptWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; +import { playgroundExamples } from './examples.ts'; + +const DOCXML_PACKAGE_NAME = '@fontoxml/docxml'; +const DOCXML_META_URL = 'https://jsr.io/@fontoxml/docxml/meta.json'; +const DOCXML_RUNTIME_URL = 'https://esm.sh/jsr/@fontoxml/docxml?bundle'; +const docxmlSourceCache = new Map(); + +const monacoEnvironmentTarget = globalThis as typeof globalThis & { + MonacoEnvironment?: { + getWorker: (_workerId: string, label: string) => Worker; + }; +}; + +monacoEnvironmentTarget.MonacoEnvironment = { + getWorker(_workerId: string, label: string) { + if (label === 'typescript' || label === 'javascript') { + return new TypeScriptWorker(); + } + + return new EditorWorker(); + }, +}; + +function ensureBrowserDenoCompat() { + const denoTarget = globalThis as typeof globalThis & { + Deno?: { + cwd?: () => string; + }; + }; + + if (!denoTarget.Deno) { + denoTarget.Deno = { + cwd: () => '/', + }; + return; + } + + if (typeof denoTarget.Deno.cwd !== 'function') { + denoTarget.Deno.cwd = () => '/'; + } +} + +async function fetchJsrSourceText(version: string, filePath: string) { + const cacheKey = `${version}:${filePath}`; + if (docxmlSourceCache.has(cacheKey)) { + return docxmlSourceCache.get(cacheKey) ?? ''; + } + + const response = await fetch( + `https://jsr.io/${DOCXML_PACKAGE_NAME}/${version}${filePath}?raw=1` + ); + + if (!response.ok) { + throw new Error(`Failed to load JSR source file: ${filePath}`); + } + + const text = await response.text(); + const contentType = response.headers.get('content-type') ?? ''; + + if (contentType.includes('text/html')) { + const document = new DOMParser().parseFromString(text, 'text/html'); + const codeElement = document.querySelector('pre'); + if (codeElement?.textContent) { + const extracted = codeElement.textContent; + docxmlSourceCache.set(cacheKey, extracted); + return extracted; + } + } + + if (!text.trim()) { + throw new Error( + `Could not extract source from JSR file page: ${filePath}` + ); + } + + docxmlSourceCache.set(cacheKey, text); + return text; +} + +function toMonacoFilePath(version: string, filePath: string) { + return `file:///jsr/${DOCXML_PACKAGE_NAME}/${version}${filePath}`; +} + +async function installMonacoTypes() { + const metaResponse = await fetch(DOCXML_META_URL); + if (!metaResponse.ok) { + throw new Error('Could not load JSR metadata for docxml.'); + } + + const meta = (await metaResponse.json()) as { + latest?: string; + versions?: Record }>; + }; + const version = meta.latest; + + if (!version) { + throw new Error('Could not resolve the latest JSR package version.'); + } + + const versionMetaResponse = await fetch( + `https://jsr.io/${DOCXML_PACKAGE_NAME}/${version}_meta.json` + ); + + if (!versionMetaResponse.ok) { + throw new Error('Could not load the latest JSR package manifest.'); + } + + const versionMeta = (await versionMetaResponse.json()) as { + manifest?: Record; + }; + + const manifest = versionMeta.manifest ?? meta.versions?.[version]?.manifest; + + if (!manifest) { + throw new Error('Could not resolve the latest JSR package manifest.'); + } + + monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ + allowSyntheticDefaultImports: true, + esModuleInterop: true, + allowImportingTsExtensions: true, + strict: true, + }); + monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ + noSemanticValidation: false, + noSyntaxValidation: false, + }); + monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true); + + const sourceFiles = Object.keys(manifest).filter( + (filePath) => filePath.endsWith('.ts') || filePath.endsWith('.tsx') + ); + + const sourceEntries = await Promise.all( + sourceFiles.map(async (filePath) => ({ + filePath, + text: await fetchJsrSourceText(version, filePath), + })) + ); + + for (const { filePath, text } of sourceEntries) { + monaco.languages.typescript.typescriptDefaults.addExtraLib( + text, + toMonacoFilePath(version, filePath) + ); + } + + monaco.languages.typescript.typescriptDefaults.addExtraLib( + [ + `declare module 'docxml' {`, + `\texport * from '${toMonacoFilePath(version, '/mod.ts')}';`, + `\texport { default } from '${toMonacoFilePath(version, '/mod.ts')}';`, + '}', + '', + ].join('\n'), + 'file:///docxml-playground/docxml-module.d.ts' + ); +} + +function fallbackUuid() { + const bytes = new Uint8Array(16); + + if (globalThis.crypto?.getRandomValues) { + globalThis.crypto.getRandomValues(bytes); + } else { + for (let index = 0; index < bytes.length; index++) { + bytes[index] = Math.floor(Math.random() * 256); + } + } + + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + const hex = Array.from(bytes, (value) => + value.toString(16).padStart(2, '0') + ); + return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex + .slice(6, 8) + .join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`; +} + +function ensureRandomUuid() { + const cryptoTarget = globalThis.crypto as Crypto & { + randomUUID?: () => string; + }; + + if (typeof cryptoTarget?.randomUUID === 'function') { + return; + } + + if (!globalThis.crypto) { + (globalThis as typeof globalThis & { crypto: Crypto }).crypto = { + randomUUID: fallbackUuid, + } as unknown as Crypto; + return; + } + + Object.defineProperty(globalThis.crypto, 'randomUUID', { + value: fallbackUuid, + configurable: true, + writable: true, + }); +} + +async function normalizeResult(result: unknown) { + if (result instanceof Uint8Array) { + return result; + } + + if (result instanceof ArrayBuffer) { + return new Uint8Array(result); + } + + if (result instanceof Blob) { + return new Uint8Array(await result.arrayBuffer()); + } + + if ( + result && + typeof result === 'object' && + 'asUint8Array' in result && + typeof (result as { asUint8Array: () => Uint8Array }).asUint8Array === + 'function' + ) { + return (result as { asUint8Array: () => Uint8Array }).asUint8Array(); + } + + if ( + result && + typeof result === 'object' && + 'toArchive' in result && + typeof ( + result as { + toArchive: () => Promise<{ asUint8Array: () => Uint8Array }>; + } + ).toArchive === 'function' + ) { + const archive = await ( + result as { + toArchive: () => Promise<{ asUint8Array: () => Uint8Array }>; + } + ).toArchive(); + return archive.asUint8Array(); + } + + throw new Error('Result type is not supported for DOCX download'); +} + +function downloadFile(data: Uint8Array, fileName: string) { + const view = new Uint8Array(data); + const stableBuffer = view.buffer.slice( + view.byteOffset, + view.byteOffset + view.byteLength + ); + + const blob = new Blob([stableBuffer], { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }); + + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = fileName; + anchor.click(); + URL.revokeObjectURL(url); +} + +function transpileTypeScript(source: string) { + return ts.transpileModule(source, { + compilerOptions: { + target: ts.ScriptTarget.ES2020, + module: ts.ModuleKind.ES2022, + strict: true, + }, + fileName: 'playground.ts', + reportDiagnostics: false, + }).outputText; +} + +function buildRunnableModuleSource(source: string) { + const hasDefaultExport = /\bexport\s+default\b/m.test(source); + const hasTopLevelModuleSyntax = + /^\s*import\s/m.test(source) || /^\s*export\s/m.test(source); + + if (hasDefaultExport) { + return source; + } + + if (hasTopLevelModuleSyntax) { + throw new Error( + 'This code has module syntax. Export a default function to run it.' + ); + } + + const symbolNames = [ + 'Docx', + 'Paragraph', + 'Text', + 'Section', + 'Table', + 'Row', + 'Cell', + 'Comment', + 'CommentRangeStart', + 'CommentRangeEnd', + 'cm', + 'pt', + 'inch', + ]; + + return ` +import DocxDefault, * as __docxml from '${DOCXML_RUNTIME_URL}'; +const Docx = __docxml.Docx ?? DocxDefault; +const { ${symbolNames.filter((name) => name !== 'Docx').join(', ')} } = __docxml; + +export default async function __playgroundRun() { +${source} +} + +function registerFallbackCompletions() { + const sharedSymbols = [ + 'Docx', + 'Paragraph', + 'Text', + 'Section', + 'Table', + 'Row', + 'Cell', + 'Comment', + 'CommentRangeStart', + 'CommentRangeEnd', + 'cm', + 'pt', + 'inch', + ]; + + return monaco.languages.registerCompletionItemProvider('typescript', { + triggerCharacters: ['.', "'", '"'], + provideCompletionItems(model, position) { + const currentLine = model.getLineContent(position.lineNumber); + const range = new monaco.Range( + position.lineNumber, + position.column, + position.lineNumber, + position.column + ); + + const suggestions: monaco.languages.CompletionItem[] = [ + { + label: "import docxml", + kind: monaco.languages.CompletionItemKind.Snippet, + insertText: + "import Docx, { Paragraph, Text } from 'docxml';\n\nexport default function buildDocx() {\n\tconst docx = Docx.fromNothing();\n\n\t$0\n\n\treturn docx;\n}\n", + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + }, + ]; + + for (const symbol of sharedSymbols) { + suggestions.push({ + label: symbol, + kind: monaco.languages.CompletionItemKind.Class, + insertText: symbol, + range, + }); + } + + if (/from\s+['\"]?$/.test(currentLine)) { + suggestions.push({ + label: 'docxml', + kind: monaco.languages.CompletionItemKind.Module, + insertText: 'docxml', + range, + }); + } + + return { suggestions }; + }, + }); +} +`; +} + +export function App() { + const editorElementRef = useRef(null); + const editorRef = useRef(null); + const modelRef = useRef(null); + const completionProviderRef = useRef(null); + const [status, setStatus] = useState('Ready.'); + const [hasError, setHasError] = useState(false); + const [selectedExampleId, setSelectedExampleId] = useState(''); + const [isGenerating, setIsGenerating] = useState(false); + + useEffect(() => { + ensureBrowserDenoCompat(); + + if (!editorElementRef.current) { + setStatus('Editor container not found.'); + setHasError(true); + return; + } + + modelRef.current = monaco.editor.createModel( + playgroundExamples[0]?.source ?? '', + 'typescript', + monaco.Uri.parse('file:///playground/main.ts') + ); + + editorRef.current = monaco.editor.create(editorElementRef.current, { + model: modelRef.current, + theme: 'vs', + automaticLayout: true, + minimap: { enabled: false }, + fontSize: 13, + suggestOnTriggerCharacters: true, + quickSuggestions: { + other: true, + comments: false, + strings: true, + }, + }); + + completionProviderRef.current = registerFallbackCompletions(); + + setStatus('Loading docxml types...'); + setHasError(false); + + void installMonacoTypes() + .then(() => { + setStatus('Ready.'); + setHasError(false); + }) + .catch((error) => { + const message = + error instanceof Error ? error.message : String(error); + setStatus(`Types unavailable: ${message}`); + setHasError(true); + }); + + return () => { + completionProviderRef.current?.dispose(); + editorRef.current?.dispose(); + modelRef.current?.dispose(); + completionProviderRef.current = null; + editorRef.current = null; + modelRef.current = null; + }; + }, []); + + async function runCurrentEditor() { + const editor = editorRef.current; + if (!editor) { + setStatus('Editor is not ready yet.'); + setHasError(true); + return; + } + + const source = editor.getValue(); + + setStatus('Compiling...'); + setHasError(false); + setIsGenerating(true); + ensureRandomUuid(); + + try { + const runnableSource = buildRunnableModuleSource(source); + const jsSource = transpileTypeScript(runnableSource); + const runtimeSource = jsSource + .replaceAll(`from 'docxml'`, `from '${DOCXML_RUNTIME_URL}'`) + .replaceAll(`from "docxml"`, `from "${DOCXML_RUNTIME_URL}"`); + + const moduleBlob = new Blob([runtimeSource], { + type: 'text/javascript', + }); + const moduleUrl = URL.createObjectURL(moduleBlob); + + try { + const run = (await import(moduleUrl)) as { + default?: () => unknown; + }; + setStatus('Generating DOCX...'); + + if (typeof run.default !== 'function') { + throw new Error( + 'The module must export a default function.' + ); + } + + const result = await run.default(); + const data = await normalizeResult(result); + + downloadFile(data, 'playground-output.docx'); + setStatus(`Document generated (${data.byteLength} bytes).`); + setHasError(false); + } finally { + URL.revokeObjectURL(moduleUrl); + } + } catch (error) { + const message = + error instanceof Error ? error.message : String(error); + setStatus(message); + setHasError(true); + } finally { + setIsGenerating(false); + } + } + + function loadSelectedExample() { + if (!selectedExampleId) { + setStatus('Select an example to load.'); + setHasError(false); + return; + } + + const editor = editorRef.current; + if (!editor) { + setStatus('Editor is not ready yet.'); + setHasError(true); + return; + } + + const example = playgroundExamples.find( + (entry) => entry.id === selectedExampleId + ); + + if (!example) { + setStatus('Selected example was not found.'); + setHasError(true); + return; + } + + editor.getModel()?.setValue(example.source); + setStatus(`Loaded example: ${example.label}`); + setHasError(false); + } + + return ( +
+
+
+

Docxml Playground

+

+ Write TypeScript and generate your DOCX in the browser. +

+
+
+ + + +
+
+ +
+
+
{status}
+
+ +

+ Use plain TypeScript. Available symbols include Docx, Paragraph, + Text, Section, Table, Row, Cell, Comment, CommentRangeStart, + CommentRangeEnd, cm, pt, and inch. Return Docx, Archive, + Uint8Array, ArrayBuffer, or Blob. +

+
+ ); +} diff --git a/playground/src/examples.ts b/playground/src/examples.ts new file mode 100644 index 0000000..2a3c344 --- /dev/null +++ b/playground/src/examples.ts @@ -0,0 +1,26 @@ +export type PlaygroundExample = { + id: string; + label: string; + sourcePath: string; + source: string; +}; + +export const playgroundExamples: PlaygroundExample[] = [ + { + id: 'hello', + label: 'Simple paragraph', + sourcePath: '', + 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; +} +`, + }, +]; 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..542440e --- /dev/null +++ b/playground/src/style.css @@ -0,0 +1,158 @@ +:root { + --bg-1: #f3efe2; + --bg-2: #dbe7d5; + --panel: #fffdf7; + --ink: #20201c; + --muted: #5d6157; + --line: #c6c0ae; + --accent: #1f7a5c; + --error: #9f1d1d; +} + +* { + box-sizing: border-box; +} + +html, +body, +#app { + margin: 0; + min-height: 100%; + font-family: + system-ui, + -apple-system, + 'Segoe UI', + sans-serif; + color: var(--ink); + background: + radial-gradient( + circle at 18% 20%, + rgba(31, 122, 92, 0.18), + transparent 30% + ), + radial-gradient( + circle at 82% 5%, + rgba(15, 95, 143, 0.2), + transparent 25% + ), + linear-gradient(145deg, var(--bg-1), var(--bg-2)); +} + +#app { + padding: 1.4rem; +} + +.layout { + margin: 0 auto; + max-width: 1100px; + display: grid; + gap: 1rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: end; + gap: 1rem; +} + +.title { + margin: 0; + font-size: clamp(1.5rem, 3vw, 2.4rem); + line-height: 1; +} + +.subtitle { + margin: 0.4rem 0 0; + color: var(--muted); +} + +.controls { + display: flex; + flex-wrap: wrap; + gap: 0.7rem; + align-items: center; +} + +.control { + display: grid; + gap: 0.35rem; + font-size: 0.85rem; + color: var(--muted); +} + +select, +button { + border: 1px solid var(--line); + border-radius: 12px; + padding: 0.5rem 0.75rem; + font: inherit; + background: white; + color: var(--ink); +} + +button { + cursor: pointer; +} + +button.primary { + border-color: var(--accent); + background: var(--accent); + color: #fff; + font-weight: 700; +} + +button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.editor-shell { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 16px; + overflow: hidden; +} + +.editor { + width: 100%; + height: min(62vh, 620px); + border: 0; + border-radius: 0; + overflow: hidden; +} + +.status { + padding: 0.8rem 1rem; + border-top: 1px solid var(--line); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace; + font-size: 0.82rem; + color: var(--muted); + min-height: 2.6rem; + background: #fbf8ee; +} + +.status.error { + color: var(--error); +} + +.hint { + margin: 0; + color: var(--muted); + font-size: 0.85rem; +} + +@media (max-width: 760px) { + #app { + padding: 1rem; + } + + .header { + flex-direction: column; + align-items: start; + } + + .editor { + height: 58vh; + } +} diff --git a/playground/src/vite-workers.d.ts b/playground/src/vite-workers.d.ts new file mode 100644 index 0000000..7fc924d --- /dev/null +++ b/playground/src/vite-workers.d.ts @@ -0,0 +1,13 @@ +declare module 'monaco-editor/esm/vs/editor/editor.worker?worker' { + const EditorWorker: { + new (): Worker; + }; + export default EditorWorker; +} + +declare module 'monaco-editor/esm/vs/language/typescript/ts.worker?worker' { + const TypeScriptWorker: { + new (): Worker; + }; + export default TypeScriptWorker; +} diff --git a/playground/tsconfig.json b/playground/tsconfig.json new file mode 100644 index 0000000..9ad6612 --- /dev/null +++ b/playground/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2021", + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "jsxImportSource": "preact", + "strict": true, + "lib": ["ES2021", "DOM", "DOM.Iterable"], + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/playground/vite.config.ts b/playground/vite.config.ts new file mode 100644 index 0000000..5c11915 --- /dev/null +++ b/playground/vite.config.ts @@ -0,0 +1,7 @@ +import preact from '@preact/preset-vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + base: './', + plugins: [preact()], +}); From feb52cb1ee2089b05d19401d659a2695793d59fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20G=C3=B3mez?= Date: Tue, 19 May 2026 13:52:14 +0200 Subject: [PATCH 02/23] Cleanup --- .github/workflows/playground-pages.yml | 14 +- deno.json | 6 +- playground/README.md | 18 +- playground/deno.json | 5 + playground/src/App.tsx | 426 ++++++------------------ playground/src/examples.ts | 26 -- playground/src/style.css | 146 ++------ playground/src/vite-workers.d.ts | 13 - playground/src/workers/editor.worker.ts | 1 + playground/src/workers/ts.worker.ts | 1 + 10 files changed, 166 insertions(+), 490 deletions(-) delete mode 100644 playground/src/examples.ts delete mode 100644 playground/src/vite-workers.d.ts create mode 100644 playground/src/workers/editor.worker.ts create mode 100644 playground/src/workers/ts.worker.ts diff --git a/.github/workflows/playground-pages.yml b/.github/workflows/playground-pages.yml index 1c03c2b..fd3fc63 100644 --- a/.github/workflows/playground-pages.yml +++ b/.github/workflows/playground-pages.yml @@ -26,13 +26,13 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup Deno - uses: denoland/setup-deno@v1 - with: - deno-version: v2.x + - name: Setup Deno + uses: denoland/setup-deno@v1 + with: + deno-version: v2.x - - name: Build playground - run: deno task playground:build + - name: Build playground + run: deno task playground:build - name: Setup Pages uses: actions/configure-pages@v5 @@ -40,7 +40,7 @@ jobs: - name: Upload playground artifact uses: actions/upload-pages-artifact@v3 with: - path: playground/dist + path: playground/dist - name: Deploy to GitHub Pages id: deployment diff --git a/deno.json b/deno.json index 7714447..e81bdc8 100644 --- a/deno.json +++ b/deno.json @@ -52,9 +52,9 @@ "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 run -A --node-modules-dir=auto npm:vite", - "playground:build": "cd playground && deno run -A --node-modules-dir=auto npm:vite build", - "playground:preview": "cd playground && deno run -A --node-modules-dir=auto npm:vite preview --host --port 4173", + "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/playground/README.md b/playground/README.md index f91e6b0..e91c177 100644 --- a/playground/README.md +++ b/playground/README.md @@ -1,9 +1,11 @@ # Docxml Playground -Vite + Preact playground to try `docxml` in the browser. +Minimal browser playground for `docxml`. -The app bundles Monaco statically at build time (including workers), and still loads the latest published `docxml` release from JSR at runtime. -Type information for Monaco is loaded from the latest JSR source files in-memory and is not written to disk. +- Monaco editor with TypeScript. +- Runtime import uses the latest release from JSR via `https://esm.sh/jsr/@fontoxml/docxml?bundle`. +- Type information is loaded dynamically from `jsr.io` (latest package metadata + source files). +- One action: compile and generate a `.docx` file. ## Run locally with Deno @@ -13,9 +15,9 @@ From the repository root: deno task playground:dev ``` -Then open `http://localhost:5173`. +Open `http://localhost:5173`. -## Build static output +## Build static output with Deno From the repository root: @@ -23,8 +25,8 @@ From the repository root: deno task playground:build ``` -The generated static site is written to `playground/dist`. +The static output is generated in `playground/dist`. -## Deploy +## Deploy to GitHub Pages -The `.github/workflows/playground-pages.yml` workflow builds the playground and publishes `playground/dist` to GitHub Pages whenever a new release is published. +`/.github/workflows/playground-pages.yml` publishes `playground/dist` to GitHub Pages when a new release is published. diff --git a/playground/deno.json b/playground/deno.json index 8e6bcd0..0e778d1 100644 --- a/playground/deno.json +++ b/playground/deno.json @@ -1,4 +1,9 @@ { + "tasks": { + "dev": "deno run -A --node-modules-dir=auto npm:vite", + "build": "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"], diff --git a/playground/src/App.tsx b/playground/src/App.tsx index 2e423fa..7cba788 100644 --- a/playground/src/App.tsx +++ b/playground/src/App.tsx @@ -2,15 +2,25 @@ 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'; -// @ts-ignore Vite resolves worker query imports during bundling. -import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; -// @ts-ignore Vite resolves worker query imports during bundling. -import TypeScriptWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; -import { playgroundExamples } from './examples.ts'; const DOCXML_PACKAGE_NAME = '@fontoxml/docxml'; const DOCXML_META_URL = 'https://jsr.io/@fontoxml/docxml/meta.json'; const DOCXML_RUNTIME_URL = 'https://esm.sh/jsr/@fontoxml/docxml?bundle'; +const PLAYGROUND_FILE = 'file:///playground/main.ts'; + +const INITIAL_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; +} +`; + const docxmlSourceCache = new Map(); const monacoEnvironmentTarget = globalThis as typeof globalThis & { @@ -22,14 +32,24 @@ const monacoEnvironmentTarget = globalThis as typeof globalThis & { monacoEnvironmentTarget.MonacoEnvironment = { getWorker(_workerId: string, label: string) { if (label === 'typescript' || label === 'javascript') { - return new TypeScriptWorker(); + return new Worker( + new URL('./workers/ts.worker.ts', import.meta.url), + { + type: 'module', + } + ); } - return new EditorWorker(); + return new Worker( + new URL('./workers/editor.worker.ts', import.meta.url), + { + type: 'module', + } + ); }, }; -function ensureBrowserDenoCompat() { +function ensureBrowserPolyfills() { const denoTarget = globalThis as typeof globalThis & { Deno?: { cwd?: () => string; @@ -40,10 +60,7 @@ function ensureBrowserDenoCompat() { denoTarget.Deno = { cwd: () => '/', }; - return; - } - - if (typeof denoTarget.Deno.cwd !== 'function') { + } else if (typeof denoTarget.Deno.cwd !== 'function') { denoTarget.Deno.cwd = () => '/'; } } @@ -59,26 +76,12 @@ async function fetchJsrSourceText(version: string, filePath: string) { ); if (!response.ok) { - throw new Error(`Failed to load JSR source file: ${filePath}`); + throw new Error(`No se pudo cargar el archivo JSR: ${filePath}`); } const text = await response.text(); - const contentType = response.headers.get('content-type') ?? ''; - - if (contentType.includes('text/html')) { - const document = new DOMParser().parseFromString(text, 'text/html'); - const codeElement = document.querySelector('pre'); - if (codeElement?.textContent) { - const extracted = codeElement.textContent; - docxmlSourceCache.set(cacheKey, extracted); - return extracted; - } - } - if (!text.trim()) { - throw new Error( - `Could not extract source from JSR file page: ${filePath}` - ); + throw new Error(`Archivo vacio en JSR: ${filePath}`); } docxmlSourceCache.set(cacheKey, text); @@ -92,50 +95,55 @@ function toMonacoFilePath(version: string, filePath: string) { async function installMonacoTypes() { const metaResponse = await fetch(DOCXML_META_URL); if (!metaResponse.ok) { - throw new Error('Could not load JSR metadata for docxml.'); + throw new Error('No se pudo cargar metadata de docxml en JSR.'); } const meta = (await metaResponse.json()) as { latest?: string; - versions?: Record }>; }; - const version = meta.latest; - if (!version) { - throw new Error('Could not resolve the latest JSR package version.'); + if (!meta.latest) { + throw new Error('No se pudo resolver la ultima version de docxml.'); } + const version = meta.latest; const versionMetaResponse = await fetch( `https://jsr.io/${DOCXML_PACKAGE_NAME}/${version}_meta.json` ); if (!versionMetaResponse.ok) { - throw new Error('Could not load the latest JSR package manifest.'); + throw new Error( + 'No se pudo cargar el manifiesto de la ultima version.' + ); } const versionMeta = (await versionMetaResponse.json()) as { manifest?: Record; }; - const manifest = versionMeta.manifest ?? meta.versions?.[version]?.manifest; - - if (!manifest) { - throw new Error('Could not resolve the latest JSR package manifest.'); + if (!versionMeta.manifest) { + throw new Error('No se pudo resolver el manifiesto de la version.'); } monaco.languages.typescript.typescriptDefaults.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, }); + monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ noSemanticValidation: false, noSyntaxValidation: false, }); + monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true); - const sourceFiles = Object.keys(manifest).filter( + const sourceFiles = Object.keys(versionMeta.manifest).filter( (filePath) => filePath.endsWith('.ts') || filePath.endsWith('.tsx') ); @@ -161,53 +169,8 @@ async function installMonacoTypes() { '}', '', ].join('\n'), - 'file:///docxml-playground/docxml-module.d.ts' - ); -} - -function fallbackUuid() { - const bytes = new Uint8Array(16); - - if (globalThis.crypto?.getRandomValues) { - globalThis.crypto.getRandomValues(bytes); - } else { - for (let index = 0; index < bytes.length; index++) { - bytes[index] = Math.floor(Math.random() * 256); - } - } - - bytes[6] = (bytes[6] & 0x0f) | 0x40; - bytes[8] = (bytes[8] & 0x3f) | 0x80; - - const hex = Array.from(bytes, (value) => - value.toString(16).padStart(2, '0') + 'file:///playground/docxml.d.ts' ); - return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex - .slice(6, 8) - .join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`; -} - -function ensureRandomUuid() { - const cryptoTarget = globalThis.crypto as Crypto & { - randomUUID?: () => string; - }; - - if (typeof cryptoTarget?.randomUUID === 'function') { - return; - } - - if (!globalThis.crypto) { - (globalThis as typeof globalThis & { crypto: Crypto }).crypto = { - randomUUID: fallbackUuid, - } as unknown as Crypto; - return; - } - - Object.defineProperty(globalThis.crypto, 'randomUUID', { - value: fallbackUuid, - configurable: true, - writable: true, - }); } async function normalizeResult(result: unknown) { @@ -251,20 +214,16 @@ async function normalizeResult(result: unknown) { return archive.asUint8Array(); } - throw new Error('Result type is not supported for DOCX download'); + throw new Error( + 'El resultado debe ser Docx, Archive, Uint8Array, Blob o ArrayBuffer.' + ); } function downloadFile(data: Uint8Array, fileName: string) { - const view = new Uint8Array(data); - const stableBuffer = view.buffer.slice( - view.byteOffset, - view.byteOffset + view.byteLength - ); - - const blob = new Blob([stableBuffer], { + const safeBytes = Uint8Array.from(data); + const blob = new Blob([safeBytes], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }); - const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; @@ -280,226 +239,105 @@ function transpileTypeScript(source: string) { module: ts.ModuleKind.ES2022, strict: true, }, - fileName: 'playground.ts', + fileName: 'main.ts', reportDiagnostics: false, }).outputText; } -function buildRunnableModuleSource(source: string) { - const hasDefaultExport = /\bexport\s+default\b/m.test(source); - const hasTopLevelModuleSyntax = - /^\s*import\s/m.test(source) || /^\s*export\s/m.test(source); - - if (hasDefaultExport) { - return source; - } - - if (hasTopLevelModuleSyntax) { - throw new Error( - 'This code has module syntax. Export a default function to run it.' - ); - } - - const symbolNames = [ - 'Docx', - 'Paragraph', - 'Text', - 'Section', - 'Table', - 'Row', - 'Cell', - 'Comment', - 'CommentRangeStart', - 'CommentRangeEnd', - 'cm', - 'pt', - 'inch', - ]; - - return ` -import DocxDefault, * as __docxml from '${DOCXML_RUNTIME_URL}'; -const Docx = __docxml.Docx ?? DocxDefault; -const { ${symbolNames.filter((name) => name !== 'Docx').join(', ')} } = __docxml; - -export default async function __playgroundRun() { -${source} -} - -function registerFallbackCompletions() { - const sharedSymbols = [ - 'Docx', - 'Paragraph', - 'Text', - 'Section', - 'Table', - 'Row', - 'Cell', - 'Comment', - 'CommentRangeStart', - 'CommentRangeEnd', - 'cm', - 'pt', - 'inch', - ]; - - return monaco.languages.registerCompletionItemProvider('typescript', { - triggerCharacters: ['.', "'", '"'], - provideCompletionItems(model, position) { - const currentLine = model.getLineContent(position.lineNumber); - const range = new monaco.Range( - position.lineNumber, - position.column, - position.lineNumber, - position.column - ); - - const suggestions: monaco.languages.CompletionItem[] = [ - { - label: "import docxml", - kind: monaco.languages.CompletionItemKind.Snippet, - insertText: - "import Docx, { Paragraph, Text } from 'docxml';\n\nexport default function buildDocx() {\n\tconst docx = Docx.fromNothing();\n\n\t$0\n\n\treturn docx;\n}\n", - insertTextRules: - monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, - range, - }, - ]; - - for (const symbol of sharedSymbols) { - suggestions.push({ - label: symbol, - kind: monaco.languages.CompletionItemKind.Class, - insertText: symbol, - range, - }); - } - - if (/from\s+['\"]?$/.test(currentLine)) { - suggestions.push({ - label: 'docxml', - kind: monaco.languages.CompletionItemKind.Module, - insertText: 'docxml', - range, - }); - } - - return { suggestions }; - }, - }); -} -`; -} - export function App() { const editorElementRef = useRef(null); const editorRef = useRef(null); - const modelRef = useRef(null); - const completionProviderRef = useRef(null); - const [status, setStatus] = useState('Ready.'); + const [status, setStatus] = useState('Inicializando...'); const [hasError, setHasError] = useState(false); - const [selectedExampleId, setSelectedExampleId] = useState(''); const [isGenerating, setIsGenerating] = useState(false); useEffect(() => { - ensureBrowserDenoCompat(); + ensureBrowserPolyfills(); if (!editorElementRef.current) { - setStatus('Editor container not found.'); + setStatus('No se encontro el contenedor del editor.'); setHasError(true); return; } - modelRef.current = monaco.editor.createModel( - playgroundExamples[0]?.source ?? '', + const model = monaco.editor.createModel( + INITIAL_SOURCE, 'typescript', - monaco.Uri.parse('file:///playground/main.ts') + monaco.Uri.parse(PLAYGROUND_FILE) ); editorRef.current = monaco.editor.create(editorElementRef.current, { - model: modelRef.current, + model, theme: 'vs', automaticLayout: true, minimap: { enabled: false }, fontSize: 13, - suggestOnTriggerCharacters: true, - quickSuggestions: { - other: true, - comments: false, - strings: true, - }, }); - completionProviderRef.current = registerFallbackCompletions(); - - setStatus('Loading docxml types...'); + setStatus('Cargando tipos desde JSR...'); setHasError(false); void installMonacoTypes() .then(() => { - setStatus('Ready.'); + setStatus('Listo.'); setHasError(false); }) .catch((error) => { const message = error instanceof Error ? error.message : String(error); - setStatus(`Types unavailable: ${message}`); + setStatus(`Tipos no disponibles: ${message}`); setHasError(true); }); return () => { - completionProviderRef.current?.dispose(); + model.dispose(); editorRef.current?.dispose(); - modelRef.current?.dispose(); - completionProviderRef.current = null; editorRef.current = null; - modelRef.current = null; }; }, []); - async function runCurrentEditor() { + async function generateDocx() { const editor = editorRef.current; if (!editor) { - setStatus('Editor is not ready yet.'); + setStatus('El editor aun no esta listo.'); setHasError(true); return; } - const source = editor.getValue(); - - setStatus('Compiling...'); - setHasError(false); setIsGenerating(true); - ensureRandomUuid(); + setHasError(false); + setStatus('Compilando...'); try { - const runnableSource = buildRunnableModuleSource(source); - const jsSource = transpileTypeScript(runnableSource); - const runtimeSource = jsSource + const source = editor.getValue(); + const jsSource = transpileTypeScript(source) .replaceAll(`from 'docxml'`, `from '${DOCXML_RUNTIME_URL}'`) - .replaceAll(`from "docxml"`, `from "${DOCXML_RUNTIME_URL}"`); + .replaceAll( + `from \"docxml\"`, + `from \"${DOCXML_RUNTIME_URL}\"` + ); - const moduleBlob = new Blob([runtimeSource], { + const moduleBlob = new Blob([jsSource], { type: 'text/javascript', }); const moduleUrl = URL.createObjectURL(moduleBlob); try { - const run = (await import(moduleUrl)) as { - default?: () => unknown; + const mod = (await import(moduleUrl)) as { + default?: () => unknown | Promise; }; - setStatus('Generating DOCX...'); - if (typeof run.default !== 'function') { + if (typeof mod.default !== 'function') { throw new Error( - 'The module must export a default function.' + 'El modulo debe exportar una funcion default.' ); } - const result = await run.default(); + setStatus('Generando DOCX...'); + const result = await mod.default(); const data = await normalizeResult(result); - downloadFile(data, 'playground-output.docx'); - setStatus(`Document generated (${data.byteLength} bytes).`); + setStatus(`Documento generado (${data.byteLength} bytes).`); setHasError(false); } finally { URL.revokeObjectURL(moduleUrl); @@ -514,90 +352,30 @@ export function App() { } } - function loadSelectedExample() { - if (!selectedExampleId) { - setStatus('Select an example to load.'); - setHasError(false); - return; - } - - const editor = editorRef.current; - if (!editor) { - setStatus('Editor is not ready yet.'); - setHasError(true); - return; - } - - const example = playgroundExamples.find( - (entry) => entry.id === selectedExampleId - ); - - if (!example) { - setStatus('Selected example was not found.'); - setHasError(true); - return; - } - - editor.getModel()?.setValue(example.source); - setStatus(`Loaded example: ${example.label}`); - setHasError(false); - } - return (
-
-

Docxml Playground

-

- Write TypeScript and generate your DOCX in the browser. -

-
-
- - - -
+

Docxml Playground

+

+ Playground minimo: escribe TypeScript, importa{' '} + docxml y genera un .docx. +

-
-
-
{status}
-
- -

- Use plain TypeScript. Available symbols include Docx, Paragraph, - Text, Section, Table, Row, Cell, Comment, CommentRangeStart, - CommentRangeEnd, cm, pt, and inch. Return Docx, Archive, - Uint8Array, ArrayBuffer, or Blob. -

+
+ +
+ +
+ +

{status}

); } diff --git a/playground/src/examples.ts b/playground/src/examples.ts deleted file mode 100644 index 2a3c344..0000000 --- a/playground/src/examples.ts +++ /dev/null @@ -1,26 +0,0 @@ -export type PlaygroundExample = { - id: string; - label: string; - sourcePath: string; - source: string; -}; - -export const playgroundExamples: PlaygroundExample[] = [ - { - id: 'hello', - label: 'Simple paragraph', - sourcePath: '', - 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; -} -`, - }, -]; diff --git a/playground/src/style.css b/playground/src/style.css index 542440e..14a5dfe 100644 --- a/playground/src/style.css +++ b/playground/src/style.css @@ -1,12 +1,11 @@ :root { - --bg-1: #f3efe2; - --bg-2: #dbe7d5; - --panel: #fffdf7; - --ink: #20201c; - --muted: #5d6157; - --line: #c6c0ae; - --accent: #1f7a5c; - --error: #9f1d1d; + --bg: #f4f5f2; + --surface: #ffffff; + --text: #111827; + --muted: #4b5563; + --line: #d1d5db; + --accent: #0f766e; + --danger: #b91c1c; } * { @@ -18,141 +17,70 @@ body, #app { margin: 0; min-height: 100%; - font-family: - system-ui, - -apple-system, - 'Segoe UI', - sans-serif; - color: var(--ink); - background: - radial-gradient( - circle at 18% 20%, - rgba(31, 122, 92, 0.18), - transparent 30% - ), - radial-gradient( - circle at 82% 5%, - rgba(15, 95, 143, 0.2), - transparent 25% - ), - linear-gradient(145deg, var(--bg-1), var(--bg-2)); + font-family: 'IBM Plex Sans', system-ui, sans-serif; + background: var(--bg); + color: var(--text); } #app { - padding: 1.4rem; + padding: 1rem; } .layout { + max-width: 980px; margin: 0 auto; - max-width: 1100px; display: grid; - gap: 1rem; + gap: 0.75rem; } -.header { - display: flex; - justify-content: space-between; - align-items: end; - gap: 1rem; -} - -.title { +.header h1 { margin: 0; - font-size: clamp(1.5rem, 3vw, 2.4rem); - line-height: 1; + font-size: 1.8rem; } -.subtitle { +.header p { margin: 0.4rem 0 0; color: var(--muted); } -.controls { - display: flex; - flex-wrap: wrap; - gap: 0.7rem; - align-items: center; +.editor { + height: min(65vh, 680px); + border: 1px solid var(--line); + background: var(--surface); } -.control { - display: grid; - gap: 0.35rem; - font-size: 0.85rem; - color: var(--muted); +.actions { + display: flex; + justify-content: flex-end; } -select, button { - border: 1px solid var(--line); - border-radius: 12px; - padding: 0.5rem 0.75rem; + border: 1px solid var(--accent); + border-radius: 10px; + padding: 0.5rem 0.9rem; font: inherit; - background: white; - color: var(--ink); -} - -button { - cursor: pointer; -} - -button.primary { - border-color: var(--accent); background: var(--accent); color: #fff; - font-weight: 700; + cursor: pointer; + font-weight: 600; } button:disabled { - opacity: 0.6; + opacity: 0.7; cursor: not-allowed; } -.editor-shell { - background: var(--panel); - border: 1px solid var(--line); - border-radius: 16px; - overflow: hidden; -} - -.editor { - width: 100%; - height: min(62vh, 620px); - border: 0; - border-radius: 0; - overflow: hidden; -} - .status { - padding: 0.8rem 1rem; - border-top: 1px solid var(--line); - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace; - font-size: 0.82rem; - color: var(--muted); - min-height: 2.6rem; - background: #fbf8ee; -} - -.status.error { - color: var(--error); -} - -.hint { margin: 0; - color: var(--muted); + 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); } -@media (max-width: 760px) { - #app { - padding: 1rem; - } - - .header { - flex-direction: column; - align-items: start; - } - - .editor { - height: 58vh; - } +.status.error { + color: var(--danger); } diff --git a/playground/src/vite-workers.d.ts b/playground/src/vite-workers.d.ts deleted file mode 100644 index 7fc924d..0000000 --- a/playground/src/vite-workers.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -declare module 'monaco-editor/esm/vs/editor/editor.worker?worker' { - const EditorWorker: { - new (): Worker; - }; - export default EditorWorker; -} - -declare module 'monaco-editor/esm/vs/language/typescript/ts.worker?worker' { - const TypeScriptWorker: { - new (): Worker; - }; - export default TypeScriptWorker; -} 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'; From bc52d4c92230d46d1c3319c2003750c2f4b3af64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20G=C3=B3mez?= Date: Tue, 19 May 2026 14:58:50 +0200 Subject: [PATCH 03/23] More cleanup --- playground/README.md | 32 ---- playground/src/App.tsx | 358 ++++++++++++++----------------------- playground/src/examples.ts | 296 ++++++++++++++++++++++++++++++ playground/src/style.css | 30 +++- playground/tsconfig.json | 13 -- 5 files changed, 455 insertions(+), 274 deletions(-) delete mode 100644 playground/README.md create mode 100644 playground/src/examples.ts delete mode 100644 playground/tsconfig.json diff --git a/playground/README.md b/playground/README.md deleted file mode 100644 index e91c177..0000000 --- a/playground/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Docxml Playground - -Minimal browser playground for `docxml`. - -- Monaco editor with TypeScript. -- Runtime import uses the latest release from JSR via `https://esm.sh/jsr/@fontoxml/docxml?bundle`. -- Type information is loaded dynamically from `jsr.io` (latest package metadata + source files). -- One action: compile and generate a `.docx` file. - -## Run locally with Deno - -From the repository root: - -```sh -deno task playground:dev -``` - -Open `http://localhost:5173`. - -## Build static output with Deno - -From the repository root: - -```sh -deno task playground:build -``` - -The static output is generated in `playground/dist`. - -## Deploy to GitHub Pages - -`/.github/workflows/playground-pages.yml` publishes `playground/dist` to GitHub Pages when a new release is published. diff --git a/playground/src/App.tsx b/playground/src/App.tsx index 7cba788..ec86dc7 100644 --- a/playground/src/App.tsx +++ b/playground/src/App.tsx @@ -1,12 +1,13 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +// @ts-ignore Vite resolves this at build time +import 'monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution'; +import 'monaco-editor/esm/vs/editor/editor.all'; 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_PACKAGE_NAME = '@fontoxml/docxml'; -const DOCXML_META_URL = 'https://jsr.io/@fontoxml/docxml/meta.json'; const DOCXML_RUNTIME_URL = 'https://esm.sh/jsr/@fontoxml/docxml?bundle'; -const PLAYGROUND_FILE = 'file:///playground/main.ts'; const INITIAL_SOURCE = `import Docx, { Paragraph, Text } from 'docxml'; @@ -21,111 +22,61 @@ export default function build() { } `; -const docxmlSourceCache = new Map(); - -const monacoEnvironmentTarget = globalThis as typeof globalThis & { - MonacoEnvironment?: { - getWorker: (_workerId: string, label: string) => Worker; - }; -}; - -monacoEnvironmentTarget.MonacoEnvironment = { +// 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', - } + { type: 'module' } ); } - return new Worker( new URL('./workers/editor.worker.ts', import.meta.url), - { - type: 'module', - } + { type: 'module' } ); }, }; -function ensureBrowserPolyfills() { - const denoTarget = globalThis as typeof globalThis & { - Deno?: { - cwd?: () => string; - }; - }; - - if (!denoTarget.Deno) { - denoTarget.Deno = { - cwd: () => '/', - }; - } else if (typeof denoTarget.Deno.cwd !== 'function') { - denoTarget.Deno.cwd = () => '/'; - } -} - -async function fetchJsrSourceText(version: string, filePath: string) { - const cacheKey = `${version}:${filePath}`; - if (docxmlSourceCache.has(cacheKey)) { - return docxmlSourceCache.get(cacheKey) ?? ''; - } - - const response = await fetch( - `https://jsr.io/${DOCXML_PACKAGE_NAME}/${version}${filePath}?raw=1` - ); - - if (!response.ok) { - throw new Error(`No se pudo cargar el archivo JSR: ${filePath}`); - } - - const text = await response.text(); - if (!text.trim()) { - throw new Error(`Archivo vacio en JSR: ${filePath}`); - } - - docxmlSourceCache.set(cacheKey, text); - return text; +// Polyfill Deno.cwd for docxml browser compat +if (!(globalThis as unknown as { Deno?: unknown }).Deno) { + (globalThis as unknown as { Deno: unknown }).Deno = { cwd: () => '/' }; } -function toMonacoFilePath(version: string, filePath: string) { - return `file:///jsr/${DOCXML_PACKAGE_NAME}/${version}${filePath}`; -} +/** + * Fetches the latest docxml version from JSR, downloads all .ts/.tsx source + * files, and feeds them to Monaco's TypeScript language service so that + * autocomplete, hover, and diagnostics work out of the box. + */ +async function loadDocxmlTypes() { + const meta = (await fetch('https://jsr.io/@fontoxml/docxml/meta.json').then( + (r) => r.json() + )) as { latest: string }; -async function installMonacoTypes() { - const metaResponse = await fetch(DOCXML_META_URL); - if (!metaResponse.ok) { - throw new Error('No se pudo cargar metadata de docxml en JSR.'); - } - - const meta = (await metaResponse.json()) as { - latest?: string; - }; + const version = meta.latest; - if (!meta.latest) { - throw new Error('No se pudo resolver la ultima version de docxml.'); - } + const versionMeta = (await fetch( + `https://jsr.io/@fontoxml/docxml/${version}_meta.json` + ).then((r) => r.json())) as { manifest: Record }; - const version = meta.latest; - const versionMetaResponse = await fetch( - `https://jsr.io/${DOCXML_PACKAGE_NAME}/${version}_meta.json` + const tsFiles = Object.keys(versionMeta.manifest).filter( + (f) => f.endsWith('.ts') || f.endsWith('.tsx') ); - if (!versionMetaResponse.ok) { - throw new Error( - 'No se pudo cargar el manifiesto de la ultima version.' - ); - } - - const versionMeta = (await versionMetaResponse.json()) as { - manifest?: Record; - }; + // Fetch all source files in parallel + const sources = await Promise.all( + tsFiles.map(async (path) => { + const text = await fetch( + `https://jsr.io/@fontoxml/docxml/${version}${path}` + ).then((r) => r.text()); + return { path, text }; + }) + ); - if (!versionMeta.manifest) { - throw new Error('No se pudo resolver el manifiesto de la version.'); - } + // Configure TS compiler options in Monaco + const tsDefaults = monaco.languages.typescript.typescriptDefaults; - monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ + tsDefaults.setCompilerOptions({ target: monaco.languages.typescript.ScriptTarget.ES2020, module: monaco.languages.typescript.ModuleKind.ESNext, moduleResolution: @@ -134,80 +85,49 @@ async function installMonacoTypes() { esModuleInterop: true, allowImportingTsExtensions: true, strict: true, + baseUrl: 'file:///', + paths: { + docxml: [`file:///docxml/${version}/mod.ts`], + }, }); - monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ + tsDefaults.setDiagnosticsOptions({ noSemanticValidation: false, noSyntaxValidation: false, }); - monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true); - - const sourceFiles = Object.keys(versionMeta.manifest).filter( - (filePath) => filePath.endsWith('.ts') || filePath.endsWith('.tsx') - ); + tsDefaults.setEagerModelSync(true); - const sourceEntries = await Promise.all( - sourceFiles.map(async (filePath) => ({ - filePath, - text: await fetchJsrSourceText(version, filePath), - })) - ); - - for (const { filePath, text } of sourceEntries) { - monaco.languages.typescript.typescriptDefaults.addExtraLib( - text, - toMonacoFilePath(version, filePath) - ); + // Add all source files as extra libs + for (const { path, text } of sources) { + tsDefaults.addExtraLib(text, `file:///docxml/${version}${path}`); } - monaco.languages.typescript.typescriptDefaults.addExtraLib( + // Declare the bare 'docxml' module so `import ... from 'docxml'` resolves + tsDefaults.addExtraLib( [ `declare module 'docxml' {`, - `\texport * from '${toMonacoFilePath(version, '/mod.ts')}';`, - `\texport { default } from '${toMonacoFilePath(version, '/mod.ts')}';`, - '}', - '', + ` export * from 'file:///docxml/${version}/mod.ts';`, + ` export { default } from 'file:///docxml/${version}/mod.ts';`, + `}`, ].join('\n'), - 'file:///playground/docxml.d.ts' + `file:///docxml/${version}/__module.d.ts` ); } -async function normalizeResult(result: unknown) { - if (result instanceof Uint8Array) { - return result; - } - - if (result instanceof ArrayBuffer) { - return new Uint8Array(result); - } - - if (result instanceof Blob) { +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()); - } - if ( - result && - typeof result === 'object' && - 'asUint8Array' in result && - typeof (result as { asUint8Array: () => Uint8Array }).asUint8Array === - 'function' - ) { - return (result as { asUint8Array: () => Uint8Array }).asUint8Array(); + const obj = result as Record; + if (typeof obj?.asUint8Array === 'function') { + return (obj as { asUint8Array: () => Uint8Array }).asUint8Array(); } - - if ( - result && - typeof result === 'object' && - 'toArchive' in result && - typeof ( - result as { - toArchive: () => Promise<{ asUint8Array: () => Uint8Array }>; - } - ).toArchive === 'function' - ) { + if (typeof obj?.toArchive === 'function') { const archive = await ( - result as { + obj as { toArchive: () => Promise<{ asUint8Array: () => Uint8Array }>; } ).toArchive(); @@ -215,58 +135,41 @@ async function normalizeResult(result: unknown) { } throw new Error( - 'El resultado debe ser Docx, Archive, Uint8Array, Blob o ArrayBuffer.' + 'Result must be Docx, Archive, Uint8Array, Blob, or ArrayBuffer.' ); } function downloadFile(data: Uint8Array, fileName: string) { - const safeBytes = Uint8Array.from(data); - const blob = new Blob([safeBytes], { + const blob = new Blob([Uint8Array.from(data)], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }); const url = URL.createObjectURL(blob); - const anchor = document.createElement('a'); - anchor.href = url; - anchor.download = fileName; - anchor.click(); + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + a.click(); URL.revokeObjectURL(url); } -function transpileTypeScript(source: string) { - return ts.transpileModule(source, { - compilerOptions: { - target: ts.ScriptTarget.ES2020, - module: ts.ModuleKind.ES2022, - strict: true, - }, - fileName: 'main.ts', - reportDiagnostics: false, - }).outputText; -} - export function App() { - const editorElementRef = useRef(null); - const editorRef = useRef(null); - const [status, setStatus] = useState('Inicializando...'); + 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(() => { - ensureBrowserPolyfills(); - - if (!editorElementRef.current) { - setStatus('No se encontro el contenedor del editor.'); - setHasError(true); - return; - } + if (!editorRef.current) return; const model = monaco.editor.createModel( INITIAL_SOURCE, 'typescript', - monaco.Uri.parse(PLAYGROUND_FILE) + monaco.Uri.parse('file:///playground/main.ts') ); - editorRef.current = monaco.editor.create(editorElementRef.current, { + instanceRef.current = monaco.editor.create(editorRef.current, { model, theme: 'vs', automaticLayout: true, @@ -274,108 +177,109 @@ export function App() { fontSize: 13, }); - setStatus('Cargando tipos desde JSR...'); - setHasError(false); - - void installMonacoTypes() + loadDocxmlTypes() .then(() => { - setStatus('Listo.'); + setStatus('Ready.'); setHasError(false); }) - .catch((error) => { - const message = - error instanceof Error ? error.message : String(error); - setStatus(`Tipos no disponibles: ${message}`); + .catch((err) => { + setStatus( + `Types failed: ${err instanceof Error ? err.message : err}` + ); setHasError(true); }); return () => { model.dispose(); - editorRef.current?.dispose(); - editorRef.current = null; + instanceRef.current?.dispose(); }; }, []); async function generateDocx() { - const editor = editorRef.current; - if (!editor) { - setStatus('El editor aun no esta listo.'); - setHasError(true); - return; - } + const editor = instanceRef.current; + if (!editor) return; setIsGenerating(true); setHasError(false); - setStatus('Compilando...'); + setStatus('Compiling...'); try { const source = editor.getValue(); - const jsSource = transpileTypeScript(source) - .replaceAll(`from 'docxml'`, `from '${DOCXML_RUNTIME_URL}'`) - .replaceAll( - `from \"docxml\"`, - `from \"${DOCXML_RUNTIME_URL}\"` - ); - - const moduleBlob = new Blob([jsSource], { - type: 'text/javascript', - }); - const moduleUrl = URL.createObjectURL(moduleBlob); + 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(moduleUrl)) as { - default?: () => unknown | Promise; - }; - + const mod = await import(/* @vite-ignore */ url); if (typeof mod.default !== 'function') { - throw new Error( - 'El modulo debe exportar una funcion default.' - ); + throw new Error('Module must export a default function.'); } - - setStatus('Generando DOCX...'); + setStatus('Generating DOCX...'); const result = await mod.default(); const data = await normalizeResult(result); downloadFile(data, 'playground-output.docx'); - setStatus(`Documento generado (${data.byteLength} bytes).`); + setStatus(`Done (${data.byteLength} bytes).`); setHasError(false); } finally { - URL.revokeObjectURL(moduleUrl); + URL.revokeObjectURL(url); } - } catch (error) { - const message = - error instanceof Error ? error.message : String(error); - setStatus(message); + } 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

-

- Playground minimo: escribe TypeScript, importa{' '} - docxml y genera un .docx. -

+
-
+
-
+
+

{status}

- -

{status}

); } diff --git a/playground/src/examples.ts b/playground/src/examples.ts new file mode 100644 index 0000000..f6cce49 --- /dev/null +++ b/playground/src/examples.ts @@ -0,0 +1,296 @@ +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; +} +`, + }, +]; diff --git a/playground/src/style.css b/playground/src/style.css index 14a5dfe..61a6073 100644 --- a/playground/src/style.css +++ b/playground/src/style.css @@ -33,11 +33,28 @@ body, gap: 0.75rem; } +.header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + .header h1 { margin: 0; font-size: 1.8rem; } +.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); +} + .header p { margin: 0.4rem 0 0; color: var(--muted); @@ -49,9 +66,10 @@ body, background: var(--surface); } -.actions { +.statusRow { display: flex; - justify-content: flex-end; + align-items: center; + gap: 0.65rem; } button { @@ -72,6 +90,7 @@ button:disabled { .status { margin: 0; + flex: 1; padding: 0.65rem 0.75rem; border: 1px solid var(--line); border-radius: 8px; @@ -84,3 +103,10 @@ button:disabled { .status.error { color: var(--danger); } + +@media (max-width: 700px) { + .statusRow { + flex-direction: column; + align-items: stretch; + } +} diff --git a/playground/tsconfig.json b/playground/tsconfig.json deleted file mode 100644 index 9ad6612..0000000 --- a/playground/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2021", - "module": "ESNext", - "moduleResolution": "Bundler", - "jsx": "react-jsx", - "jsxImportSource": "preact", - "strict": true, - "lib": ["ES2021", "DOM", "DOM.Iterable"], - "types": ["vite/client"] - }, - "include": ["src"] -} From a9487e342a4871f569748db6bb653ff0aa6dab5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20G=C3=B3mez?= Date: Tue, 19 May 2026 15:13:46 +0200 Subject: [PATCH 04/23] More examples --- playground/src/examples.ts | 138 +++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/playground/src/examples.ts b/playground/src/examples.ts index f6cce49..8544159 100644 --- a/playground/src/examples.ts +++ b/playground/src/examples.ts @@ -291,6 +291,144 @@ export default function build() { 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; +} `, }, ]; From 7ea72f4fb92ad5cdeb57d7a9fe1b9cad8e48338f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20G=C3=B3mez?= Date: Tue, 19 May 2026 15:15:18 +0200 Subject: [PATCH 05/23] Fixed versions --- playground/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/playground/package.json b/playground/package.json index c74e99d..78ff10b 100644 --- a/playground/package.json +++ b/playground/package.json @@ -9,12 +9,12 @@ "preview": "vite preview" }, "dependencies": { - "monaco-editor": "^0.53.0", - "preact": "^10.26.9", - "typescript": "^5.8.3" + "monaco-editor": "0.53.0", + "preact": "10.29.2", + "typescript": "5.9.3" }, "devDependencies": { - "@preact/preset-vite": "^2.10.1", - "vite": "^5.4.10" + "@preact/preset-vite": "2.10.5", + "vite": "5.4.21" } } From 1cebc9b2068be9cd63d3589dc267777d1275d4b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20G=C3=B3mez?= Date: Tue, 19 May 2026 15:20:48 +0200 Subject: [PATCH 06/23] Tweaks --- playground/src/App.tsx | 31 +++++++++++++++++++++++++++++++ playground/src/style.css | 36 +++++++++++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/playground/src/App.tsx b/playground/src/App.tsx index ec86dc7..d1f592a 100644 --- a/playground/src/App.tsx +++ b/playground/src/App.tsx @@ -272,6 +272,23 @@ export function App() {

{status}

+
+ +
); } diff --git a/playground/src/style.css b/playground/src/style.css index 61a6073..45020a5 100644 --- a/playground/src/style.css +++ b/playground/src/style.css @@ -16,7 +16,7 @@ html, body, #app { margin: 0; - min-height: 100%; + height: 100%; font-family: 'IBM Plex Sans', system-ui, sans-serif; background: var(--bg); color: var(--text); @@ -24,12 +24,16 @@ body, #app { padding: 1rem; + height: 100%; } .layout { - max-width: 980px; + width: 100%; + max-width: 1400px; + height: 100%; margin: 0 auto; display: grid; + grid-template-rows: auto 1fr auto auto; gap: 0.75rem; } @@ -38,6 +42,7 @@ body, align-items: center; justify-content: space-between; gap: 1rem; + flex-wrap: wrap; } .header h1 { @@ -61,9 +66,10 @@ body, } .editor { - height: min(65vh, 680px); + min-height: 300px; border: 1px solid var(--line); background: var(--surface); + overflow: hidden; } .statusRow { @@ -88,6 +94,11 @@ button:disabled { cursor: not-allowed; } +button.secondary { + background: var(--surface); + color: var(--accent); +} + .status { margin: 0; flex: 1; @@ -104,6 +115,25 @@ button:disabled { 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; +} + @media (max-width: 700px) { .statusRow { flex-direction: column; From 71fbb472b2be28a96019b3130806c772c10df74b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20G=C3=B3mez?= Date: Tue, 19 May 2026 15:49:06 +0200 Subject: [PATCH 07/23] zzz --- playground/.gitignore | 3 +- playground/deno.json | 5 +- playground/deno.lock | 21 ++++---- playground/scripts/generate-docxml-dts.ts | 64 ++++++++++++++++++++++ playground/src/App.tsx | 66 ++++------------------- 5 files changed, 90 insertions(+), 69 deletions(-) create mode 100644 playground/scripts/generate-docxml-dts.ts diff --git a/playground/.gitignore b/playground/.gitignore index 763301f..9a18220 100644 --- a/playground/.gitignore +++ b/playground/.gitignore @@ -1,2 +1,3 @@ dist/ -node_modules/ \ No newline at end of file +node_modules/ +public/ \ No newline at end of file diff --git a/playground/deno.json b/playground/deno.json index 0e778d1..b0b3c1c 100644 --- a/playground/deno.json +++ b/playground/deno.json @@ -1,7 +1,8 @@ { "tasks": { - "dev": "deno run -A --node-modules-dir=auto npm:vite", - "build": "deno run -A --node-modules-dir=auto npm:vite build", + "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", diff --git a/playground/deno.lock b/playground/deno.lock index f19d2e2..04353bb 100644 --- a/playground/deno.lock +++ b/playground/deno.lock @@ -1,12 +1,13 @@ { "version": "5", "specifiers": { - "npm:@preact/preset-vite@^2.10.1": "2.10.5_@babel+core@7.29.0_vite@5.4.21_preact@10.29.2", - "npm:monaco-editor@0.53": "0.53.0", - "npm:preact@^10.26.9": "10.29.2", - "npm:typescript@^5.8.3": "5.9.3", + "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.10": "5.4.21" + "npm:vite@5.4.21": "5.4.21" }, "npm": { "@babel/code-frame@7.29.0": { @@ -796,11 +797,11 @@ "workspace": { "packageJson": { "dependencies": [ - "npm:@preact/preset-vite@^2.10.1", - "npm:monaco-editor@0.53", - "npm:preact@^10.26.9", - "npm:typescript@^5.8.3", - "npm:vite@^5.4.10" + "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/scripts/generate-docxml-dts.ts b/playground/scripts/generate-docxml-dts.ts new file mode 100644 index 0000000..7acedb7 --- /dev/null +++ b/playground/scripts/generate-docxml-dts.ts @@ -0,0 +1,64 @@ +/// +import ts from 'typescript'; + +const outDir = new URL('../public/docxml/', import.meta.url); +const outDtsPath = new URL('docxml.d.ts', outDir); +const outVersionPath = new URL('version.json', 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); + await Deno.writeTextFile(outVersionPath, 'local'); + + console.log(`Generated ${outDtsPath.pathname} (${dts.length} chars)`); +} + +await main(); diff --git a/playground/src/App.tsx b/playground/src/App.tsx index d1f592a..61784c7 100644 --- a/playground/src/App.tsx +++ b/playground/src/App.tsx @@ -1,7 +1,6 @@ -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; -// @ts-ignore Vite resolves this at build time 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'; @@ -9,18 +8,7 @@ import { examples } from './examples.ts'; const DOCXML_RUNTIME_URL = 'https://esm.sh/jsr/@fontoxml/docxml?bundle'; -const INITIAL_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; -} -`; +const INITIAL_SOURCE = examples[0]?.source ?? ''; // The "Hello world" example. // Workers for Monaco (self as unknown as { MonacoEnvironment: unknown }).MonacoEnvironment = { @@ -44,34 +32,14 @@ if (!(globalThis as unknown as { Deno?: unknown }).Deno) { } /** - * Fetches the latest docxml version from JSR, downloads all .ts/.tsx source - * files, and feeds them to Monaco's TypeScript language service so that - * autocomplete, hover, and diagnostics work out of the box. + * Loads the precompiled docxml declaration bundle generated at build time + * and feeds it to Monaco's TypeScript language service. */ async function loadDocxmlTypes() { - const meta = (await fetch('https://jsr.io/@fontoxml/docxml/meta.json').then( - (r) => r.json() - )) as { latest: string }; - - const version = meta.latest; - - const versionMeta = (await fetch( - `https://jsr.io/@fontoxml/docxml/${version}_meta.json` - ).then((r) => r.json())) as { manifest: Record }; - - const tsFiles = Object.keys(versionMeta.manifest).filter( - (f) => f.endsWith('.ts') || f.endsWith('.tsx') - ); - - // Fetch all source files in parallel - const sources = await Promise.all( - tsFiles.map(async (path) => { - const text = await fetch( - `https://jsr.io/@fontoxml/docxml/${version}${path}` - ).then((r) => r.text()); - return { path, text }; - }) - ); + const [version, bundledDts] = await Promise.all([ + fetch('/docxml/version.json').then((r) => r.text()), + fetch('/docxml/docxml.d.ts').then((r) => r.text()), + ]); // Configure TS compiler options in Monaco const tsDefaults = monaco.languages.typescript.typescriptDefaults; @@ -86,9 +54,6 @@ async function loadDocxmlTypes() { allowImportingTsExtensions: true, strict: true, baseUrl: 'file:///', - paths: { - docxml: [`file:///docxml/${version}/mod.ts`], - }, }); tsDefaults.setDiagnosticsOptions({ @@ -98,20 +63,9 @@ async function loadDocxmlTypes() { tsDefaults.setEagerModelSync(true); - // Add all source files as extra libs - for (const { path, text } of sources) { - tsDefaults.addExtraLib(text, `file:///docxml/${version}${path}`); - } - - // Declare the bare 'docxml' module so `import ... from 'docxml'` resolves tsDefaults.addExtraLib( - [ - `declare module 'docxml' {`, - ` export * from 'file:///docxml/${version}/mod.ts';`, - ` export { default } from 'file:///docxml/${version}/mod.ts';`, - `}`, - ].join('\n'), - `file:///docxml/${version}/__module.d.ts` + bundledDts, + `file:///docxml/${version.trim()}/docxml-bundle.d.ts` ); } From 9ace2632c58dbed4009ee88abce7188b63828eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20G=C3=B3mez?= Date: Tue, 19 May 2026 15:54:09 +0200 Subject: [PATCH 08/23] Last cleanup --- playground/scripts/generate-docxml-dts.ts | 2 -- playground/src/App.tsx | 38 +++++++++++------------ playground/src/style.css | 5 --- 3 files changed, 18 insertions(+), 27 deletions(-) diff --git a/playground/scripts/generate-docxml-dts.ts b/playground/scripts/generate-docxml-dts.ts index 7acedb7..e1774ae 100644 --- a/playground/scripts/generate-docxml-dts.ts +++ b/playground/scripts/generate-docxml-dts.ts @@ -3,7 +3,6 @@ import ts from 'typescript'; const outDir = new URL('../public/docxml/', import.meta.url); const outDtsPath = new URL('docxml.d.ts', outDir); -const outVersionPath = new URL('version.json', outDir); const entryFilePath = new URL('../../mod.ts', import.meta.url).pathname; function emitSingleDts(): string { @@ -56,7 +55,6 @@ async function main() { const dts = emitSingleDts(); await Deno.writeTextFile(outDtsPath, dts); - await Deno.writeTextFile(outVersionPath, 'local'); console.log(`Generated ${outDtsPath.pathname} (${dts.length} chars)`); } diff --git a/playground/src/App.tsx b/playground/src/App.tsx index 61784c7..38bd77c 100644 --- a/playground/src/App.tsx +++ b/playground/src/App.tsx @@ -7,6 +7,7 @@ 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. @@ -36,10 +37,7 @@ if (!(globalThis as unknown as { Deno?: unknown }).Deno) { * and feeds it to Monaco's TypeScript language service. */ async function loadDocxmlTypes() { - const [version, bundledDts] = await Promise.all([ - fetch('/docxml/version.json').then((r) => r.text()), - fetch('/docxml/docxml.d.ts').then((r) => r.text()), - ]); + const bundledDts = await fetch(DOCXML_TYPES_URL).then((r) => r.text()); // Configure TS compiler options in Monaco const tsDefaults = monaco.languages.typescript.typescriptDefaults; @@ -63,10 +61,7 @@ async function loadDocxmlTypes() { tsDefaults.setEagerModelSync(true); - tsDefaults.addExtraLib( - bundledDts, - `file:///docxml/${version.trim()}/docxml-bundle.d.ts` - ); + tsDefaults.addExtraLib(bundledDts, 'file:///docxml/docxml-bundle.d.ts'); } async function normalizeResult(result: unknown): Promise { @@ -93,10 +88,7 @@ async function normalizeResult(result: unknown): Promise { ); } -function downloadFile(data: Uint8Array, fileName: string) { - const blob = new Blob([Uint8Array.from(data)], { - type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - }); +function downloadBlob(blob: Blob, fileName: string) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -105,6 +97,15 @@ function downloadFile(data: Uint8Array, fileName: string) { URL.revokeObjectURL(url); } +function downloadDocx(data: Uint8Array, fileName: string) { + downloadBlob( + new Blob([Uint8Array.from(data)], { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }), + fileName + ); +} + export function App() { const editorRef = useRef(null); const instanceRef = useRef( @@ -184,7 +185,7 @@ export function App() { setStatus('Generating DOCX...'); const result = await mod.default(); const data = await normalizeResult(result); - downloadFile(data, 'playground-output.docx'); + downloadDocx(data, 'playground-output.docx'); setStatus(`Done (${data.byteLength} bytes).`); setHasError(false); } finally { @@ -232,13 +233,10 @@ export function App() { onClick={() => { const source = instanceRef.current?.getValue(); if (!source) return; - const blob = new Blob([source], { type: 'text/plain' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'playground.ts'; - a.click(); - URL.revokeObjectURL(url); + downloadBlob( + new Blob([source], { type: 'text/plain' }), + 'playground.ts' + ); }} > Download .ts diff --git a/playground/src/style.css b/playground/src/style.css index 45020a5..f7dc00e 100644 --- a/playground/src/style.css +++ b/playground/src/style.css @@ -60,11 +60,6 @@ body, color: var(--text); } -.header p { - margin: 0.4rem 0 0; - color: var(--muted); -} - .editor { min-height: 300px; border: 1px solid var(--line); From 3b0fc340da8275d5271ea001dc15ade84be229b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20G=C3=B3mez?= Date: Tue, 19 May 2026 15:55:54 +0200 Subject: [PATCH 09/23] Allow custom runs --- .github/workflows/playground-pages.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/playground-pages.yml b/.github/workflows/playground-pages.yml index fd3fc63..eca350c 100644 --- a/.github/workflows/playground-pages.yml +++ b/.github/workflows/playground-pages.yml @@ -5,6 +5,11 @@ on: types: - published workflow_dispatch: + inputs: + ref: + description: Git ref to deploy (branch, tag, or commit SHA) + required: false + type: string permissions: contents: read @@ -25,6 +30,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || github.ref }} - name: Setup Deno uses: denoland/setup-deno@v1 From d516fcf4a669ef57084c58a0f52cd774fb1b64f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20G=C3=B3mez?= Date: Wed, 20 May 2026 10:31:51 +0200 Subject: [PATCH 10/23] Fix pipe? --- .github/workflows/playground-pages.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/playground-pages.yml b/.github/workflows/playground-pages.yml index eca350c..0c9ef12 100644 --- a/.github/workflows/playground-pages.yml +++ b/.github/workflows/playground-pages.yml @@ -26,7 +26,6 @@ jobs: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest - steps: - name: Checkout uses: actions/checkout@v4 From 12d46004b7a01eed933b7772eb690d33ce23b17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20G=C3=B3mez?= Date: Wed, 20 May 2026 10:44:58 +0200 Subject: [PATCH 11/23] Simplify the pipeline --- .github/workflows/playground-pages.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/playground-pages.yml b/.github/workflows/playground-pages.yml index 0c9ef12..374f5db 100644 --- a/.github/workflows/playground-pages.yml +++ b/.github/workflows/playground-pages.yml @@ -5,11 +5,6 @@ on: types: - published workflow_dispatch: - inputs: - ref: - description: Git ref to deploy (branch, tag, or commit SHA) - required: false - type: string permissions: contents: read @@ -33,9 +28,9 @@ jobs: ref: ${{ inputs.ref || github.ref }} - name: Setup Deno - uses: denoland/setup-deno@v1 + uses: denoland/setup-deno@v2 with: - deno-version: v2.x + deno-version: '2.7.2' - name: Build playground run: deno task playground:build From 9a2fb1b583d64c3c017c83642e54c397bf3cded6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20G=C3=B3mez?= Date: Wed, 20 May 2026 10:55:16 +0200 Subject: [PATCH 12/23] Add a header --- playground/src/App.tsx | 2 +- playground/src/style.css | 39 +++++++++++++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/playground/src/App.tsx b/playground/src/App.tsx index 38bd77c..ec33eda 100644 --- a/playground/src/App.tsx +++ b/playground/src/App.tsx @@ -209,7 +209,7 @@ export function App() { return (
-

Docxml Playground

+

Docxml Playground