This repository has been archived by the owner on Nov 3, 2021. It is now read-only.
/
latin.js
1285 lines (1144 loc) · 43.8 KB
/
latin.js
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
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* global PAGE_INDEX_DEFAULT, InputMethods, KeyEvent, PromiseStorage */
'use strict';
/*
* This latin input method provides four forms of input assistance:
*
* 1) word suggestions
*
* 2) auto correction
*
* 3) auto capitalization
*
* 4) punctuation assistance by converting space space to period space
* and by transposing space followed by punctuation.
*
* These input modifications are controlled by the type and inputmode
* properties of the input element that has the focus. If inputmode is
* "verbatim" then the input method does not modify the user's input in any
* way. See getInputMode() for a description of input modes.
*
* TODO:
*
* when deciding whether to autocorrect, if the first 2 choices are
* a prefix of one another, then consider the ratio of 1st to 3rd instead
* of 1st to second possibly? If there different forms of the same word
* and that word is the most likely, then substitute it?
*
* add a per-language settings-based list of customizable corrections?
*
* Display an X icon in the suggestions line to give the user a way
* to dismiss an autocorrection? (Easier than space, backspace, space).
*
* Display a + icon in the suggestions line to give the user a way to
* add the current input to a personal word list so it doesn't get
* auto-corrected?
*
* Use color somehow to indicate that a word is properly spelled?
*
* Make the phone vibrate when it makes an automatic corection?
*/
(function() {
// Register ourselves in the keyboard's set of input methods
// The functions used here are all defined below
InputMethods.latin = {
init: init,
activate: activate,
deactivate: deactivate,
displaysCandidates: displaysCandidates,
click: click,
select: select,
dismissSuggestions: dismissSuggestions,
setLayoutPage: setLayoutPage,
setLayoutParams: setLayoutParams,
setLanguage: setLanguage,
selectionChange: selectionChange,
generateNearbyKeyMap: findNearbyKeys
};
// This is the object that is passed to init().
// We use the methods of this object to communicate with the keyboard.
var keyboard;
// If defined, this is a worker thread that produces word suggestions for us
var worker;
// PromiseStorage for access to indexedDB for user dictionary
var dbStore;
// These variables are the input method's state. Most of them are
// passed to the activate() method or are derived in that method.
var language; // The user's language
var inputMode; // The inputmode we're using: see getInputMode()
var capitalizing; // Are we auto-capitalizing for this activation?
var suggesting; // Are we offering suggestions for this activation?
var correcting; // Are we auto-correcting user input?
var punctuating; // Are we offering punctuation assistance?
var inputText; // The input text
var cursor; // The insertion position
var selection; // The end of the selection, if there is one, or 0
var lastSpaceTimestamp; // If the last key was a space, this is the timestamp
var layoutParams; // Parameters passed to setLayoutParams
var nearbyKeyMap; // Map keys to nearby keys
var serializedNearbyKeyMap; // A stringified version of the above
var idleTimer; // Used by deactivate
var suggestionsTimer; // Used by updateSuggestions;
var autoCorrection; // Correction to make if next input is space
var revertTo; // Revert to this on backspace after autocorrect
var revertFrom; // Revert away from this on backspace
var disableOnRevert; // Do we disable auto correction when reverting?
var correctionDisabled; // Temporarily diabled after reverting?
var currentPage; // The current layout page
var gotNormalKey; // Indicate if we got a normal key in an alt page
// Terminate the worker when the keyboard is inactive for this long.
var WORKER_TIMEOUT = 30000; // 30 seconds of idle time
// If we get an autorepeating key is sent to us, don't offer suggestions
// for this long, until we're pretty certain that the autorepeat
// has stopped.
var AUTOREPEAT_DELAY = 250;
// Some keycodes that we use
var SPACE = KeyEvent.DOM_VK_SPACE;
var BACKSPACE = KeyEvent.DOM_VK_BACK_SPACE;
var RETURN = KeyEvent.DOM_VK_RETURN;
var PERIOD = 46;
var QUESTION = 63;
var EXCLAMATION = 33;
var COMMA = 44;
var COLON = 58;
var SEMICOLON = 59;
var ATPERSAND = 64;
var DOUBLEQUOTE = 34;
var CLOSEPAREN = 41;
// all whitespace characters
// U+FFFC place holder is added to white space
// this enables suggestions
// when cursor is before place holder.
var WS = /^[\s\ufffc]+$/;
// word separator characters
// U+FFFC is the placeholder character for non-text object
var WORDSEP = /^[\s.,?!;:\ufffc]+$/;
var DOUBLE_SPACE_TIME = 700; // ms between spaces to convert to ". "
// Don't offer to autocorrect unless we're reasonably certain that the
// user wants this correction. The first suggested word must be at least
// this much more highly weighted than the second suggested word.
var AUTO_CORRECT_THRESHOLD = 1.30;
// If the length doesn't match between input and first suggestion.
// The min. frequency the first suggestion needs to have if we turn it into
// an autocorrect action
var MIN_LENGTH_MISMATCH_THRESHOLD = 5;
var USER_DICT_DB_NAME = 'UserDictLatin';
// Predictions from user dictionary need to have weight larger than this in
// order to take precedence over built-in dictionary predictions
var USER_DICT_PREDICTION_BUMP_THRESHOLD = 1;
// If this promise exists, that means we are
// currently loading the new dictionary.
var getDictionaryDataPromise = null;
// The promise for getting user dictionary from PromiseStorage
var getUserDictionaryBlobPromise = null;
// keyboard.js calls this to pass us the interface object we need
// to communicate with it
function init(interfaceObject) {
keyboard = interfaceObject;
dbStore = new PromiseStorage(USER_DICT_DB_NAME);
dbStore.start();
}
// Given the type property and inputmode attribute of a form element,
// this function returns the inputmode that this IM should use. The
// return value will be one of these strings:
//
// 'verbatim': don't alter the user's input at all
// 'latin': offer word suggestions/corrections, but no capitalization
// 'latin-prose': offer word suggestions and capitalization
//
function getInputMode(type, mode) {
// For text, textarea and search types, use the requested inputmode
// if it is valid and supported except numeric/digit mode. For
// numeric/digit mode, we return verbatim since no typing assitance
// is required. Otherwise default to latin for text and search and to
// latin-prose for textarea. For all other form fields, use verbatim mode
// so we never alter input.
switch (type) {
case 'text':
case 'textarea':
case 'search':
switch (mode) {
case 'verbatim':
case 'latin':
case 'latin-prose':
return mode;
case 'numeric':
case 'digit':
return 'verbatim';
default:
return (type === 'textarea') ? 'latin-prose' : 'latin';
}
break;
default:
return 'verbatim';
}
}
// This gets called whenever the keyboard pops up to tell us everything
// we need to provide useful typing assistance. It also gets called whenever
// the user taps on an input field to move the cursor. That means that there
// may be multiple calls to activate() without calls to deactivate between
// them.
function activate(lang, state, options) {
inputMode = getInputMode(state.type, state.inputmode);
inputText = state.value;
cursor = state.selectionStart;
if (state.selectionEnd > state.selectionStart) {
selection = state.selectionEnd;
} else {
selection = 0;
}
// Figure out what kind of input assistance we're providing for this
// activation.
capitalizing = punctuating = (inputMode === 'latin-prose');
suggesting = (options.suggest && inputMode !== 'verbatim');
correcting = (options.correct && inputMode !== 'verbatim');
// Some layouts (like French) need to disable punctuation autocorrection
// all the time.
if (options.correctPunctuation === false) {
punctuating = false;
}
// Reset our state
lastSpaceTimestamp = 0;
autoCorrection = null;
revertTo = revertFrom = '';
disableOnRevert = false;
correctionDisabled = false;
currentPage = PAGE_INDEX_DEFAULT;
gotNormalKey = false;
// The keyboard isn't idle anymore, so clear the timer
if (idleTimer) {
clearTimeout(idleTimer);
idleTimer = null;
}
// Start off with the correct capitalization
updateCapitalization();
// Get user dictionary blob.
getUserDictionaryBlobPromise =
dbStore.getItem('dictblob').then(function(blob) {
// If worker is there, tell it we have a (new) user dictionary blob,
// since setLanguage() may be bypassed due to language not changing.
// (User dictionary is langauge-neutral.)
// Do pass the argument if blob is undefined (no user dictionary words),
// Since the worker needs to know if user has deleted all words.
// Worker will bypass UserDictionary prediction as needed.
// |undefined| can't be Transferred so we'll need to check for it.
if (worker) {
worker.postMessage({
cmd: 'setUserDictionary',
args: [blob]
}, typeof blob === 'undefined' ? undefined : [blob]);
}
return blob;
})['catch'](function(e) {
e && console.error('latin.js', e);
});
// If we are going to offer suggestions, ensure that there is a worker
// thread created and that it knows what language we're using, and then
// start things off by requesting a first batch of suggestions.
if (suggesting || correcting) {
if (!worker || lang !== language) {
setLanguage(lang); // This calls updateSuggestions
} else {
updateSuggestions();
}
}
}
function deactivate() {
if (!worker || idleTimer) {
return;
}
idleTimer = setTimeout(terminateWorker, WORKER_TIMEOUT);
}
function terminateWorker() {
if (idleTimer) {
clearTimeout(idleTimer);
idleTimer = null;
}
if (worker) {
worker.terminate();
worker = null;
keyboard.sendCandidates([]); // Clear any displayed suggestions
autoCorrection = null; // and forget any pending correction.
}
}
function setLanguage(newlang) {
// If there is no worker and no language, or if there is a worker and
// the language has not changed, then there is nothing to do here.
if ((!worker && !newlang) || (worker && newlang === language)) {
return;
}
language = newlang;
// If there is a worker, and no new language, then kill the worker
if (worker && !newlang) {
terminateWorker();
return;
}
// Built-in dictionary is a hard requirement, i.e. if it is undefined, we
// reset ourselves.
// User dictionary is not a hard requirement. We swallow any abnormal
// situation there at activate(), to make sure Promise.all() doesn't fail
// on its rejection.
getDictionaryDataPromise = Promise.all([
keyboard.getData('dictionaries/' + newlang + '.dict'),
getUserDictionaryBlobPromise
]);
getDictionaryDataPromise
.then(function(values) {
getDictionaryDataPromise = null;
var builtInDict = values[0];
var userDict = values[1];
if (!builtInDict) {
console.error(
'latin.js: dictionary is specified but can\'t be loaded.');
language = undefined;
terminateWorker();
return;
}
setLanguageSync(newlang, builtInDict, userDict);
})['catch'](function(e) { // workaround gjslint error
e && console.error('latin.js', e);
getDictionaryDataPromise = null;
language = undefined;
terminateWorker();
})['catch'](function(e) { e && console.error(e); });
}
function setLanguageSync(newlang, dictData, userDict) {
// If we get here, then we have to create a worker and set its language
// or change the language of an existing worker.
if (!worker) {
// If we haven't created the worker before, do it now
worker = new Worker('js/imes/latin/worker.js');
if (layoutParams && nearbyKeyMap) {
worker.postMessage({ cmd: 'setNearbyKeys', args: [nearbyKeyMap]});
}
worker.onmessage = function(e) {
switch (e.data.cmd) {
case 'log':
console.log(e.data.message);
break;
case 'error':
console.error(e.data.message);
// If the error was a result of our setLanguage call, then
// kill the worker because it can't do anything without
// a valid dictionary.
if (e.data.message.startsWith('setLanguage')) {
terminateWorker();
}
break;
case 'predictions':
// The worker is suggesting words: ask the keyboard to display them
handleSuggestions(e.data.input, e.data.suggestions);
break;
}
};
}
// Tell the worker what language we're using and also pass the dictionary
// data.
// |undefined| can't be Transferred so we'll need to check for it.
worker.postMessage({
cmd: 'setLanguage',
args: [language, dictData, userDict]
}, typeof userDict === 'undefined' ? [dictData] : [dictData, userDict]);
// And now that we've changed the language, ask for new suggestions
updateSuggestions();
}
function displaysCandidates() {
// If we are suggesting for the language,
// we should always display the condidate panel.
return !!(suggesting && language);
}
/*
* The keyboard calls this method to tell us about user input.
*
* What we do with the input depends on various things:
*
* - whether we are suggesting, correcting, punctuating and/or capitalizing:
* these are controlled by settings, input mode and input type
*
* - whether there is a selected region in the input field
*
* - whether there is an auto-correction ready (when input is space
* or punctuation).
*
* - whether the last action was an autocorrection (when input is backspace)
*
* - the cursor position (affects suggestions, capitalization, etc.)
*
* If there is a selection just handle simple insertions and deletions
* with no extra behavior. (I think)
*
* If input is a space or punctuation:
*
* If there is a autocorrection ready, and we are correcting and
* the cursor is at the end of a word, make the correction
*
* If the previous character is a space, fix the punctuation. Note that
* this only works for a subset of the punctuation characters.
*
* Otherwise just insert it.
*
* If we're correcting and corrections are temporarily diabled, turn them
* back on.
*
* If input is a backspace:
*
* If we just did an auto-correction, revert it and turn off corrections
* until the next space or punctuation character.
*
* If we just inserted a suggested word that the user selected, revert
* the insertion, but don't disable autocorrect.
*
* Should we undo punctuation corrections this way, too?
*
* Otherwise, just delete the character before the cursor
*
* For any other input character, just insert it.
*
* Reset the backspace reversion state
*
* Update the capitalization state, if we're capitalizing
*/
function click(keyCode, repeat) {
// If the key is anything other than a backspace, forget about any
// previous changes that we would otherwise revert.
if (keyCode !== BACKSPACE) {
revertTo = revertFrom = '';
disableOnRevert = false;
}
var handler;
if (selection) {
// If there is selected text, don't do anything fancy here.
handler = handleKey(keyCode);
} else {
switch (keyCode) {
case SPACE: // This list of characters matches the WORDSEP regexp
case RETURN:
case PERIOD:
case QUESTION:
case EXCLAMATION:
case COMMA:
case COLON:
case SEMICOLON:
case DOUBLEQUOTE:
case CLOSEPAREN:
// These keys may trigger word or punctuation corrections
handler = handleCorrections(keyCode);
correctionDisabled = false;
break;
case BACKSPACE:
handler = handleBackspace(repeat);
break;
default:
handler = handleKey(keyCode);
}
}
// After the next key is resolved, we could update the state here.
return handler.then(function() {
// handleCorrections() above or it is now out of date, so clear it
// so it doesn't get used later
autoCorrection = null;
// And update the keyboard capitalization state, if necessary
updateCapitalization();
// If we're offering suggestions, ask the worker to make them now
updateSuggestions(repeat);
// Exit symbol layout mode after space or return key is pressed.
var isNonDefaultPage = currentPage !== PAGE_INDEX_DEFAULT;
var isSpace = keyCode === SPACE;
var isReturn = keyCode === RETURN;
var isAtpersand = keyCode === ATPERSAND;
var keyResetLayout = (isSpace && gotNormalKey) || isReturn || isAtpersand;
if (isNonDefaultPage && keyResetLayout) {
keyboard.setLayoutPage(PAGE_INDEX_DEFAULT);
}
// Next space key will change the layout
if (keyCode !== SPACE) {
gotNormalKey = true;
}
lastSpaceTimestamp = (keyCode === SPACE) ? Date.now() : 0;
}, function() {
// the previous sendKey or replaceSurroundingText has been rejected,
// No need to update the state.
})['catch'](function(e) { // ['catch'] for gjslint error
// Print the error and make sure inputSequencePromise always resolves.
console.error(e);
});
}
// Handle any key (including backspace) and do the right thing even if
// there is a selection in the text field. This method does not perform
// auto-correction or auto-punctuation.
function handleKey(keycode, repeat) {
// Generate the key event
return keyboard.sendKey(keycode, repeat).then(function() {
// First, update our internal state
if (keycode === BACKSPACE) {
if (selection) {
// backspace while a region is selected erases the selection
// and leaves the cursor at the selection start
inputText = inputText.substring(0, cursor) +
inputText.substring(selection);
selection = 0;
} else if (cursor > 0) {
cursor--;
inputText = inputText.substring(0, cursor) +
inputText.substring(cursor + 1);
// If we have temporarily disabled auto correction for the current
// word and we've just backspaced over the entire word, then we can
// re-enabled it again
if (correctionDisabled && !wordBeforeCursor()) {
correctionDisabled = false;
}
}
} else {
if (selection) {
inputText =
inputText.substring(0, cursor) +
String.fromCharCode(keycode) +
inputText.substring(selection);
selection = 0;
} else {
inputText =
inputText.substring(0, cursor) +
String.fromCharCode(keycode) +
inputText.substring(cursor);
}
cursor++;
}
}, function() {
// sendKey got canceled, keep state the same
});
}
// Assuming that the word before the cursor is oldWord, send a
// minimal number of key events to change it to newWord in the text
// field. Also update our internal state to match the new textfield
// content and cursor position.
function replaceBeforeCursor(oldWord, newWord) {
var oldWordLen = oldWord.length;
var replPromise =
keyboard.replaceSurroundingText(newWord, -oldWordLen, oldWordLen);
return replPromise.then(function() {
// Now update internal state
inputText =
inputText.substring(0, cursor - oldWordLen) +
newWord +
inputText.substring(cursor);
cursor += newWord.length - oldWordLen;
});
}
// If we just did auto correction or auto punctuation, then backspace
// should undo it. Otherwise it is just an ordinary backspace.
function handleBackspace(repeat) {
// If we made a correction and haven't changed it at all yet,
// then revert it.
var len = revertFrom ? revertFrom.length : 0;
if (len && cursor >= len &&
inputText.substring(cursor - len, cursor) === revertFrom) {
// Revert the content of the text field
return replaceBeforeCursor(revertFrom, revertTo).then(function() {
// If the change we just reverted was an auto-correction then
// temporarily disable auto correction until the next space
if (disableOnRevert) {
correctionDisabled = true;
}
revertFrom = revertTo = '';
disableOnRevert = false;
});
}
else {
return handleKey(BACKSPACE, repeat);
}
}
// This function is called when the user types space, return or a punctuation
// character. It performs auto correction or auto punctuation or just
// inserts the character.
function handleCorrections(keycode) {
if (correcting && autoCorrection && !correctionDisabled && atWordEnd() &&
wordBeforeCursor() !== autoCorrection) {
return autoCorrect(keycode);
}
else if (punctuating && cursor >= 2 &&
isWhiteSpace(inputText[cursor - 1]) &&
inputText[cursor - 1].charCodeAt(0) !== KeyEvent.DOM_VK_RETURN &&
!WORDSEP.test(inputText[cursor - 2]))
{
return autoPunctuate(keycode);
}
else {
return handleKey(keycode);
}
}
// Perform an autocorrection. Assumes that all pre-conditions for
// auto-correction have been met.
function autoCorrect(keycode) {
// Get the word before the cursor
var currentWord = wordBeforeCursor();
// Figure out the auto correction text
var newWord = autoCorrection;
// Make the correction
return replaceBeforeCursor(currentWord, newWord).then(function() {
// Remember the change we just made so we can revert it if the
// user types backspace
revertTo = currentWord;
revertFrom = newWord;
disableOnRevert = true;
}).then(function() {
// Send the keycode as separate key event because it may get canceled
return handleKey(keycode).then(function() {
revertFrom += String.fromCharCode(keycode);
});
});
}
// Auto punctuate, converting space punctuation to punctuation space
// or converting space space to period space if the two spaces were
// close enough together. Assumes that pre-conditions for auto punctuation
// have been met.
function autoPunctuate(keycode) {
switch (keycode) {
case SPACE:
if (Date.now() - lastSpaceTimestamp < DOUBLE_SPACE_TIME) {
return fixPunctuation(PERIOD, SPACE);
} else {
return handleKey(keycode);
}
break;
case PERIOD:
case QUESTION:
case EXCLAMATION:
case COMMA:
return fixPunctuation(keycode);
default:
// colon and semicolon don't auto-punctuate because they're
// used after spaces for smileys.
return handleKey(keycode);
}
// In both the space space and the space period case we call this function
// Second argument is the character reverting to if cancelling auto
// punctuation
// If the second argument is omitted, assume it is the same as the first
function fixPunctuation(keycode, revertToKeycode) {
return keyboard.sendKey(BACKSPACE)
.then(function() {
return keyboard.sendKey(keycode);
})
.then(function() {
return keyboard.sendKey(SPACE);
})
.then(function() {
var newtext = String.fromCharCode(keycode) + ' ';
inputText = inputText.substring(0, cursor - 1) +
newtext +
inputText.substring(cursor);
cursor++;
// Remember this change so we can revert it on backspace
revertTo = ' ' + String.fromCharCode(revertToKeycode || keycode);
revertFrom = newtext;
disableOnRevert = false;
});
}
}
// When the worker thread sends us a batch of suggestions, deal
// with them here.
function handleSuggestions(input, suggestions) {
// If we didn't get any suggestions just send the empty array to
// clear any suggestions that are currently displayed. Do the same
// if the word before the cursor has changed since we requested
// these suggestions. That is, if the user has typed faster than we could
// offer suggestions, ignore them.
if (suggestions.length === 0 || wordBeforeCursor() !== input) {
keyboard.sendCandidates([]); // Clear any displayed suggestions
return;
}
// If input is ucase, then make all suggestions ucase as well.
// Ignore input.length of 1, it never gets autocorrected anyway
if (input.length > 1 && isUpperCase(input)) {
for (var ix = 0; ix < suggestions.length; ix++) {
suggestions[ix][0] = suggestions[ix][0].toUpperCase();
}
}
// We show no more than 3 suggestions; but we'd like to keep at least one
// suggestion from user dictionary, if its weight is less than 1. However,
// that suggestion can still be dropped if it matches the user input.
// User dictionary suggestion is identified by suggestion[2] === true.
var inputDefinedInUserDict = false;
// See if the user's input is a valid word on the list of suggestions
var inputIsSuggestion = false;
var inputWeight = 0;
var inputIndex;
for (inputIndex = 0; inputIndex < suggestions.length; inputIndex++) {
if (suggestions[inputIndex][0] === input) {
inputIsSuggestion = true;
inputDefinedInUserDict = !!suggestions[inputIndex][2];
inputWeight = suggestions[inputIndex][1];
break;
}
}
// We never want to display the user's input as a suggestion so
// remove it from the list if it is there.
if (inputIsSuggestion) {
suggestions.splice(inputIndex, 1);
}
// If we don't have any suggestions we're done
if (suggestions.length === 0) {
keyboard.sendCandidates([]); // Clear any displayed suggestions
return;
}
//
// If there was an exact match for the user input in the suggestions
// it has already been removed. Now we need to loop through again
// looking for approximate matches and bump those to the start of the
// list. A word is an approximate match if all the letters match
// when we ignore case, apostrophes and hyphens. The assumption here
// is that the user is not expected to have to use the shift key or
// the alternate forms menu or the punctuation menus. If they type all
// the right letters, a matching word should have higher priority than
// a non-matching word. In particular this code ensures that "im" gets
// autocorrected to "I'm" instead of "in" and "id" gets autocorrected to
// "I'd" instead of "is". (Bug 1164421)
//
// XXX In English, apostrophes and hyphens are the only punctuation that
// commonly occurs within words. If other wordlists include other
// punctuation, we may need to add them to the regular expression below.
//
// XXX In the future, we might want to generalize this to consider
// accented forms when looking for approximate matches. If "jose"
// was autocorrecting to "hose" instead of "José", for example, then
// we might need to extend the definition of an approximate match here.
// For now, I don't have any examples of corrections that are coming
// out incorrectly for accented letters, though, so am keeping this
// simple.
//
// We loop backward through the suggestions, bumping any approximate
// match to the start of the list. This ensures that the highest ranked
// approximate match comes before lower ranked approximate matches,
// and all approximate matches come before non-matching suggestions.
//
var i = suggestions.length - 1;
var approximateMatches = 0; // how many approximate matches we've bumped
var lcInput = input.toLowerCase();
while(i > approximateMatches) {
if (suggestions[i][0].toLowerCase().replace(/[-']/g, '') === lcInput) {
// If this suggestion was an approximate match for the users input..
var match = suggestions[i]; // get the match,
suggestions.splice(i, 1); // remove it,
suggestions.unshift(match); // and put it back at the start.
approximateMatches++; // Increment this instead of i-- here.
}
else {
i--; // if not an approximate match move to next suggestion
}
}
// Make sure we have no more than three words
if (suggestions.length > 3) {
// We want to keep at least a user dictionary word here (if the heighest
// user dictionary word has weight >= 1).
// If for the first two suggestions we see one from user dictionary, or
// if we dropped a user dict suggestion above (matching user input),
// we can just append the third suggestion.
// Otherwise, we'll need to search through the remaining suggestions and
// append the first user-dict suggestion we find (that has weight >= 1);
// if we can't find any suitable user-dict suggestions, we just append
// the third suggestion (i.e. whatever that has the largest frequency
// in the remaining suggestions).
var trimmedSuggestions = suggestions.slice(0, 2);
var userDictionaryWordEncountered =
inputDefinedInUserDict ||
(!!suggestions[0][2]) ||
(!!suggestions[1][2]);
if (userDictionaryWordEncountered) {
trimmedSuggestions.push(suggestions[2]);
} else {
userDictionaryWordEncountered =
suggestions.slice(2).some(function(suggestion) {
if (suggestion[2] && suggestion[1] >=
USER_DICT_PREDICTION_BUMP_THRESHOLD) {
trimmedSuggestions.push(suggestion);
return true;
} else {
return false;
}
});
if (!userDictionaryWordEncountered) {
trimmedSuggestions.push(suggestions[2]);
}
}
suggestions = trimmedSuggestions;
}
// Now get an array of just the suggested words
var words = suggestions.map(function(x) { return x[0]; });
// see whether words[0] and input have same length
var lengthMismatch;
switch (Math.abs(input.length - words[0].length)) {
case 0:
lengthMismatch = false;
break;
case 1:
lengthMismatch = suggestions[0][1] < MIN_LENGTH_MISMATCH_THRESHOLD;
break;
default:
lengthMismatch = true;
break;
}
// Decide whether the first word is going to be an autocorrection.
// If the user's input is already a valid word, then don't
// autocorrect unless the first suggested word is more common than
// the input. Note that if the first suggested word has a higher
// weight even after whatever penalty is applied for not matching
// exactly, then it is significantly more common than the actual input.
// (This rule means that "ill" will autocorrect to "I'll",
// "wont" to "won't", etc.)
// Also, don't autocorrect if the input is a single letter and
// the first word is more than a single letter. (But still autocorrect
// "i" to "I")
if (correcting &&
!correctionDisabled &&
(!inputIsSuggestion ||
suggestions[0][1] > inputWeight * AUTO_CORRECT_THRESHOLD) &&
(input.length > 1 || words[0].length === 1) &&
!lengthMismatch) {
// Remember the word to use if the next character is a space.
autoCorrection = words[0];
// Mark the auto-correction so the renderer can highlight it
words[0] = '*' + words[0];
}
else {
autoCorrection = null;
}
keyboard.sendCandidates(words);
}
//
// If the user selects one of the suggestions offered by this input method
// the keyboard calls this method to tell us it has been selected.
// We have to delete the current word, insert this new word, and
// update our internal state to match.
// word: the text displayed as the suggestion, might contain ellipsis
// data: the actual data we need to output
// In the past we would automatically insert a space after the selected
// word, but that, combined with the revert-on-backspace behavior made
// it impossible to add a suffix to the selected word.
//
function select(word, data) {
var oldWord = wordBeforeCursor();
var newWord = data;
// The user has selected a suggestion, so we don't need to display
// them anymore. Note that calling this function resets all the
// autocorrect state. We'll reset much of that state below after
// the word has been replace with the new one.
dismissSuggestions();
return replaceBeforeCursor(oldWord, newWord).then(function() {
// Remember the change we just made so we can revert it if the
// next key is a backspace. If the word is reverted we disable
// autocorrection for this word.
revertFrom = newWord;
revertTo = oldWord;
// Given that the user has selected this word, we don't ever
// want to autocorrect the word because if they keep typing to
// add a suffix to the word, we don't want to modify the
// original. This also means that if they revert the selection
// autocorrect will still be disabled which is what we want.
// Note, however, that most often the user will type a space
// after this selection and autocorrect will be enabled again.
correctionDisabled = true;
// And update the keyboard capitalization state, if necessary
updateCapitalization();
});
}
function dismissSuggestions() {
// Clear the list of candidates
keyboard.sendCandidates([]);
// Get rid of any autocorrection that is pending and reset the rest
// of our state, too.
lastSpaceTimestamp = 0;
autoCorrection = null;
revertTo = revertFrom = '';
disableOnRevert = false;
correctionDisabled = false;
}
function setLayoutPage(page) {
if (currentPage === PAGE_INDEX_DEFAULT) {
// we don't want to reset gotNormalKey if we're already in an alternative
// layout.
gotNormalKey = false;
}
currentPage = page;
}
function setLayoutParams(params) {
layoutParams = params;
// We don't need to update the nearbyKeys when using number/digit layout.
if (inputMode === 'verbatim') {
return;
}
// XXX We call nearbyKeys() every time the keyboard pops up.
// Maybe it would be better to compute it once in keyboard.js and
// cache it.
// We get called every time the keyboard case changes. Don't bother
// passing this data to the prediction engine if nothing has changed.
var newmap = findNearbyKeys(params);
var serialized = JSON.stringify(newmap);
if (serialized === serializedNearbyKeyMap) {
return;
}
nearbyKeyMap = newmap;
serializedNearbyKeyMap = serialized;
if (worker) {
worker.postMessage({ cmd: 'setNearbyKeys', args: [nearbyKeyMap]});
// Ask for new suggestions since the new layout may affect them.
// (When switching from QWERTY to Dvorak, e.g.)
updateSuggestions();
}
}
function findNearbyKeys(layout) {
var nearbyKeys = {};
var keys = layout.keyArray;
// Make sure that all the keycodes are lowercase, not uppercase
for (var p = 0; p < keys.length; ++p) {
keys[p].code =