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,594 @@
|
|
|
1
|
+
"""Shared OOXML plumbing for the office-documents scripts.
|
|
2
|
+
|
|
3
|
+
Every other script in this directory imports this module after adding its own
|
|
4
|
+
folder to ``sys.path``. It packages the small set of zip + lxml operations the
|
|
5
|
+
WordprocessingML helpers need (part access, relationship and content-type
|
|
6
|
+
bookkeeping, run/paragraph utilities, field scanning) so each script stays a
|
|
7
|
+
thin, single-purpose CLI.
|
|
8
|
+
|
|
9
|
+
Also usable on its own:
|
|
10
|
+
|
|
11
|
+
python docx_kit.py parts input.docx
|
|
12
|
+
python docx_kit.py text input.docx
|
|
13
|
+
python docx_kit.py xml input.docx word/document.xml
|
|
14
|
+
|
|
15
|
+
Exit codes: 0 success, 1 not found / unreadable, 2 usage error.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import posixpath
|
|
24
|
+
import shutil
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
import zipfile
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from lxml import etree
|
|
31
|
+
|
|
32
|
+
# Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
|
|
33
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
34
|
+
try:
|
|
35
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
36
|
+
except (AttributeError, ValueError):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
40
|
+
R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
41
|
+
A = "http://schemas.openxmlformats.org/drawingml/2006/main"
|
|
42
|
+
WP = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
|
|
43
|
+
V = "urn:schemas-microsoft-com:vml"
|
|
44
|
+
W15 = "http://schemas.microsoft.com/office/word/2012/wordml"
|
|
45
|
+
XML = "http://www.w3.org/XML/1998/namespace"
|
|
46
|
+
|
|
47
|
+
CT_RELS = "application/vnd.openxmlformats-package.relationships+xml"
|
|
48
|
+
REL_HEADER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header"
|
|
49
|
+
REL_FOOTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
|
|
50
|
+
REL_COMMENTS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
|
|
51
|
+
REL_FOOTNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes"
|
|
52
|
+
REL_ENDNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes"
|
|
53
|
+
REL_STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"
|
|
54
|
+
REL_NUMBERING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering"
|
|
55
|
+
REL_SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings"
|
|
56
|
+
|
|
57
|
+
CT_BY_REL = {
|
|
58
|
+
REL_COMMENTS: "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml",
|
|
59
|
+
REL_FOOTNOTES: "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
|
|
60
|
+
REL_ENDNOTES: "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml",
|
|
61
|
+
REL_HEADER: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
|
|
62
|
+
REL_FOOTER: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Sections of a document whose text and revisions the scripts touch.
|
|
66
|
+
TEXT_PARTS = (
|
|
67
|
+
"word/document.xml",
|
|
68
|
+
"word/footnotes.xml",
|
|
69
|
+
"word/endnotes.xml",
|
|
70
|
+
"word/comments.xml",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def q(tag: str, ns: str = W) -> str:
|
|
75
|
+
return "{%s}%s" % (ns, tag)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def rels_name(part: str) -> str:
|
|
79
|
+
folder, base = posixpath.split(part)
|
|
80
|
+
return posixpath.join(folder, "_rels", base + ".rels")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def resolve_target(part: str, target: str) -> str:
|
|
84
|
+
if target.startswith("/"):
|
|
85
|
+
return target.lstrip("/")
|
|
86
|
+
return posixpath.normpath(posixpath.join(posixpath.dirname(part), target))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def text_of(element) -> str:
|
|
90
|
+
"""Concatenated w:t text below an element (deleted text included)."""
|
|
91
|
+
chunks = []
|
|
92
|
+
for node in element.iter():
|
|
93
|
+
if node.tag in (q("t"), q("delText")):
|
|
94
|
+
chunks.append(node.text or "")
|
|
95
|
+
return "".join(chunks)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def paragraph_text(paragraph) -> str:
|
|
99
|
+
return text_of(paragraph)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Docx:
|
|
103
|
+
"""In-memory copy of a .docx package with parse/serialize helpers."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, path: str | Path):
|
|
106
|
+
self.path = Path(path)
|
|
107
|
+
if not self.path.is_file():
|
|
108
|
+
raise FileNotFoundError(str(self.path))
|
|
109
|
+
with zipfile.ZipFile(self.path) as zf:
|
|
110
|
+
self.order = zf.namelist()
|
|
111
|
+
self.parts = {name: zf.read(name) for name in self.order}
|
|
112
|
+
self._cache: dict[str, etree._Element] = {}
|
|
113
|
+
|
|
114
|
+
# -- parts -------------------------------------------------------------
|
|
115
|
+
def exists(self, part: str) -> bool:
|
|
116
|
+
return part in self.parts
|
|
117
|
+
|
|
118
|
+
def names(self, prefix: str = "") -> list[str]:
|
|
119
|
+
return [name for name in self.parts if name.startswith(prefix)]
|
|
120
|
+
|
|
121
|
+
def xml(self, part: str) -> etree._Element:
|
|
122
|
+
if part not in self.parts:
|
|
123
|
+
raise KeyError(part)
|
|
124
|
+
if part not in self._cache:
|
|
125
|
+
parser = etree.XMLParser(remove_blank_text=False, huge_tree=True)
|
|
126
|
+
self._cache[part] = etree.fromstring(self.parts[part], parser)
|
|
127
|
+
return self._cache[part]
|
|
128
|
+
|
|
129
|
+
def set_xml(self, part: str, root) -> None:
|
|
130
|
+
self._cache[part] = root
|
|
131
|
+
if part not in self.parts:
|
|
132
|
+
self.parts[part] = b""
|
|
133
|
+
self.order.append(part)
|
|
134
|
+
|
|
135
|
+
def drop(self, part: str) -> None:
|
|
136
|
+
self.parts.pop(part, None)
|
|
137
|
+
self._cache.pop(part, None)
|
|
138
|
+
if part in self.order:
|
|
139
|
+
self.order.remove(part)
|
|
140
|
+
|
|
141
|
+
# -- relationships and content types -----------------------------------
|
|
142
|
+
def rels(self, part: str) -> etree._Element:
|
|
143
|
+
name = rels_name(part)
|
|
144
|
+
if name not in self.parts:
|
|
145
|
+
root = etree.Element("{http://schemas.openxmlformats.org/package/2006/relationships}Relationships", nsmap={None: "http://schemas.openxmlformats.org/package/2006/relationships"})
|
|
146
|
+
self.parts[name] = b"<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"/>"
|
|
147
|
+
self.order.append(name)
|
|
148
|
+
self._cache.pop(name, None)
|
|
149
|
+
return self.xml(name)
|
|
150
|
+
return self.xml(name)
|
|
151
|
+
|
|
152
|
+
def relationships(self, part: str) -> list[dict]:
|
|
153
|
+
if rels_name(part) not in self.parts:
|
|
154
|
+
return []
|
|
155
|
+
ns = "{http://schemas.openxmlformats.org/package/2006/relationships}"
|
|
156
|
+
out = []
|
|
157
|
+
for node in self.xml(rels_name(part)).findall(ns + "Relationship"):
|
|
158
|
+
out.append(
|
|
159
|
+
{
|
|
160
|
+
"id": node.get("Id"),
|
|
161
|
+
"type": node.get("Type"),
|
|
162
|
+
"target": node.get("Target"),
|
|
163
|
+
"mode": node.get("TargetMode"),
|
|
164
|
+
"resolved": resolve_target(part, node.get("Target") or ""),
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
return out
|
|
168
|
+
|
|
169
|
+
def ensure_rel(self, part: str, rel_type: str, target: str, external: bool = False) -> str:
|
|
170
|
+
"""Return the rId for target, adding the relationship when absent."""
|
|
171
|
+
for rel in self.relationships(part):
|
|
172
|
+
if rel["type"] != rel_type:
|
|
173
|
+
continue
|
|
174
|
+
if external and rel["mode"] == "External" and rel["target"] == target:
|
|
175
|
+
return rel["id"]
|
|
176
|
+
if not external and rel["resolved"] == resolve_target(part, target):
|
|
177
|
+
return rel["id"]
|
|
178
|
+
root = self.rels(part)
|
|
179
|
+
ns = "{http://schemas.openxmlformats.org/package/2006/relationships}"
|
|
180
|
+
ids = [int(n.get("Id")[3:]) for n in root.findall(ns + "Relationship") if (n.get("Id") or "").startswith("rId") and (n.get("Id") or "")[3:].isdigit()]
|
|
181
|
+
rid = "rId%d" % (max(ids, default=0) + 1)
|
|
182
|
+
node = etree.SubElement(root, ns + "Relationship")
|
|
183
|
+
node.set("Id", rid)
|
|
184
|
+
node.set("Type", rel_type)
|
|
185
|
+
node.set("Target", target)
|
|
186
|
+
if external:
|
|
187
|
+
node.set("TargetMode", "External")
|
|
188
|
+
return rid
|
|
189
|
+
|
|
190
|
+
def drop_rel_target(self, part: str, resolved: str) -> None:
|
|
191
|
+
if rels_name(part) not in self.parts:
|
|
192
|
+
return
|
|
193
|
+
root = self.rels(part)
|
|
194
|
+
ns = "{http://schemas.openxmlformats.org/package/2006/relationships}"
|
|
195
|
+
for node in list(root.findall(ns + "Relationship")):
|
|
196
|
+
if resolve_target(part, node.get("Target") or "") == resolved:
|
|
197
|
+
root.remove(node)
|
|
198
|
+
|
|
199
|
+
def content_types(self) -> etree._Element:
|
|
200
|
+
return self.xml("[Content_Types].xml")
|
|
201
|
+
|
|
202
|
+
def ensure_override(self, part: str, content_type: str) -> None:
|
|
203
|
+
root = self.content_types()
|
|
204
|
+
ct = "{http://schemas.openxmlformats.org/package/2006/content-types}"
|
|
205
|
+
part_name = "/" + part
|
|
206
|
+
for node in root.findall(ct + "Override"):
|
|
207
|
+
if node.get("PartName") == part_name:
|
|
208
|
+
node.set("ContentType", content_type)
|
|
209
|
+
return
|
|
210
|
+
node = etree.SubElement(root, ct + "Override")
|
|
211
|
+
node.set("PartName", part_name)
|
|
212
|
+
node.set("ContentType", content_type)
|
|
213
|
+
|
|
214
|
+
def drop_override(self, part: str) -> None:
|
|
215
|
+
root = self.content_types()
|
|
216
|
+
ct = "{http://schemas.openxmlformats.org/package/2006/content-types}"
|
|
217
|
+
for node in list(root.findall(ct + "Override")):
|
|
218
|
+
if node.get("PartName") == "/" + part:
|
|
219
|
+
root.remove(node)
|
|
220
|
+
|
|
221
|
+
# -- document structure ------------------------------------------------
|
|
222
|
+
def body(self):
|
|
223
|
+
return self.xml("word/document.xml").find(q("body"))
|
|
224
|
+
|
|
225
|
+
def paragraphs(self, part: str = "word/document.xml"):
|
|
226
|
+
return list(self.xml(part).iter(q("p")))
|
|
227
|
+
|
|
228
|
+
def text_parts(self) -> list[str]:
|
|
229
|
+
return [name for name in TEXT_PARTS if self.exists(name)]
|
|
230
|
+
|
|
231
|
+
def sections(self):
|
|
232
|
+
return [node for node in self.xml("word/document.xml").iter(q("sectPr"))]
|
|
233
|
+
|
|
234
|
+
def ensure_header_footer(self, kind: str) -> str:
|
|
235
|
+
"""Return the part name for this section's header/footer, creating it."""
|
|
236
|
+
body = self.body()
|
|
237
|
+
sect = body.find(q("sectPr")) if body is not None else None
|
|
238
|
+
if sect is None:
|
|
239
|
+
sect = etree.SubElement(body, q("sectPr"))
|
|
240
|
+
tag = q("headerReference") if kind == "header" else q("footerReference")
|
|
241
|
+
rel_type = REL_HEADER if kind == "header" else REL_FOOTER
|
|
242
|
+
refs = sect.findall(tag)
|
|
243
|
+
for ref in refs:
|
|
244
|
+
rid = ref.get(q("id", R))
|
|
245
|
+
for rel in self.relationships("word/document.xml"):
|
|
246
|
+
if rel["id"] == rid and rel["resolved"]:
|
|
247
|
+
if self.exists(rel["resolved"]):
|
|
248
|
+
return rel["resolved"]
|
|
249
|
+
index = 1
|
|
250
|
+
while self.exists("word/%s%d.xml" % (kind, index)):
|
|
251
|
+
index += 1
|
|
252
|
+
part = "word/%s%d.xml" % (kind, index)
|
|
253
|
+
root = etree.Element(q(kind), nsmap={"w": W, "r": R, "wp": WP, "v": V})
|
|
254
|
+
etree.SubElement(etree.SubElement(root, q("p")), q("r"))
|
|
255
|
+
self.set_xml(part, root)
|
|
256
|
+
self.ensure_override(part, CT_BY_REL[rel_type])
|
|
257
|
+
rid = self.ensure_rel("word/document.xml", rel_type, posixpath.basename(part))
|
|
258
|
+
ref = etree.Element(tag)
|
|
259
|
+
ref.set(q("type"), "default")
|
|
260
|
+
ref.set(q("id", R), rid)
|
|
261
|
+
if kind == "header":
|
|
262
|
+
sect.insert(0, ref)
|
|
263
|
+
else:
|
|
264
|
+
position = 0
|
|
265
|
+
for child in sect:
|
|
266
|
+
if child.tag in (q("headerReference"), q("footerReference")):
|
|
267
|
+
position += 1
|
|
268
|
+
else:
|
|
269
|
+
break
|
|
270
|
+
sect.insert(position, ref)
|
|
271
|
+
return part
|
|
272
|
+
|
|
273
|
+
# -- ids ---------------------------------------------------------------
|
|
274
|
+
def next_id(self) -> int:
|
|
275
|
+
best = 0
|
|
276
|
+
for name in self.text_parts() + ["word/document.xml"]:
|
|
277
|
+
for node in self.xml(name).iter():
|
|
278
|
+
for key, value in node.attrib.items():
|
|
279
|
+
if key.endswith("}id") and value.isdigit():
|
|
280
|
+
best = max(best, int(value))
|
|
281
|
+
return best + 1
|
|
282
|
+
|
|
283
|
+
# -- save --------------------------------------------------------------
|
|
284
|
+
def save(self, out: str | Path) -> Path:
|
|
285
|
+
out = Path(out)
|
|
286
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
287
|
+
for name, root in list(self._cache.items()):
|
|
288
|
+
self.parts[name] = etree.tostring(root, xml_declaration=True, encoding="UTF-8", standalone=True)
|
|
289
|
+
ordered = [name for name in self.order if name in self.parts]
|
|
290
|
+
ordered = ["[Content_Types].xml"] + [n for n in ordered if n != "[Content_Types].xml"]
|
|
291
|
+
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
292
|
+
for name in ordered:
|
|
293
|
+
zf.writestr(name, self.parts[name])
|
|
294
|
+
return out
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# -- run / paragraph helpers ------------------------------------------------
|
|
298
|
+
|
|
299
|
+
def make_run(text: str, rpr=None, tag: str = "t"):
|
|
300
|
+
run = etree.Element(q("r"))
|
|
301
|
+
if rpr is not None:
|
|
302
|
+
run.append(rpr)
|
|
303
|
+
node = etree.SubElement(run, q(tag))
|
|
304
|
+
node.text = text
|
|
305
|
+
node.set(q("space", XML), "preserve")
|
|
306
|
+
return run
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def first_rpr(paragraph):
|
|
310
|
+
for run in paragraph.iter(q("r")):
|
|
311
|
+
rpr = run.find(q("rPr"))
|
|
312
|
+
if rpr is not None:
|
|
313
|
+
return rpr
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def clear_paragraph_content(paragraph, keep=("pPr", "bookmarkStart", "bookmarkEnd")) -> None:
|
|
318
|
+
for child in list(paragraph):
|
|
319
|
+
if child.tag in {q(k) for k in keep}:
|
|
320
|
+
continue
|
|
321
|
+
paragraph.remove(child)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def set_paragraph_text(paragraph, text: str) -> None:
|
|
325
|
+
rpr = first_rpr(paragraph)
|
|
326
|
+
clear_paragraph_content(paragraph)
|
|
327
|
+
paragraph.append(make_run(text, rpr))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def paragraph_style(paragraph) -> str:
|
|
331
|
+
ppr = paragraph.find(q("pPr"))
|
|
332
|
+
if ppr is None:
|
|
333
|
+
return ""
|
|
334
|
+
style = ppr.find(q("pStyle"))
|
|
335
|
+
return style.get(q("val")) if style is not None else ""
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def heading_level(paragraph) -> int:
|
|
339
|
+
style = paragraph_style(paragraph).lower()
|
|
340
|
+
if style == "title":
|
|
341
|
+
return 0
|
|
342
|
+
for prefix in ("heading", "heading"):
|
|
343
|
+
if style.startswith(prefix):
|
|
344
|
+
tail = style[len(prefix):].strip()
|
|
345
|
+
if tail.isdigit():
|
|
346
|
+
return int(tail)
|
|
347
|
+
ppr = paragraph.find(q("pPr"))
|
|
348
|
+
if ppr is not None:
|
|
349
|
+
outline = ppr.find(q("outlineLvl"))
|
|
350
|
+
if outline is not None and (outline.get(q("val")) or "").isdigit():
|
|
351
|
+
return int(outline.get(q("val"))) + 1
|
|
352
|
+
return 0
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# -- fields -----------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
def _field_type(instr: str) -> str:
|
|
358
|
+
token = (instr or "").strip().split(" ")[0] if (instr or "").strip() else ""
|
|
359
|
+
return token.upper()
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def paragraph_fields(paragraph) -> list[dict]:
|
|
363
|
+
"""Return fields in a paragraph as {kind, instr, result, span}."""
|
|
364
|
+
out: list[dict] = []
|
|
365
|
+
start = None
|
|
366
|
+
buffer_instr: list[str] = []
|
|
367
|
+
buffer_result: list[str] = []
|
|
368
|
+
span: list = []
|
|
369
|
+
for child in paragraph:
|
|
370
|
+
if child.tag == q("fldSimple"):
|
|
371
|
+
instr = (child.get(q("instr")) or "").strip()
|
|
372
|
+
out.append({"kind": "simple", "instr": instr, "type": _field_type(instr), "result": text_of(child), "span": [child]})
|
|
373
|
+
continue
|
|
374
|
+
if child.tag != q("r"):
|
|
375
|
+
if start is not None:
|
|
376
|
+
span.append(child)
|
|
377
|
+
buffer_result.append(text_of(child))
|
|
378
|
+
continue
|
|
379
|
+
fld = child.find(q("fldChar"))
|
|
380
|
+
if fld is not None:
|
|
381
|
+
kind = fld.get(q("fldCharType"))
|
|
382
|
+
if kind == "begin":
|
|
383
|
+
start = child
|
|
384
|
+
span = [child]
|
|
385
|
+
buffer_instr = []
|
|
386
|
+
buffer_result = []
|
|
387
|
+
continue
|
|
388
|
+
if kind == "separate":
|
|
389
|
+
if start is not None:
|
|
390
|
+
span.append(child)
|
|
391
|
+
continue
|
|
392
|
+
if kind == "end" and start is not None:
|
|
393
|
+
span.append(child)
|
|
394
|
+
instr = " ".join(part.strip() for part in buffer_instr if part.strip())
|
|
395
|
+
out.append({"kind": "complex", "instr": instr, "type": _field_type(instr), "result": "".join(buffer_result), "span": list(span)})
|
|
396
|
+
start = None
|
|
397
|
+
continue
|
|
398
|
+
if start is not None:
|
|
399
|
+
span.append(child)
|
|
400
|
+
for node in child.findall(q("instrText")):
|
|
401
|
+
buffer_instr.append(node.text or "")
|
|
402
|
+
buffer_result.append(text_of(child))
|
|
403
|
+
return out
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def iter_fields(doc: Docx):
|
|
407
|
+
for part in doc.text_parts():
|
|
408
|
+
for paragraph in doc.paragraphs(part):
|
|
409
|
+
for field in paragraph_fields(paragraph):
|
|
410
|
+
yield part, paragraph, field
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def field_runs(instr: str, result: str = "", rpr=None) -> list:
|
|
414
|
+
begin = etree.Element(q("r"))
|
|
415
|
+
if rpr is not None:
|
|
416
|
+
begin.append(_copy(rpr))
|
|
417
|
+
fld = etree.SubElement(begin, q("fldChar"))
|
|
418
|
+
fld.set(q("fldCharType"), "begin")
|
|
419
|
+
instr_run = etree.Element(q("r"))
|
|
420
|
+
if rpr is not None:
|
|
421
|
+
instr_run.append(_copy(rpr))
|
|
422
|
+
node = etree.SubElement(instr_run, q("instrText"))
|
|
423
|
+
node.text = " %s " % instr
|
|
424
|
+
node.set(q("space", XML), "preserve")
|
|
425
|
+
sep = etree.Element(q("r"))
|
|
426
|
+
if rpr is not None:
|
|
427
|
+
sep.append(_copy(rpr))
|
|
428
|
+
etree.SubElement(sep, q("fldChar")).set(q("fldCharType"), "separate")
|
|
429
|
+
out = [begin, instr_run, sep]
|
|
430
|
+
if result:
|
|
431
|
+
out.append(make_run(result, _copy(rpr) if rpr is not None else None))
|
|
432
|
+
end = etree.Element(q("r"))
|
|
433
|
+
if rpr is not None:
|
|
434
|
+
end.append(_copy(rpr))
|
|
435
|
+
etree.SubElement(end, q("fldChar")).set(q("fldCharType"), "end")
|
|
436
|
+
out.append(end)
|
|
437
|
+
return out
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _copy(node):
|
|
441
|
+
return etree.fromstring(etree.tostring(node))
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def field_simple(instr: str, result: str = "") -> etree._Element:
|
|
445
|
+
node = etree.Element(q("fldSimple"))
|
|
446
|
+
node.set(q("instr"), " %s " % instr)
|
|
447
|
+
if result:
|
|
448
|
+
node.append(make_run(result))
|
|
449
|
+
return node
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def add_bookmark(parent, name: str, ident: int) -> None:
|
|
453
|
+
start = etree.SubElement(parent, q("bookmarkStart"))
|
|
454
|
+
start.set(q("id"), str(ident))
|
|
455
|
+
start.set(q("name"), name)
|
|
456
|
+
end = etree.SubElement(parent, q("bookmarkEnd"))
|
|
457
|
+
end.set(q("id"), str(ident))
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
# -- watermarks -------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
_WATERMARK_SHAPETYPE = """<v:shapetype xmlns:v="urn:schemas-microsoft-com:vml" id="_x0000_t136" coordsize="21600,21600" adj="10800" path="m@7,l@8,m@5,21600l@11,21600e"><v:formulas><v:f eqn="sum #0 0 10800"/><v:f eqn="prod #0 2 1"/><v:f eqn="sum 21600 0 @1"/><v:f eqn="sum 0 0 @2"/><v:f eqn="sum 21600 0 @3"/><v:f eqn="if @0 @3 0"/><v:f eqn="if @0 21600 @1"/><v:f eqn="if @0 0 @2"/><v:f eqn="if @0 @4 21600"/><v:f eqn="mid @5 @6"/><v:f eqn="mid @8 @5"/><v:f eqn="mid @7 @8"/><v:f eqn="mid @6 @7"/><v:f eqn="sum @6 0 @5"/></v:formulas><v:path textpathok="t"/><v:textpath on="t" fitshape="t"/><v:handles><v:h position="#0,bottomRight" xrange="6629,14971"/></v:handles></v:shapetype>"""
|
|
463
|
+
|
|
464
|
+
_WATERMARK_STYLE = (
|
|
465
|
+
"position:absolute;margin-left:0;margin-top:0;width:468pt;height:117pt;"
|
|
466
|
+
"rotation:-45;z-index:-251658752;mso-position-horizontal:center;"
|
|
467
|
+
"mso-position-horizontal-relative:margin;mso-position-vertical:center;"
|
|
468
|
+
"mso-position-vertical-relative:margin"
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def watermark_paragraph(text: str, ident: int = 1, color: str = "#C0C0C0"):
|
|
473
|
+
"""A header paragraph holding one VML text watermark."""
|
|
474
|
+
paragraph = etree.Element(q("p"))
|
|
475
|
+
run = etree.SubElement(paragraph, q("r"))
|
|
476
|
+
run_props = etree.SubElement(run, q("rPr"))
|
|
477
|
+
etree.SubElement(run_props, q("noProof"))
|
|
478
|
+
pict = etree.SubElement(run, q("pict"))
|
|
479
|
+
pict.append(etree.fromstring(_WATERMARK_SHAPETYPE))
|
|
480
|
+
shape = etree.SubElement(pict, q("shape", V))
|
|
481
|
+
shape.set("id", "WaterMarkObject%d" % ident)
|
|
482
|
+
shape.set("type", "#_x0000_t136")
|
|
483
|
+
shape.set("style", _WATERMARK_STYLE)
|
|
484
|
+
shape.set("fillcolor", color)
|
|
485
|
+
shape.set("stroked", "f")
|
|
486
|
+
textpath = etree.SubElement(shape, q("textpath", V))
|
|
487
|
+
textpath.set("style", 'font-family:"Calibri";font-size:1pt')
|
|
488
|
+
textpath.set("string", text)
|
|
489
|
+
return paragraph
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def watermark_texts(doc: Docx) -> list[dict]:
|
|
493
|
+
"""Every VML text watermark in the headers, with the paragraph that holds it."""
|
|
494
|
+
found = []
|
|
495
|
+
for part in doc.names("word/header"):
|
|
496
|
+
for textpath in doc.xml(part).iter(q("textpath", V)):
|
|
497
|
+
paragraph = textpath
|
|
498
|
+
while paragraph is not None and paragraph.tag != q("p"):
|
|
499
|
+
paragraph = paragraph.getparent()
|
|
500
|
+
found.append({"part": part, "text": textpath.get("string"), "paragraph": paragraph})
|
|
501
|
+
return found
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
# -- helper CLI -------------------------------------------------------------
|
|
505
|
+
|
|
506
|
+
def office_cli_path() -> Path | None:
|
|
507
|
+
"""Locate python/office_cli.py: DSH_OFFICE_CLI, then the repo around this skill."""
|
|
508
|
+
override = os.environ.get("DSH_OFFICE_CLI")
|
|
509
|
+
candidates = [Path(override)] if override else []
|
|
510
|
+
here = Path(__file__).resolve()
|
|
511
|
+
for depth in range(1, 7):
|
|
512
|
+
try:
|
|
513
|
+
candidates.append(here.parents[depth] / "python" / "office_cli.py")
|
|
514
|
+
except IndexError:
|
|
515
|
+
break
|
|
516
|
+
for candidate in candidates:
|
|
517
|
+
if candidate.is_file():
|
|
518
|
+
return candidate
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def run_office_cli(argv: list[str], timeout: int = 600) -> tuple[int, str, str]:
|
|
523
|
+
"""Run the helper CLI and return (returncode, stdout, stderr)."""
|
|
524
|
+
cli = office_cli_path()
|
|
525
|
+
if cli is None:
|
|
526
|
+
return 127, "", "helper CLI not found; set DSH_OFFICE_CLI to python/office_cli.py"
|
|
527
|
+
try:
|
|
528
|
+
finished = subprocess.run([sys.executable, str(cli), *argv], capture_output=True, text=True, timeout=timeout)
|
|
529
|
+
except subprocess.TimeoutExpired:
|
|
530
|
+
return 124, "", "helper CLI timed out after %ds" % timeout
|
|
531
|
+
return finished.returncode, finished.stdout, finished.stderr
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def cli_json(text: str) -> dict | None:
|
|
535
|
+
"""Parse the last JSON object printed by the helper CLI (pretty-printed or one line)."""
|
|
536
|
+
if not text:
|
|
537
|
+
return None
|
|
538
|
+
position = text.find("{")
|
|
539
|
+
while position != -1:
|
|
540
|
+
try:
|
|
541
|
+
return json.loads(text[position:])
|
|
542
|
+
except json.JSONDecodeError:
|
|
543
|
+
position = text.find("{", position + 1)
|
|
544
|
+
return None
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def emit(payload, out: str | None = None) -> None:
|
|
548
|
+
text = json.dumps(payload, indent=2, ensure_ascii=False)
|
|
549
|
+
if out:
|
|
550
|
+
Path(out).write_text(text + "\n", encoding="utf-8")
|
|
551
|
+
else:
|
|
552
|
+
print(text)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
# -- CLI --------------------------------------------------------------------
|
|
556
|
+
|
|
557
|
+
def main(argv: list[str] | None = None) -> int:
|
|
558
|
+
parser = argparse.ArgumentParser(description="Inspect or dump parts of a .docx package.")
|
|
559
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
560
|
+
parts = sub.add_parser("parts", help="list package parts with sizes")
|
|
561
|
+
parts.add_argument("docx")
|
|
562
|
+
text = sub.add_parser("text", help="dump paragraph text with style and index")
|
|
563
|
+
text.add_argument("docx")
|
|
564
|
+
text.add_argument("--part", default="word/document.xml")
|
|
565
|
+
xml = sub.add_parser("xml", help="print one part as XML")
|
|
566
|
+
xml.add_argument("docx")
|
|
567
|
+
xml.add_argument("part")
|
|
568
|
+
args = parser.parse_args(argv)
|
|
569
|
+
|
|
570
|
+
try:
|
|
571
|
+
doc = Docx(args.docx)
|
|
572
|
+
except (OSError, zipfile.BadZipFile) as exc:
|
|
573
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
574
|
+
return 1
|
|
575
|
+
if args.command == "parts":
|
|
576
|
+
emit({"ok": True, "parts": [{"name": name, "bytes": len(doc.parts[name])} for name in doc.order]})
|
|
577
|
+
elif args.command == "text":
|
|
578
|
+
if not doc.exists(args.part):
|
|
579
|
+
print("error: no part %s" % args.part, file=sys.stderr)
|
|
580
|
+
return 1
|
|
581
|
+
rows = []
|
|
582
|
+
for index, paragraph in enumerate(doc.paragraphs(args.part)):
|
|
583
|
+
rows.append({"index": index, "style": paragraph_style(paragraph), "level": heading_level(paragraph), "text": paragraph_text(paragraph)})
|
|
584
|
+
emit({"ok": True, "part": args.part, "paragraphs": rows})
|
|
585
|
+
else:
|
|
586
|
+
if not doc.exists(args.part):
|
|
587
|
+
print("error: no part %s" % args.part, file=sys.stderr)
|
|
588
|
+
return 1
|
|
589
|
+
print(etree.tostring(doc.xml(args.part), pretty_print=True, encoding="unicode"))
|
|
590
|
+
return 0
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
if __name__ == "__main__":
|
|
594
|
+
raise SystemExit(main())
|