overtype 1.2.4 → 1.2.6

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. ~84KB minified with all features.
3
+ A lightweight markdown editor library with perfect WYSIWYG alignment using an invisible textarea overlay technique. Includes optional toolbar. ~85KB minified with all features.
4
4
 
5
5
  ## Live Examples
6
6
 
@@ -19,7 +19,7 @@ A lightweight markdown editor library with perfect WYSIWYG alignment using an in
19
19
  - ⌨️ **Keyboard shortcuts** - Common markdown shortcuts (Cmd/Ctrl+B for bold, etc.)
20
20
  - 📱 **Mobile optimized** - Responsive design with mobile-specific styles
21
21
  - 🔄 **DOM persistence aware** - Recovers from existing DOM (perfect for HyperClay and similar platforms)
22
- - 🚀 **Lightweight** - ~84KB minified
22
+ - 🚀 **Lightweight** - ~85KB minified
23
23
  - 🎯 **Optional toolbar** - Clean, minimal toolbar with all essential formatting
24
24
  - ✨ **Smart shortcuts** - Keyboard shortcuts with selection preservation
25
25
  - 📝 **Smart list continuation** - GitHub-style automatic list continuation on Enter
@@ -35,7 +35,7 @@ We overlap an invisible textarea on top of styled output, giving the illusion of
35
35
 
36
36
  | Feature | OverType | HyperMD | Milkdown | TUI Editor | EasyMDE |
37
37
  |---------|----------|---------|----------|------------|---------|
38
- | **Size** | ~84KB | 364.02 KB | 344.51 KB | 560.99 KB | 323.69 KB |
38
+ | **Size** | ~85KB | 364.02 KB | 344.51 KB | 560.99 KB | 323.69 KB |
39
39
  | **Dependencies** | Bundled | CodeMirror | ProseMirror + plugins | Multiple libs | CodeMirror |
40
40
  | **Setup** | Single file | Complex config | Build step required | Complex config | Moderate |
41
41
  | **Approach** | Invisible textarea | ContentEditable | ContentEditable | ContentEditable | CodeMirror |
@@ -574,31 +574,21 @@ Special thanks to:
574
574
 
575
575
  MIT
576
576
 
577
- ## Related Projects
578
-
579
- ### Synesthesia
577
+ ## Contributing
580
578
 
581
- [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:
579
+ Contributions are welcome! Please feel free to submit a Pull Request.
582
580
 
583
- - **Pluggable parser system** - Support for any programming language or syntax
584
- - **Parser registry** - Automatic language detection by file extension or MIME type
585
- - **Cleaner separation** - Extracted the overlay technique without markdown-specific features
586
- - **Smaller footprint** - ~84KB minified (vs OverType's ~78KB)
581
+ ---
587
582
 
588
- Key components extracted from OverType to Synesthesia:
589
- - The transparent textarea overlay technique for perfect WYSIWYG alignment
590
- - Theme system with CSS variable support
591
- - DOM persistence and recovery mechanisms
592
- - Auto-resize functionality
593
- - Event delegation for efficient multi-instance support
583
+ Built with the radical idea that sometimes dumb ideas work.
594
584
 
595
- 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.
585
+ ---
596
586
 
597
- ## Contributing
587
+ **Ready for another radical idea?**
588
+ Let's remove every layer of the web application stack.
598
589
 
599
- Contributions are welcome! Please feel free to submit a Pull Request.
590
+ ### Hyperclay
600
591
 
601
- ---
592
+ [Hyperclay](https://hyperclay.com) by @panphora allows you to make a web app in a single, portable, self-updating, vanilla HTML file. No frameworks, no build steps, no deployment pipelines. Just a single HTML file that persists its own state and can be edited live.
602
593
 
603
- Ready for another radical idea?
604
- [Let's remove every layer of the web application stack.](https://hyperclay.com)
594
+ Think of it as a Google Document for interactive code, where the UI, logic, and data all live in one self-modifying file. Share apps instantly, edit them directly, use them offline.
package/dist/overtype.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * OverType v1.2.4
2
+ * OverType v1.2.6
3
3
  * A lightweight markdown editor library with perfect WYSIWYG alignment
4
4
  * @license MIT
5
5
  * @author Demo User
@@ -152,7 +152,7 @@ var MarkdownParser = class {
152
152
  */
153
153
  static parseItalic(html) {
154
154
  html = html.replace(new RegExp("(?<!\\*)\\*(?!\\*)(.+?)(?<!\\*)\\*(?!\\*)", "g"), '<em><span class="syntax-marker">*</span>$1<span class="syntax-marker">*</span></em>');
155
- html = html.replace(new RegExp("(?<!_)_(?!_)(.+?)(?<!_)_(?!_)", "g"), '<em><span class="syntax-marker">_</span>$1<span class="syntax-marker">_</span></em>');
155
+ html = html.replace(new RegExp("(?<=^|\\s)_(?!_)(.+?)(?<!_)_(?!_)(?=\\s|$)", "g"), '<em><span class="syntax-marker">_</span>$1<span class="syntax-marker">_</span></em>');
156
156
  return html;
157
157
  }
158
158
  /**
@@ -209,31 +209,116 @@ var MarkdownParser = class {
209
209
  });
210
210
  }
211
211
  /**
212
- * Parse all inline elements in correct order
213
- * @param {string} text - Text with potential inline markdown
214
- * @returns {string} HTML with all inline styling
212
+ * Identify and protect sanctuaries (code and links) before parsing
213
+ * @param {string} text - Text with potential markdown
214
+ * @returns {Object} Object with protected text and sanctuary map
215
215
  */
216
- static parseInlineElements(text) {
217
- let html = text;
218
- html = this.parseInlineCode(html);
216
+ static identifyAndProtectSanctuaries(text) {
219
217
  const sanctuaries = /* @__PURE__ */ new Map();
220
- html = html.replace(/(<code>.*?<\/code>)/g, (match) => {
221
- const placeholder = `\uE000${sanctuaries.size}\uE001`;
222
- sanctuaries.set(placeholder, match);
223
- return placeholder;
218
+ let sanctuaryCounter = 0;
219
+ let protectedText = text;
220
+ const protectedRegions = [];
221
+ const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
222
+ let linkMatch;
223
+ while ((linkMatch = linkRegex.exec(text)) !== null) {
224
+ const bracketPos = linkMatch.index + linkMatch[0].indexOf("](");
225
+ const urlStart = bracketPos + 2;
226
+ const urlEnd = urlStart + linkMatch[2].length;
227
+ protectedRegions.push({ start: urlStart, end: urlEnd });
228
+ }
229
+ const codeRegex = new RegExp("(?<!`)(`+)(?!`)((?:(?!\\1).)+?)(\\1)(?!`)", "g");
230
+ let codeMatch;
231
+ const codeMatches = [];
232
+ while ((codeMatch = codeRegex.exec(text)) !== null) {
233
+ const codeStart = codeMatch.index;
234
+ const codeEnd = codeMatch.index + codeMatch[0].length;
235
+ const inProtectedRegion = protectedRegions.some(
236
+ (region) => codeStart >= region.start && codeEnd <= region.end
237
+ );
238
+ if (!inProtectedRegion) {
239
+ codeMatches.push({
240
+ match: codeMatch[0],
241
+ index: codeMatch.index,
242
+ openTicks: codeMatch[1],
243
+ content: codeMatch[2],
244
+ closeTicks: codeMatch[3]
245
+ });
246
+ }
247
+ }
248
+ codeMatches.sort((a, b) => b.index - a.index);
249
+ codeMatches.forEach((codeInfo) => {
250
+ const placeholder = `\uE000${sanctuaryCounter++}\uE001`;
251
+ sanctuaries.set(placeholder, {
252
+ type: "code",
253
+ original: codeInfo.match,
254
+ openTicks: codeInfo.openTicks,
255
+ content: codeInfo.content,
256
+ closeTicks: codeInfo.closeTicks
257
+ });
258
+ protectedText = protectedText.substring(0, codeInfo.index) + placeholder + protectedText.substring(codeInfo.index + codeInfo.match.length);
224
259
  });
225
- html = this.parseLinks(html);
226
- html = html.replace(/(<a[^>]*>.*?<\/a>)/g, (match) => {
227
- const placeholder = `\uE000${sanctuaries.size}\uE001`;
228
- sanctuaries.set(placeholder, match);
260
+ protectedText = protectedText.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => {
261
+ const placeholder = `\uE000${sanctuaryCounter++}\uE001`;
262
+ sanctuaries.set(placeholder, {
263
+ type: "link",
264
+ original: match,
265
+ linkText,
266
+ url
267
+ });
229
268
  return placeholder;
230
269
  });
270
+ return { protectedText, sanctuaries };
271
+ }
272
+ /**
273
+ * Restore and transform sanctuaries back to HTML
274
+ * @param {string} html - HTML with sanctuary placeholders
275
+ * @param {Map} sanctuaries - Map of sanctuaries to restore
276
+ * @returns {string} HTML with sanctuaries restored and transformed
277
+ */
278
+ static restoreAndTransformSanctuaries(html, sanctuaries) {
279
+ const placeholders = Array.from(sanctuaries.keys()).sort((a, b) => {
280
+ const indexA = html.indexOf(a);
281
+ const indexB = html.indexOf(b);
282
+ return indexA - indexB;
283
+ });
284
+ placeholders.forEach((placeholder) => {
285
+ const sanctuary = sanctuaries.get(placeholder);
286
+ let replacement;
287
+ if (sanctuary.type === "code") {
288
+ replacement = `<code><span class="syntax-marker">${sanctuary.openTicks}</span>${this.escapeHtml(sanctuary.content)}<span class="syntax-marker">${sanctuary.closeTicks}</span></code>`;
289
+ } else if (sanctuary.type === "link") {
290
+ let processedLinkText = sanctuary.linkText;
291
+ sanctuaries.forEach((innerSanctuary, innerPlaceholder) => {
292
+ if (processedLinkText.includes(innerPlaceholder)) {
293
+ if (innerSanctuary.type === "code") {
294
+ const codeHtml = `<code><span class="syntax-marker">${innerSanctuary.openTicks}</span>${this.escapeHtml(innerSanctuary.content)}<span class="syntax-marker">${innerSanctuary.closeTicks}</span></code>`;
295
+ processedLinkText = processedLinkText.replace(innerPlaceholder, codeHtml);
296
+ }
297
+ }
298
+ });
299
+ processedLinkText = this.parseStrikethrough(processedLinkText);
300
+ processedLinkText = this.parseBold(processedLinkText);
301
+ processedLinkText = this.parseItalic(processedLinkText);
302
+ const anchorName = `--link-${this.linkIndex++}`;
303
+ const safeUrl = this.sanitizeUrl(sanctuary.url);
304
+ 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>`;
305
+ }
306
+ html = html.replace(placeholder, replacement);
307
+ });
308
+ return html;
309
+ }
310
+ /**
311
+ * Parse all inline elements in correct order
312
+ * @param {string} text - Text with potential inline markdown
313
+ * @returns {string} HTML with all inline styling
314
+ */
315
+ static parseInlineElements(text) {
316
+ const { protectedText, sanctuaries } = this.identifyAndProtectSanctuaries(text);
317
+ let html = protectedText;
231
318
  html = this.parseStrikethrough(html);
232
319
  html = this.parseBold(html);
233
320
  html = this.parseItalic(html);
234
- sanctuaries.forEach((content, placeholder) => {
235
- html = html.replace(placeholder, content);
236
- });
321
+ html = this.restoreAndTransformSanctuaries(html, sanctuaries);
237
322
  return html;
238
323
  }
239
324
  /**
@@ -2523,51 +2608,6 @@ var Toolbar = class {
2523
2608
  this.buttons = {};
2524
2609
  this.buttonConfig = buttonConfig;
2525
2610
  }
2526
- /**
2527
- * Check if cursor/selection is inside a markdown link
2528
- * @param {HTMLTextAreaElement} textarea - The textarea element
2529
- * @returns {boolean} True if inside a link
2530
- */
2531
- isInsideLink(textarea) {
2532
- const value = textarea.value;
2533
- const start = textarea.selectionStart;
2534
- const end = textarea.selectionEnd;
2535
- let insideLink = false;
2536
- let openBracket = -1;
2537
- let closeBracket = -1;
2538
- for (let i = start - 1; i >= 0; i--) {
2539
- if (value[i] === "[") {
2540
- openBracket = i;
2541
- break;
2542
- }
2543
- if (value[i] === "\n") {
2544
- break;
2545
- }
2546
- }
2547
- if (openBracket >= 0) {
2548
- for (let i = end; i < value.length - 1; i++) {
2549
- if (value[i] === "]" && value[i + 1] === "(") {
2550
- closeBracket = i;
2551
- break;
2552
- }
2553
- if (value[i] === "\n") {
2554
- break;
2555
- }
2556
- }
2557
- }
2558
- if (openBracket >= 0 && closeBracket >= 0) {
2559
- for (let i = closeBracket + 2; i < value.length; i++) {
2560
- if (value[i] === ")") {
2561
- insideLink = true;
2562
- break;
2563
- }
2564
- if (value[i] === "\n" || value[i] === " ") {
2565
- break;
2566
- }
2567
- }
2568
- }
2569
- return insideLink;
2570
- }
2571
2611
  /**
2572
2612
  * Create and attach toolbar to editor
2573
2613
  */
@@ -2671,9 +2711,6 @@ var Toolbar = class {
2671
2711
  insertLink(textarea);
2672
2712
  break;
2673
2713
  case "toggleCode":
2674
- if (this.isInsideLink(textarea)) {
2675
- return;
2676
- }
2677
2714
  toggleCode(textarea);
2678
2715
  break;
2679
2716
  case "toggleBulletList":