quasar-ui-danx 0.4.99 → 0.5.1
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/danx.es.js +17884 -12732
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +192 -118
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +11 -2
- package/scripts/publish.sh +76 -0
- package/src/components/Utility/Code/CodeViewer.vue +31 -14
- package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
- package/src/components/Utility/Code/CodeViewerFooter.vue +1 -1
- package/src/components/Utility/Code/LanguageBadge.vue +278 -5
- package/src/components/Utility/Code/MarkdownContent.vue +160 -6
- package/src/components/Utility/Code/index.ts +3 -0
- package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
- package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
- package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
- package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
- package/src/components/Utility/Markdown/MarkdownEditor.vue +228 -0
- package/src/components/Utility/Markdown/MarkdownEditorContent.vue +235 -0
- package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
- package/src/components/Utility/Markdown/TablePopover.vue +420 -0
- package/src/components/Utility/Markdown/index.ts +11 -0
- package/src/components/Utility/Markdown/types.ts +27 -0
- package/src/components/Utility/index.ts +1 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
- package/src/composables/markdown/features/useBlockquotes.ts +248 -0
- package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
- package/src/composables/markdown/features/useCodeBlocks.spec.ts +779 -0
- package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
- package/src/composables/markdown/features/useContextMenu.ts +444 -0
- package/src/composables/markdown/features/useFocusTracking.ts +116 -0
- package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
- package/src/composables/markdown/features/useHeadings.ts +290 -0
- package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
- package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
- package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
- package/src/composables/markdown/features/useLinks.spec.ts +369 -0
- package/src/composables/markdown/features/useLinks.ts +374 -0
- package/src/composables/markdown/features/useLists.spec.ts +834 -0
- package/src/composables/markdown/features/useLists.ts +747 -0
- package/src/composables/markdown/features/usePopoverManager.ts +181 -0
- package/src/composables/markdown/features/useTables.spec.ts +1601 -0
- package/src/composables/markdown/features/useTables.ts +1107 -0
- package/src/composables/markdown/index.ts +16 -0
- package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
- package/src/composables/markdown/useMarkdownEditor.ts +1068 -0
- package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
- package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
- package/src/composables/markdown/useMarkdownSelection.ts +219 -0
- package/src/composables/markdown/useMarkdownSync.ts +549 -0
- package/src/composables/useCodeViewerEditor.spec.ts +655 -0
- package/src/composables/useCodeViewerEditor.ts +174 -20
- package/src/helpers/formats/index.ts +1 -1
- package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
- package/src/helpers/formats/markdown/escapeSequences.ts +60 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +412 -0
- package/src/helpers/formats/markdown/index.ts +92 -0
- package/src/helpers/formats/markdown/linePatterns.spec.ts +495 -0
- package/src/helpers/formats/markdown/linePatterns.ts +172 -0
- package/src/helpers/formats/markdown/parseInline.ts +124 -0
- package/src/helpers/formats/markdown/render/index.ts +92 -0
- package/src/helpers/formats/markdown/render/renderFootnotes.ts +30 -0
- package/src/helpers/formats/markdown/render/renderList.ts +69 -0
- package/src/helpers/formats/markdown/render/renderTable.ts +38 -0
- package/src/helpers/formats/markdown/state.ts +58 -0
- package/src/helpers/formats/markdown/tokenize/extractDefinitions.ts +39 -0
- package/src/helpers/formats/markdown/tokenize/index.ts +139 -0
- package/src/helpers/formats/markdown/tokenize/parseBlockquote.ts +34 -0
- package/src/helpers/formats/markdown/tokenize/parseCodeBlock.ts +85 -0
- package/src/helpers/formats/markdown/tokenize/parseDefinitionList.ts +88 -0
- package/src/helpers/formats/markdown/tokenize/parseHeading.ts +65 -0
- package/src/helpers/formats/markdown/tokenize/parseHorizontalRule.ts +22 -0
- package/src/helpers/formats/markdown/tokenize/parseList.ts +119 -0
- package/src/helpers/formats/markdown/tokenize/parseParagraph.ts +59 -0
- package/src/helpers/formats/markdown/tokenize/parseTable.ts +70 -0
- package/src/helpers/formats/markdown/tokenize/parseTaskList.ts +47 -0
- package/src/helpers/formats/markdown/tokenize/utils.ts +25 -0
- package/src/helpers/formats/markdown/types.ts +63 -0
- package/src/styles/danx.scss +1 -0
- package/src/styles/themes/danx/markdown.scss +96 -0
- package/src/test/helpers/editorTestUtils.spec.ts +296 -0
- package/src/test/helpers/editorTestUtils.ts +253 -0
- package/src/test/helpers/index.ts +1 -0
- package/src/test/setup.test.ts +12 -0
- package/src/test/setup.ts +12 -0
- package/vitest.config.ts +19 -0
- package/src/helpers/formats/renderMarkdown.ts +0 -338
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline markdown element parser
|
|
3
|
+
* Handles: bold, italic, strikethrough, highlight, superscript, subscript,
|
|
4
|
+
* links, images, reference-style links, footnotes, autolinks, code
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { escapeHtml } from "./escapeHtml";
|
|
8
|
+
import { applyEscapes, revertEscapes } from "./escapeSequences";
|
|
9
|
+
import { getLinkRefs, getFootnotes } from "./state";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse inline markdown elements within text
|
|
13
|
+
* Order matters: more specific patterns first
|
|
14
|
+
*/
|
|
15
|
+
export function parseInline(text: string, sanitize: boolean = true): string {
|
|
16
|
+
if (!text) return "";
|
|
17
|
+
|
|
18
|
+
const currentLinkRefs = getLinkRefs();
|
|
19
|
+
const currentFootnotes = getFootnotes();
|
|
20
|
+
|
|
21
|
+
// Escape HTML if sanitizing (before applying markdown)
|
|
22
|
+
let result = sanitize ? escapeHtml(text) : text;
|
|
23
|
+
|
|
24
|
+
// 1. ESCAPE SEQUENCES - Pre-process backslash escapes to placeholders
|
|
25
|
+
// Must be first so escaped characters aren't treated as markdown
|
|
26
|
+
result = applyEscapes(result);
|
|
27
|
+
|
|
28
|
+
// 2. HARD LINE BREAKS - Two trailing spaces + newline becomes <br />
|
|
29
|
+
result = result.replace(/ {2,}\n/g, "<br />\n");
|
|
30
|
+
|
|
31
|
+
// 3. AUTOLINKS - Must be before regular link parsing
|
|
32
|
+
// URL autolinks: <https://example.com>
|
|
33
|
+
result = result.replace(/<(https?:\/\/[^&]+)>/g, '<a href="$1">$1</a>');
|
|
34
|
+
// Email autolinks: <user@example.com>
|
|
35
|
+
result = result.replace(/<([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>/g, '<a href="mailto:$1">$1</a>');
|
|
36
|
+
|
|
37
|
+
// 4. FOOTNOTE REFERENCES: [^id]
|
|
38
|
+
// Must be before regular link/image parsing to avoid conflicts
|
|
39
|
+
result = result.replace(/\[\^([^\]]+)\]/g, (match, fnId) => {
|
|
40
|
+
const fn = currentFootnotes[fnId];
|
|
41
|
+
if (fn) {
|
|
42
|
+
return `<sup class="footnote-ref"><a href="#fn-${fnId}" id="fnref-${fnId}">[${fn.index}]</a></sup>`;
|
|
43
|
+
}
|
|
44
|
+
return match; // Keep original if footnote not defined
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// 5. IMAGES:  - must be before links
|
|
48
|
+
result = result.replace(
|
|
49
|
+
/!\[([^\]]*)\]\(([^)]+)\)/g,
|
|
50
|
+
'<img src="$2" alt="$1" />'
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// 6. INLINE LINKS: [text](url)
|
|
54
|
+
result = result.replace(
|
|
55
|
+
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
56
|
+
'<a href="$2">$1</a>'
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// 7. REFERENCE-STYLE LINKS - Process after regular links
|
|
60
|
+
// Full reference: [text][ref-id]
|
|
61
|
+
result = result.replace(/\[([^\]]+)\]\[([^\]]+)\]/g, (match, text, refId) => {
|
|
62
|
+
const ref = currentLinkRefs[refId.toLowerCase()];
|
|
63
|
+
if (ref) {
|
|
64
|
+
const title = ref.title ? ` title="${escapeHtml(ref.title)}"` : "";
|
|
65
|
+
return `<a href="${ref.url}"${title}>${text}</a>`;
|
|
66
|
+
}
|
|
67
|
+
return match; // Keep original if ref not found
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Collapsed reference: [text][]
|
|
71
|
+
result = result.replace(/\[([^\]]+)\]\[\]/g, (match, text) => {
|
|
72
|
+
const ref = currentLinkRefs[text.toLowerCase()];
|
|
73
|
+
if (ref) {
|
|
74
|
+
const title = ref.title ? ` title="${escapeHtml(ref.title)}"` : "";
|
|
75
|
+
return `<a href="${ref.url}"${title}>${text}</a>`;
|
|
76
|
+
}
|
|
77
|
+
return match;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Shortcut reference: [ref] alone (must not match [text](url) or [text][ref])
|
|
81
|
+
// Only match [word] not followed by ( or [
|
|
82
|
+
result = result.replace(/\[([^\]]+)\](?!\(|\[)/g, (match, text) => {
|
|
83
|
+
const ref = currentLinkRefs[text.toLowerCase()];
|
|
84
|
+
if (ref) {
|
|
85
|
+
const title = ref.title ? ` title="${escapeHtml(ref.title)}"` : "";
|
|
86
|
+
return `<a href="${ref.url}"${title}>${text}</a>`;
|
|
87
|
+
}
|
|
88
|
+
return match;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// 8. INLINE CODE: `code`
|
|
92
|
+
result = result.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
93
|
+
|
|
94
|
+
// 9. STRIKETHROUGH: ~~text~~ - Must be before subscript (single tilde)
|
|
95
|
+
result = result.replace(/~~([^~]+)~~/g, "<del>$1</del>");
|
|
96
|
+
|
|
97
|
+
// 10. HIGHLIGHT: ==text==
|
|
98
|
+
result = result.replace(/==([^=]+)==/g, "<mark>$1</mark>");
|
|
99
|
+
|
|
100
|
+
// 11. SUPERSCRIPT: X^2^ - Must be before subscript
|
|
101
|
+
result = result.replace(/\^([^\^]+)\^/g, "<sup>$1</sup>");
|
|
102
|
+
|
|
103
|
+
// 12. SUBSCRIPT: H~2~O - Single tilde, use negative lookbehind/lookahead to avoid ~~
|
|
104
|
+
result = result.replace(/(?<!~)~([^~]+)~(?!~)/g, "<sub>$1</sub>");
|
|
105
|
+
|
|
106
|
+
// 13. BOLD + ITALIC: ***text*** or ___text___
|
|
107
|
+
result = result.replace(/\*\*\*([^*]+)\*\*\*/g, "<strong><em>$1</em></strong>");
|
|
108
|
+
result = result.replace(/___([^_]+)___/g, "<strong><em>$1</em></strong>");
|
|
109
|
+
|
|
110
|
+
// 14. BOLD: **text** or __text__
|
|
111
|
+
result = result.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
112
|
+
result = result.replace(/__([^_]+)__/g, "<strong>$1</strong>");
|
|
113
|
+
|
|
114
|
+
// 15. ITALIC: *text* or _text_ (but not inside words for underscores)
|
|
115
|
+
// For asterisks, match any single asterisk pairs
|
|
116
|
+
result = result.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
117
|
+
// For underscores, only match at word boundaries
|
|
118
|
+
result = result.replace(/(^|[^a-zA-Z0-9])_([^_]+)_([^a-zA-Z0-9]|$)/g, "$1<em>$2</em>$3");
|
|
119
|
+
|
|
120
|
+
// LAST: Restore escaped characters from placeholders to literals
|
|
121
|
+
result = revertEscapes(result);
|
|
122
|
+
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token renderer orchestrator
|
|
3
|
+
* Renders block tokens to HTML
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { BlockToken } from "../types";
|
|
7
|
+
import { parseInline } from "../parseInline";
|
|
8
|
+
import { escapeHtml } from "../escapeHtml";
|
|
9
|
+
import { tokenizeBlocks } from "../tokenize";
|
|
10
|
+
import { renderUnorderedList, renderOrderedList, renderTaskList } from "./renderList";
|
|
11
|
+
import { renderTable } from "./renderTable";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render tokens to HTML
|
|
15
|
+
*/
|
|
16
|
+
export function renderTokens(tokens: BlockToken[], sanitize: boolean): string {
|
|
17
|
+
const htmlParts: string[] = [];
|
|
18
|
+
|
|
19
|
+
for (const token of tokens) {
|
|
20
|
+
switch (token.type) {
|
|
21
|
+
case "heading": {
|
|
22
|
+
const content = parseInline(token.content, sanitize);
|
|
23
|
+
htmlParts.push(`<h${token.level}>${content}</h${token.level}>`);
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
case "code_block": {
|
|
28
|
+
// Always escape code block content for safety
|
|
29
|
+
const escapedContent = escapeHtml(token.content);
|
|
30
|
+
const langAttr = token.language ? ` class="language-${escapeHtml(token.language)}"` : "";
|
|
31
|
+
htmlParts.push(`<pre><code${langAttr}>${escapedContent}</code></pre>`);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
case "blockquote": {
|
|
36
|
+
// Recursively parse blockquote content
|
|
37
|
+
const innerTokens = tokenizeBlocks(token.content);
|
|
38
|
+
const innerHtml = renderTokens(innerTokens, sanitize);
|
|
39
|
+
htmlParts.push(`<blockquote>${innerHtml}</blockquote>`);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case "ul": {
|
|
44
|
+
htmlParts.push(renderUnorderedList(token, sanitize, renderTokens));
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case "ol": {
|
|
49
|
+
htmlParts.push(renderOrderedList(token, sanitize, renderTokens));
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case "task_list": {
|
|
54
|
+
htmlParts.push(renderTaskList(token, sanitize));
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
case "table": {
|
|
59
|
+
htmlParts.push(renderTable(token, sanitize));
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case "dl": {
|
|
64
|
+
let dlHtml = "<dl>";
|
|
65
|
+
for (const item of token.items) {
|
|
66
|
+
dlHtml += `<dt>${parseInline(item.term, sanitize)}</dt>`;
|
|
67
|
+
for (const def of item.definitions) {
|
|
68
|
+
dlHtml += `<dd>${parseInline(def, sanitize)}</dd>`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
dlHtml += "</dl>";
|
|
72
|
+
htmlParts.push(dlHtml);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case "hr": {
|
|
77
|
+
htmlParts.push("<hr />");
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case "paragraph": {
|
|
82
|
+
const content = parseInline(token.content, sanitize);
|
|
83
|
+
// Convert single newlines to <br> within paragraphs
|
|
84
|
+
const withBreaks = content.replace(/\n/g, "<br />");
|
|
85
|
+
htmlParts.push(`<p>${withBreaks}</p>`);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return htmlParts.join("\n");
|
|
92
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Footnotes section renderer
|
|
3
|
+
* Renders the footnotes section at the end of the document
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { FootnoteDefinition } from "../types";
|
|
7
|
+
import { parseInline } from "../parseInline";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Render the footnotes section
|
|
11
|
+
*/
|
|
12
|
+
export function renderFootnotesSection(
|
|
13
|
+
footnotes: Record<string, FootnoteDefinition>,
|
|
14
|
+
sanitize: boolean
|
|
15
|
+
): string {
|
|
16
|
+
const footnoteEntries = Object.entries(footnotes)
|
|
17
|
+
.sort((a, b) => a[1].index - b[1].index);
|
|
18
|
+
|
|
19
|
+
let html = "<section class=\"footnotes\"><hr /><ol class=\"footnote-list\">";
|
|
20
|
+
|
|
21
|
+
for (const [fnId, fn] of footnoteEntries) {
|
|
22
|
+
html += `<li id="fn-${fnId}" class="footnote-item">`;
|
|
23
|
+
html += parseInline(fn.content, sanitize);
|
|
24
|
+
html += ` <a href="#fnref-${fnId}" class="footnote-backref">\u21a9</a>`;
|
|
25
|
+
html += "</li>";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
html += "</ol></section>";
|
|
29
|
+
return html;
|
|
30
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified list renderer for ordered and unordered lists
|
|
3
|
+
* DRY implementation handling both list types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { BlockToken, ListItem } from "../types";
|
|
7
|
+
import { parseInline } from "../parseInline";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Render list items recursively
|
|
11
|
+
*/
|
|
12
|
+
function renderListItems(
|
|
13
|
+
items: ListItem[],
|
|
14
|
+
sanitize: boolean,
|
|
15
|
+
renderTokensFn: (tokens: BlockToken[], sanitize: boolean) => string
|
|
16
|
+
): string {
|
|
17
|
+
return items
|
|
18
|
+
.map((item) => {
|
|
19
|
+
let content = parseInline(item.content, sanitize);
|
|
20
|
+
if (item.children && item.children.length > 0) {
|
|
21
|
+
content += renderTokensFn(item.children, sanitize);
|
|
22
|
+
}
|
|
23
|
+
return `<li>${content}</li>`;
|
|
24
|
+
})
|
|
25
|
+
.join("");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Render an unordered list token
|
|
30
|
+
*/
|
|
31
|
+
export function renderUnorderedList(
|
|
32
|
+
token: Extract<BlockToken, { type: "ul" }>,
|
|
33
|
+
sanitize: boolean,
|
|
34
|
+
renderTokensFn: (tokens: BlockToken[], sanitize: boolean) => string
|
|
35
|
+
): string {
|
|
36
|
+
const items = renderListItems(token.items, sanitize, renderTokensFn);
|
|
37
|
+
return `<ul>${items}</ul>`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Render an ordered list token
|
|
42
|
+
*/
|
|
43
|
+
export function renderOrderedList(
|
|
44
|
+
token: Extract<BlockToken, { type: "ol" }>,
|
|
45
|
+
sanitize: boolean,
|
|
46
|
+
renderTokensFn: (tokens: BlockToken[], sanitize: boolean) => string
|
|
47
|
+
): string {
|
|
48
|
+
const items = renderListItems(token.items, sanitize, renderTokensFn);
|
|
49
|
+
const startAttr = token.start !== 1 ? ` start="${token.start}"` : "";
|
|
50
|
+
return `<ol${startAttr}>${items}</ol>`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Render a task list token
|
|
55
|
+
*/
|
|
56
|
+
export function renderTaskList(
|
|
57
|
+
token: Extract<BlockToken, { type: "task_list" }>,
|
|
58
|
+
sanitize: boolean
|
|
59
|
+
): string {
|
|
60
|
+
const items = token.items
|
|
61
|
+
.map((item) => {
|
|
62
|
+
const checkbox = item.checked
|
|
63
|
+
? "<input type=\"checkbox\" checked disabled />"
|
|
64
|
+
: "<input type=\"checkbox\" disabled />";
|
|
65
|
+
return `<li class="task-list-item">${checkbox} ${parseInline(item.content, sanitize)}</li>`;
|
|
66
|
+
})
|
|
67
|
+
.join("");
|
|
68
|
+
return `<ul class="task-list">${items}</ul>`;
|
|
69
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table renderer
|
|
3
|
+
* Handles alignment styles and cell rendering
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { BlockToken, TableAlignment } from "../types";
|
|
7
|
+
import { parseInline } from "../parseInline";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate style attribute for alignment
|
|
11
|
+
*/
|
|
12
|
+
function alignStyle(align: TableAlignment): string {
|
|
13
|
+
if (!align) return "";
|
|
14
|
+
return ` style="text-align: ${align}"`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Render a table token
|
|
19
|
+
*/
|
|
20
|
+
export function renderTable(
|
|
21
|
+
token: Extract<BlockToken, { type: "table" }>,
|
|
22
|
+
sanitize: boolean
|
|
23
|
+
): string {
|
|
24
|
+
const headerCells = token.headers
|
|
25
|
+
.map((h, idx) => `<th${alignStyle(token.alignments[idx])}>${parseInline(h, sanitize)}</th>`)
|
|
26
|
+
.join("");
|
|
27
|
+
|
|
28
|
+
const bodyRows = token.rows
|
|
29
|
+
.map(row => {
|
|
30
|
+
const cells = row
|
|
31
|
+
.map((cell, idx) => `<td${alignStyle(token.alignments[idx])}>${parseInline(cell, sanitize)}</td>`)
|
|
32
|
+
.join("");
|
|
33
|
+
return `<tr>${cells}</tr>`;
|
|
34
|
+
})
|
|
35
|
+
.join("");
|
|
36
|
+
|
|
37
|
+
return `<table><thead><tr>${headerCells}</tr></thead><tbody>${bodyRows}</tbody></table>`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser state management for markdown rendering
|
|
3
|
+
* Handles link references and footnotes across tokenization and rendering phases
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { LinkReference, FootnoteDefinition } from "./types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Module-level storage for link references extracted during tokenization
|
|
10
|
+
* Used to share link definitions between tokenizeBlocks and parseInline
|
|
11
|
+
*/
|
|
12
|
+
let currentLinkRefs: Record<string, LinkReference> = {};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Module-level storage for footnotes extracted during tokenization
|
|
16
|
+
* Used to share footnote definitions between tokenizeBlocks and renderMarkdown
|
|
17
|
+
*/
|
|
18
|
+
let currentFootnotes: Record<string, FootnoteDefinition> = {};
|
|
19
|
+
let footnoteCounter = 0;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the current footnotes (useful for Vue component rendering)
|
|
23
|
+
*/
|
|
24
|
+
export function getFootnotes(): Record<string, FootnoteDefinition> {
|
|
25
|
+
return currentFootnotes;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the current link references
|
|
30
|
+
*/
|
|
31
|
+
export function getLinkRefs(): Record<string, LinkReference> {
|
|
32
|
+
return currentLinkRefs;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Set a link reference
|
|
37
|
+
*/
|
|
38
|
+
export function setLinkRef(id: string, ref: LinkReference): void {
|
|
39
|
+
currentLinkRefs[id] = ref;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Set a footnote definition
|
|
44
|
+
*/
|
|
45
|
+
export function setFootnote(id: string, content: string): void {
|
|
46
|
+
footnoteCounter++;
|
|
47
|
+
currentFootnotes[id] = { content, index: footnoteCounter };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reset the parser state (link refs and footnotes)
|
|
52
|
+
* Call this before starting a new document parse
|
|
53
|
+
*/
|
|
54
|
+
export function resetParserState(): void {
|
|
55
|
+
currentLinkRefs = {};
|
|
56
|
+
currentFootnotes = {};
|
|
57
|
+
footnoteCounter = 0;
|
|
58
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Definition extraction for link references and footnotes
|
|
3
|
+
* First-pass processing to collect definitions before tokenization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { setLinkRef, setFootnote } from "../state";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extract link reference and footnote definitions from lines
|
|
10
|
+
* Returns the filtered lines with definitions removed
|
|
11
|
+
*/
|
|
12
|
+
export function extractDefinitions(rawLines: string[]): string[] {
|
|
13
|
+
const lines: string[] = [];
|
|
14
|
+
|
|
15
|
+
for (const line of rawLines) {
|
|
16
|
+
// Match footnote definition: [^id]: content
|
|
17
|
+
const footnoteDefMatch = line.match(/^\s*\[\^([^\]]+)\]:\s+(.+)$/);
|
|
18
|
+
if (footnoteDefMatch) {
|
|
19
|
+
const [, fnId, content] = footnoteDefMatch;
|
|
20
|
+
setFootnote(fnId, content);
|
|
21
|
+
// Don't add this line to filtered output (remove from rendered content)
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Match link definition: [ref-id]: URL "optional title"
|
|
26
|
+
// URL can optionally be wrapped in angle brackets
|
|
27
|
+
const refMatch = line.match(/^\s*\[([^\]]+)\]:\s+<?([^>\s]+)>?(?:\s+["']([^"']+)["'])?\s*$/);
|
|
28
|
+
|
|
29
|
+
if (refMatch) {
|
|
30
|
+
const [, refId, url, title] = refMatch;
|
|
31
|
+
setLinkRef(refId.toLowerCase(), { url, title });
|
|
32
|
+
// Don't add this line to filtered output (remove from rendered content)
|
|
33
|
+
} else {
|
|
34
|
+
lines.push(line);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return lines;
|
|
39
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block tokenizer orchestrator
|
|
3
|
+
* Coordinates all block-level parsers to tokenize markdown
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { BlockToken } from "../types";
|
|
7
|
+
import { extractDefinitions } from "./extractDefinitions";
|
|
8
|
+
import { parseFencedCodeBlock, parseIndentedCodeBlock } from "./parseCodeBlock";
|
|
9
|
+
import { parseAtxHeading, parseSetextHeading } from "./parseHeading";
|
|
10
|
+
import { parseList } from "./parseList";
|
|
11
|
+
import { parseTable } from "./parseTable";
|
|
12
|
+
import { parseBlockquote } from "./parseBlockquote";
|
|
13
|
+
import { parseTaskList } from "./parseTaskList";
|
|
14
|
+
import { parseDefinitionList } from "./parseDefinitionList";
|
|
15
|
+
import { parseHorizontalRule } from "./parseHorizontalRule";
|
|
16
|
+
import { parseParagraph } from "./parseParagraph";
|
|
17
|
+
import { getIndent } from "./utils";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Tokenize markdown into block-level elements
|
|
21
|
+
*/
|
|
22
|
+
export function tokenizeBlocks(markdown: string): BlockToken[] {
|
|
23
|
+
const tokens: BlockToken[] = [];
|
|
24
|
+
const rawLines = markdown.split("\n");
|
|
25
|
+
|
|
26
|
+
// First pass: Extract link reference and footnote definitions
|
|
27
|
+
const lines = extractDefinitions(rawLines);
|
|
28
|
+
|
|
29
|
+
let i = 0;
|
|
30
|
+
|
|
31
|
+
while (i < lines.length) {
|
|
32
|
+
const line = lines[i];
|
|
33
|
+
const trimmedLine = line.trim();
|
|
34
|
+
|
|
35
|
+
// Skip empty lines between blocks
|
|
36
|
+
if (!trimmedLine) {
|
|
37
|
+
i++;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Try parsers in priority order
|
|
42
|
+
let result;
|
|
43
|
+
|
|
44
|
+
// 1. Fenced code blocks: ```language ... ```
|
|
45
|
+
result = parseFencedCodeBlock(lines, i);
|
|
46
|
+
if (result) {
|
|
47
|
+
tokens.push(result.token);
|
|
48
|
+
i = result.endIndex;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. ATX-style headings: # through ######
|
|
53
|
+
result = parseAtxHeading(line, i);
|
|
54
|
+
if (result) {
|
|
55
|
+
tokens.push(result.token);
|
|
56
|
+
i = result.endIndex;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 3. Setext-style headings: text followed by === or ---
|
|
61
|
+
// Must check BEFORE hr detection since --- could be either
|
|
62
|
+
result = parseSetextHeading(lines, i);
|
|
63
|
+
if (result) {
|
|
64
|
+
tokens.push(result.token);
|
|
65
|
+
i = result.endIndex;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 4. Horizontal rules: ---, ***, ___
|
|
70
|
+
result = parseHorizontalRule(line, i);
|
|
71
|
+
if (result) {
|
|
72
|
+
tokens.push(result.token);
|
|
73
|
+
i = result.endIndex;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 5. Blockquotes: > text
|
|
78
|
+
result = parseBlockquote(lines, i);
|
|
79
|
+
if (result) {
|
|
80
|
+
tokens.push(result.token);
|
|
81
|
+
i = result.endIndex;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 6. Task lists: - [ ] or - [x]
|
|
86
|
+
// Must be before regular unordered list detection
|
|
87
|
+
result = parseTaskList(lines, i);
|
|
88
|
+
if (result) {
|
|
89
|
+
tokens.push(result.token);
|
|
90
|
+
i = result.endIndex;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 7. Lists: unordered (-, *, +) and ordered (1., 2., etc.)
|
|
95
|
+
if (/^[-*+]\s+/.test(trimmedLine) || /^\d+\.\s+/.test(trimmedLine)) {
|
|
96
|
+
const listResult = parseList(lines, i, getIndent(line));
|
|
97
|
+
tokens.push(...listResult.tokens);
|
|
98
|
+
i = listResult.endIndex;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 8. Indented code blocks: 4+ spaces or tab at start
|
|
103
|
+
result = parseIndentedCodeBlock(lines, i);
|
|
104
|
+
if (result) {
|
|
105
|
+
tokens.push(result.token);
|
|
106
|
+
i = result.endIndex;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 9. Tables: | col | col |
|
|
111
|
+
result = parseTable(lines, i);
|
|
112
|
+
if (result) {
|
|
113
|
+
tokens.push(result.token);
|
|
114
|
+
i = result.endIndex;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 10. Definition lists: Term\n: Definition
|
|
119
|
+
result = parseDefinitionList(lines, i);
|
|
120
|
+
if (result) {
|
|
121
|
+
tokens.push(result.token);
|
|
122
|
+
i = result.endIndex;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 11. Paragraphs (fallback): collect consecutive non-empty lines
|
|
127
|
+
result = parseParagraph(lines, i);
|
|
128
|
+
if (result) {
|
|
129
|
+
tokens.push(result.token);
|
|
130
|
+
i = result.endIndex;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Should never reach here, but advance to prevent infinite loop
|
|
135
|
+
i++;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return tokens;
|
|
139
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blockquote parser
|
|
3
|
+
* Handles > prefixed content
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ParseResult } from "../types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse a blockquote section
|
|
10
|
+
*/
|
|
11
|
+
export function parseBlockquote(lines: string[], index: number): ParseResult | null {
|
|
12
|
+
const trimmedLine = lines[index].trim();
|
|
13
|
+
|
|
14
|
+
if (!trimmedLine.startsWith(">")) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const quoteLines: string[] = [];
|
|
19
|
+
let i = index;
|
|
20
|
+
|
|
21
|
+
while (i < lines.length && lines[i].trim().startsWith(">")) {
|
|
22
|
+
// Remove the leading > and optional space
|
|
23
|
+
quoteLines.push(lines[i].trim().replace(/^>\s?/, ""));
|
|
24
|
+
i++;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
token: {
|
|
29
|
+
type: "blockquote",
|
|
30
|
+
content: quoteLines.join("\n")
|
|
31
|
+
},
|
|
32
|
+
endIndex: i
|
|
33
|
+
};
|
|
34
|
+
}
|