Important: All
scripts/paths are relative to this skill directory. Userun_skill_scripttool to execute scripts, or run with:cd {this_skill_dir} && python scripts/...
DOCX creation, editing, and analysis
Quick Start — Pick the Right Tool
| Task | Recommended Tool |
|---|---|
| Create a new document (report / résumé / contract / memo) | renderDocx() — millisecond render, no subprocess |
| Edit an existing .docx (content / formatting) | unpack → edit XML → pack workflow below |
| Add tracked changes / comments | unpack → edit XML → pack workflow below |
| GB/T 9704 official document | writeGongwen() (BmacClaw only) |
Create a new document (recommended path)
Call the in-process Java tool — no Node.js install, no fork, no disk round-trip:
renderDocx( markdown="# Title\n\nBody paragraph...", filename="monthly-report", pageSize="A4" )
Returns a clickable link of the form
[monthly-report.docx](/api/v1/files/generated/<uuid>) valid for 10 minutes.
The user clicks it to download — no follow-up Agent step needed.
renderDocx supports headings (# ## ###), bold (**text**), bullet
lists (- item), numbered lists (1. item), pipe-style tables, and plain
paragraphs. For images, headers/footers, or precise OOXML control, fall back
to the docx-js workflow below.
Prerequisites
- python-docx (
pip install python-docx): direct structure reading and light editing (paragraphs, styles, tables) - docx (
npm install -g docx): new document creation - LibreOffice (
soffice):.doc->.docxconversion, tracked-changes acceptance, and PDF export - pandoc: text extraction
- pdftoppm (poppler-utils): document-to-image workflows
- If
pdftoppmis unavailable, a Python fallback path may usepdf2image. - On Windows, dependencies must be installed and available in
PATH; if missing, report the dependency issue and stop (do not keep retrying).
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:
bashpython scripts/office/soffice.py --headless --convert-to docx document.doc
Reading Content
Option A: python-docx (recommended for structured access)
Install: pip install python-docx. Gives direct access to paragraphs, styles, tables, and metadata without unpacking ZIP.
pythonfrom docx import Document doc = Document("document.docx") # Paragraphs with styles for para in doc.paragraphs: print(f"[{para.style.name}] {para.text}") # Tables for i, table in enumerate(doc.tables): print(f"Table {i+1}:") for row in table.rows: print([cell.text for cell in row.cells]) # Inline styles within a paragraph for para in doc.paragraphs: for run in para.runs: print(f" run: bold={run.bold} italic={run.italic} text={run.text!r}")
Use python-docx when you need to read or lightly modify content. Fall back to the unpack/XML workflow for complex structural changes.
Option B: pandoc (plain text extraction)
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
bashpython 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):
bashpython scripts/accept_changes.py input.docx output.docx
Creating New Documents
Generate .docx files with JavaScript, then validate. Install: npm install -g docx
Setup
javascriptconst { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun, Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink, 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.
bashpython 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:
javascriptsize: { 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.
javascriptconst 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// CORRECT - use numbering config with LevelFormat.BULLET const doc = new Document({ numbering: { config: [ { reference: "bullets", levels: [{ level: 0, format: LevelFormat.BULLET, text: "\u2022", 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.
javascript// 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 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 }, children: [new Paragraph({ children: [new TextRun("Cell")] })] }) ] }) ] })
Width rules:
- Always use
WidthType.DXA- neverWidthType.PERCENTAGE - Table width must equal the sum of
columnWidths - Cell
widthmust match correspondingcolumnWidth
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// PageBreak must be inside a Paragraph new Paragraph({ children: [new PageBreak()] })
Table of Contents
javascript// Headings must use HeadingLevel ONLY - no custom styles new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" })
Headers/Footers
javascriptsections: [{ properties: { page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } }, 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
- Landscape: pass portrait dimensions - docx-js swaps width/height internally
- Never use
\n- use separate Paragraph elements - Never use unicode bullets - use
LevelFormat.BULLETwith numbering config - PageBreak must be in Paragraph
- ImageRun requires
type - Always set table
widthwith DXA - never useWidthType.PERCENTAGE - Tables need dual widths -
columnWidthsarray AND cellwidth - Use
ShadingType.CLEAR- never SOLID for table shading - TOC requires HeadingLevel only
- 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
bashpython scripts/office/unpack.py document.docx unpacked/
Extracts XML, pretty-prints, merges adjacent runs, and converts smart quotes to XML entities. Use --merge-runs false to skip run merging.
Step 2: Edit XML
Edit files in unpacked/word/. See XML Reference below for patterns.
Use "MateClaw" as the author for tracked changes and comments, unless the user explicitly requests a different name.
CRITICAL: Use smart quotes for new content:
xml<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:
bashpython 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"
Then add markers to document.xml (see Comments in XML Reference).
Step 3: Pack
bashpython 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
Common Pitfalls
- Replace entire
<w:r>elements: When adding tracked changes, replace the whole<w:r>...</w:r>block. - Preserve
<w:rPr>formatting: Copy the original run's<w:rPr>block into your tracked change runs.
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="MateClaw" 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="MateClaw" 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="MateClaw" w:date="..."> <w:r><w:delText>30</w:delText></w:r> </w:del> <w:ins w:id="2" w:author="MateClaw" 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 - mark the paragraph mark as deleted:
xml<w:p> <w:pPr> <w:rPr> <w:del w:id="1" w:author="MateClaw" w:date="2025-01-01T00:00:00Z"/> </w:rPr> </w:pPr> <w:del w:id="2" w:author="MateClaw" w:date="2025-01-01T00:00:00Z"> <w:r><w:delText>Entire paragraph content being deleted...</w:delText></w:r> </w:del> </w:p>
Rejecting another author's insertion:
xml<w:ins w:author="Jane" w:id="5"> <w:del w:author="MateClaw" w:id="10"> <w:r><w:delText>their inserted text</w:delText></w:r> </w:del> </w:ins>
Restoring another author's 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="MateClaw" w:id="10"> <w:r><w:t>deleted text</w:t></w:r> </w:ins>
Comments
After running comment.py, add markers to document.xml:
CRITICAL: <w:commentRangeStart> and <w:commentRangeEnd> are siblings of <w:r>, never inside <w:r>.
xml<w:commentRangeStart w:id="0"/> <w:r><w:t>commented 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>
Images
- Add image file to
word/media/ - Add relationship to
word/_rels/document.xml.rels:
xml<Relationship Id="rId5" Type=".../image" Target="media/image1.png"/>
- Add content type to
[Content_Types].xml:
xml<Default Extension="png" ContentType="image/png"/>
- 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>


