Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
# MIT License. Shared helpers for the docx skill scripts.
|
||||
"""Shared helpers: paragraph iteration and run-preserving text replacement."""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def iter_all_paragraphs(doc, include_headers_footers: bool = True):
|
||||
"""Yield every paragraph in body, tables (recursively), headers, footers."""
|
||||
yield from _iter_container(doc)
|
||||
if include_headers_footers:
|
||||
for section in doc.sections:
|
||||
for part in (
|
||||
section.header, section.footer,
|
||||
section.first_page_header, section.first_page_footer,
|
||||
section.even_page_header, section.even_page_footer,
|
||||
):
|
||||
if part is not None:
|
||||
yield from _iter_container(part)
|
||||
|
||||
|
||||
def _iter_container(container):
|
||||
for para in container.paragraphs:
|
||||
yield para
|
||||
for table in container.tables:
|
||||
yield from _iter_table(table)
|
||||
|
||||
|
||||
def _iter_table(table):
|
||||
for row in table.rows:
|
||||
for cell in row.cells:
|
||||
for para in cell.paragraphs:
|
||||
yield para
|
||||
for nested in cell.tables:
|
||||
yield from _iter_table(nested)
|
||||
|
||||
|
||||
def iter_part_roots(doc):
|
||||
"""Yield the XML root of the body plus every header/footer part."""
|
||||
yield doc.element.body
|
||||
seen = set()
|
||||
for section in doc.sections:
|
||||
for part in (
|
||||
section.header, section.footer,
|
||||
section.first_page_header, section.first_page_footer,
|
||||
section.even_page_header, section.even_page_footer,
|
||||
):
|
||||
if part is not None and id(part._element) not in seen:
|
||||
seen.add(id(part._element))
|
||||
yield part._element
|
||||
|
||||
|
||||
def replace_in_paragraph(para, old: str, new: str) -> int:
|
||||
"""Replace `old` with `new` in a paragraph, preserving run formatting.
|
||||
|
||||
Strategy: first replace occurrences fully contained in a single run
|
||||
(formatting fully preserved). If the needle spans multiple runs, the
|
||||
matched runs are collapsed: the replacement inherits the formatting of
|
||||
the run where the match starts. Returns number of replacements made.
|
||||
"""
|
||||
if not old or old not in para.text:
|
||||
return 0
|
||||
count = 0
|
||||
# Pass 1: within-run replacements.
|
||||
for run in para.runs:
|
||||
if old in run.text:
|
||||
count += run.text.count(old)
|
||||
run.text = run.text.replace(old, new)
|
||||
# Pass 2: cross-run occurrences.
|
||||
while old in para.text:
|
||||
runs = para.runs
|
||||
# Map paragraph text offsets to (run_index, offset_in_run).
|
||||
full = "".join(r.text for r in runs)
|
||||
start = full.find(old)
|
||||
if start < 0:
|
||||
break
|
||||
end = start + len(old)
|
||||
pos = 0
|
||||
spans = [] # (run_idx, cut_start, cut_end) portions inside the match
|
||||
for i, r in enumerate(runs):
|
||||
r_start, r_end = pos, pos + len(r.text)
|
||||
if r_end > start and r_start < end:
|
||||
spans.append((i, max(start, r_start) - r_start,
|
||||
min(end, r_end) - r_start))
|
||||
pos = r_end
|
||||
first = True
|
||||
for i, cs, ce in spans:
|
||||
t = runs[i].text
|
||||
if first:
|
||||
runs[i].text = t[:cs] + new + t[ce:]
|
||||
first = False
|
||||
else:
|
||||
runs[i].text = t[:cs] + t[ce:]
|
||||
count += 1
|
||||
return count
|
||||
Reference in New Issue
Block a user