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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +591 -15
- package/README.md +202 -7
- package/bin/slidemind.mjs +50 -2
- package/package.json +2 -2
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/engine-slides.mjs +1 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- 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
|
+
}
|