UNPKG

jszhuyin

Version:

Smart Chinese Zhuyin Input Method in JavaScript.

373 lines (319 loc) 9.66 kB
'use strict'; (function(factory) { if (typeof module === 'object' && module.exports) { // CommonJS factory(module.exports, {}); } else if (typeof self === 'object') { // Window or WorkerGlobalScope factory(self, self); } }(function(exports, required) { // This implements a packer/unpacker of the arraybuffer we get from storage. // We would like to keep structured data in the JS world as few as possible to // conserve memory and GC time. /** * Float32Encoder encodes a given float number to an arraybuffer, * and vise versa. * @type {Object} */ var Float32Encoder = exports.Float32Encoder = { isSupported: (typeof DataView !== 'undefined'), BUFFER_BYTE_LENGTH: 4, encode: function encodeFloat32Number(number, type) { type = type || 'arraybuffer'; switch (type) { case 'arraybuffer': return this.encodeArrayBuffer(number); default: throw new Error('Unsupported encode to type.'); } }, encodeArrayBuffer: function Float32NumberToArrayBuffer(number) { if (typeof number !== 'number') { throw new Error('Argument received is not a number.'); } var buf = new ArrayBuffer(4); new DataView(buf).setFloat32(0, number, true); return buf; }, decode: function decodeFloat32Number(data) { switch (data.constructor) { case ArrayBuffer: return this.decodeArrayBuffer(data); default: throw new Error('Unsupported data type.'); } }, decodeArrayBuffer: function ArrayBufferToFloat32Number(buffer, byteOffset) { return (new DataView(buffer)).getFloat32(byteOffset, true); } }; var JSZhuyinDataPackCollection = exports.JSZhuyinDataPackCollection = function(dataPacks) { this.dataPacks = dataPacks; if (!dataPacks.length) { throw new Error('JSZhuyinDataPackCollection: ' + 'Expects an non-empty array.'); } this.firstResultDataPack = null; this.results = []; }; JSZhuyinDataPackCollection.prototype.getFirstResultScore = function() { if (this.results.length) { return this.results[0].score; } if (!this.firstResultDataPack) { this._getFirstResultDataPack(); } return this.firstResultDataPack.getFirstResultScore(); }; JSZhuyinDataPackCollection.prototype.getFirstResult = function() { if (this.results.length) { return this.results[0]; } if (!this.firstResultDataPack) { this._getFirstResultDataPack(); } return this.firstResultDataPack.getFirstResult(); }; JSZhuyinDataPackCollection.prototype._getFirstResultDataPack = function() { if (this.dataPacks.length === 1) { this.firstResultDataPack = this.dataPacks[0]; return; } var score = -Infinity; this.dataPacks.forEach(function(dataPack, i) { if (dataPack.getFirstResultScore() > score) { this.firstResultDataPack = dataPack; score = dataPack.getFirstResultScore(); } }.bind(this)); }; JSZhuyinDataPackCollection.prototype.getResults = function() { if (this.results.length) { return this.results; } if (this.dataPacks.length === 1) { this.results = this.dataPacks[0].getResults(); return this.results; } var results = []; // TODO: Optimize the sorting here. this.dataPacks.forEach(function(dataPack, i) { var resArr = dataPack.getResults(); resArr.forEach(function(res) { var found = results.some(function(currentRes) { return (currentRes.str === res.str); }); if (!found) { results.push(res); } }); }.bind(this)); results = results.sort(function(a, b) { return (b.score - a.score); }); this.results = results; return results; }; /** * JSZhuyinDataPack instance is the representation of the data in the database. * @param {arraybuffer|array} imeData arraybuffer the packed data, or an * array of the structured data. * @constructor */ var JSZhuyinDataPack = exports.JSZhuyinDataPack = function(imeData, byteOffset, length, index) { if (imeData.constructor === ArrayBuffer) { this.packed = imeData; this.byteOffset = byteOffset || 0; this.length = length || (imeData.byteLength >> 1); this.unpacked = undefined; } else if (Array.isArray(imeData)) { this.packed = undefined; this.unpacked = imeData; } else { this.packed = undefined; this.unpacked = undefined; } this.index = index; }; /** * Get the score of the first item. * The arraybuffer will not be unpacked. * @return {number} Score. */ JSZhuyinDataPack.prototype.getFirstResultScore = function() { if (this.unpacked) { return this.unpacked[0].score; } return Float32Encoder.decodeArrayBuffer(this.packed, this.byteOffset + 2); }; /** * Get the first item. * The arraybuffer will not be unpacked. * @return {object} The first item. */ JSZhuyinDataPack.prototype.getFirstResult = function() { if (this.unpacked) { return this.unpacked[0]; } var view = new DataView(this.packed, this.byteOffset, this.length << 1); var controlByte = view.getUint16(0, true); var length = controlByte & 0x0f; var result = { str: this._getStringFromDataView(view, 3 << 1, length), score: this.getFirstResultScore(), index: this.index }; return result; }; /** * Get all items in an array. The arraybuffer will be unpacked automatically. * @return {array({str: string, score: number})} Results. */ JSZhuyinDataPack.prototype.getResults = function() { this.unpack(); return this.unpacked; }; /** * Get items in the packed result but only with the ones begins with * giving string. * @param {string} str String to match. * @return {array({str: string, score: number})} Results. */ JSZhuyinDataPack.prototype.getResultsBeginsWith = function(str) { var filterFn = function(res) { return (res.str.substr(0, str.length) === str); }; if (this.unpacked) { return this.unpacked.filter(filterFn); } return this._getPackedResults(filterFn); }; /** * Get the packed array buffer. The data will be packed automatically. * The byteLength is always dividable by 4. * @return {arraybuffer} The packed arraybuffer. */ JSZhuyinDataPack.prototype.getPacked = function() { this.pack(); return this.packed; }; /** * Unpack the arraybuffer and remove it. */ JSZhuyinDataPack.prototype.unpack = function() { if (this.unpacked) { return; } if (typeof this.packed === 'undefined') { throw new Error('No packed IME data.'); } this.unpacked = this._getPackedResults(); this.packed = undefined; }; /** * Go through the packed buffer and extract the results. * @param {function} filterFn Test function for testing results. Optional. * @return {array({str: string, score: number})} Results. */ JSZhuyinDataPack.prototype._getPackedResults = function(filterFn) { var results = []; var view = new DataView(this.packed, this.byteOffset, this.length << 1); var controlByte = view.getUint16(0, true); var length = controlByte & 0x0f; var bytePos = 2; while (bytePos < view.byteLength) { // Ensure we'll not be getting out of range error. if ((bytePos + 4 + (length << 1)) > view.byteLength) { break; } var result = { str: this._getStringFromDataView(view, bytePos + 4, length), score: Float32Encoder.decodeArrayBuffer( this.packed, this.byteOffset + bytePos), index: this.index }; bytePos += (length + 2) << 1; if (typeof filterFn === 'function' && !filterFn(result)) { continue; } results.push(result); } return results; }; /** * Pack the arraybuffer and remove the structured data object. */ JSZhuyinDataPack.prototype.pack = function() { if (this.packed) { return; } if (typeof this.unpacked === 'undefined') { throw new Error('No unpacked IME data.'); } var length = 0; this.unpacked.forEach(function(result, i) { if (result.str.length > length) { length = result.str.length; } }); if (length > 0xf) { throw new Error( 'JSZhuyinDataPack: Longest string length is longer than expected.'); } var arrayLength = 1 + (length + 2) * this.unpacked.length; var packedView = new DataView(new ArrayBuffer(arrayLength << 1)); // 0x40 puts the control byte into printable characters region. packedView.setUint16(0, 0x40 ^ length, true); var bytePos = 1 << 1; this.unpacked.forEach(function(result, i) { packedView.setFloat32(bytePos, result.score, true); bytePos += (2 << 1); this._setStringToDataView(packedView, bytePos, result.str); bytePos += (length << 1); }, this); this.packed = packedView.buffer; this.byteOffset = 0; this.length = arrayLength; }; /** * Overwrite the native toString() method. * @return {string} String representation of the JSZhuyinDataPack instance. */ JSZhuyinDataPack.prototype.toString = function() { if (this.unpacked) { return this.unpacked.toString(); } if (this.packed) { return this.packed.toString(); } return '[object JSZhuyinDataPack]'; }; /** * Get the string from given DataView instance. */ JSZhuyinDataPack.prototype._getStringFromDataView = function(view, byteOffset, length) { var charCodes = [], charCode; for (var i = 0; i < length; i++) { charCode = view.getUint16(byteOffset + (i << 1), true); if (charCode) { charCodes.push(charCode); } } return String.fromCharCode.apply(String, charCodes); }; /** * Set the string to given DataView instance. */ JSZhuyinDataPack.prototype._setStringToDataView = function(view, byteOffset, str) { var i = 0; while (i < str.length) { view.setUint16(byteOffset + (i << 1), str.charCodeAt(i), true); i++; } }; }));