overtype 1.2.1 → 1.2.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/src/overtype.js CHANGED
@@ -164,7 +164,8 @@ class OverType {
164
164
  showActiveLineRaw: false,
165
165
  showStats: false,
166
166
  toolbar: false,
167
- statsFormatter: null
167
+ statsFormatter: null,
168
+ smartLists: true // Enable smart list continuation
168
169
  };
169
170
 
170
171
  // Remove theme and colors from options - these are now global
@@ -489,25 +490,9 @@ class OverType {
489
490
  openParent.classList.add('code-block-line');
490
491
  closeParent.classList.add('code-block-line');
491
492
 
492
- // Apply class to all divs between the parent divs
493
- let currentDiv = openParent.nextElementSibling;
494
- while (currentDiv && currentDiv !== closeParent) {
495
- // Apply class to divs between the fences
496
- if (currentDiv.tagName === 'DIV') {
497
- currentDiv.classList.add('code-block-line');
498
-
499
- // Strip all formatting by replacing with plain text
500
- // This prevents markdown formatting inside code blocks
501
- const plainText = currentDiv.textContent;
502
- currentDiv.textContent = plainText;
503
- }
504
-
505
- // Move to next sibling
506
- currentDiv = currentDiv.nextElementSibling;
507
-
508
- // Safety check to prevent infinite loop
509
- if (!currentDiv) break;
510
- }
493
+ // With the new structure, there's a <pre> block between fences, not DIVs
494
+ // We don't need to process anything between the fences anymore
495
+ // The <pre><code> structure already handles the content correctly
511
496
  }
512
497
  }
513
498
 
@@ -599,6 +584,14 @@ class OverType {
599
584
  return;
600
585
  }
601
586
 
587
+ // Handle Enter key for smart list continuation
588
+ if (event.key === 'Enter' && !event.shiftKey && !event.metaKey && !event.ctrlKey && this.options.smartLists) {
589
+ if (this.handleSmartListContinuation()) {
590
+ event.preventDefault();
591
+ return;
592
+ }
593
+ }
594
+
602
595
  // Let shortcuts manager handle other keys
603
596
  const handled = this.shortcuts.handleKeydown(event);
604
597
 
@@ -608,6 +601,141 @@ class OverType {
608
601
  }
609
602
  }
610
603
 
604
+ /**
605
+ * Handle smart list continuation
606
+ * @returns {boolean} Whether the event was handled
607
+ */
608
+ handleSmartListContinuation() {
609
+ const textarea = this.textarea;
610
+ const cursorPos = textarea.selectionStart;
611
+ const context = MarkdownParser.getListContext(textarea.value, cursorPos);
612
+
613
+ if (!context || !context.inList) return false;
614
+
615
+ // Handle empty list item (exit list)
616
+ if (context.content.trim() === '' && cursorPos >= context.markerEndPos) {
617
+ this.deleteListMarker(context);
618
+ return true;
619
+ }
620
+
621
+ // Handle text splitting if cursor is in middle of content
622
+ if (cursorPos > context.markerEndPos && cursorPos < context.lineEnd) {
623
+ this.splitListItem(context, cursorPos);
624
+ } else {
625
+ // Just add new item after current line
626
+ this.insertNewListItem(context);
627
+ }
628
+
629
+ // Handle numbered list renumbering
630
+ if (context.listType === 'numbered') {
631
+ this.scheduleNumberedListUpdate();
632
+ }
633
+
634
+ return true;
635
+ }
636
+
637
+ /**
638
+ * Delete list marker and exit list
639
+ * @private
640
+ */
641
+ deleteListMarker(context) {
642
+ // Select from line start to marker end
643
+ this.textarea.setSelectionRange(context.lineStart, context.markerEndPos);
644
+ document.execCommand('delete');
645
+
646
+ // Trigger input event
647
+ this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
648
+ }
649
+
650
+ /**
651
+ * Insert new list item
652
+ * @private
653
+ */
654
+ insertNewListItem(context) {
655
+ const newItem = MarkdownParser.createNewListItem(context);
656
+ document.execCommand('insertText', false, '\n' + newItem);
657
+
658
+ // Trigger input event
659
+ this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
660
+ }
661
+
662
+ /**
663
+ * Split list item at cursor position
664
+ * @private
665
+ */
666
+ splitListItem(context, cursorPos) {
667
+ // Get text after cursor
668
+ const textAfterCursor = context.content.substring(cursorPos - context.markerEndPos);
669
+
670
+ // Delete text after cursor
671
+ this.textarea.setSelectionRange(cursorPos, context.lineEnd);
672
+ document.execCommand('delete');
673
+
674
+ // Insert new list item with remaining text
675
+ const newItem = MarkdownParser.createNewListItem(context);
676
+ document.execCommand('insertText', false, '\n' + newItem + textAfterCursor);
677
+
678
+ // Position cursor after new list marker
679
+ const newCursorPos = this.textarea.selectionStart - textAfterCursor.length;
680
+ this.textarea.setSelectionRange(newCursorPos, newCursorPos);
681
+
682
+ // Trigger input event
683
+ this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
684
+ }
685
+
686
+ /**
687
+ * Schedule numbered list renumbering
688
+ * @private
689
+ */
690
+ scheduleNumberedListUpdate() {
691
+ // Clear any pending update
692
+ if (this.numberUpdateTimeout) {
693
+ clearTimeout(this.numberUpdateTimeout);
694
+ }
695
+
696
+ // Schedule update after current input cycle
697
+ this.numberUpdateTimeout = setTimeout(() => {
698
+ this.updateNumberedLists();
699
+ }, 10);
700
+ }
701
+
702
+ /**
703
+ * Update/renumber all numbered lists
704
+ * @private
705
+ */
706
+ updateNumberedLists() {
707
+ const value = this.textarea.value;
708
+ const cursorPos = this.textarea.selectionStart;
709
+
710
+ const newValue = MarkdownParser.renumberLists(value);
711
+
712
+ if (newValue !== value) {
713
+ // Calculate cursor offset
714
+ let offset = 0;
715
+ const oldLines = value.split('\n');
716
+ const newLines = newValue.split('\n');
717
+ let charCount = 0;
718
+
719
+ for (let i = 0; i < oldLines.length && charCount < cursorPos; i++) {
720
+ if (oldLines[i] !== newLines[i]) {
721
+ const diff = newLines[i].length - oldLines[i].length;
722
+ if (charCount + oldLines[i].length < cursorPos) {
723
+ offset += diff;
724
+ }
725
+ }
726
+ charCount += oldLines[i].length + 1; // +1 for newline
727
+ }
728
+
729
+ // Update textarea
730
+ this.textarea.value = newValue;
731
+ const newCursorPos = cursorPos + offset;
732
+ this.textarea.setSelectionRange(newCursorPos, newCursorPos);
733
+
734
+ // Trigger update
735
+ this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
736
+ }
737
+ }
738
+
611
739
  /**
612
740
  * Handle scroll events
613
741
  * @private
package/src/parser.js CHANGED
@@ -286,6 +286,8 @@ export class MarkdownParser {
286
286
 
287
287
  // Wrap in div to maintain line structure
288
288
  if (html.trim() === '') {
289
+ // Intentionally use &nbsp; for empty lines to maintain vertical spacing
290
+ // This causes a 0->1 character count difference but preserves visual alignment
289
291
  return '<div>&nbsp;</div>';
290
292
  }
291
293
 
@@ -304,6 +306,8 @@ export class MarkdownParser {
304
306
  this.resetLinkIndex();
305
307
 
306
308
  const lines = text.split('\n');
309
+ let inCodeBlock = false;
310
+
307
311
  const parsedLines = lines.map((line, index) => {
308
312
  // Show raw markdown on active line if requested
309
313
  if (showActiveLineRaw && index === activeLine) {
@@ -311,6 +315,21 @@ export class MarkdownParser {
311
315
  return `<div class="raw-line">${content}</div>`;
312
316
  }
313
317
 
318
+ // Check if this line is a code fence
319
+ const codeFenceRegex = /^```[^`]*$/;
320
+ if (codeFenceRegex.test(line)) {
321
+ inCodeBlock = !inCodeBlock;
322
+ // Parse fence markers normally to get styled output
323
+ return this.parseLine(line);
324
+ }
325
+
326
+ // If we're inside a code block, don't parse as markdown
327
+ if (inCodeBlock) {
328
+ const escaped = this.escapeHtml(line);
329
+ const indented = this.preserveIndentation(escaped, line);
330
+ return `<div>${indented || '&nbsp;'}</div>`;
331
+ }
332
+
314
333
  // Otherwise, parse the markdown normally
315
334
  return this.parseLine(line);
316
335
  });
@@ -358,8 +377,10 @@ export class MarkdownParser {
358
377
  const fenceText = codeFence.textContent;
359
378
  if (fenceText.startsWith('```')) {
360
379
  if (!inCodeBlock) {
361
- // Start of code block
380
+ // Start of code block - keep fence visible, then add pre/code
362
381
  inCodeBlock = true;
382
+
383
+ // Create the code block that will follow the fence
363
384
  currentCodeBlock = document.createElement('pre');
364
385
  const codeElement = document.createElement('code');
365
386
  currentCodeBlock.appendChild(codeElement);
@@ -371,14 +392,16 @@ export class MarkdownParser {
371
392
  codeElement.className = `language-${lang}`;
372
393
  }
373
394
 
374
- container.insertBefore(currentCodeBlock, child);
375
- child.remove();
395
+ // Insert code block after the fence div (don't remove the fence)
396
+ container.insertBefore(currentCodeBlock, child.nextSibling);
397
+
398
+ // Store reference to the code element for adding content
399
+ currentCodeBlock._codeElement = codeElement;
376
400
  continue;
377
401
  } else {
378
- // End of code block
402
+ // End of code block - fence stays visible
379
403
  inCodeBlock = false;
380
404
  currentCodeBlock = null;
381
- child.remove();
382
405
  continue;
383
406
  }
384
407
  }
@@ -386,13 +409,15 @@ export class MarkdownParser {
386
409
 
387
410
  // Check if we're in a code block - any div that's not a code fence
388
411
  if (inCodeBlock && currentCodeBlock && child.tagName === 'DIV' && !child.querySelector('.code-fence')) {
389
- const codeElement = currentCodeBlock.querySelector('code');
412
+ const codeElement = currentCodeBlock._codeElement || currentCodeBlock.querySelector('code');
390
413
  // Add the line content to the code block
391
414
  if (codeElement.textContent.length > 0) {
392
415
  codeElement.textContent += '\n';
393
416
  }
394
417
  // Get the actual text content, preserving spaces
395
- const lineText = child.innerHTML.replace(/&nbsp;/g, ' ').replace(/<[^>]*>/g, '');
418
+ // Use textContent instead of innerHTML to avoid double-escaping
419
+ // textContent automatically decodes HTML entities
420
+ const lineText = child.textContent.replace(/\u00A0/g, ' '); // \u00A0 is nbsp
396
421
  codeElement.textContent += lineText;
397
422
  child.remove();
398
423
  continue;
@@ -465,27 +490,193 @@ export class MarkdownParser {
465
490
  return match;
466
491
  });
467
492
 
468
- // Process code blocks
469
- const codeBlockRegex = /<div><span class="code-fence">```([^<]*)<\/span><\/div>(.*?)<div><span class="code-fence">```<\/span><\/div>/gs;
470
- processed = processed.replace(codeBlockRegex, (match, lang, content) => {
493
+ // Process code blocks - KEEP the fence markers for alignment AND use semantic pre/code
494
+ const codeBlockRegex = /<div><span class="code-fence">(```[^<]*)<\/span><\/div>(.*?)<div><span class="code-fence">(```)<\/span><\/div>/gs;
495
+ processed = processed.replace(codeBlockRegex, (match, openFence, content, closeFence) => {
471
496
  // Extract the content between fences
472
497
  const lines = content.match(/<div>(.*?)<\/div>/gs) || [];
473
498
  const codeContent = lines.map(line => {
474
- // Extract text from each div
499
+ // Extract text from each div - content is already escaped
475
500
  const text = line.replace(/<div>(.*?)<\/div>/s, '$1')
476
- .replace(/&nbsp;/g, ' ')
477
- .replace(/&lt;/g, '<')
478
- .replace(/&gt;/g, '>')
479
- .replace(/&quot;/g, '"')
480
- .replace(/&#39;/g, "'")
481
- .replace(/&amp;/g, '&');
501
+ .replace(/&nbsp;/g, ' ');
482
502
  return text;
483
503
  }).join('\n');
484
504
 
485
- const langClass = lang ? ` class="language-${lang.trim()}"` : '';
486
- return `<pre class="code-block"><code${langClass}>${this.escapeHtml(codeContent)}</code></pre>`;
505
+ // Extract language from the opening fence
506
+ const lang = openFence.slice(3).trim();
507
+ const langClass = lang ? ` class="language-${lang}"` : '';
508
+
509
+ // Keep fence markers visible as separate divs, with pre/code block between them
510
+ let result = `<div><span class="code-fence">${openFence}</span></div>`;
511
+ // Content is already escaped, don't double-escape
512
+ result += `<pre class="code-block"><code${langClass}>${codeContent}</code></pre>`;
513
+ result += `<div><span class="code-fence">${closeFence}</span></div>`;
514
+
515
+ return result;
487
516
  });
488
517
 
489
518
  return processed;
490
519
  }
520
+
521
+ /**
522
+ * List pattern definitions
523
+ */
524
+ static LIST_PATTERNS = {
525
+ bullet: /^(\s*)([-*+])\s+(.*)$/,
526
+ numbered: /^(\s*)(\d+)\.\s+(.*)$/,
527
+ checkbox: /^(\s*)-\s+\[([ x])\]\s+(.*)$/
528
+ };
529
+
530
+ /**
531
+ * Get list context at cursor position
532
+ * @param {string} text - Full text content
533
+ * @param {number} cursorPosition - Current cursor position
534
+ * @returns {Object} List context information
535
+ */
536
+ static getListContext(text, cursorPosition) {
537
+ // Find the line containing the cursor
538
+ const lines = text.split('\n');
539
+ let currentPos = 0;
540
+ let lineIndex = 0;
541
+ let lineStart = 0;
542
+
543
+ for (let i = 0; i < lines.length; i++) {
544
+ const lineLength = lines[i].length;
545
+ if (currentPos + lineLength >= cursorPosition) {
546
+ lineIndex = i;
547
+ lineStart = currentPos;
548
+ break;
549
+ }
550
+ currentPos += lineLength + 1; // +1 for newline
551
+ }
552
+
553
+ const currentLine = lines[lineIndex];
554
+ const lineEnd = lineStart + currentLine.length;
555
+
556
+ // Check for checkbox first (most specific)
557
+ const checkboxMatch = currentLine.match(this.LIST_PATTERNS.checkbox);
558
+ if (checkboxMatch) {
559
+ return {
560
+ inList: true,
561
+ listType: 'checkbox',
562
+ indent: checkboxMatch[1],
563
+ marker: '-',
564
+ checked: checkboxMatch[2] === 'x',
565
+ content: checkboxMatch[3],
566
+ lineStart,
567
+ lineEnd,
568
+ markerEndPos: lineStart + checkboxMatch[1].length + checkboxMatch[2].length + 5 // indent + "- [ ] "
569
+ };
570
+ }
571
+
572
+ // Check for bullet list
573
+ const bulletMatch = currentLine.match(this.LIST_PATTERNS.bullet);
574
+ if (bulletMatch) {
575
+ return {
576
+ inList: true,
577
+ listType: 'bullet',
578
+ indent: bulletMatch[1],
579
+ marker: bulletMatch[2],
580
+ content: bulletMatch[3],
581
+ lineStart,
582
+ lineEnd,
583
+ markerEndPos: lineStart + bulletMatch[1].length + bulletMatch[2].length + 1 // indent + marker + space
584
+ };
585
+ }
586
+
587
+ // Check for numbered list
588
+ const numberedMatch = currentLine.match(this.LIST_PATTERNS.numbered);
589
+ if (numberedMatch) {
590
+ return {
591
+ inList: true,
592
+ listType: 'numbered',
593
+ indent: numberedMatch[1],
594
+ marker: parseInt(numberedMatch[2]),
595
+ content: numberedMatch[3],
596
+ lineStart,
597
+ lineEnd,
598
+ markerEndPos: lineStart + numberedMatch[1].length + numberedMatch[2].length + 2 // indent + number + ". "
599
+ };
600
+ }
601
+
602
+ // Not in a list
603
+ return {
604
+ inList: false,
605
+ listType: null,
606
+ indent: '',
607
+ marker: null,
608
+ content: currentLine,
609
+ lineStart,
610
+ lineEnd,
611
+ markerEndPos: lineStart
612
+ };
613
+ }
614
+
615
+ /**
616
+ * Create a new list item based on context
617
+ * @param {Object} context - List context from getListContext
618
+ * @returns {string} New list item text
619
+ */
620
+ static createNewListItem(context) {
621
+ switch (context.listType) {
622
+ case 'bullet':
623
+ return `${context.indent}${context.marker} `;
624
+ case 'numbered':
625
+ return `${context.indent}${context.marker + 1}. `;
626
+ case 'checkbox':
627
+ return `${context.indent}- [ ] `;
628
+ default:
629
+ return '';
630
+ }
631
+ }
632
+
633
+ /**
634
+ * Renumber all numbered lists in text
635
+ * @param {string} text - Text containing numbered lists
636
+ * @returns {string} Text with renumbered lists
637
+ */
638
+ static renumberLists(text) {
639
+ const lines = text.split('\n');
640
+ const numbersByIndent = new Map();
641
+ let inList = false;
642
+
643
+ const result = lines.map(line => {
644
+ const match = line.match(this.LIST_PATTERNS.numbered);
645
+
646
+ if (match) {
647
+ const indent = match[1];
648
+ const indentLevel = indent.length;
649
+ const content = match[3];
650
+
651
+ // If we weren't in a list or indent changed, reset lower levels
652
+ if (!inList) {
653
+ numbersByIndent.clear();
654
+ }
655
+
656
+ // Get the next number for this indent level
657
+ const currentNumber = (numbersByIndent.get(indentLevel) || 0) + 1;
658
+ numbersByIndent.set(indentLevel, currentNumber);
659
+
660
+ // Clear deeper indent levels
661
+ for (const [level] of numbersByIndent) {
662
+ if (level > indentLevel) {
663
+ numbersByIndent.delete(level);
664
+ }
665
+ }
666
+
667
+ inList = true;
668
+ return `${indent}${currentNumber}. ${content}`;
669
+ } else {
670
+ // Not a numbered list item
671
+ if (line.trim() === '' || !line.match(/^\s/)) {
672
+ // Empty line or non-indented line breaks the list
673
+ inList = false;
674
+ numbersByIndent.clear();
675
+ }
676
+ return line;
677
+ }
678
+ });
679
+
680
+ return result.join('\n');
681
+ }
491
682
  }
package/src/styles.js CHANGED
@@ -79,11 +79,17 @@ export function generateStyles(options = {}) {
79
79
  position: relative !important; /* Override reset - needed for absolute children */
80
80
  overflow: visible !important; /* Allow dropdown to overflow container */
81
81
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
82
+ text-align: left !important;
82
83
  ${themeVars ? `
83
84
  /* Theme Variables */
84
85
  ${themeVars}` : ''}
85
86
  }
86
87
 
88
+ /* Force left alignment for all elements in the editor */
89
+ .overtype-container .overtype-wrapper * {
90
+ text-align: left !important;
91
+ }
92
+
87
93
  /* Auto-resize mode styles */
88
94
  .overtype-container.overtype-auto-resize {
89
95
  height: auto !important;