@xiaobuyu/nodesign 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +140 -0
- package/bin/nodesign.js +176 -0
- package/package.json +104 -0
- package/server/api/README.md +45 -0
- package/server/api/_guard.js +75 -0
- package/server/api/admin.js +187 -0
- package/server/api/assets/docx-page.js +103 -0
- package/server/api/assets/helpers.js +22 -0
- package/server/api/assets/notes.js +144 -0
- package/server/api/assets.js +896 -0
- package/server/api/board.js +57 -0
- package/server/api/browse.js +138 -0
- package/server/api/canvas.js +444 -0
- package/server/api/chatai.js +324 -0
- package/server/api/exports/build-standalone.js +980 -0
- package/server/api/exports/cards.js +118 -0
- package/server/api/exports/docx-pdf.js +46 -0
- package/server/api/exports/export-page.js +142 -0
- package/server/api/exports/handoff.js +147 -0
- package/server/api/exports.js +690 -0
- package/server/api/instruction.js +75 -0
- package/server/api/local.js +115 -0
- package/server/api/me.js +116 -0
- package/server/api/memory.js +185 -0
- package/server/api/pending-changes.js +366 -0
- package/server/api/plugins.js +164 -0
- package/server/api/projects.js +220 -0
- package/server/api/publish.js +64 -0
- package/server/api/recent.js +73 -0
- package/server/api/sessions.js +589 -0
- package/server/api/skills.js +77 -0
- package/server/api/standalone-fit.js +168 -0
- package/server/api/turn-compose.js +180 -0
- package/server/api/turn-inflight.js +44 -0
- package/server/api/turn.js +669 -0
- package/server/auth/README.md +17 -0
- package/server/auth/middleware.js +138 -0
- package/server/auth/origin-guard.js +71 -0
- package/server/auth/session.js +108 -0
- package/server/auth/tier.js +113 -0
- package/server/auth/users-store.js +348 -0
- package/server/edit/README.md +29 -0
- package/server/engine/README.md +113 -0
- package/server/engine/agent/agent-shared.js +550 -0
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -0
- package/server/engine/agent/auto-mode-rules.js +78 -0
- package/server/engine/agent/context.js +418 -0
- package/server/engine/agent/events.js +464 -0
- package/server/engine/agent/hooks/canvas-validate.js +199 -0
- package/server/engine/agent/hooks/failure.js +120 -0
- package/server/engine/agent/hooks/file-events.js +75 -0
- package/server/engine/agent/hooks/lifecycle.js +233 -0
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -0
- package/server/engine/agent/hooks/post-guidance.js +206 -0
- package/server/engine/agent/hooks/post-subagent-report.js +104 -0
- package/server/engine/agent/hooks/post-trim.js +58 -0
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -0
- package/server/engine/agent/hooks/pre-defaults.js +84 -0
- package/server/engine/agent/hooks/pre-injectors.js +277 -0
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -0
- package/server/engine/agent/hooks/pre-starter-files.js +61 -0
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -0
- package/server/engine/agent/hooks/site-validate.js +94 -0
- package/server/engine/agent/hooks/tool-prompts.js +31 -0
- package/server/engine/agent/hooks/turn-state-memory.js +56 -0
- package/server/engine/agent/hooks/user-prompt-submit.js +215 -0
- package/server/engine/agent/hooks.js +354 -0
- package/server/engine/agent/init-contract.js +69 -0
- package/server/engine/agent/isolation.js +159 -0
- package/server/engine/agent/model-context.js +359 -0
- package/server/engine/agent/model-table.js +292 -0
- package/server/engine/agent/plugin-loader.js +266 -0
- package/server/engine/agent/prompts/nodesign-prelude.md +336 -0
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -0
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -0
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -0
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -0
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -0
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -0
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -0
- package/server/engine/agent/prompts/tools/site-reference.md +106 -0
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -0
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -0
- package/server/engine/agent/session-loop.js +1024 -0
- package/server/engine/agent/session-model.js +164 -0
- package/server/engine/agent/skill.js +204 -0
- package/server/engine/agent/system-prompts.js +86 -0
- package/server/engine/agent/task-events.js +78 -0
- package/server/engine/agents/ds-extractor.md +132 -0
- package/server/engine/agents/explorer.md +190 -0
- package/server/engine/agents/index.js +230 -0
- package/server/engine/agents/schemas/design-system.json +156 -0
- package/server/engine/agents/schemas/tweak-schema.json +113 -0
- package/server/engine/agents/tweak-proposer.md +127 -0
- package/server/engine/agents/vision-checker.md +254 -0
- package/server/engine/browse/capture.js +422 -0
- package/server/engine/browse/card.js +137 -0
- package/server/engine/browse/handover.js +59 -0
- package/server/engine/browse/page-digest.js +119 -0
- package/server/engine/browse/refs.js +196 -0
- package/server/engine/browse/registry.js +307 -0
- package/server/engine/browse/screencast.js +170 -0
- package/server/engine/browse/state.js +121 -0
- package/server/engine/chatai/chat-log.js +79 -0
- package/server/engine/chatai/index.js +193 -0
- package/server/engine/chatai/openai-compat.js +152 -0
- package/server/engine/chatai/orchestrate.js +309 -0
- package/server/engine/chatai/perform.js +40 -0
- package/server/engine/chatai/summarize.js +79 -0
- package/server/engine/mcp/capability-gate.js +52 -0
- package/server/engine/mcp/index.js +339 -0
- package/server/engine/mcp/param-sanitizer.js +142 -0
- package/server/engine/mcp/tools/arrange-on-board.js +124 -0
- package/server/engine/mcp/tools/artifact-session.js +278 -0
- package/server/engine/mcp/tools/browse-computer.js +421 -0
- package/server/engine/mcp/tools/browse-find-batch.js +183 -0
- package/server/engine/mcp/tools/browse-screenshot.js +157 -0
- package/server/engine/mcp/tools/browse.js +480 -0
- package/server/engine/mcp/tools/build-docx.js +101 -0
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -0
- package/server/engine/mcp/tools/create-on-board.js +155 -0
- package/server/engine/mcp/tools/crystallize-skill.js +168 -0
- package/server/engine/mcp/tools/deliver-files.js +0 -0
- package/server/engine/mcp/tools/explain-style.js +237 -0
- package/server/engine/mcp/tools/export-handoff.js +133 -0
- package/server/engine/mcp/tools/expose-tweaks.js +149 -0
- package/server/engine/mcp/tools/generate-image.js +842 -0
- package/server/engine/mcp/tools/get-computed-styles.js +164 -0
- package/server/engine/mcp/tools/get-pending-changes.js +205 -0
- package/server/engine/mcp/tools/h3box-ssh.js +84 -0
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -0
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -0
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -0
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -0
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -0
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -0
- package/server/engine/mcp/tools/helpers/rembg.js +273 -0
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -0
- package/server/engine/mcp/tools/highlight.js +68 -0
- package/server/engine/mcp/tools/list-pages.js +222 -0
- package/server/engine/mcp/tools/lookup-tags.js +59 -0
- package/server/engine/mcp/tools/navigate-to-page.js +60 -0
- package/server/engine/mcp/tools/organize-board.js +64 -0
- package/server/engine/mcp/tools/paint-still.js +434 -0
- package/server/engine/mcp/tools/pin-to-board.js +130 -0
- package/server/engine/mcp/tools/preview-deck.js +130 -0
- package/server/engine/mcp/tools/profile-scroll.js +305 -0
- package/server/engine/mcp/tools/publish-site.js +102 -0
- package/server/engine/mcp/tools/query-elements.js +195 -0
- package/server/engine/mcp/tools/read-board.js +103 -0
- package/server/engine/mcp/tools/read-document.js +93 -0
- package/server/engine/mcp/tools/read-page.js +264 -0
- package/server/engine/mcp/tools/read-tavern-json.js +143 -0
- package/server/engine/mcp/tools/record-decision.js +140 -0
- package/server/engine/mcp/tools/relate-on-board.js +110 -0
- package/server/engine/mcp/tools/remove-background.js +391 -0
- package/server/engine/mcp/tools/report-issue.js +115 -0
- package/server/engine/mcp/tools/roll-film.js +266 -0
- package/server/engine/mcp/tools/screenshot-docx.js +138 -0
- package/server/engine/mcp/tools/screenshot-url.js +195 -0
- package/server/engine/mcp/tools/screenshot.js +583 -0
- package/server/engine/mcp/tools/tier-gate.js +96 -0
- package/server/engine/mcp/tools/trace-motion.js +215 -0
- package/server/engine/mcp/tools/web-search.js +548 -0
- package/server/engine/motion/inventory.js +349 -0
- package/server/engine/perception/session.js +235 -0
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -0
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -0
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -0
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -0
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -0
- package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -0
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -0
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -0
- package/server/engine/runs/active-runs.js +698 -0
- package/server/engine/runs/live-turn.js +252 -0
- package/server/engine/runs/store.js +364 -0
- package/server/engine/runs/turn-relay.js +217 -0
- package/server/engine/runtime/workspace.js +164 -0
- package/server/index.js +235 -0
- package/server/lib/artifact-file-path.js +80 -0
- package/server/lib/artifact-target.js +284 -0
- package/server/lib/asset-refs.js +239 -0
- package/server/lib/async-queue.js +104 -0
- package/server/lib/auto-relations.js +130 -0
- package/server/lib/binding-types.js +60 -0
- package/server/lib/board-kind-sizes.js +62 -0
- package/server/lib/board-relations.js +133 -0
- package/server/lib/browse-proxy.js +180 -0
- package/server/lib/canvas-id.js +36 -0
- package/server/lib/cover.js +227 -0
- package/server/lib/danbooru-tags.js +291 -0
- package/server/lib/doc-extract.js +156 -0
- package/server/lib/docx/build-from-source.js +260 -0
- package/server/lib/docx/build.js +490 -0
- package/server/lib/docx/dump-styles.js +287 -0
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -0
- package/server/lib/docx/merge-runs.js +129 -0
- package/server/lib/docx/numbering.js +218 -0
- package/server/lib/docx/order.js +152 -0
- package/server/lib/docx/rawzip.js +172 -0
- package/server/lib/docx/render.js +92 -0
- package/server/lib/docx/text-lint.js +225 -0
- package/server/lib/docx/tokens.js +335 -0
- package/server/lib/docx/units.js +29 -0
- package/server/lib/docx/xml.js +291 -0
- package/server/lib/docx-pages.js +187 -0
- package/server/lib/export-collect.js +289 -0
- package/server/lib/export-package.js +210 -0
- package/server/lib/html-srcset.js +145 -0
- package/server/lib/image-variant.js +460 -0
- package/server/lib/ingress/forward-openai-chat.js +305 -0
- package/server/lib/ingress/openai-chat.js +491 -0
- package/server/lib/ingress/session-notice.js +66 -0
- package/server/lib/ingress/session-routes.js +68 -0
- package/server/lib/ingress/slot-probe.js +130 -0
- package/server/lib/ingress/upstream-billing.js +52 -0
- package/server/lib/ingress/upstream-fail-streak.js +72 -0
- package/server/lib/ingress/upstream-truncation.js +48 -0
- package/server/lib/issues-store.js +182 -0
- package/server/lib/kinds/deck.js +72 -0
- package/server/lib/kinds/docx.js +283 -0
- package/server/lib/kinds/file-kinds.js +113 -0
- package/server/lib/kinds/index.js +202 -0
- package/server/lib/kinds/site.js +161 -0
- package/server/lib/model-ingress.js +543 -0
- package/server/lib/moderation.js +255 -0
- package/server/lib/notice-store.js +103 -0
- package/server/lib/plugin-install.js +104 -0
- package/server/lib/plugin-validator.js +721 -0
- package/server/lib/publish-store.js +115 -0
- package/server/lib/quick-summary.js +52 -0
- package/server/lib/quota.js +270 -0
- package/server/lib/rate-window.js +29 -0
- package/server/lib/reference-assets.js +92 -0
- package/server/lib/region-shot.js +135 -0
- package/server/lib/safe-path.js +73 -0
- package/server/lib/sdk-session.js +33 -0
- package/server/lib/showcase-store.js +92 -0
- package/server/lib/site-publish.js +465 -0
- package/server/lib/ssrf-guard.js +432 -0
- package/server/lib/task-scan.js +158 -0
- package/server/lib/tavern-json.js +154 -0
- package/server/lib/video-variant.js +237 -0
- package/server/lib/workspace-path.js +33 -0
- package/server/ops/fix-sdk-musl.mjs +40 -0
- package/server/ops/install-macos-fonts.sh +114 -0
- package/server/ops/macos-fonts.conf +336 -0
- package/server/ops/sandbox-shim/bwrap +57 -0
- package/server/projects/assets-summary.js +119 -0
- package/server/projects/auto-name.js +53 -0
- package/server/projects/board-store.js +640 -0
- package/server/projects/move-entry.js +103 -0
- package/server/projects/store.js +270 -0
- package/server/projects/ui-config.js +54 -0
- package/server/projects/workspace-templates.js +53 -0
- package/server/projects/workspace.js +1025 -0
- package/server/runtime/capabilities.js +153 -0
- package/server/runtime/local-config.js +177 -0
- package/server/runtime/local-env.js +88 -0
- package/server/runtime/platform.js +342 -0
- package/server/runtime/profile.js +72 -0
- package/server/services/rembg-launcher.js +313 -0
- package/server/services/rembg-service.py +334 -0
- package/server/shared/README.md +25 -0
- package/server/shared/deck.js +54 -0
- package/server/shared/time.js +49 -0
- package/server/style-pipeline/README.md +53 -0
- package/server/ws/broker.js +36 -0
- package/server/ws/browse-channel.js +177 -0
- package/server/ws/index.js +386 -0
- package/web/README.md +89 -0
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +11 -0
- package/web/dist/assets/DeckWindow-4G0xX7P3.js +16 -0
- package/web/dist/assets/DocxWindow-BXQsoPkh.js +16 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/SiteWindow-D6eS-2gM.js +18 -0
- package/web/dist/assets/again-D8tfgKKO.webp +0 -0
- package/web/dist/assets/caveat-nd-CP6HlsNg.woff2 +0 -0
- package/web/dist/assets/clock-xJhvfYpS.webp +0 -0
- package/web/dist/assets/film-cut-BBKMwH7q.webp +0 -0
- package/web/dist/assets/film-still-BZ_DL9Tt.webp +0 -0
- package/web/dist/assets/index-C0KUf6Wd.css +1 -0
- package/web/dist/assets/index-Cbl3ATvp.js +2125 -0
- package/web/dist/assets/ink-desk-BRbxeDsR.webp +0 -0
- package/web/dist/assets/ink-night-Bz4BT3XH.webp +0 -0
- package/web/dist/assets/ink-portrait-C0DM4V8Q.webp +0 -0
- package/web/dist/assets/longcang-regular-C0uvTuKa.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
- package/web/dist/assets/pending-edit-apply-DTjo2geW.js +31 -0
- package/web/dist/assets/question-8H4HSqxh.webp +0 -0
- package/web/dist/assets/reject-BaPJqElj.webp +0 -0
- package/web/dist/assets/rotate-cw-LMT3x1Zj.js +11 -0
- package/web/dist/assets/rp-portrait-CbrGpew2.webp +0 -0
- package/web/dist/assets/rp-street-C4jVKjxx.webp +0 -0
- package/web/dist/assets/square-dashed-mouse-pointer-CBoAkfhi.js +11 -0
- package/web/dist/assets/tangle-DIxHVNuT.webp +0 -0
- package/web/dist/assets/thisone-BylZKglV.webp +0 -0
- package/web/dist/assets/thumb-DO1wfK1I.webp +0 -0
- package/web/dist/index.html +18 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* build.js — token JSON + 内容块 → 完整 docx(Buffer)。
|
|
3
|
+
*
|
|
4
|
+
* 所有属性容器(pPr/rPr/sectPr/style/…)都过 order.sortChildren(),
|
|
5
|
+
* 出厂前可用 order.checkOrder() 体检;对外证据是 OpenXmlValidator 0 错。
|
|
6
|
+
*
|
|
7
|
+
* 内容块模型(generation 路线的雏形,编辑路线不走这里):
|
|
8
|
+
* {t:'p', style?, blocks 见 buildPara} 段落
|
|
9
|
+
* {t:'table', widthsTwip:[..], rows:[[cell]]} 表格;cell = string | {text, style?}
|
|
10
|
+
* {t:'pageBreak'}
|
|
11
|
+
* 段落 runs:string | {text, bold?, italic?, color?, font?, sizePt?, em?, underline?}
|
|
12
|
+
* | {fld:'PAGE'|'NUMPAGES'}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { elem, textNode, serializeNode } from './xml.js';
|
|
16
|
+
import { sortChildren } from './order.js';
|
|
17
|
+
import { ptToHalf, ptToTwip, sizePt as toPt, PAGE_SIZES } from './units.js';
|
|
18
|
+
import { buildNumberingXml, numIdsFor } from './numbering.js';
|
|
19
|
+
|
|
20
|
+
const W = 'xmlns:w';
|
|
21
|
+
const NS_W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
|
|
22
|
+
const NS_R = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
|
|
23
|
+
const DECL = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r\n';
|
|
24
|
+
|
|
25
|
+
/* ── 低层小件 ─────────────────────────────────── */
|
|
26
|
+
|
|
27
|
+
function val(name, v) { return elem(name, [['w:val', v]]); }
|
|
28
|
+
function flag(name, on) { return on === false ? elem(name, [['w:val', '0']]) : elem(name); }
|
|
29
|
+
|
|
30
|
+
function resolveFont(tokens, ref) {
|
|
31
|
+
if (ref == null) return null;
|
|
32
|
+
const f = typeof ref === 'string' ? tokens.fonts?.[ref] : ref;
|
|
33
|
+
if (!f) throw new Error(`unknown font slot: ${ref}`);
|
|
34
|
+
return f;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function rFontsEl(f) {
|
|
38
|
+
const attrs = [];
|
|
39
|
+
if (f.ascii) attrs.push(['w:ascii', f.ascii]);
|
|
40
|
+
if (f.eastAsia) attrs.push(['w:eastAsia', f.eastAsia]);
|
|
41
|
+
attrs.push(['w:hAnsi', f.hAnsi ?? f.ascii ?? f.eastAsia]);
|
|
42
|
+
if (f.cs ?? f.ascii) attrs.push(['w:cs', f.cs ?? f.ascii]);
|
|
43
|
+
return elem('w:rFonts', attrs);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** run token → w:rPr(无内容时返回 null) */
|
|
47
|
+
export function buildRPr(tokens, run, { forStyle = false } = {}) {
|
|
48
|
+
if (!run) return null;
|
|
49
|
+
const kids = [];
|
|
50
|
+
const f = resolveFont(tokens, run.font);
|
|
51
|
+
if (f) kids.push(rFontsEl(f));
|
|
52
|
+
if (run.bold != null) kids.push(flag('w:b', run.bold));
|
|
53
|
+
if (run.italic != null) kids.push(flag('w:i', run.italic));
|
|
54
|
+
if (run.caps) kids.push(elem('w:caps'));
|
|
55
|
+
if (run.smallCaps) kids.push(elem('w:smallCaps'));
|
|
56
|
+
if (run.strike) kids.push(elem('w:strike'));
|
|
57
|
+
if (run.color) kids.push(val('w:color', run.color));
|
|
58
|
+
if (run.spacingTwip != null) kids.push(val('w:spacing', run.spacingTwip));
|
|
59
|
+
if (run.kernPt != null) kids.push(val('w:kern', ptToHalf(run.kernPt)));
|
|
60
|
+
if (run.sizePt != null) {
|
|
61
|
+
kids.push(val('w:sz', ptToHalf(toPt(run.sizePt))));
|
|
62
|
+
kids.push(val('w:szCs', ptToHalf(toPt(run.sizePt))));
|
|
63
|
+
}
|
|
64
|
+
if (run.highlight) kids.push(val('w:highlight', run.highlight));
|
|
65
|
+
if (run.underline) kids.push(val('w:u', run.underline === true ? 'single' : run.underline));
|
|
66
|
+
if (run.vertAlign) kids.push(val('w:vertAlign', run.vertAlign));
|
|
67
|
+
if (run.em) kids.push(val('w:em', run.em));
|
|
68
|
+
if (!kids.length) return null;
|
|
69
|
+
return sortChildren(elem('w:rPr', [], kids));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const CJK_FLAGS = [
|
|
73
|
+
['kinsoku', 'w:kinsoku'], ['wordWrap', 'w:wordWrap'], ['overflowPunct', 'w:overflowPunct'],
|
|
74
|
+
['autoSpaceDE', 'w:autoSpaceDE'], ['autoSpaceDN', 'w:autoSpaceDN'],
|
|
75
|
+
['adjustRightInd', 'w:adjustRightInd'], ['snapToGrid', 'w:snapToGrid'],
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 沿 basedOn 链算风格的有效字号(磅)。块级 run 覆盖 > 风格链 > base。
|
|
80
|
+
*/
|
|
81
|
+
export function effectiveSizePt(tokens, styleId, runSizePt) {
|
|
82
|
+
if (runSizePt != null) return toPt(runSizePt);
|
|
83
|
+
let id = styleId;
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
while (id && tokens.styles?.[id] && !seen.has(id)) {
|
|
86
|
+
seen.add(id);
|
|
87
|
+
const st = tokens.styles[id];
|
|
88
|
+
if (st.run?.sizePt != null) return toPt(st.run.sizePt);
|
|
89
|
+
id = st.basedOn;
|
|
90
|
+
}
|
|
91
|
+
return toPt(tokens.base?.sizePt ?? 12);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 【实测军规】LO 25.2 无视单独的 w:firstLineChars/leftChars(lab/02 A/B:
|
|
96
|
+
* chars-only 缩进 0pt,chars+twip 精确 32pt)。Word 里 chars 优先于 twip。
|
|
97
|
+
* 所以字符单位缩进一律双发:chars(Word 真语义,随字号缩放)+ 按有效字号
|
|
98
|
+
* 算好的 twip 兜底(LO/QC 与旧阅读器用)。
|
|
99
|
+
*/
|
|
100
|
+
function indentAttrs(i, sizePt) {
|
|
101
|
+
const attrs = [];
|
|
102
|
+
const chars2twip = (chars) => Math.round((chars / 100) * sizePt * 20);
|
|
103
|
+
if (i.leftChars != null) {
|
|
104
|
+
attrs.push(['w:left', i.leftTwip ?? chars2twip(i.leftChars)]);
|
|
105
|
+
attrs.push(['w:leftChars', i.leftChars]);
|
|
106
|
+
} else if (i.leftTwip != null) attrs.push(['w:left', i.leftTwip]);
|
|
107
|
+
if (i.rightChars != null) {
|
|
108
|
+
attrs.push(['w:right', i.rightTwip ?? chars2twip(i.rightChars)]);
|
|
109
|
+
attrs.push(['w:rightChars', i.rightChars]);
|
|
110
|
+
} else if (i.rightTwip != null) attrs.push(['w:right', i.rightTwip]);
|
|
111
|
+
if (i.hangingChars != null) {
|
|
112
|
+
attrs.push(['w:hanging', i.hangingTwip ?? chars2twip(i.hangingChars)]);
|
|
113
|
+
attrs.push(['w:hangingChars', i.hangingChars]);
|
|
114
|
+
} else if (i.hangingTwip != null) attrs.push(['w:hanging', i.hangingTwip]);
|
|
115
|
+
if (i.firstLineChars != null) {
|
|
116
|
+
attrs.push(['w:firstLine', i.firstLineTwip ?? chars2twip(i.firstLineChars)]);
|
|
117
|
+
attrs.push(['w:firstLineChars', i.firstLineChars]);
|
|
118
|
+
} else if (i.firstLineTwip != null) attrs.push(['w:firstLine', i.firstLineTwip]);
|
|
119
|
+
return attrs;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** para token → w:pPr 子元素数组(不含 pStyle/rPr,调用方拼)
|
|
123
|
+
* ctx.sizePt = 本段有效字号,字符单位缩进换算 twip 兜底用 */
|
|
124
|
+
function paraProps(para, ctx = { sizePt: 12 }) {
|
|
125
|
+
const kids = [];
|
|
126
|
+
if (!para) return kids;
|
|
127
|
+
if (para.keepNext) kids.push(elem('w:keepNext'));
|
|
128
|
+
if (para.keepLines) kids.push(elem('w:keepLines'));
|
|
129
|
+
if (para.pageBreakBefore) kids.push(elem('w:pageBreakBefore'));
|
|
130
|
+
if (para.widowControl != null) kids.push(flag('w:widowControl', para.widowControl));
|
|
131
|
+
if (para.list) {
|
|
132
|
+
// agent 按**名字**引用编号(`list: {name:'条款', ilvl:1}` 或简写 `list:'条款'`),
|
|
133
|
+
// numId 是 OOXML 的实现细节,这里现算。名字对不上是 token 校验的活,
|
|
134
|
+
// 到这儿只可能是内部不一致,宁可产一个显式错误也别产一个悬空 numId。
|
|
135
|
+
const spec = typeof para.list === 'string' ? { name: para.list } : para.list;
|
|
136
|
+
const numId = ctx.numIds?.get(spec.name);
|
|
137
|
+
if (numId == null) throw new Error(`para.list: 没有名为 '${spec.name}' 的编号定义`);
|
|
138
|
+
kids.push(elem('w:numPr', [], [val('w:ilvl', spec.ilvl ?? 0), val('w:numId', numId)]));
|
|
139
|
+
}
|
|
140
|
+
if (para.borders) {
|
|
141
|
+
const sides = [];
|
|
142
|
+
for (const side of ['top', 'left', 'bottom', 'right']) {
|
|
143
|
+
const b = para.borders[side];
|
|
144
|
+
if (!b) continue;
|
|
145
|
+
sides.push(elem(`w:${side}`, [
|
|
146
|
+
['w:val', b.style ?? 'single'], ['w:sz', b.sizePt8 ?? 4],
|
|
147
|
+
['w:space', b.spacePt ?? 0], ['w:color', b.color ?? '000000'],
|
|
148
|
+
]));
|
|
149
|
+
}
|
|
150
|
+
if (sides.length) kids.push(sortChildren(elem('w:pBdr', [], sides)));
|
|
151
|
+
}
|
|
152
|
+
if (para.shading) {
|
|
153
|
+
kids.push(elem('w:shd', [['w:val', 'clear'], ['w:color', 'auto'], ['w:fill', para.shading]]));
|
|
154
|
+
}
|
|
155
|
+
if (para.tabs?.length) {
|
|
156
|
+
kids.push(elem('w:tabs', [], para.tabs.map((t) => elem('w:tab', [
|
|
157
|
+
['w:val', t.val ?? 'left'], ...(t.leader ? [['w:leader', t.leader]] : []), ['w:pos', t.pos],
|
|
158
|
+
]))));
|
|
159
|
+
}
|
|
160
|
+
for (const [key, name] of CJK_FLAGS) {
|
|
161
|
+
if (para.cjk && para.cjk[key] != null) kids.push(flag(name, para.cjk[key]));
|
|
162
|
+
}
|
|
163
|
+
if (para.spacing) {
|
|
164
|
+
const s = para.spacing;
|
|
165
|
+
const attrs = [];
|
|
166
|
+
if (s.beforePt != null) attrs.push(['w:before', ptToTwip(s.beforePt)]);
|
|
167
|
+
if (s.beforeLines != null) attrs.push(['w:beforeLines', s.beforeLines]);
|
|
168
|
+
if (s.afterPt != null) attrs.push(['w:after', ptToTwip(s.afterPt)]);
|
|
169
|
+
if (s.afterLines != null) attrs.push(['w:afterLines', s.afterLines]);
|
|
170
|
+
if (s.line != null) {
|
|
171
|
+
const rule = s.lineRule ?? 'multiple';
|
|
172
|
+
attrs.push(['w:line', rule === 'multiple' ? Math.round(s.line * 240) : ptToTwip(s.line)]);
|
|
173
|
+
attrs.push(['w:lineRule', rule === 'multiple' ? 'auto' : rule]);
|
|
174
|
+
}
|
|
175
|
+
if (attrs.length) kids.push(elem('w:spacing', attrs));
|
|
176
|
+
}
|
|
177
|
+
if (para.indent) {
|
|
178
|
+
const attrs = indentAttrs(para.indent, ctx.sizePt);
|
|
179
|
+
if (attrs.length) kids.push(elem('w:ind', attrs));
|
|
180
|
+
}
|
|
181
|
+
if (para.contextualSpacing) kids.push(elem('w:contextualSpacing'));
|
|
182
|
+
if (para.align) kids.push(val('w:jc', para.align));
|
|
183
|
+
if (para.outlineLevel != null) kids.push(val('w:outlineLvl', para.outlineLevel));
|
|
184
|
+
return kids;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* ── styles.xml ───────────────────────────────── */
|
|
188
|
+
|
|
189
|
+
export function buildStylesXml(tokens) {
|
|
190
|
+
const base = tokens.base ?? {};
|
|
191
|
+
const rDef = [];
|
|
192
|
+
const bf = resolveFont(tokens, base.font ?? 'body');
|
|
193
|
+
if (bf) rDef.push(rFontsEl(bf));
|
|
194
|
+
if (base.kernPt != null) rDef.push(val('w:kern', ptToHalf(base.kernPt)));
|
|
195
|
+
if (base.sizePt != null) {
|
|
196
|
+
rDef.push(val('w:sz', ptToHalf(toPt(base.sizePt))));
|
|
197
|
+
rDef.push(val('w:szCs', ptToHalf(toPt(base.sizePt))));
|
|
198
|
+
}
|
|
199
|
+
if (base.color && base.color !== '000000') rDef.push(val('w:color', base.color));
|
|
200
|
+
if (tokens.lang) {
|
|
201
|
+
rDef.push(elem('w:lang', [
|
|
202
|
+
['w:val', tokens.lang.latin ?? 'en-US'],
|
|
203
|
+
['w:eastAsia', tokens.lang.eastAsia ?? 'zh-CN'],
|
|
204
|
+
['w:bidi', 'ar-SA'],
|
|
205
|
+
]));
|
|
206
|
+
}
|
|
207
|
+
const pDefKids = [];
|
|
208
|
+
for (const [key, name] of CJK_FLAGS) {
|
|
209
|
+
if (base.cjk && base.cjk[key] != null) pDefKids.push(flag(name, base.cjk[key]));
|
|
210
|
+
}
|
|
211
|
+
if (base.spacing) pDefKids.push(...paraProps({ spacing: base.spacing }));
|
|
212
|
+
|
|
213
|
+
const styleEls = [];
|
|
214
|
+
for (const [id, st] of Object.entries(tokens.styles ?? {})) {
|
|
215
|
+
const kids = [val('w:name', st.name ?? id)];
|
|
216
|
+
if (st.basedOn) kids.push(val('w:basedOn', st.basedOn));
|
|
217
|
+
if (st.next) kids.push(val('w:next', st.next));
|
|
218
|
+
if (st.link) kids.push(val('w:link', st.link));
|
|
219
|
+
if (st.uiPriority != null) kids.push(val('w:uiPriority', st.uiPriority));
|
|
220
|
+
if (st.qFormat) kids.push(elem('w:qFormat'));
|
|
221
|
+
const pKids = paraProps(st.para, { sizePt: effectiveSizePt(tokens, id), numIds: numIdsFor(tokens) });
|
|
222
|
+
if (pKids.length) kids.push(sortChildren(elem('w:pPr', [], pKids)));
|
|
223
|
+
const rPr = buildRPr(tokens, st.run, { forStyle: true });
|
|
224
|
+
if (rPr) kids.push(rPr);
|
|
225
|
+
const el = sortChildren(elem('w:style', [['w:type', st.type ?? 'paragraph'], ['w:styleId', id]], kids));
|
|
226
|
+
styleEls.push(el);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const root = elem('w:styles', [[W, NS_W]], [
|
|
230
|
+
elem('w:docDefaults', [], [
|
|
231
|
+
elem('w:rPrDefault', [], rDef.length ? [sortChildren(elem('w:rPr', [], rDef))] : []),
|
|
232
|
+
elem('w:pPrDefault', [], pDefKids.length ? [sortChildren(elem('w:pPr', [], pDefKids))] : []),
|
|
233
|
+
]),
|
|
234
|
+
...styleEls,
|
|
235
|
+
]);
|
|
236
|
+
return DECL + serializeNode(root);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* ── document.xml ─────────────────────────────── */
|
|
240
|
+
|
|
241
|
+
function buildRun(tokens, r, links) {
|
|
242
|
+
if (typeof r === 'string') r = { text: r };
|
|
243
|
+
const kids = [];
|
|
244
|
+
const rPr = buildRPr(tokens, r);
|
|
245
|
+
if (rPr) kids.push(rPr);
|
|
246
|
+
if (r.fld) {
|
|
247
|
+
return elem('w:fldSimple', [['w:instr', ` ${r.fld} `]], [
|
|
248
|
+
elem('w:r', [], rPr ? [rPr, elem('w:t', [], [textNode('1')])] : [elem('w:t', [], [textNode('1')])]),
|
|
249
|
+
]);
|
|
250
|
+
}
|
|
251
|
+
if (r.br) kids.push(elem('w:br', r.br === true ? [] : [['w:type', r.br]]));
|
|
252
|
+
if (r.text != null) {
|
|
253
|
+
const t = String(r.text);
|
|
254
|
+
const attrs = /^\s|\s$/.test(t) ? [['xml:space', 'preserve']] : [];
|
|
255
|
+
kids.push(elem('w:t', attrs, [textNode(t)]));
|
|
256
|
+
}
|
|
257
|
+
const runEl = elem('w:r', [], kids);
|
|
258
|
+
// 超链接:run 包进 w:hyperlink,目标在关系表里(TargetMode="External")。
|
|
259
|
+
// 视觉刻意不动 —— 中文简历那种「可点但不染色」不该被迫对抗默认蓝下划线;
|
|
260
|
+
// 要经典观感就照常写 color/underline
|
|
261
|
+
if (r.link) {
|
|
262
|
+
const id = links?.get(r.link);
|
|
263
|
+
// 到这儿查不到只可能是收集和构建不一致 —— 宁可炸也不产悬空 r:id(Word 报文档损坏)
|
|
264
|
+
if (id == null) throw new Error(`run.link 没登记进关系表:${r.link}`);
|
|
265
|
+
return elem('w:hyperlink', [['r:id', id], ['w:history', '1']], [runEl]);
|
|
266
|
+
}
|
|
267
|
+
return runEl;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function buildPara(tokens, block, links) {
|
|
271
|
+
const pKids = [];
|
|
272
|
+
const propKids = [];
|
|
273
|
+
if (block.style) propKids.push(val('w:pStyle', block.style));
|
|
274
|
+
const firstRun = (block.runs ?? []).find((r) => typeof r === 'object' && r.sizePt != null);
|
|
275
|
+
const ctx = { sizePt: effectiveSizePt(tokens, block.style, block.sizePt ?? firstRun?.sizePt), numIds: numIdsFor(tokens) };
|
|
276
|
+
propKids.push(...paraProps(block, ctx)); // 直接格式(block 上的 align/indent/... 覆盖)
|
|
277
|
+
if (propKids.length) pKids.push(sortChildren(elem('w:pPr', [], propKids)));
|
|
278
|
+
const runs = block.runs ?? (block.text != null ? [block.text] : []);
|
|
279
|
+
for (const r of runs) pKids.push(buildRun(tokens, r, links));
|
|
280
|
+
return elem('w:p', [], pKids);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function buildTable(tokens, block, links) {
|
|
284
|
+
const widths = block.widthsTwip;
|
|
285
|
+
const kids = [
|
|
286
|
+
sortChildren(elem('w:tblPr', [], [
|
|
287
|
+
elem('w:tblW', [['w:w', widths.reduce((a, b) => a + b, 0)], ['w:type', 'dxa']]),
|
|
288
|
+
elem('w:tblBorders', [], ['top', 'left', 'bottom', 'right', 'insideH', 'insideV'].map(
|
|
289
|
+
(s) => elem(`w:${s}`, [['w:val', 'single'], ['w:sz', 4], ['w:space', 0], ['w:color', '000000']]),
|
|
290
|
+
)),
|
|
291
|
+
elem('w:tblLayout', [['w:type', 'fixed']]),
|
|
292
|
+
])),
|
|
293
|
+
elem('w:tblGrid', [], widths.map((w) => elem('w:gridCol', [['w:w', w]]))),
|
|
294
|
+
];
|
|
295
|
+
for (const row of block.rows) {
|
|
296
|
+
const tcs = row.map((cell, i) => {
|
|
297
|
+
const c = typeof cell === 'string' ? { text: cell } : cell;
|
|
298
|
+
return elem('w:tc', [], [
|
|
299
|
+
sortChildren(elem('w:tcPr', [], [
|
|
300
|
+
elem('w:tcW', [['w:w', widths[i]], ['w:type', 'dxa']]),
|
|
301
|
+
...(c.shading ? [elem('w:shd', [['w:val', 'clear'], ['w:color', 'auto'], ['w:fill', c.shading]])] : []),
|
|
302
|
+
val('w:vAlign', 'center'),
|
|
303
|
+
])),
|
|
304
|
+
buildPara(tokens, { ...c, indent: c.indent ?? {} }, links),
|
|
305
|
+
]);
|
|
306
|
+
});
|
|
307
|
+
kids.push(elem('w:tr', [], tcs));
|
|
308
|
+
}
|
|
309
|
+
return elem('w:tbl', [], kids);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function buildBody(tokens, content, rels, links) {
|
|
313
|
+
const kids = [];
|
|
314
|
+
for (const block of content) {
|
|
315
|
+
if (block.t === 'p') kids.push(buildPara(tokens, block, links));
|
|
316
|
+
else if (block.t === 'table') kids.push(buildTable(tokens, block, links));
|
|
317
|
+
else if (block.t === 'pageBreak') {
|
|
318
|
+
kids.push(elem('w:p', [], [elem('w:r', [], [elem('w:br', [['w:type', 'page']])])]));
|
|
319
|
+
} else throw new Error(`unknown block: ${block.t}`);
|
|
320
|
+
}
|
|
321
|
+
// sectPr 最后
|
|
322
|
+
const page = tokens.page ?? {};
|
|
323
|
+
const size = typeof page.size === 'string' ? PAGE_SIZES[page.size] : page.size ?? PAGE_SIZES.A4;
|
|
324
|
+
const m = page.marginsTwip ?? { top: 1440, bottom: 1440, left: 1800, right: 1800, header: 851, footer: 992, gutter: 0 };
|
|
325
|
+
const sect = [];
|
|
326
|
+
if (rels.header) sect.push(elem('w:headerReference', [['w:type', 'default'], ['r:id', rels.header]]));
|
|
327
|
+
if (rels.footer) sect.push(elem('w:footerReference', [['w:type', 'default'], ['r:id', rels.footer]]));
|
|
328
|
+
sect.push(elem('w:pgSz', [
|
|
329
|
+
['w:w', page.landscape ? size.hTwip : size.wTwip],
|
|
330
|
+
['w:h', page.landscape ? size.wTwip : size.hTwip],
|
|
331
|
+
...(page.landscape ? [['w:orient', 'landscape']] : []),
|
|
332
|
+
]));
|
|
333
|
+
sect.push(elem('w:pgMar', [
|
|
334
|
+
['w:top', m.top], ['w:right', m.right], ['w:bottom', m.bottom], ['w:left', m.left],
|
|
335
|
+
['w:header', m.header ?? 851], ['w:footer', m.footer ?? 992], ['w:gutter', m.gutter ?? 0],
|
|
336
|
+
]));
|
|
337
|
+
if (page.docGrid) {
|
|
338
|
+
sect.push(elem('w:docGrid', [
|
|
339
|
+
['w:type', page.docGrid.type ?? 'lines'],
|
|
340
|
+
['w:linePitch', page.docGrid.linePitchTwip],
|
|
341
|
+
...(page.docGrid.charSpace != null ? [['w:charSpace', page.docGrid.charSpace]] : []),
|
|
342
|
+
]));
|
|
343
|
+
}
|
|
344
|
+
kids.push(sortChildren(elem('w:sectPr', [], sect)));
|
|
345
|
+
return elem('w:body', [], kids);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function buildDocumentXml(tokens, content, rels = {}, links = null) {
|
|
349
|
+
const root = elem('w:document', [[W, NS_W], ['xmlns:r', NS_R]], [buildBody(tokens, content, rels, links)]);
|
|
350
|
+
return DECL + serializeNode(root);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** 正文(含表格单元格)里出现过的超链接目标,按出现顺序去重。
|
|
354
|
+
* 只扫 content —— 页眉页脚的 link 在校验层就被拒了(要单独关系表,没做) */
|
|
355
|
+
export function collectLinks(content) {
|
|
356
|
+
const urls = [];
|
|
357
|
+
const seen = new Set();
|
|
358
|
+
const fromRuns = (runs) => {
|
|
359
|
+
for (const r of runs ?? []) {
|
|
360
|
+
if (r && typeof r === 'object' && typeof r.link === 'string' && r.link && !seen.has(r.link)) {
|
|
361
|
+
seen.add(r.link);
|
|
362
|
+
urls.push(r.link);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
for (const b of content ?? []) {
|
|
367
|
+
if (!b || typeof b !== 'object') continue;
|
|
368
|
+
if (b.t === 'p') fromRuns(b.runs);
|
|
369
|
+
else if (b.t === 'table') {
|
|
370
|
+
for (const row of b.rows ?? []) {
|
|
371
|
+
for (const cell of row ?? []) {
|
|
372
|
+
if (cell && typeof cell === 'object') fromRuns(cell.runs);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return urls;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function buildHdrFtr(tokens, name, blocks) {
|
|
381
|
+
const root = elem(name, [[W, NS_W], ['xmlns:r', NS_R]],
|
|
382
|
+
blocks.map((b) => buildPara(tokens, b)));
|
|
383
|
+
return DECL + serializeNode(root);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* ── 容器组装 ─────────────────────────────────── */
|
|
387
|
+
|
|
388
|
+
const CT = (over) => `${DECL}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">`
|
|
389
|
+
+ '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
|
|
390
|
+
+ '<Default Extension="xml" ContentType="application/xml"/>'
|
|
391
|
+
+ over.map(([p, t]) => `<Override PartName="${p}" ContentType="${t}"/>`).join('')
|
|
392
|
+
+ '</Types>';
|
|
393
|
+
|
|
394
|
+
/** 关系表是字符串拼的,超链接 Target 是外来 URL —— & 这类字符不转义就是坏 XML */
|
|
395
|
+
const escAttr = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
396
|
+
|
|
397
|
+
const REL = (rels) => `${DECL}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">`
|
|
398
|
+
+ rels.map(([id, type, target, mode]) => `<Relationship Id="${id}" Type="${type}" Target="${escAttr(target)}"${mode ? ` TargetMode="${mode}"` : ''}/>`).join('')
|
|
399
|
+
+ '</Relationships>';
|
|
400
|
+
|
|
401
|
+
const T_DOC = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml';
|
|
402
|
+
const T_STY = 'application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml';
|
|
403
|
+
const T_SET = 'application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml';
|
|
404
|
+
const T_HDR = 'application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml';
|
|
405
|
+
const T_FTR = 'application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml';
|
|
406
|
+
const T_NUM = 'application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml';
|
|
407
|
+
const R_BASE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
|
|
408
|
+
|
|
409
|
+
export function buildSettingsXml(tokens) {
|
|
410
|
+
const kids = [
|
|
411
|
+
elem('w:defaultTabStop', [['w:val', 420]]),
|
|
412
|
+
elem('w:characterSpacingControl', [['w:val', 'compressPunctuation']]),
|
|
413
|
+
elem('w:compat', [], [
|
|
414
|
+
elem('w:compatSetting', [['w:name', 'compatibilityMode'],
|
|
415
|
+
['w:uri', 'http://schemas.microsoft.com/office/word'], ['w:val', '15']]),
|
|
416
|
+
elem('w:compatSetting', [['w:name', 'useWord2013TrackBottomHyphenation'],
|
|
417
|
+
['w:uri', 'http://schemas.microsoft.com/office/word'], ['w:val', '0']]),
|
|
418
|
+
]),
|
|
419
|
+
elem('w:themeFontLang', [['w:val', tokens.lang?.latin ?? 'en-US'], ['w:eastAsia', tokens.lang?.eastAsia ?? 'zh-CN']]),
|
|
420
|
+
];
|
|
421
|
+
const root = elem('w:settings', [[W, NS_W]], kids);
|
|
422
|
+
return DECL + serializeNode(root);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* @param {object} tokens token JSON
|
|
427
|
+
* @param {Array} content 内容块
|
|
428
|
+
* @param {object} opts {header?: blocks, footer?: blocks, coreProps?: {title, creator}}
|
|
429
|
+
* @returns {Buffer} docx
|
|
430
|
+
*/
|
|
431
|
+
export async function buildDocx(tokens, content, opts = {}) {
|
|
432
|
+
const { addEntry, writeZip } = await import('./rawzip.js');
|
|
433
|
+
const zip = { entries: new Map(), order: [] };
|
|
434
|
+
|
|
435
|
+
const overrides = [
|
|
436
|
+
['/word/document.xml', T_DOC],
|
|
437
|
+
['/word/styles.xml', T_STY],
|
|
438
|
+
['/word/settings.xml', T_SET],
|
|
439
|
+
];
|
|
440
|
+
const docRels = [
|
|
441
|
+
['rId1', `${R_BASE}/styles`, 'styles.xml'],
|
|
442
|
+
['rId2', `${R_BASE}/settings`, 'settings.xml'],
|
|
443
|
+
];
|
|
444
|
+
const rels = {};
|
|
445
|
+
let nextId = 3;
|
|
446
|
+
// 自动编号(2026-08-18):有定义才产这个 part。空的 numbering.xml 是合法的但
|
|
447
|
+
// 毫无意义,而 Word 对"声明了 rel 却没有 part"是硬报错,所以三处登记要么全有
|
|
448
|
+
// 要么全没有 —— 这就是为什么它们挤在一个 if 里而不是散在三处。
|
|
449
|
+
const numberingXml = buildNumberingXml(tokens.numbering);
|
|
450
|
+
if (numberingXml) {
|
|
451
|
+
docRels.push([`rId${nextId}`, `${R_BASE}/numbering`, 'numbering.xml']);
|
|
452
|
+
overrides.push(['/word/numbering.xml', T_NUM]);
|
|
453
|
+
nextId += 1;
|
|
454
|
+
}
|
|
455
|
+
if (opts.header) {
|
|
456
|
+
rels.header = `rId${nextId}`;
|
|
457
|
+
docRels.push([`rId${nextId}`, `${R_BASE}/header`, 'header1.xml']);
|
|
458
|
+
overrides.push(['/word/header1.xml', T_HDR]);
|
|
459
|
+
nextId += 1;
|
|
460
|
+
}
|
|
461
|
+
if (opts.footer) {
|
|
462
|
+
rels.footer = `rId${nextId}`;
|
|
463
|
+
docRels.push([`rId${nextId}`, `${R_BASE}/footer`, 'footer1.xml']);
|
|
464
|
+
overrides.push(['/word/footer1.xml', T_FTR]);
|
|
465
|
+
nextId += 1;
|
|
466
|
+
}
|
|
467
|
+
// 超链接(2026-08-19):rId 从同一个计数器发 —— 撞上 styles/settings/numbering/
|
|
468
|
+
// header/footer 已占的号段,Word 打开直接报「文档已损坏」(agent 手写后处理时
|
|
469
|
+
// 实踩过)。同一 URL 复用同一个 rId
|
|
470
|
+
const links = new Map();
|
|
471
|
+
for (const url of collectLinks(content)) {
|
|
472
|
+
const id = `rId${nextId}`;
|
|
473
|
+
nextId += 1;
|
|
474
|
+
links.set(url, id);
|
|
475
|
+
docRels.push([id, `${R_BASE}/hyperlink`, url, 'External']);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
addEntry(zip, '[Content_Types].xml', CT(overrides));
|
|
479
|
+
addEntry(zip, '_rels/.rels', REL([
|
|
480
|
+
['rId1', `${R_BASE}/officeDocument`, 'word/document.xml'],
|
|
481
|
+
]));
|
|
482
|
+
addEntry(zip, 'word/_rels/document.xml.rels', REL(docRels));
|
|
483
|
+
if (numberingXml) addEntry(zip, 'word/numbering.xml', numberingXml);
|
|
484
|
+
addEntry(zip, 'word/styles.xml', buildStylesXml(tokens));
|
|
485
|
+
addEntry(zip, 'word/settings.xml', buildSettingsXml(tokens));
|
|
486
|
+
if (opts.header) addEntry(zip, 'word/header1.xml', buildHdrFtr(tokens, 'w:hdr', opts.header));
|
|
487
|
+
if (opts.footer) addEntry(zip, 'word/footer1.xml', buildHdrFtr(tokens, 'w:ftr', opts.footer));
|
|
488
|
+
addEntry(zip, 'word/document.xml', buildDocumentXml(tokens, content, rels, links));
|
|
489
|
+
return writeZip(zip);
|
|
490
|
+
}
|