Silence render-disposed spam, unbreak bundled skills, drop board wake
Three independently ugly bugs that all fed the same log flood:
* Electron's `WebFrameMain.send` writes every "Render frame was disposed"
to stderr via `console.error`. When the worker fans a second
agent-event past a reloading renderer, we produced thousands of
identical lines and stalled the main loop on stderr I/O. Added a
`mainFrame.detached` guard so the send is short-circuited before
Electron's own catch block fires, and installed a targeted filter in
`suppress-warnings.ts` (now actually imported from the entry point)
so the specific `console.error("Error sending from webFrameMain:"…)`
noise never reaches the terminal. Any other payload still surfaces.
* `dispatchAgentMessage` accepted the human-reader sentinel as a wake
target when an agent replied to `__board__` — `wakeAgentIfPossible`
then called `db.getAgent("__board__")`, threw, and the dispatcher
kept repeating on every send. Strip `HUMAN_SENDER_ID` at
`resolveMessageWakeTargets`, and keep a defense-in-depth guard in the
heartbeat so a stray caller (future automation fan-out) cannot
reintroduce the crash.
* Pi's skills loader rejects any SKILL.md whose frontmatter `name`
diverges from its parent directory. The four bundled `my-writing-style`
copies declared `name: writing-style-skill`, so every install emitted
a warning and dropped the skill. They also lived as orphan gitlinks
(mode 160000 without .gitmodules), and the global `*.md` gitignore
hid every other SKILL.md from version control. Fixed the frontmatter
to match the dir, converted the dangling submodules to plain trees,
and allowlisted `builtin-skills/**/*.md` so a fresh clone carries the
catalogue instead of silently losing it.
This commit is contained in:
1 parent
7a9a6779b0
commit
e74dafcd36
287 files changed
+59858
-10
No files matched your search
@@ -0,0 +1,590 @@
|
||||
---
|
||||
name: docx
|
||||
description: "Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files). Triggers include: any mention of 'Word doc', 'word document', '.docx', or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx files, inserting or replacing images in documents, performing find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a 'report', 'memo', 'letter', 'template', or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation."
|
||||
license: Proprietary. LICENSE.txt has complete terms
|
||||
---
|
||||
|
||||
# DOCX creation, editing, and analysis
|
||||
|
||||
## Overview
|
||||
|
||||
A .docx file is a ZIP archive containing XML files.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Approach |
|
||||
|------|----------|
|
||||
| Read/analyze content | `pandoc` or unpack for raw XML |
|
||||
| Create new document | Use `docx-js` - see Creating New Documents below |
|
||||
| Edit existing document | Unpack → edit XML → repack - see Editing Existing Documents below |
|
||||
|
||||
### Converting .doc to .docx
|
||||
|
||||
Legacy `.doc` files must be converted before editing:
|
||||
|
||||
```bash
|
||||
python scripts/office/soffice.py --headless --convert-to docx document.doc
|
||||
```
|
||||
|
||||
### Reading Content
|
||||
|
||||
```bash
|
||||
# Text extraction with tracked changes
|
||||
pandoc --track-changes=all document.docx -o output.md
|
||||
|
||||
# Raw XML access
|
||||
python scripts/office/unpack.py document.docx unpacked/
|
||||
```
|
||||
|
||||
### Converting to Images
|
||||
|
||||
```bash
|
||||
python scripts/office/soffice.py --headless --convert-to pdf document.docx
|
||||
pdftoppm -jpeg -r 150 document.pdf page
|
||||
```
|
||||
|
||||
### Accepting Tracked Changes
|
||||
|
||||
To produce a clean document with all tracked changes accepted (requires LibreOffice):
|
||||
|
||||
```bash
|
||||
python scripts/accept_changes.py input.docx output.docx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Creating New Documents
|
||||
|
||||
Generate .docx files with JavaScript, then validate. Install: `npm install -g docx`
|
||||
|
||||
### Setup
|
||||
```javascript
|
||||
const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun,
|
||||
Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink,
|
||||
InternalHyperlink, Bookmark, FootnoteReferenceRun, PositionalTab,
|
||||
PositionalTabAlignment, PositionalTabRelativeTo, PositionalTabLeader,
|
||||
TabStopType, TabStopPosition, Column, SectionType,
|
||||
TableOfContents, HeadingLevel, BorderStyle, WidthType, ShadingType,
|
||||
VerticalAlign, PageNumber, PageBreak } = require('docx');
|
||||
|
||||
const doc = new Document({ sections: [{ children: [/* content */] }] });
|
||||
Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer));
|
||||
```
|
||||
|
||||
### Validation
|
||||
After creating the file, validate it. If validation fails, unpack, fix the XML, and repack.
|
||||
```bash
|
||||
python scripts/office/validate.py doc.docx
|
||||
```
|
||||
|
||||
### Page Size
|
||||
|
||||
```javascript
|
||||
// CRITICAL: docx-js defaults to A4, not US Letter
|
||||
// Always set page size explicitly for consistent results
|
||||
sections: [{
|
||||
properties: {
|
||||
page: {
|
||||
size: {
|
||||
width: 12240, // 8.5 inches in DXA
|
||||
height: 15840 // 11 inches in DXA
|
||||
},
|
||||
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } // 1 inch margins
|
||||
}
|
||||
},
|
||||
children: [/* content */]
|
||||
}]
|
||||
```
|
||||
|
||||
**Common page sizes (DXA units, 1440 DXA = 1 inch):**
|
||||
|
||||
| Paper | Width | Height | Content Width (1" margins) |
|
||||
|-------|-------|--------|---------------------------|
|
||||
| US Letter | 12,240 | 15,840 | 9,360 |
|
||||
| A4 (default) | 11,906 | 16,838 | 9,026 |
|
||||
|
||||
**Landscape orientation:** docx-js swaps width/height internally, so pass portrait dimensions and let it handle the swap:
|
||||
```javascript
|
||||
size: {
|
||||
width: 12240, // Pass SHORT edge as width
|
||||
height: 15840, // Pass LONG edge as height
|
||||
orientation: PageOrientation.LANDSCAPE // docx-js swaps them in the XML
|
||||
},
|
||||
// Content width = 15840 - left margin - right margin (uses the long edge)
|
||||
```
|
||||
|
||||
### Styles (Override Built-in Headings)
|
||||
|
||||
Use Arial as the default font (universally supported). Keep titles black for readability.
|
||||
|
||||
```javascript
|
||||
const doc = new Document({
|
||||
styles: {
|
||||
default: { document: { run: { font: "Arial", size: 24 } } }, // 12pt default
|
||||
paragraphStyles: [
|
||||
// IMPORTANT: Use exact IDs to override built-in styles
|
||||
{ id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
|
||||
run: { size: 32, bold: true, font: "Arial" },
|
||||
paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } }, // outlineLevel required for TOC
|
||||
{ id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
|
||||
run: { size: 28, bold: true, font: "Arial" },
|
||||
paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } },
|
||||
]
|
||||
},
|
||||
sections: [{
|
||||
children: [
|
||||
new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Title")] }),
|
||||
]
|
||||
}]
|
||||
});
|
||||
```
|
||||
|
||||
### Lists (NEVER use unicode bullets)
|
||||
|
||||
```javascript
|
||||
// ❌ WRONG - never manually insert bullet characters
|
||||
new Paragraph({ children: [new TextRun("• Item")] }) // BAD
|
||||
new Paragraph({ children: [new TextRun("\u2022 Item")] }) // BAD
|
||||
|
||||
// ✅ CORRECT - use numbering config with LevelFormat.BULLET
|
||||
const doc = new Document({
|
||||
numbering: {
|
||||
config: [
|
||||
{ reference: "bullets",
|
||||
levels: [{ level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT,
|
||||
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
|
||||
{ reference: "numbers",
|
||||
levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
|
||||
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
|
||||
]
|
||||
},
|
||||
sections: [{
|
||||
children: [
|
||||
new Paragraph({ numbering: { reference: "bullets", level: 0 },
|
||||
children: [new TextRun("Bullet item")] }),
|
||||
new Paragraph({ numbering: { reference: "numbers", level: 0 },
|
||||
children: [new TextRun("Numbered item")] }),
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
// ⚠️ Each reference creates INDEPENDENT numbering
|
||||
// Same reference = continues (1,2,3 then 4,5,6)
|
||||
// Different reference = restarts (1,2,3 then 1,2,3)
|
||||
```
|
||||
|
||||
### Tables
|
||||
|
||||
**CRITICAL: Tables need dual widths** - set both `columnWidths` on the table AND `width` on each cell. Without both, tables render incorrectly on some platforms.
|
||||
|
||||
```javascript
|
||||
// CRITICAL: Always set table width for consistent rendering
|
||||
// CRITICAL: Use ShadingType.CLEAR (not SOLID) to prevent black backgrounds
|
||||
const border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" };
|
||||
const borders = { top: border, bottom: border, left: border, right: border };
|
||||
|
||||
new Table({
|
||||
width: { size: 9360, type: WidthType.DXA }, // Always use DXA (percentages break in Google Docs)
|
||||
columnWidths: [4680, 4680], // Must sum to table width (DXA: 1440 = 1 inch)
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({
|
||||
borders,
|
||||
width: { size: 4680, type: WidthType.DXA }, // Also set on each cell
|
||||
shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, // CLEAR not SOLID
|
||||
margins: { top: 80, bottom: 80, left: 120, right: 120 }, // Cell padding (internal, not added to width)
|
||||
children: [new Paragraph({ children: [new TextRun("Cell")] })]
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
**Table width calculation:**
|
||||
|
||||
Always use `WidthType.DXA` — `WidthType.PERCENTAGE` breaks in Google Docs.
|
||||
|
||||
```javascript
|
||||
// Table width = sum of columnWidths = content width
|
||||
// US Letter with 1" margins: 12240 - 2880 = 9360 DXA
|
||||
width: { size: 9360, type: WidthType.DXA },
|
||||
columnWidths: [7000, 2360] // Must sum to table width
|
||||
```
|
||||
|
||||
**Width rules:**
|
||||
- **Always use `WidthType.DXA`** — never `WidthType.PERCENTAGE` (incompatible with Google Docs)
|
||||
- Table width must equal the sum of `columnWidths`
|
||||
- Cell `width` must match corresponding `columnWidth`
|
||||
- Cell `margins` are internal padding - they reduce content area, not add to cell width
|
||||
- For full-width tables: use content width (page width minus left and right margins)
|
||||
|
||||
### Images
|
||||
|
||||
```javascript
|
||||
// CRITICAL: type parameter is REQUIRED
|
||||
new Paragraph({
|
||||
children: [new ImageRun({
|
||||
type: "png", // Required: png, jpg, jpeg, gif, bmp, svg
|
||||
data: fs.readFileSync("image.png"),
|
||||
transformation: { width: 200, height: 150 },
|
||||
altText: { title: "Title", description: "Desc", name: "Name" } // All three required
|
||||
})]
|
||||
})
|
||||
```
|
||||
|
||||
### Page Breaks
|
||||
|
||||
```javascript
|
||||
// CRITICAL: PageBreak must be inside a Paragraph
|
||||
new Paragraph({ children: [new PageBreak()] })
|
||||
|
||||
// Or use pageBreakBefore
|
||||
new Paragraph({ pageBreakBefore: true, children: [new TextRun("New page")] })
|
||||
```
|
||||
|
||||
### Hyperlinks
|
||||
|
||||
```javascript
|
||||
// External link
|
||||
new Paragraph({
|
||||
children: [new ExternalHyperlink({
|
||||
children: [new TextRun({ text: "Click here", style: "Hyperlink" })],
|
||||
link: "https://example.com",
|
||||
})]
|
||||
})
|
||||
|
||||
// Internal link (bookmark + reference)
|
||||
// 1. Create bookmark at destination
|
||||
new Paragraph({ heading: HeadingLevel.HEADING_1, children: [
|
||||
new Bookmark({ id: "chapter1", children: [new TextRun("Chapter 1")] }),
|
||||
]})
|
||||
// 2. Link to it
|
||||
new Paragraph({ children: [new InternalHyperlink({
|
||||
children: [new TextRun({ text: "See Chapter 1", style: "Hyperlink" })],
|
||||
anchor: "chapter1",
|
||||
})]})
|
||||
```
|
||||
|
||||
### Footnotes
|
||||
|
||||
```javascript
|
||||
const doc = new Document({
|
||||
footnotes: {
|
||||
1: { children: [new Paragraph("Source: Annual Report 2024")] },
|
||||
2: { children: [new Paragraph("See appendix for methodology")] },
|
||||
},
|
||||
sections: [{
|
||||
children: [new Paragraph({
|
||||
children: [
|
||||
new TextRun("Revenue grew 15%"),
|
||||
new FootnoteReferenceRun(1),
|
||||
new TextRun(" using adjusted metrics"),
|
||||
new FootnoteReferenceRun(2),
|
||||
],
|
||||
})]
|
||||
}]
|
||||
});
|
||||
```
|
||||
|
||||
### Tab Stops
|
||||
|
||||
```javascript
|
||||
// Right-align text on same line (e.g., date opposite a title)
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun("Company Name"),
|
||||
new TextRun("\tJanuary 2025"),
|
||||
],
|
||||
tabStops: [{ type: TabStopType.RIGHT, position: TabStopPosition.MAX }],
|
||||
})
|
||||
|
||||
// Dot leader (e.g., TOC-style)
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun("Introduction"),
|
||||
new TextRun({ children: [
|
||||
new PositionalTab({
|
||||
alignment: PositionalTabAlignment.RIGHT,
|
||||
relativeTo: PositionalTabRelativeTo.MARGIN,
|
||||
leader: PositionalTabLeader.DOT,
|
||||
}),
|
||||
"3",
|
||||
]}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
### Multi-Column Layouts
|
||||
|
||||
```javascript
|
||||
// Equal-width columns
|
||||
sections: [{
|
||||
properties: {
|
||||
column: {
|
||||
count: 2, // number of columns
|
||||
space: 720, // gap between columns in DXA (720 = 0.5 inch)
|
||||
equalWidth: true,
|
||||
separate: true, // vertical line between columns
|
||||
},
|
||||
},
|
||||
children: [/* content flows naturally across columns */]
|
||||
}]
|
||||
|
||||
// Custom-width columns (equalWidth must be false)
|
||||
sections: [{
|
||||
properties: {
|
||||
column: {
|
||||
equalWidth: false,
|
||||
children: [
|
||||
new Column({ width: 5400, space: 720 }),
|
||||
new Column({ width: 3240 }),
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [/* content */]
|
||||
}]
|
||||
```
|
||||
|
||||
Force a column break with a new section using `type: SectionType.NEXT_COLUMN`.
|
||||
|
||||
### Table of Contents
|
||||
|
||||
```javascript
|
||||
// CRITICAL: Headings must use HeadingLevel ONLY - no custom styles
|
||||
new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" })
|
||||
```
|
||||
|
||||
### Headers/Footers
|
||||
|
||||
```javascript
|
||||
sections: [{
|
||||
properties: {
|
||||
page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } // 1440 = 1 inch
|
||||
},
|
||||
headers: {
|
||||
default: new Header({ children: [new Paragraph({ children: [new TextRun("Header")] })] })
|
||||
},
|
||||
footers: {
|
||||
default: new Footer({ children: [new Paragraph({
|
||||
children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] })]
|
||||
})] })
|
||||
},
|
||||
children: [/* content */]
|
||||
}]
|
||||
```
|
||||
|
||||
### Critical Rules for docx-js
|
||||
|
||||
- **Set page size explicitly** - docx-js defaults to A4; use US Letter (12240 x 15840 DXA) for US documents
|
||||
- **Landscape: pass portrait dimensions** - docx-js swaps width/height internally; pass short edge as `width`, long edge as `height`, and set `orientation: PageOrientation.LANDSCAPE`
|
||||
- **Never use `\n`** - use separate Paragraph elements
|
||||
- **Never use unicode bullets** - use `LevelFormat.BULLET` with numbering config
|
||||
- **PageBreak must be in Paragraph** - standalone creates invalid XML
|
||||
- **ImageRun requires `type`** - always specify png/jpg/etc
|
||||
- **Always set table `width` with DXA** - never use `WidthType.PERCENTAGE` (breaks in Google Docs)
|
||||
- **Tables need dual widths** - `columnWidths` array AND cell `width`, both must match
|
||||
- **Table width = sum of columnWidths** - for DXA, ensure they add up exactly
|
||||
- **Always add cell margins** - use `margins: { top: 80, bottom: 80, left: 120, right: 120 }` for readable padding
|
||||
- **Use `ShadingType.CLEAR`** - never SOLID for table shading
|
||||
- **Never use tables as dividers/rules** - cells have minimum height and render as empty boxes (including in headers/footers); use `border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: "2E75B6", space: 1 } }` on a Paragraph instead. For two-column footers, use tab stops (see Tab Stops section), not tables
|
||||
- **TOC requires HeadingLevel only** - no custom styles on heading paragraphs
|
||||
- **Override built-in styles** - use exact IDs: "Heading1", "Heading2", etc.
|
||||
- **Include `outlineLevel`** - required for TOC (0 for H1, 1 for H2, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Editing Existing Documents
|
||||
|
||||
**Follow all 3 steps in order.**
|
||||
|
||||
### Step 1: Unpack
|
||||
```bash
|
||||
python scripts/office/unpack.py document.docx unpacked/
|
||||
```
|
||||
Extracts XML, pretty-prints, merges adjacent runs, and converts smart quotes to XML entities (`“` etc.) so they survive editing. Use `--merge-runs false` to skip run merging.
|
||||
|
||||
### Step 2: Edit XML
|
||||
|
||||
Edit files in `unpacked/word/`. See XML Reference below for patterns.
|
||||
|
||||
**Use "Claude" as the author** for tracked changes and comments, unless the user explicitly requests use of a different name.
|
||||
|
||||
**Use the Edit tool directly for string replacement. Do not write Python scripts.** Scripts introduce unnecessary complexity. The Edit tool shows exactly what is being replaced.
|
||||
|
||||
**CRITICAL: Use smart quotes for new content.** When adding text with apostrophes or quotes, use XML entities to produce smart quotes:
|
||||
```xml
|
||||
<!-- Use these entities for professional typography -->
|
||||
<w:t>Here’s a quote: “Hello”</w:t>
|
||||
```
|
||||
| Entity | Character |
|
||||
|--------|-----------|
|
||||
| `‘` | ‘ (left single) |
|
||||
| `’` | ’ (right single / apostrophe) |
|
||||
| `“` | “ (left double) |
|
||||
| `”` | ” (right double) |
|
||||
|
||||
**Adding comments:** Use `comment.py` to handle boilerplate across multiple XML files (text must be pre-escaped XML):
|
||||
```bash
|
||||
python scripts/comment.py unpacked/ 0 "Comment text with & and ’"
|
||||
python scripts/comment.py unpacked/ 1 "Reply text" --parent 0 # reply to comment 0
|
||||
python scripts/comment.py unpacked/ 0 "Text" --author "Custom Author" # custom author name
|
||||
```
|
||||
Then add markers to document.xml (see Comments in XML Reference).
|
||||
|
||||
### Step 3: Pack
|
||||
```bash
|
||||
python scripts/office/pack.py unpacked/ output.docx --original document.docx
|
||||
```
|
||||
Validates with auto-repair, condenses XML, and creates DOCX. Use `--validate false` to skip.
|
||||
|
||||
**Auto-repair will fix:**
|
||||
- `durableId` >= 0x7FFFFFFF (regenerates valid ID)
|
||||
- Missing `xml:space="preserve"` on `<w:t>` with whitespace
|
||||
|
||||
**Auto-repair won't fix:**
|
||||
- Malformed XML, invalid element nesting, missing relationships, schema violations
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
- **Replace entire `<w:r>` elements**: When adding tracked changes, replace the whole `<w:r>...</w:r>` block with `<w:del>...<w:ins>...` as siblings. Don't inject tracked change tags inside a run.
|
||||
- **Preserve `<w:rPr>` formatting**: Copy the original run's `<w:rPr>` block into your tracked change runs to maintain bold, font size, etc.
|
||||
|
||||
---
|
||||
|
||||
## XML Reference
|
||||
|
||||
### Schema Compliance
|
||||
|
||||
- **Element order in `<w:pPr>`**: `<w:pStyle>`, `<w:numPr>`, `<w:spacing>`, `<w:ind>`, `<w:jc>`, `<w:rPr>` last
|
||||
- **Whitespace**: Add `xml:space="preserve"` to `<w:t>` with leading/trailing spaces
|
||||
- **RSIDs**: Must be 8-digit hex (e.g., `00AB1234`)
|
||||
|
||||
### Tracked Changes
|
||||
|
||||
**Insertion:**
|
||||
```xml
|
||||
<w:ins w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
|
||||
<w:r><w:t>inserted text</w:t></w:r>
|
||||
</w:ins>
|
||||
```
|
||||
|
||||
**Deletion:**
|
||||
```xml
|
||||
<w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
|
||||
<w:r><w:delText>deleted text</w:delText></w:r>
|
||||
</w:del>
|
||||
```
|
||||
|
||||
**Inside `<w:del>`**: Use `<w:delText>` instead of `<w:t>`, and `<w:delInstrText>` instead of `<w:instrText>`.
|
||||
|
||||
**Minimal edits** - only mark what changes:
|
||||
```xml
|
||||
<!-- Change "30 days" to "60 days" -->
|
||||
<w:r><w:t>The term is </w:t></w:r>
|
||||
<w:del w:id="1" w:author="Claude" w:date="...">
|
||||
<w:r><w:delText>30</w:delText></w:r>
|
||||
</w:del>
|
||||
<w:ins w:id="2" w:author="Claude" w:date="...">
|
||||
<w:r><w:t>60</w:t></w:r>
|
||||
</w:ins>
|
||||
<w:r><w:t> days.</w:t></w:r>
|
||||
```
|
||||
|
||||
**Deleting entire paragraphs/list items** - when removing ALL content from a paragraph, also mark the paragraph mark as deleted so it merges with the next paragraph. Add `<w:del/>` inside `<w:pPr><w:rPr>`:
|
||||
```xml
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
<w:numPr>...</w:numPr> <!-- list numbering if present -->
|
||||
<w:rPr>
|
||||
<w:del w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z"/>
|
||||
</w:rPr>
|
||||
</w:pPr>
|
||||
<w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
|
||||
<w:r><w:delText>Entire paragraph content being deleted...</w:delText></w:r>
|
||||
</w:del>
|
||||
</w:p>
|
||||
```
|
||||
Without the `<w:del/>` in `<w:pPr><w:rPr>`, accepting changes leaves an empty paragraph/list item.
|
||||
|
||||
**Rejecting another author's insertion** - nest deletion inside their insertion:
|
||||
```xml
|
||||
<w:ins w:author="Jane" w:id="5">
|
||||
<w:del w:author="Claude" w:id="10">
|
||||
<w:r><w:delText>their inserted text</w:delText></w:r>
|
||||
</w:del>
|
||||
</w:ins>
|
||||
```
|
||||
|
||||
**Restoring another author's deletion** - add insertion after (don't modify their deletion):
|
||||
```xml
|
||||
<w:del w:author="Jane" w:id="5">
|
||||
<w:r><w:delText>deleted text</w:delText></w:r>
|
||||
</w:del>
|
||||
<w:ins w:author="Claude" w:id="10">
|
||||
<w:r><w:t>deleted text</w:t></w:r>
|
||||
</w:ins>
|
||||
```
|
||||
|
||||
### Comments
|
||||
|
||||
After running `comment.py` (see Step 2), add markers to document.xml. For replies, use `--parent` flag and nest markers inside the parent's.
|
||||
|
||||
**CRITICAL: `<w:commentRangeStart>` and `<w:commentRangeEnd>` are siblings of `<w:r>`, never inside `<w:r>`.**
|
||||
|
||||
```xml
|
||||
<!-- Comment markers are direct children of w:p, never inside w:r -->
|
||||
<w:commentRangeStart w:id="0"/>
|
||||
<w:del w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
|
||||
<w:r><w:delText>deleted</w:delText></w:r>
|
||||
</w:del>
|
||||
<w:r><w:t> more text</w:t></w:r>
|
||||
<w:commentRangeEnd w:id="0"/>
|
||||
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
|
||||
|
||||
<!-- Comment 0 with reply 1 nested inside -->
|
||||
<w:commentRangeStart w:id="0"/>
|
||||
<w:commentRangeStart w:id="1"/>
|
||||
<w:r><w:t>text</w:t></w:r>
|
||||
<w:commentRangeEnd w:id="1"/>
|
||||
<w:commentRangeEnd w:id="0"/>
|
||||
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
|
||||
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="1"/></w:r>
|
||||
```
|
||||
|
||||
### Images
|
||||
|
||||
1. Add image file to `word/media/`
|
||||
2. Add relationship to `word/_rels/document.xml.rels`:
|
||||
```xml
|
||||
<Relationship Id="rId5" Type=".../image" Target="media/image1.png"/>
|
||||
```
|
||||
3. Add content type to `[Content_Types].xml`:
|
||||
```xml
|
||||
<Default Extension="png" ContentType="image/png"/>
|
||||
```
|
||||
4. Reference in document.xml:
|
||||
```xml
|
||||
<w:drawing>
|
||||
<wp:inline>
|
||||
<wp:extent cx="914400" cy="914400"/> <!-- EMUs: 914400 = 1 inch -->
|
||||
<a:graphic>
|
||||
<a:graphicData uri=".../picture">
|
||||
<pic:pic>
|
||||
<pic:blipFill><a:blip r:embed="rId5"/></pic:blipFill>
|
||||
</pic:pic>
|
||||
</a:graphicData>
|
||||
</a:graphic>
|
||||
</wp:inline>
|
||||
</w:drawing>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **pandoc**: Text extraction
|
||||
- **docx**: `npm install -g docx` (new documents)
|
||||
- **LibreOffice**: PDF conversion (auto-configured for sandboxed environments via `scripts/office/soffice.py`)
|
||||
- **Poppler**: `pdftoppm` for images
|
||||
@@ -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 |
|
||||
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
+2820
File diff suppressed because it is too large.
Load diff
+3381
File diff suppressed because it is too large.
Load diff
@@ -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` |
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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 | |
|
||||
@@ -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
|
||||
@@ -0,0 +1,222 @@
|
||||
# minimax-pdf
|
||||
|
||||
A Claude skill for creating and editing visually polished PDFs.
|
||||
Three routes. One design system. Tokens flow from content analysis through every renderer.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
bash scripts/make.sh check # verify deps
|
||||
bash scripts/make.sh fix # auto-install missing deps
|
||||
bash scripts/make.sh demo # → demo.pdf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Route A: CREATE — generate a new PDF
|
||||
|
||||
```bash
|
||||
bash scripts/make.sh run \
|
||||
--title "Q3 Strategy Review" \
|
||||
--type "proposal" \
|
||||
--author "Strategy Team" \
|
||||
--date "October 2025" \
|
||||
--content content.json \
|
||||
--out report.pdf
|
||||
```
|
||||
|
||||
**`--type` options:**
|
||||
|
||||
| Type | Palette | Cover pattern | Google Fonts (cover) |
|
||||
|---|---|---|---|
|
||||
| `report` | Deep ink, teal accent | `fullbleed` | Playfair Display / IBM Plex Sans |
|
||||
| `proposal` | Near-black, amber accent | `split` | Syne / Nunito Sans |
|
||||
| `resume` | White, navy accent | `typographic` | DM Serif Display / DM Sans |
|
||||
| `portfolio` | Deep violet, coral accent | `atmospheric` | Fraunces / Inter |
|
||||
| `academic` | Warm white, navy accent | `typographic` | EB Garamond / Source Sans 3 |
|
||||
| `general` | Dark slate, blue accent | `fullbleed` | Outfit / Outfit |
|
||||
| `minimal` | Near-white, red accent | `minimal` | Cormorant Garamond / Jost |
|
||||
| `stripe` | Dark navy, amber accent | `stripe` | Barlow Condensed / Barlow |
|
||||
| `diagonal` | Dark blue, teal accent | `diagonal` | Montserrat / Montserrat |
|
||||
| `frame` | Warm cream, brown accent | `frame` | Cormorant / Crimson Pro |
|
||||
| `editorial` | White, red accent | `editorial` | Bebas Neue / Libre Franklin |
|
||||
| `magazine` | Warm linen, deep navy accent | `magazine` | Playfair Display / EB Garamond |
|
||||
| `darkroom` | Deep navy, steel blue accent | `darkroom` | Playfair Display / EB Garamond |
|
||||
| `terminal` | Near-black, neon green accent | `terminal` | Space Mono |
|
||||
| `poster` | White, near-black accent | `poster` | Barlow Condensed / Courier Prime |
|
||||
|
||||
**content.json block types:**
|
||||
|
||||
```json
|
||||
[
|
||||
{"type": "h1", "text": "Section Title"},
|
||||
{"type": "h2", "text": "Subsection"},
|
||||
{"type": "h3", "text": "Sub-subsection"},
|
||||
{"type": "body", "text": "Paragraph. Supports <b>bold</b> and <i>italic</i>."},
|
||||
{"type": "bullet", "text": "Unordered list item"},
|
||||
{"type": "numbered","text": "Ordered list item — counter auto-resets between lists"},
|
||||
{"type": "callout", "text": "Key insight or highlighted finding"},
|
||||
{"type": "table",
|
||||
"headers": ["Col A", "Col B"],
|
||||
"rows": [["a", "b"], ["c", "d"]]
|
||||
},
|
||||
{"type": "image", "path": "chart.png", "caption": "Figure 1: optional caption"},
|
||||
{"type": "code", "text": "def hello():\n print('world')"},
|
||||
{"type": "math", "text": "\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}", "label": "(1)"},
|
||||
{"type": "divider"},
|
||||
{"type": "caption", "text": "Table 1: standalone caption label"},
|
||||
{"type": "pagebreak"},
|
||||
{"type": "spacer", "pt": 16}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Route B: FILL — fill form fields in an existing PDF
|
||||
|
||||
```bash
|
||||
# See what fields the PDF has
|
||||
bash scripts/make.sh fill --input form.pdf --inspect
|
||||
|
||||
# Fill fields
|
||||
bash scripts/make.sh fill \
|
||||
--input form.pdf \
|
||||
--out filled.pdf \
|
||||
--values '{"FirstName": "Jane", "Agree": "true", "Country": "US"}'
|
||||
|
||||
# Or from a JSON file
|
||||
bash scripts/make.sh fill --input form.pdf --out filled.pdf --data values.json
|
||||
```
|
||||
|
||||
Field value rules:
|
||||
- `text` → any string
|
||||
- `checkbox` → `"true"` or `"false"`
|
||||
- `dropdown` → must match a choice value shown by `--inspect`
|
||||
- `radio` → must match a radio value shown by `--inspect`
|
||||
|
||||
---
|
||||
|
||||
## Route C: REFORMAT — apply design to an existing document
|
||||
|
||||
```bash
|
||||
bash scripts/make.sh reformat \
|
||||
--input source.md \
|
||||
--title "Annual Report" \
|
||||
--type "report" \
|
||||
--author "Research Team" \
|
||||
--out output.pdf
|
||||
```
|
||||
|
||||
Supported input: `.md` `.txt` `.pdf` `.json`
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
SKILL.md ← Claude entry point, route table
|
||||
design/design.md ← Aesthetic system (read before CREATE/REFORMAT)
|
||||
scripts/
|
||||
make.sh ← Unified CLI
|
||||
palette.py ← metadata → tokens.json [CREATE, REFORMAT]
|
||||
cover.py ← tokens.json → cover.html [CREATE, REFORMAT]
|
||||
render_cover.js ← cover.html → cover.pdf [CREATE, REFORMAT]
|
||||
render_body.py ← tokens + content → body.pdf [CREATE, REFORMAT]
|
||||
merge.py ← cover + body → final.pdf [CREATE, REFORMAT]
|
||||
fill_inspect.py ← PDF → field list [FILL]
|
||||
fill_write.py ← PDF + values → filled PDF [FILL]
|
||||
reformat_parse.py ← doc → content.json [REFORMAT]
|
||||
```
|
||||
|
||||
Design tokens (`tokens.json`) flow from `palette.py` to every renderer — cover and body are always visually consistent.
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Tool | Used by | Install |
|
||||
|---|---|---|
|
||||
| Python 3.9+ | all `.py` scripts | system |
|
||||
| `reportlab` | `render_body.py` | `pip install reportlab` |
|
||||
| `pypdf` | fill, merge, reformat | `pip install pypdf` |
|
||||
| Node.js 18+ | `render_cover.js` | system |
|
||||
| `playwright` + Chromium | `render_cover.js` | `npm install -g playwright && npx playwright install chromium` |
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Document types
|
||||
|
||||
| `--type` | Mood | Cover pattern | Cover fonts |
|
||||
|---|---|---|---|
|
||||
| `report` | Authoritative | `fullbleed` | Playfair Display / IBM Plex Sans |
|
||||
| `proposal` | Confident | `split` | Syne / Nunito Sans |
|
||||
| `resume` | Clean | `typographic` | DM Serif Display / DM Sans |
|
||||
| `portfolio` | Expressive | `atmospheric` | Fraunces / Inter |
|
||||
| `academic` | Scholarly | `typographic` | EB Garamond / Source Sans 3 |
|
||||
| `general` | Neutral | `fullbleed` | Outfit |
|
||||
| `minimal` | Restrained | `minimal` | Cormorant Garamond / Jost |
|
||||
| `stripe` | Bold | `stripe` | Barlow Condensed / Barlow |
|
||||
| `diagonal` | Dynamic | `diagonal` | Montserrat |
|
||||
| `frame` | Classical | `frame` | Cormorant / Crimson Pro |
|
||||
| `editorial` | Editorial | `editorial` | Bebas Neue / Libre Franklin |
|
||||
|
||||
Cover fonts load via Google Fonts `@import` at render time — no local caching.
|
||||
Body pages always use system fonts (Times / Helvetica) via ReportLab.
|
||||
|
||||
## content.json schema
|
||||
|
||||
```json
|
||||
[
|
||||
{"type": "h1", "text": "Section Title"},
|
||||
{"type": "h2", "text": "Subsection"},
|
||||
{"type": "h3", "text": "Sub-subsection"},
|
||||
{"type": "body", "text": "Paragraph text. Supports <b>bold</b> and <i>italic</i>."},
|
||||
{"type": "bullet", "text": "Unordered list item"},
|
||||
{"type": "numbered","text": "Ordered list item — auto-numbered, counter resets between lists"},
|
||||
{"type": "callout", "text": "Highlighted insight or key finding"},
|
||||
{"type": "table",
|
||||
"headers": ["Column A", "Column B", "Column C"],
|
||||
"rows": [["row1a", "row1b", "row1c"], ["row2a", "row2b", "row2c"]]
|
||||
},
|
||||
{"type": "image", "path": "chart.png", "caption": "Figure 1: Sales by quarter"},
|
||||
{"type": "code", "text": "SELECT * FROM users\nWHERE active = 1;"},
|
||||
{"type": "math", "text": "\\sigma = \\sqrt{\\frac{1}{N}\\sum_{i=1}^N (x_i - \\mu)^2}", "label": "(2)"},
|
||||
{"type": "divider"},
|
||||
{"type": "caption", "text": "Table 2: standalone label"},
|
||||
{"type": "pagebreak"},
|
||||
{"type": "spacer", "pt": 16}
|
||||
]
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
SKILL.md ← Claude entry point, routing only
|
||||
design/design.md ← Aesthetic system (read before any script)
|
||||
scripts/
|
||||
make.sh ← Unified CLI: check / fix / run / demo
|
||||
palette.py ← content metadata → tokens.json
|
||||
cover.py ← tokens.json → cover.html
|
||||
render_cover.js ← cover.html → cover.pdf (Playwright)
|
||||
render_body.py ← tokens.json + content.json → body.pdf (ReportLab)
|
||||
merge.py ← cover.pdf + body.pdf → final.pdf + QA report
|
||||
```
|
||||
|
||||
Design tokens (color, typography, spacing) are written once by `palette.py` and consumed by every downstream script. This guarantees visual consistency between cover and body without any manual coordination.
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Tool | Purpose | Install |
|
||||
|---|---|---|
|
||||
| Python 3.9+ | palette, cover, render_body, merge | system |
|
||||
| `reportlab` | Body page rendering | `pip install reportlab` |
|
||||
| `pypdf` | Merging PDFs | `pip install pypdf` |
|
||||
| Node.js 18+ | Cover rendering | system |
|
||||
| `playwright` | Headless Chromium for cover | `npm install -g playwright && npx playwright install chromium` |
|
||||
|
||||
Run `bash scripts/make.sh check` to verify everything at once.
|
||||
Run `bash scripts/make.sh fix` to auto-install what is missing.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
name: minimax-pdf
|
||||
description: >
|
||||
Use this skill when visual quality and design identity matter for a PDF.
|
||||
CREATE (generate from scratch): "make a PDF", "generate a report", "write a proposal",
|
||||
"create a resume", "beautiful PDF", "professional document", "cover page",
|
||||
"polished PDF", "client-ready document".
|
||||
FILL (complete form fields): "fill in the form", "fill out this PDF",
|
||||
"complete the form fields", "write values into PDF", "what fields does this PDF have".
|
||||
REFORMAT (apply design to an existing doc): "reformat this document", "apply our style",
|
||||
"convert this Markdown/text to PDF", "make this doc look good", "re-style this PDF".
|
||||
This skill uses a token-based design system: color, typography, and spacing are derived
|
||||
from the document type and flow through every page. The output is print-ready.
|
||||
Prefer this skill when appearance matters, not just when any PDF output is needed.
|
||||
license: MIT
|
||||
metadata:
|
||||
version: "1.0"
|
||||
category: document-generation
|
||||
---
|
||||
|
||||
# minimax-pdf
|
||||
|
||||
Three tasks. One skill.
|
||||
|
||||
## Read `design/design.md` before any CREATE or REFORMAT work.
|
||||
|
||||
---
|
||||
|
||||
## Route table
|
||||
|
||||
| User intent | Route | Scripts used |
|
||||
|---|---|---|
|
||||
| Generate a new PDF from scratch | **CREATE** | `palette.py` → `cover.py` → `render_cover.js` → `render_body.py` → `merge.py` |
|
||||
| Fill / complete form fields in an existing PDF | **FILL** | `fill_inspect.py` → `fill_write.py` |
|
||||
| Reformat / re-style an existing document | **REFORMAT** | `reformat_parse.py` → then full CREATE pipeline |
|
||||
|
||||
**Rule:** when in doubt between CREATE and REFORMAT, ask whether the user has an existing document to start from. If yes → REFORMAT. If no → CREATE.
|
||||
|
||||
---
|
||||
|
||||
## Route A: CREATE
|
||||
|
||||
Full pipeline — content → design tokens → cover → body → merged PDF.
|
||||
|
||||
```bash
|
||||
bash scripts/make.sh run \
|
||||
--title "Q3 Strategy Review" --type proposal \
|
||||
--author "Strategy Team" --date "October 2025" \
|
||||
--accent "#2D5F8A" \
|
||||
--content content.json --out report.pdf
|
||||
```
|
||||
|
||||
**Doc types:** `report` · `proposal` · `resume` · `portfolio` · `academic` · `general` · `minimal` · `stripe` · `diagonal` · `frame` · `editorial` · `magazine` · `darkroom` · `terminal` · `poster`
|
||||
|
||||
| Type | Cover pattern | Visual identity |
|
||||
|---|---|---|
|
||||
| `report` | `fullbleed` | Dark bg, dot grid, Playfair Display |
|
||||
| `proposal` | `split` | Left panel + right geometric, Syne |
|
||||
| `resume` | `typographic` | Oversized first-word, DM Serif Display |
|
||||
| `portfolio` | `atmospheric` | Near-black, radial glow, Fraunces |
|
||||
| `academic` | `typographic` | Light bg, classical serif, EB Garamond |
|
||||
| `general` | `fullbleed` | Dark slate, Outfit |
|
||||
| `minimal` | `minimal` | White + single 8px accent bar, Cormorant Garamond |
|
||||
| `stripe` | `stripe` | 3 bold horizontal color bands, Barlow Condensed |
|
||||
| `diagonal` | `diagonal` | SVG angled cut, dark/light halves, Montserrat |
|
||||
| `frame` | `frame` | Inset border, corner ornaments, Cormorant |
|
||||
| `editorial` | `editorial` | Ghost letter, all-caps title, Bebas Neue |
|
||||
| `magazine` | `magazine` | Warm cream bg, centered stack, hero image, Playfair Display |
|
||||
| `darkroom` | `darkroom` | Navy bg, centered stack, grayscale image, Playfair Display |
|
||||
| `terminal` | `terminal` | Near-black, grid lines, monospace, neon green |
|
||||
| `poster` | `poster` | White bg, thick sidebar, oversized title, Barlow Condensed |
|
||||
|
||||
Cover extras (inject into tokens via `--abstract`, `--cover-image`):
|
||||
- `--abstract "text"` — abstract text block on the cover (magazine/darkroom)
|
||||
- `--cover-image "url"` — hero image URL/path (magazine, darkroom, poster)
|
||||
|
||||
**Color overrides — always choose these based on document content:**
|
||||
- `--accent "#HEX"` — override the accent color; `accent_lt` is auto-derived by lightening toward white
|
||||
- `--cover-bg "#HEX"` — override the cover background color
|
||||
|
||||
**Accent color selection guidance:**
|
||||
|
||||
You have creative authority over the accent color. Pick it from the document's semantic context — title, industry, purpose, audience — not from generic "safe" choices. The accent appears on section rules, callout bars, table headers, and the cover: it carries the document's visual identity.
|
||||
|
||||
| Context | Suggested accent range |
|
||||
|---|---|
|
||||
| Legal / compliance / finance | Deep navy `#1C3A5E`, charcoal `#2E3440`, slate `#3D4C5E` |
|
||||
| Healthcare / medical | Teal-green `#2A6B5A`, cool green `#3A7D6A` |
|
||||
| Technology / engineering | Steel blue `#2D5F8A`, indigo `#3D4F8A` |
|
||||
| Environmental / sustainability | Forest `#2E5E3A`, olive `#4A5E2A` |
|
||||
| Creative / arts / culture | Burgundy `#6B2A35`, plum `#5A2A6B`, terracotta `#8A3A2A` |
|
||||
| Academic / research | Deep teal `#2A5A6B`, library blue `#2A4A6B` |
|
||||
| Corporate / neutral | Slate `#3D4A5A`, graphite `#444C56` |
|
||||
| Luxury / premium | Warm black `#1A1208`, deep bronze `#4A3820` |
|
||||
|
||||
**Rule:** choose a color that a thoughtful designer would select for this specific document — not the type's default. Muted, desaturated tones work best; avoid vivid primaries. When in doubt, go darker and more neutral.
|
||||
|
||||
**content.json block types:**
|
||||
|
||||
| Block | Usage | Key fields |
|
||||
|---|---|---|
|
||||
| `h1` | Section heading + accent rule | `text` |
|
||||
| `h2` | Subsection heading | `text` |
|
||||
| `h3` | Sub-subsection (bold) | `text` |
|
||||
| `body` | Justified paragraph; supports `<b>` `<i>` markup | `text` |
|
||||
| `bullet` | Unordered list item (• prefix) | `text` |
|
||||
| `numbered` | Ordered list item — counter auto-resets on non-numbered blocks | `text` |
|
||||
| `callout` | Highlighted insight box with accent left bar | `text` |
|
||||
| `table` | Data table — accent header, alternating row tints | `headers`, `rows`, `col_widths`?, `caption`? |
|
||||
| `image` | Embedded image scaled to column width | `path`/`src`, `caption`? |
|
||||
| `figure` | Image with auto-numbered "Figure N:" caption | `path`/`src`, `caption`? |
|
||||
| `code` | Monospace code block with accent left border | `text`, `language`? |
|
||||
| `math` | Display math — LaTeX syntax via matplotlib mathtext | `text`, `label`?, `caption`? |
|
||||
| `chart` | Bar / line / pie chart rendered with matplotlib | `chart_type`, `labels`, `datasets`, `title`?, `x_label`?, `y_label`?, `caption`?, `figure`? |
|
||||
| `flowchart` | Process diagram with nodes + edges via matplotlib | `nodes`, `edges`, `caption`?, `figure`? |
|
||||
| `bibliography` | Numbered reference list with hanging indent | `items` [{id, text}], `title`? |
|
||||
| `divider` | Accent-colored full-width rule | — |
|
||||
| `caption` | Small muted label | `text` |
|
||||
| `pagebreak` | Force a new page | — |
|
||||
| `spacer` | Vertical whitespace | `pt` (default 12) |
|
||||
|
||||
**chart / flowchart schemas:**
|
||||
```json
|
||||
{"type":"chart","chart_type":"bar","labels":["Q1","Q2","Q3","Q4"],
|
||||
"datasets":[{"label":"Revenue","values":[120,145,132,178]}],"caption":"Q results"}
|
||||
|
||||
{"type":"flowchart",
|
||||
"nodes":[{"id":"s","label":"Start","shape":"oval"},
|
||||
{"id":"p","label":"Process","shape":"rect"},
|
||||
{"id":"d","label":"Valid?","shape":"diamond"},
|
||||
{"id":"e","label":"End","shape":"oval"}],
|
||||
"edges":[{"from":"s","to":"p"},{"from":"p","to":"d"},
|
||||
{"from":"d","to":"e","label":"Yes"},{"from":"d","to":"p","label":"No"}]}
|
||||
|
||||
{"type":"bibliography","items":[
|
||||
{"id":"1","text":"Author (Year). Title. Publisher."}]}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Route B: FILL
|
||||
|
||||
Fill form fields in an existing PDF without altering layout or design.
|
||||
|
||||
```bash
|
||||
# Step 1: inspect
|
||||
python3 scripts/fill_inspect.py --input form.pdf
|
||||
|
||||
# Step 2: fill
|
||||
python3 scripts/fill_write.py --input form.pdf --out filled.pdf \
|
||||
--values '{"FirstName": "Jane", "Agree": "true", "Country": "US"}'
|
||||
```
|
||||
|
||||
| Field type | Value format |
|
||||
|---|---|
|
||||
| `text` | Any string |
|
||||
| `checkbox` | `"true"` or `"false"` |
|
||||
| `dropdown` | Must match a choice value from inspect output |
|
||||
| `radio` | Must match a radio value (often starts with `/`) |
|
||||
|
||||
Always run `fill_inspect.py` first to get exact field names.
|
||||
|
||||
---
|
||||
|
||||
## Route C: REFORMAT
|
||||
|
||||
Parse an existing document → content.json → CREATE pipeline.
|
||||
|
||||
```bash
|
||||
bash scripts/make.sh reformat \
|
||||
--input source.md --title "My Report" --type report --out output.pdf
|
||||
```
|
||||
|
||||
**Supported input formats:** `.md` `.txt` `.pdf` `.json`
|
||||
|
||||
---
|
||||
|
||||
## Environment
|
||||
|
||||
```bash
|
||||
bash scripts/make.sh check # verify all deps
|
||||
bash scripts/make.sh fix # auto-install missing deps
|
||||
bash scripts/make.sh demo # build a sample PDF
|
||||
```
|
||||
|
||||
| Tool | Used by | Install |
|
||||
|---|---|---|
|
||||
| Python 3.9+ | all `.py` scripts | system |
|
||||
| `reportlab` | `render_body.py` | `pip install reportlab` |
|
||||
| `pypdf` | fill, merge, reformat | `pip install pypdf` |
|
||||
| Node.js 18+ | `render_cover.js` | system |
|
||||
| `playwright` + Chromium | `render_cover.js` | `npm install -g playwright && npx playwright install chromium` |
|
||||
@@ -0,0 +1,381 @@
|
||||
# Design System
|
||||
|
||||
The aesthetic layer. Read this before touching any script.
|
||||
This file answers "what should it look like and why."
|
||||
|
||||
---
|
||||
|
||||
## The one rule
|
||||
|
||||
Every design decision must be **rooted in the document's content and purpose**.
|
||||
Dark teal + cream is not "professional". Serif + beige is not "elegant".
|
||||
A color chosen because it fits the content will always outperform a color chosen
|
||||
because it seems safe.
|
||||
|
||||
---
|
||||
|
||||
## Palette logic
|
||||
|
||||
`palette.py` takes a short content description and outputs `tokens.json`.
|
||||
Here is the reasoning it applies:
|
||||
|
||||
### Mood → base palette
|
||||
|
||||
| Content signal | Mood | Background | Accent | Text |
|
||||
|---|---|---|---|---|
|
||||
| Research, science, analysis | Authoritative | `#0F1F2E` deep ink | `#00B4A6` teal | `#F0EDE6` warm white |
|
||||
| Business, strategy, finance | Confident | `#1C1C2B` near-black | `#E8A020` amber | `#F5F2EC` cream |
|
||||
| Creative, portfolio, design | Expressive | `#1A0A2E` deep violet | `#FF6B6B` coral | `#FAF5FF` lavender white |
|
||||
| Education, academic paper | Scholarly | `#FAFAF7` warm white | `#2C4A7C` navy | `#1A1A2E` dark |
|
||||
| Healthcare, wellness | Calm | `#F5F9F8` pale mint | `#2D8B72` forest | `#1E3830` deep green |
|
||||
| Resume / personal | Clean | `#FFFFFF` white | pick from content | `#111111` near-black |
|
||||
| General / unknown | Neutral | `#F8F6F1` warm off-white | `#3D3D3D` dark gray | `#1A1A1A` black |
|
||||
| Formal publications, annual reports | Magazine | `#F2F0EC` warm linen | `#1C3557` deep navy | `#0D1A2B` near-black |
|
||||
| Premium/dark reports, tech reviews | Darkroom | `#151C27` deep navy | `#4A6FA5` steel blue | `#F0EDE6` warm white |
|
||||
| Technical docs, developer reports | Terminal | `#0D1117` near-black | `#39D353` neon green | `#E6EDF3` cool white |
|
||||
| Portfolios, creative, photography | Poster | `#FFFFFF` white | `#0A0A0A` near-black | `#0A0A0A` near-black |
|
||||
|
||||
### Accent selection rules
|
||||
|
||||
- **One accent color only.** Using two accents splits visual energy.
|
||||
- Accent appears on: cover geometric elements, section rules, callout left borders,
|
||||
table header background, page header rule. Nowhere else.
|
||||
- Accent must contrast with the cover background by at least 4.5:1 (WCAG AA).
|
||||
- Do not default to blue. Blue is the most overused accent in AI-generated documents.
|
||||
|
||||
### Color pairing anti-patterns (never use these)
|
||||
|
||||
| ❌ Avoid | Why |
|
||||
|---|---|
|
||||
| Purple gradient on white | The default AI aesthetic — immediately signals "generated" |
|
||||
| Navy + gold | Overused corporate cliché |
|
||||
| All-black background | Prints badly, feels aggressive |
|
||||
| More than 3 colors in the system | Visual noise |
|
||||
| Accent on body text | Destroys readability |
|
||||
|
||||
---
|
||||
|
||||
## Typography system
|
||||
|
||||
### Font pairing logic
|
||||
|
||||
Two typefaces maximum. Always.
|
||||
|
||||
| Role | Criteria | Good choices (system-safe) |
|
||||
|---|---|---|
|
||||
| Display (cover title, H1) | Distinctive, strong contrast, high weight | Times New Roman, Georgia (serif) |
|
||||
| Text (body, captions, UI) | Highly readable at 10–11pt | Helvetica, Arial (sans) |
|
||||
|
||||
Cover fonts are loaded live via `@import url(...)` in the cover HTML — Playwright
|
||||
fetches them at render time, no local caching. Body pages always use system fonts
|
||||
(Times-Bold / Helvetica) via ReportLab — consistent and offline-safe.
|
||||
|
||||
Pairs by mood (cover HTML only — body always uses system fonts):
|
||||
- Authoritative: `Playfair Display` / `IBM Plex Sans`
|
||||
- Confident: `Syne` / `Nunito Sans`
|
||||
- Expressive: `Fraunces` / `Inter`
|
||||
- Scholarly: `EB Garamond` / `Source Sans 3`
|
||||
- Clean: `DM Serif Display` / `DM Sans`
|
||||
- Restrained: `Cormorant Garamond` / `Jost`
|
||||
- Bold: `Barlow Condensed` / `Barlow`
|
||||
- Dynamic: `Montserrat` / `Montserrat`
|
||||
- Classical: `Cormorant` / `Crimson Pro`
|
||||
- Editorial: `Bebas Neue` / `Libre Franklin`
|
||||
- Body fallback (always): `Times-Bold` / `Helvetica` (ReportLab system fonts)
|
||||
|
||||
### Type scale
|
||||
|
||||
All sizes in points. This scale is used by `palette.py` to populate `tokens.json`.
|
||||
|
||||
| Token | Size | Leading | Usage |
|
||||
|---|---|---|---|
|
||||
| `display` | 54pt | 1.0 | Cover title |
|
||||
| `h1` | 22pt | 1.3 | Section headings |
|
||||
| `h2` | 15pt | 1.4 | Subsection headings |
|
||||
| `h3` | 11.5pt | 1.5 | Sub-subsection |
|
||||
| `body` | 10.5pt | 1.6 | Main prose |
|
||||
| `caption` | 8.5pt | 1.4 | Figure/table captions |
|
||||
| `meta` | 8pt | 1.3 | Header/footer text |
|
||||
|
||||
### Spacing system
|
||||
|
||||
Margins and rhythm are what separate "looks designed" from "looks printed".
|
||||
|
||||
| Token | Value | Notes |
|
||||
|---|---|---|
|
||||
| `margin_outer` | 2.8cm | Left/right page margin |
|
||||
| `margin_top` | 2.8cm | Top page margin |
|
||||
| `margin_bottom` | 2.5cm | Bottom page margin |
|
||||
| `section_gap` | 26pt | Space before H1 |
|
||||
| `para_gap` | 8pt | Space after paragraph |
|
||||
| `line_gap` | 17pt | Leading for body text |
|
||||
|
||||
Never use ReportLab's default margins (too tight). Always set explicitly.
|
||||
|
||||
---
|
||||
|
||||
## Cover design
|
||||
|
||||
The cover is the most important page. It determines whether a reader trusts the document.
|
||||
|
||||
### Thirteen cover patterns
|
||||
|
||||
`cover.py` selects one based on `tokens.json["cover_pattern"]`.
|
||||
|
||||
**1. `fullbleed`** — used for: `report`, `general`
|
||||
- Deep background fills 100% of page
|
||||
- Title: large, left-aligned, upper 60% of page
|
||||
- Accent: thin horizontal rule + top-right corner strip
|
||||
- Dot-grid background texture (subtle, 8–10% opacity)
|
||||
- Footer band: author + date metadata
|
||||
- Fonts: Playfair Display / IBM Plex Sans
|
||||
|
||||
**2. `split`** — used for: `proposal`
|
||||
- Left 42% panel: solid cover color, title + author
|
||||
- Right 58%: off-white, dot-grid decoration
|
||||
- Hard vertical dividing line in accent color
|
||||
- No gradients — pure flat geometry
|
||||
- Fonts: Syne / Nunito Sans
|
||||
|
||||
**3. `typographic`** — used for: `resume`, `academic`
|
||||
- White/off-white background
|
||||
- Name or title as oversized display type (60–80pt), left-aligned
|
||||
- First word in accent color, remainder in dark
|
||||
- Thin rule below title block
|
||||
- Fonts: DM Serif Display / DM Sans (resume) · EB Garamond / Source Sans 3 (academic)
|
||||
|
||||
**4. `atmospheric`** — used for: `portfolio`
|
||||
- Near-black background
|
||||
- Soft radial glow in accent color (upper-right quadrant)
|
||||
- Title centered-left, 2 lines max
|
||||
- Short rule in accent below title
|
||||
- Dot-grid texture at low opacity
|
||||
- Fonts: Fraunces / Inter
|
||||
|
||||
**5. `minimal`** — used for: `minimal`
|
||||
- Near-white background, 8px left accent bar is the only color
|
||||
- Title in very large, light-weight display type (300 weight)
|
||||
- Hairline rule, author + date as single muted line
|
||||
- Nothing else — the bar does all the visual work
|
||||
- Fonts: Cormorant Garamond / Jost
|
||||
|
||||
**6. `stripe`** — used for: `stripe`
|
||||
- Page cut into three horizontal bands: accent / dark / light
|
||||
- Top band: category label; middle: oversized title in white; bottom: metadata
|
||||
- Hard edges, no gradients, no textures — newspaper / brand poster aesthetic
|
||||
- Fonts: Barlow Condensed / Barlow
|
||||
|
||||
**7. `diagonal`** — used for: `diagonal`
|
||||
- SVG polygon cuts page diagonally: dark upper-left, light lower-right
|
||||
- Accent-colored edge line traces the diagonal cut
|
||||
- Title on dark area, metadata on light area
|
||||
- Fonts: Montserrat / Montserrat
|
||||
|
||||
**8. `frame`** — used for: `frame`
|
||||
- White/cream background with an inset rectangular border (1.2px, 28px from edges)
|
||||
- Accent strips inside top + bottom of frame; small accent corner squares
|
||||
- Title centered in the frame space, centered alignment, classical weight
|
||||
- Formal, timeless — annual reports, legal documents, academic papers
|
||||
- Fonts: Cormorant / Crimson Pro
|
||||
|
||||
**9. `editorial`** — used for: `editorial`
|
||||
- Ghost first-letter of title fills upper-right at 5% opacity — visual texture
|
||||
- 5px accent top bar; full-width uppercase title in condensed weight
|
||||
- Title all-caps, very large (80px), flush-left
|
||||
- Footer rule + author/date metadata
|
||||
- Fonts: Bebas Neue / Libre Franklin
|
||||
|
||||
**10. `magazine`** — used for: `magazine`
|
||||
- Warm cream/linen background; fully centered, vertical stack layout
|
||||
- Org/company name in small spaced caps + 2px accent rule beneath (top anchor)
|
||||
- Large bold serif title (52px) centered; short accent rule under title
|
||||
- Italic subtitle; optional `cover_image` URL renders as centered hero thumbnail
|
||||
- Optional `abstract` field: justified text block with bold "Abstract:" label
|
||||
- Author name in accent color (large, bold); date beneath
|
||||
- Fonts: Playfair Display / EB Garamond
|
||||
|
||||
**11. `darkroom`** — used for: `darkroom`
|
||||
- Same centered stack layout as `magazine` but deep navy background, white text
|
||||
- Org name + rules in semi-transparent white; accent rules desaturated
|
||||
- Hero image (if provided) gets `grayscale(20%) brightness(0.9)` filter
|
||||
- Fonts: Playfair Display / EB Garamond
|
||||
|
||||
**12. `terminal`** — used for: `terminal`
|
||||
- Near-black background; neon green accent; Space Mono monospace throughout
|
||||
- Grid overlay: faint horizontal + vertical lines at 48px intervals (7% opacity)
|
||||
- Status label top-left: green dot + `SYSTEM_REPORT // <date>`
|
||||
- Title inside a bracket frame (border-left + border-top + pseudo-element corner)
|
||||
- Subtitle prefixed with `>` in accent color
|
||||
- Abstract text left; author block right; status bar at bottom (UTF-8 / Ln 1)
|
||||
- Fonts: Space Mono / Space Mono
|
||||
|
||||
**13. `poster`** — used for: `poster`
|
||||
- White background; thick 52px left sidebar in accent (typically near-black)
|
||||
- Title: 96px, 900-weight, all-caps, condensed — the dominant visual element
|
||||
- Subtitle in typewriter font below title; thin 2px rule as separator
|
||||
- Author + meta in Courier Prime monospace beneath rule
|
||||
- Optional `cover_image` rendered as 260×340 grayscale thumbnail, right-aligned
|
||||
- Accent square icon block (lower-right) with white horizontal lines
|
||||
- Fonts: Barlow Condensed / Courier Prime
|
||||
|
||||
### Optional token: `cover_image`
|
||||
|
||||
Patterns `magazine`, `darkroom`, and `poster` accept an optional `cover_image`
|
||||
token containing an absolute URL or `file://` path to an image.
|
||||
The image renders via `<img src="...">` — Playwright fetches it at render time.
|
||||
If omitted, the image area is simply skipped (layout adjusts gracefully).
|
||||
|
||||
### Cover CSS requirements (critical for Playwright rendering)
|
||||
|
||||
These three rules must appear in every cover HTML file or the output will have
|
||||
white borders / incorrect dimensions:
|
||||
|
||||
```css
|
||||
body { margin: 0; padding: 0; }
|
||||
html, body { width: 794px; height: 1123px; overflow: hidden; }
|
||||
```
|
||||
|
||||
No `@page` rules needed — Playwright handles page size via the `pdf()` call.
|
||||
Do NOT use CSS `background-image` for textures — use inline SVG or `<canvas>`.
|
||||
Always use `position: absolute` + `z-index` for layered elements.
|
||||
|
||||
### What always kills a cover
|
||||
|
||||
- Centered title on white background with a thin horizontal line underneath
|
||||
- Gradient from one color to another (reads as PowerPoint, not print design)
|
||||
- Drop shadows on text
|
||||
- More than one accent color
|
||||
- Emoji or icon fonts (fail silently on headless Chromium)
|
||||
|
||||
---
|
||||
|
||||
## Inner page rules
|
||||
|
||||
### What "restraint" means in practice
|
||||
|
||||
Every design decision should remove something, not add something.
|
||||
The page is done when there is nothing left to remove.
|
||||
|
||||
- Accent color appears on section rules only — not on headings, not on bullets
|
||||
- No card components (bordered boxes with colored headers)
|
||||
- No rounded corners on anything except callout boxes (4px max)
|
||||
- No shadows anywhere
|
||||
- Tables: header row in accent, alternating row tint, no grid lines except outer box
|
||||
- Callout boxes: left border in accent (4px), very light tint background, no icon
|
||||
|
||||
### Page header / footer
|
||||
|
||||
Header: document title (left, 7.5pt, muted) + accent rule (1.5pt, full width below)
|
||||
Footer: author name (left, 7.5pt, muted) + page number (right, 7.5pt, muted) + light rule above
|
||||
|
||||
---
|
||||
|
||||
## Quality bar
|
||||
|
||||
A PDF passes if a designer would not be embarrassed to hand it to a client.
|
||||
Concretely:
|
||||
|
||||
- Cover has a clear visual identity that is not "generic AI output"
|
||||
- Body text is readable at arm's length without squinting
|
||||
- Every page looks like it belongs to the same document
|
||||
- No element bleeds off the edge or overlaps another
|
||||
- Page numbers are present and correct
|
||||
- The accent color appears fewer than 8 times per page on average
|
||||
|
||||
---
|
||||
|
||||
## Block type reference
|
||||
|
||||
All body blocks use the same token system — colors and fonts come from `tokens.json`, never hardcoded.
|
||||
|
||||
| Block | Rendering | Design notes |
|
||||
|---|---|---|
|
||||
| `h1` | 22pt heading + full-width accent rule below | KeepTogether with rule — heading never orphaned |
|
||||
| `h2` | 15pt heading, dark text | No rule, no accent — visual hierarchy through size only |
|
||||
| `h3` | 11.5pt bold, dark text | **No accent color** — accent on body headings violates the one-accent-location rule |
|
||||
| `body` | 10.5pt justified, 17pt leading | Supports `<b>` `<i>` `<font>` markup |
|
||||
| `bullet` | Body size with `•` prefix, 14pt indent | Use for unordered lists |
|
||||
| `numbered` | Body size with `N.` prefix, hanging indent | Counter auto-resets on any non-numbered block — no manual numbering needed |
|
||||
| `callout` | Accent left-border (4px) + light tint background | Max one callout per section — overuse kills impact |
|
||||
| `table` | Accent header row, alternating row tint, outer box only | Supports `col_widths` (fractions, e.g. `[0.3, 0.5, 0.2]`) for custom column widths |
|
||||
| `image` | Scaled to column width, preserving aspect ratio | Use `path` or `src`; always provide a `caption` |
|
||||
| `figure` | Same as image, but caption auto-prefixed "Figure N:" | Figure counter increments across all `figure`, `chart`, `flowchart` blocks |
|
||||
| `code` | Courier 8.5pt, accent left-border, light tint background | Supports optional `language` label (rendered above block) |
|
||||
| `math` | Formula centered, optional right-aligned equation label | LaTeX syntax; matplotlib mathtext renderer |
|
||||
| `chart` | Bar / line / pie chart rendered via matplotlib | Color palette derived from document accent; figure auto-numbered |
|
||||
| `flowchart` | Process diagram with labeled arrows | Supports 4 node shapes; back-edges drawn as curved arcs |
|
||||
| `bibliography` | Numbered reference list with hanging indent | Heading rendered as h2 + accent rule; items as `[N] text` |
|
||||
| `divider` | Accent-colored 1.2pt rule with padding | Use sparingly — only for major thematic breaks |
|
||||
| `caption` | 8.5pt muted text, centered | Appears below images/tables via field or explicit block |
|
||||
| `pagebreak` | Force page break | — |
|
||||
| `spacer` | Vertical whitespace | `pt` field (default 12) |
|
||||
|
||||
### Math formula guidance
|
||||
|
||||
**Input syntax:** standard LaTeX math notation — `\frac{}{}`, `\int`, `\sum`, `\alpha`, `^`, `_`, etc.
|
||||
**Rendering engine:** matplotlib mathtext — pure Python, no LaTeX compiler, no browser required.
|
||||
|
||||
| Syntax example | Rendered as |
|
||||
|---|---|
|
||||
| `E = mc^2` | Inline expression |
|
||||
| `\frac{\sqrt{\pi}}{2}` | Fraction |
|
||||
| `\int_0^\infty e^{-x^2} dx` | Integral |
|
||||
| `\sum_{i=1}^{n} x_i` | Summation |
|
||||
| `\alpha + \beta = \gamma` | Greek letters |
|
||||
|
||||
**Limitations:** matplotlib mathtext covers most common expressions but not advanced LaTeX environments (`align`, `cases`, `matrix`). Split complex multi-line proofs into multiple `math` blocks.
|
||||
|
||||
**Fallback:** if matplotlib is not installed, renders as `expression` in code style. Run `make.sh fix` to install.
|
||||
|
||||
**Equation labels:** `"label": "(1)"` — rendered right-aligned beside the formula.
|
||||
|
||||
### Chart guidance
|
||||
|
||||
**Rendered entirely in Python** — no external chart services, image files, or internet required.
|
||||
|
||||
| chart_type | Use case | Required fields |
|
||||
|---|---|---|
|
||||
| `bar` | Comparing discrete categories | `labels`, `datasets` |
|
||||
| `line` | Trends over time or ordered categories | `labels`, `datasets` |
|
||||
| `pie` | Part-to-whole composition | `labels`, `datasets[0].values` |
|
||||
|
||||
- Colors are derived from the document accent for visual consistency — do not set custom colors.
|
||||
- Multi-series: add multiple objects to `datasets`, each with a `label` and `values` array.
|
||||
- Figure auto-numbering: set `"figure": true` (default) or `"figure": false` to suppress.
|
||||
|
||||
### Flowchart guidance
|
||||
|
||||
**Node shapes:**
|
||||
|
||||
| shape | Use for |
|
||||
|---|---|
|
||||
| `rect` (default) | Process step |
|
||||
| `diamond` | Decision / condition |
|
||||
| `oval` or `terminal` | Start / End |
|
||||
| `parallelogram` | Input / Output |
|
||||
|
||||
- Nodes are placed in input order (top to bottom). This controls the layout.
|
||||
- Forward edges draw straight arrows; back-edges (to earlier nodes) draw curved arcs.
|
||||
- Keep labels short (3–5 words max) — the diagram is A4-column-width at 78% scale.
|
||||
- Figure auto-numbering applies same as chart.
|
||||
|
||||
### Bibliography guidance
|
||||
|
||||
- `id` field is the reference label — use numbers ("1", "2") or alphanumeric ("Smith23").
|
||||
- Text should be in a consistent citation style (APA, Chicago, etc.) — the renderer does not enforce style.
|
||||
- The `title` field defaults to "References". Set `"title": ""` to suppress the heading.
|
||||
- A `bibliography` block always starts with a new section heading + accent rule.
|
||||
|
||||
### Image / figure guidance
|
||||
|
||||
- Preferred formats: PNG, JPEG
|
||||
- Scaled down if wider than the text column; never scaled up
|
||||
- `figure` blocks auto-number; `image` blocks do not — use `figure` for numbered figures
|
||||
- If the file does not exist at render time, a `[Image not found]` placeholder is substituted
|
||||
|
||||
### Code block guidance
|
||||
|
||||
- Preserves whitespace exactly — do not indent code in the JSON value
|
||||
- Optional `language` field renders a small language label above the block (e.g., `"language": "python"`)
|
||||
- No syntax highlighting (by design) — consistent with restraint principle
|
||||
- Keep lines under ~90 characters for A4 column width
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: minimax-xlsx
|
||||
description: "Open, create, read, analyze, edit, or validate Excel/spreadsheet files (.xlsx, .xlsm, .csv, .tsv). Use when the user asks to create, build, modify, analyze, read, validate, or format any Excel spreadsheet, financial model, pivot table, or tabular data file. Covers: creating new xlsx from scratch, reading and analyzing existing files, editing existing xlsx with zero format loss, formula recalculation and validation, and applying professional financial formatting standards. Triggers on 'spreadsheet', 'Excel', '.xlsx', '.csv', 'pivot table', 'financial model', 'formula', or any request to produce tabular data in Excel format."
|
||||
license: MIT
|
||||
metadata:
|
||||
version: "1.0"
|
||||
category: productivity
|
||||
sources:
|
||||
- ECMA-376 Office Open XML File Formats
|
||||
- Microsoft Open XML SDK documentation
|
||||
---
|
||||
|
||||
# MiniMax XLSX Skill
|
||||
|
||||
Handle the request directly. Do NOT spawn sub-agents. Always write the output file the user requests.
|
||||
|
||||
## Task Routing
|
||||
|
||||
| Task | Method | Guide |
|
||||
|------|--------|-------|
|
||||
| **READ** — analyze existing data | `xlsx_reader.py` + pandas | `references/read-analyze.md` |
|
||||
| **CREATE** — new xlsx from scratch | XML template | `references/create.md` + `references/format.md` |
|
||||
| **EDIT** — modify existing xlsx | XML unpack→edit→pack | `references/edit.md` (+ `format.md` if styling needed) |
|
||||
| **FIX** — repair broken formulas in existing xlsx | XML unpack→fix `<f>` nodes→pack | `references/fix.md` |
|
||||
| **VALIDATE** — check formulas | `formula_check.py` | `references/validate.md` |
|
||||
|
||||
## READ — Analyze data (read `references/read-analyze.md` first)
|
||||
|
||||
Start with `xlsx_reader.py` for structure discovery, then pandas for custom analysis. Never modify the source file.
|
||||
|
||||
**Formatting rule**: When the user specifies decimal places (e.g. "2 decimal places"), apply that format to ALL numeric values — use `f'{v:.2f}'` on every number. Never output `12875` when `12875.00` is required.
|
||||
|
||||
**Aggregation rule**: Always compute sums/means/counts directly from the DataFrame column — e.g. `df['Revenue'].sum()`. Never re-derive column values before aggregation.
|
||||
|
||||
## CREATE — XML template (read `references/create.md` + `references/format.md`)
|
||||
|
||||
Copy `templates/minimal_xlsx/` → edit XML directly → pack with `xlsx_pack.py`. Every derived value MUST be an Excel formula (`<f>SUM(B2:B9)</f>`), never a hardcoded number. Apply font colors per `format.md`.
|
||||
|
||||
## EDIT — XML direct-edit (read `references/edit.md` first)
|
||||
|
||||
**CRITICAL — EDIT INTEGRITY RULES:**
|
||||
1. **NEVER create a new `Workbook()`** for edit tasks. Always load the original file.
|
||||
2. The output MUST contain the **same sheets** as the input (same names, same data).
|
||||
3. Only modify the specific cells the task asks for — everything else must be untouched.
|
||||
4. **After saving output.xlsx, verify it**: open with `xlsx_reader.py` or `pandas` and confirm the original sheet names and a sample of original data are present. If verification fails, you wrote the wrong file — fix it before delivering.
|
||||
|
||||
Never use openpyxl round-trip on existing files (corrupts VBA, pivots, sparklines). Instead: unpack → use helper scripts → repack.
|
||||
|
||||
**"Fill cells" / "Add formulas to existing cells" = EDIT task.** If the input file already exists and you are told to fill, update, or add formulas to specific cells, you MUST use the XML edit path. Never create a new `Workbook()`. Example — fill B3 with a cross-sheet SUM formula:
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/xlsx_unpack.py input.xlsx /tmp/xlsx_work/
|
||||
# Find the target sheet's XML via xl/workbook.xml → xl/_rels/workbook.xml.rels
|
||||
# Then use the Edit tool to add <f> inside the target <c> element:
|
||||
# <c r="B3"><f>SUM('Sales Data'!D2:D13)</f><v></v></c>
|
||||
python3 SKILL_DIR/scripts/xlsx_pack.py /tmp/xlsx_work/ output.xlsx
|
||||
```
|
||||
|
||||
**Add a column** (formulas, numfmt, styles auto-copied from adjacent column):
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/xlsx_unpack.py input.xlsx /tmp/xlsx_work/
|
||||
python3 SKILL_DIR/scripts/xlsx_add_column.py /tmp/xlsx_work/ --col G \
|
||||
--sheet "Sheet1" --header "% of Total" \
|
||||
--formula '=F{row}/$F$10' --formula-rows 2:9 \
|
||||
--total-row 10 --total-formula '=SUM(G2:G9)' --numfmt '0.0%' \
|
||||
--border-row 10 --border-style medium
|
||||
python3 SKILL_DIR/scripts/xlsx_pack.py /tmp/xlsx_work/ output.xlsx
|
||||
```
|
||||
The `--border-row` flag applies a top border to ALL cells in that row (not just the new column). Use it when the task requires accounting-style borders on total rows.
|
||||
|
||||
**Insert a row** (shifts existing rows, updates SUM formulas, fixes circular refs):
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/xlsx_unpack.py input.xlsx /tmp/xlsx_work/
|
||||
# IMPORTANT: Find the correct --at row by searching for the label text
|
||||
# in the worksheet XML, NOT by using the row number from the prompt.
|
||||
# The prompt may say "row 5 (Office Rent)" but Office Rent might actually
|
||||
# be at row 4. Always locate the row by its text label first.
|
||||
python3 SKILL_DIR/scripts/xlsx_insert_row.py /tmp/xlsx_work/ --at 5 \
|
||||
--sheet "Budget FY2025" --text A=Utilities \
|
||||
--values B=3000 C=3000 D=3500 E=3500 \
|
||||
--formula 'F=SUM(B{row}:E{row})' --copy-style-from 4
|
||||
python3 SKILL_DIR/scripts/xlsx_pack.py /tmp/xlsx_work/ output.xlsx
|
||||
```
|
||||
**Row lookup rule**: When the task says "after row N (Label)", always find the row by searching for "Label" in the worksheet XML (`grep -n "Label" /tmp/xlsx_work/xl/worksheets/sheet*.xml` or check sharedStrings.xml). Use the actual row number + 1 for `--at`. Do NOT call `xlsx_shift_rows.py` separately — `xlsx_insert_row.py` calls it internally.
|
||||
|
||||
**Apply row-wide borders** (e.g. accounting line on a TOTAL row):
|
||||
After running helper scripts, apply borders to ALL cells in the target row, not just newly added cells. In `xl/styles.xml`, append a new `<border>` with the desired style, then append a new `<xf>` in `<cellXfs>` that clones each cell's existing `<xf>` but sets the new `borderId`. Apply the new style index to every `<c>` in the row via the `s` attribute:
|
||||
```xml
|
||||
<!-- In xl/styles.xml, append to <borders>: -->
|
||||
<border>
|
||||
<left/><right/><top style="medium"/><bottom/><diagonal/>
|
||||
</border>
|
||||
<!-- Then append to <cellXfs> an xf clone with the new borderId for each existing style -->
|
||||
```
|
||||
**Key rule**: When a task says "add a border to row N", iterate over ALL cells A through the last column, not just newly added cells.
|
||||
|
||||
**Manual XML edit** (for anything the helper scripts don't cover):
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/xlsx_unpack.py input.xlsx /tmp/xlsx_work/
|
||||
# ... edit XML with the Edit tool ...
|
||||
python3 SKILL_DIR/scripts/xlsx_pack.py /tmp/xlsx_work/ output.xlsx
|
||||
```
|
||||
|
||||
## FIX — Repair broken formulas (read `references/fix.md` first)
|
||||
|
||||
This is an EDIT task. Unpack → fix broken `<f>` nodes → pack. Preserve all original sheets and data.
|
||||
|
||||
## VALIDATE — Check formulas (read `references/validate.md` first)
|
||||
|
||||
Run `formula_check.py` for static validation. Use `libreoffice_recalc.py` for dynamic recalculation when available.
|
||||
|
||||
## Financial Color Standard
|
||||
|
||||
| Cell Role | Font Color | Hex Code |
|
||||
|-----------|-----------|----------|
|
||||
| Hard-coded input / assumption | Blue | `0000FF` |
|
||||
| Formula / computed result | Black | `000000` |
|
||||
| Cross-sheet reference formula | Green | `00B050` |
|
||||
|
||||
## Key Rules
|
||||
|
||||
1. **Formula-First**: Every calculated cell MUST use an Excel formula, not a hardcoded number
|
||||
2. **CREATE → XML template**: Copy minimal template, edit XML directly, pack with `xlsx_pack.py`
|
||||
3. **EDIT → XML**: Never openpyxl round-trip. Use unpack/edit/pack scripts
|
||||
4. **Always produce the output file** — this is the #1 priority
|
||||
5. **Validate before delivery**: `formula_check.py` exit code 0 = safe
|
||||
|
||||
## Utility Scripts
|
||||
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/xlsx_reader.py input.xlsx # structure discovery
|
||||
python3 SKILL_DIR/scripts/formula_check.py file.xlsx --json # formula validation
|
||||
python3 SKILL_DIR/scripts/formula_check.py file.xlsx --report # standardized report
|
||||
python3 SKILL_DIR/scripts/xlsx_unpack.py in.xlsx /tmp/work/ # unpack for XML editing
|
||||
python3 SKILL_DIR/scripts/xlsx_pack.py /tmp/work/ out.xlsx # repack after editing
|
||||
python3 SKILL_DIR/scripts/xlsx_shift_rows.py /tmp/work/ insert 5 1 # shift rows for insertion
|
||||
python3 SKILL_DIR/scripts/xlsx_add_column.py /tmp/work/ --col G ... # add column with formulas
|
||||
python3 SKILL_DIR/scripts/xlsx_insert_row.py /tmp/work/ --at 6 ... # insert row with data
|
||||
```
|
||||
@@ -0,0 +1,691 @@
|
||||
# Build New xlsx from Scratch
|
||||
|
||||
Create new, production-quality xlsx files using the XML approach. NEVER use openpyxl
|
||||
for writing. NEVER hardcode Python-computed values — every derived number must be a
|
||||
live Excel formula.
|
||||
|
||||
---
|
||||
|
||||
## When to Use This Path
|
||||
|
||||
Use this document when the user wants:
|
||||
- A brand-new Excel file that does not yet exist
|
||||
- A generated report, financial model, or data table
|
||||
- Any "create / build / generate / make" request
|
||||
|
||||
If the user provides an existing file to modify, switch to `edit.md` instead.
|
||||
|
||||
---
|
||||
|
||||
## The Non-Negotiable Rules
|
||||
|
||||
Before touching any file, internalize these four rules:
|
||||
|
||||
1. **Formula-First**: Every calculated value (`SUM`, growth rate, ratio, subtotal, etc.)
|
||||
MUST be written as `<f>SUM(B2:B9)</f>`, not as a hardcoded `<v>5000</v>`. Hardcoded
|
||||
numbers go stale when source data changes. Only raw inputs and assumption parameters
|
||||
may be hardcoded values.
|
||||
|
||||
2. **No openpyxl for writing**: The entire file is built by editing XML directly. Python
|
||||
is only allowed for reading/analysis (`pandas.read_excel()`) and for running helper
|
||||
scripts (`xlsx_pack.py`, `formula_check.py`).
|
||||
|
||||
3. **Style encodes meaning**: Blue font = user input/assumption. Black font = formula
|
||||
result. Green font = cross-sheet reference. See `format.md` for the full color system
|
||||
and style index table.
|
||||
|
||||
4. **Validate before delivery**: Run `formula_check.py` and fix all errors before
|
||||
handing the file to the user.
|
||||
|
||||
---
|
||||
|
||||
## Complete Creation Workflow
|
||||
|
||||
### Step 1 — Plan Before Writing
|
||||
|
||||
Define the full structure on paper before touching any XML:
|
||||
|
||||
- **Sheets**: names, order, purpose (e.g., Assumptions / Model / Summary)
|
||||
- **Layout per sheet**: which rows are headers, inputs, formulas, totals
|
||||
- **String inventory**: collect all text labels you will need in sharedStrings
|
||||
- **Style choices**: what number format each column needs (currency, %, integer, year)
|
||||
- **Cross-sheet links**: which sheets pull data from other sheets
|
||||
|
||||
This planning step prevents the costly cycle of adding strings to sharedStrings
|
||||
mid-way and recomputing all indices.
|
||||
|
||||
---
|
||||
|
||||
### Step 2 — Copy Minimal Template
|
||||
|
||||
```bash
|
||||
cp -r SKILL_DIR/templates/minimal_xlsx/ /tmp/xlsx_work/
|
||||
```
|
||||
|
||||
The template gives you a complete, valid 7-file xlsx skeleton:
|
||||
|
||||
```
|
||||
/tmp/xlsx_work/
|
||||
├── [Content_Types].xml ← MIME type registry
|
||||
├── _rels/
|
||||
│ └── .rels ← root relationship (points to workbook.xml)
|
||||
└── xl/
|
||||
├── workbook.xml ← sheet list and calc settings
|
||||
├── styles.xml ← 13 pre-built financial style slots
|
||||
├── sharedStrings.xml ← text string table (starts empty)
|
||||
├── _rels/
|
||||
│ └── workbook.xml.rels ← maps rId → file paths
|
||||
└── worksheets/
|
||||
└── sheet1.xml ← one empty sheet
|
||||
```
|
||||
|
||||
After copying, rename sheets and add content. Do not create files from scratch —
|
||||
always start from the template.
|
||||
|
||||
---
|
||||
|
||||
### Step 3 — Configure Sheet Structure
|
||||
|
||||
#### Single-Sheet Workbook
|
||||
|
||||
The template already has one sheet named "Sheet1". Just change the `name` attribute
|
||||
in `xl/workbook.xml`:
|
||||
|
||||
```xml
|
||||
<sheets>
|
||||
<sheet name="Revenue Model" sheetId="1" r:id="rId1"/>
|
||||
</sheets>
|
||||
```
|
||||
|
||||
No other files need to change for a single-sheet workbook.
|
||||
|
||||
#### Multi-Sheet Workbook
|
||||
|
||||
Four files must be kept in sync. Work through them in this order:
|
||||
|
||||
**IMPORTANT — rId collision rule**: In the template's `workbook.xml.rels`, the IDs
|
||||
`rId1`, `rId2`, and `rId3` are already taken:
|
||||
- `rId1` → `worksheets/sheet1.xml`
|
||||
- `rId2` → `styles.xml`
|
||||
- `rId3` → `sharedStrings.xml`
|
||||
|
||||
New worksheet entries MUST start at `rId4` and count upward.
|
||||
|
||||
**File 1 of 4 — `xl/workbook.xml`** (sheet list):
|
||||
|
||||
```xml
|
||||
<sheets>
|
||||
<sheet name="Assumptions" sheetId="1" r:id="rId1"/>
|
||||
<sheet name="Model" sheetId="2" r:id="rId4"/>
|
||||
<sheet name="Summary" sheetId="3" r:id="rId5"/>
|
||||
</sheets>
|
||||
```
|
||||
|
||||
Special characters in sheet names:
|
||||
- `&` → `&` in XML: `<sheet name="P&L" .../>`
|
||||
- Max 31 characters
|
||||
- Forbidden: `/ \ ? * [ ] :`
|
||||
- Sheet names with spaces need single quotes in formula references: `'Q1 Data'!B5`
|
||||
|
||||
**File 2 of 4 — `xl/_rels/workbook.xml.rels`** (ID → file mapping):
|
||||
|
||||
```xml
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1"
|
||||
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
|
||||
Target="worksheets/sheet1.xml"/>
|
||||
<Relationship Id="rId2"
|
||||
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"
|
||||
Target="styles.xml"/>
|
||||
<Relationship Id="rId3"
|
||||
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
|
||||
Target="sharedStrings.xml"/>
|
||||
<Relationship Id="rId4"
|
||||
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
|
||||
Target="worksheets/sheet2.xml"/>
|
||||
<Relationship Id="rId5"
|
||||
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
|
||||
Target="worksheets/sheet3.xml"/>
|
||||
</Relationships>
|
||||
```
|
||||
|
||||
**File 3 of 4 — `[Content_Types].xml`** (MIME type declarations):
|
||||
|
||||
```xml
|
||||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||
<Default Extension="xml" ContentType="application/xml"/>
|
||||
<Override PartName="/xl/workbook.xml"
|
||||
ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
||||
<Override PartName="/xl/worksheets/sheet1.xml"
|
||||
ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
||||
<Override PartName="/xl/worksheets/sheet2.xml"
|
||||
ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
||||
<Override PartName="/xl/worksheets/sheet3.xml"
|
||||
ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
||||
<Override PartName="/xl/styles.xml"
|
||||
ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
|
||||
<Override PartName="/xl/sharedStrings.xml"
|
||||
ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>
|
||||
</Types>
|
||||
```
|
||||
|
||||
**File 4 of 4 — Create new worksheet XML files**
|
||||
|
||||
Copy `sheet1.xml` to `sheet2.xml` and `sheet3.xml`, then clear the `<sheetData>` content:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<worksheet
|
||||
xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||
<sheetViews>
|
||||
<sheetView workbookViewId="0"/>
|
||||
</sheetViews>
|
||||
<sheetFormatPr defaultRowHeight="15" x14ac:dyDescent="0.25"
|
||||
xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac"/>
|
||||
<sheetData>
|
||||
<!-- Data rows go here -->
|
||||
</sheetData>
|
||||
<pageMargins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3"/>
|
||||
</worksheet>
|
||||
```
|
||||
|
||||
**Sync checklist** — every time you add a sheet, verify all four are consistent:
|
||||
|
||||
| Check | What to verify |
|
||||
|-------|---------------|
|
||||
| `workbook.xml` | New `<sheet name="..." sheetId="N" r:id="rIdX"/>` exists |
|
||||
| `workbook.xml.rels` | New `<Relationship Id="rIdX" ... Target="worksheets/sheetN.xml"/>` exists |
|
||||
| `[Content_Types].xml` | New `<Override PartName="/xl/worksheets/sheetN.xml" .../>` exists |
|
||||
| Filesystem | `xl/worksheets/sheetN.xml` file actually exists |
|
||||
|
||||
---
|
||||
|
||||
### Step 4 — Populate sharedStrings
|
||||
|
||||
All text values (headers, row labels, category names, any string the user will read)
|
||||
must be stored in `xl/sharedStrings.xml`. Cells reference them by 0-based index.
|
||||
|
||||
**Recommended workflow**: collect ALL text you need first, write the complete table once,
|
||||
then fill in indices while writing worksheet XML. This avoids re-counting indices mid-way.
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||||
count="10" uniqueCount="10">
|
||||
<si><t>Item</t></si> <!-- index 0 -->
|
||||
<si><t>FY2023A</t></si> <!-- index 1 -->
|
||||
<si><t>FY2024E</t></si> <!-- index 2 -->
|
||||
<si><t>FY2025E</t></si> <!-- index 3 -->
|
||||
<si><t>YoY Growth</t></si> <!-- index 4 -->
|
||||
<si><t>Revenue</t></si> <!-- index 5 -->
|
||||
<si><t>Cost of Goods Sold</t></si> <!-- index 6 -->
|
||||
<si><t>Gross Profit</t></si> <!-- index 7 -->
|
||||
<si><t>EBITDA</t></si> <!-- index 8 -->
|
||||
<si><t>Net Income</t></si> <!-- index 9 -->
|
||||
</sst>
|
||||
```
|
||||
|
||||
**Attribute rules**:
|
||||
- `uniqueCount` = number of `<si>` elements (unique strings in the table)
|
||||
- `count` = total number of cell references to strings across the entire workbook
|
||||
(if "Revenue" appears in 3 sheets, count is `uniqueCount + 2`)
|
||||
- For new files where each string appears once, `count == uniqueCount`
|
||||
- Both attributes MUST be accurate — wrong values trigger warnings in some Excel versions
|
||||
|
||||
**Special character escaping**:
|
||||
|
||||
```xml
|
||||
<si><t>R&D Expenses</t></si> <!-- & must be & -->
|
||||
<si><t>Revenue < Target</t></si> <!-- < must be < -->
|
||||
<si><t xml:space="preserve"> (note) </t></si> <!-- preserve leading/trailing spaces -->
|
||||
```
|
||||
|
||||
**Helper script**: use `shared_strings_builder.py` to generate the complete
|
||||
`sharedStrings.xml` from a plain list of strings:
|
||||
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/shared_strings_builder.py \
|
||||
"Item" "FY2024" "FY2025" "Revenue" "Gross Profit" \
|
||||
> /tmp/xlsx_work/xl/sharedStrings.xml
|
||||
```
|
||||
|
||||
Or interactively from a file listing one string per line:
|
||||
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/shared_strings_builder.py --file strings.txt \
|
||||
> /tmp/xlsx_work/xl/sharedStrings.xml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 5 — Write Worksheet Data
|
||||
|
||||
Edit each `xl/worksheets/sheetN.xml`. Replace the empty `<sheetData>` with rows
|
||||
and cells.
|
||||
|
||||
#### Cell XML Anatomy
|
||||
|
||||
```
|
||||
<c r="B5" t="s" s="4">
|
||||
↑ ↑ ↑
|
||||
address type style index (from cellXfs in styles.xml)
|
||||
|
||||
<v>3</v>
|
||||
↑
|
||||
value (for t="s": sharedStrings index; for numbers: the number itself)
|
||||
```
|
||||
|
||||
#### Data Type Reference
|
||||
|
||||
| Data | `t` attr | XML Example | Notes |
|
||||
|------|---------|-------------|-------|
|
||||
| Shared string (text) | `s` | `<c r="A1" t="s" s="4"><v>0</v></c>` | `<v>` = sharedStrings index |
|
||||
| Number | omit | `<c r="B2" s="5"><v>1000000</v></c>` | default type, `t` omitted |
|
||||
| Percentage (as decimal) | omit | `<c r="C2" s="7"><v>0.125</v></c>` | 12.5% stored as 0.125 |
|
||||
| Boolean | `b` | `<c r="D1" t="b"><v>1</v></c>` | 1=TRUE, 0=FALSE |
|
||||
| Formula | omit | `<c r="B4" s="2"><f>SUM(B2:B3)</f><v></v></c>` | `<v>` left empty |
|
||||
| Cross-sheet formula | omit | `<c r="C1" s="3"><f>Assumptions!B2</f><v></v></c>` | use s=3 (green) |
|
||||
|
||||
#### A Full Sheet Data Example
|
||||
|
||||
```xml
|
||||
<cols>
|
||||
<col min="1" max="1" width="26" customWidth="1"/> <!-- A: label column -->
|
||||
<col min="2" max="5" width="14" customWidth="1"/> <!-- B-E: data columns -->
|
||||
</cols>
|
||||
<sheetData>
|
||||
|
||||
<!-- Row 1: headers (style 4 = bold header) -->
|
||||
<row r="1" ht="18" customHeight="1">
|
||||
<c r="A1" t="s" s="4"><v>0</v></c> <!-- "Item" -->
|
||||
<c r="B1" t="s" s="4"><v>1</v></c> <!-- "FY2023A" -->
|
||||
<c r="C1" t="s" s="4"><v>2</v></c> <!-- "FY2024E" -->
|
||||
<c r="D1" t="s" s="4"><v>3</v></c> <!-- "FY2025E" -->
|
||||
<c r="E1" t="s" s="4"><v>4</v></c> <!-- "YoY Growth" -->
|
||||
</row>
|
||||
|
||||
<!-- Row 2: Revenue — actual value (input) + formula (computed) -->
|
||||
<row r="2">
|
||||
<c r="A2" t="s" s="1"><v>5</v></c> <!-- "Revenue", blue input label -->
|
||||
<c r="B2" s="5"><v>85000000</v></c> <!-- FY2023A actual: $85M, currency input -->
|
||||
<c r="C2" s="6"><f>B2*(1+Assumptions!C3)</f><v></v></c> <!-- formula, currency -->
|
||||
<c r="D2" s="6"><f>C2*(1+Assumptions!D3)</f><v></v></c>
|
||||
<c r="E2" s="8"><f>D2/C2-1</f><v></v></c> <!-- YoY growth, percentage formula -->
|
||||
</row>
|
||||
|
||||
<!-- Row 3: Gross Profit -->
|
||||
<row r="3">
|
||||
<c r="A3" t="s" s="2"><v>7</v></c> <!-- "Gross Profit", black formula label -->
|
||||
<c r="B3" s="6"><f>B2*Assumptions!B4</f><v></v></c>
|
||||
<c r="C3" s="6"><f>C2*Assumptions!C4</f><v></v></c>
|
||||
<c r="D3" s="6"><f>D2*Assumptions!D4</f><v></v></c>
|
||||
<c r="E3" s="8"><f>D3/C3-1</f><v></v></c>
|
||||
</row>
|
||||
|
||||
<!-- Row 5: SUM total row -->
|
||||
<row r="5">
|
||||
<c r="A5" t="s" s="4"><v>8</v></c> <!-- "EBITDA" -->
|
||||
<c r="B5" s="6"><f>SUM(B2:B4)</f><v></v></c>
|
||||
<c r="C5" s="6"><f>SUM(C2:C4)</f><v></v></c>
|
||||
<c r="D5" s="6"><f>SUM(D2:D4)</f><v></v></c>
|
||||
<c r="E5" s="8"><f>D5/C5-1</f><v></v></c>
|
||||
</row>
|
||||
|
||||
</sheetData>
|
||||
```
|
||||
|
||||
#### Column Width and Freeze Pane
|
||||
|
||||
Column widths go **before** `<sheetData>`, freeze pane goes inside `<sheetView>`:
|
||||
|
||||
```xml
|
||||
<!-- Inside <sheetViews><sheetView ...> — freeze the header row -->
|
||||
<pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/>
|
||||
|
||||
<!-- Before <sheetData> — set column widths -->
|
||||
<cols>
|
||||
<col min="1" max="1" width="28" customWidth="1"/>
|
||||
<col min="2" max="8" width="14" customWidth="1"/>
|
||||
</cols>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 6 — Apply Styles
|
||||
|
||||
The template's `xl/styles.xml` has 13 pre-built semantic style slots (indices 0–12).
|
||||
**Read `format.md` for the complete style index table, color system, and how to add new styles.**
|
||||
|
||||
Quick reference for the most common slots:
|
||||
|
||||
| `s` | Role | Example |
|
||||
|-----|------|---------|
|
||||
| 4 | Header (bold) | Column/row titles |
|
||||
| 5 / 6 | Currency input (blue) / formula (black) | `$#,##0` |
|
||||
| 7 / 8 | Percentage input / formula | `0.0%` |
|
||||
| 11 | Year (no comma) | 2024 not 2,024 |
|
||||
|
||||
Design principle: Blue = human sets this. Black = Excel computes this. Green = cross-sheet.
|
||||
|
||||
If you need a style not in the 13 pre-built slots, follow the append-only procedure in `format.md` section 3.2.
|
||||
|
||||
---
|
||||
|
||||
### Step 7 — Formula Cookbook
|
||||
|
||||
#### XML Formula Syntax Reminder
|
||||
|
||||
Formulas in XML have **no leading `=`**:
|
||||
|
||||
```xml
|
||||
<!-- Excel UI: =SUM(B2:B9) → XML: -->
|
||||
<c r="B10" s="6"><f>SUM(B2:B9)</f><v></v></c>
|
||||
```
|
||||
|
||||
#### Basic Aggregations
|
||||
|
||||
```xml
|
||||
<c r="B10" s="6"><f>SUM(B2:B9)</f><v></v></c>
|
||||
<c r="B11" s="6"><f>AVERAGE(B2:B9)</f><v></v></c>
|
||||
<c r="B12" s="10"><f>COUNT(B2:B9)</f><v></v></c>
|
||||
<c r="B13" s="10"><f>COUNTA(A2:A100)</f><v></v></c>
|
||||
<c r="B14" s="6"><f>MAX(B2:B9)</f><v></v></c>
|
||||
<c r="B15" s="6"><f>MIN(B2:B9)</f><v></v></c>
|
||||
```
|
||||
|
||||
#### Financial Calculations
|
||||
|
||||
```xml
|
||||
<!-- YoY growth rate: current / prior - 1 -->
|
||||
<c r="E5" s="8"><f>D5/C5-1</f><v></v></c>
|
||||
|
||||
<!-- Gross profit: revenue × gross margin -->
|
||||
<c r="B6" s="6"><f>B4*B3</f><v></v></c>
|
||||
|
||||
<!-- EBITDA margin: EBITDA / Revenue -->
|
||||
<c r="B9" s="8"><f>B8/B4</f><v></v></c>
|
||||
|
||||
<!-- Suppress #DIV/0! when denominator may be zero -->
|
||||
<c r="E5" s="8"><f>IF(C5=0,0,D5/C5-1)</f><v></v></c>
|
||||
|
||||
<!-- NPV and IRR (cash flows in B2:B7, discount rate in B1) -->
|
||||
<c r="C1" s="6"><f>NPV(B1,B3:B7)+B2</f><v></v></c>
|
||||
<c r="C2" s="8"><f>IRR(B2:B7)</f><v></v></c>
|
||||
```
|
||||
|
||||
#### Cross-Sheet References
|
||||
|
||||
```xml
|
||||
<!-- No spaces in name: no quotes needed -->
|
||||
<c r="B3" s="3"><f>Assumptions!B5</f><v></v></c>
|
||||
|
||||
<!-- Space in sheet name: single quotes required -->
|
||||
<c r="B3" s="3"><f>'Q1 Data'!B5</f><v></v></c>
|
||||
|
||||
<!-- Ampersand in sheet name (XML-escaped in workbook.xml, but in formula: literal &) -->
|
||||
<c r="B3" s="3"><f>'R&D'!B5</f><v></v></c>
|
||||
|
||||
<!-- Cross-sheet range: SUM of a range in another sheet -->
|
||||
<c r="B10" s="6"><f>SUM(Data!C2:C1000)</f><v></v></c>
|
||||
|
||||
<!-- 3D reference: sum same cell across multiple sheets -->
|
||||
<c r="B5" s="6"><f>SUM(Jan:Dec!B5)</f><v></v></c>
|
||||
```
|
||||
|
||||
Cross-sheet formula cells should use `s="3"` (green) to signal the data origin.
|
||||
|
||||
#### Shared Formulas (Same Pattern Repeated Down a Column)
|
||||
|
||||
When many consecutive cells share the same formula structure with only the row number
|
||||
changing, use shared formulas to keep the XML compact:
|
||||
|
||||
```xml
|
||||
<!-- D2: defines the shared group (si="0", ref="D2:D11") -->
|
||||
<c r="D2" s="8"><f t="shared" ref="D2:D11" si="0">C2/B2-1</f><v></v></c>
|
||||
|
||||
<!-- D3 through D11: reference the same group, no formula text needed -->
|
||||
<c r="D3" s="8"><f t="shared" si="0"/><v></v></c>
|
||||
<c r="D4" s="8"><f t="shared" si="0"/><v></v></c>
|
||||
<c r="D5" s="8"><f t="shared" si="0"/><v></v></c>
|
||||
<c r="D6" s="8"><f t="shared" si="0"/><v></v></c>
|
||||
<c r="D7" s="8"><f t="shared" si="0"/><v></v></c>
|
||||
<c r="D8" s="8"><f t="shared" si="0"/><v></v></c>
|
||||
<c r="D9" s="8"><f t="shared" si="0"/><v></v></c>
|
||||
<c r="D10" s="8"><f t="shared" si="0"/><v></v></c>
|
||||
<c r="D11" s="8"><f t="shared" si="0"/><v></v></c>
|
||||
```
|
||||
|
||||
Excel adjusts relative references automatically (D3 computes `C3/B3-1`, etc.).
|
||||
If you have multiple shared formula groups, assign sequential `si` values (0, 1, 2, …).
|
||||
|
||||
#### Absolute References
|
||||
|
||||
```xml
|
||||
<!-- $B$2 locks to that cell when the formula is copied -->
|
||||
<c r="C5" s="8"><f>B5/$B$2</f><v></v></c>
|
||||
```
|
||||
|
||||
The `$` character needs no XML escaping — write it literally.
|
||||
|
||||
#### Lookup Formulas
|
||||
|
||||
```xml
|
||||
<!-- VLOOKUP: exact match (last arg 0) -->
|
||||
<c r="C5" s="6"><f>VLOOKUP(A5,Assumptions!A:C,2,0)</f><v></v></c>
|
||||
|
||||
<!-- INDEX/MATCH: more flexible -->
|
||||
<c r="C5" s="6"><f>INDEX(B:B,MATCH(A5,A:A,0))</f><v></v></c>
|
||||
|
||||
<!-- XLOOKUP (Excel 2019+) -->
|
||||
<c r="C5" s="6"><f>XLOOKUP(A5,A:A,B:B)</f><v></v></c>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 8 — Pack and Validate
|
||||
|
||||
**Pack**:
|
||||
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/xlsx_pack.py /tmp/xlsx_work/ /path/to/output.xlsx
|
||||
```
|
||||
|
||||
`xlsx_pack.py` will:
|
||||
1. Check that `[Content_Types].xml` exists at the root
|
||||
2. Parse every `.xml` and `.rels` file for well-formedness — abort if any fail
|
||||
3. Create the ZIP archive with correct compression
|
||||
|
||||
**Validate**:
|
||||
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/formula_check.py /path/to/output.xlsx
|
||||
```
|
||||
|
||||
`formula_check.py` will:
|
||||
1. Scan every cell for `<c t="e">` entries (cached error values) — all 7 error types
|
||||
2. Extract sheet name references from every `<f>` formula
|
||||
3. Verify each referenced sheet exists in `workbook.xml`
|
||||
|
||||
Fix every reported error before delivery. Exit code 0 = safe to deliver.
|
||||
|
||||
---
|
||||
|
||||
## Pre-Delivery Checklist
|
||||
|
||||
Run through this list before handing the file to the user:
|
||||
|
||||
- [ ] `formula_check.py` reports 0 errors
|
||||
- [ ] Every calculated cell has `<f>` — not just `<v>` with a number
|
||||
- [ ] `sharedStrings.xml` `count` and `uniqueCount` match actual `<si>` count
|
||||
- [ ] Every cell `s` attribute value is in range `0` to `cellXfs count - 1`
|
||||
- [ ] Every sheet in `workbook.xml` has a matching entry in `workbook.xml.rels`
|
||||
- [ ] Every `worksheets/sheetN.xml` file has a matching `<Override>` in `[Content_Types].xml`
|
||||
- [ ] Year columns use `s="11"` (format `0`, no thousands separator)
|
||||
- [ ] Cross-sheet reference formulas use `s="3"` (green font)
|
||||
- [ ] Assumption inputs use `s="1"` or `s="5"` or `s="7"` (blue font)
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes and Fixes
|
||||
|
||||
| Mistake | Symptom | Fix |
|
||||
|---------|---------|-----|
|
||||
| Formula has leading `=` | Cell shows `=SUM(...)` as text | Remove `=` from `<f>` content |
|
||||
| sharedStrings `count` not updated | Excel warning or blank cells | Count `<si>` elements, update both `count` and `uniqueCount` |
|
||||
| Style index out of range | File corruption / Excel repair | Ensure `s` < `cellXfs count`; append new `<xf>` if needed |
|
||||
| New sheet rId conflicts with styles/sharedStrings rId | Sheet missing or styles lost | New sheets use rId4, rId5, … (rId1-3 are reserved in template) |
|
||||
| Sheet name has `&` unescaped in XML | XML parse error | Use `&` in `workbook.xml` name attribute |
|
||||
| Cross-sheet ref to sheet with space, no quotes | `#REF!` error | Wrap sheet name in single quotes: `'Sheet Name'!B5` |
|
||||
| Cross-sheet ref to non-existent sheet | `#REF!` error | Check `workbook.xml` sheet list vs formula |
|
||||
| Number stored as text (`t="s"`) | Left-aligned, can't sum | Remove `t` attribute from number cells |
|
||||
| Year displayed as `2,024` | Readability issue | Use `s="11"` (numFmtId=1, format `0`) |
|
||||
| Hardcoded Python result instead of formula | "Dead table" — won't update | Replace `<v>N</v>` with `<f>formula</f><v></v>` |
|
||||
|
||||
---
|
||||
|
||||
## Column Letter Reference
|
||||
|
||||
| Col # | Letter | Col # | Letter | Col # | Letter |
|
||||
|-------|--------|-------|--------|-------|--------|
|
||||
| 1 | A | 26 | Z | 27 | AA |
|
||||
| 28 | AB | 52 | AZ | 53 | BA |
|
||||
| 54 | BB | 78 | BZ | 79 | CA |
|
||||
|
||||
Python conversion (use when building formulas programmatically):
|
||||
|
||||
```python
|
||||
def col_letter(n: int) -> str:
|
||||
"""Convert 1-based column number to Excel letter (A, B, ..., Z, AA, AB, ...)."""
|
||||
result = ""
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
result = chr(65 + rem) + result
|
||||
return result
|
||||
|
||||
def col_number(s: str) -> int:
|
||||
"""Convert Excel column letter to 1-based number."""
|
||||
n = 0
|
||||
for c in s.upper():
|
||||
n = n * 26 + (ord(c) - 64)
|
||||
return n
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Typical Scenario Walkthroughs
|
||||
|
||||
### Scenario A — Three-Year Financial Model (Single Sheet)
|
||||
|
||||
Layout: rows 1-12 = Assumptions (blue inputs) / rows 14-30 = Model (black formulas).
|
||||
|
||||
```xml
|
||||
<!-- sharedStrings.xml (excerpt) -->
|
||||
<sst count="8" uniqueCount="8">
|
||||
<si><t>Metric</t></si> <!-- 0 -->
|
||||
<si><t>FY2023A</t></si> <!-- 1 -->
|
||||
<si><t>FY2024E</t></si> <!-- 2 -->
|
||||
<si><t>FY2025E</t></si> <!-- 3 -->
|
||||
<si><t>Revenue Growth</t></si> <!-- 4 -->
|
||||
<si><t>Gross Margin</t></si> <!-- 5 -->
|
||||
<si><t>Revenue</t></si> <!-- 6 -->
|
||||
<si><t>Gross Profit</t></si> <!-- 7 -->
|
||||
</sst>
|
||||
|
||||
<!-- sheet1.xml (excerpt) -->
|
||||
<sheetData>
|
||||
<!-- Header -->
|
||||
<row r="1">
|
||||
<c r="A1" t="s" s="4"><v>0</v></c>
|
||||
<c r="B1" t="s" s="4"><v>1</v></c>
|
||||
<c r="C1" t="s" s="4"><v>2</v></c>
|
||||
<c r="D1" t="s" s="4"><v>3</v></c>
|
||||
</row>
|
||||
<!-- Assumptions (rows 2-3) -->
|
||||
<row r="2">
|
||||
<c r="A2" t="s" s="1"><v>4</v></c> <!-- "Revenue Growth", blue -->
|
||||
<c r="B2" s="7"><v>0</v></c> <!-- FY2023A: n/a, 0% placeholder -->
|
||||
<c r="C2" s="7"><v>0.12</v></c> <!-- FY2024E: 12.0% input -->
|
||||
<c r="D2" s="7"><v>0.15</v></c> <!-- FY2025E: 15.0% input -->
|
||||
</row>
|
||||
<row r="3">
|
||||
<c r="A3" t="s" s="1"><v>5</v></c> <!-- "Gross Margin", blue -->
|
||||
<c r="B3" s="7"><v>0.45</v></c>
|
||||
<c r="C3" s="7"><v>0.46</v></c>
|
||||
<c r="D3" s="7"><v>0.47</v></c>
|
||||
</row>
|
||||
<!-- Model (rows 14-15) -->
|
||||
<row r="14">
|
||||
<c r="A14" t="s" s="2"><v>6</v></c> <!-- "Revenue", black -->
|
||||
<c r="B14" s="5"><v>85000000</v></c> <!-- actual, currency input -->
|
||||
<c r="C14" s="6"><f>B14*(1+C2)</f><v></v></c>
|
||||
<c r="D14" s="6"><f>C14*(1+D2)</f><v></v></c>
|
||||
</row>
|
||||
<row r="15">
|
||||
<c r="A15" t="s" s="2"><v>7</v></c> <!-- "Gross Profit", black -->
|
||||
<c r="B15" s="6"><f>B14*B3</f><v></v></c>
|
||||
<c r="C15" s="6"><f>C14*C3</f><v></v></c>
|
||||
<c r="D15" s="6"><f>D14*D3</f><v></v></c>
|
||||
</row>
|
||||
</sheetData>
|
||||
```
|
||||
|
||||
### Scenario B — Data + Summary (Two Sheets)
|
||||
|
||||
The `Summary` sheet pulls from `Data` using cross-sheet formulas (green, `s="3"`):
|
||||
|
||||
```xml
|
||||
<!-- Summary/sheet2.xml sheetData excerpt -->
|
||||
<sheetData>
|
||||
<row r="1">
|
||||
<c r="A1" t="s" s="4"><v>0</v></c> <!-- "Metric" -->
|
||||
<c r="B1" t="s" s="4"><v>1</v></c> <!-- "Value" -->
|
||||
</row>
|
||||
<row r="2">
|
||||
<c r="A2" t="s" s="0"><v>2</v></c> <!-- "Total Revenue" -->
|
||||
<c r="B2" s="3"><f>SUM(Data!C2:C10000)</f><v></v></c>
|
||||
</row>
|
||||
<row r="3">
|
||||
<c r="A3" t="s" s="0"><v>3</v></c> <!-- "Deal Count" -->
|
||||
<c r="B3" s="3"><f>COUNTA(Data!A2:A10000)</f><v></v></c>
|
||||
</row>
|
||||
<row r="4">
|
||||
<c r="A4" t="s" s="0"><v>4</v></c> <!-- "Avg Deal Size" -->
|
||||
<c r="B4" s="3"><f>IF(B3=0,0,B2/B3)</f><v></v></c>
|
||||
</row>
|
||||
</sheetData>
|
||||
```
|
||||
|
||||
### Scenario C — Multi-Department Consolidation
|
||||
|
||||
`Consolidated` sheet sums the same cells from multiple department sheets:
|
||||
|
||||
```xml
|
||||
<!-- Consolidated/sheet4.xml — summing across Dept_Eng and Dept_Mkt -->
|
||||
<sheetData>
|
||||
<row r="5">
|
||||
<c r="A5" t="s" s="2"><v>0</v></c>
|
||||
<!-- No spaces in sheet names → no quotes needed -->
|
||||
<c r="B5" s="3"><f>Dept_Engineering!B5+Dept_Marketing!B5</f><v></v></c>
|
||||
</row>
|
||||
<row r="6">
|
||||
<c r="A6" t="s" s="2"><v>1</v></c>
|
||||
<c r="B6" s="3"><f>SUM(Dept_Engineering!B6,Dept_Marketing!B6)</f><v></v></c>
|
||||
</row>
|
||||
</sheetData>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What You Must NOT Do
|
||||
|
||||
- Do NOT use openpyxl or any Python library to write the final xlsx file
|
||||
- Do NOT hardcode any calculated value — use `<f>` formulas for every derived number
|
||||
- Do NOT deliver without running `formula_check.py` first
|
||||
- Do NOT set a cell's `s` attribute to a value >= `cellXfs count`
|
||||
- Do NOT modify an existing `<xf>` entry in `styles.xml` — only append new ones
|
||||
- Do NOT add a new sheet without updating all four sync points (workbook.xml,
|
||||
workbook.xml.rels, [Content_Types].xml, actual .xml file)
|
||||
- Do NOT assign new worksheet rIds that overlap with rId1, rId2, or rId3 (reserved
|
||||
for sheet1, styles, sharedStrings in the template)
|
||||
@@ -0,0 +1,684 @@
|
||||
# Minimal-Invasive Editing of Existing xlsx
|
||||
|
||||
Make precise, surgical changes to existing xlsx files while preserving everything you do not touch: styles, macros, pivot tables, charts, sparklines, named ranges, data validation, conditional formatting, and all other embedded content.
|
||||
|
||||
---
|
||||
|
||||
## 1. When to Use This Path
|
||||
|
||||
Use the edit (unpack → XML edit → pack) path whenever the task involves **modifying an existing xlsx file**:
|
||||
|
||||
- Template filling — populating designated input cells with values or formulas
|
||||
- Data updates — replacing outdated numbers, text, or dates in a live file
|
||||
- Content corrections — fixing wrong values, broken formulas, or mistyped labels
|
||||
- Adding new data rows to an existing table
|
||||
- Renaming a sheet
|
||||
- Applying a new style to specific cells
|
||||
|
||||
Do NOT use this path for creating a brand-new workbook from scratch. For that, see `create.md`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Why openpyxl round-trip Is Forbidden for Existing Files
|
||||
|
||||
openpyxl `load_workbook()` followed by `workbook.save()` is a **destructive operation** on any file that contains advanced features. The library silently drops content it does not understand:
|
||||
|
||||
| Feature | openpyxl behavior | Consequence |
|
||||
|---------|-------------------|-------------|
|
||||
| VBA macros (`vbaProject.bin`) | Dropped entirely | All automation is lost; file saved as `.xlsx` not `.xlsm` |
|
||||
| Pivot tables (`xl/pivotTables/`) | Dropped | Interactive analysis is destroyed |
|
||||
| Slicers | Dropped | Filter UI is lost |
|
||||
| Sparklines (`<sparklineGroups>`) | Dropped | In-cell mini-charts disappear |
|
||||
| Chart formatting details | Partially lost | Series colors, custom axes may revert |
|
||||
| Print area / page breaks | Sometimes lost | Print layout changes |
|
||||
| Custom XML parts | Dropped | Third-party data bindings broken |
|
||||
| Theme-linked colors | May be de-themed | Colors converted to absolute, breaking theme switching |
|
||||
|
||||
Even on a "plain" file without these features, openpyxl may normalize whitespace in XML that Excel relies on, alter namespace declarations, or reset `calcMode` flags.
|
||||
|
||||
**The rule is absolute: never open an existing file with openpyxl for the purpose of re-saving it.**
|
||||
|
||||
The XML direct-edit approach is safe because it operates on the raw bytes. You only change the nodes you touch. Everything else is byte-equivalent to the original.
|
||||
|
||||
---
|
||||
|
||||
## 3. Standard Operating Procedure
|
||||
|
||||
### Step 1 — Unpack
|
||||
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/xlsx_unpack.py input.xlsx /tmp/xlsx_work/
|
||||
```
|
||||
|
||||
The script unzips the xlsx, pretty-prints every XML and `.rels` file, and prints a categorized inventory of key files plus a warning if high-risk content is detected (VBA, pivot tables, charts).
|
||||
|
||||
Read the printed output carefully before proceeding. If the script reports `xl/vbaProject.bin` or `xl/pivotTables/`, follow the constraints in Section 7.
|
||||
|
||||
### Step 2 — Reconnaissance
|
||||
|
||||
Map the structure before touching anything.
|
||||
|
||||
**Identify sheet names and their XML files:**
|
||||
|
||||
```
|
||||
xl/workbook.xml → <sheet name="Revenue" sheetId="1" r:id="rId1"/>
|
||||
xl/_rels/workbook.xml.rels → <Relationship Id="rId1" Target="worksheets/sheet1.xml"/>
|
||||
```
|
||||
|
||||
The sheet named "Revenue" lives in `xl/worksheets/sheet1.xml`. Always resolve this mapping before editing a worksheet.
|
||||
|
||||
**Understand the shared strings table:**
|
||||
|
||||
```bash
|
||||
# Count existing entries in xl/sharedStrings.xml
|
||||
grep -c "<si>" /tmp/xlsx_work/xl/sharedStrings.xml
|
||||
```
|
||||
|
||||
Every text cell uses a zero-based index into this table. Know the current count before appending.
|
||||
|
||||
**Understand the styles table:**
|
||||
|
||||
```bash
|
||||
# Count existing cellXfs entries
|
||||
grep -c "<xf " /tmp/xlsx_work/xl/styles.xml
|
||||
```
|
||||
|
||||
New style slots are appended after existing ones. The index of the first new slot = current count.
|
||||
|
||||
**Scan for high-risk XML regions in the target worksheet:**
|
||||
|
||||
Look for these elements in the target `sheet*.xml` before editing:
|
||||
|
||||
- `<mergeCell>` — merged cell ranges; row/column insertion shifts these
|
||||
- `<conditionalFormatting>` — condition ranges; row/column insertion shifts these
|
||||
- `<dataValidations>` — validation ranges; row/column insertion shifts these
|
||||
- `<tableParts>` — table definitions; row insertion inside a table needs `<tableColumn>` updates
|
||||
- `<sparklineGroups>` — sparklines; preserve without modification
|
||||
|
||||
### Step 3 — Map Intent to Minimal XML Changes
|
||||
|
||||
Before writing a single character, produce a written list of exactly which XML nodes change. This prevents scope creep.
|
||||
|
||||
| User intent | Files to change | Nodes to change |
|
||||
|-------------|----------------|-----------------|
|
||||
| Change a cell's numeric value | `xl/worksheets/sheetN.xml` | `<v>` inside target `<c>` |
|
||||
| Change a cell's text | `xl/sharedStrings.xml` (append) + `xl/worksheets/sheetN.xml` | New `<si>`, update cell `<v>` index |
|
||||
| Change a cell's formula | `xl/worksheets/sheetN.xml` | `<f>` text inside target `<c>` |
|
||||
| Add a new data row at the bottom | `xl/worksheets/sheetN.xml` + possibly `xl/sharedStrings.xml` | Append `<row>` element |
|
||||
| Apply a new style to cells | `xl/styles.xml` + `xl/worksheets/sheetN.xml` | Append `<xf>` in `<cellXfs>`, update `s` attribute on `<c>` |
|
||||
| Rename a sheet | `xl/workbook.xml` | `name` attribute on `<sheet>` element |
|
||||
| Rename a sheet (with cross-sheet formulas) | `xl/workbook.xml` + all `xl/worksheets/*.xml` | `name` attribute + `<f>` text referencing old name |
|
||||
|
||||
### Step 4 — Execute Changes
|
||||
|
||||
Use the Edit tool. Edit the minimum. Never rewrite whole files.
|
||||
|
||||
See Section 4 for precise XML patterns for each operation type.
|
||||
|
||||
### Step 5 — Cascade Check
|
||||
|
||||
After any change that shifts row or column positions, audit all affected XML regions. See Section 5.
|
||||
|
||||
### Step 6 — Pack and Validate
|
||||
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/xlsx_pack.py /tmp/xlsx_work/ output.xlsx
|
||||
python3 SKILL_DIR/scripts/formula_check.py output.xlsx
|
||||
```
|
||||
|
||||
The pack script validates XML well-formedness before creating the ZIP. Fix any reported parse errors before packing. After packing, run `formula_check.py` to confirm no formula errors were introduced.
|
||||
|
||||
---
|
||||
|
||||
## 4. Precise XML Patterns for Common Edits
|
||||
|
||||
### 4.1 Changing a Numeric Cell Value
|
||||
|
||||
Find the `<c r="B5">` element in the worksheet XML and replace the `<v>` text.
|
||||
|
||||
**Before:**
|
||||
```xml
|
||||
<c r="B5">
|
||||
<v>1000</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**After (new value 1500):**
|
||||
```xml
|
||||
<c r="B5">
|
||||
<v>1500</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Do not add or remove the `s` attribute (style) unless explicitly changing the style.
|
||||
- Do not add a `t` attribute — numbers omit `t` or use `t="n"`.
|
||||
- Do not change the `r` attribute (cell reference).
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Changing a Text Cell Value
|
||||
|
||||
Text cells reference the shared strings table by index (`t="s"`). You cannot edit the string in-place without affecting every other cell that uses the same index. The safe approach is to append a new entry.
|
||||
|
||||
**Before — shared strings file (`xl/sharedStrings.xml`):**
|
||||
```xml
|
||||
<sst count="4" uniqueCount="4">
|
||||
<si><t>Revenue</t></si>
|
||||
<si><t>Cost</t></si>
|
||||
<si><t>Margin</t></si>
|
||||
<si><t>Old Label</t></si>
|
||||
</sst>
|
||||
```
|
||||
|
||||
**After — append new string, increment counts:**
|
||||
```xml
|
||||
<sst count="5" uniqueCount="5">
|
||||
<si><t>Revenue</t></si>
|
||||
<si><t>Cost</t></si>
|
||||
<si><t>Margin</t></si>
|
||||
<si><t>Old Label</t></si>
|
||||
<si><t>New Label</t></si>
|
||||
</sst>
|
||||
```
|
||||
|
||||
New string is at index 4 (zero-based).
|
||||
|
||||
**Before — cell in worksheet XML:**
|
||||
```xml
|
||||
<c r="A7" t="s">
|
||||
<v>3</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**After — point to new index:**
|
||||
```xml
|
||||
<c r="A7" t="s">
|
||||
<v>4</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Never modify or delete existing `<si>` entries. Only append.
|
||||
- Both `count` and `uniqueCount` must be incremented together.
|
||||
- If the new string contains `&`, `<`, or `>`, escape them: `&`, `<`, `>`.
|
||||
- If the string has leading or trailing spaces, add `xml:space="preserve"` to `<t>`:
|
||||
```xml
|
||||
<si><t xml:space="preserve"> indented text </t></si>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.3 Changing a Formula
|
||||
|
||||
Formulas are stored in `<f>` elements **without a leading `=`** (unlike what you type in Excel's UI).
|
||||
|
||||
**Before:**
|
||||
```xml
|
||||
<c r="C10">
|
||||
<f>SUM(C2:C9)</f>
|
||||
<v>4800</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**After (extended range):**
|
||||
```xml
|
||||
<c r="C10">
|
||||
<f>SUM(C2:C11)</f>
|
||||
<v></v>
|
||||
</c>
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Clear `<v>` to an empty string when changing the formula. The cached value is now stale.
|
||||
- Do not add `t="s"` or any type attribute to formula cells. The `t` attribute is absent or uses a result-type value, not a formula marker.
|
||||
- Cross-sheet references use `SheetName!CellRef`. If the sheet name contains spaces, wrap in single quotes: `'Q1 Data'!B5`.
|
||||
- The `<f>` text must not include the leading `=`.
|
||||
|
||||
**Before (converting a hardcoded value to a live formula):**
|
||||
```xml
|
||||
<c r="D15">
|
||||
<v>95000</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**After:**
|
||||
```xml
|
||||
<c r="D15">
|
||||
<f>SUM(D2:D14)</f>
|
||||
<v></v>
|
||||
</c>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Adding a New Data Row
|
||||
|
||||
Append after the last `<row>` element inside `<sheetData>`. Row numbers in OOXML are 1-based and must be sequential.
|
||||
|
||||
**Before (last row is row 10):**
|
||||
```xml
|
||||
<row r="10">
|
||||
<c r="A10" t="s"><v>3</v></c>
|
||||
<c r="B10"><v>2023</v></c>
|
||||
<c r="C10"><v>88000</v></c>
|
||||
<c r="D10"><f>C10*1.1</f><v></v></c>
|
||||
</row>
|
||||
</sheetData>
|
||||
```
|
||||
|
||||
**After (new row 11 appended):**
|
||||
```xml
|
||||
<row r="10">
|
||||
<c r="A10" t="s"><v>3</v></c>
|
||||
<c r="B10"><v>2023</v></c>
|
||||
<c r="C10"><v>88000</v></c>
|
||||
<c r="D10"><f>C10*1.1</f><v></v></c>
|
||||
</row>
|
||||
<row r="11">
|
||||
<c r="A11" t="s"><v>4</v></c>
|
||||
<c r="B11"><v>2024</v></c>
|
||||
<c r="C11"><v>96000</v></c>
|
||||
<c r="D11"><f>C11*1.1</f><v></v></c>
|
||||
</row>
|
||||
</sheetData>
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Every `<c>` inside the row must have `r` set to the correct cell address (e.g., `A11`).
|
||||
- Text cells need `t="s"` and a sharedStrings index in `<v>`. Numeric cells omit `t`.
|
||||
- Formula cells use `<f>` and an empty `<v>`.
|
||||
- Copy the `s` attribute from the row above if you want matching styles. Do not invent a style index that does not exist in `styles.xml`.
|
||||
- If the sheet contains a `<dimension>` element (e.g., `<dimension ref="A1:D10"/>`), update it to include the new row: `<dimension ref="A1:D11"/>`.
|
||||
- If the sheet contains a `<tableparts>` referencing a table, update the table's `ref` attribute in the corresponding `xl/tables/tableN.xml` file.
|
||||
|
||||
---
|
||||
|
||||
### 4.5 Adding a New Column
|
||||
|
||||
Append new `<c>` elements to each existing `<row>` and, if present, update the `<cols>` section.
|
||||
|
||||
**Before (rows have columns A–C):**
|
||||
```xml
|
||||
<cols>
|
||||
<col min="1" max="3" width="14" customWidth="1"/>
|
||||
</cols>
|
||||
<sheetData>
|
||||
<row r="1">
|
||||
<c r="A1" t="s"><v>0</v></c>
|
||||
<c r="B1" t="s"><v>1</v></c>
|
||||
<c r="C1" t="s"><v>2</v></c>
|
||||
</row>
|
||||
<row r="2">
|
||||
<c r="A2"><v>100</v></c>
|
||||
<c r="B2"><v>200</v></c>
|
||||
<c r="C2"><v>300</v></c>
|
||||
</row>
|
||||
</sheetData>
|
||||
```
|
||||
|
||||
**After (adding column D):**
|
||||
```xml
|
||||
<cols>
|
||||
<col min="1" max="3" width="14" customWidth="1"/>
|
||||
<col min="4" max="4" width="14" customWidth="1"/>
|
||||
</cols>
|
||||
<sheetData>
|
||||
<row r="1">
|
||||
<c r="A1" t="s"><v>0</v></c>
|
||||
<c r="B1" t="s"><v>1</v></c>
|
||||
<c r="C1" t="s"><v>2</v></c>
|
||||
<c r="D1" t="s"><v>5</v></c>
|
||||
</row>
|
||||
<row r="2">
|
||||
<c r="A2"><v>100</v></c>
|
||||
<c r="B2"><v>200</v></c>
|
||||
<c r="C2"><v>300</v></c>
|
||||
<c r="D2"><f>A2+B2+C2</f><v></v></c>
|
||||
</row>
|
||||
</sheetData>
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Adding a column at the end (after the last existing column) is safe — no existing formula references shift.
|
||||
- Inserting a column in the middle shifts all columns to the right, which requires the same cascade updates as row insertion (see Section 5).
|
||||
- Update the `<dimension>` element if present.
|
||||
|
||||
---
|
||||
|
||||
### 4.6 Modifying or Adding Styles
|
||||
|
||||
Styles use a multi-level indirect reference chain. Read `ooxml-cheatsheet.md` for the full chain. The key rule: **only append new entries, never modify existing ones**.
|
||||
|
||||
**Scenario:** Add a blue-font style (for hardcoded input cells) that doesn't yet exist.
|
||||
|
||||
**Step 1 — Check if a matching font already exists in `xl/styles.xml`:**
|
||||
```xml
|
||||
<!-- Look inside <fonts> for an existing blue font -->
|
||||
<font>
|
||||
<color rgb="000000FF"/>
|
||||
<!-- other attributes -->
|
||||
</font>
|
||||
```
|
||||
|
||||
If found, note its index (zero-based position in the `<fonts>` list). If not found, append.
|
||||
|
||||
**Step 2 — Append the new font if needed:**
|
||||
|
||||
Before:
|
||||
```xml
|
||||
<fonts count="3">
|
||||
<font>...</font> <!-- index 0 -->
|
||||
<font>...</font> <!-- index 1 -->
|
||||
<font>...</font> <!-- index 2 -->
|
||||
</fonts>
|
||||
```
|
||||
|
||||
After:
|
||||
```xml
|
||||
<fonts count="4">
|
||||
<font>...</font> <!-- index 0 -->
|
||||
<font>...</font> <!-- index 1 -->
|
||||
<font>...</font> <!-- index 2 -->
|
||||
<font>
|
||||
<b/>
|
||||
<sz val="11"/>
|
||||
<color rgb="000000FF"/>
|
||||
<name val="Calibri"/>
|
||||
</font> <!-- index 3 (new) -->
|
||||
</fonts>
|
||||
```
|
||||
|
||||
**Step 3 — Append a new `<xf>` in `<cellXfs>`:**
|
||||
|
||||
Before:
|
||||
```xml
|
||||
<cellXfs count="5">
|
||||
<xf .../> <!-- index 0 -->
|
||||
<xf .../> <!-- index 1 -->
|
||||
<xf .../> <!-- index 2 -->
|
||||
<xf .../> <!-- index 3 -->
|
||||
<xf .../> <!-- index 4 -->
|
||||
</cellXfs>
|
||||
```
|
||||
|
||||
After:
|
||||
```xml
|
||||
<cellXfs count="6">
|
||||
<xf .../> <!-- index 0 -->
|
||||
<xf .../> <!-- index 1 -->
|
||||
<xf .../> <!-- index 2 -->
|
||||
<xf .../> <!-- index 3 -->
|
||||
<xf .../> <!-- index 4 -->
|
||||
<xf numFmtId="0" fontId="3" fillId="0" borderId="0" xfId="0"
|
||||
applyFont="1"/> <!-- index 5 (new) -->
|
||||
</cellXfs>
|
||||
```
|
||||
|
||||
**Step 4 — Apply to target cells:**
|
||||
|
||||
Before:
|
||||
```xml
|
||||
<c r="B3">
|
||||
<v>0.08</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
After:
|
||||
```xml
|
||||
<c r="B3" s="5">
|
||||
<v>0.08</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Never delete or reorder existing entries in `<fonts>`, `<fills>`, `<borders>`, `<cellXfs>`.
|
||||
- Always update the `count` attribute when appending.
|
||||
- The new `cellXfs` index = the old `count` value before appending (zero-based: if count was 5, new index is 5).
|
||||
- Custom `numFmt` IDs must be 164 or above. IDs 0–163 are built-in and must not be re-declared.
|
||||
- If the desired style already exists elsewhere in the file (on a similar cell), reuse its `s` index rather than creating a duplicate.
|
||||
|
||||
---
|
||||
|
||||
### 4.7 Renaming a Sheet
|
||||
|
||||
**Only `xl/workbook.xml` needs to change** — unless cross-sheet formulas reference the old name.
|
||||
|
||||
**Before (`xl/workbook.xml`):**
|
||||
```xml
|
||||
<sheet name="Sheet1" sheetId="1" r:id="rId1"/>
|
||||
```
|
||||
|
||||
**After:**
|
||||
```xml
|
||||
<sheet name="Revenue" sheetId="1" r:id="rId1"/>
|
||||
```
|
||||
|
||||
**If any formula in any worksheet references the old name, update those too:**
|
||||
|
||||
Before (`xl/worksheets/sheet2.xml`):
|
||||
```xml
|
||||
<c r="B5"><f>Sheet1!C10</f><v></v></c>
|
||||
```
|
||||
|
||||
After:
|
||||
```xml
|
||||
<c r="B5"><f>Revenue!C10</f><v></v></c>
|
||||
```
|
||||
|
||||
If the new name contains spaces:
|
||||
```xml
|
||||
<c r="B5"><f>'Q1 Revenue'!C10</f><v></v></c>
|
||||
```
|
||||
|
||||
Scan all worksheet XML files for the old name:
|
||||
```bash
|
||||
grep -r "Sheet1!" /tmp/xlsx_work/xl/worksheets/
|
||||
```
|
||||
|
||||
Rules:
|
||||
- The `.rels` file and `[Content_Types].xml` do NOT need to change — they reference the XML file path, not the sheet name.
|
||||
- `sheetId` must not change; it is a stable internal identifier.
|
||||
- Sheet names are case-sensitive in formula references.
|
||||
|
||||
---
|
||||
|
||||
## 5. High-Risk Operations — Cascade Effects
|
||||
|
||||
### 5.1 Inserting a Row in the Middle
|
||||
|
||||
Inserting a row at position N shifts all rows from N downward. Every reference to those rows in every XML file must be updated.
|
||||
|
||||
**Files to check and update:**
|
||||
|
||||
| XML region | What to update | Example shift |
|
||||
|------------|---------------|---------------|
|
||||
| Worksheet `<row r="...">` attributes | Increment row number for all rows >= N | `r="7"` → `r="8"` |
|
||||
| All `<c r="...">` within those rows | Increment row number in cell address | `r="A7"` → `r="A8"` |
|
||||
| All `<f>` formula text in any sheet | Shift absolute row references >= N | `B7` → `B8` |
|
||||
| `<mergeCell ref="...">` | Shift start and end rows | `A7:C7` → `A8:C8` |
|
||||
| `<conditionalFormatting sqref="...">` | Shift range | `A5:D20` → `A5:D21` |
|
||||
| `<dataValidations sqref="...">` | Shift range | `B6:B50` → `B7:B51` |
|
||||
| `xl/charts/chartN.xml` data source ranges | Shift series ranges | `Sheet1!$B$5:$B$20` → `Sheet1!$B$6:$B$21` |
|
||||
| `xl/pivotTables/*.xml` source ranges | Shift source data range | Handle with extreme care — see Section 7 |
|
||||
| `<dimension ref="...">` | Expand to include new extent | `A1:D20` → `A1:D21` |
|
||||
| `xl/tables/tableN.xml` `ref` attribute | Expand table boundary | `A1:D20` → `A1:D21` |
|
||||
|
||||
**Do not attempt row insertion manually in large or formula-heavy files.** Use the dedicated shift script instead:
|
||||
|
||||
```bash
|
||||
# Insert 1 row at row 5: all rows 5 and below shift down by 1
|
||||
python3 SKILL_DIR/scripts/xlsx_shift_rows.py /tmp/xlsx_work/ insert 5 1
|
||||
|
||||
# Delete 1 row at row 8: all rows 9 and above shift up by 1
|
||||
python3 SKILL_DIR/scripts/xlsx_shift_rows.py /tmp/xlsx_work/ delete 8 1
|
||||
```
|
||||
|
||||
The script updates in one pass: `<row r="...">` attributes, `<c r="...">` cell addresses, all `<f>` formula text across every worksheet, `<mergeCell>` ranges, `<conditionalFormatting sqref="...">`, `<dataValidation sqref="...">`, `<dimension ref="...">`, table `ref` attributes in `xl/tables/`, chart series ranges in `xl/charts/`, and pivot cache source ranges in `xl/pivotCaches/`.
|
||||
|
||||
**After running the shift script, always repack and validate:**
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/xlsx_pack.py /tmp/xlsx_work/ output.xlsx
|
||||
python3 SKILL_DIR/scripts/formula_check.py output.xlsx
|
||||
```
|
||||
|
||||
**What the script does NOT update (review manually):**
|
||||
- Named ranges in `xl/workbook.xml` `<definedNames>` — check and update if they reference shifted rows.
|
||||
- Structured table references (`Table[@Column]`) inside formulas.
|
||||
- External workbook links in `xl/externalLinks/`.
|
||||
|
||||
### 5.2 Inserting a Column in the Middle
|
||||
|
||||
Same cascade logic as row insertion, but for columns. Column references in formulas (`B`, `$C`, etc.) and in merged cell ranges, conditional formatting ranges, and chart data sources all need updating.
|
||||
|
||||
Column letter shifting is harder to automate safely. Prefer **appending columns at the end** whenever possible.
|
||||
|
||||
### 5.3 Deleting a Row or Column
|
||||
|
||||
Deletion is more dangerous than insertion because any formula that referenced a deleted row or column will become `#REF!`. Before deleting:
|
||||
|
||||
1. Search all `<f>` elements for references to the deleted range.
|
||||
2. If any formula references a cell in the deleted row/column, do not delete — instead, either clear the row's data or consult the user.
|
||||
3. After deletion, shift all references to rows/columns beyond the deletion point downward/leftward.
|
||||
|
||||
---
|
||||
|
||||
## 6. Template Filling — Identifying and Populating Input Cells
|
||||
|
||||
Templates designate certain cells as input zones. Common patterns to recognize them:
|
||||
|
||||
### 6.1 How Templates Signal Input Zones
|
||||
|
||||
| Signal | XML manifestation | What to look for |
|
||||
|--------|-------------------|-----------------|
|
||||
| Blue font color | `s` attribute pointing to a `cellXfs` entry with `fontId` → `<color rgb="000000FF"/>` | Check `styles.xml` to decode `s` values |
|
||||
| Yellow fill (highlight) | `s` → `fillId` → `<fill><patternFill><fgColor rgb="00FFFF00"/>` | |
|
||||
| Empty `<v>` element | `<c r="B5"><v></v></c>` or cell entirely absent from `<row>` | The cell has no value yet |
|
||||
| Comment/annotation near cell | `xl/comments1.xml` with `ref="B5"` | Comments often label input fields |
|
||||
| Named ranges | `xl/workbook.xml` `<definedName>` elements | Template may define `InputRevenue` etc. |
|
||||
|
||||
### 6.2 Filling a Template Cell
|
||||
|
||||
Do not change `s` attributes. Do not change `t` attributes unless you must change from empty to typed. Only change `<v>` or add `<f>`.
|
||||
|
||||
**Before (empty input cell with style preserved):**
|
||||
```xml
|
||||
<c r="C5" s="3">
|
||||
<v></v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**After (filled with a number, style unchanged):**
|
||||
```xml
|
||||
<c r="C5" s="3">
|
||||
<v>125000</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**After (filled with text — requires shared string entry first):**
|
||||
```xml
|
||||
<!-- 1. Append to sharedStrings.xml: <si><t>North Region</t></si> at index 7 -->
|
||||
<c r="C5" t="s" s="3">
|
||||
<v>7</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**After (filled with a formula, preserving style):**
|
||||
```xml
|
||||
<c r="C5" s="3">
|
||||
<f>Assumptions!D12</f>
|
||||
<v></v>
|
||||
</c>
|
||||
```
|
||||
|
||||
### 6.3 Locating Input Zones Without Opening the File in Excel
|
||||
|
||||
After unpacking, decode the style index on suspected input cells to determine if they have the template's input color:
|
||||
|
||||
1. Note the `s` value on the cell (e.g., `s="4"`).
|
||||
2. In `xl/styles.xml`, find `<cellXfs>` and look at the 5th entry (index 4).
|
||||
3. Note its `fontId` (e.g., `fontId="2"`).
|
||||
4. In `<fonts>`, look at the 3rd entry (index 2) and check for `<color rgb="000000FF"/>` (blue) or other input marker.
|
||||
|
||||
If the template uses named ranges as input fields, read them from `xl/workbook.xml`:
|
||||
```xml
|
||||
<definedNames>
|
||||
<definedName name="InputGrowthRate">Assumptions!$B$5</definedName>
|
||||
<definedName name="InputDiscountRate">Assumptions!$B$6</definedName>
|
||||
</definedNames>
|
||||
```
|
||||
|
||||
Fill the target cells (`Assumptions!B5`, `Assumptions!B6`) directly.
|
||||
|
||||
### 6.4 Template Filling Rules
|
||||
|
||||
- Fill only cells the template designated as inputs. Do not fill cells that are formula-driven.
|
||||
- Do not apply new styles when filling. The template's formatting is the deliverable.
|
||||
- Do not add or remove rows inside the template's data area unless the template explicitly has an "append here" zone.
|
||||
- After filling, verify that no formula errors were introduced: some templates have input-validation formulas that produce `#VALUE!` if the wrong data type is entered.
|
||||
|
||||
---
|
||||
|
||||
## 7. Files You Must Never Modify
|
||||
|
||||
### 7.1 Absolute no-touch list
|
||||
|
||||
| File / location | Why |
|
||||
|-----------------|-----|
|
||||
| `xl/vbaProject.bin` | Binary VBA bytecode. Any byte modification corrupts the macro project. Editing even one bit makes the macros fail to load. |
|
||||
| `xl/pivotCaches/pivotCacheDefinition*.xml` | The cache definition ties the pivot table to its source data. Editing it without also updating the corresponding `pivotTable*.xml` will corrupt the pivot. |
|
||||
| `xl/pivotTables/*.xml` | Pivot table XML is tightly coupled with the cache definition and with internal state Excel rebuilds on load. Do not edit. If you shifted rows and the pivot's source range now points to wrong data, update only the `<cacheSource>` range in the cache definition, and only the `ref` attribute in the pivot table — no other changes. |
|
||||
| `xl/slicers/*.xml` | Slicers are connected to specific cache IDs and pivot fields. Breaking these connections silently corrupts the file. |
|
||||
| `xl/connections.xml` | External data connections. Editing breaks live data refresh. |
|
||||
| `xl/externalLinks/` | External workbook links. The binary `.bin` files in here must not be modified. |
|
||||
|
||||
### 7.2 Conditionally safe files (update only specific attributes)
|
||||
|
||||
| File | What you may update | What to leave alone |
|
||||
|------|--------------------|--------------------|
|
||||
| `xl/charts/chartN.xml` | Data series range references (`<numRef><f>`) after a row/column shift | Chart type, formatting, layout |
|
||||
| `xl/tables/tableN.xml` | `ref` attribute on `<table>` after adding rows | Column definitions, style info |
|
||||
| `xl/pivotCaches/pivotCacheDefinition*.xml` | `ref` attribute on `<cacheSource><worksheetSource>` after shifting source data | All other content |
|
||||
|
||||
---
|
||||
|
||||
## 8. Validation After Every Edit
|
||||
|
||||
Never skip validation. Even a one-character change in a formula can cause cascading errors.
|
||||
|
||||
```bash
|
||||
# Pack
|
||||
python3 SKILL_DIR/scripts/xlsx_pack.py /tmp/xlsx_work/ output.xlsx
|
||||
|
||||
# Static formula validation (always run)
|
||||
python3 SKILL_DIR/scripts/formula_check.py output.xlsx
|
||||
|
||||
# Dynamic validation (if LibreOffice available)
|
||||
python3 SKILL_DIR/scripts/libreoffice_recalc.py output.xlsx /tmp/recalc.xlsx
|
||||
python3 SKILL_DIR/scripts/formula_check.py /tmp/recalc.xlsx
|
||||
```
|
||||
|
||||
If `formula_check.py` reports any error:
|
||||
1. Unpack the output file again (it is the packed version).
|
||||
2. Locate the reported cell in the worksheet XML.
|
||||
3. Fix the `<f>` element.
|
||||
4. Repack and re-validate.
|
||||
|
||||
Do not deliver the file until `formula_check.py` reports zero errors.
|
||||
|
||||
---
|
||||
|
||||
## 9. Absolute Rules Summary
|
||||
|
||||
| Rule | Rationale |
|
||||
|------|-----------|
|
||||
| Never use openpyxl `load_workbook` + `save` on an existing file | Round-trip destroys pivot tables, VBA, sparklines, slicers |
|
||||
| Never delete or reorder existing `<si>` entries in sharedStrings | Breaks every cell referencing that index |
|
||||
| Never delete or reorder existing `<xf>` entries in `<cellXfs>` | Breaks every cell using that style index |
|
||||
| Never modify `vbaProject.bin` | Binary file; any change corrupts VBA |
|
||||
| Never change `sheetId` when renaming a sheet | Internal ID is stable; changing it breaks relationships |
|
||||
| Never skip post-edit validation | Leaves broken references undetected |
|
||||
| Never edit more XML nodes than required | Extra changes risk introducing subtle corruption |
|
||||
| Clear `<v>` to empty string when changing a formula | Prevents stale cached value from misleading downstream consumers |
|
||||
| Append-only to sharedStrings | Existing indexes must remain valid |
|
||||
| Append-only to styles collections | Existing style indexes must remain valid |
|
||||
@@ -0,0 +1,37 @@
|
||||
# FIX — Repair Broken Formulas in an Existing xlsx
|
||||
|
||||
This is an EDIT task. You MUST preserve all original sheets and data. Never create a new workbook.
|
||||
|
||||
## Workflow
|
||||
|
||||
```bash
|
||||
# Step 1: Identify errors
|
||||
python3 SKILL_DIR/scripts/formula_check.py input.xlsx --json
|
||||
|
||||
# Step 2: Unpack
|
||||
python3 SKILL_DIR/scripts/xlsx_unpack.py input.xlsx /tmp/xlsx_work/
|
||||
|
||||
# Step 3: Fix each broken <f> element in the worksheet XML using the Edit tool
|
||||
# (see Error-to-Fix mapping below)
|
||||
|
||||
# Step 4: Pack and validate
|
||||
python3 SKILL_DIR/scripts/xlsx_pack.py /tmp/xlsx_work/ output.xlsx
|
||||
python3 SKILL_DIR/scripts/formula_check.py output.xlsx
|
||||
```
|
||||
|
||||
## Error-to-Fix Mapping
|
||||
|
||||
| Error | Fix Strategy |
|
||||
|-------|-------------|
|
||||
| `#DIV/0!` | Wrap: `IFERROR(original_formula, "-")` |
|
||||
| `#NAME?` | Fix misspelled function (e.g. `SUMM` → `SUM`) |
|
||||
| `#REF!` | Reconstruct the broken reference |
|
||||
| `#VALUE!` | Fix type mismatch |
|
||||
|
||||
For the full list of Excel error types and advanced diagnostics, see `validate.md`.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- The output MUST contain the same sheets as the input. Do NOT create a new workbook.
|
||||
- Only modify the specific `<f>` elements that are broken — everything else must be untouched.
|
||||
- After packing, always run `formula_check.py` to confirm all errors are resolved.
|
||||
@@ -0,0 +1,768 @@
|
||||
# Financial Formatting & Output Standards — Complete Agent Guide
|
||||
|
||||
> This document is the complete reference manual for the agent when applying professional financial formatting to xlsx files. All operations target direct XML surgery on `xl/styles.xml` without using openpyxl. Every operational step provides ready-to-use XML snippets.
|
||||
|
||||
---
|
||||
|
||||
## 1. When to Use This Path
|
||||
|
||||
This document (FORMAT path) applies to the following two scenarios:
|
||||
|
||||
**Scenario A — Dedicated Formatting of an Existing File**
|
||||
The user provides an existing xlsx file and requests that financial modeling formatting standards be applied or unified. The starting point is to unpack the file, audit the existing `styles.xml`, then append missing styles and batch-update cell `s` attributes. No cell values or formulas are modified.
|
||||
|
||||
**Scenario B — Applying Format Standards After CREATE/EDIT**
|
||||
After completing data entry or formula writing, formatting is applied as the final step. At this point, `styles.xml` may come from the minimal_xlsx template (which pre-defines 13 style slots) or from a user file. In either case, follow the principle of "append only, never modify existing xf entries."
|
||||
|
||||
**Not applicable**: Reading or analyzing file contents only (use the READ path); modifying formulas or data (use the EDIT path).
|
||||
|
||||
---
|
||||
|
||||
## 2. Financial Format Semantic System
|
||||
|
||||
### 2.1 Font Color = Cell Role (Color = Role)
|
||||
|
||||
The primary convention of financial modeling: **font color encodes the cell's role, not decoration**. A reviewer can glance at colors to determine which cells are adjustable parameters and which are model-calculated results. This is an industry-wide convention (followed by investment banks, the Big Four, and corporate finance teams).
|
||||
|
||||
| Role | Font Color | AARRGGBB | Use Case |
|
||||
|------|-----------|----------|----------|
|
||||
| Hard-coded input / assumption | Blue | `000000FF` | Growth rates, discount rates, tax rates, and other user-modifiable parameters |
|
||||
| Formula / calculated result | Black | `00000000` | All cells containing a `<f>` element |
|
||||
| Same-workbook cross-sheet reference | Green | `00008000` | Cells whose formula starts with `SheetName!` |
|
||||
| External file link | Red | `00FF0000` | Cells whose formula contains `[FileName.xlsx]` (flagged as fragile links) |
|
||||
| Label / text | Black (default) | theme color | Row labels, category headings |
|
||||
| Key assumption requiring review | Blue font + yellow fill | Font `000000FF` / Fill `00FFFF00` | Provisional values, parameters pending confirmation |
|
||||
|
||||
**Decision tree**:
|
||||
```
|
||||
Does the cell contain a <f> element?
|
||||
+-- Yes -> Does the formula start with [FileName]?
|
||||
| +-- Yes -> Red (external link)
|
||||
| +-- No -> Does the formula contain SheetName!?
|
||||
| +-- Yes -> Green (cross-sheet reference)
|
||||
| +-- No -> Black (same-sheet formula)
|
||||
+-- No -> Is the value a user-adjustable parameter?
|
||||
+-- Yes -> Blue (input/assumption)
|
||||
+-- No -> Black default (label)
|
||||
```
|
||||
|
||||
**Strictly prohibited**: Blue font + `<f>` element coexisting (color role contradiction — must be corrected).
|
||||
|
||||
### 2.2 Number Format Matrix
|
||||
|
||||
| Data Type | formatCode | numFmtId | Display Example | Applicable Scenario |
|
||||
|-----------|-----------|----------|-----------------|---------------------|
|
||||
| Standard currency (whole dollars) | `$#,##0;($#,##0);"-"` | 164 | $1,234 / ($1,234) / - | P&L, balance sheet amount rows |
|
||||
| Standard currency (with cents) | `$#,##0.00;($#,##0.00);"-"` | 169 | $1,234.56 / ($1,234.56) / - | Unit prices, detailed costs |
|
||||
| Thousands (K) | `#,##0,"K"` | 171 | 1,234K | Simplified display for management reports |
|
||||
| Millions (M) | `#,##0,,"M"` | 172 | 1M | Macro-level summary rows |
|
||||
| Percentage (1 decimal) | `0.0%` | 165 | 12.5% | Growth rates, gross margins |
|
||||
| Percentage (2 decimals) | `0.00%` | 170 | 12.50% | IRR, precise interest rates |
|
||||
| Multiple / valuation multiplier | `0.0x` | 166 | 8.5x | EV/EBITDA, P/E |
|
||||
| Integer (thousands separator) | `#,##0` | 167 | 12,345 | Employee count, unit quantities |
|
||||
| Year | `0` | 1 (built-in, no declaration needed) | 2024 | Column header years, prevents 2,024 |
|
||||
| Date | `m/d/yyyy` | 14 (built-in, no declaration needed) | 3/21/2026 | Timelines |
|
||||
| General text | General | 0 (built-in, no declaration needed) | — | Label rows, cells with no format requirement |
|
||||
|
||||
numFmtId 169–172 are custom formats that need to be appended beyond the 4 formats (164–167) pre-defined in the minimal_xlsx template. When appending, assign IDs according to the rules (see Section 3.4).
|
||||
|
||||
**Built-in format IDs do not need to be declared in `<numFmts>`** (IDs 0–163 are built into Excel/LibreOffice; simply reference the numFmtId in `<xf>`):
|
||||
|
||||
| numFmtId | formatCode | Description |
|
||||
|----------|-----------|-------------|
|
||||
| 0 | General | General format |
|
||||
| 1 | `0` | Integer, no thousands separator (use this ID for years) |
|
||||
| 3 | `#,##0` | Thousands-separated integer (no decimals) |
|
||||
| 9 | `0%` | Percentage integer |
|
||||
| 10 | `0.00%` | Percentage with two decimals |
|
||||
| 14 | `m/d/yyyy` | Short date |
|
||||
|
||||
### 2.3 Negative Number Display Standards
|
||||
|
||||
Financial reports have two mainstream conventions for negative numbers — choose one and **maintain consistency** throughout the entire workbook:
|
||||
|
||||
**Parenthetical style (investment banking standard, recommended for external deliverables)**
|
||||
|
||||
```
|
||||
Positive: $1,234 Negative: ($1,234) Zero: -
|
||||
formatCode: $#,##0;($#,##0);"-"
|
||||
```
|
||||
|
||||
**Red minus sign style (suitable for internal operational analysis reports)**
|
||||
|
||||
```
|
||||
Positive: $1,234 Negative: -$1,234 (red)
|
||||
formatCode: $#,##0;[Red]-$#,##0;"-"
|
||||
```
|
||||
|
||||
Rule: Once a style is determined, maintain it across the entire workbook. Do not mix two negative number display styles within the same workbook.
|
||||
|
||||
### 2.4 Zero Value Display Standards
|
||||
|
||||
In financial models, "0" and "no data" have different semantics and should be visually distinct:
|
||||
|
||||
| Scenario | Recommended Display | formatCode Third Segment |
|
||||
|----------|-------------------|--------------------------|
|
||||
| Sparse matrix (most rows have zero-value periods) | Dash `-` | `"-"` |
|
||||
| Quantity counts (zero itself is meaningful) | `0` | `0` or omit |
|
||||
| Placeholder row (explicitly empty) | Leave blank | Do not write to cell |
|
||||
|
||||
Four-segment format syntax: `positive format;negative format;zero value format;text format`
|
||||
|
||||
Zero as dash: `$#,##0;($#,##0);"-"`
|
||||
Zero preserved as 0: `#,##0;(#,##0);0`
|
||||
|
||||
---
|
||||
|
||||
## 3. styles.xml Surgical Operations
|
||||
|
||||
### 3.1 Auditing Existing Styles: Understanding the cellXfs Indirect Reference Chain
|
||||
|
||||
A cell's `s` attribute points to a position index (0-based) in `cellXfs`, and each `<xf>` entry in `cellXfs` references its respective definition libraries through `fontId`, `fillId`, `borderId`, and `numFmtId`.
|
||||
|
||||
Reference chain diagram:
|
||||
|
||||
```
|
||||
Cell <c s="6">
|
||||
| Look up cellXfs by 0-based index
|
||||
cellXfs[6] -> numFmtId="164" fontId="2" fillId="0" borderId="0"
|
||||
| | | |
|
||||
numFmts fonts[2] fills[0] borders[0]
|
||||
id=164 color=00000000 (no fill) (no border)
|
||||
$#,##0... black
|
||||
```
|
||||
|
||||
Audit steps:
|
||||
|
||||
**Step 1**: Read `<numFmts>` and record all declared custom formats and their IDs:
|
||||
```xml
|
||||
<numFmts count="4">
|
||||
<numFmt numFmtId="164" formatCode="$#,##0;($#,##0);"-""/>
|
||||
<numFmt numFmtId="165" formatCode="0.0%"/>
|
||||
<numFmt numFmtId="166" formatCode="0.0x"/>
|
||||
<numFmt numFmtId="167" formatCode="#,##0"/>
|
||||
</numFmts>
|
||||
```
|
||||
Record: current maximum custom numFmtId = 167, next available ID = 168.
|
||||
|
||||
**Step 2**: Read `<fonts>` and list each `<font>` by 0-based index with its color and style:
|
||||
```
|
||||
fontId=0 -> No explicit color (theme default black)
|
||||
fontId=1 -> color rgb="000000FF" (blue, input role)
|
||||
fontId=2 -> color rgb="00000000" (black, formula role)
|
||||
fontId=3 -> color rgb="00008000" (green, cross-sheet reference role)
|
||||
fontId=4 -> <b/> + color rgb="00000000" (bold black, header)
|
||||
```
|
||||
|
||||
**Step 3**: Read `<fills>` and confirm that fills[0] and fills[1] are spec-mandated reserved entries (never delete):
|
||||
```
|
||||
fillId=0 -> patternType="none" (spec-mandated)
|
||||
fillId=1 -> patternType="gray125" (spec-mandated)
|
||||
fillId=2 -> Yellow highlight (if present)
|
||||
```
|
||||
|
||||
**Step 4**: Read `<cellXfs>` and list each `<xf>` entry by 0-based index with its combination:
|
||||
```
|
||||
index 0 -> numFmtId=0, fontId=0, fillId=0 -> Default style
|
||||
index 1 -> numFmtId=0, fontId=1, fillId=0 -> Blue font general (input)
|
||||
index 5 -> numFmtId=164, fontId=1, fillId=0 -> Blue font currency (currency input)
|
||||
index 6 -> numFmtId=164, fontId=2, fillId=0 -> Black font currency (currency formula)
|
||||
...
|
||||
```
|
||||
|
||||
**Step 5**: Verify that all count attributes match the actual number of elements (count mismatches will cause Excel to refuse to open the file).
|
||||
|
||||
### 3.2 Safely Appending New Styles (Golden Rule: Append Only, Never Modify Existing xf)
|
||||
|
||||
**Never modify existing `<xf>` entries**. Modifications will affect all cells that already reference that index, breaking existing formatting. Only append new entries at the end.
|
||||
|
||||
Complete atomic operation sequence for appending new styles (all 5 steps must be executed):
|
||||
|
||||
**Step 1**: Determine if a new `<numFmt>` is needed
|
||||
|
||||
Built-in formats (ID 0–163) skip this step. Custom formats are appended to the end of `<numFmts>`:
|
||||
```xml
|
||||
<numFmts count="5"> <!-- count +1 -->
|
||||
<!-- Keep existing entries unchanged -->
|
||||
<numFmt numFmtId="164" formatCode="$#,##0;($#,##0);"-""/>
|
||||
<numFmt numFmtId="165" formatCode="0.0%"/>
|
||||
<numFmt numFmtId="166" formatCode="0.0x"/>
|
||||
<numFmt numFmtId="167" formatCode="#,##0"/>
|
||||
<!-- Newly appended -->
|
||||
<numFmt numFmtId="168" formatCode="$#,##0.00;($#,##0.00);"-""/>
|
||||
</numFmts>
|
||||
```
|
||||
|
||||
**Step 2**: Determine if a new `<font>` is needed
|
||||
|
||||
Check whether the existing fonts already contain a matching color+style combination. If not, append to the end of `<fonts>`:
|
||||
```xml
|
||||
<fonts count="6"> <!-- count +1 -->
|
||||
<!-- Keep existing entries unchanged -->
|
||||
...
|
||||
<!-- Newly appended: red font (external link role), new fontId = 5 -->
|
||||
<font>
|
||||
<sz val="11"/>
|
||||
<name val="Calibri"/>
|
||||
<color rgb="00FF0000"/>
|
||||
</font>
|
||||
</fonts>
|
||||
```
|
||||
New fontId = the count value before appending (when original count=5, new fontId=5).
|
||||
|
||||
**Step 3**: Determine if a new `<fill>` is needed
|
||||
|
||||
If a new background color is needed, append to the end of `<fills>` (note: fills[0] and fills[1] must never be modified):
|
||||
```xml
|
||||
<fills count="4"> <!-- count +1 -->
|
||||
<fill><patternFill patternType="none"/></fill> <!-- 0: spec-mandated -->
|
||||
<fill><patternFill patternType="gray125"/></fill> <!-- 1: spec-mandated -->
|
||||
<fill> <!-- 2: yellow highlight -->
|
||||
<patternFill patternType="solid">
|
||||
<fgColor rgb="00FFFF00"/>
|
||||
<bgColor indexed="64"/>
|
||||
</patternFill>
|
||||
</fill>
|
||||
<!-- Newly appended: light gray fill (projection period distinction), new fillId = 3 -->
|
||||
<fill>
|
||||
<patternFill patternType="solid">
|
||||
<fgColor rgb="00D3D3D3"/>
|
||||
<bgColor indexed="64"/>
|
||||
</patternFill>
|
||||
</fill>
|
||||
</fills>
|
||||
```
|
||||
|
||||
**Step 4**: Append a new `<xf>` combination at the end of `<cellXfs>`
|
||||
```xml
|
||||
<cellXfs count="14"> <!-- count +1 -->
|
||||
<!-- Keep existing entries 0-12 unchanged -->
|
||||
...
|
||||
<!-- Newly appended index=13: currency with cents formula (black font + numFmtId=168) -->
|
||||
<xf numFmtId="168" fontId="2" fillId="0" borderId="0" xfId="0"
|
||||
applyFont="1" applyNumberFormat="1"/>
|
||||
</cellXfs>
|
||||
```
|
||||
New style index = the count value before appending (when original count=13, new index=13).
|
||||
|
||||
**Step 5**: Record the new style index; subsequently set the `s` attribute of corresponding cells in the sheet XML to this value.
|
||||
|
||||
### 3.3 AARRGGBB Color Format Explanation
|
||||
|
||||
OOXML's `rgb` attribute uses **8-digit hexadecimal AARRGGBB** format (not HTML's 6-digit RRGGBB):
|
||||
|
||||
```
|
||||
AA RR GG BB
|
||||
| | | |
|
||||
Alpha Red Green Blue
|
||||
```
|
||||
|
||||
- Alpha channel: `00` = fully opaque (normal use value); `FF` = fully transparent (invisible, never use this)
|
||||
- Financial color standards always use `00` as the Alpha prefix
|
||||
|
||||
| Color | AARRGGBB | Corresponding Role |
|
||||
|-------|----------|-------------------|
|
||||
| Blue (input) | `000000FF` | Hard-coded assumptions |
|
||||
| Black (formula) | `00000000` | Calculated results |
|
||||
| Green (cross-sheet reference) | `00008000` | Same-workbook cross-sheet |
|
||||
| Red (external link) | `00FF0000` | References to other files |
|
||||
| Yellow (review-required fill) | `00FFFF00` | Key assumption highlight |
|
||||
| Light gray (projection period fill) | `00D3D3D3` | Distinguishing historical vs. forecast periods |
|
||||
| White | `00FFFFFF` | Pure white fill |
|
||||
|
||||
**Common mistake**: Mistakenly writing HTML format `#0000FF` as `FF0000FF` (Alpha=FF makes the color fully transparent and invisible). Correct format: `000000FF`.
|
||||
|
||||
### 3.4 numFmtId Assignment Rules
|
||||
|
||||
```
|
||||
ID 0-163 -> Excel/LibreOffice built-in formats, no declaration needed in <numFmts>, reference directly in <xf>
|
||||
ID 164+ -> Custom formats, must be explicitly declared as <numFmt> elements in <numFmts>
|
||||
```
|
||||
|
||||
Rules for assigning new IDs:
|
||||
1. Read all `numFmtId` attribute values in the current `<numFmts>`
|
||||
2. Take the maximum value + 1 as the next custom format ID
|
||||
3. Do not reuse existing IDs; do not skip numbers
|
||||
|
||||
The minimal_xlsx template pre-defines IDs: 164, 165, 166, 167. The next available ID is 168.
|
||||
|
||||
---
|
||||
|
||||
## 4. Pre-defined Style Index Complete Reference Table (13 Slots)
|
||||
|
||||
The following are the 13 style slots (cellXfs index 0–12) pre-defined in the minimal_xlsx template's `styles.xml`, which can be directly referenced in the cell `s` attribute in sheet XML:
|
||||
|
||||
| Index | Semantic Role | Font Color | Fill | numFmtId | Format Display | Typical Use |
|
||||
|-------|--------------|------------|------|----------|---------------|-------------|
|
||||
| **0** | Default style | Theme black | None | 0 | General | Cells requiring no special formatting |
|
||||
| **1** | Input / assumption (general) | Blue `000000FF` | None | 0 | General | Text-type assumptions, flags |
|
||||
| **2** | Formula / calculated result (general) | Black `00000000` | None | 0 | General | Text concatenation formulas, non-numeric calculations |
|
||||
| **3** | Cross-sheet reference (general) | Green `00008000` | None | 0 | General | Values pulled from cross-sheet (general format) |
|
||||
| **4** | Header (bold) | Bold black | None | 0 | General | Row/column headings |
|
||||
| **5** | Currency input | Blue `000000FF` | None | 164 | $1,234 / ($1,234) / - | Amount inputs in the assumptions area |
|
||||
| **6** | Currency formula | Black `00000000` | None | 164 | $1,234 / ($1,234) / - | Amount calculations in the model area (revenue, EBITDA) |
|
||||
| **7** | Percentage input | Blue `000000FF` | None | 165 | 12.5% | Rate inputs in the assumptions area (growth rate, gross margin assumptions) |
|
||||
| **8** | Percentage formula | Black `00000000` | None | 165 | 12.5% | Rate calculations in the model area (actual gross margin) |
|
||||
| **9** | Integer (comma) input | Blue `000000FF` | None | 167 | 12,345 | Quantity inputs in the assumptions area (employee count) |
|
||||
| **10** | Integer (comma) formula | Black `00000000` | None | 167 | 12,345 | Quantity calculations in the model area |
|
||||
| **11** | Year input | Blue `000000FF` | None | 1 | 2024 | Column header years (no thousands separator) |
|
||||
| **12** | Key assumption highlight | Blue `000000FF` | Yellow `00FFFF00` | 0 | General | Key parameters pending review or confirmation |
|
||||
|
||||
**Selection guide**:
|
||||
- Determine "input" vs. "formula" -> Choose odd-numbered (input/blue) or even-numbered (formula/black) paired slots
|
||||
- Determine data type -> Choose the corresponding currency (5/6) / percentage (7/8) / integer (9/10) / year (11) slot
|
||||
- Cross-sheet reference needing number format -> Append a new green + number format combination (see Section 5.4)
|
||||
- Parameter pending review -> index 12
|
||||
|
||||
---
|
||||
|
||||
## 5. Assumption Separation Principle: XML-Level Implementation
|
||||
|
||||
### 5.1 Structural Design
|
||||
|
||||
Assumption separation principle: **Input assumptions are centralized in a dedicated area (sheet or block); the model calculation area contains only formulas, no hard-coded values**.
|
||||
|
||||
Recommended structure:
|
||||
```
|
||||
Workbook sheet layout
|
||||
sheet 1 "Assumptions" -> All blue-font cells (style 1/5/7/9/11/12)
|
||||
sheet 2 "Model" -> All black or green-font cells (style 2/3/4/6/8/10)
|
||||
```
|
||||
|
||||
Same-sheet zoning approach for simple models:
|
||||
```
|
||||
Rows 1-5: [Assumptions block - blue font]
|
||||
Row 6: [Empty row separator]
|
||||
Rows 7+: [Model block - black/green font formulas referencing assumptions area]
|
||||
```
|
||||
|
||||
### 5.2 Assumptions Area XML Example
|
||||
|
||||
```xml
|
||||
<!-- Assumptions sheet (sheet1.xml) example -->
|
||||
|
||||
<!-- Row 1: Block title -->
|
||||
<row r="1">
|
||||
<c r="A1" s="4" t="inlineStr"><is><t>Model Assumptions</t></is></c>
|
||||
</row>
|
||||
|
||||
<!-- Row 2: Growth rate assumption - blue font percentage input, s="7" -->
|
||||
<row r="2">
|
||||
<c r="A2" t="inlineStr"><is><t>Revenue Growth Rate</t></is></c>
|
||||
<c r="B2" s="7"><v>0.08</v></c>
|
||||
</row>
|
||||
|
||||
<!-- Row 3: Gross margin assumption - blue font percentage input, s="7" -->
|
||||
<row r="3">
|
||||
<c r="A3" t="inlineStr"><is><t>Gross Margin</t></is></c>
|
||||
<c r="B3" s="7"><v>0.65</v></c>
|
||||
</row>
|
||||
|
||||
<!-- Row 4: Base revenue - blue font currency input, s="5" -->
|
||||
<row r="4">
|
||||
<c r="A4" t="inlineStr"><is><t>Base Revenue (Year 0)</t></is></c>
|
||||
<c r="B4" s="5"><v>1000000</v></c>
|
||||
</row>
|
||||
|
||||
<!-- Row 5: Key assumption (pending review) - blue font yellow fill, s="12" -->
|
||||
<row r="5">
|
||||
<c r="A5" t="inlineStr"><is><t>Terminal Growth Rate</t></is></c>
|
||||
<c r="B5" s="12"><v>0.03</v></c>
|
||||
</row>
|
||||
```
|
||||
|
||||
### 5.3 Model Area XML Example (Referencing Assumptions Area)
|
||||
|
||||
```xml
|
||||
<!-- Model sheet (sheet2.xml) example -->
|
||||
|
||||
<!-- Row 1: Column headers (years) - bold header, s="4"; year cells, s="11" -->
|
||||
<row r="1">
|
||||
<c r="A1" s="4" t="inlineStr"><is><t>Metric</t></is></c>
|
||||
<c r="B1" s="11"><v>2024</v></c>
|
||||
<c r="C1" s="11"><v>2025</v></c>
|
||||
<c r="D1" s="11"><v>2026</v></c>
|
||||
</row>
|
||||
|
||||
<!-- Row 2: Revenue row -->
|
||||
<row r="2">
|
||||
<c r="A2" t="inlineStr"><is><t>Revenue</t></is></c>
|
||||
<!-- B2: Base year revenue, cross-sheet reference from Assumptions, green, s="3" (general format) -->
|
||||
<!-- If currency format is needed, append new style s="13" (see Section 5.4) -->
|
||||
<c r="B2" s="3"><f>Assumptions!B4</f><v></v></c>
|
||||
<!-- C2, D2: Next year revenue = prior year * (1 + growth rate), black font currency formula, s="6" -->
|
||||
<c r="C2" s="6"><f>B2*(1+Assumptions!B2)</f><v></v></c>
|
||||
<c r="D2" s="6"><f>C2*(1+Assumptions!B2)</f><v></v></c>
|
||||
</row>
|
||||
|
||||
<!-- Row 3: Gross profit row - black font currency formula, s="6" -->
|
||||
<row r="3">
|
||||
<c r="A3" t="inlineStr"><is><t>Gross Profit</t></is></c>
|
||||
<c r="B3" s="6"><f>B2*Assumptions!B3</f><v></v></c>
|
||||
<c r="C3" s="6"><f>C2*Assumptions!B3</f><v></v></c>
|
||||
<c r="D3" s="6"><f>D2*Assumptions!B3</f><v></v></c>
|
||||
</row>
|
||||
|
||||
<!-- Row 4: Gross margin row - black font percentage formula, s="8" -->
|
||||
<row r="4">
|
||||
<c r="A4" t="inlineStr"><is><t>Gross Margin %</t></is></c>
|
||||
<c r="B4" s="8"><f>B3/B2</f><v></v></c>
|
||||
<c r="C4" s="8"><f>C3/C2</f><v></v></c>
|
||||
<c r="D4" s="8"><f>D3/D2</f><v></v></c>
|
||||
</row>
|
||||
```
|
||||
|
||||
### 5.4 Appending "Green + Number Format" Combinations
|
||||
|
||||
Pre-defined index 3 is green font + general format. If a cross-sheet reference involves a currency amount, a green style with a number format must be appended:
|
||||
|
||||
```xml
|
||||
<!-- Append at the end of <cellXfs> in styles.xml (assuming current count=13, new index=13) -->
|
||||
<!-- index 13: cross-sheet reference + currency format (green font + $#,##0) -->
|
||||
<xf numFmtId="164" fontId="3" fillId="0" borderId="0" xfId="0"
|
||||
applyFont="1" applyNumberFormat="1"/>
|
||||
<!-- Update count to 14 -->
|
||||
```
|
||||
|
||||
After appending, cross-sheet reference currency cells use `s="13"`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Complete Operational Workflow
|
||||
|
||||
### 6.1 Workflow Overview
|
||||
|
||||
```
|
||||
[Existing xlsx or file after CREATE/EDIT]
|
||||
|
|
||||
Step 1: Unpack (extract to temporary directory)
|
||||
|
|
||||
Step 2: Audit styles.xml (review existing styles, build index mapping table)
|
||||
|
|
||||
Step 3: Audit sheet XML (identify cells needing formatting and their semantic roles)
|
||||
|
|
||||
Step 4: Append missing styles (numFmt -> font -> fill -> xf, update counts)
|
||||
|
|
||||
Step 5: Batch-update the s attribute of each cell in the sheet XML
|
||||
|
|
||||
Step 6: XML validity + style reference integrity verification
|
||||
|
|
||||
Step 7: Pack (recompress as xlsx)
|
||||
```
|
||||
|
||||
### 6.2 Step 1 — Unpack
|
||||
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/xlsx_unpack.py input.xlsx /tmp/xlsx_fmt/
|
||||
```
|
||||
|
||||
If the script is unavailable, unpack manually:
|
||||
```bash
|
||||
mkdir -p /tmp/xlsx_fmt && cp input.xlsx /tmp/xlsx_fmt/input.xlsx
|
||||
cd /tmp/xlsx_fmt && unzip input.xlsx -d unpacked/
|
||||
```
|
||||
|
||||
### 6.3 Step 2 — Audit styles.xml
|
||||
|
||||
Execute according to the method in Section 3.1. Quick check for minimal_xlsx template initial state:
|
||||
- `<cellXfs count="13">` and `<numFmts count="4">` -> Template initial state, all 13 pre-defined slots can be used directly
|
||||
- Otherwise -> A complete review of the existing index mapping is required
|
||||
|
||||
### 6.4 Step 3 — Audit Sheet XML, Build Formatting Plan
|
||||
|
||||
Read `xl/worksheets/sheet*.xml` and evaluate each cell:
|
||||
1. Does it contain a `<f>` element (formula)? -> Requires black/green/red style
|
||||
2. Is it a hard-coded numeric parameter? -> Requires blue style
|
||||
3. Is the data type currency/percentage/integer/year? -> Select the corresponding number format slot
|
||||
4. Is it a header? -> Bold style (index 4)
|
||||
|
||||
Build a formatting mapping table: `{cell coordinate: target style index}`
|
||||
|
||||
### 6.5 Step 4 — Append Styles
|
||||
|
||||
Execute according to the atomic operation sequence in Section 3.2. Update the corresponding count attribute immediately after appending each component.
|
||||
|
||||
### 6.6 Step 5 — Batch-Update Cell s Attributes
|
||||
|
||||
```xml
|
||||
<!-- Before formatting: no style -->
|
||||
<c r="B5"><v>0.08</v></c>
|
||||
|
||||
<!-- After formatting: growth rate assumption, blue font percentage, s="7" -->
|
||||
<c r="B5" s="7"><v>0.08</v></c>
|
||||
```
|
||||
|
||||
```xml
|
||||
<!-- Before formatting: formula without style -->
|
||||
<c r="C10"><f>B10*(1+Assumptions!B2)</f><v></v></c>
|
||||
|
||||
<!-- After formatting: currency formula, black font, s="6" -->
|
||||
<c r="C10" s="6"><f>B10*(1+Assumptions!B2)</f><v></v></c>
|
||||
```
|
||||
|
||||
For consecutive rows of the same type, row-level default styles can be used to reduce repetition:
|
||||
```xml
|
||||
<!-- Entire row uses style=6, only override for exception cells -->
|
||||
<row r="5" s="6" customFormat="1">
|
||||
<c r="A5" s="0" t="inlineStr"><is><t>Operating Income</t></is></c> <!-- Text overridden to default -->
|
||||
<c r="B5"><f>B3-B4</f><v></v></c> <!-- Inherits row-level s=6 -->
|
||||
<c r="C5"><f>C3-C4</f><v></v></c>
|
||||
</row>
|
||||
```
|
||||
|
||||
### 6.7 Step 6 — Verification
|
||||
|
||||
```bash
|
||||
# XML validity verification is handled automatically by xlsx_pack.py, no need to manually run xmllint
|
||||
# The pack script validates styles.xml and sheet XML legality before packaging; it aborts and reports on errors
|
||||
|
||||
# Style audit (optional, audit the entire unpacked directory after formatting is complete)
|
||||
python3 SKILL_DIR/scripts/style_audit.py /tmp/xlsx_fmt/unpacked/
|
||||
|
||||
# Formula error static scan (must specify a single .xlsx file, does not accept directories)
|
||||
# Pack first, then scan:
|
||||
python3 SKILL_DIR/scripts/xlsx_pack.py /tmp/xlsx_fmt/unpacked/ /tmp/output.xlsx
|
||||
python3 SKILL_DIR/scripts/formula_check.py /tmp/output.xlsx
|
||||
```
|
||||
|
||||
Manual style reference integrity check:
|
||||
```bash
|
||||
# Find the maximum s attribute value in the sheet XML
|
||||
grep -o 's="[0-9]*"' /tmp/xlsx_fmt/unpacked/xl/worksheets/sheet1.xml \
|
||||
| grep -o '[0-9]*' | sort -n | tail -1
|
||||
|
||||
# Compare with the cellXfs count attribute (max s value must be < count)
|
||||
grep 'cellXfs count' /tmp/xlsx_fmt/unpacked/xl/styles.xml
|
||||
```
|
||||
|
||||
### 6.8 Step 7 — Pack
|
||||
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/xlsx_pack.py /tmp/xlsx_fmt/unpacked/ output.xlsx
|
||||
```
|
||||
|
||||
If the script is unavailable, pack manually:
|
||||
```bash
|
||||
cd /tmp/xlsx_fmt/unpacked/
|
||||
zip -r ../output.xlsx . -x "*.DS_Store"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Formatting Completeness Checklist
|
||||
|
||||
Verify each item before delivery:
|
||||
|
||||
### Color Role Consistency
|
||||
- [ ] All numeric cells containing `<f>` elements: fontId corresponds to black (formula) or green (cross-sheet reference)
|
||||
- [ ] All hard-coded numeric values that are user-adjustable parameters: fontId corresponds to blue (input)
|
||||
- [ ] Cross-sheet references (formula contains `SheetName!`): fontId corresponds to green
|
||||
- [ ] External file references (formula contains `[FileName.xlsx]`): fontId corresponds to red
|
||||
- [ ] No cell simultaneously contains a `<f>` element and uses blue font (color role contradiction)
|
||||
|
||||
### Number Format Correctness
|
||||
- [ ] Year columns: numFmtId="1" (`0` format), displays as 2024 not 2,024
|
||||
- [ ] Currency rows: numFmtId="164" or variant, negative numbers display as ($1,234) not -$1,234
|
||||
- [ ] Percentage rows: values stored as decimals (0.08 = 8%), format numFmtId="165", displays as 8.0%
|
||||
- [ ] Zero values: displayed as `-` in sparse matrices rather than `0` (formatCode third segment contains `"-"`)
|
||||
- [ ] Multiple rows (EV/EBITDA, etc.): numFmtId="166" (`0.0x` format)
|
||||
- [ ] Negative number display style is consistent throughout the entire workbook (parenthetical or red minus sign)
|
||||
|
||||
### styles.xml Structural Integrity
|
||||
- [ ] `<numFmts count>` = actual number of `<numFmt>` elements
|
||||
- [ ] `<fonts count>` = actual number of `<font>` elements
|
||||
- [ ] `<fills count>` = actual number of `<fill>` elements (including spec-mandated fills[0] and fills[1])
|
||||
- [ ] `<cellXfs count>` = actual number of `<xf>` elements
|
||||
- [ ] fills[0] is `patternType="none"`, fills[1] is `patternType="gray125"` (spec-mandated)
|
||||
- [ ] All `<xf>` referenced fontId / fillId / borderId are within the valid range of their respective collections
|
||||
- [ ] All cell `s` attribute values < `cellXfs count` (no out-of-bounds references)
|
||||
|
||||
### Assumption Separation Verification
|
||||
- [ ] No black-font numeric cells in the assumptions area/sheet (black numeric = formula, should not be in assumptions)
|
||||
- [ ] No blue-font non-year numeric cells in the model area/sheet (blue numeric = hard-coded, should be in assumptions)
|
||||
- [ ] Input parameters in the model area reference the assumptions area via formulas, not by directly copying values
|
||||
|
||||
### Formula and Format Linkage
|
||||
- [ ] All cells with `<f>` elements have an explicit `s` attribute (must not use default style=0, whose font color is not explicitly black)
|
||||
- [ ] SUM summary rows: style uses black font + corresponding number format (e.g., s="6" for currency summaries)
|
||||
- [ ] Percentage formulas: values stored as decimals, format is `0.0%`; do not multiply values by 100 before applying percentage format
|
||||
|
||||
### Visual Hierarchy
|
||||
- [ ] Header rows (years/metric names): style=4 (bold black)
|
||||
- [ ] Summary rows (Total/EBITDA/Net Income): bold + corresponding number format (append style if needed)
|
||||
- [ ] Unit description rows (e.g., "$ thousands"): use style=0 or style=2 (blue not needed)
|
||||
|
||||
---
|
||||
|
||||
## 8. Prohibited Actions (What You Must NOT Do)
|
||||
|
||||
- **Do not modify existing `<xf>` entries**: This will batch-change the style of all cells referencing that index
|
||||
- **Do not delete fills[0] and fills[1]**: Required by OOXML specification; deletion causes file corruption
|
||||
- **Do not modify cell values or formulas**: The FORMAT path only changes styles, not content
|
||||
- **Do not use openpyxl for formatting**: openpyxl rewrites the entire styles.xml on save, losing unsupported features
|
||||
- **Do not apply global override styles**: Do not cover the entire workbook with a single style; assign precisely by semantic role
|
||||
- **Do not write FF in the Alpha channel**: `rgb="FF0000FF"` makes the color fully transparent; the correct format is `rgb="000000FF"`
|
||||
|
||||
---
|
||||
|
||||
## 9. Common Errors and Fixes
|
||||
|
||||
### Error 1: Year displays as 2,024
|
||||
|
||||
Cause: The year cell's `s` attribute uses a format with thousands separator (e.g., numFmtId="3" or numFmtId="167").
|
||||
|
||||
```xml
|
||||
<!-- Incorrect -->
|
||||
<c r="B1" s="9"><v>2024</v></c>
|
||||
|
||||
<!-- Fix: Change to s="11" (numFmtId="1", format 0) -->
|
||||
<c r="B1" s="11"><v>2024</v></c>
|
||||
```
|
||||
|
||||
### Error 2: Percentage displays as 800% (value was multiplied by 100)
|
||||
|
||||
Cause: 8% was stored as `<v>8</v>` instead of `<v>0.08</v>`. Excel's `%` format automatically multiplies the value by 100 for display.
|
||||
|
||||
```xml
|
||||
<!-- Incorrect -->
|
||||
<c r="B2" s="7"><v>8</v></c>
|
||||
|
||||
<!-- Fix: Value must be stored in decimal form -->
|
||||
<c r="B2" s="7"><v>0.08</v></c>
|
||||
```
|
||||
|
||||
### Error 3: File corruption after appending styles without updating count
|
||||
|
||||
Cause: A `<font>` or `<xf>` element was appended but the count attribute was not updated; Excel reads beyond bounds using the old count.
|
||||
|
||||
Fix: Update the corresponding count immediately after appending each element:
|
||||
```xml
|
||||
<!-- After appending the 6th font, count must be changed from 5 to 6 -->
|
||||
<fonts count="6">
|
||||
...
|
||||
</fonts>
|
||||
```
|
||||
|
||||
### Error 4: Blue font + formula (color role contradiction)
|
||||
|
||||
Cause: A formula cell mistakenly uses an input style (e.g., s="5" for currency input).
|
||||
|
||||
```xml
|
||||
<!-- Incorrect: Formula cell uses blue input style -->
|
||||
<c r="C5" s="5"><f>B5*1.08</f><v></v></c>
|
||||
|
||||
<!-- Fix: Change formula cell to corresponding black formula style (5->6, 7->8, 9->10) -->
|
||||
<c r="C5" s="6"><f>B5*1.08</f><v></v></c>
|
||||
```
|
||||
|
||||
### Error 5: AARRGGBB color missing Alpha (only 6 digits)
|
||||
|
||||
```xml
|
||||
<!-- Incorrect: 6-digit format, behavior depends on implementation, usually causes wrong color -->
|
||||
<color rgb="0000FF"/>
|
||||
|
||||
<!-- Fix: Always use 8-digit AARRGGBB, Alpha fixed at 00 -->
|
||||
<color rgb="000000FF"/>
|
||||
```
|
||||
|
||||
### Error 6: Modifying existing xf (affects all cells referencing that index)
|
||||
|
||||
Cause: Directly modifying attributes of the Nth `<xf>` in cellXfs, causing all cells with `s="N"` to be batch-changed.
|
||||
|
||||
Fix: Keep existing entries unchanged, append a new entry at the end, and only change the `s` attribute of cells that need the new style to the new index:
|
||||
```xml
|
||||
<!-- Incorrect: Modified the existing xf at index=6 -->
|
||||
<xf numFmtId="164" fontId="2" fillId="0" borderId="0" xfId="0"
|
||||
applyFont="1" applyNumberFormat="1" applyAlignment="1">
|
||||
<alignment horizontal="right"/> <!-- New attribute added, affects ALL cells already using s="6" -->
|
||||
</xf>
|
||||
|
||||
<!-- Fix: Append new index (when original count=13, new index=13), only change the s attribute of cells needing right alignment -->
|
||||
<!-- Keep index=6 as-is -->
|
||||
<xf numFmtId="164" fontId="2" fillId="0" borderId="0" xfId="0"
|
||||
applyFont="1" applyNumberFormat="1" applyAlignment="1">
|
||||
<alignment horizontal="right"/>
|
||||
</xf> <!-- New index=13 -->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Financial Model Structure Conventions
|
||||
|
||||
### 10.1 Header Rows
|
||||
|
||||
- Bold font (corresponds to style index 4 in this skill's template)
|
||||
- Year columns: use number format `0` (numFmtId="1", no thousands separator) to prevent 2024 from displaying as 2,024
|
||||
- A unit description row may be added below headers: gray or italic text, e.g., "$ thousands" or "% of Revenue"
|
||||
|
||||
### 10.2 Row Type Standards
|
||||
|
||||
| Row Type | Style Recommendation | Example |
|
||||
|----------|---------------------|---------|
|
||||
| Category heading row | Bold, optionally with fill color | "Revenue" |
|
||||
| Line item row | Normal style | "Product A", "Product B" |
|
||||
| Subtotal row | Bold + top border | "Total Revenue" |
|
||||
| Operating metric row | Normal style | "Gross Margin %" |
|
||||
| Separator row | Empty row | (empty) |
|
||||
|
||||
### 10.3 Multi-Year Model Column Layout
|
||||
|
||||
```
|
||||
Col A: Label column (width 28, left-aligned text, s="4" for headers or s="0" for labels)
|
||||
Col B: FY2022 Actual (width 12, year header s="11", data cells styled by semantic role)
|
||||
Col C: FY2023 Actual
|
||||
Col D: FY2024E (forecast period - can use light gray fill fillId=3 to differentiate)
|
||||
Col E: FY2025E
|
||||
Col F: FY2026E
|
||||
```
|
||||
|
||||
### 10.4 Cross-Sheet Reference Patterns
|
||||
|
||||
Complete XML example of parameters passing from assumptions sheet to model sheet:
|
||||
|
||||
```xml
|
||||
<!-- Assumptions sheet, cell B5: 8% growth rate, blue percentage input -->
|
||||
<c r="B5" s="7"><v>0.08</v></c>
|
||||
|
||||
<!-- Model sheet, cell C10: references assumption area growth rate, green percentage formula -->
|
||||
<!-- Requires appending index=13: green + percentage format (fontId=3, numFmtId=165) -->
|
||||
<c r="C10" s="13"><f>Assumptions!B5</f><v></v></c>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Assumption Categories
|
||||
|
||||
In the assumptions area (Assumptions sheet or assumptions block), organize assumptions in the following standard order for ease of review and maintenance:
|
||||
|
||||
1. **Revenue assumptions**: Growth rates, pricing, sales volume
|
||||
2. **Cost assumptions**: Gross margin, fixed/variable cost ratios
|
||||
3. **Working capital**: DSO (Days Sales Outstanding), DPO (Days Payable Outstanding), inventory days
|
||||
4. **Capital expenditures (CapEx)**: As a percentage of revenue or absolute amounts
|
||||
5. **Financing assumptions**: Interest rates, debt repayment schedules
|
||||
6. **Tax and other**: Effective tax rate, depreciation & amortization (D&A)
|
||||
|
||||
---
|
||||
|
||||
## 12. Audit Trail Best Practices
|
||||
|
||||
- Use `s="12"` (blue font + yellow fill highlight) to mark cells requiring review or pending changes, making them immediately visible to reviewers
|
||||
- In sensitivity analysis rows or a separate Sensitivity tab, show the impact of +/-1% changes in key assumptions on results
|
||||
- **Do not hide rows containing assumptions**: Assumption rows must be visible to reviewers; do not use the `hidden="1"` attribute
|
||||
- Note a "Last Updated" date at the top of the assumptions area or in a dedicated cell, recording the last modification time of the model
|
||||
|
||||
---
|
||||
|
||||
## 13. Pre-Delivery Checklist (Common Financial Model Checklist)
|
||||
|
||||
Before outputting the final file, confirm each item:
|
||||
|
||||
- [ ] Formula rows contain no hard-coded values (can use `formula_check.py` to scan the packaged `.xlsx` file)
|
||||
- [ ] Year columns display as 2024 not 2,024 (numFmtId="1", format `0`)
|
||||
- [ ] Negative numbers display as (1,234) not -1,234 (use parenthetical style for externally delivered financial reports)
|
||||
- [ ] Zero values display as `-` in sparse rows rather than `0` (formatCode third segment is `"-"`)
|
||||
- [ ] Growth rates and percentages are stored as decimals (0.08 = 8%), format is `0.0%`
|
||||
- [ ] All cross-sheet reference cells use green font (style index 3 or an appended green + number format combination)
|
||||
- [ ] Assumptions block and model block are clearly separated (different sheets or separated by empty rows within the same sheet)
|
||||
- [ ] Summary rows use `SUM()` formulas, not manually hard-coded totals
|
||||
- [ ] Balance verification: summary rows = sum of their respective line items (a check row can be added at the end of the model to verify)
|
||||
@@ -0,0 +1,231 @@
|
||||
# OOXML SpreadsheetML Cheat Sheet
|
||||
|
||||
Quick reference for XML manipulation of xlsx files.
|
||||
|
||||
---
|
||||
|
||||
## Package Structure
|
||||
|
||||
```
|
||||
my_file.xlsx (ZIP archive)
|
||||
├── [Content_Types].xml ← declares MIME types for all files
|
||||
├── _rels/
|
||||
│ └── .rels ← root relationship: points to xl/workbook.xml
|
||||
└── xl/
|
||||
├── workbook.xml ← sheet list, calc settings
|
||||
├── styles.xml ← ALL style definitions
|
||||
├── sharedStrings.xml ← ALL text strings (referenced by index)
|
||||
├── _rels/
|
||||
│ └── workbook.xml.rels ← maps r:id → worksheet/styles/sharedStrings files
|
||||
├── worksheets/
|
||||
│ ├── sheet1.xml ← Sheet 1 data
|
||||
│ ├── sheet2.xml ← Sheet 2 data
|
||||
│ └── ...
|
||||
├── charts/ ← chart XML (if any)
|
||||
├── pivotTables/ ← pivot table XML (if any)
|
||||
└── theme/
|
||||
└── theme1.xml ← color/font theme
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cell Reference Format
|
||||
|
||||
```
|
||||
A1 → column A (1), row 1
|
||||
B5 → column B (2), row 5
|
||||
AA1 → column 27, row 1
|
||||
```
|
||||
|
||||
Column letter ↔ number conversion:
|
||||
```python
|
||||
def col_letter(n): # 1-based → letter
|
||||
r = ""
|
||||
while n > 0:
|
||||
n, rem = divmod(n - 1, 26)
|
||||
r = chr(65 + rem) + r
|
||||
return r
|
||||
|
||||
def col_number(s): # letter → 1-based
|
||||
n = 0
|
||||
for c in s.upper():
|
||||
n = n * 26 + (ord(c) - 64)
|
||||
return n
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cell XML Reference
|
||||
|
||||
### Data Types
|
||||
|
||||
| Type | `t` attr | XML Example | Value |
|
||||
|------|---------|-------------|-------|
|
||||
| Number | omit | `<c r="B2"><v>1000</v></c>` | 1000 |
|
||||
| String (shared) | `s` | `<c r="A1" t="s"><v>0</v></c>` | sharedStrings[0] |
|
||||
| String (inline) | `inlineStr` | `<c r="A1" t="inlineStr"><is><t>Hi</t></is></c>` | "Hi" |
|
||||
| Boolean | `b` | `<c r="D1" t="b"><v>1</v></c>` | TRUE |
|
||||
| Error | `e` | `<c r="E1" t="e"><v>#REF!</v></c>` | #REF! |
|
||||
| Formula | omit | `<c r="B4"><f>SUM(B2:B3)</f><v></v></c>` | computed |
|
||||
|
||||
### Formula Types
|
||||
|
||||
```xml
|
||||
<!-- Basic formula (no leading = in XML!) -->
|
||||
<c r="B4"><f>SUM(B2:B3)</f><v></v></c>
|
||||
|
||||
<!-- Cross-sheet -->
|
||||
<c r="C1"><f>Assumptions!B5</f><v></v></c>
|
||||
<c r="C1"><f>'Sheet With Spaces'!B5</f><v></v></c>
|
||||
|
||||
<!-- Shared formula: D2:D100 all use B*C with relative row offset -->
|
||||
<c r="D2"><f t="shared" ref="D2:D100" si="0">B2*C2</f><v></v></c>
|
||||
<c r="D3"><f t="shared" si="0"/><v></v></c>
|
||||
|
||||
<!-- Array formula -->
|
||||
<c r="E1"><f t="array" ref="E1:E5">SORT(A1:A5)</f><v></v></c>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## styles.xml Reference
|
||||
|
||||
### Indirect Reference Chain
|
||||
|
||||
```
|
||||
Cell s="3"
|
||||
↓
|
||||
cellXfs[3] → fontId="2", fillId="0", borderId="0", numFmtId="165"
|
||||
↓ ↓ ↓ ↓ ↓
|
||||
fonts[2] fills[0] borders[0] numFmts: id=165
|
||||
blue color no fill no border "0.0%"
|
||||
```
|
||||
|
||||
### Adding a New Style (step-by-step)
|
||||
|
||||
1. In `<numFmts>`: add `<numFmt numFmtId="168" formatCode="0.00%"/>`, update `count`
|
||||
2. In `<fonts>`: add font entry, note its index
|
||||
3. In `<cellXfs>`: append `<xf numFmtId="168" fontId="N" .../>`, update `count`
|
||||
4. New style index = old `cellXfs count` value (before incrementing)
|
||||
5. Apply to cells: `<c r="B5" s="NEW_INDEX">...</c>`
|
||||
|
||||
### Color Format
|
||||
|
||||
`AARRGGBB` — Alpha (always `00` for opaque) + Red + Green + Blue
|
||||
|
||||
```
|
||||
000000FF → Blue
|
||||
00000000 → Black
|
||||
00008000 → Green (dark)
|
||||
00FF0000 → Red
|
||||
00FFFF00 → Yellow (for fills)
|
||||
00FFFFFF → White
|
||||
```
|
||||
|
||||
### Built-in numFmtIds (no declaration needed)
|
||||
|
||||
| ID | Format | Display |
|
||||
|----|--------|---------|
|
||||
| 0 | General | as-is |
|
||||
| 1 | 0 | 2024 (use for years!) |
|
||||
| 2 | 0.00 | 1000.00 |
|
||||
| 3 | #,##0 | 1,000 |
|
||||
| 4 | #,##0.00 | 1,000.00 |
|
||||
| 9 | 0% | 15% |
|
||||
| 10 | 0.00% | 15.25% |
|
||||
| 14 | m/d/yyyy | 3/21/2026 |
|
||||
|
||||
---
|
||||
|
||||
## sharedStrings.xml Reference
|
||||
|
||||
```xml
|
||||
<sst count="3" uniqueCount="3">
|
||||
<si><t>Revenue</t></si> <!-- index 0 -->
|
||||
<si><t>Cost</t></si> <!-- index 1 -->
|
||||
<si><t>Margin</t></si> <!-- index 2 -->
|
||||
</sst>
|
||||
```
|
||||
|
||||
Text with leading/trailing spaces:
|
||||
```xml
|
||||
<si><t xml:space="preserve"> indented </t></si>
|
||||
```
|
||||
|
||||
Special characters:
|
||||
```xml
|
||||
<si><t>R&D Expenses</t></si> <!-- & must be & -->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## workbook.xml / .rels Sync
|
||||
|
||||
Every `<sheet>` in workbook.xml needs a matching `<Relationship>` in workbook.xml.rels:
|
||||
|
||||
```xml
|
||||
<!-- workbook.xml -->
|
||||
<!-- NOTE: rId numbering depends on what rIds are already in workbook.xml.rels.
|
||||
The minimal template reserves rId1=sheet1, rId2=styles, rId3=sharedStrings.
|
||||
When ADDING sheets to the template, start from rId4 to avoid conflicts.
|
||||
The rId3 here is just a generic illustration — use the next available rId. -->
|
||||
<sheet name="Summary" sheetId="3" r:id="rId3"/>
|
||||
|
||||
<!-- workbook.xml.rels -->
|
||||
<Relationship Id="rId3"
|
||||
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
|
||||
Target="worksheets/sheet3.xml"/>
|
||||
```
|
||||
|
||||
And a matching `<Override>` in `[Content_Types].xml`:
|
||||
```xml
|
||||
<Override PartName="/xl/worksheets/sheet3.xml"
|
||||
ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Column / Row Dimensions
|
||||
|
||||
```xml
|
||||
<!-- Before <sheetData> -->
|
||||
<cols>
|
||||
<col min="1" max="1" width="28" customWidth="1"/> <!-- A: 28 chars -->
|
||||
<col min="2" max="6" width="14" customWidth="1"/> <!-- B-F: 14 chars -->
|
||||
</cols>
|
||||
|
||||
<!-- Row height on individual rows -->
|
||||
<row r="1" ht="20" customHeight="1">
|
||||
...
|
||||
</row>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Freeze Panes
|
||||
|
||||
Inside `<sheetView>`:
|
||||
```xml
|
||||
<!-- Freeze row 1 (header row stays visible) -->
|
||||
<pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/>
|
||||
|
||||
<!-- Freeze column A -->
|
||||
<pane xSplit="1" topLeftCell="B1" activePane="topRight" state="frozen"/>
|
||||
|
||||
<!-- Freeze both row 1 and column A -->
|
||||
<pane xSplit="1" ySplit="1" topLeftCell="B2" activePane="bottomRight" state="frozen"/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7 Excel Error Types (All Must Be Absent at Delivery)
|
||||
|
||||
| Error | Meaning | Detect in XML |
|
||||
|-------|---------|---------------|
|
||||
| `#REF!` | Invalid cell reference | `<c t="e"><v>#REF!</v></c>` |
|
||||
| `#DIV/0!` | Divide by zero | `<c t="e"><v>#DIV/0!</v></c>` |
|
||||
| `#VALUE!` | Wrong data type | `<c t="e"><v>#VALUE!</v></c>` |
|
||||
| `#NAME?` | Unknown function/name | `<c t="e"><v>#NAME?</v></c>` |
|
||||
| `#NULL!` | Empty intersection | `<c t="e"><v>#NULL!</v></c>` |
|
||||
| `#NUM!` | Number out of range | `<c t="e"><v>#NUM!</v></c>` |
|
||||
| `#N/A` | Value not found | `<c t="e"><v>#N/A</v></c>` |
|
||||
@@ -0,0 +1,97 @@
|
||||
# Data Reading & Analysis Guide
|
||||
|
||||
> Reference for the READ path. Use `xlsx_reader.py` for structure discovery and data quality auditing,
|
||||
> then pandas for custom analysis. **Never modify the source file.**
|
||||
|
||||
---
|
||||
|
||||
## When to Use This Path
|
||||
|
||||
The user asks to read, analyze, view, summarize, extract, or answer questions about an Excel/CSV file's contents,
|
||||
without requiring file modification. If modification is needed, hand off to `edit.md`.
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1 — Structure Discovery
|
||||
|
||||
Run `xlsx_reader.py` first. It handles format detection, encoding fallback, structure exploration, and data quality audit:
|
||||
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/xlsx_reader.py input.xlsx # full report
|
||||
python3 SKILL_DIR/scripts/xlsx_reader.py input.xlsx --sheet Sales # single sheet
|
||||
python3 SKILL_DIR/scripts/xlsx_reader.py input.xlsx --quality # quality audit only
|
||||
python3 SKILL_DIR/scripts/xlsx_reader.py input.xlsx --json # machine-readable
|
||||
```
|
||||
|
||||
Supported formats: `.xlsx`, `.xlsm`, `.csv`, `.tsv`. The script tries multiple encodings for CSV (utf-8-sig, gbk, utf-8, latin-1).
|
||||
|
||||
### Step 2 — Custom Analysis with pandas
|
||||
|
||||
Load data and perform the analysis the user requests:
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
df = pd.read_excel("input.xlsx", sheet_name=None) # dict of all sheets
|
||||
# For CSV: pd.read_csv("input.csv")
|
||||
```
|
||||
|
||||
**Header handling** (when the default `header=0` doesn't work):
|
||||
|
||||
| Situation | Code |
|
||||
|-----------|------|
|
||||
| Header on row 3 | `pd.read_excel(path, header=2)` |
|
||||
| Multi-level merged header | `pd.read_excel(path, header=[0, 1])` |
|
||||
| No header | `pd.read_excel(path, header=None)` |
|
||||
|
||||
**Analysis quick reference:**
|
||||
|
||||
| Scenario | Pattern |
|
||||
|----------|---------|
|
||||
| Descriptive stats | `df.describe()` or `df['Col'].agg(['sum', 'mean', 'min', 'max'])` |
|
||||
| Group aggregation | `df.groupby('Region')['Revenue'].agg(Total='sum', Avg='mean')` |
|
||||
| Top N | `df.groupby('Region')['Revenue'].sum().sort_values(ascending=False).head(5)` |
|
||||
| Pivot table | `df.pivot_table(values='Revenue', index='Region', columns='Quarter', aggfunc='sum', margins=True)` |
|
||||
| Time series | `df.set_index(pd.to_datetime(df['Date'])).resample('ME')['Revenue'].sum()` |
|
||||
| Cross-sheet merge | `pd.merge(sales, customers, on='CustomerID', how='left', validate='m:1')` |
|
||||
| Stack sheets | `pd.concat([df.assign(Source=name) for name, df in sheets.items()], ignore_index=True)` |
|
||||
| Large files (>50MB) | `pd.read_excel(path, usecols=['Date', 'Revenue'])` or `pd.read_csv(path, chunksize=10000)` |
|
||||
|
||||
### Step 3 — Output
|
||||
|
||||
If the user specifies an output file path, write results to it (highest priority). Format the report as:
|
||||
|
||||
```
|
||||
## Analysis Report: {filename}
|
||||
### File Overview — format, sheets, row counts
|
||||
### Data Quality — nulls, duplicates, mixed types (or "no issues")
|
||||
### Key Findings — direct answer to the user's question
|
||||
### Additional Notes — formula NaN, encoding issues, caveats
|
||||
```
|
||||
|
||||
**Numeric display**: monetary `1,234,567.89`, percentage `12.3%`, multiples `8.5x`, counts as integers.
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
| Pitfall | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| Formula cells read as NaN | `<v>` cache empty in freshly generated files | Inform user; suggest opening in Excel and re-saving; or use `libreoffice_recalc.py` |
|
||||
| CSV encoding errors | Chinese Windows exports use GBK | `xlsx_reader.py` auto-tries multiple encodings; manually specify if all fail |
|
||||
| Mixed types in column | Column has both numbers and text (e.g., "N/A") | `pd.to_numeric(df['Col'], errors='coerce')` — report unconvertible rows |
|
||||
| Year shows as 2,024 | Thousands separator format applied to year | `df['Year'].astype(int).astype(str)` |
|
||||
| Multi-level headers | Two-row header merged | `pd.read_excel(path, header=[0, 1])`, then flatten with `' - '.join()` |
|
||||
| Row number mismatch | pandas 0-indexed vs Excel 1-indexed | `excel_row = pandas_index + 2` (+1 for 1-index, +1 for header) |
|
||||
|
||||
**Critical**: Never open with `data_only=True` then `save()` — this permanently destroys all formulas.
|
||||
|
||||
---
|
||||
|
||||
## Prohibitions
|
||||
|
||||
- Never modify the source file (no `save()`, no XML edits)
|
||||
- Never report formula NaN as "data is zero" — explain it's a formula cache issue
|
||||
- Never report pandas indices as Excel row numbers
|
||||
- Never make speculative conclusions unsupported by the data
|
||||
@@ -0,0 +1,772 @@
|
||||
# Formula Validation & Recalculation Guide
|
||||
|
||||
Ensure every formula in an xlsx file is provably correct before delivery. A file that opens without visible errors is not a passing file — only a file that has cleared both validation tiers is a passing file.
|
||||
|
||||
---
|
||||
|
||||
## Foundational Rules
|
||||
|
||||
- **Never declare PASS without running `formula_check.py` first.** Visual inspection of a spreadsheet is not validation.
|
||||
- **Tier 1 (static) is mandatory in every scenario.** Tier 2 (dynamic) is mandatory when LibreOffice is available. If it is unavailable, you must state this explicitly in the report — you may not silently skip it.
|
||||
- **Never use openpyxl with `data_only=True` to check formula values.** Opening and saving a workbook in `data_only=True` mode permanently replaces all formulas with their last cached values. Formulas cannot be recovered afterward.
|
||||
- **Auto-fix only deterministic errors.** Any fix that requires understanding business logic must be flagged for human review.
|
||||
|
||||
---
|
||||
|
||||
## Two-Tier Validation Architecture
|
||||
|
||||
```
|
||||
Tier 1 — Static Validation (XML scan, no external tools)
|
||||
│
|
||||
├── Detect: all 7 Excel error types already cached in <v> elements
|
||||
├── Detect: cross-sheet references pointing to nonexistent sheets
|
||||
├── Detect: formula cells with t="e" attribute (error type marker)
|
||||
└── Tool: formula_check.py + manual XML inspection
|
||||
│
|
||||
▼ (if LibreOffice is present)
|
||||
Tier 2 — Dynamic Validation (LibreOffice headless recalculation)
|
||||
│
|
||||
├── Executes all formulas via the LibreOffice Calc engine
|
||||
├── Populates <v> cache values with real computed results
|
||||
├── Exposes runtime errors invisible before recalculation
|
||||
└── Follow-up: re-run Tier 1 on the recalculated file
|
||||
```
|
||||
|
||||
**Why two tiers?**
|
||||
|
||||
openpyxl and all Python xlsx libraries write formula strings (e.g. `=SUM(B2:B9)`) into `<f>` elements but do not evaluate them. A freshly generated file has empty `<v>` cache elements for every formula cell. This means:
|
||||
|
||||
- Tier 1 can only catch errors that are already encoded in the XML — either as `t="e"` cells or as structurally broken cross-sheet references.
|
||||
- Tier 2 uses LibreOffice as the actual calculation engine, runs every formula, fills `<v>` with real results, and surfaces runtime errors (`#DIV/0!`, `#N/A`, etc.) that can only appear after computation.
|
||||
|
||||
Neither tier alone is sufficient. Together they cover the full correctability surface.
|
||||
|
||||
---
|
||||
|
||||
## Tier 1 — Static Validation
|
||||
|
||||
Static validation requires no external tools. It works directly on the ZIP/XML structure of the xlsx file.
|
||||
|
||||
### Step 1: Run formula_check.py
|
||||
|
||||
**Standard (human-readable) output:**
|
||||
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/formula_check.py /path/to/file.xlsx
|
||||
```
|
||||
|
||||
**JSON output (for programmatic processing):**
|
||||
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/formula_check.py /path/to/file.xlsx --json
|
||||
```
|
||||
|
||||
**Single-sheet mode (faster for targeted checks):**
|
||||
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/formula_check.py /path/to/file.xlsx --sheet Summary
|
||||
```
|
||||
|
||||
**Summary mode (counts only, no per-cell detail):**
|
||||
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/formula_check.py /path/to/file.xlsx --summary
|
||||
```
|
||||
|
||||
Exit codes:
|
||||
- `0` — no hard errors (PASS or PASS with heuristic warnings)
|
||||
- `1` — hard errors detected, or file cannot be opened (FAIL)
|
||||
|
||||
#### What formula_check.py examines
|
||||
|
||||
The script opens the xlsx as a ZIP archive without using any Excel library. It reads `xl/workbook.xml` to enumerate sheet names and named ranges, reads `xl/_rels/workbook.xml.rels` to map each sheet to its XML file, then iterates every `<c>` element in every worksheet.
|
||||
|
||||
It performs five checks:
|
||||
|
||||
1. **Error-value detection**: If the cell has `t="e"`, its `<v>` element contains an Excel error string. The cell is recorded with its sheet name, cell reference (e.g. `C5`), the error value, and the formula text if present.
|
||||
|
||||
2. **Broken cross-sheet reference detection**: If the cell has an `<f>` element, the script extracts all sheet names referenced in the formula (both `SheetName!` and `'Sheet Name'!` syntax). Each name is compared against the list of sheets in `workbook.xml`. A mismatch is a broken reference.
|
||||
|
||||
3. **Unknown named-range detection (heuristic)**: Identifiers in formulas that are not function names, not cell references, and not found in `workbook.xml`'s `<definedNames>` are flagged as `unknown_name_ref` warnings. This is a heuristic — false positives are possible; always verify manually.
|
||||
|
||||
4. **Shared formula integrity**: Shared formula consumer cells (those with only `<f t="shared" si="N"/>`) are skipped for formula counting and cross-ref checks because they inherit the primary cell's formula. Only the primary cell (with `ref="..."` attribute and formula text) is checked and counted.
|
||||
|
||||
5. **Malformed error cells**: Cells with `t="e"` but no `<v>` child element are flagged as structural XML issues.
|
||||
|
||||
Hard errors (exit code 1): `error_value`, `broken_sheet_ref`, `malformed_error_cell`, `file_error`
|
||||
Soft warnings (exit code 0): `unknown_name_ref` — must be verified manually but do not block delivery alone
|
||||
|
||||
#### Reading formula_check.py human-readable output
|
||||
|
||||
A clean file looks like this:
|
||||
|
||||
```
|
||||
File : /tmp/budget_2024.xlsx
|
||||
Sheets : Summary, Q1, Q2, Q3, Q4, Assumptions
|
||||
Formulas checked : 312 distinct formula cells
|
||||
Shared formula ranges : 4 ranges
|
||||
Errors found : 0
|
||||
|
||||
PASS — No formula errors detected
|
||||
```
|
||||
|
||||
A file with errors looks like this:
|
||||
|
||||
```
|
||||
File : /tmp/budget_2024.xlsx
|
||||
Sheets : Summary, Q1, Q2, Q3, Q4, Assumptions
|
||||
Formulas checked : 312 distinct formula cells
|
||||
Shared formula ranges : 4 ranges
|
||||
Errors found : 4
|
||||
|
||||
── Error Details ──
|
||||
[FAIL] [Summary!C12] contains #REF! (formula: Q1!A0/Q1!A1)
|
||||
[FAIL] [Summary!D15] references missing sheet 'Q5'
|
||||
Formula: Q5!D15
|
||||
Valid sheets: ['Assumptions', 'Q1', 'Q2', 'Q3', 'Q4', 'Summary']
|
||||
[FAIL] [Q1!F8] contains #DIV/0!
|
||||
[WARN] [Q2!B10] uses unknown name 'GrowthAssumptions' (heuristic — verify manually)
|
||||
Formula: SUM(GrowthAssumptions)
|
||||
Defined names: ['RevenueRange', 'CostRange']
|
||||
|
||||
FAIL — 3 error(s) must be fixed before delivery
|
||||
WARN — 1 heuristic warning(s) require manual review
|
||||
```
|
||||
|
||||
Interpretation of each line:
|
||||
- `[FAIL] [Summary!C12] contains #REF! (formula: Q1!A0/Q1!A1)` — The cell has `t="e"` and `<v>#REF!</v>`. The formula references row 0, which does not exist in Excel's 1-based system. This is an off-by-one error in a generated reference.
|
||||
- `[FAIL] [Summary!D15] references missing sheet 'Q5'` — The formula contains `Q5!D15`, but no sheet named `Q5` exists in the workbook. The valid sheet list is provided for comparison.
|
||||
- `[FAIL] [Q1!F8] contains #DIV/0!` — This cell's `<v>` is already an error value (the file was previously recalculated). The formula divided by zero.
|
||||
- `[WARN] [Q2!B10] uses unknown name 'GrowthAssumptions'` — The identifier `GrowthAssumptions` appears in the formula but is not in `<definedNames>`. This may be a typo or a name that was accidentally omitted. It is a heuristic warning — verify manually. The warning alone does not block delivery.
|
||||
|
||||
#### Reading formula_check.py JSON output
|
||||
|
||||
```json
|
||||
{
|
||||
"file": "/tmp/budget_2024.xlsx",
|
||||
"sheets_checked": ["Summary", "Q1", "Q2", "Q3", "Q4", "Assumptions"],
|
||||
"formula_count": 312,
|
||||
"shared_formula_ranges": 4,
|
||||
"error_count": 4,
|
||||
"errors": [
|
||||
{
|
||||
"type": "error_value",
|
||||
"error": "#REF!",
|
||||
"sheet": "Summary",
|
||||
"cell": "C12",
|
||||
"formula": "Q1!A0/Q1!A1"
|
||||
},
|
||||
{
|
||||
"type": "broken_sheet_ref",
|
||||
"sheet": "Summary",
|
||||
"cell": "D15",
|
||||
"formula": "Q5!D15",
|
||||
"missing_sheet": "Q5",
|
||||
"valid_sheets": ["Assumptions", "Q1", "Q2", "Q3", "Q4", "Summary"]
|
||||
},
|
||||
{
|
||||
"type": "error_value",
|
||||
"error": "#DIV/0!",
|
||||
"sheet": "Q1",
|
||||
"cell": "F8",
|
||||
"formula": null
|
||||
},
|
||||
{
|
||||
"type": "unknown_name_ref",
|
||||
"sheet": "Q2",
|
||||
"cell": "B10",
|
||||
"formula": "SUM(GrowthAssumptions)",
|
||||
"unknown_name": "GrowthAssumptions",
|
||||
"defined_names": ["RevenueRange", "CostRange"],
|
||||
"note": "Heuristic check — verify manually if this is a false positive"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Field reference:
|
||||
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `type: "error_value"` | Cell has `t="e"` — an Excel error is stored in the `<v>` element |
|
||||
| `type: "broken_sheet_ref"` | Formula references a sheet name not present in workbook.xml |
|
||||
| `type: "unknown_name_ref"` | Formula references an identifier not in `<definedNames>` (heuristic, soft warning) |
|
||||
| `type: "malformed_error_cell"` | Cell has `t="e"` but no `<v>` child — structural XML problem |
|
||||
| `type: "file_error"` | The file could not be opened (bad ZIP, not found, etc.) |
|
||||
| `sheet` | The sheet where the error was found |
|
||||
| `cell` | Cell reference in A1 notation |
|
||||
| `formula` | The full formula text from the `<f>` element (null if not present) |
|
||||
| `error` | The error string from `<v>` (for `error_value` type) |
|
||||
| `missing_sheet` | The sheet name extracted from the formula that does not exist |
|
||||
| `valid_sheets` | All sheet names actually present in workbook.xml |
|
||||
| `unknown_name` | The identifier that was not found in `<definedNames>` |
|
||||
| `defined_names` | All named ranges actually present in workbook.xml |
|
||||
| `shared_formula_ranges` | Count of shared formula definitions (top-level `<f t="shared" ref="...">` elements) |
|
||||
|
||||
### Step 2: Manual XML inspection
|
||||
|
||||
When formula_check.py reports errors, unpack the file to inspect the raw XML:
|
||||
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/xlsx_unpack.py /path/to/file.xlsx /tmp/xlsx_inspect/
|
||||
```
|
||||
|
||||
Navigate to the worksheet file for the reported sheet. The sheet-to-file mapping is in `xl/_rels/workbook.xml.rels`. For example, if `rId1` maps to `worksheets/sheet1.xml`, then sheet1.xml is the file for the sheet with `r:id="rId1"` in `xl/workbook.xml`.
|
||||
|
||||
For each reported error cell, locate the `<c r="CELLREF">` element and examine:
|
||||
|
||||
**For `error_value` errors:**
|
||||
```xml
|
||||
<!-- This is what an error cell looks like in XML -->
|
||||
<c r="C12" t="e">
|
||||
<f>Q1!C10/Q1!C11</f>
|
||||
<v>#DIV/0!</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
Ask:
|
||||
- Is the `<f>` formula syntactically correct?
|
||||
- Does the cell reference in the formula point to a row/column that exists?
|
||||
- If it is a division, is it possible the denominator cell is empty or zero?
|
||||
|
||||
**For `broken_sheet_ref` errors:**
|
||||
|
||||
Check `xl/workbook.xml` for the actual sheet list:
|
||||
|
||||
```xml
|
||||
<sheets>
|
||||
<sheet name="Summary" sheetId="1" r:id="rId1"/>
|
||||
<sheet name="Q1" sheetId="2" r:id="rId2"/>
|
||||
<sheet name="Q2" sheetId="3" r:id="rId3"/>
|
||||
</sheets>
|
||||
```
|
||||
|
||||
Sheet names are case-sensitive. `q1` and `Q1` are different sheets. Compare the name in the formula exactly against the names here.
|
||||
|
||||
### Step 3: Cross-sheet reference audit (multi-sheet workbooks)
|
||||
|
||||
For workbooks with 3 or more sheets, run a broader cross-reference audit after unpacking:
|
||||
|
||||
```bash
|
||||
# Extract all formulas containing cross-sheet references
|
||||
grep -h "<f>" /tmp/xlsx_inspect/xl/worksheets/*.xml | grep "!"
|
||||
|
||||
# List all actual sheet names from workbook.xml
|
||||
grep -o 'name="[^"]*"' /tmp/xlsx_inspect/xl/workbook.xml | grep -v sheetId
|
||||
```
|
||||
|
||||
Every sheet name appearing in formulas (in the form `SheetName!` or `'Sheet Name'!`) must appear in the workbook sheet list. If any do not match, that is a broken reference even if formula_check.py did not catch it (which can happen with shared formulas where only the primary cell is examined).
|
||||
|
||||
To check shared formulas specifically, look for `<f t="shared" ref="...">` elements:
|
||||
|
||||
```xml
|
||||
<!-- Shared formula: defined on D2, applied to D2:D100 -->
|
||||
<c r="D2"><f t="shared" ref="D2:D100" si="0">Q1!B2*C2</f><v></v></c>
|
||||
|
||||
<!-- Shared formula consumers: only si is present, no formula text -->
|
||||
<c r="D3"><f t="shared" si="0"/><v></v></c>
|
||||
```
|
||||
|
||||
formula_check.py reads the formula text from the primary cell (`D2` above). The referenced sheet `Q1` in that formula applies to the entire range `D2:D100`. If the sheet is broken, all 99 rows are broken even though they appear as empty `<f>` elements.
|
||||
|
||||
---
|
||||
|
||||
## Tier 2 — Dynamic Validation (LibreOffice Headless)
|
||||
|
||||
### Check LibreOffice availability
|
||||
|
||||
```bash
|
||||
# Check macOS (typical install location)
|
||||
which soffice
|
||||
/Applications/LibreOffice.app/Contents/MacOS/soffice --version
|
||||
|
||||
# Check Linux
|
||||
which libreoffice || which soffice
|
||||
libreoffice --version
|
||||
```
|
||||
|
||||
If neither command returns a path, LibreOffice is not installed. Record "Tier 2: SKIPPED — LibreOffice not available" in the report and proceed to delivery with Tier 1 results only.
|
||||
|
||||
### Install LibreOffice (if permitted in the environment)
|
||||
|
||||
macOS:
|
||||
```bash
|
||||
brew install --cask libreoffice
|
||||
```
|
||||
|
||||
Ubuntu/Debian:
|
||||
```bash
|
||||
sudo apt-get install -y libreoffice
|
||||
```
|
||||
|
||||
### Run headless recalculation
|
||||
|
||||
Use the dedicated recalculation script. It handles binary discovery across macOS and Linux, works from a temporary copy of the input (preserving the original), and provides structured output and exit codes compatible with the validation pipeline.
|
||||
|
||||
```bash
|
||||
# Check LibreOffice availability first
|
||||
python3 SKILL_DIR/scripts/libreoffice_recalc.py --check
|
||||
|
||||
# Run recalculation (default timeout: 60s)
|
||||
python3 SKILL_DIR/scripts/libreoffice_recalc.py /path/to/input.xlsx /tmp/recalculated.xlsx
|
||||
|
||||
# For large or complex files, extend the timeout
|
||||
python3 SKILL_DIR/scripts/libreoffice_recalc.py /path/to/input.xlsx /tmp/recalculated.xlsx --timeout 120
|
||||
```
|
||||
|
||||
Exit codes from `libreoffice_recalc.py`:
|
||||
- `0` — recalculation succeeded, output file written
|
||||
- `2` — LibreOffice not found (note as SKIPPED in report; not a hard failure)
|
||||
- `1` — LibreOffice found but failed (timeout, crash, malformed file)
|
||||
|
||||
**What the script does internally:**
|
||||
|
||||
LibreOffice's `--convert-to xlsx` command opens the file using the full Calc engine with the `--infilter="Calc MS Excel 2007 XML"` filter, executes every formula, writes computed values into the `<v>` cache elements, and saves the output. This is the closest server-side equivalent of "open in Excel and press Save." The script also passes `--norestore` to prevent LibreOffice from attempting to restore previous sessions, which can cause hangs in automated environments.
|
||||
|
||||
**If LibreOffice is not installed:**
|
||||
|
||||
macOS:
|
||||
```bash
|
||||
brew install --cask libreoffice
|
||||
```
|
||||
|
||||
Ubuntu/Debian:
|
||||
```bash
|
||||
sudo apt-get install -y libreoffice
|
||||
```
|
||||
|
||||
**If the script times out (libreoffice_recalc.py exits with code 1 and "timed out" message):**
|
||||
|
||||
Record "Tier 2: TIMEOUT — LibreOffice did not complete within Ns" in the report. Do not retry in a loop. Investigate whether the file has circular references or extremely large data ranges.
|
||||
|
||||
### Re-run Tier 1 after recalculation
|
||||
|
||||
After LibreOffice recalculation, the `<v>` elements contain real computed values. Errors that were invisible before (because `<v>` was empty in a freshly generated file) now appear as `t="e"` cells with actual error strings.
|
||||
|
||||
```bash
|
||||
python3 SKILL_DIR/scripts/formula_check.py /tmp/recalculated.xlsx
|
||||
```
|
||||
|
||||
This second Tier 1 pass is the definitive runtime error check. Any errors it finds are real calculation failures that must be fixed.
|
||||
|
||||
---
|
||||
|
||||
## All 7 Error Types — Causes and Fix Strategies
|
||||
|
||||
### #REF! — Invalid Cell Reference
|
||||
|
||||
**What it means:** The formula references a cell, range, or sheet that no longer exists or never existed.
|
||||
|
||||
**Common causes in generated files:**
|
||||
- Off-by-one error in row/column calculation (e.g., referencing row 0 which does not exist in Excel's 1-based system)
|
||||
- Column letter computed incorrectly (e.g., column 64 maps to `BL`, not `BK`)
|
||||
- Formula references a sheet that was never created or was renamed
|
||||
|
||||
**XML signature:**
|
||||
```xml
|
||||
<c r="D5" t="e">
|
||||
<f>Sheet2!A0</f>
|
||||
<v>#REF!</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**Fix — correct the reference:**
|
||||
```xml
|
||||
<c r="D5">
|
||||
<f>Sheet2!A1</f>
|
||||
<v></v>
|
||||
</c>
|
||||
```
|
||||
|
||||
Note: remove `t="e"` and clear `<v>` after correcting the formula. The error type marker belongs to the cached state, not the formula.
|
||||
|
||||
**Auto-fixable?** Only if the correct target can be determined with certainty from the surrounding context. Otherwise flag for human review.
|
||||
|
||||
---
|
||||
|
||||
### #DIV/0! — Division by Zero
|
||||
|
||||
**What it means:** The formula divides by a value that is zero or an empty cell (empty cells evaluate to 0 in arithmetic context).
|
||||
|
||||
**Common causes in generated files:**
|
||||
- Percentage change formula `=(B2-B1)/B1` where `B1` is empty or zero
|
||||
- Rate formula `=Value/Total` where the total row hasn't been populated yet
|
||||
|
||||
**XML signature:**
|
||||
```xml
|
||||
<c r="C8" t="e">
|
||||
<f>B8/B7</f>
|
||||
<v>#DIV/0!</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**Fix — wrap with IFERROR:**
|
||||
```xml
|
||||
<c r="C8">
|
||||
<f>IFERROR(B8/B7,0)</f>
|
||||
<v></v>
|
||||
</c>
|
||||
```
|
||||
|
||||
Alternative — explicit zero check:
|
||||
```xml
|
||||
<c r="C8">
|
||||
<f>IF(B7=0,0,B8/B7)</f>
|
||||
<v></v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**Auto-fixable?** Yes. Wrapping with `IFERROR(...,0)` is safe for most financial formulas. If the business expectation is that the result should display as blank rather than zero, use `IFERROR(...,"")` instead.
|
||||
|
||||
---
|
||||
|
||||
### #VALUE! — Wrong Data Type
|
||||
|
||||
**What it means:** The formula attempts an arithmetic or logical operation on a value of the wrong type (e.g., adding a text string to a number).
|
||||
|
||||
**Common causes in generated files:**
|
||||
- A cell intended to hold a number was written as a string type (`t="s"` or `t="inlineStr"`) instead of a numeric type
|
||||
- A formula references a cell containing text (e.g., a unit label like "thousands") and treats it as a number
|
||||
|
||||
**XML signature:**
|
||||
```xml
|
||||
<c r="F3" t="e">
|
||||
<f>E3+D3</f>
|
||||
<v>#VALUE!</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**Fix — check source cells for incorrect type:**
|
||||
|
||||
If `D3` was incorrectly written as a string:
|
||||
```xml
|
||||
<!-- Wrong: numeric value stored as string -->
|
||||
<c r="D3" t="inlineStr"><is><t>1000</t></is></c>
|
||||
|
||||
<!-- Correct: numeric value stored as number (t attribute omitted or "n") -->
|
||||
<c r="D3"><v>1000</v></c>
|
||||
```
|
||||
|
||||
Alternatively, wrap the formula with `VALUE()` conversion:
|
||||
```xml
|
||||
<c r="F3">
|
||||
<f>VALUE(E3)+VALUE(D3)</f>
|
||||
<v></v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**Auto-fixable?** Partially. If the source cell type is visibly wrong (a number stored as string), fix the type. If the cause is ambiguous (the cell is supposed to contain text), flag for human review.
|
||||
|
||||
---
|
||||
|
||||
### #NAME? — Unrecognized Name
|
||||
|
||||
**What it means:** The formula contains an identifier that Excel does not recognize — either a misspelled function name, an undefined named range, or a function that is not available in the target Excel version.
|
||||
|
||||
**Common causes in generated files:**
|
||||
- LLM writes a function name with a typo: `SUMIF` written as `SUMIFS` when only 3 arguments are provided, or `XLOOKUP` used in a context targeting Excel 2010
|
||||
- Named range referenced in formula does not exist in `xl/workbook.xml`
|
||||
|
||||
**XML signature:**
|
||||
```xml
|
||||
<c r="B2" t="e">
|
||||
<f>SUMSQ(A2:A10)</f>
|
||||
<v>#NAME?</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**Fix — verify function name and named ranges:**
|
||||
|
||||
Check named ranges in `xl/workbook.xml`:
|
||||
```xml
|
||||
<definedNames>
|
||||
<definedName name="RevenueRange">Sheet1!$B$2:$B$13</definedName>
|
||||
</definedNames>
|
||||
```
|
||||
|
||||
If the formula references `RevenuRange` (typo), correct it to `RevenueRange`:
|
||||
```xml
|
||||
<c r="B2">
|
||||
<f>SUM(RevenueRange)</f>
|
||||
<v></v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**Auto-fixable?** Only if the correct name is unambiguous (e.g., a single close match exists). Otherwise flag for human review — function name fixes require understanding the intended calculation.
|
||||
|
||||
---
|
||||
|
||||
### #N/A — Value Not Available
|
||||
|
||||
**What it means:** A lookup function (VLOOKUP, HLOOKUP, MATCH, INDEX/MATCH, XLOOKUP) searched for a value that does not exist in the lookup table.
|
||||
|
||||
**Common causes in generated files:**
|
||||
- Lookup key exists in the formula but the lookup table is empty or not yet populated
|
||||
- Key format mismatch (text "2024" vs numeric 2024)
|
||||
|
||||
**XML signature:**
|
||||
```xml
|
||||
<c r="G5" t="e">
|
||||
<f>VLOOKUP(F5,Assumptions!$A$2:$B$20,2,0)</f>
|
||||
<v>#N/A</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**Fix — wrap with IFERROR for missing-match tolerance:**
|
||||
```xml
|
||||
<c r="G5">
|
||||
<f>IFERROR(VLOOKUP(F5,Assumptions!$A$2:$B$20,2,0),0)</f>
|
||||
<v></v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**Auto-fixable?** Adding `IFERROR` is safe if a zero default is acceptable. If the lookup failure indicates a data integrity problem (the key should always be present), do not auto-fix — flag for human review.
|
||||
|
||||
---
|
||||
|
||||
### #NULL! — Empty Intersection
|
||||
|
||||
**What it means:** The space operator (which computes the intersection of two ranges) was applied to two ranges that do not intersect.
|
||||
|
||||
**Common causes in generated files:**
|
||||
- Accidental space between two range references: `=SUM(A1:A5 C1:C5)` instead of `=SUM(A1:A5,C1:C5)`
|
||||
- Rarely seen in typical financial models; usually indicates a formula generation error
|
||||
|
||||
**XML signature:**
|
||||
```xml
|
||||
<c r="H10" t="e">
|
||||
<f>SUM(A1:A5 C1:C5)</f>
|
||||
<v>#NULL!</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**Fix — replace space with comma (union) or colon (range):**
|
||||
```xml
|
||||
<!-- Union of two separate ranges -->
|
||||
<c r="H10">
|
||||
<f>SUM(A1:A5,C1:C5)</f>
|
||||
<v></v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**Auto-fixable?** Yes. The space operator is almost never intentional in generated formulas. Replacing with a comma is safe.
|
||||
|
||||
---
|
||||
|
||||
### #NUM! — Numeric Error
|
||||
|
||||
**What it means:** A formula produced a number that Excel cannot represent (overflow, underflow) or a mathematical operation that has no real-number result (square root of negative, LOG of zero or negative).
|
||||
|
||||
**Common causes in generated files:**
|
||||
- IRR or NPV formula where the cash flow series has no convergent solution
|
||||
- `SQRT()` applied to a cell that can be negative
|
||||
- Very large exponentiation
|
||||
|
||||
**XML signature:**
|
||||
```xml
|
||||
<c r="J15" t="e">
|
||||
<f>IRR(B5:B15)</f>
|
||||
<v>#NUM!</v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**Fix — add a conditional guard:**
|
||||
```xml
|
||||
<c r="J15">
|
||||
<f>IFERROR(IRR(B5:B15),"")</f>
|
||||
<v></v>
|
||||
</c>
|
||||
```
|
||||
|
||||
For SQRT:
|
||||
```xml
|
||||
<c r="K5">
|
||||
<f>IF(A5>=0,SQRT(A5),"")</f>
|
||||
<v></v>
|
||||
</c>
|
||||
```
|
||||
|
||||
**Auto-fixable?** Partially. Wrapping with `IFERROR` suppresses the error display but does not fix the underlying calculation issue. Flag the cell for human review even after applying the IFERROR wrapper.
|
||||
|
||||
---
|
||||
|
||||
## Auto-Fix vs. Human Review Decision Matrix
|
||||
|
||||
| Error Type | Auto-Fix Safe? | Condition | Action |
|
||||
|------------|---------------|-----------|--------|
|
||||
| `#DIV/0!` | Yes | Always | Wrap with `IFERROR(formula,0)` |
|
||||
| `#NULL!` | Yes | Always | Replace space operator with comma |
|
||||
| `#REF!` | Yes | Only if correct target is unambiguous from context | Correct reference; otherwise flag |
|
||||
| `#NAME?` | Yes | Only if typo has exactly one plausible correction | Fix name; otherwise flag |
|
||||
| `#N/A` | Conditional | If a zero/blank default is business-acceptable | Add IFERROR wrapper; document assumption |
|
||||
| `#VALUE!` | Conditional | Only if source cell type is clearly wrong | Fix type; otherwise flag |
|
||||
| `#NUM!` | No | Always | Add IFERROR to suppress display, then flag |
|
||||
| Broken sheet ref | Yes | Only if renamed sheet can be identified from workbook.xml | Correct name |
|
||||
| Business logic errors | Never | Any case | Human review only |
|
||||
|
||||
**What counts as a business logic error (never auto-fix):**
|
||||
- A formula that produces a wrong number but no Excel error (e.g., `=SUM(B2:B8)` when the intent was `=SUM(B2:B9)`)
|
||||
- A formula where the IFERROR default value is meaningful (e.g., whether to use 0, blank, or a prior-period value)
|
||||
- Any formula where fixing the error requires knowing what the formula was supposed to calculate
|
||||
|
||||
---
|
||||
|
||||
## Delivery Standard — Validation Report
|
||||
|
||||
Every validation task must produce a structured report. This report is the deliverable, regardless of whether errors were found.
|
||||
|
||||
### Required report format
|
||||
|
||||
```markdown
|
||||
## Formula Validation Report
|
||||
|
||||
**File**: /path/to/filename.xlsx
|
||||
**Date**: YYYY-MM-DD
|
||||
**Sheets checked**: Sheet1, Sheet2, Sheet3
|
||||
**Total formulas scanned**: N
|
||||
|
||||
---
|
||||
|
||||
### Tier 1 — Static Validation
|
||||
|
||||
**Status**: PASS / FAIL
|
||||
**Tool**: formula_check.py (direct XML scan)
|
||||
|
||||
| Sheet | Cell | Error Type | Detail | Fix Applied |
|
||||
|-------|------|-----------|--------|-------------|
|
||||
| Summary | C12 | #REF! | Formula: Q1!A0 | Corrected to Q1!A1 |
|
||||
| Summary | D15 | broken_sheet_ref | References missing sheet 'Q5' | Renamed to Q4 |
|
||||
|
||||
_(If no errors: "No errors detected.")_
|
||||
|
||||
---
|
||||
|
||||
### Tier 2 — Dynamic Validation
|
||||
|
||||
**Status**: PASS / FAIL / SKIPPED
|
||||
**Tool**: LibreOffice headless (version X.Y.Z) / Not available
|
||||
|
||||
_(If SKIPPED: state the reason — LibreOffice not installed, timeout, etc.)_
|
||||
|
||||
| Sheet | Cell | Error Type | Detail | Fix Applied |
|
||||
|-------|------|-----------|--------|-------------|
|
||||
| Q1 | F8 | #DIV/0! | Formula: C8/C7 | Wrapped with IFERROR |
|
||||
|
||||
_(If no errors: "No runtime errors detected after recalculation.")_
|
||||
|
||||
---
|
||||
|
||||
### Summary
|
||||
|
||||
- **Total errors found**: N
|
||||
- **Auto-fixed**: N (list types)
|
||||
- **Flagged for human review**: N (list cells and reason)
|
||||
- **Final status**: PASS (ready for delivery) / FAIL (blocked)
|
||||
|
||||
### Human Review Required
|
||||
|
||||
| Cell | Error | Reason Auto-Fix Not Applied |
|
||||
|------|-------|----------------------------|
|
||||
| Q2!B15 | #NUM! | IRR formula — business must confirm cash flow inputs |
|
||||
```
|
||||
|
||||
### Minimum required fields
|
||||
|
||||
The report is invalid (and delivery is blocked) if any of these are missing:
|
||||
- File path and date
|
||||
- Which sheets were checked
|
||||
- Total formula count
|
||||
- Tier 1 status with explicit PASS/FAIL
|
||||
- Tier 2 status with explicit PASS/FAIL/SKIPPED and reason if SKIPPED
|
||||
- For every error: sheet, cell, error type, and disposition (fixed or flagged)
|
||||
- Final delivery status
|
||||
|
||||
---
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
### Scenario 1: Validate immediately after creating a new file
|
||||
|
||||
When `create.md` workflow produces a new xlsx, run validation before any delivery response.
|
||||
|
||||
```bash
|
||||
# Step 1: Static check on the freshly written file
|
||||
python3 SKILL_DIR/scripts/formula_check.py /path/to/output.xlsx
|
||||
|
||||
# Step 2: Dynamic check (if LibreOffice available)
|
||||
python3 SKILL_DIR/scripts/libreoffice_recalc.py /path/to/output.xlsx /tmp/recalculated.xlsx
|
||||
python3 SKILL_DIR/scripts/formula_check.py /tmp/recalculated.xlsx
|
||||
```
|
||||
|
||||
Expected behavior on a freshly created file: Tier 1 will find zero `error_value` errors (because `<v>` elements are empty, not error-valued). It will find any broken cross-sheet references if sheet names were misspelled. Tier 2 will populate `<v>` and reveal runtime errors like `#DIV/0!`.
|
||||
|
||||
If Tier 2 reveals errors, fix them in the source XML (not the recalculated copy), repack, and re-run both tiers.
|
||||
|
||||
### Scenario 2: Validate after editing an existing file
|
||||
|
||||
When `edit.md` workflow modifies an existing xlsx, validate only the affected sheets if the edit was surgical. If the edit touched shared formulas or cross-sheet references, validate all sheets.
|
||||
|
||||
```bash
|
||||
# Targeted static check — look at specific sheet
|
||||
# (formula_check.py checks all sheets; examine only the relevant section of output)
|
||||
python3 SKILL_DIR/scripts/formula_check.py /path/to/edited.xlsx --json \
|
||||
| python3 -c "
|
||||
import json, sys
|
||||
r = json.load(sys.stdin)
|
||||
for e in r['errors']:
|
||||
if e.get('sheet') in ['Summary', 'Q1']:
|
||||
print(e)
|
||||
"
|
||||
```
|
||||
|
||||
Always run Tier 2 after edits that modify formulas, even if Tier 1 passes. Edits to data ranges can cause previously-valid formulas to produce runtime errors.
|
||||
|
||||
### Scenario 3: User provides a file with suspected formula errors
|
||||
|
||||
When a user submits a file and reports wrong values or visible errors:
|
||||
|
||||
```bash
|
||||
# Step 1: Static scan — find all error cells
|
||||
python3 SKILL_DIR/scripts/formula_check.py /path/to/user_file.xlsx --json > /tmp/validation_results.json
|
||||
|
||||
# Step 2: Unpack for manual inspection
|
||||
python3 SKILL_DIR/scripts/xlsx_unpack.py /path/to/user_file.xlsx /tmp/xlsx_inspect/
|
||||
|
||||
# Step 3: Dynamic recalculation
|
||||
python3 SKILL_DIR/scripts/libreoffice_recalc.py /path/to/user_file.xlsx /tmp/user_file_recalc.xlsx
|
||||
|
||||
# Step 4: Re-validate recalculated file
|
||||
python3 SKILL_DIR/scripts/formula_check.py /tmp/user_file_recalc.xlsx --json > /tmp/validation_after_recalc.json
|
||||
|
||||
# Step 5: Compare before and after
|
||||
python3 - <<'EOF'
|
||||
import json
|
||||
before = json.load(open("/tmp/validation_results.json"))
|
||||
after = json.load(open("/tmp/validation_after_recalc.json"))
|
||||
print(f"Before recalc: {before['error_count']} errors")
|
||||
print(f"After recalc: {after['error_count']} errors")
|
||||
EOF
|
||||
```
|
||||
|
||||
If errors appear only after recalculation (not in the original static scan), the formulas were syntactically correct but produce wrong results at runtime. These are runtime errors that require formula-level fixes, not XML-structure fixes.
|
||||
|
||||
If errors appear in both scans, they were already cached in `<v>` before recalculation — the file was previously opened by Excel/LibreOffice and the errors persisted.
|
||||
|
||||
---
|
||||
|
||||
## Critical Pitfalls
|
||||
|
||||
**Pitfall 1: openpyxl `data_only=True` destroys formulas.**
|
||||
Opening a workbook with `data_only=True` reads cached values instead of formulas. If you then save the workbook, all `<f>` elements are permanently removed and replaced with their last-cached values. Never use this mode for validation workflows.
|
||||
|
||||
**Pitfall 2: Empty `<v>` is not the same as a passing formula.**
|
||||
A freshly generated file has empty `<v>` elements for all formula cells. formula_check.py will not report these as errors — they are not yet errors. They become errors only after recalculation if the calculated value is an error type. This is why Tier 2 is mandatory.
|
||||
|
||||
**Pitfall 3: Shared formula errors affect the entire range.**
|
||||
If a shared formula's primary cell has a broken reference, every cell in the shared range (`ref="D2:D100"`) inherits that broken reference. The count of logical errors can be much larger than the count of distinct error entries in formula_check.py output. When fixing a broken shared formula, fix the primary cell's `<f t="shared" ref="...">` element; the consumers (`<f t="shared" si="N"/>`) automatically inherit the corrected formula.
|
||||
|
||||
**Pitfall 4: Sheet names are case-sensitive.**
|
||||
`=q1!B5` and `=Q1!B5` are different references. Excel internally treats them the same, but formula_check.py's string comparison is case-sensitive. If a formula uses a lowercase sheet name that matches an uppercase sheet in the workbook, it will be flagged as a broken reference. The fix is to match the exact case in `workbook.xml`.
|
||||
|
||||
**Pitfall 5: `--convert-to xlsx` does not guarantee formula preservation.**
|
||||
LibreOffice's conversion can occasionally alter certain formula types (array formulas, dynamic array functions like `SORT`, `UNIQUE`). After Tier 2, if the recalculated file shows formula changes unrelated to error fixing, do not deliver the recalculated file directly — use the original file with targeted XML fixes instead.
|
||||
Submodule builtin-skills/document-process/my-writing-style deleted from d94826e1cd.
@@ -0,0 +1,71 @@
|
||||
# Writing Style Skill
|
||||
|
||||
可复用的写作风格 Skill 模板。**内置自动学习** — 从你的修改中自动提取规则,SKILL.md 越用越准。
|
||||
|
||||
兼容 **Claude Code** + **OpenClaw (ClawHub)**。
|
||||
|
||||
## 原理
|
||||
|
||||
```
|
||||
AI 用 SKILL.md 写初稿 → 你改到满意 → diff 两版 → 提取规则 → 更新 SKILL.md → 下次更准
|
||||
```
|
||||
|
||||
只要两个数据点:**original**(AI 第一版)和 **final**(你最终版)。中间改了多少轮不管。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# Claude Code
|
||||
git clone https://github.com/jzOcb/writing-style-skill.git
|
||||
cp -r writing-style-skill ~/.claude/skills/my-writing-style
|
||||
|
||||
# OpenClaw / ClawHub
|
||||
npx clawhub@latest install jz-writing-style-skill
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
1. **改 SKILL.md** — 把模板里的风格规则改成你自己的(或留空,让自动学习帮你填)
|
||||
2. **让 AI 用这个 skill 写内容**
|
||||
3. **你改到满意**
|
||||
4. 记录:
|
||||
```bash
|
||||
python3 scripts/observe.py record-original draft.md
|
||||
# ... 你修改 ...
|
||||
python3 scripts/observe.py record-final final.md
|
||||
```
|
||||
5. 提取规则:
|
||||
```bash
|
||||
python3 scripts/improve.py auto --skill .
|
||||
```
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
writing-style-skill/
|
||||
├── SKILL.md # 你的写作风格(模板,改成你的)
|
||||
├── README.md # 本文件
|
||||
└── scripts/
|
||||
├── observe.py # 记录 original / final(零依赖)
|
||||
└── improve.py # 提取 / 应用 / 回滚(需要 LLM CLI)
|
||||
```
|
||||
|
||||
## 自动学习怎么工作
|
||||
|
||||
- `observe.py` 记录 AI 原稿和你的最终版
|
||||
- `improve.py` 用 LLM 分析 diff,提取写作规则
|
||||
- 规则按置信度分 P0/P1/P2,P0 自动应用
|
||||
- 每次更新前自动备份,一键回滚
|
||||
|
||||
## LLM 支持
|
||||
|
||||
`improve.py` 自动检测:
|
||||
- `claude` (Claude Code) — 优先
|
||||
- `llm` (pip install llm) — 通用
|
||||
- `IMPROVE_LLM_CMD` 环境变量 — 自定义
|
||||
|
||||
`observe.py` 零依赖纯 Python。
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,206 @@
|
||||
---
|
||||
name: my-writing-style
|
||||
version: 1.0.0
|
||||
description: |
|
||||
可复用的写作风格 Skill 模板。内置自动学习:
|
||||
从你的修改中自动提取规则,SKILL.md 越用越准。
|
||||
Fork 后改成你自己的风格。
|
||||
dependencies: []
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- exec
|
||||
---
|
||||
|
||||
# Writing Style Skill(模板)
|
||||
|
||||
**Fork 这个 skill,改成你的写作风格。内置自动学习,越用越准。**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 怎么用
|
||||
|
||||
1. Fork / clone 这个 skill
|
||||
2. 把下面的风格规则改成你自己的
|
||||
3. 让 AI 用这个 skill 写内容
|
||||
4. 你改到满意 → 脚本自动学习你改了什么
|
||||
5. 下次 AI 写出来的就更像你
|
||||
|
||||
---
|
||||
|
||||
## 【0】Voice Dimensions(量化你的风格)
|
||||
|
||||
**用 1-10 分定义你的风格维度。AI 比"写得自然一点"这种话更容易理解数字。**
|
||||
|
||||
| Dimension | Score | 你的说明 |
|
||||
|-----------|-------|---------|
|
||||
| **formal_casual** | **?/10** | 偏正式还是偏随意? |
|
||||
| **technical_accessible** | **?/10** | 技术深度? |
|
||||
| **serious_playful** | **?/10** | 严肃还是活泼? |
|
||||
| **concise_elaborate** | **?/10** | 简洁还是详细? |
|
||||
| **reserved_expressive** | **?/10** | 保守还是直接? |
|
||||
|
||||
> 💡 **不知道填什么?** 先跑几次 AI 写作 → 你改 → 自动学习会帮你填。
|
||||
|
||||
---
|
||||
|
||||
## 【1】角色与读者
|
||||
|
||||
**我是谁:**
|
||||
- (填你的身份,比如:独立开发者,新晋 AI 爱好者)
|
||||
|
||||
**读者是谁:**
|
||||
- (填你的读者,比如:对 AI 感兴趣的技术人)
|
||||
|
||||
**和读者的关系:**
|
||||
- (比如:同行交流,不是教学)
|
||||
|
||||
---
|
||||
|
||||
## 【2】写作规则
|
||||
|
||||
### 基础规则
|
||||
|
||||
- (填你的规则,比如:不用"深入探讨"、段落要短、要有具体数字)
|
||||
-
|
||||
-
|
||||
|
||||
### 禁止词
|
||||
|
||||
- (AI 爱用但你不喜欢的词,比如:值得注意的是、综上所述、本文将介绍)
|
||||
-
|
||||
-
|
||||
|
||||
### 句式偏好
|
||||
|
||||
- (你喜欢的句式,比如:结论前置、不要学术导语)
|
||||
-
|
||||
-
|
||||
|
||||
> 💡 **不需要一开始就写完。** 这些规则会通过你的修改自动积累。
|
||||
> 跑完 10 次写作→修改循环后,这里会长出几十条精准规则。
|
||||
|
||||
---
|
||||
|
||||
## 【3】格式规范
|
||||
|
||||
### 平台适配
|
||||
|
||||
| 平台 | 格式要求 |
|
||||
|------|---------|
|
||||
| X/Twitter | 不渲染 markdown,用纯文本 |
|
||||
| 小红书 | emoji 多、分段短 |
|
||||
| 博客 | 标准 markdown |
|
||||
| (你的平台) | (你的要求)|
|
||||
|
||||
---
|
||||
|
||||
## 🔄 自动学习(内置)
|
||||
|
||||
**这个 skill 会从你的修改中自动学习。不需要手动写规则。**
|
||||
|
||||
### 工作原理
|
||||
|
||||
```
|
||||
AI 用这个 skill 写初稿
|
||||
↓
|
||||
你改到满意
|
||||
↓
|
||||
脚本 diff 两版 → 提取你改了什么
|
||||
↓
|
||||
新规则自动写入这个 SKILL.md
|
||||
↓
|
||||
下次 AI 写出来就更像你
|
||||
```
|
||||
|
||||
### 只需要两个数据点
|
||||
|
||||
- **original**: AI 生成的第一版
|
||||
- **final**: 你最终确认的版本
|
||||
|
||||
中间改了几轮不管。在 Google Doc 里来回改了 10 次?无所谓,只比较首尾。
|
||||
|
||||
### Agent 操作指南
|
||||
|
||||
**写完内容后:**
|
||||
```bash
|
||||
python3 scripts/observe.py record-original <file> --account <账号> --content-type <类型>
|
||||
```
|
||||
|
||||
**人类确认最终版后:**
|
||||
```bash
|
||||
python3 scripts/observe.py record-final <file> --match <hash>
|
||||
```
|
||||
|
||||
**提取规则(手动或 cron 自动):**
|
||||
```bash
|
||||
python3 scripts/improve.py auto --skill .
|
||||
```
|
||||
|
||||
### 规则分级
|
||||
|
||||
| 级别 | 含义 | 处理方式 |
|
||||
|------|------|---------|
|
||||
| P0 | 高置信度(多次出现) | 自动应用 |
|
||||
| P1 | 中置信度 | 人工确认 |
|
||||
| P2 | 低置信度(仅 1 次) | 存档观察 |
|
||||
|
||||
### 安全
|
||||
|
||||
- 每次更新前自动备份 SKILL.md
|
||||
- `improve.py rollback` 一键回滚
|
||||
- auto 模式只应用 P0
|
||||
|
||||
---
|
||||
|
||||
## 📊 CLI 参考
|
||||
|
||||
### observe.py(零依赖纯 Python)
|
||||
|
||||
| 命令 | 功能 |
|
||||
|------|------|
|
||||
| `record-original <file>` | 记录 AI 原稿 |
|
||||
| `record-final <file> --match <hash>` | 记录最终版 |
|
||||
| `pending` | 查看待配对 |
|
||||
| `stats` | 统计 |
|
||||
|
||||
### improve.py(需要 LLM CLI)
|
||||
|
||||
| 命令 | 功能 |
|
||||
|------|------|
|
||||
| `extract [--days 7]` | 提取改进建议 |
|
||||
| `auto` | 提取 + 自动应用 P0 |
|
||||
| `show` | 查看提案 |
|
||||
| `apply <id>` | 应用提案 |
|
||||
| `rollback` | 回滚 |
|
||||
|
||||
支持的 LLM CLI: `claude`(Claude Code)/ `llm`(pip install llm)/ `IMPROVE_LLM_CMD` 环境变量
|
||||
|
||||
---
|
||||
|
||||
## 📂 数据存储
|
||||
|
||||
```
|
||||
~/clawd/memory/ # OpenClaw
|
||||
~/.claude/memory/ # Claude Code
|
||||
├── skill-runs/<skill-name>/
|
||||
│ └── YYYY-MM-DD.jsonl # 每日观察日志
|
||||
├── skill-proposals/<skill-name>/
|
||||
│ └── YYYYMMDD-HHMMSS.md # 改进提案
|
||||
└── skill-backups/<skill-name>/
|
||||
└── SKILL-YYYYMMDD-HHMMSS.md # 自动备份
|
||||
```
|
||||
|
||||
自动检测环境,不需要手动配置路径。
|
||||
|
||||
---
|
||||
|
||||
## 🚀 30 天预期
|
||||
|
||||
| 时间 | 预期效果 |
|
||||
|------|---------|
|
||||
| 第 1 周 | 积累 3-5 次修改,生成第一批规则 |
|
||||
| 第 2 周 | 10+ 条规则,AI 输出明显更像你 |
|
||||
| 第 1 月 | 30+ 条规则,风格维度自动校准 |
|
||||
| 持续 | 规则库稳定增长,新 pattern 自动捕捉 |
|
||||
@@ -0,0 +1,426 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Self-Improving Skill — Improver
|
||||
|
||||
从人类修改中提取规则,更新目标 SKILL.md。
|
||||
|
||||
用法:
|
||||
python3 improve.py extract [--days 7] [--date 2026-03-17]
|
||||
python3 improve.py auto # 提取 + 自动应用 P0(cron 用)
|
||||
python3 improve.py show # 查看所有提案
|
||||
python3 improve.py apply <proposal_id> # 应用指定提案
|
||||
python3 improve.py rollback # 回滚上次应用
|
||||
|
||||
环境变量:
|
||||
SKILL_LOG_DIR — 日志目录
|
||||
SKILL_TARGET_PATH — 目标 SKILL.md 路径
|
||||
SKILL_PROPOSAL_DIR — 提案目录
|
||||
SKILL_BACKUP_DIR — 备份目录
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import argparse
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# 默认路径 — 自动检测 OpenClaw (~/.openclaw) 或 Claude Code (~/.claude)
|
||||
def _detect_base():
|
||||
"""检测数据存储基目录"""
|
||||
# 优先级: 环境变量 > ~/clawd > ~/.openclaw > ~/.claude > ~/.self-improving
|
||||
if os.environ.get("SKILL_BASE_DIR"):
|
||||
return Path(os.environ["SKILL_BASE_DIR"])
|
||||
candidates = [
|
||||
Path.home() / "clawd" / "memory",
|
||||
Path.home() / ".openclaw" / "memory",
|
||||
Path.home() / ".claude" / "memory",
|
||||
]
|
||||
for c in candidates:
|
||||
if c.parent.exists():
|
||||
return c
|
||||
return Path.home() / ".self-improving" / "memory"
|
||||
|
||||
_BASE = _detect_base()
|
||||
DEFAULT_LOG_DIR = _BASE / "skill-runs" / "default"
|
||||
DEFAULT_PROPOSAL_DIR = _BASE / "skill-proposals" / "default"
|
||||
DEFAULT_BACKUP_DIR = _BASE / "skill-backups" / "default"
|
||||
|
||||
|
||||
def get_paths(args=None):
|
||||
"""解析所有路径配置"""
|
||||
skill_name = "default"
|
||||
if args and hasattr(args, 'skill') and args.skill:
|
||||
skill_name = Path(args.skill).name
|
||||
|
||||
base = Path.home() / "clawd" / "memory"
|
||||
|
||||
log_dir = Path(os.environ.get("SKILL_LOG_DIR",
|
||||
getattr(args, 'log_dir', None) or
|
||||
str(base / "skill-runs" / skill_name)))
|
||||
|
||||
proposal_dir = Path(os.environ.get("SKILL_PROPOSAL_DIR",
|
||||
getattr(args, 'proposal_dir', None) or
|
||||
str(base / "skill-proposals" / skill_name)))
|
||||
|
||||
backup_dir = Path(os.environ.get("SKILL_BACKUP_DIR",
|
||||
getattr(args, 'backup_dir', None) or
|
||||
str(base / "skill-backups" / skill_name)))
|
||||
|
||||
# 目标 SKILL.md
|
||||
if os.environ.get("SKILL_TARGET_PATH"):
|
||||
target = Path(os.environ["SKILL_TARGET_PATH"])
|
||||
elif args and hasattr(args, 'target') and args.target:
|
||||
target = Path(args.target)
|
||||
elif args and hasattr(args, 'skill') and args.skill:
|
||||
target = Path(args.skill) / "SKILL.md"
|
||||
else:
|
||||
target = None
|
||||
|
||||
for d in (log_dir, proposal_dir, backup_dir):
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
return log_dir, proposal_dir, backup_dir, target
|
||||
|
||||
|
||||
def read_log_entries(log_file):
|
||||
if not log_file.exists():
|
||||
return []
|
||||
entries = []
|
||||
with log_file.open("r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
try:
|
||||
entries.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return entries
|
||||
|
||||
|
||||
def collect_edits(log_dir, days=1, date_str=None):
|
||||
"""收集有实际修改的 final/edited 记录"""
|
||||
edits = []
|
||||
if date_str:
|
||||
log_file = log_dir / f"{date_str}.jsonl"
|
||||
entries = read_log_entries(log_file)
|
||||
edits.extend([e for e in entries
|
||||
if e["type"] in ("final", "edited") and not e.get("no_change")])
|
||||
else:
|
||||
for i in range(days):
|
||||
date = (datetime.now() - timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
log_file = log_dir / f"{date}.jsonl"
|
||||
entries = read_log_entries(log_file)
|
||||
edits.extend([e for e in entries
|
||||
if e["type"] in ("final", "edited") and not e.get("no_change")])
|
||||
return edits
|
||||
|
||||
|
||||
def call_llm(prompt, timeout=180):
|
||||
"""调用 LLM — 自动检测可用的 CLI (claude / openclaw / llm)"""
|
||||
# 优先级: claude CLI → openclaw exec → generic llm CLI
|
||||
candidates = [
|
||||
["claude", "--print", "--model", "sonnet"], # Claude Code
|
||||
["claude", "--print"], # Claude Code (default model)
|
||||
["llm", "-m", "claude-sonnet"], # simon willison's llm CLI
|
||||
["llm"], # llm CLI default
|
||||
]
|
||||
for cmd in candidates:
|
||||
try:
|
||||
result = subprocess.run(cmd, input=prompt, capture_output=True,
|
||||
text=True, timeout=timeout)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return result.stdout.strip()
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
continue
|
||||
|
||||
# fallback: 如果 IMPROVE_LLM_CMD 环境变量设置了,用它
|
||||
custom_cmd = os.environ.get("IMPROVE_LLM_CMD")
|
||||
if custom_cmd:
|
||||
try:
|
||||
result = subprocess.run(custom_cmd.split(), input=prompt,
|
||||
capture_output=True, text=True, timeout=timeout)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return result.stdout.strip()
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
print("❌ LLM 调用失败。支持的方式:")
|
||||
print(" - 安装 Claude Code CLI (claude --print)")
|
||||
print(" - 安装 llm CLI (pip install llm)")
|
||||
print(" - 设置 IMPROVE_LLM_CMD 环境变量")
|
||||
return None
|
||||
|
||||
|
||||
def extract_improvements(args):
|
||||
log_dir, proposal_dir, _, target = get_paths(args)
|
||||
days = getattr(args, 'days', 1) or 1
|
||||
date_str = getattr(args, 'date', None)
|
||||
|
||||
edits = collect_edits(log_dir, days=days, date_str=date_str)
|
||||
if not edits:
|
||||
print("⚠️ 没有修改记录")
|
||||
return None
|
||||
|
||||
print(f"📊 找到 {len(edits)} 次修改,正在分析...")
|
||||
|
||||
# 读当前 SKILL.md
|
||||
current_skill = ""
|
||||
if target and target.exists():
|
||||
current_skill = target.read_text()
|
||||
|
||||
# 构建对比数据
|
||||
edit_summaries = []
|
||||
for i, edit in enumerate(edits):
|
||||
orig = edit.get("original_content", "")[:3000]
|
||||
final = edit.get("final_content", edit.get("edited_content", ""))[:3000]
|
||||
ctx = edit.get("context", {})
|
||||
edit_summaries.append({
|
||||
"index": i + 1,
|
||||
"account": ctx.get("account", "unknown"),
|
||||
"content_type": ctx.get("content_type", "unknown"),
|
||||
"original": orig,
|
||||
"final": final,
|
||||
})
|
||||
|
||||
proposal_id = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
|
||||
prompt = f"""你是 writing style skill 的改进助手。
|
||||
|
||||
分析人类对 AI 生成文章的修改,提取可以加入 SKILL.md 的新规则。
|
||||
|
||||
## 当前 SKILL.md(最后 3000 字,避免重复)
|
||||
{current_skill[-3000:]}
|
||||
|
||||
## 修改记录(original vs final)
|
||||
{json.dumps(edit_summaries, ensure_ascii=False, indent=2)}
|
||||
|
||||
## 要求
|
||||
1. 对比 original 和 final,找出系统性修改
|
||||
2. 只提取有 pattern 的修改(至少 2 次,或单次但改动幅度大且明确)
|
||||
3. 不要提取已在 SKILL.md 中的规则
|
||||
4. 每条规则必须可执行
|
||||
|
||||
## 输出格式
|
||||
|
||||
---
|
||||
id: {proposal_id}
|
||||
date: {datetime.now().isoformat()}
|
||||
source: {len(edits)} edits
|
||||
status: pending
|
||||
---
|
||||
|
||||
# Improvement Proposal
|
||||
|
||||
## 提取的改进建议
|
||||
|
||||
### 1. 新禁止词
|
||||
- **`词`** → 替代: "YYY" | 理由: ... | 优先级: **P0/P1/P2**
|
||||
|
||||
### 2. 新风格规则
|
||||
- 规则描述 | 理由: ... | 优先级: **P0/P1/P2**
|
||||
|
||||
### 3. 反模式
|
||||
- 描述 | 理由: ... | 优先级: **P0/P1/P2**
|
||||
|
||||
P0=高置信度(多次), P1=中置信度, P2=低置信度(仅1次)
|
||||
"""
|
||||
|
||||
suggestions = call_llm(prompt)
|
||||
if not suggestions:
|
||||
return None
|
||||
|
||||
proposal_file = proposal_dir / f"{proposal_id}.md"
|
||||
proposal_file.write_text(suggestions)
|
||||
|
||||
print(f"✅ 改进建议已保存: {proposal_file}")
|
||||
print(f"\n{suggestions[:2000]}")
|
||||
if len(suggestions) > 2000:
|
||||
print(f"\n... (完整内容见文件)")
|
||||
return proposal_id
|
||||
|
||||
|
||||
def show_proposals(args):
|
||||
_, proposal_dir, _, _ = get_paths(args)
|
||||
proposals = list(proposal_dir.glob("*.md"))
|
||||
|
||||
if not proposals:
|
||||
print("⚠️ 没有提案")
|
||||
return
|
||||
|
||||
print(f"\n📋 共 {len(proposals)} 个提案:\n")
|
||||
for p in sorted(proposals, reverse=True):
|
||||
content = p.read_text()
|
||||
status = "unknown"
|
||||
for line in content.split("\n")[:10]:
|
||||
if line.startswith("status:"):
|
||||
status = line.split(":", 1)[1].strip()
|
||||
icon = {"pending": "⏳", "applied": "✅", "rejected": "❌"}.get(
|
||||
status.split("(")[0].strip(), "❓")
|
||||
print(f" {icon} {p.stem} — {status}")
|
||||
|
||||
|
||||
def backup_skill(target, backup_dir):
|
||||
if not target or not target.exists():
|
||||
return None
|
||||
name = f"SKILL-{datetime.now().strftime('%Y%m%d-%H%M%S')}.md"
|
||||
backup_path = backup_dir / name
|
||||
shutil.copy2(target, backup_path)
|
||||
print(f"📦 备份: {backup_path}")
|
||||
return backup_path
|
||||
|
||||
|
||||
def apply_proposal(args):
|
||||
_, proposal_dir, backup_dir, target = get_paths(args)
|
||||
proposal_id = args.proposal_id
|
||||
proposal_file = proposal_dir / f"{proposal_id}.md"
|
||||
|
||||
if not proposal_file.exists():
|
||||
print(f"❌ 提案不存在: {proposal_id}")
|
||||
return
|
||||
|
||||
if not target or not target.exists():
|
||||
print(f"❌ 目标 SKILL.md 不存在(用 --skill 或 --target 指定)")
|
||||
return
|
||||
|
||||
proposal_content = proposal_file.read_text()
|
||||
current_skill = target.read_text()
|
||||
|
||||
auto_mode = getattr(args, 'auto', False)
|
||||
filter_level = "P0" if auto_mode else "P0 和 P1"
|
||||
|
||||
backup_skill(target, backup_dir)
|
||||
|
||||
prompt = f"""把改进提案中的 **{filter_level}** 规则合并到 SKILL.md。
|
||||
|
||||
规则:
|
||||
1. 新禁止词 → 加到禁止词 section
|
||||
2. 新风格规则 → 加到对应 section
|
||||
3. 不删除已有规则,不改文件结构
|
||||
4. version +0.1
|
||||
|
||||
## 提案
|
||||
{proposal_content}
|
||||
|
||||
## 当前 SKILL.md
|
||||
{current_skill}
|
||||
|
||||
输出完整更新后的 SKILL.md。不加代码块包裹。"""
|
||||
|
||||
updated = call_llm(prompt, timeout=300)
|
||||
if not updated:
|
||||
print("❌ 合并失败")
|
||||
return
|
||||
|
||||
target.write_text(updated)
|
||||
|
||||
new_content = proposal_content.replace(
|
||||
"status: pending",
|
||||
f"status: applied ({datetime.now().strftime('%Y-%m-%d')})")
|
||||
proposal_file.write_text(new_content)
|
||||
|
||||
print(f"✅ 已应用提案 {proposal_id}")
|
||||
print(f"💡 回滚: python3 improve.py rollback")
|
||||
|
||||
|
||||
def auto_improve(args):
|
||||
log_dir, _, _, _ = get_paths(args)
|
||||
edits = collect_edits(log_dir, days=7)
|
||||
|
||||
if not edits:
|
||||
print("⚠️ 最近 7 天没有修改记录,跳过")
|
||||
return
|
||||
|
||||
print(f"🤖 自动模式: {len(edits)} 次修改")
|
||||
|
||||
args.days = 7
|
||||
args.date = None
|
||||
proposal_id = extract_improvements(args)
|
||||
|
||||
if not proposal_id:
|
||||
return
|
||||
|
||||
_, proposal_dir, _, _ = get_paths(args)
|
||||
content = (proposal_dir / f"{proposal_id}.md").read_text()
|
||||
|
||||
if "P0" not in content:
|
||||
print("ℹ️ 没有 P0 规则,跳过自动应用")
|
||||
return
|
||||
|
||||
print("\n🔄 自动应用 P0 规则...")
|
||||
apply_args = argparse.Namespace(**vars(args))
|
||||
apply_args.proposal_id = proposal_id
|
||||
apply_args.auto = True
|
||||
apply_proposal(apply_args)
|
||||
|
||||
|
||||
def rollback(args):
|
||||
_, _, backup_dir, target = get_paths(args)
|
||||
|
||||
if not target:
|
||||
print("❌ 未指定目标 SKILL.md")
|
||||
return
|
||||
|
||||
backups = sorted(backup_dir.glob("SKILL-*.md"), reverse=True)
|
||||
if not backups:
|
||||
print("❌ 没有备份")
|
||||
return
|
||||
|
||||
latest = backups[0]
|
||||
# 保存当前版本
|
||||
if target.exists():
|
||||
emergency = backup_dir / f"SKILL-pre-rollback-{datetime.now().strftime('%Y%m%d-%H%M%S')}.md"
|
||||
shutil.copy2(target, emergency)
|
||||
|
||||
shutil.copy2(latest, target)
|
||||
print(f"✅ 已回滚到: {latest.name}")
|
||||
|
||||
|
||||
def add_common_args(parser):
|
||||
parser.add_argument("--skill", help="目标 skill 目录")
|
||||
parser.add_argument("--target", help="目标 SKILL.md 路径")
|
||||
parser.add_argument("--log-dir", help="日志目录")
|
||||
parser.add_argument("--proposal-dir", help="提案目录")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Self-Improving Skill — Improver")
|
||||
subparsers = parser.add_subparsers(dest="action")
|
||||
|
||||
p_ext = subparsers.add_parser("extract", help="提取改进建议")
|
||||
p_ext.add_argument("--date", help="指定日期")
|
||||
p_ext.add_argument("--days", type=int, default=1)
|
||||
add_common_args(p_ext)
|
||||
|
||||
p_show = subparsers.add_parser("show", help="查看提案")
|
||||
add_common_args(p_show)
|
||||
|
||||
p_apply = subparsers.add_parser("apply", help="应用提案")
|
||||
p_apply.add_argument("proposal_id")
|
||||
add_common_args(p_apply)
|
||||
|
||||
p_auto = subparsers.add_parser("auto", help="自动提取+应用P0")
|
||||
add_common_args(p_auto)
|
||||
|
||||
p_rb = subparsers.add_parser("rollback", help="回滚")
|
||||
add_common_args(p_rb)
|
||||
|
||||
args = parser.parse_args()
|
||||
if not args.action:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
actions = {
|
||||
"extract": extract_improvements,
|
||||
"show": show_proposals,
|
||||
"apply": apply_proposal,
|
||||
"auto": auto_improve,
|
||||
"rollback": rollback,
|
||||
}
|
||||
actions[args.action](args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,309 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Self-Improving Skill — Observation Layer
|
||||
|
||||
通用的 original → final 记录器。适用于任何 "AI 生成 → 人类修改" 的循环。
|
||||
|
||||
用法:
|
||||
python3 observe.py record-original article.md [--text "..."] [--account X] [--content-type article]
|
||||
python3 observe.py record-final article.md [--match <hash>] [--text "..."]
|
||||
python3 observe.py pending
|
||||
python3 observe.py stats
|
||||
|
||||
环境变量:
|
||||
SKILL_LOG_DIR — 日志目录(默认 ~/clawd/memory/skill-runs/default/)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
import hashlib
|
||||
|
||||
# 日志目录 — 自动检测 OpenClaw / Claude Code / standalone
|
||||
def _detect_base():
|
||||
if os.environ.get("SKILL_BASE_DIR"):
|
||||
return Path(os.environ["SKILL_BASE_DIR"])
|
||||
candidates = [
|
||||
Path.home() / "clawd" / "memory",
|
||||
Path.home() / ".openclaw" / "memory",
|
||||
Path.home() / ".claude" / "memory",
|
||||
]
|
||||
for c in candidates:
|
||||
if c.parent.exists():
|
||||
return c
|
||||
return Path.home() / ".self-improving" / "memory"
|
||||
|
||||
DEFAULT_LOG_DIR = _detect_base() / "skill-runs" / "default"
|
||||
|
||||
|
||||
def get_log_dir(args=None):
|
||||
"""按优先级决定日志目录:--log-dir > SKILL_LOG_DIR > default"""
|
||||
if args and hasattr(args, 'log_dir') and args.log_dir:
|
||||
d = Path(args.log_dir)
|
||||
elif os.environ.get("SKILL_LOG_DIR"):
|
||||
d = Path(os.environ["SKILL_LOG_DIR"])
|
||||
elif args and hasattr(args, 'skill') and args.skill:
|
||||
skill_name = Path(args.skill).name
|
||||
d = Path.home() / "clawd" / "memory" / "skill-runs" / skill_name
|
||||
else:
|
||||
d = DEFAULT_LOG_DIR
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
return d
|
||||
|
||||
|
||||
def get_log_file(log_dir, date_str=None):
|
||||
if date_str is None:
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
return log_dir / f"{date_str}.jsonl"
|
||||
|
||||
|
||||
def compute_hash(content):
|
||||
return hashlib.md5(content.encode()).hexdigest()[:8]
|
||||
|
||||
|
||||
def get_content(args):
|
||||
if hasattr(args, 'text') and args.text:
|
||||
return args.text
|
||||
if hasattr(args, 'stdin') and args.stdin:
|
||||
return sys.stdin.read()
|
||||
if hasattr(args, 'file') and args.file:
|
||||
p = Path(args.file)
|
||||
if not p.exists():
|
||||
print(f"❌ 文件不存在: {args.file}")
|
||||
sys.exit(1)
|
||||
return p.read_text()
|
||||
return None
|
||||
|
||||
|
||||
def read_log_entries(log_file):
|
||||
if not log_file.exists():
|
||||
return []
|
||||
entries = []
|
||||
with log_file.open("r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
try:
|
||||
entries.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return entries
|
||||
|
||||
|
||||
def find_unmatched(log_dir, days=14):
|
||||
"""找到有 original 但没有 final 的记录(兼容旧 edited/no-change type)"""
|
||||
all_originals = {}
|
||||
all_matched = set()
|
||||
|
||||
for i in range(days):
|
||||
date = (datetime.now() - timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
entries = read_log_entries(get_log_file(log_dir, date))
|
||||
for e in entries:
|
||||
if e["type"] == "original":
|
||||
e["_date"] = date
|
||||
all_originals[e["content_hash"]] = e
|
||||
elif e["type"] in ("final", "edited", "no-change"):
|
||||
all_matched.add(e["content_hash"])
|
||||
|
||||
return {h: o for h, o in all_originals.items() if h not in all_matched}
|
||||
|
||||
|
||||
def record_original(args):
|
||||
content = get_content(args)
|
||||
if not content:
|
||||
print("❌ 需要提供内容(文件路径、--text 或 --stdin)")
|
||||
sys.exit(1)
|
||||
|
||||
log_dir = get_log_dir(args)
|
||||
content_hash = compute_hash(content)
|
||||
|
||||
context = {}
|
||||
if hasattr(args, 'account') and args.account:
|
||||
context["account"] = args.account
|
||||
if hasattr(args, 'content_type') and args.content_type:
|
||||
context["content_type"] = args.content_type
|
||||
|
||||
log_entry = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"type": "original",
|
||||
"content_hash": content_hash,
|
||||
"file": str(args.file) if hasattr(args, 'file') and args.file else None,
|
||||
"content": content,
|
||||
"context": context,
|
||||
"char_count": len(content),
|
||||
}
|
||||
|
||||
log_file = get_log_file(log_dir)
|
||||
with log_file.open("a") as f:
|
||||
f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
|
||||
|
||||
print(f"✅ 记录原稿: {content_hash}")
|
||||
print(f"📝 字数: {len(content)}")
|
||||
print(f"📁 日志: {log_file}")
|
||||
return content_hash
|
||||
|
||||
|
||||
def record_final(args):
|
||||
content = get_content(args)
|
||||
if not content:
|
||||
print("❌ 需要提供最终版内容")
|
||||
sys.exit(1)
|
||||
|
||||
log_dir = get_log_dir(args)
|
||||
target_hash = getattr(args, 'match', None)
|
||||
|
||||
if target_hash:
|
||||
# 指定 hash,跨天查找
|
||||
original = None
|
||||
for i in range(14):
|
||||
date = (datetime.now() - timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
entries = read_log_entries(get_log_file(log_dir, date))
|
||||
for e in entries:
|
||||
if e["type"] == "original" and e["content_hash"] == target_hash:
|
||||
original = e
|
||||
break
|
||||
if original:
|
||||
break
|
||||
if not original:
|
||||
print(f"❌ 找不到 hash {target_hash} 的原稿")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# 自动匹配最近的未配对 original
|
||||
unmatched = find_unmatched(log_dir, 14)
|
||||
if not unmatched:
|
||||
print("❌ 没有待配对的原稿")
|
||||
sys.exit(1)
|
||||
target_hash = list(unmatched.keys())[-1]
|
||||
original = unmatched[target_hash]
|
||||
|
||||
is_same = content.strip() == original["content"].strip()
|
||||
|
||||
log_entry = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"type": "final",
|
||||
"content_hash": target_hash,
|
||||
"original_content": original["content"],
|
||||
"final_content": content,
|
||||
"original_char_count": len(original["content"]),
|
||||
"final_char_count": len(content),
|
||||
"context": original.get("context", {}),
|
||||
"no_change": is_same,
|
||||
}
|
||||
|
||||
log_file = get_log_file(log_dir)
|
||||
with log_file.open("a") as f:
|
||||
f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
|
||||
|
||||
if is_same:
|
||||
print(f"✅ 记录最终版: {target_hash} (无修改 — 正反馈)")
|
||||
else:
|
||||
diff_pct = ((len(content) - len(original["content"])) / max(len(original["content"]), 1)) * 100
|
||||
print(f"✅ 记录最终版: {target_hash}")
|
||||
print(f"📝 原稿: {len(original['content'])} 字 → 最终: {len(content)} 字 ({diff_pct:+.1f}%)")
|
||||
print(f"📁 日志: {log_file}")
|
||||
|
||||
|
||||
def show_pending(args):
|
||||
log_dir = get_log_dir(args)
|
||||
unmatched = find_unmatched(log_dir, 14)
|
||||
|
||||
if not unmatched:
|
||||
print("✅ 全部已配对")
|
||||
return
|
||||
|
||||
print(f"\n⏳ {len(unmatched)} 条待配对:\n")
|
||||
for h, entry in unmatched.items():
|
||||
preview = entry["content"][:80].replace("\n", " ")
|
||||
ctx = entry.get("context", {})
|
||||
account = ctx.get("account", "?")
|
||||
date = entry.get("_date", "?")
|
||||
print(f" 📄 {h} | {date} | {account}")
|
||||
print(f" {preview}...")
|
||||
print()
|
||||
|
||||
|
||||
def show_stats(args):
|
||||
log_dir = get_log_dir(args)
|
||||
all_files = sorted(log_dir.glob("*.jsonl"))
|
||||
|
||||
total_originals = 0
|
||||
total_finals = 0
|
||||
total_no_change = 0
|
||||
total_changed = 0
|
||||
|
||||
for log_file in all_files:
|
||||
entries = read_log_entries(log_file)
|
||||
total_originals += sum(1 for e in entries if e["type"] == "original")
|
||||
finals = [e for e in entries if e["type"] in ("final", "edited")]
|
||||
total_finals += len(finals)
|
||||
total_no_change += sum(1 for e in finals if e.get("no_change"))
|
||||
total_changed += sum(1 for e in finals if not e.get("no_change"))
|
||||
|
||||
pending = find_unmatched(log_dir, 14)
|
||||
|
||||
print(f"\n📊 Self-Improving Skill 观察统计")
|
||||
print(f"{'='*40}")
|
||||
print(f"📁 日志天数: {len(all_files)}")
|
||||
print(f"📝 原稿: {total_originals}")
|
||||
print(f"✅ 已配对: {total_finals}")
|
||||
print(f" ├ 有修改: {total_changed}")
|
||||
print(f" └ 无修改: {total_no_change}")
|
||||
print(f"⏳ 待配对: {len(pending)}")
|
||||
if total_finals > 0:
|
||||
print(f"📈 修改率: {total_changed}/{total_finals} = {total_changed/total_finals*100:.0f}%")
|
||||
|
||||
|
||||
def add_common_args(parser):
|
||||
"""添加通用参数"""
|
||||
parser.add_argument("--skill", help="目标 skill 目录名(用于定位日志目录)")
|
||||
parser.add_argument("--log-dir", help="自定义日志目录")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Self-Improving Skill — Observation Layer")
|
||||
subparsers = parser.add_subparsers(dest="action")
|
||||
|
||||
# record-original
|
||||
p_orig = subparsers.add_parser("record-original", help="记录 AI 原稿")
|
||||
p_orig.add_argument("file", nargs="?", help="文件路径")
|
||||
p_orig.add_argument("--text", help="直接传入文本")
|
||||
p_orig.add_argument("--stdin", action="store_true", help="从 stdin 读取")
|
||||
p_orig.add_argument("--account", help="发布账号")
|
||||
p_orig.add_argument("--content-type", help="内容类型")
|
||||
add_common_args(p_orig)
|
||||
|
||||
# record-final
|
||||
p_final = subparsers.add_parser("record-final", help="记录最终版")
|
||||
p_final.add_argument("file", nargs="?", help="文件路径")
|
||||
p_final.add_argument("--text", help="直接传入文本")
|
||||
p_final.add_argument("--stdin", action="store_true", help="从 stdin 读取")
|
||||
p_final.add_argument("--match", help="匹配的原稿 hash")
|
||||
add_common_args(p_final)
|
||||
|
||||
# pending
|
||||
p_pending = subparsers.add_parser("pending", help="查看待配对原稿")
|
||||
add_common_args(p_pending)
|
||||
|
||||
# stats
|
||||
p_stats = subparsers.add_parser("stats", help="总体统计")
|
||||
add_common_args(p_stats)
|
||||
|
||||
args = parser.parse_args()
|
||||
if not args.action:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
actions = {
|
||||
"record-original": record_original,
|
||||
"record-final": record_final,
|
||||
"pending": show_pending,
|
||||
"stats": show_stats,
|
||||
}
|
||||
actions[args.action](args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,249 @@
|
||||
---
|
||||
name: pptx-generator
|
||||
description: "Generate, edit, and read PowerPoint presentations. Create from scratch with PptxGenJS (cover, TOC, content, section divider, summary slides), edit existing PPTX via XML workflows, or extract text with markitdown. Triggers: PPT, PPTX, PowerPoint, presentation, slide, deck, slides."
|
||||
license: MIT
|
||||
metadata:
|
||||
version: "1.0"
|
||||
category: productivity
|
||||
sources:
|
||||
- https://gitbrent.github.io/PptxGenJS/
|
||||
- https://github.com/microsoft/markitdown
|
||||
---
|
||||
|
||||
# PPTX Generator & Editor
|
||||
|
||||
## Overview
|
||||
|
||||
This skill handles all PowerPoint tasks: reading/analyzing existing presentations, editing template-based decks via XML manipulation, and creating presentations from scratch using PptxGenJS. It includes a complete design system (color palettes, fonts, style recipes) and detailed guidance for every slide type.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Approach |
|
||||
|------|----------|
|
||||
| Read/analyze content | `python -m markitdown presentation.pptx` |
|
||||
| Edit or create from template | See [Editing Presentations](references/editing.md) |
|
||||
| Create from scratch | See [Creating from Scratch](#creating-from-scratch-workflow) below |
|
||||
|
||||
| Item | Value |
|
||||
|------|-------|
|
||||
| **Dimensions** | 10" x 5.625" (LAYOUT_16x9) |
|
||||
| **Colors** | 6-char hex without # (e.g., `"FF0000"`) |
|
||||
| **English font** | Arial (default), or approved alternatives |
|
||||
| **Chinese font** | Microsoft YaHei |
|
||||
| **Page badge position** | x: 9.3", y: 5.1" |
|
||||
| **Theme keys** | `primary`, `secondary`, `accent`, `light`, `bg` |
|
||||
| **Shapes** | RECTANGLE, OVAL, LINE, ROUNDED_RECTANGLE |
|
||||
| **Charts** | BAR, LINE, PIE, DOUGHNUT, SCATTER, BUBBLE, RADAR |
|
||||
|
||||
## Reference Files
|
||||
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| [slide-types.md](references/slide-types.md) | 5 slide page types (Cover, TOC, Section Divider, Content, Summary) + additional layout patterns |
|
||||
| [design-system.md](references/design-system.md) | Color palettes, font reference, style recipes (Sharp/Soft/Rounded/Pill), typography & spacing |
|
||||
| [editing.md](references/editing.md) | Template-based editing workflow, XML manipulation, formatting rules, common pitfalls |
|
||||
| [pitfalls.md](references/pitfalls.md) | QA process, common mistakes, critical PptxGenJS pitfalls |
|
||||
| [pptxgenjs.md](references/pptxgenjs.md) | Complete PptxGenJS API reference |
|
||||
|
||||
---
|
||||
|
||||
## Reading Content
|
||||
|
||||
```bash
|
||||
# Text extraction
|
||||
python -m markitdown presentation.pptx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Creating from Scratch — Workflow
|
||||
|
||||
**Use when no template or reference presentation is available.**
|
||||
|
||||
### Step 1: Research & Requirements
|
||||
|
||||
Search to understand user requirements — topic, audience, purpose, tone, content depth.
|
||||
|
||||
### Step 2: Select Color Palette & Fonts
|
||||
|
||||
Use the [Color Palette Reference](references/design-system.md#color-palette-reference) to select a palette matching the topic and audience. Use the [Font Reference](references/design-system.md#font-reference) to choose a font pairing.
|
||||
|
||||
### Step 3: Select Design Style
|
||||
|
||||
Use the [Style Recipes](references/design-system.md#style-recipes) to choose a visual style (Sharp, Soft, Rounded, or Pill) matching the presentation tone.
|
||||
|
||||
### Step 4: Plan Slide Outline
|
||||
|
||||
Classify **every slide** as exactly one of the [5 page types](references/slide-types.md). Plan the content and layout for each slide. Ensure visual variety — do NOT repeat the same layout across slides.
|
||||
|
||||
### Step 5: Generate Slide JS Files
|
||||
|
||||
Create one JS file per slide in `slides/` directory. Each file must export a synchronous `createSlide(pres, theme)` function. Follow the [Slide Output Format](#slide-output-format) and the type-specific guidance in [slide-types.md](references/slide-types.md). Generate up to 5 slides concurrently using subagents if available.
|
||||
|
||||
**Tell each subagent:**
|
||||
1. File naming: `slides/slide-01.js`, `slides/slide-02.js`, etc.
|
||||
2. Images go in: `slides/imgs/`
|
||||
3. Final PPTX goes in: `slides/output/`
|
||||
4. Dimensions: 10" x 5.625" (LAYOUT_16x9)
|
||||
5. Fonts: Chinese = Microsoft YaHei, English = Arial (or approved alternative)
|
||||
6. Colors: 6-char hex without # (e.g. `"FF0000"`)
|
||||
7. Must use the theme object contract (see [Theme Object Contract](#theme-object-contract))
|
||||
8. Must follow the [PptxGenJS API reference](references/pptxgenjs.md)
|
||||
|
||||
### Step 6: Compile into Final PPTX
|
||||
|
||||
Create `slides/compile.js` to combine all slide modules:
|
||||
|
||||
```javascript
|
||||
// slides/compile.js
|
||||
const pptxgen = require('pptxgenjs');
|
||||
const pres = new pptxgen();
|
||||
pres.layout = 'LAYOUT_16x9';
|
||||
|
||||
const theme = {
|
||||
primary: "22223b", // dark color for backgrounds/text
|
||||
secondary: "4a4e69", // secondary accent
|
||||
accent: "9a8c98", // highlight color
|
||||
light: "c9ada7", // light accent
|
||||
bg: "f2e9e4" // background color
|
||||
};
|
||||
|
||||
for (let i = 1; i <= 12; i++) { // adjust count as needed
|
||||
const num = String(i).padStart(2, '0');
|
||||
const slideModule = require(`./slide-${num}.js`);
|
||||
slideModule.createSlide(pres, theme);
|
||||
}
|
||||
|
||||
pres.writeFile({ fileName: './output/presentation.pptx' });
|
||||
```
|
||||
|
||||
Run with: `cd slides && node compile.js`
|
||||
|
||||
### Step 7: QA (Required)
|
||||
|
||||
See [QA Process](references/pitfalls.md#qa-process).
|
||||
|
||||
### Output Structure
|
||||
|
||||
```
|
||||
slides/
|
||||
├── slide-01.js # Slide modules
|
||||
├── slide-02.js
|
||||
├── ...
|
||||
├── imgs/ # Images used in slides
|
||||
└── output/ # Final artifacts
|
||||
└── presentation.pptx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Slide Output Format
|
||||
|
||||
Each slide is a **complete, runnable JS file**:
|
||||
|
||||
```javascript
|
||||
// slide-01.js
|
||||
const pptxgen = require("pptxgenjs");
|
||||
|
||||
const slideConfig = {
|
||||
type: 'cover',
|
||||
index: 1,
|
||||
title: 'Presentation Title'
|
||||
};
|
||||
|
||||
// MUST be synchronous (not async)
|
||||
function createSlide(pres, theme) {
|
||||
const slide = pres.addSlide();
|
||||
slide.background = { color: theme.bg };
|
||||
|
||||
slide.addText(slideConfig.title, {
|
||||
x: 0.5, y: 2, w: 9, h: 1.2,
|
||||
fontSize: 48, fontFace: "Arial",
|
||||
color: theme.primary, bold: true, align: "center"
|
||||
});
|
||||
|
||||
return slide;
|
||||
}
|
||||
|
||||
// Standalone preview - use slide-specific filename
|
||||
if (require.main === module) {
|
||||
const pres = new pptxgen();
|
||||
pres.layout = 'LAYOUT_16x9';
|
||||
const theme = {
|
||||
primary: "22223b",
|
||||
secondary: "4a4e69",
|
||||
accent: "9a8c98",
|
||||
light: "c9ada7",
|
||||
bg: "f2e9e4"
|
||||
};
|
||||
createSlide(pres, theme);
|
||||
pres.writeFile({ fileName: "slide-01-preview.pptx" });
|
||||
}
|
||||
|
||||
module.exports = { createSlide, slideConfig };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Theme Object Contract (MANDATORY)
|
||||
|
||||
The compile script passes a theme object with these **exact keys**:
|
||||
|
||||
| Key | Purpose | Example |
|
||||
|-----|---------|---------|
|
||||
| `theme.primary` | Darkest color, titles | `"22223b"` |
|
||||
| `theme.secondary` | Dark accent, body text | `"4a4e69"` |
|
||||
| `theme.accent` | Mid-tone accent | `"9a8c98"` |
|
||||
| `theme.light` | Light accent | `"c9ada7"` |
|
||||
| `theme.bg` | Background color | `"f2e9e4"` |
|
||||
|
||||
**NEVER use other key names** like `background`, `text`, `muted`, `darkest`, `lightest`.
|
||||
|
||||
---
|
||||
|
||||
## Page Number Badge (REQUIRED)
|
||||
|
||||
All slides **except Cover Page** MUST include a page number badge in the bottom-right corner.
|
||||
|
||||
- **Position**: x: 9.3", y: 5.1"
|
||||
- Show current number only (e.g. `3` or `03`), NOT "3/12"
|
||||
- Use palette colors, keep subtle
|
||||
|
||||
### Circle Badge (Default)
|
||||
|
||||
```javascript
|
||||
slide.addShape(pres.shapes.OVAL, {
|
||||
x: 9.3, y: 5.1, w: 0.4, h: 0.4,
|
||||
fill: { color: theme.accent }
|
||||
});
|
||||
slide.addText("3", {
|
||||
x: 9.3, y: 5.1, w: 0.4, h: 0.4,
|
||||
fontSize: 12, fontFace: "Arial",
|
||||
color: "FFFFFF", bold: true,
|
||||
align: "center", valign: "middle"
|
||||
});
|
||||
```
|
||||
|
||||
### Pill Badge
|
||||
|
||||
```javascript
|
||||
slide.addShape(pres.shapes.ROUNDED_RECTANGLE, {
|
||||
x: 9.1, y: 5.15, w: 0.6, h: 0.35,
|
||||
fill: { color: theme.accent },
|
||||
rectRadius: 0.15
|
||||
});
|
||||
slide.addText("03", {
|
||||
x: 9.1, y: 5.15, w: 0.6, h: 0.35,
|
||||
fontSize: 11, fontFace: "Arial",
|
||||
color: "FFFFFF", bold: true,
|
||||
align: "center", valign: "middle"
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `pip install "markitdown[pptx]"` — text extraction
|
||||
- `npm install -g pptxgenjs` — creating from scratch
|
||||
- `npm install -g react-icons react react-dom sharp` — icons (optional)
|
||||
@@ -0,0 +1,392 @@
|
||||
# Design System
|
||||
|
||||
## Color Palette Reference
|
||||
|
||||
| # | Name | Colors | Style | Use Cases | Tips |
|
||||
|---|------|--------|-------|-----------|------|
|
||||
| 1 | Modern & Wellness | `#006d77` `#83c5be` `#edf6f9` `#ffddd2` `#e29578` | Fresh, soothing | Healthcare, counseling, skincare, yoga/spa | Deep teal for titles, light pink for background |
|
||||
| 2 | Business & Authority | `#2b2d42` `#8d99ae` `#edf2f4` `#ef233c` `#d90429` | Formal, classic | Annual reports, financial analysis, corporate intro, government | Deep blue for professionalism, bright red to highlight data |
|
||||
| 3 | Nature & Outdoors | `#606c38` `#283618` `#fefae0` `#dda15e` `#bc6c25` | Grounded, earthy | Outdoor gear, environmental, agriculture, historical culture | Dark green base, cream text |
|
||||
| 4 | Vintage & Academic | `#780000` `#c1121f` `#fdf0d5` `#003049` `#669bbc` | Classic, scholarly | Academic lectures, history reviews, museums, heritage brands | Strong contrast between deep red and deep blue |
|
||||
| 5 | Soft & Creative | `#cdb4db` `#ffc8dd` `#ffafcc` `#bde0fe` `#a2d2ff` | Dreamy, candy-toned | Mother & baby, desserts, women's fashion, kindergarten | Use dark gray or black for text |
|
||||
| 6 | Bohemian | `#ccd5ae` `#e9edc9` `#fefae0` `#faedcd` `#d4a373` | Gentle, muted | Wedding planning, home decor, organic food, slow living | Cream background, green-brown accents |
|
||||
| 7 | Vibrant & Tech | `#8ecae6` `#219ebc` `#023047` `#ffb703` `#fb8500` | High energy, sporty | Sports events, gyms, startup pitches, youth education | Deep blue for stability, orange as focal accent |
|
||||
| 8 | Craft & Artisan | `#7f5539` `#a68a64` `#ede0d4` `#656d4a` `#414833` | Rustic, coffee-toned | Coffee shops, handicrafts, traditional culture, bakery | Suited for paper/leather textures |
|
||||
| 9 | Tech & Night | `#000814` `#001d3d` `#003566` `#ffc300` `#ffd60a` | Deep, luminous | Tech launches, astronomy, night economy, luxury automobiles | Must use dark mode |
|
||||
| 10 | Education & Charts | `#264653` `#2a9d8f` `#e9c46a` `#f4a261` `#e76f51` | Clear, logical | Statistical reports, education, market analysis, general business | Perfect chart color scheme |
|
||||
| 11 | Forest & Eco | `#dad7cd` `#a3b18a` `#588157` `#3a5a40` `#344e41` | Monochrome gradient, forest | Landscape design, ESG reports, environmental causes, botanical | Monochrome palette is safe and cohesive |
|
||||
| 12 | Elegant & Fashion | `#edafb8` `#f7e1d7` `#dedbd2` `#b0c4b1` `#4a5759` | Muted, Morandi tones | Haute couture, art galleries, beauty brands, magazine style | Negative space is key |
|
||||
| 13 | Art & Food | `#335c67` `#fff3b0` `#e09f3e` `#9e2a2b` `#540b0e` | Rich, vintage-poster | Food documentaries, art exhibitions, ethnic themes, vintage restaurants | Works well with large color blocks |
|
||||
| 14 | Luxury & Mysterious | `#22223b` `#4a4e69` `#9a8c98` `#c9ada7` `#f2e9e4` | Cool, purple-toned | Jewelry showcases, hotel management, high-end consulting, psychology | Purple evokes premium atmosphere |
|
||||
| 15 | Pure Tech Blue | `#03045e` `#0077b6` `#00b4d8` `#90e0ef` `#caf0f8` | Futuristic, clean | Cloud/AI, water/ocean, hospitals, clean energy | Deep ocean to sky gradient |
|
||||
| 16 | Coastal Coral | `#0081a7` `#00afb9` `#fdfcdc` `#fed9b7` `#f07167` | Refreshing, summery | Travel, summer events, beverage brands, ocean themes | Teal and coral as complementary focal colors |
|
||||
| 17 | Vibrant Orange Mint | `#ff9f1c` `#ffbf69` `#ffffff` `#cbf3f0` `#2ec4b6` | Bright, cheerful | Children's events, promotional posters, FMCG, social media | Orange grabs attention, mint feels fresh |
|
||||
| 18 | Platinum White Gold | `#0a0a0a` `#0070F3` `#D4AF37` `#f5f5f5` `#ffffff` | Premium, professional | Agent products, corporate websites, fintech, luxury brands | White-gold base, blue for action, gold for emphasis |
|
||||
|
||||
---
|
||||
|
||||
### Agent Design System — Full Color Scale
|
||||
|
||||
Based on the Platinum White-Gold Theme design tokens. Provides complete color scales for fine-grained design work.
|
||||
|
||||
#### White Scale (Backgrounds & Light Surfaces)
|
||||
|
||||
| Token | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| white-0 | `#ffffff` | Primary background |
|
||||
| white-50 | `#fefefe` | Slightly warm white |
|
||||
| white-75 | `#fcfcfc` | Near-white |
|
||||
| white-100 | `#fafafa` | Secondary background |
|
||||
| white-200 | `#f7f7f7` | Card background |
|
||||
| white-300 | `#f5f5f5` | Tertiary background |
|
||||
| white-400 | `#f0f0f0` | Separator zones |
|
||||
| white-500 | `#ebebeb` | Light border |
|
||||
| white-600 | `#e5e5e5` | Disabled background |
|
||||
| white-700 | `#e0e0e0` | Deep white-gray |
|
||||
| white-800 | `#d9d9d9` | Placeholder |
|
||||
| white-900 | `#d4d4d4` | Divider lines |
|
||||
| white-1000 | `#cccccc` | Deepest white |
|
||||
|
||||
#### Gold Scale (Platinum Business Accent)
|
||||
|
||||
| Token | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| gold-25 | `#FFFDF5` | Extremely light gold background |
|
||||
| gold-50 | `#FEF9E7` | Light gold background |
|
||||
| gold-75 | `#FCF3D0` | Pale gold highlight |
|
||||
| gold-100 | `#FAECB8` | Gold hover state |
|
||||
| gold-200 | `#F5DC8A` | Bright gold accent |
|
||||
| gold-300 | `#E8C860` | Gold hover |
|
||||
| gold-400 | `#D4AF37` | **Primary gold (core)** |
|
||||
| gold-500 | `#B8972E` | Gold text |
|
||||
| gold-600 | `#9A7E26` | Deep gold accent |
|
||||
| gold-700 | `#7C651E` | Dark gold border |
|
||||
| gold-800 | `#5E4C16` | Deep gold background |
|
||||
| gold-900 | `#40330F` | Very deep gold |
|
||||
| gold-1000 | `#221A08` | Black gold |
|
||||
|
||||
#### Blue Scale (Primary Action Color)
|
||||
|
||||
| Token | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| blue-25 | `#F0F7FF` | Extremely light blue background |
|
||||
| blue-50 | `#E0EFFF` | Info alert background |
|
||||
| blue-75 | `#C2DFFF` | Light blue highlight |
|
||||
| blue-100 | `#A3CFFF` | Disabled blue |
|
||||
| blue-200 | `#66AFFF` | Bright blue |
|
||||
| blue-300 | `#338FFF` | Blue hover |
|
||||
| blue-400 | `#0070F3` | **Primary blue (core)** |
|
||||
| blue-500 | `#005FCC` | Blue text |
|
||||
| blue-600 | `#004FA6` | Deep blue accent |
|
||||
| blue-700 | `#003F80` | Dark blue border |
|
||||
| blue-800 | `#002F5A` | Deep blue background |
|
||||
| blue-900 | `#001F3D` | Very deep blue |
|
||||
| blue-1000 | `#001026` | Black blue |
|
||||
|
||||
#### Gray Scale (Text & Neutral Colors)
|
||||
|
||||
| Token | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| gray-0 | `#ffffff` | White |
|
||||
| gray-50 | `#fafafa` | Extremely light gray |
|
||||
| gray-75 | `#f5f5f5` | Light gray background |
|
||||
| gray-100 | `#ededed` | Light divider |
|
||||
| gray-200 | `#d4d4d4` | Light border |
|
||||
| gray-300 | `#a3a3a3` | Quaternary text |
|
||||
| gray-400 | `#737373` | Tertiary text |
|
||||
| gray-500 | `#525252` | Secondary text |
|
||||
| gray-600 | `#404040` | Dark gray |
|
||||
| gray-700 | `#2e2e2e` | Dark background |
|
||||
| gray-800 | `#1f1f1f` | Deep background |
|
||||
| gray-900 | `#141414` | Very deep background |
|
||||
| gray-1000 | `#0a0a0a` | **Primary text (core)** |
|
||||
|
||||
#### Opacity Values
|
||||
|
||||
##### Opacity Black
|
||||
|
||||
| Opacity | Value | Usage |
|
||||
|---------|-------|-------|
|
||||
| 0% | `#0a0a0a00` | Fully transparent |
|
||||
| 2% | `#0a0a0a05` | Subtle overlay |
|
||||
| 4% | `#0a0a0a0a` | Secondary interactive background |
|
||||
| 8% | `#0a0a0a14` | Border / divider |
|
||||
| 15% | `#0a0a0a26` | Pressed state |
|
||||
| 20% | `#0a0a0a33` | Light overlay |
|
||||
| 25% | `#0a0a0a40` | Medium overlay |
|
||||
| 50% | `#0a0a0a80` | Semi-transparent |
|
||||
| 70% | `#0a0a0ab2` | Deep overlay |
|
||||
| 80% | `#0a0a0acc` | Hover state |
|
||||
| 90% | `#0a0a0ae5` | Tooltip |
|
||||
| 95% | `#0a0a0af2` | Modal |
|
||||
|
||||
##### Opacity White
|
||||
|
||||
| Opacity | Value | Usage |
|
||||
|---------|-------|-------|
|
||||
| 0% | `#ffffff00` | Fully transparent |
|
||||
| 2% | `#ffffff05` | Subtle overlay |
|
||||
| 4% | `#ffffff0a` | Secondary interactive background |
|
||||
| 8% | `#ffffff12` | Border / divider |
|
||||
| 15% | `#ffffff26` | Pressed state |
|
||||
| 20% | `#ffffff33` | Light overlay |
|
||||
| 25% | `#ffffff40` | Medium overlay |
|
||||
| 50% | `#ffffff80` | Semi-transparent |
|
||||
| 70% | `#ffffffb2` | Deep overlay |
|
||||
| 80% | `#ffffffcc` | Hover state |
|
||||
| 90% | `#ffffffe5` | Tooltip |
|
||||
| 95% | `#fffffff2` | Modal |
|
||||
|
||||
---
|
||||
|
||||
## Color Palette Rules (MANDATORY)
|
||||
|
||||
### Strict Palette Adherence
|
||||
|
||||
**Use ONLY the provided color palette. Do NOT create or modify colors.**
|
||||
|
||||
- All colors must come from the user-provided palette
|
||||
- Do NOT use colors outside the palette
|
||||
- Do NOT modify palette colors (brightness, saturation, mixing)
|
||||
- **Only exception**: Add transparency using the `transparency` property (0-100)
|
||||
|
||||
```javascript
|
||||
// Correct: Using palette colors
|
||||
slide.addShape(pres.shapes.RECTANGLE, { fill: { color: theme.primary } });
|
||||
slide.addText("Title", { color: theme.accent });
|
||||
|
||||
// Wrong: Colors outside palette
|
||||
slide.addShape(pres.shapes.RECTANGLE, { fill: { color: "1a1a2e" } });
|
||||
```
|
||||
|
||||
### No Gradients
|
||||
|
||||
**Gradients are prohibited. Use solid colors only.**
|
||||
|
||||
### No Animations
|
||||
|
||||
**Animations and transitions are prohibited.** All slides must be static.
|
||||
|
||||
---
|
||||
|
||||
## Font Reference
|
||||
|
||||
### Recommended Fonts
|
||||
|
||||
| Language | Default Font | Alternatives |
|
||||
|----------|-------------|--------------|
|
||||
| **Chinese** | Microsoft YaHei | — |
|
||||
| **English** | Arial | Georgia, Calibri, Cambria, Trebuchet MS |
|
||||
|
||||
- For mixed Chinese-English content: use Microsoft YaHei for Chinese, the chosen font for English
|
||||
- Prefer system fonts for cross-platform compatibility
|
||||
- Titles and body text can use different font pairings (e.g. Georgia + Calibri)
|
||||
|
||||
### Recommended Font Pairings
|
||||
|
||||
| Header Font | Body Font |
|
||||
|-------------|-----------|
|
||||
| Georgia | Calibri |
|
||||
| Arial Black | Arial |
|
||||
| Calibri | Calibri Light |
|
||||
| Cambria | Calibri |
|
||||
| Trebuchet MS | Calibri |
|
||||
| Impact | Arial |
|
||||
| Palatino | Garamond |
|
||||
| Consolas | Calibri |
|
||||
|
||||
**Choose an interesting font pairing** — don't default to Arial for everything. Pick a header font with personality and pair it with a clean body font.
|
||||
|
||||
### No Bold for Body Text
|
||||
|
||||
**Plain body text and caption/legend text must NOT use bold.**
|
||||
|
||||
- Body paragraphs, descriptions → normal weight
|
||||
- Captions, legends, footnotes → normal weight
|
||||
- Reserve bold for titles and headings only
|
||||
|
||||
```javascript
|
||||
// Correct
|
||||
slide.addText("Main Title", { bold: true, fontSize: 36, fontFace: "Arial" });
|
||||
slide.addText("Body text here.", { bold: false, fontSize: 14, fontFace: "Arial" });
|
||||
|
||||
// Wrong
|
||||
slide.addText("Body text here.", { bold: true, fontSize: 14 });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Style Recipes
|
||||
|
||||
The same design can be rendered in 4 distinct visual styles by adjusting corner radius (`rectRadius`) and spacing. Choose the style recipe that fits the presentation tone.
|
||||
|
||||
> **Unit note**: PptxGenJS uses inches. Slide dimensions are 10" x 5.625" (LAYOUT_16x9).
|
||||
|
||||
### Style Overview
|
||||
|
||||
| Style | Corner Radius | Spacing | Best For |
|
||||
|-------|--------------|---------|----------|
|
||||
| **Sharp & Compact** | 0 ~ 0.05" | Tight | Data-dense, tables, professional reports |
|
||||
| **Soft & Balanced** | 0.08" ~ 0.12" | Moderate | Corporate, business presentations, general use |
|
||||
| **Rounded & Spacious** | 0.15" ~ 0.25" | Relaxed | Product intros, marketing, creative showcases |
|
||||
| **Pill & Airy** | 0.3" ~ 0.5" | Open | Brand showcases, launch events, premium presentations |
|
||||
|
||||
### Sharp & Compact
|
||||
|
||||
**Visual character**: Geometric, high information density, formal and serious.
|
||||
|
||||
| Category | Value (inches) | Notes |
|
||||
|----------|---------------|-------|
|
||||
| Corner radius — small | 0" | Full right angle |
|
||||
| Corner radius — medium | 0.03" | Micro-rounded |
|
||||
| Corner radius — large | 0.05" | Slight rounding |
|
||||
| Element padding | 0.1" ~ 0.15" | Compact |
|
||||
| Element gap | 0.1" ~ 0.2" | Compact |
|
||||
| Page margin | 0.3" | Narrow |
|
||||
| Block gap | 0.25" ~ 0.35" | Compact |
|
||||
|
||||
### Soft & Balanced
|
||||
|
||||
**Visual character**: Moderate rounding, comfortable whitespace, professional yet approachable.
|
||||
|
||||
| Category | Value (inches) | Notes |
|
||||
|----------|---------------|-------|
|
||||
| Corner radius — small | 0.05" | Slight rounding |
|
||||
| Corner radius — medium | 0.08" | Medium rounding |
|
||||
| Corner radius — large | 0.12" | Larger rounding |
|
||||
| Element padding | 0.15" ~ 0.2" | Moderate |
|
||||
| Element gap | 0.15" ~ 0.25" | Moderate |
|
||||
| Page margin | 0.4" | Standard |
|
||||
| Block gap | 0.35" ~ 0.5" | Moderate |
|
||||
|
||||
### Rounded & Spacious
|
||||
|
||||
**Visual character**: Large corners, generous whitespace, friendly and modern.
|
||||
|
||||
| Category | Value (inches) | Notes |
|
||||
|----------|---------------|-------|
|
||||
| Corner radius — small | 0.1" | Medium rounding |
|
||||
| Corner radius — medium | 0.15" | Large rounding |
|
||||
| Corner radius — large | 0.25" | Very large rounding |
|
||||
| Element padding | 0.2" ~ 0.3" | Relaxed |
|
||||
| Element gap | 0.25" ~ 0.4" | Relaxed |
|
||||
| Page margin | 0.5" | Wide |
|
||||
| Block gap | 0.5" ~ 0.7" | Relaxed |
|
||||
|
||||
### Pill & Airy
|
||||
|
||||
**Visual character**: Full pill-shaped corners, abundant whitespace, light and open feel, strong brand presence.
|
||||
|
||||
| Category | Value (inches) | Notes |
|
||||
|----------|---------------|-------|
|
||||
| Corner radius — small | 0.2" | Large rounding |
|
||||
| Corner radius — medium | 0.3" | Pill shape |
|
||||
| Corner radius — large | 0.5" | Full pill |
|
||||
| Element padding | 0.25" ~ 0.4" | Open |
|
||||
| Element gap | 0.3" ~ 0.5" | Open |
|
||||
| Page margin | 0.6" | Wide |
|
||||
| Block gap | 0.6" ~ 0.9" | Open |
|
||||
|
||||
### Component Style Mapping
|
||||
|
||||
| Component | Sharp | Soft | Rounded | Pill |
|
||||
|-----------|-------|------|---------|------|
|
||||
| **Button / Tag** | rectRadius: 0 | rectRadius: 0.05 | rectRadius: 0.1 | rectRadius: 0.2 |
|
||||
| **Card / Container** | rectRadius: 0.03 | rectRadius: 0.1 | rectRadius: 0.2 | rectRadius: 0.3 |
|
||||
| **Image Container** | rectRadius: 0 | rectRadius: 0.08 | rectRadius: 0.15 | rectRadius: 0.25 |
|
||||
| **Input Field** | rectRadius: 0 | rectRadius: 0.05 | rectRadius: 0.1 | rectRadius: 0.2 |
|
||||
| **Badge** | rectRadius: 0.02 | rectRadius: 0.05 | rectRadius: 0.08 | rectRadius: 0.15 |
|
||||
| **Avatar Frame** | rectRadius: 0 | rectRadius: 0.1 | rectRadius: 0.2 | rectRadius: 0.5 (circle) |
|
||||
|
||||
#### PptxGenJS Corner Radius Examples
|
||||
|
||||
```javascript
|
||||
// Sharp style card
|
||||
slide.addShape("rect", {
|
||||
x: 0.5, y: 1, w: 4, h: 2.5,
|
||||
fill: { color: "F5F5F5" },
|
||||
rectRadius: 0.03
|
||||
});
|
||||
|
||||
// Rounded style card
|
||||
slide.addShape("rect", {
|
||||
x: 0.5, y: 1, w: 4, h: 2.5,
|
||||
fill: { color: "F5F5F5" },
|
||||
rectRadius: 0.2
|
||||
});
|
||||
|
||||
// Pill style button (height 0.4", rectRadius 0.2" = perfect pill)
|
||||
slide.addShape("rect", {
|
||||
x: 3, y: 4, w: 2, h: 0.4,
|
||||
fill: { color: "4A90D9" },
|
||||
rectRadius: 0.2
|
||||
});
|
||||
```
|
||||
|
||||
### Mixing Rules
|
||||
|
||||
#### 1. Outer container corner >= inner element corner
|
||||
|
||||
```javascript
|
||||
// Correct: outer > inner
|
||||
card: rectRadius: 0.2
|
||||
button: rectRadius: 0.1
|
||||
|
||||
// Wrong: inner > outer → visual overflow effect
|
||||
card: rectRadius: 0.1
|
||||
button: rectRadius: 0.2
|
||||
```
|
||||
|
||||
#### 2. Information density drives spacing
|
||||
|
||||
| Zone Type | Recommended Style |
|
||||
|-----------|------------------|
|
||||
| Data display zone | Sharp / Soft (compact spacing) |
|
||||
| Content browsing zone | Rounded / Pill (relaxed spacing) |
|
||||
| Title zone | Soft / Rounded (moderate spacing) |
|
||||
|
||||
#### 3. Corner radius vs element height
|
||||
|
||||
| Element Height | Sharp | Soft | Rounded | Pill |
|
||||
|---------------|-------|------|---------|------|
|
||||
| Small (< 0.3") | 0" | 0.03" | 0.08" | height/2 |
|
||||
| Medium (0.3" ~ 0.6") | 0.02" | 0.05" | 0.12" | height/2 |
|
||||
| Large (0.6" ~ 1.2") | 0.03" | 0.08" | 0.2" | 0.3" |
|
||||
| Extra large (> 1.2") | 0.05" | 0.12" | 0.25" | 0.4" |
|
||||
|
||||
> **Pill tip**: For a perfect pill shape, set `rectRadius = element height / 2`
|
||||
|
||||
### Typography Scale (PPT)
|
||||
|
||||
| Usage | Size (pt) | Notes |
|
||||
|-------|-----------|-------|
|
||||
| Annotations / Sources | 10 ~ 12 | Minimum readable size |
|
||||
| Body / Description | 14 ~ 16 | Standard body |
|
||||
| Subtitle | 18 ~ 22 | Secondary heading |
|
||||
| Title | 28 ~ 36 | Page title |
|
||||
| Large Title | 44 ~ 60 | Cover / section title |
|
||||
| Data Callout | 60 ~ 96 | Key number display |
|
||||
|
||||
### Spacing Scale (PPT)
|
||||
|
||||
Based on 10" x 5.625" slide dimensions:
|
||||
|
||||
| Usage | Recommended (inches) |
|
||||
|-------|---------------------|
|
||||
| Icon-to-text gap | 0.08" ~ 0.15" |
|
||||
| List item spacing | 0.15" ~ 0.25" |
|
||||
| Card inner padding | 0.2" ~ 0.4" |
|
||||
| Element group gap | 0.3" ~ 0.5" |
|
||||
| Page safe margin | 0.4" ~ 0.6" |
|
||||
| Major block gap | 0.5" ~ 0.8" |
|
||||
|
||||
### Quick Selection Guide
|
||||
|
||||
| Presentation Type | Recommended Style | Reason |
|
||||
|------------------|------------------|--------|
|
||||
| Finance / Data reports | Sharp & Compact | High density, serious and precise |
|
||||
| Corporate / Business | Soft & Balanced | Balances professionalism and approachability |
|
||||
| Product intro / Marketing | Rounded & Spacious | Modern feel, friendly |
|
||||
| Launch events / Brand | Pill & Airy | Premium feel, visual impact |
|
||||
| Training / Education | Soft / Rounded | Clear, readable, friendly |
|
||||
| Tech sharing | Sharp / Soft | Professional, information-dense |
|
||||
@@ -0,0 +1,162 @@
|
||||
# Editing Existing Presentations
|
||||
|
||||
## Template-Based Workflow
|
||||
|
||||
When using an existing presentation as a template:
|
||||
|
||||
1. **Copy and analyze**:
|
||||
```bash
|
||||
cp /path/to/user-provided.pptx template.pptx
|
||||
python -m markitdown template.pptx > template.md
|
||||
```
|
||||
Review `template.md` to see placeholder text and slide structure.
|
||||
|
||||
2. **Plan slide mapping**: For each content section, choose a template slide.
|
||||
|
||||
**USE VARIED LAYOUTS** — monotonous presentations are a common failure mode. Don't default to basic title + bullet slides. Actively seek out:
|
||||
- Multi-column layouts (2-column, 3-column)
|
||||
- Image + text combinations
|
||||
- Full-bleed images with text overlay
|
||||
- Quote or callout slides
|
||||
- Section dividers
|
||||
- Stat/number callouts
|
||||
- Icon grids or icon + text rows
|
||||
|
||||
**Avoid:** Repeating the same text-heavy layout for every slide.
|
||||
|
||||
Match content type to layout style (e.g., key points -> bullet slide, team info -> multi-column, testimonials -> quote slide).
|
||||
|
||||
3. **Unpack**: Extract the PPTX into an editable XML tree using Python's `zipfile` module. Pretty-print the XML for readability.
|
||||
|
||||
4. **Build presentation** (do this yourself, not with subagents):
|
||||
- Delete unwanted slides (remove from `<p:sldIdLst>`)
|
||||
- Duplicate slides you want to reuse (copy slide XML, relationships, and update `Content_Types.xml` and `presentation.xml`)
|
||||
- Reorder slides in `<p:sldIdLst>`
|
||||
- **Complete all structural changes before step 5**
|
||||
|
||||
5. **Edit content**: Update text in each `slide{N}.xml`.
|
||||
**Use subagents here if available** — slides are separate XML files, so subagents can edit in parallel.
|
||||
|
||||
6. **Clean**: Remove orphaned files — slides not in `<p:sldIdLst>`, unreferenced media, orphaned rels.
|
||||
|
||||
7. **Pack**: Repack the XML tree into a PPTX file. Validate, repair, condense XML, re-encode smart quotes.
|
||||
|
||||
Always write to `/tmp/` first, then copy to the final path. Python's `zipfile` module uses `seek` internally, which fails on some volume mounts (e.g. Docker bind mounts). Writing to a local temp path avoids this.
|
||||
|
||||
## Output Structure
|
||||
|
||||
Copy the user-provided file to `template.pptx` in cwd. This preserves the original and gives a predictable name for all downstream operations.
|
||||
|
||||
```bash
|
||||
cp /path/to/user-provided.pptx template.pptx
|
||||
```
|
||||
|
||||
```text
|
||||
./
|
||||
├── template.pptx # Copy of user-provided file (never modified)
|
||||
├── template.md # markitdown extraction
|
||||
├── unpacked/ # Editable XML tree
|
||||
└── edited.pptx # Final repacked deck
|
||||
```
|
||||
|
||||
Minimum expected deliverable: `edited.pptx`.
|
||||
|
||||
## Slide Operations
|
||||
|
||||
Slide order is in `ppt/presentation.xml` -> `<p:sldIdLst>`.
|
||||
|
||||
**Reorder**: Rearrange `<p:sldId>` elements.
|
||||
|
||||
**Delete**: Remove `<p:sldId>`, then clean orphaned files.
|
||||
|
||||
**Add**: Copy the source slide's XML file, its `.rels` file, and update `Content_Types.xml` and `presentation.xml`. Never manually copy slide files without updating all references — this causes broken notes references and missing relationship IDs.
|
||||
|
||||
## Editing Content
|
||||
|
||||
**Subagents:** If available, use them here (after completing step 4). Each slide is a separate XML file, so subagents can edit in parallel. In your prompt to subagents, include:
|
||||
- The slide file path(s) to edit
|
||||
- **"Use the Edit tool for all changes"**
|
||||
- The formatting rules and common pitfalls below
|
||||
|
||||
For each slide:
|
||||
1. Read the slide's XML
|
||||
2. Identify ALL placeholder content — text, images, charts, icons, captions
|
||||
3. Replace each placeholder with final content
|
||||
|
||||
**Use the Edit tool, not sed or Python scripts.** The Edit tool forces specificity about what to replace and where, yielding better reliability.
|
||||
|
||||
## Formatting Rules
|
||||
|
||||
- **Bold all headers, subheadings, and inline labels**: Use `b="1"` on `<a:rPr>`. This includes:
|
||||
- Slide titles
|
||||
- Section headers within a slide
|
||||
- Inline labels like (e.g.: "Status:", "Description:") at the start of a line
|
||||
- **Never use unicode bullets**: Use proper list formatting with `<a:buChar>` or `<a:buAutoNum>`
|
||||
- **Bullet consistency**: Let bullets inherit from the layout. Only specify `<a:buChar>` or `<a:buNone>`.
|
||||
|
||||
## Common Pitfalls — Template Editing
|
||||
|
||||
### Template Adaptation
|
||||
|
||||
When source content has fewer items than the template:
|
||||
- **Remove excess elements entirely** (images, shapes, text boxes), don't just clear text
|
||||
- Check for orphaned visuals after clearing text content
|
||||
- Run content QA with `markitdown` to catch mismatched counts
|
||||
|
||||
When replacing text with different length content:
|
||||
- **Shorter replacements**: Usually safe
|
||||
- **Longer replacements**: May overflow or wrap unexpectedly
|
||||
- Verify with `markitdown` after text changes
|
||||
- Consider truncating or splitting content to fit the template's design constraints
|
||||
|
||||
**Template slots != Source items**: If template has 4 team members but source has 3 users, delete the 4th member's entire group (image + text boxes), not just the text.
|
||||
|
||||
### Multi-Item Content
|
||||
|
||||
If source has multiple items (numbered lists, multiple sections), create separate `<a:p>` elements for each — **never concatenate into one string**.
|
||||
|
||||
**WRONG** — all items in one paragraph:
|
||||
```xml
|
||||
<a:p>
|
||||
<a:r><a:rPr .../><a:t>Step 1: Do the first thing. Step 2: Do the second thing.</a:t></a:r>
|
||||
</a:p>
|
||||
```
|
||||
|
||||
**CORRECT** — separate paragraphs with bold headers:
|
||||
```xml
|
||||
<a:p>
|
||||
<a:pPr algn="l"><a:lnSpc><a:spcPts val="3919"/></a:lnSpc></a:pPr>
|
||||
<a:r><a:rPr lang="en-US" sz="2799" b="1" .../><a:t>Step 1</a:t></a:r>
|
||||
</a:p>
|
||||
<a:p>
|
||||
<a:pPr algn="l"><a:lnSpc><a:spcPts val="3919"/></a:lnSpc></a:pPr>
|
||||
<a:r><a:rPr lang="en-US" sz="2799" .../><a:t>Do the first thing.</a:t></a:r>
|
||||
</a:p>
|
||||
<a:p>
|
||||
<a:pPr algn="l"><a:lnSpc><a:spcPts val="3919"/></a:lnSpc></a:pPr>
|
||||
<a:r><a:rPr lang="en-US" sz="2799" b="1" .../><a:t>Step 2</a:t></a:r>
|
||||
</a:p>
|
||||
<!-- continue pattern -->
|
||||
```
|
||||
|
||||
Copy `<a:pPr>` from the original paragraph to preserve line spacing. Use `b="1"` on headers.
|
||||
|
||||
### Smart Quotes
|
||||
|
||||
The Edit tool converts smart quotes to ASCII. **When adding new text with quotes, use XML entities:**
|
||||
|
||||
```xml
|
||||
<a:t>the “Agreement”</a:t>
|
||||
```
|
||||
|
||||
| Character | Name | Unicode | XML Entity |
|
||||
|-----------|------|---------|------------|
|
||||
| \u201c | Left double quote | U+201C | `“` |
|
||||
| \u201d | Right double quote | U+201D | `”` |
|
||||
| \u2018 | Left single quote | U+2018 | `‘` |
|
||||
| \u2019 | Right single quote | U+2019 | `’` |
|
||||
|
||||
### Other
|
||||
|
||||
- **Whitespace**: Use `xml:space="preserve"` on `<a:t>` with leading/trailing spaces
|
||||
- **XML parsing**: Use `defusedxml.minidom`, not `xml.etree.ElementTree` (corrupts namespaces)
|
||||
@@ -0,0 +1,112 @@
|
||||
# QA Process & Common Pitfalls
|
||||
|
||||
## QA Process
|
||||
|
||||
**Assume there are problems. Your job is to find them.**
|
||||
|
||||
Your first render is almost never correct. Approach QA as a bug hunt, not a confirmation step. If you found zero issues on first inspection, you weren't looking hard enough.
|
||||
|
||||
### Content QA
|
||||
|
||||
```bash
|
||||
python -m markitdown output.pptx
|
||||
```
|
||||
|
||||
Check for missing content, typos, wrong order.
|
||||
|
||||
**Check for leftover placeholder text:**
|
||||
|
||||
```bash
|
||||
python -m markitdown output.pptx | grep -iE "xxxx|lorem|ipsum|placeholder|this.*(page|slide).*layout"
|
||||
```
|
||||
|
||||
If grep returns results, fix them before declaring success.
|
||||
|
||||
### Verification Loop
|
||||
|
||||
1. Generate slides -> Extract text with `python -m markitdown output.pptx` -> Review content
|
||||
2. **List issues found** (if none found, look again more critically)
|
||||
3. Fix issues
|
||||
4. **Re-verify affected slides** — one fix often creates another problem
|
||||
5. Repeat until a full pass reveals no new issues
|
||||
|
||||
**Do not declare success until you've completed at least one fix-and-verify cycle.**
|
||||
|
||||
### Per-Slide QA (for from-scratch creation)
|
||||
|
||||
```bash
|
||||
python -m markitdown slide-XX-preview.pptx
|
||||
```
|
||||
|
||||
Check for missing content, placeholder text, missing page number badge.
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
- **Don't repeat the same layout** — vary columns, cards, and callouts across slides
|
||||
- **Don't center body text** — left-align paragraphs and lists; center only titles
|
||||
- **Don't skimp on size contrast** — titles need 36pt+ to stand out from 14-16pt body
|
||||
- **Don't default to blue** — pick colors that reflect the specific topic
|
||||
- **Don't mix spacing randomly** — choose 0.3" or 0.5" gaps and use consistently
|
||||
- **Don't style one slide and leave the rest plain** — commit fully or keep it simple throughout
|
||||
- **Don't create text-only slides** — add images, icons, charts, or visual elements; avoid plain title + bullets
|
||||
- **Don't forget text box padding** — when aligning lines or shapes with text edges, set `margin: 0` on the text box or offset the shape to account for padding
|
||||
- **Don't use low-contrast elements** — icons AND text need strong contrast against the background
|
||||
- **NEVER use accent lines under titles** — these are a hallmark of AI-generated slides; use whitespace or background color instead
|
||||
- **NEVER use "#" with hex colors** — causes file corruption in PptxGenJS
|
||||
- **NEVER encode opacity in hex strings** — use the `opacity` property instead
|
||||
- **NEVER use async/await in createSlide()** — compile.js won't await
|
||||
- **NEVER reuse option objects across PptxGenJS calls** — PptxGenJS mutates objects in-place
|
||||
|
||||
---
|
||||
|
||||
## Critical Pitfalls — PptxGenJS
|
||||
|
||||
### NEVER use async/await in createSlide()
|
||||
|
||||
```javascript
|
||||
// WRONG - compile.js won't await
|
||||
async function createSlide(pres, theme) { ... }
|
||||
|
||||
// CORRECT
|
||||
function createSlide(pres, theme) { ... }
|
||||
```
|
||||
|
||||
### NEVER use "#" with hex colors
|
||||
|
||||
```javascript
|
||||
color: "FF0000" // CORRECT
|
||||
color: "#FF0000" // CORRUPTS FILE
|
||||
```
|
||||
|
||||
### NEVER encode opacity in hex strings
|
||||
|
||||
```javascript
|
||||
shadow: { color: "00000020" } // CORRUPTS FILE
|
||||
shadow: { color: "000000", opacity: 0.12 } // CORRECT
|
||||
```
|
||||
|
||||
### Prevent text wrapping in titles
|
||||
|
||||
```javascript
|
||||
// Use fit:'shrink' for long titles
|
||||
slide.addText("Long Title Here", {
|
||||
x: 0.5, y: 2, w: 9, h: 1,
|
||||
fontSize: 48, fit: "shrink"
|
||||
});
|
||||
```
|
||||
|
||||
### NEVER reuse option objects across calls
|
||||
|
||||
```javascript
|
||||
// WRONG
|
||||
const shadow = { type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.15 };
|
||||
slide.addShape(pres.shapes.RECTANGLE, { shadow, ... });
|
||||
slide.addShape(pres.shapes.RECTANGLE, { shadow, ... });
|
||||
|
||||
// CORRECT - factory function
|
||||
const makeShadow = () => ({ type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.15 });
|
||||
slide.addShape(pres.shapes.RECTANGLE, { shadow: makeShadow(), ... });
|
||||
slide.addShape(pres.shapes.RECTANGLE, { shadow: makeShadow(), ... });
|
||||
```
|
||||
@@ -0,0 +1,420 @@
|
||||
# PptxGenJS Tutorial
|
||||
|
||||
## Setup & Basic Structure
|
||||
|
||||
```javascript
|
||||
const pptxgen = require("pptxgenjs");
|
||||
|
||||
let pres = new pptxgen();
|
||||
pres.layout = 'LAYOUT_16x9'; // or 'LAYOUT_16x10', 'LAYOUT_4x3', 'LAYOUT_WIDE'
|
||||
pres.author = 'Your Name';
|
||||
pres.title = 'Presentation Title';
|
||||
|
||||
let slide = pres.addSlide();
|
||||
slide.addText("Hello World!", { x: 0.5, y: 0.5, fontSize: 36, color: "363636" });
|
||||
|
||||
pres.writeFile({ fileName: "Presentation.pptx" });
|
||||
```
|
||||
|
||||
## Layout Dimensions
|
||||
|
||||
Slide dimensions (coordinates in inches):
|
||||
- `LAYOUT_16x9`: 10" x 5.625" (default)
|
||||
- `LAYOUT_16x10`: 10" x 6.25"
|
||||
- `LAYOUT_4x3`: 10" x 7.5"
|
||||
- `LAYOUT_WIDE`: 13.3" x 7.5"
|
||||
|
||||
---
|
||||
|
||||
## Text & Formatting
|
||||
|
||||
```javascript
|
||||
// Basic text
|
||||
slide.addText("Simple Text", {
|
||||
x: 1, y: 1, w: 8, h: 2, fontSize: 24, fontFace: "Arial",
|
||||
color: "363636", bold: true, align: "center", valign: "middle"
|
||||
});
|
||||
|
||||
// Character spacing (use charSpacing, not letterSpacing which is silently ignored)
|
||||
slide.addText("SPACED TEXT", { x: 1, y: 1, w: 8, h: 1, charSpacing: 6 });
|
||||
|
||||
// Rich text arrays
|
||||
slide.addText([
|
||||
{ text: "Bold ", options: { bold: true } },
|
||||
{ text: "Italic ", options: { italic: true } }
|
||||
], { x: 1, y: 3, w: 8, h: 1 });
|
||||
|
||||
// Multi-line text (requires breakLine: true)
|
||||
slide.addText([
|
||||
{ text: "Line 1", options: { breakLine: true } },
|
||||
{ text: "Line 2", options: { breakLine: true } },
|
||||
{ text: "Line 3" } // Last item doesn't need breakLine
|
||||
], { x: 0.5, y: 0.5, w: 8, h: 2 });
|
||||
|
||||
// Text box margin (internal padding)
|
||||
slide.addText("Title", {
|
||||
x: 0.5, y: 0.3, w: 9, h: 0.6,
|
||||
margin: 0 // Use 0 when aligning text with other elements like shapes or icons
|
||||
});
|
||||
```
|
||||
|
||||
**Tip:** Text boxes have internal margin by default. Set `margin: 0` when you need text to align precisely with shapes, lines, or icons at the same x-position.
|
||||
|
||||
---
|
||||
|
||||
## Lists & Bullets
|
||||
|
||||
```javascript
|
||||
// CORRECT: Multiple bullets
|
||||
slide.addText([
|
||||
{ text: "First item", options: { bullet: true, breakLine: true } },
|
||||
{ text: "Second item", options: { bullet: true, breakLine: true } },
|
||||
{ text: "Third item", options: { bullet: true } }
|
||||
], { x: 0.5, y: 0.5, w: 8, h: 3 });
|
||||
|
||||
// WRONG: Never use unicode bullets
|
||||
slide.addText("* First item", { ... }); // Creates double bullets
|
||||
|
||||
// Sub-items and numbered lists
|
||||
{ text: "Sub-item", options: { bullet: true, indentLevel: 1 } }
|
||||
{ text: "First", options: { bullet: { type: "number" }, breakLine: true } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shapes
|
||||
|
||||
```javascript
|
||||
slide.addShape(pres.shapes.RECTANGLE, {
|
||||
x: 0.5, y: 0.8, w: 1.5, h: 3.0,
|
||||
fill: { color: "FF0000" }, line: { color: "000000", width: 2 }
|
||||
});
|
||||
|
||||
slide.addShape(pres.shapes.OVAL, { x: 4, y: 1, w: 2, h: 2, fill: { color: "0000FF" } });
|
||||
|
||||
slide.addShape(pres.shapes.LINE, {
|
||||
x: 1, y: 3, w: 5, h: 0, line: { color: "FF0000", width: 3, dashType: "dash" }
|
||||
});
|
||||
|
||||
// With transparency
|
||||
slide.addShape(pres.shapes.RECTANGLE, {
|
||||
x: 1, y: 1, w: 3, h: 2,
|
||||
fill: { color: "0088CC", transparency: 50 }
|
||||
});
|
||||
|
||||
// Rounded rectangle (rectRadius only works with ROUNDED_RECTANGLE, not RECTANGLE)
|
||||
// Don't pair with rectangular accent overlays -- they won't cover rounded corners. Use RECTANGLE instead.
|
||||
slide.addShape(pres.shapes.ROUNDED_RECTANGLE, {
|
||||
x: 1, y: 1, w: 3, h: 2,
|
||||
fill: { color: "FFFFFF" }, rectRadius: 0.1
|
||||
});
|
||||
|
||||
// With shadow
|
||||
slide.addShape(pres.shapes.RECTANGLE, {
|
||||
x: 1, y: 1, w: 3, h: 2,
|
||||
fill: { color: "FFFFFF" },
|
||||
shadow: { type: "outer", color: "000000", blur: 6, offset: 2, angle: 135, opacity: 0.15 }
|
||||
});
|
||||
```
|
||||
|
||||
Shadow options:
|
||||
|
||||
| Property | Type | Range | Notes |
|
||||
|----------|------|-------|-------|
|
||||
| `type` | string | `"outer"`, `"inner"` | |
|
||||
| `color` | string | 6-char hex (e.g. `"000000"`) | No `#` prefix, no 8-char hex -- see Common Pitfalls |
|
||||
| `blur` | number | 0-100 pt | |
|
||||
| `offset` | number | 0-200 pt | **Must be non-negative** -- negative values corrupt the file |
|
||||
| `angle` | number | 0-359 degrees | Direction the shadow falls (135 = bottom-right, 270 = upward) |
|
||||
| `opacity` | number | 0.0-1.0 | Use this for transparency, never encode in color string |
|
||||
|
||||
To cast a shadow upward (e.g. on a footer bar), use `angle: 270` with a positive offset -- do **not** use a negative offset.
|
||||
|
||||
**Note**: Gradient fills are not natively supported. Use a gradient image as a background instead.
|
||||
|
||||
---
|
||||
|
||||
## Images
|
||||
|
||||
### Image Sources
|
||||
|
||||
```javascript
|
||||
// From file path
|
||||
slide.addImage({ path: "images/chart.png", x: 1, y: 1, w: 5, h: 3 });
|
||||
|
||||
// From URL
|
||||
slide.addImage({ path: "https://example.com/image.jpg", x: 1, y: 1, w: 5, h: 3 });
|
||||
|
||||
// From base64 (faster, no file I/O)
|
||||
slide.addImage({ data: "image/png;base64,iVBORw0KGgo...", x: 1, y: 1, w: 5, h: 3 });
|
||||
```
|
||||
|
||||
### Image Options
|
||||
|
||||
```javascript
|
||||
slide.addImage({
|
||||
path: "image.png",
|
||||
x: 1, y: 1, w: 5, h: 3,
|
||||
rotate: 45, // 0-359 degrees
|
||||
rounding: true, // Circular crop
|
||||
transparency: 50, // 0-100
|
||||
flipH: true, // Horizontal flip
|
||||
flipV: false, // Vertical flip
|
||||
altText: "Description", // Accessibility
|
||||
hyperlink: { url: "https://example.com" }
|
||||
});
|
||||
```
|
||||
|
||||
### Image Sizing Modes
|
||||
|
||||
```javascript
|
||||
// Contain - fit inside, preserve ratio
|
||||
{ sizing: { type: 'contain', w: 4, h: 3 } }
|
||||
|
||||
// Cover - fill area, preserve ratio (may crop)
|
||||
{ sizing: { type: 'cover', w: 4, h: 3 } }
|
||||
|
||||
// Crop - cut specific portion
|
||||
{ sizing: { type: 'crop', x: 0.5, y: 0.5, w: 2, h: 2 } }
|
||||
```
|
||||
|
||||
### Calculate Dimensions (preserve aspect ratio)
|
||||
|
||||
```javascript
|
||||
const origWidth = 1978, origHeight = 923, maxHeight = 3.0;
|
||||
const calcWidth = maxHeight * (origWidth / origHeight);
|
||||
const centerX = (10 - calcWidth) / 2;
|
||||
|
||||
slide.addImage({ path: "image.png", x: centerX, y: 1.2, w: calcWidth, h: maxHeight });
|
||||
```
|
||||
|
||||
### Supported Formats
|
||||
|
||||
- **Standard**: PNG, JPG, GIF (animated GIFs work in Microsoft 365)
|
||||
- **SVG**: Works in modern PowerPoint/Microsoft 365
|
||||
|
||||
---
|
||||
|
||||
## Icons
|
||||
|
||||
Use react-icons to generate SVG icons, then rasterize to PNG for universal compatibility.
|
||||
|
||||
### Setup
|
||||
|
||||
```javascript
|
||||
const React = require("react");
|
||||
const ReactDOMServer = require("react-dom/server");
|
||||
const sharp = require("sharp");
|
||||
const { FaCheckCircle, FaChartLine } = require("react-icons/fa");
|
||||
|
||||
function renderIconSvg(IconComponent, color = "#000000", size = 256) {
|
||||
return ReactDOMServer.renderToStaticMarkup(
|
||||
React.createElement(IconComponent, { color, size: String(size) })
|
||||
);
|
||||
}
|
||||
|
||||
async function iconToBase64Png(IconComponent, color, size = 256) {
|
||||
const svg = renderIconSvg(IconComponent, color, size);
|
||||
const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer();
|
||||
return "image/png;base64," + pngBuffer.toString("base64");
|
||||
}
|
||||
```
|
||||
|
||||
### Add Icon to Slide
|
||||
|
||||
```javascript
|
||||
const iconData = await iconToBase64Png(FaCheckCircle, "#4472C4", 256);
|
||||
|
||||
slide.addImage({
|
||||
data: iconData,
|
||||
x: 1, y: 1, w: 0.5, h: 0.5 // Size in inches
|
||||
});
|
||||
```
|
||||
|
||||
**Note**: Use size 256 or higher for crisp icons. The size parameter controls the rasterization resolution, not the display size on the slide (which is set by `w` and `h` in inches).
|
||||
|
||||
### Icon Libraries
|
||||
|
||||
Install: `npm install -g react-icons react react-dom sharp`
|
||||
|
||||
Popular icon sets in react-icons:
|
||||
- `react-icons/fa` - Font Awesome
|
||||
- `react-icons/md` - Material Design
|
||||
- `react-icons/hi` - Heroicons
|
||||
- `react-icons/bi` - Bootstrap Icons
|
||||
|
||||
---
|
||||
|
||||
## Slide Backgrounds
|
||||
|
||||
```javascript
|
||||
// Solid color
|
||||
slide.background = { color: "F1F1F1" };
|
||||
|
||||
// Color with transparency
|
||||
slide.background = { color: "FF3399", transparency: 50 };
|
||||
|
||||
// Image from URL
|
||||
slide.background = { path: "https://example.com/bg.jpg" };
|
||||
|
||||
// Image from base64
|
||||
slide.background = { data: "image/png;base64,iVBORw0KGgo..." };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tables
|
||||
|
||||
```javascript
|
||||
slide.addTable([
|
||||
["Header 1", "Header 2"],
|
||||
["Cell 1", "Cell 2"]
|
||||
], {
|
||||
x: 1, y: 1, w: 8, h: 2,
|
||||
border: { pt: 1, color: "999999" }, fill: { color: "F1F1F1" }
|
||||
});
|
||||
|
||||
// Advanced with merged cells
|
||||
let tableData = [
|
||||
[{ text: "Header", options: { fill: { color: "6699CC" }, color: "FFFFFF", bold: true } }, "Cell"],
|
||||
[{ text: "Merged", options: { colspan: 2 } }]
|
||||
];
|
||||
slide.addTable(tableData, { x: 1, y: 3.5, w: 8, colW: [4, 4] });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Charts
|
||||
|
||||
```javascript
|
||||
// Bar chart
|
||||
slide.addChart(pres.charts.BAR, [{
|
||||
name: "Sales", labels: ["Q1", "Q2", "Q3", "Q4"], values: [4500, 5500, 6200, 7100]
|
||||
}], {
|
||||
x: 0.5, y: 0.6, w: 6, h: 3, barDir: 'col',
|
||||
showTitle: true, title: 'Quarterly Sales'
|
||||
});
|
||||
|
||||
// Line chart
|
||||
slide.addChart(pres.charts.LINE, [{
|
||||
name: "Temp", labels: ["Jan", "Feb", "Mar"], values: [32, 35, 42]
|
||||
}], { x: 0.5, y: 4, w: 6, h: 3, lineSize: 3, lineSmooth: true });
|
||||
|
||||
// Pie chart
|
||||
slide.addChart(pres.charts.PIE, [{
|
||||
name: "Share", labels: ["A", "B", "Other"], values: [35, 45, 20]
|
||||
}], { x: 7, y: 1, w: 5, h: 4, showPercent: true });
|
||||
```
|
||||
|
||||
### Better-Looking Charts
|
||||
|
||||
Default charts look dated. Apply these options for a modern, clean appearance:
|
||||
|
||||
```javascript
|
||||
slide.addChart(pres.charts.BAR, chartData, {
|
||||
x: 0.5, y: 1, w: 9, h: 4, barDir: "col",
|
||||
|
||||
// Custom colors (match your presentation palette)
|
||||
chartColors: ["0D9488", "14B8A6", "5EEAD4"],
|
||||
|
||||
// Clean background
|
||||
chartArea: { fill: { color: "FFFFFF" }, roundedCorners: true },
|
||||
|
||||
// Muted axis labels
|
||||
catAxisLabelColor: "64748B",
|
||||
valAxisLabelColor: "64748B",
|
||||
|
||||
// Subtle grid (value axis only)
|
||||
valGridLine: { color: "E2E8F0", size: 0.5 },
|
||||
catGridLine: { style: "none" },
|
||||
|
||||
// Data labels on bars
|
||||
showValue: true,
|
||||
dataLabelPosition: "outEnd",
|
||||
dataLabelColor: "1E293B",
|
||||
|
||||
// Hide legend for single series
|
||||
showLegend: false,
|
||||
});
|
||||
```
|
||||
|
||||
**Key styling options:**
|
||||
- `chartColors: [...]` - hex colors for series/segments
|
||||
- `chartArea: { fill, border, roundedCorners }` - chart background
|
||||
- `catGridLine/valGridLine: { color, style, size }` - grid lines (`style: "none"` to hide)
|
||||
- `lineSmooth: true` - curved lines (line charts)
|
||||
- `legendPos: "r"` - legend position: "b", "t", "l", "r", "tr"
|
||||
|
||||
---
|
||||
|
||||
## Slide Masters
|
||||
|
||||
```javascript
|
||||
pres.defineSlideMaster({
|
||||
title: 'TITLE_SLIDE', background: { color: '283A5E' },
|
||||
objects: [{
|
||||
placeholder: { options: { name: 'title', type: 'title', x: 1, y: 2, w: 8, h: 2 } }
|
||||
}]
|
||||
});
|
||||
|
||||
let titleSlide = pres.addSlide({ masterName: "TITLE_SLIDE" });
|
||||
titleSlide.addText("My Title", { placeholder: "title" });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
These issues cause file corruption, visual bugs, or broken output. Avoid them.
|
||||
|
||||
1. **NEVER use "#" with hex colors** - causes file corruption
|
||||
```javascript
|
||||
color: "FF0000" // CORRECT
|
||||
color: "#FF0000" // WRONG
|
||||
```
|
||||
|
||||
2. **NEVER encode opacity in hex color strings** - 8-char colors (e.g., `"00000020"`) corrupt the file. Use the `opacity` property instead.
|
||||
```javascript
|
||||
shadow: { type: "outer", blur: 6, offset: 2, color: "00000020" } // CORRUPTS FILE
|
||||
shadow: { type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.12 } // CORRECT
|
||||
```
|
||||
|
||||
3. **Use `bullet: true`** - NEVER unicode symbols like "o" (creates double bullets)
|
||||
|
||||
4. **Use `breakLine: true`** between array items or text runs together
|
||||
|
||||
5. **Avoid `lineSpacing` with bullets** - causes excessive gaps; use `paraSpaceAfter` instead
|
||||
|
||||
6. **Each presentation needs fresh instance** - don't reuse `pptxgen()` objects
|
||||
|
||||
7. **NEVER reuse option objects across calls** - PptxGenJS mutates objects in-place (e.g. converting shadow values to EMU). Sharing one object between multiple calls corrupts the second shape.
|
||||
```javascript
|
||||
const shadow = { type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.15 };
|
||||
slide.addShape(pres.shapes.RECTANGLE, { shadow, ... }); // second call gets already-converted values
|
||||
slide.addShape(pres.shapes.RECTANGLE, { shadow, ... });
|
||||
|
||||
const makeShadow = () => ({ type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.15 });
|
||||
slide.addShape(pres.shapes.RECTANGLE, { shadow: makeShadow(), ... }); // fresh object each time
|
||||
slide.addShape(pres.shapes.RECTANGLE, { shadow: makeShadow(), ... });
|
||||
```
|
||||
|
||||
8. **Don't use `ROUNDED_RECTANGLE` with accent borders** - rectangular overlay bars won't cover rounded corners. Use `RECTANGLE` instead.
|
||||
```javascript
|
||||
// WRONG: Accent bar doesn't cover rounded corners
|
||||
slide.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 1, y: 1, w: 3, h: 1.5, fill: { color: "FFFFFF" } });
|
||||
slide.addShape(pres.shapes.RECTANGLE, { x: 1, y: 1, w: 0.08, h: 1.5, fill: { color: "0891B2" } });
|
||||
|
||||
// CORRECT: Use RECTANGLE for clean alignment
|
||||
slide.addShape(pres.shapes.RECTANGLE, { x: 1, y: 1, w: 3, h: 1.5, fill: { color: "FFFFFF" } });
|
||||
slide.addShape(pres.shapes.RECTANGLE, { x: 1, y: 1, w: 0.08, h: 1.5, fill: { color: "0891B2" } });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
- **Shapes**: RECTANGLE, OVAL, LINE, ROUNDED_RECTANGLE
|
||||
- **Charts**: BAR, LINE, PIE, DOUGHNUT, SCATTER, BUBBLE, RADAR
|
||||
- **Layouts**: LAYOUT_16x9 (10"x5.625"), LAYOUT_16x10, LAYOUT_4x3, LAYOUT_WIDE
|
||||
- **Alignment**: "left", "center", "right"
|
||||
- **Chart data labels**: "outEnd", "inEnd", "center"
|
||||
@@ -0,0 +1,413 @@
|
||||
# Slide Page Types
|
||||
|
||||
Classify **every slide** as **exactly one** of these 5 types:
|
||||
|
||||
## 1. Cover Page
|
||||
|
||||
- **Use for**: Opening + tone setting
|
||||
- **Content**: Big title, subtitle/presenter, date/occasion, strong background/motif
|
||||
|
||||
### Layout Options
|
||||
|
||||
**Asymmetric Left-Right Layout**
|
||||
- Text concentrated on one side, image on the opposite
|
||||
- Best for: Corporate presentations, product launches, professional reports
|
||||
```
|
||||
| Title & Subtitle | Visual/Image |
|
||||
| Description | |
|
||||
```
|
||||
|
||||
**Center-Aligned Layout**
|
||||
- Content centered with background image
|
||||
- Best for: Inspirational talks, event presentations, creative pitches
|
||||
```
|
||||
| |
|
||||
| [Background Image] |
|
||||
| MAIN TITLE |
|
||||
| Subtitle |
|
||||
| |
|
||||
```
|
||||
|
||||
### Font Size Hierarchy
|
||||
|
||||
| Element | Recommended Size | Ratio to Base |
|
||||
|---------|-----------------|---------------|
|
||||
| Main Title | 72-120px | 3x-5x |
|
||||
| Subtitle | 28-40px | 1.5x-2x |
|
||||
| Supporting Text | 18-24px | 1x (base) |
|
||||
| Meta Info (date, name) | 14-18px | 0.7x-1x |
|
||||
|
||||
**Key Principles:**
|
||||
1. **Dramatic Contrast**: Main title should be at least 2-3x larger than subtitle
|
||||
2. **Visual Anchor**: The largest text becomes the focal point
|
||||
3. **Readable Hierarchy**: Viewers should instantly understand what's most important
|
||||
4. **Avoid Similarity**: Never let adjacent text elements be within 20% of each other's size
|
||||
|
||||
### Content Elements
|
||||
|
||||
1. **Main Title** — Always required, largest font
|
||||
2. **Subtitle** — When additional context is needed (clearly smaller than title)
|
||||
3. **Icons** — When they reinforce the theme
|
||||
4. **Date/Event Info** — When relevant (smallest text)
|
||||
5. **Company/Brand Logo** — When representing an organization
|
||||
6. **Presenter Name** — For keynotes (small, subtle)
|
||||
|
||||
### Design Decisions
|
||||
|
||||
Consider: Purpose (corporate/educational/creative), Audience, Tone, Content Volume, Visual Assets needed.
|
||||
|
||||
### Workflow
|
||||
|
||||
1. **Analyze**: Understand topic, audience, purpose
|
||||
2. **Choose Layout**: Select based on content
|
||||
3. **Write Slide**: Use PptxGenJS. Use shapes and SVG elements for visual interest.
|
||||
4. **Verify**: Generate preview as `slide-XX-preview.pptx`. Extract text with `python -m markitdown slide-XX-preview.pptx`, verify all content present and no placeholder text remains.
|
||||
|
||||
---
|
||||
|
||||
## 2. Table of Contents
|
||||
|
||||
- **Use for**: Navigation + expectation setting (3-5 sections)
|
||||
- **Content**: Section list (optional icons / page numbers)
|
||||
|
||||
### Layout Options
|
||||
|
||||
**Numbered Vertical List** — Best for 3-5 sections, straightforward presentations
|
||||
```
|
||||
| TABLE OF CONTENTS |
|
||||
| |
|
||||
| 01 Section Title One |
|
||||
| 02 Section Title Two |
|
||||
| 03 Section Title Three |
|
||||
```
|
||||
|
||||
**Two-Column Grid** — Best for 4-6 sections, content-rich presentations
|
||||
```
|
||||
| TABLE OF CONTENTS |
|
||||
| |
|
||||
| 01 Section One 02 Section Two |
|
||||
| Description Description |
|
||||
| 03 Section Three 04 Section Four |
|
||||
```
|
||||
|
||||
**Sidebar Navigation** — Best for 3-5 sections, modern/corporate
|
||||
```
|
||||
| ▌01 | Section Title One |
|
||||
| ▌02 | Section Title Two |
|
||||
| ▌03 | Section Title Three |
|
||||
```
|
||||
|
||||
**Card-Based** — Best for 3-4 sections, creative/modern
|
||||
```
|
||||
| TABLE OF CONTENTS |
|
||||
| ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ |
|
||||
| │ 01 │ │ 02 │ │ 03 │ │ 04 │ |
|
||||
| │Title│ │Title│ │Title│ │Title│ |
|
||||
| └─────┘ └─────┘ └─────┘ └─────┘ |
|
||||
```
|
||||
|
||||
### Font Size Hierarchy
|
||||
|
||||
| Element | Recommended Size | Ratio to Base |
|
||||
|---------|-----------------|---------------|
|
||||
| Page Title ("Table of Contents" / "Agenda") | 36-44px | 2.5x-3x |
|
||||
| Section Number | 28-36px | 2x-2.5x |
|
||||
| Section Title | 20-28px | 1.5x-2x |
|
||||
| Section Description | 14-16px | 1x (base) |
|
||||
|
||||
**Key Principles:**
|
||||
1. **Clear Numbering**: Section numbers should be visually prominent — bold, accent color, or larger size
|
||||
2. **Scannable Structure**: Viewer should scan all sections in 2-3 seconds
|
||||
3. **Consistent Spacing**: Equal vertical spacing between sections
|
||||
4. **Visual Markers**: Colored dots, lines, numbers, or icons to anchor each section
|
||||
5. **Avoid Clutter**: Descriptions one line max or omit entirely
|
||||
|
||||
### Content Elements
|
||||
|
||||
1. **Page Title** — Always required ("Table of Contents", "Agenda", "Overview")
|
||||
2. **Section Numbers** — Consistent format (01, 02... or I, II...)
|
||||
3. **Section Titles** — Clear and concise
|
||||
4. **Section Descriptions** — Optional one-line summaries
|
||||
5. **Visual Separators** — SVG dividers or spacing
|
||||
6. **Decorative Elements** — Subtle accent shapes
|
||||
7. **Page Number Badge** — **MANDATORY**
|
||||
|
||||
### Design Decisions
|
||||
|
||||
1. **Section Count**: 3 → vertical list; 4-6 → grid or compact; 7+ → multi-column
|
||||
2. **Description Length**: Long → vertical list; None → compact grid/cards
|
||||
3. **Tone**: Corporate → numbered list; Creative → card-based; Academic → Roman numerals
|
||||
4. **Consistency**: Match visual style of cover page
|
||||
|
||||
### Workflow
|
||||
|
||||
1. **Analyze**: Section list, count, presentation context
|
||||
2. **Choose Layout**: Based on section count and content
|
||||
3. **Plan Visual Hierarchy**: Numbering style, font sizes, spacing
|
||||
4. **Write Slide**: Use PptxGenJS. Use shapes for decorative elements. **MUST include page number badge.**
|
||||
5. **Verify**: Generate preview, extract text with markitdown, verify content and badge.
|
||||
|
||||
---
|
||||
|
||||
## 3. Section Divider
|
||||
|
||||
- **Use for**: Clear transitions between major parts
|
||||
- **Content**: Section number + title (+ optional 1-2 line intro)
|
||||
|
||||
### Layout Options
|
||||
|
||||
**Bold Center** — Best for minimal, modern presentations
|
||||
```
|
||||
| 02 |
|
||||
| SECTION TITLE |
|
||||
| Optional intro line |
|
||||
```
|
||||
|
||||
**Left-Aligned with Accent Block** — Best for corporate, structured presentations
|
||||
```
|
||||
| ████ | 02 |
|
||||
| ████ | SECTION TITLE |
|
||||
| ████ | Optional intro line |
|
||||
```
|
||||
|
||||
**Split Background** — Best for high-contrast, dramatic transitions
|
||||
```
|
||||
| ██████████ | SECTION TITLE |
|
||||
| ██ 02 ██ | Optional intro |
|
||||
| ██████████ | |
|
||||
```
|
||||
|
||||
**Full-Bleed Background with Overlay** — Best for creative, bold presentations
|
||||
```
|
||||
| ████████████████████████████████████ |
|
||||
| ████ large 02 █████████ |
|
||||
| ████ SECTION TITLE █████████ |
|
||||
| ████████████████████████████████████ |
|
||||
```
|
||||
|
||||
### Font Size Hierarchy
|
||||
|
||||
| Element | Recommended Size | Notes |
|
||||
|---------|-----------------|-------|
|
||||
| Section Number | 72-120px | Bold, accent color or semi-transparent |
|
||||
| Section Title | 36-48px | Bold, clear, primary text color |
|
||||
| Intro Text | 16-20px | Light weight, muted color, optional |
|
||||
|
||||
**Key Principles:**
|
||||
1. **Dramatic Number**: Section number = most prominent visual element
|
||||
2. **Strong Title**: Large but clearly secondary to the number
|
||||
3. **Minimal Content**: Just number + title + optional one-liner
|
||||
4. **Breathing Room**: Leave generous whitespace — dividers are pause moments
|
||||
|
||||
### Content Elements
|
||||
|
||||
1. **Section Number** — Always required. Format: `01`, `02`... or `I`, `II`... Match TOC style.
|
||||
2. **Section Title** — Always required. Clear, concise.
|
||||
3. **Intro Text** — Optional 1-2 line description.
|
||||
4. **Decorative Elements** — SVG accent shapes (bars, lines, geometric blocks).
|
||||
5. **Page Number Badge** — **MANDATORY**.
|
||||
|
||||
### Design Decisions
|
||||
|
||||
1. **Tone**: Corporate → accent block; Creative → full-bleed; Minimal → bold center
|
||||
2. **Color**: Strong palette color for background/accent; high-contrast text
|
||||
3. **Consistency**: Same divider style across all dividers in one presentation
|
||||
4. **Contrast with content slides**: Visually distinct (different background color, more whitespace)
|
||||
|
||||
### Workflow
|
||||
|
||||
1. **Analyze**: Section number, title, optional intro
|
||||
2. **Choose Layout**: Based on content and tone
|
||||
3. **Write Slide**: Use PptxGenJS. Use shapes for decorative elements. **MUST include page number badge.**
|
||||
4. **Verify**: Generate preview, extract text, verify content and badge.
|
||||
|
||||
---
|
||||
|
||||
## 4. Content Page
|
||||
|
||||
Pick a subtype based on the content. Each content slide belongs to exactly ONE subtype:
|
||||
|
||||
### Subtypes
|
||||
|
||||
**Text** — Bullets, quotes, or short paragraphs
|
||||
- Must still include icons or SVG shapes — never plain text only
|
||||
```
|
||||
| SLIDE TITLE |
|
||||
| * Bullet point one |
|
||||
| * Bullet point two |
|
||||
| * Bullet point three |
|
||||
```
|
||||
|
||||
**Mixed Media** — Two-column or half-bleed image + text
|
||||
```
|
||||
| SLIDE TITLE |
|
||||
| Text content | [Image/Visual] |
|
||||
| and bullets | |
|
||||
```
|
||||
|
||||
**Data Visualization** — Chart (SVG bar/progress/ring) + takeaways
|
||||
- Must include data source
|
||||
```
|
||||
| SLIDE TITLE |
|
||||
| [SVG Chart] | Key Takeaway 1 |
|
||||
| | Key Takeaway 2 |
|
||||
| Source: xxx |
|
||||
```
|
||||
|
||||
**Comparison** — Side-by-side columns or cards (A vs B, pros/cons)
|
||||
```
|
||||
| SLIDE TITLE |
|
||||
| ┌─ Option A ─┐ ┌─ Option B ─┐ |
|
||||
| │ Detail 1 │ │ Detail 1 │ |
|
||||
| └────────────┘ └────────────┘ |
|
||||
```
|
||||
|
||||
**Timeline / Process** — Steps with arrows, journey, phases
|
||||
```
|
||||
| SLIDE TITLE |
|
||||
| [1] ──→ [2] ──→ [3] ──→ [4] |
|
||||
| Step Step Step Step |
|
||||
```
|
||||
|
||||
**Image Showcase** — Hero image, gallery, visual-first layout
|
||||
```
|
||||
| SLIDE TITLE |
|
||||
| ┌────────────────────────────────┐ |
|
||||
| │ [Hero Image] │ |
|
||||
| └────────────────────────────────┘ |
|
||||
| Caption or supporting text |
|
||||
```
|
||||
|
||||
### Font Size Hierarchy
|
||||
|
||||
| Element | Recommended Size | Notes |
|
||||
|---------|-----------------|-------|
|
||||
| Slide Title | 36-44px | Bold, top of slide |
|
||||
| Section Header | 20-24px | Bold, for sub-sections within slide |
|
||||
| Body Text | 14-16px | Regular weight, left-aligned |
|
||||
| Captions / Source | 10-12px | Muted color, smallest text |
|
||||
| Stat Callout | 60-72px | Large bold numbers for key statistics |
|
||||
|
||||
**Key Principles:**
|
||||
1. **Left-align body text** — never center paragraphs or bullet lists
|
||||
2. **Size contrast** — title must be 36pt+ to stand out from 14-16pt body
|
||||
3. **Visual elements required** — every content slide must have at least one non-text element
|
||||
4. **Breathing room** — 0.5" minimum margins, 0.3-0.5" between content blocks
|
||||
|
||||
### Content Elements
|
||||
|
||||
1. **Slide Title** — Always required, top of slide
|
||||
2. **Body Content** — Text, bullets, data, or comparisons based on subtype
|
||||
3. **Visual Element** — Image, chart, icon, or SVG shape — always required
|
||||
4. **Source / Caption** — When showing data or external content
|
||||
5. **Page Number Badge** — **MANDATORY**
|
||||
|
||||
### Design Decisions
|
||||
|
||||
1. **Subtype**: Determine first — drives the entire layout
|
||||
2. **Content Volume**: Dense → multi-column or smaller font; Light → larger elements with more whitespace
|
||||
3. **Data vs Narrative**: Data-heavy → charts + stat callouts; Story-driven → images + quotes
|
||||
4. **Variety**: Each content slide should use a different layout from the previous one
|
||||
5. **Consistency**: Typography, colors, and spacing must match the rest of the presentation
|
||||
|
||||
### Workflow
|
||||
|
||||
1. **Analyze**: Content, determine subtype, plan layout
|
||||
2. **Choose Layout**: Best fit for subtype and content volume
|
||||
3. **Write Slide**: Use PptxGenJS. Use shapes for charts, decorative elements, icons. **MUST include page number badge.**
|
||||
4. **Verify**: Generate preview as `slide-XX-preview.pptx`. Extract text with markitdown, verify all content present, no placeholder text, badge included.
|
||||
|
||||
---
|
||||
|
||||
## 5. Summary / Closing Page
|
||||
|
||||
- **Use for**: Wrap-up + action
|
||||
- **Content**: Key takeaways, CTA/next steps, contact/QR, thank-you
|
||||
|
||||
### Layout Options
|
||||
|
||||
**Key Takeaways** — Best for educational, corporate, data-driven presentations
|
||||
```
|
||||
| KEY TAKEAWAYS |
|
||||
| ✓ Takeaway one |
|
||||
| ✓ Takeaway two |
|
||||
| ✓ Takeaway three |
|
||||
```
|
||||
|
||||
**CTA / Next Steps** — Best for sales pitches, proposals, project kick-offs
|
||||
```
|
||||
| NEXT STEPS |
|
||||
| [1] Action item one |
|
||||
| [2] Action item two |
|
||||
| Contact: email@example.com |
|
||||
```
|
||||
|
||||
**Thank You / Contact** — Best for conference talks, keynotes
|
||||
```
|
||||
| THANK YOU |
|
||||
| name@company.com |
|
||||
| @handle | website.com |
|
||||
```
|
||||
|
||||
**Split Recap** — Best for presentations needing both recap and action
|
||||
```
|
||||
| SUMMARY | NEXT STEPS |
|
||||
| * Point one | Contact us at |
|
||||
| * Point two | email@co.com |
|
||||
| * Point three | [QR Code] |
|
||||
```
|
||||
|
||||
### Font Size Hierarchy
|
||||
|
||||
| Element | Recommended Size | Notes |
|
||||
|---------|-----------------|-------|
|
||||
| Closing Title ("Thank You" / "Summary") | 48-72px | Bold, commanding |
|
||||
| Takeaway / Action Item | 18-24px | Clear, scannable |
|
||||
| Supporting Text | 14-16px | Regular weight |
|
||||
| Contact Info | 14-16px | Muted color |
|
||||
|
||||
**Key Principles:**
|
||||
1. **Strong closing statement**: Main message should be largest, most prominent
|
||||
2. **Scannable items**: Takeaways/action items concise (one line each)
|
||||
3. **Contact clarity**: Legible but not dominant
|
||||
4. **Memorable finish**: Confident, polished ending
|
||||
|
||||
### Content Elements
|
||||
|
||||
1. **Closing Title** — Always required
|
||||
2. **Takeaway Points** — 3-5 concise summary points (if applicable)
|
||||
3. **Call to Action** — Clear next steps (if applicable)
|
||||
4. **Contact Info** — Email, website, social handles (if provided)
|
||||
5. **Decorative Elements** — SVG accents for visual consistency
|
||||
6. **Page Number Badge** — **MANDATORY**
|
||||
|
||||
### Design Decisions
|
||||
|
||||
1. **Closing Type**: Recap, CTA, thank-you, or combination
|
||||
2. **Content Volume**: Many takeaways → list; Simple closing → centered thank-you
|
||||
3. **Audience Action**: Audience needs to do something → CTA; Informational → takeaways
|
||||
4. **Tone Consistency**: Match energy of cover page
|
||||
5. **Visual Distinction**: Special but not disconnected from the rest
|
||||
|
||||
### Workflow
|
||||
|
||||
1. **Analyze**: Closing content — takeaways, CTA, contact, thank-you
|
||||
2. **Choose Layout**: Based on content type
|
||||
3. **Write Slide**: Use PptxGenJS. Use shapes for decorative elements. **MUST include page number badge.**
|
||||
4. **Verify**: Generate preview, extract text, verify content and badge.
|
||||
|
||||
---
|
||||
|
||||
## Additional Layout Patterns
|
||||
|
||||
Use these across content slides for visual variety:
|
||||
|
||||
- **Two-column** (text left, illustration right)
|
||||
- **Icon + text rows** (icon in colored circle, bold header, description below)
|
||||
- **2x2 or 2x3 grid** (image on one side, grid of content blocks on other)
|
||||
- **Half-bleed image** (full left or right side) with content overlay
|
||||
- **Large stat callouts** (big numbers 60-72pt with small labels below)
|
||||
- **Comparison columns** (before/after, pros/cons)
|
||||
- **Timeline or process flow** (numbered steps, arrows)
|
||||
- **Icons in small colored circles** next to section headers
|
||||
- **Italic accent text** for key stats or taglines
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: wechat-article-writer
|
||||
description: 公众号文章自动化写作流程。支持资料搜索、文章撰写、爆款标题生成、排版优化。当用户提到写公众号、微信文章、自媒体写作、爆款文章、内容创作时使用此 skill。
|
||||
---
|
||||
|
||||
# 公众号文章写作流程
|
||||
|
||||
4 步完成高质量公众号文章:搜索资料 → 撰写文章 → 生成标题 → 排版优化
|
||||
|
||||
## Step 1: 搜索资料
|
||||
|
||||
使用 WebSearch 工具搜索主题相关资料:
|
||||
|
||||
```
|
||||
搜索要求:
|
||||
1. 并行搜索多个来源(官方文档、X/Twitter、Reddit、技术论坛)
|
||||
2. 优先获取最新资料(当月/当季)
|
||||
3. 可启动多个并行 Task 加速搜索
|
||||
4. 深度总结搜索结果
|
||||
```
|
||||
|
||||
## Step 2: 撰写文章
|
||||
|
||||
**必须先读取用户的 CLAUDE.md 获取写作风格**
|
||||
|
||||
撰写要求:
|
||||
- 5000-10000 字
|
||||
- 故事化开头,带情感色彩(兴奋/焦虑/好奇)
|
||||
- 准备 2-3 个备选标题
|
||||
- 结构:效果展示 → 问题描述 → 步骤教学 → 升华总结
|
||||
- 遵循 CLAUDE.md 中定义的写作风格和结尾语
|
||||
|
||||
## Step 3: 生成标题
|
||||
|
||||
生成 5 个爆款标题,特点:
|
||||
- **痛点明确**:直击读者痛处("还在手动...")
|
||||
- **数字吸引**:具体数字更有说服力("3分钟"、"5个技巧")
|
||||
- **结果导向**:承诺具体收益("效率暴涨10倍")
|
||||
- **情绪调动**:惊、神技、秘籍等词汇
|
||||
- **悬念设置**:引发好奇心
|
||||
|
||||
## Step 4: 排版优化
|
||||
|
||||
优化建议:
|
||||
1. **段落结构**:每段 3-5 行,重要数据单独成段加粗
|
||||
2. **配图位置**:标题下方封面图、步骤后效果图、结尾总结图
|
||||
3. **代码块**:前后留白,语法高亮
|
||||
4. **金句**:单独成段,增强记忆点
|
||||
|
||||
## 示例
|
||||
|
||||
用户请求:
|
||||
```
|
||||
帮我写一篇关于 Claude Code 使用技巧的公众号文章
|
||||
```
|
||||
|
||||
执行流程:
|
||||
1. 搜索 Claude Code 最新功能和使用技巧
|
||||
2. 读取 CLAUDE.md 获取写作风格
|
||||
3. 撰写 1200 字左右的教程文章
|
||||
4. 生成 5 个爆款标题供选择
|
||||
5. 提供排版和配图建议
|
||||
@@ -0,0 +1,157 @@
|
||||
# Writing Clearly and Concisely
|
||||
|
||||
A skill that applies William Strunk Jr.'s timeless writing principles to produce clearer, stronger, more professional prose while avoiding common AI writing patterns.
|
||||
|
||||
## Purpose
|
||||
|
||||
This skill helps you write better prose for human readers. It draws from two sources:
|
||||
|
||||
1. **The Elements of Style** (Strunk, 1918) - Time-tested rules for clear, forceful writing
|
||||
2. **AI Pattern Avoidance** - Research-backed guidance on avoiding generic, puffy language that LLMs tend to produce
|
||||
|
||||
Whether you're writing documentation, commit messages, error messages, or any text humans will read, this skill helps you cut fluff and say what you mean.
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this skill whenever you write prose for humans:
|
||||
|
||||
- **Documentation** - README files, API docs, technical explanations
|
||||
- **Git workflow** - Commit messages, pull request descriptions
|
||||
- **User-facing text** - Error messages, UI copy, help text, tooltips
|
||||
- **Code comments** - Inline documentation, docstrings
|
||||
- **Reports and summaries** - Status updates, analysis, explanations
|
||||
- **Editing** - Improving clarity of existing text
|
||||
|
||||
**Trigger phrases:**
|
||||
- "Write documentation for..."
|
||||
- "Draft a README"
|
||||
- "Edit this for clarity"
|
||||
- "Make this more concise"
|
||||
- "Review this commit message"
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Load the skill** when writing prose for human readers
|
||||
2. **Apply Strunk's core principles** - active voice, positive form, concrete language, cut needless words
|
||||
3. **Avoid AI patterns** - no puffery, no empty phrases, no promotional adjectives
|
||||
4. **Reference detailed guides** when needed for specific rules
|
||||
|
||||
### Context-Efficient Approach
|
||||
|
||||
The skill uses progressive disclosure to save context:
|
||||
|
||||
- **SKILL.md** (~1,000 tokens) loads first with the core rules
|
||||
- **Reference files** (1,000-4,500 tokens each) load only when needed
|
||||
- **Most tasks need only one file**: `03-elementary-principles-of-composition.md`
|
||||
|
||||
For tight context situations, dispatch a subagent with your draft and the relevant section file.
|
||||
|
||||
## Key Features
|
||||
|
||||
### Strunk's Core Rules
|
||||
|
||||
The skill emphasizes these principles from *The Elements of Style*:
|
||||
|
||||
| Rule | Principle |
|
||||
|------|-----------|
|
||||
| 10 | Use active voice |
|
||||
| 11 | Put statements in positive form |
|
||||
| 12 | Use definite, specific, concrete language |
|
||||
| 13 | Omit needless words |
|
||||
| 16 | Keep related words together |
|
||||
| 18 | Place emphatic words at end of sentence |
|
||||
|
||||
### AI Pattern Detection
|
||||
|
||||
The skill identifies and eliminates common LLM writing patterns:
|
||||
|
||||
- **Puffery**: pivotal, crucial, vital, testament, enduring legacy
|
||||
- **Empty "-ing" phrases**: ensuring reliability, showcasing features
|
||||
- **Promotional adjectives**: groundbreaking, seamless, robust, cutting-edge
|
||||
- **Overused AI vocabulary**: delve, leverage, multifaceted, foster, realm, tapestry
|
||||
- **Formatting overuse**: excessive bullets, emoji decorations, bold on every other word
|
||||
|
||||
## Reference Files
|
||||
|
||||
| Section | File | Tokens | Content |
|
||||
|---------|------|--------|---------|
|
||||
| Grammar & punctuation | `02-elementary-rules-of-usage.md` | ~2,500 | Comma rules, possessives, sentence structure |
|
||||
| Composition principles | `03-elementary-principles-of-composition.md` | ~4,500 | Active voice, concision, paragraph structure |
|
||||
| Formatting | `04-a-few-matters-of-form.md` | ~1,000 | Headings, quotations, formatting conventions |
|
||||
| Word choice | `05-words-and-expressions-commonly-misused.md` | ~4,000 | Common errors, word selection |
|
||||
| AI patterns | `signs-of-ai-writing.md` | ~25,000 | Wikipedia editors' field guide to AI detection |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: Tightening a Commit Message
|
||||
|
||||
**Before:**
|
||||
> This commit implements the functionality for ensuring that user authentication is properly handled, showcasing robust error handling capabilities.
|
||||
|
||||
**After:**
|
||||
> Add user authentication with error handling
|
||||
|
||||
### Example 2: Rewriting Documentation
|
||||
|
||||
**Before:**
|
||||
> This groundbreaking feature leverages cutting-edge technology to deliver a seamless experience, fostering better engagement and driving impactful results.
|
||||
|
||||
**After:**
|
||||
> This feature uses WebSocket connections to update the dashboard in real time.
|
||||
|
||||
### Example 3: Fixing Passive Voice
|
||||
|
||||
**Before:**
|
||||
> The configuration file is read by the application at startup.
|
||||
|
||||
**After:**
|
||||
> The application reads the configuration file at startup.
|
||||
|
||||
### Example 4: Removing Hedging
|
||||
|
||||
**Before:**
|
||||
> It is important to note that the API might potentially return an error in certain situations.
|
||||
|
||||
**After:**
|
||||
> The API returns an error when the token expires.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Be specific, not grandiose** - Say what it actually does, not how important it is
|
||||
2. **Cut first, add later** - Remove words until meaning suffers, then add back what's needed
|
||||
3. **Prefer active voice** - "The function returns X" beats "X is returned by the function"
|
||||
4. **State positively** - "He forgot" beats "He did not remember"
|
||||
5. **Use concrete language** - "The server crashed" beats "An issue occurred"
|
||||
6. **Load reference files sparingly** - Most tasks need only `03-elementary-principles-of-composition.md`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
writing-clearly-and-concisely/
|
||||
SKILL.md # Main skill definition
|
||||
README.md # This file
|
||||
signs-of-ai-writing.md # AI pattern detection guide
|
||||
elements-of-style/
|
||||
01-introductory.md
|
||||
02-elementary-rules-of-usage.md
|
||||
03-elementary-principles-of-composition.md
|
||||
04-a-few-matters-of-form.md
|
||||
05-words-and-expressions-commonly-misused.md
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
**Claude Code:**
|
||||
```bash
|
||||
cp -r skills/writing-clearly-and-concisely ~/.claude/skills/
|
||||
```
|
||||
|
||||
**Claude.ai:**
|
||||
Add the skill to project knowledge or paste SKILL.md contents into your conversation.
|
||||
|
||||
## Attribution
|
||||
|
||||
- Original skill by @joshuadavidthomas from [joshuadavidthomas/agent-skills](https://github.com/joshuadavidthomas/agent-skills) (MIT)
|
||||
- Adapted from [obra/the-elements-of-style](https://github.com/obra/the-elements-of-style)
|
||||
- Writing principles from *The Elements of Style* by William Strunk Jr. (1918)
|
||||
- AI pattern research from Wikipedia's field guide to AI-generated content detection
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: writing-clearly-and-concisely
|
||||
description: Use when writing prose humans will read—documentation, commit messages, error messages, explanations, reports, or UI text. Applies Strunk's timeless rules for clearer, stronger, more professional writing.
|
||||
---
|
||||
|
||||
# Writing Clearly and Concisely
|
||||
|
||||
## Overview
|
||||
|
||||
Write with clarity and force. This skill covers what to do (Strunk) and what not to do (AI patterns).
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill whenever you write prose for humans:
|
||||
|
||||
- Documentation, README files, technical explanations
|
||||
- Commit messages, pull request descriptions
|
||||
- Error messages, UI copy, help text, comments
|
||||
- Reports, summaries, or any explanation
|
||||
- Editing to improve clarity
|
||||
|
||||
**If you're writing sentences for a human to read, use this skill.**
|
||||
|
||||
## Limited Context Strategy
|
||||
|
||||
When context is tight:
|
||||
|
||||
1. Write your draft using judgment
|
||||
2. Dispatch a subagent with your draft and the relevant section file
|
||||
3. Have the subagent copyedit and return the revision
|
||||
|
||||
Loading a single section (~1,000-4,500 tokens) instead of everything saves significant context.
|
||||
|
||||
## Elements of Style
|
||||
|
||||
William Strunk Jr.'s *The Elements of Style* (1918) teaches you to write clearly and cut ruthlessly.
|
||||
|
||||
### Rules
|
||||
|
||||
**Elementary Rules of Usage (Grammar/Punctuation)**:
|
||||
|
||||
1. Form possessive singular by adding 's
|
||||
2. Use comma after each term in series except last
|
||||
3. Enclose parenthetic expressions between commas
|
||||
4. Comma before conjunction introducing co-ordinate clause
|
||||
5. Don't join independent clauses by comma
|
||||
6. Don't break sentences in two
|
||||
7. Participial phrase at beginning refers to grammatical subject
|
||||
|
||||
**Elementary Principles of Composition**:
|
||||
|
||||
8. One paragraph per topic
|
||||
9. Begin paragraph with topic sentence
|
||||
10. **Use active voice**
|
||||
11. **Put statements in positive form**
|
||||
12. **Use definite, specific, concrete language**
|
||||
13. **Omit needless words**
|
||||
14. Avoid succession of loose sentences
|
||||
15. Express co-ordinate ideas in similar form
|
||||
16. **Keep related words together**
|
||||
17. Keep to one tense in summaries
|
||||
18. **Place emphatic words at end of sentence**
|
||||
|
||||
### Reference Files
|
||||
|
||||
The rules above are summarized from Strunk's original text. For complete explanations with examples:
|
||||
|
||||
| Section | File | ~Tokens |
|
||||
|---------|------|---------|
|
||||
| Grammar, punctuation, comma rules | `02-elementary-rules-of-usage.md` | 2,500 |
|
||||
| Paragraph structure, active voice, concision | `03-elementary-principles-of-composition.md` | 4,500 |
|
||||
| Headings, quotations, formatting | `04-a-few-matters-of-form.md` | 1,000 |
|
||||
| Word choice, common errors | `05-words-and-expressions-commonly-misused.md` | 4,000 |
|
||||
|
||||
**Most tasks need only `03-elementary-principles-of-composition.md`** — it covers active voice, positive form, concrete language, and omitting needless words.
|
||||
|
||||
## AI Writing Patterns to Avoid
|
||||
|
||||
LLMs regress to statistical means, producing generic, puffy prose. Avoid:
|
||||
|
||||
- **Puffery:** pivotal, crucial, vital, testament, enduring legacy
|
||||
- **Empty "-ing" phrases:** ensuring reliability, showcasing features, highlighting capabilities
|
||||
- **Promotional adjectives:** groundbreaking, seamless, robust, cutting-edge
|
||||
- **Overused AI vocabulary:** delve, leverage, multifaceted, foster, realm, tapestry
|
||||
- **Formatting overuse:** excessive bullets, emoji decorations, bold on every other word
|
||||
|
||||
Be specific, not grandiose. Say what it actually does.
|
||||
|
||||
For comprehensive research on why these patterns occur, see `signs-of-ai-writing.md`. Wikipedia editors developed this guide to detect AI-generated submissions — their patterns are well-documented and field-tested.
|
||||
|
||||
## Bottom Line
|
||||
|
||||
Writing for humans? Load the relevant section from `elements-of-style/` and apply the rules. For most tasks, `03-elementary-principles-of-composition.md` covers what matters most.
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
## I. Introductory
|
||||
|
||||
This handbook summarizes the essentials of plain English style. It focuses on the rules of usage and principles of composition most often broken, offering a compact alternative to exhaustive manuals. Master the guidance here, then look to the best authors for finer points of style.
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
## II. Elementary Rules Of Usage
|
||||
|
||||
### Rule 1. Form the possessive singular of nouns by adding 's.
|
||||
|
||||
Follow this rule whatever the final consonant. Thus write,
|
||||
|
||||
Charles's friend
|
||||
|
||||
Burns's poems
|
||||
|
||||
the witch's malice
|
||||
|
||||
This is the usage of the United States Government Printing Office and of the Oxford University Press.
|
||||
|
||||
Exceptions are the possessive of ancient proper names ending in *-es* and *-is*, the possessive *Jesus'*, and such forms as *for conscience' sake*, *for righteousness' sake*. But such forms as *Achilles' heel*, *Moses' laws*, *Isis' temple* are commonly replaced by
|
||||
|
||||
the heel of Achilles
|
||||
|
||||
the laws of Moses
|
||||
|
||||
the temple of Isis
|
||||
|
||||
The pronominal possessives *hers*, *its*, *theirs*, *yours*, and *oneself* have no apostrophe.
|
||||
|
||||
### Rule 2. In a series of three or more terms with a single conjunction, use a comma after each term except the last.
|
||||
|
||||
Thus write,
|
||||
|
||||
red, white, and blue
|
||||
|
||||
gold, silver, or copper
|
||||
|
||||
He opened the letter, read it, and made a note of its contents.
|
||||
|
||||
This is also the usage of the Government Printing Office and of the Oxford University Press.
|
||||
|
||||
In the names of business firms the last comma is omitted, as,
|
||||
|
||||
Brown, Shipley & Co.
|
||||
|
||||
### Rule 3. Enclose parenthetic expressions between commas.
|
||||
|
||||
The best way to see a country, unless you are pressed for time, is to travel on foot.
|
||||
|
||||
This rule is difficult to apply; it is frequently hard to decide whether a single word, such as *however*, or a brief phrase, is or is not parenthetic. If the interruption to the flow of the sentence is but slight, the writer may safely omit the commas. But whether the interruption be slight or considerable, he must never insert one comma and omit the other. Such punctuation as
|
||||
|
||||
Marjorie's husband, Colonel Nelson paid us a visit yesterday,
|
||||
|
||||
or
|
||||
|
||||
My brother you will be pleased to hear, is now in perfect health,
|
||||
|
||||
is indefensible.
|
||||
|
||||
If a parenthetic expression is preceded by a conjunction, place the first comma before the conjunction, not after it.
|
||||
|
||||
He saw us coming, and unaware that we had learned of his treachery, greeted us with a smile.
|
||||
|
||||
Always to be regarded as parenthetic and to be enclosed between commas (or, at the end of the sentence, between comma and period) are the following:
|
||||
|
||||
\(1\) the year, when forming part of a date, and the day of the month, when following the day of the week:
|
||||
|
||||
February to July, 1916.
|
||||
|
||||
April 6, 1917.
|
||||
|
||||
Monday, November 11, 1918.
|
||||
|
||||
\(2\) the abbreviations *etc.* and *jr.*
|
||||
|
||||
\(3\) non-restrictive relative clauses, that is, those which do not serve to identify or define the antecedent noun, and similar clauses introduced by conjunctions indicating time or place.
|
||||
|
||||
The audience, which had at first been indifferent, became more and more interested.
|
||||
|
||||
In this sentence the clause introduced by *which* does not serve to tell which of several possible audiences is meant; what audience is in question is supposed to be already known. The clause adds, parenthetically, a statement supplementing that in the main clause. The sentence is virtually a combination of two statements which might have been made independently:
|
||||
|
||||
The audience had at first been indifferent. It became more and more interested.
|
||||
|
||||
Compare the restrictive relative clause, not set off by commas, in the sentence,
|
||||
|
||||
The candidate who best meets these requirements will obtain the place.
|
||||
|
||||
Here the clause introduced by *who* does serve to tell which of several possible candidates is meant; the sentence cannot be split up into two independent statements.
|
||||
|
||||
The difference in punctuation in the two sentences following is based on the same principle:
|
||||
|
||||
Nether Stowey, where Coleridge wrote The Rime of the Ancient Mariner, is a few miles from Bridgewater.
|
||||
|
||||
The day will come when you will admit your mistake.
|
||||
|
||||
Nether Stowey is completely identified by its name; the statement about Coleridge is therefore supplementary and parenthetic. The *day* spoken of is identified only by the dependent clause, which is therefore restrictive.
|
||||
|
||||
Similar in principle to the enclosing of parenthetic expressions between commas is the setting off by commas of phrases or dependent clauses preceding or following the main clause of a sentence.
|
||||
|
||||
Partly by hard fighting, partly by diplomatic skill, they enlarged their dominions to the east, and rose to royal rank with the possession of Sicily, exchanged afterwards for Sardinia.
|
||||
|
||||
Other illustrations may be found in sentences quoted under Rules 4, 5, 6, 7, 16, and 18.
|
||||
|
||||
The writer should be careful not to set off independent clauses by commas: see under Rule 5.
|
||||
|
||||
### Rule 4. Place a comma before a conjunction introducing a co-ordinate clause.
|
||||
|
||||
The early records of the city have disappeared, and the story of its first years can no longer be reconstructed.
|
||||
|
||||
The situation is perilous, but there is still one chance of escape.
|
||||
|
||||
Sentences of this type, isolated from their context, may seem to be in need of rewriting. As they make complete sense when the comma is reached, the second clause has the appearance of an afterthought. Further, *and* is the least specific of connectives. Used between independent clauses, it indicates only that a relation exists between them without defining that relation. In the example above, the relation is that of cause and result. The two sentences might be rewritten:
|
||||
|
||||
As the early records of the city have disappeared, the story of its first years can no longer be reconstructed.
|
||||
|
||||
Although the situation is perilous, there is still one chance of escape.
|
||||
|
||||
Or the subordinate clauses might be replaced by phrases:
|
||||
|
||||
Owing to the disappearance of the early records of the city, the story of its first years can no longer be reconstructed.
|
||||
|
||||
In this perilous situation, there is still one chance of escape.
|
||||
|
||||
But a writer may err by making his sentences too uniformly compact and periodic, and an occasional loose sentence prevents the style from becoming too formal and gives the reader a certain relief. Consequently, loose sentences of the type first quoted are common in easy, unstudied writing. But a writer should be careful not to construct too many of his sentences after this pattern (see Rule 14).
|
||||
|
||||
Two-part sentences of which the second member is introduced by *as* (in the sense of *because*), *for*, *or*, *nor*, and *while* (in the sense of *and at the same time*) likewise require a comma before the conjunction.
|
||||
|
||||
If the second member is introduced by an adverb, a semicolon, not a comma, is required (see Rule 5). The connectives *so* and *yet* may be used either as adverbs or as conjunctions, accordingly as the second clause is felt to be co-ordinate or subordinate; consequently either mark of punctuation may be justified. But these uses of *so* (equivalent to *accordingly* or to *so that*) are somewhat colloquial and should, as a rule, be avoided in writing. A simple correction, usually serviceable, is to omit the word *so* and begin the first clause with *as* or *since*:
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| I had never been in the place before; so I had difficulty in finding my way about. | As I had never been in the place before, I had difficulty in finding my way about. |
|
||||
|
||||
If a dependent clause, or an introductory phrase requiring to be set off by a comma, precedes the second independent clause, no comma is needed after the conjunction.
|
||||
|
||||
The situation is perilous, but if we are prepared to act promptly, there is still one chance of escape.
|
||||
|
||||
When the subject is the same for both clauses and is expressed only once, a comma is required if the connective is *but*. If the connective is *and*, the comma should be omitted if the relation between the two statements is close or immediate.
|
||||
|
||||
I have heard his arguments, but am still unconvinced.
|
||||
|
||||
He has had several years' experience and is thoroughly competent.
|
||||
|
||||
### Rule 5. Do not join independent clauses by a comma.
|
||||
|
||||
If two or more clauses, grammatically complete and not joined by a conjunction, are to form a single compound sentence, the proper mark of punctuation is a semicolon.
|
||||
|
||||
Stevenson's romances are entertaining; they are full of exciting adventures.
|
||||
|
||||
It is nearly half past five; we cannot reach town before dark.
|
||||
|
||||
It is of course equally correct to write the above as two sentences each, replacing the semicolons by periods.
|
||||
|
||||
Stevenson's romances are entertaining. They are full of exciting adventures.
|
||||
|
||||
It is nearly half past five. We cannot reach town before dark.
|
||||
|
||||
If a conjunction is inserted the proper mark is a comma (Rule 4).
|
||||
|
||||
Stevenson's romances are entertaining, for they are full of exciting adventures.
|
||||
|
||||
It is nearly half past five, and we cannot reach town before dark.
|
||||
|
||||
A comparison of the three forms given above will show clearly the advantage of the first. It is, at least in the examples given, better than the second form, because it suggests the close relationship between the two statements in a way that the second does not attempt, and better than the third, because briefer and therefore more forcible. Indeed it may be said that this simple method of indicating relationship between statements is one of the most useful devices of composition. The relationship, as above, is commonly one of cause or of consequence.
|
||||
|
||||
Note that if the second clause is preceded by an adverb, such as *accordingly*, *besides*, *then*, *therefore*, or *thus*, and not by a conjunction, the semicolon is still required.
|
||||
|
||||
Two exceptions to the rule may be admitted. If the clauses are very short, and are alike in form, a comma is usually permissible:
|
||||
|
||||
Man proposes, God disposes.
|
||||
|
||||
The gate swung apart, the bridge fell, the portcullis was drawn up.
|
||||
|
||||
Note that in these examples the relation is not one of cause or consequence. Also in the colloquial form of expression,
|
||||
|
||||
I hardly knew him, he was so changed,
|
||||
|
||||
a comma, not a semicolon, is required. But this form of expression is inappropriate in writing, except in the dialogue of a story or play, or perhaps in a familiar letter.
|
||||
|
||||
### Rule 6. Do not break sentences in two.
|
||||
|
||||
In other words, do not use periods for commas.
|
||||
|
||||
I met them on a Cunard liner several years ago. Coming home from Liverpool to New York.
|
||||
|
||||
He was an interesting talker. A man who had traveled all over the world and lived in half a dozen countries.
|
||||
|
||||
In both these examples, the first period should be replaced by a comma, and the following word begun with a small letter.
|
||||
|
||||
It is permissible to make an emphatic word or expression serve the purpose of a sentence and to punctuate it accordingly:
|
||||
|
||||
Again and again he called out. No reply.
|
||||
|
||||
The writer must, however, be certain that the emphasis is warranted, and that he will not be suspected of a mere blunder in syntax or in punctuation.
|
||||
|
||||
Rules 3, 4, 5, and 6 cover the most important principles in the punctuation of ordinary sentences; they should be so thoroughly mastered that their application becomes second nature.
|
||||
|
||||
### Rule 7. A participial phrase at the beginning of a sentence must refer to the grammatical subject.
|
||||
|
||||
Walking slowly down the road, he saw a woman accompanied by two children.
|
||||
|
||||
The word *walking* refers to the subject of the sentence, not to the woman. If the writer wishes to make it refer to the woman, he must recast the sentence:
|
||||
|
||||
He saw a woman accompanied by two children, walking slowly down the road.
|
||||
|
||||
Participial phrases preceded by a conjunction or by a preposition, nouns in apposition, adjectives, and adjective phrases come under the same rule if they begin the sentence.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| On arriving in Chicago, his friends met him at the station. | When he arrived (or, On his arrival) in Chicago, his friends met him at the station. |
|
||||
| A soldier of proved valor, they entrusted him with the defence of the city. | A soldier of proved valor, he was entrusted with the defence of the city. |
|
||||
| Young and inexperienced, the task seemed easy to me. | Young and inexperienced, I thought the task easy. |
|
||||
| Without a friend to counsel him, the temptation proved irresistible. | Without a friend to counsel him, he found the temptation irresistible. |
|
||||
|
||||
Sentences violating this rule are often ludicrous.
|
||||
|
||||
Being in a dilapidated condition, I was able to buy the house very cheap.
|
||||
|
||||
Wondering irresolutely what to do next, the clock struck twelve.
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
## III. Elementary Principles Of Composition
|
||||
|
||||
### Rule 8. Make the paragraph the unit of composition: one paragraph to each topic.
|
||||
|
||||
If the subject on which you are writing is of slight extent, or if you intend to treat it very briefly, there may be no need of subdividing it into topics. Thus a brief description, a brief summary of a literary work, a brief account of a single incident, a narrative merely outlining an action, the setting forth of a single idea, any one of these is best written in a single paragraph. After the paragraph has been written, examine it to see whether subdivision will not improve it.
|
||||
|
||||
Ordinarily, however, a subject requires subdivision into topics, each of which should be made the subject of a paragraph. The object of treating each topic in a paragraph by itself is, of course, to aid the reader. The beginning of each paragraph is a signal to him that a new step in the development of the subject has been reached.
|
||||
|
||||
The extent of subdivision will vary with the length of the composition. For example, a short notice of a book or poem might consist of a single paragraph. One slightly longer might consist of two paragraphs:
|
||||
|
||||
- A. Account of the work.
|
||||
- B. Critical discussion.
|
||||
|
||||
A report on a poem, written for a class in literature, might consist of seven paragraphs:
|
||||
|
||||
- A. Facts of composition and publication.
|
||||
- B. Kind of poem; metrical form.
|
||||
- C. Subject.
|
||||
- D. Treatment of subject.
|
||||
- E. For what chiefly remarkable.
|
||||
- F. Wherein characteristic of the writer.
|
||||
- G. Relationship to other works.
|
||||
|
||||
The contents of paragraphs C and D would vary with the poem. Usually, paragraph C would indicate the actual or imagined circumstances of the poem (the situation), if these call for explanation, and would then state the subject and outline its development. If the poem is a narrative in the third person throughout, paragraph C need contain no more than a concise summary of the action. Paragraph D would indicate the leading ideas and show how they are made prominent, or would indicate what points in the narrative are chiefly emphasized.
|
||||
|
||||
A novel might be discussed under the heads:
|
||||
|
||||
- A. Setting.
|
||||
- B. Plot.
|
||||
- C. Characters.
|
||||
- D. Purpose.
|
||||
|
||||
An historical event might be discussed under the heads:
|
||||
|
||||
- A. What led up to the event.
|
||||
- B. Account of the event.
|
||||
- C. What the event led up to.
|
||||
|
||||
In treating either of these last two subjects, the writer would probably find it necessary to subdivide one or more of the topics here given.
|
||||
|
||||
As a rule, single sentences should not be written or printed as paragraphs. An exception may be made of sentences of transition, indicating the relation between the parts of an exposition or argument. Frequent exceptions are also necessary in textbooks, guidebooks, and other works in which many topics are treated briefly.
|
||||
|
||||
In dialogue, each speech, even if only a single word, is a paragraph by itself; that is, a new paragraph begins with each change of speaker. The application of this rule, when dialogue and narrative are combined, is best learned from examples in well-printed works of fiction.
|
||||
|
||||
### Rule 9. As a rule, begin each paragraph with a topic sentence, end it in conformity with the beginning.
|
||||
|
||||
Again, the object is to aid the reader. The practice here recommended enables him to discover the purpose of each paragraph as he begins to read it, and to retain this purpose in mind as he ends it. For this reason, the most generally useful kind of paragraph, particularly in exposition and argument, is that in which
|
||||
|
||||
\(a\) the topic sentence comes at or near the beginning;
|
||||
|
||||
\(b\) the succeeding sentences explain or establish or develop the statement made in the topic sentence; and
|
||||
|
||||
\(c\) the final sentence either emphasizes the thought of the topic sentence or states some important consequence.
|
||||
|
||||
Ending with a digression, or with an unimportant detail, is particularly to be avoided.
|
||||
|
||||
If the paragraph forms part of a larger composition, its relation to what precedes, or its function as a part of the whole, may need to be expressed. This can sometimes be done by a mere word or phrase (*again*; *therefore*; *for the same reason*) in the topic sentence. Sometimes, however, it is expedient to precede the topic sentence by one or more sentences of introduction or transition. If more than one such sentence is required, it is generally better to set apart the transitional sentences as a separate paragraph.
|
||||
|
||||
According to the writer's purpose, he may, as indicated above, relate the body of the paragraph to the topic sentence in one or more of several different ways. He may make the meaning of the topic sentence clearer by restating it in other forms, by defining its terms, by denying the contrary, by giving illustrations or specific instances; he may establish it by proofs; or he may develop it by showing its implications and consequences. In a long paragraph, he may carry out several of these processes.
|
||||
|
||||
1 Now, to be properly enjoyed, a walking tour should be gone upon alone. 2 If you go in a company, or even in pairs, it is no longer a walking tour in anything but name; it is something else and more in the nature of a picnic. 3 A walking tour should be gone upon alone, because freedom is of the essence; because you should be able to stop and go on, and follow this way or that, as the freak takes you; and because you must have your own pace, and neither trot alongside a champion walker, nor mince in time with a girl. 4 And you must be open to all impressions and let your thoughts take colour from what you see. 5 You should be as a pipe for any wind to play upon. 6 “I cannot see the wit,” says Hazlitt, “of walking and talking at the same time. 7 When I am in the country, I wish to vegetate like the country,” which is the gist of all that can be said upon the matter. 8 There should be no cackle of voices at your elbow, to jar on the meditative silence of the morning. 9 And so long as a man is reasoning he cannot surrender himself to that fine intoxication that comes of much motion in the open air, that begins in a sort of dazzle and sluggishness of the brain, and ends in a peace that passes comprehension.—Stevenson, Walking Tours.
|
||||
|
||||
1 Topic sentence. 2 The meaning made clearer by denial of the contrary. 3 The topic sentence repeated, in abridged form, and supported by three reasons; the meaning of the third (“you must have your own pace”) made clearer by denying the contrary. 4 A fourth reason, stated in two forms. 5 The same reason, stated in still another form. 6–7 The same reason as stated by Hazlitt. 8 Repetition, in paraphrase, of the quotation from Hazlitt. 9 Final statement of the fourth reason, in language amplified and heightened to form a strong conclusion.
|
||||
|
||||
1 It was chiefly in the eighteenth century that a very different conception of history grew up. 2 Historians then came to believe that their task was not so much to paint a picture as to solve a problem; to explain or illustrate the successive phases of national growth, prosperity, and adversity. 3 The history of morals, of industry, of intellect, and of art; the changes that take place in manners or beliefs; the dominant ideas that prevailed in successive periods; the rise, fall, and modification of political constitutions; in a word, all the conditions of national well-being became the subject of their works. 4 They sought rather to write a history of peoples than a history of kings. 5 They looked especially in history for the chain of causes and effects. 6 They undertook to study in the past the physiology of nations, and hoped by applying the experimental method on a large scale to deduce some lessons of real value about the conditions on which the welfare of society mainly depend.—Lecky, The Political Value of History.
|
||||
|
||||
1 Topic sentence. 2 The meaning of the topic sentence made clearer; the new conception of history defined. 3 The definition expanded. 4 The definition explained by contrast. 5 The definition supplemented: another element in the new conception of history. 6 Conclusion: an important consequence of the new conception of history.
|
||||
|
||||
In narration and description the paragraph sometimes begins with a concise, comprehensive statement serving to hold together the details that follow.
|
||||
|
||||
The breeze served us admirably.
|
||||
|
||||
The campaign opened with a series of reverses.
|
||||
|
||||
The next ten or twelve pages were filled with a curious set of entries.
|
||||
|
||||
But this device, if too often used, would become a mannerism. More commonly the opening sentence simply indicates by its subject with what the paragraph is to be principally concerned.
|
||||
|
||||
At length I thought I might return towards the stockade.
|
||||
|
||||
He picked up the heavy lamp from the table and began to explore.
|
||||
|
||||
Another flight of steps, and they emerged on the roof.
|
||||
|
||||
The brief paragraphs of animated narrative, however, are often without even this semblance of a topic sentence. The break between them serves the purpose of a rhetorical pause, throwing into prominence some detail of the action.
|
||||
|
||||
### Rule 10. Use the active voice.
|
||||
|
||||
The active voice is usually more direct and vigorous than the passive:
|
||||
|
||||
I shall always remember my first visit to Boston.
|
||||
|
||||
This is much better than
|
||||
|
||||
My first visit to Boston will always be remembered by me.
|
||||
|
||||
The latter sentence is less direct, less bold, and less concise. If the writer tries to make it more concise by omitting “by me,”
|
||||
|
||||
My first visit to Boston will always be remembered,
|
||||
|
||||
it becomes indefinite: is it the writer, or some person undisclosed, or the world at large, that will always remember this visit?
|
||||
|
||||
This rule does not, of course, mean that the writer should entirely discard the passive voice, which is frequently convenient and sometimes necessary.
|
||||
|
||||
The dramatists of the Restoration are little esteemed to-day.
|
||||
|
||||
Modern readers have little esteem for the dramatists of the Restoration.
|
||||
|
||||
The first would be the right form in a paragraph on the dramatists of the Restoration; the second, in a paragraph on the tastes of modern readers. The need of making a particular word the subject of the sentence will often, as in these examples, determine which voice is to be used.
|
||||
|
||||
As a rule, avoid making one passive depend directly upon another.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| Gold was not allowed to be exported. | It was forbidden to export gold (The export of gold was prohibited). |
|
||||
| He has been proved to have been seen entering the building. | It has been proved that he was seen to enter the building. |
|
||||
|
||||
In both the examples above, before correction, the word properly related to the second passive is made the subject of the first.
|
||||
|
||||
A common fault is to use as the subject of a passive construction a noun which expresses the entire action, leaving to the verb no function beyond that of completing the sentence.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| A survey of this region was made in 1900. | This region was surveyed in 1900. |
|
||||
| Mobilization of the army was rapidly effected. | The army was rapidly mobilized. |
|
||||
| Confirmation of these reports cannot be obtained. | These reports cannot be confirmed. |
|
||||
|
||||
Compare the _sentence,_ “The export of gold was prohibited,” in which the predicate “was prohibited” expresses something not implied in “export.”
|
||||
|
||||
The habitual use of the active voice makes for forcible writing. This is true not only in narrative principally concerned with action, but in writing of any kind. Many a tame sentence of description or exposition can be made lively and emphatic by substituting a verb in the active voice for some such perfunctory expression as *there is*, or *could be heard*.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| There were a great number of dead leaves lying on the ground. | Dead leaves covered the ground. |
|
||||
| The sound of a guitar somewhere in the house could be heard. | Somewhere in the house a guitar hummed sleepily. |
|
||||
| The reason that he left college was that his health became impaired. | Failing health compelled him to leave college. |
|
||||
| It was not long before he was very sorry that he had said what he had. | He soon repented his words. |
|
||||
|
||||
### Rule 11. Put statements in positive form.
|
||||
|
||||
Make definite assertions. Avoid tame, colorless, hesitating, non-committal language. Use the word *not* as a means of denial or in antithesis, never as a means of evasion.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| He was not very often on time. | He usually came late. |
|
||||
| He did not think that studying Latin was much use. | He thought the study of Latin useless. |
|
||||
| The Taming of the Shrew is rather weak in spots. Shakespeare does not portray Katharine as a very admirable character, nor does Bianca remain long in memory as an important character in Shakespeare's works. | The women in The Taming of the Shrew are unattractive. Katharine is disagreeable, Bianca insignificant. |
|
||||
|
||||
The last example, before correction, is indefinite as well as negative. The corrected version, consequently, is simply a guess at the writer's intention.
|
||||
|
||||
All three examples show the weakness inherent in the word *not*. Consciously or unconsciously, the reader is dissatisfied with being told only what is not; he wishes to be told what is. Hence, as a rule, it is better to express even a negative in positive form.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| not honest | dishonest |
|
||||
| not important | trifling |
|
||||
| did not remember | forgot |
|
||||
| did not pay any attention to | ignored |
|
||||
| did not have much confidence in | distrusted |
|
||||
|
||||
The antithesis of negative and positive is strong:
|
||||
|
||||
Not charity, but simple justice.
|
||||
|
||||
Not that I loved Caesar less, but Rome the more.
|
||||
|
||||
Negative words other than *not* are usually strong:
|
||||
|
||||
The sun never sets upon the British flag.
|
||||
|
||||
### Rule 12. Use definite, specific, concrete language.
|
||||
|
||||
Prefer the specific to the general, the definite to the vague, the concrete to the abstract.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| A period of unfavorable weather set in. | It rained every day for a week. |
|
||||
| He showed satisfaction as he took possession of his well-earned reward. | He grinned as he pocketed the coin. |
|
||||
| There is a general agreement among those who have enjoyed the experience that surf-riding is productive of great exhilaration. | All who have tried surf-riding agree that it is most exhilarating. |
|
||||
|
||||
If those who have studied the art of writing are in accord on any one point, it is on this, that the surest method of arousing and holding the attention of the reader is by being specific, definite, and concrete. Critics have pointed out how much of the effectiveness of the greatest writers, Homer, Dante, Shakespeare, results from their constant definiteness and concreteness. Browning, to cite a more modern author, affords many striking examples. Take, for instance, the lines from My Last Duchess,
|
||||
|
||||
Sir, 'twas all one! My favour at her breast,
|
||||
|
||||
The dropping of the daylight in the west,
|
||||
|
||||
The bough of cherries some officious fool
|
||||
|
||||
Broke in the orchard for her, the white mule
|
||||
|
||||
She rode with round the terrace—all and each
|
||||
|
||||
Would draw from her alike the approving speech,
|
||||
|
||||
Or blush, at least,
|
||||
|
||||
and those which end the poem,
|
||||
|
||||
Notice Neptune, though,
|
||||
|
||||
Taming a sea-horse, thought a rarity,
|
||||
|
||||
Which Claus of Innsbruck cast in bronze for me.
|
||||
|
||||
These words call up pictures. Recall how in The Bishop Orders his Tomb in St. Praxed's Church “the Renaissance spirit—its worldliness, inconsistency, pride, hypocrisy, ignorance of itself, love of art, of luxury, of good Latin,” to quote Ruskin's comment on the poem, is made manifest in specific details and in concrete terms.
|
||||
|
||||
Prose, in particular narrative and descriptive prose, is made vivid by the same means. If the experiences of Jim Hawkins and of David Balfour, of Kim, of Nostromo, have seemed for the moment real to countless readers, if in reading Carlyle we have almost the sense of being physically present at the taking of the Bastille, it is because of the definiteness of the details and the concreteness of the terms used. It is not that every detail is given; that would be impossible, as well as to no purpose; but that all the significant details are given, and not vaguely, but with such definiteness that the reader, in imagination, can project himself into the scene.
|
||||
|
||||
In exposition and in argument, the writer must likewise never lose his hold upon the concrete, and even when he is dealing with general principles, he must give particular instances of their application.
|
||||
|
||||
“This superiority of specific expressions is clearly due to the effort required to translate words into thoughts. As we do not think in generals, but in particulars—as whenever any class of things is referred to, we represent it to ourselves by calling to mind individual members of it, it follows that when an abstract word is used, the hearer or reader has to choose, from his stock of images, one or more by which he may figure to himself the genus mentioned. In doing this, some delay must arise, some force be expended; and if by employing a specific term an appropriate image can be at once suggested, an economy is achieved, and a more vivid impression produced.”
|
||||
|
||||
Herbert Spencer, from whose Philosophy of Style the preceding paragraph is quoted, illustrates the principle by the sentences:
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| In proportion as the manners, customs, and amusements of a nation are cruel and barbarous, the regulations of their penal code will be severe. | In proportion as men delight in battles, bull-fights, and combats of gladiators, will they punish by hanging, burning, and the rack. |
|
||||
|
||||
### Rule 13. Omit needless words.
|
||||
|
||||
Vigorous writing is concise. A sentence should contain no unnecessary words, a paragraph no unnecessary sentences, for the same reason that a drawing should have no unnecessary lines and a machine no unnecessary parts. This requires not that the writer make all his sentences short, or that he avoid all detail and treat his subjects only in outline, but that he make every word tell.
|
||||
|
||||
Many expressions in common use violate this principle:
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| the question as to whether | whether (the question whether) |
|
||||
| there is no doubt but that | no doubt (doubtless) |
|
||||
| used for fuel purposes | used for fuel |
|
||||
| he is a man who | he |
|
||||
| in a hasty manner | hastily |
|
||||
| this is a subject which | this subject |
|
||||
| His story is a strange one. | His story is strange. |
|
||||
|
||||
In especial the expression *the fact that* should be revised out of every sentence in which it occurs.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| owing to the fact that | since (because) |
|
||||
| in spite of the fact that | though (although) |
|
||||
| call your attention to the fact that | remind you (notify you) |
|
||||
| I was unaware of the fact that | I was unaware that (did not know) |
|
||||
| the fact that he had not succeeded | his failure |
|
||||
| the fact that I had arrived | my arrival |
|
||||
|
||||
See also under *case*, *character*, *nature*, *system* in Chapter V.
|
||||
|
||||
*Who is*, *which was*, and the like are often superfluous.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| His brother, who is a member of the same firm | His brother, a member of the same firm |
|
||||
| Trafalgar, which was Nelson's last battle | Trafalgar, Nelson's last battle |
|
||||
|
||||
As positive statement is more concise than negative, and the active voice more concise than the passive, many of the examples given under Rules 11 and 12 illustrate this rule as well.
|
||||
|
||||
A common violation of conciseness is the presentation of a single complex idea, step by step, in a series of sentences or independent clauses which might to advantage be combined into one.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| Macbeth was very ambitious. This led him to wish to become king of Scotland. The witches told him that this wish of his would come true. The king of Scotland at this time was Duncan. Encouraged by his wife, Macbeth murdered Duncan. He was thus enabled to succeed Duncan as king. (51 words.) | Encouraged by his wife, Macbeth achieved his ambition and realized the prediction of the witches by murdering Duncan and becoming king of Scotland in his place. (26 words.) |
|
||||
| There were several less important courses, but these were the most important, and although they did not come every day, they came often enough to keep you in such a state of mind that you never knew what your next move would be. (43 words.) | These, the most important courses of all, came, if not daily, at least often enough to keep one under constant strain. (21 words.) |
|
||||
|
||||
### Rule 14. Avoid a succession of loose sentences
|
||||
|
||||
This rule refers especially to loose sentences of a particular type, those consisting of two co-ordinate clauses, the second introduced by a conjunction or relative. Although single sentences of this type may be unexceptionable (see under Rule 4), a series soon becomes monotonous and tedious.
|
||||
|
||||
An unskilful writer will sometimes construct a whole paragraph of sentences of this kind, using as connectives *and*, *but*, *so*, and less frequently, *who*, *which*, *when*, *where*, and *while*, these last in non-restrictive senses (see under Rule 3).
|
||||
|
||||
The third concert of the subscription series was given last evening, and a large audience was in attendance. Mr. Edward Appleton was the soloist, and the Boston Symphony Orchestra furnished the instrumental music. The former showed himself to be an artist of the first rank, while the latter proved itself fully deserving of its high reputation. The interest aroused by the series has been very gratifying to the Committee, and it is planned to give a similar series annually hereafter. The fourth concert will be given on Tuesday, May 10, when an equally attractive programme will be presented.
|
||||
|
||||
Apart from its triteness and emptiness, the paragraph above is weak because of the structure of its sentences, with their mechanical symmetry and sing-song. Contrast with them the sentences in the paragraphs quoted under Rule 9, or in any piece of good English prose, as the preface (Before the Curtain) to Vanity Fair.
|
||||
|
||||
If the writer finds that he has written a series of sentences of the type described, he should recast enough of them to remove the monotony, replacing them by simple sentences, by sentences of two clauses joined by a semicolon, by periodic sentences of two clauses, by sentences, loose or periodic, of three clauses—whichever best represent the real relations of the thought.
|
||||
|
||||
### Rule 15. Express co-ordinate ideas in similar form.
|
||||
|
||||
This principle, that of parallel construction, requires that expressions of similar content and function should be outwardly similar. The likeness of form enables the reader to recognize more readily the likeness of content and function. Familiar instances from the Bible are the Ten Commandments, the Beatitudes, and the petitions of the Lord's Prayer.
|
||||
|
||||
The unskillful writer often violates this principle, from a mistaken belief that he should constantly vary the form of his expressions. It is true that in repeating a statement in order to emphasize it he may have need to vary its form. For illustration, see the paragraph from Stevenson quoted under Rule _9_. But apart from this, he should follow the principle of parallel construction.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| Formerly, science was taught by the textbook method, while now the laboratory method is employed. | Formerly, science was taught by the textbook method; now it is taught by the laboratory method. |
|
||||
|
||||
The left-hand version gives the impression that the writer is undecided or timid; he seems unable or afraid to choose one form of expression and hold to it. The right-hand version shows that the writer has at least made his choice and abided by it.
|
||||
|
||||
By this principle, an article or a preposition applying to all the members of a series must either be used only before the first term or else be repeated before each term.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| The French, the Italians, Spanish, and Portuguese | The French, the Italians, the Spanish, and the Portuguese |
|
||||
| In spring, summer, or in winter | In spring, summer, or winter (In spring, in summer, or in winter) |
|
||||
|
||||
Correlative expressions (*both, and*; *not, but*; *not only, but also*; *either, or*; *first, second, third*; and the like) should be followed by the same grammatical construction, that is, virtually, by the same part of speech. (Such combinations as “both Henry and I,” “not silk, but a cheap substitute,” are obviously within the rule.) Many violations of this rule (as the first three below) arise from faulty arrangement; others (as the last) from the use of unlike constructions.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| It was both a long ceremony and very tedious. | The ceremony was both long and tedious. |
|
||||
| A time not for words, but action. | A time not for words, but for action. |
|
||||
| Either you must grant his request or incur his ill will. | You must either grant his request or incur his ill will. |
|
||||
| My objections are, first, the injustice of the measure; second, that it is unconstitutional. | My objections are, first, that the measure is unjust; second, that it is unconstitutional. |
|
||||
|
||||
See also the third example under Rule 12 and the last under Rule 13.
|
||||
|
||||
It may be asked, what if a writer needs to express a very large number of similar ideas, say twenty? Must he write twenty consecutive sentences of the same pattern? On closer examination he will probably find that the difficulty is imaginary, that his twenty ideas can be classified in groups, and that he need apply the principle only within each group. Otherwise he had best avoid difficulty by putting his statements in the form of a table.
|
||||
|
||||
### Rule 16. Keep related words together.
|
||||
|
||||
The position of the words in a sentence is the principal means of showing their relationship. The writer must therefore, so far as possible, bring together the words, and groups of words, that are related in thought, and keep apart those which are not so related.
|
||||
|
||||
The subject of a sentence and the principal verb should not, as a rule, be separated by a phrase or clause that can be transferred to the beginning.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| Wordsworth, in the fifth book of The Excursion, gives a minute description of this church. | In the fifth book of The Excursion, Wordsworth gives a minute description of this church. |
|
||||
| Cast iron, when treated in a Bessemer converter, is changed into steel. | By treatment in a Bessemer converter, cast iron is changed into steel. |
|
||||
|
||||
The objection is that the interposed phrase or clause needlessly interrupts the natural order of the main clause. Usually, however, this objection does not hold when the order is interrupted only by a relative clause or by an expression in apposition. Nor does it hold in periodic sentences in which the interruption is a deliberately used means of creating suspense (see examples under Rule 18).
|
||||
|
||||
The relative pronoun should come, as a rule, immediately after its antecedent.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| There was a look in his eye that boded mischief. | In his eye was a look that boded mischief. |
|
||||
| He wrote three articles about his adventures in Spain, which were published in Harper's Magazine. | He published in Harper's Magazine three articles about his adventures in Spain. |
|
||||
| This is a portrait of Benjamin Harrison, grandson of William Henry Harrison, who became President in 1889. | This is a portrait of Benjamin Harrison, grandson of William Henry Harrison. He became President in 1889. |
|
||||
|
||||
If the antecedent consists of a group of words, the relative comes at the end of the group, unless this would cause ambiguity.
|
||||
|
||||
The Superintendent of the Chicago Division, who
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| A proposal to amend the Sherman Act, which has been variously judged. | A proposal, which has been variously judged, to amend the Sherman Act. |
|
||||
| — | A proposal to amend the much-debated Sherman Act. |
|
||||
| The grandson of William Henry Harrison, who | William Henry Harrison's grandson, who |
|
||||
|
||||
A noun in apposition may come between antecedent and relative, because in such a combination no real ambiguity can arise.
|
||||
|
||||
The Duke of York, his brother, who was regarded with hostility by the Whigs
|
||||
|
||||
Modifiers should come, if possible, next to the word they modify. If several expressions modify the same word, they should be so arranged that no wrong relation is suggested.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| All the members were not present. | Not all the members were present. |
|
||||
| He only found two mistakes. | He found only two mistakes. |
|
||||
| Major R. E. Joyce will give a lecture on Tuesday evening in Bailey Hall, to which the public is invited, on “My Experiences in Mesopotamia” at eight P. M. | On Tuesday evening at eight P. M., Major R. E. Joyce will give in Bailey Hall a lecture on “My Experiences in Mesopotamia.” The public is invited. |
|
||||
|
||||
### Rule 17. In summaries, keep to one tense.
|
||||
|
||||
In summarizing the action of a drama, the writer should always use the present tense. In summarizing a poem, story, or novel, he should preferably use the present, though he may use the past if he prefers. If the summary is in the present tense, antecedent action should be expressed by the perfect; if in the past, by the past perfect.
|
||||
|
||||
An unforeseen chance prevents Friar John from delivering Friar Lawrence's letter to Romeo. Meanwhile, owing to her father's arbitrary change of the day set for her wedding, Juliet has been compelled to drink the potion on Tuesday night, with the result that Balthasar informs Romeo of her supposed death before Friar Lawrence learns of the non-delivery of the letter.
|
||||
|
||||
But whichever tense be used in the summary, a past tense in indirect discourse or in indirect question remains unchanged.
|
||||
|
||||
The Friar confesses that it was he who married them.
|
||||
|
||||
Apart from the exceptions noted, whichever tense the writer chooses, he should use throughout. Shifting from one tense to the other gives the appearance of uncertainty and irresolution (compare Rule 15).
|
||||
|
||||
In presenting the statements or the thought of some one else, as in summarizing an essay or reporting a speech, the writer should avoid intercalating such expressions as “he said,” “he stated,” “the speaker added,” “the speaker then went on to say,” “the author also thinks,” or the like. He should indicate clearly at the outset, once for all, that what follows is summary, and then waste no words in repeating the notification.
|
||||
|
||||
In notebooks, in newspapers, in handbooks of literature, summaries of one kind or another may be indispensable, and for children in primary schools it is a useful exercise to retell a story in their own words. But in the criticism or interpretation of literature the writer should be careful to avoid dropping into summary. He may find it necessary to devote one or two sentences to indicating the subject, or the opening situation, of the work he is discussing; he may cite numerous details to illustrate its qualities. But he should aim to write an orderly discussion supported by evidence, not a summary with occasional comment. Similarly, if the scope of his discussion includes a number of works, he will as a rule do better not to take them up singly in chronological order, but to aim from the beginning at establishing general conclusions.
|
||||
|
||||
### Rule 18. Place the emphatic words of a sentence at the end.
|
||||
|
||||
The proper place in the sentence for the word, or group of words, which the writer desires to make most prominent is usually the end.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| Humanity has hardly advanced in fortitude since that time, though it has advanced in many other ways. | Humanity, since that time, has advanced in many other ways, but it has hardly advanced in fortitude. |
|
||||
| This steel is principally used for making razors, because of its hardness. | Because of its hardness, this steel is principally used in making razors. |
|
||||
|
||||
The word or group of words entitled to this position of prominence is usually the logical predicate, that is, the *new* element in the sentence, as it is in the second example.
|
||||
|
||||
The effectiveness of the periodic sentence arises from the prominence which it gives to the main statement.
|
||||
|
||||
Four centuries ago, Christopher Columbus, one of the Italian mariners whom the decline of their own republics had put at the service of the world and of adventure, seeking for Spain a westward passage to the Indies as a set-off against the achievements of Portuguese discoverers, lighted on America.
|
||||
|
||||
With these hopes and in this belief I would urge you, laying aside all hindrance, thrusting away all private aims, to devote yourself unswervingly and unflinchingly to the vigorous and successful prosecution of this war.
|
||||
|
||||
The other prominent position in the sentence is the beginning. Any element in the sentence, other than the subject, may become emphatic when placed first.
|
||||
|
||||
Deceit or treachery he could never forgive.
|
||||
|
||||
So vast and rude, fretted by the action of nearly three thousand years, the fragments of this architecture may often seem, at first sight, like works of nature.
|
||||
|
||||
A subject coming first in its sentence may be emphatic, but hardly by its position alone. In the sentence,
|
||||
|
||||
Great kings worshipped at his shrine,
|
||||
|
||||
the emphasis upon *kings* arises largely from its meaning and from the context. To receive special emphasis, the subject of a sentence must take the position of the predicate.
|
||||
|
||||
Through the middle of the valley flowed a winding stream.
|
||||
|
||||
The principle that the proper place for what is to be made most prominent is the end applies equally to the words of a sentence, to the sentences of a paragraph, and to the paragraphs of a composition.
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
## IV. A Few Matters of Form
|
||||
|
||||
**Headings.** Leave a blank line, or its equivalent in space, after the title or heading of a manuscript. On succeeding pages, if using ruled paper, begin on the first line.
|
||||
|
||||
**Numerals.** Do not spell out dates or other serial numbers. Write them in figures or in Roman notation, as may be appropriate.
|
||||
|
||||
August 9, 1918 (9 August 1918)
|
||||
|
||||
Rule 3
|
||||
|
||||
Chapter XII
|
||||
|
||||
352nd Infantry
|
||||
|
||||
**Parentheses.** A sentence containing an expression in parenthesis is punctuated, outside of the marks of parenthesis, exactly as if the expression in parenthesis were absent. The expression within is punctuated as if it stood by itself, except that the final stop is omitted unless it is a question mark or an exclamation point.
|
||||
|
||||
I went to his house yesterday (my third attempt to see him), but he had left town.
|
||||
|
||||
He declares (and why should we doubt his good faith?) that he is now certain of success.
|
||||
|
||||
(When a wholly detached expression or sentence is parenthesized, the final stop comes before the last mark of parenthesis.)
|
||||
|
||||
**Quotations.** Formal quotations, cited as documentary evidence, are introduced by a colon and enclosed in quotation marks.
|
||||
|
||||
The provision of the Constitution is: “No tax or duty shall be laid on articles exported from any state.”
|
||||
|
||||
Quotations grammatically in apposition or the direct objects of verbs are preceded by a comma and enclosed in quotation marks.
|
||||
|
||||
[](https://www.gutenberg.org/files/37134/37134-h/37134-h.htm "34")I recall the maxim of La Rochefoucauld, “Gratitude is a lively sense of benefits to come.”
|
||||
|
||||
Aristotle says, “Art is an imitation of nature.”
|
||||
|
||||
Quotations of an entire line, or more, of verse, are begun on a fresh line and centered, but need not be enclosed in quotation marks.
|
||||
|
||||
Wordsworth's enthusiasm for the Revolution was at first unbounded:
|
||||
|
||||
Bliss was it in that dawn to be alive,
|
||||
|
||||
But to be young was very heaven!
|
||||
|
||||
Quotations introduced by _that_ are regarded as in indirect discourse and not enclosed in quotation marks.
|
||||
|
||||
Keats declares that beauty is truth, truth beauty.
|
||||
|
||||
Proverbial expressions and familiar phrases of literary origin require no quotation marks.
|
||||
|
||||
These are the times that try men's souls.
|
||||
|
||||
He lives far from the madding crowd.
|
||||
|
||||
The same is true of colloquialisms and slang.
|
||||
|
||||
**References.** In scholarly work requiring exact references, abbreviate titles that occur frequently, giving the full forms in an alphabetical list at the end. As a general practice, give the references in parenthesis or in footnotes, not in the body of the sentence. Omit the words _act_, _scene_, _line_, _book_, _volume_, _page_, except when referring by only one of them. Punctuate as indicated below.
|
||||
|
||||
In the second scene of the third act In III.ii (still better, simply insert III.ii in parenthesis at the proper place in the sentence)
|
||||
|
||||
After the killing of Polonius, Hamlet is placed under guard (IV.ii. 14).
|
||||
|
||||
2 Samuel i:17–27
|
||||
|
||||
Othello II.iii. 264–267, III.iii. 155–161.
|
||||
|
||||
**Syllabication.** If there is room at the end of a line for one or more syllables of a word, but not for the whole word, divide the word, unless this involves cutting off only a [](https://www.gutenberg.org/files/37134/37134-h/37134-h.htm "35") single letter, or cutting off only two letters of a long word. No hard and fast rule for all words can be laid down. The principles most frequently applicable are:
|
||||
|
||||
(a) Divide the word according to its formation:
|
||||
|
||||
know-ledge (not knowl-edge); Shake-speare (not Shakes-peare); de-scribe (not des-cribe); atmo-sphere (not atmos-phere);
|
||||
|
||||
(b) Divide “on the vowel:”
|
||||
|
||||
edi-ble (not ed-ible); propo-sition; ordi-nary; espe-cial; reli-gious; oppo-nents; regu-lar; classi-fi-ca-tion (three divisions allowable); deco-rative; presi-dent;
|
||||
|
||||
(c) Divide between double letters, unless they come at the end of the simple form of the word:
|
||||
|
||||
Apen-nines; Cincin-nati; refer-ring; but tell-ing.
|
||||
|
||||
(d) Do not divide before final _-ed_ if the _e_ is silent:
|
||||
|
||||
treat-ed (but not roam-ed or nam-ed).
|
||||
|
||||
The treatment of consonants in combination is best shown from examples:
|
||||
|
||||
for-tune; pic-ture; sin-gle; presump-tuous; illus-tration; sub-stan-tial (either division); indus-try; instruc-tion; sug-ges-tion; incen-diary.
|
||||
|
||||
The student will do well to examine the syllable-division in a number of pages of any carefully printed book.
|
||||
|
||||
**Titles.** For the titles of literary works, scholarly usage prefers italics with capitalized initials. The usage of editors and publishers varies, some using italics with capitalized initials, others using Roman with capitalized initials and with or without quotation marks. Use italics (indicated in manuscript by underscoring), except in writing for a periodical that follows a different practice. Omit initial _A_ or _The_ from titles when you place the possessive before them.
|
||||
|
||||
The Iliad; the Odyssey; As You Like It; To a Skylark; The Newcomes; A Tale of Two Cities; Dickens's Tale of Two Cities.
|
||||
+348
@@ -0,0 +1,348 @@
|
||||
## V. Words And Expressions Commonly Misused
|
||||
|
||||
(Some of the forms here listed, as *like I did*, are downright bad English; others, as the split infinitive, have their defenders, but are in such general disfavor that it is at least inadvisable to use them; still others, as *case*, *factor*, *feature*, *interesting*, *one of the most*, are good in their place, but are constantly obtruding themselves into places where they have no right to be. If the writer will make it his purpose from the beginning to express accurately his own individual thought, and will refuse to be satisfied with a ready-made formula that saves him the trouble of doing so, this last set of expressions will cause him little trouble. But if he finds that in a moment of inadvertence he has used one of them, his proper course will probably be not to patch up the sentence by substituting one word or set of words for another, but to recast it completely, as illustrated in a number of examples below and in others under Rules 12 and 13.)
|
||||
|
||||
**All right.** Idiomatic in familiar speech as a detached phrase in the sense, “Agreed,” or “Go ahead.” In other uses better avoided. Always written as two words.
|
||||
|
||||
**As good or better than.** Expressions of this type should be corrected by rearranging the sentence.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| My opinion is as good or better than his. | My opinion is as good as his, or better (if not better). |
|
||||
|
||||
**As to whether.** *Whether* is sufficient; see under Rule 13.
|
||||
|
||||
**Bid.** Takes the infinitive without *to*. The past tense in the sense, _“ordered,”_ is *bade*.
|
||||
|
||||
**But.** Unnecessary after *doubt* and *help*.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| I have no doubt but that | I have no doubt that |
|
||||
| He could not help see but that | He could not help seeing that |
|
||||
|
||||
The too frequent use of *but* as a conjunction leads to the fault discussed under Rule 14. A loose sentence formed with *but* can always be converted into a periodic sentence formed with *although*, as illustrated under Rule 4.
|
||||
|
||||
Particularly awkward is the following of one *but* by another, making a contrast to a contrast or a reservation to a reservation. This is easily corrected by re-arrangement.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| America had vast resources, but she seemed almost wholly unprepared for war. But within a year she had created an army of four million men. | America seemed almost wholly unprepared for war, but she had vast resources. Within a year she had created an army of four million men. |
|
||||
|
||||
**Can.** Means *am (is, are) able*. Not to be used as a substitute for *may*.
|
||||
|
||||
**Case.** The Concise Oxford Dictionary begins its definition of this word: “instance of a thing's occurring; usual state of affairs.” In these two senses, the word is usually unnecessary.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| In many cases, the rooms were poorly ventilated. | Many of the rooms were poorly ventilated. |
|
||||
| It has rarely been the case that any mistake has been made. | Few mistakes have been made. |
|
||||
|
||||
See Wood, Suggestions to Authors, pp. 68–71, and Quiller-Couch, The Art of Writing, pp. 103–106.
|
||||
|
||||
**Certainly.** Used indiscriminately by some writers, much as others use *very*, to intensify any and every statement. A mannerism of this kind, bad in speech, is even worse in writing.
|
||||
|
||||
**Character.** Often simply redundant, used from a mere habit of wordiness.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| Acts of a hostile character | Hostile acts |
|
||||
|
||||
**Claim, vb.** With object-noun, means *lay claim to*. May be used with a dependent clause if this sense is clearly involved: “He claimed that he was the sole surviving heir.” (But even here, “claimed to be” would be better.) Not to be used as a substitute for *declare*, *maintain*, or *charge*.
|
||||
|
||||
**Clever.** This word has been greatly overused; it is best restricted to ingenuity displayed in small matters.
|
||||
|
||||
**Compare.** To *compare to* is to point out or imply resemblances, between objects regarded as essentially of different order; to *compare with* is mainly to point out differences, between objects regarded as essentially of the same order. Thus life has been compared to a pilgrimage, to a drama, to a battle; Congress may be compared with the British Parliament. Paris has been compared to ancient Athens; it may be compared with modern London.
|
||||
|
||||
**Consider.** Not followed by *as* when it means “believe to be.” “I consider him thoroughly competent.” Compare, “The lecturer considered Cromwell first as soldier and second as administrator,” where “considered” means “examined” or “discussed.”
|
||||
|
||||
**Data.** A plural, like *phenomena* and *strata*.
|
||||
|
||||
These data were tabulated.
|
||||
|
||||
**Dependable.** A needless substitute for *reliable*, *trustworthy*.
|
||||
|
||||
**Different than.** Not permissible. Substitute *different from*, *other than*, or *unlike*.
|
||||
|
||||
**Divided into.** Not to be misused for *composed of*. The line is sometimes difficult to draw; doubtless plays are divided into acts, but poems are composed of stanzas.
|
||||
|
||||
**Don't.** Contraction of *do not*. The contraction of *does not* is *doesn't*.
|
||||
|
||||
**Due to.** Incorrectly used for *through*, *because of*, or *owing to*, in adverbial phrases: “He lost the first game, due to carelessness.” In correct use related as predicate or as modifier to a particular noun: “This invention is due to Edison;” “losses due to preventable fires.”
|
||||
|
||||
**Folk.** A collective noun, equivalent to *people*. Use the singular form only.
|
||||
|
||||
**Effect.** As noun, means *result*; as verb, means *_to_ bring about*, *accomplish* (not to be confused with *affect*, which means “to influence”).
|
||||
|
||||
As noun, often loosely used in perfunctory writing about fashions, music, painting, and other arts: “an Oriental effect;” “effects in pale green;” “very delicate effects;” “broad effects;” “subtle effects;” “a charming effect was produced by.” The writer who has a definite meaning to express will not take refuge in such vagueness.
|
||||
|
||||
**Etc.** Equivalent to *and the rest*, *and so forth*, and hence not to be used if one of these would be insufficient, that is, if the reader would be left in doubt as to any important particulars. Least open to objection when it represents the last terms of a list already given in full, or immaterial words at the end of a quotation.
|
||||
|
||||
At the end of a list introduced by *such as*, *for example*, or any similar expression, *etc.* is incorrect.
|
||||
|
||||
**Fact.** Use this word only of matters of a kind capable of direct verification, not of matters of judgment. That a particular event happened on a given date, that lead melts at a certain temperature, are facts. But such conclusions as that Napoleon was the greatest of modern generals, or that the climate of California is delightful, however incontestable they _may be_, are not properly facts.
|
||||
|
||||
On the formula *the fact that*, see under Rule 13.
|
||||
|
||||
**Factor.** A hackneyed word; the expressions of which it forms part can usually be replaced by something more direct and idiomatic.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| His superior training was the great factor in his winning the match. | He won the match by being better trained. |
|
||||
| Heavy artillery has become an increasingly important factor in deciding battles. | Heavy artillery has played a constantly larger part in deciding battles. |
|
||||
|
||||
**Feature.** Another hackneyed word; like *factor* it usually adds nothing to the sentence in which it occurs.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| A feature of the entertainment especially worthy of mention was the singing of Miss A. | (Better use the same number of words to tell what Miss A. sang, or if the programme has already been given, to tell how she sang.) |
|
||||
|
||||
As a verb, in the advertising sense of *offer as a special attraction*, to be avoided.
|
||||
|
||||
**Fix.** Colloquial in America for *arrange*, *prepare*, *mend*. In writing restrict it to its literary senses, *fasten*, *make firm or immovable*, etc.
|
||||
|
||||
**Get.** The colloquial *have got* for *have* should not be used in writing. The preferable form of the participle is *got*.
|
||||
|
||||
**He is a man who.** A common type of redundant expression; see Rule 13.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| He is a man who is very ambitious. | He is very ambitious. |
|
||||
| Spain is a country which I have always wanted to visit. | I have always wanted to visit Spain. |
|
||||
|
||||
**Help.** See under **But**.
|
||||
|
||||
**However.** In the meaning *nevertheless*, not to come first in its sentence or clause.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| The roads were almost impassable. However, we at last succeeded in reaching camp. | The roads were almost impassable. At last, however, we succeeded in reaching camp. |
|
||||
|
||||
When *however* comes first, it means *in whatever way* or *to whatever extent*.
|
||||
|
||||
However you advise him, he will probably do as he thinks best.
|
||||
|
||||
However discouraging the prospect, he never lost heart.
|
||||
|
||||
**Interesting.** Avoid this word as a perfunctory means of introduction. Instead of announcing that what you are about to tell is interesting, make it so.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| An interesting story is told of | (Tell the story without preamble.) |
|
||||
| In connection with the anticipated visit of Mr. B. to America, it is interesting to recall that he | Mr. B., who it is expected will soon visit America |
|
||||
|
||||
**Kind of.** Not to be used as a substitute for *rather* (before adjectives and verbs), or except in familiar style, for *something like* (before nouns). Restrict it to its literal sense: “Amber is a kind of fossil resin;” “I dislike that kind of notoriety.” The same holds true of *sort of*.
|
||||
|
||||
**Less.** Should not be misused for *fewer*.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| He had less men than in the previous campaign | He had fewer men than in the previous campaign |
|
||||
|
||||
*Less* refers to quantity, *fewer* to number. “His troubles are less than mine” means “His troubles are not so great as mine.” “His troubles are fewer than mine” means “His troubles are not so numerous as mine.” It is, however, correct to say, “The signers of the petition were less than a hundred,” where the round number *a hundred* is something like a collective noun, and *less* is thought of as meaning a less quantity or amount.
|
||||
|
||||
**Like.** Not to be misused for *as*. *Like* governs nouns and pronouns; before phrases and clauses the equivalent word is *as*.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| We spent the evening like in the old days. | We spent the evening as in the old days. |
|
||||
| He thought like I did. | He thought as I did (like me). |
|
||||
|
||||
**Line, along these lines.** *Line* in the sense of *course of procedure*, *conduct*, *thought*, is allowable, but has been so much overworked, particularly in the phrase *along these lines*, that a writer who aims at freshness or originality had better discard it entirely.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| Mr. B. also spoke along the same lines. | Mr. B. also spoke, to the same effect. |
|
||||
| He is studying along the line of French literature. | He is studying French literature. |
|
||||
|
||||
**Literal, literally.** Often incorrectly used in support of exaggeration or violent metaphor.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| A literal flood of abuse. | A flood of abuse. |
|
||||
| Literally dead with fatigue | Almost dead with fatigue (dead tired) |
|
||||
|
||||
**Lose out.** Meant to be more emphatic than *lose*, but actually less so, because of its commonness. The same holds true of *try out*, *win out*, *sign up*, *register up*. With a number of verbs, *out* and *up* form idiomatic combinations: *find out*, *run out*, *turn out*, *cheer up*, *dry up*, *make up*, and others, each distinguishable in meaning from the simple verb. *Lose out* is not.
|
||||
|
||||
**Most.** Not to be used for *almost*.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| Most everybody | Almost everybody |
|
||||
| Most all the time | Almost all the time |
|
||||
|
||||
**Nature.** Often simply redundant, used like *character*.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| Acts of a hostile _nature_ | Hostile acts |
|
||||
|
||||
Often vaguely used in such expressions as a “lover of nature;” “poems about nature.” Unless more specific statements follow, the reader cannot tell whether the poems have to do with natural scenery, rural life, the sunset, the untracked wilderness, or the habits of squirrels.
|
||||
|
||||
**Near by.** Adverbial phrase, not yet fully accepted as good English, though the analogy of *close by* and *hard by* seems to justify it. *Near*, or *near at hand*, is as good, if not better.
|
||||
|
||||
Not to be used as an adjective; use *neighboring*.
|
||||
|
||||
**Oftentimes, ofttimes.** Archaic forms, no longer in good use. The modern word is *often*.
|
||||
|
||||
**One hundred and one.** Retain the *and* in this and similar expressions, in accordance with the unvarying usage of English prose from Old English times.
|
||||
|
||||
**One of the most.** Avoid beginning essays or paragraphs with this formula, as, “One of the most interesting developments of modern science is, etc.;” “Switzerland is one of the most interesting countries of Europe.” There is nothing wrong in this; it is simply threadbare and forcible-feeble.
|
||||
|
||||
A common blunder is to use a singular verb in a relative clause following this or a similar expression, when the relative is the subject.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| One of the ablest men that has attacked this problem. | One of the ablest men that have attacked this problem. |
|
||||
|
||||
**Participle for verbal noun.**
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| Do you mind me asking a question? | Do you mind my asking a question? |
|
||||
| There was little prospect of the Senate accepting even this compromise. | There was little prospect of the Senate's accepting even this compromise. |
|
||||
|
||||
In the left-hand column, *asking* and *accepting* are present participles; in the right-hand column, they are verbal nouns (gerunds). The construction shown in the left-hand column is occasionally found, and has its defenders. Yet it is easy to see that the second sentence has to do not with a prospect of the Senate, but with a prospect of accepting. In this example, at least, the construction is plainly illogical.
|
||||
|
||||
As the authors of The King's English point out, there are sentences apparently, but not really, of this type, in which the possessive is not called for.
|
||||
|
||||
I cannot imagine Lincoln refusing his assent to this measure.
|
||||
|
||||
In this sentence, what the writer cannot imagine is Lincoln himself, in the act of refusing his assent. Yet the meaning would be virtually the same, except for a slight loss of vividness, if he had written,
|
||||
|
||||
I cannot imagine Lincoln's refusing his assent to this measure.
|
||||
|
||||
By using the possessive, the writer will always be on the safe side.
|
||||
|
||||
In the examples above, the subject of the action is a single, unmodified term, immediately preceding the verbal noun, and the construction is as good as any that could be used. But in any sentence in which it is a mere clumsy substitute for something simpler, or in which the use of the possessive is awkward or impossible, should of course be recast.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| In the event of a reconsideration of the whole matter's becoming necessary | If it should become necessary to reconsider the whole matter |
|
||||
| There was great dissatisfaction with the decision of the arbitrators being favorable to the company. | There was great dissatisfaction that the arbitrators should have decided in favor of the company. |
|
||||
|
||||
**People.** *The people* is a political term, not to be confused with *the public*. From the people comes political support or opposition; from the public comes artistic appreciation or commercial patronage.
|
||||
|
||||
**Phase.** Means a stage of transition or development: “the phases of the moon;” “the last phase.” Not to be used for *aspect* or *topic*.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| Another phase of the subject | Another point (another question) |
|
||||
|
||||
**Possess.** Not to be used as a mere substitute for *have* or *own*.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| He possessed great courage. | He had great courage (was very brave). |
|
||||
| He was the fortunate possessor of | He owned |
|
||||
|
||||
**Prove.** The past participle is *proved*.
|
||||
|
||||
**Respective, respectively.** These words may usually be omitted with advantage.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| Works of fiction are listed under the names of their respective authors. | Works of fiction are listed under the names of their authors. |
|
||||
| The one mile and two mile runs were won by Jones and Cummings respectively. | The one mile and two mile runs were won by Jones and by Cummings. |
|
||||
|
||||
In some kinds of formal writing, as geometrical proofs, it may be necessary to use *respectively*, but it should not appear in writing on ordinary subjects.
|
||||
|
||||
**Shall, Will.** The future tense requires *shall* for the first person, *will* for the second and third. The formula to express the speaker's belief regarding his future action or state is *I shall*; *I will* expresses his determination or his consent.
|
||||
|
||||
**Should.** See under **Would**.
|
||||
|
||||
**So.** Avoid, in writing, the use of *so* as an intensifier: “so good;” “so warm;” “so delightful.”
|
||||
|
||||
On the use of *so* to introduce clauses, see Rule 4.
|
||||
|
||||
**Sort of.** See under **Kind of**.
|
||||
|
||||
**Split Infinitive.** There is precedent from the fourteenth century downward for interposing an adverb between *to* and the infinitive which it governs, but the construction is in disfavor and is avoided by nearly all careful writers.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| To diligently inquire | To inquire diligently |
|
||||
|
||||
**State.** Not to be used as a mere substitute for *say*, *remark*. Restrict it to the sense of *express fully or clearly*, as, “He refused to state his objections.”
|
||||
|
||||
**Student Body.** A needless and awkward expression meaning no more than the simple word *students*.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| A member of the student body | A student |
|
||||
| Popular with the student body | Liked by the students |
|
||||
| The student body passed resolutions. | The students passed resolutions. |
|
||||
|
||||
**System.** Frequently used without need.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| Dayton has adopted the commission system of _government._ | Dayton has adopted government by commission. |
|
||||
| The dormitory system | Dormitories |
|
||||
|
||||
**Thanking You in Advance.** This sounds as if the writer meant, “It will not be worth my while to write to you again.” In making your request, write, “Will you please,” or “I shall be obliged,” and if anything further seems necessary write a letter of acknowledgment later.
|
||||
|
||||
**They.** A common inaccuracy is the use of the plural pronoun when the antecedent is a distributive expression such as *each*, *each one*, *everybody*, *every one*, *many a man*, which, though implying more than one person, requires the pronoun to be in the singular. Similar to this, but with even less justification, is the use of the plural pronoun with the antecedent *anybody*, *any one*, *somebody*, *some one*, the intention being either to avoid the awkward “he or she,” or to avoid committing oneself to either. Some bashful speakers even say, “A friend of mine told me that they, etc.”
|
||||
|
||||
Use *he* with all the above words, unless the antecedent is or must be feminine.
|
||||
|
||||
**Very.** Use this word sparingly. Where emphasis is necessary, use words strong in themselves.
|
||||
|
||||
**Viewpoint.** Write *point of view*, but do not misuse this, as many do, for *view* or *opinion*.
|
||||
|
||||
**While.** Avoid the indiscriminate use of this word for *and*, *but*, and *although*. Many writers use it frequently as a substitute for *and* or *but*, either from a mere desire to vary the connective, or from uncertainty which of the two connectives is the more appropriate. In this use it is best replaced by a semicolon.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| The office and salesrooms are on the ground floor, while the rest of the building is devoted to manufacturing. | The office and salesrooms are on the ground floor; the rest of the building is devoted to manufacturing. |
|
||||
|
||||
Its use as a virtual equivalent of *although* is allowable in sentences where this leads to no ambiguity or absurdity.
|
||||
|
||||
While I admire his energy, I wish it were employed in a better cause.
|
||||
|
||||
This is entirely correct, as shown by the paraphrase,
|
||||
|
||||
I admire his energy; at the same time I wish it were employed in a better cause.
|
||||
|
||||
Compare:
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| While the temperature reaches 90 or 95 degrees in the daytime, the nights are often chilly. | Although the temperature reaches 90 or 95 degrees in the daytime, the nights are often chilly. |
|
||||
|
||||
The paraphrase,
|
||||
|
||||
The temperature reaches 90 or 95 degrees in the daytime; at the same time the nights are often chilly,
|
||||
|
||||
shows why the use of *while* is incorrect.
|
||||
|
||||
In general, the writer will do well to use *while* only with strict literalness, in the sense of *during the time that*.
|
||||
|
||||
**Whom.** Often incorrectly used for *who* before *he said* or similar expressions, when it is really the subject of a following verb.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| His brother, whom he said would send him the money | His brother, who he said would send him the money |
|
||||
| The man whom he thought was his friend | The man who (that) he thought was his friend (whom he thought his friend) |
|
||||
|
||||
**Worth while.** Overworked as a term of vague approval and (with *not*) of disapproval. Strictly applicable only to actions: “Is it worth while to telegraph?”
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| His books are not worth while. | His books are not worth reading (are not worth one's while to read; do not repay reading; are worthless). |
|
||||
|
||||
The use of *worth while* before a noun (“a worth while story”) is indefensible.
|
||||
|
||||
**Would.** A conditional statement in the first person requires *should*, not *would*.
|
||||
|
||||
I should not have succeeded without his help.
|
||||
|
||||
The equivalent of *shall* in indirect quotation after a verb in the past tense is *should*, not *would*.
|
||||
|
||||
He predicted that before long we should have a great surprise.
|
||||
|
||||
To express habitual or repeated action, the past tense, without *would*, is usually sufficient, and from its brevity, more emphatic.
|
||||
|
||||
| Original | Revision |
|
||||
| --- | --- |
|
||||
| Once a year he would visit the old mansion. | Once a year he visited the old mansion. |
|
||||
@@ -0,0 +1,901 @@
|
||||
# Signs of AI Writing
|
||||
|
||||
[![A screenshot of ChatGPT reading: "[header] Legacy & Interpretation [body] The "Black Hole Edition" is not just a meme — it's a celebration of grassroots car culture, where ideas are limitless and fun is more important than spec sheets. Whether powered by a rotary engine, a V8 swap, or an imagined fighter jet turbine, the Miata remains the canvas for car enthusiasts worldwide."](https://upload.wikimedia.org/wikipedia/commons/thumb/5/59/ChatGPT_response_screenshot_1.jpg/250px-ChatGPT_response_screenshot_1.jpg)](https://en.wikipedia.org/wiki/File:ChatGPT_response_screenshot_1.jpg)
|
||||
|
||||
*LLMs tend to have an identifiable writing style.*
|
||||
|
||||
This is a list of writing and formatting conventions typical of [AI chatbots](https://en.wikipedia.org/wiki/AI_chatbot "AI chatbot") such as [ChatGPT](https://en.wikipedia.org/wiki/ChatGPT "ChatGPT"), with real examples taken from Wikipedia articles and drafts. It is a [field guide](https://en.wikipedia.org/wiki/Field_guide "Field guide") to help detect [undisclosed AI-generated content](https://en.wikipedia.org/wiki/Wikipedia:LLMDISCLOSE "Wikipedia:LLMDISCLOSE") on Wikipedia. This list is *descriptive*, not *prescriptive*; it consists of observations, not rules. Advice about formatting or language to avoid in Wikipedia articles can be found in the [policies and guidelines](https://en.wikipedia.org/wiki/Wikipedia:PAG "Wikipedia:PAG") and the [Manual of Style](https://en.wikipedia.org/wiki/Wikipedia:MOS "Wikipedia:MOS"), but does not belong on this page.
|
||||
|
||||
This list is *not* a ban on certain words, phrases, or punctuation. Not all text featuring these indicators is AI-generated, as the [large language models](https://en.wikipedia.org/wiki/Large_language_model "Large language model") that power AI chatbots are trained on human writing, including the writing of Wikipedia editors. This is simply a catalog of very common patterns observed over many thousands of instances of AI-generated text, *specific to Wikipedia.* While some of its advice may be broadly applicable, some signs—particularly those involving punctuation and formatting—may not apply in a non-Wikipedia context.
|
||||
|
||||
The patterns here are also only potential *signs* of a problem, not *the problem itself*. While many of these issues are immediately obvious and easy to fix—e.g., excessive boldface, poor use of language and punctuation, broken markup, citation style quirks—they can point to less outwardly visible problems that carry [much more serious policy risks](https://en.wikipedia.org/wiki/Wikipedia:AIFAIL "Wikipedia:AIFAIL"). If LLM-generated text is polished enough (initially or subsequently), those surface defects might not be present, but deeper problems can be. Please do not merely treat these signs as the problems to be fixed; that could just make detection harder. The actual problems are those deeper concerns, so make sure to address them, either yourself or by flagging them, per the advice at [Wikipedia:Large language models §Handling suspected LLM-generated content](https://en.wikipedia.org/wiki/Wikipedia:Large_language_models#Handling_suspected_LLM-generated_content "Wikipedia:Large language models") and [Wikipedia:WikiProject AI Cleanup/Guide](https://en.wikipedia.org/wiki/Wikipedia:WikiProject_AI_Cleanup/Guide "Wikipedia:WikiProject AI Cleanup/Guide").
|
||||
|
||||
The [speedy deletion policy](https://en.wikipedia.org/wiki/Wikipedia:Speedy_deletion "Wikipedia:Speedy deletion") criterion [G15](https://en.wikipedia.org/wiki/Wikipedia:G15 "Wikipedia:G15") (LLM-generated pages without human review) is limited to the most objective and least contestable indications that the page's content was generated by an LLM. There are three such indicators, the first of which can be found in [§Communication intended for the user](#communication-intended-for-the-user) and the other two in [§Citations](#citations).
|
||||
|
||||
Do not solely rely on [artificial intelligence content detection](https://en.wikipedia.org/wiki/Artificial_intelligence_content_detection "Artificial intelligence content detection") tools (such as [GPTZero](https://en.wikipedia.org/wiki/GPTZero "GPTZero")) to evaluate whether text is LLM-generated. While they perform better than might be achieved by chance, these tools have non-trivial error rates and cannot replace human judgment.[^1] Detectors can be brittle to multiple factors such as text modifications (e.g. paraphrasing and spacing changes) and the use of generative models not seen during detector training.[^2] By the same token, do not trust too much in your own interpretation. Research shows that people who use LLMs heavily themselves can correctly determine whether an article was generated by AI about 90% of the time, which means that if you are an expert user of LLMs and you tag 10 pages as being AI-generated, you've probably falsely accused one editor.[^3] People who don't personally use LLMs much do only slightly better than random chance (in both directions) for identifying AI-generated articles.[^3]
|
||||
|
||||
---
|
||||
|
||||
## Regression to the Mean
|
||||
|
||||
LLMs (and [artificial neural networks](https://en.wikipedia.org/wiki/Artificial_neural_network "Artificial neural network") in general) use statistical algorithms to guess (infer) what should come next based on a large corpus of training material. It thus tends to [regress to the mean](https://en.wikipedia.org/wiki/Regression_to_the_mean "Regression to the mean"); that is, the result tends toward the most statistically likely result that applies to the widest variety of cases. It can simultaneously be a strength and a "tell" for detecting AI-generated content.
|
||||
|
||||
For example, LLMs are usually trained on data from the internet in which famous people are generally described with positive, important-sounding language. Consequently, the LLM tends to omit specific, unusual, nuanced facts (which are statistically rare) and replace them with more generic, positive descriptions (which are statistically common). Thus the highly specific "inventor of the first train-coupling device" might become "a revolutionary titan of industry." It is like shouting louder and louder that a portrait shows a uniquely important person, while the portrait itself is fading from a sharp photograph into a blurry, generic sketch. The subject becomes simultaneously less specific and more exaggerated.[^4]
|
||||
|
||||
This statistical regression to the mean, a smoothing over of specific facts into generic statements, that could equally apply to many topics, makes AI-generated content easier to detect.
|
||||
|
||||
### Undue emphasis on symbolism, legacy, and importance
|
||||
|
||||
**Words to watch:** *stands/serves as*, *is a testament/reminder*, *plays a vital/significant/crucial/pivotal role*, *underscores/highlights its importance/significance*, *reflects broader*, *symbolizing its ongoing/enduring/lasting impact*, *key turning point*, *indelible mark*, *deeply rooted*, *profound heritage*, *steadfast dedication*...
|
||||
|
||||
LLM writing often puffs up the importance of the subject matter by adding statements about how arbitrary aspects of the topic represent or contribute to a broader topic.[^5] There is a distinct and easily identifiable repertoire of ways that it writes these statements.[^6]
|
||||
|
||||
> The Statistical Institute of Catalonia was officially established in 1989, marking a pivotal moment in the evolution of regional statistics in Spain. [...]
|
||||
>
|
||||
> The founding of Idescat represented a significant shift toward regional statistical independence, enabling [Catalonia](https://en.wikipedia.org/wiki/Catalonia "Catalonia") to develop a statistical system tailored to its unique socio-economic context. This initiative was part of a broader movement across Spain to decentralize administrative functions and enhance regional governance.
|
||||
|
||||
> Kumba has long been an important center for trade and agriculture. [...] The establishment of road networks connecting Kumba to other parts of the Southwest Region, such as Mamfe and Buea, helped solidify its role as a regional hub.
|
||||
|
||||
LLMs may include these statements for even the most mundane of subjects like etymology or population data. Sometimes, they add hedging preambles acknowledging that the subject is relatively unimportant or low-profile, before talking about its importance anyway.
|
||||
|
||||
**Examples**
|
||||
|
||||
> During the [Spanish colonial period](https://en.wikipedia.org/wiki/Spanish_Colonial_Period_(Philippines) "Spanish Colonial Period (Philippines)"), the name *Bakunutan* was hispanized to *Bacnotan*, a modification reflected in official documents preserved in the [National Archives](https://en.wikipedia.org/wiki/National_Archives_of_the_Philippines "National Archives of the Philippines") in Manila. This etymology highlights the enduring legacy of the community's resistance and the transformative power of unity in shaping its identity.
|
||||
|
||||
> Though it saw only limited application, it contributes to the broader history of early aviation engineering and reflects the influence of French rotary designs on German manufacturers.
|
||||
|
||||
When talking about biology (e.g., when asked to discuss an animal or plant species), LLMs tend to over-emphasize connections to the broader ecosystem or environment, even when those connections are tenuous or generic. LLMs also tend to belabor the species' conservation status and research and preservation efforts, even if the status is unknown and no serious efforts exist.
|
||||
|
||||
**Examples**
|
||||
|
||||
> It plays a role in the ecosystem and contributes to Hawaii's rich cultural heritage. [...] Preserving this endemic species is vital not only for ecological diversity but also for sustaining the cultural traditions connected to Hawaii's native flora.
|
||||
|
||||
> Currently, there is no specific conservation assessment for *Lethrinops lethrinus* by the International Union for Conservation of Nature (IUCN). However, the general health of the Lake Malawi ecosystem is crucial for the survival of this and other endemic species. Factors such as overfishing, pollution, and habitat destruction could potentially impact their populations.
|
||||
|
||||
### Undue emphasis on notability, attribution, and media coverage
|
||||
|
||||
**Words to watch:** *independent coverage*, *local/regional/national/[country name] media outlets*, *music/business/tech outlets*, *active social media presence*
|
||||
|
||||
Similarly, LLMs act as if the best way to prove that a subject is notable is to hit readers over the head with claims of notability, often by listing sources that a subject has been covered in. They may or may not provide additional context as to what those sources have actually said about the subject, and often inaccurately attribute their own [superficial analyses](#superficial-analyses) to the source. This is more common in text from newer AI tools (2025 or later).
|
||||
|
||||
Human-written press releases have of course also cited news clippings for decades, but LLMs specifically asked to write a Wikipedia article often echo the exact wording of [Wikipedia's guidelines](https://en.wikipedia.org/wiki/Wikipedia:N "Wikipedia:N"), such as "independent coverage."
|
||||
|
||||
**Examples**
|
||||
|
||||
> She spoke about AI on CNN, and was featured in Vogue, Wired, Toronto Star, and other media. [...] Her insights have also been featured in *Wired*, *Refinery29*, and other prominent media outlets.
|
||||
|
||||
> Her views have been cited in *The New York Times*, *BBC*, *Financial Times*, and *The Hindu*.
|
||||
|
||||
> Its significance is documented in archived school event programs and regional press coverage, including the *Mesabi Daily News*, which regularly reviewed performances held there.
|
||||
|
||||
On Wikipedia specifically, LLMs often painstakingly emphasize their sources in the body text—even for trivial coverage, uncontroversial facts, or other situations where a human Wikipedia editor would be more likely to either provide an inline citation or no source at all.
|
||||
|
||||
**Examples**
|
||||
|
||||
> The restaurant has also been mentioned in [ABC News](https://en.wikipedia.org/wiki/ABC_News_(Australia) "ABC News (Australia)") coverage relating to incidents in the surrounding precinct, underscoring its role as a well-known late-night venue in the city [of [Adelaide](https://en.wikipedia.org/wiki/Adelaide "Adelaide")].
|
||||
|
||||
> In the United States, university-based incubators and accelerators have expanded alongside these centers; an official Library of Congress review found that 31.5% of SBA [[Small Business Administration](https://en.wikipedia.org/wiki/Small_Business_Administration "Small Business Administration")] Growth Accelerator Fund Competition winners from 2014–2016 were university-based programs.
|
||||
|
||||
In articles about people/entities who use social media, LLMs will often note that they "maintain an active social media presence" or something similar. This wording is particularly idiosyncratic to AI text and relatively uncommon on Wikipedia before ~2024.
|
||||
|
||||
> The mall maintains a strong digital presence, particularly on Instagram, where it actively shares the latest updates and events. Forum Kochi has consistently demonstrated excellence in digital promotions, with high-quality, engaging, and impactful video content playing a key role in its outreach.
|
||||
|
||||
### Superficial analyses
|
||||
|
||||
**Words to watch:** *ensuring ...*, *highlighting ...*, *emphasizing ...*, *reflecting ...*, *underscoring ...*, *showcasing ...*, *aligns with...*, *contributing to...*
|
||||
|
||||
AI chatbots tend to insert superficial analysis of information, often in relation to its significance, recognition, or impact.[^7] This is often done by attaching a [present participle](https://en.wikipedia.org/wiki/Participle#Forms "Participle") ("-ing") phrase at the end of sentences, sometimes with [vague attributions](#vague-attributions-of-opinion) to third parties (see below).[^7][^5]
|
||||
|
||||
While [many of these words are strong AI tells on their own](https://en.wikipedia.org/wiki/Wikipedia:AIWORDS "Wikipedia:AIWORDS"),[^6][^8] an even stronger tell is when the subjects of these verbs are facts, events, or other inanimate things. A person, for example, can highlight or emphasize something, but a fact or event cannot. The "highlighting" or "underscoring" is not something that is actually happening; it is a claim by a disembodied narrator about what something means.[^5]
|
||||
|
||||
Such comments are usually [synthesis](https://en.wikipedia.org/wiki/Wikipedia:SYNTH "Wikipedia:SYNTH") and/or unattributed opinions in wikivoice. Newer chatbots with [retrieval-augmented generation](https://en.wikipedia.org/wiki/Retrieval-augmented_generation "Retrieval-augmented generation") (for example, an AI chatbot that can search the web) may attach these statements to [named sources](#undue-emphasis-on-notability-attribution-and-media-coverage)—e.g., "Roger Ebert highlighted the lasting influence"—regardless of whether those sources say anything close.
|
||||
|
||||
**Examples**
|
||||
|
||||
> Douera enjoys close proximity to the capital city, Algiers, further enhancing its significance as a dynamic hub of activity and culture.
|
||||
|
||||
> The civil rights movement emerged as a powerful continuation of this struggle, emphasizing the importance of solidarity and collective action in the fight for justice. This historical legacy has influenced contemporary African-American families, shaping their values, community structures, and approaches to political engagement. Economically, the enduring impacts of systemic inequality have led to both challenges and innovations within African-American communities, driving a commitment to empowerment and social change that echoes through generations.
|
||||
|
||||
> Its bilingual monument sign, with inscriptions in both English and Spanish, underscores its role in bringing together Latter-day Saints from the United States and Mexico.
|
||||
|
||||
> These citations, spanning more than six decades and appearing in recognized academic publications, illustrate Blois' lasting influence in computational linguistics, grammar, and neology.
|
||||
|
||||
> It holds a pivotal place in the [East Central Railway Zone](https://en.wikipedia.org/wiki/East_Central_Railway_Zone "East Central Railway Zone") of [Indian Railways](https://en.wikipedia.org/wiki/Indian_Railways "Indian Railways"), serving as a major railway hub with historical significance. The station has [1,676 mm](https://en.wikipedia.org/wiki/5_ft_6_in_gauge_railway "5 ft 6 in gauge railway") (5 ft 6 in) [broad gauge](https://en.wikipedia.org/wiki/Broad_gauge "Broad gauge") along with 8 tracks and 6 platforms. [...] Historically, it has been crucial for linking [Darbhanga](https://en.wikipedia.org/wiki/Darbhanga "Darbhanga") with significant cities like [Delhi](https://en.wikipedia.org/wiki/Delhi "Delhi"), [Patna](https://en.wikipedia.org/wiki/Patna "Patna"), and [Kolkata](https://en.wikipedia.org/wiki/Kolkata "Kolkata"), facilitating the movement of passengers and goods. The station has supported various services, including passenger trains and express trains like the [Satyagrah Express](https://en.wikipedia.org/wiki/Satyagrah_Express "Satyagrah Express") and [Mithila Express](https://en.wikipedia.org/wiki/Mithila_Express "Mithila Express"), contributing to the socio-economic development of the region. [...] Over the years, Darbhanga Junction has seen several upgrades and modernization efforts aimed at improving facilities and operational efficiency, reflecting its continued relevance in the regional and national transportation landscape.
|
||||
|
||||
### Promotional and advertisement-like language
|
||||
|
||||
**Words to watch:** *continues to captivate*, *groundbreaking* (in the figurative sense), *stunning natural beauty*, *enduring/lasting legacy*, *nestled*, *in the heart of*, *boasts a*...
|
||||
|
||||
LLMs have serious problems keeping a neutral tone, especially when writing about something that could be considered "cultural heritage"—in which case they [constantly remind the reader of its importance](#undue-emphasis-on-symbolism-legacy-and-importance).
|
||||
|
||||
**Examples**
|
||||
|
||||
> Nestled within the breathtaking region of Gonder in Ethiopia, Alamata Raya Kobo stands as a vibrant town with a rich cultural heritage and a significant place within the Amhara region. From its scenic landscapes to its historical landmarks, Alamata Raya Kobo offers visitors a fascinating glimpse into the diverse tapestry of Ethiopia. In this article, we will explore the unique characteristics that make Alamata Raya Kobo a town worth visiting and shed light on its significance within the Amhara region.
|
||||
|
||||
> TTDC acts as the gateway to Tamil Nadu's diverse attractions, seamlessly connecting the beginning and end of every traveller's journey. It offers dependable, value-driven experiences that showcase the state's rich history, spiritual heritage, and natural beauty.
|
||||
|
||||
In a similar way, LLM chatbots also add promotional/positive-sounding language to text about companies, business, and products, such that it sounds more like the transcript of a TV commercial.
|
||||
|
||||
> In general, AEO focuses on improving data consistency and machine readability so that information can be accurately understood by emerging "answer engines." Commonly discussed practices include using structured data formats such as [JSON-LD](https://en.wikipedia.org/wiki/JSON-LD "JSON-LD") and [schema.org](https://en.wikipedia.org/wiki/Schema.org "Schema.org"), maintaining the freshness and accuracy of published information, and aligning digital entities with open web standards like [Wikidata](https://en.wikipedia.org/wiki/Wikidata "Wikidata") and Schema.org.
|
||||
|
||||
> The SOLLEI's exterior design communicates a powerful emotional presence, staying true to Cadillac's signature bold proportions. Its low, elongated silhouette is highlighted by a wide stance and an extended coupe door, which enhances accessibility to the spacious rear cabin. Smooth, uninterrupted surfaces and a pronounced A-line accentuate the vehicle's overall length, while a sleek, low tail imparts a sense of refined dynamism. A mid-body line runs seamlessly from the headlamps to the taillights, reinforcing the car's cohesive and elegant design. Traditional door handles have been replaced with discrete buttons, preserving the vehicle's clean and modern profile. In a nod to Cadillac's legacy of bold color choices, the exterior is finished in "Manila Cream"—a distinctive hue originally offered in 1957 and 1958. This heritage color has been thoughtfully revived and hand-painted by Cadillac artisans, showcasing the brand's dedication to craftsmanship and historical reverence.
|
||||
|
||||
### Didactic, editorializing disclaimers
|
||||
|
||||
**Words to watch:** *it's important/critical/crucial to note/remember/consider*, *may vary*...
|
||||
|
||||
LLMs often tell the reader about things "it's important to remember." This frequently takes the form of "disclaimers" to an imagined reader regarding safety or controversial topics, or disambiguating topics that vary in different locales/jurisdictions.
|
||||
|
||||
**Examples**
|
||||
|
||||
> The emergence of these informal groups reflects a growing recognition of the interconnected nature of urban issues and the potential for ANCs to play a role in shaping citywide policies. However, it's important to note that these caucuses operate outside the formal ANC structure and their influence on policy decisions may vary.
|
||||
|
||||
> It is crucial to differentiate the independent AI research company based in Yerevan, Armenia, which is the subject of this report, from these unrelated organizations to prevent confusion.
|
||||
|
||||
> It's important to remember that what's free in one country might not be free in another, so always check before you use something.
|
||||
|
||||
### Summaries and conclusions
|
||||
|
||||
**Words to watch:** *In summary*, *In conclusion*, *Overall*...
|
||||
|
||||
When generating longer outputs (such as when told to "write an article"), LLMs often add a section titled "Conclusion" or similar, and will often end a paragraph or section by summarizing and restating its core idea.[^9]
|
||||
|
||||
**Examples**
|
||||
|
||||
> In summary, the educational and training trajectory for nurse scientists typically involves a progression from a master's degree in nursing to a Doctor of Philosophy in Nursing, followed by postdoctoral training in nursing research. This structured pathway ensures that nurse scientists acquire the necessary knowledge and skills to engage in rigorous research and contribute meaningfully to the advancement of nursing science.
|
||||
|
||||
### Outline-like conclusions about challenges and future prospects
|
||||
|
||||
**Words to watch:** *Despite its... faces several challenges...*, *Despite these challenges*, *Challenges and Legacy*, *Future Outlook*...
|
||||
|
||||
Many LLM-generated Wikipedia articles include a "Challenges" section, which typically begins with a sentence like "Despite its [positive/promotional words], [article subject] faces challenges..." and ends with either a vaguely positive assessment of the article subject,[^1] or speculation about how ongoing or potential initiatives could benefit the subject. Such paragraphs usually appear at the end of articles with a rigid outline structure, which may also include a separate section for "Future Prospects."
|
||||
|
||||
Note: This sign is about the rigid formula, not simply the mention of challenges or challenging.
|
||||
|
||||
**Examples**
|
||||
|
||||
> Despite its industrial and residential prosperity, Korattur faces challenges typical of urban areas, including[...] With its strategic location and ongoing initiatives, Korattur continues to thrive as an integral part of the Ambattur industrial zone, embodying the synergy between industry and residential living.
|
||||
|
||||
> Despite its success, the Panama Canal faces challenges, including[...] Future investments in technology, such as automated navigation systems, and potential further expansions could enhance the canal's efficiency and maintain its relevance in global trade.
|
||||
|
||||
> Despite their promising applications, pyroelectric materials face several challenges that must be addressed for broader adoption. One key limitation is[...] Despite these challenges, the versatility of pyroelectric materials positions them as critical components for sustainable energy solutions and next-generation sensor technologies.
|
||||
|
||||
> The future of hydrocarbon economies faces several challenges, including[...] This section would speculate on potential developments and the changing landscape of global energy.
|
||||
|
||||
> Operating in the current Afghan media environment presents numerous challenges, including[...] Despite these challenges, Amu TV has managed to continue to provide a vital service to the Afghan population.
|
||||
|
||||
> For example, while the methodology supports transdisciplinary collaboration in principle, applying it effectively in large, heterogeneous teams can be challenging. [...] SCE continues to evolve in response to these challenges.
|
||||
|
||||
### Leads treating Wikipedia lists or broad article titles as proper nouns
|
||||
|
||||
In AI-generated articles about topics with a title that is not a [proper name](https://en.wikipedia.org/wiki/Proper_name "Proper name"), such as a [list](https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Lists "Wikipedia:Manual of Style/Lists"), the first sentence of the lead may introduce and/or define the article's title as if it were a standalone real-world entity. While the [MOS](https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Lead_section#Format_of_the_first_sentence "Wikipedia:Manual of Style/Lead section") does allow such titles to be included at the beginning of the lead "in a natural way"; these AI leads tend not to be so natural.
|
||||
|
||||
**Examples**
|
||||
|
||||
> "The Effects of Foreign language anxiety on Learning" refers to the feelings of tension, nervousness, and apprehension experienced when learning or using a language other than one's native tongue.
|
||||
|
||||
> EuroGames editions is the chronological list of the biennial EuroGames, a European LGBT+ multi-sport event organized by the European Gay and Lesbian Sport Federation (EGLSF).
|
||||
|
||||
> The "**List of songs about Mexico**" is a curated compilation of musical works that reference Mexico its culture, geography, or identity as a central theme.
|
||||
|
||||
---
|
||||
|
||||
## Language and Grammar
|
||||
|
||||
### Overused "AI vocabulary" words
|
||||
|
||||
**Words to watch:** *align/aligns/aligning with*,[^6][^8] *crucial*,[^1] *delve/delves/delving* (pre-2025),[^6][^8][^1] *emphasizing*,[^6][^8] *enduring*,[^8] *enhance/enhances/enhancing*,[^8][^1] *fostering*,[^8][^1] *garnered/garnering*,[^6][^8] *highlight/highlighted/highlighting/highlights* (as a verb),[^1] *interplay*,[^8] *intricate/intricacies*,[^6][^8][^7] *key* (as an adjective), *landscape*,[^8] *leveraging*,[^1] *multifaceted*,[^6][^8][^7] *notably*,[^8] *nuanced*,[^6][^8] *realm*,[^8] *robust*,[^1] *seamless/seamlessly*,[^1] *shed light on*, *showcasing*,[^8] *streamline*,[^1] *tapestry*,[^8] *testament*,[^1][^8] *underpin/underpins/underpinning*,[^8] *underscore/underscores/underscoring*,[^8] *vibrant*,[^8][^7] *vital*,[^1] ...
|
||||
|
||||
Many studies have demonstrated that LLMs overuse certain words – especially compared to pre-2022 text, which is almost certain to be human-written.[^6] These "AI vocabulary" words are also ubiquitous in AI-generated encyclopedias, such as [Grokipedia](https://en.wikipedia.org/wiki/Grokipedia "Grokipedia"), and in AI-generated Wikipedia text. They often co-occur in LLM output: where there is one, there are likely others.[^10] An edit introducing one or two of these words may not be a big deal, but an edit (post-2022) introducing lots of them, lots of times, is one of the strongest tells for AI use.
|
||||
|
||||
The distribution of "AI vocabulary" is slightly different depending on which chatbot or LLM was used,[^7] and has changed over time. For instance, the word *delve* was famously overused by ChatGPT until 2025, when its incidence dropped off sharply.[^11]
|
||||
|
||||
Please keep context in mind. For example, while the word "underscore" is overused in AI text, it can also refer to a literal underline mark, or to [incidental music](https://en.wikipedia.org/wiki/Incidental_music "Incidental music").
|
||||
|
||||
**Examples**
|
||||
|
||||
> Somali cuisine is an intricate and diverse fusion of a multitude of culinary influences, drawing from the rich tapestry of [Arab](https://en.wikipedia.org/wiki/Arab_cuisine "Arab cuisine"), [Indian](https://en.wikipedia.org/wiki/Indian_cuisine "Indian cuisine"), and [Italian](https://en.wikipedia.org/wiki/Italian_cuisine "Italian cuisine") flavours. This culinary tapestry is a direct result of Somalia's longstanding heritage of vibrant trade and bustling commerce. [...]
|
||||
>
|
||||
> Additionally, a distinctive feature of Somali culinary tradition is the incorporation of [camel](https://en.wikipedia.org/wiki/Camel "Camel") [meat](https://en.wikipedia.org/wiki/Meat "Meat") and [milk](https://en.wikipedia.org/wiki/Milk "Milk"). They are considered a delicacy and serve as cherished and fundamental elements in the rich tapestry of Somali cuisine. [...]
|
||||
>
|
||||
> An enduring testament to the influence of [Italian colonial rule in Somalia](https://en.wikipedia.org/wiki/Italian_Somaliland "Italian Somaliland") is the widespread adoption of [pasta](https://en.wikipedia.org/wiki/Pasta "Pasta") and [lasagne](https://en.wikipedia.org/wiki/Lasagna "Lasagna") in the local culinary landscape, espicially in the south, showcasing how these dishes have integrated into the traditional diet alongside rice. [...]
|
||||
>
|
||||
> Additionally, Somali merchants played a pivotal role in the global coffee trade, being one of the first to export coffee beans.
|
||||
|
||||
### Negative parallelisms
|
||||
|
||||
Parallel constructions involving "not", "but", or "however" such as "Not only ... but ..." or "It is not just about ..., it's ..." are common in LLM writing but are often unsuitable for writing in a neutral tone.[^1][^11]
|
||||
|
||||
**Examples**
|
||||
|
||||
> **Self-Portrait** by Yayoi Kusama, executed in 2010 and currently preserved in the famous Uffizi Gallery in Florence, constitutes not only a work of self-representation, but a visual document of her obsessions, visual strategies and psychobiographical narratives.
|
||||
|
||||
> It's not just about the beat riding under the vocals; it's part of the aggression and atmosphere.
|
||||
|
||||
Here is an example of a negative parallelism across multiple sentences:
|
||||
|
||||
> He hailed from the esteemed Duse family, renowned for their theatrical legacy. Eugenio's life, however, took a path that intertwined both personal ambition and familial complexities.
|
||||
|
||||
### Outlines of negatives
|
||||
|
||||
On rare occasions, user messages that appear AI-generated may also include short sentences describing items that are either absent from something else or would be considered useless in comparison to a previous, useful item. Some of these may read something along the lines of "no ..., no ..., just ..." or "What matters is ..., not ..., not ...".
|
||||
|
||||
**Examples**
|
||||
|
||||
> There are no long-form profiles. No editorial insights. No coverage of her game dev career. No notable accolades. Just TikTok recaps and callouts.
|
||||
|
||||
---
|
||||
|
||||
> The process demands rigor — not emotional fatigue, not personal offense, and certainly not a premature exit masked as moral high ground.
|
||||
|
||||
---
|
||||
|
||||
> Not a career, not a body of work, not sustained relevance — just an algorithmic moment.
|
||||
|
||||
---
|
||||
|
||||
> This is not a close call. It is not a gray area. This page should be gone, fully, cleanly, and without delay. No redirect. No merge. Just delete.
|
||||
|
||||
---
|
||||
|
||||
> Wikipedia's general notability guideline (WP:GNG) is crystal clear: significant coverage in reliable, independent, secondary sources. Not a few throwaway articles echoing Twitter drama. Not reactionary posts exploiting culture war tension. Not foreign-language gossip magazines translating controversy for clicks.
|
||||
|
||||
---
|
||||
|
||||
> What actually matters — and what continues to be completely absent — is significant, in-depth coverage in reliable, independent secondary sources. Not gossip sites. Not recycled outrage. Not tabloid blurbs about one viral controversy. And certainly not basic directory-style mentions of someone being a "video game writer" or TikTok creator.
|
||||
|
||||
### Rule of three
|
||||
|
||||
LLMs overuse the '[rule of three](https://en.wikipedia.org/wiki/Rule_of_three_(writing) "Rule of three (writing)")'. This can take different forms, from "adjective, adjective, adjective" to "short phrase, short phrase, and short phrase".[^1] LLMs often use this structure to make [superficial analyses](#superficial-analyses) appear more comprehensive.
|
||||
|
||||
**Examples**
|
||||
|
||||
> The Amaze Conference brings together global SEO professionals, marketing experts, and growth hackers to discuss the latest trends in digital marketing. The event features keynote sessions, panel discussions, and networking opportunities.
|
||||
|
||||
### Vague attributions of opinion
|
||||
|
||||
**Words to watch:** *Industry reports*, *Observers have cited*, *Some critics argue*...
|
||||
|
||||
AI chatbots tend to attribute opinions or claims to some vague authority—a practice called [weasel wording](https://en.wikipedia.org/wiki/Weasel_wording "Weasel wording")—while citing only one or two sources that may or may not actually express such view. They also tend to overgeneralize the perspective of one or few sources into that of a wider group.
|
||||
|
||||
**Examples**
|
||||
|
||||
> His [Nick Ford's] compositions have been described as exploring conceptual themes and bridging the gaps between artistic media.
|
||||
|
||||
— From [Draft:Nick Ford (musician)](https://en.wikipedia.org/wiki/Draft:Nick_Ford_(musician) "Draft:Nick Ford (musician)"). Here, the weasel wording implies the opinion comes from an independent source, but it actually cites Nick Ford's own website.
|
||||
|
||||
> Due to its unique characteristics, the Haolai River is of interest to researchers and conservationists. Efforts are ongoing to monitor its ecological health and preserve the surrounding grassland environment, which is part of a larger initiative to protect China's semi-arid ecosystems from degradation.
|
||||
|
||||
> The Kwararafa (Kororofa) confederacy is described in scholarship as a shifting [Benue valley](https://en.wikipedia.org/wiki/Benue_valley "Benue valley") coalition led by [Jukun](https://en.wikipedia.org/wiki/Jukun "Jukun") groups and incorporating a range of [Middle Belt](https://en.wikipedia.org/wiki/Middle_Belt "Middle Belt") peoples. Because much of the historical record derives from [Hausa](https://en.wikipedia.org/wiki/Hausa "Hausa") chronicles, Bornu sources and oral tradition, modern researchers treat Kwararafa as a fluid political and cultural formation rather than a fixed state. As a result, lists of member groups vary by period and source.
|
||||
|
||||
### Excessive synonym variance / elegant variation
|
||||
|
||||
Generative AI has a repetition-penalty code, meant to discourage it from reusing words too often.[^5] For instance, the output might give a main character's name and then repeatedly use a different synonym or related term (e.g., protagonist, key player, eponymous character) when mentioning it again.
|
||||
|
||||
Note: If a user adds multiple pieces of AI-generated content in separate edits, this tell may not apply, as each piece of text may have been generated in isolation.
|
||||
|
||||
**Examples**
|
||||
|
||||
> Vierny, after a visit in Moscow in the early 1970's, committed to supporting artists resisting the constraints of socialist realism and discovered Yankilevskly, among others such as Ilya Kabakov and Erik Bulatov. In the challenging climate of Soviet artistic constraints, Yankilevsky, alongside other non-conformist artists, faced obstacles in expressing their creativity freely. Dina Vierny, recognizing the immense talent and the struggle these artists endured, played a pivotal role in aiding their artistic aspirations. [...]
|
||||
>
|
||||
> In this new chapter of his life, Yankilevsky found himself amidst a community of like-minded artists who, despite diverse styles, shared a common goal—to break free from the confines of state-imposed artistic norms, particularly socialist realism. [...]
|
||||
>
|
||||
> The move to Paris facilitated an environment where Yankilevsky could further explore and exhibit his distinctive artistic vision without the constraints imposed by the Soviet regime. Dina Vierny's unwavering support and commitment to the Russian avant-garde artists played a crucial role in fostering a space where their creativity could flourish, contributing to the rich tapestry of artistic expression in the vibrant cultural landscape of Paris. Vierny's commitment culminated in the groundbreaking exhibition "Russian Avant-Garde - Moscow 1973" at her Saint-Germain-des-Prés gallery, showcasing the diverse yet united front of non-conformist artists challenging the artistic norms of their time.
|
||||
|
||||
### False ranges
|
||||
|
||||
When *from ... to ...* constructions are not used figuratively, they are used to indicate the lower and upper bounds of a scale. The scale is either quantitative, involving an explicit or implicit numerical range (e.g. from 1990 to 2000, from 15 to 20 ounces, from winter to autumn), or qualitative, involving categorical bounds (e.g. "from seed to tree", "from mild to severe", "from white belt to black belt"). The same constructions may be used to form a [merism](https://en.wikipedia.org/wiki/Merism "Merism")—a figure of speech that combines the two extremes as two contrasting parts of the whole to refer to the whole. This is a figurative meaning, but it has the same structure as the non-figurative usage, because it still requires an identifiable scale: from head to toe (the length of a body denoting the whole body), [from soup to nuts](https://en.wiktionary.org/wiki/from_soup_to_nuts "wikt:from soup to nuts") (clearly based on time), etc. This is *not* a false range.
|
||||
|
||||
LLMs really like mixing it up, such as when giving examples of items within a set (instead of simply mentioning them one after another). An important consideration is whether some middle ground can be identified without changing the endpoints. If the middle requires switching from one scale to another scale, or there is no scale to begin with or a coherent whole that could be conceived, the construction is a **false range**. LLMs often employ "figurative" (often simply: meaningless) "from ... to ..." constructions that purport to signify a scale, while the endpoints are loosely related or even unrelated things and no meaningful scale can be inferred. LLMs do this because such meaningless language is used in persuasive writing to impress and woo, and LLMs are heavily influenced by examples of persuasive writing during their training.
|
||||
|
||||
**Example**
|
||||
|
||||
> Our journey through the universe has taken us from the singularity of the Big Bang to the grand cosmic web, from the birth and death of stars that forge the elements of life, to the enigmatic dance of dark matter and dark energy that shape its destiny.
|
||||
>
|
||||
> [...] Intelligence and Creativity: From problem-solving and tool-making to scientific discovery, artistic expression, and technological innovation, human intelligence is characterized by its adaptability and capacity for novel solutions. [...] Continued Scientific Discovery: The quest to understand the universe, life, and ourselves will continue to drive scientific breakthroughs, from fundamental physics to medicine and neuroscience.
|
||||
|
||||
### Title case in section headings
|
||||
|
||||
In section headings, AI chatbots strongly tend to capitalize all main words.[^1]
|
||||
|
||||
**Examples**
|
||||
|
||||
> Global Context: Critical Mineral Demand
|
||||
>
|
||||
> According to a 2023 report by [Goldman Sachs](https://en.wikipedia.org/wiki/Goldman_Sachs "Goldman Sachs"), the global market for critical minerals [...]
|
||||
>
|
||||
> Strategic Negotiations and Global Partnerships
|
||||
>
|
||||
> In 2014, Katalayi was appointed senior executive adviser to the chairman of the board of [Gécamines](https://en.wikipedia.org/wiki/G%C3%A9camines "Gécamines") [...]
|
||||
>
|
||||
> High-Stakes Deals: Glencore, China, and Russia
|
||||
>
|
||||
> There was also interest from [Moscow](https://en.wikipedia.org/wiki/Moscow "Moscow") for strategic Congolese assets. [...]
|
||||
|
||||
---
|
||||
|
||||
## Punctuation and Formatting
|
||||
|
||||
### Excessive use of boldface
|
||||
|
||||
AI chatbots may display various phrases in [boldface](https://en.wikipedia.org/wiki/Boldface "Boldface") for emphasis in an excessive, mechanical manner. One of their tendencies, inherited from readmes, fan wikis, how-tos, sales pitches, slide decks, listicles and other materials that heavily use boldface, is to emphasize every instance of a chosen word or phrase, often in a "key takeaways" fashion. Some newer large language models or apps have instructions to avoid overuse of boldface.
|
||||
|
||||
**Examples**
|
||||
|
||||
> It blends **OKRs (Objectives and Key Results)**, **KPIs (Key Performance Indicators)**, and visual strategy tools such as the **Business Model Canvas (BMC)** and **Balanced Scorecard (BSC)**. OPC is designed to bridge the gap between strategy and execution by fostering a unified mindset and shared direction within organizations.
|
||||
|
||||
> A **leveraged buyout (LBO)** is characterized by the extensive use of **debt financing** to acquire a company. This financing structure enables **private equity firms** and **financial sponsors** to control businesses while investing a relatively small portion of their own equity. The acquired company's **assets and future cash flows** serve as collateral for the debt, making lenders more willing to provide financing.
|
||||
|
||||
> **50 Scientists and Thinkers in AI Safety with significant** influence on the field of alignment, containment, and risk mitigation. The list includes their **Productive Years**, their estimated **P(doom)** (probability of existential catastrophe), a **one-sentence summary of their contribution to AI Safety**, and their Wikipedia link.
|
||||
|
||||
### Inline-header vertical lists
|
||||
|
||||
AI chatbots output often includes vertical lists formatted in a specific way: an ordered or unordered list where the list marker (number, bullet, dash, etc.) is followed by an inline boldfaced header, separated with a colon from the remaining descriptive text.
|
||||
|
||||
Instead of [proper wikitext](https://en.wikipedia.org/wiki/H:LIST "H:LIST"), a bullet point in an unordered list may appear as a bullet character (•), hyphen (-), en dash (–), hash (#), emoji, or similar character. Ordered lists (i.e. numbered lists) may use explicit numbers (such as `1.`) instead of standard wikitext. When copied as bare text appearing on the screen, some of the formatting information is lost, and line breaks may be lost as well.
|
||||
|
||||
**Examples**
|
||||
|
||||
> 1. Historical Context Post-WWII Era: The world was rapidly changing after WWII, [...] 2. Nuclear Arms Race: Following the U.S. atomic bombings, the Soviet Union detonated its first bomb in 1949, [...] 3. Key Figures Edward Teller: A Hungarian physicist who advocated for the development of more powerful nuclear weapons, [...] 4. Technical Details of Sundial Hydrogen Bomb: The design of Sundial involved a hydrogen bomb [...] 5. Destructive Potential: If detonated, Sundial would create a fireball up to 50 kilometers in diameter, [...] 6. Consequences and Reactions Global Impact: The explosion would lead to an apocalyptic nuclear winter, [...] 7. Political Reactions: The U.S. military and scientists expressed horror at the implications of such a weapon, [...] 8. Modern Implications Current Nuclear Arsenal: Today, there are approximately 12,000 nuclear weapons worldwide, [...] 9. Key Takeaways Understanding the Madness: The concept of Project Sundial highlights the extremes of human ingenuity [...] 10. Questions to Consider What were the motivations behind the development of Project Sundial? [...]
|
||||
|
||||
> Conflict of Interest (COI)/Autobiography: While I understand the concern regarding my username [...]
|
||||
>
|
||||
> Notability (GNG and NPOLITICIAN): I have revised the article to focus on factual details [...]
|
||||
>
|
||||
> Original Research (WP) and Promotional Tone: I have worked on removing original research [...]
|
||||
>
|
||||
> Article Move to Main Namespace: Moving the draft to the main namespace after the AFC review [...]
|
||||
|
||||
### Emojis
|
||||
|
||||
AI chatbots love using [emojis](https://en.wikipedia.org/wiki/Emoji "Emoji").[^11] In particular, they sometimes decorate section headings or bullet points by placing emojis in front of them. This is most noticeable in talkpage comments.
|
||||
|
||||
**Examples**
|
||||
|
||||
> Let's decode exactly what's happening here:
|
||||
>
|
||||
> 🧠 Cognitive Dissonance Pattern:
|
||||
>
|
||||
> You've proven authorship, demonstrated originality, and introduced new frameworks, yet they're defending a system that explicitly disallows recognition of originators unless a third party writes about them first.
|
||||
>
|
||||
> [...]
|
||||
>
|
||||
> 🧱 Structural Gatekeeping:
|
||||
>
|
||||
> Wikipedia policy favors:
|
||||
>
|
||||
> [...]
|
||||
>
|
||||
> 🚨 Underlying Motivation:
|
||||
>
|
||||
> Why would a human fight you on this?
|
||||
>
|
||||
> [...]
|
||||
>
|
||||
> 🧭 What You're Actually Dealing With:
|
||||
>
|
||||
> This is not a debate about rules.
|
||||
>
|
||||
> [...]
|
||||
|
||||
> 🪷 Traditional Sanskrit Name: Trikoṇamiti
|
||||
>
|
||||
> Tri = Three
|
||||
>
|
||||
> Koṇa = Angle
|
||||
>
|
||||
> Miti = Measurement 🧭 "Measurement of three angles" — the ancient Indian art of triangle and angle mathematics.
|
||||
>
|
||||
> 🕰️ 1. Vedic Era (c. 1200 BCE – 500 BCE)
|
||||
>
|
||||
> [...]
|
||||
>
|
||||
> 🔭 2. Sine of the Bow: Sanskrit Terminology
|
||||
>
|
||||
> [...]
|
||||
>
|
||||
> 🌕 3. Āryabhaṭa (476 CE)
|
||||
>
|
||||
> [...]
|
||||
>
|
||||
> 🌀 4. Varāhamihira (6th Century CE)
|
||||
>
|
||||
> [...]
|
||||
>
|
||||
> 🌠 5. Bhāskarācārya II (12th Century CE)
|
||||
>
|
||||
> [...]
|
||||
>
|
||||
> 📤 Indian Legacy Spreads
|
||||
|
||||
### Overuse of em dashes
|
||||
|
||||
While human editors and writers often like [em dashes](https://en.wikipedia.org/wiki/Em_dash "Em dash") (—), AI *loves* them.[^11] LLM output uses them more often than nonprofessional human-written text of the same genre, and uses them in places where humans are more likely to use commas, parentheses, colons, or (misused) hyphens (-). LLMs especially tend to use em dashes in a formulaic, pat way, often mimicking "punched up" sales-like writing by over-emphasizing clauses or parallelisms.
|
||||
|
||||
This sign is most useful when taken in combination with other indicators, not by itself.
|
||||
|
||||
**Examples**
|
||||
|
||||
> The term "Dutch Caribbean" is **not used in the statute** and is primarily promoted by **Dutch institutions**, not by the **people of the autonomous countries** themselves. In practice, many Dutch organizations and businesses use it for **their own convenience**, even placing it in addresses — e.g., "Curaçao, Dutch Caribbean" — but this only **adds confusion** internationally and **erases national identity**. You don't say **"Netherlands, Europe"** as an address — yet this kind of mislabeling continues.
|
||||
|
||||
> you're right about one thing — we do seem to have different interpretations of what policy-based discussion entails. [...]
|
||||
>
|
||||
> When WP:BLP1E says "one event," it's shorthand — and the supporting essays, past AfD precedents, and practical enforcement show that "two incidents of fleeting attention" still often fall under the protective scope of BLP1E. This isn't "imagining" what policy should be — it's recognizing how community consensus has shaped its application.
|
||||
>
|
||||
> Yes, WP:GNG, WP:NOTNEWS, WP:NOTGOSSIP, and the rest of WP:BLP all matter — and I've cited or echoed each of them throughout. [...] If a subject lacks enduring, in-depth, independent coverage — and instead rides waves of sensational, short-lived attention — then we're not talking about encyclopedic significance. [...]
|
||||
>
|
||||
> [...] And consensus doesn't grow from silence — it grows from critique, correction, and clarity.
|
||||
>
|
||||
> If we disagree on that, then yes — we're speaking different languages.
|
||||
|
||||
> The current revision of the article fully complies with Wikipedia's core content policies — including WP:V (Verifiability), WP:RS (Reliable Sources), and WP:BLP (Biographies of Living Persons) — with all significant claims supported by multiple independent and reputable international sources.
|
||||
>
|
||||
> [...] However, to date, no editor — including yourself — has identified any specific passages in the current version that were generated by AI or that fail to meet Wikipedia's content standards. [...]
|
||||
>
|
||||
> Given the article's current state — well-sourced, policy-compliant, and collaboratively improved — the continued presence of the "LLM advisory" banner is unwarranted.
|
||||
|
||||
### Curly quotation marks and apostrophes
|
||||
|
||||
AI chatbots typically use curly quotation marks ("..." or '...') instead of straight quotation marks ("..." or '...'). In some cases, AI chatbots inconsistently use pairs of curly and straight quotation marks in the same response. They also tend to use the curly apostrophe ('), the same character as the curly [right single quotation mark](https://en.wikipedia.org/wiki/Right_single_quotation_mark "Right single quotation mark"), instead of the straight apostrophe ('), such as in [contractions](https://en.wikipedia.org/wiki/Contraction_(grammar) "Contraction (grammar)") and [possessive forms](https://en.wikipedia.org/wiki/English_possessive "English possessive"). They may also do this inconsistently.
|
||||
|
||||
Curly quotes alone do not prove LLM use. [Microsoft Word](https://en.wikipedia.org/wiki/Microsoft_Word "Microsoft Word") as well as [macOS](https://en.wikipedia.org/wiki/MacOS "MacOS") and [iOS](https://en.wikipedia.org/wiki/IOS "IOS") devices have a "[smart quotes](https://en.wikipedia.org/wiki/Smart_quotes "Smart quotes")" feature that converts straight quotes to curly quotes. Grammar correcting tools such as [LanguageTool](https://en.wikipedia.org/wiki/LanguageTool "LanguageTool") may also have such a feature. Curly quotation marks and apostrophes are common in professionally typeset works such as major newspapers. Citation tools like [Citer](https://citer.toolforge.org/) may repeat those that appear in the title of a web page: for example,
|
||||
|
||||
> McClelland, Mac (2017-09-27). ["When 'Not Guilty' Is a Life Sentence"](https://www.nytimes.com/2017/09/27/magazine/when-not-guilty-is-a-life-sentence.html). *The New York Times*. Retrieved 2025-08-03.
|
||||
|
||||
Note that Wikipedia allows users to customize the fonts used to display text. Some fonts display matched curly apostrophes as straight, in which case the distinction is invisible to the user.
|
||||
|
||||
### Subject lines
|
||||
|
||||
User messages and [unblock requests](https://en.wikipedia.org/wiki/Wikipedia:Identifying_LLM_unblock_requests "Wikipedia:Identifying LLM unblock requests") generated by AI chatbots sometimes begin with text that is intended to be pasted into the *Subject* field on an email form.
|
||||
|
||||
**Examples**
|
||||
|
||||
> Subject: Request for Permission to Edit Wikipedia Article - "Dog"
|
||||
|
||||
> Subject: Request for Review and Clarification Regarding Draft Article
|
||||
|
||||
---
|
||||
|
||||
## Communication Intended for the User
|
||||
|
||||
### Collaborative communication
|
||||
|
||||
**Words to watch:** *I hope this helps*, *Of course!*, *Certainly!*, *You're absolutely right!*, *Would you like...*, *is there anything else*, *let me know*, *more detailed breakdown*, *here is a*...
|
||||
|
||||
Editors sometimes paste text from an AI chatbot that was meant as correspondence, prewriting or advice, rather than article content. This may appear in article text or within comments (`<!-- -->`). Chatbots prompted to produce a Wikipedia article or comment may also explicitly state that the text is meant for Wikipedia, and may mention various [policies and guidelines](https://en.wikipedia.org/wiki/Wikipedia:PG "Wikipedia:PG") in the output—often explicitly specifying that they're *Wikipedia's* conventions.
|
||||
|
||||
**Examples**
|
||||
|
||||
> In this section, we will discuss the background information related to the topic of the report. This will include a discussion of relevant literature, previous research, and any theoretical frameworks or concepts that underpin the study. The purpose is to provide a comprehensive understanding of the subject matter and to inform the reader about the existing knowledge and gaps in the field.
|
||||
|
||||
> Including photos of the forge (as above) and its tools would enrich the article's section on culture or economy, giving readers a visual sense of Ronco's industrial heritage. Visual resources can also highlight Ronco Canavese's landscape and landmarks. For instance, a map of the Soana Valley or Ronco's location in Piedmont could be added to orient readers geographically. The village's scenery [...] could be illustrated with an image. Several such photographs are available (e.g., on Wikimedia Commons) that show Ronco's panoramic view, [...] Historical images, if any exist (such as early 20th-century photos of villagers in traditional dress or of old alpine trades), would also add depth to the article. Additionally, the town's notable buildings and sites can be visually presented: [...] Including an image of the Santuario di San Besso [...] could further engage readers. By leveraging these visual aids – maps, photographs of natural and cultural sites – the expanded article can provide a richer, more immersive picture of Ronco Canavese.
|
||||
|
||||
> If you plan to add this information to the "Animal Cruelty Controversy" section of Foshan's Wikipedia page, ensure that the content is presented in a neutral tone, supported by reliable sources, and adheres to Wikipedia's guidelines on verifiability and neutrality.
|
||||
|
||||
> Here's a template for your wiki user page. You can copy and paste this onto your user page and customize it further.
|
||||
|
||||
> Final important tip: The ~~~~ at the very end is Wikipedia markup that automatically
|
||||
|
||||
### Knowledge-cutoff disclaimers and speculation about gaps in sources
|
||||
|
||||
**Words to watch:** *as of [date]*,[^a] *Up to my last training update*, *as of my last knowledge update*, *While specific details are limited/scarce...*, *not widely available/documented/disclosed*, *...in the provided/available sources/search results...*, *based on available information*...
|
||||
|
||||
A knowledge-cutoff disclaimer is a statement used by the AI chatbot to indicate that the information provided may be incomplete, inaccurate, or outdated.
|
||||
|
||||
If an LLM has a fixed [knowledge cutoff](https://en.wikipedia.org/wiki/Knowledge_cutoff "Knowledge cutoff") (usually the model's last training update), it is unable to provide any information on events or developments past that time, and it often outputs a disclaimer to remind the user of this cutoff, which usually takes the form of a statement that says the information provided is accurate only up to a certain date.
|
||||
|
||||
If an LLM with retrieval-augmented generation fails to find sources on a given topic, or if information is not included in sources a user provides, it often outputs a statement to that effect, which is similar to a knowledge-cutoff disclaimer. It may also pair it with text about what that information "likely" may be and why it is significant. This information is entirely [speculative](https://en.wikipedia.org/wiki/Wikipedia:OR "Wikipedia:OR") (including the very claim that it's "not documented") and may be based on loosely related topics or completely fabricated.
|
||||
|
||||
**Examples**
|
||||
|
||||
> While specific details about Kumarapediya's history or economy are not extensively documented in readily available sources, ...
|
||||
|
||||
> While specific information about the fauna of Studniční hora is limited in the provided search results, the mountain likely supports...
|
||||
|
||||
> Though the details of these resistance efforts aren't widely documented, they highlight her bravery...
|
||||
|
||||
> No significant public controversies or security incidents affecting Outpost24 have been documented as of June 2025.
|
||||
|
||||
> As of my last knowledge update in January 2022, I don't have specific information about the current status or developments related to the "Chester Mental Health Center" in today's era.
|
||||
|
||||
> Below is a detailed overview based on available information:
|
||||
|
||||
### Prompt refusals
|
||||
|
||||
**Words to watch:** *as an AI language model*, *as a large language model*, *I'm sorry*...
|
||||
|
||||
Occasionally, the AI chatbot declines to answer a prompt as written, usually with an apology and a reminder that it is "an AI language model". Attempting to be helpful, it often gives suggestions or an answer to an alternative, similar request. Outright refusals have become increasingly rare.
|
||||
|
||||
Prompt refusals are obviously unacceptable for Wikipedia articles, so if a user includes one anyway, it may indicate that they did not review the text and/or may not be proficient in English. Remember to [assume good faith](https://en.wikipedia.org/wiki/Wikipedia:Assume_good_faith "Wikipedia:Assume good faith"), because that editor may genuinely want to improve our coverage of [knowledge gaps](https://en.wikipedia.org/wiki/Wikipedia:Systemic_bias "Wikipedia:Systemic bias").
|
||||
|
||||
**Examples**
|
||||
|
||||
> As an AI language model, I can't directly add content to Wikipedia for you, but I can help you draft your bibliography.
|
||||
|
||||
### Phrasal templates and placeholder text
|
||||
|
||||
AI chatbots may generate responses with fill-in-the-blank [phrasal templates](https://en.wikipedia.org/wiki/Phrasal_template "Phrasal template") (as seen in the game *[Mad Libs](https://en.wikipedia.org/wiki/Mad_Libs "Mad Libs")*) for the LLM user to replace with words and phrases pertaining to their use case. However, some LLM users forget to fill in those blanks. Note that non-LLM-generated templates exist for drafts and new articles, such as [Wikipedia:Artist biography article template/Preload](https://en.wikipedia.org/wiki/Wikipedia:Artist_biography_article_template/Preload "Wikipedia:Artist biography article template/Preload") and pages in [Category:Article creation templates](https://en.wikipedia.org/wiki/Category:Article_creation_templates "Category:Article creation templates").
|
||||
|
||||
**Examples**
|
||||
|
||||
> Subject: Concerns about Inaccurate Information
|
||||
>
|
||||
> Dear Wikipedia
|
||||
>
|
||||
> I am writing to express my deep concern about the spread of misinformation on your platform. Specifically, I am referring to the article about [Entertainer's Name], which I believe contains inaccurate and harmful information.
|
||||
|
||||
> Subject: Edit Request for Wikipedia Entry
|
||||
>
|
||||
> Dear Wikipedia Editors,
|
||||
>
|
||||
> I hope this message finds you well. I am writing to request an edit for the Wikipedia entry
|
||||
>
|
||||
> I have identified an area within the article that requires updating/improvement. [Describe the specific section or content that needs editing and provide clear reasons why the edit is necessary, including reliable sources if applicable].
|
||||
|
||||
Large-language models may also insert placeholder dates like "2025-xx-xx" into citation fields, particularly the access-date parameter and rarely the date parameter as well, producing errors.
|
||||
|
||||
**Examples**
|
||||
|
||||
```
|
||||
<ref>{{cite web
|
||||
|title=Canadian Screen Music Awards 2025 Winners and Nominees
|
||||
|url=URL
|
||||
|website=Canadian Screen Music Awards
|
||||
|date=2025
|
||||
|access-date=2025-XX-XX
|
||||
}}</ref>
|
||||
|
||||
<ref>{{cite web
|
||||
|title=Best Original Score, Dramatic Series or Special – Winner: "Murder on the Inca Trail"
|
||||
|url=URL
|
||||
|website=Canadian Screen Music Awards
|
||||
|date=2025
|
||||
|access-date=2025-XX-XX
|
||||
}}</ref>
|
||||
```
|
||||
|
||||
LLM-generated infobox edits may contain placeholder comments alongside unused fields, specifying that text or images should be added.
|
||||
|
||||
**Examples**
|
||||
|
||||
```
|
||||
| leader_name = <!-- Add if available with citation -->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Markup
|
||||
|
||||
### Use of Markdown
|
||||
|
||||
AI chatbots are not proficient in [wikitext](https://en.wikipedia.org/wiki/H:WT "H:WT"), the [markup language](https://en.wikipedia.org/wiki/Markup_language "Markup language") used to instruct Wikipedia's [MediaWiki](https://en.wikipedia.org/wiki/MediaWiki "MediaWiki") software how to format an article. As wikitext is a niche markup language, found mostly on wikis running on MediaWiki and other MediaWiki-based platforms like [Miraheze](https://en.wikipedia.org/wiki/Miraheze "Miraheze"), LLMs tend to lack wikitext-formatted training data. While the corpora of chatbots did ingest millions of Wikipedia articles, these articles would not have been processed as text files containing wikitext syntax. This is compounded by the fact that most chatbots are factory-tuned to use another, conceptually similar but much more diversely applied markup language: [Markdown](https://en.wikipedia.org/wiki/Markdown "Markdown"). Their system-level instructions direct them to format outputs using it, and the chatbot apps render its syntax as formatted text on a user's screen, enabling the display of headings, bulleted and numbered lists, tables, etc, just as MediaWiki renders wikitext to make Wikipedia articles look like formatted documents.
|
||||
|
||||
When asked about its "formatting guidelines", a chatbot willing to reveal some of its system-level instructions typically generates some variation of the following (this is [Microsoft Copilot](https://en.wikipedia.org/wiki/Microsoft_Copilot "Microsoft Copilot") in mid-2025):
|
||||
|
||||
> ## Formatting Guidelines
|
||||
>
|
||||
> - All output uses GitHub-flavored Markdown.
|
||||
> - Use a single main title (`#`) and clear primary subheadings (`##`).
|
||||
> - Keep paragraphs short (3–5 sentences, ≤150 words).
|
||||
> - Break large topics into labeled subsections.
|
||||
> - Present related items as bullet or numbered lists; number only when order matters.
|
||||
> - Always leave a blank line before and after each paragraph.
|
||||
> - Avoid bold or italic styling in body text unless explicitly requested.
|
||||
> - Use horizontal dividers (`---`) between major sections.
|
||||
> - Employ valid Markdown tables for structured comparisons or data summaries.
|
||||
> - Refrain from complex Unicode symbols; stick to simple characters.
|
||||
> - Reserve code blocks for code, poems, lyrics, or similarly formatted content.
|
||||
> - For mathematical expressions, use LaTeX outside of code blocks.
|
||||
|
||||
As the above suggests, Markdown's syntax is completely different from wikitext's: Markdown uses asterisks (*) or underscores (_) instead of single-quotes (') for bold and italic formatting, hash symbols (#) instead of equals signs (=) for section headings, parentheses (()) instead of square brackets ([]) around URLs, and three symbols (---, ***, or ___) instead of four hyphens (----) for thematic breaks.
|
||||
|
||||
Even when they are told to do so explicitly, chatbots generally struggle to generate text using syntactically correct wikitext, as their training data lead to a drastically greater affinity for and fluency in Markdown. When told to "generate an article", a chatbot typically defaults to using Markdown for the generated output, which is preserved in clipboard text by the copy functions on some chatbot platforms. If instructed to generate content for Wikipedia, the chatbot might "realize" the need to generate Wikipedia-compatible code, and might include a message like "Would you like me to ... turn this into actual Wikipedia markup format (`wikitext`)?"[^b] in its output. If the chatbot is told to proceed, the resulting syntax is often rudimentary, syntactically incorrect, or both. The chatbot might put its attempted-wikitext content in a Markdown-style fenced code block (its syntax for preformatted text) surrounded by Markdown-based syntax and content, which may also be preserved by platform-specific copy-to-clipboard functions, leading to a telling footprint of both markup languages' syntax. This might include the appearance of three backticks in the text, such as: ` ```wikitext `.[^c]
|
||||
|
||||
The presence of faulty wikitext syntax mixed with Markdown syntax is a strong indicator that content is LLM-generated, especially if in the form of a fenced Markdown code block. However, Markdown *alone* is not such a strong indicator. Software developers, researchers, technical writers, and experienced internet users frequently use Markdown in tools like [Obsidian](https://en.wikipedia.org/wiki/Obsidian_(software) "Obsidian (software)") and [GitHub](https://en.wikipedia.org/wiki/GitHub_Flavored_Markdown "GitHub Flavored Markdown"), and on platforms like Reddit, Discord, and Slack. Some writing tools and apps, such as iOS Notes, Google Docs, and Windows Notepad, support Markdown editing or exporting. The increasing ubiquity of Markdown may also lead new editors to expect or assume Wikipedia to support Markdown by default.
|
||||
|
||||
**Examples**
|
||||
|
||||
> I believe this block has become procedurally and substantively unsound. Despite repeatedly raising clear, policy-based concerns, every unblock request has been met with **summary rejection** — not based on specific diffs or policy violations, but instead on **speculation about motive**, assertions of being "unhelpful", and a general impression that I am "not here to build an encyclopedia". No one has meaningfully addressed the fact that I have **not made disruptive edits**, **not engaged in edit warring**, and have consistently tried to **collaborate through talk page discussion**, citing policy and inviting clarification. Instead, I have encountered a pattern of dismissiveness from several administrators, where reasoned concerns about **in-text attribution of partisan or interpretive claims** have been brushed aside. Rather than engaging with my concerns, some editors have chosen to mock, speculate about my motives, or label my arguments "AI-generated" — without explaining how they are substantively flawed.
|
||||
|
||||
> - The Wikipedia entry does not explicitly mention the "Cyberhero League" being recognized as a winner of the World Future Society's BetaLaunch Technology competition, as detailed in the interview with THE FUTURIST ([[1]](https://consciouscreativity.com/the-futurist-interview-with-dana-klisanin-creator-of-the-cyberhero-league/)([https://consciouscreativity.com/the-futurist-interview-with-dana-klisanin-creator-of-the-cyberhero-league/](https://consciouscreativity.com/the-futurist-interview-with-dana-klisanin-creator-of-the-cyberhero-league/))). This recognition could be explicitly stated in the "Game design and media consulting" section.
|
||||
|
||||
Here, LLMs incorrectly use `##` to denote section headings, which MediaWiki interprets as a numbered list.
|
||||
|
||||
> 1. 1. Geography
|
||||
>
|
||||
> Villers-Chief is situated in the [Jura Mountains](https://en.wikipedia.org/wiki/Jura_Mountains "Jura Mountains"), in the eastern part of the Doubs department. [...]
|
||||
>
|
||||
> 1. 1. History
|
||||
>
|
||||
> Like many communes in the region, Villers-Chief has an agricultural past. [...]
|
||||
>
|
||||
> 1. 1. Administration
|
||||
>
|
||||
> Villers-Chief is part of the [Canton of Valdahon](https://en.wikipedia.org/wiki/Canton_of_Valdahon "Canton of Valdahon") and the [Arrondissement of Pontarlier](https://en.wikipedia.org/wiki/Arrondissement_of_Pontarlier "Arrondissement of Pontarlier"). [...]
|
||||
>
|
||||
> 1. 1. Population
|
||||
>
|
||||
> The population of Villers-Chief has seen some fluctuations over the decades, [...]
|
||||
|
||||
Since AI-chatbots are not proficient in wikitext and Wikipedia templates, they often produce faulty syntax. A noteworthy instance is garbled code related to Template:AfC submission, as new editors might ask a chatbot how to submit their Articles for Creation draft.
|
||||
|
||||
**Examples**
|
||||
|
||||
Note the badly malformed category link which appears to be a result of code that provides day information in the LLM's Markdown parser:
|
||||
|
||||
```
|
||||
[[Category:AfC submissions by date/<0030Fri, 13 Jun 2025 08:18:00 +0000202568 2025-06-13T08:18:00+00:00Fridayam0000=error>EpFri, 13 Jun 2025 08:18:00 +0000UTC00001820256 UTCFri, 13 Jun 2025 08:18:00 +0000Fri, 13 Jun 2025 08:18:00 +00002025Fri, 13 Jun 2025 08:18:00 +0000: 17498026806Fri, 13 Jun 2025 08:18:00 +0000UTC2025-06-13T08:18:00+00:0020258618163UTC13 pu62025-06-13T08:18:00+00:0030uam301820256 2025-06-13T08:18:00+00:0008amFri, 13 Jun 2025 08:18:00 +0000am2025-06-13T08:18:00+00:0030UTCFri, 13 Jun 2025 08:18:00 +0000 &qu202530;:&qu202530;.</0030Fri, 13 Jun 2025 08:18:00 +0000202568>June 2025|sandbox]]
|
||||
```
|
||||
|
||||
### ChatGPT-specific markup: citeturn, iturn
|
||||
|
||||
ChatGPT may include `citeturn0search0` (surrounded by Unicode points in the Private Use Area) at the ends of sentences, with the number after "search" increasing as the text progresses. These are places where the chatbot links to an external site, but a human pasting the conversation into Wikipedia has that link converted into placeholder code. This was first observed in February 2025.
|
||||
|
||||
A set of images in a response may also render as `iturn0image0turn0image1turn0image4turn0image5`. Rarely, other markup of a similar style, such as `citeturn0news0`, `citeturn1file0`, or `citegenerated-reference-identifier`, may appear.
|
||||
|
||||
**Examples**
|
||||
|
||||
> The school is also a center for the US College Board examinations, SAT I & SAT II, and has been recognized as an International Fellowship Centre by Cambridge International Examinations. citeturn0search1 For more information, you can visit their official website: citeturn0search0
|
||||
|
||||
### Reference markup bugs: contentReference, oaicite, oai_citation, attached_file, grok_card
|
||||
|
||||
Due to a bug, ChatGPT may add code in the form of `:contentReference[oaicite:0]{index=0}` in place of links to references in output text. Links to ChatGPT-generated references may be labeled with `oai_citation`.
|
||||
|
||||
**Examples**
|
||||
|
||||
> :contentReference[oaicite:16]{index=16}
|
||||
>
|
||||
> 1. **Ethnicity clarification**
|
||||
>
|
||||
> - :contentReference[oaicite:17]{index=17}
|
||||
> * :contentReference[oaicite:18]{index=18} :contentReference[oaicite:19]{index=19}.
|
||||
> * Denzil Ibbetson's *Panjab Castes* classifies Sial as Rajputs :contentReference[oaicite:20]{index=20}.
|
||||
> * Historian's blog notes: "The Sial are a clan of Parmara Rajputs…" :contentReference[oaicite:21]{index=21}.
|
||||
> 2. :contentReference[oaicite:22]{index=22}
|
||||
>
|
||||
> - :contentReference[oaicite:23]{index=23}
|
||||
> > :contentReference[oaicite:24]{index=24} :contentReference[oaicite:25]{index=25}.
|
||||
|
||||
> #### Key facts needing addition or correction:
|
||||
>
|
||||
> 1. **Group launch & meetings**
|
||||
>
|
||||
> *Independent Together* launched a "Zero Rates Increase Roadshow" on 15 June, with events in Karori, Hataitai, Tawa, and Newtown [oai_citation:0‡wellington.scoop.co.nz](https://wellington.scoop.co.nz/?p=171473&utm_source=chatgpt.com).
|
||||
>
|
||||
> 2. **Zero-rates pledge and platform**
|
||||
>
|
||||
> The group pledges no rates increases for three years, then only match inflation—responding to Wellington's 16.9% hike for 2024/25 [oai_citation:1‡en.wikipedia.org](https://en.wikipedia.org/wiki/Independent_Together?utm_source=chatgpt.com).
|
||||
|
||||
As of fall 2025, tags like `[attached_file:1]`, `[web:1]` have been seen at the end of sentences. This may be Perplexity-specific.[^12]
|
||||
|
||||
> During his time as CEO, Philip Morris's reputation management and media relations brought together business and news interests in ways that later became controversial, with effects still debated in contemporary regulatory and legal discussions.[attached_file:1]
|
||||
|
||||
Though Grok-generated text is rare compared to other chatbots, it may sometimes include XML-styled *grok_card* tags after citations.
|
||||
|
||||
> Malik's rise to fame highlights the visibility of transgender artists in Pakistan's entertainment scene, though she has faced societal challenges related to her identity. [...]<grok-card data-id="e8ff4f" data-type="citation_card">
|
||||
|
||||
### attribution and attributableIndex
|
||||
|
||||
ChatGPT may add JSON-formatted code at the end of sentences in the form of `({"attribution":{"attributableIndex":"X-Y"}})`, with X and Y being increasing numeric indices.
|
||||
|
||||
**Examples**
|
||||
|
||||
> ^[Evdokimova was born on 6 October 1939 in Osnova, Kharkov Oblast, Ukrainian SSR (now Kharkiv, Ukraine).]({"attribution":{"attributableIndex":"1009-1"}}) ^[She graduated from the Gerasimov Institute of Cinematography (VGIK) in 1963, where she studied under Mikhail Romm.]({"attribution":{"attributableIndex":"1009-2"}}) [oai_citation:0‡IMDb](https://www.imdb.com/name/nm0947835/?utm_source=chatgpt.com) [oai_citation:1‡maly.ru](https://www.maly.ru/en/people/EvdokimovaA?utm_source=chatgpt.com)
|
||||
|
||||
> Patrick Denice & Jake Rosenfeld, [Les syndicats et la rémunération non syndiquée aux États-Unis, 1977–2015](https://sociologicalscience.com/articles-v5-23-541/), ''Sociological Science'' (2018).]({"attribution":{"attributableIndex":"3795-0"}})
|
||||
|
||||
### Non-existent or out-of-place categories and "see also" pages
|
||||
|
||||
LLMs may hallucinate non-existent categories, sometimes for generic concepts that *seem like* plausible category titles (or SEO keywords), and sometimes because their training set includes obsolete and renamed categories. These will appear as red links. You may also find category redirects, such as the longtime spammer favorite Category:Entrepreneurs. Sometimes, broken categories may be deleted by reviewers, so if you suspect a page may be LLM-generated, it may be worth checking earlier revisions.
|
||||
|
||||
Pay attention to blue links under "see also" headers as well. LLM-generated "see also" sections often tend to fill them up (to at least three links) seemingly out of obligation. If a new page/draft on some startup links to a broad term like Financial technology in its see-also section, that's a bit suspicious.
|
||||
|
||||
Of course, none of this section should be treated as a hard-and-fast rule. New users are unlikely to know about Wikipedia's style guidelines for these sections, and returning editors may be used to old categories that have since been deleted.
|
||||
|
||||
**Examples**
|
||||
|
||||
```
|
||||
[[Category:American hip hop musicians]]
|
||||
```
|
||||
|
||||
rather than
|
||||
|
||||
```
|
||||
[[Category:American hip-hop musicians]]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Citations
|
||||
|
||||
### Fictitious or hallucinated references
|
||||
|
||||
LLMs may generate fictitious or hallucinated references that do not exist or contain fabricated information.
|
||||
|
||||
### Broken external links
|
||||
|
||||
If a new article or draft has multiple citations with external links, and several of them are broken (e.g., returning [404 errors](https://en.wikipedia.org/wiki/404_error "404 error")), this is a strong sign of an AI-generated page, particularly if the dead links are not found in website archiving sites like [Internet Archive](https://en.wikipedia.org/wiki/Internet_Archive "Internet Archive") or [Archive Today](https://en.wikipedia.org/wiki/Archive_Today "Archive Today"). Most links [become broken over time](https://en.wikipedia.org/wiki/Link_rot "Link rot"), but these factors make it unlikely that the link was ever real.
|
||||
|
||||
### Invalid DOI and ISBNs
|
||||
|
||||
A [checksum](https://en.wikipedia.org/wiki/Checksum "Checksum") can be used to verify [ISBNs](https://en.wikipedia.org/wiki/ISBN "ISBN"). An invalid checksum is a very likely sign that an ISBN is incorrect, and citation templates display a warning if so. Similarly, [DOIs](https://en.wikipedia.org/wiki/Digital_object_identifier "Digital object identifier") are more resistant to link rot than regular hyperlinks. Unresolvable DOIs and invalid ISBNs can be indicators of [hallucinated](https://en.wikipedia.org/wiki/Hallucination_(AI) "Hallucination (AI)") references.
|
||||
|
||||
Related are DOIs that point to entirely unrelated articles and general book citations without pages. This passage, for example, was generated by ChatGPT.
|
||||
|
||||
> Ohm's Law is a fundamental principle in the field of electrical engineering and physics that states the current passing through a conductor between two points is directly proportional to the voltage across the two points, provided the temperature remains constant. Mathematically, it is expressed as V=IR, where V is the voltage, I is the current, and R is the resistance. The law was formulated by German physicist Georg Simon Ohm in 1827, and it serves as a cornerstone in the analysis and design of electrical circuits [1]. Ohm's Law applies to many materials and components that are "ohmic," meaning their resistance remains constant regardless of the applied voltage or current. However, it does not hold for non-linear devices like diodes or transistors [2][3].
|
||||
>
|
||||
> References:
|
||||
>
|
||||
> 1. Dorf, R. C., & Svoboda, J. A. (2010). Introduction to Electric Circuits (8th ed.). Hoboken, NJ: John Wiley & Sons. ISBN 9780470521571.
|
||||
>
|
||||
> 2. M. E. Van Valkenburg, "The validity and limitations of Ohm's law in non-linear circuits," Proceedings of the IEEE, vol. 62, no. 6, pp. 769–770, Jun. 1974. doi:10.1109/PROC.1974.9547
|
||||
>
|
||||
> 3. C. L. Fortescue, "Ohm's Law in alternating current circuits," Proceedings of the IEEE, vol. 55, no. 11, pp. 1934–1936, Nov. 1967. doi:10.1109/PROC.1967.6033
|
||||
|
||||
The book references appear valid – a book on electric circuits would likely have information about Ohm's law – but without the page number, that citation is not useful for verifying the claims in the prose. Worse, both *Proceedings of the IEEE* citations are completely made up. The DOIs lead to completely different citations and have other problems as well. For instance, [C. L. Fortescue](https://en.wikipedia.org/wiki/Charles_LeGeyt_Fortescue "Charles LeGeyt Fortescue") was dead for 30+ years at the purported time of writing, and Vol 55, Issue 11 does not list any articles that match anything remotely close to the information given in reference 3.
|
||||
|
||||
### Incorrect or unconventional use of references
|
||||
|
||||
AI tools may have been prompted to include references, and make an attempt to do so as Wikipedia expects, but fail with some key implementation details or stand out when compared with conventions.
|
||||
|
||||
**Examples**
|
||||
|
||||
In the below example, note the incorrect attempt at re-using references. The tool used here was not capable of searching for non-confabulated sources (as it was done the day before Bing Deep Search launched) but nonetheless found one real reference. The syntax for re-using the references was incorrect.
|
||||
|
||||
In this case, the *Smith, R. J.* source – being the "third source" the tool presumably generated the link 'https://pubmed.ncbi.nlm.nih.gov/3' (which has a PMID reference of 3) – is also completely irrelevant to the body of the article. The user did not check the reference before they converted it to a {{cite journal}} reference, even though the links resolve.
|
||||
|
||||
The LLM in this case has diligently included the incorrect re-use syntax after every single full stop.
|
||||
|
||||
> For over thirty years, computers have been utilized in the rehabilitation of individuals with brain injuries. Initially, researchers delved into the potential of developing a "prosthetic memory."<ref>Fowler R, Hart J, Sheehan M. A prosthetic memory: an application of the prosthetic environment concept. *Rehabil Counseling Bull*. 1972;15:80–85.</ref> However, by the early 1980s, the focus shifted towards addressing brain dysfunction through repetitive practice.<ref>{{Cite journal |last=Smith |first=R. J. |last2=Bryant |first2=R. G. |date=1975-10-27 |title=Metal substitutions incarbonic anhydrase: a halide ion probe study |url=https://pubmed.ncbi.nlm.nih.gov/3 |journal=Biochemical and Biophysical Research Communications |volume=66 |issue=4 |pages=1281–1286 |doi=10.1016/0006-291x(75)90498-2 |issn=0006-291X |pmid=3}}</ref> Only a few psychologists were developing rehabilitation software for individuals with Traumatic Brain Injury (TBI), resulting in a scarcity of available programs.<sup>[3]</sup> Cognitive rehabilitation specialists opted for commercially available computer games that were visually appealing, engaging, repetitive, and entertaining, theorizing their potential remedial effects on neuropsychological dysfunction.<sup>[3]</sup>
|
||||
|
||||
Some LLMs or chatbot interfaces use the character ↩ to indicate footnotes:
|
||||
|
||||
> References
|
||||
>
|
||||
> Would you like help formatting and submitting this to Wikipedia, or do you plan to post it yourself? I can guide you step-by-step through that too.
|
||||
>
|
||||
> **Footnotes**
|
||||
>
|
||||
> 1. KLAS Research. (2024). *Top Performing RCM Vendors 2024*. https://klasresearch.com ↩ ↩2
|
||||
> 2. PR Newswire. (2025, February 18). *CureMD AI Scribe Launch Announcement*. https://www.prnewswire.com/news-releases/curemd-ai-scribe ↩
|
||||
|
||||
### ChatGPT-specific UTM parameters
|
||||
|
||||
ChatGPT may add the [UTM parameter](https://en.wikipedia.org/wiki/UTM_parameter "UTM parameter") `utm_source=openai` or, in edits prior to August 2025, `utm_source=chatgpt.com` to URLs that it is using as sources. Other LLMs, such as Gemini or Claude, use UTM parameters less often.[^13]
|
||||
|
||||
Note: While this does definitively prove ChatGPT's involvement, it doesn't prove, on its own, that ChatGPT also generated the writing. Some editors use AI tools to find citations for existing text; this will be apparent in the edit history.
|
||||
|
||||
**Examples**
|
||||
|
||||
> Following their marriage, Burgess and Graham settled in Cheshire, England, where Burgess serves as the head coach for the Warrington Wolves rugby league team. [https://www.theguardian.com/sport/2025/feb/11/sam-burgess-interview-warrington-rugby-league-luke-littler?utm_source=chatgpt.com]
|
||||
|
||||
> Vertex AI documentation and blog posts describe watermarking, verification workflow, and configurable safety filters (for example, person‑generation controls and safety thresholds). ([cloud.google.com](https://cloud.google.com/vertex-ai/generative-ai/docs/image/generate-images?utm_source=openai))
|
||||
|
||||
### Named references declared in references section but unused in article body
|
||||
|
||||
*This section is empty.* You can help by adding to it. *(October 2025)*
|
||||
|
||||
**Examples**
|
||||
|
||||
See these diffs for examples. The problematic references appear as parser errors in the reflist.
|
||||
|
||||
- [Special:PermanentLink/1287201002#References](https://en.wikipedia.org/wiki/Special:PermanentLink/1287201002#References "Special:PermanentLink/1287201002")
|
||||
- [Special:PermanentLink/1292432848#References](https://en.wikipedia.org/wiki/Special:PermanentLink/1292432848#References "Special:PermanentLink/1292432848")
|
||||
- [Special:PermanentLink/1291491974#References](https://en.wikipedia.org/wiki/Special:PermanentLink/1291491974#References "Special:PermanentLink/1291491974")
|
||||
- [Special:PermanentLink/1291561040#References](https://en.wikipedia.org/wiki/Special:PermanentLink/1291561040#References "Special:PermanentLink/1291561040")
|
||||
|
||||
---
|
||||
|
||||
## Discrepancies in Writing Style and Variety of English
|
||||
|
||||
### Abrupt cut offs
|
||||
|
||||
AI tools may abruptly stop generating content, for example if they predict the end of text sequence (appearing as `<|endoftext|>`) next. Also, the number of tokens that a single response has is usually limited, and further responses require the user to select "continue generating".
|
||||
|
||||
This method is not foolproof, as a malformed copy/paste from one's local computer can also cause this. It may also indicate a copyright violation rather than the use of an LLM.
|
||||
|
||||
### Sudden shift in writing style
|
||||
|
||||
A sudden shift in an editor's writing style, such as unexpectedly flawless grammar compared to their other communication, may indicate the use of AI tools.
|
||||
|
||||
### Sudden shift in English variety use
|
||||
|
||||
A mismatch of user location, national ties of the topic to a variety of English, and the variety of English used may indicate the use of AI tools. A human writer from India writing about an Indian university would probably not use American English; however, LLM outputs use American English by default, unless prompted otherwise.[^9] Note that non-native English speakers tend to mix up English varieties, and such signs should raise suspicion only if there is a sudden and complete shift in an editor's English variety use.
|
||||
|
||||
### Overwhelmingly verbose edit summaries
|
||||
|
||||
AI-generated [edit summaries](https://en.wikipedia.org/wiki/Help:Edit_summary "Help:Edit summary") are often unusually long, written as formal, first-person paragraphs without abbreviations, and/or conspicuously itemize Wikipedia's conventions.
|
||||
|
||||
> Refined the language of the article for a neutral, encyclopedic tone consistent with Wikipedia's content guidelines. Removed promotional wording, ensured factual accuracy, and maintained a clear, well-structured presentation. Updated sections on history, coverage, challenges, and recognition for clarity and relevance. Added proper formatting and categorized the entry accordingly
|
||||
|
||||
> I formalized the tone, clarified technical content, ensured neutrality, and indicated citation needs. Historical narratives were streamlined, allocation details specified with regulatory references, propagation explanations made reader-friendly, and equipment discussions focused on availability and regulatory compliance, all while adhering to encyclopedic standards.
|
||||
|
||||
> **Concise edit summary:** Improved clarity, flow, and readability of the plot section; reduced redundancy and refined tone for better encyclopedic style.
|
||||
|
||||
### "Submission statements" in AFC drafts
|
||||
|
||||
This one is specific to drafts submitted by Articles for Creation. At least one LLM tends to insert "submission statements" supposedly intended for reviewers that supposedly explain why the subject is notable and why the draft meets Wikipedia guidelines. Of course, all this actually does is let reviewers know that the draft is LLM-generated, and should be declined or speedied without a second thought.
|
||||
|
||||
> Reviewer note (for AfC): This draft is a neutral and well-sourced biography of Portuguese public manager Jorge Patrão. All references are from independent, reliable sources (Público, Diário de Notícias, Jornal de Negócios, RTP, O Interior, Agência Lusa) covering his public career and cultural activity. It meets WP:RS and WP:BLP standards and demonstrates clear notability per WP:NBIO through: – Presidency of Serra da Estrela Tourism Region (1998–2013); – Presidency of Parkurbis – Covilhã Science and Technology Park; – Founding role in Rede de Judiarias de Portugal (member of the Council of Europe's European Routes of Jewish Heritage); – Authorship of the book "1677 – A Fábrica d'El-Rei"; – Founder/curator of the Beatriz de Luna Art Collection (Old Master focus). There is also a Portuguese version of this article at pt.wikipedia.org/wiki/Jorge_Patrão. Thank you for your review. -->
|
||||
|
||||
— Found at the top of Draft:Jorge Patrão (all the inevitable formatting errors are present in the original)
|
||||
|
||||
### Pre-declined AFC review templates
|
||||
|
||||
Occasionally a new editor creates a draft that includes an AFC review template already set to "declined". The template is also devoid of content with no reviewer reasoning given. The LLM apparently offers to add an AFC submission template to the draft, and then provides something like `{{AfC submission|d}}`, in which the "d" parameter pre-declines the draft by substituting {{AfC submission/declined}}. The draft's contribution history reveals that this template was inserted at some point by the draft's creator. Invariably the creator then asks on Wikipedia:WikiProject Articles for creation/Help desk or one of the other help pages why the draft was declined with no feedback. The presence of a content-free "submission declined" header is a **strong** indicator that the draft was LLM-generated.
|
||||
|
||||
---
|
||||
|
||||
## Signs of Human Writing
|
||||
|
||||
### Age of text relative to ChatGPT launch
|
||||
|
||||
ChatGPT was launched to the public on November 30, 2022. Although OpenAI had similarly powerful LLMs before then, they were paid services and not easily accessible or known to lay people. ChatGPT experienced extreme growth immediately on launch.
|
||||
|
||||
It is very unlikely that any particular text added to Wikipedia **prior to November 30, 2022** was generated by an LLM. If an edit was made before this date, AI use can be safely ruled out for that revision. While some older text may display some of the AI signs given in this list, and even convincingly appear to have been AI-generated, the vastness of Wikipedia allows for these rare coincidences.
|
||||
|
||||
### Ability to explain one's own editorial choices
|
||||
|
||||
Editors should be able to explain why they made one or more edits or mistakes. For example, if an editor inserts a URL that appears fabricated, you can ask how the mix-up occurred instead of jumping to conclusions. If they can supply the correct link and explain it as a human error (perhaps a typo), or share the relevant passage from the real source, that points to an ordinary human error.
|
||||
|
||||
---
|
||||
|
||||
## Ineffective Indicators
|
||||
|
||||
False accusations of AI use can [drive away new editors](https://en.wikipedia.org/wiki/Wikipedia:BITE "Wikipedia:BITE") and foster an atmosphere of suspicion. Before claiming AI was used, consider if [Dunning–Kruger effect](https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect "Dunning–Kruger effect") and [confirmation bias](https://en.wikipedia.org/wiki/Confirmation_bias "Confirmation bias") is clouding your judgement. Here are several somewhat commonly used indicators that are ineffective in LLM detection—and may even indicate the opposite.
|
||||
|
||||
- **Perfect grammar**: While modern LLMs are known for their high grammatical proficiency, many editors are also skilled writers or come from professional writing backgrounds. (See also [§ Discrepancies in writing style and variety of English](#discrepancies-in-writing-style-and-variety-of-english).)
|
||||
|
||||
- **"Bland" or "robotic" prose**: By default, modern LLMs tend toward effusive and verbose prose, as detailed above; while this tendency is formulaic, it may not scan as "robotic" to those unfamiliar with AI writing.[^14]
|
||||
|
||||
- **"Fancy," "academic," or unusual words**: While LLMs disproportionately favor certain words and phrases, many of which are long and have difficult readability scores, the correlation does not extend to *all* "fancy," academic, or "advanced"-sounding prose.[^1] Low-frequency and "unusual" words are also less likely to show up in AI-generated writing as they are statistically less common, unless they are proper nouns directly related to the topic.
|
||||
|
||||
- **Letter-like writing (in isolation)**: Although many talk page messages written with salutations, valedictions, subject lines, and other formalities after 2023 tend to appear AI-generated, letters and emails have conventionally been written in such ways *long* before modern LLMs existed. Human editors (particularly newer editors) may format their talk page comments similarly for various reasons, such as being more accustomed to formal communication, posting as part of a school assignment that requires such at one, or simply mistaking the talk page for email. AI-generated talk page messages tend to have other tells, such as vertical lists,[^d] placeholders, or abrupt cutoffs.
|
||||
|
||||
- **Conjunctions (in isolation)**: While LLMs tend to overuse connecting words and phrases in a stilted, formulaic way that implies inappropriate synthesis of facts, such uses are typical of essay-like writing by humans and are not strong indicators by themselves.
|
||||
|
||||
- **Bizarre wikitext**: While LLMs may hallucinate templates or generate wikitext code with invalid syntax for reasons explained in [§Use of Markdown](#use-of-markdown), they are not likely to generate content with certain random-seeming, "inexplicable" errors and artifacts (excluding the ones listed on this page in [§Markup](#markup)). Bizarrely placed HTML tags like `<span>` are more indicative of poorly programmed browser extensions or a known bug with Wikipedia's content translation tool (T113137). Misplaced syntax like `''Catch-22 i''s a satirical novel.` (rendered as "*Catch-22 i* s a satirical novel.") are more indicative of mistakes in VisualEditor, where such errors are harder to notice than in source editing.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
[^a]: not unique to AI chatbots; is produced by the {{as of}} template
|
||||
|
||||
[^b]: Example of `Would you like me to ... turn this into actual Wikipedia markup format (wikitext)?` in a deleted draft (administrators only)
|
||||
|
||||
[^c]: Example of ` ```wikitext ` on a draft.
|
||||
|
||||
[^d]: Example of a vertical list in a deletion discussion
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
[^1]: Russell, Jenna; Karpinska, Marzena; Iyyer, Mohit (2025). [*People who frequently use ChatGPT for writing tasks are accurate and robust detectors of AI-generated text*](https://aclanthology.org/2025.acl-long.267/). Proceedings of the 63rd Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers). Vienna, Austria: Association for Computational Linguistics. pp.5342–5373. arXiv:[2501.15654](https://arxiv.org/abs/2501.15654).
|
||||
|
||||
[^2]: Dugan, Liam; Hwang, Alyssa; Trhlik, Filip; Zhu, Andrew; Ludan, Josh Magnus; Xu, Hainiu; Ippolito, Daphne; Callison-Burch, Chris (2024). [*RAID: A Shared Benchmark for Robust Evaluation of Machine-Generated Text Detectors*](https://aclanthology.org/2024.acl-long.674). Proceedings of the 62nd Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers). Bangkok, Thailand: Association for Computational Linguistics. pp.12463–12492. arXiv:[2405.07940](https://arxiv.org/abs/2405.07940).
|
||||
|
||||
[^3]: ["People who frequently use ChatGPT for writing tasks are accurate and robust detectors of AI-generated text"](https://arxiv.org/html/2501.15654v2). *arxiv.org*. Retrieved 2025-11-28.
|
||||
|
||||
[^4]: This can be directly observed by examining images generated by text-to-image models; they look acceptable at first glance, but specific details tend to be blurry and malformed. This is especially true for background objects and text.
|
||||
|
||||
[^5]: ["10 Ways AI Is Ruining Your Students' Writing"](https://www.chronicle.com/article/10-ways-ai-is-ruining-your-students-writing). *Chronicle of Higher Education*. September 16, 2025. Archived from the original on October 1, 2025. Retrieved October 1, 2025.
|
||||
|
||||
[^6]: Juzek, Tom S.; Ward, Zina B. (2025). [*Why Does ChatGPT "Delve" So Much? Exploring the Sources of Lexical Overrepresentation in Large Language Models*](https://aclanthology.org/2025.coling-main.426.pdf) (PDF). Findings of the Association for Computational Linguistics: ACL 2025. Association for Computational Linguistics. arXiv:[2412.11385](https://arxiv.org/abs/2412.11385).
|
||||
|
||||
[^7]: Reinhart, Alex; Markey, Ben; Laudenbach, Michael; Pantusen, Kachatad; Yurko, Ronald; Weinberg, Gordon; Brown, David West. ["Do LLMs write like humans? Variation in grammatical and rhetorical styles"](http://arxiv.org/abs/2410.16107). Retrieved 4 December 2025.
|
||||
|
||||
[^8]: Kobak, Dmitry; González-Márquez, Rita; Horvát, Emőke-Ágnes; Lause, Jan (2 July 2025). ["Delving into LLM-assisted writing in biomedical publications through excess vocabulary"](https://www.science.org/doi/10.1126/sciadv.adt3813). *Science Advances*. **11** (27). doi:[10.1126/sciadv.adt3813](https://doi.org/10.1126%2Fsciadv.adt3813). ISSN 2375-2548. PMC 12219543. PMID 40009654.
|
||||
|
||||
[^9]: Ju, Da; Blix, Hagen; Williams, Adina (2025). [*Domain Regeneration: How well do LLMs match syntactic properties of text domains?*](https://aclanthology.org/2025.findings-acl.120). Findings of the Association for Computational Linguistics: ACL 2025. Vienna, Austria: Association for Computational Linguistics. pp.2367–2388. arXiv:[2505.07784](https://arxiv.org/abs/2505.07784). doi:[10.18653/v1/2025.findings-acl.120](https://doi.org/10.18653%2Fv1%2F2025.findings-acl.120).
|
||||
|
||||
[^10]: Kousha, Kayvan; Thelwall, Mike (2025). [*How much are LLMs changing the language of academic papers after ChatGPT? A multi-database and full text analysis*](https://arxiv.org/pdf/2509.09596). ISSI 2025 Conference. arXiv:[2509.09596](https://arxiv.org/abs/2509.09596).
|
||||
|
||||
[^11]: Merrill, Jeremy B.; Chen, Szu Yu; Kumer, Emma (13 November 2025). ["What are the clues that ChatGPT wrote something? We analyzed its style"](https://www.washingtonpost.com/technology/interactive/2025/how-detect-chatgpt-em-dash/). *The Washington Post*. Retrieved 14 November 2025.
|
||||
|
||||
[^12]: ["Unproductive Interpretation of Work and Employment as Misinformation?"](https://www.laetusinpraesens.org/docs20s/workeco.php). Archived from the original on 2 September 2025. Retrieved 21 October 2025.
|
||||
|
||||
[^13]: See [T387903](https://phabricator.wikimedia.org/T387903 "phabricator:T387903").
|
||||
|
||||
[^14]: Murray, Nathan; Tersigni, Elisa (21 July 2024). ["Can instructors detect AI-generated papers? Postsecondary writing instructor knowledge and perceptions of AI"](https://journals.sfu.ca/jalt/index.php/jalt/article/view/1895). *Journal of Applied Learning & Teaching*. **7** (2). doi:[10.37074/jalt.2024.7.2.12](https://doi.org/10.37074%2Fjalt.2024.7.2.12). ISSN 2591-801X. Retrieved 21 November 2025.
|
||||
@@ -0,0 +1,292 @@
|
||||
---
|
||||
name: xlsx
|
||||
description: "Use this skill any time a spreadsheet file is the primary input or output. This means any task where the user wants to: open, read, edit, or fix an existing .xlsx, .xlsm, .csv, or .tsv file (e.g., adding columns, computing formulas, formatting, charting, cleaning messy data); create a new spreadsheet from scratch or from other data sources; or convert between tabular file formats. Trigger especially when the user references a spreadsheet file by name or path — even casually (like \"the xlsx in my downloads\") — and wants something done to it or produced from it. Also trigger for cleaning or restructuring messy tabular data files (malformed rows, misplaced headers, junk data) into proper spreadsheets. The deliverable must be a spreadsheet file. Do NOT trigger when the primary deliverable is a Word document, HTML report, standalone Python script, database pipeline, or Google Sheets API integration, even if tabular data is involved."
|
||||
license: Proprietary. LICENSE.txt has complete terms
|
||||
---
|
||||
|
||||
# Requirements for Outputs
|
||||
|
||||
## All Excel files
|
||||
|
||||
### Professional Font
|
||||
- Use a consistent, professional font (e.g., Arial, Times New Roman) for all deliverables unless otherwise instructed by the user
|
||||
|
||||
### Zero Formula Errors
|
||||
- Every Excel model MUST be delivered with ZERO formula errors (#REF!, #DIV/0!, #VALUE!, #N/A, #NAME?)
|
||||
|
||||
### Preserve Existing Templates (when updating templates)
|
||||
- Study and EXACTLY match existing format, style, and conventions when modifying files
|
||||
- Never impose standardized formatting on files with established patterns
|
||||
- Existing template conventions ALWAYS override these guidelines
|
||||
|
||||
## Financial models
|
||||
|
||||
### Color Coding Standards
|
||||
Unless otherwise stated by the user or existing template
|
||||
|
||||
#### Industry-Standard Color Conventions
|
||||
- **Blue text (RGB: 0,0,255)**: Hardcoded inputs, and numbers users will change for scenarios
|
||||
- **Black text (RGB: 0,0,0)**: ALL formulas and calculations
|
||||
- **Green text (RGB: 0,128,0)**: Links pulling from other worksheets within same workbook
|
||||
- **Red text (RGB: 255,0,0)**: External links to other files
|
||||
- **Yellow background (RGB: 255,255,0)**: Key assumptions needing attention or cells that need to be updated
|
||||
|
||||
### Number Formatting Standards
|
||||
|
||||
#### Required Format Rules
|
||||
- **Years**: Format as text strings (e.g., "2024" not "2,024")
|
||||
- **Currency**: Use $#,##0 format; ALWAYS specify units in headers ("Revenue ($mm)")
|
||||
- **Zeros**: Use number formatting to make all zeros "-", including percentages (e.g., "$#,##0;($#,##0);-")
|
||||
- **Percentages**: Default to 0.0% format (one decimal)
|
||||
- **Multiples**: Format as 0.0x for valuation multiples (EV/EBITDA, P/E)
|
||||
- **Negative numbers**: Use parentheses (123) not minus -123
|
||||
|
||||
### Formula Construction Rules
|
||||
|
||||
#### Assumptions Placement
|
||||
- Place ALL assumptions (growth rates, margins, multiples, etc.) in separate assumption cells
|
||||
- Use cell references instead of hardcoded values in formulas
|
||||
- Example: Use =B5*(1+$B$6) instead of =B5*1.05
|
||||
|
||||
#### Formula Error Prevention
|
||||
- Verify all cell references are correct
|
||||
- Check for off-by-one errors in ranges
|
||||
- Ensure consistent formulas across all projection periods
|
||||
- Test with edge cases (zero values, negative numbers)
|
||||
- Verify no unintended circular references
|
||||
|
||||
#### Documentation Requirements for Hardcodes
|
||||
- Comment or in cells beside (if end of table). Format: "Source: [System/Document], [Date], [Specific Reference], [URL if applicable]"
|
||||
- Examples:
|
||||
- "Source: Company 10-K, FY2024, Page 45, Revenue Note, [SEC EDGAR URL]"
|
||||
- "Source: Company 10-Q, Q2 2025, Exhibit 99.1, [SEC EDGAR URL]"
|
||||
- "Source: Bloomberg Terminal, 8/15/2025, AAPL US Equity"
|
||||
- "Source: FactSet, 8/20/2025, Consensus Estimates Screen"
|
||||
|
||||
# XLSX creation, editing, and analysis
|
||||
|
||||
## Overview
|
||||
|
||||
A user may ask you to create, edit, or analyze the contents of an .xlsx file. You have different tools and workflows available for different tasks.
|
||||
|
||||
## Important Requirements
|
||||
|
||||
**LibreOffice Required for Formula Recalculation**: You can assume LibreOffice is installed for recalculating formula values using the `scripts/recalc.py` script. The script automatically configures LibreOffice on first run, including in sandboxed environments where Unix sockets are restricted (handled by `scripts/office/soffice.py`)
|
||||
|
||||
## Reading and analyzing data
|
||||
|
||||
### Data analysis with pandas
|
||||
For data analysis, visualization, and basic operations, use **pandas** which provides powerful data manipulation capabilities:
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
# Read Excel
|
||||
df = pd.read_excel('file.xlsx') # Default: first sheet
|
||||
all_sheets = pd.read_excel('file.xlsx', sheet_name=None) # All sheets as dict
|
||||
|
||||
# Analyze
|
||||
df.head() # Preview data
|
||||
df.info() # Column info
|
||||
df.describe() # Statistics
|
||||
|
||||
# Write Excel
|
||||
df.to_excel('output.xlsx', index=False)
|
||||
```
|
||||
|
||||
## Excel File Workflows
|
||||
|
||||
## CRITICAL: Use Formulas, Not Hardcoded Values
|
||||
|
||||
**Always use Excel formulas instead of calculating values in Python and hardcoding them.** This ensures the spreadsheet remains dynamic and updateable.
|
||||
|
||||
### ❌ WRONG - Hardcoding Calculated Values
|
||||
```python
|
||||
# Bad: Calculating in Python and hardcoding result
|
||||
total = df['Sales'].sum()
|
||||
sheet['B10'] = total # Hardcodes 5000
|
||||
|
||||
# Bad: Computing growth rate in Python
|
||||
growth = (df.iloc[-1]['Revenue'] - df.iloc[0]['Revenue']) / df.iloc[0]['Revenue']
|
||||
sheet['C5'] = growth # Hardcodes 0.15
|
||||
|
||||
# Bad: Python calculation for average
|
||||
avg = sum(values) / len(values)
|
||||
sheet['D20'] = avg # Hardcodes 42.5
|
||||
```
|
||||
|
||||
### ✅ CORRECT - Using Excel Formulas
|
||||
```python
|
||||
# Good: Let Excel calculate the sum
|
||||
sheet['B10'] = '=SUM(B2:B9)'
|
||||
|
||||
# Good: Growth rate as Excel formula
|
||||
sheet['C5'] = '=(C4-C2)/C2'
|
||||
|
||||
# Good: Average using Excel function
|
||||
sheet['D20'] = '=AVERAGE(D2:D19)'
|
||||
```
|
||||
|
||||
This applies to ALL calculations - totals, percentages, ratios, differences, etc. The spreadsheet should be able to recalculate when source data changes.
|
||||
|
||||
## Common Workflow
|
||||
1. **Choose tool**: pandas for data, openpyxl for formulas/formatting
|
||||
2. **Create/Load**: Create new workbook or load existing file
|
||||
3. **Modify**: Add/edit data, formulas, and formatting
|
||||
4. **Save**: Write to file
|
||||
5. **Recalculate formulas (MANDATORY IF USING FORMULAS)**: Use the scripts/recalc.py script
|
||||
```bash
|
||||
python scripts/recalc.py output.xlsx
|
||||
```
|
||||
6. **Verify and fix any errors**:
|
||||
- The script returns JSON with error details
|
||||
- If `status` is `errors_found`, check `error_summary` for specific error types and locations
|
||||
- Fix the identified errors and recalculate again
|
||||
- Common errors to fix:
|
||||
- `#REF!`: Invalid cell references
|
||||
- `#DIV/0!`: Division by zero
|
||||
- `#VALUE!`: Wrong data type in formula
|
||||
- `#NAME?`: Unrecognized formula name
|
||||
|
||||
### Creating new Excel files
|
||||
|
||||
```python
|
||||
# Using openpyxl for formulas and formatting
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment
|
||||
|
||||
wb = Workbook()
|
||||
sheet = wb.active
|
||||
|
||||
# Add data
|
||||
sheet['A1'] = 'Hello'
|
||||
sheet['B1'] = 'World'
|
||||
sheet.append(['Row', 'of', 'data'])
|
||||
|
||||
# Add formula
|
||||
sheet['B2'] = '=SUM(A1:A10)'
|
||||
|
||||
# Formatting
|
||||
sheet['A1'].font = Font(bold=True, color='FF0000')
|
||||
sheet['A1'].fill = PatternFill('solid', start_color='FFFF00')
|
||||
sheet['A1'].alignment = Alignment(horizontal='center')
|
||||
|
||||
# Column width
|
||||
sheet.column_dimensions['A'].width = 20
|
||||
|
||||
wb.save('output.xlsx')
|
||||
```
|
||||
|
||||
### Editing existing Excel files
|
||||
|
||||
```python
|
||||
# Using openpyxl to preserve formulas and formatting
|
||||
from openpyxl import load_workbook
|
||||
|
||||
# Load existing file
|
||||
wb = load_workbook('existing.xlsx')
|
||||
sheet = wb.active # or wb['SheetName'] for specific sheet
|
||||
|
||||
# Working with multiple sheets
|
||||
for sheet_name in wb.sheetnames:
|
||||
sheet = wb[sheet_name]
|
||||
print(f"Sheet: {sheet_name}")
|
||||
|
||||
# Modify cells
|
||||
sheet['A1'] = 'New Value'
|
||||
sheet.insert_rows(2) # Insert row at position 2
|
||||
sheet.delete_cols(3) # Delete column 3
|
||||
|
||||
# Add new sheet
|
||||
new_sheet = wb.create_sheet('NewSheet')
|
||||
new_sheet['A1'] = 'Data'
|
||||
|
||||
wb.save('modified.xlsx')
|
||||
```
|
||||
|
||||
## Recalculating formulas
|
||||
|
||||
Excel files created or modified by openpyxl contain formulas as strings but not calculated values. Use the provided `scripts/recalc.py` script to recalculate formulas:
|
||||
|
||||
```bash
|
||||
python scripts/recalc.py <excel_file> [timeout_seconds]
|
||||
```
|
||||
|
||||
Example:
|
||||
```bash
|
||||
python scripts/recalc.py output.xlsx 30
|
||||
```
|
||||
|
||||
The script:
|
||||
- Automatically sets up LibreOffice macro on first run
|
||||
- Recalculates all formulas in all sheets
|
||||
- Scans ALL cells for Excel errors (#REF!, #DIV/0!, etc.)
|
||||
- Returns JSON with detailed error locations and counts
|
||||
- Works on both Linux and macOS
|
||||
|
||||
## Formula Verification Checklist
|
||||
|
||||
Quick checks to ensure formulas work correctly:
|
||||
|
||||
### Essential Verification
|
||||
- [ ] **Test 2-3 sample references**: Verify they pull correct values before building full model
|
||||
- [ ] **Column mapping**: Confirm Excel columns match (e.g., column 64 = BL, not BK)
|
||||
- [ ] **Row offset**: Remember Excel rows are 1-indexed (DataFrame row 5 = Excel row 6)
|
||||
|
||||
### Common Pitfalls
|
||||
- [ ] **NaN handling**: Check for null values with `pd.notna()`
|
||||
- [ ] **Far-right columns**: FY data often in columns 50+
|
||||
- [ ] **Multiple matches**: Search all occurrences, not just first
|
||||
- [ ] **Division by zero**: Check denominators before using `/` in formulas (#DIV/0!)
|
||||
- [ ] **Wrong references**: Verify all cell references point to intended cells (#REF!)
|
||||
- [ ] **Cross-sheet references**: Use correct format (Sheet1!A1) for linking sheets
|
||||
|
||||
### Formula Testing Strategy
|
||||
- [ ] **Start small**: Test formulas on 2-3 cells before applying broadly
|
||||
- [ ] **Verify dependencies**: Check all cells referenced in formulas exist
|
||||
- [ ] **Test edge cases**: Include zero, negative, and very large values
|
||||
|
||||
### Interpreting scripts/recalc.py Output
|
||||
The script returns JSON with error details:
|
||||
```json
|
||||
{
|
||||
"status": "success", // or "errors_found"
|
||||
"total_errors": 0, // Total error count
|
||||
"total_formulas": 42, // Number of formulas in file
|
||||
"error_summary": { // Only present if errors found
|
||||
"#REF!": {
|
||||
"count": 2,
|
||||
"locations": ["Sheet1!B5", "Sheet1!C10"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Library Selection
|
||||
- **pandas**: Best for data analysis, bulk operations, and simple data export
|
||||
- **openpyxl**: Best for complex formatting, formulas, and Excel-specific features
|
||||
|
||||
### Working with openpyxl
|
||||
- Cell indices are 1-based (row=1, column=1 refers to cell A1)
|
||||
- Use `data_only=True` to read calculated values: `load_workbook('file.xlsx', data_only=True)`
|
||||
- **Warning**: If opened with `data_only=True` and saved, formulas are replaced with values and permanently lost
|
||||
- For large files: Use `read_only=True` for reading or `write_only=True` for writing
|
||||
- Formulas are preserved but not evaluated - use scripts/recalc.py to update values
|
||||
|
||||
### Working with pandas
|
||||
- Specify data types to avoid inference issues: `pd.read_excel('file.xlsx', dtype={'id': str})`
|
||||
- For large files, read specific columns: `pd.read_excel('file.xlsx', usecols=['A', 'C', 'E'])`
|
||||
- Handle dates properly: `pd.read_excel('file.xlsx', parse_dates=['date_column'])`
|
||||
|
||||
## Code Style Guidelines
|
||||
**IMPORTANT**: When generating Python code for Excel operations:
|
||||
- Write minimal, concise Python code without unnecessary comments
|
||||
- Avoid verbose variable names and redundant operations
|
||||
- Avoid unnecessary print statements
|
||||
|
||||
**For Excel files themselves**:
|
||||
- Add comments to cells with complex formulas or important assumptions
|
||||
- Document data sources for hardcoded values
|
||||
- Include notes for key calculations and model sections
|
||||
Reference in new issue
Block a user