# docx-js Generation Patterns — California CRC 2.100-2.119 Working recipes for producing CRC-compliant `.docx` files using the `docx` npm package (https://docx.js.org/) for conversion to PDF and upload to a California eCourt filing portal. ## Why .docx, not direct PDF? California courts accept (and for represented parties in most courts, require) PDF for eFiling. The clean workflow is: 1. Generate the `.docx` from a template using `docx-js` 2. Save it locally so the litigant can review/edit in Word 3. Print or "Save As PDF" to produce the upload file 4. Upload the PDF to the eFiling portal (Odyssey eFileCA or File & ServeXpress) with the correct document code Producing `.docx` gives the litigant a review surface — essential for pro se filers who need to read and verify before filing. ## California-specific formatting challenges California CRC formatting has three elements that require careful handling in docx-js: 1. **2.5-inch top margin on page 1** — requires a section break between page 1 and subsequent pages in `docx-js` 2. **Line numbering — MANDATORY under CRC 2.108(b)** — consecutively numbered lines on the left margin of every page. Applied via the section's `lineNumbers` property (see "Line numbering (CRC 2.108(b))" below for the recipe) 3. **CRC 2.110 footer on every page** including page 1 ## Document setup ```javascript import { Document, Packer, Paragraph, PageOrientation, SectionType, SectionProperties, Footer, AlignmentType, TextRun, PageNumber, TabStopType, TabStopPosition, Table, TableRow, TableCell, WidthType, BorderStyle, LineNumberRestartFormat, convertInchesToTwip } from "docx"; const LETTER_WIDTH = 12240; // 8.5" in twips const LETTER_HEIGHT = 15840; // 11" in twips const MARGIN_1IN = 1440; // 1" in twips const MARGIN_2_5IN = 3600; // 2.5" in twips (page-1 top margin) const CONTENT_WIDTH = 9360; // 6.5" content area (8.5 - 1 - 1) const doc = new Document({ styles: { default: { document: { run: { font: "Times New Roman", size: 24, // 12 pt (docx uses half-points) }, paragraph: { spacing: { line: 480, // double-spaced: 480 twips (240 × 2) lineRule: "auto", }, }, }, }, }, sections: [ // ─── Section 1: Page 1 (2.5" top margin) ──────────────────── { properties: { type: SectionType.CONTINUOUS, page: { size: { width: LETTER_WIDTH, height: LETTER_HEIGHT }, margin: { top: MARGIN_2_5IN, // 2.5" on page 1 for attorney block right: MARGIN_1IN, bottom: MARGIN_1IN, left: MARGIN_1IN, header: 720, // 0.5" footer: 720, }, }, lineNumbers: { // CRC 2.108(b) countBy: 1, restart: LineNumberRestartFormat.NEW_PAGE, }, }, footers: { default: makeFooter("DEFENDANT'S MOTION TO COMPEL", "24STCV12345"), }, children: [ ...attyBlock, // attorney / party info block courtNamePara, // centered court name captionTable, // party block + case info table dividerParagraph, // horizontal rule below caption ...bodyContent, // body paragraphs (auto-line-numbered by section property) // End of page 1 — section break after last line of page 1 // docx-js auto-paginates; the section break fires at the // end of this section's content ], }, // ─── Section 2: Pages 2+ (1" top margin) ──────────────────── { properties: { type: SectionType.NEXT_PAGE, page: { size: { width: LETTER_WIDTH, height: LETTER_HEIGHT }, margin: { top: MARGIN_1IN, // 1" on subsequent pages right: MARGIN_1IN, bottom: MARGIN_1IN, left: MARGIN_1IN, header: 720, footer: 720, }, }, lineNumbers: { // CRC 2.108(b) countBy: 1, restart: LineNumberRestartFormat.NEW_PAGE, }, }, footers: { default: makeFooter("DEFENDANT'S MOTION TO COMPEL", "24STCV12345"), }, children: [ ...remainingBodyContent, ...exhibits, ], }, ], }); ``` **Note on section breaks**: `docx-js` v7+ supports the two-section pattern. Place page-1 content (attorney block, caption, first page of body) in section 1, and the rest of the document in section 2. The `NEXT_PAGE` section type forces the margin change at the exact page boundary. ## Footer with "Page X of Y" (CRC 2.110) ```javascript function makeFooter(docTitle, caseNo) { return new Footer({ children: [ new Paragraph({ tabStops: [{ type: TabStopType.RIGHT, position: 9360 }], children: [ new TextRun({ text: `${docTitle} — Case No. ${caseNo}`, font: "Times New Roman", size: 20, // 10 pt }), // Right-aligned page counter new TextRun({ children: ["\t"] }), new TextRun({ text: "Page ", font: "Times New Roman", size: 20, }), new TextRun({ children: [PageNumber.CURRENT], font: "Times New Roman", size: 20, }), new TextRun({ text: " of ", font: "Times New Roman", size: 20, }), new TextRun({ children: [PageNumber.TOTAL_PAGES], font: "Times New Roman", size: 20, }), ], }), ], }); } ``` ## Line numbering (CRC 2.108(b)) **MANDATORY under CRC 2.108(b).** Apply line numbering through the section's `lineNumbers` property. Number every line (`countBy: 1`) and restart the count on each page (`restart: NEW_PAGE`) — the California pleading-paper convention. Both sections (page 1 and pages 2+) must set the same property so numbering is continuous across the section break. ```javascript import { LineNumberRestartFormat } from "docx"; properties: { // `type` is set per the Document setup above: // section 1 (page 1) -> SectionType.CONTINUOUS // section 2 (pages 2+) -> SectionType.NEXT_PAGE page: { /* size + margins, as in Document setup above */ }, lineNumbers: { countBy: 1, // number every line restart: LineNumberRestartFormat.NEW_PAGE, // restart 1.. each page }, } ``` This emits `` into the section's ``. The numbers render in the left margin and do NOT shift the 1-inch text margin. CRC 2.108(b) lays out the substantive requirements that the recipe satisfies: - **Each line on each page numbered consecutively** (countBy: 1) - **Beginning with the number 1 on each page** (restart: NEW_PAGE) - **Lines spaced 1/4-inch (1.5-spaced) or 1/3-inch (double- spaced) apart**, matching the body text's line spacing The `docx` default body line spacing of 480 twips (double-spaced) keeps the line-numbers and body lines aligned. At 1.5-line spacing, set body `spacing: { line: 360, lineRule: "auto" }` and the numbers will follow. ### `start` attribute caveat **Do NOT set `start` explicitly.** When `start` is omitted, the OOXML default is 1 and the first line renders as "1". But passing `start: 1` emits `w:start="1"`, which LibreOffice (commonly used for headless PDF rendering) renders off by one — the first body line shows "2". Omitting `start` renders correctly as 1..N on every page in both Microsoft Word and LibreOffice. `distance` (the gap between the numbers and the text) is optional; the default is fine. Verified against the `docx` npm package: the `lineNumbers` section property, the `countBy` / `restart` keys, and `LineNumberRestartFormat.NEW_PAGE`. ### Two-section pattern When using the two-section pattern shown in the Document setup (section 1 for page 1's 2.5-inch top margin; section 2 for pages 2+ at 1-inch top margin), apply the same `lineNumbers` property to BOTH sections. Without it on section 2, the numbers stop appearing at the section break. ## Margin rule (double vertical line) Line-numbered pleading paper conventionally carries a *double* vertical rule — two thin parallel lines down the left margin, between the line numbers and the body text — matching the pre-printed Judicial Council pleading paper. Apply it by default to every line-numbered pleading; exhibits are exempt. ### Do NOT use an OOXML `double` page border A `` left border with `w:val="double"` does draw two lines, but the OOXML `double` style **couples the inter-line gap to the line weight** — widening the gap (via `w:sz`) also thickens both lines into heavy bars. You cannot get thin lines *and* a wide gap this way. Use two independent line shapes instead. ### Two header-anchored line shapes Draw the two lines as two separate thin filled rectangles (VML `v:rect`), anchored in the **page header** so they repeat on every page, positioned absolutely relative to the page. **One shape per ``.** Multiple `` elements inside a single `` will **not** all render — each rectangle needs its own `` inside its own `` run: ```xml ``` The second run is identical with `margin-left:67pt` and `id="VRuleInner"`. `docx-js` has no first-class API for header-anchored, absolutely-positioned VML shapes. Add them with the docx skill's **unpack → edit → pack** workflow: generate the file with docx-js (give each section an empty `Header` so a `word/header1.xml` part exists, and keep the `lineNumbers` section property from above), then rewrite the header XML and repack: ```bash python scripts/office/unpack.py out.docx unpacked/ # ... inject the rule into unpacked/word/header*.xml (helper below) ... python scripts/office/pack.py unpacked/ out.docx --original out.docx ``` Inside each `unpacked/word/header*.xml`, declare the VML namespace on the root `` and add the two runs — one `v:rect` per `` per `` — inside the header paragraph. A document can have several header parts (first-page / even / default, or one per section); add the runs to **each** distinct `word/header*.xml`, giving every `v:rect` a unique `id`. This stdlib helper (no `python-docx`) does it and is idempotent: ```python import pathlib, re def _runs(i): outer = ('') % i return outer + (outer.replace('VRuleOuter%d' % i, 'VRuleInner%d' % i) .replace('margin-left:60pt', 'margin-left:67pt')) word = pathlib.Path('unpacked/word') # 1. Double rule: one set of shapes per distinct header part (unique ids). for i, hdr in enumerate(sorted(word.glob('header*.xml'))): xml = hdr.read_text(encoding='utf-8') if 'VRuleOuter' in xml: # idempotent continue if 'xmlns:v=' not in xml: xml = re.sub(r'(', _runs(i) + '', 1) # first header para hdr.write_text(xml, encoding='utf-8') # 2. Keep line numbers off header/footer text. for f in list(word.glob('header*.xml')) + list(word.glob('footer*.xml')): xml = f.read_text(encoding='utf-8') xml = re.sub(r'(]*)?>)(?!)', r'\1', xml) xml = re.sub(r'(]*)?>)(?!', xml) f.write_text(xml, encoding='utf-8') ``` The `` line-numbering element is already emitted by docx-js's `lineNumbers` section property (above), so the patch only touches the header / footer parts. **Geometry** (US Letter, 1-inch margins, line-number `distance` of 360 twips = ¼″): - Body text left edge sits at 72 pt from the page edge; the line-number gutter at ~54 pt. - The two rules at `margin-left` **60 pt** and **67 pt** sit ~7 pt apart, both clear of the numbers (~54 pt) and the text (72 pt). The gap is the difference of the two values; tune them, keeping both between the gutter and the text margin. - `margin-top:21.6pt` + `height:748.8pt` spans ~0.3″–10.7″ on an 11″ page (effectively full height); `width:0.75pt` is the line weight; `fillcolor="black" stroked="f"`. ### Verify ```bash python scripts/office/validate.py out.docx python scripts/office/soffice.py --headless --convert-to pdf out.docx ``` Confirm two thin parallel lines with a clear gap appear between the numbers and the text on **every** page (including page 2 and later), and that the header / footer text is not line-numbered. Then **open the `.docx` in Microsoft Word** — VML renders slightly differently in Word vs LibreOffice, and the output must be correct in Word; this check cannot be skipped. ## Caption table (CRC 2.111) The caption table and other borderless tables in this file share a small helper for the borders config: ```javascript function allNoBorders() { const none = { style: BorderStyle.NONE }; return { top: none, bottom: none, left: none, right: none, insideH: none, insideV: none }; } ``` ```javascript // ) column separating party block from case info // Left cell: ~3.5" wide; right cell: ~3" wide const captionTable = new Table({ width: { size: CONTENT_WIDTH, type: WidthType.DXA }, borders: allNoBorders(), rows: [ new TableRow({ children: [ new TableCell({ width: { size: 5040, type: WidthType.DXA }, borders: allNoBorders(), children: [ cPara("VELOCITY INVESTMENTS, LLC, )"), cPara(" )"), cPara(" Plaintiff, )"), cPara(" )"), cPara(" vs. )"), cPara(" )"), cPara("JANE DOE, )"), cPara(" )"), cPara(" Defendant. )"), ], }), new TableCell({ width: { size: 4320, type: WidthType.DXA }, borders: allNoBorders(), children: [ cPara("Case No. 24STCV12345"), cPara(""), cPara("DEFENDANT'S NOTICE OF MOTION"), cPara("AND MOTION TO COMPEL;"), cPara("MEMORANDUM OF POINTS AND"), cPara("AUTHORITIES"), cPara(""), cPara("Date: March 15, 2025"), cPara("Time: 10:00 a.m."), cPara("Dept.: 36"), cPara("Judge: Hon. John Smith"), ], }), ], }), ], }); function cPara(text) { return new Paragraph({ children: [new TextRun({ text, font: "Times New Roman", size: 24 })], spacing: { before: 0, after: 0, line: 320, lineRule: "auto" }, }); } ``` ## Attorney/party info block (page 1, upper-left) The attorney block appears in the top-left of page 1, within the 2.5-inch top margin space. Because the page margin is 2.5 inches, the content area begins 2.5 inches from the top. The attorney block is the first content in section 1. ```javascript const attyBlock = [ aPara("JANE DOE"), aPara("123 Main Street"), aPara("Los Angeles, CA 90012"), aPara("Tel: (213) 555-0100"), aPara("Email: jane.doe@email.com"), aPara(""), aPara("Defendant, self-represented"), aPara(""), ]; function aPara(text) { return new Paragraph({ children: [new TextRun({ text, font: "Times New Roman", size: 22 })], spacing: { before: 0, after: 0, line: 240, lineRule: "auto" }, }); } ``` ## Packaging and saving ```javascript const buffer = await Packer.toBuffer(doc); fs.writeFileSync("motion-to-compel.docx", buffer); ``` To convert to PDF for filing, use the `.docx` as the working copy and either: - Open in Microsoft Word → File → Save as PDF (best fidelity) - Use `python scripts/office/soffice.py --headless --convert-to pdf motion-to-compel.docx` (for server-side conversion; minor formatting differences from Word) - Use the `docx-pdf` npm package (wraps LibreOffice) ## Validation Run `plugins/ca-court-docs/scripts/format-check.py` on the generated `.docx` to validate: - 2.5-inch top margin on page 1 - CRC 2.110 footer present on all pages - Font: Times New Roman 12 pt - Line spacing: double or 1.5 - Line numbers present on left margin The script will FAIL on a document missing the footer, having the wrong top margin, or missing line numbers. ## California docx layout — quick reference | Element | California requirement | |---------|------------------------| | Top margin, page 1 | 2.5" (CRC 2.110 attorney/party info block) | | Top margin, pages 2+ | 1" | | Section break needed? | Yes — 2-section pattern for the page-1 → pages-2+ top-margin shift | | Line numbers | **Required** under CRC 2.108(b) — both sections carry the `lineNumbers` property | | Footer | CRC 2.110: short document title + case number + Page X of Y | | Caption style | Two-column with right-side `)` separator between party block and case info | | Two-hole punch | Top center, 2¾" apart (paper filings only; CRC 2.119) |