/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global InputMethods */ (function() { 'use strict'; /* BoGo Engine. https://github.com/lewtds/bogo.js * * Copyright 2014, Trung Ngo * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ function BoGo() { var EffectType = { APPENDING: 0, MARK: 1, TONE: 2 }; var Mark = { NONE: 0, HAT: 1, BREVE: 2, HORN: 3, DASH: 4 }; var Tone = { NONE: 0, GRAVE: 1, ACUTE: 2, HOOK: 3, TILDE: 4, DOT: 5 }; var VOWELS = 'aàáảãạăằắẳẵặâầấẩẫậeèéẻẽẹêềếểễệiìíỉĩị' + 'oòóỏõọôồốổỗộơờớởỡợuùúủũụưừứửữựyỳýỷỹỵ'; var composition = []; var rules = []; var MARKS_MAP = { 'a': 'aâă__', 'â': 'aâă__', 'ă': 'aâă__', 'e': 'eê___', 'ê': 'eê___', 'o': 'oô_ơ_', 'ô': 'oô_ơ_', 'ơ': 'oô_ơ_', 'u': 'u__ư_', 'ư': 'u__ư_', 'd': 'd___đ', 'đ': 'd___đ' }; var MARK_CHARS = { '^': Mark.HAT, '(': Mark.BREVE, '+': Mark.HORN, '-': Mark.DASH }; var TONE_CHARS = { '~': Tone.TILDE, '\'': Tone.ACUTE, '?': Tone.HOOK, '`': Tone.GRAVE, '.': Tone.DOT }; function is_vowel(chr) { return VOWELS.indexOf(chr) != -1; } function add_mark_to_char(chr, mark) { var result; var tone = get_tone_from_char(chr); chr = add_tone_to_char(chr, Tone.NONE); if (chr in MARKS_MAP && MARKS_MAP[chr][mark] != '_') { result = MARKS_MAP[chr][mark]; } else { result = chr; } result = add_tone_to_char(result, tone); return result; } function get_tone_from_char(chr) { var position = VOWELS.indexOf(chr); if (position != -1) { return position % 6; } else { return Tone.NONE; } } function add_tone_to_char(chr, tone) { var result; var position = VOWELS.indexOf(chr); if (position != -1) { var current_tone = position % 6; var offset = tone - current_tone; result = VOWELS[position + offset]; } else { result = chr; } return result; } function find_mark_target(rule) { var target; for (var i = composition.length - 1; i > -1; i--) { if (composition[i].rule.key == rule.effective_on) { target = composition[i]; } } return target; } function find_rightmost_vowels() { var vowels = []; for (var i = composition.length - 1; i >= 0; i--) { var trans = composition[i]; if (trans.rule.type == EffectType.APPENDING && is_vowel(trans.rule.key)) { vowels.unshift(trans); } } return vowels; } function find_next_appending_trans(trans) { var from_index = composition.indexOf(trans); var next_appending_trans; // FIXME: Need not-found guard. for (var i = from_index + 1; i < composition.length; i++) { if (composition[i].rule.type == EffectType.APPENDING) { next_appending_trans = composition[i]; } } return next_appending_trans; } function find_tone_target(rule) { var vowels = find_rightmost_vowels(); var target; if (vowels.length == 1) { // cá target = vowels[0]; } else if (vowels.length == 2) { if (find_next_appending_trans(vowels[1]) !== undefined || flatten(vowels) == 'uo') { // nước, thuở target = vowels[1]; } else { // cáo target = vowels[0]; } } else if (vowels.length == 3) { if (flatten(vowels) == 'uye') { // chuyển target = vowels[2]; } else { // khuỷu target = vowels[1]; } } return target; } function refresh_last_tone_target() { // Refresh the tone position of the last EffectType.TONE transformation. for (var i = composition.length - 1; i >= 0; i--) { var trans = composition[i]; if (trans.rule.type == EffectType.TONE) { var new_target = find_tone_target(trans.rule); trans.target = new_target; break; } } } function process_char(chr) { var isUpperCase = chr === chr.toUpperCase(); chr = chr.toLowerCase(); var applicable_rules = []; rules.forEach(function(rule) { if (rule.key == chr) { applicable_rules.push(rule); } }); // If none of the applicable_rules can actually be applied then this new // transformation fallbacks to an APPENDING one. var trans = { rule: { type: EffectType.APPENDING, key: chr }, isUpperCase: isUpperCase }; for (var i = 0; i < applicable_rules.length; i++) { var rule = applicable_rules[i]; var target; if (rule.type == EffectType.MARK) { target = find_mark_target(rule); } else if (rule.type == EffectType.TONE) { target = find_tone_target(rule); } if (target !== undefined) { // Fix uaw being wrongly processed to muă by skipping // the aw rule. Then the uw rule will be matched later. // Note that this requires the aw rule be placed before // uw in the rule list. if (chr == 'w') { var target_index = composition.indexOf(target); var prev_trans = composition[target_index - 1]; if (target.rule.key == 'a' && prev_trans.rule.key == 'u') { continue; } } trans.rule = rule; trans.target = target; break; } } composition.push(trans); // Implement the uow typing shortcut by creating a virtual // Mark.HORN rule that targets 'u'. // // FIXME: This is a potential slowdown. Perhaps it should be // toggled by a config key. if (flatten().match(/uơ.+$/)) { var vowels = find_rightmost_vowels(); var virtual_trans = { rule: { type: EffectType.MARK, key: '', // This is a virtual rule, // it should not appear in the raw string. effect: Mark.HORN }, target: vowels[0] }; composition.push(virtual_trans); } // Sometimes, a tone's position in a previous state must be changed to // fit the new state. // // e.g. // prev state: chuyenr -> chuỷen // this state: chuyenre -> chuyển if (trans.rule.type == EffectType.APPENDING) { refresh_last_tone_target(); } } function flatten() { var canvas = []; composition.forEach(function(trans, index) { function apply_effect(func, trans) { var index = trans.target.dest; var char_with_effect = func(canvas[index], trans.rule.effect); // Double typing an effect key undoes it. Btw, we're playing // fast-and-loose here by relying on the fact that Tone.NONE // equals Mark.None and equals 0. if (char_with_effect == canvas[index]) { canvas[index] = func(canvas[index], Tone.NONE); } else { canvas[index] = char_with_effect; } } switch (trans.rule.type) { case EffectType.APPENDING: trans.dest = canvas.length; canvas.push(trans.rule.key); break; case EffectType.MARK: apply_effect(add_mark_to_char, trans); break; case EffectType.TONE: apply_effect(add_tone_to_char, trans); break; default: break; } }); composition.forEach(function(trans) { if (trans.rule.type == EffectType.APPENDING) { if (trans.isUpperCase) { canvas[trans.dest] = canvas[trans.dest].toUpperCase(); } } }); return canvas.join(''); } function process_string(string) { for (var i = 0; i < string.length; i++) { process_char(string[i]); } } // js > parse_rule('a a a^') // {type: EffectType.MARK, effect: HAT, key: a, effective_on: a} // // js > parse_rule('a w a(') // {type: EffectType.MARK, effect: BREVE, key: w, effective_on: a} // // js > parse_rule('a f a`') // {type: EffectType.MARK, effect: HAT, key: a, effective_on: a} // // js > parse_rule('w u+') // {type: EffectType.APPEND, effect: ư, key: w} function parse_rule(string) { var tokens = string.trim().replace(/\s\s+/, ' ').split(' '); var effective_on = tokens[0]; var key = tokens[1]; var type; var effect; var effect_char = tokens[2][1]; if (effect_char in MARK_CHARS) { type = EffectType.MARK; effect = MARK_CHARS[effect_char]; } else if (effect_char in TONE_CHARS) { type = EffectType.TONE; effect = TONE_CHARS[effect_char]; } var trans = { type: type, key: key, effect: effect, effective_on: effective_on }; return trans; } function process_backspace() { var indexes_to_remove = []; var last_appending_trans; var i; var trans; // Find the last APPENDING transformation and all // the transformations that add effects to it. for (i = composition.length - 1; i >= 0; i--) { trans = composition[i]; if (trans.rule.type == EffectType.APPENDING) { last_appending_trans = trans; indexes_to_remove.push(i); break; } } for (i = indexes_to_remove[0] + 1; i < composition.length; i++) { trans = composition[i]; if (trans.hasOwnProperty('target') && trans.target === last_appending_trans) { indexes_to_remove.push(i); } } // Then remove them indexes_to_remove.sort().reverse(); indexes_to_remove.forEach(function(index) { composition.splice(index, 1); }); } function get_raw_input_string() { var raw_input_keys = []; composition.forEach(function(trans) { raw_input_keys.push(trans.rule.key); }); return raw_input_keys.join(''); } function clear_composition() { composition = []; } function clear_rules() { rules = []; } function has_composition() { return composition.length !== 0; } var exports = { add_rule: function(rule_string) { rules.push(parse_rule(rule_string)); }, clear_rules: clear_rules, process_char: process_char, process_string: process_string, process_backspace: process_backspace, clear_composition: clear_composition, get_processed_string: flatten, get_raw_input_string: get_raw_input_string, has_composition: has_composition }; return exports; } var input_context; var engine; function init(_input_context) { console.log('KEYBOARD: ' + _input_context); input_context = _input_context; engine = new BoGo(); engine.add_rule('o w o+'); engine.add_rule('u w u+'); engine.add_rule('a a a^'); engine.add_rule('a w a('); engine.add_rule('e e e^'); engine.add_rule('o o o^'); engine.add_rule('d d d-'); engine.add_rule('_ f _`'); engine.add_rule('_ r _?'); engine.add_rule('_ x _~'); engine.add_rule('_ j _.'); engine.add_rule('_ s _\''); } function click(keycode, x, y) { if (keycode == 32) { input_context.endComposition(engine.get_processed_string()); engine.clear_composition(); input_context.sendKey(keycode); } else if (keycode == 8) { if (engine.has_composition()) { engine.process_backspace(); input_context.setComposition(engine.get_processed_string()); } else { engine.clear_composition(); input_context.sendKey(keycode); } } else { var chr = String.fromCharCode(keycode); engine.process_char(chr); input_context.setComposition(engine.get_processed_string()); } } // Expose the engine to the Gaia keyboard InputMethods.jstelex = { init: init, click: click }; })();