@honeydeck/honeydeck 0.3.0 → 0.5.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/DEVELOPMENT.md +4 -1
- package/Readme.md +2 -2
- package/SPEC.md +3 -3
- package/docs/components-browser-frame.md +34 -0
- package/docs/components-keyboard.md +31 -0
- package/docs/components-list-style.md +49 -0
- package/docs/components-notes.md +36 -0
- package/docs/components-reveal-group.md +58 -0
- package/docs/components-reveal-with.md +37 -0
- package/docs/components-reveal.md +33 -0
- package/docs/components-timeline-steps.md +48 -0
- package/docs/components.md +13 -54
- package/docs/configuration.md +11 -0
- package/docs/deeper-dive.md +30 -7
- package/docs/getting-started.md +2 -2
- package/docs/navigation.md +1 -1
- package/docs/pdf-export.md +4 -2
- package/docs/presenter-mode.md +6 -3
- package/docs/skills.md +3 -3
- package/docs/slidev-migration.md +3 -0
- package/docs/steps-and-reveals.md +143 -8
- package/package.json +4 -1
- package/skills/SPEC.md +2 -2
- package/skills/honeydeck/SKILL.md +2 -2
- package/skills/slidev-migration/SKILL.md +1 -0
- package/src/SPEC.md +8 -3
- package/src/cli/SPEC.md +3 -2
- package/src/cli/pdf.ts +11 -4
- package/src/layouts/SPEC.md +1 -1
- package/src/remark/SPEC.md +102 -2
- package/src/remark/code-utils.ts +151 -0
- package/src/remark/shiki-code-blocks.ts +329 -136
- package/src/remark/step-numbering.ts +408 -103
- package/src/runtime/Deck.tsx +133 -116
- package/src/runtime/EffectiveColorModeContext.tsx +37 -0
- package/src/runtime/SPEC.md +21 -8
- package/src/runtime/SlideCanvas.tsx +19 -16
- package/src/runtime/SlideScaleContext.tsx +23 -0
- package/src/runtime/components/CodeBlock.tsx +19 -202
- package/src/runtime/components/CodeBlockCopyButton.tsx +64 -0
- package/src/runtime/components/CodeBlockShared.ts +17 -0
- package/src/runtime/components/Fade.tsx +51 -0
- package/src/runtime/components/FadeGroup.tsx +175 -0
- package/src/runtime/components/FadeWith.tsx +54 -0
- package/src/runtime/components/MagicCodeBlock.tsx +223 -0
- package/src/runtime/components/NavBar.tsx +1 -1
- package/src/runtime/components/NormalCodeBlock.tsx +128 -0
- package/src/runtime/components/Reveal.tsx +27 -27
- package/src/runtime/components/RevealGroup.tsx +143 -41
- package/src/runtime/components/RevealWith.tsx +63 -0
- package/src/runtime/components/SPEC.md +112 -7
- package/src/runtime/components/TimelineReveal.tsx +81 -0
- package/src/runtime/components/index.ts +13 -5
- package/src/runtime/components/timelineVisibility.ts +45 -0
- package/src/runtime/index.ts +9 -1
- package/src/runtime/navigation.ts +6 -4
- package/src/runtime/presentationApi.ts +449 -0
- package/src/runtime/views/PresenterCastButton.tsx +39 -0
- package/src/runtime/views/PresenterView.tsx +21 -4
- package/src/runtime/views/SPEC.md +7 -5
- package/src/theme/base.css +67 -2
- package/src/vite-plugin/SPEC.md +20 -2
- package/src/vite-plugin/index.ts +16 -2
- package/src/vite-plugin/layout-demo-crawler.ts +304 -33
- package/src/vite-plugin/splitter.ts +1 -0
- package/src/vite-plugin/virtual-modules.ts +16 -6
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/** Shared helpers for Honeydeck fenced code and Magic Code remark transforms. */
|
|
2
|
+
|
|
3
|
+
import type { Code } from "mdast";
|
|
4
|
+
|
|
5
|
+
/** A single step-through group: either an array of 1-based line numbers or 'all'. */
|
|
6
|
+
export type StepGroup = number[] | "all";
|
|
7
|
+
|
|
8
|
+
export type ParsedCodeFence = {
|
|
9
|
+
lang: string;
|
|
10
|
+
meta: string | null;
|
|
11
|
+
value: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type MagicCodeDurationResult =
|
|
15
|
+
| { ok: true; duration: number }
|
|
16
|
+
| { ok: false; message: string };
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_MAGIC_CODE_DURATION = 800;
|
|
19
|
+
|
|
20
|
+
/** Parse a code fence meta string like `{2|4-5|all}` into step groups. */
|
|
21
|
+
export function parseStepMeta(meta: string | null | undefined): StepGroup[] {
|
|
22
|
+
if (!meta) return [];
|
|
23
|
+
|
|
24
|
+
const match = meta.match(/\{([^}]+)\}/);
|
|
25
|
+
if (!match?.[1]) return [];
|
|
26
|
+
|
|
27
|
+
const groupStrings = match[1].split("|").filter(Boolean);
|
|
28
|
+
if (groupStrings.length === 0) return [];
|
|
29
|
+
|
|
30
|
+
return groupStrings.map((group): StepGroup => {
|
|
31
|
+
const trimmed = group.trim();
|
|
32
|
+
if (trimmed === "all") return "all";
|
|
33
|
+
|
|
34
|
+
const lines: number[] = [];
|
|
35
|
+
for (const part of trimmed.split(",")) {
|
|
36
|
+
const dashIdx = part.indexOf("-");
|
|
37
|
+
if (dashIdx !== -1) {
|
|
38
|
+
const start = parseInt(part.slice(0, dashIdx).trim(), 10);
|
|
39
|
+
const end = parseInt(part.slice(dashIdx + 1).trim(), 10);
|
|
40
|
+
if (!Number.isNaN(start) && !Number.isNaN(end)) {
|
|
41
|
+
for (let i = start; i <= end; i++) lines.push(i);
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
const n = parseInt(part.trim(), 10);
|
|
45
|
+
if (!Number.isNaN(n)) lines.push(n);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return lines;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function codeFenceGroups(meta: string | null | undefined): StepGroup[] {
|
|
53
|
+
const steps = parseStepMeta(meta);
|
|
54
|
+
return steps.length > 0 ? steps : ["all"];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function countCodeFenceGroups(meta: string | null | undefined): number {
|
|
58
|
+
if (!meta) return 0;
|
|
59
|
+
const match = meta.match(/\{([^}]+)\}/);
|
|
60
|
+
if (!match?.[1]) return 0;
|
|
61
|
+
return match[1].split("|").filter(Boolean).length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function countCodeFenceSteps(meta: string | null | undefined): number {
|
|
65
|
+
return Math.max(0, countCodeFenceGroups(meta) - 1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function isMagicCodeFence(node: Code): boolean {
|
|
69
|
+
if (node.lang !== "md") return false;
|
|
70
|
+
const firstMetaToken = node.meta?.trim().split(/\s+/)[0];
|
|
71
|
+
return firstMetaToken === "magic-code" || firstMetaToken === "magic-move";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function parseMagicCodeDuration(
|
|
75
|
+
meta: string | null | undefined,
|
|
76
|
+
deckDefault: unknown,
|
|
77
|
+
): MagicCodeDurationResult {
|
|
78
|
+
const fallback =
|
|
79
|
+
typeof deckDefault === "number" &&
|
|
80
|
+
Number.isFinite(deckDefault) &&
|
|
81
|
+
deckDefault >= 0
|
|
82
|
+
? deckDefault
|
|
83
|
+
: DEFAULT_MAGIC_CODE_DURATION;
|
|
84
|
+
|
|
85
|
+
const match = meta?.match(/\{\s*duration\s*:\s*([^}]+)\}/);
|
|
86
|
+
if (!match?.[1]) return { ok: true, duration: fallback };
|
|
87
|
+
|
|
88
|
+
const raw = match[1].trim();
|
|
89
|
+
const duration = Number(raw);
|
|
90
|
+
if (!Number.isFinite(duration) || duration < 0) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
message: `Honeydeck Magic Code duration must be a non-negative number of milliseconds, got ${JSON.stringify(raw)}.`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { ok: true, duration };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function splitFenceInfo(info: string): { lang: string; meta: string | null } {
|
|
101
|
+
const trimmed = info.trim();
|
|
102
|
+
if (!trimmed) return { lang: "text", meta: null };
|
|
103
|
+
|
|
104
|
+
const firstSpace = trimmed.search(/\s/);
|
|
105
|
+
if (firstSpace === -1) return { lang: trimmed, meta: null };
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
lang: trimmed.slice(0, firstSpace),
|
|
109
|
+
meta: trimmed.slice(firstSpace).trim() || null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Parse fenced code blocks inside an outer Markdown code fence. */
|
|
114
|
+
export function parseInnerCodeFences(markdown: string): ParsedCodeFence[] {
|
|
115
|
+
const lines = markdown.split(/\r?\n/);
|
|
116
|
+
const fences: ParsedCodeFence[] = [];
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < lines.length; i++) {
|
|
119
|
+
const opener = lines[i]?.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
120
|
+
if (!opener?.[1]) continue;
|
|
121
|
+
|
|
122
|
+
const marker = opener[1][0];
|
|
123
|
+
const length = opener[1].length;
|
|
124
|
+
const { lang, meta } = splitFenceInfo(opener[2] ?? "");
|
|
125
|
+
const body: string[] = [];
|
|
126
|
+
i++;
|
|
127
|
+
|
|
128
|
+
for (; i < lines.length; i++) {
|
|
129
|
+
const closer = lines[i]?.match(/^ {0,3}(`{3,}|~{3,})\s*$/);
|
|
130
|
+
if (
|
|
131
|
+
closer?.[1] &&
|
|
132
|
+
closer[1][0] === marker &&
|
|
133
|
+
closer[1].length >= length
|
|
134
|
+
) {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
body.push(lines[i] ?? "");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fences.push({ lang, meta, value: body.join("\n") });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return fences;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function countMagicCodeStates(markdown: string): number {
|
|
147
|
+
return parseInnerCodeFences(markdown).reduce(
|
|
148
|
+
(total, fence) => total + codeFenceGroups(fence.meta).length,
|
|
149
|
+
0,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
* (JSON-encoded step groups), and `startAt` (1-based timeline step where
|
|
19
19
|
* the second group activates, from `node.data.honeydeckStartAt`), plus
|
|
20
20
|
* `source` (the original fenced code text for clipboard copying).
|
|
21
|
-
* 3. Injects
|
|
22
|
-
*
|
|
21
|
+
* 3. Injects `HoneydeckCodeBlock` from '@honeydeck/honeydeck/components/code-block/normal'
|
|
22
|
+
* when at least one normal code block was transformed, and `HoneydeckMagicCodeBlock`
|
|
23
|
+
* from '@honeydeck/honeydeck/components/code-block/magic' when needed.
|
|
23
24
|
*
|
|
24
25
|
* ### CSS variable activation
|
|
25
26
|
* The highlighted HTML uses shiki's dual-theme variable names
|
|
@@ -31,10 +32,13 @@
|
|
|
31
32
|
* - `{2}` — highlight line 2 immediately; consumes no timeline steps
|
|
32
33
|
* - `{2|4-5|all}` — three step groups; first group is baseline, then later groups activate at `startAt`, `startAt+1`
|
|
33
34
|
*
|
|
34
|
-
* @see HoneydeckCodeBlock in `src/runtime/components/
|
|
35
|
+
* @see HoneydeckCodeBlock in `src/runtime/components/NormalCodeBlock.tsx`
|
|
36
|
+
* @see HoneydeckMagicCodeBlock in `src/runtime/components/MagicCodeBlock.tsx`
|
|
35
37
|
* @see remarkStepNumbering for the counter that provides `honeydeckStartAt`
|
|
36
38
|
*/
|
|
37
39
|
|
|
40
|
+
import type { KeyedTokensInfo } from "@shikijs/magic-move/core";
|
|
41
|
+
import { codeToKeyedTokens } from "@shikijs/magic-move/core";
|
|
38
42
|
import type { Program } from "estree";
|
|
39
43
|
import type { Code, Root } from "mdast";
|
|
40
44
|
import type {
|
|
@@ -46,13 +50,40 @@ import type { MdxjsEsm } from "mdast-util-mdxjs-esm";
|
|
|
46
50
|
import { getSingletonHighlighter } from "shiki";
|
|
47
51
|
import type { Plugin } from "unified";
|
|
48
52
|
import { visit } from "unist-util-visit";
|
|
53
|
+
import {
|
|
54
|
+
codeFenceGroups,
|
|
55
|
+
isMagicCodeFence,
|
|
56
|
+
type ParsedCodeFence,
|
|
57
|
+
parseInnerCodeFences,
|
|
58
|
+
parseMagicCodeDuration,
|
|
59
|
+
parseStepMeta,
|
|
60
|
+
type StepGroup,
|
|
61
|
+
} from "./code-utils.ts";
|
|
62
|
+
|
|
63
|
+
export type { StepGroup } from "./code-utils.ts";
|
|
64
|
+
export { parseStepMeta } from "./code-utils.ts";
|
|
49
65
|
|
|
50
66
|
// ---------------------------------------------------------------------------
|
|
51
67
|
// Types
|
|
52
68
|
// ---------------------------------------------------------------------------
|
|
53
69
|
|
|
54
|
-
|
|
55
|
-
|
|
70
|
+
export type RemarkShikiCodeBlocksOptions = {
|
|
71
|
+
/** Deck-level Magic Code default duration from root frontmatter. */
|
|
72
|
+
magicCodeDuration?: unknown;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type MagicTimelineState = {
|
|
76
|
+
fence: ParsedCodeFence;
|
|
77
|
+
group: StepGroup;
|
|
78
|
+
tokenStateIndex: number;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type MagicTokenPayload = {
|
|
82
|
+
lightTokenStates: KeyedTokensInfo[];
|
|
83
|
+
darkTokenStates: KeyedTokensInfo[];
|
|
84
|
+
tokenStateIndexes: number[];
|
|
85
|
+
sources: string[];
|
|
86
|
+
};
|
|
56
87
|
|
|
57
88
|
// ---------------------------------------------------------------------------
|
|
58
89
|
// Singleton shiki highlighter
|
|
@@ -88,55 +119,6 @@ async function loadLang(
|
|
|
88
119
|
}
|
|
89
120
|
}
|
|
90
121
|
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
|
-
// Step meta parsing
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Parse a code fence meta string like `{2|4-5|all}` into an array of step groups.
|
|
97
|
-
*
|
|
98
|
-
* Returns `[]` when there is no `{…}` block in the meta string.
|
|
99
|
-
* Even a single group like `{2}` produces `[[2]]` — a baseline highlight that
|
|
100
|
-
* consumes no timeline steps.
|
|
101
|
-
*
|
|
102
|
-
* @example
|
|
103
|
-
* parseStepMeta('{2|4-5|all}') // → [[2], [4,5], 'all']
|
|
104
|
-
* parseStepMeta('{2}') // → [[2]] (baseline highlight, no timeline step)
|
|
105
|
-
* parseStepMeta(null) // → []
|
|
106
|
-
*/
|
|
107
|
-
export function parseStepMeta(meta: string | null | undefined): StepGroup[] {
|
|
108
|
-
if (!meta) return [];
|
|
109
|
-
|
|
110
|
-
const match = meta.match(/\{([^}]+)\}/);
|
|
111
|
-
if (!match?.[1]) return [];
|
|
112
|
-
|
|
113
|
-
const groupStrings = match[1].split("|").filter(Boolean);
|
|
114
|
-
if (groupStrings.length === 0) return [];
|
|
115
|
-
// Note: single groups are valid — they represent a baseline highlight and
|
|
116
|
-
// consume no timeline steps.
|
|
117
|
-
|
|
118
|
-
return groupStrings.map((group): StepGroup => {
|
|
119
|
-
const trimmed = group.trim();
|
|
120
|
-
if (trimmed === "all") return "all";
|
|
121
|
-
|
|
122
|
-
const lines: number[] = [];
|
|
123
|
-
for (const part of trimmed.split(",")) {
|
|
124
|
-
const dashIdx = part.indexOf("-");
|
|
125
|
-
if (dashIdx !== -1) {
|
|
126
|
-
const start = parseInt(part.slice(0, dashIdx).trim(), 10);
|
|
127
|
-
const end = parseInt(part.slice(dashIdx + 1).trim(), 10);
|
|
128
|
-
if (!Number.isNaN(start) && !Number.isNaN(end)) {
|
|
129
|
-
for (let i = start; i <= end; i++) lines.push(i);
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
const n = parseInt(part.trim(), 10);
|
|
133
|
-
if (!Number.isNaN(n)) lines.push(n);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return lines;
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
122
|
// ---------------------------------------------------------------------------
|
|
141
123
|
// AST helpers
|
|
142
124
|
// ---------------------------------------------------------------------------
|
|
@@ -226,12 +208,196 @@ function injectImport(tree: Root, specifier: string, source: string): void {
|
|
|
226
208
|
);
|
|
227
209
|
}
|
|
228
210
|
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Highlight helpers
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
async function codeToHighlightedHtml(
|
|
216
|
+
highlighter: Awaited<ReturnType<typeof getSingletonHighlighter>>,
|
|
217
|
+
value: string,
|
|
218
|
+
rawLang: string,
|
|
219
|
+
): Promise<string> {
|
|
220
|
+
const lang = await loadLang(highlighter, rawLang);
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
return highlighter.codeToHtml(value, {
|
|
224
|
+
lang,
|
|
225
|
+
themes: { light: "github-light", dark: "github-dark" },
|
|
226
|
+
defaultColor: false,
|
|
227
|
+
transformers: [
|
|
228
|
+
{
|
|
229
|
+
line(lineNode, lineNumber) {
|
|
230
|
+
if (!lineNode.properties) lineNode.properties = {};
|
|
231
|
+
lineNode.properties["data-line"] = lineNumber;
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
} catch {
|
|
237
|
+
const escaped = value
|
|
238
|
+
.replace(/&/g, "&")
|
|
239
|
+
.replace(/</g, "<")
|
|
240
|
+
.replace(/>/g, ">");
|
|
241
|
+
return `<pre class="honeydeck-code-plain"><code>${escaped}</code></pre>`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getMagicTokenStateKey(fence: ParsedCodeFence): string {
|
|
246
|
+
return `${fence.lang}\0${fence.value}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function flattenMagicTimeline(fences: ParsedCodeFence[]): MagicTimelineState[] {
|
|
250
|
+
const tokenStateIndexes = new Map<string, number>();
|
|
251
|
+
return fences.flatMap((fence) => {
|
|
252
|
+
const key = getMagicTokenStateKey(fence);
|
|
253
|
+
let tokenStateIndex = tokenStateIndexes.get(key);
|
|
254
|
+
if (tokenStateIndex === undefined) {
|
|
255
|
+
tokenStateIndex = tokenStateIndexes.size;
|
|
256
|
+
tokenStateIndexes.set(key, tokenStateIndex);
|
|
257
|
+
}
|
|
258
|
+
return codeFenceGroups(fence.meta).map((group) => ({
|
|
259
|
+
fence,
|
|
260
|
+
group,
|
|
261
|
+
tokenStateIndex,
|
|
262
|
+
}));
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function getUniqueMagicTokenStates(
|
|
267
|
+
states: MagicTimelineState[],
|
|
268
|
+
): ParsedCodeFence[] {
|
|
269
|
+
const uniqueStates: ParsedCodeFence[] = [];
|
|
270
|
+
for (const state of states) {
|
|
271
|
+
if (!uniqueStates[state.tokenStateIndex]) {
|
|
272
|
+
uniqueStates[state.tokenStateIndex] = state.fence;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return uniqueStates;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function codeToMagicTokens(
|
|
279
|
+
highlighter: Awaited<ReturnType<typeof getSingletonHighlighter>>,
|
|
280
|
+
state: MagicTimelineState,
|
|
281
|
+
theme: "github-light" | "github-dark",
|
|
282
|
+
): Promise<KeyedTokensInfo> {
|
|
283
|
+
const lang = await loadLang(highlighter, state.fence.lang);
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
return codeToKeyedTokens(
|
|
287
|
+
highlighter,
|
|
288
|
+
state.fence.value,
|
|
289
|
+
{ lang, theme } as Parameters<typeof highlighter.codeToTokens>[1],
|
|
290
|
+
false,
|
|
291
|
+
);
|
|
292
|
+
} catch {
|
|
293
|
+
return codeToKeyedTokens(
|
|
294
|
+
highlighter,
|
|
295
|
+
state.fence.value,
|
|
296
|
+
{ lang: "text", theme } as Parameters<typeof highlighter.codeToTokens>[1],
|
|
297
|
+
false,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function compileMagicTokens(
|
|
303
|
+
highlighter: Awaited<ReturnType<typeof getSingletonHighlighter>>,
|
|
304
|
+
fences: ParsedCodeFence[],
|
|
305
|
+
theme: "github-light" | "github-dark",
|
|
306
|
+
): Promise<KeyedTokensInfo[]> {
|
|
307
|
+
const tokens: KeyedTokensInfo[] = [];
|
|
308
|
+
for (const fence of fences) {
|
|
309
|
+
tokens.push(
|
|
310
|
+
await codeToMagicTokens(
|
|
311
|
+
highlighter,
|
|
312
|
+
{ fence, group: "all", tokenStateIndex: 0 },
|
|
313
|
+
theme,
|
|
314
|
+
),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return tokens;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function compileMagicTokenPayload(
|
|
321
|
+
highlighter: Awaited<ReturnType<typeof getSingletonHighlighter>>,
|
|
322
|
+
states: MagicTimelineState[],
|
|
323
|
+
): Promise<MagicTokenPayload> {
|
|
324
|
+
const tokenFences = getUniqueMagicTokenStates(states);
|
|
325
|
+
return {
|
|
326
|
+
lightTokenStates: await compileMagicTokens(
|
|
327
|
+
highlighter,
|
|
328
|
+
tokenFences,
|
|
329
|
+
"github-light",
|
|
330
|
+
),
|
|
331
|
+
darkTokenStates: await compileMagicTokens(
|
|
332
|
+
highlighter,
|
|
333
|
+
tokenFences,
|
|
334
|
+
"github-dark",
|
|
335
|
+
),
|
|
336
|
+
tokenStateIndexes: states.map((state) => state.tokenStateIndex),
|
|
337
|
+
sources: tokenFences.map((fence) => fence.value),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function makeCodeBlockNode(
|
|
342
|
+
html: string,
|
|
343
|
+
steps: StepGroup[],
|
|
344
|
+
startAt: number,
|
|
345
|
+
source: string,
|
|
346
|
+
): MdxJsxFlowElement {
|
|
347
|
+
return {
|
|
348
|
+
type: "mdxJsxFlowElement",
|
|
349
|
+
name: CODE_BLOCK_COMPONENT_NAME,
|
|
350
|
+
attributes: [
|
|
351
|
+
makeStringAttr("html", html),
|
|
352
|
+
makeStringAttr("stepsJson", JSON.stringify(steps)),
|
|
353
|
+
makeNumericAttr("startAt", startAt),
|
|
354
|
+
makeStringAttr("source", source),
|
|
355
|
+
],
|
|
356
|
+
children: [],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function makeMagicCodeNode(
|
|
361
|
+
payload: MagicTokenPayload,
|
|
362
|
+
stateGroups: StepGroup[],
|
|
363
|
+
startAt: number,
|
|
364
|
+
duration: number,
|
|
365
|
+
): MdxJsxFlowElement {
|
|
366
|
+
return {
|
|
367
|
+
type: "mdxJsxFlowElement",
|
|
368
|
+
name: MAGIC_CODE_COMPONENT_NAME,
|
|
369
|
+
attributes: [
|
|
370
|
+
makeStringAttr(
|
|
371
|
+
"lightTokenStatesJson",
|
|
372
|
+
JSON.stringify(payload.lightTokenStates),
|
|
373
|
+
),
|
|
374
|
+
makeStringAttr(
|
|
375
|
+
"darkTokenStatesJson",
|
|
376
|
+
JSON.stringify(payload.darkTokenStates),
|
|
377
|
+
),
|
|
378
|
+
makeStringAttr(
|
|
379
|
+
"tokenStateIndexesJson",
|
|
380
|
+
JSON.stringify(payload.tokenStateIndexes),
|
|
381
|
+
),
|
|
382
|
+
makeStringAttr("stepGroupsJson", JSON.stringify(stateGroups)),
|
|
383
|
+
makeStringAttr("sourcesJson", JSON.stringify(payload.sources)),
|
|
384
|
+
makeNumericAttr("startAt", startAt),
|
|
385
|
+
makeNumericAttr("duration", duration),
|
|
386
|
+
],
|
|
387
|
+
children: [],
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
229
391
|
// ---------------------------------------------------------------------------
|
|
230
392
|
// Plugin
|
|
231
393
|
// ---------------------------------------------------------------------------
|
|
232
394
|
|
|
233
|
-
const
|
|
234
|
-
const
|
|
395
|
+
const CODE_BLOCK_COMPONENT_NAME = "HoneydeckCodeBlock";
|
|
396
|
+
const MAGIC_CODE_COMPONENT_NAME = "HoneydeckMagicCodeBlock";
|
|
397
|
+
const CODE_BLOCK_IMPORT_SOURCE =
|
|
398
|
+
"@honeydeck/honeydeck/components/code-block/normal";
|
|
399
|
+
const MAGIC_CODE_IMPORT_SOURCE =
|
|
400
|
+
"@honeydeck/honeydeck/components/code-block/magic";
|
|
235
401
|
|
|
236
402
|
/**
|
|
237
403
|
* Async remark plugin that transforms fenced code blocks to `<HoneydeckCodeBlock>`
|
|
@@ -239,87 +405,114 @@ const IMPORT_SOURCE = "@honeydeck/honeydeck/components/code-block";
|
|
|
239
405
|
*
|
|
240
406
|
* Plugin ordering: `[remarkFrontmatter, remarkH1Extract, remarkStepNumbering, remarkShikiCodeBlocks]`
|
|
241
407
|
*/
|
|
242
|
-
export const remarkShikiCodeBlocks: Plugin<
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
408
|
+
export const remarkShikiCodeBlocks: Plugin<
|
|
409
|
+
[RemarkShikiCodeBlocksOptions?],
|
|
410
|
+
Root
|
|
411
|
+
> =
|
|
412
|
+
(options = {}) =>
|
|
413
|
+
async (tree) => {
|
|
414
|
+
// Collect code nodes before mutating the tree.
|
|
415
|
+
type CodeEntry = {
|
|
416
|
+
node: Code;
|
|
417
|
+
parent: { children: unknown[] };
|
|
418
|
+
};
|
|
419
|
+
const codeEntries: CodeEntry[] = [];
|
|
420
|
+
|
|
421
|
+
visit(tree, "code", (node, index, parent) => {
|
|
422
|
+
if (index !== null && index !== undefined && parent) {
|
|
423
|
+
codeEntries.push({
|
|
424
|
+
node: node as unknown as Code,
|
|
425
|
+
parent: parent as unknown as { children: unknown[] },
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
});
|
|
260
429
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const highlighter = await getHighlighter();
|
|
264
|
-
let didTransform = false;
|
|
265
|
-
|
|
266
|
-
for (const { node, index, parent } of codeEntries) {
|
|
267
|
-
const rawLang = node.lang ?? "text";
|
|
268
|
-
const meta = node.meta;
|
|
269
|
-
const steps = parseStepMeta(meta);
|
|
270
|
-
const startAt: number =
|
|
271
|
-
((node.data as Record<string, unknown> | undefined)
|
|
272
|
-
?.honeydeckStartAt as number) ?? 0;
|
|
273
|
-
|
|
274
|
-
// Load language grammar (no-op if already cached)
|
|
275
|
-
const lang = await loadLang(highlighter, rawLang);
|
|
276
|
-
|
|
277
|
-
// Highlight with dual themes (CSS variable approach)
|
|
278
|
-
let html: string;
|
|
279
|
-
try {
|
|
280
|
-
html = highlighter.codeToHtml(node.value, {
|
|
281
|
-
lang,
|
|
282
|
-
themes: { light: "github-light", dark: "github-dark" },
|
|
283
|
-
defaultColor: false,
|
|
284
|
-
transformers: [
|
|
285
|
-
{
|
|
286
|
-
line(lineNode, lineNumber) {
|
|
287
|
-
// Ensure properties bag exists, then stamp data-line
|
|
288
|
-
if (!lineNode.properties) lineNode.properties = {};
|
|
289
|
-
lineNode.properties["data-line"] = lineNumber;
|
|
290
|
-
},
|
|
291
|
-
},
|
|
292
|
-
],
|
|
293
|
-
});
|
|
294
|
-
} catch {
|
|
295
|
-
// Fallback: plain pre/code block without syntax colours
|
|
296
|
-
const escaped = node.value
|
|
297
|
-
.replace(/&/g, "&")
|
|
298
|
-
.replace(/</g, "<")
|
|
299
|
-
.replace(/>/g, ">");
|
|
300
|
-
html = `<pre class="honeydeck-code-plain"><code>${escaped}</code></pre>`;
|
|
301
|
-
}
|
|
430
|
+
if (codeEntries.length === 0) return;
|
|
302
431
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
name: COMPONENT_NAME,
|
|
307
|
-
attributes: [
|
|
308
|
-
makeStringAttr("html", html),
|
|
309
|
-
makeStringAttr("stepsJson", JSON.stringify(steps)),
|
|
310
|
-
makeNumericAttr("startAt", startAt),
|
|
311
|
-
makeStringAttr("source", node.value),
|
|
312
|
-
],
|
|
313
|
-
children: [],
|
|
314
|
-
};
|
|
432
|
+
const highlighter = await getHighlighter();
|
|
433
|
+
let didTransformCodeBlock = false;
|
|
434
|
+
let didTransformMagicCode = false;
|
|
315
435
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
436
|
+
for (const { node, parent } of codeEntries) {
|
|
437
|
+
const currentIndex = parent.children.indexOf(node);
|
|
438
|
+
if (currentIndex === -1) continue;
|
|
320
439
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
440
|
+
const startAt: number =
|
|
441
|
+
((node.data as Record<string, unknown> | undefined)
|
|
442
|
+
?.honeydeckStartAt as number) ?? 0;
|
|
443
|
+
|
|
444
|
+
if (isMagicCodeFence(node)) {
|
|
445
|
+
const fences = parseInnerCodeFences(node.value);
|
|
446
|
+
const duration = parseMagicCodeDuration(
|
|
447
|
+
node.meta,
|
|
448
|
+
options.magicCodeDuration,
|
|
449
|
+
);
|
|
450
|
+
if (!duration.ok) throw new Error(duration.message);
|
|
451
|
+
|
|
452
|
+
if (fences.length === 0) {
|
|
453
|
+
parent.children.splice(currentIndex, 1);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (fences.length === 1) {
|
|
458
|
+
const [fence] = fences;
|
|
459
|
+
if (!fence) continue;
|
|
460
|
+
const html = await codeToHighlightedHtml(
|
|
461
|
+
highlighter,
|
|
462
|
+
fence.value,
|
|
463
|
+
fence.lang,
|
|
464
|
+
);
|
|
465
|
+
parent.children.splice(
|
|
466
|
+
currentIndex,
|
|
467
|
+
1,
|
|
468
|
+
makeCodeBlockNode(
|
|
469
|
+
html,
|
|
470
|
+
parseStepMeta(fence.meta),
|
|
471
|
+
startAt,
|
|
472
|
+
fence.value,
|
|
473
|
+
),
|
|
474
|
+
);
|
|
475
|
+
didTransformCodeBlock = true;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const states = flattenMagicTimeline(fences);
|
|
480
|
+
const tokenPayload = await compileMagicTokenPayload(
|
|
481
|
+
highlighter,
|
|
482
|
+
states,
|
|
483
|
+
);
|
|
484
|
+
parent.children.splice(
|
|
485
|
+
currentIndex,
|
|
486
|
+
1,
|
|
487
|
+
makeMagicCodeNode(
|
|
488
|
+
tokenPayload,
|
|
489
|
+
states.map((state) => state.group),
|
|
490
|
+
startAt,
|
|
491
|
+
duration.duration,
|
|
492
|
+
),
|
|
493
|
+
);
|
|
494
|
+
didTransformMagicCode = true;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const rawLang = node.lang ?? "text";
|
|
499
|
+
const html = await codeToHighlightedHtml(
|
|
500
|
+
highlighter,
|
|
501
|
+
node.value,
|
|
502
|
+
rawLang,
|
|
503
|
+
);
|
|
504
|
+
parent.children.splice(
|
|
505
|
+
currentIndex,
|
|
506
|
+
1,
|
|
507
|
+
makeCodeBlockNode(html, parseStepMeta(node.meta), startAt, node.value),
|
|
508
|
+
);
|
|
509
|
+
didTransformCodeBlock = true;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (didTransformCodeBlock) {
|
|
513
|
+
injectImport(tree, CODE_BLOCK_COMPONENT_NAME, CODE_BLOCK_IMPORT_SOURCE);
|
|
514
|
+
}
|
|
515
|
+
if (didTransformMagicCode) {
|
|
516
|
+
injectImport(tree, MAGIC_CODE_COMPONENT_NAME, MAGIC_CODE_IMPORT_SOURCE);
|
|
517
|
+
}
|
|
518
|
+
};
|