auwla-markdown 0.0.3 → 0.0.4
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 +435 -2
- package/package.json +2 -2
- package/dist/adapters/shiki.js +0 -29
- package/dist/engine.js +0 -46
- package/dist/features.js +0 -341
- package/dist/frontmatter.js +0 -32
- package/dist/headings.js +0 -42
- package/dist/types.js +0 -0
package/dist/index.js
CHANGED
|
@@ -1,2 +1,435 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/engine.ts
|
|
5
|
+
import { Marked } from "marked";
|
|
6
|
+
import { markedHighlight } from "marked-highlight";
|
|
7
|
+
|
|
8
|
+
// src/frontmatter.ts
|
|
9
|
+
function extractFrontmatter(rawString) {
|
|
10
|
+
const match = rawString.match(/^=<Header(?:[^>]*)>\r?\n([\s\S]*?)\r?\n=<\/Header>\r?\n([\s\S]*)$/);
|
|
11
|
+
if (!match)
|
|
12
|
+
return { content: rawString, meta: {} };
|
|
13
|
+
const rawMeta = match[1];
|
|
14
|
+
const content = match[2];
|
|
15
|
+
const meta = {};
|
|
16
|
+
for (const line of rawMeta.split(`
|
|
17
|
+
`)) {
|
|
18
|
+
const sep = line.indexOf(":");
|
|
19
|
+
if (sep === -1)
|
|
20
|
+
continue;
|
|
21
|
+
const key = line.slice(0, sep).trim();
|
|
22
|
+
const val = line.slice(sep + 1).trim();
|
|
23
|
+
if (val === "true") {
|
|
24
|
+
meta[key] = true;
|
|
25
|
+
} else if (val === "false") {
|
|
26
|
+
meta[key] = false;
|
|
27
|
+
} else if (!isNaN(Number(val)) && val !== "") {
|
|
28
|
+
meta[key] = Number(val);
|
|
29
|
+
} else {
|
|
30
|
+
meta[key] = val.replace(/^["']|["']$/g, "");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { content, meta };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/headings.ts
|
|
37
|
+
function extractHeadings(content) {
|
|
38
|
+
const headings = [];
|
|
39
|
+
const lines = content.split(`
|
|
40
|
+
`);
|
|
41
|
+
let inCodeBlock = false;
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
const trimmed = line.trim();
|
|
44
|
+
if (trimmed.startsWith("```")) {
|
|
45
|
+
inCodeBlock = !inCodeBlock;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (inCodeBlock)
|
|
49
|
+
continue;
|
|
50
|
+
const tagMatch = trimmed.match(/^=<h([1-6])(?:[^>]*?)>(.*?)=<\/h\1>$/);
|
|
51
|
+
if (tagMatch) {
|
|
52
|
+
const level = parseInt(tagMatch[1], 10);
|
|
53
|
+
const text = tagMatch[2].trim();
|
|
54
|
+
const id = text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-");
|
|
55
|
+
headings.push({ level, text, id });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const match = line.match(/^(#{1,6})\s+(.+)$/);
|
|
59
|
+
if (match) {
|
|
60
|
+
const level = match[1].length;
|
|
61
|
+
const text = match[2].trim();
|
|
62
|
+
const id = text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-");
|
|
63
|
+
headings.push({ level, text, id });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return headings;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/features.ts
|
|
70
|
+
var inlineWrapperTags = new Set(["h1", "h2", "h3", "h4", "h5", "h6", "p", "span", "a", "li"]);
|
|
71
|
+
var defaultComponents = {
|
|
72
|
+
Callout: async (props, rawContent, parse, parseInline, features) => {
|
|
73
|
+
const type = props.type || "note";
|
|
74
|
+
const title = props.title || type.toUpperCase();
|
|
75
|
+
const htmlContent = await parse(rawContent);
|
|
76
|
+
const isCollapsible = "collapsible" in props && props.collapsible !== "false";
|
|
77
|
+
const isCollapsed = props.collapsed === "true";
|
|
78
|
+
const customClass = props.class ? ` ${props.class}` : "";
|
|
79
|
+
const className = `callout callout-${type.toLowerCase()}${customClass}`;
|
|
80
|
+
const otherAttrs = Object.entries(props).filter(([k]) => !["value", "type", "title", "collapsible", "collapsed", "class"].includes(k)).map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
81
|
+
const attrString = otherAttrs ? ` ${otherAttrs}` : "";
|
|
82
|
+
if (isCollapsible) {
|
|
83
|
+
const openAttr = isCollapsed ? "" : " open";
|
|
84
|
+
return `<details class="${className}"${openAttr}${attrString}><summary class="callout-title">${title}</summary><div class="callout-content">${htmlContent}</div></details>`;
|
|
85
|
+
}
|
|
86
|
+
return `<div class="${className}"${attrString}><div class="callout-title">${title}</div><div class="callout-content">${htmlContent}</div></div>`;
|
|
87
|
+
},
|
|
88
|
+
Tabs: async (props, rawContent, parse, parseInline, features) => {
|
|
89
|
+
const content = await parse(rawContent);
|
|
90
|
+
const regex = /<div class="tab-panel[^"]*"\s+data-title="([^"]*)"/g;
|
|
91
|
+
const titles = [];
|
|
92
|
+
let match;
|
|
93
|
+
while ((match = regex.exec(content)) !== null) {
|
|
94
|
+
titles.push(match[1]);
|
|
95
|
+
}
|
|
96
|
+
if (titles.length === 0)
|
|
97
|
+
return content;
|
|
98
|
+
const tabButtons = titles.map((title, idx) => {
|
|
99
|
+
const activeClass = idx === 0 ? "active" : "";
|
|
100
|
+
const clickHandler = `const c = this.closest('.tabs-container'); c.querySelectorAll('.tab-btn').forEach((b, i) => { b.classList.toggle('active', b === this); c.querySelectorAll('.tab-panel')[i].style.display = (b === this) ? 'block' : 'none'; })`;
|
|
101
|
+
return `<button class="tab-btn ${activeClass}" onclick="${clickHandler}">${title}</button>`;
|
|
102
|
+
}).join("");
|
|
103
|
+
let panelIndex = 0;
|
|
104
|
+
const adjustedContent = content.replace(/class="tab-panel([^"]*)"/g, (match2, customClasses) => {
|
|
105
|
+
const activeClass = panelIndex === 0 ? " active" : "";
|
|
106
|
+
const display = panelIndex === 0 ? "block" : "none";
|
|
107
|
+
panelIndex++;
|
|
108
|
+
return `class="tab-panel${customClasses}${activeClass}" style="display: ${display};"`;
|
|
109
|
+
});
|
|
110
|
+
const customClass = props.class ? ` ${props.class}` : "";
|
|
111
|
+
const className = `tabs-container${customClass}`;
|
|
112
|
+
const otherAttrs = Object.entries(props).filter(([k]) => !["value", "class"].includes(k)).map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
113
|
+
const attrString = otherAttrs ? ` ${otherAttrs}` : "";
|
|
114
|
+
return `<div class="${className}"${attrString}><div class="tabs-header">${tabButtons}</div><div class="tabs-content">${adjustedContent}</div></div>`;
|
|
115
|
+
},
|
|
116
|
+
Tab: async (props, rawContent, parse, parseInline, features) => {
|
|
117
|
+
const title = props.title || "";
|
|
118
|
+
const content = await parse(rawContent);
|
|
119
|
+
const customClass = props.class ? ` ${props.class}` : "";
|
|
120
|
+
const className = `tab-panel${customClass}`;
|
|
121
|
+
const otherAttrs = Object.entries(props).filter(([k]) => !["value", "title", "class"].includes(k)).map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
122
|
+
const attrString = otherAttrs ? ` ${otherAttrs}` : "";
|
|
123
|
+
return `<div class="${className}" data-title="${title}"${attrString}>${content}</div>`;
|
|
124
|
+
},
|
|
125
|
+
Table: async (props, rawContent, parse, parseInline, features) => {
|
|
126
|
+
const content = await parseInline(rawContent);
|
|
127
|
+
const defaultClass = "auwla-table";
|
|
128
|
+
const customClass = props.class ? ` ${props.class}` : "";
|
|
129
|
+
const mergedClass = `${defaultClass}${customClass}`;
|
|
130
|
+
const attrPairs = Object.entries(props).filter(([k]) => k !== "value" && k !== "class").map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
131
|
+
const attrString = attrPairs ? ` ${attrPairs}` : "";
|
|
132
|
+
return `<table class="${mergedClass}"${attrString}>${content}</table>`;
|
|
133
|
+
},
|
|
134
|
+
Row: async (props, rawContent, parse, parseInline, features) => {
|
|
135
|
+
const content = await parseInline(rawContent);
|
|
136
|
+
const attrPairs = Object.entries(props).filter(([k]) => k !== "value").map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
137
|
+
const attrString = attrPairs ? ` ${attrPairs}` : "";
|
|
138
|
+
return `<tr${attrString}>${content}</tr>`;
|
|
139
|
+
},
|
|
140
|
+
Column: async (props, rawContent, parse, parseInline, features) => {
|
|
141
|
+
const content = await parseInline(rawContent);
|
|
142
|
+
const attrPairs = Object.entries(props).filter(([k]) => k !== "value").map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
143
|
+
const attrString = attrPairs ? ` ${attrPairs}` : "";
|
|
144
|
+
return `<th${attrString}>${content}</th>`;
|
|
145
|
+
},
|
|
146
|
+
Cell: async (props, rawContent, parse, parseInline, features) => {
|
|
147
|
+
const content = await parseInline(rawContent);
|
|
148
|
+
const attrPairs = Object.entries(props).filter(([k]) => k !== "value").map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
149
|
+
const attrString = attrPairs ? ` ${attrPairs}` : "";
|
|
150
|
+
return `<td${attrString}>${content}</td>`;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
function parseAttributes(attrString) {
|
|
154
|
+
const attrs = {};
|
|
155
|
+
const trimmed = attrString.trim();
|
|
156
|
+
if (!trimmed)
|
|
157
|
+
return attrs;
|
|
158
|
+
if (trimmed.startsWith("=")) {
|
|
159
|
+
const match2 = trimmed.match(/^=\s*(?:"(.*?)"|'(.*?)'|([^\s>]+))/);
|
|
160
|
+
if (match2) {
|
|
161
|
+
attrs.value = match2[1] ?? match2[2] ?? match2[3] ?? "";
|
|
162
|
+
return attrs;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const regex = /(\w+)(?:\s*=\s*(?:"(.*?)"|'(.*?)'|([^\s>]+)))?/g;
|
|
166
|
+
let match;
|
|
167
|
+
while ((match = regex.exec(attrString)) !== null) {
|
|
168
|
+
const key = match[1];
|
|
169
|
+
const val = match[2] ?? match[3] ?? match[4] ?? "true";
|
|
170
|
+
attrs[key] = val;
|
|
171
|
+
}
|
|
172
|
+
return attrs;
|
|
173
|
+
}
|
|
174
|
+
async function renderComponent(tagName, props, rawContent, parseFn, parseInlineFn, features, customComponents) {
|
|
175
|
+
if (customComponents[tagName]) {
|
|
176
|
+
return await customComponents[tagName](props, rawContent, parseFn, parseInlineFn, features);
|
|
177
|
+
}
|
|
178
|
+
if (defaultComponents[tagName]) {
|
|
179
|
+
return await defaultComponents[tagName](props, rawContent, parseFn, parseInlineFn, features);
|
|
180
|
+
}
|
|
181
|
+
const isInline = inlineWrapperTags.has(tagName.toLowerCase());
|
|
182
|
+
const compiledContent = isInline ? await parseInlineFn(rawContent) : await parseFn(rawContent);
|
|
183
|
+
if (isInline && tagName.toLowerCase().startsWith("h") && tagName.length === 2 && !isNaN(Number(tagName.slice(1)))) {
|
|
184
|
+
const depth = tagName.toLowerCase().slice(1);
|
|
185
|
+
const plainText = compiledContent.replace(/<[^>]*>/g, "");
|
|
186
|
+
const id = props.id || plainText.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-");
|
|
187
|
+
const idAttr = `id="${id}"`;
|
|
188
|
+
const classAttr = props.class ? `class="${props.class}"` : "";
|
|
189
|
+
const otherAttrs = Object.entries(props).filter(([k]) => k !== "value" && k !== "id" && k !== "class").map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
190
|
+
const attrString2 = [idAttr, classAttr, otherAttrs].filter(Boolean).map((s) => s.trim()).join(" ");
|
|
191
|
+
const finalAttrString = attrString2 ? ` ${attrString2}` : "";
|
|
192
|
+
if (features?.headerAnchors) {
|
|
193
|
+
return `<h${depth}${finalAttrString}><a href="#${id}" class="header-anchor" aria-hidden="true">#</a>${compiledContent}</h${depth}>`;
|
|
194
|
+
} else {
|
|
195
|
+
return `<h${depth}${finalAttrString}>${compiledContent}</h${depth}>`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const attrPairs = Object.entries(props).filter(([k]) => k !== "value").map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
199
|
+
const attrString = attrPairs ? ` ${attrPairs}` : "";
|
|
200
|
+
return `<${tagName}${attrString}>${compiledContent}</${tagName}>`;
|
|
201
|
+
}
|
|
202
|
+
async function preprocessComponents(rawMarkdown, parseFn, parseInlineFn, features = {}, customComponents = {}) {
|
|
203
|
+
const lines = rawMarkdown.split(`
|
|
204
|
+
`);
|
|
205
|
+
const result = [];
|
|
206
|
+
const stack = [];
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
const trimmed = line.trim();
|
|
209
|
+
const singleLineMatch = trimmed.match(/^=<(\w+)([^>]*?)>(.*?)=<\/\1>$/);
|
|
210
|
+
if (singleLineMatch) {
|
|
211
|
+
const tagName = singleLineMatch[1];
|
|
212
|
+
const attrString = singleLineMatch[2] || "";
|
|
213
|
+
const innerContent = singleLineMatch[3] || "";
|
|
214
|
+
const props = parseAttributes(attrString);
|
|
215
|
+
const html = await renderComponent(tagName, props, innerContent, parseFn, parseInlineFn, features, customComponents);
|
|
216
|
+
if (stack.length > 0) {
|
|
217
|
+
stack[stack.length - 1].contentLines.push(html);
|
|
218
|
+
} else {
|
|
219
|
+
result.push(html);
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (trimmed.startsWith("=<") && trimmed.endsWith("/>")) {
|
|
224
|
+
const match = trimmed.match(/^=<(\w+)([^>]*?)\/>$/);
|
|
225
|
+
if (match) {
|
|
226
|
+
const tagName = match[1];
|
|
227
|
+
const attrString = match[2] || "";
|
|
228
|
+
const props = parseAttributes(attrString);
|
|
229
|
+
const html = await renderComponent(tagName, props, "", parseFn, parseInlineFn, features, customComponents);
|
|
230
|
+
if (stack.length > 0) {
|
|
231
|
+
stack[stack.length - 1].contentLines.push(html);
|
|
232
|
+
} else {
|
|
233
|
+
result.push(html);
|
|
234
|
+
}
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (trimmed.startsWith("=<") && !trimmed.startsWith("=</") && trimmed.endsWith(">")) {
|
|
239
|
+
const match = trimmed.match(/^=<(\w+)([^>]*?)>$/);
|
|
240
|
+
if (match) {
|
|
241
|
+
const tagName = match[1];
|
|
242
|
+
const attrString = match[2] || "";
|
|
243
|
+
const props = parseAttributes(attrString);
|
|
244
|
+
stack.push({
|
|
245
|
+
tagName,
|
|
246
|
+
props,
|
|
247
|
+
contentLines: []
|
|
248
|
+
});
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (trimmed.startsWith("=</") && trimmed.endsWith(">")) {
|
|
253
|
+
const match = trimmed.match(/^=<\/(.*?)>$/);
|
|
254
|
+
if (match) {
|
|
255
|
+
const tagName = match[1].trim();
|
|
256
|
+
if (stack.length > 0 && stack[stack.length - 1].tagName === tagName) {
|
|
257
|
+
const block = stack.pop();
|
|
258
|
+
const rawContent = block.contentLines.join(`
|
|
259
|
+
`);
|
|
260
|
+
const html = await renderComponent(block.tagName, block.props, rawContent, parseFn, parseInlineFn, features, customComponents);
|
|
261
|
+
if (stack.length > 0) {
|
|
262
|
+
stack[stack.length - 1].contentLines.push(html);
|
|
263
|
+
} else {
|
|
264
|
+
result.push(html);
|
|
265
|
+
}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (stack.length > 0) {
|
|
271
|
+
stack[stack.length - 1].contentLines.push(line);
|
|
272
|
+
} else {
|
|
273
|
+
result.push(line);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
while (stack.length > 0) {
|
|
277
|
+
const block = stack.pop();
|
|
278
|
+
result.push(`=<${block.tagName}>`);
|
|
279
|
+
result.push(block.contentLines.join(`
|
|
280
|
+
`));
|
|
281
|
+
result.push(`=</${block.tagName}>`);
|
|
282
|
+
}
|
|
283
|
+
return result.join(`
|
|
284
|
+
`);
|
|
285
|
+
}
|
|
286
|
+
function extractRawCode(raw) {
|
|
287
|
+
if (raw.startsWith("```") || raw.startsWith("~~~")) {
|
|
288
|
+
const lines = raw.split(`
|
|
289
|
+
`);
|
|
290
|
+
return lines.slice(1, -1).join(`
|
|
291
|
+
`);
|
|
292
|
+
}
|
|
293
|
+
return raw;
|
|
294
|
+
}
|
|
295
|
+
function parseCodeMeta(langString) {
|
|
296
|
+
const parts = langString.trim().split(/\s+/);
|
|
297
|
+
const lang = parts[0] || "";
|
|
298
|
+
let filename;
|
|
299
|
+
const highlightedLines = new Set;
|
|
300
|
+
for (const part of parts.slice(1)) {
|
|
301
|
+
if (part.startsWith("[") && part.endsWith("]")) {
|
|
302
|
+
filename = part.slice(1, -1);
|
|
303
|
+
} else if (part.startsWith("{") && part.endsWith("}")) {
|
|
304
|
+
const ranges = part.slice(1, -1).split(",");
|
|
305
|
+
for (const range of ranges) {
|
|
306
|
+
if (range.includes("-")) {
|
|
307
|
+
const [startStr, endStr] = range.split("-");
|
|
308
|
+
const start = parseInt(startStr || "0", 10);
|
|
309
|
+
const end = parseInt(endStr || "0", 10);
|
|
310
|
+
if (!isNaN(start) && !isNaN(end)) {
|
|
311
|
+
for (let i = start;i <= end; i++) {
|
|
312
|
+
highlightedLines.add(i);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
const val = parseInt(range, 10);
|
|
317
|
+
if (!isNaN(val)) {
|
|
318
|
+
highlightedLines.add(val);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return { lang, filename, highlightedLines };
|
|
325
|
+
}
|
|
326
|
+
function injectHighlightedLines(html, highlightedLines) {
|
|
327
|
+
if (highlightedLines.size === 0)
|
|
328
|
+
return html;
|
|
329
|
+
if (html.includes('class="line"')) {
|
|
330
|
+
let lineIndex = 0;
|
|
331
|
+
return html.replace(/class="line"/g, () => {
|
|
332
|
+
lineIndex++;
|
|
333
|
+
if (highlightedLines.has(lineIndex)) {
|
|
334
|
+
return 'class="line highlighted-line"';
|
|
335
|
+
}
|
|
336
|
+
return 'class="line"';
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
const lines = html.split(`
|
|
340
|
+
`);
|
|
341
|
+
const processed = lines.map((line, idx) => {
|
|
342
|
+
const lineNum = idx + 1;
|
|
343
|
+
if (highlightedLines.has(lineNum)) {
|
|
344
|
+
return `<div class="highlighted-line">${line}</div>`;
|
|
345
|
+
}
|
|
346
|
+
return line;
|
|
347
|
+
});
|
|
348
|
+
return processed.join(`
|
|
349
|
+
`);
|
|
350
|
+
}
|
|
351
|
+
function copyCodeButtonRenderer(info) {
|
|
352
|
+
const rawCode = extractRawCode(info.raw);
|
|
353
|
+
const escapedCode = encodeURIComponent(rawCode).replace(/'/g, "%27");
|
|
354
|
+
const { lang, filename, highlightedLines } = parseCodeMeta(info.lang ?? "");
|
|
355
|
+
let codeContent = info.text.trim().startsWith("<pre") ? info.text : `<pre><code class="language-${lang}">${info.text}</code></pre>`;
|
|
356
|
+
codeContent = injectHighlightedLines(codeContent, highlightedLines);
|
|
357
|
+
const filenameHeader = filename ? `<div class="code-block-filename">${filename}</div>` : "";
|
|
358
|
+
return `<div class="code-block-wrapper" style="position: relative;">${filenameHeader}<button onclick="navigator.clipboard.writeText(decodeURIComponent('${escapedCode}')); this.textContent = 'Copied!'; setTimeout(() => this.textContent = 'Copy', 2000)" class="copy-code-btn" style="position: absolute; right: 0.5rem; top: ${filename ? "2.5rem" : "0.5rem"}; z-index: 10;">Copy</button>${codeContent}</div>`;
|
|
359
|
+
}
|
|
360
|
+
function headerAnchorsRenderer(info) {
|
|
361
|
+
const text = info.text ?? (this.parser ? this.parser.parseInline(info.tokens) : info.raw);
|
|
362
|
+
const depth = info.depth;
|
|
363
|
+
const id = text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-");
|
|
364
|
+
return `<h${depth} id="${id}"><a href="#${id}" class="header-anchor" aria-hidden="true">#</a>${text}</h${depth}>`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/engine.ts
|
|
368
|
+
function createMDConfig(config = {}) {
|
|
369
|
+
const markedInstance = new Marked({
|
|
370
|
+
gfm: true
|
|
371
|
+
});
|
|
372
|
+
if (config.highlighter) {
|
|
373
|
+
markedInstance.use(markedHighlight({
|
|
374
|
+
async: true,
|
|
375
|
+
highlight: async (code, lang) => {
|
|
376
|
+
if (!lang)
|
|
377
|
+
return code;
|
|
378
|
+
return await config.highlighter(code, lang);
|
|
379
|
+
}
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
382
|
+
const customRenderer = {};
|
|
383
|
+
if (config.features?.headerAnchors) {
|
|
384
|
+
customRenderer.heading = headerAnchorsRenderer;
|
|
385
|
+
}
|
|
386
|
+
if (config.features?.copyCodeButton) {
|
|
387
|
+
customRenderer.code = copyCodeButtonRenderer;
|
|
388
|
+
}
|
|
389
|
+
if (Object.keys(customRenderer).length > 0) {
|
|
390
|
+
markedInstance.use({ renderer: customRenderer });
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
parse: async (rawString) => {
|
|
394
|
+
let { content, meta } = extractFrontmatter(rawString);
|
|
395
|
+
const headings = extractHeadings(content);
|
|
396
|
+
content = await preprocessComponents(content, async (str) => await markedInstance.parse(str), async (str) => await markedInstance.parseInline(str), config.features, config.components);
|
|
397
|
+
const html = await markedInstance.parse(content);
|
|
398
|
+
return {
|
|
399
|
+
html,
|
|
400
|
+
meta,
|
|
401
|
+
headings
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
// src/adapters/shiki.ts
|
|
407
|
+
function shikiHighlighter(options = {}) {
|
|
408
|
+
let shikiInstance = null;
|
|
409
|
+
const theme = options.theme || "github-dark";
|
|
410
|
+
return async (code, language) => {
|
|
411
|
+
const { createHighlighter } = await import("shiki");
|
|
412
|
+
if (!shikiInstance) {
|
|
413
|
+
shikiInstance = await createHighlighter({
|
|
414
|
+
themes: typeof theme === "string" ? [theme] : [theme.light, theme.dark],
|
|
415
|
+
langs: options.langs || [language, "javascript", "typescript", "bash", "html", "css"]
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
if (!shikiInstance.getLoadedLanguages().includes(language)) {
|
|
419
|
+
await shikiInstance.loadLanguage(language).catch(() => {});
|
|
420
|
+
}
|
|
421
|
+
const shikiOptions = {
|
|
422
|
+
lang: language
|
|
423
|
+
};
|
|
424
|
+
if (typeof theme === "string") {
|
|
425
|
+
shikiOptions.theme = theme;
|
|
426
|
+
} else {
|
|
427
|
+
shikiOptions.themes = theme;
|
|
428
|
+
}
|
|
429
|
+
return shikiInstance.codeToHtml(code, shikiOptions);
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
export {
|
|
433
|
+
shikiHighlighter,
|
|
434
|
+
createMDConfig
|
|
435
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auwla-markdown",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -30,6 +30,6 @@
|
|
|
30
30
|
"marked-highlight": "^2.2.4"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
|
-
"build": "tsc --project tsconfig.lib.json"
|
|
33
|
+
"build": "bun -e \"import { rmSync } from 'fs'; rmSync('dist', { recursive: true, force: true })\" && bun build src/index.ts --outdir dist --target node --external shiki --external prismjs --external marked --external marked-highlight && tsc --project tsconfig.lib.json --emitDeclarationOnly"
|
|
34
34
|
}
|
|
35
35
|
}
|
package/dist/adapters/shiki.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export function shikiHighlighter(options = {}) {
|
|
2
|
-
let shikiInstance = null;
|
|
3
|
-
const theme = options.theme || 'github-dark';
|
|
4
|
-
return async (code, language) => {
|
|
5
|
-
// 1. Dynamically import Shiki ONLY when this function is actually called
|
|
6
|
-
const { createHighlighter } = await import('shiki');
|
|
7
|
-
// 2. Initialize it as a singleton on the first run
|
|
8
|
-
if (!shikiInstance) {
|
|
9
|
-
shikiInstance = await createHighlighter({
|
|
10
|
-
themes: typeof theme === 'string' ? [theme] : [theme.light, theme.dark],
|
|
11
|
-
langs: options.langs || [language, 'javascript', 'typescript', 'bash', 'html', 'css'],
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
// 3. Load the language if it hasn't been loaded yet (catch-all for unexpected languages)
|
|
15
|
-
if (!shikiInstance.getLoadedLanguages().includes(language)) {
|
|
16
|
-
await shikiInstance.loadLanguage(language).catch(() => { });
|
|
17
|
-
}
|
|
18
|
-
const shikiOptions = {
|
|
19
|
-
lang: language,
|
|
20
|
-
};
|
|
21
|
-
if (typeof theme === 'string') {
|
|
22
|
-
shikiOptions.theme = theme;
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
shikiOptions.themes = theme;
|
|
26
|
-
}
|
|
27
|
-
return shikiInstance.codeToHtml(code, shikiOptions);
|
|
28
|
-
};
|
|
29
|
-
}
|
package/dist/engine.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { Marked } from 'marked';
|
|
2
|
-
import { markedHighlight } from 'marked-highlight';
|
|
3
|
-
import { extractFrontmatter } from './frontmatter';
|
|
4
|
-
import { extractHeadings } from './headings';
|
|
5
|
-
import { preprocessComponents, copyCodeButtonRenderer, headerAnchorsRenderer, } from './features';
|
|
6
|
-
/** Configures and returns an isolated instance of the Markdown parsing engine. */
|
|
7
|
-
export function createMDConfig(config = {}) {
|
|
8
|
-
const markedInstance = new Marked({
|
|
9
|
-
gfm: true,
|
|
10
|
-
});
|
|
11
|
-
if (config.highlighter) {
|
|
12
|
-
markedInstance.use(markedHighlight({
|
|
13
|
-
async: true,
|
|
14
|
-
highlight: async (code, lang) => {
|
|
15
|
-
if (!lang)
|
|
16
|
-
return code;
|
|
17
|
-
return await config.highlighter(code, lang);
|
|
18
|
-
},
|
|
19
|
-
}));
|
|
20
|
-
}
|
|
21
|
-
// Register built-in features using custom Marked renderers
|
|
22
|
-
const customRenderer = {};
|
|
23
|
-
if (config.features?.headerAnchors) {
|
|
24
|
-
customRenderer.heading = headerAnchorsRenderer;
|
|
25
|
-
}
|
|
26
|
-
if (config.features?.copyCodeButton) {
|
|
27
|
-
customRenderer.code = copyCodeButtonRenderer;
|
|
28
|
-
}
|
|
29
|
-
if (Object.keys(customRenderer).length > 0) {
|
|
30
|
-
markedInstance.use({ renderer: customRenderer });
|
|
31
|
-
}
|
|
32
|
-
return {
|
|
33
|
-
parse: async (rawString) => {
|
|
34
|
-
let { content, meta } = extractFrontmatter(rawString);
|
|
35
|
-
const headings = extractHeadings(content);
|
|
36
|
-
// Preprocess component blocks and custom tags before sending to Marked
|
|
37
|
-
content = await preprocessComponents(content, async (str) => await markedInstance.parse(str), async (str) => await markedInstance.parseInline(str), config.features, config.components);
|
|
38
|
-
const html = await markedInstance.parse(content);
|
|
39
|
-
return {
|
|
40
|
-
html,
|
|
41
|
-
meta,
|
|
42
|
-
headings,
|
|
43
|
-
};
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
}
|
package/dist/features.js
DELETED
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
const inlineWrapperTags = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'a', 'li']);
|
|
2
|
-
export const defaultComponents = {
|
|
3
|
-
Callout: async (props, rawContent, parse, parseInline, features) => {
|
|
4
|
-
const type = props.type || 'note';
|
|
5
|
-
const title = props.title || type.toUpperCase();
|
|
6
|
-
const htmlContent = await parse(rawContent);
|
|
7
|
-
const isCollapsible = 'collapsible' in props && props.collapsible !== 'false';
|
|
8
|
-
const isCollapsed = props.collapsed === 'true';
|
|
9
|
-
const customClass = props.class ? ` ${props.class}` : '';
|
|
10
|
-
const className = `callout callout-${type.toLowerCase()}${customClass}`;
|
|
11
|
-
const otherAttrs = Object.entries(props)
|
|
12
|
-
.filter(([k]) => !['value', 'type', 'title', 'collapsible', 'collapsed', 'class'].includes(k))
|
|
13
|
-
.map(([k, v]) => `${k}="${v}"`)
|
|
14
|
-
.join(' ');
|
|
15
|
-
const attrString = otherAttrs ? ` ${otherAttrs}` : '';
|
|
16
|
-
if (isCollapsible) {
|
|
17
|
-
const openAttr = isCollapsed ? '' : ' open';
|
|
18
|
-
return `<details class="${className}"${openAttr}${attrString}><summary class="callout-title">${title}</summary><div class="callout-content">${htmlContent}</div></details>`;
|
|
19
|
-
}
|
|
20
|
-
return `<div class="${className}"${attrString}><div class="callout-title">${title}</div><div class="callout-content">${htmlContent}</div></div>`;
|
|
21
|
-
},
|
|
22
|
-
Tabs: async (props, rawContent, parse, parseInline, features) => {
|
|
23
|
-
const content = await parse(rawContent);
|
|
24
|
-
const regex = /<div class="tab-panel[^"]*"\s+data-title="([^"]*)"/g;
|
|
25
|
-
const titles = [];
|
|
26
|
-
let match;
|
|
27
|
-
while ((match = regex.exec(content)) !== null) {
|
|
28
|
-
titles.push(match[1]);
|
|
29
|
-
}
|
|
30
|
-
if (titles.length === 0)
|
|
31
|
-
return content;
|
|
32
|
-
const tabButtons = titles.map((title, idx) => {
|
|
33
|
-
const activeClass = idx === 0 ? 'active' : '';
|
|
34
|
-
const clickHandler = `const c = this.closest('.tabs-container'); c.querySelectorAll('.tab-btn').forEach((b, i) => { b.classList.toggle('active', b === this); c.querySelectorAll('.tab-panel')[i].style.display = (b === this) ? 'block' : 'none'; })`;
|
|
35
|
-
return `<button class="tab-btn ${activeClass}" onclick="${clickHandler}">${title}</button>`;
|
|
36
|
-
}).join('');
|
|
37
|
-
let panelIndex = 0;
|
|
38
|
-
const adjustedContent = content.replace(/class="tab-panel([^"]*)"/g, (match, customClasses) => {
|
|
39
|
-
const activeClass = panelIndex === 0 ? ' active' : '';
|
|
40
|
-
const display = panelIndex === 0 ? 'block' : 'none';
|
|
41
|
-
panelIndex++;
|
|
42
|
-
return `class="tab-panel${customClasses}${activeClass}" style="display: ${display};"`;
|
|
43
|
-
});
|
|
44
|
-
const customClass = props.class ? ` ${props.class}` : '';
|
|
45
|
-
const className = `tabs-container${customClass}`;
|
|
46
|
-
const otherAttrs = Object.entries(props)
|
|
47
|
-
.filter(([k]) => !['value', 'class'].includes(k))
|
|
48
|
-
.map(([k, v]) => `${k}="${v}"`)
|
|
49
|
-
.join(' ');
|
|
50
|
-
const attrString = otherAttrs ? ` ${otherAttrs}` : '';
|
|
51
|
-
return `<div class="${className}"${attrString}><div class="tabs-header">${tabButtons}</div><div class="tabs-content">${adjustedContent}</div></div>`;
|
|
52
|
-
},
|
|
53
|
-
Tab: async (props, rawContent, parse, parseInline, features) => {
|
|
54
|
-
const title = props.title || '';
|
|
55
|
-
const content = await parse(rawContent);
|
|
56
|
-
const customClass = props.class ? ` ${props.class}` : '';
|
|
57
|
-
const className = `tab-panel${customClass}`;
|
|
58
|
-
const otherAttrs = Object.entries(props)
|
|
59
|
-
.filter(([k]) => !['value', 'title', 'class'].includes(k))
|
|
60
|
-
.map(([k, v]) => `${k}="${v}"`)
|
|
61
|
-
.join(' ');
|
|
62
|
-
const attrString = otherAttrs ? ` ${otherAttrs}` : '';
|
|
63
|
-
return `<div class="${className}" data-title="${title}"${attrString}>${content}</div>`;
|
|
64
|
-
},
|
|
65
|
-
Table: async (props, rawContent, parse, parseInline, features) => {
|
|
66
|
-
const content = await parseInline(rawContent);
|
|
67
|
-
const defaultClass = 'auwla-table';
|
|
68
|
-
const customClass = props.class ? ` ${props.class}` : '';
|
|
69
|
-
const mergedClass = `${defaultClass}${customClass}`;
|
|
70
|
-
const attrPairs = Object.entries(props)
|
|
71
|
-
.filter(([k]) => k !== 'value' && k !== 'class')
|
|
72
|
-
.map(([k, v]) => `${k}="${v}"`)
|
|
73
|
-
.join(' ');
|
|
74
|
-
const attrString = attrPairs ? ` ${attrPairs}` : '';
|
|
75
|
-
return `<table class="${mergedClass}"${attrString}>${content}</table>`;
|
|
76
|
-
},
|
|
77
|
-
Row: async (props, rawContent, parse, parseInline, features) => {
|
|
78
|
-
const content = await parseInline(rawContent);
|
|
79
|
-
const attrPairs = Object.entries(props)
|
|
80
|
-
.filter(([k]) => k !== 'value')
|
|
81
|
-
.map(([k, v]) => `${k}="${v}"`)
|
|
82
|
-
.join(' ');
|
|
83
|
-
const attrString = attrPairs ? ` ${attrPairs}` : '';
|
|
84
|
-
return `<tr${attrString}>${content}</tr>`;
|
|
85
|
-
},
|
|
86
|
-
Column: async (props, rawContent, parse, parseInline, features) => {
|
|
87
|
-
const content = await parseInline(rawContent);
|
|
88
|
-
const attrPairs = Object.entries(props)
|
|
89
|
-
.filter(([k]) => k !== 'value')
|
|
90
|
-
.map(([k, v]) => `${k}="${v}"`)
|
|
91
|
-
.join(' ');
|
|
92
|
-
const attrString = attrPairs ? ` ${attrPairs}` : '';
|
|
93
|
-
return `<th${attrString}>${content}</th>`;
|
|
94
|
-
},
|
|
95
|
-
Cell: async (props, rawContent, parse, parseInline, features) => {
|
|
96
|
-
const content = await parseInline(rawContent);
|
|
97
|
-
const attrPairs = Object.entries(props)
|
|
98
|
-
.filter(([k]) => k !== 'value')
|
|
99
|
-
.map(([k, v]) => `${k}="${v}"`)
|
|
100
|
-
.join(' ');
|
|
101
|
-
const attrString = attrPairs ? ` ${attrPairs}` : '';
|
|
102
|
-
return `<td${attrString}>${content}</td>`;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
function parseAttributes(attrString) {
|
|
106
|
-
const attrs = {};
|
|
107
|
-
const trimmed = attrString.trim();
|
|
108
|
-
if (!trimmed)
|
|
109
|
-
return attrs;
|
|
110
|
-
if (trimmed.startsWith('=')) {
|
|
111
|
-
const match = trimmed.match(/^=\s*(?:"(.*?)"|'(.*?)'|([^\s>]+))/);
|
|
112
|
-
if (match) {
|
|
113
|
-
attrs.value = match[1] ?? match[2] ?? match[3] ?? '';
|
|
114
|
-
return attrs;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
const regex = /(\w+)(?:\s*=\s*(?:"(.*?)"|'(.*?)'|([^\s>]+)))?/g;
|
|
118
|
-
let match;
|
|
119
|
-
while ((match = regex.exec(attrString)) !== null) {
|
|
120
|
-
const key = match[1];
|
|
121
|
-
const val = match[2] ?? match[3] ?? match[4] ?? 'true';
|
|
122
|
-
attrs[key] = val;
|
|
123
|
-
}
|
|
124
|
-
return attrs;
|
|
125
|
-
}
|
|
126
|
-
async function renderComponent(tagName, props, rawContent, parseFn, parseInlineFn, features, customComponents) {
|
|
127
|
-
if (customComponents[tagName]) {
|
|
128
|
-
return await customComponents[tagName](props, rawContent, parseFn, parseInlineFn, features);
|
|
129
|
-
}
|
|
130
|
-
if (defaultComponents[tagName]) {
|
|
131
|
-
return await defaultComponents[tagName](props, rawContent, parseFn, parseInlineFn, features);
|
|
132
|
-
}
|
|
133
|
-
const isInline = inlineWrapperTags.has(tagName.toLowerCase());
|
|
134
|
-
const compiledContent = isInline ? await parseInlineFn(rawContent) : await parseFn(rawContent);
|
|
135
|
-
// Match custom tag heading (e.g. h1, h2, h3, etc.)
|
|
136
|
-
if (isInline && tagName.toLowerCase().startsWith('h') && tagName.length === 2 && !isNaN(Number(tagName.slice(1)))) {
|
|
137
|
-
const depth = tagName.toLowerCase().slice(1);
|
|
138
|
-
const plainText = compiledContent.replace(/<[^>]*>/g, '');
|
|
139
|
-
const id = props.id || plainText
|
|
140
|
-
.toLowerCase()
|
|
141
|
-
.replace(/[^\w\s-]/g, '')
|
|
142
|
-
.replace(/\s+/g, '-');
|
|
143
|
-
const idAttr = `id="${id}"`;
|
|
144
|
-
const classAttr = props.class ? `class="${props.class}"` : '';
|
|
145
|
-
const otherAttrs = Object.entries(props)
|
|
146
|
-
.filter(([k]) => k !== 'value' && k !== 'id' && k !== 'class')
|
|
147
|
-
.map(([k, v]) => `${k}="${v}"`)
|
|
148
|
-
.join(' ');
|
|
149
|
-
const attrString = [idAttr, classAttr, otherAttrs].filter(Boolean).map(s => s.trim()).join(' ');
|
|
150
|
-
const finalAttrString = attrString ? ` ${attrString}` : '';
|
|
151
|
-
if (features?.headerAnchors) {
|
|
152
|
-
return `<h${depth}${finalAttrString}><a href="#${id}" class="header-anchor" aria-hidden="true">#</a>${compiledContent}</h${depth}>`;
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
return `<h${depth}${finalAttrString}>${compiledContent}</h${depth}>`;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
const attrPairs = Object.entries(props).filter(([k]) => k !== 'value').map(([k, v]) => `${k}="${v}"`).join(' ');
|
|
159
|
-
const attrString = attrPairs ? ` ${attrPairs}` : '';
|
|
160
|
-
return `<${tagName}${attrString}>${compiledContent}</${tagName}>`;
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Preprocesses component-like tags (=<TagName>) inside a raw markdown string.
|
|
164
|
-
* Looks up default and custom components, compiles, and injects HTML.
|
|
165
|
-
*/
|
|
166
|
-
export async function preprocessComponents(rawMarkdown, parseFn, parseInlineFn, features = {}, customComponents = {}) {
|
|
167
|
-
const lines = rawMarkdown.split('\n');
|
|
168
|
-
const result = [];
|
|
169
|
-
const stack = [];
|
|
170
|
-
for (const line of lines) {
|
|
171
|
-
const trimmed = line.trim();
|
|
172
|
-
// 1. Single-line components (e.g. =<Column>Name=</Column>)
|
|
173
|
-
const singleLineMatch = trimmed.match(/^=<(\w+)([^>]*?)>(.*?)=<\/\1>$/);
|
|
174
|
-
if (singleLineMatch) {
|
|
175
|
-
const tagName = singleLineMatch[1];
|
|
176
|
-
const attrString = singleLineMatch[2] || '';
|
|
177
|
-
const innerContent = singleLineMatch[3] || '';
|
|
178
|
-
const props = parseAttributes(attrString);
|
|
179
|
-
const html = await renderComponent(tagName, props, innerContent, parseFn, parseInlineFn, features, customComponents);
|
|
180
|
-
if (stack.length > 0) {
|
|
181
|
-
stack[stack.length - 1].contentLines.push(html);
|
|
182
|
-
}
|
|
183
|
-
else {
|
|
184
|
-
result.push(html);
|
|
185
|
-
}
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
// 2. Self-closing components
|
|
189
|
-
if (trimmed.startsWith('=<') && trimmed.endsWith('/>')) {
|
|
190
|
-
const match = trimmed.match(/^=<(\w+)([^>]*?)\/>$/);
|
|
191
|
-
if (match) {
|
|
192
|
-
const tagName = match[1];
|
|
193
|
-
const attrString = match[2] || '';
|
|
194
|
-
const props = parseAttributes(attrString);
|
|
195
|
-
const html = await renderComponent(tagName, props, '', parseFn, parseInlineFn, features, customComponents);
|
|
196
|
-
if (stack.length > 0) {
|
|
197
|
-
stack[stack.length - 1].contentLines.push(html);
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
result.push(html);
|
|
201
|
-
}
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
// 3. Opening component tags
|
|
206
|
-
if (trimmed.startsWith('=<') && !trimmed.startsWith('=</') && trimmed.endsWith('>')) {
|
|
207
|
-
const match = trimmed.match(/^=<(\w+)([^>]*?)>$/);
|
|
208
|
-
if (match) {
|
|
209
|
-
const tagName = match[1];
|
|
210
|
-
const attrString = match[2] || '';
|
|
211
|
-
const props = parseAttributes(attrString);
|
|
212
|
-
stack.push({
|
|
213
|
-
tagName,
|
|
214
|
-
props,
|
|
215
|
-
contentLines: []
|
|
216
|
-
});
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
// 4. Closing component tags
|
|
221
|
-
if (trimmed.startsWith('=</') && trimmed.endsWith('>')) {
|
|
222
|
-
const match = trimmed.match(/^=<\/(.*?)>$/);
|
|
223
|
-
if (match) {
|
|
224
|
-
const tagName = match[1].trim();
|
|
225
|
-
if (stack.length > 0 && stack[stack.length - 1].tagName === tagName) {
|
|
226
|
-
const block = stack.pop();
|
|
227
|
-
const rawContent = block.contentLines.join('\n');
|
|
228
|
-
const html = await renderComponent(block.tagName, block.props, rawContent, parseFn, parseInlineFn, features, customComponents);
|
|
229
|
-
if (stack.length > 0) {
|
|
230
|
-
stack[stack.length - 1].contentLines.push(html);
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
result.push(html);
|
|
234
|
-
}
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
// 5. Standard line
|
|
240
|
-
if (stack.length > 0) {
|
|
241
|
-
stack[stack.length - 1].contentLines.push(line);
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
result.push(line);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
while (stack.length > 0) {
|
|
248
|
-
const block = stack.pop();
|
|
249
|
-
result.push(`=<${block.tagName}>`);
|
|
250
|
-
result.push(block.contentLines.join('\n'));
|
|
251
|
-
result.push(`=</${block.tagName}>`);
|
|
252
|
-
}
|
|
253
|
-
return result.join('\n');
|
|
254
|
-
}
|
|
255
|
-
function extractRawCode(raw) {
|
|
256
|
-
if (raw.startsWith('```') || raw.startsWith('~~~')) {
|
|
257
|
-
const lines = raw.split('\n');
|
|
258
|
-
return lines.slice(1, -1).join('\n');
|
|
259
|
-
}
|
|
260
|
-
return raw;
|
|
261
|
-
}
|
|
262
|
-
function parseCodeMeta(langString) {
|
|
263
|
-
const parts = langString.trim().split(/\s+/);
|
|
264
|
-
const lang = parts[0] || '';
|
|
265
|
-
let filename;
|
|
266
|
-
const highlightedLines = new Set();
|
|
267
|
-
for (const part of parts.slice(1)) {
|
|
268
|
-
if (part.startsWith('[') && part.endsWith(']')) {
|
|
269
|
-
filename = part.slice(1, -1);
|
|
270
|
-
}
|
|
271
|
-
else if (part.startsWith('{') && part.endsWith('}')) {
|
|
272
|
-
const ranges = part.slice(1, -1).split(',');
|
|
273
|
-
for (const range of ranges) {
|
|
274
|
-
if (range.includes('-')) {
|
|
275
|
-
const [startStr, endStr] = range.split('-');
|
|
276
|
-
const start = parseInt(startStr || '0', 10);
|
|
277
|
-
const end = parseInt(endStr || '0', 10);
|
|
278
|
-
if (!isNaN(start) && !isNaN(end)) {
|
|
279
|
-
for (let i = start; i <= end; i++) {
|
|
280
|
-
highlightedLines.add(i);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
const val = parseInt(range, 10);
|
|
286
|
-
if (!isNaN(val)) {
|
|
287
|
-
highlightedLines.add(val);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
return { lang, filename, highlightedLines };
|
|
294
|
-
}
|
|
295
|
-
function injectHighlightedLines(html, highlightedLines) {
|
|
296
|
-
if (highlightedLines.size === 0)
|
|
297
|
-
return html;
|
|
298
|
-
if (html.includes('class="line"')) {
|
|
299
|
-
let lineIndex = 0;
|
|
300
|
-
return html.replace(/class="line"/g, () => {
|
|
301
|
-
lineIndex++;
|
|
302
|
-
if (highlightedLines.has(lineIndex)) {
|
|
303
|
-
return 'class="line highlighted-line"';
|
|
304
|
-
}
|
|
305
|
-
return 'class="line"';
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
const lines = html.split('\n');
|
|
309
|
-
const processed = lines.map((line, idx) => {
|
|
310
|
-
const lineNum = idx + 1;
|
|
311
|
-
if (highlightedLines.has(lineNum)) {
|
|
312
|
-
return `<div class="highlighted-line">${line}</div>`;
|
|
313
|
-
}
|
|
314
|
-
return line;
|
|
315
|
-
});
|
|
316
|
-
return processed.join('\n');
|
|
317
|
-
}
|
|
318
|
-
/** Custom Marked renderer for a button that copies code snippets to clipboard. */
|
|
319
|
-
export function copyCodeButtonRenderer(info) {
|
|
320
|
-
const rawCode = extractRawCode(info.raw);
|
|
321
|
-
const escapedCode = encodeURIComponent(rawCode).replace(/'/g, '%27');
|
|
322
|
-
const { lang, filename, highlightedLines } = parseCodeMeta(info.lang ?? '');
|
|
323
|
-
let codeContent = info.text.trim().startsWith('<pre')
|
|
324
|
-
? info.text
|
|
325
|
-
: `<pre><code class="language-${lang}">${info.text}</code></pre>`;
|
|
326
|
-
codeContent = injectHighlightedLines(codeContent, highlightedLines);
|
|
327
|
-
const filenameHeader = filename
|
|
328
|
-
? `<div class="code-block-filename">${filename}</div>`
|
|
329
|
-
: '';
|
|
330
|
-
return `<div class="code-block-wrapper" style="position: relative;">${filenameHeader}<button onclick="navigator.clipboard.writeText(decodeURIComponent('${escapedCode}')); this.textContent = 'Copied!'; setTimeout(() => this.textContent = 'Copy', 2000)" class="copy-code-btn" style="position: absolute; right: 0.5rem; top: ${filename ? '2.5rem' : '0.5rem'}; z-index: 10;">Copy</button>${codeContent}</div>`;
|
|
331
|
-
}
|
|
332
|
-
/** Custom Marked renderer for adding hoverable anchors pointing to header IDs. */
|
|
333
|
-
export function headerAnchorsRenderer(info) {
|
|
334
|
-
const text = info.text ?? (this.parser ? this.parser.parseInline(info.tokens) : info.raw);
|
|
335
|
-
const depth = info.depth;
|
|
336
|
-
const id = text
|
|
337
|
-
.toLowerCase()
|
|
338
|
-
.replace(/[^\w\s-]/g, '')
|
|
339
|
-
.replace(/\s+/g, '-');
|
|
340
|
-
return `<h${depth} id="${id}"><a href="#${id}" class="header-anchor" aria-hidden="true">#</a>${text}</h${depth}>`;
|
|
341
|
-
}
|
package/dist/frontmatter.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parses header-based metadata blocks (e.g. key: value lines enclosed within =<Header> ... =</Header> tags)
|
|
3
|
-
* at the top of a Markdown document and removes them from the parsed content body.
|
|
4
|
-
*/
|
|
5
|
-
export function extractFrontmatter(rawString) {
|
|
6
|
-
const match = rawString.match(/^=<Header(?:[^>]*)>\r?\n([\s\S]*?)\r?\n=<\/Header>\r?\n([\s\S]*)$/);
|
|
7
|
-
if (!match)
|
|
8
|
-
return { content: rawString, meta: {} };
|
|
9
|
-
const rawMeta = match[1];
|
|
10
|
-
const content = match[2];
|
|
11
|
-
const meta = {};
|
|
12
|
-
for (const line of rawMeta.split('\n')) {
|
|
13
|
-
const sep = line.indexOf(':');
|
|
14
|
-
if (sep === -1)
|
|
15
|
-
continue;
|
|
16
|
-
const key = line.slice(0, sep).trim();
|
|
17
|
-
const val = line.slice(sep + 1).trim();
|
|
18
|
-
if (val === 'true') {
|
|
19
|
-
meta[key] = true;
|
|
20
|
-
}
|
|
21
|
-
else if (val === 'false') {
|
|
22
|
-
meta[key] = false;
|
|
23
|
-
}
|
|
24
|
-
else if (!isNaN(Number(val)) && val !== '') {
|
|
25
|
-
meta[key] = Number(val);
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
meta[key] = val.replace(/^["']|["']$/g, ''); // strip optional wrapping quotes
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return { content, meta };
|
|
32
|
-
}
|
package/dist/headings.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scans a Markdown content string and extracts all headings (level 1-6)
|
|
3
|
-
* to populate the table of contents. Supports both standard # headers and custom tag-based headings (=<h1>...=</h1>).
|
|
4
|
-
*/
|
|
5
|
-
export function extractHeadings(content) {
|
|
6
|
-
const headings = [];
|
|
7
|
-
const lines = content.split('\n');
|
|
8
|
-
let inCodeBlock = false;
|
|
9
|
-
for (const line of lines) {
|
|
10
|
-
const trimmed = line.trim();
|
|
11
|
-
if (trimmed.startsWith('```')) {
|
|
12
|
-
inCodeBlock = !inCodeBlock;
|
|
13
|
-
continue;
|
|
14
|
-
}
|
|
15
|
-
if (inCodeBlock)
|
|
16
|
-
continue;
|
|
17
|
-
// 1. Match custom single-line tag headings, e.g. =<h1>Getting Started=</h1>
|
|
18
|
-
const tagMatch = trimmed.match(/^=<h([1-6])(?:[^>]*?)>(.*?)=<\/h\1>$/);
|
|
19
|
-
if (tagMatch) {
|
|
20
|
-
const level = parseInt(tagMatch[1], 10);
|
|
21
|
-
const text = tagMatch[2].trim();
|
|
22
|
-
const id = text
|
|
23
|
-
.toLowerCase()
|
|
24
|
-
.replace(/[^\w\s-]/g, '')
|
|
25
|
-
.replace(/\s+/g, '-');
|
|
26
|
-
headings.push({ level, text, id });
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
// 2. Match standard markdown headings, e.g. # Title
|
|
30
|
-
const match = line.match(/^(#{1,6})\s+(.+)$/);
|
|
31
|
-
if (match) {
|
|
32
|
-
const level = match[1].length;
|
|
33
|
-
const text = match[2].trim();
|
|
34
|
-
const id = text
|
|
35
|
-
.toLowerCase()
|
|
36
|
-
.replace(/[^\w\s-]/g, '')
|
|
37
|
-
.replace(/\s+/g, '-');
|
|
38
|
-
headings.push({ level, text, id });
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return headings;
|
|
42
|
-
}
|
package/dist/types.js
DELETED
|
File without changes
|