iframeの中を覗き込む。Web要素を抽出・解析する専用ブラウザを作った理由
最近、AIを使ったコーディングが増えました。とても便利です。私もUIデザインの細かい調整でLLM(大規模言語モデル)の力をよく借ります。
ただ、ここで一つ問題が起きます。LLMとの会話の中で、「あのヘッダーの右にある青いボタンの、文字と枠の間の隙間をもっと広げてくれ」と自然言語で説明するのは一苦労なのです。伝わらなくて何往復もやり取りするハメになります。
もし、画面上の要素をマウスで視覚的にパッと選択して、そのHTMLやCSSの情報をまるごとコピペできたら。それをLLMとの会話に流し込めば、確実性の高い修正が一発で通るのではないか。そう考えました。
そんなわけで、「今見えているHTML要素をパッと取り出して、AIに渡せるようにクリップボードにコピーするだけのブラウザ」を自作してみました。名付けて「Karaage Browser(唐揚げ・ブラウザ)」です。
実際のKaraage Browserの画面。大きな唐揚げのHTMLソースとCSSを見事に抜き出しています。
📝
なんで唐揚げかと言うと、そんなに深い意味はありません。「外側のパリッとした衣(Border)と、内側のジューシーな肉(Content)がボックスモデルに似ている」なんて後付けの理由を考えたりもしましたが、本当のところは、開発中に「あぁ、唐揚げ食べたいなぁ〜〜」とふと思いついただけです。おっさんの食い意地なんてそんなものです。
外部から iframe の中身を触るという壁
このブラウザのコアな仕組みは、画面左側にデカデカと配置された iframe (仮想ブラウザ画面)です。ここでプレビューを見ながら、マウスポインターを合わせた要素を解析します。
最初、ただマウスポスの座標を取るだけなら簡単だろうと考えていました。しかし、すぐに「Cross-Originの壁」というウェブセキュリティの厳しさに直面します。
⚠️
WARNING
外部のサイトを読み込んだ場合、親であるブラウザ(Karaage Browser)は iframe の中の世界を勝手に覗くことはできません。これは当然のセキュリティ仕様です。しかし、ローカルで開発している自分のファイルや、同一ドメイン内の画面なら覗き込むことができます。
// マウスの座標から、iframe内の要素を特定する処理
overlay.addEventListener('mousemove', (e) => {
try {
const frameWin = iframe.contentWindow;
const frameDoc = iframe.contentDocument || frameWin.document;
const rect = iframe.getBoundingClientRect();
// iframeの枠の左上を原点(0,0)とした相対座標に変換
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 指定座標にある要素を取得する魔法のメソッド
const el = frameDoc.elementFromPoint(x, y);
if (el) {
updateInspection(el, rect);
}
} catch (err) {
console.warn("Cross-origin restriction or Iframe not ready");
}
});
この elementFromPoint(x, y) というメソッドが肝でした。XとYの座標を渡すだけで「今そこに重なっている一番上の要素」をピンポイントで返してくれます。これを知ったときは、思わず膝を打ちました。
ボックスモデルの計算地獄
要素が特定できたら、次にやるのはその要素に「青い枠(ハイライト)」をかぶせることです。ただかぶせるだけなら要素の幅と高さを取るだけですが、このブラウザの開発目的は「AIにスタイル情報を教えること」です。
つまり、Margin(外側の余白)と Padding(内側の余白)、そしてBorder(枠線)を厳密に抽出して可視化したいと考えました。ChromeのF12ツールで見慣れたあの色分けです。
これが思いのほか面倒でした。getBoundingClientRect() で取得できるのは「Borderを含む全体のサイズ」です。ところが、Paddingの量を可視化しようと思うと、要素の内側に緑色の枠を引く必要があります。
// 要素のスタイルをすべて取得
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect(); // Border込みのサイズ
// Paddingを可視化するための緑色のオーバーレイ領域
const pt = parseFloat(style.paddingTop);
const bt = parseFloat(style.borderTopWidth);
// (中略)
// Border線の太さ分だけ内側にずらす
paddingOverlay.style.width = `${rect.width - bt * 2}px`;
paddingOverlay.style.left = `${rect.left + bt}px`;
paddingOverlay.style.top = `${rect.top + bt}px`;
// CSS border機能を利用して、内側に緑色のPadding帯を描画するハック
paddingOverlay.style.borderTop = `${pt}px solid var(--color-padding)`;
✅
SUCCESS
あれこれ計算式をこねくり回した結果、「Paddingのオーバーレイは、CSSの border に色をつけて描画すれば、真ん中がくり抜かれた枠を作れる」ことに気づきました。貧乏人の知恵というか、こういう泥臭いハックが決まった瞬間は技術者冥利に尽きるものです。
HTMLの正規表現ハイライトの限界と恩恵
画面の右半分には、取得した要素のHTMLソース(スニペット)とCSSを表示するエリアを作りました。真っ白な文字列だと読みにくいので、シンタックスハイライトが必要です。ここで highlight.js のようなライブラリを入れるのが定石ですが、今回は「軽量でパッと動く」をテーマにしていたので、自分で正規表現を書くことにしました。
// 軽量自作シンタックスハイライト
function highlightHTML(htmlStr) {
return htmlStr
.replace(/</g, "<").replace(/>/g, ">") // サニタイズ
.replace(/(<\/?)([a-zA-Z0-9\-]+)/g, '$1<span class="token-tag">$2</span>')
.replace(/([a-zA-Z0-9\-]+)=/g, '<span class="token-attr">$1</span>=')
.replace(/"([^"]*)"/g, '<span class="token-val">"$1"</span>');
}
この正規表現、シンプルですがタグ名と属性、クォーテーションの中の値を綺麗に色分けしてくれます。完璧ではありませんが、開発中のちょっとしたコードの切れ端を見るくらいなら、この程度で十分役に立つことがわかりました。
コピー・トゥ・クリップボードの裏話 泥臭いハック
最後に、このツールのメイン機能である「AIに渡すためのクリップボードコピー」ボタンの実装です。情報をJSON形式にまとめて一発でコピーできるようにしました。
しかし、ここでも罠がありました。Iframeを含む複雑な構成や、セキュリティの厳しい環境だと、最新の navigator.clipboard.writeText() がブロックされることがあるのです。
これも昔ながらのハックを使いました。画面に見えない textarea を一瞬だけ作り、そこに文字を入れて選択状態にし、document.execCommand('copy') を呼び出す。令和の時代になんという土臭いコードだと笑われるかもしれませんが、これが一番確実でした。
こうして、必要な要素をクリックし、「Copy Element Data」ボタンを押すだけで、AIに「この要素のスタイルを元に、〇〇を作って」と伝えられるツールが完成しました。
自動リロードの心地よさ
おまけの機能として、エディタ側でファイルが切り替わったら、自動的にこのブラウザのプレビューもそのファイルに切り替わるようにしました。
// 1秒に1回、連携用テキストファイルを読みに行ってURLを更新する
async function checkSyncFile() {
try {
const response = await fetch('./.current_file.txt');
if (response.ok) {
let targetName = await response.text();
if (targetName && targetName !== currentFileInKaraage) {
currentFileInKaraage = targetName;
iframe.src = '../' + targetName;
}
}
} catch (e) {
// 何も言わずに握りつぶす
}
}
setInterval(checkSyncFile, 1000);
ポーリング(定期確認)という、これまた強引な手段です。でも、これがあるおかげで、エディタとプレビュー画面を行ったり来たりする手間が省けました。
泥臭い実装や、強引なハックの寄せ集めのような構成になってしまいました。ですが、自分にとって本当に必要な情報を、必要な形ですぐに取り出せるツールが手元にあると、不思議と作業が捗ります。高度なツールはたくさんありますが、手に馴染む小さな道具を自分で作る楽しさも、また良いものです。
おまけ:唐揚げブラウザ V2 ダウンロードと使い方