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.
Files changed (197) hide show
  1. package/CAPABILITY.md +147 -68
  2. package/README.md +3 -1
  3. package/assets/skills/excel-live-control/SKILL.md +234 -0
  4. package/assets/skills/excel-live-control/container_tools/live_excel.py +1589 -0
  5. package/assets/skills/excel-live-control/domain_guidance/financial_models.md +86 -0
  6. package/assets/skills/excel-live-control/domain_guidance/healthcare.md +58 -0
  7. package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +53 -0
  8. package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +53 -0
  9. package/assets/skills/excel-live-control/references/office-automation.md +207 -0
  10. package/assets/skills/excel-live-control/references/read_only_qna.md +59 -0
  11. package/assets/skills/excel-live-control/style_guidelines.md +87 -0
  12. package/assets/skills/excel-live-control/workflows/create_workflows.md +78 -0
  13. package/assets/skills/excel-live-control/workflows/edit_workflows.md +72 -0
  14. package/assets/skills/office-documents/SKILL.md +90 -77
  15. package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +86 -0
  16. package/assets/skills/office-documents/ooxml/comments.md +40 -0
  17. package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +66 -0
  18. package/assets/skills/office-documents/ooxml/rels_and_content_types.md +62 -0
  19. package/assets/skills/office-documents/ooxml/tracked_changes.md +53 -0
  20. package/assets/skills/office-documents/references/docx-structure.md +83 -0
  21. package/assets/skills/office-documents/references/template-elicitation.md +49 -0
  22. package/assets/skills/office-documents/references/writing-quality.md +49 -0
  23. package/assets/skills/office-documents/render_docx.py +99 -0
  24. package/assets/skills/office-documents/scripts/a11y_audit.py +201 -0
  25. package/assets/skills/office-documents/scripts/accept_tracked_changes.py +158 -0
  26. package/assets/skills/office-documents/scripts/add_tracked_replacements.py +155 -0
  27. package/assets/skills/office-documents/scripts/append_blocks.py +115 -0
  28. package/assets/skills/office-documents/scripts/apply_template_styles.py +111 -0
  29. package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +109 -0
  30. package/assets/skills/office-documents/scripts/comments_add.py +143 -0
  31. package/assets/skills/office-documents/scripts/comments_apply_patch.py +121 -0
  32. package/assets/skills/office-documents/scripts/comments_extract.py +92 -0
  33. package/assets/skills/office-documents/scripts/comments_strip.py +81 -0
  34. package/assets/skills/office-documents/scripts/content_controls.py +199 -0
  35. package/assets/skills/office-documents/scripts/core_properties.py +131 -0
  36. package/assets/skills/office-documents/scripts/docx_kit.py +594 -0
  37. package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +232 -0
  38. package/assets/skills/office-documents/scripts/docx_table_to_csv.py +106 -0
  39. package/assets/skills/office-documents/scripts/fields_materialize.py +73 -0
  40. package/assets/skills/office-documents/scripts/fields_report.py +66 -0
  41. package/assets/skills/office-documents/scripts/flatten_ref_fields.py +69 -0
  42. package/assets/skills/office-documents/scripts/footnotes_report.py +84 -0
  43. package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +103 -0
  44. package/assets/skills/office-documents/scripts/heading_audit.py +88 -0
  45. package/assets/skills/office-documents/scripts/images_audit.py +87 -0
  46. package/assets/skills/office-documents/scripts/insert_note.py +133 -0
  47. package/assets/skills/office-documents/scripts/insert_ref_fields.py +97 -0
  48. package/assets/skills/office-documents/scripts/insert_toc.py +91 -0
  49. package/assets/skills/office-documents/scripts/internal_nav.py +153 -0
  50. package/assets/skills/office-documents/scripts/make_fixtures.py +176 -0
  51. package/assets/skills/office-documents/scripts/merge_docx_append.py +246 -0
  52. package/assets/skills/office-documents/scripts/privacy_scrub.py +138 -0
  53. package/assets/skills/office-documents/scripts/redact_docx.py +120 -0
  54. package/assets/skills/office-documents/scripts/render_and_diff.py +108 -0
  55. package/assets/skills/office-documents/scripts/replace_text.py +161 -0
  56. package/assets/skills/office-documents/scripts/section_audit.py +98 -0
  57. package/assets/skills/office-documents/scripts/set_protection.py +99 -0
  58. package/assets/skills/office-documents/scripts/style_lint.py +118 -0
  59. package/assets/skills/office-documents/scripts/style_normalize.py +110 -0
  60. package/assets/skills/office-documents/scripts/table_geometry.py +174 -0
  61. package/assets/skills/office-documents/scripts/watermark_add.py +55 -0
  62. package/assets/skills/office-documents/scripts/watermark_audit_remove.py +74 -0
  63. package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +124 -0
  64. package/assets/skills/office-documents/tasks/accessibility_a11y.md +45 -0
  65. package/assets/skills/office-documents/tasks/captions_crossrefs.md +49 -0
  66. package/assets/skills/office-documents/tasks/clean_tracked_changes.md +49 -0
  67. package/assets/skills/office-documents/tasks/comments_manage.md +59 -0
  68. package/assets/skills/office-documents/tasks/compare_diff.md +40 -0
  69. package/assets/skills/office-documents/tasks/create_edit.md +107 -0
  70. package/assets/skills/office-documents/tasks/equations.md +59 -0
  71. package/assets/skills/office-documents/tasks/fields_update.md +44 -0
  72. package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +50 -0
  73. package/assets/skills/office-documents/tasks/footnotes_endnotes.md +38 -0
  74. package/assets/skills/office-documents/tasks/forms_content_controls.md +48 -0
  75. package/assets/skills/office-documents/tasks/headings_numbering.md +55 -0
  76. package/assets/skills/office-documents/tasks/images_figures.md +63 -0
  77. package/assets/skills/office-documents/tasks/multi_doc_merge.md +43 -0
  78. package/assets/skills/office-documents/tasks/navigation_internal_links.md +38 -0
  79. package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +45 -0
  80. package/assets/skills/office-documents/tasks/protection_restrict_editing.md +41 -0
  81. package/assets/skills/office-documents/tasks/read_review.md +50 -0
  82. package/assets/skills/office-documents/tasks/redaction_anonymization.md +42 -0
  83. package/assets/skills/office-documents/tasks/sections_layout.md +54 -0
  84. package/assets/skills/office-documents/tasks/style_lint_normalize.md +44 -0
  85. package/assets/skills/office-documents/tasks/tables_spreadsheets.md +64 -0
  86. package/assets/skills/office-documents/tasks/templates_style_packs.md +53 -0
  87. package/assets/skills/office-documents/tasks/toc_workflow.md +39 -0
  88. package/assets/skills/office-documents/tasks/verify_render.md +50 -0
  89. package/assets/skills/office-documents/tasks/watermarks_background.md +35 -0
  90. package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +47 -0
  91. package/assets/skills/office-documents/troubleshooting/run_splitting.md +36 -0
  92. package/assets/skills/office-pdf/SKILL.md +73 -56
  93. package/assets/skills/office-pdf/references/form-fields.md +56 -0
  94. package/assets/skills/office-pdf/references/geometry.md +44 -0
  95. package/assets/skills/office-pdf/references/layout-quality.md +41 -0
  96. package/assets/skills/office-pdf/references/limits.md +58 -0
  97. package/assets/skills/office-pdf/scripts/extract_text.py +172 -0
  98. package/assets/skills/office-pdf/scripts/forms.py +556 -0
  99. package/assets/skills/office-pdf/scripts/metadata.py +161 -0
  100. package/assets/skills/office-pdf/scripts/page_ops.py +198 -0
  101. package/assets/skills/office-pdf/scripts/redact_region.py +347 -0
  102. package/assets/skills/office-pdf/scripts/render_pages.py +207 -0
  103. package/assets/skills/office-pdf/scripts/split_pages.py +155 -0
  104. package/assets/skills/office-pdf/scripts/stamp_overlay.py +199 -0
  105. package/assets/skills/office-pdf/tasks/annotations.md +98 -0
  106. package/assets/skills/office-pdf/tasks/create.md +75 -0
  107. package/assets/skills/office-pdf/tasks/edit-pages.md +71 -0
  108. package/assets/skills/office-pdf/tasks/forms.md +67 -0
  109. package/assets/skills/office-pdf/tasks/read.md +62 -0
  110. package/assets/skills/office-pdf/tasks/redact.md +72 -0
  111. package/assets/skills/office-pdf/tasks/stamp.md +60 -0
  112. package/assets/skills/office-presentations/SKILL.md +141 -87
  113. package/assets/skills/office-presentations/container_tools/add_slides.py +143 -0
  114. package/assets/skills/office-presentations/container_tools/core_properties.py +127 -0
  115. package/assets/skills/office-presentations/container_tools/create_montage.py +133 -0
  116. package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +292 -0
  117. package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +164 -0
  118. package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +151 -0
  119. package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +206 -0
  120. package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +219 -0
  121. package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +352 -0
  122. package/assets/skills/office-presentations/container_tools/preview_render.py +382 -0
  123. package/assets/skills/office-presentations/container_tools/render_slides.py +146 -0
  124. package/assets/skills/office-presentations/container_tools/replace_text.py +175 -0
  125. package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +236 -0
  126. package/assets/skills/office-presentations/domain_guidance/finance.md +66 -0
  127. package/assets/skills/office-presentations/recipes/README.md +71 -0
  128. package/assets/skills/office-presentations/recipes/autolayout.md +88 -0
  129. package/assets/skills/office-presentations/recipes/connectors.md +83 -0
  130. package/assets/skills/office-presentations/recipes/images.md +80 -0
  131. package/assets/skills/office-presentations/recipes/master-and-layouts.md +127 -0
  132. package/assets/skills/office-presentations/recipes/native-charts.md +87 -0
  133. package/assets/skills/office-presentations/recipes/rich-text-runs.md +86 -0
  134. package/assets/skills/office-presentations/recipes/speaker-notes.md +65 -0
  135. package/assets/skills/office-presentations/recipes/tables.md +88 -0
  136. package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +71 -0
  137. package/assets/skills/office-presentations/recipes/theme-tokens.md +87 -0
  138. package/assets/skills/office-presentations/references/authoring-workflow.md +65 -0
  139. package/assets/skills/office-presentations/references/finalization.md +63 -0
  140. package/assets/skills/office-presentations/references/native-bullets.md +78 -0
  141. package/assets/skills/office-presentations/references/native-evidence.md +67 -0
  142. package/assets/skills/office-presentations/references/style-guidelines.md +66 -0
  143. package/assets/skills/office-presentations/references/templates.md +76 -0
  144. package/assets/skills/office-presentations/routing/google_slides.md +45 -0
  145. package/assets/skills/office-presentations/tasks/apply-annotations.md +83 -0
  146. package/assets/skills/office-presentations/tasks/charts-tables-notes.md +79 -0
  147. package/assets/skills/office-presentations/tasks/create-deck.md +106 -0
  148. package/assets/skills/office-presentations/tasks/edit-deck.md +93 -0
  149. package/assets/skills/office-presentations/tasks/inspect-and-verify.md +107 -0
  150. package/assets/skills/office-presentations/tasks/template-following.md +132 -0
  151. package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py +183 -0
  152. package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +161 -0
  153. package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +118 -0
  154. package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +246 -0
  155. package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +192 -0
  156. package/assets/skills/office-spreadsheets/SKILL.md +105 -101
  157. package/assets/skills/office-spreadsheets/container_tools/add_chart.py +243 -0
  158. package/assets/skills/office-spreadsheets/container_tools/add_image.py +114 -0
  159. package/assets/skills/office-spreadsheets/container_tools/append_rows.py +129 -0
  160. package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +313 -0
  161. package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +405 -0
  162. package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +287 -0
  163. package/assets/skills/office-spreadsheets/container_tools/set_cells.py +142 -0
  164. package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +141 -0
  165. package/assets/skills/office-spreadsheets/container_tools/style_cells.py +140 -0
  166. package/assets/skills/office-spreadsheets/recipes/charts.md +66 -0
  167. package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +69 -0
  168. package/assets/skills/office-spreadsheets/recipes/data-tables.md +58 -0
  169. package/assets/skills/office-spreadsheets/recipes/named-ranges.md +41 -0
  170. package/assets/skills/office-spreadsheets/recipes/number-formats.md +72 -0
  171. package/assets/skills/office-spreadsheets/recipes/sparklines.md +43 -0
  172. package/assets/skills/office-spreadsheets/references/charts.md +63 -0
  173. package/assets/skills/office-spreadsheets/references/domain/financial-models.md +40 -0
  174. package/assets/skills/office-spreadsheets/references/domain/healthcare.md +22 -0
  175. package/assets/skills/office-spreadsheets/references/domain/marketing.md +28 -0
  176. package/assets/skills/office-spreadsheets/references/domain/research.md +22 -0
  177. package/assets/skills/office-spreadsheets/references/formula-patterns.md +78 -0
  178. package/assets/skills/office-spreadsheets/references/style-guidelines.md +69 -0
  179. package/assets/skills/office-spreadsheets/references/workbook-structure.md +60 -0
  180. package/assets/skills/office-spreadsheets/routing/google-sheets.md +36 -0
  181. package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +37 -0
  182. package/assets/skills/office-spreadsheets/tasks/annotations.md +51 -0
  183. package/assets/skills/office-spreadsheets/tasks/create-workbook.md +66 -0
  184. package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +38 -0
  185. package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +69 -0
  186. package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +38 -0
  187. package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +57 -0
  188. package/assets/skills/shared/annotation-recipes.md +252 -0
  189. package/assets/skills/shared/check_runtime.py +260 -0
  190. package/assets/skills/shared/common-workflow.md +139 -0
  191. package/assets/skills/shared/scripts/ensure_raster_image.py +187 -0
  192. package/dist/client.js +4 -4
  193. package/dist/client.js.map +1 -1
  194. package/dist/index.js +246 -89
  195. package/package.json +1 -1
  196. package/postinstall.mjs +92 -82
  197. 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())