slidemind 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +65 -15
- package/README.md +36 -2
- package/bin/slidemind.mjs +1609 -225
- package/install.ps1 +70 -0
- package/install.sh +65 -0
- package/package.json +3 -1
- package/skills/create-slides/SKILL.md +47 -19
- package/skills/search/SKILL.md +26 -18
- package/src/bridge/build-and-push.mjs +431 -38
- package/src/bridge/capture.mjs +12 -1
- package/src/bridge/client.mjs +1214 -107
- package/src/bridge/config.mjs +193 -1
- package/src/bridge/engine-download.mjs +61 -8
- package/src/bridge/graph-auth.mjs +859 -0
- package/src/bridge/http-json.mjs +15 -6
- package/src/bridge/secrets.mjs +75 -0
- package/src/bridge/setup-claim.mjs +6 -0
- package/src/builders/build.mjs +3 -1
- package/src/builders/gate-server.mjs +156 -0
- package/src/builders/gate-session.js +355 -0
- package/src/builders/plan-overlay.js +507 -225
- package/src/builders/plan.mjs +169 -98
- package/src/builders/resolve-skill.mjs +64 -2
- package/src/builders/review-overlay.js +390 -302
- package/src/builders/review.mjs +136 -50
- package/src/lib/adopt.mjs +407 -0
- package/src/lib/conversions.mjs +263 -0
- package/src/lib/overwrite-guard.mjs +91 -0
- package/src/lib/reconcile-ack.mjs +46 -0
- package/src/lib/reconcile.mjs +306 -0
- package/src/lib/slide-guids.mjs +143 -0
- package/src/pptx/fill/engine.mjs +181 -135
- package/src/pptx/hidden-ole.mjs +22 -1
- package/src/pptx/slide-swap.mjs +23 -1
- package/src/pptx/tags.mjs +676 -0
package/src/pptx/fill/engine.mjs
CHANGED
|
@@ -17,6 +17,18 @@ import path from "node:path";
|
|
|
17
17
|
|
|
18
18
|
import { unzip, zip, readText } from "../zip.mjs";
|
|
19
19
|
import { slideOrderPaths, relsPathFor, resolvePartPath } from "../ooxml.mjs";
|
|
20
|
+
import {
|
|
21
|
+
bakeSlideSourceTags,
|
|
22
|
+
encodeSourceTags,
|
|
23
|
+
mintSlideGuid,
|
|
24
|
+
referencedAssets,
|
|
25
|
+
} from "../tags.mjs";
|
|
26
|
+
import {
|
|
27
|
+
reconcileSlideGuids,
|
|
28
|
+
saveSlideGuids,
|
|
29
|
+
slideGuidEntry,
|
|
30
|
+
} from "../../lib/slide-guids.mjs";
|
|
31
|
+
import { readDeckBundleRef } from "../../bridge/bundle.mjs";
|
|
20
32
|
import { parseSlideFile, layoutNumber } from "./parse-slides.mjs";
|
|
21
33
|
import {
|
|
22
34
|
setShapeText, setParagraphText, setShapeFill, deleteShapes, cloneShape,
|
|
@@ -35,69 +47,11 @@ export async function buildFillTemplateDeck({ deckDir, skill, outPath, onLog })
|
|
|
35
47
|
const locale = skill.locale || "en";
|
|
36
48
|
const fieldsByNo = pack.fields[locale] ?? {};
|
|
37
49
|
const repeatsByNo = pack.repeats[locale] ?? {};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ---- deck reading ---------------------------------------------------------
|
|
46
|
-
const slidesDir = path.join(deckDir, "slides");
|
|
47
|
-
const entries = fs.readdirSync(slidesDir)
|
|
48
|
-
.filter((f) => f.endsWith(".jsx") || f.endsWith(".pptx"))
|
|
49
|
-
.sort((a, b) => a.localeCompare(b, "en", { numeric: true }));
|
|
50
|
-
if (entries.length === 0) throw new Error(`no .jsx or .pptx slides in ${slidesDir}`);
|
|
51
|
-
|
|
52
|
-
const problems = [];
|
|
53
|
-
const warnings = [];
|
|
54
|
-
const sequence = []; // { kind: "master", no, props, file } | { kind: "insert", file, buf }
|
|
55
|
-
|
|
56
|
-
// In-deck freeform pairing: NN-name.jsx (create-slides source) renders to a
|
|
57
|
-
// sibling NN-name.pptx artifact — when both exist, the .pptx carries the
|
|
58
|
-
// slide and the .jsx is source only.
|
|
59
|
-
const renderedStems = new Set(
|
|
60
|
-
entries.filter((f) => f.endsWith(".pptx")).map((f) => f.replace(/\.pptx$/, "")),
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
for (const file of entries) {
|
|
64
|
-
if (file.endsWith(".pptx")) {
|
|
65
|
-
sequence.push({ kind: "insert", file, buf: fs.readFileSync(path.join(slidesDir, file)) });
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (renderedStems.has(file.replace(/\.jsx$/, ""))) continue;
|
|
69
|
-
const src = fs.readFileSync(path.join(slidesDir, file), "utf8");
|
|
70
|
-
let parsed;
|
|
71
|
-
try {
|
|
72
|
-
parsed = parseSlideFile(src, file);
|
|
73
|
-
} catch (err) {
|
|
74
|
-
problems.push(err.message);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
const layout = layoutNumber(parsed.component, locale, pack.components);
|
|
78
|
-
if (!layout) {
|
|
79
|
-
problems.push(`${file}: <${parsed.component}> is a freeform (create-slides) slide with no ` +
|
|
80
|
-
`rendered artifact next to it — 'slidemind build-pptx' renders slides/${file.replace(/\.jsx$/, "")}.pptx ` +
|
|
81
|
-
`automatically when a paired add-in backend is reachable`);
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (layout.warn) warnings.push(`${file}: ${layout.warn}`);
|
|
85
|
-
const no = String(layout.no);
|
|
86
|
-
|
|
87
|
-
if ((pack.policy.freeform ?? []).includes(no)) {
|
|
88
|
-
if (Object.keys(parsed.props).length > 0) {
|
|
89
|
-
problems.push(`${file}: slide ${no} is a freeform placeholder — it takes no props. ` +
|
|
90
|
-
`Keep it bare to keep the master placeholder, or delete the file to drop the slide.`);
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
warnings.push(`${file}: keeping the master's slide-${no} placeholder (freeform replacement pending)`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
validateProps(parsed.props, fieldsByNo[no] ?? {}, repeatsByNo[no] ?? {}, pack.policy, no, file, problems);
|
|
97
|
-
sequence.push({ kind: "master", no: Number(no), props: parsed.props, file });
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
checkIdentities(sequence, warnings, problems);
|
|
50
|
+
const { sequence, warnings, problems } = inspectFillTemplateDeck({
|
|
51
|
+
deckDir,
|
|
52
|
+
skill,
|
|
53
|
+
includeBuffers: true,
|
|
54
|
+
});
|
|
101
55
|
if (problems.length) {
|
|
102
56
|
const err = new Error(`deck has ${problems.length} authoring error(s):\n - ${problems.join("\n - ")}`);
|
|
103
57
|
err.problems = problems;
|
|
@@ -142,7 +96,7 @@ export async function buildFillTemplateDeck({ deckDir, skill, outPath, onLog })
|
|
|
142
96
|
xml = applyDeckChrome(xml, fieldMap, skill.planMeta ?? {}, item.props, log);
|
|
143
97
|
xml = applyHygiene(xml, pack.policy, item.no, log);
|
|
144
98
|
files[part] = enc(xml);
|
|
145
|
-
output.push({ part, isNew: n > 1 });
|
|
99
|
+
output.push({ part, isNew: n > 1, sourceFile: item.file });
|
|
146
100
|
}
|
|
147
101
|
|
|
148
102
|
// Hygiene also applies to untouched (clone-only) slides that made it in.
|
|
@@ -161,6 +115,13 @@ export async function buildFillTemplateDeck({ deckDir, skill, outPath, onLog })
|
|
|
161
115
|
// sweep them up as orphans.
|
|
162
116
|
pruneOrphanParts(files, log);
|
|
163
117
|
|
|
118
|
+
// Deck-embedded source (spec §5.4): every declarative slide carries its
|
|
119
|
+
// slide-file source + a durable GUID as SLIDEMIND tags, so `adopt` can
|
|
120
|
+
// reconstruct the deck from the file alone. Freeform/native inserts arrive
|
|
121
|
+
// with their own tags (server-baked) and are left untouched. GUIDs persist
|
|
122
|
+
// in the deck's .slide-guids.json so a re-render keeps identities stable.
|
|
123
|
+
bakeDeclarativeSourceTags(files, output, deckDir, log);
|
|
124
|
+
|
|
164
125
|
const integrity = checkIntegrity(files);
|
|
165
126
|
for (const issue of integrity) warnings.push(`integrity: ${issue}`);
|
|
166
127
|
if (integrity.length) {
|
|
@@ -174,6 +135,162 @@ export async function buildFillTemplateDeck({ deckDir, skill, outPath, onLog })
|
|
|
174
135
|
return { outPath: finalOut, slideCount: output.length, warnings };
|
|
175
136
|
}
|
|
176
137
|
|
|
138
|
+
// Bake SLIDEMIND source tags for the deck's declarative slides. Best-effort:
|
|
139
|
+
// an unreadable slide file skips only itself (the deck still builds).
|
|
140
|
+
function bakeDeclarativeSourceTags(files, output, deckDir, log) {
|
|
141
|
+
const bundleRef = readDeckBundleRef(deckDir);
|
|
142
|
+
// Reconciled, not merely loaded: a renamed slide file keeps its GUID via the
|
|
143
|
+
// recorded content hash, and dead entries are pruned (see slide-guids.mjs).
|
|
144
|
+
const guids = reconcileSlideGuids(deckDir);
|
|
145
|
+
const minted = {};
|
|
146
|
+
const entries = [];
|
|
147
|
+
for (const { part, sourceFile } of output) {
|
|
148
|
+
if (!sourceFile || !sourceFile.endsWith(".jsx")) continue;
|
|
149
|
+
let jsx;
|
|
150
|
+
try {
|
|
151
|
+
jsx = fs.readFileSync(path.join(deckDir, "slides", sourceFile), "utf8");
|
|
152
|
+
} catch {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const key = `slides/${sourceFile}`;
|
|
156
|
+
const guid = guids[key] ?? minted[key]?.guid ?? mintSlideGuid();
|
|
157
|
+
minted[key] = slideGuidEntry(guid, jsx);
|
|
158
|
+
entries.push({
|
|
159
|
+
partName: part,
|
|
160
|
+
tags: encodeSourceTags({
|
|
161
|
+
name: sourceFile.replace(/\.jsx$/, ""),
|
|
162
|
+
jsx,
|
|
163
|
+
guid,
|
|
164
|
+
bundle: {
|
|
165
|
+
presFP: bundleRef?.presFP ?? "unknown",
|
|
166
|
+
etag: bundleRef?.etag ?? "",
|
|
167
|
+
},
|
|
168
|
+
renderer: "slidemind-cli-fill",
|
|
169
|
+
// Deck-local images the slide file references ride along (smallest-
|
|
170
|
+
// first within the budget, spec §2.2) so `adopt` rebuilds a deck that
|
|
171
|
+
// still has its pictures.
|
|
172
|
+
assets: referencedAssets(jsx, path.join(deckDir, "assets")),
|
|
173
|
+
pushedAt: new Date().toISOString(),
|
|
174
|
+
// Declarative slides are template fills, not freeform renders — the
|
|
175
|
+
// add-in's hydration treats them like any source (decoders ignore
|
|
176
|
+
// unknown meta properties).
|
|
177
|
+
metaExtra: { kind: "template-fill" },
|
|
178
|
+
}),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (entries.length && bakeSlideSourceTags(files, entries)) {
|
|
182
|
+
saveSlideGuids(deckDir, minted);
|
|
183
|
+
log.slide?.(`embedded source tags on ${entries.length} slide(s)`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Parse and structurally validate a fill-template deck without mutating it.
|
|
188
|
+
// External packs use this generic inspection surface to run their own domain
|
|
189
|
+
// checks before invoking build-pptx. Buffers are opt-in because JSON callers
|
|
190
|
+
// must receive a compact, serializable sequence.
|
|
191
|
+
export function inspectFillTemplateDeck({
|
|
192
|
+
deckDir,
|
|
193
|
+
skill,
|
|
194
|
+
allowUnrenderedFreeform = false,
|
|
195
|
+
includeBuffers = false,
|
|
196
|
+
}) {
|
|
197
|
+
const pack = loadPack(skill);
|
|
198
|
+
const locale = skill.locale || "en";
|
|
199
|
+
const fieldsByNo = pack.fields[locale] ?? {};
|
|
200
|
+
const repeatsByNo = pack.repeats[locale] ?? {};
|
|
201
|
+
const slidesDir = path.join(deckDir, "slides");
|
|
202
|
+
const problems = [];
|
|
203
|
+
const warnings = [];
|
|
204
|
+
const sequence = [];
|
|
205
|
+
|
|
206
|
+
if (!skill.templatePath || !fs.existsSync(skill.templatePath)) {
|
|
207
|
+
problems.push(
|
|
208
|
+
`template "${skill.templateKey}" not found in pack ${skill.skillDir} — check the pack manifest templates`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (!fs.existsSync(slidesDir) || !fs.statSync(slidesDir).isDirectory()) {
|
|
212
|
+
problems.push(`expected 'slides/' folder in ${deckDir}`);
|
|
213
|
+
return { sequence, warnings, problems, locale, templateKey: skill.templateKey };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const entries = fs.readdirSync(slidesDir)
|
|
217
|
+
.filter((f) => f.endsWith(".jsx") || f.endsWith(".pptx"))
|
|
218
|
+
.sort((a, b) => a.localeCompare(b, "en", { numeric: true }));
|
|
219
|
+
if (entries.length === 0) {
|
|
220
|
+
problems.push(`no .jsx or .pptx slides in ${slidesDir}`);
|
|
221
|
+
return { sequence, warnings, problems, locale, templateKey: skill.templateKey };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const renderedStems = new Set(
|
|
225
|
+
entries.filter((f) => f.endsWith(".pptx")).map((f) => f.replace(/\.pptx$/, "")),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
for (const file of entries) {
|
|
229
|
+
const full = path.join(slidesDir, file);
|
|
230
|
+
if (file.endsWith(".pptx")) {
|
|
231
|
+
sequence.push({
|
|
232
|
+
kind: "insert",
|
|
233
|
+
file,
|
|
234
|
+
...(includeBuffers ? { buf: fs.readFileSync(full) } : {}),
|
|
235
|
+
});
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (renderedStems.has(file.replace(/\.jsx$/, ""))) continue;
|
|
239
|
+
|
|
240
|
+
const src = fs.readFileSync(full, "utf8");
|
|
241
|
+
const component = src.match(/<([A-Z][A-Za-z0-9]*)/)?.[1] ?? null;
|
|
242
|
+
let layout = null;
|
|
243
|
+
if (component) {
|
|
244
|
+
try {
|
|
245
|
+
layout = layoutNumber(component, locale, pack.components);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
problems.push(err.message);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// A pack validator needs to see freeform source positions even before the
|
|
253
|
+
// paired add-in artifact exists. The build path still rejects such a
|
|
254
|
+
// source until ensureFreeformRenders has produced its .pptx sibling.
|
|
255
|
+
if (!layout && allowUnrenderedFreeform) {
|
|
256
|
+
sequence.push({ kind: "freeform", file, component });
|
|
257
|
+
warnings.push(`${file}: freeform source has no rendered .pptx artifact yet`);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let parsed;
|
|
262
|
+
try {
|
|
263
|
+
parsed = parseSlideFile(src, file);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
problems.push(err.message);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
layout = layoutNumber(parsed.component, locale, pack.components);
|
|
269
|
+
if (!layout) {
|
|
270
|
+
problems.push(`${file}: <${parsed.component}> is a freeform (create-slides) slide with no ` +
|
|
271
|
+
`rendered artifact next to it — render slides/${file.replace(/\.jsx$/, "")}.pptx ` +
|
|
272
|
+
`through the paired add-in before building`);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (layout.warn) warnings.push(`${file}: ${layout.warn}`);
|
|
276
|
+
const no = String(layout.no);
|
|
277
|
+
|
|
278
|
+
if ((pack.policy.freeform ?? []).includes(no)) {
|
|
279
|
+
if (Object.keys(parsed.props).length > 0) {
|
|
280
|
+
problems.push(`${file}: slide ${no} is a freeform placeholder — it takes no props. ` +
|
|
281
|
+
`Keep it bare to keep the master placeholder, or delete the file to drop the slide.`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
warnings.push(`${file}: keeping the master's slide-${no} placeholder (freeform replacement pending)`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
validateProps(parsed.props, fieldsByNo[no] ?? {}, repeatsByNo[no] ?? {}, pack.policy, no, file, problems);
|
|
288
|
+
sequence.push({ kind: "master", no: Number(no), props: parsed.props, file });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return { sequence, warnings, problems, locale, templateKey: skill.templateKey };
|
|
292
|
+
}
|
|
293
|
+
|
|
177
294
|
// ---------------------------------------------------------------------------
|
|
178
295
|
// Pack
|
|
179
296
|
// ---------------------------------------------------------------------------
|
|
@@ -266,77 +383,6 @@ function editDistance(a, b) {
|
|
|
266
383
|
return dp[a.length][b.length];
|
|
267
384
|
}
|
|
268
385
|
|
|
269
|
-
// ---------------------------------------------------------------------------
|
|
270
|
-
// Cross-slide identities (the catalog's consistency table, as code)
|
|
271
|
-
// ---------------------------------------------------------------------------
|
|
272
|
-
|
|
273
|
-
const asLines = (v) => (Array.isArray(v) ? v : String(v ?? "").split("\n")).map((s) => s.trim()).filter(Boolean);
|
|
274
|
-
|
|
275
|
-
function checkIdentities(sequence, warnings, problems) {
|
|
276
|
-
const slide = (no) => sequence.filter((s) => s.kind === "master" && s.no === no);
|
|
277
|
-
const approach = slide(10)[0];
|
|
278
|
-
const details = slide(11);
|
|
279
|
-
const gantt = slide(13)[0];
|
|
280
|
-
const calc = slide(16)[0];
|
|
281
|
-
|
|
282
|
-
const wpItems = approach?.props?.repeat?.workPackages ?? null;
|
|
283
|
-
const ganttItems = gantt?.props?.repeat?.workPackages ?? null;
|
|
284
|
-
|
|
285
|
-
if (wpItems && details.length && wpItems.length !== details.length) {
|
|
286
|
-
problems.push(`WP count mismatch: slide 10 declares ${wpItems.length} work packages but there are ${details.length} WP-detail files (slide 11)`);
|
|
287
|
-
}
|
|
288
|
-
// Every WP detail must carry FULL bullet lists — a section left empty (or a
|
|
289
|
-
// one-liner) is a defect in a client deliverable, not a style choice.
|
|
290
|
-
details.forEach((d, i) => {
|
|
291
|
-
for (const section of ["services", "assumptions", "deliverables"]) {
|
|
292
|
-
const lines = asLines(d.props?.[section]);
|
|
293
|
-
if (lines.length === 0) {
|
|
294
|
-
problems.push(`${d.file}: WP detail ${i + 1} has no "${section}" — every WP detail needs all three bullet lists filled`);
|
|
295
|
-
} else if (lines.length < 3) {
|
|
296
|
-
warnings.push(`${d.file}: "${section}" has only ${lines.length} bullet(s) — WP details expect full lists (3–5)`);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
if (wpItems && ganttItems && wpItems.length !== ganttItems.length) {
|
|
301
|
-
problems.push(`WP count mismatch: slide 10 has ${wpItems.length} work packages, the project plan (13) has ${ganttItems.length} rows`);
|
|
302
|
-
}
|
|
303
|
-
if (ganttItems && details.length === ganttItems.length) {
|
|
304
|
-
ganttItems.forEach((item, i) => {
|
|
305
|
-
const canonical = asLines(details[i]?.props?.deliverables);
|
|
306
|
-
const row = asLines(item.deliverable);
|
|
307
|
-
if (canonical.length && row.length && canonical.join("\n") !== row.join("\n")) {
|
|
308
|
-
problems.push(`deliverables drift: gantt row ${i + 1} must repeat WP-${i + 1}'s deliverables verbatim (slide 11 owns them)`);
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
if (wpItems && ganttItems) {
|
|
313
|
-
ganttItems.forEach((item, i) => {
|
|
314
|
-
if (item.title && wpItems[i]?.title && item.title !== wpItems[i].title) {
|
|
315
|
-
problems.push(`WP name drift: slide 10 card ${i + 1} is "${wpItems[i].title}" but gantt row ${i + 1} is "${item.title}"`);
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
for (const [frameId, rows] of Object.entries(calc?.props?.tables ?? {})) {
|
|
320
|
-
if (!Array.isArray(rows) || !wpItems) continue;
|
|
321
|
-
const labels = rows.slice(1, -1).map((r) => String(r[0] ?? ""));
|
|
322
|
-
labels.forEach((label, i) => {
|
|
323
|
-
if (wpItems[i]?.title && label && label !== wpItems[i].title) {
|
|
324
|
-
problems.push(`WP name drift: calculation table ${frameId} row ${i + 1} is "${label}" but slide 10 card ${i + 1} is "${wpItems[i].title}"`);
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
// Price echo: the contracted total should appear in the management summary.
|
|
329
|
-
const costRows = calc?.props?.tables?.["15"];
|
|
330
|
-
if (Array.isArray(costRows)) {
|
|
331
|
-
const grand = String(costRows[costRows.length - 1]?.slice(-1)[0] ?? "");
|
|
332
|
-
const digits = grand.replace(/[^\d.,]/g, "");
|
|
333
|
-
const summary = slide(7)[0];
|
|
334
|
-
if (digits && summary?.props?.approach && !String(summary.props.approach).includes(digits)) {
|
|
335
|
-
warnings.push(`slide 7's approach paragraph does not mention the contracted total ${grand} from table 15`);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
386
|
// ---------------------------------------------------------------------------
|
|
341
387
|
// Per-slide ops
|
|
342
388
|
// ---------------------------------------------------------------------------
|
package/src/pptx/hidden-ole.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { readText } from "./zip.mjs";
|
|
2
2
|
import { relsFor, relsPathFor, resolvePartPath } from "./ooxml.mjs";
|
|
3
|
+
import { slideHasForeignTags, slideReferencesEmbeddedWorkbook } from "./tags.mjs";
|
|
3
4
|
|
|
4
5
|
const CONTENT_TYPES = "[Content_Types].xml";
|
|
5
6
|
|
|
6
|
-
export function stripHiddenOleGraphicFrames(files) {
|
|
7
|
+
export function stripHiddenOleGraphicFrames(files, { keepSlideOle = null } = {}) {
|
|
7
8
|
let changed = false;
|
|
8
9
|
const removedTargets = new Set();
|
|
9
10
|
const candidates = Object.keys(files).filter((part) =>
|
|
@@ -14,6 +15,26 @@ export function stripHiddenOleGraphicFrames(files) {
|
|
|
14
15
|
const before = readText(files, part);
|
|
15
16
|
if (!before) continue;
|
|
16
17
|
|
|
18
|
+
// Coexistence rule C4 (intern-slidemind docs/deck-embedded-source-spec.md
|
|
19
|
+
// §13): a SLIDE that carries another add-in's state — foreign tags or an
|
|
20
|
+
// embedded workbook/OLE part (think-cell "slide workbook" pattern) — keeps
|
|
21
|
+
// its hidden OLE frames untouched; stripping them would destroy that
|
|
22
|
+
// add-in's data. Slidemind's own rendered/template output never has
|
|
23
|
+
// foreign tags, so the strip stays fully active there. Masters/layouts
|
|
24
|
+
// (template chain) keep the old behavior. The verdict is constant per
|
|
25
|
+
// slide CONTENT, so a caller running the strip once per pick over the
|
|
26
|
+
// same package (foldPickedSlides) precomputes it and passes `keepSlideOle`
|
|
27
|
+
// instead of paying the rels + tag-part parse again on every pick.
|
|
28
|
+
if (
|
|
29
|
+
part.startsWith("ppt/slides/") &&
|
|
30
|
+
(keepSlideOle
|
|
31
|
+
? keepSlideOle(part)
|
|
32
|
+
: slideHasForeignTags(files, part) ||
|
|
33
|
+
slideReferencesEmbeddedWorkbook(files, part))
|
|
34
|
+
) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
17
38
|
const removedIds = new Set();
|
|
18
39
|
const after = before.replace(
|
|
19
40
|
/<p:graphicFrame\b[\s\S]*?<\/p:graphicFrame>/g,
|
package/src/pptx/slide-swap.mjs
CHANGED
|
@@ -31,6 +31,10 @@ import {
|
|
|
31
31
|
} from "./ooxml.mjs";
|
|
32
32
|
import { stripHiddenOleGraphicFrames } from "./hidden-ole.mjs";
|
|
33
33
|
import { stripRiskyImageExtensions } from "./image-extensions.mjs";
|
|
34
|
+
import {
|
|
35
|
+
slideHasForeignTags,
|
|
36
|
+
slideReferencesEmbeddedWorkbook,
|
|
37
|
+
} from "./tags.mjs";
|
|
34
38
|
|
|
35
39
|
// The four package parts we always keep and rewrite for standalone validity.
|
|
36
40
|
const SKELETON = [
|
|
@@ -63,6 +67,19 @@ export function foldPickedSlides(buffer, picks, { sourceName = "reused" } = {})
|
|
|
63
67
|
const lowest = firstSlidePath(files);
|
|
64
68
|
const lowestRels = relsPathFor(lowest);
|
|
65
69
|
|
|
70
|
+
// The C4 keep-OLE verdict (foreign tags / embedded workbook) is per slide
|
|
71
|
+
// CONTENT and constant across picks — computed inside the strip it re-parsed
|
|
72
|
+
// every slide's rels and tag parts once per pick, O(picks × slides) on a big
|
|
73
|
+
// adopt. One pass here; the per-pick lookup follows the slide swap below
|
|
74
|
+
// (the lowest slot holds the picked slide's content).
|
|
75
|
+
const keepOleByPart = new Map(
|
|
76
|
+
order.map((part) => [
|
|
77
|
+
part,
|
|
78
|
+
slideHasForeignTags(files, part) ||
|
|
79
|
+
slideReferencesEmbeddedWorkbook(files, part),
|
|
80
|
+
]),
|
|
81
|
+
);
|
|
82
|
+
|
|
66
83
|
return picks.map((n) => {
|
|
67
84
|
const picked = order[n - 1];
|
|
68
85
|
const sldId = entries[n - 1] ? entries[n - 1].sldId : null;
|
|
@@ -89,7 +106,12 @@ export function foldPickedSlides(buffer, picks, { sourceName = "reused" } = {})
|
|
|
89
106
|
copy[lowestRels] = stripNotesRel(copy[lowestRels]);
|
|
90
107
|
notesStripped = true;
|
|
91
108
|
}
|
|
92
|
-
const hiddenOleStripped = stripHiddenOleGraphicFrames(copy
|
|
109
|
+
const hiddenOleStripped = stripHiddenOleGraphicFrames(copy, {
|
|
110
|
+
keepSlideOle: (part) =>
|
|
111
|
+
keepOleByPart.get(
|
|
112
|
+
part === lowest ? picked : part === picked ? lowest : part,
|
|
113
|
+
) ?? false,
|
|
114
|
+
});
|
|
93
115
|
const riskyImagesStripped = stripRiskyImageExtensions(copy);
|
|
94
116
|
|
|
95
117
|
// Prune to the lowest slide's dependency closure + trimmed skeleton.
|