@testgorilla/tgo-typing-test 0.0.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.
Files changed (65) hide show
  1. package/.eslintrc.json +45 -0
  2. package/README.md +95 -0
  3. package/jest.config.ts +21 -0
  4. package/ng-package.json +15 -0
  5. package/package.json +20 -0
  6. package/project.json +36 -0
  7. package/src/assets/typing-test-languages/english.json +207 -0
  8. package/src/assets/typing-test-languages/english_punctuation.json +18 -0
  9. package/src/assets/typing-test-languages/quotes/english_version_1.json +35291 -0
  10. package/src/assets/typing-test-languages/quotes/english_version_2.json +720 -0
  11. package/src/assets/typing-test-languages/quotes/filtered_sources.json +79 -0
  12. package/src/index.ts +2 -0
  13. package/src/lib/components/tgo-typing-replay-input/tgo-typing-replay-input.component.html +30 -0
  14. package/src/lib/components/tgo-typing-replay-input/tgo-typing-replay-input.component.spec.ts +250 -0
  15. package/src/lib/components/tgo-typing-replay-input/tgo-typing-replay-input.component.ts +48 -0
  16. package/src/lib/components/tgo-typing-test/tgo-typing-test.component.html +72 -0
  17. package/src/lib/components/tgo-typing-test/tgo-typing-test.component.spec.ts +699 -0
  18. package/src/lib/components/tgo-typing-test/tgo-typing-test.component.ts +294 -0
  19. package/src/lib/helpers/config.ts +28 -0
  20. package/src/lib/helpers/constants/default-config.ts +103 -0
  21. package/src/lib/helpers/controllers/input-controller.ts +710 -0
  22. package/src/lib/helpers/controllers/quotes-controller.ts +183 -0
  23. package/src/lib/helpers/observables/banner-event.ts +18 -0
  24. package/src/lib/helpers/observables/config-event.ts +31 -0
  25. package/src/lib/helpers/observables/timer-event.ts +18 -0
  26. package/src/lib/helpers/states/active-page.ts +9 -0
  27. package/src/lib/helpers/states/composition.ts +29 -0
  28. package/src/lib/helpers/states/page-transition.ts +9 -0
  29. package/src/lib/helpers/states/slow-timer.ts +16 -0
  30. package/src/lib/helpers/states/test-active.ts +9 -0
  31. package/src/lib/helpers/states/time.ts +13 -0
  32. package/src/lib/helpers/test/caps-warning.ts +50 -0
  33. package/src/lib/helpers/test/caret.ts +92 -0
  34. package/src/lib/helpers/test/custom-text.ts +73 -0
  35. package/src/lib/helpers/test/english-punctuation.ts +38 -0
  36. package/src/lib/helpers/test/focus.ts +39 -0
  37. package/src/lib/helpers/test/manual-restart-tracker.ts +13 -0
  38. package/src/lib/helpers/test/out-of-focus.ts +19 -0
  39. package/src/lib/helpers/test/replay.ts +265 -0
  40. package/src/lib/helpers/test/test-input.ts +320 -0
  41. package/src/lib/helpers/test/test-logic.ts +1039 -0
  42. package/src/lib/helpers/test/test-state.ts +17 -0
  43. package/src/lib/helpers/test/test-stats.ts +442 -0
  44. package/src/lib/helpers/test/test-timer.ts +209 -0
  45. package/src/lib/helpers/test/test-ui.ts +370 -0
  46. package/src/lib/helpers/test/test-words.ts +72 -0
  47. package/src/lib/helpers/test/timer-progress.ts +16 -0
  48. package/src/lib/helpers/test/tts.ts +42 -0
  49. package/src/lib/helpers/test/weak-spot.ts +74 -0
  50. package/src/lib/helpers/test/wordset.ts +109 -0
  51. package/src/lib/styles/animations.scss +101 -0
  52. package/src/lib/styles/caret.scss +108 -0
  53. package/src/lib/styles/core.scss +498 -0
  54. package/src/lib/styles/index.scss +19 -0
  55. package/src/lib/styles/inputs.scss +290 -0
  56. package/src/lib/styles/popups.scss +1311 -0
  57. package/src/lib/styles/test.scss +1008 -0
  58. package/src/lib/styles/z_media-queries.scss +848 -0
  59. package/src/lib/types/types.d.ts +731 -0
  60. package/src/lib/utils/misc.ts +776 -0
  61. package/src/test-setup.ts +1 -0
  62. package/tsconfig.json +16 -0
  63. package/tsconfig.lib.json +14 -0
  64. package/tsconfig.lib.prod.json +9 -0
  65. package/tsconfig.spec.json +11 -0
@@ -0,0 +1,1039 @@
1
+ import Config from '../../helpers/config';
2
+ import QuotesController from '../controllers/quotes-controller';
3
+ import * as Misc from '../../utils/misc';
4
+ import * as ManualRestart from './manual-restart-tracker';
5
+ import * as TestUI from './test-ui';
6
+ import * as CustomText from './custom-text';
7
+ import * as TestStats from './test-stats';
8
+ import * as Focus from './focus';
9
+ import * as Caret from './caret';
10
+ import * as TimerProgress from './timer-progress';
11
+ import * as OutOfFocus from './out-of-focus';
12
+ import * as TestTimer from './test-timer';
13
+ import * as Replay from './replay';
14
+ import * as WeakSpot from './weak-spot';
15
+ import * as Wordset from './wordset';
16
+ import * as EnglishPunctuation from './english-punctuation';
17
+ import * as ActivePage from '../states/active-page';
18
+ import * as TestActive from '../states/test-active';
19
+ import * as TestInput from './test-input';
20
+ import * as TestState from './test-state';
21
+ import * as TestWords from './test-words';
22
+ import * as PageTransition from '../states/page-transition';
23
+ import { BehaviorSubject, Subject } from 'rxjs';
24
+ import * as TimerEvent from '../observables/timer-event';
25
+ import { MonkeyTypes } from '../../types/types';
26
+
27
+ let failReason = '';
28
+
29
+ let wordsInput: HTMLInputElement;
30
+
31
+ export const startTestSubject = new BehaviorSubject<boolean>(false);
32
+ export const resultSub = new Subject<any>();
33
+
34
+ export let notSignedInLastResult: MonkeyTypes.Result<MonkeyTypes.Mode> | null = null;
35
+
36
+ export function setWordsInputElementTestLogic(wordsElement: HTMLInputElement) {
37
+ wordsInput = wordsElement;
38
+ }
39
+
40
+ export function clearNotSignedInResult(): void {
41
+ notSignedInLastResult = null;
42
+ }
43
+
44
+ function shouldCapitalize(lastChar: string): boolean {
45
+ return /[?!.؟]/.test(lastChar);
46
+ }
47
+
48
+ function addDecimalPoint(word: string): string {
49
+ if (Math.random() < 0.8) {
50
+ const decimal = Misc.getNumbers(3, 0);
51
+ if (decimal != '') {
52
+ word = word + '.' + decimal;
53
+ }
54
+ }
55
+ return word;
56
+ }
57
+
58
+ let spanishSentenceTracker = '';
59
+ export async function punctuateWord(
60
+ previousWord: string,
61
+ currentWord: string,
62
+ index: number,
63
+ maxindex: number
64
+ ): Promise<string> {
65
+ let word = currentWord;
66
+
67
+ const currentLanguage = Config.language.split('_')[0];
68
+
69
+ const lastChar = Misc.getLastChar(previousWord);
70
+
71
+ if (Config.funbox === '58008') {
72
+ if (currentWord.length > 3) {
73
+ if (Math.random() < 0.5) {
74
+ word = Misc.setCharAt(word, Misc.randomIntFromRange(1, word.length - 2), '.');
75
+ }
76
+ if (Math.random() < 0.75) {
77
+ const index = Misc.randomIntFromRange(1, word.length - 2);
78
+ if (word[index - 1] !== '.' && word[index + 1] !== '.' && word[index + 1] !== '0') {
79
+ const special = Misc.randomElementFromArray(['/', '*', '-', '+']);
80
+ word = Misc.setCharAt(word, index, special);
81
+ }
82
+ }
83
+ }
84
+ } else if (Config.funbox === 'tenKeyMode') {
85
+ word = addDecimalPoint(word);
86
+ const special = Misc.randomElementFromArray(['/', '*', '-', '+']);
87
+ word = word + special + Misc.getNumbers(4);
88
+ // 2nd number of expression
89
+ word = addDecimalPoint(word);
90
+ } else {
91
+ if (currentLanguage != 'code' && (index == 0 || shouldCapitalize(lastChar))) {
92
+ //always capitalise the first word or if there was a dot unless using a code alphabet
93
+
94
+ word = Misc.capitalizeFirstLetterOfEachWord(word);
95
+
96
+ if (currentLanguage == 'turkish') {
97
+ word = word.replace(/I/g, 'İ');
98
+ }
99
+
100
+ if (currentLanguage == 'spanish' || currentLanguage == 'catalan') {
101
+ const rand = Math.random();
102
+ if (rand > 0.9) {
103
+ word = '¿' + word;
104
+ spanishSentenceTracker = '?';
105
+ } else if (rand > 0.8) {
106
+ word = '¡' + word;
107
+ spanishSentenceTracker = '!';
108
+ }
109
+ }
110
+ } else if (
111
+ (Math.random() < 0.1 && lastChar != '.' && lastChar != ',' && index != maxindex - 2) ||
112
+ index == maxindex - 1
113
+ ) {
114
+ if (currentLanguage == 'spanish' || currentLanguage == 'catalan') {
115
+ if (spanishSentenceTracker == '?' || spanishSentenceTracker == '!') {
116
+ word += spanishSentenceTracker;
117
+ spanishSentenceTracker = '';
118
+ }
119
+ } else {
120
+ const rand = Math.random();
121
+ if (rand <= 0.8) {
122
+ if (currentLanguage == 'kurdish') {
123
+ word += '.';
124
+ } else {
125
+ word += '.';
126
+ }
127
+ } else if (rand > 0.8 && rand < 0.9) {
128
+ if (currentLanguage == 'french') {
129
+ word = '?';
130
+ } else if (
131
+ currentLanguage == 'arabic' ||
132
+ currentLanguage == 'persian' ||
133
+ currentLanguage == 'urdu' ||
134
+ currentLanguage == 'kurdish'
135
+ ) {
136
+ word += '؟';
137
+ } else if (currentLanguage == 'greek') {
138
+ word += ';';
139
+ } else {
140
+ word += '?';
141
+ }
142
+ } else {
143
+ if (currentLanguage == 'french') {
144
+ word = '!';
145
+ } else {
146
+ word += '!';
147
+ }
148
+ }
149
+ }
150
+ } else if (
151
+ Math.random() < 0.01 &&
152
+ lastChar != ',' &&
153
+ lastChar != '.' &&
154
+ currentLanguage !== 'russian'
155
+ ) {
156
+ word = `"${word}"`;
157
+ } else if (
158
+ Math.random() < 0.011 &&
159
+ lastChar != ',' &&
160
+ lastChar != '.' &&
161
+ currentLanguage !== 'russian' &&
162
+ currentLanguage !== 'ukrainian'
163
+ ) {
164
+ word = `'${word}'`;
165
+ } else if (Math.random() < 0.012 && lastChar != ',' && lastChar != '.') {
166
+ if (currentLanguage == 'code') {
167
+ const r = Math.random();
168
+ if (r < 0.25) {
169
+ word = `(${word})`;
170
+ } else if (r < 0.5) {
171
+ word = `{${word}}`;
172
+ } else if (r < 0.75) {
173
+ word = `[${word}]`;
174
+ } else {
175
+ word = `<${word}>`;
176
+ }
177
+ } else {
178
+ word = `(${word})`;
179
+ }
180
+ } else if (
181
+ Math.random() < 0.013 &&
182
+ lastChar != ',' &&
183
+ lastChar != '.' &&
184
+ lastChar != ';' &&
185
+ lastChar != '؛' &&
186
+ lastChar != ':'
187
+ ) {
188
+ if (currentLanguage == 'french') {
189
+ word = ':';
190
+ } else if (currentLanguage == 'greek') {
191
+ word = '·';
192
+ } else {
193
+ word += ':';
194
+ }
195
+ } else if (Math.random() < 0.014 && lastChar != ',' && lastChar != '.' && previousWord != '-') {
196
+ word = '-';
197
+ } else if (
198
+ Math.random() < 0.015 &&
199
+ lastChar != ',' &&
200
+ lastChar != '.' &&
201
+ lastChar != ';' &&
202
+ lastChar != '؛' &&
203
+ lastChar != ':'
204
+ ) {
205
+ if (currentLanguage == 'french') {
206
+ word = ';';
207
+ } else if (currentLanguage == 'greek') {
208
+ word = '·';
209
+ } else if (currentLanguage == 'arabic' || currentLanguage == 'kurdish') {
210
+ word += '؛';
211
+ } else {
212
+ word += ';';
213
+ }
214
+ } else if (Math.random() < 0.2 && lastChar != ',') {
215
+ if (
216
+ currentLanguage == 'arabic' ||
217
+ currentLanguage == 'urdu' ||
218
+ currentLanguage == 'persian' ||
219
+ currentLanguage == 'kurdish'
220
+ ) {
221
+ word += '،';
222
+ } else {
223
+ word += ',';
224
+ }
225
+ } else if (Math.random() < 0.25 && currentLanguage == 'code') {
226
+ const specials = ['{', '}', '[', ']', '(', ')', ';', '=', '+', '%', '/'];
227
+
228
+ word = Misc.randomElementFromArray(specials);
229
+ } else if (
230
+ Math.random() < 0.5 &&
231
+ currentLanguage === 'english' &&
232
+ (await EnglishPunctuation.check(word))
233
+ ) {
234
+ word = await applyEnglishPunctuationToWord(word);
235
+ }
236
+ }
237
+ return word;
238
+ }
239
+
240
+ async function applyEnglishPunctuationToWord(word: string): Promise<string> {
241
+ return EnglishPunctuation.replace(word);
242
+ }
243
+
244
+ export function startTest(): boolean {
245
+ if (PageTransition.get()) {
246
+ return false;
247
+ }
248
+ startTestSubject.next(true);
249
+ TestActive.set(true);
250
+ Replay.startReplayRecording();
251
+ Replay.replayGetWordsList(TestWords.words.list);
252
+ TestInput.resetKeypressTimings();
253
+ TimerProgress.update();
254
+ TestTimer.clear();
255
+
256
+ TestStats.setStart(performance.now());
257
+ TestTimer.start();
258
+ return true;
259
+ }
260
+
261
+ interface RestartOptions {
262
+ withSameWordset?: boolean;
263
+ nosave?: boolean;
264
+ event?: { shiftKey: any };
265
+ practiseMissed?: boolean;
266
+ noAnim?: boolean;
267
+ }
268
+
269
+ export function restart(options = {} as RestartOptions): void {
270
+ const defaultOptions = {
271
+ withSameWordset: false,
272
+ practiseMissed: false,
273
+ noAnim: false,
274
+ nosave: false,
275
+ };
276
+
277
+ options = { ...defaultOptions, ...options };
278
+
279
+ if (TestUI.testRestarting || TestUI.resultCalculating) {
280
+ event?.preventDefault();
281
+ return;
282
+ }
283
+ if (ActivePage.get() == 'test' && !TestUI.resultVisible) {
284
+ if (!ManualRestart.get()) {
285
+ if (TestWords.hasTab && !options.event?.shiftKey && Config.quickRestart !== 'esc') {
286
+ return;
287
+ }
288
+ if (Config.mode !== 'zen') event?.preventDefault();
289
+ if (!Misc.canQuickRestart(Config.mode, Config.words, Config.time, CustomText)) {
290
+ return;
291
+ }
292
+ }
293
+ }
294
+ if (TestActive.get()) {
295
+ if (
296
+ Config.repeatQuotes === 'typing' &&
297
+ Config.mode === 'quote' &&
298
+ Config.language.replace(/_\d*k$/g, '') === TestWords.randomQuote.language
299
+ ) {
300
+ options.withSameWordset = true;
301
+ }
302
+ if (TestState.isRepeated) {
303
+ options.withSameWordset = true;
304
+ }
305
+
306
+ TestInput.pushKeypressesToHistory();
307
+ const testSeconds = TestStats.calculateTestSeconds(performance.now());
308
+ const afkseconds = TestStats.calculateAfkSeconds(testSeconds);
309
+ let tt = testSeconds - afkseconds;
310
+ if (tt < 0) tt = 0;
311
+ TestStats.incrementIncompleteSeconds(tt);
312
+ TestStats.incrementRestartCount();
313
+ }
314
+
315
+ let repeatWithPace = false;
316
+ if (TestUI.resultVisible && Config.repeatedPace && options.withSameWordset) {
317
+ repeatWithPace = true;
318
+ }
319
+
320
+ ManualRestart.reset();
321
+ TestTimer.clear();
322
+ TestStats.restart();
323
+ TestInput.restart();
324
+ TestInput.corrected.reset();
325
+ startTestSubject.next(false);
326
+ Caret.hide();
327
+ TestActive.set(false);
328
+ TestInput.setBailout(false);
329
+
330
+ if (ActivePage.get() == 'test' && window.scrollY > 0) {
331
+ window.scrollTo({ top: 0, behavior: 'smooth' });
332
+ }
333
+
334
+ wordsInput.value = ' ';
335
+
336
+ TestUI.reset();
337
+
338
+ TestUI.setResultVisible(false);
339
+ PageTransition.set(true);
340
+ TestUI.setTestRestarting(true);
341
+ (async () => {
342
+ if (ActivePage.get() == 'test') {
343
+ Focus.set(false);
344
+ }
345
+ TestUI.focusWords();
346
+ wordsInput.value = ' ';
347
+ let shouldQuoteRepeat = false;
348
+ if (Config.mode === 'quote' && Config.repeatQuotes === 'typing' && failReason !== '') {
349
+ shouldQuoteRepeat = true;
350
+ }
351
+
352
+ if (!options.withSameWordset && !shouldQuoteRepeat) {
353
+ TestState.setRepeated(false);
354
+ TestState.setPaceRepeat(repeatWithPace);
355
+ TestWords.setHasTab(false);
356
+ try {
357
+ await init();
358
+ } catch {
359
+ TestUI.setTestRestarting(false);
360
+ return;
361
+ }
362
+ } else {
363
+ TestState.setRepeated(true);
364
+ TestState.setPaceRepeat(repeatWithPace);
365
+ TestActive.set(false);
366
+ TestWords.words.resetCurrentIndex();
367
+ TestInput.input.reset();
368
+ TestUI.showWords();
369
+ }
370
+ failReason = '';
371
+
372
+ let fbtext = '';
373
+ if (Config.funbox !== 'none') {
374
+ fbtext = ' ' + Config.funbox;
375
+ }
376
+
377
+ TestUI.setTestRestarting(false);
378
+ TestTimer.clear();
379
+ PageTransition.set(false);
380
+
381
+ // Added by me
382
+ startTest();
383
+ })();
384
+ }
385
+
386
+ function applyFunboxesToWord(word: string, wordset?: Wordset.Wordset): string {
387
+ if (Config.funbox === 'rAnDoMcAsE') {
388
+ let randomcaseword = '';
389
+ for (let i = 0; i < word.length; i++) {
390
+ if (i % 2 != 0) {
391
+ randomcaseword += word[i].toUpperCase();
392
+ } else {
393
+ randomcaseword += word[i];
394
+ }
395
+ }
396
+ word = randomcaseword;
397
+ } else if (Config.funbox === 'capitals') {
398
+ word = Misc.capitalizeFirstLetterOfEachWord(word);
399
+ } else if (Config.funbox === 'gibberish') {
400
+ word = Misc.getGibberish();
401
+ } else if (Config.funbox === 'arrows') {
402
+ word = Misc.getArrows();
403
+ } else if (Config.funbox === 'tenKeyMode') {
404
+ word = Misc.getNumbers(Config.punctuation ? 4 : 5, Config.punctuation ? 1 : 2);
405
+ if (Config.language.split('_')[0] === 'kurdish') {
406
+ word = Misc.convertNumberToArabicIndic(word);
407
+ }
408
+ } else if (Config.funbox === '58008') {
409
+ word = Misc.getNumbers(7);
410
+ if (Config.language.split('_')[0] === 'kurdish') {
411
+ word = Misc.convertNumberToArabicIndic(word);
412
+ }
413
+ } else if (Config.funbox === 'specials') {
414
+ word = Misc.getSpecials();
415
+ } else if (Config.funbox === 'ascii') {
416
+ word = Misc.getASCII();
417
+ } else if (wordset !== undefined && Config.funbox === 'weakspot') {
418
+ word = WeakSpot.getWord(wordset);
419
+ }
420
+ return word;
421
+ }
422
+
423
+ async function getNextWord(
424
+ wordset: Wordset.Wordset,
425
+ language: MonkeyTypes.LanguageObject,
426
+ wordsBound: number
427
+ ): Promise<string> {
428
+ let randomWord = wordset.randomWord();
429
+ const previousWord = TestWords.words.get(TestWords.words.length - 1, true);
430
+ const previousWord2 = TestWords.words.get(TestWords.words.length - 2, true);
431
+
432
+ // Added by TG
433
+ if (Config.customTGQuotes) {
434
+ if (TestWords.words.length - TestWords.words.currentIndex <= 40) {
435
+ const randomQuote = QuotesController.getRandomQuote();
436
+ let rq: MonkeyTypes.Quote | undefined | null = undefined;
437
+ rq = randomQuote;
438
+
439
+ if (rq !== (undefined || null)) {
440
+ rq.text = rq.text.replace(/ +/gm, ' ');
441
+ rq.text = rq.text.replace(/\\\\t/gm, '\t');
442
+ rq.text = rq.text.replace(/\\\\n/gm, '\n');
443
+ rq.text = rq.text.replace(/\\t/gm, '\t');
444
+ rq.text = rq.text.replace(/\\n/gm, '\n');
445
+ rq.text = rq.text.replace(/( *(\r\n|\r|\n) *)/g, '\n ');
446
+ rq.text = rq.text.replace(/…/g, '...');
447
+ rq.text = rq.text.trim();
448
+ rq.textSplit = rq.text.split(' ');
449
+ rq.language = Config.language.replace(/_\d*k$/g, '');
450
+ TestWords.setRandomQuote(rq);
451
+ }
452
+
453
+ return TestWords.randomQuote.text;
454
+ }
455
+ } else if (Config.mode === 'quote') {
456
+ randomWord = TestWords.randomQuote.textSplit?.[TestWords.words.length] ?? '';
457
+ } else if (Config.mode == 'custom' && !CustomText.isWordRandom && !CustomText.isTimeRandom) {
458
+ randomWord = CustomText.text[TestWords.words.length];
459
+ } else {
460
+ let regenarationCount = 0; //infinite loop emergency stop button
461
+ while (
462
+ regenarationCount < 100 &&
463
+ (previousWord == randomWord ||
464
+ previousWord2 == randomWord ||
465
+ (Config.mode !== 'custom' && !Config.punctuation && randomWord == 'I') ||
466
+ (Config.mode !== 'custom' &&
467
+ !Config.punctuation &&
468
+ /[-=_+[\]{};'\\:"|,./<>?]/i.test(randomWord)))
469
+ ) {
470
+ regenarationCount++;
471
+ randomWord = wordset.randomWord();
472
+ }
473
+ }
474
+
475
+ if (randomWord === undefined) {
476
+ randomWord = wordset.randomWord();
477
+ }
478
+
479
+ randomWord = randomWord.replace(/ +/gm, ' ');
480
+ randomWord = randomWord.replace(/^ | $/gm, '');
481
+ randomWord = applyFunboxesToWord(randomWord, wordset);
482
+
483
+ if (Config.punctuation) {
484
+ randomWord = await punctuateWord(
485
+ TestWords.words.get(TestWords.words.length - 1),
486
+ randomWord,
487
+ TestWords.words.length,
488
+ wordsBound
489
+ );
490
+ }
491
+ if (Config.numbers) {
492
+ if (Math.random() < 0.1) {
493
+ randomWord = Misc.getNumbers(4);
494
+
495
+ if (Config.language.split('_')[0] === 'kurdish') {
496
+ randomWord = Misc.convertNumberToArabicIndic(randomWord);
497
+ }
498
+ }
499
+ }
500
+
501
+ return randomWord;
502
+ }
503
+
504
+ export async function init(): Promise<void> {
505
+ TestActive.set(false);
506
+ TestWords.words.reset();
507
+ TestUI.setCurrentWordElementIndex(0);
508
+
509
+ TestInput.input.resetHistory();
510
+ TestInput.input.resetCurrent();
511
+
512
+ let language;
513
+ try {
514
+ language = await Misc.getLanguage(Config.language);
515
+ } catch (e) {
516
+ Misc.emitQuoteNetworkError();
517
+ throw e;
518
+ }
519
+
520
+ let wordsBound = 100;
521
+ if (Config.showAllLines) {
522
+ if (Config.mode === 'quote') {
523
+ wordsBound = 100;
524
+ } else if (Config.mode === 'custom') {
525
+ if (CustomText.isWordRandom) {
526
+ wordsBound = CustomText.word;
527
+ } else if (CustomText.isTimeRandom) {
528
+ wordsBound = 100;
529
+ } else {
530
+ wordsBound = CustomText.text.length;
531
+ }
532
+ } else if (Config.mode != 'time') {
533
+ wordsBound = Config.words;
534
+ }
535
+ } else {
536
+ if (Config.mode === 'words' && Config.words < wordsBound) {
537
+ wordsBound = Config.words;
538
+ }
539
+ if (Config.mode == 'custom' && CustomText.isWordRandom && CustomText.word < wordsBound) {
540
+ wordsBound = CustomText.word;
541
+ }
542
+ if (Config.mode == 'custom' && CustomText.isTimeRandom) {
543
+ wordsBound = 100;
544
+ }
545
+ if (
546
+ Config.mode == 'custom' &&
547
+ !CustomText.isWordRandom &&
548
+ !CustomText.isTimeRandom &&
549
+ CustomText.text.length < wordsBound
550
+ ) {
551
+ wordsBound = CustomText.text.length;
552
+ }
553
+ }
554
+
555
+ if (
556
+ (Config.mode === 'custom' && CustomText.isWordRandom && CustomText.word == 0) ||
557
+ (Config.mode === 'custom' && CustomText.isTimeRandom && CustomText.time == 0)
558
+ ) {
559
+ wordsBound = 100;
560
+ }
561
+
562
+ if (Config.mode === 'words' && Config.words === 0) {
563
+ wordsBound = 100;
564
+ }
565
+ if (Config.funbox === 'plus_one') {
566
+ wordsBound = 2;
567
+ if (Config.mode === 'words' && Config.words < wordsBound) {
568
+ wordsBound = Config.words;
569
+ }
570
+ }
571
+ if (Config.funbox === 'plus_two') {
572
+ wordsBound = 3;
573
+ if (Config.mode === 'words' && Config.words < wordsBound) {
574
+ wordsBound = Config.words;
575
+ }
576
+ }
577
+
578
+ if (
579
+ (Config.mode == 'time' || Config.mode == 'words' || Config.mode == 'custom') &&
580
+ !Config.customTGQuotes
581
+ ) {
582
+ let wordList = language.words;
583
+ if (Config.mode == 'custom') {
584
+ wordList = CustomText.text;
585
+ }
586
+ const wordset = Wordset.withWords(wordList, Config.funbox);
587
+
588
+ if ((Config.funbox == 'wikipedia' || Config.funbox == 'poetry') && Config.mode != 'custom') {
589
+ // Skip word generation for wikipedia/poetry funboxes in non-custom mode
590
+ } else {
591
+ for (let i = 0; i < wordsBound; i++) {
592
+ const randomWord = await getNextWord(wordset, language, wordsBound);
593
+
594
+ if (/\t/g.test(randomWord)) {
595
+ TestWords.setHasTab(true);
596
+ }
597
+
598
+ const te = randomWord.replace('\n', '\n ').trim();
599
+
600
+ if (/ +/.test(te)) {
601
+ const randomList = te.split(' ');
602
+ let id = 0;
603
+ while (id < randomList.length) {
604
+ TestWords.words.push(randomList[id]);
605
+ id++;
606
+
607
+ if (
608
+ TestWords.words.length == wordsBound &&
609
+ Config.mode == 'custom' &&
610
+ CustomText.isWordRandom
611
+ ) {
612
+ break;
613
+ }
614
+ }
615
+ if (Config.mode == 'custom' && !CustomText.isWordRandom && !CustomText.isTimeRandom) {
616
+ // Custom mode with fixed text - no adjustment needed
617
+ } else {
618
+ i = TestWords.words.length - 1;
619
+ }
620
+ } else {
621
+ TestWords.words.push(randomWord);
622
+ }
623
+ }
624
+ }
625
+ }
626
+
627
+ // Added by TG
628
+ if (Config.customTGQuotes) {
629
+ let quotesCollection;
630
+
631
+ try {
632
+ quotesCollection = await QuotesController.getQuotes(
633
+ Config.language,
634
+ Config.testVersion,
635
+ Config.quoteLength
636
+ );
637
+ } catch (e) {
638
+ Misc.emitQuoteNetworkError();
639
+ throw e;
640
+ }
641
+
642
+ let rq: MonkeyTypes.Quote | undefined | null = undefined;
643
+ const randomQuote = QuotesController.getRandomQuote();
644
+ rq = randomQuote;
645
+
646
+ if (rq === (undefined || null)) return;
647
+
648
+ rq.text = rq.text.replace(/ +/gm, ' ');
649
+ rq.text = rq.text.replace(/\\\\t/gm, '\t');
650
+ rq.text = rq.text.replace(/\\\\n/gm, '\n');
651
+ rq.text = rq.text.replace(/\\t/gm, '\t');
652
+ rq.text = rq.text.replace(/\\n/gm, '\n');
653
+ rq.text = rq.text.replace(/( *(\r\n|\r|\n) *)/g, '\n ');
654
+ rq.text = rq.text.replace(/…/g, '...');
655
+ rq.text = rq.text.trim();
656
+ rq.textSplit = rq.text.split(' ');
657
+ rq.language = Config.language.replace(/_\d*k$/g, '');
658
+
659
+ TestWords.setRandomQuote(rq);
660
+
661
+ const w = TestWords.randomQuote.textSplit;
662
+
663
+ if (w === undefined) return;
664
+
665
+ for (let i = 0; i < w.length; i++) {
666
+ if (/\t/g.test(w[i])) {
667
+ TestWords.setHasTab(true);
668
+ }
669
+
670
+ w[i] = applyFunboxesToWord(w[i]);
671
+
672
+ TestWords.words.push(w[i]);
673
+ }
674
+ }
675
+
676
+ TestUI.showWords();
677
+ }
678
+
679
+ export async function addWord(): Promise<void> {
680
+ let bound = 100;
681
+ if (Config.funbox === 'plus_one') bound = 1;
682
+ if (Config.funbox === 'plus_two') bound = 2;
683
+ if (
684
+ TestWords.words.length - TestInput.input.history.length > bound ||
685
+ (Config.mode === 'words' && TestWords.words.length >= Config.words && Config.words > 0) ||
686
+ (Config.mode === 'custom' &&
687
+ CustomText.isWordRandom &&
688
+ TestWords.words.length >= CustomText.word &&
689
+ CustomText.word != 0) ||
690
+ (Config.mode === 'custom' &&
691
+ !CustomText.isWordRandom &&
692
+ !CustomText.isTimeRandom &&
693
+ TestWords.words.length >= CustomText.text.length) ||
694
+ (Config.mode === 'quote' &&
695
+ TestWords.words.length >= (TestWords.randomQuote.textSplit?.length ?? 0))
696
+ ) {
697
+ return;
698
+ }
699
+
700
+ const language: MonkeyTypes.LanguageObject =
701
+ Config.mode !== 'custom'
702
+ ? await Misc.getCurrentLanguage(Config.language)
703
+ : {
704
+ //borrow the direction of the current language
705
+ ...(await Misc.getCurrentLanguage(Config.language)),
706
+ words: CustomText.text,
707
+ };
708
+ const wordset = Wordset.withWords(language.words, Config.funbox);
709
+
710
+ const randomWord = await getNextWord(wordset, language, bound);
711
+
712
+ const split = randomWord.split(' ');
713
+
714
+ if (!Config.customTGQuotes) {
715
+ if (split.length > 1) {
716
+ split.forEach(word => {
717
+ TestWords.words.push(word);
718
+ TestUI.addWord(word);
719
+ });
720
+ } else {
721
+ TestWords.words.push(randomWord);
722
+ TestUI.addWord(randomWord);
723
+ }
724
+ } else {
725
+ if (split.length > 1) {
726
+ split.forEach(word => {
727
+ TestWords.words.push(word);
728
+ TestUI.addWord(word);
729
+ });
730
+ }
731
+ }
732
+ }
733
+
734
+ interface CompletedEvent extends MonkeyTypes.Result<MonkeyTypes.Mode> {
735
+ keySpacing: number[] | 'toolong';
736
+ keyDuration: number[] | 'toolong';
737
+ customText: MonkeyTypes.CustomText;
738
+ smoothConsistency: number;
739
+ wpmConsistency: number;
740
+ lang: string;
741
+ challenge?: string | null;
742
+ }
743
+
744
+ type PartialCompletedEvent = Omit<Partial<CompletedEvent>, 'chartData'> & {
745
+ chartData: Partial<MonkeyTypes.ChartData>;
746
+ };
747
+
748
+ interface RetrySaving {
749
+ completedEvent: CompletedEvent | null;
750
+ canRetry: boolean;
751
+ }
752
+
753
+ const retrySaving: RetrySaving = {
754
+ completedEvent: null,
755
+ canRetry: false,
756
+ };
757
+
758
+ function buildCompletedEvent(difficultyFailed: boolean): CompletedEvent {
759
+ //build completed event object
760
+ const completedEvent: PartialCompletedEvent = {
761
+ wpm: undefined,
762
+ rawWpm: undefined,
763
+ kph: -1,
764
+ charStats: undefined,
765
+ acc: undefined,
766
+ mode: Config.mode,
767
+ mode2: undefined as never,
768
+ quoteLength: -1,
769
+ punctuation: Config.punctuation,
770
+ numbers: Config.numbers,
771
+ lazyMode: Config.lazyMode,
772
+ timestamp: Date.now(),
773
+ language: Config.language,
774
+ restartCount: TestStats.restartCount,
775
+ incompleteTestSeconds:
776
+ TestStats.incompleteSeconds < 0 ? 0 : Misc.roundTo2(TestStats.incompleteSeconds),
777
+ difficulty: Config.difficulty,
778
+ blindMode: Config.blindMode,
779
+ tags: undefined,
780
+ keySpacing: TestInput.keypressTimings.spacing.array,
781
+ keyDuration: TestInput.keypressTimings.duration.array,
782
+ consistency: undefined,
783
+ keyConsistency: undefined,
784
+ funbox: Config.funbox,
785
+ bailedOut: TestInput.bailout,
786
+ chartData: {
787
+ wpm: TestInput.wpmHistory,
788
+ raw: undefined,
789
+ err: undefined,
790
+ },
791
+ customText: undefined,
792
+ testDuration: undefined,
793
+ afkDuration: undefined,
794
+ };
795
+
796
+ // stats
797
+ const stats = TestStats.calculateStats();
798
+ if (stats.time % 1 != 0 && Config.mode !== 'time') {
799
+ TestStats.setLastSecondNotRound();
800
+ }
801
+ TestStats.setLastTestWpm(stats.wpm);
802
+ completedEvent.wpm = stats.wpm;
803
+ completedEvent.rawWpm = stats.wpmRaw;
804
+ completedEvent.charStats = [
805
+ stats.correctChars + stats.correctSpaces,
806
+ stats.incorrectChars,
807
+ stats.extraChars,
808
+ stats.missedChars,
809
+ ];
810
+ completedEvent.acc = stats.acc;
811
+ if (Config.funbox === 'tenKeyMode') {
812
+ completedEvent.kph = stats.wpm * 5 * 60;
813
+ }
814
+
815
+ // if the last second was not rounded, add another data point to the history
816
+ if (TestStats.lastSecondNotRound && !difficultyFailed) {
817
+ const wpmAndRaw = TestStats.calculateWpmAndRaw();
818
+ TestInput.pushToWpmHistory(wpmAndRaw.wpm);
819
+ TestInput.pushToRawHistory(wpmAndRaw.raw);
820
+ TestInput.pushKeypressesToHistory();
821
+ }
822
+
823
+ //consistency
824
+ const rawPerSecond = TestInput.keypressPerSecond.map(f => Math.round((f.count / 5) * 60));
825
+ const stddev = Misc.stdDev(rawPerSecond);
826
+ const avg = Misc.mean(rawPerSecond);
827
+ let consistency = Misc.roundTo2(Misc.kogasa(stddev / avg));
828
+ let keyConsistencyArray =
829
+ TestInput.keypressTimings.spacing.array === 'toolong'
830
+ ? []
831
+ : TestInput.keypressTimings.spacing.array.slice();
832
+ if (keyConsistencyArray.length > 0) {
833
+ keyConsistencyArray = keyConsistencyArray.slice(0, keyConsistencyArray.length - 1);
834
+ }
835
+ let keyConsistency = Misc.roundTo2(
836
+ Misc.kogasa(Misc.stdDev(keyConsistencyArray) / Misc.mean(keyConsistencyArray))
837
+ );
838
+ if (!consistency || isNaN(consistency)) {
839
+ consistency = 0;
840
+ }
841
+ if (!keyConsistency || isNaN(keyConsistency)) {
842
+ keyConsistency = 0;
843
+ }
844
+ completedEvent.keyConsistency = keyConsistency;
845
+ completedEvent.consistency = consistency;
846
+ const smoothedraw = Misc.smooth(rawPerSecond, 1);
847
+ completedEvent.chartData.raw = smoothedraw;
848
+ completedEvent.chartData.unsmoothedRaw = rawPerSecond;
849
+
850
+ //smoothed consistency
851
+ const stddev2 = Misc.stdDev(smoothedraw);
852
+ const avg2 = Misc.mean(smoothedraw);
853
+ const smoothConsistency = Misc.roundTo2(Misc.kogasa(stddev2 / avg2));
854
+ completedEvent.smoothConsistency = smoothConsistency;
855
+
856
+ //wpm consistency
857
+ const stddev3 = Misc.stdDev(completedEvent.chartData.wpm ?? []);
858
+ const avg3 = Misc.mean(completedEvent.chartData.wpm ?? []);
859
+ const wpmConsistency = Misc.roundTo2(Misc.kogasa(stddev3 / avg3));
860
+ completedEvent.wpmConsistency = wpmConsistency;
861
+
862
+ completedEvent.testDuration = parseFloat(stats.time.toString());
863
+ completedEvent.afkDuration = TestStats.calculateAfkSeconds(completedEvent.testDuration);
864
+
865
+ completedEvent.chartData.err = [];
866
+ for (let i = 0; i < TestInput.keypressPerSecond.length; i++) {
867
+ completedEvent.chartData.err.push(TestInput.keypressPerSecond[i].errors);
868
+ }
869
+
870
+ if (Config.mode === 'quote') {
871
+ completedEvent.quoteLength = TestWords.randomQuote.group;
872
+ completedEvent.language = Config.language.replace(/_\d*k$/g, '');
873
+ } else {
874
+ delete completedEvent.quoteLength;
875
+ }
876
+
877
+ // @ts-expect-error TODO fix this
878
+ completedEvent.mode2 = Misc.getMode2(Config, TestWords.randomQuote);
879
+
880
+ if (Config.mode === 'custom') {
881
+ completedEvent.customText = <MonkeyTypes.CustomText>{};
882
+ completedEvent.customText.textLen = CustomText.text.length;
883
+ completedEvent.customText.isWordRandom = CustomText.isWordRandom;
884
+ completedEvent.customText.isTimeRandom = CustomText.isTimeRandom;
885
+ completedEvent.customText.word = CustomText.word;
886
+ completedEvent.customText.time = CustomText.time;
887
+ } else {
888
+ delete completedEvent.customText;
889
+ }
890
+
891
+ //tags
892
+ const activeTagsIds: string[] = [];
893
+ completedEvent.tags = activeTagsIds;
894
+
895
+ if (completedEvent.mode != 'custom') delete completedEvent.customText;
896
+
897
+ return <CompletedEvent>completedEvent;
898
+ }
899
+
900
+ export function stopTest(): void {
901
+ TestActive.set(false);
902
+ Focus.set(false);
903
+ Caret.hide();
904
+ OutOfFocus.hide();
905
+ TestTimer.clear();
906
+ }
907
+
908
+ export async function finish(difficultyFailed = false): Promise<void> {
909
+ if (!TestActive.get()) return;
910
+
911
+ startTestSubject.next(false);
912
+
913
+ TestInput.recordKeypressSpacing(); //this is needed in case there is afk time at the end - to make sure test duration makes sense
914
+
915
+ TestUI.setResultCalculating(true);
916
+ TestUI.setResultVisible(true);
917
+ TestStats.setEnd(performance.now());
918
+ TestActive.set(false);
919
+ Focus.set(false);
920
+ Caret.hide();
921
+ OutOfFocus.hide();
922
+ TestTimer.clear();
923
+
924
+ if (TestInput.burstHistory.length !== TestInput.input.getHistory().length) {
925
+ const burst = TestStats.calculateBurst();
926
+ TestInput.pushBurstToHistory(burst);
927
+ }
928
+
929
+ if (Config.mode == 'zen' || TestInput.bailout) {
930
+ TestStats.removeAfkData();
931
+ }
932
+
933
+ const completedEvent = buildCompletedEvent(difficultyFailed);
934
+
935
+ function countUndefined(input: unknown): number {
936
+ if (typeof input === 'undefined') {
937
+ return 1;
938
+ } else if (typeof input === 'object' && input !== null) {
939
+ return Object.values(input).reduce((a, b) => a + countUndefined(b), 0) as number;
940
+ } else {
941
+ return 0;
942
+ }
943
+ }
944
+
945
+ if (countUndefined(completedEvent) > 0) {
946
+ console.log(completedEvent);
947
+ return;
948
+ }
949
+
950
+ const kps = TestInput.keypressPerSecond.slice(-5);
951
+ let afkDetected = kps.every(second => second.afk);
952
+ if (TestInput.bailout) afkDetected = false;
953
+
954
+ let tooShort = false;
955
+ let dontSave = false;
956
+ if (difficultyFailed) {
957
+ dontSave = true;
958
+ } else if (afkDetected) {
959
+ dontSave = true;
960
+ } else if (TestState.isRepeated) {
961
+ dontSave = true;
962
+ } else if (
963
+ (Config.mode === 'time' && completedEvent.mode2 < 15 && completedEvent.mode2 > 0) ||
964
+ (Config.mode === 'time' && completedEvent.mode2 == 0 && completedEvent.testDuration < 15) ||
965
+ (Config.mode === 'words' && completedEvent.mode2 < 10 && completedEvent.mode2 > 0) ||
966
+ (Config.mode === 'words' && completedEvent.mode2 == 0 && completedEvent.testDuration < 15) ||
967
+ (Config.mode === 'custom' &&
968
+ !CustomText.isWordRandom &&
969
+ !CustomText.isTimeRandom &&
970
+ CustomText.text.length < 10) ||
971
+ (Config.mode === 'custom' &&
972
+ CustomText.isWordRandom &&
973
+ !CustomText.isTimeRandom &&
974
+ CustomText.word < 10) ||
975
+ (Config.mode === 'custom' &&
976
+ !CustomText.isWordRandom &&
977
+ CustomText.isTimeRandom &&
978
+ CustomText.time < 15) ||
979
+ (Config.mode === 'zen' && completedEvent.testDuration < 15)
980
+ ) {
981
+ tooShort = true;
982
+ dontSave = true;
983
+ } else if (completedEvent.wpm < 0 || completedEvent.wpm > 350) {
984
+ TestStats.setInvalid();
985
+ dontSave = true;
986
+ } else if (completedEvent.acc < 75 || completedEvent.acc > 100) {
987
+ TestStats.setInvalid();
988
+ dontSave = true;
989
+ }
990
+
991
+ TestStats.setLastResult(completedEvent);
992
+
993
+ resultSub.next({
994
+ completedEvent: completedEvent,
995
+ difficultyFailed: difficultyFailed,
996
+ failReason: failReason,
997
+ afkDetected: afkDetected,
998
+ isRepeated: TestState.isRepeated,
999
+ tooShort: tooShort,
1000
+ randomQuote: TestWords.randomQuote,
1001
+ dontSave: dontSave,
1002
+ wpmHistory: TestInput.wpmHistory,
1003
+ });
1004
+
1005
+ if (completedEvent.chartData !== 'toolong') {
1006
+ delete completedEvent.chartData.unsmoothedRaw;
1007
+ }
1008
+
1009
+ if (completedEvent.testDuration > 122) {
1010
+ completedEvent.chartData = 'toolong';
1011
+ completedEvent.keySpacing = 'toolong';
1012
+ completedEvent.keyDuration = 'toolong';
1013
+ TestInput.setKeypressTimingsTooLong();
1014
+ }
1015
+
1016
+ if (
1017
+ Config.difficulty == 'normal' ||
1018
+ ((Config.difficulty == 'master' || Config.difficulty == 'expert') && !difficultyFailed)
1019
+ ) {
1020
+ TestStats.resetIncomplete();
1021
+ }
1022
+ }
1023
+
1024
+ export function fail(reason: string): void {
1025
+ failReason = reason;
1026
+ TestInput.pushKeypressesToHistory();
1027
+ finish(true);
1028
+ const testSeconds = TestStats.calculateTestSeconds(performance.now());
1029
+ const afkseconds = TestStats.calculateAfkSeconds(testSeconds);
1030
+ let tt = testSeconds - afkseconds;
1031
+ if (tt < 0) tt = 0;
1032
+ TestStats.incrementIncompleteSeconds(tt);
1033
+ TestStats.incrementRestartCount();
1034
+ }
1035
+
1036
+ TimerEvent.subscribe((eventKey, eventValue) => {
1037
+ if (eventKey === 'fail' && eventValue !== undefined) fail(eventValue);
1038
+ if (eventKey === 'finish') finish();
1039
+ });