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/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
+ '<': '&lt;',
19
+ '>': '&gt;',
20
+ '"': '&quot;',
21
+ "'": '&#39;'
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, '&nbsp;');
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(/^&gt; (.+)$/, (match, content) => {
70
+ return `<span class="blockquote"><span class="syntax-marker">&gt;</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(/^((?:&nbsp;)*)([-*])\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(/^((?:&nbsp;)*)(\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>&nbsp;</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) || '&nbsp;';
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
+ }
@@ -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
+ }