Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
423 lines
14 KiB
JavaScript
423 lines
14 KiB
JavaScript
/* 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';
|
|
|
|
var 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).
|
|
// (Any usual Vietnamese syllable can be prefixed with an X
|
|
// when transcribing foreign proper nouns.)
|
|
initialsNotQ: '(x?(?:b|c|ch|d|đ|g|gh|gi|h|k|kh|l|m|n|' +
|
|
'ng|ngh|nh|p|ph|r|s|t|th|tr|v)?)',
|
|
|
|
// All the initials that start with Q.
|
|
initialQ: '(x?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ôi|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ô|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
|
|
};
|
|
|
|
var 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;
|
|
|
|
var BACKSPACE = 8;
|
|
var 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.
|
|
// And UO -> UÔ
|
|
if (s == 'e' && buffer.match(/(y|i)$/i)) {
|
|
keycode = 'ê'.charCodeAt(0);
|
|
}
|
|
else if (s == 'E' && buffer.match(/(y|i)$/i)) {
|
|
keycode = 'Ê'.charCodeAt(0);
|
|
} else if (s == 'o' && buffer.match(/u$/i)) {
|
|
keycode = 'ô'.charCodeAt(0);
|
|
} else if (s == 'O' && buffer.match(/u$/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([]);
|
|
}
|
|
|
|
})();
|