From 2de5304b8685356c7f3e1320c9e38d8b7aeab078 Mon Sep 17 00:00:00 2001 From: "Ted Clancy (:tedders1)" Date: Tue, 8 Apr 2014 20:16:59 -0700 Subject: [PATCH] Bug 974794: Vietnamese IME. Incorporates Trung Ngo's Telex IME code. --- apps/keyboard/js/imes/jstelex/jstelex.js | 488 ++++++++++++++++++ .../keyboard/js/imes/vietnamese/vietnamese.js | 416 +++++++++++++++ apps/keyboard/js/layouts/vi-Qwerty.js | 39 ++ apps/keyboard/js/layouts/vi-Telex.js | 26 + apps/keyboard/js/layouts/vi-Typewriter.js | 39 ++ apps/keyboard/js/render.js | 26 +- apps/keyboard/style/keyboard.css | 4 + build/config/keyboard-layouts.json | 4 + 8 files changed, 1039 insertions(+), 3 deletions(-) create mode 100644 apps/keyboard/js/imes/jstelex/jstelex.js create mode 100644 apps/keyboard/js/imes/vietnamese/vietnamese.js create mode 100644 apps/keyboard/js/layouts/vi-Qwerty.js create mode 100644 apps/keyboard/js/layouts/vi-Telex.js create mode 100644 apps/keyboard/js/layouts/vi-Typewriter.js diff --git a/apps/keyboard/js/imes/jstelex/jstelex.js b/apps/keyboard/js/imes/jstelex/jstelex.js new file mode 100644 index 000000000000..e1f66a536a97 --- /dev/null +++ b/apps/keyboard/js/imes/jstelex/jstelex.js @@ -0,0 +1,488 @@ +/* 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 + }; +})(); diff --git a/apps/keyboard/js/imes/vietnamese/vietnamese.js b/apps/keyboard/js/imes/vietnamese/vietnamese.js new file mode 100644 index 000000000000..ae27366c556d --- /dev/null +++ b/apps/keyboard/js/imes/vietnamese/vietnamese.js @@ -0,0 +1,416 @@ +/* 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/. */ + +/* jshint moz:true */ +/* jshint unused:true */ +/* global InputMethods */ +/* global KeyEvent */ + +// ================================================================== +// WARNING. THIS FUNCTION USES PRECOMPOSED UNICODE CHARACTERS IN +// STRING LITERALS. DO NOT NORMALIZE THIS FILE. +// IF YOU NORMALIZE THIS FILE, THE STRINGS WILL CHANGE AND THE +// PROGRAM WILL BREAK. +// +// The use of precomposed Unicode characters is deliberate. This +// IM outputs precomposed Vietnamese characters rather than +// using combining character sequences because Firefox OS +// has trouble rendering Vietnamese combining character sequences. +// +// I considered using Unicode escape sequences instead of the +// actual Vietnamese characters, but that would have made the code +// much harder to read, understand, and maintain. Furthermore, I +// don't think there's much risk that someone will normalize this +// file. +// +// Existing Javascript standards say that all strings should be +// normalized in NFC format. In practice, no Javascript interpreter +// does that, and nor should they. There is a proposal to remove +// that requirement from the next standard, partly out of consider- +// ation for languages like Vietnamese which usually aren't NFC +// normalized, and partly to reflect actual practice. +// +// http://wiki.ecmascript.org/doku.php?id=strawman:unicode_normalization +// +// ================================================================== + + + +// Like pinyin, Vietnam's writing system (Quốc Ngữ) has fixed rules for +// what makes a valid syllable. + +// A syllable consists of: +// optional initial + vowel cluster + optional final + +// Not all vowel clusters can occur before all finals. +// The vowel cluster can optionally carry one of 5 tone marks. + +// The spelling is fairly regular, but it inherits a few irregularities +// from Portuguese. +// * The /k/ sound can be spelt C or K depending on the +// following vowel. (Or Q before a /w/ glide.) +// * The initial G becomes GH before E or I (or Ê or Y). Similarly, NG +// becomes NGH +// * The /w/ sound at the start of a vowel cluster can be spelt O or U +// depending on the following vowel. However, after a Q, it is always +// spelt U, regardless of the following vowel. Basically, the list of +// valid vowel clusters completely changes after a Q. +// * The vowel cluster IÊ becomes YÊ at the start of a word. Likewise, +// IÊU> becomes YÊU. + +(function() { + 'use strict'; + + const QN = { + unmarkedVowels: 'AaĂăÂâEeÊêIiOoÔôƠơUuƯưYy', + + // The elements of this array represent the five marked tones of + // Vietnamese. (A sixth tone is unmarked.) + markedVowels: [ + 'ÁáẮắẤấÉéẾếÍíÓóỐốỚớÚúỨứÝý', // Sắc Tone + 'ÀàẰằẦầÈèỀềÌìÒòỒồỜờÙùỪừỲỳ', // Huyền Tone + 'ẢảẲẳẨẩẺẻỂểỈỉỎỏỔổỞởỦủỬửỶỷ', // Hỏi Tone + 'ÃãẴẵẪẫẼẽỄễĨĩÕõỖỗỠỡŨũỮữỸỹ', // Ngã Tone + 'ẠạẶặẬậẸẹỆệỊịỌọỘộỢợỤụỰựỴỵ' // Nặng Tone + ], + + // All the intials except Q (including the null initial). + initialsNotQ: '(b|c|ch|d|đ|g|gh|gi|h|k|kh|l|m|n|' + + 'ng|ngh|nh|p|ph|r|s|t|th|tr|v|x|)', + + // All the initials that start with Q. + initialQ: '(q)', + + // All the finals + finals: '(c|ch|m|n|ng|nh|p|t)', + + // These vowel clusters are valid when there is no final + // (except when the intial is Q) + nucleusOpen: '(a|e|ê|i|y|o|ô|ơ|u|ư|' + // vowel + 'ai|ay|ăy|oi|ơi|ôi|ui|ưi|' + // vowel + /j/ + 'ao|au|âu|eo|êu|iu|ưu|' + // vowel + /w/ + 'ia|ua|ưa|' + // diphthong + 'uơi|ươi|' + // diphthong + /j/ + 'iêu|yêu|ươu|' + // diphthong + /w/ + 'oa|oe|uê|uy|uơ|' + // /w/ + vowel + 'uya|' + // /w/ + diphthong + 'oai|oay|uây)', // /w/ + vowel + /j/ + + // These vowel clusters are valid before any final, + // (except when the intial is Q) + nucleusClosed: '(a|ă|â|e|ê|i|o|ô|ơ|u|ư|' + // vowel + 'iê|yê|uơ|ươ|' + // diphthong + 'oa|oă|uâ|oe|uê|uy|uơ|' + // /w/ + vowel + 'uyê|' + // /w/ + diphthong + 'oo)', // 'oo' only appears in French loan words + // like 'xoong' (casserole). Some Vietnamese + // textbooks omit it, but it definitely exists. + + + // These vowel clusters are valid when there is no final + // and the initial is Q. + nucleusOpenAfterQ: '(ua|ue|uê|ui|uy|uơ|' + // /w/ + vowel + 'uya|' + // /w/ + diphthong + 'uai|uay|uây)', // /w/ + vowel + /j/ + + // These vowel clusters are valid before any final, + // when the initial is Q. + nucleusClosedAfterQ: '(ua|uă|uâ|ue|uê|ui|uy|uơ|' + // /w/ + vowel + 'uyê)' // /w/ + diphthong + }; + + const vietWordParser = { + p1: new RegExp('^' + QN.initialsNotQ + QN.nucleusOpen + '$'), + p2: new RegExp('^' + QN.initialsNotQ + QN.nucleusClosed + QN.finals + '$'), + p3: new RegExp('^' + QN.initialQ + QN.nucleusOpenAfterQ + '$'), + p4: new RegExp('^' + QN.initialQ + QN.nucleusClosedAfterQ + QN.finals + + '$'), + + isValidWord: function(word) { + var w = word.toLowerCase(); + var array = w.match(this.p1) || w.match(this.p2) || + w.match(this.p3) || w.match(this.p4); + + if (!array) { + return false; + } + + var initialCluster = array[1]; + var vowelCluster = array[2]; + var finalCluster = array[3] || ''; + + // G -> GH and NG -> NGH before E/Ê or I/Y + if (initialCluster.match(/^(g|ng)$/) && + vowelCluster.match(/^(e|ê|i|y)/)) { + return false; + } + if (initialCluster.match(/^(gh|ngh)$/) && + !vowelCluster.match(/^(e|ê|i|y)/)) { + return false; + } + + // GI can't come before I/Y + if (initialCluster === 'gi' && vowelCluster.match(/^(i|y)/)) { + return false; + } + + // K before I/Y or E; C otherwise + if (initialCluster === 'k' && !vowelCluster.match(/^(e|ê|i|y)/)) { + return false; + } + if (initialCluster === 'c' && vowelCluster.match(/^(e|ê|i|y)/)) { + return false; + } + + // IÊ -> YÊ and IÊU -> YÊU when there's no initial + if (initialCluster === '' && vowelCluster.match(/^iê/)) { + return false; + } + if (initialCluster !== '' && vowelCluster.match(/^yê/)) { + return false; + } + + // The vowel clusters U and UYÊ can only come before T or N. + if (vowelCluster.match(/^(uâ|uyê)$/) && !finalCluster.match(/^(t|n)$/)) { + return false; + } + + return true; + }, + + addToneMarkToVowel: function(vowel, tone) { + var vowelIndex = QN.unmarkedVowels.indexOf(vowel); + if (vowelIndex >= 0) { + return QN.markedVowels[tone][vowelIndex]; + } + }, + + findTonePosition: function(word) { + var w = word.toLowerCase(); + + var array = w.match(this.p1) || w.match(this.p2) || + w.match(this.p3) || w.match(this.p4); + + if (!array) { + return 0; + } + + var initialCluster = array[1]; + var vowelCluster = array[2]; + var finalCluster = array[3] || ''; + + // When placing the tone mark, the U in QU should be considered + // part of the initial, not part of the vowel cluster. + // It never carries the tone mark. + if (initialCluster == 'q') { + initialCluster = 'qu'; + vowelCluster = vowelCluster.substr(1); + } + + // Here are the traditional rules for where to place the tone mark: + // * Vowels with diacritics are preferred over vowels without one. + // If that doesn't settle it, use these rules: + // * If the word has three vowels, it goes on the middle one. + // * If the word has two vowels followed by a consonant, it goes on the + // last vowel. + // * If the word ends with two vowels, it goes on the first vowel. + + // The only time a word can have more than one diacritic is when it + // contains ươ, which has to be followed by a consonant. By the above + // rules, that means the ơ gets the diacritic, not the ư. + + return w.search(/(ơ)/) + 1 || + w.search(/(ă|â|ê|ô|ư)/) + 1 || + (vowelCluster.length === 3 ? initialCluster.length + 2 : + finalCluster !== '' ? initialCluster.length + vowelCluster.length : + initialCluster.length + 1); + }, + + addHat: function(word) { + return word.replace('a', 'â').replace('A', 'Â') + .replace('e', 'ê').replace('E', 'Ê') + .replace('o', 'ô').replace('O', 'Ô'); + }, + + generateCandidates: function(word) { + if (!this.isValidWord(word)) { + return []; + } + + // Vietnamese has five tone marks. However, if a word ends + // in P, T, C, or CH, only the SẮC and NẶNG tones can be used. + var stopFinal = !!word.match(/(p|t|c|ch)$/i); + var marks = stopFinal ? [0, 4] : [0, 1, 2, 3, 4]; + + var tonePos = this.findTonePosition(word); + + var candidates = marks.map((mark) => { + return word.substr(0, tonePos - 1) + + this.addToneMarkToVowel(word.charAt(tonePos - 1), mark) + + word.substr(tonePos); + }); + + return candidates; + } + }; + + var keyboard, buffer = ''; + var capitalize = false; + var capitalizeNext = false; + var tentativeSpace = false; + + const BACKSPACE = 8; + const HAT = 94; + + function isBufferEmpty() { + return buffer === ''; + } + + function clearBuffer() { + buffer = ''; + } + + function addToBuffer(keycode) { + if (keycode == BACKSPACE) { + backspace(); + } + buffer = buffer + String.fromCharCode(keycode); + } + + function backspace() { + if (!isBufferEmpty()) { + buffer = buffer.substring(0, buffer.length -1); + keyboard.setComposition(buffer); + } + else { + keyboard.sendKey(KeyEvent.DOM_VK_BACK_SPACE); + } + } + + function inputDone() { + keyboard.endComposition(buffer); + clearBuffer(); + } + + InputMethods.vietnamese = { + init: init, + activate: activate, + deactivate: deactivate, + click: click, + select: select + }; + + function init(interfaceObject) { + keyboard = interfaceObject; + } + + function activate(lang, state) { + capitalize = !!state.type.match(/^(text|textarea|search)$/); + tentativeSpace = false; + + var cursor = state.selectionStart; + var inputText = state.value; + + capitalizeNext = false; + if (capitalize) { + if (cursor === 0) { + capitalizeNext = true; + } + else { + var charBeforeCursor = cursor - 1; + while (charBeforeCursor >= 0 && inputText[charBeforeCursor] == ' ') { + --charBeforeCursor; + } + capitalizeNext = !!inputText[charBeforeCursor].match(/[\!\?\.]/); + } + } + keyboard.setUpperCase(capitalizeNext); + } + + function deactivate() { + if (!isBufferEmpty()) { + inputDone(); + keyboard.sendCandidates([]); + } + } + + function click(keycode) { + var s = String.fromCharCode(keycode); + + var wasTentativeSpace = tentativeSpace; + tentativeSpace = false; + + var wasCapitalizeNext = capitalizeNext; + capitalizeNext = false; + + // Automatically transform E -> Ê after an I or Y. I find this really handy. + if (s == 'e' && buffer.match(/(y|i)$/i)) { + keycode = 'ê'.charCodeAt(0); + } + else if (s == 'E' && buffer.match(/(y|i)$/i)) { + keycode = 'Ê'.charCodeAt(0); + } + + // Letters + if (s.match(/[A-Za-zĂÂĐÊÔƠUƯ]/i)) { + addToBuffer(keycode); + } + // The special hat key. + else if (keycode == HAT) { + buffer = vietWordParser.addHat(buffer); + } + // Backspace + else if (keycode == BACKSPACE) { + backspace(); + } + // Handling tentative space + else if (wasTentativeSpace && + (s == '.' || s == ',' || s == '!' || s == '?')) { + keyboard.sendKey(KeyEvent.DOM_VK_BACK_SPACE); + keyboard.sendKey(keycode); + keyboard.sendKey(KeyEvent.DOM_VK_SPACE); + if (s == '.' || s == '!' || s == '?') { + capitalizeNext = true; + } + } + // Using spacebar to finish input + else if (s == ' ' && !isBufferEmpty()) { + buffer += s; + tentativeSpace = true; + inputDone(); + } + // Punctuation, symbols, other. + else { + if (!isBufferEmpty()) { + inputDone(); + } + if (s == '.' || s == '!' || s == '?') { + capitalizeNext = true; + } + if (s == ' ') { + capitalizeNext = wasCapitalizeNext; + } + keyboard.sendKey(keycode); + } + + if (!isBufferEmpty()) { + keyboard.setComposition(buffer); + var candidates = vietWordParser.generateCandidates(buffer); + keyboard.sendCandidates(candidates); + } else { + keyboard.sendCandidates([]); + } + + keyboard.setUpperCase(capitalizeNext); + } + + function select(s) { + buffer = s; + buffer += ' '; + tentativeSpace = true; + inputDone(); + keyboard.sendCandidates([]); + } + +})(); diff --git a/apps/keyboard/js/layouts/vi-Qwerty.js b/apps/keyboard/js/layouts/vi-Qwerty.js new file mode 100644 index 000000000000..91dee83e5967 --- /dev/null +++ b/apps/keyboard/js/layouts/vi-Qwerty.js @@ -0,0 +1,39 @@ +/* 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/. */ + +Keyboards['vi-Qwerty'] = { + label: 'Vietnamese', + menuLabel: 'Tiếng Việt (QWERTY)', + imEngine: 'vietnamese', + needsCandidatePanel: true, + width: 10, + types: ['text', 'url', 'email'], + alt: { + 'a': 'ăâ', + 'd': 'đ', + 'e': 'ê', + 'o': 'ôơ', + 'u': 'ư', + '.': ',?!-;:' + }, + keys: [ + [ + { value: 'q' }, { value: 'w' }, { value: 'e' }, { value: 'r' }, + { value: 't' } , { value: 'y' }, { value: 'u' }, { value: 'i' }, + { value: 'o' }, { value: 'p' } + ], [ + { value: 'a' }, { value: 's' }, { value: 'd' }, { value: 'f' }, + { value: 'g' } , { value: 'h' }, { value: 'j' }, { value: 'k' }, + { value: 'l' } + ], [ + { value: '⇪', ratio: 1.5, keyCode: KeyEvent.DOM_VK_CAPS_LOCK }, + { value: 'z' }, { value: 'x' }, { value: 'c' }, { value: 'v' }, + { value: 'b' }, { value: 'n' }, { value: 'm' }, + { value: '⌫', ratio: 1.5, keyCode: KeyEvent.DOM_VK_BACK_SPACE } + ], [ + { value: ' ', ratio: 8, keyCode: KeyboardEvent.DOM_VK_SPACE }, + { value: '↵', ratio: 2, keyCode: KeyEvent.DOM_VK_RETURN } + ] + ] +}; diff --git a/apps/keyboard/js/layouts/vi-Telex.js b/apps/keyboard/js/layouts/vi-Telex.js new file mode 100644 index 000000000000..c8f2942d9467 --- /dev/null +++ b/apps/keyboard/js/layouts/vi-Telex.js @@ -0,0 +1,26 @@ +Keyboards['vi-Telex'] = { + label: 'Vietnamese (Telex)', + menuLabel: 'Tiếng Việt (Telex)', + imEngine: 'jstelex', + types: ['text', 'url', 'email'], + keys: [ + [ + { value: 'q' }, { value: 'w' }, { value: 'e' } , { value: 'r' }, + { value: 't' } , { value: 'y' }, { value: 'u' } , { value: 'i' }, + { value: 'o' }, { value: 'p' } + ], [ + { value: 'a' }, { value: 's' }, { value: 'd' }, { value: 'f' }, + { value: 'g' } , { value: 'h' }, { value: 'j' }, { value: 'k' }, + { value: 'l' }, + { value: ':', visible: ['url']}, { value: '_', visible: ['email']} + ], [ + { value: '⇪', ratio: 1.5, keyCode: KeyEvent.DOM_VK_CAPS_LOCK }, + { value: 'z' }, { value: 'x' }, { value: 'c' }, { value: 'v' }, + { value: 'b' }, { value: 'n' }, { value: 'm' }, + { value: '⌫', ratio: 1.5, keyCode: KeyEvent.DOM_VK_BACK_SPACE } + ], [ + { value: ' ', ratio: 8, keyCode: KeyboardEvent.DOM_VK_SPACE }, + { value: '↵', ratio: 2, keyCode: KeyEvent.DOM_VK_RETURN } + ] + ] +}; diff --git a/apps/keyboard/js/layouts/vi-Typewriter.js b/apps/keyboard/js/layouts/vi-Typewriter.js new file mode 100644 index 000000000000..052edc575a4a --- /dev/null +++ b/apps/keyboard/js/layouts/vi-Typewriter.js @@ -0,0 +1,39 @@ +/* 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/. */ + +// This is a traditional Vietnamese typewriter layout. +Keyboards['vi-Typewriter'] = { + label: 'Vietnamese', + menuLabel: 'Tiếng Việt', + imEngine: 'vietnamese', + needsCandidatePanel: true, + width: 10, + types: ['text', 'url', 'email'], + alt: { + 'đ': 'z', + 'ư': 'f', + 'ơ': 'j', + 'ă': 'w', + '.': ',?!-;:' + }, + keys: [ + [ + { value: 'a' }, { value: 'đ' }, { value: 'e' }, { value: 'r' }, + { value: 't' } , { value: 'y' }, { value: 'u' }, { value: 'i' }, + { value: 'o' }, { value: 'p' } + ], [ + { value: 'q' }, { value: 's' }, { value: 'd' }, { value: 'ư' }, + { value: 'g' } , { value: 'h' }, { value: 'ơ' }, { value: 'k' }, + { value: 'l' }, { value: 'm' } + ], [ + { value: '⇪', ratio: 1.5, keyCode: KeyEvent.DOM_VK_CAPS_LOCK }, + { value: 'ă' }, { value: 'x' }, { value: 'c' }, { value: 'v' }, + { value: 'b' }, { value: 'n' }, { value: '^', keyCode: 94 }, + { value: '⌫', ratio: 1.5, keyCode: KeyEvent.DOM_VK_BACK_SPACE } + ], [ + { value: ' ', ratio: 8, keyCode: KeyboardEvent.DOM_VK_SPACE }, + { value: '↵', ratio: 2, keyCode: KeyEvent.DOM_VK_RETURN } + ] + ] +}; diff --git a/apps/keyboard/js/render.js b/apps/keyboard/js/render.js index 0a36bfc590fe..346b03dc2e94 100644 --- a/apps/keyboard/js/render.js +++ b/apps/keyboard/js/render.js @@ -337,6 +337,14 @@ const IMERender = (function() { if (!activeIme) return; + if (inputMethodName == 'vietnamese' && candidates.length) { + // In the Vietnamese IM, the candidates correspond to tones. + // There will be either 2 or 5. All must appear. + numberOfCandidatesPerRow = candidates.length; + candidateUnitWidth = + Math.floor(ime.clientWidth / numberOfCandidatesPerRow); + } + // TODO: Save the element var candidatePanel = activeIme.querySelector('.keyboard-candidate-panel'); var candidatePanelToggleButton = @@ -487,11 +495,20 @@ const IMERender = (function() { var candidatesLength = candidates.length; for (var i = 0; i < candidatesLength; i++) { - var cand = candidates[i][0]; - var data = candidates[i][1]; - var span = document.createElement('span'); + var cand, data; + if (typeof candidates[i] == 'string') { + cand = data = candidates[i]; + } else { + cand = candidates[i][0]; + data = candidates[i][1]; + } + var unit = (cand.length >> 1) + 1; + if (inputMethodName == 'vietnamese') { + unit = 1; + } + var span = document.createElement('span'); span.textContent = cand; span.dataset.selection = true; span.dataset.data = data; @@ -708,6 +725,9 @@ const IMERender = (function() { ime.querySelectorAll('.candidate-row span'), function(item) { var unit = (item.textContent.length >> 1) + 1; + if (inputMethodName == 'vietnamese') { + unit = 1; + } item.style.width = (unit * candidateUnitWidth - 2) + 'px'; } ); diff --git a/apps/keyboard/style/keyboard.css b/apps/keyboard/style/keyboard.css index d24955d973fe..13274cef07fb 100644 --- a/apps/keyboard/style/keyboard.css +++ b/apps/keyboard/style/keyboard.css @@ -441,6 +441,10 @@ bubble above the key when you tap and hold. */ border: none; } +.keyboard-candidate-panel.vietnamese .candidate-row span { + font-size: 1.7rem; +} + /* for latin suggestions we don't need such a tall box */ /* and in latin we hide the toggle button, so we can be full-width */ .candidate-panel .keyboard-candidate-panel.latin { diff --git a/build/config/keyboard-layouts.json b/build/config/keyboard-layouts.json index 8e31bc47be76..fd2e53197393 100644 --- a/build/config/keyboard-layouts.json +++ b/build/config/keyboard-layouts.json @@ -166,6 +166,10 @@ "ur": [ {"layoutId": "en", "app": ["apps", "keyboard"]} ], + "vi": [ + {"layoutId": "vi-Typewriter", "app": ["apps", "keyboard"]}, + {"layoutId": "fr", "app": ["apps", "keyboard"]} + ], "zh-CN": [ {"layoutId": "zh-Hans-Pinyin", "app": ["apps", "keyboard"]}, {"layoutId": "en", "app": ["apps", "keyboard"]}