[[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]] をブラウザで動かすのはちょっと面倒そうだった & [[バンドラー]]使うのも厳しそうだったのでここまでにする。時間もないので...==