14
fold() — matching what Word actually stored
Every mapping is exactly one character to one character. That is what makes an offset computed on the folded string valid on the raw string it came from, and it is the whole reason offsets can be handed straight to the edit primitives.
examples/14_fold_matching.py — ran in 0.25s · exit 0
Console output
============================================================================
14 · fold()
============================================================================
--- what it folds ---
smart quotes '“Agreement” and Customer’s'
-> '"Agreement" and Customer\'s'
dashes 'twelve–12—month − rule'
-> 'twelve-12-month - rule'
prime marks '6′ by 4″'
-> '6\' by 4"'
nbsp / thin sp '30\xa0days\u2009apart'
-> '30 days apart'
zero width 'Order\u200bForm\ufeff'
-> 'Order Form '
tab 'Fees\tpayable'
-> 'Fees payable'
fullwidth 'ABC'
-> 'ABC'
--- length is preserved, always — that is the invariant ---
len 24 -> 24 ok
len 20 -> 20 ok
len 20 -> 20 ok
len 11 -> 11 ok
len 5 -> 5 ok
--- ligatures are left alone on purpose ---
NFKC('office') = 'office' (len 6) -- would shift every later offset
fold('office') = 'office' (len 5) -- left as-is
NFKC is applied one character at a time and kept only when it maps 1 -> 1
--- why it matters: a model quotes straight, Word stored curly ---
document has: (“Agreement”)
1 applied, 0 rejected
result: Master Agreement") is entered into as of September 1, 2026 (“Effective
--- find() and locate() fold both sides too ---
find('("Agreement")') -> no hit (already edited)
find('Customer\'s') straight -> [6, 8, 12, 14, 26] ...
--- it does not fold case — that is what ignore_case is for ---
fold('PROVIDER') == 'PROVIDER'
find('PROVIDER') -> [46] <- only 9.3, which really is in caps
find('PROVIDER', ignore_case=True) -> [4, 6, 10, 14, 16] ...
Source
"""14 · fold() — matching what Word actually stored.
Every mapping is exactly one character to one character. That is what makes an
offset computed on the folded string valid on the raw string it came from, and
it is the whole reason offsets can be handed straight to the edit primitives.
"""
import unicodedata
from _shared import banner, fresh, section
from docx_redline import ParagraphIndex, RedlineEdit, fold
banner("14 · fold()")
section("what it folds")
for label, raw in [
("smart quotes ", "“Agreement” and Customer’s"),
("dashes ", "twelve–12—month − rule"),
("prime marks ", "6′ by 4″"),
("nbsp / thin sp", "30 days apart"),
("zero width ", "OrderForm"),
("tab ", "Fees\tpayable"),
("fullwidth ", "ABC"),
]:
print(f" {label} {raw!r}\n{'':17} -> {fold(raw)!r}")
section("length is preserved, always — that is the invariant")
for raw in [
"“twelve (12)–month” term",
"a non-breaking space",
"an apostrophe’s curl",
"A fullwidth",
"a\ttab",
]:
print(
f" len {len(raw):>3} -> {len(fold(raw)):>3} "
f"{'ok' if len(fold(raw)) == len(raw) else 'BROKEN'}"
)
section("ligatures are left alone on purpose")
print(
f" NFKC('office') = {unicodedata.normalize('NFKC', 'office')!r} (len "
f"{len(unicodedata.normalize('NFKC', 'office'))}) -- would shift every later offset"
)
print(f" fold('office') = {fold('office')!r} (len {len(fold('office'))}) -- left as-is")
print(" NFKC is applied one character at a time and kept only when it maps 1 -> 1")
section("why it matters: a model quotes straight, Word stored curly")
rl = fresh()
index = ParagraphIndex(rl)
print(" document has:", index[3].text[index[3].text.index("(") : index[3].text.index(")") + 1])
report = index.apply([RedlineEdit(3, '("Agreement")', '("Master Agreement")', agent="definitions")])
print(" ", report.summary().splitlines()[0])
print(" result:", index[3].text[60:130])
section("find() and locate() fold both sides too")
print(" find('(\"Agreement\")') ->", index.find('("Agreement")') or "no hit (already edited)")
print(" find('Customer\\'s') straight ->", ParagraphIndex(fresh()).find("Customer's")[:5], "...")
section("it does not fold case — that is what ignore_case is for")
print(" fold('PROVIDER') ==", repr(fold("PROVIDER")))
print(
" find('PROVIDER') ->",
ParagraphIndex(fresh()).find("PROVIDER"),
"<- only 9.3, which really is in caps",
)
print(
" find('PROVIDER', ignore_case=True) ->",
ParagraphIndex(fresh()).find("PROVIDER", ignore_case=True)[:5],
"...",
)
What it wrote
This example prints its result rather than saving a document — read the
console output beside it.