overtype 1.0.6 → 1.1.1

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Demo User
3
+ Copyright (c) 2025 David Miranda
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -383,6 +383,27 @@ Check the `examples` folder for complete examples:
383
383
  - `custom-theme.html` - Theme customization
384
384
  - `dynamic.html` - Dynamic creation/destruction
385
385
 
386
+ ## Limitations
387
+
388
+ Due to the transparent textarea overlay approach, OverType has some intentional design limitations:
389
+
390
+ ### Images Not Supported
391
+ Images (`![alt](url)`) are not rendered. Variable-height images would break the character alignment between textarea and preview.
392
+
393
+ ### Monospace Font Required
394
+ All text must use a monospace font to maintain alignment. Variable-width fonts would cause the textarea cursor position to drift from the visual text position.
395
+
396
+ ### Fixed Font Size
397
+ All content must use the same font size. Different sizes for headers or other elements would break vertical alignment.
398
+
399
+ ### Visible Markdown Syntax
400
+ All markdown formatting characters remain visible (e.g., `**bold**` shows the asterisks). This is intentional - hiding them would break the 1:1 character mapping.
401
+
402
+ ### Links Require Modifier Key
403
+ Links are clickable with Cmd/Ctrl+Click only. Direct clicking would interfere with text editing since clicks need to position the cursor in the textarea.
404
+
405
+ These limitations are what enable OverType's core benefits: perfect native textarea behavior, tiny size, and zero complexity.
406
+
386
407
  ## Development
387
408
 
388
409
  ```bash
@@ -431,6 +452,11 @@ OverType uses a unique invisible textarea overlay approach:
431
452
  - Textarea content drives everything
432
453
  - One-way data flow: textarea → parser → preview
433
454
 
455
+ ## Contributors
456
+
457
+ Special thanks to:
458
+ - [Josh Doman](https://github.com/joshdoman) - Fixed inline code formatting preservation ([#6](https://github.com/panphora/overtype/pull/6))
459
+
434
460
  ## License
435
461
 
436
462
  MIT
@@ -1,5 +1,5 @@
1
1
  /**
2
- * OverType v1.0.6
2
+ * OverType v1.1.0
3
3
  * A lightweight markdown editor library with perfect WYSIWYG alignment
4
4
  * @license MIT
5
5
  * @author Demo User
@@ -14,6 +14,12 @@ var __publicField = (obj, key, value) => {
14
14
 
15
15
  // src/parser.js
16
16
  var MarkdownParser = class {
17
+ /**
18
+ * Reset link index (call before parsing a new document)
19
+ */
20
+ static resetLinkIndex() {
21
+ this.linkIndex = 0;
22
+ }
17
23
  /**
18
24
  * Escape HTML special characters
19
25
  * @param {string} text - Raw text to escape
@@ -139,7 +145,10 @@ var MarkdownParser = class {
139
145
  * @returns {string} HTML with link styling
140
146
  */
141
147
  static parseLinks(html) {
142
- return html.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2"><span class="syntax-marker">[</span>$1<span class="syntax-marker">](</span><span class="syntax-marker">$2</span><span class="syntax-marker">)</span></a>');
148
+ return html.replace(/\[(.+?)\]\((.+?)\)/g, (match, text, url) => {
149
+ const anchorName = `--link-${this.linkIndex++}`;
150
+ return `<a href="${url}" style="anchor-name: ${anchorName}"><span class="syntax-marker">[</span>${text}<span class="syntax-marker">](</span><span class="syntax-marker">${url}</span><span class="syntax-marker">)</span></a>`;
151
+ });
143
152
  }
144
153
  /**
145
154
  * Parse all inline elements in correct order
@@ -149,9 +158,18 @@ var MarkdownParser = class {
149
158
  static parseInlineElements(text) {
150
159
  let html = text;
151
160
  html = this.parseInlineCode(html);
161
+ const codeBlocks = /* @__PURE__ */ new Map();
162
+ html = html.replace(/(<code>.*?<\/code>)/g, (match) => {
163
+ const placeholder = `\uE000${codeBlocks.size}\uE001`;
164
+ codeBlocks.set(placeholder, match);
165
+ return placeholder;
166
+ });
152
167
  html = this.parseLinks(html);
153
168
  html = this.parseBold(html);
154
169
  html = this.parseItalic(html);
170
+ codeBlocks.forEach((codeBlock, placeholder) => {
171
+ html = html.replace(placeholder, codeBlock);
172
+ });
155
173
  return html;
156
174
  }
157
175
  /**
@@ -186,6 +204,7 @@ var MarkdownParser = class {
186
204
  * @returns {string} Parsed HTML
187
205
  */
188
206
  static parse(text, activeLine = -1, showActiveLineRaw = false) {
207
+ this.resetLinkIndex();
189
208
  const lines = text.split("\n");
190
209
  const parsedLines = lines.map((line, index) => {
191
210
  if (showActiveLineRaw && index === activeLine) {
@@ -197,6 +216,8 @@ var MarkdownParser = class {
197
216
  return parsedLines.join("");
198
217
  }
199
218
  };
219
+ // Track link index for anchor naming
220
+ __publicField(MarkdownParser, "linkIndex", 0);
200
221
 
201
222
  // node_modules/markdown-actions/dist/markdown-actions.esm.js
202
223
  var __defProp2 = Object.defineProperty;
@@ -1487,6 +1508,8 @@ function generateStyles(options = {}) {
1487
1508
  padding: 0 !important;
1488
1509
  border-radius: 2px !important;
1489
1510
  font-family: inherit !important;
1511
+ font-size: inherit !important;
1512
+ line-height: inherit !important;
1490
1513
  font-weight: normal !important;
1491
1514
  }
1492
1515
 
@@ -1962,6 +1985,152 @@ var Toolbar = class {
1962
1985
  }
1963
1986
  };
1964
1987
 
1988
+ // src/link-tooltip.js
1989
+ var LinkTooltip = class {
1990
+ constructor(editor) {
1991
+ this.editor = editor;
1992
+ this.tooltip = null;
1993
+ this.currentLink = null;
1994
+ this.hideTimeout = null;
1995
+ this.init();
1996
+ }
1997
+ init() {
1998
+ const supportsAnchor = CSS.supports("position-anchor: --x") && CSS.supports("position-area: center");
1999
+ if (!supportsAnchor) {
2000
+ return;
2001
+ }
2002
+ this.createTooltip();
2003
+ this.editor.textarea.addEventListener("selectionchange", () => this.checkCursorPosition());
2004
+ this.editor.textarea.addEventListener("keyup", (e) => {
2005
+ if (e.key.includes("Arrow") || e.key === "Home" || e.key === "End") {
2006
+ this.checkCursorPosition();
2007
+ }
2008
+ });
2009
+ this.editor.textarea.addEventListener("input", () => this.hide());
2010
+ this.editor.textarea.addEventListener("scroll", () => this.hide());
2011
+ this.tooltip.addEventListener("mouseenter", () => this.cancelHide());
2012
+ this.tooltip.addEventListener("mouseleave", () => this.scheduleHide());
2013
+ }
2014
+ createTooltip() {
2015
+ this.tooltip = document.createElement("div");
2016
+ this.tooltip.className = "overtype-link-tooltip";
2017
+ const tooltipStyles = document.createElement("style");
2018
+ tooltipStyles.textContent = `
2019
+ @supports (position-anchor: --x) and (position-area: center) {
2020
+ .overtype-link-tooltip {
2021
+ position: absolute;
2022
+ position-anchor: var(--target-anchor, --link-0);
2023
+ position-area: block-end center;
2024
+ margin-top: 8px;
2025
+
2026
+ background: #333;
2027
+ color: white;
2028
+ padding: 6px 10px;
2029
+ border-radius: 16px;
2030
+ font-size: 12px;
2031
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
2032
+ display: none;
2033
+ z-index: 10000;
2034
+ cursor: pointer;
2035
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
2036
+ max-width: 300px;
2037
+ white-space: nowrap;
2038
+ overflow: hidden;
2039
+ text-overflow: ellipsis;
2040
+
2041
+ position-try: most-width block-end inline-end, flip-inline, block-start center;
2042
+ position-visibility: anchors-visible;
2043
+ }
2044
+
2045
+ .overtype-link-tooltip.visible {
2046
+ display: flex;
2047
+ }
2048
+ }
2049
+ `;
2050
+ document.head.appendChild(tooltipStyles);
2051
+ this.tooltip.innerHTML = `
2052
+ <span style="display: flex; align-items: center; gap: 6px;">
2053
+ <svg width="12" height="12" viewBox="0 0 20 20" fill="currentColor" style="flex-shrink: 0;">
2054
+ <path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"></path>
2055
+ <path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"></path>
2056
+ </svg>
2057
+ <span class="overtype-link-tooltip-url"></span>
2058
+ </span>
2059
+ `;
2060
+ this.tooltip.addEventListener("click", (e) => {
2061
+ e.preventDefault();
2062
+ e.stopPropagation();
2063
+ if (this.currentLink) {
2064
+ window.open(this.currentLink.url, "_blank");
2065
+ this.hide();
2066
+ }
2067
+ });
2068
+ this.editor.container.appendChild(this.tooltip);
2069
+ }
2070
+ checkCursorPosition() {
2071
+ const cursorPos = this.editor.textarea.selectionStart;
2072
+ const text = this.editor.textarea.value;
2073
+ const linkInfo = this.findLinkAtPosition(text, cursorPos);
2074
+ if (linkInfo) {
2075
+ if (!this.currentLink || this.currentLink.url !== linkInfo.url || this.currentLink.index !== linkInfo.index) {
2076
+ this.show(linkInfo);
2077
+ }
2078
+ } else {
2079
+ this.scheduleHide();
2080
+ }
2081
+ }
2082
+ findLinkAtPosition(text, position) {
2083
+ const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
2084
+ let match;
2085
+ let linkIndex = 0;
2086
+ while ((match = linkRegex.exec(text)) !== null) {
2087
+ const start = match.index;
2088
+ const end = match.index + match[0].length;
2089
+ if (position >= start && position <= end) {
2090
+ return {
2091
+ text: match[1],
2092
+ url: match[2],
2093
+ index: linkIndex,
2094
+ start,
2095
+ end
2096
+ };
2097
+ }
2098
+ linkIndex++;
2099
+ }
2100
+ return null;
2101
+ }
2102
+ show(linkInfo) {
2103
+ this.currentLink = linkInfo;
2104
+ this.cancelHide();
2105
+ const urlSpan = this.tooltip.querySelector(".overtype-link-tooltip-url");
2106
+ urlSpan.textContent = linkInfo.url;
2107
+ this.tooltip.style.setProperty("--target-anchor", `--link-${linkInfo.index}`);
2108
+ this.tooltip.classList.add("visible");
2109
+ }
2110
+ hide() {
2111
+ this.tooltip.classList.remove("visible");
2112
+ this.currentLink = null;
2113
+ }
2114
+ scheduleHide() {
2115
+ this.cancelHide();
2116
+ this.hideTimeout = setTimeout(() => this.hide(), 300);
2117
+ }
2118
+ cancelHide() {
2119
+ if (this.hideTimeout) {
2120
+ clearTimeout(this.hideTimeout);
2121
+ this.hideTimeout = null;
2122
+ }
2123
+ }
2124
+ destroy() {
2125
+ this.cancelHide();
2126
+ if (this.tooltip && this.tooltip.parentNode) {
2127
+ this.tooltip.parentNode.removeChild(this.tooltip);
2128
+ }
2129
+ this.tooltip = null;
2130
+ this.currentLink = null;
2131
+ }
2132
+ };
2133
+
1965
2134
  // src/overtype.js
1966
2135
  var _OverType = class _OverType {
1967
2136
  /**
@@ -2020,6 +2189,7 @@ var _OverType = class _OverType {
2020
2189
  this._buildFromScratch();
2021
2190
  }
2022
2191
  this.shortcuts = new ShortcutsManager(this);
2192
+ this.linkTooltip = new LinkTooltip(this);
2023
2193
  if (this.options.toolbar) {
2024
2194
  this.toolbar = new Toolbar(this);
2025
2195
  this.toolbar.create();
@@ -2290,6 +2460,36 @@ var _OverType = class _OverType {
2290
2460
  * @private
2291
2461
  */
2292
2462
  handleKeydown(event) {
2463
+ if (event.key === "Tab") {
2464
+ event.preventDefault();
2465
+ const start = this.textarea.selectionStart;
2466
+ const end = this.textarea.selectionEnd;
2467
+ const value = this.textarea.value;
2468
+ if (start !== end && event.shiftKey) {
2469
+ const before = value.substring(0, start);
2470
+ const selection = value.substring(start, end);
2471
+ const after = value.substring(end);
2472
+ const lines = selection.split("\n");
2473
+ const outdented = lines.map((line) => line.replace(/^ /, "")).join("\n");
2474
+ this.textarea.value = before + outdented + after;
2475
+ this.textarea.selectionStart = start;
2476
+ this.textarea.selectionEnd = start + outdented.length;
2477
+ } else if (start !== end) {
2478
+ const before = value.substring(0, start);
2479
+ const selection = value.substring(start, end);
2480
+ const after = value.substring(end);
2481
+ const lines = selection.split("\n");
2482
+ const indented = lines.map((line) => " " + line).join("\n");
2483
+ this.textarea.value = before + indented + after;
2484
+ this.textarea.selectionStart = start;
2485
+ this.textarea.selectionEnd = start + indented.length;
2486
+ } else {
2487
+ this.textarea.value = value.substring(0, start) + " " + value.substring(end);
2488
+ this.textarea.selectionStart = this.textarea.selectionEnd = start + 2;
2489
+ }
2490
+ this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
2491
+ return;
2492
+ }
2293
2493
  const handled = this.shortcuts.handleKeydown(event);
2294
2494
  if (!handled && this.options.onKeydown) {
2295
2495
  this.options.onKeydown(event, this);