MouseDictionaryを読んでエスペラント語用に改造した

[MouseDictionary]( https://github.com/wtetsu/mouse-dictionary ) をエスペラント語向けに改造したlina_dicto_wemdを書いた。
その際にMouseDictionary (以下MD)のコードを読んだ。その際に書いたメモをここに残す。

lina_dicto_wemd:
- [For Chrome]( https://chrome.google.com/webstore/detail/mlladnmekfebjllfgofppdaekfabfdgi/publish-accepted?authuser=0&hl=ja )
- [For Firefox]( https://addons.mozilla.org/ja/firefox/addon/lina_dicto-wemd/ )
- [GitHub]( https://github.com/MichinariNukazawa/mouse-dictionary/tree/esperanto-variant-lina-dicto )


<img width="881" alt="20200502.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153822/2eb8e3d6-3e22-9fa2-9233-b3e2cfbf0fbf.png">


# 当初目標 （ゴール）
- lina_dicto シリーズに mouse dictionary の日本語機能を取り込む。
- lina_dicto for webextension を mouse dictionary ベースに置き換える。



# MouseDictionaryの参照情報
[MouseDictionary(GitHub)]
https://github.com/wtetsu/mouse-dictionary
[Chrome拡張の高速な英語辞書ツールをつくりました(Mouse Dictionary)]
https://qiita.com/wtetsu/items/c43232c6c44918e977c9
[Mouse Dictionaryの技術的な話]
https://qiita.com/wtetsu/items/2a5568cb0b5a38c003fb
[(日本語の変形)活用もまあ扱えます deinja]
https://github.com/wtetsu/deinja



# 主な課題
MDをエスペラント化するにあたって、以下の箇所を変更します。
- 初期辞書データの読み込みと辞書データフォーマット
（MDが使用している英和辞書データを、エス和辞書データに置き換える）
- 代用表記への対応
（辞書データはcaret-sistemoだが、Web上では他の各代用表記とalfabetoがありうる）
- 英単語(エス単語)のルックアップ候補生成
 (英語では不規則動詞リスト等々。エスペラントの場合、規則的な変化のみ)

ついでに以下も見ました。
- 和単語のルックアップ候補生成(変形活用をdeinjaで処理している部分)



# MouseDictionary 基本構造

MouseDictionaryのmanifest.jsでは、options_ui/pageとbackground/scriptsを読み込んでいる。
(content-scriptは使っていないらしい。)



# 初期辞書データの読み込みと辞書データフォーマット
初回に辞書データをダウンロード配置するまでの流れ、ダウンロードだと思っていたらローカルファイルだった。

src/options/logic/dict.js[93]
`const dict = await loadJsonFile("/data/dict.json");`
dict.json (REPO: static/data/dict.json) 自体は、分割された辞書ファイル
`["/data/initial_dict1.json", "/data/initial_dict2.json"]`
を指す内容のみ入っている。
辞書ファイルは、単純な "英単語":"日本語文" のJSONの連想配列

```
ls -lh static/data/
-rw-r--r-- 1 nuka nuka   72  2月  5 22:15 dict.json
-rw-r--r-- 1 nuka nuka 2.4M  2月  5 22:15 initial_dict1.json
-rw-r--r-- 1 nuka nuka 2.3M  2月  5 22:15 initial_dict2.json
```
FireFoxのAMOは、アドオン全体で最大200MB
https://extensionworkshop.com/documentation/publish/submitting-an-add-on/
含まれるファイル１つの最大サイズ4MB
(こちらは公式の参照URLが見つからなかった)

## その他備考
MD自体はその他いくつかのフォーマットに対応している。
読み込んで使える、外部辞書データのリスト。
https://github.com/wtetsu/mouse-dictionary/wiki/Download-dictionary-data



# ルックアップ機能の呼び出しグラフ(検索)
「代用表記への対応」「エス和向けのルックアップ候補生成」のために、検索機能の概要を掴む。

### マウスイベント発生から日英判定までの関数コール
ユーザのマウス移動操作を起点に、events.js内のマウスイベントコールバックから処理がスタート。

Lookuper.run()が、events.js のマウスイベント系や、Loopuper自身から呼ばれる。

 : lookuper.js

```
  async run(textToLookup, withCapitalized, includeOrgText, enableShortWord) {
    const { entries, lang } = entry.build(textToLookup, withCapitalized, includeOrgText);
```

 : src/main/entry/entry.js

```
import entryGeneratorJa from "./entry/ja";
const build = (text, withCapitalized, mustIncludeOriginalText) => {
  const lang = isEnglishText(text) ? "en" : "ja";
  ~
      entries = entryGeneratorEn(text, withCapitalized, mustIncludeOriginalText);
      ~
      entries = entryGeneratorJa(text, withCapitalized, mustIncludeOriginalText);
export default { build };
```
entry内で日英判定している


### 和単語のルックアップ候補生成
(変形活用をdeinjaで処理している部分)
本筋とは外れるが、和単語の処理部・denijaの呼び出し。

 : src/main/entry/ja.js

```
const createLookupWordsJa = sourceStr => {
      const deinedWords = rule.doJa(part);
export default createLookupWordsJa;
```

 : src/main/rule.js

```
import buildDeinja from "deinja/build";
const registerJa = data => {
  deinjaConvert = buildDeinja(data);
};
  doJa: word => deinjaConvert(word)
};
```


### denijaのテスト出力・評価の方法
denijaを試したり出力を見てみるには、ユニットテストが使える。

 : `__test__/transform.test.js`


```
test("", () => {
  expect(rule.doJa("死んだ")).toEqual(expect.arrayContaining(["死ぬ"]));
  expect(rule.doJa("殺った")).toEqual(expect.arrayContaining(["殺る"]));
});
```

["愛します" -> "愛する"]
https://github.com/wtetsu/deinja/issues/2

愛します
愛している
->愛する

```
愛する      []
愛した      [ '愛しる', '愛す' ]
愛します    [ '愛しる', '愛する', '愛す' ]
愛している  []
愛す        []
愛し        [ '愛す' ]
```


# 英語処理部のエスペラント対応

1.
src/main/entry/entry.js では、ASCII範囲と下記特殊記号のみで構成されていた場合に英文判定している。

 : src/main/entry/entry.js [25] // 英字判定

```
const isEnglishLike = (0x20 <= code && code <= 0x7e) || code === 0x2011 || code === 0x200c;
```
0x2011    | ("NON-BREAKING HYPHEN", ハイフン('-')の仲間)
0x200c    | ("zero-width non-joiner", 特殊用途の幅のない空文字)
エスペラントに対応するには、ASCII範囲の他にサーカムフレックス付き文字などを使用するため、書き換えが必要。

2.
src/lib/traverser.js にも英字の判定があるので書き換えておく。
src/lib/traverser.js [181] // 英字判定

```
const isEnglishLikeCharacter = code => 0x20 <= code && code <= 0x7e;
```

3.
マウスポインタ下の英文を取る処理で、英単語・記号（そして多分それ以外）を分類して分割処理に反映している。
ex. "Ĉiuj ~"の英文にポイントすると先頭のサーカムフレックス付き文字が抜けて"iuj ~"が取れる。
エスペラントで使う文字に対応するよう書き換える。

マウスオーバーイベントで始まる単語分割までの関数の呼び出しグラフ

```
event.js::onMouseMoveSecondOrLater()
->traverse.js::getTextUnderCursor()
->traverse.js::*()
->traverse.js::searchStartIndex()
->traverse.js::doGetCharacterType() == rule.js::doLetters()
```

エスペラント化において英文を取る処理の焦点はこのあたり。
 : travarser.js [79] // 先頭文字を探す

```
const searchStartIndex = (text, index, doGetCharacterType) => {
  let startIndex;
  let i = index;
  for (;;) {
    const code = text.charCodeAt(i);
    // ここの`doGetCharacterType(code)`は実行時に`rule.js::doLetters()`が呼ばれる。
    // `& 1`は範囲外のcharacterを渡すと帰ってくるundefinedをゼロに変換するための処理？
    const toPursue = doGetCharacterType(code) & 1;
    if (!toPursue) {
      startIndex = i + 1;
      break;
    }
```

分類には、 data/rule.json から読み込んだ文字リストを用いている。
data/rule/letter.json5 (data/rule.json) の、ドキュメンテーションは見当たらないが、定数値の意味は多分こう。
**※ここはコメントで正しい意味をご指摘頂きました。ありがとうございます。**  
  0 | 記号(ex. /[!"#$%&]/)
  1 | (使用していない)
  2 | 単語分割の記号(ex. /[,.]/)
  3 | 英字または英文に含まれる記号(ex. /[-'_A-z]/

エスペラントに対応するために、「単語に含まれる文字」の範囲を増やした。
 : data/rule/letter.json5 (data/rule.json)

```
  // エスペラント単語で使用する文字・記号
  [94, 3], // ^
  [126, 3], // ~
  [0x0108, 3], // "C^"],
  [0x0109, 3], // "c^"],
  [0x011C, 3], // "G^"],
  [0x011D, 3], // "g^"],
  [0x0124, 3], // "H^"],
  [0x0125, 3], // "h^"],
  [0x0134, 3], // "J^"],
  [0x0135, 3], // "j^"],
  [0x015C, 3], // "S^"],
  [0x015D, 3], // "s^"],
  [0x016C, 3], // "U^"],
  [0x016D, 3], // "u^"],
```



# 代用表記への対応
エスペラント語ではASCII文字の他に、サーカムフレックス付き文字(ex. "ĉ, ĝ, ĥ, ĵ, ŝ")などを用いる。
エスペラント語には、サーカムフレックス付き文字の入力や表示が難しい環境でも使えるよう、ASCII文字のみで特殊文字を記述することのできる「代用表記」が4種類ほど存在する。

内部辞書はcaret-sistemoであるため、ルックアップの前に、ブラウザ上文字列のalfabetoを変換する。

日エス判定後の、src/main/entry/en.js の入り口で対応した。

```
const convert_caret_from_alfabeto_sistemo = (str) =>
{
  const replaces = [
    [/\u0108/g, "C^"],
    [/\u0109/g, "c^"],
    [/\u011C/g, "G^"],
    [/\u011D/g, "g^"],
    [/\u0124/g, "H^"],
    [/\u0125/g, "h^"],
    [/\u0134/g, "J^"],
    [/\u0135/g, "j^"],
    [/\u015C/g, "S^"],
    [/\u015D/g, "s^"],
    [/\u016C/g, "U^"],	// 公式の変換には無いがとりあえず
    [/\u016D/g, "u^"],	// 公式の変換には無いがとりあえず
  ];

  for(const replace of replaces){
    str = str.replace(replace[0], replace[1]);
  }

  return str;
}

const createLookupWordsEn = (rawSourceStr, withCapitalized = false, mustIncludeOriginalText = false) => {
  const replacedSourceStr = convert_caret_from_alfabeto_sistemo(rawSourceStr.replace(RE_UNNECESSARY_CHARACTERS, "").replace(RE_SLASH, " / "));
```

(代用表記の相互変換などは、lina_dictoのesperanto.jsを持ってきてimportしようかと考えたのだけれど、import方式が違うので、今はとりあえずやめておきました。)



# ルックアップ候補生成
英語には単語の不規則変化(ex. run->ran)があるため、MDには不規則動詞リスト等々がどこかにあるはず。
エスペラント単語においては英語のような不規則変化は無いため、ルールベースで変換が可能。

エスペラントの単語の変形は、単語の原型に相当する語幹の後ろに、動詞語尾[aeiou], 複数形`-j`, 対格`-n` の順で並ぶ。
よって、複数形などの末尾変換をまとめていた下記データ構造を置き換えることで、エスペラント対応した。
（読み込んでいるメソッドを見る限りそうは見えないのだが、これで`-aj -> -o` といった変換もしてくれるようになる。）

 : main/entry/en.js

```
const TRAILING_RULES = [
  [{ search: "n", new: "" }],
  [{ search: "j", new: "" }],
  [{ search: "jn", new: "" }],
  [{ search: "i", new: "o" }],
  [{ search: "o", new: "o" }],
  [{ search: "a", new: "o" }],
  [{ search: "e", new: "o" }],
  [{ search: "u", new: "o" }],
  [{ search: "as", new: "o" }],
  [{ search: "is", new: "o" }],
  [{ search: "os", new: "o" }],
  [{ search: "i",  new: "i" }],
  [{ search: "o",  new: "i" }],
  [{ search: "a",  new: "i" }],
  [{ search: "e",  new: "i" }],
  [{ search: "u",  new: "i" }],
  [{ search: "as", new: "i" }],
  [{ search: "is", new: "i" }],
  [{ search: "os", new: "i" }],
];
```

TODO: とはいえ将来的には接頭辞や分詞なども扱いたいので、エスペラント用の変換関数を書くことになると思う。
(main/entry/en.js にハードコートされているこのarray以外にも data/rule/*.json5 (data/rule.json) がルックアップ候補生成に使われていそう。)


# その他
WebExtension名の置き換え
アイコン等の画像リソースの置き換え
デフォルト背景色の変更（MouseDictionaryと区別するため）



# TODO
- caret-sistemo以外の代用表記(H-sistemo, X-sistemo)への対応。
- 結果表示UI上でcaret-sistemoをalfabeto表示に変換したい。（フォントの対応が微妙かもしれないからしないほうが良いかも）
- ルックアップ候補生成、接頭辞(mal-), 分詞(-ant-)への対応。
// 真面目にやると、接頭辞・分詞自体も意味表示したくなる。lina_dicto本家側でも完全対応に至っていない。

日エスは引けなくてもまあいいかな...。