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.
Files changed (38) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/dist/danx.es.js +13869 -12976
  3. package/dist/danx.es.js.map +1 -1
  4. package/dist/danx.umd.js +159 -151
  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/Files/FilePreview.vue +2 -2
  13. package/src/components/Utility/Markdown/MarkdownEditor.vue +7 -2
  14. package/src/components/Utility/Markdown/MarkdownEditorContent.vue +69 -8
  15. package/src/components/Utility/Widgets/LabelPillWidget.vue +20 -0
  16. package/src/composables/markdown/features/useCodeBlocks.spec.ts +59 -33
  17. package/src/composables/markdown/features/useLinks.spec.ts +29 -10
  18. package/src/composables/markdown/useMarkdownEditor.ts +16 -7
  19. package/src/composables/useCodeFormat.ts +17 -10
  20. package/src/composables/useCodeViewerCollapse.ts +7 -0
  21. package/src/composables/useFileNavigation.ts +5 -1
  22. package/src/helpers/formats/highlightCSS.ts +236 -0
  23. package/src/helpers/formats/highlightHTML.ts +483 -0
  24. package/src/helpers/formats/highlightJavaScript.ts +346 -0
  25. package/src/helpers/formats/highlightSyntax.ts +15 -4
  26. package/src/helpers/formats/index.ts +3 -0
  27. package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +42 -4
  28. package/src/helpers/formats/markdown/linePatterns.spec.ts +7 -4
  29. package/src/helpers/formats/markdown/parseInline.ts +26 -13
  30. package/src/helpers/routes.ts +3 -1
  31. package/src/styles/danx.scss +3 -3
  32. package/src/styles/index.scss +5 -5
  33. package/src/styles/themes/danx/code.scss +257 -1
  34. package/src/styles/themes/danx/index.scss +10 -10
  35. package/src/styles/themes/danx/markdown.scss +81 -0
  36. package/src/test/highlighters.test.ts +153 -0
  37. package/src/types/widgets.d.ts +2 -2
  38. package/vite.config.js +5 -1
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Lightweight syntax highlighting for CSS
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
+ * CSS parsing context states
21
+ */
22
+ type CSSContext = "selector" | "property" | "value" | "at-rule";
23
+
24
+ /**
25
+ * Highlight CSS syntax by tokenizing character-by-character
26
+ * This prevents issues with regex replacing content inside already-matched strings
27
+ */
28
+ export function highlightCSS(code: string): string {
29
+ if (!code) return "";
30
+
31
+ const result: string[] = [];
32
+ let i = 0;
33
+ let context: CSSContext = "selector";
34
+ let buffer = "";
35
+ // Track brace depth for nested blocks (e.g., @media)
36
+ let braceDepth = 0;
37
+ // Track if we're inside an at-rule name
38
+ let inAtRuleName = false;
39
+
40
+ /**
41
+ * Flush the current buffer with appropriate highlighting
42
+ */
43
+ function flushBuffer(): void {
44
+ if (!buffer) return;
45
+
46
+ const trimmed = buffer.trim();
47
+ if (!trimmed) {
48
+ // Whitespace only - just escape and add
49
+ result.push(escapeHtml(buffer));
50
+ buffer = "";
51
+ return;
52
+ }
53
+
54
+ // Determine what type of content this is based on context
55
+ switch (context) {
56
+ case "selector":
57
+ result.push(`<span class="syntax-selector">${escapeHtml(buffer)}</span>`);
58
+ break;
59
+ case "property":
60
+ result.push(`<span class="syntax-property">${escapeHtml(buffer)}</span>`);
61
+ break;
62
+ case "value":
63
+ result.push(`<span class="syntax-value">${escapeHtml(buffer)}</span>`);
64
+ break;
65
+ case "at-rule":
66
+ result.push(`<span class="syntax-at-rule">${escapeHtml(buffer)}</span>`);
67
+ break;
68
+ }
69
+ buffer = "";
70
+ }
71
+
72
+ while (i < code.length) {
73
+ const char = code[i];
74
+
75
+ // Handle comments: /* ... */
76
+ if (char === "/" && code[i + 1] === "*") {
77
+ flushBuffer();
78
+ const startIndex = i;
79
+ i += 2; // Skip /*
80
+
81
+ // Find closing */
82
+ while (i < code.length) {
83
+ if (code[i] === "*" && code[i + 1] === "/") {
84
+ i += 2; // Include */
85
+ break;
86
+ }
87
+ i++;
88
+ }
89
+
90
+ const comment = code.slice(startIndex, i);
91
+ result.push(`<span class="syntax-comment">${escapeHtml(comment)}</span>`);
92
+ continue;
93
+ }
94
+
95
+ // Handle strings (single or double quoted)
96
+ if (char === '"' || char === "'") {
97
+ flushBuffer();
98
+ const quoteChar = char;
99
+ const startIndex = i;
100
+ i++; // Skip opening quote
101
+
102
+ // Find closing quote, handling escape sequences
103
+ while (i < code.length) {
104
+ if (code[i] === "\\" && i + 1 < code.length) {
105
+ i += 2; // Skip escaped character
106
+ } else if (code[i] === quoteChar) {
107
+ i++; // Include closing quote
108
+ break;
109
+ } else {
110
+ i++;
111
+ }
112
+ }
113
+
114
+ const str = code.slice(startIndex, i);
115
+ result.push(`<span class="syntax-string">${escapeHtml(str)}</span>`);
116
+ continue;
117
+ }
118
+
119
+ // Handle at-rules: @media, @import, @keyframes, etc.
120
+ if (char === "@") {
121
+ flushBuffer();
122
+ buffer = "@";
123
+ i++;
124
+ inAtRuleName = true;
125
+ context = "at-rule";
126
+ continue;
127
+ }
128
+
129
+ // If we're building an at-rule name, continue until whitespace or {
130
+ if (inAtRuleName) {
131
+ if (/\s/.test(char) || char === "{" || char === ";") {
132
+ flushBuffer();
133
+ inAtRuleName = false;
134
+ // Don't increment i, let the character be processed normally
135
+ // After at-rule name, we're in selector context (for params) until { or ;
136
+ context = "selector";
137
+ } else {
138
+ buffer += char;
139
+ i++;
140
+ continue;
141
+ }
142
+ }
143
+
144
+ // Handle opening brace
145
+ if (char === "{") {
146
+ flushBuffer();
147
+ result.push(`<span class="syntax-punctuation">${escapeHtml(char)}</span>`);
148
+ braceDepth++;
149
+ // After {, we're in property context
150
+ context = "property";
151
+ i++;
152
+ continue;
153
+ }
154
+
155
+ // Handle closing brace
156
+ if (char === "}") {
157
+ flushBuffer();
158
+ result.push(`<span class="syntax-punctuation">${escapeHtml(char)}</span>`);
159
+ braceDepth--;
160
+ // After }, we're back to selector context
161
+ context = "selector";
162
+ i++;
163
+ continue;
164
+ }
165
+
166
+ // Handle colon (property: value separator)
167
+ if (char === ":") {
168
+ // Check if this is a pseudo-selector (::before, :hover, etc.)
169
+ // A colon is a pseudo-selector if we're in selector context
170
+ if (context === "selector") {
171
+ // This is part of a selector (pseudo-class/element)
172
+ buffer += char;
173
+ i++;
174
+ continue;
175
+ }
176
+ // Otherwise it's a property-value separator
177
+ flushBuffer();
178
+ result.push(`<span class="syntax-punctuation">${escapeHtml(char)}</span>`);
179
+ // After :, we're in value context
180
+ context = "value";
181
+ i++;
182
+ continue;
183
+ }
184
+
185
+ // Handle semicolon (declaration terminator)
186
+ if (char === ";") {
187
+ flushBuffer();
188
+ result.push(`<span class="syntax-punctuation">${escapeHtml(char)}</span>`);
189
+ // After ;, we're back to property context (still inside braces)
190
+ if (braceDepth > 0) {
191
+ context = "property";
192
+ } else {
193
+ context = "selector";
194
+ }
195
+ i++;
196
+ continue;
197
+ }
198
+
199
+ // Handle comma
200
+ if (char === ",") {
201
+ flushBuffer();
202
+ result.push(`<span class="syntax-punctuation">${escapeHtml(char)}</span>`);
203
+ i++;
204
+ continue;
205
+ }
206
+
207
+ // Handle parentheses (for functions like url(), rgb(), etc.)
208
+ if (char === "(" || char === ")") {
209
+ flushBuffer();
210
+ result.push(`<span class="syntax-punctuation">${escapeHtml(char)}</span>`);
211
+ i++;
212
+ continue;
213
+ }
214
+
215
+ // Handle whitespace
216
+ if (/\s/.test(char)) {
217
+ // If buffer has content, flush it first
218
+ if (buffer.trim()) {
219
+ flushBuffer();
220
+ }
221
+ // Add whitespace directly
222
+ result.push(escapeHtml(char));
223
+ i++;
224
+ continue;
225
+ }
226
+
227
+ // Accumulate regular characters into buffer
228
+ buffer += char;
229
+ i++;
230
+ }
231
+
232
+ // Flush any remaining buffer
233
+ flushBuffer();
234
+
235
+ return result.join("");
236
+ }
@@ -0,0 +1,483 @@
1
+ /**
2
+ * Lightweight syntax highlighting for HTML
3
+ * Returns HTML string with syntax highlighting spans
4
+ * Supports embedded CSS (<style>) and JavaScript (<script>) highlighting
5
+ */
6
+
7
+ import { highlightCSS } from "./highlightCSS";
8
+ import { highlightJavaScript } from "./highlightJavaScript";
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
+ * HTML parsing states
24
+ */
25
+ type HTMLState = "text" | "tag-open" | "tag-name" | "attribute-name" | "attribute-equals" | "attribute-value" | "tag-close" | "comment" | "doctype";
26
+
27
+ /**
28
+ * Highlight HTML syntax by tokenizing character-by-character
29
+ * Delegates to CSS and JavaScript highlighters for embedded content
30
+ */
31
+ export function highlightHTML(code: string): string {
32
+ if (!code) return "";
33
+
34
+ const result: string[] = [];
35
+ let i = 0;
36
+ let state: HTMLState = "text";
37
+ let buffer = "";
38
+ let currentTagName = "";
39
+ let inClosingTag = false;
40
+ let quoteChar = "";
41
+
42
+ /**
43
+ * Flush the current buffer with appropriate highlighting
44
+ */
45
+ function flushBuffer(className?: string): void {
46
+ if (!buffer) return;
47
+
48
+ if (className) {
49
+ result.push(`<span class="${className}">${escapeHtml(buffer)}</span>`);
50
+ } else {
51
+ result.push(escapeHtml(buffer));
52
+ }
53
+ buffer = "";
54
+ }
55
+
56
+ /**
57
+ * Find the closing tag for style or script
58
+ * Returns the index of the closing tag or -1 if not found
59
+ */
60
+ function findClosingTag(tagName: string, startIndex: number): number {
61
+ const closePattern = new RegExp(`<\\s*/\\s*${tagName}\\s*>`, "i");
62
+ const remaining = code.slice(startIndex);
63
+ const match = remaining.match(closePattern);
64
+ if (match && match.index !== undefined) {
65
+ return startIndex + match.index;
66
+ }
67
+ return -1;
68
+ }
69
+
70
+ while (i < code.length) {
71
+ const char = code[i];
72
+
73
+ // Handle comments: <!-- ... -->
74
+ if (state === "text" && code.slice(i, i + 4) === "<!--") {
75
+ flushBuffer();
76
+ const startIndex = i;
77
+ i += 4; // Skip <!--
78
+
79
+ // Find closing -->
80
+ while (i < code.length) {
81
+ if (code.slice(i, i + 3) === "-->") {
82
+ i += 3; // Include -->
83
+ break;
84
+ }
85
+ i++;
86
+ }
87
+
88
+ const comment = code.slice(startIndex, i);
89
+ result.push(`<span class="syntax-comment">${escapeHtml(comment)}</span>`);
90
+ continue;
91
+ }
92
+
93
+ // Handle DOCTYPE: <!DOCTYPE ...>
94
+ if (state === "text" && code.slice(i, i + 9).toUpperCase() === "<!DOCTYPE") {
95
+ flushBuffer();
96
+ const startIndex = i;
97
+
98
+ // Find closing >
99
+ while (i < code.length && code[i] !== ">") {
100
+ i++;
101
+ }
102
+ if (code[i] === ">") i++; // Include >
103
+
104
+ const doctype = code.slice(startIndex, i);
105
+ result.push(`<span class="syntax-doctype">${escapeHtml(doctype)}</span>`);
106
+ continue;
107
+ }
108
+
109
+ // Handle CDATA sections: <![CDATA[ ... ]]>
110
+ if (state === "text" && code.slice(i, i + 9) === "<![CDATA[") {
111
+ flushBuffer();
112
+ const startIndex = i;
113
+ i += 9; // Skip <![CDATA[
114
+
115
+ // Find closing ]]>
116
+ while (i < code.length) {
117
+ if (code.slice(i, i + 3) === "]]>") {
118
+ i += 3; // Include ]]>
119
+ break;
120
+ }
121
+ i++;
122
+ }
123
+
124
+ const cdata = code.slice(startIndex, i);
125
+ result.push(`<span class="syntax-comment">${escapeHtml(cdata)}</span>`);
126
+ continue;
127
+ }
128
+
129
+ // Handle tag opening: <
130
+ if (state === "text" && char === "<") {
131
+ flushBuffer();
132
+ buffer = "<";
133
+ i++;
134
+
135
+ // Check for closing tag
136
+ if (code[i] === "/") {
137
+ inClosingTag = true;
138
+ buffer += "/";
139
+ i++;
140
+ } else {
141
+ inClosingTag = false;
142
+ }
143
+
144
+ state = "tag-name";
145
+ currentTagName = "";
146
+ continue;
147
+ }
148
+
149
+ // Handle tag name
150
+ if (state === "tag-name") {
151
+ if (/[a-zA-Z0-9-]/.test(char)) {
152
+ buffer += char;
153
+ currentTagName += char.toLowerCase();
154
+ i++;
155
+ continue;
156
+ }
157
+
158
+ // Tag name complete
159
+ flushBuffer("syntax-tag");
160
+
161
+ // Check for style or script tags (only opening tags)
162
+ if (!inClosingTag && (currentTagName === "style" || currentTagName === "script")) {
163
+ // Find the end of the opening tag
164
+ let tagEndIndex = i;
165
+ while (tagEndIndex < code.length && code[tagEndIndex] !== ">") {
166
+ tagEndIndex++;
167
+ }
168
+ tagEndIndex++; // Include >
169
+
170
+ // Highlight the rest of the opening tag (attributes)
171
+ const tagRemainder = code.slice(i, tagEndIndex);
172
+ result.push(highlightHTMLAttributes(tagRemainder));
173
+ i = tagEndIndex;
174
+
175
+ // Find the closing tag
176
+ const closingTagIndex = findClosingTag(currentTagName, i);
177
+
178
+ if (closingTagIndex !== -1) {
179
+ // Extract and highlight the embedded content
180
+ const embeddedContent = code.slice(i, closingTagIndex);
181
+
182
+ if (currentTagName === "style") {
183
+ result.push(highlightCSS(embeddedContent));
184
+ } else if (currentTagName === "script") {
185
+ result.push(highlightJavaScript(embeddedContent));
186
+ }
187
+
188
+ i = closingTagIndex;
189
+ }
190
+
191
+ state = "text";
192
+ continue;
193
+ }
194
+
195
+ if (/\s/.test(char)) {
196
+ result.push(escapeHtml(char));
197
+ i++;
198
+ state = "attribute-name";
199
+ } else if (char === ">") {
200
+ result.push(`<span class="syntax-tag">${escapeHtml(char)}</span>`);
201
+ i++;
202
+ state = "text";
203
+ } else if (char === "/" && code[i + 1] === ">") {
204
+ result.push(`<span class="syntax-tag">/&gt;</span>`);
205
+ i += 2;
206
+ state = "text";
207
+ } else {
208
+ state = "attribute-name";
209
+ }
210
+ continue;
211
+ }
212
+
213
+ // Handle attribute name
214
+ if (state === "attribute-name") {
215
+ if (/\s/.test(char)) {
216
+ flushBuffer("syntax-attribute");
217
+ result.push(escapeHtml(char));
218
+ i++;
219
+ continue;
220
+ }
221
+
222
+ if (char === "=") {
223
+ flushBuffer("syntax-attribute");
224
+ result.push(`<span class="syntax-punctuation">=</span>`);
225
+ i++;
226
+ state = "attribute-equals";
227
+ continue;
228
+ }
229
+
230
+ if (char === ">") {
231
+ flushBuffer("syntax-attribute");
232
+ result.push(`<span class="syntax-tag">&gt;</span>`);
233
+ i++;
234
+ state = "text";
235
+ continue;
236
+ }
237
+
238
+ if (char === "/" && code[i + 1] === ">") {
239
+ flushBuffer("syntax-attribute");
240
+ result.push(`<span class="syntax-tag">/&gt;</span>`);
241
+ i += 2;
242
+ state = "text";
243
+ continue;
244
+ }
245
+
246
+ if (/[a-zA-Z0-9\-_:@.]/.test(char)) {
247
+ buffer += char;
248
+ i++;
249
+ continue;
250
+ }
251
+
252
+ // Unknown character in attribute context
253
+ flushBuffer("syntax-attribute");
254
+ result.push(escapeHtml(char));
255
+ i++;
256
+ continue;
257
+ }
258
+
259
+ // Handle after equals sign (before attribute value)
260
+ if (state === "attribute-equals") {
261
+ if (/\s/.test(char)) {
262
+ result.push(escapeHtml(char));
263
+ i++;
264
+ continue;
265
+ }
266
+
267
+ if (char === '"' || char === "'") {
268
+ quoteChar = char;
269
+ buffer = char;
270
+ i++;
271
+ state = "attribute-value";
272
+ continue;
273
+ }
274
+
275
+ // Unquoted attribute value
276
+ if (char !== ">" && char !== "/") {
277
+ buffer = "";
278
+ state = "attribute-value";
279
+ quoteChar = "";
280
+ continue;
281
+ }
282
+
283
+ // No value, go back to attribute name state
284
+ state = "attribute-name";
285
+ continue;
286
+ }
287
+
288
+ // Handle attribute value
289
+ if (state === "attribute-value") {
290
+ if (quoteChar) {
291
+ // Quoted attribute value
292
+ buffer += char;
293
+ i++;
294
+
295
+ if (char === quoteChar) {
296
+ flushBuffer("syntax-string");
297
+ state = "attribute-name";
298
+ quoteChar = "";
299
+ }
300
+ continue;
301
+ } else {
302
+ // Unquoted attribute value
303
+ if (/\s/.test(char) || char === ">" || (char === "/" && code[i + 1] === ">")) {
304
+ flushBuffer("syntax-string");
305
+ state = "attribute-name";
306
+ continue;
307
+ }
308
+
309
+ buffer += char;
310
+ i++;
311
+ continue;
312
+ }
313
+ }
314
+
315
+ // Default text handling
316
+ if (state === "text") {
317
+ if (char === "<") {
318
+ // Will be handled at the top of the loop
319
+ continue;
320
+ }
321
+
322
+ buffer += char;
323
+ i++;
324
+ } else {
325
+ // Unknown state, just advance
326
+ buffer += char;
327
+ i++;
328
+ }
329
+ }
330
+
331
+ // Flush any remaining buffer
332
+ if (buffer) {
333
+ if (state === "text") {
334
+ flushBuffer();
335
+ } else if (state === "tag-name") {
336
+ flushBuffer("syntax-tag");
337
+ } else if (state === "attribute-name") {
338
+ flushBuffer("syntax-attribute");
339
+ } else if (state === "attribute-value") {
340
+ flushBuffer("syntax-string");
341
+ } else {
342
+ flushBuffer();
343
+ }
344
+ }
345
+
346
+ return result.join("");
347
+ }
348
+
349
+ /**
350
+ * Highlight HTML attributes (everything between tag name and >)
351
+ * This is a helper for processing the remainder of a tag after the name
352
+ */
353
+ function highlightHTMLAttributes(code: string): string {
354
+ if (!code) return "";
355
+
356
+ const result: string[] = [];
357
+ let i = 0;
358
+ let state: "space" | "attribute-name" | "equals" | "value" = "space";
359
+ let buffer = "";
360
+ let quoteChar = "";
361
+
362
+ while (i < code.length) {
363
+ const char = code[i];
364
+
365
+ // Handle end of tag
366
+ if (char === ">" || (char === "/" && code[i + 1] === ">")) {
367
+ // Flush buffer
368
+ if (buffer) {
369
+ if (state === "attribute-name") {
370
+ result.push(`<span class="syntax-attribute">${escapeHtml(buffer)}</span>`);
371
+ } else if (state === "value") {
372
+ result.push(`<span class="syntax-string">${escapeHtml(buffer)}</span>`);
373
+ } else {
374
+ result.push(escapeHtml(buffer));
375
+ }
376
+ buffer = "";
377
+ }
378
+
379
+ if (char === "/" && code[i + 1] === ">") {
380
+ result.push(`<span class="syntax-tag">/&gt;</span>`);
381
+ i += 2;
382
+ } else {
383
+ result.push(`<span class="syntax-tag">&gt;</span>`);
384
+ i++;
385
+ }
386
+ continue;
387
+ }
388
+
389
+ // Handle whitespace
390
+ if (/\s/.test(char) && state !== "value") {
391
+ if (buffer && state === "attribute-name") {
392
+ result.push(`<span class="syntax-attribute">${escapeHtml(buffer)}</span>`);
393
+ buffer = "";
394
+ }
395
+ result.push(escapeHtml(char));
396
+ i++;
397
+ state = "space";
398
+ continue;
399
+ }
400
+
401
+ // Handle equals sign
402
+ if (char === "=" && state !== "value") {
403
+ if (buffer) {
404
+ result.push(`<span class="syntax-attribute">${escapeHtml(buffer)}</span>`);
405
+ buffer = "";
406
+ }
407
+ result.push(`<span class="syntax-punctuation">=</span>`);
408
+ i++;
409
+ state = "equals";
410
+ continue;
411
+ }
412
+
413
+ // Handle quote start
414
+ if ((char === '"' || char === "'") && (state === "equals" || state === "space")) {
415
+ quoteChar = char;
416
+ buffer = char;
417
+ i++;
418
+ state = "value";
419
+ continue;
420
+ }
421
+
422
+ // Handle quoted value
423
+ if (state === "value" && quoteChar) {
424
+ buffer += char;
425
+ if (char === quoteChar) {
426
+ result.push(`<span class="syntax-string">${escapeHtml(buffer)}</span>`);
427
+ buffer = "";
428
+ quoteChar = "";
429
+ state = "space";
430
+ }
431
+ i++;
432
+ continue;
433
+ }
434
+
435
+ // Handle unquoted value
436
+ if (state === "value" && !quoteChar) {
437
+ if (/\s/.test(char)) {
438
+ result.push(`<span class="syntax-string">${escapeHtml(buffer)}</span>`);
439
+ buffer = "";
440
+ result.push(escapeHtml(char));
441
+ state = "space";
442
+ i++;
443
+ continue;
444
+ }
445
+ buffer += char;
446
+ i++;
447
+ continue;
448
+ }
449
+
450
+ // Attribute name
451
+ if (state === "space" || state === "attribute-name") {
452
+ buffer += char;
453
+ state = "attribute-name";
454
+ i++;
455
+ continue;
456
+ }
457
+
458
+ // Unquoted value after equals
459
+ if (state === "equals") {
460
+ buffer += char;
461
+ state = "value";
462
+ i++;
463
+ continue;
464
+ }
465
+
466
+ // Default
467
+ result.push(escapeHtml(char));
468
+ i++;
469
+ }
470
+
471
+ // Flush remaining buffer
472
+ if (buffer) {
473
+ if (state === "attribute-name") {
474
+ result.push(`<span class="syntax-attribute">${escapeHtml(buffer)}</span>`);
475
+ } else if (state === "value") {
476
+ result.push(`<span class="syntax-string">${escapeHtml(buffer)}</span>`);
477
+ } else {
478
+ result.push(escapeHtml(buffer));
479
+ }
480
+ }
481
+
482
+ return result.join("");
483
+ }