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,103 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Undo the heading damage left by importing a document from a web editor.
|
|
3
|
+
|
|
4
|
+
Imports commonly arrive with a run of empty paragraphs before the body starts and
|
|
5
|
+
with the document title duplicated as a level-1 heading, which makes the outline
|
|
6
|
+
read "My Doc" twice and pushes the real first section down. The cleanup only
|
|
7
|
+
touches the leading region, so a deliberate title page further in is untouched.
|
|
8
|
+
|
|
9
|
+
Exit codes: 0 changed or nothing to change, 1 --check found problems, 2 bad input.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
19
|
+
import docx_kit as kit
|
|
20
|
+
|
|
21
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
22
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
23
|
+
try:
|
|
24
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
25
|
+
except (AttributeError, ValueError):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def leading_region(body, limit: int) -> list:
|
|
30
|
+
seen = 0
|
|
31
|
+
region = []
|
|
32
|
+
for child in body:
|
|
33
|
+
if child.tag == kit.q("p") and kit.paragraph_text(child).strip():
|
|
34
|
+
seen += 1
|
|
35
|
+
if seen >= limit:
|
|
36
|
+
region.append(child)
|
|
37
|
+
break
|
|
38
|
+
region.append(child)
|
|
39
|
+
return region
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def sanitize(body, limit: int) -> dict:
|
|
43
|
+
removed_empty = 0
|
|
44
|
+
removed_duplicate = 0
|
|
45
|
+
region = leading_region(body, limit)
|
|
46
|
+
for paragraph in list(region):
|
|
47
|
+
if paragraph.tag != kit.q("p") or kit.paragraph_text(paragraph).strip():
|
|
48
|
+
continue
|
|
49
|
+
parent = paragraph.getparent()
|
|
50
|
+
if parent is not None:
|
|
51
|
+
parent.remove(paragraph)
|
|
52
|
+
removed_empty += 1
|
|
53
|
+
|
|
54
|
+
region = leading_region(body, limit)
|
|
55
|
+
for index, paragraph in enumerate(region):
|
|
56
|
+
if paragraph.tag != kit.q("p") or kit.paragraph_style(paragraph) != "Title":
|
|
57
|
+
continue
|
|
58
|
+
title = kit.paragraph_text(paragraph).strip()
|
|
59
|
+
for follower in region[index + 1 :]:
|
|
60
|
+
if follower.tag != kit.q("p"):
|
|
61
|
+
continue
|
|
62
|
+
text = kit.paragraph_text(follower).strip()
|
|
63
|
+
if not text:
|
|
64
|
+
continue
|
|
65
|
+
if text == title and kit.heading_level(follower) == 1:
|
|
66
|
+
body.remove(follower)
|
|
67
|
+
removed_duplicate += 1
|
|
68
|
+
break
|
|
69
|
+
return {"removed_empty": removed_empty, "removed_duplicate_heading": removed_duplicate}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main(argv: list[str] | None = None) -> int:
|
|
73
|
+
parser = argparse.ArgumentParser(description="Remove leading empty paragraphs and a duplicated title heading after import.")
|
|
74
|
+
parser.add_argument("input_docx", help="Input .docx")
|
|
75
|
+
parser.add_argument("--out", default=None, help="Output .docx")
|
|
76
|
+
parser.add_argument("--in-place", action="store_true", help="Rewrite the input file")
|
|
77
|
+
parser.add_argument("--check", action="store_true", help="Report only; exit 1 when cleanup is needed")
|
|
78
|
+
parser.add_argument("--leading-nonempty-paragraphs", type=int, default=3, help="How far into the document to look")
|
|
79
|
+
args = parser.parse_args(argv)
|
|
80
|
+
|
|
81
|
+
if not (args.out or args.in_place or args.check):
|
|
82
|
+
parser.error("choose --out, --in-place or --check")
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
doc = kit.Docx(args.input_docx)
|
|
86
|
+
except (OSError, KeyError) as exc:
|
|
87
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
88
|
+
return 2
|
|
89
|
+
|
|
90
|
+
result = sanitize(doc.body(), max(1, args.leading_nonempty_paragraphs))
|
|
91
|
+
changed = bool(result["removed_empty"] or result["removed_duplicate_heading"])
|
|
92
|
+
|
|
93
|
+
output = None
|
|
94
|
+
if args.in_place or args.out:
|
|
95
|
+
output = args.out or args.input_docx
|
|
96
|
+
doc.save(output)
|
|
97
|
+
|
|
98
|
+
kit.emit({"ok": not (args.check and changed), "changed": changed, "check": args.check, "output": str(Path(output)) if output else None, **result})
|
|
99
|
+
return 1 if (args.check and changed) else 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Audit the heading outline: skipped levels, empty headings, style escapes.
|
|
3
|
+
|
|
4
|
+
A reader who meets "Heading 3" straight after "Heading 1" loses the document's
|
|
5
|
+
shape, and a heading drawn with bold text instead of a heading style is invisible
|
|
6
|
+
to the navigation pane, the TOC field and every outline tool.
|
|
7
|
+
|
|
8
|
+
Exit codes: 0 clean, 1 findings, 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
|
+
BOLD_LIKE = ("Heading", "Title")
|
|
28
|
+
HEADING_SIZE_HALF_POINTS = 26 # 13pt: larger than any body style should be
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def manual_heading(paragraph) -> bool:
|
|
32
|
+
"""True when bold, oversized runs stand in for a heading style."""
|
|
33
|
+
for run in paragraph.iter(kit.q("r")):
|
|
34
|
+
props = run.find(kit.q("rPr"))
|
|
35
|
+
if props is None or props.find(kit.q("b")) is None:
|
|
36
|
+
continue
|
|
37
|
+
size = props.find(kit.q("sz"))
|
|
38
|
+
if size is not None and (size.get(kit.q("val")) or "0").isdigit() and int(size.get(kit.q("val"))) >= HEADING_SIZE_HALF_POINTS:
|
|
39
|
+
return True
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main(argv: list[str] | None = None) -> int:
|
|
44
|
+
parser = argparse.ArgumentParser(description="Report heading-level jumps, empty headings and manual heading formatting.")
|
|
45
|
+
parser.add_argument("docx", help="Input .docx")
|
|
46
|
+
parser.add_argument("--max_findings", type=int, default=50, help="Cap the reported findings")
|
|
47
|
+
args = parser.parse_args(argv)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
doc = kit.Docx(args.docx)
|
|
51
|
+
except (OSError, KeyError) as exc:
|
|
52
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
53
|
+
return 2
|
|
54
|
+
|
|
55
|
+
findings = []
|
|
56
|
+
outline = []
|
|
57
|
+
previous = 0
|
|
58
|
+
for index, paragraph in enumerate(doc.paragraphs(), start=1):
|
|
59
|
+
text = kit.paragraph_text(paragraph).strip()
|
|
60
|
+
level = kit.heading_level(paragraph)
|
|
61
|
+
if level == 0:
|
|
62
|
+
outline.append({"index": index, "style": "Title", "text": text})
|
|
63
|
+
continue
|
|
64
|
+
if level:
|
|
65
|
+
if not text:
|
|
66
|
+
findings.append({"kind": "empty_heading", "index": index, "level": level})
|
|
67
|
+
continue
|
|
68
|
+
outline.append({"index": index, "level": level, "text": text})
|
|
69
|
+
if previous and level > previous + 1:
|
|
70
|
+
findings.append({"kind": "skipped_level", "index": index, "from": previous, "to": level, "text": text})
|
|
71
|
+
if manual_heading(paragraph):
|
|
72
|
+
findings.append({"kind": "manual_formatting", "index": index, "text": text})
|
|
73
|
+
previous = level
|
|
74
|
+
elif not text:
|
|
75
|
+
continue
|
|
76
|
+
elif manual_heading(paragraph) and len(text) <= 120:
|
|
77
|
+
findings.append({"kind": "heading_not_styled", "index": index, "text": text})
|
|
78
|
+
|
|
79
|
+
counts = {}
|
|
80
|
+
for finding in findings:
|
|
81
|
+
counts[finding["kind"]] = counts.get(finding["kind"], 0) + 1
|
|
82
|
+
|
|
83
|
+
kit.emit({"ok": not findings, "counts": counts, "headings": len(outline), "findings": findings[: args.max_findings], "truncated": max(0, len(findings) - args.max_findings)})
|
|
84
|
+
return 1 if findings else 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Inventory images: where they come from, how big they are, and alt text.
|
|
3
|
+
|
|
4
|
+
Alt text lives on ``wp:docPr`` (``descr`` preferred, ``title`` accepted). Missing
|
|
5
|
+
alt text is the accessibility defect readers hit first, and an oversized image is
|
|
6
|
+
the layout defect authors hit first, so both are reported with the numbers that
|
|
7
|
+
need changing.
|
|
8
|
+
|
|
9
|
+
Exit codes: 0 clean, 1 findings, 2 bad input.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from lxml import etree
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
21
|
+
import docx_kit as kit
|
|
22
|
+
|
|
23
|
+
# 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
|
+
EMU_PER_INCH = 914400
|
|
31
|
+
MAX_WIDTH_INCHES = 8.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def part_for(rel_id: str, rels: dict) -> str | None:
|
|
35
|
+
entry = rels.get(rel_id)
|
|
36
|
+
return entry["resolved"] if entry else None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main(argv: list[str] | None = None) -> int:
|
|
40
|
+
parser = argparse.ArgumentParser(description="List inline images with size, source part and alt-text status.")
|
|
41
|
+
parser.add_argument("docx", help="Input .docx")
|
|
42
|
+
parser.add_argument("--max_rows", type=int, default=50)
|
|
43
|
+
args = parser.parse_args(argv)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
doc = kit.Docx(args.docx)
|
|
47
|
+
except (OSError, KeyError) as exc:
|
|
48
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
49
|
+
return 2
|
|
50
|
+
|
|
51
|
+
rows = []
|
|
52
|
+
findings = []
|
|
53
|
+
for part in ["word/document.xml"] + doc.names("word/header") + doc.names("word/footer"):
|
|
54
|
+
if not doc.exists(part):
|
|
55
|
+
continue
|
|
56
|
+
rels = {entry["id"]: entry for entry in doc.relationships(part)}
|
|
57
|
+
for doc_pr in doc.xml(part).iter(kit.q("docPr", kit.WP)):
|
|
58
|
+
blip = doc_pr.getparent().find(".//" + kit.q("blip", kit.A))
|
|
59
|
+
embed = blip.get(kit.q("embed", kit.R)) if blip is not None else None
|
|
60
|
+
extent = doc_pr.getparent().find(".//" + kit.q("extent", kit.WP))
|
|
61
|
+
width = int(extent.get("cx")) / EMU_PER_INCH if extent is not None and extent.get("cx") else 0.0
|
|
62
|
+
height = int(extent.get("cy")) / EMU_PER_INCH if extent is not None and extent.get("cy") else 0.0
|
|
63
|
+
alt = doc_pr.get("descr") or doc_pr.get("title") or ""
|
|
64
|
+
row = {
|
|
65
|
+
"part": part,
|
|
66
|
+
"name": doc_pr.get("name"),
|
|
67
|
+
"source": part_for(embed, rels) if embed else None,
|
|
68
|
+
"width_in": round(width, 2),
|
|
69
|
+
"height_in": round(height, 2),
|
|
70
|
+
"alt": alt or None,
|
|
71
|
+
}
|
|
72
|
+
rows.append(row)
|
|
73
|
+
if not alt.strip():
|
|
74
|
+
findings.append({"kind": "missing_alt", "part": part, "name": row["name"], "source": row["source"]})
|
|
75
|
+
if width > MAX_WIDTH_INCHES:
|
|
76
|
+
findings.append({"kind": "oversized", "part": part, "name": row["name"], "width_in": row["width_in"]})
|
|
77
|
+
|
|
78
|
+
counts = {}
|
|
79
|
+
for finding in findings:
|
|
80
|
+
counts[finding["kind"]] = counts.get(finding["kind"], 0) + 1
|
|
81
|
+
|
|
82
|
+
kit.emit({"ok": not findings, "images": len(rows), "counts": counts, "rows": rows[: args.max_rows], "findings": findings[: args.max_rows]})
|
|
83
|
+
return 1 if findings else 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Insert a real footnote or endnote at a text marker.
|
|
3
|
+
|
|
4
|
+
Word stores notes outside the body: the paragraph keeps a small reference run
|
|
5
|
+
and the note text goes in footnotes.xml/endnotes.xml. This script creates that
|
|
6
|
+
part when absent (including the required separator notes), allocates the next
|
|
7
|
+
id, and replaces the marker run with the reference at the exact position.
|
|
8
|
+
|
|
9
|
+
Exit codes: 0 success, 1 marker not found, 2 bad input.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from lxml import etree
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
21
|
+
import docx_kit as kit
|
|
22
|
+
|
|
23
|
+
# 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
|
+
PARTS = {"footnote": ("word/footnotes.xml", "footnotes", "footnote", kit.REL_FOOTNOTES), "endnote": ("word/endnotes.xml", "endnotes", "endnote", kit.REL_ENDNOTES)}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def note_part(doc: kit.Docx, kind: str):
|
|
34
|
+
part, collection, item, rel_type = PARTS[kind]
|
|
35
|
+
if not doc.exists(part):
|
|
36
|
+
root = etree.Element(kit.q(collection), nsmap={"w": kit.W, "r": kit.R})
|
|
37
|
+
for ident, note_kind in ((-1, "separator"), (0, "continuationSeparator")):
|
|
38
|
+
note = etree.SubElement(root, kit.q(item))
|
|
39
|
+
note.set(kit.q("type"), note_kind)
|
|
40
|
+
note.set(kit.q("id"), str(ident))
|
|
41
|
+
paragraph = etree.SubElement(note, kit.q("p"))
|
|
42
|
+
run = etree.SubElement(paragraph, kit.q("r"))
|
|
43
|
+
etree.SubElement(run, kit.q(note_kind))
|
|
44
|
+
doc.set_xml(part, root)
|
|
45
|
+
doc.ensure_override(part, kit.CT_BY_REL[rel_type])
|
|
46
|
+
doc.ensure_rel("word/document.xml", rel_type, part.split("/")[-1])
|
|
47
|
+
return doc.xml(part), item
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def append_note(root, item: str, ident: int, text: str) -> None:
|
|
51
|
+
note = etree.SubElement(root, kit.q(item))
|
|
52
|
+
note.set(kit.q("id"), str(ident))
|
|
53
|
+
paragraph = etree.SubElement(note, kit.q("p"))
|
|
54
|
+
ppr = etree.SubElement(paragraph, kit.q("pPr"))
|
|
55
|
+
etree.SubElement(ppr, kit.q("pStyle")).set(kit.q("val"), "FootnoteText")
|
|
56
|
+
run = etree.SubElement(paragraph, kit.q("r"))
|
|
57
|
+
rpr = etree.SubElement(run, kit.q("rPr"))
|
|
58
|
+
etree.SubElement(rpr, kit.q("rStyle")).set(kit.q("val"), "FootnoteReference")
|
|
59
|
+
node = etree.SubElement(run, kit.q("t"))
|
|
60
|
+
node.text = text
|
|
61
|
+
node.set(kit.q("space"), "preserve")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def reference_run(kind: str, ident: int):
|
|
65
|
+
run = etree.Element(kit.q("r"))
|
|
66
|
+
rpr = etree.SubElement(run, kit.q("rPr"))
|
|
67
|
+
etree.SubElement(rpr, kit.q("rStyle")).set(kit.q("val"), "FootnoteReference")
|
|
68
|
+
reference = etree.SubElement(run, kit.q(kind + "Reference"))
|
|
69
|
+
reference.set(kit.q("id"), str(ident))
|
|
70
|
+
return run
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def place_reference(paragraph, marker: str, kind: str, ident: int) -> bool:
|
|
74
|
+
for run in paragraph.iter(kit.q("r")):
|
|
75
|
+
text = "".join(node.text or "" for node in run.findall(kit.q("t")))
|
|
76
|
+
if marker not in text:
|
|
77
|
+
continue
|
|
78
|
+
before, after = text.split(marker, 1)
|
|
79
|
+
rpr = run.find(kit.q("rPr"))
|
|
80
|
+
parent = run.getparent()
|
|
81
|
+
position = parent.index(run)
|
|
82
|
+
parent.remove(run)
|
|
83
|
+
pieces = []
|
|
84
|
+
if before:
|
|
85
|
+
pieces.append(kit.make_run(before, etree.fromstring(etree.tostring(rpr)) if rpr is not None else None))
|
|
86
|
+
pieces.append(reference_run(kind, ident))
|
|
87
|
+
if after:
|
|
88
|
+
pieces.append(kit.make_run(after, etree.fromstring(etree.tostring(rpr)) if rpr is not None else None))
|
|
89
|
+
for offset, piece in enumerate(pieces):
|
|
90
|
+
parent.insert(position + offset, piece)
|
|
91
|
+
return True
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def main(argv: list[str] | None = None) -> int:
|
|
96
|
+
parser = argparse.ArgumentParser(description="Insert a true footnote or endnote at a [[NOTE]]-style marker.")
|
|
97
|
+
parser.add_argument("in_docx", help="Input .docx")
|
|
98
|
+
parser.add_argument("--kind", required=True, choices=["footnote", "endnote"])
|
|
99
|
+
parser.add_argument("--text", required=True, help="Note body")
|
|
100
|
+
parser.add_argument("--marker", default="[[NOTE]]", help="Text token that marks the anchor")
|
|
101
|
+
parser.add_argument("--out", required=True, help="Output .docx")
|
|
102
|
+
args = parser.parse_args(argv)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
doc = kit.Docx(args.in_docx)
|
|
106
|
+
except (OSError, KeyError) as exc:
|
|
107
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
108
|
+
return 2
|
|
109
|
+
|
|
110
|
+
target = None
|
|
111
|
+
for paragraph in doc.paragraphs():
|
|
112
|
+
if args.marker in "".join(node.text or "" for node in paragraph.iter(kit.q("t"))):
|
|
113
|
+
target = paragraph
|
|
114
|
+
break
|
|
115
|
+
if target is None:
|
|
116
|
+
print("error: marker %r not found" % args.marker, file=sys.stderr)
|
|
117
|
+
return 1
|
|
118
|
+
|
|
119
|
+
root, item = note_part(doc, args.kind)
|
|
120
|
+
existing = [int(node.get(kit.q("id")) or 0) for node in root.findall(kit.q(item))]
|
|
121
|
+
ident = max(existing + [0]) + 1
|
|
122
|
+
if not place_reference(target, args.marker, args.kind, ident):
|
|
123
|
+
print("error: could not split the marker run", file=sys.stderr)
|
|
124
|
+
return 1
|
|
125
|
+
append_note(root, item, ident, args.text)
|
|
126
|
+
|
|
127
|
+
doc.save(args.out)
|
|
128
|
+
kit.emit({"ok": True, "output": str(Path(args.out)), "kind": args.kind, "id": ident, "text": args.text})
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Turn marker text such as ``[[REF:tbl1]]`` into real REF cross-reference fields.
|
|
3
|
+
|
|
4
|
+
Markers keep the source text readable while drafting; this pass converts them to
|
|
5
|
+
fields that Word resolves to the bookmark's caption or number. The field is
|
|
6
|
+
inserted at the marker position with an optional text prefix, and the paragraph
|
|
7
|
+
is rebuilt with the formatting of its first run.
|
|
8
|
+
|
|
9
|
+
Exit codes: 0 success (no markers means no change), 1 pattern has no 'bookmark'
|
|
10
|
+
group, 2 bad input.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import re
|
|
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 rebuild(paragraph, pattern: re.Pattern, prefix: str) -> int:
|
|
34
|
+
text = kit.paragraph_text(paragraph)
|
|
35
|
+
matches = list(pattern.finditer(text))
|
|
36
|
+
if not matches:
|
|
37
|
+
return 0
|
|
38
|
+
rpr = kit.first_rpr(paragraph)
|
|
39
|
+
pieces: list = []
|
|
40
|
+
cursor = 0
|
|
41
|
+
for match in matches:
|
|
42
|
+
if match.start() > cursor:
|
|
43
|
+
pieces.append(kit.make_run(text[cursor : match.start()], etree.fromstring(etree.tostring(rpr)) if rpr is not None else None))
|
|
44
|
+
bookmark = match.groupdict().get("bookmark") or ""
|
|
45
|
+
if prefix:
|
|
46
|
+
pieces.append(kit.make_run(prefix, etree.fromstring(etree.tostring(rpr)) if rpr is not None else None))
|
|
47
|
+
pieces.extend(kit.field_runs("REF %s \\h" % bookmark, "", etree.fromstring(etree.tostring(rpr)) if rpr is not None else None))
|
|
48
|
+
cursor = match.end()
|
|
49
|
+
if cursor < len(text):
|
|
50
|
+
pieces.append(kit.make_run(text[cursor:], etree.fromstring(etree.tostring(rpr)) if rpr is not None else None))
|
|
51
|
+
ppr = paragraph.find(kit.q("pPr"))
|
|
52
|
+
for child in list(paragraph):
|
|
53
|
+
if child is not ppr:
|
|
54
|
+
paragraph.remove(child)
|
|
55
|
+
for piece in pieces:
|
|
56
|
+
paragraph.append(piece)
|
|
57
|
+
return len(matches)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main(argv: list[str] | None = None) -> int:
|
|
61
|
+
parser = argparse.ArgumentParser(description="Replace [[REF:bookmark]] markers with REF cross-reference fields.")
|
|
62
|
+
parser.add_argument("input_docx", help="Input .docx")
|
|
63
|
+
parser.add_argument("output_docx", help="Output .docx")
|
|
64
|
+
parser.add_argument("--pattern", default=r"\[\[REF:(?P<bookmark>[A-Za-z0-9_:\-]+)\]\]", help="Regex for markers; must contain a 'bookmark' group")
|
|
65
|
+
parser.add_argument("--prefix", default="", help="Text inserted immediately before each field")
|
|
66
|
+
args = parser.parse_args(argv)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
pattern = re.compile(args.pattern)
|
|
70
|
+
except re.error as exc:
|
|
71
|
+
print("error: bad --pattern: %s" % exc, file=sys.stderr)
|
|
72
|
+
return 1
|
|
73
|
+
if "bookmark" not in pattern.groupindex:
|
|
74
|
+
print("error: --pattern needs a named group 'bookmark'", file=sys.stderr)
|
|
75
|
+
return 1
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
doc = kit.Docx(args.input_docx)
|
|
79
|
+
except (OSError, KeyError) as exc:
|
|
80
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
81
|
+
return 2
|
|
82
|
+
|
|
83
|
+
replaced = 0
|
|
84
|
+
touched = 0
|
|
85
|
+
for paragraph in doc.paragraphs():
|
|
86
|
+
count = rebuild(paragraph, pattern, args.prefix)
|
|
87
|
+
if count:
|
|
88
|
+
replaced += count
|
|
89
|
+
touched += 1
|
|
90
|
+
|
|
91
|
+
doc.save(args.output_docx)
|
|
92
|
+
kit.emit({"ok": True, "output": str(Path(args.output_docx)), "fields": replaced, "paragraphs": touched})
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Replace a TOC placeholder with a real Word table-of-contents field.
|
|
3
|
+
|
|
4
|
+
The field is the portable choice: Word rebuilds the entries, numbering and page
|
|
5
|
+
numbers when it opens the file. ``w:updateFields`` is set so that refresh
|
|
6
|
+
happens without the user pressing F9. A cached hint line is included as the
|
|
7
|
+
field result so the area is not simply blank in renderers that ignore fields.
|
|
8
|
+
|
|
9
|
+
Exit codes: 0 success, 1 placeholder not found, 2 bad input.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from lxml import etree
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
21
|
+
import docx_kit as kit
|
|
22
|
+
|
|
23
|
+
# 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
|
+
|
|
31
|
+
def level_range(raw: str) -> str:
|
|
32
|
+
cleaned = raw.replace(" ", "")
|
|
33
|
+
if "-" in cleaned:
|
|
34
|
+
low, high = cleaned.split("-", 1)
|
|
35
|
+
if low.isdigit() and high.isdigit() and 1 <= int(low) <= int(high) <= 9:
|
|
36
|
+
return "%s-%s" % (low, high)
|
|
37
|
+
raise argparse.ArgumentTypeError("--levels must look like 1-3")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def request_update(settings) -> None:
|
|
41
|
+
for node in settings.findall(kit.q("updateFields")):
|
|
42
|
+
settings.remove(node)
|
|
43
|
+
etree.SubElement(settings, kit.q("updateFields")).set(kit.q("val"), "true")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main(argv: list[str] | None = None) -> int:
|
|
47
|
+
parser = argparse.ArgumentParser(description="Insert a TOC field where a placeholder paragraph sits.")
|
|
48
|
+
parser.add_argument("docx", help="Input .docx")
|
|
49
|
+
parser.add_argument("--out", default=None, help="Output .docx (default: overwrite the input)")
|
|
50
|
+
parser.add_argument("--placeholder", default="[[TOC]]", help="Paragraph text token to replace")
|
|
51
|
+
parser.add_argument("--levels", default="1-3", help='Heading levels to include, e.g. "1-3"')
|
|
52
|
+
args = parser.parse_args(argv)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
levels = level_range(args.levels)
|
|
56
|
+
except argparse.ArgumentTypeError as exc:
|
|
57
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
58
|
+
return 2
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
doc = kit.Docx(args.docx)
|
|
62
|
+
except (OSError, KeyError) as exc:
|
|
63
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
64
|
+
return 2
|
|
65
|
+
|
|
66
|
+
target = None
|
|
67
|
+
for paragraph in doc.paragraphs():
|
|
68
|
+
if args.placeholder in kit.paragraph_text(paragraph):
|
|
69
|
+
target = paragraph
|
|
70
|
+
break
|
|
71
|
+
if target is None:
|
|
72
|
+
print("error: placeholder %r not found" % args.placeholder, file=sys.stderr)
|
|
73
|
+
return 1
|
|
74
|
+
|
|
75
|
+
parent = target.getparent()
|
|
76
|
+
position = parent.index(target)
|
|
77
|
+
root = etree.Element(kit.q("p"))
|
|
78
|
+
for run in kit.field_runs('#TOC \\o "%s" \\h \\z \\u' % levels, "Right-click and choose Update Field to build the contents."):
|
|
79
|
+
root.append(run)
|
|
80
|
+
parent.remove(target)
|
|
81
|
+
parent.insert(position, root)
|
|
82
|
+
|
|
83
|
+
request_update(doc.xml("word/settings.xml"))
|
|
84
|
+
out = args.out or args.docx
|
|
85
|
+
doc.save(out)
|
|
86
|
+
kit.emit({"ok": True, "output": str(Path(out)), "levels": levels, "paragraph_index": position})
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
raise SystemExit(main())
|