auwla-markdown 0.0.1 → 0.0.3

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.
@@ -0,0 +1,10 @@
1
+ import type { HighlighterAdapter } from '../types';
2
+ import type { BundledLanguage, BundledTheme } from 'shiki';
3
+ export interface ShikiAdapterOptions {
4
+ theme?: BundledTheme | {
5
+ light: BundledTheme;
6
+ dark: BundledTheme;
7
+ };
8
+ langs?: BundledLanguage[];
9
+ }
10
+ export declare function shikiHighlighter(options?: ShikiAdapterOptions): HighlighterAdapter;
@@ -0,0 +1,29 @@
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
+ }
@@ -0,0 +1,3 @@
1
+ import type { MarkdownConfig, MarkdownEngine } from './types';
2
+ /** Configures and returns an isolated instance of the Markdown parsing engine. */
3
+ export declare function createMDConfig(config?: MarkdownConfig): MarkdownEngine;
package/dist/engine.js ADDED
@@ -0,0 +1,46 @@
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
+ }
@@ -0,0 +1,11 @@
1
+ import type { ComponentRenderer, MarkdownFeatures } from './types';
2
+ export declare const defaultComponents: Record<string, ComponentRenderer>;
3
+ /**
4
+ * Preprocesses component-like tags (=<TagName>) inside a raw markdown string.
5
+ * Looks up default and custom components, compiles, and injects HTML.
6
+ */
7
+ export declare function preprocessComponents(rawMarkdown: string, parseFn: (str: string) => Promise<string>, parseInlineFn: (str: string) => Promise<string>, features?: MarkdownFeatures, customComponents?: Record<string, ComponentRenderer>): Promise<string>;
8
+ /** Custom Marked renderer for a button that copies code snippets to clipboard. */
9
+ export declare function copyCodeButtonRenderer(info: any): string;
10
+ /** Custom Marked renderer for adding hoverable anchors pointing to header IDs. */
11
+ export declare function headerAnchorsRenderer(this: any, info: any): string;
@@ -0,0 +1,341 @@
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
+ }
@@ -0,0 +1,8 @@
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 declare function extractFrontmatter(rawString: string): {
6
+ content: string;
7
+ meta: Record<string, any>;
8
+ };
@@ -0,0 +1,32 @@
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
+ }
@@ -0,0 +1,9 @@
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 declare function extractHeadings(content: string): Array<{
6
+ level: number;
7
+ text: string;
8
+ id: string;
9
+ }>;
@@ -0,0 +1,42 @@
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/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createMDConfig } from './engine';
2
+ export { shikiHighlighter } from './adapters/shiki';
@@ -0,0 +1,25 @@
1
+ export type HighlighterAdapter = (code: string, language: string) => Promise<string> | string;
2
+ export interface MarkdownFeatures {
3
+ copyCodeButton?: boolean;
4
+ tabs?: boolean;
5
+ callouts?: boolean;
6
+ headerAnchors?: boolean;
7
+ }
8
+ export type ComponentRenderer = (props: Record<string, string>, rawContent: string, parseFn: (str: string) => Promise<string>, parseInlineFn: (str: string) => Promise<string>, features?: MarkdownFeatures) => Promise<string> | string;
9
+ export interface MarkdownConfig {
10
+ highlighter?: HighlighterAdapter | false;
11
+ features?: MarkdownFeatures;
12
+ components?: Record<string, ComponentRenderer>;
13
+ }
14
+ export interface ParsedMarkdown {
15
+ html: string;
16
+ meta: Record<string, any>;
17
+ headings: Array<{
18
+ level: number;
19
+ text: string;
20
+ id: string;
21
+ }>;
22
+ }
23
+ export interface MarkdownEngine {
24
+ parse: (rawString: string) => Promise<ParsedMarkdown>;
25
+ }
package/dist/types.js ADDED
File without changes
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "auwla-markdown",
3
- "version": "0.0.1",
4
- "main": "src/index.ts",
5
- "module": "src/index.ts",
6
- "types": "src/index.ts",
3
+ "version": "0.0.3",
4
+ "main": "./dist/index.js",
5
+ "module": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
7
  "type": "module",
8
+ "files": [
9
+ "dist"
10
+ ],
8
11
  "devDependencies": {
9
12
  "@types/bun": "latest",
10
13
  "@types/prismjs": "^1.26.6",
@@ -25,5 +28,8 @@
25
28
  "dependencies": {
26
29
  "marked": "^18.0.5",
27
30
  "marked-highlight": "^2.2.4"
31
+ },
32
+ "scripts": {
33
+ "build": "tsc --project tsconfig.lib.json"
28
34
  }
29
35
  }