bit-ppt-generator 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/AI_CONTENT_GUIDE.md +661 -0
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/assets/bit-campus-line.png +0 -0
- package/assets/bit-campus-photo.png +0 -0
- package/assets/bit-emblem-gray.png +0 -0
- package/assets/bit-seal-small.png +0 -0
- package/assets/bit-wordmark-white.png +0 -0
- package/bin/bit-ppt-http.mjs +58 -0
- package/bin/bit-ppt-mcp.mjs +27 -0
- package/bin/bit-ppt.mjs +480 -0
- package/content/body-layout-test.yaml +112 -0
- package/content/chart-flow-test.yaml +82 -0
- package/content/example.yaml +193 -0
- package/content/extended-layout-test.yaml +120 -0
- package/content/formula-test.yaml +31 -0
- package/content/image-layout-demo.yaml +64 -0
- package/content/inline-formula-test.yaml +62 -0
- package/content/invalid-deck-test.yaml +25 -0
- package/content/overflow-test.yaml +77 -0
- package/content/placeholder-image-demo.yaml +59 -0
- package/content/speaker-notes-demo.yaml +30 -0
- package/content/table-formula-test.yaml +21 -0
- package/package.json +42 -0
- package/src/core/layouts.mjs +58 -0
- package/src/core/preflight.mjs +263 -0
- package/src/core/validation.mjs +372 -0
- package/src/core/yaml-parse.mjs +80 -0
- package/src/generate.mjs +1708 -0
- package/src/http-server.mjs +1201 -0
- package/src/layout-guides.mjs +315 -0
- package/src/mcp-server.mjs +197 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { CHART_TYPES, SUPPORTED_LAYOUTS } from "./layouts.mjs";
|
|
2
|
+
import {
|
|
3
|
+
expandSlidesWithReport,
|
|
4
|
+
getSpeakerNotesValue,
|
|
5
|
+
hasKnownPlaceholderRatio,
|
|
6
|
+
imagePlaceholderNeedsVariants,
|
|
7
|
+
isPlaceholderImage,
|
|
8
|
+
normalizeSpeakerNotes,
|
|
9
|
+
normalizeText,
|
|
10
|
+
resolvePlaceholderRatio,
|
|
11
|
+
} from "./preflight.mjs";
|
|
12
|
+
|
|
13
|
+
function classifyImageRatio(ratio) {
|
|
14
|
+
if (!Number.isFinite(ratio) || ratio <= 0) return "unknown";
|
|
15
|
+
if (ratio >= 2.15) return "panoramic";
|
|
16
|
+
if (ratio >= 1.18) return "landscape";
|
|
17
|
+
if (ratio <= 0.52) return "tall";
|
|
18
|
+
if (ratio <= 0.84) return "portrait";
|
|
19
|
+
return "square";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function pureResolveImageInfo(value) {
|
|
23
|
+
if (isPlaceholderImage(value)) {
|
|
24
|
+
const aspectRatio = normalizeText(value.aspectRatio || value.ratio || value.size).toLowerCase();
|
|
25
|
+
const ratio = resolvePlaceholderRatio(aspectRatio);
|
|
26
|
+
return {
|
|
27
|
+
placeholder: true,
|
|
28
|
+
prompt: normalizeText(value.prompt || value.description || value.imagePrompt || value.alt),
|
|
29
|
+
aspectRatio,
|
|
30
|
+
exists: true,
|
|
31
|
+
format: "placeholder",
|
|
32
|
+
ratio,
|
|
33
|
+
orientation: classifyImageRatio(ratio),
|
|
34
|
+
uncertainRatio: !hasKnownPlaceholderRatio(aspectRatio),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
placeholder: false,
|
|
39
|
+
exists: true,
|
|
40
|
+
path: normalizeText(value),
|
|
41
|
+
ratio: 1,
|
|
42
|
+
orientation: "square",
|
|
43
|
+
unchecked: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function charCount(value) {
|
|
48
|
+
return [...normalizeText(value)].length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function lineCount(value) {
|
|
52
|
+
const text = normalizeText(value);
|
|
53
|
+
if (!text) return 0;
|
|
54
|
+
return text.split(/\r?\n/).length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function makeIssue(level, slideIndex, pathName, message, repair) {
|
|
58
|
+
return { level, slideIndex, path: pathName, message, repair };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validateDeck(deck, options = {}) {
|
|
62
|
+
const resolveImageInfo = options.resolveImageInfo || pureResolveImageInfo;
|
|
63
|
+
const issues = [];
|
|
64
|
+
const slides = Array.isArray(deck?.slides) ? deck.slides : [];
|
|
65
|
+
const add = (level, slideIndex, pathName, message, repair) => issues.push(makeIssue(level, slideIndex, pathName, message, repair));
|
|
66
|
+
if (!deck || typeof deck !== "object") {
|
|
67
|
+
return {
|
|
68
|
+
errors: [makeIssue("error", 0, "deck", "Input must be a YAML object.", "Rewrite the deck as an object with meta and slides.")],
|
|
69
|
+
warnings: [],
|
|
70
|
+
repairPrompt: "deck: Rewrite the deck as an object with meta and slides.",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (!Array.isArray(deck.slides)) {
|
|
74
|
+
add("error", 0, "slides", "`slides` must be an array.", "Return a top-level `slides` array.");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const maxText = (slideIndex, pathName, value, max, label = "text") => {
|
|
78
|
+
if (value === undefined || value === null || value === "") return;
|
|
79
|
+
const length = charCount(value);
|
|
80
|
+
if (length > max) add("warning", slideIndex, pathName, `${label} is ${length} chars; recommended max is ${max}.`, `Shorten ${pathName} to ${max} characters or split the slide.`);
|
|
81
|
+
};
|
|
82
|
+
const maxItems = (slideIndex, pathName, value, max) => {
|
|
83
|
+
if (!Array.isArray(value)) return;
|
|
84
|
+
if (value.length > max) add("warning", slideIndex, pathName, `${pathName} has ${value.length} items; recommended max is ${max}.`, `Keep only the strongest ${max} items or split into multiple slides.`);
|
|
85
|
+
};
|
|
86
|
+
const requireArray = (slideIndex, pathName, value) => {
|
|
87
|
+
if (value !== undefined && !Array.isArray(value)) add("error", slideIndex, pathName, `${pathName} must be an array.`, `Rewrite ${pathName} as a YAML list.`);
|
|
88
|
+
};
|
|
89
|
+
const checkBullets = (slideIndex, pathName, value, max = 5, textMax = 34) => {
|
|
90
|
+
requireArray(slideIndex, pathName, value);
|
|
91
|
+
maxItems(slideIndex, pathName, value, max);
|
|
92
|
+
if (Array.isArray(value)) value.forEach((item, idx) => maxText(slideIndex, `${pathName}[${idx}]`, item, textMax, "bullet"));
|
|
93
|
+
};
|
|
94
|
+
const checkImage = (slideIndex, pathName, value, imageOptions = {}) => {
|
|
95
|
+
if (value === undefined || value === null || value === "") return;
|
|
96
|
+
const image = resolveImageInfo(value);
|
|
97
|
+
if (image.placeholder) {
|
|
98
|
+
if (!image.prompt) add("warning", slideIndex, pathName, "Image placeholder has no prompt or description.", `Add ${pathName}.prompt so the placeholder tells the user what image to add later.`);
|
|
99
|
+
else maxText(slideIndex, `${pathName}.prompt`, image.prompt, 160, "image placeholder prompt");
|
|
100
|
+
if (image.uncertainRatio && !imageOptions.variants) add("warning", slideIndex, pathName, "Image placeholder aspect ratio is unknown.", "Set aspectRatio to 16:9, 4:3, 1:1, or 3:4.");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!image.exists) {
|
|
104
|
+
add("error", slideIndex, pathName, `Image file does not exist: ${image.path}.`, `Fix ${pathName} to point to an existing image file.`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (image.path && !image.unchecked && (!image.width || !image.height)) {
|
|
108
|
+
add("warning", slideIndex, pathName, `Image dimensions could not be read: ${image.path}.`, "Use PNG, JPEG, GIF, or WebP when automatic image layout is needed.");
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
slides.forEach((slide, idx) => {
|
|
113
|
+
const slideIndex = idx + 1;
|
|
114
|
+
if (!slide || typeof slide !== "object") {
|
|
115
|
+
add("error", slideIndex, "slide", "Each slide must be an object.", "Rewrite this slide as a YAML object with a `layout` field.");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!SUPPORTED_LAYOUTS.has(slide.layout)) {
|
|
119
|
+
add("error", slideIndex, "layout", `Unknown layout: ${slide.layout || "(missing)"}.`, `Use one of: ${[...SUPPORTED_LAYOUTS].join(", ")}.`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
maxText(slideIndex, "title", slide.title, 24, "title");
|
|
123
|
+
const speakerNotesRaw = getSpeakerNotesValue(slide);
|
|
124
|
+
if (speakerNotesRaw !== undefined) {
|
|
125
|
+
if (typeof speakerNotesRaw !== "string" && !Array.isArray(speakerNotesRaw)) {
|
|
126
|
+
add("warning", slideIndex, "speakerNotes", "speakerNotes should be a string block or an array of short strings.", "Rewrite speakerNotes as a YAML block scalar or string list.");
|
|
127
|
+
}
|
|
128
|
+
const speakerNotes = normalizeSpeakerNotes(speakerNotesRaw);
|
|
129
|
+
maxText(slideIndex, "speakerNotes", speakerNotes, 1200, "speaker notes");
|
|
130
|
+
if (lineCount(speakerNotes) > 30) add("warning", slideIndex, "speakerNotes", `speakerNotes has ${lineCount(speakerNotes)} lines; recommended max is 30.`, "Shorten the speaker script or split it across slides.");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
switch (slide.layout) {
|
|
134
|
+
case "agenda":
|
|
135
|
+
checkBullets(slideIndex, "items", slide.items, 7, 24);
|
|
136
|
+
break;
|
|
137
|
+
case "section":
|
|
138
|
+
maxText(slideIndex, "subtitle", slide.subtitle, 42, "subtitle");
|
|
139
|
+
break;
|
|
140
|
+
case "bullets":
|
|
141
|
+
maxText(slideIndex, "lead", slide.lead, 58, "lead");
|
|
142
|
+
checkBullets(slideIndex, "bullets", slide.bullets, 8, 38);
|
|
143
|
+
break;
|
|
144
|
+
case "claim":
|
|
145
|
+
maxText(slideIndex, "claim", slide.claim, 54, "claim");
|
|
146
|
+
checkBullets(slideIndex, "evidence", slide.evidence, 5, 38);
|
|
147
|
+
break;
|
|
148
|
+
case "twoColumn":
|
|
149
|
+
["left", "right"].forEach((side) => {
|
|
150
|
+
maxText(slideIndex, `${side}.title`, slide[side]?.title, 16, "column title");
|
|
151
|
+
maxText(slideIndex, `${side}.text`, slide[side]?.text, 105, "column text");
|
|
152
|
+
checkBullets(slideIndex, `${side}.bullets`, slide[side]?.bullets, 5, 34);
|
|
153
|
+
});
|
|
154
|
+
break;
|
|
155
|
+
case "cards":
|
|
156
|
+
maxItems(slideIndex, "cards", slide.cards, 6);
|
|
157
|
+
if (Array.isArray(slide.cards)) slide.cards.forEach((card, cardIdx) => {
|
|
158
|
+
maxText(slideIndex, `cards[${cardIdx}].title`, card.title, 14, "card title");
|
|
159
|
+
maxText(slideIndex, `cards[${cardIdx}].text`, card.text, 52, "card text");
|
|
160
|
+
});
|
|
161
|
+
break;
|
|
162
|
+
case "table":
|
|
163
|
+
requireArray(slideIndex, "columns", slide.columns);
|
|
164
|
+
requireArray(slideIndex, "rows", slide.rows);
|
|
165
|
+
if (Array.isArray(slide.columns) && slide.columns.length > 5) add("warning", slideIndex, "columns", `Table has ${slide.columns.length} columns; recommended max is 5.`, "Split wide tables by columns or use multiple slides.");
|
|
166
|
+
if (Array.isArray(slide.rows)) slide.rows.forEach((row, rowIdx) => {
|
|
167
|
+
if (!Array.isArray(row)) add("error", slideIndex, `rows[${rowIdx}]`, "Each table row must be an array.", `Rewrite rows[${rowIdx}] as a YAML list.`);
|
|
168
|
+
else row.forEach((cell, cellIdx) => maxText(slideIndex, `rows[${rowIdx}][${cellIdx}]`, cell, 42, "table cell"));
|
|
169
|
+
});
|
|
170
|
+
break;
|
|
171
|
+
case "comparison":
|
|
172
|
+
["left", "right"].forEach((side) => {
|
|
173
|
+
maxText(slideIndex, `${side}.title`, slide[side]?.title, 18, "comparison title");
|
|
174
|
+
checkBullets(slideIndex, `${side}.bullets`, slide[side]?.bullets, 5, 32);
|
|
175
|
+
});
|
|
176
|
+
break;
|
|
177
|
+
case "timeline":
|
|
178
|
+
maxItems(slideIndex, "items", slide.items, 6);
|
|
179
|
+
if (Array.isArray(slide.items)) slide.items.forEach((item, itemIdx) => {
|
|
180
|
+
maxText(slideIndex, `items[${itemIdx}].title`, item.title, 10, "timeline title");
|
|
181
|
+
maxText(slideIndex, `items[${itemIdx}].text`, item.text, 24, "timeline text");
|
|
182
|
+
});
|
|
183
|
+
break;
|
|
184
|
+
case "process":
|
|
185
|
+
maxItems(slideIndex, "steps", slide.steps, 5);
|
|
186
|
+
if (Array.isArray(slide.steps)) slide.steps.forEach((step, stepIdx) => {
|
|
187
|
+
maxText(slideIndex, `steps[${stepIdx}].title`, step.title, 8, "step title");
|
|
188
|
+
maxText(slideIndex, `steps[${stepIdx}].text`, step.text, 24, "step text");
|
|
189
|
+
});
|
|
190
|
+
break;
|
|
191
|
+
case "architecture":
|
|
192
|
+
maxItems(slideIndex, "layers", slide.layers, 4);
|
|
193
|
+
if (Array.isArray(slide.layers)) slide.layers.forEach((layer, layerIdx) => {
|
|
194
|
+
maxText(slideIndex, `layers[${layerIdx}].title`, layer.title, 8, "layer title");
|
|
195
|
+
maxItems(slideIndex, `layers[${layerIdx}].components`, layer.components, 5);
|
|
196
|
+
if (Array.isArray(layer.components)) layer.components.forEach((component, compIdx) => maxText(slideIndex, `layers[${layerIdx}].components[${compIdx}]`, component, 10, "component label"));
|
|
197
|
+
maxText(slideIndex, `layers[${layerIdx}].note`, layer.note, 44, "layer note");
|
|
198
|
+
});
|
|
199
|
+
break;
|
|
200
|
+
case "ablation":
|
|
201
|
+
maxText(slideIndex, "baseline", slide.baseline, 58, "baseline");
|
|
202
|
+
maxItems(slideIndex, "items", slide.items, 6);
|
|
203
|
+
if (Array.isArray(slide.items)) slide.items.forEach((item, itemIdx) => ["factor", "setting", "delta", "conclusion"].forEach((key) => maxText(slideIndex, `items[${itemIdx}].${key}`, item[key], key === "conclusion" ? 28 : 18, key)));
|
|
204
|
+
break;
|
|
205
|
+
case "caseStudy":
|
|
206
|
+
checkImage(slideIndex, "image", slide.image);
|
|
207
|
+
checkBullets(slideIndex, "context", slide.context, 2, 34);
|
|
208
|
+
checkBullets(slideIndex, "method", slide.method, 2, 34);
|
|
209
|
+
checkBullets(slideIndex, "result", slide.result, 2, 34);
|
|
210
|
+
maxText(slideIndex, "caption", slide.caption, 40, "caption");
|
|
211
|
+
break;
|
|
212
|
+
case "imageGrid":
|
|
213
|
+
maxItems(slideIndex, "images", slide.images, 6);
|
|
214
|
+
if (Array.isArray(slide.images)) slide.images.forEach((image, imageIdx) => {
|
|
215
|
+
checkImage(slideIndex, `images[${imageIdx}]`, image);
|
|
216
|
+
const caption = image && typeof image === "object" ? image.caption : "";
|
|
217
|
+
maxText(slideIndex, `images[${imageIdx}].caption`, caption, 16, "image caption");
|
|
218
|
+
});
|
|
219
|
+
break;
|
|
220
|
+
case "code":
|
|
221
|
+
if (lineCount(slide.code || slide.algorithm) > 12) add("warning", slideIndex, "code", `Code block has ${lineCount(slide.code || slide.algorithm)} lines; recommended max is 12.`, "Summarize the code as pseudocode under 12 short lines.");
|
|
222
|
+
normalizeText(slide.code || slide.algorithm).split(/\r?\n/).forEach((line, lineIdx) => maxText(slideIndex, `code line ${lineIdx + 1}`, line, 72, "code line"));
|
|
223
|
+
checkBullets(slideIndex, "notes", slide.notes, 5, 32);
|
|
224
|
+
break;
|
|
225
|
+
case "appendix":
|
|
226
|
+
maxItems(slideIndex, "items", slide.items, 8);
|
|
227
|
+
if (Array.isArray(slide.items)) slide.items.forEach((item, itemIdx) => {
|
|
228
|
+
maxText(slideIndex, `items[${itemIdx}].title`, item.title, 14, "appendix title");
|
|
229
|
+
maxText(slideIndex, `items[${itemIdx}].text`, item.text, 42, "appendix text");
|
|
230
|
+
});
|
|
231
|
+
break;
|
|
232
|
+
case "flowchart": {
|
|
233
|
+
requireArray(slideIndex, "nodes", slide.nodes);
|
|
234
|
+
if (!Array.isArray(slide.nodes) || !slide.nodes.length) add("error", slideIndex, "nodes", "Flowchart requires at least one node.", "Add a `nodes` list with id and text fields.");
|
|
235
|
+
maxItems(slideIndex, "nodes", slide.nodes, 10);
|
|
236
|
+
const ids = new Set();
|
|
237
|
+
if (Array.isArray(slide.nodes)) slide.nodes.forEach((node, nodeIdx) => {
|
|
238
|
+
const id = node.id || String(nodeIdx + 1);
|
|
239
|
+
if (ids.has(id)) add("error", slideIndex, `nodes[${nodeIdx}].id`, `Duplicate flowchart node id: ${id}.`, "Use unique ids for every flowchart node.");
|
|
240
|
+
ids.add(id);
|
|
241
|
+
maxText(slideIndex, `nodes[${nodeIdx}].text`, node.text || node.label, 12, "flowchart node text");
|
|
242
|
+
maxText(slideIndex, `nodes[${nodeIdx}].note`, node.note, 22, "flowchart node note");
|
|
243
|
+
});
|
|
244
|
+
if (Array.isArray(slide.edges)) slide.edges.forEach((edge, edgeIdx) => {
|
|
245
|
+
if (!ids.has(edge.from)) add("error", slideIndex, `edges[${edgeIdx}].from`, `Unknown flowchart edge source: ${edge.from}.`, "Point edge.from to an existing node id.");
|
|
246
|
+
if (!ids.has(edge.to)) add("error", slideIndex, `edges[${edgeIdx}].to`, `Unknown flowchart edge target: ${edge.to}.`, "Point edge.to to an existing node id.");
|
|
247
|
+
});
|
|
248
|
+
maxText(slideIndex, "note", slide.note, 56, "flowchart note");
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case "chart": {
|
|
252
|
+
const type = normalizeText(slide.type || "bar").toLowerCase();
|
|
253
|
+
if (!CHART_TYPES.has(type)) add("error", slideIndex, "type", `Unsupported chart type: ${slide.type}.`, "Use chart type bar, line, pie, doughnut, scatter, or area.");
|
|
254
|
+
const categories = slide.categories || slide.labels || [];
|
|
255
|
+
if (!Array.isArray(categories) || !categories.length) add("error", slideIndex, "categories", "Chart requires categories or labels.", "Add a `categories` list whose length matches every series values list.");
|
|
256
|
+
maxItems(slideIndex, "categories", categories, type === "pie" || type === "doughnut" ? 6 : 8);
|
|
257
|
+
if (Array.isArray(categories)) categories.forEach((category, catIdx) => maxText(slideIndex, `categories[${catIdx}]`, category, 18, "category label"));
|
|
258
|
+
if (slide.series !== undefined && !Array.isArray(slide.series)) add("error", slideIndex, "series", "`series` must be an array.", "Rewrite chart series as a YAML list.");
|
|
259
|
+
if (!Array.isArray(slide.series) && slide.values === undefined) add("error", slideIndex, "series", "Chart requires `series` or shortcut `values`.", "Add `series` with numeric values, or add a single `values` list.");
|
|
260
|
+
const seriesList = Array.isArray(slide.series) ? slide.series : slide.values !== undefined ? [{ name: slide.name || "Series", values: slide.values }] : [];
|
|
261
|
+
seriesList.forEach((series, seriesIdx) => {
|
|
262
|
+
maxText(slideIndex, `series[${seriesIdx}].name`, series.name, 16, "series name");
|
|
263
|
+
if (!Array.isArray(series.values)) add("error", slideIndex, `series[${seriesIdx}].values`, "Chart series values must be an array.", `Rewrite series[${seriesIdx}].values as a numeric list.`);
|
|
264
|
+
else {
|
|
265
|
+
if (Array.isArray(categories) && categories.length && series.values.length !== categories.length) add("error", slideIndex, `series[${seriesIdx}].values`, `Series has ${series.values.length} values but categories has ${categories.length}.`, "Make every chart series have the same number of values as categories.");
|
|
266
|
+
series.values.forEach((value, valueIdx) => {
|
|
267
|
+
if (!Number.isFinite(Number(value))) add("error", slideIndex, `series[${seriesIdx}].values[${valueIdx}]`, `Chart value is not numeric: ${value}.`, "Use numeric chart values only.");
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
maxText(slideIndex, "caption", slide.caption, 58, "chart caption");
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
case "problemSolution":
|
|
275
|
+
["problem", "solution", "impact"].forEach((key) => {
|
|
276
|
+
maxText(slideIndex, `${key}.title`, slide[key]?.title, 12, `${key} title`);
|
|
277
|
+
checkBullets(slideIndex, `${key}.bullets`, slide[key]?.bullets, 4, 26);
|
|
278
|
+
});
|
|
279
|
+
break;
|
|
280
|
+
case "painOpportunity":
|
|
281
|
+
["status", "pain", "opportunity"].forEach((key) => {
|
|
282
|
+
maxText(slideIndex, `${key}.title`, slide[key]?.title, 12, `${key} title`);
|
|
283
|
+
checkBullets(slideIndex, `${key}.bullets`, slide[key]?.bullets, 4, 30);
|
|
284
|
+
});
|
|
285
|
+
break;
|
|
286
|
+
case "experimentDesign":
|
|
287
|
+
["dataset", "variables", "metrics", "baselines"].forEach((key) => checkBullets(slideIndex, key, Array.isArray(slide[key]) ? slide[key] : slide[key] ? [slide[key]] : [], 4, 26));
|
|
288
|
+
maxItems(slideIndex, "procedure", slide.procedure, 5);
|
|
289
|
+
break;
|
|
290
|
+
case "resultAnalysis":
|
|
291
|
+
maxText(slideIndex, "finding", slide.finding, 52, "finding");
|
|
292
|
+
maxItems(slideIndex, "metrics", slide.metrics, 3);
|
|
293
|
+
checkBullets(slideIndex, "analysis", slide.analysis, 4, 38);
|
|
294
|
+
break;
|
|
295
|
+
case "riskMitigation":
|
|
296
|
+
maxItems(slideIndex, "items", slide.items, 5);
|
|
297
|
+
if (Array.isArray(slide.items)) slide.items.forEach((item, itemIdx) => ["risk", "impact", "mitigation"].forEach((key) => maxText(slideIndex, `items[${itemIdx}].${key}`, item[key], 28, key)));
|
|
298
|
+
break;
|
|
299
|
+
case "contribution":
|
|
300
|
+
maxItems(slideIndex, "items", slide.items, 4);
|
|
301
|
+
if (Array.isArray(slide.items)) slide.items.forEach((item, itemIdx) => {
|
|
302
|
+
maxText(slideIndex, `items[${itemIdx}].title`, item.title, 14, "contribution title");
|
|
303
|
+
maxText(slideIndex, `items[${itemIdx}].text`, item.text, 44, "contribution text");
|
|
304
|
+
});
|
|
305
|
+
break;
|
|
306
|
+
case "summary":
|
|
307
|
+
maxText(slideIndex, "takeaway", slide.takeaway, 52, "takeaway");
|
|
308
|
+
checkBullets(slideIndex, "points", slide.points, 5, 36);
|
|
309
|
+
break;
|
|
310
|
+
case "metrics":
|
|
311
|
+
maxItems(slideIndex, "metrics", slide.metrics, 4);
|
|
312
|
+
if (Array.isArray(slide.metrics)) slide.metrics.forEach((metric, metricIdx) => {
|
|
313
|
+
maxText(slideIndex, `metrics[${metricIdx}].value`, metric.value, 8, "metric value");
|
|
314
|
+
maxText(slideIndex, `metrics[${metricIdx}].label`, metric.label, 12, "metric label");
|
|
315
|
+
maxText(slideIndex, `metrics[${metricIdx}].note`, metric.note, 30, "metric note");
|
|
316
|
+
});
|
|
317
|
+
break;
|
|
318
|
+
case "matrix":
|
|
319
|
+
maxItems(slideIndex, "cells", slide.cells, 4);
|
|
320
|
+
if (Array.isArray(slide.cells)) slide.cells.forEach((cell, cellIdx) => {
|
|
321
|
+
maxText(slideIndex, `cells[${cellIdx}].title`, cell.title, 18, "matrix title");
|
|
322
|
+
maxText(slideIndex, `cells[${cellIdx}].text`, cell.text, 52, "matrix text");
|
|
323
|
+
});
|
|
324
|
+
break;
|
|
325
|
+
case "quote":
|
|
326
|
+
maxText(slideIndex, "quote", slide.quote, 70, "quote");
|
|
327
|
+
break;
|
|
328
|
+
case "formula":
|
|
329
|
+
if (!slide.formula || (typeof slide.formula === "object" && !slide.formula.latex)) add("warning", slideIndex, "formula", "Formula slide has no formula.", "Add formula.latex or change the slide layout.");
|
|
330
|
+
checkBullets(slideIndex, "explanation", slide.explanation || slide.notes, 4, 48);
|
|
331
|
+
break;
|
|
332
|
+
case "references":
|
|
333
|
+
requireArray(slideIndex, "items", slide.items);
|
|
334
|
+
if (Array.isArray(slide.items)) slide.items.forEach((item, itemIdx) => maxText(slideIndex, `items[${itemIdx}]`, item, 140, "reference"));
|
|
335
|
+
break;
|
|
336
|
+
case "imageText":
|
|
337
|
+
checkImage(slideIndex, "image", slide.image, { variants: imagePlaceholderNeedsVariants(slide) });
|
|
338
|
+
checkBullets(slideIndex, "text", slide.text, 5, 38);
|
|
339
|
+
break;
|
|
340
|
+
default:
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const errors = issues.filter((item) => item.level === "error");
|
|
346
|
+
const warnings = issues.filter((item) => item.level === "warning");
|
|
347
|
+
const repairPrompt = issues.length
|
|
348
|
+
? issues.map((item) => `Slide ${item.slideIndex} ${item.path}: ${item.repair || item.message}`).join("\n")
|
|
349
|
+
: "";
|
|
350
|
+
return { errors, warnings, repairPrompt };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function checkDeck(deck, options = {}) {
|
|
354
|
+
const validation = validateDeck(deck, options);
|
|
355
|
+
const preflightOnly = expandSlidesWithReport(Array.isArray(deck?.slides) ? deck.slides : []);
|
|
356
|
+
return {
|
|
357
|
+
inputSlides: Array.isArray(deck?.slides) ? deck.slides.length : 0,
|
|
358
|
+
outputSlides: preflightOnly.slides.length,
|
|
359
|
+
actions: preflightOnly.report,
|
|
360
|
+
validation: {
|
|
361
|
+
errors: validation.errors,
|
|
362
|
+
warnings: validation.warnings,
|
|
363
|
+
},
|
|
364
|
+
repairPrompt: validation.repairPrompt,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export {
|
|
369
|
+
checkDeck,
|
|
370
|
+
pureResolveImageInfo,
|
|
371
|
+
validateDeck,
|
|
372
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import YAML, { LineCounter } from "yaml";
|
|
2
|
+
|
|
3
|
+
function lineContext(source, line, radius = 2) {
|
|
4
|
+
const lines = String(source || "").replace(/\r\n?/g, "\n").split("\n");
|
|
5
|
+
const start = Math.max(1, line - radius);
|
|
6
|
+
const end = Math.min(lines.length, line + radius);
|
|
7
|
+
const width = String(end).length;
|
|
8
|
+
const context = [];
|
|
9
|
+
for (let current = start; current <= end; current += 1) {
|
|
10
|
+
context.push(`${String(current).padStart(width, " ")} | ${lines[current - 1] ?? ""}`);
|
|
11
|
+
}
|
|
12
|
+
return context.join("\n");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function pointerLine(column) {
|
|
16
|
+
return `${" ".repeat(Math.max(0, column - 1))}^`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makeYamlSyntaxError(source, errors, label = "YAML") {
|
|
20
|
+
const diagnostics = errors.map((error) => {
|
|
21
|
+
const start = error.linePos?.[0] || { line: 1, col: 1 };
|
|
22
|
+
const end = error.linePos?.[1] || start;
|
|
23
|
+
return {
|
|
24
|
+
level: "error",
|
|
25
|
+
code: error.code || error.name || "YAML_PARSE_ERROR",
|
|
26
|
+
message: error.message,
|
|
27
|
+
line: start.line,
|
|
28
|
+
column: start.col,
|
|
29
|
+
endLine: end.line,
|
|
30
|
+
endColumn: end.col,
|
|
31
|
+
context: lineContext(source, start.line),
|
|
32
|
+
pointer: pointerLine(start.col),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
const first = diagnostics[0] || {
|
|
36
|
+
code: "YAML_PARSE_ERROR",
|
|
37
|
+
message: "YAML could not be parsed.",
|
|
38
|
+
line: 1,
|
|
39
|
+
column: 1,
|
|
40
|
+
context: "",
|
|
41
|
+
pointer: "^",
|
|
42
|
+
};
|
|
43
|
+
const repairPrompt = diagnostics
|
|
44
|
+
.map((item) => [
|
|
45
|
+
`${label} syntax error at line ${item.line}, column ${item.column}: ${item.message}`,
|
|
46
|
+
item.context,
|
|
47
|
+
item.pointer,
|
|
48
|
+
"Fix the YAML syntax first, then keep the deck schema unchanged.",
|
|
49
|
+
].filter(Boolean).join("\n"))
|
|
50
|
+
.join("\n\n");
|
|
51
|
+
const wrapped = new Error(`${label} syntax error at line ${first.line}, column ${first.column}: ${first.message}`);
|
|
52
|
+
wrapped.statusCode = 400;
|
|
53
|
+
wrapped.kind = "yaml_syntax";
|
|
54
|
+
wrapped.syntax = {
|
|
55
|
+
errors: diagnostics,
|
|
56
|
+
};
|
|
57
|
+
wrapped.repairPrompt = repairPrompt;
|
|
58
|
+
return wrapped;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseDeckYaml(source, label = "YAML") {
|
|
62
|
+
const lineCounter = new LineCounter();
|
|
63
|
+
const doc = YAML.parseDocument(source, {
|
|
64
|
+
lineCounter,
|
|
65
|
+
prettyErrors: false,
|
|
66
|
+
});
|
|
67
|
+
if (doc.errors.length) {
|
|
68
|
+
for (const error of doc.errors) {
|
|
69
|
+
if (Array.isArray(error.pos)) {
|
|
70
|
+
error.linePos = error.pos.map((pos) => lineCounter.linePos(pos));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
throw makeYamlSyntaxError(source, doc.errors, label);
|
|
74
|
+
}
|
|
75
|
+
return doc.toJSON();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export {
|
|
79
|
+
parseDeckYaml,
|
|
80
|
+
};
|