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,143 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Attach Word comments to the paragraphs that contain given text.
|
|
3
|
+
|
|
4
|
+
Each ``--add "needle=comment body"`` anchors a comment over the first paragraph
|
|
5
|
+
whose text contains ``needle``: range start, range end and the reference run are
|
|
6
|
+
inserted inline, and the comment body is appended to comments.xml. Nothing is
|
|
7
|
+
reworded; the body is exactly the text you pass.
|
|
8
|
+
|
|
9
|
+
Exit codes: 0 all comments placed, 1 a pattern matched nothing (with
|
|
10
|
+
--require_all nothing is written), 2 bad input.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import datetime
|
|
17
|
+
import sys
|
|
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 comments_root(doc: kit.Docx):
|
|
34
|
+
if not doc.exists("word/comments.xml"):
|
|
35
|
+
root = etree.Element(kit.q("comments"), nsmap={"w": kit.W, "r": kit.R})
|
|
36
|
+
doc.set_xml("word/comments.xml", root)
|
|
37
|
+
doc.ensure_override("word/comments.xml", kit.CT_BY_REL[kit.REL_COMMENTS])
|
|
38
|
+
doc.ensure_rel("word/document.xml", kit.REL_COMMENTS, "comments.xml")
|
|
39
|
+
return doc.xml("word/comments.xml")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def initials(name: str) -> str:
|
|
43
|
+
parts = [piece for piece in name.replace(".", " ").split() if piece]
|
|
44
|
+
return "".join(piece[0] for piece in parts)[:3].upper() or "OR"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def append_comment(root, text: str, author: str, stamp: str, ident: int) -> None:
|
|
48
|
+
comment = etree.SubElement(root, kit.q("comment"))
|
|
49
|
+
comment.set(kit.q("id"), str(ident))
|
|
50
|
+
comment.set(kit.q("author"), author)
|
|
51
|
+
comment.set(kit.q("initials"), initials(author))
|
|
52
|
+
comment.set(kit.q("date"), stamp)
|
|
53
|
+
paragraph = etree.SubElement(comment, kit.q("p"))
|
|
54
|
+
run = etree.SubElement(paragraph, kit.q("r"))
|
|
55
|
+
node = etree.SubElement(run, kit.q("t"))
|
|
56
|
+
node.text = text
|
|
57
|
+
node.set(kit.q("space", kit.XML), "preserve")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def anchor(paragraph, ident: int) -> None:
|
|
61
|
+
start = etree.Element(kit.q("commentRangeStart"))
|
|
62
|
+
start.set(kit.q("id"), str(ident))
|
|
63
|
+
position = 1 if paragraph.find(kit.q("pPr")) is not None else 0
|
|
64
|
+
paragraph.insert(position, start)
|
|
65
|
+
end = etree.SubElement(paragraph, kit.q("commentRangeEnd"))
|
|
66
|
+
end.set(kit.q("id"), str(ident))
|
|
67
|
+
run = etree.SubElement(paragraph, kit.q("r"))
|
|
68
|
+
ref = etree.SubElement(run, kit.q("commentReference"))
|
|
69
|
+
ref.set(kit.q("id"), str(ident))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def parse_add(raw: str) -> tuple[str, str]:
|
|
73
|
+
if "=" not in raw:
|
|
74
|
+
raise ValueError("--add expects NEEDLE=COMMENT, got %r" % raw)
|
|
75
|
+
needle, body = raw.split("=", 1)
|
|
76
|
+
if not needle or not body:
|
|
77
|
+
raise ValueError("--add needs a non-empty needle and comment")
|
|
78
|
+
return needle, body
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main(argv: list[str] | None = None) -> int:
|
|
82
|
+
parser = argparse.ArgumentParser(description="Add Word comments over paragraphs matched by substring.")
|
|
83
|
+
parser.add_argument("in_docx", help="Input .docx")
|
|
84
|
+
parser.add_argument("--out", required=True, help="Output .docx")
|
|
85
|
+
parser.add_argument("--author", default="dsh-office", help="Comment author")
|
|
86
|
+
parser.add_argument("--add", action="append", default=[], help="NEEDLE=COMMENT (repeatable)")
|
|
87
|
+
parser.add_argument("--ignore_case", action="store_true", help="Match needle case-insensitively")
|
|
88
|
+
parser.add_argument("--require_all", action="store_true", help="Fail without writing if any needle is absent")
|
|
89
|
+
parser.add_argument("--date", default=None, help="ISO timestamp for the comments (default: now, UTC)")
|
|
90
|
+
args = parser.parse_args(argv)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
doc = kit.Docx(args.in_docx)
|
|
94
|
+
except (OSError, KeyError) as exc:
|
|
95
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
96
|
+
return 2
|
|
97
|
+
if not args.add:
|
|
98
|
+
parser.error("at least one --add NEEDLE=COMMENT is required")
|
|
99
|
+
|
|
100
|
+
paragraphs = doc.paragraphs()
|
|
101
|
+
requests = []
|
|
102
|
+
for raw in args.add:
|
|
103
|
+
try:
|
|
104
|
+
needle, body = parse_add(raw)
|
|
105
|
+
except ValueError as exc:
|
|
106
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
107
|
+
return 2
|
|
108
|
+
target = None
|
|
109
|
+
for paragraph in paragraphs:
|
|
110
|
+
haystack = kit.paragraph_text(paragraph)
|
|
111
|
+
if (needle.lower() in haystack.lower()) if args.ignore_case else (needle in haystack):
|
|
112
|
+
target = paragraph
|
|
113
|
+
break
|
|
114
|
+
requests.append({"needle": needle, "body": body, "paragraph": target})
|
|
115
|
+
|
|
116
|
+
missing = [item["needle"] for item in requests if item["paragraph"] is None]
|
|
117
|
+
if missing and args.require_all:
|
|
118
|
+
kit.emit({"ok": False, "output": None, "matched": 0, "missing": missing})
|
|
119
|
+
return 1
|
|
120
|
+
|
|
121
|
+
stamp = args.date or datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
122
|
+
root = comments_root(doc)
|
|
123
|
+
ident = doc.next_id()
|
|
124
|
+
placed = []
|
|
125
|
+
for item in requests:
|
|
126
|
+
if item["paragraph"] is None:
|
|
127
|
+
continue
|
|
128
|
+
append_comment(root, item["body"], args.author, stamp, ident)
|
|
129
|
+
anchor(item["paragraph"], ident)
|
|
130
|
+
placed.append({"id": ident, "needle": item["needle"], "body": item["body"]})
|
|
131
|
+
ident += 1
|
|
132
|
+
|
|
133
|
+
if not placed:
|
|
134
|
+
kit.emit({"ok": False, "output": None, "matched": 0, "missing": missing})
|
|
135
|
+
return 1
|
|
136
|
+
|
|
137
|
+
doc.save(args.out)
|
|
138
|
+
kit.emit({"ok": True, "output": str(Path(args.out)), "author": args.author, "placed": placed, "missing": missing})
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Edit existing comments in place from a JSON patch file.
|
|
3
|
+
|
|
4
|
+
The patch is a JSON list (or an object with an ``ops`` list) of operations keyed
|
|
5
|
+
by comment id:
|
|
6
|
+
|
|
7
|
+
[{"id": 1, "append": "Also check 2024."},
|
|
8
|
+
{"id": 2, "replace": "Rewritten note."},
|
|
9
|
+
{"id": 3, "resolved": true}]
|
|
10
|
+
|
|
11
|
+
``append`` adds a paragraph, ``replace`` rewrites the body, and ``resolved``
|
|
12
|
+
marks the comment done (the threading metadata Word uses for "Resolve").
|
|
13
|
+
|
|
14
|
+
Exit codes: 0 all operations applied, 1 some ids were missing, 2 bad input.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from lxml import etree
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
27
|
+
import docx_kit as kit
|
|
28
|
+
|
|
29
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
30
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
31
|
+
try:
|
|
32
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
33
|
+
except (AttributeError, ValueError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def set_body(comment, text: str) -> None:
|
|
38
|
+
paragraphs = comment.findall(kit.q("p"))
|
|
39
|
+
if not paragraphs:
|
|
40
|
+
paragraph = etree.SubElement(comment, kit.q("p"))
|
|
41
|
+
else:
|
|
42
|
+
paragraph = paragraphs[0]
|
|
43
|
+
for extra in paragraphs[1:]:
|
|
44
|
+
comment.remove(extra)
|
|
45
|
+
ppr = paragraph.find(kit.q("pPr"))
|
|
46
|
+
for child in list(paragraph):
|
|
47
|
+
if child is not ppr:
|
|
48
|
+
paragraph.remove(child)
|
|
49
|
+
run = etree.SubElement(paragraph, kit.q("r"))
|
|
50
|
+
node = etree.SubElement(run, kit.q("t"))
|
|
51
|
+
node.text = text
|
|
52
|
+
node.set(kit.q("space", kit.XML), "preserve")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def append_body(comment, text: str) -> None:
|
|
56
|
+
paragraph = etree.SubElement(comment, kit.q("p"))
|
|
57
|
+
run = etree.SubElement(paragraph, kit.q("r"))
|
|
58
|
+
node = etree.SubElement(run, kit.q("t"))
|
|
59
|
+
node.text = text
|
|
60
|
+
node.set(kit.q("space", kit.XML), "preserve")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def main(argv: list[str] | None = None) -> int:
|
|
64
|
+
parser = argparse.ArgumentParser(description="Apply append/replace/resolve operations to existing Word comments.")
|
|
65
|
+
parser.add_argument("in_docx", help="Input .docx")
|
|
66
|
+
parser.add_argument("patch_json", help="JSON patch file")
|
|
67
|
+
parser.add_argument("--out", required=True, help="Output .docx")
|
|
68
|
+
args = parser.parse_args(argv)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
patch = json.loads(Path(args.patch_json).read_text(encoding="utf-8"))
|
|
72
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
73
|
+
print("error: cannot read patch: %s" % exc, file=sys.stderr)
|
|
74
|
+
return 2
|
|
75
|
+
ops = patch.get("ops") if isinstance(patch, dict) else patch
|
|
76
|
+
if not isinstance(ops, list):
|
|
77
|
+
print("error: patch must be a list or an object with an 'ops' list", file=sys.stderr)
|
|
78
|
+
return 2
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
doc = kit.Docx(args.in_docx)
|
|
82
|
+
except (OSError, KeyError) as exc:
|
|
83
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
84
|
+
return 2
|
|
85
|
+
if not doc.exists("word/comments.xml"):
|
|
86
|
+
print("error: document has no comments", file=sys.stderr)
|
|
87
|
+
return 1
|
|
88
|
+
|
|
89
|
+
by_id = {node.get(kit.q("id")): node for node in doc.xml("word/comments.xml").findall(kit.q("comment"))}
|
|
90
|
+
applied, missing = [], []
|
|
91
|
+
for op in ops:
|
|
92
|
+
if not isinstance(op, dict):
|
|
93
|
+
print("error: each operation must be an object", file=sys.stderr)
|
|
94
|
+
return 2
|
|
95
|
+
ident = str(op.get("id"))
|
|
96
|
+
comment = by_id.get(ident)
|
|
97
|
+
if comment is None:
|
|
98
|
+
missing.append(ident)
|
|
99
|
+
continue
|
|
100
|
+
changed = []
|
|
101
|
+
if "replace" in op:
|
|
102
|
+
set_body(comment, str(op["replace"]))
|
|
103
|
+
changed.append("replace")
|
|
104
|
+
if "append" in op:
|
|
105
|
+
append_body(comment, str(op["append"]))
|
|
106
|
+
changed.append("append")
|
|
107
|
+
if "resolved" in op:
|
|
108
|
+
if op["resolved"]:
|
|
109
|
+
comment.set(kit.q("done", kit.W15), "1")
|
|
110
|
+
else:
|
|
111
|
+
comment.attrib.pop(kit.q("done", kit.W15), None)
|
|
112
|
+
changed.append("resolved")
|
|
113
|
+
applied.append({"id": ident, "changes": changed})
|
|
114
|
+
|
|
115
|
+
doc.save(args.out)
|
|
116
|
+
kit.emit({"ok": not missing, "output": str(Path(args.out)), "applied": applied, "missing_ids": missing})
|
|
117
|
+
return 1 if missing else 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Extract Word comments to JSON, including the text each comment is anchored to.
|
|
3
|
+
|
|
4
|
+
The anchor is recovered by walking the marked-up parts in document order and
|
|
5
|
+
collecting the text between ``commentRangeStart`` and ``commentRangeEnd`` for
|
|
6
|
+
the same id. Comments whose range was lost still appear, with an empty anchor.
|
|
7
|
+
|
|
8
|
+
Exit codes: 0 success (an empty list when the file has no comments), 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
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
21
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
22
|
+
try:
|
|
23
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
24
|
+
except (AttributeError, ValueError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def anchor_map(doc: kit.Docx) -> dict[str, dict]:
|
|
29
|
+
found: dict[str, dict] = {}
|
|
30
|
+
hf = [name for name in doc.names("word/header") + doc.names("word/footer") if name.endswith(".xml") and "_rels" not in name]
|
|
31
|
+
parts = ["word/document.xml"] + hf + [part for part in doc.text_parts() if part != "word/document.xml"]
|
|
32
|
+
for part in parts:
|
|
33
|
+
active = None
|
|
34
|
+
for node in doc.xml(part).iter():
|
|
35
|
+
if not isinstance(node.tag, str):
|
|
36
|
+
continue
|
|
37
|
+
local = node.tag.split("}", 1)[1] if node.tag.startswith("{%s}" % kit.W) else ""
|
|
38
|
+
if local == "commentRangeStart":
|
|
39
|
+
ident = node.get(kit.q("id")) or ""
|
|
40
|
+
active = ident
|
|
41
|
+
found.setdefault(ident, {"part": part, "anchored_text": ""})
|
|
42
|
+
elif local == "commentRangeEnd":
|
|
43
|
+
if active is not None and (node.get(kit.q("id")) or "") == active:
|
|
44
|
+
active = None
|
|
45
|
+
elif local == "commentReference" and active is None:
|
|
46
|
+
ident = node.get(kit.q("id")) or ""
|
|
47
|
+
found.setdefault(ident, {"part": part, "anchored_text": ""})
|
|
48
|
+
elif active is not None and local in ("t", "delText"):
|
|
49
|
+
found.setdefault(active, {"part": part, "anchored_text": ""})["anchored_text"] += node.text or ""
|
|
50
|
+
return found
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main(argv: list[str] | None = None) -> int:
|
|
54
|
+
parser = argparse.ArgumentParser(description="Extract comments (author, date, body, anchored text) from a .docx.")
|
|
55
|
+
parser.add_argument("in_docx", help="Input .docx")
|
|
56
|
+
parser.add_argument("--out", required=True, help="Write the JSON report here")
|
|
57
|
+
args = parser.parse_args(argv)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
doc = kit.Docx(args.in_docx)
|
|
61
|
+
except (OSError, KeyError) as exc:
|
|
62
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
63
|
+
return 2
|
|
64
|
+
if not doc.exists("word/comments.xml"):
|
|
65
|
+
kit.emit({"ok": True, "input": str(Path(args.in_docx)), "count": 0, "comments": []}, args.out)
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
anchors = anchor_map(doc)
|
|
69
|
+
comments = []
|
|
70
|
+
for node in doc.xml("word/comments.xml").findall(kit.q("comment")):
|
|
71
|
+
ident = node.get(kit.q("id")) or ""
|
|
72
|
+
paragraphs = [kit.text_of(paragraph) for paragraph in node.iter(kit.q("p"))]
|
|
73
|
+
info = anchors.get(ident, {})
|
|
74
|
+
comments.append(
|
|
75
|
+
{
|
|
76
|
+
"id": int(ident) if ident.lstrip("-").isdigit() else ident,
|
|
77
|
+
"author": node.get(kit.q("author")) or "",
|
|
78
|
+
"initials": node.get(kit.q("initials")) or "",
|
|
79
|
+
"date": node.get(kit.q("date")) or "",
|
|
80
|
+
"text": "\n".join(text for text in paragraphs if text),
|
|
81
|
+
"anchored_text": info.get("anchored_text", "").strip(),
|
|
82
|
+
"part": info.get("part"),
|
|
83
|
+
"resolved": node.get(kit.q("done", kit.W15)) in ("1", "true"),
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
comments.sort(key=lambda item: item["id"] if isinstance(item["id"], int) else 0)
|
|
87
|
+
kit.emit({"ok": True, "input": str(Path(args.in_docx)), "count": len(comments), "comments": comments}, args.out)
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Remove every comment from a .docx: markers, bodies, relationships, part.
|
|
3
|
+
|
|
4
|
+
Word needs all four removed together. Leaving a stray ``commentReference``
|
|
5
|
+
behind makes Word report a corrupt package, and leaving comments.xml in the zip
|
|
6
|
+
re-adds metadata you thought you deleted, so each is handled here.
|
|
7
|
+
|
|
8
|
+
Exit codes: 0 success (also when there was nothing to strip), 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
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
21
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
22
|
+
try:
|
|
23
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
24
|
+
except (AttributeError, ValueError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
MARKERS = ("commentRangeStart", "commentRangeEnd", "commentReference")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main(argv: list[str] | None = None) -> int:
|
|
31
|
+
parser = argparse.ArgumentParser(description="Strip all comments and comment markers from a .docx.")
|
|
32
|
+
parser.add_argument("in_docx", help="Input .docx")
|
|
33
|
+
parser.add_argument("--out", required=True, help="Output .docx")
|
|
34
|
+
args = parser.parse_args(argv)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
doc = kit.Docx(args.in_docx)
|
|
38
|
+
except (OSError, KeyError) as exc:
|
|
39
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
40
|
+
return 2
|
|
41
|
+
|
|
42
|
+
hf = [name for name in doc.names("word/header") + doc.names("word/footer") if name.endswith(".xml") and "_rels" not in name]
|
|
43
|
+
removed_markers = 0
|
|
44
|
+
for part in ["word/document.xml"] + hf + doc.text_parts():
|
|
45
|
+
if not doc.exists(part):
|
|
46
|
+
continue
|
|
47
|
+
root = doc.xml(part)
|
|
48
|
+
for local in MARKERS:
|
|
49
|
+
for node in list(root.iter(kit.q(local))):
|
|
50
|
+
parent = node.getparent()
|
|
51
|
+
if parent is None:
|
|
52
|
+
continue
|
|
53
|
+
if local == "commentReference" and parent.tag == kit.q("r") and len(parent) <= 1:
|
|
54
|
+
grand = parent.getparent()
|
|
55
|
+
if grand is not None:
|
|
56
|
+
grand.remove(parent)
|
|
57
|
+
else:
|
|
58
|
+
parent.remove(node)
|
|
59
|
+
removed_markers += 1
|
|
60
|
+
|
|
61
|
+
had_part = doc.exists("word/comments.xml")
|
|
62
|
+
if had_part:
|
|
63
|
+
doc.drop("word/comments.xml")
|
|
64
|
+
doc.drop_rel_target("word/document.xml", "word/comments.xml")
|
|
65
|
+
doc.drop_override("word/comments.xml")
|
|
66
|
+
|
|
67
|
+
doc.save(args.out)
|
|
68
|
+
kit.emit(
|
|
69
|
+
{
|
|
70
|
+
"ok": True,
|
|
71
|
+
"input": str(Path(args.in_docx)),
|
|
72
|
+
"output": str(Path(args.out)),
|
|
73
|
+
"comments_part_removed": had_part,
|
|
74
|
+
"markers_removed": removed_markers,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Read or edit Word content controls (structured document tags / SDT).
|
|
3
|
+
|
|
4
|
+
python-docx has no API for content controls, so this works directly on the
|
|
5
|
+
``w:sdt`` elements: list them, wrap a paragraph into a typed control, or replace
|
|
6
|
+
the text of controls matched by tag. Tags are the stable key — they survive
|
|
7
|
+
copying between documents better than the numeric ids.
|
|
8
|
+
|
|
9
|
+
Exit codes: 0 success, 1 a tag was not found, 2 bad input or bad subcommand.
|
|
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
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
24
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
25
|
+
try:
|
|
26
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
27
|
+
except (AttributeError, ValueError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
TYPE_TAGS = ("text", "richText", "date", "dropDownList", "comboBox", "picture", "checkbox")
|
|
31
|
+
CONTAINER_TAGS = ("docPartObj", "group", "citation", "bibliography")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def part_list(doc: kit.Docx, include_headers: bool) -> list[str]:
|
|
35
|
+
parts = ["word/document.xml"]
|
|
36
|
+
if include_headers:
|
|
37
|
+
parts += [name for name in doc.names("word/header") + doc.names("word/footer") if name.endswith(".xml") and "_rels" not in name]
|
|
38
|
+
return [part for part in parts if doc.exists(part)]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def controls(doc: kit.Docx, part: str):
|
|
42
|
+
return [node for node in doc.xml(part).iter(kit.q("sdt")) if node.find(kit.q("sdtPr")) is not None]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def describe(doc: kit.Docx, part: str, sdt) -> dict:
|
|
46
|
+
props = sdt.find(kit.q("sdtPr"))
|
|
47
|
+
tag = props.find(kit.q("tag")) if props is not None else None
|
|
48
|
+
alias = props.find(kit.q("alias")) if props is not None else None
|
|
49
|
+
ident = props.find(kit.q("id")) if props is not None else None
|
|
50
|
+
kind = "unknown"
|
|
51
|
+
if props is not None:
|
|
52
|
+
for name in TYPE_TAGS:
|
|
53
|
+
if props.find(kit.q(name)) is not None:
|
|
54
|
+
kind = name
|
|
55
|
+
break
|
|
56
|
+
if kind == "unknown" and any(props.find(kit.q(name)) is not None for name in CONTAINER_TAGS):
|
|
57
|
+
kind = "container"
|
|
58
|
+
content = sdt.find(kit.q("sdtContent"))
|
|
59
|
+
text = kit.text_of(content) if content is not None else ""
|
|
60
|
+
return {
|
|
61
|
+
"part": part,
|
|
62
|
+
"tag": tag.get(kit.q("val")) if tag is not None else "",
|
|
63
|
+
"alias": alias.get(kit.q("val")) if alias is not None else "",
|
|
64
|
+
"id": ident.get(kit.q("val")) if ident is not None else "",
|
|
65
|
+
"type": kind,
|
|
66
|
+
"text": text,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def build_control(tag: str, alias: str, text: str, ident: int, content) -> etree._Element:
|
|
71
|
+
sdt = etree.Element(kit.q("sdt"))
|
|
72
|
+
props = etree.SubElement(sdt, kit.q("sdtPr"))
|
|
73
|
+
if tag:
|
|
74
|
+
etree.SubElement(props, kit.q("tag")).set(kit.q("val"), tag)
|
|
75
|
+
if alias:
|
|
76
|
+
etree.SubElement(props, kit.q("alias")).set(kit.q("val"), alias)
|
|
77
|
+
etree.SubElement(props, kit.q("id")).set(kit.q("val"), str(ident))
|
|
78
|
+
etree.SubElement(props, kit.q("text"))
|
|
79
|
+
body = etree.SubElement(sdt, kit.q("sdtContent"))
|
|
80
|
+
if content is not None:
|
|
81
|
+
body.append(content)
|
|
82
|
+
else:
|
|
83
|
+
paragraph = etree.SubElement(body, kit.q("p"))
|
|
84
|
+
run = etree.SubElement(paragraph, kit.q("r"))
|
|
85
|
+
node = etree.SubElement(run, kit.q("t"))
|
|
86
|
+
node.text = text
|
|
87
|
+
return sdt
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def replace_text(sdt, value: str) -> bool:
|
|
91
|
+
content = sdt.find(kit.q("sdtContent"))
|
|
92
|
+
if content is None:
|
|
93
|
+
return False
|
|
94
|
+
paragraphs = list(content.iter(kit.q("p")))
|
|
95
|
+
if not paragraphs:
|
|
96
|
+
return False
|
|
97
|
+
paragraph = paragraphs[0]
|
|
98
|
+
for extra in paragraphs[1:]:
|
|
99
|
+
parent = extra.getparent()
|
|
100
|
+
if parent is not None:
|
|
101
|
+
parent.remove(extra)
|
|
102
|
+
ppr = paragraph.find(kit.q("pPr"))
|
|
103
|
+
for child in list(paragraph):
|
|
104
|
+
if child is not ppr:
|
|
105
|
+
paragraph.remove(child)
|
|
106
|
+
run = etree.SubElement(paragraph, kit.q("r"))
|
|
107
|
+
node = etree.SubElement(run, kit.q("t"))
|
|
108
|
+
node.text = value
|
|
109
|
+
node.set(kit.q("space"), kit.XML)
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main(argv: list[str] | None = None) -> int:
|
|
114
|
+
parser = argparse.ArgumentParser(description="List, create, or fill Word content controls (SDTs).")
|
|
115
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
116
|
+
|
|
117
|
+
listing = sub.add_parser("list", help="list content controls")
|
|
118
|
+
listing.add_argument("docx")
|
|
119
|
+
listing.add_argument("--json", action="store_true", help="emit JSON (always on; kept for scripts)")
|
|
120
|
+
listing.add_argument("--no_headers_footers", action="store_true")
|
|
121
|
+
|
|
122
|
+
create = sub.add_parser("create", help="wrap a paragraph or add a new control")
|
|
123
|
+
create.add_argument("docx")
|
|
124
|
+
create.add_argument("--tag", default="", help="Stable tag used by later edits")
|
|
125
|
+
create.add_argument("--alias", default="", help="Display name shown in Word")
|
|
126
|
+
create.add_argument("--text", default="", help="Initial value for a new control")
|
|
127
|
+
create.add_argument("--placeholder", default="", help="Wrap the paragraph containing this text instead of adding a new control")
|
|
128
|
+
create.add_argument("--output", required=True)
|
|
129
|
+
create.add_argument("--no_headers_footers", action="store_true")
|
|
130
|
+
|
|
131
|
+
update = sub.add_parser("set", help="fill controls by tag")
|
|
132
|
+
update.add_argument("docx")
|
|
133
|
+
update.add_argument("--set", action="append", default=[], help="TAG=VALUE (repeatable)")
|
|
134
|
+
update.add_argument("--output", required=True)
|
|
135
|
+
update.add_argument("--no_headers_footers", action="store_true")
|
|
136
|
+
|
|
137
|
+
args = parser.parse_args(argv)
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
doc = kit.Docx(args.docx)
|
|
141
|
+
except (OSError, KeyError) as exc:
|
|
142
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
143
|
+
return 2
|
|
144
|
+
|
|
145
|
+
if args.command == "list":
|
|
146
|
+
rows = []
|
|
147
|
+
for part in part_list(doc, not args.no_headers_footers):
|
|
148
|
+
for sdt in controls(doc, part):
|
|
149
|
+
rows.append(describe(doc, part, sdt))
|
|
150
|
+
kit.emit({"ok": True, "input": str(Path(args.docx)), "count": len(rows), "controls": rows})
|
|
151
|
+
return 0
|
|
152
|
+
|
|
153
|
+
if args.command == "set":
|
|
154
|
+
if not args.set:
|
|
155
|
+
parser.error("at least one --set TAG=VALUE is required")
|
|
156
|
+
wanted = {}
|
|
157
|
+
for raw in args.set:
|
|
158
|
+
if "=" not in raw:
|
|
159
|
+
parser.error("--set expects TAG=VALUE, got %r" % raw)
|
|
160
|
+
tag, value = raw.split("=", 1)
|
|
161
|
+
wanted[tag] = value
|
|
162
|
+
filled, missing = [], []
|
|
163
|
+
for part in part_list(doc, not args.no_headers_footers):
|
|
164
|
+
for sdt in controls(doc, part):
|
|
165
|
+
info = describe(doc, part, sdt)
|
|
166
|
+
if info["tag"] in wanted and replace_text(sdt, wanted[info["tag"]]):
|
|
167
|
+
filled.append({"tag": info["tag"], "value": wanted[info["tag"]], "part": part})
|
|
168
|
+
wanted.pop(info["tag"], None)
|
|
169
|
+
missing = sorted(wanted)
|
|
170
|
+
doc.save(args.output)
|
|
171
|
+
kit.emit({"ok": not missing, "output": str(Path(args.output)), "filled": filled, "missing_tags": missing})
|
|
172
|
+
return 1 if missing else 0
|
|
173
|
+
|
|
174
|
+
ident = doc.next_id()
|
|
175
|
+
if args.placeholder:
|
|
176
|
+
target = None
|
|
177
|
+
for paragraph in doc.paragraphs():
|
|
178
|
+
if args.placeholder in kit.paragraph_text(paragraph):
|
|
179
|
+
target = paragraph
|
|
180
|
+
break
|
|
181
|
+
if target is None:
|
|
182
|
+
print("error: placeholder %r not found" % args.placeholder, file=sys.stderr)
|
|
183
|
+
return 1
|
|
184
|
+
parent = target.getparent()
|
|
185
|
+
index = list(parent).index(target)
|
|
186
|
+
sdt = build_control(args.tag, args.alias, args.text, ident, target)
|
|
187
|
+
parent.insert(index, sdt)
|
|
188
|
+
where = "wrapped"
|
|
189
|
+
else:
|
|
190
|
+
sdt = build_control(args.tag, args.alias, args.text, ident, None)
|
|
191
|
+
doc.body().append(sdt)
|
|
192
|
+
where = "appended"
|
|
193
|
+
doc.save(args.output)
|
|
194
|
+
kit.emit({"ok": True, "output": str(Path(args.output)), "mode": where, "tag": args.tag, "id": ident})
|
|
195
|
+
return 0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
raise SystemExit(main())
|