feat: Import 35+ skills, merge duplicates, add openclaw installer
Major updates: - Added 35+ new skills from awesome-opencode-skills and antigravity repos - Merged SEO skills into seo-master - Merged architecture skills into architecture - Merged security skills into security-auditor and security-coder - Merged testing skills into testing-master and testing-patterns - Merged pentesting skills into pentesting - Renamed website-creator to thai-frontend-dev - Replaced skill-creator with github version - Removed Chutes references (use MiniMax API instead) - Added install-openclaw-skills.sh for cross-platform installation - Updated .env.example with MiniMax API credentials
This commit is contained in:
3
skills/minimax-docx/.gitignore
vendored
Normal file
3
skills/minimax-docx/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
obj/
|
||||
bin/
|
||||
*.user
|
||||
21
skills/minimax-docx/LICENSE
Normal file
21
skills/minimax-docx/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 MiniMaxAI
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
274
skills/minimax-docx/SKILL.md
Normal file
274
skills/minimax-docx/SKILL.md
Normal file
@@ -0,0 +1,274 @@
|
||||
---
|
||||
name: minimax-docx
|
||||
license: MIT
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
category: document-processing
|
||||
author: MiniMaxAI
|
||||
sources:
|
||||
- "ECMA-376 Office Open XML File Formats"
|
||||
- "GB/T 9704-2012 Layout Standard for Official Documents"
|
||||
- "IEEE / ACM / APA / MLA / Chicago / Turabian Style Guides"
|
||||
- "Springer LNCS / Nature / HBR Document Templates"
|
||||
description: >
|
||||
Professional DOCX document creation, editing, and formatting using OpenXML SDK (.NET).
|
||||
Three pipelines: (A) create new documents from scratch, (B) fill/edit content in existing
|
||||
documents, (C) apply template formatting with XSD validation gate-check.
|
||||
MUST use this skill whenever the user wants to produce, modify, or format a Word document —
|
||||
including when they say "write a report", "draft a proposal", "make a contract",
|
||||
"fill in this form", "reformat to match this template", or any task whose final output
|
||||
is a .docx file. Even if the user doesn't mention "docx" explicitly, if the task
|
||||
implies a printable/formal document, use this skill.
|
||||
triggers:
|
||||
- Word
|
||||
- docx
|
||||
- document
|
||||
- 文档
|
||||
- Word文档
|
||||
- 报告
|
||||
- 合同
|
||||
- 公文
|
||||
- 排版
|
||||
- 套模板
|
||||
---
|
||||
|
||||
# minimax-docx
|
||||
|
||||
Create, edit, and format DOCX documents via CLI tools or direct C# scripts built on OpenXML SDK (.NET).
|
||||
|
||||
## Setup
|
||||
|
||||
**First time:** `bash scripts/setup.sh` (or `powershell scripts/setup.ps1` on Windows, `--minimal` to skip optional deps).
|
||||
|
||||
**First operation in session:** `scripts/env_check.sh` — do not proceed if `NOT READY`. (Skip on subsequent operations within the same session.)
|
||||
|
||||
## Quick Start: Direct C# Path
|
||||
|
||||
When the task requires structural document manipulation (custom styles, complex tables, multi-section layouts, headers/footers, TOC, images), write C# directly instead of wrestling with CLI limitations. Use this scaffold:
|
||||
|
||||
```csharp
|
||||
// File: scripts/dotnet/task.csx (or a new .cs in a Console project)
|
||||
// dotnet run --project scripts/dotnet/MiniMaxAIDocx.Cli -- run-script task.csx
|
||||
#r "nuget: DocumentFormat.OpenXml, 3.2.0"
|
||||
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
|
||||
using var doc = WordprocessingDocument.Create("output.docx", WordprocessingDocumentType.Document);
|
||||
var mainPart = doc.AddMainDocumentPart();
|
||||
mainPart.Document = new Document(new Body());
|
||||
|
||||
// --- Your logic here ---
|
||||
// Read the relevant Samples/*.cs file FIRST for tested patterns.
|
||||
// See Samples/ table in References section below.
|
||||
```
|
||||
|
||||
**Before writing any C#, read the relevant `Samples/*.cs` file** — they contain compilable, SDK-version-verified patterns. The Samples table in the References section below maps topics to files.
|
||||
|
||||
## CLI shorthand
|
||||
|
||||
All CLI commands below use `$CLI` as shorthand for:
|
||||
```bash
|
||||
dotnet run --project scripts/dotnet/MiniMaxAIDocx.Cli --
|
||||
```
|
||||
|
||||
## Pipeline routing
|
||||
|
||||
Route by checking: does the user have an input .docx file?
|
||||
|
||||
```
|
||||
User task
|
||||
├─ No input file → Pipeline A: CREATE
|
||||
│ signals: "write", "create", "draft", "generate", "new", "make a report/proposal/memo"
|
||||
│ → Read references/scenario_a_create.md
|
||||
│
|
||||
└─ Has input .docx
|
||||
├─ Replace/fill/modify content → Pipeline B: FILL-EDIT
|
||||
│ signals: "fill in", "replace", "update", "change text", "add section", "edit"
|
||||
│ → Read references/scenario_b_edit_content.md
|
||||
│
|
||||
└─ Reformat/apply style/template → Pipeline C: FORMAT-APPLY
|
||||
signals: "reformat", "apply template", "restyle", "match this format", "套模板", "排版"
|
||||
├─ Template is pure style (no content) → C-1: OVERLAY (apply styles to source)
|
||||
└─ Template has structure (cover/TOC/example sections) → C-2: BASE-REPLACE
|
||||
(use template as base, replace example content with user content)
|
||||
→ Read references/scenario_c_apply_template.md
|
||||
```
|
||||
|
||||
If the request spans multiple pipelines, run them sequentially (e.g., Create then Format-Apply).
|
||||
|
||||
## Pre-processing
|
||||
|
||||
Convert `.doc` → `.docx` if needed: `scripts/doc_to_docx.sh input.doc output_dir/`
|
||||
|
||||
Preview before editing (avoids reading raw XML): `scripts/docx_preview.sh document.docx`
|
||||
|
||||
Analyze structure for editing scenarios: `$CLI analyze --input document.docx`
|
||||
|
||||
## Scenario A: Create
|
||||
|
||||
Read `references/scenario_a_create.md`, `references/typography_guide.md`, and `references/design_principles.md` first. Pick an aesthetic recipe from `Samples/AestheticRecipeSamples.cs` that matches the document type — do not invent formatting values. For CJK, also read `references/cjk_typography.md`.
|
||||
|
||||
**Choose your path:**
|
||||
- **Simple** (plain text, minimal formatting): use CLI — `$CLI create --type report --output out.docx --config content.json`
|
||||
- **Structural** (custom styles, multi-section, TOC, images, complex tables): write C# directly. Read the relevant `Samples/*.cs` first.
|
||||
|
||||
CLI options: `--type` (report|letter|memo|academic), `--title`, `--author`, `--page-size` (letter|a4|legal|a3), `--margins` (standard|narrow|wide), `--header`, `--footer`, `--page-numbers`, `--toc`, `--content-json`.
|
||||
|
||||
Then run the **validation pipeline** (below).
|
||||
|
||||
## Scenario B: Edit / Fill
|
||||
|
||||
Read `references/scenario_b_edit_content.md` first. Preview → analyze → edit → validate.
|
||||
|
||||
**Choose your path:**
|
||||
- **Simple** (text replacement, placeholder fill): use CLI subcommands.
|
||||
- **Structural** (add/reorganize sections, modify styles, manipulate tables, insert images): write C# directly. Read `references/openxml_element_order.md` and the relevant `Samples/*.cs`.
|
||||
|
||||
Available CLI edit subcommands:
|
||||
- `replace-text --find "X" --replace "Y"`
|
||||
- `fill-placeholders --data '{"key":"value"}'`
|
||||
- `fill-table --data table.json`
|
||||
- `insert-section`, `remove-section`, `update-header-footer`
|
||||
|
||||
```bash
|
||||
$CLI edit replace-text --input in.docx --output out.docx --find "OLD" --replace "NEW"
|
||||
$CLI edit fill-placeholders --input in.docx --output out.docx --data '{"name":"John"}'
|
||||
```
|
||||
|
||||
Then run the **validation pipeline**. Also run diff to verify minimal changes:
|
||||
```bash
|
||||
$CLI diff --before in.docx --after out.docx
|
||||
```
|
||||
|
||||
## Scenario C: Apply Template
|
||||
|
||||
Read `references/scenario_c_apply_template.md` first. Preview and analyze both source and template.
|
||||
|
||||
```bash
|
||||
$CLI apply-template --input source.docx --template template.docx --output out.docx
|
||||
```
|
||||
|
||||
For complex template operations (multi-template merge, per-section headers/footers, style merging), write C# directly — see Critical Rules below for required patterns.
|
||||
|
||||
Run the **validation pipeline**, then the **hard gate-check**:
|
||||
```bash
|
||||
$CLI validate --input out.docx --gate-check assets/xsd/business-rules.xsd
|
||||
```
|
||||
Gate-check is a **hard requirement**. Do NOT deliver until it passes. If it fails: diagnose, fix, re-run.
|
||||
|
||||
Also diff to verify content preservation: `$CLI diff --before source.docx --after out.docx`
|
||||
|
||||
## Validation pipeline
|
||||
|
||||
Run after every write operation. For Scenario C the full pipeline is **mandatory**; for A/B it is **recommended** (skip only if the operation was trivially simple).
|
||||
|
||||
```bash
|
||||
$CLI merge-runs --input doc.docx # 1. consolidate runs
|
||||
$CLI validate --input doc.docx --xsd assets/xsd/wml-subset.xsd # 2. XSD structure
|
||||
$CLI validate --input doc.docx --business # 3. business rules
|
||||
```
|
||||
|
||||
If XSD fails, auto-repair and retry:
|
||||
```bash
|
||||
$CLI fix-order --input doc.docx
|
||||
$CLI validate --input doc.docx --xsd assets/xsd/wml-subset.xsd
|
||||
```
|
||||
|
||||
If XSD still fails, fall back to business rules + preview:
|
||||
```bash
|
||||
$CLI validate --input doc.docx --business
|
||||
scripts/docx_preview.sh doc.docx
|
||||
# Verify: font contamination=0, table count correct, drawing count correct, sectPr count correct
|
||||
```
|
||||
|
||||
Final preview: `scripts/docx_preview.sh doc.docx`
|
||||
|
||||
## Critical rules
|
||||
|
||||
These prevent file corruption — OpenXML is strict about element ordering.
|
||||
|
||||
**Element order** (properties always first):
|
||||
|
||||
| Parent | Order |
|
||||
|--------|-------|
|
||||
| `w:p` | `pPr` → runs |
|
||||
| `w:r` | `rPr` → `t`/`br`/`tab` |
|
||||
| `w:tbl`| `tblPr` → `tblGrid` → `tr` |
|
||||
| `w:tr` | `trPr` → `tc` |
|
||||
| `w:tc` | `tcPr` → `p` (min 1 `<w:p/>`) |
|
||||
| `w:body` | block content → `sectPr` (LAST child) |
|
||||
|
||||
**Direct format contamination:** When copying content from a source document, inline `rPr` (fonts, color) and `pPr` (borders, shading, spacing) override template styles. Always strip direct formatting — keep only `pStyle` reference and `t` text. Clean tables too (including `pPr/rPr` inside cells).
|
||||
|
||||
**Track changes:** `<w:del>` uses `<w:delText>`, never `<w:t>`. `<w:ins>` uses `<w:t>`, never `<w:delText>`.
|
||||
|
||||
**Font size:** `w:sz` = points × 2 (12pt → `sz="24"`). Margins/spacing in DXA (1 inch = 1440, 1cm ≈ 567).
|
||||
|
||||
**Heading styles MUST have OutlineLevel:** When defining heading styles (Heading1, ThesisH1, etc.), always include `new OutlineLevel { Val = N }` in `StyleParagraphProperties` (H1→0, H2→1, H3→2). Without this, Word sees them as plain styled text — TOC and navigation pane won't work.
|
||||
|
||||
**Multi-template merge:** When given multiple template files (font, heading, breaks), read `references/scenario_c_apply_template.md` section "Multi-Template Merge" FIRST. Key rules:
|
||||
- Merge styles from all templates into one styles.xml. Structure (sections/breaks) comes from the breaks template.
|
||||
- Each content paragraph must appear exactly ONCE — never duplicate when inserting section breaks.
|
||||
- NEVER insert empty/blank paragraphs as padding or section separators. Output paragraph count must equal input. Use section break properties (`w:sectPr` inside `w:pPr`) and style spacing (`w:spacing` before/after) for visual separation.
|
||||
- Insert oddPage section breaks before EVERY chapter heading, not just the first. Even if a chapter has dual-column content, it MUST start with oddPage; use a second continuous break after the heading for column switching.
|
||||
- Dual-column chapters need THREE section breaks: (1) oddPage in preceding para's pPr, (2) continuous+cols=2 in the chapter HEADING's pPr, (3) continuous+cols=1 in the last body para's pPr to revert.
|
||||
- Copy `titlePg` settings from the breaks template for EACH section. Abstract and TOC sections typically need `titlePg=true`.
|
||||
|
||||
**Multi-section headers/footers:** Templates with 10+ sections (e.g., Chinese thesis) have DIFFERENT headers/footers per section (Roman vs Arabic page numbers, different header text per zone). Rules:
|
||||
- Use C-2 Base-Replace: copy the TEMPLATE as output base, then replace body content. This preserves all sections, headers, footers, and titlePg settings automatically.
|
||||
- NEVER recreate headers/footers from scratch — copy template header/footer XML byte-for-byte.
|
||||
- NEVER add formatting (borders, alignment, font size) not present in the template header XML.
|
||||
- Non-cover sections MUST have header/footer XML files (at least empty header + page number footer).
|
||||
- See `references/scenario_c_apply_template.md` section "Multi-Section Header/Footer Transfer".
|
||||
|
||||
## References
|
||||
|
||||
Load as needed — don't load all at once. Pick the most relevant files for the task.
|
||||
|
||||
**The C# samples and design references below are the project's knowledge base ("encyclopedia").** When writing OpenXML code, ALWAYS read the relevant sample file first — it contains compilable, SDK-version-verified patterns that prevent common errors. When making aesthetic decisions, read the design principles and recipe files — they encode tested, harmonious parameter sets from authoritative sources (IEEE, ACM, APA, Nature, etc.), not guesses.
|
||||
|
||||
### Scenario guides (read first for each pipeline)
|
||||
|
||||
| File | When |
|
||||
|------|------|
|
||||
| `references/scenario_a_create.md` | Pipeline A: creating from scratch |
|
||||
| `references/scenario_b_edit_content.md` | Pipeline B: editing existing content |
|
||||
| `references/scenario_c_apply_template.md` | Pipeline C: applying template formatting |
|
||||
|
||||
### C# code samples (compilable, heavily commented — read when writing code)
|
||||
|
||||
| File | Topic |
|
||||
|------|-------|
|
||||
| `Samples/DocumentCreationSamples.cs` | Document lifecycle: create, open, save, streams, doc defaults, settings, properties, page setup, multi-section |
|
||||
| `Samples/StyleSystemSamples.cs` | Styles: Normal/Heading chain, character/table/list styles, DocDefaults, latentStyles, CJK 公文, APA 7th, import, resolve inheritance |
|
||||
| `Samples/CharacterFormattingSamples.cs` | RunProperties: fonts, size, bold/italic, all underlines, color, highlight, strike, sub/super, caps, spacing, shading, border, emphasis marks |
|
||||
| `Samples/ParagraphFormattingSamples.cs` | ParagraphProperties: justification, indentation, line/paragraph spacing, keep/widow, outline level, borders, tabs, numbering, bidi, frame |
|
||||
| `Samples/TableSamples.cs` | Tables: borders, grid, cell props, margins, row height, header repeat, merge (H+V), nested, floating, three-line 三线表, zebra striping |
|
||||
| `Samples/HeaderFooterSamples.cs` | Headers/footers: page numbers, "Page X of Y", first/even/odd, logo image, table layout, 公文 "-X-", per-section |
|
||||
| `Samples/ImageSamples.cs` | Images: inline, floating, text wrapping, border, alt text, in header/table, replace, SVG fallback, dimension calc |
|
||||
| `Samples/ListAndNumberingSamples.cs` | Numbering: bullets, multi-level decimal, custom symbols, outline→headings, legal, Chinese 一/(一)/1./(1), restart/continue |
|
||||
| `Samples/FieldAndTocSamples.cs` | Fields: TOC, SimpleField vs complex field, DATE/PAGE/REF/SEQ/MERGEFIELD/IF/STYLEREF, TOC styles |
|
||||
| `Samples/FootnoteAndCommentSamples.cs` | Footnotes, endnotes, comments (4-file system), bookmarks, hyperlinks (internal + external) |
|
||||
| `Samples/TrackChangesSamples.cs` | Revisions: insertions (w:t), deletions (w:delText!), formatting changes, accept/reject all, move tracking |
|
||||
| `Samples/AestheticRecipeSamples.cs` | 13 aesthetic recipes from authoritative sources: ModernCorporate, AcademicThesis, ExecutiveBrief, ChineseGovernment (GB/T 9704), MinimalModern, IEEE Conference, ACM sigconf, APA 7th, MLA 9th, Chicago/Turabian, Springer LNCS, Nature, HBR — each with exact values from official style guides |
|
||||
|
||||
Note: `Samples/` path is relative to `scripts/dotnet/MiniMaxAIDocx.Core/`.
|
||||
|
||||
### Markdown references (read when you need specifications or design rules)
|
||||
|
||||
| File | When |
|
||||
|------|------|
|
||||
| `references/openxml_element_order.md` | XML element ordering rules (prevents corruption) |
|
||||
| `references/openxml_units.md` | Unit conversion: DXA, EMU, half-points, eighth-points |
|
||||
| `references/openxml_encyclopedia_part1.md` | Detailed C# encyclopedia: document creation, styles, character & paragraph formatting |
|
||||
| `references/openxml_encyclopedia_part2.md` | Detailed C# encyclopedia: page setup, tables, headers/footers, sections, doc properties |
|
||||
| `references/openxml_encyclopedia_part3.md` | Detailed C# encyclopedia: TOC, footnotes, fields, track changes, comments, images, math, numbering, protection |
|
||||
| `references/typography_guide.md` | Font pairing, sizes, spacing, page layout, table design, color schemes |
|
||||
| `references/cjk_typography.md` | CJK fonts, 字号 sizes, RunFonts mapping, GB/T 9704 公文 standard |
|
||||
| `references/cjk_university_template_guide.md` | Chinese university thesis templates: numeric styleIds (1/2/3 vs Heading1), document zone structure (cover→abstract→TOC→body→references), font expectations, common mistakes |
|
||||
| `references/design_principles.md` | **Aesthetic foundations**: 6 design principles (white space, contrast/scale, proximity, alignment, repetition, hierarchy) — teaches WHY, not just WHAT |
|
||||
| `references/design_good_bad_examples.md` | **Good vs Bad comparisons**: 10 categories of typography mistakes with OpenXML values, ASCII mockups, and fixes |
|
||||
| `references/track_changes_guide.md` | Revision marks deep dive |
|
||||
| `references/troubleshooting.md` | **Symptom-driven fixes**: 13 common problems indexed by what you SEE (headings wrong, images missing, TOC broken, etc.) — search by symptom, find the fix |
|
||||
250
skills/minimax-docx/assets/styles/academic_styles.xml
Normal file
250
skills/minimax-docx/assets/styles/academic_styles.xml
Normal file
@@ -0,0 +1,250 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
|
||||
|
||||
<!-- Document Defaults -->
|
||||
<w:docDefaults>
|
||||
<w:rPrDefault>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:eastAsia="SimSun" w:cs="Times New Roman" />
|
||||
<w:sz w:val="24" />
|
||||
<w:szCs w:val="24" />
|
||||
<w:lang w:val="en-US" w:eastAsia="zh-CN" w:bidi="ar-SA" />
|
||||
</w:rPr>
|
||||
</w:rPrDefault>
|
||||
<w:pPrDefault>
|
||||
<w:pPr>
|
||||
<w:spacing w:after="0" w:line="480" w:lineRule="auto" />
|
||||
</w:pPr>
|
||||
</w:pPrDefault>
|
||||
</w:docDefaults>
|
||||
|
||||
<w:latentStyles w:defLockedState="0" w:defUIPriority="99" w:defSemiHidden="0" w:defUnhideWhenUsed="0" w:defQFormat="0" w:count="376" />
|
||||
|
||||
<!-- Normal — Times New Roman 12pt, double spaced, first line indent -->
|
||||
<w:style w:type="paragraph" w:default="1" w:styleId="Normal">
|
||||
<w:name w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:pPr>
|
||||
<w:spacing w:after="0" w:line="480" w:lineRule="auto" />
|
||||
<w:ind w:firstLine="720" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" />
|
||||
<w:sz w:val="24" />
|
||||
<w:szCs w:val="24" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Default Paragraph Font -->
|
||||
<w:style w:type="character" w:default="1" w:styleId="DefaultParagraphFont">
|
||||
<w:name w:val="Default Paragraph Font" />
|
||||
<w:uiPriority w:val="1" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
</w:style>
|
||||
|
||||
<!-- Heading 1 — Bold, 14pt, no color, no indent -->
|
||||
<w:style w:type="paragraph" w:styleId="Heading1">
|
||||
<w:name w:val="heading 1" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="9" />
|
||||
<w:pPr>
|
||||
<w:keepNext />
|
||||
<w:keepLines />
|
||||
<w:spacing w:before="480" w:after="240" w:line="480" w:lineRule="auto" />
|
||||
<w:ind w:firstLine="0" />
|
||||
<w:jc w:val="center" />
|
||||
<w:outlineLvl w:val="0" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:b />
|
||||
<w:sz w:val="28" />
|
||||
<w:szCs w:val="28" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Heading 2 — Bold, 13pt -->
|
||||
<w:style w:type="paragraph" w:styleId="Heading2">
|
||||
<w:name w:val="heading 2" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="9" />
|
||||
<w:pPr>
|
||||
<w:keepNext />
|
||||
<w:keepLines />
|
||||
<w:spacing w:before="360" w:after="120" w:line="480" w:lineRule="auto" />
|
||||
<w:ind w:firstLine="0" />
|
||||
<w:outlineLvl w:val="1" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:b />
|
||||
<w:sz w:val="26" />
|
||||
<w:szCs w:val="26" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Heading 3 — Bold, 12pt -->
|
||||
<w:style w:type="paragraph" w:styleId="Heading3">
|
||||
<w:name w:val="heading 3" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="9" />
|
||||
<w:pPr>
|
||||
<w:keepNext />
|
||||
<w:keepLines />
|
||||
<w:spacing w:before="240" w:after="80" w:line="480" w:lineRule="auto" />
|
||||
<w:ind w:firstLine="0" />
|
||||
<w:outlineLvl w:val="2" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:b />
|
||||
<w:sz w:val="24" />
|
||||
<w:szCs w:val="24" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Title — Centered, bold, 14pt (academic title page) -->
|
||||
<w:style w:type="paragraph" w:styleId="Title">
|
||||
<w:name w:val="Title" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="10" />
|
||||
<w:pPr>
|
||||
<w:spacing w:after="480" w:line="480" w:lineRule="auto" />
|
||||
<w:ind w:firstLine="0" />
|
||||
<w:jc w:val="center" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:b />
|
||||
<w:sz w:val="28" />
|
||||
<w:szCs w:val="28" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<w:style w:type="paragraph" w:styleId="Subtitle">
|
||||
<w:name w:val="Subtitle" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="11" />
|
||||
<w:pPr>
|
||||
<w:spacing w:after="240" w:line="480" w:lineRule="auto" />
|
||||
<w:ind w:firstLine="0" />
|
||||
<w:jc w:val="center" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:sz w:val="24" />
|
||||
<w:szCs w:val="24" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Quote — Block quote, indented 0.5 inch on both sides -->
|
||||
<w:style w:type="paragraph" w:styleId="Quote">
|
||||
<w:name w:val="Quote" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="29" />
|
||||
<w:pPr>
|
||||
<w:spacing w:before="240" w:after="240" w:line="480" w:lineRule="auto" />
|
||||
<w:ind w:left="720" w:right="720" w:firstLine="0" />
|
||||
</w:pPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Table Normal -->
|
||||
<w:style w:type="table" w:default="1" w:styleId="TableNormal">
|
||||
<w:name w:val="Normal Table" />
|
||||
<w:uiPriority w:val="99" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
<w:tblPr>
|
||||
<w:tblInd w:w="0" w:type="dxa" />
|
||||
<w:tblCellMar>
|
||||
<w:top w:w="0" w:type="dxa" />
|
||||
<w:left w:w="108" w:type="dxa" />
|
||||
<w:bottom w:w="0" w:type="dxa" />
|
||||
<w:right w:w="108" w:type="dxa" />
|
||||
</w:tblCellMar>
|
||||
</w:tblPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Table Grid — Simple borders, no color -->
|
||||
<w:style w:type="table" w:styleId="TableGrid">
|
||||
<w:name w:val="Table Grid" />
|
||||
<w:basedOn w:val="TableNormal" />
|
||||
<w:uiPriority w:val="39" />
|
||||
<w:tblPr>
|
||||
<w:tblBorders>
|
||||
<w:top w:val="single" w:sz="4" w:space="0" w:color="auto" />
|
||||
<w:left w:val="single" w:sz="4" w:space="0" w:color="auto" />
|
||||
<w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto" />
|
||||
<w:right w:val="single" w:sz="4" w:space="0" w:color="auto" />
|
||||
<w:insideH w:val="single" w:sz="4" w:space="0" w:color="auto" />
|
||||
<w:insideV w:val="single" w:sz="4" w:space="0" w:color="auto" />
|
||||
</w:tblBorders>
|
||||
</w:tblPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Header -->
|
||||
<w:style w:type="paragraph" w:styleId="Header">
|
||||
<w:name w:val="header" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:uiPriority w:val="99" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
<w:pPr>
|
||||
<w:tabs>
|
||||
<w:tab w:val="center" w:pos="4680" />
|
||||
<w:tab w:val="right" w:pos="9360" />
|
||||
</w:tabs>
|
||||
<w:spacing w:after="0" w:line="240" w:lineRule="auto" />
|
||||
<w:ind w:firstLine="0" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:sz w:val="24" />
|
||||
<w:szCs w:val="24" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Footer -->
|
||||
<w:style w:type="paragraph" w:styleId="Footer">
|
||||
<w:name w:val="footer" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:uiPriority w:val="99" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
<w:pPr>
|
||||
<w:tabs>
|
||||
<w:tab w:val="center" w:pos="4680" />
|
||||
<w:tab w:val="right" w:pos="9360" />
|
||||
</w:tabs>
|
||||
<w:spacing w:after="0" w:line="240" w:lineRule="auto" />
|
||||
<w:ind w:firstLine="0" />
|
||||
<w:jc w:val="center" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:sz w:val="24" />
|
||||
<w:szCs w:val="24" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Hyperlink -->
|
||||
<w:style w:type="character" w:styleId="Hyperlink">
|
||||
<w:name w:val="Hyperlink" />
|
||||
<w:uiPriority w:val="99" />
|
||||
<w:unhideWhenUsed />
|
||||
<w:rPr>
|
||||
<w:color w:val="0563C1" />
|
||||
<w:u w:val="single" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
</w:styles>
|
||||
284
skills/minimax-docx/assets/styles/corporate_styles.xml
Normal file
284
skills/minimax-docx/assets/styles/corporate_styles.xml
Normal file
@@ -0,0 +1,284 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
|
||||
|
||||
<!-- Document Defaults -->
|
||||
<w:docDefaults>
|
||||
<w:rPrDefault>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:eastAsia="Microsoft YaHei" w:cs="Arial" />
|
||||
<w:color w:val="333333" />
|
||||
<w:sz w:val="22" />
|
||||
<w:szCs w:val="22" />
|
||||
<w:lang w:val="en-US" w:eastAsia="zh-CN" w:bidi="ar-SA" />
|
||||
</w:rPr>
|
||||
</w:rPrDefault>
|
||||
<w:pPrDefault>
|
||||
<w:pPr>
|
||||
<w:spacing w:after="160" w:line="259" w:lineRule="auto" />
|
||||
</w:pPr>
|
||||
</w:pPrDefault>
|
||||
</w:docDefaults>
|
||||
|
||||
<w:latentStyles w:defLockedState="0" w:defUIPriority="99" w:defSemiHidden="0" w:defUnhideWhenUsed="0" w:defQFormat="0" w:count="376" />
|
||||
|
||||
<!-- Normal -->
|
||||
<w:style w:type="paragraph" w:default="1" w:styleId="Normal">
|
||||
<w:name w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:pPr>
|
||||
<w:spacing w:after="160" w:line="240" w:lineRule="auto" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" />
|
||||
<w:color w:val="333333" />
|
||||
<w:sz w:val="22" />
|
||||
<w:szCs w:val="22" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Default Paragraph Font -->
|
||||
<w:style w:type="character" w:default="1" w:styleId="DefaultParagraphFont">
|
||||
<w:name w:val="Default Paragraph Font" />
|
||||
<w:uiPriority w:val="1" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
</w:style>
|
||||
|
||||
<!-- Heading 1 — Dark Blue -->
|
||||
<w:style w:type="paragraph" w:styleId="Heading1">
|
||||
<w:name w:val="heading 1" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="9" />
|
||||
<w:pPr>
|
||||
<w:keepNext />
|
||||
<w:keepLines />
|
||||
<w:spacing w:before="480" w:after="240" w:line="240" w:lineRule="auto" />
|
||||
<w:outlineLvl w:val="0" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri Light" w:hAnsi="Calibri Light" />
|
||||
<w:b />
|
||||
<w:color w:val="1F3864" />
|
||||
<w:sz w:val="56" />
|
||||
<w:szCs w:val="56" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Heading 2 -->
|
||||
<w:style w:type="paragraph" w:styleId="Heading2">
|
||||
<w:name w:val="heading 2" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="9" />
|
||||
<w:pPr>
|
||||
<w:keepNext />
|
||||
<w:keepLines />
|
||||
<w:spacing w:before="360" w:after="120" w:line="240" w:lineRule="auto" />
|
||||
<w:outlineLvl w:val="1" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri Light" w:hAnsi="Calibri Light" />
|
||||
<w:b />
|
||||
<w:color w:val="1F3864" />
|
||||
<w:sz w:val="48" />
|
||||
<w:szCs w:val="48" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Heading 3 -->
|
||||
<w:style w:type="paragraph" w:styleId="Heading3">
|
||||
<w:name w:val="heading 3" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="9" />
|
||||
<w:pPr>
|
||||
<w:keepNext />
|
||||
<w:keepLines />
|
||||
<w:spacing w:before="240" w:after="80" w:line="240" w:lineRule="auto" />
|
||||
<w:outlineLvl w:val="2" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri Light" w:hAnsi="Calibri Light" />
|
||||
<w:b />
|
||||
<w:color w:val="1F3864" />
|
||||
<w:sz w:val="36" />
|
||||
<w:szCs w:val="36" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Heading 4 -->
|
||||
<w:style w:type="paragraph" w:styleId="Heading4">
|
||||
<w:name w:val="heading 4" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="9" />
|
||||
<w:pPr>
|
||||
<w:keepNext />
|
||||
<w:keepLines />
|
||||
<w:spacing w:before="160" w:after="80" w:line="240" w:lineRule="auto" />
|
||||
<w:outlineLvl w:val="3" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri Light" w:hAnsi="Calibri Light" />
|
||||
<w:b />
|
||||
<w:i />
|
||||
<w:color w:val="1F3864" />
|
||||
<w:sz w:val="28" />
|
||||
<w:szCs w:val="28" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Title -->
|
||||
<w:style w:type="paragraph" w:styleId="Title">
|
||||
<w:name w:val="Title" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="10" />
|
||||
<w:pPr>
|
||||
<w:spacing w:after="240" w:line="240" w:lineRule="auto" />
|
||||
<w:jc w:val="center" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri Light" w:hAnsi="Calibri Light" />
|
||||
<w:color w:val="1F3864" />
|
||||
<w:sz w:val="72" />
|
||||
<w:szCs w:val="72" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<w:style w:type="paragraph" w:styleId="Subtitle">
|
||||
<w:name w:val="Subtitle" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="11" />
|
||||
<w:pPr>
|
||||
<w:spacing w:after="360" w:line="240" w:lineRule="auto" />
|
||||
<w:jc w:val="center" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:i />
|
||||
<w:color w:val="595959" />
|
||||
<w:sz w:val="32" />
|
||||
<w:szCs w:val="32" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Table Grid — Corporate with blue header -->
|
||||
<w:style w:type="table" w:default="1" w:styleId="TableNormal">
|
||||
<w:name w:val="Normal Table" />
|
||||
<w:uiPriority w:val="99" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
<w:tblPr>
|
||||
<w:tblInd w:w="0" w:type="dxa" />
|
||||
<w:tblCellMar>
|
||||
<w:top w:w="0" w:type="dxa" />
|
||||
<w:left w:w="108" w:type="dxa" />
|
||||
<w:bottom w:w="0" w:type="dxa" />
|
||||
<w:right w:w="108" w:type="dxa" />
|
||||
</w:tblCellMar>
|
||||
</w:tblPr>
|
||||
</w:style>
|
||||
|
||||
<w:style w:type="table" w:styleId="TableGrid">
|
||||
<w:name w:val="Table Grid" />
|
||||
<w:basedOn w:val="TableNormal" />
|
||||
<w:uiPriority w:val="39" />
|
||||
<w:tblPr>
|
||||
<w:tblBorders>
|
||||
<w:top w:val="single" w:sz="4" w:space="0" w:color="BFBFBF" />
|
||||
<w:left w:val="single" w:sz="4" w:space="0" w:color="BFBFBF" />
|
||||
<w:bottom w:val="single" w:sz="4" w:space="0" w:color="BFBFBF" />
|
||||
<w:right w:val="single" w:sz="4" w:space="0" w:color="BFBFBF" />
|
||||
<w:insideH w:val="single" w:sz="4" w:space="0" w:color="BFBFBF" />
|
||||
<w:insideV w:val="single" w:sz="4" w:space="0" w:color="BFBFBF" />
|
||||
</w:tblBorders>
|
||||
</w:tblPr>
|
||||
<w:tblStylePr w:type="firstRow">
|
||||
<w:rPr>
|
||||
<w:b />
|
||||
<w:color w:val="FFFFFF" />
|
||||
</w:rPr>
|
||||
<w:tcPr>
|
||||
<w:shd w:val="clear" w:color="auto" w:fill="2F5496" />
|
||||
<w:tcBorders>
|
||||
<w:top w:val="single" w:sz="4" w:space="0" w:color="2F5496" />
|
||||
<w:left w:val="single" w:sz="4" w:space="0" w:color="2F5496" />
|
||||
<w:bottom w:val="single" w:sz="4" w:space="0" w:color="2F5496" />
|
||||
<w:right w:val="single" w:sz="4" w:space="0" w:color="2F5496" />
|
||||
<w:insideH w:val="single" w:sz="4" w:space="0" w:color="3A6BC5" />
|
||||
<w:insideV w:val="single" w:sz="4" w:space="0" w:color="3A6BC5" />
|
||||
</w:tcBorders>
|
||||
</w:tcPr>
|
||||
</w:tblStylePr>
|
||||
<w:tblStylePr w:type="band1Horz">
|
||||
<w:tcPr>
|
||||
<w:shd w:val="clear" w:color="auto" w:fill="D9E2F3" />
|
||||
</w:tcPr>
|
||||
</w:tblStylePr>
|
||||
</w:style>
|
||||
|
||||
<!-- Header -->
|
||||
<w:style w:type="paragraph" w:styleId="Header">
|
||||
<w:name w:val="header" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:uiPriority w:val="99" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
<w:pPr>
|
||||
<w:tabs>
|
||||
<w:tab w:val="center" w:pos="4680" />
|
||||
<w:tab w:val="right" w:pos="9360" />
|
||||
</w:tabs>
|
||||
<w:spacing w:after="0" w:line="240" w:lineRule="auto" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:sz w:val="18" />
|
||||
<w:szCs w:val="18" />
|
||||
<w:color w:val="808080" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Footer -->
|
||||
<w:style w:type="paragraph" w:styleId="Footer">
|
||||
<w:name w:val="footer" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:uiPriority w:val="99" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
<w:pPr>
|
||||
<w:tabs>
|
||||
<w:tab w:val="center" w:pos="4680" />
|
||||
<w:tab w:val="right" w:pos="9360" />
|
||||
</w:tabs>
|
||||
<w:spacing w:after="0" w:line="240" w:lineRule="auto" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:sz w:val="18" />
|
||||
<w:szCs w:val="18" />
|
||||
<w:color w:val="808080" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Hyperlink -->
|
||||
<w:style w:type="character" w:styleId="Hyperlink">
|
||||
<w:name w:val="Hyperlink" />
|
||||
<w:uiPriority w:val="99" />
|
||||
<w:unhideWhenUsed />
|
||||
<w:rPr>
|
||||
<w:color w:val="0563C1" />
|
||||
<w:u w:val="single" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
</w:styles>
|
||||
449
skills/minimax-docx/assets/styles/default_styles.xml
Normal file
449
skills/minimax-docx/assets/styles/default_styles.xml
Normal file
@@ -0,0 +1,449 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
|
||||
|
||||
<!-- Document Defaults -->
|
||||
<w:docDefaults>
|
||||
<w:rPrDefault>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:eastAsia="SimSun" w:cs="Arial" />
|
||||
<w:sz w:val="22" />
|
||||
<w:szCs w:val="22" />
|
||||
<w:lang w:val="en-US" w:eastAsia="zh-CN" w:bidi="ar-SA" />
|
||||
</w:rPr>
|
||||
</w:rPrDefault>
|
||||
<w:pPrDefault>
|
||||
<w:pPr>
|
||||
<w:spacing w:after="160" w:line="259" w:lineRule="auto" />
|
||||
</w:pPr>
|
||||
</w:pPrDefault>
|
||||
</w:docDefaults>
|
||||
|
||||
<!-- Latent Styles -->
|
||||
<w:latentStyles w:defLockedState="0" w:defUIPriority="99" w:defSemiHidden="0" w:defUnhideWhenUsed="0" w:defQFormat="0" w:count="376" />
|
||||
|
||||
<!-- Normal (Default Paragraph Style) -->
|
||||
<w:style w:type="paragraph" w:default="1" w:styleId="Normal">
|
||||
<w:name w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:pPr>
|
||||
<w:spacing w:after="160" w:line="240" w:lineRule="auto" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" />
|
||||
<w:sz w:val="22" />
|
||||
<w:szCs w:val="22" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Default Paragraph Font -->
|
||||
<w:style w:type="character" w:default="1" w:styleId="DefaultParagraphFont">
|
||||
<w:name w:val="Default Paragraph Font" />
|
||||
<w:uiPriority w:val="1" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
</w:style>
|
||||
|
||||
<!-- Heading 1 -->
|
||||
<w:style w:type="paragraph" w:styleId="Heading1">
|
||||
<w:name w:val="heading 1" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="9" />
|
||||
<w:pPr>
|
||||
<w:keepNext />
|
||||
<w:keepLines />
|
||||
<w:spacing w:before="480" w:after="240" w:line="240" w:lineRule="auto" />
|
||||
<w:outlineLvl w:val="0" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri Light" w:hAnsi="Calibri Light" />
|
||||
<w:b />
|
||||
<w:color w:val="2F5496" />
|
||||
<w:sz w:val="56" />
|
||||
<w:szCs w:val="56" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Heading 2 -->
|
||||
<w:style w:type="paragraph" w:styleId="Heading2">
|
||||
<w:name w:val="heading 2" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="9" />
|
||||
<w:pPr>
|
||||
<w:keepNext />
|
||||
<w:keepLines />
|
||||
<w:spacing w:before="360" w:after="120" w:line="240" w:lineRule="auto" />
|
||||
<w:outlineLvl w:val="1" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri Light" w:hAnsi="Calibri Light" />
|
||||
<w:b />
|
||||
<w:color w:val="2F5496" />
|
||||
<w:sz w:val="48" />
|
||||
<w:szCs w:val="48" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Heading 3 -->
|
||||
<w:style w:type="paragraph" w:styleId="Heading3">
|
||||
<w:name w:val="heading 3" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="9" />
|
||||
<w:pPr>
|
||||
<w:keepNext />
|
||||
<w:keepLines />
|
||||
<w:spacing w:before="240" w:after="80" w:line="240" w:lineRule="auto" />
|
||||
<w:outlineLvl w:val="2" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri Light" w:hAnsi="Calibri Light" />
|
||||
<w:b />
|
||||
<w:color w:val="2F5496" />
|
||||
<w:sz w:val="36" />
|
||||
<w:szCs w:val="36" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Heading 4 -->
|
||||
<w:style w:type="paragraph" w:styleId="Heading4">
|
||||
<w:name w:val="heading 4" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="9" />
|
||||
<w:pPr>
|
||||
<w:keepNext />
|
||||
<w:keepLines />
|
||||
<w:spacing w:before="160" w:after="80" w:line="240" w:lineRule="auto" />
|
||||
<w:outlineLvl w:val="3" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri Light" w:hAnsi="Calibri Light" />
|
||||
<w:b />
|
||||
<w:i />
|
||||
<w:color w:val="2F5496" />
|
||||
<w:sz w:val="28" />
|
||||
<w:szCs w:val="28" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Heading 5 -->
|
||||
<w:style w:type="paragraph" w:styleId="Heading5">
|
||||
<w:name w:val="heading 5" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="9" />
|
||||
<w:pPr>
|
||||
<w:keepNext />
|
||||
<w:keepLines />
|
||||
<w:spacing w:before="160" w:after="80" w:line="240" w:lineRule="auto" />
|
||||
<w:outlineLvl w:val="4" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri Light" w:hAnsi="Calibri Light" />
|
||||
<w:b />
|
||||
<w:color w:val="2F5496" />
|
||||
<w:sz w:val="24" />
|
||||
<w:szCs w:val="24" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Heading 6 -->
|
||||
<w:style w:type="paragraph" w:styleId="Heading6">
|
||||
<w:name w:val="heading 6" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="9" />
|
||||
<w:pPr>
|
||||
<w:keepNext />
|
||||
<w:keepLines />
|
||||
<w:spacing w:before="160" w:after="80" w:line="240" w:lineRule="auto" />
|
||||
<w:outlineLvl w:val="5" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri Light" w:hAnsi="Calibri Light" />
|
||||
<w:b />
|
||||
<w:i />
|
||||
<w:color w:val="2F5496" />
|
||||
<w:sz w:val="22" />
|
||||
<w:szCs w:val="22" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Title -->
|
||||
<w:style w:type="paragraph" w:styleId="Title">
|
||||
<w:name w:val="Title" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="10" />
|
||||
<w:pPr>
|
||||
<w:spacing w:after="240" w:line="240" w:lineRule="auto" />
|
||||
<w:jc w:val="center" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri Light" w:hAnsi="Calibri Light" />
|
||||
<w:color w:val="2F5496" />
|
||||
<w:sz w:val="72" />
|
||||
<w:szCs w:val="72" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<w:style w:type="paragraph" w:styleId="Subtitle">
|
||||
<w:name w:val="Subtitle" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="11" />
|
||||
<w:pPr>
|
||||
<w:spacing w:after="360" w:line="240" w:lineRule="auto" />
|
||||
<w:jc w:val="center" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:i />
|
||||
<w:color w:val="595959" />
|
||||
<w:sz w:val="32" />
|
||||
<w:szCs w:val="32" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Quote -->
|
||||
<w:style w:type="paragraph" w:styleId="Quote">
|
||||
<w:name w:val="Quote" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="29" />
|
||||
<w:pPr>
|
||||
<w:spacing w:before="240" w:after="240" />
|
||||
<w:ind w:left="720" w:right="720" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:i />
|
||||
<w:color w:val="404040" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Intense Quote -->
|
||||
<w:style w:type="paragraph" w:styleId="IntenseQuote">
|
||||
<w:name w:val="Intense Quote" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:qFormat />
|
||||
<w:uiPriority w:val="30" />
|
||||
<w:pPr>
|
||||
<w:spacing w:before="240" w:after="240" />
|
||||
<w:ind w:left="720" w:right="720" />
|
||||
<w:pBdr>
|
||||
<w:left w:val="single" w:sz="18" w:space="12" w:color="2F5496" />
|
||||
</w:pBdr>
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:b />
|
||||
<w:i />
|
||||
<w:color w:val="2F5496" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- TOC Heading -->
|
||||
<w:style w:type="paragraph" w:styleId="TOCHeading">
|
||||
<w:name w:val="TOC Heading" />
|
||||
<w:basedOn w:val="Heading1" />
|
||||
<w:next w:val="Normal" />
|
||||
<w:uiPriority w:val="39" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
<w:pPr>
|
||||
<w:outlineLvl w:val="9" />
|
||||
</w:pPr>
|
||||
</w:style>
|
||||
|
||||
<!-- TOC 1 -->
|
||||
<w:style w:type="paragraph" w:styleId="TOC1">
|
||||
<w:name w:val="toc 1" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:uiPriority w:val="39" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
<w:pPr>
|
||||
<w:spacing w:before="120" w:after="0" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:b />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- TOC 2 -->
|
||||
<w:style w:type="paragraph" w:styleId="TOC2">
|
||||
<w:name w:val="toc 2" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:uiPriority w:val="39" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
<w:pPr>
|
||||
<w:spacing w:after="0" />
|
||||
<w:ind w:left="240" />
|
||||
</w:pPr>
|
||||
</w:style>
|
||||
|
||||
<!-- TOC 3 -->
|
||||
<w:style w:type="paragraph" w:styleId="TOC3">
|
||||
<w:name w:val="toc 3" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:uiPriority w:val="39" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
<w:pPr>
|
||||
<w:spacing w:after="0" />
|
||||
<w:ind w:left="480" />
|
||||
</w:pPr>
|
||||
</w:style>
|
||||
|
||||
<!-- List Bullet -->
|
||||
<w:style w:type="paragraph" w:styleId="ListBullet">
|
||||
<w:name w:val="List Bullet" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:uiPriority w:val="36" />
|
||||
<w:pPr>
|
||||
<w:spacing w:after="0" />
|
||||
<w:ind w:left="720" w:hanging="360" />
|
||||
<w:contextualSpacing />
|
||||
</w:pPr>
|
||||
</w:style>
|
||||
|
||||
<!-- List Number -->
|
||||
<w:style w:type="paragraph" w:styleId="ListNumber">
|
||||
<w:name w:val="List Number" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:uiPriority w:val="36" />
|
||||
<w:pPr>
|
||||
<w:spacing w:after="0" />
|
||||
<w:ind w:left="720" w:hanging="360" />
|
||||
<w:contextualSpacing />
|
||||
</w:pPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Table Normal -->
|
||||
<w:style w:type="table" w:default="1" w:styleId="TableNormal">
|
||||
<w:name w:val="Normal Table" />
|
||||
<w:uiPriority w:val="99" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
<w:tblPr>
|
||||
<w:tblInd w:w="0" w:type="dxa" />
|
||||
<w:tblCellMar>
|
||||
<w:top w:w="0" w:type="dxa" />
|
||||
<w:left w:w="108" w:type="dxa" />
|
||||
<w:bottom w:w="0" w:type="dxa" />
|
||||
<w:right w:w="108" w:type="dxa" />
|
||||
</w:tblCellMar>
|
||||
</w:tblPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Table Grid -->
|
||||
<w:style w:type="table" w:styleId="TableGrid">
|
||||
<w:name w:val="Table Grid" />
|
||||
<w:basedOn w:val="TableNormal" />
|
||||
<w:uiPriority w:val="39" />
|
||||
<w:tblPr>
|
||||
<w:tblBorders>
|
||||
<w:top w:val="single" w:sz="4" w:space="0" w:color="auto" />
|
||||
<w:left w:val="single" w:sz="4" w:space="0" w:color="auto" />
|
||||
<w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto" />
|
||||
<w:right w:val="single" w:sz="4" w:space="0" w:color="auto" />
|
||||
<w:insideH w:val="single" w:sz="4" w:space="0" w:color="auto" />
|
||||
<w:insideV w:val="single" w:sz="4" w:space="0" w:color="auto" />
|
||||
</w:tblBorders>
|
||||
</w:tblPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Header -->
|
||||
<w:style w:type="paragraph" w:styleId="Header">
|
||||
<w:name w:val="header" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:uiPriority w:val="99" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
<w:pPr>
|
||||
<w:tabs>
|
||||
<w:tab w:val="center" w:pos="4680" />
|
||||
<w:tab w:val="right" w:pos="9360" />
|
||||
</w:tabs>
|
||||
<w:spacing w:after="0" w:line="240" w:lineRule="auto" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:sz w:val="18" />
|
||||
<w:szCs w:val="18" />
|
||||
<w:color w:val="808080" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Footer -->
|
||||
<w:style w:type="paragraph" w:styleId="Footer">
|
||||
<w:name w:val="footer" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:uiPriority w:val="99" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
<w:pPr>
|
||||
<w:tabs>
|
||||
<w:tab w:val="center" w:pos="4680" />
|
||||
<w:tab w:val="right" w:pos="9360" />
|
||||
</w:tabs>
|
||||
<w:spacing w:after="0" w:line="240" w:lineRule="auto" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:sz w:val="18" />
|
||||
<w:szCs w:val="18" />
|
||||
<w:color w:val="808080" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Hyperlink -->
|
||||
<w:style w:type="character" w:styleId="Hyperlink">
|
||||
<w:name w:val="Hyperlink" />
|
||||
<w:uiPriority w:val="99" />
|
||||
<w:unhideWhenUsed />
|
||||
<w:rPr>
|
||||
<w:color w:val="0563C1" />
|
||||
<w:u w:val="single" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Comment Text -->
|
||||
<w:style w:type="paragraph" w:styleId="CommentText">
|
||||
<w:name w:val="annotation text" />
|
||||
<w:basedOn w:val="Normal" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
<w:pPr>
|
||||
<w:spacing w:line="240" w:lineRule="auto" />
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:sz w:val="20" />
|
||||
<w:szCs w:val="20" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Comment Reference -->
|
||||
<w:style w:type="character" w:styleId="CommentReference">
|
||||
<w:name w:val="annotation reference" />
|
||||
<w:semiHidden />
|
||||
<w:unhideWhenUsed />
|
||||
<w:rPr>
|
||||
<w:sz w:val="16" />
|
||||
<w:szCs w:val="16" />
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
</w:styles>
|
||||
470
skills/minimax-docx/assets/xsd/aesthetic-rules.xsd
Normal file
470
skills/minimax-docx/assets/xsd/aesthetic-rules.xsd
Normal file
@@ -0,0 +1,470 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- ============================================================================ -->
|
||||
<!-- Aesthetic Rules Schema for minimax-docx -->
|
||||
<!-- ============================================================================ -->
|
||||
<!-- Purpose: Validates whether a document follows basic aesthetic rules that -->
|
||||
<!-- produce visually harmonious results. This is a "taste checker" that flags -->
|
||||
<!-- common ugly patterns. -->
|
||||
<!-- -->
|
||||
<!-- IMPORTANT: XSD validates STRUCTURE and VALUE RANGES, not SEMANTICS. -->
|
||||
<!-- Many aesthetic rules require cross-element comparison (e.g., "H1 must be -->
|
||||
<!-- larger than H2") which XSD cannot express. These rules are documented in -->
|
||||
<!-- comments and must be enforced by a programmatic validator. -->
|
||||
<!-- -->
|
||||
<!-- Rules that CAN be expressed in XSD: -->
|
||||
<!-- - Font size ranges (body 10-14pt, headings 10-26pt) -->
|
||||
<!-- - Line spacing ranges (1.0x to 2.33x) -->
|
||||
<!-- - Margin minimums (at least 0.5in on all sides) -->
|
||||
<!-- - Table cell padding minimums -->
|
||||
<!-- -->
|
||||
<!-- Rules that CANNOT be expressed in XSD (enforce programmatically): -->
|
||||
<!-- - H1 sz > H2 sz > H3 sz > body sz (hierarchy) -->
|
||||
<!-- - Maximum 3 font families across all styles -->
|
||||
<!-- - Heading space-before >= space-after -->
|
||||
<!-- - Color contrast ratio between text and background -->
|
||||
<!-- - Consistent font family within heading vs body groups -->
|
||||
<!-- - Line spacing and font size harmony (larger text needs tighter spacing) -->
|
||||
<!-- -->
|
||||
<!-- MIT License - minimax-docx project -->
|
||||
<!-- ============================================================================ -->
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
targetNamespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
elementFormDefault="qualified">
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- RULE 1: Body Font Size Range -->
|
||||
<!-- ============================================================ -->
|
||||
<!-- Body text must be 10-14pt (half-points: 20-28). -->
|
||||
<!-- WHY: Below 10pt is hard to read for most adults. -->
|
||||
<!-- Above 14pt body text looks childish or wasteful. -->
|
||||
<!-- The sweet spot is 10.5-12pt for most font families. -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:simpleType name="ST_AestheticBodyFontSize">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Body text font size in half-points.
|
||||
Acceptable range: 20-28 (10pt-14pt).
|
||||
- 10pt (20): minimum for comfortable reading
|
||||
- 11pt (22): modern default (Calibri, Aptos)
|
||||
- 12pt (24): traditional default (Times New Roman)
|
||||
- 14pt (28): maximum before body text looks oversized
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:positiveInteger">
|
||||
<xs:minInclusive value="20"/> <!-- 10pt minimum -->
|
||||
<xs:maxInclusive value="28"/> <!-- 14pt maximum -->
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- RULE 2: Heading Font Size Range -->
|
||||
<!-- ============================================================ -->
|
||||
<!-- Headings must be 12-26pt (half-points: 24-52). -->
|
||||
<!-- WHY: Below 12pt, a heading cannot be visually distinguished -->
|
||||
<!-- from body text by size alone. -->
|
||||
<!-- Above 26pt is poster-sized and wastes vertical space. -->
|
||||
<!-- NOTE: Some academic styles use 12pt headings (same as body) -->
|
||||
<!-- and differentiate via bold/italic/centering instead. -->
|
||||
<!-- The lower bound of 24 (12pt) accommodates this. -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:simpleType name="ST_AestheticHeadingFontSize">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Heading font size in half-points.
|
||||
Acceptable range: 24-52 (12pt-26pt).
|
||||
- 12pt (24): APA-style (hierarchy via bold/italic, not size)
|
||||
- 16pt (32): typical H2/H3
|
||||
- 20pt (40): typical H1
|
||||
- 26pt (52): maximum before headings dominate the page
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:positiveInteger">
|
||||
<xs:minInclusive value="24"/> <!-- 12pt minimum -->
|
||||
<xs:maxInclusive value="52"/> <!-- 26pt maximum -->
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- RULE 3: Line Spacing Range -->
|
||||
<!-- ============================================================ -->
|
||||
<!-- Line spacing (in auto mode) must be 240-560 (1.0x-2.33x). -->
|
||||
<!-- WHY: Below 1.0x, ascenders/descenders overlap — unreadable. -->
|
||||
<!-- Above 2.33x, lines appear disconnected. -->
|
||||
<!-- Sweet spots: 1.15x (276) for sans, 1.5x (360) for -->
|
||||
<!-- generous layouts, 2.0x (480) for academic. -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:simpleType name="ST_AestheticLineSpacing">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Line spacing value for auto line-spacing rule.
|
||||
In 240ths of single spacing: 240 = 1.0x, 480 = 2.0x.
|
||||
Acceptable range: 240-560 (1.0x to 2.33x).
|
||||
Common values:
|
||||
- 240: single spacing (dense, technical)
|
||||
- 259: Word's 1.08x default
|
||||
- 276: 1.15x (modern corporate default)
|
||||
- 336: 1.4x (executive/generous)
|
||||
- 360: 1.5x (generous/minimal)
|
||||
- 480: 2.0x (academic double spacing)
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:positiveInteger">
|
||||
<xs:minInclusive value="240"/> <!-- 1.0x single spacing -->
|
||||
<xs:maxInclusive value="560"/> <!-- ~2.33x — beyond double feels disconnected -->
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- RULE 3b: Fixed Line Spacing Range -->
|
||||
<!-- ============================================================ -->
|
||||
<!-- For lineRule="exact", line value is in DXA (twentieths of pt) -->
|
||||
<!-- Range: 200-720 DXA (10pt-36pt fixed line height) -->
|
||||
<!-- Chinese government standard uses 560 DXA (28pt). -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:simpleType name="ST_AestheticFixedLineSpacing">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Fixed line spacing value (lineRule="exact") in DXA.
|
||||
Acceptable range: 200-720 (10pt-36pt).
|
||||
- 560: Chinese government standard (28pt, for 16pt body)
|
||||
- 480: double-space equivalent for 12pt body
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:positiveInteger">
|
||||
<xs:minInclusive value="200"/> <!-- 10pt minimum fixed height -->
|
||||
<xs:maxInclusive value="720"/> <!-- 36pt maximum fixed height -->
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- RULE 4: Margin Minimums -->
|
||||
<!-- ============================================================ -->
|
||||
<!-- All margins must be at least 720 DXA (0.5 inch). -->
|
||||
<!-- WHY: Below 0.5in, most printers clip content. -->
|
||||
<!-- Also, narrow margins create a cramped, unprofessional -->
|
||||
<!-- appearance. Even "full bleed" designs need internal -->
|
||||
<!-- text margins. -->
|
||||
<!-- Max set to 4320 DXA (3 inches) to prevent absurd margins. -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:simpleType name="ST_AestheticMargin">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Page margin in DXA. Minimum 720 (0.5 inch), maximum 4320 (3 inches).
|
||||
Common values:
|
||||
- 720: 0.5in (minimum printable)
|
||||
- 1440: 1.0in (standard US)
|
||||
- 1588: 28mm (Chinese government left margin)
|
||||
- 1800: 1.25in (executive/premium)
|
||||
- 2160: 1.5in (binding margin or narrow-column design)
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:positiveInteger">
|
||||
<xs:minInclusive value="720"/> <!-- 0.5in — minimum for print safety -->
|
||||
<xs:maxInclusive value="4320"/> <!-- 3in — beyond this is absurd -->
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Top/bottom margins: signed because negative values can create -->
|
||||
<!-- overlap effects, but we still enforce a reasonable minimum. -->
|
||||
<xs:simpleType name="ST_AestheticVerticalMargin">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Vertical (top/bottom) page margin in DXA.
|
||||
Range: 360 to 4320 (0.25in to 3in).
|
||||
Slightly more permissive than horizontal margins because
|
||||
header/footer areas may reduce effective vertical margin.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:integer">
|
||||
<xs:minInclusive value="360"/> <!-- 0.25in — tighter vertical is sometimes acceptable -->
|
||||
<xs:maxInclusive value="4320"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- RULE 5: Paragraph Spacing Ranges -->
|
||||
<!-- ============================================================ -->
|
||||
<!-- Space before/after paragraphs should be 0-960 DXA (0-48pt). -->
|
||||
<!-- WHY: More than 48pt of space before/after creates awkward -->
|
||||
<!-- gaps that disrupt reading flow. -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:simpleType name="ST_AestheticParaSpacing">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Paragraph spacing (before/after) in DXA.
|
||||
Range: 0-960 (0pt-48pt).
|
||||
Common values:
|
||||
- 0: academic style (uses first-line indent instead)
|
||||
- 80: 4pt (tight, used after H2/H3)
|
||||
- 120: 6pt (moderate)
|
||||
- 160: 8pt (standard modern spacing)
|
||||
- 200: 10pt (generous/executive)
|
||||
- 240: 12pt (very generous/minimal)
|
||||
- 480: 24pt (heading before — creates section break)
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:nonNegativeInteger">
|
||||
<xs:minInclusive value="0"/>
|
||||
<xs:maxInclusive value="960"/> <!-- 48pt max — beyond this is a page break, not spacing -->
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- RULE 6: Table Cell Padding Minimum -->
|
||||
<!-- ============================================================ -->
|
||||
<!-- Table cells need at least 28 DXA (~1.4pt) padding. -->
|
||||
<!-- WHY: Without padding, text touches cell borders — visually -->
|
||||
<!-- cramped and hard to read. Even borderless tables need -->
|
||||
<!-- padding for column separation. -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:simpleType name="ST_AestheticCellPadding">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Table cell padding in DXA. Minimum 28 DXA (~1.4pt).
|
||||
Recommended: 57 DXA (~2.85pt) for comfortable spacing.
|
||||
Maximum: 288 DXA (~14pt) — beyond this wastes space.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:nonNegativeInteger">
|
||||
<xs:minInclusive value="28"/> <!-- ~1.4pt minimum breathing room -->
|
||||
<xs:maxInclusive value="288"/> <!-- ~14pt — more than this is excessive -->
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- RULE 7: Border Size Range -->
|
||||
<!-- ============================================================ -->
|
||||
<!-- Border size (in eighth-points) should be 2-24 (0.25pt-3pt). -->
|
||||
<!-- WHY: Below 0.25pt borders may not render or print. -->
|
||||
<!-- Above 3pt borders look heavy and distracting. -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:simpleType name="ST_AestheticBorderSize">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Border width in eighth-points.
|
||||
Range: 2-24 (0.25pt to 3pt).
|
||||
Common values:
|
||||
- 4: 0.5pt (thin, standard)
|
||||
- 6: 0.75pt (header separator in three-line tables)
|
||||
- 8: 1.0pt (medium, good for framing borders)
|
||||
- 12: 1.5pt (heavy, used for top/bottom in three-line tables)
|
||||
- 24: 3.0pt (maximum before borders dominate)
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:positiveInteger">
|
||||
<xs:minInclusive value="2"/> <!-- 0.25pt minimum visible -->
|
||||
<xs:maxInclusive value="24"/> <!-- 3pt maximum tasteful -->
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- RULE 8: Color Value Format -->
|
||||
<!-- ============================================================ -->
|
||||
<!-- Colors must be valid 6-digit hex (RRGGBB) or "auto". -->
|
||||
<!-- This is structural validation, not aesthetic validation. -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:simpleType name="ST_AestheticColor">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Color value: 6-digit hex (RRGGBB) or "auto".
|
||||
Examples: "000000", "1F3864", "2C3E50", "auto".
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:pattern value="[0-9A-Fa-f]{6}|auto"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- RULE 9: First-Line Indent Range -->
|
||||
<!-- ============================================================ -->
|
||||
<!-- If first-line indent is used, it should be 360-1440 DXA -->
|
||||
<!-- (0.25in - 1.0in). -->
|
||||
<!-- WHY: Below 0.25in the indent is barely visible. -->
|
||||
<!-- Above 1.0in the indent looks like a tab error. -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:simpleType name="ST_AestheticFirstLineIndent">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
First-line indent in DXA. Range: 0-1440 (0in to 1.0in).
|
||||
- 0: no indent (modern style with space-after)
|
||||
- 480: 0.33in (compact)
|
||||
- 640: ~0.44in (2 Chinese characters at 16pt)
|
||||
- 720: 0.5in (standard APA/academic)
|
||||
- 1440: 1.0in (maximum before it looks wrong)
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:nonNegativeInteger">
|
||||
<xs:minInclusive value="0"/>
|
||||
<xs:maxInclusive value="1440"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- COMPOSITE TYPE: Aesthetic Run Properties Check -->
|
||||
<!-- ============================================================ -->
|
||||
<!-- Validates run-level properties for aesthetic compliance. -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_AestheticRPr">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Aesthetic run properties validator.
|
||||
Checks font size and color format at the run level.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:all>
|
||||
<xs:element name="sz" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="w:ST_AestheticBodyFontSize" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="szCs" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="w:ST_AestheticBodyFontSize" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="color" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="w:ST_AestheticColor" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- COMPOSITE TYPE: Aesthetic Spacing Check -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_AestheticSpacing">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Aesthetic spacing validator for paragraph spacing properties.
|
||||
Validates line spacing and before/after spacing are in range.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:attribute name="line" type="w:ST_AestheticLineSpacing" use="optional"/>
|
||||
<xs:attribute name="before" type="w:ST_AestheticParaSpacing" use="optional"/>
|
||||
<xs:attribute name="after" type="w:ST_AestheticParaSpacing" use="optional"/>
|
||||
<xs:attribute name="lineRule" use="optional">
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="auto"/>
|
||||
<xs:enumeration value="exact"/>
|
||||
<xs:enumeration value="atLeast"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- COMPOSITE TYPE: Aesthetic Page Margins Check -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_AestheticPageMargins">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Aesthetic page margin validator.
|
||||
Ensures all margins meet minimum print-safe thresholds.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:attribute name="top" type="w:ST_AestheticVerticalMargin" use="required"/>
|
||||
<xs:attribute name="bottom" type="w:ST_AestheticVerticalMargin" use="required"/>
|
||||
<xs:attribute name="left" type="w:ST_AestheticMargin" use="required"/>
|
||||
<xs:attribute name="right" type="w:ST_AestheticMargin" use="required"/>
|
||||
<xs:attribute name="header" type="xs:nonNegativeInteger" use="optional"/>
|
||||
<xs:attribute name="footer" type="xs:nonNegativeInteger" use="optional"/>
|
||||
<xs:attribute name="gutter" type="xs:nonNegativeInteger" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- COMPOSITE TYPE: Aesthetic Table Cell Margin Check -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_AestheticTableCellMargin">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Aesthetic table cell margin validator.
|
||||
Ensures minimum padding for readability.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:attribute name="w" type="w:ST_AestheticCellPadding" use="required"/>
|
||||
<xs:attribute name="type" use="required">
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="dxa"/>
|
||||
<xs:enumeration value="nil"/>
|
||||
<xs:enumeration value="pct"/>
|
||||
<xs:enumeration value="auto"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- PROGRAMMATIC RULES (cannot be expressed in XSD) -->
|
||||
<!-- ============================================================ -->
|
||||
<!-- -->
|
||||
<!-- The following rules must be checked by a programmatic -->
|
||||
<!-- validator (e.g., AestheticRuleValidator.cs). They are -->
|
||||
<!-- documented here for completeness. -->
|
||||
<!-- -->
|
||||
<!-- ── RULE P1: Heading Size Hierarchy ── -->
|
||||
<!-- H1 sz >= H2 sz >= H3 sz >= body sz -->
|
||||
<!-- Exception: APA-style where all headings = body size. -->
|
||||
<!-- Implementation: Collect sz from Heading1/2/3 styles and -->
|
||||
<!-- docDefaults. Verify monotonic decrease (or equality). -->
|
||||
<!-- -->
|
||||
<!-- ── RULE P2: Maximum 3 Font Families ── -->
|
||||
<!-- Across docDefaults rPr + all style rPr, at most 3 distinct -->
|
||||
<!-- font families (by Ascii name) should be used. -->
|
||||
<!-- WHY: More than 3 fonts creates visual chaos. Professional -->
|
||||
<!-- designs typically use 1-2 families. -->
|
||||
<!-- Implementation: Collect all rFonts.ascii values from -->
|
||||
<!-- docDefaults and all styles. Count distinct. Warn if > 3. -->
|
||||
<!-- -->
|
||||
<!-- ── RULE P3: Heading Space-Before >= Space-After ── -->
|
||||
<!-- For heading styles, spaceBefore should be >= spaceAfter. -->
|
||||
<!-- WHY: Headings should be visually closer to the content they -->
|
||||
<!-- introduce than to the content above. This is the -->
|
||||
<!-- "proximity principle" of Gestalt design. -->
|
||||
<!-- Implementation: For each Heading style, compare pPr spacing -->
|
||||
<!-- before vs after values. -->
|
||||
<!-- -->
|
||||
<!-- ── RULE P4: Spacing-Size Coherence ── -->
|
||||
<!-- Paragraph after-spacing should be proportional to body size: -->
|
||||
<!-- after >= bodySize * 0.5 AND after <= bodySize * 1.5 -->
|
||||
<!-- WHY: Too little spacing makes paragraphs run together. -->
|
||||
<!-- Too much spacing disconnects them. -->
|
||||
<!-- Implementation: Get body sz from docDefaults, convert to DXA -->
|
||||
<!-- (multiply by 10), check after-spacing ratio. -->
|
||||
<!-- -->
|
||||
<!-- ── RULE P5: Color Consistency ── -->
|
||||
<!-- All heading styles should use the same color value. -->
|
||||
<!-- Body text color (if set) should be consistent across styles. -->
|
||||
<!-- WHY: Inconsistent colors look accidental, not designed. -->
|
||||
<!-- Exception: Caption and footnote styles may differ. -->
|
||||
<!-- Implementation: Collect color.val from heading styles. -->
|
||||
<!-- Verify all are identical. -->
|
||||
<!-- -->
|
||||
<!-- ── RULE P6: Indent/Spacing Mutual Exclusion ── -->
|
||||
<!-- If first-line indent > 0 in docDefaults, then after-spacing -->
|
||||
<!-- should be 0 (and vice versa). Using BOTH indent AND spacing -->
|
||||
<!-- is visually redundant — it signals uncertainty. -->
|
||||
<!-- Exception: Headings may override this. -->
|
||||
<!-- Implementation: Check docDefaults pPr. If firstLine > 0 AND -->
|
||||
<!-- after > 0, emit a warning (not error). -->
|
||||
<!-- -->
|
||||
<!-- ── RULE P7: Table Border Consistency ── -->
|
||||
<!-- Within a single table, border styles should be internally -->
|
||||
<!-- consistent (all single, or all none — not a random mix). -->
|
||||
<!-- Implementation: Check tblBorders for consistent val values. -->
|
||||
<!-- -->
|
||||
<!-- ── RULE P8: Line Spacing vs Font Size Harmony ── -->
|
||||
<!-- For fixed line spacing (lineRule="exact"): -->
|
||||
<!-- lineHeight >= fontSize * 1.2 -->
|
||||
<!-- WHY: Fixed line spacing less than 1.2x the font size causes -->
|
||||
<!-- ascender/descender clipping. -->
|
||||
<!-- Implementation: When lineRule="exact", compare line value -->
|
||||
<!-- against the effective font size. -->
|
||||
<!-- -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
</xs:schema>
|
||||
130
skills/minimax-docx/assets/xsd/business-rules.xsd
Normal file
130
skills/minimax-docx/assets/xsd/business-rules.xsd
Normal file
@@ -0,0 +1,130 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Business Rules Gate-Check Schema for minimax-docx -->
|
||||
<!-- Used in Scenario C (template application) as hard gate -->
|
||||
<!-- Validates business compliance beyond XML correctness -->
|
||||
<!-- MIT License - minimax-docx project -->
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
targetNamespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
elementFormDefault="qualified">
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Page margins: constrained to reasonable bounds -->
|
||||
<!-- Minimum 360 DXA (0.25 inch), maximum 4320 DXA (3 inches) -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:simpleType name="ST_MarginMeasure">
|
||||
<xs:restriction base="xs:integer">
|
||||
<xs:minInclusive value="360"/>
|
||||
<xs:maxInclusive value="4320"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Signed margin (top/bottom can be negative for overlap) -->
|
||||
<xs:simpleType name="ST_SignedMarginMeasure">
|
||||
<xs:restriction base="xs:integer">
|
||||
<xs:minInclusive value="-4320"/>
|
||||
<xs:maxInclusive value="4320"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Font size constraints -->
|
||||
<!-- Body text: 16-144 half-points (8-72pt) -->
|
||||
<!-- Heading text: 20-192 half-points (10-96pt) -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:simpleType name="ST_BodyFontSize">
|
||||
<xs:restriction base="xs:positiveInteger">
|
||||
<xs:minInclusive value="16"/>
|
||||
<xs:maxInclusive value="144"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="ST_HeadingFontSize">
|
||||
<xs:restriction base="xs:positiveInteger">
|
||||
<xs:minInclusive value="20"/>
|
||||
<xs:maxInclusive value="192"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Valid standard page sizes (width x height in DXA) -->
|
||||
<!-- ============================================================ -->
|
||||
<!-- Letter: 12240 x 15840 -->
|
||||
<!-- A4: 11906 x 16838 -->
|
||||
<!-- Legal: 12240 x 20160 -->
|
||||
<!-- A3: 16838 x 23811 -->
|
||||
<!-- A5: 8391 x 11906 -->
|
||||
|
||||
<xs:simpleType name="ST_PageWidth">
|
||||
<xs:restriction base="xs:positiveInteger">
|
||||
<xs:minInclusive value="5040"/>
|
||||
<xs:maxInclusive value="31680"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:simpleType name="ST_PageHeight">
|
||||
<xs:restriction base="xs:positiveInteger">
|
||||
<xs:minInclusive value="5040"/>
|
||||
<xs:maxInclusive value="31680"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Constrained section properties for gate-check -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_GateCheckSectPr">
|
||||
<xs:all>
|
||||
<xs:element name="pgSz" minOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="w" type="w:ST_PageWidth" use="required"/>
|
||||
<xs:attribute name="h" type="w:ST_PageHeight" use="required"/>
|
||||
<xs:attribute name="orient" use="optional">
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="portrait"/>
|
||||
<xs:enumeration value="landscape"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="pgMar" minOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="top" type="w:ST_SignedMarginMeasure" use="required"/>
|
||||
<xs:attribute name="bottom" type="w:ST_SignedMarginMeasure" use="required"/>
|
||||
<xs:attribute name="left" type="w:ST_MarginMeasure" use="required"/>
|
||||
<xs:attribute name="right" type="w:ST_MarginMeasure" use="required"/>
|
||||
<xs:attribute name="header" type="xs:nonNegativeInteger" use="optional"/>
|
||||
<xs:attribute name="footer" type="xs:nonNegativeInteger" use="optional"/>
|
||||
<xs:attribute name="gutter" type="xs:nonNegativeInteger" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Required styles: at minimum Normal and Heading1 must exist -->
|
||||
<!-- This is enforced programmatically by GateCheckValidator -->
|
||||
<!-- rather than via XSD, since XSD cannot validate style presence -->
|
||||
<!-- across separate XML parts. -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Constrained run properties for font size validation -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_GateCheckRPr">
|
||||
<xs:all>
|
||||
<xs:element name="sz" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="w:ST_BodyFontSize" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="szCs" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="w:ST_BodyFontSize" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
</xs:schema>
|
||||
159
skills/minimax-docx/assets/xsd/common-types.xsd
Normal file
159
skills/minimax-docx/assets/xsd/common-types.xsd
Normal file
@@ -0,0 +1,159 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Common type definitions for WordprocessingML subset schema -->
|
||||
<!-- MIT License - minimax-docx project -->
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
targetNamespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
elementFormDefault="qualified">
|
||||
|
||||
<!-- Measurement: non-negative twips (1/1440 inch) -->
|
||||
<xs:simpleType name="ST_TwipsMeasure">
|
||||
<xs:restriction base="xs:nonNegativeInteger"/>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Measurement: signed twips (for negative margins/indents) -->
|
||||
<xs:simpleType name="ST_SignedTwipsMeasure">
|
||||
<xs:restriction base="xs:integer"/>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Half-point measure for font sizes (1 = 0.5pt) -->
|
||||
<xs:simpleType name="ST_HpsMeasure">
|
||||
<xs:restriction base="xs:positiveInteger"/>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Hex color: 6 hex digits -->
|
||||
<xs:simpleType name="ST_HexColor">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:pattern value="auto|[0-9a-fA-F]{6}"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- On/Off toggle -->
|
||||
<xs:simpleType name="ST_OnOff">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="true"/>
|
||||
<xs:enumeration value="false"/>
|
||||
<xs:enumeration value="0"/>
|
||||
<xs:enumeration value="1"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Justification -->
|
||||
<xs:simpleType name="ST_Jc">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="left"/>
|
||||
<xs:enumeration value="center"/>
|
||||
<xs:enumeration value="right"/>
|
||||
<xs:enumeration value="both"/>
|
||||
<xs:enumeration value="distribute"/>
|
||||
<xs:enumeration value="start"/>
|
||||
<xs:enumeration value="end"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Break type -->
|
||||
<xs:simpleType name="ST_BrType">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="page"/>
|
||||
<xs:enumeration value="column"/>
|
||||
<xs:enumeration value="textWrapping"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Underline patterns -->
|
||||
<xs:simpleType name="ST_Underline">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="none"/>
|
||||
<xs:enumeration value="single"/>
|
||||
<xs:enumeration value="words"/>
|
||||
<xs:enumeration value="double"/>
|
||||
<xs:enumeration value="thick"/>
|
||||
<xs:enumeration value="dotted"/>
|
||||
<xs:enumeration value="dash"/>
|
||||
<xs:enumeration value="dotDash"/>
|
||||
<xs:enumeration value="dotDotDash"/>
|
||||
<xs:enumeration value="wave"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Vertical alignment for subscript/superscript -->
|
||||
<xs:simpleType name="ST_VerticalAlignRun">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="baseline"/>
|
||||
<xs:enumeration value="superscript"/>
|
||||
<xs:enumeration value="subscript"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Section break type -->
|
||||
<xs:simpleType name="ST_SectionMark">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="nextPage"/>
|
||||
<xs:enumeration value="nextColumn"/>
|
||||
<xs:enumeration value="continuous"/>
|
||||
<xs:enumeration value="evenPage"/>
|
||||
<xs:enumeration value="oddPage"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Header/footer type -->
|
||||
<xs:simpleType name="ST_HdrFtr">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="even"/>
|
||||
<xs:enumeration value="default"/>
|
||||
<xs:enumeration value="first"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Table width type -->
|
||||
<xs:simpleType name="ST_TblWidth">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="auto"/>
|
||||
<xs:enumeration value="dxa"/>
|
||||
<xs:enumeration value="nil"/>
|
||||
<xs:enumeration value="pct"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Vertical merge -->
|
||||
<xs:simpleType name="ST_Merge">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="continue"/>
|
||||
<xs:enumeration value="restart"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Highlight colors -->
|
||||
<xs:simpleType name="ST_HighlightColor">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="black"/>
|
||||
<xs:enumeration value="blue"/>
|
||||
<xs:enumeration value="cyan"/>
|
||||
<xs:enumeration value="darkBlue"/>
|
||||
<xs:enumeration value="darkCyan"/>
|
||||
<xs:enumeration value="darkGray"/>
|
||||
<xs:enumeration value="darkGreen"/>
|
||||
<xs:enumeration value="darkMagenta"/>
|
||||
<xs:enumeration value="darkRed"/>
|
||||
<xs:enumeration value="darkYellow"/>
|
||||
<xs:enumeration value="green"/>
|
||||
<xs:enumeration value="lightGray"/>
|
||||
<xs:enumeration value="magenta"/>
|
||||
<xs:enumeration value="none"/>
|
||||
<xs:enumeration value="red"/>
|
||||
<xs:enumeration value="white"/>
|
||||
<xs:enumeration value="yellow"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Percentage (for table width pct, etc.) -->
|
||||
<xs:simpleType name="ST_DecimalNumber">
|
||||
<xs:restriction base="xs:integer"/>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- Relationship ID reference -->
|
||||
<xs:simpleType name="ST_RelationshipId">
|
||||
<xs:restriction base="xs:string"/>
|
||||
</xs:simpleType>
|
||||
|
||||
</xs:schema>
|
||||
589
skills/minimax-docx/assets/xsd/wml-subset.xsd
Normal file
589
skills/minimax-docx/assets/xsd/wml-subset.xsd
Normal file
@@ -0,0 +1,589 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- WordprocessingML Subset Schema for minimax-docx -->
|
||||
<!-- Curated subset of ISO 29500 covering elements agents commonly generate -->
|
||||
<!-- MIT License - minimax-docx project -->
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
|
||||
targetNamespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
elementFormDefault="qualified">
|
||||
|
||||
<xs:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"/>
|
||||
<xs:import namespace="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"/>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Root element -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:element name="document" type="w:CT_Document"/>
|
||||
|
||||
<xs:complexType name="CT_Document">
|
||||
<xs:sequence>
|
||||
<xs:element name="body" type="w:CT_Body" minOccurs="0"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Body -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_Body">
|
||||
<xs:sequence>
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="p" type="w:CT_P"/>
|
||||
<xs:element name="tbl" type="w:CT_Tbl"/>
|
||||
<xs:element name="sdt" type="w:CT_SdtBlock"/>
|
||||
<xs:element name="bookmarkStart" type="w:CT_BookmarkStart"/>
|
||||
<xs:element name="bookmarkEnd" type="w:CT_BookmarkEnd"/>
|
||||
</xs:choice>
|
||||
<xs:element name="sectPr" type="w:CT_SectPr" minOccurs="0"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Paragraph -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_P">
|
||||
<xs:sequence>
|
||||
<xs:element name="pPr" type="w:CT_PPr" minOccurs="0"/>
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="r" type="w:CT_R"/>
|
||||
<xs:element name="hyperlink" type="w:CT_Hyperlink"/>
|
||||
<xs:element name="bookmarkStart" type="w:CT_BookmarkStart"/>
|
||||
<xs:element name="bookmarkEnd" type="w:CT_BookmarkEnd"/>
|
||||
<xs:element name="commentRangeStart" type="w:CT_MarkupRange"/>
|
||||
<xs:element name="commentRangeEnd" type="w:CT_MarkupRange"/>
|
||||
<xs:element name="ins" type="w:CT_RunTrackChange"/>
|
||||
<xs:element name="del" type="w:CT_RunTrackChange"/>
|
||||
</xs:choice>
|
||||
</xs:sequence>
|
||||
<xs:attribute ref="r:id" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- Paragraph Properties -->
|
||||
<xs:complexType name="CT_PPr">
|
||||
<xs:all>
|
||||
<xs:element name="pStyle" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="keepNext" type="w:CT_OnOff" minOccurs="0"/>
|
||||
<xs:element name="keepLines" type="w:CT_OnOff" minOccurs="0"/>
|
||||
<xs:element name="pageBreakBefore" type="w:CT_OnOff" minOccurs="0"/>
|
||||
<xs:element name="widowControl" type="w:CT_OnOff" minOccurs="0"/>
|
||||
<xs:element name="numPr" type="w:CT_NumPr" minOccurs="0"/>
|
||||
<xs:element name="spacing" type="w:CT_Spacing" minOccurs="0"/>
|
||||
<xs:element name="ind" type="w:CT_Ind" minOccurs="0"/>
|
||||
<xs:element name="jc" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="outlineLvl" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:integer" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="rPr" type="w:CT_RPr" minOccurs="0"/>
|
||||
<xs:element name="pBdr" type="w:CT_PBdr" minOccurs="0"/>
|
||||
<xs:element name="shd" type="w:CT_Shd" minOccurs="0"/>
|
||||
<xs:element name="tabs" type="w:CT_Tabs" minOccurs="0"/>
|
||||
<xs:element name="sectPr" type="w:CT_SectPr" minOccurs="0"/>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Run -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_R">
|
||||
<xs:sequence>
|
||||
<xs:element name="rPr" type="w:CT_RPr" minOccurs="0"/>
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="t" type="w:CT_Text"/>
|
||||
<xs:element name="delText" type="w:CT_Text"/>
|
||||
<xs:element name="br" type="w:CT_Br"/>
|
||||
<xs:element name="tab" type="w:CT_Empty"/>
|
||||
<xs:element name="cr" type="w:CT_Empty"/>
|
||||
<xs:element name="drawing" type="w:CT_Drawing"/>
|
||||
<xs:element name="commentReference" type="w:CT_MarkupRef"/>
|
||||
<xs:element name="footnoteReference" type="w:CT_FtnEdnRef"/>
|
||||
<xs:element name="endnoteReference" type="w:CT_FtnEdnRef"/>
|
||||
</xs:choice>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- Run Properties -->
|
||||
<xs:complexType name="CT_RPr">
|
||||
<xs:all>
|
||||
<xs:element name="rStyle" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="rFonts" type="w:CT_Fonts" minOccurs="0"/>
|
||||
<xs:element name="b" type="w:CT_OnOff" minOccurs="0"/>
|
||||
<xs:element name="bCs" type="w:CT_OnOff" minOccurs="0"/>
|
||||
<xs:element name="i" type="w:CT_OnOff" minOccurs="0"/>
|
||||
<xs:element name="iCs" type="w:CT_OnOff" minOccurs="0"/>
|
||||
<xs:element name="caps" type="w:CT_OnOff" minOccurs="0"/>
|
||||
<xs:element name="smallCaps" type="w:CT_OnOff" minOccurs="0"/>
|
||||
<xs:element name="strike" type="w:CT_OnOff" minOccurs="0"/>
|
||||
<xs:element name="dstrike" type="w:CT_OnOff" minOccurs="0"/>
|
||||
<xs:element name="vanish" type="w:CT_OnOff" minOccurs="0"/>
|
||||
<xs:element name="color" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:string" use="required"/>
|
||||
<xs:attribute name="themeColor" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="spacing" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:integer" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="sz" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:positiveInteger" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="szCs" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:positiveInteger" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="highlight" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="u" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:string" use="required"/>
|
||||
<xs:attribute name="color" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="vertAlign" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="lang" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="eastAsia" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="bidi" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Text -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_Text" mixed="true">
|
||||
<xs:attribute ref="xml:space" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Table -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_Tbl">
|
||||
<xs:sequence>
|
||||
<xs:element name="tblPr" type="w:CT_TblPr" minOccurs="0"/>
|
||||
<xs:element name="tblGrid" type="w:CT_TblGrid" minOccurs="0"/>
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="tr" type="w:CT_Row"/>
|
||||
<xs:element name="bookmarkStart" type="w:CT_BookmarkStart"/>
|
||||
<xs:element name="bookmarkEnd" type="w:CT_BookmarkEnd"/>
|
||||
</xs:choice>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_TblPr">
|
||||
<xs:all>
|
||||
<xs:element name="tblStyle" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="tblW" type="w:CT_TblWidth" minOccurs="0"/>
|
||||
<xs:element name="jc" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="tblInd" type="w:CT_TblWidth" minOccurs="0"/>
|
||||
<xs:element name="tblBorders" type="w:CT_TblBorders" minOccurs="0"/>
|
||||
<xs:element name="shd" type="w:CT_Shd" minOccurs="0"/>
|
||||
<xs:element name="tblLayout" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="type" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="tblCellMar" type="w:CT_TblCellMar" minOccurs="0"/>
|
||||
<xs:element name="tblLook" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="firstRow" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="lastRow" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="firstColumn" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="lastColumn" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="noHBand" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="noVBand" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_TblGrid">
|
||||
<xs:sequence>
|
||||
<xs:element name="gridCol" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="w" type="xs:nonNegativeInteger" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_Row">
|
||||
<xs:sequence>
|
||||
<xs:element name="trPr" type="w:CT_TrPr" minOccurs="0"/>
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="tc" type="w:CT_Cell"/>
|
||||
<xs:element name="bookmarkStart" type="w:CT_BookmarkStart"/>
|
||||
<xs:element name="bookmarkEnd" type="w:CT_BookmarkEnd"/>
|
||||
</xs:choice>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_TrPr">
|
||||
<xs:all>
|
||||
<xs:element name="trHeight" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:nonNegativeInteger" use="optional"/>
|
||||
<xs:attribute name="hRule" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="tblHeader" type="w:CT_OnOff" minOccurs="0"/>
|
||||
<xs:element name="jc" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_Cell">
|
||||
<xs:sequence>
|
||||
<xs:element name="tcPr" type="w:CT_TcPr" minOccurs="0"/>
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="p" type="w:CT_P"/>
|
||||
<xs:element name="tbl" type="w:CT_Tbl"/>
|
||||
</xs:choice>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_TcPr">
|
||||
<xs:all>
|
||||
<xs:element name="tcW" type="w:CT_TblWidth" minOccurs="0"/>
|
||||
<xs:element name="gridSpan" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:positiveInteger" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="vMerge" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="tcBorders" type="w:CT_TcBorders" minOccurs="0"/>
|
||||
<xs:element name="shd" type="w:CT_Shd" minOccurs="0"/>
|
||||
<xs:element name="vAlign" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="noWrap" type="w:CT_OnOff" minOccurs="0"/>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Section Properties -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_SectPr">
|
||||
<xs:all>
|
||||
<xs:element name="headerReference" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="type" type="xs:string" use="required"/>
|
||||
<xs:attribute ref="r:id" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="footerReference" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="type" type="xs:string" use="required"/>
|
||||
<xs:attribute ref="r:id" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="type" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="pgSz" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="w" type="xs:nonNegativeInteger" use="required"/>
|
||||
<xs:attribute name="h" type="xs:nonNegativeInteger" use="required"/>
|
||||
<xs:attribute name="orient" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="pgMar" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="top" type="xs:integer" use="required"/>
|
||||
<xs:attribute name="right" type="xs:nonNegativeInteger" use="required"/>
|
||||
<xs:attribute name="bottom" type="xs:integer" use="required"/>
|
||||
<xs:attribute name="left" type="xs:nonNegativeInteger" use="required"/>
|
||||
<xs:attribute name="header" type="xs:nonNegativeInteger" use="optional"/>
|
||||
<xs:attribute name="footer" type="xs:nonNegativeInteger" use="optional"/>
|
||||
<xs:attribute name="gutter" type="xs:nonNegativeInteger" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="pgNumType" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="fmt" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="start" type="xs:nonNegativeInteger" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="cols" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="space" type="xs:nonNegativeInteger" use="optional"/>
|
||||
<xs:attribute name="num" type="xs:positiveInteger" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="docGrid" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="linePitch" type="xs:nonNegativeInteger" use="optional"/>
|
||||
<xs:attribute name="type" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="titlePg" type="w:CT_OnOff" minOccurs="0"/>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Hyperlink -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_Hyperlink">
|
||||
<xs:sequence>
|
||||
<xs:element name="r" type="w:CT_R" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute ref="r:id" use="optional"/>
|
||||
<xs:attribute name="anchor" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="history" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Track Changes -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_RunTrackChange">
|
||||
<xs:sequence>
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="r" type="w:CT_R"/>
|
||||
</xs:choice>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="id" type="xs:nonNegativeInteger" use="required"/>
|
||||
<xs:attribute name="author" type="xs:string" use="required"/>
|
||||
<xs:attribute name="date" type="xs:dateTime" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Bookmarks -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_BookmarkStart">
|
||||
<xs:attribute name="id" type="xs:nonNegativeInteger" use="required"/>
|
||||
<xs:attribute name="name" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_BookmarkEnd">
|
||||
<xs:attribute name="id" type="xs:nonNegativeInteger" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Comments -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_MarkupRange">
|
||||
<xs:attribute name="id" type="xs:nonNegativeInteger" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_MarkupRef">
|
||||
<xs:attribute name="id" type="xs:nonNegativeInteger" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Footnote/Endnote reference -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_FtnEdnRef">
|
||||
<xs:attribute name="id" type="xs:nonNegativeInteger" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Drawing (basic inline image) -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_Drawing">
|
||||
<xs:sequence>
|
||||
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Structured Document Tag (content control) -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_SdtBlock">
|
||||
<xs:sequence>
|
||||
<xs:element name="sdtPr" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:any processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="sdtContent" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="p" type="w:CT_P"/>
|
||||
<xs:element name="tbl" type="w:CT_Tbl"/>
|
||||
</xs:choice>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Helper types -->
|
||||
<!-- ============================================================ -->
|
||||
<xs:complexType name="CT_OnOff">
|
||||
<xs:attribute name="val" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_Empty"/>
|
||||
|
||||
<xs:complexType name="CT_Br">
|
||||
<xs:attribute name="type" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="clear" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_Fonts">
|
||||
<xs:attribute name="ascii" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="hAnsi" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="eastAsia" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="cs" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="asciiTheme" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="hAnsiTheme" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="eastAsiaTheme" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="cstheme" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_NumPr">
|
||||
<xs:all>
|
||||
<xs:element name="ilvl" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:nonNegativeInteger" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="numId" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:nonNegativeInteger" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_Spacing">
|
||||
<xs:attribute name="before" type="xs:nonNegativeInteger" use="optional"/>
|
||||
<xs:attribute name="after" type="xs:nonNegativeInteger" use="optional"/>
|
||||
<xs:attribute name="line" type="xs:integer" use="optional"/>
|
||||
<xs:attribute name="lineRule" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="beforeAutospacing" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="afterAutospacing" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_Ind">
|
||||
<xs:attribute name="left" type="xs:integer" use="optional"/>
|
||||
<xs:attribute name="right" type="xs:integer" use="optional"/>
|
||||
<xs:attribute name="hanging" type="xs:nonNegativeInteger" use="optional"/>
|
||||
<xs:attribute name="firstLine" type="xs:nonNegativeInteger" use="optional"/>
|
||||
<xs:attribute name="start" type="xs:integer" use="optional"/>
|
||||
<xs:attribute name="end" type="xs:integer" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_TblWidth">
|
||||
<xs:attribute name="w" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="type" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_Shd">
|
||||
<xs:attribute name="val" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="color" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="fill" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="themeFill" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_Border">
|
||||
<xs:attribute name="val" type="xs:string" use="required"/>
|
||||
<xs:attribute name="sz" type="xs:nonNegativeInteger" use="optional"/>
|
||||
<xs:attribute name="space" type="xs:nonNegativeInteger" use="optional"/>
|
||||
<xs:attribute name="color" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="themeColor" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_PBdr">
|
||||
<xs:all>
|
||||
<xs:element name="top" type="w:CT_Border" minOccurs="0"/>
|
||||
<xs:element name="left" type="w:CT_Border" minOccurs="0"/>
|
||||
<xs:element name="bottom" type="w:CT_Border" minOccurs="0"/>
|
||||
<xs:element name="right" type="w:CT_Border" minOccurs="0"/>
|
||||
<xs:element name="between" type="w:CT_Border" minOccurs="0"/>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_TblBorders">
|
||||
<xs:all>
|
||||
<xs:element name="top" type="w:CT_Border" minOccurs="0"/>
|
||||
<xs:element name="left" type="w:CT_Border" minOccurs="0"/>
|
||||
<xs:element name="bottom" type="w:CT_Border" minOccurs="0"/>
|
||||
<xs:element name="right" type="w:CT_Border" minOccurs="0"/>
|
||||
<xs:element name="insideH" type="w:CT_Border" minOccurs="0"/>
|
||||
<xs:element name="insideV" type="w:CT_Border" minOccurs="0"/>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_TcBorders">
|
||||
<xs:all>
|
||||
<xs:element name="top" type="w:CT_Border" minOccurs="0"/>
|
||||
<xs:element name="left" type="w:CT_Border" minOccurs="0"/>
|
||||
<xs:element name="bottom" type="w:CT_Border" minOccurs="0"/>
|
||||
<xs:element name="right" type="w:CT_Border" minOccurs="0"/>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_TblCellMar">
|
||||
<xs:all>
|
||||
<xs:element name="top" type="w:CT_TblWidth" minOccurs="0"/>
|
||||
<xs:element name="left" type="w:CT_TblWidth" minOccurs="0"/>
|
||||
<xs:element name="bottom" type="w:CT_TblWidth" minOccurs="0"/>
|
||||
<xs:element name="right" type="w:CT_TblWidth" minOccurs="0"/>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="CT_Tabs">
|
||||
<xs:sequence>
|
||||
<xs:element name="tab" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="val" type="xs:string" use="required"/>
|
||||
<xs:attribute name="pos" type="xs:integer" use="required"/>
|
||||
<xs:attribute name="leader" type="xs:string" use="optional"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
</xs:schema>
|
||||
357
skills/minimax-docx/references/cjk_typography.md
Normal file
357
skills/minimax-docx/references/cjk_typography.md
Normal file
@@ -0,0 +1,357 @@
|
||||
# CJK Typography & Mixed-Script Guide
|
||||
|
||||
Rules for Chinese, Japanese, and Korean text in DOCX documents.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Font Selection](#font-selection)
|
||||
2. [Font Size Names (CJK)](#font-size-names)
|
||||
3. [RunFonts Mapping](#runfonts-mapping)
|
||||
4. [Punctuation & Line Breaking](#punctuation--line-breaking)
|
||||
5. [Paragraph Indentation](#paragraph-indentation)
|
||||
6. [Line Spacing for CJK](#line-spacing)
|
||||
7. [Chinese Government Standard (GB/T 9704)](#gbt-9704)
|
||||
8. [Mixed CJK + Latin Best Practices](#mixed-script)
|
||||
9. [OpenXML Quick Reference](#openxml-quick-reference)
|
||||
|
||||
---
|
||||
|
||||
## Font Selection
|
||||
|
||||
### Recommended CJK Fonts
|
||||
|
||||
| Language | Serif (正文) | Sans (标题) | Notes |
|
||||
|----------|-------------|-------------|-------|
|
||||
| **Simplified Chinese** | 宋体 (SimSun) | 微软雅黑 (Microsoft YaHei) | YaHei for screen, SimSun for print |
|
||||
| **Simplified Chinese** | 仿宋 (FangSong) | 黑体 (SimHei) | Government documents |
|
||||
| **Traditional Chinese** | 新細明體 (PMingLiU) | 微軟正黑體 (Microsoft JhengHei) | Taiwan standard |
|
||||
| **Japanese** | MS 明朝 (MS Mincho) | MS ゴシック (MS Gothic) | Classic pairing |
|
||||
| **Japanese** | 游明朝 (Yu Mincho) | 游ゴシック (Yu Gothic) | Modern, Windows 10+ |
|
||||
| **Korean** | 바탕 (Batang) | 맑은 고딕 (Malgun Gothic) | Standard pairing |
|
||||
|
||||
### Government Document Fonts (公文)
|
||||
|
||||
| Element | Font | Size |
|
||||
|---------|------|------|
|
||||
| 标题 (title) | 小标宋 (FZXiaoBiaoSong-B05S) | 二号 (22pt) |
|
||||
| 一级标题 | 黑体 (SimHei) | 三号 (16pt) |
|
||||
| 二级标题 | 楷体_GB2312 (KaiTi_GB2312) | 三号 (16pt) |
|
||||
| 三级标题 | 仿宋_GB2312 加粗 | 三号 (16pt) |
|
||||
| 正文 (body) | 仿宋_GB2312 (FangSong_GB2312) | 三号 (16pt) |
|
||||
| 附注/页码 | 宋体 (SimSun) | 四号 (14pt) |
|
||||
|
||||
---
|
||||
|
||||
## Font Size Names
|
||||
|
||||
CJK uses named sizes. Map to points and `w:sz` half-point values:
|
||||
|
||||
| 字号 | Points | `w:sz` | Common Use |
|
||||
|------|--------|--------|------------|
|
||||
| 初号 | 42pt | 84 | Display title |
|
||||
| 小初 | 36pt | 72 | Large title |
|
||||
| 一号 | 26pt | 52 | Chapter heading |
|
||||
| 小一 | 24pt | 48 | Major heading |
|
||||
| 二号 | 22pt | 44 | Document title (公文) |
|
||||
| 小二 | 18pt | 36 | Western H1 equivalent |
|
||||
| 三号 | 16pt | 32 | CJK heading / 公文 body |
|
||||
| 小三 | 15pt | 30 | Sub-heading |
|
||||
| 四号 | 14pt | 28 | CJK subheading |
|
||||
| 小四 | 12pt | 24 | Standard body (CJK) |
|
||||
| 五号 | 10.5pt | 21 | Compact CJK body |
|
||||
| 小五 | 9pt | 18 | Footnotes |
|
||||
| 六号 | 7.5pt | 15 | Fine print |
|
||||
|
||||
---
|
||||
|
||||
## RunFonts Mapping
|
||||
|
||||
OpenXML uses four font slots to handle multilingual text:
|
||||
|
||||
```xml
|
||||
<w:rFonts
|
||||
w:ascii="Calibri" <!-- Latin characters (U+0000–U+007F) -->
|
||||
w:hAnsi="Calibri" <!-- Latin extended, Greek, Cyrillic -->
|
||||
w:eastAsia="SimSun" <!-- CJK Unified Ideographs, Kana, Hangul -->
|
||||
w:cs="Arial" <!-- Arabic, Hebrew, Thai, Devanagari -->
|
||||
/>
|
||||
```
|
||||
|
||||
**Word's character classification logic:**
|
||||
|
||||
1. Character is in CJK range → uses `w:eastAsia` font
|
||||
2. Character is in complex script range → uses `w:cs` font
|
||||
3. Character is basic Latin (ASCII) → uses `w:ascii` font
|
||||
4. Everything else → uses `w:hAnsi` font
|
||||
|
||||
**Key**: `w:eastAsia` is the **only** way to set CJK fonts. Setting just `w:ascii` will NOT affect CJK characters. Mixed text within a single run auto-switches fonts at the character level — no need for separate runs.
|
||||
|
||||
### Document Defaults
|
||||
|
||||
```xml
|
||||
<w:docDefaults>
|
||||
<w:rPrDefault>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:eastAsia="SimSun" w:cs="Arial" />
|
||||
<w:sz w:val="22" />
|
||||
<w:szCs w:val="22" />
|
||||
<w:lang w:val="en-US" w:eastAsia="zh-CN" />
|
||||
</w:rPr>
|
||||
</w:rPrDefault>
|
||||
</w:docDefaults>
|
||||
```
|
||||
|
||||
`w:lang w:eastAsia` helps Word resolve ambiguous characters (e.g., punctuation shared between CJK and Latin).
|
||||
|
||||
---
|
||||
|
||||
## Punctuation & Line Breaking
|
||||
|
||||
### Full-Width vs Half-Width
|
||||
|
||||
CJK text uses full-width punctuation:
|
||||
|
||||
| Type | CJK | Latin |
|
||||
|------|-----|-------|
|
||||
| Period | 。(U+3002) | . |
|
||||
| Comma | ,(U+FF0C) 、(U+3001) | , |
|
||||
| Colon | :(U+FF1A) | : |
|
||||
| Semicolon | ;(U+FF1B) | ; |
|
||||
| Quotes | 「」『』 or ""'' | "" '' |
|
||||
| Parentheses | ()(U+FF08/09) | () |
|
||||
|
||||
In mixed text, use the punctuation style of the **surrounding language context**.
|
||||
|
||||
### OpenXML Controls
|
||||
|
||||
```xml
|
||||
<w:pPr>
|
||||
<w:adjustRightInd w:val="true" /> <!-- Adjust right indent for CJK punctuation -->
|
||||
<w:snapToGrid w:val="true" /> <!-- Align to document grid -->
|
||||
<w:kinsoku w:val="true" /> <!-- Enable CJK line breaking rules -->
|
||||
<w:overflowPunct w:val="true" /> <!-- Allow punctuation to overflow margins -->
|
||||
</w:pPr>
|
||||
```
|
||||
|
||||
### Kinsoku Rules (禁則処理)
|
||||
|
||||
Prevents certain characters from appearing at the start or end of a line:
|
||||
- **Cannot start a line**: `)」』】〉》。、,!?;:` and closing brackets
|
||||
- **Cannot end a line**: `(「『【〈《` and opening brackets
|
||||
|
||||
Word applies these automatically when `w:kinsoku` is enabled.
|
||||
|
||||
### Line Breaking
|
||||
|
||||
- CJK characters can break between **any two characters** (no word boundaries needed)
|
||||
- Latin words within CJK text still follow word-boundary breaking
|
||||
- `w:wordWrap w:val="false"` enables CJK-style breaking (break anywhere)
|
||||
|
||||
---
|
||||
|
||||
## Paragraph Indentation
|
||||
|
||||
### Chinese Standard: 2-Character Indent
|
||||
|
||||
Chinese body text conventionally uses a 2-character first-line indent:
|
||||
|
||||
```xml
|
||||
<w:ind w:firstLineChars="200" /> <!-- 200 = 2 characters × 100 -->
|
||||
```
|
||||
|
||||
Preferred over `w:firstLine` with fixed DXA because `firstLineChars` scales with font size.
|
||||
|
||||
| Indent | Value |
|
||||
|--------|-------|
|
||||
| 1 character | `w:firstLineChars="100"` |
|
||||
| 2 characters | `w:firstLineChars="200"` |
|
||||
| 3 characters | `w:firstLineChars="300"` |
|
||||
|
||||
---
|
||||
|
||||
## Line Spacing
|
||||
|
||||
- CJK characters are taller than Latin characters at the same point size
|
||||
- Default `1.0` line spacing may feel cramped with CJK text
|
||||
- Recommended: `1.15–1.5` for mixed CJK+Latin, `1.0` with fixed 28pt for 公文
|
||||
|
||||
### Auto Spacing
|
||||
|
||||
```xml
|
||||
<w:pPr>
|
||||
<w:autoSpaceDE w:val="true"/> <!-- auto space between CJK and Latin -->
|
||||
<w:autoSpaceDN w:val="true"/> <!-- auto space between CJK and numbers -->
|
||||
</w:pPr>
|
||||
```
|
||||
|
||||
Adds ~¼ em spacing between CJK and non-CJK characters automatically. **Recommended: always enable.**
|
||||
|
||||
---
|
||||
|
||||
## GB/T 9704
|
||||
|
||||
Chinese government document standard (党政机关公文格式). These are **strict requirements**, not suggestions.
|
||||
|
||||
### Page Setup
|
||||
|
||||
| Parameter | Value | OpenXML |
|
||||
|-----------|-------|---------|
|
||||
| Page size | A4 (210×297mm) | Width=11906, Height=16838 |
|
||||
| Top margin | 37mm | 2098 DXA |
|
||||
| Bottom margin | 35mm | 1984 DXA |
|
||||
| Left margin | 28mm | 1588 DXA |
|
||||
| Right margin | 26mm | 1474 DXA |
|
||||
| Characters/line | 28 | |
|
||||
| Lines/page | 22 | |
|
||||
| Line spacing | Fixed 28pt | `line="560"` lineRule="exact" |
|
||||
|
||||
### Document Structure
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ 发文机关标志 (红头) │ ← 小标宋 or 红色大字
|
||||
│ ══════════════════ (红线) │ ← Red #FF0000, 2pt
|
||||
├─────────────────────────────────┤
|
||||
│ 发文字号: X机发〔2025〕X号 │ ← 仿宋 三号, centered
|
||||
│ │
|
||||
│ 标题 (Title) │ ← 小标宋 二号, centered
|
||||
│ │ 可分多行,回行居中
|
||||
│ 主送机关: │ ← 仿宋 三号
|
||||
│ │
|
||||
│ 正文 (Body)... │ ← 仿宋_GB2312 三号
|
||||
│ 一、一级标题 │ ← 黑体 三号
|
||||
│ (一)二级标题 │ ← 楷体 三号
|
||||
│ 1. 三级标题 │ ← 仿宋 三号 加粗
|
||||
│ (1) 四级标题 │ ← 仿宋 三号
|
||||
│ │
|
||||
│ 附件: 1. xxx │ ← 仿宋 三号
|
||||
│ │
|
||||
│ 发文机关署名 │ ← 仿宋 三号
|
||||
│ 成文日期 │ ← 仿宋 三号, 小写中文数字
|
||||
├─────────────────────────────────┤
|
||||
│ ══════════════════ (版记线) │
|
||||
│ 抄送: xxx │ ← 仿宋 四号
|
||||
│ 印发机关及日期 │ ← 仿宋 四号
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Numbering System
|
||||
|
||||
```
|
||||
一、 ← 黑体 (SimHei), no indentation
|
||||
(一) ← 楷体 (KaiTi), indented 2 chars
|
||||
1. ← 仿宋加粗 (FangSong Bold), indented 2 chars
|
||||
(1) ← 仿宋 (FangSong), indented 2 chars
|
||||
```
|
||||
|
||||
### Colors
|
||||
|
||||
| Element | Color | Requirement |
|
||||
|---------|-------|-------------|
|
||||
| All body text | Black #000000 | Mandatory |
|
||||
| 红头 (agency name) | Red #FF0000 | Mandatory |
|
||||
| 红线 (separator) | Red #FF0000 | Mandatory |
|
||||
| 公章 (official seal) | Red | Mandatory |
|
||||
|
||||
### Page Numbers
|
||||
|
||||
- Position: bottom center
|
||||
- Format: `-X-` (dash-number-dash)
|
||||
- Font: 宋体 四号 (SimSun 14pt, `sz="28"`)
|
||||
- No page number on cover page if present
|
||||
|
||||
---
|
||||
|
||||
## Mixed Script
|
||||
|
||||
### Font Size Harmony
|
||||
|
||||
CJK characters appear larger than Latin characters at the same point size. Compensation:
|
||||
|
||||
- If body is Calibri 11pt, pair with CJK at 11pt (same size — CJK looks slightly larger but acceptable)
|
||||
- If precise visual match needed, CJK can be set 0.5–1pt smaller
|
||||
- In practice, same point size is standard — don't over-optimize
|
||||
|
||||
### Bold and Italic
|
||||
|
||||
- **Chinese/Japanese have no true italic.** Word synthesizes a slant which looks poor
|
||||
- Use **bold** for emphasis in CJK text
|
||||
- Use 着重号 (emphasis dots) for traditional emphasis: `<w:em w:val="dot"/>` on RunProperties
|
||||
|
||||
---
|
||||
|
||||
## OpenXML Quick Reference
|
||||
|
||||
### Set EastAsia Font (C#)
|
||||
|
||||
```csharp
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new RunFonts { EastAsia = "SimSun", Ascii = "Calibri", HighAnsi = "Calibri" },
|
||||
new FontSize { Val = "32" } // 三号 = 16pt = sz 32
|
||||
),
|
||||
new Text("这是正文内容")
|
||||
);
|
||||
```
|
||||
|
||||
### Document Defaults (C#)
|
||||
|
||||
```csharp
|
||||
new DocDefaults(new RunPropertiesDefault(new RunPropertiesBaseStyle(
|
||||
new RunFonts {
|
||||
Ascii = "Calibri", HighAnsi = "Calibri",
|
||||
EastAsia = "Microsoft YaHei"
|
||||
},
|
||||
new Languages { Val = "en-US", EastAsia = "zh-CN" }
|
||||
)));
|
||||
```
|
||||
|
||||
### 公文 Style Definitions (C#)
|
||||
|
||||
```csharp
|
||||
// Title style — 小标宋 二号 centered
|
||||
new Style(
|
||||
new StyleName { Val = "GongWen Title" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new StyleRunProperties(
|
||||
new RunFonts { EastAsia = "FZXiaoBiaoSong-B05S" },
|
||||
new FontSize { Val = "44" }, // 二号 = 22pt
|
||||
new Bold()
|
||||
),
|
||||
new StyleParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center },
|
||||
new SpacingBetweenLines { Line = "560", LineRule = LineSpacingRuleValues.Exact }
|
||||
)
|
||||
) { Type = StyleValues.Paragraph, StyleId = "GongWenTitle" };
|
||||
|
||||
// Body style — 仿宋_GB2312 三号
|
||||
new Style(
|
||||
new StyleName { Val = "GongWen Body" },
|
||||
new StyleRunProperties(
|
||||
new RunFonts { EastAsia = "FangSong_GB2312", Ascii = "FangSong_GB2312" },
|
||||
new FontSize { Val = "32" } // 三号 = 16pt
|
||||
),
|
||||
new StyleParagraphProperties(
|
||||
new SpacingBetweenLines { Line = "560", LineRule = LineSpacingRuleValues.Exact }
|
||||
)
|
||||
) { Type = StyleValues.Paragraph, StyleId = "GongWenBody" };
|
||||
```
|
||||
|
||||
### Emphasis Dots (着重号)
|
||||
|
||||
```csharp
|
||||
new RunProperties(new Emphasis { Val = EmphasisMarkValues.Dot });
|
||||
```
|
||||
|
||||
### East Asian Text Layout
|
||||
|
||||
```xml
|
||||
<!-- Snap to grid (align CJK chars to character grid) -->
|
||||
<w:snapToGrid w:val="true"/>
|
||||
|
||||
<!-- Two-lines-in-one (双行合一) -->
|
||||
<w:eastAsianLayout w:id="1" w:combine="true"/>
|
||||
|
||||
<!-- Vertical text in a cell -->
|
||||
<w:textDirection w:val="tbRl"/>
|
||||
```
|
||||
184
skills/minimax-docx/references/cjk_university_template_guide.md
Normal file
184
skills/minimax-docx/references/cjk_university_template_guide.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Chinese University Thesis Template Guide (中国高校论文模板指南)
|
||||
|
||||
## Why This Guide Exists
|
||||
|
||||
Chinese university thesis templates (.docx) have structural patterns that differ significantly
|
||||
from Western templates. Agents that assume Western conventions (Heading1/Heading2/Normal) will
|
||||
fail repeatedly. This guide documents the ACTUAL patterns found in Chinese templates.
|
||||
|
||||
## Common StyleId Patterns
|
||||
|
||||
### Pattern A: Numeric IDs (most common in Chinese Word templates)
|
||||
|
||||
| Style Purpose | styleId | w:name | w:basedOn |
|
||||
|--------------|---------|--------|-----------|
|
||||
| Normal body | `a` | "Normal" | — |
|
||||
| Default paragraph font | `a0` | "Default Paragraph Font" | — |
|
||||
| Heading 1 (章标题) | `1` | "heading 1" | `a` |
|
||||
| Heading 2 (节标题) | `2` | "heading 2" | `a` |
|
||||
| Heading 3 (小节标题) | `3` | "heading 3" | `a` |
|
||||
| TOC 1 | `11` | "toc 1" | `a` |
|
||||
| TOC 2 | `21` | "toc 2" | `a` |
|
||||
| TOC 3 | `31` | "toc 3" | `a` |
|
||||
| Header | `a3` | "header" | `a` |
|
||||
| Footer | `a4` | "footer" | `a` |
|
||||
| Table of Contents heading | `10` | "TOC Heading" | `1` |
|
||||
|
||||
### Pattern B: English IDs (less common, usually from international templates)
|
||||
Standard Heading1/Heading2/Heading3/Normal — these follow the Western pattern.
|
||||
|
||||
### Pattern C: Mixed (some Chinese, some English)
|
||||
Some templates define custom styles with Chinese names:
|
||||
| Style Purpose | styleId | w:name |
|
||||
|--------------|---------|--------|
|
||||
| 论文标题 | `lunwenbiaoti` | "论文标题" |
|
||||
| 章标题 | `zhangbiaoti` | "章标题" |
|
||||
| 正文 | `zhengwen` | "正文" |
|
||||
|
||||
### How to Identify Which Pattern
|
||||
|
||||
```bash
|
||||
# Extract all styleIds from the template
|
||||
$CLI analyze --input template.docx --styles-only
|
||||
|
||||
# Or manually:
|
||||
# unzip template.docx word/styles.xml
|
||||
# Search for w:styleId= in the extracted file
|
||||
```
|
||||
|
||||
Look at the first few styleIds. If you see `1`, `2`, `3`, `a`, `a0` → Pattern A.
|
||||
If you see `Heading1`, `Normal` → Pattern B.
|
||||
|
||||
## Standard Thesis Structure
|
||||
|
||||
Chinese university theses follow a highly standardized structure:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 封面 (Cover Page) │ ← Usually 1-2 pages
|
||||
│ - 校名、校徽 │
|
||||
│ - 论文题目 (title) │
|
||||
│ - 作者、导师、院系、日期 │
|
||||
├─────────────────────────────────────┤
|
||||
│ 学术诚信承诺书 / 独创性声明 │ ← 1 page
|
||||
│ (Academic Integrity Declaration) │
|
||||
├─────────────────────────────────────┤
|
||||
│ 中文摘要 (Chinese Abstract) │ ← 1-2 pages
|
||||
│ - "摘 要" heading │
|
||||
│ - Abstract body │
|
||||
│ - "关键词:" line │
|
||||
├─────────────────────────────────────┤
|
||||
│ 英文摘要 (English Abstract) │ ← 1-2 pages
|
||||
│ - "ABSTRACT" heading │
|
||||
│ - Abstract body │
|
||||
│ - "Keywords:" line │
|
||||
├─────────────────────────────────────┤
|
||||
│ 目录 (Table of Contents) │ ← 1-3 pages
|
||||
│ - Often inside SDT block │
|
||||
│ - Static example entries │
|
||||
│ - TOC field code │
|
||||
├─────────────────────────────────────┤
|
||||
│ 正文 (Body) │ ← Main content
|
||||
│ 第1章 绪论 │
|
||||
│ 1.1 研究背景 │
|
||||
│ 1.2 研究目的和意义 │
|
||||
│ 第2章 文献综述 │
|
||||
│ ... │
|
||||
│ 第N章 结论与展望 │
|
||||
├─────────────────────────────────────┤
|
||||
│ 参考文献 (References) │ ← Styled differently
|
||||
├─────────────────────────────────────┤
|
||||
│ 致谢 (Acknowledgments) │ ← Optional
|
||||
├─────────────────────────────────────┤
|
||||
│ 附录 (Appendices) │ ← Optional
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Identifying Zone Boundaries in Templates
|
||||
|
||||
Templates contain EXAMPLE content that must be replaced. Here's how to find the zones:
|
||||
|
||||
### Zone A (Front matter) — KEEP from template
|
||||
- Starts at: paragraph 0
|
||||
- Ends at: the paragraph BEFORE the first chapter heading
|
||||
- Contains: cover, declaration, abstracts, TOC
|
||||
- How to detect end: search for first paragraph with style `1` (or Heading1) containing "第1章" or "绪论"
|
||||
|
||||
### Zone B (Body content) — REPLACE with user content
|
||||
- Starts at: first chapter heading ("第1章...")
|
||||
- Ends at: "参考文献" heading (inclusive) or last body paragraph before acknowledgments
|
||||
- How to detect:
|
||||
```python
|
||||
for i, el in enumerate(body_elements):
|
||||
text = get_text(el)
|
||||
style = get_style(el)
|
||||
if style in ('1', 'Heading1') and ('第1章' in text or '绪论' in text):
|
||||
zone_b_start = i
|
||||
if '参考文献' in text:
|
||||
zone_b_end = i
|
||||
```
|
||||
|
||||
### Zone C (Back matter) — KEEP from template (or remove)
|
||||
- Starts after: 参考文献
|
||||
- Contains: 致谢, 附录, final sectPr
|
||||
|
||||
## Font Expectations in Chinese Thesis Templates
|
||||
|
||||
| Element | Font | Size (字号) | Size (pt) | w:sz |
|
||||
|---------|------|------------|-----------|------|
|
||||
| 论文标题 | 华文中宋 or 黑体 | 二号 or 小二 | 22pt or 18pt | 44 or 36 |
|
||||
| 章标题 (H1) | 黑体 | 三号 | 16pt | 32 |
|
||||
| 节标题 (H2) | 黑体 | 四号 | 14pt | 28 |
|
||||
| 小节标题 (H3) | 黑体 | 小四 | 12pt | 24 |
|
||||
| 正文 | 宋体 | 小四 | 12pt | 24 |
|
||||
| 页眉 | 宋体 | 五号 | 10.5pt | 21 |
|
||||
| 页脚/页码 | 宋体 | 五号 | 10.5pt | 21 |
|
||||
| 表格内容 | 宋体 | 五号 | 10.5pt | 21 |
|
||||
| 参考文献条目 | 宋体 | 五号 | 10.5pt | 21 |
|
||||
|
||||
## RunFonts for CJK Body Text
|
||||
|
||||
```xml
|
||||
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"
|
||||
w:eastAsia="宋体" w:cs="Times New Roman"/>
|
||||
```
|
||||
|
||||
For headings:
|
||||
```xml
|
||||
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"
|
||||
w:eastAsia="黑体" w:cs="Times New Roman"/>
|
||||
```
|
||||
|
||||
IMPORTANT: When cleaning direct formatting, ALWAYS preserve w:eastAsia.
|
||||
Removing it causes Chinese text to fall back to the wrong font.
|
||||
|
||||
## Common Mistakes with Chinese Templates
|
||||
|
||||
1. **Searching for `Heading1`** — Chinese templates use `1`, not `Heading1`
|
||||
2. **Clearing all rFonts** — Must keep eastAsia font declarations
|
||||
3. **Assuming "第1章" is the first paragraph** — It's typically paragraph 100+ after cover/abstract/TOC
|
||||
4. **Ignoring SDT blocks in TOC** — The TOC is wrapped in an SDT, not just field codes
|
||||
5. **Wrong line spacing** — Chinese theses typically use fixed 20pt (line="400") or 22pt (line="440"), not the 28pt used in government documents
|
||||
6. **Missing section breaks** — Each zone (abstract, TOC, body) usually has its own sectPr for different headers/footers
|
||||
|
||||
## Style Mapping Quick Reference
|
||||
|
||||
When source document uses Western IDs and template uses Chinese numeric IDs:
|
||||
|
||||
```json
|
||||
{
|
||||
"Heading1": "1",
|
||||
"Heading2": "2",
|
||||
"Heading3": "3",
|
||||
"Heading4": "3",
|
||||
"Normal": "a",
|
||||
"BodyText": "a",
|
||||
"ListParagraph": "a",
|
||||
"Caption": "a",
|
||||
"TOC1": "11",
|
||||
"TOC2": "21",
|
||||
"TOC3": "31"
|
||||
}
|
||||
```
|
||||
|
||||
When source uses Chinese numeric IDs and template uses Western IDs — reverse the mapping.
|
||||
191
skills/minimax-docx/references/comments_guide.md
Normal file
191
skills/minimax-docx/references/comments_guide.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Comments System Guide (4-File Architecture)
|
||||
|
||||
## Overview
|
||||
|
||||
Word comments require coordination across **four XML files** plus references in `document.xml`, `[Content_Types].xml`, and `document.xml.rels`.
|
||||
|
||||
---
|
||||
|
||||
## The Four Comment Files
|
||||
|
||||
### 1. `word/comments.xml` — Main Comment Content
|
||||
|
||||
Contains the actual comment text:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||
<w:comment w:id="1" w:author="Alice" w:date="2026-03-21T09:00:00Z" w:initials="A">
|
||||
<w:p>
|
||||
<w:pPr><w:pStyle w:val="CommentText" /></w:pPr>
|
||||
<w:r>
|
||||
<w:rPr><w:rStyle w:val="CommentReference" /></w:rPr>
|
||||
<w:annotationRef />
|
||||
</w:r>
|
||||
<w:r>
|
||||
<w:t>This needs clarification.</w:t>
|
||||
</w:r>
|
||||
</w:p>
|
||||
</w:comment>
|
||||
</w:comments>
|
||||
```
|
||||
|
||||
Key attributes: `w:id` (unique integer), `w:author`, `w:date` (ISO 8601), `w:initials`.
|
||||
|
||||
### 2. `word/commentsExtended.xml` — W15 Extensions
|
||||
|
||||
Links comments to paragraphs and tracks resolved status:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<w15:commentsEx xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml">
|
||||
<w15:commentEx w15:paraId="1A2B3C4D" w15:done="0" />
|
||||
</w15:commentsEx>
|
||||
```
|
||||
|
||||
- `w15:paraId` — matches the `w14:paraId` of the comment's paragraph in `comments.xml`
|
||||
- `w15:done` — `"0"` = open, `"1"` = resolved
|
||||
|
||||
### 3. `word/commentsIds.xml` — Persistent ID Mapping
|
||||
|
||||
Provides durable IDs that survive copy/paste across documents:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<w16cid:commentsIds xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid">
|
||||
<w16cid:commentId w16cid:paraId="1A2B3C4D" w16cid:durableId="12345678" />
|
||||
</w16cid:commentsIds>
|
||||
```
|
||||
|
||||
- `w16cid:paraId` — same as `w15:paraId`
|
||||
- `w16cid:durableId` — globally unique identifier (8-digit hex)
|
||||
|
||||
### 4. `word/commentsExtensible.xml` — W16 Extensions
|
||||
|
||||
Modern comment extensions (used in newer Word versions):
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<w16cex:commentsExtensible xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex">
|
||||
<w16cex:commentExtensible w16cex:durableId="12345678" w16cex:dateUtc="2026-03-21T09:00:00Z" />
|
||||
</w16cex:commentsExtensible>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Document.xml References
|
||||
|
||||
Comments are anchored in document content using three elements:
|
||||
|
||||
```xml
|
||||
<w:p>
|
||||
<w:commentRangeStart w:id="1" />
|
||||
<w:r><w:t>This text has a comment.</w:t></w:r>
|
||||
<w:commentRangeEnd w:id="1" />
|
||||
<w:r>
|
||||
<w:rPr><w:rStyle w:val="CommentReference" /></w:rPr>
|
||||
<w:commentReference w:id="1" />
|
||||
</w:r>
|
||||
</w:p>
|
||||
```
|
||||
|
||||
- `w:commentRangeStart` — marks where the commented text begins
|
||||
- `w:commentRangeEnd` — marks where the commented text ends
|
||||
- `w:commentReference` — the visible comment marker (superscript number), placed in a run after the range end
|
||||
|
||||
The `w:id` on all three must match the `w:id` in `comments.xml`.
|
||||
|
||||
---
|
||||
|
||||
## Content Types Registration
|
||||
|
||||
Add to `[Content_Types].xml`:
|
||||
|
||||
```xml
|
||||
<Override PartName="/word/comments.xml"
|
||||
ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml" />
|
||||
<Override PartName="/word/commentsExtended.xml"
|
||||
ContentType="application/vnd.ms-word.commentsExtended+xml" />
|
||||
<Override PartName="/word/commentsIds.xml"
|
||||
ContentType="application/vnd.ms-word.commentsIds+xml" />
|
||||
<Override PartName="/word/commentsExtensible.xml"
|
||||
ContentType="application/vnd.ms-word.commentsExtensible+xml" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Relationship Registration
|
||||
|
||||
Add to `word/_rels/document.xml.rels`:
|
||||
|
||||
```xml
|
||||
<Relationship Id="rId20" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
|
||||
Target="comments.xml" />
|
||||
<Relationship Id="rId21" Type="http://schemas.microsoft.com/office/2011/relationships/commentsExtended"
|
||||
Target="commentsExtended.xml" />
|
||||
<Relationship Id="rId22" Type="http://schemas.microsoft.com/office/2016/09/relationships/commentsIds"
|
||||
Target="commentsIds.xml" />
|
||||
<Relationship Id="rId23" Type="http://schemas.microsoft.com/office/2018/08/relationships/commentsExtensible"
|
||||
Target="commentsExtensible.xml" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step: Adding a New Comment
|
||||
|
||||
1. **Choose a unique comment ID** (scan existing `w:id` values, use max + 1)
|
||||
2. **Generate a paraId** (8-character hex, e.g., `"1A2B3C4D"`) and durableId (8-digit hex)
|
||||
3. **Add to `comments.xml`**: Create `w:comment` element with content
|
||||
4. **Add to `commentsExtended.xml`**: Create `w15:commentEx` with `paraId`, `done="0"`
|
||||
5. **Add to `commentsIds.xml`**: Create `w16cid:commentId` with `paraId` and `durableId`
|
||||
6. **Add to `commentsExtensible.xml`**: Create `w16cex:commentExtensible` with `durableId` and `dateUtc`
|
||||
7. **Add to `document.xml`**: Insert `w:commentRangeStart`, `w:commentRangeEnd`, and `w:commentReference` around target text
|
||||
8. **Verify `[Content_Types].xml`** and `document.xml.rels` have entries for all 4 files
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step: Adding a Reply
|
||||
|
||||
Replies are comments whose paragraph's `w14:paraId` links to a parent comment:
|
||||
|
||||
1. Create a new `w:comment` in `comments.xml` with a new `w:id`
|
||||
2. In `commentsExtended.xml`, add `w15:commentEx` with:
|
||||
- `w15:paraId` = new paragraph ID
|
||||
- `w15:paraIdParent` = the `paraId` of the comment being replied to
|
||||
- `w15:done="0"`
|
||||
3. Add entries in `commentsIds.xml` and `commentsExtensible.xml`
|
||||
4. In `document.xml`, the reply does NOT need its own range markers — it shares the parent's range
|
||||
|
||||
```xml
|
||||
<!-- In commentsExtended.xml -->
|
||||
<w15:commentEx w15:paraId="5E6F7A8B" w15:paraIdParent="1A2B3C4D" w15:done="0" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step: Resolving a Comment
|
||||
|
||||
Set `w15:done="1"` on the comment's `w15:commentEx` entry:
|
||||
|
||||
```xml
|
||||
<!-- Before -->
|
||||
<w15:commentEx w15:paraId="1A2B3C4D" w15:done="0" />
|
||||
|
||||
<!-- After -->
|
||||
<w15:commentEx w15:paraId="1A2B3C4D" w15:done="1" />
|
||||
```
|
||||
|
||||
This marks the comment (and all its replies) as resolved. The comment remains visible but appears grayed out in Word.
|
||||
|
||||
---
|
||||
|
||||
## Minimum Viable Comment
|
||||
|
||||
At minimum, a working comment requires:
|
||||
1. `comments.xml` with the `w:comment` element
|
||||
2. `document.xml` with range markers and reference
|
||||
3. Relationship in `document.xml.rels`
|
||||
4. Content type in `[Content_Types].xml`
|
||||
|
||||
The extended files (`commentsExtended`, `commentsIds`, `commentsExtensible`) are optional but recommended for full compatibility with modern Word.
|
||||
829
skills/minimax-docx/references/design_good_bad_examples.md
Normal file
829
skills/minimax-docx/references/design_good_bad_examples.md
Normal file
@@ -0,0 +1,829 @@
|
||||
# GOOD vs BAD Document Design — Concrete OpenXML Examples
|
||||
|
||||
A side-by-side reference showing common design mistakes and their fixes, with exact OpenXML parameter values. Use this to develop an intuitive sense of what makes a document look professional versus amateur.
|
||||
|
||||
Format: Each comparison shows the **BAD** version first (the mistake), then the **GOOD** version (the fix), with OpenXML markup and a short explanation.
|
||||
|
||||
---
|
||||
|
||||
## 1. Font Size Disasters
|
||||
|
||||
### 1a. No Hierarchy — Everything the Same Size
|
||||
|
||||
**BAD: Body=12pt, H1=12pt bold**
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ INTRODUCTION │ ← 12pt bold... same visual weight
|
||||
│ This is the body text of the │ ← 12pt regular
|
||||
│ report. It discusses findings │
|
||||
│ from the quarterly review. │
|
||||
│ METHODOLOGY │ ← Where does the section start?
|
||||
│ We collected data from three │
|
||||
│ sources across the enterprise. │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
```xml
|
||||
<!-- H1: bold but same size as body — no visual separation -->
|
||||
<w:rPr><w:b/><w:sz w:val="24"/></w:rPr>
|
||||
<!-- Body -->
|
||||
<w:rPr><w:sz w:val="24"/></w:rPr>
|
||||
```
|
||||
|
||||
**GOOD: Modular scale — body=11pt, H3=13pt, H2=16pt, H1=20pt**
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ │
|
||||
│ Introduction │ ← 20pt, clearly a title
|
||||
│ │
|
||||
│ This is the body text of the │ ← 11pt, comfortable reading size
|
||||
│ report. It discusses findings │
|
||||
│ from the quarterly review. │
|
||||
│ │
|
||||
│ Methodology │ ← 20pt, section break is obvious
|
||||
│ │
|
||||
│ We collected data from three │
|
||||
│ sources across the enterprise. │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
```xml
|
||||
<!-- H1: 20pt = w:sz 40 -->
|
||||
<w:rPr><w:rFonts w:ascii="Calibri Light"/><w:sz w:val="40"/></w:rPr>
|
||||
<!-- H2: 16pt = w:sz 32 -->
|
||||
<w:rPr><w:rFonts w:ascii="Calibri Light"/><w:sz w:val="32"/></w:rPr>
|
||||
<!-- H3: 13pt = w:sz 26, bold -->
|
||||
<w:rPr><w:rFonts w:ascii="Calibri"/><w:b/><w:sz w:val="26"/></w:rPr>
|
||||
<!-- Body: 11pt = w:sz 22 -->
|
||||
<w:rPr><w:rFonts w:ascii="Calibri"/><w:sz w:val="22"/></w:rPr>
|
||||
```
|
||||
**Why better:** A clear size progression (ratio ~1.25x per step) lets readers instantly identify structure without reading a word.
|
||||
|
||||
---
|
||||
|
||||
### 1b. Too Much Contrast — Children's Book Look
|
||||
|
||||
**BAD: H1=28pt with body=10pt (ratio 2.8x)**
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ │
|
||||
│ QUARTERLY REPORT │ ← 28pt, dominates the page
|
||||
│ │
|
||||
│ This is body text set very small │ ← 10pt, straining to read
|
||||
│ and the contrast with the title │
|
||||
│ makes it feel like a poster. │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
```xml
|
||||
<w:rPr><w:b/><w:sz w:val="56"/></w:rPr> <!-- 28pt heading -->
|
||||
<w:rPr><w:sz w:val="20"/></w:rPr> <!-- 10pt body -->
|
||||
```
|
||||
|
||||
**GOOD: H1=20pt with body=11pt (ratio ~1.8x)**
|
||||
```xml
|
||||
<w:rPr><w:sz w:val="40"/></w:rPr> <!-- 20pt heading -->
|
||||
<w:rPr><w:sz w:val="22"/></w:rPr> <!-- 11pt body -->
|
||||
```
|
||||
**Why better:** A heading-to-body ratio between 1.5x and 2.0x reads as "structured" rather than "shouting."
|
||||
|
||||
---
|
||||
|
||||
## 2. Spacing Crimes
|
||||
|
||||
### 2a. Wall of Text — No Paragraph or Line Spacing
|
||||
|
||||
**BAD: Single line spacing, 0pt between paragraphs**
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│The findings indicate a strong │
|
||||
│correlation between training hours│
|
||||
│and performance metrics. │
|
||||
│Further analysis revealed that │ ← No gap — where does the new
|
||||
│departments with higher budgets │ paragraph start?
|
||||
│achieved better outcomes in all │
|
||||
│measured categories. │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
```xml
|
||||
<w:pPr>
|
||||
<w:spacing w:line="240" w:lineRule="auto"/> <!-- 1.0 spacing (240/240) -->
|
||||
<w:spacing w:after="0"/> <!-- no paragraph gap -->
|
||||
</w:pPr>
|
||||
```
|
||||
|
||||
**GOOD: 1.15x line spacing, 8pt after each paragraph**
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│The findings indicate a strong │
|
||||
│correlation between training │ ← Slightly more air between lines
|
||||
│hours and performance metrics. │
|
||||
│ │ ← 8pt gap signals new paragraph
|
||||
│Further analysis revealed that │
|
||||
│departments with higher budgets │
|
||||
│achieved better outcomes in all │
|
||||
│measured categories. │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
```xml
|
||||
<w:pPr>
|
||||
<w:spacing w:line="276" w:lineRule="auto"/> <!-- 1.15x (276/240) -->
|
||||
<w:spacing w:after="160"/> <!-- 8pt = 160 twips -->
|
||||
</w:pPr>
|
||||
```
|
||||
**Why better:** Line spacing gives each line room to breathe; paragraph spacing separates ideas without wasting a full blank line.
|
||||
|
||||
---
|
||||
|
||||
### 2b. Floating Headings — Same Space Above and Below
|
||||
|
||||
**BAD: 12pt before and 12pt after heading**
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ ...end of previous section. │
|
||||
│ │ ← 12pt gap
|
||||
│ Section Two │ ← Heading floats in the middle
|
||||
│ │ ← 12pt gap
|
||||
│ Start of section two content. │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
```xml
|
||||
<w:pPr>
|
||||
<w:spacing w:before="240" w:after="240"/> <!-- 12pt both sides -->
|
||||
</w:pPr>
|
||||
```
|
||||
|
||||
**GOOD: 24pt before, 8pt after heading**
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ ...end of previous section. │
|
||||
│ │
|
||||
│ │ ← 24pt gap — clear section break
|
||||
│ Section Two │ ← Heading is close to its content
|
||||
│ │ ← 8pt gap
|
||||
│ Start of section two content. │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
```xml
|
||||
<w:pPr>
|
||||
<w:spacing w:before="480" w:after="160"/> <!-- 24pt before, 8pt after -->
|
||||
</w:pPr>
|
||||
```
|
||||
**Why better:** Proximity principle: a heading belongs to the text that follows it, so more space above and less space below anchors it to its content.
|
||||
|
||||
---
|
||||
|
||||
### 2c. Wasteful Gaps — Huge Spacing Everywhere
|
||||
|
||||
**BAD: 24pt after every paragraph, including body text**
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ First paragraph of text here. │
|
||||
│ │
|
||||
│ │ ← 24pt gap after every paragraph
|
||||
│ │
|
||||
│ Second paragraph of text here. │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ Third paragraph. │ ← Document looks mostly white space
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
```xml
|
||||
<w:spacing w:after="480"/> <!-- 24pt = 480 twips after every paragraph -->
|
||||
```
|
||||
|
||||
**GOOD: Proportional spacing — body=8pt, H2=6pt after, H1=10pt after**
|
||||
```xml
|
||||
<!-- Body paragraph -->
|
||||
<w:spacing w:after="160"/> <!-- 8pt after body -->
|
||||
<!-- H1 -->
|
||||
<w:spacing w:before="480" w:after="200"/> <!-- 24pt before, 10pt after -->
|
||||
<!-- H2 -->
|
||||
<w:spacing w:before="320" w:after="120"/> <!-- 16pt before, 6pt after -->
|
||||
```
|
||||
**Why better:** Spacing should vary by element role, creating a visual rhythm rather than uniform gaps.
|
||||
|
||||
---
|
||||
|
||||
## 3. Margin Mistakes
|
||||
|
||||
### 3a. Cramped Margins — Text Running to the Edge
|
||||
|
||||
**BAD: 0.5in margins all around**
|
||||
```
|
||||
┌────────────────────────────────────────────────┐
|
||||
│Text starts almost at the paper edge and runs │
|
||||
│all the way across making extremely long lines │
|
||||
│that are hard to track from end back to start. │
|
||||
│The eye loses its place on every line return. │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
```xml
|
||||
<w:pgMar w:top="720" w:right="720" w:bottom="720" w:left="720"/>
|
||||
<!-- 720 twips = 0.5in — line length ~7.5in on letter paper -->
|
||||
```
|
||||
|
||||
**GOOD: 1in margins (standard)**
|
||||
```xml
|
||||
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/>
|
||||
<!-- 1440 twips = 1.0in — line length ~6.5in, ideal for 11pt body -->
|
||||
```
|
||||
**Why better:** Optimal line length is 60-75 characters. At 11pt Calibri, 6.5in width achieves roughly 70 characters per line.
|
||||
|
||||
---
|
||||
|
||||
### 3b. Over-Padded Margins — Looks Like the Content is Hiding
|
||||
|
||||
**BAD: 2in margins on a short document**
|
||||
```xml
|
||||
<w:pgMar w:top="2880" w:right="2880" w:bottom="2880" w:left="2880"/>
|
||||
<!-- 2880 twips = 2.0in — only 4.5in of text width, looks padded -->
|
||||
```
|
||||
|
||||
**GOOD: 1in standard, or 1.25in for formal documents**
|
||||
```xml
|
||||
<!-- Standard -->
|
||||
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/>
|
||||
<!-- Formal / bound documents with gutter -->
|
||||
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1800" w:gutter="0"/>
|
||||
<!-- 1800 twips = 1.25in left for binding margin -->
|
||||
```
|
||||
**Why better:** Margins should frame the content, not overwhelm it. 1-1.25in works for virtually all business and academic documents.
|
||||
|
||||
---
|
||||
|
||||
## 4. Table Ugliness
|
||||
|
||||
### 4a. Prison Grid — Full Borders on Every Cell
|
||||
|
||||
**BAD: Every cell with 1pt borders on all four sides**
|
||||
```
|
||||
┌───────┬───────┬───────┬───────┐
|
||||
│ Name │ Dept │ Score │ Grade │
|
||||
├───────┼───────┼───────┼───────┤
|
||||
│ Alice │ Eng │ 92 │ A │
|
||||
├───────┼───────┼───────┼───────┤
|
||||
│ Bob │ Sales │ 85 │ B │
|
||||
├───────┼───────┼───────┼───────┤
|
||||
│ Carol │ Eng │ 78 │ C+ │
|
||||
└───────┴───────┴───────┴───────┘
|
||||
```
|
||||
```xml
|
||||
<w:tcBorders>
|
||||
<w:top w:val="single" w:sz="4" w:color="000000"/>
|
||||
<w:left w:val="single" w:sz="4" w:color="000000"/>
|
||||
<w:bottom w:val="single" w:sz="4" w:color="000000"/>
|
||||
<w:right w:val="single" w:sz="4" w:color="000000"/>
|
||||
</w:tcBorders>
|
||||
```
|
||||
|
||||
**GOOD: Three-line table (三线表) — top thick, header-bottom medium, table-bottom thick**
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ← 1.5pt top border
|
||||
Name Dept Score Grade
|
||||
────────────────────────────────── ← 0.75pt header separator
|
||||
Alice Eng 92 A
|
||||
Bob Sales 85 B
|
||||
Carol Eng 78 C+
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ← 1.5pt bottom border
|
||||
```
|
||||
```xml
|
||||
<!-- Top border of header row cells -->
|
||||
<w:top w:val="single" w:sz="12" w:color="000000"/> <!-- 1.5pt -->
|
||||
<w:left w:val="nil"/><w:right w:val="nil"/>
|
||||
<w:bottom w:val="single" w:sz="6" w:color="000000"/> <!-- 0.75pt -->
|
||||
|
||||
<!-- Data row cells: no left/right/top borders -->
|
||||
<w:top w:val="nil"/><w:left w:val="nil"/><w:right w:val="nil"/>
|
||||
<w:bottom w:val="nil"/>
|
||||
|
||||
<!-- Last row bottom border -->
|
||||
<w:bottom w:val="single" w:sz="12" w:color="000000"/> <!-- 1.5pt -->
|
||||
```
|
||||
**Why better:** Removing inner borders lets the eye scan data freely. Three lines provide structure without visual clutter.
|
||||
|
||||
---
|
||||
|
||||
### 4b. Text Touching Borders — No Cell Padding
|
||||
|
||||
**BAD: Zero cell margins**
|
||||
```
|
||||
┌──────────┬──────────┐
|
||||
│Name │Department│ ← Text cramped against borders
|
||||
├──────────┼──────────┤
|
||||
│Alice │Engineering│
|
||||
└──────────┴──────────┘
|
||||
```
|
||||
```xml
|
||||
<w:tcMar>
|
||||
<w:top w:w="0" w:type="dxa"/>
|
||||
<w:start w:w="0" w:type="dxa"/>
|
||||
<w:bottom w:w="0" w:type="dxa"/>
|
||||
<w:end w:w="0" w:type="dxa"/>
|
||||
</w:tcMar>
|
||||
```
|
||||
|
||||
**GOOD: 0.08in vertical, 0.12in horizontal padding**
|
||||
```xml
|
||||
<w:tcMar>
|
||||
<w:top w:w="115" w:type="dxa"/> <!-- ~0.08in = 115 twips -->
|
||||
<w:start w:w="173" w:type="dxa"/> <!-- ~0.12in = 173 twips -->
|
||||
<w:bottom w:w="115" w:type="dxa"/>
|
||||
<w:end w:w="173" w:type="dxa"/>
|
||||
</w:tcMar>
|
||||
```
|
||||
**Why better:** Padding gives text breathing room inside cells, making every value easier to read.
|
||||
|
||||
---
|
||||
|
||||
### 4c. Invisible Headers — Header Row Same Style as Data
|
||||
|
||||
**BAD: Header row indistinguishable from data**
|
||||
```xml
|
||||
<!-- Header cell run properties — identical to data -->
|
||||
<w:rPr><w:sz w:val="22"/></w:rPr>
|
||||
```
|
||||
|
||||
**GOOD: Bold header text, subtle background fill, bottom border**
|
||||
```xml
|
||||
<!-- Header cell run properties -->
|
||||
<w:rPr><w:b/><w:sz w:val="22"/><w:color w:val="333333"/></w:rPr>
|
||||
|
||||
<!-- Header cell shading -->
|
||||
<w:tcPr>
|
||||
<w:shd w:val="clear" w:color="auto" w:fill="F2F2F2"/> <!-- light gray bg -->
|
||||
<w:tcBorders>
|
||||
<w:bottom w:val="single" w:sz="8" w:color="666666"/> <!-- 1pt separator -->
|
||||
</w:tcBorders>
|
||||
</w:tcPr>
|
||||
|
||||
<!-- Mark row as header (repeats on page break) -->
|
||||
<w:trPr><w:tblHeader/></w:trPr>
|
||||
```
|
||||
**Why better:** Distinct header styling lets readers instantly locate column meanings, especially in long tables that span pages. The `w:tblHeader` element ensures the header row repeats on every page.
|
||||
|
||||
---
|
||||
|
||||
## 5. Font Pairing Failures
|
||||
|
||||
### 5a. Visual Chaos — Too Many Fonts
|
||||
|
||||
**BAD: 4+ fonts in one document**
|
||||
```xml
|
||||
<!-- H1 in Impact -->
|
||||
<w:rPr><w:rFonts w:ascii="Impact"/><w:sz w:val="40"/></w:rPr>
|
||||
<!-- H2 in Georgia -->
|
||||
<w:rPr><w:rFonts w:ascii="Georgia"/><w:sz w:val="32"/></w:rPr>
|
||||
<!-- Body in Verdana -->
|
||||
<w:rPr><w:rFonts w:ascii="Verdana"/><w:sz w:val="22"/></w:rPr>
|
||||
<!-- Captions in Courier New -->
|
||||
<w:rPr><w:rFonts w:ascii="Courier New"/><w:sz w:val="18"/></w:rPr>
|
||||
```
|
||||
|
||||
**GOOD: One font family with weight variation, or two complementary families**
|
||||
```xml
|
||||
<!-- H1: Calibri Light (thin weight of Calibri family) -->
|
||||
<w:rPr><w:rFonts w:ascii="Calibri Light"/><w:sz w:val="40"/></w:rPr>
|
||||
<!-- H2: Calibri Light -->
|
||||
<w:rPr><w:rFonts w:ascii="Calibri Light"/><w:sz w:val="32"/></w:rPr>
|
||||
<!-- Body: Calibri (regular weight) -->
|
||||
<w:rPr><w:rFonts w:ascii="Calibri"/><w:sz w:val="22"/></w:rPr>
|
||||
<!-- Captions: Calibri -->
|
||||
<w:rPr><w:rFonts w:ascii="Calibri"/><w:sz w:val="18"/></w:rPr>
|
||||
```
|
||||
**Why better:** Limiting to one or two font families creates visual coherence. Vary by size and weight, not by font.
|
||||
|
||||
---
|
||||
|
||||
### 5b. Mismatched Personality — Comic Sans Meets Times New Roman
|
||||
|
||||
**BAD:**
|
||||
```xml
|
||||
<w:rPr><w:rFonts w:ascii="Comic Sans MS"/><w:sz w:val="36"/></w:rPr> <!-- heading -->
|
||||
<w:rPr><w:rFonts w:ascii="Times New Roman"/><w:sz w:val="24"/></w:rPr> <!-- body -->
|
||||
```
|
||||
|
||||
**GOOD: Fonts with compatible character**
|
||||
```xml
|
||||
<w:rPr><w:rFonts w:ascii="Calibri Light"/><w:sz w:val="36"/></w:rPr> <!-- heading -->
|
||||
<w:rPr><w:rFonts w:ascii="Calibri"/><w:sz w:val="22"/></w:rPr> <!-- body -->
|
||||
```
|
||||
**Why better:** Paired fonts should share a similar level of formality and geometric character. Comic Sans is playful/informal; Times New Roman is formal/traditional. They clash.
|
||||
|
||||
---
|
||||
|
||||
### 5c. Everything Bold — Nothing Stands Out
|
||||
|
||||
**BAD: Bold on body, headings, captions, everything**
|
||||
```xml
|
||||
<w:rPr><w:b/><w:sz w:val="40"/></w:rPr> <!-- heading: bold -->
|
||||
<w:rPr><w:b/><w:sz w:val="22"/></w:rPr> <!-- body: also bold -->
|
||||
<w:rPr><w:b/><w:sz w:val="18"/></w:rPr> <!-- caption: still bold -->
|
||||
```
|
||||
|
||||
**GOOD: Bold reserved for headings and key terms only**
|
||||
```xml
|
||||
<w:rPr><w:b/><w:sz w:val="40"/></w:rPr> <!-- H1: bold -->
|
||||
<w:rPr><w:sz w:val="32"/></w:rPr> <!-- H2: size alone is enough -->
|
||||
<w:rPr><w:sz w:val="22"/></w:rPr> <!-- body: regular weight -->
|
||||
<w:rPr><w:b/><w:sz w:val="22"/></w:rPr> <!-- key term inline: bold -->
|
||||
<w:rPr><w:sz w:val="18"/></w:rPr> <!-- caption: regular, small -->
|
||||
```
|
||||
**Why better:** When everything is emphasized, nothing is emphasized. Bold should be a signal, not a default.
|
||||
|
||||
---
|
||||
|
||||
## 6. Color Abuse
|
||||
|
||||
### 6a. Rainbow Headings
|
||||
|
||||
**BAD: Each heading level a different bright color**
|
||||
```xml
|
||||
<w:rPr><w:color w:val="FF0000"/><w:sz w:val="40"/></w:rPr> <!-- H1: red -->
|
||||
<w:rPr><w:color w:val="00AA00"/><w:sz w:val="32"/></w:rPr> <!-- H2: green -->
|
||||
<w:rPr><w:color w:val="0000FF"/><w:sz w:val="26"/></w:rPr> <!-- H3: blue -->
|
||||
```
|
||||
|
||||
**GOOD: Single accent color for headings, black or dark gray for body**
|
||||
```xml
|
||||
<!-- All headings use the same muted accent -->
|
||||
<w:rPr><w:color w:val="1F4E79"/><w:sz w:val="40"/></w:rPr> <!-- H1: dark blue -->
|
||||
<w:rPr><w:color w:val="1F4E79"/><w:sz w:val="32"/></w:rPr> <!-- H2: same blue -->
|
||||
<w:rPr><w:color w:val="1F4E79"/><w:sz w:val="26"/></w:rPr> <!-- H3: same blue -->
|
||||
<!-- Body in near-black -->
|
||||
<w:rPr><w:color w:val="333333"/><w:sz w:val="22"/></w:rPr>
|
||||
```
|
||||
**Why better:** A single accent color establishes brand consistency. Multiple bright colors compete for attention and look unprofessional.
|
||||
|
||||
---
|
||||
|
||||
### 6b. Low Contrast — Light Gray on White
|
||||
|
||||
**BAD: #CCCCCC text on white background**
|
||||
```xml
|
||||
<w:rPr><w:color w:val="CCCCCC"/></w:rPr>
|
||||
<!-- Contrast ratio: ~1.6:1 — fails WCAG AA (minimum 4.5:1) -->
|
||||
```
|
||||
|
||||
**GOOD: #333333 text on white**
|
||||
```xml
|
||||
<w:rPr><w:color w:val="333333"/></w:rPr>
|
||||
<!-- Contrast ratio: ~12:1 — passes WCAG AAA -->
|
||||
```
|
||||
**Why better:** Sufficient contrast is not just an accessibility requirement; it makes text physically easier to read for everyone, especially in printed documents.
|
||||
|
||||
---
|
||||
|
||||
### 6c. Bright Body Text
|
||||
|
||||
**BAD: Body text in a saturated color**
|
||||
```xml
|
||||
<w:rPr><w:color w:val="0066FF"/><w:sz w:val="22"/></w:rPr> <!-- blue body text -->
|
||||
```
|
||||
|
||||
**GOOD: Color reserved for headings and inline accents only**
|
||||
```xml
|
||||
<!-- Body: neutral dark -->
|
||||
<w:rPr><w:color w:val="333333"/><w:sz w:val="22"/></w:rPr>
|
||||
<!-- Hyperlink: color is functional here -->
|
||||
<w:rPr><w:color w:val="0563C1"/><w:u w:val="single"/></w:rPr>
|
||||
```
|
||||
**Why better:** Colored body text causes eye fatigue over long reading. Reserve color for elements that need to attract attention (headings, links, warnings).
|
||||
|
||||
---
|
||||
|
||||
## 7. List Formatting Issues
|
||||
|
||||
### 7a. Bullet at the Margin — No Indent
|
||||
|
||||
**BAD: List items start at the left margin**
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│Here is a paragraph of text. │
|
||||
│• First item │ ← Bullet at margin, no indent
|
||||
│• Second item │
|
||||
│• Third item │
|
||||
│Next paragraph continues here. │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
```xml
|
||||
<w:pPr>
|
||||
<w:ind w:left="0" w:hanging="0"/>
|
||||
</w:pPr>
|
||||
```
|
||||
|
||||
**GOOD: 0.25in left indent with hanging indent for the bullet**
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│Here is a paragraph of text. │
|
||||
│ • First item │ ← Indented, clearly a list
|
||||
│ • Second item │
|
||||
│ • Third item │
|
||||
│Next paragraph continues here. │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
```xml
|
||||
<w:pPr>
|
||||
<w:ind w:left="360" w:hanging="360"/> <!-- 0.25in = 360 twips -->
|
||||
<w:numPr>
|
||||
<w:ilvl w:val="0"/>
|
||||
<w:numId w:val="1"/>
|
||||
</w:numPr>
|
||||
</w:pPr>
|
||||
```
|
||||
For nested lists, increment by 360 twips per level:
|
||||
```xml
|
||||
<!-- Level 1 -->
|
||||
<w:ind w:left="720" w:hanging="360"/> <!-- 0.5in left -->
|
||||
<!-- Level 2 -->
|
||||
<w:ind w:left="1080" w:hanging="360"/> <!-- 0.75in left -->
|
||||
```
|
||||
**Why better:** Indentation visually separates lists from body text and makes nesting levels clear.
|
||||
|
||||
---
|
||||
|
||||
### 7b. List Items with Full Paragraph Spacing
|
||||
|
||||
**BAD: List items have the same 8-10pt spacing as body paragraphs**
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ • First item │
|
||||
│ │ ← 10pt gap — looks like separate
|
||||
│ • Second item │ paragraphs, not a list
|
||||
│ │
|
||||
│ • Third item │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
```xml
|
||||
<w:spacing w:after="200"/> <!-- 10pt after each list item -->
|
||||
```
|
||||
|
||||
**GOOD: Tight spacing between list items (2-4pt)**
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ • First item │
|
||||
│ • Second item │ ← 2pt gap — cohesive list
|
||||
│ • Third item │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
```xml
|
||||
<w:spacing w:after="40" w:line="276" w:lineRule="auto"/> <!-- 2pt after -->
|
||||
<!-- Or 4pt: -->
|
||||
<w:spacing w:after="80"/>
|
||||
```
|
||||
**Why better:** Tight spacing groups list items as a single unit, matching how readers expect a list to behave.
|
||||
|
||||
---
|
||||
|
||||
## 8. Header/Footer Problems
|
||||
|
||||
### 8a. Header Text Too Large — Competes with Body
|
||||
|
||||
**BAD: Header in 12pt, same as body**
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ Quarterly Report - Q3 2025 │ ← 12pt header, same as body
|
||||
│──────────────────────────────────│
|
||||
│ Introduction │
|
||||
│ This is the body text... │ ← 12pt body — header distracts
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
```xml
|
||||
<!-- Header paragraph -->
|
||||
<w:rPr><w:sz w:val="24"/></w:rPr> <!-- 12pt, same as body -->
|
||||
```
|
||||
|
||||
**GOOD: Header in 9pt, gray color, subtle**
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ Quarterly Report - Q3 2025 │ ← 9pt, gray — present but quiet
|
||||
│──────────────────────────────────│
|
||||
│ Introduction │
|
||||
│ This is the body text... │ ← Body stands out as primary
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
```xml
|
||||
<!-- Header paragraph -->
|
||||
<w:rPr>
|
||||
<w:sz w:val="18"/> <!-- 9pt -->
|
||||
<w:color w:val="808080"/> <!-- medium gray -->
|
||||
</w:rPr>
|
||||
<w:pPr>
|
||||
<w:pBdr>
|
||||
<w:bottom w:val="single" w:sz="4" w:color="D9D9D9"/> <!-- subtle separator -->
|
||||
</w:pBdr>
|
||||
</w:pPr>
|
||||
```
|
||||
**Why better:** Headers are reference information, not primary content. They should be legible but visually subordinate.
|
||||
|
||||
---
|
||||
|
||||
### 8b. No Page Numbers on a Long Document
|
||||
|
||||
**BAD: 20-page document with no page numbers**
|
||||
```xml
|
||||
<!-- Footer section: empty or missing -->
|
||||
```
|
||||
|
||||
**GOOD: Page numbers in footer, right-aligned or centered**
|
||||
```xml
|
||||
<!-- Footer paragraph with page number field -->
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:jc w:val="center"/>
|
||||
<w:rPr><w:sz w:val="18"/><w:color w:val="808080"/></w:rPr>
|
||||
</w:pPr>
|
||||
<w:r>
|
||||
<w:rPr><w:sz w:val="18"/><w:color w:val="808080"/></w:rPr>
|
||||
<w:fldChar w:fldCharType="begin"/>
|
||||
</w:r>
|
||||
<w:r>
|
||||
<w:instrText> PAGE </w:instrText>
|
||||
</w:r>
|
||||
<w:r>
|
||||
<w:fldChar w:fldCharType="separate"/>
|
||||
</w:r>
|
||||
<w:r>
|
||||
<w:t>1</w:t>
|
||||
</w:r>
|
||||
<w:r>
|
||||
<w:fldChar w:fldCharType="end"/>
|
||||
</w:r>
|
||||
</w:p>
|
||||
```
|
||||
**Why better:** Page numbers are essential for navigation in any document over ~3 pages. Readers need to reference specific pages, and printed documents need an ordering mechanism.
|
||||
|
||||
---
|
||||
|
||||
## 9. CJK-Specific Mistakes
|
||||
|
||||
### 9a. Using Italic for Chinese Emphasis
|
||||
|
||||
**BAD: Italic applied to Chinese text**
|
||||
```xml
|
||||
<w:rPr>
|
||||
<w:i/>
|
||||
<w:rFonts w:eastAsia="SimSun"/>
|
||||
<w:sz w:val="24"/>
|
||||
</w:rPr>
|
||||
```
|
||||
CJK glyphs have no true italic form. The renderer applies a synthetic slant that looks broken and ugly — characters appear to lean awkwardly.
|
||||
|
||||
**GOOD: Use bold or emphasis dots (着重号) for Chinese emphasis**
|
||||
```xml
|
||||
<!-- Option A: Bold emphasis -->
|
||||
<w:rPr>
|
||||
<w:b/>
|
||||
<w:rFonts w:eastAsia="SimHei"/> <!-- Switch to bold-capable font -->
|
||||
<w:sz w:val="24"/>
|
||||
</w:rPr>
|
||||
|
||||
<!-- Option B: Emphasis marks (dots under characters) -->
|
||||
<w:rPr>
|
||||
<w:em w:val="dot"/>
|
||||
<w:rFonts w:eastAsia="SimSun"/>
|
||||
<w:sz w:val="24"/>
|
||||
</w:rPr>
|
||||
```
|
||||
**Why better:** Chinese typography has its own emphasis traditions. Bold and emphasis dots are native CJK conventions; italic is a Latin-script concept that does not translate.
|
||||
|
||||
---
|
||||
|
||||
### 9b. Latin Font for Chinese Characters
|
||||
|
||||
**BAD: Only ASCII font set, no EastAsia font specified**
|
||||
```xml
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Arial"/> <!-- No eastAsia attribute -->
|
||||
<w:sz w:val="24"/>
|
||||
</w:rPr>
|
||||
<!-- Word falls back to a random font. Chinese characters may render
|
||||
with wrong metrics, inconsistent stroke widths, or missing glyphs. -->
|
||||
```
|
||||
|
||||
**GOOD: Explicit EastAsia font alongside ASCII font**
|
||||
```xml
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:eastAsia="Microsoft YaHei"/>
|
||||
<w:sz w:val="22"/>
|
||||
</w:rPr>
|
||||
```
|
||||
For formal/academic Chinese documents:
|
||||
```xml
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"
|
||||
w:eastAsia="SimSun"/>
|
||||
<w:sz w:val="24"/> <!-- 小四 12pt -->
|
||||
</w:rPr>
|
||||
```
|
||||
**Why better:** Setting `w:eastAsia` ensures Chinese characters render in a font designed for CJK glyphs, with correct stroke widths, spacing, and metrics.
|
||||
|
||||
---
|
||||
|
||||
### 9c. English Line Spacing for Dense CJK Text
|
||||
|
||||
**BAD: 1.15x line spacing for Chinese body text**
|
||||
```xml
|
||||
<w:spacing w:line="276" w:lineRule="auto"/> <!-- 1.15x — too tight for CJK -->
|
||||
```
|
||||
CJK characters are taller and denser than Latin letters. At 1.15x, lines of Chinese text feel cramped and hard to read.
|
||||
|
||||
**GOOD: 1.5x line spacing or fixed 28pt for CJK body at 12pt (小四)**
|
||||
```xml
|
||||
<!-- Option A: 1.5x proportional -->
|
||||
<w:spacing w:line="360" w:lineRule="auto"/> <!-- 360/240 = 1.5x -->
|
||||
|
||||
<!-- Option B: Fixed 28pt (standard for 小四/12pt CJK body) -->
|
||||
<w:spacing w:line="560" w:lineRule="exact"/> <!-- 28pt = 560 twips -->
|
||||
```
|
||||
For 公文 (government documents) at 三号/16pt body:
|
||||
```xml
|
||||
<w:spacing w:line="580" w:lineRule="exact"/> <!-- 29pt fixed line spacing -->
|
||||
```
|
||||
**Why better:** CJK characters occupy a full em square with no ascenders/descenders providing natural gaps. Extra line spacing compensates, improving readability of dense text blocks.
|
||||
|
||||
---
|
||||
|
||||
## 10. Overall Document Feel
|
||||
|
||||
### Student Homework vs Professional Document
|
||||
|
||||
**BAD: "Student homework" — every setting is Word's default, no intentional choices**
|
||||
```xml
|
||||
<!-- Default everything: Calibri 11pt, no heading styles, 1.08 spacing -->
|
||||
<w:rPr><w:rFonts w:ascii="Calibri"/><w:sz w:val="22"/></w:rPr>
|
||||
<w:pPr><w:spacing w:after="160" w:line="259" w:lineRule="auto"/></w:pPr>
|
||||
<!-- Headings: just bold body text, no style applied -->
|
||||
<w:rPr><w:b/><w:sz w:val="22"/></w:rPr>
|
||||
<!-- No section breaks, no headers/footers, no page numbers -->
|
||||
<!-- Tables with default full grid borders -->
|
||||
<!-- No intentional color or spacing variations -->
|
||||
```
|
||||
|
||||
**GOOD: Intentional design at every level**
|
||||
```xml
|
||||
<!-- Theme fonts defined -->
|
||||
<w:rFonts w:asciiTheme="minorHAnsi" w:hAnsiTheme="minorHAnsi"/>
|
||||
|
||||
<!-- H1: Calibri Light 20pt, dark blue, generous spacing -->
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="Heading1"/>
|
||||
<w:spacing w:before="480" w:after="200"/>
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri Light"/>
|
||||
<w:color w:val="1F4E79"/>
|
||||
<w:sz w:val="40"/>
|
||||
</w:rPr>
|
||||
|
||||
<!-- H2: Calibri Light 16pt, same blue -->
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="Heading2"/>
|
||||
<w:spacing w:before="320" w:after="120"/>
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri Light"/>
|
||||
<w:color w:val="1F4E79"/>
|
||||
<w:sz w:val="32"/>
|
||||
</w:rPr>
|
||||
|
||||
<!-- Body: Calibri 11pt, dark gray, 1.15 spacing, 8pt after -->
|
||||
<w:pPr>
|
||||
<w:spacing w:after="160" w:line="276" w:lineRule="auto"/>
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri"/>
|
||||
<w:color w:val="333333"/>
|
||||
<w:sz w:val="22"/>
|
||||
</w:rPr>
|
||||
|
||||
<!-- Tables: three-line style, padded cells, repeated headers -->
|
||||
<!-- Headers/footers: 9pt gray with page numbers -->
|
||||
<!-- Margins: 1in all around -->
|
||||
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/>
|
||||
```
|
||||
**Why better:** Professional documents result from deliberate, consistent choices across all design dimensions. Each element reinforces the same visual language. The reader may not consciously notice good typography, but they feel the difference in credibility and readability.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Safe Defaults
|
||||
|
||||
A cheat sheet of values that produce a professional result for most Western business documents:
|
||||
|
||||
| Element | Value | OpenXML |
|
||||
|---------|-------|---------|
|
||||
| Body font | Calibri 11pt | `w:sz="22"` |
|
||||
| H1 | Calibri Light 20pt | `w:sz="40"` |
|
||||
| H2 | Calibri Light 16pt | `w:sz="32"` |
|
||||
| H3 | Calibri 13pt bold | `w:sz="26"`, `w:b` |
|
||||
| Body color | #333333 | `w:color="333333"` |
|
||||
| Heading color | #1F4E79 | `w:color="1F4E79"` |
|
||||
| Line spacing | 1.15x | `w:line="276" w:lineRule="auto"` |
|
||||
| Para spacing after | 8pt | `w:after="160"` |
|
||||
| H1 spacing | 24pt before, 10pt after | `w:before="480" w:after="200"` |
|
||||
| H2 spacing | 16pt before, 6pt after | `w:before="320" w:after="120"` |
|
||||
| Margins | 1in all around | `w:pgMar` all `"1440"` |
|
||||
| Table cell padding | 0.08in / 0.12in | `w:w="115"` / `w:w="173"` |
|
||||
| Header/footer size | 9pt gray | `w:sz="18" w:color="808080"` |
|
||||
| List indent | 0.25in per level | `w:left="360" w:hanging="360"` |
|
||||
| List item spacing | 2pt after | `w:after="40"` |
|
||||
|
||||
For CJK documents, adjust: body font to SimSun/YaHei, line spacing to 1.5x (`w:line="360"`), and set `w:eastAsia` on all `w:rFonts`.
|
||||
819
skills/minimax-docx/references/design_principles.md
Normal file
819
skills/minimax-docx/references/design_principles.md
Normal file
@@ -0,0 +1,819 @@
|
||||
# Design Principles for Document Typography
|
||||
|
||||
WHY certain typographic choices look good -- the perceptual and psychological
|
||||
reasons behind professional document design. Use this to make judgment calls
|
||||
when exact specs are not provided.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [White Space & Breathing Room](#1-white-space--breathing-room)
|
||||
2. [Contrast & Scale](#2-contrast--scale)
|
||||
3. [Proximity & Grouping](#3-proximity--grouping)
|
||||
4. [Alignment & Grid](#4-alignment--grid)
|
||||
5. [Repetition & Consistency](#5-repetition--consistency)
|
||||
6. [Visual Hierarchy & Flow](#6-visual-hierarchy--flow)
|
||||
|
||||
---
|
||||
|
||||
## 1. White Space & Breathing Room
|
||||
|
||||
### Why It Works
|
||||
|
||||
The human eye does not read continuously. It jumps in saccades, fixating on
|
||||
small clusters of words. White space provides landing zones for these fixations
|
||||
and gives the reader's peripheral vision a "frame" that makes each text block
|
||||
feel manageable. When a page is packed to the edges, every glance returns more
|
||||
text than working memory can buffer, triggering fatigue and avoidance.
|
||||
|
||||
Research on content density consistently shows:
|
||||
|
||||
- **60-70% content coverage** feels comfortable and professional.
|
||||
- **80%+** starts to feel dense and bureaucratic.
|
||||
- **90%+** feels oppressive -- the reader unconsciously rushes or skips.
|
||||
- **Below 50%** feels wasteful or pretentious (unless intentional, like poetry).
|
||||
|
||||
Wider margins also carry cultural signals. Academic and luxury documents use
|
||||
generous margins (1.25-1.5 inches). Internal memos and drafts use narrower
|
||||
margins (0.75-1.0 inches). The margin width tells the reader how much care
|
||||
went into the document before they read a single word.
|
||||
|
||||
Line spacing has a direct physiological basis: the eye must track back to the
|
||||
start of the next line after each line break. If lines are too close, the eye
|
||||
"slips" to the wrong line. If too far apart, the eye loses its sense of
|
||||
continuity. The sweet spot is 120-145% of the font size.
|
||||
|
||||
**Rule of thumb: when in doubt, add more space, not less.**
|
||||
|
||||
### Good Example
|
||||
|
||||
```
|
||||
Margins: 1 inch (1440 twips) all sides for business documents.
|
||||
Line spacing: 1.15 (276 twips at 240 twips-per-line = 115%).
|
||||
Paragraph spacing after: 8pt (160 twips) between body paragraphs.
|
||||
```
|
||||
|
||||
```xml
|
||||
<!-- Page margins: 1 inch = 1440 twips on all sides -->
|
||||
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"
|
||||
w:header="720" w:footer="720" w:gutter="0"/>
|
||||
|
||||
<!-- Body paragraph: 1.15 line spacing, 8pt after -->
|
||||
<w:pPr>
|
||||
<w:spacing w:after="160" w:line="276" w:lineRule="auto"/>
|
||||
</w:pPr>
|
||||
```
|
||||
|
||||
This produces a page where content occupies roughly 65% of the area. The
|
||||
reader sees clear top/bottom breathing room, and paragraphs are distinct
|
||||
without feeling disconnected.
|
||||
|
||||
```
|
||||
Page layout (good):
|
||||
+----------------------------------+
|
||||
| 1" margin |
|
||||
| +------------------------+ |
|
||||
| | Heading | |
|
||||
| | | |
|
||||
| | Body text here with | |
|
||||
| | comfortable spacing | |
|
||||
| | between lines. | |
|
||||
| | | | <- visible gap between paragraphs
|
||||
| | Another paragraph of | |
|
||||
| | body text follows. | |
|
||||
| | | |
|
||||
| +------------------------+ |
|
||||
| 1" margin |
|
||||
+----------------------------------+
|
||||
```
|
||||
|
||||
### Bad Example
|
||||
|
||||
```xml
|
||||
<!-- Cramped margins: 0.5 inch = 720 twips -->
|
||||
<w:pgMar w:top="720" w:right="720" w:bottom="720" w:left="720"
|
||||
w:header="360" w:footer="360" w:gutter="0"/>
|
||||
|
||||
<!-- No paragraph spacing, single line spacing -->
|
||||
<w:pPr>
|
||||
<w:spacing w:after="0" w:line="240" w:lineRule="auto"/>
|
||||
</w:pPr>
|
||||
```
|
||||
|
||||
This fills ~85% of the page. Text runs edge-to-edge with no visual rest stops.
|
||||
The reader sees a wall of text.
|
||||
|
||||
```
|
||||
Page layout (bad):
|
||||
+----------------------------------+
|
||||
| Heading |
|
||||
| Body text crammed right up to |
|
||||
| the margins with no spacing |
|
||||
| between lines or paragraphs. |
|
||||
| Another paragraph starts here |
|
||||
| and the reader cannot tell where |
|
||||
| one idea ends and another begins |
|
||||
| because everything blurs into a |
|
||||
| single dense block of text. |
|
||||
+----------------------------------+
|
||||
```
|
||||
|
||||
### Quick Test
|
||||
|
||||
1. Zoom out to 50% in your document viewer. If you cannot see clear "channels"
|
||||
of white between text blocks, the spacing is too tight.
|
||||
2. Print a test page. Hold it at arm's length. The text area should look like
|
||||
a rectangle floating in white, not filling the page.
|
||||
3. Check: is the line spacing value at least 264 (`w:line` for 1.1x) for body
|
||||
text? If it is 240 (single), it is too tight for anything over 10pt.
|
||||
|
||||
---
|
||||
|
||||
## 2. Contrast & Scale
|
||||
|
||||
### Why It Works
|
||||
|
||||
The brain processes visual hierarchy through relative difference, not absolute
|
||||
size. A 20pt heading above 11pt body text creates a clear "this is important"
|
||||
signal. But if every heading is 20pt and every sub-heading is 19pt, the brain
|
||||
cannot distinguish them -- they merge into the same level.
|
||||
|
||||
The key insight is **modular scale**: font sizes that grow by a consistent
|
||||
ratio. This mirrors natural proportions and feels harmonious for the same
|
||||
reason musical intervals do.
|
||||
|
||||
Common scales and their character:
|
||||
|
||||
| Ratio | Name | Character | Example progression (from 11pt) |
|
||||
|-------|----------------|---------------------------------|---------------------------------|
|
||||
| 1.200 | Minor third | Subtle, refined | 11 → 13.2 → 15.8 → 19.0 |
|
||||
| 1.250 | Major third | Balanced, professional | 11 → 13.75 → 17.2 → 21.5 |
|
||||
| 1.333 | Perfect fourth | Strong, authoritative | 11 → 14.7 → 19.5 → 26.0 |
|
||||
| 1.414 | Augmented 4th | Dramatic, presentation-style | 11 → 15.6 → 22.0 → 31.1 |
|
||||
|
||||
For most business documents, 1.25 (major third) works best:
|
||||
|
||||
```
|
||||
Body = 11pt (w:sz="22")
|
||||
H3 = 13pt (w:sz="26") -- 11 * 1.25 ≈ 13.75, round to 13
|
||||
H2 = 16pt (w:sz="32") -- 13 * 1.25 ≈ 16.25, round to 16
|
||||
H1 = 20pt (w:sz="40") -- 16 * 1.25 = 20
|
||||
```
|
||||
|
||||
Beyond size, **weight contrast** creates hierarchy without consuming vertical
|
||||
space. Regular (400) vs Bold (700) is visible at any size. Semi-bold (600) vs
|
||||
Regular is subtle and best avoided unless you also vary size or color.
|
||||
|
||||
**Color contrast** adds a third dimension. Dark blue headings (#1F3864) against
|
||||
softer dark gray body text (#333333) signals "heading" without needing a huge
|
||||
size jump. Pure black (#000000) body text is harsher than necessary on white
|
||||
backgrounds -- #333333 or #2D2D2D reduces glare without losing legibility.
|
||||
|
||||
### Good Example
|
||||
|
||||
```xml
|
||||
<!-- H1: 20pt, bold, dark navy -->
|
||||
<w:rPr>
|
||||
<w:b/>
|
||||
<w:sz w:val="40"/>
|
||||
<w:color w:val="1F3864"/>
|
||||
</w:rPr>
|
||||
|
||||
<!-- H2: 16pt, bold, dark navy -->
|
||||
<w:rPr>
|
||||
<w:b/>
|
||||
<w:sz w:val="32"/>
|
||||
<w:color w:val="1F3864"/>
|
||||
</w:rPr>
|
||||
|
||||
<!-- H3: 13pt, bold, dark navy -->
|
||||
<w:rPr>
|
||||
<w:b/>
|
||||
<w:sz w:val="26"/>
|
||||
<w:color w:val="1F3864"/>
|
||||
</w:rPr>
|
||||
|
||||
<!-- Body: 11pt, regular, dark gray -->
|
||||
<w:rPr>
|
||||
<w:sz w:val="22"/>
|
||||
<w:color w:val="333333"/>
|
||||
</w:rPr>
|
||||
```
|
||||
|
||||
```
|
||||
Visual hierarchy (good):
|
||||
|
||||
[████████████████████] <- H1: 20pt bold navy (clearly dominant)
|
||||
(generous space)
|
||||
[██████████████] <- H2: 16pt bold navy (distinct step down)
|
||||
(moderate space)
|
||||
[████████████] <- H3: 13pt bold navy (smaller but still bold)
|
||||
[░░░░░░░░░░░░░░░░░░░░░░] <- Body: 11pt regular gray
|
||||
[░░░░░░░░░░░░░░░░░░░░░░]
|
||||
[░░░░░░░░░░░░░░░░░░░░░░]
|
||||
```
|
||||
|
||||
Each level is visually distinct from its neighbors. You can identify the
|
||||
hierarchy even in peripheral vision.
|
||||
|
||||
### Bad Example
|
||||
|
||||
```xml
|
||||
<!-- H1: 14pt bold black -->
|
||||
<w:rPr>
|
||||
<w:b/>
|
||||
<w:sz w:val="28"/>
|
||||
<w:color w:val="000000"/>
|
||||
</w:rPr>
|
||||
|
||||
<!-- H2: 13pt bold black -->
|
||||
<w:rPr>
|
||||
<w:b/>
|
||||
<w:sz w:val="26"/>
|
||||
<w:color w:val="000000"/>
|
||||
</w:rPr>
|
||||
|
||||
<!-- H3: 12pt bold black -->
|
||||
<w:rPr>
|
||||
<w:b/>
|
||||
<w:sz w:val="24"/>
|
||||
<w:color w:val="000000"/>
|
||||
</w:rPr>
|
||||
|
||||
<!-- Body: 12pt regular black -->
|
||||
<w:rPr>
|
||||
<w:sz w:val="24"/>
|
||||
<w:color w:val="000000"/>
|
||||
</w:rPr>
|
||||
```
|
||||
|
||||
Problems:
|
||||
- H3 (12pt bold) and body (12pt regular) differ only by weight -- too subtle.
|
||||
- H1 (14pt) to H2 (13pt) is a 1pt step -- invisible at reading distance.
|
||||
- Everything is pure black so color provides no differentiating signal.
|
||||
- The ratio between levels is ~1.07, far too flat.
|
||||
|
||||
### Quick Test
|
||||
|
||||
1. **The squint test**: blur your eyes or step back from the screen. Can you
|
||||
count the number of heading levels? If two levels merge, their contrast
|
||||
is insufficient.
|
||||
2. **Ratio check**: divide each heading size by the next smaller size. If any
|
||||
ratio is below 1.15, the levels will look too similar.
|
||||
3. **Color check**: do headings look distinct from body text when you glance
|
||||
at the page? If everything is the same color, you are relying solely on
|
||||
size/weight, which limits your hierarchy to ~3 effective levels.
|
||||
|
||||
---
|
||||
|
||||
## 3. Proximity & Grouping
|
||||
|
||||
### Why It Works
|
||||
|
||||
The Gestalt principle of proximity: items that are close together are perceived
|
||||
as belonging to the same group. In document typography, this means a heading
|
||||
must be **closer to the content it introduces** than to the content above it.
|
||||
|
||||
If a heading sits equidistant between two paragraphs, it looks orphaned -- the
|
||||
reader's eye does not know if it belongs to the text above or below. The fix
|
||||
is asymmetric spacing: **large space before the heading, small space after**.
|
||||
|
||||
The recommended ratio is 2:1 or 3:1 (space-before : space-after).
|
||||
|
||||
This same principle applies to:
|
||||
- **List items**: spacing between items should be less than spacing between
|
||||
paragraphs. Items in a list are a group and should visually cluster.
|
||||
- **Captions**: a figure caption should be close to its figure, not floating
|
||||
in the middle between the figure and the next paragraph.
|
||||
- **Table titles**: the title sits close above the table, with more space
|
||||
separating the title from preceding text.
|
||||
|
||||
### Good Example
|
||||
|
||||
```xml
|
||||
<!-- H2: 18pt before, 6pt after (3:1 ratio) -->
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="Heading2"/>
|
||||
<w:spacing w:before="360" w:after="120"/>
|
||||
</w:pPr>
|
||||
|
||||
<!-- Body paragraph: 0pt before, 8pt after -->
|
||||
<w:pPr>
|
||||
<w:spacing w:before="0" w:after="160"/>
|
||||
</w:pPr>
|
||||
|
||||
<!-- List item: 0pt before, 2pt after (tight grouping) -->
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="ListParagraph"/>
|
||||
<w:spacing w:before="0" w:after="40"/>
|
||||
</w:pPr>
|
||||
```
|
||||
|
||||
```
|
||||
Proximity (good):
|
||||
|
||||
...end of previous section text.
|
||||
<- 18pt gap (w:before="360")
|
||||
## Section Heading
|
||||
<- 6pt gap (w:after="120")
|
||||
First paragraph of new section
|
||||
continues here with content.
|
||||
<- 8pt gap (w:after="160")
|
||||
Second paragraph follows.
|
||||
|
||||
The heading clearly "belongs to" the text below it.
|
||||
```
|
||||
|
||||
```
|
||||
List grouping (good):
|
||||
|
||||
Consider these factors:
|
||||
- First item <- 2pt gap between items
|
||||
- Second item <- items cluster as a group
|
||||
- Third item
|
||||
<- 8pt gap after list
|
||||
The next paragraph starts here.
|
||||
```
|
||||
|
||||
### Bad Example
|
||||
|
||||
```xml
|
||||
<!-- H2: 12pt before, 12pt after (1:1 ratio -- orphaned heading) -->
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="Heading2"/>
|
||||
<w:spacing w:before="240" w:after="240"/>
|
||||
</w:pPr>
|
||||
|
||||
<!-- List item: same spacing as body (10pt after) -->
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="ListParagraph"/>
|
||||
<w:spacing w:before="0" w:after="200"/>
|
||||
</w:pPr>
|
||||
```
|
||||
|
||||
```
|
||||
Proximity (bad):
|
||||
|
||||
...end of previous section text.
|
||||
<- 12pt gap
|
||||
## Section Heading
|
||||
<- 12pt gap (same!)
|
||||
First paragraph of new section.
|
||||
|
||||
The heading floats between sections. It is unclear what it belongs to.
|
||||
```
|
||||
|
||||
```
|
||||
List grouping (bad):
|
||||
|
||||
Consider these factors:
|
||||
<- 10pt gap
|
||||
- First item
|
||||
<- 10pt gap (same as paragraphs)
|
||||
- Second item
|
||||
<- 10pt gap
|
||||
- Third item
|
||||
<- 10pt gap
|
||||
Next paragraph.
|
||||
|
||||
The list does not feel like a group. Each item looks like a
|
||||
separate paragraph that happens to have a bullet.
|
||||
```
|
||||
|
||||
### Quick Test
|
||||
|
||||
1. **Cover test**: cover the heading text. Looking only at the whitespace,
|
||||
can you tell which block of text the heading belongs to? If the gaps above
|
||||
and below are equal, the answer is "no."
|
||||
2. **Number check**: `w:before` on headings should be at least 2x `w:after`.
|
||||
Common good values: before=360 / after=120, or before=240 / after=80.
|
||||
3. **List check**: `w:after` on list items should be less than half of
|
||||
`w:after` on body paragraphs. If body uses 160, list items should use
|
||||
40-60.
|
||||
|
||||
---
|
||||
|
||||
## 4. Alignment & Grid
|
||||
|
||||
### Why It Works
|
||||
|
||||
Alignment creates invisible lines that the eye follows down the page. When
|
||||
elements share the same left edge, the reader perceives order and intention.
|
||||
When elements are slightly misaligned (off by a few twips), the page looks
|
||||
sloppy even if the reader cannot consciously identify why.
|
||||
|
||||
**Left-align vs Justify:**
|
||||
|
||||
- **Left-aligned** (ragged right) is best for English and other Latin-script
|
||||
languages. The uneven right edge actually helps reading because each line
|
||||
has a unique silhouette, making it easier for the eye to find the next line.
|
||||
Justified text forces uneven word spacing that creates distracting "rivers"
|
||||
of white running vertically through paragraphs.
|
||||
|
||||
- **Justified** is best for CJK text. Chinese, Japanese, and Korean characters
|
||||
are monospaced by design -- each occupies the same cell in an invisible grid.
|
||||
Justification preserves this grid perfectly. Ragged right in CJK text breaks
|
||||
the grid and looks untidy.
|
||||
|
||||
**Indentation rule:** Use first-line indent OR paragraph spacing to separate
|
||||
paragraphs -- never both. They serve the same purpose (marking paragraph
|
||||
boundaries). Using both wastes space and creates visual stutter.
|
||||
|
||||
- Western convention: paragraph spacing (no indent) is more modern.
|
||||
- CJK convention: first-line indent of 2 characters is standard.
|
||||
- Academic convention: first-line indent of 0.5 inch is traditional.
|
||||
|
||||
### Good Example
|
||||
|
||||
```xml
|
||||
<!-- English body: left-aligned, paragraph spacing, no indent -->
|
||||
<w:pPr>
|
||||
<w:jc w:val="left"/>
|
||||
<w:spacing w:after="160" w:line="276" w:lineRule="auto"/>
|
||||
<!-- No w:ind firstLine -->
|
||||
</w:pPr>
|
||||
|
||||
<!-- CJK body: justified, first-line indent 2 chars, no paragraph spacing -->
|
||||
<w:pPr>
|
||||
<w:jc w:val="both"/>
|
||||
<w:spacing w:after="0" w:line="360" w:lineRule="auto"/>
|
||||
<w:ind w:firstLineChars="200"/>
|
||||
</w:pPr>
|
||||
|
||||
<!-- Tab stops creating aligned columns -->
|
||||
<w:pPr>
|
||||
<w:tabs>
|
||||
<w:tab w:val="left" w:pos="2880"/> <!-- 2 inches -->
|
||||
<w:tab w:val="right" w:pos="9360"/> <!-- 6.5 inches (right margin) -->
|
||||
</w:tabs>
|
||||
</w:pPr>
|
||||
```
|
||||
|
||||
```
|
||||
English paragraph separation (good -- spacing, no indent):
|
||||
|
||||
This is the first paragraph with some text
|
||||
that wraps to a second line naturally.
|
||||
|
||||
This is the second paragraph. The gap above
|
||||
clearly marks the boundary.
|
||||
|
||||
|
||||
CJK paragraph separation (good -- indent, no spacing):
|
||||
|
||||
第一段正文内容从这里开始,使用两个字符
|
||||
的首行缩进来标记段落边界。
|
||||
第二段紧跟其后,没有段间距,但首行缩进
|
||||
清晰地标识了新段落的开始。
|
||||
```
|
||||
|
||||
### Bad Example
|
||||
|
||||
```xml
|
||||
<!-- English body: justified (creates word-spacing rivers) -->
|
||||
<w:pPr>
|
||||
<w:jc w:val="both"/>
|
||||
<w:spacing w:after="160" w:line="276" w:lineRule="auto"/>
|
||||
<w:ind w:firstLine="720"/> <!-- BOTH indent AND spacing: redundant -->
|
||||
</w:pPr>
|
||||
|
||||
<!-- CJK body: left-aligned (breaks character grid) -->
|
||||
<w:pPr>
|
||||
<w:jc w:val="left"/>
|
||||
<w:spacing w:after="200" w:line="276" w:lineRule="auto"/>
|
||||
<!-- No indent, using spacing instead -- unidiomatic for CJK -->
|
||||
</w:pPr>
|
||||
```
|
||||
|
||||
Problems:
|
||||
- Justified English text with narrow columns creates uneven word gaps.
|
||||
- Using both first-line indent AND paragraph spacing is redundant.
|
||||
- Left-aligned CJK breaks the character grid that CJK readers expect.
|
||||
- CJK with spacing-based separation looks like translated western layout.
|
||||
|
||||
### Quick Test
|
||||
|
||||
1. **River test**: in justified English text, squint and look for vertical
|
||||
white streaks running through the paragraph. If you see them, switch to
|
||||
left-align or increase the column width.
|
||||
2. **Double signal check**: does the document use BOTH first-line indent AND
|
||||
paragraph spacing? If yes, remove one. Choose indent for CJK/academic,
|
||||
spacing for modern western.
|
||||
3. **Tab alignment**: if you use tabs for columns, do all tab stops across
|
||||
the document use the same positions? Inconsistent tab stops create jagged
|
||||
invisible grid lines.
|
||||
|
||||
---
|
||||
|
||||
## 5. Repetition & Consistency
|
||||
|
||||
### Why It Works
|
||||
|
||||
Consistency is a trust signal. When a reader sees that every H2 looks the same,
|
||||
every table follows the same pattern, and every page number sits in the same
|
||||
spot, they unconsciously trust that the document was crafted with care. A single
|
||||
inconsistency -- one H2 that is 15pt instead of 14pt, one table with different
|
||||
borders -- breaks that trust and makes the reader question the content.
|
||||
|
||||
Consistency also reduces cognitive load. Once the reader learns "bold dark blue
|
||||
= section heading," they stop spending mental effort on identifying structure
|
||||
and focus entirely on content. Every inconsistency forces them to re-evaluate:
|
||||
"Is this a different kind of heading, or did someone just forget to apply the
|
||||
style?"
|
||||
|
||||
The implementation rule is simple: **use named styles, not direct formatting.**
|
||||
If you define Heading2 as a style and apply it everywhere, consistency is
|
||||
automatic. If you manually set font size, bold, and color on each heading
|
||||
individually, inconsistency is inevitable.
|
||||
|
||||
### Good Example
|
||||
|
||||
```xml
|
||||
<!-- Define styles once in styles.xml -->
|
||||
<w:style w:type="paragraph" w:styleId="Heading2">
|
||||
<w:name w:val="heading 2"/>
|
||||
<w:basedOn w:val="Normal"/>
|
||||
<w:next w:val="Normal"/>
|
||||
<w:pPr>
|
||||
<w:keepNext/>
|
||||
<w:keepLines/>
|
||||
<w:spacing w:before="360" w:after="120"/>
|
||||
<w:outlineLvl w:val="1"/>
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:asciiTheme="majorHAnsi" w:hAnsiTheme="majorHAnsi"/>
|
||||
<w:b/>
|
||||
<w:sz w:val="32"/>
|
||||
<w:color w:val="1F3864"/>
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Apply consistently: every H2 references the style -->
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="Heading2"/>
|
||||
<!-- No direct formatting overrides -->
|
||||
</w:pPr>
|
||||
<w:r><w:t>Market Analysis</w:t></w:r>
|
||||
</w:p>
|
||||
```
|
||||
|
||||
When using a table style, define it once and reference it for every table:
|
||||
|
||||
```xml
|
||||
<!-- All tables reference the same style -->
|
||||
<w:tblPr>
|
||||
<w:tblStyle w:val="GridTable4Accent1"/>
|
||||
<w:tblW w:w="0" w:type="auto"/>
|
||||
</w:tblPr>
|
||||
```
|
||||
|
||||
### Bad Example
|
||||
|
||||
```xml
|
||||
<!-- First H2: manually formatted -->
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:spacing w:before="360" w:after="120"/>
|
||||
</w:pPr>
|
||||
<w:r>
|
||||
<w:rPr>
|
||||
<w:b/>
|
||||
<w:sz w:val="32"/>
|
||||
<w:color w:val="1F3864"/>
|
||||
</w:rPr>
|
||||
<w:t>Market Analysis</w:t>
|
||||
</w:r>
|
||||
</w:p>
|
||||
|
||||
<!-- Second H2: slightly different (16pt instead of 16pt? No, 15pt!) -->
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:spacing w:before="240" w:after="160"/> <!-- different spacing! -->
|
||||
</w:pPr>
|
||||
<w:r>
|
||||
<w:rPr>
|
||||
<w:b/>
|
||||
<w:sz w:val="30"/> <!-- 15pt instead of 16pt! -->
|
||||
<w:color w:val="2E74B5"/> <!-- different shade of blue! -->
|
||||
</w:rPr>
|
||||
<w:t>Financial Overview</w:t>
|
||||
</w:r>
|
||||
</w:p>
|
||||
```
|
||||
|
||||
Problems:
|
||||
- No style references -- everything is direct formatting.
|
||||
- Second H2 has different size (30 vs 32), color, and spacing.
|
||||
- If there are 20 headings, each could drift slightly differently.
|
||||
- Changing the design later means editing every heading individually.
|
||||
|
||||
### Quick Test
|
||||
|
||||
1. **Style audit**: does every paragraph reference a `w:pStyle`? If you find
|
||||
paragraphs with only direct formatting and no style, that is a consistency
|
||||
risk.
|
||||
2. **Search for variance**: search the XML for all `w:sz` values used with
|
||||
`w:b` (bold). If you find three different sizes for what should be the same
|
||||
heading level, there is an inconsistency.
|
||||
3. **Table check**: do all tables in the document reference the same
|
||||
`w:tblStyle`? If some tables have manual border definitions while others
|
||||
use a style, the document will look patchy.
|
||||
4. **Page numbers**: check that header/footer content is defined in the
|
||||
default section properties and inherited by all sections, not redefined
|
||||
inconsistently in each section.
|
||||
|
||||
---
|
||||
|
||||
## 6. Visual Hierarchy & Flow
|
||||
|
||||
### Why It Works
|
||||
|
||||
A well-designed document guides the reader's eye in a predictable path:
|
||||
title at the top, subtitle below it, section headings as signposts, body text
|
||||
as the main content, footnotes and captions as supporting details. This flow
|
||||
mirrors reading priority -- the most important information is the most visually
|
||||
prominent.
|
||||
|
||||
Each level in the hierarchy must be **distinguishable from its adjacent
|
||||
levels**. It is not enough for H1 to differ from body text; H1 must also
|
||||
clearly differ from H2, and H2 from H3. If any two adjacent levels are too
|
||||
similar, the hierarchy collapses at that point.
|
||||
|
||||
Effective hierarchy uses **multiple simultaneous signals**:
|
||||
|
||||
| Level | Size | Weight | Color | Spacing above |
|
||||
|----------|-------|---------|---------|---------------|
|
||||
| Title | 26pt | Bold | #1F3864 | 0 (top) |
|
||||
| Subtitle | 15pt | Regular | #4472C4 | 4pt |
|
||||
| H1 | 20pt | Bold | #1F3864 | 24pt |
|
||||
| H2 | 16pt | Bold | #1F3864 | 18pt |
|
||||
| H3 | 13pt | Bold | #1F3864 | 12pt |
|
||||
| Body | 11pt | Regular | #333333 | 0pt |
|
||||
| Caption | 9pt | Italic | #666666 | 4pt |
|
||||
| Footnote | 9pt | Regular | #666666 | 0pt |
|
||||
|
||||
Notice how each level differs from its neighbors on at least two dimensions
|
||||
(size + weight, or size + color, or weight + style). Single-dimension
|
||||
differences are fragile and can be missed.
|
||||
|
||||
**Section breaks** create rhythm in long documents. A page break before each
|
||||
major section (H1) gives the reader a mental reset. Within sections, consistent
|
||||
heading + body patterns create a predictable cadence that makes long documents
|
||||
less intimidating.
|
||||
|
||||
### Good Example
|
||||
|
||||
```xml
|
||||
<!-- Title: large, bold, navy, centered -->
|
||||
<w:style w:type="paragraph" w:styleId="Title">
|
||||
<w:pPr>
|
||||
<w:jc w:val="center"/>
|
||||
<w:spacing w:after="80"/>
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:b/>
|
||||
<w:sz w:val="52"/>
|
||||
<w:color w:val="1F3864"/>
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Subtitle: medium, regular weight, lighter blue, centered -->
|
||||
<w:style w:type="paragraph" w:styleId="Subtitle">
|
||||
<w:pPr>
|
||||
<w:jc w:val="center"/>
|
||||
<w:spacing w:after="320"/>
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:sz w:val="30"/>
|
||||
<w:color w:val="4472C4"/>
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- H1: page break before, large bold navy -->
|
||||
<w:style w:type="paragraph" w:styleId="Heading1">
|
||||
<w:pPr>
|
||||
<w:pageBreakBefore/>
|
||||
<w:keepNext/>
|
||||
<w:keepLines/>
|
||||
<w:spacing w:before="480" w:after="160"/>
|
||||
<w:outlineLvl w:val="0"/>
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:b/>
|
||||
<w:sz w:val="40"/>
|
||||
<w:color w:val="1F3864"/>
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Caption: small, italic, gray -->
|
||||
<w:style w:type="paragraph" w:styleId="Caption">
|
||||
<w:pPr>
|
||||
<w:spacing w:before="80" w:after="200"/>
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:i/>
|
||||
<w:sz w:val="18"/>
|
||||
<w:color w:val="666666"/>
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
```
|
||||
|
||||
```
|
||||
Visual flow (good):
|
||||
|
||||
+----------------------------------+
|
||||
| |
|
||||
| ANNUAL REPORT 2025 | <- Title: 26pt bold navy centered
|
||||
| Acme Corporation | <- Subtitle: 15pt regular blue
|
||||
| |
|
||||
| |
|
||||
+----------------------------------+
|
||||
|
||||
+----------------------------------+
|
||||
| |
|
||||
| 1. Executive Summary | <- H1: 20pt bold navy (page break)
|
||||
| |
|
||||
| Body text introducing the | <- Body: 11pt regular gray
|
||||
| main findings of the year. |
|
||||
| |
|
||||
| 1.1 Revenue Highlights | <- H2: 16pt bold navy
|
||||
| |
|
||||
| Revenue grew by 23% year | <- Body
|
||||
| over year, driven by... |
|
||||
| |
|
||||
| Figure 1: Revenue Growth | <- Caption: 9pt italic gray
|
||||
| |
|
||||
+----------------------------------+
|
||||
|
||||
Each level is immediately identifiable. The eye flows naturally
|
||||
from title -> heading -> body -> caption.
|
||||
```
|
||||
|
||||
### Bad Example
|
||||
|
||||
```xml
|
||||
<!-- All headings same color as body, minimal size difference -->
|
||||
<w:style w:type="paragraph" w:styleId="Heading1">
|
||||
<w:rPr>
|
||||
<w:b/>
|
||||
<w:sz w:val="28"/> <!-- 14pt -- only 3pt above body -->
|
||||
<w:color w:val="000000"/> <!-- same color as body -->
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- Caption same size as body, not italic -->
|
||||
<w:style w:type="paragraph" w:styleId="Caption">
|
||||
<w:rPr>
|
||||
<w:sz w:val="22"/> <!-- same 11pt as body! -->
|
||||
<w:color w:val="000000"/> <!-- same color as body -->
|
||||
</w:rPr>
|
||||
</w:style>
|
||||
|
||||
<!-- No page breaks between major sections -->
|
||||
<!-- H1 has no pageBreakBefore, keepNext, or keepLines -->
|
||||
```
|
||||
|
||||
Problems:
|
||||
- H1 at 14pt is too close to body at 11pt (ratio 1.27 -- acceptable in
|
||||
isolation but with black color matching body, the hierarchy is weak).
|
||||
- Caption is indistinguishable from body text.
|
||||
- No page breaks means major sections bleed into each other with no
|
||||
visual rhythm.
|
||||
- Everything is black, so color provides zero hierarchy signal.
|
||||
|
||||
### Quick Test
|
||||
|
||||
1. **The squint test**: blur your eyes while looking at a full page. You
|
||||
should see 3-4 distinct "weight levels" of gray. If the page looks like
|
||||
one uniform shade, the hierarchy is too flat.
|
||||
2. **The scan test**: flip through pages quickly. Can you identify section
|
||||
boundaries in under one second per page? If yes, the visual hierarchy is
|
||||
working. If pages blur together, you need stronger differentiation at H1.
|
||||
3. **Adjacent level test**: for each heading level, check that it differs
|
||||
from the next level on at least 2 of: size, weight, color, style (italic).
|
||||
Single-dimension differences get lost.
|
||||
4. **Rhythm test**: in a document over 10 pages, do major sections (H1) start
|
||||
on new pages? If not, long documents will feel like an undifferentiated
|
||||
stream. Add `w:pageBreakBefore` to Heading1.
|
||||
|
||||
---
|
||||
|
||||
## Summary: Decision Checklist
|
||||
|
||||
When you are unsure about a typographic choice, run through these checks:
|
||||
|
||||
| Principle | Question | If No... |
|
||||
|-----------|----------|----------|
|
||||
| White Space | Does the page have at least 30% white space? | Increase margins or spacing |
|
||||
| Contrast | Can I count heading levels by squinting? | Increase size ratios (target 1.25x) |
|
||||
| Proximity | Does each heading clearly belong to text below it? | Make space-before > space-after (2:1) |
|
||||
| Alignment | Is English left-aligned and CJK justified? | Switch alignment mode |
|
||||
| Repetition | Do all same-level elements use the same style? | Replace direct formatting with styles |
|
||||
| Hierarchy | Can I see the document structure at arm's length? | Add more differentiation signals |
|
||||
|
||||
**When two principles conflict, prioritize in this order:**
|
||||
|
||||
1. **Readability** (white space, line spacing) -- always wins
|
||||
2. **Hierarchy** (contrast, scale) -- readers must find what they need
|
||||
3. **Consistency** (repetition) -- builds trust
|
||||
4. **Aesthetics** (alignment, grouping) -- the finishing touch
|
||||
308
skills/minimax-docx/references/openxml_element_order.md
Normal file
308
skills/minimax-docx/references/openxml_element_order.md
Normal file
@@ -0,0 +1,308 @@
|
||||
# OpenXML Child Element Ordering Rules
|
||||
|
||||
Element ordering in OpenXML is defined by the XSD schema. Incorrect ordering produces invalid documents that Word may refuse to open or silently repair (potentially losing data).
|
||||
|
||||
> **Key rule**: Properties elements (`*Pr`) must always be the **first child** of their parent.
|
||||
|
||||
---
|
||||
|
||||
## w:document
|
||||
|
||||
```
|
||||
Children in order:
|
||||
1. w:background [0..1] — page background color/fill
|
||||
2. w:body [0..1] — document content container
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## w:body
|
||||
|
||||
```
|
||||
Children in order (repeating group):
|
||||
1. w:p [0..*] — paragraph
|
||||
2. w:tbl [0..*] — table
|
||||
3. w:sdt [0..*] — structured document tag (content control)
|
||||
4. w:sectPr [0..1] — LAST child: final section properties
|
||||
```
|
||||
|
||||
Note: `w:p`, `w:tbl`, and `w:sdt` are interleaved in document order. The only strict rule is that `w:sectPr` must be the **last child** of `w:body`.
|
||||
|
||||
---
|
||||
|
||||
## w:p (Paragraph)
|
||||
|
||||
```
|
||||
Children in order:
|
||||
1. w:pPr [0..1] — paragraph properties (MUST be first)
|
||||
|
||||
Then any mix of (interleaved in document order):
|
||||
- w:r [0..*] — run
|
||||
- w:hyperlink [0..*] — hyperlink wrapper
|
||||
- w:ins [0..*] — tracked insertion
|
||||
- w:del [0..*] — tracked deletion
|
||||
- w:bookmarkStart [0..*] — bookmark anchor start
|
||||
- w:bookmarkEnd [0..*] — bookmark anchor end
|
||||
- w:commentRangeStart [0..*] — comment range start
|
||||
- w:commentRangeEnd [0..*] — comment range end
|
||||
- w:proofErr [0..*] — proofing error marker
|
||||
- w:fldSimple [0..*] — simple field
|
||||
- w:sdt [0..*] — inline content control
|
||||
- w:smartTag [0..*] — smart tag
|
||||
```
|
||||
|
||||
**Practical note**: After `w:pPr`, the remaining children appear in document reading order. Runs, hyperlinks, bookmarks, and comment ranges intermix freely based on their position in the text.
|
||||
|
||||
---
|
||||
|
||||
## w:pPr (Paragraph Properties)
|
||||
|
||||
```
|
||||
Children in order:
|
||||
1. w:pStyle [0..1] — paragraph style reference
|
||||
2. w:keepNext [0..1] — keep with next paragraph
|
||||
3. w:keepLines [0..1] — keep lines together
|
||||
4. w:pageBreakBefore [0..1] — page break before paragraph
|
||||
5. w:framePr [0..1] — text frame properties
|
||||
6. w:widowControl [0..1] — widow/orphan control
|
||||
7. w:numPr [0..1] — numbering properties
|
||||
8. w:suppressLineNumbers [0..1]
|
||||
9. w:pBdr [0..1] — paragraph borders
|
||||
10. w:shd [0..1] — shading
|
||||
11. w:tabs [0..1] — tab stops
|
||||
12. w:suppressAutoHyphens [0..1]
|
||||
13. w:kinsoku [0..1] — CJK kinsoku settings
|
||||
14. w:wordWrap [0..1]
|
||||
15. w:overflowPunct [0..1]
|
||||
16. w:topLinePunct [0..1]
|
||||
17. w:autoSpaceDE [0..1]
|
||||
18. w:autoSpaceDN [0..1]
|
||||
19. w:bidi [0..1] — right-to-left paragraph
|
||||
20. w:adjustRightInd [0..1]
|
||||
21. w:snapToGrid [0..1]
|
||||
22. w:spacing [0..1] — line and paragraph spacing
|
||||
23. w:ind [0..1] — indentation
|
||||
24. w:contextualSpacing [0..1]
|
||||
25. w:mirrorIndents [0..1]
|
||||
26. w:suppressOverlap [0..1]
|
||||
27. w:jc [0..1] — justification (left/center/right/both)
|
||||
28. w:textDirection [0..1]
|
||||
29. w:textAlignment [0..1]
|
||||
30. w:outlineLvl [0..1] — outline level
|
||||
31. w:divId [0..1]
|
||||
32. w:rPr [0..1] — run properties for paragraph mark
|
||||
33. w:sectPr [0..1] — section break (section ends at this paragraph)
|
||||
34. w:pPrChange [0..1] — tracked paragraph property change
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## w:r (Run)
|
||||
|
||||
```
|
||||
Children in order:
|
||||
1. w:rPr [0..1] — run properties (MUST be first)
|
||||
|
||||
Then any of (one per run, typically):
|
||||
- w:t [0..*] — text content
|
||||
- w:br [0..*] — break (line, page, column)
|
||||
- w:tab [0..*] — tab character
|
||||
- w:cr [0..*] — carriage return
|
||||
- w:sym [0..*] — symbol character
|
||||
- w:drawing [0..*] — DrawingML object (images)
|
||||
- w:pict [0..*] — VML picture (legacy)
|
||||
- w:fldChar [0..*] — complex field character
|
||||
- w:instrText [0..*] — field instruction text
|
||||
- w:delText [0..*] — deleted text (inside w:del)
|
||||
- w:footnoteReference [0..*]
|
||||
- w:endnoteReference [0..*]
|
||||
- w:commentReference [0..*]
|
||||
- w:lastRenderedPageBreak [0..*]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## w:rPr (Run Properties)
|
||||
|
||||
```
|
||||
Children in order:
|
||||
1. w:rStyle [0..1] — character style reference
|
||||
2. w:rFonts [0..1] — font specification
|
||||
3. w:b [0..1] — bold
|
||||
4. w:bCs [0..1] — complex script bold
|
||||
5. w:i [0..1] — italic
|
||||
6. w:iCs [0..1] — complex script italic
|
||||
7. w:caps [0..1] — all capitals
|
||||
8. w:smallCaps [0..1] — small capitals
|
||||
9. w:strike [0..1] — strikethrough
|
||||
10. w:dstrike [0..1] — double strikethrough
|
||||
11. w:outline [0..1]
|
||||
12. w:shadow [0..1]
|
||||
13. w:emboss [0..1]
|
||||
14. w:imprint [0..1]
|
||||
15. w:noProof [0..1] — suppress proofing
|
||||
16. w:snapToGrid [0..1]
|
||||
17. w:vanish [0..1] — hidden text
|
||||
18. w:color [0..1] — text color
|
||||
19. w:spacing [0..1] — character spacing
|
||||
20. w:w [0..1] — character width scaling
|
||||
21. w:kern [0..1] — font kerning
|
||||
22. w:position [0..1] — vertical position (raise/lower)
|
||||
23. w:sz [0..1] — font size (half-points)
|
||||
24. w:szCs [0..1] — complex script font size
|
||||
25. w:highlight [0..1] — text highlight color
|
||||
26. w:u [0..1] — underline
|
||||
27. w:effect [0..1] — text effect (animated)
|
||||
28. w:bdr [0..1] — run border
|
||||
29. w:shd [0..1] — run shading
|
||||
30. w:vertAlign [0..1] — superscript/subscript
|
||||
31. w:rtl [0..1] — right-to-left
|
||||
32. w:cs [0..1] — complex script
|
||||
33. w:lang [0..1] — language
|
||||
34. w:rPrChange [0..1] — tracked run property change
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## w:tbl (Table)
|
||||
|
||||
```
|
||||
Children in order:
|
||||
1. w:tblPr [1..1] — table properties (REQUIRED, must be first)
|
||||
2. w:tblGrid [1..1] — column width definitions (REQUIRED)
|
||||
3. w:tr [1..*] — table row(s)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## w:tblPr (Table Properties)
|
||||
|
||||
```
|
||||
Children in order:
|
||||
1. w:tblStyle [0..1] — table style reference
|
||||
2. w:tblpPr [0..1] — table positioning
|
||||
3. w:tblOverlap [0..1]
|
||||
4. w:bidiVisual [0..1] — right-to-left table
|
||||
5. w:tblStyleRowBandSize [0..1]
|
||||
6. w:tblStyleColBandSize [0..1]
|
||||
7. w:tblW [0..1] — preferred table width
|
||||
8. w:jc [0..1] — table alignment
|
||||
9. w:tblCellSpacing [0..1]
|
||||
10. w:tblInd [0..1] — table indent from margin
|
||||
11. w:tblBorders [0..1] — table borders
|
||||
12. w:shd [0..1] — table shading
|
||||
13. w:tblLayout [0..1] — fixed or autofit
|
||||
14. w:tblCellMar [0..1] — default cell margins
|
||||
15. w:tblLook [0..1] — conditional formatting flags
|
||||
16. w:tblCaption [0..1] — accessibility caption
|
||||
17. w:tblDescription [0..1] — accessibility description
|
||||
18. w:tblPrChange [0..1] — tracked table property change
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## w:tr (Table Row)
|
||||
|
||||
```
|
||||
Children in order:
|
||||
1. w:trPr [0..1] — row properties (must be first)
|
||||
2. w:tc [1..*] — table cell(s)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## w:trPr (Table Row Properties)
|
||||
|
||||
```
|
||||
Children in order:
|
||||
1. w:cnfStyle [0..1] — conditional formatting
|
||||
2. w:divId [0..1]
|
||||
3. w:gridBefore [0..1] — grid columns before first cell
|
||||
4. w:gridAfter [0..1] — grid columns after last cell
|
||||
5. w:wBefore [0..1]
|
||||
6. w:wAfter [0..1]
|
||||
7. w:cantSplit [0..1] — don't split row across pages
|
||||
8. w:trHeight [0..1] — row height
|
||||
9. w:tblHeader [0..1] — repeat as header row
|
||||
10. w:tblCellSpacing [0..1]
|
||||
11. w:jc [0..1] — row alignment
|
||||
12. w:hidden [0..1]
|
||||
13. w:ins [0..1] — tracked row insertion
|
||||
14. w:del [0..1] — tracked row deletion
|
||||
15. w:trPrChange [0..1] — tracked row property change
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## w:tc (Table Cell)
|
||||
|
||||
```
|
||||
Children in order:
|
||||
1. w:tcPr [0..1] — cell properties (must be first)
|
||||
2. w:p [1..*] — paragraph(s) — at least one required
|
||||
3. w:tbl [0..*] — nested table(s)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## w:tcPr (Table Cell Properties)
|
||||
|
||||
```
|
||||
Children in order:
|
||||
1. w:cnfStyle [0..1]
|
||||
2. w:tcW [0..1] — cell width
|
||||
3. w:gridSpan [0..1] — horizontal merge (column span)
|
||||
4. w:hMerge [0..1] — legacy horizontal merge
|
||||
5. w:vMerge [0..1] — vertical merge
|
||||
6. w:tcBorders [0..1] — cell borders
|
||||
7. w:shd [0..1] — cell shading
|
||||
8. w:noWrap [0..1]
|
||||
9. w:tcMar [0..1] — cell margins
|
||||
10. w:textDirection [0..1]
|
||||
11. w:tcFitText [0..1]
|
||||
12. w:vAlign [0..1] — vertical alignment
|
||||
13. w:hideMark [0..1]
|
||||
14. w:tcPrChange [0..1] — tracked cell property change
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## w:sectPr (Section Properties)
|
||||
|
||||
```
|
||||
Children in order:
|
||||
1. w:headerReference [0..*] — header references (type: default/first/even)
|
||||
2. w:footerReference [0..*] — footer references
|
||||
3. w:endnotePr [0..1]
|
||||
4. w:footnotePr [0..1]
|
||||
5. w:type [0..1] — section break type (nextPage/continuous/evenPage/oddPage)
|
||||
6. w:pgSz [0..1] — page size
|
||||
7. w:pgMar [0..1] — page margins
|
||||
8. w:paperSrc [0..1]
|
||||
9. w:pgBorders [0..1] — page borders
|
||||
10. w:lnNumType [0..1] — line numbering
|
||||
11. w:pgNumType [0..1] — page numbering
|
||||
12. w:cols [0..1] — column definitions
|
||||
13. w:formProt [0..1]
|
||||
14. w:vAlign [0..1] — vertical alignment of page
|
||||
15. w:noEndnote [0..1]
|
||||
16. w:titlePg [0..1] — different first page header/footer
|
||||
17. w:textDirection [0..1]
|
||||
18. w:bidi [0..1]
|
||||
19. w:rtlGutter [0..1]
|
||||
20. w:docGrid [0..1] — document grid
|
||||
21. w:sectPrChange [0..1] — tracked section property change
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## w:hdr (Header) / w:ftr (Footer)
|
||||
|
||||
```
|
||||
Children (same structure as w:body content):
|
||||
1. w:p [0..*] — paragraph(s)
|
||||
2. w:tbl [0..*] — table(s)
|
||||
3. w:sdt [0..*] — content controls
|
||||
```
|
||||
|
||||
Headers and footers are essentially mini-documents. They follow the same content model as `w:body` but without a final `w:sectPr`.
|
||||
4061
skills/minimax-docx/references/openxml_encyclopedia_part1.md
Normal file
4061
skills/minimax-docx/references/openxml_encyclopedia_part1.md
Normal file
File diff suppressed because it is too large
Load Diff
2820
skills/minimax-docx/references/openxml_encyclopedia_part2.md
Normal file
2820
skills/minimax-docx/references/openxml_encyclopedia_part2.md
Normal file
File diff suppressed because it is too large
Load Diff
3381
skills/minimax-docx/references/openxml_encyclopedia_part3.md
Normal file
3381
skills/minimax-docx/references/openxml_encyclopedia_part3.md
Normal file
File diff suppressed because it is too large
Load Diff
82
skills/minimax-docx/references/openxml_namespaces.md
Normal file
82
skills/minimax-docx/references/openxml_namespaces.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# OpenXML Namespaces, Relationship Types, and Content Types
|
||||
|
||||
## Core Namespaces
|
||||
|
||||
| Prefix | URI | Used In |
|
||||
|--------|-----|---------|
|
||||
| `w` | `http://schemas.openxmlformats.org/wordprocessingml/2006/main` | document.xml, styles.xml, numbering.xml, headers, footers |
|
||||
| `r` | `http://schemas.openxmlformats.org/officeDocument/2006/relationships` | Relationship references (r:id) |
|
||||
| `wp` | `http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing` | Image/drawing placement in document |
|
||||
| `a` | `http://schemas.openxmlformats.org/drawingml/2006/main` | DrawingML core (shapes, images, themes) |
|
||||
| `pic` | `http://schemas.openxmlformats.org/drawingml/2006/picture` | Picture element in DrawingML |
|
||||
| `v` | `urn:schemas-microsoft-com:vml` | VML (legacy shapes, watermarks) |
|
||||
| `o` | `urn:schemas-microsoft-com:office:office` | Office VML extensions |
|
||||
| `m` | `http://schemas.openxmlformats.org/officeDocument/2006/math` | Math equations (OMML) |
|
||||
| `mc` | `http://schemas.openxmlformats.org/markup-compatibility/2006` | Markup compatibility (Ignorable, AlternateContent) |
|
||||
|
||||
## Extended Namespaces
|
||||
|
||||
| Prefix | URI | Purpose |
|
||||
|--------|-----|---------|
|
||||
| `w14` | `http://schemas.microsoft.com/office/word/2010/wordml` | Word 2010 extensions (contentPart, etc.) |
|
||||
| `w15` | `http://schemas.microsoft.com/office/word/2012/wordml` | Word 2013 extensions (commentEx, etc.) |
|
||||
| `w16cid` | `http://schemas.microsoft.com/office/word/2016/wordml/cid` | Comment IDs (durable IDs) |
|
||||
| `w16cex` | `http://schemas.microsoft.com/office/word/2018/wordml/cex` | Comment extensible |
|
||||
| `w16se` | `http://schemas.microsoft.com/office/word/2015/wordml/symex` | Symbol extensions |
|
||||
| `wps` | `http://schemas.microsoft.com/office/word/2010/wordprocessingShape` | WordprocessingML shapes |
|
||||
| `wpc` | `http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas` | Drawing canvas |
|
||||
|
||||
## Relationship Types
|
||||
|
||||
| Relationship | Type URI |
|
||||
|-------------|----------|
|
||||
| Document | `http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument` |
|
||||
| Styles | `http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles` |
|
||||
| Numbering | `http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering` |
|
||||
| Font Table | `http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable` |
|
||||
| Settings | `http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings` |
|
||||
| Theme | `http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme` |
|
||||
| Image | `http://schemas.openxmlformats.org/officeDocument/2006/relationships/image` |
|
||||
| Hyperlink | `http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink` |
|
||||
| Header | `http://schemas.openxmlformats.org/officeDocument/2006/relationships/header` |
|
||||
| Footer | `http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer` |
|
||||
| Comments | `http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments` |
|
||||
| CommentsExtended | `http://schemas.microsoft.com/office/2011/relationships/commentsExtended` |
|
||||
| CommentsIds | `http://schemas.microsoft.com/office/2016/09/relationships/commentsIds` |
|
||||
| CommentsExtensible | `http://schemas.microsoft.com/office/2018/08/relationships/commentsExtensible` |
|
||||
| Footnotes | `http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes` |
|
||||
| Endnotes | `http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes` |
|
||||
| Glossary | `http://schemas.openxmlformats.org/officeDocument/2006/relationships/glossaryDocument` |
|
||||
| Web Settings | `http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings` |
|
||||
|
||||
## Content Types (`[Content_Types].xml`)
|
||||
|
||||
### Default Extensions
|
||||
|
||||
```xml
|
||||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
|
||||
<Default Extension="xml" ContentType="application/xml" />
|
||||
<Default Extension="png" ContentType="image/png" />
|
||||
<Default Extension="jpeg" ContentType="image/jpeg" />
|
||||
<Default Extension="gif" ContentType="image/gif" />
|
||||
<Default Extension="emf" ContentType="image/x-emf" />
|
||||
```
|
||||
|
||||
### Part Overrides
|
||||
|
||||
| Part | Content Type |
|
||||
|------|-------------|
|
||||
| `/word/document.xml` | `application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml` |
|
||||
| `/word/styles.xml` | `application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml` |
|
||||
| `/word/numbering.xml` | `application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml` |
|
||||
| `/word/settings.xml` | `application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml` |
|
||||
| `/word/fontTable.xml` | `application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml` |
|
||||
| `/word/theme/theme1.xml` | `application/vnd.openxmlformats-officedocument.theme+xml` |
|
||||
| `/word/header1.xml` | `application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml` |
|
||||
| `/word/footer1.xml` | `application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml` |
|
||||
| `/word/comments.xml` | `application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml` |
|
||||
| `/word/commentsExtended.xml` | `application/vnd.ms-word.commentsExtended+xml` |
|
||||
| `/word/commentsIds.xml` | `application/vnd.ms-word.commentsIds+xml` |
|
||||
| `/word/commentsExtensible.xml` | `application/vnd.ms-word.commentsExtensible+xml` |
|
||||
| `/word/footnotes.xml` | `application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml` |
|
||||
| `/word/endnotes.xml` | `application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml` |
|
||||
72
skills/minimax-docx/references/openxml_units.md
Normal file
72
skills/minimax-docx/references/openxml_units.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# OpenXML Unit Conversion Quick Reference
|
||||
|
||||
## Master Conversion Table
|
||||
|
||||
| Unit | 1 inch | 1 cm | 1 mm | 1 pt | Description |
|
||||
|------|--------|------|------|------|-------------|
|
||||
| DXA (twips) | 1440 | 567 | 56.7 | 20 | 1/20 of a point. Used for margins, indents, spacing, page size. |
|
||||
| EMU | 914400 | 360000 | 36000 | 12700 | English Metric Unit. Used for images, drawings, shapes. |
|
||||
| Half-points | 144 | 56.7 | 5.67 | 2 | Used for font sizes (`w:sz`, `w:szCs`). |
|
||||
| Points | 72 | 28.35 | 2.835 | 1 | Standard typographic unit. Not used directly in most attributes. |
|
||||
| Eighths of a point | 576 | 226.8 | 22.68 | 8 | Used for `w:spacing` character spacing. |
|
||||
|
||||
## Common Page Sizes
|
||||
|
||||
| Size | Width (DXA) | Height (DXA) | Width (mm) | Height (mm) |
|
||||
|------|-------------|--------------|------------|-------------|
|
||||
| A4 | 11906 | 16838 | 210 | 297 |
|
||||
| Letter | 12240 | 15840 | 215.9 | 279.4 |
|
||||
| Legal | 12240 | 20160 | 215.9 | 355.6 |
|
||||
| A3 | 16838 | 23811 | 297 | 420 |
|
||||
| A5 | 8391 | 11906 | 148 | 210 |
|
||||
|
||||
## Common Margin Values
|
||||
|
||||
| Margin | DXA | Inches | cm |
|
||||
|--------|-----|--------|----|
|
||||
| 0.5 inch | 720 | 0.5 | 1.27 |
|
||||
| 0.75 inch | 1080 | 0.75 | 1.91 |
|
||||
| 1 inch | 1440 | 1.0 | 2.54 |
|
||||
| 1.25 inch | 1800 | 1.25 | 3.18 |
|
||||
| 1.5 inch | 2160 | 1.5 | 3.81 |
|
||||
|
||||
## Font Size Values (`w:sz`)
|
||||
|
||||
| Display Size | w:sz value | Notes |
|
||||
|-------------|-----------|-------|
|
||||
| 8pt | 16 | |
|
||||
| 9pt | 18 | |
|
||||
| 10pt | 20 | |
|
||||
| 10.5pt | 21 | Common CJK body size |
|
||||
| 11pt | 22 | Default Calibri body |
|
||||
| 12pt | 24 | Default TNR body |
|
||||
| 14pt | 28 | Small heading |
|
||||
| 16pt | 32 | |
|
||||
| 18pt | 36 | |
|
||||
| 20pt | 40 | |
|
||||
| 24pt | 48 | |
|
||||
| 28pt | 56 | |
|
||||
| 36pt | 72 | |
|
||||
|
||||
## Line Spacing Values
|
||||
|
||||
Line spacing in `w:spacing` uses the `w:line` attribute in 240ths of a line (when `w:lineRule="auto"`):
|
||||
|
||||
| Spacing | w:line value | w:lineRule |
|
||||
|---------|-------------|-----------|
|
||||
| Single | 240 | auto |
|
||||
| 1.15 (Word default) | 276 | auto |
|
||||
| 1.5 | 360 | auto |
|
||||
| Double | 480 | auto |
|
||||
| Exact 12pt | 240 | exact |
|
||||
| At least 12pt | 240 | atLeast |
|
||||
|
||||
Note: When `lineRule="exact"` or `"atLeast"`, `w:line` is in **twips** (DXA), not 240ths. So `line="240"` with `lineRule="exact"` means exactly 12pt (240/20 = 12pt).
|
||||
|
||||
## Conversion Formulas
|
||||
|
||||
```
|
||||
DXA = inches × 1440 = cm × 567 = pt × 20
|
||||
EMU = inches × 914400 = cm × 360000 = pt × 12700
|
||||
sz = pt × 2 (half-points)
|
||||
```
|
||||
284
skills/minimax-docx/references/scenario_a_create.md
Normal file
284
skills/minimax-docx/references/scenario_a_create.md
Normal file
@@ -0,0 +1,284 @@
|
||||
# Scenario A: Creating a New DOCX from Scratch
|
||||
|
||||
## When to Use
|
||||
|
||||
Use Scenario A when:
|
||||
- The user has no existing file and wants a brand new document
|
||||
- The user provides content (text, tables, images) and wants it assembled into a DOCX
|
||||
- The user specifies a document type (report, letter, memo, academic) or describes a custom layout
|
||||
|
||||
Do NOT use when: the user already has a DOCX they want to modify (→ Scenario B) or wants to restyle an existing document (→ Scenario C).
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Workflow
|
||||
|
||||
### 1. Determine Document Type
|
||||
|
||||
Ask or infer the document type from the user's request:
|
||||
|
||||
| Type | Typical Signals |
|
||||
|------|----------------|
|
||||
| Report | "report", "analysis", "whitepaper", sections with headings |
|
||||
| Letter | "letter", "dear", address block, salutation |
|
||||
| Memo | "memo", "memorandum", To/From/Subject fields |
|
||||
| Academic | "paper", "essay", "thesis", APA/MLA/Chicago mention |
|
||||
| Custom | None of the above, or user specifies exact formatting |
|
||||
|
||||
### 2. Gather Content Requirements
|
||||
|
||||
Collect from the user:
|
||||
- Title and subtitle (if any)
|
||||
- Author / organization
|
||||
- Section structure (headings and nesting)
|
||||
- Body content per section
|
||||
- Tables (headers + rows)
|
||||
- Images (file paths or placeholders)
|
||||
- Special elements: TOC, page numbers, watermark, headers/footers
|
||||
|
||||
### 3. Select Style Set
|
||||
|
||||
Based on document type, load the matching styles XML asset:
|
||||
- Report → `assets/styles/default_styles.xml` or `assets/styles/corporate_styles.xml`
|
||||
- Academic → `assets/styles/academic_styles.xml`
|
||||
- Letter / Memo / Custom → `assets/styles/default_styles.xml` (with overrides)
|
||||
|
||||
### 4. Configure Page Setup
|
||||
|
||||
Set `w:sectPr` values based on document type defaults (see below) or user overrides.
|
||||
|
||||
```xml
|
||||
<w:sectPr>
|
||||
<w:pgSz w:w="11906" w:h="16838" /> <!-- A4 -->
|
||||
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"
|
||||
w:header="720" w:footer="720" w:gutter="0" />
|
||||
</w:sectPr>
|
||||
```
|
||||
|
||||
### 5. Build Document Structure
|
||||
|
||||
Assemble `word/document.xml` with:
|
||||
1. `w:body` as root container
|
||||
2. Paragraphs (`w:p`) with heading styles for section titles
|
||||
3. Body paragraphs with `Normal` style
|
||||
4. Tables, images, and other elements as needed
|
||||
5. Final `w:sectPr` as last child of `w:body`
|
||||
|
||||
### 6. Apply Typography Defaults
|
||||
|
||||
Set document-level defaults in `styles.xml` under `w:docDefaults`:
|
||||
```xml
|
||||
<w:docDefaults>
|
||||
<w:rPrDefault>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:eastAsia="SimSun" w:cs="Arial" />
|
||||
<w:sz w:val="22" /> <!-- 11pt -->
|
||||
<w:szCs w:val="22" />
|
||||
</w:rPr>
|
||||
</w:rPrDefault>
|
||||
<w:pPrDefault>
|
||||
<w:pPr>
|
||||
<w:spacing w:after="160" w:line="259" w:lineRule="auto" />
|
||||
</w:pPr>
|
||||
</w:pPrDefault>
|
||||
</w:docDefaults>
|
||||
```
|
||||
|
||||
### 7. Add Complex Elements
|
||||
|
||||
See the Complex Elements Guide section below.
|
||||
|
||||
### 8. Run Validation Pipeline
|
||||
|
||||
```
|
||||
dotnet run ... validate --xsd wml-subset.xsd
|
||||
dotnet run ... validate --xsd business-rules.xsd # if applying a template
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Document Type Defaults
|
||||
|
||||
### Report
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Body font | Calibri 11pt |
|
||||
| Heading font | Calibri Light |
|
||||
| H1 / H2 / H3 / H4 size | 28pt / 24pt / 18pt / 14pt |
|
||||
| Heading color | #2F5496 (corporate blue) |
|
||||
| Margins | 1 inch (1440 DXA) all sides |
|
||||
| Page size | A4 (11906 × 16838 DXA) |
|
||||
| Line spacing | Single (line="240") |
|
||||
| Paragraph spacing | 0pt before, 8pt after body |
|
||||
|
||||
### Letter
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Font | Calibri 11pt |
|
||||
| Page size | Letter (12240 × 15840 DXA) |
|
||||
| Margins | 1 inch all sides |
|
||||
| Structure | Date → Address → Salutation → Body → Closing → Signature |
|
||||
| Line spacing | Single |
|
||||
|
||||
### Memo
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Font | Arial 11pt |
|
||||
| Page size | Letter |
|
||||
| Margins | 0.75 inch (1080 DXA) |
|
||||
| Header | "MEMO" centered, bold, 16pt |
|
||||
| Fields | To, From, Date, Subject (bold labels, tab-aligned values) |
|
||||
|
||||
### Academic
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Font | Times New Roman 12pt |
|
||||
| Line spacing | Double (line="480") |
|
||||
| Margins | 1 inch all sides |
|
||||
| Page size | Letter |
|
||||
| Headings | Bold, same font, 14/13/12pt for H1/H2/H3 |
|
||||
| First line indent | 0.5 inch (720 DXA) |
|
||||
| Heading color | Black (no color) |
|
||||
|
||||
---
|
||||
|
||||
## Content Configuration JSON Format
|
||||
|
||||
The CLI `create` command accepts a JSON config:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "report",
|
||||
"title": "Quarterly Revenue Analysis",
|
||||
"subtitle": "Q1 2026",
|
||||
"author": "Finance Team",
|
||||
"pageSize": "A4",
|
||||
"margins": { "top": 1440, "right": 1440, "bottom": 1440, "left": 1440 },
|
||||
"sections": [
|
||||
{
|
||||
"heading": "Executive Summary",
|
||||
"level": 1,
|
||||
"content": [
|
||||
{ "type": "paragraph", "text": "Revenue grew 12% year-over-year..." },
|
||||
{
|
||||
"type": "table",
|
||||
"headers": ["Region", "Revenue", "Growth"],
|
||||
"rows": [
|
||||
["North America", "$4.2M", "+15%"],
|
||||
["Europe", "$2.8M", "+8%"],
|
||||
["Asia Pacific", "$1.9M", "+18%"]
|
||||
]
|
||||
},
|
||||
{ "type": "image", "path": "charts/revenue.png", "width": "5in", "alt": "Revenue chart" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"heading": "Detailed Analysis",
|
||||
"level": 1,
|
||||
"content": [
|
||||
{ "type": "paragraph", "text": "Breaking down by product line..." }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Supported content types:
|
||||
- `paragraph` — body text (applies Normal style)
|
||||
- `table` — headers + rows (applies TableGrid style)
|
||||
- `image` — inline image with width/height control
|
||||
- `list` — bulleted or numbered list items
|
||||
- `pageBreak` — forces a page break
|
||||
|
||||
---
|
||||
|
||||
## Complex Elements Guide
|
||||
|
||||
### Table of Contents
|
||||
|
||||
Insert a TOC field code. Word will update the actual entries when the file is opened:
|
||||
|
||||
```xml
|
||||
<w:p>
|
||||
<w:pPr><w:pStyle w:val="TOCHeading" /></w:pPr>
|
||||
<w:r><w:t>Table of Contents</w:t></w:r>
|
||||
</w:p>
|
||||
<w:p>
|
||||
<w:r>
|
||||
<w:fldChar w:fldCharType="begin" />
|
||||
</w:r>
|
||||
<w:r>
|
||||
<w:instrText xml:space="preserve"> TOC \o "1-3" \h \z \u </w:instrText>
|
||||
</w:r>
|
||||
<w:r>
|
||||
<w:fldChar w:fldCharType="separate" />
|
||||
</w:r>
|
||||
<w:r>
|
||||
<w:t>[Table of contents — update to populate]</w:t>
|
||||
</w:r>
|
||||
<w:r>
|
||||
<w:fldChar w:fldCharType="end" />
|
||||
</w:r>
|
||||
</w:p>
|
||||
```
|
||||
|
||||
### Page Numbers in Footer
|
||||
|
||||
Add a footer part (`word/footer1.xml`) and reference it in `w:sectPr`:
|
||||
|
||||
```xml
|
||||
<!-- In footer1.xml -->
|
||||
<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
||||
<w:p>
|
||||
<w:pPr><w:jc w:val="center" /></w:pPr>
|
||||
<w:r>
|
||||
<w:fldChar w:fldCharType="begin" />
|
||||
</w:r>
|
||||
<w:r>
|
||||
<w:instrText>PAGE</w:instrText>
|
||||
</w:r>
|
||||
<w:r>
|
||||
<w:fldChar w:fldCharType="separate" />
|
||||
</w:r>
|
||||
<w:r><w:t>1</w:t></w:r>
|
||||
<w:r>
|
||||
<w:fldChar w:fldCharType="end" />
|
||||
</w:r>
|
||||
</w:p>
|
||||
</w:ftr>
|
||||
|
||||
<!-- In sectPr -->
|
||||
<w:footerReference w:type="default" r:id="rId8" />
|
||||
```
|
||||
|
||||
### Watermark
|
||||
|
||||
Add a header part with a shape behind the text:
|
||||
|
||||
```xml
|
||||
<w:hdr>
|
||||
<w:p>
|
||||
<w:r>
|
||||
<w:pict>
|
||||
<v:shape style="position:absolute;margin-left:0;margin-top:0;width:468pt;height:180pt;
|
||||
z-index:-251657216;mso-position-horizontal:center;
|
||||
mso-position-vertical:center"
|
||||
fillcolor="silver" stroked="f">
|
||||
<v:textpath style="font-family:'Calibri';font-size:1pt" string="DRAFT" />
|
||||
</v:shape>
|
||||
</w:pict>
|
||||
</w:r>
|
||||
</w:p>
|
||||
</w:hdr>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Post-Creation Checklist
|
||||
|
||||
1. **Validate** against `wml-subset.xsd` — all elements in correct order, required attributes present
|
||||
2. **Merge adjacent runs** with identical formatting to keep XML clean
|
||||
3. **Verify relationships** — every `r:id` in document.xml has a matching entry in `document.xml.rels`
|
||||
4. **Check content types** — every part in the package is registered in `[Content_Types].xml`
|
||||
5. **Preview** — open in Word or LibreOffice to visually confirm layout
|
||||
6. **File size** — confirm images are reasonably sized (compress if > 2MB each)
|
||||
295
skills/minimax-docx/references/scenario_b_edit_content.md
Normal file
295
skills/minimax-docx/references/scenario_b_edit_content.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# Scenario B: Editing / Filling Content in Existing DOCX
|
||||
|
||||
## Core Principle
|
||||
|
||||
**"First, do no harm."** When editing an existing document, minimize changes. Touch only what needs to change. Preserve all formatting, styles, relationships, and structure that are not directly involved in the edit.
|
||||
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
- Replacing placeholder text (`{{name}}`, `$DATE$`, `[PLACEHOLDER]`)
|
||||
- Updating specific paragraphs or table cells
|
||||
- Filling in form fields
|
||||
- Adding or removing paragraphs in a known location
|
||||
- Inserting tracked changes for review workflows
|
||||
|
||||
Do NOT use when: the user wants to change the look/style of the entire document (→ Scenario C) or create from scratch (→ Scenario A).
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
1. Preview → CLI: analyze <input.docx>
|
||||
2. Analyze → Understand structure: sections, styles, headings, tables
|
||||
3. Identify → Locate exact edit targets (paragraph index, table index, placeholder text)
|
||||
4. Edit → Apply surgical changes via CLI or direct XML
|
||||
5. Validate → CLI: validate <output.docx>
|
||||
6. Diff → Compare before/after to verify only intended changes were made
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use API vs Direct XML
|
||||
|
||||
### Use CLI Edit Command When:
|
||||
- Replacing placeholder text (e.g., `{{fieldName}}` → actual value)
|
||||
- Filling table data from JSON
|
||||
- Updating document properties (title, author)
|
||||
- Simple text insertions or deletions
|
||||
|
||||
### Use Direct XML Manipulation When:
|
||||
- Text spans multiple runs with different formatting (run-boundary issues)
|
||||
- Adding complex structures (nested tables, multi-image layouts)
|
||||
- Manipulating Track Changes markup
|
||||
- Modifying header/footer content
|
||||
- Adjusting section properties
|
||||
|
||||
---
|
||||
|
||||
## Placeholder Patterns
|
||||
|
||||
The CLI natively supports `{{fieldName}}` placeholders:
|
||||
|
||||
```bash
|
||||
# Replace all {{placeholders}} from a JSON map
|
||||
dotnet run ... edit input.docx --fill-placeholders data.json --output filled.docx
|
||||
```
|
||||
|
||||
Where `data.json`:
|
||||
```json
|
||||
{
|
||||
"companyName": "Acme Corp",
|
||||
"date": "March 21, 2026",
|
||||
"amount": "$15,000.00",
|
||||
"recipientName": "Jane Smith"
|
||||
}
|
||||
```
|
||||
|
||||
Other placeholder formats (`$FIELD$`, `[PLACEHOLDER]`) require text replacement:
|
||||
```bash
|
||||
dotnet run ... edit input.docx --replace "$DATE$" "March 21, 2026" --output updated.docx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Text Replacement Strategies
|
||||
|
||||
### Simple Replacement
|
||||
|
||||
When the entire search text is within a single `w:r` (run):
|
||||
|
||||
```xml
|
||||
<!-- Before -->
|
||||
<w:r>
|
||||
<w:rPr><w:b /></w:rPr>
|
||||
<w:t>{{companyName}}</w:t>
|
||||
</w:r>
|
||||
|
||||
<!-- After — formatting preserved -->
|
||||
<w:r>
|
||||
<w:rPr><w:b /></w:rPr>
|
||||
<w:t>Acme Corp</w:t>
|
||||
</w:r>
|
||||
```
|
||||
|
||||
Direct replacement. The run's `w:rPr` is untouched.
|
||||
|
||||
### Complex Replacement (Split Runs)
|
||||
|
||||
When the search text is split across multiple runs (common when Word applies spell-check or formatting mid-text):
|
||||
|
||||
```xml
|
||||
<!-- "{{companyName}}" split into 3 runs -->
|
||||
<w:r><w:rPr><w:b /></w:rPr><w:t>{{company</w:t></w:r>
|
||||
<w:r><w:rPr><w:b /><w:i /></w:rPr><w:t>Na</w:t></w:r>
|
||||
<w:r><w:rPr><w:b /></w:rPr><w:t>me}}</w:t></w:r>
|
||||
```
|
||||
|
||||
Strategy:
|
||||
1. Concatenate text across runs to find the match
|
||||
2. Place the replacement text in the **first** run (preserving its `w:rPr`)
|
||||
3. Remove the text from subsequent runs (or remove the runs entirely if empty)
|
||||
|
||||
```xml
|
||||
<!-- After -->
|
||||
<w:r><w:rPr><w:b /></w:rPr><w:t>Acme Corp</w:t></w:r>
|
||||
```
|
||||
|
||||
**Rule**: Always preserve the formatting of the first run in the match.
|
||||
|
||||
---
|
||||
|
||||
## Table Editing
|
||||
|
||||
### By Index
|
||||
|
||||
Tables are 0-indexed in document order:
|
||||
|
||||
```bash
|
||||
dotnet run ... edit input.docx --table-index 0 --table-data data.json --output updated.docx
|
||||
```
|
||||
|
||||
### By Header Matching
|
||||
|
||||
Find a table by its header row content:
|
||||
|
||||
```bash
|
||||
dotnet run ... edit input.docx --table-match "Name,Amount,Date" --table-data data.json
|
||||
```
|
||||
|
||||
### Table Data JSON Format
|
||||
|
||||
```json
|
||||
{
|
||||
"rows": [
|
||||
["Alice Johnson", "$5,000", "2026-03-15"],
|
||||
["Bob Smith", "$3,200", "2026-03-18"]
|
||||
],
|
||||
"appendRows": true
|
||||
}
|
||||
```
|
||||
|
||||
- `appendRows: true` — add rows after existing data
|
||||
- `appendRows: false` (default) — replace all data rows (keeps header row)
|
||||
|
||||
### Direct XML Table Editing
|
||||
|
||||
To modify a specific cell, locate it by row/column index:
|
||||
|
||||
```xml
|
||||
<!-- Row 2 (0-indexed), Column 1 -->
|
||||
<w:tr> <!-- tr[2] -->
|
||||
<w:tc>...</w:tc>
|
||||
<w:tc> <!-- tc[1] — target cell -->
|
||||
<w:p>
|
||||
<w:r><w:t>Old Value</w:t></w:r>
|
||||
</w:p>
|
||||
</w:tc>
|
||||
</w:tr>
|
||||
```
|
||||
|
||||
Replace the `w:t` content. Do NOT modify `w:tcPr` (cell properties) or `w:tblPr` (table properties).
|
||||
|
||||
---
|
||||
|
||||
## Track Changes Guidance
|
||||
|
||||
### When to Add Revision Marks
|
||||
- User explicitly requests tracked changes
|
||||
- Document already has tracking enabled (`w:trackChanges` in settings)
|
||||
- Collaborative review workflow
|
||||
|
||||
### When NOT to Add Revision Marks
|
||||
- Form filling / placeholder replacement (these are "completing" the document, not "revising" it)
|
||||
- Direct edits where the user wants a clean result
|
||||
- Batch data filling operations
|
||||
|
||||
### Adding Tracked Changes
|
||||
|
||||
See `references/track_changes_guide.md` for full XML examples.
|
||||
|
||||
Quick reference — inserting text with tracking:
|
||||
```xml
|
||||
<w:ins w:id="1" w:author="MiniMaxAI" w:date="2026-03-21T10:00:00Z">
|
||||
<w:r>
|
||||
<w:t>New text here</w:t>
|
||||
</w:r>
|
||||
</w:ins>
|
||||
```
|
||||
|
||||
Deleting text with tracking:
|
||||
```xml
|
||||
<w:del w:id="2" w:author="MiniMaxAI" w:date="2026-03-21T10:00:00Z">
|
||||
<w:r>
|
||||
<w:delText>Removed text</w:delText> <!-- MUST use delText, not t -->
|
||||
</w:r>
|
||||
</w:del>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### 1. Breaking Run Boundaries
|
||||
|
||||
**Problem**: Replacing text that spans runs by naively modifying individual runs destroys inline formatting.
|
||||
|
||||
**Fix**: Concatenate run text, find match boundaries, consolidate into the first run, remove consumed runs.
|
||||
|
||||
### 2. Hyperlink Content
|
||||
|
||||
**Problem**: Replacing text inside a `w:hyperlink` element without preserving the hyperlink wrapper removes the link.
|
||||
|
||||
```xml
|
||||
<w:hyperlink r:id="rId5">
|
||||
<w:r>
|
||||
<w:rPr><w:rStyle w:val="Hyperlink" /></w:rPr>
|
||||
<w:t>Click here</w:t> <!-- Only replace this text -->
|
||||
</w:r>
|
||||
</w:hyperlink>
|
||||
```
|
||||
|
||||
**Fix**: Only modify the `w:t` inside the hyperlink's run. Never remove or replace the `w:hyperlink` element itself.
|
||||
|
||||
### 3. Tracked Change Context
|
||||
|
||||
**Problem**: Replacing text that is inside a `w:ins` or `w:del` element without understanding the revision context creates invalid markup.
|
||||
|
||||
**Fix**: If the target text is inside a revision mark, either:
|
||||
- Replace within the revision context (preserving the `w:ins`/`w:del` wrapper)
|
||||
- Or delete the old revision and create a new one
|
||||
|
||||
### 4. Style Preservation
|
||||
|
||||
**Problem**: Adding new paragraphs without specifying a style causes them to inherit `Normal`, which may not match the surrounding context.
|
||||
|
||||
**Fix**: When inserting paragraphs, copy the `w:pStyle` from an adjacent paragraph of the same type.
|
||||
|
||||
### 5. Numbering Continuity
|
||||
|
||||
**Problem**: Inserting a new list item breaks numbering sequence.
|
||||
|
||||
**Fix**: Ensure the new paragraph has the same `w:numId` and `w:ilvl` as adjacent list items. If continuing a sequence, set `w:numPr` to match.
|
||||
|
||||
### 6. XML Special Characters
|
||||
|
||||
**Problem**: User content contains `&`, `<`, `>`, `"`, `'` — these must be escaped in XML.
|
||||
|
||||
**Fix**: Always XML-escape user-provided text before inserting into `w:t` elements:
|
||||
- `&` → `&`
|
||||
- `<` → `<`
|
||||
- `>` → `>`
|
||||
- `"` → `"`
|
||||
- `'` → `'`
|
||||
|
||||
### 7. Whitespace Preservation
|
||||
|
||||
**Problem**: Leading/trailing spaces in `w:t` are stripped by XML parsers.
|
||||
|
||||
**Fix**: Add `xml:space="preserve"` attribute:
|
||||
```xml
|
||||
<w:t xml:space="preserve"> text with leading space</w:t>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diff Verification
|
||||
|
||||
After editing, always compare the before and after states:
|
||||
|
||||
```bash
|
||||
# Structural diff — shows only changed elements
|
||||
dotnet run ... diff original.docx modified.docx
|
||||
|
||||
# Text-only diff — shows content changes
|
||||
dotnet run ... diff original.docx modified.docx --text-only
|
||||
```
|
||||
|
||||
Verify:
|
||||
- Only intended text changed
|
||||
- No styles were modified
|
||||
- No relationships were added/removed unexpectedly
|
||||
- Table structure intact (same number of rows/columns unless intentionally changed)
|
||||
- Images and other media unchanged
|
||||
456
skills/minimax-docx/references/scenario_c_apply_template.md
Normal file
456
skills/minimax-docx/references/scenario_c_apply_template.md
Normal file
@@ -0,0 +1,456 @@
|
||||
# Scenario C: Applying Formatting / Templates
|
||||
|
||||
## When to Use
|
||||
|
||||
Use Scenario C when:
|
||||
- The user has an existing document and wants to apply a different visual style
|
||||
- The user wants to rebrand a document (new fonts, colors, heading styles)
|
||||
- The user provides a template DOCX and wants its look applied to a content document
|
||||
- The user wants consistent formatting across multiple documents
|
||||
|
||||
Do NOT use when: the user wants to edit content (→ Scenario B) or create from scratch (→ Scenario A).
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
1. Analyze source → CLI: analyze source.docx (list styles, fonts, structure)
|
||||
2. Analyze template → CLI: analyze template.docx (list styles, fonts, structure)
|
||||
3. Map styles → Create mapping plan (source style → template style)
|
||||
4. Apply template → CLI: apply-template source.docx --template template.docx --output result.docx
|
||||
5. Validate (XSD) → CLI: validate result.docx --xsd wml-subset.xsd
|
||||
6. GATE-CHECK → CLI: validate result.docx --xsd business-rules.xsd ← MUST PASS
|
||||
7. Diff verify → CLI: diff source.docx result.docx --text-only (content must be identical)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What Gets Copied from Template
|
||||
|
||||
| Part | File | Description |
|
||||
|------|------|-------------|
|
||||
| Styles | `word/styles.xml` | All style definitions (paragraph, character, table, numbering) |
|
||||
| Theme | `word/theme/theme1.xml` | Color scheme, font scheme, format scheme |
|
||||
| Numbering | `word/numbering.xml` | List and numbering definitions |
|
||||
| Headers | `word/header*.xml` | Header content and formatting |
|
||||
| Footers | `word/footer*.xml` | Footer content and formatting |
|
||||
| Section props | `w:sectPr` | Margins, page size, orientation, columns |
|
||||
|
||||
## What Does NOT Get Copied
|
||||
|
||||
| Part | Reason |
|
||||
|------|--------|
|
||||
| Document content | Paragraphs, tables, images stay from source |
|
||||
| Comments | Belong to source document's review history |
|
||||
| Tracked changes | Belong to source document's revision history |
|
||||
| Custom XML parts | Application-specific data, not visual |
|
||||
| Document properties | Title, author, dates belong to source |
|
||||
| Glossary document | Template's building blocks are not transferred |
|
||||
|
||||
---
|
||||
|
||||
## Template Structure Analysis (REQUIRED)
|
||||
|
||||
Before choosing Overlay or Base-Replace, you MUST analyze the template's internal structure. This is the #1 cause of failure when skipped.
|
||||
|
||||
### Step 1: Count template paragraphs and identify structural zones
|
||||
|
||||
Run `$CLI analyze --input template.docx` or manually inspect:
|
||||
|
||||
```bash
|
||||
# Quick structure scan
|
||||
scripts/docx_preview.sh template.docx
|
||||
```
|
||||
|
||||
Identify these zones in the template:
|
||||
```
|
||||
Zone A: Front matter (cover page, declaration, abstract, TOC)
|
||||
→ These are KEPT from template, never replaced
|
||||
Zone B: Example/placeholder body content ("第1章 XXX", sample paragraphs)
|
||||
→ This is REPLACED with user's actual content
|
||||
Zone C: Back matter (appendices, acknowledgments, blank pages)
|
||||
→ These are KEPT from template or removed
|
||||
Zone D: Final sectPr
|
||||
→ ALWAYS kept from template
|
||||
```
|
||||
|
||||
### Step 2: Find Zone B boundaries (replacement range)
|
||||
|
||||
Search the template's document.xml for anchor text that marks the start and end of example content:
|
||||
|
||||
**Start anchor patterns** (first paragraph of example body):
|
||||
- "第1章", "第一章", "Chapter 1", "1 Introduction", "绪论"
|
||||
- The first paragraph with a Heading1-equivalent style after TOC
|
||||
|
||||
**End anchor patterns** (last paragraph before back matter):
|
||||
- "参考文献", "References", "致谢", "Acknowledgments"
|
||||
- The last paragraph before appendices or final sectPr
|
||||
|
||||
```python
|
||||
# Pseudocode for finding replacement range
|
||||
for i, element in enumerate(template_body_elements):
|
||||
text = get_text(element)
|
||||
style = get_style(element)
|
||||
if style in heading1_styles and ("第1章" in text or "Chapter 1" in text):
|
||||
replace_start = i
|
||||
if "参考文献" in text or "References" in text:
|
||||
replace_end = i
|
||||
break
|
||||
```
|
||||
|
||||
**CRITICAL**: Verify the range by printing what's inside:
|
||||
```
|
||||
Template elements [0..replace_start-1]: front matter (KEEP)
|
||||
Template elements [replace_start..replace_end]: example content (REPLACE)
|
||||
Template elements [replace_end+1..end]: back matter (KEEP)
|
||||
```
|
||||
|
||||
If replace_start or replace_end cannot be found, DO NOT proceed. Ask the user to identify the replacement boundaries.
|
||||
|
||||
### Step 3: Decide Overlay vs Base-Replace
|
||||
|
||||
Now that you know the structure:
|
||||
|
||||
| Observation | Decision |
|
||||
|-------------|----------|
|
||||
| Template has ≤30 paragraphs, no cover/TOC | **C-1: Overlay** (pure style template) |
|
||||
| Template has >100 paragraphs with cover/TOC/example sections | **C-2: Base-Replace** |
|
||||
| Template paragraph count ≈ user document | **C-1: Overlay** (similar structure) |
|
||||
| Template paragraph count >> user document (e.g., 263 vs 134) | **C-2: Base-Replace** |
|
||||
|
||||
### Step 4: For Base-Replace, execute the replacement
|
||||
|
||||
1. Load template as base (all files)
|
||||
2. Extract user content elements using `list(body)` — NOT `findall('w:p')` (which misses tables)
|
||||
3. Build new body: `template[0:replace_start] + cleaned_user_content + template[replace_end+1:]`
|
||||
4. Apply style mapping to every paragraph
|
||||
5. Clean direct formatting (see rules below)
|
||||
6. Rebuild document.xml, keeping template's namespace declarations
|
||||
7. Merge relationships (images + hyperlinks)
|
||||
8. Write output using template as ZIP base
|
||||
|
||||
---
|
||||
|
||||
## Style Mapping Strategy
|
||||
|
||||
When template style names differ from source style names, a mapping is required. **This step is mandatory** — skipping it is the #1 cause of formatting failures in template application.
|
||||
|
||||
### Step 0: Extract StyleIds from Both Documents (REQUIRED)
|
||||
|
||||
Before any template application, extract and compare styleIds from both documents:
|
||||
|
||||
```bash
|
||||
# Extract all styleIds from source
|
||||
$CLI analyze --input source.docx --styles-only
|
||||
# Output example:
|
||||
# Heading1 (paragraph, basedOn: Normal)
|
||||
# Heading2 (paragraph, basedOn: Normal)
|
||||
# Normal (paragraph)
|
||||
# ListBullet (paragraph, basedOn: Normal)
|
||||
|
||||
# Extract all styleIds from template
|
||||
$CLI analyze --input template.docx --styles-only
|
||||
# Output example:
|
||||
# 1 (paragraph, basedOn: a, name: "heading 1")
|
||||
# 2 (paragraph, basedOn: a, name: "heading 2")
|
||||
# 3 (paragraph, basedOn: a, name: "heading 3")
|
||||
# a (paragraph, name: "Normal")
|
||||
# a0 (character, name: "Default Paragraph Font")
|
||||
```
|
||||
|
||||
**Critical distinction**: `w:styleId` vs `w:name`:
|
||||
```xml
|
||||
<!-- styleId="1" but name="heading 1" -->
|
||||
<w:style w:type="paragraph" w:styleId="1">
|
||||
<w:name w:val="heading 1"/>
|
||||
<w:basedOn w:val="a"/>
|
||||
</w:style>
|
||||
```
|
||||
|
||||
The `w:styleId` attribute is what `<w:pStyle w:val="..."/>` references. The `w:name` attribute is the human-readable display name. **They can be completely different.** Many CJK templates use numeric styleIds (`1`, `2`, `3`, `a`, `a0`) instead of English names.
|
||||
|
||||
### Tier 1: Exact StyleId Match
|
||||
If source uses `Heading1` and template defines `Heading1` as a styleId, map directly. No action needed.
|
||||
|
||||
### Tier 2: Name-Based Match
|
||||
If no exact styleId match, try matching by `w:name` attribute:
|
||||
- Source `Heading1` (name="heading 1") → Template styleId `1` (name="heading 1")
|
||||
- Match is case-insensitive on the name value
|
||||
|
||||
Within the same type, also try matching by:
|
||||
- Built-in style ID (Word's internal ID, e.g., heading 1 = built-in ID 1)
|
||||
- Style type (paragraph → paragraph, character → character, table → table)
|
||||
|
||||
### Tier 3: Manual Mapping
|
||||
For renamed or custom styles, provide an explicit mapping:
|
||||
|
||||
```json
|
||||
{
|
||||
"styleMap": {
|
||||
"Heading1": "1",
|
||||
"Heading2": "2",
|
||||
"Heading3": "3",
|
||||
"Heading4": "3",
|
||||
"Normal": "a",
|
||||
"BodyText": "a",
|
||||
"ListBullet": "a",
|
||||
"CompanyName": "Title",
|
||||
"OldTableStyle": "TableGrid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Common Non-Standard StyleId Patterns
|
||||
|
||||
| Template Origin | StyleId Pattern | Example |
|
||||
|----------------|-----------------|---------|
|
||||
| Chinese Word (default) | Numeric/alphabetic | `1`, `2`, `3`, `a`, `a0` |
|
||||
| English Word (default) | English names | `Heading1`, `Normal`, `Title` |
|
||||
| Google Docs export | Prefixed | `Subtitle`, `NormalWeb` |
|
||||
| WPS Office | Mixed | `1`, `Heading1`, custom names |
|
||||
| Academic templates | Custom | `ThesisHeading1`, `ThesisBody` |
|
||||
|
||||
### Building the Mapping Table
|
||||
|
||||
Follow this algorithm:
|
||||
|
||||
1. **List source styleIds** actually used in `document.xml` (not all defined in `styles.xml`):
|
||||
```python
|
||||
# Pseudocode: find all unique pStyle values in source document.xml
|
||||
used_styles = set()
|
||||
for p in body.iter('w:p'):
|
||||
pStyle = p.find('w:pPr/w:pStyle')
|
||||
if pStyle is not None:
|
||||
used_styles.add(pStyle.get('val'))
|
||||
```
|
||||
|
||||
2. **For each used style**, find the best match in template:
|
||||
- First try: exact styleId match
|
||||
- Second try: match by `w:name` value (case-insensitive)
|
||||
- Third try: match by style purpose (any heading → template's heading style)
|
||||
- Fallback: map to template's default paragraph style (usually `Normal` or `a`)
|
||||
|
||||
3. **Validate the mapping** — every source styleId must map to an existing template styleId:
|
||||
```
|
||||
✓ Heading1 → 1 (name match: "heading 1")
|
||||
✓ Heading2 → 2 (name match: "heading 2")
|
||||
✓ Normal → a (name match: "Normal")
|
||||
✗ CustomCallout → ??? (no match found, will fallback to 'a')
|
||||
```
|
||||
|
||||
4. **Apply the mapping** when copying content — update every `<w:pStyle w:val="..."/>`:
|
||||
```xml
|
||||
<!-- Source -->
|
||||
<w:pPr><w:pStyle w:val="Heading1"/></w:pPr>
|
||||
<!-- After mapping -->
|
||||
<w:pPr><w:pStyle w:val="1"/></w:pPr>
|
||||
```
|
||||
|
||||
### Unmapped Styles
|
||||
Styles in the source document that have no match in the template are logged as warnings:
|
||||
```
|
||||
WARNING: Style 'CustomCallout' has no mapping in template. Content will fall back to 'a' (Normal).
|
||||
```
|
||||
|
||||
The content is preserved; only the style reference is updated to the template's default paragraph style.
|
||||
|
||||
### C-2 BASE-REPLACE: Additional StyleId Considerations
|
||||
|
||||
When using the template as a base document (C-2 strategy), the template's `styles.xml` is already in place. You must:
|
||||
|
||||
1. **Never copy source `styles.xml`** — the template's styles are the authority
|
||||
2. **Map every content paragraph's pStyle** to the template's styleId before insertion
|
||||
3. **Strip direct formatting selectively** (see detailed rules below) — let the template style control appearance
|
||||
4. **Verify table styles** — if source tables use `TableGrid` but template defines it as `a3` or similar, remap `<w:tblStyle>` too
|
||||
5. **Check character styles** — `rPr` inside runs may reference character styles like `Hyperlink` or `Strong` that have different IDs in the template
|
||||
|
||||
### Direct Formatting Cleanup Rules (Detailed)
|
||||
|
||||
When copying content from source to template, apply these rules to EACH paragraph and run:
|
||||
|
||||
**REMOVE from `<w:rPr>`:**
|
||||
- `<w:rFonts w:ascii="..." w:hAnsi="..."/>` — Latin font overrides (EXCEPT: keep `w:eastAsia`)
|
||||
- `<w:sz>`, `<w:szCs>` — font size (let style control)
|
||||
- `<w:color>` — text color
|
||||
- `<w:highlight>` — highlight color
|
||||
- `<w:shd>` — shading
|
||||
- `<w:b>`, `<w:i>` — bold/italic UNLESS the source style requires it (e.g., emphasis)
|
||||
- `<w:u>` — underline
|
||||
- `<w:spacing>` — character spacing
|
||||
|
||||
**KEEP in `<w:rPr>`:**
|
||||
- `<w:rFonts w:eastAsia="宋体"/>` — CJK font declaration (MUST keep, or Chinese text renders wrong)
|
||||
- `<w:rFonts w:eastAsia="华文中宋"/>` — same reason
|
||||
- Anything inside `<w:drawing>` — image references (handle separately via rId remapping)
|
||||
|
||||
**REMOVE from `<w:pPr>`:**
|
||||
- `<w:pBdr>` — paragraph borders
|
||||
- `<w:shd>` — paragraph shading
|
||||
- `<w:spacing>` — line/paragraph spacing (let style control)
|
||||
- `<w:jc>` — justification (let style control)
|
||||
- `<w:tabs>` — custom tab stops
|
||||
- `<w:rPr>` inside pPr — default run formatting for the paragraph
|
||||
|
||||
**KEEP in `<w:pPr>`:**
|
||||
- `<w:pStyle>` — style reference (after mapping to template's styleId)
|
||||
- `<w:sectPr>` — section properties (if intentionally inserting section breaks)
|
||||
- `<w:numPr>` — numbering reference (after mapping numId to template's numbering)
|
||||
|
||||
**Table cells (`<w:tc>`):**
|
||||
Apply the same rPr/pPr cleanup to every paragraph inside every cell. Also:
|
||||
- Keep `<w:tcPr>` structural properties (column span, row span, width)
|
||||
- Remove `<w:tcPr><w:shd>` (cell shading — let table style control)
|
||||
|
||||
---
|
||||
|
||||
## Relationship ID Remapping
|
||||
|
||||
When copying parts (headers, footers, images) from the template into the source package, relationship IDs (`r:id`) may collide.
|
||||
|
||||
**Problem**:
|
||||
- Source has `rId7` → `image1.png`
|
||||
- Template has `rId7` → `header1.xml`
|
||||
- Copying template's `rId7` overwrites source's image reference
|
||||
|
||||
**Solution**:
|
||||
1. Scan source's `document.xml.rels` for all existing `rId` values
|
||||
2. Find the maximum numeric ID (e.g., `rId12`)
|
||||
3. Remap all template relationship IDs starting from `rId13`
|
||||
4. Update all references in copied parts to use new IDs
|
||||
|
||||
```xml
|
||||
<!-- Template original -->
|
||||
<Relationship Id="rId1" Type="...header" Target="header1.xml" />
|
||||
|
||||
<!-- After remapping into source package -->
|
||||
<Relationship Id="rId13" Type="...header" Target="header1.xml" />
|
||||
|
||||
<!-- Update sectPr reference -->
|
||||
<w:headerReference w:type="default" r:id="rId13" />
|
||||
```
|
||||
|
||||
### Hyperlink Relationship Merging
|
||||
|
||||
When the source document contains external hyperlinks (e.g., URLs in references or footnotes), these are stored as relationships in `word/_rels/document.xml.rels`:
|
||||
|
||||
```xml
|
||||
<Relationship Id="rId15" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
|
||||
Target="https://example.com/paper" TargetMode="External"/>
|
||||
```
|
||||
|
||||
The corresponding text in document.xml references this rId:
|
||||
```xml
|
||||
<w:hyperlink r:id="rId15">
|
||||
<w:r><w:t>https://example.com/paper</w:t></w:r>
|
||||
</w:hyperlink>
|
||||
```
|
||||
|
||||
**Merging steps:**
|
||||
1. Scan source document.xml for all `<w:hyperlink r:id="...">` elements
|
||||
2. For each, find the corresponding relationship in source's rels file
|
||||
3. Check if template already has a relationship with the same Target URL
|
||||
- If yes: reuse the existing rId, update the hyperlink reference
|
||||
- If no: assign a new rId (starting from template's max rId + 1), add the relationship to template's rels, update the hyperlink reference
|
||||
4. Also check for hyperlink relationships used in footnotes (`word/_rels/footnotes.xml.rels`) and endnotes
|
||||
|
||||
**Common mistake:** Copying hyperlink paragraphs without merging rels → hyperlinks silently break (clicking does nothing in Word).
|
||||
|
||||
---
|
||||
|
||||
## XSD Gate-Check
|
||||
|
||||
### What It Is
|
||||
|
||||
After template application, the output document **MUST** pass `business-rules.xsd` validation. This is a **hard gate** — if it fails, the document is **NOT deliverable**.
|
||||
|
||||
### What business-rules.xsd Checks
|
||||
|
||||
| Rule | What It Validates |
|
||||
|------|-------------------|
|
||||
| Template styles exist | All styles referenced by content paragraphs are defined in `styles.xml` |
|
||||
| Margins match | Page margins match template specification |
|
||||
| Fonts correct | `w:docDefaults` fonts match template's font scheme |
|
||||
| Heading hierarchy | Heading levels are sequential (no H1 → H3 without H2) |
|
||||
| Required styles present | `Normal`, `Heading1`-`Heading3`, `TableGrid` exist |
|
||||
| Page size | Matches template's declared page size |
|
||||
|
||||
### Handling Failures
|
||||
|
||||
```
|
||||
GATE-CHECK FAILED:
|
||||
- Style 'CustomStyle1' referenced in paragraph 14 but not defined in styles.xml
|
||||
- Margin w:left=1080 does not match template requirement 1440
|
||||
```
|
||||
|
||||
Fix each failure:
|
||||
1. **Missing style**: Add the style definition to `styles.xml`, or remap the paragraph to an existing style
|
||||
2. **Margin mismatch**: Update `w:sectPr` margins to match template
|
||||
3. **Font mismatch**: Update `w:docDefaults` to match template font scheme
|
||||
4. **Heading hierarchy gap**: Insert intermediate heading levels or adjust existing levels
|
||||
|
||||
Re-validate after every fix until gate-check passes.
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### 1. Orphaned Numbering References
|
||||
|
||||
**Problem**: Source document uses `w:numId="5"` in list paragraphs, but after replacing `numbering.xml` with the template's version, numbering ID 5 doesn't exist.
|
||||
|
||||
**Symptom**: Lists appear as plain paragraphs (no bullets/numbers).
|
||||
|
||||
**Fix**:
|
||||
- Map source numbering IDs to template numbering IDs
|
||||
- Update all `w:numId` references in document content
|
||||
- Or merge source numbering definitions into template's `numbering.xml`
|
||||
|
||||
### 2. Missing Theme Colors
|
||||
|
||||
**Problem**: Source document's styles reference theme colors (`w:themeColor="accent1"`) that have different values in the template's theme.
|
||||
|
||||
**Symptom**: Colors change unexpectedly (usually acceptable — this IS the point of re-theming). But if a style uses `w:color` with both `w:val` and `w:themeColor`, the theme color wins in Word.
|
||||
|
||||
**Fix**: Review color changes. If specific colors must be preserved, use explicit `w:val` without `w:themeColor`.
|
||||
|
||||
### 3. Section Property Conflicts
|
||||
|
||||
**Problem**: Source document has multiple sections (e.g., portrait + landscape pages), but the template assumes a single section.
|
||||
|
||||
**Symptom**: All sections get the same margins/orientation, breaking landscape pages.
|
||||
|
||||
**Fix**:
|
||||
- Only apply template section properties to the final `w:sectPr` in `w:body`
|
||||
- Preserve intermediate `w:sectPr` elements (inside `w:pPr`) from the source
|
||||
- Or apply template properties to all sections but preserve orientation overrides
|
||||
|
||||
### 4. Embedded Font Conflicts
|
||||
|
||||
**Problem**: Template specifies fonts not available on the target system.
|
||||
|
||||
**Fix**: Either embed fonts in the DOCX (`word/fonts/`) or use web-safe alternatives:
|
||||
- Calibri → available on Windows/Mac/Office online
|
||||
- Arial → universal fallback
|
||||
- Times New Roman → universal serif fallback
|
||||
|
||||
### 5. Broken Style Inheritance
|
||||
|
||||
**Problem**: Template has `Heading1` based on `Normal`, but after applying template, `Normal` has different properties, cascading unwanted changes to headings.
|
||||
|
||||
**Fix**: Verify the `w:basedOn` chain for all critical styles. Ensure base styles are also correctly transferred from template.
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After template application, verify:
|
||||
|
||||
1. **Content preserved** — text diff shows zero content changes
|
||||
2. **Gate-check passed** — `business-rules.xsd` validation succeeds
|
||||
3. **Styles applied** — headings, body text, tables use template formatting
|
||||
4. **Images intact** — all images render correctly (relationship IDs valid)
|
||||
5. **Lists working** — numbered and bulleted lists display correctly
|
||||
6. **Headers/footers** — template headers/footers appear on all pages
|
||||
7. **Page layout** — margins, page size, orientation match template
|
||||
8. **No corruption** — file opens without errors in Word
|
||||
200
skills/minimax-docx/references/track_changes_guide.md
Normal file
200
skills/minimax-docx/references/track_changes_guide.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Track Changes Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Track Changes in OpenXML uses revision markup elements to record insertions, deletions, and formatting changes. Each revision has a unique ID, author, and timestamp.
|
||||
|
||||
---
|
||||
|
||||
## Insertion: `<w:ins>`
|
||||
|
||||
Wraps runs that were inserted during tracking:
|
||||
|
||||
```xml
|
||||
<w:ins w:id="1" w:author="John Smith" w:date="2026-03-21T10:30:00Z">
|
||||
<w:r>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" />
|
||||
<w:sz w:val="22" />
|
||||
</w:rPr>
|
||||
<w:t>This text was inserted.</w:t>
|
||||
</w:r>
|
||||
</w:ins>
|
||||
```
|
||||
|
||||
- `w:id` — unique revision ID (integer, must be unique across document)
|
||||
- `w:author` — free text string identifying the author
|
||||
- `w:date` — ISO 8601 format with timezone: `YYYY-MM-DDTHH:MM:SSZ`
|
||||
- Content inside is normal runs (`w:r`) with optional formatting
|
||||
|
||||
---
|
||||
|
||||
## Deletion: `<w:del>`
|
||||
|
||||
Wraps runs that were deleted during tracking:
|
||||
|
||||
```xml
|
||||
<w:del w:id="2" w:author="John Smith" w:date="2026-03-21T10:31:00Z">
|
||||
<w:r>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" />
|
||||
<w:sz w:val="22" />
|
||||
</w:rPr>
|
||||
<w:delText xml:space="preserve">This text was deleted.</w:delText>
|
||||
</w:r>
|
||||
</w:del>
|
||||
```
|
||||
|
||||
**CRITICAL**: Inside `<w:del>`, text MUST use `<w:delText>`, NOT `<w:t>`. Using `<w:t>` inside a deletion is invalid and will cause corruption or unexpected behavior. Word may silently repair it, but other consumers will fail.
|
||||
|
||||
---
|
||||
|
||||
## Formatting Change: `<w:rPrChange>`
|
||||
|
||||
Records that a run's formatting was changed. Placed inside `w:rPr`, it stores the **previous** formatting:
|
||||
|
||||
```xml
|
||||
<w:r>
|
||||
<w:rPr>
|
||||
<w:b /> <!-- Current: bold -->
|
||||
<w:rPrChange w:id="3" w:author="Jane Doe" w:date="2026-03-21T11:00:00Z">
|
||||
<w:rPr>
|
||||
<!-- Previous: not bold (empty rPr means no formatting) -->
|
||||
</w:rPr>
|
||||
</w:rPrChange>
|
||||
</w:rPr>
|
||||
<w:t>This text was made bold.</w:t>
|
||||
</w:r>
|
||||
```
|
||||
|
||||
The outer `w:rPr` holds the **new** (current) formatting. The `w:rPrChange` child holds the **old** (previous) formatting.
|
||||
|
||||
---
|
||||
|
||||
## Paragraph Property Change: `<w:pPrChange>`
|
||||
|
||||
Records paragraph-level formatting changes (alignment, spacing, style):
|
||||
|
||||
```xml
|
||||
<w:pPr>
|
||||
<w:jc w:val="center" /> <!-- Current: centered -->
|
||||
<w:pPrChange w:id="4" w:author="Jane Doe" w:date="2026-03-21T11:05:00Z">
|
||||
<w:pPr>
|
||||
<w:jc w:val="left" /> <!-- Previous: left-aligned -->
|
||||
</w:pPr>
|
||||
</w:pPrChange>
|
||||
</w:pPr>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Revision ID Management
|
||||
|
||||
- Every revision element (`w:ins`, `w:del`, `w:rPrChange`, `w:pPrChange`, `w:tblPrChange`, etc.) requires a `w:id` attribute
|
||||
- IDs must be **unique integers** across the entire document
|
||||
- IDs should be **monotonically increasing** (not strictly required, but expected by Word)
|
||||
- When adding revisions, scan for the current maximum `w:id` and increment from there
|
||||
|
||||
```
|
||||
Existing max ID: 47
|
||||
New insertion: w:id="48"
|
||||
New deletion: w:id="49"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Author and Date
|
||||
|
||||
- **Author**: Free text. Use consistent strings (e.g., `"MiniMaxAI"` for all automated edits)
|
||||
- **Date**: ISO 8601 with UTC timezone marker: `2026-03-21T10:30:00Z`
|
||||
- Must include the `T` separator and `Z` suffix (or `+HH:MM` offset)
|
||||
- Omitting the date is allowed but not recommended
|
||||
|
||||
---
|
||||
|
||||
## Operations
|
||||
|
||||
### Propose Insertion
|
||||
|
||||
Add `<w:ins>` wrapper around new content at the target location:
|
||||
|
||||
```xml
|
||||
<w:p>
|
||||
<w:r><w:t>Existing text. </w:t></w:r>
|
||||
<w:ins w:id="5" w:author="MiniMaxAI" w:date="2026-03-21T12:00:00Z">
|
||||
<w:r><w:t>Proposed new text. </w:t></w:r>
|
||||
</w:ins>
|
||||
<w:r><w:t>More existing text.</w:t></w:r>
|
||||
</w:p>
|
||||
```
|
||||
|
||||
### Propose Deletion
|
||||
|
||||
Wrap existing content in `<w:del>` and change `<w:t>` to `<w:delText>`:
|
||||
|
||||
```xml
|
||||
<w:p>
|
||||
<w:r><w:t>Keep this. </w:t></w:r>
|
||||
<w:del w:id="6" w:author="MiniMaxAI" w:date="2026-03-21T12:01:00Z">
|
||||
<w:r>
|
||||
<w:rPr><w:b /></w:rPr>
|
||||
<w:delText>Remove this.</w:delText>
|
||||
</w:r>
|
||||
</w:del>
|
||||
<w:r><w:t> Keep this too.</w:t></w:r>
|
||||
</w:p>
|
||||
```
|
||||
|
||||
### Accept a Tracked Change
|
||||
|
||||
- **Accept insertion**: Remove the `<w:ins>` wrapper, keep the inner runs as normal content
|
||||
- **Accept deletion**: Remove the entire `<w:del>` element and its content
|
||||
|
||||
### Reject a Tracked Change
|
||||
|
||||
- **Reject insertion**: Remove the entire `<w:ins>` element and its content
|
||||
- **Reject deletion**: Remove the `<w:del>` wrapper, change `<w:delText>` back to `<w:t>`
|
||||
|
||||
---
|
||||
|
||||
## Cross-Paragraph Operations
|
||||
|
||||
### Deleting a Paragraph Break (Merging Paragraphs)
|
||||
|
||||
When tracked deletion spans a paragraph boundary, use `<w:pPrChange>` on the merged paragraph:
|
||||
|
||||
```xml
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:pPrChange w:id="7" w:author="MiniMaxAI" w:date="2026-03-21T12:05:00Z">
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="Normal" />
|
||||
</w:pPr>
|
||||
</w:pPrChange>
|
||||
</w:pPr>
|
||||
<w:r><w:t>First paragraph text. </w:t></w:r>
|
||||
<w:del w:id="8" w:author="MiniMaxAI" w:date="2026-03-21T12:05:00Z">
|
||||
<w:r><w:delText> </w:delText></w:r>
|
||||
</w:del>
|
||||
<w:r><w:t>Second paragraph text (now merged).</w:t></w:r>
|
||||
</w:p>
|
||||
```
|
||||
|
||||
### Inserting a New Paragraph
|
||||
|
||||
The entire new paragraph is wrapped in `<w:ins>`:
|
||||
|
||||
```xml
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:rPr>
|
||||
<w:ins w:id="9" w:author="MiniMaxAI" w:date="2026-03-21T12:10:00Z" />
|
||||
</w:rPr>
|
||||
</w:pPr>
|
||||
<w:ins w:id="10" w:author="MiniMaxAI" w:date="2026-03-21T12:10:00Z">
|
||||
<w:r><w:t>Entirely new paragraph.</w:t></w:r>
|
||||
</w:ins>
|
||||
</w:p>
|
||||
```
|
||||
|
||||
The paragraph mark itself is marked as inserted via `w:ins` inside `w:pPr > w:rPr`.
|
||||
506
skills/minimax-docx/references/troubleshooting.md
Normal file
506
skills/minimax-docx/references/troubleshooting.md
Normal file
@@ -0,0 +1,506 @@
|
||||
# Troubleshooting Guide — Symptom-Driven
|
||||
|
||||
## How to Use This Guide
|
||||
|
||||
Search by the **SYMPTOM** you observe, not the technical concept. Each entry follows:
|
||||
- **Symptom** — what you see or what the user reports
|
||||
- **Diagnosis** — how to confirm the root cause
|
||||
- **Fix** — exact steps, commands, or code
|
||||
- **Prevention** — how to avoid it next time
|
||||
|
||||
**Quick search keywords:** headings wrong, body text, repair, corrupt, font, tables missing, images missing, TOC broken, update table, page break, section break, hyperlink, numbered list, bullets, margins, page size, Chinese tofu, cover page, track changes, revision marks
|
||||
|
||||
---
|
||||
|
||||
## 1. "All headings look like body text" (Heading Styles Not Applied)
|
||||
|
||||
**Symptom:** After template application, headings have no formatting — they look like Normal paragraphs. Font size, bold, spacing are all wrong.
|
||||
|
||||
**Diagnosis:** The `pStyle` values in `document.xml` don't match the `styleId` values in `styles.xml`.
|
||||
|
||||
Common mismatches:
|
||||
- Source uses `Heading1` but template defines the style as `1` (Chinese templates often use numeric styleIds)
|
||||
- Source uses `heading1` (lowercase) but template has `Heading1` (case-sensitive!)
|
||||
- `pStyle` references a style that simply doesn't exist in the output's `styles.xml`
|
||||
|
||||
Check with:
|
||||
```bash
|
||||
# List all pStyle values used in the document
|
||||
$CLI analyze --input output.docx | grep -i "pStyle"
|
||||
|
||||
# List all styleIds defined in styles.xml
|
||||
$CLI analyze --input template.docx --part styles | grep "styleId"
|
||||
```
|
||||
|
||||
**Fix:** Build a styleId mapping table before applying the template. Update every `pStyle` value in the document content.
|
||||
|
||||
```csharp
|
||||
// Build mapping: source styleId → template styleId
|
||||
var mapping = new Dictionary<string, string>();
|
||||
// Compare by style name (w:name), not by styleId
|
||||
foreach (var srcStyle in sourceStyles)
|
||||
{
|
||||
var templateStyle = templateStyles.FirstOrDefault(
|
||||
s => s.StyleName?.Val?.Value == srcStyle.StyleName?.Val?.Value);
|
||||
if (templateStyle != null)
|
||||
mapping[srcStyle.StyleId!] = templateStyle.StyleId!;
|
||||
}
|
||||
|
||||
// Apply mapping to all paragraphs
|
||||
foreach (var para in body.Descendants<Paragraph>())
|
||||
{
|
||||
var pStyle = para.ParagraphProperties?.ParagraphStyleId;
|
||||
if (pStyle != null && mapping.TryGetValue(pStyle.Val!, out var newId))
|
||||
pStyle.Val = newId;
|
||||
}
|
||||
```
|
||||
|
||||
**Prevention:** ALWAYS extract and compare styleIds from both source and template before template application. Never assume styleIds are the same across documents.
|
||||
|
||||
---
|
||||
|
||||
## 2. "Document opens with repair warnings" (XML Corruption)
|
||||
|
||||
**Symptom:** Word says "We found a problem with some content" or "Word found unreadable content" when opening.
|
||||
|
||||
**Diagnosis:** Element ordering is wrong. OpenXML is strict about child element order.
|
||||
|
||||
Common violations:
|
||||
- `pPr` must come before runs in `w:p`
|
||||
- `tblPr` must come before `tblGrid` in `w:tbl`
|
||||
- `rPr` must come before `t`/`br`/`tab` in `w:r`
|
||||
- `trPr` must come before `tc` in `w:tr`
|
||||
- `tcPr` must come before content in `w:tc`
|
||||
|
||||
```bash
|
||||
# Validate to find ordering issues
|
||||
$CLI validate --input doc.docx --xsd assets/xsd/wml-subset.xsd
|
||||
|
||||
# Auto-fix element ordering
|
||||
$CLI fix-order --input doc.docx
|
||||
|
||||
# Re-validate
|
||||
$CLI validate --input doc.docx --xsd assets/xsd/wml-subset.xsd
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
$CLI fix-order --input doc.docx
|
||||
```
|
||||
|
||||
If auto-fix doesn't resolve it, unpack and inspect manually:
|
||||
```bash
|
||||
$CLI unpack --input doc.docx --output unpacked/
|
||||
# Check word/document.xml for ordering issues
|
||||
# Fix, then repack:
|
||||
$CLI pack --input unpacked/ --output fixed.docx
|
||||
```
|
||||
|
||||
**Prevention:** Read `references/openxml_element_order.md` before writing any XML manipulation code. Always append properties elements first, then content elements.
|
||||
|
||||
---
|
||||
|
||||
## 3. "All text is in wrong font" (Font Contamination)
|
||||
|
||||
**Symptom:** Template specifies 宋体/Times New Roman but document shows Google Sans, Arial, Calibri, or whatever font the source document used.
|
||||
|
||||
**Diagnosis:** Source document's `rPr` contains inline `rFonts` declarations that override template styles. Direct formatting always wins over style-based formatting in OpenXML.
|
||||
|
||||
```bash
|
||||
# Check for font contamination
|
||||
$CLI analyze --input output.docx | grep -i "font"
|
||||
# Look for rFonts in the content — if present, they're overriding styles
|
||||
```
|
||||
|
||||
**Fix:** Strip `rFonts` from `rPr` when copying content, but KEEP `w:eastAsia` for CJK text:
|
||||
|
||||
```csharp
|
||||
foreach (var rPr in body.Descendants<RunProperties>())
|
||||
{
|
||||
var rFonts = rPr.GetFirstChild<RunFonts>();
|
||||
if (rFonts != null)
|
||||
{
|
||||
// Preserve EastAsia font for CJK — removing it causes tofu (□□□)
|
||||
var eastAsia = rFonts.EastAsia?.Value;
|
||||
rFonts.Remove();
|
||||
|
||||
// Re-add only eastAsia if it was set and text contains CJK
|
||||
if (!string.IsNullOrEmpty(eastAsia))
|
||||
{
|
||||
rPr.Append(new RunFonts { EastAsia = eastAsia });
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Also strip these common direct formatting overrides:
|
||||
- `w:sz` / `w:szCs` (font size)
|
||||
- `w:color` (text color)
|
||||
- `w:b` / `w:i` when they contradict the style
|
||||
|
||||
**Prevention:** Always clean direct formatting when copying content between documents. Keep only `pStyle`/`rStyle` references and `w:t` text.
|
||||
|
||||
---
|
||||
|
||||
## 4. "Tables are missing" (Tables Lost During Copy)
|
||||
|
||||
**Symptom:** Source had 5 tables but output only has 2 (or 0).
|
||||
|
||||
**Diagnosis:** Code used `body.findall('w:p')` or `body.Descendants<Paragraph>()` at the top level instead of iterating all children. This skips `w:tbl` elements.
|
||||
|
||||
```bash
|
||||
# Verify table count
|
||||
$CLI analyze --input source.docx | grep -i "table"
|
||||
$CLI analyze --input output.docx | grep -i "table"
|
||||
```
|
||||
|
||||
**Fix:** Use `list(body)` or `body.ChildElements` to get ALL top-level children including tables:
|
||||
|
||||
```csharp
|
||||
// WRONG — skips tables, section properties, and other non-paragraph elements
|
||||
var paragraphs = body.Elements<Paragraph>();
|
||||
|
||||
// CORRECT — gets everything: paragraphs, tables, SDT blocks, etc.
|
||||
var allElements = body.ChildElements.ToList();
|
||||
```
|
||||
|
||||
In Python with lxml:
|
||||
```python
|
||||
# WRONG
|
||||
elements = body.findall('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p')
|
||||
|
||||
# CORRECT
|
||||
elements = list(body) # all direct children
|
||||
```
|
||||
|
||||
**Prevention:** Always use `list(body)` or `body.ChildElements` for iteration, never filter by a single element type alone when copying content.
|
||||
|
||||
---
|
||||
|
||||
## 5. "Images are missing or show broken icon"
|
||||
|
||||
**Symptom:** Image placeholders appear but images don't render. Or images are completely absent.
|
||||
|
||||
**Diagnosis:** The `r:embed` rId in `w:drawing` doesn't match any relationship in `document.xml.rels`, or the media file wasn't copied to the output ZIP.
|
||||
|
||||
```bash
|
||||
# Check relationships
|
||||
$CLI analyze --input output.docx --part rels | grep -i "image"
|
||||
|
||||
# Check if media files exist
|
||||
$CLI unpack --input output.docx --output unpacked/
|
||||
ls unpacked/word/media/
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
1. Check source rels for image file paths
|
||||
2. Copy media files from source to output
|
||||
3. Add/update relationships in output rels
|
||||
4. Update `r:embed` values in drawing elements
|
||||
|
||||
```csharp
|
||||
// When copying content with images between documents:
|
||||
foreach (var drawing in body.Descendants<Drawing>())
|
||||
{
|
||||
var blip = drawing.Descendants<DocumentFormat.OpenXml.Drawing.Blip>().FirstOrDefault();
|
||||
if (blip?.Embed?.Value != null)
|
||||
{
|
||||
var sourceRel = sourcePart.GetReferenceRelationship(blip.Embed.Value);
|
||||
// Copy the image part to the target document
|
||||
var imagePart = targetPart.AddImagePart(ImagePartType.Png);
|
||||
using var stream = sourcePart.GetPartById(blip.Embed.Value).GetStream();
|
||||
imagePart.FeedData(stream);
|
||||
// Update the rId reference
|
||||
blip.Embed = targetPart.GetIdOfPart(imagePart);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Prevention:** Always do rId remapping + media file copy when moving content between documents. Never assume rIds are portable across documents.
|
||||
|
||||
---
|
||||
|
||||
## 6. "TOC shows stale/wrong entries" or "Update Table doesn't work"
|
||||
|
||||
**Symptom:** Table of contents shows the template's example entries (e.g., "第1章 绪论...1") instead of actual headings. Or clicking "Update Table" in Word does nothing.
|
||||
|
||||
**Diagnosis:**
|
||||
- **Stale entries (normal):** TOC entries are static text cached inside the field. They don't auto-update until the user explicitly updates in Word.
|
||||
- **Update Table fails:** The SDT wrapper or field code structure is damaged. The TOC in real templates is a mixed structure: SDT block + field code + static entries.
|
||||
|
||||
```bash
|
||||
# Check if TOC SDT exists
|
||||
$CLI analyze --input output.docx | grep -i "sdt\|toc"
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
- **If entries are just stale:** This is expected behavior. The user must right-click TOC, then "Update Field" in Word. Or enable auto-update:
|
||||
```csharp
|
||||
// See FieldAndTocSamples.EnableUpdateFieldsOnOpen()
|
||||
FieldAndTocSamples.EnableUpdateFieldsOnOpen(settingsPart);
|
||||
```
|
||||
- **If SDT is damaged:** Keep the entire SDT block from the template intact. Do not modify it.
|
||||
- **If field code is missing:** Ensure the TOC contains: `fldChar begin` + `instrText` + `fldChar separate` + static entries + `fldChar end`. See `FieldAndTocSamples.CreateMixedTocStructure()` for the complete pattern.
|
||||
- **If you rebuilt TOC from scratch (common mistake):** You likely destroyed the SDT wrapper. Use the template's original SDT block instead. See `Samples/FieldAndTocSamples.cs` method `CreateMixedTocStructure` for how real-world TOC is structured.
|
||||
|
||||
**Prevention:** When doing Base-Replace (C-2), keep the template's TOC zone completely untouched. Do not strip, rebuild, or modify the SDT block. The TOC will auto-update when the user opens in Word.
|
||||
|
||||
---
|
||||
|
||||
## 7. "Chapters don't start on new pages" (Missing Section Breaks)
|
||||
|
||||
**Symptom:** Content flows continuously without page breaks between chapters. Chapter 2 starts right after Chapter 1's last paragraph on the same page.
|
||||
|
||||
**Diagnosis:** No `sectPr` elements or page break paragraphs between chapters.
|
||||
|
||||
**Fix:** Insert a paragraph with `sectPr` in its `pPr` before each chapter heading, or insert a page break:
|
||||
|
||||
```csharp
|
||||
// Option 1: Section break (preserves per-section settings like headers/margins)
|
||||
var breakPara = new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new SectionProperties(
|
||||
new SectionType { Val = SectionMarkValues.NextPage })));
|
||||
|
||||
// Option 2: Simple page break (lighter weight)
|
||||
var breakPara = new Paragraph(
|
||||
new Run(new Break { Type = BreakValues.Page }));
|
||||
|
||||
// Insert before each Heading1
|
||||
body.InsertBefore(breakPara, heading1Paragraph);
|
||||
```
|
||||
|
||||
**Prevention:** When copying content, insert page/section breaks before Heading1 paragraphs as needed. Check source document's section structure before copying.
|
||||
|
||||
---
|
||||
|
||||
## 8. "Hyperlinks don't work" (Broken Links)
|
||||
|
||||
**Symptom:** Clicking a hyperlink in the output document does nothing, or it navigates to the wrong URL.
|
||||
|
||||
**Diagnosis:** `w:hyperlink r:id` points to a relationship that doesn't exist in `document.xml.rels`.
|
||||
|
||||
```bash
|
||||
# Check hyperlink relationships
|
||||
$CLI analyze --input output.docx --part rels | grep -i "hyperlink"
|
||||
```
|
||||
|
||||
**Fix:** Merge source document's hyperlink relationships into output's rels file. Update rId references.
|
||||
|
||||
```csharp
|
||||
foreach (var hyperlink in body.Descendants<Hyperlink>())
|
||||
{
|
||||
if (hyperlink.Id?.Value != null)
|
||||
{
|
||||
var sourceRel = sourcePart.HyperlinkRelationships
|
||||
.FirstOrDefault(r => r.Id == hyperlink.Id.Value);
|
||||
if (sourceRel != null)
|
||||
{
|
||||
targetPart.AddHyperlinkRelationship(sourceRel.Uri, sourceRel.IsExternal);
|
||||
var newRel = targetPart.HyperlinkRelationships.Last();
|
||||
hyperlink.Id = newRel.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Prevention:** Always merge ALL relationship types (images, hyperlinks, headers, footers) when combining documents. Never assume source rIds work in the target.
|
||||
|
||||
---
|
||||
|
||||
## 9. "Numbered lists show wrong numbers" or "Bullets disappeared"
|
||||
|
||||
**Symptom:** Lists that were numbered 1, 2, 3 now show 1, 1, 1 or have no numbers/bullets at all.
|
||||
|
||||
**Diagnosis:** `numId` in `pPr` references a numbering definition that doesn't exist in `numbering.xml`, or `abstractNumId` mapping is broken.
|
||||
|
||||
```bash
|
||||
# Check numbering definitions
|
||||
$CLI analyze --input output.docx --part numbering
|
||||
```
|
||||
|
||||
**Fix:** Map source numIds to template numIds, or merge numbering definitions:
|
||||
|
||||
```csharp
|
||||
// 1. Copy abstractNum definitions from source to target numbering.xml
|
||||
// 2. Create new num entries pointing to the copied abstractNum
|
||||
// 3. Update all numId references in document content
|
||||
|
||||
var sourceNumbering = sourceNumberingPart.Numbering;
|
||||
var targetNumbering = targetNumberingPart.Numbering;
|
||||
|
||||
// Get max existing IDs to avoid collisions
|
||||
int maxAbstractNumId = targetNumbering.Elements<AbstractNum>()
|
||||
.Max(a => a.AbstractNumberId?.Value ?? 0) + 1;
|
||||
int maxNumId = targetNumbering.Elements<NumberingInstance>()
|
||||
.Max(n => n.NumberID?.Value ?? 0) + 1;
|
||||
```
|
||||
|
||||
**Prevention:** Include `numbering.xml` reconciliation in template application workflow. See `Samples/ListAndNumberingSamples.cs` for correct numbering setup.
|
||||
|
||||
---
|
||||
|
||||
## 10. "Page margins/size are wrong"
|
||||
|
||||
**Symptom:** Output has different margins, page size, or orientation than the template.
|
||||
|
||||
**Diagnosis:** Source document's `sectPr` is overriding the template's `sectPr`. The final `sectPr` (child of `body`) controls the last section's layout.
|
||||
|
||||
```bash
|
||||
# Compare section properties
|
||||
$CLI analyze --input template.docx | grep -i "sectPr\|margin\|pgSz"
|
||||
$CLI analyze --input output.docx | grep -i "sectPr\|margin\|pgSz"
|
||||
```
|
||||
|
||||
**Fix:** Use the template's final `sectPr`. For intermediate `sectPr` elements (multi-section documents), merge carefully.
|
||||
|
||||
```csharp
|
||||
// Replace output's final sectPr with template's
|
||||
var templateSectPr = templateBody.Elements<SectionProperties>().LastOrDefault();
|
||||
var outputSectPr = outputBody.Elements<SectionProperties>().LastOrDefault();
|
||||
|
||||
if (templateSectPr != null)
|
||||
{
|
||||
var cloned = templateSectPr.CloneNode(true) as SectionProperties;
|
||||
if (outputSectPr != null)
|
||||
outputBody.ReplaceChild(cloned!, outputSectPr);
|
||||
else
|
||||
outputBody.Append(cloned!);
|
||||
}
|
||||
```
|
||||
|
||||
**Prevention:** Always use the template's `sectPr` as authority for page layout. Strip source document's `sectPr` before copying content.
|
||||
|
||||
---
|
||||
|
||||
## 11. "Chinese text renders as boxes/tofu"
|
||||
|
||||
**Symptom:** Chinese characters display as square boxes (□□□) or missing glyphs.
|
||||
|
||||
**Diagnosis:** `rFonts w:eastAsia` is set to a font that doesn't exist on the system, or is missing entirely. Without an East Asian font declaration, the rendering engine may fall back to a font without CJK coverage.
|
||||
|
||||
**Fix:** Ensure all CJK text has `w:eastAsia` set to an available font:
|
||||
|
||||
```csharp
|
||||
foreach (var run in body.Descendants<Run>())
|
||||
{
|
||||
var text = run.InnerText;
|
||||
if (ContainsCjk(text))
|
||||
{
|
||||
var rPr = run.RunProperties ?? new RunProperties();
|
||||
var rFonts = rPr.GetFirstChild<RunFonts>();
|
||||
if (rFonts == null)
|
||||
{
|
||||
rFonts = new RunFonts();
|
||||
rPr.Append(rFonts);
|
||||
}
|
||||
// Set to a universally available CJK font
|
||||
rFonts.EastAsia = "SimSun"; // 宋体 — safest default
|
||||
if (run.RunProperties == null) run.PrependChild(rPr);
|
||||
}
|
||||
}
|
||||
|
||||
static bool ContainsCjk(string text)
|
||||
{
|
||||
return text.Any(c => c >= 0x4E00 && c <= 0x9FFF);
|
||||
}
|
||||
```
|
||||
|
||||
Common safe CJK fonts: 宋体 (SimSun), 黑体 (SimHei), 仿宋 (FangSong), 楷体 (KaiTi).
|
||||
|
||||
**Prevention:** When cleaning `rPr` formatting, ALWAYS preserve `w:eastAsia` font declarations. See also `references/cjk_typography.md`.
|
||||
|
||||
---
|
||||
|
||||
## 12. "Template's cover page / declaration page is missing"
|
||||
|
||||
**Symptom:** Output document starts directly with body content — no cover page, no declaration, no abstract, no table of contents. The template's structural front matter was discarded.
|
||||
|
||||
**Diagnosis:** Used Overlay (C-1) strategy when Base-Replace (C-2) was needed. Overlay applies styles to the source document but discards the template's structural content (cover, declaration, abstract, TOC).
|
||||
|
||||
```bash
|
||||
# Check template structure
|
||||
$CLI analyze --input template.docx
|
||||
# If template has >50 paragraphs with cover/TOC/declaration, C-2 is needed
|
||||
```
|
||||
|
||||
**Fix:** Use Base-Replace (C-2) strategy — template is the base, only replace the example body content zone with the user's content:
|
||||
|
||||
1. Identify the template's "body zone" (everything between TOC and final sectPr)
|
||||
2. Remove the template's example body content
|
||||
3. Insert the user's content into the body zone
|
||||
4. Keep everything else from the template (cover, declaration, abstract, TOC, sectPr)
|
||||
|
||||
```bash
|
||||
$CLI apply-template --input source.docx --template template.docx --output out.docx --strategy base-replace
|
||||
```
|
||||
|
||||
**Prevention:** Analyze template structure FIRST. If template has structural content (cover, TOC, declaration sections), always use C-2 (Base-Replace). Read `references/scenario_c_apply_template.md` for detailed decision criteria.
|
||||
|
||||
---
|
||||
|
||||
## 13. "Track changes markers appear unexpectedly"
|
||||
|
||||
**Symptom:** Output shows red/green revision marks (insertions, deletions) that weren't in the source document.
|
||||
|
||||
**Diagnosis:** Template had track changes enabled, or content was inserted as revisions rather than normal text.
|
||||
|
||||
```bash
|
||||
# Check for revision marks
|
||||
$CLI analyze --input output.docx | grep -i "revision\|ins\|del\|track"
|
||||
```
|
||||
|
||||
**Fix:** Accept all revisions by flattening `w:ins` and `w:del` elements:
|
||||
|
||||
```csharp
|
||||
// Accept insertions: unwrap w:ins, keep content
|
||||
foreach (var ins in body.Descendants<InsertedRun>().ToList())
|
||||
{
|
||||
var parent = ins.Parent!;
|
||||
foreach (var child in ins.ChildElements.ToList())
|
||||
{
|
||||
parent.InsertBefore(child.CloneNode(true), ins);
|
||||
}
|
||||
ins.Remove();
|
||||
}
|
||||
|
||||
// Accept deletions: remove w:del and its content entirely
|
||||
foreach (var del in body.Descendants<DeletedRun>().ToList())
|
||||
{
|
||||
del.Remove();
|
||||
}
|
||||
```
|
||||
|
||||
Or disable tracking in settings:
|
||||
```csharp
|
||||
var settings = settingsPart.Settings;
|
||||
var trackChanges = settings.GetFirstChild<TrackChanges>();
|
||||
trackChanges?.Remove();
|
||||
```
|
||||
|
||||
**Prevention:** Check template's `settings.xml` for `trackChanges` before starting. If present, accept all revisions in the template first.
|
||||
|
||||
---
|
||||
|
||||
## Recovery Strategy — When Multiple Issues Exist
|
||||
|
||||
When a document has multiple problems, fix them in this priority order:
|
||||
|
||||
```
|
||||
1. [Content_Types].xml — without this, nothing opens
|
||||
2. _rels/.rels — package relationships
|
||||
3. word/_rels/document.xml.rels — part relationships (images, hyperlinks)
|
||||
4. word/document.xml — element ordering (fix-order)
|
||||
5. word/styles.xml — style definitions and styleId mapping
|
||||
6. word/numbering.xml — list/numbering definitions
|
||||
7. Everything else — headers, footers, comments, settings
|
||||
```
|
||||
|
||||
```bash
|
||||
# Full recovery pipeline
|
||||
$CLI unpack --input broken.docx --output unpacked/
|
||||
$CLI validate --input broken.docx --xsd assets/xsd/wml-subset.xsd # find all errors
|
||||
$CLI fix-order --input broken.docx # fix element ordering
|
||||
$CLI validate --input broken.docx --business # check business rules
|
||||
scripts/docx_preview.sh broken.docx # visual check
|
||||
```
|
||||
294
skills/minimax-docx/references/typography_guide.md
Normal file
294
skills/minimax-docx/references/typography_guide.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# Professional Document Design & Typography Guide
|
||||
|
||||
## Table of Contents
|
||||
1. [Font Pairing](#font-pairing)
|
||||
2. [Font Sizes by Document Type](#font-sizes-by-document-type)
|
||||
3. [Line Spacing](#line-spacing)
|
||||
4. [Paragraph Spacing](#paragraph-spacing)
|
||||
5. [Page Layout](#page-layout)
|
||||
6. [Table Design](#table-design)
|
||||
7. [Color Schemes](#color-schemes)
|
||||
8. [Visual Hierarchy](#visual-hierarchy)
|
||||
9. [Quick Reference Defaults](#quick-reference-defaults)
|
||||
|
||||
---
|
||||
|
||||
## Font Pairing
|
||||
|
||||
### Recommended Pairs
|
||||
|
||||
| Headings | Body | Style | Best For |
|
||||
|----------|------|-------|----------|
|
||||
| Calibri Light | Calibri | Modern sans | Corporate reports |
|
||||
| Aptos | Aptos | Office 365 default | Modern business docs |
|
||||
| Cambria | Calibri | Serif + sans | Academic-corporate hybrid |
|
||||
| Times New Roman | Times New Roman | Traditional serif | Academic, legal |
|
||||
| Arial | Arial | Clean sans | Memos, internal docs |
|
||||
| Georgia | Garamond | Classical serif pair | Formal reports |
|
||||
|
||||
### Rules
|
||||
|
||||
- **Limit**: 2 font families max (3 if CJK mixed)
|
||||
- **Contrast**: Pair serif with sans-serif, OR use weight contrast within one family
|
||||
- **Consistency**: Same font for all body text, same font for all headings
|
||||
|
||||
---
|
||||
|
||||
## Font Sizes by Document Type
|
||||
|
||||
| Document Type | Body | H1 | H2 | H3 | Footnotes |
|
||||
|--------------|------|----|----|----|----|
|
||||
| **Business report** | 11pt | 18-20pt | 14-16pt | 12-13pt bold | 9pt |
|
||||
| **Business letter** | 11-12pt | — | — | — | 9-10pt |
|
||||
| **Memo** | 11pt | 14pt bold | 12pt bold | 11pt bold | 9pt |
|
||||
| **Contract / Legal** | 12pt | 14pt bold caps | 12pt bold | 12pt bold | 10pt |
|
||||
| **Academic (APA 7)** | 12pt | 12pt bold center | 12pt bold left | 12pt bold italic | 10pt |
|
||||
| **Resume / CV** | 10-11pt | 14-16pt | 12pt bold | 11pt bold | 8-9pt |
|
||||
| **Chinese 公文** | 三号(16pt) | 二号(22pt) | 三号(16pt) | 四号(14pt) | 小四(12pt) |
|
||||
|
||||
### OpenXML `w:sz` Values (half-points)
|
||||
|
||||
| Point Size | `w:sz` Val | Common Use |
|
||||
|-----------|-----------|------------|
|
||||
| 9pt | 18 | Footnotes, captions |
|
||||
| 10pt | 20 | Compact body text |
|
||||
| 10.5pt (五号) | 21 | CJK body small |
|
||||
| 11pt | 22 | Standard body (Calibri) |
|
||||
| 12pt (小四) | 24 | Standard body (TNR), CJK |
|
||||
| 14pt (四号) | 28 | CJK body, subheading |
|
||||
| 16pt (三号) | 32 | CJK heading, western H2 |
|
||||
| 18pt (小二) | 36 | Western H1 |
|
||||
| 22pt (二号) | 44 | CJK document title |
|
||||
| 26pt (一号) | 52 | Large title |
|
||||
|
||||
---
|
||||
|
||||
## Line Spacing
|
||||
|
||||
| Spacing | OpenXML `w:spacing line` | When to Use |
|
||||
|---------|--------------------------|-------------|
|
||||
| Single (1.0) | `line="240"` lineRule="auto" | Tables, footnotes, captions |
|
||||
| 1.08 (MS default) | `line="259"` lineRule="auto" | Modern Office documents |
|
||||
| 1.15 | `line="276"` lineRule="auto" | Business reports — best general default |
|
||||
| 1.5 | `line="360"` lineRule="auto" | Some academic, drafts for markup |
|
||||
| Double (2.0) | `line="480"` lineRule="auto" | APA/MLA manuscripts, legal briefs |
|
||||
| Fixed 28pt | `line="560"` lineRule="exact" | Chinese 公文 (GB/T 9704) |
|
||||
|
||||
**`lineRule` values**: `auto` = proportional (240 = 1 line), `exact` = fixed height, `atLeast` = minimum.
|
||||
|
||||
---
|
||||
|
||||
## Paragraph Spacing
|
||||
|
||||
| Element | Space Before (DXA) | Space After (DXA) |
|
||||
|---------|-------------------|-------------------|
|
||||
| Body paragraph | 0 | 120-160 (6-8pt) |
|
||||
| Heading 1 | 480 (24pt) | 120-240 |
|
||||
| Heading 2 | 360 (18pt) | 120 |
|
||||
| Heading 3 | 240 (12pt) | 80-120 |
|
||||
| List items | 0 | 40-80 (2-4pt) |
|
||||
| Block quote | 120-240 | 120-240 |
|
||||
| Table/Figure caption | 240 | 240 |
|
||||
|
||||
**Principle**: Space before a heading > space after, so heading visually "belongs to" content below (2:1 or 3:1 ratio).
|
||||
|
||||
---
|
||||
|
||||
## Page Layout
|
||||
|
||||
### Margins by Document Type
|
||||
|
||||
| Document Type | Top | Bottom | Left | Right | DXA Values |
|
||||
|--------------|-----|--------|------|-------|------------|
|
||||
| **Standard business** | 1 in | 1 in | 1 in | 1 in | 1440 all |
|
||||
| **Academic (APA/MLA)** | 1 in | 1 in | 1 in | 1 in | 1440 all |
|
||||
| **Thesis (binding)** | 1 in | 1 in | 1.5 in | 1 in | T/B:1440 L:2160 R:1440 |
|
||||
| **Chinese 公文** | 37mm | 35mm | 28mm | 26mm | T:2098 B:1984 L:1588 R:1474 |
|
||||
| **Narrow modern** | 0.75 in | 0.75 in | 0.75 in | 0.75 in | 1080 all |
|
||||
| **Wide** | 1 in | 1 in | 2 in | 2 in | T/B:1440 L/R:2880 |
|
||||
|
||||
### Page Sizes
|
||||
|
||||
| Size | Width × Height | DXA Width × Height |
|
||||
|------|---------------|-------------------|
|
||||
| US Letter | 8.5 × 11 in | 12240 × 15840 |
|
||||
| A4 | 210 × 297 mm | 11906 × 16838 |
|
||||
| Legal | 8.5 × 14 in | 12240 × 20160 |
|
||||
| A3 | 297 × 420 mm | 16838 × 23811 |
|
||||
|
||||
**Rule**: A4 for international audiences, Letter for US-only.
|
||||
|
||||
### Page Numbers
|
||||
|
||||
| Convention | Placement | Common In |
|
||||
|-----------|-----------|-----------|
|
||||
| Bottom center | Footer, centered | Academic, government |
|
||||
| Bottom right | Footer, right-aligned | Business reports |
|
||||
| "Page X of Y" | Footer, right-aligned | Contracts, legal |
|
||||
| Bottom outside | Alternating L/R for odd/even | Books, bound reports |
|
||||
| Chinese 公文 | Bottom center, format "-X-" | Government documents |
|
||||
|
||||
---
|
||||
|
||||
## Table Design
|
||||
|
||||
### Style Patterns
|
||||
|
||||
| Style | Description | When to Use |
|
||||
|-------|------------|-------------|
|
||||
| **Three-line (三线表)** | Top rule + header-bottom rule + bottom rule only, no vertical lines | Academic, scientific — gold standard |
|
||||
| **Banded rows** | Alternating white/light-gray, no borders | Modern corporate |
|
||||
| **Light grid** | Thin 0.5pt gray borders all cells | Business reports |
|
||||
| **Header-accent** | Dark/colored header row, no other borders | Modern templates |
|
||||
| **Full border** | All cells bordered | Financial tables, forms |
|
||||
|
||||
### Border Weights (OpenXML `w:sz` in eighths of a point)
|
||||
|
||||
| Visual | `Size` value | Points |
|
||||
|--------|-------------|--------|
|
||||
| Hairline | 2 | 0.25pt |
|
||||
| Thin | 4 | 0.5pt |
|
||||
| Medium | 8 | 1pt |
|
||||
| Thick | 12 | 1.5pt |
|
||||
|
||||
### Cell Padding
|
||||
|
||||
- **Minimum**: 0.05 in (28 DXA) — too tight for most uses
|
||||
- **Recommended**: 0.08-0.1 in (57-72 DXA) top/bottom, 0.1-0.15 in (72-108 DXA) left/right
|
||||
- **Spacious**: 0.12 in (86 DXA) top/bottom, 0.19 in (137 DXA) left/right
|
||||
|
||||
### Header Row Best Practices
|
||||
|
||||
- Bold text, optionally SMALL CAPS
|
||||
- Background: light gray (#F2F2F2) or dark with white text (#2F5496 + white)
|
||||
- Repeat header row on each page (`w:tblHeader` on `w:trPr`)
|
||||
- Right-align number columns, left-align text columns
|
||||
|
||||
---
|
||||
|
||||
## Color Schemes
|
||||
|
||||
### Corporate / Business
|
||||
|
||||
| Element | Hex | Notes |
|
||||
|---------|-----|-------|
|
||||
| Primary heading | #1F3864 | Dark navy, authoritative |
|
||||
| Secondary heading | #2E75B6 | Medium blue |
|
||||
| Body text | #333333 | Near-black (softer than #000) |
|
||||
| Table header bg | #4472C4 | With white #FFFFFF text |
|
||||
| Alternate row | #F2F2F2 | Subtle gray banding |
|
||||
| Hyperlink | #0563C1 | Standard blue |
|
||||
|
||||
### Academic
|
||||
|
||||
All text **#000000** (black). Color only in figures/charts.
|
||||
|
||||
### Chinese Government (公文)
|
||||
|
||||
| Element | Color |
|
||||
|---------|-------|
|
||||
| All body text | Black (required) |
|
||||
| 红头 agency name | Red #FF0000 |
|
||||
| 红线 separator | Red #FF0000 |
|
||||
| 公章 seal | Red |
|
||||
|
||||
### Accessibility
|
||||
|
||||
- Minimum contrast ratio 4.5:1 for normal text, 3:1 for large text (WCAG AA)
|
||||
- Never use color as sole means of conveying information
|
||||
- Ensure distinguishable in grayscale for printed documents
|
||||
|
||||
---
|
||||
|
||||
## Visual Hierarchy
|
||||
|
||||
### Heading Levels by Document Length
|
||||
|
||||
| Pages | Recommended Levels |
|
||||
|-------|-------------------|
|
||||
| 1-5 (memo, letter) | 1-2 levels |
|
||||
| 5-20 (report) | 2-3 levels |
|
||||
| 20-100 (long report) | 3-4 levels |
|
||||
| 100+ (thesis) | 4-5 levels max |
|
||||
|
||||
### Numbering Systems
|
||||
|
||||
**Decimal (ISO 2145)** — technical, international:
|
||||
```
|
||||
1 → 1.1 → 1.1.1 → 1.1.1.1
|
||||
```
|
||||
|
||||
**Traditional outline (US legal):**
|
||||
```
|
||||
I. → A. → 1. → a. → (1) → (a)
|
||||
```
|
||||
|
||||
**Chinese government (公文):**
|
||||
```
|
||||
一、(黑体) → (一)(楷体) → 1.(仿宋加粗) → (1)(仿宋)
|
||||
```
|
||||
|
||||
### Typography Emphasis
|
||||
|
||||
| Format | Use For | Avoid |
|
||||
|--------|---------|-------|
|
||||
| **Bold** | Key terms, headings, emphasis | Entire paragraphs |
|
||||
| *Italic* | Titles, foreign words, mild emphasis | Long passages (hard to read) |
|
||||
| Underline | Hyperlinks only (digital) | General emphasis (archaic) |
|
||||
| SMALL CAPS | Legal defined terms, acronyms | Body text |
|
||||
| ALL CAPS | Very short headings | Long text (reduces readability 15%) |
|
||||
|
||||
**CJK note**: Chinese/Japanese have no true italic. Use bold for emphasis.
|
||||
|
||||
### List Formatting
|
||||
|
||||
**Bullets** (unordered): `•` → `○` → `■` by level
|
||||
|
||||
**Numbers** (ordered): `1.` → `a.` → `i.` by level
|
||||
|
||||
- Indent each level 0.25-0.5 in (360-720 DXA)
|
||||
- Hanging indent: number hangs, text aligns consistently
|
||||
- Spacing between items: 2-4pt (less than paragraph spacing)
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Defaults
|
||||
|
||||
### Business Report (Safe Default)
|
||||
|
||||
| Parameter | Value | OpenXML |
|
||||
|-----------|-------|---------|
|
||||
| Body font | Calibri 11pt | sz="22", RunFonts Ascii="Calibri" |
|
||||
| H1 | 18pt Bold Dark Blue | sz="36", Bold, Color="#1F3864" |
|
||||
| H2 | 14pt Bold Dark Blue | sz="28", Bold |
|
||||
| H3 | 12pt Bold Dark Blue | sz="24", Bold |
|
||||
| Line spacing | 1.15 | line="276" lineRule="auto" |
|
||||
| Para after | 8pt | after="160" |
|
||||
| Margins | 1 in all | 1440 DXA all |
|
||||
| Page size | Letter or A4 | 12240×15840 or 11906×16838 |
|
||||
| Page numbers | Bottom right, 10pt | |
|
||||
|
||||
### Academic Paper (APA 7th)
|
||||
|
||||
| Parameter | Value | OpenXML |
|
||||
|-----------|-------|---------|
|
||||
| Font | Times New Roman 12pt | sz="24" |
|
||||
| Line spacing | Double | line="480" lineRule="auto" |
|
||||
| First-line indent | 0.5 in | ind firstLine="720" |
|
||||
| Margins | 1 in all | 1440 DXA all |
|
||||
| Page numbers | Top right | Header, right-aligned |
|
||||
|
||||
### Chinese Government (公文 GB/T 9704)
|
||||
|
||||
| Parameter | Value | OpenXML |
|
||||
|-----------|-------|---------|
|
||||
| Body font | 仿宋_GB2312 三号 | sz="32", EastAsia="FangSong_GB2312" |
|
||||
| Title | 小标宋 二号 centered | sz="44" |
|
||||
| L1 heading | 黑体 三号 | sz="32", EastAsia="SimHei" |
|
||||
| L2 heading | 楷体 三号 | sz="32", EastAsia="KaiTi_GB2312" |
|
||||
| Line spacing | Fixed 28pt | line="560" lineRule="exact" |
|
||||
| Margins | T:37mm B:35mm L:28mm R:26mm | T:2098 B:1984 L:1588 R:1474 |
|
||||
| Page size | A4 | 11906×16838 |
|
||||
| Page numbers | Bottom center, 宋体 四号, "-X-" | sz="28" |
|
||||
| Chars/line | 28 | |
|
||||
| Lines/page | 22 | |
|
||||
158
skills/minimax-docx/references/xsd_validation_guide.md
Normal file
158
skills/minimax-docx/references/xsd_validation_guide.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# XSD Validation Guide
|
||||
|
||||
## Running Validation
|
||||
|
||||
```bash
|
||||
# Validate against the WML subset schema
|
||||
dotnet run --project minimax-docx validate input.docx --xsd assets/xsd/wml-subset.xsd
|
||||
|
||||
# Validate against business rules (REQUIRED for Scenario C gate-check)
|
||||
dotnet run --project minimax-docx validate input.docx --xsd assets/xsd/business-rules.xsd
|
||||
|
||||
# Validate against both
|
||||
dotnet run --project minimax-docx validate input.docx --xsd assets/xsd/wml-subset.xsd --xsd assets/xsd/business-rules.xsd
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What wml-subset.xsd Covers
|
||||
|
||||
The subset schema validates the most common WordprocessingML elements:
|
||||
|
||||
| Area | Elements Validated |
|
||||
|------|--------------------|
|
||||
| Document structure | `w:document`, `w:body`, `w:sectPr` |
|
||||
| Paragraphs | `w:p`, `w:pPr`, `w:r`, `w:rPr`, `w:t` |
|
||||
| Tables | `w:tbl`, `w:tblPr`, `w:tblGrid`, `w:tr`, `w:tc` |
|
||||
| Styles | `w:styles`, `w:style`, `w:docDefaults` |
|
||||
| Lists | `w:numbering`, `w:abstractNum`, `w:num` |
|
||||
| Headers/Footers | `w:hdr`, `w:ftr` |
|
||||
| Track Changes | `w:ins`, `w:del`, `w:rPrChange`, `w:pPrChange` |
|
||||
| Comments | `w:comment`, `w:commentRangeStart`, `w:commentRangeEnd` |
|
||||
|
||||
### What It Does NOT Cover
|
||||
|
||||
- DrawingML elements (`a:`, `pic:`, `wp:`) — image/shape internals
|
||||
- VML elements (`v:`, `o:`) — legacy shapes
|
||||
- Math elements (`m:`) — equations
|
||||
- Extended namespaces (`w14`, `w15`, `w16*`) — vendor extensions
|
||||
- Custom XML data parts
|
||||
- Relationship and content type validation (structural, not schema-based)
|
||||
|
||||
---
|
||||
|
||||
## Interpreting Errors
|
||||
|
||||
### Element Ordering Error
|
||||
|
||||
```
|
||||
ERROR: Element 'w:jc' is not expected at this position.
|
||||
Expected: w:spacing, w:ind, w:contextualSpacing, ...
|
||||
Location: /word/document.xml, line 45
|
||||
```
|
||||
|
||||
**Cause**: Child elements are in wrong order. See `references/openxml_element_order.md`.
|
||||
**Fix**: Reorder children to match schema sequence.
|
||||
|
||||
### Missing Required Element
|
||||
|
||||
```
|
||||
ERROR: Element 'w:tbl' missing required child 'w:tblPr'.
|
||||
Location: /word/document.xml, line 102
|
||||
```
|
||||
|
||||
**Cause**: A required child element is absent.
|
||||
**Fix**: Add the missing element. Tables require both `w:tblPr` and `w:tblGrid`.
|
||||
|
||||
### Invalid Attribute Value
|
||||
|
||||
```
|
||||
ERROR: Attribute 'w:val' has invalid value 'middle'.
|
||||
Expected: 'left', 'center', 'right', 'both', 'distribute'
|
||||
Location: /word/document.xml, line 78
|
||||
```
|
||||
|
||||
**Cause**: An attribute value is not in the allowed enumeration.
|
||||
**Fix**: Use one of the valid values listed in the error.
|
||||
|
||||
### Unexpected Element
|
||||
|
||||
```
|
||||
ERROR: Element 'w:customTag' is not expected.
|
||||
Location: /word/document.xml, line 200
|
||||
```
|
||||
|
||||
**Cause**: An element not defined in the subset schema. May be a vendor extension.
|
||||
**Fix**: Check if it's a known extension (w14/w15/w16). If so, it's likely safe. If unknown, investigate or remove.
|
||||
|
||||
---
|
||||
|
||||
## Business Rules XSD
|
||||
|
||||
The `business-rules.xsd` schema enforces project-specific constraints beyond standard OpenXML validity:
|
||||
|
||||
| Rule | What It Checks |
|
||||
|------|---------------|
|
||||
| Required styles | `Normal`, `Heading1`-`Heading3`, `TableGrid` must exist in `styles.xml` |
|
||||
| Font consistency | `w:docDefaults` fonts match expected values |
|
||||
| Margin ranges | Page margins within acceptable range (720-2160 DXA) |
|
||||
| Page size | Must be A4 or Letter |
|
||||
| Heading hierarchy | No gaps (e.g., H1 → H3 without H2) |
|
||||
| Style chain | `w:basedOn` references must resolve to existing styles |
|
||||
|
||||
### Extending Business Rules
|
||||
|
||||
To add project-specific rules, add `xs:assert` or `xs:restriction` elements:
|
||||
|
||||
```xml
|
||||
<!-- Require minimum 1-inch margins -->
|
||||
<xs:element name="pgMar">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="top" type="xs:integer">
|
||||
<xs:restriction>
|
||||
<xs:minInclusive value="1440" />
|
||||
</xs:restriction>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gate-Check: Scenario C Hard Gate
|
||||
|
||||
In Scenario C (Apply Template), the output document **MUST** pass `business-rules.xsd` validation before delivery:
|
||||
|
||||
```
|
||||
1. Apply template → output.docx
|
||||
2. Validate → dotnet run ... validate output.docx --xsd business-rules.xsd
|
||||
3. PASS? → Deliver to user
|
||||
4. FAIL? → Fix issues, re-validate, repeat until PASS
|
||||
```
|
||||
|
||||
**This is a hard gate.** A document that fails business-rules validation is NOT deliverable, even if it opens correctly in Word.
|
||||
|
||||
---
|
||||
|
||||
## False Positives
|
||||
|
||||
### Vendor Extensions
|
||||
|
||||
Elements from extended namespaces (`w14`, `w15`, `w16*`) are not in the subset schema and may trigger warnings:
|
||||
|
||||
```
|
||||
WARNING: Element '{http://schemas.microsoft.com/office/word/2010/wordml}shadow' is not expected.
|
||||
```
|
||||
|
||||
These are generally safe to ignore — they are Microsoft extensions for newer features (e.g., advanced text effects, comment extensions).
|
||||
|
||||
### Markup Compatibility
|
||||
|
||||
Documents may contain `mc:AlternateContent` blocks with fallback content. The subset schema may not recognize the `mc:` namespace processing. These are safe if the document opens correctly in Word.
|
||||
|
||||
### Recommended Approach
|
||||
|
||||
1. Run validation
|
||||
2. Treat **errors** as must-fix
|
||||
3. Review **warnings** — ignore known vendor extensions, investigate unknown elements
|
||||
4. After fixing errors, re-validate to confirm
|
||||
40
skills/minimax-docx/scripts/doc_to_docx.sh
Executable file
40
skills/minimax-docx/scripts/doc_to_docx.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $(basename "$0") <file.doc> [output_directory]"
|
||||
echo "Convert .doc to .docx using LibreOffice."
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
INPUT="$1"
|
||||
OUTDIR="${2:-.}"
|
||||
|
||||
if [ ! -f "$INPUT" ]; then
|
||||
echo "Error: File not found: $INPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v soffice &>/dev/null; then
|
||||
echo "Error: soffice (LibreOffice) is required for .doc conversion but not found."
|
||||
echo "Install LibreOffice: brew install --cask libreoffice"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BASENAME=$(basename "$INPUT" .doc)
|
||||
mkdir -p "$OUTDIR"
|
||||
|
||||
echo "Converting: $INPUT -> $OUTDIR/$BASENAME.docx"
|
||||
soffice --headless --convert-to docx --outdir "$OUTDIR" "$INPUT" >/dev/null 2>&1
|
||||
|
||||
OUTPUT="$OUTDIR/$BASENAME.docx"
|
||||
if [ ! -f "$OUTPUT" ]; then
|
||||
echo "Error: Conversion failed. Output file not created: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Success: $OUTPUT"
|
||||
37
skills/minimax-docx/scripts/docx_preview.sh
Executable file
37
skills/minimax-docx/scripts/docx_preview.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $(basename "$0") <file.docx>"
|
||||
echo "Preview DOCX content as plain text."
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
INPUT="$1"
|
||||
|
||||
if [ ! -f "$INPUT" ]; then
|
||||
echo "Error: File not found: $INPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FILE_SIZE=$(du -h "$INPUT" | cut -f1)
|
||||
echo "=== DOCX Preview: $(basename "$INPUT") ==="
|
||||
echo "File size: $FILE_SIZE"
|
||||
|
||||
if command -v pandoc &>/dev/null; then
|
||||
CONTENT=$(pandoc -f docx -t plain "$INPUT" 2>/dev/null)
|
||||
WORD_COUNT=$(echo "$CONTENT" | wc -w | tr -d ' ')
|
||||
EST_PAGES=$(( (WORD_COUNT + 249) / 250 ))
|
||||
echo "Word count: $WORD_COUNT"
|
||||
echo "Estimated pages: $EST_PAGES"
|
||||
echo "---"
|
||||
echo "$CONTENT"
|
||||
else
|
||||
echo "(pandoc not available, falling back to raw XML extract)"
|
||||
echo "---"
|
||||
unzip -p "$INPUT" word/document.xml 2>/dev/null | head -100
|
||||
fi
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MiniMaxAIDocx.Core\MiniMaxAIDocx.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.CommandLine;
|
||||
using MiniMaxAIDocx.Core.Commands;
|
||||
|
||||
var rootCommand = new RootCommand("minimax-docx: OpenXML document generation and manipulation CLI");
|
||||
|
||||
// Scenario commands
|
||||
rootCommand.Add(CreateCommand.Create());
|
||||
rootCommand.Add(EditContentCommand.Create());
|
||||
rootCommand.Add(ApplyTemplateCommand.Create());
|
||||
|
||||
// Tool commands
|
||||
rootCommand.Add(ValidateCommand.Create());
|
||||
rootCommand.Add(MergeRunsCommand.Create());
|
||||
rootCommand.Add(FixOrderCommand.Create());
|
||||
rootCommand.Add(AnalyzeCommand.Create());
|
||||
rootCommand.Add(DiffCommand.Create());
|
||||
|
||||
return rootCommand.Parse(args).Invoke();
|
||||
@@ -0,0 +1,147 @@
|
||||
using System.CommandLine;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Commands;
|
||||
|
||||
public static class AnalyzeCommand
|
||||
{
|
||||
private static readonly XNamespace W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||
private static readonly XNamespace WP = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
|
||||
|
||||
public static Command Create()
|
||||
{
|
||||
var inputOption = new Option<string>("--input") { Description = "DOCX file to analyze", Required = true };
|
||||
var jsonOption = new Option<bool>("--json") { Description = "Output as JSON" };
|
||||
|
||||
var cmd = new Command("analyze", "Analyze document structure and styles")
|
||||
{
|
||||
inputOption, jsonOption
|
||||
};
|
||||
|
||||
cmd.SetAction((parseResult) =>
|
||||
{
|
||||
var input = parseResult.GetValue(inputOption)!;
|
||||
var asJson = parseResult.GetValue(jsonOption);
|
||||
|
||||
if (!File.Exists(input))
|
||||
{
|
||||
Console.Error.WriteLine($"File not found: {input}");
|
||||
return;
|
||||
}
|
||||
|
||||
using var zip = ZipFile.OpenRead(input);
|
||||
var docEntry = zip.GetEntry("word/document.xml");
|
||||
if (docEntry == null)
|
||||
{
|
||||
Console.Error.WriteLine("Not a valid DOCX");
|
||||
return;
|
||||
}
|
||||
|
||||
XDocument doc;
|
||||
using (var stream = docEntry.Open())
|
||||
doc = XDocument.Load(stream);
|
||||
|
||||
var body = doc.Root?.Element(W + "body");
|
||||
if (body == null) return;
|
||||
|
||||
// Sections
|
||||
var sections = body.Descendants(W + "sectPr").ToList();
|
||||
var sectionBreaks = sections.Select(s => (string?)s.Element(W + "type")?.Attribute(W + "val") ?? "nextPage").ToList();
|
||||
|
||||
// Headings
|
||||
var headings = new List<object>();
|
||||
foreach (var p in body.Descendants(W + "p"))
|
||||
{
|
||||
var style = (string?)p.Element(W + "pPr")?.Element(W + "pStyle")?.Attribute(W + "val");
|
||||
if (style?.StartsWith("Heading", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
var text = string.Concat(p.Descendants(W + "t").Select(t => t.Value));
|
||||
headings.Add(new { style, text });
|
||||
}
|
||||
}
|
||||
|
||||
// Tables
|
||||
var tables = body.Descendants(W + "tbl").Select(tbl => new
|
||||
{
|
||||
rows = tbl.Elements(W + "tr").Count(),
|
||||
cols = tbl.Elements(W + "tr").FirstOrDefault()?.Elements(W + "tc").Count() ?? 0
|
||||
}).ToList();
|
||||
|
||||
// Images
|
||||
var images = body.Descendants(W + "drawing").Count();
|
||||
|
||||
// Headers/footers
|
||||
var headerRefs = sections.SelectMany(s => s.Elements(W + "headerReference")).Count();
|
||||
var footerRefs = sections.SelectMany(s => s.Elements(W + "footerReference")).Count();
|
||||
|
||||
// Paragraphs and word count
|
||||
var paragraphs = body.Descendants(W + "p").ToList();
|
||||
var allText = string.Concat(body.Descendants(W + "t").Select(t => t.Value));
|
||||
var wordCount = allText.Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries).Length;
|
||||
|
||||
// XML file sizes
|
||||
var fileSizes = zip.Entries
|
||||
.Where(e => e.FullName.StartsWith("word/") && e.FullName.EndsWith(".xml"))
|
||||
.Select(e => new { file = e.FullName, size = e.Length })
|
||||
.OrderByDescending(e => e.size)
|
||||
.ToList();
|
||||
|
||||
// Styles
|
||||
var styleNames = new List<string>();
|
||||
var stylesEntry = zip.GetEntry("word/styles.xml");
|
||||
if (stylesEntry != null)
|
||||
{
|
||||
using var stream = stylesEntry.Open();
|
||||
var stylesDoc = XDocument.Load(stream);
|
||||
styleNames = stylesDoc.Descendants(W + "style")
|
||||
.Where(s => (string?)s.Attribute(W + "customStyle") == "1")
|
||||
.Select(s => (string?)s.Attribute(W + "styleId") ?? "")
|
||||
.Where(s => s != "")
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var analysis = new
|
||||
{
|
||||
sections = new { count = sections.Count, breakTypes = sectionBreaks },
|
||||
headings,
|
||||
tables = new { count = tables.Count, details = tables },
|
||||
images,
|
||||
headerFooter = new { headers = headerRefs, footers = footerRefs },
|
||||
paragraphs = paragraphs.Count,
|
||||
estimatedWordCount = wordCount,
|
||||
xmlFileSizes = fileSizes,
|
||||
customStyles = new { count = styleNames.Count, names = styleNames }
|
||||
};
|
||||
|
||||
if (asJson)
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(analysis, new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Sections: {sections.Count} ({string.Join(", ", sectionBreaks)})");
|
||||
Console.WriteLine($"Headings: {headings.Count}");
|
||||
foreach (var h in headings)
|
||||
Console.WriteLine($" {h}");
|
||||
Console.WriteLine($"Tables: {tables.Count}");
|
||||
foreach (var t in tables)
|
||||
Console.WriteLine($" {t.rows} rows x {t.cols} cols");
|
||||
Console.WriteLine($"Images: {images}");
|
||||
Console.WriteLine($"Headers: {headerRefs}");
|
||||
Console.WriteLine($"Footers: {footerRefs}");
|
||||
Console.WriteLine($"Paragraphs: {paragraphs.Count}");
|
||||
Console.WriteLine($"Word count: ~{wordCount}");
|
||||
Console.WriteLine($"Custom styles: {styleNames.Count}");
|
||||
foreach (var s in styleNames)
|
||||
Console.WriteLine($" {s}");
|
||||
Console.WriteLine("XML file sizes:");
|
||||
foreach (var f in fileSizes)
|
||||
Console.WriteLine($" {f.file}: {f.size:N0} bytes");
|
||||
}
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
using System.CommandLine;
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Scenario C: Apply formatting from a template DOCX to a source DOCX.
|
||||
/// Copies styles, theme, numbering, headers/footers, and section properties
|
||||
/// from the template while preserving all content from the source.
|
||||
/// </summary>
|
||||
public static class ApplyTemplateCommand
|
||||
{
|
||||
public static Command Create()
|
||||
{
|
||||
var inputOpt = new Option<string>("--input") { Description = "Source DOCX (content to keep)", Required = true };
|
||||
var templateOpt = new Option<string>("--template") { Description = "Template DOCX (formatting to apply)", Required = true };
|
||||
var outputOpt = new Option<string>("--output") { Description = "Output DOCX file path", Required = true };
|
||||
var applyStylesOpt = new Option<bool>("--apply-styles") { Description = "Copy styles.xml from template" };
|
||||
applyStylesOpt.DefaultValueFactory = _ => true;
|
||||
var applyThemeOpt = new Option<bool>("--apply-theme") { Description = "Copy theme from template" };
|
||||
applyThemeOpt.DefaultValueFactory = _ => true;
|
||||
var applyNumberingOpt = new Option<bool>("--apply-numbering") { Description = "Copy numbering.xml from template" };
|
||||
applyNumberingOpt.DefaultValueFactory = _ => true;
|
||||
var applyHeadersFootersOpt = new Option<bool>("--apply-headers-footers") { Description = "Copy headers/footers from template" };
|
||||
var applySectionsOpt = new Option<bool>("--apply-sections") { Description = "Apply section properties from template" };
|
||||
applySectionsOpt.DefaultValueFactory = _ => true;
|
||||
|
||||
var cmd = new Command("apply-template", "Apply template formatting to a DOCX")
|
||||
{
|
||||
inputOpt, templateOpt, outputOpt, applyStylesOpt, applyThemeOpt,
|
||||
applyNumberingOpt, applyHeadersFootersOpt, applySectionsOpt
|
||||
};
|
||||
|
||||
cmd.SetAction((parseResult) =>
|
||||
{
|
||||
var inputPath = parseResult.GetValue(inputOpt)!;
|
||||
var templatePath = parseResult.GetValue(templateOpt)!;
|
||||
var outputPath = parseResult.GetValue(outputOpt)!;
|
||||
var applyStyles = parseResult.GetValue(applyStylesOpt);
|
||||
var applyTheme = parseResult.GetValue(applyThemeOpt);
|
||||
var applyNumbering = parseResult.GetValue(applyNumberingOpt);
|
||||
var applyHeadersFooters = parseResult.GetValue(applyHeadersFootersOpt);
|
||||
var applySections = parseResult.GetValue(applySectionsOpt);
|
||||
|
||||
if (!File.Exists(inputPath)) { Console.Error.WriteLine($"Input file not found: {inputPath}"); return; }
|
||||
if (!File.Exists(templatePath)) { Console.Error.WriteLine($"Template file not found: {templatePath}"); return; }
|
||||
|
||||
// Create output as a copy of the source
|
||||
File.Copy(inputPath, outputPath, overwrite: true);
|
||||
|
||||
using var output = WordprocessingDocument.Open(outputPath, true);
|
||||
using var template = WordprocessingDocument.Open(templatePath, false);
|
||||
|
||||
var outputMain = output.MainDocumentPart;
|
||||
var templateMain = template.MainDocumentPart;
|
||||
if (outputMain == null || templateMain == null)
|
||||
{
|
||||
Console.Error.WriteLine("Invalid document: missing main document part.");
|
||||
return;
|
||||
}
|
||||
|
||||
int appliedCount = 0;
|
||||
|
||||
if (applyStyles)
|
||||
{
|
||||
CopyStyles(templateMain, outputMain);
|
||||
appliedCount++;
|
||||
Console.WriteLine(" Applied: styles");
|
||||
}
|
||||
|
||||
if (applyTheme)
|
||||
{
|
||||
CopyTheme(templateMain, outputMain);
|
||||
appliedCount++;
|
||||
Console.WriteLine(" Applied: theme");
|
||||
}
|
||||
|
||||
if (applyNumbering)
|
||||
{
|
||||
CopyNumbering(templateMain, outputMain);
|
||||
appliedCount++;
|
||||
Console.WriteLine(" Applied: numbering");
|
||||
}
|
||||
|
||||
if (applyHeadersFooters)
|
||||
{
|
||||
CopyHeadersAndFooters(templateMain, outputMain);
|
||||
appliedCount++;
|
||||
Console.WriteLine(" Applied: headers/footers");
|
||||
}
|
||||
|
||||
if (applySections)
|
||||
{
|
||||
CopySectionProperties(templateMain, outputMain);
|
||||
appliedCount++;
|
||||
Console.WriteLine(" Applied: section properties");
|
||||
}
|
||||
|
||||
outputMain.Document.Save();
|
||||
Console.WriteLine($"Applied {appliedCount} formatting component(s) from template to {outputPath}");
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the output's StyleDefinitionsPart with the template's version.
|
||||
/// </summary>
|
||||
private static void CopyStyles(MainDocumentPart template, MainDocumentPart output)
|
||||
{
|
||||
var templateStyles = template.StyleDefinitionsPart;
|
||||
if (templateStyles == null) return;
|
||||
|
||||
if (output.StyleDefinitionsPart != null)
|
||||
output.DeletePart(output.StyleDefinitionsPart);
|
||||
|
||||
var newStylesPart = output.AddNewPart<StyleDefinitionsPart>();
|
||||
|
||||
using var stream = templateStyles.GetStream(FileMode.Open, FileAccess.Read);
|
||||
newStylesPart.FeedData(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the output's ThemePart with the template's version.
|
||||
/// </summary>
|
||||
private static void CopyTheme(MainDocumentPart template, MainDocumentPart output)
|
||||
{
|
||||
var templateTheme = template.ThemePart;
|
||||
if (templateTheme == null) return;
|
||||
|
||||
if (output.ThemePart != null)
|
||||
output.DeletePart(output.ThemePart);
|
||||
|
||||
var newThemePart = output.AddNewPart<ThemePart>();
|
||||
|
||||
using var stream = templateTheme.GetStream(FileMode.Open, FileAccess.Read);
|
||||
newThemePart.FeedData(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies numbering definitions from template, remapping numbering IDs
|
||||
/// referenced in the output document's paragraphs.
|
||||
/// </summary>
|
||||
private static void CopyNumbering(MainDocumentPart template, MainDocumentPart output)
|
||||
{
|
||||
var templateNumbering = template.NumberingDefinitionsPart;
|
||||
if (templateNumbering == null) return;
|
||||
|
||||
var referencedNumIds = new HashSet<string>();
|
||||
var body = output.Document.Body;
|
||||
if (body != null)
|
||||
{
|
||||
foreach (var numId in body.Descendants<NumberingId>())
|
||||
{
|
||||
if (numId.Val?.Value != null)
|
||||
referencedNumIds.Add(numId.Val.Value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
if (output.NumberingDefinitionsPart != null)
|
||||
output.DeletePart(output.NumberingDefinitionsPart);
|
||||
|
||||
var newNumberingPart = output.AddNewPart<NumberingDefinitionsPart>();
|
||||
|
||||
using var stream = templateNumbering.GetStream(FileMode.Open, FileAccess.Read);
|
||||
newNumberingPart.FeedData(stream);
|
||||
|
||||
if (referencedNumIds.Count > 0)
|
||||
{
|
||||
Console.WriteLine($" Note: {referencedNumIds.Count} numbering reference(s) in document content mapped to template definitions.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies headers and footers from the template, remapping relationship IDs.
|
||||
/// </summary>
|
||||
private static void CopyHeadersAndFooters(MainDocumentPart template, MainDocumentPart output)
|
||||
{
|
||||
var outputBody = output.Document.Body;
|
||||
if (outputBody == null) return;
|
||||
|
||||
// Remove existing header/footer parts from output
|
||||
foreach (var hp in output.HeaderParts.ToList())
|
||||
output.DeletePart(hp);
|
||||
foreach (var fp in output.FooterParts.ToList())
|
||||
output.DeletePart(fp);
|
||||
|
||||
// Remove existing header/footer references from all section properties
|
||||
foreach (var sectPr in outputBody.Descendants<SectionProperties>())
|
||||
{
|
||||
foreach (var hr in sectPr.Elements<HeaderReference>().ToList())
|
||||
hr.Remove();
|
||||
foreach (var fr in sectPr.Elements<FooterReference>().ToList())
|
||||
fr.Remove();
|
||||
}
|
||||
|
||||
var templateBody = template.Document?.Body;
|
||||
if (templateBody == null) return;
|
||||
|
||||
var templateFinalSectPr = templateBody.Descendants<SectionProperties>().LastOrDefault();
|
||||
if (templateFinalSectPr == null) return;
|
||||
|
||||
var outputFinalSectPr = outputBody.Descendants<SectionProperties>().LastOrDefault();
|
||||
if (outputFinalSectPr == null)
|
||||
{
|
||||
outputFinalSectPr = new SectionProperties();
|
||||
outputBody.Append(outputFinalSectPr);
|
||||
}
|
||||
|
||||
// Copy headers
|
||||
foreach (var headerRef in templateFinalSectPr.Elements<HeaderReference>())
|
||||
{
|
||||
var templateHeaderPart = template.GetPartById(headerRef.Id!) as HeaderPart;
|
||||
if (templateHeaderPart == null) continue;
|
||||
|
||||
var newHeaderPart = output.AddNewPart<HeaderPart>();
|
||||
using (var stream = templateHeaderPart.GetStream(FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
newHeaderPart.FeedData(stream);
|
||||
}
|
||||
|
||||
CopyPartRelationships(templateHeaderPart, newHeaderPart);
|
||||
|
||||
var newRefId = output.GetIdOfPart(newHeaderPart);
|
||||
outputFinalSectPr.InsertAt(new HeaderReference
|
||||
{
|
||||
Type = headerRef.Type,
|
||||
Id = newRefId
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Copy footers
|
||||
foreach (var footerRef in templateFinalSectPr.Elements<FooterReference>())
|
||||
{
|
||||
var templateFooterPart = template.GetPartById(footerRef.Id!) as FooterPart;
|
||||
if (templateFooterPart == null) continue;
|
||||
|
||||
var newFooterPart = output.AddNewPart<FooterPart>();
|
||||
using (var stream = templateFooterPart.GetStream(FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
newFooterPart.FeedData(stream);
|
||||
}
|
||||
|
||||
CopyPartRelationships(templateFooterPart, newFooterPart);
|
||||
|
||||
var newRefId = output.GetIdOfPart(newFooterPart);
|
||||
var lastHeaderRef = outputFinalSectPr.Elements<HeaderReference>().LastOrDefault();
|
||||
if (lastHeaderRef != null)
|
||||
lastHeaderRef.InsertAfterSelf(new FooterReference { Type = footerRef.Type, Id = newRefId });
|
||||
else
|
||||
outputFinalSectPr.InsertAt(new FooterReference { Type = footerRef.Type, Id = newRefId }, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies sub-relationships (images, etc.) from a source part to a target part.
|
||||
/// </summary>
|
||||
private static void CopyPartRelationships(OpenXmlPart source, OpenXmlPart target)
|
||||
{
|
||||
foreach (var rel in source.ExternalRelationships)
|
||||
{
|
||||
target.AddExternalRelationship(rel.RelationshipType, rel.Uri, rel.Id);
|
||||
}
|
||||
|
||||
foreach (var childPart in source.Parts)
|
||||
{
|
||||
try
|
||||
{
|
||||
var contentType = childPart.OpenXmlPart.ContentType;
|
||||
if (contentType.StartsWith("image/"))
|
||||
{
|
||||
var newChild = target.AddNewPart<ImagePart>(contentType, childPart.RelationshipId);
|
||||
using var stream = childPart.OpenXmlPart.GetStream(FileMode.Open, FileAccess.Read);
|
||||
newChild.FeedData(stream);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[WARN] Skipped non-image embedded part: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies page size, margins, columns, and document grid from template section properties.
|
||||
/// </summary>
|
||||
private static void CopySectionProperties(MainDocumentPart template, MainDocumentPart output)
|
||||
{
|
||||
var templateBody = template.Document?.Body;
|
||||
var outputBody = output.Document?.Body;
|
||||
if (templateBody == null || outputBody == null) return;
|
||||
|
||||
var templateSectPr = templateBody.Descendants<SectionProperties>().LastOrDefault();
|
||||
if (templateSectPr == null) return;
|
||||
|
||||
var outputSectPr = outputBody.Descendants<SectionProperties>().LastOrDefault();
|
||||
if (outputSectPr == null)
|
||||
{
|
||||
outputSectPr = new SectionProperties();
|
||||
outputBody.Append(outputSectPr);
|
||||
}
|
||||
|
||||
CopyChildElement<PageSize>(templateSectPr, outputSectPr);
|
||||
CopyChildElement<PageMargin>(templateSectPr, outputSectPr);
|
||||
CopyChildElement<Columns>(templateSectPr, outputSectPr);
|
||||
CopyChildElement<DocGrid>(templateSectPr, outputSectPr);
|
||||
CopyChildElement<PageBorders>(templateSectPr, outputSectPr);
|
||||
}
|
||||
|
||||
private static void CopyChildElement<T>(SectionProperties source, SectionProperties target) where T : OpenXmlElement
|
||||
{
|
||||
var sourceElement = source.GetFirstChild<T>();
|
||||
if (sourceElement == null) return;
|
||||
|
||||
var existing = target.GetFirstChild<T>();
|
||||
existing?.Remove();
|
||||
|
||||
target.Append((T)sourceElement.CloneNode(true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
using System.CommandLine;
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
using MiniMaxAIDocx.Core.OpenXml;
|
||||
using MiniMaxAIDocx.Core.Typography;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Scenario A: Create a new DOCX document from scratch with proper styles, sections,
|
||||
/// headers/footers, and typography defaults.
|
||||
/// </summary>
|
||||
public static class CreateCommand
|
||||
{
|
||||
public static Command Create()
|
||||
{
|
||||
var outputOption = new Option<string>("--output") { Description = "Output DOCX file path", Required = true };
|
||||
var typeOption = new Option<string>("--type") { Description = "Document type: report, letter, memo, academic" };
|
||||
typeOption.DefaultValueFactory = _ => "report";
|
||||
var titleOption = new Option<string>("--title") { Description = "Document title" };
|
||||
var authorOption = new Option<string>("--author") { Description = "Document author" };
|
||||
var pageSizeOption = new Option<string>("--page-size") { Description = "Page size: letter, a4, legal, a3" };
|
||||
pageSizeOption.DefaultValueFactory = _ => "letter";
|
||||
var marginsOption = new Option<string>("--margins") { Description = "Margin preset: standard, narrow, wide" };
|
||||
marginsOption.DefaultValueFactory = _ => "standard";
|
||||
var headerTextOption = new Option<string>("--header") { Description = "Header text" };
|
||||
var footerTextOption = new Option<string>("--footer") { Description = "Footer text" };
|
||||
var pageNumbersOption = new Option<bool>("--page-numbers") { Description = "Add page numbers in footer" };
|
||||
var tocOption = new Option<bool>("--toc") { Description = "Insert table of contents placeholder" };
|
||||
var contentJsonOption = new Option<string>("--content-json") { Description = "Path to JSON file describing document content" };
|
||||
|
||||
var cmd = new Command("create", "Create a new DOCX document from scratch")
|
||||
{
|
||||
outputOption, typeOption, titleOption, authorOption, pageSizeOption,
|
||||
marginsOption, headerTextOption, footerTextOption, pageNumbersOption,
|
||||
tocOption, contentJsonOption
|
||||
};
|
||||
|
||||
cmd.SetAction((parseResult) =>
|
||||
{
|
||||
var output = parseResult.GetValue(outputOption)!;
|
||||
var docType = parseResult.GetValue(typeOption) ?? "report";
|
||||
var title = parseResult.GetValue(titleOption);
|
||||
var author = parseResult.GetValue(authorOption);
|
||||
var pageSizeName = parseResult.GetValue(pageSizeOption) ?? "letter";
|
||||
var marginsName = parseResult.GetValue(marginsOption) ?? "standard";
|
||||
var headerText = parseResult.GetValue(headerTextOption);
|
||||
var footerText = parseResult.GetValue(footerTextOption);
|
||||
var pageNumbers = parseResult.GetValue(pageNumbersOption);
|
||||
var tocPlaceholder = parseResult.GetValue(tocOption);
|
||||
var contentJson = parseResult.GetValue(contentJsonOption);
|
||||
|
||||
var fontConfig = GetFontConfig(docType);
|
||||
var pageSize = GetPageSizeConfig(pageSizeName);
|
||||
var margins = GetMargins(marginsName);
|
||||
|
||||
using var doc = WordprocessingDocument.Create(output, WordprocessingDocumentType.Document);
|
||||
var mainPart = doc.AddMainDocumentPart();
|
||||
mainPart.Document = new Document(new Body());
|
||||
var body = mainPart.Document.Body!;
|
||||
|
||||
// Add styles part with defaults
|
||||
AddDefaultStyles(mainPart, fontConfig);
|
||||
|
||||
// Add section properties (page size, margins)
|
||||
var sectPr = new SectionProperties();
|
||||
sectPr.Append(new DocumentFormat.OpenXml.Wordprocessing.PageSize
|
||||
{
|
||||
Width = (UInt32Value)(uint)pageSize.WidthDxa,
|
||||
Height = (UInt32Value)(uint)pageSize.HeightDxa
|
||||
});
|
||||
sectPr.Append(new PageMargin
|
||||
{
|
||||
Top = margins.TopDxa,
|
||||
Bottom = margins.BottomDxa,
|
||||
Left = (UInt32Value)(uint)margins.LeftDxa,
|
||||
Right = (UInt32Value)(uint)margins.RightDxa
|
||||
});
|
||||
|
||||
// Add header if requested
|
||||
if (!string.IsNullOrEmpty(headerText))
|
||||
{
|
||||
var headerPart = mainPart.AddNewPart<HeaderPart>();
|
||||
headerPart.Header = new Header(
|
||||
new Paragraph(new Run(new Text(headerText))));
|
||||
var headerRefId = mainPart.GetIdOfPart(headerPart);
|
||||
sectPr.Append(new HeaderReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
Id = headerRefId
|
||||
});
|
||||
}
|
||||
|
||||
// Add footer if requested
|
||||
if (!string.IsNullOrEmpty(footerText) || pageNumbers)
|
||||
{
|
||||
var footerPart = mainPart.AddNewPart<FooterPart>();
|
||||
var footerParagraph = new Paragraph();
|
||||
|
||||
if (!string.IsNullOrEmpty(footerText))
|
||||
{
|
||||
footerParagraph.Append(new Run(new Text(footerText)));
|
||||
}
|
||||
|
||||
if (pageNumbers)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(footerText))
|
||||
footerParagraph.Append(new Run(new Text(" — ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
footerParagraph.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.Begin }));
|
||||
footerParagraph.Append(new Run(
|
||||
new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
footerParagraph.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.End }));
|
||||
}
|
||||
|
||||
footerPart.Footer = new Footer(footerParagraph);
|
||||
var footerRefId = mainPart.GetIdOfPart(footerPart);
|
||||
sectPr.Append(new FooterReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
Id = footerRefId
|
||||
});
|
||||
}
|
||||
|
||||
// Title
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
var titlePara = new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "Title" }),
|
||||
new Run(new Text(title)));
|
||||
body.Append(titlePara);
|
||||
}
|
||||
|
||||
// Author subtitle
|
||||
if (!string.IsNullOrEmpty(author))
|
||||
{
|
||||
var authorPara = new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "Subtitle" }),
|
||||
new Run(new Text(author)));
|
||||
body.Append(authorPara);
|
||||
}
|
||||
|
||||
// TOC placeholder
|
||||
if (tocPlaceholder)
|
||||
{
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "TOCHeading" }),
|
||||
new Run(new Text("Table of Contents"))));
|
||||
|
||||
// Insert TOC field
|
||||
var tocPara = new Paragraph();
|
||||
tocPara.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }));
|
||||
tocPara.Append(new Run(new FieldCode(" TOC \\o \"1-3\" \\h \\z \\u ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
tocPara.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Separate }));
|
||||
tocPara.Append(new Run(new Text("Update this field to generate table of contents.")));
|
||||
tocPara.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.End }));
|
||||
body.Append(tocPara);
|
||||
|
||||
// Page break after TOC
|
||||
body.Append(new Paragraph(new Run(new Break { Type = BreakValues.Page })));
|
||||
}
|
||||
|
||||
// Content from JSON (if provided)
|
||||
if (!string.IsNullOrEmpty(contentJson) && File.Exists(contentJson))
|
||||
{
|
||||
var jsonContent = File.ReadAllText(contentJson);
|
||||
AddContentFromJson(body, jsonContent, fontConfig);
|
||||
}
|
||||
|
||||
// Ensure body has at least one paragraph
|
||||
if (!body.Elements<Paragraph>().Any())
|
||||
{
|
||||
body.Append(new Paragraph());
|
||||
}
|
||||
|
||||
// sectPr must be the last child of body
|
||||
body.Append(sectPr);
|
||||
|
||||
mainPart.Document.Save();
|
||||
Console.WriteLine($"Created {docType} document: {output}");
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private static FontConfig GetFontConfig(string docType) => docType.ToLowerInvariant() switch
|
||||
{
|
||||
"letter" => FontDefaults.Letter,
|
||||
"memo" => FontDefaults.Memo,
|
||||
"academic" => FontDefaults.Academic,
|
||||
_ => FontDefaults.Report,
|
||||
};
|
||||
|
||||
private static Typography.PageSize GetPageSizeConfig(string name) => name.ToLowerInvariant() switch
|
||||
{
|
||||
"a4" => PageSizes.A4,
|
||||
"legal" => PageSizes.Legal,
|
||||
"a3" => PageSizes.A3,
|
||||
_ => PageSizes.Letter,
|
||||
};
|
||||
|
||||
private static MarginConfig GetMargins(string name) => name.ToLowerInvariant() switch
|
||||
{
|
||||
"narrow" => PageSizes.NarrowMargins,
|
||||
"wide" => PageSizes.WideMargins,
|
||||
_ => PageSizes.StandardMargins,
|
||||
};
|
||||
|
||||
private static void AddDefaultStyles(MainDocumentPart mainPart, FontConfig fontConfig)
|
||||
{
|
||||
var stylesPart = mainPart.AddNewPart<StyleDefinitionsPart>();
|
||||
var styles = new Styles();
|
||||
|
||||
// Default run properties
|
||||
var defaultRPr = new StyleRunProperties(
|
||||
new RunFonts { Ascii = fontConfig.BodyFont, HighAnsi = fontConfig.BodyFont },
|
||||
new FontSize { Val = UnitConverter.FontSizeToSz(fontConfig.BodySize) },
|
||||
new FontSizeComplexScript { Val = UnitConverter.FontSizeToSz(fontConfig.BodySize) });
|
||||
|
||||
// Normal style
|
||||
styles.Append(new Style(
|
||||
new StyleName { Val = "Normal" },
|
||||
new PrimaryStyle(),
|
||||
defaultRPr)
|
||||
{ Type = StyleValues.Paragraph, StyleId = "Normal", Default = true });
|
||||
|
||||
// Heading styles 1-6
|
||||
double[] headingSizes = [fontConfig.Heading1Size, fontConfig.Heading2Size, fontConfig.Heading3Size,
|
||||
fontConfig.Heading4Size, fontConfig.Heading5Size, fontConfig.Heading6Size];
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
var level = i + 1;
|
||||
var headingStyle = new Style(
|
||||
new StyleName { Val = $"heading {level}" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new NextParagraphStyle { Val = "Normal" },
|
||||
new PrimaryStyle(),
|
||||
new StyleParagraphProperties(
|
||||
new KeepNext(),
|
||||
new KeepLines(),
|
||||
new SpacingBetweenLines { Before = "240", After = "120" },
|
||||
new OutlineLevel { Val = i }),
|
||||
new StyleRunProperties(
|
||||
new RunFonts { Ascii = fontConfig.HeadingFont, HighAnsi = fontConfig.HeadingFont },
|
||||
new FontSize { Val = UnitConverter.FontSizeToSz(headingSizes[i]) },
|
||||
new FontSizeComplexScript { Val = UnitConverter.FontSizeToSz(headingSizes[i]) },
|
||||
new Bold()))
|
||||
{ Type = StyleValues.Paragraph, StyleId = $"Heading{level}" };
|
||||
styles.Append(headingStyle);
|
||||
}
|
||||
|
||||
// Title style
|
||||
styles.Append(new Style(
|
||||
new StyleName { Val = "Title" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new NextParagraphStyle { Val = "Normal" },
|
||||
new PrimaryStyle(),
|
||||
new StyleParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center },
|
||||
new SpacingBetweenLines { After = "300" }),
|
||||
new StyleRunProperties(
|
||||
new RunFonts { Ascii = fontConfig.HeadingFont, HighAnsi = fontConfig.HeadingFont },
|
||||
new FontSize { Val = UnitConverter.FontSizeToSz(fontConfig.Heading1Size + 6) },
|
||||
new FontSizeComplexScript { Val = UnitConverter.FontSizeToSz(fontConfig.Heading1Size + 6) }))
|
||||
{ Type = StyleValues.Paragraph, StyleId = "Title" });
|
||||
|
||||
// Subtitle style
|
||||
styles.Append(new Style(
|
||||
new StyleName { Val = "Subtitle" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new NextParagraphStyle { Val = "Normal" },
|
||||
new StyleParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center },
|
||||
new SpacingBetweenLines { After = "200" }),
|
||||
new StyleRunProperties(
|
||||
new Color { Val = "5A5A5A" },
|
||||
new FontSize { Val = UnitConverter.FontSizeToSz(fontConfig.BodySize + 2) }))
|
||||
{ Type = StyleValues.Paragraph, StyleId = "Subtitle" });
|
||||
|
||||
stylesPart.Styles = styles;
|
||||
stylesPart.Styles.Save();
|
||||
}
|
||||
|
||||
private static void AddContentFromJson(Body body, string jsonContent, FontConfig fontConfig)
|
||||
{
|
||||
// Simple JSON content format: array of {type, text, level?}
|
||||
// e.g. [{"type":"heading","text":"Introduction","level":1},{"type":"paragraph","text":"..."}]
|
||||
try
|
||||
{
|
||||
using var jsonDoc = System.Text.Json.JsonDocument.Parse(jsonContent);
|
||||
foreach (var element in jsonDoc.RootElement.EnumerateArray())
|
||||
{
|
||||
var type = element.GetProperty("type").GetString() ?? "paragraph";
|
||||
var text = element.GetProperty("text").GetString() ?? "";
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "heading":
|
||||
var level = element.TryGetProperty("level", out var lvl) ? lvl.GetInt32() : 1;
|
||||
level = Math.Clamp(level, 1, 6);
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = $"Heading{level}" }),
|
||||
new Run(new Text(text))));
|
||||
break;
|
||||
|
||||
case "paragraph":
|
||||
body.Append(new Paragraph(new Run(new Text(text))));
|
||||
break;
|
||||
|
||||
case "pagebreak":
|
||||
body.Append(new Paragraph(new Run(new Break { Type = BreakValues.Page })));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Text.Json.JsonException ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Warning: could not parse content JSON: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using System.CommandLine;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Commands;
|
||||
|
||||
public static class DiffCommand
|
||||
{
|
||||
private static readonly XNamespace W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||
|
||||
public static Command Create()
|
||||
{
|
||||
var beforeOption = new Option<string>("--before") { Description = "Original DOCX", Required = true };
|
||||
var afterOption = new Option<string>("--after") { Description = "Modified DOCX", Required = true };
|
||||
var jsonOption = new Option<bool>("--json") { Description = "Output as JSON" };
|
||||
|
||||
var cmd = new Command("diff", "Compare two DOCX files")
|
||||
{
|
||||
beforeOption, afterOption, jsonOption
|
||||
};
|
||||
|
||||
cmd.SetAction((parseResult) =>
|
||||
{
|
||||
var before = parseResult.GetValue(beforeOption)!;
|
||||
var after = parseResult.GetValue(afterOption)!;
|
||||
var asJson = parseResult.GetValue(jsonOption);
|
||||
|
||||
if (!File.Exists(before)) { Console.Error.WriteLine($"File not found: {before}"); return; }
|
||||
if (!File.Exists(after)) { Console.Error.WriteLine($"File not found: {after}"); return; }
|
||||
|
||||
var beforeParas = ExtractParagraphs(before);
|
||||
var afterParas = ExtractParagraphs(after);
|
||||
var beforeStyles = ExtractStyleIds(before);
|
||||
var afterStyles = ExtractStyleIds(after);
|
||||
var beforeStructure = ExtractStructure(before);
|
||||
var afterStructure = ExtractStructure(after);
|
||||
|
||||
// Text diff
|
||||
var textChanges = new List<object>();
|
||||
int maxLen = Math.Max(beforeParas.Count, afterParas.Count);
|
||||
int changedParas = 0;
|
||||
for (int i = 0; i < maxLen; i++)
|
||||
{
|
||||
var bText = i < beforeParas.Count ? beforeParas[i] : null;
|
||||
var aText = i < afterParas.Count ? afterParas[i] : null;
|
||||
|
||||
if (bText != aText)
|
||||
{
|
||||
changedParas++;
|
||||
textChanges.Add(new
|
||||
{
|
||||
paragraph = i + 1,
|
||||
before = bText ?? "(absent)",
|
||||
after = aText ?? "(absent)"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Style diff
|
||||
var addedStyles = afterStyles.Except(beforeStyles).ToList();
|
||||
var removedStyles = beforeStyles.Except(afterStyles).ToList();
|
||||
|
||||
// Structure diff
|
||||
var structureChanges = new List<string>();
|
||||
if (beforeStructure.Sections != afterStructure.Sections)
|
||||
structureChanges.Add($"Sections: {beforeStructure.Sections} -> {afterStructure.Sections}");
|
||||
if (beforeStructure.Tables != afterStructure.Tables)
|
||||
structureChanges.Add($"Tables: {beforeStructure.Tables} -> {afterStructure.Tables}");
|
||||
if (beforeStructure.Images != afterStructure.Images)
|
||||
structureChanges.Add($"Images: {beforeStructure.Images} -> {afterStructure.Images}");
|
||||
|
||||
var result = new
|
||||
{
|
||||
textChanges,
|
||||
styleChanges = new { added = addedStyles, removed = removedStyles },
|
||||
structureChanges,
|
||||
summary = $"{changedParas} paragraphs changed, {addedStyles.Count + removedStyles.Count} styles modified, {structureChanges.Count} structural changes"
|
||||
};
|
||||
|
||||
if (asJson)
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(result.summary);
|
||||
Console.WriteLine();
|
||||
|
||||
if (textChanges.Count > 0)
|
||||
{
|
||||
Console.WriteLine($"Text changes ({textChanges.Count}):");
|
||||
foreach (var tc in textChanges.Take(20))
|
||||
Console.WriteLine($" {tc}");
|
||||
if (textChanges.Count > 20)
|
||||
Console.WriteLine($" ... and {textChanges.Count - 20} more");
|
||||
}
|
||||
|
||||
if (addedStyles.Count > 0)
|
||||
Console.WriteLine($"Added styles: {string.Join(", ", addedStyles)}");
|
||||
if (removedStyles.Count > 0)
|
||||
Console.WriteLine($"Removed styles: {string.Join(", ", removedStyles)}");
|
||||
|
||||
foreach (var sc in structureChanges)
|
||||
Console.WriteLine($"Structure: {sc}");
|
||||
}
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private static List<string> ExtractParagraphs(string docxPath)
|
||||
{
|
||||
using var zip = ZipFile.OpenRead(docxPath);
|
||||
var entry = zip.GetEntry("word/document.xml");
|
||||
if (entry == null) return new();
|
||||
|
||||
using var stream = entry.Open();
|
||||
var doc = XDocument.Load(stream);
|
||||
return doc.Descendants(W + "p")
|
||||
.Select(p => string.Concat(p.Descendants(W + "t").Select(t => t.Value)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static HashSet<string> ExtractStyleIds(string docxPath)
|
||||
{
|
||||
using var zip = ZipFile.OpenRead(docxPath);
|
||||
var entry = zip.GetEntry("word/styles.xml");
|
||||
if (entry == null) return new();
|
||||
|
||||
using var stream = entry.Open();
|
||||
var doc = XDocument.Load(stream);
|
||||
return doc.Descendants(W + "style")
|
||||
.Select(s => (string?)s.Attribute(W + "styleId"))
|
||||
.Where(id => id != null)
|
||||
.ToHashSet()!;
|
||||
}
|
||||
|
||||
private record StructureInfo(int Sections, int Tables, int Images);
|
||||
|
||||
private static StructureInfo ExtractStructure(string docxPath)
|
||||
{
|
||||
using var zip = ZipFile.OpenRead(docxPath);
|
||||
var entry = zip.GetEntry("word/document.xml");
|
||||
if (entry == null) return new(0, 0, 0);
|
||||
|
||||
using var stream = entry.Open();
|
||||
var doc = XDocument.Load(stream);
|
||||
return new(
|
||||
doc.Descendants(W + "sectPr").Count(),
|
||||
doc.Descendants(W + "tbl").Count(),
|
||||
doc.Descendants(W + "drawing").Count()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
using System.CommandLine;
|
||||
using System.Text.RegularExpressions;
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
using MiniMaxAIDocx.Core.OpenXml;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Scenario B: Surgical content editing operations on existing DOCX files.
|
||||
/// Preserves all existing formatting and minimizes XML changes.
|
||||
/// </summary>
|
||||
public static class EditContentCommand
|
||||
{
|
||||
public static Command Create()
|
||||
{
|
||||
var cmd = new Command("edit", "Edit existing DOCX content");
|
||||
|
||||
cmd.Add(CreateReplaceTextCommand());
|
||||
cmd.Add(CreateFillTableCommand());
|
||||
cmd.Add(CreateInsertParagraphCommand());
|
||||
cmd.Add(CreateUpdateFieldCommand());
|
||||
cmd.Add(CreateListPlaceholdersCommand());
|
||||
cmd.Add(CreateFillPlaceholdersCommand());
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private static Command CreateReplaceTextCommand()
|
||||
{
|
||||
var inputOpt = new Option<string>("--input") { Description = "Input DOCX file", Required = true };
|
||||
var outputOpt = new Option<string>("--output") { Description = "Output file path (defaults to overwriting input)" };
|
||||
var searchOpt = new Option<string>("--search") { Description = "Text to search for", Required = true };
|
||||
var replaceOpt = new Option<string>("--replace") { Description = "Replacement text", Required = true };
|
||||
var regexOpt = new Option<bool>("--regex") { Description = "Treat search as a regex pattern" };
|
||||
|
||||
var cmd = new Command("replace-text", "Replace text while preserving formatting")
|
||||
{
|
||||
inputOpt, outputOpt, searchOpt, replaceOpt, regexOpt
|
||||
};
|
||||
|
||||
cmd.SetAction((parseResult) =>
|
||||
{
|
||||
var input = parseResult.GetValue(inputOpt)!;
|
||||
var output = parseResult.GetValue(outputOpt) ?? input;
|
||||
var search = parseResult.GetValue(searchOpt)!;
|
||||
var replace = parseResult.GetValue(replaceOpt)!;
|
||||
var useRegex = parseResult.GetValue(regexOpt);
|
||||
|
||||
if (output != input) File.Copy(input, output, overwrite: true);
|
||||
|
||||
using var doc = WordprocessingDocument.Open(output, true);
|
||||
var body = doc.MainDocumentPart?.Document.Body;
|
||||
if (body == null) { Console.Error.WriteLine("No document body found."); return; }
|
||||
|
||||
int count = 0;
|
||||
foreach (var paragraph in body.Descendants<Paragraph>())
|
||||
{
|
||||
count += ReplaceInParagraph(paragraph, search, replace, useRegex);
|
||||
}
|
||||
|
||||
doc.MainDocumentPart!.Document.Save();
|
||||
Console.WriteLine($"Replaced {count} occurrence(s) in {output}");
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private static Command CreateFillTableCommand()
|
||||
{
|
||||
var inputOpt = new Option<string>("--input") { Description = "Input DOCX file", Required = true };
|
||||
var outputOpt = new Option<string>("--output") { Description = "Output file path" };
|
||||
var tableIndexOpt = new Option<int>("--table-index") { Description = "Zero-based index of the table to fill" };
|
||||
tableIndexOpt.DefaultValueFactory = _ => 0;
|
||||
var csvOpt = new Option<string>("--csv") { Description = "CSV file with data to fill", Required = true };
|
||||
var appendOpt = new Option<bool>("--append") { Description = "Append rows instead of replacing existing data rows" };
|
||||
|
||||
var cmd = new Command("fill-table", "Fill a table with data from CSV")
|
||||
{
|
||||
inputOpt, outputOpt, tableIndexOpt, csvOpt, appendOpt
|
||||
};
|
||||
|
||||
cmd.SetAction((parseResult) =>
|
||||
{
|
||||
var input = parseResult.GetValue(inputOpt)!;
|
||||
var output = parseResult.GetValue(outputOpt) ?? input;
|
||||
var tableIndex = parseResult.GetValue(tableIndexOpt);
|
||||
var csvPath = parseResult.GetValue(csvOpt)!;
|
||||
var append = parseResult.GetValue(appendOpt);
|
||||
|
||||
if (output != input) File.Copy(input, output, overwrite: true);
|
||||
|
||||
if (!File.Exists(csvPath)) { Console.Error.WriteLine($"CSV file not found: {csvPath}"); return; }
|
||||
|
||||
using var doc = WordprocessingDocument.Open(output, true);
|
||||
var body = doc.MainDocumentPart?.Document.Body;
|
||||
if (body == null) { Console.Error.WriteLine("No document body found."); return; }
|
||||
|
||||
var tables = body.Elements<Table>().ToList();
|
||||
if (tableIndex >= tables.Count)
|
||||
{
|
||||
Console.Error.WriteLine($"Table index {tableIndex} out of range (found {tables.Count} tables).");
|
||||
return;
|
||||
}
|
||||
|
||||
var table = tables[tableIndex];
|
||||
var csvLines = File.ReadAllLines(csvPath);
|
||||
if (csvLines.Length == 0) { Console.WriteLine("CSV is empty, nothing to fill."); return; }
|
||||
|
||||
// Get template row properties from the first data row (second row, after header)
|
||||
var existingRows = table.Elements<TableRow>().ToList();
|
||||
TableRow? templateRow = existingRows.Count > 1 ? existingRows[1] : existingRows.FirstOrDefault();
|
||||
var templateTrPr = templateRow?.TableRowProperties?.CloneNode(true) as TableRowProperties;
|
||||
|
||||
if (!append)
|
||||
{
|
||||
// Remove all rows except the header row
|
||||
for (int i = existingRows.Count - 1; i >= 1; i--)
|
||||
existingRows[i].Remove();
|
||||
}
|
||||
|
||||
int rowsAdded = 0;
|
||||
// Skip header line in CSV (index 0)
|
||||
for (int i = 1; i < csvLines.Length; i++)
|
||||
{
|
||||
var values = ParseCsvLine(csvLines[i]);
|
||||
var newRow = new TableRow();
|
||||
if (templateTrPr != null)
|
||||
newRow.Append(templateTrPr.CloneNode(true));
|
||||
|
||||
foreach (var val in values)
|
||||
{
|
||||
var cell = new TableCell(
|
||||
new Paragraph(new Run(new Text(val))));
|
||||
newRow.Append(cell);
|
||||
}
|
||||
|
||||
table.Append(newRow);
|
||||
rowsAdded++;
|
||||
}
|
||||
|
||||
doc.MainDocumentPart!.Document.Save();
|
||||
Console.WriteLine($"Added {rowsAdded} rows to table {tableIndex} in {output}");
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private static Command CreateInsertParagraphCommand()
|
||||
{
|
||||
var inputOpt = new Option<string>("--input") { Description = "Input DOCX file", Required = true };
|
||||
var outputOpt = new Option<string>("--output") { Description = "Output file path" };
|
||||
var textOpt = new Option<string>("--text") { Description = "Paragraph text", Required = true };
|
||||
var styleOpt = new Option<string>("--style") { Description = "Paragraph style (e.g. Heading1, Normal)" };
|
||||
var afterOpt = new Option<int>("--after-paragraph") { Description = "Insert after this paragraph index (0-based)" };
|
||||
afterOpt.DefaultValueFactory = _ => -1; // -1 = append at end
|
||||
|
||||
var cmd = new Command("insert-paragraph", "Insert a new paragraph")
|
||||
{
|
||||
inputOpt, outputOpt, textOpt, styleOpt, afterOpt
|
||||
};
|
||||
|
||||
cmd.SetAction((parseResult) =>
|
||||
{
|
||||
var input = parseResult.GetValue(inputOpt)!;
|
||||
var output = parseResult.GetValue(outputOpt) ?? input;
|
||||
var text = parseResult.GetValue(textOpt)!;
|
||||
var style = parseResult.GetValue(styleOpt);
|
||||
var afterIndex = parseResult.GetValue(afterOpt);
|
||||
|
||||
if (output != input) File.Copy(input, output, overwrite: true);
|
||||
|
||||
using var doc = WordprocessingDocument.Open(output, true);
|
||||
var body = doc.MainDocumentPart?.Document.Body;
|
||||
if (body == null) { Console.Error.WriteLine("No document body found."); return; }
|
||||
|
||||
var newPara = new Paragraph();
|
||||
if (!string.IsNullOrEmpty(style))
|
||||
newPara.Append(new ParagraphProperties(new ParagraphStyleId { Val = style }));
|
||||
newPara.Append(new Run(new Text(text)));
|
||||
|
||||
var paragraphs = body.Elements<Paragraph>().ToList();
|
||||
if (afterIndex >= 0 && afterIndex < paragraphs.Count)
|
||||
{
|
||||
paragraphs[afterIndex].InsertAfterSelf(newPara);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Insert before sectPr if present, otherwise append
|
||||
var sectPr = body.Elements<SectionProperties>().FirstOrDefault();
|
||||
if (sectPr != null)
|
||||
sectPr.InsertBeforeSelf(newPara);
|
||||
else
|
||||
body.Append(newPara);
|
||||
}
|
||||
|
||||
doc.MainDocumentPart!.Document.Save();
|
||||
Console.WriteLine($"Inserted paragraph in {output}");
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private static Command CreateUpdateFieldCommand()
|
||||
{
|
||||
var inputOpt = new Option<string>("--input") { Description = "Input DOCX file", Required = true };
|
||||
var outputOpt = new Option<string>("--output") { Description = "Output file path" };
|
||||
var fieldNameOpt = new Option<string>("--field") { Description = "Document property field name (e.g. TITLE, AUTHOR)", Required = true };
|
||||
var valueOpt = new Option<string>("--value") { Description = "New field value", Required = true };
|
||||
|
||||
var cmd = new Command("update-field", "Update a document property field value")
|
||||
{
|
||||
inputOpt, outputOpt, fieldNameOpt, valueOpt
|
||||
};
|
||||
|
||||
cmd.SetAction((parseResult) =>
|
||||
{
|
||||
var input = parseResult.GetValue(inputOpt)!;
|
||||
var output = parseResult.GetValue(outputOpt) ?? input;
|
||||
var fieldName = parseResult.GetValue(fieldNameOpt)!;
|
||||
var value = parseResult.GetValue(valueOpt)!;
|
||||
|
||||
if (output != input) File.Copy(input, output, overwrite: true);
|
||||
|
||||
using var doc = WordprocessingDocument.Open(output, true);
|
||||
|
||||
// Update core properties
|
||||
var props = doc.PackageProperties;
|
||||
switch (fieldName.ToUpperInvariant())
|
||||
{
|
||||
case "TITLE": props.Title = value; break;
|
||||
case "AUTHOR": props.Creator = value; break;
|
||||
case "SUBJECT": props.Subject = value; break;
|
||||
case "KEYWORDS": props.Keywords = value; break;
|
||||
case "DESCRIPTION": props.Description = value; break;
|
||||
case "CATEGORY": props.Category = value; break;
|
||||
default:
|
||||
Console.Error.WriteLine($"Unknown field: {fieldName}. Supported: TITLE, AUTHOR, SUBJECT, KEYWORDS, DESCRIPTION, CATEGORY");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Updated {fieldName} to \"{value}\" in {output}");
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private static Command CreateListPlaceholdersCommand()
|
||||
{
|
||||
var inputOpt = new Option<string>("--input") { Description = "Input DOCX file", Required = true };
|
||||
var patternOpt = new Option<string>("--pattern") { Description = "Placeholder pattern (regex)" };
|
||||
patternOpt.DefaultValueFactory = _ => @"\{\{(\w+)\}\}"; // {{PLACEHOLDER}}
|
||||
|
||||
var cmd = new Command("list-placeholders", "List all placeholders found in the document")
|
||||
{
|
||||
inputOpt, patternOpt
|
||||
};
|
||||
|
||||
cmd.SetAction((parseResult) =>
|
||||
{
|
||||
var input = parseResult.GetValue(inputOpt)!;
|
||||
var pattern = parseResult.GetValue(patternOpt)!;
|
||||
|
||||
using var doc = WordprocessingDocument.Open(input, false);
|
||||
var body = doc.MainDocumentPart?.Document.Body;
|
||||
if (body == null) { Console.Error.WriteLine("No document body found."); return; }
|
||||
|
||||
var placeholders = new HashSet<string>();
|
||||
var regex = new Regex(pattern);
|
||||
|
||||
foreach (var paragraph in body.Descendants<Paragraph>())
|
||||
{
|
||||
var fullText = string.Concat(paragraph.Descendants<Text>().Select(t => t.Text));
|
||||
foreach (Match match in regex.Matches(fullText))
|
||||
{
|
||||
placeholders.Add(match.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (placeholders.Count == 0)
|
||||
{
|
||||
Console.WriteLine("No placeholders found.");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Found {placeholders.Count} unique placeholder(s):");
|
||||
foreach (var p in placeholders.OrderBy(x => x))
|
||||
Console.WriteLine($" {p}");
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private static Command CreateFillPlaceholdersCommand()
|
||||
{
|
||||
var inputOpt = new Option<string>("--input") { Description = "Input DOCX file", Required = true };
|
||||
var outputOpt = new Option<string>("--output") { Description = "Output file path" };
|
||||
var mappingOpt = new Option<string>("--mapping") { Description = "JSON file mapping placeholder names to values", Required = true };
|
||||
var patternOpt = new Option<string>("--pattern") { Description = "Placeholder pattern with capture group for the name" };
|
||||
patternOpt.DefaultValueFactory = _ => @"\{\{(\w+)\}\}";
|
||||
|
||||
var cmd = new Command("fill-placeholders", "Replace placeholders with values from a mapping file")
|
||||
{
|
||||
inputOpt, outputOpt, mappingOpt, patternOpt
|
||||
};
|
||||
|
||||
cmd.SetAction((parseResult) =>
|
||||
{
|
||||
var input = parseResult.GetValue(inputOpt)!;
|
||||
var output = parseResult.GetValue(outputOpt) ?? input;
|
||||
var mappingPath = parseResult.GetValue(mappingOpt)!;
|
||||
var pattern = parseResult.GetValue(patternOpt)!;
|
||||
|
||||
if (!File.Exists(mappingPath)) { Console.Error.WriteLine($"Mapping file not found: {mappingPath}"); return; }
|
||||
|
||||
var mappingJson = File.ReadAllText(mappingPath);
|
||||
Dictionary<string, string> mapping;
|
||||
try
|
||||
{
|
||||
mapping = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(mappingJson) ?? [];
|
||||
}
|
||||
catch (System.Text.Json.JsonException ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Invalid mapping JSON: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (output != input) File.Copy(input, output, overwrite: true);
|
||||
|
||||
using var doc = WordprocessingDocument.Open(output, true);
|
||||
var body = doc.MainDocumentPart?.Document.Body;
|
||||
if (body == null) { Console.Error.WriteLine("No document body found."); return; }
|
||||
|
||||
int totalReplacements = 0;
|
||||
var regex = new Regex(pattern);
|
||||
|
||||
foreach (var paragraph in body.Descendants<Paragraph>())
|
||||
{
|
||||
var fullText = string.Concat(paragraph.Descendants<Text>().Select(t => t.Text));
|
||||
var matches = regex.Matches(fullText);
|
||||
if (matches.Count == 0) continue;
|
||||
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
var placeholderName = match.Groups.Count > 1 ? match.Groups[1].Value : match.Value;
|
||||
if (mapping.TryGetValue(placeholderName, out var replacement))
|
||||
{
|
||||
totalReplacements += ReplaceInParagraph(paragraph, match.Value, replacement, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doc.MainDocumentPart!.Document.Save();
|
||||
Console.WriteLine($"Filled {totalReplacements} placeholder(s) in {output}");
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces text within a paragraph while preserving run formatting.
|
||||
/// Handles the case where search text may span multiple runs.
|
||||
/// </summary>
|
||||
private static int ReplaceInParagraph(Paragraph paragraph, string search, string replace, bool useRegex)
|
||||
{
|
||||
var runs = paragraph.Elements<Run>().ToList();
|
||||
if (runs.Count == 0) return 0;
|
||||
|
||||
// Build the full paragraph text and a map from character index to (run, position within run)
|
||||
var fullText = string.Concat(runs.SelectMany(r => r.Elements<Text>().Select(t => t.Text)));
|
||||
if (string.IsNullOrEmpty(fullText)) return 0;
|
||||
|
||||
int count = 0;
|
||||
|
||||
if (!useRegex)
|
||||
{
|
||||
// Simple case: search within each run first
|
||||
foreach (var run in runs)
|
||||
{
|
||||
foreach (var textElement in run.Elements<Text>().ToList())
|
||||
{
|
||||
if (textElement.Text.Contains(search))
|
||||
{
|
||||
var newText = textElement.Text.Replace(search, replace);
|
||||
count += (textElement.Text.Length - newText.Length + replace.Length - search.Length) == 0 ? 0 :
|
||||
CountOccurrences(textElement.Text, search);
|
||||
textElement.Text = newText;
|
||||
if (newText.StartsWith(' ') || newText.EndsWith(' '))
|
||||
textElement.Space = SpaceProcessingModeValues.Preserve;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle cross-run matches by concatenating all runs, replacing, and rebuilding
|
||||
if (count == 0 && fullText.Contains(search))
|
||||
{
|
||||
var newFullText = fullText.Replace(search, replace);
|
||||
count = CountOccurrences(fullText, search);
|
||||
RebuildRunsWithText(paragraph, runs, newFullText);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var regex = new Regex(search);
|
||||
if (regex.IsMatch(fullText))
|
||||
{
|
||||
count = regex.Matches(fullText).Count;
|
||||
var newFullText = regex.Replace(fullText, replace);
|
||||
RebuildRunsWithText(paragraph, runs, newFullText);
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the text content of existing runs with new text,
|
||||
/// preserving the formatting of the first run.
|
||||
/// </summary>
|
||||
private static void RebuildRunsWithText(Paragraph paragraph, List<Run> runs, string newText)
|
||||
{
|
||||
if (runs.Count == 0) return;
|
||||
|
||||
// Keep the first run's formatting, set its text to the full new text
|
||||
var firstRun = runs[0];
|
||||
var firstText = firstRun.Elements<Text>().FirstOrDefault();
|
||||
if (firstText != null)
|
||||
{
|
||||
firstText.Text = newText;
|
||||
if (newText.StartsWith(' ') || newText.EndsWith(' '))
|
||||
firstText.Space = SpaceProcessingModeValues.Preserve;
|
||||
}
|
||||
|
||||
// Remove all other runs
|
||||
for (int i = 1; i < runs.Count; i++)
|
||||
runs[i].Remove();
|
||||
}
|
||||
|
||||
private static int CountOccurrences(string text, string search)
|
||||
{
|
||||
int count = 0;
|
||||
int index = 0;
|
||||
while ((index = text.IndexOf(search, index, StringComparison.Ordinal)) != -1)
|
||||
{
|
||||
count++;
|
||||
index += search.Length;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static string[] ParseCsvLine(string line)
|
||||
{
|
||||
// Simple CSV parser (handles quoted fields)
|
||||
var result = new List<string>();
|
||||
bool inQuotes = false;
|
||||
var current = new System.Text.StringBuilder();
|
||||
|
||||
for (int i = 0; i < line.Length; i++)
|
||||
{
|
||||
char c = line[i];
|
||||
if (c == '"')
|
||||
{
|
||||
if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
|
||||
{
|
||||
current.Append('"');
|
||||
i++;
|
||||
}
|
||||
else
|
||||
{
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
}
|
||||
else if (c == ',' && !inQuotes)
|
||||
{
|
||||
result.Add(current.ToString());
|
||||
current.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
current.Append(c);
|
||||
}
|
||||
}
|
||||
result.Add(current.ToString());
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using System.CommandLine;
|
||||
using System.IO.Compression;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Commands;
|
||||
|
||||
public static class FixOrderCommand
|
||||
{
|
||||
private static readonly XNamespace W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||
|
||||
// Canonical element ordering within common parent elements per ISO 29500
|
||||
private static readonly Dictionary<string, List<string>> ElementOrder = new()
|
||||
{
|
||||
["pPr"] = new() { "pStyle", "keepNext", "keepLines", "pageBreakBefore", "widowControl", "numPr", "suppressLineNumbers", "pBdr", "shd", "tabs", "suppressAutoHyphens", "spacing", "ind", "jc", "outlineLvl", "rPr" },
|
||||
["rPr"] = new() { "rStyle", "rFonts", "b", "bCs", "i", "iCs", "caps", "smallCaps", "strike", "dstrike", "vanish", "color", "spacing", "w", "kern", "position", "sz", "szCs", "highlight", "u", "effect", "vertAlign", "lang" },
|
||||
["tblPr"] = new() { "tblStyle", "tblpPr", "tblOverlap", "tblW", "jc", "tblInd", "tblBorders", "shd", "tblLayout", "tblCellMar", "tblLook" },
|
||||
["tcPr"] = new() { "cnfStyle", "tcW", "gridSpan", "hMerge", "vMerge", "tcBorders", "shd", "noWrap", "tcMar", "textDirection", "tcFitText", "vAlign" },
|
||||
["sectPr"] = new() { "headerReference", "footerReference", "footnotePr", "endnotePr", "type", "pgSz", "pgMar", "paperSrc", "pgBorders", "lnNumType", "pgNumType", "cols", "docGrid" },
|
||||
};
|
||||
|
||||
public static Command Create()
|
||||
{
|
||||
var inputOption = new Option<string>("--input") { Description = "DOCX file to fix", Required = true };
|
||||
var outputOption = new Option<string>("--output") { Description = "Output path (default: overwrite input)" };
|
||||
var backupOption = new Option<bool>("--backup") { Description = "Create .bak before modifying", DefaultValueFactory = (_) => true };
|
||||
|
||||
var cmd = new Command("fix-order", "Fix OpenXML element ordering per ISO 29500")
|
||||
{
|
||||
inputOption, outputOption, backupOption
|
||||
};
|
||||
|
||||
cmd.SetAction((parseResult) =>
|
||||
{
|
||||
var input = parseResult.GetValue(inputOption)!;
|
||||
var output = parseResult.GetValue(outputOption) ?? input;
|
||||
var backup = parseResult.GetValue(backupOption);
|
||||
|
||||
if (!File.Exists(input))
|
||||
{
|
||||
Console.Error.WriteLine($"File not found: {input}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (backup && output == input)
|
||||
File.Copy(input, input + ".bak", true);
|
||||
|
||||
var tempPath = Path.GetTempFileName();
|
||||
File.Copy(input, tempPath, true);
|
||||
|
||||
using var zip = ZipFile.Open(tempPath, ZipArchiveMode.Update);
|
||||
var entry = zip.GetEntry("word/document.xml");
|
||||
if (entry == null)
|
||||
{
|
||||
Console.Error.WriteLine("Not a valid DOCX");
|
||||
return;
|
||||
}
|
||||
|
||||
XDocument doc;
|
||||
using (var stream = entry.Open())
|
||||
doc = XDocument.Load(stream);
|
||||
|
||||
int reorderedCount = 0;
|
||||
|
||||
foreach (var (parentName, order) in ElementOrder)
|
||||
{
|
||||
foreach (var parent in doc.Descendants(W + parentName))
|
||||
{
|
||||
var children = parent.Elements().ToList();
|
||||
var sorted = children.OrderBy(e =>
|
||||
{
|
||||
var idx = order.IndexOf(e.Name.LocalName);
|
||||
return idx >= 0 ? idx : order.Count;
|
||||
}).ToList();
|
||||
|
||||
bool changed = false;
|
||||
for (int i = 0; i < children.Count; i++)
|
||||
{
|
||||
if (children[i] != sorted[i])
|
||||
{
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
parent.ReplaceNodes(sorted);
|
||||
reorderedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entry.Delete();
|
||||
var newEntry = zip.CreateEntry("word/document.xml", CompressionLevel.Optimal);
|
||||
using (var stream = newEntry.Open())
|
||||
doc.Save(stream);
|
||||
|
||||
zip.Dispose();
|
||||
File.Copy(tempPath, output, true);
|
||||
File.Delete(tempPath);
|
||||
|
||||
Console.WriteLine($"Reordered {reorderedCount} element group(s)");
|
||||
Console.WriteLine($"Written to: {output}");
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using System.CommandLine;
|
||||
using System.IO.Compression;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Commands;
|
||||
|
||||
public static class MergeRunsCommand
|
||||
{
|
||||
private static readonly XNamespace W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||
|
||||
public static Command Create()
|
||||
{
|
||||
var inputOption = new Option<string>("--input") { Description = "DOCX file to optimize", Required = true };
|
||||
var outputOption = new Option<string>("--output") { Description = "Output path (default: overwrite input)" };
|
||||
var dryRunOption = new Option<bool>("--dry-run") { Description = "Report without modifying" };
|
||||
|
||||
var cmd = new Command("merge-runs", "Merge adjacent runs with identical formatting")
|
||||
{
|
||||
inputOption, outputOption, dryRunOption
|
||||
};
|
||||
|
||||
cmd.SetAction((parseResult) =>
|
||||
{
|
||||
var input = parseResult.GetValue(inputOption)!;
|
||||
var output = parseResult.GetValue(outputOption) ?? input;
|
||||
var dryRun = parseResult.GetValue(dryRunOption);
|
||||
|
||||
if (!File.Exists(input))
|
||||
{
|
||||
Console.Error.WriteLine($"File not found: {input}");
|
||||
return;
|
||||
}
|
||||
|
||||
var tempPath = Path.GetTempFileName();
|
||||
File.Copy(input, tempPath, true);
|
||||
|
||||
using var zip = ZipFile.Open(tempPath, ZipArchiveMode.Update);
|
||||
var entry = zip.GetEntry("word/document.xml");
|
||||
if (entry == null)
|
||||
{
|
||||
Console.Error.WriteLine("Not a valid DOCX: missing word/document.xml");
|
||||
return;
|
||||
}
|
||||
|
||||
XDocument doc;
|
||||
using (var stream = entry.Open())
|
||||
doc = XDocument.Load(stream);
|
||||
|
||||
int originalCount = 0;
|
||||
int mergedCount = 0;
|
||||
|
||||
foreach (var p in doc.Descendants(W + "p"))
|
||||
{
|
||||
var runs = p.Elements(W + "r").ToList();
|
||||
originalCount += runs.Count;
|
||||
|
||||
for (int i = runs.Count - 1; i > 0; i--)
|
||||
{
|
||||
var current = runs[i];
|
||||
var previous = runs[i - 1];
|
||||
|
||||
var curProps = current.Element(W + "rPr")?.ToString() ?? "";
|
||||
var prevProps = previous.Element(W + "rPr")?.ToString() ?? "";
|
||||
|
||||
if (curProps == prevProps)
|
||||
{
|
||||
// Only merge if both contain only text elements
|
||||
var curChildren = current.Elements().Where(e => e.Name != W + "rPr").ToList();
|
||||
var prevChildren = previous.Elements().Where(e => e.Name != W + "rPr").ToList();
|
||||
|
||||
if (curChildren.All(e => e.Name == W + "t") && prevChildren.All(e => e.Name == W + "t"))
|
||||
{
|
||||
var prevText = previous.Elements(W + "t").LastOrDefault();
|
||||
var curText = current.Elements(W + "t").FirstOrDefault();
|
||||
|
||||
if (prevText != null && curText != null)
|
||||
{
|
||||
prevText.Value += curText.Value;
|
||||
prevText.SetAttributeValue(XNamespace.Xml + "space", "preserve");
|
||||
|
||||
foreach (var extra in current.Elements(W + "t").Skip(1))
|
||||
{
|
||||
previous.Add(new XElement(extra));
|
||||
}
|
||||
|
||||
current.Remove();
|
||||
runs.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mergedCount += runs.Count;
|
||||
}
|
||||
|
||||
if (dryRun)
|
||||
{
|
||||
Console.WriteLine($"Original runs: {originalCount}");
|
||||
Console.WriteLine($"After merge: {mergedCount}");
|
||||
Console.WriteLine($"Reduction: {(originalCount > 0 ? (originalCount - mergedCount) * 100.0 / originalCount : 0):F1}%");
|
||||
File.Delete(tempPath);
|
||||
return;
|
||||
}
|
||||
|
||||
entry.Delete();
|
||||
var newEntry = zip.CreateEntry("word/document.xml", CompressionLevel.Optimal);
|
||||
using (var stream = newEntry.Open())
|
||||
doc.Save(stream);
|
||||
|
||||
zip.Dispose();
|
||||
File.Copy(tempPath, output, true);
|
||||
File.Delete(tempPath);
|
||||
|
||||
Console.WriteLine($"Original runs: {originalCount}");
|
||||
Console.WriteLine($"After merge: {mergedCount}");
|
||||
Console.WriteLine($"Reduction: {(originalCount > 0 ? (originalCount - mergedCount) * 100.0 / originalCount : 0):F1}%");
|
||||
Console.WriteLine($"Written to: {output}");
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using System.CommandLine;
|
||||
using System.Text.Json;
|
||||
using MiniMaxAIDocx.Core.Validation;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Commands;
|
||||
|
||||
public static class ValidateCommand
|
||||
{
|
||||
public static Command Create()
|
||||
{
|
||||
var inputOption = new Option<string>("--input") { Description = "DOCX file to validate", Required = true };
|
||||
var xsdOption = new Option<string>("--xsd") { Description = "XSD schema path for XML validation" };
|
||||
var businessOption = new Option<bool>("--business") { Description = "Run business rule validation" };
|
||||
var gateCheckOption = new Option<string>("--gate-check") { Description = "Template DOCX for gate-check validation" };
|
||||
var jsonOption = new Option<bool>("--json") { Description = "Output results as JSON" };
|
||||
|
||||
var cmd = new Command("validate", "Validate DOCX structure and content")
|
||||
{
|
||||
inputOption, xsdOption, businessOption, gateCheckOption, jsonOption
|
||||
};
|
||||
|
||||
cmd.SetAction((parseResult) =>
|
||||
{
|
||||
var input = parseResult.GetValue(inputOption)!;
|
||||
var xsd = parseResult.GetValue(xsdOption);
|
||||
var business = parseResult.GetValue(businessOption);
|
||||
var gateCheck = parseResult.GetValue(gateCheckOption);
|
||||
var asJson = parseResult.GetValue(jsonOption);
|
||||
|
||||
if (!File.Exists(input))
|
||||
{
|
||||
Console.Error.WriteLine($"File not found: {input}");
|
||||
return;
|
||||
}
|
||||
|
||||
var combinedResult = new ValidationResult();
|
||||
GateCheckResult? gateResult = null;
|
||||
|
||||
if (xsd != null)
|
||||
{
|
||||
var xsdValidator = new XsdValidator();
|
||||
combinedResult.Merge(xsdValidator.Validate(input, xsd));
|
||||
}
|
||||
|
||||
if (business)
|
||||
{
|
||||
var bizValidator = new BusinessRuleValidator();
|
||||
combinedResult.Merge(bizValidator.Validate(input));
|
||||
}
|
||||
|
||||
if (gateCheck != null)
|
||||
{
|
||||
var gateValidator = new GateCheckValidator();
|
||||
gateResult = gateValidator.Validate(input, gateCheck);
|
||||
}
|
||||
|
||||
if (asJson)
|
||||
{
|
||||
var output = new
|
||||
{
|
||||
isValid = combinedResult.IsValid && (gateResult?.Passed ?? true),
|
||||
errors = combinedResult.Errors,
|
||||
warnings = combinedResult.Warnings,
|
||||
gateCheck = gateResult == null ? null : new
|
||||
{
|
||||
passed = gateResult.Passed,
|
||||
violations = gateResult.Violations
|
||||
}
|
||||
};
|
||||
Console.WriteLine(JsonSerializer.Serialize(output, new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (combinedResult.Errors.Count > 0)
|
||||
{
|
||||
Console.WriteLine($"ERRORS ({combinedResult.Errors.Count}):");
|
||||
foreach (var e in combinedResult.Errors)
|
||||
Console.WriteLine($" [{e.Severity}] {e.Message}" + (e.LineNumber > 0 ? $" (line {e.LineNumber}:{e.LinePosition})" : ""));
|
||||
}
|
||||
|
||||
if (combinedResult.Warnings.Count > 0)
|
||||
{
|
||||
Console.WriteLine($"WARNINGS ({combinedResult.Warnings.Count}):");
|
||||
foreach (var w in combinedResult.Warnings)
|
||||
Console.WriteLine($" [{w.Severity}] {w.Message}");
|
||||
}
|
||||
|
||||
if (gateResult != null)
|
||||
{
|
||||
Console.WriteLine(gateResult.Passed ? "GATE CHECK: PASSED" : "GATE CHECK: FAILED");
|
||||
foreach (var v in gateResult.Violations)
|
||||
Console.WriteLine($" - {v}");
|
||||
}
|
||||
|
||||
if (combinedResult.IsValid && (gateResult?.Passed ?? true))
|
||||
Console.WriteLine("Validation: PASSED");
|
||||
else
|
||||
Console.WriteLine("Validation: FAILED");
|
||||
}
|
||||
|
||||
if (!combinedResult.IsValid || gateResult is { Passed: false })
|
||||
Environment.ExitCode = 1;
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="3.5.1" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,169 @@
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.OpenXml;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the 4-file comment system (comments.xml, commentsExtended.xml,
|
||||
/// commentsIds.xml, commentsExtensible.xml) plus document.xml markers.
|
||||
/// </summary>
|
||||
public static class CommentSynchronizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a comment to the document, updating all required parts.
|
||||
/// </summary>
|
||||
public static int AddComment(WordprocessingDocument doc, string text, string author, string rangeBookmark)
|
||||
{
|
||||
var mainPart = doc.MainDocumentPart
|
||||
?? throw new InvalidOperationException("Document has no main part.");
|
||||
|
||||
int commentId = GetNextCommentId(doc);
|
||||
|
||||
// Ensure comments part exists
|
||||
var commentsPart = mainPart.WordprocessingCommentsPart
|
||||
?? mainPart.AddNewPart<WordprocessingCommentsPart>();
|
||||
|
||||
if (commentsPart.Comments == null)
|
||||
commentsPart.Comments = new Comments();
|
||||
|
||||
// Create the comment
|
||||
var comment = new Comment
|
||||
{
|
||||
Id = commentId.ToString(),
|
||||
Author = author,
|
||||
Date = DateTime.UtcNow,
|
||||
Initials = author.Length > 0 ? author[..1].ToUpperInvariant() : "A"
|
||||
};
|
||||
comment.Append(new Paragraph(new Run(new Text(text))));
|
||||
commentsPart.Comments.Append(comment);
|
||||
|
||||
// Add range markers in document body
|
||||
var body = mainPart.Document.Body;
|
||||
if (body != null)
|
||||
{
|
||||
// Find bookmark or append at end
|
||||
var rangeStart = new CommentRangeStart { Id = commentId.ToString() };
|
||||
var rangeEnd = new CommentRangeEnd { Id = commentId.ToString() };
|
||||
var reference = new Run(new CommentReference { Id = commentId.ToString() });
|
||||
|
||||
body.Append(rangeStart);
|
||||
body.Append(rangeEnd);
|
||||
body.Append(new Paragraph(reference));
|
||||
}
|
||||
|
||||
return commentId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a reply to an existing comment.
|
||||
/// </summary>
|
||||
public static int AddReply(WordprocessingDocument doc, int parentCommentId, string text, string author)
|
||||
{
|
||||
var mainPart = doc.MainDocumentPart
|
||||
?? throw new InvalidOperationException("Document has no main part.");
|
||||
|
||||
var commentsPart = mainPart.WordprocessingCommentsPart
|
||||
?? throw new InvalidOperationException("Document has no comments part.");
|
||||
|
||||
int replyId = GetNextCommentId(doc);
|
||||
|
||||
var reply = new Comment
|
||||
{
|
||||
Id = replyId.ToString(),
|
||||
Author = author,
|
||||
Date = DateTime.UtcNow,
|
||||
Initials = author.Length > 0 ? author[..1].ToUpperInvariant() : "A"
|
||||
};
|
||||
reply.Append(new Paragraph(new Run(new Text(text))));
|
||||
commentsPart.Comments?.Append(reply);
|
||||
|
||||
// Link reply to parent via commentsExtended.xml
|
||||
LinkReplyToParent(doc, replyId, parentCommentId);
|
||||
|
||||
return replyId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a comment as resolved/done by setting done="1" in commentsExtended.xml.
|
||||
/// Uses raw XML manipulation since these extended parts lack typed SDK support.
|
||||
/// </summary>
|
||||
public static void ResolveComment(WordprocessingDocument doc, int commentId)
|
||||
{
|
||||
var mainPart = doc.MainDocumentPart;
|
||||
if (mainPart == null) return;
|
||||
|
||||
// commentsExtended.xml is an untyped part — manipulate via raw XML
|
||||
const string ceUri = "http://schemas.microsoft.com/office/word/2018/wordml/cex";
|
||||
foreach (var part in mainPart.Parts)
|
||||
{
|
||||
if (part.OpenXmlPart.ContentType.Contains("commentsExtensible"))
|
||||
{
|
||||
using var stream = part.OpenXmlPart.GetStream(FileMode.Open, FileAccess.ReadWrite);
|
||||
var xdoc = System.Xml.Linq.XDocument.Load(stream);
|
||||
var ns = System.Xml.Linq.XNamespace.Get(ceUri);
|
||||
var commentEl = xdoc.Descendants(ns + "comment")
|
||||
.FirstOrDefault(e => e.Attribute(ns + "paraId")?.Value != null);
|
||||
// Set done flag if element found for this comment
|
||||
if (commentEl != null)
|
||||
{
|
||||
commentEl.SetAttributeValue("done", "1");
|
||||
stream.SetLength(0);
|
||||
xdoc.Save(stream);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Links a reply comment to its parent via commentsExtended.xml (w15:commentEx).
|
||||
/// Uses raw XML since the extended comment parts lack typed SDK support.
|
||||
/// </summary>
|
||||
private static void LinkReplyToParent(WordprocessingDocument doc, int replyId, int parentCommentId)
|
||||
{
|
||||
var mainPart = doc.MainDocumentPart;
|
||||
if (mainPart == null) return;
|
||||
|
||||
const string w15Uri = "http://schemas.microsoft.com/office/word/2012/wordml";
|
||||
var w15 = System.Xml.Linq.XNamespace.Get(w15Uri);
|
||||
|
||||
// Find or create commentsExtended part
|
||||
foreach (var part in mainPart.Parts)
|
||||
{
|
||||
if (part.OpenXmlPart.ContentType.Contains("commentsExtended"))
|
||||
{
|
||||
using var stream = part.OpenXmlPart.GetStream(FileMode.Open, FileAccess.ReadWrite);
|
||||
var xdoc = System.Xml.Linq.XDocument.Load(stream);
|
||||
var root = xdoc.Root;
|
||||
if (root == null) return;
|
||||
|
||||
root.Add(new System.Xml.Linq.XElement(w15 + "commentEx",
|
||||
new System.Xml.Linq.XAttribute(w15 + "paraId", replyId.ToString("X8")),
|
||||
new System.Xml.Linq.XAttribute(w15 + "paraIdParent", parentCommentId.ToString("X8")),
|
||||
new System.Xml.Linq.XAttribute(w15 + "done", "0")));
|
||||
|
||||
stream.SetLength(0);
|
||||
xdoc.Save(stream);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the maximum existing comment ID and returns the next one.
|
||||
/// </summary>
|
||||
public static int GetNextCommentId(WordprocessingDocument doc)
|
||||
{
|
||||
var commentsPart = doc.MainDocumentPart?.WordprocessingCommentsPart;
|
||||
if (commentsPart?.Comments == null) return 1;
|
||||
|
||||
int maxId = 0;
|
||||
foreach (var comment in commentsPart.Comments.Elements<Comment>())
|
||||
{
|
||||
if (comment.Id?.Value != null && int.TryParse(comment.Id.Value, out int id) && id > maxId)
|
||||
maxId = id;
|
||||
}
|
||||
return maxId + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.OpenXml;
|
||||
|
||||
/// <summary>
|
||||
/// Defines canonical child element ordering for key OpenXML parent elements
|
||||
/// and provides reordering utilities.
|
||||
/// </summary>
|
||||
public static class ElementOrder
|
||||
{
|
||||
private static readonly Dictionary<string, string[]> OrderMap = new()
|
||||
{
|
||||
["w:body"] = ["w:p", "w:tbl", "w:sdt", "w:sectPr"],
|
||||
["w:p"] = ["w:pPr", "w:hyperlink", "w:r", "w:ins", "w:del", "w:bookmarkStart", "w:bookmarkEnd", "w:commentRangeStart", "w:commentRangeEnd", "w:fldSimple"],
|
||||
["w:pPr"] = ["w:pStyle", "w:keepNext", "w:keepLines", "w:pageBreakBefore", "w:widowControl", "w:numPr", "w:pBdr", "w:shd", "w:tabs", "w:suppressAutoHyphens", "w:spacing", "w:ind", "w:jc", "w:rPr", "w:sectPr", "w:pPrChange"],
|
||||
["w:r"] = ["w:rPr", "w:t", "w:br", "w:tab", "w:cr", "w:sym", "w:drawing", "w:delText", "w:fldChar", "w:instrText", "w:lastRenderedPageBreak", "w:noBreakHyphen", "w:softHyphen"],
|
||||
["w:rPr"] = ["w:rStyle", "w:rFonts", "w:b", "w:bCs", "w:i", "w:iCs", "w:caps", "w:smallCaps", "w:strike", "w:dstrike", "w:vanish", "w:color", "w:sz", "w:szCs", "w:u", "w:shd", "w:highlight", "w:lang", "w:rPrChange"],
|
||||
["w:tbl"] = ["w:tblPr", "w:tblGrid", "w:tr"],
|
||||
["w:tblPr"] = ["w:tblStyle", "w:tblpPr", "w:tblOverlap", "w:tblW", "w:jc", "w:tblCellSpacing", "w:tblInd", "w:tblBorders", "w:shd", "w:tblLayout", "w:tblCellMar", "w:tblLook", "w:tblPrChange"],
|
||||
["w:tr"] = ["w:trPr", "w:tc"],
|
||||
["w:trPr"] = ["w:cnfStyle", "w:divId", "w:gridBefore", "w:gridAfter", "w:wBefore", "w:wAfter", "w:cantSplit", "w:trHeight", "w:tblHeader", "w:tblCellSpacing", "w:jc", "w:hidden", "w:ins", "w:del", "w:trPrChange"],
|
||||
["w:tc"] = ["w:tcPr", "w:p", "w:tbl"],
|
||||
["w:tcPr"] = ["w:cnfStyle", "w:tcW", "w:gridSpan", "w:hMerge", "w:vMerge", "w:tcBorders", "w:shd", "w:noWrap", "w:tcMar", "w:textDirection", "w:tcFitText", "w:vAlign", "w:hideMark", "w:headers", "w:cellIns", "w:cellDel", "w:cellMerge", "w:tcPrChange"],
|
||||
["w:sectPr"] = ["w:headerReference", "w:footerReference", "w:type", "w:pgSz", "w:pgMar", "w:paperSrc", "w:pgBorders", "w:lnNumType", "w:pgNumType", "w:cols", "w:formProt", "w:vAlign", "w:noEndnote", "w:titlePg", "w:textDirection", "w:bidi", "w:rtlGutter", "w:docGrid"],
|
||||
["w:hdr"] = ["w:p", "w:tbl", "w:sdt"],
|
||||
["w:ftr"] = ["w:p", "w:tbl", "w:sdt"],
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the canonical child ordering for a given parent element name (e.g. "w:p").
|
||||
/// Returns null if no ordering is defined.
|
||||
/// </summary>
|
||||
public static string[]? GetChildOrder(string parentElement)
|
||||
{
|
||||
return OrderMap.TryGetValue(parentElement, out var order) ? order : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reorders children of the given XElement according to the canonical ordering rules.
|
||||
/// Children not listed in the ordering are placed at the end in their original order.
|
||||
/// </summary>
|
||||
public static void ReorderChildren(XElement parent)
|
||||
{
|
||||
var qualifiedName = GetQualifiedName(parent);
|
||||
var order = GetChildOrder(qualifiedName);
|
||||
if (order == null) return;
|
||||
|
||||
var children = parent.Elements().ToList();
|
||||
if (children.Count <= 1) return;
|
||||
|
||||
var orderIndex = new Dictionary<string, int>();
|
||||
for (int i = 0; i < order.Length; i++)
|
||||
orderIndex[order[i]] = i;
|
||||
|
||||
int unknownBase = order.Length;
|
||||
int unknownCounter = 0;
|
||||
|
||||
var sorted = children
|
||||
.Select(c => (Element: c, QName: GetQualifiedName(c)))
|
||||
.OrderBy(x => orderIndex.TryGetValue(x.QName, out var idx) ? idx : unknownBase + unknownCounter++)
|
||||
.Select(x => x.Element)
|
||||
.ToList();
|
||||
|
||||
parent.RemoveNodes();
|
||||
foreach (var child in sorted)
|
||||
parent.Add(child);
|
||||
}
|
||||
|
||||
private static string GetQualifiedName(XElement element)
|
||||
{
|
||||
var ns = element.Name.Namespace;
|
||||
var local = element.Name.LocalName;
|
||||
|
||||
if (ns == Ns.W) return $"w:{local}";
|
||||
if (ns == Ns.R) return $"r:{local}";
|
||||
if (ns == Ns.MC) return $"mc:{local}";
|
||||
|
||||
return local;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.OpenXml;
|
||||
|
||||
/// <summary>
|
||||
/// All OpenXML namespace URIs and common content/relationship type constants.
|
||||
/// </summary>
|
||||
public static class Ns
|
||||
{
|
||||
public static readonly XNamespace W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||
public static readonly XNamespace R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
||||
public static readonly XNamespace WP = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
|
||||
public static readonly XNamespace A = "http://schemas.openxmlformats.org/drawingml/2006/main";
|
||||
public static readonly XNamespace MC = "http://schemas.openxmlformats.org/markup-compatibility/2006";
|
||||
public static readonly XNamespace PIC = "http://schemas.openxmlformats.org/drawingml/2006/picture";
|
||||
public static readonly XNamespace W14 = "http://schemas.microsoft.com/office/word/2010/wordml";
|
||||
public static readonly XNamespace W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
|
||||
public static readonly XNamespace W16CID = "http://schemas.microsoft.com/office/word/2016/wordml/cid";
|
||||
public static readonly XNamespace W16CEX = "http://schemas.microsoft.com/office/word/2018/wordml/cex";
|
||||
public static readonly XNamespace WPC = "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas";
|
||||
public static readonly XNamespace WPS = "http://schemas.microsoft.com/office/word/2010/wordprocessingShape";
|
||||
|
||||
// Content types
|
||||
public const string MainDocumentContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml";
|
||||
public const string StylesContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml";
|
||||
public const string HeaderContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml";
|
||||
public const string FooterContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml";
|
||||
public const string CommentsContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
|
||||
|
||||
// Relationship types
|
||||
public const string DocumentRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
|
||||
public const string StylesRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
|
||||
public const string HeaderRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header";
|
||||
public const string FooterRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer";
|
||||
public const string CommentsRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
|
||||
public const string ImageRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
|
||||
public const string HyperlinkRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
|
||||
public const string NumberingRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
|
||||
public const string FontTableRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable";
|
||||
public const string ThemeRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme";
|
||||
public const string SettingsRelationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings";
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.OpenXml;
|
||||
|
||||
/// <summary>
|
||||
/// Result of a run merge operation.
|
||||
/// </summary>
|
||||
public record RunMergeResult(int OriginalRunCount, int MergedRunCount, int SizeReductionBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Merges adjacent w:r elements with identical w:rPr formatting to reduce document size.
|
||||
/// </summary>
|
||||
public static class RunMerger
|
||||
{
|
||||
/// <summary>
|
||||
/// Merges adjacent runs with identical formatting in all paragraphs of the document body.
|
||||
/// </summary>
|
||||
public static RunMergeResult MergeRuns(XDocument document)
|
||||
{
|
||||
var body = document.Root?.Element(Ns.W + "body");
|
||||
if (body == null) return new(0, 0, 0);
|
||||
|
||||
int originalCount = 0;
|
||||
int removedCount = 0;
|
||||
|
||||
foreach (var paragraph in body.Descendants(Ns.W + "p"))
|
||||
{
|
||||
var runs = paragraph.Elements(Ns.W + "r").ToList();
|
||||
originalCount += runs.Count;
|
||||
|
||||
for (int i = runs.Count - 1; i > 0; i--)
|
||||
{
|
||||
var current = runs[i];
|
||||
var previous = runs[i - 1];
|
||||
|
||||
if (!AreRunPropertiesEqual(previous, current)) continue;
|
||||
|
||||
// Merge text content from current into previous
|
||||
var prevText = GetOrCreateTextElement(previous);
|
||||
var currText = current.Element(Ns.W + "t");
|
||||
if (currText != null && prevText != null)
|
||||
{
|
||||
prevText.Value += currText.Value;
|
||||
// Preserve xml:space="preserve" if either has it
|
||||
if (currText.Attribute(XNamespace.Xml + "space")?.Value == "preserve" ||
|
||||
prevText.Value.StartsWith(' ') || prevText.Value.EndsWith(' '))
|
||||
{
|
||||
prevText.SetAttributeValue(XNamespace.Xml + "space", "preserve");
|
||||
}
|
||||
}
|
||||
|
||||
current.Remove();
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return new(originalCount, originalCount - removedCount, 0);
|
||||
}
|
||||
|
||||
private static bool AreRunPropertiesEqual(XElement run1, XElement run2)
|
||||
{
|
||||
var rPr1 = run1.Element(Ns.W + "rPr");
|
||||
var rPr2 = run2.Element(Ns.W + "rPr");
|
||||
|
||||
if (rPr1 == null && rPr2 == null) return true;
|
||||
if (rPr1 == null || rPr2 == null) return false;
|
||||
|
||||
return XNode.DeepEquals(rPr1, rPr2);
|
||||
}
|
||||
|
||||
private static XElement? GetOrCreateTextElement(XElement run)
|
||||
{
|
||||
var t = run.Element(Ns.W + "t");
|
||||
if (t == null)
|
||||
{
|
||||
t = new XElement(Ns.W + "t");
|
||||
run.Add(t);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.OpenXml;
|
||||
|
||||
public record StyleInfo(string Id, string? Name, string Type, string? BasedOn, bool IsDefault);
|
||||
|
||||
public record StyleReport(
|
||||
List<StyleInfo> AllStyles,
|
||||
Dictionary<string, List<string>> InheritanceTree,
|
||||
string? DefaultParagraphStyle,
|
||||
string? DefaultCharacterStyle,
|
||||
int DirectFormattingCount);
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes the style hierarchy of a DOCX document.
|
||||
/// </summary>
|
||||
public static class StyleAnalyzer
|
||||
{
|
||||
/// <summary>
|
||||
/// Analyzes styles.xml content and document.xml for direct formatting usage.
|
||||
/// </summary>
|
||||
public static StyleReport Analyze(XDocument stylesXml, XDocument documentXml)
|
||||
{
|
||||
var styles = ExtractStyles(stylesXml);
|
||||
var tree = BuildInheritanceTree(styles);
|
||||
var defaultPara = styles.FirstOrDefault(s => s.Type == "paragraph" && s.IsDefault)?.Id;
|
||||
var defaultChar = styles.FirstOrDefault(s => s.Type == "character" && s.IsDefault)?.Id;
|
||||
var directCount = CountDirectFormatting(documentXml);
|
||||
|
||||
return new(styles, tree, defaultPara, defaultChar, directCount);
|
||||
}
|
||||
|
||||
private static List<StyleInfo> ExtractStyles(XDocument stylesXml)
|
||||
{
|
||||
var result = new List<StyleInfo>();
|
||||
var root = stylesXml.Root;
|
||||
if (root == null) return result;
|
||||
|
||||
foreach (var style in root.Elements(Ns.W + "style"))
|
||||
{
|
||||
var id = style.Attribute(Ns.W + "styleId")?.Value ?? "";
|
||||
var name = style.Element(Ns.W + "name")?.Attribute(Ns.W + "val")?.Value;
|
||||
var type = style.Attribute(Ns.W + "type")?.Value ?? "unknown";
|
||||
var basedOn = style.Element(Ns.W + "basedOn")?.Attribute(Ns.W + "val")?.Value;
|
||||
var isDefault = style.Attribute(Ns.W + "default")?.Value == "1";
|
||||
result.Add(new(id, name, type, basedOn, isDefault));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Dictionary<string, List<string>> BuildInheritanceTree(List<StyleInfo> styles)
|
||||
{
|
||||
var tree = new Dictionary<string, List<string>>();
|
||||
foreach (var style in styles)
|
||||
{
|
||||
var parent = style.BasedOn ?? "(root)";
|
||||
if (!tree.ContainsKey(parent))
|
||||
tree[parent] = [];
|
||||
tree[parent].Add(style.Id);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
private static int CountDirectFormatting(XDocument documentXml)
|
||||
{
|
||||
var body = documentXml.Root?.Element(Ns.W + "body");
|
||||
if (body == null) return 0;
|
||||
|
||||
int count = 0;
|
||||
// Count inline rPr on runs (direct character formatting)
|
||||
count += body.Descendants(Ns.W + "r")
|
||||
.Count(r => r.Element(Ns.W + "rPr") != null);
|
||||
// Count inline pPr that contain more than just pStyle (direct paragraph formatting)
|
||||
count += body.Descendants(Ns.W + "p")
|
||||
.Select(p => p.Element(Ns.W + "pPr"))
|
||||
.Count(pPr => pPr != null && pPr.Elements().Any(e => e.Name != Ns.W + "pStyle"));
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.OpenXml;
|
||||
|
||||
/// <summary>
|
||||
/// Helpers for Track Changes (revision marks) operations.
|
||||
/// </summary>
|
||||
public static class TrackChangesHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Wraps a run in a w:ins element to propose an insertion.
|
||||
/// </summary>
|
||||
public static InsertedRun ProposeInsertion(Run run, string author, DateTime date)
|
||||
{
|
||||
var ins = new InsertedRun
|
||||
{
|
||||
Author = author,
|
||||
Date = date,
|
||||
Id = run.Parent is Body body ? GetNextRevisionId(body).ToString() : "1"
|
||||
};
|
||||
run.Remove();
|
||||
ins.Append(run);
|
||||
return ins;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps a run in a w:del element, converting w:t to w:delText.
|
||||
/// </summary>
|
||||
public static DeletedRun ProposeDeletion(Run run, string author, DateTime date)
|
||||
{
|
||||
// Convert w:t elements to w:delText
|
||||
foreach (var text in run.Elements<Text>().ToList())
|
||||
{
|
||||
var delText = new DeletedText { Text = text.Text, Space = SpaceProcessingModeValues.Preserve };
|
||||
text.InsertAfterSelf(delText);
|
||||
text.Remove();
|
||||
}
|
||||
|
||||
var del = new DeletedRun
|
||||
{
|
||||
Author = author,
|
||||
Date = date,
|
||||
Id = run.Parent is Body body ? GetNextRevisionId(body).ToString() : "1"
|
||||
};
|
||||
run.Remove();
|
||||
del.Append(run);
|
||||
return del;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accepts an insertion by removing the w:ins wrapper and keeping content.
|
||||
/// </summary>
|
||||
public static void AcceptInsertion(OpenXmlElement insElement)
|
||||
{
|
||||
if (insElement is not InsertedRun) return;
|
||||
var parent = insElement.Parent;
|
||||
if (parent == null) return;
|
||||
|
||||
var children = insElement.ChildElements.ToList();
|
||||
foreach (var child in children)
|
||||
{
|
||||
child.Remove();
|
||||
insElement.InsertBeforeSelf(child);
|
||||
}
|
||||
insElement.Remove();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accepts a deletion by removing the entire w:del element and its content.
|
||||
/// </summary>
|
||||
public static void AcceptDeletion(OpenXmlElement delElement)
|
||||
{
|
||||
delElement.Remove();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the maximum existing revision ID in the document and returns the next one.
|
||||
/// </summary>
|
||||
public static int GetNextRevisionId(WordprocessingDocument doc)
|
||||
{
|
||||
var body = doc.MainDocumentPart?.Document?.Body;
|
||||
if (body == null) return 1;
|
||||
return GetNextRevisionId(body);
|
||||
}
|
||||
|
||||
private static int GetNextRevisionId(OpenXmlElement root)
|
||||
{
|
||||
int maxId = 0;
|
||||
foreach (var element in root.Descendants())
|
||||
{
|
||||
var idAttr = element.GetAttributes().FirstOrDefault(a => a.LocalName == "id");
|
||||
if (idAttr.Value != null && int.TryParse(idAttr.Value, out int id) && id > maxId)
|
||||
maxId = id;
|
||||
}
|
||||
return maxId + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace MiniMaxAIDocx.Core.OpenXml;
|
||||
|
||||
/// <summary>
|
||||
/// Conversion utilities between OpenXML measurement units (DXA, EMU, points, half-points).
|
||||
/// </summary>
|
||||
public static class UnitConverter
|
||||
{
|
||||
// 1 inch = 1440 DXA = 914400 EMU = 72 pt = 144 half-pt
|
||||
|
||||
public static int InchesToDxa(double inches) => (int)(inches * 1440);
|
||||
public static int CmToDxa(double cm) => (int)(cm * 567.0);
|
||||
public static int PtToDxa(double pt) => (int)(pt * 20);
|
||||
public static long InchesToEmu(double inches) => (long)(inches * 914400);
|
||||
public static long CmToEmu(double cm) => (long)(cm * 360000);
|
||||
public static int PtToHalfPt(double pt) => (int)(pt * 2);
|
||||
public static string FontSizeToSz(double ptSize) => ((int)(ptSize * 2)).ToString();
|
||||
|
||||
public static double DxaToInches(int dxa) => dxa / 1440.0;
|
||||
public static double DxaToCm(int dxa) => dxa / 567.0;
|
||||
public static double DxaToPt(int dxa) => dxa / 20.0;
|
||||
public static double EmuToInches(long emu) => emu / 914400.0;
|
||||
public static double EmuToCm(long emu) => emu / 360000.0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,910 @@
|
||||
// ============================================================================
|
||||
// AestheticRecipeSamples_Batch1.cs — IEEE & ACM conference paper recipes
|
||||
// ============================================================================
|
||||
// Two-column academic conference styles faithfully reproducing the typographic
|
||||
// conventions of IEEEtran.cls and acmart.cls for DOCX output.
|
||||
//
|
||||
// UNIT REFERENCE:
|
||||
// Font size: half-points (20 = 10pt, 18 = 9pt, 16 = 8pt)
|
||||
// Spacing: DXA = twentieths of a point (1440 DXA = 1 inch)
|
||||
// Borders: eighth-points (4 = 0.5pt, 8 = 1pt, 12 = 1.5pt)
|
||||
// Line spacing "line": 240ths of single spacing (240 = 1.0x)
|
||||
// ============================================================================
|
||||
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
|
||||
using WpColumns = DocumentFormat.OpenXml.Wordprocessing.Columns;
|
||||
using WpPageSize = DocumentFormat.OpenXml.Wordprocessing.PageSize;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Samples;
|
||||
|
||||
public static partial class AestheticRecipeSamples
|
||||
{
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// RECIPE 6: IEEE CONFERENCE (IEEEtran)
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// Recipe: IEEE Conference Paper (IEEEtran.cls v1.8b)
|
||||
/// Source: IEEEtran.cls v1.8b — the standard LaTeX class for IEEE transactions
|
||||
/// and conference proceedings.
|
||||
///
|
||||
/// Feel: Dense, formal, information-rich two-column layout.
|
||||
/// Best for: IEEE conference submissions, transactions papers, technical reports
|
||||
/// following IEEE style.
|
||||
///
|
||||
/// Design rationale (all values from IEEEtran.cls source):
|
||||
/// - US Letter, narrow margins (0.625in L/R): maximizes text area for the
|
||||
/// two-column layout. IEEE papers prioritize information density.
|
||||
/// - Two columns with 0.25in (360 DXA) gutter: standard IEEE column separation.
|
||||
/// Narrow gutter is feasible because the small font creates short line lengths.
|
||||
/// - 10pt Times New Roman body (sz=20): IEEE's standard body size. TNR is the
|
||||
/// required typeface. 10pt in two columns yields ~40 characters per line —
|
||||
/// optimal for rapid technical reading.
|
||||
/// - 24pt title, centered, NOT bold (sz=48): IEEEtran titles are large but
|
||||
/// use regular weight. The size alone provides hierarchy.
|
||||
/// - Section headings (H1): 10pt small caps, centered, Roman numeral prefix
|
||||
/// convention (sz=20). Small caps at body size creates subtle hierarchy
|
||||
/// without disrupting the dense layout.
|
||||
/// - Subsection headings (H2): 10pt italic, flush left (sz=20). Italic at
|
||||
/// body size is the minimal viable distinction from body text.
|
||||
/// - Single spacing (line=240): mandatory for IEEE camera-ready format.
|
||||
/// - First-line indent 0.125in (180 DXA): very small indent suits the narrow
|
||||
/// column width.
|
||||
/// - 0pt paragraph spacing: IEEE uses no inter-paragraph space; the first-line
|
||||
/// indent is the sole paragraph separator.
|
||||
/// - Captions: 8pt (sz=16) — subordinate to body, centered under figures/tables.
|
||||
/// </summary>
|
||||
public static void CreateIEEEConferenceDocument(string outputPath)
|
||||
{
|
||||
using var doc = WordprocessingDocument.Create(outputPath, WordprocessingDocumentType.Document);
|
||||
|
||||
var mainPart = doc.AddMainDocumentPart();
|
||||
mainPart.Document = new Document(new Body());
|
||||
var body = mainPart.Document.Body!;
|
||||
|
||||
// ── Styles ──
|
||||
var stylesPart = mainPart.AddNewPart<StyleDefinitionsPart>();
|
||||
stylesPart.Styles = new Styles();
|
||||
var styles = stylesPart.Styles;
|
||||
|
||||
// DocDefaults: Times New Roman 10pt, single spacing, 0.125in first-line indent
|
||||
styles.Append(new DocDefaults(
|
||||
new RunPropertiesDefault(
|
||||
new RunPropertiesBaseStyle(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman",
|
||||
EastAsia = "SimSun",
|
||||
ComplexScript = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "20" }, // 10pt body (IEEEtran standard)
|
||||
new FontSizeComplexScript { Val = "20" },
|
||||
new Color { Val = "000000" }, // Pure black
|
||||
new Languages { Val = "en-US", EastAsia = "zh-CN" }
|
||||
)
|
||||
),
|
||||
new ParagraphPropertiesDefault(
|
||||
new ParagraphPropertiesBaseStyle(
|
||||
new SpacingBetweenLines
|
||||
{
|
||||
// Single spacing: mandatory for IEEE camera-ready
|
||||
Line = "240",
|
||||
LineRule = LineSpacingRuleValues.Auto,
|
||||
After = "0",
|
||||
Before = "0"
|
||||
},
|
||||
// First-line indent: 0.125in = 180 DXA (very small, suits narrow columns)
|
||||
new Indentation { FirstLine = "180" }
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
// ── Normal style ──
|
||||
styles.Append(CreateParagraphStyle(
|
||||
styleId: "Normal",
|
||||
styleName: "Normal",
|
||||
isDefault: true,
|
||||
uiPriority: 0
|
||||
));
|
||||
|
||||
// ── Title style: 24pt centered, NOT bold ──
|
||||
// IEEEtran.cls \maketitle: \LARGE (24pt at 10pt base), centered, no bold
|
||||
var titleRPr = new StyleRunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman",
|
||||
EastAsia = "SimSun",
|
||||
ComplexScript = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "48" }, // 24pt
|
||||
new FontSizeComplexScript { Val = "48" },
|
||||
new Color { Val = "000000" }
|
||||
// No Bold — IEEEtran titles are NOT bold
|
||||
);
|
||||
|
||||
styles.Append(new Style(
|
||||
new StyleName { Val = "Title" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new NextParagraphStyle { Val = "Normal" },
|
||||
new UIPriority { Val = 10 },
|
||||
new PrimaryStyle(),
|
||||
new StyleParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center },
|
||||
new SpacingBetweenLines { Before = "0", After = "240" },
|
||||
new Indentation { FirstLine = "0" } // No indent for title
|
||||
),
|
||||
titleRPr
|
||||
)
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = "Title",
|
||||
Default = false
|
||||
});
|
||||
|
||||
// ── Heading 1: 10pt small caps, centered ──
|
||||
// IEEEtran \section: \centering\scshape at body size, Roman numeral prefix
|
||||
var h1RPr = new StyleRunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman",
|
||||
EastAsia = "SimSun",
|
||||
ComplexScript = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "20" }, // 10pt — same as body
|
||||
new FontSizeComplexScript { Val = "20" },
|
||||
new Color { Val = "000000" },
|
||||
new SmallCaps() // Small caps for section headings
|
||||
);
|
||||
|
||||
styles.Append(new Style(
|
||||
new StyleName { Val = "heading 1" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new NextParagraphStyle { Val = "Normal" },
|
||||
new UIPriority { Val = 9 },
|
||||
new PrimaryStyle(),
|
||||
new StyleParagraphProperties(
|
||||
new KeepNext(),
|
||||
new KeepLines(),
|
||||
new Justification { Val = JustificationValues.Center },
|
||||
new SpacingBetweenLines { Before = "240", After = "120" },
|
||||
new Indentation { FirstLine = "0" },
|
||||
new OutlineLevel { Val = 0 }
|
||||
),
|
||||
h1RPr
|
||||
)
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = "Heading1",
|
||||
Default = false
|
||||
});
|
||||
|
||||
// ── Heading 2: 10pt italic, flush left ──
|
||||
// IEEEtran \subsection: \itshape at body size, flush left
|
||||
var h2RPr = new StyleRunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman",
|
||||
EastAsia = "SimSun",
|
||||
ComplexScript = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "20" }, // 10pt — same as body
|
||||
new FontSizeComplexScript { Val = "20" },
|
||||
new Color { Val = "000000" },
|
||||
new Italic() // Italic for subsection headings
|
||||
);
|
||||
|
||||
styles.Append(new Style(
|
||||
new StyleName { Val = "heading 2" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new NextParagraphStyle { Val = "Normal" },
|
||||
new UIPriority { Val = 9 },
|
||||
new PrimaryStyle(),
|
||||
new StyleParagraphProperties(
|
||||
new KeepNext(),
|
||||
new KeepLines(),
|
||||
new SpacingBetweenLines { Before = "180", After = "60" },
|
||||
new Indentation { FirstLine = "0" },
|
||||
new OutlineLevel { Val = 1 }
|
||||
),
|
||||
h2RPr
|
||||
)
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = "Heading2",
|
||||
Default = false
|
||||
});
|
||||
|
||||
// ── Abstract style: 9pt bold "Abstract" label convention ──
|
||||
styles.Append(CreateParagraphStyle(
|
||||
styleId: "Abstract",
|
||||
styleName: "Abstract",
|
||||
basedOn: "Normal",
|
||||
uiPriority: 11
|
||||
));
|
||||
|
||||
// ── Caption style: 8pt (sz=16) ──
|
||||
styles.Append(CreateCaptionStyle(
|
||||
fontSizeHalfPts: "16", // 8pt — IEEE standard caption size
|
||||
color: "000000",
|
||||
italic: false // IEEE captions are not italic
|
||||
));
|
||||
|
||||
// ── Page setup: US Letter, IEEE margins, two-column ──
|
||||
// IEEEtran.cls: top=0.75in, bottom=1in, left=right=0.625in
|
||||
var sectPr = new SectionProperties(
|
||||
new WpPageSize { Width = 12240U, Height = 15840U }, // US Letter
|
||||
new PageMargin
|
||||
{
|
||||
Top = 1080, // 0.75in
|
||||
Bottom = 1440, // 1in
|
||||
Left = 900U, // 0.625in
|
||||
Right = 900U, // 0.625in
|
||||
Header = 720U, Footer = 720U, Gutter = 0U
|
||||
},
|
||||
// Two-column layout: 0.25in gutter = 360 DXA
|
||||
new WpColumns { ColumnCount = 2, Space = "360" }
|
||||
);
|
||||
|
||||
// ── Page numbers: bottom center, 8pt ──
|
||||
AddPageNumberFooter(mainPart, sectPr,
|
||||
alignment: JustificationValues.Center,
|
||||
fontSizeHalfPts: "16", // 8pt
|
||||
color: "000000",
|
||||
format: PageNumberFormat.Plain
|
||||
);
|
||||
|
||||
// ── Sample content: IEEE paper structure ──
|
||||
|
||||
// Title (spans both columns via the Title style)
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Title" }
|
||||
),
|
||||
new Run(new Text("Deep Learning Approaches for Automated Document Layout Analysis"))
|
||||
));
|
||||
|
||||
// Author line (centered, no indent)
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center },
|
||||
new SpacingBetweenLines { After = "120" },
|
||||
new Indentation { FirstLine = "0" }
|
||||
),
|
||||
new Run(
|
||||
new RunProperties(new FontSize { Val = "20" }, new FontSizeComplexScript { Val = "20" }),
|
||||
new Text("Jane A. Smith, John B. Doe, and Alice C. Johnson")
|
||||
)
|
||||
));
|
||||
|
||||
// Affiliation (centered, italic, smaller)
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center },
|
||||
new SpacingBetweenLines { After = "240" },
|
||||
new Indentation { FirstLine = "0" }
|
||||
),
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new FontSize { Val = "18" }, new FontSizeComplexScript { Val = "18" },
|
||||
new Italic()
|
||||
),
|
||||
new Text("Department of Computer Science, Example University, City, Country")
|
||||
)
|
||||
));
|
||||
|
||||
// Abstract
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Abstract" },
|
||||
new Indentation { FirstLine = "0" },
|
||||
new SpacingBetweenLines { After = "120" }
|
||||
),
|
||||
new Run(
|
||||
new RunProperties(new Bold(), new Italic(), new FontSize { Val = "18" }, new FontSizeComplexScript { Val = "18" }),
|
||||
new Text("Abstract") { Space = SpaceProcessingModeValues.Preserve }
|
||||
),
|
||||
new Run(
|
||||
new RunProperties(new FontSize { Val = "18" }, new FontSizeComplexScript { Val = "18" }),
|
||||
new Text("\u2014This paper presents a comprehensive framework for automated document "
|
||||
+ "layout analysis using deep learning. We propose a novel architecture that "
|
||||
+ "combines convolutional neural networks with transformer-based attention "
|
||||
+ "mechanisms to accurately segment and classify document regions. Experimental "
|
||||
+ "results on benchmark datasets demonstrate state-of-the-art performance.")
|
||||
{ Space = SpaceProcessingModeValues.Preserve }
|
||||
)
|
||||
));
|
||||
|
||||
// I. INTRODUCTION (Roman numeral convention rendered in text)
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Heading1" }
|
||||
),
|
||||
new Run(new Text("I. Introduction"))
|
||||
));
|
||||
|
||||
AddSampleParagraph(body, "Document layout analysis is a fundamental step in document "
|
||||
+ "understanding pipelines. The ability to automatically identify and classify "
|
||||
+ "regions within a document image has applications in digitization, information "
|
||||
+ "extraction, and accessibility.", "Normal");
|
||||
|
||||
AddSampleParagraph(body, "Recent advances in deep learning have significantly improved "
|
||||
+ "the accuracy of layout analysis systems. However, challenges remain in handling "
|
||||
+ "complex multi-column layouts and heterogeneous document types.", "Normal");
|
||||
|
||||
// II. RELATED WORK
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Heading1" }
|
||||
),
|
||||
new Run(new Text("II. Related Work"))
|
||||
));
|
||||
|
||||
// A. Subsection
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Heading2" }
|
||||
),
|
||||
new Run(new Text("A. Traditional Methods"))
|
||||
));
|
||||
|
||||
AddSampleParagraph(body, "Early approaches to document layout analysis relied on "
|
||||
+ "rule-based methods and connected component analysis. These methods perform well "
|
||||
+ "on structured documents but struggle with complex layouts.", "Normal");
|
||||
|
||||
// B. Subsection
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Heading2" }
|
||||
),
|
||||
new Run(new Text("B. Deep Learning Methods"))
|
||||
));
|
||||
|
||||
AddSampleParagraph(body, "Convolutional neural networks have been successfully applied "
|
||||
+ "to document layout analysis, achieving significant improvements over traditional "
|
||||
+ "methods on standard benchmarks.", "Normal");
|
||||
|
||||
// III. PROPOSED METHOD
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Heading1" }
|
||||
),
|
||||
new Run(new Text("III. Proposed Method"))
|
||||
));
|
||||
|
||||
AddSampleParagraph(body, "Our proposed framework integrates a feature pyramid network "
|
||||
+ "backbone with a transformer decoder module. The architecture processes document "
|
||||
+ "images at multiple scales to capture both fine-grained character-level features "
|
||||
+ "and coarse layout structures.", "Normal");
|
||||
|
||||
// Table
|
||||
body.Append(CreateThreeLineTable(
|
||||
new[] { "Method", "Precision", "Recall", "F1" },
|
||||
new[]
|
||||
{
|
||||
new[] { "Rule-based", "0.823", "0.791", "0.807" },
|
||||
new[] { "CNN-only", "0.912", "0.887", "0.899" },
|
||||
new[] { "Ours", "0.956", "0.943", "0.949" }
|
||||
}
|
||||
));
|
||||
|
||||
AddSampleParagraph(body, "TABLE I: Comparison of layout analysis methods on PubLayNet.", "Caption");
|
||||
|
||||
// IV. CONCLUSION
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Heading1" }
|
||||
),
|
||||
new Run(new Text("IV. Conclusion"))
|
||||
));
|
||||
|
||||
AddSampleParagraph(body, "We have presented a novel deep learning framework for document "
|
||||
+ "layout analysis that achieves state-of-the-art results. Future work will explore "
|
||||
+ "extending the approach to handle more diverse document types.", "Normal");
|
||||
|
||||
// Section properties must be last child of body
|
||||
body.Append(sectPr);
|
||||
}
|
||||
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// RECIPE 7: ACM CONFERENCE (acmart)
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// Recipe: ACM Conference Paper (acmart.cls v2.x, ACM Author Guide)
|
||||
/// Source: acmart.cls v2.x — the consolidated ACM master article template,
|
||||
/// and the ACM Author Guide for typographic specifications.
|
||||
///
|
||||
/// Feel: Clean, structured, slightly more open than IEEE.
|
||||
/// Best for: ACM conference proceedings (SIGCHI, SIGMOD, SIGGRAPH, etc.),
|
||||
/// ACM journal submissions.
|
||||
///
|
||||
/// Design rationale (all values from acmart.cls and ACM Author Guide):
|
||||
/// - US Letter, 1.25in top/bottom, 0.75in L/R: more generous vertical margins
|
||||
/// than IEEE, giving a less cramped appearance.
|
||||
/// - Two columns with 0.33in (480 DXA) gutter: slightly wider than IEEE's
|
||||
/// 0.25in, providing better visual separation between columns.
|
||||
/// - 9pt Times New Roman body (sz=18): ACM's standard body size. The original
|
||||
/// acmart uses Linux Libertine, but TNR is the accessible fallback specified
|
||||
/// in the ACM Author Guide for systems without Libertine.
|
||||
/// - 14.4pt bold title, flush left (sz=29): ACM titles are bold and left-aligned,
|
||||
/// unlike IEEE's centered unbolded titles. The 14.4pt size (1.6x body) creates
|
||||
/// strong but not overwhelming hierarchy.
|
||||
/// - H1: 10pt bold ALL CAPS, flush left, arabic numbered (sz=20). ALL CAPS at
|
||||
/// body size with bold creates definitive section breaks.
|
||||
/// - H2: 10pt bold title case, flush left (sz=20). Bold without caps is the
|
||||
/// minimal step down from H1.
|
||||
/// - H3: 10pt bold italic, flush left (sz=20). Adding italic distinguishes
|
||||
/// from H2 while maintaining the same weight.
|
||||
/// - Single spacing: required for ACM camera-ready format.
|
||||
/// - First-line indent ~10pt (200 DXA): slightly larger than IEEE's 0.125in,
|
||||
/// matching ACM's convention of a roughly 1em indent at 9pt.
|
||||
/// - Captions: 8pt (sz=16) — consistent with ACM figure/table caption style.
|
||||
/// - References: 7.5pt (sz=15) — ACM uses a smaller font for the bibliography
|
||||
/// to maximize space for content.
|
||||
/// </summary>
|
||||
public static void CreateACMConferenceDocument(string outputPath)
|
||||
{
|
||||
using var doc = WordprocessingDocument.Create(outputPath, WordprocessingDocumentType.Document);
|
||||
|
||||
var mainPart = doc.AddMainDocumentPart();
|
||||
mainPart.Document = new Document(new Body());
|
||||
var body = mainPart.Document.Body!;
|
||||
|
||||
// ── Styles ──
|
||||
var stylesPart = mainPart.AddNewPart<StyleDefinitionsPart>();
|
||||
stylesPart.Styles = new Styles();
|
||||
var styles = stylesPart.Styles;
|
||||
|
||||
// DocDefaults: Times New Roman 9pt (TNR as Libertine fallback), single spacing
|
||||
styles.Append(new DocDefaults(
|
||||
new RunPropertiesDefault(
|
||||
new RunPropertiesBaseStyle(
|
||||
new RunFonts
|
||||
{
|
||||
// ACM specifies Linux Libertine; TNR is the accessible fallback
|
||||
// per ACM Author Guide for systems without Libertine installed
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman",
|
||||
EastAsia = "SimSun",
|
||||
ComplexScript = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "18" }, // 9pt body (acmart standard)
|
||||
new FontSizeComplexScript { Val = "18" },
|
||||
new Color { Val = "000000" }, // Pure black
|
||||
new Languages { Val = "en-US", EastAsia = "zh-CN" }
|
||||
)
|
||||
),
|
||||
new ParagraphPropertiesDefault(
|
||||
new ParagraphPropertiesBaseStyle(
|
||||
new SpacingBetweenLines
|
||||
{
|
||||
// Single spacing: ACM camera-ready requirement
|
||||
Line = "240",
|
||||
LineRule = LineSpacingRuleValues.Auto,
|
||||
After = "0",
|
||||
Before = "0"
|
||||
},
|
||||
// First-line indent: ~10pt = 200 DXA (roughly 1em at 9pt)
|
||||
new Indentation { FirstLine = "200" }
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
// ── Normal style ──
|
||||
styles.Append(CreateParagraphStyle(
|
||||
styleId: "Normal",
|
||||
styleName: "Normal",
|
||||
isDefault: true,
|
||||
uiPriority: 0
|
||||
));
|
||||
|
||||
// ── Title style: 14.4pt bold, flush left ──
|
||||
// acmart \maketitle: \LARGE\bfseries, left-aligned
|
||||
var titleRPr = new StyleRunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman",
|
||||
EastAsia = "SimSun",
|
||||
ComplexScript = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "29" }, // 14.4pt (≈29 half-points)
|
||||
new FontSizeComplexScript { Val = "29" },
|
||||
new Color { Val = "000000" },
|
||||
new Bold() // ACM titles ARE bold
|
||||
);
|
||||
|
||||
styles.Append(new Style(
|
||||
new StyleName { Val = "Title" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new NextParagraphStyle { Val = "Normal" },
|
||||
new UIPriority { Val = 10 },
|
||||
new PrimaryStyle(),
|
||||
new StyleParagraphProperties(
|
||||
// Flush left — ACM titles are NOT centered
|
||||
new SpacingBetweenLines { Before = "0", After = "200" },
|
||||
new Indentation { FirstLine = "0" }
|
||||
),
|
||||
titleRPr
|
||||
)
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = "Title",
|
||||
Default = false
|
||||
});
|
||||
|
||||
// ── Heading 1: 10pt bold ALL CAPS, flush left ──
|
||||
// acmart \section: \bfseries at body size, uppercase
|
||||
var h1RPr = new StyleRunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman",
|
||||
EastAsia = "SimSun",
|
||||
ComplexScript = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "20" }, // 10pt
|
||||
new FontSizeComplexScript { Val = "20" },
|
||||
new Color { Val = "000000" },
|
||||
new Bold(),
|
||||
new Caps() // ALL CAPS for H1
|
||||
);
|
||||
|
||||
styles.Append(new Style(
|
||||
new StyleName { Val = "heading 1" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new NextParagraphStyle { Val = "Normal" },
|
||||
new UIPriority { Val = 9 },
|
||||
new PrimaryStyle(),
|
||||
new StyleParagraphProperties(
|
||||
new KeepNext(),
|
||||
new KeepLines(),
|
||||
new SpacingBetweenLines { Before = "240", After = "120" },
|
||||
new Indentation { FirstLine = "0" },
|
||||
new OutlineLevel { Val = 0 }
|
||||
),
|
||||
h1RPr
|
||||
)
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = "Heading1",
|
||||
Default = false
|
||||
});
|
||||
|
||||
// ── Heading 2: 10pt bold title case, flush left ──
|
||||
// acmart \subsection: \bfseries, no case change
|
||||
var h2RPr = new StyleRunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman",
|
||||
EastAsia = "SimSun",
|
||||
ComplexScript = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "20" }, // 10pt
|
||||
new FontSizeComplexScript { Val = "20" },
|
||||
new Color { Val = "000000" },
|
||||
new Bold() // Bold, no caps
|
||||
);
|
||||
|
||||
styles.Append(new Style(
|
||||
new StyleName { Val = "heading 2" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new NextParagraphStyle { Val = "Normal" },
|
||||
new UIPriority { Val = 9 },
|
||||
new PrimaryStyle(),
|
||||
new StyleParagraphProperties(
|
||||
new KeepNext(),
|
||||
new KeepLines(),
|
||||
new SpacingBetweenLines { Before = "200", After = "80" },
|
||||
new Indentation { FirstLine = "0" },
|
||||
new OutlineLevel { Val = 1 }
|
||||
),
|
||||
h2RPr
|
||||
)
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = "Heading2",
|
||||
Default = false
|
||||
});
|
||||
|
||||
// ── Heading 3: 10pt bold italic, flush left ──
|
||||
// acmart \subsubsection: \bfseries\itshape
|
||||
var h3RPr = new StyleRunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman",
|
||||
EastAsia = "SimSun",
|
||||
ComplexScript = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "20" }, // 10pt
|
||||
new FontSizeComplexScript { Val = "20" },
|
||||
new Color { Val = "000000" },
|
||||
new Bold(),
|
||||
new Italic() // Bold italic for H3
|
||||
);
|
||||
|
||||
styles.Append(new Style(
|
||||
new StyleName { Val = "heading 3" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new NextParagraphStyle { Val = "Normal" },
|
||||
new UIPriority { Val = 9 },
|
||||
new PrimaryStyle(),
|
||||
new StyleParagraphProperties(
|
||||
new KeepNext(),
|
||||
new KeepLines(),
|
||||
new SpacingBetweenLines { Before = "160", After = "60" },
|
||||
new Indentation { FirstLine = "0" },
|
||||
new OutlineLevel { Val = 2 }
|
||||
),
|
||||
h3RPr
|
||||
)
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = "Heading3",
|
||||
Default = false
|
||||
});
|
||||
|
||||
// ── Caption style: 8pt (sz=16) ──
|
||||
styles.Append(CreateCaptionStyle(
|
||||
fontSizeHalfPts: "16", // 8pt — ACM standard caption size
|
||||
color: "000000",
|
||||
italic: false
|
||||
));
|
||||
|
||||
// ── References style: 7.5pt (sz=15) ──
|
||||
var refsRPr = new StyleRunProperties(
|
||||
new FontSize { Val = "15" }, // 7.5pt
|
||||
new FontSizeComplexScript { Val = "15" }
|
||||
);
|
||||
|
||||
styles.Append(new Style(
|
||||
new StyleName { Val = "References" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new UIPriority { Val = 37 },
|
||||
new PrimaryStyle(),
|
||||
new StyleParagraphProperties(
|
||||
new SpacingBetweenLines { After = "40" },
|
||||
new Indentation { FirstLine = "0", Left = "360", Hanging = "360" }
|
||||
),
|
||||
refsRPr
|
||||
)
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = "References",
|
||||
Default = false
|
||||
});
|
||||
|
||||
// ── Page setup: US Letter, ACM margins, two-column ──
|
||||
// acmart.cls: top=1.25in, bottom=1.25in, left=right=0.75in
|
||||
var sectPr = new SectionProperties(
|
||||
new WpPageSize { Width = 12240U, Height = 15840U }, // US Letter
|
||||
new PageMargin
|
||||
{
|
||||
Top = 1800, // 1.25in
|
||||
Bottom = 1800, // 1.25in
|
||||
Left = 1080U, // 0.75in
|
||||
Right = 1080U, // 0.75in
|
||||
Header = 720U, Footer = 720U, Gutter = 0U
|
||||
},
|
||||
// Two-column layout: 0.33in gutter = 480 DXA
|
||||
new WpColumns { ColumnCount = 2, Space = "480" }
|
||||
);
|
||||
|
||||
// ── Page numbers: bottom center, 8pt ──
|
||||
AddPageNumberFooter(mainPart, sectPr,
|
||||
alignment: JustificationValues.Center,
|
||||
fontSizeHalfPts: "16", // 8pt
|
||||
color: "000000",
|
||||
format: PageNumberFormat.Plain
|
||||
);
|
||||
|
||||
// ── Sample content: ACM paper structure ──
|
||||
|
||||
// Title (flush left, bold)
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Title" }
|
||||
),
|
||||
new Run(new Text("Towards Scalable Graph Neural Networks for Heterogeneous Document Understanding"))
|
||||
));
|
||||
|
||||
// Author block (flush left)
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new SpacingBetweenLines { After = "60" },
|
||||
new Indentation { FirstLine = "0" }
|
||||
),
|
||||
new Run(
|
||||
new RunProperties(new FontSize { Val = "18" }, new FontSizeComplexScript { Val = "18" }),
|
||||
new Text("Maria R. Garcia")
|
||||
)
|
||||
));
|
||||
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new SpacingBetweenLines { After = "60" },
|
||||
new Indentation { FirstLine = "0" }
|
||||
),
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new FontSize { Val = "16" }, new FontSizeComplexScript { Val = "16" },
|
||||
new Italic()
|
||||
),
|
||||
new Text("Example University, City, Country")
|
||||
)
|
||||
));
|
||||
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new SpacingBetweenLines { After = "200" },
|
||||
new Indentation { FirstLine = "0" }
|
||||
),
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new FontSize { Val = "16" }, new FontSizeComplexScript { Val = "16" }
|
||||
),
|
||||
new Text("garcia@example.edu")
|
||||
)
|
||||
));
|
||||
|
||||
// Abstract section
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Indentation { FirstLine = "0" },
|
||||
new SpacingBetweenLines { After = "80" }
|
||||
),
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new Bold(),
|
||||
new FontSize { Val = "18" }, new FontSizeComplexScript { Val = "18" }
|
||||
),
|
||||
new Text("ABSTRACT")
|
||||
)
|
||||
));
|
||||
|
||||
AddSampleParagraph(body, "Graph neural networks (GNNs) have emerged as a powerful tool for "
|
||||
+ "document understanding tasks that require modeling relationships between document "
|
||||
+ "elements. We present a scalable GNN architecture that processes heterogeneous "
|
||||
+ "document graphs containing text, table, and figure nodes. Our approach achieves "
|
||||
+ "competitive results while reducing computational costs by 40%.", "Normal");
|
||||
|
||||
// CCS Concepts / Keywords (ACM-specific metadata)
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Indentation { FirstLine = "0" },
|
||||
new SpacingBetweenLines { Before = "120", After = "120" }
|
||||
),
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new Bold(),
|
||||
new FontSize { Val = "16" }, new FontSizeComplexScript { Val = "16" }
|
||||
),
|
||||
new Text("Keywords: ") { Space = SpaceProcessingModeValues.Preserve }
|
||||
),
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new FontSize { Val = "16" }, new FontSizeComplexScript { Val = "16" }
|
||||
),
|
||||
new Text("graph neural networks, document understanding, scalability")
|
||||
)
|
||||
));
|
||||
|
||||
// 1 INTRODUCTION (arabic numbered, ALL CAPS via style)
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Heading1" }
|
||||
),
|
||||
new Run(new Text("1 Introduction"))
|
||||
));
|
||||
|
||||
AddSampleParagraph(body, "Document understanding encompasses a broad set of tasks including "
|
||||
+ "layout analysis, information extraction, and document classification. Recent work "
|
||||
+ "has demonstrated that modeling the structural relationships between document "
|
||||
+ "elements can significantly improve performance on these tasks.", "Normal");
|
||||
|
||||
AddSampleParagraph(body, "Graph neural networks provide a natural framework for representing "
|
||||
+ "and reasoning about document structure. However, existing GNN-based approaches face "
|
||||
+ "scalability challenges when processing large or complex documents.", "Normal");
|
||||
|
||||
// 2 RELATED WORK
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Heading1" }
|
||||
),
|
||||
new Run(new Text("2 Related Work"))
|
||||
));
|
||||
|
||||
// 2.1 Subsection
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Heading2" }
|
||||
),
|
||||
new Run(new Text("2.1 Document Representation Learning"))
|
||||
));
|
||||
|
||||
AddSampleParagraph(body, "Pre-trained language models have been adapted for document "
|
||||
+ "understanding by incorporating layout information. LayoutLM and its successors "
|
||||
+ "demonstrate the value of multi-modal pre-training for document tasks.", "Normal");
|
||||
|
||||
// 2.1.1 Sub-subsection
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Heading3" }
|
||||
),
|
||||
new Run(new Text("2.1.1 Multi-Modal Approaches"))
|
||||
));
|
||||
|
||||
AddSampleParagraph(body, "Multi-modal approaches jointly model text, layout, and visual "
|
||||
+ "features. This integration has proven critical for tasks where visual appearance "
|
||||
+ "carries semantic meaning, such as form understanding.", "Normal");
|
||||
|
||||
// 3 METHOD
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Heading1" }
|
||||
),
|
||||
new Run(new Text("3 Proposed Method"))
|
||||
));
|
||||
|
||||
AddSampleParagraph(body, "We propose HetDocGNN, a heterogeneous graph neural network "
|
||||
+ "designed specifically for document understanding. The architecture operates on "
|
||||
+ "a document graph where nodes represent text blocks, tables, and figures, and "
|
||||
+ "edges encode spatial and logical relationships.", "Normal");
|
||||
|
||||
// Results table
|
||||
body.Append(CreateThreeLineTable(
|
||||
new[] { "Model", "DocVQA", "InfoVQA", "Params" },
|
||||
new[]
|
||||
{
|
||||
new[] { "LayoutLMv3", "83.4", "45.1", "133M" },
|
||||
new[] { "UDOP", "84.7", "47.4", "770M" },
|
||||
new[] { "HetDocGNN", "85.2", "48.9", "89M" }
|
||||
}
|
||||
));
|
||||
|
||||
AddSampleParagraph(body, "Table 1: Comparison on document understanding benchmarks.", "Caption");
|
||||
|
||||
// 4 CONCLUSION
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Heading1" }
|
||||
),
|
||||
new Run(new Text("4 Conclusion"))
|
||||
));
|
||||
|
||||
AddSampleParagraph(body, "We have presented HetDocGNN, a scalable graph neural network "
|
||||
+ "for heterogeneous document understanding. Our approach achieves state-of-the-art "
|
||||
+ "results with significantly fewer parameters than competing methods.", "Normal");
|
||||
|
||||
// REFERENCES section
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Heading1" }
|
||||
),
|
||||
new Run(new Text("References"))
|
||||
));
|
||||
|
||||
// Sample references in ACM style (7.5pt)
|
||||
AddSampleParagraph(body, "[1] Yiheng Xu, et al. 2020. LayoutLM: Pre-training of Text and "
|
||||
+ "Layout for Document Image Understanding. In KDD '20. ACM, 1192\u20131200.", "References");
|
||||
|
||||
AddSampleParagraph(body, "[2] Zhiliang Peng, et al. 2023. UDOP: Unifying Vision, Text, "
|
||||
+ "and Layout for Universal Document Processing. In CVPR '23. 19254\u201319264.", "References");
|
||||
|
||||
AddSampleParagraph(body, "[3] Zilong Wang, et al. 2022. DocFormer: End-to-End Transformer "
|
||||
+ "for Document Understanding. In ICCV '22. 993\u20131003.", "References");
|
||||
|
||||
// Section properties must be last child of body
|
||||
body.Append(sectPr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,999 @@
|
||||
// ============================================================================
|
||||
// AestheticRecipeSamples_Batch2.cs — Academic citation style recipes (APA 7, MLA 9)
|
||||
// ============================================================================
|
||||
// Recipes 8-9: Strict compliance with academic citation style guides.
|
||||
// These are NOT aesthetic "design" choices — they are codified standards
|
||||
// mandated by publishers, universities, and professional organizations.
|
||||
//
|
||||
// UNIT REFERENCE:
|
||||
// Font size: half-points (22 = 11pt, 24 = 12pt, 32 = 16pt)
|
||||
// Spacing: DXA = twentieths of a point (1440 DXA = 1 inch)
|
||||
// Borders: eighth-points (4 = 0.5pt, 8 = 1pt, 12 = 1.5pt)
|
||||
// Line spacing "line": 240ths of single spacing (240 = 1.0x, 480 = 2.0x)
|
||||
// ============================================================================
|
||||
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
|
||||
using WpPageSize = DocumentFormat.OpenXml.Wordprocessing.PageSize;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Samples;
|
||||
|
||||
public static partial class AestheticRecipeSamples
|
||||
{
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// RECIPE 8: APA 7TH EDITION (PROFESSIONAL PAPER)
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// Recipe: APA 7th Edition — Professional Paper
|
||||
/// Source: Publication Manual of the American Psychological Association,
|
||||
/// 7th edition (2020), Chapters 2 (Paper Elements) and 6 (Mechanics of Style).
|
||||
///
|
||||
/// Key APA 7 specifications:
|
||||
/// - Font: 12pt Times New Roman (Section 2.19). Also acceptable: 11pt Calibri,
|
||||
/// 11pt Arial, 10pt Lucida Sans Unicode, or 11pt Georgia.
|
||||
/// - Margins: 1 inch on all sides (Section 2.22).
|
||||
/// - Line spacing: Double-spaced throughout, including title page and references (Section 2.21).
|
||||
/// - Paragraph indent: 0.5 inch first-line indent for body paragraphs (Section 2.24).
|
||||
/// - Heading levels (Section 2.27):
|
||||
/// Level 1: Centered, Bold, Title Case Heading
|
||||
/// Level 2: Flush Left, Bold, Title Case Heading
|
||||
/// Level 3: Flush Left, Bold Italic, Title Case Heading
|
||||
/// Level 4: Indented, Bold, Title Case Heading, Ending With a Period. (run-in)
|
||||
/// Level 5: Indented, Bold Italic, Title Case Heading, Ending With a Period. (run-in)
|
||||
/// All headings are 12pt — hierarchy through format, NOT size.
|
||||
/// - Page numbers: top right corner on every page including title page (Section 2.18).
|
||||
/// - Running head: flush left, ALL CAPS, for professional papers only (Section 2.18).
|
||||
/// - Abstract: "Abstract" centered bold; single paragraph, not indented (Section 2.9).
|
||||
/// - No numbered headings (APA does not use section numbers).
|
||||
///
|
||||
/// Design rationale:
|
||||
/// - Every parameter is dictated by the style guide, not aesthetic preference.
|
||||
/// - Double spacing with first-line indent (no paragraph spacing) is the
|
||||
/// traditional academic convention — it provides annotation room and
|
||||
/// clear paragraph boundaries without wasting vertical space.
|
||||
/// - Uniform 12pt headings ensure the text content is primary; headings
|
||||
/// serve as navigational aids, not visual statements.
|
||||
/// </summary>
|
||||
public static void CreateAPA7Document(string outputPath)
|
||||
{
|
||||
using var doc = WordprocessingDocument.Create(outputPath, WordprocessingDocumentType.Document);
|
||||
|
||||
var mainPart = doc.AddMainDocumentPart();
|
||||
mainPart.Document = new Document(new Body());
|
||||
var body = mainPart.Document.Body!;
|
||||
|
||||
// ── Styles ──
|
||||
var stylesPart = mainPart.AddNewPart<StyleDefinitionsPart>();
|
||||
stylesPart.Styles = new Styles();
|
||||
var styles = stylesPart.Styles;
|
||||
|
||||
// DocDefaults: 12pt Times New Roman, double spacing, 0.5in first-line indent
|
||||
// NOTE: 11pt Calibri and 11pt Arial are also acceptable per APA 7 Section 2.19
|
||||
styles.Append(new DocDefaults(
|
||||
new RunPropertiesDefault(
|
||||
new RunPropertiesBaseStyle(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman",
|
||||
EastAsia = "SimSun",
|
||||
ComplexScript = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "24" }, // 12pt (half-points)
|
||||
new FontSizeComplexScript { Val = "24" },
|
||||
new Color { Val = "000000" }, // Pure black
|
||||
new Languages { Val = "en-US", EastAsia = "zh-CN" }
|
||||
)
|
||||
),
|
||||
new ParagraphPropertiesDefault(
|
||||
new ParagraphPropertiesBaseStyle(
|
||||
new SpacingBetweenLines
|
||||
{
|
||||
// Double spacing throughout (APA 7, Section 2.21)
|
||||
// 480 = 2.0x (240 = single spacing)
|
||||
Line = "480",
|
||||
LineRule = LineSpacingRuleValues.Auto,
|
||||
After = "0" // No paragraph spacing — APA uses indent, not space
|
||||
},
|
||||
// First-line indent 0.5in = 720 DXA (APA 7, Section 2.24)
|
||||
new Indentation { FirstLine = "720" }
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
// ── Normal style ──
|
||||
styles.Append(CreateParagraphStyle(
|
||||
styleId: "Normal",
|
||||
styleName: "Normal",
|
||||
isDefault: true,
|
||||
uiPriority: 0
|
||||
));
|
||||
|
||||
// ── APA Level 1: Centered, Bold, Title Case ──
|
||||
// Same 12pt as body — hierarchy via format, NOT size (APA 7, Section 2.27)
|
||||
styles.Append(CreateAcademicHeadingStyle(
|
||||
level: 1,
|
||||
sizeHalfPts: "24", // 12pt — same as body
|
||||
bold: true,
|
||||
italic: false,
|
||||
centered: true,
|
||||
spaceBefore: "480", // One double-spaced blank line before
|
||||
spaceAfter: "0"
|
||||
));
|
||||
|
||||
// ── APA Level 2: Flush Left, Bold, Title Case ──
|
||||
styles.Append(CreateAcademicHeadingStyle(
|
||||
level: 2,
|
||||
sizeHalfPts: "24", // 12pt — same as body
|
||||
bold: true,
|
||||
italic: false,
|
||||
centered: false,
|
||||
spaceBefore: "480",
|
||||
spaceAfter: "0"
|
||||
));
|
||||
|
||||
// ── APA Level 3: Flush Left, Bold Italic, Title Case ──
|
||||
styles.Append(CreateAcademicHeadingStyle(
|
||||
level: 3,
|
||||
sizeHalfPts: "24", // 12pt — same as body
|
||||
bold: true,
|
||||
italic: true,
|
||||
centered: false,
|
||||
spaceBefore: "480",
|
||||
spaceAfter: "0"
|
||||
));
|
||||
|
||||
// ── APA Level 4: Indented 0.5in, Bold, Title Case, Ending With Period. ──
|
||||
// This is a "run-in" heading in APA — the heading text runs into the paragraph.
|
||||
// In OpenXML we approximate by creating an indented bold paragraph.
|
||||
styles.Append(CreateAPA7RunInHeadingStyle(
|
||||
level: 4,
|
||||
bold: true,
|
||||
italic: false
|
||||
));
|
||||
|
||||
// ── APA Level 5: Indented 0.5in, Bold Italic, Title Case, Ending With Period. ──
|
||||
styles.Append(CreateAPA7RunInHeadingStyle(
|
||||
level: 5,
|
||||
bold: true,
|
||||
italic: true
|
||||
));
|
||||
|
||||
// ── "Abstract" label style: centered, bold, no indent ──
|
||||
styles.Append(CreateAPA7NoIndentCenteredStyle(
|
||||
styleId: "APAAbstractLabel",
|
||||
styleName: "APA Abstract Label",
|
||||
bold: true
|
||||
));
|
||||
|
||||
// ── Abstract body style: no first-line indent ──
|
||||
styles.Append(CreateAPA7NoIndentStyle(
|
||||
styleId: "APAAbstractBody",
|
||||
styleName: "APA Abstract Body"
|
||||
));
|
||||
|
||||
// ── Title page style: centered, bold, no indent ──
|
||||
styles.Append(CreateAPA7NoIndentCenteredStyle(
|
||||
styleId: "APATitlePageTitle",
|
||||
styleName: "APA Title Page Title",
|
||||
bold: true
|
||||
));
|
||||
|
||||
// ── Title page author/affiliation: centered, no indent, not bold ──
|
||||
styles.Append(CreateAPA7NoIndentCenteredStyle(
|
||||
styleId: "APATitlePageInfo",
|
||||
styleName: "APA Title Page Info",
|
||||
bold: false
|
||||
));
|
||||
|
||||
// ── Page setup: US Letter, 1in all sides (APA 7, Section 2.22) ──
|
||||
var sectPr = new SectionProperties(
|
||||
new WpPageSize { Width = 12240U, Height = 15840U }, // 8.5" x 11"
|
||||
new PageMargin
|
||||
{
|
||||
Top = 1440, Bottom = 1440,
|
||||
Left = 1440U, Right = 1440U,
|
||||
Header = 720U, Footer = 720U, Gutter = 0U
|
||||
}
|
||||
);
|
||||
|
||||
// ── Running head + page number in header ──
|
||||
// Professional papers: running head flush left (ALL CAPS), page number flush right
|
||||
// Both in the same header (APA 7, Section 2.18)
|
||||
AddAPA7Header(mainPart, sectPr, "COGNITIVE EFFECTS OF SLEEP DEPRIVATION");
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// SAMPLE CONTENT: Title Page, Abstract, Body with all 5 heading levels
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
// ── Title page ──
|
||||
// Title: centered, bold, upper half of page (3-4 blank lines before)
|
||||
AddAPA7TitlePage(body,
|
||||
title: "Cognitive Effects of Sleep Deprivation on Working Memory Performance",
|
||||
authorName: "Sarah J. Mitchell",
|
||||
affiliation: "Department of Psychology, University of Washington",
|
||||
courseLine: "PSY 401: Advanced Cognitive Psychology",
|
||||
instructorLine: "Dr. Robert Chen",
|
||||
dateLine: "October 15, 2024"
|
||||
);
|
||||
|
||||
// ── Abstract page ──
|
||||
AddSampleParagraph(body, "Abstract", "APAAbstractLabel");
|
||||
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "APAAbstractBody" }
|
||||
),
|
||||
new Run(new Text(
|
||||
"This study examined the effects of acute sleep deprivation on working memory "
|
||||
+ "performance in college-aged adults. Participants (N = 48) were randomly assigned "
|
||||
+ "to either a sleep deprivation condition (24 hours without sleep) or a control "
|
||||
+ "condition (normal sleep). Working memory was assessed using a dual n-back task. "
|
||||
+ "Results indicated that sleep-deprived participants showed significantly lower "
|
||||
+ "accuracy (M = 72.3%, SD = 8.1) compared to controls (M = 89.7%, SD = 5.4), "
|
||||
+ "t(46) = 9.12, p < .001, d = 2.52. These findings suggest that even a single "
|
||||
+ "night of sleep deprivation substantially impairs working memory capacity."
|
||||
))
|
||||
));
|
||||
|
||||
// ── Body: Level 1 heading ──
|
||||
AddSampleParagraph(body, "Cognitive Effects of Sleep Deprivation on Working Memory Performance", "Heading1");
|
||||
|
||||
AddSampleParagraph(body,
|
||||
"Sleep deprivation is increasingly prevalent among college students, with approximately "
|
||||
+ "50% reporting insufficient sleep on a regular basis (Hershner & Chervin, 2014). The "
|
||||
+ "consequences of inadequate sleep extend beyond daytime drowsiness, affecting core "
|
||||
+ "cognitive processes including attention, executive function, and working memory.",
|
||||
"Normal");
|
||||
|
||||
// ── Level 2 heading ──
|
||||
AddSampleParagraph(body, "Theoretical Framework", "Heading2");
|
||||
|
||||
AddSampleParagraph(body,
|
||||
"Working memory, as conceptualized by Baddeley and Hitch (1974), comprises a central "
|
||||
+ "executive system supported by the phonological loop and visuospatial sketchpad. Sleep "
|
||||
+ "deprivation has been hypothesized to primarily affect the central executive component, "
|
||||
+ "which governs attentional control and task coordination.",
|
||||
"Normal");
|
||||
|
||||
// ── Level 3 heading ──
|
||||
AddSampleParagraph(body, "Neural Mechanisms of Sleep-Related Cognitive Decline", "Heading3");
|
||||
|
||||
AddSampleParagraph(body,
|
||||
"Neuroimaging studies have demonstrated that sleep deprivation is associated with "
|
||||
+ "reduced activation in the prefrontal cortex, the neural substrate most closely linked "
|
||||
+ "to working memory function (Chee & Chuah, 2007). Additionally, thalamic deactivation "
|
||||
+ "may impair the relay of sensory information necessary for memory encoding.",
|
||||
"Normal");
|
||||
|
||||
// ── Level 4 heading (run-in, bold, ends with period) ──
|
||||
// APA Level 4 is a run-in heading: the heading text and paragraph text
|
||||
// share the same line. We approximate with a bold indented paragraph.
|
||||
body.Append(CreateAPA7RunInParagraph(
|
||||
headingText: "Prefrontal Cortex Involvement.",
|
||||
bodyText: " The dorsolateral prefrontal cortex (DLPFC) shows the greatest "
|
||||
+ "susceptibility to sleep loss. Functional MRI studies reveal a dose-dependent "
|
||||
+ "relationship between hours of wakefulness and DLPFC activation levels during "
|
||||
+ "working memory tasks.",
|
||||
bold: true,
|
||||
italic: false
|
||||
));
|
||||
|
||||
// ── Level 5 heading (run-in, bold italic, ends with period) ──
|
||||
body.Append(CreateAPA7RunInParagraph(
|
||||
headingText: "Glutamatergic Pathways.",
|
||||
bodyText: " Recent research has identified glutamatergic signaling in the "
|
||||
+ "prefrontal cortex as a key mediator of sleep deprivation effects on working "
|
||||
+ "memory. Antagonism of NMDA receptors produces cognitive deficits similar to "
|
||||
+ "those observed following 24 hours of sleep loss.",
|
||||
bold: true,
|
||||
italic: true
|
||||
));
|
||||
|
||||
// ── Level 2: Method section ──
|
||||
AddSampleParagraph(body, "Method", "Heading2");
|
||||
|
||||
AddSampleParagraph(body,
|
||||
"This experiment used a between-subjects design with sleep condition (deprived vs. "
|
||||
+ "control) as the independent variable and working memory accuracy as the dependent "
|
||||
+ "variable. All procedures were approved by the University of Washington Institutional "
|
||||
+ "Review Board (Protocol #2024-0847).",
|
||||
"Normal");
|
||||
|
||||
// ── Level 2: Results ──
|
||||
AddSampleParagraph(body, "Results", "Heading2");
|
||||
|
||||
AddSampleParagraph(body,
|
||||
"An independent-samples t test revealed a statistically significant difference in "
|
||||
+ "working memory accuracy between the sleep-deprived group (M = 72.3%, SD = 8.1) "
|
||||
+ "and the control group (M = 89.7%, SD = 5.4), t(46) = 9.12, p < .001. The effect "
|
||||
+ "size was large (Cohen's d = 2.52), indicating a substantial practical difference.",
|
||||
"Normal");
|
||||
|
||||
// ── Level 2: Discussion ──
|
||||
AddSampleParagraph(body, "Discussion", "Heading2");
|
||||
|
||||
AddSampleParagraph(body,
|
||||
"The findings of this study are consistent with previous research demonstrating the "
|
||||
+ "deleterious effects of sleep deprivation on cognitive performance. The magnitude of "
|
||||
+ "the effect observed here exceeds that reported in meta-analytic reviews, possibly "
|
||||
+ "due to the use of a more demanding dual n-back paradigm that places greater demands "
|
||||
+ "on executive control processes.",
|
||||
"Normal");
|
||||
|
||||
// Section properties must be last child of body
|
||||
body.Append(sectPr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an APA 7 "run-in" heading style (Levels 4 and 5).
|
||||
/// These headings are indented 0.5in and end with a period;
|
||||
/// the paragraph text runs in on the same line as the heading.
|
||||
/// In OpenXML, we create a paragraph style with the appropriate formatting.
|
||||
/// </summary>
|
||||
private static Style CreateAPA7RunInHeadingStyle(int level, bool bold, bool italic)
|
||||
{
|
||||
var rPr = new StyleRunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman",
|
||||
EastAsia = "SimSun",
|
||||
ComplexScript = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "24" }, // 12pt — same as body
|
||||
new FontSizeComplexScript { Val = "24" },
|
||||
new Color { Val = "000000" }
|
||||
);
|
||||
|
||||
if (bold)
|
||||
rPr.Append(new Bold());
|
||||
if (italic)
|
||||
rPr.Append(new Italic());
|
||||
|
||||
var pPr = new StyleParagraphProperties(
|
||||
new KeepNext(),
|
||||
new KeepLines(),
|
||||
new SpacingBetweenLines
|
||||
{
|
||||
Before = "480",
|
||||
After = "0",
|
||||
Line = "480",
|
||||
LineRule = LineSpacingRuleValues.Auto
|
||||
},
|
||||
// Indented 0.5in = 720 DXA (APA 7 Levels 4-5)
|
||||
new Indentation { FirstLine = "720" },
|
||||
new OutlineLevel { Val = level - 1 }
|
||||
);
|
||||
|
||||
return new Style(
|
||||
new StyleName { Val = $"heading {level}" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new NextParagraphStyle { Val = "Normal" },
|
||||
new UIPriority { Val = 9 },
|
||||
new PrimaryStyle(),
|
||||
pPr,
|
||||
rPr
|
||||
)
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = $"Heading{level}",
|
||||
Default = false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a centered, optionally bold paragraph style with no first-line indent.
|
||||
/// Used for APA title page elements and the "Abstract" label.
|
||||
/// </summary>
|
||||
private static Style CreateAPA7NoIndentCenteredStyle(string styleId, string styleName, bool bold)
|
||||
{
|
||||
var rPr = new StyleRunProperties(
|
||||
new FontSize { Val = "24" },
|
||||
new FontSizeComplexScript { Val = "24" }
|
||||
);
|
||||
|
||||
if (bold)
|
||||
rPr.Append(new Bold());
|
||||
|
||||
return new Style(
|
||||
new StyleName { Val = styleName },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new UIPriority { Val = 1 },
|
||||
new StyleParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center },
|
||||
new Indentation { FirstLine = "0" },
|
||||
new SpacingBetweenLines
|
||||
{
|
||||
Line = "480",
|
||||
LineRule = LineSpacingRuleValues.Auto,
|
||||
After = "0"
|
||||
}
|
||||
),
|
||||
rPr
|
||||
)
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = styleId,
|
||||
Default = false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a left-aligned paragraph style with no first-line indent.
|
||||
/// Used for the abstract body text (APA 7 specifies no indent for abstract).
|
||||
/// </summary>
|
||||
private static Style CreateAPA7NoIndentStyle(string styleId, string styleName)
|
||||
{
|
||||
return new Style(
|
||||
new StyleName { Val = styleName },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new UIPriority { Val = 1 },
|
||||
new StyleParagraphProperties(
|
||||
new Indentation { FirstLine = "0" },
|
||||
new SpacingBetweenLines
|
||||
{
|
||||
Line = "480",
|
||||
LineRule = LineSpacingRuleValues.Auto,
|
||||
After = "0"
|
||||
}
|
||||
)
|
||||
)
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = styleId,
|
||||
Default = false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the APA 7 professional paper header: running head flush left (ALL CAPS)
|
||||
/// and page number flush right, both in the same header line.
|
||||
/// Per APA 7, Section 2.18: the running head appears on every page.
|
||||
/// </summary>
|
||||
private static void AddAPA7Header(MainDocumentPart mainPart, SectionProperties sectPr, string runningHeadText)
|
||||
{
|
||||
// Use a tab stop at the right margin to position the page number flush right
|
||||
// Right margin position: page width (12240) - left margin (1440) - right margin (1440) = 9360 DXA
|
||||
var headerParagraph = new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Normal" },
|
||||
new Indentation { FirstLine = "0" },
|
||||
new SpacingBetweenLines { Line = "240", LineRule = LineSpacingRuleValues.Auto, After = "0" },
|
||||
new Tabs(
|
||||
new TabStop
|
||||
{
|
||||
Val = TabStopValues.Right,
|
||||
Position = 9360 // Flush right at the text area edge
|
||||
}
|
||||
)
|
||||
),
|
||||
// Running head text (flush left, ALL CAPS)
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "24" },
|
||||
new FontSizeComplexScript { Val = "24" }
|
||||
),
|
||||
new Text(runningHeadText) { Space = SpaceProcessingModeValues.Preserve }
|
||||
),
|
||||
// Tab to move to right-aligned position
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "24" },
|
||||
new FontSizeComplexScript { Val = "24" }
|
||||
),
|
||||
new TabChar()
|
||||
),
|
||||
// Page number (flush right)
|
||||
new SimpleField(
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "24" },
|
||||
new FontSizeComplexScript { Val = "24" }
|
||||
),
|
||||
new Text("1")
|
||||
)
|
||||
)
|
||||
{ Instruction = " PAGE " }
|
||||
);
|
||||
|
||||
var headerPart = mainPart.AddNewPart<HeaderPart>();
|
||||
headerPart.Header = new Header(headerParagraph);
|
||||
headerPart.Header.Save();
|
||||
|
||||
string headerPartId = mainPart.GetIdOfPart(headerPart);
|
||||
sectPr.Append(new HeaderReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
Id = headerPartId
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the APA 7 title page content: title, author, affiliation,
|
||||
/// course, instructor, and date — all centered and double-spaced.
|
||||
/// Per APA 7, Section 2.3: title should be bold, centered, in upper half of page.
|
||||
/// </summary>
|
||||
private static void AddAPA7TitlePage(Body body,
|
||||
string title, string authorName, string affiliation,
|
||||
string courseLine, string instructorLine, string dateLine)
|
||||
{
|
||||
// Add some blank lines to position title in upper half of page
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "APATitlePageInfo" }
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
// Title: centered, bold
|
||||
AddSampleParagraph(body, title, "APATitlePageTitle");
|
||||
|
||||
// Author name
|
||||
AddSampleParagraph(body, authorName, "APATitlePageInfo");
|
||||
|
||||
// Affiliation
|
||||
AddSampleParagraph(body, affiliation, "APATitlePageInfo");
|
||||
|
||||
// Course
|
||||
AddSampleParagraph(body, courseLine, "APATitlePageInfo");
|
||||
|
||||
// Instructor
|
||||
AddSampleParagraph(body, instructorLine, "APATitlePageInfo");
|
||||
|
||||
// Date
|
||||
AddSampleParagraph(body, dateLine, "APATitlePageInfo");
|
||||
|
||||
// Page break after title page
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "APATitlePageInfo" }
|
||||
),
|
||||
new Run(new Break { Type = BreakValues.Page })
|
||||
));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an APA Level 4 or 5 "run-in" paragraph where the heading text
|
||||
/// (bold or bold italic) is followed by the body text on the same line.
|
||||
/// The heading ends with a period per APA 7 convention.
|
||||
/// </summary>
|
||||
private static Paragraph CreateAPA7RunInParagraph(
|
||||
string headingText, string bodyText, bool bold, bool italic)
|
||||
{
|
||||
var headingRunProps = new RunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman",
|
||||
ComplexScript = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "24" },
|
||||
new FontSizeComplexScript { Val = "24" }
|
||||
);
|
||||
|
||||
if (bold)
|
||||
headingRunProps.Append(new Bold());
|
||||
if (italic)
|
||||
headingRunProps.Append(new Italic());
|
||||
|
||||
return new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Indentation { FirstLine = "720" }, // 0.5in indent
|
||||
new SpacingBetweenLines
|
||||
{
|
||||
Line = "480",
|
||||
LineRule = LineSpacingRuleValues.Auto,
|
||||
After = "0"
|
||||
}
|
||||
),
|
||||
// Heading run (bold / bold italic)
|
||||
new Run(
|
||||
headingRunProps,
|
||||
new Text(headingText) { Space = SpaceProcessingModeValues.Preserve }
|
||||
),
|
||||
// Body text run (regular)
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman",
|
||||
ComplexScript = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "24" },
|
||||
new FontSizeComplexScript { Val = "24" }
|
||||
),
|
||||
new Text(bodyText) { Space = SpaceProcessingModeValues.Preserve }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// RECIPE 9: MLA 9TH EDITION
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// Recipe: MLA 9th Edition
|
||||
/// Source: MLA Handbook, 9th edition (2021), Part 1 (Principles of Scholarship)
|
||||
/// and Part 2 (Details of MLA Style).
|
||||
///
|
||||
/// Key MLA 9 specifications:
|
||||
/// - Font: 12pt Times New Roman (or other readable font; Times New Roman is standard).
|
||||
/// - Margins: 1 inch on all sides.
|
||||
/// - Line spacing: Double-spaced throughout, including block quotes and Works Cited.
|
||||
/// - Paragraph indent: 0.5 inch first-line indent for body paragraphs.
|
||||
/// - Title: Centered, same size as body text (12pt), NOT bold, italic, or underlined.
|
||||
/// MLA eschews visual hierarchy — the title is distinguished only by centering.
|
||||
/// - No mandatory heading system. If headings are used, they should be simple and
|
||||
/// consistent. MLA does not prescribe heading levels like APA does.
|
||||
/// - Running header: Author's last name and page number, flush right, 0.5 inch from top.
|
||||
/// - First-page header block: Student's name, instructor's name, course title, and
|
||||
/// date — upper left, double-spaced, NO extra spacing.
|
||||
/// - Works Cited: title "Works Cited" centered (not bold), entries have hanging indent
|
||||
/// of 0.5 inch (first line flush left, subsequent lines indented).
|
||||
/// - No title page required (unless specifically requested by instructor).
|
||||
///
|
||||
/// Design rationale:
|
||||
/// - MLA's aesthetic is deliberately plain — the writing is the content.
|
||||
/// - No bold headings, no size variation, no decorative elements.
|
||||
/// - The only structural markers are centering (title, Works Cited label)
|
||||
/// and indentation (paragraphs, hanging indent for citations).
|
||||
/// - This uniformity reflects MLA's roots in literary studies, where the
|
||||
/// text itself is paramount and formatting should be invisible.
|
||||
/// </summary>
|
||||
public static void CreateMLA9Document(string outputPath)
|
||||
{
|
||||
using var doc = WordprocessingDocument.Create(outputPath, WordprocessingDocumentType.Document);
|
||||
|
||||
var mainPart = doc.AddMainDocumentPart();
|
||||
mainPart.Document = new Document(new Body());
|
||||
var body = mainPart.Document.Body!;
|
||||
|
||||
// ── Styles ──
|
||||
var stylesPart = mainPart.AddNewPart<StyleDefinitionsPart>();
|
||||
stylesPart.Styles = new Styles();
|
||||
var styles = stylesPart.Styles;
|
||||
|
||||
// DocDefaults: 12pt Times New Roman, double spacing, 0.5in first-line indent
|
||||
styles.Append(new DocDefaults(
|
||||
new RunPropertiesDefault(
|
||||
new RunPropertiesBaseStyle(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman",
|
||||
EastAsia = "SimSun",
|
||||
ComplexScript = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "24" }, // 12pt
|
||||
new FontSizeComplexScript { Val = "24" },
|
||||
new Color { Val = "000000" },
|
||||
new Languages { Val = "en-US", EastAsia = "zh-CN" }
|
||||
)
|
||||
),
|
||||
new ParagraphPropertiesDefault(
|
||||
new ParagraphPropertiesBaseStyle(
|
||||
new SpacingBetweenLines
|
||||
{
|
||||
Line = "480", // Double spacing throughout
|
||||
LineRule = LineSpacingRuleValues.Auto,
|
||||
After = "0"
|
||||
},
|
||||
new Indentation { FirstLine = "720" } // 0.5in first-line indent
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
// ── Normal style ──
|
||||
styles.Append(CreateParagraphStyle(
|
||||
styleId: "Normal",
|
||||
styleName: "Normal",
|
||||
isDefault: true,
|
||||
uiPriority: 0
|
||||
));
|
||||
|
||||
// ── MLA Title style: centered, NOT bold/italic/underlined ──
|
||||
// MLA is distinctive: the title has NO special formatting beyond centering.
|
||||
styles.Append(CreateMLA9TitleStyle());
|
||||
|
||||
// ── MLA Header Block style: flush left, no indent ──
|
||||
styles.Append(CreateMLA9HeaderBlockStyle());
|
||||
|
||||
// ── MLA Works Cited label style: centered, not bold ──
|
||||
styles.Append(CreateMLA9WorksCitedLabelStyle());
|
||||
|
||||
// ── MLA Works Cited entry style: hanging indent 0.5in ──
|
||||
styles.Append(CreateMLA9WorksCitedEntryStyle());
|
||||
|
||||
// ── Page setup: US Letter, 1in all sides ──
|
||||
var sectPr = new SectionProperties(
|
||||
new WpPageSize { Width = 12240U, Height = 15840U },
|
||||
new PageMargin
|
||||
{
|
||||
Top = 1440, Bottom = 1440,
|
||||
Left = 1440U, Right = 1440U,
|
||||
Header = 720U, Footer = 720U, Gutter = 0U
|
||||
}
|
||||
);
|
||||
|
||||
// ── Running header: "LastName PageNumber" flush right ──
|
||||
AddMLA9Header(mainPart, sectPr, "Mitchell");
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// SAMPLE CONTENT: MLA header block, title, body, Works Cited
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
// ── First-page header block (upper left, double-spaced) ──
|
||||
AddSampleParagraph(body, "Sarah Mitchell", "MLAHeaderBlock");
|
||||
AddSampleParagraph(body, "Professor Johnson", "MLAHeaderBlock");
|
||||
AddSampleParagraph(body, "English 201: American Literature", "MLAHeaderBlock");
|
||||
AddSampleParagraph(body, "15 October 2024", "MLAHeaderBlock");
|
||||
|
||||
// ── Title: centered, 12pt, plain (not bold) ──
|
||||
AddSampleParagraph(body, "The Function of the Unreliable Narrator in Nabokov's Lolita", "MLATitle");
|
||||
|
||||
// ── Body paragraphs ──
|
||||
AddSampleParagraph(body,
|
||||
"Vladimir Nabokov's Lolita (1955) remains one of the most studied examples of "
|
||||
+ "unreliable narration in twentieth-century fiction. Humbert Humbert's elaborate, "
|
||||
+ "self-justifying prose has been analyzed through numerous critical lenses, yet the "
|
||||
+ "question of how the novel's narrative structure shapes reader complicity continues "
|
||||
+ "to generate scholarly debate.",
|
||||
"Normal");
|
||||
|
||||
AddSampleParagraph(body,
|
||||
"The concept of the unreliable narrator, first articulated by Wayne C. Booth in "
|
||||
+ "The Rhetoric of Fiction (1961), provides a foundational framework for understanding "
|
||||
+ "Humbert's discourse. Booth argues that unreliable narrators are those whose values "
|
||||
+ "diverge from those of the implied author (158-59). In Lolita, this divergence is "
|
||||
+ "particularly complex because Nabokov layers multiple forms of unreliability: "
|
||||
+ "factual, evaluative, and interpretive.",
|
||||
"Normal");
|
||||
|
||||
AddSampleParagraph(body,
|
||||
"Michael Wood has observed that \"Nabokov's genius lies in making us forget, "
|
||||
+ "momentarily, that Humbert is a monster\" (127). This temporary forgetting is not "
|
||||
+ "a failure of reading but a designed effect of the narrative voice. The luxurious "
|
||||
+ "prose, the literary allusions, the self-deprecating wit \u2014 all serve to create what "
|
||||
+ "Nomi Tamir-Ghez calls \"rhetorical seduction\" (42), in which readers find "
|
||||
+ "themselves sympathizing with a narrator whose actions they would condemn.",
|
||||
"Normal");
|
||||
|
||||
AddSampleParagraph(body,
|
||||
"The structural implications of Humbert's unreliability extend beyond mere "
|
||||
+ "factual distortion. As Eric Naiman demonstrates, the novel's famous opening "
|
||||
+ "paragraph \u2014 with its incantatory repetition of \"Lolita\" \u2014 establishes a "
|
||||
+ "pattern of linguistic possession that mirrors Humbert's physical possession of "
|
||||
+ "Dolores Haze (85). The language itself becomes an instrument of control, one "
|
||||
+ "that operates on the reader as well as on the characters within the narrative.",
|
||||
"Normal");
|
||||
|
||||
// ── Works Cited ──
|
||||
// Page break before Works Cited
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "MLAHeaderBlock" }
|
||||
),
|
||||
new Run(new Break { Type = BreakValues.Page })
|
||||
));
|
||||
|
||||
AddSampleParagraph(body, "Works Cited", "MLAWorksCitedLabel");
|
||||
|
||||
// Works Cited entries with hanging indent
|
||||
AddSampleParagraph(body,
|
||||
"Booth, Wayne C. The Rhetoric of Fiction. 2nd ed., U of Chicago P, 1983.",
|
||||
"MLAWorksCitedEntry");
|
||||
|
||||
AddSampleParagraph(body,
|
||||
"Nabokov, Vladimir. Lolita. 1955. Vintage International, 1989.",
|
||||
"MLAWorksCitedEntry");
|
||||
|
||||
AddSampleParagraph(body,
|
||||
"Naiman, Eric. Nabokov, Perversely. Cornell UP, 2010.",
|
||||
"MLAWorksCitedEntry");
|
||||
|
||||
AddSampleParagraph(body,
|
||||
"Tamir-Ghez, Nomi. \"The Art of Persuasion in Nabokov's Lolita.\" Poetics Today, "
|
||||
+ "vol. 1, no. 1-2, 1979, pp. 65-83.",
|
||||
"MLAWorksCitedEntry");
|
||||
|
||||
AddSampleParagraph(body,
|
||||
"Wood, Michael. The Magician's Doubts: Nabokov and the Risks of Fiction. "
|
||||
+ "Princeton UP, 1995.",
|
||||
"MLAWorksCitedEntry");
|
||||
|
||||
// Section properties must be last child of body
|
||||
body.Append(sectPr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MLA title style: centered, 12pt, NO bold/italic/underline.
|
||||
/// MLA's radical plainness — the title is distinguished only by position.
|
||||
/// </summary>
|
||||
private static Style CreateMLA9TitleStyle()
|
||||
{
|
||||
return new Style(
|
||||
new StyleName { Val = "MLA Title" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new UIPriority { Val = 1 },
|
||||
new StyleParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center },
|
||||
new Indentation { FirstLine = "0" },
|
||||
new SpacingBetweenLines
|
||||
{
|
||||
Line = "480",
|
||||
LineRule = LineSpacingRuleValues.Auto,
|
||||
After = "0"
|
||||
}
|
||||
)
|
||||
)
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = "MLATitle",
|
||||
Default = false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MLA first-page header block style: flush left, no first-line indent, double-spaced.
|
||||
/// Used for the student name, instructor, course, and date lines.
|
||||
/// </summary>
|
||||
private static Style CreateMLA9HeaderBlockStyle()
|
||||
{
|
||||
return new Style(
|
||||
new StyleName { Val = "MLA Header Block" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new UIPriority { Val = 1 },
|
||||
new StyleParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Left },
|
||||
new Indentation { FirstLine = "0" },
|
||||
new SpacingBetweenLines
|
||||
{
|
||||
Line = "480",
|
||||
LineRule = LineSpacingRuleValues.Auto,
|
||||
After = "0"
|
||||
}
|
||||
)
|
||||
)
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = "MLAHeaderBlock",
|
||||
Default = false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MLA Works Cited label style: centered, 12pt, NOT bold.
|
||||
/// Like the title, the label is plain — only centering distinguishes it.
|
||||
/// </summary>
|
||||
private static Style CreateMLA9WorksCitedLabelStyle()
|
||||
{
|
||||
return new Style(
|
||||
new StyleName { Val = "MLA Works Cited Label" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new UIPriority { Val = 1 },
|
||||
new StyleParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center },
|
||||
new Indentation { FirstLine = "0" },
|
||||
new SpacingBetweenLines
|
||||
{
|
||||
Line = "480",
|
||||
LineRule = LineSpacingRuleValues.Auto,
|
||||
After = "0"
|
||||
}
|
||||
)
|
||||
)
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = "MLAWorksCitedLabel",
|
||||
Default = false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MLA Works Cited entry style: hanging indent of 0.5 inch (720 DXA).
|
||||
/// First line is flush left; subsequent lines indent 0.5 inch.
|
||||
/// This is the standard format for bibliography entries in MLA style.
|
||||
/// </summary>
|
||||
private static Style CreateMLA9WorksCitedEntryStyle()
|
||||
{
|
||||
return new Style(
|
||||
new StyleName { Val = "MLA Works Cited Entry" },
|
||||
new BasedOn { Val = "Normal" },
|
||||
new UIPriority { Val = 1 },
|
||||
new StyleParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Left },
|
||||
// Hanging indent: Left = 720, FirstLine is negative (Hanging = 720)
|
||||
new Indentation { Left = "720", Hanging = "720" },
|
||||
new SpacingBetweenLines
|
||||
{
|
||||
Line = "480",
|
||||
LineRule = LineSpacingRuleValues.Auto,
|
||||
After = "0"
|
||||
}
|
||||
)
|
||||
)
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = "MLAWorksCitedEntry",
|
||||
Default = false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the MLA 9 running header: author last name and page number, flush right,
|
||||
/// 0.5 inch from top of page. Per MLA convention, this appears on every page.
|
||||
/// </summary>
|
||||
private static void AddMLA9Header(MainDocumentPart mainPart, SectionProperties sectPr, string authorLastName)
|
||||
{
|
||||
var headerParagraph = new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Right },
|
||||
new Indentation { FirstLine = "0" },
|
||||
new SpacingBetweenLines { Line = "240", LineRule = LineSpacingRuleValues.Auto, After = "0" }
|
||||
),
|
||||
// Author last name
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "24" },
|
||||
new FontSizeComplexScript { Val = "24" }
|
||||
),
|
||||
new Text(authorLastName + " ") { Space = SpaceProcessingModeValues.Preserve }
|
||||
),
|
||||
// Page number
|
||||
new SimpleField(
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "Times New Roman",
|
||||
HighAnsi = "Times New Roman"
|
||||
},
|
||||
new FontSize { Val = "24" },
|
||||
new FontSizeComplexScript { Val = "24" }
|
||||
),
|
||||
new Text("1")
|
||||
)
|
||||
)
|
||||
{ Instruction = " PAGE " }
|
||||
);
|
||||
|
||||
var headerPart = mainPart.AddNewPart<HeaderPart>();
|
||||
headerPart.Header = new Header(headerParagraph);
|
||||
headerPart.Header.Save();
|
||||
|
||||
string headerPartId = mainPart.GetIdOfPart(headerPart);
|
||||
sectPr.Append(new HeaderReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
Id = headerPartId
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,624 @@
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Samples;
|
||||
|
||||
/// <summary>
|
||||
/// Reference implementations for field codes and Table of Contents (TOC).
|
||||
///
|
||||
/// KEY CONCEPTS:
|
||||
/// - SimpleField: single-element shorthand, e.g. <w:fldSimple w:instr="PAGE"/>
|
||||
/// - Complex field: three FieldChar elements (Begin / Separate / End) with FieldCode between them.
|
||||
/// Word always writes complex fields; SimpleField is only used for trivial cases.
|
||||
/// - TOC is a structured document tag (SdtBlock) wrapping a complex field.
|
||||
/// - UpdateFieldsOnOpen tells Word to recalculate all fields when opening.
|
||||
/// </summary>
|
||||
public static class FieldAndTocSamples
|
||||
{
|
||||
// ──────────────────────────────────────────────
|
||||
// 1. InsertToc — TOC levels 1-3 inside SdtBlock
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a Table of Contents covering heading levels 1-3.
|
||||
/// Uses an SdtBlock wrapper with a complex field code:
|
||||
/// TOC \o "1-3" \h \z \u
|
||||
///
|
||||
/// Switches:
|
||||
/// \o "1-3" — outline levels 1-3
|
||||
/// \h — hyperlinks
|
||||
/// \z — hide tab leaders / page numbers in Web Layout
|
||||
/// \u — use applied paragraph outline level
|
||||
/// </summary>
|
||||
public static SdtBlock InsertToc(Body body)
|
||||
{
|
||||
var sdtBlock = new SdtBlock();
|
||||
|
||||
// SdtProperties — mark as TOC
|
||||
var sdtPr = new SdtProperties();
|
||||
sdtPr.Append(new SdtContentDocPartObject(
|
||||
new DocPartGallery { Val = "Table of Contents" },
|
||||
new DocPartUnique()));
|
||||
sdtBlock.Append(sdtPr);
|
||||
|
||||
// SdtContent — contains the field code paragraph(s)
|
||||
var sdtContent = new SdtContentBlock();
|
||||
|
||||
// TOC title paragraph
|
||||
var titlePara = new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "TOCHeading" }),
|
||||
new Run(new Text("Table of Contents")));
|
||||
sdtContent.Append(titlePara);
|
||||
|
||||
// Complex field paragraph for TOC
|
||||
var fieldPara = new Paragraph();
|
||||
InsertComplexFieldInline(fieldPara, " TOC \\o \"1-3\" \\h \\z \\u ");
|
||||
sdtContent.Append(fieldPara);
|
||||
|
||||
sdtBlock.Append(sdtContent);
|
||||
body.Append(sdtBlock);
|
||||
return sdtBlock;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 2. InsertTocWithCustomLevels — TOC 1-4 levels
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a TOC covering heading levels 1-4.
|
||||
/// Identical structure to <see cref="InsertToc"/> but with "\o 1-4".
|
||||
/// </summary>
|
||||
public static SdtBlock InsertTocWithCustomLevels(Body body)
|
||||
{
|
||||
var sdtBlock = new SdtBlock();
|
||||
|
||||
var sdtPr = new SdtProperties();
|
||||
sdtPr.Append(new SdtContentDocPartObject(
|
||||
new DocPartGallery { Val = "Table of Contents" },
|
||||
new DocPartUnique()));
|
||||
sdtBlock.Append(sdtPr);
|
||||
|
||||
var sdtContent = new SdtContentBlock();
|
||||
|
||||
var titlePara = new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "TOCHeading" }),
|
||||
new Run(new Text("Table of Contents")));
|
||||
sdtContent.Append(titlePara);
|
||||
|
||||
// 1-4 levels instead of 1-3
|
||||
var fieldPara = new Paragraph();
|
||||
InsertComplexFieldInline(fieldPara, " TOC \\o \"1-4\" \\h \\z \\u ");
|
||||
sdtContent.Append(fieldPara);
|
||||
|
||||
sdtBlock.Append(sdtContent);
|
||||
body.Append(sdtBlock);
|
||||
return sdtBlock;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 3. InsertSimpleField — PAGE, NUMPAGES, DATE, etc.
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a SimpleField element into a paragraph.
|
||||
///
|
||||
/// SimpleField is the compact form: <w:fldSimple w:instr=" PAGE "><w:r>...</w:r></w:fldSimple>
|
||||
///
|
||||
/// Common instructions: "PAGE", "NUMPAGES", "DATE", "TIME", "FILENAME".
|
||||
/// The run inside is the cached display value; Word recalculates on open.
|
||||
/// </summary>
|
||||
public static SimpleField InsertSimpleField(Paragraph para, string instruction)
|
||||
{
|
||||
var simpleField = new SimpleField { Instruction = $" {instruction} " };
|
||||
|
||||
// Cached display value — Word replaces this on recalculation
|
||||
simpleField.Append(new Run(
|
||||
new RunProperties(new NoProof()),
|
||||
new Text("«" + instruction + "»")));
|
||||
|
||||
para.Append(simpleField);
|
||||
return simpleField;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 4. InsertComplexField — Begin/Separate/End
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a complex field into a paragraph using the FieldChar Begin/Separate/End pattern.
|
||||
///
|
||||
/// Structure:
|
||||
/// Run1: FieldChar(Begin) + FieldCode(" PAGE ")
|
||||
/// Run2: FieldChar(Separate)
|
||||
/// Run3: Text("1") ← cached display value
|
||||
/// Run4: FieldChar(End)
|
||||
///
|
||||
/// Use complex fields when you need dirty flags, lock, or nested fields.
|
||||
/// </summary>
|
||||
public static void InsertComplexField(Paragraph para, string instruction)
|
||||
{
|
||||
InsertComplexFieldInline(para, $" {instruction} ");
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 5. InsertDateField — DATE with format switch
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a DATE field with a format switch: DATE \@ "yyyy-MM-dd"
|
||||
///
|
||||
/// The \@ switch specifies the date/time picture.
|
||||
/// Common formats:
|
||||
/// \@ "yyyy-MM-dd" → 2026-03-22
|
||||
/// \@ "MMMM d, yyyy" → March 22, 2026
|
||||
/// \@ "M/d/yyyy h:mm am/pm" → 3/22/2026 2:30 PM
|
||||
/// </summary>
|
||||
public static void InsertDateField(Paragraph para, string format)
|
||||
{
|
||||
// Field instruction with date-time picture switch
|
||||
string instruction = $" DATE \\@ \"{format}\" ";
|
||||
InsertComplexFieldInline(para, instruction);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 6. InsertCrossReference — REF field
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a REF cross-reference field that refers to a bookmark.
|
||||
///
|
||||
/// Instruction: REF bookmarkName \h
|
||||
/// \h — creates a hyperlink to the bookmark
|
||||
/// \p — inserts "above" or "below" relative position
|
||||
/// \n — inserts paragraph number of the bookmark
|
||||
/// </summary>
|
||||
public static void InsertCrossReference(Paragraph para, string bookmarkName)
|
||||
{
|
||||
string instruction = $" REF {bookmarkName} \\h ";
|
||||
InsertComplexFieldInline(para, instruction);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 7. InsertSequenceField — SEQ for numbering
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a SEQ (sequence) field for auto-numbering figures, tables, etc.
|
||||
///
|
||||
/// Usage pattern for "Figure 1":
|
||||
/// 1. Append a run with text "Figure " to the paragraph
|
||||
/// 2. Call InsertSequenceField(para, "Figure")
|
||||
///
|
||||
/// Usage pattern for "Table 1":
|
||||
/// 1. Append a run with text "Table " to the paragraph
|
||||
/// 2. Call InsertSequenceField(para, "Table")
|
||||
///
|
||||
/// Each unique seqName maintains its own counter across the document.
|
||||
/// </summary>
|
||||
public static void InsertSequenceField(Paragraph para, string seqName)
|
||||
{
|
||||
string instruction = $" SEQ {seqName} \\* ARABIC ";
|
||||
InsertComplexFieldInline(para, instruction);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 8. InsertMergeField — MERGEFIELD for mail merge
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a MERGEFIELD for mail merge scenarios.
|
||||
///
|
||||
/// Instruction: MERGEFIELD fieldName \* MERGEFORMAT
|
||||
/// \* MERGEFORMAT — preserves formatting applied to the field result
|
||||
/// \b "text" — text before if field is non-empty
|
||||
/// \f "text" — text after if field is non-empty
|
||||
///
|
||||
/// The cached display shows «fieldName» as a placeholder.
|
||||
/// </summary>
|
||||
public static void InsertMergeField(Paragraph para, string fieldName)
|
||||
{
|
||||
string instruction = $" MERGEFIELD {fieldName} \\* MERGEFORMAT ";
|
||||
|
||||
// Begin
|
||||
para.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.Begin }));
|
||||
|
||||
// Field code
|
||||
para.Append(new Run(
|
||||
new FieldCode(instruction) { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
// Separate
|
||||
para.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.Separate }));
|
||||
|
||||
// Cached value — shows merge field placeholder
|
||||
para.Append(new Run(
|
||||
new RunProperties(new NoProof()),
|
||||
new Text($"\u00AB{fieldName}\u00BB") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
// End
|
||||
para.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.End }));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 9. InsertConditionalField — IF field
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts an IF conditional field.
|
||||
///
|
||||
/// Syntax: IF expression1 operator expression2 "true-text" "false-text"
|
||||
/// Example: IF { MERGEFIELD Gender } = "Male" "Mr." "Ms."
|
||||
///
|
||||
/// This example checks if MERGEFIELD Amount > 1000 and displays different text.
|
||||
/// Nested fields (MERGEFIELD inside IF) require nested Begin/End pairs.
|
||||
/// </summary>
|
||||
public static void InsertConditionalField(Paragraph para)
|
||||
{
|
||||
// Outer IF field Begin
|
||||
para.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.Begin }));
|
||||
|
||||
para.Append(new Run(
|
||||
new FieldCode(" IF ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
// Nested MERGEFIELD inside the IF condition
|
||||
para.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.Begin }));
|
||||
para.Append(new Run(
|
||||
new FieldCode(" MERGEFIELD Amount ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
para.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.Separate }));
|
||||
para.Append(new Run(
|
||||
new Text("0") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
para.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.End }));
|
||||
|
||||
// Continuation of IF instruction
|
||||
para.Append(new Run(
|
||||
new FieldCode(" > \"1000\" \"High Value\" \"Standard\" ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
// Separate — cached result
|
||||
para.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.Separate }));
|
||||
para.Append(new Run(
|
||||
new RunProperties(new NoProof()),
|
||||
new Text("Standard") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
// End
|
||||
para.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.End }));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 10. InsertStyleRef — STYLEREF for running headers
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a STYLEREF field, commonly used in headers/footers
|
||||
/// to display the current chapter or section title.
|
||||
///
|
||||
/// Instruction: STYLEREF "Heading 1"
|
||||
/// Displays the text of the nearest paragraph with style "Heading 1".
|
||||
/// \l — search from bottom of page up (for last instance on page)
|
||||
/// \n — insert the paragraph number, not text
|
||||
/// </summary>
|
||||
public static void InsertStyleRef(Paragraph para)
|
||||
{
|
||||
string instruction = " STYLEREF \"Heading 1\" ";
|
||||
InsertComplexFieldInline(para, instruction);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 11. EnableUpdateFieldsOnOpen
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Sets the UpdateFieldsOnOpen property so Word recalculates
|
||||
/// all fields (PAGE, TOC, SEQ, etc.) when the document is opened.
|
||||
///
|
||||
/// Without this, TOC and cross-references show stale cached values
|
||||
/// until the user manually presses Ctrl+A, F9 to update.
|
||||
/// </summary>
|
||||
public static void EnableUpdateFieldsOnOpen(DocumentSettingsPart settingsPart)
|
||||
{
|
||||
settingsPart.Settings ??= new Settings();
|
||||
var existing = settingsPart.Settings.GetFirstChild<UpdateFieldsOnOpen>();
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Val = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
settingsPart.Settings.Append(new UpdateFieldsOnOpen { Val = true });
|
||||
}
|
||||
settingsPart.Settings.Save();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 12. CreateTocStyles — TOC1/2/3 with tab leaders
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Creates TOC1, TOC2, TOC3 paragraph styles with right-aligned tab stops
|
||||
/// and dot leaders (the "....." between entry text and page number).
|
||||
///
|
||||
/// Each TOC level is indented further:
|
||||
/// TOC1 — 0 indent
|
||||
/// TOC2 — 240 twips (1/6 inch)
|
||||
/// TOC3 — 480 twips (1/3 inch)
|
||||
///
|
||||
/// Tab leader: dot-filled right tab at 9360 twips (6.5 inches for letter paper).
|
||||
/// </summary>
|
||||
public static void CreateTocStyles(StyleDefinitionsPart stylesPart)
|
||||
{
|
||||
stylesPart.Styles ??= new Styles();
|
||||
|
||||
string[] tocStyleIds = ["TOC1", "TOC2", "TOC3"];
|
||||
string[] tocStyleNames = ["toc 1", "toc 2", "toc 3"];
|
||||
int[] indents = [0, 240, 480]; // twips
|
||||
|
||||
// Right tab position: 6.5 inches = 9360 twips (standard for US Letter)
|
||||
const int tabPosition = 9360;
|
||||
|
||||
for (int i = 0; i < tocStyleIds.Length; i++)
|
||||
{
|
||||
var style = new Style
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = tocStyleIds[i],
|
||||
CustomStyle = false
|
||||
};
|
||||
|
||||
style.Append(new StyleName { Val = tocStyleNames[i] });
|
||||
style.Append(new BasedOn { Val = "Normal" });
|
||||
style.Append(new NextParagraphStyle { Val = "Normal" });
|
||||
style.Append(new UIPriority { Val = 39 });
|
||||
|
||||
var pPr = new StyleParagraphProperties();
|
||||
|
||||
// Indentation for nested levels
|
||||
if (indents[i] > 0)
|
||||
{
|
||||
pPr.Append(new Indentation { Left = indents[i].ToString() });
|
||||
}
|
||||
|
||||
// Spacing: no space after for compact TOC
|
||||
pPr.Append(new SpacingBetweenLines { After = "0", Line = "276", LineRule = LineSpacingRuleValues.Auto });
|
||||
|
||||
// Right-aligned tab with dot leader
|
||||
var tabs = new Tabs();
|
||||
tabs.Append(new TabStop
|
||||
{
|
||||
Val = TabStopValues.Right,
|
||||
Leader = TabStopLeaderCharValues.Dot,
|
||||
Position = tabPosition
|
||||
});
|
||||
pPr.Append(tabs);
|
||||
|
||||
style.Append(pPr);
|
||||
stylesPart.Styles.Append(style);
|
||||
}
|
||||
|
||||
stylesPart.Styles.Save();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 13. CreateMixedTocStructure — Real-world TOC
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Real-world TOC structure: Mixed SDT block + static entries + field code.
|
||||
///
|
||||
/// IMPORTANT: Most templates do NOT have a clean TOC field code alone.
|
||||
/// Instead, they contain:
|
||||
/// 1. An SDT (Structured Document Tag) wrapper with alias "TOC"
|
||||
/// 2. Inside the SDT: a field code BEGIN + SEPARATE + static example entries + END
|
||||
/// 3. The static entries are placeholder text (e.g., "第1章 绪论...........1")
|
||||
/// that Word replaces when user presses "Update Fields"
|
||||
///
|
||||
/// When applying a template (Scenario C), you should:
|
||||
/// - KEEP the entire SDT block from the template (don't rebuild it)
|
||||
/// - DO NOT replace static entries with programmatic content
|
||||
/// - The entries will auto-update when the user opens in Word and updates fields
|
||||
/// - If you must update entries programmatically, replace the content INSIDE
|
||||
/// the SDT between fldChar separate and fldChar end
|
||||
///
|
||||
/// Common mistake: Treating TOC as pure field code and rebuilding it from scratch,
|
||||
/// which destroys the SDT wrapper and breaks Word's "Update Table" functionality.
|
||||
/// </summary>
|
||||
public static void CreateMixedTocStructure(string outputPath)
|
||||
{
|
||||
using var doc = WordprocessingDocument.Create(outputPath, WordprocessingDocumentType.Document);
|
||||
var mainPart = doc.AddMainDocumentPart();
|
||||
mainPart.Document = new Document();
|
||||
var body = new Body();
|
||||
mainPart.Document.Append(body);
|
||||
|
||||
// Add styles part with TOC styles
|
||||
var stylesPart = mainPart.AddNewPart<StyleDefinitionsPart>();
|
||||
CreateTocStyles(stylesPart);
|
||||
|
||||
// ─── SDT Block wrapping the entire TOC ───
|
||||
var sdtBlock = new SdtBlock();
|
||||
|
||||
// SDT Properties: alias "TOC", tag, and DocPartGallery
|
||||
var sdtPr = new SdtProperties();
|
||||
sdtPr.Append(new SdtAlias { Val = "TOC" });
|
||||
sdtPr.Append(new Tag { Val = "TOC" });
|
||||
sdtPr.Append(new SdtContentDocPartObject(
|
||||
new DocPartGallery { Val = "Table of Contents" },
|
||||
new DocPartUnique()));
|
||||
sdtBlock.Append(sdtPr);
|
||||
|
||||
// SDT Content: field code + static entries
|
||||
var sdtContent = new SdtContentBlock();
|
||||
|
||||
// ─── TOC title paragraph ───
|
||||
var titlePara = new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "TOCHeading" }),
|
||||
new Run(new Text("目 录")));
|
||||
sdtContent.Append(titlePara);
|
||||
|
||||
// ─── Field code BEGIN paragraph ───
|
||||
var fieldBeginPara = new Paragraph();
|
||||
|
||||
// fldChar Begin
|
||||
fieldBeginPara.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.Begin }));
|
||||
|
||||
// instrText: TOC \o "1-3" \h \z \u
|
||||
fieldBeginPara.Append(new Run(
|
||||
new FieldCode(" TOC \\o \"1-3\" \\h \\z \\u ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
// fldChar Separate
|
||||
fieldBeginPara.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.Separate }));
|
||||
|
||||
sdtContent.Append(fieldBeginPara);
|
||||
|
||||
// ─── Static placeholder entries (TOC1/TOC2/TOC3) ───
|
||||
// These are the example entries that Word will replace when user clicks "Update Table".
|
||||
// In real templates, these show example chapter titles with dot leaders and page numbers.
|
||||
|
||||
// TOC level 1 entry: "第1章 绪论...........1"
|
||||
sdtContent.Append(CreateStaticTocEntry("TOC1", "第1章 绪论", "1"));
|
||||
|
||||
// TOC level 2 entry: "1.1 研究背景...........1"
|
||||
sdtContent.Append(CreateStaticTocEntry("TOC2", "1.1 研究背景", "1"));
|
||||
|
||||
// TOC level 2 entry: "1.2 研究目的...........2"
|
||||
sdtContent.Append(CreateStaticTocEntry("TOC2", "1.2 研究目的", "2"));
|
||||
|
||||
// TOC level 1 entry: "第2章 文献综述...........3"
|
||||
sdtContent.Append(CreateStaticTocEntry("TOC1", "第2章 文献综述", "3"));
|
||||
|
||||
// TOC level 2 entry: "2.1 国内研究现状...........3"
|
||||
sdtContent.Append(CreateStaticTocEntry("TOC2", "2.1 国内研究现状", "3"));
|
||||
|
||||
// TOC level 3 entry: "2.1.1 早期研究...........4"
|
||||
sdtContent.Append(CreateStaticTocEntry("TOC3", "2.1.1 早期研究", "4"));
|
||||
|
||||
// TOC level 1 entry: "第3章 研究方法...........5"
|
||||
sdtContent.Append(CreateStaticTocEntry("TOC1", "第3章 研究方法", "5"));
|
||||
|
||||
// ─── Field code END paragraph ───
|
||||
var fieldEndPara = new Paragraph();
|
||||
fieldEndPara.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.End }));
|
||||
sdtContent.Append(fieldEndPara);
|
||||
|
||||
sdtBlock.Append(sdtContent);
|
||||
body.Append(sdtBlock);
|
||||
|
||||
// ─── Actual heading paragraphs (what the TOC references) ───
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "Heading1" }),
|
||||
new Run(new Text("第1章 绪论"))));
|
||||
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "Heading2" }),
|
||||
new Run(new Text("1.1 研究背景"))));
|
||||
|
||||
body.Append(new Paragraph(
|
||||
new Run(new Text("本研究旨在探讨……"))));
|
||||
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "Heading2" }),
|
||||
new Run(new Text("1.2 研究目的"))));
|
||||
|
||||
body.Append(new Paragraph(
|
||||
new Run(new Text("研究目的包括……"))));
|
||||
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "Heading1" }),
|
||||
new Run(new Text("第2章 文献综述"))));
|
||||
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "Heading2" }),
|
||||
new Run(new Text("2.1 国内研究现状"))));
|
||||
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "Heading3" }),
|
||||
new Run(new Text("2.1.1 早期研究"))));
|
||||
|
||||
body.Append(new Paragraph(
|
||||
new Run(new Text("早期研究表明……"))));
|
||||
|
||||
body.Append(new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "Heading1" }),
|
||||
new Run(new Text("第3章 研究方法"))));
|
||||
|
||||
body.Append(new Paragraph(
|
||||
new Run(new Text("本章介绍研究方法……"))));
|
||||
|
||||
// ─── Enable UpdateFieldsOnOpen so TOC auto-refreshes ───
|
||||
var settingsPart = mainPart.AddNewPart<DocumentSettingsPart>();
|
||||
EnableUpdateFieldsOnOpen(settingsPart);
|
||||
|
||||
mainPart.Document.Save();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper: creates a single static TOC entry paragraph with style, text, tab leader, and page number.
|
||||
/// This mirrors what Word generates inside a TOC SDT block.
|
||||
/// </summary>
|
||||
private static Paragraph CreateStaticTocEntry(string tocStyleId, string entryText, string pageNumber)
|
||||
{
|
||||
var para = new Paragraph();
|
||||
|
||||
// Paragraph properties: TOC style + right-aligned tab with dot leader
|
||||
var pPr = new ParagraphProperties();
|
||||
pPr.Append(new ParagraphStyleId { Val = tocStyleId });
|
||||
para.Append(pPr);
|
||||
|
||||
// Run with entry text
|
||||
para.Append(new Run(
|
||||
new RunProperties(new NoProof()),
|
||||
new Text(entryText) { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
// Tab character (creates the dot leader between text and page number)
|
||||
para.Append(new Run(new TabChar()));
|
||||
|
||||
// Page number
|
||||
para.Append(new Run(
|
||||
new RunProperties(new NoProof()),
|
||||
new Text(pageNumber)));
|
||||
|
||||
return para;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Private helper: insert complex field inline
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Shared helper that appends Begin / FieldCode / Separate / CachedValue / End
|
||||
/// runs to a paragraph.
|
||||
/// </summary>
|
||||
private static void InsertComplexFieldInline(Paragraph para, string instruction)
|
||||
{
|
||||
// Run 1: FieldChar Begin
|
||||
para.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.Begin }));
|
||||
|
||||
// Run 2: FieldCode (the instruction text)
|
||||
para.Append(new Run(
|
||||
new FieldCode(instruction) { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
// Run 3: FieldChar Separate
|
||||
para.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.Separate }));
|
||||
|
||||
// Run 4: Cached display value (placeholder until Word recalculates)
|
||||
para.Append(new Run(
|
||||
new RunProperties(new NoProof()),
|
||||
new Text("1") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
// Run 5: FieldChar End
|
||||
para.Append(new Run(
|
||||
new FieldChar { FieldCharType = FieldCharValues.End }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,675 @@
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
|
||||
// W15 types for people.xml (Office 2013+ comment author tracking)
|
||||
using W15Person = DocumentFormat.OpenXml.Office2013.Word.Person;
|
||||
using W15People = DocumentFormat.OpenXml.Office2013.Word.People;
|
||||
using W15PresenceInfo = DocumentFormat.OpenXml.Office2013.Word.PresenceInfo;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Samples;
|
||||
|
||||
/// <summary>
|
||||
/// Reference implementations for footnotes, endnotes, comments, bookmarks, and hyperlinks.
|
||||
///
|
||||
/// KEY CONCEPTS:
|
||||
/// - FootnotesPart must contain separator (id=-1) and continuationSeparator (id=0) footnotes.
|
||||
/// - Comments require up to 4 parts: comments.xml, commentsExtended.xml, commentsIds.xml, people.xml.
|
||||
/// - CommentRangeStart/CommentRangeEnd wrap the commented text; CommentReference goes in a run after CommentRangeEnd.
|
||||
/// - Bookmarks use BookmarkStart/BookmarkEnd pairs with matching Id attributes.
|
||||
/// - External hyperlinks require a HyperlinkRelationship in the part's relationships.
|
||||
/// </summary>
|
||||
public static class FootnoteAndCommentSamples
|
||||
{
|
||||
// ──────────────────────────────────────────────
|
||||
// 1. SetupFootnotesPart — required separator footnotes
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the FootnotesPart with the two REQUIRED special footnotes:
|
||||
/// - id=-1: separator (the short horizontal line between body text and footnotes)
|
||||
/// - id=0: continuationSeparator (line shown when a footnote spans pages)
|
||||
///
|
||||
/// Word will refuse to render footnotes correctly without these.
|
||||
/// Call this once before adding any footnotes.
|
||||
/// </summary>
|
||||
public static FootnotesPart SetupFootnotesPart(MainDocumentPart mainPart)
|
||||
{
|
||||
var footnotesPart = mainPart.FootnotesPart
|
||||
?? mainPart.AddNewPart<FootnotesPart>();
|
||||
|
||||
footnotesPart.Footnotes = new Footnotes();
|
||||
|
||||
// Separator footnote (id = -1): renders as a short horizontal rule
|
||||
var separator = new Footnote { Type = FootnoteEndnoteValues.Separator, Id = -1 };
|
||||
separator.Append(new Paragraph(
|
||||
new ParagraphProperties(new SpacingBetweenLines { After = "0", Line = "240", LineRule = LineSpacingRuleValues.Auto }),
|
||||
new Run(new SeparatorMark())));
|
||||
footnotesPart.Footnotes.Append(separator);
|
||||
|
||||
// Continuation separator footnote (id = 0): renders as a full-width rule
|
||||
var contSeparator = new Footnote { Type = FootnoteEndnoteValues.ContinuationSeparator, Id = 0 };
|
||||
contSeparator.Append(new Paragraph(
|
||||
new ParagraphProperties(new SpacingBetweenLines { After = "0", Line = "240", LineRule = LineSpacingRuleValues.Auto }),
|
||||
new Run(new ContinuationSeparatorMark())));
|
||||
footnotesPart.Footnotes.Append(contSeparator);
|
||||
|
||||
footnotesPart.Footnotes.Save();
|
||||
return footnotesPart;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 2. AddFootnote — reference in body + content in part
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Adds a footnote with two coordinated pieces:
|
||||
/// 1. A FootnoteReference in the body paragraph (superscript number in the text)
|
||||
/// 2. A Footnote element in the FootnotesPart (the actual footnote content)
|
||||
///
|
||||
/// The footnote id links the two together. IDs must be unique and > 0
|
||||
/// (ids -1 and 0 are reserved for separator and continuationSeparator).
|
||||
/// </summary>
|
||||
public static int AddFootnote(MainDocumentPart mainPart, Paragraph para, string footnoteText)
|
||||
{
|
||||
// Ensure footnotes part exists with separators
|
||||
if (mainPart.FootnotesPart == null)
|
||||
{
|
||||
SetupFootnotesPart(mainPart);
|
||||
}
|
||||
|
||||
int footnoteId = GetNextFootnoteId(mainPart.FootnotesPart!);
|
||||
|
||||
// 1. Add the footnote reference in the body paragraph
|
||||
// This renders the superscript number (e.g., "1") in the text
|
||||
var refRun = new Run(
|
||||
new RunProperties(new VerticalTextAlignment { Val = VerticalPositionValues.Superscript }),
|
||||
new FootnoteReference { Id = footnoteId });
|
||||
para.Append(refRun);
|
||||
|
||||
// 2. Add the footnote content in the FootnotesPart
|
||||
var footnote = new Footnote { Id = footnoteId };
|
||||
|
||||
// Footnote paragraph starts with a self-referencing FootnoteReference
|
||||
var footnotePara = new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "FootnoteText" }),
|
||||
new Run(
|
||||
new RunProperties(new VerticalTextAlignment { Val = VerticalPositionValues.Superscript }),
|
||||
new FootnoteReferenceMark()),
|
||||
new Run(
|
||||
new Text(" " + footnoteText) { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
footnote.Append(footnotePara);
|
||||
mainPart.FootnotesPart!.Footnotes!.Append(footnote);
|
||||
mainPart.FootnotesPart.Footnotes.Save();
|
||||
|
||||
return footnoteId;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 3. AddEndnote — same pattern for endnotes
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Adds an endnote. Same two-part pattern as footnotes:
|
||||
/// 1. EndnoteReference in body paragraph
|
||||
/// 2. Endnote element in EndnotesPart
|
||||
///
|
||||
/// EndnotesPart also requires separator (id=-1) and continuationSeparator (id=0).
|
||||
/// Endnotes appear at the end of the document (or section) rather than page bottom.
|
||||
/// </summary>
|
||||
public static int AddEndnote(MainDocumentPart mainPart, Paragraph para, string endnoteText)
|
||||
{
|
||||
// Ensure endnotes part exists with separators
|
||||
if (mainPart.EndnotesPart == null)
|
||||
{
|
||||
SetupEndnotesPart(mainPart);
|
||||
}
|
||||
|
||||
int endnoteId = GetNextEndnoteId(mainPart.EndnotesPart!);
|
||||
|
||||
// 1. Endnote reference in body text
|
||||
var refRun = new Run(
|
||||
new RunProperties(new VerticalTextAlignment { Val = VerticalPositionValues.Superscript }),
|
||||
new EndnoteReference { Id = endnoteId });
|
||||
para.Append(refRun);
|
||||
|
||||
// 2. Endnote content in EndnotesPart
|
||||
var endnote = new Endnote { Id = endnoteId };
|
||||
var endnotePara = new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "EndnoteText" }),
|
||||
new Run(
|
||||
new RunProperties(new VerticalTextAlignment { Val = VerticalPositionValues.Superscript }),
|
||||
new EndnoteReferenceMark()),
|
||||
new Run(
|
||||
new Text(" " + endnoteText) { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
endnote.Append(endnotePara);
|
||||
mainPart.EndnotesPart!.Endnotes!.Append(endnote);
|
||||
mainPart.EndnotesPart.Endnotes.Save();
|
||||
|
||||
return endnoteId;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 4. SetFootnoteProperties — position, numbering restart
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Configures footnote properties on a section:
|
||||
/// - Position: page bottom (default) vs. beneath text
|
||||
/// - Numbering format: decimal, lowerRoman, symbol, etc.
|
||||
/// - Numbering restart: continuous, eachSection, eachPage
|
||||
///
|
||||
/// These go inside SectionProperties as w:footnotePr.
|
||||
/// </summary>
|
||||
public static void SetFootnoteProperties(SectionProperties sectPr)
|
||||
{
|
||||
var footnotePr = new FootnoteProperties();
|
||||
|
||||
// Position: PageBottom is default; BeneathText puts them right after text
|
||||
footnotePr.Append(new FootnotePosition { Val = FootnotePositionValues.PageBottom });
|
||||
|
||||
// Numbering format: decimal (1, 2, 3...)
|
||||
footnotePr.Append(new NumberingFormat { Val = NumberFormatValues.Decimal });
|
||||
|
||||
// Restart numbering each section (alternatives: Continuous, EachPage)
|
||||
footnotePr.Append(new NumberingRestart { Val = RestartNumberValues.EachSection });
|
||||
|
||||
// Starting number
|
||||
footnotePr.Append(new NumberingStart { Val = 1 });
|
||||
|
||||
sectPr.Append(footnotePr);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 5. SetupCommentSystem — all 4 parts
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the complete comment system with all required parts:
|
||||
/// 1. WordprocessingCommentsPart — comments.xml (the Comment elements)
|
||||
/// 2. WordprocessingCommentsExPart — commentsExtended.xml (reply threading, done state)
|
||||
/// 3. WordprocessingCommentsIdsPart — commentsIds.xml (durable GUID-based comment IDs)
|
||||
/// 4. WordprocessingPeoplePart — people.xml (author identities)
|
||||
///
|
||||
/// All four parts must be present and consistent for modern Word to
|
||||
/// display comments correctly without repair prompts.
|
||||
/// </summary>
|
||||
public static void SetupCommentSystem(MainDocumentPart mainPart)
|
||||
{
|
||||
// Part 1: comments.xml
|
||||
if (mainPart.WordprocessingCommentsPart == null)
|
||||
{
|
||||
var commentsPart = mainPart.AddNewPart<WordprocessingCommentsPart>();
|
||||
commentsPart.Comments = new Comments();
|
||||
commentsPart.Comments.Save();
|
||||
}
|
||||
|
||||
// Part 2: commentsExtended.xml — for reply threading and done/resolved state
|
||||
// Uses W15 namespace (word/2012/wordml)
|
||||
if (mainPart.WordprocessingCommentsExPart == null)
|
||||
{
|
||||
var commentsExPart = mainPart.AddNewPart<WordprocessingCommentsExPart>();
|
||||
// Initialize with root element via raw XML since the typed API is limited
|
||||
using var writer = new System.IO.StreamWriter(commentsExPart.GetStream(System.IO.FileMode.Create));
|
||||
writer.Write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
|
||||
+ "<w15:commentsEx xmlns:w15=\"http://schemas.microsoft.com/office/word/2012/wordml\""
|
||||
+ " xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\""
|
||||
+ " mc:Ignorable=\"w15\"/>");
|
||||
}
|
||||
|
||||
// Part 3: commentsIds.xml — durable comment identifiers (W16CID namespace)
|
||||
if (mainPart.WordprocessingCommentsIdsPart == null)
|
||||
{
|
||||
var commentsIdsPart = mainPart.AddNewPart<WordprocessingCommentsIdsPart>();
|
||||
using var writer = new System.IO.StreamWriter(commentsIdsPart.GetStream(System.IO.FileMode.Create));
|
||||
writer.Write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
|
||||
+ "<w16cid:commentsIds xmlns:w16cid=\"http://schemas.microsoft.com/office/word/2016/wordml/cid\"/>");
|
||||
}
|
||||
|
||||
// Part 4: people.xml — author info for comments
|
||||
if (mainPart.WordprocessingPeoplePart == null)
|
||||
{
|
||||
var peoplePart = mainPart.AddNewPart<WordprocessingPeoplePart>();
|
||||
peoplePart.People = new W15People();
|
||||
peoplePart.People.Save();
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 6. AddComment — full comment with range markers
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Adds a comment anchored to an entire paragraph with three coordinated elements:
|
||||
///
|
||||
/// In the document body (inside the paragraph):
|
||||
/// 1. CommentRangeStart { Id = commentId } — before commented content
|
||||
/// 2. CommentRangeEnd { Id = commentId } — after commented content
|
||||
/// 3. Run containing CommentReference { Id = commentId } — immediately after RangeEnd
|
||||
///
|
||||
/// In comments.xml:
|
||||
/// 4. Comment { Id = commentId } with paragraph content
|
||||
///
|
||||
/// The CommentReference run is what makes the comment indicator appear in the margin.
|
||||
/// </summary>
|
||||
public static int AddComment(MainDocumentPart mainPart, Paragraph para, string author, string text)
|
||||
{
|
||||
SetupCommentSystem(mainPart);
|
||||
|
||||
var commentsPart = mainPart.WordprocessingCommentsPart!;
|
||||
int commentId = GetNextCommentId(commentsPart);
|
||||
string idStr = commentId.ToString();
|
||||
|
||||
// Add comment range markers to the paragraph
|
||||
// Insert CommentRangeStart before existing content
|
||||
para.InsertAt(new CommentRangeStart { Id = idStr }, 0);
|
||||
|
||||
// Append CommentRangeEnd + CommentReference after content
|
||||
para.Append(new CommentRangeEnd { Id = idStr });
|
||||
para.Append(new Run(
|
||||
new RunProperties(
|
||||
new RunStyle { Val = "CommentReference" }),
|
||||
new CommentReference { Id = idStr }));
|
||||
|
||||
// Create the comment content in comments.xml
|
||||
var comment = new Comment
|
||||
{
|
||||
Id = idStr,
|
||||
Author = author,
|
||||
Date = DateTime.UtcNow,
|
||||
Initials = GetInitials(author)
|
||||
};
|
||||
comment.Append(new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "CommentText" }),
|
||||
new Run(
|
||||
new RunProperties(new RunStyle { Val = "CommentReference" }),
|
||||
new AnnotationReferenceMark()),
|
||||
new Run(new Text(text) { Space = SpaceProcessingModeValues.Preserve })));
|
||||
|
||||
commentsPart.Comments!.Append(comment);
|
||||
commentsPart.Comments.Save();
|
||||
|
||||
// Register author in people.xml
|
||||
EnsurePersonEntry(mainPart, author);
|
||||
|
||||
return commentId;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 7. AddCommentReply — reply via commentsExtended
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Adds a reply to an existing comment. Replies are threaded via commentsExtended.xml
|
||||
/// which links the reply's paraId to the parent comment's paraId using w15:paraIdParent.
|
||||
///
|
||||
/// The reply is a separate Comment element in comments.xml (with its own unique id),
|
||||
/// but it does NOT get CommentRangeStart/End markers in the document body.
|
||||
/// The threading relationship is purely in commentsExtended.xml.
|
||||
/// </summary>
|
||||
public static int AddCommentReply(MainDocumentPart mainPart, int parentCommentId, string author, string replyText)
|
||||
{
|
||||
SetupCommentSystem(mainPart);
|
||||
|
||||
var commentsPart = mainPart.WordprocessingCommentsPart!;
|
||||
int replyId = GetNextCommentId(commentsPart);
|
||||
string replyIdStr = replyId.ToString();
|
||||
|
||||
// Generate a unique paraId for the reply paragraph (w14:paraId)
|
||||
string replyParaId = GenerateParaId();
|
||||
|
||||
// Create reply as a Comment in comments.xml
|
||||
var reply = new Comment
|
||||
{
|
||||
Id = replyIdStr,
|
||||
Author = author,
|
||||
Date = DateTime.UtcNow,
|
||||
Initials = GetInitials(author)
|
||||
};
|
||||
|
||||
var replyPara = new Paragraph(
|
||||
new ParagraphProperties(new ParagraphStyleId { Val = "CommentText" }),
|
||||
new Run(new Text(replyText) { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
// Set paraId on the paragraph via extended attributes (W14 namespace)
|
||||
replyPara.SetAttribute(new OpenXmlAttribute("w14", "paraId", "http://schemas.microsoft.com/office/word/2010/wordml", replyParaId));
|
||||
|
||||
reply.Append(replyPara);
|
||||
commentsPart.Comments!.Append(reply);
|
||||
commentsPart.Comments.Save();
|
||||
|
||||
// Link the reply to the parent in commentsExtended.xml
|
||||
// Find the parent comment's paraId, then create a commentEx element
|
||||
var parentComment = commentsPart.Comments.Elements<Comment>()
|
||||
.FirstOrDefault(c => c.Id?.Value == parentCommentId.ToString());
|
||||
|
||||
string parentParaId = "00000000";
|
||||
if (parentComment != null)
|
||||
{
|
||||
var firstPara = parentComment.GetFirstChild<Paragraph>();
|
||||
if (firstPara != null)
|
||||
{
|
||||
var attr = firstPara.GetAttributes().FirstOrDefault(a => a.LocalName == "paraId");
|
||||
if (attr.Value != null) parentParaId = attr.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// Write commentEx entry to commentsExtended.xml
|
||||
// This links replyParaId -> parentParaId
|
||||
if (mainPart.WordprocessingCommentsExPart != null)
|
||||
{
|
||||
var stream = mainPart.WordprocessingCommentsExPart.GetStream(System.IO.FileMode.Open);
|
||||
var doc = System.Xml.Linq.XDocument.Load(stream);
|
||||
stream.Dispose();
|
||||
|
||||
System.Xml.Linq.XNamespace w15 = "http://schemas.microsoft.com/office/word/2012/wordml";
|
||||
doc.Root!.Add(new System.Xml.Linq.XElement(w15 + "commentEx",
|
||||
new System.Xml.Linq.XAttribute(w15 + "paraId", replyParaId),
|
||||
new System.Xml.Linq.XAttribute(w15 + "paraIdParent", parentParaId)));
|
||||
|
||||
using var writeStream = mainPart.WordprocessingCommentsExPart.GetStream(System.IO.FileMode.Create);
|
||||
doc.Save(writeStream);
|
||||
}
|
||||
|
||||
EnsurePersonEntry(mainPart, author);
|
||||
|
||||
return replyId;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 8. DeleteComment — remove from all parts + markers
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Completely removes a comment from the document by cleaning up all four locations:
|
||||
/// 1. CommentRangeStart/End from document body
|
||||
/// 2. CommentReference run from document body
|
||||
/// 3. Comment element from comments.xml
|
||||
/// 4. CommentEx entry from commentsExtended.xml
|
||||
///
|
||||
/// Failing to remove from all locations causes Word to show repair prompts.
|
||||
/// </summary>
|
||||
public static void DeleteComment(MainDocumentPart mainPart, int commentId)
|
||||
{
|
||||
string idStr = commentId.ToString();
|
||||
|
||||
// 1. Remove markers from document body
|
||||
var body = mainPart.Document?.Body;
|
||||
if (body != null)
|
||||
{
|
||||
// Remove all CommentRangeStart with matching id
|
||||
foreach (var start in body.Descendants<CommentRangeStart>()
|
||||
.Where(s => s.Id?.Value == idStr).ToList())
|
||||
{
|
||||
start.Remove();
|
||||
}
|
||||
|
||||
// Remove all CommentRangeEnd with matching id
|
||||
foreach (var end in body.Descendants<CommentRangeEnd>()
|
||||
.Where(e => e.Id?.Value == idStr).ToList())
|
||||
{
|
||||
end.Remove();
|
||||
}
|
||||
|
||||
// Remove runs containing CommentReference with matching id
|
||||
foreach (var reference in body.Descendants<CommentReference>()
|
||||
.Where(r => r.Id?.Value == idStr).ToList())
|
||||
{
|
||||
// Remove the parent Run, not just the CommentReference
|
||||
reference.Parent?.Remove();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Remove from comments.xml
|
||||
var commentsPart = mainPart.WordprocessingCommentsPart;
|
||||
if (commentsPart?.Comments != null)
|
||||
{
|
||||
var comment = commentsPart.Comments.Elements<Comment>()
|
||||
.FirstOrDefault(c => c.Id?.Value == idStr);
|
||||
comment?.Remove();
|
||||
commentsPart.Comments.Save();
|
||||
}
|
||||
|
||||
// 3. Remove from commentsExtended.xml (reply threading)
|
||||
if (mainPart.WordprocessingCommentsExPart != null)
|
||||
{
|
||||
var stream = mainPart.WordprocessingCommentsExPart.GetStream(System.IO.FileMode.Open);
|
||||
var doc = System.Xml.Linq.XDocument.Load(stream);
|
||||
stream.Dispose();
|
||||
|
||||
System.Xml.Linq.XNamespace w15 = "http://schemas.microsoft.com/office/word/2012/wordml";
|
||||
// Find and remove commentEx entries that reference this comment's paraId
|
||||
// We need to find the paraId from the comment first, but since we already removed it,
|
||||
// we remove by matching — in practice you would track paraIds before deletion
|
||||
var toRemove = doc.Root!.Elements(w15 + "commentEx").ToList();
|
||||
// Remove entries whose paraId matches any paragraph in the deleted comment
|
||||
foreach (var elem in toRemove)
|
||||
{
|
||||
// In a full implementation, match by paraId correlation
|
||||
// For safety, this removes entries that are no longer referenced
|
||||
_ = elem; // kept for reference
|
||||
}
|
||||
|
||||
using var writeStream = mainPart.WordprocessingCommentsExPart.GetStream(System.IO.FileMode.Create);
|
||||
doc.Save(writeStream);
|
||||
}
|
||||
|
||||
// 4. Remove from commentsIds.xml if present
|
||||
if (mainPart.WordprocessingCommentsIdsPart != null)
|
||||
{
|
||||
var stream = mainPart.WordprocessingCommentsIdsPart.GetStream(System.IO.FileMode.Open);
|
||||
var doc = System.Xml.Linq.XDocument.Load(stream);
|
||||
stream.Dispose();
|
||||
|
||||
System.Xml.Linq.XNamespace w16cid = "http://schemas.microsoft.com/office/word/2016/wordml/cid";
|
||||
var toRemove = doc.Root!.Elements(w16cid + "commentId")
|
||||
.Where(e => (string?)e.Attribute(w16cid + "paraId") == idStr)
|
||||
.ToList();
|
||||
foreach (var elem in toRemove)
|
||||
{
|
||||
elem.Remove();
|
||||
}
|
||||
|
||||
using var writeStream = mainPart.WordprocessingCommentsIdsPart.GetStream(System.IO.FileMode.Create);
|
||||
doc.Save(writeStream);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 9. AddBookmark — BookmarkStart + BookmarkEnd
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Adds a bookmark spanning the entire paragraph content.
|
||||
///
|
||||
/// Structure:
|
||||
/// <w:bookmarkStart w:id="1" w:name="my_bookmark"/>
|
||||
/// ... paragraph content ...
|
||||
/// <w:bookmarkEnd w:id="1"/>
|
||||
///
|
||||
/// The id must be unique across all bookmarks in the document.
|
||||
/// The name is used to reference the bookmark in REF fields and hyperlinks.
|
||||
/// Bookmark names are case-insensitive and cannot contain spaces.
|
||||
/// </summary>
|
||||
public static void AddBookmark(Paragraph para, string bookmarkName, int bookmarkId)
|
||||
{
|
||||
string idStr = bookmarkId.ToString();
|
||||
|
||||
// Insert BookmarkStart at the beginning of the paragraph
|
||||
para.InsertAt(new BookmarkStart { Id = idStr, Name = bookmarkName }, 0);
|
||||
|
||||
// Append BookmarkEnd at the end of the paragraph
|
||||
para.Append(new BookmarkEnd { Id = idStr });
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 10. AddInternalHyperlink — Hyperlink with Anchor
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Adds a hyperlink that jumps to a bookmark within the same document.
|
||||
///
|
||||
/// Uses the Anchor property (NOT a relationship) to reference the bookmark name.
|
||||
/// The run inside the Hyperlink should have "Hyperlink" character style for blue underline.
|
||||
///
|
||||
/// Structure:
|
||||
/// <w:hyperlink w:anchor="bookmarkName">
|
||||
/// <w:r><w:rPr><w:rStyle w:val="Hyperlink"/></w:rPr><w:t>Click here</w:t></w:r>
|
||||
/// </w:hyperlink>
|
||||
/// </summary>
|
||||
public static Hyperlink AddInternalHyperlink(Paragraph para, string bookmarkName)
|
||||
{
|
||||
var hyperlink = new Hyperlink { Anchor = bookmarkName };
|
||||
|
||||
hyperlink.Append(new Run(
|
||||
new RunProperties(
|
||||
new RunStyle { Val = "Hyperlink" },
|
||||
new Color { Val = "0563C1", ThemeColor = ThemeColorValues.Hyperlink }),
|
||||
new Text(bookmarkName) { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
para.Append(hyperlink);
|
||||
return hyperlink;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 11. AddExternalHyperlink — Hyperlink with relationship
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Adds a hyperlink to an external URL.
|
||||
///
|
||||
/// Unlike internal hyperlinks, external ones require a HyperlinkRelationship
|
||||
/// in the part's .rels file. The Hyperlink element references the relationship Id.
|
||||
///
|
||||
/// Steps:
|
||||
/// 1. Create a HyperlinkRelationship with the URL (isExternal: true)
|
||||
/// 2. Create a Hyperlink element with Id = relationship Id
|
||||
/// 3. Style the run with "Hyperlink" character style
|
||||
/// </summary>
|
||||
public static Hyperlink AddExternalHyperlink(MainDocumentPart mainPart, Paragraph para, string url, string displayText)
|
||||
{
|
||||
// Step 1: Create the relationship (external = true)
|
||||
var relationship = mainPart.AddHyperlinkRelationship(new Uri(url, UriKind.Absolute), isExternal: true);
|
||||
|
||||
// Step 2: Create the Hyperlink element referencing the relationship
|
||||
var hyperlink = new Hyperlink { Id = relationship.Id };
|
||||
|
||||
// Step 3: Styled run inside the hyperlink
|
||||
hyperlink.Append(new Run(
|
||||
new RunProperties(
|
||||
new RunStyle { Val = "Hyperlink" },
|
||||
new Color { Val = "0563C1", ThemeColor = ThemeColorValues.Hyperlink },
|
||||
new Underline { Val = UnderlineValues.Single }),
|
||||
new Text(displayText) { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
para.Append(hyperlink);
|
||||
return hyperlink;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Private helpers
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
private static EndnotesPart SetupEndnotesPart(MainDocumentPart mainPart)
|
||||
{
|
||||
var endnotesPart = mainPart.EndnotesPart
|
||||
?? mainPart.AddNewPart<EndnotesPart>();
|
||||
|
||||
endnotesPart.Endnotes = new Endnotes();
|
||||
|
||||
var separator = new Endnote { Type = FootnoteEndnoteValues.Separator, Id = -1 };
|
||||
separator.Append(new Paragraph(
|
||||
new ParagraphProperties(new SpacingBetweenLines { After = "0", Line = "240", LineRule = LineSpacingRuleValues.Auto }),
|
||||
new Run(new SeparatorMark())));
|
||||
endnotesPart.Endnotes.Append(separator);
|
||||
|
||||
var contSeparator = new Endnote { Type = FootnoteEndnoteValues.ContinuationSeparator, Id = 0 };
|
||||
contSeparator.Append(new Paragraph(
|
||||
new ParagraphProperties(new SpacingBetweenLines { After = "0", Line = "240", LineRule = LineSpacingRuleValues.Auto }),
|
||||
new Run(new ContinuationSeparatorMark())));
|
||||
endnotesPart.Endnotes.Append(contSeparator);
|
||||
|
||||
endnotesPart.Endnotes.Save();
|
||||
return endnotesPart;
|
||||
}
|
||||
|
||||
private static int GetNextFootnoteId(FootnotesPart footnotesPart)
|
||||
{
|
||||
int maxId = 0;
|
||||
if (footnotesPart.Footnotes != null)
|
||||
{
|
||||
foreach (var fn in footnotesPart.Footnotes.Elements<Footnote>())
|
||||
{
|
||||
if (fn.Id?.Value != null && fn.Id.Value > maxId)
|
||||
maxId = (int)fn.Id.Value;
|
||||
}
|
||||
}
|
||||
return maxId + 1;
|
||||
}
|
||||
|
||||
private static int GetNextEndnoteId(EndnotesPart endnotesPart)
|
||||
{
|
||||
int maxId = 0;
|
||||
if (endnotesPart.Endnotes != null)
|
||||
{
|
||||
foreach (var en in endnotesPart.Endnotes.Elements<Endnote>())
|
||||
{
|
||||
if (en.Id?.Value != null && en.Id.Value > maxId)
|
||||
maxId = (int)en.Id.Value;
|
||||
}
|
||||
}
|
||||
return maxId + 1;
|
||||
}
|
||||
|
||||
private static int GetNextCommentId(WordprocessingCommentsPart commentsPart)
|
||||
{
|
||||
int maxId = 0;
|
||||
if (commentsPart.Comments != null)
|
||||
{
|
||||
foreach (var c in commentsPart.Comments.Elements<Comment>())
|
||||
{
|
||||
if (c.Id?.Value != null && int.TryParse(c.Id.Value, out int id) && id > maxId)
|
||||
maxId = id;
|
||||
}
|
||||
}
|
||||
return maxId + 1;
|
||||
}
|
||||
|
||||
private static string GetInitials(string author)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(author)) return "A";
|
||||
var parts = author.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
return string.Concat(parts.Select(p => p[..1].ToUpperInvariant()));
|
||||
}
|
||||
|
||||
private static string GenerateParaId()
|
||||
{
|
||||
// paraId is an 8-character hex string (32-bit unsigned integer)
|
||||
return Random.Shared.Next(0x10000000, int.MaxValue).ToString("X8");
|
||||
}
|
||||
|
||||
private static void EnsurePersonEntry(MainDocumentPart mainPart, string author)
|
||||
{
|
||||
var peoplePart = mainPart.WordprocessingPeoplePart;
|
||||
if (peoplePart?.People == null) return;
|
||||
|
||||
// Check if this author already has an entry
|
||||
bool exists = peoplePart.People.Elements<W15Person>()
|
||||
.Any(p => p.Author?.Value == author);
|
||||
|
||||
if (!exists)
|
||||
{
|
||||
var person = new W15Person { Author = author };
|
||||
// PresenceInfo — the provider/userId for the author's identity
|
||||
person.Append(new W15PresenceInfo
|
||||
{
|
||||
ProviderId = "None",
|
||||
UserId = author
|
||||
});
|
||||
peoplePart.People.Append(person);
|
||||
peoplePart.People.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,838 @@
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
using A = DocumentFormat.OpenXml.Drawing;
|
||||
using DW = DocumentFormat.OpenXml.Drawing.Wordprocessing;
|
||||
using PIC = DocumentFormat.OpenXml.Drawing.Pictures;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Samples;
|
||||
|
||||
/// <summary>
|
||||
/// Comprehensive reference for OpenXML headers, footers, and page numbers.
|
||||
///
|
||||
/// Architecture:
|
||||
/// - Headers/footers live in separate HeaderPart/FooterPart containers.
|
||||
/// - They are linked to sections via HeaderReference/FooterReference in SectionProperties.
|
||||
/// - Each reference has a Type: Default, First, Even.
|
||||
/// - The relationship ID (r:id) connects the reference to the part.
|
||||
///
|
||||
/// XML structure in SectionProperties:
|
||||
/// <w:sectPr>
|
||||
/// <w:headerReference w:type="default" r:id="rId7"/>
|
||||
/// <w:footerReference w:type="default" r:id="rId8"/>
|
||||
/// <w:headerReference w:type="first" r:id="rId9"/>
|
||||
/// <w:titlePg/> <!-- needed to activate first-page header/footer -->
|
||||
/// </w:sectPr>
|
||||
///
|
||||
/// Header/Footer XML (in separate part):
|
||||
/// <w:hdr> (or <w:ftr>)
|
||||
/// <w:p>
|
||||
/// <w:pPr>...</w:pPr>
|
||||
/// <w:r><w:t>Header text</w:t></w:r>
|
||||
/// </w:p>
|
||||
/// </w:hdr>
|
||||
///
|
||||
/// Page number fields use complex field codes:
|
||||
/// PAGE — current page number
|
||||
/// NUMPAGES — total page count
|
||||
/// </summary>
|
||||
public static class HeaderFooterSamples
|
||||
{
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 1. AddSimpleHeader — basic text header
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// Adds a simple text header to the default header slot.
|
||||
///
|
||||
/// Steps:
|
||||
/// 1. Create a HeaderPart on the MainDocumentPart
|
||||
/// 2. Set its Header content (must contain at least one Paragraph)
|
||||
/// 3. Get the relationship ID
|
||||
/// 4. Add HeaderReference to SectionProperties with type="default"
|
||||
///
|
||||
/// XML in header part:
|
||||
/// <w:hdr>
|
||||
/// <w:p>
|
||||
/// <w:pPr><w:jc w:val="right"/></w:pPr>
|
||||
/// <w:r>
|
||||
/// <w:rPr><w:color w:val="808080"/><w:sz w:val="18"/></w:rPr>
|
||||
/// <w:t>My Document Header</w:t>
|
||||
/// </w:r>
|
||||
/// </w:p>
|
||||
/// </w:hdr>
|
||||
///
|
||||
/// XML in sectPr:
|
||||
/// <w:headerReference w:type="default" r:id="rIdXX"/>
|
||||
/// </summary>
|
||||
public static void AddSimpleHeader(MainDocumentPart mainPart, SectionProperties sectPr, string text)
|
||||
{
|
||||
var headerPart = mainPart.AddNewPart<HeaderPart>();
|
||||
|
||||
headerPart.Header = new Header(
|
||||
new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Right }),
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new Color { Val = "808080" },
|
||||
new FontSize { Val = "18" }), // 9pt (half-points)
|
||||
new Text(text) { Space = SpaceProcessingModeValues.Preserve })));
|
||||
headerPart.Header.Save();
|
||||
|
||||
var headerRefId = mainPart.GetIdOfPart(headerPart);
|
||||
sectPr.Append(new HeaderReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
Id = headerRefId
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 2. AddSimpleFooter — basic text footer
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// Adds a simple text footer to the default footer slot.
|
||||
///
|
||||
/// XML in footer part:
|
||||
/// <w:ftr>
|
||||
/// <w:p>
|
||||
/// <w:pPr><w:jc w:val="center"/></w:pPr>
|
||||
/// <w:r><w:t>Confidential</w:t></w:r>
|
||||
/// </w:p>
|
||||
/// </w:ftr>
|
||||
///
|
||||
/// XML in sectPr:
|
||||
/// <w:footerReference w:type="default" r:id="rIdXX"/>
|
||||
/// </summary>
|
||||
public static void AddSimpleFooter(MainDocumentPart mainPart, SectionProperties sectPr, string text)
|
||||
{
|
||||
var footerPart = mainPart.AddNewPart<FooterPart>();
|
||||
|
||||
footerPart.Footer = new Footer(
|
||||
new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center }),
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new Color { Val = "808080" },
|
||||
new FontSize { Val = "18" }),
|
||||
new Text(text) { Space = SpaceProcessingModeValues.Preserve })));
|
||||
footerPart.Footer.Save();
|
||||
|
||||
var footerRefId = mainPart.GetIdOfPart(footerPart);
|
||||
sectPr.Append(new FooterReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
Id = footerRefId
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 3. AddPageNumberFooter — centered page number
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// Adds a centered page number footer using the PAGE field code.
|
||||
///
|
||||
/// Field code pattern (3 runs):
|
||||
/// Run 1: FieldChar Begin
|
||||
/// Run 2: FieldCode " PAGE "
|
||||
/// Run 3: FieldChar End
|
||||
///
|
||||
/// XML:
|
||||
/// <w:ftr>
|
||||
/// <w:p>
|
||||
/// <w:pPr><w:jc w:val="center"/></w:pPr>
|
||||
/// <w:r><w:fldChar w:fldCharType="begin"/></w:r>
|
||||
/// <w:r><w:instrText xml:space="preserve"> PAGE </w:instrText></w:r>
|
||||
/// <w:r><w:fldChar w:fldCharType="end"/></w:r>
|
||||
/// </w:p>
|
||||
/// </w:ftr>
|
||||
///
|
||||
/// GOTCHA: FieldCode text MUST have leading/trailing spaces: " PAGE ", not "PAGE".
|
||||
/// GOTCHA: Use Space = SpaceProcessingModeValues.Preserve on FieldCode to keep spaces.
|
||||
/// </summary>
|
||||
public static void AddPageNumberFooter(MainDocumentPart mainPart, SectionProperties sectPr)
|
||||
{
|
||||
var footerPart = mainPart.AddNewPart<FooterPart>();
|
||||
|
||||
var paragraph = new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center }));
|
||||
|
||||
// PAGE field: Begin → InstrText → End
|
||||
paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }));
|
||||
paragraph.Append(new Run(new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.End }));
|
||||
|
||||
footerPart.Footer = new Footer(paragraph);
|
||||
footerPart.Footer.Save();
|
||||
|
||||
var footerRefId = mainPart.GetIdOfPart(footerPart);
|
||||
sectPr.Append(new FooterReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
Id = footerRefId
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 4. AddPageXofYFooter — "Page X of Y"
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// Adds a footer with "Page X of Y" format using PAGE and NUMPAGES field codes.
|
||||
///
|
||||
/// XML:
|
||||
/// <w:ftr>
|
||||
/// <w:p>
|
||||
/// <w:pPr><w:jc w:val="center"/></w:pPr>
|
||||
/// <w:r><w:t xml:space="preserve">Page </w:t></w:r>
|
||||
/// <w:r><w:fldChar w:fldCharType="begin"/></w:r>
|
||||
/// <w:r><w:instrText xml:space="preserve"> PAGE </w:instrText></w:r>
|
||||
/// <w:r><w:fldChar w:fldCharType="end"/></w:r>
|
||||
/// <w:r><w:t xml:space="preserve"> of </w:t></w:r>
|
||||
/// <w:r><w:fldChar w:fldCharType="begin"/></w:r>
|
||||
/// <w:r><w:instrText xml:space="preserve"> NUMPAGES </w:instrText></w:r>
|
||||
/// <w:r><w:fldChar w:fldCharType="end"/></w:r>
|
||||
/// </w:p>
|
||||
/// </w:ftr>
|
||||
/// </summary>
|
||||
public static void AddPageXofYFooter(MainDocumentPart mainPart, SectionProperties sectPr)
|
||||
{
|
||||
var footerPart = mainPart.AddNewPart<FooterPart>();
|
||||
|
||||
var paragraph = new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center }));
|
||||
|
||||
// "Page "
|
||||
paragraph.Append(new Run(new Text("Page ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
// PAGE field
|
||||
paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }));
|
||||
paragraph.Append(new Run(new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.End }));
|
||||
|
||||
// " of "
|
||||
paragraph.Append(new Run(new Text(" of ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
// NUMPAGES field
|
||||
paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }));
|
||||
paragraph.Append(new Run(new FieldCode(" NUMPAGES ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.End }));
|
||||
|
||||
footerPart.Footer = new Footer(paragraph);
|
||||
footerPart.Footer.Save();
|
||||
|
||||
var footerRefId = mainPart.GetIdOfPart(footerPart);
|
||||
sectPr.Append(new FooterReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
Id = footerRefId
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 5. AddDifferentFirstPageHeader — TitlePage element
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// Adds a different header for the first page vs. subsequent pages.
|
||||
///
|
||||
/// Requires:
|
||||
/// 1. <w:titlePg/> in SectionProperties to enable first-page header/footer
|
||||
/// 2. HeaderReference with Type="first" for the first page header
|
||||
/// 3. HeaderReference with Type="default" for subsequent pages
|
||||
///
|
||||
/// XML in sectPr:
|
||||
/// <w:sectPr>
|
||||
/// <w:headerReference w:type="first" r:id="rIdFirst"/>
|
||||
/// <w:headerReference w:type="default" r:id="rIdDefault"/>
|
||||
/// <w:titlePg/> <!-- CRITICAL: without this, first-page header is ignored -->
|
||||
/// </w:sectPr>
|
||||
///
|
||||
/// GOTCHA: Without <w:titlePg/>, the "first" type header is completely ignored.
|
||||
/// GOTCHA: If you want a blank first-page header, you still need a HeaderPart
|
||||
/// with an empty Paragraph — just don't add text to it.
|
||||
/// </summary>
|
||||
public static void AddDifferentFirstPageHeader(MainDocumentPart mainPart, SectionProperties sectPr)
|
||||
{
|
||||
// First page header: e.g., cover page with large title
|
||||
var firstHeaderPart = mainPart.AddNewPart<HeaderPart>();
|
||||
firstHeaderPart.Header = new Header(
|
||||
new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center }),
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new Bold(),
|
||||
new FontSize { Val = "32" }), // 16pt
|
||||
new Text("COMPANY CONFIDENTIAL"))));
|
||||
firstHeaderPart.Header.Save();
|
||||
|
||||
// Default header for subsequent pages
|
||||
var defaultHeaderPart = mainPart.AddNewPart<HeaderPart>();
|
||||
defaultHeaderPart.Header = new Header(
|
||||
new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Right }),
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new FontSize { Val = "18" }), // 9pt
|
||||
new Text("Internal Document"))));
|
||||
defaultHeaderPart.Header.Save();
|
||||
|
||||
// Link both headers to section
|
||||
sectPr.Append(new HeaderReference
|
||||
{
|
||||
Type = HeaderFooterValues.First,
|
||||
Id = mainPart.GetIdOfPart(firstHeaderPart)
|
||||
});
|
||||
sectPr.Append(new HeaderReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
Id = mainPart.GetIdOfPart(defaultHeaderPart)
|
||||
});
|
||||
|
||||
// CRITICAL: Enable first page header/footer
|
||||
sectPr.Append(new TitlePage());
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 6. AddEvenOddHeaders — EvenAndOddHeaders in Settings
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// Creates different headers for even and odd pages (e.g., for book-style printing).
|
||||
///
|
||||
/// Requires:
|
||||
/// 1. <w:evenAndOddHeaders/> in document Settings (DocumentSettingsPart)
|
||||
/// 2. HeaderReference with Type="default" for odd pages
|
||||
/// 3. HeaderReference with Type="even" for even pages
|
||||
///
|
||||
/// XML in settings.xml:
|
||||
/// <w:settings>
|
||||
/// <w:evenAndOddHeaders/>
|
||||
/// </w:settings>
|
||||
///
|
||||
/// XML in sectPr:
|
||||
/// <w:sectPr>
|
||||
/// <w:headerReference w:type="default" r:id="rIdOdd"/>
|
||||
/// <w:headerReference w:type="even" r:id="rIdEven"/>
|
||||
/// </w:sectPr>
|
||||
///
|
||||
/// GOTCHA: "default" means ODD pages when evenAndOddHeaders is enabled.
|
||||
/// GOTCHA: Without the Settings flag, the "even" header is ignored entirely.
|
||||
/// </summary>
|
||||
public static void AddEvenOddHeaders(MainDocumentPart mainPart, SectionProperties sectPr)
|
||||
{
|
||||
// Enable even/odd header distinction in document settings
|
||||
var settingsPart = mainPart.DocumentSettingsPart
|
||||
?? mainPart.AddNewPart<DocumentSettingsPart>();
|
||||
if (settingsPart.Settings == null)
|
||||
settingsPart.Settings = new Settings();
|
||||
|
||||
// Add EvenAndOddHeaders if not already present
|
||||
if (settingsPart.Settings.GetFirstChild<EvenAndOddHeaders>() == null)
|
||||
{
|
||||
settingsPart.Settings.Append(new EvenAndOddHeaders());
|
||||
}
|
||||
settingsPart.Settings.Save();
|
||||
|
||||
// Odd page header (Type="default" means odd when even/odd is enabled)
|
||||
var oddHeaderPart = mainPart.AddNewPart<HeaderPart>();
|
||||
oddHeaderPart.Header = new Header(
|
||||
new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Right }),
|
||||
new Run(new Text("Chapter Title — Odd Page"))));
|
||||
oddHeaderPart.Header.Save();
|
||||
|
||||
// Even page header
|
||||
var evenHeaderPart = mainPart.AddNewPart<HeaderPart>();
|
||||
evenHeaderPart.Header = new Header(
|
||||
new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Left }),
|
||||
new Run(new Text("Book Title — Even Page"))));
|
||||
evenHeaderPart.Header.Save();
|
||||
|
||||
// Link to section
|
||||
sectPr.Append(new HeaderReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default, // = odd pages
|
||||
Id = mainPart.GetIdOfPart(oddHeaderPart)
|
||||
});
|
||||
sectPr.Append(new HeaderReference
|
||||
{
|
||||
Type = HeaderFooterValues.Even,
|
||||
Id = mainPart.GetIdOfPart(evenHeaderPart)
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 7. AddHeaderWithLogo — image in header
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// Adds a header containing an image (logo).
|
||||
///
|
||||
/// Steps:
|
||||
/// 1. Create HeaderPart
|
||||
/// 2. Add ImagePart to the HeaderPart (NOT to MainDocumentPart)
|
||||
/// 3. Feed the image stream
|
||||
/// 4. Build Drawing element with inline image
|
||||
/// 5. Link HeaderPart to sectPr
|
||||
///
|
||||
/// Image sizing uses EMU (English Metric Units):
|
||||
/// 914400 EMU = 1 inch
|
||||
/// 360000 EMU = 1 cm
|
||||
///
|
||||
/// XML for inline image:
|
||||
/// <w:drawing>
|
||||
/// <wp:inline distT="0" distB="0" distL="0" distR="0">
|
||||
/// <wp:extent cx="914400" cy="457200"/>
|
||||
/// <wp:docPr id="1" name="Logo"/>
|
||||
/// <a:graphic>
|
||||
/// <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
||||
/// <pic:pic>
|
||||
/// <pic:nvPicPr>...</pic:nvPicPr>
|
||||
/// <pic:blipFill><a:blip r:embed="rIdImg"/></pic:blipFill>
|
||||
/// <pic:spPr>...</pic:spPr>
|
||||
/// </pic:pic>
|
||||
/// </a:graphicData>
|
||||
/// </a:graphic>
|
||||
/// </wp:inline>
|
||||
/// </w:drawing>
|
||||
///
|
||||
/// GOTCHA: The ImagePart must be added to the HeaderPart, not the MainDocumentPart.
|
||||
/// If you add it to MainDocumentPart, the relationship ID won't resolve in the header.
|
||||
/// </summary>
|
||||
public static void AddHeaderWithLogo(MainDocumentPart mainPart, SectionProperties sectPr, string imagePath)
|
||||
{
|
||||
var headerPart = mainPart.AddNewPart<HeaderPart>();
|
||||
|
||||
// Add image part to the HEADER part (not main document part)
|
||||
var imagePart = headerPart.AddImagePart(ImagePartType.Png);
|
||||
using (var stream = new FileStream(imagePath, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
imagePart.FeedData(stream);
|
||||
}
|
||||
var imageRelId = headerPart.GetIdOfPart(imagePart);
|
||||
|
||||
// Image dimensions in EMU: 1 inch wide x 0.5 inch tall
|
||||
long widthEmu = 914400; // 1 inch
|
||||
long heightEmu = 457200; // 0.5 inch
|
||||
|
||||
// Build the Drawing element with inline image
|
||||
var drawing = new Drawing(
|
||||
new DW.Inline(
|
||||
new DW.Extent { Cx = widthEmu, Cy = heightEmu },
|
||||
new DW.EffectExtent { LeftEdge = 0, TopEdge = 0, RightEdge = 0, BottomEdge = 0 },
|
||||
new DW.DocProperties { Id = 1U, Name = "Logo" },
|
||||
new A.Graphic(
|
||||
new A.GraphicData(
|
||||
new PIC.Picture(
|
||||
new PIC.NonVisualPictureProperties(
|
||||
new PIC.NonVisualDrawingProperties { Id = 0U, Name = "logo.png" },
|
||||
new PIC.NonVisualPictureDrawingProperties()),
|
||||
new PIC.BlipFill(
|
||||
new A.Blip { Embed = imageRelId },
|
||||
new A.Stretch(new A.FillRectangle())),
|
||||
new PIC.ShapeProperties(
|
||||
new A.Transform2D(
|
||||
new A.Offset { X = 0, Y = 0 },
|
||||
new A.Extents { Cx = widthEmu, Cy = heightEmu }),
|
||||
new A.PresetGeometry(
|
||||
new A.AdjustValueList())
|
||||
{ Preset = A.ShapeTypeValues.Rectangle }))
|
||||
) { Uri = "http://schemas.openxmlformats.org/drawingml/2006/picture" })
|
||||
)
|
||||
{
|
||||
DistanceFromTop = 0U,
|
||||
DistanceFromBottom = 0U,
|
||||
DistanceFromLeft = 0U,
|
||||
DistanceFromRight = 0U
|
||||
});
|
||||
|
||||
headerPart.Header = new Header(
|
||||
new Paragraph(new Run(drawing)));
|
||||
headerPart.Header.Save();
|
||||
|
||||
var headerRefId = mainPart.GetIdOfPart(headerPart);
|
||||
sectPr.Append(new HeaderReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
Id = headerRefId
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 8. AddTableLayoutHeader — 3-column invisible table
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// Creates a header with a 3-column invisible table for precise layout:
|
||||
/// Left cell: Logo placeholder text
|
||||
/// Center cell: Document title (centered)
|
||||
/// Right cell: Page number (right-aligned)
|
||||
///
|
||||
/// The table has no borders, so it's invisible but provides column alignment.
|
||||
///
|
||||
/// XML structure:
|
||||
/// <w:hdr>
|
||||
/// <w:tbl>
|
||||
/// <w:tblPr>
|
||||
/// <w:tblW w:w="5000" w:type="pct"/>
|
||||
/// <w:tblBorders>
|
||||
/// <w:top w:val="none"/> <w:left w:val="none"/> ...
|
||||
/// </w:tblBorders>
|
||||
/// </w:tblPr>
|
||||
/// <w:tblGrid>
|
||||
/// <w:gridCol w:w="3120"/> <w:gridCol w:w="3120"/> <w:gridCol w:w="3120"/>
|
||||
/// </w:tblGrid>
|
||||
/// <w:tr>
|
||||
/// <w:tc> <!-- left: logo text --> </w:tc>
|
||||
/// <w:tc> <!-- center: title --> </w:tc>
|
||||
/// <w:tc> <!-- right: page num --> </w:tc>
|
||||
/// </w:tr>
|
||||
/// </w:tbl>
|
||||
/// </w:hdr>
|
||||
/// </summary>
|
||||
public static void AddTableLayoutHeader(MainDocumentPart mainPart, SectionProperties sectPr)
|
||||
{
|
||||
var headerPart = mainPart.AddNewPart<HeaderPart>();
|
||||
|
||||
// Invisible table (no borders)
|
||||
var table = new Table();
|
||||
var tblPr = new TableProperties(
|
||||
new TableWidth { Width = "5000", Type = TableWidthUnitValues.Pct },
|
||||
new TableBorders(
|
||||
new TopBorder { Val = BorderValues.None, Size = 0, Space = 0, Color = "auto" },
|
||||
new LeftBorder { Val = BorderValues.None, Size = 0, Space = 0, Color = "auto" },
|
||||
new BottomBorder { Val = BorderValues.None, Size = 0, Space = 0, Color = "auto" },
|
||||
new RightBorder { Val = BorderValues.None, Size = 0, Space = 0, Color = "auto" },
|
||||
new InsideHorizontalBorder { Val = BorderValues.None, Size = 0, Space = 0, Color = "auto" },
|
||||
new InsideVerticalBorder { Val = BorderValues.None, Size = 0, Space = 0, Color = "auto" }
|
||||
),
|
||||
// Fixed layout so columns don't shift
|
||||
new TableLayout { Type = TableLayoutValues.Fixed });
|
||||
table.Append(tblPr);
|
||||
|
||||
var grid = new TableGrid(
|
||||
new GridColumn { Width = "3120" },
|
||||
new GridColumn { Width = "3120" },
|
||||
new GridColumn { Width = "3120" });
|
||||
table.Append(grid);
|
||||
|
||||
var row = new TableRow();
|
||||
|
||||
// Left cell: logo/company name
|
||||
var leftCell = new TableCell(
|
||||
new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Left }),
|
||||
new Run(
|
||||
new RunProperties(new Bold(), new FontSize { Val = "18" }),
|
||||
new Text("ACME Corp"))));
|
||||
row.Append(leftCell);
|
||||
|
||||
// Center cell: document title
|
||||
var centerCell = new TableCell(
|
||||
new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center }),
|
||||
new Run(
|
||||
new RunProperties(new FontSize { Val = "18" }),
|
||||
new Text("Technical Report"))));
|
||||
row.Append(centerCell);
|
||||
|
||||
// Right cell: page number
|
||||
var pageNumPara = new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Right }));
|
||||
pageNumPara.Append(new Run(
|
||||
new RunProperties(new FontSize { Val = "18" }),
|
||||
new Text("Page ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
pageNumPara.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }));
|
||||
pageNumPara.Append(new Run(new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
pageNumPara.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.End }));
|
||||
|
||||
var rightCell = new TableCell(pageNumPara);
|
||||
row.Append(rightCell);
|
||||
|
||||
table.Append(row);
|
||||
|
||||
headerPart.Header = new Header(table);
|
||||
headerPart.Header.Save();
|
||||
|
||||
var headerRefId = mainPart.GetIdOfPart(headerPart);
|
||||
sectPr.Append(new HeaderReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
Id = headerRefId
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 9. AddChineseGongWenFooter — "-X-" format, SimSun 14pt
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// Adds a Chinese government document (公文) style footer:
|
||||
/// - Page number in "-X-" format (e.g., "- 1 -")
|
||||
/// - Centered at bottom
|
||||
/// - SimSun (宋体) font, 14pt (Chinese 四号)
|
||||
///
|
||||
/// XML:
|
||||
/// <w:ftr>
|
||||
/// <w:p>
|
||||
/// <w:pPr><w:jc w:val="center"/></w:pPr>
|
||||
/// <w:r>
|
||||
/// <w:rPr>
|
||||
/// <w:rFonts w:ascii="SimSun" w:eastAsia="SimSun"/>
|
||||
/// <w:sz w:val="28"/>
|
||||
/// </w:rPr>
|
||||
/// <w:t xml:space="preserve">- </w:t>
|
||||
/// </w:r>
|
||||
/// <w:r>..PAGE field..</w:r>
|
||||
/// <w:r>
|
||||
/// <w:rPr>...</w:rPr>
|
||||
/// <w:t xml:space="preserve"> -</w:t>
|
||||
/// </w:r>
|
||||
/// </w:p>
|
||||
/// </w:ftr>
|
||||
///
|
||||
/// Chinese font size reference:
|
||||
/// 四号 = 14pt = sz val="28" (half-points)
|
||||
/// 小四 = 12pt = sz val="24"
|
||||
/// 五号 = 10.5pt = sz val="21"
|
||||
/// </summary>
|
||||
public static void AddChineseGongWenFooter(MainDocumentPart mainPart, SectionProperties sectPr)
|
||||
{
|
||||
var footerPart = mainPart.AddNewPart<FooterPart>();
|
||||
|
||||
// Common run properties for the footer: SimSun 14pt (四号)
|
||||
// 14pt = 28 half-points
|
||||
RunProperties MakeGongWenRunProps() => new RunProperties(
|
||||
new RunFonts { Ascii = "SimSun", EastAsia = "SimSun", HighAnsi = "SimSun" },
|
||||
new FontSize { Val = "28" },
|
||||
new FontSizeComplexScript { Val = "28" });
|
||||
|
||||
var paragraph = new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center }));
|
||||
|
||||
// "- " prefix
|
||||
paragraph.Append(new Run(
|
||||
MakeGongWenRunProps(),
|
||||
new Text("- ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
// PAGE field with same formatting
|
||||
paragraph.Append(new Run(
|
||||
MakeGongWenRunProps(),
|
||||
new FieldChar { FieldCharType = FieldCharValues.Begin }));
|
||||
paragraph.Append(new Run(
|
||||
MakeGongWenRunProps(),
|
||||
new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
paragraph.Append(new Run(
|
||||
MakeGongWenRunProps(),
|
||||
new FieldChar { FieldCharType = FieldCharValues.End }));
|
||||
|
||||
// " -" suffix
|
||||
paragraph.Append(new Run(
|
||||
MakeGongWenRunProps(),
|
||||
new Text(" -") { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
footerPart.Footer = new Footer(paragraph);
|
||||
footerPart.Footer.Save();
|
||||
|
||||
var footerRefId = mainPart.GetIdOfPart(footerPart);
|
||||
sectPr.Append(new FooterReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
Id = footerRefId
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 10. AddHeaderWithHorizontalLine — bottom border line
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// Adds a header with a horizontal line (bottom border) beneath the text.
|
||||
/// This is a common style: header text with a line separating it from content.
|
||||
///
|
||||
/// The line is achieved via a paragraph bottom border in the header, NOT a
|
||||
/// separate drawing element.
|
||||
///
|
||||
/// XML:
|
||||
/// <w:hdr>
|
||||
/// <w:p>
|
||||
/// <w:pPr>
|
||||
/// <w:pBdr>
|
||||
/// <w:bottom w:val="single" w:sz="6" w:space="1" w:color="000000"/>
|
||||
/// </w:pBdr>
|
||||
/// <w:jc w:val="center"/>
|
||||
/// </w:pPr>
|
||||
/// <w:r><w:t>Document Header</w:t></w:r>
|
||||
/// </w:p>
|
||||
/// </w:hdr>
|
||||
///
|
||||
/// Border space attribute: space between text and border line, in points.
|
||||
/// Border size: in eighth-points (6 = 0.75pt).
|
||||
/// </summary>
|
||||
public static void AddHeaderWithHorizontalLine(MainDocumentPart mainPart, SectionProperties sectPr)
|
||||
{
|
||||
var headerPart = mainPart.AddNewPart<HeaderPart>();
|
||||
|
||||
var paragraph = new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new ParagraphBorders(
|
||||
new BottomBorder
|
||||
{
|
||||
Val = BorderValues.Single,
|
||||
Size = 6, // 0.75pt line (in eighth-points)
|
||||
Space = 1, // 1pt spacing between text and line
|
||||
Color = "000000"
|
||||
}),
|
||||
new Justification { Val = JustificationValues.Center }),
|
||||
new Run(
|
||||
new RunProperties(
|
||||
new Bold(),
|
||||
new FontSize { Val = "20" }), // 10pt
|
||||
new Text("Document Header")));
|
||||
|
||||
headerPart.Header = new Header(paragraph);
|
||||
headerPart.Header.Save();
|
||||
|
||||
var headerRefId = mainPart.GetIdOfPart(headerPart);
|
||||
sectPr.Append(new HeaderReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
Id = headerRefId
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 11. ChangeHeaderPerSection — different headers per section
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// Creates a document with multiple sections, each having its own header.
|
||||
///
|
||||
/// In OOXML, sections are delimited by SectionProperties:
|
||||
/// - Inner sections: sectPr inside a Paragraph's ParagraphProperties (section break)
|
||||
/// - Last section: sectPr as direct child of Body
|
||||
///
|
||||
/// Each sectPr can reference different HeaderPart/FooterPart via its own
|
||||
/// HeaderReference/FooterReference elements.
|
||||
///
|
||||
/// XML structure for multi-section document:
|
||||
/// <w:body>
|
||||
/// <!-- Section 1 content -->
|
||||
/// <w:p><w:r><w:t>Section 1 content</w:t></w:r></w:p>
|
||||
/// <w:p>
|
||||
/// <w:pPr>
|
||||
/// <w:sectPr> <!-- Section 1 break -->
|
||||
/// <w:headerReference w:type="default" r:id="rId_hdr1"/>
|
||||
/// <w:type w:val="nextPage"/>
|
||||
/// </w:sectPr>
|
||||
/// </w:pPr>
|
||||
/// </w:p>
|
||||
///
|
||||
/// <!-- Section 2 content -->
|
||||
/// <w:p><w:r><w:t>Section 2 content</w:t></w:r></w:p>
|
||||
///
|
||||
/// <!-- Final section properties (last child of body) -->
|
||||
/// <w:sectPr>
|
||||
/// <w:headerReference w:type="default" r:id="rId_hdr2"/>
|
||||
/// </w:sectPr>
|
||||
/// </w:body>
|
||||
///
|
||||
/// GOTCHA: A section break sectPr is placed inside a paragraph's ParagraphProperties.
|
||||
/// The paragraph that contains the sectPr is the LAST paragraph of that section.
|
||||
///
|
||||
/// GOTCHA: If a section does not have its own HeaderReference, it inherits
|
||||
/// the header from the previous section. To have NO header in a section,
|
||||
/// you must explicitly link to an empty HeaderPart.
|
||||
/// </summary>
|
||||
public static void ChangeHeaderPerSection(MainDocumentPart mainPart, Body body)
|
||||
{
|
||||
// --- Create two different header parts ---
|
||||
|
||||
// Header for Section 1
|
||||
var header1Part = mainPart.AddNewPart<HeaderPart>();
|
||||
header1Part.Header = new Header(
|
||||
new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Left }),
|
||||
new Run(new Text("Section 1 — Introduction"))));
|
||||
header1Part.Header.Save();
|
||||
|
||||
// Header for Section 2
|
||||
var header2Part = mainPart.AddNewPart<HeaderPart>();
|
||||
header2Part.Header = new Header(
|
||||
new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Left }),
|
||||
new Run(new Text("Section 2 — Analysis"))));
|
||||
header2Part.Header.Save();
|
||||
|
||||
// --- Section 1 content ---
|
||||
body.Append(new Paragraph(
|
||||
new Run(new Text("This is content in Section 1."))));
|
||||
body.Append(new Paragraph(
|
||||
new Run(new Text("More Section 1 content..."))));
|
||||
|
||||
// --- Section 1 break: sectPr inside a paragraph's pPr ---
|
||||
// This paragraph is the LAST paragraph of Section 1.
|
||||
var sect1Pr = new SectionProperties(
|
||||
new HeaderReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
Id = mainPart.GetIdOfPart(header1Part)
|
||||
},
|
||||
// Section break type: start next section on a new page
|
||||
new SectionType { Val = SectionMarkValues.NextPage });
|
||||
|
||||
// Page size and margins for section 1 (required for valid sectPr)
|
||||
sect1Pr.Append(new DocumentFormat.OpenXml.Wordprocessing.PageSize
|
||||
{
|
||||
Width = (UInt32Value)12240U, // Letter width: 8.5" = 12240 DXA
|
||||
Height = (UInt32Value)15840U // Letter height: 11" = 15840 DXA
|
||||
});
|
||||
sect1Pr.Append(new PageMargin
|
||||
{
|
||||
Top = 1440,
|
||||
Bottom = 1440,
|
||||
Left = (UInt32Value)1440U,
|
||||
Right = (UInt32Value)1440U
|
||||
});
|
||||
|
||||
// Wrap the sectPr in a paragraph's ParagraphProperties
|
||||
var sectionBreakPara = new Paragraph(
|
||||
new ParagraphProperties(sect1Pr));
|
||||
body.Append(sectionBreakPara);
|
||||
|
||||
// --- Section 2 content ---
|
||||
body.Append(new Paragraph(
|
||||
new Run(new Text("This is content in Section 2."))));
|
||||
body.Append(new Paragraph(
|
||||
new Run(new Text("More Section 2 content..."))));
|
||||
|
||||
// --- Final section: sectPr as last child of Body ---
|
||||
// This is the sectPr for the LAST section of the document.
|
||||
var finalSectPr = new SectionProperties(
|
||||
new HeaderReference
|
||||
{
|
||||
Type = HeaderFooterValues.Default,
|
||||
Id = mainPart.GetIdOfPart(header2Part)
|
||||
});
|
||||
finalSectPr.Append(new DocumentFormat.OpenXml.Wordprocessing.PageSize
|
||||
{
|
||||
Width = (UInt32Value)12240U,
|
||||
Height = (UInt32Value)15840U
|
||||
});
|
||||
finalSectPr.Append(new PageMargin
|
||||
{
|
||||
Top = 1440,
|
||||
Bottom = 1440,
|
||||
Left = (UInt32Value)1440U,
|
||||
Right = (UInt32Value)1440U
|
||||
});
|
||||
body.Append(finalSectPr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,917 @@
|
||||
// ============================================================================
|
||||
// ImageSamples.cs — Comprehensive OpenXML image handling reference
|
||||
// ============================================================================
|
||||
// EMU (English Metric Unit) is the universal measurement in DrawingML:
|
||||
// 1 inch = 914400 EMU
|
||||
// 1 cm = 360000 EMU
|
||||
// 1 px@96dpi = 9525 EMU (914400 / 96 = 9525)
|
||||
//
|
||||
// Image architecture in OpenXML:
|
||||
// Paragraph → Run → Drawing → DW.Inline (or DW.Anchor)
|
||||
// → A.Graphic → A.GraphicData → PIC.Picture
|
||||
// → PIC.BlipFill → A.Blip (references the image part via r:embed)
|
||||
// → PIC.ShapeProperties → A.Transform2D → A.Extents (cx, cy)
|
||||
//
|
||||
// CRITICAL RULES:
|
||||
// 1. Extent.Cx/Cy on DW.Inline/DW.Anchor MUST match A.Extents.Cx/Cy
|
||||
// on PIC.ShapeProperties. Mismatch causes rendering issues.
|
||||
// 2. Each Drawing element needs a unique DocProperties.Id within the document.
|
||||
// 3. ImagePart must be added to the PART that references it:
|
||||
// - MainDocumentPart for images in body
|
||||
// - HeaderPart for images in headers
|
||||
// - FooterPart for images in footers
|
||||
// 4. Blip.Embed contains the relationship ID (rId) linking to the ImagePart.
|
||||
// ============================================================================
|
||||
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
|
||||
using A = DocumentFormat.OpenXml.Drawing;
|
||||
using DW = DocumentFormat.OpenXml.Drawing.Wordprocessing;
|
||||
using PIC = DocumentFormat.OpenXml.Drawing.Pictures;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Samples;
|
||||
|
||||
/// <summary>
|
||||
/// Reference implementations for every common image operation in OpenXML.
|
||||
/// All methods produce valid, Word-renderable markup.
|
||||
/// </summary>
|
||||
public static class ImageSamples
|
||||
{
|
||||
// ── Constants ──────────────────────────────────────────────────────
|
||||
private const long EmuPerInch = 914400L;
|
||||
private const long EmuPerCm = 360000L;
|
||||
private const long EmuPerPixel96Dpi = 9525L; // 914400 / 96
|
||||
|
||||
// GraphicData URI that tells Word "this is a picture"
|
||||
private const string PicGraphicDataUri = "http://schemas.openxmlformats.org/drawingml/2006/picture";
|
||||
|
||||
// ── 1. Inline Image (most common) ──────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts an inline image into the body. Inline images flow with text
|
||||
/// and do not float. This is the most common image insertion pattern.
|
||||
/// </summary>
|
||||
/// <param name="mainPart">The MainDocumentPart to add the image relationship to.</param>
|
||||
/// <param name="body">The Body element to append the paragraph to.</param>
|
||||
/// <param name="imagePath">Filesystem path to the image file (png, jpg, etc.).</param>
|
||||
/// <param name="widthPx">Desired display width in pixels (at 96 dpi).</param>
|
||||
/// <param name="heightPx">Desired display height in pixels (at 96 dpi).</param>
|
||||
public static void InsertInlineImage(
|
||||
MainDocumentPart mainPart, Body body,
|
||||
string imagePath, int widthPx, int heightPx)
|
||||
{
|
||||
// Step 1: Add the image file as a part. The ImagePartType must match
|
||||
// the actual file format. AddImagePart returns the ImagePart; we then
|
||||
// feed data into it.
|
||||
var imageType = GetImagePartType(imagePath);
|
||||
ImagePart imagePart = mainPart.AddImagePart(imageType);
|
||||
|
||||
using (FileStream stream = new FileStream(imagePath, FileMode.Open))
|
||||
{
|
||||
imagePart.FeedData(stream);
|
||||
}
|
||||
|
||||
// Step 2: Get the relationship ID that links the Blip to this ImagePart.
|
||||
string relId = mainPart.GetIdOfPart(imagePart);
|
||||
|
||||
// Step 3: Convert pixel dimensions to EMU.
|
||||
// Formula: pixels * 9525 = EMU (at 96 dpi, which is Word's assumption)
|
||||
long cx = widthPx * EmuPerPixel96Dpi;
|
||||
long cy = heightPx * EmuPerPixel96Dpi;
|
||||
|
||||
// Step 4: Build the Drawing element using the reusable helper.
|
||||
// docPropId must be unique across the entire document.
|
||||
Drawing drawing = BuildDrawingElement(
|
||||
relId, cx, cy,
|
||||
docPropId: 1U,
|
||||
name: "Image1",
|
||||
description: null);
|
||||
|
||||
// Step 5: Wrap in Paragraph → Run → Drawing
|
||||
Paragraph para = new Paragraph(
|
||||
new Run(drawing));
|
||||
|
||||
body.AppendChild(para);
|
||||
}
|
||||
|
||||
// ── 2. Floating Image (Anchor) ─────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts a floating image with absolute positioning using DW.Anchor.
|
||||
/// Floating images are positioned relative to a reference point (page,
|
||||
/// column, paragraph, etc.) and text wraps around them.
|
||||
/// </summary>
|
||||
public static void InsertFloatingImage(
|
||||
MainDocumentPart mainPart, Body body, string imagePath)
|
||||
{
|
||||
ImagePart imagePart = mainPart.AddImagePart(GetImagePartType(imagePath));
|
||||
using (FileStream stream = new FileStream(imagePath, FileMode.Open))
|
||||
{
|
||||
imagePart.FeedData(stream);
|
||||
}
|
||||
string relId = mainPart.GetIdOfPart(imagePart);
|
||||
|
||||
long cx = (long)(3.0 * EmuPerInch); // 3 inches wide
|
||||
long cy = (long)(2.0 * EmuPerInch); // 2 inches tall
|
||||
|
||||
// DW.Anchor is used instead of DW.Inline for floating images.
|
||||
// Key differences from Inline:
|
||||
// - Has positioning (SimplePos, HorizontalPosition, VerticalPosition)
|
||||
// - Has wrapping mode (WrapSquare, WrapTight, WrapNone, etc.)
|
||||
// - Has BehindDoc and LayoutInCell flags
|
||||
DW.Anchor anchor = new DW.Anchor(
|
||||
// SimplePosition: when SimplePos=true, uses SimplePosition x/y directly.
|
||||
// Normally false; we use HorizontalPosition/VerticalPosition instead.
|
||||
new DW.SimplePosition { X = 0L, Y = 0L },
|
||||
|
||||
// HorizontalPosition: where the image sits horizontally.
|
||||
// RelativeFrom can be: Column, Page, Margin, Character, LeftMargin, etc.
|
||||
new DW.HorizontalPosition(
|
||||
new DW.PositionOffset("914400") // 1 inch from reference
|
||||
)
|
||||
{ RelativeFrom = DW.HorizontalRelativePositionValues.Column },
|
||||
|
||||
// VerticalPosition: where the image sits vertically.
|
||||
new DW.VerticalPosition(
|
||||
new DW.PositionOffset("457200") // 0.5 inch from reference
|
||||
)
|
||||
{ RelativeFrom = DW.VerticalRelativePositionValues.Paragraph },
|
||||
|
||||
// Extent: overall size of the drawing object
|
||||
new DW.Extent { Cx = cx, Cy = cy },
|
||||
|
||||
// EffectExtent: extra space for shadows, glow, etc. (0 if none)
|
||||
new DW.EffectExtent
|
||||
{
|
||||
LeftEdge = 0L,
|
||||
TopEdge = 0L,
|
||||
RightEdge = 0L,
|
||||
BottomEdge = 0L
|
||||
},
|
||||
|
||||
// WrapSquare: text wraps in a square around the image bounding box.
|
||||
new DW.WrapSquare { WrapText = DW.WrapTextValues.BothSides },
|
||||
|
||||
// DocProperties: unique ID + name for the drawing object
|
||||
new DW.DocProperties { Id = 2U, Name = "FloatingImage1" },
|
||||
|
||||
// Non-visual graphic frame properties (required but usually empty)
|
||||
new DW.NonVisualGraphicFrameDrawingProperties(
|
||||
new A.GraphicFrameLocks { NoChangeAspect = true }),
|
||||
|
||||
// The actual graphic content
|
||||
new A.Graphic(
|
||||
new A.GraphicData(
|
||||
new PIC.Picture(
|
||||
new PIC.NonVisualPictureProperties(
|
||||
new PIC.NonVisualDrawingProperties
|
||||
{
|
||||
Id = 0U,
|
||||
Name = "FloatingImage1.png"
|
||||
},
|
||||
new PIC.NonVisualPictureDrawingProperties()),
|
||||
new PIC.BlipFill(
|
||||
new A.Blip { Embed = relId },
|
||||
new A.Stretch(new A.FillRectangle())),
|
||||
new PIC.ShapeProperties(
|
||||
new A.Transform2D(
|
||||
new A.Offset { X = 0L, Y = 0L },
|
||||
// CRITICAL: These cx/cy MUST match the Extent above
|
||||
new A.Extents { Cx = cx, Cy = cy }),
|
||||
new A.PresetGeometry(
|
||||
new A.AdjustValueList())
|
||||
{ Preset = A.ShapeTypeValues.Rectangle }))
|
||||
)
|
||||
{ Uri = PicGraphicDataUri })
|
||||
)
|
||||
{
|
||||
// Anchor attributes
|
||||
DistanceFromTop = 0U,
|
||||
DistanceFromBottom = 0U,
|
||||
DistanceFromLeft = 114300U, // ~0.125 inch gap between text and image
|
||||
DistanceFromRight = 114300U,
|
||||
SimplePos = false,
|
||||
RelativeHeight = 251658240U, // z-order; higher = in front
|
||||
BehindDoc = false, // true = behind text (like a watermark)
|
||||
Locked = false,
|
||||
LayoutInCell = true,
|
||||
AllowOverlap = true
|
||||
};
|
||||
|
||||
Paragraph para = new Paragraph(new Run(new Drawing(anchor)));
|
||||
body.AppendChild(para);
|
||||
}
|
||||
|
||||
// ── 3. Image with Various Text Wrapping ────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrates the four main text wrapping modes for floating images.
|
||||
/// Each wrapping mode controls how body text flows around the image.
|
||||
/// </summary>
|
||||
public static void InsertImageWithTextWrapping(
|
||||
MainDocumentPart mainPart, Body body, string imagePath)
|
||||
{
|
||||
// All wrapping modes require DW.Anchor (not DW.Inline).
|
||||
// The wrapping element is a direct child of the Anchor element.
|
||||
|
||||
ImagePart imagePart = mainPart.AddImagePart(GetImagePartType(imagePath));
|
||||
using (FileStream stream = new FileStream(imagePath, FileMode.Open))
|
||||
{
|
||||
imagePart.FeedData(stream);
|
||||
}
|
||||
string relId = mainPart.GetIdOfPart(imagePart);
|
||||
|
||||
long cx = (long)(2.5 * EmuPerInch);
|
||||
long cy = (long)(2.0 * EmuPerInch);
|
||||
|
||||
// ── WrapSquare ──
|
||||
// Text wraps in a rectangular bounding box around the image.
|
||||
// WrapText controls which sides text appears on.
|
||||
var wrapSquare = new DW.WrapSquare
|
||||
{
|
||||
WrapText = DW.WrapTextValues.BothSides
|
||||
// Other options: Left, Right, Largest
|
||||
};
|
||||
|
||||
// ── WrapTight ──
|
||||
// Text wraps tightly around the actual contour of the image.
|
||||
// Uses a WrapPolygon to define the outline; Word can auto-generate this.
|
||||
// The coordinates are in EMU relative to the image's top-left.
|
||||
var wrapTight = new DW.WrapTight(
|
||||
new DW.WrapPolygon(
|
||||
new DW.StartPoint { X = 0L, Y = 0L },
|
||||
new DW.LineTo { X = 0L, Y = 21600L },
|
||||
new DW.LineTo { X = 21600L, Y = 21600L },
|
||||
new DW.LineTo { X = 21600L, Y = 0L },
|
||||
new DW.LineTo { X = 0L, Y = 0L }
|
||||
)
|
||||
{ Edited = false }
|
||||
)
|
||||
{
|
||||
WrapText = DW.WrapTextValues.BothSides
|
||||
};
|
||||
|
||||
// ── WrapTopAndBottom ──
|
||||
// No text appears beside the image. Text only above and below.
|
||||
// This effectively makes the image act as a block-level element
|
||||
// but still floating (not inline).
|
||||
var wrapTopAndBottom = new DW.WrapTopBottom
|
||||
{
|
||||
DistanceFromTop = 0U,
|
||||
DistanceFromBottom = 0U
|
||||
};
|
||||
|
||||
// ── WrapNone ──
|
||||
// No text wrapping at all. Image floats over or behind text.
|
||||
// Combined with BehindDoc=true, this creates a watermark effect.
|
||||
var wrapNone = new DW.WrapNone();
|
||||
|
||||
// Example: build anchor with WrapSquare (swap in any wrapping element above)
|
||||
DW.Anchor anchor = BuildAnchorElement(
|
||||
relId, cx, cy,
|
||||
docPropId: 3U,
|
||||
name: "WrappedImage",
|
||||
wrapElement: wrapSquare,
|
||||
behindDoc: false);
|
||||
|
||||
body.AppendChild(new Paragraph(new Run(new Drawing(anchor))));
|
||||
}
|
||||
|
||||
// ── 4. Image with Border ───────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts an image with a visible outline/border. The border is applied
|
||||
/// via A.Outline on the PIC.ShapeProperties element.
|
||||
/// </summary>
|
||||
public static void InsertImageWithBorder(
|
||||
MainDocumentPart mainPart, Body body, string imagePath)
|
||||
{
|
||||
ImagePart imagePart = mainPart.AddImagePart(GetImagePartType(imagePath));
|
||||
using (FileStream stream = new FileStream(imagePath, FileMode.Open))
|
||||
{
|
||||
imagePart.FeedData(stream);
|
||||
}
|
||||
string relId = mainPart.GetIdOfPart(imagePart);
|
||||
|
||||
long cx = (long)(3.0 * EmuPerInch);
|
||||
long cy = (long)(2.0 * EmuPerInch);
|
||||
|
||||
// Build PIC.ShapeProperties with an Outline element for the border.
|
||||
// Outline width is in EMU. 1pt = 12700 EMU.
|
||||
var shapeProperties = new PIC.ShapeProperties(
|
||||
new A.Transform2D(
|
||||
new A.Offset { X = 0L, Y = 0L },
|
||||
new A.Extents { Cx = cx, Cy = cy }),
|
||||
new A.PresetGeometry(
|
||||
new A.AdjustValueList())
|
||||
{ Preset = A.ShapeTypeValues.Rectangle },
|
||||
// The Outline element defines the border
|
||||
new A.Outline(
|
||||
// SolidFill sets the border color
|
||||
new A.SolidFill(
|
||||
new A.RgbColorModelHex { Val = "2F5496" }), // Dark blue
|
||||
// PresetDash sets the line style (solid, dash, dot, etc.)
|
||||
new A.PresetDash { Val = A.PresetLineDashValues.Solid }
|
||||
)
|
||||
{
|
||||
Width = 25400, // 2pt border (12700 EMU per pt)
|
||||
CompoundLineType = A.CompoundLineValues.Single
|
||||
}
|
||||
);
|
||||
|
||||
var picture = new PIC.Picture(
|
||||
new PIC.NonVisualPictureProperties(
|
||||
new PIC.NonVisualDrawingProperties { Id = 0U, Name = "BorderedImage.png" },
|
||||
new PIC.NonVisualPictureDrawingProperties()),
|
||||
new PIC.BlipFill(
|
||||
new A.Blip { Embed = relId },
|
||||
new A.Stretch(new A.FillRectangle())),
|
||||
shapeProperties);
|
||||
|
||||
var drawing = new Drawing(
|
||||
new DW.Inline(
|
||||
new DW.Extent { Cx = cx, Cy = cy },
|
||||
new DW.EffectExtent
|
||||
{
|
||||
// Must account for border width in effect extent so it is not clipped
|
||||
LeftEdge = 25400L,
|
||||
TopEdge = 25400L,
|
||||
RightEdge = 25400L,
|
||||
BottomEdge = 25400L
|
||||
},
|
||||
new DW.DocProperties { Id = 4U, Name = "BorderedImage" },
|
||||
new DW.NonVisualGraphicFrameDrawingProperties(
|
||||
new A.GraphicFrameLocks { NoChangeAspect = true }),
|
||||
new A.Graphic(
|
||||
new A.GraphicData(picture)
|
||||
{ Uri = PicGraphicDataUri })
|
||||
)
|
||||
{
|
||||
DistanceFromTop = 0U,
|
||||
DistanceFromBottom = 0U,
|
||||
DistanceFromLeft = 0U,
|
||||
DistanceFromRight = 0U
|
||||
});
|
||||
|
||||
body.AppendChild(new Paragraph(new Run(drawing)));
|
||||
}
|
||||
|
||||
// ── 5. Image with Alt Text ─────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts an image with alt text for accessibility. The alt text is set
|
||||
/// on the DocProperties.Description attribute. Screen readers use this.
|
||||
/// Word also shows it in the "Alt Text" pane.
|
||||
/// </summary>
|
||||
public static void InsertImageWithAltText(
|
||||
MainDocumentPart mainPart, Body body, string imagePath)
|
||||
{
|
||||
ImagePart imagePart = mainPart.AddImagePart(GetImagePartType(imagePath));
|
||||
using (FileStream stream = new FileStream(imagePath, FileMode.Open))
|
||||
{
|
||||
imagePart.FeedData(stream);
|
||||
}
|
||||
string relId = mainPart.GetIdOfPart(imagePart);
|
||||
|
||||
long cx = (long)(3.0 * EmuPerInch);
|
||||
long cy = (long)(2.0 * EmuPerInch);
|
||||
|
||||
// DocProperties.Description is the standard alt text field.
|
||||
// DocProperties.Title is an optional short title shown in some UIs.
|
||||
Drawing drawing = BuildDrawingElement(
|
||||
relId, cx, cy,
|
||||
docPropId: 5U,
|
||||
name: "AccessibleImage",
|
||||
description: "A chart showing quarterly revenue growth from Q1 to Q4 2025");
|
||||
|
||||
body.AppendChild(new Paragraph(new Run(drawing)));
|
||||
}
|
||||
|
||||
// ── 6. Image in Header ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts an image into a header part. The image relationship MUST be
|
||||
/// added to the HeaderPart, NOT the MainDocumentPart. If you add it
|
||||
/// to MainDocumentPart, Word will show a broken image in the header
|
||||
/// because relationship IDs are scoped to their containing part.
|
||||
/// </summary>
|
||||
public static void InsertImageInHeader(HeaderPart headerPart, string imagePath)
|
||||
{
|
||||
// CRITICAL: AddImagePart to headerPart, not mainDocumentPart!
|
||||
// Each OpenXML part has its own relationship namespace.
|
||||
// An rId in the header must point to a relationship in the header's .rels file.
|
||||
ImagePart imagePart = headerPart.AddImagePart(GetImagePartType(imagePath));
|
||||
using (FileStream stream = new FileStream(imagePath, FileMode.Open))
|
||||
{
|
||||
imagePart.FeedData(stream);
|
||||
}
|
||||
|
||||
// GetIdOfPart must also be called on headerPart
|
||||
string relId = headerPart.GetIdOfPart(imagePart);
|
||||
|
||||
long cx = (long)(1.5 * EmuPerInch); // Company logo, typically small
|
||||
long cy = (long)(0.5 * EmuPerInch);
|
||||
|
||||
Drawing drawing = BuildDrawingElement(
|
||||
relId, cx, cy,
|
||||
docPropId: 6U,
|
||||
name: "HeaderLogo",
|
||||
description: "Company logo");
|
||||
|
||||
// Headers use the Header element with Paragraph children (same as Body)
|
||||
Header header = headerPart.Header;
|
||||
Paragraph para = new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new Justification { Val = JustificationValues.Center }),
|
||||
new Run(drawing));
|
||||
|
||||
header.AppendChild(para);
|
||||
}
|
||||
|
||||
// ── 7. Image in Table Cell ─────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts an image into a table cell, sized to fit. Table cells constrain
|
||||
/// content width, so we calculate appropriate dimensions to avoid overflow.
|
||||
/// The image part is still added to MainDocumentPart (the cell is in the body).
|
||||
/// </summary>
|
||||
/// <param name="mainPart">MainDocumentPart (owns the relationship).</param>
|
||||
/// <param name="cell">The TableCell to insert the image into.</param>
|
||||
/// <param name="imagePath">Path to the image file.</param>
|
||||
public static void InsertImageInTableCell(
|
||||
MainDocumentPart mainPart, TableCell cell, string imagePath)
|
||||
{
|
||||
ImagePart imagePart = mainPart.AddImagePart(GetImagePartType(imagePath));
|
||||
using (FileStream stream = new FileStream(imagePath, FileMode.Open))
|
||||
{
|
||||
imagePart.FeedData(stream);
|
||||
}
|
||||
string relId = mainPart.GetIdOfPart(imagePart);
|
||||
|
||||
// Determine cell width from TableCellWidth if available.
|
||||
// TableCellWidth.Width is in DXA (twentieths of a point).
|
||||
// If not set, use a reasonable default (e.g., 2 inches).
|
||||
long maxWidthEmu = (long)(2.0 * EmuPerInch); // default
|
||||
|
||||
TableCellProperties? tcPr = cell.GetFirstChild<TableCellProperties>();
|
||||
TableCellWidth? tcWidth = tcPr?.GetFirstChild<TableCellWidth>();
|
||||
if (tcWidth?.Width is not null && tcWidth.Type?.Value == TableWidthUnitValues.Dxa)
|
||||
{
|
||||
// Convert DXA to EMU: 1 DXA = 1/20 pt = 1/1440 inch = 914400/1440 EMU
|
||||
int dxa = int.Parse(tcWidth.Width);
|
||||
maxWidthEmu = (long)(dxa * (EmuPerInch / 1440.0));
|
||||
}
|
||||
|
||||
// Calculate image dimensions to fit within the cell width
|
||||
(long cx, long cy) = CalculateImageDimensions(imagePath, maxWidthEmu / (double)EmuPerInch);
|
||||
|
||||
Drawing drawing = BuildDrawingElement(
|
||||
relId, cx, cy,
|
||||
docPropId: 7U,
|
||||
name: "CellImage",
|
||||
description: null);
|
||||
|
||||
// A TableCell MUST contain at least one Paragraph.
|
||||
// We add the image inside that paragraph.
|
||||
Paragraph para = cell.GetFirstChild<Paragraph>() ?? cell.AppendChild(new Paragraph());
|
||||
para.AppendChild(new Run(drawing));
|
||||
}
|
||||
|
||||
// ── 8. Replace Existing Image ──────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Replaces an existing image by updating the ImagePart data behind a
|
||||
/// known relationship ID. The Blip.Embed attribute (rId) stays the same;
|
||||
/// only the binary content changes. This avoids needing to rebuild the
|
||||
/// entire Drawing XML tree.
|
||||
/// </summary>
|
||||
/// <param name="mainPart">The MainDocumentPart containing the image relationship.</param>
|
||||
/// <param name="oldRelId">The existing relationship ID (e.g., "rId5") of the image to replace.</param>
|
||||
/// <param name="newImagePath">Path to the replacement image file.</param>
|
||||
public static void ReplaceExistingImage(
|
||||
MainDocumentPart mainPart, string oldRelId, string newImagePath)
|
||||
{
|
||||
// Look up the existing ImagePart by its relationship ID
|
||||
OpenXmlPart part = mainPart.GetPartById(oldRelId);
|
||||
if (part is not ImagePart imagePart)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Relationship {oldRelId} does not point to an ImagePart.");
|
||||
}
|
||||
|
||||
// Feed new image data into the existing part.
|
||||
// This replaces the binary content while keeping the same rId.
|
||||
using (FileStream stream = new FileStream(newImagePath, FileMode.Open))
|
||||
{
|
||||
imagePart.FeedData(stream);
|
||||
}
|
||||
|
||||
// NOTE: If the new image has different dimensions, you should also
|
||||
// update the Extent.Cx/Cy and A.Extents.Cx/Cy in the Drawing element.
|
||||
// Find all Blip elements referencing this relId:
|
||||
//
|
||||
// var blips = mainPart.Document.Descendants<A.Blip>()
|
||||
// .Where(b => b.Embed == oldRelId);
|
||||
// foreach (var blip in blips)
|
||||
// {
|
||||
// // Navigate up to find the Extent and A.Extents to update dimensions
|
||||
// }
|
||||
}
|
||||
|
||||
// ── 9. SVG with PNG Fallback ───────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts an SVG image with a PNG fallback for compatibility.
|
||||
/// Word 2019+ supports SVG natively; older versions show the PNG.
|
||||
/// The SVG is referenced via an extension element (SvgBlip) inside the Blip,
|
||||
/// while the Blip.Embed itself points to the PNG fallback.
|
||||
/// </summary>
|
||||
public static void InsertSvgWithPngFallback(
|
||||
MainDocumentPart mainPart, Body body,
|
||||
string svgPath, string pngFallbackPath)
|
||||
{
|
||||
// Add PNG fallback as the primary image part
|
||||
ImagePart pngPart = mainPart.AddImagePart(ImagePartType.Png);
|
||||
using (FileStream pngStream = new FileStream(pngFallbackPath, FileMode.Open))
|
||||
{
|
||||
pngPart.FeedData(pngStream);
|
||||
}
|
||||
string pngRelId = mainPart.GetIdOfPart(pngPart);
|
||||
|
||||
// Add SVG as a separate image part
|
||||
ImagePart svgPart = mainPart.AddImagePart(ImagePartType.Svg);
|
||||
using (FileStream svgStream = new FileStream(svgPath, FileMode.Open))
|
||||
{
|
||||
svgPart.FeedData(svgStream);
|
||||
}
|
||||
string svgRelId = mainPart.GetIdOfPart(svgPart);
|
||||
|
||||
long cx = (long)(3.0 * EmuPerInch);
|
||||
long cy = (long)(3.0 * EmuPerInch);
|
||||
|
||||
// The Blip.Embed points to the PNG fallback.
|
||||
// The SVG is added as an extension element (asvg:svgBlip) inside the Blip.
|
||||
// Namespace: http://schemas.microsoft.com/office/drawing/2016/SVG/main
|
||||
var blip = new A.Blip { Embed = pngRelId };
|
||||
|
||||
// Add SVG extension to the Blip using BlipExtensionList
|
||||
var svgExtension = new A.BlipExtensionList(
|
||||
new A.BlipExtension(
|
||||
// The SVG blip element references the SVG image part
|
||||
new OpenXmlUnknownElement(
|
||||
"asvg", "svgBlip",
|
||||
"http://schemas.microsoft.com/office/drawing/2016/SVG/main")
|
||||
// NOTE: In production, set the r:embed attribute on this element
|
||||
// to svgRelId. OpenXmlUnknownElement requires manual attribute setting.
|
||||
)
|
||||
{ Uri = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}" }
|
||||
);
|
||||
blip.Append(svgExtension);
|
||||
|
||||
var picture = new PIC.Picture(
|
||||
new PIC.NonVisualPictureProperties(
|
||||
new PIC.NonVisualDrawingProperties { Id = 0U, Name = "SvgImage.svg" },
|
||||
new PIC.NonVisualPictureDrawingProperties()),
|
||||
new PIC.BlipFill(
|
||||
blip,
|
||||
new A.Stretch(new A.FillRectangle())),
|
||||
new PIC.ShapeProperties(
|
||||
new A.Transform2D(
|
||||
new A.Offset { X = 0L, Y = 0L },
|
||||
new A.Extents { Cx = cx, Cy = cy }),
|
||||
new A.PresetGeometry(new A.AdjustValueList())
|
||||
{ Preset = A.ShapeTypeValues.Rectangle }));
|
||||
|
||||
var drawing = new Drawing(
|
||||
new DW.Inline(
|
||||
new DW.Extent { Cx = cx, Cy = cy },
|
||||
new DW.EffectExtent
|
||||
{
|
||||
LeftEdge = 0L, TopEdge = 0L,
|
||||
RightEdge = 0L, BottomEdge = 0L
|
||||
},
|
||||
new DW.DocProperties { Id = 9U, Name = "SvgImage" },
|
||||
new DW.NonVisualGraphicFrameDrawingProperties(
|
||||
new A.GraphicFrameLocks { NoChangeAspect = true }),
|
||||
new A.Graphic(
|
||||
new A.GraphicData(picture)
|
||||
{ Uri = PicGraphicDataUri })
|
||||
)
|
||||
{
|
||||
DistanceFromTop = 0U,
|
||||
DistanceFromBottom = 0U,
|
||||
DistanceFromLeft = 0U,
|
||||
DistanceFromRight = 0U
|
||||
});
|
||||
|
||||
body.AppendChild(new Paragraph(new Run(drawing)));
|
||||
}
|
||||
|
||||
// ── 10. Calculate Image Dimensions ─────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Reads the actual pixel dimensions of an image file (PNG or JPEG) and
|
||||
/// calculates EMU values that fit within a maximum width while maintaining
|
||||
/// the original aspect ratio. Uses raw byte reading to avoid a dependency
|
||||
/// on System.Drawing (which is Windows-only on modern .NET).
|
||||
/// </summary>
|
||||
/// <param name="imagePath">Path to a PNG or JPEG image file.</param>
|
||||
/// <param name="maxWidthInches">Maximum allowed width in inches.</param>
|
||||
/// <returns>Tuple of (cx, cy) in EMU, scaled to fit maxWidthInches.</returns>
|
||||
/// <remarks>
|
||||
/// For production use, consider SkiaSharp or SixLabors.ImageSharp for
|
||||
/// cross-platform image metadata reading with broader format support.
|
||||
/// This implementation handles PNG and JPEG only.
|
||||
/// </remarks>
|
||||
public static (long cx, long cy) CalculateImageDimensions(
|
||||
string imagePath, double maxWidthInches)
|
||||
{
|
||||
// Read pixel dimensions from the image file header.
|
||||
// We parse PNG IHDR or JPEG SOF0 markers directly to avoid
|
||||
// pulling in System.Drawing.Common (Windows-only on .NET 6+).
|
||||
(int widthPx, int heightPx, double dpiX, double dpiY) = ReadImageMetadata(imagePath);
|
||||
|
||||
// Calculate actual size in inches based on pixel count and DPI
|
||||
double widthInches = widthPx / dpiX;
|
||||
double heightInches = heightPx / dpiY;
|
||||
|
||||
// Scale down if wider than maxWidthInches, preserving aspect ratio
|
||||
if (widthInches > maxWidthInches)
|
||||
{
|
||||
double scale = maxWidthInches / widthInches;
|
||||
widthInches = maxWidthInches;
|
||||
heightInches *= scale;
|
||||
}
|
||||
|
||||
long cx = (long)(widthInches * EmuPerInch);
|
||||
long cy = (long)(heightInches * EmuPerInch);
|
||||
|
||||
return (cx, cy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads width, height, and DPI from a PNG or JPEG file header.
|
||||
/// Returns 96 DPI as default if DPI metadata is not found.
|
||||
/// </summary>
|
||||
private static (int widthPx, int heightPx, double dpiX, double dpiY) ReadImageMetadata(
|
||||
string imagePath)
|
||||
{
|
||||
const double DefaultDpi = 96.0;
|
||||
byte[] header = new byte[32];
|
||||
|
||||
using var fs = new FileStream(imagePath, FileMode.Open, FileAccess.Read);
|
||||
int bytesRead = fs.Read(header, 0, header.Length);
|
||||
|
||||
// PNG: starts with 0x89 0x50 0x4E 0x47 (‰PNG)
|
||||
// IHDR chunk is always first; width and height are at bytes 16-23 (big-endian)
|
||||
if (bytesRead >= 24 &&
|
||||
header[0] == 0x89 && header[1] == 0x50 &&
|
||||
header[2] == 0x4E && header[3] == 0x47)
|
||||
{
|
||||
int width = (header[16] << 24) | (header[17] << 16) |
|
||||
(header[18] << 8) | header[19];
|
||||
int height = (header[20] << 24) | (header[21] << 16) |
|
||||
(header[22] << 8) | header[23];
|
||||
// PNG DPI is in the pHYs chunk (not in IHDR); use default for simplicity
|
||||
return (width, height, DefaultDpi, DefaultDpi);
|
||||
}
|
||||
|
||||
// JPEG: starts with 0xFF 0xD8
|
||||
// Scan for SOF0 (0xFF 0xC0) marker to find dimensions
|
||||
if (bytesRead >= 2 && header[0] == 0xFF && header[1] == 0xD8)
|
||||
{
|
||||
fs.Position = 2;
|
||||
while (fs.Position < fs.Length - 1)
|
||||
{
|
||||
int b = fs.ReadByte();
|
||||
if (b != 0xFF) continue;
|
||||
|
||||
int marker = fs.ReadByte();
|
||||
if (marker == -1) break;
|
||||
|
||||
// SOF0 (0xC0) or SOF2 (0xC2, progressive)
|
||||
if (marker == 0xC0 || marker == 0xC2)
|
||||
{
|
||||
byte[] sof = new byte[7];
|
||||
if (fs.Read(sof, 0, 7) == 7)
|
||||
{
|
||||
// SOF structure: length(2) + precision(1) + height(2) + width(2)
|
||||
int height = (sof[3] << 8) | sof[4];
|
||||
int width = (sof[5] << 8) | sof[6];
|
||||
return (width, height, DefaultDpi, DefaultDpi);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip other markers: read 2-byte length and advance
|
||||
if (marker is not (0xD0 or 0xD1 or 0xD2 or 0xD3 or 0xD4 or
|
||||
0xD5 or 0xD6 or 0xD7 or 0xD8 or 0xD9 or 0x01))
|
||||
{
|
||||
byte[] lenBytes = new byte[2];
|
||||
if (fs.Read(lenBytes, 0, 2) < 2) break;
|
||||
int len = (lenBytes[0] << 8) | lenBytes[1];
|
||||
if (len < 2) break;
|
||||
fs.Position += len - 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: cannot determine dimensions; return a reasonable default
|
||||
// Caller should handle this gracefully.
|
||||
return (300, 200, DefaultDpi, DefaultDpi);
|
||||
}
|
||||
|
||||
// ── 11. Reusable Drawing Builder (Inline) ──────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Builds a complete Drawing element for an inline image. This is the
|
||||
/// reusable core that most insertion methods delegate to.
|
||||
/// </summary>
|
||||
/// <param name="relId">Relationship ID pointing to the ImagePart (e.g., "rId4").</param>
|
||||
/// <param name="cx">Image width in EMU. Must be positive.</param>
|
||||
/// <param name="cy">Image height in EMU. Must be positive.</param>
|
||||
/// <param name="docPropId">Unique ID for DocProperties within the document.
|
||||
/// Each Drawing in a document must have a distinct DocProperties.Id.</param>
|
||||
/// <param name="name">Name for DocProperties (shows in Word selection pane).</param>
|
||||
/// <param name="description">Alt text for accessibility. Null if not needed.</param>
|
||||
/// <returns>A fully constructed Drawing element ready to append to a Run.</returns>
|
||||
public static Drawing BuildDrawingElement(
|
||||
string relId, long cx, long cy,
|
||||
uint docPropId, string name, string? description)
|
||||
{
|
||||
// ── Complete element hierarchy ──
|
||||
// Drawing
|
||||
// └─ DW.Inline
|
||||
// ├─ DW.Extent (cx, cy) ← bounding box size
|
||||
// ├─ DW.EffectExtent ← extra space for effects
|
||||
// ├─ DW.DocProperties (id, name, descr) ← identity + alt text
|
||||
// ├─ DW.NonVisualGraphicFrameDrawingProperties
|
||||
// │ └─ A.GraphicFrameLocks ← lock aspect ratio
|
||||
// └─ A.Graphic
|
||||
// └─ A.GraphicData (uri = picture namespace)
|
||||
// └─ PIC.Picture
|
||||
// ├─ PIC.NonVisualPictureProperties
|
||||
// │ ├─ PIC.NonVisualDrawingProperties
|
||||
// │ └─ PIC.NonVisualPictureDrawingProperties
|
||||
// ├─ PIC.BlipFill
|
||||
// │ ├─ A.Blip (embed = relId)
|
||||
// │ └─ A.Stretch → A.FillRectangle
|
||||
// └─ PIC.ShapeProperties
|
||||
// ├─ A.Transform2D
|
||||
// │ ├─ A.Offset (0, 0)
|
||||
// │ └─ A.Extents (cx, cy) ← MUST match DW.Extent!
|
||||
// └─ A.PresetGeometry (rect)
|
||||
|
||||
var docProps = new DW.DocProperties
|
||||
{
|
||||
Id = docPropId,
|
||||
Name = name
|
||||
};
|
||||
if (description is not null)
|
||||
{
|
||||
docProps.Description = description;
|
||||
}
|
||||
|
||||
var picture = new PIC.Picture(
|
||||
new PIC.NonVisualPictureProperties(
|
||||
new PIC.NonVisualDrawingProperties
|
||||
{
|
||||
Id = 0U,
|
||||
Name = name
|
||||
},
|
||||
new PIC.NonVisualPictureDrawingProperties()),
|
||||
new PIC.BlipFill(
|
||||
new A.Blip
|
||||
{
|
||||
Embed = relId,
|
||||
// CompressionState controls image quality vs file size.
|
||||
// Print = high quality, Screen = medium, Email = low, None = original
|
||||
CompressionState = A.BlipCompressionValues.Print
|
||||
},
|
||||
new A.Stretch(new A.FillRectangle())),
|
||||
new PIC.ShapeProperties(
|
||||
new A.Transform2D(
|
||||
new A.Offset { X = 0L, Y = 0L },
|
||||
new A.Extents { Cx = cx, Cy = cy }), // MUST match DW.Extent
|
||||
new A.PresetGeometry(
|
||||
new A.AdjustValueList())
|
||||
{ Preset = A.ShapeTypeValues.Rectangle }));
|
||||
|
||||
var inline = new DW.Inline(
|
||||
new DW.Extent { Cx = cx, Cy = cy }, // MUST match A.Extents
|
||||
new DW.EffectExtent
|
||||
{
|
||||
LeftEdge = 0L,
|
||||
TopEdge = 0L,
|
||||
RightEdge = 0L,
|
||||
BottomEdge = 0L
|
||||
},
|
||||
docProps,
|
||||
new DW.NonVisualGraphicFrameDrawingProperties(
|
||||
new A.GraphicFrameLocks { NoChangeAspect = true }),
|
||||
new A.Graphic(
|
||||
new A.GraphicData(picture)
|
||||
{ Uri = PicGraphicDataUri }))
|
||||
{
|
||||
DistanceFromTop = 0U,
|
||||
DistanceFromBottom = 0U,
|
||||
DistanceFromLeft = 0U,
|
||||
DistanceFromRight = 0U
|
||||
};
|
||||
|
||||
return new Drawing(inline);
|
||||
}
|
||||
|
||||
// ── Private Helpers ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Builds a DW.Anchor element for floating images with configurable wrapping.
|
||||
/// </summary>
|
||||
private static DW.Anchor BuildAnchorElement(
|
||||
string relId, long cx, long cy,
|
||||
uint docPropId, string name,
|
||||
OpenXmlElement wrapElement,
|
||||
bool behindDoc)
|
||||
{
|
||||
return new DW.Anchor(
|
||||
new DW.SimplePosition { X = 0L, Y = 0L },
|
||||
new DW.HorizontalPosition(
|
||||
new DW.PositionOffset("0"))
|
||||
{ RelativeFrom = DW.HorizontalRelativePositionValues.Column },
|
||||
new DW.VerticalPosition(
|
||||
new DW.PositionOffset("0"))
|
||||
{ RelativeFrom = DW.VerticalRelativePositionValues.Paragraph },
|
||||
new DW.Extent { Cx = cx, Cy = cy },
|
||||
new DW.EffectExtent
|
||||
{
|
||||
LeftEdge = 0L,
|
||||
TopEdge = 0L,
|
||||
RightEdge = 0L,
|
||||
BottomEdge = 0L
|
||||
},
|
||||
wrapElement,
|
||||
new DW.DocProperties { Id = docPropId, Name = name },
|
||||
new DW.NonVisualGraphicFrameDrawingProperties(
|
||||
new A.GraphicFrameLocks { NoChangeAspect = true }),
|
||||
new A.Graphic(
|
||||
new A.GraphicData(
|
||||
new PIC.Picture(
|
||||
new PIC.NonVisualPictureProperties(
|
||||
new PIC.NonVisualDrawingProperties
|
||||
{
|
||||
Id = 0U,
|
||||
Name = name
|
||||
},
|
||||
new PIC.NonVisualPictureDrawingProperties()),
|
||||
new PIC.BlipFill(
|
||||
new A.Blip { Embed = relId },
|
||||
new A.Stretch(new A.FillRectangle())),
|
||||
new PIC.ShapeProperties(
|
||||
new A.Transform2D(
|
||||
new A.Offset { X = 0L, Y = 0L },
|
||||
new A.Extents { Cx = cx, Cy = cy }),
|
||||
new A.PresetGeometry(
|
||||
new A.AdjustValueList())
|
||||
{ Preset = A.ShapeTypeValues.Rectangle }))
|
||||
)
|
||||
{ Uri = PicGraphicDataUri })
|
||||
)
|
||||
{
|
||||
DistanceFromTop = 0U,
|
||||
DistanceFromBottom = 0U,
|
||||
DistanceFromLeft = 114300U,
|
||||
DistanceFromRight = 114300U,
|
||||
SimplePos = false,
|
||||
RelativeHeight = 251658240U,
|
||||
BehindDoc = behindDoc,
|
||||
Locked = false,
|
||||
LayoutInCell = true,
|
||||
AllowOverlap = true
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps file extensions to OpenXML PartTypeInfo values via ImagePartType.
|
||||
/// In SDK 3.x, ImagePartType is a static class whose members return PartTypeInfo.
|
||||
/// </summary>
|
||||
private static PartTypeInfo GetImagePartType(string imagePath)
|
||||
{
|
||||
string ext = Path.GetExtension(imagePath).ToLowerInvariant();
|
||||
return ext switch
|
||||
{
|
||||
".png" => ImagePartType.Png,
|
||||
".jpg" or ".jpeg" => ImagePartType.Jpeg,
|
||||
".gif" => ImagePartType.Gif,
|
||||
".bmp" => ImagePartType.Bmp,
|
||||
".tif" or ".tiff" => ImagePartType.Tiff,
|
||||
".svg" => ImagePartType.Svg,
|
||||
".emf" => ImagePartType.Emf,
|
||||
".wmf" => ImagePartType.Wmf,
|
||||
".ico" => ImagePartType.Icon,
|
||||
_ => throw new NotSupportedException(
|
||||
$"Image format '{ext}' is not supported by OpenXML.")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,826 @@
|
||||
// ============================================================================
|
||||
// ListAndNumberingSamples.cs — OpenXML numbering system deep dive
|
||||
// ============================================================================
|
||||
// OpenXML list/numbering architecture (3 layers):
|
||||
//
|
||||
// 1. AbstractNum — defines the numbering FORMAT (bullet chars, number formats,
|
||||
// indentation, fonts). Contains Level elements (0-8) for multi-level lists.
|
||||
//
|
||||
// 2. NumberingInstance (Num) — a concrete "instance" that references an
|
||||
// AbstractNum. Multiple paragraphs share the same NumId to form one list.
|
||||
// LevelOverride on a NumberingInstance can restart numbering.
|
||||
//
|
||||
// 3. NumberingProperties on Paragraph — links a paragraph to a NumberingInstance
|
||||
// via NumId + Level (ilvl). This is what makes a paragraph a list item.
|
||||
//
|
||||
// CRITICAL RULES:
|
||||
// - In the Numbering root element, ALL AbstractNum elements MUST appear
|
||||
// BEFORE any NumberingInstance (Num) elements. Violating this order causes
|
||||
// Word to report corruption.
|
||||
// - LevelText uses %1, %2, %3 etc. as placeholders for the current value
|
||||
// at each level. %1 = level 0's value, %2 = level 1's value, etc.
|
||||
// - NumberingSymbolRunProperties (rPr inside Level) sets the font for the
|
||||
// bullet character or number. Without it, the bullet may render in the
|
||||
// paragraph's font, which can produce wrong glyphs.
|
||||
// - IsLegalNumberingStyle on a Level forces "legal" flat numbering
|
||||
// (e.g., "1.1.1" instead of outline style) regardless of heading level.
|
||||
//
|
||||
// Storage: Numbering definitions live in numbering.xml, accessed via
|
||||
// NumberingDefinitionsPart on the MainDocumentPart.
|
||||
// ============================================================================
|
||||
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
|
||||
using A = DocumentFormat.OpenXml.Drawing;
|
||||
using DW = DocumentFormat.OpenXml.Drawing.Wordprocessing;
|
||||
using PIC = DocumentFormat.OpenXml.Drawing.Pictures;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Samples;
|
||||
|
||||
/// <summary>
|
||||
/// Reference implementations for bullet lists, numbered lists, custom numbering,
|
||||
/// and all related numbering infrastructure in OpenXML.
|
||||
/// </summary>
|
||||
public static class ListAndNumberingSamples
|
||||
{
|
||||
// ── 1. Bullet List (3 levels) ──────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Creates a 3-level bullet list: bullet (•) → circle (○) → square (■).
|
||||
/// Uses Symbol font for standard bullet characters.
|
||||
/// </summary>
|
||||
public static void CreateBulletList(
|
||||
NumberingDefinitionsPart numPart, Body body)
|
||||
{
|
||||
int abstractNumId = 0;
|
||||
int numId = 1;
|
||||
|
||||
// Level 0: solid bullet • (Unicode F0B7 in Symbol font)
|
||||
// Level 1: open circle ○ (Unicode F06F in Symbol font = ○, or "o" in Courier New)
|
||||
// Level 2: solid square ■ (Unicode F0A7 in Wingdings)
|
||||
var levels = new Level[]
|
||||
{
|
||||
CreateBulletLevel(
|
||||
levelIndex: 0,
|
||||
bulletChar: "\xF0B7", // • in Symbol
|
||||
font: "Symbol",
|
||||
indentLeftDxa: 720, // 0.5 inch
|
||||
hangingDxa: 360), // bullet hangs 0.25 inch
|
||||
|
||||
CreateBulletLevel(
|
||||
levelIndex: 1,
|
||||
bulletChar: "o", // ○ in Courier New
|
||||
font: "Courier New",
|
||||
indentLeftDxa: 1440, // 1.0 inch
|
||||
hangingDxa: 360),
|
||||
|
||||
CreateBulletLevel(
|
||||
levelIndex: 2,
|
||||
bulletChar: "\xF0A7", // ■ in Wingdings
|
||||
font: "Wingdings",
|
||||
indentLeftDxa: 2160, // 1.5 inch
|
||||
hangingDxa: 360)
|
||||
};
|
||||
|
||||
// Build the abstract numbering definition and instance
|
||||
SetupAbstractNum(numPart, abstractNumId, levels);
|
||||
SetupNumberingInstance(numPart, numId, abstractNumId);
|
||||
|
||||
// Create sample list items at each level
|
||||
string[] level0Items = ["First item", "Second item", "Third item"];
|
||||
string[] level1Items = ["Sub-item A", "Sub-item B"];
|
||||
string[] level2Items = ["Detail 1", "Detail 2"];
|
||||
|
||||
foreach (string text in level0Items)
|
||||
{
|
||||
Paragraph para = CreateListParagraph(text, numId, level: 0);
|
||||
body.AppendChild(para);
|
||||
}
|
||||
foreach (string text in level1Items)
|
||||
{
|
||||
Paragraph para = CreateListParagraph(text, numId, level: 1);
|
||||
body.AppendChild(para);
|
||||
}
|
||||
foreach (string text in level2Items)
|
||||
{
|
||||
Paragraph para = CreateListParagraph(text, numId, level: 2);
|
||||
body.AppendChild(para);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2. Numbered List (3 levels) ────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Creates a 3-level numbered list: 1. → 1.1. → 1.1.1.
|
||||
/// Uses NumberFormatValues.Decimal with compound LevelText patterns.
|
||||
/// </summary>
|
||||
public static void CreateNumberedList(
|
||||
NumberingDefinitionsPart numPart, Body body)
|
||||
{
|
||||
int abstractNumId = 1;
|
||||
int numId = 2;
|
||||
|
||||
// LevelText explanation:
|
||||
// "%1" → just the level-0 counter: 1, 2, 3...
|
||||
// "%1.%2" → level-0.level-1: 1.1, 1.2, 2.1...
|
||||
// "%1.%2.%3" → level-0.level-1.level-2: 1.1.1, 1.1.2...
|
||||
var levels = new Level[]
|
||||
{
|
||||
CreateNumberLevel(
|
||||
levelIndex: 0,
|
||||
format: NumberFormatValues.Decimal,
|
||||
levelText: "%1.", // "1.", "2.", "3."
|
||||
indentLeftDxa: 720,
|
||||
hangingDxa: 360,
|
||||
start: 1),
|
||||
|
||||
CreateNumberLevel(
|
||||
levelIndex: 1,
|
||||
format: NumberFormatValues.Decimal,
|
||||
levelText: "%1.%2.", // "1.1.", "1.2.", "2.1."
|
||||
indentLeftDxa: 1440,
|
||||
hangingDxa: 720, // wider hanging for "1.1."
|
||||
start: 1),
|
||||
|
||||
CreateNumberLevel(
|
||||
levelIndex: 2,
|
||||
format: NumberFormatValues.Decimal,
|
||||
levelText: "%1.%2.%3.", // "1.1.1.", "1.1.2."
|
||||
indentLeftDxa: 2160,
|
||||
hangingDxa: 1080,
|
||||
start: 1)
|
||||
};
|
||||
|
||||
SetupAbstractNum(numPart, abstractNumId, levels);
|
||||
SetupNumberingInstance(numPart, numId, abstractNumId);
|
||||
|
||||
// Sample items
|
||||
body.AppendChild(CreateListParagraph("Chapter One", numId, level: 0));
|
||||
body.AppendChild(CreateListParagraph("Section One", numId, level: 1));
|
||||
body.AppendChild(CreateListParagraph("Detail A", numId, level: 2));
|
||||
body.AppendChild(CreateListParagraph("Detail B", numId, level: 2));
|
||||
body.AppendChild(CreateListParagraph("Section Two", numId, level: 1));
|
||||
body.AppendChild(CreateListParagraph("Chapter Two", numId, level: 0));
|
||||
}
|
||||
|
||||
// ── 3. Custom Bullet Characters ────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Creates bullets with custom Unicode characters: ✓ (check), ➢ (arrow), ★ (star).
|
||||
/// Uses specific fonts that contain these glyphs.
|
||||
/// </summary>
|
||||
public static void CreateCustomBullets(
|
||||
NumberingDefinitionsPart numPart, Body body)
|
||||
{
|
||||
int abstractNumId = 2;
|
||||
int numId = 3;
|
||||
|
||||
// For custom Unicode bullets, the font in NumberingSymbolRunProperties
|
||||
// MUST contain the glyph. Common choices:
|
||||
// - "Segoe UI Symbol" — broad Unicode coverage on Windows
|
||||
// - "Arial Unicode MS" — wide coverage
|
||||
// - "Wingdings" / "Webdings" — symbol fonts (use their private codepoints)
|
||||
var levels = new Level[]
|
||||
{
|
||||
CreateBulletLevel(
|
||||
levelIndex: 0,
|
||||
bulletChar: "\u2713", // ✓ CHECK MARK
|
||||
font: "Segoe UI Symbol",
|
||||
indentLeftDxa: 720,
|
||||
hangingDxa: 360),
|
||||
|
||||
CreateBulletLevel(
|
||||
levelIndex: 1,
|
||||
bulletChar: "\u27A2", // ➢ THREE-D TOP-LIGHTED RIGHTWARDS ARROWHEAD
|
||||
font: "Segoe UI Symbol",
|
||||
indentLeftDxa: 1440,
|
||||
hangingDxa: 360),
|
||||
|
||||
CreateBulletLevel(
|
||||
levelIndex: 2,
|
||||
bulletChar: "\u2605", // ★ BLACK STAR
|
||||
font: "Segoe UI Symbol",
|
||||
indentLeftDxa: 2160,
|
||||
hangingDxa: 360)
|
||||
};
|
||||
|
||||
SetupAbstractNum(numPart, abstractNumId, levels);
|
||||
SetupNumberingInstance(numPart, numId, abstractNumId);
|
||||
|
||||
body.AppendChild(CreateListParagraph("Completed task", numId, level: 0));
|
||||
body.AppendChild(CreateListParagraph("Action item", numId, level: 1));
|
||||
body.AppendChild(CreateListParagraph("Starred note", numId, level: 2));
|
||||
}
|
||||
|
||||
// ── 4. Outline Numbering Linked to Heading Styles ──────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Creates outline numbering (Article 1, Section 1.1, etc.) linked to
|
||||
/// Heading1, Heading2, Heading3 styles. This is how Word's built-in
|
||||
/// "List Number" styles work for legal/technical documents.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When a Level has ParagraphStyleIdInLevel, any paragraph with that
|
||||
/// style ID automatically gets numbered. The numbering is "linked" to
|
||||
/// the style — you don't need NumberingProperties on each paragraph
|
||||
/// (though it's also valid to add them explicitly).
|
||||
/// </remarks>
|
||||
public static void CreateOutlineNumbering(
|
||||
NumberingDefinitionsPart numPart,
|
||||
StyleDefinitionsPart stylesPart)
|
||||
{
|
||||
int abstractNumId = 3;
|
||||
int numId = 4;
|
||||
|
||||
var abstractNum = new AbstractNum(
|
||||
// Level 0: "1" — linked to Heading1
|
||||
new Level(
|
||||
new StartNumberingValue { Val = 1 },
|
||||
new NumberingFormat { Val = NumberFormatValues.Decimal },
|
||||
new LevelText { Val = "%1" },
|
||||
new LevelJustification { Val = LevelJustificationValues.Left },
|
||||
new ParagraphStyleIdInLevel { Val = "Heading1" },
|
||||
new PreviousParagraphProperties(
|
||||
new Indentation { Left = "432", Hanging = "432" })
|
||||
)
|
||||
{ LevelIndex = 0 },
|
||||
|
||||
// Level 1: "1.1" — linked to Heading2
|
||||
new Level(
|
||||
new StartNumberingValue { Val = 1 },
|
||||
new NumberingFormat { Val = NumberFormatValues.Decimal },
|
||||
new LevelText { Val = "%1.%2" },
|
||||
new LevelJustification { Val = LevelJustificationValues.Left },
|
||||
new ParagraphStyleIdInLevel { Val = "Heading2" },
|
||||
new PreviousParagraphProperties(
|
||||
new Indentation { Left = "576", Hanging = "576" })
|
||||
)
|
||||
{ LevelIndex = 1 },
|
||||
|
||||
// Level 2: "1.1.1" — linked to Heading3
|
||||
new Level(
|
||||
new StartNumberingValue { Val = 1 },
|
||||
new NumberingFormat { Val = NumberFormatValues.Decimal },
|
||||
new LevelText { Val = "%1.%2.%3" },
|
||||
new LevelJustification { Val = LevelJustificationValues.Left },
|
||||
new ParagraphStyleIdInLevel { Val = "Heading3" },
|
||||
new PreviousParagraphProperties(
|
||||
new Indentation { Left = "720", Hanging = "720" })
|
||||
)
|
||||
{ LevelIndex = 2 }
|
||||
)
|
||||
{
|
||||
AbstractNumberId = abstractNumId,
|
||||
// MultiLevelType controls how Word treats level transitions:
|
||||
// - HybridMultilevel: each level is somewhat independent (most common)
|
||||
// - Multilevel: true outline numbering where sub-levels nest under parents
|
||||
// - SingleLevel: only one level
|
||||
MultiLevelType = new MultiLevelType
|
||||
{
|
||||
Val = MultiLevelValues.Multilevel
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure AbstractNum appears first, then NumberingInstance
|
||||
EnsureNumberingRoot(numPart);
|
||||
numPart.Numbering.Append(abstractNum);
|
||||
|
||||
var numInstance = new NumberingInstance(
|
||||
new AbstractNumId { Val = abstractNumId })
|
||||
{ NumberID = numId };
|
||||
numPart.Numbering.Append(numInstance);
|
||||
|
||||
// Link the styles to the numbering definition.
|
||||
// Each heading style gets a NumberingProperties pointing to this numId.
|
||||
Styles styles = stylesPart.Styles ?? (stylesPart.Styles = new Styles());
|
||||
|
||||
LinkStyleToNumbering(styles, "Heading1", numId, level: 0);
|
||||
LinkStyleToNumbering(styles, "Heading2", numId, level: 1);
|
||||
LinkStyleToNumbering(styles, "Heading3", numId, level: 2);
|
||||
}
|
||||
|
||||
// ── 5. Legal Numbering ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Creates a legal document numbering pattern:
|
||||
/// Article I, Article II (Roman numerals)
|
||||
/// Section 1, Section 2 (Decimal)
|
||||
/// (a), (b), (c) (Lowercase letters)
|
||||
/// </summary>
|
||||
public static void CreateLegalNumbering(
|
||||
NumberingDefinitionsPart numPart, Body body)
|
||||
{
|
||||
int abstractNumId = 4;
|
||||
int numId = 5;
|
||||
|
||||
var abstractNum = new AbstractNum(
|
||||
// Level 0: "Article I" — Upper Roman
|
||||
new Level(
|
||||
new StartNumberingValue { Val = 1 },
|
||||
new NumberingFormat { Val = NumberFormatValues.UpperRoman },
|
||||
new LevelText { Val = "Article %1" },
|
||||
new LevelJustification { Val = LevelJustificationValues.Left },
|
||||
new PreviousParagraphProperties(
|
||||
new Indentation { Left = "720", Hanging = "720" }),
|
||||
new NumberingSymbolRunProperties(
|
||||
new Bold(),
|
||||
new RunFonts { Ascii = "Times New Roman", HighAnsi = "Times New Roman" })
|
||||
)
|
||||
{ LevelIndex = 0 },
|
||||
|
||||
// Level 1: "Section 1" — Decimal
|
||||
new Level(
|
||||
new StartNumberingValue { Val = 1 },
|
||||
new NumberingFormat { Val = NumberFormatValues.Decimal },
|
||||
new LevelText { Val = "Section %2" },
|
||||
new LevelJustification { Val = LevelJustificationValues.Left },
|
||||
new PreviousParagraphProperties(
|
||||
new Indentation { Left = "1440", Hanging = "720" })
|
||||
)
|
||||
{ LevelIndex = 1 },
|
||||
|
||||
// Level 2: "(a)" — Lowercase letter
|
||||
new Level(
|
||||
new StartNumberingValue { Val = 1 },
|
||||
new NumberingFormat { Val = NumberFormatValues.LowerLetter },
|
||||
new LevelText { Val = "(%3)" },
|
||||
new LevelJustification { Val = LevelJustificationValues.Left },
|
||||
new PreviousParagraphProperties(
|
||||
new Indentation { Left = "2160", Hanging = "720" })
|
||||
)
|
||||
{ LevelIndex = 2 }
|
||||
)
|
||||
{
|
||||
AbstractNumberId = abstractNumId,
|
||||
MultiLevelType = new MultiLevelType { Val = MultiLevelValues.Multilevel }
|
||||
};
|
||||
|
||||
EnsureNumberingRoot(numPart);
|
||||
numPart.Numbering.Append(abstractNum);
|
||||
SetupNumberingInstance(numPart, numId, abstractNumId);
|
||||
|
||||
// Sample legal document structure
|
||||
body.AppendChild(CreateListParagraph("Definitions", numId, level: 0));
|
||||
body.AppendChild(CreateListParagraph("General Terms", numId, level: 1));
|
||||
body.AppendChild(CreateListParagraph(
|
||||
"\"Agreement\" means this document and all exhibits.", numId, level: 2));
|
||||
body.AppendChild(CreateListParagraph(
|
||||
"\"Party\" means any signatory to this Agreement.", numId, level: 2));
|
||||
body.AppendChild(CreateListParagraph("Scope of Work", numId, level: 1));
|
||||
body.AppendChild(CreateListParagraph("Obligations", numId, level: 0));
|
||||
}
|
||||
|
||||
// ── 6. Chinese Numbering ───────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Chinese document numbering hierarchy:
|
||||
/// Level 0: 一、二、三、 (Chinese ideographic, followed by 、)
|
||||
/// Level 1: (一)(二)(三) (Chinese ideographic in parentheses)
|
||||
/// Level 2: 1. 2. 3. (Decimal, Arabic numerals)
|
||||
/// Level 3: (1) (2) (3) (Decimal in parentheses)
|
||||
///
|
||||
/// Chinese numbering uses NumberFormatValues.ChineseCounting or
|
||||
/// ChineseCountingThousand for 一二三 style characters.
|
||||
/// The font for Chinese number characters should be a CJK font like SimSun or SimHei.
|
||||
/// </summary>
|
||||
public static void CreateChineseNumbering(
|
||||
NumberingDefinitionsPart numPart, Body body)
|
||||
{
|
||||
int abstractNumId = 5;
|
||||
int numId = 6;
|
||||
|
||||
var abstractNum = new AbstractNum(
|
||||
// Level 0: 一、 二、 三、
|
||||
// ChineseCountingThousand produces 一 二 三 四 五 六 七 八 九 十
|
||||
new Level(
|
||||
new StartNumberingValue { Val = 1 },
|
||||
new NumberingFormat { Val = NumberFormatValues.ChineseCountingThousand },
|
||||
new LevelText { Val = "%1\u3001" }, // 、 is the Chinese enumeration comma
|
||||
new LevelJustification { Val = LevelJustificationValues.Left },
|
||||
new PreviousParagraphProperties(
|
||||
new Indentation { Left = "840", Hanging = "420" }),
|
||||
// NumberingSymbolRunProperties MUST specify a CJK font
|
||||
// so the Chinese number renders correctly
|
||||
new NumberingSymbolRunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "SimSun",
|
||||
HighAnsi = "SimSun",
|
||||
EastAsia = "SimSun", // Critical for CJK rendering
|
||||
ComplexScript = "SimSun"
|
||||
})
|
||||
)
|
||||
{ LevelIndex = 0 },
|
||||
|
||||
// Level 1: (一)(二)(三)
|
||||
new Level(
|
||||
new StartNumberingValue { Val = 1 },
|
||||
new NumberingFormat { Val = NumberFormatValues.ChineseCountingThousand },
|
||||
new LevelText { Val = "\uFF08%2\uFF09" }, // ( and ) are fullwidth parens
|
||||
new LevelJustification { Val = LevelJustificationValues.Left },
|
||||
new PreviousParagraphProperties(
|
||||
new Indentation { Left = "1260", Hanging = "420" }),
|
||||
new NumberingSymbolRunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = "SimSun",
|
||||
HighAnsi = "SimSun",
|
||||
EastAsia = "SimSun",
|
||||
ComplexScript = "SimSun"
|
||||
})
|
||||
)
|
||||
{ LevelIndex = 1 },
|
||||
|
||||
// Level 2: 1. 2. 3.
|
||||
new Level(
|
||||
new StartNumberingValue { Val = 1 },
|
||||
new NumberingFormat { Val = NumberFormatValues.Decimal },
|
||||
new LevelText { Val = "%3." },
|
||||
new LevelJustification { Val = LevelJustificationValues.Left },
|
||||
new PreviousParagraphProperties(
|
||||
new Indentation { Left = "1680", Hanging = "420" })
|
||||
)
|
||||
{ LevelIndex = 2 },
|
||||
|
||||
// Level 3: (1) (2) (3)
|
||||
new Level(
|
||||
new StartNumberingValue { Val = 1 },
|
||||
new NumberingFormat { Val = NumberFormatValues.Decimal },
|
||||
new LevelText { Val = "(%4)" },
|
||||
new LevelJustification { Val = LevelJustificationValues.Left },
|
||||
new PreviousParagraphProperties(
|
||||
new Indentation { Left = "2100", Hanging = "420" })
|
||||
)
|
||||
{ LevelIndex = 3 }
|
||||
)
|
||||
{
|
||||
AbstractNumberId = abstractNumId,
|
||||
MultiLevelType = new MultiLevelType { Val = MultiLevelValues.Multilevel }
|
||||
};
|
||||
|
||||
EnsureNumberingRoot(numPart);
|
||||
numPart.Numbering.Append(abstractNum);
|
||||
SetupNumberingInstance(numPart, numId, abstractNumId);
|
||||
|
||||
body.AppendChild(CreateListParagraph("总则", numId, level: 0));
|
||||
body.AppendChild(CreateListParagraph("目的和依据", numId, level: 1));
|
||||
body.AppendChild(CreateListParagraph("本办法适用于全体员工。", numId, level: 2));
|
||||
body.AppendChild(CreateListParagraph("自发布之日起施行。", numId, level: 3));
|
||||
body.AppendChild(CreateListParagraph("适用范围", numId, level: 1));
|
||||
body.AppendChild(CreateListParagraph("职责与权限", numId, level: 0));
|
||||
}
|
||||
|
||||
// ── 7. Restart Numbering ───────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrates how to restart a numbered list at 1 using LevelOverride
|
||||
/// with StartOverride. This creates a new NumberingInstance that shares
|
||||
/// the same AbstractNum but overrides the start value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Scenario: You have items 1-5 in one list, then want a separate list
|
||||
/// that starts again at 1 with the same formatting. You need a new
|
||||
/// NumberingInstance (new NumId) with LevelOverride.
|
||||
/// </remarks>
|
||||
public static void RestartNumbering(
|
||||
NumberingDefinitionsPart numPart, Body body)
|
||||
{
|
||||
int abstractNumId = 6;
|
||||
int numId1 = 7;
|
||||
int numId2 = 8; // Second instance for restarted list
|
||||
|
||||
// Simple single-level numbered list
|
||||
var levels = new Level[]
|
||||
{
|
||||
CreateNumberLevel(
|
||||
levelIndex: 0,
|
||||
format: NumberFormatValues.Decimal,
|
||||
levelText: "%1.",
|
||||
indentLeftDxa: 720,
|
||||
hangingDxa: 360,
|
||||
start: 1)
|
||||
};
|
||||
|
||||
SetupAbstractNum(numPart, abstractNumId, levels);
|
||||
SetupNumberingInstance(numPart, numId1, abstractNumId);
|
||||
|
||||
// First list: 1, 2, 3
|
||||
body.AppendChild(CreateListParagraph("First list item 1", numId1, level: 0));
|
||||
body.AppendChild(CreateListParagraph("First list item 2", numId1, level: 0));
|
||||
body.AppendChild(CreateListParagraph("First list item 3", numId1, level: 0));
|
||||
|
||||
// Non-list paragraph between the lists
|
||||
body.AppendChild(new Paragraph(
|
||||
new Run(new Text("Some text between lists."))));
|
||||
|
||||
// Create a NEW NumberingInstance with LevelOverride to restart at 1.
|
||||
// LevelOverride on a NumberingInstance overrides a specific level's
|
||||
// start value WITHOUT creating a new AbstractNum.
|
||||
var restartedInstance = new NumberingInstance(
|
||||
new AbstractNumId { Val = abstractNumId },
|
||||
// LevelOverride resets level 0 to start at 1
|
||||
new LevelOverride(
|
||||
new StartOverrideNumberingValue { Val = 1 }
|
||||
)
|
||||
{ LevelIndex = 0 }
|
||||
)
|
||||
{ NumberID = numId2 };
|
||||
|
||||
numPart.Numbering.Append(restartedInstance);
|
||||
|
||||
// Second list uses numId2: starts at 1 again
|
||||
body.AppendChild(CreateListParagraph("Restarted item 1", numId2, level: 0));
|
||||
body.AppendChild(CreateListParagraph("Restarted item 2", numId2, level: 0));
|
||||
body.AppendChild(CreateListParagraph("Restarted item 3", numId2, level: 0));
|
||||
}
|
||||
|
||||
// ── 8. Continue Numbering ──────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Continues numbering from a previous list by using the same NumId.
|
||||
/// All paragraphs sharing a NumId form a single continuous sequence.
|
||||
/// Inserting non-list paragraphs between them does NOT break the sequence.
|
||||
/// </summary>
|
||||
/// <param name="body">The Body to append paragraphs to.</param>
|
||||
/// <param name="existingNumId">The NumId of the list to continue.</param>
|
||||
public static void ContinueNumbering(Body body, int existingNumId)
|
||||
{
|
||||
// Simply use the SAME numId as the existing list.
|
||||
// Word automatically continues the counter from wherever it left off.
|
||||
// Even if there are non-list paragraphs in between, the numbering
|
||||
// picks up seamlessly.
|
||||
|
||||
body.AppendChild(new Paragraph(
|
||||
new Run(new Text("(Non-list paragraph — numbering continues after this.)"))));
|
||||
|
||||
// These will be numbered 4, 5 (assuming previous list ended at 3)
|
||||
body.AppendChild(CreateListParagraph(
|
||||
"Continued item", existingNumId, level: 0));
|
||||
body.AppendChild(CreateListParagraph(
|
||||
"Another continued item", existingNumId, level: 0));
|
||||
}
|
||||
|
||||
// ── 9. Setup AbstractNum (Helper) ──────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Builds an AbstractNum from an array of Level definitions and appends
|
||||
/// it to the Numbering root. AbstractNum defines the *format* of a list
|
||||
/// (bullet characters, number format, indentation, fonts).
|
||||
/// </summary>
|
||||
/// <param name="numPart">The NumberingDefinitionsPart to append to.</param>
|
||||
/// <param name="abstractNumId">Unique ID for this abstract definition.</param>
|
||||
/// <param name="levels">Array of Level elements (one per nesting level, max 9).</param>
|
||||
public static void SetupAbstractNum(
|
||||
NumberingDefinitionsPart numPart, int abstractNumId, Level[] levels)
|
||||
{
|
||||
EnsureNumberingRoot(numPart);
|
||||
|
||||
var abstractNum = new AbstractNum
|
||||
{
|
||||
AbstractNumberId = abstractNumId,
|
||||
// MultiLevelType:
|
||||
// HybridMultilevel — most common; each level can have independent formatting
|
||||
// Multilevel — true outline; sub-levels inherit parent context
|
||||
// SingleLevel — only level 0 is used
|
||||
MultiLevelType = new MultiLevelType
|
||||
{
|
||||
Val = levels.Length > 1
|
||||
? MultiLevelValues.HybridMultilevel
|
||||
: MultiLevelValues.SingleLevel
|
||||
}
|
||||
};
|
||||
|
||||
foreach (Level level in levels)
|
||||
{
|
||||
abstractNum.Append(level.CloneNode(true));
|
||||
}
|
||||
|
||||
// IMPORTANT: AbstractNum must be inserted BEFORE any NumberingInstance
|
||||
// elements in the Numbering root. Find the right position.
|
||||
NumberingInstance? firstNumInstance =
|
||||
numPart.Numbering.GetFirstChild<NumberingInstance>();
|
||||
|
||||
if (firstNumInstance is not null)
|
||||
{
|
||||
numPart.Numbering.InsertBefore(abstractNum, firstNumInstance);
|
||||
}
|
||||
else
|
||||
{
|
||||
numPart.Numbering.Append(abstractNum);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 10. Setup NumberingInstance (Helper) ────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Creates a NumberingInstance (Num element) that references an AbstractNum.
|
||||
/// The NumberingInstance is what paragraphs actually point to via NumId.
|
||||
/// Multiple paragraphs with the same NumId form one continuous list.
|
||||
/// </summary>
|
||||
/// <param name="numPart">The NumberingDefinitionsPart to append to.</param>
|
||||
/// <param name="numId">Unique instance ID (referenced by paragraphs).
|
||||
/// Must be >= 1; value 0 is reserved for "no numbering".</param>
|
||||
/// <param name="abstractNumId">The AbstractNum this instance uses.</param>
|
||||
public static void SetupNumberingInstance(
|
||||
NumberingDefinitionsPart numPart, int numId, int abstractNumId)
|
||||
{
|
||||
EnsureNumberingRoot(numPart);
|
||||
|
||||
// NumberingInstance (w:num) links to AbstractNum via AbstractNumId child
|
||||
var numInstance = new NumberingInstance(
|
||||
new AbstractNumId { Val = abstractNumId })
|
||||
{
|
||||
// NumberID is the w:numId attribute; this is what paragraphs reference
|
||||
NumberID = numId
|
||||
};
|
||||
|
||||
// NumberingInstance MUST come after all AbstractNum elements
|
||||
numPart.Numbering.Append(numInstance);
|
||||
}
|
||||
|
||||
// ── 11. Apply Numbering to Paragraph (Helper) ──────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Applies numbering to an existing paragraph by setting NumberingProperties
|
||||
/// in the ParagraphProperties. This is the final link that makes a
|
||||
/// paragraph display as a list item.
|
||||
/// </summary>
|
||||
/// <param name="para">The paragraph to make into a list item.</param>
|
||||
/// <param name="numId">The NumberingInstance ID to use.</param>
|
||||
/// <param name="level">The indentation level (0 = top level, max 8).</param>
|
||||
public static void ApplyNumberingToParagraph(Paragraph para, int numId, int level)
|
||||
{
|
||||
// NumberingProperties contains:
|
||||
// - NumberingLevelReference (w:ilvl) — which level (0-8)
|
||||
// - NumberingId (w:numId) — which NumberingInstance to use
|
||||
var numberingProperties = new NumberingProperties(
|
||||
new NumberingLevelReference { Val = level },
|
||||
new NumberingId { Val = numId });
|
||||
|
||||
// Ensure ParagraphProperties exists
|
||||
ParagraphProperties pPr = para.GetFirstChild<ParagraphProperties>()
|
||||
?? para.PrependChild(new ParagraphProperties());
|
||||
|
||||
// Replace existing NumberingProperties if present
|
||||
NumberingProperties? existing = pPr.GetFirstChild<NumberingProperties>();
|
||||
if (existing is not null)
|
||||
{
|
||||
pPr.ReplaceChild(numberingProperties, existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
// NumberingProperties should appear early in ParagraphProperties
|
||||
// (after ParagraphStyleId if present)
|
||||
ParagraphStyleId? styleId = pPr.GetFirstChild<ParagraphStyleId>();
|
||||
if (styleId is not null)
|
||||
{
|
||||
pPr.InsertAfter(numberingProperties, styleId);
|
||||
}
|
||||
else
|
||||
{
|
||||
pPr.PrependChild(numberingProperties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Private Helper Methods ─────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Creates a bullet-type Level definition.
|
||||
/// </summary>
|
||||
private static Level CreateBulletLevel(
|
||||
int levelIndex,
|
||||
string bulletChar,
|
||||
string font,
|
||||
int indentLeftDxa,
|
||||
int hangingDxa)
|
||||
{
|
||||
return new Level(
|
||||
// Bullets don't increment, but StartNumberingValue is still required
|
||||
new StartNumberingValue { Val = 1 },
|
||||
// NumberFormatValues.Bullet tells Word this is a bullet, not a number
|
||||
new NumberingFormat { Val = NumberFormatValues.Bullet },
|
||||
// LevelText.Val is the actual bullet character
|
||||
new LevelText { Val = bulletChar },
|
||||
new LevelJustification { Val = LevelJustificationValues.Left },
|
||||
// PreviousParagraphProperties controls indentation of the text
|
||||
// (confusingly named; it's the paragraph indent for THIS level)
|
||||
new PreviousParagraphProperties(
|
||||
new Indentation
|
||||
{
|
||||
Left = indentLeftDxa.ToString(),
|
||||
Hanging = hangingDxa.ToString()
|
||||
}),
|
||||
// NumberingSymbolRunProperties sets the font for the bullet character.
|
||||
// Without this, the bullet renders in the paragraph's body font,
|
||||
// which may not contain the glyph (e.g., Symbol characters).
|
||||
new NumberingSymbolRunProperties(
|
||||
new RunFonts
|
||||
{
|
||||
Ascii = font,
|
||||
HighAnsi = font,
|
||||
Hint = FontTypeHintValues.Default
|
||||
})
|
||||
)
|
||||
{ LevelIndex = levelIndex };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a number-type Level definition.
|
||||
/// </summary>
|
||||
private static Level CreateNumberLevel(
|
||||
int levelIndex,
|
||||
NumberFormatValues format,
|
||||
string levelText,
|
||||
int indentLeftDxa,
|
||||
int hangingDxa,
|
||||
int start)
|
||||
{
|
||||
return new Level(
|
||||
new StartNumberingValue { Val = start },
|
||||
new NumberingFormat { Val = format },
|
||||
new LevelText { Val = levelText },
|
||||
new LevelJustification { Val = LevelJustificationValues.Left },
|
||||
new PreviousParagraphProperties(
|
||||
new Indentation
|
||||
{
|
||||
Left = indentLeftDxa.ToString(),
|
||||
Hanging = hangingDxa.ToString()
|
||||
})
|
||||
)
|
||||
{ LevelIndex = levelIndex };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a paragraph with text and numbering properties applied.
|
||||
/// </summary>
|
||||
private static Paragraph CreateListParagraph(string text, int numId, int level)
|
||||
{
|
||||
var para = new Paragraph(
|
||||
new ParagraphProperties(
|
||||
new NumberingProperties(
|
||||
new NumberingLevelReference { Val = level },
|
||||
new NumberingId { Val = numId })),
|
||||
new Run(new Text(text)));
|
||||
return para;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the Numbering root element exists on the NumberingDefinitionsPart.
|
||||
/// </summary>
|
||||
private static void EnsureNumberingRoot(NumberingDefinitionsPart numPart)
|
||||
{
|
||||
if (numPart.Numbering is null)
|
||||
{
|
||||
numPart.Numbering = new Numbering();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Links a named style to a numbering definition by adding NumberingProperties
|
||||
/// to the style's ParagraphProperties.
|
||||
/// </summary>
|
||||
private static void LinkStyleToNumbering(
|
||||
Styles styles, string styleId, int numId, int level)
|
||||
{
|
||||
// Find existing style or create it
|
||||
Style? style = styles.Elements<Style>()
|
||||
.FirstOrDefault(s => s.StyleId?.Value == styleId);
|
||||
|
||||
if (style is null)
|
||||
{
|
||||
style = new Style
|
||||
{
|
||||
Type = StyleValues.Paragraph,
|
||||
StyleId = styleId,
|
||||
StyleName = new StyleName { Val = styleId }
|
||||
};
|
||||
styles.Append(style);
|
||||
}
|
||||
|
||||
// Ensure StyleParagraphProperties exists
|
||||
StyleParagraphProperties? spPr = style.GetFirstChild<StyleParagraphProperties>();
|
||||
if (spPr is null)
|
||||
{
|
||||
spPr = new StyleParagraphProperties();
|
||||
style.Append(spPr);
|
||||
}
|
||||
|
||||
// Set NumberingProperties on the style
|
||||
NumberingProperties? existingNumPr = spPr.GetFirstChild<NumberingProperties>();
|
||||
var newNumPr = new NumberingProperties(
|
||||
new NumberingLevelReference { Val = level },
|
||||
new NumberingId { Val = numId });
|
||||
|
||||
if (existingNumPr is not null)
|
||||
{
|
||||
spPr.ReplaceChild(newNumPr, existingNumPr);
|
||||
}
|
||||
else
|
||||
{
|
||||
spPr.Append(newNumPr);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,595 @@
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Samples;
|
||||
|
||||
/// <summary>
|
||||
/// Reference implementations for revision tracking (Track Changes).
|
||||
///
|
||||
/// ╔══════════════════════════════════════════════════════════════════╗
|
||||
/// ║ CRITICAL: w:del uses w:delText, NEVER w:t ║
|
||||
/// ║ w:ins uses w:t, NEVER w:delText ║
|
||||
/// ║ Getting this wrong silently corrupts the document. ║
|
||||
/// ║ Word will open without error but display garbled text or ║
|
||||
/// ║ lose content when accepting/rejecting changes. ║
|
||||
/// ╚══════════════════════════════════════════════════════════════════╝
|
||||
///
|
||||
/// KEY CONCEPTS:
|
||||
/// - Every revision element (ins, del, rPrChange, pPrChange) needs:
|
||||
/// w:id — unique revision ID (string, must be unique across all revisions)
|
||||
/// w:author — who made the change
|
||||
/// w:date — ISO 8601 timestamp
|
||||
/// - InsertedRun (w:ins) wraps normal Run elements with w:t text
|
||||
/// - DeletedRun (w:del) wraps Run elements that use DeletedText (w:delText) instead of Text (w:t)
|
||||
/// - MoveFrom/MoveTo track text that was moved (not just deleted+inserted)
|
||||
/// </summary>
|
||||
public static class TrackChangesSamples
|
||||
{
|
||||
/// <summary>
|
||||
/// Thread-safe counter for generating unique revision IDs.
|
||||
/// In production, scan the document for the max existing ID first.
|
||||
/// </summary>
|
||||
private static int s_revisionCounter;
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 1. EnableTrackChanges
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Enables revision tracking in the document settings.
|
||||
/// This makes Word record all subsequent edits as tracked changes.
|
||||
///
|
||||
/// Maps to: <w:trackChanges/> in settings.xml
|
||||
///
|
||||
/// Note: This only controls whether NEW edits are tracked.
|
||||
/// Existing revision marks are always preserved regardless of this setting.
|
||||
/// </summary>
|
||||
public static void EnableTrackChanges(DocumentSettingsPart settingsPart)
|
||||
{
|
||||
settingsPart.Settings ??= new Settings();
|
||||
|
||||
var existing = settingsPart.Settings.GetFirstChild<TrackRevisions>();
|
||||
if (existing == null)
|
||||
{
|
||||
settingsPart.Settings.Append(new TrackRevisions());
|
||||
}
|
||||
|
||||
settingsPart.Settings.Save();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 2. InsertTrackedInsertion — w:ins with w:t
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts text as a tracked insertion (w:ins).
|
||||
///
|
||||
/// ╔══════════════════════════════════════════════════════╗
|
||||
/// ║ w:ins uses w:t (Text), NOT w:delText. ║
|
||||
/// ║ The text appears with green underline in Word. ║
|
||||
/// ╚══════════════════════════════════════════════════════╝
|
||||
///
|
||||
/// XML structure:
|
||||
/// <w:ins w:id="1" w:author="John" w:date="2026-03-22T00:00:00Z">
|
||||
/// <w:r>
|
||||
/// <w:t>inserted text</w:t> <!-- w:t, NOT w:delText -->
|
||||
/// </w:r>
|
||||
/// </w:ins>
|
||||
/// </summary>
|
||||
public static InsertedRun InsertTrackedInsertion(Paragraph para, string text, string author)
|
||||
{
|
||||
var ins = new InsertedRun
|
||||
{
|
||||
Id = GenerateRevisionId(),
|
||||
Author = author,
|
||||
Date = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// CORRECT: w:ins contains w:r with w:t (normal Text element)
|
||||
ins.Append(new Run(
|
||||
new Text(text) { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
para.Append(ins);
|
||||
return ins;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 3. InsertTrackedDeletion — w:del with w:delText
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Inserts text as a tracked deletion (w:del).
|
||||
///
|
||||
/// ╔══════════════════════════════════════════════════════╗
|
||||
/// ║ w:del uses w:delText (DeletedText), NOT w:t. ║
|
||||
/// ║ Using w:t inside w:del SILENTLY CORRUPTS the file. ║
|
||||
/// ║ The text appears with red strikethrough in Word. ║
|
||||
/// ╚══════════════════════════════════════════════════════╝
|
||||
///
|
||||
/// XML structure:
|
||||
/// <w:del w:id="2" w:author="John" w:date="2026-03-22T00:00:00Z">
|
||||
/// <w:r>
|
||||
/// <w:delText xml:space="preserve">deleted text</w:delText> <!-- w:delText, NOT w:t -->
|
||||
/// </w:r>
|
||||
/// </w:del>
|
||||
/// </summary>
|
||||
public static DeletedRun InsertTrackedDeletion(Paragraph para, string deletedText, string author)
|
||||
{
|
||||
var del = new DeletedRun
|
||||
{
|
||||
Id = GenerateRevisionId(),
|
||||
Author = author,
|
||||
Date = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// CORRECT: w:del contains w:r with w:delText (DeletedText element)
|
||||
// WRONG would be: new Text(deletedText) — this creates w:t which corrupts the document
|
||||
del.Append(new Run(
|
||||
new DeletedText(deletedText) { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
para.Append(del);
|
||||
return del;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 4. InsertFormattingChange — RunPropertiesChange
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Records a formatting change on a run (e.g., text was made bold).
|
||||
///
|
||||
/// RunPropertiesChange (w:rPrChange) stores the PREVIOUS formatting.
|
||||
/// The current RunProperties on the run reflects the NEW formatting.
|
||||
///
|
||||
/// Example: text changed from normal to bold:
|
||||
/// <w:rPr>
|
||||
/// <w:b/> <!-- current: bold -->
|
||||
/// <w:rPrChange w:id="3" w:author="John" w:date="...">
|
||||
/// <w:rPr/> <!-- previous: no bold -->
|
||||
/// </w:rPrChange>
|
||||
/// </w:rPr>
|
||||
/// </summary>
|
||||
public static void InsertFormattingChange(Run run, string author)
|
||||
{
|
||||
// Ensure RunProperties exists
|
||||
run.RunProperties ??= new RunProperties();
|
||||
|
||||
// Store the previous (empty/normal) formatting as the "before" state
|
||||
var rPrChange = new RunPropertiesChange
|
||||
{
|
||||
Id = GenerateRevisionId(),
|
||||
Author = author,
|
||||
Date = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// The child RunProperties inside rPrChange is the OLD formatting (before the change).
|
||||
// An empty RunProperties means "was default/normal formatting."
|
||||
rPrChange.Append(new PreviousRunProperties());
|
||||
|
||||
run.RunProperties.Append(rPrChange);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 5. InsertParagraphFormatChange — ParagraphPropertiesChange
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Records a paragraph formatting change (e.g., alignment changed).
|
||||
///
|
||||
/// ParagraphPropertiesChange (w:pPrChange) stores the PREVIOUS paragraph properties.
|
||||
/// The current ParagraphProperties reflects the NEW formatting.
|
||||
///
|
||||
/// Example: paragraph changed from left-aligned to centered:
|
||||
/// <w:pPr>
|
||||
/// <w:jc w:val="center"/> <!-- current: centered -->
|
||||
/// <w:pPrChange w:id="4" w:author="John" w:date="...">
|
||||
/// <w:pPr>
|
||||
/// <w:jc w:val="left"/> <!-- previous: left -->
|
||||
/// </w:pPr>
|
||||
/// </w:pPrChange>
|
||||
/// </w:pPr>
|
||||
/// </summary>
|
||||
public static void InsertParagraphFormatChange(Paragraph para, string author)
|
||||
{
|
||||
para.ParagraphProperties ??= new ParagraphProperties();
|
||||
|
||||
var pPrChange = new ParagraphPropertiesChange
|
||||
{
|
||||
Id = GenerateRevisionId(),
|
||||
Author = author,
|
||||
Date = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Store previous paragraph properties (before the change)
|
||||
// Example: was left-aligned before changing to whatever the current alignment is
|
||||
var previousPPr = new ParagraphPropertiesExtended();
|
||||
previousPPr.Append(new Justification { Val = JustificationValues.Left });
|
||||
pPrChange.Append(previousPPr);
|
||||
|
||||
para.ParagraphProperties.Append(pPrChange);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 6. InsertTableRowInsertion — table revision marks
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Marks a table row as a tracked insertion.
|
||||
///
|
||||
/// Table-level track changes use TableRowProperties with InsertedMathControl
|
||||
/// mapped from w:trPr/w:ins — indicating the entire row was inserted.
|
||||
///
|
||||
/// Structure:
|
||||
/// <w:tr>
|
||||
/// <w:trPr>
|
||||
/// <w:ins w:id="5" w:author="John" w:date="..."/>
|
||||
/// </w:trPr>
|
||||
/// <w:tc>...</w:tc>
|
||||
/// </w:tr>
|
||||
/// </summary>
|
||||
public static void InsertTableRowInsertion(TableRow row, string author)
|
||||
{
|
||||
row.TableRowProperties ??= new TableRowProperties();
|
||||
|
||||
var inserted = new Inserted
|
||||
{
|
||||
Id = GenerateRevisionId(),
|
||||
Author = author,
|
||||
Date = DateTime.UtcNow
|
||||
};
|
||||
|
||||
row.TableRowProperties.Append(inserted);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 7. AcceptAllRevisions — accept all tracked changes
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Programmatically accepts all tracked changes in the document body.
|
||||
///
|
||||
/// For insertions (w:ins): unwrap the content (keep the runs, remove the w:ins wrapper)
|
||||
/// For deletions (w:del): remove the entire element (the deleted text disappears)
|
||||
/// For formatting changes: remove the rPrChange/pPrChange (keep new formatting)
|
||||
/// For table row insertions: remove the w:ins from trPr
|
||||
///
|
||||
/// ╔══════════════════════════════════════════════════════════════╗
|
||||
/// ║ Process deletions before insertions to avoid invalidating ║
|
||||
/// ║ element references. Always call .ToList() before ║
|
||||
/// ║ iterating to avoid modifying the collection during ║
|
||||
/// ║ enumeration. ║
|
||||
/// ╚══════════════════════════════════════════════════════════════╝
|
||||
/// </summary>
|
||||
public static void AcceptAllRevisions(Body body)
|
||||
{
|
||||
// 1. Accept deletions — remove the w:del and all its content
|
||||
foreach (var del in body.Descendants<DeletedRun>().ToList())
|
||||
{
|
||||
del.Remove();
|
||||
}
|
||||
|
||||
// 2. Accept insertions — unwrap w:ins, keeping child runs in place
|
||||
foreach (var ins in body.Descendants<InsertedRun>().ToList())
|
||||
{
|
||||
var parent = ins.Parent;
|
||||
if (parent == null) continue;
|
||||
|
||||
// Move all child elements before the ins element, then remove ins
|
||||
var children = ins.ChildElements.ToList();
|
||||
foreach (var child in children)
|
||||
{
|
||||
child.Remove();
|
||||
ins.InsertBeforeSelf(child);
|
||||
}
|
||||
ins.Remove();
|
||||
}
|
||||
|
||||
// 3. Accept formatting changes — remove rPrChange (keep new formatting)
|
||||
foreach (var rPrChange in body.Descendants<RunPropertiesChange>().ToList())
|
||||
{
|
||||
rPrChange.Remove();
|
||||
}
|
||||
|
||||
// 4. Accept paragraph formatting changes
|
||||
foreach (var pPrChange in body.Descendants<ParagraphPropertiesChange>().ToList())
|
||||
{
|
||||
pPrChange.Remove();
|
||||
}
|
||||
|
||||
// 5. Accept table row insertions — remove w:ins from trPr
|
||||
foreach (var inserted in body.Descendants<TableRowProperties>()
|
||||
.SelectMany(trPr => trPr.Elements<Inserted>()).ToList())
|
||||
{
|
||||
inserted.Remove();
|
||||
}
|
||||
|
||||
// 6. Accept MoveFrom/MoveTo — keep MoveTo content, remove MoveFrom
|
||||
foreach (var moveFrom in body.Descendants<MoveFromRun>().ToList())
|
||||
{
|
||||
moveFrom.Remove();
|
||||
}
|
||||
foreach (var moveTo in body.Descendants<MoveToRun>().ToList())
|
||||
{
|
||||
var parent = moveTo.Parent;
|
||||
if (parent == null) continue;
|
||||
var children = moveTo.ChildElements.ToList();
|
||||
foreach (var child in children)
|
||||
{
|
||||
child.Remove();
|
||||
moveTo.InsertBeforeSelf(child);
|
||||
}
|
||||
moveTo.Remove();
|
||||
}
|
||||
|
||||
// 7. Remove move range markers
|
||||
foreach (var marker in body.Descendants<MoveFromRangeStart>().ToList()) marker.Remove();
|
||||
foreach (var marker in body.Descendants<MoveFromRangeEnd>().ToList()) marker.Remove();
|
||||
foreach (var marker in body.Descendants<MoveToRangeStart>().ToList()) marker.Remove();
|
||||
foreach (var marker in body.Descendants<MoveToRangeEnd>().ToList()) marker.Remove();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 8. RejectAllRevisions — reject all tracked changes
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Programmatically rejects all tracked changes in the document body.
|
||||
///
|
||||
/// For insertions (w:ins): remove the entire element (the inserted text disappears)
|
||||
/// For deletions (w:del): unwrap the content and convert w:delText back to w:t
|
||||
/// (the "deleted" text is restored)
|
||||
/// For formatting changes: restore old formatting from rPrChange/pPrChange
|
||||
///
|
||||
/// ╔══════════════════════════════════════════════════════════════╗
|
||||
/// ║ When rejecting deletions, you MUST convert w:delText back ║
|
||||
/// ║ to w:t. Leaving w:delText in a non-deleted run causes ║
|
||||
/// ║ the text to be invisible in Word. ║
|
||||
/// ╚══════════════════════════════════════════════════════════════╝
|
||||
/// </summary>
|
||||
public static void RejectAllRevisions(Body body)
|
||||
{
|
||||
// 1. Reject insertions — remove the entire w:ins and its content
|
||||
foreach (var ins in body.Descendants<InsertedRun>().ToList())
|
||||
{
|
||||
ins.Remove();
|
||||
}
|
||||
|
||||
// 2. Reject deletions — restore deleted text by unwrapping w:del
|
||||
// and converting w:delText back to w:t
|
||||
foreach (var del in body.Descendants<DeletedRun>().ToList())
|
||||
{
|
||||
var parent = del.Parent;
|
||||
if (parent == null) continue;
|
||||
|
||||
// Convert DeletedText -> Text in each run inside the deletion
|
||||
foreach (var run in del.Elements<Run>().ToList())
|
||||
{
|
||||
foreach (var delText in run.Elements<DeletedText>().ToList())
|
||||
{
|
||||
// IMPORTANT: convert w:delText back to w:t
|
||||
var text = new Text(delText.Text ?? "") { Space = SpaceProcessingModeValues.Preserve };
|
||||
delText.InsertAfterSelf(text);
|
||||
delText.Remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Unwrap — move children before the del element
|
||||
var children = del.ChildElements.ToList();
|
||||
foreach (var child in children)
|
||||
{
|
||||
child.Remove();
|
||||
del.InsertBeforeSelf(child);
|
||||
}
|
||||
del.Remove();
|
||||
}
|
||||
|
||||
// 3. Reject formatting changes — restore old RunProperties
|
||||
foreach (var rPrChange in body.Descendants<RunPropertiesChange>().ToList())
|
||||
{
|
||||
var runProperties = rPrChange.Parent as RunProperties;
|
||||
if (runProperties == null) continue;
|
||||
|
||||
// Get the previous (old) formatting
|
||||
var previousRPr = rPrChange.GetFirstChild<PreviousRunProperties>();
|
||||
if (previousRPr != null)
|
||||
{
|
||||
// Remove current formatting (except the rPrChange itself)
|
||||
var currentProps = runProperties.ChildElements
|
||||
.Where(c => c is not RunPropertiesChange).ToList();
|
||||
foreach (var prop in currentProps)
|
||||
{
|
||||
prop.Remove();
|
||||
}
|
||||
|
||||
// Restore old formatting from PreviousRunProperties
|
||||
foreach (var oldProp in previousRPr.ChildElements.ToList())
|
||||
{
|
||||
oldProp.Remove();
|
||||
runProperties.Append(oldProp);
|
||||
}
|
||||
}
|
||||
rPrChange.Remove();
|
||||
}
|
||||
|
||||
// 4. Reject paragraph formatting changes — restore old ParagraphProperties
|
||||
foreach (var pPrChange in body.Descendants<ParagraphPropertiesChange>().ToList())
|
||||
{
|
||||
var paragraphProperties = pPrChange.Parent as ParagraphProperties;
|
||||
if (paragraphProperties == null) continue;
|
||||
|
||||
var previousPPr = pPrChange.GetFirstChild<ParagraphPropertiesExtended>();
|
||||
if (previousPPr != null)
|
||||
{
|
||||
var currentProps = paragraphProperties.ChildElements
|
||||
.Where(c => c is not ParagraphPropertiesChange).ToList();
|
||||
foreach (var prop in currentProps)
|
||||
{
|
||||
prop.Remove();
|
||||
}
|
||||
foreach (var oldProp in previousPPr.ChildElements.ToList())
|
||||
{
|
||||
oldProp.Remove();
|
||||
paragraphProperties.Append(oldProp);
|
||||
}
|
||||
}
|
||||
pPrChange.Remove();
|
||||
}
|
||||
|
||||
// 5. Reject table row insertions — remove the entire row
|
||||
foreach (var row in body.Descendants<TableRow>().ToList())
|
||||
{
|
||||
var trPr = row.TableRowProperties;
|
||||
if (trPr?.GetFirstChild<Inserted>() != null)
|
||||
{
|
||||
row.Remove();
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Reject MoveFrom/MoveTo — keep MoveFrom content (original position), remove MoveTo
|
||||
foreach (var moveTo in body.Descendants<MoveToRun>().ToList())
|
||||
{
|
||||
moveTo.Remove();
|
||||
}
|
||||
foreach (var moveFrom in body.Descendants<MoveFromRun>().ToList())
|
||||
{
|
||||
var parent = moveFrom.Parent;
|
||||
if (parent == null) continue;
|
||||
|
||||
// Convert any DeletedText back to Text in MoveFrom runs
|
||||
foreach (var run in moveFrom.Elements<Run>().ToList())
|
||||
{
|
||||
foreach (var delText in run.Elements<DeletedText>().ToList())
|
||||
{
|
||||
var text = new Text(delText.Text ?? "") { Space = SpaceProcessingModeValues.Preserve };
|
||||
delText.InsertAfterSelf(text);
|
||||
delText.Remove();
|
||||
}
|
||||
}
|
||||
|
||||
var children = moveFrom.ChildElements.ToList();
|
||||
foreach (var child in children)
|
||||
{
|
||||
child.Remove();
|
||||
moveFrom.InsertBeforeSelf(child);
|
||||
}
|
||||
moveFrom.Remove();
|
||||
}
|
||||
|
||||
// 7. Remove move range markers
|
||||
foreach (var marker in body.Descendants<MoveFromRangeStart>().ToList()) marker.Remove();
|
||||
foreach (var marker in body.Descendants<MoveFromRangeEnd>().ToList()) marker.Remove();
|
||||
foreach (var marker in body.Descendants<MoveToRangeStart>().ToList()) marker.Remove();
|
||||
foreach (var marker in body.Descendants<MoveToRangeEnd>().ToList()) marker.Remove();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 9. InsertMoveFromTo — MoveFrom + MoveTo blocks
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Creates a tracked move operation (text moved from one location to another).
|
||||
///
|
||||
/// A move consists of:
|
||||
/// - MoveFromRangeStart/End markers around the original location
|
||||
/// - MoveFrom (w:moveFrom) containing the original text with w:delText
|
||||
/// - MoveToRangeStart/End markers around the new location
|
||||
/// - MoveTo (w:moveTo) containing the moved text with w:t
|
||||
/// - Both share the same name attribute to link them
|
||||
///
|
||||
/// ╔══════════════════════════════════════════════════════════════╗
|
||||
/// ║ MoveFrom uses w:delText (like w:del — text is "leaving") ║
|
||||
/// ║ MoveTo uses w:t (like w:ins — text is "arriving") ║
|
||||
/// ╚══════════════════════════════════════════════════════════════╝
|
||||
/// </summary>
|
||||
public static void InsertMoveFromTo(Body body, string movedText, string author)
|
||||
{
|
||||
string moveId = GenerateRevisionId();
|
||||
string moveId2 = GenerateRevisionId();
|
||||
string moveName = "move" + moveId;
|
||||
|
||||
// ── MoveFrom paragraph (original location — text shown with strikethrough) ──
|
||||
var moveFromPara = new Paragraph();
|
||||
|
||||
moveFromPara.Append(new MoveFromRangeStart
|
||||
{
|
||||
Id = moveId,
|
||||
Author = author,
|
||||
Date = DateTime.UtcNow,
|
||||
Name = moveName
|
||||
});
|
||||
|
||||
var moveFrom = new MoveFromRun
|
||||
{
|
||||
Id = GenerateRevisionId(),
|
||||
Author = author,
|
||||
Date = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// MoveFrom uses DeletedText (w:delText), NOT Text (w:t)
|
||||
// The text is visually struck through in Word
|
||||
moveFrom.Append(new Run(
|
||||
new DeletedText(movedText) { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
moveFromPara.Append(moveFrom);
|
||||
moveFromPara.Append(new MoveFromRangeEnd { Id = moveId });
|
||||
|
||||
body.Append(moveFromPara);
|
||||
|
||||
// ── MoveTo paragraph (destination — text shown with double underline) ──
|
||||
var moveToPara = new Paragraph();
|
||||
|
||||
moveToPara.Append(new MoveToRangeStart
|
||||
{
|
||||
Id = moveId2,
|
||||
Author = author,
|
||||
Date = DateTime.UtcNow,
|
||||
Name = moveName
|
||||
});
|
||||
|
||||
var moveTo = new MoveToRun
|
||||
{
|
||||
Id = GenerateRevisionId(),
|
||||
Author = author,
|
||||
Date = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// MoveTo uses Text (w:t), NOT DeletedText (w:delText)
|
||||
// The text is visually double-underlined in green in Word
|
||||
moveTo.Append(new Run(
|
||||
new Text(movedText) { Space = SpaceProcessingModeValues.Preserve }));
|
||||
|
||||
moveToPara.Append(moveTo);
|
||||
moveToPara.Append(new MoveToRangeEnd { Id = moveId2 });
|
||||
|
||||
body.Append(moveToPara);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 10. GenerateRevisionId — unique ID pattern
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Generates a unique revision ID string.
|
||||
///
|
||||
/// Revision IDs (w:id) must be unique across ALL revision elements in the document:
|
||||
/// ins, del, rPrChange, pPrChange, moveFrom, moveTo, table row ins/del, etc.
|
||||
///
|
||||
/// Word uses simple incrementing integers starting from 0.
|
||||
/// When programmatically adding revisions to an existing document,
|
||||
/// first scan for the maximum existing ID and start from there.
|
||||
///
|
||||
/// For new documents, a simple counter suffices.
|
||||
/// For existing documents, use:
|
||||
/// int maxId = body.Descendants()
|
||||
/// .SelectMany(e => e.GetAttributes())
|
||||
/// .Where(a => a.LocalName == "id")
|
||||
/// .Select(a => int.TryParse(a.Value, out int v) ? v : 0)
|
||||
/// .DefaultIfEmpty(0)
|
||||
/// .Max();
|
||||
/// </summary>
|
||||
public static string GenerateRevisionId()
|
||||
{
|
||||
return Interlocked.Increment(ref s_revisionCounter).ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Typography;
|
||||
|
||||
/// <summary>
|
||||
/// CJK mixed typography helpers for East Asian font and paragraph configuration.
|
||||
/// </summary>
|
||||
public static class CjkHelper
|
||||
{
|
||||
public const string DefaultSimplifiedChinese = "SimSun";
|
||||
public const string DefaultJapanese = "MS Mincho";
|
||||
public const string DefaultKorean = "Batang";
|
||||
|
||||
/// <summary>
|
||||
/// Sets the East Asia font on run properties.
|
||||
/// </summary>
|
||||
public static void SetEastAsiaFont(RunProperties rPr, string fontName)
|
||||
{
|
||||
var fonts = rPr.RunFonts;
|
||||
if (fonts == null)
|
||||
{
|
||||
fonts = new RunFonts();
|
||||
rPr.RunFonts = fonts;
|
||||
}
|
||||
fonts.EastAsia = fontName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures CJK-appropriate paragraph properties.
|
||||
/// </summary>
|
||||
public static void ConfigureCjkParagraph(ParagraphProperties pPr)
|
||||
{
|
||||
// Enable word wrap for CJK
|
||||
pPr.WordWrap = new WordWrap { Val = true };
|
||||
// Allow auto space between CJK and Latin/numbers
|
||||
pPr.AutoSpaceDE = new AutoSpaceDE { Val = true };
|
||||
pPr.AutoSpaceDN = new AutoSpaceDN { Val = true };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace MiniMaxAIDocx.Core.Typography;
|
||||
|
||||
public record FontConfig(
|
||||
string BodyFont,
|
||||
string HeadingFont,
|
||||
double BodySize,
|
||||
double Heading1Size,
|
||||
double Heading2Size,
|
||||
double Heading3Size,
|
||||
double Heading4Size,
|
||||
double Heading5Size,
|
||||
double Heading6Size,
|
||||
double LineSpacing);
|
||||
|
||||
/// <summary>
|
||||
/// Default font configurations by document type.
|
||||
/// </summary>
|
||||
public static class FontDefaults
|
||||
{
|
||||
public static FontConfig Report => new("Calibri", "Calibri Light", 11.0, 26.0, 20.0, 16.0, 14.0, 12.0, 11.0, 1.15);
|
||||
public static FontConfig Letter => new("Calibri", "Calibri", 11.0, 16.0, 14.0, 12.0, 11.0, 11.0, 11.0, 1.0);
|
||||
public static FontConfig Memo => new("Arial", "Arial", 11.0, 16.0, 14.0, 12.0, 11.0, 11.0, 11.0, 1.15);
|
||||
public static FontConfig Academic => new("Times New Roman", "Times New Roman", 12.0, 16.0, 14.0, 13.0, 12.0, 12.0, 12.0, 2.0);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace MiniMaxAIDocx.Core.Typography;
|
||||
|
||||
public record PageSize(int WidthDxa, int HeightDxa);
|
||||
public record MarginConfig(int TopDxa, int BottomDxa, int LeftDxa, int RightDxa);
|
||||
|
||||
/// <summary>
|
||||
/// Standard page sizes and margin presets in DXA units.
|
||||
/// </summary>
|
||||
public static class PageSizes
|
||||
{
|
||||
public static PageSize Letter => new(12240, 15840); // 8.5 x 11 inches
|
||||
public static PageSize A4 => new(11906, 16838); // 210 x 297 mm
|
||||
public static PageSize Legal => new(12240, 20160); // 8.5 x 14 inches
|
||||
public static PageSize A3 => new(16838, 23811); // 297 x 420 mm
|
||||
public static PageSize A5 => new(8391, 11906); // 148 x 210 mm
|
||||
|
||||
public static MarginConfig StandardMargins => new(1440, 1440, 1440, 1440); // 1 inch all
|
||||
public static MarginConfig NarrowMargins => new(720, 720, 720, 720); // 0.5 inch all
|
||||
public static MarginConfig WideMargins => new(1440, 1440, 2160, 2160); // 1" top/bottom, 1.5" left/right
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
using System.IO.Compression;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Validation;
|
||||
|
||||
public class BusinessRuleValidator
|
||||
{
|
||||
private static readonly XNamespace W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||
private static readonly XNamespace R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
||||
private static readonly XNamespace WP = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
|
||||
private static readonly XNamespace A = "http://schemas.openxmlformats.org/drawingml/2006/main";
|
||||
|
||||
private const int MinMarginDxa = 360; // 0.25 inch
|
||||
private const int MaxMarginDxa = 4320; // 3 inches
|
||||
private const int MinBodyFontHps = 16; // 8pt
|
||||
private const int MaxBodyFontHps = 144; // 72pt
|
||||
private const int MinHeadingFontHps = 20; // 10pt
|
||||
private const int MaxHeadingFontHps = 192; // 96pt
|
||||
|
||||
public ValidationResult Validate(string docxPath)
|
||||
{
|
||||
var result = new ValidationResult();
|
||||
|
||||
using var zip = ZipFile.OpenRead(docxPath);
|
||||
var docEntry = zip.GetEntry("word/document.xml")
|
||||
?? throw new InvalidOperationException("Missing word/document.xml");
|
||||
|
||||
var doc = LoadXml(docEntry);
|
||||
var body = doc.Root?.Element(W + "body");
|
||||
if (body == null)
|
||||
{
|
||||
result.Errors.Add(Error("Document has no body element"));
|
||||
return result;
|
||||
}
|
||||
|
||||
ValidateMargins(body, result);
|
||||
ValidateFontSizes(body, result);
|
||||
ValidateHeadingHierarchy(body, result);
|
||||
ValidateTableColumnWidths(body, result);
|
||||
ValidateRelationships(zip, doc, result);
|
||||
ValidateComments(zip, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ValidateMargins(XElement body, ValidationResult result)
|
||||
{
|
||||
foreach (var sectPr in body.Descendants(W + "sectPr"))
|
||||
{
|
||||
var pgMar = sectPr.Element(W + "pgMar");
|
||||
if (pgMar == null) continue;
|
||||
|
||||
foreach (var attr in new[] { "top", "bottom", "left", "right" })
|
||||
{
|
||||
var val = (string?)pgMar.Attribute(W + attr);
|
||||
if (val != null && int.TryParse(val, out var dxa))
|
||||
{
|
||||
var absDxa = Math.Abs(dxa);
|
||||
if (absDxa < MinMarginDxa)
|
||||
result.Errors.Add(Error($"Margin '{attr}' is {absDxa} DXA ({absDxa / 1440.0:F2}\"), below minimum {MinMarginDxa} DXA"));
|
||||
if (absDxa > MaxMarginDxa)
|
||||
result.Warnings.Add(Warning($"Margin '{attr}' is {absDxa} DXA ({absDxa / 1440.0:F2}\"), above maximum {MaxMarginDxa} DXA"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateFontSizes(XElement body, ValidationResult result)
|
||||
{
|
||||
foreach (var p in body.Descendants(W + "p"))
|
||||
{
|
||||
var pStyle = p.Element(W + "pPr")?.Element(W + "pStyle")?.Attribute(W + "val")?.Value;
|
||||
bool isHeading = pStyle?.StartsWith("Heading", StringComparison.OrdinalIgnoreCase) == true;
|
||||
|
||||
foreach (var rPr in p.Descendants(W + "rPr"))
|
||||
{
|
||||
var szEl = rPr.Element(W + "sz");
|
||||
var val = (string?)szEl?.Attribute(W + "val");
|
||||
if (val != null && int.TryParse(val, out var hps))
|
||||
{
|
||||
int min = isHeading ? MinHeadingFontHps : MinBodyFontHps;
|
||||
int max = isHeading ? MaxHeadingFontHps : MaxBodyFontHps;
|
||||
if (hps < min || hps > max)
|
||||
result.Warnings.Add(Warning($"Font size {hps / 2.0}pt is outside {(isHeading ? "heading" : "body")} range ({min / 2}-{max / 2}pt)"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateHeadingHierarchy(XElement body, ValidationResult result)
|
||||
{
|
||||
int lastLevel = 0;
|
||||
foreach (var p in body.Descendants(W + "p"))
|
||||
{
|
||||
var pStyle = p.Element(W + "pPr")?.Element(W + "pStyle")?.Attribute(W + "val")?.Value;
|
||||
if (pStyle == null) continue;
|
||||
|
||||
int level = 0;
|
||||
if (pStyle.StartsWith("Heading", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var numPart = pStyle.AsSpan(7);
|
||||
if (int.TryParse(numPart, out var parsed)) level = parsed;
|
||||
}
|
||||
|
||||
if (level > 0)
|
||||
{
|
||||
if (lastLevel > 0 && level > lastLevel + 1)
|
||||
result.Warnings.Add(Warning($"Heading level skips from {lastLevel} to {level} (missing Heading{lastLevel + 1})"));
|
||||
lastLevel = level;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateTableColumnWidths(XElement body, ValidationResult result)
|
||||
{
|
||||
var sectPr = body.Element(W + "sectPr");
|
||||
if (sectPr == null) return;
|
||||
|
||||
var pgSz = sectPr.Element(W + "pgSz");
|
||||
var pgMar = sectPr.Element(W + "pgMar");
|
||||
if (pgSz == null || pgMar == null) return;
|
||||
|
||||
if (!int.TryParse((string?)pgSz.Attribute(W + "w"), out var pageWidth)) return;
|
||||
int.TryParse((string?)pgMar.Attribute(W + "left"), out var marginLeft);
|
||||
int.TryParse((string?)pgMar.Attribute(W + "right"), out var marginRight);
|
||||
var contentWidth = pageWidth - marginLeft - marginRight;
|
||||
|
||||
int tableIndex = 0;
|
||||
foreach (var tbl in body.Descendants(W + "tbl"))
|
||||
{
|
||||
tableIndex++;
|
||||
var firstRow = tbl.Element(W + "tr");
|
||||
if (firstRow == null) continue;
|
||||
|
||||
int totalWidth = 0;
|
||||
foreach (var tc in firstRow.Elements(W + "tc"))
|
||||
{
|
||||
var tcW = tc.Element(W + "tcPr")?.Element(W + "tcW");
|
||||
var w = (string?)tcW?.Attribute(W + "w");
|
||||
if (w != null && int.TryParse(w, out var cellWidth))
|
||||
totalWidth += cellWidth;
|
||||
}
|
||||
|
||||
if (totalWidth > 0)
|
||||
{
|
||||
var tolerance = contentWidth * 0.02;
|
||||
if (Math.Abs(totalWidth - contentWidth) > tolerance)
|
||||
result.Warnings.Add(Warning($"Table {tableIndex}: column widths sum to {totalWidth} DXA but content width is {contentWidth} DXA"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateRelationships(ZipArchive zip, XDocument doc, ValidationResult result)
|
||||
{
|
||||
var relsEntry = zip.GetEntry("word/_rels/document.xml.rels");
|
||||
if (relsEntry == null) return;
|
||||
|
||||
var relDoc = LoadXml(relsEntry);
|
||||
var ns = relDoc.Root?.Name.Namespace ?? XNamespace.None;
|
||||
var definedIds = new HashSet<string>();
|
||||
|
||||
foreach (var rel in relDoc.Descendants(ns + "Relationship"))
|
||||
{
|
||||
var id = (string?)rel.Attribute("Id");
|
||||
if (id != null) definedIds.Add(id);
|
||||
}
|
||||
|
||||
var referencedIds = new HashSet<string>();
|
||||
foreach (var el in doc.Descendants())
|
||||
{
|
||||
var rid = (string?)el.Attribute(R + "id") ?? (string?)el.Attribute(R + "embed");
|
||||
if (rid != null) referencedIds.Add(rid);
|
||||
}
|
||||
|
||||
foreach (var id in referencedIds.Except(definedIds))
|
||||
result.Errors.Add(Error($"Reference r:id='{id}' has no matching relationship"));
|
||||
|
||||
foreach (var id in definedIds.Except(referencedIds))
|
||||
result.Warnings.Add(Warning($"Orphaned relationship: Id='{id}' is defined but never referenced"));
|
||||
}
|
||||
|
||||
private void ValidateComments(ZipArchive zip, ValidationResult result)
|
||||
{
|
||||
var commentFiles = new[] { "word/comments.xml", "word/commentsExtended.xml", "word/commentsIds.xml", "word/commentsExtensible.xml" };
|
||||
var existing = commentFiles.Where(f => zip.GetEntry(f) != null).ToList();
|
||||
|
||||
if (existing.Count > 0 && existing.Count < 4)
|
||||
{
|
||||
var missing = commentFiles.Except(existing);
|
||||
result.Warnings.Add(Warning($"Comments partially present. Missing: {string.Join(", ", missing)}"));
|
||||
}
|
||||
|
||||
if (zip.GetEntry("word/comments.xml") is { } commentsEntry)
|
||||
{
|
||||
var commentsDoc = LoadXml(commentsEntry);
|
||||
var commentIds = commentsDoc.Descendants(W + "comment")
|
||||
.Select(c => (string?)c.Attribute(W + "id"))
|
||||
.Where(id => id != null)
|
||||
.ToHashSet();
|
||||
|
||||
if (zip.GetEntry("word/commentsExtended.xml") is { } extEntry)
|
||||
{
|
||||
var W15 = XNamespace.Get("http://schemas.microsoft.com/office/word/2012/wordml");
|
||||
var extDoc = LoadXml(extEntry);
|
||||
var extIds = extDoc.Descendants(W15 + "commentEx")
|
||||
.Select(c => (string?)c.Attribute(W15 + "paraId"))
|
||||
.Where(id => id != null)
|
||||
.ToHashSet();
|
||||
|
||||
if (commentIds.Count > 0 && extIds.Count == 0)
|
||||
result.Warnings.Add(Warning("comments.xml has entries but commentsExtended.xml has none"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static XDocument LoadXml(ZipArchiveEntry entry)
|
||||
{
|
||||
using var stream = entry.Open();
|
||||
return XDocument.Load(stream);
|
||||
}
|
||||
|
||||
private static ValidationError Error(string msg) => new() { Message = msg, Severity = "Error" };
|
||||
private static ValidationError Warning(string msg) => new() { Message = msg, Severity = "Warning" };
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
using System.IO.Compression;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Validation;
|
||||
|
||||
public class GateCheckResult
|
||||
{
|
||||
public bool Passed => Violations.Count == 0;
|
||||
public List<string> Violations { get; set; } = new();
|
||||
}
|
||||
|
||||
public class GateCheckValidator
|
||||
{
|
||||
private static readonly XNamespace W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||
|
||||
public GateCheckResult Validate(string outputDocxPath, string templateDocxPath)
|
||||
{
|
||||
var result = new GateCheckResult();
|
||||
|
||||
var templateStyles = ExtractStyles(templateDocxPath);
|
||||
var outputStyles = ExtractStyles(outputDocxPath);
|
||||
var templateSectPr = ExtractSectionProperties(templateDocxPath);
|
||||
var outputSectPr = ExtractSectionProperties(outputDocxPath);
|
||||
|
||||
// All template styles must exist in output
|
||||
foreach (var style in templateStyles)
|
||||
{
|
||||
if (!outputStyles.Contains(style))
|
||||
result.Violations.Add($"Missing style: '{style}' defined in template but absent from output");
|
||||
}
|
||||
|
||||
// Page margins must match
|
||||
if (templateSectPr.Margins != null && outputSectPr.Margins != null)
|
||||
{
|
||||
var tm = templateSectPr.Margins;
|
||||
var om = outputSectPr.Margins;
|
||||
if (tm.Top != om.Top || tm.Bottom != om.Bottom || tm.Left != om.Left || tm.Right != om.Right)
|
||||
result.Violations.Add($"Page margins mismatch: template=({tm.Top},{tm.Bottom},{tm.Left},{tm.Right}) output=({om.Top},{om.Bottom},{om.Left},{om.Right})");
|
||||
}
|
||||
|
||||
// Page size must match
|
||||
if (templateSectPr.PageWidth != outputSectPr.PageWidth || templateSectPr.PageHeight != outputSectPr.PageHeight)
|
||||
result.Violations.Add($"Page size mismatch: template=({templateSectPr.PageWidth}x{templateSectPr.PageHeight}) output=({outputSectPr.PageWidth}x{outputSectPr.PageHeight})");
|
||||
|
||||
// Default font must match
|
||||
var templateFont = ExtractDefaultFont(templateDocxPath);
|
||||
var outputFont = ExtractDefaultFont(outputDocxPath);
|
||||
if (templateFont != null && outputFont != null && templateFont != outputFont)
|
||||
result.Violations.Add($"Default font mismatch: template='{templateFont}' output='{outputFont}'");
|
||||
|
||||
// Heading font hierarchy consistency
|
||||
ValidateHeadingFontHierarchy(outputDocxPath, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private HashSet<string> ExtractStyles(string docxPath)
|
||||
{
|
||||
using var zip = ZipFile.OpenRead(docxPath);
|
||||
var entry = zip.GetEntry("word/styles.xml");
|
||||
if (entry == null) return new();
|
||||
|
||||
using var stream = entry.Open();
|
||||
var doc = XDocument.Load(stream);
|
||||
return doc.Descendants(W + "style")
|
||||
.Select(s => (string?)s.Attribute(W + "styleId"))
|
||||
.Where(id => id != null)
|
||||
.ToHashSet()!;
|
||||
}
|
||||
|
||||
private record SectionProps(int PageWidth, int PageHeight, MarginInfo? Margins);
|
||||
private record MarginInfo(int Top, int Bottom, int Left, int Right);
|
||||
|
||||
private SectionProps ExtractSectionProperties(string docxPath)
|
||||
{
|
||||
using var zip = ZipFile.OpenRead(docxPath);
|
||||
var entry = zip.GetEntry("word/document.xml")!;
|
||||
using var stream = entry.Open();
|
||||
var doc = XDocument.Load(stream);
|
||||
|
||||
var sectPr = doc.Descendants(W + "sectPr").LastOrDefault();
|
||||
if (sectPr == null) return new(0, 0, null);
|
||||
|
||||
int.TryParse((string?)sectPr.Element(W + "pgSz")?.Attribute(W + "w"), out var pw);
|
||||
int.TryParse((string?)sectPr.Element(W + "pgSz")?.Attribute(W + "h"), out var ph);
|
||||
|
||||
var pgMar = sectPr.Element(W + "pgMar");
|
||||
MarginInfo? margins = null;
|
||||
if (pgMar != null)
|
||||
{
|
||||
int.TryParse((string?)pgMar.Attribute(W + "top"), out var t);
|
||||
int.TryParse((string?)pgMar.Attribute(W + "bottom"), out var b);
|
||||
int.TryParse((string?)pgMar.Attribute(W + "left"), out var l);
|
||||
int.TryParse((string?)pgMar.Attribute(W + "right"), out var r);
|
||||
margins = new(t, b, l, r);
|
||||
}
|
||||
|
||||
return new(pw, ph, margins);
|
||||
}
|
||||
|
||||
private string? ExtractDefaultFont(string docxPath)
|
||||
{
|
||||
using var zip = ZipFile.OpenRead(docxPath);
|
||||
var entry = zip.GetEntry("word/styles.xml");
|
||||
if (entry == null) return null;
|
||||
|
||||
using var stream = entry.Open();
|
||||
var doc = XDocument.Load(stream);
|
||||
|
||||
var defaultStyle = doc.Descendants(W + "style")
|
||||
.FirstOrDefault(s => (string?)s.Attribute(W + "type") == "paragraph"
|
||||
&& (string?)s.Attribute(W + "default") == "1");
|
||||
|
||||
return (string?)defaultStyle?.Descendants(W + "rFonts").FirstOrDefault()?.Attribute(W + "ascii");
|
||||
}
|
||||
|
||||
private void ValidateHeadingFontHierarchy(string docxPath, GateCheckResult result)
|
||||
{
|
||||
using var zip = ZipFile.OpenRead(docxPath);
|
||||
var entry = zip.GetEntry("word/styles.xml");
|
||||
if (entry == null) return;
|
||||
|
||||
using var stream = entry.Open();
|
||||
var doc = XDocument.Load(stream);
|
||||
|
||||
var headingSizes = new SortedDictionary<int, int>();
|
||||
foreach (var style in doc.Descendants(W + "style"))
|
||||
{
|
||||
var id = (string?)style.Attribute(W + "styleId");
|
||||
if (id == null || !id.StartsWith("Heading", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
|
||||
var numPart = id.AsSpan(7);
|
||||
if (!int.TryParse(numPart, out var level)) continue;
|
||||
|
||||
var sz = (string?)style.Descendants(W + "sz").FirstOrDefault()?.Attribute(W + "val");
|
||||
if (sz != null && int.TryParse(sz, out var hps))
|
||||
headingSizes[level] = hps;
|
||||
}
|
||||
|
||||
int prevSize = int.MaxValue;
|
||||
foreach (var (level, size) in headingSizes)
|
||||
{
|
||||
if (size > prevSize)
|
||||
result.Violations.Add($"Heading{level} ({size / 2}pt) is larger than a higher-level heading ({prevSize / 2}pt)");
|
||||
prevSize = size;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace MiniMaxAIDocx.Core.Validation;
|
||||
|
||||
public class ValidationResult
|
||||
{
|
||||
public bool IsValid => Errors.Count == 0;
|
||||
public List<ValidationError> Errors { get; set; } = new();
|
||||
public List<ValidationError> Warnings { get; set; } = new();
|
||||
|
||||
public void Merge(ValidationResult other)
|
||||
{
|
||||
Errors.AddRange(other.Errors);
|
||||
Warnings.AddRange(other.Warnings);
|
||||
}
|
||||
}
|
||||
|
||||
public class ValidationError
|
||||
{
|
||||
public int LineNumber { get; set; }
|
||||
public int LinePosition { get; set; }
|
||||
public string Element { get; set; } = "";
|
||||
public string Message { get; set; } = "";
|
||||
public string Severity { get; set; } = "Error";
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.IO.Compression;
|
||||
using System.Xml;
|
||||
using System.Xml.Schema;
|
||||
|
||||
namespace MiniMaxAIDocx.Core.Validation;
|
||||
|
||||
public class XsdValidator
|
||||
{
|
||||
public ValidationResult Validate(string docxPath, string xsdPath)
|
||||
{
|
||||
using var zip = ZipFile.OpenRead(docxPath);
|
||||
var entry = zip.GetEntry("word/document.xml")
|
||||
?? throw new InvalidOperationException("DOCX does not contain word/document.xml");
|
||||
|
||||
using var stream = entry.Open();
|
||||
using var reader = new StreamReader(stream);
|
||||
var xmlContent = reader.ReadToEnd();
|
||||
|
||||
return ValidateXml(xmlContent, xsdPath);
|
||||
}
|
||||
|
||||
public ValidationResult ValidateXml(string xmlContent, string xsdPath)
|
||||
{
|
||||
var result = new ValidationResult();
|
||||
var settings = new XmlReaderSettings();
|
||||
|
||||
var schemaSet = new XmlSchemaSet();
|
||||
schemaSet.Add(null, xsdPath);
|
||||
settings.Schemas = schemaSet;
|
||||
settings.ValidationType = ValidationType.Schema;
|
||||
settings.ValidationFlags |= XmlSchemaValidationFlags.ReportValidationWarnings;
|
||||
|
||||
settings.ValidationEventHandler += (sender, e) =>
|
||||
{
|
||||
var error = new ValidationError
|
||||
{
|
||||
LineNumber = e.Exception?.LineNumber ?? 0,
|
||||
LinePosition = e.Exception?.LinePosition ?? 0,
|
||||
Message = e.Message,
|
||||
Severity = e.Severity == XmlSeverityType.Warning ? "Warning" : "Error"
|
||||
};
|
||||
|
||||
if (e.Severity == XmlSeverityType.Warning)
|
||||
result.Warnings.Add(error);
|
||||
else
|
||||
result.Errors.Add(error);
|
||||
};
|
||||
|
||||
using var stringReader = new StringReader(xmlContent);
|
||||
using var xmlReader = XmlReader.Create(stringReader, settings);
|
||||
|
||||
try
|
||||
{
|
||||
while (xmlReader.Read()) { }
|
||||
}
|
||||
catch (XmlException ex)
|
||||
{
|
||||
result.Errors.Add(new ValidationError
|
||||
{
|
||||
LineNumber = ex.LineNumber,
|
||||
LinePosition = ex.LinePosition,
|
||||
Message = $"XML parse error: {ex.Message}",
|
||||
Severity = "Error"
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
4
skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.slnx
Normal file
4
skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.slnx
Normal file
@@ -0,0 +1,4 @@
|
||||
<Solution>
|
||||
<Project Path="MiniMaxAIDocx.Cli/MiniMaxAIDocx.Cli.csproj" />
|
||||
<Project Path="MiniMaxAIDocx.Core/MiniMaxAIDocx.Core.csproj" />
|
||||
</Solution>
|
||||
196
skills/minimax-docx/scripts/env_check.sh
Executable file
196
skills/minimax-docx/scripts/env_check.sh
Executable file
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env bash
|
||||
# minimax-docx Quick Environment Check
|
||||
# Cross-platform: macOS, Linux, WSL, Git Bash
|
||||
# Run this BEFORE any minimax-docx operation. Use setup.sh for initial installation.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DOTNET_DIR="$SCRIPT_DIR/dotnet"
|
||||
|
||||
# Force English output for dotnet CLI
|
||||
export DOTNET_CLI_UI_LANGUAGE=en
|
||||
|
||||
echo "=== minimax-docx Environment Check ==="
|
||||
echo ""
|
||||
|
||||
STATUS="READY"
|
||||
WARNINGS=0
|
||||
|
||||
# --- Detect platform ---
|
||||
OS="unknown"
|
||||
case "$(uname -s)" in
|
||||
Darwin) OS="macos" ;;
|
||||
Linux)
|
||||
OS="linux"
|
||||
grep -qi microsoft /proc/version 2>/dev/null && OS="wsl"
|
||||
;;
|
||||
MINGW*|MSYS*|CYGWIN*) OS="windows-shell" ;;
|
||||
esac
|
||||
|
||||
# --- Critical: .NET SDK ---
|
||||
if ! command -v dotnet &>/dev/null; then
|
||||
printf "[FAIL] %-14s not found\n" "dotnet"
|
||||
echo ""
|
||||
echo " .NET SDK is REQUIRED. Install it:"
|
||||
case "$OS" in
|
||||
macos) echo " brew install --cask dotnet-sdk" ;;
|
||||
linux|wsl)
|
||||
echo " # Option 1: Microsoft install script"
|
||||
echo " wget https://dot.net/v1/dotnet-install.sh -O /tmp/dotnet-install.sh"
|
||||
echo " chmod +x /tmp/dotnet-install.sh && /tmp/dotnet-install.sh --channel 8.0"
|
||||
echo " # Option 2 (Ubuntu/Debian): sudo apt-get install -y dotnet-sdk-8.0"
|
||||
;;
|
||||
windows-shell) echo " winget install Microsoft.DotNet.SDK.8" ;;
|
||||
*) echo " https://dotnet.microsoft.com/download" ;;
|
||||
esac
|
||||
echo ""
|
||||
echo " Or run the full setup: bash scripts/setup.sh"
|
||||
echo ""
|
||||
STATUS="NOT READY"
|
||||
else
|
||||
local_ver=$(dotnet --version 2>/dev/null || echo "0.0.0")
|
||||
local_major="${local_ver%%.*}"
|
||||
if [ "$local_major" -ge 8 ] 2>/dev/null; then
|
||||
printf "[OK] %-14s %s (>= 8.0)\n" "dotnet" "$local_ver"
|
||||
else
|
||||
printf "[FAIL] %-14s %s (requires >= 8.0)\n" "dotnet" "$local_ver"
|
||||
STATUS="NOT READY"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Critical: NuGet packages ---
|
||||
if [ -d "$DOTNET_DIR" ]; then
|
||||
if [ -f "$DOTNET_DIR/MiniMaxAIDocx.Cli/bin/Debug/net10.0/MiniMaxAIDocx.Cli.dll" ] || \
|
||||
[ -f "$DOTNET_DIR/MiniMaxAIDocx.Cli/bin/Debug/net8.0/MiniMaxAIDocx.Cli.dll" ]; then
|
||||
printf "[OK] %-14s built\n" "project"
|
||||
else
|
||||
# Try restore + build
|
||||
if dotnet restore "$DOTNET_DIR" --verbosity quiet &>/dev/null; then
|
||||
printf "[OK] %-14s packages restored\n" "nuget"
|
||||
if dotnet build "$DOTNET_DIR" --verbosity quiet --no-restore &>/dev/null; then
|
||||
printf "[OK] %-14s build succeeded\n" "project"
|
||||
else
|
||||
printf "[FAIL] %-14s build failed (run: dotnet build %s)\n" "project" "$DOTNET_DIR"
|
||||
STATUS="NOT READY"
|
||||
fi
|
||||
else
|
||||
printf "[FAIL] %-14s restore failed\n" "nuget"
|
||||
echo ""
|
||||
echo " Common causes:"
|
||||
echo " - No internet access (NuGet needs to download packages)"
|
||||
echo " - Corporate proxy blocking nuget.org"
|
||||
echo " - SSL certificate issues (try: dotnet nuget list source)"
|
||||
echo ""
|
||||
STATUS="NOT READY"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
printf "[FAIL] %-14s directory not found: %s\n" "project" "$DOTNET_DIR"
|
||||
STATUS="NOT READY"
|
||||
fi
|
||||
|
||||
# --- Optional: pandoc ---
|
||||
if command -v pandoc &>/dev/null; then
|
||||
pandoc_ver=$(pandoc --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1 || echo "?")
|
||||
printf "[OK] %-14s %s (content preview)\n" "pandoc" "$pandoc_ver"
|
||||
else
|
||||
printf "[WARN] %-14s not found — docx_preview.sh will use fallback\n" "pandoc"
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
case "$OS" in
|
||||
macos) echo " Install: brew install pandoc" ;;
|
||||
linux|wsl) echo " Install: sudo apt-get install pandoc # or dnf/pacman" ;;
|
||||
windows-shell) echo " Install: winget install JohnMacFarlane.Pandoc" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# --- Optional: LibreOffice ---
|
||||
if command -v soffice &>/dev/null; then
|
||||
soffice_ver=$(soffice --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1 || echo "?")
|
||||
printf "[OK] %-14s %s (.doc conversion)\n" "soffice" "$soffice_ver"
|
||||
else
|
||||
# Check common paths
|
||||
soffice_found=false
|
||||
for p in \
|
||||
"/Applications/LibreOffice.app/Contents/MacOS/soffice" \
|
||||
"/usr/lib/libreoffice/program/soffice" \
|
||||
"/snap/bin/libreoffice" \
|
||||
"/opt/libreoffice/program/soffice"; do
|
||||
if [ -x "$p" ]; then
|
||||
printf "[OK] %-14s found at %s (.doc conversion)\n" "soffice" "$p"
|
||||
soffice_found=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if ! $soffice_found; then
|
||||
printf "[WARN] %-14s not found — .doc files cannot be converted\n" "soffice"
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
case "$OS" in
|
||||
macos) echo " Install: brew install --cask libreoffice" ;;
|
||||
linux|wsl) echo " Install: sudo apt-get install libreoffice-core" ;;
|
||||
windows-shell) echo " Install: winget install TheDocumentFoundation.LibreOffice" ;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Optional: zip/unzip ---
|
||||
zip_ok=true
|
||||
if ! command -v zip &>/dev/null; then
|
||||
printf "[WARN] %-14s not found (optional, .NET handles DOCX natively)\n" "zip"
|
||||
zip_ok=false
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
fi
|
||||
if ! command -v unzip &>/dev/null; then
|
||||
printf "[WARN] %-14s not found (optional, .NET handles DOCX natively)\n" "unzip"
|
||||
zip_ok=false
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
fi
|
||||
if $zip_ok; then
|
||||
printf "[OK] %-14s available\n" "zip/unzip"
|
||||
fi
|
||||
|
||||
# --- Encoding check ---
|
||||
current_lang="${LANG:-}"
|
||||
if [ -n "$current_lang" ] && echo "$current_lang" | grep -qi "utf-8\|utf8"; then
|
||||
printf "[OK] %-14s %s\n" "locale" "$current_lang"
|
||||
else
|
||||
if [ -z "$current_lang" ]; then
|
||||
printf "[WARN] %-14s LANG not set (CJK text may have issues)\n" "locale"
|
||||
else
|
||||
printf "[WARN] %-14s %s (not UTF-8, CJK text may have issues)\n" "locale" "$current_lang"
|
||||
fi
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
echo " Fix: export LANG=en_US.UTF-8"
|
||||
fi
|
||||
|
||||
# --- Shell script permissions ---
|
||||
perm_issues=0
|
||||
for s in "$SCRIPT_DIR"/*.sh; do
|
||||
if [ -f "$s" ] && [ ! -x "$s" ]; then
|
||||
perm_issues=$((perm_issues + 1))
|
||||
fi
|
||||
done
|
||||
if [ "$perm_issues" -gt 0 ]; then
|
||||
printf "[WARN] %-14s %d script(s) not executable\n" "permissions" "$perm_issues"
|
||||
echo " Fix: chmod +x scripts/*.sh"
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
else
|
||||
printf "[OK] %-14s all scripts executable\n" "permissions"
|
||||
fi
|
||||
|
||||
# --- Result ---
|
||||
echo ""
|
||||
if [ "$STATUS" = "READY" ]; then
|
||||
if [ "$WARNINGS" -gt 0 ]; then
|
||||
echo "Status: READY (with $WARNINGS warning(s) — optional features may be limited)"
|
||||
else
|
||||
echo "Status: READY"
|
||||
fi
|
||||
else
|
||||
echo "Status: NOT READY"
|
||||
echo ""
|
||||
echo "Critical dependencies missing. Run the full setup:"
|
||||
echo " bash scripts/setup.sh # macOS / Linux / WSL"
|
||||
echo " powershell scripts/setup.ps1 # Windows PowerShell"
|
||||
exit 1
|
||||
fi
|
||||
274
skills/minimax-docx/scripts/setup.ps1
Normal file
274
skills/minimax-docx/scripts/setup.ps1
Normal file
@@ -0,0 +1,274 @@
|
||||
# minimax-docx Environment Setup & Initialization Script (Windows PowerShell)
|
||||
# Supports: Windows 10/11, Windows Server 2019+
|
||||
# License: MIT
|
||||
#Requires -Version 5.1
|
||||
|
||||
param(
|
||||
[switch]$Minimal,
|
||||
[switch]$SkipVerify,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$ProjectDir = Split-Path -Parent $ScriptDir
|
||||
$DotnetDir = Join-Path $ScriptDir "dotnet"
|
||||
$LogFile = Join-Path $ProjectDir ".setup.log"
|
||||
|
||||
# --- Output Helpers ---
|
||||
function Log { Write-Host "[OK] $args" -ForegroundColor Green }
|
||||
function Warn { Write-Host "[WARN] $args" -ForegroundColor Yellow }
|
||||
function Fail { Write-Host "[FAIL] $args" -ForegroundColor Red }
|
||||
function Info { Write-Host "[INFO] $args" -ForegroundColor Cyan }
|
||||
function Step { Write-Host "`n=== $args ===" -ForegroundColor Blue }
|
||||
|
||||
if ($Help) {
|
||||
Write-Host @"
|
||||
Usage: setup.ps1 [options]
|
||||
-Minimal Only install critical dependencies (skip pandoc, soffice, fonts)
|
||||
-SkipVerify Skip the verification test at the end
|
||||
-Help Show this help
|
||||
"@
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "============================================"
|
||||
Write-Host " minimax-docx Setup & Initialization (Windows)"
|
||||
Write-Host " $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
Write-Host "============================================"
|
||||
|
||||
"" | Set-Content $LogFile
|
||||
|
||||
# --- Detect Package Manager ---
|
||||
$HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue)
|
||||
$HasChoco = $null -ne (Get-Command choco -ErrorAction SilentlyContinue)
|
||||
$HasScoop = $null -ne (Get-Command scoop -ErrorAction SilentlyContinue)
|
||||
|
||||
if ($HasWinget) { Info "Package manager: winget" }
|
||||
elseif ($HasChoco) { Info "Package manager: chocolatey" }
|
||||
elseif ($HasScoop) { Info "Package manager: scoop" }
|
||||
else { Warn "No package manager found (winget/choco/scoop). Manual install may be needed." }
|
||||
|
||||
# --- .NET SDK ---
|
||||
Step "Checking .NET SDK"
|
||||
|
||||
$dotnetCmd = Get-Command dotnet -ErrorAction SilentlyContinue
|
||||
if ($dotnetCmd) {
|
||||
$dotnetVer = & dotnet --version 2>$null
|
||||
$majorVer = [int]($dotnetVer -split '\.')[0]
|
||||
if ($majorVer -ge 8) {
|
||||
Log "dotnet $dotnetVer already installed (>= 8.0 OK)"
|
||||
} else {
|
||||
Warn "dotnet $dotnetVer found but < 8.0, upgrading..."
|
||||
$dotnetCmd = $null
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $dotnetCmd -or $majorVer -lt 8) {
|
||||
Info "Installing .NET SDK..."
|
||||
if ($HasWinget) {
|
||||
winget install Microsoft.DotNet.SDK.8 --accept-source-agreements --accept-package-agreements 2>>$LogFile
|
||||
} elseif ($HasChoco) {
|
||||
choco install dotnet-sdk -y 2>>$LogFile
|
||||
} elseif ($HasScoop) {
|
||||
scoop install dotnet-sdk 2>>$LogFile
|
||||
} else {
|
||||
Fail "Cannot auto-install .NET SDK. Download from: https://dotnet.microsoft.com/download"
|
||||
Fail "After installing, restart PowerShell and re-run this script."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Refresh PATH
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
|
||||
|
||||
if (Get-Command dotnet -ErrorAction SilentlyContinue) {
|
||||
Log "dotnet $(dotnet --version) installed"
|
||||
} else {
|
||||
Fail "dotnet installation failed. Restart PowerShell and retry, or install manually."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# --- Pandoc (Optional) ---
|
||||
if (-not $Minimal) {
|
||||
Step "Checking pandoc (optional: content preview)"
|
||||
|
||||
if (Get-Command pandoc -ErrorAction SilentlyContinue) {
|
||||
$pandocVer = (pandoc --version | Select-Object -First 1) -replace '.*?(\d+\.\d+(\.\d+)?)', '$1'
|
||||
Log "pandoc $pandocVer already installed"
|
||||
} else {
|
||||
Info "Installing pandoc..."
|
||||
if ($HasWinget) { winget install JohnMacFarlane.Pandoc --accept-source-agreements 2>>$LogFile }
|
||||
elseif ($HasChoco) { choco install pandoc -y 2>>$LogFile }
|
||||
elseif ($HasScoop) { scoop install pandoc 2>>$LogFile }
|
||||
else { Warn "Install pandoc manually: https://pandoc.org/installing.html" }
|
||||
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
|
||||
|
||||
if (Get-Command pandoc -ErrorAction SilentlyContinue) {
|
||||
Log "pandoc installed"
|
||||
} else {
|
||||
Warn "pandoc not found after install (optional, will degrade gracefully)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- LibreOffice (Optional) ---
|
||||
if (-not $Minimal) {
|
||||
Step "Checking LibreOffice/soffice (optional: .doc conversion)"
|
||||
|
||||
$sofficeFound = $false
|
||||
|
||||
# Check common Windows install paths
|
||||
$sofficePaths = @(
|
||||
"C:\Program Files\LibreOffice\program\soffice.exe",
|
||||
"C:\Program Files (x86)\LibreOffice\program\soffice.exe",
|
||||
"${env:LOCALAPPDATA}\Programs\LibreOffice\program\soffice.exe"
|
||||
)
|
||||
|
||||
if (Get-Command soffice -ErrorAction SilentlyContinue) {
|
||||
Log "soffice found in PATH"
|
||||
$sofficeFound = $true
|
||||
} else {
|
||||
foreach ($p in $sofficePaths) {
|
||||
if (Test-Path $p) {
|
||||
Log "soffice found at: $p"
|
||||
Info "Tip: Add to PATH: `$env:Path += ';$(Split-Path $p)'"
|
||||
$sofficeFound = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $sofficeFound) {
|
||||
Info "Installing LibreOffice (this may take a while)..."
|
||||
if ($HasWinget) { winget install TheDocumentFoundation.LibreOffice --accept-source-agreements 2>>$LogFile }
|
||||
elseif ($HasChoco) { choco install libreoffice-fresh -y 2>>$LogFile }
|
||||
else { Warn "Install LibreOffice manually: https://www.libreoffice.org/download/" }
|
||||
}
|
||||
}
|
||||
|
||||
# --- NuGet Configuration ---
|
||||
Step "Checking NuGet configuration"
|
||||
|
||||
$nugetSources = & dotnet nuget list source 2>$null
|
||||
if ($nugetSources -match "nuget.org") {
|
||||
Log "nuget.org source is configured"
|
||||
} else {
|
||||
Warn "nuget.org not in sources. Adding..."
|
||||
& dotnet nuget add source "https://api.nuget.org/v3/index.json" --name "nuget.org" 2>>$LogFile
|
||||
}
|
||||
|
||||
# --- Encoding Check ---
|
||||
Step "Checking console encoding"
|
||||
|
||||
$currentEncoding = [Console]::OutputEncoding.EncodingName
|
||||
if ($currentEncoding -match "UTF-8|Unicode") {
|
||||
Log "Console encoding: $currentEncoding (UTF-8 compatible)"
|
||||
} else {
|
||||
Warn "Console encoding: $currentEncoding (may cause issues with CJK text)"
|
||||
Info "To fix: [Console]::OutputEncoding = [System.Text.Encoding]::UTF8"
|
||||
Info "Or set system-wide: Settings > Time & Language > Language > Administrative > Change system locale > Beta: UTF-8"
|
||||
# Apply for this session
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
Log "Set UTF-8 encoding for this session"
|
||||
}
|
||||
|
||||
# --- Font Check ---
|
||||
if (-not $Minimal) {
|
||||
Step "Checking fonts"
|
||||
|
||||
$fonts = [System.Drawing.FontFamily]::Families 2>$null
|
||||
if ($fonts) {
|
||||
$fontNames = $fonts | ForEach-Object { $_.Name }
|
||||
$hasCalibri = $fontNames -contains "Calibri"
|
||||
$hasTimes = $fontNames -contains "Times New Roman"
|
||||
$hasCJK = ($fontNames | Where-Object { $_ -match "SimSun|Microsoft YaHei|MS Mincho|Malgun Gothic" }).Count -gt 0
|
||||
|
||||
if ($hasCalibri) { Log "Western fonts: Calibri found" } else { Warn "Calibri not found (install Microsoft Office or fonts)" }
|
||||
if ($hasTimes) { Log "Western fonts: Times New Roman found" } else { Warn "Times New Roman not found" }
|
||||
if ($hasCJK) { Log "CJK fonts: available" } else { Warn "CJK fonts not found (install language packs for Chinese/Japanese/Korean)" }
|
||||
} else {
|
||||
Info "Cannot enumerate fonts (System.Drawing not loaded). Skipping font check."
|
||||
}
|
||||
}
|
||||
|
||||
# --- Build Project ---
|
||||
Step "Building minimax-docx .NET project"
|
||||
|
||||
if (-not (Test-Path $DotnetDir)) {
|
||||
Fail "Dotnet project directory not found: $DotnetDir"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Push-Location $DotnetDir
|
||||
|
||||
Info "Restoring NuGet packages..."
|
||||
$restoreResult = & dotnet restore --verbosity quiet 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Fail "NuGet restore failed:"
|
||||
$restoreResult | ForEach-Object { Fail " $_" }
|
||||
Fail "Common causes:"
|
||||
Fail " - No internet (NuGet needs to download packages)"
|
||||
Fail " - Corporate proxy/firewall blocking nuget.org"
|
||||
Fail " - Insufficient disk space"
|
||||
Fail "Try: dotnet restore --verbosity detailed"
|
||||
Pop-Location
|
||||
exit 1
|
||||
}
|
||||
Log "NuGet packages restored"
|
||||
|
||||
Info "Building project..."
|
||||
$buildResult = & dotnet build --verbosity quiet --no-restore 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Fail "Build failed:"
|
||||
$buildResult | ForEach-Object { Fail " $_" }
|
||||
Pop-Location
|
||||
exit 1
|
||||
}
|
||||
Log "Project built successfully"
|
||||
|
||||
Pop-Location
|
||||
|
||||
# --- Verification ---
|
||||
if (-not $SkipVerify) {
|
||||
Step "Verification Test"
|
||||
|
||||
$testOutput = Join-Path $env:TEMP "minimax-docx-setup-test-$PID.docx"
|
||||
|
||||
Info "Creating a test document..."
|
||||
Push-Location $DotnetDir
|
||||
$testResult = & dotnet run --project MiniMaxAIDocx.Cli -- create --type report --output $testOutput --title "Setup Test" 2>&1
|
||||
$testExitCode = $LASTEXITCODE
|
||||
Pop-Location
|
||||
|
||||
if ($testExitCode -eq 0 -and (Test-Path $testOutput)) {
|
||||
Log "Test document created: $testOutput"
|
||||
|
||||
if (Get-Command pandoc -ErrorAction SilentlyContinue) {
|
||||
$preview = & pandoc -f docx -t plain $testOutput 2>$null | Select-Object -First 3
|
||||
if ($preview) { Log "Preview working: `"$($preview -join ' ')`"" }
|
||||
}
|
||||
|
||||
Remove-Item $testOutput -Force
|
||||
Log "Test passed - minimax-docx is ready to use!"
|
||||
} else {
|
||||
Fail "Test document creation failed. Output:"
|
||||
$testResult | ForEach-Object { Fail " $_" }
|
||||
}
|
||||
}
|
||||
|
||||
# --- Summary ---
|
||||
Step "Setup Complete"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " Environment: Windows $([System.Environment]::OSVersion.Version)"
|
||||
Write-Host " .NET SDK: $(dotnet --version 2>$null)"
|
||||
$pandocInfo = if (Get-Command pandoc -ErrorAction SilentlyContinue) { pandoc --version | Select-Object -First 1 } else { "not installed (optional)" }
|
||||
Write-Host " pandoc: $pandocInfo"
|
||||
Write-Host " Project: $DotnetDir"
|
||||
Write-Host ""
|
||||
Write-Host " Usage:"
|
||||
Write-Host " dotnet run --project $DotnetDir\MiniMaxAIDocx.Cli -- create --type report --output my_report.docx"
|
||||
Write-Host ""
|
||||
Write-Host " Log file: $LogFile"
|
||||
504
skills/minimax-docx/scripts/setup.sh
Executable file
504
skills/minimax-docx/scripts/setup.sh
Executable file
@@ -0,0 +1,504 @@
|
||||
#!/usr/bin/env bash
|
||||
# minimax-docx Environment Setup & Initialization Script
|
||||
# Supports: macOS (Homebrew), Linux (apt/dnf/pacman), WSL
|
||||
# License: MIT
|
||||
set -euo pipefail
|
||||
|
||||
# Force English output for dotnet CLI
|
||||
export DOTNET_CLI_UI_LANGUAGE=en
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DOTNET_DIR="$SCRIPT_DIR/dotnet"
|
||||
LOG_FILE="$PROJECT_DIR/.setup.log"
|
||||
|
||||
# --- Colors ---
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
fail() { echo -e "${RED}[FAIL]${NC} $*"; }
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||
step() { echo -e "\n${BLUE}=== $* ===${NC}"; }
|
||||
|
||||
# --- Detect OS & Package Manager ---
|
||||
detect_platform() {
|
||||
OS="unknown"
|
||||
PKG_MGR="unknown"
|
||||
ARCH="$(uname -m)"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
OS="macos"
|
||||
if command -v brew &>/dev/null; then
|
||||
PKG_MGR="brew"
|
||||
else
|
||||
PKG_MGR="none"
|
||||
fi
|
||||
;;
|
||||
Linux)
|
||||
OS="linux"
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
case "$ID" in
|
||||
ubuntu|debian|linuxmint|pop)
|
||||
PKG_MGR="apt"
|
||||
;;
|
||||
fedora|rhel|centos|rocky|alma)
|
||||
PKG_MGR="dnf"
|
||||
;;
|
||||
arch|manjaro|endeavouros)
|
||||
PKG_MGR="pacman"
|
||||
;;
|
||||
opensuse*|sles)
|
||||
PKG_MGR="zypper"
|
||||
;;
|
||||
alpine)
|
||||
PKG_MGR="apk"
|
||||
;;
|
||||
*)
|
||||
PKG_MGR="unknown"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
# Detect WSL
|
||||
if grep -qi microsoft /proc/version 2>/dev/null; then
|
||||
OS="wsl"
|
||||
fi
|
||||
;;
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
OS="windows-git-bash"
|
||||
PKG_MGR="none"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Platform: $OS ($ARCH), Package Manager: $PKG_MGR"
|
||||
}
|
||||
|
||||
# --- .NET SDK Installation ---
|
||||
install_dotnet() {
|
||||
step "Checking .NET SDK"
|
||||
|
||||
if command -v dotnet &>/dev/null; then
|
||||
local ver
|
||||
ver=$(dotnet --version 2>/dev/null || echo "0")
|
||||
local major="${ver%%.*}"
|
||||
if [ "$major" -ge 8 ] 2>/dev/null; then
|
||||
log "dotnet $ver already installed (>= 8.0 OK)"
|
||||
return 0
|
||||
else
|
||||
warn "dotnet $ver found but < 8.0, upgrading..."
|
||||
fi
|
||||
fi
|
||||
|
||||
info "Installing .NET SDK..."
|
||||
case "$PKG_MGR" in
|
||||
brew)
|
||||
brew install --cask dotnet-sdk
|
||||
;;
|
||||
apt)
|
||||
# Microsoft package repo for Ubuntu/Debian
|
||||
if ! dpkg -l dotnet-sdk-8.0 &>/dev/null 2>&1; then
|
||||
info "Adding Microsoft package repository..."
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq wget apt-transport-https
|
||||
wget -q "https://dot.net/v1/dotnet-install.sh" -O /tmp/dotnet-install.sh
|
||||
chmod +x /tmp/dotnet-install.sh
|
||||
/tmp/dotnet-install.sh --channel 8.0 --install-dir "$HOME/.dotnet"
|
||||
export PATH="$HOME/.dotnet:$PATH"
|
||||
echo 'export PATH="$HOME/.dotnet:$PATH"' >> "$HOME/.bashrc"
|
||||
fi
|
||||
;;
|
||||
dnf)
|
||||
sudo dnf install -y dotnet-sdk-8.0
|
||||
;;
|
||||
pacman)
|
||||
sudo pacman -S --noconfirm dotnet-sdk
|
||||
;;
|
||||
zypper)
|
||||
sudo zypper install -y dotnet-sdk-8.0
|
||||
;;
|
||||
apk)
|
||||
apk add --no-cache dotnet8-sdk
|
||||
;;
|
||||
none)
|
||||
if [ "$OS" = "windows-git-bash" ]; then
|
||||
fail "On Windows, install .NET SDK from: https://dotnet.microsoft.com/download"
|
||||
fail "Then restart your terminal and re-run this script."
|
||||
return 1
|
||||
fi
|
||||
# Fallback: use Microsoft install script
|
||||
info "Using Microsoft install script..."
|
||||
wget -q "https://dot.net/v1/dotnet-install.sh" -O /tmp/dotnet-install.sh || \
|
||||
curl -sSL "https://dot.net/v1/dotnet-install.sh" -o /tmp/dotnet-install.sh
|
||||
chmod +x /tmp/dotnet-install.sh
|
||||
/tmp/dotnet-install.sh --channel 8.0 --install-dir "$HOME/.dotnet"
|
||||
export PATH="$HOME/.dotnet:$PATH"
|
||||
echo 'export PATH="$HOME/.dotnet:$PATH"' >> "$HOME/.bashrc"
|
||||
;;
|
||||
*)
|
||||
warn "Unknown package manager. Install .NET SDK manually: https://dotnet.microsoft.com/download"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Verify
|
||||
if command -v dotnet &>/dev/null; then
|
||||
log "dotnet $(dotnet --version) installed"
|
||||
else
|
||||
fail "dotnet installation failed. Install manually: https://dotnet.microsoft.com/download"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Pandoc Installation (Optional) ---
|
||||
install_pandoc() {
|
||||
step "Checking pandoc (optional: content preview)"
|
||||
|
||||
if command -v pandoc &>/dev/null; then
|
||||
log "pandoc $(pandoc --version | head -1 | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?') already installed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "Installing pandoc..."
|
||||
case "$PKG_MGR" in
|
||||
brew) brew install pandoc ;;
|
||||
apt) sudo apt-get install -y -qq pandoc ;;
|
||||
dnf) sudo dnf install -y pandoc ;;
|
||||
pacman) sudo pacman -S --noconfirm pandoc ;;
|
||||
zypper) sudo zypper install -y pandoc ;;
|
||||
apk) apk add --no-cache pandoc ;;
|
||||
*)
|
||||
warn "Cannot auto-install pandoc. Install manually: https://pandoc.org/installing.html"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v pandoc &>/dev/null; then
|
||||
log "pandoc installed"
|
||||
else
|
||||
warn "pandoc installation failed (optional, will degrade gracefully)"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- LibreOffice Installation (Optional) ---
|
||||
install_soffice() {
|
||||
step "Checking LibreOffice/soffice (optional: .doc conversion)"
|
||||
|
||||
if command -v soffice &>/dev/null; then
|
||||
log "soffice already installed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Also check common install paths
|
||||
local soffice_paths=(
|
||||
"/usr/bin/soffice"
|
||||
"/usr/local/bin/soffice"
|
||||
"/opt/libreoffice/program/soffice"
|
||||
"/snap/bin/libreoffice"
|
||||
"/Applications/LibreOffice.app/Contents/MacOS/soffice"
|
||||
)
|
||||
for p in "${soffice_paths[@]}"; do
|
||||
if [ -x "$p" ]; then
|
||||
log "soffice found at $p"
|
||||
if [ "$OS" = "macos" ] && [ "$p" = "/Applications/LibreOffice.app/Contents/MacOS/soffice" ]; then
|
||||
info "Tip: Add to PATH: ln -s '$p' /usr/local/bin/soffice"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
info "Installing LibreOffice (this may take a while)..."
|
||||
case "$PKG_MGR" in
|
||||
brew) brew install --cask libreoffice ;;
|
||||
apt) sudo apt-get install -y -qq libreoffice-core ;;
|
||||
dnf) sudo dnf install -y libreoffice-core ;;
|
||||
pacman) sudo pacman -S --noconfirm libreoffice-still ;;
|
||||
zypper) sudo zypper install -y libreoffice ;;
|
||||
apk) apk add --no-cache libreoffice ;;
|
||||
*)
|
||||
warn "Cannot auto-install LibreOffice. Install manually: https://www.libreoffice.org/download/"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v soffice &>/dev/null; then
|
||||
log "soffice installed"
|
||||
else
|
||||
warn "soffice not found after install (optional, .doc conversion unavailable)"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- zip/unzip ---
|
||||
install_zip_tools() {
|
||||
step "Checking zip/unzip"
|
||||
|
||||
local need_zip=false need_unzip=false
|
||||
command -v zip &>/dev/null && log "zip already installed" || need_zip=true
|
||||
command -v unzip &>/dev/null && log "unzip already installed" || need_unzip=true
|
||||
|
||||
if ! $need_zip && ! $need_unzip; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "Installing zip/unzip..."
|
||||
case "$PKG_MGR" in
|
||||
brew) brew install zip unzip 2>/dev/null || true ;;
|
||||
apt) sudo apt-get install -y -qq zip unzip ;;
|
||||
dnf) sudo dnf install -y zip unzip ;;
|
||||
pacman) sudo pacman -S --noconfirm zip unzip ;;
|
||||
zypper) sudo zypper install -y zip unzip ;;
|
||||
apk) apk add --no-cache zip unzip ;;
|
||||
*) warn "Install zip/unzip manually (optional, .NET handles DOCX natively)" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# --- .NET Project Build ---
|
||||
build_project() {
|
||||
step "Building minimax-docx .NET project"
|
||||
|
||||
if [ ! -d "$DOTNET_DIR" ]; then
|
||||
fail "Dotnet project directory not found: $DOTNET_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
cd "$DOTNET_DIR"
|
||||
|
||||
info "Restoring NuGet packages..."
|
||||
if ! dotnet restore --verbosity quiet 2>>"$LOG_FILE"; then
|
||||
fail "NuGet restore failed. Check network and $LOG_FILE for details."
|
||||
fail "Common causes:"
|
||||
fail " - No internet access (NuGet needs to download packages)"
|
||||
fail " - Corporate proxy blocking nuget.org"
|
||||
fail " - Disk space insufficient"
|
||||
echo ""
|
||||
fail "Try manually: cd $DOTNET_DIR && dotnet restore --verbosity detailed"
|
||||
return 1
|
||||
fi
|
||||
log "NuGet packages restored"
|
||||
|
||||
info "Building project..."
|
||||
if ! dotnet build --verbosity quiet --no-restore 2>>"$LOG_FILE"; then
|
||||
fail "Build failed. Check $LOG_FILE for details."
|
||||
fail "Try manually: cd $DOTNET_DIR && dotnet build --verbosity normal"
|
||||
return 1
|
||||
fi
|
||||
log "Project built successfully"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
}
|
||||
|
||||
# --- Shell Script Permissions ---
|
||||
fix_permissions() {
|
||||
step "Setting script permissions"
|
||||
|
||||
local scripts=(
|
||||
"$SCRIPT_DIR/env_check.sh"
|
||||
"$SCRIPT_DIR/docx_preview.sh"
|
||||
"$SCRIPT_DIR/doc_to_docx.sh"
|
||||
"$SCRIPT_DIR/setup.sh"
|
||||
)
|
||||
|
||||
for s in "${scripts[@]}"; do
|
||||
if [ -f "$s" ]; then
|
||||
chmod +x "$s"
|
||||
log "chmod +x $(basename "$s")"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# --- NuGet Proxy / Certificate Issues (Corporate Environments) ---
|
||||
check_nuget_config() {
|
||||
step "Checking NuGet configuration"
|
||||
|
||||
local nuget_config="$HOME/.nuget/NuGet/NuGet.Config"
|
||||
if [ -f "$nuget_config" ]; then
|
||||
log "NuGet config exists: $nuget_config"
|
||||
else
|
||||
info "No custom NuGet config found (using defaults)"
|
||||
fi
|
||||
|
||||
# Test NuGet connectivity
|
||||
if dotnet nuget list source 2>/dev/null | grep -q "nuget.org"; then
|
||||
log "nuget.org source is configured"
|
||||
else
|
||||
warn "nuget.org not in sources. Adding..."
|
||||
dotnet nuget add source "https://api.nuget.org/v3/index.json" --name "nuget.org" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Locale / Encoding Check ---
|
||||
check_locale() {
|
||||
step "Checking locale and encoding"
|
||||
|
||||
local current_lang="${LANG:-not set}"
|
||||
local current_lc="${LC_ALL:-not set}"
|
||||
|
||||
if echo "$current_lang" | grep -qi "utf-8\|utf8"; then
|
||||
log "Locale supports UTF-8: LANG=$current_lang"
|
||||
else
|
||||
warn "Locale may not support UTF-8: LANG=$current_lang"
|
||||
warn "CJK document processing requires UTF-8. Set: export LANG=en_US.UTF-8"
|
||||
if [ "$OS" = "linux" ] || [ "$OS" = "wsl" ]; then
|
||||
info "To fix permanently: sudo locale-gen en_US.UTF-8 && sudo update-locale LANG=en_US.UTF-8"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Font Check (for CJK and professional documents) ---
|
||||
check_fonts() {
|
||||
step "Checking fonts for document rendering"
|
||||
|
||||
if [ "$OS" = "macos" ]; then
|
||||
# macOS has good CJK support built-in
|
||||
log "macOS: built-in CJK font support (PingFang, Hiragino, Apple SD Gothic)"
|
||||
log "macOS: built-in Western fonts (Helvetica, Times, Calibri via Office)"
|
||||
if [ -d "/Applications/Microsoft Word.app" ] || [ -d "/Applications/Microsoft Office" ]; then
|
||||
log "Microsoft Office fonts available (Calibri, Cambria, etc.)"
|
||||
else
|
||||
warn "Microsoft Office not installed — Calibri/Cambria fonts may be missing"
|
||||
info "Documents will render with fallback fonts on this machine"
|
||||
info "Recipients with Office installed will see correct fonts"
|
||||
fi
|
||||
elif [ "$OS" = "linux" ] || [ "$OS" = "wsl" ]; then
|
||||
# Check for key font packages
|
||||
local missing_fonts=()
|
||||
|
||||
if ! fc-list 2>/dev/null | grep -qi "liberation\|times new roman\|calibri"; then
|
||||
missing_fonts+=("Western: liberation-fonts or msttcorefonts")
|
||||
fi
|
||||
|
||||
if ! fc-list 2>/dev/null | grep -qi "noto.*cjk\|wqy\|simsun\|pingfang"; then
|
||||
missing_fonts+=("CJK: noto-fonts-cjk or wqy-microhei")
|
||||
fi
|
||||
|
||||
if [ ${#missing_fonts[@]} -eq 0 ]; then
|
||||
log "Font support looks good"
|
||||
else
|
||||
warn "Missing fonts may affect document rendering:"
|
||||
for f in "${missing_fonts[@]}"; do
|
||||
warn " - $f"
|
||||
done
|
||||
info "Install fonts:"
|
||||
case "$PKG_MGR" in
|
||||
apt)
|
||||
info " sudo apt-get install -y fonts-liberation fonts-noto-cjk"
|
||||
info " # For MS core fonts: sudo apt-get install -y ttf-mscorefonts-installer"
|
||||
;;
|
||||
dnf)
|
||||
info " sudo dnf install -y liberation-fonts google-noto-sans-cjk-fonts"
|
||||
;;
|
||||
pacman)
|
||||
info " sudo pacman -S ttf-liberation noto-fonts-cjk"
|
||||
;;
|
||||
*)
|
||||
info " Install Liberation Fonts and Noto CJK fonts for your distribution"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Verification Run ---
|
||||
verify_installation() {
|
||||
step "Verification Test"
|
||||
|
||||
local test_output="/tmp/minimax-docx-setup-test-$$.docx"
|
||||
|
||||
info "Creating a test document..."
|
||||
if cd "$DOTNET_DIR" && dotnet run --project MiniMaxAIDocx.Cli -- create \
|
||||
--type report --output "$test_output" --title "Setup Test" 2>>"$LOG_FILE"; then
|
||||
log "Test document created: $test_output"
|
||||
|
||||
# Try preview
|
||||
if command -v pandoc &>/dev/null; then
|
||||
local preview
|
||||
preview=$(pandoc -f docx -t plain "$test_output" 2>/dev/null | head -5)
|
||||
if [ -n "$preview" ]; then
|
||||
log "Preview working: \"$preview\""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f "$test_output"
|
||||
log "Test passed — minimax-docx is ready to use!"
|
||||
else
|
||||
fail "Test document creation failed. Check $LOG_FILE for details."
|
||||
return 1
|
||||
fi
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
}
|
||||
|
||||
# --- Summary ---
|
||||
print_summary() {
|
||||
step "Setup Complete"
|
||||
|
||||
echo ""
|
||||
echo " Environment: $OS ($ARCH)"
|
||||
echo " .NET SDK: $(dotnet --version 2>/dev/null || echo 'NOT FOUND')"
|
||||
echo " pandoc: $(pandoc --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' || echo 'not installed (optional)')"
|
||||
echo " soffice: $(soffice --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' || echo 'not installed (optional)')"
|
||||
echo " Project: $DOTNET_DIR"
|
||||
echo ""
|
||||
echo " Usage:"
|
||||
echo " dotnet run --project $DOTNET_DIR/MiniMaxAIDocx.Cli -- create --type report --output my_report.docx"
|
||||
echo " bash $SCRIPT_DIR/env_check.sh # Quick environment check"
|
||||
echo ""
|
||||
echo " Log file: $LOG_FILE"
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
main() {
|
||||
echo "============================================"
|
||||
echo " minimax-docx Setup & Initialization"
|
||||
echo " $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "============================================"
|
||||
|
||||
: > "$LOG_FILE" # Clear log
|
||||
|
||||
detect_platform
|
||||
|
||||
# Parse arguments
|
||||
local SKIP_OPTIONAL=false
|
||||
local SKIP_VERIFY=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--minimal) SKIP_OPTIONAL=true ;;
|
||||
--skip-verify) SKIP_VERIFY=true ;;
|
||||
--help|-h)
|
||||
echo "Usage: setup.sh [options]"
|
||||
echo " --minimal Only install critical dependencies (skip pandoc, soffice, fonts)"
|
||||
echo " --skip-verify Skip the verification test at the end"
|
||||
echo " --help Show this help"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
install_dotnet
|
||||
install_zip_tools
|
||||
|
||||
if ! $SKIP_OPTIONAL; then
|
||||
install_pandoc
|
||||
install_soffice
|
||||
check_fonts
|
||||
fi
|
||||
|
||||
check_locale
|
||||
check_nuget_config
|
||||
fix_permissions
|
||||
build_project
|
||||
|
||||
if ! $SKIP_VERIFY; then
|
||||
verify_installation
|
||||
fi
|
||||
|
||||
print_summary
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user