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