slidemind 0.3.17 → 0.3.18

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidemind",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "description": "Connect Claude Code to the slidemind PowerPoint add-in — build and edit native, on-brand PowerPoint decks against the active corporate design, from the terminal.",
5
5
  "author": {
6
6
  "name": "statworx",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidemind",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "description": "Connect Codex to the slidemind PowerPoint add-in — build and edit native, on-brand PowerPoint decks against the active corporate design. Requires the slidemind CLI (npm install -g slidemind); the skills explain the install when it is missing.",
5
5
  "author": {
6
6
  "name": "statworx",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidemind",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "description": "Connect Claude Code, Codex & opencode to the slidemind PowerPoint add-in — let your coding agent build and edit native, on-brand PowerPoint decks against your active corporate design, from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -106,7 +106,7 @@ function convertedHostStubs(slideFiles, conversions) {
106
106
  return [...hosts].map(
107
107
  ([name, { w, h }]) =>
108
108
  `<script type="text/jsx-template" data-kind="layout" data-name="${name}" data-binding="${name}">\n` +
109
- `function ${name}({ background, children }) { return <div style={{ position: "relative", width: "${w}", height: "${h}", overflow: "hidden", background: background || "#FFFFFF" }}>{children}</div>; }\n` +
109
+ `export default function ${name}({ background, children }) { return <div style={{ position: "relative", width: "${w}", height: "${h}", overflow: "hidden", background: background || "#FFFFFF" }}>{children}</div>; }\n` +
110
110
  `</script>`,
111
111
  );
112
112
  }
package/src/lib/adopt.mjs CHANGED
@@ -30,7 +30,14 @@ import { decodeSourceTags, mintSlideGuid, readSlideTags } from "../pptx/tags.mjs
30
30
  import { foldPickedSlides } from "../pptx/slide-swap.mjs";
31
31
  import { readDeckBundleRef, writeDeckBundleRef } from "../bridge/bundle.mjs";
32
32
  import { loadSlideGuids, saveSlideGuids, slideGuidEntry } from "./slide-guids.mjs";
33
- import { hasSourcedChart, writeConversionRecord } from "./conversions.mjs";
33
+ import { writeConversionRecord } from "./conversions.mjs";
34
+
35
+ // Converted JSX needs a fresh baseline against the imported PPTX. Source tags
36
+ // alone cannot recover the original conversion evidence or preserved natives.
37
+ export function needsConversion(decoded) {
38
+ return !decoded || /<ConvertedHost\d+x\d+\b/.test(decoded.jsx) ||
39
+ /^\s*\/\/\s*SLIDEMIND_DIALECT:\s*converted\b/m.test(decoded.jsx);
40
+ }
34
41
 
35
42
  export const LIVE_DECK_UNSUPPORTED =
36
43
  "conversion of live presentations requires the add-in path in v1 — " +
@@ -174,21 +181,13 @@ export function adoptDeckFromPptx({ pptxPath, deckDir, converted = null, source
174
181
  const native = [];
175
182
  let bundle = null;
176
183
 
177
- // Fold in ONE pass: foldPickedSlides unzips and prunes the whole source
178
- // package per call, so folding slide by slide redid that work for the entire
179
- // deck on every native slide. Three kinds of pick share the pass — a slide
180
- // that stays native, a CONVERTED slide whose preserved natives (OLE /
181
- // SmartArt / unsupported chart kinds) need their verbatim source package kept
182
- // for the render-time splice (design D4/D6), and a converted slide carrying
183
- // the deck's own chart as `<Chart data-sm-source-id>`, which the render
184
- // patches against that same package (native-charts 5.4a).
184
+ // Fold every converted slide in one pass. The package supplies preserved
185
+ // natives AND the original envelope (layouts, passive third-party tags,
186
+ // relationships). Tagged converted JSX alone cannot restore either safely.
185
187
  const picks = [];
186
188
  decodedByIndex.forEach((decoded, index) => {
187
- if (decoded) return;
188
- const conv = convertedSlides.get(index + 1);
189
- if (!conv || conv.preserved?.length > 0 || hasSourcedChart(conv.jsx)) {
190
- picks.push(index + 1);
191
- }
189
+ if (!needsConversion(decoded)) return;
190
+ picks.push(index + 1);
192
191
  });
193
192
  const foldByPick = new Map(
194
193
  (picks.length
@@ -229,7 +228,7 @@ export function adoptDeckFromPptx({ pptxPath, deckDir, converted = null, source
229
228
  }
230
229
 
231
230
  decodedByIndex.forEach((decoded, index) => {
232
- if (decoded) {
231
+ if (decoded && !needsConversion(decoded)) {
233
232
  const stem = stemFor(decoded.meta.name, index, taken);
234
233
  let jsx = decoded.jsx;
235
234
  for (const asset of decoded.assets) {
@@ -251,11 +250,14 @@ export function adoptDeckFromPptx({ pptxPath, deckDir, converted = null, source
251
250
  if (conv) {
252
251
  // Deterministically converted: editable jsx in the converted dialect,
253
252
  // plus the identity/evidence record push threads back to the server.
254
- const stem = stemFor(conv.name ?? `${pad2(index + 1)}-converted`, index, taken);
253
+ const stem = stemFor(decoded?.meta.name ?? conv.name ?? `${pad2(index + 1)}-converted`, index, taken);
255
254
  let jsx = conv.jsx;
256
255
  for (const [from, to] of convAssetRenames) jsx = jsx.split(from).join(to);
257
256
  fs.writeFileSync(path.join(slidesDir, `${stem}.jsx`), jsx, "utf8");
258
- const guid = conv.guid && !seenGuids.has(conv.guid) ? conv.guid : mintSlideGuid();
257
+ const candidateGuid = decoded?.guid ?? conv.guid;
258
+ const owns = !decoded || ownerIndexByGuid.get(decoded.guid)?.index === index;
259
+ const guid = owns && candidateGuid && !seenGuids.has(candidateGuid)
260
+ ? candidateGuid : mintSlideGuid();
259
261
  seenGuids.add(guid);
260
262
  guids[`slides/${stem}.jsx`] = slideGuidEntry(guid, jsx);
261
263
  writeConversionRecord(
@@ -277,6 +279,7 @@ export function adoptDeckFromPptx({ pptxPath, deckDir, converted = null, source
277
279
  },
278
280
  fold?.bytes ?? null,
279
281
  );
282
+ bundle ??= decoded?.meta.bundle ?? null;
280
283
  convertedFiles.push(`${stem}.jsx`);
281
284
  return;
282
285
  }
@@ -329,7 +332,10 @@ export function adoptDeckFromPptx({ pptxPath, deckDir, converted = null, source
329
332
  }
330
333
 
331
334
  /**
332
- * Deterministically convert a FILE deck's untagged slides through the add-in's
335
+ * Convert foreign slides and re-convert embedded converted slides against the
336
+ * CURRENT imported PPTX. This recovers a fresh baseline/evidence pair without
337
+ * pretending the original conversion was reviewed. Ordinary authored JSX is
338
+ * restored verbatim. Requests go through the add-in's
333
339
  * `POST /api/cli/brands/:brandId/convert-pptx` route. Returns the route result
334
340
  * (`{ slides, assets, skipped, warnings }` + the `brandId` it ran against) to
335
341
  * hand to {@link adoptDeckFromPptx}, or null when conversion is not available
@@ -349,7 +355,7 @@ export async function convertUntaggedSlides({
349
355
  const { buffer, decodedByIndex } = source ?? readSourceDeck(pptxPath);
350
356
  const slides = [];
351
357
  decodedByIndex.forEach((decoded, i) => {
352
- if (!decoded) slides.push(i + 1);
358
+ if (needsConversion(decoded)) slides.push(i + 1);
353
359
  });
354
360
  if (slides.length === 0) return null;
355
361
 
@@ -372,7 +378,7 @@ export async function convertUntaggedSlides({
372
378
  if (brands.length !== 1) {
373
379
  note(
374
380
  brands.length === 0
375
- ? "no corporate design available to convert against — untagged slides stay native"
381
+ ? "no corporate design available to convert against — slides requiring conversion stay native"
376
382
  : "several corporate designs available — pass `--brand <presFP>` to convert the untagged slides",
377
383
  );
378
384
  return null;
@@ -383,7 +389,7 @@ export async function convertUntaggedSlides({
383
389
  // capability probe — do it here, so a whole .pptx is never shipped to a
384
390
  // backend that will just 404 the route.
385
391
  if (backendURL && !(await client.backendSupportsConvertPptx(backendURL))) {
386
- note("this add-in does not convert foreign slides — untagged slides stay native");
392
+ note("this add-in does not convert foreign slides — slides requiring conversion stay native");
387
393
  return null;
388
394
  }
389
395
  }
@@ -396,15 +402,15 @@ export async function convertUntaggedSlides({
396
402
  backendURL,
397
403
  });
398
404
  if (result?.ok === false) {
399
- note("the add-in declined to convert this file — untagged slides stay native");
405
+ note("the add-in declined to convert this file — slides requiring conversion stay native");
400
406
  return null;
401
407
  }
402
408
  return { ...result, brandId: resolved };
403
409
  } catch (err) {
404
410
  note(
405
411
  err?.code === "convert_route_unavailable"
406
- ? "this add-in does not convert foreign slides — untagged slides stay native"
407
- : `slide conversion unavailable (${err?.message ?? err}) — untagged slides stay native`,
412
+ ? "this add-in does not convert foreign slides — slides requiring conversion stay native"
413
+ : `slide conversion unavailable (${err?.message ?? err}) — slides requiring conversion stay native`,
408
414
  );
409
415
  return null;
410
416
  }
@@ -20,7 +20,6 @@ import { unzip, zip, readText } from "./zip.mjs";
20
20
  import { slideOrderPaths, decodeXmlEntities } from "./ooxml.mjs";
21
21
  import { slideText } from "./read-local.mjs";
22
22
  import { stripHiddenOleGraphicFrames } from "./hidden-ole.mjs";
23
- import { stripRiskyImageExtensions } from "./image-extensions.mjs";
24
23
 
25
24
  const APP_PROPS = "docProps/app.xml";
26
25
 
@@ -61,9 +60,6 @@ export function normalizePptxAppProperties(buffer) {
61
60
  if (stripHiddenOleGraphicFrames(entries)) {
62
61
  changed = true;
63
62
  }
64
- if (stripRiskyImageExtensions(entries)) {
65
- changed = true;
66
- }
67
63
 
68
64
  return {
69
65
  bytes: changed ? zip(entries) : buffer,
@@ -30,7 +30,6 @@ import {
30
30
  resolvePartPath,
31
31
  } from "./ooxml.mjs";
32
32
  import { stripHiddenOleGraphicFrames } from "./hidden-ole.mjs";
33
- import { stripRiskyImageExtensions } from "./image-extensions.mjs";
34
33
  import {
35
34
  slideHasForeignTags,
36
35
  slideReferencesEmbeddedWorkbook,
@@ -112,7 +111,6 @@ export function foldPickedSlides(buffer, picks, { sourceName = "reused" } = {})
112
111
  part === lowest ? picked : part === picked ? lowest : part,
113
112
  ) ?? false,
114
113
  });
115
- const riskyImagesStripped = stripRiskyImageExtensions(copy);
116
114
 
117
115
  // Prune to the lowest slide's dependency closure + trimmed skeleton.
118
116
  const { entries: pruned, changed } = pruneToLowest(copy);
@@ -123,7 +121,6 @@ export function foldPickedSlides(buffer, picks, { sourceName = "reused" } = {})
123
121
  !swapped &&
124
122
  !notesStripped &&
125
123
  !hiddenOleStripped &&
126
- !riskyImagesStripped &&
127
124
  !changed
128
125
  ) {
129
126
  return { selection: n, slideId, bytes: buffer };
@@ -1,190 +0,0 @@
1
- import { readText } from "./zip.mjs";
2
- import { relsFor, relsPathFor, resolvePartPath } from "./ooxml.mjs";
3
-
4
- const CONTENT_TYPES = "[Content_Types].xml";
5
-
6
- export function stripRiskyImageExtensions(files) {
7
- let changed = false;
8
- const removedTargets = new Set();
9
- const candidates = Object.keys(files).filter((part) =>
10
- /^ppt\/(?:slides|slideMasters|slideLayouts)\/[^/]+\.xml$/.test(part),
11
- );
12
-
13
- for (const part of candidates) {
14
- const before = readText(files, part);
15
- if (!before) continue;
16
-
17
- const removedIds = new Set();
18
- const relsById = new Map(relsFor(files, part).map((rel) => [rel.id, rel]));
19
- const partDir = part.includes("/") ? part.slice(0, part.lastIndexOf("/")) : "";
20
- let after = before.replace(
21
- /<a:ext\b[\s\S]*?<\/a:ext>/g,
22
- (ext) => {
23
- if (!/<(?:asvg:svgBlip|a14:imgLayer)\b/.test(ext)) return ext;
24
- for (const id of relationshipIdsIn(ext)) removedIds.add(id);
25
- return "";
26
- },
27
- );
28
- after = after.replace(/<a:extLst\b[^>]*>\s*<\/a:extLst>/g, "");
29
- after = after.replace(
30
- /<p:pic\b[\s\S]*?<\/p:pic>/g,
31
- (pic) => {
32
- if (!/<a:blip\b(?![^>]*\br:(?:embed|link)=)[^>]*>\s*<\/a:blip>/.test(pic)) {
33
- return pic;
34
- }
35
- for (const id of relationshipIdsIn(pic)) removedIds.add(id);
36
- return "";
37
- },
38
- );
39
- after = after.replace(
40
- /<p:pic\b[\s\S]*?<\/p:pic>/g,
41
- (pic) => {
42
- for (const id of relationshipIdsIn(pic)) {
43
- const rel = relsById.get(id);
44
- if (
45
- rel &&
46
- rel.targetMode !== "External" &&
47
- /\.svg(?:$|[?#])/i.test(rel.target ?? "")
48
- ) {
49
- removedIds.add(id);
50
- return "";
51
- }
52
- }
53
- return pic;
54
- },
55
- );
56
- const relsPath = relsPathFor(part);
57
- const relsXml = readText(files, relsPath);
58
- const remainingIds = relationshipIdsIn(after);
59
- if (relsXml) {
60
- for (const rel of relsById.values()) {
61
- if (
62
- rel.targetMode !== "External" &&
63
- !remainingIds.has(rel.id) &&
64
- /\.(?:svg|wdp)(?:$|[?#])/i.test(rel.target ?? "")
65
- ) {
66
- removedIds.add(rel.id);
67
- }
68
- }
69
- }
70
-
71
- if (after === before && removedIds.size === 0) continue;
72
-
73
- if (after !== before) {
74
- files[part] = new TextEncoder().encode(after);
75
- changed = true;
76
- }
77
- if (!relsXml) continue;
78
-
79
- let nextRels = relsXml;
80
- for (const id of removedIds) {
81
- if (remainingIds.has(id)) continue;
82
- nextRels = removeRelationshipById(nextRels, id);
83
- const rel = relsById.get(id);
84
- if (rel && rel.targetMode !== "External") {
85
- removedTargets.add(resolvePartPath(partDir, rel.target));
86
- }
87
- }
88
- if (nextRels !== relsXml) {
89
- files[relsPath] = new TextEncoder().encode(nextRels);
90
- }
91
- }
92
-
93
- const deleted = deleteUnreferencedTargets(files, removedTargets);
94
- if (deleted.size > 0) {
95
- removeContentTypeOverrides(files, deleted);
96
- changed = true;
97
- }
98
- if (removeUnusedMediaDefaults(files, ["svg", "wdp"])) {
99
- changed = true;
100
- }
101
-
102
- return changed;
103
- }
104
-
105
- function deleteUnreferencedTargets(files, removedTargets) {
106
- const remainingRefs = referencedInternalTargets(files);
107
- const deleted = new Set();
108
- for (const part of removedTargets) {
109
- if (!part || remainingRefs.has(part) || files[part] === undefined) continue;
110
- delete files[part];
111
- const sidecar = relsPathFor(part);
112
- if (files[sidecar] !== undefined) delete files[sidecar];
113
- deleted.add(part);
114
- }
115
- return deleted;
116
- }
117
-
118
- function referencedInternalTargets(files) {
119
- const refs = new Set();
120
- for (const relsPath of Object.keys(files)) {
121
- if (!relsPath.endsWith(".rels")) continue;
122
- const dir = relsPath.replace(/\/?_rels\/[^/]+$/, "");
123
- const owner =
124
- dir === ""
125
- ? relsPath.replace(/^_rels\/(.*)\.rels$/, "$1")
126
- : `${dir}/${relsPath.replace(/^.*_rels\/(.*)\.rels$/, "$1")}`;
127
- for (const rel of relsFor(files, owner)) {
128
- if (rel.targetMode === "External") continue;
129
- refs.add(resolvePartPath(dir, rel.target));
130
- }
131
- }
132
- return refs;
133
- }
134
-
135
- function removeContentTypeOverrides(files, deletedParts) {
136
- const before = readText(files, CONTENT_TYPES);
137
- if (!before) return;
138
- let after = before;
139
- for (const part of deletedParts) {
140
- after = after.replace(
141
- new RegExp(
142
- `<Override\\b(?=[^>]*\\bPartName="/${escapeRe(part)}")[^>]*?/>`,
143
- "g",
144
- ),
145
- "",
146
- );
147
- }
148
- if (after !== before) files[CONTENT_TYPES] = new TextEncoder().encode(after);
149
- }
150
-
151
- function removeUnusedMediaDefaults(files, exts) {
152
- const before = readText(files, CONTENT_TYPES);
153
- if (!before) return false;
154
- let after = before;
155
- for (const ext of exts) {
156
- const hasPart = Object.keys(files).some((part) =>
157
- part.toLowerCase().endsWith(`.${ext}`),
158
- );
159
- if (hasPart) continue;
160
- after = after.replace(
161
- new RegExp(`<Default\\b(?=[^>]*\\bExtension="${escapeRe(ext)}")[^>]*?/>`, "g"),
162
- "",
163
- );
164
- }
165
- if (after !== before) {
166
- files[CONTENT_TYPES] = new TextEncoder().encode(after);
167
- return true;
168
- }
169
- return false;
170
- }
171
-
172
- function relationshipIdsIn(xml) {
173
- const ids = new Set();
174
- for (const m of String(xml).matchAll(/\br:(?:id|embed|link)\s*=\s*"([^"]+)"/g)) {
175
- ids.add(m[1]);
176
- }
177
- return ids;
178
- }
179
-
180
- function removeRelationshipById(xml, id) {
181
- return xml.replace(
182
- new RegExp(
183
- `<Relationship\\b(?=[^>]*\\bId="${escapeRe(id)}")[^>]*?(?:/>|>[\\s\\S]*?</Relationship>)`,
184
- "g",
185
- ),
186
- "",
187
- );
188
- }
189
-
190
- const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");