VulpesOS/gaia/apps/keyboard/js/imes/latin/worker.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

316 lines
10 KiB
JavaScript
Executable file

'use strict';
/* global Predictions */
//
// The Latin input method provides word suggestions/auto completions
// using the algorithm in predictions.js. For efficiency, however, this code
// runs in a different thread. This is the web worker wrapper around
// predictions.js. This file defines the code that interfaces predictions.js
// with latin.js.
//
// This worker uses the convention that all messages send or received have
// "cmd" and "args" fields. cmd is the message name or command. args is
// an array of arguments required by the command, in a form suitable for
// passing to Function.apply().
//
// This worker responds to the following messages:
//
// setLanguage: the input method uses this message to tell the worker
// to load a dictionary for the prediction engine. If no such dictionary
// is found or if the dictionary is invalid, the worker responds with
// an "error" message.
//
// setNearbyKeys: the input method uses this message to pass information about
// what keys are near each other in the current keyboard layout
//
// predict: the input method uses this message to ask the prediction
// engine to suggestion completions for (or corrections to) the specified
// string. The worker responds with a "predictions" message whose argument
// is an array of up to 3 predicted words. If the the specified string
// is a word in the dictionary, the worker may send a "word" message
// first to quickly tell the keyboard that the word is valid.
//
// Load the predictions.js module. This defines the Predictions object.
importScripts('predictions.js');
// We invoke a "null" predictor when user dictionary is empty (incoming dict
// blob is "undefined"). This is to avoid modifying the complex predictions.js
// to adapt to "undefined" dict blob.
// The switching mechanism is implemented in ProxyUserDictPredictor depending on
// its setDictionary parameter when called. Note that PUDP.setNearByKeys is
// always called upon userDictPredictor for setting the predictor up.
// For pattern conformity in this file we don't use "new" to instantiate the
// null and proxy instances.
var NullPredictions = function(){
return {
predict: function(input, maxSuggestions, maxCandidates, maxCorrections,
callback, onerror) {
var status = {
state: 'predicting',
abort: function nullAbort() {
if (this.state !== 'done' && this.state !== 'aborted') {
this.state = 'aborting';
}
}
};
setTimeout(function nullFinish() {
if (status.state === 'aborting') {
status.state = 'aborted';
}
if (status.state === 'aborted') {
return;
}
status.state = 'done';
callback([]);
});
return status;
}
};
};
var ProxyUserDictPredictor = function(){
var userDictPredictor = Predictions();
var nullPredictor = NullPredictions();
var activePredictor;
return {
setDictionary: function(buffer) {
if (buffer === undefined) {
activePredictor = nullPredictor;
} else {
activePredictor = userDictPredictor;
userDictPredictor.setDictionary(buffer);
}
},
setNearbyKeys: function(data) {
userDictPredictor.setNearbyKeys(data);
},
// Directly pass arguments to wrapped predictor
predict: function(/* args */) {
return activePredictor.predict.apply(activePredictor, arguments);
}
};
};
var builtInPredictor = Predictions();
var userDictPredictor = ProxyUserDictPredictor();
// Track our current language so we don't load dictionaries more often
// than we have to.
var currentLanguage;
// The prediction that is currently running, if any. So that it can be cancelled
var pendingBuiltInPrediction;
var pendingUserDictPrediction;
var Commands = {
setLanguage: function setLanguage(language, builtInDict, userDict) {
var _setLanguage = function(predictor, dict, tag) {
try {
predictor.setDictionary(dict);
postMessage({
cmd: 'success',
fn: 'setLanguage/' + tag,
language: language
});
} catch (e) {
postMessage({
cmd: 'error',
message: 'setDictionary/' + tag + ' failed: ' + e
});
}
};
// always set userDict regardless of language change or not
_setLanguage(userDictPredictor, userDict, 'User');
if (language === currentLanguage) {
return;
}
currentLanguage = language;
_setLanguage(builtInPredictor, builtInDict, 'BuiltIn');
},
setUserDictionary: function setUserDictionary(userDict) {
try {
userDictPredictor.setDictionary(userDict);
postMessage({cmd: 'success', fn: 'setUserDictionary'});
}
catch (e) {
postMessage({cmd: 'error', fn: 'setUserDictionary: ' + e.message});
}
},
setNearbyKeys: function setNearbyKeys(nearbyKeys) {
var _setNearbyKeys = function(predictor, tag) {
try {
predictor.setNearbyKeys(nearbyKeys);
postMessage({ cmd: 'success', fn: 'setNearbyKeys/' + tag });
}
catch (e) {
postMessage({
cmd: 'error', message: 'setNearbyKeys/' + tag + ': ' + e.message});
}
};
_setNearbyKeys(builtInPredictor, 'BuiltIn');
_setNearbyKeys(userDictPredictor, 'User');
},
predict: function predict(prefix) {
// Make sure we're not still running a previous one
if (pendingBuiltInPrediction) {
pendingBuiltInPrediction.abort();
}
if (pendingUserDictPrediction) {
pendingUserDictPrediction.abort();
}
// We merge predictions from built-in dictionaries and user dictionarys by:
// [1] For UserDict, ask for 4 predictions with 40 candidates, and
// considering only words with an edit distance of 1 (i.e. make only one
// correction per word)
// [2] For Built-In dict, everything remains the same (50 candidates if
// prefix is short, 24 if not; do a second round:
// [a] if there were results but results were bad; if prefix is long,
// use 50 candidates.
// [b] if there weren't results, use 60 candidates and allow 2
// corrections.
// As before weget 4 predictions.
// [3] Results from [1] and [2] are merged by:
// [a] Results from [1] are not discarded even if quality is low.
// [b] Results from [1] are annotated to come from user dict.
// [c] Results are merged and re-sorted. Duplicated words (case- and
// variants-sensitive) are eliminated by:
// - Keeping the higher priority.
// - Keeping the user dictionary flag.
// The rationale is: latin.js will always show at least one user
// dictionary word; since user specifically define some dict word,
// he probably wants it very much.
// [4] latin.js will always retain at least one user dict prediction (unless
// it decides to drop the prediction because it equals user input)
var BUILT_IN_CANDIDATES_LOW = 24;
var BUILT_IN_CANDIDATES_HIGH = 50;
var BUILT_IN_CANDIDATES_EXTRA = 60;
var USER_DICT_CANDIDATES = 40;
var BUILT_IN_TOO_LOW_THRESHOLD = 4;
var builtInPromise = new Promise(function(resolve, reject){
var success = function(suggestions) {
if (suggestions.length > 0) {
// If the quality of the suggestions is very low, up candidates
if (prefix.length > 4 &&
suggestions[0][1] < BUILT_IN_TOO_LOW_THRESHOLD) {
pendingBuiltInPrediction =
builtInPredictor.predict(
prefix, 4, BUILT_IN_CANDIDATES_HIGH, 1,
function(suggestions){resolve(suggestions);},
reject);
}
else {
resolve(suggestions);
}
}
else {
pendingBuiltInPrediction =
builtInPredictor.predict(
prefix, 4, BUILT_IN_CANDIDATES_EXTRA, 2,
function(suggestions){resolve(suggestions);},
reject);
}
};
var noOfCandidates = prefix.length >= 2 && prefix.length <= 4 ?
BUILT_IN_CANDIDATES_HIGH :
BUILT_IN_CANDIDATES_LOW;
pendingBuiltInPrediction =
builtInPredictor.predict(prefix, 4, noOfCandidates, 1, success, reject);
});
var userDictPromise = new Promise(function(resolve, reject){
var success = function(suggestions) {
suggestions = suggestions.map(
function(suggestion){
return [
suggestion[0],
suggestion[1],
true // coming from user dictionary
];
}
);
resolve(suggestions);
};
pendingUserDictPrediction =
userDictPredictor.predict(prefix, 4, USER_DICT_CANDIDATES, 1,
success, reject);
});
Promise.all([builtInPromise, userDictPromise])
.then(function(suggestions2D){
// we could do a correct merge sort algorithm here, but since the arrays
// are so small (only 4*2 entries), let's just concat and apply sort()
var suggestions =
[].concat(suggestions2D[0]).concat(suggestions2D[1]).sort(
function(suggestionA, suggestionB){
return suggestionB[1] - suggestionA[1];
}
);
// Shortcut on eliminate duplicated entries: "duplicated entries" means
// they're coming from both dictionaries; and since our array is sorted,
// we'll simply the (higher-priority one's) suggestion[2] as true (= from
// user dict) and delete the lower-priority one.
// map from "word" to the index in suggestions
var seenWord = {};
suggestions = suggestions.reduce(function(prev, curr, index){
var word = curr[0];
// we've seen this word. mark the higher-priority one as from user-dict
// and don't push this one to the resulting array.
if (seenWord.hasOwnProperty(word)) {
prev[seenWord[word]][2] = true;
} else {
seenWord[word] = index;
prev.push(curr);
}
return prev;
}, []);
postMessage({
cmd: 'predictions', input: prefix, suggestions: suggestions
});
}).catch(function(e){
log('Error in prediction: ' + e);
});
}
};
// When we receive messages, translate them into function calls to one
// of the functions in the Commands object below.
self.onmessage = function(e) {
Commands[e.data.cmd].apply(null, e.data.args);
};
// Send console messages back to the main thread with this method
function log(msg) {
postMessage({cmd: 'log', message: msg});
}