quasar-ui-danx 0.5.1 → 0.5.3
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/.claude/settings.local.json +8 -0
- package/dist/danx.es.js +13869 -12976
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +159 -151
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/Utility/Buttons/ActionButton.vue +15 -5
- package/src/components/Utility/Code/CodeViewer.vue +10 -2
- package/src/components/Utility/Code/CodeViewerFooter.vue +2 -0
- package/src/components/Utility/Code/MarkdownContent.vue +31 -163
- package/src/components/Utility/Files/FilePreview.vue +2 -2
- package/src/components/Utility/Markdown/MarkdownEditor.vue +7 -2
- package/src/components/Utility/Markdown/MarkdownEditorContent.vue +69 -8
- package/src/components/Utility/Widgets/LabelPillWidget.vue +20 -0
- package/src/composables/markdown/features/useCodeBlocks.spec.ts +59 -33
- package/src/composables/markdown/features/useLinks.spec.ts +29 -10
- package/src/composables/markdown/useMarkdownEditor.ts +16 -7
- package/src/composables/useCodeFormat.ts +17 -10
- package/src/composables/useCodeViewerCollapse.ts +7 -0
- package/src/composables/useFileNavigation.ts +5 -1
- package/src/helpers/formats/highlightCSS.ts +236 -0
- package/src/helpers/formats/highlightHTML.ts +483 -0
- package/src/helpers/formats/highlightJavaScript.ts +346 -0
- package/src/helpers/formats/highlightSyntax.ts +15 -4
- package/src/helpers/formats/index.ts +3 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +42 -4
- package/src/helpers/formats/markdown/linePatterns.spec.ts +7 -4
- package/src/helpers/formats/markdown/parseInline.ts +26 -13
- package/src/helpers/routes.ts +3 -1
- package/src/styles/danx.scss +3 -3
- package/src/styles/index.scss +5 -5
- package/src/styles/themes/danx/code.scss +257 -1
- package/src/styles/themes/danx/index.scss +10 -10
- package/src/styles/themes/danx/markdown.scss +81 -0
- package/src/test/highlighters.test.ts +153 -0
- package/src/types/widgets.d.ts +2 -2
- package/vite.config.js +5 -1
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight syntax highlighting for JavaScript
|
|
3
|
+
* Returns HTML string with syntax highlighting spans
|
|
4
|
+
* Uses character-by-character tokenization for accurate parsing
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Escape HTML entities to prevent XSS
|
|
9
|
+
*/
|
|
10
|
+
function escapeHtml(text: string): string {
|
|
11
|
+
return text
|
|
12
|
+
.replace(/&/g, "&")
|
|
13
|
+
.replace(/</g, "<")
|
|
14
|
+
.replace(/>/g, ">")
|
|
15
|
+
.replace(/"/g, """)
|
|
16
|
+
.replace(/'/g, "'");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* JavaScript keywords that should be highlighted
|
|
21
|
+
*/
|
|
22
|
+
const JS_KEYWORDS = new Set([
|
|
23
|
+
// Declarations
|
|
24
|
+
"const", "let", "var", "function", "class", "extends", "static",
|
|
25
|
+
// Control flow
|
|
26
|
+
"if", "else", "switch", "case", "default", "for", "while", "do",
|
|
27
|
+
"break", "continue", "return", "throw", "try", "catch", "finally",
|
|
28
|
+
// Operators/values
|
|
29
|
+
"new", "delete", "typeof", "instanceof", "in", "of", "void",
|
|
30
|
+
// Async
|
|
31
|
+
"async", "await", "yield",
|
|
32
|
+
// Module
|
|
33
|
+
"import", "export", "from", "as",
|
|
34
|
+
// Other
|
|
35
|
+
"this", "super", "debugger", "with"
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* JavaScript built-in values
|
|
40
|
+
*/
|
|
41
|
+
const JS_BUILTINS = new Set([
|
|
42
|
+
"true", "false", "null", "undefined", "NaN", "Infinity"
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if character can start an identifier
|
|
47
|
+
*/
|
|
48
|
+
function isIdentifierStart(char: string): boolean {
|
|
49
|
+
return /[a-zA-Z_$]/.test(char);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if character can be part of an identifier
|
|
54
|
+
*/
|
|
55
|
+
function isIdentifierPart(char: string): boolean {
|
|
56
|
+
return /[a-zA-Z0-9_$]/.test(char);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if a character could precede a regex literal
|
|
61
|
+
* Regex can appear after: ( [ { , ; : ! & | = + - * / ? ~ ^ %
|
|
62
|
+
* or at the start of a statement (after newline, return, etc.)
|
|
63
|
+
*/
|
|
64
|
+
function canPrecedeRegex(lastToken: string): boolean {
|
|
65
|
+
if (!lastToken) return true;
|
|
66
|
+
const operators = ["(", "[", "{", ",", ";", ":", "!", "&", "|", "=", "+", "-", "*", "/", "?", "~", "^", "%", "<", ">"];
|
|
67
|
+
const keywords = ["return", "throw", "case", "delete", "void", "typeof", "instanceof", "in", "of", "new"];
|
|
68
|
+
return operators.includes(lastToken) || keywords.includes(lastToken);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Highlight JavaScript syntax by tokenizing character-by-character
|
|
73
|
+
*/
|
|
74
|
+
export function highlightJavaScript(code: string): string {
|
|
75
|
+
if (!code) return "";
|
|
76
|
+
|
|
77
|
+
const result: string[] = [];
|
|
78
|
+
let i = 0;
|
|
79
|
+
let lastToken = ""; // Track last significant token for regex detection
|
|
80
|
+
|
|
81
|
+
while (i < code.length) {
|
|
82
|
+
const char = code[i];
|
|
83
|
+
|
|
84
|
+
// Handle single-line comments: // ...
|
|
85
|
+
if (char === "/" && code[i + 1] === "/") {
|
|
86
|
+
const startIndex = i;
|
|
87
|
+
i += 2; // Skip //
|
|
88
|
+
|
|
89
|
+
// Find end of line
|
|
90
|
+
while (i < code.length && code[i] !== "\n") {
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const comment = code.slice(startIndex, i);
|
|
95
|
+
result.push(`<span class="syntax-comment">${escapeHtml(comment)}</span>`);
|
|
96
|
+
lastToken = "";
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Handle multi-line comments: /* ... */
|
|
101
|
+
if (char === "/" && code[i + 1] === "*") {
|
|
102
|
+
const startIndex = i;
|
|
103
|
+
i += 2; // Skip /*
|
|
104
|
+
|
|
105
|
+
// Find closing */
|
|
106
|
+
while (i < code.length) {
|
|
107
|
+
if (code[i] === "*" && code[i + 1] === "/") {
|
|
108
|
+
i += 2; // Include */
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
i++;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const comment = code.slice(startIndex, i);
|
|
115
|
+
result.push(`<span class="syntax-comment">${escapeHtml(comment)}</span>`);
|
|
116
|
+
lastToken = "";
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle template literals: `...`
|
|
121
|
+
if (char === "`") {
|
|
122
|
+
const startIndex = i;
|
|
123
|
+
i++; // Skip opening backtick
|
|
124
|
+
|
|
125
|
+
// Find closing backtick, handling escape sequences and ${} expressions
|
|
126
|
+
while (i < code.length) {
|
|
127
|
+
if (code[i] === "\\" && i + 1 < code.length) {
|
|
128
|
+
i += 2; // Skip escaped character
|
|
129
|
+
} else if (code[i] === "$" && code[i + 1] === "{") {
|
|
130
|
+
// Template expression - for now, just include it in the string
|
|
131
|
+
// A more sophisticated approach would recursively highlight the expression
|
|
132
|
+
let braceDepth = 1;
|
|
133
|
+
i += 2; // Skip ${
|
|
134
|
+
while (i < code.length && braceDepth > 0) {
|
|
135
|
+
if (code[i] === "{") braceDepth++;
|
|
136
|
+
else if (code[i] === "}") braceDepth--;
|
|
137
|
+
i++;
|
|
138
|
+
}
|
|
139
|
+
} else if (code[i] === "`") {
|
|
140
|
+
i++; // Include closing backtick
|
|
141
|
+
break;
|
|
142
|
+
} else {
|
|
143
|
+
i++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const str = code.slice(startIndex, i);
|
|
148
|
+
result.push(`<span class="syntax-template">${escapeHtml(str)}</span>`);
|
|
149
|
+
lastToken = "string";
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Handle strings (single or double quoted)
|
|
154
|
+
if (char === '"' || char === "'") {
|
|
155
|
+
const quoteChar = char;
|
|
156
|
+
const startIndex = i;
|
|
157
|
+
i++; // Skip opening quote
|
|
158
|
+
|
|
159
|
+
// Find closing quote, handling escape sequences
|
|
160
|
+
while (i < code.length) {
|
|
161
|
+
if (code[i] === "\\" && i + 1 < code.length) {
|
|
162
|
+
i += 2; // Skip escaped character
|
|
163
|
+
} else if (code[i] === quoteChar) {
|
|
164
|
+
i++; // Include closing quote
|
|
165
|
+
break;
|
|
166
|
+
} else if (code[i] === "\n") {
|
|
167
|
+
// Unterminated string - stop at newline
|
|
168
|
+
break;
|
|
169
|
+
} else {
|
|
170
|
+
i++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const str = code.slice(startIndex, i);
|
|
175
|
+
result.push(`<span class="syntax-string">${escapeHtml(str)}</span>`);
|
|
176
|
+
lastToken = "string";
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Handle regex literals: /pattern/flags
|
|
181
|
+
if (char === "/" && canPrecedeRegex(lastToken)) {
|
|
182
|
+
// Check if this looks like a regex (not division)
|
|
183
|
+
// A regex must be followed by at least one character that's not / or *
|
|
184
|
+
if (code[i + 1] && code[i + 1] !== "/" && code[i + 1] !== "*") {
|
|
185
|
+
const startIndex = i;
|
|
186
|
+
i++; // Skip opening /
|
|
187
|
+
|
|
188
|
+
// Find closing / handling escape sequences and character classes
|
|
189
|
+
let inCharClass = false;
|
|
190
|
+
while (i < code.length) {
|
|
191
|
+
if (code[i] === "\\" && i + 1 < code.length) {
|
|
192
|
+
i += 2; // Skip escaped character
|
|
193
|
+
} else if (code[i] === "[" && !inCharClass) {
|
|
194
|
+
inCharClass = true;
|
|
195
|
+
i++;
|
|
196
|
+
} else if (code[i] === "]" && inCharClass) {
|
|
197
|
+
inCharClass = false;
|
|
198
|
+
i++;
|
|
199
|
+
} else if (code[i] === "/" && !inCharClass) {
|
|
200
|
+
i++; // Include closing /
|
|
201
|
+
// Include flags (g, i, m, s, u, y, d)
|
|
202
|
+
while (i < code.length && /[gimsuy]/.test(code[i])) {
|
|
203
|
+
i++;
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
} else if (code[i] === "\n") {
|
|
207
|
+
// Unterminated regex - stop at newline
|
|
208
|
+
break;
|
|
209
|
+
} else {
|
|
210
|
+
i++;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const regex = code.slice(startIndex, i);
|
|
215
|
+
result.push(`<span class="syntax-regex">${escapeHtml(regex)}</span>`);
|
|
216
|
+
lastToken = "regex";
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Handle numbers
|
|
222
|
+
if (/\d/.test(char) || (char === "." && /\d/.test(code[i + 1] || ""))) {
|
|
223
|
+
const startIndex = i;
|
|
224
|
+
|
|
225
|
+
// Check for hex, octal, binary
|
|
226
|
+
if (char === "0" && code[i + 1]) {
|
|
227
|
+
const next = code[i + 1].toLowerCase();
|
|
228
|
+
if (next === "x") {
|
|
229
|
+
// Hex: 0x[0-9a-f]+
|
|
230
|
+
i += 2;
|
|
231
|
+
while (i < code.length && /[0-9a-fA-F_]/.test(code[i])) i++;
|
|
232
|
+
} else if (next === "b") {
|
|
233
|
+
// Binary: 0b[01]+
|
|
234
|
+
i += 2;
|
|
235
|
+
while (i < code.length && /[01_]/.test(code[i])) i++;
|
|
236
|
+
} else if (next === "o") {
|
|
237
|
+
// Octal: 0o[0-7]+
|
|
238
|
+
i += 2;
|
|
239
|
+
while (i < code.length && /[0-7_]/.test(code[i])) i++;
|
|
240
|
+
} else {
|
|
241
|
+
// Regular number or legacy octal
|
|
242
|
+
i++;
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
i++;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Continue with decimal part
|
|
249
|
+
while (i < code.length && /[\d_]/.test(code[i])) i++;
|
|
250
|
+
|
|
251
|
+
// Decimal point
|
|
252
|
+
if (code[i] === "." && /\d/.test(code[i + 1] || "")) {
|
|
253
|
+
i++;
|
|
254
|
+
while (i < code.length && /[\d_]/.test(code[i])) i++;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Exponent
|
|
258
|
+
if ((code[i] === "e" || code[i] === "E") && /[\d+-]/.test(code[i + 1] || "")) {
|
|
259
|
+
i++;
|
|
260
|
+
if (code[i] === "+" || code[i] === "-") i++;
|
|
261
|
+
while (i < code.length && /[\d_]/.test(code[i])) i++;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// BigInt suffix
|
|
265
|
+
if (code[i] === "n") i++;
|
|
266
|
+
|
|
267
|
+
const num = code.slice(startIndex, i);
|
|
268
|
+
result.push(`<span class="syntax-number">${escapeHtml(num)}</span>`);
|
|
269
|
+
lastToken = "number";
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Handle identifiers and keywords
|
|
274
|
+
if (isIdentifierStart(char)) {
|
|
275
|
+
const startIndex = i;
|
|
276
|
+
while (i < code.length && isIdentifierPart(code[i])) {
|
|
277
|
+
i++;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const identifier = code.slice(startIndex, i);
|
|
281
|
+
|
|
282
|
+
if (JS_KEYWORDS.has(identifier)) {
|
|
283
|
+
result.push(`<span class="syntax-keyword">${escapeHtml(identifier)}</span>`);
|
|
284
|
+
lastToken = identifier;
|
|
285
|
+
} else if (JS_BUILTINS.has(identifier)) {
|
|
286
|
+
if (identifier === "true" || identifier === "false") {
|
|
287
|
+
result.push(`<span class="syntax-boolean">${escapeHtml(identifier)}</span>`);
|
|
288
|
+
} else {
|
|
289
|
+
result.push(`<span class="syntax-null">${escapeHtml(identifier)}</span>`);
|
|
290
|
+
}
|
|
291
|
+
lastToken = identifier;
|
|
292
|
+
} else {
|
|
293
|
+
// Regular identifier
|
|
294
|
+
result.push(escapeHtml(identifier));
|
|
295
|
+
lastToken = "identifier";
|
|
296
|
+
}
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Handle operators
|
|
301
|
+
const operators = ["===", "!==", "==", "!=", "<=", ">=", "&&", "||", "??",
|
|
302
|
+
"++", "--", "+=", "-=", "*=", "/=", "%=", "**=", "&=", "|=", "^=",
|
|
303
|
+
"<<=", ">>=", ">>>=", "=>", "...", "**", "<<", ">>", ">>>",
|
|
304
|
+
"+", "-", "*", "/", "%", "&", "|", "^", "~", "!", "<", ">", "=", "?", ":"
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
let matchedOp = "";
|
|
308
|
+
for (const op of operators) {
|
|
309
|
+
if (code.slice(i, i + op.length) === op) {
|
|
310
|
+
if (op.length > matchedOp.length) {
|
|
311
|
+
matchedOp = op;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (matchedOp) {
|
|
317
|
+
result.push(`<span class="syntax-operator">${escapeHtml(matchedOp)}</span>`);
|
|
318
|
+
lastToken = matchedOp;
|
|
319
|
+
i += matchedOp.length;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Handle punctuation
|
|
324
|
+
if (/[{}()\[\];,.]/.test(char)) {
|
|
325
|
+
result.push(`<span class="syntax-punctuation">${escapeHtml(char)}</span>`);
|
|
326
|
+
lastToken = char;
|
|
327
|
+
i++;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Handle whitespace
|
|
332
|
+
if (/\s/.test(char)) {
|
|
333
|
+
result.push(escapeHtml(char));
|
|
334
|
+
// Don't reset lastToken for whitespace
|
|
335
|
+
i++;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Default: just escape and add
|
|
340
|
+
result.push(escapeHtml(char));
|
|
341
|
+
lastToken = char;
|
|
342
|
+
i++;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return result.join("");
|
|
346
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Lightweight syntax highlighting for JSON and
|
|
2
|
+
* Lightweight syntax highlighting for JSON, YAML, HTML, CSS, and JavaScript
|
|
3
3
|
* Returns HTML string with syntax highlighting spans
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import { highlightCSS } from "./highlightCSS";
|
|
7
|
+
import { highlightJavaScript } from "./highlightJavaScript";
|
|
8
|
+
import { highlightHTML } from "./highlightHTML";
|
|
9
|
+
|
|
10
|
+
export type HighlightFormat = "json" | "yaml" | "text" | "markdown" | "html" | "css" | "javascript";
|
|
7
11
|
|
|
8
12
|
export interface HighlightOptions {
|
|
9
13
|
format: HighlightFormat;
|
|
@@ -25,7 +29,7 @@ function escapeHtml(text: string): string {
|
|
|
25
29
|
* Highlight JSON syntax by tokenizing first, then applying highlights
|
|
26
30
|
* This prevents issues with regex replacing content inside already-matched strings
|
|
27
31
|
*/
|
|
28
|
-
|
|
32
|
+
function highlightJSON(code: string): string {
|
|
29
33
|
if (!code) return "";
|
|
30
34
|
|
|
31
35
|
const result: string[] = [];
|
|
@@ -156,7 +160,7 @@ function getIndentLevel(line: string): number {
|
|
|
156
160
|
/**
|
|
157
161
|
* Highlight YAML syntax with multi-line string support
|
|
158
162
|
*/
|
|
159
|
-
|
|
163
|
+
function highlightYAML(code: string): string {
|
|
160
164
|
if (!code) return "";
|
|
161
165
|
|
|
162
166
|
const lines = code.split("\n");
|
|
@@ -320,7 +324,14 @@ export function highlightSyntax(code: string, options: HighlightOptions): string
|
|
|
320
324
|
return highlightJSON(code);
|
|
321
325
|
case "yaml":
|
|
322
326
|
return highlightYAML(code);
|
|
327
|
+
case "html":
|
|
328
|
+
return highlightHTML(code);
|
|
329
|
+
case "css":
|
|
330
|
+
return highlightCSS(code);
|
|
331
|
+
case "javascript":
|
|
332
|
+
return highlightJavaScript(code);
|
|
323
333
|
case "text":
|
|
334
|
+
case "markdown":
|
|
324
335
|
default:
|
|
325
336
|
return escapeHtml(code);
|
|
326
337
|
}
|
|
@@ -41,6 +41,19 @@ function processInlineContent(element: Element): string {
|
|
|
41
41
|
} else if (child.nodeType === Node.ELEMENT_NODE) {
|
|
42
42
|
const el = child as Element;
|
|
43
43
|
const tagName = el.tagName.toLowerCase();
|
|
44
|
+
|
|
45
|
+
// Handle color-preview spans - skip the swatch, return only the hex code text
|
|
46
|
+
if (tagName === "span" && el.classList.contains("color-preview")) {
|
|
47
|
+
// Get text content directly, skipping the color-swatch span
|
|
48
|
+
parts.push(el.textContent || "");
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Skip color-swatch spans entirely (they're purely decorative)
|
|
53
|
+
if (tagName === "span" && el.classList.contains("color-swatch")) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
44
57
|
const content = processInlineContent(el);
|
|
45
58
|
|
|
46
59
|
// Skip empty formatting elements
|
|
@@ -364,11 +377,36 @@ function processNode(node: Node): string {
|
|
|
364
377
|
break;
|
|
365
378
|
}
|
|
366
379
|
|
|
367
|
-
// Divs
|
|
368
|
-
case "div":
|
|
369
|
-
|
|
370
|
-
|
|
380
|
+
// Divs - check for code block wrapper first
|
|
381
|
+
case "div": {
|
|
382
|
+
// Handle code block wrapper structure
|
|
383
|
+
if (element.hasAttribute("data-code-block-id")) {
|
|
384
|
+
const mountPoint = element.querySelector(".code-viewer-mount-point");
|
|
385
|
+
const content = mountPoint?.getAttribute("data-content") || "";
|
|
386
|
+
const language = mountPoint?.getAttribute("data-language") || "";
|
|
387
|
+
parts.push(`\`\`\`${language}\n${content}\n\`\`\`\n\n`);
|
|
388
|
+
} else {
|
|
389
|
+
parts.push(processNode(element));
|
|
390
|
+
}
|
|
371
391
|
break;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Spans - handle special cases first
|
|
395
|
+
case "span": {
|
|
396
|
+
// Color preview: return only the hex color text
|
|
397
|
+
if (element.classList.contains("color-preview")) {
|
|
398
|
+
parts.push(element.textContent || "");
|
|
399
|
+
}
|
|
400
|
+
// Color swatch: skip entirely (purely decorative)
|
|
401
|
+
else if (element.classList.contains("color-swatch")) {
|
|
402
|
+
// Skip - don't output anything
|
|
403
|
+
}
|
|
404
|
+
// Default: process children
|
|
405
|
+
else {
|
|
406
|
+
parts.push(processNode(element));
|
|
407
|
+
}
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
372
410
|
|
|
373
411
|
// Tables
|
|
374
412
|
case "table":
|
|
@@ -204,8 +204,10 @@ describe('linePatterns', () => {
|
|
|
204
204
|
});
|
|
205
205
|
|
|
206
206
|
describe('detectCodeFenceStart', () => {
|
|
207
|
-
it('
|
|
208
|
-
|
|
207
|
+
it('returns null for code fence without language (requires language identifier)', () => {
|
|
208
|
+
// Implementation intentionally requires at least one character in language identifier
|
|
209
|
+
// to avoid triggering on just "```" before user finishes typing the language
|
|
210
|
+
expect(detectCodeFenceStart('```')).toBeNull();
|
|
209
211
|
});
|
|
210
212
|
|
|
211
213
|
it('detects code fence with javascript language', () => {
|
|
@@ -426,8 +428,9 @@ describe('linePatterns', () => {
|
|
|
426
428
|
});
|
|
427
429
|
|
|
428
430
|
describe('code block patterns', () => {
|
|
429
|
-
it('
|
|
430
|
-
|
|
431
|
+
it('returns null for code block without language (requires language identifier)', () => {
|
|
432
|
+
// Implementation intentionally requires language identifier
|
|
433
|
+
expect(detectLinePattern('```')).toBeNull();
|
|
431
434
|
});
|
|
432
435
|
|
|
433
436
|
it('detects code block with language', () => {
|
|
@@ -28,13 +28,26 @@ export function parseInline(text: string, sanitize: boolean = true): string {
|
|
|
28
28
|
// 2. HARD LINE BREAKS - Two trailing spaces + newline becomes <br />
|
|
29
29
|
result = result.replace(/ {2,}\n/g, "<br />\n");
|
|
30
30
|
|
|
31
|
-
// 3.
|
|
31
|
+
// 3. HEX COLOR PREVIEW - Display color swatch before hex color codes
|
|
32
|
+
// Match 6-digit hex colors first (more specific), then 3-digit
|
|
33
|
+
// Use word boundary to avoid matching inside URLs or other contexts
|
|
34
|
+
// The pattern must NOT match 4, 5, 7+ digit hex codes
|
|
35
|
+
result = result.replace(
|
|
36
|
+
/(?<![&\w])#([0-9a-fA-F]{6})(?![0-9a-fA-F])/g,
|
|
37
|
+
'<span class="color-preview"><span class="color-swatch" style="background-color: #$1"></span>#$1</span>'
|
|
38
|
+
);
|
|
39
|
+
result = result.replace(
|
|
40
|
+
/(?<![&\w])#([0-9a-fA-F]{3})(?![0-9a-fA-F])/g,
|
|
41
|
+
'<span class="color-preview"><span class="color-swatch" style="background-color: #$1"></span>#$1</span>'
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// 4. AUTOLINKS - Must be before regular link parsing
|
|
32
45
|
// URL autolinks: <https://example.com>
|
|
33
46
|
result = result.replace(/<(https?:\/\/[^&]+)>/g, '<a href="$1">$1</a>');
|
|
34
47
|
// Email autolinks: <user@example.com>
|
|
35
48
|
result = result.replace(/<([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>/g, '<a href="mailto:$1">$1</a>');
|
|
36
49
|
|
|
37
|
-
//
|
|
50
|
+
// 5. FOOTNOTE REFERENCES: [^id]
|
|
38
51
|
// Must be before regular link/image parsing to avoid conflicts
|
|
39
52
|
result = result.replace(/\[\^([^\]]+)\]/g, (match, fnId) => {
|
|
40
53
|
const fn = currentFootnotes[fnId];
|
|
@@ -44,19 +57,19 @@ export function parseInline(text: string, sanitize: boolean = true): string {
|
|
|
44
57
|
return match; // Keep original if footnote not defined
|
|
45
58
|
});
|
|
46
59
|
|
|
47
|
-
//
|
|
60
|
+
// 6. IMAGES:  - must be before links
|
|
48
61
|
result = result.replace(
|
|
49
62
|
/!\[([^\]]*)\]\(([^)]+)\)/g,
|
|
50
63
|
'<img src="$2" alt="$1" />'
|
|
51
64
|
);
|
|
52
65
|
|
|
53
|
-
//
|
|
66
|
+
// 7. INLINE LINKS: [text](url)
|
|
54
67
|
result = result.replace(
|
|
55
68
|
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
56
69
|
'<a href="$2">$1</a>'
|
|
57
70
|
);
|
|
58
71
|
|
|
59
|
-
//
|
|
72
|
+
// 8. REFERENCE-STYLE LINKS - Process after regular links
|
|
60
73
|
// Full reference: [text][ref-id]
|
|
61
74
|
result = result.replace(/\[([^\]]+)\]\[([^\]]+)\]/g, (match, text, refId) => {
|
|
62
75
|
const ref = currentLinkRefs[refId.toLowerCase()];
|
|
@@ -88,30 +101,30 @@ export function parseInline(text: string, sanitize: boolean = true): string {
|
|
|
88
101
|
return match;
|
|
89
102
|
});
|
|
90
103
|
|
|
91
|
-
//
|
|
104
|
+
// 9. INLINE CODE: `code`
|
|
92
105
|
result = result.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
93
106
|
|
|
94
|
-
//
|
|
107
|
+
// 10. STRIKETHROUGH: ~~text~~ - Must be before subscript (single tilde)
|
|
95
108
|
result = result.replace(/~~([^~]+)~~/g, "<del>$1</del>");
|
|
96
109
|
|
|
97
|
-
//
|
|
110
|
+
// 11. HIGHLIGHT: ==text==
|
|
98
111
|
result = result.replace(/==([^=]+)==/g, "<mark>$1</mark>");
|
|
99
112
|
|
|
100
|
-
//
|
|
113
|
+
// 12. SUPERSCRIPT: X^2^ - Must be before subscript
|
|
101
114
|
result = result.replace(/\^([^\^]+)\^/g, "<sup>$1</sup>");
|
|
102
115
|
|
|
103
|
-
//
|
|
116
|
+
// 13. SUBSCRIPT: H~2~O - Single tilde, use negative lookbehind/lookahead to avoid ~~
|
|
104
117
|
result = result.replace(/(?<!~)~([^~]+)~(?!~)/g, "<sub>$1</sub>");
|
|
105
118
|
|
|
106
|
-
//
|
|
119
|
+
// 14. BOLD + ITALIC: ***text*** or ___text___
|
|
107
120
|
result = result.replace(/\*\*\*([^*]+)\*\*\*/g, "<strong><em>$1</em></strong>");
|
|
108
121
|
result = result.replace(/___([^_]+)___/g, "<strong><em>$1</em></strong>");
|
|
109
122
|
|
|
110
|
-
//
|
|
123
|
+
// 15. BOLD: **text** or __text__
|
|
111
124
|
result = result.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
112
125
|
result = result.replace(/__([^_]+)__/g, "<strong>$1</strong>");
|
|
113
126
|
|
|
114
|
-
//
|
|
127
|
+
// 16. ITALIC: *text* or _text_ (but not inside words for underscores)
|
|
115
128
|
// For asterisks, match any single asterisk pairs
|
|
116
129
|
result = result.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
117
130
|
// For underscores, only match at word boundaries
|
package/src/helpers/routes.ts
CHANGED
|
@@ -40,7 +40,9 @@ export function useActionRoutes(baseUrl: string, extend?: object): ListControlsR
|
|
|
40
40
|
...options,
|
|
41
41
|
ignoreAbort: true
|
|
42
42
|
};
|
|
43
|
-
|
|
43
|
+
if (fields) {
|
|
44
|
+
options.params = { ...options.params, fields };
|
|
45
|
+
}
|
|
44
46
|
const item = await request.get(`${baseUrl}/${target.id}/details`, options);
|
|
45
47
|
return storeObject(item);
|
|
46
48
|
},
|
package/src/styles/danx.scss
CHANGED
package/src/styles/index.scss
CHANGED