Skip to content

Commit 5002cd4

Browse files
committed
Support svelte live code blocks
1 parent e461194 commit 5002cd4

8 files changed

Lines changed: 181 additions & 5 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ test-*
1414
# Content Collections
1515
.content-collections
1616

17+
# mdsx live code
18+
.live-code/
19+
1720
# Generated API documentation
1821
**/generated/api/*.json
1922

docs/mdsx.config.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import rehypePrettyCode from 'rehype-pretty-code';
77
import {
88
prettyCodeOptions,
99
rehypeCodeBlockTitle,
10-
rehypeHandleCodeBlocks
10+
rehypeHandleCodeBlocks,
11+
remarkLiveCode
1112
} from './src/lib/markdown/config/index.js';
1213

1314
export const mdsxConfig = defineConfig({
1415
extensions: ['.md'],
15-
remarkPlugins: [remarkGfm],
16+
remarkPlugins: [remarkGfm, remarkLiveCode],
1617
rehypePlugins: [
1718
rehypeSlug,
1819
// rehypeComponentExample,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script lang="ts">
2+
import type { Snippet } from 'svelte';
3+
4+
interface Props {
5+
preview: Snippet;
6+
children: Snippet;
7+
}
8+
9+
let { preview, children }: Props = $props();
10+
</script>
11+
12+
<div class="live-code">
13+
<div class="live-code-preview p-6 border border-t rounded-t-lg">
14+
{@render preview()}
15+
</div>
16+
{@render children()}
17+
</div>

docs/src/lib/markdown/components/pre.svelte

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
'data-title': dataTitle,
99
...restProps
1010
}: HTMLAttributes<HTMLPreElement> = $props();
11-
$inspect({ restProps });
1211
</script>
1312

1413
<pre

docs/src/lib/markdown/config/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Remark plugins
2+
export { remarkLiveCode } from '../rehype/live-code.js';
3+
14
// Rehype plugins
25
export { rehypeCodeBlockTitle } from '../rehype/code-block-title.js';
36
export { rehypeHandleCodeBlocks } from '../rehype/handle-code-blocks.js';
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2+
import { resolve } from 'node:path';
3+
import process from 'node:process';
4+
import { visit } from 'unist-util-visit';
5+
6+
const BASE_PATH = resolve(process.cwd(), '.live-code');
7+
const LIVE_CODE_MAP = resolve(BASE_PATH, 'live-code-map.json');
8+
9+
/**
10+
* Remark plugin to handle live code blocks (e.g., ```svelte live)
11+
* Writes live code to .svelte files and imports them
12+
* MUST run BEFORE any rehype plugins (operates on markdown AST, not HTML)
13+
* @returns {(tree: import('mdast').Root, vFile: import('vfile').VFile) => void}
14+
*/
15+
export function remarkLiveCode() {
16+
// Ensure directory exists
17+
if (!existsSync(BASE_PATH)) {
18+
mkdirSync(BASE_PATH, { recursive: true });
19+
}
20+
21+
// Initialize or load the map file
22+
if (!existsSync(LIVE_CODE_MAP)) {
23+
writeFileSync(LIVE_CODE_MAP, '{}');
24+
}
25+
26+
return (tree, vFile) => {
27+
const liveCodeImports = [];
28+
let hasScript = false;
29+
30+
visit(tree, 'code', (node, index, parent) => {
31+
if (index === null || !parent) return;
32+
const { meta, lang, value } = node;
33+
const metaArray = (meta || '').split(' ').filter(Boolean);
34+
35+
// Check if this is a live code block
36+
if (lang !== 'svelte' || !metaArray.includes('live')) return;
37+
38+
// Use the raw code value
39+
const rawCode = value || '';
40+
41+
// Generate unique ID for this code block
42+
const blockId = `${vFile.path}-${index}`;
43+
const idMap = JSON.parse(readFileSync(LIVE_CODE_MAP, 'utf-8'));
44+
45+
let componentFileName = idMap[blockId];
46+
if (!componentFileName) {
47+
// Generate unique filename
48+
const hash = Math.random().toString(36).substring(2, 11);
49+
componentFileName = `LiveCode${hash}.svelte`;
50+
idMap[blockId] = componentFileName;
51+
writeFileSync(LIVE_CODE_MAP, JSON.stringify(idMap, null, 2));
52+
}
53+
54+
// Write the live code to a .svelte file
55+
const componentPath = resolve(BASE_PATH, componentFileName);
56+
writeFileSync(componentPath, rawCode);
57+
58+
// Generate component name (remove .svelte extension)
59+
const componentName = componentFileName.replace(/\.svelte$/, '');
60+
61+
// Track import for later injection
62+
liveCodeImports.push({
63+
componentName,
64+
path: `/.live-code/${componentFileName}`
65+
});
66+
67+
// Create the live code container structure wrapped in LiveCodeWrapper
68+
const liveCodeContainer = {
69+
type: 'paragraph',
70+
data: {
71+
hName: 'div',
72+
hProperties: {}
73+
},
74+
children: [
75+
{
76+
type: 'html',
77+
value: `<LiveCode>{#snippet preview()}<${componentName} />{/snippet}<div class="live-code-source">`
78+
}
79+
]
80+
};
81+
82+
// Add code section with original code block (title will be handled by rehype-code-block-title)
83+
liveCodeContainer.children.push(node);
84+
85+
liveCodeContainer.children.push({
86+
type: 'html',
87+
value: '</div></LiveCode>' // Close live-code-source and LiveCode
88+
});
89+
90+
// Replace the code node with the container
91+
parent.children[index] = liveCodeContainer;
92+
});
93+
94+
// Inject imports at the beginning of the file
95+
if (liveCodeImports.length > 0) {
96+
const importStatements = [
97+
"import LiveCode from '$lib/markdown/components/LiveCode.svelte';",
98+
...liveCodeImports.map(
99+
({ componentName, path }) => `import ${componentName} from '${path}';`
100+
)
101+
].join('\n');
102+
103+
// Find existing script tag or create new one
104+
visit(tree, 'html', (node, idx, parent) => {
105+
if (node.value.startsWith('<script') && !hasScript) {
106+
hasScript = true;
107+
node.value = node.value.replace(
108+
/<script[^>]*>/,
109+
(match) => `${match}\n${importStatements}`
110+
);
111+
return visit.EXIT;
112+
}
113+
});
114+
115+
if (!hasScript) {
116+
// Create new script tag at the beginning
117+
tree.children.unshift({
118+
type: 'html',
119+
value: `<script>\n${importStatements}\n</script>`
120+
});
121+
}
122+
}
123+
};
124+
}

docs/src/routes/docs/markdown/+page.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,38 @@
9090
<div>Test</div>
9191
```
9292

93+
### Live Code
94+
95+
```svelte live title="Counter.svelte"
96+
<script>
97+
let count = $state(0);
98+
</script>
99+
100+
<button onclick={() => count++}>
101+
Clicked {count}
102+
{count === 1 ? 'time' : 'times'}
103+
</button>
104+
105+
<style>
106+
button {
107+
padding: 0.5rem 1rem;
108+
background: purple;
109+
color: white;
110+
border: none;
111+
border-radius: 0.25rem;
112+
cursor: pointer;
113+
}
114+
</style>
115+
```
116+
93117
## Table
94118

95-
````md
119+
```md
96120
| First | Second | Third |
97121
| ----- | ------ | ----- |
98122
| 1 | 2 | 3 |
99123
| 4 | 5 | 6 |
100-
````
124+
```
101125

102126
| First | Second | Third |
103127
| ----- | ------ | ----- |

docs/vite.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ export default defineConfig({
2525
}
2626
}) /*, devtoolsJson()*/
2727
],
28+
server: {
29+
fs: {
30+
allow: ['.live-code']
31+
}
32+
},
2833
test: {
2934
expect: { requireAssertions: true },
3035
projects: [

0 commit comments

Comments
 (0)