@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.
Files changed (372) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +140 -0
  3. package/bin/nodesign.js +176 -0
  4. package/package.json +104 -0
  5. package/server/api/README.md +45 -0
  6. package/server/api/_guard.js +75 -0
  7. package/server/api/admin.js +187 -0
  8. package/server/api/assets/docx-page.js +103 -0
  9. package/server/api/assets/helpers.js +22 -0
  10. package/server/api/assets/notes.js +144 -0
  11. package/server/api/assets.js +896 -0
  12. package/server/api/board.js +57 -0
  13. package/server/api/browse.js +138 -0
  14. package/server/api/canvas.js +444 -0
  15. package/server/api/chatai.js +324 -0
  16. package/server/api/exports/build-standalone.js +980 -0
  17. package/server/api/exports/cards.js +118 -0
  18. package/server/api/exports/docx-pdf.js +46 -0
  19. package/server/api/exports/export-page.js +142 -0
  20. package/server/api/exports/handoff.js +147 -0
  21. package/server/api/exports.js +690 -0
  22. package/server/api/instruction.js +75 -0
  23. package/server/api/local.js +115 -0
  24. package/server/api/me.js +116 -0
  25. package/server/api/memory.js +185 -0
  26. package/server/api/pending-changes.js +366 -0
  27. package/server/api/plugins.js +164 -0
  28. package/server/api/projects.js +220 -0
  29. package/server/api/publish.js +64 -0
  30. package/server/api/recent.js +73 -0
  31. package/server/api/sessions.js +589 -0
  32. package/server/api/skills.js +77 -0
  33. package/server/api/standalone-fit.js +168 -0
  34. package/server/api/turn-compose.js +180 -0
  35. package/server/api/turn-inflight.js +44 -0
  36. package/server/api/turn.js +669 -0
  37. package/server/auth/README.md +17 -0
  38. package/server/auth/middleware.js +138 -0
  39. package/server/auth/origin-guard.js +71 -0
  40. package/server/auth/session.js +108 -0
  41. package/server/auth/tier.js +113 -0
  42. package/server/auth/users-store.js +348 -0
  43. package/server/edit/README.md +29 -0
  44. package/server/engine/README.md +113 -0
  45. package/server/engine/agent/agent-shared.js +550 -0
  46. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -0
  47. package/server/engine/agent/auto-mode-rules.js +78 -0
  48. package/server/engine/agent/context.js +418 -0
  49. package/server/engine/agent/events.js +464 -0
  50. package/server/engine/agent/hooks/canvas-validate.js +199 -0
  51. package/server/engine/agent/hooks/failure.js +120 -0
  52. package/server/engine/agent/hooks/file-events.js +75 -0
  53. package/server/engine/agent/hooks/lifecycle.js +233 -0
  54. package/server/engine/agent/hooks/post-canvas-focus.js +80 -0
  55. package/server/engine/agent/hooks/post-guidance.js +206 -0
  56. package/server/engine/agent/hooks/post-subagent-report.js +104 -0
  57. package/server/engine/agent/hooks/post-trim.js +58 -0
  58. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -0
  59. package/server/engine/agent/hooks/pre-defaults.js +84 -0
  60. package/server/engine/agent/hooks/pre-injectors.js +277 -0
  61. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -0
  62. package/server/engine/agent/hooks/pre-starter-files.js +61 -0
  63. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -0
  64. package/server/engine/agent/hooks/site-validate.js +94 -0
  65. package/server/engine/agent/hooks/tool-prompts.js +31 -0
  66. package/server/engine/agent/hooks/turn-state-memory.js +56 -0
  67. package/server/engine/agent/hooks/user-prompt-submit.js +215 -0
  68. package/server/engine/agent/hooks.js +354 -0
  69. package/server/engine/agent/init-contract.js +69 -0
  70. package/server/engine/agent/isolation.js +159 -0
  71. package/server/engine/agent/model-context.js +359 -0
  72. package/server/engine/agent/model-table.js +292 -0
  73. package/server/engine/agent/plugin-loader.js +266 -0
  74. package/server/engine/agent/prompts/nodesign-prelude.md +336 -0
  75. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -0
  76. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -0
  77. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -0
  78. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -0
  79. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -0
  80. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -0
  81. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -0
  82. package/server/engine/agent/prompts/tools/site-reference.md +106 -0
  83. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -0
  84. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -0
  85. package/server/engine/agent/session-loop.js +1024 -0
  86. package/server/engine/agent/session-model.js +164 -0
  87. package/server/engine/agent/skill.js +204 -0
  88. package/server/engine/agent/system-prompts.js +86 -0
  89. package/server/engine/agent/task-events.js +78 -0
  90. package/server/engine/agents/ds-extractor.md +132 -0
  91. package/server/engine/agents/explorer.md +190 -0
  92. package/server/engine/agents/index.js +230 -0
  93. package/server/engine/agents/schemas/design-system.json +156 -0
  94. package/server/engine/agents/schemas/tweak-schema.json +113 -0
  95. package/server/engine/agents/tweak-proposer.md +127 -0
  96. package/server/engine/agents/vision-checker.md +254 -0
  97. package/server/engine/browse/capture.js +422 -0
  98. package/server/engine/browse/card.js +137 -0
  99. package/server/engine/browse/handover.js +59 -0
  100. package/server/engine/browse/page-digest.js +119 -0
  101. package/server/engine/browse/refs.js +196 -0
  102. package/server/engine/browse/registry.js +307 -0
  103. package/server/engine/browse/screencast.js +170 -0
  104. package/server/engine/browse/state.js +121 -0
  105. package/server/engine/chatai/chat-log.js +79 -0
  106. package/server/engine/chatai/index.js +193 -0
  107. package/server/engine/chatai/openai-compat.js +152 -0
  108. package/server/engine/chatai/orchestrate.js +309 -0
  109. package/server/engine/chatai/perform.js +40 -0
  110. package/server/engine/chatai/summarize.js +79 -0
  111. package/server/engine/mcp/capability-gate.js +52 -0
  112. package/server/engine/mcp/index.js +339 -0
  113. package/server/engine/mcp/param-sanitizer.js +142 -0
  114. package/server/engine/mcp/tools/arrange-on-board.js +124 -0
  115. package/server/engine/mcp/tools/artifact-session.js +278 -0
  116. package/server/engine/mcp/tools/browse-computer.js +421 -0
  117. package/server/engine/mcp/tools/browse-find-batch.js +183 -0
  118. package/server/engine/mcp/tools/browse-screenshot.js +157 -0
  119. package/server/engine/mcp/tools/browse.js +480 -0
  120. package/server/engine/mcp/tools/build-docx.js +101 -0
  121. package/server/engine/mcp/tools/clear-pending-changes.js +99 -0
  122. package/server/engine/mcp/tools/create-on-board.js +155 -0
  123. package/server/engine/mcp/tools/crystallize-skill.js +168 -0
  124. package/server/engine/mcp/tools/deliver-files.js +0 -0
  125. package/server/engine/mcp/tools/explain-style.js +237 -0
  126. package/server/engine/mcp/tools/export-handoff.js +133 -0
  127. package/server/engine/mcp/tools/expose-tweaks.js +149 -0
  128. package/server/engine/mcp/tools/generate-image.js +842 -0
  129. package/server/engine/mcp/tools/get-computed-styles.js +164 -0
  130. package/server/engine/mcp/tools/get-pending-changes.js +205 -0
  131. package/server/engine/mcp/tools/h3box-ssh.js +84 -0
  132. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -0
  133. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -0
  134. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -0
  135. package/server/engine/mcp/tools/helpers/perception-page.js +215 -0
  136. package/server/engine/mcp/tools/helpers/reference-download.js +103 -0
  137. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -0
  138. package/server/engine/mcp/tools/helpers/rembg.js +273 -0
  139. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -0
  140. package/server/engine/mcp/tools/highlight.js +68 -0
  141. package/server/engine/mcp/tools/list-pages.js +222 -0
  142. package/server/engine/mcp/tools/lookup-tags.js +59 -0
  143. package/server/engine/mcp/tools/navigate-to-page.js +60 -0
  144. package/server/engine/mcp/tools/organize-board.js +64 -0
  145. package/server/engine/mcp/tools/paint-still.js +434 -0
  146. package/server/engine/mcp/tools/pin-to-board.js +130 -0
  147. package/server/engine/mcp/tools/preview-deck.js +130 -0
  148. package/server/engine/mcp/tools/profile-scroll.js +305 -0
  149. package/server/engine/mcp/tools/publish-site.js +102 -0
  150. package/server/engine/mcp/tools/query-elements.js +195 -0
  151. package/server/engine/mcp/tools/read-board.js +103 -0
  152. package/server/engine/mcp/tools/read-document.js +93 -0
  153. package/server/engine/mcp/tools/read-page.js +264 -0
  154. package/server/engine/mcp/tools/read-tavern-json.js +143 -0
  155. package/server/engine/mcp/tools/record-decision.js +140 -0
  156. package/server/engine/mcp/tools/relate-on-board.js +110 -0
  157. package/server/engine/mcp/tools/remove-background.js +391 -0
  158. package/server/engine/mcp/tools/report-issue.js +115 -0
  159. package/server/engine/mcp/tools/roll-film.js +266 -0
  160. package/server/engine/mcp/tools/screenshot-docx.js +138 -0
  161. package/server/engine/mcp/tools/screenshot-url.js +195 -0
  162. package/server/engine/mcp/tools/screenshot.js +583 -0
  163. package/server/engine/mcp/tools/tier-gate.js +96 -0
  164. package/server/engine/mcp/tools/trace-motion.js +215 -0
  165. package/server/engine/mcp/tools/web-search.js +548 -0
  166. package/server/engine/motion/inventory.js +349 -0
  167. package/server/engine/perception/session.js +235 -0
  168. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -0
  169. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -0
  170. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -0
  171. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -0
  172. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -0
  173. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -0
  174. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -0
  175. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -0
  176. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -0
  177. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -0
  178. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -0
  179. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -0
  180. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -0
  181. 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
  182. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -0
  183. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -0
  184. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -0
  185. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -0
  186. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -0
  187. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -0
  188. package/server/engine/runs/active-runs.js +698 -0
  189. package/server/engine/runs/live-turn.js +252 -0
  190. package/server/engine/runs/store.js +364 -0
  191. package/server/engine/runs/turn-relay.js +217 -0
  192. package/server/engine/runtime/workspace.js +164 -0
  193. package/server/index.js +235 -0
  194. package/server/lib/artifact-file-path.js +80 -0
  195. package/server/lib/artifact-target.js +284 -0
  196. package/server/lib/asset-refs.js +239 -0
  197. package/server/lib/async-queue.js +104 -0
  198. package/server/lib/auto-relations.js +130 -0
  199. package/server/lib/binding-types.js +60 -0
  200. package/server/lib/board-kind-sizes.js +62 -0
  201. package/server/lib/board-relations.js +133 -0
  202. package/server/lib/browse-proxy.js +180 -0
  203. package/server/lib/canvas-id.js +36 -0
  204. package/server/lib/cover.js +227 -0
  205. package/server/lib/danbooru-tags.js +291 -0
  206. package/server/lib/doc-extract.js +156 -0
  207. package/server/lib/docx/build-from-source.js +260 -0
  208. package/server/lib/docx/build.js +490 -0
  209. package/server/lib/docx/dump-styles.js +287 -0
  210. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -0
  211. package/server/lib/docx/merge-runs.js +129 -0
  212. package/server/lib/docx/numbering.js +218 -0
  213. package/server/lib/docx/order.js +152 -0
  214. package/server/lib/docx/rawzip.js +172 -0
  215. package/server/lib/docx/render.js +92 -0
  216. package/server/lib/docx/text-lint.js +225 -0
  217. package/server/lib/docx/tokens.js +335 -0
  218. package/server/lib/docx/units.js +29 -0
  219. package/server/lib/docx/xml.js +291 -0
  220. package/server/lib/docx-pages.js +187 -0
  221. package/server/lib/export-collect.js +289 -0
  222. package/server/lib/export-package.js +210 -0
  223. package/server/lib/html-srcset.js +145 -0
  224. package/server/lib/image-variant.js +460 -0
  225. package/server/lib/ingress/forward-openai-chat.js +305 -0
  226. package/server/lib/ingress/openai-chat.js +491 -0
  227. package/server/lib/ingress/session-notice.js +66 -0
  228. package/server/lib/ingress/session-routes.js +68 -0
  229. package/server/lib/ingress/slot-probe.js +130 -0
  230. package/server/lib/ingress/upstream-billing.js +52 -0
  231. package/server/lib/ingress/upstream-fail-streak.js +72 -0
  232. package/server/lib/ingress/upstream-truncation.js +48 -0
  233. package/server/lib/issues-store.js +182 -0
  234. package/server/lib/kinds/deck.js +72 -0
  235. package/server/lib/kinds/docx.js +283 -0
  236. package/server/lib/kinds/file-kinds.js +113 -0
  237. package/server/lib/kinds/index.js +202 -0
  238. package/server/lib/kinds/site.js +161 -0
  239. package/server/lib/model-ingress.js +543 -0
  240. package/server/lib/moderation.js +255 -0
  241. package/server/lib/notice-store.js +103 -0
  242. package/server/lib/plugin-install.js +104 -0
  243. package/server/lib/plugin-validator.js +721 -0
  244. package/server/lib/publish-store.js +115 -0
  245. package/server/lib/quick-summary.js +52 -0
  246. package/server/lib/quota.js +270 -0
  247. package/server/lib/rate-window.js +29 -0
  248. package/server/lib/reference-assets.js +92 -0
  249. package/server/lib/region-shot.js +135 -0
  250. package/server/lib/safe-path.js +73 -0
  251. package/server/lib/sdk-session.js +33 -0
  252. package/server/lib/showcase-store.js +92 -0
  253. package/server/lib/site-publish.js +465 -0
  254. package/server/lib/ssrf-guard.js +432 -0
  255. package/server/lib/task-scan.js +158 -0
  256. package/server/lib/tavern-json.js +154 -0
  257. package/server/lib/video-variant.js +237 -0
  258. package/server/lib/workspace-path.js +33 -0
  259. package/server/ops/fix-sdk-musl.mjs +40 -0
  260. package/server/ops/install-macos-fonts.sh +114 -0
  261. package/server/ops/macos-fonts.conf +336 -0
  262. package/server/ops/sandbox-shim/bwrap +57 -0
  263. package/server/projects/assets-summary.js +119 -0
  264. package/server/projects/auto-name.js +53 -0
  265. package/server/projects/board-store.js +640 -0
  266. package/server/projects/move-entry.js +103 -0
  267. package/server/projects/store.js +270 -0
  268. package/server/projects/ui-config.js +54 -0
  269. package/server/projects/workspace-templates.js +53 -0
  270. package/server/projects/workspace.js +1025 -0
  271. package/server/runtime/capabilities.js +153 -0
  272. package/server/runtime/local-config.js +177 -0
  273. package/server/runtime/local-env.js +88 -0
  274. package/server/runtime/platform.js +342 -0
  275. package/server/runtime/profile.js +72 -0
  276. package/server/services/rembg-launcher.js +313 -0
  277. package/server/services/rembg-service.py +334 -0
  278. package/server/shared/README.md +25 -0
  279. package/server/shared/deck.js +54 -0
  280. package/server/shared/time.js +49 -0
  281. package/server/style-pipeline/README.md +53 -0
  282. package/server/ws/broker.js +36 -0
  283. package/server/ws/browse-channel.js +177 -0
  284. package/server/ws/index.js +386 -0
  285. package/web/README.md +89 -0
  286. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +11 -0
  287. package/web/dist/assets/DeckWindow-4G0xX7P3.js +16 -0
  288. package/web/dist/assets/DocxWindow-BXQsoPkh.js +16 -0
  289. package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  290. package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  291. package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  292. package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  293. package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  294. package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  295. package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  296. package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  297. package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  298. package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  299. package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  300. package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  301. package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  302. package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  303. package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  304. package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  305. package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  306. package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  307. package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  308. package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  309. package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  310. package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  311. package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  312. package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  313. package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  314. package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  315. package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  316. package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  317. package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  318. package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  319. package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  320. package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  321. package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  322. package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  323. package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  324. package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  325. package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  326. package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  327. package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  328. package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  329. package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  330. package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  331. package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  332. package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  333. package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  334. package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  335. package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  336. package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  337. package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  338. package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  339. package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  340. package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  341. package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  342. package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  343. package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  344. package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  345. package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  346. package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  347. package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  348. package/web/dist/assets/SiteWindow-D6eS-2gM.js +18 -0
  349. package/web/dist/assets/again-D8tfgKKO.webp +0 -0
  350. package/web/dist/assets/caveat-nd-CP6HlsNg.woff2 +0 -0
  351. package/web/dist/assets/clock-xJhvfYpS.webp +0 -0
  352. package/web/dist/assets/film-cut-BBKMwH7q.webp +0 -0
  353. package/web/dist/assets/film-still-BZ_DL9Tt.webp +0 -0
  354. package/web/dist/assets/index-C0KUf6Wd.css +1 -0
  355. package/web/dist/assets/index-Cbl3ATvp.js +2125 -0
  356. package/web/dist/assets/ink-desk-BRbxeDsR.webp +0 -0
  357. package/web/dist/assets/ink-night-Bz4BT3XH.webp +0 -0
  358. package/web/dist/assets/ink-portrait-C0DM4V8Q.webp +0 -0
  359. package/web/dist/assets/longcang-regular-C0uvTuKa.woff2 +0 -0
  360. package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
  361. package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
  362. package/web/dist/assets/pending-edit-apply-DTjo2geW.js +31 -0
  363. package/web/dist/assets/question-8H4HSqxh.webp +0 -0
  364. package/web/dist/assets/reject-BaPJqElj.webp +0 -0
  365. package/web/dist/assets/rotate-cw-LMT3x1Zj.js +11 -0
  366. package/web/dist/assets/rp-portrait-CbrGpew2.webp +0 -0
  367. package/web/dist/assets/rp-street-C4jVKjxx.webp +0 -0
  368. package/web/dist/assets/square-dashed-mouse-pointer-CBoAkfhi.js +11 -0
  369. package/web/dist/assets/tangle-DIxHVNuT.webp +0 -0
  370. package/web/dist/assets/thisone-BylZKglV.webp +0 -0
  371. package/web/dist/assets/thumb-DO1wfK1I.webp +0 -0
  372. package/web/dist/index.html +18 -0
@@ -0,0 +1,225 @@
1
+ /**
2
+ * text-lint.js — 构建后的字符层/版式层体检(2026-08-19)。
3
+ *
4
+ * 闭合 schema 管格式键、渲染目视管版面,**文字符号这一层两边都管不着**:
5
+ * 斜杠带不带空格不一致、`12ms` 缺空隙、中文后面跟半角逗号 —— 人眼查全文
6
+ * 一致性不可靠,正则穷举才靠得住。检查跑在 **content 源**上(不解 docx),
7
+ * 每条发现都能定位到具体块。
8
+ *
9
+ * 三条设计红线(放宽任何一条都会把这层变成噪音):
10
+ * 1. **只收高精度规则**。假警报会训练 agent 忽略警报 —— 拿不准的宁可不报。
11
+ * 2. **一致性类规则全文聚合后才裁决**:两种写法并存才是问题,全文统一用
12
+ * 哪种都不是(斜杠风格、中西文间距都是排版偏好,不是对错)。
13
+ * 3. **只报不改**。文字内容是 agent/用户的,引擎不代笔。
14
+ *
15
+ * 两档措辞:`errors` = 按 GB/T 15834 / 排版常识基本可断定是错的;
16
+ * `notes` = 建议(惯例/一致性),采不采纳由写的人定。都不挡构建。
17
+ */
18
+
19
+ import { effectiveSizePt } from './build.js';
20
+
21
+ const CJK = '\\u4e00-\\u9fff\\u3400-\\u4dbf';
22
+
23
+ /** 一个段落块 → 拼好的纯文本(run 边界处的间距问题也要能查到,所以先拼再扫) */
24
+ function textOfPara(b) {
25
+ if (typeof b?.text === 'string') return b.text;
26
+ const parts = [];
27
+ for (const r of b?.runs ?? []) {
28
+ if (typeof r === 'string') parts.push(r);
29
+ else if (r && typeof r.text === 'string') parts.push(r.text);
30
+ else if (r?.br) parts.push('\n');
31
+ // fld(页码域)没有文字形态,跳过
32
+ }
33
+ return parts.join('');
34
+ }
35
+
36
+ /** content + 页眉页脚 + 表格单元格 → [{ where, text }],where 能定位回源 */
37
+ export function collectTexts(content, opts = {}) {
38
+ const out = [];
39
+ const push = (where, b) => {
40
+ const t = textOfPara(b);
41
+ if (t.trim()) out.push({ where, text: t });
42
+ };
43
+ for (const [i, b] of (content ?? []).entries()) {
44
+ if (!b || typeof b !== 'object') continue;
45
+ if (b.t === 'p') push(`content[${i}]`, b);
46
+ else if (b.t === 'table') {
47
+ for (const [ri, row] of (b.rows ?? []).entries()) {
48
+ for (const [ci, cell] of (row ?? []).entries()) {
49
+ if (typeof cell === 'string') { if (cell.trim()) out.push({ where: `content[${i}].rows[${ri}][${ci}]`, text: cell }); }
50
+ else if (cell && typeof cell === 'object') push(`content[${i}].rows[${ri}][${ci}]`, cell);
51
+ }
52
+ }
53
+ }
54
+ }
55
+ for (const [name, blocks] of [['header', opts.header], ['footer', opts.footer]]) {
56
+ for (const [i, b] of (blocks ?? []).entries()) {
57
+ if (b?.t === 'p') push(`${name}[${i}]`, b);
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+
63
+ /** 命中清单 → 一行报文:最多点名 4 处,多的收成 ×N */
64
+ function fmtHits(hits, max = 4) {
65
+ const shown = hits.slice(0, max).map(h => `${h.where}「${h.sample}」`).join('、');
66
+ return hits.length > max ? `${shown} 等 ${hits.length} 处` : shown;
67
+ }
68
+
69
+ /** 在 text 里跑正则收命中(sample 带一点上下文,agent 才对得上号) */
70
+ function scan(texts, re) {
71
+ const hits = [];
72
+ for (const { where, text } of texts) {
73
+ for (const m of text.matchAll(re)) {
74
+ const s = Math.max(0, m.index - 4);
75
+ hits.push({ where, sample: text.slice(s, m.index + m[0].length + 4).replace(/\n/g, ' ') });
76
+ }
77
+ }
78
+ return hits;
79
+ }
80
+
81
+ export function lintDocxSource({ tokens, content, opts = {} }) {
82
+ const errors = [];
83
+ const notes = [];
84
+ const texts = collectTexts(content, opts);
85
+
86
+ /* ── 错误档(基本可断定是错的) ── */
87
+
88
+ // 重复标点:「。。」「,,」几乎必是手误。!?的叠用是修辞,不管
89
+ for (const [re, label] of [[new RegExp('([,。;、:])\\1+', 'g'), '重复标点']]) {
90
+ const hits = scan(texts, re);
91
+ if (hits.length) errors.push(`${label}:${fmtHits(hits)}`);
92
+ }
93
+
94
+ // 括号全半角不配对:「(x)」「(x)」
95
+ {
96
+ const hits = scan(texts, /([^()()\n]*\)|\([^()()\n]*)/g);
97
+ if (hits.length) errors.push(`括号全半角不配对:${fmtHits(hits)}`);
98
+ }
99
+
100
+ // 中文后紧跟半角标点(GB/T 15834:中文语境用全角)。只查后继是中文/空白/行尾
101
+ // 的情形 —— 「12:30」「Node.js, Express」这类西文语境不碰
102
+ {
103
+ const hits = scan(texts, new RegExp(`[${CJK}][,;?!:](?=[${CJK}\\s]|$)`, 'gm'));
104
+ if (hits.length) errors.push(`中文后面跟着半角标点(该用全角):${fmtHits(hits)}`);
105
+ }
106
+
107
+ /* ── 建议档:单点惯例 ── */
108
+
109
+ // 数值与单位符号之间留空隙(GB 3100 惯例)。单位表故意收窄:裸 s/m/g 歧义太大
110
+ {
111
+ const hits = scan(texts, /\d(?:ms|GB|MB|KB|TB|Hz|kHz|MHz|fps|mm|cm|km|kg|dpi)(?![A-Za-z])/g);
112
+ if (hits.length) notes.push(`数字和单位之间通常留一个空格(如「150 ms」):${fmtHits(hits)}`);
113
+ }
114
+
115
+ /* ── 建议档:一致性类(全文聚合,两种写法并存才报) ── */
116
+
117
+ // 中西文之间的空格。数字不算(「第3个」加空格属于过度矫正),URL 整段跳过
118
+ {
119
+ let spaced = 0;
120
+ const unspaced = [];
121
+ for (const { where, text } of texts) {
122
+ const t = text.replace(/\S*:\/\/\S*/g, ' ');
123
+ for (const m of t.matchAll(new RegExp(`[${CJK}][A-Za-z]|[A-Za-z][${CJK}]`, 'g'))) {
124
+ unspaced.push({ where, sample: t.slice(Math.max(0, m.index - 4), m.index + m[0].length + 4) });
125
+ }
126
+ spaced += (t.match(new RegExp(`[${CJK}] [A-Za-z]|[A-Za-z] [${CJK}]`, 'g')) ?? []).length;
127
+ }
128
+ if (unspaced.length && spaced > unspaced.length) {
129
+ notes.push(`中西文之间的空格不一致:全文 ${spaced} 处有空格,这 ${unspaced.length} 处没有 —— ${fmtHits(unspaced)}`);
130
+ }
131
+ // 全文都不空 = 一种自洽的风格,不报
132
+ }
133
+
134
+ // 斜杠两侧空格的一致性。三类豁免:带协议的 URL、无协议的域名路径
135
+ //(github.com/xxx —— 给它"统一"加空格反而是改错)、两侧都是数字的(日期 03/07)
136
+ {
137
+ const spaced = [];
138
+ const tight = [];
139
+ for (const { where, text } of texts) {
140
+ const t = text.replace(/\S*:\/\/\S*/g, ' ');
141
+ for (const m of t.matchAll(/([\w.\-一-鿿]+) ?\/ ?([\w.\-一-鿿])/g)) {
142
+ if (/\.[a-z]{2,4}$/i.test(m[1])) continue; // 左边以 .com/.cn/.js 这类结尾 = 域名或文件名
143
+ if (/\d$/.test(m[1]) && /\d/.test(m[2])) continue;
144
+ (m[0].includes(' ') ? spaced : tight).push({ where, sample: t.slice(Math.max(0, m.index - 3), m.index + m[0].length + 6) });
145
+ }
146
+ }
147
+ if (spaced.length && tight.length) {
148
+ const [minor, label] = spaced.length < tight.length ? [spaced, '带空格'] : [tight, '不带空格'];
149
+ notes.push(`斜杠写法不一致(${spaced.length} 处「A / B」vs ${tight.length} 处「A/B」),少数派是${label}的:${fmtHits(minor)}`);
150
+ }
151
+ }
152
+
153
+ // 日期范围连接号的一致性(字符本身不裁 —— 「-」「–」「至」都是有人在用的惯例,
154
+ // 一份文档里混着用才是问题)
155
+ {
156
+ const styles = new Map(); // 连接串 → hits
157
+ for (const { where, text } of texts) {
158
+ for (const m of text.matchAll(/\d{4}[..年/-]\d{1,2}[月]?( ?[-–—~~至] ?)(?=\d{4}|至今|今)/g)) {
159
+ const key = m[1];
160
+ if (!styles.has(key)) styles.set(key, []);
161
+ styles.get(key).push({ where, sample: text.slice(Math.max(0, m.index), m.index + m[0].length + 7) });
162
+ }
163
+ }
164
+ if (styles.size > 1) {
165
+ const desc = [...styles.entries()].map(([k, v]) => `「${k.replace(/ /g, '␣')}」×${v.length}`).join('、');
166
+ notes.push(`日期范围的连接号写法不统一:${desc}`);
167
+ }
168
+ }
169
+
170
+ /* ── 版式断言(token 层,只收零误报的三条) ── */
171
+
172
+ // 行距小于单倍 = 文字上下行叠压,多半是把磅值当倍数写了
173
+ const checkSp = (whereName, sp) => {
174
+ if (sp && (sp.lineRule ?? 'multiple') === 'multiple' && sp.line != null && sp.line < 1) {
175
+ errors.push(`${whereName}.spacing.line = ${sp.line}(倍数小于 1,文字会叠压)`);
176
+ }
177
+ };
178
+ checkSp('base', tokens?.base?.spacing);
179
+ for (const [id, st] of Object.entries(tokens?.styles ?? {})) {
180
+ checkSp(`styles.${id}.para`, st?.para?.spacing);
181
+
182
+ // 标题(有 outlineLevel 的)不带 keepNext → 会孤零零留在页底
183
+ if (st?.para?.outlineLevel != null && st.para.keepNext !== true) {
184
+ notes.push(`styles.${id} 是标题(outlineLevel ${st.para.outlineLevel})但没写 keepNext —— 分页时它可能孤零零留在页底`);
185
+ }
186
+ }
187
+
188
+ // 标题层级字号倒挂:更深一级的某个标题比浅一级的某个标题还大。
189
+ // 同层可以有多种字号(Title 和 Heading1 常同在层级 0),所以比的是
190
+ // 浅层的**最小**对深层的**最大** —— 只要深层最大越过了浅层最小就是倒挂
191
+ {
192
+ const byLevel = new Map(); // level → { min: {id,pt}, max: {id,pt} }
193
+ for (const [id, st] of Object.entries(tokens?.styles ?? {})) {
194
+ const lv = st?.para?.outlineLevel;
195
+ if (lv == null) continue;
196
+ const pt = effectiveSizePt(tokens, id);
197
+ const e = byLevel.get(lv) ?? { min: { id, pt }, max: { id, pt } };
198
+ if (pt < e.min.pt) e.min = { id, pt };
199
+ if (pt > e.max.pt) e.max = { id, pt };
200
+ byLevel.set(lv, e);
201
+ }
202
+ const levels = [...byLevel.keys()].sort((a, b) => a - b);
203
+ for (let i = 1; i < levels.length; i++) {
204
+ const hi = byLevel.get(levels[i - 1]).min;
205
+ const lo = byLevel.get(levels[i]).max;
206
+ if (lo.pt > hi.pt) {
207
+ notes.push(`标题字号倒挂:${lo.id}(层级 ${levels[i]},${lo.pt}pt)比 ${hi.id}(层级 ${levels[i - 1]},${hi.pt}pt)还大`);
208
+ }
209
+ }
210
+ }
211
+
212
+ return { errors, notes };
213
+ }
214
+
215
+ /** 给 build_docx 的返回文案拼提醒段。空的返回 ''(一个字都不占) */
216
+ export function formatLint({ errors, notes }, max = 10) {
217
+ const lines = [];
218
+ for (const e of errors) lines.push(`⚠ ${e}`);
219
+ for (const n of notes) lines.push(`· ${n}`);
220
+ if (!lines.length) return '';
221
+ const shown = lines.slice(0, max);
222
+ const more = lines.length - shown.length;
223
+ return `\n\n排版/符号体检(${errors.length} 条较确定的问题,${notes.length} 条建议 —— 不挡构建,采不采纳你判断,改了记得重新 build):\n`
224
+ + shown.join('\n') + (more > 0 ? `\n(另有 ${more} 条略去)` : '');
225
+ }
@@ -0,0 +1,335 @@
1
+ /**
2
+ * tokens.js — docx 形态的「真相源」:token JSON schema + 三个内置词典条目。
3
+ *
4
+ * 原则(已拍板):
5
+ * - token JSON 是权威,styles.xml/docDefaults 是构建产物;
6
+ * - 风格名开放(agent 可以起新名字调 token),schema 闭合(字段全集在
7
+ * 这里,build 层只认识这些字段,不接受任意 XML 注入——外来文档的
8
+ * 未建模部分走 extraXml/rawStyles 透传,不走 token 字段扩张)。
9
+ *
10
+ * ── schema v1 ────────────────────────────────────────────────
11
+ * {
12
+ * v: 1,
13
+ * provenance: 'nodesign' | 'imported', // imported = 外来 docx dump 出来的
14
+ * page: {
15
+ * size: 'A4'|'Letter'|{wTwip,hTwip}, landscape?: bool,
16
+ * marginsTwip: {top,bottom,left,right,header,footer,gutter},
17
+ * docGrid?: {type:'lines'|'linesAndChars', linePitchTwip, charSpace?}|null,
18
+ * },
19
+ * lang: {latin:'en-US', eastAsia:'zh-CN'},
20
+ * fonts: { // 具名字体槽;style.run.font 用名字引用
21
+ * body:{eastAsia,ascii,hAnsi?,cs?}, heading:{...}, quote:{...}, mono:{...}, ...自定义槽
22
+ * },
23
+ * base: { // docDefaults
24
+ * sizePt: number|字号名, color:'RRGGBB',
25
+ * font:'body'|{...}, // 引用字体槽
26
+ * spacing?: {line,lineRule,beforePt,afterPt},
27
+ * cjk: {kinsoku,autoSpaceDE,autoSpaceDN,adjustRightInd,snapToGrid,overflowPunct,wordWrap},
28
+ * },
29
+ * styles: { [styleId]: Style },
30
+ * numbering?: { [numName]: {levels:[{fmt,text,indentTwip,hangingTwip,alignedAt?}]} },
31
+ * rawStyles?: [xmlString], // dump 时装不进模型的整条 w:style 原文
32
+ * }
33
+ *
34
+ * Style = {
35
+ * type:'paragraph'|'character'|'table',
36
+ * name: string, // Word UI 名('heading 1' 等内置名有魔法含义)
37
+ * basedOn?, next?, link?, qFormat?: bool, uiPriority?: number,
38
+ * run?: {
39
+ * font?:'body'|{eastAsia?,ascii?,hAnsi?,cs?}, sizePt?: number|字号名,
40
+ * bold?, italic?, color?, underline?: true|'single'|'double'|...,
41
+ * strike?, caps?, smallCaps?, vertAlign?:'superscript'|'subscript',
42
+ * em?:'dot'|'comma'|'circle'|'underDot', // 着重号
43
+ * kernPt?: number, spacingTwip?: number, highlight?: string,
44
+ * },
45
+ * para?: {
46
+ * align?:'left'|'center'|'right'|'both'|'distribute',
47
+ * outlineLevel?: 0-8,
48
+ * indent?: {firstLineChars?, firstLineTwip?, hangingChars?, hangingTwip?,
49
+ * leftChars?, leftTwip?, rightChars?, rightTwip?},
50
+ * spacing?: {beforePt?, afterPt?, beforeLines?, afterLines?,
51
+ * line?: number, lineRule?:'multiple'|'exact'|'atLeast'},
52
+ * // lineRule=multiple 时 line 是倍数(1.5);exact/atLeast 时是磅
53
+ * keepNext?, keepLines?, pageBreakBefore?, widowControl?, contextualSpacing?,
54
+ * borders?: {top?,bottom?,left?,right?: {style,sizePt8?,color,spacePt?}},
55
+ * shading?: 'RRGGBB',
56
+ * tabs?: [{pos:twip, val:'left'|'center'|'right'|'decimal', leader?}],
57
+ * cjk?: 覆盖 base.cjk 的同名开关,
58
+ * },
59
+ * extraXml?: {pPr?: string, rPr?: string}, // dump 透传,build 时原样并入
60
+ * }
61
+ */
62
+
63
+ import { ZIHAO } from './units.js';
64
+ import { validateNumbering } from './numbering.js';
65
+
66
+ /** 词典条目一:办公标准(素颜 Word)——中文 Word 默认观感 */
67
+ export function presetOffice() {
68
+ return {
69
+ v: 1,
70
+ provenance: 'nodesign',
71
+ page: {
72
+ size: 'A4',
73
+ marginsTwip: { top: 1440, bottom: 1440, left: 1800, right: 1800, header: 851, footer: 992, gutter: 0 },
74
+ },
75
+ lang: { latin: 'en-US', eastAsia: 'zh-CN' },
76
+ fonts: {
77
+ body: { eastAsia: '宋体', ascii: 'Calibri' },
78
+ heading: { eastAsia: '黑体', ascii: 'Arial' },
79
+ quote: { eastAsia: '楷体', ascii: 'Calibri' },
80
+ mono: { eastAsia: '宋体', ascii: 'Courier New' },
81
+ },
82
+ base: {
83
+ font: 'body', sizePt: '小四', color: '000000',
84
+ spacing: { line: 1.3, lineRule: 'multiple' },
85
+ cjk: { kinsoku: true, autoSpaceDE: true, autoSpaceDN: true, adjustRightInd: true, snapToGrid: true },
86
+ },
87
+ styles: {
88
+ Normal: { type: 'paragraph', name: 'Normal', qFormat: true, para: { indent: { firstLineChars: 200 } } },
89
+ Title: {
90
+ type: 'paragraph', name: 'Title', basedOn: 'Normal', next: 'Normal', qFormat: true,
91
+ run: { font: 'heading', sizePt: '二号', bold: true },
92
+ para: { align: 'center', indent: { firstLineChars: 0 }, spacing: { beforePt: 12, afterPt: 18 }, keepNext: true, outlineLevel: 0 },
93
+ },
94
+ Heading1: {
95
+ type: 'paragraph', name: 'heading 1', basedOn: 'Normal', next: 'Normal', qFormat: true,
96
+ run: { font: 'heading', sizePt: '三号', bold: true },
97
+ para: { indent: { firstLineChars: 0 }, spacing: { beforePt: 13, afterPt: 13 }, keepNext: true, outlineLevel: 0 },
98
+ },
99
+ Heading2: {
100
+ type: 'paragraph', name: 'heading 2', basedOn: 'Normal', next: 'Normal', qFormat: true,
101
+ run: { font: 'heading', sizePt: '四号', bold: true },
102
+ para: { indent: { firstLineChars: 0 }, spacing: { beforePt: 10, afterPt: 10 }, keepNext: true, outlineLevel: 1 },
103
+ },
104
+ Heading3: {
105
+ type: 'paragraph', name: 'heading 3', basedOn: 'Normal', next: 'Normal', qFormat: true,
106
+ run: { font: 'heading', sizePt: '小四', bold: true },
107
+ para: { indent: { firstLineChars: 0 }, spacing: { beforePt: 8, afterPt: 8 }, keepNext: true, outlineLevel: 2 },
108
+ },
109
+ Quote: {
110
+ type: 'paragraph', name: 'Quote', basedOn: 'Normal', qFormat: true,
111
+ run: { font: 'quote', color: '404040' },
112
+ para: { indent: { leftChars: 200, firstLineChars: 0 } },
113
+ },
114
+ Header: { type: 'paragraph', name: 'header', basedOn: 'Normal', para: { indent: { firstLineChars: 0 }, align: 'center' }, run: { sizePt: '小五' } },
115
+ Footer: { type: 'paragraph', name: 'footer', basedOn: 'Normal', para: { indent: { firstLineChars: 0 }, align: 'center' }, run: { sizePt: '小五' } },
116
+ },
117
+ };
118
+ }
119
+
120
+ /** 词典条目二:公文体(GB/T 9704-2012 版式硬指标) */
121
+ export function presetGongwen() {
122
+ return {
123
+ v: 1,
124
+ provenance: 'nodesign',
125
+ page: {
126
+ size: 'A4',
127
+ // GB/T 9704: 上37 下35 左28 右26 (mm)
128
+ marginsTwip: { top: 2098, bottom: 1984, left: 1588, right: 1474, header: 851, footer: 992, gutter: 0 },
129
+ docGrid: { type: 'lines', linePitchTwip: 560 }, // 固定 28pt 行距的格
130
+ },
131
+ lang: { latin: 'zh-CN', eastAsia: 'zh-CN' },
132
+ fonts: {
133
+ body: { eastAsia: '仿宋_GB2312', ascii: 'Times New Roman' },
134
+ biaosong: { eastAsia: '方正小标宋简体', ascii: 'Times New Roman' },
135
+ hei: { eastAsia: '黑体', ascii: 'Times New Roman' },
136
+ kai: { eastAsia: '楷体_GB2312', ascii: 'Times New Roman' },
137
+ song: { eastAsia: '宋体', ascii: 'Times New Roman' },
138
+ },
139
+ base: {
140
+ font: 'body', sizePt: '三号', color: '000000',
141
+ spacing: { line: 28, lineRule: 'exact' },
142
+ cjk: { kinsoku: true, autoSpaceDE: false, autoSpaceDN: false, adjustRightInd: true, snapToGrid: true },
143
+ },
144
+ styles: {
145
+ Normal: { type: 'paragraph', name: 'Normal', qFormat: true, para: { indent: { firstLineChars: 200 } } },
146
+ // 发文机关标志(红头)
147
+ Hongtou: {
148
+ type: 'paragraph', name: '红头', basedOn: 'Normal', next: 'Normal',
149
+ run: { font: 'biaosong', sizePt: 32, color: 'FF0000', bold: false },
150
+ para: { align: 'center', indent: { firstLineChars: 0 }, spacing: { line: 40, lineRule: 'exact' } },
151
+ },
152
+ Fawenzihao: {
153
+ type: 'paragraph', name: '发文字号', basedOn: 'Normal', next: 'Normal',
154
+ para: { align: 'center', indent: { firstLineChars: 0 } },
155
+ },
156
+ GwTitle: {
157
+ type: 'paragraph', name: '公文标题', basedOn: 'Normal', next: 'Normal', qFormat: true,
158
+ // 标题前后各空一行(28pt)。用 pt 不用 beforeLines:lab/02 已证 LO 对
159
+ // 字符/行单位的兼容不可信,*Lines 同族,公文版式是硬指标不赌。
160
+ run: { font: 'biaosong', sizePt: '二号' },
161
+ para: { align: 'center', indent: { firstLineChars: 0 }, spacing: { beforePt: 28, afterPt: 28 }, keepNext: true, outlineLevel: 0 },
162
+ },
163
+ GwH1: { // 一、 黑体
164
+ type: 'paragraph', name: '公文一级', basedOn: 'Normal', next: 'Normal', qFormat: true,
165
+ run: { font: 'hei' }, para: { keepNext: true, outlineLevel: 0 },
166
+ },
167
+ GwH2: { // (一) 楷体
168
+ type: 'paragraph', name: '公文二级', basedOn: 'Normal', next: 'Normal', qFormat: true,
169
+ run: { font: 'kai' }, para: { keepNext: true, outlineLevel: 1 },
170
+ },
171
+ GwH3: { // 1. 仿宋加粗
172
+ type: 'paragraph', name: '公文三级', basedOn: 'Normal', next: 'Normal', qFormat: true,
173
+ run: { bold: true }, para: { keepNext: true, outlineLevel: 2 },
174
+ },
175
+ Chaosong: {
176
+ type: 'paragraph', name: '抄送', basedOn: 'Normal',
177
+ run: { sizePt: '四号', font: 'body' }, para: { indent: { firstLineChars: 0 } },
178
+ },
179
+ Footer: { type: 'paragraph', name: 'footer', basedOn: 'Normal', para: { indent: { firstLineChars: 0 }, align: 'center' }, run: { font: 'song', sizePt: '四号' } },
180
+ Header: { type: 'paragraph', name: 'header', basedOn: 'Normal', para: { indent: { firstLineChars: 0 }, align: 'center' }, run: { font: 'song', sizePt: '小五' } },
181
+ },
182
+ };
183
+ }
184
+
185
+ /** 词典条目三:学术论文体(中文期刊/学位论文常规) */
186
+ export function presetAcademic() {
187
+ return {
188
+ v: 1,
189
+ provenance: 'nodesign',
190
+ page: {
191
+ size: 'A4',
192
+ marginsTwip: { top: 1700, bottom: 1700, left: 1800, right: 1800, header: 851, footer: 992, gutter: 0 },
193
+ },
194
+ lang: { latin: 'en-US', eastAsia: 'zh-CN' },
195
+ fonts: {
196
+ body: { eastAsia: '宋体', ascii: 'Times New Roman' },
197
+ heading: { eastAsia: '黑体', ascii: 'Times New Roman' },
198
+ quote: { eastAsia: '楷体', ascii: 'Times New Roman' },
199
+ caption: { eastAsia: '宋体', ascii: 'Times New Roman' },
200
+ mono: { eastAsia: '宋体', ascii: 'Courier New' },
201
+ },
202
+ base: {
203
+ font: 'body', sizePt: '小四', color: '000000',
204
+ spacing: { line: 1.5, lineRule: 'multiple' },
205
+ cjk: { kinsoku: true, autoSpaceDE: true, autoSpaceDN: true, adjustRightInd: true, snapToGrid: true },
206
+ },
207
+ styles: {
208
+ Normal: { type: 'paragraph', name: 'Normal', qFormat: true, para: { indent: { firstLineChars: 200 } } },
209
+ Title: {
210
+ type: 'paragraph', name: 'Title', basedOn: 'Normal', next: 'Normal', qFormat: true,
211
+ run: { font: 'heading', sizePt: '三号', bold: true },
212
+ para: { align: 'center', indent: { firstLineChars: 0 }, spacing: { beforePt: 17, afterPt: 17 }, keepNext: true, outlineLevel: 0 },
213
+ },
214
+ Abstract: {
215
+ type: 'paragraph', name: '摘要', basedOn: 'Normal',
216
+ run: { sizePt: '五号', font: 'quote' },
217
+ para: { indent: { firstLineChars: 200 } },
218
+ },
219
+ Heading1: {
220
+ type: 'paragraph', name: 'heading 1', basedOn: 'Normal', next: 'Normal', qFormat: true,
221
+ run: { font: 'heading', sizePt: '四号', bold: true },
222
+ para: { indent: { firstLineChars: 0 }, spacing: { beforePt: 13, afterPt: 13 }, keepNext: true, outlineLevel: 0 },
223
+ },
224
+ Heading2: {
225
+ type: 'paragraph', name: 'heading 2', basedOn: 'Normal', next: 'Normal', qFormat: true,
226
+ run: { font: 'heading', sizePt: '小四', bold: true },
227
+ para: { indent: { firstLineChars: 0 }, spacing: { beforePt: 13, afterPt: 6 }, keepNext: true, outlineLevel: 1 },
228
+ },
229
+ Caption: {
230
+ type: 'paragraph', name: 'caption', basedOn: 'Normal', qFormat: true,
231
+ run: { font: 'caption', sizePt: '五号', bold: true },
232
+ para: { align: 'center', indent: { firstLineChars: 0 }, spacing: { beforePt: 6, afterPt: 6 } },
233
+ },
234
+ Quote: {
235
+ type: 'paragraph', name: 'Quote', basedOn: 'Normal', qFormat: true,
236
+ run: { font: 'quote' },
237
+ para: { indent: { leftChars: 200, firstLineChars: 0 } },
238
+ },
239
+ Header: { type: 'paragraph', name: 'header', basedOn: 'Normal', para: { indent: { firstLineChars: 0 }, align: 'center' }, run: { sizePt: '小五' } },
240
+ Footer: { type: 'paragraph', name: 'footer', basedOn: 'Normal', para: { indent: { firstLineChars: 0 }, align: 'center' }, run: { sizePt: '小五' } },
241
+ },
242
+ };
243
+ }
244
+
245
+ export const PRESETS = {
246
+ 办公标准: presetOffice,
247
+ 公文: presetGongwen,
248
+ 学术论文: presetAcademic,
249
+ };
250
+
251
+ /** token 校验:字段闭合 —— 未知字段直接报错,防 schema 野蛮生长 */
252
+ export const STYLE_KEYS = new Set(['type', 'name', 'basedOn', 'next', 'link', 'qFormat', 'uiPriority', 'run', 'para', 'extraXml']);
253
+ export const RUN_KEYS = new Set(['font', 'sizePt', 'bold', 'italic', 'color', 'underline', 'strike', 'caps',
254
+ 'smallCaps', 'vertAlign', 'em', 'kernPt', 'spacingTwip', 'highlight']);
255
+ export const PARA_KEYS = new Set(['align', 'outlineLevel', 'indent', 'spacing', 'keepNext', 'keepLines',
256
+ 'pageBreakBefore', 'widowControl', 'contextualSpacing', 'borders', 'shading', 'tabs', 'cjk', 'list']);
257
+
258
+ /**
259
+ * 行距规则的合法值。
260
+ *
261
+ * ⚠️ 单独校验它是因为**写错这个字段不会静默失效,会静默生效成荒谬值**:
262
+ * `lineRule` 只有 'multiple' 时 `line` 才是倍数,其余按磅算。写 `'auto'`
263
+ * (CSS/OOXML 里都存在这个词,很容易顺手打出来)会落进磅值分支,
264
+ * `line: 360` 本意「1.5 倍」就变成每行 360 磅 = 5 英寸,正文整个被挤出页面。
265
+ * 只查键名的闭合 schema 挡不住这种,得查值。
266
+ */
267
+ const LINE_RULES = new Set(['multiple', 'exact', 'atLeast']);
268
+
269
+ /** 字号:数字直接用,字符串必须是字号名 */
270
+ function badSize(v) {
271
+ return typeof v === 'string' && !(v in ZIHAO);
272
+ }
273
+
274
+ /**
275
+ * indent 里 firstLine* 和 hanging* 互斥。OOXML 的 w:ind 里它们是**同一个属性的
276
+ * 正负两面**(悬挂 = 负的首行缩进),同时给的话哪个生效随渲染器不同——LO 实测
277
+ * firstLine 覆盖 hanging,ECMA-376 又说 hanging 优先。写 `firstLineChars: 0` +
278
+ * `hangingChars` 的人是想"清掉继承的首行缩进再悬挂",但悬挂本身就含这层意思,
279
+ * 多写那个 0 反而把悬挂静默打掉(agent 实踩:两轮 build 白跑,2026-08-19 上报)。
280
+ * 跟 lineRule:'auto' 同族:写了不报错但生效成别的东西,必须在校验层拦。
281
+ */
282
+ export function indentConflict(ind) {
283
+ if (!ind || typeof ind !== 'object') return null;
284
+ const fl = ind.firstLineChars ?? ind.firstLineTwip;
285
+ const hg = ind.hangingChars ?? ind.hangingTwip;
286
+ if (fl == null || hg == null) return null;
287
+ return 'indent 的 firstLine* 和 hanging* 互斥(同一个属性的正负两面,同时给会静默覆盖悬挂)。'
288
+ + '要悬挂缩进只写 hanging*——悬挂天然含「首行回到左缘」,不用再补 firstLineChars: 0';
289
+ }
290
+
291
+ export function validateTokens(tok) {
292
+ const errs = [];
293
+ if (tok.v !== 1) errs.push('v must be 1');
294
+ const checkSpacing = (where, sp) => {
295
+ if (!sp) return;
296
+ if (sp.lineRule != null && !LINE_RULES.has(sp.lineRule)) {
297
+ errs.push(`${where}.spacing.lineRule: '${sp.lineRule}' 不是合法值,只有 ${[...LINE_RULES].join(' / ')}`
298
+ + "(multiple 时 line 是倍数如 1.5;exact/atLeast 时 line 是磅)");
299
+ }
300
+ if (sp.line != null && sp.lineRule === 'multiple' && sp.line > 10) {
301
+ errs.push(`${where}.spacing: lineRule=multiple 时 line 是**倍数**,${sp.line} 倍行距几乎肯定是把磅值写这儿了`);
302
+ }
303
+ };
304
+ for (const [id, st] of Object.entries(tok.styles ?? {})) {
305
+ for (const k of Object.keys(st)) if (!STYLE_KEYS.has(k)) errs.push(`styles.${id}: unknown key ${k}`);
306
+ for (const k of Object.keys(st.run ?? {})) if (!RUN_KEYS.has(k)) errs.push(`styles.${id}.run: unknown key ${k}`);
307
+ for (const k of Object.keys(st.para ?? {})) if (!PARA_KEYS.has(k)) errs.push(`styles.${id}.para: unknown key ${k}`);
308
+ if (st.run?.font && typeof st.run.font === 'string' && !tok.fonts?.[st.run.font]) {
309
+ errs.push(`styles.${id}.run.font: no such font slot '${st.run.font}'`);
310
+ }
311
+ if (badSize(st.run?.sizePt)) errs.push(`styles.${id}.run.sizePt: unknown 字号 ${st.run.sizePt}`);
312
+ if (st.basedOn && !tok.styles[st.basedOn]) errs.push(`styles.${id}.basedOn: no such style '${st.basedOn}'`);
313
+ checkSpacing(`styles.${id}.para`, st.para?.spacing);
314
+ const indErr = indentConflict(st.para?.indent);
315
+ if (indErr) errs.push(`styles.${id}.para.indent: ${indErr}`);
316
+ }
317
+ if (badSize(tok.base?.sizePt)) errs.push(`base.sizePt: unknown 字号 ${tok.base.sizePt}`);
318
+ checkSpacing('base', tok.base?.spacing);
319
+ // 自动编号(2026-08-18):定义本身的校验在 numbering.js,这里再钉一条**引用完整性**
320
+ // —— 引用一个不存在的编号名以前会产出悬空 numId(Word 打开可能直接报文档损坏),
321
+ // 这正是"闭合 schema 只查键名不查值"那类漏的延续。
322
+ errs.push(...validateNumbering(tok.numbering));
323
+ const numNames = new Set(Object.keys(tok.numbering ?? {}));
324
+ const checkList = (where, list) => {
325
+ if (!list) return;
326
+ const name = typeof list === 'string' ? list : list.name;
327
+ if (!name) errs.push(`${where}.list: 要写 { name: '编号名', ilvl?: N } 或直接写编号名`);
328
+ else if (!numNames.has(name)) {
329
+ errs.push(`${where}.list: 没有名为 '${name}' 的编号定义`
330
+ + (numNames.size ? `,现有:${[...numNames].join(' / ')}` : '(tokens.numbering 是空的)'));
331
+ }
332
+ };
333
+ for (const [id, st] of Object.entries(tok.styles ?? {})) checkList(`styles.${id}.para`, st.para?.list);
334
+ return errs;
335
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * units.js — OOXML 单位换算 + 中文字号表。
3
+ * twip = 1/20 pt;w:sz 单位是半磅;mm→twip 按 1440/25.4。
4
+ */
5
+
6
+ export const ptToTwip = (pt) => Math.round(pt * 20);
7
+ export const ptToHalf = (pt) => Math.round(pt * 2);
8
+ export const mmToTwip = (mm) => Math.round(mm * 1440 / 25.4);
9
+ export const twipToPt = (t) => t / 20;
10
+
11
+ /** 中文字号 → 磅 */
12
+ export const ZIHAO = {
13
+ 初号: 42, 小初: 36, 一号: 26, 小一: 24, 二号: 22, 小二: 18,
14
+ 三号: 16, 小三: 15, 四号: 14, 小四: 12, 五号: 10.5, 小五: 9,
15
+ 六号: 7.5, 小六: 6.5, 七号: 5.5, 八号: 5,
16
+ };
17
+
18
+ export function sizePt(v) {
19
+ if (typeof v === 'number') return v;
20
+ if (typeof v === 'string' && ZIHAO[v] != null) return ZIHAO[v];
21
+ throw new Error(`unknown font size: ${v}`);
22
+ }
23
+
24
+ export const PAGE_SIZES = {
25
+ A4: { wTwip: 11906, hTwip: 16838 },
26
+ A3: { wTwip: 16838, hTwip: 23811 },
27
+ Letter: { wTwip: 12240, hTwip: 15840 },
28
+ B5: { wTwip: 9978, hTwip: 14170 },
29
+ };