astro-xmdx 0.0.2
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/index.ts +8 -0
- package/package.json +80 -0
- package/src/constants.ts +52 -0
- package/src/index.ts +150 -0
- package/src/pipeline/index.ts +38 -0
- package/src/pipeline/orchestrator.test.ts +324 -0
- package/src/pipeline/orchestrator.ts +121 -0
- package/src/pipeline/pipe.test.ts +251 -0
- package/src/pipeline/pipe.ts +70 -0
- package/src/pipeline/types.ts +59 -0
- package/src/plugins.test.ts +274 -0
- package/src/presets/index.ts +225 -0
- package/src/transforms/blocks-to-jsx.test.ts +590 -0
- package/src/transforms/blocks-to-jsx.ts +617 -0
- package/src/transforms/expressive-code.test.ts +274 -0
- package/src/transforms/expressive-code.ts +147 -0
- package/src/transforms/index.test.ts +143 -0
- package/src/transforms/index.ts +100 -0
- package/src/transforms/inject-components.test.ts +406 -0
- package/src/transforms/inject-components.ts +184 -0
- package/src/transforms/shiki.test.ts +289 -0
- package/src/transforms/shiki.ts +312 -0
- package/src/types.ts +92 -0
- package/src/utils/config.test.ts +252 -0
- package/src/utils/config.ts +146 -0
- package/src/utils/frontmatter.ts +33 -0
- package/src/utils/imports.test.ts +518 -0
- package/src/utils/imports.ts +201 -0
- package/src/utils/mdx-detection.test.ts +41 -0
- package/src/utils/mdx-detection.ts +209 -0
- package/src/utils/paths.test.ts +206 -0
- package/src/utils/paths.ts +92 -0
- package/src/utils/validation.test.ts +60 -0
- package/src/utils/validation.ts +15 -0
- package/src/vite-plugin/binding-loader.ts +81 -0
- package/src/vite-plugin/directive-rewriter.test.ts +331 -0
- package/src/vite-plugin/directive-rewriter.ts +272 -0
- package/src/vite-plugin/esbuild-pool.ts +173 -0
- package/src/vite-plugin/index.ts +37 -0
- package/src/vite-plugin/jsx-module.ts +106 -0
- package/src/vite-plugin/mdx-wrapper.ts +328 -0
- package/src/vite-plugin/normalize-config.test.ts +78 -0
- package/src/vite-plugin/normalize-config.ts +29 -0
- package/src/vite-plugin/shiki-highlighter.ts +46 -0
- package/src/vite-plugin/shiki-manager.test.ts +175 -0
- package/src/vite-plugin/shiki-manager.ts +53 -0
- package/src/vite-plugin/types.ts +189 -0
- package/src/vite-plugin.ts +1342 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test';
|
|
2
|
+
import { blocksToJsx, type Block } from './blocks-to-jsx.js';
|
|
3
|
+
import { createRegistry, starlightLibrary } from 'xmdx/registry';
|
|
4
|
+
|
|
5
|
+
describe('blocksToJsx', () => {
|
|
6
|
+
describe('user imports', () => {
|
|
7
|
+
it('should include user imports in output', () => {
|
|
8
|
+
const blocks: Block[] = [
|
|
9
|
+
{ type: 'html', content: '<p>Hello</p>' },
|
|
10
|
+
];
|
|
11
|
+
const userImports = ["import Card from '~/components/Card.astro';"];
|
|
12
|
+
|
|
13
|
+
const result = blocksToJsx(blocks, {}, [], null, undefined, userImports);
|
|
14
|
+
|
|
15
|
+
expect(result).toContain("import Card from '~/components/Card.astro';");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should skip registry imports for user-imported components', () => {
|
|
19
|
+
const registry = createRegistry([starlightLibrary]);
|
|
20
|
+
|
|
21
|
+
const blocks: Block[] = [
|
|
22
|
+
{ type: 'component', name: 'Card', props: {}, slotChildren: [{ type: 'html', content: '<p>Content</p>' }] },
|
|
23
|
+
];
|
|
24
|
+
const userImports = ["import Card from '~/components/Landing/Card.astro';"];
|
|
25
|
+
|
|
26
|
+
const result = blocksToJsx(blocks, {}, [], registry, undefined, userImports);
|
|
27
|
+
|
|
28
|
+
// Should include user import
|
|
29
|
+
expect(result).toContain("import Card from '~/components/Landing/Card.astro';");
|
|
30
|
+
// Should NOT include registry import for Card
|
|
31
|
+
expect(result).not.toContain('@astrojs/starlight/components');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should generate registry imports for non-user-imported components', () => {
|
|
35
|
+
const registry = createRegistry([starlightLibrary]);
|
|
36
|
+
|
|
37
|
+
const blocks: Block[] = [
|
|
38
|
+
{ type: 'component', name: 'Card', props: {}, slotChildren: [{ type: 'html', content: '<p>Card Content</p>' }] },
|
|
39
|
+
{ type: 'component', name: 'Aside', props: {}, slotChildren: [{ type: 'html', content: '<p>Aside Content</p>' }] },
|
|
40
|
+
];
|
|
41
|
+
// Only Card is user-imported
|
|
42
|
+
const userImports = ["import Card from '~/components/Card.astro';"];
|
|
43
|
+
|
|
44
|
+
const result = blocksToJsx(blocks, {}, [], registry, undefined, userImports);
|
|
45
|
+
|
|
46
|
+
// User import for Card
|
|
47
|
+
expect(result).toContain("import Card from '~/components/Card.astro';");
|
|
48
|
+
// Registry import for Aside (since it's not user-imported)
|
|
49
|
+
expect(result).toContain("import { Aside } from '@astrojs/starlight/components';");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should handle multiple user imports', () => {
|
|
53
|
+
const blocks: Block[] = [
|
|
54
|
+
{ type: 'component', name: 'Card', props: {} },
|
|
55
|
+
{ type: 'component', name: 'Button', props: {} },
|
|
56
|
+
];
|
|
57
|
+
const userImports = [
|
|
58
|
+
"import Card from '~/components/Card.astro';",
|
|
59
|
+
"import Button from '~/components/Button.astro';",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const result = blocksToJsx(blocks, {}, [], null, undefined, userImports);
|
|
63
|
+
|
|
64
|
+
expect(result).toContain("import Card from '~/components/Card.astro';");
|
|
65
|
+
expect(result).toContain("import Button from '~/components/Button.astro';");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should handle named imports in user imports', () => {
|
|
69
|
+
const registry = createRegistry([starlightLibrary]);
|
|
70
|
+
|
|
71
|
+
const blocks: Block[] = [
|
|
72
|
+
{ type: 'component', name: 'Aside', props: {} },
|
|
73
|
+
];
|
|
74
|
+
// User provides named import for Aside
|
|
75
|
+
const userImports = ["import { Aside } from './my-components';"];
|
|
76
|
+
|
|
77
|
+
const result = blocksToJsx(blocks, {}, [], registry, undefined, userImports);
|
|
78
|
+
|
|
79
|
+
// Should include user import
|
|
80
|
+
expect(result).toContain("import { Aside } from './my-components';");
|
|
81
|
+
// Should NOT include registry import for Aside
|
|
82
|
+
expect(result).not.toContain('@astrojs/starlight/components');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should handle aliased imports in user imports', () => {
|
|
86
|
+
const registry = createRegistry([starlightLibrary]);
|
|
87
|
+
|
|
88
|
+
const blocks: Block[] = [
|
|
89
|
+
{ type: 'component', name: 'MyCard', props: {} },
|
|
90
|
+
];
|
|
91
|
+
// User imports Card as MyCard
|
|
92
|
+
const userImports = ["import { Card as MyCard } from './my-components';"];
|
|
93
|
+
|
|
94
|
+
const result = blocksToJsx(blocks, {}, [], registry, undefined, userImports);
|
|
95
|
+
|
|
96
|
+
// Should include user import
|
|
97
|
+
expect(result).toContain("import { Card as MyCard } from './my-components';");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should default to empty user imports when not provided', () => {
|
|
101
|
+
const blocks: Block[] = [
|
|
102
|
+
{ type: 'html', content: '<p>Hello</p>' },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
// Call without userImports parameter
|
|
106
|
+
const result = blocksToJsx(blocks, {}, [], null, undefined);
|
|
107
|
+
|
|
108
|
+
// Should generate valid output without errors
|
|
109
|
+
expect(result).toContain('export const frontmatter');
|
|
110
|
+
expect(result).toContain('export default XmdxContent');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('basic functionality', () => {
|
|
115
|
+
it('should generate valid JSX module for HTML blocks', () => {
|
|
116
|
+
const blocks: Block[] = [
|
|
117
|
+
{ type: 'html', content: '<p>Hello World</p>' },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const result = blocksToJsx(blocks);
|
|
121
|
+
|
|
122
|
+
expect(result).toContain('export const frontmatter');
|
|
123
|
+
expect(result).toContain('export function getHeadings()');
|
|
124
|
+
expect(result).toContain('export const Content');
|
|
125
|
+
expect(result).toContain('export default XmdxContent');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should use set:html for HTML content', () => {
|
|
129
|
+
const blocks: Block[] = [
|
|
130
|
+
{ type: 'html', content: '<p>Test</p>' },
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
const result = blocksToJsx(blocks);
|
|
134
|
+
|
|
135
|
+
expect(result).toContain('set:html=');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should include runtime imports', () => {
|
|
139
|
+
const blocks: Block[] = [];
|
|
140
|
+
|
|
141
|
+
const result = blocksToJsx(blocks);
|
|
142
|
+
|
|
143
|
+
expect(result).toContain("import { createComponent, renderJSX } from 'astro/runtime/server/index.js';");
|
|
144
|
+
expect(result).toContain("import { Fragment, Fragment as _Fragment, jsx as _jsx } from 'astro/jsx-runtime';");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('nested components', () => {
|
|
149
|
+
it('should embed nested components directly without set:html', () => {
|
|
150
|
+
const blocks: Block[] = [
|
|
151
|
+
{
|
|
152
|
+
type: 'component',
|
|
153
|
+
name: 'CardGrid',
|
|
154
|
+
props: {},
|
|
155
|
+
// Use HTML-style attributes (what the Rust renderer produces)
|
|
156
|
+
slotChildren: [{ type: 'html', content: '<Card title="Getting Started">Content here</Card>' }],
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
const result = blocksToJsx(blocks);
|
|
161
|
+
|
|
162
|
+
// Should embed JSX directly, not use set:html
|
|
163
|
+
expect(result).toContain('<CardGrid><Card title="Getting Started">Content here</Card></CardGrid>');
|
|
164
|
+
expect(result).not.toContain('set:html={');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should use set:html for pure HTML slot content', () => {
|
|
168
|
+
const blocks: Block[] = [
|
|
169
|
+
{
|
|
170
|
+
type: 'component',
|
|
171
|
+
name: 'Card',
|
|
172
|
+
props: {},
|
|
173
|
+
slotChildren: [{ type: 'html', content: '<p>Hello <strong>world</strong></p>' }],
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const result = blocksToJsx(blocks);
|
|
178
|
+
|
|
179
|
+
// Should use set:html for HTML content
|
|
180
|
+
expect(result).toContain('set:html=');
|
|
181
|
+
expect(result).toContain('<Card><_Fragment set:html=');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should use set:html for uppercase HTML tags (not components)', () => {
|
|
185
|
+
// Uppercase HTML tags like <SVG>, <DIV> should NOT be treated as components
|
|
186
|
+
// Only true PascalCase (uppercase followed by lowercase) should be components
|
|
187
|
+
const blocks: Block[] = [
|
|
188
|
+
{
|
|
189
|
+
type: 'component',
|
|
190
|
+
name: 'Container',
|
|
191
|
+
props: {},
|
|
192
|
+
slotChildren: [{ type: 'html', content: '<SVG><path d="M0 0h24v24H0z"/></SVG>' }],
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
const result = blocksToJsx(blocks);
|
|
197
|
+
|
|
198
|
+
// Should use set:html because <SVG> is not a PascalCase component
|
|
199
|
+
expect(result).toContain('set:html=');
|
|
200
|
+
expect(result).toContain('<Container><_Fragment set:html=');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should detect acronym-prefixed PascalCase components like MDXProvider', () => {
|
|
204
|
+
// Components that start with acronyms like MDX, URL, API should be detected
|
|
205
|
+
const blocks: Block[] = [
|
|
206
|
+
{
|
|
207
|
+
type: 'component',
|
|
208
|
+
name: 'Container',
|
|
209
|
+
props: {},
|
|
210
|
+
slotChildren: [{ type: 'html', content: '<MDXProvider>content</MDXProvider>' }],
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const result = blocksToJsx(blocks);
|
|
215
|
+
|
|
216
|
+
// Should embed JSX directly, not use set:html
|
|
217
|
+
expect(result).toContain('<Container><MDXProvider>content</MDXProvider></Container>');
|
|
218
|
+
expect(result).not.toContain('set:html={');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should detect URLTable and other acronym-prefixed components', () => {
|
|
222
|
+
const blocks: Block[] = [
|
|
223
|
+
{
|
|
224
|
+
type: 'component',
|
|
225
|
+
name: 'Section',
|
|
226
|
+
props: {},
|
|
227
|
+
slotChildren: [{ type: 'html', content: '<URLTable /><APIClient>data</APIClient>' }],
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const result = blocksToJsx(blocks);
|
|
232
|
+
|
|
233
|
+
// Should embed JSX directly because these are PascalCase components
|
|
234
|
+
expect(result).toContain('<Section><URLTable /><APIClient>data</APIClient></Section>');
|
|
235
|
+
expect(result).not.toContain('set:html={');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should still use set:html for all-uppercase tags like HTML, DIV', () => {
|
|
239
|
+
const blocks: Block[] = [
|
|
240
|
+
{
|
|
241
|
+
type: 'component',
|
|
242
|
+
name: 'Container',
|
|
243
|
+
props: {},
|
|
244
|
+
slotChildren: [{ type: 'html', content: '<DIV>content</DIV><HTML><BODY></BODY></HTML>' }],
|
|
245
|
+
},
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
const result = blocksToJsx(blocks);
|
|
249
|
+
|
|
250
|
+
// All-uppercase should use set:html path
|
|
251
|
+
expect(result).toContain('set:html=');
|
|
252
|
+
expect(result).toContain('<Container><_Fragment set:html=');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should handle multiple nested components', () => {
|
|
256
|
+
const blocks: Block[] = [
|
|
257
|
+
{
|
|
258
|
+
type: 'component',
|
|
259
|
+
name: 'CardGrid',
|
|
260
|
+
props: {},
|
|
261
|
+
// Use HTML-style attributes (what the Rust renderer produces)
|
|
262
|
+
slotChildren: [{ type: 'html', content: '<Card title="First">First card</Card><Card title="Second">Second card</Card>' }],
|
|
263
|
+
},
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
const result = blocksToJsx(blocks);
|
|
267
|
+
|
|
268
|
+
// Should embed JSX directly
|
|
269
|
+
expect(result).toContain('<CardGrid><Card title="First">First card</Card><Card title="Second">Second card</Card></CardGrid>');
|
|
270
|
+
expect(result).not.toContain('set:html={');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should handle self-closing nested components', () => {
|
|
274
|
+
const blocks: Block[] = [
|
|
275
|
+
{
|
|
276
|
+
type: 'component',
|
|
277
|
+
name: 'Container',
|
|
278
|
+
props: {},
|
|
279
|
+
slotChildren: [{ type: 'html', content: '<Icon name="star" />' }],
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
const result = blocksToJsx(blocks);
|
|
284
|
+
|
|
285
|
+
// Should embed JSX directly for self-closing component
|
|
286
|
+
expect(result).toContain('<Container><Icon name="star" /></Container>');
|
|
287
|
+
expect(result).not.toContain('set:html={');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should handle mixed HTML and component content', () => {
|
|
291
|
+
const blocks: Block[] = [
|
|
292
|
+
{
|
|
293
|
+
type: 'component',
|
|
294
|
+
name: 'Section',
|
|
295
|
+
props: {},
|
|
296
|
+
slotChildren: [{ type: 'html', content: '<p>Intro text</p><Card>Content</Card><p>More text</p>' }],
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
const result = blocksToJsx(blocks);
|
|
301
|
+
|
|
302
|
+
// Should embed JSX directly because it contains a component
|
|
303
|
+
expect(result).toContain('<Section><p>Intro text</p><Card>Content</Card><p>More text</p></Section>');
|
|
304
|
+
expect(result).not.toContain('set:html={');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should self-close void HTML tags when embedding slot content', () => {
|
|
308
|
+
const blocks: Block[] = [
|
|
309
|
+
{
|
|
310
|
+
type: 'component',
|
|
311
|
+
name: 'Section',
|
|
312
|
+
props: {},
|
|
313
|
+
slotChildren: [{ type: 'html', content: '<Card>Content</Card><img src="/img.png">' }],
|
|
314
|
+
},
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
const result = blocksToJsx(blocks);
|
|
318
|
+
|
|
319
|
+
expect(result).toContain('<Section><Card>Content</Card><img src="/img.png" /></Section>');
|
|
320
|
+
expect(result).not.toContain('<img src="/img.png">');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should convert HTML entities to JSX expressions in nested component content', () => {
|
|
324
|
+
const blocks: Block[] = [
|
|
325
|
+
{
|
|
326
|
+
type: 'component',
|
|
327
|
+
name: 'Card',
|
|
328
|
+
props: {},
|
|
329
|
+
// HTML entities that would appear literally in JSX
|
|
330
|
+
slotChildren: [{ type: 'html', content: '<Badge>a < b && c</Badge>' }],
|
|
331
|
+
},
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
const result = blocksToJsx(blocks);
|
|
335
|
+
|
|
336
|
+
// Entities should become JSX expressions: < becomes {"<"}, & becomes {"&"}
|
|
337
|
+
expect(result).toContain('<Card><Badge>a {"<"} b {"&"}{"&"} c</Badge></Card>');
|
|
338
|
+
// Should NOT contain the encoded entities
|
|
339
|
+
expect(result).not.toContain('<');
|
|
340
|
+
expect(result).not.toContain('&');
|
|
341
|
+
// Should NOT decode to raw characters (that would break JSX)
|
|
342
|
+
expect(result).not.toContain('<Card><Badge>a < b && c</Badge></Card>');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should convert literal ampersands to JSX expressions in nested component content', () => {
|
|
346
|
+
const blocks: Block[] = [
|
|
347
|
+
{
|
|
348
|
+
type: 'component',
|
|
349
|
+
name: 'Card',
|
|
350
|
+
props: {},
|
|
351
|
+
// Literal & character (not encoded as entity)
|
|
352
|
+
slotChildren: [{ type: 'html', content: '<Badge>Languages & Frameworks</Badge>' }],
|
|
353
|
+
},
|
|
354
|
+
];
|
|
355
|
+
|
|
356
|
+
const result = blocksToJsx(blocks);
|
|
357
|
+
|
|
358
|
+
// Literal & should become JSX expression
|
|
359
|
+
expect(result).toContain('Languages {"&"} Frameworks');
|
|
360
|
+
// Should NOT contain raw & (that would break JSX)
|
|
361
|
+
expect(result).not.toContain('Languages & Frameworks');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should preserve unknown HTML entities in nested component content', () => {
|
|
365
|
+
const blocks: Block[] = [
|
|
366
|
+
{
|
|
367
|
+
type: 'component',
|
|
368
|
+
name: 'Card',
|
|
369
|
+
props: {},
|
|
370
|
+
// Unknown entity like should be preserved
|
|
371
|
+
slotChildren: [{ type: 'html', content: '<Badge>Hello World</Badge>' }],
|
|
372
|
+
},
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
const result = blocksToJsx(blocks);
|
|
376
|
+
|
|
377
|
+
// Unknown entities should be left as-is
|
|
378
|
+
expect(result).toContain(' ');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should preserve valid JSX expressions in nested components', () => {
|
|
382
|
+
const blocks: Block[] = [
|
|
383
|
+
{
|
|
384
|
+
type: 'component',
|
|
385
|
+
name: 'CardGrid',
|
|
386
|
+
props: {},
|
|
387
|
+
// Valid JSX expression that should NOT be escaped
|
|
388
|
+
slotChildren: [{
|
|
389
|
+
type: 'component',
|
|
390
|
+
name: 'Card',
|
|
391
|
+
props: { title: { type: 'expression', value: 'title' } },
|
|
392
|
+
slotChildren: [{ type: 'html', content: 'Content' }],
|
|
393
|
+
}],
|
|
394
|
+
},
|
|
395
|
+
];
|
|
396
|
+
|
|
397
|
+
const result = blocksToJsx(blocks);
|
|
398
|
+
|
|
399
|
+
// JSX expressions should be preserved, not escaped
|
|
400
|
+
expect(result).toContain('title={title}');
|
|
401
|
+
expect(result).not.toContain("{'{'}");
|
|
402
|
+
expect(result).toContain('<CardGrid><Card title={title}>Content</Card></CardGrid>');
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
describe('code blocks', () => {
|
|
407
|
+
it('should render standalone code block as <pre><code> in set:html Fragment', () => {
|
|
408
|
+
const blocks: Block[] = [
|
|
409
|
+
{ type: 'code', code: 'console.log("hello")' },
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
const result = blocksToJsx(blocks);
|
|
413
|
+
|
|
414
|
+
expect(result).toContain('<_Fragment set:html=');
|
|
415
|
+
expect(result).toContain('astro-code');
|
|
416
|
+
expect(result).toContain('console.log');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should include language class when lang is set', () => {
|
|
420
|
+
const blocks: Block[] = [
|
|
421
|
+
{ type: 'code', code: 'const x = 1;', lang: 'js' },
|
|
422
|
+
];
|
|
423
|
+
|
|
424
|
+
const result = blocksToJsx(blocks);
|
|
425
|
+
|
|
426
|
+
expect(result).toContain('language-js');
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should escape special characters in code content', () => {
|
|
430
|
+
const blocks: Block[] = [
|
|
431
|
+
{ type: 'code', code: 'if (a < b && c > d) { run(); }', lang: 'js' },
|
|
432
|
+
];
|
|
433
|
+
|
|
434
|
+
const result = blocksToJsx(blocks);
|
|
435
|
+
|
|
436
|
+
expect(result).toContain('<');
|
|
437
|
+
expect(result).toContain('&');
|
|
438
|
+
expect(result).toContain('{');
|
|
439
|
+
expect(result).toContain('}');
|
|
440
|
+
// Raw chars should not appear unescaped in the HTML
|
|
441
|
+
expect(result).not.toContain('"if (a < b');
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('should render code block in component slot via slotChildrenToHtml', () => {
|
|
445
|
+
const blocks: Block[] = [
|
|
446
|
+
{
|
|
447
|
+
type: 'component',
|
|
448
|
+
name: 'Card',
|
|
449
|
+
props: {},
|
|
450
|
+
slotChildren: [
|
|
451
|
+
{ type: 'html', content: '<p>Intro</p>' },
|
|
452
|
+
{ type: 'code', code: 'let x = 1;', lang: 'ts' },
|
|
453
|
+
],
|
|
454
|
+
},
|
|
455
|
+
];
|
|
456
|
+
|
|
457
|
+
const result = blocksToJsx(blocks);
|
|
458
|
+
|
|
459
|
+
expect(result).toContain('astro-code');
|
|
460
|
+
expect(result).toContain('language-ts');
|
|
461
|
+
expect(result).toContain('let x = 1;');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should always render code blocks as <pre><code> (EC rewriting is pipeline-only)', () => {
|
|
465
|
+
const blocks: Block[] = [
|
|
466
|
+
{ type: 'code', code: 'const x = 1;', lang: 'js', meta: 'title="example"' },
|
|
467
|
+
];
|
|
468
|
+
|
|
469
|
+
const result = blocksToJsx(blocks);
|
|
470
|
+
|
|
471
|
+
expect(result).toContain('astro-code');
|
|
472
|
+
expect(result).toContain('language-js');
|
|
473
|
+
expect(result).toContain('const x = 1;');
|
|
474
|
+
expect(result).not.toContain('<Code code=');
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('should render code blocks as <pre><code> in slots too', () => {
|
|
478
|
+
const blocks: Block[] = [
|
|
479
|
+
{
|
|
480
|
+
type: 'component',
|
|
481
|
+
name: 'Card',
|
|
482
|
+
props: {},
|
|
483
|
+
slotChildren: [
|
|
484
|
+
{ type: 'code', code: 'hello()', lang: 'py' },
|
|
485
|
+
],
|
|
486
|
+
},
|
|
487
|
+
];
|
|
488
|
+
|
|
489
|
+
const result = blocksToJsx(blocks);
|
|
490
|
+
|
|
491
|
+
expect(result).toContain('astro-code');
|
|
492
|
+
expect(result).toContain('language-py');
|
|
493
|
+
expect(result).toContain('hello()');
|
|
494
|
+
expect(result).not.toContain('<Code code=');
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('should handle mixed code and HTML in slot', () => {
|
|
498
|
+
const blocks: Block[] = [
|
|
499
|
+
{
|
|
500
|
+
type: 'component',
|
|
501
|
+
name: 'Section',
|
|
502
|
+
props: {},
|
|
503
|
+
slotChildren: [
|
|
504
|
+
{ type: 'html', content: '<p>Before code</p>' },
|
|
505
|
+
{ type: 'code', code: 'fn main() {}', lang: 'rust' },
|
|
506
|
+
{ type: 'html', content: '<p>After code</p>' },
|
|
507
|
+
],
|
|
508
|
+
},
|
|
509
|
+
];
|
|
510
|
+
|
|
511
|
+
const result = blocksToJsx(blocks);
|
|
512
|
+
|
|
513
|
+
expect(result).toContain('<p>Before code</p>');
|
|
514
|
+
expect(result).toContain('astro-code');
|
|
515
|
+
expect(result).toContain('language-rust');
|
|
516
|
+
expect(result).toContain('After code');
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
describe('Fragment slot stripping', () => {
|
|
521
|
+
it('should strip <p> wrapper from Fragment with slot attribute', () => {
|
|
522
|
+
const blocks: Block[] = [
|
|
523
|
+
{
|
|
524
|
+
type: 'component',
|
|
525
|
+
name: 'IslandsDiagram',
|
|
526
|
+
props: {},
|
|
527
|
+
slotChildren: [{ type: 'html', content: '<p><Fragment slot="headerApp">Header text</Fragment></p>' }],
|
|
528
|
+
},
|
|
529
|
+
];
|
|
530
|
+
|
|
531
|
+
const result = blocksToJsx(blocks);
|
|
532
|
+
|
|
533
|
+
// Should NOT contain the <p> wrapper
|
|
534
|
+
expect(result).not.toContain('<p><Fragment slot=');
|
|
535
|
+
// Should contain the Fragment with slot directly
|
|
536
|
+
expect(result).toContain('<Fragment slot="headerApp">');
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('should strip multiple <p> wrappers from Fragment slots', () => {
|
|
540
|
+
const blocks: Block[] = [
|
|
541
|
+
{
|
|
542
|
+
type: 'component',
|
|
543
|
+
name: 'Container',
|
|
544
|
+
props: {},
|
|
545
|
+
slotChildren: [{ type: 'html', content: '<p><Fragment slot="header">Header</Fragment></p><p><Fragment slot="footer">Footer</Fragment></p>' }],
|
|
546
|
+
},
|
|
547
|
+
];
|
|
548
|
+
|
|
549
|
+
const result = blocksToJsx(blocks);
|
|
550
|
+
|
|
551
|
+
// Should NOT contain any <p><Fragment patterns
|
|
552
|
+
expect(result).not.toContain('<p><Fragment slot=');
|
|
553
|
+
// Should contain both Fragment slots
|
|
554
|
+
expect(result).toContain('<Fragment slot="header">Header</Fragment>');
|
|
555
|
+
expect(result).toContain('<Fragment slot="footer">Footer</Fragment>');
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('should preserve regular paragraphs', () => {
|
|
559
|
+
const blocks: Block[] = [
|
|
560
|
+
{
|
|
561
|
+
type: 'component',
|
|
562
|
+
name: 'Card',
|
|
563
|
+
props: {},
|
|
564
|
+
slotChildren: [{ type: 'html', content: '<p>Regular paragraph content</p>' }],
|
|
565
|
+
},
|
|
566
|
+
];
|
|
567
|
+
|
|
568
|
+
const result = blocksToJsx(blocks);
|
|
569
|
+
|
|
570
|
+
// Regular paragraphs should be preserved
|
|
571
|
+
expect(result).toContain('<p>Regular paragraph content</p>');
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('should preserve Fragment without slot attribute', () => {
|
|
575
|
+
const blocks: Block[] = [
|
|
576
|
+
{
|
|
577
|
+
type: 'component',
|
|
578
|
+
name: 'Wrapper',
|
|
579
|
+
props: {},
|
|
580
|
+
slotChildren: [{ type: 'html', content: '<p><Fragment>Content without slot</Fragment></p>' }],
|
|
581
|
+
},
|
|
582
|
+
];
|
|
583
|
+
|
|
584
|
+
const result = blocksToJsx(blocks);
|
|
585
|
+
|
|
586
|
+
// Fragment without slot= should NOT be stripped
|
|
587
|
+
expect(result).toContain('<p><Fragment>Content without slot</Fragment></p>');
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
});
|