dsh-office-kit 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +92 -0
- package/assets/skills/office-documents/scripts/a11y_audit.py +194 -0
- package/assets/skills/office-documents/scripts/accept_tracked_changes.py +151 -0
- package/assets/skills/office-documents/scripts/add_tracked_replacements.py +148 -0
- package/assets/skills/office-documents/scripts/append_blocks.py +108 -0
- package/assets/skills/office-documents/scripts/apply_template_styles.py +104 -0
- package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +102 -0
- package/assets/skills/office-documents/scripts/comments_add.py +136 -0
- package/assets/skills/office-documents/scripts/comments_apply_patch.py +114 -0
- package/assets/skills/office-documents/scripts/comments_extract.py +85 -0
- package/assets/skills/office-documents/scripts/comments_strip.py +74 -0
- package/assets/skills/office-documents/scripts/content_controls.py +192 -0
- package/assets/skills/office-documents/scripts/core_properties.py +124 -0
- package/assets/skills/office-documents/scripts/docx_kit.py +587 -0
- package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +225 -0
- package/assets/skills/office-documents/scripts/docx_table_to_csv.py +99 -0
- package/assets/skills/office-documents/scripts/fields_materialize.py +66 -0
- package/assets/skills/office-documents/scripts/fields_report.py +59 -0
- package/assets/skills/office-documents/scripts/flatten_ref_fields.py +62 -0
- package/assets/skills/office-documents/scripts/footnotes_report.py +77 -0
- package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +96 -0
- package/assets/skills/office-documents/scripts/heading_audit.py +81 -0
- package/assets/skills/office-documents/scripts/images_audit.py +80 -0
- package/assets/skills/office-documents/scripts/insert_note.py +126 -0
- package/assets/skills/office-documents/scripts/insert_ref_fields.py +90 -0
- package/assets/skills/office-documents/scripts/insert_toc.py +84 -0
- package/assets/skills/office-documents/scripts/internal_nav.py +146 -0
- package/assets/skills/office-documents/scripts/make_fixtures.py +169 -0
- package/assets/skills/office-documents/scripts/merge_docx_append.py +239 -0
- package/assets/skills/office-documents/scripts/privacy_scrub.py +131 -0
- package/assets/skills/office-documents/scripts/redact_docx.py +113 -0
- package/assets/skills/office-documents/scripts/render_and_diff.py +101 -0
- package/assets/skills/office-documents/scripts/replace_text.py +154 -0
- package/assets/skills/office-documents/scripts/section_audit.py +91 -0
- package/assets/skills/office-documents/scripts/set_protection.py +92 -0
- package/assets/skills/office-documents/scripts/style_lint.py +111 -0
- package/assets/skills/office-documents/scripts/style_normalize.py +103 -0
- package/assets/skills/office-documents/scripts/table_geometry.py +167 -0
- package/assets/skills/office-documents/scripts/watermark_add.py +48 -0
- package/assets/skills/office-documents/scripts/watermark_audit_remove.py +67 -0
- package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +117 -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 +136 -0
- package/assets/skills/office-presentations/container_tools/core_properties.py +120 -0
- package/assets/skills/office-presentations/container_tools/create_montage.py +126 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +285 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +157 -0
- package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +144 -0
- package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +199 -0
- package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +212 -0
- package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +345 -0
- package/assets/skills/office-presentations/container_tools/preview_render.py +375 -0
- package/assets/skills/office-presentations/container_tools/render_slides.py +139 -0
- package/assets/skills/office-presentations/container_tools/replace_text.py +168 -0
- package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +229 -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 +176 -0
- package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +154 -0
- package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +111 -0
- package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +239 -0
- package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +185 -0
- package/assets/skills/office-spreadsheets/SKILL.md +105 -101
- package/assets/skills/office-spreadsheets/container_tools/add_chart.py +236 -0
- package/assets/skills/office-spreadsheets/container_tools/add_image.py +107 -0
- package/assets/skills/office-spreadsheets/container_tools/append_rows.py +122 -0
- package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +306 -0
- package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +398 -0
- package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +275 -0
- package/assets/skills/office-spreadsheets/container_tools/set_cells.py +135 -0
- package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +134 -0
- package/assets/skills/office-spreadsheets/container_tools/style_cells.py +133 -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 +253 -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 +239 -89
- package/package.json +1 -1
- package/postinstall.mjs +92 -82
- package/python/office_cli.py +171 -246
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Tile slide previews into one image so the whole deck can be judged at once.
|
|
2
|
+
|
|
3
|
+
Purpose: reading ten previews one by one costs ten looks and hides pace,
|
|
4
|
+
repetition and colour drift between slides. One contact sheet shows whether the
|
|
5
|
+
deck has a rhythm: which slides are dense, which are empty, whether the cover
|
|
6
|
+
carries the same accent as the close.
|
|
7
|
+
|
|
8
|
+
Exit codes: 0 sheet written, 2 nothing readable.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import math
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "container_tools"))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _natural_key(path: Path) -> list:
|
|
22
|
+
import re
|
|
23
|
+
|
|
24
|
+
return [int(part) if part.isdigit() else part.lower() for part in re.split(r"(\d+)", path.stem)]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_sheet(images: list[Path], out_path: Path, columns: int = 3, label_height: int = 48, padding: int = 18) -> dict:
|
|
28
|
+
from PIL import Image, ImageDraw
|
|
29
|
+
|
|
30
|
+
columns = max(1, columns)
|
|
31
|
+
rows = max(1, math.ceil(len(images) / columns))
|
|
32
|
+
thumbs = []
|
|
33
|
+
cell_w = 0
|
|
34
|
+
cell_h = 0
|
|
35
|
+
for path in images:
|
|
36
|
+
try:
|
|
37
|
+
with Image.open(path) as opened:
|
|
38
|
+
thumb = opened.convert("RGB")
|
|
39
|
+
thumbs.append((path, thumb.copy(), None))
|
|
40
|
+
cell_w = max(cell_w, thumb.width)
|
|
41
|
+
cell_h = max(cell_h, thumb.height)
|
|
42
|
+
except Exception as exc: # noqa: BLE001 - unreadable previews become placeholder cells
|
|
43
|
+
thumbs.append((path, None, str(exc)))
|
|
44
|
+
if not cell_w or not cell_h:
|
|
45
|
+
return {"ok": False, "error": {"code": "no_readable_images", "message": "none of the inputs could be opened"}}
|
|
46
|
+
|
|
47
|
+
width = columns * cell_w + (columns + 1) * padding
|
|
48
|
+
height = rows * (cell_h + label_height) + (rows + 1) * padding
|
|
49
|
+
sheet = Image.new("RGB", (width, height), (246, 246, 246))
|
|
50
|
+
draw = ImageDraw.Draw(sheet)
|
|
51
|
+
|
|
52
|
+
for index, (path, thumb, error) in enumerate(thumbs):
|
|
53
|
+
row, column = divmod(index, columns)
|
|
54
|
+
x = padding + column * (cell_w + padding)
|
|
55
|
+
y = padding + row * (cell_h + label_height + padding)
|
|
56
|
+
if thumb is None:
|
|
57
|
+
draw.rectangle([x, y, x + cell_w, y + cell_h], fill=(232, 232, 232), outline=(180, 60, 60), width=3)
|
|
58
|
+
draw.line([x, y, x + cell_w, y + cell_h], fill=(180, 60, 60), width=3)
|
|
59
|
+
draw.line([x + cell_w, y, x, y + cell_h], fill=(180, 60, 60), width=3)
|
|
60
|
+
else:
|
|
61
|
+
sheet.paste(thumb, (x, y))
|
|
62
|
+
draw.rectangle([x, y, x + cell_w, y + cell_h], outline=(190, 190, 190), width=1)
|
|
63
|
+
draw.text((x + 4, y + cell_h + 8), path.stem, fill=(40, 40, 40))
|
|
64
|
+
|
|
65
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
sheet.save(out_path)
|
|
67
|
+
return {
|
|
68
|
+
"ok": True,
|
|
69
|
+
"output": str(out_path.resolve()),
|
|
70
|
+
"cells": len(thumbs),
|
|
71
|
+
"grid": {"columns": columns, "rows": rows},
|
|
72
|
+
"size": [width, height],
|
|
73
|
+
"unreadable": [path.name for path, thumb, _ in thumbs if thumb is None],
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _collect(args) -> list[Path]:
|
|
78
|
+
found: list[Path] = []
|
|
79
|
+
for value in [*args.images, *([args.dir] if args.dir else [])]:
|
|
80
|
+
path = Path(value)
|
|
81
|
+
if path.is_dir():
|
|
82
|
+
found.extend(sorted((item for item in path.iterdir() if item.suffix.lower() in {".png", ".jpg", ".jpeg", ".webp"}), key=_natural_key))
|
|
83
|
+
else:
|
|
84
|
+
found.append(path)
|
|
85
|
+
return found
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main(argv: list[str] | None = None) -> int:
|
|
89
|
+
import argparse
|
|
90
|
+
|
|
91
|
+
parser = argparse.ArgumentParser(description="Build one contact sheet from slide previews.")
|
|
92
|
+
parser.add_argument("images", nargs="*", help="preview images and/or directories of them")
|
|
93
|
+
parser.add_argument("--dir", help="a directory of preview images to include (same as passing it positionally)")
|
|
94
|
+
parser.add_argument("--out", required=True, help="path of the contact sheet to write")
|
|
95
|
+
parser.add_argument("--columns", type=int, default=3, help="grid width (default 3)")
|
|
96
|
+
parser.add_argument("--json-out", help="also write the report here")
|
|
97
|
+
args = parser.parse_args(argv)
|
|
98
|
+
if not args.images and not args.dir:
|
|
99
|
+
parser.error("give at least one image, one directory, or --dir")
|
|
100
|
+
|
|
101
|
+
images = _collect(args)
|
|
102
|
+
report = build_sheet(images, Path(args.out), args.columns) if images else {"ok": False, "error": {"code": "no_images", "message": "no preview images found"}}
|
|
103
|
+
if args.json_out:
|
|
104
|
+
Path(args.json_out).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
105
|
+
json.dump(report, sys.stdout, ensure_ascii=False, indent=2)
|
|
106
|
+
sys.stdout.write("\n")
|
|
107
|
+
return 0 if report.get("ok") else 2
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Build a starter deck out of the slides the plan keeps.
|
|
2
|
+
|
|
3
|
+
Purpose: the safe way to follow a supplied design is to keep its actual slides,
|
|
4
|
+
not to redraw a lookalike. This copies the template file, keeps the mapped
|
|
5
|
+
source slides in plan order, drops the rest, then applies the element actions
|
|
6
|
+
that only move text: keep, delete, fill-placeholder, rewrite and
|
|
7
|
+
rewrite-and-reposition.
|
|
8
|
+
|
|
9
|
+
Ceiling: slides are reused, not cloned. Two output slides may not map to the
|
|
10
|
+
same source slide, and 'replace' is left to the author (build the new element
|
|
11
|
+
with a recipe, then run check_template_fidelity). Charts are never rebuilt here.
|
|
12
|
+
|
|
13
|
+
Exit codes: 0 starter written, 1 plan rejected, 2 unreadable input.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "container_tools"))
|
|
23
|
+
|
|
24
|
+
from ooxml_helpers import box, placeholder_kind, slide_size_in # noqa: E402
|
|
25
|
+
|
|
26
|
+
PLACEHOLDER_ALIASES = {
|
|
27
|
+
"title": ("TITLE", "CENTER_TITLE"),
|
|
28
|
+
"subtitle": ("SUBTITLE",),
|
|
29
|
+
"body": ("BODY", "OBJECT"),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _slides(prs) -> list:
|
|
34
|
+
return list(prs.slides)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _drop_slide(prs, slide) -> None:
|
|
38
|
+
slide_id = None
|
|
39
|
+
for element in prs.slides._sldIdLst:
|
|
40
|
+
if element.rId == slide.part.relate_to(slide.part, "") if False else False:
|
|
41
|
+
break
|
|
42
|
+
# find the sldId whose relationship points at this slide part
|
|
43
|
+
rels = prs.part.rels
|
|
44
|
+
rId = None
|
|
45
|
+
for key, relationship in rels.items():
|
|
46
|
+
if relationship.reltype.endswith("/slide") and relationship.target_part is slide.part:
|
|
47
|
+
rId = key
|
|
48
|
+
break
|
|
49
|
+
if rId is None:
|
|
50
|
+
return
|
|
51
|
+
for element in list(prs.slides._sldIdLst):
|
|
52
|
+
if element.rId == rId:
|
|
53
|
+
slide_id = element
|
|
54
|
+
break
|
|
55
|
+
if slide_id is not None:
|
|
56
|
+
prs.slides._sldIdLst.remove(slide_id)
|
|
57
|
+
prs.part.drop_rel(rId)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _keep_only(prs, wanted: list[int]) -> None:
|
|
61
|
+
ordered = [element for element in prs.slides._sldIdLst]
|
|
62
|
+
keep_rids = []
|
|
63
|
+
for position in wanted:
|
|
64
|
+
keep_rids.append(ordered[position - 1].rId)
|
|
65
|
+
for slide in list(prs.slides):
|
|
66
|
+
current = None
|
|
67
|
+
for key, relationship in prs.part.rels.items():
|
|
68
|
+
if relationship.reltype.endswith("/slide") and relationship.target_part is slide.part:
|
|
69
|
+
current = key
|
|
70
|
+
break
|
|
71
|
+
if current not in keep_rids:
|
|
72
|
+
_drop_slide(prs, slide)
|
|
73
|
+
# reorder what survived to match the plan
|
|
74
|
+
by_rid = {element.rId: element for element in prs.slides._sldIdLst}
|
|
75
|
+
for element in list(prs.slides._sldIdLst):
|
|
76
|
+
prs.slides._sldIdLst.remove(element)
|
|
77
|
+
for rId in keep_rids:
|
|
78
|
+
prs.slides._sldIdLst.append(by_rid[rId])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _find_shape(slide, element_id: str):
|
|
82
|
+
shapes = list(slide.shapes)
|
|
83
|
+
if element_id.isdigit():
|
|
84
|
+
index = int(element_id) - 1
|
|
85
|
+
return shapes[index] if 0 <= index < len(shapes) else None
|
|
86
|
+
for shape in shapes:
|
|
87
|
+
if shape.name == element_id:
|
|
88
|
+
return shape
|
|
89
|
+
if element_id.lower().startswith("shape-") and element_id[6:].isdigit():
|
|
90
|
+
index = int(element_id[6:]) - 1
|
|
91
|
+
return shapes[index] if 0 <= index < len(shapes) else None
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _find_placeholder(slide, alias: str):
|
|
96
|
+
wanted = PLACEHOLDER_ALIASES.get(alias.lower())
|
|
97
|
+
for shape in slide.shapes:
|
|
98
|
+
if not shape.is_placeholder:
|
|
99
|
+
continue
|
|
100
|
+
kind = (placeholder_kind(shape) or "").upper()
|
|
101
|
+
if alias.isdigit() and shape.placeholder_format.idx == int(alias):
|
|
102
|
+
return shape
|
|
103
|
+
if wanted and kind in wanted:
|
|
104
|
+
return shape
|
|
105
|
+
if alias.lower() == (placeholder_kind(shape) or "").lower():
|
|
106
|
+
return shape
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _set_text(shape, text: str) -> None:
|
|
111
|
+
frame = shape.text_frame
|
|
112
|
+
paragraphs = list(frame.paragraphs)
|
|
113
|
+
first = paragraphs[0]
|
|
114
|
+
runs = list(first.runs)
|
|
115
|
+
if runs:
|
|
116
|
+
runs[0].text = text
|
|
117
|
+
for run in runs[1:]:
|
|
118
|
+
run._r.getparent().remove(run._r)
|
|
119
|
+
for paragraph in paragraphs[1:]:
|
|
120
|
+
paragraph._p.getparent().remove(paragraph._p)
|
|
121
|
+
else:
|
|
122
|
+
frame.text = text
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def build_starter(template_path: str | Path, plan_path: str | Path, out_path: str | Path) -> dict:
|
|
126
|
+
from pptx import Presentation
|
|
127
|
+
|
|
128
|
+
template = Path(template_path)
|
|
129
|
+
plan_file = Path(plan_path)
|
|
130
|
+
if not template.is_file():
|
|
131
|
+
return {"ok": False, "error": {"code": "template_missing", "message": str(template)}}
|
|
132
|
+
if not plan_file.is_file():
|
|
133
|
+
return {"ok": False, "error": {"code": "plan_missing", "message": str(plan_file)}}
|
|
134
|
+
|
|
135
|
+
plan = json.loads(plan_file.read_text(encoding="utf-8"))
|
|
136
|
+
entries = plan.get("outputSlides") or []
|
|
137
|
+
if not entries:
|
|
138
|
+
return {"ok": False, "error": {"code": "plan_empty", "message": "outputSlides is empty"}}
|
|
139
|
+
|
|
140
|
+
sources = [int(entry.get("sourceSlide", 0)) for entry in entries]
|
|
141
|
+
duplicates = sorted({value for value in sources if sources.count(value) > 1})
|
|
142
|
+
if duplicates:
|
|
143
|
+
return {
|
|
144
|
+
"ok": False,
|
|
145
|
+
"error": {
|
|
146
|
+
"code": "duplicate_source_slide",
|
|
147
|
+
"message": f"source slides {duplicates} are mapped more than once; this builder reuses slides instead of cloning them",
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
prs = Presentation(str(template))
|
|
152
|
+
total = len(prs.slides)
|
|
153
|
+
out_of_range = [value for value in sources if not 1 <= value <= total]
|
|
154
|
+
if out_of_range:
|
|
155
|
+
return {"ok": False, "error": {"code": "source_out_of_range", "message": f"template has {total} slides, plan asks for {out_of_range}"}}
|
|
156
|
+
|
|
157
|
+
slide_w, slide_h = slide_size_in(prs)
|
|
158
|
+
_keep_only(prs, sources)
|
|
159
|
+
kept = _slides(prs)
|
|
160
|
+
|
|
161
|
+
applied: list[dict] = []
|
|
162
|
+
for position, entry in enumerate(entries):
|
|
163
|
+
slide = kept[position]
|
|
164
|
+
for element in entry.get("elements", []):
|
|
165
|
+
element_id = str(element.get("elementId", ""))
|
|
166
|
+
action = str(element.get("action", "keep"))
|
|
167
|
+
target = element.get("target") or {}
|
|
168
|
+
if action == "keep":
|
|
169
|
+
continue
|
|
170
|
+
if action == "fill-placeholder":
|
|
171
|
+
alias = str(target.get("placeholder") or "")
|
|
172
|
+
shape = _find_placeholder(slide, alias) if alias else _find_shape(slide, element_id)
|
|
173
|
+
if shape is None:
|
|
174
|
+
applied.append({"slide": position + 1, "element": element_id, "action": action, "status": "not_found"})
|
|
175
|
+
continue
|
|
176
|
+
if target.get("text") is not None:
|
|
177
|
+
_set_text(shape, str(target["text"]))
|
|
178
|
+
applied.append({"slide": position + 1, "element": shape.name, "action": action, "status": "applied"})
|
|
179
|
+
continue
|
|
180
|
+
shape = _find_shape(slide, element_id)
|
|
181
|
+
if shape is None:
|
|
182
|
+
applied.append({"slide": position + 1, "element": element_id, "action": action, "status": "not_found"})
|
|
183
|
+
continue
|
|
184
|
+
if action == "delete":
|
|
185
|
+
shape._element.getparent().remove(shape._element)
|
|
186
|
+
applied.append({"slide": position + 1, "element": element_id, "action": action, "status": "applied"})
|
|
187
|
+
continue
|
|
188
|
+
if action in {"rewrite", "rewrite-and-reposition"}:
|
|
189
|
+
_set_text(shape, str(target.get("text", "")))
|
|
190
|
+
if action == "rewrite-and-reposition" and isinstance(target.get("zone"), list):
|
|
191
|
+
x, y, width, height = (float(value) for value in target["zone"])
|
|
192
|
+
shape.left = int(x * slide_w * 914400)
|
|
193
|
+
shape.top = int(y * slide_h * 914400)
|
|
194
|
+
shape.width = int(width * slide_w * 914400)
|
|
195
|
+
shape.height = int(height * slide_h * 914400)
|
|
196
|
+
applied.append({"slide": position + 1, "element": element_id, "action": action, "status": "applied"})
|
|
197
|
+
continue
|
|
198
|
+
applied.append({"slide": position + 1, "element": element_id, "action": action, "status": "left_for_author"})
|
|
199
|
+
|
|
200
|
+
destination = Path(out_path)
|
|
201
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
202
|
+
prs.save(str(destination))
|
|
203
|
+
|
|
204
|
+
leftovers = []
|
|
205
|
+
for index, slide in enumerate(kept, start=1):
|
|
206
|
+
for shape in slide.shapes:
|
|
207
|
+
if shape.is_placeholder and not (shape.text_frame.text or "").strip():
|
|
208
|
+
leftovers.append({"slide": index, "placeholder": shape.name, "bboxIn": [round(value, 2) for value in (box(shape) or (0, 0, 0, 0))]})
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
"ok": True,
|
|
212
|
+
"starter": str(destination.resolve()),
|
|
213
|
+
"slides": len(kept),
|
|
214
|
+
"sourceSlides": sources,
|
|
215
|
+
"applied": applied,
|
|
216
|
+
"emptyPlaceholders": leftovers,
|
|
217
|
+
"note": "empty placeholders still show their prompt in edit view; fill or delete them before delivery",
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def main(argv: list[str] | None = None) -> int:
|
|
222
|
+
import argparse
|
|
223
|
+
|
|
224
|
+
parser = argparse.ArgumentParser(description="Build a starter deck from the template slides a plan keeps.")
|
|
225
|
+
parser.add_argument("--template", required=True, help="path to the template .pptx")
|
|
226
|
+
parser.add_argument("--plan", required=True, help="path to template-frame-map.json")
|
|
227
|
+
parser.add_argument("--out", required=True, help="path of the starter .pptx to write")
|
|
228
|
+
args = parser.parse_args(argv)
|
|
229
|
+
|
|
230
|
+
report = build_starter(args.template, args.plan, args.out)
|
|
231
|
+
json.dump(report, sys.stdout, ensure_ascii=False, indent=2)
|
|
232
|
+
sys.stdout.write("\n")
|
|
233
|
+
if report.get("ok"):
|
|
234
|
+
return 0
|
|
235
|
+
return 2 if report["error"]["code"] in {"template_missing", "plan_missing"} else 1
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
raise SystemExit(main())
|
package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Check a template plan before any slide is touched.
|
|
2
|
+
|
|
3
|
+
Purpose: a template plan is a contract between "what the design gives you" and
|
|
4
|
+
"what you intend to write". Validating it first turns a silent redesign into a
|
|
5
|
+
list of violations, because the expensive mistakes (deleting a logo, inventing
|
|
6
|
+
new blocks the brand never had, covering inherited art) are all visible on
|
|
7
|
+
paper before anything is built.
|
|
8
|
+
|
|
9
|
+
Plan shape (template-frame-map.json):
|
|
10
|
+
{
|
|
11
|
+
"template": "path/to/template.pptx",
|
|
12
|
+
"newPrimitives": {"allowed": false, "justification": ""},
|
|
13
|
+
"outputSlides": [
|
|
14
|
+
{"slide": 1, "mode": "duplicate-slide", "sourceSlide": 3,
|
|
15
|
+
"elements": [{"elementId": "shape-7", "action": "fill-placeholder",
|
|
16
|
+
"target": {"placeholder": "title", "text": "Q3 results"}}]}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Exit codes: 0 valid, 1 violations, 2 unreadable plan.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "container_tools"))
|
|
30
|
+
|
|
31
|
+
ALLOWED_ACTIONS = {"rewrite", "rewrite-and-reposition", "replace", "delete", "keep", "fill-placeholder"}
|
|
32
|
+
PRESERVE_ONLY_ROLES = {"brand", "logo", "bumper", "divider", "section", "chrome", "footer"}
|
|
33
|
+
MODES = {"duplicate-slide"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _violation(code: str, message: str, severity: str = "fail", **extra) -> dict:
|
|
37
|
+
return {"code": code, "severity": severity, "message": message, **extra}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def validate_plan(plan: dict) -> dict:
|
|
41
|
+
violations: list[dict] = []
|
|
42
|
+
warnings: list[dict] = []
|
|
43
|
+
|
|
44
|
+
output_slides = plan.get("outputSlides")
|
|
45
|
+
if not isinstance(output_slides, list) or not output_slides:
|
|
46
|
+
return {"gate": "template-plan", "passed": False, "violations": [_violation("missing_output_slides", "the plan declares no output slides")], "checks": 0}
|
|
47
|
+
|
|
48
|
+
indices = []
|
|
49
|
+
sources: dict[int, int] = {}
|
|
50
|
+
element_actions = 0
|
|
51
|
+
for position, entry in enumerate(output_slides, start=1):
|
|
52
|
+
if not isinstance(entry, dict):
|
|
53
|
+
violations.append(_violation("malformed_slide", f"output slide #{position} is not an object"))
|
|
54
|
+
continue
|
|
55
|
+
slide_no = entry.get("slide", position)
|
|
56
|
+
indices.append(slide_no)
|
|
57
|
+
mode = entry.get("mode", "duplicate-slide")
|
|
58
|
+
if mode not in MODES:
|
|
59
|
+
violations.append(_violation("unsupported_mode", f"slide {slide_no}: mode '{mode}' is not supported", slide=slide_no))
|
|
60
|
+
source = entry.get("sourceSlide")
|
|
61
|
+
if not isinstance(source, int) or source < 1:
|
|
62
|
+
violations.append(_violation("missing_source_slide", f"slide {slide_no}: sourceSlide must be a positive integer", slide=slide_no))
|
|
63
|
+
else:
|
|
64
|
+
if source in sources:
|
|
65
|
+
warnings.append(
|
|
66
|
+
_violation(
|
|
67
|
+
"duplicated_source_slide",
|
|
68
|
+
f"slides {sources[source]} and {slide_no} both duplicate source slide {source}",
|
|
69
|
+
severity="review",
|
|
70
|
+
slide=slide_no,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
sources[source] = slide_no
|
|
74
|
+
elements = entry.get("elements", [])
|
|
75
|
+
if not isinstance(elements, list):
|
|
76
|
+
violations.append(_violation("malformed_elements", f"slide {slide_no}: elements must be a list", slide=slide_no))
|
|
77
|
+
continue
|
|
78
|
+
for element in elements:
|
|
79
|
+
if not isinstance(element, dict) or not element.get("elementId"):
|
|
80
|
+
violations.append(_violation("missing_element_id", f"slide {slide_no}: an element has no elementId", slide=slide_no))
|
|
81
|
+
continue
|
|
82
|
+
element_actions += 1
|
|
83
|
+
element_id = element["elementId"]
|
|
84
|
+
action = element.get("action")
|
|
85
|
+
target = element.get("target") or {}
|
|
86
|
+
if action not in ALLOWED_ACTIONS:
|
|
87
|
+
violations.append(
|
|
88
|
+
_violation(
|
|
89
|
+
"unknown_action",
|
|
90
|
+
f"slide {slide_no} element {element_id}: '{action}' is not one of {sorted(ALLOWED_ACTIONS)}",
|
|
91
|
+
slide=slide_no,
|
|
92
|
+
elementId=element_id,
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
continue
|
|
96
|
+
role = str(target.get("role") or element.get("role") or "").lower()
|
|
97
|
+
if role in PRESERVE_ONLY_ROLES and action != "keep":
|
|
98
|
+
violations.append(
|
|
99
|
+
_violation(
|
|
100
|
+
"preserve_only_element",
|
|
101
|
+
f"slide {slide_no} element {element_id}: role '{role}' must stay untouched, found '{action}'",
|
|
102
|
+
slide=slide_no,
|
|
103
|
+
elementId=element_id,
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
if action == "fill-placeholder" and not (target.get("placeholder") or target.get("text")):
|
|
107
|
+
violations.append(
|
|
108
|
+
_violation("placeholder_target_missing", f"slide {slide_no} element {element_id}: fill-placeholder needs a placeholder or text target", slide=slide_no, elementId=element_id)
|
|
109
|
+
)
|
|
110
|
+
if action in {"rewrite", "rewrite-and-reposition"} and not target.get("text"):
|
|
111
|
+
violations.append(
|
|
112
|
+
_violation("rewrite_text_missing", f"slide {slide_no} element {element_id}: {action} needs target.text", slide=slide_no, elementId=element_id)
|
|
113
|
+
)
|
|
114
|
+
if action == "rewrite-and-reposition":
|
|
115
|
+
zone = target.get("zone")
|
|
116
|
+
if not (isinstance(zone, list) and len(zone) == 4 and all(isinstance(value, (int, float)) for value in zone)):
|
|
117
|
+
violations.append(
|
|
118
|
+
_violation("zone_missing", f"slide {slide_no} element {element_id}: rewrite-and-reposition needs a zone [x, y, w, h]", slide=slide_no, elementId=element_id)
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
x, y, width, height = zone
|
|
122
|
+
if not all(0.0 <= value <= 1.0 for value in zone) or x + width > 1.0001 or y + height > 1.0001:
|
|
123
|
+
violations.append(
|
|
124
|
+
_violation("zone_out_of_bounds", f"slide {slide_no} element {element_id}: zone {zone} does not fit inside 0-1", slide=slide_no, elementId=element_id)
|
|
125
|
+
)
|
|
126
|
+
if action == "replace":
|
|
127
|
+
warnings.append(
|
|
128
|
+
_violation(
|
|
129
|
+
"replace_needs_authoring",
|
|
130
|
+
f"slide {slide_no} element {element_id}: 'replace' is applied by the author with a recipe, not by the starter-deck builder",
|
|
131
|
+
severity="review",
|
|
132
|
+
slide=slide_no,
|
|
133
|
+
elementId=element_id,
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if indices != list(range(1, len(output_slides) + 1)):
|
|
138
|
+
violations.append(_violation("non_sequential_slides", f"output slide numbers must start at 1 and be sequential, found {indices}"))
|
|
139
|
+
|
|
140
|
+
primitives = plan.get("newPrimitives") or {}
|
|
141
|
+
if primitives.get("allowed") is True and not str(primitives.get("justification") or "").strip():
|
|
142
|
+
violations.append(_violation("new_primitives_unjustified", "newPrimitives.allowed is true but no justification was given"))
|
|
143
|
+
|
|
144
|
+
blocking = [item for item in violations if item["severity"] == "fail"]
|
|
145
|
+
return {
|
|
146
|
+
"gate": "template-plan",
|
|
147
|
+
"passed": not blocking,
|
|
148
|
+
"checks": {"outputSlides": len(output_slides), "elementActions": element_actions},
|
|
149
|
+
"violations": violations,
|
|
150
|
+
"warnings": warnings,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def main(argv: list[str] | None = None) -> int:
|
|
155
|
+
import argparse
|
|
156
|
+
|
|
157
|
+
parser = argparse.ArgumentParser(description="Validate a template-frame-map before building the deck.")
|
|
158
|
+
parser.add_argument("--plan", required=True, help="path to template-frame-map.json")
|
|
159
|
+
parser.add_argument("--template", help="optionally confirm the plan names an existing template file")
|
|
160
|
+
parser.add_argument("--json-out", help="also write the report here")
|
|
161
|
+
args = parser.parse_args(argv)
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
plan = json.loads(Path(args.plan).read_text(encoding="utf-8"))
|
|
165
|
+
except Exception as exc: # noqa: BLE001 - a bad plan is the finding
|
|
166
|
+
json.dump({"gate": "template-plan", "passed": False, "error": {"code": "unparsable_plan", "message": str(exc)}}, sys.stdout)
|
|
167
|
+
sys.stdout.write("\n")
|
|
168
|
+
return 2
|
|
169
|
+
|
|
170
|
+
report = validate_plan(plan)
|
|
171
|
+
if args.template:
|
|
172
|
+
declared = plan.get("template") or args.template
|
|
173
|
+
report["template"] = str(declared)
|
|
174
|
+
if not Path(declared).is_file():
|
|
175
|
+
report["violations"].append({"code": "template_missing", "severity": "fail", "message": f"template not found: {declared}"})
|
|
176
|
+
report["passed"] = False
|
|
177
|
+
if args.json_out:
|
|
178
|
+
Path(args.json_out).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
179
|
+
json.dump(report, sys.stdout, ensure_ascii=False, indent=2)
|
|
180
|
+
sys.stdout.write("\n")
|
|
181
|
+
return 0 if report["passed"] else 1
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
raise SystemExit(main())
|