Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
771 lines
18 KiB
JavaScript
Executable file
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;
|
|
}
|
|
})();
|