@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.
Files changed (166) hide show
  1. package/package.json +3 -3
  2. package/skills/cozy-static-svg/SKILL.md +76 -0
  3. package/skills/cozy-static-svg/references/approved-preview.png +0 -0
  4. package/skills/cozy-static-svg/references/cross-platform.md +73 -0
  5. package/skills/cozy-static-svg/references/moon_keeps_watch.svg +229 -0
  6. package/skills/cozy-static-svg/references/style-anchor.md +271 -0
  7. package/skills/cozy-static-svg/scripts/artkit.py +321 -0
  8. package/skills/cozy-static-svg/scripts/to_png.py +333 -0
  9. package/skills/diagram-design/SKILL.md +568 -0
  10. package/skills/diagram-design/assets/example-architecture-dark.html +182 -0
  11. package/skills/diagram-design/assets/example-architecture-full.html +194 -0
  12. package/skills/diagram-design/assets/example-architecture.html +183 -0
  13. package/skills/diagram-design/assets/example-bar-dark.html +129 -0
  14. package/skills/diagram-design/assets/example-bar-full.html +105 -0
  15. package/skills/diagram-design/assets/example-bar.html +129 -0
  16. package/skills/diagram-design/assets/example-data-flow-dark.html +143 -0
  17. package/skills/diagram-design/assets/example-data-flow-full.html +185 -0
  18. package/skills/diagram-design/assets/example-data-flow.html +143 -0
  19. package/skills/diagram-design/assets/example-datalake-dark.html +243 -0
  20. package/skills/diagram-design/assets/example-datalake-full.html +248 -0
  21. package/skills/diagram-design/assets/example-datalake.html +269 -0
  22. package/skills/diagram-design/assets/example-dp-integration-dark.html +80 -0
  23. package/skills/diagram-design/assets/example-dp-integration-full.html +90 -0
  24. package/skills/diagram-design/assets/example-dp-integration.html +80 -0
  25. package/skills/diagram-design/assets/example-dp-security-matrix-dark.html +31 -0
  26. package/skills/diagram-design/assets/example-dp-security-matrix-full.html +32 -0
  27. package/skills/diagram-design/assets/example-dp-security-matrix.html +88 -0
  28. package/skills/diagram-design/assets/example-er-dark.html +202 -0
  29. package/skills/diagram-design/assets/example-er-full.html +205 -0
  30. package/skills/diagram-design/assets/example-er.html +202 -0
  31. package/skills/diagram-design/assets/example-flowchart-dark.html +156 -0
  32. package/skills/diagram-design/assets/example-flowchart-full.html +159 -0
  33. package/skills/diagram-design/assets/example-flowchart.html +156 -0
  34. package/skills/diagram-design/assets/example-gantt-dark.html +137 -0
  35. package/skills/diagram-design/assets/example-gantt-full.html +127 -0
  36. package/skills/diagram-design/assets/example-gantt.html +137 -0
  37. package/skills/diagram-design/assets/example-high-level-dark.html +251 -0
  38. package/skills/diagram-design/assets/example-high-level-full.html +247 -0
  39. package/skills/diagram-design/assets/example-high-level-vertical-dark.html +274 -0
  40. package/skills/diagram-design/assets/example-high-level-vertical-full.html +252 -0
  41. package/skills/diagram-design/assets/example-high-level-vertical.html +325 -0
  42. package/skills/diagram-design/assets/example-high-level.html +301 -0
  43. package/skills/diagram-design/assets/example-import-drawio.html +185 -0
  44. package/skills/diagram-design/assets/example-import-mermaid.html +70 -0
  45. package/skills/diagram-design/assets/example-it-state-dark.html +90 -0
  46. package/skills/diagram-design/assets/example-it-state-full.html +69 -0
  47. package/skills/diagram-design/assets/example-it-state.html +128 -0
  48. package/skills/diagram-design/assets/example-layers-dark.html +123 -0
  49. package/skills/diagram-design/assets/example-layers-full.html +126 -0
  50. package/skills/diagram-design/assets/example-layers.html +123 -0
  51. package/skills/diagram-design/assets/example-line-dark.html +111 -0
  52. package/skills/diagram-design/assets/example-line-full.html +105 -0
  53. package/skills/diagram-design/assets/example-line.html +111 -0
  54. package/skills/diagram-design/assets/example-loop-dark.html +100 -0
  55. package/skills/diagram-design/assets/example-loop-full.html +137 -0
  56. package/skills/diagram-design/assets/example-loop-terminal.html +347 -0
  57. package/skills/diagram-design/assets/example-loop.html +100 -0
  58. package/skills/diagram-design/assets/example-medallion-dark.html +159 -0
  59. package/skills/diagram-design/assets/example-medallion-full.html +201 -0
  60. package/skills/diagram-design/assets/example-medallion.html +159 -0
  61. package/skills/diagram-design/assets/example-nested-dark.html +132 -0
  62. package/skills/diagram-design/assets/example-nested-full.html +135 -0
  63. package/skills/diagram-design/assets/example-nested.html +136 -0
  64. package/skills/diagram-design/assets/example-org-chart-dark.html +45 -0
  65. package/skills/diagram-design/assets/example-org-chart-full.html +45 -0
  66. package/skills/diagram-design/assets/example-org-chart.html +45 -0
  67. package/skills/diagram-design/assets/example-paved-road-animated.html +546 -0
  68. package/skills/diagram-design/assets/example-policy-trace-animated.html +490 -0
  69. package/skills/diagram-design/assets/example-process-dark.html +64 -0
  70. package/skills/diagram-design/assets/example-process-full.html +74 -0
  71. package/skills/diagram-design/assets/example-process.html +64 -0
  72. package/skills/diagram-design/assets/example-pyramid-dark.html +119 -0
  73. package/skills/diagram-design/assets/example-pyramid-full.html +122 -0
  74. package/skills/diagram-design/assets/example-pyramid.html +119 -0
  75. package/skills/diagram-design/assets/example-quadrant-consultant.html +166 -0
  76. package/skills/diagram-design/assets/example-quadrant-dark.html +135 -0
  77. package/skills/diagram-design/assets/example-quadrant-full.html +138 -0
  78. package/skills/diagram-design/assets/example-quadrant.html +135 -0
  79. package/skills/diagram-design/assets/example-queue-animated.html +577 -0
  80. package/skills/diagram-design/assets/example-radar-dark.html +139 -0
  81. package/skills/diagram-design/assets/example-radar-full.html +135 -0
  82. package/skills/diagram-design/assets/example-radar.html +139 -0
  83. package/skills/diagram-design/assets/example-scatter-dark.html +141 -0
  84. package/skills/diagram-design/assets/example-scatter-full.html +116 -0
  85. package/skills/diagram-design/assets/example-scatter.html +141 -0
  86. package/skills/diagram-design/assets/example-sequence-dark.html +222 -0
  87. package/skills/diagram-design/assets/example-sequence-full.html +388 -0
  88. package/skills/diagram-design/assets/example-sequence-oauth-dark.html +188 -0
  89. package/skills/diagram-design/assets/example-sequence-oauth-full.html +350 -0
  90. package/skills/diagram-design/assets/example-sequence-oauth.html +227 -0
  91. package/skills/diagram-design/assets/example-sequence.html +222 -0
  92. package/skills/diagram-design/assets/example-state-dark.html +147 -0
  93. package/skills/diagram-design/assets/example-state-full.html +150 -0
  94. package/skills/diagram-design/assets/example-state.html +147 -0
  95. package/skills/diagram-design/assets/example-swimlane-dark.html +172 -0
  96. package/skills/diagram-design/assets/example-swimlane-full.html +175 -0
  97. package/skills/diagram-design/assets/example-swimlane.html +172 -0
  98. package/skills/diagram-design/assets/example-timeline-dark.html +135 -0
  99. package/skills/diagram-design/assets/example-timeline-full.html +138 -0
  100. package/skills/diagram-design/assets/example-timeline.html +135 -0
  101. package/skills/diagram-design/assets/example-tree-dark.html +173 -0
  102. package/skills/diagram-design/assets/example-tree-full.html +176 -0
  103. package/skills/diagram-design/assets/example-tree.html +173 -0
  104. package/skills/diagram-design/assets/example-venn-dark.html +132 -0
  105. package/skills/diagram-design/assets/example-venn-full.html +135 -0
  106. package/skills/diagram-design/assets/example-venn.html +110 -0
  107. package/skills/diagram-design/assets/icons.html +230 -0
  108. package/skills/diagram-design/assets/index.html +369 -0
  109. package/skills/diagram-design/assets/template-dark.html +85 -0
  110. package/skills/diagram-design/assets/template-full.html +413 -0
  111. package/skills/diagram-design/assets/template-motion.html +434 -0
  112. package/skills/diagram-design/assets/template-terminal.html +173 -0
  113. package/skills/diagram-design/assets/template.html +86 -0
  114. package/skills/diagram-design/references/animation.md +141 -0
  115. package/skills/diagram-design/references/export.md +136 -0
  116. package/skills/diagram-design/references/import-drawio.md +171 -0
  117. package/skills/diagram-design/references/import-mermaid.md +126 -0
  118. package/skills/diagram-design/references/onboarding.md +297 -0
  119. package/skills/diagram-design/references/output-spec.md +181 -0
  120. package/skills/diagram-design/references/primitive-annotation.md +36 -0
  121. package/skills/diagram-design/references/primitive-icons.md +827 -0
  122. package/skills/diagram-design/references/primitive-sketchy.md +43 -0
  123. package/skills/diagram-design/references/primitive-terminal.md +76 -0
  124. package/skills/diagram-design/references/semantic-patterns.md +122 -0
  125. package/skills/diagram-design/references/style-guide.md +139 -0
  126. package/skills/diagram-design/references/type-architecture.md +78 -0
  127. package/skills/diagram-design/references/type-bar.md +48 -0
  128. package/skills/diagram-design/references/type-data-flow.md +374 -0
  129. package/skills/diagram-design/references/type-dp-integration.md +410 -0
  130. package/skills/diagram-design/references/type-dp-security-matrix.md +379 -0
  131. package/skills/diagram-design/references/type-er.md +23 -0
  132. package/skills/diagram-design/references/type-flowchart.md +23 -0
  133. package/skills/diagram-design/references/type-gantt.md +45 -0
  134. package/skills/diagram-design/references/type-high-level.md +458 -0
  135. package/skills/diagram-design/references/type-it-state.md +470 -0
  136. package/skills/diagram-design/references/type-layers.md +26 -0
  137. package/skills/diagram-design/references/type-line.md +44 -0
  138. package/skills/diagram-design/references/type-loop.md +223 -0
  139. package/skills/diagram-design/references/type-medallion.md +356 -0
  140. package/skills/diagram-design/references/type-nested.md +22 -0
  141. package/skills/diagram-design/references/type-org-chart.md +44 -0
  142. package/skills/diagram-design/references/type-process.md +495 -0
  143. package/skills/diagram-design/references/type-pyramid.md +33 -0
  144. package/skills/diagram-design/references/type-quadrant.md +81 -0
  145. package/skills/diagram-design/references/type-radar.md +80 -0
  146. package/skills/diagram-design/references/type-scatter.md +39 -0
  147. package/skills/diagram-design/references/type-sequence.md +130 -0
  148. package/skills/diagram-design/references/type-state.md +21 -0
  149. package/skills/diagram-design/references/type-swimlane.md +20 -0
  150. package/skills/diagram-design/references/type-timeline.md +20 -0
  151. package/skills/diagram-design/references/type-tree.md +24 -0
  152. package/skills/diagram-design/references/type-venn.md +26 -0
  153. package/skills/diagram-design/scripts/drawio_extract.py +856 -0
  154. package/skills/diagram-design/scripts/mermaid_extract.py +1285 -0
  155. package/skills/diagram-design/scripts/self_check.py +389 -0
  156. package/skills/document/bin/doc-gen.cjs +3 -6
  157. package/skills/document/bin/html2pptx.cjs +17 -9
  158. package/skills/document/bin/pptx-clone.cjs +41 -30
  159. package/skills/document/bin/pptx-embed-fonts.cjs +6 -2
  160. package/skills/document/bin/pptx-slide.cjs +46 -50
  161. package/skills/document/content-pipeline.md +54 -5
  162. package/skills/document/layouts/README.md +8 -0
  163. package/skills/document/layouts/blocks-2x2.tpl +19 -9
  164. package/skills/document/layouts/cards-3.tpl +1 -1
  165. package/skills/document/layouts/org-tree.tpl +1 -1
  166. 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-bg:${c.tableHeaderBg}`,
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(foldFullwidthAlnum2(String(s)));
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-bg);border:1px solid var(--border);border-radius:16px";
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-bg:${c.tableHeaderBg}`,
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: DOCUMENT_CAPTURE_DENSITY
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 * DOCUMENT_CAPTURE_DENSITY, clip.height * DOCUMENT_CAPTURE_DENSITY];
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 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]);
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, previewIndexes ? { pages: previewIndexes } : {}).catch((e) => die(e.message, 1)));
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(text2) {
9863
+ function visualWidth(text3) {
9864
9864
  let w = 0;
9865
- for (const ch of text2) {
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 text2 = unescXml(m[2]);
9895
- segs.push({ el: m[0], attrs: m[1] ?? "", text: text2, start: flat.length });
9896
- flat += text2;
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-clone.js
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 text(zip, path) {
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 text(zip, srcXmlPath);
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
- const ctPath = "[Content_Types].xml";
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 text(zip, presRelsPath);
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 text(zip, presPath);
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 text(zip, relsPath) : "";
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 text(zip, p)).includes(notesTarget)) {
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 text(zip, "[Content_Types].xml");
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
- var FONT_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font";
9823
+
9824
+ // packages/core/dist/document/ooxml-util.js
9824
9825
  function escXml(s) {
9825
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
9826
+ return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" })[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)