overtype 1.2.2 → 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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OverType
2
2
 
3
- A lightweight markdown editor library with perfect WYSIWYG alignment using an invisible textarea overlay technique. Includes optional toolbar. ~79KB minified with all features.
3
+ A lightweight markdown editor library with perfect WYSIWYG alignment using an invisible textarea overlay technique. Includes optional toolbar. ~82KB minified with all features.
4
4
 
5
5
  ## Features
6
6
 
@@ -9,9 +9,10 @@ A lightweight markdown editor library with perfect WYSIWYG alignment using an in
9
9
  - ⌨️ **Keyboard shortcuts** - Common markdown shortcuts (Cmd/Ctrl+B for bold, etc.)
10
10
  - 📱 **Mobile optimized** - Responsive design with mobile-specific styles
11
11
  - 🔄 **DOM persistence aware** - Recovers from existing DOM (perfect for HyperClay and similar platforms)
12
- - 🚀 **Lightweight** - ~79KB minified
12
+ - 🚀 **Lightweight** - ~82KB minified
13
13
  - 🎯 **Optional toolbar** - Clean, minimal toolbar with all essential formatting
14
14
  - ✨ **Smart shortcuts** - Keyboard shortcuts with selection preservation
15
+ - 📝 **Smart list continuation** - GitHub-style automatic list continuation on Enter
15
16
  - 🔧 **Framework agnostic** - Works with React, Vue, vanilla JS, and more
16
17
 
17
18
  ## How it works
@@ -24,7 +25,7 @@ We overlap an invisible textarea on top of styled output, giving the illusion of
24
25
 
25
26
  | Feature | OverType | HyperMD | Milkdown | TUI Editor | EasyMDE |
26
27
  |---------|----------|---------|----------|------------|---------|
27
- | **Size** | ~79KB | 364.02 KB | 344.51 KB | 560.99 KB | 323.69 KB |
28
+ | **Size** | ~82KB | 364.02 KB | 344.51 KB | 560.99 KB | 323.69 KB |
28
29
  | **Dependencies** | Bundled | CodeMirror | ProseMirror + plugins | Multiple libs | CodeMirror |
29
30
  | **Setup** | Single file | Complex config | Build step required | Complex config | Moderate |
30
31
  | **Approach** | Invisible textarea | ContentEditable | ContentEditable | ContentEditable | CodeMirror |
@@ -345,6 +346,9 @@ new OverType(target, options)
345
346
  // Toolbar
346
347
  toolbar: false, // Enable/disable toolbar with formatting buttons
347
348
 
349
+ // Smart lists
350
+ smartLists: true, // Enable GitHub-style list continuation on Enter
351
+
348
352
  // Stats bar
349
353
  showStats: false, // Enable/disable stats bar
350
354
  statsFormatter: (stats) => { // Custom stats format
@@ -547,6 +551,7 @@ Special thanks to:
547
551
  - [kbhomes](https://github.com/kbhomes) - Fixed text selection desynchronization during overscroll ([#17](https://github.com/panphora/overtype/pull/17))
548
552
  - [merlinz01](https://github.com/merlinz01) - Initial TypeScript definitions implementation ([#20](https://github.com/panphora/overtype/pull/20))
549
553
  - [Max Bernstein](https://github.com/tekknolagi) - Fixed typo in website ([#11](https://github.com/panphora/overtype/pull/11))
554
+ - [davidlazar](https://github.com/davidlazar) - Suggested view mode feature for toggling overlay and preview modes ([#24](https://github.com/panphora/overtype/issues/24))
550
555
 
551
556
  ## License
552
557
 
@@ -561,7 +566,7 @@ MIT
561
566
  - **Pluggable parser system** - Support for any programming language or syntax
562
567
  - **Parser registry** - Automatic language detection by file extension or MIME type
563
568
  - **Cleaner separation** - Extracted the overlay technique without markdown-specific features
564
- - **Smaller footprint** - ~79KB minified (vs OverType's ~78KB)
569
+ - **Smaller footprint** - ~82KB minified (vs OverType's ~78KB)
565
570
 
566
571
  Key components extracted from OverType to Synesthesia:
567
572
  - The transparent textarea overlay technique for perfect WYSIWYG alignment
package/dist/overtype.cjs CHANGED
@@ -397,9 +397,147 @@ var MarkdownParser = class {
397
397
  });
398
398
  return processed;
399
399
  }
400
+ /**
401
+ * Get list context at cursor position
402
+ * @param {string} text - Full text content
403
+ * @param {number} cursorPosition - Current cursor position
404
+ * @returns {Object} List context information
405
+ */
406
+ static getListContext(text, cursorPosition) {
407
+ const lines = text.split("\n");
408
+ let currentPos = 0;
409
+ let lineIndex = 0;
410
+ let lineStart = 0;
411
+ for (let i = 0; i < lines.length; i++) {
412
+ const lineLength = lines[i].length;
413
+ if (currentPos + lineLength >= cursorPosition) {
414
+ lineIndex = i;
415
+ lineStart = currentPos;
416
+ break;
417
+ }
418
+ currentPos += lineLength + 1;
419
+ }
420
+ const currentLine = lines[lineIndex];
421
+ const lineEnd = lineStart + currentLine.length;
422
+ const checkboxMatch = currentLine.match(this.LIST_PATTERNS.checkbox);
423
+ if (checkboxMatch) {
424
+ return {
425
+ inList: true,
426
+ listType: "checkbox",
427
+ indent: checkboxMatch[1],
428
+ marker: "-",
429
+ checked: checkboxMatch[2] === "x",
430
+ content: checkboxMatch[3],
431
+ lineStart,
432
+ lineEnd,
433
+ markerEndPos: lineStart + checkboxMatch[1].length + checkboxMatch[2].length + 5
434
+ // indent + "- [ ] "
435
+ };
436
+ }
437
+ const bulletMatch = currentLine.match(this.LIST_PATTERNS.bullet);
438
+ if (bulletMatch) {
439
+ return {
440
+ inList: true,
441
+ listType: "bullet",
442
+ indent: bulletMatch[1],
443
+ marker: bulletMatch[2],
444
+ content: bulletMatch[3],
445
+ lineStart,
446
+ lineEnd,
447
+ markerEndPos: lineStart + bulletMatch[1].length + bulletMatch[2].length + 1
448
+ // indent + marker + space
449
+ };
450
+ }
451
+ const numberedMatch = currentLine.match(this.LIST_PATTERNS.numbered);
452
+ if (numberedMatch) {
453
+ return {
454
+ inList: true,
455
+ listType: "numbered",
456
+ indent: numberedMatch[1],
457
+ marker: parseInt(numberedMatch[2]),
458
+ content: numberedMatch[3],
459
+ lineStart,
460
+ lineEnd,
461
+ markerEndPos: lineStart + numberedMatch[1].length + numberedMatch[2].length + 2
462
+ // indent + number + ". "
463
+ };
464
+ }
465
+ return {
466
+ inList: false,
467
+ listType: null,
468
+ indent: "",
469
+ marker: null,
470
+ content: currentLine,
471
+ lineStart,
472
+ lineEnd,
473
+ markerEndPos: lineStart
474
+ };
475
+ }
476
+ /**
477
+ * Create a new list item based on context
478
+ * @param {Object} context - List context from getListContext
479
+ * @returns {string} New list item text
480
+ */
481
+ static createNewListItem(context) {
482
+ switch (context.listType) {
483
+ case "bullet":
484
+ return `${context.indent}${context.marker} `;
485
+ case "numbered":
486
+ return `${context.indent}${context.marker + 1}. `;
487
+ case "checkbox":
488
+ return `${context.indent}- [ ] `;
489
+ default:
490
+ return "";
491
+ }
492
+ }
493
+ /**
494
+ * Renumber all numbered lists in text
495
+ * @param {string} text - Text containing numbered lists
496
+ * @returns {string} Text with renumbered lists
497
+ */
498
+ static renumberLists(text) {
499
+ const lines = text.split("\n");
500
+ const numbersByIndent = /* @__PURE__ */ new Map();
501
+ let inList = false;
502
+ const result = lines.map((line) => {
503
+ const match = line.match(this.LIST_PATTERNS.numbered);
504
+ if (match) {
505
+ const indent = match[1];
506
+ const indentLevel = indent.length;
507
+ const content = match[3];
508
+ if (!inList) {
509
+ numbersByIndent.clear();
510
+ }
511
+ const currentNumber = (numbersByIndent.get(indentLevel) || 0) + 1;
512
+ numbersByIndent.set(indentLevel, currentNumber);
513
+ for (const [level] of numbersByIndent) {
514
+ if (level > indentLevel) {
515
+ numbersByIndent.delete(level);
516
+ }
517
+ }
518
+ inList = true;
519
+ return `${indent}${currentNumber}. ${content}`;
520
+ } else {
521
+ if (line.trim() === "" || !line.match(/^\s/)) {
522
+ inList = false;
523
+ numbersByIndent.clear();
524
+ }
525
+ return line;
526
+ }
527
+ });
528
+ return result.join("\n");
529
+ }
400
530
  };
401
531
  // Track link index for anchor naming
402
532
  __publicField(MarkdownParser, "linkIndex", 0);
533
+ /**
534
+ * List pattern definitions
535
+ */
536
+ __publicField(MarkdownParser, "LIST_PATTERNS", {
537
+ bullet: /^(\s*)([-*+])\s+(.*)$/,
538
+ numbered: /^(\s*)(\d+)\.\s+(.*)$/,
539
+ checkbox: /^(\s*)-\s+\[([ x])\]\s+(.*)$/
540
+ });
403
541
 
404
542
  // node_modules/markdown-actions/dist/markdown-actions.esm.js
405
543
  var __defProp2 = Object.defineProperty;
@@ -2872,7 +3010,9 @@ var _OverType = class _OverType {
2872
3010
  showActiveLineRaw: false,
2873
3011
  showStats: false,
2874
3012
  toolbar: false,
2875
- statsFormatter: null
3013
+ statsFormatter: null,
3014
+ smartLists: true
3015
+ // Enable smart list continuation
2876
3016
  };
2877
3017
  const { theme, colors, ...cleanOptions } = options;
2878
3018
  return {
@@ -3165,11 +3305,113 @@ var _OverType = class _OverType {
3165
3305
  this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
3166
3306
  return;
3167
3307
  }
3308
+ if (event.key === "Enter" && !event.shiftKey && !event.metaKey && !event.ctrlKey && this.options.smartLists) {
3309
+ if (this.handleSmartListContinuation()) {
3310
+ event.preventDefault();
3311
+ return;
3312
+ }
3313
+ }
3168
3314
  const handled = this.shortcuts.handleKeydown(event);
3169
3315
  if (!handled && this.options.onKeydown) {
3170
3316
  this.options.onKeydown(event, this);
3171
3317
  }
3172
3318
  }
3319
+ /**
3320
+ * Handle smart list continuation
3321
+ * @returns {boolean} Whether the event was handled
3322
+ */
3323
+ handleSmartListContinuation() {
3324
+ const textarea = this.textarea;
3325
+ const cursorPos = textarea.selectionStart;
3326
+ const context = MarkdownParser.getListContext(textarea.value, cursorPos);
3327
+ if (!context || !context.inList)
3328
+ return false;
3329
+ if (context.content.trim() === "" && cursorPos >= context.markerEndPos) {
3330
+ this.deleteListMarker(context);
3331
+ return true;
3332
+ }
3333
+ if (cursorPos > context.markerEndPos && cursorPos < context.lineEnd) {
3334
+ this.splitListItem(context, cursorPos);
3335
+ } else {
3336
+ this.insertNewListItem(context);
3337
+ }
3338
+ if (context.listType === "numbered") {
3339
+ this.scheduleNumberedListUpdate();
3340
+ }
3341
+ return true;
3342
+ }
3343
+ /**
3344
+ * Delete list marker and exit list
3345
+ * @private
3346
+ */
3347
+ deleteListMarker(context) {
3348
+ this.textarea.setSelectionRange(context.lineStart, context.markerEndPos);
3349
+ document.execCommand("delete");
3350
+ this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
3351
+ }
3352
+ /**
3353
+ * Insert new list item
3354
+ * @private
3355
+ */
3356
+ insertNewListItem(context) {
3357
+ const newItem = MarkdownParser.createNewListItem(context);
3358
+ document.execCommand("insertText", false, "\n" + newItem);
3359
+ this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
3360
+ }
3361
+ /**
3362
+ * Split list item at cursor position
3363
+ * @private
3364
+ */
3365
+ splitListItem(context, cursorPos) {
3366
+ const textAfterCursor = context.content.substring(cursorPos - context.markerEndPos);
3367
+ this.textarea.setSelectionRange(cursorPos, context.lineEnd);
3368
+ document.execCommand("delete");
3369
+ const newItem = MarkdownParser.createNewListItem(context);
3370
+ document.execCommand("insertText", false, "\n" + newItem + textAfterCursor);
3371
+ const newCursorPos = this.textarea.selectionStart - textAfterCursor.length;
3372
+ this.textarea.setSelectionRange(newCursorPos, newCursorPos);
3373
+ this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
3374
+ }
3375
+ /**
3376
+ * Schedule numbered list renumbering
3377
+ * @private
3378
+ */
3379
+ scheduleNumberedListUpdate() {
3380
+ if (this.numberUpdateTimeout) {
3381
+ clearTimeout(this.numberUpdateTimeout);
3382
+ }
3383
+ this.numberUpdateTimeout = setTimeout(() => {
3384
+ this.updateNumberedLists();
3385
+ }, 10);
3386
+ }
3387
+ /**
3388
+ * Update/renumber all numbered lists
3389
+ * @private
3390
+ */
3391
+ updateNumberedLists() {
3392
+ const value = this.textarea.value;
3393
+ const cursorPos = this.textarea.selectionStart;
3394
+ const newValue = MarkdownParser.renumberLists(value);
3395
+ if (newValue !== value) {
3396
+ let offset = 0;
3397
+ const oldLines = value.split("\n");
3398
+ const newLines = newValue.split("\n");
3399
+ let charCount = 0;
3400
+ for (let i = 0; i < oldLines.length && charCount < cursorPos; i++) {
3401
+ if (oldLines[i] !== newLines[i]) {
3402
+ const diff = newLines[i].length - oldLines[i].length;
3403
+ if (charCount + oldLines[i].length < cursorPos) {
3404
+ offset += diff;
3405
+ }
3406
+ }
3407
+ charCount += oldLines[i].length + 1;
3408
+ }
3409
+ this.textarea.value = newValue;
3410
+ const newCursorPos = cursorPos + offset;
3411
+ this.textarea.setSelectionRange(newCursorPos, newCursorPos);
3412
+ this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
3413
+ }
3414
+ }
3173
3415
  /**
3174
3416
  * Handle scroll events
3175
3417
  * @private