UNPKG

jszhuyin

Version:

Smart Chinese Zhuyin Input Method in JavaScript.

562 lines (472 loc) 17.8 kB
'use strict'; (function(factory) { if (typeof module === 'object' && module.exports) { // CommonJS module.exports = factory(); } else if (typeof self === 'object') { // Window or WorkerGlobalScope self.BopomofoEncoder = factory(); } }(function() { // 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. * @param {number} length Maximum length. * @return {array(array(number))} All combinations of * the given symbols string. */ getSymbolCombinations: function(symbols, length) { // Construct an encodeExpended encoded sounds array, // or use the input as such. var expendedEncodedSounds = (typeof symbols === 'string') ? this.encodeExpended(symbols) : symbols; if (typeof length !== 'number') { length = expendedEncodedSounds.length * 3; } // Don't bother try. No combination can possibly fit into the length. if (expendedEncodedSounds.length > length * 3) { return []; } var combinations = expendedEncodedSounds .reduce(function(currentCombinations, code, i) { if (i === 0) { currentCombinations.push([ code ]); return currentCombinations; } return currentCombinations .reduce(function(currentCombinations, combination) { if (combination === null) { return currentCombinations; } 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)) { if (combination.length === length) { return currentCombinations; } combination.push(code); currentCombinations.push(combination); return currentCombinations; } 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)) { if (combination.length === length) { return currentCombinations; } combination.push(code); currentCombinations.push(combination); return currentCombinations; } if (combination.length < length) { // 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; currentCombinations.push(combination); return currentCombinations; }.bind(this), /* currentCombinations */ []); }.bind(this), []); return combinations; }, isIncompletionOf: function(code, codeToMatch) { return this.createCompletionComparisonFunction(code)(codeToMatch); }, createCompletionComparisonFunction: function(code) { 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; return function createdIsIncompletionOf(codeToMatch) { 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); }.bind(this); }, 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; } }; return BopomofoEncoder; }));