VulpesOS/gaia/apps/keyboard/js/imes/jspinyin/jspinyin.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

771 lines
18 KiB
JavaScript
Executable file

/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* global KeyEvent, define, InputMethods */
'use strict';
(function() {
var debugging = false;
var debug = function jspinyin_debug(str) {
if (!debugging) {
return;
}
var done = false;
if (typeof window != 'undefined' && window.dump) {
window.dump('jspinyin: ' + str + '\n');
done = true;
}
if (typeof console != 'undefined' && console.log) {
console.log('jspinyin: ' + str);
if (arguments.length > 1) {
console.log.apply(this, arguments);
}
done = true;
}
if (done) {
return;
}
if (typeof Test != 'undefined') {
print('jspinyin: ' + str + '\n');
}
};
var assert = function jspinyin_assert(condition, msg) {
if (!debugging) {
return;
}
if (!condition) {
var str = typeof msg === 'undefined' ? assert.caller.toString() : msg;
if (typeof alert === 'function') {
alert(msg);
} else {
throw str;
}
}
};
var IMEngineBase = function engineBase_constructor() {
this._glue = {};
};
IMEngineBase.prototype = {
/**
* Glue ojbect between the IMEngieBase and the IMEManager.
*/
_glue: {
/**
* The source code path of the IMEngine
* @type String
*/
path: '',
/**
* Sends candidates to the IMEManager
*/
sendCandidates: function(candidates) {},
/**
* Sends pending symbols to the IMEManager.
*/
sendPendingSymbols: function(symbols) {},
/**
* Passes the clicked key to IMEManager for default action.
* @param {number} keyCode The key code of an integer.
*/
sendKey: function(keyCode) {},
/**
* Sends the input string to the IMEManager.
* @param {String} str The input string.
*/
sendString: function(str) {}
},
/**
* Initialization.
* @param {Glue} glue Glue object of the IMManager.
*/
init: function engineBase_init(glue) {
this._glue = glue;
},
/**
* Destruction.
*/
uninit: function engineBase_uninit() {
},
/**
* Notifies when a keyboard key is clicked.
* @param {number} keyCode The key code of an integer number.
*/
click: function engineBase_click(keyCode) {
},
/**
* Notifies when pending symbols need be cleared
*/
empty: function engineBase_empty() {
},
/**
* Notifies when a candidate is selected.
* @param {String} text The text of the candidate.
* @param {Object} data User data of the candidate.
*/
select: function engineBase_select(text, data) {
},
/**
* Notifies when selection changes
*/
stateChange: function engineBase_stateChange(detail) {
},
/**
* Notifies when surrounding text changes
*/
surroundingtextChange: function engineBase_surroundingtextChange(detail) {
},
/**
* Notifies when the IM is shown
*/
activate: function engineBase_activate(language, state, options) {
},
/**
* Called when the keyboard is hidden
*/
deactivate: function engineBase_deactivate() {
}
};
var emEngineWrapper = {
_worker: null,
_callback: null,
_initialized: false,
post: function(id, param, callback) {
if (!this._initialized && id != 'init') {
throw 'Database not ready!';
}
if (!this._callback[id]) {
this._callback[id] = [];
}
this._callback[id].push(callback);
this._worker.postMessage({
id: id,
param: param
});
return true;
},
init: function(path, byteArray, callback) {
if (this._initialized) {
callback(true);
return;
}
var self = this;
this._callback = {};
this._worker = new Worker(path + '/worker.js');
this._worker.onmessage = function(e) {
var data = e.data;
switch (data.id) {
case 'message':
console.log('emEngineWrapper: ' + data.returnValue);
break;
default:
var msgCallback = self._callback[data.id].shift();
if (msgCallback) {
msgCallback(data.returnValue);
}
}
};
this.post('init', {
userDict: byteArray
}, function(isOk) {
if (isOk) {
self._initialized = true;
} else {
self.uninit();
}
callback(isOk);
});
},
uninit: function() {
if (this._worker) {
this._worker.terminate();
}
this._worker = null;
this._callback = null;
this._initialized = false;
},
isReady: function() {
return this._initialized;
}
};
var IMEngine = function engine_constructor() {
IMEngineBase.call(this);
};
// We use keycode 65 to represent "'" for pinyin input method
var SPECIAL_KEYCODE_FOR_DELIMITER = 65;
IMEngine.prototype = {
// Implements IMEngineBase
__proto__: new IMEngineBase(),
// Buffer limit will force output the longest matching terms
// if the length of the syllables buffer is reached.
_kBufferLenLimit: 30,
// Remember the candidate length of last searching result because we don't
// want to output all candidates at a time.
// Set it to 0 when we don't need the candidates buffer anymore.
_candidatesLength: 0,
_workerTimeout: 600000,
/**
* The last selected text used to generate prediction.
* @type string
*/
_historyText: '',
_pendingSymbols: '',
_firstCandidate: '',
_keypressQueue: [],
_isWorking: false,
_isActive: false,
_uninitTimer: null,
_sendPendingSymbols: function engine_sendPendingSymbols() {
debug('SendPendingSymbol: ' + this._pendingSymbols);
if (this._pendingSymbols) {
var self = this;
emEngineWrapper.post(
'im_get_pending_symbols_info',
{},
function(returnValue) {
var fixedLen = returnValue.fixedLen;
var splStart = returnValue.splStart;
var splStartLen = splStart.length;
var display = self._firstCandidate.substring(0, fixedLen);
if (splStartLen > 1) {
for (var i = fixedLen; i < splStartLen - 1; i++) {
display += self._pendingSymbols.substring(splStart[i],
splStart[i + 1]) + ' ';
}
display +=
self._pendingSymbols.substring(splStart[splStartLen - 1]);
} else {
display += self._pendingSymbols;
}
self._glue.setComposition(display.trim());
}
);
} else {
this._glue.endComposition();
}
},
/**
* Send candidates list.
* @param {Array.<string>} candidates The candidates to be sent.
* @return {void} No return value.
*/
_sendCandidates: function engine_sendCandidates(candidates) {
var list = [];
var len = candidates.length;
for (var id = 0; id < len; id++) {
var cand = candidates[id];
if (id === 0) {
this._firstCandidate = cand;
}
list.push([cand, id]);
}
this._glue.sendCandidates(list);
},
_start: function engine_start() {
if (this._isWorking) {
return;
}
if (!emEngineWrapper.isReady()) {
debug('emEngineWrapper is not ready!');
return;
}
this._isWorking = true;
debug('Start keyQueue loop.');
this._next();
},
_next: function engine_next() {
debug('Processing keypress');
if (!this._keypressQueue.length) {
debug('keyQueue emptied.');
this._isWorking = false;
return;
}
var code = this._keypressQueue.shift();
// We use keycode 65 to represent "'" for pinyin input method
var realCode = (code === SPECIAL_KEYCODE_FOR_DELIMITER) ? 39 : code;
if (code === 0) {
// This is a select function operation.
this._updateCandidatesAndSymbols(this._next.bind(this));
return;
}
debug('key code: ' + code);
// Backspace - delete last input symbol if exists
if (code === KeyEvent.DOM_VK_BACK_SPACE) {
debug('Backspace key');
if (!this._pendingSymbols) {
if (this._firstCandidate) {
debug('Remove candidates.');
// prevent updateCandidateList from making the same suggestions
this.empty();
}
// pass the key to IMEManager for default action
debug('Default action.');
this._glue.sendKey(realCode);
this._next();
} else {
this._pendingSymbols = this._pendingSymbols.substring(0,
this._pendingSymbols.length - 1);
this._updateCandidatesAndSymbols(this._next.bind(this));
}
return;
}
// Select the first candidate if needed.
if (code === KeyEvent.DOM_VK_RETURN ||
!this._isPinyinKey(code) ||
this._pendingSymbols.length >= this._kBufferLenLimit) {
debug('Non-pinyin key is pressed or the input is too long.');
var sendKey = true;
if (this._firstCandidate) {
if (this._pendingSymbols) {
// candidate list exists; output the first candidate
debug('Sending first candidate.');
this._glue.endComposition(this._firstCandidate);
// no return here
if (code === KeyEvent.DOM_VK_RETURN) {
sendKey = false;
}
}
this._sendCandidates([]);
}
//pass the key to IMEManager for default action
debug('Default action.');
this.empty();
if (sendKey) {
this._glue.sendKey(realCode);
}
this._next();
return;
}
debug('Processing symbol: ' + String.fromCharCode(realCode));
// add symbol to pendingSymbols
this._appendNewSymbol(realCode);
this._updateCandidatesAndSymbols(this._next.bind(this));
},
_isPinyinKey: function engine_isPinyinKey(code) {
// keycode 65 is used to represent "'" for pinyin input method
// a-z
if ((code >= 97 && code <= 122) ||
code === SPECIAL_KEYCODE_FOR_DELIMITER) {
return true;
}
return false;
},
_appendNewSymbol: function engine_appendNewSymbol(code) {
var symbol = String.fromCharCode(code);
this._pendingSymbols += symbol;
},
_updateCandidatesAndSymbols: function engine_updateCandsAndSymbols(callback) {
var self = this;
this._updateCandidateList(function() {
self._sendPendingSymbols();
callback();
});
},
_updateCandidateList: function engine_updateCandidateList(callback) {
debug('Update Candidate List.');
var self = this;
var numberOfCandidatesPerRow = this._glue.getNumberOfCandidatesPerRow ?
this._glue.getNumberOfCandidatesPerRow() : Number.Infinity;
this._candidatesLength = 0;
if (!this._pendingSymbols) {
// If there is no pending symbols, make prediction with the previous
// select words.
if (this._historyText) {
debug('Buffer is empty; make suggestions based on select term.');
emEngineWrapper.post('im_search_predicts', {
queryString: this._historyText,
limit: numberOfCandidatesPerRow + 1
}, function(returnValue) {
var num = returnValue.length;
var predicts = returnValue.results;
if (num > numberOfCandidatesPerRow + 1) {
self._candidatesLength = num;
}
self._sendCandidates(predicts);
callback();
});
} else {
this._sendCandidates([]);
callback();
}
} else {
// Update the candidates list by the pending pinyin string.
this._historyText = '';
emEngineWrapper.post('im_search', {
queryString: this._pendingSymbols,
limit: numberOfCandidatesPerRow + 1
}, function(returnValue) {
var num = returnValue.length;
var candidates = returnValue.results;
if (num > numberOfCandidatesPerRow + 1) {
self._candidatesLength = num;
}
self._sendCandidates(candidates);
callback();
});
}
},
_resetKeypressQueue: function engine_abortKeypressQueue() {
this._keypressQueue = [];
this._isWorking = false;
},
_accessUserDict: function engine_loadUserDict(action, param, callback) {
var indexedDB = window.indexedDB;
if (!indexedDB) {
callback(null);
return;
}
// Access user_dict.data from IndexedDB
var dbVersion = 1;
var STORE_NAME = 'files';
var USER_DICT = 'user_dict';
var request = indexedDB.open('EmpinyinDatabase', dbVersion);
request.onerror = function opendb_onerror(event) {
debug('Error occurs when openning database: ' + event.target.errorCode);
callback(null);
};
request.onsuccess = function opendb_onsuccess(event) {
var db = event.target.result;
var request;
if (action == 'load') {
request = db.transaction([STORE_NAME], 'readonly')
.objectStore(STORE_NAME).get(USER_DICT);
request.onsuccess = function readdb_oncomplete(event) {
db.close();
if (!event.target.result) {
callback(null);
} else {
callback(event.target.result.content);
}
};
request.onerror = function readdb_oncomplete(event) {
debug('Failed to read file from DB: ' + event.target.result.name);
db.close();
callback(null);
};
} else if (action == 'save') {
var obj = {
name: USER_DICT,
content: param
};
request = db.transaction([STORE_NAME], 'readwrite')
.objectStore(STORE_NAME).put(obj);
request.onsuccess = function readdb_oncomplete(event) {
db.close();
callback(true);
};
request.onerror = function readdb_oncomplete(event) {
debug('Failed to write file to DB: ' + event.target.result.name);
db.close();
callback(false);
};
}
};
request.onupgradeneeded = function opendb_onupgradeneeded(event) {
var db = event.target.result;
// Delete the old ObjectStore if present
if (db.objectStoreNames.length !== 0) {
db.deleteObjectStore(STORE_NAME);
}
db.createObjectStore(STORE_NAME, { keyPath: 'name' });
};
},
/**
* Override
*/
init: function engine_init(glue) {
IMEngineBase.prototype.init.call(this, glue);
debug('init.');
},
/**
* Override
*/
uninit: function engine_uninit() {
IMEngineBase.prototype.uninit.call(this);
debug('Uninit.');
if (this._uninitTimer) {
clearTimeout(this._uninitTimer);
this._uninitTimer = null;
}
if (emEngineWrapper.isReady()) {
emEngineWrapper.uninit();
}
this._resetKeypressQueue();
this.empty();
},
/**
*Override
*/
click: function engine_click(keyCode) {
IMEngineBase.prototype.click.call(this, keyCode);
this._keypressQueue.push(keyCode);
this._start();
},
/**
* Override
*/
select: function engine_select(text, data) {
IMEngineBase.prototype.select.call(this, text, data);
if (!emEngineWrapper.isReady()) {
return;
}
var self = this;
var nextStep = function(text) {
if (text) {
if (self._pendingSymbols) {
self._pendingSymbols = '';
self._glue.endComposition(text);
} else {
self._glue.setComposition('');
self._glue.endComposition(text);
}
self._historyText = text;
self._candidatesLength = 0;
}
self._keypressQueue.push(0);
self._start();
};
if (this._pendingSymbols) {
emEngineWrapper.post('im_choose', {
candId: parseInt(data)
}, nextStep);
} else {
// A predication candidate is selected.
nextStep(text);
}
},
/**
* Override
*/
empty: function engine_empty() {
IMEngineBase.prototype.empty.call(this);
debug('empty.');
this._pendingSymbols = '';
this._historyText = '';
this._firstCandidate = '';
this._sendPendingSymbols();
this._sendCandidates([]);
},
/**
* Override
*/
stateChange: function engine_stateChange(detail) {
debug('stateChange');
if (detail.ownAction) {
return;
}
this.empty();
},
/**
* Override
*/
activate: function engine_activate(language, state, options) {
IMEngineBase.prototype.activate.call(this, language, state, options);
if (this._uninitTimer) {
clearTimeout(this._uninitTimer);
this._uninitTimer = null;
}
var inputType = state.type;
debug('Activate. Input type: ' + inputType);
if (!emEngineWrapper.isReady()) {
var self = this;
this._accessUserDict('load', null, function(byteArray) {
emEngineWrapper.init(self._glue.path, byteArray, function(isOk) {
if (isOk) {
self._start();
} else {
debug('emEngineWrapper initialize failed!!');
}
});
});
} else {
emEngineWrapper.post('im_flush_cache', {}, null);
}
this._isActive = true;
},
/**
* Override
*/
deactivate: function engine_deactivate() {
IMEngineBase.prototype.deactivate.call(this);
debug('Deactivate.');
if (!this._isActive) {
return;
}
this._isActive = false;
this._resetKeypressQueue();
this.empty();
var self = this;
emEngineWrapper.post('im_get_user_dict_data', {}, function(byteArray) {
if (byteArray) {
self._accessUserDict('save', byteArray, function(isOk) {
if (isOk) {
debug('Saved user dictionary to DB.');
} else {
debug('Failed to save user dictionary to DB.');
}
if (!self._uninitTimer) {
self._uninitTimer =
setTimeout(self.uninit.bind(self), self._workerTimeout);
}
});
}
});
},
getMoreCandidates: function engine_getMore(indicator, maxCount, callback) {
if (this._candidatesLength === 0) {
callback(null);
return;
}
var num = this._candidatesLength;
maxCount = Math.min((maxCount || num) + indicator, num);
var msgId = this._pendingSymbols ? 'im_get_candidates' : 'im_get_predicts';
emEngineWrapper.post(msgId, {
start: indicator,
count: maxCount
}, function(results) {
var len = results.length;
var list = [];
for (var i = 0; i < len; i++) {
list.push([results[i], i + indicator]);
}
callback(list);
});
}
};
var jspinyin = new IMEngine();
// Expose jspinyin as an AMD module
if (typeof define === 'function' && define.amd) {
define('jspinyin', [], function() { return jspinyin; });
}
// Expose the engine to the Gaia keyboard
if (typeof InputMethods !== 'undefined') {
InputMethods.jspinyin = jspinyin;
}
})();