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.
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Link Tooltip - CSS Anchor Positioning with index-based anchors
3
+ * Shows a clickable tooltip when cursor is within a link
4
+ * Uses CSS anchor positioning with dynamically selected anchor
5
+ */
6
+
7
+ export class LinkTooltip {
8
+ constructor(editor) {
9
+ this.editor = editor;
10
+ this.tooltip = null;
11
+ this.currentLink = null;
12
+ this.hideTimeout = null;
13
+
14
+ this.init();
15
+ }
16
+
17
+ init() {
18
+ // Check for CSS anchor positioning support
19
+ const supportsAnchor =
20
+ CSS.supports('position-anchor: --x') &&
21
+ CSS.supports('position-area: center');
22
+
23
+ if (!supportsAnchor) {
24
+ // Don't show anything if not supported
25
+ return;
26
+ }
27
+
28
+ // Create tooltip element
29
+ this.createTooltip();
30
+
31
+ // Listen for cursor position changes
32
+ this.editor.textarea.addEventListener('selectionchange', () => this.checkCursorPosition());
33
+ this.editor.textarea.addEventListener('keyup', (e) => {
34
+ if (e.key.includes('Arrow') || e.key === 'Home' || e.key === 'End') {
35
+ this.checkCursorPosition();
36
+ }
37
+ });
38
+
39
+ // Hide tooltip when typing or scrolling
40
+ this.editor.textarea.addEventListener('input', () => this.hide());
41
+ this.editor.textarea.addEventListener('scroll', () => this.hide());
42
+
43
+ // Keep tooltip visible on hover
44
+ this.tooltip.addEventListener('mouseenter', () => this.cancelHide());
45
+ this.tooltip.addEventListener('mouseleave', () => this.scheduleHide());
46
+ }
47
+
48
+ createTooltip() {
49
+ // Create tooltip element
50
+ this.tooltip = document.createElement('div');
51
+ this.tooltip.className = 'overtype-link-tooltip';
52
+
53
+ // Add CSS anchor positioning styles
54
+ const tooltipStyles = document.createElement('style');
55
+ tooltipStyles.textContent = `
56
+ @supports (position-anchor: --x) and (position-area: center) {
57
+ .overtype-link-tooltip {
58
+ position: absolute;
59
+ position-anchor: var(--target-anchor, --link-0);
60
+ position-area: block-end center;
61
+ margin-top: 8px;
62
+
63
+ background: #333;
64
+ color: white;
65
+ padding: 6px 10px;
66
+ border-radius: 16px;
67
+ font-size: 12px;
68
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
69
+ display: none;
70
+ z-index: 10000;
71
+ cursor: pointer;
72
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
73
+ max-width: 300px;
74
+ white-space: nowrap;
75
+ overflow: hidden;
76
+ text-overflow: ellipsis;
77
+
78
+ position-try: most-width block-end inline-end, flip-inline, block-start center;
79
+ position-visibility: anchors-visible;
80
+ }
81
+
82
+ .overtype-link-tooltip.visible {
83
+ display: flex;
84
+ }
85
+ }
86
+ `;
87
+ document.head.appendChild(tooltipStyles);
88
+
89
+ // Add link icon and text container
90
+ this.tooltip.innerHTML = `
91
+ <span style="display: flex; align-items: center; gap: 6px;">
92
+ <svg width="12" height="12" viewBox="0 0 20 20" fill="currentColor" style="flex-shrink: 0;">
93
+ <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>
94
+ <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>
95
+ </svg>
96
+ <span class="overtype-link-tooltip-url"></span>
97
+ </span>
98
+ `;
99
+
100
+ // Click handler to open link
101
+ this.tooltip.addEventListener('click', (e) => {
102
+ e.preventDefault();
103
+ e.stopPropagation();
104
+ if (this.currentLink) {
105
+ window.open(this.currentLink.url, '_blank');
106
+ this.hide();
107
+ }
108
+ });
109
+
110
+ // Append tooltip to editor container
111
+ this.editor.container.appendChild(this.tooltip);
112
+ }
113
+
114
+ checkCursorPosition() {
115
+ const cursorPos = this.editor.textarea.selectionStart;
116
+ const text = this.editor.textarea.value;
117
+
118
+ // Find if cursor is within a markdown link
119
+ const linkInfo = this.findLinkAtPosition(text, cursorPos);
120
+
121
+ if (linkInfo) {
122
+ if (!this.currentLink || this.currentLink.url !== linkInfo.url || this.currentLink.index !== linkInfo.index) {
123
+ this.show(linkInfo);
124
+ }
125
+ } else {
126
+ this.scheduleHide();
127
+ }
128
+ }
129
+
130
+ findLinkAtPosition(text, position) {
131
+ // Regex to find markdown links: [text](url)
132
+ const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
133
+ let match;
134
+ let linkIndex = 0;
135
+
136
+ while ((match = linkRegex.exec(text)) !== null) {
137
+ const start = match.index;
138
+ const end = match.index + match[0].length;
139
+
140
+ if (position >= start && position <= end) {
141
+ return {
142
+ text: match[1],
143
+ url: match[2],
144
+ index: linkIndex,
145
+ start: start,
146
+ end: end
147
+ };
148
+ }
149
+ linkIndex++;
150
+ }
151
+
152
+ return null;
153
+ }
154
+
155
+ show(linkInfo) {
156
+ this.currentLink = linkInfo;
157
+ this.cancelHide();
158
+
159
+ // Update tooltip content
160
+ const urlSpan = this.tooltip.querySelector('.overtype-link-tooltip-url');
161
+ urlSpan.textContent = linkInfo.url;
162
+
163
+ // Set the CSS variable to point to the correct anchor
164
+ this.tooltip.style.setProperty('--target-anchor', `--link-${linkInfo.index}`);
165
+
166
+ // Show tooltip (CSS anchor positioning handles the rest)
167
+ this.tooltip.classList.add('visible');
168
+ }
169
+
170
+ hide() {
171
+ this.tooltip.classList.remove('visible');
172
+ this.currentLink = null;
173
+ }
174
+
175
+ scheduleHide() {
176
+ this.cancelHide();
177
+ this.hideTimeout = setTimeout(() => this.hide(), 300);
178
+ }
179
+
180
+ cancelHide() {
181
+ if (this.hideTimeout) {
182
+ clearTimeout(this.hideTimeout);
183
+ this.hideTimeout = null;
184
+ }
185
+ }
186
+
187
+ destroy() {
188
+ this.cancelHide();
189
+ if (this.tooltip && this.tooltip.parentNode) {
190
+ this.tooltip.parentNode.removeChild(this.tooltip);
191
+ }
192
+ this.tooltip = null;
193
+ this.currentLink = null;
194
+ }
195
+ }
package/src/overtype.js CHANGED
@@ -9,6 +9,7 @@ import { ShortcutsManager } from './shortcuts.js';
9
9
  import { generateStyles } from './styles.js';
10
10
  import { getTheme, mergeTheme, solar, themeToCSSVars } from './themes.js';
11
11
  import { Toolbar } from './toolbar.js';
12
+ import { LinkTooltip } from './link-tooltip.js';
12
13
 
13
14
  /**
14
15
  * OverType Editor Class
@@ -97,6 +98,9 @@ class OverType {
97
98
 
98
99
  // Setup shortcuts manager
99
100
  this.shortcuts = new ShortcutsManager(this);
101
+
102
+ // Setup link tooltip
103
+ this.linkTooltip = new LinkTooltip(this);
100
104
 
101
105
  // Setup toolbar if enabled
102
106
  if (this.options.toolbar) {
@@ -471,7 +475,52 @@ class OverType {
471
475
  * @private
472
476
  */
473
477
  handleKeydown(event) {
474
- // Let shortcuts manager handle it first
478
+ // Handle Tab key to prevent focus loss and insert spaces
479
+ if (event.key === 'Tab') {
480
+ event.preventDefault();
481
+
482
+ // Insert 2 spaces at cursor position
483
+ const start = this.textarea.selectionStart;
484
+ const end = this.textarea.selectionEnd;
485
+ const value = this.textarea.value;
486
+
487
+ // If there's a selection, indent/outdent based on shift key
488
+ if (start !== end && event.shiftKey) {
489
+ // Outdent: remove 2 spaces from start of each selected line
490
+ const before = value.substring(0, start);
491
+ const selection = value.substring(start, end);
492
+ const after = value.substring(end);
493
+
494
+ const lines = selection.split('\n');
495
+ const outdented = lines.map(line => line.replace(/^ /, '')).join('\n');
496
+
497
+ this.textarea.value = before + outdented + after;
498
+ this.textarea.selectionStart = start;
499
+ this.textarea.selectionEnd = start + outdented.length;
500
+ } else if (start !== end) {
501
+ // Indent: add 2 spaces to start of each selected line
502
+ const before = value.substring(0, start);
503
+ const selection = value.substring(start, end);
504
+ const after = value.substring(end);
505
+
506
+ const lines = selection.split('\n');
507
+ const indented = lines.map(line => ' ' + line).join('\n');
508
+
509
+ this.textarea.value = before + indented + after;
510
+ this.textarea.selectionStart = start;
511
+ this.textarea.selectionEnd = start + indented.length;
512
+ } else {
513
+ // No selection: just insert 2 spaces
514
+ this.textarea.value = value.substring(0, start) + ' ' + value.substring(end);
515
+ this.textarea.selectionStart = this.textarea.selectionEnd = start + 2;
516
+ }
517
+
518
+ // Trigger input event to update preview
519
+ this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
520
+ return;
521
+ }
522
+
523
+ // Let shortcuts manager handle other keys
475
524
  const handled = this.shortcuts.handleKeydown(event);
476
525
 
477
526
  // Call user callback if provided
package/src/parser.js CHANGED
@@ -7,6 +7,16 @@
7
7
  * - Markdown tokens remain visible but styled
8
8
  */
9
9
  export class MarkdownParser {
10
+ // Track link index for anchor naming
11
+ static linkIndex = 0;
12
+
13
+ /**
14
+ * Reset link index (call before parsing a new document)
15
+ */
16
+ static resetLinkIndex() {
17
+ this.linkIndex = 0;
18
+ }
19
+
10
20
  /**
11
21
  * Escape HTML special characters
12
22
  * @param {string} text - Raw text to escape
@@ -143,7 +153,10 @@ export class MarkdownParser {
143
153
  * @returns {string} HTML with link styling
144
154
  */
145
155
  static parseLinks(html) {
146
- 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>');
156
+ return html.replace(/\[(.+?)\]\((.+?)\)/g, (match, text, url) => {
157
+ const anchorName = `--link-${this.linkIndex++}`;
158
+ 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>`;
159
+ });
147
160
  }
148
161
 
149
162
  /**
@@ -155,9 +168,28 @@ export class MarkdownParser {
155
168
  let html = text;
156
169
  // Order matters: parse code first to avoid conflicts
157
170
  html = this.parseInlineCode(html);
171
+ // Use placeholders to protect inline code while preserving formatting spans
172
+ // We use Unicode Private Use Area (U+E000-U+F8FF) as placeholders because:
173
+ // 1. These characters are reserved for application-specific use
174
+ // 2. They'll never appear in user text
175
+ // 3. They maintain single-character width (important for alignment)
176
+ // 4. They're invisible if accidentally rendered
177
+ // This allows formatting like *text `code` text* to span across code blocks
178
+ // while preventing formatting inside code like `__init__` from being bolded
179
+ const codeBlocks = new Map();
180
+ html = html.replace(/(<code>.*?<\/code>)/g, (match) => {
181
+ const placeholder = `\uE000${codeBlocks.size}\uE001`;
182
+ codeBlocks.set(placeholder, match);
183
+ return placeholder;
184
+ });
185
+ // Process other inline elements on text with placeholders
158
186
  html = this.parseLinks(html);
159
187
  html = this.parseBold(html);
160
188
  html = this.parseItalic(html);
189
+ // Restore code blocks
190
+ codeBlocks.forEach((codeBlock, placeholder) => {
191
+ html = html.replace(placeholder, codeBlock);
192
+ });
161
193
  return html;
162
194
  }
163
195
 
@@ -204,6 +236,9 @@ export class MarkdownParser {
204
236
  * @returns {string} Parsed HTML
205
237
  */
206
238
  static parse(text, activeLine = -1, showActiveLineRaw = false) {
239
+ // Reset link counter for each parse
240
+ this.resetLinkIndex();
241
+
207
242
  const lines = text.split('\n');
208
243
  const parsedLines = lines.map((line, index) => {
209
244
  // Show raw markdown on active line if requested
package/src/styles.js CHANGED
@@ -235,6 +235,8 @@ export function generateStyles(options = {}) {
235
235
  padding: 0 !important;
236
236
  border-radius: 2px !important;
237
237
  font-family: inherit !important;
238
+ font-size: inherit !important;
239
+ line-height: inherit !important;
238
240
  font-weight: normal !important;
239
241
  }
240
242