starry-slides 0.1.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/LICENSE +10 -0
- package/README.md +131 -0
- package/dist/assets/index---ub3Dty.css +1 -0
- package/dist/assets/index-7Xs_Dyp0.js +354 -0
- package/dist/assets/index-8ul7coaw.js +451 -0
- package/dist/assets/index-B4_bJa9t.js +412 -0
- package/dist/assets/index-B9p6L9Wx.js +324 -0
- package/dist/assets/index-BF-Vi1Nh.js +413 -0
- package/dist/assets/index-BKvVbgXz.css +1 -0
- package/dist/assets/index-BNZJ_Wz-.js +417 -0
- package/dist/assets/index-BO2gtiKn.js +413 -0
- package/dist/assets/index-BYpD8kgo.js +456 -0
- package/dist/assets/index-BaZRfz_9.css +1 -0
- package/dist/assets/index-BlsWm4PI.js +412 -0
- package/dist/assets/index-BnUIBFtw.js +393 -0
- package/dist/assets/index-BvhCbfCi.js +412 -0
- package/dist/assets/index-BxEkxfy1.js +412 -0
- package/dist/assets/index-BzrA7O0L.js +422 -0
- package/dist/assets/index-C0xGkdRg.js +413 -0
- package/dist/assets/index-C1Rjncf1.js +456 -0
- package/dist/assets/index-CCgb2gqc.js +413 -0
- package/dist/assets/index-CDFJQRFx.js +413 -0
- package/dist/assets/index-CEOai0RW.js +456 -0
- package/dist/assets/index-CG2uWTey.css +1 -0
- package/dist/assets/index-CLe3_iTu.js +412 -0
- package/dist/assets/index-CS_optob.js +324 -0
- package/dist/assets/index-CXgwXOZH.css +1 -0
- package/dist/assets/index-CcBvAcE2.js +408 -0
- package/dist/assets/index-CllmR_MT.js +408 -0
- package/dist/assets/index-CnMy6wxq.js +412 -0
- package/dist/assets/index-CoWNjYgb.js +422 -0
- package/dist/assets/index-CtfA3BPy.css +1 -0
- package/dist/assets/index-D-JiuJIv.js +324 -0
- package/dist/assets/index-D2uEYXyQ.css +1 -0
- package/dist/assets/index-D70beOHW.js +456 -0
- package/dist/assets/index-D8xuxnF1.js +456 -0
- package/dist/assets/index-DKAtkRd8.css +1 -0
- package/dist/assets/index-DU3l9_k0.css +1 -0
- package/dist/assets/index-DaLY98jk.js +456 -0
- package/dist/assets/index-DkNHJHQl.js +412 -0
- package/dist/assets/index-DmFnifK8.js +456 -0
- package/dist/assets/index-DndZR7Ds.css +1 -0
- package/dist/assets/index-DuRa7Y6g.js +324 -0
- package/dist/assets/index-EnDjTeHF.js +451 -0
- package/dist/assets/index-J3nQgjqJ.js +451 -0
- package/dist/assets/index-wD_iWTBn.js +413 -0
- package/dist/chunk-AK5J4CXH.js +1671 -0
- package/dist/chunk-ARFDESSF.js +1583 -0
- package/dist/chunk-DHWTBXGS.js +985 -0
- package/dist/chunk-FCRRFL7N.js +1571 -0
- package/dist/chunk-J7W4Y7WJ.js +1620 -0
- package/dist/chunk-OCJULB7Z.js +1140 -0
- package/dist/chunk-WCJWV5SO.js +36 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +1119 -0
- package/dist/cli/index.test.d.ts +1 -0
- package/dist/core/deck-slide-operations.d.ts +5 -0
- package/dist/core/generated-deck.d.ts +8 -0
- package/dist/core/generated-deck.test.d.ts +1 -0
- package/dist/core/group-operations.d.ts +24 -0
- package/dist/core/history.d.ts +20 -0
- package/dist/core/history.test.d.ts +1 -0
- package/dist/core/html-export.d.ts +20 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/layout.d.ts +36 -0
- package/dist/core/layout.test.d.ts +1 -0
- package/dist/core/pdf-export.d.ts +20 -0
- package/dist/core/presentation.d.ts +7 -0
- package/dist/core/slide-contract.d.ts +41 -0
- package/dist/core/slide-document.d.ts +5 -0
- package/dist/core/slide-document.test.d.ts +1 -0
- package/dist/core/slide-html-document.d.ts +3 -0
- package/dist/core/slide-operation-reducer.d.ts +4 -0
- package/dist/core/slide-operation-reducer.test.d.ts +1 -0
- package/dist/core/slide-operation-types.d.ts +123 -0
- package/dist/core/slide-operations-helpers.d.ts +14 -0
- package/dist/core/slide-operations.d.ts +5 -0
- package/dist/core/slide-operations.test.d.ts +1 -0
- package/dist/core/verify-deck.d.ts +48 -0
- package/dist/core/verify-deck.test.d.ts +1 -0
- package/dist/editor/app/App.d.ts +2 -0
- package/dist/editor/app/main.d.ts +1 -0
- package/dist/editor/app/use-slides-data.d.ts +14 -0
- package/dist/editor/components/block-manipulation-overlay.d.ts +32 -0
- package/dist/editor/components/color-picker.d.ts +10 -0
- package/dist/editor/components/context-menu.d.ts +26 -0
- package/dist/editor/components/editor-header.d.ts +18 -0
- package/dist/editor/components/floating-toolbar-feature.d.ts +8 -0
- package/dist/editor/components/floating-toolbar-parts.d.ts +46 -0
- package/dist/editor/components/floating-toolbar.d.ts +27 -0
- package/dist/editor/components/presenter-view.d.ts +13 -0
- package/dist/editor/components/slide-sidebar.d.ts +18 -0
- package/dist/editor/components/stage-canvas.d.ts +77 -0
- package/dist/editor/components/ui/accordion.d.ts +7 -0
- package/dist/editor/components/ui/button.d.ts +10 -0
- package/dist/editor/components/ui/context-menu.d.ts +25 -0
- package/dist/editor/components/ui/dialog.d.ts +10 -0
- package/dist/editor/components/ui/input.d.ts +3 -0
- package/dist/editor/components/ui/popover.d.ts +10 -0
- package/dist/editor/components/ui/scroll-area.d.ts +5 -0
- package/dist/editor/components/ui/select.d.ts +15 -0
- package/dist/editor/components/ui/separator.d.ts +4 -0
- package/dist/editor/components/ui/tabs.d.ts +11 -0
- package/dist/editor/components/ui/textarea.d.ts +3 -0
- package/dist/editor/components/ui/toggle-group.d.ts +9 -0
- package/dist/editor/components/ui/toggle.d.ts +9 -0
- package/dist/editor/components/ui/tooltip.d.ts +7 -0
- package/dist/editor/editor-operations.d.ts +15 -0
- package/dist/editor/hooks/block-manipulation-geometry.d.ts +16 -0
- package/dist/editor/hooks/block-manipulation-operations.d.ts +10 -0
- package/dist/editor/hooks/block-manipulation-overlay.d.ts +11 -0
- package/dist/editor/hooks/block-manipulation-types.d.ts +73 -0
- package/dist/editor/hooks/editor-keyboard-geometry.d.ts +22 -0
- package/dist/editor/hooks/editor-keyboard-operations.d.ts +10 -0
- package/dist/editor/hooks/editor-keyboard-types.d.ts +35 -0
- package/dist/editor/hooks/iframe-editing-session.d.ts +10 -0
- package/dist/editor/hooks/iframe-text-editing-dom.d.ts +6 -0
- package/dist/editor/hooks/iframe-text-editing-types.d.ts +38 -0
- package/dist/editor/hooks/object-clipboard-commands.d.ts +28 -0
- package/dist/editor/hooks/use-block-manipulation.d.ts +3 -0
- package/dist/editor/hooks/use-editor-keyboard-shortcuts.d.ts +3 -0
- package/dist/editor/hooks/use-iframe-text-editing.d.ts +3 -0
- package/dist/editor/hooks/use-slide-history.d.ts +17 -0
- package/dist/editor/hooks/use-slide-inspector.d.ts +24 -0
- package/dist/editor/hooks/use-slide-thumbnails.d.ts +2 -0
- package/dist/editor/hooks/use-stage-viewport.d.ts +13 -0
- package/dist/editor/index.d.ts +13 -0
- package/dist/editor/index.js +8260 -0
- package/dist/editor/lib/block-snap-constants.d.ts +8 -0
- package/dist/editor/lib/block-snap-guides.d.ts +6 -0
- package/dist/editor/lib/block-snap-targets.d.ts +13 -0
- package/dist/editor/lib/block-snap-types.d.ts +30 -0
- package/dist/editor/lib/block-snapping.d.ts +17 -0
- package/dist/editor/lib/collect-css-properties.d.ts +5 -0
- package/dist/editor/lib/element-tool-commit.d.ts +18 -0
- package/dist/editor/lib/element-tool-model.d.ts +16 -0
- package/dist/editor/lib/element-tool-types.d.ts +35 -0
- package/dist/editor/lib/element-tool-values.d.ts +23 -0
- package/dist/editor/lib/motion.d.ts +8 -0
- package/dist/editor/lib/selection-overlay.d.ts +4 -0
- package/dist/editor/lib/style-controls.d.ts +17 -0
- package/dist/editor/lib/thumbnail-renderer.d.ts +2 -0
- package/dist/editor/lib/utils.d.ts +3 -0
- package/dist/index.html +13 -0
- package/dist/node/deck-runtime-middleware.d.ts +8 -0
- package/dist/node/deck-source.d.ts +1 -0
- package/dist/node/html-export.d.ts +16 -0
- package/dist/node/open-browser.d.ts +1 -0
- package/dist/node/pdf-export.d.ts +20 -0
- package/dist/node/ports.d.ts +1 -0
- package/dist/node/view-renderer.d.ts +31 -0
- package/dist/runtime/deck-source.d.ts +1 -0
- package/dist/runtime/html-export.d.ts +16 -0
- package/dist/runtime/open-browser.d.ts +1 -0
- package/dist/runtime/pdf-export.d.ts +20 -0
- package/dist/runtime/ports.d.ts +1 -0
- package/dist/runtime/view-renderer.d.ts +31 -0
- package/dist/runtime/view-renderer.test.d.ts +1 -0
- package/dist/test/deck-fixtures.d.ts +14 -0
- package/package.json +94 -0
|
@@ -0,0 +1,1119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_SLIDE_HEIGHT,
|
|
4
|
+
DEFAULT_SLIDE_WIDTH,
|
|
5
|
+
SELECTOR_ATTR,
|
|
6
|
+
SLIDE_ROOT_ATTR,
|
|
7
|
+
createSingleHtmlExportDocument,
|
|
8
|
+
parseDimension,
|
|
9
|
+
planHtmlExportSlides,
|
|
10
|
+
planPdfExport
|
|
11
|
+
} from "../chunk-J7W4Y7WJ.js";
|
|
12
|
+
|
|
13
|
+
// src/cli/index.ts
|
|
14
|
+
import { spawn as spawn2 } from "child_process";
|
|
15
|
+
|
|
16
|
+
// src/core/verify-deck.ts
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import path from "path";
|
|
19
|
+
import { JSDOM, VirtualConsole } from "jsdom";
|
|
20
|
+
function createVerifyIssue(severity, code, message, details) {
|
|
21
|
+
const slideFile = typeof details?.slideFile === "string" ? details.slideFile : void 0;
|
|
22
|
+
const selector = typeof details?.selector === "string" ? details.selector : void 0;
|
|
23
|
+
return {
|
|
24
|
+
severity,
|
|
25
|
+
code,
|
|
26
|
+
message,
|
|
27
|
+
...slideFile ? { slideFile } : {},
|
|
28
|
+
...selector ? { selector } : {},
|
|
29
|
+
...details ? { details } : {}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function issue(severity, code, message, details) {
|
|
33
|
+
return createVerifyIssue(severity, code, message, details);
|
|
34
|
+
}
|
|
35
|
+
function collectHtmlFiles(targetPath) {
|
|
36
|
+
const stat = fs.statSync(targetPath);
|
|
37
|
+
if (stat.isFile()) {
|
|
38
|
+
return targetPath.endsWith(".html") ? [targetPath] : [];
|
|
39
|
+
}
|
|
40
|
+
const files = [];
|
|
41
|
+
for (const entry of fs.readdirSync(targetPath, { withFileTypes: true })) {
|
|
42
|
+
const entryPath = path.join(targetPath, entry.name);
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
files.push(...collectHtmlFiles(entryPath));
|
|
45
|
+
} else if (entry.isFile() && entry.name.endsWith(".html")) {
|
|
46
|
+
files.push(entryPath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return files.sort();
|
|
50
|
+
}
|
|
51
|
+
function parseManifest(manifestPath) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
error instanceof Error ? `invalid manifest.json: ${error.message}` : "invalid manifest.json"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function validateSlideHtml(_filePath, slideFile, html) {
|
|
61
|
+
const dom = new JSDOM(html, { virtualConsole: new VirtualConsole() });
|
|
62
|
+
const { document } = dom.window;
|
|
63
|
+
const issues = [];
|
|
64
|
+
const roots = Array.from(document.querySelectorAll(`[${SLIDE_ROOT_ATTR}]`));
|
|
65
|
+
if (roots.length === 0) {
|
|
66
|
+
issues.push(
|
|
67
|
+
issue("error", "structure.missing-root", "missing required slide root", {
|
|
68
|
+
slideFile
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (roots.length > 1) {
|
|
73
|
+
issues.push(
|
|
74
|
+
issue("error", "structure.multiple-roots", "found multiple slide roots", {
|
|
75
|
+
slideFile
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const root = roots[0] ?? null;
|
|
80
|
+
if (root) {
|
|
81
|
+
if (!root.getAttribute("data-slide-width")) {
|
|
82
|
+
issues.push(
|
|
83
|
+
issue(
|
|
84
|
+
"warning",
|
|
85
|
+
"structure.missing-width",
|
|
86
|
+
"missing data-slide-width, default 1920 will be assumed",
|
|
87
|
+
{
|
|
88
|
+
slideFile
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (!root.getAttribute("data-slide-height")) {
|
|
94
|
+
issues.push(
|
|
95
|
+
issue(
|
|
96
|
+
"warning",
|
|
97
|
+
"structure.missing-height",
|
|
98
|
+
"missing data-slide-height, default 1080 will be assumed",
|
|
99
|
+
{ slideFile }
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const editableNodes = Array.from(document.querySelectorAll("[data-editable]"));
|
|
105
|
+
if (editableNodes.length === 0) {
|
|
106
|
+
issues.push(
|
|
107
|
+
issue("warning", "structure.empty-slide", "slide contains no editable nodes", {
|
|
108
|
+
slideFile
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
for (const node of editableNodes) {
|
|
113
|
+
const editableType = node.getAttribute("data-editable") ?? "";
|
|
114
|
+
if (!["text", "image", "block"].includes(editableType)) {
|
|
115
|
+
issues.push(
|
|
116
|
+
issue(
|
|
117
|
+
"error",
|
|
118
|
+
"structure.invalid-editable",
|
|
119
|
+
`invalid data-editable value "${editableType}" on <${node.tagName.toLowerCase()}>`,
|
|
120
|
+
{
|
|
121
|
+
slideFile,
|
|
122
|
+
selector: node.getAttribute(SELECTOR_ATTR) ?? void 0
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (node.getAttribute("data-group") === "true" && editableType !== "block") {
|
|
128
|
+
issues.push(
|
|
129
|
+
issue(
|
|
130
|
+
"error",
|
|
131
|
+
"structure.invalid-group",
|
|
132
|
+
'data-group="true" is only allowed on block editables',
|
|
133
|
+
{
|
|
134
|
+
slideFile,
|
|
135
|
+
selector: node.getAttribute(SELECTOR_ATTR) ?? void 0
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return issues;
|
|
142
|
+
}
|
|
143
|
+
function allowsOverflow(node) {
|
|
144
|
+
return Boolean(node.closest('[data-allow-overflow="true"]'));
|
|
145
|
+
}
|
|
146
|
+
function validateStaticOverflow(_filePath, slideFile, html) {
|
|
147
|
+
const dom = new JSDOM(html, { virtualConsole: new VirtualConsole() });
|
|
148
|
+
const { document } = dom.window;
|
|
149
|
+
const issues = [];
|
|
150
|
+
const candidates = [
|
|
151
|
+
document.querySelector(`[${SLIDE_ROOT_ATTR}]`),
|
|
152
|
+
...Array.from(document.querySelectorAll("[data-editable]"))
|
|
153
|
+
].filter((node) => Boolean(node));
|
|
154
|
+
for (const node of candidates) {
|
|
155
|
+
if (allowsOverflow(node)) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const overflow = node.style.overflow.trim().toLowerCase();
|
|
159
|
+
const overflowX = node.style.overflowX.trim().toLowerCase();
|
|
160
|
+
const overflowY = node.style.overflowY.trim().toLowerCase();
|
|
161
|
+
const hasExplicitOverflow = ["auto", "scroll"].includes(overflow) || ["auto", "scroll"].includes(overflowX) || ["auto", "scroll"].includes(overflowY);
|
|
162
|
+
if (!hasExplicitOverflow) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
issues.push(
|
|
166
|
+
issue("error", "overflow.static", "explicit scrolling overflow is not allowed", {
|
|
167
|
+
slideFile,
|
|
168
|
+
selector: node.getAttribute(SELECTOR_ATTR) ?? void 0
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return issues;
|
|
173
|
+
}
|
|
174
|
+
function loadVerifyDeckSource(deckPath) {
|
|
175
|
+
const deck = path.resolve(process.cwd(), deckPath);
|
|
176
|
+
const manifestPath = path.join(deck, "manifest.json");
|
|
177
|
+
const issues = [];
|
|
178
|
+
if (!fs.existsSync(deck)) {
|
|
179
|
+
return {
|
|
180
|
+
deck,
|
|
181
|
+
manifestPath,
|
|
182
|
+
manifest: null,
|
|
183
|
+
slideFiles: [],
|
|
184
|
+
issues: [issue("error", "structure.missing-deck", "deck path does not exist")]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (!fs.existsSync(manifestPath)) {
|
|
188
|
+
return {
|
|
189
|
+
deck,
|
|
190
|
+
manifestPath,
|
|
191
|
+
manifest: null,
|
|
192
|
+
slideFiles: [],
|
|
193
|
+
issues: [
|
|
194
|
+
issue("error", "structure.missing-manifest", "deck package is missing manifest.json")
|
|
195
|
+
]
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
let manifest = null;
|
|
199
|
+
try {
|
|
200
|
+
manifest = parseManifest(manifestPath);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return {
|
|
203
|
+
deck,
|
|
204
|
+
manifestPath,
|
|
205
|
+
manifest: null,
|
|
206
|
+
slideFiles: [],
|
|
207
|
+
issues: [
|
|
208
|
+
issue(
|
|
209
|
+
"error",
|
|
210
|
+
"structure.invalid-manifest",
|
|
211
|
+
error instanceof Error ? error.message : "invalid manifest.json"
|
|
212
|
+
)
|
|
213
|
+
]
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const slideFiles = [];
|
|
217
|
+
const manifestSlidePaths = /* @__PURE__ */ new Set();
|
|
218
|
+
if (!Array.isArray(manifest?.slides) || !manifest?.slides?.length) {
|
|
219
|
+
issues.push(
|
|
220
|
+
issue("error", "structure.empty-manifest", "manifest.json must include at least one slide")
|
|
221
|
+
);
|
|
222
|
+
} else {
|
|
223
|
+
for (const [index, slide] of manifest.slides.entries()) {
|
|
224
|
+
if ("hidden" in slide && typeof slide.hidden !== "boolean") {
|
|
225
|
+
issues.push(
|
|
226
|
+
issue(
|
|
227
|
+
"error",
|
|
228
|
+
"structure.invalid-slide-hidden",
|
|
229
|
+
`manifest slide ${index + 1} hidden must be a boolean when present`,
|
|
230
|
+
{
|
|
231
|
+
slideIndex: index,
|
|
232
|
+
...typeof slide.file === "string" ? { slideFile: slide.file } : {}
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (typeof slide.file !== "string" || !slide.file.trim()) {
|
|
238
|
+
issues.push(
|
|
239
|
+
issue(
|
|
240
|
+
"error",
|
|
241
|
+
"structure.missing-slide-file",
|
|
242
|
+
`manifest slide ${index + 1} is missing file`,
|
|
243
|
+
{ slideIndex: index }
|
|
244
|
+
)
|
|
245
|
+
);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const slidePath = path.resolve(deck, slide.file);
|
|
249
|
+
if (slidePath !== deck && !slidePath.startsWith(`${deck}${path.sep}`)) {
|
|
250
|
+
issues.push(
|
|
251
|
+
issue(
|
|
252
|
+
"error",
|
|
253
|
+
"structure.slide-escape",
|
|
254
|
+
`manifest slide escapes deck directory: ${slide.file}`,
|
|
255
|
+
{
|
|
256
|
+
slideFile: slide.file
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (!fs.existsSync(slidePath)) {
|
|
263
|
+
issues.push(
|
|
264
|
+
issue(
|
|
265
|
+
"error",
|
|
266
|
+
"structure.missing-slide",
|
|
267
|
+
`manifest slide file does not exist: ${slide.file}`,
|
|
268
|
+
{
|
|
269
|
+
slideFile: slide.file
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
slideFiles.push(slide.file);
|
|
276
|
+
manifestSlidePaths.add(slidePath);
|
|
277
|
+
const html = fs.readFileSync(slidePath, "utf8");
|
|
278
|
+
issues.push(...validateSlideHtml(slidePath, slide.file, html));
|
|
279
|
+
issues.push(...validateStaticOverflow(slidePath, slide.file, html));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const filePath of collectHtmlFiles(deck)) {
|
|
283
|
+
if (manifestSlidePaths.has(filePath)) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const slideFile = path.relative(deck, filePath);
|
|
287
|
+
const html = fs.readFileSync(filePath, "utf8");
|
|
288
|
+
issues.push(...validateSlideHtml(filePath, slideFile, html));
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
deck,
|
|
292
|
+
manifestPath,
|
|
293
|
+
manifest,
|
|
294
|
+
slideFiles,
|
|
295
|
+
issues
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function createVerifyResult({
|
|
299
|
+
deck,
|
|
300
|
+
mode,
|
|
301
|
+
checks,
|
|
302
|
+
issues
|
|
303
|
+
}) {
|
|
304
|
+
const errorCount = issues.filter((item) => item.severity === "error").length;
|
|
305
|
+
const warningCount = issues.filter((item) => item.severity === "warning").length;
|
|
306
|
+
return {
|
|
307
|
+
ok: errorCount === 0,
|
|
308
|
+
deck,
|
|
309
|
+
mode,
|
|
310
|
+
checks,
|
|
311
|
+
issues,
|
|
312
|
+
summary: {
|
|
313
|
+
errorCount,
|
|
314
|
+
warningCount
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function verifyDeck(deckPath, options = {}) {
|
|
319
|
+
const source = loadVerifyDeckSource(deckPath);
|
|
320
|
+
const mode = options.mode ?? "static";
|
|
321
|
+
const renderedIssues = mode === "complete" ? options.renderedIssues ?? [] : [];
|
|
322
|
+
const issues = [...source.issues, ...renderedIssues];
|
|
323
|
+
return createVerifyResult({
|
|
324
|
+
deck: source.deck,
|
|
325
|
+
mode,
|
|
326
|
+
checks: mode === "complete" ? ["structure", "static-overflow", "rendered-overflow"] : ["structure", "static-overflow"],
|
|
327
|
+
issues
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/node/deck-source.ts
|
|
332
|
+
import path2 from "path";
|
|
333
|
+
function resolveDeckPath(deckPath) {
|
|
334
|
+
if (deckPath?.trim()) {
|
|
335
|
+
return path2.resolve(process.cwd(), deckPath);
|
|
336
|
+
}
|
|
337
|
+
return path2.resolve(import.meta.dirname, "../../sample-slides");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/node/html-export.ts
|
|
341
|
+
import fs3 from "fs";
|
|
342
|
+
import path4 from "path";
|
|
343
|
+
|
|
344
|
+
// src/node/view-renderer.ts
|
|
345
|
+
import fs2 from "fs";
|
|
346
|
+
import { createRequire } from "module";
|
|
347
|
+
import path3 from "path";
|
|
348
|
+
import { pathToFileURL } from "url";
|
|
349
|
+
function getManifestSlides(deckPath) {
|
|
350
|
+
const source = loadVerifyDeckSource(deckPath);
|
|
351
|
+
const slidesByFile = new Map(
|
|
352
|
+
source.manifest?.slides?.filter(
|
|
353
|
+
(slide) => typeof slide.file === "string"
|
|
354
|
+
).map((slide, index) => [
|
|
355
|
+
slide.file,
|
|
356
|
+
{
|
|
357
|
+
index,
|
|
358
|
+
file: slide.file,
|
|
359
|
+
title: typeof slide.title === "string" ? slide.title : void 0,
|
|
360
|
+
hidden: slide.hidden === true,
|
|
361
|
+
filePath: path3.resolve(source.deck, slide.file)
|
|
362
|
+
}
|
|
363
|
+
]) ?? []
|
|
364
|
+
);
|
|
365
|
+
return source.slideFiles.flatMap((file) => {
|
|
366
|
+
const slide = slidesByFile.get(file);
|
|
367
|
+
return slide && fs2.existsSync(slide.filePath) ? [slide] : [];
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
async function verifyRenderedOverflow(deckPath) {
|
|
371
|
+
const slides = getManifestSlides(deckPath);
|
|
372
|
+
if (slides.length === 0) {
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
const chromium = await loadChromium();
|
|
376
|
+
const browser = await chromium.launch({ headless: true });
|
|
377
|
+
try {
|
|
378
|
+
const issues = [];
|
|
379
|
+
const page = await browser.newPage();
|
|
380
|
+
for (const slide of slides) {
|
|
381
|
+
await loadSlide(page, slide.filePath);
|
|
382
|
+
const measurements = await measureOverflow(page);
|
|
383
|
+
for (const measurement of measurements) {
|
|
384
|
+
issues.push(
|
|
385
|
+
createVerifyIssue("error", measurement.code, measurement.message, {
|
|
386
|
+
slideFile: slide.file,
|
|
387
|
+
selector: measurement.selector,
|
|
388
|
+
...measurement.details
|
|
389
|
+
})
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return issues;
|
|
394
|
+
} finally {
|
|
395
|
+
await browser.close();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async function renderPreviewManifest({
|
|
399
|
+
deckPath,
|
|
400
|
+
slideFile,
|
|
401
|
+
outDir
|
|
402
|
+
}) {
|
|
403
|
+
const deck = path3.resolve(process.cwd(), deckPath);
|
|
404
|
+
const slides = getManifestSlides(deck);
|
|
405
|
+
const selectedSlides = slideFile ? slides.filter((slide) => slide.file === slideFile) : slides;
|
|
406
|
+
if (slideFile && selectedSlides.length === 0) {
|
|
407
|
+
throw new Error(`--slide must match a manifest slide file exactly: ${slideFile}`);
|
|
408
|
+
}
|
|
409
|
+
const outputDir = outDir ? path3.resolve(process.cwd(), outDir) : path3.join(deck, ".starry-slides", "view");
|
|
410
|
+
clearPreviewOutput(outputDir);
|
|
411
|
+
const chromium = await loadChromium();
|
|
412
|
+
const browser = await chromium.launch({ headless: true });
|
|
413
|
+
try {
|
|
414
|
+
const page = await browser.newPage();
|
|
415
|
+
const renders = [];
|
|
416
|
+
for (const slide of selectedSlides) {
|
|
417
|
+
const { width, height } = await loadSlide(page, slide.filePath);
|
|
418
|
+
const file = `${previewFileStem(slide.file)}.png`;
|
|
419
|
+
const imagePath = path3.join(outputDir, file);
|
|
420
|
+
await page.screenshot({
|
|
421
|
+
path: imagePath,
|
|
422
|
+
clip: { x: 0, y: 0, width, height }
|
|
423
|
+
});
|
|
424
|
+
renders.push({
|
|
425
|
+
index: slide.index,
|
|
426
|
+
slideFile: slide.file,
|
|
427
|
+
...slide.title ? { title: slide.title } : {},
|
|
428
|
+
file,
|
|
429
|
+
path: imagePath,
|
|
430
|
+
width,
|
|
431
|
+
height,
|
|
432
|
+
scale: 1
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
deck,
|
|
437
|
+
mode: slideFile ? "single" : "all",
|
|
438
|
+
outputDir,
|
|
439
|
+
slides: renders
|
|
440
|
+
};
|
|
441
|
+
} finally {
|
|
442
|
+
await browser.close();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async function loadSlide(page, filePath) {
|
|
446
|
+
await page.goto(pathToFileURL(filePath).href, { waitUntil: "load" });
|
|
447
|
+
const root = page.locator('[data-slide-root="true"]');
|
|
448
|
+
const size = {
|
|
449
|
+
width: Number(await root.getAttribute("data-slide-width") || 1920),
|
|
450
|
+
height: Number(await root.getAttribute("data-slide-height") || 1080)
|
|
451
|
+
};
|
|
452
|
+
await page.setViewportSize(size);
|
|
453
|
+
await page.evaluate("document.fonts ? document.fonts.ready : Promise.resolve()");
|
|
454
|
+
return size;
|
|
455
|
+
}
|
|
456
|
+
async function measureOverflow(page) {
|
|
457
|
+
return page.evaluate(`(() => {
|
|
458
|
+
const root = document.querySelector('[data-slide-root="true"]');
|
|
459
|
+
if (!root) {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const roundRect = (rect) => ({
|
|
464
|
+
left: Math.round(rect.left * 100) / 100,
|
|
465
|
+
top: Math.round(rect.top * 100) / 100,
|
|
466
|
+
right: Math.round(rect.right * 100) / 100,
|
|
467
|
+
bottom: Math.round(rect.bottom * 100) / 100,
|
|
468
|
+
width: Math.round(rect.width * 100) / 100,
|
|
469
|
+
height: Math.round(rect.height * 100) / 100,
|
|
470
|
+
});
|
|
471
|
+
const selectorFor = (node) =>
|
|
472
|
+
node.getAttribute("data-editor-id")
|
|
473
|
+
? '[data-editor-id="' + node.getAttribute("data-editor-id") + '"]'
|
|
474
|
+
: node.getAttribute("data-editable")
|
|
475
|
+
? node.tagName.toLowerCase() + '[data-editable="' + node.getAttribute("data-editable") + '"]'
|
|
476
|
+
: node.tagName.toLowerCase();
|
|
477
|
+
const hasAllowedOverflow = (node) =>
|
|
478
|
+
Boolean(node.closest('[data-allow-overflow="true"]'));
|
|
479
|
+
const rootRect = root.getBoundingClientRect();
|
|
480
|
+
const measurements = [];
|
|
481
|
+
const tolerance = 1;
|
|
482
|
+
|
|
483
|
+
if (!hasAllowedOverflow(root)) {
|
|
484
|
+
const rootOverflowX = root.scrollWidth - root.clientWidth;
|
|
485
|
+
const rootOverflowY = root.scrollHeight - root.clientHeight;
|
|
486
|
+
if (rootOverflowX > tolerance || rootOverflowY > tolerance) {
|
|
487
|
+
measurements.push({
|
|
488
|
+
code: "overflow.slide",
|
|
489
|
+
selector: selectorFor(root),
|
|
490
|
+
message: "slide root has rendered overflow",
|
|
491
|
+
details: {
|
|
492
|
+
rootScrollWidth: root.scrollWidth,
|
|
493
|
+
rootClientWidth: root.clientWidth,
|
|
494
|
+
rootScrollHeight: root.scrollHeight,
|
|
495
|
+
rootClientHeight: root.clientHeight,
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const body = document.body;
|
|
502
|
+
if (body && !hasAllowedOverflow(body) && !hasAllowedOverflow(root)) {
|
|
503
|
+
const viewportWidth = document.documentElement.clientWidth;
|
|
504
|
+
const viewportHeight = document.documentElement.clientHeight;
|
|
505
|
+
const bodyOverflowX = Math.max(
|
|
506
|
+
document.documentElement.scrollWidth,
|
|
507
|
+
body.scrollWidth
|
|
508
|
+
) - viewportWidth;
|
|
509
|
+
const bodyOverflowY = Math.max(
|
|
510
|
+
document.documentElement.scrollHeight,
|
|
511
|
+
body.scrollHeight
|
|
512
|
+
) - viewportHeight;
|
|
513
|
+
|
|
514
|
+
if (bodyOverflowX > tolerance || bodyOverflowY > tolerance) {
|
|
515
|
+
measurements.push({
|
|
516
|
+
code: "overflow.slide",
|
|
517
|
+
selector: "body",
|
|
518
|
+
message: "document body has rendered overflow",
|
|
519
|
+
details: {
|
|
520
|
+
bodyScrollWidth: body.scrollWidth,
|
|
521
|
+
bodyScrollHeight: body.scrollHeight,
|
|
522
|
+
documentScrollWidth: document.documentElement.scrollWidth,
|
|
523
|
+
documentScrollHeight: document.documentElement.scrollHeight,
|
|
524
|
+
viewportWidth,
|
|
525
|
+
viewportHeight,
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
for (const node of Array.from(document.querySelectorAll("[data-editable]"))) {
|
|
532
|
+
if (hasAllowedOverflow(node)) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const rect = node.getBoundingClientRect();
|
|
537
|
+
if (
|
|
538
|
+
rect.left < rootRect.left - tolerance ||
|
|
539
|
+
rect.top < rootRect.top - tolerance ||
|
|
540
|
+
rect.right > rootRect.right + tolerance ||
|
|
541
|
+
rect.bottom > rootRect.bottom + tolerance
|
|
542
|
+
) {
|
|
543
|
+
measurements.push({
|
|
544
|
+
code: "overflow.element-bounds",
|
|
545
|
+
selector: selectorFor(node),
|
|
546
|
+
message: "editable element renders outside slide bounds",
|
|
547
|
+
details: {
|
|
548
|
+
elementRect: roundRect(rect),
|
|
549
|
+
slideRect: roundRect(rootRect),
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const overflowX = node.scrollWidth - node.clientWidth;
|
|
555
|
+
const overflowY = node.scrollHeight - node.clientHeight;
|
|
556
|
+
if (hasConstrainedContentBox(node) && (overflowX > tolerance || overflowY > tolerance)) {
|
|
557
|
+
measurements.push({
|
|
558
|
+
code: "overflow.element-content",
|
|
559
|
+
selector: selectorFor(node),
|
|
560
|
+
message: "editable element content has rendered overflow",
|
|
561
|
+
details: {
|
|
562
|
+
elementRect: roundRect(rect),
|
|
563
|
+
scrollWidth: node.scrollWidth,
|
|
564
|
+
clientWidth: node.clientWidth,
|
|
565
|
+
scrollHeight: node.scrollHeight,
|
|
566
|
+
clientHeight: node.clientHeight,
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return measurements;
|
|
573
|
+
|
|
574
|
+
function hasConstrainedContentBox(node) {
|
|
575
|
+
const style = window.getComputedStyle(node);
|
|
576
|
+
const overflowValues = [
|
|
577
|
+
style.overflow,
|
|
578
|
+
style.overflowX,
|
|
579
|
+
style.overflowY,
|
|
580
|
+
];
|
|
581
|
+
const clipsOverflow = overflowValues.some((value) =>
|
|
582
|
+
["hidden", "clip", "auto", "scroll"].includes(value)
|
|
583
|
+
);
|
|
584
|
+
const hasExplicitBox =
|
|
585
|
+
style.position === "absolute" ||
|
|
586
|
+
style.position === "fixed" ||
|
|
587
|
+
style.display === "block" ||
|
|
588
|
+
style.display === "inline-block" ||
|
|
589
|
+
style.display === "inline-flex" ||
|
|
590
|
+
style.display === "flex" ||
|
|
591
|
+
style.display === "grid" ||
|
|
592
|
+
style.maxWidth !== "none" ||
|
|
593
|
+
style.maxHeight !== "none" ||
|
|
594
|
+
node.style.width ||
|
|
595
|
+
node.style.height ||
|
|
596
|
+
node.style.maxWidth ||
|
|
597
|
+
node.style.maxHeight;
|
|
598
|
+
|
|
599
|
+
return clipsOverflow && hasExplicitBox;
|
|
600
|
+
}
|
|
601
|
+
})()`);
|
|
602
|
+
}
|
|
603
|
+
async function loadChromium() {
|
|
604
|
+
const require2 = createRequire(import.meta.url);
|
|
605
|
+
const playwright = require2("@playwright/test");
|
|
606
|
+
return playwright.chromium;
|
|
607
|
+
}
|
|
608
|
+
function clearPreviewOutput(outputDir) {
|
|
609
|
+
fs2.rmSync(outputDir, { recursive: true, force: true });
|
|
610
|
+
fs2.mkdirSync(outputDir, { recursive: true });
|
|
611
|
+
}
|
|
612
|
+
function previewFileStem(slideFile) {
|
|
613
|
+
const safeName = slideFile.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/\.html?$/i, "");
|
|
614
|
+
let hash = 0;
|
|
615
|
+
for (let index = 0; index < slideFile.length; index += 1) {
|
|
616
|
+
hash = hash * 31 + slideFile.charCodeAt(index) >>> 0;
|
|
617
|
+
}
|
|
618
|
+
return `${safeName}-${hash.toString(16).padStart(8, "0")}`;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/node/html-export.ts
|
|
622
|
+
async function exportHtml({
|
|
623
|
+
deckPath,
|
|
624
|
+
outFile
|
|
625
|
+
}) {
|
|
626
|
+
const deck = path4.resolve(process.cwd(), deckPath);
|
|
627
|
+
const outputPath = path4.resolve(process.cwd(), outFile);
|
|
628
|
+
const manifestSlides = getManifestSlides(deck);
|
|
629
|
+
const slides = manifestSlides.map((slide) => ({
|
|
630
|
+
file: slide.file,
|
|
631
|
+
...slide.title ? { title: slide.title } : {},
|
|
632
|
+
...slide.hidden ? { hidden: slide.hidden } : {},
|
|
633
|
+
htmlSource: fs3.readFileSync(slide.filePath, "utf8")
|
|
634
|
+
}));
|
|
635
|
+
const html = createSingleHtmlExportDocument({
|
|
636
|
+
title: path4.basename(deck),
|
|
637
|
+
slides
|
|
638
|
+
});
|
|
639
|
+
const exportedSlides = planHtmlExportSlides(slides);
|
|
640
|
+
fs3.mkdirSync(path4.dirname(outputPath), { recursive: true });
|
|
641
|
+
fs3.writeFileSync(outputPath, html, "utf8");
|
|
642
|
+
return {
|
|
643
|
+
deck,
|
|
644
|
+
mode: "all",
|
|
645
|
+
outFile: outputPath,
|
|
646
|
+
path: outputPath,
|
|
647
|
+
slides: exportedSlides.map((slide) => {
|
|
648
|
+
const manifestSlide = manifestSlides.find((item) => item.file === slide.file);
|
|
649
|
+
return {
|
|
650
|
+
index: manifestSlide?.index ?? 0,
|
|
651
|
+
slideFile: slide.file,
|
|
652
|
+
...slide.title ? { title: slide.title } : {}
|
|
653
|
+
};
|
|
654
|
+
})
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/node/open-browser.ts
|
|
659
|
+
import { spawn } from "child_process";
|
|
660
|
+
function openBrowser(url) {
|
|
661
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
662
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
663
|
+
const child = spawn(command, args, {
|
|
664
|
+
detached: true,
|
|
665
|
+
stdio: "ignore"
|
|
666
|
+
});
|
|
667
|
+
child.unref();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/node/pdf-export.ts
|
|
671
|
+
import fs4 from "fs";
|
|
672
|
+
import { createRequire as createRequire2 } from "module";
|
|
673
|
+
import os from "os";
|
|
674
|
+
import path5 from "path";
|
|
675
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
676
|
+
import { JSDOM as JSDOM2, VirtualConsole as VirtualConsole2 } from "jsdom";
|
|
677
|
+
async function exportPdf({
|
|
678
|
+
deckPath,
|
|
679
|
+
outFile,
|
|
680
|
+
selection
|
|
681
|
+
}) {
|
|
682
|
+
const deck = path5.resolve(process.cwd(), deckPath);
|
|
683
|
+
const outputPath = path5.resolve(process.cwd(), outFile);
|
|
684
|
+
const manifestSlides = getManifestSlides(deck);
|
|
685
|
+
const plan = planPdfExport({ slides: manifestSlides, selection });
|
|
686
|
+
const selectedSlides = resolveSelectedSlides(
|
|
687
|
+
manifestSlides,
|
|
688
|
+
plan.slides.map((slide) => slide.file)
|
|
689
|
+
);
|
|
690
|
+
if (selectedSlides.length === 0) {
|
|
691
|
+
throw new Error("PDF export requires at least one manifest slide.");
|
|
692
|
+
}
|
|
693
|
+
const sizedSlides = selectedSlides.map((slide) => ({
|
|
694
|
+
...slide,
|
|
695
|
+
...readSlideSize(slide.filePath)
|
|
696
|
+
}));
|
|
697
|
+
const [firstSlide] = sizedSlides;
|
|
698
|
+
const mismatchedSlide = sizedSlides.find(
|
|
699
|
+
(slide) => slide.width !== firstSlide.width || slide.height !== firstSlide.height
|
|
700
|
+
);
|
|
701
|
+
if (mismatchedSlide) {
|
|
702
|
+
throw new Error(
|
|
703
|
+
`PDF export requires selected slides to share one size; ${mismatchedSlide.file} is ${mismatchedSlide.width}x${mismatchedSlide.height}, expected ${firstSlide.width}x${firstSlide.height}.`
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
const tempDir = fs4.mkdtempSync(path5.join(os.tmpdir(), "starry-slides-pdf-"));
|
|
707
|
+
const printFile = path5.join(tempDir, "print.html");
|
|
708
|
+
try {
|
|
709
|
+
fs4.writeFileSync(
|
|
710
|
+
printFile,
|
|
711
|
+
createPrintDocument({
|
|
712
|
+
slides: sizedSlides,
|
|
713
|
+
width: firstSlide.width,
|
|
714
|
+
height: firstSlide.height
|
|
715
|
+
}),
|
|
716
|
+
"utf8"
|
|
717
|
+
);
|
|
718
|
+
fs4.mkdirSync(path5.dirname(outputPath), { recursive: true });
|
|
719
|
+
const chromium = await loadChromium2();
|
|
720
|
+
const browser = await chromium.launch({ headless: true });
|
|
721
|
+
try {
|
|
722
|
+
const page = await browser.newPage();
|
|
723
|
+
await page.goto(pathToFileURL2(printFile).href, { waitUntil: "load" });
|
|
724
|
+
await page.evaluate("document.fonts ? document.fonts.ready : Promise.resolve()");
|
|
725
|
+
await page.pdf({
|
|
726
|
+
path: outputPath,
|
|
727
|
+
width: `${firstSlide.width}px`,
|
|
728
|
+
height: `${firstSlide.height}px`,
|
|
729
|
+
margin: { top: "0", right: "0", bottom: "0", left: "0" },
|
|
730
|
+
printBackground: true,
|
|
731
|
+
preferCSSPageSize: true
|
|
732
|
+
});
|
|
733
|
+
} finally {
|
|
734
|
+
await browser.close();
|
|
735
|
+
}
|
|
736
|
+
} finally {
|
|
737
|
+
fs4.rmSync(tempDir, { recursive: true, force: true });
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
deck,
|
|
741
|
+
mode: plan.mode,
|
|
742
|
+
outFile: outputPath,
|
|
743
|
+
path: outputPath,
|
|
744
|
+
slides: sizedSlides.map((slide) => ({
|
|
745
|
+
index: slide.index,
|
|
746
|
+
slideFile: slide.file,
|
|
747
|
+
...slide.title ? { title: slide.title } : {},
|
|
748
|
+
width: slide.width,
|
|
749
|
+
height: slide.height
|
|
750
|
+
}))
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function resolveSelectedSlides(slides, selectedFiles) {
|
|
754
|
+
const slideByFile = new Map(slides.map((slide) => [slide.file, slide]));
|
|
755
|
+
return selectedFiles.map((file) => {
|
|
756
|
+
const slide = slideByFile.get(file);
|
|
757
|
+
if (!slide) {
|
|
758
|
+
throw new Error(`PDF export could not resolve selected slide file: ${file}`);
|
|
759
|
+
}
|
|
760
|
+
return slide;
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
function readSlideSize(filePath) {
|
|
764
|
+
const dom = new JSDOM2(fs4.readFileSync(filePath, "utf8"), {
|
|
765
|
+
virtualConsole: new VirtualConsole2()
|
|
766
|
+
});
|
|
767
|
+
const root = dom.window.document.querySelector(`[${SLIDE_ROOT_ATTR}]`);
|
|
768
|
+
return {
|
|
769
|
+
width: parseDimension(root?.getAttribute("data-slide-width") ?? null, DEFAULT_SLIDE_WIDTH),
|
|
770
|
+
height: parseDimension(root?.getAttribute("data-slide-height") ?? null, DEFAULT_SLIDE_HEIGHT)
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function createPrintDocument({
|
|
774
|
+
slides,
|
|
775
|
+
width,
|
|
776
|
+
height
|
|
777
|
+
}) {
|
|
778
|
+
return `<!DOCTYPE html>
|
|
779
|
+
<html>
|
|
780
|
+
<head>
|
|
781
|
+
<meta charset="utf-8" />
|
|
782
|
+
<style>
|
|
783
|
+
@page { size: ${width}px ${height}px; margin: 0; }
|
|
784
|
+
html, body { margin: 0; padding: 0; width: ${width}px; background: white; }
|
|
785
|
+
iframe {
|
|
786
|
+
display: block;
|
|
787
|
+
width: ${width}px;
|
|
788
|
+
height: ${height}px;
|
|
789
|
+
border: 0;
|
|
790
|
+
margin: 0;
|
|
791
|
+
padding: 0;
|
|
792
|
+
break-after: page;
|
|
793
|
+
page-break-after: always;
|
|
794
|
+
}
|
|
795
|
+
iframe:last-child {
|
|
796
|
+
break-after: auto;
|
|
797
|
+
page-break-after: auto;
|
|
798
|
+
}
|
|
799
|
+
</style>
|
|
800
|
+
</head>
|
|
801
|
+
<body>
|
|
802
|
+
${slides.map(
|
|
803
|
+
(slide) => `<iframe title="${escapeAttribute(slide.title ?? slide.file)}" src="${pathToFileURL2(slide.filePath).href}"></iframe>`
|
|
804
|
+
).join("\n ")}
|
|
805
|
+
</body>
|
|
806
|
+
</html>`;
|
|
807
|
+
}
|
|
808
|
+
async function loadChromium2() {
|
|
809
|
+
const require2 = createRequire2(import.meta.url);
|
|
810
|
+
const playwright = require2("@playwright/test");
|
|
811
|
+
return playwright.chromium;
|
|
812
|
+
}
|
|
813
|
+
function escapeAttribute(value) {
|
|
814
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/node/ports.ts
|
|
818
|
+
import net from "net";
|
|
819
|
+
async function findAvailablePort(preferredPort = 5173) {
|
|
820
|
+
for (let port = preferredPort; port < preferredPort + 100; port += 1) {
|
|
821
|
+
if (await isPortAvailable(port)) {
|
|
822
|
+
return port;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
throw new Error(`No available port found near ${preferredPort}.`);
|
|
826
|
+
}
|
|
827
|
+
function isPortAvailable(port) {
|
|
828
|
+
return new Promise((resolve) => {
|
|
829
|
+
const server = net.createServer();
|
|
830
|
+
server.once("error", () => {
|
|
831
|
+
resolve(false);
|
|
832
|
+
});
|
|
833
|
+
server.once("listening", () => {
|
|
834
|
+
server.close(() => resolve(true));
|
|
835
|
+
});
|
|
836
|
+
server.listen(port, "127.0.0.1");
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// src/cli/index.ts
|
|
841
|
+
var COMMANDS = /* @__PURE__ */ new Set(["open", "verify", "view", "export", "add-skill", "help", "--help", "-h"]);
|
|
842
|
+
function usage() {
|
|
843
|
+
return `Usage:
|
|
844
|
+
starry-slides [deck]
|
|
845
|
+
starry-slides open [deck]
|
|
846
|
+
starry-slides verify [deck]
|
|
847
|
+
starry-slides verify [deck] --static
|
|
848
|
+
starry-slides view [deck] --slide <manifest-file>
|
|
849
|
+
starry-slides view [deck] --all
|
|
850
|
+
starry-slides view [deck] --all --out-dir <directory>
|
|
851
|
+
starry-slides export pdf [deck] --out <file>
|
|
852
|
+
starry-slides export pdf [deck] --all --out <file>
|
|
853
|
+
starry-slides export pdf [deck] --slide <manifest-file> --out <file>
|
|
854
|
+
starry-slides export pdf [deck] --slides <manifest-file>[,<manifest-file>...] --out <file>
|
|
855
|
+
starry-slides export html [deck] --out <file>
|
|
856
|
+
starry-slides add-skill`;
|
|
857
|
+
}
|
|
858
|
+
function parseArgs(argv) {
|
|
859
|
+
const [first, ...rest] = argv;
|
|
860
|
+
const command = normalizeCommand(first);
|
|
861
|
+
const remaining = command === "open" && first && !COMMANDS.has(first) ? [first, ...rest] : rest;
|
|
862
|
+
let deckPath;
|
|
863
|
+
let staticVerify = false;
|
|
864
|
+
let viewMode;
|
|
865
|
+
let exportFormat;
|
|
866
|
+
let exportMode;
|
|
867
|
+
let slideFile;
|
|
868
|
+
let slideFiles;
|
|
869
|
+
let outDir;
|
|
870
|
+
let outFile;
|
|
871
|
+
if (command === "export") {
|
|
872
|
+
const format = remaining.shift();
|
|
873
|
+
if (format !== "pdf" && format !== "html") {
|
|
874
|
+
throw new Error("export requires a format: pdf or html");
|
|
875
|
+
}
|
|
876
|
+
exportFormat = format;
|
|
877
|
+
}
|
|
878
|
+
for (let index = 0; index < remaining.length; index += 1) {
|
|
879
|
+
const arg = remaining[index];
|
|
880
|
+
if (!arg) {
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
if (arg === "--static") {
|
|
884
|
+
staticVerify = true;
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
if (arg === "--all") {
|
|
888
|
+
viewMode = "all";
|
|
889
|
+
exportMode = "all";
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (arg === "--slide") {
|
|
893
|
+
const value = remaining[index + 1];
|
|
894
|
+
if (!value || value.startsWith("--")) {
|
|
895
|
+
throw new Error("--slide requires a manifest slide file value");
|
|
896
|
+
}
|
|
897
|
+
viewMode = "slide";
|
|
898
|
+
exportMode = "slide";
|
|
899
|
+
slideFile = value;
|
|
900
|
+
index += 1;
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (arg === "--slides") {
|
|
904
|
+
const value = remaining[index + 1];
|
|
905
|
+
if (!value || value.startsWith("--")) {
|
|
906
|
+
throw new Error("--slides requires at least one manifest slide file value");
|
|
907
|
+
}
|
|
908
|
+
exportMode = "slides";
|
|
909
|
+
slideFiles = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
910
|
+
index += 1;
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
if (arg === "--out-dir") {
|
|
914
|
+
const value = remaining[index + 1];
|
|
915
|
+
if (!value || value.startsWith("--")) {
|
|
916
|
+
throw new Error("--out-dir requires a directory path");
|
|
917
|
+
}
|
|
918
|
+
outDir = value;
|
|
919
|
+
index += 1;
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
if (arg === "--out") {
|
|
923
|
+
const value = remaining[index + 1];
|
|
924
|
+
if (!value || value.startsWith("--")) {
|
|
925
|
+
throw new Error("--out requires a file path");
|
|
926
|
+
}
|
|
927
|
+
outFile = value;
|
|
928
|
+
index += 1;
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
if (arg.startsWith("--")) {
|
|
932
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
933
|
+
}
|
|
934
|
+
if (deckPath) {
|
|
935
|
+
throw new Error(`Unexpected extra argument: ${arg}`);
|
|
936
|
+
}
|
|
937
|
+
deckPath = arg;
|
|
938
|
+
}
|
|
939
|
+
return {
|
|
940
|
+
command,
|
|
941
|
+
deckPath,
|
|
942
|
+
staticVerify,
|
|
943
|
+
viewMode,
|
|
944
|
+
exportFormat,
|
|
945
|
+
exportMode,
|
|
946
|
+
slideFile,
|
|
947
|
+
slideFiles,
|
|
948
|
+
outDir,
|
|
949
|
+
outFile
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function normalizeCommand(first) {
|
|
953
|
+
if (!first) {
|
|
954
|
+
return "open";
|
|
955
|
+
}
|
|
956
|
+
if (first === "help" || first === "--help" || first === "-h") {
|
|
957
|
+
return "help";
|
|
958
|
+
}
|
|
959
|
+
if (first === "open" || first === "verify" || first === "view" || first === "export" || first === "add-skill") {
|
|
960
|
+
return first;
|
|
961
|
+
}
|
|
962
|
+
return "open";
|
|
963
|
+
}
|
|
964
|
+
function writeJson(value) {
|
|
965
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
966
|
+
`);
|
|
967
|
+
}
|
|
968
|
+
async function runStaticVerify(deckPath) {
|
|
969
|
+
return verifyDeck(deckPath, { mode: "static" });
|
|
970
|
+
}
|
|
971
|
+
async function runCompleteVerify(deckPath) {
|
|
972
|
+
const staticResult = verifyDeck(deckPath, { mode: "static" });
|
|
973
|
+
if (!staticResult.ok) {
|
|
974
|
+
return createVerifyResult({
|
|
975
|
+
deck: staticResult.deck,
|
|
976
|
+
mode: "complete",
|
|
977
|
+
checks: ["structure", "static-overflow", "rendered-overflow"],
|
|
978
|
+
issues: staticResult.issues
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
const renderedIssues = await verifyRenderedOverflow(deckPath);
|
|
982
|
+
return verifyDeck(deckPath, {
|
|
983
|
+
mode: "complete",
|
|
984
|
+
renderedIssues
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
async function runVerify(deckPath, mode) {
|
|
988
|
+
const result = mode === "static" ? await runStaticVerify(deckPath) : await runCompleteVerify(deckPath);
|
|
989
|
+
writeJson(result);
|
|
990
|
+
return result.ok;
|
|
991
|
+
}
|
|
992
|
+
async function runView(deckPath, parsed) {
|
|
993
|
+
if (parsed.staticVerify) {
|
|
994
|
+
throw new Error("view always runs Static Verify; do not pass --static");
|
|
995
|
+
}
|
|
996
|
+
if (parsed.viewMode === "slide" && !parsed.slideFile) {
|
|
997
|
+
throw new Error("--slide requires a manifest slide file value");
|
|
998
|
+
}
|
|
999
|
+
if (!parsed.viewMode) {
|
|
1000
|
+
throw new Error("view requires either --slide <manifest-file> or --all");
|
|
1001
|
+
}
|
|
1002
|
+
const staticResult = await runStaticVerify(deckPath);
|
|
1003
|
+
if (!staticResult.ok) {
|
|
1004
|
+
writeJson(staticResult);
|
|
1005
|
+
process.exitCode = 1;
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const manifest = await renderPreviewManifest({
|
|
1009
|
+
deckPath,
|
|
1010
|
+
slideFile: parsed.viewMode === "slide" ? parsed.slideFile : void 0,
|
|
1011
|
+
outDir: parsed.outDir
|
|
1012
|
+
});
|
|
1013
|
+
writeJson(manifest);
|
|
1014
|
+
}
|
|
1015
|
+
async function runExport(deckPath, parsed) {
|
|
1016
|
+
if (parsed.exportFormat !== "pdf" && parsed.exportFormat !== "html") {
|
|
1017
|
+
throw new Error("export requires a format: pdf or html");
|
|
1018
|
+
}
|
|
1019
|
+
if (parsed.staticVerify) {
|
|
1020
|
+
throw new Error(
|
|
1021
|
+
`export ${parsed.exportFormat} runs Static Verify internally; do not pass --static`
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
if (!parsed.outFile) {
|
|
1025
|
+
throw new Error(`export ${parsed.exportFormat} requires --out <file>`);
|
|
1026
|
+
}
|
|
1027
|
+
const staticResult = await runStaticVerify(deckPath);
|
|
1028
|
+
if (!staticResult.ok) {
|
|
1029
|
+
writeJson(staticResult);
|
|
1030
|
+
process.exitCode = 1;
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (parsed.exportFormat === "html") {
|
|
1034
|
+
if (parsed.exportMode && parsed.exportMode !== "all") {
|
|
1035
|
+
throw new Error("export html currently supports full-deck export only");
|
|
1036
|
+
}
|
|
1037
|
+
const result2 = await exportHtml({
|
|
1038
|
+
deckPath,
|
|
1039
|
+
outFile: parsed.outFile
|
|
1040
|
+
});
|
|
1041
|
+
writeJson(result2);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const result = await exportPdf({
|
|
1045
|
+
deckPath,
|
|
1046
|
+
outFile: parsed.outFile,
|
|
1047
|
+
selection: parsed.exportMode === "slide" ? { mode: "slide", slideFile: parsed.slideFile } : parsed.exportMode === "slides" ? { mode: "slides", slideFiles: parsed.slideFiles } : { mode: "all" }
|
|
1048
|
+
});
|
|
1049
|
+
writeJson(result);
|
|
1050
|
+
}
|
|
1051
|
+
async function runOpen(deckPath) {
|
|
1052
|
+
const result = await runCompleteVerify(deckPath);
|
|
1053
|
+
if (!result.ok) {
|
|
1054
|
+
writeJson(result);
|
|
1055
|
+
process.exitCode = 1;
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const port = await findAvailablePort(Number(process.env.PORT ?? 5173));
|
|
1059
|
+
const url = `http://127.0.0.1:${port}/`;
|
|
1060
|
+
if (process.env.STARRY_SLIDES_TEST_STUB_OPEN === "1") {
|
|
1061
|
+
console.error(`Opening Starry Slides at ${url}`);
|
|
1062
|
+
console.error(`Editor startup stub: STARRY_SLIDES_DECK_DIR=${deckPath}`);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const child = spawn2("vite", ["--host", "127.0.0.1", "--port", String(port), "--strictPort"], {
|
|
1066
|
+
env: {
|
|
1067
|
+
...process.env,
|
|
1068
|
+
STARRY_SLIDES_DECK_DIR: deckPath
|
|
1069
|
+
},
|
|
1070
|
+
stdio: "inherit",
|
|
1071
|
+
shell: process.platform === "win32"
|
|
1072
|
+
});
|
|
1073
|
+
child.on("exit", (code, signal) => {
|
|
1074
|
+
if (signal) {
|
|
1075
|
+
process.kill(process.pid, signal);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
process.exit(code ?? 0);
|
|
1079
|
+
});
|
|
1080
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
1081
|
+
process.on(signal, () => {
|
|
1082
|
+
child.kill(signal);
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
console.error(`Opening Starry Slides at ${url}`);
|
|
1086
|
+
setTimeout(() => openBrowser(url), 750);
|
|
1087
|
+
}
|
|
1088
|
+
async function main() {
|
|
1089
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
1090
|
+
if (parsed.command === "help") {
|
|
1091
|
+
console.log(usage());
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (parsed.command === "add-skill") {
|
|
1095
|
+
console.error("add-skill is not implemented yet.");
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const deckPath = resolveDeckPath(parsed.deckPath);
|
|
1099
|
+
if (parsed.command === "verify") {
|
|
1100
|
+
const ok = await runVerify(deckPath, parsed.staticVerify ? "static" : "complete");
|
|
1101
|
+
if (!ok) {
|
|
1102
|
+
process.exitCode = 1;
|
|
1103
|
+
}
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (parsed.command === "view") {
|
|
1107
|
+
await runView(deckPath, parsed);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
if (parsed.command === "export") {
|
|
1111
|
+
await runExport(deckPath, parsed);
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
await runOpen(deckPath);
|
|
1115
|
+
}
|
|
1116
|
+
main().catch((error) => {
|
|
1117
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1118
|
+
process.exitCode = 1;
|
|
1119
|
+
});
|