@kadal/deck 0.0.0-stage → 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 +661 -0
- package/LICENSING.md +40 -0
- package/README.md +170 -2
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/audience/index.d.ts +14 -0
- package/dist/audience/index.js +680 -0
- package/dist/backgrounds/index.d.ts +65 -0
- package/dist/backgrounds/index.js +604 -0
- package/dist/backgrounds/shared.d.ts +7 -0
- package/dist/backgrounds/shared.js +89 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +8 -0
- package/dist/cli/host.js +111 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +140 -0
- package/dist/cli/login.js +56 -0
- package/dist/cli/publish.js +159 -0
- package/dist/cli/vite.js +39 -0
- package/dist/compiler/code-meta.d.ts +33 -0
- package/dist/compiler/code-meta.js +81 -0
- package/dist/compiler/index.d.ts +8 -0
- package/dist/compiler/index.js +8 -0
- package/dist/compiler/magic-move.d.ts +26 -0
- package/dist/compiler/magic-move.js +33 -0
- package/dist/compiler/plain-text.js +7 -0
- package/dist/compiler/rehype-code.d.ts +18 -0
- package/dist/compiler/rehype-code.js +66 -0
- package/dist/compiler/rehype-glossary.d.ts +35 -0
- package/dist/compiler/rehype-glossary.js +211 -0
- package/dist/compiler/slots.d.ts +3 -0
- package/dist/compiler/slots.js +36 -0
- package/dist/compiler/split.d.ts +63 -0
- package/dist/compiler/split.js +195 -0
- package/dist/components/Background.d.ts +15 -0
- package/dist/components/Background.js +24 -0
- package/dist/components/Click.d.ts +54 -0
- package/dist/components/Click.js +129 -0
- package/dist/components/Code.d.ts +27 -0
- package/dist/components/Code.js +124 -0
- package/dist/components/Motion.d.ts +25 -0
- package/dist/components/Motion.js +45 -0
- package/dist/components/Sketch.d.ts +44 -0
- package/dist/components/Sketch.js +141 -0
- package/dist/components/Term.d.ts +63 -0
- package/dist/components/Term.js +492 -0
- package/dist/components/bling.d.ts +75 -0
- package/dist/components/bling.js +146 -0
- package/dist/components/content.d.ts +232 -0
- package/dist/components/content.js +637 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.js +88 -0
- package/dist/core/clicks.d.ts +45 -0
- package/dist/core/clicks.js +75 -0
- package/dist/core/hash.d.ts +33 -0
- package/dist/core/hash.js +49 -0
- package/dist/core/keyboard.d.ts +17 -0
- package/dist/core/keyboard.js +198 -0
- package/dist/core/layout.d.ts +16 -0
- package/dist/core/layout.js +25 -0
- package/dist/core/model.d.ts +259 -0
- package/dist/core/model.js +377 -0
- package/dist/core/navigation.d.ts +74 -0
- package/dist/core/navigation.js +244 -0
- package/dist/core/poster.js +30 -0
- package/dist/core/store.d.ts +20 -0
- package/dist/core/store.js +64 -0
- package/dist/core/touch.js +27 -0
- package/dist/draw/DrawingLayer.js +315 -0
- package/dist/draw/strokes.js +188 -0
- package/dist/export/index.d.ts +68 -0
- package/dist/export/index.js +251 -0
- package/dist/export/run.js +81 -0
- package/dist/glossary/model.d.ts +81 -0
- package/dist/glossary/model.js +156 -0
- package/dist/glossary/parse.d.ts +27 -0
- package/dist/glossary/parse.js +90 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +30 -0
- package/dist/live/LiveProvider.d.ts +20 -0
- package/dist/live/LiveProvider.js +450 -0
- package/dist/live/analytics.d.ts +15 -0
- package/dist/live/analytics.js +103 -0
- package/dist/live/client.d.ts +51 -0
- package/dist/live/client.js +193 -0
- package/dist/live/components.d.ts +64 -0
- package/dist/live/components.js +471 -0
- package/dist/live/context.d.ts +15 -0
- package/dist/live/context.js +28 -0
- package/dist/live/index.d.ts +7 -0
- package/dist/live/index.js +7 -0
- package/dist/live/protocol.d.ts +284 -0
- package/dist/live/protocol.js +42 -0
- package/dist/live/turnstile.js +49 -0
- package/dist/presenter/index.d.ts +20 -0
- package/dist/presenter/index.js +1211 -0
- package/dist/react/AutoAnimate.js +92 -0
- package/dist/react/Backgrounds.d.ts +11 -0
- package/dist/react/Backgrounds.js +154 -0
- package/dist/react/ConfettiLayer.js +85 -0
- package/dist/react/Deck.d.ts +35 -0
- package/dist/react/Deck.js +983 -0
- package/dist/react/Frame.d.ts +16 -0
- package/dist/react/Frame.js +174 -0
- package/dist/react/Hints.d.ts +3 -0
- package/dist/react/Hints.js +201 -0
- package/dist/react/Lightbox.js +54 -0
- package/dist/react/Overview.js +93 -0
- package/dist/react/PrintView.js +128 -0
- package/dist/react/ScrollView.js +128 -0
- package/dist/react/SlideFrame.d.ts +51 -0
- package/dist/react/SlideFrame.js +323 -0
- package/dist/react/Stage.js +151 -0
- package/dist/react/ThemeMenu.d.ts +5 -0
- package/dist/react/ThemeMenu.js +124 -0
- package/dist/react/Zoom.js +23 -0
- package/dist/react/chrome.js +339 -0
- package/dist/react/context.d.ts +165 -0
- package/dist/react/context.js +19 -0
- package/dist/react/hooks.d.ts +33 -0
- package/dist/react/hooks.js +70 -0
- package/dist/react/layouts.d.ts +21 -0
- package/dist/react/layouts.js +104 -0
- package/dist/react/mdx.d.ts +4 -0
- package/dist/react/mdx.js +33 -0
- package/dist/react/terms.d.ts +6 -0
- package/dist/react/terms.js +66 -0
- package/dist/renderers/Island.d.ts +57 -0
- package/dist/renderers/Island.js +223 -0
- package/dist/renderers/contract.d.ts +173 -0
- package/dist/renderers/contract.js +152 -0
- package/dist/renderers/index.d.ts +12 -0
- package/dist/renderers/index.js +16 -0
- package/dist/renderers/react.d.ts +16 -0
- package/dist/renderers/react.js +32 -0
- package/dist/renderers/web-components.d.ts +4 -0
- package/dist/renderers/web-components.js +103 -0
- package/dist/sketch/Canvas.js +49 -0
- package/dist/sketch/dsl.d.ts +131 -0
- package/dist/sketch/dsl.js +281 -0
- package/dist/sketch/index.d.ts +4 -0
- package/dist/sketch/index.js +3 -0
- package/dist/sketch/palette.d.ts +21 -0
- package/dist/sketch/palette.js +66 -0
- package/dist/sketch/render.d.ts +43 -0
- package/dist/sketch/render.js +198 -0
- package/dist/themes/base.css +1704 -0
- package/dist/themes/bling.css +592 -0
- package/dist/themes/glossary.css +317 -0
- package/dist/themes/live.css +1557 -0
- package/dist/themes/minimal.css +83 -0
- package/dist/themes/rla.css +92 -0
- package/dist/themes/sketch.css +121 -0
- package/dist/themes/stack.css +62 -0
- package/dist/vite/index.d.ts +15 -0
- package/dist/vite/index.js +320 -0
- package/package.json +159 -4
- package/tools/verify-island.mjs +150 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//#region src/compiler/code-meta.ts
|
|
2
|
+
function parseLineSet(spec, total) {
|
|
3
|
+
const s = spec.trim();
|
|
4
|
+
if (s === "" || s === "all" || s === "*") return null;
|
|
5
|
+
if (s === "none" || s === "hide") return /* @__PURE__ */ new Set();
|
|
6
|
+
const out = /* @__PURE__ */ new Set();
|
|
7
|
+
for (const part of s.split(",")) {
|
|
8
|
+
const p = part.trim();
|
|
9
|
+
if (!p) continue;
|
|
10
|
+
const range = /^(\d+)\s*-\s*(\d+)?$/.exec(p);
|
|
11
|
+
if (range) {
|
|
12
|
+
const a = Number(range[1]);
|
|
13
|
+
const b = range[2] ? Number(range[2]) : total ?? a;
|
|
14
|
+
for (let i = Math.min(a, b); i <= Math.max(a, b); i++) out.add(i);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const n = Number(p);
|
|
18
|
+
if (Number.isFinite(n)) out.add(n);
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
function parseOptions(text, into) {
|
|
23
|
+
for (const pair of text.split(",")) {
|
|
24
|
+
const m = /^\s*([\w-]+)\s*:\s*(.+?)\s*$/.exec(pair);
|
|
25
|
+
if (!m) continue;
|
|
26
|
+
const key = m[1];
|
|
27
|
+
const raw = m[2].replace(/^['"]|['"]$/g, "");
|
|
28
|
+
if (key === "lines") into.lines = raw !== "false";
|
|
29
|
+
else if (key === "startLine") into.startLine = Number(raw) || 1;
|
|
30
|
+
else if (key === "maxHeight") into.maxHeight = raw;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function parseCodeMeta(meta) {
|
|
34
|
+
const out = {
|
|
35
|
+
steps: null,
|
|
36
|
+
lines: false,
|
|
37
|
+
startLine: 1,
|
|
38
|
+
title: null,
|
|
39
|
+
maxHeight: null,
|
|
40
|
+
hideUntilStep: false
|
|
41
|
+
};
|
|
42
|
+
if (!meta) return out;
|
|
43
|
+
let rest = meta.trim();
|
|
44
|
+
const reveal = /data-line-numbers(?:="([^"]*)")?/.exec(rest);
|
|
45
|
+
if (reveal) {
|
|
46
|
+
out.lines = true;
|
|
47
|
+
if (reveal[1]) out.steps = reveal[1].split("|").map((s) => parseLineSet(s));
|
|
48
|
+
rest = rest.replace(reveal[0], "");
|
|
49
|
+
const start = /data-ln-start-from="(\d+)"/.exec(rest);
|
|
50
|
+
if (start) out.startLine = Number(start[1]);
|
|
51
|
+
}
|
|
52
|
+
const title = /\[([^\]]+)\]/.exec(rest);
|
|
53
|
+
if (title) {
|
|
54
|
+
out.title = title[1].trim();
|
|
55
|
+
rest = rest.replace(title[0], "");
|
|
56
|
+
}
|
|
57
|
+
const braces = [...rest.matchAll(/\{([^}]*)\}/g)].map((m) => m[1]);
|
|
58
|
+
if (braces.length) {
|
|
59
|
+
const first = braces[0];
|
|
60
|
+
if (!/^\s*[\w-]+\s*:/.test(first)) {
|
|
61
|
+
const stages = first.split("|").map((s) => s.trim());
|
|
62
|
+
if (stages[0] === "hide") {
|
|
63
|
+
out.hideUntilStep = true;
|
|
64
|
+
stages.shift();
|
|
65
|
+
}
|
|
66
|
+
out.steps = stages.map((s) => parseLineSet(s));
|
|
67
|
+
if (out.steps.length === 0) out.steps = null;
|
|
68
|
+
} else parseOptions(first, out);
|
|
69
|
+
for (const extra of braces.slice(1)) parseOptions(extra, out);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/** Serialise steps for a `data-steps` attribute: `2,3|5|` (empty = all). */
|
|
74
|
+
function serialiseSteps(steps) {
|
|
75
|
+
return steps.map((s) => s === null ? "*" : s.size === 0 ? "none" : [...s].sort((a, b) => a - b).join(",")).join("|");
|
|
76
|
+
}
|
|
77
|
+
function deserialiseSteps(text) {
|
|
78
|
+
return text.split("|").map((s) => parseLineSet(s));
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
export { deserialiseSteps, parseCodeMeta, parseLineSet, serialiseSteps };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Frontmatter, RawSlide, SplitOptions, SplitResult, levelOf, splitDeck, titleOf } from "./split.js";
|
|
2
|
+
import { CodeMeta, LineSet, deserialiseSteps, parseCodeMeta, parseLineSet, serialiseSteps } from "./code-meta.js";
|
|
3
|
+
import { MagicMoveBlock, MagicMoveStep, findMagicMoves } from "./magic-move.js";
|
|
4
|
+
import { rehypeDeckCode } from "./rehype-code.js";
|
|
5
|
+
import { RehypeGlossaryOptions, rehypeGlossary } from "./rehype-glossary.js";
|
|
6
|
+
import { ParsedGlossary, parseGlossary } from "../glossary/parse.js";
|
|
7
|
+
import { applySlots } from "./slots.js";
|
|
8
|
+
export { type CodeMeta, type Frontmatter, type LineSet, type MagicMoveBlock, type MagicMoveStep, type ParsedGlossary, type RawSlide, type RehypeGlossaryOptions, type SplitOptions, type SplitResult, applySlots, deserialiseSteps, findMagicMoves, levelOf, parseCodeMeta, parseGlossary, parseLineSet, rehypeDeckCode, rehypeGlossary, serialiseSteps, splitDeck, titleOf };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { deserialiseSteps, parseCodeMeta, parseLineSet, serialiseSteps } from "./code-meta.js";
|
|
2
|
+
import { levelOf, splitDeck, titleOf } from "./split.js";
|
|
3
|
+
import { findMagicMoves } from "./magic-move.js";
|
|
4
|
+
import { rehypeDeckCode } from "./rehype-code.js";
|
|
5
|
+
import { rehypeGlossary } from "./rehype-glossary.js";
|
|
6
|
+
import { parseGlossary } from "../glossary/parse.js";
|
|
7
|
+
import { applySlots } from "./slots.js";
|
|
8
|
+
export { applySlots, deserialiseSteps, findMagicMoves, levelOf, parseCodeMeta, parseGlossary, parseLineSet, rehypeDeckCode, rehypeGlossary, serialiseSteps, splitDeck, titleOf };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/compiler/magic-move.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shiki Magic Move, the Slidev syntax: a Markdown fence of four backticks with the language `md`
|
|
4
|
+
* and the word `magic-move`, holding one code fence per step. The block morphs from step to step
|
|
5
|
+
* on each click, Keynote-style. We find the block in the raw slide text (before MDX sees it),
|
|
6
|
+
* precompute the keyed tokens for every step at build time with shiki-magic-move's core, and hand
|
|
7
|
+
* the slide a `<MagicMove steps={…} />` in its place — the runtime then animates without shipping
|
|
8
|
+
* a highlighter to the browser.
|
|
9
|
+
*/
|
|
10
|
+
export interface MagicMoveStep {
|
|
11
|
+
code: string;
|
|
12
|
+
lang: string;
|
|
13
|
+
/** The fence meta, e.g. `{1|2-3}{lines:true}` — highlight steps within this step. */
|
|
14
|
+
meta: string;
|
|
15
|
+
}
|
|
16
|
+
export interface MagicMoveBlock {
|
|
17
|
+
/** `{at:4, lines: true, duration: 500}`-style options on the outer fence. */
|
|
18
|
+
options: Record<string, string>;
|
|
19
|
+
title: string | null;
|
|
20
|
+
steps: MagicMoveStep[];
|
|
21
|
+
}
|
|
22
|
+
export declare function findMagicMoves(text: string): {
|
|
23
|
+
text: string;
|
|
24
|
+
blocks: MagicMoveBlock[];
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/compiler/magic-move.ts
|
|
2
|
+
const OUTER = /^([ \t]*)````+\s*md\s+magic-move([^\n]*)\n([\s\S]*?)\n\1````+[ \t]*$/gm;
|
|
3
|
+
const INNER = /```([\w+-]*)([^\n]*)\n([\s\S]*?)\n```/g;
|
|
4
|
+
function findMagicMoves(text) {
|
|
5
|
+
const blocks = [];
|
|
6
|
+
return {
|
|
7
|
+
text: text.replace(OUTER, (_m, _indent, header, body) => {
|
|
8
|
+
const options = {};
|
|
9
|
+
const braces = /\{([^}]*)\}/.exec(header);
|
|
10
|
+
if (braces) for (const pair of braces[1].split(",")) {
|
|
11
|
+
const kv = /^\s*([\w-]+)\s*:\s*(.+?)\s*$/.exec(pair);
|
|
12
|
+
if (kv) options[kv[1]] = kv[2].replace(/^['"]|['"]$/g, "");
|
|
13
|
+
}
|
|
14
|
+
const title = /\[([^\]]+)\]/.exec(header);
|
|
15
|
+
const steps = [];
|
|
16
|
+
for (const m of body.matchAll(INNER)) steps.push({
|
|
17
|
+
lang: m[1] || "text",
|
|
18
|
+
meta: m[2].trim(),
|
|
19
|
+
code: m[3]
|
|
20
|
+
});
|
|
21
|
+
const id = blocks.length;
|
|
22
|
+
blocks.push({
|
|
23
|
+
options,
|
|
24
|
+
title: title ? title[1].trim() : null,
|
|
25
|
+
steps
|
|
26
|
+
});
|
|
27
|
+
return `\n<MagicMove block={${id}} />\n`;
|
|
28
|
+
}),
|
|
29
|
+
blocks
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { findMagicMoves };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/compiler/plain-text.ts
|
|
2
|
+
/** A slide's MDX reduced to searchable text: no tags, no fence noise, no attribute braces. */
|
|
3
|
+
function plainText(mdx) {
|
|
4
|
+
return mdx.replace(/```[^\n]*\n?/g, "").replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/<[^>]+>/g, " ").replace(/\{[^}]*\}/g, " ").replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[#*_`>~|]+/g, " ").replace(/\s+/g, " ").trim();
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
7
|
+
export { plainText };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Highlighter } from "shiki";
|
|
2
|
+
import { Root } from "hast";
|
|
3
|
+
//#region src/compiler/rehype-code.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Turn every fenced code block into highlighted HTML at build time, and carry the fence meta over
|
|
6
|
+
* as data attributes the runtime `<Code>` reads: which lines light up on which click, whether to
|
|
7
|
+
* number lines, the title, the max height. Highlighting is Shiki with a light and a dark theme at
|
|
8
|
+
* once; the CSS picks by the deck's colour scheme.
|
|
9
|
+
*/
|
|
10
|
+
export interface RehypeCodeOptions {
|
|
11
|
+
highlighter: Highlighter;
|
|
12
|
+
themes: {
|
|
13
|
+
light: string;
|
|
14
|
+
dark: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare function rehypeDeckCode(options: RehypeCodeOptions): (tree: Root) => void;
|
|
18
|
+
//#endregion
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { parseCodeMeta, serialiseSteps } from "./code-meta.js";
|
|
2
|
+
import { visit } from "unist-util-visit";
|
|
3
|
+
//#region src/compiler/rehype-code.ts
|
|
4
|
+
function langOf(node) {
|
|
5
|
+
const cls = node.properties?.className;
|
|
6
|
+
const list = Array.isArray(cls) ? cls : typeof cls === "string" ? [cls] : [];
|
|
7
|
+
for (const c of list) {
|
|
8
|
+
const m = /^language-([\w+-]+)$/.exec(String(c));
|
|
9
|
+
if (m) return m[1];
|
|
10
|
+
}
|
|
11
|
+
return "text";
|
|
12
|
+
}
|
|
13
|
+
function textOf(node) {
|
|
14
|
+
let out = "";
|
|
15
|
+
const walk = (n) => {
|
|
16
|
+
if (n.type === "text") out += n.value;
|
|
17
|
+
for (const c of n.children ?? []) walk(c);
|
|
18
|
+
};
|
|
19
|
+
walk(node);
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
function rehypeDeckCode(options) {
|
|
23
|
+
const { highlighter, themes } = options;
|
|
24
|
+
return (tree) => {
|
|
25
|
+
visit(tree, "element", (node, index, parent) => {
|
|
26
|
+
if (node.tagName !== "pre" || !parent || index === void 0) return;
|
|
27
|
+
const code = node.children.find((c) => c.type === "element" && c.tagName === "code");
|
|
28
|
+
if (!code) return;
|
|
29
|
+
const lang = langOf(code);
|
|
30
|
+
if (lang === "math") return;
|
|
31
|
+
const meta = parseCodeMeta(code.data?.meta);
|
|
32
|
+
const source = textOf(code).replace(/\n$/, "");
|
|
33
|
+
const useLang = highlighter.getLoadedLanguages().includes(lang) ? lang : "text";
|
|
34
|
+
const pre = highlighter.codeToHast(source, {
|
|
35
|
+
lang: useLang,
|
|
36
|
+
themes,
|
|
37
|
+
defaultColor: false,
|
|
38
|
+
transformers: [{ line(el, line) {
|
|
39
|
+
el.properties["data-line"] = String(line + meta.startLine - 1);
|
|
40
|
+
} }]
|
|
41
|
+
}).children.find((c) => c.type === "element" && c.tagName === "pre");
|
|
42
|
+
if (!pre) return;
|
|
43
|
+
pre.properties = {
|
|
44
|
+
...pre.properties,
|
|
45
|
+
...dataAttributes(meta, lang, source.split("\n").length)
|
|
46
|
+
};
|
|
47
|
+
pre.properties.className = ["deck-code", "shiki"];
|
|
48
|
+
parent.children[index] = pre;
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function dataAttributes(meta, lang, totalLines) {
|
|
53
|
+
const out = {
|
|
54
|
+
"data-lang": lang,
|
|
55
|
+
"data-total": String(totalLines)
|
|
56
|
+
};
|
|
57
|
+
if (meta.steps) out["data-steps"] = serialiseSteps(meta.steps);
|
|
58
|
+
if (meta.lines) out["data-lines"] = "true";
|
|
59
|
+
if (meta.startLine !== 1) out["data-start"] = String(meta.startLine);
|
|
60
|
+
if (meta.title) out["data-title"] = meta.title;
|
|
61
|
+
if (meta.maxHeight) out["data-max-height"] = meta.maxHeight;
|
|
62
|
+
if (meta.hideUntilStep) out["data-hide-until-step"] = "true";
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { rehypeDeckCode };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { TermMatcher } from "../glossary/model.js";
|
|
2
|
+
import { Root } from "hast";
|
|
3
|
+
//#region src/compiler/rehype-glossary.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Links a slide's key terms to the deck's glossary: each mention the matcher finds is wrapped in
|
|
6
|
+
* `<Term id auto>`, which shows the definition in a card after a short hover. The same idea as
|
|
7
|
+
* the pass that links Paadam's notes (apps/paadam/mdx/rehype-glossary.ts), retuned for slides:
|
|
8
|
+
*
|
|
9
|
+
* · A slide is the unit. With `repeat: "first"` a term is linked once per slide — every slide
|
|
10
|
+
* stands alone in front of an audience, nobody can scroll back to where it was first defined.
|
|
11
|
+
* · Body text goes first and headings only pick up what the body never mentions (`headings:
|
|
12
|
+
* "fallback"`): a slide whose title is its only mention of UTM still gets the link, but a
|
|
13
|
+
* title is not underlined when the sentence below it can carry the term instead.
|
|
14
|
+
* · Code, maths, links, controls and the components that animate or paint their own text
|
|
15
|
+
* (GradientText, Typewriter, Counter…) are left alone; `className="no-terms"` opts a block out.
|
|
16
|
+
*
|
|
17
|
+
* Explicit `<Term>` elements are settled here as well, so the author hears about a typo at build
|
|
18
|
+
* time: `<Term>UTM</Term>` gets its `id`, `<Term def="…">` gets its definition rendered (with
|
|
19
|
+
* KaTeX, which the browser never has to load), and both count as the slide's mention of the term.
|
|
20
|
+
*/
|
|
21
|
+
export interface RehypeGlossaryOptions {
|
|
22
|
+
matcher: TermMatcher;
|
|
23
|
+
/** False settles the slide's explicit `<Term>`s and links nothing else (`terms: false`). */
|
|
24
|
+
auto?: boolean;
|
|
25
|
+
repeat: "first" | "all";
|
|
26
|
+
headings: "fallback" | boolean;
|
|
27
|
+
/** Extra component names to leave alone, from the headmatter. */
|
|
28
|
+
skip?: string[];
|
|
29
|
+
/** Term ids this slide does not link (`terms: { skip: [...] }` in its frontmatter). */
|
|
30
|
+
exclude?: Set<string>;
|
|
31
|
+
math?: (tex: string) => string;
|
|
32
|
+
warn?: (message: string) => void;
|
|
33
|
+
}
|
|
34
|
+
export declare function rehypeGlossary(options: RehypeGlossaryOptions): (tree: Root) => void;
|
|
35
|
+
//#endregion
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { adhocId, inlineHtml } from "../glossary/model.js";
|
|
2
|
+
//#region src/compiler/rehype-glossary.ts
|
|
3
|
+
const HEADINGS = /* @__PURE__ */ new Set([
|
|
4
|
+
"h1",
|
|
5
|
+
"h2",
|
|
6
|
+
"h3",
|
|
7
|
+
"h4",
|
|
8
|
+
"h5",
|
|
9
|
+
"h6"
|
|
10
|
+
]);
|
|
11
|
+
const SKIP_TAGS = /* @__PURE__ */ new Set([
|
|
12
|
+
"code",
|
|
13
|
+
"pre",
|
|
14
|
+
"a",
|
|
15
|
+
"script",
|
|
16
|
+
"style",
|
|
17
|
+
"svg",
|
|
18
|
+
"math",
|
|
19
|
+
"kbd",
|
|
20
|
+
"abbr",
|
|
21
|
+
"button",
|
|
22
|
+
"select",
|
|
23
|
+
"option",
|
|
24
|
+
"textarea",
|
|
25
|
+
"iframe",
|
|
26
|
+
"video",
|
|
27
|
+
"audio"
|
|
28
|
+
]);
|
|
29
|
+
const SKIP_CLASSES = /* @__PURE__ */ new Set([
|
|
30
|
+
"no-terms",
|
|
31
|
+
"katex",
|
|
32
|
+
"math",
|
|
33
|
+
"math-inline",
|
|
34
|
+
"math-display"
|
|
35
|
+
]);
|
|
36
|
+
const SKIP_JSX = /* @__PURE__ */ new Set([
|
|
37
|
+
"Code",
|
|
38
|
+
"MagicMove",
|
|
39
|
+
"Tex",
|
|
40
|
+
"Math",
|
|
41
|
+
"Mermaid",
|
|
42
|
+
"Sketch",
|
|
43
|
+
"Notes",
|
|
44
|
+
"Link",
|
|
45
|
+
"Anchor",
|
|
46
|
+
"Toc",
|
|
47
|
+
"Image",
|
|
48
|
+
"Video",
|
|
49
|
+
"Audio",
|
|
50
|
+
"Iframe",
|
|
51
|
+
"Youtube",
|
|
52
|
+
"Tweet",
|
|
53
|
+
"Kbd",
|
|
54
|
+
"Counter",
|
|
55
|
+
"Typewriter",
|
|
56
|
+
"Encrypted",
|
|
57
|
+
"Timer",
|
|
58
|
+
"FitText",
|
|
59
|
+
"Qr",
|
|
60
|
+
"QrJoin",
|
|
61
|
+
"Background",
|
|
62
|
+
"GradientText",
|
|
63
|
+
"Shimmer",
|
|
64
|
+
"Icon",
|
|
65
|
+
"Poll",
|
|
66
|
+
"Quiz",
|
|
67
|
+
"Questions",
|
|
68
|
+
"Leaderboard",
|
|
69
|
+
"WordCloud",
|
|
70
|
+
"Presence",
|
|
71
|
+
"Reactions",
|
|
72
|
+
"Pace"
|
|
73
|
+
]);
|
|
74
|
+
const isJsx = (n) => n.type === "mdxJsxTextElement" || n.type === "mdxJsxFlowElement";
|
|
75
|
+
function attr(el, name) {
|
|
76
|
+
return el.attributes.find((a) => a.type === "mdxJsxAttribute" && a.name === name);
|
|
77
|
+
}
|
|
78
|
+
const stringAttr = (el, name) => {
|
|
79
|
+
const v = attr(el, name)?.value;
|
|
80
|
+
return typeof v === "string" ? v : null;
|
|
81
|
+
};
|
|
82
|
+
function setAttr(el, name, value) {
|
|
83
|
+
const existing = attr(el, name);
|
|
84
|
+
if (existing) existing.value = value;
|
|
85
|
+
else el.attributes.push({
|
|
86
|
+
type: "mdxJsxAttribute",
|
|
87
|
+
name,
|
|
88
|
+
value
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function classesOf(node) {
|
|
92
|
+
if (node.type === "element") {
|
|
93
|
+
const cls = node.properties?.className;
|
|
94
|
+
return Array.isArray(cls) ? cls.map(String) : typeof cls === "string" ? cls.split(/\s+/) : [];
|
|
95
|
+
}
|
|
96
|
+
if (isJsx(node)) return (stringAttr(node, "className") ?? stringAttr(node, "class") ?? "").split(/\s+/);
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
function textOf(node) {
|
|
100
|
+
if (node.type === "text") return node.value;
|
|
101
|
+
return (node.children ?? []).map(textOf).join("");
|
|
102
|
+
}
|
|
103
|
+
function rehypeGlossary(options) {
|
|
104
|
+
const { matcher, repeat, headings, exclude, math, warn } = options;
|
|
105
|
+
const skipJsx = /* @__PURE__ */ new Set([...SKIP_JSX, ...options.skip ?? []]);
|
|
106
|
+
return (tree) => {
|
|
107
|
+
const seen = new Set(exclude ?? []);
|
|
108
|
+
const jobs = [];
|
|
109
|
+
const settle = (el) => {
|
|
110
|
+
const def = stringAttr(el, "def");
|
|
111
|
+
if (def !== null) {
|
|
112
|
+
setAttr(el, "html", inlineHtml(def.replace(/\s+/g, " ").trim(), math));
|
|
113
|
+
const more = stringAttr(el, "more");
|
|
114
|
+
if (more !== null) setAttr(el, "moreHtml", inlineHtml(more.replace(/\s+/g, " ").trim(), math));
|
|
115
|
+
if (stringAttr(el, "id") === null) setAttr(el, "id", adhocId(`${textOf(el)}|${def}`));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (attr(el, "def")) return;
|
|
119
|
+
const wanted = stringAttr(el, "id") ?? textOf(el);
|
|
120
|
+
const entry = matcher.resolve(wanted);
|
|
121
|
+
if (!entry) {
|
|
122
|
+
warn?.(`<Term> "${wanted.trim()}" is not in the glossary and has no def`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
setAttr(el, "id", entry.id);
|
|
126
|
+
seen.add(entry.id);
|
|
127
|
+
};
|
|
128
|
+
const collect = (node, heading) => {
|
|
129
|
+
let inHeading = heading;
|
|
130
|
+
if (node.type === "element") {
|
|
131
|
+
const tag = node.tagName;
|
|
132
|
+
if (SKIP_TAGS.has(tag)) return;
|
|
133
|
+
if (HEADINGS.has(tag)) inHeading = true;
|
|
134
|
+
} else if (isJsx(node)) {
|
|
135
|
+
if (node.name === "Term") return settle(node);
|
|
136
|
+
if (node.name && (skipJsx.has(node.name) || SKIP_TAGS.has(node.name))) return;
|
|
137
|
+
if (node.name && HEADINGS.has(node.name)) inHeading = true;
|
|
138
|
+
} else if (node.type !== "root") return;
|
|
139
|
+
if (classesOf(node).some((c) => SKIP_CLASSES.has(c))) return;
|
|
140
|
+
for (const child of node.children ?? []) if (child.type === "text") {
|
|
141
|
+
if (child.value.trim()) jobs.push({
|
|
142
|
+
node: child,
|
|
143
|
+
heading: inHeading
|
|
144
|
+
});
|
|
145
|
+
} else collect(child, inHeading);
|
|
146
|
+
};
|
|
147
|
+
collect(tree, false);
|
|
148
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
149
|
+
const link = (job) => {
|
|
150
|
+
const src = job.node.value;
|
|
151
|
+
const out = [];
|
|
152
|
+
let last = 0;
|
|
153
|
+
for (const m of matcher.find(src)) {
|
|
154
|
+
if (exclude?.has(m.entry.id)) continue;
|
|
155
|
+
if (repeat === "first" && seen.has(m.entry.id)) continue;
|
|
156
|
+
seen.add(m.entry.id);
|
|
157
|
+
if (m.index > last) out.push({
|
|
158
|
+
type: "text",
|
|
159
|
+
value: src.slice(last, m.index)
|
|
160
|
+
});
|
|
161
|
+
out.push({
|
|
162
|
+
type: "mdxJsxTextElement",
|
|
163
|
+
name: "Term",
|
|
164
|
+
attributes: [{
|
|
165
|
+
type: "mdxJsxAttribute",
|
|
166
|
+
name: "id",
|
|
167
|
+
value: m.entry.id
|
|
168
|
+
}, {
|
|
169
|
+
type: "mdxJsxAttribute",
|
|
170
|
+
name: "auto",
|
|
171
|
+
value: null
|
|
172
|
+
}],
|
|
173
|
+
children: [{
|
|
174
|
+
type: "text",
|
|
175
|
+
value: m.text
|
|
176
|
+
}]
|
|
177
|
+
});
|
|
178
|
+
last = m.index + m.text.length;
|
|
179
|
+
}
|
|
180
|
+
if (!out.length) return;
|
|
181
|
+
if (last < src.length) out.push({
|
|
182
|
+
type: "text",
|
|
183
|
+
value: src.slice(last)
|
|
184
|
+
});
|
|
185
|
+
replacements.set(job.node, out);
|
|
186
|
+
};
|
|
187
|
+
if (options.auto === false) return;
|
|
188
|
+
if (headings === "fallback") {
|
|
189
|
+
jobs.filter((j) => !j.heading).forEach(link);
|
|
190
|
+
jobs.filter((j) => j.heading).forEach(link);
|
|
191
|
+
} else jobs.filter((j) => headings || !j.heading).forEach(link);
|
|
192
|
+
if (!replacements.size) return;
|
|
193
|
+
const rebuild = (node) => {
|
|
194
|
+
const kids = node.children;
|
|
195
|
+
if (!kids) return;
|
|
196
|
+
const next = [];
|
|
197
|
+
for (const child of kids) {
|
|
198
|
+
const swap = child.type === "text" ? replacements.get(child) : void 0;
|
|
199
|
+
if (swap) next.push(...swap);
|
|
200
|
+
else {
|
|
201
|
+
rebuild(child);
|
|
202
|
+
next.push(child);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
node.children = next;
|
|
206
|
+
};
|
|
207
|
+
rebuild(tree);
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
export { rehypeGlossary };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/compiler/slots.ts
|
|
2
|
+
/**
|
|
3
|
+
* Slidev's named slots for layouts: a line `::right::` (or any `::name::`) splits the slide body
|
|
4
|
+
* into slots. Each slot becomes a `<div data-slot="name">` so a layout's CSS can place it — the
|
|
5
|
+
* two-cols layout puts `default` in the left column and `right` in the right one. A body with no
|
|
6
|
+
* markers is left untouched, so plain slides pay nothing for this.
|
|
7
|
+
*/
|
|
8
|
+
const MARKER = /^::([a-z][\w-]*)::[ \t]*$/;
|
|
9
|
+
function applySlots(body) {
|
|
10
|
+
const lines = body.split("\n");
|
|
11
|
+
if (!lines.some((l) => MARKER.test(l))) return body;
|
|
12
|
+
const slots = [{
|
|
13
|
+
name: "default",
|
|
14
|
+
lines: []
|
|
15
|
+
}];
|
|
16
|
+
let fence = null;
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
const f = line.match(/^\s*(`{3,}|~{3,})/);
|
|
19
|
+
if (f) {
|
|
20
|
+
if (!fence) fence = f[1];
|
|
21
|
+
else if (line.trim().startsWith(fence[0]) && line.trim().length >= fence.length) fence = null;
|
|
22
|
+
}
|
|
23
|
+
const m = !fence && line.match(MARKER);
|
|
24
|
+
if (m) {
|
|
25
|
+
slots.push({
|
|
26
|
+
name: m[1],
|
|
27
|
+
lines: []
|
|
28
|
+
});
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
slots[slots.length - 1].lines.push(line);
|
|
32
|
+
}
|
|
33
|
+
return slots.filter((s) => s.lines.join("\n").trim()).map((s) => `<div data-slot="${s.name}" className="deck-slot deck-slot-${s.name}">\n\n${s.lines.join("\n").trim()}\n\n</div>`).join("\n\n");
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { applySlots };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//#region src/compiler/split.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
* THE DECK FILE
|
|
5
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* A deck is one MDX file, `deck.mdx`, read the way Slidev reads its Markdown and reveal.js reads
|
|
8
|
+
* its external Markdown — as text first, slides second:
|
|
9
|
+
*
|
|
10
|
+
* --- a line of three dashes starts the next slide (horizontal)
|
|
11
|
+
* ---- a line of four or more dashes starts a slide BENEATH the previous one
|
|
12
|
+
* (a vertical stack, the reveal.js idea: a deep dive the audience may skip)
|
|
13
|
+
* --- the block right after a separator is that slide's FRONTMATTER when it is YAML
|
|
14
|
+
* key: value and closes with another line of three dashes; a fenced block at the very top
|
|
15
|
+
* --- of the file is the HEADMATTER, which configures the whole deck
|
|
16
|
+
* <Notes>…</Notes> anywhere in a slide: speaker notes, compiled as MDX of their own
|
|
17
|
+
* src: ./file.mdx a frontmatter-only slide that pulls the slides of another file in
|
|
18
|
+
*
|
|
19
|
+
* Everything in between is MDX: Markdown, JSX, the engine's components. This module only cuts
|
|
20
|
+
* the text; the Vite plugin turns the pieces into modules.
|
|
21
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
*/
|
|
23
|
+
export type Frontmatter = Record<string, unknown>;
|
|
24
|
+
export interface RawSlide {
|
|
25
|
+
/** Flat index in authored order, 0-based. */
|
|
26
|
+
index: number;
|
|
27
|
+
/** Horizontal column and vertical position in it. */
|
|
28
|
+
h: number;
|
|
29
|
+
v: number;
|
|
30
|
+
frontmatter: Frontmatter;
|
|
31
|
+
/** The slide's MDX with notes and frontmatter removed. */
|
|
32
|
+
content: string;
|
|
33
|
+
/** The notes' MDX, or null when the slide has none. */
|
|
34
|
+
notes: string | null;
|
|
35
|
+
/** Where the slide starts, for error messages and the editor. */
|
|
36
|
+
source: {
|
|
37
|
+
file: string;
|
|
38
|
+
line: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export interface SplitResult {
|
|
42
|
+
headmatter: Frontmatter;
|
|
43
|
+
slides: RawSlide[];
|
|
44
|
+
}
|
|
45
|
+
export interface SplitOptions {
|
|
46
|
+
file: string;
|
|
47
|
+
/** Returns the text of an included file (the `src:` frontmatter), or null when it is missing. */
|
|
48
|
+
resolveInclude?: (path: string, from: string) => {
|
|
49
|
+
file: string;
|
|
50
|
+
text: string;
|
|
51
|
+
} | null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Split a deck file into slides. Includes (`src:`) are expanded in place; a piece that is only
|
|
55
|
+
* frontmatter with a `src` contributes the included file's slides, with the included file's own
|
|
56
|
+
* headmatter and the piece's remaining frontmatter merged into the first of them.
|
|
57
|
+
*/
|
|
58
|
+
export declare function splitDeck(text: string, options: SplitOptions): SplitResult;
|
|
59
|
+
/** The title a slide reports to the table of contents: frontmatter `title`, else its first heading. */
|
|
60
|
+
export declare function titleOf(slide: Pick<RawSlide, "content" | "frontmatter">): string | null;
|
|
61
|
+
/** The level of the title, for the table of contents: frontmatter `level`, else the heading depth. */
|
|
62
|
+
export declare function levelOf(slide: Pick<RawSlide, "content" | "frontmatter">): number;
|
|
63
|
+
//#endregion
|