2010年6月8日火曜日

[Programming] Safari Extension を作ってみた

Google Chrome に見切りを付けた私といたしましては渡りに舟ということで、早速、手を出してみました。まだ大雑把にしか把握してませんが、流石はWebKitベースという事で、アーキテクチャが Chrome Extension にそっくりです。
  • Global HTML は Chrome で言うところのバックグラウンドページ
  • injected Scripts → content_scripts
  • Extension Bar → ブラウザアクション
  • dispatchMessage → chrome.extension.sendRequest
  • info.plist → manifest.json
といった感じですね。

試しに作ってみたのは、ページ内の単語やフレーズをGoogle翻訳、アルク、Urban Dictionaryといった辞書サイトに渡して新規タブにて表示するというものです。マウスで単語を選択しコンテキストメニューからどの辞書サイトを呼ぶかを選ぶようにしています。


構成は Global HTML と injected script の組み合わせという事になります。以下は現状のソースです。

global.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript" charset="utf-8">
function initAll() {
safari.application.addEventListener(
"command",
function(event) {
safari.application.activeBrowserWindow.activeTab.page.dispatchMessage(event.command);
},
false
);
}
</script>
<title>test - Global HTML page</title>
<body onload="initAll();">
</body>
</html>

injected.js
safari.self.addEventListener(
"message",
function(msg) {
var w = document.getSelection().toString();
switch (msg.name) {
case "google":
window.open("http://translate.google.co.jp/?hl=ja&tab=wT#en|ja|" + encodeURIComponent(w), "_blank");
break;
case "alc":
window.open("http://eow.alc.co.jp/" + encodeURIComponent(w) + "/UTF-8/?ref=sa", "_blank");
break;
case "ud":
window.open("http://www.urbandictionary.com/define.php?term=" + encodeURIComponent(w), "_blank");
break;
}
},
false
);

info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>lookup</string>
<key>CFBundleIdentifier</key>
<string>com.yourcompany.lookup</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>Chrome</key>
<dict>
<key>Context Menu Items</key>
<array>
<dict>
<key>Command</key>
<string>google</string>
<key>Identifier</key>
<string>google</string>
<key>Title</key>
<string>Google翻訳</string>
</dict>
<dict>
<key>Command</key>
<string>alc</string>
<key>Identifier</key>
<string>alc</string>
<key>Title</key>
<string>アルク</string>
</dict>
<dict>
<key>Command</key>
<string>ud</string>
<key>Identifier</key>
<string>ud</string>
<key>Title</key>
<string>Urban Dictionary</string>
</dict>
</array>
<key>Global Page</key>
<string>global.html</string>
</dict>
<key>Content</key>
<dict>
<key>Scripts</key>
<dict>
<key>Start</key>
<array>
<string>injected.js</string>
</array>
</dict>
</dict>
<key>ExtensionInfoDictionaryVersion</key>
<string>1.0</string>
<key>Permissions</key>
<dict>
<key>Website Access</key>
<dict>
<key>Level</key>
<string>All</string>
</dict>
</dict>
</dict>
</plist>

なんといってもコンテキストメニューがさっくり使えるのがうれしいですな。まだ設定画面等については未調査ですが、ざっくりと見た感じでは Chrome Extension よりも使い出がありそうです。機能拡張ビルダーまわりは、まだまだこなれてないようで良く落ちるんですが、まぁこれはそのうち改善されるでしょう。そういえば「拡張機能」じゃなくて「機能拡張」なんすよね。INIT/cdev時代を知るものにとっては、やっぱり「機能拡張」の方がしっくり来るんですわw

処理の流れとしては
  1. コンテキストメニューからどのサイトを開くかを選択するコマンドを発行
  2. Global HTML がコマンドを受信
  3. Global HTML はページ内コンテンツにアクセス出来ない(つまりページ内の単語やフレーズが拾えない)ので injected script に対して dipatchMessage を発行し、その際にパラメータとしてコマンドを引き渡す
  4. injected script は document.getSelection() により、ページ内の単語やフレーズを取得し、コマンドに応じたサイトのURL を生成し、window.open(url, "_blank") により新規タブでページを開く
という感じです。

ちなみに window.open()でタブを開いているので、ポップアップが禁止されていると(「Safari→ポップアップウィンドウを表示しない」にチェックが入ってると)ページが開きません。これは問題ですね。これを回避する為には safari.self.browserWindow.openTab() といった Window/Tab 関連の新規 API を呼ぶしか無いようですが injected scripts からはこれらの API が呼べないようです。ということは、injected scripts から Global HTML に URL を渡す dispatchMessage を発行するってことですか。結構めんどくさいかもw


追記: Global HTML 側で openTab() を呼び出すように変更。それほど面倒じゃないですね。アプリとコンテンツの分離という観点からすれば、これはこれで一貫性があると言えましょう。

global.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript" charset="utf-8">
function initAll() {
safari.application.addEventListener(
"command",
function(event) {
safari.application.activeBrowserWindow.activeTab.page.dispatchMessage(event.command);
},
false
);
safari.application.addEventListener(
"message",
function(msg) {
var newTab = safari.application.activeBrowserWindow.activeTab.browserWindow.openTab();
newTab.url = msg.name;
},
false
);
}
</script>
<title>test - Global HTML page</title>
<body onload="initAll();">
</body>
</html>

injected.js
safari.self.addEventListener(
"message",
function(msg) {
const pattern = {
google: "http://translate.google.co.jp/?hl=ja&tab=wT#en|ja|{}",
alc: "http://eow.alc.co.jp/{}/UTF-8/?ref=sa",
ud: "http://www.urbandictionary.com/define.php?term={}"
};
safari.self.tab.dispatchMessage(pattern[msg.name].replace("{}", encodeURIComponent(document.getSelection().toString())));
},
false
);

0 件のコメント:

コメントを投稿