quasar-ui-danx 0.4.95 → 0.5.0
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 +25284 -23176
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +133 -120
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +4 -2
- package/scripts/publish.sh +76 -0
- package/src/components/Utility/Buttons/ActionButton.vue +11 -3
- package/src/components/Utility/Code/CodeViewer.vue +219 -0
- package/src/components/Utility/Code/CodeViewerCollapsed.vue +34 -0
- package/src/components/Utility/Code/CodeViewerFooter.vue +53 -0
- package/src/components/Utility/Code/LanguageBadge.vue +122 -0
- package/src/components/Utility/Code/MarkdownContent.vue +405 -0
- package/src/components/Utility/Code/index.ts +5 -0
- package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +134 -38
- package/src/components/Utility/Files/CarouselHeader.vue +24 -0
- package/src/components/Utility/Files/FileMetadataDialog.vue +69 -0
- package/src/components/Utility/Files/FilePreview.vue +118 -166
- package/src/components/Utility/Files/index.ts +1 -0
- package/src/components/Utility/index.ts +1 -0
- package/src/composables/index.ts +5 -0
- package/src/composables/useCodeFormat.ts +199 -0
- package/src/composables/useCodeViewerCollapse.ts +125 -0
- package/src/composables/useCodeViewerEditor.ts +420 -0
- package/src/composables/useFilePreview.ts +119 -0
- package/src/composables/useTranscodeLoader.ts +68 -0
- package/src/helpers/formats/highlightSyntax.ts +327 -0
- package/src/helpers/formats/index.ts +3 -1
- package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
- package/src/helpers/formats/markdown/escapeSequences.ts +60 -0
- package/src/helpers/formats/markdown/index.ts +85 -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 +4 -0
- package/src/styles/themes/danx/code.scss +158 -0
- package/src/styles/themes/danx/index.scss +2 -0
- package/src/styles/themes/danx/markdown.scss +241 -0
- package/src/styles/themes/danx/scrollbar.scss +125 -0
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|