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,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code block parser
|
|
3
|
+
* Handles both fenced (```) and indented (4 spaces) code blocks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ParseResult } from "../types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse a fenced code block (``` ... ```)
|
|
10
|
+
*/
|
|
11
|
+
export function parseFencedCodeBlock(lines: string[], index: number): ParseResult | null {
|
|
12
|
+
const trimmedLine = lines[index].trim();
|
|
13
|
+
|
|
14
|
+
if (!trimmedLine.startsWith("```")) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const language = trimmedLine.slice(3).trim();
|
|
19
|
+
const contentLines: string[] = [];
|
|
20
|
+
let i = index + 1;
|
|
21
|
+
|
|
22
|
+
while (i < lines.length && !lines[i].trim().startsWith("```")) {
|
|
23
|
+
contentLines.push(lines[i]);
|
|
24
|
+
i++;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Skip closing ```
|
|
28
|
+
if (i < lines.length) i++;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
token: {
|
|
32
|
+
type: "code_block",
|
|
33
|
+
language,
|
|
34
|
+
content: contentLines.join("\n")
|
|
35
|
+
},
|
|
36
|
+
endIndex: i
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse an indented code block (4+ spaces or tab at start)
|
|
42
|
+
*/
|
|
43
|
+
export function parseIndentedCodeBlock(lines: string[], index: number): ParseResult | null {
|
|
44
|
+
const line = lines[index];
|
|
45
|
+
|
|
46
|
+
if (!/^( {4}|\t)/.test(line)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const contentLines: string[] = [];
|
|
51
|
+
let i = index;
|
|
52
|
+
|
|
53
|
+
while (i < lines.length) {
|
|
54
|
+
const codeLine = lines[i];
|
|
55
|
+
if (/^( {4}|\t)/.test(codeLine)) {
|
|
56
|
+
// Remove the 4 spaces or tab prefix
|
|
57
|
+
contentLines.push(codeLine.replace(/^( {4}|\t)/, ""));
|
|
58
|
+
i++;
|
|
59
|
+
} else if (codeLine.trim() === "") {
|
|
60
|
+
// Empty lines within indented block are kept
|
|
61
|
+
contentLines.push("");
|
|
62
|
+
i++;
|
|
63
|
+
} else {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Remove trailing empty lines
|
|
69
|
+
while (contentLines.length > 0 && contentLines[contentLines.length - 1] === "") {
|
|
70
|
+
contentLines.pop();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (contentLines.length === 0) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
token: {
|
|
79
|
+
type: "code_block",
|
|
80
|
+
language: "",
|
|
81
|
+
content: contentLines.join("\n")
|
|
82
|
+
},
|
|
83
|
+
endIndex: i
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Definition list parser
|
|
3
|
+
* Handles term/definition pairs with : prefix
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ParseResult } from "../types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse a definition list section
|
|
10
|
+
* Format: Term\n: Definition
|
|
11
|
+
*/
|
|
12
|
+
export function parseDefinitionList(lines: string[], index: number): ParseResult | null {
|
|
13
|
+
const trimmedLine = lines[index].trim();
|
|
14
|
+
|
|
15
|
+
// Check if current line could be a term (non-empty, doesn't start with special chars)
|
|
16
|
+
// and next line starts with `: `
|
|
17
|
+
if (
|
|
18
|
+
!trimmedLine ||
|
|
19
|
+
trimmedLine.startsWith(":") ||
|
|
20
|
+
/^[-*+#>\d]/.test(trimmedLine) ||
|
|
21
|
+
index + 1 >= lines.length
|
|
22
|
+
) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const nextLine = lines[index + 1].trim();
|
|
27
|
+
if (!nextLine.startsWith(": ")) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// This is a definition list
|
|
32
|
+
const items: Array<{ term: string; definitions: string[] }> = [];
|
|
33
|
+
let i = index;
|
|
34
|
+
|
|
35
|
+
while (i < lines.length) {
|
|
36
|
+
const termLine = lines[i].trim();
|
|
37
|
+
|
|
38
|
+
// Empty line ends the definition list
|
|
39
|
+
if (!termLine) {
|
|
40
|
+
i++;
|
|
41
|
+
// Check if there's another term after empty line
|
|
42
|
+
if (i < lines.length && lines[i].trim() && !lines[i].trim().startsWith(":")) {
|
|
43
|
+
const afterEmpty = lines[i + 1]?.trim();
|
|
44
|
+
if (afterEmpty?.startsWith(": ")) {
|
|
45
|
+
continue; // Another term-definition pair follows
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// If line starts with :, it's a definition for previous term
|
|
52
|
+
if (termLine.startsWith(": ")) {
|
|
53
|
+
// Add to last item's definitions if exists
|
|
54
|
+
if (items.length > 0) {
|
|
55
|
+
items[items.length - 1].definitions.push(termLine.slice(2));
|
|
56
|
+
}
|
|
57
|
+
i++;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check if this could be a new term
|
|
62
|
+
if (!termLine.startsWith(":") && i + 1 < lines.length) {
|
|
63
|
+
const nextDef = lines[i + 1].trim();
|
|
64
|
+
if (nextDef.startsWith(": ")) {
|
|
65
|
+
// New term
|
|
66
|
+
items.push({
|
|
67
|
+
term: termLine,
|
|
68
|
+
definitions: []
|
|
69
|
+
});
|
|
70
|
+
i++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Not part of definition list anymore
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Must have items with at least one definition
|
|
80
|
+
if (items.length === 0 || !items.some(item => item.definitions.length > 0)) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
token: { type: "dl", items },
|
|
86
|
+
endIndex: i
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heading parser
|
|
3
|
+
* Handles both ATX-style (#) and setext-style (=== or ---) headings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ParseResult } from "../types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse an ATX-style heading (# through ######)
|
|
10
|
+
*/
|
|
11
|
+
export function parseAtxHeading(line: string, index: number): ParseResult | null {
|
|
12
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
13
|
+
|
|
14
|
+
if (!headingMatch) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
token: {
|
|
20
|
+
type: "heading",
|
|
21
|
+
level: headingMatch[1].length,
|
|
22
|
+
content: headingMatch[2]
|
|
23
|
+
},
|
|
24
|
+
endIndex: index + 1
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse a setext-style heading (text followed by === or ---)
|
|
30
|
+
*/
|
|
31
|
+
export function parseSetextHeading(lines: string[], index: number): ParseResult | null {
|
|
32
|
+
if (index + 1 >= lines.length) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const trimmedLine = lines[index].trim();
|
|
37
|
+
const nextLine = lines[index + 1].trim();
|
|
38
|
+
|
|
39
|
+
// Level 1: ===
|
|
40
|
+
if (/^=+$/.test(nextLine) && trimmedLine.length > 0) {
|
|
41
|
+
return {
|
|
42
|
+
token: {
|
|
43
|
+
type: "heading",
|
|
44
|
+
level: 1,
|
|
45
|
+
content: trimmedLine
|
|
46
|
+
},
|
|
47
|
+
endIndex: index + 2
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Level 2: ---
|
|
52
|
+
// Must not be a list item (starts with - followed by space)
|
|
53
|
+
if (/^-+$/.test(nextLine) && trimmedLine.length > 0 && !/^[-*+]\s+/.test(trimmedLine)) {
|
|
54
|
+
return {
|
|
55
|
+
token: {
|
|
56
|
+
type: "heading",
|
|
57
|
+
level: 2,
|
|
58
|
+
content: trimmedLine
|
|
59
|
+
},
|
|
60
|
+
endIndex: index + 2
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Horizontal rule parser
|
|
3
|
+
* Handles ---, ***, ___ patterns
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ParseResult } from "../types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse a horizontal rule
|
|
10
|
+
*/
|
|
11
|
+
export function parseHorizontalRule(line: string, index: number): ParseResult | null {
|
|
12
|
+
const trimmedLine = line.trim();
|
|
13
|
+
|
|
14
|
+
if (!/^(-{3,}|\*{3,}|_{3,})$/.test(trimmedLine)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
token: { type: "hr" },
|
|
20
|
+
endIndex: index + 1
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified list parser for ordered and unordered lists
|
|
3
|
+
* DRY implementation handling both list types with nested content support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { BlockToken, ListItem } from "../types";
|
|
7
|
+
import { getIndent } from "./utils";
|
|
8
|
+
|
|
9
|
+
type ListType = "ul" | "ol";
|
|
10
|
+
|
|
11
|
+
interface ListParseResult {
|
|
12
|
+
tokens: BlockToken[];
|
|
13
|
+
endIndex: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Detect if a line is a list item and return its type and content
|
|
18
|
+
*/
|
|
19
|
+
function detectListItem(trimmed: string): { type: ListType; content: string; start?: number } | null {
|
|
20
|
+
// Unordered: -, *, +
|
|
21
|
+
const ulMatch = trimmed.match(/^[-*+]\s+(.*)$/);
|
|
22
|
+
if (ulMatch) {
|
|
23
|
+
return { type: "ul", content: ulMatch[1] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Ordered: 1., 2., etc.
|
|
27
|
+
const olMatch = trimmed.match(/^(\d+)\.\s+(.*)$/);
|
|
28
|
+
if (olMatch) {
|
|
29
|
+
return { type: "ol", content: olMatch[2], start: parseInt(olMatch[1], 10) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse a list starting at current position, supporting nested lists
|
|
37
|
+
* Unified implementation for both ul and ol
|
|
38
|
+
*/
|
|
39
|
+
export function parseList(lines: string[], startIndex: number, baseIndent: number): ListParseResult {
|
|
40
|
+
const tokens: BlockToken[] = [];
|
|
41
|
+
let i = startIndex;
|
|
42
|
+
|
|
43
|
+
while (i < lines.length) {
|
|
44
|
+
const line = lines[i];
|
|
45
|
+
const trimmed = line.trim();
|
|
46
|
+
const indent = getIndent(line);
|
|
47
|
+
|
|
48
|
+
// Empty line - could be end of list or spacing within
|
|
49
|
+
if (!trimmed) {
|
|
50
|
+
i++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// If indent is less than base, we've exited this list level
|
|
55
|
+
if (indent < baseIndent && trimmed) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const itemInfo = detectListItem(trimmed);
|
|
60
|
+
|
|
61
|
+
// Check if this is a list item at our indent level
|
|
62
|
+
if (itemInfo && indent === baseIndent) {
|
|
63
|
+
// Collect all items of the same type
|
|
64
|
+
const items: ListItem[] = [];
|
|
65
|
+
const listType = itemInfo.type;
|
|
66
|
+
const startNum = itemInfo.start || 1;
|
|
67
|
+
|
|
68
|
+
while (i < lines.length) {
|
|
69
|
+
const itemLine = lines[i];
|
|
70
|
+
const itemTrimmed = itemLine.trim();
|
|
71
|
+
const itemIndent = getIndent(itemLine);
|
|
72
|
+
|
|
73
|
+
if (!itemTrimmed) {
|
|
74
|
+
i++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (itemIndent < baseIndent) break;
|
|
79
|
+
|
|
80
|
+
const currentItem = detectListItem(itemTrimmed);
|
|
81
|
+
|
|
82
|
+
// Must be same list type and at base indent
|
|
83
|
+
if (currentItem && currentItem.type === listType && itemIndent === baseIndent) {
|
|
84
|
+
const content = currentItem.content;
|
|
85
|
+
i++;
|
|
86
|
+
|
|
87
|
+
// Check for nested content (items indented more than base)
|
|
88
|
+
const nestedIndent = baseIndent + 2;
|
|
89
|
+
const nested = parseList(lines, i, nestedIndent);
|
|
90
|
+
|
|
91
|
+
items.push({
|
|
92
|
+
content,
|
|
93
|
+
children: nested.tokens.length > 0 ? nested.tokens : undefined
|
|
94
|
+
});
|
|
95
|
+
i = nested.endIndex;
|
|
96
|
+
} else if (itemIndent > baseIndent) {
|
|
97
|
+
// This is nested content, let recursion handle it
|
|
98
|
+
break;
|
|
99
|
+
} else {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (items.length > 0) {
|
|
105
|
+
if (listType === "ul") {
|
|
106
|
+
tokens.push({ type: "ul", items });
|
|
107
|
+
} else {
|
|
108
|
+
tokens.push({ type: "ol", items, start: startNum });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Not a list item at this level
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { tokens, endIndex: i };
|
|
119
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paragraph parser
|
|
3
|
+
* Collects consecutive non-block lines into paragraphs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ParseResult } from "../types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if a line starts a block-level element
|
|
10
|
+
*/
|
|
11
|
+
function isBlockStarter(trimmed: string): boolean {
|
|
12
|
+
return (
|
|
13
|
+
trimmed.startsWith("#") ||
|
|
14
|
+
trimmed.startsWith("```") ||
|
|
15
|
+
trimmed.startsWith(">") ||
|
|
16
|
+
/^[-*+]\s+/.test(trimmed) ||
|
|
17
|
+
/^\d+\.\s+/.test(trimmed) ||
|
|
18
|
+
/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse a paragraph (consecutive non-empty, non-block lines)
|
|
24
|
+
*/
|
|
25
|
+
export function parseParagraph(lines: string[], index: number): ParseResult | null {
|
|
26
|
+
const paragraphLines: string[] = [];
|
|
27
|
+
let i = index;
|
|
28
|
+
|
|
29
|
+
while (i < lines.length) {
|
|
30
|
+
const pLine = lines[i];
|
|
31
|
+
const pTrimmed = pLine.trim();
|
|
32
|
+
|
|
33
|
+
// Stop on empty line or block-level element
|
|
34
|
+
if (!pTrimmed) {
|
|
35
|
+
i++;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check for block-level starters
|
|
40
|
+
if (isBlockStarter(pTrimmed)) {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
paragraphLines.push(pLine);
|
|
45
|
+
i++;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (paragraphLines.length === 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
token: {
|
|
54
|
+
type: "paragraph",
|
|
55
|
+
content: paragraphLines.join("\n")
|
|
56
|
+
},
|
|
57
|
+
endIndex: i
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table parser
|
|
3
|
+
* Handles markdown tables with alignment support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TableAlignment, ParseResult } from "../types";
|
|
7
|
+
import { parsePipeRow } from "./utils";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse a markdown table
|
|
11
|
+
* Format: | col | col | followed by |---|---| separator
|
|
12
|
+
*/
|
|
13
|
+
export function parseTable(lines: string[], index: number): ParseResult | null {
|
|
14
|
+
const trimmedLine = lines[index].trim();
|
|
15
|
+
|
|
16
|
+
// Must start with | or contain |
|
|
17
|
+
if (!trimmedLine.startsWith("|") && !trimmedLine.includes(" | ")) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if next line is a separator
|
|
22
|
+
if (index + 1 >= lines.length) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const nextLine = lines[index + 1].trim();
|
|
27
|
+
|
|
28
|
+
// Separator pattern: |---|---| or |:---|---:| etc
|
|
29
|
+
if (!/^\|?[\s:]*-+[\s:]*(\|[\s:]*-+[\s:]*)+\|?$/.test(nextLine)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Parse header row
|
|
34
|
+
const headers = parsePipeRow(trimmedLine);
|
|
35
|
+
|
|
36
|
+
// Parse separator to get alignments
|
|
37
|
+
const separatorCells = parsePipeRow(nextLine);
|
|
38
|
+
const alignments: TableAlignment[] = separatorCells.map(cell => {
|
|
39
|
+
const trimmed = cell.trim();
|
|
40
|
+
const leftColon = trimmed.startsWith(":");
|
|
41
|
+
const rightColon = trimmed.endsWith(":");
|
|
42
|
+
if (leftColon && rightColon) return "center";
|
|
43
|
+
if (rightColon) return "right";
|
|
44
|
+
if (leftColon) return "left";
|
|
45
|
+
return null;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Collect body rows
|
|
49
|
+
const rows: string[][] = [];
|
|
50
|
+
let i = index + 2; // Skip header and separator
|
|
51
|
+
|
|
52
|
+
while (i < lines.length) {
|
|
53
|
+
const rowLine = lines[i].trim();
|
|
54
|
+
if (!rowLine || (!rowLine.startsWith("|") && !rowLine.includes(" | "))) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
rows.push(parsePipeRow(rowLine));
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
token: {
|
|
63
|
+
type: "table",
|
|
64
|
+
headers,
|
|
65
|
+
alignments,
|
|
66
|
+
rows
|
|
67
|
+
},
|
|
68
|
+
endIndex: i
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task list parser
|
|
3
|
+
* Handles - [ ] and - [x] style task items
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ParseResult } from "../types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse a task list section
|
|
10
|
+
*/
|
|
11
|
+
export function parseTaskList(lines: string[], index: number): ParseResult | null {
|
|
12
|
+
const trimmedLine = lines[index].trim();
|
|
13
|
+
const taskListMatch = trimmedLine.match(/^[-*+]\s+\[([ xX])\]\s+(.*)$/);
|
|
14
|
+
|
|
15
|
+
if (!taskListMatch) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const items: Array<{ checked: boolean; content: string }> = [];
|
|
20
|
+
let i = index;
|
|
21
|
+
|
|
22
|
+
while (i < lines.length) {
|
|
23
|
+
const taskLine = lines[i].trim();
|
|
24
|
+
const itemMatch = taskLine.match(/^[-*+]\s+\[([ xX])\]\s+(.*)$/);
|
|
25
|
+
|
|
26
|
+
if (itemMatch) {
|
|
27
|
+
items.push({
|
|
28
|
+
checked: itemMatch[1].toLowerCase() === "x",
|
|
29
|
+
content: itemMatch[2]
|
|
30
|
+
});
|
|
31
|
+
i++;
|
|
32
|
+
} else if (taskLine === "") {
|
|
33
|
+
i++;
|
|
34
|
+
const nextNonEmpty = lines.slice(i).find((l) => l.trim() !== "");
|
|
35
|
+
if (!nextNonEmpty || !/^[-*+]\s+\[([ xX])\]/.test(nextNonEmpty.trim())) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
token: { type: "task_list", items },
|
|
45
|
+
endIndex: i
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for block tokenizers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get indentation level of a line (count leading spaces/tabs)
|
|
7
|
+
* Tabs are counted as 2 spaces for indentation purposes
|
|
8
|
+
*/
|
|
9
|
+
export function getIndent(line: string): number {
|
|
10
|
+
const match = line.match(/^(\s*)/);
|
|
11
|
+
if (!match) return 0;
|
|
12
|
+
// Count tabs as 2 spaces for indentation purposes
|
|
13
|
+
return match[1].replace(/\t/g, " ").length;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse a pipe-delimited table row into cells
|
|
18
|
+
*/
|
|
19
|
+
export function parsePipeRow(line: string): string[] {
|
|
20
|
+
// Remove leading/trailing pipes and split
|
|
21
|
+
let trimmed = line.trim();
|
|
22
|
+
if (trimmed.startsWith("|")) trimmed = trimmed.slice(1);
|
|
23
|
+
if (trimmed.endsWith("|")) trimmed = trimmed.slice(0, -1);
|
|
24
|
+
return trimmed.split("|").map(cell => cell.trim());
|
|
25
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown parser and renderer type definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for markdown rendering
|
|
7
|
+
*/
|
|
8
|
+
export interface MarkdownRenderOptions {
|
|
9
|
+
sanitize?: boolean; // XSS protection (default: true)
|
|
10
|
+
preserveState?: boolean; // Don't reset link refs and footnotes (for nested rendering)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Link reference for reference-style links
|
|
15
|
+
*/
|
|
16
|
+
export interface LinkReference {
|
|
17
|
+
url: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Footnote definition for footnote references
|
|
23
|
+
*/
|
|
24
|
+
export interface FootnoteDefinition {
|
|
25
|
+
content: string;
|
|
26
|
+
index: number; // For numbered display
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* List item type supporting nested content
|
|
31
|
+
*/
|
|
32
|
+
export interface ListItem {
|
|
33
|
+
content: string;
|
|
34
|
+
children?: BlockToken[]; // Nested lists or other block content
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Token types for block-level parsing
|
|
39
|
+
*/
|
|
40
|
+
export type BlockToken =
|
|
41
|
+
| { type: "heading"; level: number; content: string }
|
|
42
|
+
| { type: "code_block"; language: string; content: string }
|
|
43
|
+
| { type: "blockquote"; content: string }
|
|
44
|
+
| { type: "ul"; items: ListItem[] }
|
|
45
|
+
| { type: "ol"; items: ListItem[]; start: number }
|
|
46
|
+
| { type: "task_list"; items: Array<{ checked: boolean; content: string }> }
|
|
47
|
+
| { type: "table"; headers: string[]; alignments: TableAlignment[]; rows: string[][] }
|
|
48
|
+
| { type: "dl"; items: Array<{ term: string; definitions: string[] }> }
|
|
49
|
+
| { type: "hr" }
|
|
50
|
+
| { type: "paragraph"; content: string };
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Table column alignment
|
|
54
|
+
*/
|
|
55
|
+
export type TableAlignment = "left" | "center" | "right" | null;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Result from a block parser function
|
|
59
|
+
*/
|
|
60
|
+
export interface ParseResult {
|
|
61
|
+
token: BlockToken;
|
|
62
|
+
endIndex: number;
|
|
63
|
+
}
|