slidemind 0.3.19 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +591 -15
  4. package/README.md +202 -7
  5. package/bin/slidemind.mjs +50 -2
  6. package/package.json +2 -2
  7. package/skills/create-slides/SKILL.md +484 -886
  8. package/skills/search/SKILL.md +46 -23
  9. package/src/bridge/build-and-push.mjs +548 -73
  10. package/src/bridge/bundle.mjs +100 -31
  11. package/src/bridge/capture.mjs +14 -17
  12. package/src/bridge/client/brands.mjs +157 -18
  13. package/src/bridge/client/engine-routing.mjs +48 -13
  14. package/src/bridge/client/engine-search.mjs +24 -1
  15. package/src/bridge/client/engine-slides.mjs +1 -1
  16. package/src/bridge/client/render.mjs +27 -1
  17. package/src/bridge/client/transport.mjs +499 -23
  18. package/src/bridge/client.mjs +10 -0
  19. package/src/bridge/config.mjs +67 -6
  20. package/src/bridge/engine-download.mjs +0 -10
  21. package/src/bridge/graph-auth.mjs +55 -3
  22. package/src/bridge/image-intent-envelope.mjs +239 -0
  23. package/src/bridge/package-preview.mjs +67 -0
  24. package/src/bridge/sharepoint.mjs +914 -0
  25. package/src/builders/asset-fetch.mjs +203 -0
  26. package/src/builders/block-dividers.mjs +83 -0
  27. package/src/builders/build.mjs +171 -87
  28. package/src/builders/card-height.mjs +294 -0
  29. package/src/builders/chart-hole.mjs +183 -0
  30. package/src/builders/chart-jsx.mjs +410 -0
  31. package/src/builders/chart-labels.mjs +326 -0
  32. package/src/builders/concept-geometry.mjs +370 -0
  33. package/src/builders/concept-intent.mjs +200 -0
  34. package/src/builders/concept-layout.mjs +79 -0
  35. package/src/builders/concept-lines.mjs +509 -0
  36. package/src/builders/concept-lists.mjs +270 -0
  37. package/src/builders/concept-marks.mjs +84 -0
  38. package/src/builders/concept-relationships.mjs +12 -0
  39. package/src/builders/concept-spec.mjs +645 -0
  40. package/src/builders/concept-text.mjs +140 -0
  41. package/src/builders/concept-waves.mjs +57 -0
  42. package/src/builders/copy-fit.mjs +139 -0
  43. package/src/builders/design-tokens.mjs +347 -0
  44. package/src/builders/draft-check.mjs +552 -340
  45. package/src/builders/draft-lines.mjs +369 -0
  46. package/src/builders/edit-route.mjs +29 -0
  47. package/src/builders/figure-sketch.mjs +375 -0
  48. package/src/builders/fold-sets.mjs +639 -0
  49. package/src/builders/gate-server.mjs +9 -4
  50. package/src/builders/gate-session.js +273 -12
  51. package/src/builders/gate-shell.js +1494 -0
  52. package/src/builders/image-intent-check.mjs +308 -0
  53. package/src/builders/image-use-reconcile.mjs +107 -0
  54. package/src/builders/kit-helpers.mjs +121 -0
  55. package/src/builders/layout-names.mjs +32 -0
  56. package/src/builders/legacy-reference-uses.mjs +179 -0
  57. package/src/builders/line-fit.mjs +224 -0
  58. package/src/builders/line-native.mjs +895 -0
  59. package/src/builders/line-relations.mjs +230 -0
  60. package/src/builders/lint-fix.mjs +117 -9
  61. package/src/builders/lint.mjs +25 -20
  62. package/src/builders/list-jsx.mjs +88 -0
  63. package/src/builders/placement.mjs +133 -0
  64. package/src/builders/plan-checks.mjs +135 -0
  65. package/src/builders/plan-comments.mjs +19 -0
  66. package/src/builders/plan-handlers.mjs +95 -0
  67. package/src/builders/plan-markdown-edit.mjs +3 -3
  68. package/src/builders/plan-overlay.js +1530 -1608
  69. package/src/builders/plan.mjs +689 -186
  70. package/src/builders/preset-definitions.json +1 -0
  71. package/src/builders/preset-eval.mjs +244 -0
  72. package/src/builders/preview.html +51 -120
  73. package/src/builders/red-thread-backbone.mjs +135 -36
  74. package/src/builders/repair.mjs +205 -0
  75. package/src/builders/repeat-groups.mjs +540 -0
  76. package/src/builders/resolve-skill.mjs +5 -4
  77. package/src/builders/review-overlay.js +296 -293
  78. package/src/builders/review.mjs +305 -84
  79. package/src/builders/rule-shift.mjs +96 -0
  80. package/src/builders/seat.mjs +354 -0
  81. package/src/builders/settle-driver.mjs +236 -0
  82. package/src/builders/settle.mjs +1448 -0
  83. package/src/builders/slide-prep.mjs +67 -0
  84. package/src/builders/spec-assets.mjs +236 -0
  85. package/src/builders/spec-match.mjs +56 -0
  86. package/src/builders/stack.mjs +413 -0
  87. package/src/builders/storyboard-markdown.mjs +70 -27
  88. package/src/builders/storyboard-schema.mjs +54 -114
  89. package/src/builders/text-blocks.mjs +212 -0
  90. package/src/builders/text-card-fit.mjs +552 -0
  91. package/src/builders/text-stack.mjs +276 -0
  92. package/src/builders/transcribe.mjs +1069 -0
  93. package/src/cli/assets.mjs +164 -0
  94. package/src/cli/common.mjs +173 -12
  95. package/src/cli/concept-spec-support.mjs +76 -0
  96. package/src/cli/concepts.mjs +1002 -103
  97. package/src/cli/deck.mjs +401 -65
  98. package/src/cli/diagnose.mjs +4 -2
  99. package/src/cli/engine-helpers.mjs +17 -10
  100. package/src/cli/engine-results.mjs +76 -3
  101. package/src/cli/help.mjs +232 -28
  102. package/src/cli/install.mjs +9 -0
  103. package/src/cli/maintenance.mjs +104 -0
  104. package/src/cli/read.mjs +2 -1
  105. package/src/cli/ready.mjs +6 -3
  106. package/src/cli/record.mjs +522 -0
  107. package/src/cli/remote.mjs +818 -0
  108. package/src/cli/revision.mjs +686 -0
  109. package/src/cli/search.mjs +30 -11
  110. package/src/cli/session.mjs +56 -13
  111. package/src/cli/setup.mjs +46 -2
  112. package/src/cli/status.mjs +142 -33
  113. package/src/cli/tasks.mjs +246 -0
  114. package/src/cli/transcribe.mjs +854 -0
  115. package/src/cli/workspace-ui.mjs +260 -0
  116. package/src/cli/workspace.mjs +212 -34
  117. package/src/install-channel.mjs +64 -7
  118. package/src/lib/adopt.mjs +346 -25
  119. package/src/lib/concept-spec-binding.mjs +96 -0
  120. package/src/lib/conversions.mjs +70 -6
  121. package/src/lib/convert-folded.mjs +107 -0
  122. package/src/lib/deck-paths.mjs +418 -0
  123. package/src/lib/durable-ids.mjs +47 -0
  124. package/src/lib/edit-operations.mjs +316 -0
  125. package/src/lib/edit-state.mjs +1244 -0
  126. package/src/lib/edit-transitions.mjs +120 -0
  127. package/src/lib/extensions.mjs +501 -0
  128. package/src/lib/image-assets.mjs +465 -0
  129. package/src/lib/image-derivatives.mjs +234 -0
  130. package/src/lib/image-intent-fingerprint.mjs +70 -0
  131. package/src/lib/image-intent-tags.mjs +176 -0
  132. package/src/lib/image-intent.mjs +802 -0
  133. package/src/lib/image-use-overrides.mjs +279 -0
  134. package/src/lib/inline-components.mjs +613 -0
  135. package/src/lib/lint-converted.mjs +68 -4
  136. package/src/lib/merge-json.mjs +163 -0
  137. package/src/lib/overwrite-guard.mjs +3 -3
  138. package/src/lib/presentation-context.mjs +531 -0
  139. package/src/lib/preservation-tags.mjs +157 -0
  140. package/src/lib/preservation-verdict.mjs +1342 -0
  141. package/src/lib/publish.mjs +1557 -0
  142. package/src/lib/rebase.mjs +945 -0
  143. package/src/lib/reconcile-ack.mjs +4 -6
  144. package/src/lib/reconcile.mjs +62 -24
  145. package/src/lib/record-client.mjs +175 -0
  146. package/src/lib/record-state.mjs +151 -0
  147. package/src/lib/remote.mjs +661 -0
  148. package/src/lib/revision.mjs +1210 -0
  149. package/src/lib/slide-guids.mjs +4 -6
  150. package/src/lib/slide-preservation.mjs +423 -0
  151. package/src/lib/slide-selection.mjs +9 -0
  152. package/src/lib/workspace-transaction.mjs +117 -0
  153. package/src/pptx/content-digest.mjs +334 -0
  154. package/src/pptx/deck-assemble.mjs +1278 -0
  155. package/src/pptx/fill/engine.mjs +2 -3
  156. package/src/pptx/fill/freeform.mjs +6 -3
  157. package/src/pptx/ooxml.mjs +20 -0
  158. package/src/pptx/package-evidence.mjs +383 -0
  159. package/src/pptx/tags.mjs +163 -11
  160. package/src/update-check.mjs +22 -4
  161. package/src/workspace/adapters.mjs +222 -0
  162. package/src/workspace/auth.mjs +256 -0
  163. package/src/workspace/autostart.mjs +204 -0
  164. package/src/workspace/daemon.mjs +360 -0
  165. package/src/workspace/discovery.mjs +180 -0
  166. package/src/workspace/edit.mjs +167 -0
  167. package/src/workspace/events.mjs +52 -0
  168. package/src/workspace/gate-client.mjs +290 -0
  169. package/src/workspace/handback.mjs +73 -0
  170. package/src/workspace/lifecycle.mjs +635 -0
  171. package/src/workspace/operations.mjs +200 -0
  172. package/src/workspace/pick-folder.mjs +152 -0
  173. package/src/workspace/registry.mjs +359 -0
  174. package/src/workspace/remote-links.mjs +397 -0
  175. package/src/workspace/remote-status.mjs +359 -0
  176. package/src/workspace/routes.mjs +1331 -0
  177. package/src/workspace/server.mjs +159 -0
  178. package/src/workspace/sync.mjs +1119 -0
  179. package/src/workspace/task-actions.mjs +215 -0
  180. package/src/workspace/tasks.mjs +507 -0
  181. package/src/workspace/ui/app.js +1886 -0
  182. package/src/workspace/ui/index.html +141 -0
  183. package/src/workspace/ui/plan-bridge.js +120 -0
  184. package/src/workspace/ui/sharepoint.svg +59 -0
  185. package/src/workspace/ui/styles.css +423 -0
  186. package/src/builders/visual-families.mjs +0 -46
@@ -0,0 +1,334 @@
1
+ // What a person changed in a package — read so that a save nobody edited in
2
+ // reads as no change at all.
3
+ //
4
+ // A three-way rebase has to answer "did the other side change this slide?" for
5
+ // every slide of a file PowerPoint just saved. Comparing bytes cannot answer it:
6
+ // one PowerPoint Online save of a one-slide deck rewrote 86 of its 98 parts
7
+ // (decks/sp-probe, 2026-09-16) — every layout and master re-serialized, the
8
+ // slide-number placeholder relocalized, every image and tag part renumbered,
9
+ // every relationship id reshuffled, SharePoint metadata added. None of that is
10
+ // an edit, and a byte comparison calls all of it one.
11
+ //
12
+ // So this module digests MEANING, never bytes or part names:
13
+ //
14
+ // slide the structural projection the manual-edit reconcile already relies
15
+ // on (`computeProjection`: geometry, text runs, fill, line, media
16
+ // content), plus what the projection does not carry — each shape's
17
+ // STYLE (shape type, scheme and gradient colours, line style, effects,
18
+ // text box anchoring, paragraph alignment / bullets / spacing / level,
19
+ // run font face and decoration), speaker notes, chart data (the
20
+ // residue, not the chart XML), table grids and cell styling, embedded
21
+ // payload bytes, comments, hyperlinks, the layout it uses, background,
22
+ // hidden flag, transition and animation.
23
+ // design the deck's shared design: theme colours and fonts, every master's
24
+ // and layout's name and projection, the slide size.
25
+ //
26
+ // Everything is keyed by content or by name, never by part path, so a
27
+ // renumbered `image7.png` is the same image it was as `image3.png`. And
28
+ // everything is an attribute PowerPoint PRESERVES: what it recomputes on a save
29
+ // (autofit scale, language and proofing flags, an empty paragraph's end-of-run
30
+ // properties, field text) is left out on purpose.
31
+ //
32
+ // What it does NOT see, stated so nobody relies on it: 3D and the finer detail
33
+ // of shadows and glows (their presence is seen, their parameters are not), a
34
+ // list style defined inside one shape, and anything in a part no slide reaches.
35
+
36
+ import { createHash } from "node:crypto";
37
+
38
+ import { decodeXmlEntities, relsFor, resolvePartPath, slideOrderPaths } from "./ooxml.mjs";
39
+ import { chartResidue } from "./package-evidence.mjs";
40
+ import { computeProjection, topLevelShapes, withoutChartHashes } from "./tags.mjs";
41
+ import { readText } from "./zip.mjs";
42
+
43
+ const sha256 = (data) => createHash("sha256").update(data).digest("hex");
44
+ const digestOf = (value) => sha256(JSON.stringify(value));
45
+ const dirOf = (part) => part.slice(0, part.lastIndexOf("/"));
46
+ const relType = (rel) => String(rel.type ?? "").slice(String(rel.type ?? "").lastIndexOf("/") + 1);
47
+
48
+ // Runs as PowerPoint may re-serialize them: an empty run dropped, two adjacent
49
+ // runs with identical formatting merged. Neither is a visible change.
50
+ function normalizeRuns(runs) {
51
+ if (!runs) return null;
52
+ const out = [];
53
+ for (const run of runs) {
54
+ if (!run.t) continue;
55
+ const prev = out[out.length - 1];
56
+ if (prev && prev.b === run.b && prev.i === run.i && prev.sz === run.sz && prev.color === run.color) {
57
+ prev.t += run.t;
58
+ } else {
59
+ out.push({ ...run });
60
+ }
61
+ }
62
+ return out.length ? out : null;
63
+ }
64
+
65
+ // `prov` is slidemind's own bookkeeping (which JSX node drew the shape): no
66
+ // person edits it, and a host is free to drop the tag that carries it.
67
+ function projectionOf(files, part) {
68
+ const projection = withoutChartHashes(computeProjection(files, part));
69
+ return (projection?.shapes ?? []).map(({ prov: _bookkeeping, ...shape }) => ({
70
+ ...shape,
71
+ text: normalizeRuns(shape.text),
72
+ }));
73
+ }
74
+
75
+ // The text a part shows, paragraph by paragraph. Fields (`a:fld` — slide
76
+ // numbers, dates) are left out on purpose: the host recomputes them on every
77
+ // save and every reorder.
78
+ function textOf(xml) {
79
+ const paragraphs = [];
80
+ for (const p of String(xml ?? "").matchAll(/<a:p>([\s\S]*?)<\/a:p>/g)) {
81
+ const text = [...p[1].matchAll(/<a:r>[\s\S]*?<a:t>([\s\S]*?)<\/a:t>[\s\S]*?<\/a:r>/g)]
82
+ .map((m) => decodeXmlEntities(m[1]))
83
+ .join("");
84
+ if (text) paragraphs.push(text);
85
+ }
86
+ return paragraphs;
87
+ }
88
+
89
+ // Every literal string a part carries — for parts whose structure is the host's
90
+ // business (a SmartArt data model, a comment) but whose words are the author's.
91
+ const stringsOf = (xml) =>
92
+ [...String(xml ?? "").matchAll(/<(?:a:t|p:text|cx:v|cx:pt\b[^>]*)>([^<]*)</g)]
93
+ .map((m) => decodeXmlEntities(m[1]).trim())
94
+ .filter(Boolean);
95
+
96
+ function background(files, part, xml) {
97
+ const bg = /<p:bg>([\s\S]*?)<\/p:bg>/.exec(xml)?.[1];
98
+ if (!bg) return null;
99
+ const rels = new Map(relsFor(files, part).map((r) => [r.id, r]));
100
+ return {
101
+ colors: [...bg.matchAll(/\bval="([^"]+)"/g)].map((m) => m[1].toUpperCase()),
102
+ media: [...bg.matchAll(/\br:embed="([^"]+)"/g)].map((m) => {
103
+ const rel = rels.get(m[1]);
104
+ const bytes = rel ? files[resolvePartPath(dirOf(part), rel.target)] : undefined;
105
+ return bytes ? sha256(bytes) : null;
106
+ }),
107
+ };
108
+ }
109
+
110
+ // The effect names a transition uses. PowerPoint wraps a transition in
111
+ // `mc:AlternateContent` with a fallback copy on save, so the names are a set.
112
+ function transition(xml) {
113
+ const names = new Set();
114
+ for (const block of String(xml).matchAll(/<p:transition\b[^>]*?(?:\/>|>([\s\S]*?)<\/p:transition>)/g)) {
115
+ names.add("transition");
116
+ for (const m of String(block[1] ?? "").matchAll(/<(p14|p15|p):(\w+)\b/g)) names.add(m[2]);
117
+ }
118
+ return names.size ? [...names].sort() : null;
119
+ }
120
+
121
+ // Each animation as the preset it applies and the shape it applies to. Time
122
+ // nodes NEST (the root holds the sequence holds every effect), so an effect is
123
+ // read from its own start tag up to the next effect's — a lazy element match
124
+ // would stop at the innermost closing tag and never see one.
125
+ function animation(xml) {
126
+ const timing = /<p:timing\b[^>]*>([\s\S]*?)<\/p:timing>/.exec(xml)?.[1];
127
+ if (!timing) return null;
128
+ const starts = [...timing.matchAll(/<p:cTn\b([^>]*)>/g)].filter((m) => /\bpresetClass="/.test(m[1]));
129
+ const effects = starts.map((m, k) => {
130
+ const scope = timing.slice(m.index, starts[k + 1]?.index ?? timing.length);
131
+ return {
132
+ cls: /\bpresetClass="([^"]+)"/.exec(m[1])[1],
133
+ id: /\bpresetID="([^"]+)"/.exec(m[1])?.[1] ?? null,
134
+ sub: /\bpresetSubtype="([^"]+)"/.exec(m[1])?.[1] ?? null,
135
+ spid: /<p:spTgt\b[^>]*\bspid="([^"]+)"/.exec(scope)?.[1] ?? null,
136
+ };
137
+ });
138
+ return effects.length ? effects : null;
139
+ }
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // style — what the projection leaves out of a shape
143
+ // ---------------------------------------------------------------------------
144
+
145
+ const attrsOf = (tag, names) => {
146
+ const out = {};
147
+ for (const name of names) {
148
+ const value = new RegExp(`\\s${name}="([^"]*)"`).exec(tag ?? "")?.[1];
149
+ if (value !== undefined) out[name] = value;
150
+ }
151
+ return out;
152
+ };
153
+ const firstTag = (xml, name) => new RegExp(`<a:${name}\\b[^>]*>`).exec(xml ?? "")?.[0] ?? null;
154
+ const block = (xml, name) => new RegExp(`<a:${name}\\b[^>]*?(?:\\/>|>[\\s\\S]*?<\\/a:${name}>)`).exec(xml ?? "")?.[0] ?? null;
155
+
156
+ // Every colour a block names, with its modifiers, in order: a scheme colour
157
+ // (accent1 → accent2) is as much an edit as an RGB one, and the projection
158
+ // reads only a literal RGB fill. A system colour's `lastClr` is a cached
159
+ // value the host refreshes, so only its name counts.
160
+ const colorsIn = (xml) =>
161
+ [...String(xml ?? "").matchAll(/<a:(srgbClr|schemeClr|prstClr|sysClr|lumMod|lumOff|alpha|tint|shade|satMod)\b[^>]*\bval="([^"]*)"/g)].map(
162
+ (m) => `${m[1]}:${m[2]}`,
163
+ );
164
+
165
+ function paragraphStyle(p) {
166
+ const pPr = block(p, "pPr") ?? "";
167
+ const bullet = [...pPr.matchAll(/<a:(bu\w+)\b([^>]*)/g)].map((m) => [m[1], attrsOf(m[2], ["char", "type", "typeface", "startAt"])]);
168
+ const spacing = ["lnSpc", "spcBef", "spcAft"].map((name) => {
169
+ const b = block(pPr, name);
170
+ return b ? `${name}:${/<a:(spcPct|spcPts)\b[^>]*\bval="([^"]*)"/.exec(b)?.slice(1).join("=") ?? ""}` : null;
171
+ });
172
+ const runs = [];
173
+ for (const r of p.matchAll(/<a:r>([\s\S]*?)<\/a:r>/g)) {
174
+ if (!/<a:t>[^<]/.test(r[1])) continue;
175
+ const rPr = block(r[1], "rPr") ?? "";
176
+ const style = JSON.stringify({
177
+ ...attrsOf(firstTag(rPr, "rPr"), ["u", "strike", "baseline", "cap", "spc"]),
178
+ fonts: [...rPr.matchAll(/<a:(latin|ea|cs|sym)\b[^>]*\btypeface="([^"]*)"/g)].map((m) => `${m[1]}:${m[2]}`),
179
+ colors: colorsIn(rPr),
180
+ });
181
+ if (runs[runs.length - 1] !== style) runs.push(style);
182
+ }
183
+ return {
184
+ ...attrsOf(firstTag(pPr, "pPr"), ["algn", "lvl", "marL", "indent", "rtl"]),
185
+ bullet,
186
+ spacing: spacing.filter(Boolean),
187
+ runs,
188
+ };
189
+ }
190
+
191
+ /** One shape's style, from the attributes a save keeps. A group is read whole. */
192
+ function shapeStyle(unitXml) {
193
+ const xml = String(unitXml ?? "");
194
+ const tbl = block(xml, "tbl");
195
+ const fills = [...xml.matchAll(/<p:spPr\b[^>]*?(?:\/>|>([\s\S]*?)<\/p:spPr>)/g)].map((m) => {
196
+ const spPr = m[1] ?? "";
197
+ const ln = block(spPr, "ln");
198
+ return {
199
+ colors: colorsIn(ln ? spPr.replace(ln, "") : spPr),
200
+ gradient: [...(block(spPr, "gradFill") ?? "").matchAll(/<a:gs\b[^>]*\bpos="([^"]*)"/g)].map((g) => g[1]),
201
+ pattern: /<a:pattFill\b[^>]*\bprst="([^"]*)"/.exec(spPr)?.[1] ?? null,
202
+ line: ln
203
+ ? {
204
+ ...attrsOf(firstTag(ln, "ln"), ["w", "cap", "cmpd"]),
205
+ dash: /<a:prstDash\b[^>]*\bval="([^"]*)"/.exec(ln)?.[1] ?? null,
206
+ ends: [...ln.matchAll(/<a:(headEnd|tailEnd)\b[^>]*\btype="([^"]*)"/g)].map((e) => `${e[1]}:${e[2]}`),
207
+ colors: colorsIn(ln),
208
+ }
209
+ : null,
210
+ effects: [...(block(spPr, "effectLst") ?? "").matchAll(/<a:(\w+)\b/g)].map((e) => e[1]).filter((n) => n !== "effectLst"),
211
+ };
212
+ });
213
+ return {
214
+ shapes: [...xml.matchAll(/<a:prstGeom\b[^>]*\bprst="([^"]*)"/g)].map((m) => m[1]),
215
+ path: [...(block(xml, "custGeom") ?? "").matchAll(/<a:pt\b([^>]*)\/>/g)].map((m) => Object.values(attrsOf(m[1], ["x", "y"])).join(",")),
216
+ fills,
217
+ crop: [...xml.matchAll(/<a:srcRect\b([^>]*)\/>/g)].map((m) => attrsOf(m[1], ["l", "t", "r", "b"])),
218
+ body: [...xml.matchAll(/<a:bodyPr\b[^>]*>/g)].map((m) =>
219
+ attrsOf(m[0], ["anchor", "anchorCtr", "vert", "wrap", "lIns", "tIns", "rIns", "bIns", "numCol"]),
220
+ ),
221
+ paragraphs: [...xml.matchAll(/<a:p(?:\/>|>([\s\S]*?)<\/a:p>)/g)].map((m) => paragraphStyle(m[1] ?? "")),
222
+ table: tbl
223
+ ? {
224
+ ...attrsOf(firstTag(tbl, "tblPr"), ["firstRow", "firstCol", "lastRow", "lastCol", "bandRow", "bandCol"]),
225
+ styleId: /<a:tableStyleId>([^<]*)</.exec(tbl)?.[1] ?? null,
226
+ cells: [...tbl.matchAll(/<a:tcPr\b[^>]*?(?:\/>|>([\s\S]*?)<\/a:tcPr>)/g)].map((m) => colorsIn(m[1]).join(" ")),
227
+ }
228
+ : null,
229
+ };
230
+ }
231
+
232
+ function styleOf(xml) {
233
+ const spTree = /<p:spTree>([\s\S]*?)<\/p:spTree>/.exec(xml)?.[1] ?? "";
234
+ return topLevelShapes(spTree).map((unit) => shapeStyle(unit.xml));
235
+ }
236
+
237
+ /** The name a layout or master carries — its identity for anything that is not a path. */
238
+ function cSldName(files, part) {
239
+ const xml = readText(files, part) ?? "";
240
+ return decodeXmlEntities(/<p:cSld\b[^>]*\bname="([^"]*)"/.exec(xml)?.[1] ?? "");
241
+ }
242
+
243
+ /**
244
+ * One slide's content digest: equal across any save that did not change what
245
+ * the slide shows, says or does.
246
+ */
247
+ export function slideContentDigest(files, part) {
248
+ const xml = readText(files, part) ?? "";
249
+ const dir = dirOf(part);
250
+ const related = [];
251
+ let layout = null;
252
+ let notes = null;
253
+ for (const rel of relsFor(files, part)) {
254
+ const type = relType(rel);
255
+ if (rel.targetMode === "External") {
256
+ if (type === "hyperlink") related.push(`link:${rel.target}`);
257
+ continue;
258
+ }
259
+ const target = resolvePartPath(dir, rel.target);
260
+ if (files[target] === undefined) continue;
261
+ if (type === "slideLayout") layout = cSldName(files, target);
262
+ else if (type === "notesSlide") notes = textOf(readText(files, target));
263
+ else if (type === "chart") related.push(`chart:${digestOf(chartResidue(readText(files, target)))}`);
264
+ else if (type === "chartEx" || type === "diagramData" || type === "comments") {
265
+ related.push(`${type}:${digestOf(stringsOf(readText(files, target)))}`);
266
+ } else if (/^(tags|slide|vmlDrawing|diagramLayout|diagramColors|diagramQuickStyle|diagramDrawing)$/.test(type)) {
267
+ // Tags are slidemind's own bookkeeping (provenance rides the projection);
268
+ // a slide link points at a part path; a VML drawing and the diagram parts
269
+ // besides its data are the host's rendering cache. None of them is
270
+ // something a person edited.
271
+ continue;
272
+ } else if (!target.endsWith(".xml")) {
273
+ related.push(`bytes:${sha256(files[target])}`);
274
+ } else {
275
+ // An XML payload this module has no reader for (a modern comment, an
276
+ // ink part): its words, never its serialization.
277
+ related.push(`${type}:${digestOf(stringsOf(readText(files, target)))}`);
278
+ }
279
+ }
280
+ return digestOf({
281
+ shapes: projectionOf(files, part),
282
+ style: styleOf(xml),
283
+ layout,
284
+ notes,
285
+ related: related.sort(),
286
+ background: background(files, part, xml),
287
+ hidden: /<p:sld\b[^>]*\bshow="0"/.test(xml),
288
+ transition: transition(xml),
289
+ animation: animation(xml),
290
+ });
291
+ }
292
+
293
+ /** Every slide's content digest, keyed by slide part. */
294
+ export function slideContentDigests(files) {
295
+ return new Map(slideOrderPaths(files).map((part) => [part, slideContentDigest(files, part)]));
296
+ }
297
+
298
+ /**
299
+ * The deck's shared design as one digest: theme colours and fonts, every
300
+ * master's and layout's name and content, and the slide size. Equal across a
301
+ * save that re-serialized all of them without changing one.
302
+ */
303
+ export function designDigest(files) {
304
+ const themes = [];
305
+ const layouts = [];
306
+ for (const part of Object.keys(files).sort()) {
307
+ if (/^ppt\/theme\/theme\d+\.xml$/.test(part)) {
308
+ const xml = readText(files, part) ?? "";
309
+ const colors = /<a:clrScheme\b[\s\S]*?<\/a:clrScheme>/.exec(xml)?.[0] ?? "";
310
+ const fonts = /<a:fontScheme\b[\s\S]*?<\/a:fontScheme>/.exec(xml)?.[0] ?? "";
311
+ themes.push(
312
+ digestOf({
313
+ colors: [...colors.matchAll(/\b(?:val|lastClr)="([^"]+)"/g)].map((m) => m[1].toUpperCase()),
314
+ fonts: [...fonts.matchAll(/<a:latin\b[^>]*\btypeface="([^"]*)"/g)].map((m) => m[1]),
315
+ }),
316
+ );
317
+ } else if (/^ppt\/(slideMasters\/slideMaster|slideLayouts\/slideLayout)\d+\.xml$/.test(part)) {
318
+ layouts.push(
319
+ digestOf({
320
+ kind: part.includes("slideMasters") ? "master" : "layout",
321
+ name: cSldName(files, part),
322
+ shapes: projectionOf(files, part),
323
+ background: background(files, part, readText(files, part) ?? ""),
324
+ }),
325
+ );
326
+ }
327
+ }
328
+ const size = /<p:sldSz\b[^>]*\/>/.exec(readText(files, "ppt/presentation.xml") ?? "")?.[0] ?? "";
329
+ return digestOf({
330
+ themes: themes.sort(),
331
+ layouts: layouts.sort(),
332
+ size: [/\bcx="(\d+)"/.exec(size)?.[1] ?? null, /\bcy="(\d+)"/.exec(size)?.[1] ?? null],
333
+ });
334
+ }