DOCX creation, editing, and analysis
Overview
A .docx file is a ZIP archive containing XML files.
Generating Reports (Recommended for research reports)
For structured reports with cover page, TOC, tables, images, and references, use the pre-built template. The DOCX generator accepts the same JSON schema as the PDF generator, so one report_data.json can produce both formats.
Step 1: Copy the generator script
cp /builtin-skills/docx/scripts/generate_report.js ./generate_report.js
Step 2: Build report_data.json
Two phases: write section text files, then assemble into JSON.
Phase 1 — Write each section as a plain text file using write_file:
For each major section, read_file the relevant research data, then write_file the section content directly:
read_file("research_data/literature.md") # refresh data in context
write_file("sections/sec_01_intro.txt", "...") # write section content
write_file("sections/sec_02_mutations.txt", "...") # next section
...
Each section file should be 1,000-2,000+ words with specific data, citations, and analysis.
NEVER write a Python script that contains section text as string literals. The section content goes directly into .txt files via write_file, not into Python code. Do NOT write scripts named "generate_sections", "create_content", "build_report" etc. that embed text in Python strings. If a sandbox script fails twice, switch to direct write_file calls.
Writing style — academic research report (CRITICAL):
- Write continuous flowing prose. Each paragraph: 8–10 sentences following the pattern: topic sentence → supporting evidence with specific data → analysis/comparison → transition to next point.
- Use in-text citations [1], [2] when referencing data. These render as blue superscript in the DOCX. Do NOT add a "References" list at the end of each chapter — all references go in ONE final
referencessection. - Synthesize across sources: "Study A [1] reported X, while Study B [2] found Y, suggesting that Z."
- Use academic connectives: "Furthermore", "In contrast", "These findings indicate", "Notably", "Taken together".
- NEVER use numbered-point structure (e.g. "1. Topic Title\n\nParagraph. 2. Topic Title\n\nParagraph."). Instead, use
##subheadings for structure and prose paragraphs for content. The template'srenderTextcorrectly renders##/###as formatted subheadings. - Bullet lists: max 5% of section, only for short enumerations (e.g. 4-5 drug names).
Language (CRITICAL):
- All report content (title, subtitle, section headings, body text, chart labels, table headers, cover metadata) MUST be written in the user's configured language as specified in the system prompt's
## Languagesection. - If the user's language is Chinese (
zh), write the entire report in Chinese; if English (en), write in English. Do NOT mix languages unless quoting a proper noun or technical term that has no standard translation.
Verification (before generating DOCX): Run a one-liner to count chars per section:
python3 -c "import os,glob; [print(f'{f}: {len(open(f).read())} chars') for f in sorted(glob.glob('sections/*.txt'))]; total=sum(len(open(f).read()) for f in glob.glob('sections/*.txt')); print(f'TOTAL: {total} chars, ~{total//500} pages')"
If total is under target, do ONE revision: read_file data again, then write_file to rewrite the thinnest 1-2 sections. Then proceed — no looping.
Phase 2 — Assemble into JSON using a standard assembler script:
import json, glob, os
SECTIONS_DIR = "sections"
TITLE = "Report Title"
SUBTITLE = "Subtitle"
# Section config: (file_pattern, heading_number, heading_text)
SECTION_MAP = [
("sec_01_*.txt", "1.", "Introduction"),
("sec_02_*.txt", "2.", "Background"),
("sec_03_*.txt", "3.", "Analysis"),
("sec_04_*.txt", "4.", "Discussion"),
("sec_05_*.txt", "5.", "Conclusion"),
]
data = {
"title": TITLE, "subtitle": SUBTITLE,
"short_title": TITLE[:40], "report_type": "Research Report",
"toc": True, "sections": [],
"cover_meta": [["Report Type", "Research Report"]],
}
for pattern, num, heading in SECTION_MAP:
matches = sorted(glob.glob(os.path.join(SECTIONS_DIR, pattern)))
if not matches:
continue
body = open(matches[0], encoding="utf-8").read().strip()
data["sections"].append({"type": "heading", "level": 1, "number": num, "text": heading})
data["sections"].append({"type": "text", "body": body})
# Add references section at end
data["sections"].append({"type": "heading", "level": 1, "text": "References"})
data["sections"].append({"type": "references", "items": []})
with open("report_data.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"Generated report_data.json ({len(data['sections'])} sections)")
Adding charts: Generate chart images separately using matplotlib, then include via the image section type.
CRITICAL — CJK font config (MUST include when chart has Chinese/Japanese/Korean text):
import matplotlib
matplotlib.use("Agg")
matplotlib.rcParams["font.sans-serif"] = [
"WenQuanYi Micro Hei", "SimHei", "Noto Sans CJK SC",
"Arial Unicode MS", "sans-serif",
]
matplotlib.rcParams["axes.unicode_minus"] = False
import matplotlib.pyplot as plt
# ... generate chart ...
plt.savefig("figures/chart.png", dpi=180, bbox_inches="tight")
plt.close()
Without this config, Chinese text in charts will appear as garbled boxes (□□□).
Then in the assembler:
data["sections"].append({"type": "text", "body": analysis_text})
data["sections"].append({"type": "image", "path": "figures/chart.png", "caption": "Figure 1: ..."})
Step 3: Generate the DOCX
python3 build_report_data.py # outputs report_data.json
node generate_report.js report_data.json output_report.docx
Supported section types
| Type | Required Fields | Optional Fields |
|---|---|---|
heading |
text |
level (1/2/3), number (e.g. "2.1") |
text |
body |
heading |
bullets |
items (string array) |
heading |
table |
headers, rows |
heading, caption |
kv |
items (2-element arrays) |
heading |
image |
path |
caption, width (mm), height (mm) |
callout |
body |
title |
references |
items (string array) |
heading, show_heading |
page_break |
(none) |
Template features
- Cover page: Title, subtitle, metadata table, disclaimer, accent styling
- Table of Contents: Auto-generated from headings (levels 1-3)
- Headers/Footers: Short title + report type in header, page numbers in footer
- In-text citations:
[1],[2,3]rendered as blue superscript - Tables: Alternating row colors, header styling
- Callout boxes: Accent-bar styled highlight boxes
- References: Hanging-indent numbered list
- Two-section layout: Cover page as separate section (no header/footer)
Writing guidelines for high-quality reports
- Flowing prose, NOT bullet points: Each
textbody must be continuous paragraphs of 8–10 sentences each, following: topic sentence → evidence with data → analysis → transition. NEVER write content as a list of bullet points or numbered items. Bullet lists max 5% of any section, only for short enumerations (e.g. 4-5 drug names). - Use numbered headings: Set
numberfield ("1.", "2.1", "3.2.1") for professional structure - Add table captions: Use
captionfield like "Table 3: Key Financial Metrics (FY2026)" - Include references: Always end with a
referencessection for credibility - In-text citations: Use
[1],[2],[1,2,3]markers intextbody — the template auto-renders them as blue superscript. Synthesize across sources: "Study A [1] reported X, while Study B [2] found Y, suggesting that Z." - Cover metadata: Use
cover_metato show report type, date, sector, etc. Do NOT include page count or total pages incover_meta— the page count is unknown at assembly time and including it produces inaccurate information. - Include visuals: Generate chart images with matplotlib and include via
imagesections (aim for 2-4 per report) - Academic connectives: Use "Furthermore", "In contrast", "These findings indicate", "Notably", "Taken together" for cohesive flow between ideas
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 |
| Generate structured report | Use generate_report.js — see above |
Converting .doc to .docx
Legacy .doc files must be converted before editing:
python3 /builtin-skills/docx/scripts/office/soffice.py --headless --convert-to docx document.doc
Reading Content
# Text extraction with tracked changes
pandoc --track-changes=all document.docx -o output.md
# Raw XML access
python3 /builtin-skills/docx/scripts/office/unpack.py document.docx unpacked/
Converting to Images
python3 /builtin-skills/docx/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):
python3 /builtin-skills/docx/scripts/accept_changes.py input.docx output.docx
Creating New Documents
Generate .docx files with JavaScript, then validate. Install: npm install -g docx
Setup
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.
python3 /builtin-skills/docx/scripts/office/validate.py doc.docx
Page Size
// 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:
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.
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)
// ❌ 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.
// 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.
// 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— neverWidthType.PERCENTAGE(incompatible with Google Docs) - Table width must equal the sum of
columnWidths - Cell
widthmust match correspondingcolumnWidth - Cell
marginsare 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
// 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
// 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
// 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
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
// 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
// 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
// CRITICAL: Headings must use HeadingLevel ONLY - no custom styles
new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" })
Headers/Footers
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 asheight, and setorientation: PageOrientation.LANDSCAPE - Never use
\n- use separate Paragraph elements - Never use unicode bullets - use
LevelFormat.BULLETwith numbering config - PageBreak must be in Paragraph - standalone creates invalid XML
- ImageRun requires
type- always specify png/jpg/etc - Always set table
widthwith DXA - never useWidthType.PERCENTAGE(breaks in Google Docs) - Tables need dual widths -
columnWidthsarray AND cellwidth, 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
python3 /builtin-skills/docx/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:
<!-- 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):
python3 /builtin-skills/docx/scripts/comment.py unpacked/ 0 "Comment text with & and ’"
python3 /builtin-skills/docx/scripts/comment.py unpacked/ 1 "Reply text" --parent 0 # reply to comment 0
python3 /builtin-skills/docx/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
python3 /builtin-skills/docx/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:
<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:
<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:
<!-- 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>:
<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:
<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):
<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>.
<!-- 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
- Add image file to
word/media/ - Add relationship to
word/_rels/document.xml.rels:
<Relationship Id="rId5" Type=".../image" Target="media/image1.png"/>
- Add content type to
[Content_Types].xml:
<Default Extension="png" ContentType="image/png"/>
- Reference in document.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
/builtin-skills/docx/scripts/office/soffice.py) - Poppler:
pdftoppmfor images