@ox-content/vite-plugin-react 0.0.1-alpha.0 → 0.3.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 +471 -378
- package/dist/index.d.cts +45 -47
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +45 -47
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +445 -350
- package/dist/index.js.map +1 -0
- package/package.json +22 -20
package/dist/index.js
CHANGED
|
@@ -1,317 +1,429 @@
|
|
|
1
|
-
// src/index.ts
|
|
2
1
|
import * as fs from "fs";
|
|
3
|
-
import * as path2 from "path";
|
|
4
|
-
import { oxContent } from "vite-plugin-ox-content";
|
|
5
|
-
|
|
6
|
-
// src/transform.ts
|
|
7
2
|
import * as path from "path";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
import { oxContent, oxContent as oxContent$1, transformMarkdown } from "@ox-content/vite-plugin";
|
|
4
|
+
|
|
5
|
+
//#region src/transform.ts
|
|
6
|
+
const COMPONENT_REGEX = /<([A-Z][a-zA-Z0-9]*)\s*([^>]*?)\s*(?:\/>|>([\s\S]*?)<\/\1>)/g;
|
|
7
|
+
const PROP_REGEX = /([a-zA-Z0-9-]+)(?:=(?:"([^"]*)"|'([^']*)'|{([^}]*)}|\[([^\]]*)\]))?/g;
|
|
8
|
+
const ISLAND_MARKER_PREFIX = "OXCONTENT-ISLAND-";
|
|
9
|
+
const ISLAND_MARKER_SUFFIX = "-PLACEHOLDER";
|
|
11
10
|
async function transformMarkdownWithReact(code, id, options) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
11
|
+
const components = options.components;
|
|
12
|
+
const usedComponents = [];
|
|
13
|
+
const islands = [];
|
|
14
|
+
let islandIndex = 0;
|
|
15
|
+
const { content: markdownContent, frontmatter } = extractFrontmatter(code);
|
|
16
|
+
const fenceRanges = collectFenceRanges(markdownContent);
|
|
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);
|
|
45
|
+
return {
|
|
46
|
+
code: generateReactModule(injectIslandMarkers((await transformMarkdown(processedContent, id, {
|
|
47
|
+
srcDir: options.srcDir,
|
|
48
|
+
outDir: options.outDir,
|
|
49
|
+
base: options.base,
|
|
50
|
+
ssg: {
|
|
51
|
+
enabled: false,
|
|
52
|
+
extension: ".html",
|
|
53
|
+
clean: false,
|
|
54
|
+
bare: false,
|
|
55
|
+
generateOgImage: false
|
|
56
|
+
},
|
|
57
|
+
gfm: options.gfm,
|
|
58
|
+
frontmatter: false,
|
|
59
|
+
toc: options.toc,
|
|
60
|
+
tocMaxDepth: options.tocMaxDepth,
|
|
61
|
+
footnotes: true,
|
|
62
|
+
tables: true,
|
|
63
|
+
taskLists: true,
|
|
64
|
+
strikethrough: true,
|
|
65
|
+
highlight: false,
|
|
66
|
+
highlightTheme: "github-dark",
|
|
67
|
+
mermaid: false,
|
|
68
|
+
ogImage: false,
|
|
69
|
+
ogImageOptions: {},
|
|
70
|
+
transformers: [],
|
|
71
|
+
docs: false,
|
|
72
|
+
search: {
|
|
73
|
+
enabled: false,
|
|
74
|
+
limit: 10,
|
|
75
|
+
prefix: true,
|
|
76
|
+
placeholder: "Search...",
|
|
77
|
+
hotkey: "k"
|
|
78
|
+
}
|
|
79
|
+
})).html, islands), usedComponents, islands, frontmatter, options, id),
|
|
80
|
+
map: null,
|
|
81
|
+
usedComponents,
|
|
82
|
+
frontmatter
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function createIslandMarker(islandId) {
|
|
86
|
+
return `${ISLAND_MARKER_PREFIX}${islandId}${ISLAND_MARKER_SUFFIX}`;
|
|
87
|
+
}
|
|
88
|
+
function collectFenceRanges(content) {
|
|
89
|
+
const ranges = [];
|
|
90
|
+
let inFence = false;
|
|
91
|
+
let fenceChar = "";
|
|
92
|
+
let fenceLength = 0;
|
|
93
|
+
let fenceStart = 0;
|
|
94
|
+
let pos = 0;
|
|
95
|
+
while (pos < content.length) {
|
|
96
|
+
const lineEnd = content.indexOf("\n", pos);
|
|
97
|
+
const next = lineEnd === -1 ? content.length : lineEnd + 1;
|
|
98
|
+
const fenceMatch = content.slice(pos, lineEnd === -1 ? content.length : lineEnd).match(/^\s{0,3}([`~]{3,})/);
|
|
99
|
+
if (fenceMatch) {
|
|
100
|
+
const marker = fenceMatch[1];
|
|
101
|
+
if (!inFence) {
|
|
102
|
+
inFence = true;
|
|
103
|
+
fenceChar = marker[0];
|
|
104
|
+
fenceLength = marker.length;
|
|
105
|
+
fenceStart = pos;
|
|
106
|
+
} else if (marker[0] === fenceChar && marker.length >= fenceLength) {
|
|
107
|
+
inFence = false;
|
|
108
|
+
ranges.push({
|
|
109
|
+
start: fenceStart,
|
|
110
|
+
end: next
|
|
111
|
+
});
|
|
112
|
+
fenceChar = "";
|
|
113
|
+
fenceLength = 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
pos = next;
|
|
117
|
+
}
|
|
118
|
+
if (inFence) ranges.push({
|
|
119
|
+
start: fenceStart,
|
|
120
|
+
end: content.length
|
|
121
|
+
});
|
|
122
|
+
return ranges;
|
|
123
|
+
}
|
|
124
|
+
function isInRanges(start, end, ranges) {
|
|
125
|
+
for (const range of ranges) if (start < range.end && end > range.start) return true;
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
function injectIslandMarkers(html, islands) {
|
|
129
|
+
let output = html;
|
|
130
|
+
for (const island of islands) {
|
|
131
|
+
const marker = createIslandMarker(island.id);
|
|
132
|
+
const propsAttr = Object.keys(island.props).length > 0 ? ` data-ox-props='${JSON.stringify(island.props).replace(/'/g, "'")}'` : "";
|
|
133
|
+
const contentAttr = island.content ? ` data-ox-content='${island.content.replace(/'/g, "'")}'` : "";
|
|
134
|
+
const attrs = `data-ox-island="${island.name}"${propsAttr}${contentAttr}`;
|
|
135
|
+
output = output.replaceAll(`<p>${marker}</p>`, `<div ${attrs}></div>`);
|
|
136
|
+
output = output.replaceAll(marker, `<span ${attrs}></span>`);
|
|
137
|
+
}
|
|
138
|
+
return output;
|
|
75
139
|
}
|
|
76
140
|
function extractFrontmatter(content) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
141
|
+
const match = /^---\n([\s\S]*?)\n---\n/.exec(content);
|
|
142
|
+
if (!match) return {
|
|
143
|
+
content,
|
|
144
|
+
frontmatter: {}
|
|
145
|
+
};
|
|
146
|
+
const frontmatterStr = match[1];
|
|
147
|
+
const frontmatter = {};
|
|
148
|
+
for (const line of frontmatterStr.split("\n")) {
|
|
149
|
+
const colonIndex = line.indexOf(":");
|
|
150
|
+
if (colonIndex > 0) {
|
|
151
|
+
const key = line.slice(0, colonIndex).trim();
|
|
152
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
153
|
+
try {
|
|
154
|
+
value = JSON.parse(value);
|
|
155
|
+
} catch {
|
|
156
|
+
if (typeof value === "string" && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
|
|
157
|
+
}
|
|
158
|
+
frontmatter[key] = value;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
content: content.slice(match[0].length),
|
|
163
|
+
frontmatter
|
|
164
|
+
};
|
|
100
165
|
}
|
|
101
166
|
function parseProps(propsString) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
167
|
+
const props = {};
|
|
168
|
+
if (!propsString) return props;
|
|
169
|
+
PROP_REGEX.lastIndex = 0;
|
|
170
|
+
let match;
|
|
171
|
+
while ((match = PROP_REGEX.exec(propsString)) !== null) {
|
|
172
|
+
const [, name, doubleQuoted, singleQuoted, braceValue, bracketValue] = match;
|
|
173
|
+
if (name) if (doubleQuoted !== void 0) props[name] = doubleQuoted;
|
|
174
|
+
else if (singleQuoted !== void 0) props[name] = singleQuoted;
|
|
175
|
+
else if (braceValue !== void 0) try {
|
|
176
|
+
props[name] = JSON.parse(braceValue);
|
|
177
|
+
} catch {
|
|
178
|
+
props[name] = braceValue;
|
|
179
|
+
}
|
|
180
|
+
else if (bracketValue !== void 0) try {
|
|
181
|
+
props[name] = JSON.parse(`[${bracketValue}]`);
|
|
182
|
+
} catch {
|
|
183
|
+
props[name] = bracketValue;
|
|
184
|
+
}
|
|
185
|
+
else props[name] = true;
|
|
186
|
+
}
|
|
187
|
+
return props;
|
|
188
|
+
}
|
|
189
|
+
function generateReactModule(content, usedComponents, islands, frontmatter, options, id) {
|
|
190
|
+
const mdDir = path.dirname(id);
|
|
191
|
+
const root = options.root || process.cwd();
|
|
192
|
+
const imports = usedComponents.map((name) => {
|
|
193
|
+
const componentPath = options.components[name];
|
|
194
|
+
if (!componentPath) return "";
|
|
195
|
+
const absolutePath = path.resolve(root, componentPath.replace(/^\.\//, ""));
|
|
196
|
+
const relativePath = path.relative(mdDir, absolutePath).replace(/\\/g, "/");
|
|
197
|
+
return `import ${name} from '${relativePath.startsWith(".") ? relativePath : "./" + relativePath}';`;
|
|
198
|
+
}).filter(Boolean).join("\n");
|
|
199
|
+
const componentMap = usedComponents.map((name) => ` ${name},`).join("\n");
|
|
200
|
+
if (islands.length === 0) return `
|
|
201
|
+
import React, { createElement } from 'react';
|
|
202
|
+
|
|
203
|
+
export const frontmatter = ${JSON.stringify(frontmatter)};
|
|
204
|
+
|
|
205
|
+
const rawHtml = ${JSON.stringify(content)};
|
|
206
|
+
|
|
207
|
+
export default function MarkdownContent() {
|
|
208
|
+
return createElement('div', {
|
|
209
|
+
className: 'ox-content',
|
|
210
|
+
dangerouslySetInnerHTML: { __html: rawHtml },
|
|
211
|
+
});
|
|
120
212
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (!componentPath) return "";
|
|
127
|
-
const absolutePath = path.resolve(root, componentPath.replace(/^\.\//, ""));
|
|
128
|
-
const relativePath = path.relative(mdDir, absolutePath).replace(/\\/g, "/");
|
|
129
|
-
const importPath = relativePath.startsWith(".") ? relativePath : "./" + relativePath;
|
|
130
|
-
return `import ${name} from '${importPath}';`;
|
|
131
|
-
}).filter(Boolean).join("\n");
|
|
132
|
-
return `
|
|
133
|
-
import React, { useState, useEffect, createElement } from 'react';
|
|
213
|
+
`;
|
|
214
|
+
return `
|
|
215
|
+
import React, { useEffect, useRef, createElement } from 'react';
|
|
216
|
+
import { createRoot } from 'react-dom/client';
|
|
217
|
+
import { initIslands } from '@ox-content/islands';
|
|
134
218
|
${imports}
|
|
135
219
|
|
|
136
|
-
const frontmatter = ${JSON.stringify(frontmatter)};
|
|
220
|
+
export const frontmatter = ${JSON.stringify(frontmatter)};
|
|
221
|
+
|
|
137
222
|
const rawHtml = ${JSON.stringify(content)};
|
|
138
|
-
const
|
|
223
|
+
const components = {
|
|
224
|
+
${componentMap}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
function createReactHydrate() {
|
|
228
|
+
const mountedRoots = [];
|
|
229
|
+
|
|
230
|
+
return (element, props) => {
|
|
231
|
+
const componentName = element.dataset.oxIsland;
|
|
232
|
+
const Component = components[componentName];
|
|
233
|
+
if (!Component) return;
|
|
139
234
|
|
|
140
|
-
const
|
|
235
|
+
const islandContent = element.dataset.oxContent || element.innerHTML;
|
|
236
|
+
const vnode = islandContent
|
|
237
|
+
? createElement(
|
|
238
|
+
Component,
|
|
239
|
+
props,
|
|
240
|
+
createElement('div', { dangerouslySetInnerHTML: { __html: islandContent } })
|
|
241
|
+
)
|
|
242
|
+
: createElement(Component, props);
|
|
243
|
+
|
|
244
|
+
const root = createRoot(element);
|
|
245
|
+
root.render(vnode);
|
|
246
|
+
mountedRoots.push(root);
|
|
247
|
+
|
|
248
|
+
return () => root.unmount();
|
|
249
|
+
};
|
|
250
|
+
}
|
|
141
251
|
|
|
142
252
|
export default function MarkdownContent() {
|
|
143
|
-
const
|
|
253
|
+
const containerRef = useRef(null);
|
|
144
254
|
|
|
145
255
|
useEffect(() => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (!mounted) {
|
|
150
|
-
return createElement('div', {
|
|
151
|
-
className: 'ox-content',
|
|
152
|
-
dangerouslySetInnerHTML: { __html: rawHtml }
|
|
256
|
+
if (!containerRef.current) return;
|
|
257
|
+
const controller = initIslands(createReactHydrate(), {
|
|
258
|
+
selector: '.ox-content [data-ox-island]',
|
|
153
259
|
});
|
|
154
|
-
|
|
260
|
+
return () => controller.destroy();
|
|
261
|
+
}, []);
|
|
155
262
|
|
|
156
|
-
return createElement('div', {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
);
|
|
263
|
+
return createElement('div', {
|
|
264
|
+
className: 'ox-content',
|
|
265
|
+
ref: containerRef,
|
|
266
|
+
dangerouslySetInnerHTML: { __html: rawHtml },
|
|
267
|
+
});
|
|
162
268
|
}
|
|
163
|
-
|
|
164
|
-
export { frontmatter };
|
|
165
269
|
`;
|
|
166
270
|
}
|
|
167
271
|
|
|
168
|
-
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/environment.ts
|
|
169
274
|
function createReactMarkdownEnvironment(mode, options) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
};
|
|
275
|
+
const isSSR = mode === "ssr";
|
|
276
|
+
return {
|
|
277
|
+
build: {
|
|
278
|
+
outDir: isSSR ? `${options.outDir}/.ox-content/ssr` : `${options.outDir}/.ox-content/client`,
|
|
279
|
+
ssr: isSSR,
|
|
280
|
+
rollupOptions: { output: {
|
|
281
|
+
format: "esm",
|
|
282
|
+
entryFileNames: isSSR ? "[name].js" : "[name].[hash].js"
|
|
283
|
+
} },
|
|
284
|
+
...isSSR && {
|
|
285
|
+
target: "node18",
|
|
286
|
+
minify: false
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
resolve: { conditions: isSSR ? ["node", "import"] : ["browser", "import"] },
|
|
290
|
+
optimizeDeps: {
|
|
291
|
+
include: isSSR ? [] : ["react", "react-dom"],
|
|
292
|
+
exclude: ["@ox-content/vite-plugin", "@ox-content/vite-plugin-react"]
|
|
293
|
+
}
|
|
294
|
+
};
|
|
191
295
|
}
|
|
192
296
|
|
|
193
|
-
|
|
194
|
-
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region src/index.ts
|
|
299
|
+
/**
|
|
300
|
+
* Vite Plugin for Ox Content React Integration
|
|
301
|
+
*
|
|
302
|
+
* Uses Vite's Environment API to enable embedding React components in Markdown.
|
|
303
|
+
*/
|
|
304
|
+
/**
|
|
305
|
+
* Creates the Ox Content React integration plugin.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```ts
|
|
309
|
+
* // vite.config.ts
|
|
310
|
+
* import { defineConfig } from 'vite';
|
|
311
|
+
* import react from '@vitejs/plugin-react';
|
|
312
|
+
* import { oxContentReact } from 'vite-plugin-ox-content-react';
|
|
313
|
+
*
|
|
314
|
+
* export default defineConfig({
|
|
315
|
+
* plugins: [
|
|
316
|
+
* react(),
|
|
317
|
+
* oxContentReact({
|
|
318
|
+
* srcDir: 'docs',
|
|
319
|
+
* components: {
|
|
320
|
+
* Counter: './src/components/Counter.tsx',
|
|
321
|
+
* },
|
|
322
|
+
* }),
|
|
323
|
+
* ],
|
|
324
|
+
* });
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
195
327
|
function oxContentReact(options = {}) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (mdModules.length > 0) {
|
|
281
|
-
server.ws.send({
|
|
282
|
-
type: "custom",
|
|
283
|
-
event: "ox-content:react-update",
|
|
284
|
-
data: { file }
|
|
285
|
-
});
|
|
286
|
-
return [...modules, ...mdModules];
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
return modules;
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
const basePlugins = oxContent(options);
|
|
293
|
-
const environmentPlugin = basePlugins.find((p) => p.name === "ox-content:environment");
|
|
294
|
-
return [
|
|
295
|
-
reactTransformPlugin,
|
|
296
|
-
reactEnvironmentPlugin,
|
|
297
|
-
reactHmrPlugin,
|
|
298
|
-
...environmentPlugin ? [environmentPlugin] : []
|
|
299
|
-
];
|
|
328
|
+
const resolved = resolveReactOptions(options);
|
|
329
|
+
let componentMap = /* @__PURE__ */ new Map();
|
|
330
|
+
let config;
|
|
331
|
+
if (typeof options.components === "object" && !Array.isArray(options.components)) componentMap = new Map(Object.entries(options.components));
|
|
332
|
+
const reactTransformPlugin = {
|
|
333
|
+
name: "ox-content:react-transform",
|
|
334
|
+
enforce: "pre",
|
|
335
|
+
async configResolved(resolvedConfig) {
|
|
336
|
+
config = resolvedConfig;
|
|
337
|
+
const componentsOption = options.components;
|
|
338
|
+
if (componentsOption) {
|
|
339
|
+
const resolvedComponents = await resolveComponentsGlob(componentsOption, config.root);
|
|
340
|
+
componentMap = new Map(Object.entries(resolvedComponents));
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
async transform(code, id) {
|
|
344
|
+
if (!id.endsWith(".md")) return null;
|
|
345
|
+
const result = await transformMarkdownWithReact(code, id, {
|
|
346
|
+
...resolved,
|
|
347
|
+
components: Object.fromEntries(componentMap),
|
|
348
|
+
root: config.root
|
|
349
|
+
});
|
|
350
|
+
return {
|
|
351
|
+
code: result.code,
|
|
352
|
+
map: result.map
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
const reactEnvironmentPlugin = {
|
|
357
|
+
name: "ox-content:react-environment",
|
|
358
|
+
config() {
|
|
359
|
+
const envOptions = {
|
|
360
|
+
...resolved,
|
|
361
|
+
components: Object.fromEntries(componentMap)
|
|
362
|
+
};
|
|
363
|
+
return { environments: {
|
|
364
|
+
oxcontent_ssr: createReactMarkdownEnvironment("ssr", envOptions),
|
|
365
|
+
oxcontent_client: createReactMarkdownEnvironment("client", envOptions)
|
|
366
|
+
} };
|
|
367
|
+
},
|
|
368
|
+
resolveId(id) {
|
|
369
|
+
if (id === "virtual:ox-content-react/runtime") return "\0virtual:ox-content-react/runtime";
|
|
370
|
+
if (id === "virtual:ox-content-react/components") return "\0virtual:ox-content-react/components";
|
|
371
|
+
return null;
|
|
372
|
+
},
|
|
373
|
+
load(id) {
|
|
374
|
+
if (id === "\0virtual:ox-content-react/runtime") return generateRuntimeModule();
|
|
375
|
+
if (id === "\0virtual:ox-content-react/components") return generateComponentsModule(componentMap);
|
|
376
|
+
return null;
|
|
377
|
+
},
|
|
378
|
+
applyToEnvironment(environment) {
|
|
379
|
+
return [
|
|
380
|
+
"oxcontent_ssr",
|
|
381
|
+
"oxcontent_client",
|
|
382
|
+
"client",
|
|
383
|
+
"ssr"
|
|
384
|
+
].includes(environment.name);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
const reactHmrPlugin = {
|
|
388
|
+
name: "ox-content:react-hmr",
|
|
389
|
+
apply: "serve",
|
|
390
|
+
handleHotUpdate({ file, server, modules }) {
|
|
391
|
+
if (Array.from(componentMap.values()).some((path) => file.endsWith(path.replace(/^\.\//, "")))) {
|
|
392
|
+
const mdModules = Array.from(server.moduleGraph.idToModuleMap.values()).filter((mod) => mod.file?.endsWith(".md"));
|
|
393
|
+
if (mdModules.length > 0) {
|
|
394
|
+
server.ws.send({
|
|
395
|
+
type: "custom",
|
|
396
|
+
event: "ox-content:react-update",
|
|
397
|
+
data: { file }
|
|
398
|
+
});
|
|
399
|
+
return [...modules, ...mdModules];
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return modules;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const environmentPlugin = oxContent$1(options).find((p) => p.name === "ox-content:environment");
|
|
406
|
+
return [
|
|
407
|
+
reactTransformPlugin,
|
|
408
|
+
reactEnvironmentPlugin,
|
|
409
|
+
reactHmrPlugin,
|
|
410
|
+
...environmentPlugin ? [environmentPlugin] : []
|
|
411
|
+
];
|
|
300
412
|
}
|
|
301
413
|
function resolveReactOptions(options) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
414
|
+
return {
|
|
415
|
+
srcDir: options.srcDir ?? "docs",
|
|
416
|
+
outDir: options.outDir ?? "dist",
|
|
417
|
+
base: options.base ?? "/",
|
|
418
|
+
gfm: options.gfm ?? true,
|
|
419
|
+
frontmatter: options.frontmatter ?? true,
|
|
420
|
+
toc: options.toc ?? true,
|
|
421
|
+
tocMaxDepth: options.tocMaxDepth ?? 3,
|
|
422
|
+
jsxRuntime: options.jsxRuntime ?? "automatic"
|
|
423
|
+
};
|
|
312
424
|
}
|
|
313
425
|
function generateRuntimeModule() {
|
|
314
|
-
|
|
426
|
+
return `
|
|
315
427
|
import React, { useState, useEffect } from 'react';
|
|
316
428
|
|
|
317
429
|
export function OxContentRenderer({ content, components = {} }) {
|
|
@@ -323,7 +435,7 @@ export function OxContentRenderer({ content, components = {} }) {
|
|
|
323
435
|
|
|
324
436
|
if (!content) return null;
|
|
325
437
|
|
|
326
|
-
const { html, frontmatter,
|
|
438
|
+
const { html, frontmatter, islands } = content;
|
|
327
439
|
|
|
328
440
|
if (!mounted) {
|
|
329
441
|
return React.createElement('div', {
|
|
@@ -333,10 +445,10 @@ export function OxContentRenderer({ content, components = {} }) {
|
|
|
333
445
|
}
|
|
334
446
|
|
|
335
447
|
return React.createElement('div', { className: 'ox-content' },
|
|
336
|
-
|
|
337
|
-
const Component = components[
|
|
448
|
+
islands.map((island) => {
|
|
449
|
+
const Component = components[island.name];
|
|
338
450
|
return Component
|
|
339
|
-
? React.createElement(Component, { key:
|
|
451
|
+
? React.createElement(Component, { key: island.id, ...island.props })
|
|
340
452
|
: null;
|
|
341
453
|
})
|
|
342
454
|
);
|
|
@@ -348,13 +460,13 @@ export function useOxContent() {
|
|
|
348
460
|
`;
|
|
349
461
|
}
|
|
350
462
|
function generateComponentsModule(componentMap) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
463
|
+
const imports = [];
|
|
464
|
+
const exports = [];
|
|
465
|
+
componentMap.forEach((path, name) => {
|
|
466
|
+
imports.push(`import ${name} from '${path}';`);
|
|
467
|
+
exports.push(` ${name},`);
|
|
468
|
+
});
|
|
469
|
+
return `
|
|
358
470
|
${imports.join("\n")}
|
|
359
471
|
|
|
360
472
|
export const components = {
|
|
@@ -365,65 +477,48 @@ export default components;
|
|
|
365
477
|
`;
|
|
366
478
|
}
|
|
367
479
|
async function resolveComponentsGlob(componentsOption, root) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
result[componentName] = relativePath;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
return result;
|
|
480
|
+
if (typeof componentsOption === "object" && !Array.isArray(componentsOption)) return componentsOption;
|
|
481
|
+
const patterns = Array.isArray(componentsOption) ? componentsOption : [componentsOption];
|
|
482
|
+
const result = {};
|
|
483
|
+
for (const pattern of patterns) {
|
|
484
|
+
const files = await globFiles(pattern, root);
|
|
485
|
+
for (const file of files) {
|
|
486
|
+
const componentName = toPascalCase(path.basename(file, path.extname(file)));
|
|
487
|
+
result[componentName] = "./" + path.relative(root, file).replace(/\\/g, "/");
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return result;
|
|
383
491
|
}
|
|
384
492
|
async function globFiles(pattern, root) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
await walkDir(baseDir, files, ext);
|
|
402
|
-
} else {
|
|
403
|
-
const entries = await fs.promises.readdir(baseDir, { withFileTypes: true });
|
|
404
|
-
for (const entry of entries) {
|
|
405
|
-
if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
406
|
-
files.push(path2.join(baseDir, entry.name));
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return files;
|
|
493
|
+
const files = [];
|
|
494
|
+
if (!pattern.includes("*")) {
|
|
495
|
+
const fullPath = path.resolve(root, pattern);
|
|
496
|
+
if (fs.existsSync(fullPath)) files.push(fullPath);
|
|
497
|
+
return files;
|
|
498
|
+
}
|
|
499
|
+
const parts = pattern.split("*");
|
|
500
|
+
const baseDir = path.resolve(root, parts[0]);
|
|
501
|
+
const ext = parts[1] || "";
|
|
502
|
+
if (!fs.existsSync(baseDir)) return files;
|
|
503
|
+
if (pattern.includes("**")) await walkDir(baseDir, files, ext);
|
|
504
|
+
else {
|
|
505
|
+
const entries = await fs.promises.readdir(baseDir, { withFileTypes: true });
|
|
506
|
+
for (const entry of entries) if (entry.isFile() && entry.name.endsWith(ext)) files.push(path.join(baseDir, entry.name));
|
|
507
|
+
}
|
|
508
|
+
return files;
|
|
411
509
|
}
|
|
412
510
|
async function walkDir(dir, files, ext) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
files.push(fullPath);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
511
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
512
|
+
for (const entry of entries) {
|
|
513
|
+
const fullPath = path.join(dir, entry.name);
|
|
514
|
+
if (entry.isDirectory()) await walkDir(fullPath, files, ext);
|
|
515
|
+
else if (entry.isFile() && entry.name.endsWith(ext)) files.push(fullPath);
|
|
516
|
+
}
|
|
422
517
|
}
|
|
423
518
|
function toPascalCase(str) {
|
|
424
|
-
|
|
519
|
+
return str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase());
|
|
425
520
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
521
|
+
|
|
522
|
+
//#endregion
|
|
523
|
+
export { oxContent, oxContentReact };
|
|
524
|
+
//# sourceMappingURL=index.js.map
|