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.
Files changed (81) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/dist/danx.es.js +16119 -10641
  3. package/dist/danx.es.js.map +1 -1
  4. package/dist/danx.umd.js +202 -123
  5. package/dist/danx.umd.js.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/package.json +8 -1
  8. package/src/components/Utility/Buttons/ActionButton.vue +15 -5
  9. package/src/components/Utility/Code/CodeViewer.vue +41 -16
  10. package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
  11. package/src/components/Utility/Code/CodeViewerFooter.vue +3 -1
  12. package/src/components/Utility/Code/LanguageBadge.vue +278 -5
  13. package/src/components/Utility/Code/MarkdownContent.vue +31 -163
  14. package/src/components/Utility/Code/index.ts +3 -0
  15. package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
  16. package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
  17. package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
  18. package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
  19. package/src/components/Utility/Markdown/MarkdownEditor.vue +233 -0
  20. package/src/components/Utility/Markdown/MarkdownEditorContent.vue +296 -0
  21. package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
  22. package/src/components/Utility/Markdown/TablePopover.vue +420 -0
  23. package/src/components/Utility/Markdown/index.ts +11 -0
  24. package/src/components/Utility/Markdown/types.ts +27 -0
  25. package/src/components/Utility/Widgets/LabelPillWidget.vue +20 -0
  26. package/src/components/Utility/index.ts +1 -0
  27. package/src/composables/index.ts +1 -0
  28. package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
  29. package/src/composables/markdown/features/useBlockquotes.ts +248 -0
  30. package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
  31. package/src/composables/markdown/features/useCodeBlocks.spec.ts +805 -0
  32. package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
  33. package/src/composables/markdown/features/useContextMenu.ts +444 -0
  34. package/src/composables/markdown/features/useFocusTracking.ts +116 -0
  35. package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
  36. package/src/composables/markdown/features/useHeadings.ts +290 -0
  37. package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
  38. package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
  39. package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
  40. package/src/composables/markdown/features/useLinks.spec.ts +388 -0
  41. package/src/composables/markdown/features/useLinks.ts +374 -0
  42. package/src/composables/markdown/features/useLists.spec.ts +834 -0
  43. package/src/composables/markdown/features/useLists.ts +747 -0
  44. package/src/composables/markdown/features/usePopoverManager.ts +181 -0
  45. package/src/composables/markdown/features/useTables.spec.ts +1601 -0
  46. package/src/composables/markdown/features/useTables.ts +1107 -0
  47. package/src/composables/markdown/index.ts +16 -0
  48. package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
  49. package/src/composables/markdown/useMarkdownEditor.ts +1077 -0
  50. package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
  51. package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
  52. package/src/composables/markdown/useMarkdownSelection.ts +219 -0
  53. package/src/composables/markdown/useMarkdownSync.ts +549 -0
  54. package/src/composables/useCodeFormat.ts +17 -10
  55. package/src/composables/useCodeViewerEditor.spec.ts +655 -0
  56. package/src/composables/useCodeViewerEditor.ts +174 -20
  57. package/src/helpers/formats/highlightCSS.ts +236 -0
  58. package/src/helpers/formats/highlightHTML.ts +483 -0
  59. package/src/helpers/formats/highlightJavaScript.ts +346 -0
  60. package/src/helpers/formats/highlightSyntax.ts +15 -4
  61. package/src/helpers/formats/index.ts +3 -0
  62. package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
  63. package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
  64. package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +425 -0
  65. package/src/helpers/formats/markdown/index.ts +7 -0
  66. package/src/helpers/formats/markdown/linePatterns.spec.ts +498 -0
  67. package/src/helpers/formats/markdown/linePatterns.ts +172 -0
  68. package/src/styles/danx.scss +3 -3
  69. package/src/styles/index.scss +5 -5
  70. package/src/styles/themes/danx/code.scss +257 -1
  71. package/src/styles/themes/danx/index.scss +10 -10
  72. package/src/styles/themes/danx/markdown.scss +59 -0
  73. package/src/test/helpers/editorTestUtils.spec.ts +296 -0
  74. package/src/test/helpers/editorTestUtils.ts +253 -0
  75. package/src/test/helpers/index.ts +1 -0
  76. package/src/test/highlighters.test.ts +153 -0
  77. package/src/test/setup.test.ts +12 -0
  78. package/src/test/setup.ts +12 -0
  79. package/src/types/widgets.d.ts +2 -2
  80. package/vite.config.js +5 -1
  81. 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 lineInfo = getCurrentLineInfo(editingContent.value, codeRef.value);
349
- if (lineInfo) {
350
- event.preventDefault();
351
- const smartIndent = getSmartIndent(lineInfo, currentFormat.value);
352
-
353
- const selection = window.getSelection();
354
- if (selection && selection.rangeCount > 0) {
355
- const range = selection.getRangeAt(0);
356
- range.deleteContents();
357
- const textNode = document.createTextNode("\n" + smartIndent);
358
- range.insertNode(textNode);
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, "&amp;")
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
+ }