quasar-ui-danx 0.5.1 → 0.5.2

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/.claude/settings.local.json +8 -0
  2. package/dist/danx.es.js +11359 -10497
  3. package/dist/danx.es.js.map +1 -1
  4. package/dist/danx.umd.js +138 -131
  5. package/dist/danx.umd.js.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/package.json +1 -1
  8. package/src/components/Utility/Buttons/ActionButton.vue +15 -5
  9. package/src/components/Utility/Code/CodeViewer.vue +10 -2
  10. package/src/components/Utility/Code/CodeViewerFooter.vue +2 -0
  11. package/src/components/Utility/Code/MarkdownContent.vue +31 -163
  12. package/src/components/Utility/Markdown/MarkdownEditor.vue +7 -2
  13. package/src/components/Utility/Markdown/MarkdownEditorContent.vue +69 -8
  14. package/src/components/Utility/Widgets/LabelPillWidget.vue +20 -0
  15. package/src/composables/markdown/features/useCodeBlocks.spec.ts +59 -33
  16. package/src/composables/markdown/features/useLinks.spec.ts +29 -10
  17. package/src/composables/markdown/useMarkdownEditor.ts +16 -7
  18. package/src/composables/useCodeFormat.ts +17 -10
  19. package/src/helpers/formats/highlightCSS.ts +236 -0
  20. package/src/helpers/formats/highlightHTML.ts +483 -0
  21. package/src/helpers/formats/highlightJavaScript.ts +346 -0
  22. package/src/helpers/formats/highlightSyntax.ts +15 -4
  23. package/src/helpers/formats/index.ts +3 -0
  24. package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +15 -2
  25. package/src/helpers/formats/markdown/linePatterns.spec.ts +7 -4
  26. package/src/styles/danx.scss +3 -3
  27. package/src/styles/index.scss +5 -5
  28. package/src/styles/themes/danx/code.scss +257 -1
  29. package/src/styles/themes/danx/index.scss +10 -10
  30. package/src/styles/themes/danx/markdown.scss +59 -0
  31. package/src/test/highlighters.test.ts +153 -0
  32. package/src/types/widgets.d.ts +2 -2
  33. 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, "&lt;")
14
+ .replace(/>/g, "&gt;")
15
+ .replace(/"/g, "&quot;")
16
+ .replace(/'/g, "&#039;");
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 YAML
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
- export type HighlightFormat = "json" | "yaml" | "text" | "markdown";
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
- export function highlightJSON(code: string): string {
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
- export function highlightYAML(code: string): string {
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
  }
@@ -1,5 +1,8 @@
1
1
  export * from "./datetime";
2
2
  export * from "./highlightSyntax";
3
+ export * from "./highlightCSS";
4
+ export * from "./highlightJavaScript";
5
+ export * from "./highlightHTML";
3
6
  export * from "./numbers";
4
7
  export * from "./parsers";
5
8
  export * from "./markdown";
@@ -364,8 +364,21 @@ function processNode(node: Node): string {
364
364
  break;
365
365
  }
366
366
 
367
- // Divs and other containers - just process children
368
- case "div":
367
+ // Divs - check for code block wrapper first
368
+ case "div": {
369
+ // Handle code block wrapper structure
370
+ if (element.hasAttribute("data-code-block-id")) {
371
+ const mountPoint = element.querySelector(".code-viewer-mount-point");
372
+ const content = mountPoint?.getAttribute("data-content") || "";
373
+ const language = mountPoint?.getAttribute("data-language") || "";
374
+ parts.push(`\`\`\`${language}\n${content}\n\`\`\`\n\n`);
375
+ } else {
376
+ parts.push(processNode(element));
377
+ }
378
+ break;
379
+ }
380
+
381
+ // Spans - just process children
369
382
  case "span":
370
383
  parts.push(processNode(element));
371
384
  break;
@@ -204,8 +204,10 @@ describe('linePatterns', () => {
204
204
  });
205
205
 
206
206
  describe('detectCodeFenceStart', () => {
207
- it('detects code fence without language', () => {
208
- expect(detectCodeFenceStart('```')).toEqual({ language: '' });
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('detects code block without language', () => {
430
- expect(detectLinePattern('```')).toEqual({ type: 'code-block', language: '' });
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', () => {
@@ -1,6 +1,6 @@
1
- @import "themes/danx/scrollbar";
2
- @import "themes/danx/code";
3
- @import "themes/danx/markdown";
1
+ @use "themes/danx/scrollbar";
2
+ @use "themes/danx/code";
3
+ @use "themes/danx/markdown";
4
4
 
5
5
  .dx-action-table {
6
6
  .dx-column-shrink {
@@ -1,8 +1,8 @@
1
+ @use "quasar-reset";
2
+ @use "general";
3
+ @use "transitions";
4
+ @use "danx";
5
+
1
6
  @tailwind base;
2
7
  @tailwind components;
3
8
  @tailwind utilities;
4
-
5
- @import "quasar-reset";
6
- @import "general";
7
- @import "transitions";
8
- @import "danx";