overtype 1.2.3 → 1.2.5

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/src/parser.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * MarkdownParser - Parses markdown into HTML while preserving character alignment
3
- *
3
+ *
4
4
  * Key principles:
5
5
  * - Every character must occupy the exact same position as in the textarea
6
6
  * - No font-size changes, no padding/margin on inline elements
@@ -9,14 +9,14 @@
9
9
  export class MarkdownParser {
10
10
  // Track link index for anchor naming
11
11
  static linkIndex = 0;
12
-
12
+
13
13
  /**
14
14
  * Reset link index (call before parsing a new document)
15
15
  */
16
16
  static resetLinkIndex() {
17
17
  this.linkIndex = 0;
18
18
  }
19
-
19
+
20
20
  /**
21
21
  * Escape HTML special characters
22
22
  * @param {string} text - Raw text to escape
@@ -134,8 +134,27 @@ export class MarkdownParser {
134
134
  * @returns {string} HTML with italic styling
135
135
  */
136
136
  static parseItalic(html) {
137
+ // Single asterisk - must not be adjacent to other asterisks
137
138
  html = html.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em><span class="syntax-marker">*</span>$1<span class="syntax-marker">*</span></em>');
138
- html = html.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, '<em><span class="syntax-marker">_</span>$1<span class="syntax-marker">_</span></em>');
139
+
140
+ // Single underscore - must be at word boundaries to avoid matching inside words
141
+ // This prevents matching underscores in the middle of words like "bold_with_underscore"
142
+ html = html.replace(/(?<=^|\s)_(?!_)(.+?)(?<!_)_(?!_)(?=\s|$)/g, '<em><span class="syntax-marker">_</span>$1<span class="syntax-marker">_</span></em>');
143
+
144
+ return html;
145
+ }
146
+
147
+ /**
148
+ * Parse strikethrough text
149
+ * Supports both single (~) and double (~~) tildes, but rejects 3+ tildes
150
+ * @param {string} html - HTML with potential strikethrough markdown
151
+ * @returns {string} HTML with strikethrough styling
152
+ */
153
+ static parseStrikethrough(html) {
154
+ // Double tilde strikethrough: ~~text~~ (but not if part of 3+ tildes)
155
+ html = html.replace(/(?<!~)~~(?!~)(.+?)(?<!~)~~(?!~)/g, '<del><span class="syntax-marker">~~</span>$1<span class="syntax-marker">~~</span></del>');
156
+ // Single tilde strikethrough: ~text~ (but not if part of 2+ tildes on either side)
157
+ html = html.replace(/(?<!~)~(?!~)(.+?)(?<!~)~(?!~)/g, '<del><span class="syntax-marker">~</span>$1<span class="syntax-marker">~</span></del>');
139
158
  return html;
140
159
  }
141
160
 
@@ -165,7 +184,7 @@ export class MarkdownParser {
165
184
  // Trim whitespace and convert to lowercase for protocol check
166
185
  const trimmed = url.trim();
167
186
  const lower = trimmed.toLowerCase();
168
-
187
+
169
188
  // Allow safe protocols
170
189
  const safeProtocols = [
171
190
  'http://',
@@ -174,22 +193,22 @@ export class MarkdownParser {
174
193
  'ftp://',
175
194
  'ftps://'
176
195
  ];
177
-
196
+
178
197
  // Check if URL starts with a safe protocol
179
198
  const hasSafeProtocol = safeProtocols.some(protocol => lower.startsWith(protocol));
180
-
199
+
181
200
  // Allow relative URLs (starting with / or # or no protocol)
182
- const isRelative = trimmed.startsWith('/') ||
183
- trimmed.startsWith('#') ||
201
+ const isRelative = trimmed.startsWith('/') ||
202
+ trimmed.startsWith('#') ||
184
203
  trimmed.startsWith('?') ||
185
204
  trimmed.startsWith('.') ||
186
205
  (!trimmed.includes(':') && !trimmed.includes('//'));
187
-
206
+
188
207
  // If safe protocol or relative URL, return as-is
189
208
  if (hasSafeProtocol || isRelative) {
190
209
  return url;
191
210
  }
192
-
211
+
193
212
  // Block dangerous protocols (javascript:, data:, vbscript:, etc.)
194
213
  return '#';
195
214
  }
@@ -210,49 +229,158 @@ export class MarkdownParser {
210
229
  }
211
230
 
212
231
  /**
213
- * Parse all inline elements in correct order
214
- * @param {string} text - Text with potential inline markdown
215
- * @returns {string} HTML with all inline styling
232
+ * Identify and protect sanctuaries (code and links) before parsing
233
+ * @param {string} text - Text with potential markdown
234
+ * @returns {Object} Object with protected text and sanctuary map
216
235
  */
217
- static parseInlineElements(text) {
218
- let html = text;
219
- // Order matters: parse code first
220
- html = this.parseInlineCode(html);
221
-
222
- // Use placeholders to protect inline code while preserving formatting spans
223
- // We use Unicode Private Use Area (U+E000-U+F8FF) as placeholders because:
224
- // 1. These characters are reserved for application-specific use
225
- // 2. They'll never appear in user text
226
- // 3. They maintain single-character width (important for alignment)
227
- // 4. They're invisible if accidentally rendered
236
+ static identifyAndProtectSanctuaries(text) {
228
237
  const sanctuaries = new Map();
238
+ let sanctuaryCounter = 0;
239
+ let protectedText = text;
240
+
241
+ // Create a map to track protected regions (URLs should not be processed)
242
+ const protectedRegions = [];
243
+
244
+ // First, find all links and mark their URL regions as protected
245
+ const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
246
+ let linkMatch;
247
+ while ((linkMatch = linkRegex.exec(text)) !== null) {
248
+ // Calculate the exact position of the URL part
249
+ // linkMatch.index is the start of the match
250
+ // We need to find where "](" starts, then add 2 to get URL start
251
+ const bracketPos = linkMatch.index + linkMatch[0].indexOf('](');
252
+ const urlStart = bracketPos + 2;
253
+ const urlEnd = urlStart + linkMatch[2].length;
254
+ protectedRegions.push({ start: urlStart, end: urlEnd });
255
+ }
256
+
257
+ // Now protect inline code, but skip if it's inside a protected region (URL)
258
+ const codeRegex = /(?<!`)(`+)(?!`)((?:(?!\1).)+?)(\1)(?!`)/g;
259
+ let codeMatch;
260
+ const codeMatches = [];
261
+
262
+ while ((codeMatch = codeRegex.exec(text)) !== null) {
263
+ const codeStart = codeMatch.index;
264
+ const codeEnd = codeMatch.index + codeMatch[0].length;
265
+
266
+ // Check if this code is inside a protected URL region
267
+ const inProtectedRegion = protectedRegions.some(region =>
268
+ codeStart >= region.start && codeEnd <= region.end
269
+ );
270
+
271
+ if (!inProtectedRegion) {
272
+ codeMatches.push({
273
+ match: codeMatch[0],
274
+ index: codeMatch.index,
275
+ openTicks: codeMatch[1],
276
+ content: codeMatch[2],
277
+ closeTicks: codeMatch[3]
278
+ });
279
+ }
280
+ }
229
281
 
230
- // Protect code blocks
231
- html = html.replace(/(<code>.*?<\/code>)/g, (match) => {
232
- const placeholder = `\uE000${sanctuaries.size}\uE001`;
233
- sanctuaries.set(placeholder, match);
282
+ // Replace code matches from end to start to preserve indices
283
+ codeMatches.sort((a, b) => b.index - a.index);
284
+ codeMatches.forEach(codeInfo => {
285
+ const placeholder = `\uE000${sanctuaryCounter++}\uE001`;
286
+ sanctuaries.set(placeholder, {
287
+ type: 'code',
288
+ original: codeInfo.match,
289
+ openTicks: codeInfo.openTicks,
290
+ content: codeInfo.content,
291
+ closeTicks: codeInfo.closeTicks
292
+ });
293
+ protectedText = protectedText.substring(0, codeInfo.index) +
294
+ placeholder +
295
+ protectedText.substring(codeInfo.index + codeInfo.match.length);
296
+ });
297
+
298
+ // Then protect links - they can contain sanctuary placeholders for code but not raw code
299
+ protectedText = protectedText.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => {
300
+ const placeholder = `\uE000${sanctuaryCounter++}\uE001`;
301
+ sanctuaries.set(placeholder, {
302
+ type: 'link',
303
+ original: match,
304
+ linkText,
305
+ url
306
+ });
234
307
  return placeholder;
235
308
  });
236
309
 
237
- // Parse links AFTER protecting code but BEFORE bold/italic
238
- // This ensures link URLs don't get processed as markdown
239
- html = this.parseLinks(html);
310
+ return { protectedText, sanctuaries };
311
+ }
312
+
313
+ /**
314
+ * Restore and transform sanctuaries back to HTML
315
+ * @param {string} html - HTML with sanctuary placeholders
316
+ * @param {Map} sanctuaries - Map of sanctuaries to restore
317
+ * @returns {string} HTML with sanctuaries restored and transformed
318
+ */
319
+ static restoreAndTransformSanctuaries(html, sanctuaries) {
320
+ // Sort sanctuary placeholders by position to restore in order
321
+ const placeholders = Array.from(sanctuaries.keys()).sort((a, b) => {
322
+ const indexA = html.indexOf(a);
323
+ const indexB = html.indexOf(b);
324
+ return indexA - indexB;
325
+ });
240
326
 
241
- // Protect entire link elements (not just the URL part)
242
- html = html.replace(/(<a[^>]*>.*?<\/a>)/g, (match) => {
243
- const placeholder = `\uE000${sanctuaries.size}\uE001`;
244
- sanctuaries.set(placeholder, match);
245
- return placeholder;
327
+ placeholders.forEach(placeholder => {
328
+ const sanctuary = sanctuaries.get(placeholder);
329
+ let replacement;
330
+
331
+ if (sanctuary.type === 'code') {
332
+ // Transform code sanctuary to HTML
333
+ replacement = `<code><span class="syntax-marker">${sanctuary.openTicks}</span>${this.escapeHtml(sanctuary.content)}<span class="syntax-marker">${sanctuary.closeTicks}</span></code>`;
334
+ } else if (sanctuary.type === 'link') {
335
+ // For links, we need to process the link text for markdown
336
+ let processedLinkText = sanctuary.linkText;
337
+
338
+ // First restore any sanctuary placeholders that were already in the link text
339
+ // (e.g., inline code that was protected before the link)
340
+ sanctuaries.forEach((innerSanctuary, innerPlaceholder) => {
341
+ if (processedLinkText.includes(innerPlaceholder)) {
342
+ if (innerSanctuary.type === 'code') {
343
+ const codeHtml = `<code><span class="syntax-marker">${innerSanctuary.openTicks}</span>${this.escapeHtml(innerSanctuary.content)}<span class="syntax-marker">${innerSanctuary.closeTicks}</span></code>`;
344
+ processedLinkText = processedLinkText.replace(innerPlaceholder, codeHtml);
345
+ }
346
+ }
347
+ });
348
+
349
+ // Now parse other markdown in the link text (bold, italic, etc)
350
+ processedLinkText = this.parseStrikethrough(processedLinkText);
351
+ processedLinkText = this.parseBold(processedLinkText);
352
+ processedLinkText = this.parseItalic(processedLinkText);
353
+
354
+ // Transform link sanctuary to HTML
355
+ // URL should NOT be processed for markdown - use it as-is
356
+ const anchorName = `--link-${this.linkIndex++}`;
357
+ const safeUrl = this.sanitizeUrl(sanctuary.url);
358
+ replacement = `<a href="${safeUrl}" style="anchor-name: ${anchorName}"><span class="syntax-marker">[</span>${processedLinkText}<span class="syntax-marker url-part">](${this.escapeHtml(sanctuary.url)})</span></a>`;
359
+ }
360
+
361
+ html = html.replace(placeholder, replacement);
246
362
  });
247
363
 
248
- // Process other inline elements on text with placeholders
364
+ return html;
365
+ }
366
+
367
+ /**
368
+ * Parse all inline elements in correct order
369
+ * @param {string} text - Text with potential inline markdown
370
+ * @returns {string} HTML with all inline styling
371
+ */
372
+ static parseInlineElements(text) {
373
+ // Step 1: Identify and protect sanctuaries (code and links)
374
+ const { protectedText, sanctuaries } = this.identifyAndProtectSanctuaries(text);
375
+
376
+ // Step 2: Parse other inline elements on protected text
377
+ let html = protectedText;
378
+ html = this.parseStrikethrough(html);
249
379
  html = this.parseBold(html);
250
380
  html = this.parseItalic(html);
251
381
 
252
- // Restore all sanctuaries
253
- sanctuaries.forEach((content, placeholder) => {
254
- html = html.replace(placeholder, content);
255
- });
382
+ // Step 3: Restore and transform sanctuaries
383
+ html = this.restoreAndTransformSanctuaries(html, sanctuaries);
256
384
 
257
385
  return html;
258
386
  }
@@ -264,33 +392,33 @@ export class MarkdownParser {
264
392
  */
265
393
  static parseLine(line) {
266
394
  let html = this.escapeHtml(line);
267
-
395
+
268
396
  // Preserve indentation
269
397
  html = this.preserveIndentation(html, line);
270
-
398
+
271
399
  // Check for block elements first
272
400
  const horizontalRule = this.parseHorizontalRule(html);
273
401
  if (horizontalRule) return horizontalRule;
274
-
402
+
275
403
  const codeBlock = this.parseCodeBlock(html);
276
404
  if (codeBlock) return codeBlock;
277
-
405
+
278
406
  // Parse block elements
279
407
  html = this.parseHeader(html);
280
408
  html = this.parseBlockquote(html);
281
409
  html = this.parseBulletList(html);
282
410
  html = this.parseNumberedList(html);
283
-
411
+
284
412
  // Parse inline elements
285
413
  html = this.parseInlineElements(html);
286
-
414
+
287
415
  // Wrap in div to maintain line structure
288
416
  if (html.trim() === '') {
289
417
  // Intentionally use &nbsp; for empty lines to maintain vertical spacing
290
418
  // This causes a 0->1 character count difference but preserves visual alignment
291
419
  return '<div>&nbsp;</div>';
292
420
  }
293
-
421
+
294
422
  return `<div>${html}</div>`;
295
423
  }
296
424
 
@@ -304,17 +432,17 @@ export class MarkdownParser {
304
432
  static parse(text, activeLine = -1, showActiveLineRaw = false) {
305
433
  // Reset link counter for each parse
306
434
  this.resetLinkIndex();
307
-
435
+
308
436
  const lines = text.split('\n');
309
437
  let inCodeBlock = false;
310
-
438
+
311
439
  const parsedLines = lines.map((line, index) => {
312
440
  // Show raw markdown on active line if requested
313
441
  if (showActiveLineRaw && index === activeLine) {
314
442
  const content = this.escapeHtml(line) || '&nbsp;';
315
443
  return `<div class="raw-line">${content}</div>`;
316
444
  }
317
-
445
+
318
446
  // Check if this line is a code fence
319
447
  const codeFenceRegex = /^```[^`]*$/;
320
448
  if (codeFenceRegex.test(line)) {
@@ -322,21 +450,21 @@ export class MarkdownParser {
322
450
  // Parse fence markers normally to get styled output
323
451
  return this.parseLine(line);
324
452
  }
325
-
453
+
326
454
  // If we're inside a code block, don't parse as markdown
327
455
  if (inCodeBlock) {
328
456
  const escaped = this.escapeHtml(line);
329
457
  const indented = this.preserveIndentation(escaped, line);
330
458
  return `<div>${indented || '&nbsp;'}</div>`;
331
459
  }
332
-
460
+
333
461
  // Otherwise, parse the markdown normally
334
462
  return this.parseLine(line);
335
463
  });
336
-
464
+
337
465
  // Join without newlines to prevent extra spacing
338
466
  const html = parsedLines.join('');
339
-
467
+
340
468
  // Apply post-processing for list consolidation
341
469
  return this.postProcessHTML(html);
342
470
  }
@@ -352,25 +480,25 @@ export class MarkdownParser {
352
480
  // In Node.js environment - do manual post-processing
353
481
  return this.postProcessHTMLManual(html);
354
482
  }
355
-
483
+
356
484
  // Parse HTML string into DOM
357
485
  const container = document.createElement('div');
358
486
  container.innerHTML = html;
359
-
487
+
360
488
  let currentList = null;
361
489
  let listType = null;
362
490
  let currentCodeBlock = null;
363
491
  let inCodeBlock = false;
364
-
492
+
365
493
  // Process all direct children - need to be careful with live NodeList
366
494
  const children = Array.from(container.children);
367
-
495
+
368
496
  for (let i = 0; i < children.length; i++) {
369
497
  const child = children[i];
370
-
498
+
371
499
  // Skip if child was already processed/removed
372
500
  if (!child.parentNode) continue;
373
-
501
+
374
502
  // Check for code fence start/end
375
503
  const codeFence = child.querySelector('.code-fence');
376
504
  if (codeFence) {
@@ -379,22 +507,22 @@ export class MarkdownParser {
379
507
  if (!inCodeBlock) {
380
508
  // Start of code block - keep fence visible, then add pre/code
381
509
  inCodeBlock = true;
382
-
510
+
383
511
  // Create the code block that will follow the fence
384
512
  currentCodeBlock = document.createElement('pre');
385
513
  const codeElement = document.createElement('code');
386
514
  currentCodeBlock.appendChild(codeElement);
387
515
  currentCodeBlock.className = 'code-block';
388
-
516
+
389
517
  // Extract language if present
390
518
  const lang = fenceText.slice(3).trim();
391
519
  if (lang) {
392
520
  codeElement.className = `language-${lang}`;
393
521
  }
394
-
522
+
395
523
  // Insert code block after the fence div (don't remove the fence)
396
524
  container.insertBefore(currentCodeBlock, child.nextSibling);
397
-
525
+
398
526
  // Store reference to the code element for adding content
399
527
  currentCodeBlock._codeElement = codeElement;
400
528
  continue;
@@ -406,7 +534,7 @@ export class MarkdownParser {
406
534
  }
407
535
  }
408
536
  }
409
-
537
+
410
538
  // Check if we're in a code block - any div that's not a code fence
411
539
  if (inCodeBlock && currentCodeBlock && child.tagName === 'DIV' && !child.querySelector('.code-fence')) {
412
540
  const codeElement = currentCodeBlock._codeElement || currentCodeBlock.querySelector('code');
@@ -422,36 +550,52 @@ export class MarkdownParser {
422
550
  child.remove();
423
551
  continue;
424
552
  }
425
-
553
+
426
554
  // Check if this div contains a list item
427
555
  let listItem = null;
428
556
  if (child.tagName === 'DIV') {
429
557
  // Look for li inside the div
430
558
  listItem = child.querySelector('li');
431
559
  }
432
-
560
+
433
561
  if (listItem) {
434
562
  const isBullet = listItem.classList.contains('bullet-list');
435
563
  const isOrdered = listItem.classList.contains('ordered-list');
436
-
564
+
437
565
  if (!isBullet && !isOrdered) {
438
566
  currentList = null;
439
567
  listType = null;
440
568
  continue;
441
569
  }
442
-
570
+
443
571
  const newType = isBullet ? 'ul' : 'ol';
444
-
572
+
445
573
  // Start new list or continue current
446
574
  if (!currentList || listType !== newType) {
447
575
  currentList = document.createElement(newType);
448
576
  container.insertBefore(currentList, child);
449
577
  listType = newType;
450
578
  }
451
-
579
+
580
+ // Extract and preserve indentation from the div before moving the list item
581
+ const indentationNodes = [];
582
+ for (const node of child.childNodes) {
583
+ if (node.nodeType === 3 && node.textContent.match(/^\u00A0+$/)) {
584
+ // This is a text node containing only non-breaking spaces (indentation)
585
+ indentationNodes.push(node.cloneNode(true));
586
+ } else if (node === listItem) {
587
+ break; // Stop when we reach the list item
588
+ }
589
+ }
590
+
591
+ // Add indentation to the list item
592
+ indentationNodes.forEach(node => {
593
+ listItem.insertBefore(node, listItem.firstChild);
594
+ });
595
+
452
596
  // Move the list item to the current list
453
597
  currentList.appendChild(listItem);
454
-
598
+
455
599
  // Remove the now-empty div wrapper
456
600
  child.remove();
457
601
  } else {
@@ -460,7 +604,7 @@ export class MarkdownParser {
460
604
  listType = null;
461
605
  }
462
606
  }
463
-
607
+
464
608
  return container.innerHTML;
465
609
  }
466
610
 
@@ -471,25 +615,53 @@ export class MarkdownParser {
471
615
  */
472
616
  static postProcessHTMLManual(html) {
473
617
  let processed = html;
474
-
618
+
475
619
  // Process unordered lists
476
620
  processed = processed.replace(/((?:<div>(?:&nbsp;)*<li class="bullet-list">.*?<\/li><\/div>\s*)+)/gs, (match) => {
477
- const items = match.match(/<li class="bullet-list">.*?<\/li>/gs) || [];
478
- if (items.length > 0) {
621
+ const divs = match.match(/<div>(?:&nbsp;)*<li class="bullet-list">.*?<\/li><\/div>/gs) || [];
622
+ if (divs.length > 0) {
623
+ const items = divs.map(div => {
624
+ // Extract indentation and list item
625
+ const indentMatch = div.match(/<div>((?:&nbsp;)*)<li/);
626
+ const listItemMatch = div.match(/<li class="bullet-list">.*?<\/li>/);
627
+
628
+ if (indentMatch && listItemMatch) {
629
+ const indentation = indentMatch[1];
630
+ const listItem = listItemMatch[0];
631
+ // Insert indentation at the start of the list item content
632
+ return listItem.replace(/<li class="bullet-list">/, `<li class="bullet-list">${indentation}`);
633
+ }
634
+ return listItemMatch ? listItemMatch[0] : '';
635
+ }).filter(Boolean);
636
+
479
637
  return '<ul>' + items.join('') + '</ul>';
480
638
  }
481
639
  return match;
482
640
  });
483
-
641
+
484
642
  // Process ordered lists
485
643
  processed = processed.replace(/((?:<div>(?:&nbsp;)*<li class="ordered-list">.*?<\/li><\/div>\s*)+)/gs, (match) => {
486
- const items = match.match(/<li class="ordered-list">.*?<\/li>/gs) || [];
487
- if (items.length > 0) {
644
+ const divs = match.match(/<div>(?:&nbsp;)*<li class="ordered-list">.*?<\/li><\/div>/gs) || [];
645
+ if (divs.length > 0) {
646
+ const items = divs.map(div => {
647
+ // Extract indentation and list item
648
+ const indentMatch = div.match(/<div>((?:&nbsp;)*)<li/);
649
+ const listItemMatch = div.match(/<li class="ordered-list">.*?<\/li>/);
650
+
651
+ if (indentMatch && listItemMatch) {
652
+ const indentation = indentMatch[1];
653
+ const listItem = listItemMatch[0];
654
+ // Insert indentation at the start of the list item content
655
+ return listItem.replace(/<li class="ordered-list">/, `<li class="ordered-list">${indentation}`);
656
+ }
657
+ return listItemMatch ? listItemMatch[0] : '';
658
+ }).filter(Boolean);
659
+
488
660
  return '<ol>' + items.join('') + '</ol>';
489
661
  }
490
662
  return match;
491
663
  });
492
-
664
+
493
665
  // Process code blocks - KEEP the fence markers for alignment AND use semantic pre/code
494
666
  const codeBlockRegex = /<div><span class="code-fence">(```[^<]*)<\/span><\/div>(.*?)<div><span class="code-fence">(```)<\/span><\/div>/gs;
495
667
  processed = processed.replace(codeBlockRegex, (match, openFence, content, closeFence) => {
@@ -501,20 +673,20 @@ export class MarkdownParser {
501
673
  .replace(/&nbsp;/g, ' ');
502
674
  return text;
503
675
  }).join('\n');
504
-
676
+
505
677
  // Extract language from the opening fence
506
678
  const lang = openFence.slice(3).trim();
507
679
  const langClass = lang ? ` class="language-${lang}"` : '';
508
-
680
+
509
681
  // Keep fence markers visible as separate divs, with pre/code block between them
510
682
  let result = `<div><span class="code-fence">${openFence}</span></div>`;
511
683
  // Content is already escaped, don't double-escape
512
684
  result += `<pre class="code-block"><code${langClass}>${codeContent}</code></pre>`;
513
685
  result += `<div><span class="code-fence">${closeFence}</span></div>`;
514
-
686
+
515
687
  return result;
516
688
  });
517
-
689
+
518
690
  return processed;
519
691
  }
520
692
 
@@ -539,7 +711,7 @@ export class MarkdownParser {
539
711
  let currentPos = 0;
540
712
  let lineIndex = 0;
541
713
  let lineStart = 0;
542
-
714
+
543
715
  for (let i = 0; i < lines.length; i++) {
544
716
  const lineLength = lines[i].length;
545
717
  if (currentPos + lineLength >= cursorPosition) {
@@ -549,10 +721,10 @@ export class MarkdownParser {
549
721
  }
550
722
  currentPos += lineLength + 1; // +1 for newline
551
723
  }
552
-
724
+
553
725
  const currentLine = lines[lineIndex];
554
726
  const lineEnd = lineStart + currentLine.length;
555
-
727
+
556
728
  // Check for checkbox first (most specific)
557
729
  const checkboxMatch = currentLine.match(this.LIST_PATTERNS.checkbox);
558
730
  if (checkboxMatch) {
@@ -568,7 +740,7 @@ export class MarkdownParser {
568
740
  markerEndPos: lineStart + checkboxMatch[1].length + checkboxMatch[2].length + 5 // indent + "- [ ] "
569
741
  };
570
742
  }
571
-
743
+
572
744
  // Check for bullet list
573
745
  const bulletMatch = currentLine.match(this.LIST_PATTERNS.bullet);
574
746
  if (bulletMatch) {
@@ -583,7 +755,7 @@ export class MarkdownParser {
583
755
  markerEndPos: lineStart + bulletMatch[1].length + bulletMatch[2].length + 1 // indent + marker + space
584
756
  };
585
757
  }
586
-
758
+
587
759
  // Check for numbered list
588
760
  const numberedMatch = currentLine.match(this.LIST_PATTERNS.numbered);
589
761
  if (numberedMatch) {
@@ -598,7 +770,7 @@ export class MarkdownParser {
598
770
  markerEndPos: lineStart + numberedMatch[1].length + numberedMatch[2].length + 2 // indent + number + ". "
599
771
  };
600
772
  }
601
-
773
+
602
774
  // Not in a list
603
775
  return {
604
776
  inList: false,
@@ -639,31 +811,31 @@ export class MarkdownParser {
639
811
  const lines = text.split('\n');
640
812
  const numbersByIndent = new Map();
641
813
  let inList = false;
642
-
814
+
643
815
  const result = lines.map(line => {
644
816
  const match = line.match(this.LIST_PATTERNS.numbered);
645
-
817
+
646
818
  if (match) {
647
819
  const indent = match[1];
648
820
  const indentLevel = indent.length;
649
821
  const content = match[3];
650
-
822
+
651
823
  // If we weren't in a list or indent changed, reset lower levels
652
824
  if (!inList) {
653
825
  numbersByIndent.clear();
654
826
  }
655
-
827
+
656
828
  // Get the next number for this indent level
657
829
  const currentNumber = (numbersByIndent.get(indentLevel) || 0) + 1;
658
830
  numbersByIndent.set(indentLevel, currentNumber);
659
-
831
+
660
832
  // Clear deeper indent levels
661
833
  for (const [level] of numbersByIndent) {
662
834
  if (level > indentLevel) {
663
835
  numbersByIndent.delete(level);
664
836
  }
665
837
  }
666
-
838
+
667
839
  inList = true;
668
840
  return `${indent}${currentNumber}. ${content}`;
669
841
  } else {
@@ -676,7 +848,7 @@ export class MarkdownParser {
676
848
  return line;
677
849
  }
678
850
  });
679
-
851
+
680
852
  return result.join('\n');
681
853
  }
682
- }
854
+ }