Document::compare_with_options (rdocx/src/comparison.rs:236) takes a ComparisonOptions with a
granularity (Run, Word, Character), ignore_formatting, ignore_whitespace, ignore_fields,
ignore_comments and ignored_stories. Neither Document.compare() in Python nor rdocx compare has a
way to pass them, so both always run with the default, and two defaults bite in practice. #76 brought
compare to the CLI and Python with the default options; this asks to open them.
Whole-run granularity. With ComparisonGranularity::Run, the default ("Preserve the legacy whole-run
comparison behavior"), one word changed in a paragraph held in a single run marks the whole paragraph as
deleted and re-inserted. Single-run paragraphs are the norm in files exported by Google Docs and in any
paragraph typed in one go, so a redline of a light editing pass reads as a rewrite of every touched
paragraph. Both sides below are written by python-docx; rdocx only compares:
import os, re, subprocess, sys, zipfile
from docx import Document
RDOCX = sys.argv[1] if len(sys.argv) > 1 else "rdocx"
TEXT = ("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut "
"labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco.")
def build(path, text, pieces=1):
d = Document()
p = d.add_paragraph()
if pieces == 1:
p.add_run(text)
else:
for part in (text[:40], text[40:120], text[120:]):
p.add_run(part)
d.save(path)
return path
def run(label, pieces):
a = build("gr_a.docx", TEXT, pieces)
b = build("gr_b.docx", TEXT.replace("magna", "MAGNA"), pieces)
if os.path.exists("gr_out.docx"):
os.remove("gr_out.docx")
subprocess.run([RDOCX, "compare", a, b, "--author", "R", "--timestamp", "2026-09-27T12:00:00Z", "-o", "gr_out.docx"],
capture_output=True, text=True, check=True)
x = zipfile.ZipFile("gr_out.docx").read("word/document.xml").decode()
dels = re.findall(r"<w:delText[^>]*>([^<]*)</w:delText>", x)
print(f"{label:<44} deleted chars={sum(map(len, dels)):4d} of {len(TEXT)}")
run("one run, one word changed", 1)
run("three runs, one word changed in the second", 3)
one run, one word changed deleted chars= 183 of 183
three runs, one word changed in the second deleted chars= 80 of 183
Through a ten-line Rust example calling compare_with_options on the same pair, Word gives
deleted ['magna'] inserted ['MAGNA'], which is what a reviewer expects to see.
Comments on one side only. compare() refuses a pair whose comment parts differ:
Error: document comparison requires identical related-story shells
In a review cycle the edited version is exactly the one that carries new comments or replies, so the
default makes the redline of two consecutive passes impossible. Two more refusals of the same family, both
with a comment on each side: one comment added on the edited side gives comparison cannot revise paragraph boundary structures at body/paragraph[1] (the message of #135, but neither side has an empty w:pPr
here: the paragraph now holds a comment range), and a comment whose date changed (w:date, written since
#117) gives comments owner shell changed at /word/comments.xml[0]. With ignore_comments: true (comparison.rs:437 to 440; adding
Comment to ignored_stories is not needed) the pair compares (Rust example again), but the redline then
carries the original's comments and drops the edited side's: in the test, the added comment is gone.
So exposing the option is necessary and not sufficient. What a reviewer needs from a redline of two passes is
the edited version's comments, replies and resolved states, next to the tracked changes of the text.
Acceptance: compare() never refuses a pair over comments (added, removed, replied, resolved, re-dated),
and the redline carries the edited side's comment threads.
The same contract should hold for a rebuilt table of contents: after rebuild_toc() on the edited side, the
umbrella's workflow (#158) gets comparison cannot revise paragraph boundary structures at body/content-control[7]/content[2] rather than a revision of the TOC entries. The rebuild moves the field's
begin and separate into a paragraph of their own and writes each entry as a hyperlink with a PAGEREF
field; the refusal stays when every identity attribute is stripped from the fixture first. Both this and
the added comment above fall in the case PR #153 left out of scope: "Paragraphs with bookmarks, comment
ranges, hyperlinks, inline controls or unknown children still report a paragraph property change as a
diagnostic, and refuse it when the text also changes." #127 replaced a refusal on table grids with a
tracked replacement; a tracked replacement of the paragraph would do here too.
What I would like:
Document.compare(edited, author, timestamp, *, granularity="run"|"word"|"character", ignore_formatting=False, ignore_whitespace=False, ignore_fields=False, ignore_comments=False, ignored_stories=()) in Python;
- the same as flags on
rdocx compare (--granularity word, --ignore-comments, --ignore-story header...);
- and, if you agree,
word as the CLI default, since the CLI is where one produces a redline for a person.
Environment: main at 9a7ed714 (S75), release build, linux x86_64, python-docx 1.2.0 for the fixtures.
Document::compare_with_options(rdocx/src/comparison.rs:236) takes aComparisonOptionswith agranularity(Run,Word,Character),ignore_formatting,ignore_whitespace,ignore_fields,ignore_commentsandignored_stories. NeitherDocument.compare()in Python norrdocx comparehas away to pass them, so both always run with the default, and two defaults bite in practice. #76 brought
compareto the CLI and Python with the default options; this asks to open them.Whole-run granularity. With
ComparisonGranularity::Run, the default ("Preserve the legacy whole-runcomparison behavior"), one word changed in a paragraph held in a single run marks the whole paragraph as
deleted and re-inserted. Single-run paragraphs are the norm in files exported by Google Docs and in any
paragraph typed in one go, so a redline of a light editing pass reads as a rewrite of every touched
paragraph. Both sides below are written by python-docx; rdocx only compares:
Through a ten-line Rust example calling
compare_with_optionson the same pair,Wordgivesdeleted ['magna'] inserted ['MAGNA'], which is what a reviewer expects to see.Comments on one side only.
compare()refuses a pair whose comment parts differ:In a review cycle the edited version is exactly the one that carries new comments or replies, so the
default makes the redline of two consecutive passes impossible. Two more refusals of the same family, both
with a comment on each side: one comment added on the edited side gives
comparison cannot revise paragraph boundary structures at body/paragraph[1](the message of #135, but neither side has an emptyw:pPrhere: the paragraph now holds a comment range), and a comment whose date changed (
w:date, written since#117) gives
comments owner shell changed at /word/comments.xml[0]. Withignore_comments: true(comparison.rs:437to440; addingCommenttoignored_storiesis not needed) the pair compares (Rust example again), but the redline thencarries the original's comments and drops the edited side's: in the test, the added comment is gone.
So exposing the option is necessary and not sufficient. What a reviewer needs from a redline of two passes is
the edited version's comments, replies and resolved states, next to the tracked changes of the text.
Acceptance:
compare()never refuses a pair over comments (added, removed, replied, resolved, re-dated),and the redline carries the edited side's comment threads.
The same contract should hold for a rebuilt table of contents: after
rebuild_toc()on the edited side, theumbrella's workflow (#158) gets
comparison cannot revise paragraph boundary structures at body/content-control[7]/content[2]rather than a revision of the TOC entries. The rebuild moves the field'sbegin and separate into a paragraph of their own and writes each entry as a hyperlink with a
PAGEREFfield; the refusal stays when every identity attribute is stripped from the fixture first. Both this and
the added comment above fall in the case PR #153 left out of scope: "Paragraphs with bookmarks, comment
ranges, hyperlinks, inline controls or unknown children still report a paragraph property change as a
diagnostic, and refuse it when the text also changes." #127 replaced a refusal on table grids with a
tracked replacement; a tracked replacement of the paragraph would do here too.
What I would like:
Document.compare(edited, author, timestamp, *, granularity="run"|"word"|"character", ignore_formatting=False, ignore_whitespace=False, ignore_fields=False, ignore_comments=False, ignored_stories=())in Python;rdocx compare(--granularity word,--ignore-comments,--ignore-story header...);wordas the CLI default, since the CLI is where one produces a redline for a person.Environment:
mainat9a7ed714(S75), release build, linux x86_64, python-docx 1.2.0 for the fixtures.