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
|
@@ -10,6 +10,14 @@ export interface UseCodeViewerEditorOptions {
|
|
|
10
10
|
editable: Ref<boolean>;
|
|
11
11
|
onEmitModelValue: (value: object | string | null) => void;
|
|
12
12
|
onEmitEditable: (editable: boolean) => void;
|
|
13
|
+
/** Callback when format changes (e.g., cycling languages) */
|
|
14
|
+
onEmitFormat?: (format: CodeFormat) => void;
|
|
15
|
+
/** Callback when user wants to exit the code block (Ctrl+Enter) */
|
|
16
|
+
onExit?: () => void;
|
|
17
|
+
/** Callback when user wants to delete the code block (Backspace/Delete on empty) */
|
|
18
|
+
onDelete?: () => void;
|
|
19
|
+
/** Callback when user wants to open the language search panel (Ctrl+Alt+Shift+L) */
|
|
20
|
+
onOpenLanguageSearch?: () => void;
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
export interface UseCodeViewerEditorReturn {
|
|
@@ -167,11 +175,27 @@ function getSmartIndent(lineInfo: { indent: string; lineContent: string }, forma
|
|
|
167
175
|
return indent;
|
|
168
176
|
}
|
|
169
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Get available formats that can be cycled through based on current format.
|
|
180
|
+
* YAML/JSON formats cycle between each other only.
|
|
181
|
+
* Text/Markdown formats cycle between each other only.
|
|
182
|
+
* Other formats don't cycle.
|
|
183
|
+
*/
|
|
184
|
+
function getAvailableFormats(format: CodeFormat): CodeFormat[] {
|
|
185
|
+
if (format === "json" || format === "yaml") {
|
|
186
|
+
return ["yaml", "json"];
|
|
187
|
+
}
|
|
188
|
+
if (format === "text" || format === "markdown") {
|
|
189
|
+
return ["text", "markdown"];
|
|
190
|
+
}
|
|
191
|
+
return [format];
|
|
192
|
+
}
|
|
193
|
+
|
|
170
194
|
/**
|
|
171
195
|
* Composable for CodeViewer editor functionality
|
|
172
196
|
*/
|
|
173
197
|
export function useCodeViewerEditor(options: UseCodeViewerEditorOptions): UseCodeViewerEditorReturn {
|
|
174
|
-
const { codeRef, codeFormat, currentFormat, canEdit, editable, onEmitModelValue, onEmitEditable } = options;
|
|
198
|
+
const { codeRef, codeFormat, currentFormat, canEdit, editable, onEmitModelValue, onEmitEditable, onEmitFormat, onExit, onDelete, onOpenLanguageSearch } = options;
|
|
175
199
|
|
|
176
200
|
// Debounce timeout handles
|
|
177
201
|
let validationTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -231,10 +255,18 @@ export function useCodeViewerEditor(options: UseCodeViewerEditorOptions): UseCod
|
|
|
231
255
|
}
|
|
232
256
|
}
|
|
233
257
|
|
|
234
|
-
// Update editing content when format changes
|
|
258
|
+
// Update editing content when format changes and re-apply syntax highlighting
|
|
235
259
|
function updateEditingContentOnFormatChange(): void {
|
|
236
260
|
if (isEditing.value) {
|
|
237
261
|
editingContent.value = codeFormat.formattedContent.value;
|
|
262
|
+
// Update the cached highlighted content with new format
|
|
263
|
+
cachedHighlightedContent.value = highlightSyntax(editingContent.value, { format: currentFormat.value });
|
|
264
|
+
// Re-apply syntax highlighting with new format in the DOM
|
|
265
|
+
nextTick(() => {
|
|
266
|
+
if (codeRef.value) {
|
|
267
|
+
codeRef.value.innerHTML = cachedHighlightedContent.value;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
238
270
|
}
|
|
239
271
|
}
|
|
240
272
|
|
|
@@ -256,6 +288,10 @@ export function useCodeViewerEditor(options: UseCodeViewerEditorOptions): UseCod
|
|
|
256
288
|
highlightTimeout = setTimeout(() => {
|
|
257
289
|
if (!codeRef.value || !isEditing.value) return;
|
|
258
290
|
|
|
291
|
+
// Check if document has active focus on the codeRef before replacing innerHTML
|
|
292
|
+
const activeElement = document.activeElement;
|
|
293
|
+
const hasFocus = activeElement === codeRef.value || codeRef.value.contains(activeElement);
|
|
294
|
+
|
|
259
295
|
// Save cursor position
|
|
260
296
|
const cursorOffset = getCursorOffset(codeRef.value);
|
|
261
297
|
|
|
@@ -264,6 +300,12 @@ export function useCodeViewerEditor(options: UseCodeViewerEditorOptions): UseCod
|
|
|
264
300
|
|
|
265
301
|
// Restore cursor position
|
|
266
302
|
setCursorOffset(codeRef.value, cursorOffset);
|
|
303
|
+
|
|
304
|
+
// Ensure the element maintains focus after innerHTML replacement
|
|
305
|
+
// This is important because innerHTML replacement can cause focus loss
|
|
306
|
+
if (hasFocus && document.activeElement !== codeRef.value) {
|
|
307
|
+
codeRef.value.focus();
|
|
308
|
+
}
|
|
267
309
|
}, 300);
|
|
268
310
|
}
|
|
269
311
|
|
|
@@ -339,31 +381,116 @@ export function useCodeViewerEditor(options: UseCodeViewerEditorOptions): UseCod
|
|
|
339
381
|
}
|
|
340
382
|
}
|
|
341
383
|
|
|
342
|
-
// Handle keyboard shortcuts in edit mode
|
|
384
|
+
// Handle keyboard shortcuts (some work in any mode, some only in edit mode)
|
|
343
385
|
function onKeyDown(event: KeyboardEvent): void {
|
|
386
|
+
// Check for Ctrl/Cmd + Alt + L combinations (with or without Shift)
|
|
387
|
+
// These should work even when not in edit mode
|
|
388
|
+
const isCtrlAltL = (event.ctrlKey || event.metaKey) && event.altKey && event.key.toLowerCase() === "l";
|
|
389
|
+
|
|
390
|
+
if (isCtrlAltL) {
|
|
391
|
+
event.preventDefault();
|
|
392
|
+
event.stopPropagation();
|
|
393
|
+
|
|
394
|
+
// Ctrl/Cmd + Alt + Shift + L - open language search panel
|
|
395
|
+
if (event.shiftKey && onOpenLanguageSearch) {
|
|
396
|
+
onOpenLanguageSearch();
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Ctrl/Cmd + Alt + L (without Shift) - cycle through available formats/languages
|
|
401
|
+
if (!event.shiftKey && onEmitFormat) {
|
|
402
|
+
const availableFormats = getAvailableFormats(currentFormat.value);
|
|
403
|
+
if (availableFormats.length > 1) {
|
|
404
|
+
const currentIndex = availableFormats.indexOf(currentFormat.value);
|
|
405
|
+
const nextIndex = (currentIndex + 1) % availableFormats.length;
|
|
406
|
+
const nextFormat = availableFormats[nextIndex];
|
|
407
|
+
onEmitFormat(nextFormat);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// All other shortcuts require edit mode
|
|
344
414
|
if (!isEditing.value) return;
|
|
345
415
|
|
|
416
|
+
// Backspace/Delete on empty content - delete the code block
|
|
417
|
+
if ((event.key === "Backspace" || event.key === "Delete") && onDelete) {
|
|
418
|
+
const content = editingContent.value.trim();
|
|
419
|
+
if (content === "") {
|
|
420
|
+
event.preventDefault();
|
|
421
|
+
onDelete();
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Ctrl/Cmd + Enter - exit the code block immediately
|
|
427
|
+
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
|
428
|
+
event.preventDefault();
|
|
429
|
+
if (onExit) {
|
|
430
|
+
// Emit the current value before exiting
|
|
431
|
+
const parsed = codeFormat.parse(editingContent.value);
|
|
432
|
+
if (parsed) {
|
|
433
|
+
onEmitModelValue(parsed);
|
|
434
|
+
} else {
|
|
435
|
+
onEmitModelValue(editingContent.value);
|
|
436
|
+
}
|
|
437
|
+
onExit();
|
|
438
|
+
}
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
346
442
|
// Enter key - smart indentation
|
|
347
443
|
if (event.key === "Enter") {
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
range.
|
|
357
|
-
|
|
358
|
-
range
|
|
359
|
-
range.setStartAfter(textNode);
|
|
360
|
-
range.setEndAfter(textNode);
|
|
361
|
-
selection.removeAllRanges();
|
|
362
|
-
selection.addRange(range);
|
|
363
|
-
|
|
364
|
-
codeRef.value?.dispatchEvent(new Event("input", { bubbles: true }));
|
|
444
|
+
const selection = window.getSelection();
|
|
445
|
+
|
|
446
|
+
// If no selection, try to ensure we have one by focusing the element
|
|
447
|
+
if (!selection || selection.rangeCount === 0) {
|
|
448
|
+
// Fallback: position cursor at end of content
|
|
449
|
+
if (codeRef.value) {
|
|
450
|
+
const range = document.createRange();
|
|
451
|
+
range.selectNodeContents(codeRef.value);
|
|
452
|
+
range.collapse(false);
|
|
453
|
+
selection?.removeAllRanges();
|
|
454
|
+
selection?.addRange(range);
|
|
365
455
|
}
|
|
366
456
|
}
|
|
457
|
+
|
|
458
|
+
// Re-check selection - if still none, let browser handle it
|
|
459
|
+
if (!selection || selection.rangeCount === 0) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
event.preventDefault();
|
|
464
|
+
|
|
465
|
+
// Check if the range is actually valid and positioned within our codeRef
|
|
466
|
+
let range = selection.getRangeAt(0);
|
|
467
|
+
const isWithinCodeRef = codeRef.value?.contains(range.startContainer);
|
|
468
|
+
|
|
469
|
+
// If selection is not within codeRef, re-create it at the end of content
|
|
470
|
+
// This can happen after innerHTML replacement in debouncedHighlight
|
|
471
|
+
if (!isWithinCodeRef && codeRef.value) {
|
|
472
|
+
range = document.createRange();
|
|
473
|
+
range.selectNodeContents(codeRef.value);
|
|
474
|
+
range.collapse(false);
|
|
475
|
+
selection.removeAllRanges();
|
|
476
|
+
selection.addRange(range);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// IMPORTANT: Always use the DOM's actual content, not editingContent.value
|
|
480
|
+
// editingContent.value may be stale if isUserEditing is false (e.g., after debounced highlight)
|
|
481
|
+
const domTextContent = codeRef.value?.innerText || "";
|
|
482
|
+
const lineInfo = getCurrentLineInfo(domTextContent, codeRef.value);
|
|
483
|
+
const smartIndent = lineInfo ? getSmartIndent(lineInfo, currentFormat.value) : "";
|
|
484
|
+
|
|
485
|
+
range.deleteContents();
|
|
486
|
+
const textNode = document.createTextNode("\n" + smartIndent);
|
|
487
|
+
range.insertNode(textNode);
|
|
488
|
+
range.setStartAfter(textNode);
|
|
489
|
+
range.setEndAfter(textNode);
|
|
490
|
+
selection.removeAllRanges();
|
|
491
|
+
selection.addRange(range);
|
|
492
|
+
|
|
493
|
+
codeRef.value?.dispatchEvent(new Event("input", { bubbles: true }));
|
|
367
494
|
}
|
|
368
495
|
|
|
369
496
|
// Tab key - insert spaces instead of moving focus
|
|
@@ -384,6 +511,33 @@ export function useCodeViewerEditor(options: UseCodeViewerEditorOptions): UseCod
|
|
|
384
511
|
event.preventDefault();
|
|
385
512
|
onContentEditableBlur();
|
|
386
513
|
}
|
|
514
|
+
|
|
515
|
+
// Ctrl/Cmd + A - select all content within this CodeViewer only
|
|
516
|
+
if ((event.ctrlKey || event.metaKey) && event.key === "a") {
|
|
517
|
+
event.preventDefault();
|
|
518
|
+
event.stopPropagation();
|
|
519
|
+
|
|
520
|
+
// Select all content in the current contenteditable element
|
|
521
|
+
const selection = window.getSelection();
|
|
522
|
+
if (selection && codeRef.value) {
|
|
523
|
+
const range = document.createRange();
|
|
524
|
+
range.selectNodeContents(codeRef.value);
|
|
525
|
+
selection.removeAllRanges();
|
|
526
|
+
selection.addRange(range);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Initialize editing content when starting in edit mode
|
|
532
|
+
// This handles the case where editable=true is passed as initial prop
|
|
533
|
+
if (isEditing.value) {
|
|
534
|
+
editingContent.value = codeFormat.formattedContent.value;
|
|
535
|
+
// Set the pre element content after it mounts
|
|
536
|
+
nextTick(() => {
|
|
537
|
+
if (codeRef.value) {
|
|
538
|
+
codeRef.value.innerHTML = highlightSyntax(editingContent.value, { format: currentFormat.value });
|
|
539
|
+
}
|
|
540
|
+
});
|
|
387
541
|
}
|
|
388
542
|
|
|
389
543
|
// Cleanup timeouts on unmount
|
|
@@ -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
|
+
}
|