24 KiB
Executable File
Common Rules
Shared rules referenced by all scene files. Scene-specific overrides take precedence.
Default Page Layout
A4 portrait. Unless the scene specifies otherwise, use:
| Property | Value | Twips |
|---|---|---|
| Page width | 21.0 cm | 11906 |
| Page height | 29.7 cm | 16838 |
| Top margin | 2.54 cm | 1440 |
| Bottom margin | 2.54 cm | 1440 |
| Left margin | 3.0 cm | 1701 |
| Right margin | 2.5 cm | 1417 |
page: {
size: { width: 11906, height: 16838, orientation: PageOrientation.PORTRAIT },
margin: { top: 1440, bottom: 1440, left: 1701, right: 1417 },
}
Scene overrides:
- Official doc (GB/T 9704 red-header): top 2098, bottom 1984, left 1588, right 1474
- Exam: top/bottom 1134 (2 cm), left/right 1134 (2 cm)
Default Font Specifications
Two font profiles exist. Each scene declares which profile it uses.
Profile A: Formal (report, academic, contract, official-doc, exam)
| Element | CN Font | EN Font | Size | Notes |
|---|---|---|---|---|
| H1 | SimHei | Times New Roman | 16 pt (size: 32) | Bold, centered |
| H2 | SimHei | Times New Roman | 15 pt (size: 30) | Bold |
| H3 | SimHei | Times New Roman | 14 pt (size: 28) | Bold |
| Body | SimSun | Times New Roman | 12 pt (size: 24) | |
| Caption | SimSun | Times New Roman | 10.5 pt (size: 21) |
- Text color: always pure black
"000000"(never dark-blue-grey) - First-line indent: 480 twips (2 chars at SimSun 12pt)
- Line spacing: 312 (1.3x).
- Color routing for non-report documents: When the document is a short-form text (essay, evaluation, letter, speech, application, reflection, etc.) rather than a structured report/whitepaper/proposal/consulting deliverable, heading color MUST use pure black
"000000"instead ofpalette.primary. Colored headings are reserved for documents that need brand/professional identity (reports with covers, whitepapers, proposals, consulting deliverables).
Profile B: Visual (resume, copywriting)
| Element | CN Font | EN Font | Size |
|---|---|---|---|
| Name/Title | Microsoft YaHei | Calibri | Varies |
| Body | Microsoft YaHei | Calibri | 10–11 pt |
| Caption | Microsoft YaHei | Calibri | 9 pt |
- First-line indent: 420 twips (2 chars at YaHei)
- Color: per design-system palette
Official-Doc Font Override (GB/T 9704)
When needsRedHeader() = true:
| Element | Font | Size |
|---|---|---|
| Red header org name | STXiaoBiaoSong (or SimSun bold) | 26 pt (size: 52) |
| Title | STXiaoBiaoSong (or SimHei) | 22 pt (size: 44) |
| Body | FangSong | 16 pt (size: 32) |
| Section heading | FangSong_GB2312 bold (or HeiTi) | 16 pt (size: 32) |
- Line spacing: 560 (28 pt fixed)
- First-line indent: 640 twips (2 chars at FangSong 16pt)
Chinese Font Size Reference
| Name | Points | Half-points (size:) |
|---|---|---|
| Chu Hao (initial) | 42 | 84 |
| Xiao Chu | 36 | 72 |
| Yi Hao (1st) | 26 | 52 |
| Xiao Yi | 24 | 48 |
| Er Hao (2nd) | 22 | 44 |
| Xiao Er | 18 | 36 |
| San Hao (3rd) | 16 | 32 |
| Xiao San | 15 | 30 |
| Si Hao (4th) | 14 | 28 |
| Xiao Si | 12 | 24 |
| Wu Hao (5th) | 10.5 | 21 |
| Xiao Wu | 9 | 18 |
| Liu Hao (6th) | 7.5 | 15 |
Placeholder Convention
When required information is missing, use standardized placeholders so users can Find & Replace in Word.
Format: Always use full-width brackets 【 】.
| Type | Format | Example |
|---|---|---|
| General field | 【field name】 |
Name: 【company name】 |
| Monetary amount | 【RMB in words: yuan (lowercase: ¥)】 |
Amount: 【RMB in words】 |
| Date field | 【____/____/____】 |
Signing date: 【//____】 |
| Long text | 【Please fill in: ______】 |
Delivery criteria: 【Please fill in: ______】 |
| Attachment ref | 【See Appendix 1: ______】 |
Rules:
- Placeholder format must be consistent throughout the entire document
- Each placeholder must specify exactly what is needed (never use vague "TBD" or "to be completed")
- Never hard-code unconfirmed critical facts; use a placeholder instead
- Never use sloppy expressions like "to be refined", "omitted", "user fills in later"
Title Orphan Prevention (All Scenes)
Body headings (H1/H2/H3) and cover titles must avoid leaving 1–2 characters alone on the last line. This rule applies to ALL document types.
For cover titles: Always use calcTitleLayout() + splitTitleLines() from design-system.md — these handle orphan prevention automatically (merges ≤2-char last lines into the previous line).
For body headings (H1/H2/H3): When a heading text is long enough to wrap, apply the same splitTitleLines() logic. If the heading would cause a single-character orphan in Word's auto-wrapping, manually split into multiple TextRun elements with a Break (soft line break) at a semantic boundary.
const { Break } = require("docx");
// Check if heading needs manual line break to prevent orphan
function buildHeadingRuns(text, maxCharsPerLine, runProps) {
// If text fits in one line, no action needed
if (text.length <= maxCharsPerLine) {
return [new TextRun({ text, ...runProps })];
}
// Use splitTitleLines to find semantic break points
const lines = splitTitleLines(text, maxCharsPerLine);
const runs = [];
for (let i = 0; i < lines.length; i++) {
if (i > 0) runs.push(new TextRun({ break: 1, ...runProps, text: "" })); // soft line break
runs.push(new TextRun({ text: lines[i], ...runProps }));
}
return runs;
}
Estimation for maxCharsPerLine: For centered headings, estimate available width = page width - left margin - right margin. For SimHei at a given pt size, each CJK char ≈ pt × 20 twips wide. Divide available width by char width to get maxCharsPerLine.
Undefined / Null Value Prevention (Mandatory)
Generated code MUST guard against outputting literal undefined, null, NaN, or empty strings for any visible text field. This is a hard requirement — these are never acceptable in a delivered document.
// ✅ MANDATORY: Safe text helper — use for ALL user-facing text values
function safeText(value, placeholder) {
if (value === undefined || value === null || value === "" || String(value) === "NaN" || String(value) === "undefined") {
return placeholder || "【Please fill in】";
}
return String(value);
}
// Usage:
new TextRun({ text: safeText(config.contact, "【Contact person】") })
new TextRun({ text: safeText(row.phone, "【Phone number】") })
Rules:
- Every
TextRundisplaying user-provided or config-derived data MUST usesafeText()or equivalent guard - If a field is optional and not provided, use
【Please fill in: field_name】placeholder (full-width brackets) - Table cells with missing data: show
【Please fill in】, never leave as empty string or undefined - This applies to ALL scenes — contracts, reports, academic, exams, etc.
WPS / Office Word Compatibility (Mandatory)
Generated .docx files must render consistently in both Microsoft Office Word and WPS Office. The following OOXML features have known compatibility issues — avoid or use carefully.
Features to AVOID (high incompatibility risk)
| Feature | Issue | Alternative |
|---|---|---|
Text-character decorative lines (e.g., ───, ━━━, ═══, ——————) |
Character-drawn lines depend on font metrics and rendering engine — they appear different widths/lengths in MS Office vs WPS, often truncated or misaligned. They cannot span a controlled width. | Always use paragraph borders (border.top, border.bottom) for horizontal decorative lines. Paragraph borders render consistently across engines and respect indent for precise width control. See recipe R2 for correct implementation. |
Default table borders on cover wrapper tables (forgetting allNoBorders) |
docx-js default table borders are single/auto/sz=4. On the 16838-high cover wrapper, these borders add ~8 twips of extra height per edge. MS Office includes border thickness in height calculation, causing content to overflow by a few twips → blank page 2. WPS is more lenient and may absorb the overflow. |
Every cover wrapper table MUST explicitly set borders: allNoBorders (all 6 border positions = NONE). Never rely on defaults. Define the allNoBorders constant and use it consistently. |
verticalAlign: "center" or "bottom" in exact-height TableRow |
WPS ignores vertical alignment in exact-height rows; content may clip or shift | Use verticalAlign: "top" + spacing.before to position content. Avoid margins.top/margins.bottom in exact-height cells — they reduce available height unpredictably across engines |
characterSpacing (large values) |
WPS renders differently from Word; letter spacing may collapse or expand | Keep characterSpacing ≤ 80; for cover English labels, test both renderers |
margins.top/margins.bottom inside exact-height cells |
MS Office and WPS calculate remaining height differently when cell margins are present | Use spacing.before on the first paragraph for vertical positioning; only use margins.left/margins.right |
| Complex nested Tables inside exact-height cells | WPS height calculation differs from Word; content may overflow or clip | Wrap everything in a single 16838 outer wrapper cell (R1 architecture). Nested tables inside are acceptable when the outer wrapper provides a safety net |
Large font without explicit spacing.line |
Paragraph inherits small line spacing from document default (e.g., 560tw for body); font taller than line height → top of characters clipped | Always set spacing: { line: fontPt * 23, lineRule: "atLeast" } on paragraphs with font size > body text |
ShadingType.SOLID |
WPS shows solid black instead of intended color | Always use ShadingType.CLEAR |
OOXML raw XML for columns (w:cols) |
WPS column rendering may differ | Use only when explicitly needed (A3 exam papers); test output |
titlePage: true with complex headers/footers |
WPS may not properly suppress first-page header/footer | Use separate sections instead of titlePage flag |
| Tab stops for alignment | WPS tab width may differ from Word | Use borderless Tables for alignment instead |
Features that are SAFE (consistent rendering)
| Feature | Notes |
|---|---|
| Borderless Tables for layout | Both renderers handle well |
ShadingType.CLEAR with fill color |
Consistent |
rule: "exact" on single-level TableRow |
Works in both (avoid with nested Tables) |
| Paragraph borders (left, bottom, etc.) | Consistent |
spacing.before / spacing.after |
Consistent |
| Standard fonts (SimHei, SimSun, YaHei, TNR, Calibri) | Available on both platforms |
PageBreak inside Paragraph |
Consistent |
Section breaks (SectionType.NEXT_PAGE) |
Consistent |
Mandatory Compatibility Checks (Post-Generation)
Add to quality self-check:
- No
ShadingType.SOLIDanywhere (search codebase) - No
verticalAlign: "center"or"bottom"in exact-height rows - No tab-stop alignment for party info or data alignment (use Tables)
- Covers use the 16838 outer wrapper architecture (R1 pattern) with
spacing.beforefor positioning; nomargins.top/margins.bottomin exact-height cells - Cover section margin =
{ top: 0, bottom: 0, left: 0, right: 0 }— non-zero margins cause wrapper to shrink away from page edges - Cover wrapper row has
height: { value: 16838, rule: "exact" }— without this, content overflows or leaves whitespace - Cover is in a separate section from body content — cover and body must not share a section
- Cover wrapper table uses explicit
allNoBorders— never rely on default table borders (causes blank page 2 in MS Office) - No text-character decorative lines (
───,━━━,═══,——————) — use paragraph borders instead characterSpacingvalues ≤ 80 throughout- TOC: follow
references/toc.mdchecklist (heading style, TableOfContents element, PageBreak, post-processing script) - All tables use
WidthType.PERCENTAGEfor column widths (WPS tblGrid bug; if DXA is unavoidable, setcolumnWidthsexplicitly)
// ✅ Correct — percentage widths, WPS-safe
new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
rows: [new TableRow({ children: [
new TableCell({ width: { size: 30, type: WidthType.PERCENTAGE }, children: [...] }),
new TableCell({ width: { size: 70, type: WidthType.PERCENTAGE }, children: [...] }),
]})],
});
// ❌ WRONG — DXA widths cause WPS tblGrid mismatch (all gridCol=100)
new TableCell({ width: { size: 3000, type: WidthType.DXA }, ... })
Universal Prohibitions
These apply to ALL scenes. Scene files may add scene-specific prohibitions.
- No outlines-only — always produce a complete, finished document
- No chat-style output — the document must not read like a conversation or explanation
- No fake TOC / page numbers / headers — use proper docx-js structures
- No excessive blank lines to pad layout
- No dirty formatting — no stray annotations, template fragments, broken hyperlinks, garbled markers
- No sloppy placeholders — "TBD", "omitted", "略", "to be refined" are forbidden; use proper
【】placeholders - No fabricated data — do not invent statistics, citations, legal references, or facts to appear professional
- No inconsistent heading/numbering — one numbering system per document, no level-skipping
- No Markdown artifacts — no
#,**,-list markers,>blockquotes, and no Markdown table syntax (| col1 | col2 |,|---|---|) in the final docx. Any tabular data MUST be rendered as a proper docxTableobject — never as plain-text pipe-delimited lines. This applies to ALL scenes including exam paper data tables, report statistics, and academic result tables. - No bullet-list documents — body text must be proper paragraphs, not endless bullet points
Letter / Correspondence Format (Universal)
When generating any letter-style document (invitation letter, thank-you letter, cover letter, recommendation letter, English essay in letter format, etc.), the following layout rules apply regardless of scene:
- Complimentary close and sender name MUST be right-aligned — e.g., "Yours sincerely,", "Best regards,", "Yours,", and the sender name below it must use
alignment: AlignmentType.RIGHT - Date — if placed at the top of the letter, right-aligned; if at the bottom, right-aligned with the closing
- Salutation ("Dear Mr. Smith," / "Dear Mike,") — left-aligned, followed by a blank line or
spacing.after - Body paragraphs — left-aligned (English) or justified (CJK), with appropriate
spacing.afterbetween paragraphs
// ✅ Correct — closing and sender right-aligned
new Paragraph({ alignment: AlignmentType.RIGHT, spacing: { before: 400 },
children: [new TextRun({ text: "Yours sincerely,", size: 24 })] }),
new Paragraph({ alignment: AlignmentType.RIGHT,
children: [new TextRun({ text: "Li Hua", size: 24 })] }),
// ❌ WRONG — closing left-aligned (default)
new Paragraph({
children: [new TextRun({ text: "Yours sincerely," })] }),
Quality Self-Check (Universal)
→ See SKILL.md § Post-Generation — Two-Layer Verification for the complete checklist.
Scene files add scene-specific checks on top of that universal checklist.
Execution Priority
When rules conflict, follow this precedence (highest first):
- User-provided template or explicit instructions — always override defaults
- Scene-specific rules — override common rules and design-system defaults
- Common rules (this file) — override design-system aesthetic defaults
- Design-system defaults — baseline aesthetics
Cover Recipes
See references/design-system.md for the 7 validated cover recipes (R1–R7) and 14 color palettes.
Cover recipe selection: selectCoverRecipe(docType, industry, titleLength) — defined in references/design-system.md (authoritative source).
Cover Title Layout Rules (Mandatory)
These rules apply to ALL cover recipes (R1–R7). They prevent the most common cover quality issues: title overflow, content spilling to page 2, and mid-word line breaks.
Rule 1: Always use calcTitleLayout()
Every cover MUST call calcTitleLayout(title, availableWidth) from design-system.md to determine:
- Font size (dynamically calculated, never hardcoded above 40pt)
- Line breaks (semantically split, never mid-word)
Forbidden: Passing the full title as a single long TextRun and letting Word auto-wrap. This causes uncontrolled line breaks at arbitrary character positions.
Rule 2: No single-character orphan lines
If the last line of a title contains only 1–2 characters, merge it into the previous line. The splitTitleLines() function handles this automatically.
Rule 3: No mid-word breaks for CJK text
Line breaks must occur at semantic boundaries: after particles (e.g., de/yu/he/ji/zhi), punctuation, connectors, spaces, or underscores. Never split a compound term (e.g., a 4-character term like a management specification must not be split into 3+1 characters).
For mixed Chinese+English titles (e.g., "基于Transformer架构的..."), use estimateTextWidth() instead of character count for line break calculation. Chinese characters are ~2× wider than English characters at the same font size.
Rule 4: Maximum 3 title lines on cover
Cover titles must not exceed 3 lines. If the title is too long, reduce font size (down to minimum 24pt) before adding more lines. If it still exceeds 3 lines at 24pt, force 3 lines with longer line lengths.
Rule 5: Always use calcCoverSpacing() for whitespace
Spacing values (spacing.before) in cover elements must be dynamically calculated, not hardcoded. Fixed values like before: 4500 assume a specific title length and will cause overflow with longer titles.
Rule 6: Cover height budget validation
Before generating, verify that total content height stays within 15638 twips (16838 page height minus 1200 twips safety margin — MS Office renders large fonts taller than calculated). Each recipe in design-system.md includes height budget annotations — verify during generation.
Rule 7: R5 meta info table (academic covers)
Academic cover meta info must use a 2-column table with percentage widths only (NOT DXA — WPS breaks with DXA widths):
- Table width: adaptive 55–75% of page, calculated by
calcR5MetaLayout()indesign-system.md. Table is centered viaalignment: CENTER. - Label column: adaptive 25–45% of table width, LEFT aligned, plain text label + ":". NO full-width space padding, NO right-alignment, NO distributed alignment.
- Value column: remaining percentage, LEFT aligned,
bottom border single sz=4= fixed-length underline (same length for all rows regardless of value text length). - Label column borders: none (NO bottom border on label cells).
- ⚠️ Do NOT use DXA widths, full-width space padding (
\u3000), spacer columns, or tab stops — these render inconsistently between MS Office and WPS.
Rule 8: Large font paragraphs must set explicit line spacing
When a paragraph uses a font size larger than the document body text (e.g., cover titles at 36pt+), it MUST set explicit spacing.line to prevent clipping. Without it, the paragraph inherits the document/style default line spacing (often 560 twips for body text), which is smaller than the font height → the top of characters gets clipped.
Formula: spacing.line = Math.ceil(fontPt * 23) with lineRule: "atLeast"
Example: A 36pt title needs spacing: { line: 828, lineRule: "atLeast" }. Without this, the inherited line=560 clips the top 160 twips of the text.
This applies to ALL large-font paragraphs (cover titles, chapter headings, decorative text), not just covers.
Rule 9: Every TextRun on a colored background MUST set explicit color
⚠️ CRITICAL: When a TextRun is inside a cell/area with a dark or colored background (shading), it MUST explicitly set the color property. Omitting color defaults to black (#000000), which is invisible on dark backgrounds.
Common mistake: Subtitle or meta text on R1/R2/R4 dark cover blocks without color → appears as invisible black text on dark bg.
Rule: For any TextRun inside a shaded cell:
- Use
P.cover.titleColorfor title text - Use
P.cover.subtitleColorfor subtitle text - Use
P.cover.metaColorfor meta info text - Use
P.cover.footerColorfor footer text - NEVER rely on default color when background is not white
Rule 10: Page number API nesting and 3-section numbering
⚠️ CRITICAL: Page number settings MUST be nested inside page.pageNumbers:
// ❌ WRONG — docx-js ignores top-level pageNumberStart/pageNumberFormatType
properties: { pageNumberStart: 1, pageNumberFormatType: NumberFormat.DECIMAL }
// ✅ CORRECT
properties: { page: { pageNumbers: { start: 1, formatType: NumberFormat.DECIMAL } } }
Standard page numbering (5-zone convention):
All multi-section documents MUST follow this five-zone page numbering scheme unless the user explicitly requests otherwise.
| Zone | Section | pageNumbers | Footer instrText | Notes |
|---|---|---|---|---|
| 1. Cover | Title page | None (no footer) | — | Always logical page 1, but number is hidden |
| 2. Front matter | Abstract, TOC, Preface | { start: 1, formatType: UPPER_ROMAN } |
PAGE \* ROMAN \* MERGEFORMAT |
Separate Roman numeral sequence (i, ii, iii…) |
| 3. Body | Main content | { start: 1, formatType: DECIMAL } |
PAGE \* arabic \* MERGEFORMAT |
Resets to 1 |
| 4. Appendix | Appendices (A, B, C…) | Continues body (no reset) | Same as body | No section break needed unless different headers required |
| 5. References | Bibliography | Continues body (no reset) | Same as body | If body ends on p.42, references continue from p.43 |
Key rules:
0. NEVER use "Page X of Y" denominator format. Footer must show only the current page number (e.g., 1, 2, iii). Do NOT display total page count. No Page 3 of 12, no 3 / 12, no 第3页/共12页. Just the bare number. PageNumber.TOTAL_PAGES / NUMPAGES is FORBIDDEN in footers.
- Cover is always page 1 internally but the page number is never displayed. Suppress footer in cover section.
- Front matter uses independent Roman numerals starting at
i. This sequence is separate from the body. - Body resets to Arabic 1. The first page of main content is always page
1. - Appendix and references continue the body sequence. No reset between body → appendix → references.
- Documents without front matter skip zone 2 (cover hidden, body starts at Arabic 1).
- Documents without cover start body (or front matter) at page 1 directly.
- Short documents (≤3 pages): simple Arabic 1, 2, 3 throughout, no cover/frontmatter distinction.
- Single-page documents (certificates, letters): no page numbering at all.
3-section docx-js implementation (for documents with TOC):
At minimum, implement zones 1–3 as separate docx sections:
// Section 1: Cover — no page number
properties: { page: { /* no pageNumbers */ } }
// No footer children, or empty footer
// Section 2: Front matter — Roman numerals
properties: { page: { pageNumbers: { start: 1, formatType: NumberFormat.UPPER_ROMAN } } }
// Footer: PAGE \* ROMAN \* MERGEFORMAT
// Section 3: Body — Arabic, reset to 1
properties: { page: { pageNumbers: { start: 1, formatType: NumberFormat.DECIMAL } } }
// Footer: PAGE \* arabic \* MERGEFORMAT
// Appendix and References: same section as body (continues numbering)
// Only create a new section if different header/footer content is needed
Post-processing required (WPS compatibility):
- Remove empty
<w:pgNumType/>from cover section XML - Patch footer instrText: replace bare
PAGEwith format-specificPAGE \* ROMANorPAGE \* arabic
See toc.md § Page Number API for full details.