@honeydeck/honeydeck 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/AGENTS.md +25 -0
- package/DEVELOPMENT.md +522 -0
- package/LICENSE +21 -0
- package/Readme.md +49 -0
- package/SPEC.md +88 -0
- package/docs/components.md +63 -0
- package/docs/configuration.md +91 -0
- package/docs/getting-started.md +116 -0
- package/docs/kit-authoring.md +207 -0
- package/docs/kits.md +387 -0
- package/docs/local-development.md +95 -0
- package/docs/mermaid.md +198 -0
- package/docs/mobile.md +108 -0
- package/docs/navigation.md +93 -0
- package/docs/next-steps.md +377 -0
- package/docs/pdf-export.md +91 -0
- package/docs/presenter-mode.md +104 -0
- package/docs/slides.md +130 -0
- package/docs/slidev-migration.md +42 -0
- package/docs/steps-and-reveals.md +171 -0
- package/package.json +134 -0
- package/skills/SPEC.md +21 -0
- package/skills/honeydeck/SKILL.md +65 -0
- package/skills/presentation-writing/SKILL.md +75 -0
- package/skills/slidev-migration/SKILL.md +153 -0
- package/src/SPEC.md +89 -0
- package/src/assets.d.ts +30 -0
- package/src/cli/SPEC.md +230 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/banner.ts +9 -0
- package/src/cli/bin.js +5 -0
- package/src/cli/build.ts +229 -0
- package/src/cli/deck-path.ts +32 -0
- package/src/cli/dev.ts +263 -0
- package/src/cli/index.ts +126 -0
- package/src/cli/init.ts +369 -0
- package/src/cli/pdf.ts +923 -0
- package/src/cli/skill.ts +75 -0
- package/src/cli/templates/SPEC.md +70 -0
- package/src/cli/templates/deck-mdx.ts +15 -0
- package/src/cli/templates/package-json.ts +36 -0
- package/src/cli/templates/sparkle-button.ts +15 -0
- package/src/cli/templates/starter/components/SparkleButton.tsx +84 -0
- package/src/cli/templates/starter/deck.mdx +153 -0
- package/src/cli/templates/starter/styles.css +14 -0
- package/src/cli/templates/styles-css.ts +14 -0
- package/src/defaults.ts +1 -0
- package/src/layouts/ColorModeImage.tsx +55 -0
- package/src/layouts/SPEC.md +393 -0
- package/src/layouts/SlideFrame.tsx +48 -0
- package/src/layouts/bee/Blank.tsx +12 -0
- package/src/layouts/bee/Cover.tsx +70 -0
- package/src/layouts/bee/Default.tsx +42 -0
- package/src/layouts/bee/Image/Image.tsx +151 -0
- package/src/layouts/bee/Image/placeholder-dark.webp +0 -0
- package/src/layouts/bee/Image/placeholder-vertical-dark.webp +0 -0
- package/src/layouts/bee/Image/placeholder-vertical.webp +0 -0
- package/src/layouts/bee/Image/placeholder.webp +0 -0
- package/src/layouts/bee/ImageLeft.tsx +27 -0
- package/src/layouts/bee/ImageRight.tsx +27 -0
- package/src/layouts/bee/ImageSide.tsx +107 -0
- package/src/layouts/bee/Section.tsx +40 -0
- package/src/layouts/bee/TwoCol.tsx +108 -0
- package/src/layouts/bee/index.ts +40 -0
- package/src/layouts/clean/Blank.tsx +12 -0
- package/src/layouts/clean/Cover.tsx +58 -0
- package/src/layouts/clean/Default.tsx +33 -0
- package/src/layouts/clean/Image/Image.tsx +103 -0
- package/src/layouts/clean/ImageLeft.tsx +27 -0
- package/src/layouts/clean/ImageRight.tsx +27 -0
- package/src/layouts/clean/ImageSide.tsx +113 -0
- package/src/layouts/clean/Section.tsx +35 -0
- package/src/layouts/clean/TwoCol.tsx +63 -0
- package/src/layouts/clean/index.ts +40 -0
- package/src/layouts/index.ts +60 -0
- package/src/layouts/placeholders.ts +9 -0
- package/src/layouts/utils.ts +13 -0
- package/src/remark/SPEC.md +49 -0
- package/src/remark/h1-extract.ts +124 -0
- package/src/remark/index.ts +4 -0
- package/src/remark/shiki-code-blocks.ts +325 -0
- package/src/remark/step-numbering.ts +412 -0
- package/src/runtime/Deck.tsx +533 -0
- package/src/runtime/SPEC.md +256 -0
- package/src/runtime/SlideCanvas.tsx +95 -0
- package/src/runtime/TimelineContext.tsx +122 -0
- package/src/runtime/app-shell/index.html +31 -0
- package/src/runtime/app-shell/main.tsx +42 -0
- package/src/runtime/aspectRatio.ts +34 -0
- package/src/runtime/colorMode.ts +23 -0
- package/src/runtime/components/BrowserFrame.tsx +233 -0
- package/src/runtime/components/Button.tsx +57 -0
- package/src/runtime/components/CodeBlock.tsx +210 -0
- package/src/runtime/components/ColorModeCycleButton.tsx +59 -0
- package/src/runtime/components/ErrorBoundary.tsx +125 -0
- package/src/runtime/components/Keyboard.tsx +87 -0
- package/src/runtime/components/ListStyle.tsx +203 -0
- package/src/runtime/components/NavBar.tsx +223 -0
- package/src/runtime/components/NavBarButton.tsx +47 -0
- package/src/runtime/components/NavBarDivider.tsx +3 -0
- package/src/runtime/components/Notes.tsx +171 -0
- package/src/runtime/components/Reveal.tsx +82 -0
- package/src/runtime/components/RevealGroup.tsx +193 -0
- package/src/runtime/components/SPEC.md +263 -0
- package/src/runtime/components/SlideNumberBadge.tsx +11 -0
- package/src/runtime/components/TimelineSteps.tsx +115 -0
- package/src/runtime/components/index.ts +55 -0
- package/src/runtime/index.ts +42 -0
- package/src/runtime/inputOwnership.ts +68 -0
- package/src/runtime/keyboardTarget.ts +7 -0
- package/src/runtime/lastSlideRoute.ts +56 -0
- package/src/runtime/navigation.ts +211 -0
- package/src/runtime/router.ts +157 -0
- package/src/runtime/slideData.ts +137 -0
- package/src/runtime/sync.ts +267 -0
- package/src/runtime/types.ts +182 -0
- package/src/runtime/useKeyboardNav.ts +138 -0
- package/src/runtime/useSwipeNav.ts +257 -0
- package/src/runtime/views/DocsView.tsx +74 -0
- package/src/runtime/views/OverviewView.tsx +386 -0
- package/src/runtime/views/PresenterNotesPanel.tsx +76 -0
- package/src/runtime/views/PresenterView.tsx +340 -0
- package/src/runtime/views/SPEC.md +152 -0
- package/src/runtime/views/docs/ComponentsTab.tsx +178 -0
- package/src/runtime/views/docs/DocsHeader.tsx +101 -0
- package/src/runtime/views/docs/Intro.tsx +20 -0
- package/src/runtime/views/docs/LayoutsTab.tsx +324 -0
- package/src/runtime/views/docs/ThemeTab.tsx +110 -0
- package/src/runtime/views/index.ts +7 -0
- package/src/runtime/views/overviewGrid.ts +106 -0
- package/src/runtime/views/presenterPreview.ts +27 -0
- package/src/runtime/virtual-modules.d.ts +98 -0
- package/src/theme/SPEC.md +179 -0
- package/src/theme/base.css +623 -0
- package/src/theme/bee.css +35 -0
- package/src/theme/clean.css +38 -0
- package/src/vite-plugin/SPEC.md +114 -0
- package/src/vite-plugin/component-doc-crawler.ts +350 -0
- package/src/vite-plugin/deck-loader.ts +148 -0
- package/src/vite-plugin/index.ts +373 -0
- package/src/vite-plugin/layout-demo-crawler.ts +802 -0
- package/src/vite-plugin/splitter.ts +353 -0
- package/src/vite-plugin/token-manifest.ts +163 -0
- package/src/vite-plugin/virtual-modules.ts +587 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-level slide splitter.
|
|
3
|
+
*
|
|
4
|
+
* Splits a raw `deck.mdx` source string into individual slide segments.
|
|
5
|
+
* Pure function — no file I/O, no side effects.
|
|
6
|
+
*
|
|
7
|
+
* ### Slide-level frontmatter
|
|
8
|
+
* Supports Slidev-style per-slide frontmatter:
|
|
9
|
+
* ```mdx
|
|
10
|
+
* # Slide 1
|
|
11
|
+
* ---
|
|
12
|
+
* layout: Cover
|
|
13
|
+
* author: My Name
|
|
14
|
+
* ---
|
|
15
|
+
* # Cover Slide
|
|
16
|
+
* Opening slide body copy.
|
|
17
|
+
* ---
|
|
18
|
+
* # Normal Slide
|
|
19
|
+
* ```
|
|
20
|
+
* A block consisting entirely of `key: value` pairs (no markdown content)
|
|
21
|
+
* is treated as frontmatter for the NEXT content block, and merged into it
|
|
22
|
+
* as a proper `---\n...\n---` YAML block. This keeps the remark-frontmatter
|
|
23
|
+
* pipeline uniform for all slides.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export type SlideSegment = {
|
|
27
|
+
/** 0-based position in the deck */
|
|
28
|
+
index: number;
|
|
29
|
+
/** Complete MDX source for this slide, with shared imports prepended */
|
|
30
|
+
rawMdx: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type SplitResult = {
|
|
34
|
+
slides: SlideSegment[];
|
|
35
|
+
/** Parsed deck-level frontmatter (first block only) */
|
|
36
|
+
deckFrontmatter: Record<string, unknown>;
|
|
37
|
+
/** Import lines found in the preamble, prepended to every slide */
|
|
38
|
+
sharedImports: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type SplitSlidesOptions = {
|
|
42
|
+
/**
|
|
43
|
+
* Root decks treat the first leading frontmatter block as deck config.
|
|
44
|
+
* Imported MDX files treat every frontmatter block as slide-level metadata.
|
|
45
|
+
*/
|
|
46
|
+
frontmatterMode?: "deck" | "slide";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Drop empty blocks before import extraction. Disable this for already-expanded
|
|
50
|
+
* root decks so imports from a leading imported slide do not become deck-wide
|
|
51
|
+
* shared imports and get duplicated on later slides.
|
|
52
|
+
*/
|
|
53
|
+
trimLeadingEmptyBlocks?: boolean;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Internal helpers
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
const DECK_FRONTMATTER_KEYS = new Set([
|
|
61
|
+
"title",
|
|
62
|
+
"description",
|
|
63
|
+
"aspectRatio",
|
|
64
|
+
"colorMode",
|
|
65
|
+
"pdfColorMode",
|
|
66
|
+
"pdfSteps",
|
|
67
|
+
"transition",
|
|
68
|
+
"layouts",
|
|
69
|
+
"defaultLayout",
|
|
70
|
+
"showSlideNumbers",
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse simple "key: value" YAML (no nesting, no arrays).
|
|
75
|
+
* Coerces booleans and numbers; everything else stays a string.
|
|
76
|
+
*
|
|
77
|
+
* Exported so other parts of the pipeline (e.g. remark/h1-extract.ts) can
|
|
78
|
+
* reuse the same parsing logic without adding an external YAML dep.
|
|
79
|
+
*/
|
|
80
|
+
export function parseFrontmatter(yaml: string): Record<string, unknown> {
|
|
81
|
+
const result: Record<string, unknown> = {};
|
|
82
|
+
|
|
83
|
+
for (const line of yaml.split("\n")) {
|
|
84
|
+
const colonIdx = line.indexOf(":");
|
|
85
|
+
if (colonIdx === -1) continue;
|
|
86
|
+
|
|
87
|
+
const key = line.slice(0, colonIdx).trim();
|
|
88
|
+
const raw = line.slice(colonIdx + 1).trim();
|
|
89
|
+
|
|
90
|
+
if (!key) continue;
|
|
91
|
+
|
|
92
|
+
if (raw === "true") {
|
|
93
|
+
result[key] = true;
|
|
94
|
+
} else if (raw === "false") {
|
|
95
|
+
result[key] = false;
|
|
96
|
+
} else if (raw !== "" && !Number.isNaN(Number(raw))) {
|
|
97
|
+
result[key] = Number(raw);
|
|
98
|
+
} else if (
|
|
99
|
+
(raw.startsWith('"') && raw.endsWith('"')) ||
|
|
100
|
+
(raw.startsWith("'") && raw.endsWith("'"))
|
|
101
|
+
) {
|
|
102
|
+
result[key] = raw.slice(1, -1);
|
|
103
|
+
} else {
|
|
104
|
+
result[key] = raw;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The first YAML block is deck config, but authors naturally expect slide-level
|
|
113
|
+
* fields such as `layout: Cover` there to affect the opening slide too.
|
|
114
|
+
* Keep deck-only keys out of the slide frontmatter and pass everything else
|
|
115
|
+
* through as first-slide metadata.
|
|
116
|
+
*/
|
|
117
|
+
function firstSlideFrontmatterFromDeckYaml(yamlLines: string[]): string[] {
|
|
118
|
+
const slideLines = yamlLines.filter((line) => {
|
|
119
|
+
const match = line.match(/^\s*([\w-]+)\s*:/);
|
|
120
|
+
const key = match?.[1];
|
|
121
|
+
if (!key) return false;
|
|
122
|
+
return !DECK_FRONTMATTER_KEYS.has(key);
|
|
123
|
+
});
|
|
124
|
+
return slideLines.some((line) => /^\s*layout\s*:/.test(line))
|
|
125
|
+
? slideLines
|
|
126
|
+
: [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Fence tracking
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Return the opening fence marker for Markdown fenced code blocks.
|
|
135
|
+
* Supports backtick and tilde fences indented by up to three spaces.
|
|
136
|
+
*/
|
|
137
|
+
function getOpeningCodeFenceMarker(line: string): string | null {
|
|
138
|
+
const match = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
139
|
+
return match?.[1] ?? null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isClosingFence(line: string, openingFence: string): boolean {
|
|
143
|
+
const openingChar = openingFence[0];
|
|
144
|
+
if (!openingChar) return false;
|
|
145
|
+
|
|
146
|
+
const escapedChar = openingChar === "`" ? "`" : "~";
|
|
147
|
+
const pattern = new RegExp(
|
|
148
|
+
`^ {0,3}(${escapedChar}{${openingFence.length},})\\s*$`,
|
|
149
|
+
);
|
|
150
|
+
return pattern.test(line);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Frontmatter-only block detection
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Returns true if every non-empty line in `lines` looks like a YAML key-value
|
|
159
|
+
* pair (`key: value`). Used to identify slide-level frontmatter blocks that
|
|
160
|
+
* appear between `---` separators and should be merged with the next slide.
|
|
161
|
+
*/
|
|
162
|
+
function isFrontmatterOnlyBlock(lines: string[]): boolean {
|
|
163
|
+
const meaningful = lines.filter((l) => l.trim().length > 0);
|
|
164
|
+
if (meaningful.length === 0) return false; // empty block → not frontmatter
|
|
165
|
+
// Every meaningful line must look like `word: ...` (YAML key-value)
|
|
166
|
+
return meaningful.every((line) => /^\s*[\w-]+\s*:/.test(line));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Public API
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Split a raw MDX deck source into individual slide segments.
|
|
175
|
+
*
|
|
176
|
+
* ### Logic
|
|
177
|
+
* 1. If the file starts with `---\n`, extract YAML frontmatter (between the
|
|
178
|
+
* first and second `---` lines) as `deckFrontmatter`.
|
|
179
|
+
* 2. The remaining content is split on separator lines (lines that are
|
|
180
|
+
* exactly `---`), except inside fenced code blocks.
|
|
181
|
+
* 3. The **first block** (before the first separator) is the preamble:
|
|
182
|
+
* - Lines matching `/^import\s/` become `sharedImports`.
|
|
183
|
+
* - All other non-empty lines become the first slide's body.
|
|
184
|
+
* 4. `sharedImports` is prepended to every slide's `rawMdx` so that
|
|
185
|
+
* components imported in the preamble are available everywhere.
|
|
186
|
+
* 5. Empty blocks (only whitespace) are skipped and produce no slide.
|
|
187
|
+
*/
|
|
188
|
+
function normalizeLineEndings(source: string): string {
|
|
189
|
+
return source.replace(/\r\n?/g, "\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function splitSlides(
|
|
193
|
+
source: string,
|
|
194
|
+
options: SplitSlidesOptions = {},
|
|
195
|
+
): SplitResult {
|
|
196
|
+
const frontmatterMode = options.frontmatterMode ?? "deck";
|
|
197
|
+
const trimLeadingEmptyBlocks = options.trimLeadingEmptyBlocks ?? true;
|
|
198
|
+
const lines = normalizeLineEndings(source).split("\n");
|
|
199
|
+
let deckFrontmatter: Record<string, unknown> = {};
|
|
200
|
+
let firstSlideFrontmatterLines: string[] = [];
|
|
201
|
+
let startLine = 0;
|
|
202
|
+
|
|
203
|
+
// ── Step 1: extract frontmatter if present ──────────────────────────────
|
|
204
|
+
if (frontmatterMode === "deck" && lines[0] === "---") {
|
|
205
|
+
// Find the closing '---' line (first occurrence after line 0)
|
|
206
|
+
const fmEnd = lines.indexOf("---", 1);
|
|
207
|
+
if (fmEnd !== -1) {
|
|
208
|
+
const yamlLines = lines.slice(1, fmEnd);
|
|
209
|
+
deckFrontmatter = parseFrontmatter(yamlLines.join("\n"));
|
|
210
|
+
firstSlideFrontmatterLines = firstSlideFrontmatterFromDeckYaml(yamlLines);
|
|
211
|
+
startLine = fmEnd + 1;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Step 2: split remaining lines into blocks on '---' separators ───────
|
|
216
|
+
const rawBlocks: string[][] = [[]];
|
|
217
|
+
let openCodeFence: string | null = null;
|
|
218
|
+
|
|
219
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
220
|
+
const line = lines[i];
|
|
221
|
+
if (line === undefined) continue;
|
|
222
|
+
|
|
223
|
+
if (!openCodeFence && line === "---") {
|
|
224
|
+
rawBlocks.push([]);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
rawBlocks[rawBlocks.length - 1]?.push(line);
|
|
229
|
+
|
|
230
|
+
const marker = getOpeningCodeFenceMarker(line);
|
|
231
|
+
if (!marker) continue;
|
|
232
|
+
|
|
233
|
+
if (openCodeFence) {
|
|
234
|
+
if (isClosingFence(line, openCodeFence)) {
|
|
235
|
+
openCodeFence = null;
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
openCodeFence = marker;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Step 2b: merge frontmatter-only blocks with the following content ────
|
|
243
|
+
//
|
|
244
|
+
// A block consisting entirely of `key: value` pairs (no real markdown
|
|
245
|
+
// content) is treated as slide-level frontmatter for the NEXT block. It is
|
|
246
|
+
// re-emitted as a YAML frontmatter header (`---\n...\n---`) prepended to
|
|
247
|
+
// the next block's MDX source. This lets remark-frontmatter parse it.
|
|
248
|
+
const blocks: string[][] = [];
|
|
249
|
+
{
|
|
250
|
+
let bi = 0;
|
|
251
|
+
while (bi < rawBlocks.length) {
|
|
252
|
+
const block = rawBlocks[bi];
|
|
253
|
+
if (!block) break;
|
|
254
|
+
const nextBlock = rawBlocks[bi + 1];
|
|
255
|
+
|
|
256
|
+
if (nextBlock !== undefined && isFrontmatterOnlyBlock(block)) {
|
|
257
|
+
// Merge: prepend `---\n<yaml>\n---\n` to the next block
|
|
258
|
+
const yamlLines = block.filter((l) => l.trim().length > 0);
|
|
259
|
+
const merged = ["---", ...yamlLines, "---", "", ...nextBlock];
|
|
260
|
+
blocks.push(merged);
|
|
261
|
+
bi += 2; // skip the consumed next block
|
|
262
|
+
} else {
|
|
263
|
+
blocks.push(block);
|
|
264
|
+
bi++;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (trimLeadingEmptyBlocks) {
|
|
270
|
+
while (blocks[0]?.every((line) => line.trim().length === 0)) {
|
|
271
|
+
blocks.shift();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── Step 3: extract shared imports from the first block ─────────────────
|
|
276
|
+
const firstBlock = blocks[0] ?? [];
|
|
277
|
+
const importLines: string[] = [];
|
|
278
|
+
const slideContentLines: string[] = [];
|
|
279
|
+
|
|
280
|
+
for (const line of firstBlock) {
|
|
281
|
+
if (/^import\s/.test(line)) {
|
|
282
|
+
importLines.push(line);
|
|
283
|
+
} else {
|
|
284
|
+
slideContentLines.push(line);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const sharedImports = importLines.join("\n").trim();
|
|
289
|
+
const firstSlideContentBody = slideContentLines.join("\n").trim();
|
|
290
|
+
|
|
291
|
+
// ── Step 4: build slide segments ────────────────────────────────────────
|
|
292
|
+
const slides: SlideSegment[] = [];
|
|
293
|
+
let pendingFirstSlideFrontmatterLines = firstSlideFrontmatterLines;
|
|
294
|
+
|
|
295
|
+
function applyPendingFirstSlideFrontmatter(content: string): string {
|
|
296
|
+
if (pendingFirstSlideFrontmatterLines.length === 0) return content;
|
|
297
|
+
|
|
298
|
+
const linesToApply = pendingFirstSlideFrontmatterLines;
|
|
299
|
+
pendingFirstSlideFrontmatterLines = [];
|
|
300
|
+
|
|
301
|
+
if (content.startsWith("---")) {
|
|
302
|
+
const lines = content.split("\n");
|
|
303
|
+
const closingIdx = lines.indexOf("---", 1);
|
|
304
|
+
if (closingIdx !== -1) {
|
|
305
|
+
return ["---", ...linesToApply, ...lines.slice(1)].join("\n");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return ["---", ...linesToApply, "---", "", content].join("\n");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function addSlide(content: string): void {
|
|
313
|
+
let trimmed = content.trim();
|
|
314
|
+
if (!trimmed) return; // skip empty / whitespace-only blocks
|
|
315
|
+
|
|
316
|
+
trimmed = applyPendingFirstSlideFrontmatter(trimmed);
|
|
317
|
+
|
|
318
|
+
// If the slide content begins with a YAML frontmatter block (`---`), the
|
|
319
|
+
// shared imports must be placed AFTER the closing `---` so that
|
|
320
|
+
// remark-frontmatter sees the `---` at the very start of the document.
|
|
321
|
+
let rawMdx: string;
|
|
322
|
+
if (sharedImports && trimmed.startsWith("---")) {
|
|
323
|
+
// Find the end of the frontmatter block (second `---` line)
|
|
324
|
+
const fmLines = trimmed.split("\n");
|
|
325
|
+
const closingIdx = fmLines.indexOf("---", 1);
|
|
326
|
+
if (closingIdx !== -1) {
|
|
327
|
+
const fmBlock = fmLines.slice(0, closingIdx + 1).join("\n"); // includes both `---`
|
|
328
|
+
const body = fmLines
|
|
329
|
+
.slice(closingIdx + 1)
|
|
330
|
+
.join("\n")
|
|
331
|
+
.trimStart();
|
|
332
|
+
rawMdx = body
|
|
333
|
+
? `${fmBlock}\n\n${sharedImports}\n\n${body}`
|
|
334
|
+
: `${fmBlock}\n\n${sharedImports}`;
|
|
335
|
+
} else {
|
|
336
|
+
// Malformed frontmatter — fall back to prepending
|
|
337
|
+
rawMdx = `${sharedImports}\n\n${trimmed}`;
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
rawMdx = sharedImports ? `${sharedImports}\n\n${trimmed}` : trimmed;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
slides.push({ index: slides.length, rawMdx });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
addSlide(firstSlideContentBody);
|
|
347
|
+
|
|
348
|
+
for (let i = 1; i < blocks.length; i++) {
|
|
349
|
+
addSlide((blocks[i] ?? []).join("\n"));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return { slides, deckFrontmatter, sharedImports };
|
|
353
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Manifest Vite plugin for Honeydeck.
|
|
3
|
+
*
|
|
4
|
+
* Parses `src/theme/base.css` for `--honeydeck-*` CSS custom property declarations
|
|
5
|
+
* and their preceding single-line `/* ... *\/` comments (descriptions).
|
|
6
|
+
*
|
|
7
|
+
* Exposes the result as a virtual module:
|
|
8
|
+
*
|
|
9
|
+
* import { tokens } from 'virtual:honeydeck/token-manifest'
|
|
10
|
+
*
|
|
11
|
+
* Shape:
|
|
12
|
+
* export const tokens: TokenManifestEntry[]
|
|
13
|
+
*
|
|
14
|
+
* where `TokenManifestEntry = { name: string; description: string; defaultValue: string }`.
|
|
15
|
+
*
|
|
16
|
+
* ### Parsing rules
|
|
17
|
+
* - Tokens are extracted from anywhere in the file (`:root`, `[data-honeydeck-color-mode=...]`, `@theme`, etc.)
|
|
18
|
+
* - If the same token name appears multiple times the **first** occurrence wins (light mode
|
|
19
|
+
* declaration takes precedence over dark mode in the manifest).
|
|
20
|
+
* - A description is the text of the nearest single-line comment (`/* ... *\/`) on the
|
|
21
|
+
* line immediately above the declaration, ignoring blank lines.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFileSync } from "node:fs";
|
|
25
|
+
import { dirname, resolve } from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
import type { HmrContext, ModuleNode, Plugin } from "vite";
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Paths
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
|
|
35
|
+
/** Absolute path to the base CSS theme file. */
|
|
36
|
+
const BASE_CSS_PATH = resolve(__dirname, "../theme/base.css");
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Virtual module IDs
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
const VIRTUAL_ID = "virtual:honeydeck/token-manifest";
|
|
43
|
+
const RESOLVED_ID = "\0virtual:honeydeck/token-manifest";
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Types
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
export type TokenManifestEntry = {
|
|
50
|
+
/** CSS custom property name, e.g. `--honeydeck-primary` */
|
|
51
|
+
name: string;
|
|
52
|
+
/** Description sourced from the preceding comment, or `""` */
|
|
53
|
+
description: string;
|
|
54
|
+
/** Raw default value from the CSS declaration, e.g. `oklch(50% 0.2 250)` */
|
|
55
|
+
defaultValue: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Parser
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extract all `--honeydeck-*` token entries from a CSS string.
|
|
64
|
+
*
|
|
65
|
+
* @param css - Raw CSS text (e.g. contents of base.css)
|
|
66
|
+
* @returns Array of token entries in document order (first occurrence wins for duplicates).
|
|
67
|
+
*/
|
|
68
|
+
export function extractTokens(css: string): TokenManifestEntry[] {
|
|
69
|
+
const tokens: TokenManifestEntry[] = [];
|
|
70
|
+
const seen = new Set<string>();
|
|
71
|
+
const lines = css.split("\n");
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < lines.length; i++) {
|
|
74
|
+
const line = lines[i]?.trim();
|
|
75
|
+
|
|
76
|
+
// Match a CSS custom property declaration:
|
|
77
|
+
// --honeydeck-something: <value>;
|
|
78
|
+
// Allow optional trailing inline comment.
|
|
79
|
+
const propMatch = line.match(
|
|
80
|
+
/^(--honeydeck-[\w-]+)\s*:\s*(.+?)\s*(?:;.*)?$/,
|
|
81
|
+
);
|
|
82
|
+
if (!propMatch) continue;
|
|
83
|
+
|
|
84
|
+
const [, name, value] = propMatch;
|
|
85
|
+
if (!name || !value) continue;
|
|
86
|
+
const rawValue = value
|
|
87
|
+
.replace(/;.*$/, "") // strip trailing semicolon + any trailing comment
|
|
88
|
+
.trim();
|
|
89
|
+
|
|
90
|
+
// First occurrence of this token name wins.
|
|
91
|
+
if (seen.has(name)) continue;
|
|
92
|
+
seen.add(name);
|
|
93
|
+
|
|
94
|
+
// Look backward (up to 3 lines) for the nearest preceding comment.
|
|
95
|
+
// Stop if we hit a blank line or another property declaration first.
|
|
96
|
+
let description = "";
|
|
97
|
+
for (let j = i - 1; j >= Math.max(0, i - 4); j--) {
|
|
98
|
+
const prev = lines[j]?.trim();
|
|
99
|
+
if (prev === "") continue; // skip blank lines
|
|
100
|
+
if (prev.startsWith("--") || prev.startsWith("}") || prev.startsWith("{"))
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
// Single-line /* ... */ comment
|
|
104
|
+
const commentMatch = prev.match(/^\/\*\s*(.*?)\s*\*\/\s*$/);
|
|
105
|
+
if (commentMatch) {
|
|
106
|
+
description = commentMatch[1]?.trim();
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
// If it's code (not a comment), stop.
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
tokens.push({ name, description, defaultValue: rawValue });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return tokens;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Plugin
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Vite plugin that exposes `virtual:honeydeck/token-manifest`.
|
|
125
|
+
* Watches `src/theme/base.css` and invalidates the module on change.
|
|
126
|
+
*/
|
|
127
|
+
export function tokenManifestPlugin(): Plugin {
|
|
128
|
+
return {
|
|
129
|
+
name: "honeydeck:token-manifest",
|
|
130
|
+
|
|
131
|
+
resolveId(id: string) {
|
|
132
|
+
if (id === VIRTUAL_ID) return RESOLVED_ID;
|
|
133
|
+
return null;
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
load(id: string) {
|
|
137
|
+
if (id !== RESOLVED_ID) return null;
|
|
138
|
+
|
|
139
|
+
const css = readFileSync(BASE_CSS_PATH, "utf-8");
|
|
140
|
+
const tokens = extractTokens(css);
|
|
141
|
+
|
|
142
|
+
return [
|
|
143
|
+
`export const tokens = ${JSON.stringify(tokens, null, 2)};`,
|
|
144
|
+
`export default tokens;`,
|
|
145
|
+
].join("\n");
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
// Ensure the CSS file is watched in dev mode.
|
|
149
|
+
configureServer(server) {
|
|
150
|
+
server.watcher.add(BASE_CSS_PATH);
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// Invalidate the virtual module when base.css changes.
|
|
154
|
+
handleHotUpdate(ctx: HmrContext): ModuleNode[] | undefined {
|
|
155
|
+
if (ctx.file !== BASE_CSS_PATH) return;
|
|
156
|
+
const mod = ctx.server.moduleGraph.getModuleById(RESOLVED_ID);
|
|
157
|
+
if (mod) {
|
|
158
|
+
ctx.server.moduleGraph.invalidateModule(mod);
|
|
159
|
+
return [mod];
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|