jskanji.js

'use strict';

TODO current bugs 1. little case hiragana cannot convert to katakana 2. return does not work in select mode 3. getSuggestion

(function() {

IME special key code map see layout.js

  var IMESpecialKey = {
    BACK: -10,
    PREVIOUS: -11,
    NEXT: -12,
    MARK: -13,
    TRANSFORM: -14,
    CASE_CHANGE: -16,
    FULL: -17,
    H2K: -18,
    EN_LAYOUT: -20,
    NUM_LAYOUT: -21,
    BASIC_LAYOUT: -22
  };

Keyboard mode mapping FIXME partly used, maybe remove used

  var IMEMode = {
      FULL_HIRAGANA: 0,
      FULL_KATAKANA: 1,
      FULL_ALPHABET: 2,
      FULL_NUMBER: 3,
      HALF_NUMBER: 4,
      HALF_ALPHABET: 5,
      HALF_KATAKANA: 6
  };

  var IMEKeyMap = {
    'あ': 0, 'か': 1, 'さ': 2, 'た': 3, 'な': 4, 'は': 5, 'ま': 6, 'や': 7, 'ら': 8, 'わ': 9, '、': 10,
    'ア': 0, 'カ': 1, 'サ': 2, 'タ': 3, 'ナ': 4, 'ハ': 5, 'マ': 6, 'ヤ': 7, 'ラ': 8, 'ワ': 9,
    'ア': 0, 'カ': 1, 'サ': 2, 'タ': 3, 'ナ': 4, 'ハ': 5, 'マ': 6, 'ヤ': 7, 'ラ': 8, 'ワ': 9, '、': 10
  }; 

Key loop Ex. あ displays when clicking あ い displays when clicking あ twice

  var IMEHiraganaCycleTable = [
    ['あ', 'い', 'う', 'え', 'お', 'ぁ', 'ぃ', 'ぅ', 'ぇ', 'ぉ'],
    ['か', 'き', 'く', 'け', 'こ'],
    ['さ', 'し', 'す', 'せ', 'そ'],
    ['た', 'ち', 'つ', 'て', 'と', 'っ'],
    ['な', 'に', 'ぬ', 'ね', 'の'],
    ['は', 'ひ', 'ふ', 'へ', 'ほ'],
    ['ま', 'み', 'む', 'め', 'も'],
    ['や', 'ゆ', 'よ', 'ゃ', 'ゅ', 'ょ'],
    ['ら', 'り', 'る', 'れ', 'ろ'],
    ['わ', 'を', 'ん', 'ゎ', 'ー'],
    ['、', '。', '?', '!', '・', ' ']
  ];
  var IMEFullKatakanaCycleTable = [
    ["ア", "イ", "ウ", "エ", "オ", "ァ", "ィ", "ゥ", "ェ", "ォ"],
    ["カ", "キ", "ク", "ケ", "コ"],
    ["サ", "シ", "ス", "セ", "ソ"],
    ["タ", "チ", "ツ", "テ", "ト", "ッ"],
    ["ナ", "ニ", "ヌ", "ネ", "ノ"],
    ["ハ", "ヒ", "フ", "ヘ", "ホ"],
    ["マ", "ミ", "ム", "メ", "モ"],
    ["ヤ", "ユ", "ヨ", "ャ", "ュ", "ョ"],
    ["ラ", "リ", "ル", "レ", "ロ"],
    ["ワ", "ヲ", "ン", "ヮ", "ー"],
    ["、", "。", "?", "!", "・", " "]
  ];
  var IMEHalfKatakanaCycleTable = [
    ["ア", "イ", "ウ", "エ", "オ", "ァ", "ィ", "ゥ", "ェ", "ォ"],
    ["カ", "キ", "ク", "ケ", "コ"],
    ["サ", "シ", "ス", "セ", "ソ"],
    ["タ", "チ", "ツ", "テ", "ト", "ッ"],
    ["ナ", "ニ", "ヌ", "ネ", "ノ"],
    ["ハ", "ヒ", "フ", "ヘ", "ホ"],
    ["マ", "ミ", "ム", "メ", "モ"],
    ["ヤ", "ユ", "ヨ", "ャ", "ュ", "ョ"],
    ["ラ", "リ", "ル", "レ", "ロ"],
    ["ワ", "ヲ", "ン", "ー"],
    ["、", "。", "?", "!", "・", " "]
  ];

Hiragana (平假名) case convert table

  var HiraganaCaseTable = {
    'あ': 'ぁ', 'い': 'ぃ', 'う': 'ぅ', 'え': 'ぇ', 'お': 'ぉ', 'ぁ': 'あ', 'ぃ': 'い', 'ぅ': 'ヴ', 'ぇ': 'え', 'ぉ': 'お', 'か': 'が', 'き': 'ぎ',
    'く': 'ぐ', 'け': 'げ', 'こ': 'ご', 'が': 'か', 'ぎ': 'き', 'ぐ': 'く', 'げ': 'け', 'ご': 'こ', 'さ': 'ざ', 'し': 'じ', 'す': 'ず', 'せ': 'ぜ',
    'そ': 'ぞ', 'ざ': 'さ', 'じ': 'し', 'ず': 'す', 'ぜ': 'せ', 'ぞ': 'そ', 'た': 'だ', 'ち': 'ぢ', 'つ': 'っ', 'て': 'で', 'と': 'ど', 'だ': 'た',
    'ぢ': 'ち', 'っ': 'づ', 'で': 'て', 'ど': 'と', 'づ': 'つ', 'ヴ': 'う', 'は': 'ば', 'ひ': 'び', 'ふ': 'ぶ', 'へ': 'べ', 'ほ': 'ぼ', 'ば': 'ぱ',
    'び': 'ぴ', 'ぶ': 'ぷ', 'べ': 'ぺ', 'ぼ': 'ぽ', 'ぱ': 'は', 'ぴ': 'ひ', 'ぷ': 'ふ', 'ぺ': 'へ', 'ぽ': 'ほ', 'や': 'ゃ', 'ゆ': 'ゅ', 'よ': 'ょ',
    'ゃ': 'や', 'ゅ': 'ゆ', 'ょ': 'よ', 'わ': 'ゎ', 'ゎ': 'わ', '゛': '゜', '゜': '゛'
  };

Katakana (片假名) case convert table

  var IMEFullKatakanaCaseTable = {
    'ア': 'ァ', 'イ': 'ィ', 'ウ': 'ゥ', 'エ': 'ェ', 'オ': 'ォ', 'ァ': 'ア', 'ィ': 'イ', 'ゥ': 'ヴ', 'ェ': 'エ', 'ォ': 'オ', 'カ': 'ガ', 'キ': 'ギ',
    'ク': 'グ', 'ケ': 'ゲ', 'コ': 'ゴ', 'ガ': 'カ', 'ギ': 'キ', 'グ': 'ク', 'ゲ': 'ケ', 'ゴ': 'コ', 'サ': 'ザ', 'シ': 'ジ', 'ス': 'ズ', 'セ': 'ゼ',
    'ソ': 'ゾ', 'ザ': 'サ', 'ジ': 'シ', 'ズ': 'ス', 'ゼ': 'セ', 'ゾ': 'ソ', 'タ': 'ダ', 'チ': 'ヂ', 'ツ': 'ッ', 'テ': 'デ', 'ト': 'ド', 'ダ': 'タ',
    'ヂ': 'チ', 'ッ': 'ヅ', 'デ': 'テ', 'ド': 'ト', 'ヅ': 'ツ', 'ヴ': 'ウ', 'ハ': 'バ', 'ヒ': 'ビ', 'フ': 'ブ', 'ヘ': 'ベ', 'ホ': 'ボ', 'バ': 'パ',
    'ビ': 'ピ', 'ブ': 'プ', 'ベ': 'ペ', 'ボ': 'ポ', 'パ': 'ハ', 'ピ': 'ヒ', 'プ': 'フ', 'ペ': 'ヘ', 'ポ': 'ホ', 'ヤ': 'ャ', 'ユ': 'ュ', 'ヨ': 'ョ',
    'ャ': 'ヤ', 'ュ': 'ユ', 'ョ': 'ヨ', 'ワ': 'ヮ', 'ヮ': 'ワ'
  };
  var IMEHalfKatakanaCaseTable = {
    "ア": "ァ",  "イ": "ィ",  "ウ": "ゥ",  "エ": "ェ",  "オ": "ォ", "ァ": "ア",  "ィ": "イ",  "ゥ": "ヴ",  "ェ": "エ",  "ォ": "オ", "カ": "ガ", 
    "キ": "ギ", "ク": "グ", "ケ": "ゲ", "コ": "ゴ", "ガ": "カ", "ギ": "キ", "グ": "ク", "ゲ": "ケ", "ゴ": "コ", "サ": "ザ", "シ": "ジ",
    "ス": "ズ", "セ": "ゼ", "ソ": "ゾ", "ザ": "サ", "ジ": "シ", "ズ": "ス", "ゼ": "セ", "ゾ": "ソ", "タ": "ダ", "チ": "ヂ", "ツ": "ッ",
    "テ": "デ", "ト": "ド", "ダ": "タ", "ヂ": "チ", "ッ": "ヅ", "デ": "テ", "ド": "ト", "ヅ": "ツ", "ハ": "バ", "ヒ": "ビ", "フ": "ブ",
    "ヘ": "ベ", "ホ": "ボ", "バ": "パ","ビ": "ピ","ブ": "プ","ベ": "ペ","ボ": "ポ", "パ": "ハ", "ピ": "ヒ", "プ": "フ", "ペ": "ヘ",
    "ポ": "ホ", "ヤ": "ャ",  "ユ": "ュ",  "ヨ": "ョ", "ャ": "ヤ",  "ュ": "ユ",  "ョ": "ヨ", "ワ": "ワ", "ヴ": "ウ"
  };

Get key info accoring to previous key and current key If current key is the first element of a line and previous key is in the same line, the next key after previous key returns Otherwise, current key returns

  var getNextKeyInfo = function ime_getNextKeyInfo(prevK, currK) {
    var line = IMEHiraganaCycleTable[IMEKeyMap[currK]];
    var len = line.length; 
    var i;
    for (i=0; i < len; i++) {
      if (line[i] === prevK) {
        return [true, line[(i+1)%len]];
      }
    }
    return [false, currK];
  };

see getNextKeyInfo only get key in the reverse way

  var getPreviousKeyInfo = function ime_getPreviousKeyInfo(currK) {
    var i, j;
    var outer_len = IMEHiraganaCycleTable.length;
    var inner_len;
    var line;
    for (i=0; i < outer_len; i++) {
      line = IMEHiraganaCycleTable[i];
      inner_len = line.length;
      for (j=0; j < inner_len; j++) {
        if (line[j] === curr){
          return [true, line[(j+inner_len-1)%inner_len]];
        }
      }
    }

    return [false, ''];
  };

  var getPosInfoByChar = function ime_getPosInfoByChar(ch) {
    var i, j;
    var table = IMEHiraganaCycleTable;
    for (i = 0; i < table.length; i++) {
      for (j = 0; j < table[i].length; j++) {
        if (table[i][j] === ch) {
          return [i, j];
        }
      }
    }
    return [-1, -1];
  };

This is a simple Japanese IME implementation.

  var IMEngine = function ime() {

keep a local copy

    var self = this;

Glue contains some callback functions

    var _glue;

Query dict

    var _dict = null;

Enable IndexedDB

    var _enableIndexedDB = true;

Max length to predict

    var _dictMaxPredictLength = 20;

Input buffer NOTICE _inputBuf only contains Hiragana

    var _inputBuf = [];

Candidate list to be displayed candidate is [kanji, kana]

    var _candidateList = [];

First term info in candidate list used for transforming

    var _firstKanji = "";
    var _firstKana = "";

Previous selected term info used to generate suggestions

    var _selectedKanji = "";
    var _selectedKana = "";

Code of previous key pressed

    var _previousKeycode = 0;

Current keyboard

    var _currLayout = 'jp-kanji';

    var KeyMode = {
      'NORMAL': 0,
      'TRANSFORM': 1,
      'SELECT': 2,
      'H2K': 3
    };
    var _keyMode = KeyMode.NORMAL;

    var _keyboardMode = IMEMode.FULL_HIRAGANA;

* The following functions are compulsory functions in IME and explicitly called in keyboard.js *

    this.init = function ime_init(options) {

debug('init');

      _glue = options;
    };

    this.uninit = function ime_uninit() {
      if (_dict) {
        _dict.uninit();
        _dict = null;
      }
      self.empty();
    };

    this.click = function ime_click(keyCode) {

debug('click ' + keyCode); push keyCode to working queue

      qPush(keyCode);
      qNext();
    };

    this.select = function ime_select(kanji, kana) {
      debug('select ' + kanji + ' ' + kana);
      _glue.sendString(kanji);

      _inputBuf.splice(0, kana.length);
      _selectedKanji = kanji;
      _selectedKana = kana;
      _firstKana = "";
      _firstKanji = "";

reset

      _previousKeycode = 0;

      qPush(0);
      qNext();
    };

    this.empty = function ime_empty() {
      debug('empty buffer.');
      _inputBuf = [];
      _selectedKanji = "";
      _selectedKana = "";
      _keyMode = KeyMode.NORMAL;
      _keyboardMode = IMEMode.FULL_HIRAGANA;
      _previousKeycode = 0;

      sendPendingSymbols();
      _qIsWorking = false;
      if (!_dict) {
        initDB();
      }
    };

    this.show = function ime_show(inputType) {
      debug('Show. Input type: ' + inputType);
      var layout = 'jp-kanji';
      if (inputType === '' || inputType === 'text' || inputType === 'textarea') {
        layout = _currLayout;
      }
  
      _glue.alterKeyboard(layout);      
    };


    /** BEGIN QUEUE **/

A queue contains all keys pressed

    var _keyQueue = [];
    var _qIsWorking = false;

    var qPush = function queue_push(code) {
        _keyQueue.push(code);
    };

Start to pop key code from queue All logic is in handleSpecialKey and handleNormalKey

    var qNext = function queue_next() {

      debug('start queue working');

      if (_qIsWorking) {
        debug('queue is working. wait');
        return;
      }

      _qIsWorking = true;

      if (!_dict) {
        debug('DB has not initialized, defer processing.');
        initDB(qNext.bind(self));
        return;
      }

      if (!_keyQueue.length) {
        debug('queue is empty');
        _qIsWorking = false;
        return;
      }

pop key code from queue

      var code = _keyQueue.shift();
      debug('queue pops key ' + String.fromCharCode(code));

      if (handleSpecialKey(code) || handleNormalKey(code)) {
      }

FIXME do we need this? pass the key to IMEManager for default action and run qNext again before exiting _glue.sendKey(code);

      _qIsWorking = false;
      qNext();
    };
    /** END QUEUE **/

    var handleNormalKey = function ime_handleNormalKey(code) {
      var kana = String.fromCharCode(code);
      debug('handleNormalKey ' + kana);

set keymode

      _keyMode = KeyMode.NORMAL;

push kana directly if previoius key is NEXT

      if (_previousKeycode === IMESpecialKey.NEXT) {
        _previousKeycode = code;
        _inputBuf.push(kana);
        handleInputBuf();
        return 1;
      }
      

append new key code to the end of _inputBuf and begin to deal with the new buf

      var prevKana = _inputBuf[_inputBuf.length-1];
      var nextKeyInfo = getNextKeyInfo(prevKana, kana);

      if (nextKeyInfo[0]) {
        _inputBuf[_inputBuf.length-1] = nextKeyInfo[1];
      } else {
        _inputBuf.push(nextKeyInfo[1]);
      }

      handleInputBuf();
      _previousKeycode = code;
      return 1;
    };

    var handleSpecialKey = function ime_handleSpecialKey(code) {

      var immiReturn = true;

      switch (code) {

        case 0:

This is a select function operation.

          handleInputBuf();
          break;

cycle the IMEHiraganaCycleTable in reversal direction Ex. ['あ', 'い', 'う', 'え', 'お', 'ぁ', 'ぃ', 'ぅ', 'ぇ', 'ぉ'] is the cycle table of あ あ will be displayed when user click BACK while い is the current char

        case IMESpecialKey.BACK:
          var info = getPreviousKeyInfo(_inputBuf[_inputBuf.length-1]);
          if (info[0]) {
            _inputBuf[_inputBuf.length-1] = info[1];
          }
          handleInputBuf();
          break;

cursor postion previous

        case IMESpecialKey.PREVIOUS:
          if (_keyMode === KeyMode.NORMAL && _inputBuf.length > 0) {
            _keyMode = KeyMode.SELECT;
            handlePorN(_inputBuf);
            break;

          } else if (_keyMode === KeyMode.SELECT || _keyMode === KeyMode.TRANSFORM) {
            if (_firstKana.length === 1) {
              _keyMode = KeyMode.NORMAL;
              handleInputBuf();
              break;
            }
            handlePorN(_inputBuf.slice(0, _firstKana.length-1));
          }
          break;
        case IMESpecialKey.NEXT:
          if (_keyMode === KeyMode.NORMAL && _inputBuf.length > 0) {
            _keyMode = KeyMode.SELECT;
            handlePorN(_inputBuf.slice(0, 1));
            break;
          } else if (_keyMode === KeyMode.SELECT || _keyMode === KeyMode.TRANSFORM) {
            if (_firstKana.length === _inputBuf.length) {
              _keyMode = KeyMode.NORMAL;
              handleInputBuf();
              break;
            }
            handlePorN(_inputBuf.slice(0, _firstKana.length+1));
          }
          break;

Transform key

        case IMESpecialKey.TRANSFORM:
          if (_keyMode === KeyMode.TRANSFORM && _previousKeycode !== IMESpecialKey.TRANSFORM) {
            break;
          }
          _keyMode = KeyMode.TRANSFORM;
          handleTransform();
          break;

Hiragana, full-width katakana and half-width katakana convertor

        case IMESpecialKey.H2K:

          _keyMode = KeyMode.H2K;

          sendPendingSymbols();

          if (_keyboardMode === IMEMode.FULL_HIRAGANA) {
            _keyboardMode = IMEMode.FULL_KATAKANA;
          } else if (_keyboardMode === IMEMode.FULL_KATAKANA) {
            _keyboardMode = IMEMode.HALF_KATAKANA;
          } else if (_keyboardMode === IMEMode.HALF_KATAKANA) {
            _keyboardMode = IMEMode.FULL_HIRAGANA;
          }

          if (_keyboardMode === IMEMode.FULL_HIRAGANA) {
            queryDict();
          }

          updateCandidateList(qNext.bind(self));
          break;

大 <-> 小 TODO num and alpha exception

        case IMESpecialKey.CASE_CHANGE:
          var last = _inputBuf[_inputBuf.length-1];
          var res = HiraganaCaseTable[last];
          if (!res) {
            res = IMEFullKatakanaCaseTable[last];
          }
          if (!res) {
            res = IMEHalfKatakanaCaseTable[last];
            break;
          }
          if (!res) {
            break;
          }
          _inputBuf[_inputBuf.length-1] = res;
          handleInputBuf();
          break;
        

Switch to basic layout

        case IMESpecialKey.BASIC_LAYOUT:
          alterKeyboard('jp-kanji');
          break;
        

Switch to english layout

        case IMESpecialKey.EN_LAYOUT:
          alterKeyboard('jp-kanji-en');
          break;
        

Switch to number layout

        case IMESpecialKey.NUM_LAYOUT:
          alterKeyboard('jp-kanji-number');
          break;

Default key event

        case KeyEvent.DOM_VK_RETURN:
          handleReturn();
          break;

Default key event

        case KeyEvent.DOM_VK_BACK_SPACE:
          handleBackspace();
          break;
      
        default:
          immiReturn = false;
          break;
      }


      if (immiReturn) {
        _previousKeycode = code;
        return 1;
      }

      _keyMode = KeyMode.NORMAL;
      _keyboardMode = IMEMode.FULL_HIRAGANA;

Ignore key code less than 0 except those special code above

      if (code <= 0) {
        debug('ignore meaningless key code <= 0');
        return 1;
      }

      return 0;

    };

    var alterKeyboard = function ime_alterKeyboard(layout) {
      _currLayout = layout;
      self.empty();
      _glue.alterKeyboard(layout);
    };

    var handlePorN = function ime_handlePorN(kanaArr) {
      debug("handlePorN " + kanaArr);

      var __getTermsCallback1 = function handlePorN_getTermsCallback1(terms) {
        if (terms.length) {
          _firstKana = terms[0].kana;
          _firstKanji = terms[0].kanji;
        } else {
          _firstKana = SyllableUtils.arrayToString(kanaArr);
          _firstKanji = SyllableUtils.arrayToString(kanaArr);
        }
      };

      var __getTermsCallback2 = function handlePorN_getTermsCallback2(terms) {
        var candidates = [];

        terms.forEach(function readTerm(term) {
          candidates.push([term.kanji, term.kana]);
        });

        if (!candidates.length) {
          candidates.push([_firstKanji, _firstKana]);
        }

        _candidateList = candidates.slice();
        return;
      };

update _firstKana and _firstKanji

      _dict.getTerms(kanaArr, __getTermsCallback1);

      debug('firstTerm  ' + _firstKanji + ' ' + _firstKana);

Send pending symbols to highlight _firstKanji

      sendPendingSymbols();

get candidates by _firstKana

      _dict.getTerms(SyllableUtils.arrayFromString(_firstKana), __getTermsCallback2);

      updateCandidateList(qNext.bind(self));
    };

    var handleReturn = function ime_handleReturn() {
      if (_keyMode === KeyMode.TRANSFORM) {
        if (_firstKana === 0) {
          return;
        }

        debug('handle return in transform mode');

select first term

        _glue.sendString(_firstKanji);
        _inputBuf.splice(0, _firstKana.length);
        debug('_inputBuf is ' + JSON.stringify(_inputBuf));

query to generate first term

        queryDict(); 

do exactly like transforming

        handleTransform();
        return;

      } else if (_keyMode === KeyMode.H2K) {
        var mode = -1;
        if (_keyboardMode === IMEMode.FULL_HIRAGANA) {
          mode = IMEMode.HALF_KATAKANA;
        } else if (_keyboardMode === IMEMode.FULL_KATAKANA) {
          mode = IMEMode.FULL_HIRAGANA;
        } else if (_keyboardMode === IMEMode.HALF_KATAKANA) {
          mode = IMEMode.FULL_KATAKANA;
        }
        _glue.sendString(_getPossibleStrings(mode)[0]);
        _inputBuf = [];
        _candidateList = [];
        _keyboardMode = IMEMode.FULL_HIRAGANA;
        _keyMode = KeyMode.NORMAL;
        sendPendingSymbols();
        updateCandidateList(qNext.bind(self));

      } else {
        if (_firstKana.length > 0) {
          debug('first term ' + _firstKanji + ' ' + _firstKana);
          _glue.sendString(_firstKanji);
          _inputBuf.splice(0, _firstKana.length);
          handleInputBuf();

        } else {
          _glue.sendKey(KeyEvent.DOM_VK_RETURN);
        }
      }
    };

    var handleBackspace = function ime_handleBackspace() {
      debug('Backspace key');

      if (_inputBuf.length === 0) {
        _firstKana = "";
        _firstKanji = "";
        _candidateList = [];

        handleInputBuf();
        return;
      }

pass the key to IMEManager for default action

      _glue.sendKey(KeyEvent.DOM_VK_BACK_SPACE);

      _inputBuf.pop();
      handleInputBuf();
    };

    var handleTransform = function ime_handleTransform() {
      debug('handleTransform');

      if (!_inputBuf.length) {
        debug('empty input buf, return');
        handleInputBuf();
        return;
      }

Send pending symbols to highlight _firstKanji

      sendPendingSymbols();

      debug('firstTerm  ' + _firstKanji + " " + _firstKana);

get candidates by _firstKana

      var __getTermsCallback = function handleTransform_getTermsCallback(terms) {
        var candidates = [];

        terms.forEach(function readTerm(term) {
          candidates.push([term.kanji, term.kana]);
        });

        if (!candidates.length) {
          candidates.push([_firstKanji, _firstKana]);
        } else {
          _firstKanji = terms[0].kanji;
          _firstKana = terms[0].kana;
        }

        _candidateList = candidates.slice();
        return;
      };

only query _firstKana

      _dict.getTerms(SyllableUtils.arrayFromString(_firstKana), __getTermsCallback);

      updateCandidateList(qNext.bind(self));
    };

query, update pending symbols and candidate syllables these three processes normally conbind as one

    var handleInputBuf = function ime_handleInputBuf() {
      queryDict();
    };

Query dict, result will be used in sendPendingSymbols and updateCandidateList

    var queryDict = function ime_queryDict() {

      var candidates = [];

      if (_inputBuf.length === 0) {
        if (_selectedKana.length) {
          debug('Buffer is empty; ' +
            'make suggestions based on select term.' + _selectedKanji);

          var kana = _selectedKana;
          var kanji = _selectedKanji;
          _selectedKana = "";
          _selectedKanji = "";
          _candidateList = [];

TODO

          _dict.getSuggestions(kana, kanji,
            function(suggestions) {
              suggestions.forEach(
                function suggestions_forEach(suggestion) {
                  candidates.push(
                    [suggestion.kanji.substr(kanji.length),
                     kana]);
                }
              );
              _candidateList = candidates.slice();
            }
          );

          sendPendingSymbols();
          updateCandidateList(qNext.bind(self));
          return;
        }

        debug('Buffer is empty; send empty candidate list.');
        _firstKana = "";
        _firstKanji = "";
        _candidateList = [];
        sendPendingSymbols();
        updateCandidateList(qNext.bind(self));
        return;
      }

reset

      _selectedKanji = "";
      _selectedKana = "";

      debug('Get term candidates for the entire buffer.');

      var __getTermsCallback = function queryDict_getTermsCallback(terms) {
        debug('queryDict getTermsCallback');

        var kanaStr = SyllableUtils.arrayToString(_inputBuf);

        if (terms.length !== 0) {
          _firstKanji = terms[0].kanji;
          _firstKana = terms[0].kana;
        } else {
          _firstKanji = kanaStr;
          _firstKana = kanaStr;
        }

        terms.forEach(function readTerm(term) {
          candidates.push([term.kanji, term.kana]);
        });

only one kana in buf

        if (_inputBuf.length === 1) {
          debug('Only one kana; skip other lookups.');

          if (!candidates.length) {
            candidates.push([kanaStr, kanaStr]);
          }

          _candidateList = candidates.slice();
          sendPendingSymbols();
          updateCandidateList(qNext.bind(self));
          return;
        }

        var __getSentenceCallback = function getSentenceCallback(sentence) {

sentence = [term, term]

          debug('getSentenceCallback:' + JSON.stringify(sentence));

          _firstKanji = sentence[0].kanji;
          _firstKana = sentence[0].kana;

          var sentenceKana = '';
          var sentenceKanji = '';

          var i;

          for (i = 0; i < sentence.length; i++) {
            sentenceKanji += sentence[i].kanji;
            sentenceKana += sentence[i].kana;
          }

          var kanaStr = SyllableUtils.arrayToString(_inputBuf);

look for candidate that is already in the list

          var exists = candidates.some(function sentenceExists(candidate) {
            return (candidate[0] === sentenceKanji);
          });

          if (!exists) {
            candidates.push([sentenceKanji, sentenceKana]);
          }

The remaining candidates doesn't match the entire buffer these candidates helps user find the exact character/term s/he wants The remaining unmatched kanas will go through lookup over and over until the buffer is emptied.

          i = Math.min(_dictMaxPredictLength, _inputBuf.length - 1);

          var ___findTerms = function lookupFindTerms() {
            debug('Lookup for terms that matches first ' + i + ' kanas.');

            var subBuf = _inputBuf.slice(0, i);

            _dict.getTerms(subBuf, function lookupCallback(terms) {
              terms.forEach(function readTerm(term) {
                candidates.push([term.kanji, term.kana]);
              });

              if (i === 1 && !terms.length) {
                debug('The first kana does not make up a word,' +
                  ' output the symbol.');
                candidates.push([kanaStr, kanaStr]);
              }

              if (!--i) {
                debug('Done Looking.');
                _candidateList = candidates.slice();
                sendPendingSymbols();
                updateCandidateList(qNext.bind(self));
                return;
              }

              ___findTerms();
              return;
            });
          };

          ___findTerms();
        };

        debug('Lookup for sentences that make up from the entire buffer');

        _dict.getSentence(_inputBuf, __getSentenceCallback);
      };

      _dict.getTerms(_inputBuf, __getTermsCallback);

    };

    var _getPossibleStrings = function ime_getPossibleStrings(mode) {
      var table;
      if (mode === IMEMode.FULL_HIRAGANA) {
        table = KeyboardFullKatakanaCycleTable;
      } else if (mode === IMEMode.FULL_KATAKANA) {
        table = KeyboardHalfKatakanaCycleTable;
      } else if (mode === IMEMode.HALF_KATAKANA) {
        table = KeyboardHiraganaCycleTable;
      }

      var i;
      var strFullKatakana = '';
      var strHalfKatakana = '';
      var strFullHiragana = '';
      var displayStr = "";

      for (i = 0; i < _inputBuf.length; i++) {
        var info = getPosInfoByChar(_inputBuf[i]);
        debug('get info ' + info[0] + "  " + info[1]);
        if (info[0] === -1) {

FIXME see bug list 1

          return ['', '', '', ''];
        }
        displayStr += table[info[0]][info[1]];
        debug(displayStr);
        
        strFullHiragana += KeyboardHiraganaCycleTable[info[0]][info[1]];
        strFullKatakana += KeyboardFullKatakanaCycleTable[info[0]][info[1]];
        strHalfKatakana += KeyboardHalfKatakanaCycleTable[info[0]][info[1]];
      }

      return [displayStr, strFullHiragana, strFullKatakana, strHalfKatakana];
    };

    var getDisplayStr = function ime_getDisplayStr() {
      var displayStr = "";

      if (_keyMode === KeyMode.NORMAL) {
        displayStr = SyllableUtils.arrayToString(_inputBuf);

      } else if (_keyMode === KeyMode.TRANSFORM) {

        if (_firstKanji.length === 0) {
          _keyMode = KeyMode.NORMAL;

        } else {
          displayStr = "<span style='background:#3333aa'>" + _firstKanji + "</span>";
          displayStr += SyllableUtils.arrayToString(_inputBuf).substr(_firstKana.length);
        }

      } else if (_keyMode === KeyMode.SELECT) {

        if (_firstKanji.length === 0) {
          _keyMode = KeyMode.NORMAL;

        } else {
          displayStr = "<span style='background:#33aa33'>" + _firstKanji + "</span>";
          displayStr += SyllableUtils.arrayToString(_inputBuf).substr(_firstKana.length);
        }

      } else if (_keyMode === KeyMode.H2K) {

        var strs = _getPossibleStrings(_keyboardMode);
        var candidates = [];
        displayStr = strs[0];

candidate list is updated here to avoide loop again in handleInputBuf TODO reorder these three candidates

        candidates.push([SyllableUtils.arrayToString(_inputBuf), SyllableUtils.arrayToString(_inputBuf)]);
        candidates.push([strs[2], SyllableUtils.arrayToString(_inputBuf)]);
        candidates.push([strs[3], SyllableUtils.arrayToString(_inputBuf)]);

        _candidateList = candidates.slice();
      }

      return displayStr;

    };

Send pending symbols to display

    var sendPendingSymbols = function ime_sendPendingSymbols() {

      var displayStr = getDisplayStr();
      debug('sending pending symbols: ' + displayStr);

      _glue.sendPendingSymbols(displayStr);
    };

Update candidate list

    var updateCandidateList = function ime_updateCandidateList(callback) {
      debug('update candidate list');

      sendCandidates(_candidateList);
      _glue.sendCandidates(candidates);
      callback();
    };

Get json and init indexedDB if possible

    var initDB = function ime_initDB(readyCallback) {
      var dbSettings = {
        enableIndexedDB: _enableIndexedDB
      };

      if (readyCallback) {
        dbSettings.ready = readyCallback;
      }

      var jsonUrl = _glue.path + '/dict.json';
      _dict = new IMEngineDatabase('jskanji', jsonUrl);
      _dict.init(dbSettings);
    };

  };

  var jskanji = new IMEngine(self);

Expose as an AMD module

  if (typeof define === 'function' && define.amd) {
    define('jskanji', [], function() { return jskanji; });
  }

Expose to IMEManager if we are in Gaia homescreen

  if (typeof IMEManager !== 'undefined') {
    IMEManager.IMEngines.jskanji = jskanji;
  }

  /* copy from jszhuyin */
  var debugging = true;
  var debug = function(str) {
    if (!debugging) {
      return;
    }

    if (window.dump) {
      window.dump('JP: ' + str + '\n');
    }

    if (console && console.log) {
      console.log('JP: ' + str);
      if (arguments.length > 1) {
        console.log.apply(this, arguments);
      }
    }
  };

for non-Mozilla browsers

  if (!KeyEvent) {
    var KeyEvent = {
      DOM_VK_BACK_SPACE: 0x8,
      DOM_VK_RETURN: 0xd
    };
  }
  /* end copy */

  var SyllableUtils = {
    /**
     * Converts a syllables array to a string.
     * For example, ['わ', 'た', 'し'] will be converted to 'わたし'.
     */
    arrayToString: function syllableUtils_arrayToString(array) {
      return array.join('');
    },

    /**
     * Converts a syllables string to an array.
     * For example, 'わたし' will be converted to ['わ', 'た', 'し'].
     */
    arrayFromString: function syllableUtils_arrayFromString(str) {
      return str.split('');
    }
  };

  var Term = function term_constructor(kana, freq, kanji) {
    this.kana = kana;
    this.freq = freq;
    this.kanji = kanji;
  };

  Term.prototype = {
    /*The actual string of the term */
    kana: '',
    /* The frequency of the term*/
    freq: 0,
    kanji: ''
  };

  /**
   * Terms with same kana
   */
  var Homonyms = function homonyms_constructor(kana, terms) {
    this.kana = kana;

Clone a new array

    this.terms = terms.concat();
  };

  Homonyms.prototype = {
    kana: '',

Terms array

    terms: null
  };

  /**
   * An index class to speed up the search operation for ojbect array.
   * @param {Array} targetArray The array to be indexed.
   * @param {String} keyPath The key path for the index to use.
   */
  var Index = function index_constructor(targetArray, keyPath) {
    this._keyMap = {};
    this._sortedKeys = [];
    for (var i = 0; i < targetArray.length; i++) {
      var key = targetArray[i][keyPath];
      if (!(key in this._keyMap)) {
        this._keyMap[key] = [];
        this._sortedKeys.push(key);
      }
      this._keyMap[key].push(i);
    }
    this._sortedKeys.sort();
  };

  Index.prototype = {

Map the key to the index of the storage array

    _keyMap: null,

Keys array in ascending order.

    _sortedKeys: null,

    /**
     * Get array indices by given key.
     * @return {Array} An array of index.
     */
    get: function index_get(key) {
      var indices = [];
      if (key in this._keyMap) {
        indices = indices.concat(this._keyMap[key]);
      }
      return indices;
    },

    /**
     * Get array indices by given key range.
     * @param {String} lower The lower bound of the key range. If null, the range
     * has no lower bound.
     * @param {String} upper The upper bound of the key range. If null, the range
     * has no upper bound.
     * @param {Boolean} lowerOpen If false, the range includes the lower bound
     * value. If the range has no lower bound, it will be ignored.
     * @param {Boolean} upperOpen If false, the range includes the upper bound
     * value. If the range has no upper bound, it will be ignored.
     * @return {Array} An array of index.
     */
    getRange: function index_getRange(lower, upper, lowerOpen, upperOpen) {
      var indices = [];
      if (this._sortedKeys.length == 0) {
        return indices;
      }

      var pos = 0;

lower bound position

      var lowerPos = 0;

uppder bound position

      var upperPos = this._sortedKeys.length - 1;

      if (lower) {
        pos = this._binarySearch(lower, 0, upperPos);
        if (pos == Infinity) {
          return indices;
        }
        if (pos != -Infinity) {
          lowerPos = Math.ceil(pos);
        }
        if (lowerOpen && this._sortedKeys[lowerPos] == lower) {
          lowerPos++;
        }
      }

      if (upper) {
        pos = this._binarySearch(upper, lowerPos, upperPos);
        if (pos == -Infinity) {
          return indices;
        }
        if (pos != Infinity) {
          upperPos = Math.floor(pos);
        }
        if (upperOpen && this._sortedKeys[upperPos] == upper) {
          upperPos--;
        }
      }

      for (var i = lowerPos; i <= upperPos; i++) {
        var key = this._sortedKeys[i];
        indices = indices.concat(this._keyMap[key]);
      }
      return indices;
    },

    /**
     * Search the key position.
     * @param {String} key The key to search.
     * @param {Number} left The begin position of the array. It should be less
     * than the right parameter.
     * @param {Number} right The end position of the array.It should be greater
     * than the left parameter.
     * @return {Number} If success, returns the index of the key.
     * If the key is between two adjacent keys, returns the average index of the
     * two keys. If the key is out of bounds, returns Infinity or -Infinity.
     */
    _binarySearch: function index_binarySearch(key, left, right) {
      if (key < this._sortedKeys[left]) {
        return -Infinity;
      }
      if (key > this._sortedKeys[right]) {
        return Infinity;
      }

      while (right > left) {
        var mid = Math.floor((left + right) / 2);
        var midKey = this._sortedKeys[mid];
        if (midKey < key) {
          left = mid + 1;
        } else if (midKey > key) {
          right = mid - 1;
        } else {
          return mid;
        }
      }

left == right == mid

      var leftKey = this._sortedKeys[left];
      if (leftKey == key) {
        return left;
      } else if (leftKey < key) {
        return left + 0.5;
      } else {
        return left - 0.5;
      }
    }
  };

  var Task = function task_constructor(taskFunc, taskData) {
    this.func = taskFunc;
    this.data = taskData;
  };

  Task.prototype = {
    /**
     * Task function
     */
    func: null,
    /**
     * Task private data
     */
    data: null
  };

  var TaskQueue = function taskQueue_constructor(oncomplete) {
    this.oncomplete = oncomplete;
    this._queue = [];
    this.data = {};
  };

  TaskQueue.prototype = {
    /**
     * Callback Javascript function object that is called when the task queue is
     * empty. The definition of callback is function oncomplete(queueData).
     */
    oncomplete: null,

    /**
     * Data sharing with all tasks of the queue
     */
    data: null,

    /**
     * Task queue array.
     */
    _queue: null,

    /**
     * Add a new task to the tail of the queue.
     * @param {Function} taskFunc Task function object. The definition is function
     * taskFunc(taskQueue, taskData).
     * The taskQueue parameter is the task queue object itself, while the taskData
     * parameter is the data property
     * of the task queue object.
     * @param {Object} taskData The task's private data.
     */
    push: function taskQueue_push(taskFunc, taskData) {
      this._queue.push(new Task(taskFunc, taskData));
    },

    /**
     * Start running the task queue or process the next task.
     * It should be called when a task, including the last one, is finished.
     */
    processNext: function taskQueue_processNext() {
      if (this._queue.length > 0) {
        var task = this._queue.shift();
        if (typeof task.func == 'function') {
          task.func(this, task.data);
        } else {
          this.processNext();
        }
      } else {
        if (typeof this.oncomplete == 'function') {
          this.oncomplete(this.data);
        }
      }
    },

    /**
     * Get the number of remaining tasks.
     */
    getSize: function taskQueue_getSize() {
      return this._queue.length;
    }
  };

  var DatabaseStorageBase = function storagebase_constructor() {
  };

  /**
   * DatabaseStorageBase status code enumeration.
   */
  DatabaseStorageBase.StatusCode = {
    /* The storage isn't initilized.*/
    UNINITIALIZED: 0,
    /* The storage is busy.*/
    BUSY: 1,
    /* The storage has been successfully initilized and is ready to use.*/
    READY: 2,
    /* The storage is failed to initilized and cannot be used.*/
    ERROR: 3
  };

  DatabaseStorageBase.prototype = {
    _status: DatabaseStorageBase.StatusCode.UNINITIALIZED,

    /**
     * Get the status code of the storage.
     * @return {DatabaseStorageBase.StatusCode} The status code.
     */
    getStatus: function storagebase_getStatus() {
      return this._status;
    },

    /**
     * Whether the database is ready to use.
     */
    isReady: function storagebase_isReady() {
      return this._status == DatabaseStorageBase.StatusCode.READY;
    },

    /**
     * Initialization.
     * @param {Function} callback Javascript function object that is called when
     * the operation is finished. The definition of callback is
     * function callback(statusCode). The statusCode parameter is of type
     * DatabaseStorageBase.StatusCode that stores the status of the storage
     * after Initialization.
     */
    init: function storagebase_init(callback) {
    },

    /**
     * Destruction.
     * @param {Function} callback Javascript function object that is called when
     * the operation is finished.
     * The definition of callback is function callback().
     */
    uninit: function storagebase_uninit(callback) {
    },


    /**
     * Whether the storage is empty.
     * @return {Boolean} true if the storage is empty; otherwise false.
     */
    isEmpty: function storagebase_isEmpty() {
    },

    /**
     * Get all terms.
     * @param {Function} callback Javascript function object that is called when
     * the operation is finished. The definition of callback is
     * function callback(homonymsArray). The homonymsArray parameter is an array
     * of Homonyms objects.
     */
    getAllTerms: function storagebase_getAllTerms(callback) {
    },

    /**
     * Set all the terms of the storage.
     * @param {Array} homonymsArray The array of Homonyms objects containing all
     * the terms.
     * @param {Function} callback Javascript function object that is called when
     * the operation is finished. The definition of callback is
     * function callback().
     */
    setAllTerms: function storagebase_setAllTerms(homonymsArray, callback) {
    },

    /**
     * Get iterm with given syllables string.
     * @param {String} syllablesStr The syllables string of the matched terms.
     * @param {Function} callback Javascript function object that is called when
     * the operation is finished. The definition of callback is
     * function callback(homonymsArray). The homonymsArray parameter is an array
     * of Homonyms objects.
     */
    getTermsByKana: function storagebase_getTermsByKana(
                             syllablesStr, callback) {
    },

    /**
     * Get iterms with given syllables string prefix.
     * @param {String} prefix The prefix of the syllables string .
     * @param {Function} callback Javascript function object that is called when
     * the operation is finished. The definition of callback is
     * function callback(homonymsArray). The homonymsArray parameter is an array
     * of Homonyms objects.
     */
    getTermsByKanaPrefix: function storagebase_getTermsByKanaPrefix(
                              prefix, callback) {
    },

    /**
     * Add a term to the storage.
     * @param {String} syllablesStr The syllables string of the term.
     * @param {Term} term The Term object of the term.
     * @param {Function} callback Javascript function object that is called when
     * the operation is finished. The definition of callback is
     * function callback().
     */
    addTerm: function storagebase_addTerm(syllablesStr, term, callback) {
    },

    /**
     * Remove a term from the storage.
     * @param {String} syllablesStr The syllables string of the term.
     * @param {Term} term The Term object of the term.
     * @param {Function} callback Javascript function object that is called when
     * the operation is finished. The definition of callback is
     * function callback().
     */
    removeTerm: function storagebase_removeTerm(syllablesStr, term, callback) {
    }
  };

  var JsonStorage = function jsonStorage_construtor(jsonUrl) {
    this._jsonUrl = jsonUrl;
    this._dataArray = [];
  };

  JsonStorage.prototype = {

Inherits DatabaseStorageBase

    __proto__: new DatabaseStorageBase(),

    _dataArray: null,

The JSON file url.

    _jsonUrl: null,

    _kanaIndex: null,

    _abrreviatedIndex: null,

    init: function jsonStorage_init(callback) {
      var self = this;
      var doCallback = function init_doCallback() {
        if (callback) {
          callback(self._status);
        }
      }

Check if we could initilize.

      if (this._status != DatabaseStorageBase.StatusCode.UNINITIALIZED) {
        doCallback();
        return;
      }

Set the status to busy.

      this._status = DatabaseStorageBase.StatusCode.BUSY;

      var xhr = new XMLHttpRequest();
      xhr.open('GET', this._jsonUrl, true);
      try {
        xhr.responseType = 'json';
      } catch (e) { }
      xhr.overrideMimeType('application/json; charset=utf-8');
      xhr.onreadystatechange = function xhrReadystatechange(ev) {
        if (xhr.readyState !== 4) {
          self._status = DatabaseStorageBase.StatusCode.ERROR;
          return;
        }

        var response;
        if (xhr.responseType == 'json') {
          try {

clone everything under response because it's readonly.

            self._dataArray = xhr.response.slice();
          } catch (e) {
          }
        }

        if (typeof self._dataArray !== 'object') {
          self._status = DatabaseStorageBase.StatusCode.ERROR;
          doCallback();
          return;
        }

        xhr = null;
        setTimeout(performBuildIndices, 100);
      };

      var performBuildIndices = function init_performBuildIndices() {
        self._buildIndices();
        self._status = DatabaseStorageBase.StatusCode.READY;
        doCallback();
      };

      xhr.send(null);
    },

    uninit: function jsonStorage_uninit(callback) {
      var doCallback = function uninit_doCallback() {
        if (callback) {
          callback();
        }
      }

Check if we could uninitilize the storage

      if (this._status == DatabaseStorageBase.StatusCode.UNINITIALIZED) {
        doCallback();
        return;
      }

Perform destruction operation

      this._dataArray = [];

      this._status = DatabaseStorageBase.StatusCode.UNINITIALIZED;
      doCallback();
    },

    isEmpty: function jsonStorage_isEmpty() {
      return this._dataArray.length == 0;
    },

    getAllTerms: function jsonStorage_getAllTerms(callback) {
      var self = this;
      var homonymsArray = [];
      var doCallback = function getAllTerms_doCallback() {
        if (callback) {
          callback(homonymsArray);
        }
      }

Check if the storage is ready.

      if (!this.isReady()) {
        doCallback();
        return;
      }

      var perform = function getAllTerms_perform() {

Query all terms

        homonymsArray = homonymsArray.concat(self._dataArray);
        doCallback();
      }

      setTimeout(perform, 0);
    },

    getTermsByKana: function jsonStorage_getTermsByKana(kanaStr, callback) {
      var self = this;
      var homonymsArray = [];
      var doCallback = function getTermsByKana_doCallback() {
        if (callback) {
          callback(homonymsArray);
        }
      }

Check if the storage is ready.

      if (!this.isReady()) {
        doCallback();
        return;
      }

      var perform = function getTermsByKana_perform() {
        var indices = self._kanaIndex.get(kanaStr);
        for (var i = 0; i < indices.length; i++) {
          var index = indices[i];
          homonymsArray.push(self._dataArray[index]);
        }
        doCallback();
      }

      setTimeout(perform, 0);
    },

   getTermsByKanaPrefix: function
     jsonStorage_getTermsByKanaPrefix(prefix, callback) {
       var self = this;
       var homonymsArray = [];
       function doCallback() {
         if (callback) {
           callback(homonymsArray);
         }
       }

Check if the storage is ready.

       if (!this.isReady()) {
         doCallback();
         return;
       }

       var perform = function() {
         var upperBound = prefix.substr(0, prefix.length - 1) +
           String.fromCharCode(prefix.substr(prefix.length - 1).charCodeAt(0) + 1);
         var indices =
           self._kanaIndex.getRange(prefix, upperBound, false, false);
         for (var i = 0; i < indices.length; i++) {
           var index = indices[i];
           homonymsArray.push(self._dataArray[index]);
         }
         doCallback();
       }

       setTimeout(perform, 0);
     },

   _buildIndices: function jsonStorage_buildIndices() {
     this._kanaIndex = new Index(this._dataArray, 'kana');
   }
  };

Interfaces of indexedDB

  var IndexedDB = {
    indexedDB: window.indexedDB || window.webkitIndexedDB ||
      window.mozIndexedDB || window.msIndexedDB,

    IDBDatabase: window.IDBDatabase || window.webkitIDBDatabase ||
      window.msIDBDatabase,

    IDBIndex: window.IDBIndex || window.webkitIDBIndex || window.msIDBIndex,

Check if the indexedDB is available on this platform

    isReady: function indexedDB_isReady() {
      if (!this.indexedDB || // No IndexedDB API implementation
        this.IDBDatabase.prototype.setVersion || // old version of IndexedDB API
        window.location.protocol === 'file:') {  // bug 643318

        debug('IndexedDB is not available on this platform.');
        return false;
      }

      return true;
    }

  };
  /* end IndexedDB */

  var IndexedDBStorage = function indexedDBStorage_constructor(dbName) {
    this._dbName = dbName;
  };

  IndexedDBStorage.kDBVersion = 1.0;

  IndexedDBStorage.prototype = {

Inherits DatabaseStorageBase

    __proto__: new DatabaseStorageBase(),

Database name

    _dbName: null,

IDBDatabase interface

    _IDBDatabase: null,

    _count: 0,

    init: function indexedDBStorage_init(callback) {
      var self = this;
      function doCallback() {
        if (callback) {
          callback(self._status);
        }
      }

Check if we could initilize.

      if (IndexedDB.isReady() &&
          this._status != DatabaseStorageBase.StatusCode.UNINITIALIZED) {
            doCallback();
            return;
          }

Set the status to busy.

      this._status = DatabaseStorageBase.StatusCode.BUSY;

Open the database

      var req = IndexedDB.indexedDB.open(this._dbName,
          IndexedDBStorage.kDBVersion);
      req.onerror = function dbopenError(ev) {
        debug('Encounter error while opening IndexedDB.');
        self._status = DatabaseStorageBase.StatusCode.ERROR;
        doCallback();
      };

      req.onupgradeneeded = function dbopenUpgradeneeded(ev) {
        debug('IndexedDB upgradeneeded.');
        self._IDBDatabase = ev.target.result;

delete the old ObjectStore if present

        if (self._IDBDatabase.objectStoreNames.length !== 0) {
          self._IDBDatabase.deleteObjectStore('homonyms');
        }

create ObjectStore

        var store = self._IDBDatabase.createObjectStore('homonyms',
            { keyPath: 'kana' });

no callback() here onupgradeneeded will follow by onsuccess event

      };

      req.onsuccess = function dbopenSuccess(ev) {
        debug('IndexedDB opened.');
        self._IDBDatabase = ev.target.result;

        self._status = DatabaseStorageBase.StatusCode.READY;
        self._count = 0;

Check the integrity of the storage

        self.getTermsByKana('_last_entry_',
          function getLastEntryCallback(homonymsArray) {
            if (homonymsArray.length === 0) {
              debug('IndexedDB is broken.');

Could not find the 'lastentry_' element. The storage is broken and ignore all the data.

              doCallback();
              return;
            }

            var transaction = self._IDBDatabase.transaction(['homonyms'], 'readonly');

Get the count

            var reqCount = transaction.objectStore('homonyms').count();

            reqCount.onsuccess = function(ev) {
              debug('IndexedDB count: ' + ev.target.result);
              self._count = ev.target.result - 1;
              self._status = DatabaseStorageBase.StatusCode.READY;
              doCallback();
            };

            reqCount.onerror = function(ev) {
              self._status = DatabaseStorageBase.StatusCode.ERROR;
              doCallback();
            };
          });
      };
    },

    uninit: function indexedDBStorage_uninit(callback) {
      function doCallback() {
        if (callback) {
          callback();
        }
      }

Check if we could uninitilize the storage

      if (this._status == DatabaseStorageBase.StatusCode.UNINITIALIZED) {
        doCallback();
        return;
      }

Perform destruction operation

      if (this._IDBDatabase) {
        this._IDBDatabase.close();
      }

      this._status = DatabaseStorageBase.StatusCode.UNINITIALIZED;
      doCallback();
    },

    isEmpty: function indexedDBStorage_isEmpty() {
      return this._count == 0;
    },

    getAllTerms: function indexedDBStorage_getAllTerms(callback) {
      var homonymsArray = [];
      function doCallback() {
        if (callback) {
          callback(homonymsArray);
        }
      }

Check if the storage is ready.

      if (!this.isReady()) {
        doCallback();
        return;
      }

Query all terms

      var store = this._IDBDatabase.transaction(['homonyms'], 'readonly')
        .objectStore('homonyms');
      var req = store.openCursor();

      req.onerror = function(ev) {
        debug('Database read error.');
        doCallback();
      };
      req.onsuccess = function(ev) {
        var cursor = ev.target.result;
        if (cursor) {
          var homonyms = cursor.value;
          if (homonyms.kana != '_last_entry_') {
            homonymsArray.push(homonyms);
          }
          cursor.continue();
        } else {
          doCallback();
        }
      };
    },

    setAllTerms: function indexedDBStorage_setAllTerms(homonymsArray, callback) {
      var self = this;
      function doCallback() {
        self._status = DatabaseStorageBase.StatusCode.READY;
        if (callback) {
          callback();
        }
      }

      var n = homonymsArray.length;

Check if the storage is ready.

      if (!this.isReady() || n == 0) {
        doCallback();
        return;
      }

Set the status to busy.

      this._status = DatabaseStorageBase.StatusCode.BUSY;

Use task queue to add the terms by batch to prevent blocking the main thread.

      var taskQueue = new TaskQueue(
          function taskQueueOnCompleteCallback(queueData) {
            self._count = n;
            doCallback();
          });

      var processNextWithDelay = function setAllTerms_rocessNextWithDelay() {
        setTimeout(function nextTask() {
          taskQueue.processNext();
        }, 0);
      };

Clear all the terms before adding

      var clearAll = function setAllTerms_clearAll(taskQueue, taskData) {
        var transaction =
          self._IDBDatabase.transaction(['homonyms'], 'readwrite');
        var store = transaction.objectStore('homonyms');
        var req = store.clear();
        req.onsuccess = function(ev) {
          debug('IndexedDB cleared.');
          processNextWithDelay();
        };

        req.onerror = function(ev) {
          debug('Failed to clear IndexedDB.');
          self._status = DatabaseStorageBase.StatusCode.ERROR;
          doCallback();
        };

      };

Add a batch of terms

      var addChunk = function setAllTerms_addChunk(taskQueue, taskData) {
        var transaction =
          self._IDBDatabase.transaction(['homonyms'], 'readwrite');
        var store = transaction.objectStore('homonyms');
        transaction.onerror = function(ev) {
          debug('Database write error.');
          doCallback();
        };

        transaction.oncomplete = function() {
          processNextWithDelay();
        };

        var begin = taskData.begin;
        var end = taskData.end;
        for (var i = begin; i <= end; i++) {
          var homonyms = homonymsArray[i];
          store.put(homonyms);
        }

Add a special element to indicate that all the items are saved.

        if (end === n - 1) {
          debug('=======================');
          store.put(new Homonyms('_last_entry_', []));

throw('jdkdkdjd');

        }
      };

      taskQueue.push(clearAll, null);

      for (var begin = 0; begin < n; begin += 2000) {
        var end = Math.min(begin + 1999, n - 1);
        taskQueue.push(addChunk, {begin: begin, end: end});
      }

      processNextWithDelay();
    },

    getTermsByKana: function indexedDBStorage_getTermsByKana(syllablesStr, callback) {
      var homonymsArray = [];
      function doCallback() {
        if (callback) {
          callback(homonymsArray);
        }
      }

Check if the storage is ready.

      if (!this.isReady()) {
        doCallback();
        return;
      }

      var store = this._IDBDatabase.transaction(['homonyms'], 'readonly')
        .objectStore('homonyms');
      var req = store.get(syllablesStr);

      req.onerror = function(ev) {
        debug('Database read error.');
        doCallback();
      };

      req.onsuccess = function(ev) {
        var homonyms = ev.target.result;
        if (homonyms) {
          homonymsArray.push(homonyms);
        }
        doCallback();
      };
    },
    /* getTermsByKana */

    getTermsByKanaPrefix: function indexedDBStorage_getTermsByKanaPrefix(
                              prefix, callback) {
      var homonymsArray = [];
      function doCallback() {
        if (callback) {
          callback(homonymsArray);
        }
      }

Check if the storage is ready.

      if (!this.isReady()) {
        doCallback();
        return;
      }

      var upperBound = prefix.substr(0, prefix.length - 1) +
        String.fromCharCode(prefix.substr(prefix.length - 1).charCodeAt(0) + 1);

      var store = this._IDBDatabase.transaction(['homonyms'], 'readonly')
        .objectStore('homonyms');
      var req =
        store.openCursor(IDBKeyRange.bound(prefix, upperBound, true, true));

      req.onerror = function(ev) {
        debug('Database read error.');
        doCallback();
      };
      req.onsuccess = function(ev) {
        var cursor = ev.target.result;
        if (cursor) {
          var homonyms = cursor.value;
          homonymsArray.push(homonyms);
          cursor.continue();
        } else {
          doCallback();
        }
      };
    }
  };

* IMEngineDatabase is exposed to IME * It contains both instances of jsonStorage and indexedDBStorage Query processes in indexedDBStorage if possible, otherwise in jsonStorage

  var IMEngineDatabase = function imedb(dbName, jsonUrl) {
    var settings;

Dictionary words' total frequency.

    var kDictTotalFreq = 1.0e8;

    var jsonStorage = new JsonStorage(jsonUrl);
    var indexedDBStorage = new IndexedDBStorage(dbName);

    var iDBCache = {};
    var cacheTimer;
    var kCacheTimeout = 10000;

    var self = this;

    /* ==== init functions ==== */
    var populateDBFromJSON = function imedb_populateDBFromJSON(callback) {
      jsonStorage.getAllTerms(function getAllTermsCallback(homonymsArray) {
        indexedDBStorage.setAllTerms(homonymsArray, callback);
      });
    };

    /* ==== helper functions ==== */

    /*
     * Data from IndexedDB gets to kept in iDBCache for kCacheTimeout seconds
     */
    var cacheSetTimeout = function imedb_cacheSetTimeout() {
      debug('Set iDBCache timeout.');
      clearTimeout(cacheTimer);
      cacheTimer = setTimeout(function imedb_cacheTimeout() {
        debug('Empty iDBCache.');
        iDBCache = {};
      }, kCacheTimeout);
    };

    /* ==== init ==== */
    this.init = function imedb_init(options) {
      settings = options;

      var ready = function() {
        debug('Ready.');
        if (settings.ready)
          settings.ready();
      };

      if (!settings.enableIndexedDB) {
        debug('IndexedDB disabled; Downloading JSON ...');
        jsonStorage.init(ready);
        return;
      }

      debug('Probing IndexedDB ...');
      indexedDBStorage.init(function indexedDBStorageInitCallback() {
        if (!indexedDBStorage.isReady()) {
          debug('IndexedDB not available; Downloading JSON ...');
          jsonStorage.init(ready);
          return;
        }
        ready();
        if (indexedDBStorage.isEmpty()) {
          jsonStorage.init(function jsonStorageInitCallback() {
            if (!jsonStorage.isReady()) {
              debug('JSON failed to download.');
              return;
            }

            debug(
              'JSON loaded,' +
              'IME is ready to use while inserting data into db ...'
              );
            populateDBFromJSON(function populateDBFromJSONCallback() {
              if (!indexedDBStorage.isEmpty()) {
                debug('IndexedDB ready and switched to indexedDB backend.');
                jsonStorage.uninit();
              } else {
                debug('Failed to populate IndexedDB from JSON.');
              }
            });
          });
        }
      });
    };

    /* ==== uninit ==== */
    this.uninit = function imedb_uninit() {
      indexedDBStorage.uninit();
      jsonStorage.uninit();
    };

return instance of indexedDBStorage if possible otherwise jsonStorage null if neither is not available

    var _getUsableStorage = function imedb__getUsableStorage() {
      if (settings.enableIndexedDB &&
          indexedDBStorage.isReady() &&
          !indexedDBStorage.isEmpty()) {
            return indexedDBStorage;
          } else if (jsonStorage.isReady() && !jsonStorage.isEmpty()) {
            return jsonStorage;
          } else {
            return null;
          }
    };

    /* ==== db lookup functions ==== */

syllables kana textStr kanji callback Function

    this.getSuggestions = function imedb_getSuggestions(kanaStr, kanjiStr, callback) {
      debug('getSuggestions ' + kanjiStr + ' ' + kanaStr);
      var storage = _getUsableStorage();
      if (!storage) {
        debug('Database not ready.');
        callback([]);
        return;
      }

      var result = [];

      var _matchTerm = function getSuggestions_matchTerm(term) {
        if (term.kanji.substr(0, kanjiStr.length) !== kanjiStr)
          return;
        if (term.kanji === kanjiStr)
          return;
        result.push(term);
      };

      var _processResult = function getSuggestions_processResult(r) {

r = r.sort( function getSuggestions_sort(a, b) { return (b.freq - a.freq); } );

        var result = [];
        var t = [];
        r.forEach(function terms_foreach(term) {
          if (t.indexOf(term.kanji) !== -1) return;
          t.push(term.kanji);
          result.push(term);
        });
        return result;
      };

      var result = [];

      debug('Get suggestion for ' + kanjiStr + '.');

      if (typeof iDBCache['SUGGESTION:' + kanjiStr] !== 'undefined') {
        debug('Found in iDBCache.');
        cacheSetTimeout();
        callback(iDBCache['SUGGESTION:' + kanjiStr]);
        return;
      }

by prefix

      storage.getTermsByKanaPrefix(kanaStr,
        function getTermsByKanaPrefix_callback(homonymsArray) {

        for (var i = 0; i < homonymsArray.length; i++) {
          var homonyms = homonymsArray[i];
          homonyms.terms.forEach(_matchTerm);
        }
        if (result.length) {
          result = _processResult(result);
        } else {
          result = [];
        }
        cacheSetTimeout();
        iDBCache['SUGGESTION:' + kanjiStr] = result;
        callback(result);
      });

    },
    /* end getSuggestions */

    this.getTerms = function imedb_getTerms(kanaArr, callback) {

      var storage = _getUsableStorage();
      if (!storage) {
        debug('Database not ready.');
        callback([]);
        return;
      }

      var kanaStr = SyllableUtils.arrayToString(kanaArr);
      debug('Get terms for ' + kanaStr + '.');

      var _processResult = function processResult(r, limit) {

r = r.sort( function sort_result(a, b) { return (b.freq - a.freq); } );

        var result = [];
        var t = [];
        r.forEach(function(term) {
          if (t.indexOf(term.kanji) !== -1) return;
          t.push(term.kanji);
          result.push(term);
        });
        if (limit > 0) {
          result = result.slice(0, limit);
        }
        return result;
      };

      if (typeof iDBCache[kanaStr] !== 'undefined') {
        debug('Found in iDBCache.');
        cacheSetTimeout();
        debug("cache " + JSON.stringify(iDBCache[kanaStr]));
        callback(iDBCache[kanaStr]);
        return;
      }

      storage.getTermsByKana(kanaStr,
        function(homonymsArray) {
          var result = [];
          for (var i = 0; i < homonymsArray.length; i++) {
            var homonyms = homonymsArray[i];
            result = result.concat(homonyms.terms);
          }
          if (result.length) {
            result = _processResult(result, -1);
          } else {
            result = [];
          }
          debug('Get terms getTermsByKana homonymsArray ' + JSON.stringify(result));
          cacheSetTimeout();
          iDBCache[kanaStr] = result;
          callback(result);
        }
      );

    };
    /* end getTerms */

    this.getTermWithHighestScore = function imedb_getTermWithHighestScore(syllables, callback) {
      debug('getTermWithHighestScore ' + syllables);
      self.getTerms(syllables,
        function getTermsCallback(terms) {
          if (terms.length == 0) {
            debug('no terms');
            callback(false);
            return;
          }
          callback(terms[0]);
        }
      );
    };
    /* end getTermWithHighestScore */

sentence is a list of terms

    this.getSentence = function imedb_getSentence(kanaStr, callback) {
      debug('getSentence ' + kanaStr);
      var self = this;

TODO variable keeps the conposition of the best sentence var

      var doCallback = function getSentence_doCallback(sentence) {
        if (callback) {
          debug('getSentence result ' + JSON.stringify(sentence));
          callback(sentence);
        }
      };

      var n = kanaStr.length;

      if (n == 0) {
        callback([]);
      }

      var taskQueue = new TaskQueue(
        function taskQueueOnCompleteCallback(queueData) {
          var sentences = queueData.sentences;
          var sentence = sentences[sentences.length - 1];
          doCallback(sentence);
        }
      );

      taskQueue.data = {
        sentences: [[], []],
        probabilities: [1, 0],
        sentenceLength: 1,
        lastPhraseLength: 1
      };

      var getSentenceSubTask = function getSentence_subTask(taskQueue, taskData) {
        var queueData = taskQueue.data;
        var sentenceLength = queueData.sentenceLength;
        var lastPhraseLength = queueData.lastPhraseLength;
        var sentences = queueData.sentences;
        var probabilities = queueData.probabilities;

        if (probabilities.length < sentenceLength + 1) {
          probabilities.push(-1);
        }
        if (sentences.length < sentenceLength + 1) {
          sentences.push(['', '']);
        }
        var maxProb = probabilities[sentenceLength];
        var s = kanaStr.slice(sentenceLength -
            lastPhraseLength, sentenceLength);
        self.getTermWithHighestScore(s,
          function getTermWithHighestScoreCallback(term) {
            var syllable = s.join('');

            if (!term) {
              term = {kanji: syllable, freq: 0, kana: syllable};
            }
            var prob = probabilities[sentenceLength -
              lastPhraseLength] * term.freq / kDictTotalFreq;
            if (prob > probabilities[sentenceLength]) {
              probabilities[sentenceLength] = prob;
              sentences[sentenceLength] = 
                sentences[sentenceLength - lastPhraseLength].concat(term);
            }

process next step

            if (lastPhraseLength < sentenceLength) {
              queueData.lastPhraseLength++;
            } else {
              queueData.lastPhraseLength = 1;
              if (sentenceLength < n) {
                queueData.sentenceLength++;
              } else {
                taskQueue.processNext();
                return;
              }
            }
            taskQueue.push(getSentenceSubTask, null);
            taskQueue.processNext();
          }
        );
      };

      taskQueue.push(getSentenceSubTask, null);
      taskQueue.processNext();
    };
    /* end getSentence */
  };
  /* end IMEngineDatabase */
})();