quasar-ui-danx 0.4.95 → 0.4.99

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 (33) hide show
  1. package/dist/danx.es.js +24452 -22880
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +133 -122
  4. package/dist/danx.umd.js.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +1 -1
  7. package/src/components/Utility/Buttons/ActionButton.vue +11 -3
  8. package/src/components/Utility/Code/CodeViewer.vue +219 -0
  9. package/src/components/Utility/Code/CodeViewerCollapsed.vue +34 -0
  10. package/src/components/Utility/Code/CodeViewerFooter.vue +53 -0
  11. package/src/components/Utility/Code/LanguageBadge.vue +122 -0
  12. package/src/components/Utility/Code/MarkdownContent.vue +251 -0
  13. package/src/components/Utility/Code/index.ts +5 -0
  14. package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +134 -38
  15. package/src/components/Utility/Files/CarouselHeader.vue +24 -0
  16. package/src/components/Utility/Files/FileMetadataDialog.vue +69 -0
  17. package/src/components/Utility/Files/FilePreview.vue +118 -166
  18. package/src/components/Utility/Files/index.ts +1 -0
  19. package/src/components/Utility/index.ts +1 -0
  20. package/src/composables/index.ts +5 -0
  21. package/src/composables/useCodeFormat.ts +199 -0
  22. package/src/composables/useCodeViewerCollapse.ts +125 -0
  23. package/src/composables/useCodeViewerEditor.ts +420 -0
  24. package/src/composables/useFilePreview.ts +119 -0
  25. package/src/composables/useTranscodeLoader.ts +68 -0
  26. package/src/helpers/formats/highlightSyntax.ts +327 -0
  27. package/src/helpers/formats/index.ts +3 -1
  28. package/src/helpers/formats/renderMarkdown.ts +338 -0
  29. package/src/styles/danx.scss +3 -0
  30. package/src/styles/themes/danx/code.scss +158 -0
  31. package/src/styles/themes/danx/index.scss +2 -0
  32. package/src/styles/themes/danx/markdown.scss +145 -0
  33. package/src/styles/themes/danx/scrollbar.scss +125 -0
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Lightweight syntax highlighting for JSON and YAML
3
+ * Returns HTML string with syntax highlighting spans
4
+ */
5
+
6
+ export type HighlightFormat = "json" | "yaml" | "text" | "markdown";
7
+
8
+ export interface HighlightOptions {
9
+ format: HighlightFormat;
10
+ }
11
+
12
+ /**
13
+ * Escape HTML entities to prevent XSS
14
+ */
15
+ function escapeHtml(text: string): string {
16
+ return text
17
+ .replace(/&/g, "&")
18
+ .replace(/</g, "&lt;")
19
+ .replace(/>/g, "&gt;")
20
+ .replace(/"/g, "&quot;")
21
+ .replace(/'/g, "&#039;");
22
+ }
23
+
24
+ /**
25
+ * Highlight JSON syntax by tokenizing first, then applying highlights
26
+ * This prevents issues with regex replacing content inside already-matched strings
27
+ */
28
+ export function highlightJSON(code: string): string {
29
+ if (!code) return "";
30
+
31
+ const result: string[] = [];
32
+ let i = 0;
33
+
34
+ while (i < code.length) {
35
+ const char = code[i];
36
+
37
+ // String (starts with ")
38
+ if (char === '"') {
39
+ const startIndex = i;
40
+ i++; // skip opening quote
41
+
42
+ // Find the closing quote, handling escape sequences
43
+ while (i < code.length) {
44
+ if (code[i] === '\\' && i + 1 < code.length) {
45
+ i += 2; // skip escaped character
46
+ } else if (code[i] === '"') {
47
+ i++; // include closing quote
48
+ break;
49
+ } else {
50
+ i++;
51
+ }
52
+ }
53
+
54
+ const str = code.slice(startIndex, i);
55
+ const escapedStr = escapeHtml(str);
56
+
57
+ // Check if this is a key (followed by colon)
58
+ const afterString = code.slice(i).match(/^(\s*):/);
59
+ if (afterString) {
60
+ result.push(`<span class="syntax-key">${escapedStr}</span>`);
61
+ // Add the whitespace and colon
62
+ result.push(`<span class="syntax-punctuation">${escapeHtml(afterString[1])}:</span>`);
63
+ i += afterString[0].length;
64
+ } else {
65
+ // It's a string value
66
+ result.push(`<span class="syntax-string">${escapedStr}</span>`);
67
+ }
68
+ continue;
69
+ }
70
+
71
+ // Number (starts with digit or minus followed by digit)
72
+ if (/[-\d]/.test(char)) {
73
+ const numMatch = code.slice(i).match(/^-?\d+(\.\d+)?([eE][+-]?\d+)?/);
74
+ if (numMatch) {
75
+ result.push(`<span class="syntax-number">${escapeHtml(numMatch[0])}</span>`);
76
+ i += numMatch[0].length;
77
+ continue;
78
+ }
79
+ }
80
+
81
+ // Boolean true
82
+ if (code.slice(i, i + 4) === 'true') {
83
+ result.push(`<span class="syntax-boolean">true</span>`);
84
+ i += 4;
85
+ continue;
86
+ }
87
+
88
+ // Boolean false
89
+ if (code.slice(i, i + 5) === 'false') {
90
+ result.push(`<span class="syntax-boolean">false</span>`);
91
+ i += 5;
92
+ continue;
93
+ }
94
+
95
+ // Null
96
+ if (code.slice(i, i + 4) === 'null') {
97
+ result.push(`<span class="syntax-null">null</span>`);
98
+ i += 4;
99
+ continue;
100
+ }
101
+
102
+ // Punctuation
103
+ if (/[{}\[\],:]/.test(char)) {
104
+ result.push(`<span class="syntax-punctuation">${escapeHtml(char)}</span>`);
105
+ i++;
106
+ continue;
107
+ }
108
+
109
+ // Whitespace and other characters
110
+ result.push(escapeHtml(char));
111
+ i++;
112
+ }
113
+
114
+ return result.join('');
115
+ }
116
+
117
+ /**
118
+ * Highlight a YAML value based on its type
119
+ */
120
+ function highlightYAMLValue(value: string): string {
121
+ if (!value) return value;
122
+
123
+ // Quoted string (complete)
124
+ if (/^(&quot;.*&quot;|&#039;.*&#039;)$/.test(value) || /^["'].*["']$/.test(value)) {
125
+ return `<span class="syntax-string">${value}</span>`;
126
+ }
127
+ // Number (strict format: integers, decimals, scientific notation)
128
+ if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(value)) {
129
+ return `<span class="syntax-number">${value}</span>`;
130
+ }
131
+ // Boolean
132
+ if (/^(true|false)$/i.test(value)) {
133
+ return `<span class="syntax-boolean">${value}</span>`;
134
+ }
135
+ // Null
136
+ if (/^(null|~)$/i.test(value)) {
137
+ return `<span class="syntax-null">${value}</span>`;
138
+ }
139
+ // Block scalar indicators - don't wrap, handle continuation separately
140
+ if (/^[|>][-+]?\d*$/.test(value)) {
141
+ return `<span class="syntax-punctuation">${value}</span>`;
142
+ }
143
+ // Unquoted string
144
+ return `<span class="syntax-string">${value}</span>`;
145
+ }
146
+
147
+ /**
148
+ * Check if a line is a continuation of a multi-line string
149
+ * (indented content following a block scalar or inside a quoted string)
150
+ */
151
+ function getIndentLevel(line: string): number {
152
+ const match = line.match(/^(\s*)/);
153
+ return match ? match[1].length : 0;
154
+ }
155
+
156
+ /**
157
+ * Highlight YAML syntax with multi-line string support
158
+ */
159
+ export function highlightYAML(code: string): string {
160
+ if (!code) return "";
161
+
162
+ const lines = code.split("\n");
163
+ const highlightedLines: string[] = [];
164
+
165
+ // State tracking for multi-line constructs
166
+ let inBlockScalar = false;
167
+ let blockScalarIndent = 0;
168
+ let inQuotedString = false;
169
+ let quoteChar = "";
170
+ let inUnquotedMultiline = false;
171
+ let unquotedMultilineKeyIndent = 0;
172
+
173
+ for (let i = 0; i < lines.length; i++) {
174
+ const line = lines[i];
175
+ const escaped = escapeHtml(line);
176
+ const currentIndent = getIndentLevel(line);
177
+ const trimmedLine = line.trim();
178
+
179
+ // Handle block scalar continuation (| or > style)
180
+ if (inBlockScalar) {
181
+ // Block scalar ends when we hit a line with less or equal indentation (and content)
182
+ if (trimmedLine && currentIndent <= blockScalarIndent) {
183
+ inBlockScalar = false;
184
+ // Fall through to normal processing
185
+ } else {
186
+ // This is a continuation line - highlight as string
187
+ highlightedLines.push(`<span class="syntax-string">${escaped}</span>`);
188
+ continue;
189
+ }
190
+ }
191
+
192
+ // Handle quoted string continuation
193
+ if (inQuotedString) {
194
+ // Check if this line closes the quote
195
+ const escapedQuote = quoteChar === '"' ? '&quot;' : '&#039;';
196
+
197
+ // Count unescaped quotes in this line
198
+ let closed = false;
199
+ let searchLine = escaped;
200
+ while (searchLine.includes(escapedQuote)) {
201
+ const idx = searchLine.indexOf(escapedQuote);
202
+ // Check if preceded by backslash (escaped)
203
+ if (idx > 0 && searchLine[idx - 1] === '\\') {
204
+ searchLine = searchLine.slice(idx + escapedQuote.length);
205
+ continue;
206
+ }
207
+ closed = true;
208
+ break;
209
+ }
210
+
211
+ if (closed) {
212
+ // Find position of closing quote
213
+ const closeIdx = escaped.indexOf(escapedQuote);
214
+ const beforeClose = escaped.slice(0, closeIdx + escapedQuote.length);
215
+ const afterClose = escaped.slice(closeIdx + escapedQuote.length);
216
+
217
+ highlightedLines.push(`<span class="syntax-string">${beforeClose}</span>${afterClose}`);
218
+ inQuotedString = false;
219
+ } else {
220
+ // Still in quoted string
221
+ highlightedLines.push(`<span class="syntax-string">${escaped}</span>`);
222
+ }
223
+ continue;
224
+ }
225
+
226
+ // Handle unquoted multi-line string continuation
227
+ if (inUnquotedMultiline) {
228
+ // Unquoted multiline ends when we hit a line with <= indentation to the key
229
+ // or when the line contains a colon (new key-value pair)
230
+ if (trimmedLine && currentIndent <= unquotedMultilineKeyIndent) {
231
+ inUnquotedMultiline = false;
232
+ // Fall through to normal processing
233
+ } else if (trimmedLine) {
234
+ // This is a continuation line - highlight as string
235
+ highlightedLines.push(`<span class="syntax-string">${escaped}</span>`);
236
+ continue;
237
+ } else {
238
+ // Empty line within multiline - keep it
239
+ highlightedLines.push(escaped);
240
+ continue;
241
+ }
242
+ }
243
+
244
+ // Comments
245
+ if (/^\s*#/.test(line)) {
246
+ highlightedLines.push(`<span class="syntax-punctuation">${escaped}</span>`);
247
+ continue;
248
+ }
249
+
250
+ // Key-value pairs
251
+ const keyValueMatch = escaped.match(/^(\s*)([^:]+?)(:)(\s*)(.*)$/);
252
+ if (keyValueMatch) {
253
+ const [, indent, key, colon, space, value] = keyValueMatch;
254
+
255
+ // Check for block scalar start
256
+ if (/^[|>][-+]?\d*$/.test(value.trim())) {
257
+ inBlockScalar = true;
258
+ blockScalarIndent = currentIndent;
259
+ const highlightedValue = `<span class="syntax-punctuation">${value}</span>`;
260
+ highlightedLines.push(`${indent}<span class="syntax-key">${key}</span><span class="syntax-punctuation">${colon}</span>${space}${highlightedValue}`);
261
+ continue;
262
+ }
263
+
264
+ // Check for start of multi-line quoted string
265
+ const startsWithQuote = /^(&quot;|&#039;|"|')/.test(value);
266
+ const endsWithQuote = /(&quot;|&#039;|"|')$/.test(value);
267
+
268
+ if (startsWithQuote && !endsWithQuote && value.length > 1) {
269
+ // Multi-line quoted string starts here
270
+ inQuotedString = true;
271
+ quoteChar = value.startsWith('&quot;') || value.startsWith('"') ? '"' : "'";
272
+ highlightedLines.push(`${indent}<span class="syntax-key">${key}</span><span class="syntax-punctuation">${colon}</span>${space}<span class="syntax-string">${value}</span>`);
273
+ continue;
274
+ }
275
+
276
+ // Check for start of unquoted multi-line string
277
+ // If value is an unquoted string and next line is more indented, it's a multiline
278
+ if (value && !startsWithQuote && i + 1 < lines.length) {
279
+ const nextLine = lines[i + 1];
280
+ const nextIndent = getIndentLevel(nextLine);
281
+ const nextTrimmed = nextLine.trim();
282
+ // Next line must be more indented than current key and not be a new key-value or array item
283
+ if (nextTrimmed && nextIndent > currentIndent && !nextTrimmed.includes(':') && !nextTrimmed.startsWith('-')) {
284
+ inUnquotedMultiline = true;
285
+ unquotedMultilineKeyIndent = currentIndent;
286
+ highlightedLines.push(`${indent}<span class="syntax-key">${key}</span><span class="syntax-punctuation">${colon}</span>${space}<span class="syntax-string">${value}</span>`);
287
+ continue;
288
+ }
289
+ }
290
+
291
+ // Normal single-line value
292
+ const highlightedValue = highlightYAMLValue(value);
293
+ highlightedLines.push(`${indent}<span class="syntax-key">${key}</span><span class="syntax-punctuation">${colon}</span>${space}${highlightedValue}`);
294
+ continue;
295
+ }
296
+
297
+ // Array items (lines starting with -)
298
+ const arrayMatch = escaped.match(/^(\s*)(-)(\s*)(.*)$/);
299
+ if (arrayMatch) {
300
+ const [, indent, dash, space, value] = arrayMatch;
301
+ const highlightedValue = value ? highlightYAMLValue(value) : "";
302
+ highlightedLines.push(`${indent}<span class="syntax-punctuation">${dash}</span>${space}${highlightedValue}`);
303
+ continue;
304
+ }
305
+
306
+ highlightedLines.push(escaped);
307
+ }
308
+
309
+ return highlightedLines.join("\n");
310
+ }
311
+
312
+ /**
313
+ * Highlight code based on format
314
+ */
315
+ export function highlightSyntax(code: string, options: HighlightOptions): string {
316
+ if (!code) return "";
317
+
318
+ switch (options.format) {
319
+ case "json":
320
+ return highlightJSON(code);
321
+ case "yaml":
322
+ return highlightYAML(code);
323
+ case "text":
324
+ default:
325
+ return escapeHtml(code);
326
+ }
327
+ }
@@ -1,4 +1,6 @@
1
1
  export * from "./datetime";
2
+ export * from "./highlightSyntax";
2
3
  export * from "./numbers";
3
- export * from "./strings";
4
4
  export * from "./parsers";
5
+ export * from "./renderMarkdown";
6
+ export * from "./strings";
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Lightweight markdown to HTML renderer
3
+ * Zero external dependencies, XSS-safe by default
4
+ */
5
+
6
+ export interface MarkdownRenderOptions {
7
+ sanitize?: boolean; // XSS protection (default: true)
8
+ }
9
+
10
+ /**
11
+ * Escape HTML entities to prevent XSS
12
+ */
13
+ function escapeHtml(text: string): string {
14
+ return text
15
+ .replace(/&/g, "&amp;")
16
+ .replace(/</g, "&lt;")
17
+ .replace(/>/g, "&gt;")
18
+ .replace(/"/g, "&quot;")
19
+ .replace(/'/g, "&#039;");
20
+ }
21
+
22
+ /**
23
+ * Token types for block-level parsing
24
+ */
25
+ export type BlockToken =
26
+ | { type: "heading"; level: number; content: string }
27
+ | { type: "code_block"; language: string; content: string }
28
+ | { type: "blockquote"; content: string }
29
+ | { type: "ul"; items: string[] }
30
+ | { type: "ol"; items: string[]; start: number }
31
+ | { type: "hr" }
32
+ | { type: "paragraph"; content: string };
33
+
34
+ /**
35
+ * Parse inline markdown elements within text
36
+ * Order matters: more specific patterns first
37
+ */
38
+ export function parseInline(text: string, sanitize: boolean = true): string {
39
+ if (!text) return "";
40
+
41
+ // Escape HTML if sanitizing (before applying markdown)
42
+ let result = sanitize ? escapeHtml(text) : text;
43
+
44
+ // Images: ![alt](url) - must be before links
45
+ result = result.replace(
46
+ /!\[([^\]]*)\]\(([^)]+)\)/g,
47
+ '<img src="$2" alt="$1" />'
48
+ );
49
+
50
+ // Links: [text](url)
51
+ result = result.replace(
52
+ /\[([^\]]+)\]\(([^)]+)\)/g,
53
+ '<a href="$2">$1</a>'
54
+ );
55
+
56
+ // Inline code: `code` (escape the content again if already escaped)
57
+ result = result.replace(/`([^`]+)`/g, "<code>$1</code>");
58
+
59
+ // Bold + Italic: ***text*** or ___text___
60
+ result = result.replace(/\*\*\*([^*]+)\*\*\*/g, "<strong><em>$1</em></strong>");
61
+ result = result.replace(/___([^_]+)___/g, "<strong><em>$1</em></strong>");
62
+
63
+ // Bold: **text** or __text__
64
+ result = result.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
65
+ result = result.replace(/__([^_]+)__/g, "<strong>$1</strong>");
66
+
67
+ // Italic: *text* or _text_ (but not inside words for underscores)
68
+ // For asterisks, match any single asterisk pairs
69
+ result = result.replace(/\*([^*]+)\*/g, "<em>$1</em>");
70
+ // For underscores, only match at word boundaries
71
+ result = result.replace(/(^|[^a-zA-Z0-9])_([^_]+)_([^a-zA-Z0-9]|$)/g, "$1<em>$2</em>$3");
72
+
73
+ return result;
74
+ }
75
+
76
+ /**
77
+ * Tokenize markdown into block-level elements
78
+ */
79
+ export function tokenizeBlocks(markdown: string): BlockToken[] {
80
+ const tokens: BlockToken[] = [];
81
+ const lines = markdown.split("\n");
82
+ let i = 0;
83
+
84
+ while (i < lines.length) {
85
+ const line = lines[i];
86
+ const trimmedLine = line.trim();
87
+
88
+ // Skip empty lines between blocks
89
+ if (!trimmedLine) {
90
+ i++;
91
+ continue;
92
+ }
93
+
94
+ // Code blocks: ```language ... ```
95
+ if (trimmedLine.startsWith("```")) {
96
+ const language = trimmedLine.slice(3).trim();
97
+ const contentLines: string[] = [];
98
+ i++;
99
+
100
+ while (i < lines.length && !lines[i].trim().startsWith("```")) {
101
+ contentLines.push(lines[i]);
102
+ i++;
103
+ }
104
+
105
+ tokens.push({
106
+ type: "code_block",
107
+ language,
108
+ content: contentLines.join("\n")
109
+ });
110
+
111
+ // Skip closing ```
112
+ if (i < lines.length) i++;
113
+ continue;
114
+ }
115
+
116
+ // Headings: # through ######
117
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
118
+ if (headingMatch) {
119
+ tokens.push({
120
+ type: "heading",
121
+ level: headingMatch[1].length,
122
+ content: headingMatch[2]
123
+ });
124
+ i++;
125
+ continue;
126
+ }
127
+
128
+ // Horizontal rules: ---, ***, ___
129
+ if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmedLine)) {
130
+ tokens.push({ type: "hr" });
131
+ i++;
132
+ continue;
133
+ }
134
+
135
+ // Blockquotes: > text
136
+ if (trimmedLine.startsWith(">")) {
137
+ const quoteLines: string[] = [];
138
+
139
+ while (i < lines.length && lines[i].trim().startsWith(">")) {
140
+ // Remove the leading > and optional space
141
+ quoteLines.push(lines[i].trim().replace(/^>\s?/, ""));
142
+ i++;
143
+ }
144
+
145
+ tokens.push({
146
+ type: "blockquote",
147
+ content: quoteLines.join("\n")
148
+ });
149
+ continue;
150
+ }
151
+
152
+ // Unordered lists: -, *, +
153
+ if (/^[-*+]\s+/.test(trimmedLine)) {
154
+ const items: string[] = [];
155
+
156
+ while (i < lines.length) {
157
+ const listLine = lines[i].trim();
158
+ const listMatch = listLine.match(/^[-*+]\s+(.+)$/);
159
+
160
+ if (listMatch) {
161
+ items.push(listMatch[1]);
162
+ i++;
163
+ } else if (listLine === "") {
164
+ // Empty line might end the list or just be spacing
165
+ i++;
166
+ // Check if next non-empty line continues the list
167
+ const nextNonEmpty = lines.slice(i).find((l) => l.trim() !== "");
168
+ if (!nextNonEmpty || !/^[-*+]\s+/.test(nextNonEmpty.trim())) {
169
+ break;
170
+ }
171
+ } else {
172
+ break;
173
+ }
174
+ }
175
+
176
+ tokens.push({ type: "ul", items });
177
+ continue;
178
+ }
179
+
180
+ // Ordered lists: 1., 2., etc.
181
+ const orderedMatch = trimmedLine.match(/^(\d+)\.\s+(.+)$/);
182
+ if (orderedMatch) {
183
+ const items: string[] = [];
184
+ const startNum = parseInt(orderedMatch[1], 10);
185
+
186
+ while (i < lines.length) {
187
+ const listLine = lines[i].trim();
188
+ const listMatch = listLine.match(/^\d+\.\s+(.+)$/);
189
+
190
+ if (listMatch) {
191
+ items.push(listMatch[1]);
192
+ i++;
193
+ } else if (listLine === "") {
194
+ i++;
195
+ // Check if next non-empty line continues the list
196
+ const nextNonEmpty = lines.slice(i).find((l) => l.trim() !== "");
197
+ if (!nextNonEmpty || !/^\d+\.\s+/.test(nextNonEmpty.trim())) {
198
+ break;
199
+ }
200
+ } else {
201
+ break;
202
+ }
203
+ }
204
+
205
+ tokens.push({ type: "ol", items, start: startNum });
206
+ continue;
207
+ }
208
+
209
+ // Paragraph: collect consecutive non-empty lines
210
+ const paragraphLines: string[] = [];
211
+
212
+ while (i < lines.length) {
213
+ const pLine = lines[i];
214
+ const pTrimmed = pLine.trim();
215
+
216
+ // Stop on empty line or block-level element
217
+ if (!pTrimmed) {
218
+ i++;
219
+ break;
220
+ }
221
+
222
+ // Check for block-level starters
223
+ if (
224
+ pTrimmed.startsWith("#") ||
225
+ pTrimmed.startsWith("```") ||
226
+ pTrimmed.startsWith(">") ||
227
+ /^[-*+]\s+/.test(pTrimmed) ||
228
+ /^\d+\.\s+/.test(pTrimmed) ||
229
+ /^(-{3,}|\*{3,}|_{3,})$/.test(pTrimmed)
230
+ ) {
231
+ break;
232
+ }
233
+
234
+ paragraphLines.push(pLine);
235
+ i++;
236
+ }
237
+
238
+ if (paragraphLines.length > 0) {
239
+ tokens.push({
240
+ type: "paragraph",
241
+ content: paragraphLines.join("\n")
242
+ });
243
+ }
244
+ }
245
+
246
+ return tokens;
247
+ }
248
+
249
+ /**
250
+ * Render tokens to HTML
251
+ */
252
+ function renderTokens(tokens: BlockToken[], sanitize: boolean): string {
253
+ const htmlParts: string[] = [];
254
+
255
+ for (const token of tokens) {
256
+ switch (token.type) {
257
+ case "heading": {
258
+ const content = parseInline(token.content, sanitize);
259
+ htmlParts.push(`<h${token.level}>${content}</h${token.level}>`);
260
+ break;
261
+ }
262
+
263
+ case "code_block": {
264
+ // Always escape code block content for safety
265
+ const escapedContent = escapeHtml(token.content);
266
+ const langAttr = token.language ? ` class="language-${escapeHtml(token.language)}"` : "";
267
+ htmlParts.push(`<pre><code${langAttr}>${escapedContent}</code></pre>`);
268
+ break;
269
+ }
270
+
271
+ case "blockquote": {
272
+ // Recursively parse blockquote content
273
+ const innerTokens = tokenizeBlocks(token.content);
274
+ const innerHtml = renderTokens(innerTokens, sanitize);
275
+ htmlParts.push(`<blockquote>${innerHtml}</blockquote>`);
276
+ break;
277
+ }
278
+
279
+ case "ul": {
280
+ const items = token.items
281
+ .map((item) => `<li>${parseInline(item, sanitize)}</li>`)
282
+ .join("");
283
+ htmlParts.push(`<ul>${items}</ul>`);
284
+ break;
285
+ }
286
+
287
+ case "ol": {
288
+ const items = token.items
289
+ .map((item) => `<li>${parseInline(item, sanitize)}</li>`)
290
+ .join("");
291
+ const startAttr = token.start !== 1 ? ` start="${token.start}"` : "";
292
+ htmlParts.push(`<ol${startAttr}>${items}</ol>`);
293
+ break;
294
+ }
295
+
296
+ case "hr": {
297
+ htmlParts.push("<hr />");
298
+ break;
299
+ }
300
+
301
+ case "paragraph": {
302
+ const content = parseInline(token.content, sanitize);
303
+ // Convert single newlines to <br> within paragraphs
304
+ const withBreaks = content.replace(/\n/g, "<br />");
305
+ htmlParts.push(`<p>${withBreaks}</p>`);
306
+ break;
307
+ }
308
+ }
309
+ }
310
+
311
+ return htmlParts.join("\n");
312
+ }
313
+
314
+ /**
315
+ * Convert markdown text to HTML
316
+ *
317
+ * Supports:
318
+ * - Headings (# through ######)
319
+ * - Paragraphs (double newlines)
320
+ * - Code blocks (```language ... ```)
321
+ * - Blockquotes (> text)
322
+ * - Unordered lists (-, *, +)
323
+ * - Ordered lists (1., 2., etc.)
324
+ * - Horizontal rules (---, ***, ___)
325
+ * - Bold (**text** or __text__)
326
+ * - Italic (*text* or _text_)
327
+ * - Bold+Italic (***text***)
328
+ * - Inline code (`code`)
329
+ * - Links [text](url)
330
+ * - Images ![alt](url)
331
+ */
332
+ export function renderMarkdown(markdown: string, options?: MarkdownRenderOptions): string {
333
+ if (!markdown) return "";
334
+
335
+ const sanitize = options?.sanitize ?? true;
336
+ const tokens = tokenizeBlocks(markdown);
337
+ return renderTokens(tokens, sanitize);
338
+ }
@@ -1,3 +1,6 @@
1
+ @import "themes/danx/scrollbar";
2
+ @import "themes/danx/code";
3
+
1
4
  .dx-action-table {
2
5
  .dx-column-shrink {
3
6
  width: 1px;