slidemind 0.1.8 → 0.2.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.md +10 -6
- package/README.md +19 -7
- package/bin/slidemind.mjs +819 -86
- package/package.json +2 -1
- package/skills/create-slides/SKILL.md +70 -18
- package/skills/search/SKILL.md +25 -6
- package/src/bridge/build-and-push.mjs +11 -0
- package/src/bridge/bundle.mjs +36 -7
- package/src/bridge/capture.mjs +20 -2
- package/src/bridge/client.mjs +513 -7
- package/src/bridge/engine-download.mjs +229 -5
- package/src/bridge/image-dimensions.mjs +181 -0
- package/src/builders/build.mjs +53 -23
- package/src/builders/gate-server.mjs +163 -22
- package/src/builders/plan-markdown-edit.mjs +120 -0
- package/src/builders/plan-overlay.js +1261 -230
- package/src/builders/plan.mjs +146 -25
- package/src/builders/red-thread-backbone.mjs +194 -0
- package/src/builders/resolve-skill.mjs +45 -1
- package/src/builders/review-overlay.js +44 -7
- package/src/builders/review.mjs +76 -11
- package/src/builders/storyboard-markdown.mjs +118 -0
- package/src/builders/storyboard-schema.mjs +180 -0
- package/src/builders/visual-families.mjs +32 -0
- package/src/pptx/app-props.mjs +222 -0
- package/src/pptx/fill/engine.mjs +1273 -0
- package/src/pptx/fill/freeform.mjs +156 -0
- package/src/pptx/fill/parse-slides.mjs +154 -0
- package/src/pptx/fill/xml.mjs +486 -0
- package/src/pptx/hidden-ole.mjs +125 -0
- package/src/pptx/image-extensions.mjs +190 -0
- package/src/pptx/ooxml.mjs +176 -0
- package/src/pptx/read-local.mjs +206 -0
- package/src/pptx/slide-swap.mjs +429 -0
- package/src/pptx/zip.mjs +30 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Normalize PowerPoint package metadata after server-side assembly.
|
|
2
|
+
//
|
|
3
|
+
// The render backend can assemble authored JSX slides plus reused native .pptx
|
|
4
|
+
// slides correctly while leaving docProps/app.xml counted from the JSX-only
|
|
5
|
+
// source deck. PowerPoint treats stale <Slides>, HeadingPairs, or TitlesOfParts
|
|
6
|
+
// metadata as repairable corruption on open. This post-process keeps those
|
|
7
|
+
// extended properties aligned with the actual slide order in ppt/presentation.xml.
|
|
8
|
+
//
|
|
9
|
+
// Reused native slides can also bring duplicate copies of a source deck's slide
|
|
10
|
+
// master tree. Those copies legitimately need distinct relationship targets, but
|
|
11
|
+
// PowerPoint allocates <p:sldMasterId id> and <p:sldLayoutId id> from a single
|
|
12
|
+
// shared id space and treats any collision across that whole space as repairable
|
|
13
|
+
// corruption on open. A splicer that assigns new master ids from a watermark that
|
|
14
|
+
// only scanned presentation.xml can hand a spliced master the same numeric id as
|
|
15
|
+
// an existing brand layout. Treat the sldMasterId values as fixed anchors and
|
|
16
|
+
// renumber only the colliding sldLayoutId values, keeping every r:id pointer
|
|
17
|
+
// untouched and never rewriting presentation.xml.
|
|
18
|
+
|
|
19
|
+
import { unzip, zip, readText } from "./zip.mjs";
|
|
20
|
+
import { slideOrderPaths, decodeXmlEntities } from "./ooxml.mjs";
|
|
21
|
+
import { slideText } from "./read-local.mjs";
|
|
22
|
+
import { stripHiddenOleGraphicFrames } from "./hidden-ole.mjs";
|
|
23
|
+
import { stripRiskyImageExtensions } from "./image-extensions.mjs";
|
|
24
|
+
|
|
25
|
+
const APP_PROPS = "docProps/app.xml";
|
|
26
|
+
|
|
27
|
+
export function normalizePptxAppProperties(buffer) {
|
|
28
|
+
let files;
|
|
29
|
+
try {
|
|
30
|
+
files = unzip(buffer);
|
|
31
|
+
} catch {
|
|
32
|
+
return { bytes: buffer, changed: false, slideCount: null };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!files["ppt/presentation.xml"]) {
|
|
36
|
+
return { bytes: buffer, changed: false, slideCount: null };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const order = slideOrderPaths(files);
|
|
40
|
+
const slideCount = order.length;
|
|
41
|
+
let entries = files;
|
|
42
|
+
let changed = false;
|
|
43
|
+
|
|
44
|
+
const before = readText(files, APP_PROPS);
|
|
45
|
+
if (before && slideCount > 0) {
|
|
46
|
+
let after = replaceElementText(before, "Slides", String(slideCount));
|
|
47
|
+
after = replaceSlideTitleHeadingCount(after, slideCount);
|
|
48
|
+
after = replaceTitlesOfParts(after, slideTitles(files, order));
|
|
49
|
+
if (after !== before) {
|
|
50
|
+
entries = { ...entries, [APP_PROPS]: new TextEncoder().encode(after) };
|
|
51
|
+
changed = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const idResult = normalizeMasterAndLayoutIds(entries);
|
|
56
|
+
if (idResult.changed) {
|
|
57
|
+
entries = idResult.entries;
|
|
58
|
+
changed = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (stripHiddenOleGraphicFrames(entries)) {
|
|
62
|
+
changed = true;
|
|
63
|
+
}
|
|
64
|
+
if (stripRiskyImageExtensions(entries)) {
|
|
65
|
+
changed = true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
bytes: changed ? zip(entries) : buffer,
|
|
70
|
+
changed,
|
|
71
|
+
slideCount,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function normalizePptxAppPropertiesBase64(base64) {
|
|
76
|
+
const normalized = normalizePptxAppProperties(Buffer.from(base64, "base64"));
|
|
77
|
+
if (!normalized.changed) {
|
|
78
|
+
return { ...normalized, base64 };
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
...normalized,
|
|
82
|
+
base64: Buffer.from(normalized.bytes).toString("base64"),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function slideTitles(files, order) {
|
|
87
|
+
return order.map((slidePath, i) => {
|
|
88
|
+
const xml = readText(files, slidePath) ?? "";
|
|
89
|
+
const { title, subtitle } = slideText(xml);
|
|
90
|
+
return title || subtitle || `Slide ${i + 1}`;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const LAYOUT_ID_FLOOR = 2147483648;
|
|
95
|
+
|
|
96
|
+
function normalizeMasterAndLayoutIds(files) {
|
|
97
|
+
const masterParts = Object.keys(files)
|
|
98
|
+
.filter((part) => /^ppt\/slideMasters\/slideMaster\d+\.xml$/.test(part))
|
|
99
|
+
.sort(numericPartSort);
|
|
100
|
+
if (masterParts.length === 0) return { entries: files, changed: false };
|
|
101
|
+
|
|
102
|
+
// sldMasterId values live in presentation.xml and are the fixed anchors of the
|
|
103
|
+
// shared id space — never renumbered here (that would mean rewriting
|
|
104
|
+
// presentation.xml, and a master-vs-master collision is the server's to fix).
|
|
105
|
+
const presentationXml = readText(files, "ppt/presentation.xml") ?? "";
|
|
106
|
+
const masterIds = [];
|
|
107
|
+
for (const tag of presentationXml.matchAll(/<p:sldMasterId\b[^>]*>/g)) {
|
|
108
|
+
const id = attr(tag[0], "id");
|
|
109
|
+
if (id !== null) masterIds.push(id);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const layoutIds = [];
|
|
113
|
+
for (const part of masterParts) {
|
|
114
|
+
const xml = readText(files, part) ?? "";
|
|
115
|
+
for (const tag of xml.matchAll(/<p:sldLayoutId\b[^>]*>/g)) {
|
|
116
|
+
const id = attr(tag[0], "id");
|
|
117
|
+
if (id !== null) layoutIds.push(id);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Inert unless the master and layout ids collide somewhere across the union —
|
|
122
|
+
// keeps well-formed (and server-fixed) output byte-stable.
|
|
123
|
+
const union = [...masterIds, ...layoutIds];
|
|
124
|
+
if (new Set(union).size === union.length) {
|
|
125
|
+
return { entries: files, changed: false };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const numericIds = union.map((id) => Number(id)).filter(Number.isFinite);
|
|
129
|
+
let nextId = numericIds.length ? Math.max(...numericIds) + 1 : LAYOUT_ID_FLOOR;
|
|
130
|
+
|
|
131
|
+
// Seed with the master anchors so a layout id equal to a master id is treated
|
|
132
|
+
// as a collision and moved off it. Then walk masters/layouts in document order,
|
|
133
|
+
// renumbering only ids already claimed.
|
|
134
|
+
const seen = new Set(masterIds);
|
|
135
|
+
let entries = files;
|
|
136
|
+
let changed = false;
|
|
137
|
+
|
|
138
|
+
for (const part of masterParts) {
|
|
139
|
+
const before = readText(files, part) ?? "";
|
|
140
|
+
const after = before.replace(/<p:sldLayoutId\b[^>]*>/g, (tag) => {
|
|
141
|
+
const id = attr(tag, "id");
|
|
142
|
+
if (id === null || !seen.has(id)) {
|
|
143
|
+
if (id !== null) seen.add(id);
|
|
144
|
+
return tag;
|
|
145
|
+
}
|
|
146
|
+
while (seen.has(String(nextId))) nextId += 1;
|
|
147
|
+
const replacement = String(nextId);
|
|
148
|
+
nextId += 1;
|
|
149
|
+
seen.add(replacement);
|
|
150
|
+
return replaceAttr(tag, "id", replacement);
|
|
151
|
+
});
|
|
152
|
+
if (after !== before) {
|
|
153
|
+
entries = { ...entries, [part]: new TextEncoder().encode(after) };
|
|
154
|
+
changed = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { entries, changed };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function replaceElementText(xml, tagName, value) {
|
|
162
|
+
return xml.replace(
|
|
163
|
+
new RegExp(`(<${tagName}>)[\\s\\S]*?(</${tagName}>)`),
|
|
164
|
+
`$1${value}$2`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function attr(tag, name) {
|
|
169
|
+
const m = new RegExp(`(^|\\s)${name}="([^"]*)"`).exec(tag);
|
|
170
|
+
return m ? m[2] : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function replaceAttr(tag, name, value) {
|
|
174
|
+
return tag.replace(
|
|
175
|
+
new RegExp(`(^|\\s)${name}="[^"]*"`),
|
|
176
|
+
`$1${name}="${value}"`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function numericPartSort(a, b) {
|
|
181
|
+
const na = Number(a.match(/(\d+)\.xml$/)?.[1] ?? 0);
|
|
182
|
+
const nb = Number(b.match(/(\d+)\.xml$/)?.[1] ?? 0);
|
|
183
|
+
return na - nb || a.localeCompare(b);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function replaceSlideTitleHeadingCount(xml, slideCount) {
|
|
187
|
+
return xml.replace(
|
|
188
|
+
/(<vt:variant>\s*<vt:lpstr>Slide Titles<\/vt:lpstr>\s*<\/vt:variant>\s*<vt:variant>\s*<vt:i4>)-?\d+(<\/vt:i4>\s*<\/vt:variant>)/,
|
|
189
|
+
`$1${slideCount}$2`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function replaceTitlesOfParts(xml, titles) {
|
|
194
|
+
return xml.replace(
|
|
195
|
+
/<TitlesOfParts>\s*<vt:vector\b[^>]*>[\s\S]*?<\/vt:vector>\s*<\/TitlesOfParts>/,
|
|
196
|
+
(match) => {
|
|
197
|
+
const themeName = firstLpstr(match) || "Theme";
|
|
198
|
+
const values = [themeName, ...titles]
|
|
199
|
+
.map((title) => `<vt:lpstr>${escapeXml(title)}</vt:lpstr>`)
|
|
200
|
+
.join("");
|
|
201
|
+
return (
|
|
202
|
+
`<TitlesOfParts><vt:vector size="${titles.length + 1}" baseType="lpstr">` +
|
|
203
|
+
values +
|
|
204
|
+
`</vt:vector></TitlesOfParts>`
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function firstLpstr(xml) {
|
|
211
|
+
const m = /<vt:lpstr>([\s\S]*?)<\/vt:lpstr>/.exec(xml);
|
|
212
|
+
return m ? decodeXmlEntities(m[1]) : null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function escapeXml(value) {
|
|
216
|
+
return String(value)
|
|
217
|
+
.replace(/&/g, "&")
|
|
218
|
+
.replace(/</g, "<")
|
|
219
|
+
.replace(/>/g, ">")
|
|
220
|
+
.replace(/"/g, """)
|
|
221
|
+
.replace(/'/g, "'");
|
|
222
|
+
}
|