@ox-content/vite-plugin-svelte 3.0.0-alpha.9 → 3.0.0-beta.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/index.cjs +616 -15
- package/dist/index.d.cts +28 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +28 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +616 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/dist/index.mjs
CHANGED
|
@@ -7,6 +7,14 @@ const COMPONENT_REGEX = /<([A-Z][a-zA-Z0-9]*)\s*([^>]*?)\s*(?:\/>|>([\s\S]*?)<\/
|
|
|
7
7
|
const PROP_REGEX = /([a-zA-Z0-9-]+)(?:=(?:"([^"]*)"|'([^']*)'|{([^}]*)}|\[([^\]]*)\]))?/g;
|
|
8
8
|
const ISLAND_MARKER_PREFIX = "OXCONTENT-ISLAND-";
|
|
9
9
|
const ISLAND_MARKER_SUFFIX = "-PLACEHOLDER";
|
|
10
|
+
const DOCUMENT_PROP_MARKER_PREFIX = "OXCONTENT-DOCUMENT-PROP-";
|
|
11
|
+
const DOCUMENT_PROP_MARKER_SUFFIX = "-PLACEHOLDER";
|
|
12
|
+
const PAYLOAD_SCRIPT = /^\s*<script type="application\/json">[\s\S]*?<\/script>/i;
|
|
13
|
+
const RUST_PAYLOAD_KEYS = /* @__PURE__ */ new Set([
|
|
14
|
+
"props",
|
|
15
|
+
"expressions",
|
|
16
|
+
"spreads"
|
|
17
|
+
]);
|
|
10
18
|
async function transformMarkdownWithSvelte(code, id, options) {
|
|
11
19
|
const components = options.components;
|
|
12
20
|
const { content: markdownContent, frontmatter } = extractFrontmatter(code);
|
|
@@ -66,7 +74,11 @@ async function transformMarkdownWithSvelte(code, id, options) {
|
|
|
66
74
|
i18n: false
|
|
67
75
|
};
|
|
68
76
|
if (mdx) {
|
|
69
|
-
const
|
|
77
|
+
const documentExpressions = options.mdxDocumentProps ? prepareMdxDocumentExpressions(markdownContent, id) : {
|
|
78
|
+
content: markdownContent,
|
|
79
|
+
expressions: []
|
|
80
|
+
};
|
|
81
|
+
const transformed = await transformMarkdown(documentExpressions.content, id, baseOptions);
|
|
70
82
|
const discovered = await discoverDocumentMdxIslands({
|
|
71
83
|
source: markdownContent,
|
|
72
84
|
html: transformed.html,
|
|
@@ -79,7 +91,8 @@ async function transformMarkdownWithSvelte(code, id, options) {
|
|
|
79
91
|
}),
|
|
80
92
|
srcDir: options.srcDir
|
|
81
93
|
});
|
|
82
|
-
|
|
94
|
+
if (options.mdxDocumentProps) return compileSvelteResult(generateMdxDocumentPropsSvelteModule(transformed.html, discovered.usedComponents, frontmatter, options, id, discovered.localBindings, documentExpressions.expressions), id, discovered.usedComponents, frontmatter, options.ssr);
|
|
95
|
+
return compileSvelteResult(generateSvelteModule(options.renderIsland ? await applyIslandSsrHtml(transformed.html, options.renderIsland, id, discovered.usedComponents) : transformed.html, discovered.usedComponents, discovered.usedComponents, frontmatter, options, id, discovered.localBindings), id, discovered.usedComponents, frontmatter, options.ssr);
|
|
83
96
|
}
|
|
84
97
|
const usedComponents = [];
|
|
85
98
|
const islands = [];
|
|
@@ -113,13 +126,13 @@ async function transformMarkdownWithSvelte(code, id, options) {
|
|
|
113
126
|
lastIndex = matchEnd;
|
|
114
127
|
}
|
|
115
128
|
processedContent += markdownContent.slice(lastIndex);
|
|
116
|
-
return compileSvelteResult(generateSvelteModule(injectIslandMarkers((await transformMarkdown(processedContent, id, baseOptions)).html, islands), usedComponents, islands, frontmatter, options, id), id, usedComponents, frontmatter);
|
|
129
|
+
return compileSvelteResult(generateSvelteModule(injectIslandMarkers((await transformMarkdown(processedContent, id, baseOptions)).html, islands), usedComponents, islands, frontmatter, options, id), id, usedComponents, frontmatter, options.ssr);
|
|
117
130
|
}
|
|
118
|
-
function compileSvelteResult(svelteCode, id, usedComponents, frontmatter) {
|
|
131
|
+
function compileSvelteResult(svelteCode, id, usedComponents, frontmatter, ssr = false) {
|
|
119
132
|
return {
|
|
120
133
|
code: `${compile(svelteCode, {
|
|
121
134
|
filename: id,
|
|
122
|
-
generate: "client",
|
|
135
|
+
generate: ssr ? "server" : "client",
|
|
123
136
|
runes: true
|
|
124
137
|
}).js.code}\nexport const frontmatter = ${JSON.stringify(frontmatter)};`,
|
|
125
138
|
map: null,
|
|
@@ -233,6 +246,588 @@ function parseProps(propsString) {
|
|
|
233
246
|
}
|
|
234
247
|
return props;
|
|
235
248
|
}
|
|
249
|
+
/** Opt a document-props component into the island contract. */
|
|
250
|
+
const MDX_ISLAND_DIRECTIVE = "oxIsland";
|
|
251
|
+
const MDX_ISLAND_MEDIA_DIRECTIVE = "oxIslandMedia";
|
|
252
|
+
const MDX_ISLAND_LOAD_STRATEGIES = /* @__PURE__ */ new Set([
|
|
253
|
+
"eager",
|
|
254
|
+
"idle",
|
|
255
|
+
"visible",
|
|
256
|
+
"media"
|
|
257
|
+
]);
|
|
258
|
+
function prepareMdxDocumentExpressions(content, filePath) {
|
|
259
|
+
const skipRanges = mergeRanges([
|
|
260
|
+
...collectFenceRanges(content),
|
|
261
|
+
...collectInlineCodeRanges(content),
|
|
262
|
+
...collectMdxEsmLineRanges(content)
|
|
263
|
+
]);
|
|
264
|
+
const expressions = [];
|
|
265
|
+
let output = "";
|
|
266
|
+
let cursor = 0;
|
|
267
|
+
let rangeIndex = 0;
|
|
268
|
+
let inTag = false;
|
|
269
|
+
let quote = null;
|
|
270
|
+
while (cursor < content.length) {
|
|
271
|
+
const range = skipRanges[rangeIndex];
|
|
272
|
+
if (range && cursor >= range.end) {
|
|
273
|
+
rangeIndex += 1;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (range && cursor === range.start) {
|
|
277
|
+
output += content.slice(range.start, range.end);
|
|
278
|
+
cursor = range.end;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const char = content[cursor];
|
|
282
|
+
if (inTag) {
|
|
283
|
+
output += char;
|
|
284
|
+
if (quote) {
|
|
285
|
+
if (char === quote && content[cursor - 1] !== "\\") quote = null;
|
|
286
|
+
} else if (char === "\"" || char === "'") quote = char;
|
|
287
|
+
else if (char === ">") inTag = false;
|
|
288
|
+
cursor += 1;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (char === "<" && startsHtmlLikeTag(content, cursor)) {
|
|
292
|
+
inTag = true;
|
|
293
|
+
output += char;
|
|
294
|
+
cursor += 1;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (char === "{" && content[cursor - 1] !== "\\") {
|
|
298
|
+
const end = findMdxExpressionEnd(content, cursor + 1);
|
|
299
|
+
if (end !== -1) {
|
|
300
|
+
const expression = content.slice(cursor + 1, end).trim();
|
|
301
|
+
const path = parseDocumentPropPath(expression);
|
|
302
|
+
if (!path) throw new Error(`[ox-content-svelte] Unsupported MDX document prop expression "{${expression}}" in ${filePath}. Only identifiers and dotted property paths are supported.`);
|
|
303
|
+
const marker = `${DOCUMENT_PROP_MARKER_PREFIX}${expressions.length}${DOCUMENT_PROP_MARKER_SUFFIX}`;
|
|
304
|
+
expressions.push({
|
|
305
|
+
marker,
|
|
306
|
+
expression,
|
|
307
|
+
path
|
|
308
|
+
});
|
|
309
|
+
output += marker;
|
|
310
|
+
cursor = end + 1;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
output += char;
|
|
315
|
+
cursor += 1;
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
content: output,
|
|
319
|
+
expressions
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function collectInlineCodeRanges(content) {
|
|
323
|
+
const ranges = [];
|
|
324
|
+
const fenceRanges = collectFenceRanges(content);
|
|
325
|
+
let lineStart = 0;
|
|
326
|
+
while (lineStart < content.length) {
|
|
327
|
+
const lineEnd = content.indexOf("\n", lineStart);
|
|
328
|
+
const end = lineEnd === -1 ? content.length : lineEnd;
|
|
329
|
+
if (!isInRanges(lineStart, end, fenceRanges)) {
|
|
330
|
+
let cursor = lineStart;
|
|
331
|
+
while (cursor < end) {
|
|
332
|
+
const marker = matchBacktickRun(content, cursor);
|
|
333
|
+
if (!marker) {
|
|
334
|
+
cursor += 1;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const close = content.indexOf(marker, cursor + marker.length);
|
|
338
|
+
if (close === -1 || close >= end) {
|
|
339
|
+
cursor += marker.length;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
ranges.push({
|
|
343
|
+
start: cursor,
|
|
344
|
+
end: close + marker.length
|
|
345
|
+
});
|
|
346
|
+
cursor = close + marker.length;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
lineStart = lineEnd === -1 ? content.length : lineEnd + 1;
|
|
350
|
+
}
|
|
351
|
+
return ranges;
|
|
352
|
+
}
|
|
353
|
+
function collectMdxEsmLineRanges(content) {
|
|
354
|
+
const ranges = [];
|
|
355
|
+
const fenceRanges = collectFenceRanges(content);
|
|
356
|
+
let lineStart = 0;
|
|
357
|
+
while (lineStart < content.length) {
|
|
358
|
+
const lineEnd = content.indexOf("\n", lineStart);
|
|
359
|
+
const end = lineEnd === -1 ? content.length : lineEnd + 1;
|
|
360
|
+
const contentEnd = lineEnd === -1 ? content.length : lineEnd;
|
|
361
|
+
if (!isInRanges(lineStart, contentEnd, fenceRanges)) {
|
|
362
|
+
const line = content.slice(lineStart, contentEnd).trimStart();
|
|
363
|
+
if (line.startsWith("import ") || line.startsWith("export ")) ranges.push({
|
|
364
|
+
start: lineStart,
|
|
365
|
+
end
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
lineStart = lineEnd === -1 ? content.length : lineEnd + 1;
|
|
369
|
+
}
|
|
370
|
+
return ranges;
|
|
371
|
+
}
|
|
372
|
+
function mergeRanges(ranges) {
|
|
373
|
+
const sorted = ranges.filter((range) => range.end > range.start).sort((left, right) => left.start - right.start || left.end - right.end);
|
|
374
|
+
const merged = [];
|
|
375
|
+
for (const range of sorted) {
|
|
376
|
+
const previous = merged.at(-1);
|
|
377
|
+
if (previous && range.start <= previous.end) previous.end = Math.max(previous.end, range.end);
|
|
378
|
+
else merged.push({ ...range });
|
|
379
|
+
}
|
|
380
|
+
return merged;
|
|
381
|
+
}
|
|
382
|
+
function matchBacktickRun(content, index) {
|
|
383
|
+
if (content[index] !== "`") return null;
|
|
384
|
+
let end = index + 1;
|
|
385
|
+
while (content[end] === "`") end += 1;
|
|
386
|
+
return content.slice(index, end);
|
|
387
|
+
}
|
|
388
|
+
function startsHtmlLikeTag(content, index) {
|
|
389
|
+
const next = content[index + 1];
|
|
390
|
+
return next === "/" || next === "!" || next === "?" || /[A-Za-z]/.test(next ?? "");
|
|
391
|
+
}
|
|
392
|
+
function findMdxExpressionEnd(content, start) {
|
|
393
|
+
let depth = 1;
|
|
394
|
+
let quote = null;
|
|
395
|
+
let escaped = false;
|
|
396
|
+
for (let index = start; index < content.length; index += 1) {
|
|
397
|
+
const char = content[index];
|
|
398
|
+
if (quote) {
|
|
399
|
+
if (escaped) escaped = false;
|
|
400
|
+
else if (char === "\\") escaped = true;
|
|
401
|
+
else if (char === quote) quote = null;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
405
|
+
quote = char;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (char === "{") {
|
|
409
|
+
depth += 1;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (char === "}") {
|
|
413
|
+
depth -= 1;
|
|
414
|
+
if (depth === 0) return index;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return -1;
|
|
418
|
+
}
|
|
419
|
+
function parseDocumentPropPath(expression) {
|
|
420
|
+
if (!/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(expression) || RESERVED_DOCUMENT_PROP_WORDS.has(expression)) return null;
|
|
421
|
+
return expression.split(".");
|
|
422
|
+
}
|
|
423
|
+
const RESERVED_DOCUMENT_PROP_WORDS = /* @__PURE__ */ new Set([
|
|
424
|
+
"false",
|
|
425
|
+
"Infinity",
|
|
426
|
+
"NaN",
|
|
427
|
+
"null",
|
|
428
|
+
"this",
|
|
429
|
+
"true",
|
|
430
|
+
"undefined"
|
|
431
|
+
]);
|
|
432
|
+
function generateMdxDocumentPropsSvelteModule(html, usedComponents, frontmatter, options, id, localBindings, documentExpressions) {
|
|
433
|
+
const filePathLiteral = JSON.stringify(id);
|
|
434
|
+
const imports = renderIslandComponentImports(usedComponents, {
|
|
435
|
+
globalComponents: options.components,
|
|
436
|
+
localBindings,
|
|
437
|
+
documentPath: id,
|
|
438
|
+
root: options.root
|
|
439
|
+
});
|
|
440
|
+
const { template, hydratedIslands } = renderMdxDocumentTemplate(html, usedComponents, id, documentExpressions);
|
|
441
|
+
return `${renderMdxIslandModuleScript(hydratedIslands, imports)}
|
|
442
|
+
<script>
|
|
443
|
+
${hydratedIslands.length > 0 ? "" : imports}
|
|
444
|
+
|
|
445
|
+
const frontmatter = ${JSON.stringify(frontmatter)};
|
|
446
|
+
export { frontmatter };
|
|
447
|
+
|
|
448
|
+
let __ox_mdx_props = $props();
|
|
449
|
+
${hydratedIslands.length > 0 ? renderMdxIslandPropsSerializer(filePathLiteral) : ""}
|
|
450
|
+
|
|
451
|
+
function __ox_mdx_document_prop(props, path, expression) {
|
|
452
|
+
const propName = path.join(".");
|
|
453
|
+
let value = props;
|
|
454
|
+
for (const segment of path) {
|
|
455
|
+
if (
|
|
456
|
+
value == null ||
|
|
457
|
+
(typeof value !== "object" && typeof value !== "function") ||
|
|
458
|
+
!(segment in Object(value))
|
|
459
|
+
) {
|
|
460
|
+
throw new Error('[ox-content-svelte] Missing MDX document prop "' + propName + '" in ' + ${filePathLiteral} + ' for expression {' + expression + '}.');
|
|
461
|
+
}
|
|
462
|
+
value = value[segment];
|
|
463
|
+
}
|
|
464
|
+
if (value === undefined) {
|
|
465
|
+
throw new Error('[ox-content-svelte] Missing MDX document prop "' + propName + '" in ' + ${filePathLiteral} + ' for expression {' + expression + '}.');
|
|
466
|
+
}
|
|
467
|
+
return value;
|
|
468
|
+
}
|
|
469
|
+
<\/script>
|
|
470
|
+
|
|
471
|
+
<div class="ox-content">${template}</div>
|
|
472
|
+
|
|
473
|
+
<style>
|
|
474
|
+
.ox-content {
|
|
475
|
+
line-height: 1.6;
|
|
476
|
+
}
|
|
477
|
+
</style>
|
|
478
|
+
`;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Island wiring for a document-props page.
|
|
482
|
+
*
|
|
483
|
+
* This host never hydrates the whole page — that is the point of the mode — so
|
|
484
|
+
* the runtime cannot be started from `onMount`. It is exported instead, and the
|
|
485
|
+
* host calls it once on the client. Pages with no islands get no module script
|
|
486
|
+
* at all and stay zero-JavaScript.
|
|
487
|
+
*/
|
|
488
|
+
function renderMdxIslandModuleScript(hydratedIslands, imports) {
|
|
489
|
+
if (hydratedIslands.length === 0) return "";
|
|
490
|
+
return `
|
|
491
|
+
<script module>
|
|
492
|
+
import { createRawSnippet, hydrate, mount, unmount } from 'svelte';
|
|
493
|
+
import { initIslands, readIslandSlotHtml } from '@ox-content/islands';
|
|
494
|
+
${imports}
|
|
495
|
+
|
|
496
|
+
const __ox_island_components = {
|
|
497
|
+
${hydratedIslands.map((name) => ` ${name},`).join("\n")}
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
export function hydrateIslands(options) {
|
|
501
|
+
return initIslands((element, props) => {
|
|
502
|
+
const Component = __ox_island_components[element.dataset.oxIsland];
|
|
503
|
+
if (!Component) return;
|
|
504
|
+
|
|
505
|
+
const islandContent = readIslandSlotHtml(element);
|
|
506
|
+
const componentProps = { ...props };
|
|
507
|
+
if (islandContent) {
|
|
508
|
+
componentProps.children = createRawSnippet(() => ({
|
|
509
|
+
render: () => \`<div>\${islandContent}</div>\`,
|
|
510
|
+
}));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const attach = element.dataset.oxSsr === 'true' ? hydrate : mount;
|
|
514
|
+
const instance = attach(Component, { target: element, props: componentProps });
|
|
515
|
+
return () => unmount(instance);
|
|
516
|
+
}, { selector: '[data-ox-island]', ...options });
|
|
517
|
+
}
|
|
518
|
+
<\/script>`;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* `JSON.stringify` drops functions and `undefined` silently and throws an
|
|
522
|
+
* opaque error on a cycle, either of which turns into an island that renders
|
|
523
|
+
* but never comes alive. Walking first turns both into a build diagnostic that
|
|
524
|
+
* names the prop.
|
|
525
|
+
*/
|
|
526
|
+
function renderMdxIslandPropsSerializer(filePathLiteral) {
|
|
527
|
+
return `
|
|
528
|
+
function __ox_mdx_island_props(componentName, props) {
|
|
529
|
+
const seen = new WeakSet();
|
|
530
|
+
const check = (value, path) => {
|
|
531
|
+
const kind = typeof value;
|
|
532
|
+
if (value === null || kind === 'string' || kind === 'number' || kind === 'boolean') return;
|
|
533
|
+
if (kind !== 'object') {
|
|
534
|
+
throw new Error('[ox-content-svelte] Island "' + componentName + '" in ' + ${filePathLiteral} + ' received a ' + kind + ' for prop "' + path + '", which cannot be serialised for hydration. Pass a JSON value, or drop ${MDX_ISLAND_DIRECTIVE} to keep the component server-only.');
|
|
535
|
+
}
|
|
536
|
+
if (seen.has(value)) {
|
|
537
|
+
throw new Error('[ox-content-svelte] Island "' + componentName + '" in ' + ${filePathLiteral} + ' received a circular value for prop "' + path + '", which cannot be serialised for hydration.');
|
|
538
|
+
}
|
|
539
|
+
seen.add(value);
|
|
540
|
+
if (Array.isArray(value)) {
|
|
541
|
+
value.forEach((item, index) => check(item, path + '[' + index + ']'));
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
for (const key of Object.keys(value)) check(value[key], path ? path + '.' + key : key);
|
|
545
|
+
};
|
|
546
|
+
for (const key of Object.keys(props)) check(props[key], key);
|
|
547
|
+
return JSON.stringify(props);
|
|
548
|
+
}
|
|
549
|
+
`;
|
|
550
|
+
}
|
|
551
|
+
function renderMdxDocumentTemplate(html, usedComponents, filePath, documentExpressions) {
|
|
552
|
+
const context = {
|
|
553
|
+
html,
|
|
554
|
+
filePath,
|
|
555
|
+
usedComponents: new Set(usedComponents),
|
|
556
|
+
expressionsByMarker: new Map(documentExpressions.map((expression) => [expression.marker, expression])),
|
|
557
|
+
islandRanges: findMdxIslandRanges(html),
|
|
558
|
+
hydratedIslands: /* @__PURE__ */ new Set()
|
|
559
|
+
};
|
|
560
|
+
return {
|
|
561
|
+
template: renderHtmlRange(context, 0, html.length),
|
|
562
|
+
hydratedIslands: [...context.hydratedIslands]
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function renderHtmlRange(context, start, end) {
|
|
566
|
+
let output = "";
|
|
567
|
+
let cursor = start;
|
|
568
|
+
while (cursor < end) {
|
|
569
|
+
const island = findNextIslandRange(context, cursor, end);
|
|
570
|
+
if (!island) {
|
|
571
|
+
output += renderRawHtmlTemplate(context.html.slice(cursor, end), context.expressionsByMarker);
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
output += renderRawHtmlTemplate(context.html.slice(cursor, island.openStart), context.expressionsByMarker);
|
|
575
|
+
output += renderMdxIslandTemplate(context, island);
|
|
576
|
+
cursor = island.closeEnd;
|
|
577
|
+
}
|
|
578
|
+
return output;
|
|
579
|
+
}
|
|
580
|
+
function findNextIslandRange(context, cursor, end) {
|
|
581
|
+
for (const island of context.islandRanges) {
|
|
582
|
+
if (island.openStart < cursor || island.closeEnd > end) continue;
|
|
583
|
+
if (context.usedComponents.has(island.name)) return island;
|
|
584
|
+
}
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
function renderRawHtmlTemplate(html, expressionsByMarker) {
|
|
588
|
+
if (!html) return "";
|
|
589
|
+
let output = "";
|
|
590
|
+
let cursor = 0;
|
|
591
|
+
while (cursor < html.length) {
|
|
592
|
+
const next = findNextDocumentExpressionMarker(html, cursor, expressionsByMarker);
|
|
593
|
+
if (!next) {
|
|
594
|
+
output += renderRawHtmlBlock(html.slice(cursor));
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
output += renderRawHtmlBlock(html.slice(cursor, next.index));
|
|
598
|
+
output += renderDocumentExpression(next.expression);
|
|
599
|
+
cursor = next.index + next.expression.marker.length;
|
|
600
|
+
}
|
|
601
|
+
return output;
|
|
602
|
+
}
|
|
603
|
+
function findNextDocumentExpressionMarker(html, start, expressionsByMarker) {
|
|
604
|
+
let nextIndex = -1;
|
|
605
|
+
let nextExpression;
|
|
606
|
+
for (const expression of expressionsByMarker.values()) {
|
|
607
|
+
const index = html.indexOf(expression.marker, start);
|
|
608
|
+
if (index !== -1 && (nextIndex === -1 || index < nextIndex)) {
|
|
609
|
+
nextIndex = index;
|
|
610
|
+
nextExpression = expression;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return nextExpression ? {
|
|
614
|
+
index: nextIndex,
|
|
615
|
+
expression: nextExpression
|
|
616
|
+
} : null;
|
|
617
|
+
}
|
|
618
|
+
function renderRawHtmlBlock(html) {
|
|
619
|
+
return html ? `{@html ${JSON.stringify(html).replaceAll("<\/script", "<\\/script")}}` : "";
|
|
620
|
+
}
|
|
621
|
+
function renderDocumentExpression(expression) {
|
|
622
|
+
return `{${documentPropResolverExpression(expression.path, expression.expression)}}`;
|
|
623
|
+
}
|
|
624
|
+
function renderMdxIslandTemplate(context, island) {
|
|
625
|
+
assertSvelteComponentName(island.name, context.filePath);
|
|
626
|
+
const payload = readMdxIslandPayload(island);
|
|
627
|
+
const hydration = takeMdxIslandHydration(payload, island.name, context.filePath);
|
|
628
|
+
const attrs = renderMdxIslandAttributes(payload, context.filePath);
|
|
629
|
+
const children = renderHtmlRange(context, island.contentStart, island.closeStart);
|
|
630
|
+
const element = children ? `<${island.name}${attrs}>${children}</${island.name}>` : `<${island.name}${attrs} />`;
|
|
631
|
+
if (!hydration) return element;
|
|
632
|
+
context.hydratedIslands.add(island.name);
|
|
633
|
+
const wrapperAttrs = [
|
|
634
|
+
`data-ox-island="${island.name}"`,
|
|
635
|
+
"data-ox-ssr=\"true\"",
|
|
636
|
+
`data-ox-load="${hydration.load}"`
|
|
637
|
+
];
|
|
638
|
+
if (hydration.media) wrapperAttrs.push(`data-ox-media=${JSON.stringify(hydration.media)}`);
|
|
639
|
+
wrapperAttrs.push(`data-ox-props={${mdxIslandPropsExpression(payload, island.name, context.filePath)}}`);
|
|
640
|
+
return `<div ${wrapperAttrs.join(" ")}>${element}</div>`;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Reads and removes the island directives so they never reach the component.
|
|
644
|
+
*
|
|
645
|
+
* The strategy is a build-time decision, so it has to be a literal: resolving
|
|
646
|
+
* it from a document prop would mean the wrapper could not be written until
|
|
647
|
+
* render time, by which point the load strategy has already been read.
|
|
648
|
+
*/
|
|
649
|
+
function takeMdxIslandHydration(payload, name, filePath) {
|
|
650
|
+
for (const directive of [MDX_ISLAND_DIRECTIVE, MDX_ISLAND_MEDIA_DIRECTIVE]) if (directive in payload.expressions) throw new Error(`[ox-content-svelte] "${directive}" on <${name}> in ${filePath} has to be a literal, not a document prop: the load strategy is chosen when the page is built.`);
|
|
651
|
+
if (!(MDX_ISLAND_DIRECTIVE in payload.props)) return void 0;
|
|
652
|
+
const raw = payload.props[MDX_ISLAND_DIRECTIVE];
|
|
653
|
+
const media = payload.props[MDX_ISLAND_MEDIA_DIRECTIVE];
|
|
654
|
+
delete payload.props[MDX_ISLAND_DIRECTIVE];
|
|
655
|
+
delete payload.props[MDX_ISLAND_MEDIA_DIRECTIVE];
|
|
656
|
+
const load = raw === true || raw === "" ? "eager" : raw;
|
|
657
|
+
if (typeof load !== "string" || !MDX_ISLAND_LOAD_STRATEGIES.has(load)) throw new Error(`[ox-content-svelte] Unknown island load strategy ${JSON.stringify(raw)} on <${name}> in ${filePath}. Use ${[...MDX_ISLAND_LOAD_STRATEGIES].join(", ")}.`);
|
|
658
|
+
if (load === "media" && typeof media !== "string") throw new Error(`[ox-content-svelte] <${name} ${MDX_ISLAND_DIRECTIVE}="media"> in ${filePath} needs ${MDX_ISLAND_MEDIA_DIRECTIVE} with the query to wait for.`);
|
|
659
|
+
return {
|
|
660
|
+
load,
|
|
661
|
+
media: typeof media === "string" ? media : void 0
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* The same props the component is rendered with, serialised for the client.
|
|
666
|
+
*
|
|
667
|
+
* Built in attribute order so a spread and a named prop resolve the way Svelte
|
|
668
|
+
* resolves them in the template above.
|
|
669
|
+
*/
|
|
670
|
+
function mdxIslandPropsExpression(payload, name, filePath) {
|
|
671
|
+
const parts = [];
|
|
672
|
+
for (const spread of payload.spreads) {
|
|
673
|
+
const expression = spread.trim().startsWith("...") ? spread.trim().slice(3).trim() : spread.trim();
|
|
674
|
+
const path = parseDocumentPropPath(expression);
|
|
675
|
+
if (!path) throw new Error(`[ox-content-svelte] Unsupported MDX document prop spread "{${spread}}" in ${filePath}. Only identifiers and dotted property paths are supported.`);
|
|
676
|
+
parts.push(documentPropResolverExpression(path, expression));
|
|
677
|
+
}
|
|
678
|
+
const entries = [];
|
|
679
|
+
for (const [key, value] of Object.entries(payload.props)) entries.push(`${JSON.stringify(key)}: ${renderSvelteLiteral(value)}`);
|
|
680
|
+
for (const [key, expression] of Object.entries(payload.expressions)) {
|
|
681
|
+
const path = parseDocumentPropPath(expression.trim());
|
|
682
|
+
if (!path) throw new Error(`[ox-content-svelte] Unsupported MDX document prop expression "{${expression}}" for prop "${key}" in ${filePath}. Only identifiers and dotted property paths are supported.`);
|
|
683
|
+
entries.push(`${JSON.stringify(key)}: ${documentPropResolverExpression(path, expression.trim())}`);
|
|
684
|
+
}
|
|
685
|
+
if (entries.length > 0) parts.push(`{ ${entries.join(", ")} }`);
|
|
686
|
+
const merged = parts.length === 0 ? "{}" : parts.length === 1 ? parts[0] : `Object.assign({}, ${parts.join(", ")})`;
|
|
687
|
+
return `__ox_mdx_island_props(${JSON.stringify(name)}, ${merged})`;
|
|
688
|
+
}
|
|
689
|
+
function renderMdxIslandAttributes(payload, filePath) {
|
|
690
|
+
const attrs = [];
|
|
691
|
+
for (const spread of payload.spreads) {
|
|
692
|
+
const expression = spread.trim().startsWith("...") ? spread.trim().slice(3).trim() : spread.trim();
|
|
693
|
+
const path = parseDocumentPropPath(expression);
|
|
694
|
+
if (!path) throw new Error(`[ox-content-svelte] Unsupported MDX document prop spread "{${spread}}" in ${filePath}. Only identifiers and dotted property paths are supported.`);
|
|
695
|
+
attrs.push(`{...${documentPropResolverExpression(path, expression)}}`);
|
|
696
|
+
}
|
|
697
|
+
for (const [name, value] of Object.entries(payload.props)) {
|
|
698
|
+
assertSvelteAttributeName(name, filePath);
|
|
699
|
+
attrs.push(`${name}={${renderSvelteLiteral(value)}}`);
|
|
700
|
+
}
|
|
701
|
+
for (const [name, expression] of Object.entries(payload.expressions)) {
|
|
702
|
+
assertSvelteAttributeName(name, filePath);
|
|
703
|
+
const path = parseDocumentPropPath(expression.trim());
|
|
704
|
+
if (!path) throw new Error(`[ox-content-svelte] Unsupported MDX document prop expression "{${expression}}" for prop "${name}" in ${filePath}. Only identifiers and dotted property paths are supported.`);
|
|
705
|
+
attrs.push(`${name}={${documentPropResolverExpression(path, expression.trim())}}`);
|
|
706
|
+
}
|
|
707
|
+
return attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
|
|
708
|
+
}
|
|
709
|
+
function documentPropResolverExpression(path, expression) {
|
|
710
|
+
return `__ox_mdx_document_prop(__ox_mdx_props, ${JSON.stringify(path)}, ${JSON.stringify(expression)})`;
|
|
711
|
+
}
|
|
712
|
+
function renderSvelteLiteral(value) {
|
|
713
|
+
const literal = JSON.stringify(value);
|
|
714
|
+
return literal === void 0 ? "undefined" : literal.replaceAll("<\/script", "<\\/script");
|
|
715
|
+
}
|
|
716
|
+
function findMdxIslandRanges(html) {
|
|
717
|
+
const ranges = [];
|
|
718
|
+
const openRe = /<(div|span)\b([^>]*\bdata-ox-island="([^"]+)"[^>]*)>/gi;
|
|
719
|
+
let match;
|
|
720
|
+
while ((match = openRe.exec(html)) !== null) {
|
|
721
|
+
const tag = match[1];
|
|
722
|
+
const name = decodeHtmlAttr(match[3] ?? "");
|
|
723
|
+
if (!name) continue;
|
|
724
|
+
const openStart = match.index;
|
|
725
|
+
const openEnd = match.index + match[0].length;
|
|
726
|
+
const closeStart = findMatchingClose(html, openEnd, tag);
|
|
727
|
+
const closeEnd = closeStart < html.length ? closeStart + tag.length + 3 : html.length;
|
|
728
|
+
const script = html.slice(openEnd, closeStart).match(PAYLOAD_SCRIPT)?.[0];
|
|
729
|
+
ranges.push({
|
|
730
|
+
name,
|
|
731
|
+
tag,
|
|
732
|
+
openStart,
|
|
733
|
+
openEnd,
|
|
734
|
+
innerStart: openEnd,
|
|
735
|
+
contentStart: openEnd + (script?.length ?? 0),
|
|
736
|
+
closeStart,
|
|
737
|
+
closeEnd,
|
|
738
|
+
propsAttr: matchAttr(match[2] ?? "", "data-ox-props"),
|
|
739
|
+
script
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
return ranges.sort((left, right) => left.openStart - right.openStart);
|
|
743
|
+
}
|
|
744
|
+
function findMatchingClose(html, from, tag) {
|
|
745
|
+
const openNeedle = `<${tag}`;
|
|
746
|
+
const closeNeedle = `</${tag}>`;
|
|
747
|
+
let depth = 1;
|
|
748
|
+
let cursor = from;
|
|
749
|
+
while (cursor < html.length) {
|
|
750
|
+
const nextOpen = indexOfTagOpen(html, openNeedle, cursor);
|
|
751
|
+
const nextClose = html.indexOf(closeNeedle, cursor);
|
|
752
|
+
if (nextClose === -1) return html.length;
|
|
753
|
+
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
754
|
+
depth += 1;
|
|
755
|
+
cursor = nextOpen + openNeedle.length;
|
|
756
|
+
} else {
|
|
757
|
+
depth -= 1;
|
|
758
|
+
if (depth === 0) return nextClose;
|
|
759
|
+
cursor = nextClose + closeNeedle.length;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return html.length;
|
|
763
|
+
}
|
|
764
|
+
function indexOfTagOpen(html, openNeedle, from) {
|
|
765
|
+
let cursor = from;
|
|
766
|
+
while (cursor < html.length) {
|
|
767
|
+
const index = html.indexOf(openNeedle, cursor);
|
|
768
|
+
if (index === -1) return -1;
|
|
769
|
+
const next = html[index + openNeedle.length];
|
|
770
|
+
if (next === " " || next === ">" || next === " " || next === "\n" || next === "/") return index;
|
|
771
|
+
cursor = index + openNeedle.length;
|
|
772
|
+
}
|
|
773
|
+
return -1;
|
|
774
|
+
}
|
|
775
|
+
function matchAttr(attrs, name) {
|
|
776
|
+
const match = new RegExp(`\\b${name}="([^"]*)"`, "i").exec(attrs);
|
|
777
|
+
return match?.[1] === void 0 ? void 0 : decodeHtmlAttr(match[1]);
|
|
778
|
+
}
|
|
779
|
+
function readMdxIslandPayload(island) {
|
|
780
|
+
const fromAttr = island.propsAttr ? tryParseJson(island.propsAttr) : void 0;
|
|
781
|
+
const fromScript = island.script ? tryParseJson(island.script.match(/<script type="application\/json">([\s\S]*?)<\/script>/i)?.[1] ?? "") : void 0;
|
|
782
|
+
return normalizeMdxIslandPayload(fromAttr ?? fromScript ?? {});
|
|
783
|
+
}
|
|
784
|
+
function normalizeMdxIslandPayload(parsed) {
|
|
785
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {
|
|
786
|
+
props: {},
|
|
787
|
+
expressions: {},
|
|
788
|
+
spreads: []
|
|
789
|
+
};
|
|
790
|
+
const record = parsed;
|
|
791
|
+
const keys = Object.keys(record);
|
|
792
|
+
if (keys.length > 0 && keys.every((key) => RUST_PAYLOAD_KEYS.has(key))) return {
|
|
793
|
+
props: toRecord(record.props),
|
|
794
|
+
expressions: toStringRecord(record.expressions),
|
|
795
|
+
spreads: toStringArray(record.spreads)
|
|
796
|
+
};
|
|
797
|
+
return {
|
|
798
|
+
props: record,
|
|
799
|
+
expressions: {},
|
|
800
|
+
spreads: []
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
function toRecord(value) {
|
|
804
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
805
|
+
}
|
|
806
|
+
function toStringRecord(value) {
|
|
807
|
+
const record = toRecord(value);
|
|
808
|
+
const output = {};
|
|
809
|
+
for (const [key, entry] of Object.entries(record)) if (typeof entry === "string") output[key] = entry;
|
|
810
|
+
return output;
|
|
811
|
+
}
|
|
812
|
+
function toStringArray(value) {
|
|
813
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
814
|
+
}
|
|
815
|
+
function tryParseJson(value) {
|
|
816
|
+
try {
|
|
817
|
+
return JSON.parse(value);
|
|
818
|
+
} catch {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function decodeHtmlAttr(value) {
|
|
823
|
+
return value.replaceAll(""", "\"").replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
824
|
+
}
|
|
825
|
+
function assertSvelteComponentName(name, filePath) {
|
|
826
|
+
if (!/^[A-Z][A-Za-z0-9_$]*$/.test(name)) throw new Error(`[ox-content-svelte] Unsupported MDX component name "${name}" in ${filePath} for mdxDocumentProps. Only simple Svelte component identifiers are supported.`);
|
|
827
|
+
}
|
|
828
|
+
function assertSvelteAttributeName(name, filePath) {
|
|
829
|
+
if (!/^[A-Za-z_$][\w$-]*$/.test(name)) throw new Error(`[ox-content-svelte] Unsupported MDX component prop name "${name}" in ${filePath}.`);
|
|
830
|
+
}
|
|
236
831
|
function generateSvelteModule(content, usedComponents, _islands, frontmatter, options, id, localBindings) {
|
|
237
832
|
const rawHtmlLiteral = JSON.stringify(content).replaceAll("<\/script", "<\\/script");
|
|
238
833
|
const imports = renderIslandComponentImports(usedComponents, {
|
|
@@ -262,7 +857,7 @@ function generateSvelteModule(content, usedComponents, _islands, frontmatter, op
|
|
|
262
857
|
const componentMap = usedComponents.map((name) => ` ${name},`).join("\n");
|
|
263
858
|
return `
|
|
264
859
|
<script>
|
|
265
|
-
import { createRawSnippet,
|
|
860
|
+
import { createRawSnippet, hydrate, mount, onMount, unmount } from 'svelte';
|
|
266
861
|
import { initIslands, readIslandSlotHtml } from '@ox-content/islands';
|
|
267
862
|
${imports}
|
|
268
863
|
|
|
@@ -292,7 +887,8 @@ ${componentMap}
|
|
|
292
887
|
}));
|
|
293
888
|
}
|
|
294
889
|
|
|
295
|
-
const
|
|
890
|
+
const attach = element.dataset.oxSsr === 'true' ? hydrate : mount;
|
|
891
|
+
const instance = attach(Component, { target: element, props: componentProps });
|
|
296
892
|
mounted.push(instance);
|
|
297
893
|
|
|
298
894
|
return () => unmount(instance);
|
|
@@ -384,6 +980,10 @@ function resolveSingleEmbedOptions(options) {
|
|
|
384
980
|
/**
|
|
385
981
|
* Creates the Ox Content Svelte integration plugin.
|
|
386
982
|
*
|
|
983
|
+
* Forwards core options such as `ssg`, `redirects`, `feeds`, and `siteMaps`.
|
|
984
|
+
* The Svelte Markdown transform and environments replace the generic core
|
|
985
|
+
* transform/`markdown` environment; other build plugins are kept.
|
|
986
|
+
*
|
|
387
987
|
* @example
|
|
388
988
|
* ```ts
|
|
389
989
|
* // vite.config.ts
|
|
@@ -420,13 +1020,14 @@ function oxContentSvelte(options = {}) {
|
|
|
420
1020
|
componentMap = new Map(Object.entries(resolvedComponents));
|
|
421
1021
|
}
|
|
422
1022
|
},
|
|
423
|
-
async transform(code, id) {
|
|
1023
|
+
async transform(code, id, transformOptions) {
|
|
424
1024
|
if (!isMarkdownFilePath(id, resolved.extensions)) return null;
|
|
425
1025
|
const result = await transformMarkdownWithSvelte(code, id, {
|
|
426
1026
|
...resolved,
|
|
427
1027
|
components: Object.fromEntries(componentMap),
|
|
428
1028
|
root: config.root,
|
|
429
|
-
renderIsland: options.renderIsland
|
|
1029
|
+
renderIsland: options.renderIsland,
|
|
1030
|
+
ssr: transformOptions?.ssr
|
|
430
1031
|
});
|
|
431
1032
|
return {
|
|
432
1033
|
code: result.code,
|
|
@@ -479,14 +1080,13 @@ function oxContentSvelte(options = {}) {
|
|
|
479
1080
|
return modules;
|
|
480
1081
|
}
|
|
481
1082
|
};
|
|
482
|
-
const
|
|
483
|
-
|
|
1083
|
+
const replacedCorePluginNames = /* @__PURE__ */ new Set(["ox-content", "ox-content:environment"]);
|
|
1084
|
+
return [
|
|
484
1085
|
svelteTransformPlugin,
|
|
485
1086
|
svelteEnvironmentPlugin,
|
|
486
|
-
svelteHmrPlugin
|
|
1087
|
+
svelteHmrPlugin,
|
|
1088
|
+
...oxContent$1(options).flatMap((plugin) => Array.isArray(plugin) ? plugin : [plugin]).filter((plugin) => !replacedCorePluginNames.has(plugin.name))
|
|
487
1089
|
];
|
|
488
|
-
if (environmentPlugin) plugins.push(environmentPlugin);
|
|
489
|
-
return plugins;
|
|
490
1090
|
}
|
|
491
1091
|
function resolveSvelteOptions(options) {
|
|
492
1092
|
return {
|
|
@@ -502,7 +1102,8 @@ function resolveSvelteOptions(options) {
|
|
|
502
1102
|
codeAnnotations: resolveCodeAnnotationsOptions(options.codeAnnotations),
|
|
503
1103
|
runes: options.runes ?? true,
|
|
504
1104
|
embeds: resolveBuiltinEmbedOptions(options.embeds),
|
|
505
|
-
mdx: options.mdx
|
|
1105
|
+
mdx: options.mdx,
|
|
1106
|
+
mdxDocumentProps: options.mdxDocumentProps ?? false
|
|
506
1107
|
};
|
|
507
1108
|
}
|
|
508
1109
|
function resolveCodeAnnotationsOptions(options) {
|