[[Vimium]]の[[Filtered Hints (Vimium)|Filtered Hints]]は便利な機能だが、日本語を検索対象にすることができない。[[jsmigemo]]など使って実現できないか調べてみる。
> [!caution]
> 途中で力尽きた。
## インストール・起動
[[Vimium]]をクローンして調べる。
プロジェクトディレクトリをターゲットディレクトリとして、[[パッケージ化されていない拡張機能を読み込む]]。
## コードリーディング
### FilterHintsクラス
[FilterHintsクラス](https://github.com/philc/vimium/blob/9d17c889e12353e6705ac69f03b197236f68a263/content_scripts/link_hints.js?plain=1#L846)の実装を確認してみる。
`getMatchingHints` がそれっぽい気がする。コメントを日本語に翻訳しつつ自分なりに加筆してみる。
```js
getMatchingHints(hintMarkers, tabCount) {
// hintKeystrokeQueue:
// pushKeyCharでputされるkeyCharのQueue
// フィルタが済んだあと(の数字入力)が1キー毎にキューになってる
// フィルタ中の入力文字は入らない
const matchString = this.hintKeystrokeQueue.join("");
// この結果に含まれるラベルだけが今後の処理に繋がる
let linksMatched = this.filterLinkHints(hintMarkers);
// フィルタがすんでラベル入力中なら、それに該当するもののみをフィルタ
// 41, 70, 75のラベルがあって、matchString=["7"] なら70 と75が通過する
linksMatched = linksMatched.filter((linkMarker) =>
linkMarker.hintString.startsWith(matchString),
);
// Visually highlight the active hint (that is, the one that will be activated if the user types
// <Enter>).
tabCount = ((linksMatched.length * Math.abs(tabCount)) + tabCount) % linksMatched.length;
if (this.activeHintMarker?.element) {
this.activeHintMarker.element.classList.remove("vimiumActiveHintMarker");
}
this.activeHintMarker = linksMatched[tabCount];
if (this.activeHintMarker?.element) {
this.activeHintMarker.element.classList.add("vimiumActiveHintMarker");
}
return {
linksMatched,
userMightOverType: (this.hintKeystrokeQueue.length === 0) &&
(this.linkTextKeystrokeQueue.length > 0),
};
}
```
`hintMarkers` は以下のようなObjectの配列。
```js
{
hintString: "3", // ヒントの文字
linkWords: ["hoge", "weseek", ..., "jp"], // 検索対象のWord?
linkText: 本文ポイけど謎,
score: 1.864602173668074,
stableSortCount: 19, // ソート順. 低いほど高い
}
```
ここまでの結果から `this.filterLinkHints` の処理が重要そう。
### filterLinkHints
見てみる。
```js
filterLinkHints(hintMarkers) {
const scoreFunction = this.scoreLinkHint(
// linkTextKeystrokeQueue は filter のために入力したアルファベットリスト
// ["v", "i"] のような感じ
this.linkTextKeystrokeQueue.join(""),
);
const matchingHintMarkers = hintMarkers
.filter((linkMarker) => {
linkMarker.score = scoreFunction(linkMarker);
return this.linkTextKeystrokeQueue.length === 0 || linkMarker.score > 0;
})
.sort(function (a, b) {
if (b.score === a.score) return b.stableSortCount - a.stableSortCount;
else return b.score - a.score;
});
if (
matchingHintMarkers.length === 0 &&
this.hintKeystrokeQueue.length === 0 &&
this.linkTextKeystrokeQueue.length > 0
) {
// We don't accept typed text which doesn't match any hints.
this.linkTextKeystrokeQueue.pop();
return this.filterLinkHints(hintMarkers);
} else {
let linkHintNumber = 1;
return matchingHintMarkers.map((m) => {
m.hintString = this.generateHintString(linkHintNumber++);
if (m.isLocalMarker()) this.renderMarker(m);
return m;
});
}
}
```
これは `scoreFunction` が肝なので、`this.scoreLinkHint` の実装を見た方がいい。
### scoreLinkHint
```js
scoreLinkHint(linkSearchString) {
// ["v", "i", "m"]
const searchWords = linkSearchString
.trim()
.toLowerCase()
.split(this.splitRegexp);
return (linkMarker) => {
if (!(searchWords.length > 0)) return 0;
// linkWordsがない場合のみ、linkTextを使う
// 日本語はlinkTextだからここは変更必要そう...
if (!linkMarker.linkWords) {
linkMarker.linkWords = linkMarker.linkText
.toLowerCase()
.split(this.splitRegexp)
.filter((term) => term);
}
// ["hoge", "weseek", ..., "jp"] とか. さっき出てきたやつ
const linkWords = linkMarker.linkWords;
const searchWordScores = searchWords.map((searchWord) => {
const linkWordScores = linkWords.map((linkWord, idx) => {
// 1文字 が linkWord に含まれていたら、場所に応じてスコアリング
const position = linkWord.indexOf(searchWord);
if (position < 0) {
return 0; // No match.
} else if (position === 0 && searchWord.length === linkWord.length) {
if (idx === 0) return 8;
else return 4; // Whole-word match.
} else if (position === 0) {
if (idx === 0) return 6;
else return 2; // Match at the start of a word.
} else {
return 1;
}
}); // 0 < position; other match.
return Math.max(...linkWordScores);
});
if (searchWordScores.includes(0)) {
return 0;
} else {
const addFunc = (a, b) => a + b;
const score = searchWordScores.reduce(addFunc, 0);
// Prefer matches in shorter texts. To keep things balanced for links without any text, we
// just weight them as if their length was 100 (so, quite long).
return score / Math.log(1 + (linkMarker.linkText.length || 100));
}
};
}
```
以下について、アルファベットのみを抽出しているぽい。
```js
// linkWordsがない場合のみ、linkTextを使う
// 日本語はlinkTextだからここは変更必要そう...
if (!linkMarker.linkWords) {
linkMarker.linkWords = linkMarker.linkText
.toLowerCase()
.split(this.splitRegexp)
.filter((term) => term);
}
```
ここは単純に
- テキスト全体に対して部分一致をかける ([[Migemo]]予定)
- ヒットしたら採用、ヒットしなければ不採用
じゃだめか?
## 改造してみる
スコア計算はシンプルにする。部分一致で大抵は事足りるはず。
```js
scoreLinkHint(query) {
return (linkMarker) => {
if (query.length === 0) {
return 0;
}
const targetText = linkMarker.linkText.toLowerCase();
const pos = targetText.indexOf(query.toLowerCase());
if (pos === 0) {
return 4;
} else if (pos > 0) {
return 2;
} else {
return 0;
}
};
}
```
プラスで[[jsmigemo]]... ==と思ったけど [[jsmigemo]] をブラウザで動かすのはちょっと面倒そうだった & [[バンドラー]]使うのも厳しそうだったのでここまでにする。時間もないので...==