@ox-content/vite-plugin-react 0.0.1-alpha.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 ADDED
@@ -0,0 +1,465 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ oxContent: () => import_vite_plugin_ox_content3.oxContent,
34
+ oxContentReact: () => oxContentReact
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+ var fs = __toESM(require("fs"), 1);
38
+ var path2 = __toESM(require("path"), 1);
39
+ var import_vite_plugin_ox_content2 = require("vite-plugin-ox-content");
40
+
41
+ // src/transform.ts
42
+ var path = __toESM(require("path"), 1);
43
+ var import_vite_plugin_ox_content = require("vite-plugin-ox-content");
44
+ var COMPONENT_REGEX = /<([A-Z][a-zA-Z0-9]*)\s*([^>]*?)\s*(?:\/>|>(?:[\s\S]*?)<\/\1>)/g;
45
+ var PROP_REGEX = /([a-zA-Z0-9-]+)(?:=(?:"([^"]*)"|'([^']*)'|{([^}]*)}))?/g;
46
+ async function transformMarkdownWithReact(code, id, options) {
47
+ const components = options.components;
48
+ const usedComponents = [];
49
+ const slots = [];
50
+ let slotIndex = 0;
51
+ const { content: markdownContent, frontmatter } = extractFrontmatter(code);
52
+ let processedContent = markdownContent;
53
+ let match;
54
+ while ((match = COMPONENT_REGEX.exec(markdownContent)) !== null) {
55
+ const [fullMatch, componentName, propsString] = match;
56
+ if (componentName in components) {
57
+ if (!usedComponents.includes(componentName)) {
58
+ usedComponents.push(componentName);
59
+ }
60
+ const props = parseProps(propsString);
61
+ const slotId = `__ox_slot_${slotIndex++}__`;
62
+ slots.push({
63
+ name: componentName,
64
+ props,
65
+ position: match.index,
66
+ id: slotId
67
+ });
68
+ processedContent = processedContent.replace(
69
+ fullMatch,
70
+ `<div data-ox-slot="${slotId}"></div>`
71
+ );
72
+ }
73
+ }
74
+ const transformed = await (0, import_vite_plugin_ox_content.transformMarkdown)(processedContent, id, {
75
+ srcDir: options.srcDir,
76
+ outDir: options.outDir,
77
+ base: options.base,
78
+ ssg: { enabled: false, extension: ".html", clean: false, bare: false, generateOgImage: false },
79
+ gfm: options.gfm,
80
+ frontmatter: false,
81
+ toc: options.toc,
82
+ tocMaxDepth: options.tocMaxDepth,
83
+ footnotes: true,
84
+ tables: true,
85
+ taskLists: true,
86
+ strikethrough: true,
87
+ highlight: false,
88
+ highlightTheme: "github-dark",
89
+ mermaid: false,
90
+ ogImage: false,
91
+ ogImageOptions: {},
92
+ transformers: [],
93
+ docs: false,
94
+ search: { enabled: false, limit: 10, prefix: true, placeholder: "Search...", hotkey: "k" }
95
+ });
96
+ const jsxCode = generateReactModule(
97
+ transformed.html,
98
+ usedComponents,
99
+ slots,
100
+ frontmatter,
101
+ options,
102
+ id
103
+ );
104
+ return {
105
+ code: jsxCode,
106
+ map: null,
107
+ usedComponents,
108
+ frontmatter
109
+ };
110
+ }
111
+ function extractFrontmatter(content) {
112
+ const frontmatterRegex = /^---\n([\s\S]*?)\n---\n/;
113
+ const match = frontmatterRegex.exec(content);
114
+ if (!match) {
115
+ return { content, frontmatter: {} };
116
+ }
117
+ const frontmatterStr = match[1];
118
+ const frontmatter = {};
119
+ for (const line of frontmatterStr.split("\n")) {
120
+ const colonIndex = line.indexOf(":");
121
+ if (colonIndex > 0) {
122
+ const key = line.slice(0, colonIndex).trim();
123
+ let value = line.slice(colonIndex + 1).trim();
124
+ try {
125
+ value = JSON.parse(value);
126
+ } catch {
127
+ if (typeof value === "string" && value.startsWith('"') && value.endsWith('"')) {
128
+ value = value.slice(1, -1);
129
+ }
130
+ }
131
+ frontmatter[key] = value;
132
+ }
133
+ }
134
+ return { content: content.slice(match[0].length), frontmatter };
135
+ }
136
+ function parseProps(propsString) {
137
+ const props = {};
138
+ if (!propsString) return props;
139
+ let match;
140
+ while ((match = PROP_REGEX.exec(propsString)) !== null) {
141
+ const [, name, doubleQuoted, singleQuoted, braceValue] = match;
142
+ if (name) {
143
+ if (doubleQuoted !== void 0) props[name] = doubleQuoted;
144
+ else if (singleQuoted !== void 0) props[name] = singleQuoted;
145
+ else if (braceValue !== void 0) {
146
+ try {
147
+ props[name] = JSON.parse(braceValue);
148
+ } catch {
149
+ props[name] = braceValue;
150
+ }
151
+ } else props[name] = true;
152
+ }
153
+ }
154
+ return props;
155
+ }
156
+ function generateReactModule(content, usedComponents, slots, frontmatter, options, id) {
157
+ const mdDir = path.dirname(id);
158
+ const root = options.root || process.cwd();
159
+ const imports = usedComponents.map((name) => {
160
+ const componentPath = options.components[name];
161
+ if (!componentPath) return "";
162
+ const absolutePath = path.resolve(root, componentPath.replace(/^\.\//, ""));
163
+ const relativePath = path.relative(mdDir, absolutePath).replace(/\\/g, "/");
164
+ const importPath = relativePath.startsWith(".") ? relativePath : "./" + relativePath;
165
+ return `import ${name} from '${importPath}';`;
166
+ }).filter(Boolean).join("\n");
167
+ return `
168
+ import React, { useState, useEffect, createElement } from 'react';
169
+ ${imports}
170
+
171
+ const frontmatter = ${JSON.stringify(frontmatter)};
172
+ const rawHtml = ${JSON.stringify(content)};
173
+ const slots = ${JSON.stringify(slots)};
174
+
175
+ const components = { ${usedComponents.join(", ")} };
176
+
177
+ export default function MarkdownContent() {
178
+ const [mounted, setMounted] = useState(false);
179
+
180
+ useEffect(() => {
181
+ setMounted(true);
182
+ }, []);
183
+
184
+ if (!mounted) {
185
+ return createElement('div', {
186
+ className: 'ox-content',
187
+ dangerouslySetInnerHTML: { __html: rawHtml }
188
+ });
189
+ }
190
+
191
+ return createElement('div', { className: 'ox-content' },
192
+ slots.map((slot) => {
193
+ const Component = components[slot.name];
194
+ return Component ? createElement(Component, { key: slot.id, ...slot.props }) : null;
195
+ })
196
+ );
197
+ }
198
+
199
+ export { frontmatter };
200
+ `;
201
+ }
202
+
203
+ // src/environment.ts
204
+ function createReactMarkdownEnvironment(mode, options) {
205
+ const isSSR = mode === "ssr";
206
+ return {
207
+ build: {
208
+ outDir: isSSR ? `${options.outDir}/.ox-content/ssr` : `${options.outDir}/.ox-content/client`,
209
+ ssr: isSSR,
210
+ rollupOptions: {
211
+ output: {
212
+ format: "esm",
213
+ entryFileNames: isSSR ? "[name].js" : "[name].[hash].js"
214
+ }
215
+ },
216
+ ...isSSR && { target: "node18", minify: false }
217
+ },
218
+ resolve: {
219
+ conditions: isSSR ? ["node", "import"] : ["browser", "import"]
220
+ },
221
+ optimizeDeps: {
222
+ include: isSSR ? [] : ["react", "react-dom"],
223
+ exclude: ["vite-plugin-ox-content", "vite-plugin-ox-content-react"]
224
+ }
225
+ };
226
+ }
227
+
228
+ // src/index.ts
229
+ var import_vite_plugin_ox_content3 = require("vite-plugin-ox-content");
230
+ function oxContentReact(options = {}) {
231
+ const resolved = resolveReactOptions(options);
232
+ let componentMap = /* @__PURE__ */ new Map();
233
+ let config;
234
+ if (typeof options.components === "object" && !Array.isArray(options.components)) {
235
+ componentMap = new Map(Object.entries(options.components));
236
+ }
237
+ const reactTransformPlugin = {
238
+ name: "ox-content:react-transform",
239
+ enforce: "pre",
240
+ async configResolved(resolvedConfig) {
241
+ config = resolvedConfig;
242
+ const componentsOption = options.components;
243
+ if (componentsOption) {
244
+ const resolvedComponents = await resolveComponentsGlob(
245
+ componentsOption,
246
+ config.root
247
+ );
248
+ componentMap = new Map(Object.entries(resolvedComponents));
249
+ }
250
+ },
251
+ async transform(code, id) {
252
+ if (!id.endsWith(".md")) {
253
+ return null;
254
+ }
255
+ const result = await transformMarkdownWithReact(code, id, {
256
+ ...resolved,
257
+ components: Object.fromEntries(componentMap),
258
+ root: config.root
259
+ });
260
+ return {
261
+ code: result.code,
262
+ map: result.map
263
+ };
264
+ }
265
+ };
266
+ const reactEnvironmentPlugin = {
267
+ name: "ox-content:react-environment",
268
+ config() {
269
+ const envOptions = {
270
+ ...resolved,
271
+ components: Object.fromEntries(componentMap)
272
+ };
273
+ return {
274
+ environments: {
275
+ oxcontent_ssr: createReactMarkdownEnvironment("ssr", envOptions),
276
+ oxcontent_client: createReactMarkdownEnvironment("client", envOptions)
277
+ }
278
+ };
279
+ },
280
+ resolveId(id) {
281
+ if (id === "virtual:ox-content-react/runtime") {
282
+ return "\0virtual:ox-content-react/runtime";
283
+ }
284
+ if (id === "virtual:ox-content-react/components") {
285
+ return "\0virtual:ox-content-react/components";
286
+ }
287
+ return null;
288
+ },
289
+ load(id) {
290
+ if (id === "\0virtual:ox-content-react/runtime") {
291
+ return generateRuntimeModule();
292
+ }
293
+ if (id === "\0virtual:ox-content-react/components") {
294
+ return generateComponentsModule(componentMap);
295
+ }
296
+ return null;
297
+ },
298
+ applyToEnvironment(environment) {
299
+ return ["oxcontent_ssr", "oxcontent_client", "client", "ssr"].includes(
300
+ environment.name
301
+ );
302
+ }
303
+ };
304
+ const reactHmrPlugin = {
305
+ name: "ox-content:react-hmr",
306
+ apply: "serve",
307
+ handleHotUpdate({ file, server, modules }) {
308
+ const isComponent = Array.from(componentMap.values()).some(
309
+ (path3) => file.endsWith(path3.replace(/^\.\//, ""))
310
+ );
311
+ if (isComponent) {
312
+ const mdModules = Array.from(
313
+ server.moduleGraph.idToModuleMap.values()
314
+ ).filter((mod) => mod.file?.endsWith(".md"));
315
+ if (mdModules.length > 0) {
316
+ server.ws.send({
317
+ type: "custom",
318
+ event: "ox-content:react-update",
319
+ data: { file }
320
+ });
321
+ return [...modules, ...mdModules];
322
+ }
323
+ }
324
+ return modules;
325
+ }
326
+ };
327
+ const basePlugins = (0, import_vite_plugin_ox_content2.oxContent)(options);
328
+ const environmentPlugin = basePlugins.find((p) => p.name === "ox-content:environment");
329
+ return [
330
+ reactTransformPlugin,
331
+ reactEnvironmentPlugin,
332
+ reactHmrPlugin,
333
+ ...environmentPlugin ? [environmentPlugin] : []
334
+ ];
335
+ }
336
+ function resolveReactOptions(options) {
337
+ return {
338
+ srcDir: options.srcDir ?? "docs",
339
+ outDir: options.outDir ?? "dist",
340
+ base: options.base ?? "/",
341
+ gfm: options.gfm ?? true,
342
+ frontmatter: options.frontmatter ?? true,
343
+ toc: options.toc ?? true,
344
+ tocMaxDepth: options.tocMaxDepth ?? 3,
345
+ jsxRuntime: options.jsxRuntime ?? "automatic"
346
+ };
347
+ }
348
+ function generateRuntimeModule() {
349
+ return `
350
+ import React, { useState, useEffect } from 'react';
351
+
352
+ export function OxContentRenderer({ content, components = {} }) {
353
+ const [mounted, setMounted] = useState(false);
354
+
355
+ useEffect(() => {
356
+ setMounted(true);
357
+ }, []);
358
+
359
+ if (!content) return null;
360
+
361
+ const { html, frontmatter, slots } = content;
362
+
363
+ if (!mounted) {
364
+ return React.createElement('div', {
365
+ className: 'ox-content',
366
+ dangerouslySetInnerHTML: { __html: html },
367
+ });
368
+ }
369
+
370
+ return React.createElement('div', { className: 'ox-content' },
371
+ slots.map((slot) => {
372
+ const Component = components[slot.name];
373
+ return Component
374
+ ? React.createElement(Component, { key: slot.id, ...slot.props })
375
+ : null;
376
+ })
377
+ );
378
+ }
379
+
380
+ export function useOxContent() {
381
+ return { OxContentRenderer };
382
+ }
383
+ `;
384
+ }
385
+ function generateComponentsModule(componentMap) {
386
+ const imports = [];
387
+ const exports2 = [];
388
+ componentMap.forEach((path3, name) => {
389
+ imports.push(`import ${name} from '${path3}';`);
390
+ exports2.push(` ${name},`);
391
+ });
392
+ return `
393
+ ${imports.join("\n")}
394
+
395
+ export const components = {
396
+ ${exports2.join("\n")}
397
+ };
398
+
399
+ export default components;
400
+ `;
401
+ }
402
+ async function resolveComponentsGlob(componentsOption, root) {
403
+ if (typeof componentsOption === "object" && !Array.isArray(componentsOption)) {
404
+ return componentsOption;
405
+ }
406
+ const patterns = Array.isArray(componentsOption) ? componentsOption : [componentsOption];
407
+ const result = {};
408
+ for (const pattern of patterns) {
409
+ const files = await globFiles(pattern, root);
410
+ for (const file of files) {
411
+ const baseName = path2.basename(file, path2.extname(file));
412
+ const componentName = toPascalCase(baseName);
413
+ const relativePath = "./" + path2.relative(root, file).replace(/\\/g, "/");
414
+ result[componentName] = relativePath;
415
+ }
416
+ }
417
+ return result;
418
+ }
419
+ async function globFiles(pattern, root) {
420
+ const files = [];
421
+ const isGlob = pattern.includes("*");
422
+ if (!isGlob) {
423
+ const fullPath = path2.resolve(root, pattern);
424
+ if (fs.existsSync(fullPath)) {
425
+ files.push(fullPath);
426
+ }
427
+ return files;
428
+ }
429
+ const parts = pattern.split("*");
430
+ const baseDir = path2.resolve(root, parts[0]);
431
+ const ext = parts[1] || "";
432
+ if (!fs.existsSync(baseDir)) {
433
+ return files;
434
+ }
435
+ if (pattern.includes("**")) {
436
+ await walkDir(baseDir, files, ext);
437
+ } else {
438
+ const entries = await fs.promises.readdir(baseDir, { withFileTypes: true });
439
+ for (const entry of entries) {
440
+ if (entry.isFile() && entry.name.endsWith(ext)) {
441
+ files.push(path2.join(baseDir, entry.name));
442
+ }
443
+ }
444
+ }
445
+ return files;
446
+ }
447
+ async function walkDir(dir, files, ext) {
448
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
449
+ for (const entry of entries) {
450
+ const fullPath = path2.join(dir, entry.name);
451
+ if (entry.isDirectory()) {
452
+ await walkDir(fullPath, files, ext);
453
+ } else if (entry.isFile() && entry.name.endsWith(ext)) {
454
+ files.push(fullPath);
455
+ }
456
+ }
457
+ }
458
+ function toPascalCase(str) {
459
+ return str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase());
460
+ }
461
+ // Annotate the CommonJS export names for ESM import in node:
462
+ 0 && (module.exports = {
463
+ oxContent,
464
+ oxContentReact
465
+ });
@@ -0,0 +1,85 @@
1
+ import { PluginOption } from 'vite';
2
+ import { OxContentOptions } from 'vite-plugin-ox-content';
3
+ export { oxContent } from 'vite-plugin-ox-content';
4
+
5
+ type ComponentsMap = Record<string, string>;
6
+ /**
7
+ * Component registration options.
8
+ * Can be a map, a glob pattern, or an array of glob patterns.
9
+ */
10
+ type ComponentsOption = ComponentsMap | string | string[];
11
+ interface ReactIntegrationOptions extends OxContentOptions {
12
+ /**
13
+ * Components to register for use in Markdown.
14
+ * Can be a map of names to paths, a glob pattern, or an array of globs.
15
+ * When using glob patterns, component names are derived from file names.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * // Glob pattern (recommended)
20
+ * components: './src/components/*.tsx'
21
+ *
22
+ * // Explicit map
23
+ * components: { Counter: './src/components/Counter.tsx' }
24
+ * ```
25
+ */
26
+ components?: ComponentsOption;
27
+ jsxRuntime?: 'automatic' | 'classic';
28
+ }
29
+ interface ResolvedReactOptions {
30
+ srcDir: string;
31
+ outDir: string;
32
+ base: string;
33
+ gfm: boolean;
34
+ frontmatter: boolean;
35
+ toc: boolean;
36
+ tocMaxDepth: number;
37
+ components: ComponentsMap;
38
+ jsxRuntime: 'automatic' | 'classic';
39
+ root?: string;
40
+ }
41
+ interface ReactTransformResult {
42
+ code: string;
43
+ map: null;
44
+ usedComponents: string[];
45
+ frontmatter: Record<string, unknown>;
46
+ }
47
+ interface ComponentSlot {
48
+ name: string;
49
+ props: Record<string, unknown>;
50
+ position: number;
51
+ id: string;
52
+ }
53
+
54
+ /**
55
+ * Vite Plugin for Ox Content React Integration
56
+ *
57
+ * Uses Vite's Environment API to enable embedding React components in Markdown.
58
+ */
59
+
60
+ /**
61
+ * Creates the Ox Content React integration plugin.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * // vite.config.ts
66
+ * import { defineConfig } from 'vite';
67
+ * import react from '@vitejs/plugin-react';
68
+ * import { oxContentReact } from 'vite-plugin-ox-content-react';
69
+ *
70
+ * export default defineConfig({
71
+ * plugins: [
72
+ * react(),
73
+ * oxContentReact({
74
+ * srcDir: 'docs',
75
+ * components: {
76
+ * Counter: './src/components/Counter.tsx',
77
+ * },
78
+ * }),
79
+ * ],
80
+ * });
81
+ * ```
82
+ */
83
+ declare function oxContentReact(options?: ReactIntegrationOptions): PluginOption[];
84
+
85
+ export { type ComponentSlot, type ComponentsMap, type ComponentsOption, type ReactIntegrationOptions, type ReactTransformResult, type ResolvedReactOptions, oxContentReact };
@@ -0,0 +1,85 @@
1
+ import { PluginOption } from 'vite';
2
+ import { OxContentOptions } from 'vite-plugin-ox-content';
3
+ export { oxContent } from 'vite-plugin-ox-content';
4
+
5
+ type ComponentsMap = Record<string, string>;
6
+ /**
7
+ * Component registration options.
8
+ * Can be a map, a glob pattern, or an array of glob patterns.
9
+ */
10
+ type ComponentsOption = ComponentsMap | string | string[];
11
+ interface ReactIntegrationOptions extends OxContentOptions {
12
+ /**
13
+ * Components to register for use in Markdown.
14
+ * Can be a map of names to paths, a glob pattern, or an array of globs.
15
+ * When using glob patterns, component names are derived from file names.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * // Glob pattern (recommended)
20
+ * components: './src/components/*.tsx'
21
+ *
22
+ * // Explicit map
23
+ * components: { Counter: './src/components/Counter.tsx' }
24
+ * ```
25
+ */
26
+ components?: ComponentsOption;
27
+ jsxRuntime?: 'automatic' | 'classic';
28
+ }
29
+ interface ResolvedReactOptions {
30
+ srcDir: string;
31
+ outDir: string;
32
+ base: string;
33
+ gfm: boolean;
34
+ frontmatter: boolean;
35
+ toc: boolean;
36
+ tocMaxDepth: number;
37
+ components: ComponentsMap;
38
+ jsxRuntime: 'automatic' | 'classic';
39
+ root?: string;
40
+ }
41
+ interface ReactTransformResult {
42
+ code: string;
43
+ map: null;
44
+ usedComponents: string[];
45
+ frontmatter: Record<string, unknown>;
46
+ }
47
+ interface ComponentSlot {
48
+ name: string;
49
+ props: Record<string, unknown>;
50
+ position: number;
51
+ id: string;
52
+ }
53
+
54
+ /**
55
+ * Vite Plugin for Ox Content React Integration
56
+ *
57
+ * Uses Vite's Environment API to enable embedding React components in Markdown.
58
+ */
59
+
60
+ /**
61
+ * Creates the Ox Content React integration plugin.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * // vite.config.ts
66
+ * import { defineConfig } from 'vite';
67
+ * import react from '@vitejs/plugin-react';
68
+ * import { oxContentReact } from 'vite-plugin-ox-content-react';
69
+ *
70
+ * export default defineConfig({
71
+ * plugins: [
72
+ * react(),
73
+ * oxContentReact({
74
+ * srcDir: 'docs',
75
+ * components: {
76
+ * Counter: './src/components/Counter.tsx',
77
+ * },
78
+ * }),
79
+ * ],
80
+ * });
81
+ * ```
82
+ */
83
+ declare function oxContentReact(options?: ReactIntegrationOptions): PluginOption[];
84
+
85
+ export { type ComponentSlot, type ComponentsMap, type ComponentsOption, type ReactIntegrationOptions, type ReactTransformResult, type ResolvedReactOptions, oxContentReact };
package/dist/index.js ADDED
@@ -0,0 +1,429 @@
1
+ // src/index.ts
2
+ 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
+ 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;
11
+ 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
+ };
75
+ }
76
+ 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 };
100
+ }
101
+ 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;
120
+ }
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';
134
+ ${imports}
135
+
136
+ const frontmatter = ${JSON.stringify(frontmatter)};
137
+ const rawHtml = ${JSON.stringify(content)};
138
+ const slots = ${JSON.stringify(slots)};
139
+
140
+ const components = { ${usedComponents.join(", ")} };
141
+
142
+ export default function MarkdownContent() {
143
+ const [mounted, setMounted] = useState(false);
144
+
145
+ useEffect(() => {
146
+ setMounted(true);
147
+ }, []);
148
+
149
+ if (!mounted) {
150
+ return createElement('div', {
151
+ className: 'ox-content',
152
+ dangerouslySetInnerHTML: { __html: rawHtml }
153
+ });
154
+ }
155
+
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
+ );
162
+ }
163
+
164
+ export { frontmatter };
165
+ `;
166
+ }
167
+
168
+ // src/environment.ts
169
+ 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
+ };
191
+ }
192
+
193
+ // src/index.ts
194
+ import { oxContent as oxContent2 } from "vite-plugin-ox-content";
195
+ 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
+ ];
300
+ }
301
+ 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
+ };
312
+ }
313
+ function generateRuntimeModule() {
314
+ return `
315
+ import React, { useState, useEffect } from 'react';
316
+
317
+ export function OxContentRenderer({ content, components = {} }) {
318
+ const [mounted, setMounted] = useState(false);
319
+
320
+ useEffect(() => {
321
+ setMounted(true);
322
+ }, []);
323
+
324
+ if (!content) return null;
325
+
326
+ const { html, frontmatter, slots } = content;
327
+
328
+ if (!mounted) {
329
+ return React.createElement('div', {
330
+ className: 'ox-content',
331
+ dangerouslySetInnerHTML: { __html: html },
332
+ });
333
+ }
334
+
335
+ return React.createElement('div', { className: 'ox-content' },
336
+ slots.map((slot) => {
337
+ const Component = components[slot.name];
338
+ return Component
339
+ ? React.createElement(Component, { key: slot.id, ...slot.props })
340
+ : null;
341
+ })
342
+ );
343
+ }
344
+
345
+ export function useOxContent() {
346
+ return { OxContentRenderer };
347
+ }
348
+ `;
349
+ }
350
+ 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 `
358
+ ${imports.join("\n")}
359
+
360
+ export const components = {
361
+ ${exports.join("\n")}
362
+ };
363
+
364
+ export default components;
365
+ `;
366
+ }
367
+ 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;
383
+ }
384
+ 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;
411
+ }
412
+ 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
+ }
422
+ }
423
+ function toPascalCase(str) {
424
+ return str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase());
425
+ }
426
+ export {
427
+ oxContent2 as oxContent,
428
+ oxContentReact
429
+ };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@ox-content/vite-plugin-react",
3
+ "version": "0.0.1-alpha.0",
4
+ "description": "React integration for Ox Content - Embed React components in Markdown",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "dev": "tsup --watch",
20
+ "typecheck": "tsc --noEmit"
21
+ },
22
+ "peerDependencies": {
23
+ "react": "catalog:",
24
+ "react-dom": "catalog:",
25
+ "vite": "catalog:"
26
+ },
27
+ "dependencies": {
28
+ "vite-plugin-ox-content": "workspace:*"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "catalog:",
32
+ "@types/react": "catalog:",
33
+ "@types/react-dom": "catalog:",
34
+ "@vitejs/plugin-react": "catalog:",
35
+ "react": "catalog:",
36
+ "react-dom": "catalog:",
37
+ "tsup": "catalog:",
38
+ "typescript": "catalog:",
39
+ "vite": "catalog:"
40
+ },
41
+ "keywords": [
42
+ "vite",
43
+ "vite-plugin",
44
+ "react",
45
+ "markdown",
46
+ "ox-content",
47
+ "mdx"
48
+ ],
49
+ "license": "MIT",
50
+ "author": "ubugeeei",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "https://github.com/ubugeeei/ox-content.git",
54
+ "directory": "npm/vite-plugin-ox-content-react"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ }
59
+ }