#!/bin/bash # Generate a Word tracked-revision showcase document covering every # revision element the docx handler supports on this branch. # # Marker creation (covered): # Run scope: # * w:ins set + revision.type=ins # * w:del set + revision.type=del # * w:rPrChange (implicit) set + font.* / bold / ... + revision.author # * w:rPrChange (explicit) set + revision.type=format + # * w:moveFrom / w:moveTo set + revision.type=moveFrom|moveTo + shared revision.id # Paragraph scope: # * w:ins (paragraph) add paragraph + revision.author # * w:del (paragraph) remove paragraph + revision.author # * w:pPrChange set paragraph prop (align/indent/...) + revision.author # Table scope: # * w:tblPrChange set tbl + style/... + revision.author # * w:trPrChange set tr + height/header/... + revision.author # * w:tcPrChange set tc + shd/borders/... + revision.author # (implicitly cascades w:tblPrExChange + w:tblGridChange when grid mutates) # * w:trPr/w:ins (rowInsertion) add row + revision.author # * w:tcPr/w:cellIns add cell + revision.author # * w:tcPr/w:cellDel remove cell + revision.author # Section scope: # * w:sectPrChange set /body/sectPr[N] + revision.author # Bonus: # * Default author (revision.author="" -> "OfficeCLI") # * Auto-allocated revision.id (omit; comes from shared paraId pool) # * Explicit revision.id (required for move pair; allowed everywhere) # # Action verb demo (on a temp copy at the bottom): # * /revision[@author=NAME] accept/reject by author # * /revision[@type=ins|del|...] accept/reject by type # * /revision[@id=N] accept/reject by stable id # * /revision[@id=N][@type=moveTo] single-end of a move pair # * native path (/body/p[N]/ins[M] ...) accept/reject in DOM terms # * /revision accept-all / reject-all (terminal sweep) # NOTE: intentionally NO `set -e`. Like the SDK twin's doc.batch, this script # tolerates forward-compat 'UNSUPPORTED props' warnings (officecli exit 2) and # keeps building so the full document is produced. DIR="$(dirname "$0")" DOCX="$DIR/revisions.docx" echo "==========================================" echo "Generating tracked-revision showcase: $DOCX" echo "==========================================" rm -f "$DOCX" officecli create "$DOCX" officecli open "$DOCX" # -------------------------------------------------------------------------- # Paragraph index map (1-based; counts EVERY body paragraph). Comments inline # below show what each set targets so the indices stay correct as the doc # grows. Tracked-delete keeps the paragraph element in place (wrapped in # w:del), so subsequent indices do NOT shift after step 2b. # -------------------------------------------------------------------------- # p[1] title, p[2] spacer officecli add "$DOCX" /body --type paragraph --prop text="Revision API — Full Coverage" --prop style=Heading1 --prop align=center officecli add "$DOCX" /body --type paragraph --prop text="" # ========================================================================== # Section 1 — Run-level edits. # p[3] H2, p[4]=ins target, p[5]=del target, # p[6]=implicit-format target, p[7]=explicit-format target # ========================================================================== echo " -> Section 1: run-level edits (ins / del / rPrChange implicit + explicit)" officecli add "$DOCX" /body --type paragraph --prop text="1. Run-level edits" --prop style=Heading2 officecli add "$DOCX" /body --type paragraph --prop text="This run will be marked as an INSERTION." officecli add "$DOCX" /body --type paragraph --prop text="This run will be marked as a DELETION." officecli add "$DOCX" /body --type paragraph --prop text="This run keeps the text and gets an IMPLICIT format change (font.color)." officecli add "$DOCX" /body --type paragraph --prop text="This run gets an EXPLICIT revision.type=format with italic toggle." # 1a. w:ins around the run. officecli set "$DOCX" '/body/p[4]/r[1]' \ --prop revision.type=ins \ --prop revision.author=Alice \ --prop revision.date=2026-05-25T10:00:00Z # 1b. w:del around the run (text becomes w:delText). officecli set "$DOCX" '/body/p[5]/r[1]' \ --prop revision.type=del \ --prop revision.author=Bob \ --prop revision.date=2026-05-25T10:05:00Z # 1c. Implicit format change — any font.* prop + revision.author captures the # previous rPr in w:rPrChange. Most natural form. officecli set "$DOCX" '/body/p[6]/r[1]' \ --prop font.color=C00000 \ --prop bold=true \ --prop revision.author=Carol \ --prop revision.date=2026-05-25T10:10:00Z # 1d. Explicit revision.type=format. Still needs a real property change # alongside (empty rPrChange records nothing, so the handler errors out). officecli set "$DOCX" '/body/p[7]/r[1]' \ --prop revision.type=format \ --prop italic=true \ --prop revision.author=Carol \ --prop revision.date=2026-05-25T10:11:00Z # ========================================================================== # Section 2 — Paragraph-level edits. # p[8] H2 # p[9] whole-paragraph tracked insertion (add + revision.author) # p[10] plain paragraph that becomes a tracked deletion (remove + ...) # p[11] paragraph that gets a pPrChange (set align + revision.author) # ========================================================================== echo " -> Section 2: paragraph-level edits (ins / del / pPrChange)" officecli add "$DOCX" /body --type paragraph --prop text="2. Paragraph-level edits" --prop style=Heading2 # 2a. w:ins around the entire paragraph (plus paragraphMarkInsertion on the ¶). officecli add "$DOCX" /body --type paragraph \ --prop text="This whole paragraph was inserted by Alice as a tracked change." \ --prop revision.author=Alice \ --prop revision.date=2026-05-25T10:15:00Z # 2b. w:del around the entire paragraph (plus paragraphMarkDeletion). # remove + revision.author KEEPS the element (wraps it); it does not drop it. officecli add "$DOCX" /body --type paragraph --prop text="This whole paragraph will be tracked-deleted by Bob." officecli remove "$DOCX" '/body/p[10]' \ --prop revision.author=Bob \ --prop revision.date=2026-05-25T10:20:00Z # 2c. pPrChange — set a paragraph-level property (alignment here) + revision.author. # Surfaces in query as revision.type=paragraph. officecli add "$DOCX" /body --type paragraph --prop text="This paragraph had alignment changed (pPrChange) by Carol." officecli set "$DOCX" '/body/p[11]' \ --prop align=center \ --prop revision.author=Carol \ --prop revision.date=2026-05-25T10:21:00Z # ========================================================================== # Section 3 — Paired move (shared revision.id binds the two halves). # p[12] H2, p[13]=moveFrom source, p[14]=moveTo destination # ========================================================================== echo " -> Section 3: paired move (moveFrom + moveTo, shared id)" officecli add "$DOCX" /body --type paragraph --prop text="3. Moved content" --prop style=Heading2 officecli add "$DOCX" /body --type paragraph --prop text="Source: this sentence is being relocated." officecli add "$DOCX" /body --type paragraph --prop text="Destination: it will land here in its new home." # revision.id MUST be supplied (and equal) for the two halves to pair. officecli set "$DOCX" '/body/p[13]/r[1]' \ --prop revision.type=moveFrom \ --prop revision.author=Alice \ --prop revision.date=2026-05-25T10:25:00Z \ --prop revision.id=500 officecli set "$DOCX" '/body/p[14]/r[1]' \ --prop revision.type=moveTo \ --prop revision.author=Alice \ --prop revision.date=2026-05-25T10:25:00Z \ --prop revision.id=500 # ========================================================================== # Section 4 — Table-scope revisions (all five table elements). # p[15] H2, tbl[1] = 3 rows x 3 cols seed. # Order of operations is chosen so per-row/per-cell indices stay correct. # ========================================================================== echo " -> Section 4: table scope (tblPrChange + trPrChange + tcPrChange + row/cell ins/del)" officecli add "$DOCX" /body --type paragraph --prop text="4. Table-scope revisions" --prop style=Heading2 officecli add "$DOCX" /body --type table --prop rows=3 --prop cols=3 # Seed content officecli set "$DOCX" '/body/tbl[1]/tr[1]/tc[1]' --prop text="Header A" --prop bold=true officecli set "$DOCX" '/body/tbl[1]/tr[1]/tc[2]' --prop text="Header B" --prop bold=true officecli set "$DOCX" '/body/tbl[1]/tr[1]/tc[3]' --prop text="Header C" --prop bold=true officecli set "$DOCX" '/body/tbl[1]/tr[2]/tc[1]' --prop text="row2 a" officecli set "$DOCX" '/body/tbl[1]/tr[2]/tc[2]' --prop text="row2 b (shading change)" officecli set "$DOCX" '/body/tbl[1]/tr[2]/tc[3]' --prop text="row2 c" officecli set "$DOCX" '/body/tbl[1]/tr[3]/tc[1]' --prop text="row3 a (cell delete)" officecli set "$DOCX" '/body/tbl[1]/tr[3]/tc[2]' --prop text="row3 b" officecli set "$DOCX" '/body/tbl[1]/tr[3]/tc[3]' --prop text="row3 c" # 4a. tblPrChange — table-level property change. officecli set "$DOCX" '/body/tbl[1]' \ --prop style=TableGrid \ --prop revision.author=Dan \ --prop revision.date=2026-05-25T10:30:00Z # 4b. trPrChange — row-level property change (row height). officecli set "$DOCX" '/body/tbl[1]/tr[1]' \ --prop height=600 \ --prop revision.author=Dan \ --prop revision.date=2026-05-25T10:31:00Z # 4c. tcPrChange — cell-level property change (shading). # Cascades tblPrExChange / tblGridChange automatically when needed. officecli set "$DOCX" '/body/tbl[1]/tr[2]/tc[2]' \ --prop shd=FFE699 \ --prop revision.author=Dan \ --prop revision.date=2026-05-25T10:32:00Z # 4d. Cell insertion — add a 4th cell to row 2. officecli add "$DOCX" '/body/tbl[1]/tr[2]' --type cell \ --prop text="row2 d (inserted)" \ --prop revision.author=Eve \ --prop revision.date=2026-05-25T10:33:00Z # 4e. Cell deletion — drop cell 1 of row 3 (tracked, not destructive). officecli remove "$DOCX" '/body/tbl[1]/tr[3]/tc[1]' \ --prop revision.author=Eve \ --prop revision.date=2026-05-25T10:34:00Z # 4f. Row insertion — append a row at the table tail; whole row marked inserted. officecli add "$DOCX" '/body/tbl[1]' --type row \ --prop revision.author=Eve \ --prop revision.date=2026-05-25T10:35:00Z # ========================================================================== # Section 5 — Section properties (sectPrChange). # The body's section properties live at /body/sectPr[1] (NOT /body/sect[1]). # ========================================================================== echo " -> Section 5: section properties (sectPrChange)" officecli add "$DOCX" /body --type paragraph --prop text="5. Section properties" --prop style=Heading2 officecli add "$DOCX" /body --type paragraph --prop text="The body sectPr below got a tracked pageWidth change." officecli set "$DOCX" '/body/sectPr[1]' \ --prop pageWidth=11906 \ --prop revision.author=Frank \ --prop revision.date=2026-05-25T10:40:00Z # ========================================================================== # Section 6 — Defaults & explicit-id (bonus). # ========================================================================== echo " -> Section 6: default author + auto-allocated id" officecli add "$DOCX" /body --type paragraph --prop text="6. Defaults" --prop style=Heading2 # 6a. Empty revision.author -> falls back to "OfficeCLI". # `add + revision.author=""` silently produces an untracked paragraph # (empty author = "no revision"); the default-author fallback fires on # the `set` path. So we add the paragraph plain, then `set` it with an # empty author to demonstrate the fallback. officecli add "$DOCX" /body --type paragraph \ --prop text="This run was wrapped via set with revision.author=\"\" (defaults to OfficeCLI)." officecli set "$DOCX" '/body/p[19]/r[1]' \ --prop revision.type=ins \ --prop revision.author="" \ --prop revision.date=2026-05-25T10:44:00Z # 6b. Explicit revision.id outside of a move pair — accepted, you control the # w:id attribute. Useful when post-processing needs a deterministic id. officecli add "$DOCX" /body \ --type paragraph \ --prop text="This paragraph carries an explicit revision.id=9001." \ --prop revision.author=Grace \ --prop revision.date=2026-05-25T10:45:00Z \ --prop revision.id=9001 # ========================================================================== # Section 7 — Find + Replace combined with revision tracking. # Mirrors Word's Find&Replace dialog with Track Changes ON: every match is # wrapped in the marker shape inferred from the props you pass alongside. # The handler auto-allocates a fresh revision.id per marker (one w:del per # matched run + one w:ins for the replacement, or one w:rPrChange per match, # etc.), so `--prop revision.id=…` is rejected on find — would collide. # ========================================================================== echo " -> Section 7: find + revision (Word-style Find&Replace with Track Changes)" officecli add "$DOCX" /body --type paragraph --prop text="7. Find + Replace + Revision" --prop style=Heading2 # Helper: add a paragraph and echo the path the handler assigned (e.g. # /body/p[@paraId=00100012]) so subsequent `set --find …` calls don't # need to hand-count positional indices. The handler-assigned paraId path # is stable across content shifts in the body, unlike /body/p[N] which # drifts every time the section count above changes. add_para_capture() { officecli add "$DOCX" /body --type paragraph --prop text="$1" 2>&1 \ | grep -oE '/body/p\[@paraId=[A-F0-9]+\]' | tail -1 } # 7a. find + replace + revision via REGEX — track only the FIRST occurrence # of "fox", leave subsequent ones alone. The bare `--find fox` would match # every occurrence; controlling which match to track is a job for the # regex pattern. # # Pattern: (? Section 8: find variants (replace='' delete-only + paragraph prop pPrChange)" officecli add "$DOCX" /body --type paragraph --prop text="8. Find variants" --prop style=Heading2 # 8a. find + replace="" + revision — tracked DELETION of every match, no insertion. # Useful for "scrub this token from the doc but keep an audit trail". P8A=$(add_para_capture "8a. Remove the OBSOLETE token here. (delete-only via find — no insertion)") officecli set "$DOCX" "$P8A" \ --find OBSOLETE \ --replace "" \ --prop revision.author=Mira \ --prop revision.date=2026-05-25T10:54:00Z # 8b. find + paragraph prop + revision — one w:pPrChange per matched paragraph. # Same code path as `set /body/p[N] --prop align=… --prop revision.author=…`, # but filtered to ONLY the paragraphs whose text actually matched the find. P8B=$(add_para_capture "8b. This paragraph contains MARK so its alignment gets tracked-centered.") officecli set "$DOCX" "$P8B" \ --find MARK \ --prop align=center \ --prop revision.author=Nora \ --prop revision.date=2026-05-25T10:55:00Z officecli close "$DOCX" # ========================================================================== # Inspection — list every revision marker in the shipped file. # ========================================================================== echo "" echo "==========================================" echo "All revisions in $DOCX:" echo "==========================================" officecli query "$DOCX" revision # ========================================================================== # Action verbs — runs on a TEMP COPY so the shipped artifact keeps every # marker intact for inspection in Word. # ========================================================================== DEMO="$(mktemp -t revisions-demo.XXXXXX).docx" cp "$DOCX" "$DEMO" echo "" echo "==========================================" echo "Accept/reject demo on temp copy:" echo " $DEMO" echo "==========================================" # A. Single-end addressing of a move pair: `/revision[@id=N][@type=…]` # addresses ONE half of a shared-id pair. Section 3 created moveFrom + # moveTo at id=500; reject only the moveTo half here (the moveFrom # survives — useful when reviewing decides "keep the deletion at source, # discard the insertion at destination"). Done BEFORE step B because B # accepts everything Alice authored, including this move pair. echo " A) single-end move reject: /revision[@id=500][@type=moveTo]" officecli set "$DEMO" '/revision[@id=500][@type=moveTo]' --prop revision.action=reject 2>&1 | tail -1 SURVIVING_MOVE=$(officecli query "$DEMO" revision --json 2>/dev/null | python3 -c " import sys, json d = json.load(sys.stdin) for r in d['data']['results']: if r.get('format',{}).get('revision.id') == '500': print(r['format'].get('revision.type')) ") echo " surviving half of id=500: ${SURVIVING_MOVE:-none} (expected: moveFrom)" # B. Accept everything Alice authored. echo " B) accept by author: /revision[@author=Alice]" officecli set "$DEMO" '/revision[@author=Alice]' --prop revision.action=accept # C. Reject every w:del-typed revision still left. echo " C) reject by type: /revision[@type=del]" officecli set "$DEMO" '/revision[@type=del]' --prop revision.action=reject # D. Accept Carol's explicit-format change by its stable id. CAROL_FMT_ID=$(officecli query "$DEMO" revision --json 2>/dev/null | python3 -c " import sys, json d = json.load(sys.stdin) for r in d['data']['results']: f = r.get('format', {}) if f.get('revision.author') == 'Carol' and f.get('revision.type') == 'formatChange': print(f['revision.id']); break ") if [ -n "$CAROL_FMT_ID" ]; then echo " D) accept by stable id: /revision[@id=$CAROL_FMT_ID]" officecli set "$DEMO" "/revision[@id=$CAROL_FMT_ID]" --prop revision.action=accept fi # E. Accept a marker via its native DOM path. Pick the first surviving marker # after steps A-D and feed its nativePath back to `set --prop revision.action=accept`. NATIVE_PATH=$(officecli query "$DEMO" revision --json 2>/dev/null | python3 -c " import sys, json d = json.load(sys.stdin) for r in d['data']['results']: np = r.get('format', {}).get('revision.nativePath','') if np: print(np); break ") if [ -n "$NATIVE_PATH" ]; then echo " E) accept by native path: $NATIVE_PATH" officecli set "$DEMO" "$NATIVE_PATH" --prop revision.action=accept fi # F. Sweep — reject everything still pending. echo " F) terminal sweep: /revision (reject-all)" officecli set "$DEMO" /revision --prop revision.action=reject REMAINING=$(officecli query "$DEMO" revision 2>&1 | grep -c "^/revision" || true) echo " remaining markers after sweep: $REMAINING (expected: 0)" rm -f "$DEMO" # ========================================================================== # Read-side capabilities — agent-friendly JSON envelope, plain-text render, # dump→batch round-trip (revision keys survive serialization, so a doc with # tracked changes can be regenerated end-to-end via `batch --input`). # ========================================================================== echo "" echo "==========================================" echo "Read-side capabilities on $DOCX:" echo "==========================================" echo " i) query revision --json (first 3 markers, agent-consumable):" officecli query "$DOCX" revision --json 2>/dev/null | python3 -c " import sys, json d = json.load(sys.stdin) print(f' matches={d[\"data\"][\"matches\"]}') for r in d['data']['results'][:3]: f = r['format'] print(f' path={r[\"path\"]} type={f.get(\"revision.type\")} author={f.get(\"revision.author\")} text={repr(r.get(\"text\",\"\"))[:40]}') " echo "" echo " ii) view text — runs/paragraphs render with their current content" echo " (inserted text shows, deleted text is suppressed; cf. Word's" echo " 'All Markup' vs 'No Markup' view):" officecli view "$DOCX" text 2>&1 | head -10 | sed 's/^/ /' echo "" echo " iii) dump --format batch round-trip — revision creation keys survive" echo " serialization so a tracked-change doc regenerates end-to-end via" echo " 'batch --input '. Sample of revision keys in dump:" officecli dump "$DOCX" / --format batch 2>/dev/null \ | python3 -c " import sys, json batch = json.load(sys.stdin) rev_steps = [s for s in batch if any(k.startswith('revision.') for k in (s.get('props') or {}))] print(f' total batch steps: {len(batch)}, steps carrying revision.* props: {len(rev_steps)}') for s in rev_steps[:3]: keys = [k for k in s['props'] if k.startswith('revision.')] print(f' {s[\"command\"]:>6} {s.get(\"path\",s.get(\"parent\",\"\")):40} keys={keys}') " echo "" echo "Done: $DOCX" ls -lh "$DOCX"