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 +1 -1
- package/README.md +26 -0
- package/dist/overtype.esm.js +202 -2
- package/dist/overtype.esm.js.map +3 -3
- package/dist/overtype.js +202 -2
- package/dist/overtype.js.map +3 -3
- package/dist/overtype.min.js +89 -44
- package/package.json +1 -1
- package/src/link-tooltip.js +195 -0
- package/src/overtype.js +50 -1
- package/src/parser.js +36 -1
- package/src/styles.js +2 -0
|
@@ -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
|
-
//
|
|
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,
|
|
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
|
|