@taloswork/ose-shared 0.1.7 → 0.1.9
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/package.json +3 -3
- package/skills/cozy-static-svg/SKILL.md +76 -0
- package/skills/cozy-static-svg/references/approved-preview.png +0 -0
- package/skills/cozy-static-svg/references/cross-platform.md +73 -0
- package/skills/cozy-static-svg/references/moon_keeps_watch.svg +229 -0
- package/skills/cozy-static-svg/references/style-anchor.md +271 -0
- package/skills/cozy-static-svg/scripts/artkit.py +321 -0
- package/skills/cozy-static-svg/scripts/to_png.py +333 -0
- package/skills/diagram-design/SKILL.md +568 -0
- package/skills/diagram-design/assets/example-architecture-dark.html +182 -0
- package/skills/diagram-design/assets/example-architecture-full.html +194 -0
- package/skills/diagram-design/assets/example-architecture.html +183 -0
- package/skills/diagram-design/assets/example-bar-dark.html +129 -0
- package/skills/diagram-design/assets/example-bar-full.html +105 -0
- package/skills/diagram-design/assets/example-bar.html +129 -0
- package/skills/diagram-design/assets/example-data-flow-dark.html +143 -0
- package/skills/diagram-design/assets/example-data-flow-full.html +185 -0
- package/skills/diagram-design/assets/example-data-flow.html +143 -0
- package/skills/diagram-design/assets/example-datalake-dark.html +243 -0
- package/skills/diagram-design/assets/example-datalake-full.html +248 -0
- package/skills/diagram-design/assets/example-datalake.html +269 -0
- package/skills/diagram-design/assets/example-dp-integration-dark.html +80 -0
- package/skills/diagram-design/assets/example-dp-integration-full.html +90 -0
- package/skills/diagram-design/assets/example-dp-integration.html +80 -0
- package/skills/diagram-design/assets/example-dp-security-matrix-dark.html +31 -0
- package/skills/diagram-design/assets/example-dp-security-matrix-full.html +32 -0
- package/skills/diagram-design/assets/example-dp-security-matrix.html +88 -0
- package/skills/diagram-design/assets/example-er-dark.html +202 -0
- package/skills/diagram-design/assets/example-er-full.html +205 -0
- package/skills/diagram-design/assets/example-er.html +202 -0
- package/skills/diagram-design/assets/example-flowchart-dark.html +156 -0
- package/skills/diagram-design/assets/example-flowchart-full.html +159 -0
- package/skills/diagram-design/assets/example-flowchart.html +156 -0
- package/skills/diagram-design/assets/example-gantt-dark.html +137 -0
- package/skills/diagram-design/assets/example-gantt-full.html +127 -0
- package/skills/diagram-design/assets/example-gantt.html +137 -0
- package/skills/diagram-design/assets/example-high-level-dark.html +251 -0
- package/skills/diagram-design/assets/example-high-level-full.html +247 -0
- package/skills/diagram-design/assets/example-high-level-vertical-dark.html +274 -0
- package/skills/diagram-design/assets/example-high-level-vertical-full.html +252 -0
- package/skills/diagram-design/assets/example-high-level-vertical.html +325 -0
- package/skills/diagram-design/assets/example-high-level.html +301 -0
- package/skills/diagram-design/assets/example-import-drawio.html +185 -0
- package/skills/diagram-design/assets/example-import-mermaid.html +70 -0
- package/skills/diagram-design/assets/example-it-state-dark.html +90 -0
- package/skills/diagram-design/assets/example-it-state-full.html +69 -0
- package/skills/diagram-design/assets/example-it-state.html +128 -0
- package/skills/diagram-design/assets/example-layers-dark.html +123 -0
- package/skills/diagram-design/assets/example-layers-full.html +126 -0
- package/skills/diagram-design/assets/example-layers.html +123 -0
- package/skills/diagram-design/assets/example-line-dark.html +111 -0
- package/skills/diagram-design/assets/example-line-full.html +105 -0
- package/skills/diagram-design/assets/example-line.html +111 -0
- package/skills/diagram-design/assets/example-loop-dark.html +100 -0
- package/skills/diagram-design/assets/example-loop-full.html +137 -0
- package/skills/diagram-design/assets/example-loop-terminal.html +347 -0
- package/skills/diagram-design/assets/example-loop.html +100 -0
- package/skills/diagram-design/assets/example-medallion-dark.html +159 -0
- package/skills/diagram-design/assets/example-medallion-full.html +201 -0
- package/skills/diagram-design/assets/example-medallion.html +159 -0
- package/skills/diagram-design/assets/example-nested-dark.html +132 -0
- package/skills/diagram-design/assets/example-nested-full.html +135 -0
- package/skills/diagram-design/assets/example-nested.html +136 -0
- package/skills/diagram-design/assets/example-org-chart-dark.html +45 -0
- package/skills/diagram-design/assets/example-org-chart-full.html +45 -0
- package/skills/diagram-design/assets/example-org-chart.html +45 -0
- package/skills/diagram-design/assets/example-paved-road-animated.html +546 -0
- package/skills/diagram-design/assets/example-policy-trace-animated.html +490 -0
- package/skills/diagram-design/assets/example-process-dark.html +64 -0
- package/skills/diagram-design/assets/example-process-full.html +74 -0
- package/skills/diagram-design/assets/example-process.html +64 -0
- package/skills/diagram-design/assets/example-pyramid-dark.html +119 -0
- package/skills/diagram-design/assets/example-pyramid-full.html +122 -0
- package/skills/diagram-design/assets/example-pyramid.html +119 -0
- package/skills/diagram-design/assets/example-quadrant-consultant.html +166 -0
- package/skills/diagram-design/assets/example-quadrant-dark.html +135 -0
- package/skills/diagram-design/assets/example-quadrant-full.html +138 -0
- package/skills/diagram-design/assets/example-quadrant.html +135 -0
- package/skills/diagram-design/assets/example-queue-animated.html +577 -0
- package/skills/diagram-design/assets/example-radar-dark.html +139 -0
- package/skills/diagram-design/assets/example-radar-full.html +135 -0
- package/skills/diagram-design/assets/example-radar.html +139 -0
- package/skills/diagram-design/assets/example-scatter-dark.html +141 -0
- package/skills/diagram-design/assets/example-scatter-full.html +116 -0
- package/skills/diagram-design/assets/example-scatter.html +141 -0
- package/skills/diagram-design/assets/example-sequence-dark.html +222 -0
- package/skills/diagram-design/assets/example-sequence-full.html +388 -0
- package/skills/diagram-design/assets/example-sequence-oauth-dark.html +188 -0
- package/skills/diagram-design/assets/example-sequence-oauth-full.html +350 -0
- package/skills/diagram-design/assets/example-sequence-oauth.html +227 -0
- package/skills/diagram-design/assets/example-sequence.html +222 -0
- package/skills/diagram-design/assets/example-state-dark.html +147 -0
- package/skills/diagram-design/assets/example-state-full.html +150 -0
- package/skills/diagram-design/assets/example-state.html +147 -0
- package/skills/diagram-design/assets/example-swimlane-dark.html +172 -0
- package/skills/diagram-design/assets/example-swimlane-full.html +175 -0
- package/skills/diagram-design/assets/example-swimlane.html +172 -0
- package/skills/diagram-design/assets/example-timeline-dark.html +135 -0
- package/skills/diagram-design/assets/example-timeline-full.html +138 -0
- package/skills/diagram-design/assets/example-timeline.html +135 -0
- package/skills/diagram-design/assets/example-tree-dark.html +173 -0
- package/skills/diagram-design/assets/example-tree-full.html +176 -0
- package/skills/diagram-design/assets/example-tree.html +173 -0
- package/skills/diagram-design/assets/example-venn-dark.html +132 -0
- package/skills/diagram-design/assets/example-venn-full.html +135 -0
- package/skills/diagram-design/assets/example-venn.html +110 -0
- package/skills/diagram-design/assets/icons.html +230 -0
- package/skills/diagram-design/assets/index.html +369 -0
- package/skills/diagram-design/assets/template-dark.html +85 -0
- package/skills/diagram-design/assets/template-full.html +413 -0
- package/skills/diagram-design/assets/template-motion.html +434 -0
- package/skills/diagram-design/assets/template-terminal.html +173 -0
- package/skills/diagram-design/assets/template.html +86 -0
- package/skills/diagram-design/references/animation.md +141 -0
- package/skills/diagram-design/references/export.md +136 -0
- package/skills/diagram-design/references/import-drawio.md +171 -0
- package/skills/diagram-design/references/import-mermaid.md +126 -0
- package/skills/diagram-design/references/onboarding.md +297 -0
- package/skills/diagram-design/references/output-spec.md +181 -0
- package/skills/diagram-design/references/primitive-annotation.md +36 -0
- package/skills/diagram-design/references/primitive-icons.md +827 -0
- package/skills/diagram-design/references/primitive-sketchy.md +43 -0
- package/skills/diagram-design/references/primitive-terminal.md +76 -0
- package/skills/diagram-design/references/semantic-patterns.md +122 -0
- package/skills/diagram-design/references/style-guide.md +139 -0
- package/skills/diagram-design/references/type-architecture.md +78 -0
- package/skills/diagram-design/references/type-bar.md +48 -0
- package/skills/diagram-design/references/type-data-flow.md +374 -0
- package/skills/diagram-design/references/type-dp-integration.md +410 -0
- package/skills/diagram-design/references/type-dp-security-matrix.md +379 -0
- package/skills/diagram-design/references/type-er.md +23 -0
- package/skills/diagram-design/references/type-flowchart.md +23 -0
- package/skills/diagram-design/references/type-gantt.md +45 -0
- package/skills/diagram-design/references/type-high-level.md +458 -0
- package/skills/diagram-design/references/type-it-state.md +470 -0
- package/skills/diagram-design/references/type-layers.md +26 -0
- package/skills/diagram-design/references/type-line.md +44 -0
- package/skills/diagram-design/references/type-loop.md +223 -0
- package/skills/diagram-design/references/type-medallion.md +356 -0
- package/skills/diagram-design/references/type-nested.md +22 -0
- package/skills/diagram-design/references/type-org-chart.md +44 -0
- package/skills/diagram-design/references/type-process.md +495 -0
- package/skills/diagram-design/references/type-pyramid.md +33 -0
- package/skills/diagram-design/references/type-quadrant.md +81 -0
- package/skills/diagram-design/references/type-radar.md +80 -0
- package/skills/diagram-design/references/type-scatter.md +39 -0
- package/skills/diagram-design/references/type-sequence.md +130 -0
- package/skills/diagram-design/references/type-state.md +21 -0
- package/skills/diagram-design/references/type-swimlane.md +20 -0
- package/skills/diagram-design/references/type-timeline.md +20 -0
- package/skills/diagram-design/references/type-tree.md +24 -0
- package/skills/diagram-design/references/type-venn.md +26 -0
- package/skills/diagram-design/scripts/drawio_extract.py +856 -0
- package/skills/diagram-design/scripts/mermaid_extract.py +1285 -0
- package/skills/diagram-design/scripts/self_check.py +389 -0
- package/skills/document/bin/doc-gen.cjs +3 -6
- package/skills/document/bin/html2pptx.cjs +17 -9
- package/skills/document/bin/pptx-clone.cjs +41 -30
- package/skills/document/bin/pptx-embed-fonts.cjs +6 -2
- package/skills/document/bin/pptx-slide.cjs +46 -50
- package/skills/document/content-pipeline.md +54 -5
- package/skills/document/layouts/README.md +8 -0
- package/skills/document/layouts/blocks-2x2.tpl +19 -9
- package/skills/document/layouts/cards-3.tpl +1 -1
- package/skills/document/layouts/org-tree.tpl +1 -1
- package/skills/document/layouts/steps-vertical.tpl +1 -1
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Self-check a generated diagram HTML file, with no third-party deps.
|
|
3
|
+
|
|
4
|
+
Ships inside the skill so an installed agent can verify its own output:
|
|
5
|
+
|
|
6
|
+
python3 <skill-dir>/scripts/self_check.py my-diagram.html
|
|
7
|
+
|
|
8
|
+
Checks the accessible-SVG contract, the single-file safety rules (no remote
|
|
9
|
+
assets beyond the approved Google Fonts stylesheet, no executable attributes,
|
|
10
|
+
no scripts other than the one canonical motion controller), and — when motion
|
|
11
|
+
markup is present — the structural motion contract. This is a distilled
|
|
12
|
+
subset of the repository gates (`lint-skin.py`, `verify-motion.py`), which
|
|
13
|
+
remain the authority for contributions to the repository itself.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
from collections import Counter
|
|
22
|
+
from html.parser import HTMLParser
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from urllib.parse import urlparse
|
|
25
|
+
|
|
26
|
+
SKILL_DIR = Path(__file__).resolve().parent.parent
|
|
27
|
+
MOTION_TEMPLATE = SKILL_DIR / "assets" / "template-motion.html"
|
|
28
|
+
MODES = {"none", "reveal", "step", "loop"}
|
|
29
|
+
ACTIONS = {"play", "pause", "replay", "prev", "next"}
|
|
30
|
+
ASCII_DECIMAL_RE = re.compile(r"^[0-9]+$")
|
|
31
|
+
REFERENCE_ATTRS = {"src", "href", "xlink:href", "poster", "srcset", "action", "formaction"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DiagramParser(HTMLParser):
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
super().__init__(convert_charrefs=True)
|
|
37
|
+
self.roots: list[dict[str, str]] = []
|
|
38
|
+
self.items: list[dict[str, str]] = []
|
|
39
|
+
self.actions: set[str] = set()
|
|
40
|
+
self.controls = 0
|
|
41
|
+
self.statuses: list[dict[str, str]] = []
|
|
42
|
+
self.statuses_in_controls = 0
|
|
43
|
+
self.scripts: list[dict[str, object]] = []
|
|
44
|
+
self.styles: list[str] = []
|
|
45
|
+
self.svgs: list[dict[str, object]] = []
|
|
46
|
+
self.unsafe: list[str] = []
|
|
47
|
+
self.references: list[tuple[str, str, str]] = []
|
|
48
|
+
self._svg_depth = 0
|
|
49
|
+
self._current_svg: dict[str, object] | None = None
|
|
50
|
+
self._capture: str | None = None
|
|
51
|
+
self._current_script: dict[str, object] | None = None
|
|
52
|
+
self._in_style = False
|
|
53
|
+
self._element_stack: list[str] = []
|
|
54
|
+
self._motion_root_depth: int | None = None
|
|
55
|
+
self._controls_depth: int | None = None
|
|
56
|
+
|
|
57
|
+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
58
|
+
tag = tag.casefold()
|
|
59
|
+
normalized_attrs = [(key.casefold(), value or "") for key, value in attrs]
|
|
60
|
+
data = {key: value for key, value in normalized_attrs}
|
|
61
|
+
if tag in {"base", "embed", "object", "iframe"}:
|
|
62
|
+
self.unsafe.append(f"<{tag}> is not allowed in a diagram file")
|
|
63
|
+
for key, value in normalized_attrs:
|
|
64
|
+
if key.startswith("on"):
|
|
65
|
+
self.unsafe.append(f"executable attribute {key} on <{tag}>")
|
|
66
|
+
if key == "srcdoc":
|
|
67
|
+
self.unsafe.append(f"srcdoc attribute on <{tag}>")
|
|
68
|
+
if key in REFERENCE_ATTRS:
|
|
69
|
+
self.references.append((tag, data.get("rel", ""), value))
|
|
70
|
+
if "data-motion-root" in data:
|
|
71
|
+
self.roots.append(data)
|
|
72
|
+
if self._motion_root_depth is None:
|
|
73
|
+
self._motion_root_depth = len(self._element_stack)
|
|
74
|
+
if self._motion_root_depth is not None:
|
|
75
|
+
if "data-motion-item" in data:
|
|
76
|
+
self.items.append(data)
|
|
77
|
+
if "data-motion-action" in data:
|
|
78
|
+
self.actions.add(data["data-motion-action"])
|
|
79
|
+
if "data-motion-controls" in data:
|
|
80
|
+
self.controls += 1
|
|
81
|
+
if self._controls_depth is None:
|
|
82
|
+
self._controls_depth = len(self._element_stack)
|
|
83
|
+
if "data-motion-status" in data:
|
|
84
|
+
self.statuses.append(data)
|
|
85
|
+
if self._controls_depth is not None:
|
|
86
|
+
self.statuses_in_controls += 1
|
|
87
|
+
if tag == "script":
|
|
88
|
+
self._current_script = {
|
|
89
|
+
"attrs": data,
|
|
90
|
+
"attr_names": [name for name, _value in normalized_attrs],
|
|
91
|
+
"body": [],
|
|
92
|
+
"closed": False,
|
|
93
|
+
}
|
|
94
|
+
self.scripts.append(self._current_script)
|
|
95
|
+
if tag == "style":
|
|
96
|
+
self._in_style = True
|
|
97
|
+
self._element_stack.append(tag)
|
|
98
|
+
if tag == "svg" and self._svg_depth == 0:
|
|
99
|
+
self._svg_depth = 1
|
|
100
|
+
self._current_svg = {"attrs": data, "first": None, "title": {}, "desc": {}}
|
|
101
|
+
self.svgs.append(self._current_svg)
|
|
102
|
+
return
|
|
103
|
+
if self._svg_depth:
|
|
104
|
+
self._svg_depth += 1
|
|
105
|
+
assert self._current_svg is not None
|
|
106
|
+
if self._svg_depth == 2 and self._current_svg["first"] is None:
|
|
107
|
+
self._current_svg["first"] = tag
|
|
108
|
+
if self._svg_depth == 2 and tag in {"title", "desc"}:
|
|
109
|
+
self._current_svg[tag] = {"attrs": data, "text": ""}
|
|
110
|
+
self._capture = tag
|
|
111
|
+
|
|
112
|
+
def handle_endtag(self, tag: str) -> None:
|
|
113
|
+
tag = tag.casefold()
|
|
114
|
+
if tag == "script" and self._current_script is not None:
|
|
115
|
+
self._current_script["closed"] = True
|
|
116
|
+
self._current_script = None
|
|
117
|
+
if tag == "style":
|
|
118
|
+
self._in_style = False
|
|
119
|
+
if self._svg_depth:
|
|
120
|
+
if tag in {"title", "desc"}:
|
|
121
|
+
self._capture = None
|
|
122
|
+
self._svg_depth -= 1
|
|
123
|
+
if self._svg_depth == 0:
|
|
124
|
+
self._current_svg = None
|
|
125
|
+
for index in range(len(self._element_stack) - 1, -1, -1):
|
|
126
|
+
if self._element_stack[index] == tag:
|
|
127
|
+
del self._element_stack[index:]
|
|
128
|
+
break
|
|
129
|
+
if (
|
|
130
|
+
self._motion_root_depth is not None
|
|
131
|
+
and len(self._element_stack) <= self._motion_root_depth
|
|
132
|
+
):
|
|
133
|
+
self._motion_root_depth = None
|
|
134
|
+
if (
|
|
135
|
+
self._controls_depth is not None
|
|
136
|
+
and len(self._element_stack) <= self._controls_depth
|
|
137
|
+
):
|
|
138
|
+
self._controls_depth = None
|
|
139
|
+
|
|
140
|
+
def handle_data(self, data: str) -> None:
|
|
141
|
+
if self._current_script is not None:
|
|
142
|
+
body = self._current_script["body"]
|
|
143
|
+
assert isinstance(body, list)
|
|
144
|
+
body.append(data)
|
|
145
|
+
if self._in_style:
|
|
146
|
+
self.styles.append(data)
|
|
147
|
+
if self._capture and self._current_svg:
|
|
148
|
+
node = self._current_svg[self._capture]
|
|
149
|
+
assert isinstance(node, dict)
|
|
150
|
+
node["text"] = str(node.get("text", "")) + data
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def normalized_controller(body: str) -> str:
|
|
154
|
+
return body.replace("\r\n", "\n").replace("\r", "\n").strip()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def parsed_document(source: str) -> DiagramParser:
|
|
158
|
+
parser = DiagramParser()
|
|
159
|
+
parser.feed(source)
|
|
160
|
+
parser.close()
|
|
161
|
+
return parser
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def is_approved_google_fonts_stylesheet(value: str) -> bool:
|
|
165
|
+
try:
|
|
166
|
+
parsed = urlparse(value)
|
|
167
|
+
except ValueError:
|
|
168
|
+
return False
|
|
169
|
+
return (
|
|
170
|
+
parsed.scheme == "https"
|
|
171
|
+
and parsed.hostname is not None
|
|
172
|
+
and parsed.hostname.casefold() == "fonts.googleapis.com"
|
|
173
|
+
and parsed.port is None
|
|
174
|
+
and parsed.path == "/css2"
|
|
175
|
+
and not parsed.fragment
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def reference_error(tag: str, rel: str, value: str) -> str | None:
|
|
180
|
+
stripped = value.strip()
|
|
181
|
+
lowered = stripped.casefold()
|
|
182
|
+
if not stripped or stripped.startswith("#"):
|
|
183
|
+
return None
|
|
184
|
+
if lowered.startswith("javascript:") or lowered.startswith("data:text/html"):
|
|
185
|
+
return f"executable URL on <{tag}>: {stripped[:80]}"
|
|
186
|
+
remote = lowered.startswith(("http://", "https://", "//")) or (
|
|
187
|
+
":" in stripped.split("/", 1)[0] and not lowered.startswith("data:")
|
|
188
|
+
)
|
|
189
|
+
if not remote:
|
|
190
|
+
if lowered.startswith("data:") and not lowered.startswith("data:image/"):
|
|
191
|
+
return f"non-image data URL on <{tag}>: {stripped[:80]}"
|
|
192
|
+
return None
|
|
193
|
+
if tag == "link" and "stylesheet" in rel.casefold().split():
|
|
194
|
+
if is_approved_google_fonts_stylesheet(stripped):
|
|
195
|
+
return None
|
|
196
|
+
return f"remote stylesheet is not the approved Google Fonts /css2 URL: {stripped[:80]}"
|
|
197
|
+
return f"remote reference on <{tag}>: {stripped[:80]}"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def canonical_controller() -> str:
|
|
201
|
+
if not MOTION_TEMPLATE.is_file():
|
|
202
|
+
raise RuntimeError(
|
|
203
|
+
f"cannot find the canonical controller at {MOTION_TEMPLATE}; "
|
|
204
|
+
"run self_check.py from its shipped location inside the skill"
|
|
205
|
+
)
|
|
206
|
+
parser = parsed_document(MOTION_TEMPLATE.read_text(encoding="utf-8"))
|
|
207
|
+
if len(parser.scripts) != 1 or not parser.scripts[0]["closed"]:
|
|
208
|
+
raise RuntimeError("template-motion.html must contain one closed controller")
|
|
209
|
+
body = parser.scripts[0]["body"]
|
|
210
|
+
assert isinstance(body, list)
|
|
211
|
+
return normalized_controller("".join(body))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def check_svgs(parser: DiagramParser, errors: list[str]) -> None:
|
|
215
|
+
checkable = [
|
|
216
|
+
svg
|
|
217
|
+
for svg in parser.svgs
|
|
218
|
+
if isinstance(svg["attrs"], dict)
|
|
219
|
+
and str(svg["attrs"].get("aria-hidden", "")).casefold() != "true"
|
|
220
|
+
]
|
|
221
|
+
if not checkable:
|
|
222
|
+
errors.append("diagram file needs at least one accessible (non-aria-hidden) SVG")
|
|
223
|
+
for number, svg in enumerate(checkable, 1):
|
|
224
|
+
attrs = svg["attrs"]
|
|
225
|
+
assert isinstance(attrs, dict)
|
|
226
|
+
if attrs.get("role") != "img":
|
|
227
|
+
errors.append(f"svg {number} needs role=img")
|
|
228
|
+
labelled = attrs.get("aria-labelledby", "").split()
|
|
229
|
+
title = svg["title"]
|
|
230
|
+
desc = svg["desc"]
|
|
231
|
+
assert isinstance(title, dict) and isinstance(desc, dict)
|
|
232
|
+
title_attrs = title.get("attrs", {})
|
|
233
|
+
desc_attrs = desc.get("attrs", {})
|
|
234
|
+
assert isinstance(title_attrs, dict) and isinstance(desc_attrs, dict)
|
|
235
|
+
if svg["first"] != "title":
|
|
236
|
+
errors.append(f"svg {number} title must be its first child")
|
|
237
|
+
if not str(title.get("text", "")).strip() or not str(desc.get("text", "")).strip():
|
|
238
|
+
errors.append(f"svg {number} needs non-empty title and desc")
|
|
239
|
+
title_id = title_attrs.get("id", "")
|
|
240
|
+
desc_id = desc_attrs.get("id", "")
|
|
241
|
+
if title_id in {"", "title"} or desc_id in {"", "desc"}:
|
|
242
|
+
errors.append(f"svg {number} title/desc IDs must be diagram-prefixed, never bare")
|
|
243
|
+
if labelled != [title_id, desc_id]:
|
|
244
|
+
errors.append(f"svg {number} aria-labelledby must name title then desc")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def check_scripts(parser: DiagramParser, errors: list[str]) -> None:
|
|
248
|
+
if not parser.scripts:
|
|
249
|
+
return
|
|
250
|
+
if len(parser.scripts) > 1:
|
|
251
|
+
errors.append(f"at most one script is allowed; found {len(parser.scripts)}")
|
|
252
|
+
for number, script in enumerate(parser.scripts, 1):
|
|
253
|
+
attrs = script["attrs"]
|
|
254
|
+
attr_names = script["attr_names"]
|
|
255
|
+
body = script["body"]
|
|
256
|
+
assert isinstance(attrs, dict) and isinstance(attr_names, list) and isinstance(body, list)
|
|
257
|
+
if not script["closed"]:
|
|
258
|
+
errors.append(f"script {number} must have a closing script tag")
|
|
259
|
+
if attr_names != ["data-diagram-controls"] or attrs.get("data-diagram-controls") != "":
|
|
260
|
+
errors.append(f"script {number} must carry only the canonical data-diagram-controls attribute")
|
|
261
|
+
continue
|
|
262
|
+
try:
|
|
263
|
+
if normalized_controller("".join(body)) != canonical_controller():
|
|
264
|
+
errors.append(f"script {number} must exactly match the controller in template-motion.html")
|
|
265
|
+
except RuntimeError as exc:
|
|
266
|
+
errors.append(str(exc))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def check_motion(parser: DiagramParser, source: str, errors: list[str]) -> None:
|
|
270
|
+
has_motion_markup = bool(parser.roots or parser.items or parser.scripts)
|
|
271
|
+
if not has_motion_markup:
|
|
272
|
+
return
|
|
273
|
+
if len(parser.roots) != 1:
|
|
274
|
+
errors.append(f"expected exactly one data-motion-root; found {len(parser.roots)}")
|
|
275
|
+
return
|
|
276
|
+
root = parser.roots[0]
|
|
277
|
+
mode = root.get("data-motion-mode", "")
|
|
278
|
+
if mode not in MODES:
|
|
279
|
+
errors.append(f"data-motion-mode must be one of {sorted(MODES)}; got {mode!r}")
|
|
280
|
+
raw_count = root.get("data-step-count", "")
|
|
281
|
+
if not ASCII_DECIMAL_RE.fullmatch(raw_count):
|
|
282
|
+
count = -1
|
|
283
|
+
errors.append("data-step-count must be an ASCII decimal integer")
|
|
284
|
+
else:
|
|
285
|
+
count = int(raw_count)
|
|
286
|
+
minimum_count = 0 if mode == "none" else 1
|
|
287
|
+
if count < minimum_count or count > 8:
|
|
288
|
+
errors.append(f"semantic step count must be {minimum_count}..8; got {count}")
|
|
289
|
+
|
|
290
|
+
if len(parser.items) > 12:
|
|
291
|
+
errors.append(f"motion item budget is 12; found {len(parser.items)}")
|
|
292
|
+
semantic_steps: list[int] = []
|
|
293
|
+
for index, item in enumerate(parser.items, 1):
|
|
294
|
+
raw_step = item.get("data-step", "")
|
|
295
|
+
if not ASCII_DECIMAL_RE.fullmatch(raw_step):
|
|
296
|
+
errors.append(f"motion item {index} has a non-ASCII-decimal data-step")
|
|
297
|
+
continue
|
|
298
|
+
step = int(raw_step)
|
|
299
|
+
decorative = "data-motion-decorative" in item
|
|
300
|
+
if not decorative:
|
|
301
|
+
semantic_steps.append(step)
|
|
302
|
+
if not item.get("aria-label", "").strip():
|
|
303
|
+
errors.append(f"semantic motion item {index} needs a non-color aria-label")
|
|
304
|
+
elif item.get("aria-hidden") != "true" or item.get("focusable") != "false":
|
|
305
|
+
errors.append(f"decorative motion item {index} needs aria-hidden=true and focusable=false")
|
|
306
|
+
inline = item.get("style", "").replace(" ", "").lower()
|
|
307
|
+
if any(token in inline for token in ("display:none", "visibility:hidden", "opacity:0")):
|
|
308
|
+
errors.append(f"motion item {index} is hidden in source; the fallback must be visible")
|
|
309
|
+
|
|
310
|
+
expected = set(range(1, count + 1)) if count > 0 else set()
|
|
311
|
+
if set(semantic_steps) != expected:
|
|
312
|
+
errors.append(f"semantic steps must be contiguous 1..{count}; found {sorted(set(semantic_steps))}")
|
|
313
|
+
crowded = {step: n for step, n in Counter(semantic_steps).items() if n > 2}
|
|
314
|
+
if crowded:
|
|
315
|
+
errors.append(f"no more than two semantic items may share a step; found {crowded}")
|
|
316
|
+
|
|
317
|
+
if mode in {"none", "loop"} and parser.scripts:
|
|
318
|
+
errors.append(f"{mode} mode must be script-free")
|
|
319
|
+
if mode in {"none", "loop"} and (parser.controls or parser.actions or parser.statuses):
|
|
320
|
+
errors.append(f"{mode} mode must not expose playback controls or live status")
|
|
321
|
+
controlled = mode == "step" or (mode == "reveal" and bool(parser.scripts))
|
|
322
|
+
if controlled:
|
|
323
|
+
if parser.controls != 1:
|
|
324
|
+
errors.append(f"controlled mode needs one in-root control group; found {parser.controls}")
|
|
325
|
+
missing = ACTIONS - parser.actions
|
|
326
|
+
if missing:
|
|
327
|
+
errors.append(f"controlled mode is missing actions: {', '.join(sorted(missing))}")
|
|
328
|
+
if not parser.statuses:
|
|
329
|
+
errors.append("controlled mode needs data-motion-status")
|
|
330
|
+
else:
|
|
331
|
+
status = parser.statuses[0]
|
|
332
|
+
if (
|
|
333
|
+
status.get("role") != "status"
|
|
334
|
+
or status.get("aria-live") != "polite"
|
|
335
|
+
or status.get("aria-atomic") != "true"
|
|
336
|
+
):
|
|
337
|
+
errors.append("motion status needs role=status, aria-live=polite, aria-atomic=true")
|
|
338
|
+
if parser.statuses_in_controls:
|
|
339
|
+
errors.append("motion status must sit outside data-motion-controls")
|
|
340
|
+
if not parser.scripts:
|
|
341
|
+
errors.append("controlled mode needs the scoped control script")
|
|
342
|
+
|
|
343
|
+
style_source = "".join(parser.styles)
|
|
344
|
+
if parser.scripts:
|
|
345
|
+
if re.search(r"prefers-reduced-motion\s*:\s*reduce", style_source, re.IGNORECASE) is None:
|
|
346
|
+
errors.append("missing reduced-motion CSS fallback (prefers-reduced-motion)")
|
|
347
|
+
if re.search(r"@media\s+print\b", style_source, re.IGNORECASE) is None:
|
|
348
|
+
errors.append("missing print CSS fallback (@media print)")
|
|
349
|
+
if "<noscript" not in source.casefold():
|
|
350
|
+
errors.append("motion file needs a <noscript> explanation of the complete static frame")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def verify(path: Path) -> list[str]:
|
|
354
|
+
source = path.read_text(encoding="utf-8")
|
|
355
|
+
parser = parsed_document(source)
|
|
356
|
+
errors: list[str] = []
|
|
357
|
+
errors.extend(parser.unsafe)
|
|
358
|
+
for tag, rel, value in parser.references:
|
|
359
|
+
finding = reference_error(tag, rel, value)
|
|
360
|
+
if finding:
|
|
361
|
+
errors.append(finding)
|
|
362
|
+
check_svgs(parser, errors)
|
|
363
|
+
check_scripts(parser, errors)
|
|
364
|
+
check_motion(parser, source, errors)
|
|
365
|
+
return errors
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def main() -> int:
|
|
369
|
+
argument_parser = argparse.ArgumentParser(description=__doc__)
|
|
370
|
+
argument_parser.add_argument("files", nargs="+", type=Path)
|
|
371
|
+
args = argument_parser.parse_args()
|
|
372
|
+
failed = False
|
|
373
|
+
for path in args.files:
|
|
374
|
+
try:
|
|
375
|
+
errors = verify(path)
|
|
376
|
+
except (OSError, UnicodeError) as exc:
|
|
377
|
+
errors = [str(exc)]
|
|
378
|
+
if errors:
|
|
379
|
+
failed = True
|
|
380
|
+
print(f"FAIL {path}")
|
|
381
|
+
for error in errors:
|
|
382
|
+
print(f" - {error}")
|
|
383
|
+
else:
|
|
384
|
+
print(f"OK {path}")
|
|
385
|
+
return 1 if failed else 0
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
if __name__ == "__main__":
|
|
389
|
+
sys.exit(main())
|
|
@@ -152164,7 +152164,7 @@ function themeCssVars(theme) {
|
|
|
152164
152164
|
`--secondary:${c.secondary}`,
|
|
152165
152165
|
`--text:${c.text}`,
|
|
152166
152166
|
`--muted:${c.muted}`,
|
|
152167
|
-
`--card
|
|
152167
|
+
`--surface-card:${c.tableHeaderBg}`,
|
|
152168
152168
|
`--border:${c.tableBorder}`,
|
|
152169
152169
|
`--slide-bg:${theme.pptx.background}`,
|
|
152170
152170
|
`--title-color:${theme.pptx.titleColor}`,
|
|
@@ -154065,11 +154065,8 @@ img { max-width: 100%; margin: 8pt 0; }
|
|
|
154065
154065
|
.cover img.logo { max-height: 60pt; margin-bottom: 24pt; }
|
|
154066
154066
|
`.trim();
|
|
154067
154067
|
}
|
|
154068
|
-
function foldFullwidthAlnum2(s) {
|
|
154069
|
-
return s.replace(/[0-9A-Za-z]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 65248)).replace(/ /g, " ");
|
|
154070
|
-
}
|
|
154071
154068
|
function norm(s) {
|
|
154072
|
-
return escapeHtml(
|
|
154069
|
+
return escapeHtml(foldFullwidthAlnum(String(s)));
|
|
154073
154070
|
}
|
|
154074
154071
|
function paragraphHtml(text) {
|
|
154075
154072
|
return `<p>${norm(text).replace(/\r?\n/g, "<br>")}</p>`;
|
|
@@ -154254,7 +154251,7 @@ function interpolate(src, data) {
|
|
|
154254
154251
|
}
|
|
154255
154252
|
|
|
154256
154253
|
// packages/core/dist/document/html-templates.js
|
|
154257
|
-
var CARD = "background:var(--card
|
|
154254
|
+
var CARD = "background:var(--surface-card);border:1px solid var(--border);border-radius:16px";
|
|
154258
154255
|
var TEMPLATES = {
|
|
154259
154256
|
cover: {
|
|
154260
154257
|
html: `<section class="slide" style="padding:0;display:flex;flex-direction:column;justify-content:center;padding-left:88px">
|
|
@@ -146935,7 +146935,7 @@ function themeCssVars(theme) {
|
|
|
146935
146935
|
`--secondary:${c.secondary}`,
|
|
146936
146936
|
`--text:${c.text}`,
|
|
146937
146937
|
`--muted:${c.muted}`,
|
|
146938
|
-
`--card
|
|
146938
|
+
`--surface-card:${c.tableHeaderBg}`,
|
|
146939
146939
|
`--border:${c.tableBorder}`,
|
|
146940
146940
|
`--slide-bg:${theme.pptx.background}`,
|
|
146941
146941
|
`--title-color:${theme.pptx.titleColor}`,
|
|
@@ -148261,7 +148261,7 @@ async function latinFontFaceCss(doc) {
|
|
|
148261
148261
|
}
|
|
148262
148262
|
return _latinFontCss;
|
|
148263
148263
|
}
|
|
148264
|
-
async function withHeadlessPage(html, fn) {
|
|
148264
|
+
async function withHeadlessPage(html, fn, density = DOCUMENT_CAPTURE_DENSITY) {
|
|
148265
148265
|
const headless = (process.env.TALOS_HTML2PPTX_HEADLESS ?? "").trim().toLowerCase();
|
|
148266
148266
|
if (headless === "off" || headless === "none" || headless === "0" || headless === "false") return null;
|
|
148267
148267
|
let pw;
|
|
@@ -148280,7 +148280,7 @@ async function withHeadlessPage(html, fn) {
|
|
|
148280
148280
|
try {
|
|
148281
148281
|
const page = await browser2.newPage({
|
|
148282
148282
|
viewport: { width: 1280, height: 720 },
|
|
148283
|
-
deviceScaleFactor:
|
|
148283
|
+
deviceScaleFactor: density
|
|
148284
148284
|
});
|
|
148285
148285
|
await page.setContent(html, { waitUntil: "load" });
|
|
148286
148286
|
await page.evaluate("document.fonts && document.fonts.ready").catch(() => {
|
|
@@ -148290,7 +148290,7 @@ async function withHeadlessPage(html, fn) {
|
|
|
148290
148290
|
screenshot: async (clip) => {
|
|
148291
148291
|
const png = await page.screenshot({ clip });
|
|
148292
148292
|
const w = png.readUInt32BE(16), h = png.readUInt32BE(20);
|
|
148293
|
-
const want = [clip.width *
|
|
148293
|
+
const want = [clip.width * density, clip.height * density];
|
|
148294
148294
|
if (w !== want[0] || h !== want[1]) throw new Error(`captured ${w}x${h}, expected ${want[0]}x${want[1]}`);
|
|
148295
148295
|
return png;
|
|
148296
148296
|
}
|
|
@@ -148370,11 +148370,16 @@ async function main() {
|
|
|
148370
148370
|
const previewPagesIdx = args.indexOf("--preview-pages");
|
|
148371
148371
|
const previewPagesArg = previewPagesIdx >= 0 ? args[previewPagesIdx + 1] : void 0;
|
|
148372
148372
|
const previewIndexes = parsePreviewPages(previewPagesArg);
|
|
148373
|
-
const
|
|
148373
|
+
const previewDensityIdx = args.indexOf("--preview-density");
|
|
148374
|
+
const previewDensityArg = previewDensityIdx >= 0 ? args[previewDensityIdx + 1] : void 0;
|
|
148375
|
+
const previewDensity = previewDensityArg === void 0 ? DOCUMENT_CAPTURE_DENSITY : Number(previewDensityArg);
|
|
148376
|
+
const consumed = /* @__PURE__ */ new Set([themeIdx, themeIdx >= 0 ? themeIdx + 1 : -1, styleIdx, styleIdx >= 0 ? styleIdx + 1 : -1, mediaIdx, mediaIdx >= 0 ? mediaIdx + 1 : -1, bdIdx, bdIdx >= 0 ? bdIdx + 1 : -1, lineIdx, lineIdx >= 0 ? lineIdx + 1 : -1, modeIdx, modeIdx >= 0 ? modeIdx + 1 : -1, notesIdx, notesIdx >= 0 ? notesIdx + 1 : -1, previewIdx, previewIdx >= 0 ? previewIdx + 1 : -1, previewPagesIdx, previewPagesIdx >= 0 ? previewPagesIdx + 1 : -1, previewDensityIdx, previewDensityIdx >= 0 ? previewDensityIdx + 1 : -1]);
|
|
148374
148377
|
const positional = args.filter((a, i) => a !== "--rasterize" && a !== "--editable" && a !== "--theme-v2" && !consumed.has(i));
|
|
148375
148378
|
const [input, out] = positional;
|
|
148376
|
-
if (!input || !out && !previewDir) die("usage: html2pptx <in.html|in.layout.json> [out.pptx] [--theme <theme.json>] [--style <name|dir>] [--theme-v2] [--media-style <id>] [--backdrop <variant>] [--line <name|none>] [--editable] [--rasterize] [--notes-doc <out.md>] [--preview-dir <dir>] [--preview-pages <all|N,N-M>]", 64);
|
|
148379
|
+
if (!input || !out && !previewDir) die("usage: html2pptx <in.html|in.layout.json> [out.pptx] [--theme <theme.json>] [--style <name|dir>] [--theme-v2] [--media-style <id>] [--backdrop <variant>] [--line <name|none>] [--editable] [--rasterize] [--notes-doc <out.md>] [--preview-dir <dir>] [--preview-pages <all|N,N-M>] [--preview-density <1..4>]", 64);
|
|
148377
148380
|
if (previewPagesArg && !previewDir) die("--preview-pages requires --preview-dir", 64);
|
|
148381
|
+
if (previewDensityArg !== void 0 && !previewDir) die("--preview-density requires --preview-dir", 64);
|
|
148382
|
+
if (!(previewDensity > 0 && previewDensity <= 4)) die("--preview-density must be a number in (0, 4]", 64);
|
|
148378
148383
|
if (previewDir && out) die("--preview-dir is preview-only; omit the .pptx output", 64);
|
|
148379
148384
|
if (previewDir && (rasterize || forceEditable || notesDocPath)) die("--preview-dir cannot be combined with --rasterize, --editable/--mode, or --notes-doc", 64);
|
|
148380
148385
|
if (themeV2 && !styleArg) die("--theme-v2 requires --style <catalog-id>", 64);
|
|
@@ -148528,9 +148533,12 @@ async function main() {
|
|
|
148528
148533
|
const html = injectHead(rawInlined, `${fontCss ? `<style>${fontCss}</style>` : ""}${tw ? `<style>${tw}</style>` : ""}<style>${themeCss}${SKELETON_CSS}${skinCss}</style>`);
|
|
148529
148534
|
if (previewDir) {
|
|
148530
148535
|
let images;
|
|
148531
|
-
if (hasService) ({ images } = await callService("/shot", html,
|
|
148536
|
+
if (hasService) ({ images } = await callService("/shot", html, {
|
|
148537
|
+
...previewIndexes ? { pages: previewIndexes } : {},
|
|
148538
|
+
...previewDensityArg !== void 0 ? { density: previewDensity } : {}
|
|
148539
|
+
}).catch((e) => die(e.message, 1)));
|
|
148532
148540
|
else {
|
|
148533
|
-
images = await withHeadlessPage(html, (p) => rasterizeHeadless(p, previewIndexes ?? void 0)).catch((e) => die(e.message, 1));
|
|
148541
|
+
images = await withHeadlessPage(html, (p) => rasterizeHeadless(p, previewIndexes ?? void 0), previewDensity).catch((e) => die(e.message, 1));
|
|
148534
148542
|
if (images == null) die(NO_BROWSER, 69);
|
|
148535
148543
|
}
|
|
148536
148544
|
await (0, import_promises.mkdir)(previewDir, { recursive: true });
|
|
@@ -148541,7 +148549,7 @@ async function main() {
|
|
|
148541
148549
|
await (0, import_promises.writeFile)(path, Buffer.from(images[i], "base64"));
|
|
148542
148550
|
files.push(path);
|
|
148543
148551
|
}
|
|
148544
|
-
process.stdout.write(JSON.stringify({ ok: true, engine: hasService ? "preview" : "preview (headless)", slides: images.length, pages: pageNumbers, files }, null, 1) + "\n");
|
|
148552
|
+
process.stdout.write(JSON.stringify({ ok: true, engine: hasService ? "preview" : "preview (headless)", slides: images.length, pages: pageNumbers, density: previewDensity, files }, null, 1) + "\n");
|
|
148545
148553
|
return;
|
|
148546
148554
|
}
|
|
148547
148555
|
if (rasterize) {
|
|
@@ -9860,9 +9860,9 @@ function unescXml(s) {
|
|
|
9860
9860
|
}
|
|
9861
9861
|
});
|
|
9862
9862
|
}
|
|
9863
|
-
function visualWidth(
|
|
9863
|
+
function visualWidth(text3) {
|
|
9864
9864
|
let w = 0;
|
|
9865
|
-
for (const ch of
|
|
9865
|
+
for (const ch of text3) {
|
|
9866
9866
|
const cp = ch.codePointAt(0);
|
|
9867
9867
|
if (ch === " " || ch === " ")
|
|
9868
9868
|
w += 0.35;
|
|
@@ -9891,9 +9891,9 @@ function replaceTextAcrossRuns(xml, find, replacement, opts) {
|
|
|
9891
9891
|
const segs = [];
|
|
9892
9892
|
let flat = "";
|
|
9893
9893
|
for (const m of para.matchAll(textRe)) {
|
|
9894
|
-
const
|
|
9895
|
-
segs.push({ el: m[0], attrs: m[1] ?? "", text:
|
|
9896
|
-
flat +=
|
|
9894
|
+
const text3 = unescXml(m[2]);
|
|
9895
|
+
segs.push({ el: m[0], attrs: m[1] ?? "", text: text3, start: flat.length });
|
|
9896
|
+
flat += text3;
|
|
9897
9897
|
}
|
|
9898
9898
|
if (!flat.includes(find))
|
|
9899
9899
|
return para;
|
|
@@ -9936,9 +9936,36 @@ function replaceTextAcrossRuns(xml, find, replacement, opts) {
|
|
|
9936
9936
|
return { xml: rewritten, count };
|
|
9937
9937
|
}
|
|
9938
9938
|
|
|
9939
|
-
// packages/core/dist/document/pptx-
|
|
9939
|
+
// packages/core/dist/document/pptx-register-slide.js
|
|
9940
9940
|
var SLIDE_CT = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml";
|
|
9941
9941
|
var SLIDE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
|
|
9942
|
+
async function text(zip, path) {
|
|
9943
|
+
const f = zip.file(path);
|
|
9944
|
+
if (!f)
|
|
9945
|
+
throw new Error(`not a PowerPoint file (${path} missing)`);
|
|
9946
|
+
return f.async("string");
|
|
9947
|
+
}
|
|
9948
|
+
async function registerSlidePart(zip, newNum, newXmlPath) {
|
|
9949
|
+
const ctPath = "[Content_Types].xml";
|
|
9950
|
+
const ct = await text(zip, ctPath);
|
|
9951
|
+
if (!ct.includes(`PartName="/${newXmlPath}"`)) {
|
|
9952
|
+
zip.file(ctPath, ct.replace("</Types>", `<Override PartName="/${newXmlPath}" ContentType="${SLIDE_CT}"/></Types>`));
|
|
9953
|
+
}
|
|
9954
|
+
const presRelsPath = "ppt/_rels/presentation.xml.rels";
|
|
9955
|
+
const presRels = await text(zip, presRelsPath);
|
|
9956
|
+
const maxRid = Math.max(0, ...[...presRels.matchAll(/Id="rId(\d+)"/g)].map((m) => Number(m[1])));
|
|
9957
|
+
const newRid = `rId${maxRid + 1}`;
|
|
9958
|
+
zip.file(presRelsPath, presRels.replace("</Relationships>", `<Relationship Id="${newRid}" Type="${SLIDE_REL_TYPE}" Target="slides/slide${newNum}.xml"/></Relationships>`));
|
|
9959
|
+
const presPath = "ppt/presentation.xml";
|
|
9960
|
+
const pres = await text(zip, presPath);
|
|
9961
|
+
const maxSldId = Math.max(255, ...[...pres.matchAll(/<p:sldId id="(\d+)"/g)].map((m) => Number(m[1])));
|
|
9962
|
+
const updated = pres.replace("</p:sldIdLst>", `<p:sldId id="${maxSldId + 1}" r:id="${newRid}"/></p:sldIdLst>`);
|
|
9963
|
+
if (updated === pres)
|
|
9964
|
+
throw new Error("could not locate the slide list in presentation.xml");
|
|
9965
|
+
zip.file(presPath, updated);
|
|
9966
|
+
}
|
|
9967
|
+
|
|
9968
|
+
// packages/core/dist/document/pptx-clone.js
|
|
9942
9969
|
var EMU_PER_PT = 12700;
|
|
9943
9970
|
function capacityWarning(slideXml, token, value) {
|
|
9944
9971
|
const idx = slideXml.indexOf(token);
|
|
@@ -9971,7 +9998,7 @@ function capacityWarning(slideXml, token, value) {
|
|
|
9971
9998
|
return void 0;
|
|
9972
9999
|
return `${token}: replacement is ~${Math.ceil(width)} width-units, slot fits ~${Math.floor(capacity)} (${Math.round(widthPt)}\xD7${Math.round(heightPt)}pt box @ ${fontPt}pt) \u2014 likely overflow. Shorten the wording; do NOT shrink the font to make it fit.`;
|
|
9973
10000
|
}
|
|
9974
|
-
async function
|
|
10001
|
+
async function text2(zip, path) {
|
|
9975
10002
|
const f = zip.file(path);
|
|
9976
10003
|
if (!f)
|
|
9977
10004
|
throw new Error(`not a PowerPoint file (${path} missing)`);
|
|
@@ -9996,7 +10023,7 @@ async function clonePptxSlide(bytes, slideNumber, replacements = {}) {
|
|
|
9996
10023
|
const newNum = Math.max(...slideNums) + 1;
|
|
9997
10024
|
const srcXmlPath = `ppt/slides/slide${slideNumber}.xml`;
|
|
9998
10025
|
const newXmlPath = `ppt/slides/slide${newNum}.xml`;
|
|
9999
|
-
let slideXml = await
|
|
10026
|
+
let slideXml = await text2(zip, srcXmlPath);
|
|
10000
10027
|
const replaced = [];
|
|
10001
10028
|
const missing = [];
|
|
10002
10029
|
const warnings = [];
|
|
@@ -10024,23 +10051,7 @@ async function clonePptxSlide(bytes, slideNumber, replacements = {}) {
|
|
|
10024
10051
|
const rels = removeElement(await srcRels.async("string"), "Relationship", (el) => /Type="[^"]*\/notesSlide"/.test(el)).xml;
|
|
10025
10052
|
zip.file(`ppt/slides/_rels/slide${newNum}.xml.rels`, rels);
|
|
10026
10053
|
}
|
|
10027
|
-
|
|
10028
|
-
const ct = await text(zip, ctPath);
|
|
10029
|
-
if (!ct.includes(`PartName="/${newXmlPath}"`)) {
|
|
10030
|
-
zip.file(ctPath, ct.replace("</Types>", `<Override PartName="/${newXmlPath}" ContentType="${SLIDE_CT}"/></Types>`));
|
|
10031
|
-
}
|
|
10032
|
-
const presRelsPath = "ppt/_rels/presentation.xml.rels";
|
|
10033
|
-
const presRels = await text(zip, presRelsPath);
|
|
10034
|
-
const maxRid = Math.max(0, ...[...presRels.matchAll(/Id="rId(\d+)"/g)].map((m) => Number(m[1])));
|
|
10035
|
-
const newRid = `rId${maxRid + 1}`;
|
|
10036
|
-
zip.file(presRelsPath, presRels.replace("</Relationships>", `<Relationship Id="${newRid}" Type="${SLIDE_REL_TYPE}" Target="slides/slide${newNum}.xml"/></Relationships>`));
|
|
10037
|
-
const presPath = "ppt/presentation.xml";
|
|
10038
|
-
const pres = await text(zip, presPath);
|
|
10039
|
-
const maxSldId = Math.max(255, ...[...pres.matchAll(/<p:sldId id="(\d+)"/g)].map((m) => Number(m[1])));
|
|
10040
|
-
const updated = pres.replace("</p:sldIdLst>", `<p:sldId id="${maxSldId + 1}" r:id="${newRid}"/></p:sldIdLst>`);
|
|
10041
|
-
if (updated === pres)
|
|
10042
|
-
throw new Error("could not locate the slide list in presentation.xml");
|
|
10043
|
-
zip.file(presPath, updated);
|
|
10054
|
+
await registerSlidePart(zip, newNum, newXmlPath);
|
|
10044
10055
|
const out = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
|
10045
10056
|
return { bytes: new Uint8Array(out), newSlideNumber: newNum, replaced, missing, warnings };
|
|
10046
10057
|
}
|
|
@@ -10065,7 +10076,7 @@ async function removePptxSlide(bytes, slideNumber) {
|
|
|
10065
10076
|
const xmlPath = `ppt/slides/slide${slideNumber}.xml`;
|
|
10066
10077
|
const relsPath = `ppt/slides/_rels/slide${slideNumber}.xml.rels`;
|
|
10067
10078
|
const presRelsPath = "ppt/_rels/presentation.xml.rels";
|
|
10068
|
-
const presRels = await
|
|
10079
|
+
const presRels = await text2(zip, presRelsPath);
|
|
10069
10080
|
const targetRe = new RegExp(`Target="(/ppt/)?slides/slide${slideNumber}\\.xml"`);
|
|
10070
10081
|
const relMatch = findElement(presRels, "Relationship", (r) => r.includes(SLIDE_REL_TYPE) && targetRe.test(r));
|
|
10071
10082
|
if (!relMatch)
|
|
@@ -10075,19 +10086,19 @@ async function removePptxSlide(bytes, slideNumber) {
|
|
|
10075
10086
|
throw new Error(`slide ${slideNumber}'s relationship carries no Id (corrupt deck?)`);
|
|
10076
10087
|
zip.file(presRelsPath, presRels.replace(relMatch, ""));
|
|
10077
10088
|
const presPath = "ppt/presentation.xml";
|
|
10078
|
-
const pres = await
|
|
10089
|
+
const pres = await text2(zip, presPath);
|
|
10079
10090
|
const sld = removeElement(pres, "p:sldId", (el) => el.includes(`r:id="${rId}"`));
|
|
10080
10091
|
if (!sld.removed)
|
|
10081
10092
|
throw new Error(`slide ${slideNumber} (${rId}) not found in the slide list (corrupt deck?)`);
|
|
10082
10093
|
zip.file(presPath, sld.xml);
|
|
10083
|
-
const slideRels = zip.file(relsPath) ? await
|
|
10094
|
+
const slideRels = zip.file(relsPath) ? await text2(zip, relsPath) : "";
|
|
10084
10095
|
const notesTarget = /<Relationship\b[^>]*Type="[^"]*\/notesSlide"[^>]*Target="\.\.\/(notesSlides\/notesSlide\d+\.xml)"/.exec(slideRels)?.[1];
|
|
10085
10096
|
const removedParts = [xmlPath, relsPath];
|
|
10086
10097
|
if (notesTarget) {
|
|
10087
10098
|
const otherSlideRels = Object.keys(zip.files).filter((p) => /^ppt\/slides\/_rels\/slide\d+\.xml\.rels$/.test(p) && p !== relsPath);
|
|
10088
10099
|
let shared = false;
|
|
10089
10100
|
for (const p of otherSlideRels) {
|
|
10090
|
-
if ((await
|
|
10101
|
+
if ((await text2(zip, p)).includes(notesTarget)) {
|
|
10091
10102
|
shared = true;
|
|
10092
10103
|
break;
|
|
10093
10104
|
}
|
|
@@ -10095,7 +10106,7 @@ async function removePptxSlide(bytes, slideNumber) {
|
|
|
10095
10106
|
if (!shared)
|
|
10096
10107
|
removedParts.push(`ppt/${notesTarget}`, `ppt/notesSlides/_rels/${notesTarget.split("/")[1]}.rels`);
|
|
10097
10108
|
}
|
|
10098
|
-
let ct = await
|
|
10109
|
+
let ct = await text2(zip, "[Content_Types].xml");
|
|
10099
10110
|
for (const part of removedParts) {
|
|
10100
10111
|
zip.remove(part);
|
|
10101
10112
|
ct = removeElement(ct, "Override", (el) => el.includes(`PartName="/${part}"`)).xml;
|
|
@@ -9820,10 +9820,14 @@ var import_promises = require("node:fs/promises");
|
|
|
9820
9820
|
|
|
9821
9821
|
// packages/core/dist/document/pptx-fonts.js
|
|
9822
9822
|
var import_jszip = __toESM(require_lib3(), 1);
|
|
9823
|
-
|
|
9823
|
+
|
|
9824
|
+
// packages/core/dist/document/ooxml-util.js
|
|
9824
9825
|
function escXml(s) {
|
|
9825
|
-
return s.replace(
|
|
9826
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
9826
9827
|
}
|
|
9828
|
+
|
|
9829
|
+
// packages/core/dist/document/pptx-fonts.js
|
|
9830
|
+
var FONT_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font";
|
|
9827
9831
|
async function text(zip, path) {
|
|
9828
9832
|
const f = zip.file(path);
|
|
9829
9833
|
if (!f)
|