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.
Files changed (55) hide show
  1. package/dist/danx.es.js +25284 -23176
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +133 -120
  4. package/dist/danx.umd.js.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +4 -2
  7. package/scripts/publish.sh +76 -0
  8. package/src/components/Utility/Buttons/ActionButton.vue +11 -3
  9. package/src/components/Utility/Code/CodeViewer.vue +219 -0
  10. package/src/components/Utility/Code/CodeViewerCollapsed.vue +34 -0
  11. package/src/components/Utility/Code/CodeViewerFooter.vue +53 -0
  12. package/src/components/Utility/Code/LanguageBadge.vue +122 -0
  13. package/src/components/Utility/Code/MarkdownContent.vue +405 -0
  14. package/src/components/Utility/Code/index.ts +5 -0
  15. package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +134 -38
  16. package/src/components/Utility/Files/CarouselHeader.vue +24 -0
  17. package/src/components/Utility/Files/FileMetadataDialog.vue +69 -0
  18. package/src/components/Utility/Files/FilePreview.vue +118 -166
  19. package/src/components/Utility/Files/index.ts +1 -0
  20. package/src/components/Utility/index.ts +1 -0
  21. package/src/composables/index.ts +5 -0
  22. package/src/composables/useCodeFormat.ts +199 -0
  23. package/src/composables/useCodeViewerCollapse.ts +125 -0
  24. package/src/composables/useCodeViewerEditor.ts +420 -0
  25. package/src/composables/useFilePreview.ts +119 -0
  26. package/src/composables/useTranscodeLoader.ts +68 -0
  27. package/src/helpers/formats/highlightSyntax.ts +327 -0
  28. package/src/helpers/formats/index.ts +3 -1
  29. package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
  30. package/src/helpers/formats/markdown/escapeSequences.ts +60 -0
  31. package/src/helpers/formats/markdown/index.ts +85 -0
  32. package/src/helpers/formats/markdown/parseInline.ts +124 -0
  33. package/src/helpers/formats/markdown/render/index.ts +92 -0
  34. package/src/helpers/formats/markdown/render/renderFootnotes.ts +30 -0
  35. package/src/helpers/formats/markdown/render/renderList.ts +69 -0
  36. package/src/helpers/formats/markdown/render/renderTable.ts +38 -0
  37. package/src/helpers/formats/markdown/state.ts +58 -0
  38. package/src/helpers/formats/markdown/tokenize/extractDefinitions.ts +39 -0
  39. package/src/helpers/formats/markdown/tokenize/index.ts +139 -0
  40. package/src/helpers/formats/markdown/tokenize/parseBlockquote.ts +34 -0
  41. package/src/helpers/formats/markdown/tokenize/parseCodeBlock.ts +85 -0
  42. package/src/helpers/formats/markdown/tokenize/parseDefinitionList.ts +88 -0
  43. package/src/helpers/formats/markdown/tokenize/parseHeading.ts +65 -0
  44. package/src/helpers/formats/markdown/tokenize/parseHorizontalRule.ts +22 -0
  45. package/src/helpers/formats/markdown/tokenize/parseList.ts +119 -0
  46. package/src/helpers/formats/markdown/tokenize/parseParagraph.ts +59 -0
  47. package/src/helpers/formats/markdown/tokenize/parseTable.ts +70 -0
  48. package/src/helpers/formats/markdown/tokenize/parseTaskList.ts +47 -0
  49. package/src/helpers/formats/markdown/tokenize/utils.ts +25 -0
  50. package/src/helpers/formats/markdown/types.ts +63 -0
  51. package/src/styles/danx.scss +4 -0
  52. package/src/styles/themes/danx/code.scss +158 -0
  53. package/src/styles/themes/danx/index.scss +2 -0
  54. package/src/styles/themes/danx/markdown.scss +241 -0
  55. package/src/styles/themes/danx/scrollbar.scss +125 -0
@@ -0,0 +1,68 @@
1
+ import { onMounted, Ref, ref, watch } from "vue";
2
+ import { danxOptions } from "../config";
3
+ import { UploadedFile } from "../types";
4
+
5
+ export interface UseTranscodeLoaderOptions {
6
+ file: Ref<UploadedFile | null | undefined>;
7
+ }
8
+
9
+ export interface UseTranscodeLoaderReturn {
10
+ isLoading: Ref<boolean>;
11
+ loadTranscodes: () => Promise<void>;
12
+ }
13
+
14
+ /**
15
+ * Composable for loading transcodes for a file
16
+ * Automatically loads transcodes on mount and when the file changes
17
+ */
18
+ export function useTranscodeLoader(options: UseTranscodeLoaderOptions): UseTranscodeLoaderReturn {
19
+ const { file } = options;
20
+ const isLoading = ref(false);
21
+
22
+ function shouldLoadTranscodes(): boolean {
23
+ if (!file.value?.id) return false;
24
+ if (isLoading.value) return false;
25
+ if (!danxOptions.value.fileUpload?.refreshFile) return false;
26
+
27
+ // Only load if transcodes is explicitly null, undefined, or an empty array
28
+ const transcodes = file.value.transcodes;
29
+ return transcodes === null || transcodes === undefined || (Array.isArray(transcodes) && transcodes.length === 0);
30
+ }
31
+
32
+ async function loadTranscodes() {
33
+ if (!shouldLoadTranscodes()) return;
34
+
35
+ isLoading.value = true;
36
+
37
+ try {
38
+ const refreshFile = danxOptions.value.fileUpload?.refreshFile;
39
+ if (refreshFile && file.value?.id) {
40
+ const refreshedFile = await refreshFile(file.value.id);
41
+
42
+ // Update the file object with the loaded transcodes
43
+ if (refreshedFile.transcodes && file.value) {
44
+ file.value.transcodes = refreshedFile.transcodes;
45
+ }
46
+ }
47
+ } catch (error) {
48
+ console.error("Failed to load transcodes:", error);
49
+ } finally {
50
+ isLoading.value = false;
51
+ }
52
+ }
53
+
54
+ // Load transcodes when component mounts
55
+ onMounted(() => {
56
+ loadTranscodes();
57
+ });
58
+
59
+ // Watch for file changes and reload transcodes if needed
60
+ watch(() => file.value?.id, () => {
61
+ loadTranscodes();
62
+ });
63
+
64
+ return {
65
+ isLoading,
66
+ loadTranscodes
67
+ };
68
+ }
@@ -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, "&amp;")
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 "./markdown";
6
+ export * from "./strings";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * HTML entity escaping for XSS prevention
3
+ */
4
+
5
+ /**
6
+ * Escape HTML entities to prevent XSS
7
+ */
8
+ export function escapeHtml(text: string): string {
9
+ return text
10
+ .replace(/&/g, "&amp;")
11
+ .replace(/</g, "&lt;")
12
+ .replace(/>/g, "&gt;")
13
+ .replace(/"/g, "&quot;")
14
+ .replace(/'/g, "&#039;");
15
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Markdown escape sequence handling
3
+ * Maps backslash-escaped characters to Unicode placeholders and back
4
+ * Using Private Use Area characters (U+E000-U+F8FF) to avoid conflicts
5
+ */
6
+
7
+ /**
8
+ * Escape sequences mapping - character to Unicode placeholder
9
+ */
10
+ export const ESCAPE_MAP: Record<string, string> = {
11
+ "\\*": "\uE000",
12
+ "\\_": "\uE001",
13
+ "\\~": "\uE002",
14
+ "\\`": "\uE003",
15
+ "\\[": "\uE004",
16
+ "\\]": "\uE005",
17
+ "\\#": "\uE006",
18
+ "\\&gt;": "\uE007", // Escaped > becomes &gt; after HTML escaping
19
+ "\\-": "\uE008",
20
+ "\\+": "\uE009",
21
+ "\\.": "\uE00A",
22
+ "\\!": "\uE00B",
23
+ "\\=": "\uE00C",
24
+ "\\^": "\uE00D"
25
+ };
26
+
27
+ /**
28
+ * Reverse mapping - placeholder back to literal character
29
+ * Generated from ESCAPE_MAP for DRY compliance
30
+ */
31
+ export const UNESCAPE_MAP: Record<string, string> = Object.fromEntries(
32
+ Object.entries(ESCAPE_MAP).map(([escaped, placeholder]) => {
33
+ // Extract the literal character from the escape sequence
34
+ // "\\*" -> "*", "\\&gt;" -> "&gt;" (special case for HTML-escaped >)
35
+ const literal = escaped.startsWith("\\&") ? escaped.slice(1) : escaped.slice(1);
36
+ return [placeholder, literal];
37
+ })
38
+ );
39
+
40
+ /**
41
+ * Apply escape sequences - convert backslash-escaped characters to placeholders
42
+ */
43
+ export function applyEscapes(text: string): string {
44
+ let result = text;
45
+ for (const [pattern, placeholder] of Object.entries(ESCAPE_MAP)) {
46
+ result = result.split(pattern).join(placeholder);
47
+ }
48
+ return result;
49
+ }
50
+
51
+ /**
52
+ * Revert escape sequences - convert placeholders back to literal characters
53
+ */
54
+ export function revertEscapes(text: string): string {
55
+ let result = text;
56
+ for (const [placeholder, literal] of Object.entries(UNESCAPE_MAP)) {
57
+ result = result.split(placeholder).join(literal);
58
+ }
59
+ return result;
60
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Lightweight markdown to HTML renderer
3
+ * Zero external dependencies, XSS-safe by default
4
+ *
5
+ * Supports:
6
+ * - Headings (# through ###### and setext-style with === or ---)
7
+ * - Paragraphs (double newlines)
8
+ * - Code blocks (```language ... ``` and indented with 4 spaces or tab)
9
+ * - Blockquotes (> text)
10
+ * - Unordered lists (-, *, +)
11
+ * - Ordered lists (1., 2., etc.)
12
+ * - Task lists (- [ ] unchecked, - [x] checked)
13
+ * - Definition lists (Term followed by : Definition)
14
+ * - Tables (| col | col | with alignment support)
15
+ * - Horizontal rules (---, ***, ___)
16
+ * - Bold (**text** or __text__)
17
+ * - Italic (*text* or _text_)
18
+ * - Bold+Italic (***text***)
19
+ * - Inline code (`code`)
20
+ * - Links [text](url)
21
+ * - Reference-style links ([text][ref], [text][], [ref] with [ref]: url definitions)
22
+ * - Images ![alt](url)
23
+ * - Escape sequences (\* \_ \~ etc.)
24
+ * - Hard line breaks (two trailing spaces)
25
+ * - Autolinks (<https://...> and <email@...>)
26
+ * - Strikethrough (~~text~~)
27
+ * - Highlight (==text==)
28
+ * - Superscript (X^2^)
29
+ * - Subscript (H~2~O)
30
+ * - Footnotes ([^id] with [^id]: content definitions)
31
+ */
32
+
33
+ // Re-export types
34
+ export type {
35
+ MarkdownRenderOptions,
36
+ LinkReference,
37
+ FootnoteDefinition,
38
+ ListItem,
39
+ BlockToken,
40
+ TableAlignment,
41
+ ParseResult
42
+ } from "./types";
43
+
44
+ // Re-export state management
45
+ export { getFootnotes, resetParserState } from "./state";
46
+
47
+ // Re-export parsers
48
+ export { parseInline } from "./parseInline";
49
+ export { tokenizeBlocks } from "./tokenize";
50
+
51
+ // Re-export renderers
52
+ export { renderTokens } from "./render";
53
+
54
+ // Import for main function
55
+ import type { MarkdownRenderOptions } from "./types";
56
+ import { getFootnotes, resetParserState } from "./state";
57
+ import { tokenizeBlocks } from "./tokenize";
58
+ import { renderTokens } from "./render";
59
+ import { renderFootnotesSection } from "./render/renderFootnotes";
60
+
61
+ /**
62
+ * Convert markdown text to HTML
63
+ */
64
+ export function renderMarkdown(markdown: string, options?: MarkdownRenderOptions): string {
65
+ if (!markdown) return "";
66
+
67
+ const sanitize = options?.sanitize ?? true;
68
+ const preserveState = options?.preserveState ?? false;
69
+
70
+ // Reset state for fresh document parse (unless preserving for nested rendering)
71
+ if (!preserveState) {
72
+ resetParserState();
73
+ }
74
+
75
+ const tokens = tokenizeBlocks(markdown);
76
+ let html = renderTokens(tokens, sanitize);
77
+
78
+ // Append footnotes section if any exist (only for top-level rendering)
79
+ const footnotes = getFootnotes();
80
+ if (!preserveState && Object.keys(footnotes).length > 0) {
81
+ html += renderFootnotesSection(footnotes, sanitize);
82
+ }
83
+
84
+ return html;
85
+ }
@@ -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
+ }