@ox-content/vite-plugin-svelte 3.0.0-alpha.1 → 3.0.0-alpha.11
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 +542 -59
- package/dist/index.d.cts +37 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +37 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +538 -61
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/dist/index.mjs
CHANGED
|
@@ -1,52 +1,30 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { oxContent, oxContent as oxContent$1, transformMarkdown } from "@ox-content/vite-plugin";
|
|
3
|
+
import { applyIslandSsrHtml, discoverDocumentMdxIslands, oxContent, oxContent as oxContent$1, renderHead, renderIslandComponentImports, resolveContentRootPath, resolveMdxForFilePath, transformMarkdown } from "@ox-content/vite-plugin";
|
|
4
4
|
import { compile } from "svelte/compiler";
|
|
5
5
|
//#region src/transform.ts
|
|
6
6
|
const COMPONENT_REGEX = /<([A-Z][a-zA-Z0-9]*)\s*([^>]*?)\s*(?:\/>|>([\s\S]*?)<\/\1>)/g;
|
|
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
|
-
const usedComponents = [];
|
|
13
|
-
const islands = [];
|
|
14
|
-
let islandIndex = 0;
|
|
15
20
|
const { content: markdownContent, frontmatter } = extractFrontmatter(code);
|
|
16
|
-
const
|
|
17
|
-
let processedContent = "";
|
|
18
|
-
let lastIndex = 0;
|
|
19
|
-
let match;
|
|
20
|
-
COMPONENT_REGEX.lastIndex = 0;
|
|
21
|
-
while ((match = COMPONENT_REGEX.exec(markdownContent)) !== null) {
|
|
22
|
-
const [fullMatch, componentName, propsString, rawIslandContent] = match;
|
|
23
|
-
const matchStart = match.index;
|
|
24
|
-
const matchEnd = matchStart + fullMatch.length;
|
|
25
|
-
if (!Object.prototype.hasOwnProperty.call(components, componentName) || isInRanges(matchStart, matchEnd, fenceRanges)) {
|
|
26
|
-
processedContent += markdownContent.slice(lastIndex, matchEnd);
|
|
27
|
-
lastIndex = matchEnd;
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (!usedComponents.includes(componentName)) usedComponents.push(componentName);
|
|
31
|
-
const props = parseProps(propsString);
|
|
32
|
-
const islandId = `ox-island-${islandIndex++}`;
|
|
33
|
-
const islandContent = typeof rawIslandContent === "string" ? rawIslandContent.trim() : void 0;
|
|
34
|
-
islands.push({
|
|
35
|
-
name: componentName,
|
|
36
|
-
props,
|
|
37
|
-
position: matchStart,
|
|
38
|
-
id: islandId,
|
|
39
|
-
content: islandContent
|
|
40
|
-
});
|
|
41
|
-
processedContent += markdownContent.slice(lastIndex, matchStart) + createIslandMarker(islandId);
|
|
42
|
-
lastIndex = matchEnd;
|
|
43
|
-
}
|
|
44
|
-
processedContent += markdownContent.slice(lastIndex);
|
|
21
|
+
const mdx = resolveMdxForFilePath(id, options.mdx);
|
|
45
22
|
const baseOptions = {
|
|
46
23
|
srcDir: options.srcDir,
|
|
47
24
|
outDir: options.outDir,
|
|
48
25
|
base: options.base,
|
|
49
26
|
extensions: options.extensions,
|
|
27
|
+
mdx,
|
|
50
28
|
ssg: {
|
|
51
29
|
enabled: false,
|
|
52
30
|
extension: ".html",
|
|
@@ -56,6 +34,7 @@ async function transformMarkdownWithSvelte(code, id, options) {
|
|
|
56
34
|
lastUpdated: false,
|
|
57
35
|
pagination: false,
|
|
58
36
|
breadcrumbs: false,
|
|
37
|
+
jsonLd: false,
|
|
59
38
|
readerChrome: false,
|
|
60
39
|
localeSwitcher: false,
|
|
61
40
|
a11y: false,
|
|
@@ -94,11 +73,66 @@ async function transformMarkdownWithSvelte(code, id, options) {
|
|
|
94
73
|
embeds: options.embeds,
|
|
95
74
|
i18n: false
|
|
96
75
|
};
|
|
97
|
-
|
|
76
|
+
if (mdx) {
|
|
77
|
+
const documentExpressions = options.mdxDocumentProps ? prepareMdxDocumentExpressions(markdownContent, id) : {
|
|
78
|
+
content: markdownContent,
|
|
79
|
+
expressions: []
|
|
80
|
+
};
|
|
81
|
+
const transformed = await transformMarkdown(documentExpressions.content, id, baseOptions);
|
|
82
|
+
const discovered = await discoverDocumentMdxIslands({
|
|
83
|
+
source: markdownContent,
|
|
84
|
+
html: transformed.html,
|
|
85
|
+
components,
|
|
86
|
+
imports: transformed.imports,
|
|
87
|
+
documentPath: id,
|
|
88
|
+
contentRoot: resolveContentRootPath({
|
|
89
|
+
srcDir: options.srcDir,
|
|
90
|
+
root: options.root
|
|
91
|
+
}),
|
|
92
|
+
srcDir: options.srcDir
|
|
93
|
+
});
|
|
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);
|
|
96
|
+
}
|
|
97
|
+
const usedComponents = [];
|
|
98
|
+
const islands = [];
|
|
99
|
+
let islandIndex = 0;
|
|
100
|
+
const fenceRanges = collectFenceRanges(markdownContent);
|
|
101
|
+
let processedContent = "";
|
|
102
|
+
let lastIndex = 0;
|
|
103
|
+
let match;
|
|
104
|
+
COMPONENT_REGEX.lastIndex = 0;
|
|
105
|
+
while ((match = COMPONENT_REGEX.exec(markdownContent)) !== null) {
|
|
106
|
+
const [fullMatch, componentName, propsString, rawIslandContent] = match;
|
|
107
|
+
const matchStart = match.index;
|
|
108
|
+
const matchEnd = matchStart + fullMatch.length;
|
|
109
|
+
if (!Object.prototype.hasOwnProperty.call(components, componentName) || isInRanges(matchStart, matchEnd, fenceRanges)) {
|
|
110
|
+
processedContent += markdownContent.slice(lastIndex, matchEnd);
|
|
111
|
+
lastIndex = matchEnd;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!usedComponents.includes(componentName)) usedComponents.push(componentName);
|
|
115
|
+
const props = parseProps(propsString);
|
|
116
|
+
const islandId = `ox-island-${islandIndex++}`;
|
|
117
|
+
const islandContent = typeof rawIslandContent === "string" ? rawIslandContent.trim() : void 0;
|
|
118
|
+
islands.push({
|
|
119
|
+
name: componentName,
|
|
120
|
+
props,
|
|
121
|
+
position: matchStart,
|
|
122
|
+
id: islandId,
|
|
123
|
+
content: islandContent
|
|
124
|
+
});
|
|
125
|
+
processedContent += markdownContent.slice(lastIndex, matchStart) + createIslandMarker(islandId);
|
|
126
|
+
lastIndex = matchEnd;
|
|
127
|
+
}
|
|
128
|
+
processedContent += markdownContent.slice(lastIndex);
|
|
129
|
+
return compileSvelteResult(generateSvelteModule(injectIslandMarkers((await transformMarkdown(processedContent, id, baseOptions)).html, islands), usedComponents, islands, frontmatter, options, id), id, usedComponents, frontmatter, options.ssr);
|
|
130
|
+
}
|
|
131
|
+
function compileSvelteResult(svelteCode, id, usedComponents, frontmatter, ssr = false) {
|
|
98
132
|
return {
|
|
99
133
|
code: `${compile(svelteCode, {
|
|
100
134
|
filename: id,
|
|
101
|
-
generate: "client",
|
|
135
|
+
generate: ssr ? "server" : "client",
|
|
102
136
|
runes: true
|
|
103
137
|
}).js.code}\nexport const frontmatter = ${JSON.stringify(frontmatter)};`,
|
|
104
138
|
map: null,
|
|
@@ -212,20 +246,455 @@ function parseProps(propsString) {
|
|
|
212
246
|
}
|
|
213
247
|
return props;
|
|
214
248
|
}
|
|
215
|
-
function
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
249
|
+
function prepareMdxDocumentExpressions(content, filePath) {
|
|
250
|
+
const skipRanges = mergeRanges([
|
|
251
|
+
...collectFenceRanges(content),
|
|
252
|
+
...collectInlineCodeRanges(content),
|
|
253
|
+
...collectMdxEsmLineRanges(content)
|
|
254
|
+
]);
|
|
255
|
+
const expressions = [];
|
|
256
|
+
let output = "";
|
|
257
|
+
let cursor = 0;
|
|
258
|
+
let rangeIndex = 0;
|
|
259
|
+
let inTag = false;
|
|
260
|
+
let quote = null;
|
|
261
|
+
while (cursor < content.length) {
|
|
262
|
+
const range = skipRanges[rangeIndex];
|
|
263
|
+
if (range && cursor >= range.end) {
|
|
264
|
+
rangeIndex += 1;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (range && cursor === range.start) {
|
|
268
|
+
output += content.slice(range.start, range.end);
|
|
269
|
+
cursor = range.end;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const char = content[cursor];
|
|
273
|
+
if (inTag) {
|
|
274
|
+
output += char;
|
|
275
|
+
if (quote) {
|
|
276
|
+
if (char === quote && content[cursor - 1] !== "\\") quote = null;
|
|
277
|
+
} else if (char === "\"" || char === "'") quote = char;
|
|
278
|
+
else if (char === ">") inTag = false;
|
|
279
|
+
cursor += 1;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (char === "<" && startsHtmlLikeTag(content, cursor)) {
|
|
283
|
+
inTag = true;
|
|
284
|
+
output += char;
|
|
285
|
+
cursor += 1;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (char === "{" && content[cursor - 1] !== "\\") {
|
|
289
|
+
const end = findMdxExpressionEnd(content, cursor + 1);
|
|
290
|
+
if (end !== -1) {
|
|
291
|
+
const expression = content.slice(cursor + 1, end).trim();
|
|
292
|
+
const path = parseDocumentPropPath(expression);
|
|
293
|
+
if (!path) throw new Error(`[ox-content-svelte] Unsupported MDX document prop expression "{${expression}}" in ${filePath}. Only identifiers and dotted property paths are supported.`);
|
|
294
|
+
const marker = `${DOCUMENT_PROP_MARKER_PREFIX}${expressions.length}${DOCUMENT_PROP_MARKER_SUFFIX}`;
|
|
295
|
+
expressions.push({
|
|
296
|
+
marker,
|
|
297
|
+
expression,
|
|
298
|
+
path
|
|
299
|
+
});
|
|
300
|
+
output += marker;
|
|
301
|
+
cursor = end + 1;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
output += char;
|
|
306
|
+
cursor += 1;
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
content: output,
|
|
310
|
+
expressions
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function collectInlineCodeRanges(content) {
|
|
314
|
+
const ranges = [];
|
|
315
|
+
const fenceRanges = collectFenceRanges(content);
|
|
316
|
+
let lineStart = 0;
|
|
317
|
+
while (lineStart < content.length) {
|
|
318
|
+
const lineEnd = content.indexOf("\n", lineStart);
|
|
319
|
+
const end = lineEnd === -1 ? content.length : lineEnd;
|
|
320
|
+
if (!isInRanges(lineStart, end, fenceRanges)) {
|
|
321
|
+
let cursor = lineStart;
|
|
322
|
+
while (cursor < end) {
|
|
323
|
+
const marker = matchBacktickRun(content, cursor);
|
|
324
|
+
if (!marker) {
|
|
325
|
+
cursor += 1;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const close = content.indexOf(marker, cursor + marker.length);
|
|
329
|
+
if (close === -1 || close >= end) {
|
|
330
|
+
cursor += marker.length;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
ranges.push({
|
|
334
|
+
start: cursor,
|
|
335
|
+
end: close + marker.length
|
|
336
|
+
});
|
|
337
|
+
cursor = close + marker.length;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
lineStart = lineEnd === -1 ? content.length : lineEnd + 1;
|
|
341
|
+
}
|
|
342
|
+
return ranges;
|
|
343
|
+
}
|
|
344
|
+
function collectMdxEsmLineRanges(content) {
|
|
345
|
+
const ranges = [];
|
|
346
|
+
const fenceRanges = collectFenceRanges(content);
|
|
347
|
+
let lineStart = 0;
|
|
348
|
+
while (lineStart < content.length) {
|
|
349
|
+
const lineEnd = content.indexOf("\n", lineStart);
|
|
350
|
+
const end = lineEnd === -1 ? content.length : lineEnd + 1;
|
|
351
|
+
const contentEnd = lineEnd === -1 ? content.length : lineEnd;
|
|
352
|
+
if (!isInRanges(lineStart, contentEnd, fenceRanges)) {
|
|
353
|
+
const line = content.slice(lineStart, contentEnd).trimStart();
|
|
354
|
+
if (line.startsWith("import ") || line.startsWith("export ")) ranges.push({
|
|
355
|
+
start: lineStart,
|
|
356
|
+
end
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
lineStart = lineEnd === -1 ? content.length : lineEnd + 1;
|
|
360
|
+
}
|
|
361
|
+
return ranges;
|
|
362
|
+
}
|
|
363
|
+
function mergeRanges(ranges) {
|
|
364
|
+
const sorted = ranges.filter((range) => range.end > range.start).sort((left, right) => left.start - right.start || left.end - right.end);
|
|
365
|
+
const merged = [];
|
|
366
|
+
for (const range of sorted) {
|
|
367
|
+
const previous = merged.at(-1);
|
|
368
|
+
if (previous && range.start <= previous.end) previous.end = Math.max(previous.end, range.end);
|
|
369
|
+
else merged.push({ ...range });
|
|
370
|
+
}
|
|
371
|
+
return merged;
|
|
372
|
+
}
|
|
373
|
+
function matchBacktickRun(content, index) {
|
|
374
|
+
if (content[index] !== "`") return null;
|
|
375
|
+
let end = index + 1;
|
|
376
|
+
while (content[end] === "`") end += 1;
|
|
377
|
+
return content.slice(index, end);
|
|
378
|
+
}
|
|
379
|
+
function startsHtmlLikeTag(content, index) {
|
|
380
|
+
const next = content[index + 1];
|
|
381
|
+
return next === "/" || next === "!" || next === "?" || /[A-Za-z]/.test(next ?? "");
|
|
382
|
+
}
|
|
383
|
+
function findMdxExpressionEnd(content, start) {
|
|
384
|
+
let depth = 1;
|
|
385
|
+
let quote = null;
|
|
386
|
+
let escaped = false;
|
|
387
|
+
for (let index = start; index < content.length; index += 1) {
|
|
388
|
+
const char = content[index];
|
|
389
|
+
if (quote) {
|
|
390
|
+
if (escaped) escaped = false;
|
|
391
|
+
else if (char === "\\") escaped = true;
|
|
392
|
+
else if (char === quote) quote = null;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
396
|
+
quote = char;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (char === "{") {
|
|
400
|
+
depth += 1;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (char === "}") {
|
|
404
|
+
depth -= 1;
|
|
405
|
+
if (depth === 0) return index;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return -1;
|
|
409
|
+
}
|
|
410
|
+
function parseDocumentPropPath(expression) {
|
|
411
|
+
if (!/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(expression) || RESERVED_DOCUMENT_PROP_WORDS.has(expression)) return null;
|
|
412
|
+
return expression.split(".");
|
|
413
|
+
}
|
|
414
|
+
const RESERVED_DOCUMENT_PROP_WORDS = /* @__PURE__ */ new Set([
|
|
415
|
+
"false",
|
|
416
|
+
"Infinity",
|
|
417
|
+
"NaN",
|
|
418
|
+
"null",
|
|
419
|
+
"this",
|
|
420
|
+
"true",
|
|
421
|
+
"undefined"
|
|
422
|
+
]);
|
|
423
|
+
function generateMdxDocumentPropsSvelteModule(html, usedComponents, frontmatter, options, id, localBindings, documentExpressions) {
|
|
424
|
+
const filePathLiteral = JSON.stringify(id);
|
|
425
|
+
const imports = renderIslandComponentImports(usedComponents, {
|
|
426
|
+
globalComponents: options.components,
|
|
427
|
+
localBindings,
|
|
428
|
+
documentPath: id,
|
|
429
|
+
root: options.root
|
|
430
|
+
});
|
|
431
|
+
const template = renderMdxDocumentTemplate(html, usedComponents, id, documentExpressions);
|
|
432
|
+
return `
|
|
433
|
+
<script>
|
|
434
|
+
${imports}
|
|
435
|
+
|
|
436
|
+
const frontmatter = ${JSON.stringify(frontmatter)};
|
|
437
|
+
export { frontmatter };
|
|
438
|
+
|
|
439
|
+
let __ox_mdx_props = $props();
|
|
440
|
+
|
|
441
|
+
function __ox_mdx_document_prop(props, path, expression) {
|
|
442
|
+
const propName = path.join(".");
|
|
443
|
+
let value = props;
|
|
444
|
+
for (const segment of path) {
|
|
445
|
+
if (
|
|
446
|
+
value == null ||
|
|
447
|
+
(typeof value !== "object" && typeof value !== "function") ||
|
|
448
|
+
!(segment in Object(value))
|
|
449
|
+
) {
|
|
450
|
+
throw new Error('[ox-content-svelte] Missing MDX document prop "' + propName + '" in ' + ${filePathLiteral} + ' for expression {' + expression + '}.');
|
|
451
|
+
}
|
|
452
|
+
value = value[segment];
|
|
453
|
+
}
|
|
454
|
+
if (value === undefined) {
|
|
455
|
+
throw new Error('[ox-content-svelte] Missing MDX document prop "' + propName + '" in ' + ${filePathLiteral} + ' for expression {' + expression + '}.');
|
|
456
|
+
}
|
|
457
|
+
return value;
|
|
458
|
+
}
|
|
459
|
+
<\/script>
|
|
460
|
+
|
|
461
|
+
<div class="ox-content">${template}</div>
|
|
462
|
+
|
|
463
|
+
<style>
|
|
464
|
+
.ox-content {
|
|
465
|
+
line-height: 1.6;
|
|
466
|
+
}
|
|
467
|
+
</style>
|
|
468
|
+
`;
|
|
469
|
+
}
|
|
470
|
+
function renderMdxDocumentTemplate(html, usedComponents, filePath, documentExpressions) {
|
|
471
|
+
return renderHtmlRange({
|
|
472
|
+
html,
|
|
473
|
+
filePath,
|
|
474
|
+
usedComponents: new Set(usedComponents),
|
|
475
|
+
expressionsByMarker: new Map(documentExpressions.map((expression) => [expression.marker, expression])),
|
|
476
|
+
islandRanges: findMdxIslandRanges(html)
|
|
477
|
+
}, 0, html.length);
|
|
478
|
+
}
|
|
479
|
+
function renderHtmlRange(context, start, end) {
|
|
480
|
+
let output = "";
|
|
481
|
+
let cursor = start;
|
|
482
|
+
while (cursor < end) {
|
|
483
|
+
const island = findNextIslandRange(context, cursor, end);
|
|
484
|
+
if (!island) {
|
|
485
|
+
output += renderRawHtmlTemplate(context.html.slice(cursor, end), context.expressionsByMarker);
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
output += renderRawHtmlTemplate(context.html.slice(cursor, island.openStart), context.expressionsByMarker);
|
|
489
|
+
output += renderMdxIslandTemplate(context, island);
|
|
490
|
+
cursor = island.closeEnd;
|
|
491
|
+
}
|
|
492
|
+
return output;
|
|
493
|
+
}
|
|
494
|
+
function findNextIslandRange(context, cursor, end) {
|
|
495
|
+
for (const island of context.islandRanges) {
|
|
496
|
+
if (island.openStart < cursor || island.closeEnd > end) continue;
|
|
497
|
+
if (context.usedComponents.has(island.name)) return island;
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
function renderRawHtmlTemplate(html, expressionsByMarker) {
|
|
502
|
+
if (!html) return "";
|
|
503
|
+
let output = "";
|
|
504
|
+
let cursor = 0;
|
|
505
|
+
while (cursor < html.length) {
|
|
506
|
+
const next = findNextDocumentExpressionMarker(html, cursor, expressionsByMarker);
|
|
507
|
+
if (!next) {
|
|
508
|
+
output += renderRawHtmlBlock(html.slice(cursor));
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
output += renderRawHtmlBlock(html.slice(cursor, next.index));
|
|
512
|
+
output += renderDocumentExpression(next.expression);
|
|
513
|
+
cursor = next.index + next.expression.marker.length;
|
|
514
|
+
}
|
|
515
|
+
return output;
|
|
516
|
+
}
|
|
517
|
+
function findNextDocumentExpressionMarker(html, start, expressionsByMarker) {
|
|
518
|
+
let nextIndex = -1;
|
|
519
|
+
let nextExpression;
|
|
520
|
+
for (const expression of expressionsByMarker.values()) {
|
|
521
|
+
const index = html.indexOf(expression.marker, start);
|
|
522
|
+
if (index !== -1 && (nextIndex === -1 || index < nextIndex)) {
|
|
523
|
+
nextIndex = index;
|
|
524
|
+
nextExpression = expression;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return nextExpression ? {
|
|
528
|
+
index: nextIndex,
|
|
529
|
+
expression: nextExpression
|
|
530
|
+
} : null;
|
|
531
|
+
}
|
|
532
|
+
function renderRawHtmlBlock(html) {
|
|
533
|
+
return html ? `{@html ${JSON.stringify(html).replaceAll("<\/script", "<\\/script")}}` : "";
|
|
534
|
+
}
|
|
535
|
+
function renderDocumentExpression(expression) {
|
|
536
|
+
return `{${documentPropResolverExpression(expression.path, expression.expression)}}`;
|
|
537
|
+
}
|
|
538
|
+
function renderMdxIslandTemplate(context, island) {
|
|
539
|
+
assertSvelteComponentName(island.name, context.filePath);
|
|
540
|
+
const attrs = renderMdxIslandAttributes(readMdxIslandPayload(island), context.filePath);
|
|
541
|
+
const children = renderHtmlRange(context, island.contentStart, island.closeStart);
|
|
542
|
+
return children ? `<${island.name}${attrs}>${children}</${island.name}>` : `<${island.name}${attrs} />`;
|
|
543
|
+
}
|
|
544
|
+
function renderMdxIslandAttributes(payload, filePath) {
|
|
545
|
+
const attrs = [];
|
|
546
|
+
for (const spread of payload.spreads) {
|
|
547
|
+
const expression = spread.trim().startsWith("...") ? spread.trim().slice(3).trim() : spread.trim();
|
|
548
|
+
const path = parseDocumentPropPath(expression);
|
|
549
|
+
if (!path) throw new Error(`[ox-content-svelte] Unsupported MDX document prop spread "{${spread}}" in ${filePath}. Only identifiers and dotted property paths are supported.`);
|
|
550
|
+
attrs.push(`{...${documentPropResolverExpression(path, expression)}}`);
|
|
551
|
+
}
|
|
552
|
+
for (const [name, value] of Object.entries(payload.props)) {
|
|
553
|
+
assertSvelteAttributeName(name, filePath);
|
|
554
|
+
attrs.push(`${name}={${renderSvelteLiteral(value)}}`);
|
|
555
|
+
}
|
|
556
|
+
for (const [name, expression] of Object.entries(payload.expressions)) {
|
|
557
|
+
assertSvelteAttributeName(name, filePath);
|
|
558
|
+
const path = parseDocumentPropPath(expression.trim());
|
|
559
|
+
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.`);
|
|
560
|
+
attrs.push(`${name}={${documentPropResolverExpression(path, expression.trim())}}`);
|
|
561
|
+
}
|
|
562
|
+
return attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
|
|
563
|
+
}
|
|
564
|
+
function documentPropResolverExpression(path, expression) {
|
|
565
|
+
return `__ox_mdx_document_prop(__ox_mdx_props, ${JSON.stringify(path)}, ${JSON.stringify(expression)})`;
|
|
566
|
+
}
|
|
567
|
+
function renderSvelteLiteral(value) {
|
|
568
|
+
const literal = JSON.stringify(value);
|
|
569
|
+
return literal === void 0 ? "undefined" : literal.replaceAll("<\/script", "<\\/script");
|
|
570
|
+
}
|
|
571
|
+
function findMdxIslandRanges(html) {
|
|
572
|
+
const ranges = [];
|
|
573
|
+
const openRe = /<(div|span)\b([^>]*\bdata-ox-island="([^"]+)"[^>]*)>/gi;
|
|
574
|
+
let match;
|
|
575
|
+
while ((match = openRe.exec(html)) !== null) {
|
|
576
|
+
const tag = match[1];
|
|
577
|
+
const name = decodeHtmlAttr(match[3] ?? "");
|
|
578
|
+
if (!name) continue;
|
|
579
|
+
const openStart = match.index;
|
|
580
|
+
const openEnd = match.index + match[0].length;
|
|
581
|
+
const closeStart = findMatchingClose(html, openEnd, tag);
|
|
582
|
+
const closeEnd = closeStart < html.length ? closeStart + tag.length + 3 : html.length;
|
|
583
|
+
const script = html.slice(openEnd, closeStart).match(PAYLOAD_SCRIPT)?.[0];
|
|
584
|
+
ranges.push({
|
|
585
|
+
name,
|
|
586
|
+
tag,
|
|
587
|
+
openStart,
|
|
588
|
+
openEnd,
|
|
589
|
+
innerStart: openEnd,
|
|
590
|
+
contentStart: openEnd + (script?.length ?? 0),
|
|
591
|
+
closeStart,
|
|
592
|
+
closeEnd,
|
|
593
|
+
propsAttr: matchAttr(match[2] ?? "", "data-ox-props"),
|
|
594
|
+
script
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
return ranges.sort((left, right) => left.openStart - right.openStart);
|
|
598
|
+
}
|
|
599
|
+
function findMatchingClose(html, from, tag) {
|
|
600
|
+
const openNeedle = `<${tag}`;
|
|
601
|
+
const closeNeedle = `</${tag}>`;
|
|
602
|
+
let depth = 1;
|
|
603
|
+
let cursor = from;
|
|
604
|
+
while (cursor < html.length) {
|
|
605
|
+
const nextOpen = indexOfTagOpen(html, openNeedle, cursor);
|
|
606
|
+
const nextClose = html.indexOf(closeNeedle, cursor);
|
|
607
|
+
if (nextClose === -1) return html.length;
|
|
608
|
+
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
609
|
+
depth += 1;
|
|
610
|
+
cursor = nextOpen + openNeedle.length;
|
|
611
|
+
} else {
|
|
612
|
+
depth -= 1;
|
|
613
|
+
if (depth === 0) return nextClose;
|
|
614
|
+
cursor = nextClose + closeNeedle.length;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return html.length;
|
|
618
|
+
}
|
|
619
|
+
function indexOfTagOpen(html, openNeedle, from) {
|
|
620
|
+
let cursor = from;
|
|
621
|
+
while (cursor < html.length) {
|
|
622
|
+
const index = html.indexOf(openNeedle, cursor);
|
|
623
|
+
if (index === -1) return -1;
|
|
624
|
+
const next = html[index + openNeedle.length];
|
|
625
|
+
if (next === " " || next === ">" || next === " " || next === "\n" || next === "/") return index;
|
|
626
|
+
cursor = index + openNeedle.length;
|
|
627
|
+
}
|
|
628
|
+
return -1;
|
|
629
|
+
}
|
|
630
|
+
function matchAttr(attrs, name) {
|
|
631
|
+
const match = new RegExp(`\\b${name}="([^"]*)"`, "i").exec(attrs);
|
|
632
|
+
return match?.[1] === void 0 ? void 0 : decodeHtmlAttr(match[1]);
|
|
633
|
+
}
|
|
634
|
+
function readMdxIslandPayload(island) {
|
|
635
|
+
const fromAttr = island.propsAttr ? tryParseJson(island.propsAttr) : void 0;
|
|
636
|
+
const fromScript = island.script ? tryParseJson(island.script.match(/<script type="application\/json">([\s\S]*?)<\/script>/i)?.[1] ?? "") : void 0;
|
|
637
|
+
return normalizeMdxIslandPayload(fromAttr ?? fromScript ?? {});
|
|
638
|
+
}
|
|
639
|
+
function normalizeMdxIslandPayload(parsed) {
|
|
640
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {
|
|
641
|
+
props: {},
|
|
642
|
+
expressions: {},
|
|
643
|
+
spreads: []
|
|
644
|
+
};
|
|
645
|
+
const record = parsed;
|
|
646
|
+
const keys = Object.keys(record);
|
|
647
|
+
if (keys.length > 0 && keys.every((key) => RUST_PAYLOAD_KEYS.has(key))) return {
|
|
648
|
+
props: toRecord(record.props),
|
|
649
|
+
expressions: toStringRecord(record.expressions),
|
|
650
|
+
spreads: toStringArray(record.spreads)
|
|
651
|
+
};
|
|
652
|
+
return {
|
|
653
|
+
props: record,
|
|
654
|
+
expressions: {},
|
|
655
|
+
spreads: []
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function toRecord(value) {
|
|
659
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
660
|
+
}
|
|
661
|
+
function toStringRecord(value) {
|
|
662
|
+
const record = toRecord(value);
|
|
663
|
+
const output = {};
|
|
664
|
+
for (const [key, entry] of Object.entries(record)) if (typeof entry === "string") output[key] = entry;
|
|
665
|
+
return output;
|
|
666
|
+
}
|
|
667
|
+
function toStringArray(value) {
|
|
668
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
669
|
+
}
|
|
670
|
+
function tryParseJson(value) {
|
|
671
|
+
try {
|
|
672
|
+
return JSON.parse(value);
|
|
673
|
+
} catch {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function decodeHtmlAttr(value) {
|
|
678
|
+
return value.replaceAll(""", "\"").replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
679
|
+
}
|
|
680
|
+
function assertSvelteComponentName(name, filePath) {
|
|
681
|
+
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.`);
|
|
682
|
+
}
|
|
683
|
+
function assertSvelteAttributeName(name, filePath) {
|
|
684
|
+
if (!/^[A-Za-z_$][\w$-]*$/.test(name)) throw new Error(`[ox-content-svelte] Unsupported MDX component prop name "${name}" in ${filePath}.`);
|
|
685
|
+
}
|
|
686
|
+
function generateSvelteModule(content, usedComponents, _islands, frontmatter, options, id, localBindings) {
|
|
687
|
+
const rawHtmlLiteral = JSON.stringify(content).replaceAll("<\/script", "<\\/script");
|
|
688
|
+
const imports = renderIslandComponentImports(usedComponents, {
|
|
689
|
+
globalComponents: options.components,
|
|
690
|
+
localBindings,
|
|
691
|
+
documentPath: id,
|
|
692
|
+
root: options.root
|
|
693
|
+
});
|
|
694
|
+
if (usedComponents.length === 0) return `
|
|
226
695
|
<script>
|
|
227
696
|
const frontmatter = ${JSON.stringify(frontmatter)};
|
|
228
|
-
const rawHtml = ${
|
|
697
|
+
const rawHtml = ${rawHtmlLiteral};
|
|
229
698
|
|
|
230
699
|
export { frontmatter };
|
|
231
700
|
<\/script>
|
|
@@ -243,12 +712,12 @@ function generateSvelteModule(content, usedComponents, islands, frontmatter, opt
|
|
|
243
712
|
const componentMap = usedComponents.map((name) => ` ${name},`).join("\n");
|
|
244
713
|
return `
|
|
245
714
|
<script>
|
|
246
|
-
import { createRawSnippet,
|
|
247
|
-
import { initIslands } from '@ox-content/islands';
|
|
715
|
+
import { createRawSnippet, hydrate, mount, onMount, unmount } from 'svelte';
|
|
716
|
+
import { initIslands, readIslandSlotHtml } from '@ox-content/islands';
|
|
248
717
|
${imports}
|
|
249
718
|
|
|
250
719
|
const frontmatter = ${JSON.stringify(frontmatter)};
|
|
251
|
-
const rawHtml = ${
|
|
720
|
+
const rawHtml = ${rawHtmlLiteral};
|
|
252
721
|
const components = {
|
|
253
722
|
${componentMap}
|
|
254
723
|
};
|
|
@@ -265,7 +734,7 @@ ${componentMap}
|
|
|
265
734
|
const Component = components[componentName];
|
|
266
735
|
if (!Component) return;
|
|
267
736
|
|
|
268
|
-
const islandContent = element
|
|
737
|
+
const islandContent = readIslandSlotHtml(element);
|
|
269
738
|
const componentProps = { ...props };
|
|
270
739
|
if (islandContent) {
|
|
271
740
|
componentProps.children = createRawSnippet(() => ({
|
|
@@ -273,7 +742,8 @@ ${componentMap}
|
|
|
273
742
|
}));
|
|
274
743
|
}
|
|
275
744
|
|
|
276
|
-
const
|
|
745
|
+
const attach = element.dataset.oxSsr === 'true' ? hydrate : mount;
|
|
746
|
+
const instance = attach(Component, { target: element, props: componentProps });
|
|
277
747
|
mounted.push(instance);
|
|
278
748
|
|
|
279
749
|
return () => unmount(instance);
|
|
@@ -365,6 +835,10 @@ function resolveSingleEmbedOptions(options) {
|
|
|
365
835
|
/**
|
|
366
836
|
* Creates the Ox Content Svelte integration plugin.
|
|
367
837
|
*
|
|
838
|
+
* Forwards core options such as `ssg`, `redirects`, `feeds`, and `siteMaps`.
|
|
839
|
+
* The Svelte Markdown transform and environments replace the generic core
|
|
840
|
+
* transform/`markdown` environment; other build plugins are kept.
|
|
841
|
+
*
|
|
368
842
|
* @example
|
|
369
843
|
* ```ts
|
|
370
844
|
* // vite.config.ts
|
|
@@ -401,12 +875,14 @@ function oxContentSvelte(options = {}) {
|
|
|
401
875
|
componentMap = new Map(Object.entries(resolvedComponents));
|
|
402
876
|
}
|
|
403
877
|
},
|
|
404
|
-
async transform(code, id) {
|
|
878
|
+
async transform(code, id, transformOptions) {
|
|
405
879
|
if (!isMarkdownFilePath(id, resolved.extensions)) return null;
|
|
406
880
|
const result = await transformMarkdownWithSvelte(code, id, {
|
|
407
881
|
...resolved,
|
|
408
882
|
components: Object.fromEntries(componentMap),
|
|
409
|
-
root: config.root
|
|
883
|
+
root: config.root,
|
|
884
|
+
renderIsland: options.renderIsland,
|
|
885
|
+
ssr: transformOptions?.ssr
|
|
410
886
|
});
|
|
411
887
|
return {
|
|
412
888
|
code: result.code,
|
|
@@ -459,14 +935,13 @@ function oxContentSvelte(options = {}) {
|
|
|
459
935
|
return modules;
|
|
460
936
|
}
|
|
461
937
|
};
|
|
462
|
-
const
|
|
463
|
-
|
|
938
|
+
const replacedCorePluginNames = /* @__PURE__ */ new Set(["ox-content", "ox-content:environment"]);
|
|
939
|
+
return [
|
|
464
940
|
svelteTransformPlugin,
|
|
465
941
|
svelteEnvironmentPlugin,
|
|
466
|
-
svelteHmrPlugin
|
|
942
|
+
svelteHmrPlugin,
|
|
943
|
+
...oxContent$1(options).flatMap((plugin) => Array.isArray(plugin) ? plugin : [plugin]).filter((plugin) => !replacedCorePluginNames.has(plugin.name))
|
|
467
944
|
];
|
|
468
|
-
if (environmentPlugin) plugins.push(environmentPlugin);
|
|
469
|
-
return plugins;
|
|
470
945
|
}
|
|
471
946
|
function resolveSvelteOptions(options) {
|
|
472
947
|
return {
|
|
@@ -481,7 +956,9 @@ function resolveSvelteOptions(options) {
|
|
|
481
956
|
tocMaxDepth: options.tocMaxDepth ?? 3,
|
|
482
957
|
codeAnnotations: resolveCodeAnnotationsOptions(options.codeAnnotations),
|
|
483
958
|
runes: options.runes ?? true,
|
|
484
|
-
embeds: resolveBuiltinEmbedOptions(options.embeds)
|
|
959
|
+
embeds: resolveBuiltinEmbedOptions(options.embeds),
|
|
960
|
+
mdx: options.mdx,
|
|
961
|
+
mdxDocumentProps: options.mdxDocumentProps ?? false
|
|
485
962
|
};
|
|
486
963
|
}
|
|
487
964
|
function resolveCodeAnnotationsOptions(options) {
|
|
@@ -564,6 +1041,6 @@ function toPascalCase(str) {
|
|
|
564
1041
|
return str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase());
|
|
565
1042
|
}
|
|
566
1043
|
//#endregion
|
|
567
|
-
export { oxContent, oxContentSvelte };
|
|
1044
|
+
export { oxContent, oxContentSvelte, renderHead };
|
|
568
1045
|
|
|
569
1046
|
//# sourceMappingURL=index.mjs.map
|