dsh-office-kit 0.2.0 → 0.3.1
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/README.md +3 -1
- 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 +99 -0
- package/assets/skills/office-documents/scripts/a11y_audit.py +201 -0
- package/assets/skills/office-documents/scripts/accept_tracked_changes.py +158 -0
- package/assets/skills/office-documents/scripts/add_tracked_replacements.py +155 -0
- package/assets/skills/office-documents/scripts/append_blocks.py +115 -0
- package/assets/skills/office-documents/scripts/apply_template_styles.py +111 -0
- package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +109 -0
- package/assets/skills/office-documents/scripts/comments_add.py +143 -0
- package/assets/skills/office-documents/scripts/comments_apply_patch.py +121 -0
- package/assets/skills/office-documents/scripts/comments_extract.py +92 -0
- package/assets/skills/office-documents/scripts/comments_strip.py +81 -0
- package/assets/skills/office-documents/scripts/content_controls.py +199 -0
- package/assets/skills/office-documents/scripts/core_properties.py +131 -0
- package/assets/skills/office-documents/scripts/docx_kit.py +594 -0
- package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +232 -0
- package/assets/skills/office-documents/scripts/docx_table_to_csv.py +106 -0
- package/assets/skills/office-documents/scripts/fields_materialize.py +73 -0
- package/assets/skills/office-documents/scripts/fields_report.py +66 -0
- package/assets/skills/office-documents/scripts/flatten_ref_fields.py +69 -0
- package/assets/skills/office-documents/scripts/footnotes_report.py +84 -0
- package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +103 -0
- package/assets/skills/office-documents/scripts/heading_audit.py +88 -0
- package/assets/skills/office-documents/scripts/images_audit.py +87 -0
- package/assets/skills/office-documents/scripts/insert_note.py +133 -0
- package/assets/skills/office-documents/scripts/insert_ref_fields.py +97 -0
- package/assets/skills/office-documents/scripts/insert_toc.py +91 -0
- package/assets/skills/office-documents/scripts/internal_nav.py +153 -0
- package/assets/skills/office-documents/scripts/make_fixtures.py +176 -0
- package/assets/skills/office-documents/scripts/merge_docx_append.py +246 -0
- package/assets/skills/office-documents/scripts/privacy_scrub.py +138 -0
- package/assets/skills/office-documents/scripts/redact_docx.py +120 -0
- package/assets/skills/office-documents/scripts/render_and_diff.py +108 -0
- package/assets/skills/office-documents/scripts/replace_text.py +161 -0
- package/assets/skills/office-documents/scripts/section_audit.py +98 -0
- package/assets/skills/office-documents/scripts/set_protection.py +99 -0
- package/assets/skills/office-documents/scripts/style_lint.py +118 -0
- package/assets/skills/office-documents/scripts/style_normalize.py +110 -0
- package/assets/skills/office-documents/scripts/table_geometry.py +174 -0
- package/assets/skills/office-documents/scripts/watermark_add.py +55 -0
- package/assets/skills/office-documents/scripts/watermark_audit_remove.py +74 -0
- package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +124 -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 +143 -0
- package/assets/skills/office-presentations/container_tools/core_properties.py +127 -0
- package/assets/skills/office-presentations/container_tools/create_montage.py +133 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +292 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +164 -0
- package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +151 -0
- package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +206 -0
- package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +219 -0
- package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +352 -0
- package/assets/skills/office-presentations/container_tools/preview_render.py +382 -0
- package/assets/skills/office-presentations/container_tools/render_slides.py +146 -0
- package/assets/skills/office-presentations/container_tools/replace_text.py +175 -0
- package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +236 -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 +183 -0
- package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +161 -0
- package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +118 -0
- package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +246 -0
- package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +192 -0
- package/assets/skills/office-spreadsheets/SKILL.md +105 -101
- package/assets/skills/office-spreadsheets/container_tools/add_chart.py +243 -0
- package/assets/skills/office-spreadsheets/container_tools/add_image.py +114 -0
- package/assets/skills/office-spreadsheets/container_tools/append_rows.py +129 -0
- package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +313 -0
- package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +405 -0
- package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +287 -0
- package/assets/skills/office-spreadsheets/container_tools/set_cells.py +142 -0
- package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +141 -0
- package/assets/skills/office-spreadsheets/container_tools/style_cells.py +140 -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 +260 -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 +246 -89
- package/package.json +1 -1
- package/postinstall.mjs +92 -82
- package/python/office_cli.py +213 -254
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Report, accept, or reject tracked revisions in a .docx.
|
|
3
|
+
|
|
4
|
+
Revision marks live inline in the WordprocessingML: ``w:ins`` wraps content that
|
|
5
|
+
was added, ``w:del`` wraps content that was removed, and paragraph-mark changes
|
|
6
|
+
sit in ``w:rPrChange``/``w:pPrChange``. Accepting keeps inserted content and
|
|
7
|
+
drops deleted content; rejecting does the reverse. Format-only revisions are
|
|
8
|
+
reported but always dropped because they carry no text.
|
|
9
|
+
|
|
10
|
+
Exit codes: 0 success, 1 nothing to report while --mode report, 2 bad input.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
20
|
+
import docx_kit as kit
|
|
21
|
+
|
|
22
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
23
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
24
|
+
try:
|
|
25
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
26
|
+
except (AttributeError, ValueError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
REVISION_TAGS = {
|
|
30
|
+
"ins": "insertion",
|
|
31
|
+
"del": "deletion",
|
|
32
|
+
"moveFrom": "move_from",
|
|
33
|
+
"moveTo": "move_to",
|
|
34
|
+
"rPrChange": "format_change",
|
|
35
|
+
"pPrChange": "format_change",
|
|
36
|
+
"sectPrChange": "format_change",
|
|
37
|
+
"tblPrChange": "format_change",
|
|
38
|
+
"trPrChange": "format_change",
|
|
39
|
+
"tcPrChange": "format_change",
|
|
40
|
+
}
|
|
41
|
+
REMOVABLE_TEXT_TAGS = ("ins", "del", "moveFrom", "moveTo")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def scan(doc: kit.Docx) -> list[dict]:
|
|
45
|
+
rows = []
|
|
46
|
+
for part in doc.text_parts():
|
|
47
|
+
for node in doc.xml(part).iter():
|
|
48
|
+
if not isinstance(node.tag, str) or not node.tag.startswith("{%s}" % kit.W):
|
|
49
|
+
continue
|
|
50
|
+
local = node.tag.split("}", 1)[1]
|
|
51
|
+
if local not in REVISION_TAGS:
|
|
52
|
+
continue
|
|
53
|
+
rows.append(
|
|
54
|
+
{
|
|
55
|
+
"part": part,
|
|
56
|
+
"type": REVISION_TAGS[local],
|
|
57
|
+
"author": node.get(kit.q("author")) or "",
|
|
58
|
+
"date": node.get(kit.q("date")) or "",
|
|
59
|
+
"id": node.get(kit.q("id")) or "",
|
|
60
|
+
"text": kit.text_of(node)[:120],
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
return rows
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def unwrap(node) -> None:
|
|
67
|
+
parent = node.getparent()
|
|
68
|
+
if parent is None:
|
|
69
|
+
return
|
|
70
|
+
index = parent.index(node)
|
|
71
|
+
for child in list(node):
|
|
72
|
+
parent.insert(index, child)
|
|
73
|
+
index += 1
|
|
74
|
+
parent.remove(node)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def convert_deleted_text(node) -> None:
|
|
78
|
+
for del_text in list(node.iter(kit.q("delText"))):
|
|
79
|
+
del_text.tag = kit.q("t")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def apply(doc: kit.Docx, mode: str) -> int:
|
|
83
|
+
"""Apply accept or reject across every text-bearing part."""
|
|
84
|
+
touched = 0
|
|
85
|
+
for part in doc.text_parts():
|
|
86
|
+
root = doc.xml(part)
|
|
87
|
+
nodes = [node for node in root.iter() if isinstance(node.tag, str) and node.tag.startswith("{%s}" % kit.W) and node.tag.split("}", 1)[1] in REMOVABLE_TEXT_TAGS]
|
|
88
|
+
for node in reversed(nodes):
|
|
89
|
+
local = node.tag.split("}", 1)[1]
|
|
90
|
+
keep = (local in ("ins", "moveTo")) == (mode == "accept")
|
|
91
|
+
if keep:
|
|
92
|
+
if local == "del" or local == "moveFrom":
|
|
93
|
+
convert_deleted_text(node)
|
|
94
|
+
unwrap(node)
|
|
95
|
+
else:
|
|
96
|
+
parent = node.getparent()
|
|
97
|
+
if parent is not None:
|
|
98
|
+
parent.remove(node)
|
|
99
|
+
touched += 1
|
|
100
|
+
# Paragraph-mark revisions have no text: drop them either way so a
|
|
101
|
+
# reopened document no longer claims to be dirty.
|
|
102
|
+
for local in ("rPrChange", "pPrChange", "sectPrChange", "tblPrChange", "trPrChange", "tcPrChange"):
|
|
103
|
+
for node in list(root.iter(kit.q(local))):
|
|
104
|
+
parent = node.getparent()
|
|
105
|
+
if parent is not None:
|
|
106
|
+
parent.remove(node)
|
|
107
|
+
touched += 1
|
|
108
|
+
return touched
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def main(argv: list[str] | None = None) -> int:
|
|
112
|
+
parser = argparse.ArgumentParser(description="Report, accept, or reject tracked changes in a .docx.")
|
|
113
|
+
parser.add_argument("in_docx", help="Input .docx")
|
|
114
|
+
parser.add_argument("--mode", required=True, choices=["report", "accept", "reject"])
|
|
115
|
+
parser.add_argument("--out", help="Output .docx (required for accept/reject)")
|
|
116
|
+
parser.add_argument("--keep_tracking_on", action="store_true", help="Leave the track-revisions flag enabled in settings.xml")
|
|
117
|
+
args = parser.parse_args(argv)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
doc = kit.Docx(args.in_docx)
|
|
121
|
+
except (OSError, KeyError) as exc:
|
|
122
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
123
|
+
return 2
|
|
124
|
+
|
|
125
|
+
revisions = scan(doc)
|
|
126
|
+
counts: dict[str, int] = {}
|
|
127
|
+
for row in revisions:
|
|
128
|
+
counts[row["type"]] = counts.get(row["type"], 0) + 1
|
|
129
|
+
|
|
130
|
+
if args.mode == "report":
|
|
131
|
+
kit.emit({"ok": True, "input": str(Path(args.in_docx)), "total": len(revisions), "counts": counts, "revisions": revisions})
|
|
132
|
+
return 0 if revisions else 1
|
|
133
|
+
|
|
134
|
+
if not args.out:
|
|
135
|
+
parser.error("--out is required for accept/reject")
|
|
136
|
+
touched = apply(doc, args.mode)
|
|
137
|
+
|
|
138
|
+
settings = doc.xml("word/settings.xml")
|
|
139
|
+
if not args.keep_tracking_on:
|
|
140
|
+
for node in list(settings.iter(kit.q("trackRevisions"))):
|
|
141
|
+
settings.remove(node)
|
|
142
|
+
|
|
143
|
+
doc.save(args.out)
|
|
144
|
+
kit.emit(
|
|
145
|
+
{
|
|
146
|
+
"ok": True,
|
|
147
|
+
"mode": args.mode,
|
|
148
|
+
"output": str(Path(args.out)),
|
|
149
|
+
"revisions_seen": len(revisions),
|
|
150
|
+
"nodes_rewritten": touched,
|
|
151
|
+
"counts": counts,
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Turn find/replace edits into tracked revisions (delete + insert).
|
|
3
|
+
|
|
4
|
+
Each ``--replace OLD=NEW`` finds the literal text inside a paragraph and
|
|
5
|
+
rewrites just the runs it covers: the old characters become a ``w:del`` and the
|
|
6
|
+
new characters a ``w:ins``, both attributed to ``--author``. Word then shows
|
|
7
|
+
the edit as a suggestion instead of an opaque rewrite. Tracking is switched on
|
|
8
|
+
in settings.xml so follow-up edits are recorded too.
|
|
9
|
+
|
|
10
|
+
Exit codes: 0 success (replacements may still be zero), 1 no replacement matched,
|
|
11
|
+
2 bad input or malformed --replace.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import datetime
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from lxml import etree
|
|
22
|
+
|
|
23
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
24
|
+
import docx_kit as kit
|
|
25
|
+
|
|
26
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
27
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
28
|
+
try:
|
|
29
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
30
|
+
except (AttributeError, ValueError):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def split_pair(raw: str) -> tuple[str, str]:
|
|
35
|
+
if "=" not in raw:
|
|
36
|
+
raise argparse.ArgumentTypeError("--replace expects OLD=NEW, got %r" % raw)
|
|
37
|
+
old, new = raw.split("=", 1)
|
|
38
|
+
if not old:
|
|
39
|
+
raise argparse.ArgumentTypeError("--replace needs a non-empty OLD value")
|
|
40
|
+
return old, new
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run_text(run) -> str:
|
|
44
|
+
return kit.text_of(run)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def tracked_replace(paragraph, old: str, new: str, author: str, stamp: str, counter: list[int]) -> bool:
|
|
48
|
+
full = kit.paragraph_text(paragraph)
|
|
49
|
+
start = full.find(old)
|
|
50
|
+
if start < 0:
|
|
51
|
+
return False
|
|
52
|
+
end = start + len(old)
|
|
53
|
+
runs = [child for child in paragraph if child.tag == kit.q("r")]
|
|
54
|
+
if not runs:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
out: list = []
|
|
58
|
+
deleted: list = []
|
|
59
|
+
insert_at: int | None = None
|
|
60
|
+
cursor = 0
|
|
61
|
+
for run in runs:
|
|
62
|
+
text = run_text(run)
|
|
63
|
+
rpr = run.find(kit.q("rPr"))
|
|
64
|
+
left_end = start
|
|
65
|
+
right_start = end
|
|
66
|
+
run_start, run_end = cursor, cursor + len(text)
|
|
67
|
+
cursor = run_end
|
|
68
|
+
if run_end <= left_end or run_start >= right_start:
|
|
69
|
+
out.append(run)
|
|
70
|
+
continue
|
|
71
|
+
left = text[: max(0, left_end - run_start)]
|
|
72
|
+
middle = text[max(0, left_end - run_start) : min(len(text), right_start - run_start)]
|
|
73
|
+
right = text[min(len(text), right_start - run_start) :]
|
|
74
|
+
if left:
|
|
75
|
+
out.append(kit.make_run(left, rpr))
|
|
76
|
+
if middle:
|
|
77
|
+
if insert_at is None:
|
|
78
|
+
insert_at = len(out)
|
|
79
|
+
deleted.append(kit.make_run(middle, rpr, tag="delText"))
|
|
80
|
+
if right:
|
|
81
|
+
out.append(kit.make_run(right, rpr))
|
|
82
|
+
|
|
83
|
+
if not deleted:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
ident = counter[0]
|
|
87
|
+
counter[0] += 2
|
|
88
|
+
deletion = etree.Element(kit.q("del"))
|
|
89
|
+
deletion.set(kit.q("id"), str(ident))
|
|
90
|
+
deletion.set(kit.q("author"), author)
|
|
91
|
+
deletion.set(kit.q("date"), stamp)
|
|
92
|
+
for node in deleted:
|
|
93
|
+
deletion.append(node)
|
|
94
|
+
insertion = etree.Element(kit.q("ins"))
|
|
95
|
+
insertion.set(kit.q("id"), str(ident + 1))
|
|
96
|
+
insertion.set(kit.q("author"), author)
|
|
97
|
+
insertion.set(kit.q("date"), stamp)
|
|
98
|
+
insertion.append(kit.make_run(new))
|
|
99
|
+
|
|
100
|
+
position = insert_at if insert_at is not None else len(out)
|
|
101
|
+
out.insert(position, deletion)
|
|
102
|
+
out.insert(position + 1, insertion)
|
|
103
|
+
|
|
104
|
+
ppr = paragraph.find(kit.q("pPr"))
|
|
105
|
+
for child in list(paragraph):
|
|
106
|
+
if child is not ppr:
|
|
107
|
+
paragraph.remove(child)
|
|
108
|
+
for child in out:
|
|
109
|
+
paragraph.append(child)
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main(argv: list[str] | None = None) -> int:
|
|
114
|
+
parser = argparse.ArgumentParser(description="Record find/replace edits as tracked deletions and insertions.")
|
|
115
|
+
parser.add_argument("in_docx", help="Input .docx")
|
|
116
|
+
parser.add_argument("--out", required=True, help="Output .docx")
|
|
117
|
+
parser.add_argument("--author", default="dsh-office", help="Revision author shown in Word")
|
|
118
|
+
parser.add_argument("--replace", action="append", default=[], help="Replacement formatted as OLD=NEW (repeatable)")
|
|
119
|
+
parser.add_argument("--date", default=None, help="ISO timestamp for the revisions (default: now, UTC)")
|
|
120
|
+
args = parser.parse_args(argv)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
doc = kit.Docx(args.in_docx)
|
|
124
|
+
except (OSError, KeyError) as exc:
|
|
125
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
126
|
+
return 2
|
|
127
|
+
if not args.replace:
|
|
128
|
+
parser.error("at least one --replace OLD=NEW is required")
|
|
129
|
+
|
|
130
|
+
stamp = args.date or datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
131
|
+
counter = [doc.next_id()]
|
|
132
|
+
applied = []
|
|
133
|
+
for raw in args.replace:
|
|
134
|
+
try:
|
|
135
|
+
old, new = split_pair(raw)
|
|
136
|
+
except argparse.ArgumentTypeError as exc:
|
|
137
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
138
|
+
return 2
|
|
139
|
+
hits = 0
|
|
140
|
+
for paragraph in doc.paragraphs():
|
|
141
|
+
if tracked_replace(paragraph, old, new, args.author, stamp, counter):
|
|
142
|
+
hits += 1
|
|
143
|
+
applied.append({"old": old, "new": new, "paragraphs": hits})
|
|
144
|
+
|
|
145
|
+
settings = doc.xml("word/settings.xml")
|
|
146
|
+
if settings.find(kit.q("trackRevisions")) is None:
|
|
147
|
+
settings.append(settings.makeelement(kit.q("trackRevisions"), {}))
|
|
148
|
+
|
|
149
|
+
doc.save(args.out)
|
|
150
|
+
kit.emit({"ok": True, "output": str(Path(args.out)), "author": args.author, "replacements": applied})
|
|
151
|
+
return 0 if any(item["paragraphs"] for item in applied) else 1
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Append blocks to the end of an existing Word document.
|
|
3
|
+
|
|
4
|
+
The block builder (headings, paragraphs, bullets, numbered lists, tables,
|
|
5
|
+
images, equations, quotes, page breaks, spacers) is the helper CLI's own
|
|
6
|
+
`_docx_add_blocks`, imported rather than re-implemented so a spec that works
|
|
7
|
+
with `docx create` works here too.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
append_blocks.py --input IN.docx [--out OUT.docx] --blocks blocks.json
|
|
11
|
+
append_blocks.py --input IN.docx [--out OUT.docx] --spec spec.json
|
|
12
|
+
|
|
13
|
+
`--blocks` takes a JSON list of blocks or {"blocks": [...]}; `--spec` takes a
|
|
14
|
+
document spec whose `blocks` (or `sections`) array is appended.
|
|
15
|
+
|
|
16
|
+
Exit codes: 0 appended, 1 the document or a block could not be handled, 2
|
|
17
|
+
argparse usage error.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
27
|
+
|
|
28
|
+
import docx_kit as kit # noqa: E402
|
|
29
|
+
|
|
30
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
31
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
32
|
+
try:
|
|
33
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
34
|
+
except (AttributeError, ValueError):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
COUNTER_KEYS = ("headings", "paragraphs", "list_items", "tables", "images", "equations", "equation_fallbacks")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def fail(payload: dict, code: int = 1) -> int:
|
|
41
|
+
kit.emit({"ok": False, **payload})
|
|
42
|
+
return code
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_builder():
|
|
46
|
+
cli = kit.office_cli_path()
|
|
47
|
+
if cli is None:
|
|
48
|
+
raise FileNotFoundError("helper CLI not found; set DSH_OFFICE_CLI to python/office_cli.py")
|
|
49
|
+
sys.path.insert(0, str(cli.parent))
|
|
50
|
+
import office_cli
|
|
51
|
+
|
|
52
|
+
return office_cli
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_blocks(args) -> list[dict]:
|
|
56
|
+
raw = args.blocks or args.spec
|
|
57
|
+
text = raw.strip()
|
|
58
|
+
if text.startswith(("[", "{")):
|
|
59
|
+
payload = json.loads(text)
|
|
60
|
+
else:
|
|
61
|
+
payload = json.loads(Path(text).expanduser().read_text(encoding="utf-8"))
|
|
62
|
+
if isinstance(payload, list):
|
|
63
|
+
blocks = payload
|
|
64
|
+
else:
|
|
65
|
+
blocks = payload.get("blocks", payload.get("sections", []))
|
|
66
|
+
if not isinstance(blocks, list):
|
|
67
|
+
raise ValueError("blocks must be a JSON array")
|
|
68
|
+
return [block for block in blocks if isinstance(block, dict)]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main() -> int:
|
|
72
|
+
parser = argparse.ArgumentParser(description="Append content blocks to a .docx copy.")
|
|
73
|
+
parser.add_argument("--input", required=True, help="source .docx (never modified)")
|
|
74
|
+
parser.add_argument("--out", help="output path; default <name>-edited.docx")
|
|
75
|
+
parser.add_argument("--blocks", help="JSON file holding an array of blocks")
|
|
76
|
+
parser.add_argument("--spec", help="JSON document spec; its blocks array is appended")
|
|
77
|
+
args = parser.parse_args()
|
|
78
|
+
|
|
79
|
+
if not (args.blocks or args.spec):
|
|
80
|
+
return fail({"error": "pass --blocks or --spec"}, 2)
|
|
81
|
+
|
|
82
|
+
source = Path(args.input).expanduser()
|
|
83
|
+
if not source.is_file():
|
|
84
|
+
return fail({"error": f"input not found: {args.input}"})
|
|
85
|
+
out = Path(args.out).expanduser() if args.out else source.with_name(f"{source.stem}-edited.docx")
|
|
86
|
+
if out.resolve() == source.resolve():
|
|
87
|
+
return fail({"error": "refusing to overwrite the input; pass a different --out"})
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
blocks = load_blocks(args)
|
|
91
|
+
except (OSError, ValueError) as exc:
|
|
92
|
+
return fail({"error": f"cannot read blocks: {exc}"}, 2)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
office_cli = load_builder()
|
|
96
|
+
except FileNotFoundError as exc:
|
|
97
|
+
return fail({"error": str(exc)})
|
|
98
|
+
|
|
99
|
+
from docx import Document
|
|
100
|
+
|
|
101
|
+
document = Document(str(source))
|
|
102
|
+
counters = {key: 0 for key in COUNTER_KEYS}
|
|
103
|
+
try:
|
|
104
|
+
office_cli._docx_add_blocks(document, blocks, counters)
|
|
105
|
+
except SystemExit as exc: # the builder reports bad blocks through the CLI
|
|
106
|
+
return fail({"error": f"a block was rejected: {exc}", "counts": counters})
|
|
107
|
+
|
|
108
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
document.save(str(out))
|
|
110
|
+
kit.emit({"ok": True, "input": str(source), "output": str(out), "appended": len(blocks), "counts": counters})
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Merge a style pack (another .docx/.dotx) into a target .docx.
|
|
3
|
+
|
|
4
|
+
Only the formatting layer is transplanted: styles, and the numbering
|
|
5
|
+
definitions the styles point at. Body content, media, headers and section
|
|
6
|
+
geometry stay exactly as they are in the target. Styles that share an id are
|
|
7
|
+
replaced, target-only styles survive, and template-only styles are appended so
|
|
8
|
+
named styles referenced by the pack resolve.
|
|
9
|
+
|
|
10
|
+
Exit codes: 0 success, 1 template has no styles part, 2 bad input.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import sys
|
|
17
|
+
import zipfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from lxml import etree
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
23
|
+
import docx_kit as kit
|
|
24
|
+
|
|
25
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
26
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
27
|
+
try:
|
|
28
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
29
|
+
except (AttributeError, ValueError):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_xml(path: Path, part: str):
|
|
34
|
+
with zipfile.ZipFile(path) as zf:
|
|
35
|
+
if part not in zf.namelist():
|
|
36
|
+
return None
|
|
37
|
+
return etree.fromstring(zf.read(part))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def merge_styles(doc: kit.Docx, template_styles) -> dict:
|
|
41
|
+
target = doc.xml("word/styles.xml")
|
|
42
|
+
incoming = {node.get(kit.q("styleId")): node for node in template_styles.findall(kit.q("style")) if node.get(kit.q("styleId"))}
|
|
43
|
+
existing = {node.get(kit.q("styleId")): node for node in target.findall(kit.q("style")) if node.get(kit.q("styleId"))}
|
|
44
|
+
replaced = added = 0
|
|
45
|
+
for style_id, node in incoming.items():
|
|
46
|
+
clone = etree.fromstring(etree.tostring(node))
|
|
47
|
+
if style_id in existing:
|
|
48
|
+
target.replace(existing[style_id], clone)
|
|
49
|
+
existing[style_id] = clone
|
|
50
|
+
replaced += 1
|
|
51
|
+
else:
|
|
52
|
+
target.append(clone)
|
|
53
|
+
existing[style_id] = clone
|
|
54
|
+
added += 1
|
|
55
|
+
template_defaults = template_styles.find(kit.q("docDefaults"))
|
|
56
|
+
if template_defaults is not None:
|
|
57
|
+
clone = etree.fromstring(etree.tostring(template_defaults))
|
|
58
|
+
current = target.find(kit.q("docDefaults"))
|
|
59
|
+
if current is not None:
|
|
60
|
+
target.replace(current, clone)
|
|
61
|
+
else:
|
|
62
|
+
target.insert(0, clone)
|
|
63
|
+
return {"replaced": replaced, "added": added, "total": len(existing)}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main(argv: list[str] | None = None) -> int:
|
|
67
|
+
parser = argparse.ArgumentParser(description="Apply the styles and numbering of a template .docx/.dotx to a target .docx.")
|
|
68
|
+
parser.add_argument("--template", required=True, help="Template or style-pack .docx/.dotx")
|
|
69
|
+
parser.add_argument("--target", required=True, help="Document whose styles should be updated")
|
|
70
|
+
parser.add_argument("--out", required=True, help="Output .docx (never overwrite the target)")
|
|
71
|
+
args = parser.parse_args(argv)
|
|
72
|
+
|
|
73
|
+
template_path = Path(args.template)
|
|
74
|
+
if template_path.suffix.lower() == ".dotx":
|
|
75
|
+
# A .dotx is a .docx package with a different content type; reading the
|
|
76
|
+
# zip directly works the same, so no conversion is needed.
|
|
77
|
+
pass
|
|
78
|
+
try:
|
|
79
|
+
template_styles = load_xml(template_path, "word/styles.xml")
|
|
80
|
+
except OSError as exc:
|
|
81
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
82
|
+
return 2
|
|
83
|
+
if template_styles is None:
|
|
84
|
+
print("error: template has no word/styles.xml", file=sys.stderr)
|
|
85
|
+
return 1
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
doc = kit.Docx(args.target)
|
|
89
|
+
except (OSError, KeyError) as exc:
|
|
90
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
91
|
+
return 2
|
|
92
|
+
|
|
93
|
+
stats = merge_styles(doc, template_styles)
|
|
94
|
+
template_numbering = None
|
|
95
|
+
try:
|
|
96
|
+
template_numbering = load_xml(template_path, "word/numbering.xml")
|
|
97
|
+
except OSError:
|
|
98
|
+
template_numbering = None
|
|
99
|
+
if template_numbering is not None:
|
|
100
|
+
doc.set_xml("word/numbering.xml", etree.fromstring(etree.tostring(template_numbering)))
|
|
101
|
+
doc.ensure_override("word/numbering.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml")
|
|
102
|
+
doc.ensure_rel("word/document.xml", kit.REL_NUMBERING, "numbering.xml")
|
|
103
|
+
stats["numbering"] = "copied"
|
|
104
|
+
|
|
105
|
+
doc.save(args.out)
|
|
106
|
+
kit.emit({"ok": True, "template": str(template_path), "target": str(Path(args.target)), "output": str(Path(args.out)), "styles": stats})
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Add numbered captions under tables and figures, with optional bookmarks.
|
|
3
|
+
|
|
4
|
+
The number is written as a ``SEQ`` field by default so Word renumbers when
|
|
5
|
+
captions move, with the current value cached as the field result so a renderer
|
|
6
|
+
that skips fields still shows a number. ``--bookmarks`` adds stable bookmark
|
|
7
|
+
names (tbl1, fig1, ...) which ``insert_ref_fields.py`` can then reference.
|
|
8
|
+
|
|
9
|
+
Exit codes: 0 success, 2 bad input, 3 --materialize requested alongside a field
|
|
10
|
+
mode conflict (never raised: materialize simply writes literal numbers).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from lxml import etree
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
22
|
+
import docx_kit as kit
|
|
23
|
+
|
|
24
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
25
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
26
|
+
try:
|
|
27
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
28
|
+
except (AttributeError, ValueError):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
CAPTION_LABEL = {"table": "Table", "figure": "Figure"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def caption_paragraph(label: str, number: int, text: str, bookmark: str | None, ident: int, materialize: bool):
|
|
35
|
+
paragraph = etree.Element(kit.q("p"))
|
|
36
|
+
props = etree.SubElement(paragraph, kit.q("pPr"))
|
|
37
|
+
etree.SubElement(props, kit.q("pStyle")).set(kit.q("val"), "Caption")
|
|
38
|
+
paragraph.append(kit.make_run("%s " % label))
|
|
39
|
+
if bookmark:
|
|
40
|
+
start = etree.SubElement(paragraph, kit.q("bookmarkStart"))
|
|
41
|
+
start.set(kit.q("id"), str(ident))
|
|
42
|
+
start.set(kit.q("name"), bookmark)
|
|
43
|
+
if materialize:
|
|
44
|
+
paragraph.append(kit.make_run(str(number)))
|
|
45
|
+
else:
|
|
46
|
+
for run in kit.field_runs("SEQ %s \\* ARABIC" % label, str(number)):
|
|
47
|
+
paragraph.append(run)
|
|
48
|
+
if bookmark:
|
|
49
|
+
end = etree.SubElement(paragraph, kit.q("bookmarkEnd"))
|
|
50
|
+
end.set(kit.q("id"), str(ident))
|
|
51
|
+
if text:
|
|
52
|
+
paragraph.append(kit.make_run(". " + text))
|
|
53
|
+
return paragraph
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main(argv: list[str] | None = None) -> int:
|
|
57
|
+
parser = argparse.ArgumentParser(description="Insert numbered table/figure captions, optionally bookmarking them.")
|
|
58
|
+
parser.add_argument("input_docx", help="Input .docx")
|
|
59
|
+
parser.add_argument("output_docx", help="Output .docx")
|
|
60
|
+
parser.add_argument("--tables", action="store_true", help="Caption tables")
|
|
61
|
+
parser.add_argument("--figures", action="store_true", help="Caption figures (paragraphs containing a drawing)")
|
|
62
|
+
parser.add_argument("--caption_text", default="", help="Text placed after the number")
|
|
63
|
+
parser.add_argument("--bookmarks", action="store_true", help="Add tblN/figN bookmarks around the number")
|
|
64
|
+
parser.add_argument("--materialize", action="store_true", help="Write literal numbers instead of SEQ fields")
|
|
65
|
+
args = parser.parse_args(argv)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
doc = kit.Docx(args.input_docx)
|
|
69
|
+
except (OSError, KeyError) as exc:
|
|
70
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
71
|
+
return 2
|
|
72
|
+
|
|
73
|
+
do_tables = args.tables or args.figures
|
|
74
|
+
want_tables = args.tables or not do_tables
|
|
75
|
+
want_figures = args.figures or not do_tables
|
|
76
|
+
|
|
77
|
+
body = doc.body()
|
|
78
|
+
tables = list(body.iter(kit.q("tbl"))) if want_tables else []
|
|
79
|
+
figures = []
|
|
80
|
+
if want_figures:
|
|
81
|
+
for paragraph in body.iter(kit.q("p")):
|
|
82
|
+
if paragraph.find(".//" + kit.q("drawing")) is not None or paragraph.find(".//" + kit.q("pict", kit.V)) is not None:
|
|
83
|
+
figures.append(paragraph)
|
|
84
|
+
|
|
85
|
+
ident = doc.next_id()
|
|
86
|
+
added = []
|
|
87
|
+
for index, table in enumerate(tables, start=1):
|
|
88
|
+
bookmark = "tbl%d" % index if args.bookmarks else None
|
|
89
|
+
paragraph = caption_paragraph(CAPTION_LABEL["table"], index, args.caption_text, bookmark, ident, args.materialize)
|
|
90
|
+
table.addnext(paragraph)
|
|
91
|
+
added.append({"kind": "table", "number": index, "bookmark": bookmark})
|
|
92
|
+
ident += 1
|
|
93
|
+
for index, figure in enumerate(figures, start=1):
|
|
94
|
+
bookmark = "fig%d" % index if args.bookmarks else None
|
|
95
|
+
paragraph = caption_paragraph(CAPTION_LABEL["figure"], index, args.caption_text, bookmark, ident, args.materialize)
|
|
96
|
+
figure.addnext(paragraph)
|
|
97
|
+
added.append({"kind": "figure", "number": index, "bookmark": bookmark})
|
|
98
|
+
ident += 1
|
|
99
|
+
|
|
100
|
+
if not added:
|
|
101
|
+
print("error: no tables or figures found to caption", file=sys.stderr)
|
|
102
|
+
return 2
|
|
103
|
+
doc.save(args.output_docx)
|
|
104
|
+
kit.emit({"ok": True, "output": str(Path(args.output_docx)), "captions": added})
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
raise SystemExit(main())
|