quasar-ui-danx 0.5.0 → 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.
- package/.claude/settings.local.json +8 -0
- package/dist/danx.es.js +16119 -10641
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +202 -123
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +8 -1
- package/src/components/Utility/Buttons/ActionButton.vue +15 -5
- package/src/components/Utility/Code/CodeViewer.vue +41 -16
- package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
- package/src/components/Utility/Code/CodeViewerFooter.vue +3 -1
- package/src/components/Utility/Code/LanguageBadge.vue +278 -5
- package/src/components/Utility/Code/MarkdownContent.vue +31 -163
- package/src/components/Utility/Code/index.ts +3 -0
- package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
- package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
- package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
- package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
- package/src/components/Utility/Markdown/MarkdownEditor.vue +233 -0
- package/src/components/Utility/Markdown/MarkdownEditorContent.vue +296 -0
- package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
- package/src/components/Utility/Markdown/TablePopover.vue +420 -0
- package/src/components/Utility/Markdown/index.ts +11 -0
- package/src/components/Utility/Markdown/types.ts +27 -0
- package/src/components/Utility/Widgets/LabelPillWidget.vue +20 -0
- package/src/components/Utility/index.ts +1 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
- package/src/composables/markdown/features/useBlockquotes.ts +248 -0
- package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
- package/src/composables/markdown/features/useCodeBlocks.spec.ts +805 -0
- package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
- package/src/composables/markdown/features/useContextMenu.ts +444 -0
- package/src/composables/markdown/features/useFocusTracking.ts +116 -0
- package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
- package/src/composables/markdown/features/useHeadings.ts +290 -0
- package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
- package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
- package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
- package/src/composables/markdown/features/useLinks.spec.ts +388 -0
- package/src/composables/markdown/features/useLinks.ts +374 -0
- package/src/composables/markdown/features/useLists.spec.ts +834 -0
- package/src/composables/markdown/features/useLists.ts +747 -0
- package/src/composables/markdown/features/usePopoverManager.ts +181 -0
- package/src/composables/markdown/features/useTables.spec.ts +1601 -0
- package/src/composables/markdown/features/useTables.ts +1107 -0
- package/src/composables/markdown/index.ts +16 -0
- package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
- package/src/composables/markdown/useMarkdownEditor.ts +1077 -0
- package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
- package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
- package/src/composables/markdown/useMarkdownSelection.ts +219 -0
- package/src/composables/markdown/useMarkdownSync.ts +549 -0
- package/src/composables/useCodeFormat.ts +17 -10
- package/src/composables/useCodeViewerEditor.spec.ts +655 -0
- package/src/composables/useCodeViewerEditor.ts +174 -20
- 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/convertHeadings.ts +41 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +425 -0
- package/src/helpers/formats/markdown/index.ts +7 -0
- package/src/helpers/formats/markdown/linePatterns.spec.ts +498 -0
- package/src/helpers/formats/markdown/linePatterns.ts +172 -0
- 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 +59 -0
- package/src/test/helpers/editorTestUtils.spec.ts +296 -0
- package/src/test/helpers/editorTestUtils.ts +253 -0
- package/src/test/helpers/index.ts +1 -0
- package/src/test/highlighters.test.ts +153 -0
- package/src/test/setup.test.ts +12 -0
- package/src/test/setup.ts +12 -0
- package/src/types/widgets.d.ts +2 -2
- package/vite.config.js +5 -1
- package/vitest.config.ts +19 -0
|
@@ -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, "&")
|
|
16
|
+
.replace(/</g, "<")
|
|
17
|
+
.replace(/>/g, ">")
|
|
18
|
+
.replace(/"/g, """)
|
|
19
|
+
.replace(/'/g, "'");
|
|
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">/></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">></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">/></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">/></span>`);
|
|
381
|
+
i += 2;
|
|
382
|
+
} else {
|
|
383
|
+
result.push(`<span class="syntax-tag">></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
|
+
}
|