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,153 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Build deterministic internal navigation: a linked contents list and anchors.
|
|
3
|
+
|
|
4
|
+
Word's TOC field needs a refresh before its links work. This script instead
|
|
5
|
+
writes static hyperlinks that any renderer follows: bookmarks on every heading,
|
|
6
|
+
a contents list at the top (with quick links and back links), and optional
|
|
7
|
+
Top/Bottom anchors. Because the entries are literal text there is nothing to
|
|
8
|
+
update before the file is read.
|
|
9
|
+
|
|
10
|
+
Exit codes: 0 success, 1 no headings, 2 bad input.
|
|
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
|
+
TOP_BOOKMARK = "nav_top"
|
|
32
|
+
BOTTOM_BOOKMARK = "nav_bottom"
|
|
33
|
+
TOC_BOOKMARK = "nav_toc"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def link_run(text: str, anchor: str):
|
|
37
|
+
link = etree.Element(kit.q("hyperlink"))
|
|
38
|
+
link.set(kit.q("anchor"), anchor)
|
|
39
|
+
link.append(kit.make_run(text))
|
|
40
|
+
return link
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def add_bookmark(paragraph, name: str, ident: int) -> None:
|
|
44
|
+
start = etree.Element(kit.q("bookmarkStart"))
|
|
45
|
+
start.set(kit.q("id"), str(ident))
|
|
46
|
+
start.set(kit.q("name"), name)
|
|
47
|
+
end = etree.Element(kit.q("bookmarkEnd"))
|
|
48
|
+
end.set(kit.q("id"), str(ident))
|
|
49
|
+
position = 1 if paragraph.find(kit.q("pPr")) is not None else 0
|
|
50
|
+
paragraph.insert(position, start)
|
|
51
|
+
paragraph.append(end)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def paragraph(style: str | None = None):
|
|
55
|
+
node = etree.Element(kit.q("p"))
|
|
56
|
+
if style:
|
|
57
|
+
props = etree.SubElement(node, kit.q("pPr"))
|
|
58
|
+
etree.SubElement(props, kit.q("pStyle")).set(kit.q("val"), style)
|
|
59
|
+
return node
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main(argv: list[str] | None = None) -> int:
|
|
63
|
+
parser = argparse.ArgumentParser(description="Add bookmark anchors and static internal links to headings.")
|
|
64
|
+
parser.add_argument("input_docx", help="Input .docx")
|
|
65
|
+
parser.add_argument("--out", required=True, help="Output .docx")
|
|
66
|
+
parser.add_argument("--toc_title", default="Table of Contents")
|
|
67
|
+
parser.add_argument("--levels", nargs="+", type=int, default=[1, 2, 3], help="Heading levels to include")
|
|
68
|
+
parser.add_argument("--no_quicklinks", action="store_true", help="Skip the quick links bar")
|
|
69
|
+
parser.add_argument("--no_back_to_toc", action="store_true", help="Skip per-heading back links")
|
|
70
|
+
parser.add_argument("--no_top_bottom", action="store_true", help="Skip Top/Bottom anchors")
|
|
71
|
+
args = parser.parse_args(argv)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
doc = kit.Docx(args.input_docx)
|
|
75
|
+
except (OSError, KeyError) as exc:
|
|
76
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
77
|
+
return 2
|
|
78
|
+
|
|
79
|
+
body = doc.body()
|
|
80
|
+
headings = []
|
|
81
|
+
ident = doc.next_id()
|
|
82
|
+
for paragraph_node in list(body.iter(kit.q("p"))):
|
|
83
|
+
level = kit.heading_level(paragraph_node)
|
|
84
|
+
text = kit.paragraph_text(paragraph_node).strip()
|
|
85
|
+
if level in args.levels and text:
|
|
86
|
+
name = "nav_h%d" % ident
|
|
87
|
+
add_bookmark(paragraph_node, name, ident)
|
|
88
|
+
headings.append({"level": level, "text": text, "anchor": name})
|
|
89
|
+
ident += 1
|
|
90
|
+
|
|
91
|
+
if not headings:
|
|
92
|
+
print("error: document has no headings at levels %s" % args.levels, file=sys.stderr)
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
first = body.find(kit.q("p"))
|
|
96
|
+
if first is None:
|
|
97
|
+
first = etree.SubElement(body, kit.q("p"))
|
|
98
|
+
position = body.index(first)
|
|
99
|
+
|
|
100
|
+
block = []
|
|
101
|
+
if not args.no_top_bottom:
|
|
102
|
+
top = paragraph()
|
|
103
|
+
add_bookmark(top, TOP_BOOKMARK, ident)
|
|
104
|
+
ident += 1
|
|
105
|
+
block.append(top)
|
|
106
|
+
|
|
107
|
+
title = paragraph("Heading1")
|
|
108
|
+
add_bookmark(title, TOC_BOOKMARK, ident)
|
|
109
|
+
ident += 1
|
|
110
|
+
title.append(kit.make_run(args.toc_title))
|
|
111
|
+
block.append(title)
|
|
112
|
+
|
|
113
|
+
if not args.no_quicklinks:
|
|
114
|
+
quick = paragraph()
|
|
115
|
+
quick.append(kit.make_run("Quick links: "))
|
|
116
|
+
for index, item in enumerate([entry for entry in headings if entry["level"] == 1] or headings):
|
|
117
|
+
if index:
|
|
118
|
+
quick.append(kit.make_run(" | "))
|
|
119
|
+
quick.append(link_run(item["text"], item["anchor"]))
|
|
120
|
+
block.append(quick)
|
|
121
|
+
|
|
122
|
+
for item in headings:
|
|
123
|
+
entry = paragraph("TOC%d" % item["level"] if item["level"] <= 3 else "TOC3")
|
|
124
|
+
props = entry.find(kit.q("pPr"))
|
|
125
|
+
ind = etree.SubElement(props, kit.q("ind"))
|
|
126
|
+
ind.set(kit.q("left"), str((item["level"] - 1) * 360))
|
|
127
|
+
entry.append(link_run(item["text"], item["anchor"]))
|
|
128
|
+
block.append(entry)
|
|
129
|
+
|
|
130
|
+
for offset, node in enumerate(block):
|
|
131
|
+
body.insert(position + offset, node)
|
|
132
|
+
|
|
133
|
+
if not args.no_back_to_toc:
|
|
134
|
+
for paragraph_node in list(body.iter(kit.q("p"))):
|
|
135
|
+
if any(entry["anchor"] == name for entry in headings for name in (paragraph_node.find(kit.q("bookmarkStart")).get(kit.q("name")),) if paragraph_node.find(kit.q("bookmarkStart")) is not None):
|
|
136
|
+
back = paragraph("Caption")
|
|
137
|
+
back.append(link_run("Back to contents", TOC_BOOKMARK))
|
|
138
|
+
paragraph_node.addnext(back)
|
|
139
|
+
|
|
140
|
+
if not args.no_top_bottom:
|
|
141
|
+
last = paragraph()
|
|
142
|
+
add_bookmark(last, BOTTOM_BOOKMARK, ident)
|
|
143
|
+
ident += 1
|
|
144
|
+
last.append(link_run("Back to top", TOP_BOOKMARK))
|
|
145
|
+
body.append(last)
|
|
146
|
+
|
|
147
|
+
doc.save(args.out)
|
|
148
|
+
kit.emit({"ok": True, "output": str(Path(args.out)), "headings": headings, "entries": len(block)})
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Generate small documents that exercise the awkward parts of the format.
|
|
3
|
+
|
|
4
|
+
Reviewing comments, tracked changes or watermarks is impossible without files
|
|
5
|
+
that contain them, and hand-building those by clicking in Word is not repeatable.
|
|
6
|
+
Each fixture targets one feature so a script's report can be judged against a
|
|
7
|
+
known-clean or known-broken input.
|
|
8
|
+
|
|
9
|
+
Exit codes: 0 success, 2 bad input.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
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
|
+
from docx import Document
|
|
25
|
+
from docx.shared import Inches
|
|
26
|
+
|
|
27
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
28
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
29
|
+
try:
|
|
30
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
31
|
+
except (AttributeError, ValueError):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def base_document(title: str, paragraphs: list[str]) -> Document:
|
|
36
|
+
document = Document()
|
|
37
|
+
document.add_heading(title, level=1)
|
|
38
|
+
for text in paragraphs:
|
|
39
|
+
document.add_paragraph(text)
|
|
40
|
+
return document
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def mark_paragraph(paragraph, before: str, deleted: str, inserted: str, after: str, author: str, date: str) -> None:
|
|
44
|
+
rpr = kit.first_rpr(paragraph)
|
|
45
|
+
kit.clear_paragraph_content(paragraph)
|
|
46
|
+
paragraph.append(kit.make_run(before, etree.fromstring(etree.tostring(rpr)) if rpr is not None else None))
|
|
47
|
+
deletion = etree.SubElement(paragraph, kit.q("del"))
|
|
48
|
+
deletion.set(kit.q("id"), "101")
|
|
49
|
+
deletion.set(kit.q("author"), author)
|
|
50
|
+
deletion.set(kit.q("date"), date)
|
|
51
|
+
deletion.append(kit.make_run(deleted, tag="delText"))
|
|
52
|
+
insertion = etree.SubElement(paragraph, kit.q("ins"))
|
|
53
|
+
insertion.set(kit.q("id"), "102")
|
|
54
|
+
insertion.set(kit.q("author"), author)
|
|
55
|
+
insertion.set(kit.q("date"), date)
|
|
56
|
+
insertion.append(kit.make_run(inserted))
|
|
57
|
+
paragraph.append(kit.make_run(after))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def make_tracked(target: Path) -> dict:
|
|
61
|
+
document = base_document("Revisions demo", ["Regional revenue grew by 12% in the quarter.", "Hardware sales stayed flat."])
|
|
62
|
+
document.save(target)
|
|
63
|
+
doc = kit.Docx(target)
|
|
64
|
+
paragraphs = doc.paragraphs()
|
|
65
|
+
mark_paragraph(paragraphs[1], "Regional revenue grew by ", "12%", "14%", " in the quarter.", "Reviewer", "2024-05-01T09:00:00Z")
|
|
66
|
+
mark_paragraph(paragraphs[2], "Hardware sales ", "stayed", "remained", " flat.", "Reviewer", "2024-05-01T09:01:00Z")
|
|
67
|
+
settings = doc.xml("word/settings.xml")
|
|
68
|
+
if settings.find(kit.q("trackRevisions")) is None:
|
|
69
|
+
settings.append(settings.makeelement(kit.q("trackRevisions"), {}))
|
|
70
|
+
doc.save(target)
|
|
71
|
+
return {"path": str(target), "tracked_changes": 4}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def make_watermark(target: Path) -> dict:
|
|
75
|
+
document = base_document("Watermark demo", ["This copy is for internal review."])
|
|
76
|
+
document.save(target)
|
|
77
|
+
doc = kit.Docx(target)
|
|
78
|
+
part = doc.ensure_header_footer("header")
|
|
79
|
+
doc.xml(part).append(kit.watermark_paragraph("DRAFT", doc.next_id()))
|
|
80
|
+
doc.save(target)
|
|
81
|
+
return {"path": str(target), "watermark": "DRAFT"}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def make_comments(target: Path) -> dict:
|
|
85
|
+
document = base_document("Comments demo", ["The first paragraph carries a question.", "The second paragraph is clean."])
|
|
86
|
+
document.save(target)
|
|
87
|
+
doc = kit.Docx(target)
|
|
88
|
+
root = etree.Element(kit.q("comments"), nsmap={"w": kit.W, "r": kit.R})
|
|
89
|
+
doc.set_xml("word/comments.xml", root)
|
|
90
|
+
doc.ensure_override("word/comments.xml", kit.CT_BY_REL[kit.REL_COMMENTS])
|
|
91
|
+
doc.ensure_rel("word/document.xml", kit.REL_COMMENTS, "comments.xml")
|
|
92
|
+
comment = etree.SubElement(root, kit.q("comment"))
|
|
93
|
+
comment.set(kit.q("id"), "1")
|
|
94
|
+
comment.set(kit.q("author"), "Reviewer")
|
|
95
|
+
comment.set(kit.q("initials"), "RV")
|
|
96
|
+
comment.set(kit.q("date"), "2024-05-02T10:00:00Z")
|
|
97
|
+
body = etree.SubElement(comment, kit.q("p"))
|
|
98
|
+
etree.SubElement(etree.SubElement(body, kit.q("r")), kit.q("t")).text = "Is this number current?"
|
|
99
|
+
|
|
100
|
+
paragraph = doc.paragraphs()[1]
|
|
101
|
+
start = etree.Element(kit.q("commentRangeStart"))
|
|
102
|
+
start.set(kit.q("id"), "1")
|
|
103
|
+
paragraph.insert(1, start)
|
|
104
|
+
end = etree.SubElement(paragraph, kit.q("commentRangeEnd"))
|
|
105
|
+
end.set(kit.q("id"), "1")
|
|
106
|
+
run = etree.SubElement(paragraph, kit.q("r"))
|
|
107
|
+
etree.SubElement(run, kit.q("commentReference")).set(kit.q("id"), "1")
|
|
108
|
+
doc.save(target)
|
|
109
|
+
return {"path": str(target), "comments": 1}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def make_edge_cases(target: Path, image_dir: Path) -> dict:
|
|
113
|
+
image_path = image_dir / "sample.png"
|
|
114
|
+
try:
|
|
115
|
+
from PIL import Image
|
|
116
|
+
|
|
117
|
+
Image.new("RGB", (320, 200), "#4472c4").save(image_path)
|
|
118
|
+
except ImportError:
|
|
119
|
+
image_path = None
|
|
120
|
+
|
|
121
|
+
document = Document()
|
|
122
|
+
document.add_heading("Edge cases", level=1)
|
|
123
|
+
document.add_paragraph("")
|
|
124
|
+
document.add_heading("A level three heading after a level one", level=3)
|
|
125
|
+
document.add_paragraph("A very long paragraph " * 40)
|
|
126
|
+
document.add_paragraph("Contact: writer@example.org or +1 555 010 9999.")
|
|
127
|
+
table = document.add_table(rows=2, cols=10)
|
|
128
|
+
for row_index in range(2):
|
|
129
|
+
for column_index in range(10):
|
|
130
|
+
table.cell(row_index, column_index).text = "R%dC%d" % (row_index + 1, column_index + 1)
|
|
131
|
+
if image_path is not None:
|
|
132
|
+
document.add_picture(str(image_path), width=Inches(4))
|
|
133
|
+
document.save(target)
|
|
134
|
+
return {"path": str(target), "fixtures": ["empty_paragraph", "skipped_heading_level", "wide_table", "image_without_alt", "contact_details"]}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
BUILDERS = {
|
|
138
|
+
"tracked": make_tracked,
|
|
139
|
+
"watermark": make_watermark,
|
|
140
|
+
"comments": make_comments,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def main(argv: list[str] | None = None) -> int:
|
|
145
|
+
parser = argparse.ArgumentParser(description="Create sample .docx files for tracked changes, watermarks, comments and edge cases.")
|
|
146
|
+
parser.add_argument("--outdir", required=True, help="Directory for the generated files")
|
|
147
|
+
parser.add_argument("--only", default="all", help="tracked, watermark, comments, edge, or all")
|
|
148
|
+
args = parser.parse_args(argv)
|
|
149
|
+
|
|
150
|
+
out_dir = Path(args.outdir).expanduser().resolve()
|
|
151
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
wanted = [name.strip() for name in args.only.split(",") if name.strip()]
|
|
153
|
+
if "all" in wanted:
|
|
154
|
+
wanted = ["tracked", "watermark", "comments", "edge"]
|
|
155
|
+
unknown = [name for name in wanted if name not in BUILDERS and name != "edge"]
|
|
156
|
+
if unknown:
|
|
157
|
+
print("error: unknown fixture %s" % ", ".join(unknown), file=sys.stderr)
|
|
158
|
+
return 2
|
|
159
|
+
|
|
160
|
+
results = []
|
|
161
|
+
try:
|
|
162
|
+
for name in wanted:
|
|
163
|
+
if name == "edge":
|
|
164
|
+
results.append(make_edge_cases(out_dir / "edge_cases.docx", out_dir))
|
|
165
|
+
else:
|
|
166
|
+
results.append(BUILDERS[name](out_dir / ("%s.docx" % name)))
|
|
167
|
+
except (OSError, KeyError) as exc:
|
|
168
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
169
|
+
return 2
|
|
170
|
+
|
|
171
|
+
kit.emit({"ok": True, "outdir": str(out_dir), "fixtures": results})
|
|
172
|
+
return 0
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Append one document's body to another, carrying its parts across.
|
|
3
|
+
|
|
4
|
+
Merging by pasting XML is the easy half; the hard half is that the second file's
|
|
5
|
+
relationships point at its own media, numbering and styles. This script copies
|
|
6
|
+
those parts into the base under free names, remaps relationship ids and
|
|
7
|
+
numbering ids, and brings over any style the appended content needs.
|
|
8
|
+
|
|
9
|
+
Drawings are refused by default: a nested drawing can carry its own rel chain and
|
|
10
|
+
silently break a merge, so it has to be asked for explicitly.
|
|
11
|
+
|
|
12
|
+
Exit codes: 0 success, 1 refused (drawings present, incompatible input), 2 bad input.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import posixpath
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from lxml import etree
|
|
23
|
+
|
|
24
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
25
|
+
import docx_kit as kit
|
|
26
|
+
|
|
27
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
28
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
29
|
+
try:
|
|
30
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
31
|
+
except (AttributeError, ValueError):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
CT = "{http://schemas.openxmlformats.org/package/2006/content-types}"
|
|
35
|
+
W_NS = "{http://schemas.openxmlformats.org/package/2006/relationships}"
|
|
36
|
+
REL_NUMBERING = kit.REL_NUMBERING
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def free_part_name(base: kit.Docx, source: str) -> str:
|
|
40
|
+
folder = posixpath.dirname(source) or "word/media"
|
|
41
|
+
stem, extension = posixpath.splitext(posixpath.basename(source))
|
|
42
|
+
candidate = posixpath.join(folder, stem + extension)
|
|
43
|
+
counter = 1
|
|
44
|
+
while base.exists(candidate):
|
|
45
|
+
candidate = posixpath.join(folder, "%s_copy%d%s" % (stem, counter, extension))
|
|
46
|
+
counter += 1
|
|
47
|
+
return candidate
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def copy_part(base: kit.Docx, append: kit.Docx, source: str) -> str:
|
|
51
|
+
target = free_part_name(base, source)
|
|
52
|
+
base.parts[target] = append.parts[source]
|
|
53
|
+
base.order.append(target)
|
|
54
|
+
base._cache.pop(target, None)
|
|
55
|
+
for node in append.content_types().findall(CT + "Override"):
|
|
56
|
+
if node.get("PartName") == "/" + source:
|
|
57
|
+
base.ensure_override(target, node.get("ContentType"))
|
|
58
|
+
break
|
|
59
|
+
return target
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def copy_defaults(base: kit.Docx, append: kit.Docx) -> None:
|
|
63
|
+
existing = {(node.get("Extension") or "").lower() for node in base.content_types().findall(CT + "Default")}
|
|
64
|
+
for node in append.content_types().findall(CT + "Default"):
|
|
65
|
+
extension = (node.get("Extension") or "").lower()
|
|
66
|
+
if extension and extension not in existing:
|
|
67
|
+
clone = etree.SubElement(base.content_types(), CT + "Default")
|
|
68
|
+
clone.set("Extension", node.get("Extension"))
|
|
69
|
+
clone.set("ContentType", node.get("ContentType"))
|
|
70
|
+
existing.add(extension)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def ensure_style(base: kit.Docx, append: kit.Docx, wanted: str) -> bool:
|
|
74
|
+
if not append.exists("word/styles.xml") or not wanted:
|
|
75
|
+
return False
|
|
76
|
+
base_styles = base.xml("word/styles.xml")
|
|
77
|
+
present = {node.get(kit.q("styleId")) for node in base_styles.findall(kit.q("style"))}
|
|
78
|
+
if wanted in present:
|
|
79
|
+
return False
|
|
80
|
+
source_styles = {node.get(kit.q("styleId")): node for node in append.xml("word/styles.xml").findall(kit.q("style"))}
|
|
81
|
+
pending = [wanted]
|
|
82
|
+
copied = False
|
|
83
|
+
while pending:
|
|
84
|
+
style_id = pending.pop()
|
|
85
|
+
node = source_styles.get(style_id)
|
|
86
|
+
if node is None or style_id in present:
|
|
87
|
+
continue
|
|
88
|
+
clone = etree.fromstring(etree.tostring(node))
|
|
89
|
+
base_styles.append(clone)
|
|
90
|
+
present.add(style_id)
|
|
91
|
+
copied = True
|
|
92
|
+
for link_tag in ("basedOn", "next", "link"):
|
|
93
|
+
ref = node.find(kit.q(link_tag))
|
|
94
|
+
if ref is not None and ref.get(kit.q("val")):
|
|
95
|
+
pending.append(ref.get(kit.q("val")))
|
|
96
|
+
return copied
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def remap_numbering(base: kit.Docx, append: kit.Docx, nodes: list) -> dict[str, str]:
|
|
100
|
+
used = set()
|
|
101
|
+
for node in nodes:
|
|
102
|
+
for num_pr in node.iter(kit.q("numPr")):
|
|
103
|
+
num_id = num_pr.find(kit.q("numId"))
|
|
104
|
+
if num_id is not None and num_id.get(kit.q("val")):
|
|
105
|
+
used.add(num_id.get(kit.q("val")))
|
|
106
|
+
if not used or not append.exists("word/numbering.xml"):
|
|
107
|
+
return {}
|
|
108
|
+
if not base.exists("word/numbering.xml"):
|
|
109
|
+
base.parts["word/numbering.xml"] = append.parts["word/numbering.xml"]
|
|
110
|
+
base.order.append("word/numbering.xml")
|
|
111
|
+
base.ensure_override("word/numbering.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml")
|
|
112
|
+
base.ensure_rel("word/document.xml", kit.REL_NUMBERING, "numbering.xml")
|
|
113
|
+
return {}
|
|
114
|
+
|
|
115
|
+
base_root = base.xml("word/numbering.xml")
|
|
116
|
+
source_root = append.xml("word/numbering.xml")
|
|
117
|
+
base_nums = {node.get(kit.q("numId")): node for node in base_root.findall(kit.q("num"))}
|
|
118
|
+
base_abstract = {node.get(kit.q("abstractNumId")): node for node in base_root.findall(kit.q("abstractNum"))}
|
|
119
|
+
next_num = max([int(value) for value in base_nums if (value or "").isdigit()], default=0) + 1
|
|
120
|
+
next_abstract = max([int(value) for value in base_abstract if (value or "").isdigit()], default=0) + 1
|
|
121
|
+
source_nums = {node.get(kit.q("numId")): node for node in source_root.findall(kit.q("num"))}
|
|
122
|
+
source_abstract = {node.get(kit.q("abstractNumId")): node for node in source_root.findall(kit.q("abstractNum"))}
|
|
123
|
+
mapping = {}
|
|
124
|
+
for old in used:
|
|
125
|
+
node = source_nums.get(old)
|
|
126
|
+
if node is None:
|
|
127
|
+
continue
|
|
128
|
+
clone = etree.fromstring(etree.tostring(node))
|
|
129
|
+
reference = clone.find(kit.q("abstractNumId"))
|
|
130
|
+
abstract_old = reference.get(kit.q("val")) if reference is not None else None
|
|
131
|
+
if abstract_old in source_abstract and abstract_old not in base_abstract:
|
|
132
|
+
abstract_clone = etree.fromstring(etree.tostring(source_abstract[abstract_old]))
|
|
133
|
+
abstract_clone.set(kit.q("abstractNumId"), str(next_abstract))
|
|
134
|
+
base_root.insert(0, abstract_clone)
|
|
135
|
+
base_abstract[str(next_abstract)] = abstract_clone
|
|
136
|
+
next_abstract += 1
|
|
137
|
+
if reference is not None:
|
|
138
|
+
reference.set(kit.q("val"), abstract_clone.get(kit.q("abstractNumId")))
|
|
139
|
+
clone.set(kit.q("numId"), str(next_num))
|
|
140
|
+
mapping[old] = str(next_num)
|
|
141
|
+
base_root.append(clone)
|
|
142
|
+
base_nums[str(next_num)] = clone
|
|
143
|
+
next_num += 1
|
|
144
|
+
return mapping
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def remap_relationships(base: kit.Docx, append: kit.Docx, nodes: list) -> int:
|
|
148
|
+
mapping = {}
|
|
149
|
+
copied = 0
|
|
150
|
+
source_rels = {entry["id"]: entry for entry in append.relationships("word/document.xml")}
|
|
151
|
+
attributes = (kit.q("embed", kit.R), kit.q("id", kit.R), kit.q("link", kit.R))
|
|
152
|
+
for node in nodes:
|
|
153
|
+
for element in node.iter():
|
|
154
|
+
for attribute in attributes:
|
|
155
|
+
rid = element.get(attribute)
|
|
156
|
+
if not rid:
|
|
157
|
+
continue
|
|
158
|
+
if rid not in mapping:
|
|
159
|
+
rel = source_rels.get(rid)
|
|
160
|
+
if rel is None:
|
|
161
|
+
mapping[rid] = rid
|
|
162
|
+
continue
|
|
163
|
+
if rel["mode"] == "External":
|
|
164
|
+
mapping[rid] = base.ensure_rel("word/document.xml", rel["type"], rel["target"], external=True)
|
|
165
|
+
elif append.exists(rel["resolved"]):
|
|
166
|
+
target = copy_part(base, append, rel["resolved"])
|
|
167
|
+
relative = posixpath.relpath(target, "word")
|
|
168
|
+
mapping[rid] = base.ensure_rel("word/document.xml", rel["type"], relative)
|
|
169
|
+
copied += 1
|
|
170
|
+
else:
|
|
171
|
+
mapping[rid] = rid
|
|
172
|
+
element.set(attribute, mapping[rid])
|
|
173
|
+
return copied
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def main(argv: list[str] | None = None) -> int:
|
|
177
|
+
parser = argparse.ArgumentParser(description="Append one .docx body to another, copying media, numbering and styles.")
|
|
178
|
+
parser.add_argument("base", help="Document to append to")
|
|
179
|
+
parser.add_argument("append", help="Document whose body is copied in")
|
|
180
|
+
parser.add_argument("--out", required=True, help="Output .docx")
|
|
181
|
+
parser.add_argument("--allow_drawings", action="store_true", help="Copy drawings and their relationships")
|
|
182
|
+
args = parser.parse_args(argv)
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
base = kit.Docx(args.base)
|
|
186
|
+
append = kit.Docx(args.append)
|
|
187
|
+
except (OSError, KeyError) as exc:
|
|
188
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
189
|
+
return 2
|
|
190
|
+
|
|
191
|
+
append_body = append.body()
|
|
192
|
+
incoming = [child for child in append_body if child.tag != kit.q("sectPr")]
|
|
193
|
+
if not incoming:
|
|
194
|
+
print("error: %s has an empty body" % args.append, file=sys.stderr)
|
|
195
|
+
return 2
|
|
196
|
+
|
|
197
|
+
has_drawings = any(child.find(".//" + tag) is not None for child in incoming for tag in (kit.q("drawing"), kit.q("pict")))
|
|
198
|
+
if has_drawings and not args.allow_drawings:
|
|
199
|
+
print("error: appended body contains drawings; re-run with --allow_drawings", file=sys.stderr)
|
|
200
|
+
return 1
|
|
201
|
+
|
|
202
|
+
style_ids = set()
|
|
203
|
+
numbering_nodes = []
|
|
204
|
+
for child in incoming:
|
|
205
|
+
for node in child.iter():
|
|
206
|
+
for tag in ("pStyle", "rStyle", "tblStyle"):
|
|
207
|
+
if node.tag == kit.q(tag) and node.get(kit.q("val")):
|
|
208
|
+
style_ids.add(node.get(kit.q("val")))
|
|
209
|
+
if child.find(".//" + kit.q("numPr")) is not None:
|
|
210
|
+
numbering_nodes.append(child)
|
|
211
|
+
|
|
212
|
+
styles_copied = sum(1 for style_id in sorted(style_ids) if ensure_style(base, append, style_id))
|
|
213
|
+
number_map = remap_numbering(base, append, numbering_nodes)
|
|
214
|
+
|
|
215
|
+
clones = [etree.fromstring(etree.tostring(child)) for child in incoming]
|
|
216
|
+
if number_map:
|
|
217
|
+
for clone in clones:
|
|
218
|
+
for num_pr in clone.iter(kit.q("numPr")):
|
|
219
|
+
num_id = num_pr.find(kit.q("numId"))
|
|
220
|
+
if num_id is not None and num_id.get(kit.q("val")) in number_map:
|
|
221
|
+
num_id.set(kit.q("val"), number_map[num_id.get(kit.q("val"))])
|
|
222
|
+
media_copied = remap_relationships(base, append, clones)
|
|
223
|
+
copy_defaults(base, append)
|
|
224
|
+
|
|
225
|
+
body = base.body()
|
|
226
|
+
sect = body.find(kit.q("sectPr"))
|
|
227
|
+
anchor = body.index(sect) if sect is not None else len(body)
|
|
228
|
+
for offset, clone in enumerate(clones):
|
|
229
|
+
body.insert(anchor + offset, clone)
|
|
230
|
+
|
|
231
|
+
base.save(args.out)
|
|
232
|
+
kit.emit(
|
|
233
|
+
{
|
|
234
|
+
"ok": True,
|
|
235
|
+
"output": str(Path(args.out)),
|
|
236
|
+
"appended_elements": len(clones),
|
|
237
|
+
"styles_copied": styles_copied,
|
|
238
|
+
"media_copied": media_copied,
|
|
239
|
+
"numbering_remapped": len(number_map),
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
return 0
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == "__main__":
|
|
246
|
+
raise SystemExit(main())
|