@slidev-react/node 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 +21 -0
- package/README.md +21 -0
- package/dist/build.d.mts +10 -0
- package/dist/build.mjs +28 -0
- package/dist/cli/buildArgs.mjs +78 -0
- package/dist/cli/devArgs.mjs +74 -0
- package/dist/cli/exportArgs.mjs +87 -0
- package/dist/cli/lintArgs.mjs +32 -0
- package/dist/cli/readOptionValue.mjs +16 -0
- package/dist/context.d.mts +11 -0
- package/dist/context.mjs +37 -0
- package/dist/dev.d.mts +13 -0
- package/dist/dev.mjs +61 -0
- package/dist/export.d.mts +11 -0
- package/dist/export.mjs +67 -0
- package/dist/exportBrowser.d.mts +8 -0
- package/dist/exportBrowser.mjs +123 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +5 -0
- package/dist/lint.d.mts +15 -0
- package/dist/lint.mjs +36 -0
- package/dist/slides/build/createSlidesViteConfig.d.mts +9 -0
- package/dist/slides/build/createSlidesViteConfig.mjs +25 -0
- package/dist/slides/build/generateCompiledSlides.mjs +264 -0
- package/dist/slides/build/slidesSourceFile.mjs +9 -0
- package/dist/slides/compiling/mdx-options.mjs +18 -0
- package/dist/slides/compiling/rehypeShikiVitesse.mjs +93 -0
- package/dist/slides/mdx/remarkDiagramComponents.mjs +38 -0
- package/dist/slides/parsing/frontmatter.mjs +29 -0
- package/dist/slides/parsing/parseSlides.mjs +159 -0
- package/dist/slides/validation/validateSlidesAuthoring.mjs +93 -0
- package/package.json +57 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { clampSlideSelection, createRangesFromSlides, createSlideSelectionLabel, expandSlideSelection, toPdfPageRanges } from "@slidev-react/core/presentation/export/selection";
|
|
4
|
+
import { chromium } from "@playwright/test";
|
|
5
|
+
import { createSlideSnapshotFileName, resolveExportSlidesBaseName } from "@slidev-react/core/presentation/export/fileNames";
|
|
6
|
+
//#region src/exportBrowser.ts
|
|
7
|
+
async function readExportViewport(page) {
|
|
8
|
+
return page.locator("[data-export-view=\"print\"]").evaluate((node) => ({
|
|
9
|
+
width: Number.parseInt(node.getAttribute("data-export-viewport-width") ?? "1920", 10),
|
|
10
|
+
height: Number.parseInt(node.getAttribute("data-export-viewport-height") ?? "1080", 10)
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
async function readSnapshotInfos(page) {
|
|
14
|
+
return page.locator("[data-export-snapshot=\"slide\"]").evaluateAll((nodes) => nodes.map((node, index) => ({
|
|
15
|
+
page: index + 1,
|
|
16
|
+
slideNumber: Number.parseInt(node.getAttribute("data-export-slide") ?? "0", 10),
|
|
17
|
+
title: node.getAttribute("data-export-slide-title") ?? "",
|
|
18
|
+
click: node.getAttribute("data-export-click") ?? "all"
|
|
19
|
+
})));
|
|
20
|
+
}
|
|
21
|
+
function resolveSnapshotSelection(snapshotInfos, slideSelection, withClicks) {
|
|
22
|
+
const totalSlides = new Set(snapshotInfos.map((info) => info.slideNumber)).size;
|
|
23
|
+
const selectedRanges = clampSlideSelection(slideSelection, totalSlides);
|
|
24
|
+
const selectedSlides = expandSlideSelection(selectedRanges);
|
|
25
|
+
if (selectedSlides.length === 0) throw new Error(`No slides matched --slides for a slides file with ${totalSlides} slides.`);
|
|
26
|
+
const selectedSlideSet = new Set(selectedSlides);
|
|
27
|
+
const selectedSnapshots = snapshotInfos.filter((info) => selectedSlideSet.has(info.slideNumber));
|
|
28
|
+
if (selectedSnapshots.length === 0) throw new Error("No export snapshots matched the requested slides.");
|
|
29
|
+
return {
|
|
30
|
+
selectedSlides,
|
|
31
|
+
selectedSnapshots,
|
|
32
|
+
variantLabel: [selectedRanges.length === 1 && selectedRanges[0].start === 1 && selectedRanges[0].end === totalSlides ? null : createSlideSelectionLabel(selectedRanges), withClicks ? "with-clicks" : null].filter(Boolean).join("-")
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function exportPdfArtifacts(page, exportViewport, slidesBaseName, slidesOutputDir, variantLabel, selectedSnapshots) {
|
|
36
|
+
const pdfPath = path.join(slidesOutputDir, variantLabel ? `${slidesBaseName}-${variantLabel}.pdf` : `${slidesBaseName}.pdf`);
|
|
37
|
+
await page.emulateMedia({ media: "print" });
|
|
38
|
+
await page.pdf({
|
|
39
|
+
path: pdfPath,
|
|
40
|
+
landscape: exportViewport.width > exportViewport.height,
|
|
41
|
+
printBackground: true,
|
|
42
|
+
preferCSSPageSize: true,
|
|
43
|
+
pageRanges: toPdfPageRanges(createRangesFromSlides(selectedSnapshots.map((info) => info.page)))
|
|
44
|
+
});
|
|
45
|
+
await page.emulateMedia({ media: "screen" });
|
|
46
|
+
return pdfPath;
|
|
47
|
+
}
|
|
48
|
+
async function applyPngExportStyles(page, exportViewport) {
|
|
49
|
+
await page.addStyleTag({ content: `
|
|
50
|
+
.print-slides-view main {
|
|
51
|
+
max-width: none !important;
|
|
52
|
+
}
|
|
53
|
+
.print-slide-shell {
|
|
54
|
+
width: ${exportViewport.width}px !important;
|
|
55
|
+
margin-left: auto !important;
|
|
56
|
+
margin-right: auto !important;
|
|
57
|
+
}
|
|
58
|
+
.print-slide-frame {
|
|
59
|
+
padding: 0 !important;
|
|
60
|
+
border: none !important;
|
|
61
|
+
background: transparent !important;
|
|
62
|
+
box-shadow: none !important;
|
|
63
|
+
}
|
|
64
|
+
.print-slide-frame [data-export-surface="slide"] {
|
|
65
|
+
width: ${exportViewport.width}px !important;
|
|
66
|
+
height: ${exportViewport.height}px !important;
|
|
67
|
+
max-width: none !important;
|
|
68
|
+
min-height: ${exportViewport.height}px !important;
|
|
69
|
+
aspect-ratio: auto !important;
|
|
70
|
+
box-shadow: none !important;
|
|
71
|
+
}
|
|
72
|
+
` });
|
|
73
|
+
}
|
|
74
|
+
async function exportPngArtifacts(page, exportViewport, pngOutputDir, withClicks, selectedSnapshots) {
|
|
75
|
+
await mkdir(pngOutputDir, { recursive: true });
|
|
76
|
+
await applyPngExportStyles(page, exportViewport);
|
|
77
|
+
const createdFiles = [];
|
|
78
|
+
for (const snapshot of selectedSnapshots) {
|
|
79
|
+
const shell = page.locator(`[data-export-snapshot="slide"][data-export-slide="${snapshot.slideNumber}"][data-export-click="${snapshot.click}"]`).first();
|
|
80
|
+
const fileName = createSlideSnapshotFileName({
|
|
81
|
+
index: snapshot.slideNumber,
|
|
82
|
+
title: snapshot.title,
|
|
83
|
+
clickStep: withClicks && snapshot.click !== "all" ? Number.parseInt(snapshot.click, 10) : null
|
|
84
|
+
});
|
|
85
|
+
const targetPath = path.join(pngOutputDir, fileName);
|
|
86
|
+
await shell.locator("[data-export-surface=\"slide\"]").screenshot({ path: targetPath });
|
|
87
|
+
createdFiles.push(targetPath);
|
|
88
|
+
}
|
|
89
|
+
return createdFiles;
|
|
90
|
+
}
|
|
91
|
+
async function exportSlidesInBrowser(options) {
|
|
92
|
+
const browser = await chromium.launch({ headless: true });
|
|
93
|
+
try {
|
|
94
|
+
const page = await browser.newPage({
|
|
95
|
+
viewport: {
|
|
96
|
+
width: 2400,
|
|
97
|
+
height: 1600
|
|
98
|
+
},
|
|
99
|
+
deviceScaleFactor: 1
|
|
100
|
+
});
|
|
101
|
+
await page.goto(options.printUrl, { waitUntil: "networkidle" });
|
|
102
|
+
await page.waitForSelector("[data-export-view=\"print\"]");
|
|
103
|
+
await page.waitForFunction(() => Array.from(document.querySelectorAll("[data-export-slide-ready=\"false\"]")).length === 0);
|
|
104
|
+
const exportViewport = await readExportViewport(page);
|
|
105
|
+
const slidesBaseName = resolveExportSlidesBaseName(await page.title());
|
|
106
|
+
const { selectedSlides, selectedSnapshots, variantLabel } = resolveSnapshotSelection(await readSnapshotInfos(page), options.slideSelection, options.withClicks);
|
|
107
|
+
const slidesOutputDir = path.resolve(options.outputDir, slidesBaseName);
|
|
108
|
+
const pngOutputDir = variantLabel ? path.join(slidesOutputDir, "png", variantLabel) : path.join(slidesOutputDir, "png");
|
|
109
|
+
await mkdir(slidesOutputDir, { recursive: true });
|
|
110
|
+
const createdFiles = [];
|
|
111
|
+
if (options.format === "all" || options.format === "pdf") createdFiles.push(await exportPdfArtifacts(page, exportViewport, slidesBaseName, slidesOutputDir, variantLabel, selectedSnapshots));
|
|
112
|
+
if (options.format === "all" || options.format === "png") createdFiles.push(...await exportPngArtifacts(page, exportViewport, pngOutputDir, options.withClicks, selectedSnapshots));
|
|
113
|
+
return {
|
|
114
|
+
createdFiles,
|
|
115
|
+
selectedSlides,
|
|
116
|
+
variantLabel
|
|
117
|
+
};
|
|
118
|
+
} finally {
|
|
119
|
+
await browser.close();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { exportSlidesInBrowser };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { buildSlidesApp, runSlidesBuild } from "./build.mjs";
|
|
2
|
+
import { exportSlidesArtifacts, runSlidesExport } from "./export.mjs";
|
|
3
|
+
import { runSlidesDev, startSlidesDevServer, stopSlidesDevServer } from "./dev.mjs";
|
|
4
|
+
import { lintSlides, runSlidesLint } from "./lint.mjs";
|
|
5
|
+
export { buildSlidesApp, exportSlidesArtifacts, lintSlides, runSlidesBuild, runSlidesDev, runSlidesExport, runSlidesLint, startSlidesDevServer, stopSlidesDevServer };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { buildSlidesApp, runSlidesBuild } from "./build.mjs";
|
|
2
|
+
import { runSlidesDev, startSlidesDevServer, stopSlidesDevServer } from "./dev.mjs";
|
|
3
|
+
import { exportSlidesArtifacts, runSlidesExport } from "./export.mjs";
|
|
4
|
+
import { lintSlides, runSlidesLint } from "./lint.mjs";
|
|
5
|
+
export { buildSlidesApp, exportSlidesArtifacts, lintSlides, runSlidesBuild, runSlidesDev, runSlidesExport, runSlidesLint, startSlidesDevServer, stopSlidesDevServer };
|
package/dist/lint.d.mts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CommandResult, SlidesCommandOptions } from "./context.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/lint.d.ts
|
|
4
|
+
interface LintSlidesOptions extends SlidesCommandOptions {
|
|
5
|
+
cliArgs?: string[];
|
|
6
|
+
}
|
|
7
|
+
interface LintSlidesResult {
|
|
8
|
+
strict: boolean;
|
|
9
|
+
warnings: string[];
|
|
10
|
+
slidesSourceFile: string;
|
|
11
|
+
}
|
|
12
|
+
declare function lintSlides(options?: LintSlidesOptions): Promise<LintSlidesResult>;
|
|
13
|
+
declare function runSlidesLint(options?: LintSlidesOptions): Promise<CommandResult>;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { lintSlides, runSlidesLint };
|
package/dist/lint.mjs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createFailureResult, createSuccessResult, resolveSlidesCommandContext } from "./context.mjs";
|
|
2
|
+
import { parseSlides } from "./slides/parsing/parseSlides.mjs";
|
|
3
|
+
import { validateSlidesAuthoring } from "./slides/validation/validateSlidesAuthoring.mjs";
|
|
4
|
+
import { parseLintArgs } from "./cli/lintArgs.mjs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
//#region src/lint.ts
|
|
8
|
+
async function lintSlides(options = {}) {
|
|
9
|
+
const parsedArgs = parseLintArgs(options.cliArgs ?? []);
|
|
10
|
+
const context = resolveSlidesCommandContext({
|
|
11
|
+
...options,
|
|
12
|
+
slidesFile: options.slidesFile ?? parsedArgs.slidesFile
|
|
13
|
+
});
|
|
14
|
+
const slides = parseSlides(await readFile(context.slidesSourceFile, "utf8"));
|
|
15
|
+
const warnings = await validateSlidesAuthoring({
|
|
16
|
+
appRoot: context.appRoot,
|
|
17
|
+
slides
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
strict: parsedArgs.strict,
|
|
21
|
+
warnings,
|
|
22
|
+
slidesSourceFile: context.slidesSourceFile
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async function runSlidesLint(options = {}) {
|
|
26
|
+
const result = await lintSlides(options);
|
|
27
|
+
if (result.warnings.length === 0) {
|
|
28
|
+
console.log(`Slides lint passed: no authoring warnings for ${path.relative(process.cwd(), result.slidesSourceFile)}`);
|
|
29
|
+
return createSuccessResult();
|
|
30
|
+
}
|
|
31
|
+
console.warn(`Slides lint found ${result.warnings.length} warning${result.warnings.length === 1 ? "" : "s"}:`);
|
|
32
|
+
for (const warning of result.warnings) console.warn(`- ${warning}`);
|
|
33
|
+
return result.strict ? createFailureResult(1) : createSuccessResult();
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { lintSlides, runSlidesLint };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { resolveSlidesSourceFile } from "./slidesSourceFile.mjs";
|
|
2
|
+
import { generatedSlidesAlias, generatedSlidesEntry, pluginCompileTimeSlides } from "./generateCompiledSlides.mjs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import react from "@vitejs/plugin-react";
|
|
5
|
+
//#region src/slides/build/createSlidesViteConfig.ts
|
|
6
|
+
function createSlidesViteConfig(options) {
|
|
7
|
+
const { appRoot, slidesFile } = options;
|
|
8
|
+
return {
|
|
9
|
+
root: appRoot,
|
|
10
|
+
plugins: [pluginCompileTimeSlides({
|
|
11
|
+
appRoot,
|
|
12
|
+
slidesSourceFile: resolveSlidesSourceFile(appRoot, slidesFile)
|
|
13
|
+
}), react()],
|
|
14
|
+
resolve: { alias: {
|
|
15
|
+
"@": path.resolve(appRoot, "./packages/client/src"),
|
|
16
|
+
[generatedSlidesAlias]: path.resolve(appRoot, generatedSlidesEntry),
|
|
17
|
+
react: path.resolve(appRoot, "./node_modules/react"),
|
|
18
|
+
"react-dom": path.resolve(appRoot, "./node_modules/react-dom"),
|
|
19
|
+
"react/jsx-runtime": path.resolve(appRoot, "./node_modules/react/jsx-runtime.js"),
|
|
20
|
+
"react/jsx-dev-runtime": path.resolve(appRoot, "./node_modules/react/jsx-dev-runtime.js")
|
|
21
|
+
} }
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { createSlidesViteConfig };
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { getMdxCompileOptions } from "../compiling/mdx-options.mjs";
|
|
2
|
+
import { parseImportedSlides, parseSlides } from "../parsing/parseSlides.mjs";
|
|
3
|
+
import { validateSlidesAuthoring } from "../validation/validateSlidesAuthoring.mjs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
7
|
+
import { compile } from "@mdx-js/mdx";
|
|
8
|
+
//#region src/slides/build/generateCompiledSlides.ts
|
|
9
|
+
const GENERATED_SLIDES_DIR_ROOT = ".generated/slides";
|
|
10
|
+
const GENERATED_SLIDE_MODULES_DIR = `${GENERATED_SLIDES_DIR_ROOT}/slides`;
|
|
11
|
+
const GENERATED_MANIFEST_FILE = `${GENERATED_SLIDES_DIR_ROOT}/index.ts`;
|
|
12
|
+
function hashString(source) {
|
|
13
|
+
return createHash("sha256").update(source).digest("hex").slice(0, 12);
|
|
14
|
+
}
|
|
15
|
+
function toModuleIdentifier(slideId) {
|
|
16
|
+
return slideId.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
17
|
+
}
|
|
18
|
+
function createSlideModuleFileName(index) {
|
|
19
|
+
return `slide-${index + 1}.tsx`;
|
|
20
|
+
}
|
|
21
|
+
function createSlideModuleCode(compiledSource) {
|
|
22
|
+
return [
|
|
23
|
+
"/* eslint-disable */",
|
|
24
|
+
"/* @generated by compile-time slides pipeline */",
|
|
25
|
+
compiledSource.trim(),
|
|
26
|
+
""
|
|
27
|
+
].join("\n");
|
|
28
|
+
}
|
|
29
|
+
function normalizeDiagramCodeProps(compiledSource) {
|
|
30
|
+
return compiledSource.replace(/<(MermaidDiagram|PlantUmlDiagram) code="([\s\S]*?)" \/>/g, (_match, componentName, code) => `<${componentName} code={${JSON.stringify(code)}} />`);
|
|
31
|
+
}
|
|
32
|
+
function createManifestCode({ deckSourceHash, deckMeta, slideEntries }) {
|
|
33
|
+
const imports = slideEntries.map((entry) => `import ${entry.importName} from '${entry.importPath}'`).join("\n");
|
|
34
|
+
const slides = slideEntries.map((entry) => ` {
|
|
35
|
+
id: ${JSON.stringify(entry.id)},
|
|
36
|
+
meta: ${JSON.stringify(entry.meta, null, 6)},
|
|
37
|
+
component: ${entry.importName},
|
|
38
|
+
}`).join(",\n");
|
|
39
|
+
return [
|
|
40
|
+
"/* eslint-disable */",
|
|
41
|
+
"/* @generated by compile-time slides pipeline */",
|
|
42
|
+
"import type { CompiledSlidesManifest } from '@slidev-react/core/slides/compiled-slides'",
|
|
43
|
+
imports,
|
|
44
|
+
"",
|
|
45
|
+
"const compiledSlides: CompiledSlidesManifest = {",
|
|
46
|
+
` sourceHash: ${JSON.stringify(deckSourceHash)},`,
|
|
47
|
+
` meta: ${JSON.stringify(deckMeta, null, 2)},`,
|
|
48
|
+
" slides: [",
|
|
49
|
+
slides,
|
|
50
|
+
" ],",
|
|
51
|
+
"}",
|
|
52
|
+
"",
|
|
53
|
+
"export default compiledSlides",
|
|
54
|
+
""
|
|
55
|
+
].join("\n");
|
|
56
|
+
}
|
|
57
|
+
async function writeIfChanged(filePath, content) {
|
|
58
|
+
try {
|
|
59
|
+
if (await readFile(filePath, "utf8") === content) return;
|
|
60
|
+
} catch {}
|
|
61
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
62
|
+
await writeFile(filePath, content, "utf8");
|
|
63
|
+
}
|
|
64
|
+
async function removeStaleGeneratedSlides(slidesDir, expectedFiles) {
|
|
65
|
+
try {
|
|
66
|
+
const files = await readdir(slidesDir);
|
|
67
|
+
await Promise.all(files.map(async (fileName) => {
|
|
68
|
+
if (!expectedFiles.has(fileName)) await unlink(path.join(slidesDir, fileName));
|
|
69
|
+
}));
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
async function compileSlideModule(source) {
|
|
73
|
+
const compiled = await compile(source, {
|
|
74
|
+
...getMdxCompileOptions(),
|
|
75
|
+
development: false,
|
|
76
|
+
jsx: true,
|
|
77
|
+
jsxImportSource: "react",
|
|
78
|
+
providerImportSource: "@mdx-js/react"
|
|
79
|
+
});
|
|
80
|
+
return normalizeDiagramCodeProps(String(compiled));
|
|
81
|
+
}
|
|
82
|
+
function createSlidesSourceHash({ slidesSource, resolvedSlides }) {
|
|
83
|
+
return hashString(JSON.stringify({
|
|
84
|
+
slidesSource,
|
|
85
|
+
slides: resolvedSlides.map((slide) => ({
|
|
86
|
+
id: slide.id,
|
|
87
|
+
source: slide.source,
|
|
88
|
+
externalFilePath: slide.externalFilePath,
|
|
89
|
+
externalFileSource: slide.externalFileSource
|
|
90
|
+
}))
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
async function resolveSlideUnitSource({ slide, slidesSourceFile }) {
|
|
94
|
+
const slideSrc = slide.meta.src?.trim();
|
|
95
|
+
if (!slideSrc) return [{
|
|
96
|
+
...slide,
|
|
97
|
+
watchedFiles: [],
|
|
98
|
+
externalFilePath: void 0,
|
|
99
|
+
externalFileSource: void 0
|
|
100
|
+
}];
|
|
101
|
+
if (slide.hasInlineSource) throw new Error(`Slide ${slide.index + 1}${slide.meta.title ? ` (${slide.meta.title})` : ""} mixes inline content with src. Use one or the other.`);
|
|
102
|
+
const externalFilePath = path.resolve(path.dirname(slidesSourceFile), slideSrc);
|
|
103
|
+
let externalFileSource;
|
|
104
|
+
try {
|
|
105
|
+
externalFileSource = await readFile(externalFilePath, "utf8");
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(`Failed to load slide ${slide.index + 1}${slide.meta.title ? ` (${slide.meta.title})` : ""} src "${slideSrc}": ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
108
|
+
}
|
|
109
|
+
const importedSlides = parseImportedSlides(externalFileSource);
|
|
110
|
+
for (const importedSlide of importedSlides) {
|
|
111
|
+
if (!importedSlide.meta.src) continue;
|
|
112
|
+
throw new Error(`Nested src is not supported in slide ${slide.index + 1}${slide.meta.title ? ` (${slide.meta.title})` : ""}.`);
|
|
113
|
+
}
|
|
114
|
+
return importedSlides.map((importedSlide) => ({
|
|
115
|
+
...importedSlide,
|
|
116
|
+
meta: {
|
|
117
|
+
...importedSlide.meta,
|
|
118
|
+
...slide.meta
|
|
119
|
+
},
|
|
120
|
+
watchedFiles: [externalFilePath],
|
|
121
|
+
externalFilePath,
|
|
122
|
+
externalFileSource
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
async function generateCompiledSlidesArtifacts(options) {
|
|
126
|
+
const { appRoot, slidesSourceFile } = options;
|
|
127
|
+
const slidesSource = await readFile(slidesSourceFile, "utf8");
|
|
128
|
+
const parsedSlides = parseSlides(slidesSource);
|
|
129
|
+
const generatedSlidesRootDir = path.join(appRoot, GENERATED_SLIDES_DIR_ROOT);
|
|
130
|
+
const generatedSlideModulesDir = path.join(appRoot, GENERATED_SLIDE_MODULES_DIR);
|
|
131
|
+
const manifestFile = path.join(appRoot, GENERATED_MANIFEST_FILE);
|
|
132
|
+
const expectedSlideFiles = /* @__PURE__ */ new Set();
|
|
133
|
+
const watchedFiles = new Set([slidesSourceFile]);
|
|
134
|
+
const manifestEntries = [];
|
|
135
|
+
const resolvedSlides = (await Promise.all(parsedSlides.slides.map((slide) => resolveSlideUnitSource({
|
|
136
|
+
slide,
|
|
137
|
+
slidesSourceFile
|
|
138
|
+
})))).flat().map((slide, index) => ({
|
|
139
|
+
...slide,
|
|
140
|
+
id: `slide-${index + 1}`,
|
|
141
|
+
index
|
|
142
|
+
}));
|
|
143
|
+
const warnings = await validateSlidesAuthoring({
|
|
144
|
+
appRoot,
|
|
145
|
+
slides: {
|
|
146
|
+
meta: parsedSlides.meta,
|
|
147
|
+
slides: resolvedSlides
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
const sourceHash = createSlidesSourceHash({
|
|
151
|
+
slidesSource,
|
|
152
|
+
resolvedSlides
|
|
153
|
+
});
|
|
154
|
+
await mkdir(generatedSlideModulesDir, { recursive: true });
|
|
155
|
+
for (const slide of resolvedSlides) {
|
|
156
|
+
const slideFileName = createSlideModuleFileName(slide.index);
|
|
157
|
+
const slideFilePath = path.join(generatedSlideModulesDir, slideFileName);
|
|
158
|
+
const importName = toModuleIdentifier(slide.id);
|
|
159
|
+
for (const watchedFile of slide.watchedFiles) watchedFiles.add(watchedFile);
|
|
160
|
+
try {
|
|
161
|
+
await writeIfChanged(slideFilePath, createSlideModuleCode(await compileSlideModule(slide.source)));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
const slideName = slide.meta.title ? `${slide.index + 1} (${slide.meta.title})` : String(slide.index + 1);
|
|
164
|
+
throw new Error(`Failed to compile slide ${slideName}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
165
|
+
}
|
|
166
|
+
expectedSlideFiles.add(slideFileName);
|
|
167
|
+
manifestEntries.push({
|
|
168
|
+
id: slide.id,
|
|
169
|
+
importName,
|
|
170
|
+
importPath: `./slides/${path.basename(slideFileName, path.extname(slideFileName))}`,
|
|
171
|
+
meta: slide.meta
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
await removeStaleGeneratedSlides(generatedSlideModulesDir, expectedSlideFiles);
|
|
175
|
+
await writeIfChanged(manifestFile, createManifestCode({
|
|
176
|
+
deckSourceHash: sourceHash,
|
|
177
|
+
deckMeta: parsedSlides.meta,
|
|
178
|
+
slideEntries: manifestEntries
|
|
179
|
+
}));
|
|
180
|
+
return {
|
|
181
|
+
generatedSlidesRootDir,
|
|
182
|
+
manifestFile,
|
|
183
|
+
sourceHash,
|
|
184
|
+
watchedFiles: [...watchedFiles],
|
|
185
|
+
warnings
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function createSlidesGenerator(options) {
|
|
189
|
+
let generatePromise = null;
|
|
190
|
+
let rerunRequested = false;
|
|
191
|
+
let watchedFiles = new Set([options.slidesSourceFile]);
|
|
192
|
+
let warnings = [];
|
|
193
|
+
const generate = async () => {
|
|
194
|
+
if (generatePromise) {
|
|
195
|
+
rerunRequested = true;
|
|
196
|
+
await generatePromise;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
generatePromise = (async () => {
|
|
200
|
+
do {
|
|
201
|
+
rerunRequested = false;
|
|
202
|
+
const result = await generateCompiledSlidesArtifacts(options);
|
|
203
|
+
watchedFiles = new Set(result.watchedFiles);
|
|
204
|
+
warnings = result.warnings;
|
|
205
|
+
} while (rerunRequested);
|
|
206
|
+
})();
|
|
207
|
+
try {
|
|
208
|
+
await generatePromise;
|
|
209
|
+
} finally {
|
|
210
|
+
generatePromise = null;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
generate,
|
|
215
|
+
getWatchedFiles: () => [...watchedFiles],
|
|
216
|
+
getWarnings: () => warnings
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
async function regenerateSlidesForServer(server, generate) {
|
|
220
|
+
try {
|
|
221
|
+
await generate();
|
|
222
|
+
server.ws.send({ type: "full-reload" });
|
|
223
|
+
} catch (error) {
|
|
224
|
+
server.config.logger.error(error instanceof Error ? error.stack ?? error.message : String(error), { timestamp: true });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function pluginCompileTimeSlides(options) {
|
|
228
|
+
const slidesSourceFile = path.resolve(options.slidesSourceFile);
|
|
229
|
+
const generator = createSlidesGenerator({
|
|
230
|
+
...options,
|
|
231
|
+
slidesSourceFile
|
|
232
|
+
});
|
|
233
|
+
return {
|
|
234
|
+
name: "slide-react-compile-time-slides",
|
|
235
|
+
async buildStart() {
|
|
236
|
+
await generator.generate();
|
|
237
|
+
for (const warning of generator.getWarnings()) this.warn(warning);
|
|
238
|
+
for (const watchedFile of generator.getWatchedFiles()) this.addWatchFile(watchedFile);
|
|
239
|
+
},
|
|
240
|
+
configureServer(server) {
|
|
241
|
+
server.watcher.add(slidesSourceFile);
|
|
242
|
+
const syncWatchedFiles = () => {
|
|
243
|
+
server.watcher.add(generator.getWatchedFiles());
|
|
244
|
+
};
|
|
245
|
+
const handleSlidesChange = async (filePath) => {
|
|
246
|
+
const resolvedFilePath = path.resolve(filePath);
|
|
247
|
+
if (!generator.getWatchedFiles().includes(resolvedFilePath)) return;
|
|
248
|
+
await regenerateSlidesForServer(server, async () => {
|
|
249
|
+
await generator.generate();
|
|
250
|
+
for (const warning of generator.getWarnings()) server.config.logger.warn(warning, { timestamp: true });
|
|
251
|
+
syncWatchedFiles();
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
syncWatchedFiles();
|
|
255
|
+
server.watcher.on("add", handleSlidesChange);
|
|
256
|
+
server.watcher.on("change", handleSlidesChange);
|
|
257
|
+
server.watcher.on("unlink", handleSlidesChange);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const generatedSlidesAlias = "@generated/slides";
|
|
262
|
+
const generatedSlidesEntry = path.join(GENERATED_SLIDES_DIR_ROOT, "index.ts");
|
|
263
|
+
//#endregion
|
|
264
|
+
export { generatedSlidesAlias, generatedSlidesEntry, pluginCompileTimeSlides };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
//#region src/slides/build/slidesSourceFile.ts
|
|
3
|
+
const DEFAULT_SLIDES_SOURCE_FILE = "slides.mdx";
|
|
4
|
+
function resolveSlidesSourceFile(appRoot, slidesFile) {
|
|
5
|
+
const configuredSlidesFile = slidesFile?.trim() || process.env.SLIDES_FILE?.trim() || DEFAULT_SLIDES_SOURCE_FILE;
|
|
6
|
+
return path.resolve(appRoot, configuredSlidesFile);
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { resolveSlidesSourceFile };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { remarkDiagramComponents } from "../mdx/remarkDiagramComponents.mjs";
|
|
2
|
+
import { rehypeShikiVitesse } from "./rehypeShikiVitesse.mjs";
|
|
3
|
+
import rehypeKatex from "rehype-katex";
|
|
4
|
+
import remarkGfm from "remark-gfm";
|
|
5
|
+
import remarkMath from "remark-math";
|
|
6
|
+
//#region src/slides/compiling/mdx-options.ts
|
|
7
|
+
function getMdxCompileOptions() {
|
|
8
|
+
return {
|
|
9
|
+
remarkPlugins: [
|
|
10
|
+
remarkGfm,
|
|
11
|
+
remarkMath,
|
|
12
|
+
remarkDiagramComponents
|
|
13
|
+
],
|
|
14
|
+
rehypePlugins: [rehypeKatex, rehypeShikiVitesse]
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { getMdxCompileOptions };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHighlighter } from "shiki";
|
|
2
|
+
//#region src/slides/compiling/rehypeShikiVitesse.ts
|
|
3
|
+
const SHIKI_THEME = "vitesse-light";
|
|
4
|
+
const PLAIN_TEXT_LANG = "txt";
|
|
5
|
+
const preloadedLangs = [
|
|
6
|
+
"md",
|
|
7
|
+
"markdown",
|
|
8
|
+
"js",
|
|
9
|
+
"jsx",
|
|
10
|
+
"ts",
|
|
11
|
+
"tsx",
|
|
12
|
+
"json",
|
|
13
|
+
"bash",
|
|
14
|
+
"sh",
|
|
15
|
+
"yaml",
|
|
16
|
+
"yml",
|
|
17
|
+
"html",
|
|
18
|
+
"css",
|
|
19
|
+
"vue",
|
|
20
|
+
"txt"
|
|
21
|
+
];
|
|
22
|
+
let highlighterPromise = null;
|
|
23
|
+
function getHighlighter() {
|
|
24
|
+
if (!highlighterPromise) highlighterPromise = createHighlighter({
|
|
25
|
+
themes: [SHIKI_THEME],
|
|
26
|
+
langs: preloadedLangs
|
|
27
|
+
});
|
|
28
|
+
return highlighterPromise;
|
|
29
|
+
}
|
|
30
|
+
function asArray(value) {
|
|
31
|
+
if (!value) return [];
|
|
32
|
+
if (Array.isArray(value)) return value.map((item) => {
|
|
33
|
+
if (typeof item === "string") return item;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.stringify(item) ?? "";
|
|
36
|
+
} catch {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
if (typeof value === "string") return [value];
|
|
41
|
+
try {
|
|
42
|
+
return [JSON.stringify(value) ?? ""];
|
|
43
|
+
} catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function isElement(node, tagName) {
|
|
48
|
+
return node?.type === "element" && (!tagName || node.tagName === tagName);
|
|
49
|
+
}
|
|
50
|
+
function extractText(node) {
|
|
51
|
+
if (!node) return "";
|
|
52
|
+
if (node.type === "text") return node.value ?? "";
|
|
53
|
+
if (!node.children) return "";
|
|
54
|
+
return node.children.map((child) => extractText(child)).join("");
|
|
55
|
+
}
|
|
56
|
+
function detectLanguage(preNode) {
|
|
57
|
+
const codeNode = preNode.children?.find((child) => isElement(child, "code")) ?? null;
|
|
58
|
+
if (!codeNode) return PLAIN_TEXT_LANG;
|
|
59
|
+
const languageClass = asArray(codeNode.properties?.className).find((name) => name.startsWith("language-"));
|
|
60
|
+
if (!languageClass) return PLAIN_TEXT_LANG;
|
|
61
|
+
return languageClass.slice(9).trim() || PLAIN_TEXT_LANG;
|
|
62
|
+
}
|
|
63
|
+
async function highlight(code, lang) {
|
|
64
|
+
const highlighter = await getHighlighter();
|
|
65
|
+
try {
|
|
66
|
+
return highlighter.codeToHast(code, {
|
|
67
|
+
lang,
|
|
68
|
+
theme: SHIKI_THEME
|
|
69
|
+
});
|
|
70
|
+
} catch {
|
|
71
|
+
return highlighter.codeToHast(code, {
|
|
72
|
+
lang: PLAIN_TEXT_LANG,
|
|
73
|
+
theme: SHIKI_THEME
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function walk(node, parent, index) {
|
|
78
|
+
if (isElement(node, "pre") && parent && index !== null) {
|
|
79
|
+
const rendered = await highlight(extractText(node), detectLanguage(node));
|
|
80
|
+
const replacement = rendered?.type === "root" ? rendered.children?.[0] : rendered;
|
|
81
|
+
if (replacement && parent.children) parent.children[index] = replacement;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!node.children) return;
|
|
85
|
+
for (let childIndex = 0; childIndex < node.children.length; childIndex += 1) await walk(node.children[childIndex], node, childIndex);
|
|
86
|
+
}
|
|
87
|
+
function rehypeShikiVitesse() {
|
|
88
|
+
return async (tree) => {
|
|
89
|
+
await walk(tree, null, null);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
export { rehypeShikiVitesse };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/slides/mdx/remarkDiagramComponents.ts
|
|
2
|
+
function createDiagramNode(name, code) {
|
|
3
|
+
return {
|
|
4
|
+
type: "mdxJsxFlowElement",
|
|
5
|
+
name,
|
|
6
|
+
attributes: [],
|
|
7
|
+
children: [{
|
|
8
|
+
type: "text",
|
|
9
|
+
value: code
|
|
10
|
+
}]
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function walk(parent) {
|
|
14
|
+
const children = parent?.children;
|
|
15
|
+
if (!Array.isArray(children)) return;
|
|
16
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
17
|
+
const node = children[index];
|
|
18
|
+
if (node?.type === "code") {
|
|
19
|
+
const lang = String(node.lang || "").trim().toLowerCase();
|
|
20
|
+
if (lang === "mermaid") {
|
|
21
|
+
children[index] = createDiagramNode("MermaidDiagram", node.value || "");
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (lang === "plantuml" || lang === "startuml") {
|
|
25
|
+
children[index] = createDiagramNode("PlantUmlDiagram", node.value || "");
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
walk(node);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function remarkDiagramComponents() {
|
|
33
|
+
return (tree) => {
|
|
34
|
+
walk(tree);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { remarkDiagramComponents };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
//#region src/slides/parsing/frontmatter.ts
|
|
4
|
+
const OPEN = "---";
|
|
5
|
+
const frontmatterDataSchema = z.object({}).catchall(z.unknown());
|
|
6
|
+
function parseFrontmatter(source) {
|
|
7
|
+
const normalized = source.replace(/\r\n/g, "\n");
|
|
8
|
+
if (!normalized.startsWith(`${OPEN}\n`)) return {
|
|
9
|
+
data: {},
|
|
10
|
+
content: normalized
|
|
11
|
+
};
|
|
12
|
+
const closeMarkerWithNewline = `\n${OPEN}\n`;
|
|
13
|
+
const closeMarkerAtEnd = `\n${OPEN}`;
|
|
14
|
+
const withNewlineIndex = normalized.indexOf(closeMarkerWithNewline, 4);
|
|
15
|
+
const atEndIndex = withNewlineIndex < 0 && normalized.endsWith(closeMarkerAtEnd) ? normalized.length - closeMarkerAtEnd.length : -1;
|
|
16
|
+
const endIndex = withNewlineIndex >= 0 ? withNewlineIndex : atEndIndex;
|
|
17
|
+
if (endIndex < 0) throw new Error("Invalid frontmatter: missing closing ---");
|
|
18
|
+
const yamlSource = normalized.slice(4, endIndex);
|
|
19
|
+
const raw = YAML.parse(yamlSource);
|
|
20
|
+
const parsed = frontmatterDataSchema.safeParse(raw ?? {});
|
|
21
|
+
if (!parsed.success) throw new Error("Invalid frontmatter: expected an object");
|
|
22
|
+
const contentStart = endIndex + (withNewlineIndex >= 0 ? closeMarkerWithNewline.length : closeMarkerAtEnd.length);
|
|
23
|
+
return {
|
|
24
|
+
data: parsed.data,
|
|
25
|
+
content: normalized.slice(contentStart)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { parseFrontmatter };
|