@ubean/markdown 0.1.13 → 0.2.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-bC6p6XCn.d.ts +89 -0
- package/dist/index.d.ts +3 -45
- package/dist/index.js +2 -145
- package/dist/jsx-runtime.d.ts +13 -0
- package/dist/jsx-runtime.js +32 -0
- package/dist/mdx.d.ts +2 -0
- package/dist/mdx.js +2 -0
- package/dist/src-B81snjFd.js +388 -0
- package/dist/vite-plugin-DIL6Ty-2.d.ts +10535 -0
- package/dist/vite-plugin.d.ts +2 -0
- package/dist/vite-plugin.js +2 -0
- package/package.json +22 -3
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { createMarkdownExit } from "markdown-exit";
|
|
2
|
+
//#region src/mdx.ts
|
|
3
|
+
/**
|
|
4
|
+
* P9-20: MDX real compilation.
|
|
5
|
+
*
|
|
6
|
+
* Compiles MDX (Markdown + JSX) files to Vue components using `@mdx-js/mdx`
|
|
7
|
+
* as an optional peer dependency. The compiled output uses ubean's Vue JSX
|
|
8
|
+
* runtime (`@ubean/markdown/jsx-runtime`) which maps JSX calls to Vue's `h()`.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Frontmatter extraction (YAML)
|
|
12
|
+
* - JSX component support in Markdown
|
|
13
|
+
* - Vue component imports
|
|
14
|
+
* - Code highlighting via remark/rehype plugins
|
|
15
|
+
* - Graceful fallback to plain Markdown when `@mdx-js/mdx` is not installed
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { compileMdx } from '@ubean/markdown/mdx';
|
|
20
|
+
*
|
|
21
|
+
* const result = await compileMdx(mdxSource, {
|
|
22
|
+
* filePath: 'post.mdx',
|
|
23
|
+
* remarkPlugins: [remarkGfm],
|
|
24
|
+
* });
|
|
25
|
+
* // result.code → JavaScript module string (Vue component)
|
|
26
|
+
* // result.frontmatter → parsed frontmatter
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
let _mdxCompiler;
|
|
30
|
+
/**
|
|
31
|
+
* Dynamically load `@mdx-js/mdx` (optional peer dependency).
|
|
32
|
+
* Returns null if not installed.
|
|
33
|
+
*/
|
|
34
|
+
async function getMdxCompiler() {
|
|
35
|
+
if (_mdxCompiler !== void 0) return _mdxCompiler;
|
|
36
|
+
try {
|
|
37
|
+
const mod = await import("@mdx-js/mdx");
|
|
38
|
+
_mdxCompiler = mod.compile || mod.default || null;
|
|
39
|
+
} catch {
|
|
40
|
+
_mdxCompiler = null;
|
|
41
|
+
}
|
|
42
|
+
return _mdxCompiler;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check if `@mdx-js/mdx` is available for real MDX compilation.
|
|
46
|
+
*/
|
|
47
|
+
async function isMdxAvailable() {
|
|
48
|
+
return await getMdxCompiler() !== null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Compile MDX source to a Vue component JavaScript module.
|
|
52
|
+
*
|
|
53
|
+
* Uses `@mdx-js/mdx` to compile the MDX body to a JS module that imports
|
|
54
|
+
* from `@ubean/markdown/jsx-runtime` (which maps to Vue's `h()` function).
|
|
55
|
+
*
|
|
56
|
+
* If `@mdx-js/mdx` is not installed, falls back to plain markdown rendering
|
|
57
|
+
* using `markdown-exit`, wrapping the HTML output in a Vue component.
|
|
58
|
+
*
|
|
59
|
+
* @param source Raw MDX source string (with optional frontmatter)
|
|
60
|
+
* @param options Compilation options
|
|
61
|
+
* @returns Compilation result with code, frontmatter, and compiled flag
|
|
62
|
+
*/
|
|
63
|
+
async function compileMdx(source, options = {}) {
|
|
64
|
+
const { data: frontmatter, content } = parseFrontmatter(source);
|
|
65
|
+
const compiler = await getMdxCompiler();
|
|
66
|
+
if (compiler) {
|
|
67
|
+
const compiled = await compiler(options.filePath ? {
|
|
68
|
+
path: options.filePath,
|
|
69
|
+
value: content
|
|
70
|
+
} : content, {
|
|
71
|
+
format: "mdx",
|
|
72
|
+
jsxImportSource: "@ubean/markdown",
|
|
73
|
+
development: options.development ?? false,
|
|
74
|
+
remarkPlugins: options.remarkPlugins || [],
|
|
75
|
+
rehypePlugins: options.rehypePlugins || []
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
code: String(compiled),
|
|
79
|
+
frontmatter,
|
|
80
|
+
compiled: true
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const { markdownToHtml } = await import("./index.js");
|
|
84
|
+
const html = markdownToHtml(content);
|
|
85
|
+
return {
|
|
86
|
+
code: generateFallbackComponent(JSON.stringify(html), frontmatter),
|
|
87
|
+
frontmatter,
|
|
88
|
+
compiled: false
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Generate a fallback Vue component when `@mdx-js/mdx` is not available.
|
|
93
|
+
* Renders the markdown HTML using `v-html`.
|
|
94
|
+
*/
|
|
95
|
+
function generateFallbackComponent(htmlJson, frontmatter) {
|
|
96
|
+
return `// Auto-generated by @ubean/markdown (fallback mode — @mdx-js/mdx not installed)
|
|
97
|
+
import { defineComponent, h } from 'vue';
|
|
98
|
+
${Object.keys(frontmatter).length > 0 ? `export const frontmatter = ${JSON.stringify(frontmatter)};` : "export const frontmatter = {};"}
|
|
99
|
+
export default defineComponent({
|
|
100
|
+
name: 'MdxPage',
|
|
101
|
+
setup() {
|
|
102
|
+
return () => h('div', { class: 'ubean-mdx-content', innerHTML: ${htmlJson} });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Synchronously check if MDX compilation is available (without dynamic import).
|
|
109
|
+
* Uses the cached result from a previous `compileMdx()` or `isMdxAvailable()` call.
|
|
110
|
+
*/
|
|
111
|
+
function isMdxAvailableSync() {
|
|
112
|
+
return _mdxCompiler !== void 0 && _mdxCompiler !== null;
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/vite-plugin.ts
|
|
116
|
+
const DEFAULT_INCLUDE = /\.mdx$/;
|
|
117
|
+
function ubeanMdxPlugin(options = {}) {
|
|
118
|
+
const include = options.include || DEFAULT_INCLUDE;
|
|
119
|
+
return {
|
|
120
|
+
name: "ubean:mdx",
|
|
121
|
+
enforce: "pre",
|
|
122
|
+
async transform(code, id) {
|
|
123
|
+
if (!include.test(id)) return null;
|
|
124
|
+
if (options.exclude && options.exclude.test(id)) return null;
|
|
125
|
+
try {
|
|
126
|
+
return {
|
|
127
|
+
code: (await compileMdx(code, {
|
|
128
|
+
filePath: id,
|
|
129
|
+
remarkPlugins: options.remarkPlugins,
|
|
130
|
+
rehypePlugins: options.rehypePlugins
|
|
131
|
+
})).code,
|
|
132
|
+
map: {
|
|
133
|
+
mappings: "",
|
|
134
|
+
version: 3,
|
|
135
|
+
sources: [],
|
|
136
|
+
names: [],
|
|
137
|
+
file: id
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
} catch (err) {
|
|
141
|
+
this.error(`[ubean:mdx] Failed to compile ${id}: ${err.message}`);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
handleHotUpdate(ctx) {
|
|
146
|
+
if (!include.test(ctx.file)) return;
|
|
147
|
+
ctx.server.ws.send({ type: "full-reload" });
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/index.ts
|
|
154
|
+
const FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
|
|
155
|
+
function parseFrontmatter(source) {
|
|
156
|
+
const match = source.match(FRONTMATTER_REGEX);
|
|
157
|
+
if (!match) return {
|
|
158
|
+
data: {},
|
|
159
|
+
content: source
|
|
160
|
+
};
|
|
161
|
+
const raw = match[1];
|
|
162
|
+
const content = source.slice(match[0].length);
|
|
163
|
+
return {
|
|
164
|
+
data: parseYamlSimple(raw),
|
|
165
|
+
content
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function tokenizeYaml(yaml) {
|
|
169
|
+
const lines = [];
|
|
170
|
+
for (const raw of yaml.split("\n")) {
|
|
171
|
+
const trimmed = raw.trimEnd();
|
|
172
|
+
if (!trimmed.trim() || trimmed.trimStart().startsWith("#")) continue;
|
|
173
|
+
let indent = 0;
|
|
174
|
+
while (indent < trimmed.length && trimmed[indent] === " ") indent++;
|
|
175
|
+
lines.push({
|
|
176
|
+
indent,
|
|
177
|
+
content: trimmed.trim()
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return lines;
|
|
181
|
+
}
|
|
182
|
+
function parseYamlScalar(raw) {
|
|
183
|
+
if (raw === "true" || raw === "false") return raw === "true";
|
|
184
|
+
if (raw === "null" || raw === "~") return null;
|
|
185
|
+
if (raw !== "" && !isNaN(Number(raw))) {
|
|
186
|
+
const num = Number(raw);
|
|
187
|
+
if (isFinite(num)) return num;
|
|
188
|
+
}
|
|
189
|
+
if (raw.startsWith("\"") && raw.endsWith("\"") || raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1);
|
|
190
|
+
if (raw.startsWith("[") && raw.endsWith("]")) return raw.slice(1, -1).split(",").map((v) => v.trim()).filter((v) => v !== "").map((v) => {
|
|
191
|
+
if (v === "true") return true;
|
|
192
|
+
if (v === "false") return false;
|
|
193
|
+
if (v === "null") return null;
|
|
194
|
+
if (!isNaN(Number(v))) return Number(v);
|
|
195
|
+
return v.replace(/^["']|["']$/g, "");
|
|
196
|
+
});
|
|
197
|
+
return raw;
|
|
198
|
+
}
|
|
199
|
+
function isSequenceItem(content) {
|
|
200
|
+
return content === "-" || content.startsWith("- ");
|
|
201
|
+
}
|
|
202
|
+
/** `- key: value` starts an inline map item (continuation keys align after the dash). */
|
|
203
|
+
function isInlineMapStart(rest) {
|
|
204
|
+
const colonIdx = rest.indexOf(":");
|
|
205
|
+
return colonIdx > 0 && (rest.length === colonIdx + 1 || rest[colonIdx + 1] === " ");
|
|
206
|
+
}
|
|
207
|
+
function assignYamlKey(map, key, value) {
|
|
208
|
+
const nestedKeys = key.split(".");
|
|
209
|
+
let current = map;
|
|
210
|
+
for (let i = 0; i < nestedKeys.length - 1; i++) {
|
|
211
|
+
const k = nestedKeys[i];
|
|
212
|
+
if (typeof current[k] !== "object" || current[k] === null) current[k] = {};
|
|
213
|
+
current = current[k];
|
|
214
|
+
}
|
|
215
|
+
current[nestedKeys[nestedKeys.length - 1]] = value;
|
|
216
|
+
}
|
|
217
|
+
function parseYamlBlock(lines, start, indent) {
|
|
218
|
+
const first = lines[start];
|
|
219
|
+
if (isSequenceItem(first.content)) {
|
|
220
|
+
const items = [];
|
|
221
|
+
let i = start;
|
|
222
|
+
while (i < lines.length && lines[i].indent === indent && isSequenceItem(lines[i].content)) {
|
|
223
|
+
const rest = lines[i].content === "-" ? "" : lines[i].content.slice(2).trim();
|
|
224
|
+
if (rest === "") {
|
|
225
|
+
if (i + 1 < lines.length && lines[i + 1].indent > indent) {
|
|
226
|
+
const res = parseYamlBlock(lines, i + 1, lines[i + 1].indent);
|
|
227
|
+
items.push(res.value);
|
|
228
|
+
i = res.next;
|
|
229
|
+
} else {
|
|
230
|
+
items.push(null);
|
|
231
|
+
i += 1;
|
|
232
|
+
}
|
|
233
|
+
} else if (isInlineMapStart(rest)) {
|
|
234
|
+
const synthesized = [{
|
|
235
|
+
indent: indent + 2,
|
|
236
|
+
content: rest
|
|
237
|
+
}];
|
|
238
|
+
let j = i + 1;
|
|
239
|
+
while (j < lines.length && lines[j].indent > indent) {
|
|
240
|
+
synthesized.push(lines[j]);
|
|
241
|
+
j += 1;
|
|
242
|
+
}
|
|
243
|
+
const res = parseYamlBlock(synthesized, 0, indent + 2);
|
|
244
|
+
items.push(res.value);
|
|
245
|
+
i = j;
|
|
246
|
+
} else {
|
|
247
|
+
items.push(parseYamlScalar(rest));
|
|
248
|
+
i += 1;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
value: items,
|
|
253
|
+
next: i
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const map = {};
|
|
257
|
+
let i = start;
|
|
258
|
+
while (i < lines.length && lines[i].indent === indent && !isSequenceItem(lines[i].content)) {
|
|
259
|
+
const colonIdx = lines[i].content.indexOf(":");
|
|
260
|
+
if (colonIdx === -1) {
|
|
261
|
+
i += 1;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const key = lines[i].content.slice(0, colonIdx).trim();
|
|
265
|
+
const rawValue = lines[i].content.slice(colonIdx + 1).trim();
|
|
266
|
+
if (!key) {
|
|
267
|
+
i += 1;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (rawValue === "") {
|
|
271
|
+
if (i + 1 < lines.length && lines[i + 1].indent > indent) {
|
|
272
|
+
const res = parseYamlBlock(lines, i + 1, lines[i + 1].indent);
|
|
273
|
+
assignYamlKey(map, key, res.value);
|
|
274
|
+
i = res.next;
|
|
275
|
+
} else {
|
|
276
|
+
assignYamlKey(map, key, true);
|
|
277
|
+
i += 1;
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
assignYamlKey(map, key, parseYamlScalar(rawValue));
|
|
281
|
+
i += 1;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
value: map,
|
|
286
|
+
next: i
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function parseYamlSimple(yaml) {
|
|
290
|
+
const lines = tokenizeYaml(yaml);
|
|
291
|
+
if (lines.length === 0) return {};
|
|
292
|
+
const { value } = parseYamlBlock(lines, 0, lines[0].indent);
|
|
293
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
294
|
+
return value;
|
|
295
|
+
}
|
|
296
|
+
function slugify(text) {
|
|
297
|
+
return text.toLowerCase().replace(/<[^>]*>/g, "").replace(/[^\w\s\u4e00-\u9fa5-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
298
|
+
}
|
|
299
|
+
function applyHeadingIds(md) {
|
|
300
|
+
const headingOpenRule = md.renderer.rules.heading_open;
|
|
301
|
+
md.renderer.rules.heading_open = (tokens, idx, options, env, self) => {
|
|
302
|
+
const token = tokens[idx];
|
|
303
|
+
const nextToken = tokens[idx + 1];
|
|
304
|
+
if (nextToken && nextToken.type === "inline") {
|
|
305
|
+
const text = nextToken.children?.filter((t) => t.type === "text" || t.type === "code_inline").map((t) => t.content).join("") || "";
|
|
306
|
+
token.attrSet("id", slugify(text));
|
|
307
|
+
}
|
|
308
|
+
if (headingOpenRule) return headingOpenRule(tokens, idx, options, env, self);
|
|
309
|
+
return self.renderToken(tokens, idx, options);
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function createInstance(options) {
|
|
313
|
+
const md = createMarkdownExit({
|
|
314
|
+
html: options.html ?? false,
|
|
315
|
+
linkify: options.linkify ?? true,
|
|
316
|
+
breaks: options.breaks ?? false,
|
|
317
|
+
typographer: options.typographer ?? false,
|
|
318
|
+
highlight: options.highlighter ? (str, lang) => {
|
|
319
|
+
const result = options.highlighter(str.trimEnd(), lang);
|
|
320
|
+
if (result.startsWith("<pre")) return result;
|
|
321
|
+
return `<pre><code class="language-${lang}">${result}</code></pre>`;
|
|
322
|
+
} : void 0
|
|
323
|
+
});
|
|
324
|
+
if (options.headingIds !== false) applyHeadingIds(md);
|
|
325
|
+
return md;
|
|
326
|
+
}
|
|
327
|
+
function markdownToHtml(markdown, options = {}) {
|
|
328
|
+
return createInstance(options).render(markdown).trim();
|
|
329
|
+
}
|
|
330
|
+
function extractHeadings(markdown) {
|
|
331
|
+
const headings = [];
|
|
332
|
+
const tokens = createMarkdownExit({ html: false }).parse(markdown, {});
|
|
333
|
+
function walkTokenList(tokenList) {
|
|
334
|
+
for (let i = 0; i < tokenList.length; i++) {
|
|
335
|
+
const token = tokenList[i];
|
|
336
|
+
if (token.type === "heading_open") {
|
|
337
|
+
const level = parseInt(token.tag.slice(1), 10);
|
|
338
|
+
const inlineToken = tokenList[i + 1];
|
|
339
|
+
if (inlineToken && inlineToken.type === "inline") {
|
|
340
|
+
const text = extractTextFromTokens(inlineToken.children || []);
|
|
341
|
+
headings.push({
|
|
342
|
+
level,
|
|
343
|
+
text,
|
|
344
|
+
id: slugify(text)
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (token.children) walkTokenList(token.children);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
walkTokenList(tokens);
|
|
352
|
+
return headings;
|
|
353
|
+
}
|
|
354
|
+
function extractTextFromTokens(tokens) {
|
|
355
|
+
let text = "";
|
|
356
|
+
for (const token of tokens) if (token.type === "text" || token.type === "code_inline") text += token.content;
|
|
357
|
+
else if (token.children) text += extractTextFromTokens(token.children);
|
|
358
|
+
return text;
|
|
359
|
+
}
|
|
360
|
+
function extractExcerpt(markdown, separator = "<!-- more -->") {
|
|
361
|
+
const idx = markdown.indexOf(separator);
|
|
362
|
+
if (idx !== -1) return markdown.slice(0, idx).trim();
|
|
363
|
+
const paragraphs = markdown.split("\n\n").filter((p) => p.trim() !== "");
|
|
364
|
+
for (const para of paragraphs) {
|
|
365
|
+
const trimmed = para.trim();
|
|
366
|
+
if (trimmed.startsWith("#")) continue;
|
|
367
|
+
if (trimmed.startsWith("```")) continue;
|
|
368
|
+
if (trimmed.startsWith(">")) continue;
|
|
369
|
+
if (trimmed.startsWith("- ") || trimmed.startsWith("* ") || /^\d+\.\s/.test(trimmed)) continue;
|
|
370
|
+
if (trimmed.length < 500) return trimmed;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function parseMarkdown(source, options = {}) {
|
|
374
|
+
const { data: frontmatter, content } = parseFrontmatter(source);
|
|
375
|
+
const headings = extractHeadings(content);
|
|
376
|
+
return {
|
|
377
|
+
frontmatter,
|
|
378
|
+
content,
|
|
379
|
+
excerpt: options.excerpt !== false ? extractExcerpt(content, options.excerptSeparator) : void 0,
|
|
380
|
+
headings,
|
|
381
|
+
html: markdownToHtml(content, options)
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function defineMarkdownPage(frontmatter) {
|
|
385
|
+
return frontmatter;
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
export { parseFrontmatter as a, compileMdx as c, markdownToHtml as i, isMdxAvailable as l, extractExcerpt as n, parseMarkdown as o, extractHeadings as r, ubeanMdxPlugin as s, defineMarkdownPage as t, isMdxAvailableSync as u };
|