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/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. ~78KB 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** - ~78KB 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** | ~78KB | 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,11 +551,32 @@ 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
 
553
558
  MIT
554
559
 
560
+ ## Related Projects
561
+
562
+ ### Synesthesia
563
+
564
+ [Synesthesia](https://github.com/panphora/synesthesia) is a lightweight syntax highlighting editor library that extracted and refined the core textarea overlay technique from OverType. While OverType is focused on markdown editing with toolbar features, Synesthesia provides a more generalized code editing solution with:
565
+
566
+ - **Pluggable parser system** - Support for any programming language or syntax
567
+ - **Parser registry** - Automatic language detection by file extension or MIME type
568
+ - **Cleaner separation** - Extracted the overlay technique without markdown-specific features
569
+ - **Smaller footprint** - ~82KB minified (vs OverType's ~78KB)
570
+
571
+ Key components extracted from OverType to Synesthesia:
572
+ - The transparent textarea overlay technique for perfect WYSIWYG alignment
573
+ - Theme system with CSS variable support
574
+ - DOM persistence and recovery mechanisms
575
+ - Auto-resize functionality
576
+ - Event delegation for efficient multi-instance support
577
+
578
+ If you need a markdown editor with toolbar and formatting features, use OverType. If you need a lightweight code editor with custom syntax highlighting, check out Synesthesia.
579
+
555
580
  ## Contributing
556
581
 
557
582
  Contributions are welcome! Please feel free to submit a Pull Request.
package/dist/overtype.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * OverType v1.2.1
2
+ * OverType v1.2.2
3
3
  * A lightweight markdown editor library with perfect WYSIWYG alignment
4
4
  * @license MIT
5
5
  * @author Demo User
@@ -258,11 +258,22 @@ var MarkdownParser = class {
258
258
  static parse(text, activeLine = -1, showActiveLineRaw = false) {
259
259
  this.resetLinkIndex();
260
260
  const lines = text.split("\n");
261
+ let inCodeBlock = false;
261
262
  const parsedLines = lines.map((line, index) => {
262
263
  if (showActiveLineRaw && index === activeLine) {
263
264
  const content = this.escapeHtml(line) || " ";
264
265
  return `<div class="raw-line">${content}</div>`;
265
266
  }
267
+ const codeFenceRegex = /^```[^`]*$/;
268
+ if (codeFenceRegex.test(line)) {
269
+ inCodeBlock = !inCodeBlock;
270
+ return this.parseLine(line);
271
+ }
272
+ if (inCodeBlock) {
273
+ const escaped = this.escapeHtml(line);
274
+ const indented = this.preserveIndentation(escaped, line);
275
+ return `<div>${indented || "&nbsp;"}</div>`;
276
+ }
266
277
  return this.parseLine(line);
267
278
  });
268
279
  const html = parsedLines.join("");
@@ -302,23 +313,22 @@ var MarkdownParser = class {
302
313
  if (lang) {
303
314
  codeElement.className = `language-${lang}`;
304
315
  }
305
- container.insertBefore(currentCodeBlock, child);
306
- child.remove();
316
+ container.insertBefore(currentCodeBlock, child.nextSibling);
317
+ currentCodeBlock._codeElement = codeElement;
307
318
  continue;
308
319
  } else {
309
320
  inCodeBlock = false;
310
321
  currentCodeBlock = null;
311
- child.remove();
312
322
  continue;
313
323
  }
314
324
  }
315
325
  }
316
326
  if (inCodeBlock && currentCodeBlock && child.tagName === "DIV" && !child.querySelector(".code-fence")) {
317
- const codeElement = currentCodeBlock.querySelector("code");
327
+ const codeElement = currentCodeBlock._codeElement || currentCodeBlock.querySelector("code");
318
328
  if (codeElement.textContent.length > 0) {
319
329
  codeElement.textContent += "\n";
320
330
  }
321
- const lineText = child.innerHTML.replace(/&nbsp;/g, " ").replace(/<[^>]*>/g, "");
331
+ const lineText = child.textContent.replace(/\u00A0/g, " ");
322
332
  codeElement.textContent += lineText;
323
333
  child.remove();
324
334
  continue;
@@ -371,21 +381,163 @@ var MarkdownParser = class {
371
381
  }
372
382
  return match;
373
383
  });
374
- const codeBlockRegex = /<div><span class="code-fence">```([^<]*)<\/span><\/div>(.*?)<div><span class="code-fence">```<\/span><\/div>/gs;
375
- processed = processed.replace(codeBlockRegex, (match, lang, content) => {
384
+ const codeBlockRegex = /<div><span class="code-fence">(```[^<]*)<\/span><\/div>(.*?)<div><span class="code-fence">(```)<\/span><\/div>/gs;
385
+ processed = processed.replace(codeBlockRegex, (match, openFence, content, closeFence) => {
376
386
  const lines = content.match(/<div>(.*?)<\/div>/gs) || [];
377
387
  const codeContent = lines.map((line) => {
378
- const text = line.replace(/<div>(.*?)<\/div>/s, "$1").replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&");
388
+ const text = line.replace(/<div>(.*?)<\/div>/s, "$1").replace(/&nbsp;/g, " ");
379
389
  return text;
380
390
  }).join("\n");
381
- const langClass = lang ? ` class="language-${lang.trim()}"` : "";
382
- return `<pre class="code-block"><code${langClass}>${this.escapeHtml(codeContent)}</code></pre>`;
391
+ const lang = openFence.slice(3).trim();
392
+ const langClass = lang ? ` class="language-${lang}"` : "";
393
+ let result = `<div><span class="code-fence">${openFence}</span></div>`;
394
+ result += `<pre class="code-block"><code${langClass}>${codeContent}</code></pre>`;
395
+ result += `<div><span class="code-fence">${closeFence}</span></div>`;
396
+ return result;
383
397
  });
384
398
  return processed;
385
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
+ }
386
530
  };
387
531
  // Track link index for anchor naming
388
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
+ });
389
541
 
390
542
  // node_modules/markdown-actions/dist/markdown-actions.esm.js
391
543
  var __defProp2 = Object.defineProperty;
@@ -1520,11 +1672,17 @@ function generateStyles(options = {}) {
1520
1672
  position: relative !important; /* Override reset - needed for absolute children */
1521
1673
  overflow: visible !important; /* Allow dropdown to overflow container */
1522
1674
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
1675
+ text-align: left !important;
1523
1676
  ${themeVars ? `
1524
1677
  /* Theme Variables */
1525
1678
  ${themeVars}` : ""}
1526
1679
  }
1527
1680
 
1681
+ /* Force left alignment for all elements in the editor */
1682
+ .overtype-container .overtype-wrapper * {
1683
+ text-align: left !important;
1684
+ }
1685
+
1528
1686
  /* Auto-resize mode styles */
1529
1687
  .overtype-container.overtype-auto-resize {
1530
1688
  height: auto !important;
@@ -2852,7 +3010,9 @@ var _OverType = class _OverType {
2852
3010
  showActiveLineRaw: false,
2853
3011
  showStats: false,
2854
3012
  toolbar: false,
2855
- statsFormatter: null
3013
+ statsFormatter: null,
3014
+ smartLists: true
3015
+ // Enable smart list continuation
2856
3016
  };
2857
3017
  const { theme, colors, ...cleanOptions } = options;
2858
3018
  return {
@@ -3079,17 +3239,6 @@ var _OverType = class _OverType {
3079
3239
  closeFence.style.display = "block";
3080
3240
  openParent.classList.add("code-block-line");
3081
3241
  closeParent.classList.add("code-block-line");
3082
- let currentDiv = openParent.nextElementSibling;
3083
- while (currentDiv && currentDiv !== closeParent) {
3084
- if (currentDiv.tagName === "DIV") {
3085
- currentDiv.classList.add("code-block-line");
3086
- const plainText = currentDiv.textContent;
3087
- currentDiv.textContent = plainText;
3088
- }
3089
- currentDiv = currentDiv.nextElementSibling;
3090
- if (!currentDiv)
3091
- break;
3092
- }
3093
3242
  }
3094
3243
  }
3095
3244
  /**
@@ -3156,11 +3305,113 @@ var _OverType = class _OverType {
3156
3305
  this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
3157
3306
  return;
3158
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
+ }
3159
3314
  const handled = this.shortcuts.handleKeydown(event);
3160
3315
  if (!handled && this.options.onKeydown) {
3161
3316
  this.options.onKeydown(event, this);
3162
3317
  }
3163
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
+ }
3164
3415
  /**
3165
3416
  * Handle scroll events
3166
3417
  * @private