-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.js
More file actions
299 lines (254 loc) · 8.32 KB
/
content.js
File metadata and controls
299 lines (254 loc) · 8.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
let selectedText = '';
let currentIndex = 0;
let isTypingMode = false;
let originalRange = null;
let isCurrentCharacterIncorrect = false;
let originalTextNode = null;
document.addEventListener('enableTypingMode', () => {
isTypingMode = true;
const selection = window.getSelection();
if (selection.toString().trim()) {
selectedText = selection.toString();
// Store the original range before any modifications
originalRange = selection.getRangeAt(0).cloneRange();
// Store the original text content
originalTextNode = originalRange.cloneContents();
currentIndex = 0;
isCurrentCharacterIncorrect = false;
// First, delete the original content
originalRange.deleteContents();
// Then clear the selection
selection.removeAllRanges();
// Finally, set up our typing interface
highlightText();
}
});
document.addEventListener('mouseup', (e) => {
if (isTypingMode) {
e.preventDefault();
e.stopPropagation();
return false;
}
const selection = window.getSelection();
if (selection.toString().trim()) {
originalRange = selection.getRangeAt(0).cloneRange();
}
});
document.addEventListener('mousedown', (e) => {
if (isTypingMode) {
e.preventDefault();
e.stopPropagation();
return false;
}
});
document.addEventListener('keydown', (e) => {
if (!isTypingMode) return;
if (e.key === 'Escape') {
exitTypingMode();
return;
}
// Special case for ellipsis (both ... and the Unicode character …)
if (e.key === '.' || e.key === ' ') {
// Check if we're at a Unicode ellipsis character (U+2026)
if (selectedText.charAt(currentIndex) === '…') {
currentIndex++;
isCurrentCharacterIncorrect = false;
highlightText();
e.preventDefault();
return;
}
// Check if we're at the start of a three-dot ellipsis
const textAhead = selectedText.substring(currentIndex, currentIndex + 3);
if (textAhead === '...') {
currentIndex += 3;
isCurrentCharacterIncorrect = false;
highlightText();
e.preventDefault();
return;
}
}
if (e.key === ' ') {
e.preventDefault();
}
});
document.addEventListener('keyup', (e) => {
if (!isTypingMode || !selectedText) return;
if (e.key === 'Escape') {
exitTypingMode();
return;
}
if (e.key === ' ') {
// Special case for Unicode ellipsis character (U+2026)
if (selectedText.charAt(currentIndex) === '…') {
currentIndex++;
isCurrentCharacterIncorrect = false;
highlightText();
return;
}
// Special case for three-dot ellipsis
const textAhead = selectedText.substring(currentIndex, currentIndex + 3);
if (textAhead === '...') {
currentIndex += 3;
isCurrentCharacterIncorrect = false;
highlightText();
return;
}
let expectedChar = selectedText[currentIndex];
// Always allow skipping over skippable characters, even if error is active
if (
expectedChar === ' ' ||
expectedChar === '.' ||
expectedChar === '…' || // Added Unicode ellipsis
/\s/.test(expectedChar)
) {
while (currentIndex < selectedText.length) {
expectedChar = selectedText[currentIndex];
if (
expectedChar === ' ' ||
expectedChar === '.' ||
expectedChar === '…' || // Added Unicode ellipsis
/\s/.test(expectedChar)
) {
currentIndex++;
} else {
break;
}
}
isCurrentCharacterIncorrect = false;
highlightText();
}
}
});
document.addEventListener('keypress', (e) => {
if (!isTypingMode || !selectedText) return;
// Special case for ellipsis
if (e.key === '.' || e.key === ' ') {
// Check for Unicode ellipsis character
if (selectedText.charAt(currentIndex) === '…') {
currentIndex++;
isCurrentCharacterIncorrect = false;
highlightText();
return;
}
// Check for three-dot ellipsis
const textAhead = selectedText.substring(currentIndex, currentIndex + 3);
if (textAhead === '...') {
currentIndex += 3;
isCurrentCharacterIncorrect = false;
highlightText();
return;
}
}
const expectedChar = selectedText[currentIndex];
const typedChar = e.key;
// Normalize apostrophes and quotes
const normalizeChar = (char) => {
const code = char.charCodeAt(0);
// Map of apostrophe-like character codes
const apostropheCodes = [
0x0027, // Straight single quote '
0x2019, // Right single quotation '
0x2018, // Left single quotation '
0x2032, // Prime ′
0x0060, // Backtick `
0x2035, // Reversed prime ‵
0x02b9, // Modifier letter prime ʹ
0x02bb, // Modifier letter turned comma ʻ
];
// Map of double quote character codes
const doubleQuoteCodes = [
0x0022, // Straight double quote "
0x201c, // Left double quote "
0x201d, // Right double quote "
0x2033, // Double prime ″
0x02dd, // Double acute accent ˝
0x3003, // Ditto mark 〃
];
if (apostropheCodes.includes(code)) {
return 0x0027; // Return the code for straight single quote
}
if (doubleQuoteCodes.includes(code)) {
return 0x0022; // Return the code for straight double quote
}
return char.toLowerCase().charCodeAt(0);
};
const normalizedExpected = normalizeChar(expectedChar);
const normalizedTyped = normalizeChar(typedChar);
// Handle quotes and regular characters
if (normalizedTyped === normalizedExpected) {
isCurrentCharacterIncorrect = false;
currentIndex++;
highlightText();
} else {
isCurrentCharacterIncorrect = true;
highlightText();
}
if (currentIndex >= selectedText.length) {
exitTypingMode();
}
});
function restoreOriginalText() {
if (originalTextNode && originalRange) {
const range = originalRange.cloneRange();
range.deleteContents();
range.insertNode(originalTextNode.cloneNode(true));
// Clear selection
window.getSelection().removeAllRanges();
}
}
function highlightText() {
if (!originalRange) return;
// Create a working copy of the original range
const range = originalRange.cloneRange();
// Remove any existing typing containers
const existingContainers = document.querySelectorAll('.typing-container');
existingContainers.forEach((container) => container.remove());
// Create and set up the new container
const container = document.createElement('span');
container.className = 'typing-container';
container.style.whiteSpace = 'pre-wrap';
// Get the original element's computed styles
const originalElement = range.startContainer.parentElement;
const computedStyle = window.getComputedStyle(originalElement);
// Copy relevant styles from the original element
container.style.fontFamily = computedStyle.fontFamily;
container.style.fontWeight = computedStyle.fontWeight;
container.style.fontStyle = computedStyle.fontStyle;
container.style.lineHeight = computedStyle.lineHeight;
container.style.letterSpacing = computedStyle.letterSpacing;
container.style.wordSpacing = computedStyle.wordSpacing;
const typedSpan = document.createElement('span');
typedSpan.className = 'typed';
typedSpan.textContent = selectedText.substring(0, currentIndex);
const currentSpan = document.createElement('span');
currentSpan.className = isCurrentCharacterIncorrect ? 'incorrect' : 'current';
currentSpan.textContent = selectedText.substring(
currentIndex,
currentIndex + 1
);
const untypedSpan = document.createElement('span');
untypedSpan.className = 'untyped';
untypedSpan.textContent = selectedText.substring(currentIndex + 1);
container.appendChild(typedSpan);
container.appendChild(currentSpan);
container.appendChild(untypedSpan);
// Clear the range contents and insert our container
range.deleteContents();
range.insertNode(container);
}
// Add new function to handle exiting typing mode
function exitTypingMode() {
if (!isTypingMode) return;
isTypingMode = false;
currentIndex = 0;
isCurrentCharacterIncorrect = false;
// Remove any existing typing containers from the page
const existingContainers = document.querySelectorAll('.typing-container');
existingContainers.forEach((container) => container.remove());
// Restore the original text
restoreOriginalText();
// Reset all variables
selectedText = '';
originalRange = null;
originalTextNode = null;
}