slidemind 0.3.17 → 0.3.19
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/package.json +1 -1
- package/src/bridge/build-and-push.mjs +91 -37
- package/src/bridge/capture.mjs +11 -0
- package/src/builders/build.mjs +23 -23
- package/src/cli/deck.mjs +67 -6
- package/src/cli/help.mjs +5 -0
- package/src/lib/adopt.mjs +30 -24
- package/src/lib/conversions.mjs +108 -0
- package/src/lib/jsx-insert.mjs +115 -0
- package/src/lib/lint-converted.mjs +433 -0
- package/src/lib/text-fit.mjs +94 -0
- package/src/pptx/app-props.mjs +0 -4
- package/src/pptx/slide-swap.mjs +0 -3
- package/src/pptx/image-extensions.mjs +0 -190
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// How much room does this text actually need? — the cheap half of a fit check.
|
|
2
|
+
//
|
|
3
|
+
// Converted slides (`slidemind adopt`) keep PowerPoint's fixed text boxes:
|
|
4
|
+
// `autofit: "noAutofit"` means the box does NOT grow and PowerPoint does NOT
|
|
5
|
+
// shrink the text — an over-long line simply spills out of the shape and ships
|
|
6
|
+
// that way. Nothing in the local toolchain measured it, so 12pt bold overflow
|
|
7
|
+
// reached a delivered deck.
|
|
8
|
+
//
|
|
9
|
+
// Real measurement needs a browser; this is Arial's advance-width table
|
|
10
|
+
// instead (1000 units/em, the AFM metrics Arial shares with Helvetica), which
|
|
11
|
+
// is exact for the master's Arial runs and close enough elsewhere to catch a
|
|
12
|
+
// line that is 30% too long. ponytail: regular-weight table × a flat bold
|
|
13
|
+
// factor; swap in per-face metrics if a non-Arial brand ever adopts.
|
|
14
|
+
|
|
15
|
+
const PT_TO_PX = 96 / 72;
|
|
16
|
+
|
|
17
|
+
// prettier-ignore
|
|
18
|
+
const W = {
|
|
19
|
+
" ": 278, "!": 278, '"': 355, "#": 556, $: 556, "%": 889, "&": 667, "'": 191,
|
|
20
|
+
"(": 333, ")": 333, "*": 389, "+": 584, ",": 278, "-": 333, ".": 278, "/": 278,
|
|
21
|
+
0: 556, 1: 556, 2: 556, 3: 556, 4: 556, 5: 556, 6: 556, 7: 556, 8: 556, 9: 556,
|
|
22
|
+
":": 278, ";": 278, "<": 584, "=": 584, ">": 584, "?": 556, "@": 1015,
|
|
23
|
+
A: 667, B: 667, C: 722, D: 722, E: 667, F: 611, G: 778, H: 722, I: 278, J: 500,
|
|
24
|
+
K: 667, L: 556, M: 833, N: 722, O: 778, P: 667, Q: 778, R: 722, S: 667, T: 611,
|
|
25
|
+
U: 722, V: 667, W: 944, X: 667, Y: 667, Z: 611,
|
|
26
|
+
"[": 278, "\\": 278, "]": 278, "^": 469, _: 556, "`": 333,
|
|
27
|
+
a: 556, b: 556, c: 500, d: 556, e: 556, f: 278, g: 556, h: 556, i: 222, j: 222,
|
|
28
|
+
k: 500, l: 222, m: 833, n: 556, o: 556, p: 556, q: 556, r: 333, s: 500, t: 278,
|
|
29
|
+
u: 556, v: 500, w: 722, x: 500, y: 500, z: 500,
|
|
30
|
+
"{": 334, "|": 260, "}": 334, "~": 584,
|
|
31
|
+
// Latin-1 / typographic characters the master's German and English copy uses.
|
|
32
|
+
"\u00A0": 278, "«": 556, "»": 556, "°": 400, "·": 278,
|
|
33
|
+
"×": 584, "÷": 584, "©": 737, "®": 737, "–": 556,
|
|
34
|
+
"—": 1000, "‘": 222, "’": 222, "‚": 222, "“": 333,
|
|
35
|
+
"”": 333, "„": 333, "…": 1000, "€": 556, "ß": 556,
|
|
36
|
+
"£": 556, "§": 556, "•": 350, "−": 584, "\u00AD": 333,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Accented letters carry their base letter's advance in Arial (the diacritic
|
|
40
|
+
// sits above the glyph box), so strip the combining marks and reuse the table.
|
|
41
|
+
function widthOf(ch) {
|
|
42
|
+
const w = W[ch];
|
|
43
|
+
if (w !== undefined) return w;
|
|
44
|
+
const base = ch.normalize("NFD")[0];
|
|
45
|
+
return W[base] ?? 556;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Rendered width of `text` in px at `pt`, Arial metrics. */
|
|
49
|
+
export function textWidth(text, pt, { bold = false } = {}) {
|
|
50
|
+
let units = 0;
|
|
51
|
+
for (const ch of String(text ?? "")) units += widthOf(ch);
|
|
52
|
+
return (units / 1000) * pt * PT_TO_PX * (bold ? 1.07 : 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* How many lines `text` takes in a box `widthPx` wide — greedy word wrap, the
|
|
57
|
+
* way PowerPoint breaks (on spaces, and inside a word that cannot fit alone).
|
|
58
|
+
* Always at least 1.
|
|
59
|
+
*/
|
|
60
|
+
export function estimateLines(text, pt, widthPx, { bold = false } = {}) {
|
|
61
|
+
const str = String(text ?? "");
|
|
62
|
+
if (!str.trim()) return 1;
|
|
63
|
+
if (!(widthPx > 0)) return 1;
|
|
64
|
+
const opts = { bold };
|
|
65
|
+
let lines = 1;
|
|
66
|
+
let used = 0;
|
|
67
|
+
for (const word of str.split(/(\s+)/)) {
|
|
68
|
+
if (!word) continue;
|
|
69
|
+
const w = textWidth(word, pt, opts);
|
|
70
|
+
if (/^\s+$/.test(word)) {
|
|
71
|
+
// Trailing whitespace never pushes a wrap on its own.
|
|
72
|
+
used += w;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (used > 0 && used + w > widthPx) {
|
|
76
|
+
lines++;
|
|
77
|
+
used = 0;
|
|
78
|
+
}
|
|
79
|
+
if (w > widthPx) {
|
|
80
|
+
// A single word wider than the box breaks mid-word.
|
|
81
|
+
for (const ch of word) {
|
|
82
|
+
const cw = textWidth(ch, pt, opts);
|
|
83
|
+
if (used > 0 && used + cw > widthPx) {
|
|
84
|
+
lines++;
|
|
85
|
+
used = 0;
|
|
86
|
+
}
|
|
87
|
+
used += cw;
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
used += w;
|
|
92
|
+
}
|
|
93
|
+
return lines;
|
|
94
|
+
}
|
package/src/pptx/app-props.mjs
CHANGED
|
@@ -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,
|
package/src/pptx/slide-swap.mjs
CHANGED
|
@@ -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, "\\$&");
|