dsh-office-kit 0.1.0 → 0.3.0
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/CAPABILITY.md +147 -68
- package/LICENSE.md +21 -21
- package/README.md +108 -106
- package/assets/skills/excel-live-control/SKILL.md +234 -0
- package/assets/skills/excel-live-control/container_tools/live_excel.py +1589 -0
- package/assets/skills/excel-live-control/domain_guidance/financial_models.md +86 -0
- package/assets/skills/excel-live-control/domain_guidance/healthcare.md +58 -0
- package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +53 -0
- package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +53 -0
- package/assets/skills/excel-live-control/references/office-automation.md +207 -0
- package/assets/skills/excel-live-control/references/read_only_qna.md +59 -0
- package/assets/skills/excel-live-control/style_guidelines.md +87 -0
- package/assets/skills/excel-live-control/workflows/create_workflows.md +78 -0
- package/assets/skills/excel-live-control/workflows/edit_workflows.md +72 -0
- package/assets/skills/office-documents/SKILL.md +90 -77
- package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +86 -0
- package/assets/skills/office-documents/ooxml/comments.md +40 -0
- package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +66 -0
- package/assets/skills/office-documents/ooxml/rels_and_content_types.md +62 -0
- package/assets/skills/office-documents/ooxml/tracked_changes.md +53 -0
- package/assets/skills/office-documents/references/docx-structure.md +83 -0
- package/assets/skills/office-documents/references/template-elicitation.md +49 -0
- package/assets/skills/office-documents/references/writing-quality.md +49 -0
- package/assets/skills/office-documents/render_docx.py +92 -0
- package/assets/skills/office-documents/scripts/a11y_audit.py +194 -0
- package/assets/skills/office-documents/scripts/accept_tracked_changes.py +151 -0
- package/assets/skills/office-documents/scripts/add_tracked_replacements.py +148 -0
- package/assets/skills/office-documents/scripts/append_blocks.py +108 -0
- package/assets/skills/office-documents/scripts/apply_template_styles.py +104 -0
- package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +102 -0
- package/assets/skills/office-documents/scripts/comments_add.py +136 -0
- package/assets/skills/office-documents/scripts/comments_apply_patch.py +114 -0
- package/assets/skills/office-documents/scripts/comments_extract.py +85 -0
- package/assets/skills/office-documents/scripts/comments_strip.py +74 -0
- package/assets/skills/office-documents/scripts/content_controls.py +192 -0
- package/assets/skills/office-documents/scripts/core_properties.py +124 -0
- package/assets/skills/office-documents/scripts/docx_kit.py +587 -0
- package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +225 -0
- package/assets/skills/office-documents/scripts/docx_table_to_csv.py +99 -0
- package/assets/skills/office-documents/scripts/fields_materialize.py +66 -0
- package/assets/skills/office-documents/scripts/fields_report.py +59 -0
- package/assets/skills/office-documents/scripts/flatten_ref_fields.py +62 -0
- package/assets/skills/office-documents/scripts/footnotes_report.py +77 -0
- package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +96 -0
- package/assets/skills/office-documents/scripts/heading_audit.py +81 -0
- package/assets/skills/office-documents/scripts/images_audit.py +80 -0
- package/assets/skills/office-documents/scripts/insert_note.py +126 -0
- package/assets/skills/office-documents/scripts/insert_ref_fields.py +90 -0
- package/assets/skills/office-documents/scripts/insert_toc.py +84 -0
- package/assets/skills/office-documents/scripts/internal_nav.py +146 -0
- package/assets/skills/office-documents/scripts/make_fixtures.py +169 -0
- package/assets/skills/office-documents/scripts/merge_docx_append.py +239 -0
- package/assets/skills/office-documents/scripts/privacy_scrub.py +131 -0
- package/assets/skills/office-documents/scripts/redact_docx.py +113 -0
- package/assets/skills/office-documents/scripts/render_and_diff.py +101 -0
- package/assets/skills/office-documents/scripts/replace_text.py +154 -0
- package/assets/skills/office-documents/scripts/section_audit.py +91 -0
- package/assets/skills/office-documents/scripts/set_protection.py +92 -0
- package/assets/skills/office-documents/scripts/style_lint.py +111 -0
- package/assets/skills/office-documents/scripts/style_normalize.py +103 -0
- package/assets/skills/office-documents/scripts/table_geometry.py +167 -0
- package/assets/skills/office-documents/scripts/watermark_add.py +48 -0
- package/assets/skills/office-documents/scripts/watermark_audit_remove.py +67 -0
- package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +117 -0
- package/assets/skills/office-documents/tasks/accessibility_a11y.md +45 -0
- package/assets/skills/office-documents/tasks/captions_crossrefs.md +49 -0
- package/assets/skills/office-documents/tasks/clean_tracked_changes.md +49 -0
- package/assets/skills/office-documents/tasks/comments_manage.md +59 -0
- package/assets/skills/office-documents/tasks/compare_diff.md +40 -0
- package/assets/skills/office-documents/tasks/create_edit.md +107 -0
- package/assets/skills/office-documents/tasks/equations.md +59 -0
- package/assets/skills/office-documents/tasks/fields_update.md +44 -0
- package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +50 -0
- package/assets/skills/office-documents/tasks/footnotes_endnotes.md +38 -0
- package/assets/skills/office-documents/tasks/forms_content_controls.md +48 -0
- package/assets/skills/office-documents/tasks/headings_numbering.md +55 -0
- package/assets/skills/office-documents/tasks/images_figures.md +63 -0
- package/assets/skills/office-documents/tasks/multi_doc_merge.md +43 -0
- package/assets/skills/office-documents/tasks/navigation_internal_links.md +38 -0
- package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +45 -0
- package/assets/skills/office-documents/tasks/protection_restrict_editing.md +41 -0
- package/assets/skills/office-documents/tasks/read_review.md +50 -0
- package/assets/skills/office-documents/tasks/redaction_anonymization.md +42 -0
- package/assets/skills/office-documents/tasks/sections_layout.md +54 -0
- package/assets/skills/office-documents/tasks/style_lint_normalize.md +44 -0
- package/assets/skills/office-documents/tasks/tables_spreadsheets.md +64 -0
- package/assets/skills/office-documents/tasks/templates_style_packs.md +53 -0
- package/assets/skills/office-documents/tasks/toc_workflow.md +39 -0
- package/assets/skills/office-documents/tasks/verify_render.md +50 -0
- package/assets/skills/office-documents/tasks/watermarks_background.md +35 -0
- package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +47 -0
- package/assets/skills/office-documents/troubleshooting/run_splitting.md +36 -0
- package/assets/skills/office-pdf/SKILL.md +73 -56
- package/assets/skills/office-pdf/references/form-fields.md +56 -0
- package/assets/skills/office-pdf/references/geometry.md +44 -0
- package/assets/skills/office-pdf/references/layout-quality.md +41 -0
- package/assets/skills/office-pdf/references/limits.md +58 -0
- package/assets/skills/office-pdf/scripts/extract_text.py +172 -0
- package/assets/skills/office-pdf/scripts/forms.py +556 -0
- package/assets/skills/office-pdf/scripts/metadata.py +161 -0
- package/assets/skills/office-pdf/scripts/page_ops.py +198 -0
- package/assets/skills/office-pdf/scripts/redact_region.py +347 -0
- package/assets/skills/office-pdf/scripts/render_pages.py +207 -0
- package/assets/skills/office-pdf/scripts/split_pages.py +155 -0
- package/assets/skills/office-pdf/scripts/stamp_overlay.py +199 -0
- package/assets/skills/office-pdf/tasks/annotations.md +98 -0
- package/assets/skills/office-pdf/tasks/create.md +75 -0
- package/assets/skills/office-pdf/tasks/edit-pages.md +71 -0
- package/assets/skills/office-pdf/tasks/forms.md +67 -0
- package/assets/skills/office-pdf/tasks/read.md +62 -0
- package/assets/skills/office-pdf/tasks/redact.md +72 -0
- package/assets/skills/office-pdf/tasks/stamp.md +60 -0
- package/assets/skills/office-presentations/SKILL.md +141 -87
- package/assets/skills/office-presentations/container_tools/add_slides.py +136 -0
- package/assets/skills/office-presentations/container_tools/core_properties.py +120 -0
- package/assets/skills/office-presentations/container_tools/create_montage.py +126 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +285 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +157 -0
- package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +144 -0
- package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +199 -0
- package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +212 -0
- package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +345 -0
- package/assets/skills/office-presentations/container_tools/preview_render.py +375 -0
- package/assets/skills/office-presentations/container_tools/render_slides.py +139 -0
- package/assets/skills/office-presentations/container_tools/replace_text.py +168 -0
- package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +229 -0
- package/assets/skills/office-presentations/domain_guidance/finance.md +66 -0
- package/assets/skills/office-presentations/recipes/README.md +71 -0
- package/assets/skills/office-presentations/recipes/autolayout.md +88 -0
- package/assets/skills/office-presentations/recipes/connectors.md +83 -0
- package/assets/skills/office-presentations/recipes/images.md +80 -0
- package/assets/skills/office-presentations/recipes/master-and-layouts.md +127 -0
- package/assets/skills/office-presentations/recipes/native-charts.md +87 -0
- package/assets/skills/office-presentations/recipes/rich-text-runs.md +86 -0
- package/assets/skills/office-presentations/recipes/speaker-notes.md +65 -0
- package/assets/skills/office-presentations/recipes/tables.md +88 -0
- package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +71 -0
- package/assets/skills/office-presentations/recipes/theme-tokens.md +87 -0
- package/assets/skills/office-presentations/references/authoring-workflow.md +65 -0
- package/assets/skills/office-presentations/references/finalization.md +63 -0
- package/assets/skills/office-presentations/references/native-bullets.md +78 -0
- package/assets/skills/office-presentations/references/native-evidence.md +67 -0
- package/assets/skills/office-presentations/references/style-guidelines.md +66 -0
- package/assets/skills/office-presentations/references/templates.md +76 -0
- package/assets/skills/office-presentations/routing/google_slides.md +45 -0
- package/assets/skills/office-presentations/tasks/apply-annotations.md +83 -0
- package/assets/skills/office-presentations/tasks/charts-tables-notes.md +79 -0
- package/assets/skills/office-presentations/tasks/create-deck.md +106 -0
- package/assets/skills/office-presentations/tasks/edit-deck.md +93 -0
- package/assets/skills/office-presentations/tasks/inspect-and-verify.md +107 -0
- package/assets/skills/office-presentations/tasks/template-following.md +132 -0
- package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py +176 -0
- package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +154 -0
- package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +111 -0
- package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +239 -0
- package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +185 -0
- package/assets/skills/office-spreadsheets/SKILL.md +105 -101
- package/assets/skills/office-spreadsheets/container_tools/add_chart.py +236 -0
- package/assets/skills/office-spreadsheets/container_tools/add_image.py +107 -0
- package/assets/skills/office-spreadsheets/container_tools/append_rows.py +122 -0
- package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +306 -0
- package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +398 -0
- package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +275 -0
- package/assets/skills/office-spreadsheets/container_tools/set_cells.py +135 -0
- package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +134 -0
- package/assets/skills/office-spreadsheets/container_tools/style_cells.py +133 -0
- package/assets/skills/office-spreadsheets/recipes/charts.md +66 -0
- package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +69 -0
- package/assets/skills/office-spreadsheets/recipes/data-tables.md +58 -0
- package/assets/skills/office-spreadsheets/recipes/named-ranges.md +41 -0
- package/assets/skills/office-spreadsheets/recipes/number-formats.md +72 -0
- package/assets/skills/office-spreadsheets/recipes/sparklines.md +43 -0
- package/assets/skills/office-spreadsheets/references/charts.md +63 -0
- package/assets/skills/office-spreadsheets/references/domain/financial-models.md +40 -0
- package/assets/skills/office-spreadsheets/references/domain/healthcare.md +22 -0
- package/assets/skills/office-spreadsheets/references/domain/marketing.md +28 -0
- package/assets/skills/office-spreadsheets/references/domain/research.md +22 -0
- package/assets/skills/office-spreadsheets/references/formula-patterns.md +78 -0
- package/assets/skills/office-spreadsheets/references/style-guidelines.md +69 -0
- package/assets/skills/office-spreadsheets/references/workbook-structure.md +60 -0
- package/assets/skills/office-spreadsheets/routing/google-sheets.md +36 -0
- package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +37 -0
- package/assets/skills/office-spreadsheets/tasks/annotations.md +51 -0
- package/assets/skills/office-spreadsheets/tasks/create-workbook.md +66 -0
- package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +38 -0
- package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +69 -0
- package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +38 -0
- package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +57 -0
- package/assets/skills/shared/annotation-recipes.md +252 -0
- package/assets/skills/shared/check_runtime.py +253 -0
- package/assets/skills/shared/common-workflow.md +139 -0
- package/assets/skills/shared/scripts/ensure_raster_image.py +187 -0
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/index.js +239 -89
- package/package.json +146 -149
- package/postinstall.mjs +92 -82
- package/python/office_cli.py +171 -246
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Audit table widths in twips, and optionally make every table fit the page.
|
|
3
|
+
|
|
4
|
+
Word stores table widths three ways (``tblW``, grid columns, cell widths) and they
|
|
5
|
+
disagree often enough that a table overflows the margin after a page-size change.
|
|
6
|
+
``--fix`` makes the table fixed-layout and distributes the text width across the
|
|
7
|
+
grid, writing the same number to each cell so the two can no longer disagree.
|
|
8
|
+
|
|
9
|
+
Exit codes: 0 clean, 1 problems found (without --fix), 2 bad input.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from lxml import etree
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
21
|
+
import docx_kit as kit
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def text_width_twips(doc: kit.Docx) -> int:
|
|
25
|
+
sections = doc.sections()
|
|
26
|
+
if not sections:
|
|
27
|
+
return 9360
|
|
28
|
+
sect = sections[0]
|
|
29
|
+
page = sect.find(kit.q("pgSz"))
|
|
30
|
+
margins = sect.find(kit.q("pgMar"))
|
|
31
|
+
width = int(page.get(kit.q("w")) or 12240) if page is not None else 12240
|
|
32
|
+
if margins is None:
|
|
33
|
+
return width - 2880
|
|
34
|
+
return width - sum(int(margins.get(kit.q(side)) or 0) for side in ("left", "right"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def grid_widths(table) -> list[int]:
|
|
38
|
+
grid = table.find(kit.q("tblGrid"))
|
|
39
|
+
if grid is None:
|
|
40
|
+
return []
|
|
41
|
+
widths = []
|
|
42
|
+
for column in grid.findall(kit.q("gridCol")):
|
|
43
|
+
value = column.get(kit.q("w"))
|
|
44
|
+
widths.append(int(value) if value and value.lstrip("-").isdigit() else 0)
|
|
45
|
+
return widths
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def table_info(table) -> dict:
|
|
49
|
+
props = table.find(kit.q("tblPr"))
|
|
50
|
+
width = None
|
|
51
|
+
layout = None
|
|
52
|
+
if props is not None:
|
|
53
|
+
node = props.find(kit.q("tblW"))
|
|
54
|
+
if node is not None:
|
|
55
|
+
width = {"type": node.get(kit.q("type")), "value": node.get(kit.q("w"))}
|
|
56
|
+
node = props.find(kit.q("tblLayout"))
|
|
57
|
+
if node is not None:
|
|
58
|
+
layout = node.get(kit.q("type"))
|
|
59
|
+
widths = grid_widths(table)
|
|
60
|
+
cells = table.findall(".//" + kit.q("tc"))
|
|
61
|
+
cell_widths = []
|
|
62
|
+
for cell in cells:
|
|
63
|
+
node = cell.find(kit.q("tcPr") + "/" + kit.q("tcW"))
|
|
64
|
+
if node is not None and (node.get(kit.q("w")) or "").isdigit():
|
|
65
|
+
cell_widths.append(int(node.get(kit.q("w"))))
|
|
66
|
+
return {
|
|
67
|
+
"tblW": width,
|
|
68
|
+
"layout": layout,
|
|
69
|
+
"grid_twips": widths,
|
|
70
|
+
"grid_total": sum(widths),
|
|
71
|
+
"cells": len(cells),
|
|
72
|
+
"cell_width_min": min(cell_widths) if cell_widths else None,
|
|
73
|
+
"cell_width_max": max(cell_widths) if cell_widths else None,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def fix_table(table, target: int) -> None:
|
|
78
|
+
props = table.find(kit.q("tblPr"))
|
|
79
|
+
if props is None:
|
|
80
|
+
props = etree.Element(kit.q("tblPr"))
|
|
81
|
+
table.insert(0, props)
|
|
82
|
+
for tag in ("tblW", "tblLayout"):
|
|
83
|
+
for existing in props.findall(kit.q(tag)):
|
|
84
|
+
props.remove(existing)
|
|
85
|
+
width = etree.SubElement(props, kit.q("tblW"))
|
|
86
|
+
width.set(kit.q("type"), "dxa")
|
|
87
|
+
width.set(kit.q("w"), str(target))
|
|
88
|
+
etree.SubElement(props, kit.q("tblLayout")).set(kit.q("type"), "fixed")
|
|
89
|
+
|
|
90
|
+
columns = grid_widths(table)
|
|
91
|
+
if not columns:
|
|
92
|
+
columns = [1]
|
|
93
|
+
total = sum(columns) or len(columns)
|
|
94
|
+
scaled = [max(1, round(target * value / total)) for value in columns]
|
|
95
|
+
grid = table.find(kit.q("tblGrid"))
|
|
96
|
+
if grid is None:
|
|
97
|
+
grid = etree.Element(kit.q("tblGrid"))
|
|
98
|
+
table.insert(1, grid)
|
|
99
|
+
for column in list(grid):
|
|
100
|
+
grid.remove(column)
|
|
101
|
+
for value in scaled:
|
|
102
|
+
etree.SubElement(grid, kit.q("gridCol")).set(kit.q("w"), str(value))
|
|
103
|
+
|
|
104
|
+
for row in table.findall(kit.q("tr")):
|
|
105
|
+
position = 0
|
|
106
|
+
for cell in row.findall(kit.q("tc")):
|
|
107
|
+
span = 1
|
|
108
|
+
cell_props = cell.find(kit.q("tcPr"))
|
|
109
|
+
if cell_props is None:
|
|
110
|
+
cell_props = etree.Element(kit.q("tcPr"))
|
|
111
|
+
cell.insert(0, cell_props)
|
|
112
|
+
node = cell_props.find(kit.q("gridSpan"))
|
|
113
|
+
if node is not None and (node.get(kit.q("val")) or "").isdigit():
|
|
114
|
+
span = int(node.get(kit.q("val")))
|
|
115
|
+
own = sum(scaled[position : position + span]) or scaled[0]
|
|
116
|
+
for existing in cell_props.findall(kit.q("tcW")):
|
|
117
|
+
cell_props.remove(existing)
|
|
118
|
+
tcw = etree.SubElement(cell_props, kit.q("tcW"))
|
|
119
|
+
tcw.set(kit.q("type"), "dxa")
|
|
120
|
+
tcw.set(kit.q("w"), str(own))
|
|
121
|
+
position += span
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main(argv: list[str] | None = None) -> int:
|
|
125
|
+
parser = argparse.ArgumentParser(description="Audit table geometry in twips and optionally fit tables to the text width.")
|
|
126
|
+
parser.add_argument("docx", help="Input .docx")
|
|
127
|
+
parser.add_argument("--fix", action="store_true", help="Set fixed layout and rewrite grid/cell widths to fit the page")
|
|
128
|
+
parser.add_argument("--out", default=None, help="Output .docx (required with --fix, defaults to overwriting)")
|
|
129
|
+
args = parser.parse_args(argv)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
doc = kit.Docx(args.docx)
|
|
133
|
+
except (OSError, KeyError) as exc:
|
|
134
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
135
|
+
return 2
|
|
136
|
+
|
|
137
|
+
target = text_width_twips(doc)
|
|
138
|
+
tables = list(doc.body().iter(kit.q("tbl")))
|
|
139
|
+
rows = [table_info(table) for table in tables]
|
|
140
|
+
findings = []
|
|
141
|
+
for index, info in enumerate(rows, start=1):
|
|
142
|
+
if info["grid_total"] and info["grid_total"] > target:
|
|
143
|
+
findings.append({"kind": "grid_wider_than_text", "table": index, "grid_total": info["grid_total"], "text_width": target})
|
|
144
|
+
if info["layout"] != "fixed":
|
|
145
|
+
findings.append({"kind": "not_fixed_layout", "table": index})
|
|
146
|
+
if info["cell_width_max"] and info["grid_total"] and info["cell_width_max"] > info["grid_total"]:
|
|
147
|
+
findings.append({"kind": "cell_wider_than_grid", "table": index})
|
|
148
|
+
if info["tblW"] and info["tblW"]["type"] == "pct" and not info["grid_total"]:
|
|
149
|
+
findings.append({"kind": "percent_width_without_grid", "table": index})
|
|
150
|
+
|
|
151
|
+
if args.fix:
|
|
152
|
+
if not tables:
|
|
153
|
+
print("error: no tables found", file=sys.stderr)
|
|
154
|
+
return 2
|
|
155
|
+
for table in tables:
|
|
156
|
+
fix_table(table, target)
|
|
157
|
+
out = args.out or args.docx
|
|
158
|
+
doc.save(out)
|
|
159
|
+
kit.emit({"ok": True, "fixed": True, "output": str(Path(out)), "tables": len(tables), "text_width": target, "findings": findings})
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
kit.emit({"ok": not findings, "text_width": target, "tables": rows, "findings": findings})
|
|
163
|
+
return 1 if findings else 0
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Add a diagonal text watermark to the page background.
|
|
3
|
+
|
|
4
|
+
The watermark is a VML text shape in the default header, which is how Word has
|
|
5
|
+
stored them since the 2007 file format: it survives conversion, prints on every
|
|
6
|
+
page, and can be removed again by deleting the header shape.
|
|
7
|
+
|
|
8
|
+
Exit codes: 0 success, 2 bad input.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
18
|
+
import docx_kit as kit
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main(argv: list[str] | None = None) -> int:
|
|
22
|
+
parser = argparse.ArgumentParser(description="Add a VML text watermark to the document header.")
|
|
23
|
+
parser.add_argument("input_docx", help="Input .docx")
|
|
24
|
+
parser.add_argument("--out", required=True, help="Output .docx")
|
|
25
|
+
parser.add_argument("--text", required=True, help="Watermark text, e.g. CONFIDENTIAL")
|
|
26
|
+
parser.add_argument("--color", default="#C0C0C0", help="Shape fill colour (hex)")
|
|
27
|
+
args = parser.parse_args(argv)
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
doc = kit.Docx(args.input_docx)
|
|
31
|
+
except (OSError, KeyError) as exc:
|
|
32
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
33
|
+
return 2
|
|
34
|
+
|
|
35
|
+
if kit.watermark_texts(doc):
|
|
36
|
+
print("error: this document already has a watermark; remove it first", file=sys.stderr)
|
|
37
|
+
return 2
|
|
38
|
+
|
|
39
|
+
part = doc.ensure_header_footer("header")
|
|
40
|
+
root = doc.xml(part)
|
|
41
|
+
root.append(kit.watermark_paragraph(args.text, doc.next_id(), args.color))
|
|
42
|
+
doc.save(args.out)
|
|
43
|
+
kit.emit({"ok": True, "output": str(Path(args.out)), "text": args.text, "part": part})
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Find and remove VML text watermarks from headers.
|
|
3
|
+
|
|
4
|
+
A watermark that was meant for a draft can end up in a signed document, and it is
|
|
5
|
+
invisible in text extraction because it lives in a shape, not in a paragraph run.
|
|
6
|
+
|
|
7
|
+
Exit codes: 0 clean, 1 watermarks found (report mode), 2 bad input.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
17
|
+
import docx_kit as kit
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main(argv: list[str] | None = None) -> int:
|
|
21
|
+
parser = argparse.ArgumentParser(description="Report or remove watermarks stored as VML shapes in headers.")
|
|
22
|
+
parser.add_argument("input_docx", help="Input .docx")
|
|
23
|
+
parser.add_argument("--mode", choices=("report", "remove"), default="report")
|
|
24
|
+
parser.add_argument("--contains", default=None, help="Only match watermarks containing this text")
|
|
25
|
+
parser.add_argument("--out", default=None, help="Output .docx for --mode remove")
|
|
26
|
+
args = parser.parse_args(argv)
|
|
27
|
+
|
|
28
|
+
if args.mode == "remove" and not args.out:
|
|
29
|
+
print("error: --mode remove needs --out", file=sys.stderr)
|
|
30
|
+
return 2
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
doc = kit.Docx(args.input_docx)
|
|
34
|
+
except (OSError, KeyError) as exc:
|
|
35
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
36
|
+
return 2
|
|
37
|
+
|
|
38
|
+
found = kit.watermark_texts(doc)
|
|
39
|
+
if args.contains:
|
|
40
|
+
wanted = args.contains.lower()
|
|
41
|
+
found = [item for item in found if wanted in (item["text"] or "").lower()]
|
|
42
|
+
|
|
43
|
+
removed = 0
|
|
44
|
+
if args.mode == "remove" and found:
|
|
45
|
+
for item in found:
|
|
46
|
+
paragraph = item["paragraph"]
|
|
47
|
+
if paragraph is not None and paragraph.getparent() is not None:
|
|
48
|
+
paragraph.getparent().remove(paragraph)
|
|
49
|
+
removed += 1
|
|
50
|
+
doc.save(args.out)
|
|
51
|
+
elif args.mode == "remove":
|
|
52
|
+
doc.save(args.out)
|
|
53
|
+
|
|
54
|
+
kit.emit(
|
|
55
|
+
{
|
|
56
|
+
"ok": args.mode == "remove" or not found,
|
|
57
|
+
"mode": args.mode,
|
|
58
|
+
"watermarks": [{"part": item["part"], "text": item["text"]} for item in found],
|
|
59
|
+
"removed": removed,
|
|
60
|
+
"output": str(Path(args.out)) if args.out else None,
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
return 0 if (args.mode == "remove" or not found) else 1
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Copy a worksheet range into a Word table.
|
|
3
|
+
|
|
4
|
+
The header row is bolded and marked as a repeating header so it appears again
|
|
5
|
+
when the table breaks across pages, which is the detail hand-built tables usually
|
|
6
|
+
miss. This script uses python-docx directly because it creates a new document
|
|
7
|
+
rather than editing an existing one.
|
|
8
|
+
|
|
9
|
+
Exit codes: 0 success, 1 sheet or data not found, 2 bad input.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from lxml import etree
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
21
|
+
import docx_kit as kit
|
|
22
|
+
|
|
23
|
+
from docx import Document
|
|
24
|
+
from docx.enum.table import WD_TABLE_ALIGNMENT
|
|
25
|
+
from docx.shared import Pt
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def cell_text(value) -> str:
|
|
29
|
+
if value is None:
|
|
30
|
+
return ""
|
|
31
|
+
if isinstance(value, float) and value.is_integer():
|
|
32
|
+
return str(int(value))
|
|
33
|
+
return str(value).strip()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main(argv: list[str] | None = None) -> int:
|
|
37
|
+
parser = argparse.ArgumentParser(description="Build a .docx whose body is a table filled from an .xlsx sheet.")
|
|
38
|
+
parser.add_argument("xlsx", help="Input .xlsx")
|
|
39
|
+
parser.add_argument("output_docx", nargs="?", default=None, help="Output .docx")
|
|
40
|
+
parser.add_argument("--out", default=None, help="Output .docx (alternative to the positional argument)")
|
|
41
|
+
parser.add_argument("--sheet", default=None, help="Sheet name (default: the active sheet)")
|
|
42
|
+
parser.add_argument("--header_rows", type=int, default=1, help="Rows to treat as headers (default 1)")
|
|
43
|
+
parser.add_argument("--title", default=None, help="Heading added above the table")
|
|
44
|
+
args = parser.parse_args(argv)
|
|
45
|
+
|
|
46
|
+
out = args.out or args.output_docx
|
|
47
|
+
if not out:
|
|
48
|
+
parser.error("provide an output path (positional or --out)")
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
from openpyxl import load_workbook
|
|
52
|
+
except ImportError:
|
|
53
|
+
print("error: openpyxl is required to read .xlsx", file=sys.stderr)
|
|
54
|
+
return 2
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
workbook = load_workbook(args.xlsx, data_only=True, read_only=True)
|
|
58
|
+
except OSError as exc:
|
|
59
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
60
|
+
return 2
|
|
61
|
+
|
|
62
|
+
if args.sheet:
|
|
63
|
+
if args.sheet not in workbook.sheetnames:
|
|
64
|
+
print("error: sheet %r not found (have %s)" % (args.sheet, ", ".join(workbook.sheetnames)), file=sys.stderr)
|
|
65
|
+
return 1
|
|
66
|
+
sheet = workbook[args.sheet]
|
|
67
|
+
else:
|
|
68
|
+
sheet = workbook[workbook.sheetnames[0]]
|
|
69
|
+
|
|
70
|
+
rows = [[cell_text(value) for value in row] for row in sheet.iter_rows(values_only=True)]
|
|
71
|
+
rows = [row for row in rows if any(cell for cell in row)]
|
|
72
|
+
if not rows:
|
|
73
|
+
print("error: sheet %r has no data" % sheet.title, file=sys.stderr)
|
|
74
|
+
return 1
|
|
75
|
+
|
|
76
|
+
width = max(len(row) for row in rows)
|
|
77
|
+
rows = [row + [""] * (width - len(row)) for row in rows]
|
|
78
|
+
header_count = max(0, min(args.header_rows, len(rows)))
|
|
79
|
+
|
|
80
|
+
document = Document()
|
|
81
|
+
if args.title:
|
|
82
|
+
document.add_heading(args.title, level=1)
|
|
83
|
+
|
|
84
|
+
table = document.add_table(rows=len(rows), cols=width)
|
|
85
|
+
table.style = "Table Grid"
|
|
86
|
+
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
|
87
|
+
for row_index, row in enumerate(rows):
|
|
88
|
+
for column_index, value in enumerate(row):
|
|
89
|
+
cell = table.cell(row_index, column_index)
|
|
90
|
+
cell.text = value
|
|
91
|
+
if row_index < header_count:
|
|
92
|
+
for paragraph in cell.paragraphs:
|
|
93
|
+
for run in paragraph.runs:
|
|
94
|
+
run.bold = True
|
|
95
|
+
run.font.size = Pt(11)
|
|
96
|
+
|
|
97
|
+
document.save(out)
|
|
98
|
+
payload = {"ok": True, "output": str(Path(out)), "sheet": sheet.title, "rows": len(rows), "columns": width, "header_rows": header_count}
|
|
99
|
+
|
|
100
|
+
if header_count:
|
|
101
|
+
doc = kit.Docx(out)
|
|
102
|
+
first_row = doc.body().find(".//" + kit.q("tr"))
|
|
103
|
+
if first_row is not None:
|
|
104
|
+
props = first_row.find(kit.q("trPr"))
|
|
105
|
+
if props is None:
|
|
106
|
+
props = etree.Element(kit.q("trPr"))
|
|
107
|
+
first_row.insert(0, props)
|
|
108
|
+
etree.SubElement(props, kit.q("tblHeader"))
|
|
109
|
+
doc.save(out)
|
|
110
|
+
payload["header_row_repeats"] = True
|
|
111
|
+
|
|
112
|
+
kit.emit(payload)
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Accessibility
|
|
2
|
+
|
|
3
|
+
**Goal.** A document a screen reader can navigate: real headings, described images, tables with a header row, no fake emphasis carrying meaning.
|
|
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`.
|
|
6
|
+
|
|
7
|
+
## Audit
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/a11y_audit.py" "<file.docx>" --out_json "<report.json>"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Checks: images without a description or title, tables without a repeating header row, heading levels that jump, empty headings, links whose text is only "click here" / "here" / "link", and a missing core title.
|
|
14
|
+
|
|
15
|
+
Exit code 1 means at least one finding; `--out_json` writes the same report for a larger pass.
|
|
16
|
+
|
|
17
|
+
## Fix the mechanical parts
|
|
18
|
+
|
|
19
|
+
```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>"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Both fixes are honest placeholders: the file name as alt text is better than an empty description, and a header row that repeats across pages helps a reader who lands mid-table. Neither substitutes for real text.
|
|
26
|
+
|
|
27
|
+
## Fix the parts that need judgement
|
|
28
|
+
|
|
29
|
+
- **Alt text.** Describe what the reader would lose without the picture: "Bar chart: revenue by region, EMEA 4.1M ahead of APAC 3.2M." Decorative rules and dividers get an empty description.
|
|
30
|
+
- **Link text.** "Download the Q2 ledger" instead of "click here".
|
|
31
|
+
- **Reading order.** Decorative text boxes and floating images insert themselves at awkward places in the reading order; convert to inline if the flow matters.
|
|
32
|
+
- **Colour.** Meaning carried only by colour (red = bad) fails for a colour-blind reader; add a word.
|
|
33
|
+
- **Tables for layout.** A layout table announces as a data table. Restructure into paragraphs if the content is not tabular.
|
|
34
|
+
|
|
35
|
+
Long documents also want a TOC (`tasks/toc_workflow.md`), which is the screen-reader equivalent of the navigation pane, and language tags so a screen reader pronounces correctly.
|
|
36
|
+
|
|
37
|
+
## Verify
|
|
38
|
+
|
|
39
|
+
Re-run the audit to zero findings, then render and read the page: heading hierarchy visible, alt text is not visible (it is metadata), table header row distinct. If a screen reader is available, open the document and navigate by heading — that is the true test.
|
|
40
|
+
|
|
41
|
+
## Pitfalls
|
|
42
|
+
|
|
43
|
+
- Alt text on a decorative image is worse than none; an empty description plus `title` marks it decorative.
|
|
44
|
+
- A table header row is `w:tblHeader` on the row, not a bold first row.
|
|
45
|
+
- Images inside a table cell still need descriptions.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Captions and cross-references
|
|
2
|
+
|
|
3
|
+
**Goal.** Number figures and tables automatically and refer to them from the prose without typing a number that will drift.
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Add captions
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/captions_and_crossrefs.py" "<file.docx>" "<file-captioned.docx>" \
|
|
11
|
+
--tables --figures --bookmarks --caption_text "Source: finance ledger"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- `--tables` adds `Table N` under each table, `--figures` adds `Figure N` under each picture. With neither flag, both run.
|
|
15
|
+
- Captions use the `Caption` style and sit inside the same visual block as the object, so a page break cannot orphan them.
|
|
16
|
+
- `--bookmarks` names the captions `tbl1`, `tbl2`, `fig1`, … so cross-references have targets.
|
|
17
|
+
- `--materialize` writes literal numbers instead of `SEQ` fields; use it for a frozen PDF export, not for a document that will change.
|
|
18
|
+
- `--caption_text` appends a fixed note after the number.
|
|
19
|
+
|
|
20
|
+
The bookmarks the script creates (`tblN`, `figN`) are what `insert_ref_fields.py` expects by default.
|
|
21
|
+
|
|
22
|
+
## Add cross-references
|
|
23
|
+
|
|
24
|
+
Write placeholders in your content, then convert them:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
"<python>" "<skill>/scripts/insert_ref_fields.py" "<file.docx>" "<file-refs.docx>"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`[[REF:tbl1]]` becomes a `REF tbl1 \h` field, which renumbers when the table moves. `--pattern` accepts your own regular expression as long as it has a named group `bookmark`; `--prefix` prepends text such as `Table ` to each result.
|
|
31
|
+
|
|
32
|
+
If the reference should read "Table 2" rather than just "2", set the `REF` field's `\n` behaviour off and include the label in the prose — a field cannot know whether its target is a table or a figure.
|
|
33
|
+
|
|
34
|
+
## Order of operations
|
|
35
|
+
|
|
36
|
+
1. Add or restructure tables and figures.
|
|
37
|
+
2. Run `captions_and_crossrefs.py` (creates captions and bookmarks).
|
|
38
|
+
3. Run `insert_ref_fields.py` (turns placeholders into fields).
|
|
39
|
+
4. Re-render: caption positions and numbering are a layout decision.
|
|
40
|
+
|
|
41
|
+
## Verify
|
|
42
|
+
|
|
43
|
+
`fields_report.py`: one `SEQ` per caption, one `REF` per reference, no `REF` left with an empty result. Then read the rendered page — a caption above a table instead of below, or a caption separated from its object by a page break, is the usual defect.
|
|
44
|
+
|
|
45
|
+
## Pitfalls
|
|
46
|
+
|
|
47
|
+
- Renumbering happens on field update. Two captions claim `Table 2` until Word recalculates.
|
|
48
|
+
- A caption added by hand with the number typed in will not renumber; use the script or a `SEQ` field.
|
|
49
|
+
- Bookmarks must be unique; copying a caption copies its bookmark name and breaks the reference.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Tracked changes
|
|
2
|
+
|
|
3
|
+
**Goal.** Propose edits as tracked changes so a reviewer can accept or reject them, or clean up a file that arrives full of markup.
|
|
4
|
+
|
|
5
|
+
A tracked edit is markup, not text: an insertion is `w:ins` wrapping runs, a deletion is `w:del` wrapping `w:delText`, and formatting changes are `w:rPrChange` / `w:pPrChange`. The setting `w:trackRevisions` in `word/settings.xml` makes Word record new edits the same way.
|
|
6
|
+
|
|
7
|
+
## Propose an edit
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/add_tracked_replacements.py" "<file.docx>" --out "<file-tracked.docx>" \
|
|
11
|
+
--author "Review" --replace "18%=21%" --replace "flat=steady"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
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
|
+
|
|
16
|
+
Limit: one replacement per pattern per paragraph, and a match split across runs is still found because the split is computed on paragraph text. Reword the pattern if the paragraph repeats the phrase.
|
|
17
|
+
|
|
18
|
+
## Inspect what is proposed
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
"<python>" "<skill>/scripts/accept_tracked_changes.py" "<file.docx>" --mode report
|
|
22
|
+
"<python>" "<skill>/scripts/docx_kit.py" xml "<file.docx>" word/document.xml
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The report counts insertions, deletions, move pairs and format changes. Use `docx_kit.py xml` when you need to know exactly which words change.
|
|
26
|
+
|
|
27
|
+
## Accept or reject
|
|
28
|
+
|
|
29
|
+
```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>"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Accept keeps inserted text and drops deleted text; reject does the reverse and restores `w:delText` as ordinary text. Format-only changes are discarded either way. `--keep_tracking_on` leaves `trackRevisions` enabled, so the next person's edits are also recorded.
|
|
35
|
+
|
|
36
|
+
## Before delivery
|
|
37
|
+
|
|
38
|
+
Rendered pages contain no markup, so a document can look finished while still carrying redlines. Always check `--mode report` on a file you are about to send, and accept or reject explicitly.
|
|
39
|
+
|
|
40
|
+
## Verify
|
|
41
|
+
|
|
42
|
+
After accepting, `docx_kit.py text` should show the new wording and no `w:ins`/`w:del` should remain in `word/document.xml`. Render and read the affected page.
|
|
43
|
+
|
|
44
|
+
## Pitfalls
|
|
45
|
+
|
|
46
|
+
- `w:del` contains `w:delText`, not `w:t`; a text extraction that only looks at `w:t` silently loses deletions.
|
|
47
|
+
- Tables, headers and footnotes can each carry their own tracked changes — the script covers every text part, but a hand edit in one part can miss another.
|
|
48
|
+
- Accepting a move pair (`w:moveFrom`/`w:moveTo`) as two unrelated edits duplicates content; the script treats them as a pair.
|
|
49
|
+
- Comments are not tracked changes. See `comments_manage.md`.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Comments
|
|
2
|
+
|
|
3
|
+
**Goal.** Add a review comment, read the comments someone else left, reply or resolve them, and remove comments before the document goes out.
|
|
4
|
+
|
|
5
|
+
Comments are not text: each one is a `w:comment` in `word/comments.xml`, anchored in the body by a `w:commentRangeStart` / `w:commentRangeEnd` pair plus a `w:commentReference` run. Editing the anchor without the comments part (or the reverse) leaves a document Word reports as corrupt.
|
|
6
|
+
|
|
7
|
+
## Add
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/comments_add.py" "<file.docx>" --out "<file-commented.docx>" \
|
|
11
|
+
--author "Review" --add "Renewals=Check the source of this claim" --add "42 new accounts=Who verified this?"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Each `--add` is `NEEDLE=COMMENT`. The needle is matched inside a paragraph; the anchored range covers exactly the matched text, so pick a distinctive phrase. `--ignore_case` relaxes matching, `--require_all` writes nothing and exits 1 if any needle is missing — use it when the comment set must be complete.
|
|
15
|
+
|
|
16
|
+
## Extract
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
"<python>" "<skill>/scripts/comments_extract.py" "<file.docx>" --out "<comments.json>"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Returns `id`, `author`, `initials`, `date`, `text`, `anchored_text`, `part` and `resolved`. An empty array means the file has no comments — do not assume comments exist because the user mentioned them.
|
|
23
|
+
|
|
24
|
+
## Patch or resolve
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
"<python>" "<skill>/scripts/comments_apply_patch.py" "<file.docx>" "<patch.json>" --out "<file-patched.docx>"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"ops": [
|
|
33
|
+
{ "id": 1, "append": "Confirmed against the ledger." },
|
|
34
|
+
{ "id": 2, "replace": "Resolved: revenue restated in Q3." },
|
|
35
|
+
{ "id": 1, "resolved": true }
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`replace` rewrites the comment's first paragraph, `append` adds a paragraph, `resolved` sets the `w15:done` flag. The command exits 1 and names the ids it could not find.
|
|
41
|
+
|
|
42
|
+
## Remove
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
"<python>" "<skill>/scripts/comments_strip.py" "<file.docx>" --out "<file-clean.docx>"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Removes every anchor, drops `word/comments.xml`, its relationship and content-type override. Removing comments is required before sharing a file with a customer; check `privacy_scrub_metadata.md` too.
|
|
49
|
+
|
|
50
|
+
## Verify
|
|
51
|
+
|
|
52
|
+
`comments_extract.py` on the output (count and anchors), then render the page the comment sat on — a stripped anchor that leaves an empty run can show as a stray space.
|
|
53
|
+
|
|
54
|
+
## Pitfalls
|
|
55
|
+
|
|
56
|
+
- A comment anchored to a paragraph mark rather than text shows no `anchored_text`; that is normal for "comment on the whole paragraph".
|
|
57
|
+
- Two comments cannot share an id. `comments_add.py` continues from the highest existing id.
|
|
58
|
+
- The comment body is ordinary paragraphs, so a long comment can contain several; `comments_extract.py` joins them with newlines.
|
|
59
|
+
- Replying in-thread needs Word's `commentsExtended` part. Anchoring a new comment on the same phrase is the portable equivalent.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Compare two versions
|
|
2
|
+
|
|
3
|
+
**Goal.** Say what actually changed between two files, with evidence — not "the text differs somewhere".
|
|
4
|
+
|
|
5
|
+
Two useful comparisons: text-level (what words changed) and page-level (what the reader sees changed). Use both; a change in a style definition can leave the text identical and move every paragraph.
|
|
6
|
+
|
|
7
|
+
## Page-level diff
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"<python>" "<skill>/scripts/render_and_diff.py" "<before.docx>" "<after.docx>" --outdir "<render-dir>" --dpi 120
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Renders both through the helper CLI, pairs the pages, and writes `diff-NN.png` coloured by pixel difference. Reports which pages changed, the fraction of pixels differing, and a page-count delta.
|
|
14
|
+
|
|
15
|
+
The exit code is 3 when no renderer is available: render both files another way, or compare text only and say which comparison you could not run.
|
|
16
|
+
|
|
17
|
+
## Text-level diff
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
"<python>" "<skill>/scripts/docx_kit.py" text '<before.docx>' > before.json
|
|
21
|
+
"<python>" "<skill>/scripts/docx_kit.py" text '<after.docx>' > after.json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Diff the two paragraph dumps (any diff tool, or a few lines of Python). This is the reliable way to separate content changes from layout changes.
|
|
25
|
+
|
|
26
|
+
## Review mode versus diff mode
|
|
27
|
+
|
|
28
|
+
If the user wants the *document* to carry the changes, use tracked changes (`tasks/clean_tracked_changes.md`) rather than a report: the reviewer accepts or rejects inside Word. A diff is evidence for a conversation; tracked changes are the artefact.
|
|
29
|
+
|
|
30
|
+
For a version that arrived with markup, `accept_tracked_changes.py --mode report` is the change list, with reviewer names and where each edit sits.
|
|
31
|
+
|
|
32
|
+
## Verify
|
|
33
|
+
|
|
34
|
+
Open the diff PNGs with `read_image` and look at them: a coloured block on a page that should not have changed usually means a font substitution or a margin change, not a content change. Cross-check with the text diff before writing a conclusion.
|
|
35
|
+
|
|
36
|
+
## Pitfalls
|
|
37
|
+
|
|
38
|
+
- Different renderers paginate differently; compare renders made by the same renderer and the same settings.
|
|
39
|
+
- Metadata and revision ids differ between saves and never appear on a page — a text diff of `word/document.xml` is noisy for that reason.
|
|
40
|
+
- An image replaced in place keeps the same relationship and page position; only a pixel comparison notices.
|