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 +9 -4
- package/dist/overtype.cjs +243 -1
- package/dist/overtype.cjs.map +2 -2
- package/dist/overtype.esm.js +243 -1
- package/dist/overtype.esm.js.map +2 -2
- package/dist/overtype.js +243 -1
- package/dist/overtype.js.map +2 -2
- package/dist/overtype.min.js +56 -49
- package/package.json +1 -1
- package/src/overtype.js +145 -1
- package/src/parser.js +162 -0
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. ~
|
|
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** - ~
|
|
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** | ~
|
|
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** - ~
|
|
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
|