@ox-content/vite-plugin-react 0.0.1-alpha.0 → 0.3.0-alpha.13

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.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 { transformMarkdown as baseTransformMarkdown } from "vite-plugin-ox-content";
9
- var COMPONENT_REGEX = /<([A-Z][a-zA-Z0-9]*)\s*([^>]*?)\s*(?:\/>|>(?:[\s\S]*?)<\/\1>)/g;
10
- var PROP_REGEX = /([a-zA-Z0-9-]+)(?:=(?:"([^"]*)"|'([^']*)'|{([^}]*)}))?/g;
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
- const components = options.components;
13
- const usedComponents = [];
14
- const slots = [];
15
- let slotIndex = 0;
16
- const { content: markdownContent, frontmatter } = extractFrontmatter(code);
17
- let processedContent = markdownContent;
18
- let match;
19
- while ((match = COMPONENT_REGEX.exec(markdownContent)) !== null) {
20
- const [fullMatch, componentName, propsString] = match;
21
- if (componentName in components) {
22
- if (!usedComponents.includes(componentName)) {
23
- usedComponents.push(componentName);
24
- }
25
- const props = parseProps(propsString);
26
- const slotId = `__ox_slot_${slotIndex++}__`;
27
- slots.push({
28
- name: componentName,
29
- props,
30
- position: match.index,
31
- id: slotId
32
- });
33
- processedContent = processedContent.replace(
34
- fullMatch,
35
- `<div data-ox-slot="${slotId}"></div>`
36
- );
37
- }
38
- }
39
- const transformed = await baseTransformMarkdown(processedContent, id, {
40
- srcDir: options.srcDir,
41
- outDir: options.outDir,
42
- base: options.base,
43
- ssg: { enabled: false, extension: ".html", clean: false, bare: false, generateOgImage: false },
44
- gfm: options.gfm,
45
- frontmatter: false,
46
- toc: options.toc,
47
- tocMaxDepth: options.tocMaxDepth,
48
- footnotes: true,
49
- tables: true,
50
- taskLists: true,
51
- strikethrough: true,
52
- highlight: false,
53
- highlightTheme: "github-dark",
54
- mermaid: false,
55
- ogImage: false,
56
- ogImageOptions: {},
57
- transformers: [],
58
- docs: false,
59
- search: { enabled: false, limit: 10, prefix: true, placeholder: "Search...", hotkey: "k" }
60
- });
61
- const jsxCode = generateReactModule(
62
- transformed.html,
63
- usedComponents,
64
- slots,
65
- frontmatter,
66
- options,
67
- id
68
- );
69
- return {
70
- code: jsxCode,
71
- map: null,
72
- usedComponents,
73
- frontmatter
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, "&#39;")}'` : "";
133
+ const contentAttr = island.content ? ` data-ox-content='${island.content.replace(/'/g, "&#39;")}'` : "";
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
- const frontmatterRegex = /^---\n([\s\S]*?)\n---\n/;
78
- const match = frontmatterRegex.exec(content);
79
- if (!match) {
80
- return { content, frontmatter: {} };
81
- }
82
- const frontmatterStr = match[1];
83
- const frontmatter = {};
84
- for (const line of frontmatterStr.split("\n")) {
85
- const colonIndex = line.indexOf(":");
86
- if (colonIndex > 0) {
87
- const key = line.slice(0, colonIndex).trim();
88
- let value = line.slice(colonIndex + 1).trim();
89
- try {
90
- value = JSON.parse(value);
91
- } catch {
92
- if (typeof value === "string" && value.startsWith('"') && value.endsWith('"')) {
93
- value = value.slice(1, -1);
94
- }
95
- }
96
- frontmatter[key] = value;
97
- }
98
- }
99
- return { content: content.slice(match[0].length), frontmatter };
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
- const props = {};
103
- if (!propsString) return props;
104
- let match;
105
- while ((match = PROP_REGEX.exec(propsString)) !== null) {
106
- const [, name, doubleQuoted, singleQuoted, braceValue] = match;
107
- if (name) {
108
- if (doubleQuoted !== void 0) props[name] = doubleQuoted;
109
- else if (singleQuoted !== void 0) props[name] = singleQuoted;
110
- else if (braceValue !== void 0) {
111
- try {
112
- props[name] = JSON.parse(braceValue);
113
- } catch {
114
- props[name] = braceValue;
115
- }
116
- } else props[name] = true;
117
- }
118
- }
119
- return props;
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
- function generateReactModule(content, usedComponents, slots, frontmatter, options, id) {
122
- const mdDir = path.dirname(id);
123
- const root = options.root || process.cwd();
124
- const imports = usedComponents.map((name) => {
125
- const componentPath = options.components[name];
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 slots = ${JSON.stringify(slots)};
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 components = { ${usedComponents.join(", ")} };
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 [mounted, setMounted] = useState(false);
253
+ const containerRef = useRef(null);
144
254
 
145
255
  useEffect(() => {
146
- setMounted(true);
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', { className: 'ox-content' },
157
- slots.map((slot) => {
158
- const Component = components[slot.name];
159
- return Component ? createElement(Component, { key: slot.id, ...slot.props }) : null;
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
- // src/environment.ts
272
+ //#endregion
273
+ //#region src/environment.ts
169
274
  function createReactMarkdownEnvironment(mode, options) {
170
- const isSSR = mode === "ssr";
171
- return {
172
- build: {
173
- outDir: isSSR ? `${options.outDir}/.ox-content/ssr` : `${options.outDir}/.ox-content/client`,
174
- ssr: isSSR,
175
- rollupOptions: {
176
- output: {
177
- format: "esm",
178
- entryFileNames: isSSR ? "[name].js" : "[name].[hash].js"
179
- }
180
- },
181
- ...isSSR && { target: "node18", minify: false }
182
- },
183
- resolve: {
184
- conditions: isSSR ? ["node", "import"] : ["browser", "import"]
185
- },
186
- optimizeDeps: {
187
- include: isSSR ? [] : ["react", "react-dom"],
188
- exclude: ["vite-plugin-ox-content", "vite-plugin-ox-content-react"]
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
- // src/index.ts
194
- import { oxContent as oxContent2 } from "vite-plugin-ox-content";
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
- const resolved = resolveReactOptions(options);
197
- let componentMap = /* @__PURE__ */ new Map();
198
- let config;
199
- if (typeof options.components === "object" && !Array.isArray(options.components)) {
200
- componentMap = new Map(Object.entries(options.components));
201
- }
202
- const reactTransformPlugin = {
203
- name: "ox-content:react-transform",
204
- enforce: "pre",
205
- async configResolved(resolvedConfig) {
206
- config = resolvedConfig;
207
- const componentsOption = options.components;
208
- if (componentsOption) {
209
- const resolvedComponents = await resolveComponentsGlob(
210
- componentsOption,
211
- config.root
212
- );
213
- componentMap = new Map(Object.entries(resolvedComponents));
214
- }
215
- },
216
- async transform(code, id) {
217
- if (!id.endsWith(".md")) {
218
- return null;
219
- }
220
- const result = await transformMarkdownWithReact(code, id, {
221
- ...resolved,
222
- components: Object.fromEntries(componentMap),
223
- root: config.root
224
- });
225
- return {
226
- code: result.code,
227
- map: result.map
228
- };
229
- }
230
- };
231
- const reactEnvironmentPlugin = {
232
- name: "ox-content:react-environment",
233
- config() {
234
- const envOptions = {
235
- ...resolved,
236
- components: Object.fromEntries(componentMap)
237
- };
238
- return {
239
- environments: {
240
- oxcontent_ssr: createReactMarkdownEnvironment("ssr", envOptions),
241
- oxcontent_client: createReactMarkdownEnvironment("client", envOptions)
242
- }
243
- };
244
- },
245
- resolveId(id) {
246
- if (id === "virtual:ox-content-react/runtime") {
247
- return "\0virtual:ox-content-react/runtime";
248
- }
249
- if (id === "virtual:ox-content-react/components") {
250
- return "\0virtual:ox-content-react/components";
251
- }
252
- return null;
253
- },
254
- load(id) {
255
- if (id === "\0virtual:ox-content-react/runtime") {
256
- return generateRuntimeModule();
257
- }
258
- if (id === "\0virtual:ox-content-react/components") {
259
- return generateComponentsModule(componentMap);
260
- }
261
- return null;
262
- },
263
- applyToEnvironment(environment) {
264
- return ["oxcontent_ssr", "oxcontent_client", "client", "ssr"].includes(
265
- environment.name
266
- );
267
- }
268
- };
269
- const reactHmrPlugin = {
270
- name: "ox-content:react-hmr",
271
- apply: "serve",
272
- handleHotUpdate({ file, server, modules }) {
273
- const isComponent = Array.from(componentMap.values()).some(
274
- (path3) => file.endsWith(path3.replace(/^\.\//, ""))
275
- );
276
- if (isComponent) {
277
- const mdModules = Array.from(
278
- server.moduleGraph.idToModuleMap.values()
279
- ).filter((mod) => mod.file?.endsWith(".md"));
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
- return {
303
- srcDir: options.srcDir ?? "docs",
304
- outDir: options.outDir ?? "dist",
305
- base: options.base ?? "/",
306
- gfm: options.gfm ?? true,
307
- frontmatter: options.frontmatter ?? true,
308
- toc: options.toc ?? true,
309
- tocMaxDepth: options.tocMaxDepth ?? 3,
310
- jsxRuntime: options.jsxRuntime ?? "automatic"
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
- return `
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, slots } = content;
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
- slots.map((slot) => {
337
- const Component = components[slot.name];
448
+ islands.map((island) => {
449
+ const Component = components[island.name];
338
450
  return Component
339
- ? React.createElement(Component, { key: slot.id, ...slot.props })
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
- const imports = [];
352
- const exports = [];
353
- componentMap.forEach((path3, name) => {
354
- imports.push(`import ${name} from '${path3}';`);
355
- exports.push(` ${name},`);
356
- });
357
- return `
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
- if (typeof componentsOption === "object" && !Array.isArray(componentsOption)) {
369
- return componentsOption;
370
- }
371
- const patterns = Array.isArray(componentsOption) ? componentsOption : [componentsOption];
372
- const result = {};
373
- for (const pattern of patterns) {
374
- const files = await globFiles(pattern, root);
375
- for (const file of files) {
376
- const baseName = path2.basename(file, path2.extname(file));
377
- const componentName = toPascalCase(baseName);
378
- const relativePath = "./" + path2.relative(root, file).replace(/\\/g, "/");
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
- const files = [];
386
- const isGlob = pattern.includes("*");
387
- if (!isGlob) {
388
- const fullPath = path2.resolve(root, pattern);
389
- if (fs.existsSync(fullPath)) {
390
- files.push(fullPath);
391
- }
392
- return files;
393
- }
394
- const parts = pattern.split("*");
395
- const baseDir = path2.resolve(root, parts[0]);
396
- const ext = parts[1] || "";
397
- if (!fs.existsSync(baseDir)) {
398
- return files;
399
- }
400
- if (pattern.includes("**")) {
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
- const entries = await fs.promises.readdir(dir, { withFileTypes: true });
414
- for (const entry of entries) {
415
- const fullPath = path2.join(dir, entry.name);
416
- if (entry.isDirectory()) {
417
- await walkDir(fullPath, files, ext);
418
- } else if (entry.isFile() && entry.name.endsWith(ext)) {
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
- return str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase());
519
+ return str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase());
425
520
  }
426
- export {
427
- oxContent2 as oxContent,
428
- oxContentReact
429
- };
521
+
522
+ //#endregion
523
+ export { oxContent, oxContentReact };
524
+ //# sourceMappingURL=index.js.map