#!/usr/bin/env python3 """ Numbering & List Showcase — generates numbering.docx exercising every supported `num` / `abstractNum` feature: • abstractNum top-level props: name, styleLink, numStyleLink, multiLevelType • Per-level dotted props on all levels: format, text, start, indent, hanging, justification, suff, font, size, color, bold, italic • num mode A (auto-create matching abstractNum from format/text/indent) • num mode B (reuse existing abstractNum via abstractNumId) • num mode C (startOverride — restart numbering for one instance) • Two num instances sharing one abstractNum → independent counters • continue=true opt-in to Word-style continuation • style-borne numPr (paragraph inherits numbering via pStyle) • Set on /numbering/abstractNum[@id=N]/level[L] after creation SDK twin of numbering.sh (officecli CLI). Both produce an equivalent numbering.docx. This one drives the **officecli Python SDK** (`pip install officecli-sdk`): one resident is started, the `add num` calls go out via `doc.send(...)` so we can read back each freshly-minted numId, and the paragraphs that reference those ids ship in `doc.batch(...)` round-trips. Each item is the same `{"command","parent","type","props"}` dict you'd put in an `officecli batch` list. Usage: pip install officecli-sdk # plus the `officecli` binary on PATH python3 numbering.py """ import os import re import sys # --- locate the SDK: prefer an installed `officecli-sdk`, else the in-repo copy try: import officecli # pip install officecli-sdk except ImportError: sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "sdk", "python")) import officecli FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "numbering.docx") def para(text, **props): """One `add paragraph` item in batch-shape.""" return {"command": "add", "parent": "/body", "type": "paragraph", "props": {"text": text, **props}} def _num_id(resp): """Pull the numId out of an `add num` envelope ("Added num at /numbering/num[@id=N]") — the SDK twin of the .sh `sed @id=N` capture.""" text = resp.get("data") or resp.get("message") or "" if isinstance(resp, dict) else str(resp) m = re.search(r"@id=(\d+)\]", text) if not m: raise RuntimeError(f"could not parse numId from add response: {resp!r}") return m.group(1) print(f"Building {FILE} ...") with officecli.create(FILE, "--force") as doc: def add_num(**props): """`add /numbering --type num` via send(), returning the new numId.""" return _num_id(doc.send({"command": "add", "parent": "/numbering", "type": "num", "props": props})) # ============================================================ # Title # ============================================================ doc.batch([ para("Numbering & List Showcase", align="center", bold="true", size="20"), para("Generated by officecli — covers abstractNum, num, and style-borne numPr", align="center", italic="true", color="666666"), para(""), # ===== Section 1 heading + abstractNum #100 (custom 3-level template) ===== para("1. Three-level numbered list (custom marker styling)", bold="true", size="14", spaceBefore="240", spaceAfter="120"), ]) # abstractNum #100 — fully customized 3-level numbered template doc.send({"command": "add", "parent": "/numbering", "type": "abstractNum", "props": { "id": "100", "name": "ShowcaseMultilevel", "type": "hybridMultilevel", "level0.format": "decimal", "level0.text": "%1.", "level0.indent": "720", "level0.hanging": "360", "level0.justification": "left", "level0.suff": "tab", "level0.color": "C00000", "level0.bold": "true", "level0.size": "14", "level1.format": "lowerLetter", "level1.text": "%2)", "level1.indent": "1440", "level1.hanging": "360", "level1.color": "2E74B5", "level1.italic": "true", "level2.format": "lowerRoman", "level2.text": "%3.", "level2.indent": "2160", "level2.hanging": "360", "level2.color": "666666", }}) # A num instance pointing at #100 num_a = add_num(abstractNumId="100") print(f" Created num #{num_a} → abstractNum #100") doc.batch([ para("Project Phoenix kickoff agenda", numId=num_a, ilvl="0"), para("Stakeholder alignment", numId=num_a, ilvl="1"), para("identify decision makers", numId=num_a, ilvl="2"), para("schedule discovery interviews", numId=num_a, ilvl="2"), para("Architecture review", numId=num_a, ilvl="1"), para("Sprint planning", numId=num_a, ilvl="0"), para("Resource allocation", numId=num_a, ilvl="0"), # ===== Section 2 heading ===== para(""), para("2. Independent counters (default) and continue=true opt-in", bold="true", size="14", spaceBefore="240", spaceAfter="120"), ]) # Two new nums on the same abstractNum: by default each gets its own # auto-injected startOverride.0 → independent counters. The third opts into # Word's literal continuation via continue=true. num_b = add_num(abstractNumId="100") print(f" Created num #{num_b} → independent counter (auto-injected startOverride.0=1)") num_cont = add_num(abstractNumId="100", **{"continue": "true"}) print(f" Created num #{num_cont} → Word-style continuation (continue=true)") doc.batch([ para("List B starts fresh at 1 (default behavior)", numId=num_b, ilvl="0"), para("List B item two (counts 2)", numId=num_b, ilvl="0"), para("List C continues from List A's count (continue=true)", numId=num_cont, ilvl="0"), para("List C item two", numId=num_cont, ilvl="0"), # ===== Section 3 heading ===== para(""), para("3. Restart numbering with startOverride", bold="true", size="14", spaceBefore="240", spaceAfter="120"), ]) # Mode C — num with startOverride (restart at 100) num_c = add_num(abstractNumId="100", start="100") print(f" Created num #{num_c} → abstractNum #100 with startOverride.0=100") doc.batch([ para("Numbered starting from 100", numId=num_c, ilvl="0"), para("Continues from 101", numId=num_c, ilvl="0"), # ===== Section 4 heading ===== para(""), para("4. Custom-styled bullet list (★ / ▶ / ●)", bold="true", size="14", spaceBefore="240", spaceAfter="120"), ]) # abstractNum #200 — bullet list with custom glyphs and font color doc.send({"command": "add", "parent": "/numbering", "type": "abstractNum", "props": { "id": "200", "name": "StarBullet", "type": "hybridMultilevel", "level0.format": "bullet", "level0.text": "★", "level0.color": "E8B003", "level0.size": "12", "level1.format": "bullet", "level1.text": "▶", "level1.font": "Arial", "level1.color": "2E74B5", "level1.indent": "1440", "level2.format": "bullet", "level2.text": "●", "level2.color": "70AD47", "level2.indent": "2160", }}) num_bullet = add_num(abstractNumId="200") doc.batch([ para("Top-level milestone", numId=num_bullet, ilvl="0"), para("Sub-milestone with deliverable", numId=num_bullet, ilvl="1"), para("Nitty-gritty detail", numId=num_bullet, ilvl="2"), para("Another top-level milestone", numId=num_bullet, ilvl="0"), # ===== Section 5 heading ===== para(""), para("5. Mode A — num auto-creates abstractNum", bold="true", size="14", spaceBefore="240", spaceAfter="120"), ]) # Mode A — num auto-creates a matching abstractNum on the fly num_auto = add_num(**{ "level0.format": "upperRoman", "level0.text": "%1.", "level0.indent": "720", "level0.size": "12", "level0.color": "7030A0", "level0.bold": "true", }) print(f" Mode A created num #{num_auto} + matching abstractNum") doc.batch([ para("The first part of the proposal", numId=num_auto, ilvl="0"), para("The second part", numId=num_auto, ilvl="0"), para("The third part", numId=num_auto, ilvl="0"), # ===== Section 6 heading ===== para(""), para("6. Style-borne numbering (paragraph inherits via pStyle)", bold="true", size="14", spaceBefore="240", spaceAfter="120"), ]) # Build a dedicated abstractNum + num for the style doc.send({"command": "add", "parent": "/numbering", "type": "abstractNum", "props": { "id": "300", "name": "StyleBorne", "level0.format": "decimalZero", "level0.text": "%1.", "level0.indent": "720", "level0.color": "C00000", }}) num_style = add_num(abstractNumId="300") # Style holds the numPr; paragraphs reference the style without their own numId doc.send({"command": "add", "parent": "/styles", "type": "style", "props": { "id": "ShowcaseListItem", "name": "Showcase List Item", "type": "paragraph", "basedOn": "Normal", "numId": num_style, "ilvl": "0", }}) doc.batch([ para("Inherits numbering through style", style="ShowcaseListItem"), para("Second item, also via style", style="ShowcaseListItem"), para("Third item — note: paragraphs themselves have no numPr", style="ShowcaseListItem"), # ===== Section 7 heading ===== para(""), para("7. Modify abstractNum after creation", bold="true", size="14", spaceBefore="240", spaceAfter="120"), ]) # Set after create — override abstractNum #100 level 3 with green + larger size doc.send({"command": "set", "path": "/numbering/abstractNum[@id=100]/level[3]", "props": {"format": "decimal", "text": "Step %4 ⇒", "color": "70AD47", "bold": "true", "size": "12"}}) num_deep = add_num(abstractNumId="100") doc.batch([ para("Outer step", numId=num_deep, ilvl="0"), para("Mid step", numId=num_deep, ilvl="1"), para("Inner step", numId=num_deep, ilvl="2"), para("Deepest step (modified after creation)", numId=num_deep, ilvl="3"), # ===== Section 8 heading ===== para(""), para("8. styleLink, numStyleLink, level.start, direction, isLgl, lvlRestart", bold="true", size="14", spaceBefore="240", spaceAfter="120"), ]) # abstractNum #400 — styleLink + numStyleLink + per-level start doc.send({"command": "add", "parent": "/numbering", "type": "abstractNum", "props": { "id": "400", "name": "CoverageAbs", "type": "multilevel", "styleLink": "CoverageStyle", "numStyleLink": "OutlineRef", "level0.format": "decimal", "level0.text": "%1.", "level0.start": "1", "level1.format": "lowerLetter", "level1.text": "%2)", "level1.start": "3", "level2.format": "lowerRoman", "level2.text": "%3.", "level2.start": "5", }}) # Set direction=rtl, isLgl=true, lvlRestart=0 on individual levels via Set doc.send({"command": "set", "path": "/numbering/abstractNum[@id=400]/level[1]", "props": {"direction": "rtl"}}) doc.send({"command": "set", "path": "/numbering/abstractNum[@id=400]/level[2]", "props": {"isLgl": "true", "lvlRestart": "0"}}) num_cov = add_num(abstractNumId="400") print(f" Created num #{num_cov} → abstractNum #400 (coverage)") doc.batch([ para("Item starting at 1 (level0.start=1)", numId=num_cov, ilvl="0"), para("Sub-item starting at c (level1.start=3, direction=rtl)", numId=num_cov, ilvl="1"), para("Deep item starting at v (level2.start=5, isLgl, lvlRestart=0)", numId=num_cov, ilvl="2"), # ===== Closer ===== para(""), para("End of showcase. Open in Word/Google Docs to see all numbering rendered.", italic="true", color="666666", align="center"), ]) print(f"Generated: {FILE}")