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.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,515 @@
'use strict';
// This file implements encoding system documented by libtabe,
// inspired by the ETen Chinese System.
// See ./libtabe/doc/BoPoMoFo.shtml
// Get libtabe at http://sourceforge.net/projects/libtabe/
// 1st Group 2nd Group 3rd Group Tone Symbols
// # of Symbols 21 3 13 5
// # of Bits 6 2 4 3
// TODO: Maybe support Extended Bopomofo in the future?
var BopomofoEncoder = {
// Unicode range of each of the Bopomofo symbol groups
// See https://en.wikipedia.org/wiki/Bopomofo_(script)#Unicode
BOPOMOFO_START_GROUP_1: 0x3105,
BOPOMOFO_END_GROUP_1: 0x3119,
BOPOMOFO_START_GROUP_2: 0x3127,
BOPOMOFO_END_GROUP_2: 0x3129,
BOPOMOFO_START_GROUP_3: 0x311A,
BOPOMOFO_END_GROUP_3: 0x3126,
// Tone symbols are placed in Spacing Modifier Letters Unicode block
BOPOMOFO_TONE_1: 0x02c9,
BOPOMOFO_TONE_2: 0x02ca,
BOPOMOFO_TONE_3: 0x02c7,
BOPOMOFO_TONE_4: 0x02cb,
BOPOMOFO_TONE_5: 0x02d9,
// Bitmask for each group
BOPOMOFO_GROUP_1_BITMASK: 0x7e00,
BOPOMOFO_GROUP_2_BITMASK: 0x0180,
BOPOMOFO_GROUP_3_BITMASK: 0x0078,
BOPOMOFO_TONE_BITMASK: 0x0007,
/**
* Encode a Bopomofo symbols string into encoded sounds.
* into encoded string.
* @param {string} symbols Symbols string.
* @param {object} options Options.
* - reorder: true for reorder.
* @return {array(number)} Encoded sounds array.
* @this BopomofoEncoder
*/
encode: function be_encode(symbols, options) {
options = options || {};
var encodedSoundsArr = [];
var currentEncodeSymbolsCode = 0;
var filled1 = false;
var filled2 = false;
var filled3 = false;
var filled4 = false;
var reorder = options.reorder;
var next = function next() {
encodedSoundsArr.push(currentEncodeSymbolsCode);
currentEncodeSymbolsCode = 0;
filled1 = filled2 = filled3 = filled4 = false;
};
for (var j = 0; j < symbols.length; j++) {
var encodedSymbolCode = this.encodeOne(symbols[j]);
if (encodedSymbolCode & this.BOPOMOFO_GROUP_1_BITMASK) {
if (!reorder && (filled1 || filled2 || filled3 || filled4)) {
next();
}
if (reorder && (filled1 || filled4)) {
next();
}
filled1 = true;
currentEncodeSymbolsCode |= encodedSymbolCode;
continue;
}
if (encodedSymbolCode & this.BOPOMOFO_GROUP_2_BITMASK) {
if (!reorder && (filled2 || filled3 || filled4)) {
next();
}
if (reorder && (filled2 || filled4)) {
next();
}
filled2 = true;
currentEncodeSymbolsCode |= encodedSymbolCode;
continue;
}
if (encodedSymbolCode & this.BOPOMOFO_GROUP_3_BITMASK) {
if (filled3 || filled4) {
next();
}
filled3 = true;
currentEncodeSymbolsCode |= encodedSymbolCode;
continue;
}
if (encodedSymbolCode & this.BOPOMOFO_TONE_BITMASK) {
filled4 = true;
currentEncodeSymbolsCode |= encodedSymbolCode;
continue;
}
throw new Error('Should not reach here.');
}
next();
return encodedSoundsArr;
},
/**
* Encode exactly one Bopomofo symbol
* @param {string} symbol Bopomofo symbol
* @return {number} Encoded code representing the symbol.
*/
encodeOne: function be_encodeOne(symbol) {
var symbolCode = symbol.charCodeAt(0);
if (symbolCode >= this.BOPOMOFO_START_GROUP_1 &&
symbolCode <= this.BOPOMOFO_END_GROUP_1) {
return (symbolCode - this.BOPOMOFO_START_GROUP_1 + 1) << 9;
}
if (symbolCode >= this.BOPOMOFO_START_GROUP_2 &&
symbolCode <= this.BOPOMOFO_END_GROUP_2) {
return (symbolCode - this.BOPOMOFO_START_GROUP_2 + 1) << 7;
}
if (symbolCode >= this.BOPOMOFO_START_GROUP_3 &&
symbolCode <= this.BOPOMOFO_END_GROUP_3) {
return (symbolCode - this.BOPOMOFO_START_GROUP_3 + 1) << 3;
}
if (symbolCode == this.BOPOMOFO_TONE_1) {
return 0x1;
}
if (symbolCode == this.BOPOMOFO_TONE_2) {
return 0x2;
}
if (symbolCode == this.BOPOMOFO_TONE_3) {
return 0x3;
}
if (symbolCode == this.BOPOMOFO_TONE_4) {
return 0x4;
}
if (symbolCode == this.BOPOMOFO_TONE_5) {
return 0x5;
}
throw new Error('Unknown symbol: ' + symbol);
},
/**
* Decode an encoded sounds string into Bopomofo symbols.
* @param {array(number)} encodedArr Encoded sounds string or array.
* @return {string} Symbols string.
* @this BopomofoEncoder
*/
decode: function be_decode(encodedArr) {
var symbols = '';
for (var i = 0; i < encodedArr.length; i++) {
var symbolsCode = encodedArr[i];
var group1Code = (symbolsCode & this.BOPOMOFO_GROUP_1_BITMASK) >> 9;
var group2Code = (symbolsCode & this.BOPOMOFO_GROUP_2_BITMASK) >> 7;
var group3Code = (symbolsCode & this.BOPOMOFO_GROUP_3_BITMASK) >> 3;
var toneCode = symbolsCode & this.BOPOMOFO_TONE_BITMASK;
if (group1Code) {
symbols +=
String.fromCharCode(this.BOPOMOFO_START_GROUP_1 - 1 + group1Code);
}
if (group2Code) {
symbols +=
String.fromCharCode(this.BOPOMOFO_START_GROUP_2 - 1 + group2Code);
}
if (group3Code) {
symbols +=
String.fromCharCode(this.BOPOMOFO_START_GROUP_3 - 1 + group3Code);
}
switch (toneCode) {
case 1:
symbols += String.fromCharCode(this.BOPOMOFO_TONE_1);
break;
case 2:
symbols += String.fromCharCode(this.BOPOMOFO_TONE_2);
break;
case 3:
symbols += String.fromCharCode(this.BOPOMOFO_TONE_3);
break;
case 4:
symbols += String.fromCharCode(this.BOPOMOFO_TONE_4);
break;
case 5:
symbols += String.fromCharCode(this.BOPOMOFO_TONE_5);
break;
}
}
return symbols;
},
/**
* Return true if the character or charCode given represents
* an accepted Bopomofo symbol.
* @param {string|number} chr String or a charCode.
* @return {boolean} Return true if the character or charCode given
* represents an accepted Bopomofo symbol.
* @this BopomofoEncoder
*/
isBopomofoSymbol: function be_isBopomofoSymbol(chr) {
var code = (typeof chr === 'string') ? chr.charCodeAt(0) : chr;
if (code >= this.BOPOMOFO_START_GROUP_1 &&
code <= this.BOPOMOFO_END_GROUP_1) {
return true;
}
if (code >= this.BOPOMOFO_START_GROUP_2 &&
code <= this.BOPOMOFO_END_GROUP_2) {
return true;
}
if (code >= this.BOPOMOFO_START_GROUP_3 &&
code <= this.BOPOMOFO_END_GROUP_3) {
return true;
}
if (code === this.BOPOMOFO_TONE_1 ||
code === this.BOPOMOFO_TONE_2 ||
code === this.BOPOMOFO_TONE_3 ||
code === this.BOPOMOFO_TONE_4 ||
code === this.BOPOMOFO_TONE_5) {
return true;
}
return false;
},
APPEND_MODE_NONE: 0,
APPEND_MODE_REORDER: 1,
appendToSymbols: function(symbols, symbolToAttach, mode) {
mode = mode || this.APPEND_MODE_NONE;
if (!this.isBopomofoSymbol(symbolToAttach)) {
throw new Error('BopomofoEncoder: ' +
'Symbol to attach is not a Bopomofo symbol.');
}
switch (mode) {
case this.APPEND_MODE_NONE:
return symbols + symbolToAttach;
case this.APPEND_MODE_REORDER:
// TODO: FIX THIS.
return this.decode(this.encode(symbols + symbolToAttach, {
reorder: true
}));
}
},
/**
* Construct and encoded sounds array that have all non-completed sounds
* expended as seperate symbols.
* Useful for getSymbolsCompositions() and also to decide the minimal # of
* composing elements of the symbols string.
*
* @param {string} symbols String of Bopomofo symbols.
* @returns {array(number)} Array consist of code of the
* encoded sounds.
*/
encodeExpended: function(symbols) {
var encodedSoundsReversedArr = [0];
var i = symbols.length;
var pos = 0;
var placeIntoCurrentSound = false;
var filled1, filled2, filled3;
var encodedSymbolCode;
while (i--) {
encodedSymbolCode = this.encodeOne(symbols[i]);
if (this.isCompleted(encodedSymbolCode)) {
placeIntoCurrentSound = true;
filled1 = filled2 = filled3 = false;
encodedSoundsReversedArr.push(encodedSymbolCode);
pos++;
} else {
if (placeIntoCurrentSound) {
if ((encodedSymbolCode & this.BOPOMOFO_GROUP_3_BITMASK) &&
!filled3 && !filled2 && !filled1) {
filled3 = true;
encodedSoundsReversedArr[pos] |= encodedSymbolCode;
} else if ((encodedSymbolCode & this.BOPOMOFO_GROUP_2_BITMASK) &&
!filled2 && !filled1) {
filled2 = true;
encodedSoundsReversedArr[pos] |= encodedSymbolCode;
} else if ((encodedSymbolCode & this.BOPOMOFO_GROUP_1_BITMASK) &&
!filled1) {
filled1 = true;
encodedSoundsReversedArr[pos] |= encodedSymbolCode;
} else {
placeIntoCurrentSound = false;
encodedSoundsReversedArr.push(encodedSymbolCode);
pos++;
}
} else {
encodedSoundsReversedArr.push(encodedSymbolCode);
pos++;
}
}
}
if (encodedSoundsReversedArr[0] === 0) {
encodedSoundsReversedArr.shift();
}
return encodedSoundsReversedArr.reverse();
},
/**
* Trim symbols to remove the part that will only match more words specify by
* length.
* @param {string|array(number)} symbols Symbols
* @param {number} length Length to trim.
* @return {string} Trimmed symbols.
*/
trimToLength: function(symbols, length) {
symbols = (typeof symbols === 'string') ? symbols : this.decode(symbols);
// No need to trim.
if (symbols.length <= length) {
return symbols;
}
var encodedSounds;
var i = symbols.length;
// XXX: Improve this; instead of a loop we could introduce a cap in
// the state machine in the encode() method.
do {
encodedSounds = this.encode(symbols.substr(0, i));
if (encodedSounds.length <= length) {
break;
}
} while (i--);
return this.decode(encodedSounds);
},
/**
* Trim symbols (from the end) to remove the part that will only match more
* words specify by length.
* @param {string|array(number)} symbols Symbols
* @param {number} length Length to trim.
* @return {string} Trimmed symbols.
*/
trimToLengthFromEnd: function(symbols, length) {
symbols = (typeof symbols === 'string') ? symbols : this.decode(symbols);
var encodedSounds;
var totalLength = this.encode(symbols).length;
var i = 0;
// XXX: Improve this; instead of a loop we could introduce a cap in
// the state machine in the encode() method.
while (++i < symbols.length) {
encodedSounds = this.encode(symbols.substr(0, i));
if (encodedSounds.length + length > totalLength) {
break;
}
}
return symbols.substr(i - 1);
},
/**
* Return all possible combinations of the given symbols string.
* @param {string|array(number)} symbols Symbol string or array
* returned from
* encodeExpended.
* @return {array(array(number))} All combinations of
* the given symbols string.
*/
getSymbolCombinations: function(symbols) {
// Construct an encodeExpended encoded sounds array,
// or use the input as such.
var expendedEncodedSounds =
(typeof symbols === 'string') ? this.encodeExpended(symbols) : symbols;
var combinations = expendedEncodedSounds
.reduce(function(currentCombinations, code, i) {
if (i === 0) {
currentCombinations.push([ code ]);
return currentCombinations;
}
currentCombinations.forEach(function(combination) {
var k = combination.length - 1;
// If the previous symbol is a complete one of this is a completed
// one, they must not occupy the same place.
if (this.isCompleted(combination[k]) || this.isCompleted(code)) {
combination.push(code);
return;
}
var filled2 = !!(combination[k] & this.BOPOMOFO_GROUP_2_BITMASK);
var filled3 = !!(combination[k] & this.BOPOMOFO_GROUP_3_BITMASK);
// If the previous one contain the same or the lower place of
// symbol, the new symbol must not occupy the same place.
if ((code & this.BOPOMOFO_GROUP_1_BITMASK) ||
(code & this.BOPOMOFO_GROUP_2_BITMASK &&
(filled2 || filled3)) ||
(code & this.BOPOMOFO_GROUP_3_BITMASK && filled3)) {
combination.push(code);
return;
}
// Create a copy of the composition, so that we could still
// put the symbols into the new place.
var newCombination = [].concat(combination);
newCombination.push(code);
currentCombinations.push(newCombination);
// Combine the current symbol into the previous symbol.
combination[k] |= code;
}.bind(this));
return currentCombinations;
}.bind(this), []);
return combinations;
},
isIncompletionOf: function(code, codeToMatch) {
var group1Code = (code & this.BOPOMOFO_GROUP_1_BITMASK) >> 9;
var group2Code = (code & this.BOPOMOFO_GROUP_2_BITMASK) >> 7;
var group3Code = (code & this.BOPOMOFO_GROUP_3_BITMASK) >> 3;
var toneCode = code & this.BOPOMOFO_TONE_BITMASK;
var group1CodeToMatch = (codeToMatch & this.BOPOMOFO_GROUP_1_BITMASK) >> 9;
var group2CodeToMatch = (codeToMatch & this.BOPOMOFO_GROUP_2_BITMASK) >> 7;
var group3CodeToMatch = (codeToMatch & this.BOPOMOFO_GROUP_3_BITMASK) >> 3;
var toneCodeToMatch = codeToMatch & this.BOPOMOFO_TONE_BITMASK;
// This is fairly complex because not only we have to consider mis-match
// of symbols at the same place, but also evaluate to false when there is
// a following up symbol but the previous one is missing.
// To future self: The best way to understand this is by reading test cases.
return (
!(group1CodeToMatch && !group1Code &&
(group2Code || group3Code || toneCode)) &&
!(group1Code && group1CodeToMatch !== group1Code) &&
!(group2CodeToMatch && !group2Code &&
(group3Code || toneCode)) &&
!(group2Code && group2CodeToMatch !== group2Code) &&
!(group3CodeToMatch && !group3Code && toneCode) &&
!(group3Code && group3CodeToMatch !== group3Code) &&
!(toneCode && toneCodeToMatch !== toneCode) &&
true);
},
isCompleted: function(code) {
// Only phontics with tone is considered completed.
return !!(code & this.BOPOMOFO_TONE_BITMASK);
},
replace: function(code, fromCode, toCode) {
var match = false;
if (fromCode & this.BOPOMOFO_GROUP_1_BITMASK) {
match = ((code & this.BOPOMOFO_GROUP_1_BITMASK) ===
(fromCode & this.BOPOMOFO_GROUP_1_BITMASK));
}
if (fromCode & this.BOPOMOFO_GROUP_2_BITMASK) {
match = ((code & this.BOPOMOFO_GROUP_2_BITMASK) ===
(fromCode & this.BOPOMOFO_GROUP_2_BITMASK));
}
if (fromCode & this.BOPOMOFO_GROUP_3_BITMASK) {
match = ((code & this.BOPOMOFO_GROUP_3_BITMASK) ===
(fromCode & this.BOPOMOFO_GROUP_3_BITMASK));
}
if (fromCode & this.BOPOMOFO_TONE_BITMASK) {
match = ((code & this.BOPOMOFO_TONE_BITMASK) ===
(fromCode & this.BOPOMOFO_TONE_BITMASK));
}
if (!match) {
return code;
}
return code & ~fromCode | toCode;
}
};
// Export as a CommonJS module if we are loaded as one.
if (typeof module === 'object' && module.exports) {
module.exports = BopomofoEncoder;
}

View file

@ -0,0 +1,78 @@
'use strict';
/**
* DataLoader wraps XMLHTTPRequest and pull the JSZhuyin dictionary if it is
* not passed into the setup.
*/
var DataLoader = function() {
this.loaded = false;
this.loading = false;
};
/**
* Database file to load.
* @type {string}
*/
DataLoader.prototype.DATA_URL = '../data/database.data';
/**
* Once loaded, data will be available here.
* @type {object}
*/
DataLoader.prototype.data = null;
/**
* Run when the loading is complete.
* @type {function}
*/
DataLoader.prototype.onloadend = null;
/**
* Run when loading is successful.
* @type {function}
*/
DataLoader.prototype.onload = null;
/**
* Run when error occours.
* @type {function}
*/
DataLoader.prototype.onerror = null;
/**
* Load the data
*/
DataLoader.prototype.load = function bs_load() {
if (this.loading) {
throw new Error('DataLoader: You cannot call load() twice.');
}
var xhr = new XMLHttpRequest();
xhr.open('GET', this.DATA_URL);
xhr.responseType = 'arraybuffer';
xhr.onreadystatechange = function xhrReadystatechange() {
if (xhr.readyState !== xhr.DONE) {
return;
}
var data = this.data = xhr.response;
if (!data || (xhr.status && xhr.status !== 200)) {
this.loading = false;
if (typeof this.onerror === 'function') {
this.onerror();
}
if (typeof this.onloadend === 'function') {
this.onloadend();
}
return;
}
this.loaded = true;
this.loading = false;
if (typeof this.onload === 'function') {
this.onload();
}
if (typeof this.onloadend === 'function') {
this.onloadend();
}
}.bind(this);
xhr.send();
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,363 @@
'use strict';
// 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 = {
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 = 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 = 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++;
}
};
// Export as a CommonJS module if we are loaded as one.
if (typeof module === 'object' && module.exports) {
module.exports = JSZhuyinDataPack;
}

View file

@ -0,0 +1,52 @@
'use strict';
/* global JSZhuyin */
// JSZhuyinServer provides a postMessage interface to communicate
// with JSZhuyin.
var JSZhuyinServer = {
engine: null,
messageTarget: self.opener || self.parent || self,
load: function w_load() {
self.addEventListener('message', this);
var engine = this.engine = new JSZhuyin();
(['loadend',
'load',
'error',
'unload',
'compositionupdate',
'compositionend',
'candidateschange',
'actionhandled']).forEach((function attachCallback(type) {
engine['on' + type] = this.getSender(type);
}).bind(this));
},
getSender: function w_getSender(type) {
return this.sendMessage.bind(this, type);
},
handleEvent: function w_handleEvent(evt) {
var msg = evt.data;
if (msg.sender === 'worker') {
return;
}
if (typeof this.engine[msg.type] !== 'function') {
throw 'Unknown message type: ' + msg.type;
}
this.engine[msg.type](msg.data, msg.requestId);
},
sendMessage: function w_sendMessage(type, data, reqId) {
if (typeof window === 'object') {
this.messageTarget.postMessage(
{ 'type': type, 'data': data,
'requestId': reqId, 'sender': 'worker' }, '*');
} else {
this.messageTarget.postMessage(
{ 'type': type, 'data': data, 'requestId': reqId });
}
}
};
JSZhuyinServer.load();

View file

@ -0,0 +1,493 @@
'use strict';
/* global JSZhuyinDataPack, JSZhuyinDataPackCollection, 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 = 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 = 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 = 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) {
// 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 &&
BopomofoEncoder.isCompleted(code)) {
a = this.searchBlock(code, address);
if (a !== -1) {
return codeAddresses.concat(a);
}
return codeAddresses;
}
// Depend on the setting, the result should be matched either by
// trying to split the encoded sound into multiple encoded sounds,
// or not.
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 addresses = [], c;
for (var i = 0; i < length; i++) {
c = this._replaceInterchangeableSymbols(
keyBlockView.getUint16(i << 1, true));
if (BopomofoEncoder.isIncompletionOf(code, c)) {
addresses.push(addressBlockView.getUint32(i << 2, true));
}
}
return addresses;
};
/**
* Attempt to find the sounds for a set of characters.
* Only search for the 0th block (one-sound entries), does not consider
* word-phrases.
* Very very slow operation.
* @param {String} str Characters to search
* @return {String} Encoded sounds of Bopomofo. \u0000 for sounds can't be
* found.
*/
JSZhuyinDataPackStorage.prototype.reverseGet = function(str) {
// Construct an buffer of the same length and filled with zero.
var res = [];
// Since the Go through the zero-th block
var bin = this._bin;
var view = new DataView(bin);
var length = view.getUint16(0, true);
var contentLength = view.getUint16(2, true);
var keyBlockByteOffset = (2 + contentLength) << 1;
var addressBlockByteOffset = (2 + contentLength + length) << 1;
var keyBlockView = new DataView(bin, keyBlockByteOffset, length << 1);
var addressBlockView = new DataView(bin, addressBlockByteOffset, length << 2);
var j, result, dataPack;
for (var i = 0; i < length; i++) {
result = this._getBlockContent(addressBlockView.getUint32(i << 2, true));
if (!result) {
continue;
}
dataPack = Object.create(JSZhuyinDataPack.prototype);
JSZhuyinDataPack.apply(dataPack, result);
for (j = 0; j < str.length; j++) {
if (dataPack.getResultsBeginsWith(str[j]).length) {
res[j] = keyBlockView.getUint16(i << 1, true);
}
}
}
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;
};

View file

@ -0,0 +1,9 @@
'use strict';
/**
* Load JSZhuyin, etc. into a Web Workers.
*
*/
importScripts('bopomofo_encoder.js', 'jszhuyin_data_pack.js',
'storage.js', 'data_loader.js', 'jszhuyin.js', 'jszhuyin_server.js');