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