@xiaobuyu/nodesign 0.0.1 → 0.0.2
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/README.md +156 -106
- package/bin/nodesign.js +0 -0
- package/package.json +1 -1
- package/server/api/README.md +45 -45
- package/server/api/_guard.js +75 -75
- package/server/api/admin.js +187 -187
- package/server/api/assets/docx-page.js +103 -103
- package/server/api/assets/helpers.js +22 -22
- package/server/api/assets/notes.js +144 -144
- package/server/api/assets.js +896 -896
- package/server/api/board.js +57 -57
- package/server/api/browse.js +138 -138
- package/server/api/canvas.js +444 -444
- package/server/api/chatai.js +324 -324
- package/server/api/exports/build-standalone.js +980 -980
- package/server/api/exports/cards.js +118 -118
- package/server/api/exports/docx-pdf.js +46 -46
- package/server/api/exports/export-page.js +142 -142
- package/server/api/exports/handoff.js +147 -147
- package/server/api/exports.js +690 -690
- package/server/api/instruction.js +75 -75
- package/server/api/me.js +116 -116
- package/server/api/memory.js +185 -185
- package/server/api/pending-changes.js +366 -366
- package/server/api/plugins.js +164 -164
- package/server/api/projects.js +220 -220
- package/server/api/publish.js +64 -64
- package/server/api/recent.js +73 -73
- package/server/api/sessions.js +589 -589
- package/server/api/skills.js +77 -77
- package/server/api/standalone-fit.js +168 -168
- package/server/api/turn-compose.js +180 -180
- package/server/api/turn-inflight.js +44 -44
- package/server/auth/README.md +17 -17
- package/server/auth/middleware.js +138 -138
- package/server/auth/origin-guard.js +71 -71
- package/server/auth/session.js +108 -108
- package/server/auth/tier.js +113 -113
- package/server/auth/users-store.js +348 -348
- package/server/edit/README.md +29 -29
- package/server/engine/README.md +113 -113
- package/server/engine/agent/agent-shared.js +550 -550
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
- package/server/engine/agent/auto-mode-rules.js +78 -78
- package/server/engine/agent/context.js +418 -418
- package/server/engine/agent/events.js +464 -464
- package/server/engine/agent/hooks/canvas-validate.js +199 -199
- package/server/engine/agent/hooks/failure.js +120 -120
- package/server/engine/agent/hooks/file-events.js +75 -75
- package/server/engine/agent/hooks/lifecycle.js +233 -233
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
- package/server/engine/agent/hooks/post-guidance.js +206 -206
- package/server/engine/agent/hooks/post-subagent-report.js +104 -104
- package/server/engine/agent/hooks/post-trim.js +58 -58
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
- package/server/engine/agent/hooks/pre-defaults.js +84 -84
- package/server/engine/agent/hooks/pre-injectors.js +277 -277
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
- package/server/engine/agent/hooks/pre-starter-files.js +61 -61
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
- package/server/engine/agent/hooks/site-validate.js +94 -94
- package/server/engine/agent/hooks/tool-prompts.js +31 -31
- package/server/engine/agent/hooks/turn-state-memory.js +56 -56
- package/server/engine/agent/hooks/user-prompt-submit.js +215 -215
- package/server/engine/agent/hooks.js +354 -354
- package/server/engine/agent/init-contract.js +69 -69
- package/server/engine/agent/isolation.js +159 -159
- package/server/engine/agent/model-table.js +292 -292
- package/server/engine/agent/plugin-loader.js +266 -266
- package/server/engine/agent/prompts/nodesign-prelude.md +336 -336
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
- package/server/engine/agent/prompts/tools/site-reference.md +106 -106
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
- package/server/engine/agent/session-loop.js +1024 -1024
- package/server/engine/agent/session-model.js +164 -164
- package/server/engine/agent/skill.js +204 -204
- package/server/engine/agent/task-events.js +78 -78
- package/server/engine/agents/ds-extractor.md +132 -132
- package/server/engine/agents/explorer.md +190 -190
- package/server/engine/agents/index.js +230 -230
- package/server/engine/agents/schemas/design-system.json +156 -156
- package/server/engine/agents/schemas/tweak-schema.json +113 -113
- package/server/engine/agents/tweak-proposer.md +127 -127
- package/server/engine/agents/vision-checker.md +254 -254
- package/server/engine/browse/capture.js +422 -422
- package/server/engine/browse/card.js +137 -137
- package/server/engine/browse/handover.js +59 -59
- package/server/engine/browse/page-digest.js +119 -119
- package/server/engine/browse/refs.js +196 -196
- package/server/engine/browse/registry.js +307 -307
- package/server/engine/browse/screencast.js +170 -170
- package/server/engine/browse/state.js +121 -121
- package/server/engine/chatai/chat-log.js +79 -79
- package/server/engine/chatai/index.js +193 -193
- package/server/engine/chatai/openai-compat.js +152 -152
- package/server/engine/chatai/orchestrate.js +309 -309
- package/server/engine/chatai/perform.js +40 -40
- package/server/engine/chatai/summarize.js +79 -79
- package/server/engine/mcp/capability-gate.js +52 -52
- package/server/engine/mcp/index.js +339 -339
- package/server/engine/mcp/param-sanitizer.js +142 -142
- package/server/engine/mcp/tools/arrange-on-board.js +124 -124
- package/server/engine/mcp/tools/artifact-session.js +278 -278
- package/server/engine/mcp/tools/browse-computer.js +421 -421
- package/server/engine/mcp/tools/browse-find-batch.js +183 -183
- package/server/engine/mcp/tools/browse-screenshot.js +157 -157
- package/server/engine/mcp/tools/browse.js +480 -480
- package/server/engine/mcp/tools/build-docx.js +101 -101
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
- package/server/engine/mcp/tools/create-on-board.js +155 -155
- package/server/engine/mcp/tools/crystallize-skill.js +168 -168
- package/server/engine/mcp/tools/explain-style.js +237 -237
- package/server/engine/mcp/tools/export-handoff.js +133 -133
- package/server/engine/mcp/tools/expose-tweaks.js +149 -149
- package/server/engine/mcp/tools/generate-image.js +842 -842
- package/server/engine/mcp/tools/get-computed-styles.js +164 -164
- package/server/engine/mcp/tools/get-pending-changes.js +205 -205
- package/server/engine/mcp/tools/h3box-ssh.js +84 -84
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
- package/server/engine/mcp/tools/highlight.js +68 -68
- package/server/engine/mcp/tools/list-pages.js +222 -222
- package/server/engine/mcp/tools/lookup-tags.js +59 -59
- package/server/engine/mcp/tools/navigate-to-page.js +60 -60
- package/server/engine/mcp/tools/organize-board.js +64 -64
- package/server/engine/mcp/tools/paint-still.js +434 -434
- package/server/engine/mcp/tools/pin-to-board.js +130 -130
- package/server/engine/mcp/tools/preview-deck.js +130 -130
- package/server/engine/mcp/tools/profile-scroll.js +305 -305
- package/server/engine/mcp/tools/publish-site.js +102 -102
- package/server/engine/mcp/tools/query-elements.js +195 -195
- package/server/engine/mcp/tools/read-board.js +103 -103
- package/server/engine/mcp/tools/read-document.js +93 -93
- package/server/engine/mcp/tools/read-page.js +264 -264
- package/server/engine/mcp/tools/read-tavern-json.js +143 -143
- package/server/engine/mcp/tools/record-decision.js +140 -140
- package/server/engine/mcp/tools/relate-on-board.js +110 -110
- package/server/engine/mcp/tools/report-issue.js +115 -115
- package/server/engine/mcp/tools/roll-film.js +266 -266
- package/server/engine/mcp/tools/screenshot-docx.js +138 -138
- package/server/engine/mcp/tools/screenshot-url.js +195 -195
- package/server/engine/mcp/tools/screenshot.js +583 -583
- package/server/engine/mcp/tools/tier-gate.js +96 -96
- package/server/engine/mcp/tools/trace-motion.js +215 -215
- package/server/engine/mcp/tools/web-search.js +548 -548
- package/server/engine/motion/inventory.js +349 -349
- package/server/engine/perception/session.js +235 -235
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -192
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -162
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -228
- 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 -78
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -323
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
- package/server/engine/runs/active-runs.js +698 -698
- package/server/engine/runs/live-turn.js +252 -252
- package/server/engine/runs/store.js +364 -364
- package/server/engine/runs/turn-relay.js +217 -217
- package/server/engine/runtime/workspace.js +164 -164
- package/server/lib/artifact-file-path.js +80 -80
- package/server/lib/artifact-target.js +284 -284
- package/server/lib/asset-refs.js +239 -239
- package/server/lib/async-queue.js +104 -104
- package/server/lib/auto-relations.js +130 -130
- package/server/lib/binding-types.js +60 -60
- package/server/lib/board-kind-sizes.js +62 -62
- package/server/lib/board-relations.js +133 -133
- package/server/lib/browse-proxy.js +180 -180
- package/server/lib/canvas-id.js +36 -36
- package/server/lib/cover.js +227 -227
- package/server/lib/danbooru-tags.js +291 -291
- package/server/lib/doc-extract.js +156 -156
- package/server/lib/docx/build-from-source.js +260 -260
- package/server/lib/docx/build.js +490 -490
- package/server/lib/docx/dump-styles.js +287 -287
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
- package/server/lib/docx/merge-runs.js +129 -129
- package/server/lib/docx/numbering.js +218 -218
- package/server/lib/docx/order.js +152 -152
- package/server/lib/docx/rawzip.js +172 -172
- package/server/lib/docx/render.js +92 -92
- package/server/lib/docx/text-lint.js +225 -225
- package/server/lib/docx/tokens.js +335 -335
- package/server/lib/docx/units.js +29 -29
- package/server/lib/docx/xml.js +291 -291
- package/server/lib/docx-pages.js +187 -187
- package/server/lib/export-collect.js +289 -289
- package/server/lib/export-package.js +210 -210
- package/server/lib/html-srcset.js +145 -145
- package/server/lib/image-variant.js +460 -460
- package/server/lib/ingress/forward-openai-chat.js +305 -305
- package/server/lib/ingress/openai-chat.js +491 -491
- package/server/lib/ingress/session-notice.js +66 -66
- package/server/lib/ingress/session-routes.js +68 -68
- package/server/lib/ingress/slot-probe.js +130 -130
- package/server/lib/ingress/upstream-billing.js +52 -52
- package/server/lib/ingress/upstream-fail-streak.js +72 -72
- package/server/lib/ingress/upstream-truncation.js +48 -48
- package/server/lib/issues-store.js +182 -182
- package/server/lib/kinds/deck.js +72 -72
- package/server/lib/kinds/docx.js +283 -283
- package/server/lib/kinds/file-kinds.js +113 -113
- package/server/lib/kinds/index.js +202 -202
- package/server/lib/kinds/site.js +161 -161
- package/server/lib/model-ingress.js +543 -543
- package/server/lib/moderation.js +255 -255
- package/server/lib/notice-store.js +103 -103
- package/server/lib/plugin-install.js +104 -104
- package/server/lib/plugin-validator.js +721 -721
- package/server/lib/publish-store.js +115 -115
- package/server/lib/quick-summary.js +52 -52
- package/server/lib/quota.js +270 -270
- package/server/lib/rate-window.js +29 -29
- package/server/lib/reference-assets.js +92 -92
- package/server/lib/region-shot.js +135 -135
- package/server/lib/safe-path.js +73 -73
- package/server/lib/sdk-session.js +33 -33
- package/server/lib/showcase-store.js +92 -92
- package/server/lib/site-publish.js +465 -465
- package/server/lib/ssrf-guard.js +432 -432
- package/server/lib/task-scan.js +158 -158
- package/server/lib/tavern-json.js +154 -154
- package/server/lib/video-variant.js +237 -237
- package/server/lib/workspace-path.js +33 -33
- package/server/ops/fix-sdk-musl.mjs +40 -40
- package/server/ops/install-macos-fonts.sh +114 -114
- package/server/ops/macos-fonts.conf +336 -336
- package/server/ops/sandbox-shim/bwrap +57 -57
- package/server/projects/assets-summary.js +119 -119
- package/server/projects/auto-name.js +53 -53
- package/server/projects/board-store.js +640 -640
- package/server/projects/move-entry.js +103 -103
- package/server/projects/store.js +270 -270
- package/server/projects/ui-config.js +54 -54
- package/server/projects/workspace-templates.js +53 -53
- package/server/projects/workspace.js +1025 -1025
- package/server/runtime/local-config.js +177 -177
- package/server/runtime/profile.js +72 -72
- package/server/services/rembg-service.py +334 -334
- package/server/shared/README.md +25 -25
- package/server/shared/deck.js +54 -54
- package/server/shared/time.js +49 -49
- package/server/style-pipeline/README.md +53 -53
- package/server/ws/broker.js +36 -36
- package/server/ws/browse-channel.js +177 -177
- package/server/ws/index.js +386 -386
- package/web/README.md +89 -89
- package/web/dist/assets/BrowserWindow-BYCgizHw.js +11 -0
- package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-pNydEM4L.js} +1 -1
- package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-PPgzh3Gq.js} +1 -1
- package/web/dist/assets/{SiteWindow-D6eS-2gM.js → SiteWindow-DM0LQPCO.js} +1 -1
- package/web/dist/assets/{index-Cbl3ATvp.js → index-BHTBJlpY.js} +32 -32
- package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-C0I13CbD.js} +1 -1
- package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-B1UQ8Hld.js} +1 -1
- package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-8lrgqmfv.js} +1 -1
- package/web/dist/index.html +17 -17
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
|
@@ -1,225 +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
|
-
}
|
|
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
|
+
}
|