dsh-office-kit 0.2.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/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 +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 +1 -1
- package/postinstall.mjs +92 -82
- package/python/office_cli.py +171 -246
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
|
|
27
|
+
def split_pair(raw: str) -> tuple[str, str]:
|
|
28
|
+
if "=" not in raw:
|
|
29
|
+
raise argparse.ArgumentTypeError("--replace expects OLD=NEW, got %r" % raw)
|
|
30
|
+
old, new = raw.split("=", 1)
|
|
31
|
+
if not old:
|
|
32
|
+
raise argparse.ArgumentTypeError("--replace needs a non-empty OLD value")
|
|
33
|
+
return old, new
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run_text(run) -> str:
|
|
37
|
+
return kit.text_of(run)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def tracked_replace(paragraph, old: str, new: str, author: str, stamp: str, counter: list[int]) -> bool:
|
|
41
|
+
full = kit.paragraph_text(paragraph)
|
|
42
|
+
start = full.find(old)
|
|
43
|
+
if start < 0:
|
|
44
|
+
return False
|
|
45
|
+
end = start + len(old)
|
|
46
|
+
runs = [child for child in paragraph if child.tag == kit.q("r")]
|
|
47
|
+
if not runs:
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
out: list = []
|
|
51
|
+
deleted: list = []
|
|
52
|
+
insert_at: int | None = None
|
|
53
|
+
cursor = 0
|
|
54
|
+
for run in runs:
|
|
55
|
+
text = run_text(run)
|
|
56
|
+
rpr = run.find(kit.q("rPr"))
|
|
57
|
+
left_end = start
|
|
58
|
+
right_start = end
|
|
59
|
+
run_start, run_end = cursor, cursor + len(text)
|
|
60
|
+
cursor = run_end
|
|
61
|
+
if run_end <= left_end or run_start >= right_start:
|
|
62
|
+
out.append(run)
|
|
63
|
+
continue
|
|
64
|
+
left = text[: max(0, left_end - run_start)]
|
|
65
|
+
middle = text[max(0, left_end - run_start) : min(len(text), right_start - run_start)]
|
|
66
|
+
right = text[min(len(text), right_start - run_start) :]
|
|
67
|
+
if left:
|
|
68
|
+
out.append(kit.make_run(left, rpr))
|
|
69
|
+
if middle:
|
|
70
|
+
if insert_at is None:
|
|
71
|
+
insert_at = len(out)
|
|
72
|
+
deleted.append(kit.make_run(middle, rpr, tag="delText"))
|
|
73
|
+
if right:
|
|
74
|
+
out.append(kit.make_run(right, rpr))
|
|
75
|
+
|
|
76
|
+
if not deleted:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
ident = counter[0]
|
|
80
|
+
counter[0] += 2
|
|
81
|
+
deletion = etree.Element(kit.q("del"))
|
|
82
|
+
deletion.set(kit.q("id"), str(ident))
|
|
83
|
+
deletion.set(kit.q("author"), author)
|
|
84
|
+
deletion.set(kit.q("date"), stamp)
|
|
85
|
+
for node in deleted:
|
|
86
|
+
deletion.append(node)
|
|
87
|
+
insertion = etree.Element(kit.q("ins"))
|
|
88
|
+
insertion.set(kit.q("id"), str(ident + 1))
|
|
89
|
+
insertion.set(kit.q("author"), author)
|
|
90
|
+
insertion.set(kit.q("date"), stamp)
|
|
91
|
+
insertion.append(kit.make_run(new))
|
|
92
|
+
|
|
93
|
+
position = insert_at if insert_at is not None else len(out)
|
|
94
|
+
out.insert(position, deletion)
|
|
95
|
+
out.insert(position + 1, insertion)
|
|
96
|
+
|
|
97
|
+
ppr = paragraph.find(kit.q("pPr"))
|
|
98
|
+
for child in list(paragraph):
|
|
99
|
+
if child is not ppr:
|
|
100
|
+
paragraph.remove(child)
|
|
101
|
+
for child in out:
|
|
102
|
+
paragraph.append(child)
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main(argv: list[str] | None = None) -> int:
|
|
107
|
+
parser = argparse.ArgumentParser(description="Record find/replace edits as tracked deletions and insertions.")
|
|
108
|
+
parser.add_argument("in_docx", help="Input .docx")
|
|
109
|
+
parser.add_argument("--out", required=True, help="Output .docx")
|
|
110
|
+
parser.add_argument("--author", default="dsh-office", help="Revision author shown in Word")
|
|
111
|
+
parser.add_argument("--replace", action="append", default=[], help="Replacement formatted as OLD=NEW (repeatable)")
|
|
112
|
+
parser.add_argument("--date", default=None, help="ISO timestamp for the revisions (default: now, UTC)")
|
|
113
|
+
args = parser.parse_args(argv)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
doc = kit.Docx(args.in_docx)
|
|
117
|
+
except (OSError, KeyError) as exc:
|
|
118
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
119
|
+
return 2
|
|
120
|
+
if not args.replace:
|
|
121
|
+
parser.error("at least one --replace OLD=NEW is required")
|
|
122
|
+
|
|
123
|
+
stamp = args.date or datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
124
|
+
counter = [doc.next_id()]
|
|
125
|
+
applied = []
|
|
126
|
+
for raw in args.replace:
|
|
127
|
+
try:
|
|
128
|
+
old, new = split_pair(raw)
|
|
129
|
+
except argparse.ArgumentTypeError as exc:
|
|
130
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
131
|
+
return 2
|
|
132
|
+
hits = 0
|
|
133
|
+
for paragraph in doc.paragraphs():
|
|
134
|
+
if tracked_replace(paragraph, old, new, args.author, stamp, counter):
|
|
135
|
+
hits += 1
|
|
136
|
+
applied.append({"old": old, "new": new, "paragraphs": hits})
|
|
137
|
+
|
|
138
|
+
settings = doc.xml("word/settings.xml")
|
|
139
|
+
if settings.find(kit.q("trackRevisions")) is None:
|
|
140
|
+
settings.append(settings.makeelement(kit.q("trackRevisions"), {}))
|
|
141
|
+
|
|
142
|
+
doc.save(args.out)
|
|
143
|
+
kit.emit({"ok": True, "output": str(Path(args.out)), "author": args.author, "replacements": applied})
|
|
144
|
+
return 0 if any(item["paragraphs"] for item in applied) else 1
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
COUNTER_KEYS = ("headings", "paragraphs", "list_items", "tables", "images", "equations", "equation_fallbacks")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def fail(payload: dict, code: int = 1) -> int:
|
|
34
|
+
kit.emit({"ok": False, **payload})
|
|
35
|
+
return code
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_builder():
|
|
39
|
+
cli = kit.office_cli_path()
|
|
40
|
+
if cli is None:
|
|
41
|
+
raise FileNotFoundError("helper CLI not found; set DSH_OFFICE_CLI to python/office_cli.py")
|
|
42
|
+
sys.path.insert(0, str(cli.parent))
|
|
43
|
+
import office_cli
|
|
44
|
+
|
|
45
|
+
return office_cli
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_blocks(args) -> list[dict]:
|
|
49
|
+
raw = args.blocks or args.spec
|
|
50
|
+
text = raw.strip()
|
|
51
|
+
if text.startswith(("[", "{")):
|
|
52
|
+
payload = json.loads(text)
|
|
53
|
+
else:
|
|
54
|
+
payload = json.loads(Path(text).expanduser().read_text(encoding="utf-8"))
|
|
55
|
+
if isinstance(payload, list):
|
|
56
|
+
blocks = payload
|
|
57
|
+
else:
|
|
58
|
+
blocks = payload.get("blocks", payload.get("sections", []))
|
|
59
|
+
if not isinstance(blocks, list):
|
|
60
|
+
raise ValueError("blocks must be a JSON array")
|
|
61
|
+
return [block for block in blocks if isinstance(block, dict)]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main() -> int:
|
|
65
|
+
parser = argparse.ArgumentParser(description="Append content blocks to a .docx copy.")
|
|
66
|
+
parser.add_argument("--input", required=True, help="source .docx (never modified)")
|
|
67
|
+
parser.add_argument("--out", help="output path; default <name>-edited.docx")
|
|
68
|
+
parser.add_argument("--blocks", help="JSON file holding an array of blocks")
|
|
69
|
+
parser.add_argument("--spec", help="JSON document spec; its blocks array is appended")
|
|
70
|
+
args = parser.parse_args()
|
|
71
|
+
|
|
72
|
+
if not (args.blocks or args.spec):
|
|
73
|
+
return fail({"error": "pass --blocks or --spec"}, 2)
|
|
74
|
+
|
|
75
|
+
source = Path(args.input).expanduser()
|
|
76
|
+
if not source.is_file():
|
|
77
|
+
return fail({"error": f"input not found: {args.input}"})
|
|
78
|
+
out = Path(args.out).expanduser() if args.out else source.with_name(f"{source.stem}-edited.docx")
|
|
79
|
+
if out.resolve() == source.resolve():
|
|
80
|
+
return fail({"error": "refusing to overwrite the input; pass a different --out"})
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
blocks = load_blocks(args)
|
|
84
|
+
except (OSError, ValueError) as exc:
|
|
85
|
+
return fail({"error": f"cannot read blocks: {exc}"}, 2)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
office_cli = load_builder()
|
|
89
|
+
except FileNotFoundError as exc:
|
|
90
|
+
return fail({"error": str(exc)})
|
|
91
|
+
|
|
92
|
+
from docx import Document
|
|
93
|
+
|
|
94
|
+
document = Document(str(source))
|
|
95
|
+
counters = {key: 0 for key in COUNTER_KEYS}
|
|
96
|
+
try:
|
|
97
|
+
office_cli._docx_add_blocks(document, blocks, counters)
|
|
98
|
+
except SystemExit as exc: # the builder reports bad blocks through the CLI
|
|
99
|
+
return fail({"error": f"a block was rejected: {exc}", "counts": counters})
|
|
100
|
+
|
|
101
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
document.save(str(out))
|
|
103
|
+
kit.emit({"ok": True, "input": str(source), "output": str(out), "appended": len(blocks), "counts": counters})
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
|
|
26
|
+
def load_xml(path: Path, part: str):
|
|
27
|
+
with zipfile.ZipFile(path) as zf:
|
|
28
|
+
if part not in zf.namelist():
|
|
29
|
+
return None
|
|
30
|
+
return etree.fromstring(zf.read(part))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def merge_styles(doc: kit.Docx, template_styles) -> dict:
|
|
34
|
+
target = doc.xml("word/styles.xml")
|
|
35
|
+
incoming = {node.get(kit.q("styleId")): node for node in template_styles.findall(kit.q("style")) if node.get(kit.q("styleId"))}
|
|
36
|
+
existing = {node.get(kit.q("styleId")): node for node in target.findall(kit.q("style")) if node.get(kit.q("styleId"))}
|
|
37
|
+
replaced = added = 0
|
|
38
|
+
for style_id, node in incoming.items():
|
|
39
|
+
clone = etree.fromstring(etree.tostring(node))
|
|
40
|
+
if style_id in existing:
|
|
41
|
+
target.replace(existing[style_id], clone)
|
|
42
|
+
existing[style_id] = clone
|
|
43
|
+
replaced += 1
|
|
44
|
+
else:
|
|
45
|
+
target.append(clone)
|
|
46
|
+
existing[style_id] = clone
|
|
47
|
+
added += 1
|
|
48
|
+
template_defaults = template_styles.find(kit.q("docDefaults"))
|
|
49
|
+
if template_defaults is not None:
|
|
50
|
+
clone = etree.fromstring(etree.tostring(template_defaults))
|
|
51
|
+
current = target.find(kit.q("docDefaults"))
|
|
52
|
+
if current is not None:
|
|
53
|
+
target.replace(current, clone)
|
|
54
|
+
else:
|
|
55
|
+
target.insert(0, clone)
|
|
56
|
+
return {"replaced": replaced, "added": added, "total": len(existing)}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main(argv: list[str] | None = None) -> int:
|
|
60
|
+
parser = argparse.ArgumentParser(description="Apply the styles and numbering of a template .docx/.dotx to a target .docx.")
|
|
61
|
+
parser.add_argument("--template", required=True, help="Template or style-pack .docx/.dotx")
|
|
62
|
+
parser.add_argument("--target", required=True, help="Document whose styles should be updated")
|
|
63
|
+
parser.add_argument("--out", required=True, help="Output .docx (never overwrite the target)")
|
|
64
|
+
args = parser.parse_args(argv)
|
|
65
|
+
|
|
66
|
+
template_path = Path(args.template)
|
|
67
|
+
if template_path.suffix.lower() == ".dotx":
|
|
68
|
+
# A .dotx is a .docx package with a different content type; reading the
|
|
69
|
+
# zip directly works the same, so no conversion is needed.
|
|
70
|
+
pass
|
|
71
|
+
try:
|
|
72
|
+
template_styles = load_xml(template_path, "word/styles.xml")
|
|
73
|
+
except OSError as exc:
|
|
74
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
75
|
+
return 2
|
|
76
|
+
if template_styles is None:
|
|
77
|
+
print("error: template has no word/styles.xml", file=sys.stderr)
|
|
78
|
+
return 1
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
doc = kit.Docx(args.target)
|
|
82
|
+
except (OSError, KeyError) as exc:
|
|
83
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
84
|
+
return 2
|
|
85
|
+
|
|
86
|
+
stats = merge_styles(doc, template_styles)
|
|
87
|
+
template_numbering = None
|
|
88
|
+
try:
|
|
89
|
+
template_numbering = load_xml(template_path, "word/numbering.xml")
|
|
90
|
+
except OSError:
|
|
91
|
+
template_numbering = None
|
|
92
|
+
if template_numbering is not None:
|
|
93
|
+
doc.set_xml("word/numbering.xml", etree.fromstring(etree.tostring(template_numbering)))
|
|
94
|
+
doc.ensure_override("word/numbering.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml")
|
|
95
|
+
doc.ensure_rel("word/document.xml", kit.REL_NUMBERING, "numbering.xml")
|
|
96
|
+
stats["numbering"] = "copied"
|
|
97
|
+
|
|
98
|
+
doc.save(args.out)
|
|
99
|
+
kit.emit({"ok": True, "template": str(template_path), "target": str(Path(args.target)), "output": str(Path(args.out)), "styles": stats})
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
CAPTION_LABEL = {"table": "Table", "figure": "Figure"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def caption_paragraph(label: str, number: int, text: str, bookmark: str | None, ident: int, materialize: bool):
|
|
28
|
+
paragraph = etree.Element(kit.q("p"))
|
|
29
|
+
props = etree.SubElement(paragraph, kit.q("pPr"))
|
|
30
|
+
etree.SubElement(props, kit.q("pStyle")).set(kit.q("val"), "Caption")
|
|
31
|
+
paragraph.append(kit.make_run("%s " % label))
|
|
32
|
+
if bookmark:
|
|
33
|
+
start = etree.SubElement(paragraph, kit.q("bookmarkStart"))
|
|
34
|
+
start.set(kit.q("id"), str(ident))
|
|
35
|
+
start.set(kit.q("name"), bookmark)
|
|
36
|
+
if materialize:
|
|
37
|
+
paragraph.append(kit.make_run(str(number)))
|
|
38
|
+
else:
|
|
39
|
+
for run in kit.field_runs("SEQ %s \\* ARABIC" % label, str(number)):
|
|
40
|
+
paragraph.append(run)
|
|
41
|
+
if bookmark:
|
|
42
|
+
end = etree.SubElement(paragraph, kit.q("bookmarkEnd"))
|
|
43
|
+
end.set(kit.q("id"), str(ident))
|
|
44
|
+
if text:
|
|
45
|
+
paragraph.append(kit.make_run(". " + text))
|
|
46
|
+
return paragraph
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main(argv: list[str] | None = None) -> int:
|
|
50
|
+
parser = argparse.ArgumentParser(description="Insert numbered table/figure captions, optionally bookmarking them.")
|
|
51
|
+
parser.add_argument("input_docx", help="Input .docx")
|
|
52
|
+
parser.add_argument("output_docx", help="Output .docx")
|
|
53
|
+
parser.add_argument("--tables", action="store_true", help="Caption tables")
|
|
54
|
+
parser.add_argument("--figures", action="store_true", help="Caption figures (paragraphs containing a drawing)")
|
|
55
|
+
parser.add_argument("--caption_text", default="", help="Text placed after the number")
|
|
56
|
+
parser.add_argument("--bookmarks", action="store_true", help="Add tblN/figN bookmarks around the number")
|
|
57
|
+
parser.add_argument("--materialize", action="store_true", help="Write literal numbers instead of SEQ fields")
|
|
58
|
+
args = parser.parse_args(argv)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
doc = kit.Docx(args.input_docx)
|
|
62
|
+
except (OSError, KeyError) as exc:
|
|
63
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
64
|
+
return 2
|
|
65
|
+
|
|
66
|
+
do_tables = args.tables or args.figures
|
|
67
|
+
want_tables = args.tables or not do_tables
|
|
68
|
+
want_figures = args.figures or not do_tables
|
|
69
|
+
|
|
70
|
+
body = doc.body()
|
|
71
|
+
tables = list(body.iter(kit.q("tbl"))) if want_tables else []
|
|
72
|
+
figures = []
|
|
73
|
+
if want_figures:
|
|
74
|
+
for paragraph in body.iter(kit.q("p")):
|
|
75
|
+
if paragraph.find(".//" + kit.q("drawing")) is not None or paragraph.find(".//" + kit.q("pict", kit.V)) is not None:
|
|
76
|
+
figures.append(paragraph)
|
|
77
|
+
|
|
78
|
+
ident = doc.next_id()
|
|
79
|
+
added = []
|
|
80
|
+
for index, table in enumerate(tables, start=1):
|
|
81
|
+
bookmark = "tbl%d" % index if args.bookmarks else None
|
|
82
|
+
paragraph = caption_paragraph(CAPTION_LABEL["table"], index, args.caption_text, bookmark, ident, args.materialize)
|
|
83
|
+
table.addnext(paragraph)
|
|
84
|
+
added.append({"kind": "table", "number": index, "bookmark": bookmark})
|
|
85
|
+
ident += 1
|
|
86
|
+
for index, figure in enumerate(figures, start=1):
|
|
87
|
+
bookmark = "fig%d" % index if args.bookmarks else None
|
|
88
|
+
paragraph = caption_paragraph(CAPTION_LABEL["figure"], index, args.caption_text, bookmark, ident, args.materialize)
|
|
89
|
+
figure.addnext(paragraph)
|
|
90
|
+
added.append({"kind": "figure", "number": index, "bookmark": bookmark})
|
|
91
|
+
ident += 1
|
|
92
|
+
|
|
93
|
+
if not added:
|
|
94
|
+
print("error: no tables or figures found to caption", file=sys.stderr)
|
|
95
|
+
return 2
|
|
96
|
+
doc.save(args.output_docx)
|
|
97
|
+
kit.emit({"ok": True, "output": str(Path(args.output_docx)), "captions": added})
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
|
|
26
|
+
def comments_root(doc: kit.Docx):
|
|
27
|
+
if not doc.exists("word/comments.xml"):
|
|
28
|
+
root = etree.Element(kit.q("comments"), nsmap={"w": kit.W, "r": kit.R})
|
|
29
|
+
doc.set_xml("word/comments.xml", root)
|
|
30
|
+
doc.ensure_override("word/comments.xml", kit.CT_BY_REL[kit.REL_COMMENTS])
|
|
31
|
+
doc.ensure_rel("word/document.xml", kit.REL_COMMENTS, "comments.xml")
|
|
32
|
+
return doc.xml("word/comments.xml")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def initials(name: str) -> str:
|
|
36
|
+
parts = [piece for piece in name.replace(".", " ").split() if piece]
|
|
37
|
+
return "".join(piece[0] for piece in parts)[:3].upper() or "OR"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def append_comment(root, text: str, author: str, stamp: str, ident: int) -> None:
|
|
41
|
+
comment = etree.SubElement(root, kit.q("comment"))
|
|
42
|
+
comment.set(kit.q("id"), str(ident))
|
|
43
|
+
comment.set(kit.q("author"), author)
|
|
44
|
+
comment.set(kit.q("initials"), initials(author))
|
|
45
|
+
comment.set(kit.q("date"), stamp)
|
|
46
|
+
paragraph = etree.SubElement(comment, kit.q("p"))
|
|
47
|
+
run = etree.SubElement(paragraph, kit.q("r"))
|
|
48
|
+
node = etree.SubElement(run, kit.q("t"))
|
|
49
|
+
node.text = text
|
|
50
|
+
node.set(kit.q("space", kit.XML), "preserve")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def anchor(paragraph, ident: int) -> None:
|
|
54
|
+
start = etree.Element(kit.q("commentRangeStart"))
|
|
55
|
+
start.set(kit.q("id"), str(ident))
|
|
56
|
+
position = 1 if paragraph.find(kit.q("pPr")) is not None else 0
|
|
57
|
+
paragraph.insert(position, start)
|
|
58
|
+
end = etree.SubElement(paragraph, kit.q("commentRangeEnd"))
|
|
59
|
+
end.set(kit.q("id"), str(ident))
|
|
60
|
+
run = etree.SubElement(paragraph, kit.q("r"))
|
|
61
|
+
ref = etree.SubElement(run, kit.q("commentReference"))
|
|
62
|
+
ref.set(kit.q("id"), str(ident))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_add(raw: str) -> tuple[str, str]:
|
|
66
|
+
if "=" not in raw:
|
|
67
|
+
raise ValueError("--add expects NEEDLE=COMMENT, got %r" % raw)
|
|
68
|
+
needle, body = raw.split("=", 1)
|
|
69
|
+
if not needle or not body:
|
|
70
|
+
raise ValueError("--add needs a non-empty needle and comment")
|
|
71
|
+
return needle, body
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main(argv: list[str] | None = None) -> int:
|
|
75
|
+
parser = argparse.ArgumentParser(description="Add Word comments over paragraphs matched by substring.")
|
|
76
|
+
parser.add_argument("in_docx", help="Input .docx")
|
|
77
|
+
parser.add_argument("--out", required=True, help="Output .docx")
|
|
78
|
+
parser.add_argument("--author", default="dsh-office", help="Comment author")
|
|
79
|
+
parser.add_argument("--add", action="append", default=[], help="NEEDLE=COMMENT (repeatable)")
|
|
80
|
+
parser.add_argument("--ignore_case", action="store_true", help="Match needle case-insensitively")
|
|
81
|
+
parser.add_argument("--require_all", action="store_true", help="Fail without writing if any needle is absent")
|
|
82
|
+
parser.add_argument("--date", default=None, help="ISO timestamp for the comments (default: now, UTC)")
|
|
83
|
+
args = parser.parse_args(argv)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
doc = kit.Docx(args.in_docx)
|
|
87
|
+
except (OSError, KeyError) as exc:
|
|
88
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
89
|
+
return 2
|
|
90
|
+
if not args.add:
|
|
91
|
+
parser.error("at least one --add NEEDLE=COMMENT is required")
|
|
92
|
+
|
|
93
|
+
paragraphs = doc.paragraphs()
|
|
94
|
+
requests = []
|
|
95
|
+
for raw in args.add:
|
|
96
|
+
try:
|
|
97
|
+
needle, body = parse_add(raw)
|
|
98
|
+
except ValueError as exc:
|
|
99
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
100
|
+
return 2
|
|
101
|
+
target = None
|
|
102
|
+
for paragraph in paragraphs:
|
|
103
|
+
haystack = kit.paragraph_text(paragraph)
|
|
104
|
+
if (needle.lower() in haystack.lower()) if args.ignore_case else (needle in haystack):
|
|
105
|
+
target = paragraph
|
|
106
|
+
break
|
|
107
|
+
requests.append({"needle": needle, "body": body, "paragraph": target})
|
|
108
|
+
|
|
109
|
+
missing = [item["needle"] for item in requests if item["paragraph"] is None]
|
|
110
|
+
if missing and args.require_all:
|
|
111
|
+
kit.emit({"ok": False, "output": None, "matched": 0, "missing": missing})
|
|
112
|
+
return 1
|
|
113
|
+
|
|
114
|
+
stamp = args.date or datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
115
|
+
root = comments_root(doc)
|
|
116
|
+
ident = doc.next_id()
|
|
117
|
+
placed = []
|
|
118
|
+
for item in requests:
|
|
119
|
+
if item["paragraph"] is None:
|
|
120
|
+
continue
|
|
121
|
+
append_comment(root, item["body"], args.author, stamp, ident)
|
|
122
|
+
anchor(item["paragraph"], ident)
|
|
123
|
+
placed.append({"id": ident, "needle": item["needle"], "body": item["body"]})
|
|
124
|
+
ident += 1
|
|
125
|
+
|
|
126
|
+
if not placed:
|
|
127
|
+
kit.emit({"ok": False, "output": None, "matched": 0, "missing": missing})
|
|
128
|
+
return 1
|
|
129
|
+
|
|
130
|
+
doc.save(args.out)
|
|
131
|
+
kit.emit({"ok": True, "output": str(Path(args.out)), "author": args.author, "placed": placed, "missing": missing})
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
raise SystemExit(main())
|