jszhuyin
Version:
Smart Chinese Zhuyin Input Method in JavaScript.
572 lines (502 loc) • 17.9 kB
JavaScript
'use strict';
(function(factory) {
if (typeof module === 'object' && module.exports) {
// CommonJS
factory(module.exports, {
JSZhuyinDataPack:
require('./jszhuyin_data_pack.js').JSZhuyinDataPack,
JSZhuyinDataPackCollection:
require('./jszhuyin_data_pack.js').JSZhuyinDataPackCollection,
BopomofoEncoder: require('./bopomofo_encoder.js')
});
} else if (typeof self === 'object') {
// Window or WorkerGlobalScope
if (typeof self.JSZhuyinDataPack === 'undefined' ||
typeof self.JSZhuyinDataPackCollection === 'undefined' ||
typeof self.BopomofoEncoder === 'undefined') {
throw new Error('Dependency not found.');
}
factory(self, self);
}
}(function(exports, required) {
var JSZhuyinDataPack = required.JSZhuyinDataPack;
var JSZhuyinDataPackCollection = required.JSZhuyinDataPackCollection;
var BopomofoEncoder = required.BopomofoEncoder;
// This file implemented BinStorage, an in-memory key-value storage (for now).
// Additional sugar is put onto JSZhuyinDataPackStorage to more effectively
// manage JSZhuyin-specific data.
/**
* A simple key-value store wrapps with JavaScript object.
* @this {object} CacheStore instance.
*/
var CacheStore = exports.CacheStore = function CacheStore() {
this.dataMap = new Map();
};
/**
* set the value of a key.
* @param {array(number)} codes Key.
* @param {any} value Value.
* @this {object} CacheStore instance.
*/
CacheStore.prototype.add = function cs_add(codes, value) {
// In the interest of not doing deep comparsion on arrays, we would convert
// them to string here.
this.dataMap.set(String.fromCharCode.apply(String, codes), value);
};
/**
* get the value of a key.
* @param {array(number)} codes Key.
* @return {any} Value of the key.
* @this {object} CacheStore instance.
*/
CacheStore.prototype.get = function cs_get(codes) {
return this.dataMap.get(String.fromCharCode.apply(String, codes));
};
/**
* Clean up the store. Any key that is not a substring of the superset
* string will have their value removed.
*
* @param {array(number)} supersetCodes The superset code array.
* @this {object} CacheStore instance.
*/
CacheStore.prototype.cleanup = function cs_cleanup(supersetCodes) {
if (!supersetCodes) {
this.dataMap.clear();
return;
}
var supersetStr = String.fromCharCode.apply(String, supersetCodes);
this.dataMap.forEach(function(v, key) {
if (supersetStr.indexOf(key) === -1) {
this.dataMap.delete(key);
}
}, this);
};
/**
* BinStorage rely on a big chunk of encoded ArrayBuffer to do the lookup.
* @constructor
*/
var BinStorage = exports.BinStorage = function BinStorage() {
this.loaded = false;
this._bin = undefined;
};
/**
* Load the database.
* @this {object} BinStorage instance.
*/
BinStorage.prototype.load = function bs_load(data) {
if (this.loaded) {
this.unload();
}
this.loaded = true;
this._bin = data;
};
/**
* Unoad the database.
* @this {object} BinStorage instance.
*/
BinStorage.prototype.unload = function bs_unload() {
this._bin = undefined;
this.loaded = false;
};
/**
* Get values from storage.
* @param {array(number)} codes Array of numbers to query.
* @return {arraybuffer} the result.
*/
BinStorage.prototype.get = function bs_get(codes) {
if (!this.loaded) {
throw new Error('BinStorage: not loaded.');
}
var code;
var byteOffset = 0;
while ((code = codes.shift()) !== undefined) {
byteOffset = this.searchBlock(code, byteOffset);
if (byteOffset === -1) {
return undefined;
}
}
return this._getBlockContent(byteOffset);
};
/**
* Look for all value begin with this key.
* @param {array(number)} codes Array of numbers to query.
* @return {array(array(...))} Array of results of _getBlockContent
*/
BinStorage.prototype.getRange = function bs_getRange(codes) {
if (!this.loaded) {
throw new Error('BinStorage: not loaded.');
}
var code;
var byteOffset = 0;
while ((code = codes.shift()) !== undefined) {
byteOffset = this.searchBlock(code, byteOffset);
if (byteOffset === -1) {
return [];
}
}
return this.getRangeFromContentIndex(byteOffset);
};
/**
* Get all of the values from this address.
* @param {numbber} byteOffset Byte offset of the block.
* @return {array(array(...))} Array of results of _getBlockContent
*/
BinStorage.prototype.getRangeFromContentIndex = function(byteOffset) {
var bin = this._bin;
var result = [];
var getBlockContents = function bs_getBlockContents(byteOffset) {
var view = new DataView(bin, byteOffset);
var length = view.getUint16(0, true);
var contentLength = view.getUint16(2, true);
if (length === 0) {
return;
}
var addressBlockByteOffset =
byteOffset + ((2 + contentLength + length) << 1);
var addressBlockView =
new DataView(bin, addressBlockByteOffset, length << 2);
var i = length;
while (i--) {
var blockAddress = addressBlockView.getUint32(i << 2, true);
var content = this._getBlockContent(blockAddress);
if (content) {
result.push(content);
}
getBlockContents(blockAddress);
}
}.bind(this);
getBlockContents(byteOffset);
return result;
};
/**
* Internal method for search a given block for a single character.
* @param {number} code code of the character.
* @param {numbber} byteOffset Byte offset of the block.
* @return {number} byteOffset of the block found, or -1.
*/
BinStorage.prototype.searchBlock = function bs_searchBlock(code, byteOffset) {
var bin = this._bin;
var view = new DataView(bin, byteOffset);
var length = view.getUint16(0, true);
var contentLength = view.getUint16(2, true);
var keyBlockByteOffset =
byteOffset + ((2 + contentLength) << 1);
var addressBlockByteOffset =
byteOffset + ((2 + contentLength + length) << 1);
var keyBlockView = new DataView(bin, keyBlockByteOffset, length << 1);
var addressBlockView = new DataView(bin, addressBlockByteOffset, length << 2);
// Do a interpolation search
var low = 0;
var high = length - 1;
var mid;
var lowCode, highCode, midCode;
while (low < length &&
(lowCode = keyBlockView.getUint16(low << 1, true)) <= code &&
(highCode = keyBlockView.getUint16(high << 1, true)) >= code) {
mid = low + (((code - lowCode) * (high - low)) / (highCode - lowCode)) | 0;
midCode = keyBlockView.getUint16(mid << 1, true);
if (midCode < code) {
low = mid + 1;
} else if (midCode > code) {
high = mid - 1;
} else {
return addressBlockView.getUint32(mid << 2, true);
}
}
if (lowCode === code) {
return addressBlockView.getUint32(low << 2, true);
} else {
return -1;
}
};
/**
* Internal method for getting the content of the block.
* @param {numbber} byteOffset Byte offset of the block.
* @return {array} array contain 4 elements
* - First element is the reference to the arrayBuffer
* - Second element is the byteOffset the content begins.
* - Third element is the length of the content.
* - Forth element is the address of the block.
*/
BinStorage.prototype._getBlockContent =
function bs_getBlockContent(byteOffset) {
var bin = this._bin;
var view = new DataView(bin, byteOffset);
var contentLength = view.getUint16(2, true);
if (contentLength === 0) {
return undefined;
}
return [bin, byteOffset + (2 << 1), contentLength, byteOffset];
};
var JSZhuyinDataPackStorage = exports.JSZhuyinDataPackStorage = function() {
this.incompleteMatchedCache = new CacheStore();
this.getCache = new CacheStore();
this._interchangeablePairs = '';
this._interchangeablePairsArr = new Uint16Array(0);
};
JSZhuyinDataPackStorage.prototype = Object.create(BinStorage.prototype);
/**
* When searching for matching words/phrases, consider these pairs of symbols
* are interchangables.
* Must be a string representing 2n sounds in Bopomofo symbols.
*
* Example string: 'ㄣㄥㄌㄖㄨㄛㄡ', making ㄣ interchangable with ㄥ and
* ㄌ interchangable with ㄖ, and ㄨㄛ with ㄡ.
*
* @param {string} str String representing the pairs.
*/
JSZhuyinDataPackStorage.prototype.setInterchangeablePairs = function(str) {
if (str === this._interchangeablePairs) {
return;
}
// String has changed, invalidate the entire cache.
this.incompleteMatchedCache.cleanup();
var encodedSounds = BopomofoEncoder.encode(str);
if (encodedSounds.length % 2) {
throw new Error('JSZhuyinDataPackStorage: Expects string to store pairs.');
}
var arr = new Uint16Array(encodedSounds);
this._interchangeablePairs = str;
this._interchangeablePairsArr = arr;
};
/**
* Get values from storage, disregards interchangeable pairs.
* @param {array(number)} codes Array of numbers to query.
* @return {JSZhuyinData} The resulting JSZhuyinData instance.
*/
JSZhuyinDataPackStorage.prototype.get = function(codes) {
// Should be null or the previous instance.
if (typeof this.getCache.get(codes) === 'object') {
return this.getCache.get(codes);
}
var result = BinStorage.prototype.get.call(this, codes);
if (result) {
// This is equal to |new JSZhuyinDataPack(..)| but take array of arugments
var dataPack = Object.create(JSZhuyinDataPack.prototype);
JSZhuyinDataPack.apply(dataPack, result);
this.getCache.add(codes, dataPack);
return dataPack;
} else {
this.getCache.add(codes, null);
return null;
}
};
/**
* Look for all value begin from this block address.
* @param {numbber} byteOffset Byte offset of the block.
* @return {array(JSZhuyinData)} The resulting JSZhuyinData instances.
*/
JSZhuyinDataPackStorage.prototype.getRangeFromContentIndex = function() {
return BinStorage.prototype.getRangeFromContentIndex.apply(this, arguments)
.map(function(result) {
var dataPack = Object.create(JSZhuyinDataPack.prototype);
JSZhuyinDataPack.apply(dataPack, result);
return dataPack;
});
};
/**
* Return all results that matches to given partial encoded Bopomofo string.
* @param {array(number)} codes string to query.
* @return {JSZhuyinDataPackCollection?}
* A JSZhuyinDataPackCollection instance
* representing the results.
*/
JSZhuyinDataPackStorage.prototype.getIncompleteMatched = function(codes) {
if (!this.loaded) {
throw new Error('JSZhuyinDataPackStorage: not loaded.');
}
// Should be null or the previous instance.
if (typeof this.incompleteMatchedCache.get(codes) === 'object') {
return this.incompleteMatchedCache.get(codes);
}
// addresses is the reduced result of checking the codes in sequences,
// start by looking at matched keys at table 0.
var addresses = codes
.reduce(function(addresses, code) {
var codeIsCompleted = BopomofoEncoder.isCompleted(code);
// Thie reduces to the every addresses that matches to the code, starting
// from empty array.
// Return back the reduced result and replaces the previous array of
// addresses.
return addresses.reduce(function(codeAddresses, address) {
// If the code represents a completed sound, we should
// use searchBlock() instead of doing linear workathough with
// _getIncompleteMatchedCodesInBlock() since there can be only
// one match.
var a;
if (!this._interchangeablePairs && codeIsCompleted) {
a = this.searchBlock(code, address);
if (a !== -1) {
return codeAddresses.concat(a);
}
return codeAddresses;
}
var results =
this._getIncompleteMatchedSingleCodesInBlock(code, address);
// Matched addresses are appended into the current list.
return codeAddresses.concat(results);
}.bind(this), /* codeAddresses */ []);
}.bind(this), /* addresses */ [0]);
// DataPacks are constructed by getting the content of these addresses,
// remove the null results, and construct with the content argument array.
var dataPacks = addresses
.map(function(address) {
return this._getBlockContent(address);
}, this)
.filter(function(result) {
return !!result;
})
.map(function(result) {
var dataPack = Object.create(JSZhuyinDataPack.prototype);
JSZhuyinDataPack.apply(dataPack, result);
return dataPack;
});
if (!dataPacks.length) {
this.incompleteMatchedCache.add(codes, null);
return null;
}
var dataPackCollection = new JSZhuyinDataPackCollection(dataPacks);
this.incompleteMatchedCache.add(codes, dataPackCollection);
return dataPackCollection;
};
/**
* Internal method to get all the addresses point from a given block that
* matches the code with BopomofoEncoder.isIncompletionOf().
* @param {number} code code of the character.
* @param {numbber} byteOffset Byte offset of the block.
* @return {array(number)} Array of byte offsets
*/
JSZhuyinDataPackStorage.prototype._getIncompleteMatchedSingleCodesInBlock =
function jdps__getIncompleteMatchedSingleCodesInBlock(code, byteOffset) {
var bin = this._bin;
var view = new DataView(bin, byteOffset);
var length = view.getUint16(0, true);
var contentLength = view.getUint16(2, true);
var keyBlockByteOffset =
byteOffset + ((2 + contentLength) << 1);
var addressBlockByteOffset =
byteOffset + ((2 + contentLength + length) << 1);
var keyBlockView = new DataView(bin, keyBlockByteOffset, length << 1);
var addressBlockView = new DataView(bin, addressBlockByteOffset, length << 2);
code = this._replaceInterchangeableSymbols(code);
var isIncompletionOfCode =
BopomofoEncoder.createCompletionComparisonFunction(code);
var addresses = [], c;
for (var i = 0; i < length; i++) {
c = this._replaceInterchangeableSymbols(
keyBlockView.getUint16(i << 1, true));
if (isIncompletionOfCode(c)) {
addresses.push(addressBlockView.getUint32(i << 2, true));
}
}
return addresses;
};
/**
* Attempt to find the sounds for a set of characters.
* Very very slow operation.
* @param {String} str Characters to search
* @param {String} phraseMaxLength Max length of the phrase to match.
* @param {Boolean} group Group encoded sounds in phrases.
* @return {array(array(number))|
* array(number)} Encoded sounds of Bopomofo, group in
* phrases. 0 for sounds can't be found.
*/
JSZhuyinDataPackStorage.prototype.reverseGet = function(str,
phraseMaxLength,
group) {
var res = [];
var bin = this._bin;
var i, j, dataPack;
if (!phraseMaxLength) {
phraseMaxLength = 1;
}
// Get all the dataPacks in the entire database.
var dataPacks = [];
var getDataPacks = function(byteOffset, keys) {
var view = new DataView(bin, byteOffset);
var length = view.getUint16(0, true);
var contentLength = view.getUint16(2, true);
var keyBlockByteOffset =
byteOffset + ((2 + contentLength) << 1);
var addressBlockByteOffset =
byteOffset + ((2 + contentLength + length) << 1);
var keyBlockView = new DataView(bin, keyBlockByteOffset, length << 1);
var addressBlockView =
new DataView(bin, addressBlockByteOffset, length << 2);
if (contentLength !== 0) {
var result = this._getBlockContent(byteOffset);
var dataPack = Object.create(JSZhuyinDataPack.prototype);
JSZhuyinDataPack.apply(dataPack, result);
if (!dataPacks[keys.length]) {
dataPacks[keys.length] = [];
}
dataPacks[keys.length].push([dataPack, keys]);
}
if ((keys.length + 1) > phraseMaxLength) {
return;
}
for (var i = 0; i < length; i++) {
getDataPacks(
addressBlockView.getUint32(i << 2, true),
[].concat(keys, keyBlockView.getUint16(i << 1, true)));
}
}.bind(this);
getDataPacks(0, []);
// Find the phrases
i = 0;
var k;
while (i < str.length) {
j = Math.min(phraseMaxLength, str.length - i);
found: do {
var s = str.substr(i, j);
for (k = 0; k < dataPacks[j].length; k++) {
dataPack = dataPacks[j][k][0];
var keys = dataPacks[j][k][1];
if (dataPack.getResultsBeginsWith(s).length) {
if (group) {
res.push(keys);
} else {
res = res.concat(keys);
}
i += j;
break found;
}
}
if (j === 1) {
if (group) {
res.push([0]);
} else {
res[i] = 0;
}
i++;
}
} while (--j);
}
return res;
};
/**
* Clean up the get() cache. Any key that is not a substring of the superset
* string will have their value removed.
* @param {array(number)?} supersetCodes The superset code array.
*/
JSZhuyinDataPackStorage.prototype.cleanupCache = function(supersetCodes) {
this.incompleteMatchedCache.cleanup(supersetCodes);
};
/**
* Internal method to reduce the bits in code to be 2nd symbols of each pair,
* in the interchange symbols property, if 1st symbols present in the phontics.
* @param {number} c One phontic represented in number
* @return {number} Converted number.
*/
JSZhuyinDataPackStorage.prototype._replaceInterchangeableSymbols = function(c) {
if (!this._interchangeablePairs) {
return c;
}
var arr = this._interchangeablePairsArr;
var n = arr.length;
for (var i = 0; i < n; i += 2) {
c = BopomofoEncoder.replace(c, arr[i], arr[i + 1]);
}
return c;
};
}));