overtype 1.0.0
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 +21 -0
- package/README.md +441 -0
- package/dist/overtype.esm.js +2576 -0
- package/dist/overtype.esm.js.map +7 -0
- package/dist/overtype.js +2599 -0
- package/dist/overtype.js.map +7 -0
- package/dist/overtype.min.js +546 -0
- package/package.json +50 -0
- package/src/icons.js +77 -0
- package/src/index.js +4 -0
- package/src/overtype.js +781 -0
- package/src/parser.js +222 -0
- package/src/shortcuts.js +125 -0
- package/src/styles.js +486 -0
- package/src/themes.js +124 -0
- package/src/toolbar.js +221 -0
package/src/parser.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MarkdownParser - Parses markdown into HTML while preserving character alignment
|
|
3
|
+
*
|
|
4
|
+
* Key principles:
|
|
5
|
+
* - Every character must occupy the exact same position as in the textarea
|
|
6
|
+
* - No font-size changes, no padding/margin on inline elements
|
|
7
|
+
* - Markdown tokens remain visible but styled
|
|
8
|
+
*/
|
|
9
|
+
export class MarkdownParser {
|
|
10
|
+
/**
|
|
11
|
+
* Escape HTML special characters
|
|
12
|
+
* @param {string} text - Raw text to escape
|
|
13
|
+
* @returns {string} Escaped HTML-safe text
|
|
14
|
+
*/
|
|
15
|
+
static escapeHtml(text) {
|
|
16
|
+
const map = {
|
|
17
|
+
'&': '&',
|
|
18
|
+
'<': '<',
|
|
19
|
+
'>': '>',
|
|
20
|
+
'"': '"',
|
|
21
|
+
"'": '''
|
|
22
|
+
};
|
|
23
|
+
return text.replace(/[&<>"']/g, m => map[m]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Preserve leading spaces as non-breaking spaces
|
|
28
|
+
* @param {string} html - HTML string
|
|
29
|
+
* @param {string} originalLine - Original line with spaces
|
|
30
|
+
* @returns {string} HTML with preserved indentation
|
|
31
|
+
*/
|
|
32
|
+
static preserveIndentation(html, originalLine) {
|
|
33
|
+
const leadingSpaces = originalLine.match(/^(\s*)/)[1];
|
|
34
|
+
const indentation = leadingSpaces.replace(/ /g, ' ');
|
|
35
|
+
return html.replace(/^\s*/, indentation);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse headers (h1-h3 only)
|
|
40
|
+
* @param {string} html - HTML line to parse
|
|
41
|
+
* @returns {string} Parsed HTML with header styling
|
|
42
|
+
*/
|
|
43
|
+
static parseHeader(html) {
|
|
44
|
+
return html.replace(/^(#{1,3})\s(.+)$/, (match, hashes, content) => {
|
|
45
|
+
const level = hashes.length;
|
|
46
|
+
const levelClasses = ['h1', 'h2', 'h3'];
|
|
47
|
+
return `<span class="header ${levelClasses[level-1]}"><span class="syntax-marker">${hashes}</span> ${content}</span>`;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse horizontal rules
|
|
53
|
+
* @param {string} html - HTML line to parse
|
|
54
|
+
* @returns {string|null} Parsed horizontal rule or null
|
|
55
|
+
*/
|
|
56
|
+
static parseHorizontalRule(html) {
|
|
57
|
+
if (html.match(/^(-{3,}|\*{3,}|_{3,})$/)) {
|
|
58
|
+
return `<div><span class="hr-marker">${html}</span></div>`;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse blockquotes
|
|
65
|
+
* @param {string} html - HTML line to parse
|
|
66
|
+
* @returns {string} Parsed blockquote
|
|
67
|
+
*/
|
|
68
|
+
static parseBlockquote(html) {
|
|
69
|
+
return html.replace(/^> (.+)$/, (match, content) => {
|
|
70
|
+
return `<span class="blockquote"><span class="syntax-marker">></span> ${content}</span>`;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse bullet lists
|
|
76
|
+
* @param {string} html - HTML line to parse
|
|
77
|
+
* @returns {string} Parsed bullet list item
|
|
78
|
+
*/
|
|
79
|
+
static parseBulletList(html) {
|
|
80
|
+
return html.replace(/^((?: )*)([-*])\s(.+)$/, (match, indent, marker, content) => {
|
|
81
|
+
return `${indent}<span class="syntax-marker">${marker}</span> ${content}`;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse numbered lists
|
|
87
|
+
* @param {string} html - HTML line to parse
|
|
88
|
+
* @returns {string} Parsed numbered list item
|
|
89
|
+
*/
|
|
90
|
+
static parseNumberedList(html) {
|
|
91
|
+
return html.replace(/^((?: )*)(\d+\.)\s(.+)$/, (match, indent, marker, content) => {
|
|
92
|
+
return `${indent}<span class="syntax-marker">${marker}</span> ${content}`;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse code blocks (markers only)
|
|
98
|
+
* @param {string} html - HTML line to parse
|
|
99
|
+
* @returns {string|null} Parsed code fence or null
|
|
100
|
+
*/
|
|
101
|
+
static parseCodeBlock(html) {
|
|
102
|
+
if (html.startsWith('```')) {
|
|
103
|
+
return `<div><span class="code-fence">${html}</span></div>`;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parse bold text
|
|
110
|
+
* @param {string} html - HTML with potential bold markdown
|
|
111
|
+
* @returns {string} HTML with bold styling
|
|
112
|
+
*/
|
|
113
|
+
static parseBold(html) {
|
|
114
|
+
html = html.replace(/\*\*(.+?)\*\*/g, '<strong><span class="syntax-marker">**</span>$1<span class="syntax-marker">**</span></strong>');
|
|
115
|
+
html = html.replace(/__(.+?)__/g, '<strong><span class="syntax-marker">__</span>$1<span class="syntax-marker">__</span></strong>');
|
|
116
|
+
return html;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Parse italic text
|
|
121
|
+
* Note: Uses lookbehind assertions - requires modern browsers
|
|
122
|
+
* @param {string} html - HTML with potential italic markdown
|
|
123
|
+
* @returns {string} HTML with italic styling
|
|
124
|
+
*/
|
|
125
|
+
static parseItalic(html) {
|
|
126
|
+
html = html.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em><span class="syntax-marker">*</span>$1<span class="syntax-marker">*</span></em>');
|
|
127
|
+
html = html.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, '<em><span class="syntax-marker">_</span>$1<span class="syntax-marker">_</span></em>');
|
|
128
|
+
return html;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Parse inline code
|
|
133
|
+
* @param {string} html - HTML with potential code markdown
|
|
134
|
+
* @returns {string} HTML with code styling
|
|
135
|
+
*/
|
|
136
|
+
static parseInlineCode(html) {
|
|
137
|
+
return html.replace(/`(.+?)`/g, '<code><span class="syntax-marker">`</span>$1<span class="syntax-marker">`</span></code>');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Parse links
|
|
142
|
+
* @param {string} html - HTML with potential link markdown
|
|
143
|
+
* @returns {string} HTML with link styling
|
|
144
|
+
*/
|
|
145
|
+
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>');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Parse all inline elements in correct order
|
|
151
|
+
* @param {string} text - Text with potential inline markdown
|
|
152
|
+
* @returns {string} HTML with all inline styling
|
|
153
|
+
*/
|
|
154
|
+
static parseInlineElements(text) {
|
|
155
|
+
let html = text;
|
|
156
|
+
// Order matters: parse code first to avoid conflicts
|
|
157
|
+
html = this.parseInlineCode(html);
|
|
158
|
+
html = this.parseLinks(html);
|
|
159
|
+
html = this.parseBold(html);
|
|
160
|
+
html = this.parseItalic(html);
|
|
161
|
+
return html;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Parse a single line of markdown
|
|
166
|
+
* @param {string} line - Raw markdown line
|
|
167
|
+
* @returns {string} Parsed HTML line
|
|
168
|
+
*/
|
|
169
|
+
static parseLine(line) {
|
|
170
|
+
let html = this.escapeHtml(line);
|
|
171
|
+
|
|
172
|
+
// Preserve indentation
|
|
173
|
+
html = this.preserveIndentation(html, line);
|
|
174
|
+
|
|
175
|
+
// Check for block elements first
|
|
176
|
+
const horizontalRule = this.parseHorizontalRule(html);
|
|
177
|
+
if (horizontalRule) return horizontalRule;
|
|
178
|
+
|
|
179
|
+
const codeBlock = this.parseCodeBlock(html);
|
|
180
|
+
if (codeBlock) return codeBlock;
|
|
181
|
+
|
|
182
|
+
// Parse block elements
|
|
183
|
+
html = this.parseHeader(html);
|
|
184
|
+
html = this.parseBlockquote(html);
|
|
185
|
+
html = this.parseBulletList(html);
|
|
186
|
+
html = this.parseNumberedList(html);
|
|
187
|
+
|
|
188
|
+
// Parse inline elements
|
|
189
|
+
html = this.parseInlineElements(html);
|
|
190
|
+
|
|
191
|
+
// Wrap in div to maintain line structure
|
|
192
|
+
if (html.trim() === '') {
|
|
193
|
+
return '<div> </div>';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return `<div>${html}</div>`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Parse full markdown text
|
|
201
|
+
* @param {string} text - Full markdown text
|
|
202
|
+
* @param {number} activeLine - Currently active line index (optional)
|
|
203
|
+
* @param {boolean} showActiveLineRaw - Show raw markdown on active line
|
|
204
|
+
* @returns {string} Parsed HTML
|
|
205
|
+
*/
|
|
206
|
+
static parse(text, activeLine = -1, showActiveLineRaw = false) {
|
|
207
|
+
const lines = text.split('\n');
|
|
208
|
+
const parsedLines = lines.map((line, index) => {
|
|
209
|
+
// Show raw markdown on active line if requested
|
|
210
|
+
if (showActiveLineRaw && index === activeLine) {
|
|
211
|
+
const content = this.escapeHtml(line) || ' ';
|
|
212
|
+
return `<div class="raw-line">${content}</div>`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Otherwise, parse the markdown normally
|
|
216
|
+
return this.parseLine(line);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Join without newlines to prevent extra spacing
|
|
220
|
+
return parsedLines.join('');
|
|
221
|
+
}
|
|
222
|
+
}
|
package/src/shortcuts.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard shortcuts handler for OverType editor
|
|
3
|
+
* Uses the same handleAction method as toolbar for consistency
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as markdownActions from 'markdown-actions';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ShortcutsManager - Handles keyboard shortcuts for the editor
|
|
10
|
+
*/
|
|
11
|
+
export class ShortcutsManager {
|
|
12
|
+
constructor(editor) {
|
|
13
|
+
this.editor = editor;
|
|
14
|
+
this.textarea = editor.textarea;
|
|
15
|
+
// No need to add our own listener - OverType will call handleKeydown
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Handle keydown events - called by OverType
|
|
20
|
+
* @param {KeyboardEvent} event - The keyboard event
|
|
21
|
+
* @returns {boolean} Whether the event was handled
|
|
22
|
+
*/
|
|
23
|
+
handleKeydown(event) {
|
|
24
|
+
const isMac = navigator.platform.toLowerCase().includes('mac');
|
|
25
|
+
const modKey = isMac ? event.metaKey : event.ctrlKey;
|
|
26
|
+
|
|
27
|
+
if (!modKey) return false;
|
|
28
|
+
|
|
29
|
+
let action = null;
|
|
30
|
+
|
|
31
|
+
// Map keyboard shortcuts to toolbar actions
|
|
32
|
+
switch(event.key.toLowerCase()) {
|
|
33
|
+
case 'b':
|
|
34
|
+
if (!event.shiftKey) {
|
|
35
|
+
action = 'toggleBold';
|
|
36
|
+
}
|
|
37
|
+
break;
|
|
38
|
+
|
|
39
|
+
case 'i':
|
|
40
|
+
if (!event.shiftKey) {
|
|
41
|
+
action = 'toggleItalic';
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
|
|
45
|
+
case 'k':
|
|
46
|
+
if (!event.shiftKey) {
|
|
47
|
+
action = 'insertLink';
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
case '7':
|
|
52
|
+
if (event.shiftKey) {
|
|
53
|
+
action = 'toggleNumberedList';
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
|
|
57
|
+
case '8':
|
|
58
|
+
if (event.shiftKey) {
|
|
59
|
+
action = 'toggleBulletList';
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// If we have an action, handle it exactly like the toolbar does
|
|
65
|
+
if (action) {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
|
|
68
|
+
// If toolbar exists, use its handleAction method (exact same code path)
|
|
69
|
+
if (this.editor.toolbar) {
|
|
70
|
+
this.editor.toolbar.handleAction(action);
|
|
71
|
+
} else {
|
|
72
|
+
// Fallback: duplicate the toolbar's handleAction logic
|
|
73
|
+
this.handleAction(action);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handle action - fallback when no toolbar exists
|
|
84
|
+
* This duplicates toolbar.handleAction for consistency
|
|
85
|
+
*/
|
|
86
|
+
async handleAction(action) {
|
|
87
|
+
const textarea = this.textarea;
|
|
88
|
+
if (!textarea) return;
|
|
89
|
+
|
|
90
|
+
// Focus textarea
|
|
91
|
+
textarea.focus();
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
switch (action) {
|
|
95
|
+
case 'toggleBold':
|
|
96
|
+
markdownActions.toggleBold(textarea);
|
|
97
|
+
break;
|
|
98
|
+
case 'toggleItalic':
|
|
99
|
+
markdownActions.toggleItalic(textarea);
|
|
100
|
+
break;
|
|
101
|
+
case 'insertLink':
|
|
102
|
+
markdownActions.insertLink(textarea);
|
|
103
|
+
break;
|
|
104
|
+
case 'toggleBulletList':
|
|
105
|
+
markdownActions.toggleBulletList(textarea);
|
|
106
|
+
break;
|
|
107
|
+
case 'toggleNumberedList':
|
|
108
|
+
markdownActions.toggleNumberedList(textarea);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Trigger input event to update preview
|
|
113
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error('Error in markdown action:', error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Cleanup
|
|
121
|
+
*/
|
|
122
|
+
destroy() {
|
|
123
|
+
// Nothing to clean up since we don't add our own listener
|
|
124
|
+
}
|
|
125
|
+
}
|