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