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 +13 -23
- package/dist/overtype.cjs +104 -67
- package/dist/overtype.cjs.map +2 -2
- package/dist/overtype.esm.js +104 -67
- package/dist/overtype.esm.js.map +2 -2
- package/dist/overtype.js +104 -67
- package/dist/overtype.js.map +2 -2
- package/dist/overtype.min.js +46 -49
- package/package.json +3 -2
- package/src/parser.js +145 -32
- package/src/toolbar.js +0 -59
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. ~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** - ~
|
|
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** | ~
|
|
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
|
-
##
|
|
578
|
-
|
|
579
|
-
### Synesthesia
|
|
577
|
+
## Contributing
|
|
580
578
|
|
|
581
|
-
|
|
579
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
582
580
|
|
|
583
|
-
|
|
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
|
-
|
|
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
|
-
|
|
585
|
+
---
|
|
596
586
|
|
|
597
|
-
|
|
587
|
+
**Ready for another radical idea?**
|
|
588
|
+
Let's remove every layer of the web application stack.
|
|
598
589
|
|
|
599
|
-
|
|
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
|
-
|
|
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.
|
|
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("(
|
|
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
|
-
*
|
|
213
|
-
* @param {string} text - Text with potential
|
|
214
|
-
* @returns {
|
|
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
|
|
217
|
-
let html = text;
|
|
218
|
-
html = this.parseInlineCode(html);
|
|
216
|
+
static identifyAndProtectSanctuaries(text) {
|
|
219
217
|
const sanctuaries = /* @__PURE__ */ new Map();
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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":
|