dsh-office-kit 0.1.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.
Files changed (198) hide show
  1. package/CAPABILITY.md +147 -68
  2. package/LICENSE.md +21 -21
  3. package/README.md +108 -106
  4. package/assets/skills/excel-live-control/SKILL.md +234 -0
  5. package/assets/skills/excel-live-control/container_tools/live_excel.py +1589 -0
  6. package/assets/skills/excel-live-control/domain_guidance/financial_models.md +86 -0
  7. package/assets/skills/excel-live-control/domain_guidance/healthcare.md +58 -0
  8. package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +53 -0
  9. package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +53 -0
  10. package/assets/skills/excel-live-control/references/office-automation.md +207 -0
  11. package/assets/skills/excel-live-control/references/read_only_qna.md +59 -0
  12. package/assets/skills/excel-live-control/style_guidelines.md +87 -0
  13. package/assets/skills/excel-live-control/workflows/create_workflows.md +78 -0
  14. package/assets/skills/excel-live-control/workflows/edit_workflows.md +72 -0
  15. package/assets/skills/office-documents/SKILL.md +90 -77
  16. package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +86 -0
  17. package/assets/skills/office-documents/ooxml/comments.md +40 -0
  18. package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +66 -0
  19. package/assets/skills/office-documents/ooxml/rels_and_content_types.md +62 -0
  20. package/assets/skills/office-documents/ooxml/tracked_changes.md +53 -0
  21. package/assets/skills/office-documents/references/docx-structure.md +83 -0
  22. package/assets/skills/office-documents/references/template-elicitation.md +49 -0
  23. package/assets/skills/office-documents/references/writing-quality.md +49 -0
  24. package/assets/skills/office-documents/render_docx.py +92 -0
  25. package/assets/skills/office-documents/scripts/a11y_audit.py +194 -0
  26. package/assets/skills/office-documents/scripts/accept_tracked_changes.py +151 -0
  27. package/assets/skills/office-documents/scripts/add_tracked_replacements.py +148 -0
  28. package/assets/skills/office-documents/scripts/append_blocks.py +108 -0
  29. package/assets/skills/office-documents/scripts/apply_template_styles.py +104 -0
  30. package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +102 -0
  31. package/assets/skills/office-documents/scripts/comments_add.py +136 -0
  32. package/assets/skills/office-documents/scripts/comments_apply_patch.py +114 -0
  33. package/assets/skills/office-documents/scripts/comments_extract.py +85 -0
  34. package/assets/skills/office-documents/scripts/comments_strip.py +74 -0
  35. package/assets/skills/office-documents/scripts/content_controls.py +192 -0
  36. package/assets/skills/office-documents/scripts/core_properties.py +124 -0
  37. package/assets/skills/office-documents/scripts/docx_kit.py +587 -0
  38. package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +225 -0
  39. package/assets/skills/office-documents/scripts/docx_table_to_csv.py +99 -0
  40. package/assets/skills/office-documents/scripts/fields_materialize.py +66 -0
  41. package/assets/skills/office-documents/scripts/fields_report.py +59 -0
  42. package/assets/skills/office-documents/scripts/flatten_ref_fields.py +62 -0
  43. package/assets/skills/office-documents/scripts/footnotes_report.py +77 -0
  44. package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +96 -0
  45. package/assets/skills/office-documents/scripts/heading_audit.py +81 -0
  46. package/assets/skills/office-documents/scripts/images_audit.py +80 -0
  47. package/assets/skills/office-documents/scripts/insert_note.py +126 -0
  48. package/assets/skills/office-documents/scripts/insert_ref_fields.py +90 -0
  49. package/assets/skills/office-documents/scripts/insert_toc.py +84 -0
  50. package/assets/skills/office-documents/scripts/internal_nav.py +146 -0
  51. package/assets/skills/office-documents/scripts/make_fixtures.py +169 -0
  52. package/assets/skills/office-documents/scripts/merge_docx_append.py +239 -0
  53. package/assets/skills/office-documents/scripts/privacy_scrub.py +131 -0
  54. package/assets/skills/office-documents/scripts/redact_docx.py +113 -0
  55. package/assets/skills/office-documents/scripts/render_and_diff.py +101 -0
  56. package/assets/skills/office-documents/scripts/replace_text.py +154 -0
  57. package/assets/skills/office-documents/scripts/section_audit.py +91 -0
  58. package/assets/skills/office-documents/scripts/set_protection.py +92 -0
  59. package/assets/skills/office-documents/scripts/style_lint.py +111 -0
  60. package/assets/skills/office-documents/scripts/style_normalize.py +103 -0
  61. package/assets/skills/office-documents/scripts/table_geometry.py +167 -0
  62. package/assets/skills/office-documents/scripts/watermark_add.py +48 -0
  63. package/assets/skills/office-documents/scripts/watermark_audit_remove.py +67 -0
  64. package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +117 -0
  65. package/assets/skills/office-documents/tasks/accessibility_a11y.md +45 -0
  66. package/assets/skills/office-documents/tasks/captions_crossrefs.md +49 -0
  67. package/assets/skills/office-documents/tasks/clean_tracked_changes.md +49 -0
  68. package/assets/skills/office-documents/tasks/comments_manage.md +59 -0
  69. package/assets/skills/office-documents/tasks/compare_diff.md +40 -0
  70. package/assets/skills/office-documents/tasks/create_edit.md +107 -0
  71. package/assets/skills/office-documents/tasks/equations.md +59 -0
  72. package/assets/skills/office-documents/tasks/fields_update.md +44 -0
  73. package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +50 -0
  74. package/assets/skills/office-documents/tasks/footnotes_endnotes.md +38 -0
  75. package/assets/skills/office-documents/tasks/forms_content_controls.md +48 -0
  76. package/assets/skills/office-documents/tasks/headings_numbering.md +55 -0
  77. package/assets/skills/office-documents/tasks/images_figures.md +63 -0
  78. package/assets/skills/office-documents/tasks/multi_doc_merge.md +43 -0
  79. package/assets/skills/office-documents/tasks/navigation_internal_links.md +38 -0
  80. package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +45 -0
  81. package/assets/skills/office-documents/tasks/protection_restrict_editing.md +41 -0
  82. package/assets/skills/office-documents/tasks/read_review.md +50 -0
  83. package/assets/skills/office-documents/tasks/redaction_anonymization.md +42 -0
  84. package/assets/skills/office-documents/tasks/sections_layout.md +54 -0
  85. package/assets/skills/office-documents/tasks/style_lint_normalize.md +44 -0
  86. package/assets/skills/office-documents/tasks/tables_spreadsheets.md +64 -0
  87. package/assets/skills/office-documents/tasks/templates_style_packs.md +53 -0
  88. package/assets/skills/office-documents/tasks/toc_workflow.md +39 -0
  89. package/assets/skills/office-documents/tasks/verify_render.md +50 -0
  90. package/assets/skills/office-documents/tasks/watermarks_background.md +35 -0
  91. package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +47 -0
  92. package/assets/skills/office-documents/troubleshooting/run_splitting.md +36 -0
  93. package/assets/skills/office-pdf/SKILL.md +73 -56
  94. package/assets/skills/office-pdf/references/form-fields.md +56 -0
  95. package/assets/skills/office-pdf/references/geometry.md +44 -0
  96. package/assets/skills/office-pdf/references/layout-quality.md +41 -0
  97. package/assets/skills/office-pdf/references/limits.md +58 -0
  98. package/assets/skills/office-pdf/scripts/extract_text.py +172 -0
  99. package/assets/skills/office-pdf/scripts/forms.py +556 -0
  100. package/assets/skills/office-pdf/scripts/metadata.py +161 -0
  101. package/assets/skills/office-pdf/scripts/page_ops.py +198 -0
  102. package/assets/skills/office-pdf/scripts/redact_region.py +347 -0
  103. package/assets/skills/office-pdf/scripts/render_pages.py +207 -0
  104. package/assets/skills/office-pdf/scripts/split_pages.py +155 -0
  105. package/assets/skills/office-pdf/scripts/stamp_overlay.py +199 -0
  106. package/assets/skills/office-pdf/tasks/annotations.md +98 -0
  107. package/assets/skills/office-pdf/tasks/create.md +75 -0
  108. package/assets/skills/office-pdf/tasks/edit-pages.md +71 -0
  109. package/assets/skills/office-pdf/tasks/forms.md +67 -0
  110. package/assets/skills/office-pdf/tasks/read.md +62 -0
  111. package/assets/skills/office-pdf/tasks/redact.md +72 -0
  112. package/assets/skills/office-pdf/tasks/stamp.md +60 -0
  113. package/assets/skills/office-presentations/SKILL.md +141 -87
  114. package/assets/skills/office-presentations/container_tools/add_slides.py +136 -0
  115. package/assets/skills/office-presentations/container_tools/core_properties.py +120 -0
  116. package/assets/skills/office-presentations/container_tools/create_montage.py +126 -0
  117. package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +285 -0
  118. package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +157 -0
  119. package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +144 -0
  120. package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +199 -0
  121. package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +212 -0
  122. package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +345 -0
  123. package/assets/skills/office-presentations/container_tools/preview_render.py +375 -0
  124. package/assets/skills/office-presentations/container_tools/render_slides.py +139 -0
  125. package/assets/skills/office-presentations/container_tools/replace_text.py +168 -0
  126. package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +229 -0
  127. package/assets/skills/office-presentations/domain_guidance/finance.md +66 -0
  128. package/assets/skills/office-presentations/recipes/README.md +71 -0
  129. package/assets/skills/office-presentations/recipes/autolayout.md +88 -0
  130. package/assets/skills/office-presentations/recipes/connectors.md +83 -0
  131. package/assets/skills/office-presentations/recipes/images.md +80 -0
  132. package/assets/skills/office-presentations/recipes/master-and-layouts.md +127 -0
  133. package/assets/skills/office-presentations/recipes/native-charts.md +87 -0
  134. package/assets/skills/office-presentations/recipes/rich-text-runs.md +86 -0
  135. package/assets/skills/office-presentations/recipes/speaker-notes.md +65 -0
  136. package/assets/skills/office-presentations/recipes/tables.md +88 -0
  137. package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +71 -0
  138. package/assets/skills/office-presentations/recipes/theme-tokens.md +87 -0
  139. package/assets/skills/office-presentations/references/authoring-workflow.md +65 -0
  140. package/assets/skills/office-presentations/references/finalization.md +63 -0
  141. package/assets/skills/office-presentations/references/native-bullets.md +78 -0
  142. package/assets/skills/office-presentations/references/native-evidence.md +67 -0
  143. package/assets/skills/office-presentations/references/style-guidelines.md +66 -0
  144. package/assets/skills/office-presentations/references/templates.md +76 -0
  145. package/assets/skills/office-presentations/routing/google_slides.md +45 -0
  146. package/assets/skills/office-presentations/tasks/apply-annotations.md +83 -0
  147. package/assets/skills/office-presentations/tasks/charts-tables-notes.md +79 -0
  148. package/assets/skills/office-presentations/tasks/create-deck.md +106 -0
  149. package/assets/skills/office-presentations/tasks/edit-deck.md +93 -0
  150. package/assets/skills/office-presentations/tasks/inspect-and-verify.md +107 -0
  151. package/assets/skills/office-presentations/tasks/template-following.md +132 -0
  152. package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py +176 -0
  153. package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +154 -0
  154. package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +111 -0
  155. package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +239 -0
  156. package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +185 -0
  157. package/assets/skills/office-spreadsheets/SKILL.md +105 -101
  158. package/assets/skills/office-spreadsheets/container_tools/add_chart.py +236 -0
  159. package/assets/skills/office-spreadsheets/container_tools/add_image.py +107 -0
  160. package/assets/skills/office-spreadsheets/container_tools/append_rows.py +122 -0
  161. package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +306 -0
  162. package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +398 -0
  163. package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +275 -0
  164. package/assets/skills/office-spreadsheets/container_tools/set_cells.py +135 -0
  165. package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +134 -0
  166. package/assets/skills/office-spreadsheets/container_tools/style_cells.py +133 -0
  167. package/assets/skills/office-spreadsheets/recipes/charts.md +66 -0
  168. package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +69 -0
  169. package/assets/skills/office-spreadsheets/recipes/data-tables.md +58 -0
  170. package/assets/skills/office-spreadsheets/recipes/named-ranges.md +41 -0
  171. package/assets/skills/office-spreadsheets/recipes/number-formats.md +72 -0
  172. package/assets/skills/office-spreadsheets/recipes/sparklines.md +43 -0
  173. package/assets/skills/office-spreadsheets/references/charts.md +63 -0
  174. package/assets/skills/office-spreadsheets/references/domain/financial-models.md +40 -0
  175. package/assets/skills/office-spreadsheets/references/domain/healthcare.md +22 -0
  176. package/assets/skills/office-spreadsheets/references/domain/marketing.md +28 -0
  177. package/assets/skills/office-spreadsheets/references/domain/research.md +22 -0
  178. package/assets/skills/office-spreadsheets/references/formula-patterns.md +78 -0
  179. package/assets/skills/office-spreadsheets/references/style-guidelines.md +69 -0
  180. package/assets/skills/office-spreadsheets/references/workbook-structure.md +60 -0
  181. package/assets/skills/office-spreadsheets/routing/google-sheets.md +36 -0
  182. package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +37 -0
  183. package/assets/skills/office-spreadsheets/tasks/annotations.md +51 -0
  184. package/assets/skills/office-spreadsheets/tasks/create-workbook.md +66 -0
  185. package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +38 -0
  186. package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +69 -0
  187. package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +38 -0
  188. package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +57 -0
  189. package/assets/skills/shared/annotation-recipes.md +252 -0
  190. package/assets/skills/shared/check_runtime.py +253 -0
  191. package/assets/skills/shared/common-workflow.md +139 -0
  192. package/assets/skills/shared/scripts/ensure_raster_image.py +187 -0
  193. package/dist/client.js +4 -4
  194. package/dist/client.js.map +1 -1
  195. package/dist/index.js +239 -89
  196. package/package.json +146 -149
  197. package/postinstall.mjs +92 -82
  198. package/python/office_cli.py +171 -246
@@ -0,0 +1,81 @@
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
+ BOLD_LIKE = ("Heading", "Title")
21
+ HEADING_SIZE_HALF_POINTS = 26 # 13pt: larger than any body style should be
22
+
23
+
24
+ def manual_heading(paragraph) -> bool:
25
+ """True when bold, oversized runs stand in for a heading style."""
26
+ for run in paragraph.iter(kit.q("r")):
27
+ props = run.find(kit.q("rPr"))
28
+ if props is None or props.find(kit.q("b")) is None:
29
+ continue
30
+ size = props.find(kit.q("sz"))
31
+ 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:
32
+ return True
33
+ return False
34
+
35
+
36
+ def main(argv: list[str] | None = None) -> int:
37
+ parser = argparse.ArgumentParser(description="Report heading-level jumps, empty headings and manual heading formatting.")
38
+ parser.add_argument("docx", help="Input .docx")
39
+ parser.add_argument("--max_findings", type=int, default=50, help="Cap the reported findings")
40
+ args = parser.parse_args(argv)
41
+
42
+ try:
43
+ doc = kit.Docx(args.docx)
44
+ except (OSError, KeyError) as exc:
45
+ print("error: %s" % exc, file=sys.stderr)
46
+ return 2
47
+
48
+ findings = []
49
+ outline = []
50
+ previous = 0
51
+ for index, paragraph in enumerate(doc.paragraphs(), start=1):
52
+ text = kit.paragraph_text(paragraph).strip()
53
+ level = kit.heading_level(paragraph)
54
+ if level == 0:
55
+ outline.append({"index": index, "style": "Title", "text": text})
56
+ continue
57
+ if level:
58
+ if not text:
59
+ findings.append({"kind": "empty_heading", "index": index, "level": level})
60
+ continue
61
+ outline.append({"index": index, "level": level, "text": text})
62
+ if previous and level > previous + 1:
63
+ findings.append({"kind": "skipped_level", "index": index, "from": previous, "to": level, "text": text})
64
+ if manual_heading(paragraph):
65
+ findings.append({"kind": "manual_formatting", "index": index, "text": text})
66
+ previous = level
67
+ elif not text:
68
+ continue
69
+ elif manual_heading(paragraph) and len(text) <= 120:
70
+ findings.append({"kind": "heading_not_styled", "index": index, "text": text})
71
+
72
+ counts = {}
73
+ for finding in findings:
74
+ counts[finding["kind"]] = counts.get(finding["kind"], 0) + 1
75
+
76
+ kit.emit({"ok": not findings, "counts": counts, "headings": len(outline), "findings": findings[: args.max_findings], "truncated": max(0, len(findings) - args.max_findings)})
77
+ return 1 if findings else 0
78
+
79
+
80
+ if __name__ == "__main__":
81
+ raise SystemExit(main())
@@ -0,0 +1,80 @@
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
+ EMU_PER_INCH = 914400
24
+ MAX_WIDTH_INCHES = 8.0
25
+
26
+
27
+ def part_for(rel_id: str, rels: dict) -> str | None:
28
+ entry = rels.get(rel_id)
29
+ return entry["resolved"] if entry else None
30
+
31
+
32
+ def main(argv: list[str] | None = None) -> int:
33
+ parser = argparse.ArgumentParser(description="List inline images with size, source part and alt-text status.")
34
+ parser.add_argument("docx", help="Input .docx")
35
+ parser.add_argument("--max_rows", type=int, default=50)
36
+ args = parser.parse_args(argv)
37
+
38
+ try:
39
+ doc = kit.Docx(args.docx)
40
+ except (OSError, KeyError) as exc:
41
+ print("error: %s" % exc, file=sys.stderr)
42
+ return 2
43
+
44
+ rows = []
45
+ findings = []
46
+ for part in ["word/document.xml"] + doc.names("word/header") + doc.names("word/footer"):
47
+ if not doc.exists(part):
48
+ continue
49
+ rels = {entry["id"]: entry for entry in doc.relationships(part)}
50
+ for doc_pr in doc.xml(part).iter(kit.q("docPr", kit.WP)):
51
+ blip = doc_pr.getparent().find(".//" + kit.q("blip", kit.A))
52
+ embed = blip.get(kit.q("embed", kit.R)) if blip is not None else None
53
+ extent = doc_pr.getparent().find(".//" + kit.q("extent", kit.WP))
54
+ width = int(extent.get("cx")) / EMU_PER_INCH if extent is not None and extent.get("cx") else 0.0
55
+ height = int(extent.get("cy")) / EMU_PER_INCH if extent is not None and extent.get("cy") else 0.0
56
+ alt = doc_pr.get("descr") or doc_pr.get("title") or ""
57
+ row = {
58
+ "part": part,
59
+ "name": doc_pr.get("name"),
60
+ "source": part_for(embed, rels) if embed else None,
61
+ "width_in": round(width, 2),
62
+ "height_in": round(height, 2),
63
+ "alt": alt or None,
64
+ }
65
+ rows.append(row)
66
+ if not alt.strip():
67
+ findings.append({"kind": "missing_alt", "part": part, "name": row["name"], "source": row["source"]})
68
+ if width > MAX_WIDTH_INCHES:
69
+ findings.append({"kind": "oversized", "part": part, "name": row["name"], "width_in": row["width_in"]})
70
+
71
+ counts = {}
72
+ for finding in findings:
73
+ counts[finding["kind"]] = counts.get(finding["kind"], 0) + 1
74
+
75
+ kit.emit({"ok": not findings, "images": len(rows), "counts": counts, "rows": rows[: args.max_rows], "findings": findings[: args.max_rows]})
76
+ return 1 if findings else 0
77
+
78
+
79
+ if __name__ == "__main__":
80
+ raise SystemExit(main())
@@ -0,0 +1,126 @@
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
+ PARTS = {"footnote": ("word/footnotes.xml", "footnotes", "footnote", kit.REL_FOOTNOTES), "endnote": ("word/endnotes.xml", "endnotes", "endnote", kit.REL_ENDNOTES)}
24
+
25
+
26
+ def note_part(doc: kit.Docx, kind: str):
27
+ part, collection, item, rel_type = PARTS[kind]
28
+ if not doc.exists(part):
29
+ root = etree.Element(kit.q(collection), nsmap={"w": kit.W, "r": kit.R})
30
+ for ident, note_kind in ((-1, "separator"), (0, "continuationSeparator")):
31
+ note = etree.SubElement(root, kit.q(item))
32
+ note.set(kit.q("type"), note_kind)
33
+ note.set(kit.q("id"), str(ident))
34
+ paragraph = etree.SubElement(note, kit.q("p"))
35
+ run = etree.SubElement(paragraph, kit.q("r"))
36
+ etree.SubElement(run, kit.q(note_kind))
37
+ doc.set_xml(part, root)
38
+ doc.ensure_override(part, kit.CT_BY_REL[rel_type])
39
+ doc.ensure_rel("word/document.xml", rel_type, part.split("/")[-1])
40
+ return doc.xml(part), item
41
+
42
+
43
+ def append_note(root, item: str, ident: int, text: str) -> None:
44
+ note = etree.SubElement(root, kit.q(item))
45
+ note.set(kit.q("id"), str(ident))
46
+ paragraph = etree.SubElement(note, kit.q("p"))
47
+ ppr = etree.SubElement(paragraph, kit.q("pPr"))
48
+ etree.SubElement(ppr, kit.q("pStyle")).set(kit.q("val"), "FootnoteText")
49
+ run = etree.SubElement(paragraph, kit.q("r"))
50
+ rpr = etree.SubElement(run, kit.q("rPr"))
51
+ etree.SubElement(rpr, kit.q("rStyle")).set(kit.q("val"), "FootnoteReference")
52
+ node = etree.SubElement(run, kit.q("t"))
53
+ node.text = text
54
+ node.set(kit.q("space"), "preserve")
55
+
56
+
57
+ def reference_run(kind: str, ident: int):
58
+ run = etree.Element(kit.q("r"))
59
+ rpr = etree.SubElement(run, kit.q("rPr"))
60
+ etree.SubElement(rpr, kit.q("rStyle")).set(kit.q("val"), "FootnoteReference")
61
+ reference = etree.SubElement(run, kit.q(kind + "Reference"))
62
+ reference.set(kit.q("id"), str(ident))
63
+ return run
64
+
65
+
66
+ def place_reference(paragraph, marker: str, kind: str, ident: int) -> bool:
67
+ for run in paragraph.iter(kit.q("r")):
68
+ text = "".join(node.text or "" for node in run.findall(kit.q("t")))
69
+ if marker not in text:
70
+ continue
71
+ before, after = text.split(marker, 1)
72
+ rpr = run.find(kit.q("rPr"))
73
+ parent = run.getparent()
74
+ position = parent.index(run)
75
+ parent.remove(run)
76
+ pieces = []
77
+ if before:
78
+ pieces.append(kit.make_run(before, etree.fromstring(etree.tostring(rpr)) if rpr is not None else None))
79
+ pieces.append(reference_run(kind, ident))
80
+ if after:
81
+ pieces.append(kit.make_run(after, etree.fromstring(etree.tostring(rpr)) if rpr is not None else None))
82
+ for offset, piece in enumerate(pieces):
83
+ parent.insert(position + offset, piece)
84
+ return True
85
+ return False
86
+
87
+
88
+ def main(argv: list[str] | None = None) -> int:
89
+ parser = argparse.ArgumentParser(description="Insert a true footnote or endnote at a [[NOTE]]-style marker.")
90
+ parser.add_argument("in_docx", help="Input .docx")
91
+ parser.add_argument("--kind", required=True, choices=["footnote", "endnote"])
92
+ parser.add_argument("--text", required=True, help="Note body")
93
+ parser.add_argument("--marker", default="[[NOTE]]", help="Text token that marks the anchor")
94
+ parser.add_argument("--out", required=True, help="Output .docx")
95
+ args = parser.parse_args(argv)
96
+
97
+ try:
98
+ doc = kit.Docx(args.in_docx)
99
+ except (OSError, KeyError) as exc:
100
+ print("error: %s" % exc, file=sys.stderr)
101
+ return 2
102
+
103
+ target = None
104
+ for paragraph in doc.paragraphs():
105
+ if args.marker in "".join(node.text or "" for node in paragraph.iter(kit.q("t"))):
106
+ target = paragraph
107
+ break
108
+ if target is None:
109
+ print("error: marker %r not found" % args.marker, file=sys.stderr)
110
+ return 1
111
+
112
+ root, item = note_part(doc, args.kind)
113
+ existing = [int(node.get(kit.q("id")) or 0) for node in root.findall(kit.q(item))]
114
+ ident = max(existing + [0]) + 1
115
+ if not place_reference(target, args.marker, args.kind, ident):
116
+ print("error: could not split the marker run", file=sys.stderr)
117
+ return 1
118
+ append_note(root, item, ident, args.text)
119
+
120
+ doc.save(args.out)
121
+ kit.emit({"ok": True, "output": str(Path(args.out)), "kind": args.kind, "id": ident, "text": args.text})
122
+ return 0
123
+
124
+
125
+ if __name__ == "__main__":
126
+ raise SystemExit(main())
@@ -0,0 +1,90 @@
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
+
26
+ def rebuild(paragraph, pattern: re.Pattern, prefix: str) -> int:
27
+ text = kit.paragraph_text(paragraph)
28
+ matches = list(pattern.finditer(text))
29
+ if not matches:
30
+ return 0
31
+ rpr = kit.first_rpr(paragraph)
32
+ pieces: list = []
33
+ cursor = 0
34
+ for match in matches:
35
+ if match.start() > cursor:
36
+ pieces.append(kit.make_run(text[cursor : match.start()], etree.fromstring(etree.tostring(rpr)) if rpr is not None else None))
37
+ bookmark = match.groupdict().get("bookmark") or ""
38
+ if prefix:
39
+ pieces.append(kit.make_run(prefix, etree.fromstring(etree.tostring(rpr)) if rpr is not None else None))
40
+ pieces.extend(kit.field_runs("REF %s \\h" % bookmark, "", etree.fromstring(etree.tostring(rpr)) if rpr is not None else None))
41
+ cursor = match.end()
42
+ if cursor < len(text):
43
+ pieces.append(kit.make_run(text[cursor:], etree.fromstring(etree.tostring(rpr)) if rpr is not None else None))
44
+ ppr = paragraph.find(kit.q("pPr"))
45
+ for child in list(paragraph):
46
+ if child is not ppr:
47
+ paragraph.remove(child)
48
+ for piece in pieces:
49
+ paragraph.append(piece)
50
+ return len(matches)
51
+
52
+
53
+ def main(argv: list[str] | None = None) -> int:
54
+ parser = argparse.ArgumentParser(description="Replace [[REF:bookmark]] markers with REF cross-reference fields.")
55
+ parser.add_argument("input_docx", help="Input .docx")
56
+ parser.add_argument("output_docx", help="Output .docx")
57
+ parser.add_argument("--pattern", default=r"\[\[REF:(?P<bookmark>[A-Za-z0-9_:\-]+)\]\]", help="Regex for markers; must contain a 'bookmark' group")
58
+ parser.add_argument("--prefix", default="", help="Text inserted immediately before each field")
59
+ args = parser.parse_args(argv)
60
+
61
+ try:
62
+ pattern = re.compile(args.pattern)
63
+ except re.error as exc:
64
+ print("error: bad --pattern: %s" % exc, file=sys.stderr)
65
+ return 1
66
+ if "bookmark" not in pattern.groupindex:
67
+ print("error: --pattern needs a named group 'bookmark'", file=sys.stderr)
68
+ return 1
69
+
70
+ try:
71
+ doc = kit.Docx(args.input_docx)
72
+ except (OSError, KeyError) as exc:
73
+ print("error: %s" % exc, file=sys.stderr)
74
+ return 2
75
+
76
+ replaced = 0
77
+ touched = 0
78
+ for paragraph in doc.paragraphs():
79
+ count = rebuild(paragraph, pattern, args.prefix)
80
+ if count:
81
+ replaced += count
82
+ touched += 1
83
+
84
+ doc.save(args.output_docx)
85
+ kit.emit({"ok": True, "output": str(Path(args.output_docx)), "fields": replaced, "paragraphs": touched})
86
+ return 0
87
+
88
+
89
+ if __name__ == "__main__":
90
+ raise SystemExit(main())
@@ -0,0 +1,84 @@
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
+
24
+ def level_range(raw: str) -> str:
25
+ cleaned = raw.replace(" ", "")
26
+ if "-" in cleaned:
27
+ low, high = cleaned.split("-", 1)
28
+ if low.isdigit() and high.isdigit() and 1 <= int(low) <= int(high) <= 9:
29
+ return "%s-%s" % (low, high)
30
+ raise argparse.ArgumentTypeError("--levels must look like 1-3")
31
+
32
+
33
+ def request_update(settings) -> None:
34
+ for node in settings.findall(kit.q("updateFields")):
35
+ settings.remove(node)
36
+ etree.SubElement(settings, kit.q("updateFields")).set(kit.q("val"), "true")
37
+
38
+
39
+ def main(argv: list[str] | None = None) -> int:
40
+ parser = argparse.ArgumentParser(description="Insert a TOC field where a placeholder paragraph sits.")
41
+ parser.add_argument("docx", help="Input .docx")
42
+ parser.add_argument("--out", default=None, help="Output .docx (default: overwrite the input)")
43
+ parser.add_argument("--placeholder", default="[[TOC]]", help="Paragraph text token to replace")
44
+ parser.add_argument("--levels", default="1-3", help='Heading levels to include, e.g. "1-3"')
45
+ args = parser.parse_args(argv)
46
+
47
+ try:
48
+ levels = level_range(args.levels)
49
+ except argparse.ArgumentTypeError as exc:
50
+ print("error: %s" % exc, file=sys.stderr)
51
+ return 2
52
+
53
+ try:
54
+ doc = kit.Docx(args.docx)
55
+ except (OSError, KeyError) as exc:
56
+ print("error: %s" % exc, file=sys.stderr)
57
+ return 2
58
+
59
+ target = None
60
+ for paragraph in doc.paragraphs():
61
+ if args.placeholder in kit.paragraph_text(paragraph):
62
+ target = paragraph
63
+ break
64
+ if target is None:
65
+ print("error: placeholder %r not found" % args.placeholder, file=sys.stderr)
66
+ return 1
67
+
68
+ parent = target.getparent()
69
+ position = parent.index(target)
70
+ root = etree.Element(kit.q("p"))
71
+ for run in kit.field_runs('#TOC \\o "%s" \\h \\z \\u' % levels, "Right-click and choose Update Field to build the contents."):
72
+ root.append(run)
73
+ parent.remove(target)
74
+ parent.insert(position, root)
75
+
76
+ request_update(doc.xml("word/settings.xml"))
77
+ out = args.out or args.docx
78
+ doc.save(out)
79
+ kit.emit({"ok": True, "output": str(Path(out)), "levels": levels, "paragraph_index": position})
80
+ return 0
81
+
82
+
83
+ if __name__ == "__main__":
84
+ raise SystemExit(main())
@@ -0,0 +1,146 @@
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
+ TOP_BOOKMARK = "nav_top"
25
+ BOTTOM_BOOKMARK = "nav_bottom"
26
+ TOC_BOOKMARK = "nav_toc"
27
+
28
+
29
+ def link_run(text: str, anchor: str):
30
+ link = etree.Element(kit.q("hyperlink"))
31
+ link.set(kit.q("anchor"), anchor)
32
+ link.append(kit.make_run(text))
33
+ return link
34
+
35
+
36
+ def add_bookmark(paragraph, name: str, ident: int) -> None:
37
+ start = etree.Element(kit.q("bookmarkStart"))
38
+ start.set(kit.q("id"), str(ident))
39
+ start.set(kit.q("name"), name)
40
+ end = etree.Element(kit.q("bookmarkEnd"))
41
+ end.set(kit.q("id"), str(ident))
42
+ position = 1 if paragraph.find(kit.q("pPr")) is not None else 0
43
+ paragraph.insert(position, start)
44
+ paragraph.append(end)
45
+
46
+
47
+ def paragraph(style: str | None = None):
48
+ node = etree.Element(kit.q("p"))
49
+ if style:
50
+ props = etree.SubElement(node, kit.q("pPr"))
51
+ etree.SubElement(props, kit.q("pStyle")).set(kit.q("val"), style)
52
+ return node
53
+
54
+
55
+ def main(argv: list[str] | None = None) -> int:
56
+ parser = argparse.ArgumentParser(description="Add bookmark anchors and static internal links to headings.")
57
+ parser.add_argument("input_docx", help="Input .docx")
58
+ parser.add_argument("--out", required=True, help="Output .docx")
59
+ parser.add_argument("--toc_title", default="Table of Contents")
60
+ parser.add_argument("--levels", nargs="+", type=int, default=[1, 2, 3], help="Heading levels to include")
61
+ parser.add_argument("--no_quicklinks", action="store_true", help="Skip the quick links bar")
62
+ parser.add_argument("--no_back_to_toc", action="store_true", help="Skip per-heading back links")
63
+ parser.add_argument("--no_top_bottom", action="store_true", help="Skip Top/Bottom anchors")
64
+ args = parser.parse_args(argv)
65
+
66
+ try:
67
+ doc = kit.Docx(args.input_docx)
68
+ except (OSError, KeyError) as exc:
69
+ print("error: %s" % exc, file=sys.stderr)
70
+ return 2
71
+
72
+ body = doc.body()
73
+ headings = []
74
+ ident = doc.next_id()
75
+ for paragraph_node in list(body.iter(kit.q("p"))):
76
+ level = kit.heading_level(paragraph_node)
77
+ text = kit.paragraph_text(paragraph_node).strip()
78
+ if level in args.levels and text:
79
+ name = "nav_h%d" % ident
80
+ add_bookmark(paragraph_node, name, ident)
81
+ headings.append({"level": level, "text": text, "anchor": name})
82
+ ident += 1
83
+
84
+ if not headings:
85
+ print("error: document has no headings at levels %s" % args.levels, file=sys.stderr)
86
+ return 1
87
+
88
+ first = body.find(kit.q("p"))
89
+ if first is None:
90
+ first = etree.SubElement(body, kit.q("p"))
91
+ position = body.index(first)
92
+
93
+ block = []
94
+ if not args.no_top_bottom:
95
+ top = paragraph()
96
+ add_bookmark(top, TOP_BOOKMARK, ident)
97
+ ident += 1
98
+ block.append(top)
99
+
100
+ title = paragraph("Heading1")
101
+ add_bookmark(title, TOC_BOOKMARK, ident)
102
+ ident += 1
103
+ title.append(kit.make_run(args.toc_title))
104
+ block.append(title)
105
+
106
+ if not args.no_quicklinks:
107
+ quick = paragraph()
108
+ quick.append(kit.make_run("Quick links: "))
109
+ for index, item in enumerate([entry for entry in headings if entry["level"] == 1] or headings):
110
+ if index:
111
+ quick.append(kit.make_run(" | "))
112
+ quick.append(link_run(item["text"], item["anchor"]))
113
+ block.append(quick)
114
+
115
+ for item in headings:
116
+ entry = paragraph("TOC%d" % item["level"] if item["level"] <= 3 else "TOC3")
117
+ props = entry.find(kit.q("pPr"))
118
+ ind = etree.SubElement(props, kit.q("ind"))
119
+ ind.set(kit.q("left"), str((item["level"] - 1) * 360))
120
+ entry.append(link_run(item["text"], item["anchor"]))
121
+ block.append(entry)
122
+
123
+ for offset, node in enumerate(block):
124
+ body.insert(position + offset, node)
125
+
126
+ if not args.no_back_to_toc:
127
+ for paragraph_node in list(body.iter(kit.q("p"))):
128
+ 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):
129
+ back = paragraph("Caption")
130
+ back.append(link_run("Back to contents", TOC_BOOKMARK))
131
+ paragraph_node.addnext(back)
132
+
133
+ if not args.no_top_bottom:
134
+ last = paragraph()
135
+ add_bookmark(last, BOTTOM_BOOKMARK, ident)
136
+ ident += 1
137
+ last.append(link_run("Back to top", TOP_BOOKMARK))
138
+ body.append(last)
139
+
140
+ doc.save(args.out)
141
+ kit.emit({"ok": True, "output": str(Path(args.out)), "headings": headings, "entries": len(block)})
142
+ return 0
143
+
144
+
145
+ if __name__ == "__main__":
146
+ raise SystemExit(main())