@ox-content/vite-plugin-vue 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 +522 -419
- package/dist/index.d.cts +99 -102
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +99 -102
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +496 -391
- package/dist/index.js.map +1 -0
- package/package.json +18 -16
package/dist/index.cjs
CHANGED
|
@@ -1,400 +1,501 @@
|
|
|
1
|
-
|
|
1
|
+
//#region rolldown:runtime
|
|
2
2
|
var __create = Object.create;
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
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
8
|
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: ((k) => from[k]).bind(null, key),
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
19
21
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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);
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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");
|
|
27
|
+
//#endregion
|
|
28
|
+
let fs = require("fs");
|
|
29
|
+
fs = __toESM(fs);
|
|
30
|
+
let path$1 = require("path");
|
|
31
|
+
path$1 = __toESM(path$1);
|
|
32
|
+
let _ox_content_vite_plugin = require("@ox-content/vite-plugin");
|
|
40
33
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
//#region src/transform.ts
|
|
35
|
+
/**
|
|
36
|
+
* Markdown to Vue SFC transformation.
|
|
37
|
+
*/
|
|
38
|
+
const COMPONENT_REGEX = /<([A-Z][a-zA-Z0-9]*)\s*([^>]*?)\s*(?:\/>|>([\s\S]*?)<\/\1>)/g;
|
|
39
|
+
const PROP_REGEX = /(?::|v-bind:)?([a-zA-Z0-9-]+)(?:=(?:"([^"]*)"|'([^']*)'|{([^}]*)}|\[([^\]]*)\]))?/g;
|
|
40
|
+
const ISLAND_MARKER_PREFIX = "OXCONTENT-ISLAND-";
|
|
41
|
+
const ISLAND_MARKER_SUFFIX = "-PLACEHOLDER";
|
|
42
|
+
/**
|
|
43
|
+
* Transforms Markdown content with Vue component support.
|
|
44
|
+
*/
|
|
46
45
|
async function transformMarkdownWithVue(code, id, options) {
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
46
|
+
const { components } = options;
|
|
47
|
+
const usedComponents = [];
|
|
48
|
+
const islands = [];
|
|
49
|
+
let islandIndex = 0;
|
|
50
|
+
const { content: markdownContent, frontmatter } = extractFrontmatter(code);
|
|
51
|
+
const fenceRanges = collectFenceRanges(markdownContent);
|
|
52
|
+
let processedContent = "";
|
|
53
|
+
let lastIndex = 0;
|
|
54
|
+
let match;
|
|
55
|
+
COMPONENT_REGEX.lastIndex = 0;
|
|
56
|
+
while ((match = COMPONENT_REGEX.exec(markdownContent)) !== null) {
|
|
57
|
+
const [fullMatch, componentName, propsString, rawIslandContent] = match;
|
|
58
|
+
const matchStart = match.index;
|
|
59
|
+
const matchEnd = matchStart + fullMatch.length;
|
|
60
|
+
if (!components.has(componentName) || isInRanges(matchStart, matchEnd, fenceRanges)) {
|
|
61
|
+
processedContent += markdownContent.slice(lastIndex, matchEnd);
|
|
62
|
+
lastIndex = matchEnd;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!usedComponents.includes(componentName)) usedComponents.push(componentName);
|
|
66
|
+
const props = parseProps(propsString);
|
|
67
|
+
const islandId = `ox-island-${islandIndex++}`;
|
|
68
|
+
const islandContent = typeof rawIslandContent === "string" ? rawIslandContent.trim() : void 0;
|
|
69
|
+
islands.push({
|
|
70
|
+
name: componentName,
|
|
71
|
+
props,
|
|
72
|
+
position: matchStart,
|
|
73
|
+
id: islandId,
|
|
74
|
+
content: islandContent
|
|
75
|
+
});
|
|
76
|
+
processedContent += markdownContent.slice(lastIndex, matchStart) + createIslandMarker(islandId);
|
|
77
|
+
lastIndex = matchEnd;
|
|
78
|
+
}
|
|
79
|
+
processedContent += markdownContent.slice(lastIndex);
|
|
80
|
+
return {
|
|
81
|
+
code: generateVueSFC(injectIslandMarkers((await (0, _ox_content_vite_plugin.transformMarkdown)(processedContent, id, {
|
|
82
|
+
srcDir: options.srcDir,
|
|
83
|
+
outDir: options.outDir,
|
|
84
|
+
base: options.base,
|
|
85
|
+
ssg: {
|
|
86
|
+
enabled: false,
|
|
87
|
+
extension: ".html",
|
|
88
|
+
clean: false,
|
|
89
|
+
bare: false,
|
|
90
|
+
generateOgImage: false
|
|
91
|
+
},
|
|
92
|
+
gfm: options.gfm,
|
|
93
|
+
frontmatter: false,
|
|
94
|
+
toc: options.toc,
|
|
95
|
+
tocMaxDepth: options.tocMaxDepth,
|
|
96
|
+
footnotes: true,
|
|
97
|
+
tables: true,
|
|
98
|
+
taskLists: true,
|
|
99
|
+
strikethrough: true,
|
|
100
|
+
highlight: false,
|
|
101
|
+
highlightTheme: "github-dark",
|
|
102
|
+
mermaid: false,
|
|
103
|
+
ogImage: false,
|
|
104
|
+
ogImageOptions: {},
|
|
105
|
+
transformers: [],
|
|
106
|
+
docs: false,
|
|
107
|
+
search: {
|
|
108
|
+
enabled: false,
|
|
109
|
+
limit: 10,
|
|
110
|
+
prefix: true,
|
|
111
|
+
placeholder: "Search...",
|
|
112
|
+
hotkey: "k"
|
|
113
|
+
}
|
|
114
|
+
})).html, islands), usedComponents, islands, frontmatter, options, id),
|
|
115
|
+
map: null,
|
|
116
|
+
usedComponents,
|
|
117
|
+
frontmatter
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function createIslandMarker(islandId) {
|
|
121
|
+
return `${ISLAND_MARKER_PREFIX}${islandId}${ISLAND_MARKER_SUFFIX}`;
|
|
122
|
+
}
|
|
123
|
+
function collectFenceRanges(content) {
|
|
124
|
+
const ranges = [];
|
|
125
|
+
let inFence = false;
|
|
126
|
+
let fenceChar = "";
|
|
127
|
+
let fenceLength = 0;
|
|
128
|
+
let fenceStart = 0;
|
|
129
|
+
let pos = 0;
|
|
130
|
+
while (pos < content.length) {
|
|
131
|
+
const lineEnd = content.indexOf("\n", pos);
|
|
132
|
+
const next = lineEnd === -1 ? content.length : lineEnd + 1;
|
|
133
|
+
const fenceMatch = content.slice(pos, lineEnd === -1 ? content.length : lineEnd).match(/^\s{0,3}([`~]{3,})/);
|
|
134
|
+
if (fenceMatch) {
|
|
135
|
+
const marker = fenceMatch[1];
|
|
136
|
+
if (!inFence) {
|
|
137
|
+
inFence = true;
|
|
138
|
+
fenceChar = marker[0];
|
|
139
|
+
fenceLength = marker.length;
|
|
140
|
+
fenceStart = pos;
|
|
141
|
+
} else if (marker[0] === fenceChar && marker.length >= fenceLength) {
|
|
142
|
+
inFence = false;
|
|
143
|
+
ranges.push({
|
|
144
|
+
start: fenceStart,
|
|
145
|
+
end: next
|
|
146
|
+
});
|
|
147
|
+
fenceChar = "";
|
|
148
|
+
fenceLength = 0;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
pos = next;
|
|
152
|
+
}
|
|
153
|
+
if (inFence) ranges.push({
|
|
154
|
+
start: fenceStart,
|
|
155
|
+
end: content.length
|
|
156
|
+
});
|
|
157
|
+
return ranges;
|
|
111
158
|
}
|
|
159
|
+
function isInRanges(start, end, ranges) {
|
|
160
|
+
for (const range of ranges) if (start < range.end && end > range.start) return true;
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
function injectIslandMarkers(html, islands) {
|
|
164
|
+
let output = html;
|
|
165
|
+
for (const island of islands) {
|
|
166
|
+
const marker = createIslandMarker(island.id);
|
|
167
|
+
const propsAttr = Object.keys(island.props).length > 0 ? ` data-ox-props='${JSON.stringify(island.props).replace(/'/g, "'")}'` : "";
|
|
168
|
+
const contentAttr = island.content ? ` data-ox-content='${island.content.replace(/'/g, "'")}'` : "";
|
|
169
|
+
const attrs = `data-ox-island="${island.name}"${propsAttr}${contentAttr}`;
|
|
170
|
+
output = output.replaceAll(`<p>${marker}</p>`, `<div ${attrs}></div>`);
|
|
171
|
+
output = output.replaceAll(marker, `<span ${attrs}></span>`);
|
|
172
|
+
}
|
|
173
|
+
return output;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Extracts frontmatter from Markdown content.
|
|
177
|
+
*/
|
|
112
178
|
function extractFrontmatter(content) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
frontmatter
|
|
138
|
-
};
|
|
179
|
+
const match = /^---\n([\s\S]*?)\n---\n/.exec(content);
|
|
180
|
+
if (!match) return {
|
|
181
|
+
content,
|
|
182
|
+
frontmatter: {}
|
|
183
|
+
};
|
|
184
|
+
const frontmatterStr = match[1];
|
|
185
|
+
const frontmatter = {};
|
|
186
|
+
for (const line of frontmatterStr.split("\n")) {
|
|
187
|
+
const colonIndex = line.indexOf(":");
|
|
188
|
+
if (colonIndex > 0) {
|
|
189
|
+
const key = line.slice(0, colonIndex).trim();
|
|
190
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
191
|
+
try {
|
|
192
|
+
value = JSON.parse(value);
|
|
193
|
+
} catch {
|
|
194
|
+
if (typeof value === "string" && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
|
|
195
|
+
}
|
|
196
|
+
frontmatter[key] = value;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
content: content.slice(match[0].length),
|
|
201
|
+
frontmatter
|
|
202
|
+
};
|
|
139
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Parses component props from a string.
|
|
206
|
+
*/
|
|
140
207
|
function parseProps(propsString) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
props[name] = bracketValue;
|
|
162
|
-
}
|
|
163
|
-
} else {
|
|
164
|
-
props[name] = true;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return props;
|
|
208
|
+
const props = {};
|
|
209
|
+
if (!propsString) return props;
|
|
210
|
+
let match;
|
|
211
|
+
while ((match = PROP_REGEX.exec(propsString)) !== null) {
|
|
212
|
+
const [, name, doubleQuoted, singleQuoted, braceValue, bracketValue] = match;
|
|
213
|
+
if (name) if (doubleQuoted !== void 0) props[name] = doubleQuoted;
|
|
214
|
+
else if (singleQuoted !== void 0) props[name] = singleQuoted;
|
|
215
|
+
else if (braceValue !== void 0) try {
|
|
216
|
+
props[name] = JSON.parse(`{${braceValue}}`);
|
|
217
|
+
} catch {
|
|
218
|
+
props[name] = braceValue;
|
|
219
|
+
}
|
|
220
|
+
else if (bracketValue !== void 0) try {
|
|
221
|
+
props[name] = JSON.parse(`[${bracketValue}]`);
|
|
222
|
+
} catch {
|
|
223
|
+
props[name] = bracketValue;
|
|
224
|
+
}
|
|
225
|
+
else props[name] = true;
|
|
226
|
+
}
|
|
227
|
+
return props;
|
|
169
228
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Generates a Vue component as JavaScript module from the processed Markdown.
|
|
231
|
+
*/
|
|
232
|
+
function generateVueSFC(content, usedComponents, islands, frontmatter, options, id) {
|
|
233
|
+
const mdDir = path$1.dirname(id);
|
|
234
|
+
const root = options.root || process.cwd();
|
|
235
|
+
const componentImports = usedComponents.map((name) => {
|
|
236
|
+
const componentPath = options.components.get(name);
|
|
237
|
+
if (!componentPath) return "";
|
|
238
|
+
const absolutePath = path$1.resolve(root, componentPath.replace(/^\.\//, ""));
|
|
239
|
+
const relativePath = path$1.relative(mdDir, absolutePath).replace(/\\/g, "/");
|
|
240
|
+
return `import ${name} from '${relativePath.startsWith(".") ? relativePath : "./" + relativePath}';`;
|
|
241
|
+
}).filter(Boolean).join("\n");
|
|
242
|
+
const componentMap = usedComponents.map((name) => ` ${name},`).join("\n");
|
|
243
|
+
if (islands.length === 0) return `
|
|
244
|
+
import { h, ref, defineComponent } from 'vue';
|
|
245
|
+
|
|
246
|
+
export const frontmatter = ${JSON.stringify(frontmatter)};
|
|
247
|
+
|
|
248
|
+
const rawHtml = ${JSON.stringify(content)};
|
|
249
|
+
|
|
250
|
+
export default defineComponent({
|
|
251
|
+
name: 'MarkdownContent',
|
|
252
|
+
setup(_, { expose }) {
|
|
253
|
+
expose({ frontmatter });
|
|
254
|
+
|
|
255
|
+
return () =>
|
|
256
|
+
h('div', {
|
|
257
|
+
class: 'ox-content',
|
|
258
|
+
innerHTML: rawHtml,
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
`;
|
|
263
|
+
return `
|
|
264
|
+
import { h, ref, onMounted, onBeforeUnmount, defineComponent, render } from 'vue';
|
|
265
|
+
import { initIslands } from '@ox-content/islands';
|
|
188
266
|
${componentImports}
|
|
189
267
|
|
|
190
268
|
export const frontmatter = ${JSON.stringify(frontmatter)};
|
|
191
269
|
|
|
192
270
|
const rawHtml = ${JSON.stringify(content)};
|
|
193
|
-
const
|
|
271
|
+
const components = {
|
|
272
|
+
${componentMap}
|
|
273
|
+
};
|
|
194
274
|
|
|
195
|
-
function
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
275
|
+
function createVueHydrate(container) {
|
|
276
|
+
const mountedTargets = [];
|
|
277
|
+
|
|
278
|
+
return (element, props) => {
|
|
279
|
+
const componentName = element.dataset.oxIsland;
|
|
280
|
+
const Component = components[componentName];
|
|
281
|
+
if (!Component) return;
|
|
282
|
+
|
|
283
|
+
const islandContent = element.dataset.oxContent || element.innerHTML;
|
|
284
|
+
const children = islandContent
|
|
285
|
+
? { default: () => h('div', { innerHTML: islandContent }) }
|
|
286
|
+
: undefined;
|
|
287
|
+
|
|
288
|
+
const vnode = h(Component, props, children);
|
|
289
|
+
render(vnode, element);
|
|
290
|
+
mountedTargets.push(element);
|
|
291
|
+
|
|
292
|
+
return () => render(null, element);
|
|
293
|
+
};
|
|
200
294
|
}
|
|
201
295
|
|
|
202
296
|
export default defineComponent({
|
|
203
297
|
name: 'MarkdownContent',
|
|
204
298
|
setup(_, { expose }) {
|
|
205
|
-
const
|
|
299
|
+
const container = ref(null);
|
|
300
|
+
let controller;
|
|
206
301
|
|
|
207
302
|
onMounted(() => {
|
|
208
|
-
|
|
303
|
+
if (container.value) {
|
|
304
|
+
controller = initIslands(createVueHydrate(container.value), {
|
|
305
|
+
selector: '.ox-content [data-ox-island]',
|
|
306
|
+
});
|
|
307
|
+
}
|
|
209
308
|
});
|
|
210
309
|
|
|
211
|
-
|
|
310
|
+
onBeforeUnmount(() => {
|
|
311
|
+
if (controller) controller.destroy();
|
|
312
|
+
});
|
|
212
313
|
|
|
213
|
-
|
|
214
|
-
if (!mounted.value) {
|
|
215
|
-
return h('div', {
|
|
216
|
-
class: 'ox-content',
|
|
217
|
-
innerHTML: rawHtml,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
314
|
+
expose({ frontmatter });
|
|
220
315
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
316
|
+
return () =>
|
|
317
|
+
h('div', {
|
|
318
|
+
class: 'ox-content',
|
|
319
|
+
ref: container,
|
|
320
|
+
innerHTML: rawHtml,
|
|
321
|
+
});
|
|
225
322
|
},
|
|
226
323
|
});
|
|
227
324
|
`;
|
|
228
325
|
}
|
|
229
326
|
|
|
230
|
-
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/environment.ts
|
|
329
|
+
/**
|
|
330
|
+
* Creates a Vite environment for Vue markdown processing.
|
|
331
|
+
*
|
|
332
|
+
* @param mode - 'ssr' for server-side rendering, 'client' for client hydration
|
|
333
|
+
* @param options - Resolved Vue integration options
|
|
334
|
+
*/
|
|
231
335
|
function createVueMarkdownEnvironment(mode, options) {
|
|
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
|
-
// Exclude ox-content packages from optimization (they're local)
|
|
258
|
-
exclude: ["vite-plugin-ox-content", "vite-plugin-ox-content-vue"]
|
|
259
|
-
},
|
|
260
|
-
// Development server options (client only)
|
|
261
|
-
...!isSSR && {
|
|
262
|
-
dev: {
|
|
263
|
-
warmup: ["./src/**/*.vue", "./docs/**/*.md"]
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
};
|
|
336
|
+
const isSSR = mode === "ssr";
|
|
337
|
+
return {
|
|
338
|
+
build: {
|
|
339
|
+
outDir: isSSR ? `${options.outDir}/.ox-content/ssr` : `${options.outDir}/.ox-content/client`,
|
|
340
|
+
ssr: isSSR,
|
|
341
|
+
rollupOptions: {
|
|
342
|
+
input: isSSR ? void 0 : void 0,
|
|
343
|
+
output: {
|
|
344
|
+
format: isSSR ? "esm" : "esm",
|
|
345
|
+
entryFileNames: isSSR ? "[name].js" : "[name].[hash].js",
|
|
346
|
+
chunkFileNames: isSSR ? "chunks/[name].js" : "chunks/[name].[hash].js"
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
...isSSR && {
|
|
350
|
+
target: "node18",
|
|
351
|
+
minify: false
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
resolve: { conditions: isSSR ? ["node", "import"] : ["browser", "import"] },
|
|
355
|
+
optimizeDeps: {
|
|
356
|
+
include: isSSR ? [] : ["vue"],
|
|
357
|
+
exclude: ["@ox-content/vite-plugin", "@ox-content/vite-plugin-vue"]
|
|
358
|
+
},
|
|
359
|
+
...!isSSR && { dev: { warmup: ["./src/**/*.vue", "./docs/**/*.md"] } }
|
|
360
|
+
};
|
|
267
361
|
}
|
|
268
362
|
|
|
269
|
-
|
|
270
|
-
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region src/index.ts
|
|
365
|
+
/**
|
|
366
|
+
* Vite Plugin for Ox Content Vue Integration
|
|
367
|
+
*
|
|
368
|
+
* Uses Vite's Environment API to enable embedding Vue components in Markdown.
|
|
369
|
+
* Provides SSR and client environments for proper hydration.
|
|
370
|
+
*/
|
|
371
|
+
/**
|
|
372
|
+
* Creates the Ox Content Vue integration plugin with Environment API support.
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* ```ts
|
|
376
|
+
* // vite.config.ts
|
|
377
|
+
* import { defineConfig } from 'vite';
|
|
378
|
+
* import vue from '@vitejs/plugin-vue';
|
|
379
|
+
* import { oxContentVue } from 'vite-plugin-ox-content-vue';
|
|
380
|
+
*
|
|
381
|
+
* export default defineConfig({
|
|
382
|
+
* plugins: [
|
|
383
|
+
* vue(),
|
|
384
|
+
* oxContentVue({
|
|
385
|
+
* srcDir: 'docs',
|
|
386
|
+
* components: {
|
|
387
|
+
* Counter: './src/components/Counter.vue',
|
|
388
|
+
* },
|
|
389
|
+
* }),
|
|
390
|
+
* ],
|
|
391
|
+
* });
|
|
392
|
+
* ```
|
|
393
|
+
*/
|
|
271
394
|
function oxContentVue(options = {}) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const isComponent = Array.from(componentMap.values()).some(
|
|
354
|
-
(path3) => file.endsWith(path3.replace(/^\.\//, ""))
|
|
355
|
-
);
|
|
356
|
-
if (isComponent) {
|
|
357
|
-
const mdModules = Array.from(
|
|
358
|
-
server.moduleGraph.idToModuleMap.values()
|
|
359
|
-
).filter((mod) => mod.file?.endsWith(".md"));
|
|
360
|
-
if (mdModules.length > 0) {
|
|
361
|
-
server.ws.send({
|
|
362
|
-
type: "custom",
|
|
363
|
-
event: "ox-content:vue-update",
|
|
364
|
-
data: { file }
|
|
365
|
-
});
|
|
366
|
-
return [...modules, ...mdModules];
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return modules;
|
|
370
|
-
}
|
|
371
|
-
};
|
|
372
|
-
const basePlugins = (0, import_vite_plugin_ox_content2.oxContent)(options);
|
|
373
|
-
const environmentPlugin = basePlugins.find((p) => p.name === "ox-content:environment");
|
|
374
|
-
return [
|
|
375
|
-
vueTransformPlugin,
|
|
376
|
-
vueEnvironmentPlugin,
|
|
377
|
-
vueHmrPlugin,
|
|
378
|
-
...environmentPlugin ? [environmentPlugin] : []
|
|
379
|
-
];
|
|
395
|
+
const resolved = resolveVueOptions(options);
|
|
396
|
+
let componentMap = /* @__PURE__ */ new Map();
|
|
397
|
+
let config;
|
|
398
|
+
if (typeof options.components === "object" && !Array.isArray(options.components)) componentMap = new Map(Object.entries(options.components));
|
|
399
|
+
const vueTransformPlugin = {
|
|
400
|
+
name: "ox-content:vue-transform",
|
|
401
|
+
enforce: "pre",
|
|
402
|
+
async configResolved(resolvedConfig) {
|
|
403
|
+
config = resolvedConfig;
|
|
404
|
+
const componentsOption = options.components;
|
|
405
|
+
if (componentsOption) {
|
|
406
|
+
const resolvedComponents = await resolveComponentsGlob(componentsOption, config.root);
|
|
407
|
+
componentMap = new Map(Object.entries(resolvedComponents));
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
async transform(code, id) {
|
|
411
|
+
if (!id.endsWith(".md")) return null;
|
|
412
|
+
const result = await transformMarkdownWithVue(code, id, {
|
|
413
|
+
...resolved,
|
|
414
|
+
components: componentMap,
|
|
415
|
+
root: config.root
|
|
416
|
+
});
|
|
417
|
+
return {
|
|
418
|
+
code: result.code,
|
|
419
|
+
map: result.map
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
const vueEnvironmentPlugin = {
|
|
424
|
+
name: "ox-content:vue-environment",
|
|
425
|
+
config() {
|
|
426
|
+
return { environments: {
|
|
427
|
+
oxcontent_ssr: createVueMarkdownEnvironment("ssr", resolved),
|
|
428
|
+
oxcontent_client: createVueMarkdownEnvironment("client", resolved)
|
|
429
|
+
} };
|
|
430
|
+
},
|
|
431
|
+
resolveId: {
|
|
432
|
+
order: "pre",
|
|
433
|
+
async handler(id, _importer, _options) {
|
|
434
|
+
if (id === "virtual:ox-content-vue/runtime") return "\0virtual:ox-content-vue/runtime";
|
|
435
|
+
if (id === "virtual:ox-content-vue/components") return "\0virtual:ox-content-vue/components";
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
load: {
|
|
440
|
+
order: "pre",
|
|
441
|
+
async handler(id) {
|
|
442
|
+
if (id === "\0virtual:ox-content-vue/runtime") return generateRuntimeModule(resolved);
|
|
443
|
+
if (id === "\0virtual:ox-content-vue/components") return generateComponentsModule(componentMap);
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
applyToEnvironment(environment) {
|
|
448
|
+
return environment.name === "oxcontent_ssr" || environment.name === "oxcontent_client" || environment.name === "client" || environment.name === "ssr";
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
const vueHmrPlugin = {
|
|
452
|
+
name: "ox-content:vue-hmr",
|
|
453
|
+
apply: "serve",
|
|
454
|
+
handleHotUpdate({ file, server, modules }) {
|
|
455
|
+
if (Array.from(componentMap.values()).some((path) => file.endsWith(path.replace(/^\.\//, "")))) {
|
|
456
|
+
const mdModules = Array.from(server.moduleGraph.idToModuleMap.values()).filter((mod) => mod.file?.endsWith(".md"));
|
|
457
|
+
if (mdModules.length > 0) {
|
|
458
|
+
server.ws.send({
|
|
459
|
+
type: "custom",
|
|
460
|
+
event: "ox-content:vue-update",
|
|
461
|
+
data: { file }
|
|
462
|
+
});
|
|
463
|
+
return [...modules, ...mdModules];
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return modules;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
const environmentPlugin = (0, _ox_content_vite_plugin.oxContent)(options).find((p) => p.name === "ox-content:environment");
|
|
470
|
+
return [
|
|
471
|
+
vueTransformPlugin,
|
|
472
|
+
vueEnvironmentPlugin,
|
|
473
|
+
vueHmrPlugin,
|
|
474
|
+
...environmentPlugin ? [environmentPlugin] : []
|
|
475
|
+
];
|
|
380
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Resolves Vue integration options with defaults.
|
|
479
|
+
*/
|
|
381
480
|
function resolveVueOptions(options) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
};
|
|
481
|
+
return {
|
|
482
|
+
srcDir: options.srcDir ?? "docs",
|
|
483
|
+
outDir: options.outDir ?? "dist",
|
|
484
|
+
base: options.base ?? "/",
|
|
485
|
+
gfm: options.gfm ?? true,
|
|
486
|
+
frontmatter: options.frontmatter ?? true,
|
|
487
|
+
toc: options.toc ?? true,
|
|
488
|
+
tocMaxDepth: options.tocMaxDepth ?? 3,
|
|
489
|
+
components: options.components ?? {},
|
|
490
|
+
reactivityTransform: options.reactivityTransform ?? false,
|
|
491
|
+
customBlocks: options.customBlocks ?? true
|
|
492
|
+
};
|
|
395
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* Generates the runtime module for Vue markdown rendering.
|
|
496
|
+
*/
|
|
396
497
|
function generateRuntimeModule(_options) {
|
|
397
|
-
|
|
498
|
+
return `
|
|
398
499
|
import { h, defineComponent, ref, onMounted } from 'vue';
|
|
399
500
|
|
|
400
501
|
export const OxContentRenderer = defineComponent({
|
|
@@ -413,19 +514,19 @@ export const OxContentRenderer = defineComponent({
|
|
|
413
514
|
return () => {
|
|
414
515
|
if (!props.content) return null;
|
|
415
516
|
|
|
416
|
-
const { html, frontmatter, toc,
|
|
517
|
+
const { html, frontmatter, toc, islands } = props.content;
|
|
417
518
|
|
|
418
|
-
// Render static HTML with component
|
|
519
|
+
// Render static HTML with component islands
|
|
419
520
|
return h('div', {
|
|
420
521
|
class: 'ox-content',
|
|
421
522
|
innerHTML: mounted.value ? undefined : html,
|
|
422
|
-
}, mounted.value ?
|
|
523
|
+
}, mounted.value ? renderWithIslands(html, islands, props.components) : undefined);
|
|
423
524
|
};
|
|
424
525
|
},
|
|
425
526
|
});
|
|
426
527
|
|
|
427
|
-
function
|
|
428
|
-
// Parse and render
|
|
528
|
+
function renderWithIslands(html, islands, components) {
|
|
529
|
+
// Parse and render islands with Vue components
|
|
429
530
|
// This is a simplified version - full implementation would use proper parsing
|
|
430
531
|
return h('div', { innerHTML: html });
|
|
431
532
|
}
|
|
@@ -437,84 +538,86 @@ export function useOxContent() {
|
|
|
437
538
|
}
|
|
438
539
|
`;
|
|
439
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* Generates the components registration module.
|
|
543
|
+
*/
|
|
440
544
|
function generateComponentsModule(componentMap) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
545
|
+
const imports = [];
|
|
546
|
+
const exports = [];
|
|
547
|
+
componentMap.forEach((path, name) => {
|
|
548
|
+
imports.push(`import ${name} from '${path}';`);
|
|
549
|
+
exports.push(` ${name},`);
|
|
550
|
+
});
|
|
551
|
+
return `
|
|
448
552
|
${imports.join("\n")}
|
|
449
553
|
|
|
450
554
|
export const components = {
|
|
451
|
-
${
|
|
555
|
+
${exports.join("\n")}
|
|
452
556
|
};
|
|
453
557
|
|
|
454
558
|
export default components;
|
|
455
559
|
`;
|
|
456
560
|
}
|
|
561
|
+
/**
|
|
562
|
+
* Resolves component glob patterns to a component map.
|
|
563
|
+
*/
|
|
457
564
|
async function resolveComponentsGlob(componentsOption, root) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
result[componentName] = relativePath;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
return result;
|
|
565
|
+
if (typeof componentsOption === "object" && !Array.isArray(componentsOption)) return componentsOption;
|
|
566
|
+
const patterns = Array.isArray(componentsOption) ? componentsOption : [componentsOption];
|
|
567
|
+
const result = {};
|
|
568
|
+
for (const pattern of patterns) {
|
|
569
|
+
const files = await globFiles(pattern, root);
|
|
570
|
+
for (const file of files) {
|
|
571
|
+
const componentName = toPascalCase(path$1.basename(file, path$1.extname(file)));
|
|
572
|
+
result[componentName] = "./" + path$1.relative(root, file).replace(/\\/g, "/");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return result;
|
|
473
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* Simple glob matcher for component files.
|
|
579
|
+
*/
|
|
474
580
|
async function globFiles(pattern, root) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
await walkDir(baseDir, files, ext);
|
|
492
|
-
} else {
|
|
493
|
-
const entries = await fs.promises.readdir(baseDir, { withFileTypes: true });
|
|
494
|
-
for (const entry of entries) {
|
|
495
|
-
if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
496
|
-
files.push(path2.join(baseDir, entry.name));
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
return files;
|
|
581
|
+
const files = [];
|
|
582
|
+
if (!pattern.includes("*")) {
|
|
583
|
+
const fullPath = path$1.resolve(root, pattern);
|
|
584
|
+
if (fs.existsSync(fullPath)) files.push(fullPath);
|
|
585
|
+
return files;
|
|
586
|
+
}
|
|
587
|
+
const parts = pattern.split("*");
|
|
588
|
+
const baseDir = path$1.resolve(root, parts[0]);
|
|
589
|
+
const ext = parts[1] || "";
|
|
590
|
+
if (!fs.existsSync(baseDir)) return files;
|
|
591
|
+
if (pattern.includes("**")) await walkDir(baseDir, files, ext);
|
|
592
|
+
else {
|
|
593
|
+
const entries = await fs.promises.readdir(baseDir, { withFileTypes: true });
|
|
594
|
+
for (const entry of entries) if (entry.isFile() && entry.name.endsWith(ext)) files.push(path$1.join(baseDir, entry.name));
|
|
595
|
+
}
|
|
596
|
+
return files;
|
|
501
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* Recursively walks a directory.
|
|
600
|
+
*/
|
|
502
601
|
async function walkDir(dir, files, ext) {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
files.push(fullPath);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
602
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
603
|
+
for (const entry of entries) {
|
|
604
|
+
const fullPath = path$1.join(dir, entry.name);
|
|
605
|
+
if (entry.isDirectory()) await walkDir(fullPath, files, ext);
|
|
606
|
+
else if (entry.isFile() && entry.name.endsWith(ext)) files.push(fullPath);
|
|
607
|
+
}
|
|
512
608
|
}
|
|
609
|
+
/**
|
|
610
|
+
* Converts a string to PascalCase.
|
|
611
|
+
*/
|
|
513
612
|
function toPascalCase(str) {
|
|
514
|
-
|
|
613
|
+
return str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase());
|
|
515
614
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
615
|
+
|
|
616
|
+
//#endregion
|
|
617
|
+
Object.defineProperty(exports, 'oxContent', {
|
|
618
|
+
enumerable: true,
|
|
619
|
+
get: function () {
|
|
620
|
+
return _ox_content_vite_plugin.oxContent;
|
|
621
|
+
}
|
|
520
622
|
});
|
|
623
|
+
exports.oxContentVue = oxContentVue;
|