repl-sdk 0.0.0 → 1.0.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/dist/assets/tar-worker-kdkltuRC.js +598 -0
- package/dist/assets/tar-worker-kdkltuRC.js.map +1 -0
- package/dist/codemirror-D4aIVflZ.js +110 -0
- package/dist/codemirror-D4aIVflZ.js.map +1 -0
- package/dist/gjs-CzFzkEFv.js +173 -0
- package/dist/gjs-CzFzkEFv.js.map +1 -0
- package/dist/gmd-D9OXs2v3.js +166 -0
- package/dist/gmd-D9OXs2v3.js.map +1 -0
- package/dist/hbs-CuhWjffM.js +62 -0
- package/dist/hbs-CuhWjffM.js.map +1 -0
- package/dist/index-CUWCqMoD.js +2133 -0
- package/dist/index-CUWCqMoD.js.map +1 -0
- package/dist/index.js +4 -104
- package/dist/index.js.map +1 -1
- package/dist/parse-aBKk9rfS.js +328 -0
- package/dist/parse-aBKk9rfS.js.map +1 -0
- package/dist/render-app-island-B-i8rvGi.js +61 -0
- package/dist/render-app-island-B-i8rvGi.js.map +1 -0
- package/package.json +82 -9
- package/src/cache.js +138 -0
- package/src/cdn.js +93 -0
- package/src/codemirror.js +161 -0
- package/src/compilers/ember/gjs.js +212 -0
- package/src/compilers/ember/gmd.js +190 -0
- package/src/compilers/ember/hbs.js +98 -0
- package/src/compilers/ember/render-app-island.js +83 -0
- package/src/compilers/ember.js +166 -0
- package/src/compilers/js.js +32 -0
- package/src/compilers/markdown/build-compiler.js +151 -0
- package/src/compilers/markdown/const.js +2 -0
- package/src/compilers/markdown/heading-id.js +75 -0
- package/src/compilers/markdown/live-code-extraction.js +198 -0
- package/src/compilers/markdown/parse.js +22 -0
- package/src/compilers/markdown/parse.test.ts +363 -0
- package/src/compilers/markdown/sanitize-for-glimmer.js +26 -0
- package/src/compilers/markdown/types.ts +21 -0
- package/src/compilers/markdown/utils.js +78 -0
- package/src/compilers/markdown.js +125 -0
- package/src/compilers/mermaid.js +35 -0
- package/src/compilers/react.js +47 -0
- package/src/compilers/svelte.js +116 -0
- package/src/compilers/vue.js +58 -0
- package/src/compilers.js +108 -0
- package/src/es-module-shim.js +53 -0
- package/src/index.d.ts +53 -4
- package/src/index.js +744 -89
- package/src/npm.js +58 -0
- package/src/request.Request.test.ts +59 -0
- package/src/request.js +140 -0
- package/src/resolve.fromImports.test.ts +35 -0
- package/src/resolve.fromInternalImport.test.ts +69 -0
- package/src/resolve.js +352 -0
- package/src/resolve.resolvePath.test.ts +24 -0
- package/src/resolve.test.ts +23 -0
- package/src/specifier.js +71 -0
- package/src/specifier.test.ts +90 -0
- package/src/tar-worker.js +61 -0
- package/src/tar.js +76 -0
- package/src/types.ts +335 -58
- package/src/utils.js +28 -1
- package/declarations/index.d.ts +0 -73
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import rehypeShiki from '@shikijs/rehype';
|
|
2
|
+
import { stripIndent } from 'common-tags';
|
|
3
|
+
import { visit } from 'unist-util-visit';
|
|
4
|
+
import { describe, expect as errorExpect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { parseMarkdown } from './parse.js';
|
|
7
|
+
import { buildCodeFenceMetaUtils } from './utils.js';
|
|
8
|
+
|
|
9
|
+
import type { Element, Root as HastRoot } from 'hast';
|
|
10
|
+
import type { Heading, Root as MdastRoot } from 'mdast';
|
|
11
|
+
|
|
12
|
+
const expect = errorExpect.soft;
|
|
13
|
+
|
|
14
|
+
function assertOutput(actual: string, expected: string) {
|
|
15
|
+
const _actual = actual?.split('\n')?.filter(Boolean)?.join('\n')?.trim();
|
|
16
|
+
const _expected = expected.split('\n').filter(Boolean).join('\n').trim();
|
|
17
|
+
|
|
18
|
+
expect(_actual).toBe(_expected);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function assertCodeBlocks(
|
|
22
|
+
actual: unknown,
|
|
23
|
+
expected: { code: string; format: string; meta: string }[]
|
|
24
|
+
) {
|
|
25
|
+
expect(actual).toMatchObject(expected);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ALLOWED_FORMATS = ['gjs', 'jsx', 'vue', 'svelte', 'hbs'];
|
|
29
|
+
const defaults = {
|
|
30
|
+
...buildCodeFenceMetaUtils({
|
|
31
|
+
getAllowedFormats: () => ALLOWED_FORMATS,
|
|
32
|
+
getFlavorsFor: (lang) => {
|
|
33
|
+
if (lang === 'jsx') {
|
|
34
|
+
return ['react'];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return [];
|
|
38
|
+
},
|
|
39
|
+
optionsFor: () => ({}),
|
|
40
|
+
}),
|
|
41
|
+
ALLOWED_FORMATS,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
describe('options', () => {
|
|
45
|
+
describe('remarkPlugins', () => {
|
|
46
|
+
it('works', async () => {
|
|
47
|
+
const result = await parseMarkdown(`# Title`, {
|
|
48
|
+
...defaults,
|
|
49
|
+
remarkPlugins: [
|
|
50
|
+
function noH1() {
|
|
51
|
+
return (tree: MdastRoot) => {
|
|
52
|
+
visit(tree, 'heading', function (node: Heading) {
|
|
53
|
+
if (node.depth === 1) {
|
|
54
|
+
node.depth = 2;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(result.text).toBe('<h2 id="title">Title</h2>');
|
|
63
|
+
expect(result.codeBlocks).to.deep.equal([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('w/ options', async () => {
|
|
67
|
+
const result = await parseMarkdown(`# Title`, {
|
|
68
|
+
...defaults,
|
|
69
|
+
remarkPlugins: [
|
|
70
|
+
[
|
|
71
|
+
function noH1(options: { depth: 1 | 2 | 3 | 4 | 5 | 6 }) {
|
|
72
|
+
return (tree: MdastRoot) => {
|
|
73
|
+
visit(tree, 'heading', function (node: Heading) {
|
|
74
|
+
if (node.depth === 1) {
|
|
75
|
+
node.depth = options.depth;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
{ depth: 3 },
|
|
81
|
+
],
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(result.text).toBe('<h3 id="title">Title</h3>');
|
|
86
|
+
expect(result.codeBlocks).to.deep.equal([]);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('rehypePlugins', () => {
|
|
91
|
+
it('works', async () => {
|
|
92
|
+
const result = await parseMarkdown(`# Title`, {
|
|
93
|
+
...defaults,
|
|
94
|
+
rehypePlugins: [
|
|
95
|
+
function noH1() {
|
|
96
|
+
return (tree: HastRoot) => {
|
|
97
|
+
visit(tree, 'element', function (node: Element) {
|
|
98
|
+
if (node.tagName === 'h1') {
|
|
99
|
+
node.tagName = 'h2';
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(result.text).toBe('<h2 id="title">Title</h2>');
|
|
108
|
+
expect(result.codeBlocks).to.deep.equal([]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('w/ options', async () => {
|
|
112
|
+
const result = await parseMarkdown(`# Title`, {
|
|
113
|
+
...defaults,
|
|
114
|
+
rehypePlugins: [
|
|
115
|
+
[
|
|
116
|
+
function noH1(options: { depth: 1 | 2 | 3 | 4 | 5 | 6 }) {
|
|
117
|
+
return (tree: HastRoot) => {
|
|
118
|
+
visit(tree, 'element', function (node: Element) {
|
|
119
|
+
if (node.tagName === 'h1') {
|
|
120
|
+
node.tagName = `h${options.depth ?? 2}`;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
{ depth: 3 },
|
|
126
|
+
],
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(result.text).toBe('<h3 id="title">Title</h3>');
|
|
131
|
+
expect(result.codeBlocks).to.deep.equal([]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('retains {{ }} escaping', async () => {
|
|
135
|
+
const result = await parseMarkdown(
|
|
136
|
+
stripIndent`
|
|
137
|
+
# Title
|
|
138
|
+
|
|
139
|
+
\`\`\`gjs
|
|
140
|
+
const two = 2
|
|
141
|
+
|
|
142
|
+
<template>
|
|
143
|
+
{{two}}
|
|
144
|
+
</template>
|
|
145
|
+
\`\`\`
|
|
146
|
+
`,
|
|
147
|
+
{
|
|
148
|
+
...defaults,
|
|
149
|
+
rehypePlugins: [[rehypeShiki, { theme: 'github-dark' }]],
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
assertOutput(
|
|
154
|
+
result.text,
|
|
155
|
+
`<h1 id="title">Title</h1>
|
|
156
|
+
<div class="repl-sdk__snippet" data-repl-output><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> two</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> 2</span></span>
|
|
157
|
+
<span class="line"></span>
|
|
158
|
+
<span class="line"><span style="color:#E1E4E8"><</span><span style="color:#85E89D">template</span><span style="color:#E1E4E8">></span></span>
|
|
159
|
+
<span class="line"><span style="color:#F97583"> \\{{</span><span style="color:#79B8FF">two</span><span style="color:#F97583">}}</span></span>
|
|
160
|
+
<span class="line"><span style="color:#E1E4E8"></</span><span style="color:#85E89D">template</span><span style="color:#E1E4E8">></span></span></code></pre></div>`
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('codefences', () => {
|
|
166
|
+
describe('hbs', () => {
|
|
167
|
+
it('Code fence is live', async () => {
|
|
168
|
+
const snippet = `{{concat "hello" " " "there"}}`;
|
|
169
|
+
const result = await parseMarkdown(
|
|
170
|
+
stripIndent`
|
|
171
|
+
# Title
|
|
172
|
+
|
|
173
|
+
\`\`\`hbs live
|
|
174
|
+
${snippet}
|
|
175
|
+
\`\`\`
|
|
176
|
+
`.trim(),
|
|
177
|
+
{ ...defaults }
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect(result.text).toMatchInlineSnapshot(`
|
|
181
|
+
"<h1 id="title">Title</h1>
|
|
182
|
+
<div id="repl_1" class="repl-sdk__demo"></div>"
|
|
183
|
+
`);
|
|
184
|
+
|
|
185
|
+
assertCodeBlocks(result.codeBlocks, [
|
|
186
|
+
{
|
|
187
|
+
code: snippet,
|
|
188
|
+
format: 'hbs',
|
|
189
|
+
meta: 'live',
|
|
190
|
+
},
|
|
191
|
+
]);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('gjs', () => {
|
|
196
|
+
it('Code fence does not have the "live" keyword', async function () {
|
|
197
|
+
const result = await parseMarkdown(
|
|
198
|
+
stripIndent`
|
|
199
|
+
# Title
|
|
200
|
+
|
|
201
|
+
\`\`\`gjs
|
|
202
|
+
const two = 2;
|
|
203
|
+
\`\`\`
|
|
204
|
+
`,
|
|
205
|
+
{ ...defaults }
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
assertOutput(
|
|
209
|
+
result.text,
|
|
210
|
+
stripIndent`
|
|
211
|
+
<h1 id="title">Title</h1>
|
|
212
|
+
|
|
213
|
+
<div class=\"repl-sdk__snippet\" data-repl-output><pre><code class=\"language-gjs\"> const two = 2;
|
|
214
|
+
</code></pre></div>
|
|
215
|
+
`
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
expect(result.codeBlocks).to.deep.equal([]);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('Code fence is live', async function () {
|
|
222
|
+
const snippet = `const two = 2`;
|
|
223
|
+
const result = await parseMarkdown(
|
|
224
|
+
stripIndent`
|
|
225
|
+
# Title
|
|
226
|
+
|
|
227
|
+
\`\`\`gjs live
|
|
228
|
+
${snippet}
|
|
229
|
+
\`\`\`
|
|
230
|
+
`,
|
|
231
|
+
{ ...defaults }
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
expect(result.text).toMatchInlineSnapshot(`
|
|
235
|
+
"<h1 id="title">Title</h1>
|
|
236
|
+
<div id="repl_2" class="repl-sdk__demo"></div>"
|
|
237
|
+
`);
|
|
238
|
+
|
|
239
|
+
assertCodeBlocks(result.codeBlocks, [
|
|
240
|
+
{
|
|
241
|
+
code: snippet,
|
|
242
|
+
format: 'gjs',
|
|
243
|
+
meta: 'live',
|
|
244
|
+
},
|
|
245
|
+
]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('Code with preview fence has {{ }} tokens escaped', async function () {
|
|
249
|
+
const result = await parseMarkdown(
|
|
250
|
+
stripIndent`
|
|
251
|
+
# Title
|
|
252
|
+
|
|
253
|
+
\`\`\`gjs
|
|
254
|
+
const two = 2
|
|
255
|
+
|
|
256
|
+
<template>
|
|
257
|
+
{{two}}
|
|
258
|
+
</template>
|
|
259
|
+
\`\`\`
|
|
260
|
+
`,
|
|
261
|
+
{ ...defaults }
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
assertOutput(
|
|
265
|
+
result.text,
|
|
266
|
+
stripIndent`
|
|
267
|
+
<h1 id="title">Title</h1>
|
|
268
|
+
|
|
269
|
+
<div class=\"repl-sdk__snippet\" data-repl-output><pre><code class=\"language-gjs\">const two = 2
|
|
270
|
+
|
|
271
|
+
<template>
|
|
272
|
+
\\{{two}}
|
|
273
|
+
</template>
|
|
274
|
+
</code></pre></div>
|
|
275
|
+
`
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('Inline Code with {{ }} tokens is escaped', async function () {
|
|
280
|
+
const result = await parseMarkdown(
|
|
281
|
+
stripIndent`
|
|
282
|
+
# Title
|
|
283
|
+
|
|
284
|
+
\`{{ foo }}\`
|
|
285
|
+
`,
|
|
286
|
+
{ ...defaults }
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
assertOutput(
|
|
290
|
+
result.text,
|
|
291
|
+
stripIndent`
|
|
292
|
+
<h1 id="title">Title</h1>
|
|
293
|
+
<p><code>\\{{ foo }}</code></p>
|
|
294
|
+
`
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('Can invoke a component again when defined in a live fence', async function () {
|
|
299
|
+
const snippet = `const two = 2`;
|
|
300
|
+
const result = await parseMarkdown(
|
|
301
|
+
stripIndent`
|
|
302
|
+
# Title
|
|
303
|
+
|
|
304
|
+
\`\`\`gjs live
|
|
305
|
+
${snippet}
|
|
306
|
+
\`\`\`
|
|
307
|
+
<Demo />
|
|
308
|
+
`,
|
|
309
|
+
{ ...defaults }
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
expect(result.text).toMatchInlineSnapshot(`
|
|
313
|
+
"<h1 id="title">Title</h1>
|
|
314
|
+
<div id="repl_3" class="repl-sdk__demo"></div>
|
|
315
|
+
<Demo />"
|
|
316
|
+
`);
|
|
317
|
+
|
|
318
|
+
assertCodeBlocks(result.codeBlocks, [
|
|
319
|
+
{
|
|
320
|
+
code: snippet,
|
|
321
|
+
format: 'gjs',
|
|
322
|
+
meta: 'live',
|
|
323
|
+
},
|
|
324
|
+
]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('Code fence imports things', async function () {
|
|
328
|
+
const snippet = stripIndent`
|
|
329
|
+
import Component from '@glimmer/component';
|
|
330
|
+
import { on } from '@ember/modifier';
|
|
331
|
+
|
|
332
|
+
<template>
|
|
333
|
+
<button {{on "click" console.log}}>Click</button>
|
|
334
|
+
</template>
|
|
335
|
+
`;
|
|
336
|
+
const result = await parseMarkdown(
|
|
337
|
+
`hi\n` + `\n` + '```gjs live preview\n' + snippet + '\n```',
|
|
338
|
+
{ ...defaults }
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
expect(result.text).toMatchInlineSnapshot(`
|
|
342
|
+
"<p>hi</p>
|
|
343
|
+
<div id="repl_4" class="repl-sdk__demo"></div>
|
|
344
|
+
<div class="repl-sdk__snippet" data-repl-output><pre><code class="language-gjs">import Component from '@glimmer/component';
|
|
345
|
+
import { on } from '@ember/modifier';
|
|
346
|
+
|
|
347
|
+
<template>
|
|
348
|
+
<button \\{{on "click" console.log}}>Click</button>
|
|
349
|
+
</template>
|
|
350
|
+
</code></pre></div>"
|
|
351
|
+
`);
|
|
352
|
+
|
|
353
|
+
assertCodeBlocks(result.codeBlocks, [
|
|
354
|
+
{
|
|
355
|
+
code: snippet,
|
|
356
|
+
format: 'gjs',
|
|
357
|
+
meta: 'live preview',
|
|
358
|
+
},
|
|
359
|
+
]);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @type {import('unified').Plugin<[], import('hast').Root>}
|
|
5
|
+
*/
|
|
6
|
+
export function sanitizeForGlimmer(/* options */) {
|
|
7
|
+
return function transformer(tree) {
|
|
8
|
+
visit(tree, 'element', function visitor(node) {
|
|
9
|
+
if (node.type === 'element' && 'tagName' in node) {
|
|
10
|
+
const element = /** @type {import('hast').Element} */ (node);
|
|
11
|
+
|
|
12
|
+
if (!['pre', 'code'].includes(element.tagName)) return;
|
|
13
|
+
|
|
14
|
+
visit(node, 'text', function textVisitor(textNode) {
|
|
15
|
+
if (textNode.type === 'text') {
|
|
16
|
+
const text = /** @type {import('hast').Text} */ (textNode);
|
|
17
|
+
|
|
18
|
+
text.value = text.value.replace(/{{/g, '\\{{');
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return 'skip';
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface LiveCodeExtractionOptions {
|
|
2
|
+
isLive?: (meta: string, lang: string) => boolean;
|
|
3
|
+
ALLOWED_FORMATS?: string[];
|
|
4
|
+
isPreview?: (meta: string) => boolean;
|
|
5
|
+
isBelow?: (meta: string) => boolean;
|
|
6
|
+
needsLive?: (lang: string) => boolean;
|
|
7
|
+
getFlavorFromMeta?: (meta: string, lang: string) => string | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PublicOptions {
|
|
11
|
+
code?: {
|
|
12
|
+
classList?: string[];
|
|
13
|
+
};
|
|
14
|
+
demo?: {
|
|
15
|
+
classList?: string[];
|
|
16
|
+
};
|
|
17
|
+
remarkPlugins?: unknown[];
|
|
18
|
+
rehypePlugins?: unknown[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type InternalOptions = PublicOptions & LiveCodeExtractionOptions;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} lang
|
|
3
|
+
*/
|
|
4
|
+
export function isNotMarkdownLike(lang) {
|
|
5
|
+
return lang !== 'md' && lang !== 'gmd' && lang !== 'mdx';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} meta
|
|
10
|
+
*/
|
|
11
|
+
export function isPreview(meta) {
|
|
12
|
+
if (!meta) return false;
|
|
13
|
+
|
|
14
|
+
return meta.includes('preview');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} meta
|
|
19
|
+
*/
|
|
20
|
+
export function isBelow(meta) {
|
|
21
|
+
if (!meta) return false;
|
|
22
|
+
|
|
23
|
+
return meta.includes('below');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {Pick<import('../../types').PublicMethods, 'getAllowedFormats' | 'getFlavorsFor' | 'optionsFor'>} api
|
|
28
|
+
*/
|
|
29
|
+
export function buildCodeFenceMetaUtils(api) {
|
|
30
|
+
const allowedFormats = api.getAllowedFormats().filter(isNotMarkdownLike);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} lang
|
|
34
|
+
* @param {string | undefined} [ flavor ]
|
|
35
|
+
*/
|
|
36
|
+
function needsLive(lang, flavor) {
|
|
37
|
+
if (!allowedFormats.includes(lang)) return false;
|
|
38
|
+
|
|
39
|
+
return api.optionsFor(lang, flavor).needsLiveMeta ?? true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} meta
|
|
44
|
+
* @param {string} lang
|
|
45
|
+
*/
|
|
46
|
+
function getFlavorFromMeta(meta, lang) {
|
|
47
|
+
const flavors = api.getFlavorsFor(lang);
|
|
48
|
+
|
|
49
|
+
const flavor = meta
|
|
50
|
+
?.trim()
|
|
51
|
+
.split(' ')
|
|
52
|
+
.find((metum) => flavors.includes(metum));
|
|
53
|
+
|
|
54
|
+
return flavor;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} meta
|
|
59
|
+
* @param {string} lang
|
|
60
|
+
*/
|
|
61
|
+
function isLive(meta, lang) {
|
|
62
|
+
const flavor = getFlavorFromMeta(meta, lang);
|
|
63
|
+
|
|
64
|
+
if (!needsLive(lang, flavor)) return true;
|
|
65
|
+
if (!meta) return false;
|
|
66
|
+
|
|
67
|
+
return meta.includes('live');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
isPreview,
|
|
72
|
+
isBelow,
|
|
73
|
+
isLive,
|
|
74
|
+
needsLive,
|
|
75
|
+
allowedFormats,
|
|
76
|
+
getFlavorFromMeta,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('unified').Plugin} Plugin
|
|
3
|
+
*/
|
|
4
|
+
import { assert, isRecord } from '../utils.js';
|
|
5
|
+
import { buildCodeFenceMetaUtils } from './markdown/utils.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {unknown} [ options ]
|
|
9
|
+
* @returns {{ remarkPlugins: Plugin[], rehypePlugins: Plugin[] }}
|
|
10
|
+
*/
|
|
11
|
+
export function filterOptions(options) {
|
|
12
|
+
if (!isRecord(options)) {
|
|
13
|
+
return { remarkPlugins: [], rehypePlugins: [] };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
remarkPlugins: /** @type {Plugin[]}*/ (options?.remarkPlugins || []),
|
|
18
|
+
rehypePlugins: /** @type {Plugin[]}*/ (options?.rehypePlugins || []),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @type {import('../types.ts').CompilerConfig}
|
|
24
|
+
*/
|
|
25
|
+
export const md = {
|
|
26
|
+
codemirror: {
|
|
27
|
+
lang: async () => {
|
|
28
|
+
const { glimdown } = await import('codemirror-lang-glimdown');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* pre-configured markdown with GFM, and a few other extensions
|
|
32
|
+
*/
|
|
33
|
+
return glimdown();
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
compiler: async (config, api) => {
|
|
37
|
+
const { isLive, isPreview, needsLive, allowedFormats, isBelow, getFlavorFromMeta } =
|
|
38
|
+
buildCodeFenceMetaUtils(api);
|
|
39
|
+
|
|
40
|
+
// Try both possible structures
|
|
41
|
+
const userOptions = filterOptions(
|
|
42
|
+
/** @type {Record<string, unknown>} */ (config.userOptions)?.md || config
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// No recursing for now.
|
|
46
|
+
|
|
47
|
+
const { parseMarkdown } = await import(/* @vite-ignore */ './markdown/parse.js');
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @type {import('../types.ts').Compiler}
|
|
51
|
+
*/
|
|
52
|
+
return {
|
|
53
|
+
compile: async (text, options) => {
|
|
54
|
+
const compileOptions = filterOptions(options);
|
|
55
|
+
const result = await parseMarkdown(text, {
|
|
56
|
+
remarkPlugins: [...userOptions.remarkPlugins, ...compileOptions.remarkPlugins],
|
|
57
|
+
rehypePlugins: [...userOptions.rehypePlugins, ...compileOptions.rehypePlugins],
|
|
58
|
+
isLive,
|
|
59
|
+
isPreview,
|
|
60
|
+
isBelow,
|
|
61
|
+
needsLive,
|
|
62
|
+
ALLOWED_FORMATS: allowedFormats,
|
|
63
|
+
getFlavorFromMeta,
|
|
64
|
+
});
|
|
65
|
+
const escaped = result.text.replace(/`/g, '\\`');
|
|
66
|
+
|
|
67
|
+
return { compiled: `export default \`${escaped}\``, ...result };
|
|
68
|
+
},
|
|
69
|
+
render: async (element, compiled, extra, compiler) => {
|
|
70
|
+
element.innerHTML = /** @type {string} */ (compiled);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @type {(() => void)[]}
|
|
74
|
+
*/
|
|
75
|
+
const destroyables = [];
|
|
76
|
+
|
|
77
|
+
await Promise.all(
|
|
78
|
+
/** @type {unknown[]} */ (extra.codeBlocks).map(async (/** @type {unknown} */ info) => {
|
|
79
|
+
/** @type {Record<string, unknown>} */
|
|
80
|
+
const infoObj = /** @type {Record<string, unknown>} */ (info);
|
|
81
|
+
|
|
82
|
+
if (
|
|
83
|
+
!api.canCompile(
|
|
84
|
+
/** @type {string} */ (infoObj.format),
|
|
85
|
+
/** @type {string} */ (infoObj.flavor)
|
|
86
|
+
)
|
|
87
|
+
) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const flavor = /** @type {string} */ (infoObj.flavor);
|
|
92
|
+
const subRender = await compiler.compile(
|
|
93
|
+
/** @type {string} */ (infoObj.format),
|
|
94
|
+
/** @type {string} */ (infoObj.code),
|
|
95
|
+
{
|
|
96
|
+
...compiler.optionsFor(/** @type {string} */ (infoObj.format), flavor),
|
|
97
|
+
flavor: flavor,
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const selector = `#${/** @type {string} */ (infoObj.placeholderId)}`;
|
|
102
|
+
const target = element.querySelector(selector);
|
|
103
|
+
|
|
104
|
+
assert(
|
|
105
|
+
`Could not find placeholder / target element (using selector: \`${selector}\`). ` +
|
|
106
|
+
`Could not render ${/** @type {string} */ (infoObj.format)} block.`,
|
|
107
|
+
target
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
destroyables.push(subRender.destroy);
|
|
111
|
+
target.appendChild(subRender.element);
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
compiler.announce('info', 'Done');
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
for (const subDestroy of destroyables) {
|
|
119
|
+
subDestroy();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { esmsh } from '../cdn.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @type {import('../types.ts').CompilerConfig}
|
|
5
|
+
*/
|
|
6
|
+
export const mermaid = {
|
|
7
|
+
needsLiveMeta: false,
|
|
8
|
+
codemirror: {
|
|
9
|
+
lang: async () => {
|
|
10
|
+
const { mermaid } = await import('codemirror-lang-mermaid');
|
|
11
|
+
|
|
12
|
+
return mermaid();
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
compiler: async (config, api) => {
|
|
16
|
+
const versions = config.versions;
|
|
17
|
+
const { default: mermaid } = await api.tryResolve('mermaid', () => {
|
|
18
|
+
return esmsh.import(versions, 'mermaid');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
let id = 0;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
compile: async (text) => {
|
|
25
|
+
return `export default \`${text}\`;`;
|
|
26
|
+
},
|
|
27
|
+
render: async (element, text, _, compiler) => {
|
|
28
|
+
const { svg } = await mermaid.render('graphDiv' + id++, text);
|
|
29
|
+
|
|
30
|
+
element.innerHTML = svg;
|
|
31
|
+
compiler.announce('info', 'Done');
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @type {import('../types.ts').CompilerConfig}
|
|
3
|
+
*/
|
|
4
|
+
export const jsx = {
|
|
5
|
+
codemirror: {
|
|
6
|
+
lang: async () => {
|
|
7
|
+
const { javascript } = await import('@codemirror/lang-javascript');
|
|
8
|
+
|
|
9
|
+
return javascript({ jsx: true });
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
resolve: (id) => {
|
|
13
|
+
switch (id) {
|
|
14
|
+
case 'react':
|
|
15
|
+
return `https://esm.sh/react`;
|
|
16
|
+
case 'react-dom/client':
|
|
17
|
+
return `https://esm.sh/react-dom/client`;
|
|
18
|
+
case '@babel/standalone':
|
|
19
|
+
return `https://esm.sh/@babel/standalone`;
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
compiler: async (config, api) => {
|
|
23
|
+
const [reactDom, babel] = await api.tryResolveAll(['react-dom/client', '@babel/standalone']);
|
|
24
|
+
|
|
25
|
+
const { createRoot } = reactDom;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
async compile(text) {
|
|
29
|
+
const result = babel.transform(text, {
|
|
30
|
+
filename: `repl.js`,
|
|
31
|
+
presets: [babel.availablePresets.react],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return result.code;
|
|
35
|
+
},
|
|
36
|
+
async render(element, component) {
|
|
37
|
+
const root = createRoot(element);
|
|
38
|
+
|
|
39
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
40
|
+
root.render(component);
|
|
41
|
+
|
|
42
|
+
// Wait for react-dom to render
|
|
43
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|