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,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, "<")
|
|
14
|
+
.replace(/>/g, ">")
|
|
15
|
+
.replace(/"/g, """)
|
|
16
|
+
.replace(/'/g, "'");
|
|
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, "&")
|
|
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
|
+
}
|