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.
Files changed (90) hide show
  1. package/dist/danx.es.js +17884 -12732
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +192 -118
  4. package/dist/danx.umd.js.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +11 -2
  7. package/scripts/publish.sh +76 -0
  8. package/src/components/Utility/Code/CodeViewer.vue +31 -14
  9. package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
  10. package/src/components/Utility/Code/CodeViewerFooter.vue +1 -1
  11. package/src/components/Utility/Code/LanguageBadge.vue +278 -5
  12. package/src/components/Utility/Code/MarkdownContent.vue +160 -6
  13. package/src/components/Utility/Code/index.ts +3 -0
  14. package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
  15. package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
  16. package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
  17. package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
  18. package/src/components/Utility/Markdown/MarkdownEditor.vue +228 -0
  19. package/src/components/Utility/Markdown/MarkdownEditorContent.vue +235 -0
  20. package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
  21. package/src/components/Utility/Markdown/TablePopover.vue +420 -0
  22. package/src/components/Utility/Markdown/index.ts +11 -0
  23. package/src/components/Utility/Markdown/types.ts +27 -0
  24. package/src/components/Utility/index.ts +1 -0
  25. package/src/composables/index.ts +1 -0
  26. package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
  27. package/src/composables/markdown/features/useBlockquotes.ts +248 -0
  28. package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
  29. package/src/composables/markdown/features/useCodeBlocks.spec.ts +779 -0
  30. package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
  31. package/src/composables/markdown/features/useContextMenu.ts +444 -0
  32. package/src/composables/markdown/features/useFocusTracking.ts +116 -0
  33. package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
  34. package/src/composables/markdown/features/useHeadings.ts +290 -0
  35. package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
  36. package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
  37. package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
  38. package/src/composables/markdown/features/useLinks.spec.ts +369 -0
  39. package/src/composables/markdown/features/useLinks.ts +374 -0
  40. package/src/composables/markdown/features/useLists.spec.ts +834 -0
  41. package/src/composables/markdown/features/useLists.ts +747 -0
  42. package/src/composables/markdown/features/usePopoverManager.ts +181 -0
  43. package/src/composables/markdown/features/useTables.spec.ts +1601 -0
  44. package/src/composables/markdown/features/useTables.ts +1107 -0
  45. package/src/composables/markdown/index.ts +16 -0
  46. package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
  47. package/src/composables/markdown/useMarkdownEditor.ts +1068 -0
  48. package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
  49. package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
  50. package/src/composables/markdown/useMarkdownSelection.ts +219 -0
  51. package/src/composables/markdown/useMarkdownSync.ts +549 -0
  52. package/src/composables/useCodeViewerEditor.spec.ts +655 -0
  53. package/src/composables/useCodeViewerEditor.ts +174 -20
  54. package/src/helpers/formats/index.ts +1 -1
  55. package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
  56. package/src/helpers/formats/markdown/escapeSequences.ts +60 -0
  57. package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
  58. package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
  59. package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +412 -0
  60. package/src/helpers/formats/markdown/index.ts +92 -0
  61. package/src/helpers/formats/markdown/linePatterns.spec.ts +495 -0
  62. package/src/helpers/formats/markdown/linePatterns.ts +172 -0
  63. package/src/helpers/formats/markdown/parseInline.ts +124 -0
  64. package/src/helpers/formats/markdown/render/index.ts +92 -0
  65. package/src/helpers/formats/markdown/render/renderFootnotes.ts +30 -0
  66. package/src/helpers/formats/markdown/render/renderList.ts +69 -0
  67. package/src/helpers/formats/markdown/render/renderTable.ts +38 -0
  68. package/src/helpers/formats/markdown/state.ts +58 -0
  69. package/src/helpers/formats/markdown/tokenize/extractDefinitions.ts +39 -0
  70. package/src/helpers/formats/markdown/tokenize/index.ts +139 -0
  71. package/src/helpers/formats/markdown/tokenize/parseBlockquote.ts +34 -0
  72. package/src/helpers/formats/markdown/tokenize/parseCodeBlock.ts +85 -0
  73. package/src/helpers/formats/markdown/tokenize/parseDefinitionList.ts +88 -0
  74. package/src/helpers/formats/markdown/tokenize/parseHeading.ts +65 -0
  75. package/src/helpers/formats/markdown/tokenize/parseHorizontalRule.ts +22 -0
  76. package/src/helpers/formats/markdown/tokenize/parseList.ts +119 -0
  77. package/src/helpers/formats/markdown/tokenize/parseParagraph.ts +59 -0
  78. package/src/helpers/formats/markdown/tokenize/parseTable.ts +70 -0
  79. package/src/helpers/formats/markdown/tokenize/parseTaskList.ts +47 -0
  80. package/src/helpers/formats/markdown/tokenize/utils.ts +25 -0
  81. package/src/helpers/formats/markdown/types.ts +63 -0
  82. package/src/styles/danx.scss +1 -0
  83. package/src/styles/themes/danx/markdown.scss +96 -0
  84. package/src/test/helpers/editorTestUtils.spec.ts +296 -0
  85. package/src/test/helpers/editorTestUtils.ts +253 -0
  86. package/src/test/helpers/index.ts +1 -0
  87. package/src/test/setup.test.ts +12 -0
  88. package/src/test/setup.ts +12 -0
  89. package/vitest.config.ts +19 -0
  90. 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(/&lt;(https?:\/\/[^&]+)&gt;/g, '<a href="$1">$1</a>');
34
+ // Email autolinks: <user@example.com>
35
+ result = result.replace(/&lt;([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})&gt;/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: ![alt](url) - 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
+ }