Files
wehub-resource-sync 8cb1f9f479
Publish SDK (PyPI) / publish (push) Has been cancelled
Publish SDK (npm) / publish (@aionui/officecli-sdk) (push) Has been cancelled
SDK smoke / smoke (windows-latest) (push) Has been cancelled
Publish SDK (npm) / publish (@officecli/officecli-sdk) (push) Has been cancelled
Publish SDK (npm) / publish (@officecli/sdk) (push) Has been cancelled
Publish SDK (npm) / publish (officecli-sdk) (push) Has been cancelled
SDK smoke / smoke (macos-latest) (push) Has been cancelled
SDK smoke / smoke (ubuntu-latest) (push) Has been cancelled
Skill parity / diff (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:09:29 +08:00

287 lines
16 KiB
Python

#!/usr/bin/env python3
r"""
Revision Showcase — generates revisions.docx exercising every tracked-revision
element the docx handler supports: run-scope (ins / del / rPrChange implicit +
explicit / moveFrom + moveTo), paragraph-scope (ins / del / pPrChange),
table-scope (tblPrChange / trPrChange / tcPrChange / row + cell ins/del),
section-scope (sectPrChange), defaults & explicit-id, and Word-style
Find&Replace-with-Track-Changes (find + replace / format / delete-only /
paragraph-prop).
SDK twin of revisions.sh (officecli CLI). Both produce an equivalent
revisions.docx. This one drives the **officecli Python SDK**
(`pip install officecli-sdk`): one resident is started and every command is
shipped over the named pipe. Each item is the same `{"command","parent"/"path",
"type","props"}` dict you'd put in an `officecli batch` list.
Ordering note (mirrors the .sh): tracked-delete KEEPS the paragraph/cell element
in place (wrapped in w:del), so positional /body/p[N] indices do NOT shift after
a tracked remove. The build order below is chosen so every positional path stays
correct as the document grows. Sections 1-6 use stable /body/p[N] indices and so
ship as one big batch; sections 7-8 use `set --find` against handler-assigned
`/body/p[@paraId=...]` paths, so each paragraph is added with `send` first and
its returned paraId path is fed to the following `set` (the SDK analogue of the
.sh `add_para_capture` helper).
Usage:
pip install officecli-sdk # plus the `officecli` binary on PATH
python3 revisions.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__)), "revisions.docx")
def para(text, **props):
"""One `add paragraph` item in batch-shape."""
return {"command": "add", "parent": "/body", "type": "paragraph",
"props": {"text": text, **props}}
def add_para_capture(doc, text):
"""Add a paragraph with `send` and return the handler-assigned paraId path
(e.g. /body/p[@paraId=00100012]). The SDK analogue of the .sh helper: the
paraId path is stable across content shifts, unlike /body/p[N]."""
resp = doc.send(para(text))
data = resp.get("data", "") if isinstance(resp, dict) else str(resp)
m = re.search(r"/body/p\[@paraId=[A-F0-9]+\]", data)
if not m:
raise RuntimeError(f"could not extract paraId path from add response: {resp!r}")
return m.group(0)
print("==========================================")
print(f"Generating tracked-revision showcase: {FILE}")
print("==========================================")
with officecli.create(FILE, "--force") as doc:
# ======================================================================
# Sections 1-6 — stable /body/p[N] indices, shipped in one batch.
#
# Paragraph index map (1-based; counts EVERY body paragraph). Tracked-delete
# keeps the paragraph element in place (wrapped in w:del), so subsequent
# indices do NOT shift after step 2b.
# ======================================================================
items = [
# p[1] title, p[2] spacer
para("Revision API — Full Coverage", style="Heading1", align="center"),
para(""),
# ----- Section 1: run-level edits (p[3]..p[7]) -----
para("1. Run-level edits", style="Heading2"), # p[3]
para("This run will be marked as an INSERTION."), # p[4]
para("This run will be marked as a DELETION."), # p[5]
para("This run keeps the text and gets an IMPLICIT format change (font.color)."), # p[6]
para("This run gets an EXPLICIT revision.type=format with italic toggle."), # p[7]
# 1a. w:ins around the run.
{"command": "set", "path": "/body/p[4]/r[1]",
"props": {"revision.type": "ins", "revision.author": "Alice",
"revision.date": "2026-05-25T10:00:00Z"}},
# 1b. w:del around the run (text becomes w:delText).
{"command": "set", "path": "/body/p[5]/r[1]",
"props": {"revision.type": "del", "revision.author": "Bob",
"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.
{"command": "set", "path": "/body/p[6]/r[1]",
"props": {"font.color": "C00000", "bold": "true", "revision.author": "Carol",
"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).
{"command": "set", "path": "/body/p[7]/r[1]",
"props": {"revision.type": "format", "italic": "true", "revision.author": "Carol",
"revision.date": "2026-05-25T10:11:00Z"}},
# ----- Section 2: paragraph-level edits (p[8]..p[11]) -----
para("2. Paragraph-level edits", style="Heading2"), # p[8]
# 2a. w:ins around the entire paragraph (plus paragraphMarkInsertion).
para("This whole paragraph was inserted by Alice as a tracked change.", # p[9]
**{"revision.author": "Alice", "revision.date": "2026-05-25T10:15:00Z"}),
# 2b. w:del around the entire paragraph. remove + revision.author KEEPS
# the element (wraps it); it does not drop it.
para("This whole paragraph will be tracked-deleted by Bob."), # p[10]
{"command": "remove", "path": "/body/p[10]",
"props": {"revision.author": "Bob", "revision.date": "2026-05-25T10:20:00Z"}},
# 2c. pPrChange — set a paragraph-level property (alignment) + author.
para("This paragraph had alignment changed (pPrChange) by Carol."), # p[11]
{"command": "set", "path": "/body/p[11]",
"props": {"align": "center", "revision.author": "Carol",
"revision.date": "2026-05-25T10:21:00Z"}},
# ----- Section 3: paired move (shared revision.id, p[12]..p[14]) -----
para("3. Moved content", style="Heading2"), # p[12]
para("Source: this sentence is being relocated."), # p[13]
para("Destination: it will land here in its new home."), # p[14]
# revision.id MUST be supplied (and equal) for the two halves to pair.
{"command": "set", "path": "/body/p[13]/r[1]",
"props": {"revision.type": "moveFrom", "revision.author": "Alice",
"revision.date": "2026-05-25T10:25:00Z", "revision.id": "500"}},
{"command": "set", "path": "/body/p[14]/r[1]",
"props": {"revision.type": "moveTo", "revision.author": "Alice",
"revision.date": "2026-05-25T10:25:00Z", "revision.id": "500"}},
# ----- Section 4: table scope (p[15] + tbl[1]) -----
para("4. Table-scope revisions", style="Heading2"), # p[15]
{"command": "add", "parent": "/body", "type": "table",
"props": {"rows": "3", "cols": "3"}},
# Seed content
{"command": "set", "path": "/body/tbl[1]/tr[1]/tc[1]", "props": {"text": "Header A", "bold": "true"}},
{"command": "set", "path": "/body/tbl[1]/tr[1]/tc[2]", "props": {"text": "Header B", "bold": "true"}},
{"command": "set", "path": "/body/tbl[1]/tr[1]/tc[3]", "props": {"text": "Header C", "bold": "true"}},
{"command": "set", "path": "/body/tbl[1]/tr[2]/tc[1]", "props": {"text": "row2 a"}},
{"command": "set", "path": "/body/tbl[1]/tr[2]/tc[2]", "props": {"text": "row2 b (shading change)"}},
{"command": "set", "path": "/body/tbl[1]/tr[2]/tc[3]", "props": {"text": "row2 c"}},
{"command": "set", "path": "/body/tbl[1]/tr[3]/tc[1]", "props": {"text": "row3 a (cell delete)"}},
{"command": "set", "path": "/body/tbl[1]/tr[3]/tc[2]", "props": {"text": "row3 b"}},
{"command": "set", "path": "/body/tbl[1]/tr[3]/tc[3]", "props": {"text": "row3 c"}},
# 4a. tblPrChange — table-level property change.
{"command": "set", "path": "/body/tbl[1]",
"props": {"style": "TableGrid", "revision.author": "Dan",
"revision.date": "2026-05-25T10:30:00Z"}},
# 4b. trPrChange — row-level property change (row height).
{"command": "set", "path": "/body/tbl[1]/tr[1]",
"props": {"height": "600", "revision.author": "Dan",
"revision.date": "2026-05-25T10:31:00Z"}},
# 4c. tcPrChange — cell-level property change (shading). Cascades
# tblPrExChange / tblGridChange automatically when needed.
{"command": "set", "path": "/body/tbl[1]/tr[2]/tc[2]",
"props": {"shd": "FFE699", "revision.author": "Dan",
"revision.date": "2026-05-25T10:32:00Z"}},
# 4d. Cell insertion — add a 4th cell to row 2.
{"command": "add", "parent": "/body/tbl[1]/tr[2]", "type": "cell",
"props": {"text": "row2 d (inserted)", "revision.author": "Eve",
"revision.date": "2026-05-25T10:33:00Z"}},
# 4e. Cell deletion — drop cell 1 of row 3 (tracked, not destructive).
{"command": "remove", "path": "/body/tbl[1]/tr[3]/tc[1]",
"props": {"revision.author": "Eve", "revision.date": "2026-05-25T10:34:00Z"}},
# 4f. Row insertion — append a row at the table tail; whole row inserted.
{"command": "add", "parent": "/body/tbl[1]", "type": "row",
"props": {"revision.author": "Eve", "revision.date": "2026-05-25T10:35:00Z"}},
# ----- Section 5: section properties (sectPrChange) -----
para("5. Section properties", style="Heading2"), # p[16]
para("The body sectPr below got a tracked pageWidth change."), # p[17]
# The body's section properties live at /body/sectPr[1] (NOT /body/sect[1]).
{"command": "set", "path": "/body/sectPr[1]",
"props": {"pageWidth": "11906", "revision.author": "Frank",
"revision.date": "2026-05-25T10:40:00Z"}},
# ----- Section 6: defaults & explicit-id -----
para("6. Defaults", style="Heading2"), # p[18]
# 6a. Empty revision.author -> falls back to "OfficeCLI". `add +
# revision.author=""` silently produces an untracked paragraph, so add
# the paragraph plain then `set` it with an empty author to fire the
# default-author fallback on the set path.
para('This run was wrapped via set with revision.author="" (defaults to OfficeCLI).'), # p[19]
{"command": "set", "path": "/body/p[19]/r[1]",
"props": {"revision.type": "ins", "revision.author": "",
"revision.date": "2026-05-25T10:44:00Z"}},
# 6b. Explicit revision.id outside of a move pair — accepted, you control
# the w:id attribute.
para("This paragraph carries an explicit revision.id=9001.", # p[20]
**{"revision.author": "Grace", "revision.date": "2026-05-25T10:45:00Z",
"revision.id": "9001"}),
]
doc.batch(items)
print(f" sections 1-6: shipped {len(items)} batch items")
# ======================================================================
# Section 7 — Find + Replace combined with revision tracking.
# Word's Find&Replace with Track Changes ON: every match is wrapped in the
# marker shape inferred from the props passed alongside. The handler
# auto-allocates a fresh revision.id per marker, so `revision.id` is
# rejected on find — it would collide.
# ======================================================================
print(" -> Section 7: find + revision (Find&Replace with Track Changes)")
doc.send(para("7. Find + Replace + Revision", style="Heading2"))
# 7a. find + replace + revision via REGEX — track only the FIRST "fox".
# Pattern (?<!fox.*)fox matches "fox" only when NOT preceded by another
# "fox" on the same line (.NET variable-width negative lookbehind).
p7a = add_para_capture(doc,
"7a. The fox jumped and another fox ran fast. (regex tracks only the 1st 'fox'→'cat')")
doc.send({"command": "set", "path": p7a,
"props": {"find": r"(?<!fox.*)fox", "replace": "cat", "regex": "true",
"revision.author": "Iris", "revision.date": "2026-05-25T10:50:00Z"}})
# 7b. find + format + revision — one w:rPrChange per matched run.
p7b = add_para_capture(doc,
"7b. Color red apples and the red barn. (tracked bold on every 'red')")
doc.send({"command": "set", "path": p7b,
"props": {"find": "red", "bold": "true",
"revision.author": "Jack", "revision.date": "2026-05-25T10:51:00Z"}})
# 7c. find + replace + format + revision — inserted run inherits the original
# rPr from the matched text AND has the new format layered on.
p7c = add_para_capture(doc,
"7c. Replace bar with FOO. (find target → bold-green replacement)")
doc.send({"command": "set", "path": p7c,
"props": {"find": "bar", "replace": "BAZ", "bold": "true", "font.color": "00B050",
"revision.author": "Kelly", "revision.date": "2026-05-25T10:52:00Z"}})
# 7d. find + regex + revision — multiple matches each get their own marker.
p7d = add_para_capture(doc,
r"7d. Prices: $100, $250, $999 (regex \$\d+ → tracked bold)")
doc.send({"command": "set", "path": p7d,
"props": {"find": r"\$\d+", "regex": "true", "bold": "true",
"revision.author": "Liam", "revision.date": "2026-05-25T10:53:00Z"}})
# ======================================================================
# Section 8 — Less-common find + revision variants.
# 8a: find + replace="" — pure tracked deletion (one w:del per match, no
# w:ins). 8b: find + paragraph property — paragraph-scope mutation captured
# as w:pPrChange instead of run-scope w:rPrChange.
# ======================================================================
print(" -> Section 8: find variants (delete-only + paragraph-prop pPrChange)")
doc.send(para("8. Find variants", style="Heading2"))
# 8a. find + replace="" + revision — tracked DELETION of every match.
p8a = add_para_capture(doc,
"8a. Remove the OBSOLETE token here. (delete-only via find — no insertion)")
doc.send({"command": "set", "path": p8a,
"props": {"find": "OBSOLETE", "replace": "",
"revision.author": "Mira", "revision.date": "2026-05-25T10:54:00Z"}})
# 8b. find + paragraph prop + revision — one w:pPrChange per matched paragraph.
p8b = add_para_capture(doc,
"8b. This paragraph contains MARK so its alignment gets tracked-centered.")
doc.send({"command": "set", "path": p8b,
"props": {"find": "MARK", "align": "center",
"revision.author": "Nora", "revision.date": "2026-05-25T10:55:00Z"}})
doc.send({"command": "save"})
# context exit closes the resident, flushing the document to disk.
# ======================================================================
# Inspection — list every revision marker in the shipped file (read-side).
# ======================================================================
print("\n==========================================")
print(f"All revisions in {FILE}:")
print("==========================================")
with officecli.open(FILE) as doc:
env = doc.send({"command": "query", "selector": "revision"})
if isinstance(env, dict):
data = env.get("data", {})
print(f" matches={data.get('matches')}")
for r in data.get("results", [])[:3]:
f = r.get("format", {})
print(f" path={r.get('path')} type={f.get('revision.type')} "
f"author={f.get('revision.author')} text={repr(r.get('text',''))[:40]}")
print(f"\nDone: {FILE}")