@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.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 transformed = await transformMarkdown(markdownContent, id, baseOptions);
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
- 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);
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("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
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, onMount, mount, unmount } from 'svelte';
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 instance = mount(Component, { target: element, props: componentProps });
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 environmentPlugin = oxContent$1(options).flatMap((plugin) => Array.isArray(plugin) ? plugin : [plugin]).find((plugin) => plugin.name === "ox-content:environment");
483
- const plugins = [
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) {