dsh-office-kit 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/skills/excel-live-control/SKILL.md +119 -152
- package/assets/skills/excel-live-control/domain_guidance/financial_models.md +42 -58
- package/assets/skills/excel-live-control/domain_guidance/healthcare.md +36 -42
- package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +33 -34
- package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +34 -36
- package/assets/skills/excel-live-control/references/office-automation.md +127 -153
- package/assets/skills/excel-live-control/references/read_only_qna.md +40 -45
- package/assets/skills/excel-live-control/style_guidelines.md +56 -64
- package/assets/skills/excel-live-control/workflows/create_workflows.md +40 -47
- package/assets/skills/excel-live-control/workflows/edit_workflows.md +37 -45
- package/assets/skills/office-documents/SKILL.md +24 -60
- package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +5 -5
- package/assets/skills/office-documents/ooxml/comments.md +8 -8
- package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +10 -10
- package/assets/skills/office-documents/ooxml/rels_and_content_types.md +7 -7
- package/assets/skills/office-documents/ooxml/tracked_changes.md +12 -12
- package/assets/skills/office-documents/references/docx-structure.md +11 -13
- package/assets/skills/office-documents/references/template-elicitation.md +11 -13
- package/assets/skills/office-documents/references/writing-quality.md +20 -20
- package/assets/skills/office-documents/scripts/add_tracked_replacements.py +4 -3
- package/assets/skills/office-documents/scripts/docx_kit.py +68 -27
- package/assets/skills/office-documents/scripts/heading_audit.py +3 -4
- package/assets/skills/office-documents/scripts/internal_nav.py +3 -1
- package/assets/skills/office-documents/scripts/merge_docx_append.py +104 -0
- package/assets/skills/office-documents/scripts/table_geometry.py +13 -3
- package/assets/skills/office-documents/scripts/watermark_add.py +6 -5
- package/assets/skills/office-documents/scripts/watermark_audit_remove.py +6 -4
- package/assets/skills/office-documents/tasks/accessibility_a11y.md +16 -22
- package/assets/skills/office-documents/tasks/captions_crossrefs.md +23 -26
- package/assets/skills/office-documents/tasks/clean_tracked_changes.md +17 -22
- package/assets/skills/office-documents/tasks/comments_manage.md +9 -11
- package/assets/skills/office-documents/tasks/compare_diff.md +10 -16
- package/assets/skills/office-documents/tasks/create_edit.md +14 -16
- package/assets/skills/office-documents/tasks/equations.md +17 -28
- package/assets/skills/office-documents/tasks/fields_update.md +14 -16
- package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +19 -33
- package/assets/skills/office-documents/tasks/footnotes_endnotes.md +12 -14
- package/assets/skills/office-documents/tasks/forms_content_controls.md +16 -20
- package/assets/skills/office-documents/tasks/headings_numbering.md +17 -22
- package/assets/skills/office-documents/tasks/images_figures.md +15 -25
- package/assets/skills/office-documents/tasks/multi_doc_merge.md +13 -23
- package/assets/skills/office-documents/tasks/navigation_internal_links.md +18 -21
- package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +16 -22
- package/assets/skills/office-documents/tasks/protection_restrict_editing.md +19 -21
- package/assets/skills/office-documents/tasks/read_review.md +9 -20
- package/assets/skills/office-documents/tasks/redaction_anonymization.md +17 -18
- package/assets/skills/office-documents/tasks/sections_layout.md +16 -23
- package/assets/skills/office-documents/tasks/style_lint_normalize.md +18 -22
- package/assets/skills/office-documents/tasks/tables_spreadsheets.md +21 -31
- package/assets/skills/office-documents/tasks/templates_style_packs.md +17 -21
- package/assets/skills/office-documents/tasks/toc_workflow.md +10 -14
- package/assets/skills/office-documents/tasks/verify_render.md +11 -23
- package/assets/skills/office-documents/tasks/watermarks_background.md +10 -14
- package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +6 -6
- package/assets/skills/office-documents/troubleshooting/run_splitting.md +12 -12
- package/assets/skills/office-pdf/SKILL.md +36 -51
- package/assets/skills/office-pdf/references/form-fields.md +20 -20
- package/assets/skills/office-pdf/references/geometry.md +10 -10
- package/assets/skills/office-pdf/references/layout-quality.md +13 -13
- package/assets/skills/office-pdf/references/limits.md +12 -56
- package/assets/skills/office-pdf/tasks/annotations.md +5 -5
- package/assets/skills/office-pdf/tasks/create.md +6 -9
- package/assets/skills/office-pdf/tasks/edit-pages.md +14 -14
- package/assets/skills/office-pdf/tasks/forms.md +15 -15
- package/assets/skills/office-pdf/tasks/read.md +9 -9
- package/assets/skills/office-pdf/tasks/redact.md +18 -18
- package/assets/skills/office-pdf/tasks/stamp.md +10 -10
- package/assets/skills/office-presentations/SKILL.md +71 -91
- package/assets/skills/office-presentations/container_tools/preview_render.py +4 -1
- package/assets/skills/office-presentations/container_tools/render_slides.py +1 -1
- package/assets/skills/office-presentations/domain_guidance/finance.md +16 -16
- package/assets/skills/office-presentations/recipes/README.md +24 -35
- package/assets/skills/office-presentations/recipes/autolayout.md +12 -13
- package/assets/skills/office-presentations/recipes/connectors.md +11 -12
- package/assets/skills/office-presentations/recipes/images.md +11 -12
- package/assets/skills/office-presentations/recipes/master-and-layouts.md +18 -18
- package/assets/skills/office-presentations/recipes/native-charts.md +8 -8
- package/assets/skills/office-presentations/recipes/rich-text-runs.md +13 -13
- package/assets/skills/office-presentations/recipes/speaker-notes.md +7 -7
- package/assets/skills/office-presentations/recipes/tables.md +14 -14
- package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +9 -9
- package/assets/skills/office-presentations/recipes/theme-tokens.md +7 -8
- package/assets/skills/office-presentations/references/authoring-workflow.md +38 -43
- package/assets/skills/office-presentations/references/finalization.md +10 -12
- package/assets/skills/office-presentations/references/native-bullets.md +14 -18
- package/assets/skills/office-presentations/references/native-evidence.md +18 -32
- package/assets/skills/office-presentations/references/style-guidelines.md +27 -32
- package/assets/skills/office-presentations/references/templates.md +31 -60
- package/assets/skills/office-presentations/routing/google_slides.md +4 -4
- package/assets/skills/office-presentations/tasks/apply-annotations.md +17 -17
- package/assets/skills/office-presentations/tasks/charts-tables-notes.md +20 -23
- package/assets/skills/office-presentations/tasks/create-deck.md +19 -19
- package/assets/skills/office-presentations/tasks/edit-deck.md +14 -16
- package/assets/skills/office-presentations/tasks/inspect-and-verify.md +31 -34
- package/assets/skills/office-presentations/tasks/template-following.md +23 -28
- package/assets/skills/office-spreadsheets/SKILL.md +32 -39
- package/assets/skills/office-spreadsheets/recipes/charts.md +8 -26
- package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +9 -26
- package/assets/skills/office-spreadsheets/recipes/data-tables.md +7 -23
- package/assets/skills/office-spreadsheets/recipes/named-ranges.md +8 -21
- package/assets/skills/office-spreadsheets/recipes/number-formats.md +7 -23
- package/assets/skills/office-spreadsheets/recipes/sparklines.md +7 -20
- package/assets/skills/office-spreadsheets/references/charts.md +15 -34
- package/assets/skills/office-spreadsheets/references/domain/financial-models.md +10 -25
- package/assets/skills/office-spreadsheets/references/domain/healthcare.md +8 -19
- package/assets/skills/office-spreadsheets/references/domain/marketing.md +8 -22
- package/assets/skills/office-spreadsheets/references/domain/research.md +8 -19
- package/assets/skills/office-spreadsheets/references/formula-patterns.md +20 -50
- package/assets/skills/office-spreadsheets/references/style-guidelines.md +19 -44
- package/assets/skills/office-spreadsheets/references/workbook-structure.md +12 -35
- package/assets/skills/office-spreadsheets/routing/google-sheets.md +11 -26
- package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +10 -25
- package/assets/skills/office-spreadsheets/tasks/annotations.md +12 -30
- package/assets/skills/office-spreadsheets/tasks/create-workbook.md +5 -5
- package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +2 -2
- package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +5 -5
- package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +9 -9
- package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +10 -30
- package/assets/skills/shared/annotation-recipes.md +98 -120
- package/assets/skills/shared/common-workflow.md +70 -80
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/python/office_cli.py +38 -4
|
@@ -250,7 +250,7 @@ class Docx:
|
|
|
250
250
|
while self.exists("word/%s%d.xml" % (kind, index)):
|
|
251
251
|
index += 1
|
|
252
252
|
part = "word/%s%d.xml" % (kind, index)
|
|
253
|
-
root = etree.Element(q(kind), nsmap={"w": W, "r": R, "wp": WP, "v": V})
|
|
253
|
+
root = etree.Element(q("hdr" if kind == "header" else "ftr"), nsmap={"w": W, "r": R, "wp": WP, "v": V})
|
|
254
254
|
etree.SubElement(etree.SubElement(root, q("p")), q("r"))
|
|
255
255
|
self.set_xml(part, root)
|
|
256
256
|
self.ensure_override(part, CT_BY_REL[rel_type])
|
|
@@ -459,45 +459,86 @@ def add_bookmark(parent, name: str, ident: int) -> None:
|
|
|
459
459
|
|
|
460
460
|
# -- watermarks -------------------------------------------------------------
|
|
461
461
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
462
|
+
WATERMARK_DESCR = "DSH Office watermark"
|
|
463
|
+
|
|
464
|
+
_WATERMARK_ANCHOR = """<wp:anchor xmlns:wp="%(wp)s" xmlns:a="%(a)s" xmlns:w="%(w)s" xmlns:wps="%(wps)s"
|
|
465
|
+
distT="0" distB="0" distL="0" distR="0" simplePos="0" relativeHeight="251658240"
|
|
466
|
+
behindDoc="1" locked="0" layoutInCell="1" allowOverlap="1">
|
|
467
|
+
<wp:simplePos x="0" y="0"/>
|
|
468
|
+
<wp:positionH relativeFrom="page"><wp:align>center</wp:align></wp:positionH>
|
|
469
|
+
<wp:positionV relativeFrom="page"><wp:align>center</wp:align></wp:positionV>
|
|
470
|
+
<wp:extent cx="5486400" cy="1371600"/>
|
|
471
|
+
<wp:effectExtent l="0" t="0" r="0" b="0"/>
|
|
472
|
+
<wp:wrapNone/>
|
|
473
|
+
<wp:docPr id="%(id)d" name="PowerPlusWaterMarkObject%(id)d" descr="%(descr)s"/>
|
|
474
|
+
<wp:cNvGraphicFramePr/>
|
|
475
|
+
<a:graphic><a:graphicData uri="%(wps)s"><wps:wsp>
|
|
476
|
+
<wps:cNvSpPr/>
|
|
477
|
+
<wps:spPr>
|
|
478
|
+
<a:xfrm rot="-2700000"><a:off x="0" y="0"/><a:ext cx="5486400" cy="1371600"/></a:xfrm>
|
|
479
|
+
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
|
|
480
|
+
<a:noFill/><a:ln><a:noFill/></a:ln>
|
|
481
|
+
</wps:spPr>
|
|
482
|
+
<wps:txbx><w:txbxContent><w:p><w:pPr><w:jc w:val="center"/></w:pPr>
|
|
483
|
+
<w:r><w:rPr><w:color w:val="%(color)s"/><w:sz w:val="120"/></w:rPr><w:t>%(text)s</w:t></w:r>
|
|
484
|
+
</w:p></w:txbxContent></wps:txbx>
|
|
485
|
+
<wps:bodyPr vert="horz" wrap="square" lIns="0" tIns="0" rIns="0" bIns="0"
|
|
486
|
+
anchor="ctr" anchorCtr="0"><a:noAutofit/></wps:bodyPr>
|
|
487
|
+
</wps:wsp></a:graphicData></a:graphic>
|
|
488
|
+
</wp:anchor>"""
|
|
489
|
+
|
|
490
|
+
_WPS_NS = "http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
|
|
470
491
|
|
|
471
492
|
|
|
472
493
|
def watermark_paragraph(text: str, ident: int = 1, color: str = "#C0C0C0"):
|
|
473
|
-
"""A header paragraph holding one
|
|
494
|
+
"""A header paragraph holding one DrawingML text watermark.
|
|
495
|
+
|
|
496
|
+
Word stores watermarks as VML, but VML shapes are routinely dropped by other
|
|
497
|
+
renderers, so the shape here is DrawingML (wps) and marked with a stable
|
|
498
|
+
``descr`` so it can be found and removed again.
|
|
499
|
+
"""
|
|
500
|
+
from xml.sax.saxutils import escape
|
|
501
|
+
|
|
502
|
+
color = color.lstrip("#").upper() or "C0C0C0"
|
|
474
503
|
paragraph = etree.Element(q("p"))
|
|
475
504
|
run = etree.SubElement(paragraph, q("r"))
|
|
476
505
|
run_props = etree.SubElement(run, q("rPr"))
|
|
477
506
|
etree.SubElement(run_props, q("noProof"))
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
507
|
+
drawing = etree.SubElement(run, q("drawing"))
|
|
508
|
+
anchor = etree.fromstring(_WATERMARK_ANCHOR % {
|
|
509
|
+
"wp": WP,
|
|
510
|
+
"a": A,
|
|
511
|
+
"w": W,
|
|
512
|
+
"wps": _WPS_NS,
|
|
513
|
+
"id": ident,
|
|
514
|
+
"descr": WATERMARK_DESCR,
|
|
515
|
+
"color": color,
|
|
516
|
+
"text": escape(str(text)),
|
|
517
|
+
})
|
|
518
|
+
drawing.append(anchor)
|
|
489
519
|
return paragraph
|
|
490
520
|
|
|
491
521
|
|
|
522
|
+
def _paragraph_above(node):
|
|
523
|
+
while node is not None and node.tag != q("p"):
|
|
524
|
+
node = node.getparent()
|
|
525
|
+
return node
|
|
526
|
+
|
|
527
|
+
|
|
492
528
|
def watermark_texts(doc: Docx) -> list[dict]:
|
|
493
|
-
"""Every VML text watermark in the headers, with
|
|
529
|
+
"""Every VML or DrawingML text watermark in the headers, with its paragraph."""
|
|
494
530
|
found = []
|
|
495
531
|
for part in doc.names("word/header"):
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
532
|
+
root = doc.xml(part)
|
|
533
|
+
for textpath in root.iter(q("textpath", V)):
|
|
534
|
+
text = textpath.get("string")
|
|
535
|
+
if text:
|
|
536
|
+
found.append({"part": part, "kind": "vml", "text": text, "paragraph": _paragraph_above(textpath)})
|
|
537
|
+
for pr in root.iter(q("docPr", WP)):
|
|
538
|
+
if pr.get("descr") != WATERMARK_DESCR:
|
|
539
|
+
continue
|
|
540
|
+
text = "".join(node.text or "" for node in pr.getparent().iter(q("t")))
|
|
541
|
+
found.append({"part": part, "kind": "drawingml", "text": text, "paragraph": _paragraph_above(pr)})
|
|
501
542
|
return found
|
|
502
543
|
|
|
503
544
|
|
|
@@ -58,7 +58,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
58
58
|
for index, paragraph in enumerate(doc.paragraphs(), start=1):
|
|
59
59
|
text = kit.paragraph_text(paragraph).strip()
|
|
60
60
|
level = kit.heading_level(paragraph)
|
|
61
|
-
|
|
61
|
+
style = (kit.paragraph_style(paragraph) or "").strip().lower()
|
|
62
|
+
if style == "title":
|
|
62
63
|
outline.append({"index": index, "style": "Title", "text": text})
|
|
63
64
|
continue
|
|
64
65
|
if level:
|
|
@@ -71,9 +72,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
71
72
|
if manual_heading(paragraph):
|
|
72
73
|
findings.append({"kind": "manual_formatting", "index": index, "text": text})
|
|
73
74
|
previous = level
|
|
74
|
-
elif
|
|
75
|
-
continue
|
|
76
|
-
elif manual_heading(paragraph) and len(text) <= 120:
|
|
75
|
+
elif text and manual_heading(paragraph) and len(text) <= 120:
|
|
77
76
|
findings.append({"kind": "heading_not_styled", "index": index, "text": text})
|
|
78
77
|
|
|
79
78
|
counts = {}
|
|
@@ -131,8 +131,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
131
131
|
body.insert(position + offset, node)
|
|
132
132
|
|
|
133
133
|
if not args.no_back_to_toc:
|
|
134
|
+
anchors = {entry["anchor"] for entry in headings}
|
|
134
135
|
for paragraph_node in list(body.iter(kit.q("p"))):
|
|
135
|
-
|
|
136
|
+
start = paragraph_node.find(kit.q("bookmarkStart"))
|
|
137
|
+
if start is not None and start.get(kit.q("name")) in anchors:
|
|
136
138
|
back = paragraph("Caption")
|
|
137
139
|
back.append(link_run("Back to contents", TOC_BOOKMARK))
|
|
138
140
|
paragraph_node.addnext(back)
|
|
@@ -173,6 +173,101 @@ def remap_relationships(base: kit.Docx, append: kit.Docx, nodes: list) -> int:
|
|
|
173
173
|
return copied
|
|
174
174
|
|
|
175
175
|
|
|
176
|
+
NOTE_PARTS = (
|
|
177
|
+
("word/footnotes.xml", kit.REL_FOOTNOTES, "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml", kit.q("footnote"), (kit.q("footnoteReference"),)),
|
|
178
|
+
("word/endnotes.xml", kit.REL_ENDNOTES, "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml", kit.q("endnote"), (kit.q("endnoteReference"),)),
|
|
179
|
+
("word/comments.xml", kit.REL_COMMENTS, "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml", kit.q("comment"), (kit.q("commentReference"), kit.q("commentRangeStart"), kit.q("commentRangeEnd"))),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def remap_part_relationships(base: kit.Docx, append: kit.Docx, append_part: str, base_part: str, nodes: list) -> int:
|
|
184
|
+
"""Remap r:embed/r:id/r:link inside nodes, copying targets into the base package."""
|
|
185
|
+
source_rels = {entry["id"]: entry for entry in append.relationships(append_part)}
|
|
186
|
+
attributes = (kit.q("embed", kit.R), kit.q("id", kit.R), kit.q("link", kit.R))
|
|
187
|
+
mapping = {}
|
|
188
|
+
copied = 0
|
|
189
|
+
for node in nodes:
|
|
190
|
+
for element in node.iter():
|
|
191
|
+
for attribute in attributes:
|
|
192
|
+
rid = element.get(attribute)
|
|
193
|
+
if not rid:
|
|
194
|
+
continue
|
|
195
|
+
if rid not in mapping:
|
|
196
|
+
rel = source_rels.get(rid)
|
|
197
|
+
if rel is None:
|
|
198
|
+
mapping[rid] = rid
|
|
199
|
+
continue
|
|
200
|
+
if rel["mode"] == "External":
|
|
201
|
+
mapping[rid] = base.ensure_rel(base_part, rel["type"], rel["target"], external=True)
|
|
202
|
+
elif append.exists(rel["resolved"]):
|
|
203
|
+
target = copy_part(base, append, rel["resolved"])
|
|
204
|
+
relative = posixpath.relpath(target, posixpath.dirname(base_part))
|
|
205
|
+
mapping[rid] = base.ensure_rel(base_part, rel["type"], relative)
|
|
206
|
+
copied += 1
|
|
207
|
+
else:
|
|
208
|
+
mapping[rid] = rid
|
|
209
|
+
element.set(attribute, mapping[rid])
|
|
210
|
+
return copied
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def merge_note_part(base: kit.Docx, append: kit.Docx, part: str, rel_type: str, content_type: str, note_tag: str) -> dict[str, str]:
|
|
214
|
+
"""Merge a footnotes/endnotes/comments part into the base, returning old→new ids."""
|
|
215
|
+
if not append.exists(part):
|
|
216
|
+
return {}
|
|
217
|
+
source_root = append.xml(part)
|
|
218
|
+
if base.exists(part):
|
|
219
|
+
target_root = base.xml(part)
|
|
220
|
+
else:
|
|
221
|
+
target_root = etree.Element(source_root.tag, nsmap=source_root.nsmap)
|
|
222
|
+
for note in source_root.findall(note_tag):
|
|
223
|
+
raw = note.get(kit.q("id"))
|
|
224
|
+
if raw is not None and raw.lstrip("-").isdigit() and int(raw) <= 0:
|
|
225
|
+
target_root.append(etree.fromstring(etree.tostring(note)))
|
|
226
|
+
base.set_xml(part, target_root)
|
|
227
|
+
base.ensure_override(part, content_type)
|
|
228
|
+
base.ensure_rel("word/document.xml", rel_type, posixpath.relpath(part, "word"))
|
|
229
|
+
|
|
230
|
+
used = {
|
|
231
|
+
int(node.get(kit.q("id")))
|
|
232
|
+
for node in target_root.findall(note_tag)
|
|
233
|
+
if (node.get(kit.q("id")) or "").lstrip("-").isdigit()
|
|
234
|
+
}
|
|
235
|
+
next_id = max(used, default=0) + 1
|
|
236
|
+
mapping: dict[str, str] = {}
|
|
237
|
+
for note in source_root.findall(note_tag):
|
|
238
|
+
raw = note.get(kit.q("id"))
|
|
239
|
+
if raw is None or not raw.lstrip("-").isdigit() or int(raw) <= 0:
|
|
240
|
+
continue
|
|
241
|
+
clone = etree.fromstring(etree.tostring(note))
|
|
242
|
+
clone.set(kit.q("id"), str(next_id))
|
|
243
|
+
remap_part_relationships(base, append, part, part, [clone])
|
|
244
|
+
target_root.append(clone)
|
|
245
|
+
mapping[raw] = str(next_id)
|
|
246
|
+
next_id += 1
|
|
247
|
+
return mapping
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def bind_note_references(clones: list, ref_tags: tuple, mapping: dict[str, str], target_exists: bool) -> int:
|
|
251
|
+
"""Point body references at the merged notes; drop references that have no note."""
|
|
252
|
+
dropped = 0
|
|
253
|
+
for clone in clones:
|
|
254
|
+
for ref_tag in ref_tags:
|
|
255
|
+
for element in list(clone.iter(ref_tag)):
|
|
256
|
+
raw = element.get(kit.q("id"))
|
|
257
|
+
if raw is None:
|
|
258
|
+
continue
|
|
259
|
+
if raw in mapping:
|
|
260
|
+
element.set(kit.q("id"), mapping[raw])
|
|
261
|
+
elif target_exists and raw.isdigit() and int(raw) <= 0:
|
|
262
|
+
continue
|
|
263
|
+
else:
|
|
264
|
+
parent = element.getparent()
|
|
265
|
+
if parent is not None:
|
|
266
|
+
parent.remove(element)
|
|
267
|
+
dropped += 1
|
|
268
|
+
return dropped
|
|
269
|
+
|
|
270
|
+
|
|
176
271
|
def main(argv: list[str] | None = None) -> int:
|
|
177
272
|
parser = argparse.ArgumentParser(description="Append one .docx body to another, copying media, numbering and styles.")
|
|
178
273
|
parser.add_argument("base", help="Document to append to")
|
|
@@ -220,6 +315,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
220
315
|
if num_id is not None and num_id.get(kit.q("val")) in number_map:
|
|
221
316
|
num_id.set(kit.q("val"), number_map[num_id.get(kit.q("val"))])
|
|
222
317
|
media_copied = remap_relationships(base, append, clones)
|
|
318
|
+
notes_merged = 0
|
|
319
|
+
references_dropped = 0
|
|
320
|
+
for part, rel_type, content_type, note_tag, ref_tags in NOTE_PARTS:
|
|
321
|
+
exists = append.exists(part)
|
|
322
|
+
mapping = merge_note_part(base, append, part, rel_type, content_type, note_tag) if exists else {}
|
|
323
|
+
notes_merged += len(mapping)
|
|
324
|
+
references_dropped += bind_note_references(clones, ref_tags, mapping, exists)
|
|
223
325
|
copy_defaults(base, append)
|
|
224
326
|
|
|
225
327
|
body = base.body()
|
|
@@ -237,6 +339,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
237
339
|
"styles_copied": styles_copied,
|
|
238
340
|
"media_copied": media_copied,
|
|
239
341
|
"numbering_remapped": len(number_map),
|
|
342
|
+
"notes_merged": notes_merged,
|
|
343
|
+
"references_dropped": references_dropped,
|
|
240
344
|
}
|
|
241
345
|
)
|
|
242
346
|
return 0
|
|
@@ -98,7 +98,11 @@ def fix_table(table, target: int) -> None:
|
|
|
98
98
|
if not columns:
|
|
99
99
|
columns = [1]
|
|
100
100
|
total = sum(columns) or len(columns)
|
|
101
|
-
scaled = [max(1,
|
|
101
|
+
scaled = [max(1, target * value // total) for value in columns]
|
|
102
|
+
remainder = target - sum(scaled)
|
|
103
|
+
order = sorted(range(len(columns)), key=lambda index: (target * columns[index]) % total, reverse=True)
|
|
104
|
+
for index in range(max(0, remainder)):
|
|
105
|
+
scaled[order[index % len(order)]] += 1
|
|
102
106
|
grid = table.find(kit.q("tblGrid"))
|
|
103
107
|
if grid is None:
|
|
104
108
|
grid = etree.Element(kit.q("tblGrid"))
|
|
@@ -163,8 +167,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
163
167
|
fix_table(table, target)
|
|
164
168
|
out = args.out or args.docx
|
|
165
169
|
doc.save(out)
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
remaining = []
|
|
171
|
+
for index, info in enumerate((table_info(table) for table in tables), start=1):
|
|
172
|
+
if info["grid_total"] and info["grid_total"] > target:
|
|
173
|
+
remaining.append({"kind": "grid_wider_than_text", "table": index, "grid_total": info["grid_total"], "text_width": target})
|
|
174
|
+
if info["layout"] != "fixed":
|
|
175
|
+
remaining.append({"kind": "not_fixed_layout", "table": index})
|
|
176
|
+
kit.emit({"ok": not remaining, "fixed": True, "output": str(Path(out)), "tables": len(tables), "text_width": target, "findings": remaining})
|
|
177
|
+
return 0 if not remaining else 1
|
|
168
178
|
|
|
169
179
|
kit.emit({"ok": not findings, "text_width": target, "tables": rows, "findings": findings})
|
|
170
180
|
return 1 if findings else 0
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
|
-
"""Add a
|
|
2
|
+
"""Add a text watermark to the page background.
|
|
3
3
|
|
|
4
|
-
The watermark is a
|
|
5
|
-
|
|
6
|
-
page
|
|
4
|
+
The watermark is a DrawingML text box in the default header: Word rotates it
|
|
5
|
+
-45 degrees, while VML-only renderers that drop VML shapes still show the mark.
|
|
6
|
+
It prints behind the body on every page and can be removed again by deleting
|
|
7
|
+
the header paragraph.
|
|
7
8
|
|
|
8
9
|
Exit codes: 0 success, 2 bad input.
|
|
9
10
|
"""
|
|
@@ -26,7 +27,7 @@ for _stream in (sys.stdout, sys.stderr):
|
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def main(argv: list[str] | None = None) -> int:
|
|
29
|
-
parser = argparse.ArgumentParser(description="Add a
|
|
30
|
+
parser = argparse.ArgumentParser(description="Add a DrawingML text watermark to the document header.")
|
|
30
31
|
parser.add_argument("input_docx", help="Input .docx")
|
|
31
32
|
parser.add_argument("--out", required=True, help="Output .docx")
|
|
32
33
|
parser.add_argument("--text", required=True, help="Watermark text, e.g. CONFIDENTIAL")
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
|
-
"""Find and remove
|
|
2
|
+
"""Find and remove text watermarks from headers.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Both DrawingML watermarks (what ``watermark_add.py`` writes) and legacy VML
|
|
5
|
+
``v:textpath`` shapes are detected. A watermark that was meant for a draft can
|
|
6
|
+
end up in a signed document, and it is invisible in text extraction because it
|
|
7
|
+
lives in a shape, not in a paragraph run.
|
|
6
8
|
|
|
7
9
|
Exit codes: 0 clean, 1 watermarks found (report mode), 2 bad input.
|
|
8
10
|
"""
|
|
@@ -25,7 +27,7 @@ for _stream in (sys.stdout, sys.stderr):
|
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
def main(argv: list[str] | None = None) -> int:
|
|
28
|
-
parser = argparse.ArgumentParser(description="Report or remove watermarks stored as VML shapes in headers.")
|
|
30
|
+
parser = argparse.ArgumentParser(description="Report or remove watermarks stored as DrawingML or VML shapes in headers.")
|
|
29
31
|
parser.add_argument("input_docx", help="Input .docx")
|
|
30
32
|
parser.add_argument("--mode", choices=("report", "remove"), default="report")
|
|
31
33
|
parser.add_argument("--contains", default=None, help="Only match watermarks containing this text")
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# Accessibility
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
The accessibility tree in Word comes from structure, not appearance. Bold text announces as bold; a `Heading 1` announces as a heading and appears in the navigation pane. Alt text lives in `wp:docPr @descr`.
|
|
3
|
+
Accessibility comes from structure, not appearance: bold announces as bold, `Heading 1` announces as a heading and appears in the navigation pane. Alt text lives in `wp:docPr @descr`.
|
|
6
4
|
|
|
7
5
|
## Audit
|
|
8
6
|
|
|
@@ -10,36 +8,32 @@ The accessibility tree in Word comes from structure, not appearance. Bold text a
|
|
|
10
8
|
"<python>" "<skill>/scripts/a11y_audit.py" "<file.docx>" --out_json "<report.json>"
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
Checks
|
|
14
|
-
|
|
15
|
-
Exit code 1 means at least one finding; `--out_json` writes the same report for a larger pass.
|
|
11
|
+
Checks images without description or title, tables without a repeating header row, heading levels that jump, empty headings, link text that is only "click here", "here" or "link", and a missing core title. Exit 1 means at least one finding.
|
|
16
12
|
|
|
17
|
-
##
|
|
13
|
+
## Mechanical fixes
|
|
18
14
|
|
|
19
15
|
```bash
|
|
20
|
-
"<python>" "<skill>/scripts/a11y_audit.py" "<file.docx>"
|
|
21
|
-
--fix_image_alt from_filename --fix_table_headers first_row \
|
|
22
|
-
--out "<file-a11y.docx>"
|
|
16
|
+
"<python>" "<skill>/scripts/a11y_audit.py" "<file.docx>" --fix_image_alt from_filename --fix_table_headers first_row --out "<file-a11y.docx"
|
|
23
17
|
```
|
|
24
18
|
|
|
25
|
-
|
|
19
|
+
These are honest placeholders, not substitutes for real text.
|
|
26
20
|
|
|
27
|
-
##
|
|
21
|
+
## Judgement fixes
|
|
28
22
|
|
|
29
|
-
- **Alt text
|
|
30
|
-
- **Link text
|
|
31
|
-
- **Reading order
|
|
32
|
-
- **Colour
|
|
33
|
-
- **
|
|
23
|
+
- **Alt text** describes what the reader loses, e.g. "Bar chart: revenue by region, EMEA 4.1M ahead of APAC 3.2M". Decorative rules and dividers get an empty description.
|
|
24
|
+
- **Link text** names the destination: "Download the Q2 ledger", not "click here".
|
|
25
|
+
- **Reading order:** decorative text boxes and floating images land awkwardly; convert them to inline.
|
|
26
|
+
- **Colour alone** (red = bad) fails colour-blind readers; add a word.
|
|
27
|
+
- **Layout tables** announce as data tables; restructure them as paragraphs.
|
|
34
28
|
|
|
35
|
-
|
|
29
|
+
Longer documents also want a TOC (`tasks/toc_workflow.md`) and language tags.
|
|
36
30
|
|
|
37
31
|
## Verify
|
|
38
32
|
|
|
39
|
-
Re-run the audit to zero findings
|
|
33
|
+
Re-run the audit to zero findings and render. Heading hierarchy is visible; alt text is metadata and stays invisible; the header row must be distinct. With a screen reader available, navigate by heading.
|
|
40
34
|
|
|
41
35
|
## Pitfalls
|
|
42
36
|
|
|
43
|
-
- Alt text on a decorative image is worse than none
|
|
44
|
-
- A
|
|
45
|
-
- Images inside
|
|
37
|
+
- Alt text on a decorative image is worse than none: use an empty description plus `title` to mark it decorative.
|
|
38
|
+
- A header row is `w:tblHeader` on the row, not a bold first row.
|
|
39
|
+
- Images inside table cells still need descriptions.
|
|
@@ -1,49 +1,46 @@
|
|
|
1
|
-
# Captions and cross-references
|
|
1
|
+
# Captions and numbered cross-references
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Two parts: a caption paragraph with a `SEQ` field (`SEQ Table \* ARABIC`), and a bookmark around it that the prose reaches with a `REF` field. Without the bookmark a `REF` has nothing to point at.
|
|
3
|
+
A caption is a paragraph with a `SEQ` field (`SEQ Table \* ARABIC`) plus a bookmark; the prose reaches that bookmark with a `REF` field. Without the bookmark a `REF` has no target.
|
|
6
4
|
|
|
7
5
|
## Add captions
|
|
8
6
|
|
|
9
7
|
```bash
|
|
10
|
-
"<python>" "<skill>/scripts/captions_and_crossrefs.py" "<file.docx>" "<file-captioned.docx>"
|
|
11
|
-
--tables --figures --bookmarks --caption_text "Source: finance ledger"
|
|
8
|
+
"<python>" "<skill>/scripts/captions_and_crossrefs.py" "<file.docx>" "<file-captioned.docx>" --tables --figures --bookmarks --caption_text "Source: finance ledger"
|
|
12
9
|
```
|
|
13
10
|
|
|
14
|
-
- `--tables` adds `Table N` under each table
|
|
15
|
-
-
|
|
16
|
-
- `--bookmarks` names
|
|
17
|
-
- `--materialize` writes literal numbers
|
|
18
|
-
- `--caption_text` appends a fixed note
|
|
11
|
+
- `--tables` adds `Table N` under each table; `--figures` adds `Figure N` under each picture.
|
|
12
|
+
- Neither flag runs both.
|
|
13
|
+
- `--bookmarks` names them `tbl1`, `tbl2`, `fig1`, … — the `tblN`/`figN` pattern `insert_ref_fields.py` expects.
|
|
14
|
+
- `--materialize` writes literal numbers: for a frozen PDF export, not for a document that will change.
|
|
15
|
+
- `--caption_text` appends a fixed note.
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
Captions use the `Caption` style and stay in the same visual block as the object, so a page break cannot orphan them.
|
|
21
18
|
|
|
22
|
-
##
|
|
19
|
+
## Cross-reference
|
|
23
20
|
|
|
24
|
-
Write placeholders in
|
|
21
|
+
Write `[[REF:tbl1]]` placeholders in the prose, then:
|
|
25
22
|
|
|
26
23
|
```bash
|
|
27
|
-
"<python>" "<skill>/scripts/insert_ref_fields.py" "<file.docx>" "<file-refs.docx
|
|
24
|
+
"<python>" "<skill>/scripts/insert_ref_fields.py" "<file.docx>" "<file-refs.docx"
|
|
28
25
|
```
|
|
29
26
|
|
|
30
|
-
|
|
27
|
+
That produces a `REF tbl1 \h` field, so the number follows the table. `--pattern` accepts your own regex with the named group `bookmark`; `--prefix` prepends text such as `Table `.
|
|
31
28
|
|
|
32
|
-
If
|
|
29
|
+
If a reference should read "Table 2" rather than "2", set the REF field's `\n` behaviour off and put the label in the prose: a field cannot know whether it points at a table or a figure.
|
|
33
30
|
|
|
34
|
-
## Order
|
|
31
|
+
## Order
|
|
35
32
|
|
|
36
|
-
1. Add or restructure tables and figures.
|
|
37
|
-
2. Run `captions_and_crossrefs.py
|
|
38
|
-
3. Run `insert_ref_fields.py
|
|
39
|
-
4. Re-render
|
|
33
|
+
1. Add or restructure the tables and figures.
|
|
34
|
+
2. Run `captions_and_crossrefs.py`.
|
|
35
|
+
3. Run `insert_ref_fields.py`.
|
|
36
|
+
4. Re-render.
|
|
40
37
|
|
|
41
38
|
## Verify
|
|
42
39
|
|
|
43
|
-
`fields_report.py`: one
|
|
40
|
+
Run `fields_report.py`: one SEQ per caption, one REF per reference, and no REF with an empty result. Then render. The usual defects are a caption above its table, or a caption separated from it by a page break.
|
|
44
41
|
|
|
45
42
|
## Pitfalls
|
|
46
43
|
|
|
47
|
-
-
|
|
48
|
-
- A
|
|
49
|
-
- Bookmarks must be unique
|
|
44
|
+
- Numbering changes on field update, so two captions can claim `Table 2` until Word recalculates.
|
|
45
|
+
- A hand-typed caption number never renumbers.
|
|
46
|
+
- Bookmarks must be unique: copying a caption copies its bookmark name and breaks the reference.
|
|
@@ -1,49 +1,44 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Proposed and tracked changes
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A tracked edit is markup: an insertion wraps runs in `w:ins`, a deletion wraps `w:delText` in `w:del`, a move is `w:moveFrom`/`w:moveTo` paired by `w:name`, and a formatting change is `w:rPrChange`/`w:pPrChange`. `w:trackRevisions` in `word/settings.xml` makes Word record new edits.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Propose an edit
|
|
5
|
+
## Propose
|
|
8
6
|
|
|
9
7
|
```bash
|
|
10
|
-
"<python>" "<skill>/scripts/add_tracked_replacements.py" "<file.docx>" --out "<file-tracked.docx>"
|
|
11
|
-
--author "Review" --replace "18%=21%" --replace "flat=steady"
|
|
8
|
+
"<python>" "<skill>/scripts/add_tracked_replacements.py" "<file.docx>" --out "<file-tracked.docx>" --author "Review" --replace "18%=21%" --replace "flat=steady"
|
|
12
9
|
```
|
|
13
10
|
|
|
14
|
-
Each `--replace` is `OLD=NEW
|
|
11
|
+
Each `--replace` is `OLD=NEW`. The script splits runs at the match so only the changed characters are marked, keeps the surrounding formatting, and turns on `trackRevisions`. Quote the `--author` the reviewer should see.
|
|
15
12
|
|
|
16
|
-
Limit: one replacement per pattern per paragraph
|
|
13
|
+
Limit: one replacement per pattern per paragraph. A match split across runs is still found because the split is computed on paragraph text; reword if a paragraph repeats the phrase.
|
|
17
14
|
|
|
18
|
-
## Inspect
|
|
15
|
+
## Inspect
|
|
19
16
|
|
|
20
17
|
```bash
|
|
21
18
|
"<python>" "<skill>/scripts/accept_tracked_changes.py" "<file.docx>" --mode report
|
|
22
19
|
"<python>" "<skill>/scripts/docx_kit.py" xml "<file.docx>" word/document.xml
|
|
23
20
|
```
|
|
24
21
|
|
|
25
|
-
The report counts insertions, deletions, move pairs and format changes.
|
|
22
|
+
The report counts insertions, deletions, move pairs and format changes.
|
|
26
23
|
|
|
27
24
|
## Accept or reject
|
|
28
25
|
|
|
29
26
|
```bash
|
|
30
|
-
"<python>" "<skill>/scripts/accept_tracked_changes.py" "<file.docx>" --mode accept --out "<file-accepted.docx
|
|
31
|
-
"<python>" "<skill>/scripts/accept_tracked_changes.py" "<file.docx>" --mode reject --out "<file-rejected.docx
|
|
27
|
+
"<python>" "<skill>/scripts/accept_tracked_changes.py" "<file.docx>" --mode accept --out "<file-accepted.docx"
|
|
28
|
+
"<python>" "<skill>/scripts/accept_tracked_changes.py" "<file.docx>" --mode reject --out "<file-rejected.docx"
|
|
32
29
|
```
|
|
33
30
|
|
|
34
|
-
Accept keeps inserted text and drops deleted text; reject
|
|
35
|
-
|
|
36
|
-
## Before delivery
|
|
31
|
+
Accept keeps inserted text and drops deleted text; reject reverses that and restores `w:delText` as ordinary text. Format-only changes are discarded either way. `--keep_tracking_on` leaves `trackRevisions` enabled.
|
|
37
32
|
|
|
38
|
-
|
|
33
|
+
Before delivery: rendered pages contain no markup, so a file can look finished while carrying redlines. Always run `--mode report` on a file about to be sent, then accept or reject explicitly.
|
|
39
34
|
|
|
40
35
|
## Verify
|
|
41
36
|
|
|
42
|
-
After accepting, `docx_kit.py text`
|
|
37
|
+
After accepting, `docx_kit.py text` shows the new wording and `word/document.xml` contains no `w:ins`/`w:del`. Render the affected page.
|
|
43
38
|
|
|
44
39
|
## Pitfalls
|
|
45
40
|
|
|
46
|
-
- `w:del` contains `w:delText`, not `w:t`;
|
|
47
|
-
- Tables, headers and footnotes can each carry
|
|
48
|
-
- Accepting a move pair
|
|
49
|
-
- Comments are not tracked changes
|
|
41
|
+
- `w:del` contains `w:delText`, not `w:t`; extraction that reads only `w:t` loses deletions.
|
|
42
|
+
- Tables, headers and footnotes can each carry tracked changes. The script covers every text part; a hand edit can miss one.
|
|
43
|
+
- Accepting a move pair as two unrelated edits duplicates content. The script treats them as a pair.
|
|
44
|
+
- Comments are not tracked changes; see `comments_manage.md`.
|