Chrome & Firefox 拡張機能で e-Gov 法令検索を使いやすくする(序)
FOLIO Advent Calendar 2021 - Adventar 19日目のエントリです。
法令解釈を仕様におとして実装するという仕事柄、e-Gov法令検索サイトを参照する機会がそれなりにあります。
弊社には優秀なコンプライアンス部門がありますが、なにかサービスをつくりたい場合には協力して仕様を決めていく必要があります。 特に、私の所属する顧客基盤部では、顧客情報管理や口座開設・解約、相続、契約管理などのシステム*1を担当するため、種々の法令*2が絡んできます。
これまでは、法令をPDFにしてiPadでメモをとるなどしていたのですが、あまりにも長大な法令を相手にするには原始的すぎるため、ブラウザの拡張機能(WebExtension)として便利機能をつくればもっと捗るなと常々思っていました。
サッと1日で書けるようなネタが思い浮かばなかったので、 e-Gov 法令検索サイトを使いやすくするべく拡張機能に手を出してみたエントリです。
なお、仕事でフロントエンド技術を触ることはほぼなく、最後の記憶はjQueryというドラゴンボールに例えるとソバくらいのレベルですので、何卒暖かい目でご笑覧ください。
つくったもの
法令では、括弧が多用されます。
これがいくつもネストされていくと非常に読みづらいものです。
そこで、括弧の範囲に色を付けることで読みやすくする機能を実装しました。
明らかに短時間で実装を終わらせたいことがミエミエのお題
「(定義)」まで強調表示されてしまっているあたりに、まだまだ改良余地がうかがえますね…。
それでは、つくり方です。
つくり方
拡張機能のつくり方はいくつもありますが、便利なテンプレートが公開されていたので、次のものをスタート地点とします。
TypeScriptの環境なのが良いですね! 使用した時点のコミットハッシュはfcc03c2です。
まず覚えておくこと
拡張機能は、この2種類のスクリプトによって構成されます。 拡張機能に関する処理はバックグラウンドスクリプトに書き、ウェブページ内のコンテンツにアクセスしたい場合はコンテンツスクリプトに書く、ということだけ覚えておけばOKです。
タブ内で実行するコンテンツスクリプトを別ファイルに分ける
先のテンプレートをチェックアウトしてきたそのままでは、スクリプトを文字列として渡すサンプルになっています。 このままでは扱いづらいので、スクリプトを外部化して実行できるようにします。
基本的には、この記事のとおりです。
ただ、バックグラウンドスクリプトとコンテンツスクリプトの間をメッセージ方式で連携するサンプルでは、onClickの度にコンテンツスクリプトが挿入されてしまいます。
コンテンツスクリプトが単発で消費されるようなものなら問題ないのですが、常駐するようなタイプの場合は二重三重に挿入されないよう制御が必要です。
コンテンツスクリプトの挿入には、大きく3つの方法があります。
今回は、一番簡単な manifest.json
の content_scripts
を設定する方法を採用します。
src
ディレクトリに適当な ts ファイルを作成するwebpack.common.js
のentry
に上記で作成したファイルを追加するmanifest.json
にcontent_scripts
を追加する
... "content_scripts": [ { "matches": ["*://elaws.e-gov.go.jp/*"], "js": ["js/作成したファイル.js"] } ], ...
DOM Manipulation
コンテンツスクリプトを書けるようになったら、ページ内の要素を更新する処理を書いていきます。 いよいよ jQuery の出番か?というところですが、ES標準が進んでいるおかげで標準APIのみで簡単に書くことができました。 次のページのチートシートが便利でした。
一番素朴なところとして、クリックした位置の条文を対象に強調表示することを考えます。
通常、括弧は条文をまたいで現れることはありません。
また、閉じ括弧がないなどのエラーケースは、十分にレビューされて公開される性質上、この際無視できるものとします*3。
このため、とりあえず ()
を固定で置換してしまえば目的は達成できます。
どうせ自分しか使わないので、恥も外聞もないコードから始めます。
function highlightParentheses(message: HighlightParentheses) { const articleDivId = articleId(message.articleNumber); const articleBlock = document.querySelector(`section#${articleDivId}`); if (!articleBlock) return; if (!articleBlock.classList.contains("__highlighted__")) { articleBlock.classList.add("__highlighted__"); articleBlock.innerHTML = insertHighlightTag(articleBlock.innerHTML); } } const colorPalette = [ "lightpink", "lightblue", "lightcoral", "lightgoldenrodyellow", "lightsalmon", "lightskyblue", ]; function insertHighlightTag(innerHtml: string): string { let i = 0; function pickColor(): string { const color = colorPalette[i]; if (i + 1 >= colorPalette.length) { i = 0; } else { i += 1; } return color; } return innerHtml .replace(/(/g, (s) => { return `<span class='__highlighted_text__' style='background-color: ${pickColor()}'>${s}`; }) .replace(/)/g, (s) => { return `${s}</span>`; }); }
ちなみに、e-Gov法令検索サイトでは jQuery が組み込まれていますので、いつでも使えます 🎅
拡張機能(バックグラウンドスクリプト)からコンテンツスクリプトを呼び出す
最後に、コンテンツスクリプトを呼び出す処理を実装します。 メッセージによる連携を行うので、次のことを準備します。
バックグラウンドスクリプト側:
export type HighlightParentheses = { kind: "highlightParentheses"; articleNumber: number; }; export type Message = HighlightParentheses | 他の機能を実装したら追加していく; // どこか適当なクリック処理などで browser.tabs .query({ active: true, currentWindow: true }) .then((tabs: Tabs.Tab[]) => { const currentTabId = tabs[0]?.id; if (!currentTabId) return; browser.tabs.sendMessage(currentTabId, { kind: "highlightParentheses", articleNumber: クリックしている条文の番号, } as Message); });
コンテンツスクリプト側:
function handler(message: Message) { switch (message.kind) { case "highlightParentheses": highlightParentheses(message); break; } } browser.runtime.onMessage.addListener(handler);
おわりに
これで無事、強調表示がされるようになりました!
実際には、ここで紹介したコードよりもいくつかの追加実装を行っています*4。
(アドベントカレンダーに間に合わせるために急いで書いたので、)まだコードは公開できていませんが、近いうちに清書して公開したいと思います。
公開しました!*5
少しずつ機能追加していければと考えています。
子どもの工作みたいなレベルですが、実際子どもレベルのスキルなので、これが1日で実装できる精一杯でした*6。 テンプレートが公開されていたことは、未だに webpack よくわからない勢なので大変助かりました。 普段はバックエンドシステム(Scala)ばかりなので、たまにはこういったものをつくるのも良いものですね。
このエントリは、会社のオンライン忘年会を横目にアドベントカレンダー休暇(ただの有給)に書かれました。 よいお年を!