@arnilo/prism-tools 0.0.0-stage → 0.13.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.
- package/CHANGELOG.md +410 -0
- package/LICENSE +21 -0
- package/README.md +44 -2
- package/browser/README.md +98 -0
- package/browser/skills/browser-automation/SKILL.md +14 -0
- package/browser/src/actions/cdp.ts +131 -0
- package/browser/src/actions/dialog.ts +25 -0
- package/browser/src/actions/download_release.ts +22 -0
- package/browser/src/actions/index.ts +37 -0
- package/browser/src/actions/locator.ts +70 -0
- package/browser/src/actions/navigate.ts +23 -0
- package/browser/src/actions/screenshot.ts +27 -0
- package/browser/src/actions/scroll.ts +24 -0
- package/browser/src/actions/select_page.ts +21 -0
- package/browser/src/actions/types.ts +114 -0
- package/browser/src/actions/upload.ts +44 -0
- package/browser/src/actions/wait.ts +45 -0
- package/browser/src/browser.ts +197 -0
- package/browser/src/cdp.ts +178 -0
- package/browser/src/checkpoint.ts +189 -0
- package/browser/src/config.ts +72 -0
- package/browser/src/downloads.ts +253 -0
- package/browser/src/errors.ts +31 -0
- package/browser/src/evaluate.ts +101 -0
- package/browser/src/index.ts +129 -0
- package/browser/src/limits.ts +271 -0
- package/browser/src/manager.ts +730 -0
- package/browser/src/network.ts +263 -0
- package/browser/src/observe.ts +178 -0
- package/browser/src/policy.ts +133 -0
- package/browser/src/screenshot.ts +107 -0
- package/browser/src/shared-sandbox.ts +53 -0
- package/browser/src/snapshot.ts +99 -0
- package/browser/src/targets.ts +165 -0
- package/browser/src/tools.ts +480 -0
- package/browser/src/types.ts +316 -0
- package/browser/src/uploads.ts +174 -0
- package/coding/README.md +337 -0
- package/coding/src/agent/acp-operations.ts +146 -0
- package/coding/src/agent/artifacts.ts +38 -0
- package/coding/src/agent/ask-user-decision.ts +623 -0
- package/coding/src/agent/atomic-write.ts +23 -0
- package/coding/src/agent/bounded-file.ts +21 -0
- package/coding/src/agent/checks.ts +301 -0
- package/coding/src/agent/coding-checkpoint.ts +828 -0
- package/coding/src/agent/delete.ts +237 -0
- package/coding/src/agent/diagnostics-tool.ts +163 -0
- package/coding/src/agent/diagnostics.ts +262 -0
- package/coding/src/agent/edit-diff.ts +382 -0
- package/coding/src/agent/edit.ts +322 -0
- package/coding/src/agent/effects.ts +102 -0
- package/coding/src/agent/env.ts +28 -0
- package/coding/src/agent/execution-policy.ts +89 -0
- package/coding/src/agent/file-mutation-queue.ts +72 -0
- package/coding/src/agent/forge/github.ts +603 -0
- package/coding/src/agent/forge/index.ts +16 -0
- package/coding/src/agent/forge/types.ts +185 -0
- package/coding/src/agent/git-aware-repository.ts +305 -0
- package/coding/src/agent/git-exec.ts +326 -0
- package/coding/src/agent/git-status.ts +177 -0
- package/coding/src/agent/git-tools.ts +360 -0
- package/coding/src/agent/git.ts +1074 -0
- package/coding/src/agent/glob-match.ts +146 -0
- package/coding/src/agent/glob.ts +182 -0
- package/coding/src/agent/goal-verify.ts +385 -0
- package/coding/src/agent/grep-worker.ts +74 -0
- package/coding/src/agent/grep.ts +459 -0
- package/coding/src/agent/identity.ts +48 -0
- package/coding/src/agent/index.ts +676 -0
- package/coding/src/agent/language/client.ts +429 -0
- package/coding/src/agent/language/framing.ts +190 -0
- package/coding/src/agent/language/index.ts +23 -0
- package/coding/src/agent/language/intelligence.ts +682 -0
- package/coding/src/agent/language/types.ts +187 -0
- package/coding/src/agent/lifecycle-events.ts +121 -0
- package/coding/src/agent/lifecycle.ts +166 -0
- package/coding/src/agent/limits.ts +214 -0
- package/coding/src/agent/move.ts +178 -0
- package/coding/src/agent/mutation-path.ts +52 -0
- package/coding/src/agent/output-accumulator.ts +304 -0
- package/coding/src/agent/path-utils.ts +120 -0
- package/coding/src/agent/process/index.ts +58 -0
- package/coding/src/agent/process/recovery.ts +508 -0
- package/coding/src/agent/process/sessions-host.ts +222 -0
- package/coding/src/agent/process/sessions-monitor.ts +141 -0
- package/coding/src/agent/process/sessions-recovery.ts +241 -0
- package/coding/src/agent/process/sessions-spawn.ts +613 -0
- package/coding/src/agent/process/sessions-teardown.ts +281 -0
- package/coding/src/agent/process/sessions.ts +30 -0
- package/coding/src/agent/process/types.ts +358 -0
- package/coding/src/agent/read-path-set.ts +126 -0
- package/coding/src/agent/read.ts +713 -0
- package/coding/src/agent/repository/glob.ts +147 -0
- package/coding/src/agent/repository/indexed-search.ts +480 -0
- package/coding/src/agent/repository/list.ts +131 -0
- package/coding/src/agent/repository/operations.ts +22 -0
- package/coding/src/agent/repository/path.ts +98 -0
- package/coding/src/agent/repository/search.ts +277 -0
- package/coding/src/agent/repository/types.ts +243 -0
- package/coding/src/agent/repository/walk.ts +126 -0
- package/coding/src/agent/repository.ts +12 -0
- package/coding/src/agent/shell-background.ts +264 -0
- package/coding/src/agent/shell-output.ts +75 -0
- package/coding/src/agent/shell.ts +526 -0
- package/coding/src/agent/todo.ts +399 -0
- package/coding/src/agent/truncate.ts +251 -0
- package/coding/src/agent/write.ts +170 -0
- package/coding/src/ask-user-host.ts +48 -0
- package/coding/src/config.ts +278 -0
- package/coding/src/hard-deny.ts +97 -0
- package/coding/src/index.ts +318 -0
- package/coding/src/principal-guard.ts +84 -0
- package/coding/src/sandbox.ts +92 -0
- package/coding/src/security/approval.ts +144 -0
- package/coding/src/security/command-rules.ts +57 -0
- package/coding/src/security/docker-cli.ts +222 -0
- package/coding/src/security/docker-sandbox.ts +1205 -0
- package/coding/src/security/e2b-sandbox.ts +853 -0
- package/coding/src/security/egress/dns-pin.ts +88 -0
- package/coding/src/security/egress/index.ts +32 -0
- package/coding/src/security/egress/limits.ts +57 -0
- package/coding/src/security/egress/policy.ts +137 -0
- package/coding/src/security/egress/proxy.ts +599 -0
- package/coding/src/security/egress/types.ts +30 -0
- package/coding/src/security/index.ts +199 -0
- package/coding/src/security/native-sandbox.ts +639 -0
- package/coding/src/security/path-containment.ts +55 -0
- package/coding/src/security/sandbox-coding-operations.ts +392 -0
- package/coding/src/security/sandbox-fs-operations.ts +644 -0
- package/coding/src/security/sandbox-limits.ts +147 -0
- package/coding/src/security/sandbox-tar.ts +308 -0
- package/coding/src/security/sandbox.ts +179 -0
- package/coding/src/security/semaphore.ts +55 -0
- package/coding/src/targets.ts +110 -0
- package/coding/src/workspace/diff.ts +78 -0
- package/coding/src/workspace/errors.ts +28 -0
- package/coding/src/workspace/index.ts +5 -0
- package/coding/src/workspace/merge.ts +151 -0
- package/coding/src/workspace/routing.ts +76 -0
- package/coding/src/workspace/service.ts +545 -0
- package/computer-use/README.md +105 -0
- package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
- package/computer-use/src/classify.ts +53 -0
- package/computer-use/src/command.ts +29 -0
- package/computer-use/src/config.ts +69 -0
- package/computer-use/src/create.ts +170 -0
- package/computer-use/src/devices.ts +247 -0
- package/computer-use/src/env.ts +28 -0
- package/computer-use/src/index.ts +157 -0
- package/computer-use/src/skill.ts +21 -0
- package/openapi/README.md +97 -0
- package/openapi/src/compile.ts +386 -0
- package/openapi/src/config.ts +73 -0
- package/openapi/src/errors.ts +16 -0
- package/openapi/src/execute.ts +241 -0
- package/openapi/src/index.ts +124 -0
- package/openapi/src/limits.ts +63 -0
- package/openapi/src/specs.ts +86 -0
- package/openapi/src/tools.ts +60 -0
- package/openapi/src/types.ts +61 -0
- package/package.json +171 -4
- package/research/README.md +72 -0
- package/research/skills/research-report/SKILL.md +12 -0
- package/research/skills/research-scoping/SKILL.md +10 -0
- package/research/skills/research-synthesis/SKILL.md +10 -0
- package/research/skills/source-triage/SKILL.md +10 -0
- package/research/src/check.ts +71 -0
- package/research/src/config.ts +41 -0
- package/research/src/index.ts +147 -0
- package/research/src/ledger.ts +157 -0
- package/research/src/urls.ts +21 -0
- package/web/README.md +287 -0
- package/web/skills/web-research/SKILL.md +17 -0
- package/web/src/brave.ts +66 -0
- package/web/src/cache.ts +151 -0
- package/web/src/config.ts +210 -0
- package/web/src/controls.ts +128 -0
- package/web/src/domains.ts +114 -0
- package/web/src/exa.ts +60 -0
- package/web/src/firecrawl.ts +160 -0
- package/web/src/index.ts +387 -0
- package/web/src/kagi.ts +73 -0
- package/web/src/limits.ts +55 -0
- package/web/src/native/convert.ts +84 -0
- package/web/src/native/fetch.ts +226 -0
- package/web/src/normalize.ts +131 -0
- package/web/src/obscura/cdp.ts +201 -0
- package/web/src/obscura/classify.ts +35 -0
- package/web/src/obscura/cli.ts +254 -0
- package/web/src/obscura/errors.ts +8 -0
- package/web/src/obscura/index.ts +59 -0
- package/web/src/obscura/limits.ts +92 -0
- package/web/src/obscura/mcp.ts +104 -0
- package/web/src/obscura/playwright.ts +133 -0
- package/web/src/obscura/process.ts +235 -0
- package/web/src/obscura/search-profile.ts +73 -0
- package/web/src/obscura/types.ts +48 -0
- package/web/src/obscura/web.ts +419 -0
- package/web/src/prompt-fetch.ts +88 -0
- package/web/src/render.ts +75 -0
- package/web/src/searxng.ts +78 -0
- package/web/src/slice.ts +139 -0
- package/web/src/tavily.ts +77 -0
- package/web/src/tools.ts +182 -0
- package/web/src/transport.ts +310 -0
- package/web/src/types.ts +199 -0
- package/work/README.md +77 -0
- package/work/THIRD_PARTY_NOTICES +24 -0
- package/work/docling/ocr.py +150 -0
- package/work/sandbox/Dockerfile +36 -0
- package/work/sandbox/soffice.sh +12 -0
- package/work/src/command.ts +29 -0
- package/work/src/config.ts +138 -0
- package/work/src/connectors/cli.ts +343 -0
- package/work/src/connectors/drafts.ts +647 -0
- package/work/src/connectors/errors.ts +8 -0
- package/work/src/connectors/file-bytes.ts +149 -0
- package/work/src/connectors/google-workspace-http.ts +399 -0
- package/work/src/connectors/google-workspace.ts +465 -0
- package/work/src/connectors/gws-tools.ts +378 -0
- package/work/src/connectors/http.ts +123 -0
- package/work/src/connectors/idempotency.ts +235 -0
- package/work/src/connectors/identity.ts +34 -0
- package/work/src/connectors/index.ts +93 -0
- package/work/src/connectors/limits.ts +50 -0
- package/work/src/connectors/m365-tools.ts +308 -0
- package/work/src/connectors/microsoft365-http.ts +428 -0
- package/work/src/connectors/microsoft365.ts +402 -0
- package/work/src/connectors/normalize.ts +120 -0
- package/work/src/connectors/tool-helpers.ts +398 -0
- package/work/src/connectors/tools.ts +42 -0
- package/work/src/connectors/types.ts +399 -0
- package/work/src/diagrams/canonicalize.ts +166 -0
- package/work/src/diagrams/caps.ts +52 -0
- package/work/src/diagrams/embed.ts +340 -0
- package/work/src/diagrams/errors.ts +76 -0
- package/work/src/diagrams/index.ts +81 -0
- package/work/src/diagrams/messages.ts +150 -0
- package/work/src/diagrams/origin.ts +52 -0
- package/work/src/diagrams/telemetry.ts +34 -0
- package/work/src/diagrams/xml.ts +267 -0
- package/work/src/document-extraction/index.ts +563 -0
- package/work/src/document-reader/ambient.d.ts +2 -0
- package/work/src/document-reader/errors.ts +7 -0
- package/work/src/document-reader/index.ts +181 -0
- package/work/src/document-reader/mistral-ocr.ts +318 -0
- package/work/src/document-reader/parsers.ts +171 -0
- package/work/src/documents/caps.ts +135 -0
- package/work/src/documents/diff.ts +269 -0
- package/work/src/documents/errors.ts +44 -0
- package/work/src/documents/fidelity.ts +98 -0
- package/work/src/documents/generate.ts +96 -0
- package/work/src/documents/hash.ts +8 -0
- package/work/src/documents/index.ts +14 -0
- package/work/src/documents/model-schema.ts +639 -0
- package/work/src/documents/parse.ts +252 -0
- package/work/src/documents/patch-history.ts +81 -0
- package/work/src/documents/patch.ts +476 -0
- package/work/src/documents/preview-html.ts +245 -0
- package/work/src/documents/preview.ts +227 -0
- package/work/src/documents/telemetry.ts +36 -0
- package/work/src/documents/translate/docx.ts +292 -0
- package/work/src/documents/translate/pptx.ts +304 -0
- package/work/src/documents/translate/xlsx.ts +137 -0
- package/work/src/documents/types.ts +188 -0
- package/work/src/index.ts +311 -0
- package/work/src/runtime/worker-pool-entry.ts +102 -0
- package/work/src/runtime/worker-pool.ts +267 -0
- package/work/src/sandbox/composition.ts +212 -0
- package/work/src/sandbox/image.ts +6 -0
- package/work/src/sandbox/index.ts +14 -0
- package/work/src/sheets/caps.ts +68 -0
- package/work/src/sheets/csv.ts +351 -0
- package/work/src/sheets/decimal.ts +388 -0
- package/work/src/sheets/errors.ts +52 -0
- package/work/src/sheets/index.ts +62 -0
- package/work/src/sheets/inference.ts +378 -0
- package/work/src/sheets/telemetry.ts +34 -0
- package/work/src/sheets/types.ts +162 -0
- package/work/src/sheets/xlsx.ts +388 -0
- package/work/src/skills/index.ts +8 -0
- package/work/src/skills/load.ts +43 -0
- package/work/src/tools/filesystem.ts +21 -0
- package/work/src/tools/index.ts +1 -0
- package/work/src/tools/local-filesystem.ts +33 -0
- package/work/src/tools/office.ts +311 -0
- package/work/vendor/hermes-agent/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
- package/work/vendor-lock.json +11 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# MIT License. Part of the Hermes docx skill.
|
|
3
|
+
"""Edit an existing .docx in place (or to a new file).
|
|
4
|
+
|
|
5
|
+
Subcommands:
|
|
6
|
+
replace find-and-replace text, preserving run formatting
|
|
7
|
+
set-cell set the text of a table cell
|
|
8
|
+
insert insert a paragraph before a given body paragraph index
|
|
9
|
+
delete delete a body paragraph by index
|
|
10
|
+
style apply a paragraph style to a body paragraph by index
|
|
11
|
+
normalize merge adjacent runs with identical formatting
|
|
12
|
+
toc insert a Table of Contents field at a body paragraph index
|
|
13
|
+
page-numbers add "Page X of Y" (PAGE/NUMPAGES fields) to the footer
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
docx_edit.py replace in.docx --find old --replace new -o out.docx
|
|
17
|
+
docx_edit.py set-cell in.docx --table 0 --row 1 --col 2 --text "42"
|
|
18
|
+
docx_edit.py insert in.docx --index 3 --text "New para" --style Normal
|
|
19
|
+
docx_edit.py delete in.docx --index 3
|
|
20
|
+
docx_edit.py style in.docx --index 0 --style "Heading 1"
|
|
21
|
+
docx_edit.py normalize in.docx -o out.docx
|
|
22
|
+
docx_edit.py toc in.docx --index 1 -o out.docx
|
|
23
|
+
docx_edit.py page-numbers in.docx -o out.docx
|
|
24
|
+
|
|
25
|
+
Field results (TOC entries, page numbers) are computed by Word or
|
|
26
|
+
LibreOffice when the document is opened, not by python-docx; until then
|
|
27
|
+
the fields show placeholder text.
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import argparse
|
|
32
|
+
import json
|
|
33
|
+
import sys
|
|
34
|
+
|
|
35
|
+
from docx import Document
|
|
36
|
+
|
|
37
|
+
from docx_common import iter_all_paragraphs, replace_in_paragraph
|
|
38
|
+
|
|
39
|
+
W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _q(tag: str) -> str:
|
|
43
|
+
return f"{{{W}}}{tag}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def cmd_replace(doc, args) -> dict:
|
|
47
|
+
n = 0
|
|
48
|
+
for para in iter_all_paragraphs(doc):
|
|
49
|
+
n += replace_in_paragraph(para, args.find, args.replace)
|
|
50
|
+
return {"replacements": n}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def cmd_set_cell(doc, args) -> dict:
|
|
54
|
+
cell = doc.tables[args.table].cell(args.row, args.col)
|
|
55
|
+
cell.text = args.text
|
|
56
|
+
return {"table": args.table, "row": args.row, "col": args.col}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def cmd_insert(doc, args) -> dict:
|
|
60
|
+
paras = doc.paragraphs
|
|
61
|
+
if args.index < len(paras):
|
|
62
|
+
anchor = paras[args.index]
|
|
63
|
+
new_para = anchor.insert_paragraph_before(args.text, style=args.style)
|
|
64
|
+
else:
|
|
65
|
+
new_para = doc.add_paragraph(args.text, style=args.style)
|
|
66
|
+
return {"inserted_at": args.index, "text": new_para.text}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def cmd_delete(doc, args) -> dict:
|
|
70
|
+
para = doc.paragraphs[args.index]
|
|
71
|
+
el = para._element
|
|
72
|
+
el.getparent().remove(el)
|
|
73
|
+
return {"deleted_index": args.index}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def cmd_style(doc, args) -> dict:
|
|
77
|
+
doc.paragraphs[args.index].style = doc.styles[args.style]
|
|
78
|
+
return {"index": args.index, "style": args.style}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _run_format_key(r_el) -> str:
|
|
82
|
+
"""Canonical string for a run's w:rPr (None when absent)."""
|
|
83
|
+
from lxml import etree
|
|
84
|
+
rpr = r_el.find(_q("rPr"))
|
|
85
|
+
return "" if rpr is None else etree.tostring(rpr).decode("utf-8")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cmd_normalize(doc) -> dict:
|
|
89
|
+
"""Merge adjacent sibling runs with identical formatting."""
|
|
90
|
+
merged = 0
|
|
91
|
+
for para in iter_all_paragraphs(doc):
|
|
92
|
+
prev = None
|
|
93
|
+
for r_el in list(para._p):
|
|
94
|
+
if r_el.tag != _q("r"):
|
|
95
|
+
prev = None
|
|
96
|
+
continue
|
|
97
|
+
# only merge plain-text runs (no breaks, tabs, drawings...)
|
|
98
|
+
kids = {c.tag for c in r_el} - {_q("rPr"), _q("t")}
|
|
99
|
+
if kids:
|
|
100
|
+
prev = None
|
|
101
|
+
continue
|
|
102
|
+
if (prev is not None
|
|
103
|
+
and _run_format_key(prev) == _run_format_key(r_el)):
|
|
104
|
+
pt = prev.find(_q("t"))
|
|
105
|
+
ct = r_el.find(_q("t"))
|
|
106
|
+
if pt is None:
|
|
107
|
+
pt = prev.makeelement(_q("t"), {})
|
|
108
|
+
prev.append(pt)
|
|
109
|
+
pt.text = (pt.text or "") + ((ct.text or "")
|
|
110
|
+
if ct is not None else "")
|
|
111
|
+
pt.set("{http://www.w3.org/XML/1998/namespace}space",
|
|
112
|
+
"preserve")
|
|
113
|
+
r_el.getparent().remove(r_el)
|
|
114
|
+
merged += 1
|
|
115
|
+
else:
|
|
116
|
+
prev = r_el
|
|
117
|
+
return {"runs_merged": merged}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _add_field(para, instr: str, placeholder: str) -> None:
|
|
121
|
+
"""Append a complex field (begin/instrText/separate/result/end)."""
|
|
122
|
+
p = para._p
|
|
123
|
+
for ftype, extra in (("begin", None), (None, instr),
|
|
124
|
+
("separate", None), (None, placeholder),
|
|
125
|
+
("end", None)):
|
|
126
|
+
r = p.makeelement(_q("r"), {})
|
|
127
|
+
p.append(r)
|
|
128
|
+
if ftype is not None:
|
|
129
|
+
fld = r.makeelement(_q("fldChar"), {_q("fldCharType"): ftype})
|
|
130
|
+
r.append(fld)
|
|
131
|
+
elif extra is instr:
|
|
132
|
+
it = r.makeelement(_q("instrText"), {})
|
|
133
|
+
it.set("{http://www.w3.org/XML/1998/namespace}space", "preserve")
|
|
134
|
+
it.text = instr
|
|
135
|
+
r.append(it)
|
|
136
|
+
else:
|
|
137
|
+
t = r.makeelement(_q("t"), {})
|
|
138
|
+
t.text = extra
|
|
139
|
+
r.append(t)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def cmd_toc(doc, args) -> dict:
|
|
143
|
+
paras = doc.paragraphs
|
|
144
|
+
if args.index < len(paras):
|
|
145
|
+
para = paras[args.index].insert_paragraph_before("")
|
|
146
|
+
else:
|
|
147
|
+
para = doc.add_paragraph("")
|
|
148
|
+
_add_field(para, r' TOC \o "1-3" \h \z \u ',
|
|
149
|
+
"Table of contents - open in Word/LibreOffice and update "
|
|
150
|
+
"fields to populate.")
|
|
151
|
+
return {"toc_inserted_at": args.index}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def cmd_page_numbers(doc, args) -> dict:
|
|
155
|
+
footer = doc.sections[0].footer
|
|
156
|
+
para = footer.paragraphs[0] if footer.paragraphs \
|
|
157
|
+
else footer.add_paragraph()
|
|
158
|
+
para.add_run("Page ")
|
|
159
|
+
_add_field(para, " PAGE ", "1")
|
|
160
|
+
para.add_run(" of ")
|
|
161
|
+
_add_field(para, " NUMPAGES ", "1")
|
|
162
|
+
return {"footer_fields": ["PAGE", "NUMPAGES"]}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def main() -> int:
|
|
166
|
+
ap = argparse.ArgumentParser(description="Edit a .docx file.")
|
|
167
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
168
|
+
|
|
169
|
+
def common(p):
|
|
170
|
+
p.add_argument("path", help="input .docx")
|
|
171
|
+
p.add_argument("-o", "--output",
|
|
172
|
+
help="output path (default: overwrite input)")
|
|
173
|
+
|
|
174
|
+
p = sub.add_parser("replace", help="find-and-replace text")
|
|
175
|
+
common(p)
|
|
176
|
+
p.add_argument("--find", required=True)
|
|
177
|
+
p.add_argument("--replace", required=True)
|
|
178
|
+
p.add_argument("--body-only", action="store_true",
|
|
179
|
+
help="skip headers/footers")
|
|
180
|
+
|
|
181
|
+
p = sub.add_parser("set-cell", help="set table cell text")
|
|
182
|
+
common(p)
|
|
183
|
+
p.add_argument("--table", type=int, required=True, help="table index")
|
|
184
|
+
p.add_argument("--row", type=int, required=True)
|
|
185
|
+
p.add_argument("--col", type=int, required=True)
|
|
186
|
+
p.add_argument("--text", required=True)
|
|
187
|
+
|
|
188
|
+
p = sub.add_parser("insert", help="insert paragraph at body index")
|
|
189
|
+
common(p)
|
|
190
|
+
p.add_argument("--index", type=int, required=True)
|
|
191
|
+
p.add_argument("--text", required=True)
|
|
192
|
+
p.add_argument("--style", default=None)
|
|
193
|
+
|
|
194
|
+
p = sub.add_parser("delete", help="delete body paragraph by index")
|
|
195
|
+
common(p)
|
|
196
|
+
p.add_argument("--index", type=int, required=True)
|
|
197
|
+
|
|
198
|
+
p = sub.add_parser("style", help="apply style to body paragraph")
|
|
199
|
+
common(p)
|
|
200
|
+
p.add_argument("--index", type=int, required=True)
|
|
201
|
+
p.add_argument("--style", required=True)
|
|
202
|
+
|
|
203
|
+
p = sub.add_parser("normalize",
|
|
204
|
+
help="merge adjacent runs with identical formatting")
|
|
205
|
+
common(p)
|
|
206
|
+
|
|
207
|
+
p = sub.add_parser("toc", help="insert a TOC field (Word computes it)")
|
|
208
|
+
common(p)
|
|
209
|
+
p.add_argument("--index", type=int, default=0,
|
|
210
|
+
help="body paragraph index to insert before (default 0)")
|
|
211
|
+
|
|
212
|
+
p = sub.add_parser("page-numbers",
|
|
213
|
+
help="add PAGE/NUMPAGES fields to the footer")
|
|
214
|
+
common(p)
|
|
215
|
+
|
|
216
|
+
args = ap.parse_args()
|
|
217
|
+
doc = Document(args.path)
|
|
218
|
+
|
|
219
|
+
if args.cmd == "replace":
|
|
220
|
+
if args.body_only:
|
|
221
|
+
n = 0
|
|
222
|
+
for para in iter_all_paragraphs(doc, include_headers_footers=False):
|
|
223
|
+
n += replace_in_paragraph(para, args.find, args.replace)
|
|
224
|
+
result = {"replacements": n}
|
|
225
|
+
else:
|
|
226
|
+
result = cmd_replace(doc, args)
|
|
227
|
+
elif args.cmd == "set-cell":
|
|
228
|
+
result = cmd_set_cell(doc, args)
|
|
229
|
+
elif args.cmd == "insert":
|
|
230
|
+
result = cmd_insert(doc, args)
|
|
231
|
+
elif args.cmd == "delete":
|
|
232
|
+
result = cmd_delete(doc, args)
|
|
233
|
+
elif args.cmd == "normalize":
|
|
234
|
+
result = cmd_normalize(doc)
|
|
235
|
+
elif args.cmd == "toc":
|
|
236
|
+
result = cmd_toc(doc, args)
|
|
237
|
+
elif args.cmd == "page-numbers":
|
|
238
|
+
result = cmd_page_numbers(doc, args)
|
|
239
|
+
else:
|
|
240
|
+
result = cmd_style(doc, args)
|
|
241
|
+
|
|
242
|
+
out = args.output or args.path
|
|
243
|
+
doc.save(out)
|
|
244
|
+
result.update({"ok": True, "output": out})
|
|
245
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
246
|
+
return 0
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
sys.exit(main())
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# MIT License. Part of the Hermes docx skill.
|
|
3
|
+
"""Read a .docx: text, structure outline, styles, images, revision detection.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
docx_read.py file.docx --text # full text incl. tables + headers/footers
|
|
7
|
+
docx_read.py file.docx --structure # JSON outline (headings, tables, counts)
|
|
8
|
+
docx_read.py file.docx --styles # JSON list of styles actually used
|
|
9
|
+
docx_read.py file.docx --images DIR # extract embedded images into DIR
|
|
10
|
+
docx_read.py file.docx --revisions # JSON: tracked changes / comments present?
|
|
11
|
+
|
|
12
|
+
Text output is JSON: {"body": [...], "tables": [[...rows]], "headers": [...],
|
|
13
|
+
"footers": [...]}. Body text is the accepted/as-is text (python-docx ignores
|
|
14
|
+
deleted-in-revision text and shows inserted text).
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import zipfile
|
|
23
|
+
|
|
24
|
+
from docx import Document
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def table_to_rows(table) -> list:
|
|
28
|
+
return [[cell.text for cell in row.cells] for row in table.rows]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def extract_text(doc) -> dict:
|
|
32
|
+
out = {"body": [p.text for p in doc.paragraphs],
|
|
33
|
+
"tables": [table_to_rows(t) for t in doc.tables],
|
|
34
|
+
"headers": [], "footers": []}
|
|
35
|
+
for section in doc.sections:
|
|
36
|
+
out["headers"].extend(p.text for p in section.header.paragraphs)
|
|
37
|
+
out["footers"].extend(p.text for p in section.footer.paragraphs)
|
|
38
|
+
for t in section.header.tables:
|
|
39
|
+
out["headers"].append(json.dumps(table_to_rows(t), ensure_ascii=False))
|
|
40
|
+
for t in section.footer.tables:
|
|
41
|
+
out["footers"].append(json.dumps(table_to_rows(t), ensure_ascii=False))
|
|
42
|
+
return out
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def extract_structure(doc) -> dict:
|
|
46
|
+
outline = []
|
|
47
|
+
for i, para in enumerate(doc.paragraphs):
|
|
48
|
+
style = para.style.name if para.style else ""
|
|
49
|
+
if style.startswith("Heading"):
|
|
50
|
+
try:
|
|
51
|
+
level = int(style.split()[-1])
|
|
52
|
+
except ValueError:
|
|
53
|
+
level = 1
|
|
54
|
+
outline.append({"index": i, "level": level, "text": para.text})
|
|
55
|
+
return {
|
|
56
|
+
"outline": outline,
|
|
57
|
+
"paragraph_count": len(doc.paragraphs),
|
|
58
|
+
"table_count": len(doc.tables),
|
|
59
|
+
"tables": [{"rows": len(t.rows), "cols": len(t.columns)}
|
|
60
|
+
for t in doc.tables],
|
|
61
|
+
"section_count": len(doc.sections),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def styles_used(doc) -> list:
|
|
66
|
+
used = set()
|
|
67
|
+
for para in doc.paragraphs:
|
|
68
|
+
if para.style:
|
|
69
|
+
used.add(para.style.name)
|
|
70
|
+
for run in para.runs:
|
|
71
|
+
if run.style:
|
|
72
|
+
used.add(run.style.name)
|
|
73
|
+
for table in doc.tables:
|
|
74
|
+
if table.style:
|
|
75
|
+
used.add(table.style.name)
|
|
76
|
+
for row in table.rows:
|
|
77
|
+
for cell in row.cells:
|
|
78
|
+
for para in cell.paragraphs:
|
|
79
|
+
if para.style:
|
|
80
|
+
used.add(para.style.name)
|
|
81
|
+
return sorted(used)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def extract_images(path: str, outdir: str) -> list:
|
|
85
|
+
os.makedirs(outdir, exist_ok=True)
|
|
86
|
+
written = []
|
|
87
|
+
with zipfile.ZipFile(path) as zf:
|
|
88
|
+
for name in zf.namelist():
|
|
89
|
+
if name.startswith("word/media/"):
|
|
90
|
+
target = os.path.join(outdir, os.path.basename(name))
|
|
91
|
+
with open(target, "wb") as f:
|
|
92
|
+
f.write(zf.read(name))
|
|
93
|
+
written.append(target)
|
|
94
|
+
return written
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def detect_revisions(path: str) -> dict:
|
|
98
|
+
"""Detect tracked changes and comments by scanning the raw XML parts."""
|
|
99
|
+
markers = {"insertions": b"<w:ins ", "deletions": b"<w:del ",
|
|
100
|
+
"format_changes": b"<w:rPrChange"}
|
|
101
|
+
result = {k: False for k in markers}
|
|
102
|
+
result["comments"] = False
|
|
103
|
+
with zipfile.ZipFile(path) as zf:
|
|
104
|
+
names = zf.namelist()
|
|
105
|
+
result["comments"] = any(n.startswith("word/comments") for n in names)
|
|
106
|
+
for name in names:
|
|
107
|
+
if name.startswith("word/") and name.endswith(".xml"):
|
|
108
|
+
data = zf.read(name)
|
|
109
|
+
for key, marker in markers.items():
|
|
110
|
+
if marker in data:
|
|
111
|
+
result[key] = True
|
|
112
|
+
result["has_tracked_changes"] = any(
|
|
113
|
+
result[k] for k in ("insertions", "deletions", "format_changes"))
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def main() -> int:
|
|
118
|
+
ap = argparse.ArgumentParser(description="Read/inspect a .docx file.")
|
|
119
|
+
ap.add_argument("path", help=".docx file to read")
|
|
120
|
+
g = ap.add_mutually_exclusive_group(required=True)
|
|
121
|
+
g.add_argument("--text", action="store_true", help="extract all text as JSON")
|
|
122
|
+
g.add_argument("--structure", action="store_true", help="outline JSON")
|
|
123
|
+
g.add_argument("--styles", action="store_true", help="styles used, JSON")
|
|
124
|
+
g.add_argument("--images", metavar="DIR", help="extract images to DIR")
|
|
125
|
+
g.add_argument("--revisions", action="store_true",
|
|
126
|
+
help="detect tracked changes / comments")
|
|
127
|
+
args = ap.parse_args()
|
|
128
|
+
|
|
129
|
+
if args.images:
|
|
130
|
+
print(json.dumps({"images": extract_images(args.path, args.images)},
|
|
131
|
+
ensure_ascii=False))
|
|
132
|
+
return 0
|
|
133
|
+
if args.revisions:
|
|
134
|
+
print(json.dumps(detect_revisions(args.path), ensure_ascii=False))
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
doc = Document(args.path)
|
|
138
|
+
if args.text:
|
|
139
|
+
out = extract_text(doc)
|
|
140
|
+
elif args.structure:
|
|
141
|
+
out = extract_structure(doc)
|
|
142
|
+
else:
|
|
143
|
+
out = {"styles": styles_used(doc)}
|
|
144
|
+
print(json.dumps(out, ensure_ascii=False, indent=2))
|
|
145
|
+
return 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
sys.exit(main())
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# MIT License. Part of the Hermes docx skill.
|
|
3
|
+
"""Inspect and resolve tracked changes (w:ins / w:del) in a .docx.
|
|
4
|
+
|
|
5
|
+
Subcommands:
|
|
6
|
+
list JSON list of revisions: id, author, date, type, text
|
|
7
|
+
accept-all accept every insertion and deletion
|
|
8
|
+
reject-all reject every insertion and deletion
|
|
9
|
+
accept accept one revision by --id
|
|
10
|
+
reject reject one revision by --id
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
docx_revisions.py list report.docx
|
|
14
|
+
docx_revisions.py accept-all report.docx -o accepted.docx
|
|
15
|
+
docx_revisions.py reject report.docx --id 3 -o out.docx
|
|
16
|
+
|
|
17
|
+
Semantics (direct XML manipulation, python-docx oxml layer):
|
|
18
|
+
accept w:ins -> unwrap (keep inserted runs) reject w:ins -> remove
|
|
19
|
+
accept w:del -> remove reject w:del -> restore
|
|
20
|
+
(restore = w:delText tags renamed to w:t, wrapper unwrapped)
|
|
21
|
+
|
|
22
|
+
Covers run-level insertions/deletions anywhere in body, tables (nested
|
|
23
|
+
included), headers and footers. Row/paragraph-mark revisions and format
|
|
24
|
+
changes (w:rPrChange etc.) are reported by docx_read.py --revisions but
|
|
25
|
+
not resolved here.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import json
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
from docx import Document
|
|
34
|
+
|
|
35
|
+
from docx_common import iter_part_roots
|
|
36
|
+
|
|
37
|
+
W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def q(tag: str) -> str:
|
|
41
|
+
return f"{{{W}}}{tag}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
INS, DEL = q("ins"), q("del")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _iter_revision_elements(doc):
|
|
48
|
+
"""Yield every w:ins / w:del element across body, headers, footers."""
|
|
49
|
+
for root in iter_part_roots(doc):
|
|
50
|
+
for el in root.iter(INS, DEL):
|
|
51
|
+
yield el
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _rev_text(el) -> str:
|
|
55
|
+
tag = q("delText") if el.tag == DEL else q("t")
|
|
56
|
+
return "".join(t.text or "" for t in el.iter(tag))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _rev_record(el) -> dict:
|
|
60
|
+
return {
|
|
61
|
+
"id": el.get(q("id")),
|
|
62
|
+
"author": el.get(q("author")),
|
|
63
|
+
"date": el.get(q("date")),
|
|
64
|
+
"type": "insertion" if el.tag == INS else "deletion",
|
|
65
|
+
"text": _rev_text(el),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _unwrap(el) -> None:
|
|
70
|
+
"""Replace `el` with its children, keeping document order."""
|
|
71
|
+
parent = el.getparent()
|
|
72
|
+
idx = list(parent).index(el)
|
|
73
|
+
for child in list(el):
|
|
74
|
+
parent.insert(idx, child)
|
|
75
|
+
idx += 1
|
|
76
|
+
parent.remove(el)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _apply(el, accept: bool) -> None:
|
|
80
|
+
if el.getparent() is None: # already detached via an outer wrapper
|
|
81
|
+
return
|
|
82
|
+
if el.tag == INS:
|
|
83
|
+
if accept:
|
|
84
|
+
_unwrap(el)
|
|
85
|
+
else:
|
|
86
|
+
el.getparent().remove(el)
|
|
87
|
+
else: # w:del
|
|
88
|
+
if accept:
|
|
89
|
+
el.getparent().remove(el)
|
|
90
|
+
else:
|
|
91
|
+
for dt in list(el.iter(q("delText"))):
|
|
92
|
+
dt.tag = q("t")
|
|
93
|
+
_unwrap(el)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def resolve(doc, accept: bool, rev_id: str | None = None) -> int:
|
|
97
|
+
targets = [el for el in _iter_revision_elements(doc)
|
|
98
|
+
if rev_id is None or el.get(q("id")) == rev_id]
|
|
99
|
+
for el in targets:
|
|
100
|
+
_apply(el, accept)
|
|
101
|
+
return len(targets)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main() -> int:
|
|
105
|
+
ap = argparse.ArgumentParser(
|
|
106
|
+
description="List, accept, or reject tracked changes in a .docx.")
|
|
107
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
108
|
+
|
|
109
|
+
def common(p, out=True):
|
|
110
|
+
p.add_argument("path", help="input .docx")
|
|
111
|
+
if out:
|
|
112
|
+
p.add_argument("-o", "--output",
|
|
113
|
+
help="output path (default: overwrite input)")
|
|
114
|
+
|
|
115
|
+
common(sub.add_parser("list", help="list revisions as JSON"), out=False)
|
|
116
|
+
common(sub.add_parser("accept-all", help="accept every revision"))
|
|
117
|
+
common(sub.add_parser("reject-all", help="reject every revision"))
|
|
118
|
+
for name in ("accept", "reject"):
|
|
119
|
+
p = sub.add_parser(name, help=f"{name} one revision by id")
|
|
120
|
+
common(p)
|
|
121
|
+
p.add_argument("--id", required=True, help="revision id (w:id)")
|
|
122
|
+
|
|
123
|
+
args = ap.parse_args()
|
|
124
|
+
doc = Document(args.path)
|
|
125
|
+
|
|
126
|
+
if args.cmd == "list":
|
|
127
|
+
revs = [_rev_record(el) for el in _iter_revision_elements(doc)]
|
|
128
|
+
print(json.dumps({"ok": True, "revisions": revs}, ensure_ascii=False))
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
accept = args.cmd in ("accept-all", "accept")
|
|
132
|
+
rev_id = getattr(args, "id", None)
|
|
133
|
+
n = resolve(doc, accept, rev_id)
|
|
134
|
+
if rev_id is not None and n == 0:
|
|
135
|
+
print(json.dumps({"ok": False,
|
|
136
|
+
"error": f"no revision with id {rev_id}"}))
|
|
137
|
+
return 1
|
|
138
|
+
out = args.output or args.path
|
|
139
|
+
doc.save(out)
|
|
140
|
+
print(json.dumps({"ok": True, "output": out, "resolved": n,
|
|
141
|
+
"action": "accept" if accept else "reject"},
|
|
142
|
+
ensure_ascii=False))
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
sys.exit(main())
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# MIT License. Part of the Hermes docx skill.
|
|
3
|
+
"""Fill {{placeholder}} tokens in a .docx from a JSON mapping.
|
|
4
|
+
|
|
5
|
+
Tokens are replaced everywhere: body paragraphs, tables (including nested
|
|
6
|
+
tables), headers and footers. Run formatting is preserved; tokens split
|
|
7
|
+
across runs are handled.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
docx_template.py template.docx values.json output.docx
|
|
11
|
+
docx_template.py template.docx values.json output.docx --strict
|
|
12
|
+
|
|
13
|
+
values.json: {"name": "Ada", "date": "2026-01-01"} fills {{name}}, {{date}}.
|
|
14
|
+
With --strict, exits 1 if any {{token}} remains unfilled after processing.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
from docx import Document
|
|
24
|
+
|
|
25
|
+
from docx_common import iter_all_paragraphs, replace_in_paragraph
|
|
26
|
+
|
|
27
|
+
TOKEN_RE = re.compile(r"\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main() -> int:
|
|
31
|
+
ap = argparse.ArgumentParser(
|
|
32
|
+
description="Fill {{token}} placeholders in a .docx from JSON.")
|
|
33
|
+
ap.add_argument("template", help="input .docx with {{tokens}}")
|
|
34
|
+
ap.add_argument("values", help="JSON file of token -> value")
|
|
35
|
+
ap.add_argument("output", help="output .docx path")
|
|
36
|
+
ap.add_argument("--strict", action="store_true",
|
|
37
|
+
help="fail if any token remains unfilled")
|
|
38
|
+
args = ap.parse_args()
|
|
39
|
+
|
|
40
|
+
with open(args.values, encoding="utf-8") as f:
|
|
41
|
+
values = json.load(f)
|
|
42
|
+
|
|
43
|
+
doc = Document(args.template)
|
|
44
|
+
filled = {}
|
|
45
|
+
for para in iter_all_paragraphs(doc):
|
|
46
|
+
# Normalize whitespace variants like {{ name }} first.
|
|
47
|
+
for m in set(TOKEN_RE.findall(para.text)):
|
|
48
|
+
if m in values:
|
|
49
|
+
# Replace any spacing variant with canonical token, then fill.
|
|
50
|
+
for variant in set(
|
|
51
|
+
t.group(0) for t in TOKEN_RE.finditer(para.text)
|
|
52
|
+
if t.group(1) == m):
|
|
53
|
+
n = replace_in_paragraph(para, variant, str(values[m]))
|
|
54
|
+
filled[m] = filled.get(m, 0) + n
|
|
55
|
+
|
|
56
|
+
remaining = sorted({m for para in iter_all_paragraphs(doc)
|
|
57
|
+
for m in TOKEN_RE.findall(para.text)})
|
|
58
|
+
doc.save(args.output)
|
|
59
|
+
result = {"ok": True, "output": args.output, "filled": filled,
|
|
60
|
+
"unfilled_tokens": remaining}
|
|
61
|
+
if args.strict and remaining:
|
|
62
|
+
result["ok"] = False
|
|
63
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
64
|
+
return 1
|
|
65
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
sys.exit(main())
|