docx-redline v1.0.2
11

ParagraphIndex — the stable integer address space

ParagraphIndex(rl, include_tables=True) len / iter / index[pid] -> ParagraphRef .clauses .paragraph(pid) .find(needle, ignore_case) .render(para_ids, with_clause_labels, skip_empty) .manifest() .fingerprint() .locate(...) .apply(...) .refresh() Clause numbers are the right address for a lawyer and the wrong one for a machine. Ids are positions in rl.paragraphs() and survive every text edit.

examples/11_paragraph_index.py — ran in 0.25s · exit 0

Console output

============================================================================
11 · ParagraphIndex
============================================================================

--- the index itself ---
  92 paragraphs, 54 numbered clauses
  fingerprint: 902892107d00df8a

--- ParagraphRef — every field ---
  para_id       = 19
  text          = '3.2  Invoicing. Provider shall invoice Customer annually i'
  style         = None
  numbered      = False
  in_table      = False
  table_index   = None
  clause_label  = '3.2'
  level         = 2
  is_empty      = False

--- a paragraph inside a table ---
  [65] in_table=True table_index=0 text='Signature'

--- include_tables=False — table cells drop out of the address space ---
  with tables   : 92
  without tables: 84

--- find(needle, ignore_case) — folded on both sides ---
  'thirty (30) days'        -> [19, 25, 26, 89]
  '("Agreement")' straight  -> [3]    <- the document has curly quotes
  'PROVIDER' ignore_case    -> [4, 6, 10, 14, 16, 19] ...

--- paragraph(pid) — the live python-docx object ---
   Paragraph | 3.2  Invoicing. Provider shall invoice Customer annu

--- render() — the cacheable prefix a model reads ---
<<clause 3>>
[17] 3. Fees and Payment

<<clause 3.1>>
[18] 3.1  Fees. Customer shall pay the fees set forth in the applicable Order Form (“Fees”). Unless otherwise specified, Fees are quoted and payable in U.S. dollars.

<<clause 3.2>>
[19] 3.2  Invoicing. Provider shall invoice Customer annually in advance unless a different billing frequency is specified in the Order Form. Payment is due within thirty (30) days of the invoice date.

<<clause 3.3>>
[20] 3.3  Late Payment. Amounts not paid when due shall accrue interest at the lesser of 1.5% per month or the maximum rate permitted by applicable law, and Provider may suspend access to the Services for accounts more than fifteen (15) days past due, upon prior written notice.

--- render options ---
  with_clause_labels=False:
    [18] 3.1  Fees. Customer shall pay the fees set forth in the applicable Order Form (“Fees”). Unless otherwise specified, Fees are quoted and payable in U.S. dollars.
    [19] 3.2  Invoicing. Provider shall invoice Customer annually in advance unless a different billing frequency is specified in the Order Form. Payment is due within thirty (30) days of the invoice date.
  skip_empty=True  -> 198 lines
  skip_empty=False -> 202 lines

--- manifest() — routing input, one line per clause ---
  1	paras 7-7	Definitions
  1.1	paras 8-8	“Authorized Users” means Customer’s employees and independen
  1.2	paras 9-9	“Customer Data” means all electronic data, text, files, or o
  1.3	paras 10-10	“Documentation” means Provider’s user guides and technical d
  1.4	paras 11-11	“Order Form” means an ordering document specifying the Servi
  1.5	paras 12-12	“Subscription Term” means the period of Customer’s subscript

--- locate() — resolve a quote without applying anything ---
  hit      : [(159, 175)]
  every hit: [(0, 8), (128, 136)]
  refusal  : Rejection.TARGET_NOT_FOUND

--- refresh() — re-index after structural work ---
  ids survive a text edit: thin forty-five (45) days of the invoice date.
  after inserting a paragraph, len(index) is stale: 92
  refresh() -> 93 paragraphs, new fingerprint 70feecc52601472b

Source

"""11 · ParagraphIndex — the stable integer address space.

    ParagraphIndex(rl, include_tables=True)
      len / iter / index[pid]      -> ParagraphRef
      .clauses .paragraph(pid) .find(needle, ignore_case)
      .render(para_ids, with_clause_labels, skip_empty)
      .manifest() .fingerprint() .locate(...) .apply(...) .refresh()

Clause numbers are the right address for a lawyer and the wrong one for a
machine. Ids are positions in rl.paragraphs() and survive every text edit.
"""

from _shared import banner, fresh, section

from docx_redline import ParagraphIndex, RedlineEdit

banner("11 · ParagraphIndex")
rl = fresh()
index = ParagraphIndex(rl)

section("the index itself")
print(f"  {len(index)} paragraphs, {len(index.clauses)} numbered clauses")
print(f"  fingerprint: {index.fingerprint()}")

section("ParagraphRef — every field")
ref = index[19]
for field in (
    "para_id",
    "text",
    "style",
    "numbered",
    "in_table",
    "table_index",
    "clause_label",
    "level",
    "is_empty",
):
    value = getattr(ref, field)
    print(
        f"  {field:<13} = {str(value)[:58]!r}" if field == "text" else f"  {field:<13} = {value!r}"
    )

section("a paragraph inside a table")
tabled = next(r for r in index if r.in_table)
print(
    f"  [{tabled.para_id}] in_table={tabled.in_table} table_index={tabled.table_index} "
    f"text={tabled.text!r}"
)

section("include_tables=False — table cells drop out of the address space")
print("  with tables   :", len(ParagraphIndex(fresh())))
print("  without tables:", len(ParagraphIndex(fresh(), include_tables=False)))

section("find(needle, ignore_case) — folded on both sides")
print("  'thirty (30) days'        ->", index.find("thirty (30) days"))
print(
    "  '(\"Agreement\")' straight  ->",
    index.find('("Agreement")'),
    "   <- the document has curly quotes",
)
print("  'PROVIDER' ignore_case    ->", index.find("PROVIDER", ignore_case=True)[:6], "...")

section("paragraph(pid) — the live python-docx object")
print("  ", type(index.paragraph(19)).__name__, "|", rl.text_of(index.paragraph(19))[:52])

section("render() — the cacheable prefix a model reads")
print(index.render(para_ids=range(17, 21)))

section("render options")
print("  with_clause_labels=False:")
print("   ", index.render(para_ids=range(18, 20), with_clause_labels=False).replace("\n", "\n    "))
print(f"  skip_empty=True  -> {len(index.render().splitlines())} lines")
print(f"  skip_empty=False -> {len(index.render(skip_empty=False).splitlines())} lines")

section("manifest() — routing input, one line per clause")
print("\n".join("  " + line for line in index.manifest().splitlines()[:6]))

section("locate() — resolve a quote without applying anything")
print("  hit      :", index.locate(19, "thirty (30) days"))
print("  every hit:", index.locate(6, "Provider", occurrence=0))
print("  refusal  :", index.locate(19, "not in this paragraph"))

section("refresh() — re-index after structural work")
index.apply([RedlineEdit(19, "thirty (30) days", "forty-five (45) days")])
print("  ids survive a text edit:", index[19].text[-46:])
rl.insert_paragraph_after(rl.find_paragraph(contains="3.4  Taxes"), "3.5  Currency.")
print("  after inserting a paragraph, len(index) is stale:", len(index))
index.refresh()
print("  refresh() ->", len(index), "paragraphs, new fingerprint", index.fingerprint())

What it wrote

This example prints its result rather than saving a document — read the console output beside it.