Chrome拡張入門
Google Chrome拡張の概要から作り方(@Chrome Extension TechTalk)
Shogo Ohta, 2010-4-9
はじめに
自己紹介
- 名前:
- 太田 昌吾
- os0x(blog, Twitter)
- Chrome ExtensionsのAPI Expertで、Chromium Extensions Japanグループの管理人
- お仕事:
- Chrome拡張について記事を書いたり、JavaScriptの記事を書いたりしています
2010/3/25に
Google Chrome OS 最新技術と戦略を完全ガイド: 小池良次, 中島聡, 伊藤千光, 太田昌吾, まえだひさこ, 向井領治が発売!
Software Design 2010年5月号にChrome拡張の特集25ページが掲載予定です。
- 得意分野:
- ほぼJavaScript専門、なかでもクロスブラウザや高速化が得意です
- 簡単な経歴:
- 元々、OperaのUserJavaScriptやGreasemonkeyを良く書いていた(oAutoPagerizeとか)
ChromeでもUserScriptsが動くらしい→拡張もできる(ようになる)らしい→UserJavaScriptより色々できて楽しい!→拡張作ったり、記事を書いていたらgihyo.jpで連載をすることに→そのまま勢いでAPI Expertに
Chromium Extensions Japan
Google Chromeの拡張機能について、APIの最新情報や開発のノウハウなどを日本語で共有するGoogle 準公式のコミュニティ
2009年12月にできたばかり。こういったイベントのほか、オンラインでの勉強会なども計画中
Google Chromeとは、Chrome拡張とは
- Chromium
- レンダリングエンジンにWebKitを、JavaScriptエンジンにV8を採用したオープンソースのウェブブラウザ
- Google Chrome
- ChromiumをGoogleがカスタムして公開しているブラウザ
- Chrome拡張
- Chrome/Chromium上で動く、ウェブブラウザに特定の機能を追加するHTMLベースのアプリケーション
Chrome vs Chromium
| Google Chrome | Chromium | 補足 |
| ロゴ |  |  | |
| 開発者 | Googler | 誰でも(多くはGoogler) | |
| 障害報告 | あり(ONにした場合) | なし | |
| 利用統計 | あり(ONにした場合) | なし | |
| HTML5のVideo、Audio | H.264、AAC、MP3、Vorbis、Theora | デフォルトではVorbisとTheoraのみ | FFmpegを使用しているので、自己責任においてこれを差し替えれば対応フォーマットを変えることができる |
| ページ翻訳機能 | あり | なし | ※日本語圏での評判はいまひとつ |
| サンドボックス(セキュリティ機構) | 常にON | 場合によってはOFFになっていることも | 非公式なパッケージなどは要注意 |
| 品質保証 | 事前にテストされてからリリースされる | Nightly Buildsのみ | |
Chromiumの特徴
オープンソースなので、すべてのソースが読める。カスタムしてビルドを作れる。
オープンソースコミュニティのパワーで、日々開発が進んでいる。また、Chromiumのカスタム版が数多く存在する。
今すぐ開発に参加できる chromium - Project Hosting on Google Codeへ!
Chromeの特徴
stable(安定版)、beta(テスト版)、dev(開発版)の3つのリリースがある
安定版は名前の通り、一般ユーザーが使用するリリース、(セキュリティフィックスを除いて)半年に1回程度のリリース
テスト版は、主に安定版のリリース前に、安定化のためにリリースされる、時期によってはまったくリリースされないが、1ヶ月に1回程度のリリース
開発版は、安定版のリリース後、次のバージョンに向けた新機能追加を主目的(そのため不安定になることも多々)にリリースされる、1週間に1回程度のリリース
※開発版もリリース前は安定する。修正が多く適用されている分だけテスト版より安定することも。
Chromeの構成
JavaScriptエンジンはV8というChromeのために開発したオープンソースのエンジンを搭載
レンダリングエンジンはWebKitのWebCoreを採用
WebCoreは、WebKitのレンダリング周りの実装(JavaScript部分はJavaScriptCore)
V8とWebCoreを繋ぐ部分にバグが潜みやすい…(Safariでは正常なのに、Chromeだとなんかおかしくなるなぁという現象はここに起因することが多い)
最近はChromiumチームが積極的にWebKitにコミットするようになっていて、かなり改善しつつある
Chrome拡張とは
HTML/CSS/JavaScriptで作る、ブラウザをもっと便利にするモノ
(2010年末に登場予定の)Chrome OSではアプリケーションの役割を果たす?
とにかく作るのは簡単、でも制約は多い
Chrome拡張の例
実物を見てもらうのが一番
公式の拡張ギャラリーには2010年4月9日時点で4000以上の拡張が登録されています。
Google Mail Checker - Google Chrome 拡張機能ギャラリー
高機能版は
Google Mail Checker Plus - Google Chrome 拡張機能ギャラリー(最近使い始めましたが便利です)
Hatena Bookmark GoogleChrome extension - Google Chrome 拡張機能ギャラリー(はてな公式のChrome拡張、こちらも便利です)
Chrome拡張でできること(API)
APIの詳細は公式ドキュメントとその非公式日本語訳(@nao58)へ
APIはまだまだ少ないが、バックグラウンド処理、クロスドメイン通信、データの永続化をベースに、可能性は無限
Chrome5では履歴を扱うAPIが追加される(chrome.history)
実験段階のAPI(experimental.* APIs)もあります
Chrome拡張のセキュリティ
拡張コンテキスト、コンテントコンテキスト、ページコンテキストの3つのコンテキストが存在し、それぞれは完全に分かれているので、お互いが干渉してしまうことはない。さらに、拡張同士も独立したコンテキストで実行される。
拡張コンテキストはタブ操作やクロスドメイン通信などの特権を実行でき、コンテントコンテキストと通信したり、スクリプトを実行したりといったことができます。
コンテントコンテキスト(Content Scripts)は特権を持っていませんが、読み込んだページのDOMを操作することができ、拡張コンテキストと相互に通信できます。
ページコンテキストは通常のウェブページで実行されるコンテキストで、コンテントコンテキストとはDOM経由でしかやり取りできませんし、拡張コンテキストとは完全に分断されています
「悪意のあるページで拡張の特権を利用されてしまう」といったことが起こりにくい仕様になっているが、開発者から見ると面倒に感じる部分も…
拡張同士の関係
拡張同士はお互いに独立しており、特にBackground Pageのプロセスは完全に分かれている
拡張同士で連携するには、chrome.extension.sendRequestなどのAPIを使用する必要がある
拡張はそれぞれIDを持っており、拡張機能のページでデベロッパー モードにすると確認できる。また、公式ギャラリーのURLにもIDがそのまま使われている。
このIDはドメインに相当するもので、CookieやlocalStorageなどはこのIDに関連付けて保存される。また、拡張コンテキスト上でlocation.hostをみるとやはりIDが使われている。
なお、拡張のスキームはchrome-extension://である。IDと組み合わせて、例えば chrome-extension://dnlfpnhinnjdgmjfpccajboogcjocdla/images/favicon32.png がフルパスとなる。
通常のウェブページと同様で、ドメインが違えばクロスドメインの制約を受けるので、拡張間でのXMLHttpRequestなどはできない。
manifest.json
{
"name": "simple note",
"description":"簡易メモ帳です",
"browser_action": {
"default_icon": "pad19.png",
"default_title": "simple note",
"popup": "popup.html"
},
"icons": {
"128": "pad128.png",
"48": "pad48.png"
},
"permissions": ["tabs" ,"http://*/", "https://*/"],
"version": "0.0.1"
}
拡張を定義するjsonファイル。名前、バージョンのほか、アイコンや拡張の持つ権限、ContentScriptsを実行するURLの定義など。
manifest.jsonで宣言していない機能は使用できない。(URLのほか、tabs/bookmarksに、Chrome5からはnotifications/historyとexperimentalを指定できます)
セキュリティを高めつつ、機械的にその拡張の権限を判断して、インストールするユーザーに警告を出すことができる。
Chrome拡張の作り方
拡張を作るにあたって必要なものは特にありません。あえて挙げるなら、アイコンは用意しておきましょう。
まず、manifest.jsonを用意します。
{
"name": "Hello",
"description": "Chrome拡張入門",
"version": "0.1"
}
このmanifest.jsonだけで拡張として読み込むことができます。もちろん、何の機能もありません。
{
"name": "Hello World",
"description": "Chrome拡張入門",
"version": "0.1",
"browser_action": {
"default_icon": "icon.png",
"popup": "popup.html"
},
"icons": {
"48": "icon.png"
}
}
browser_actionとiconsを追加しました。popupには以下のhtmlを用意しました。
<!doctype html>
<html>
<style>
*{
margin: 0px;
padding: 0px;
}
body > h1{
width:300px;
text-align:center;
background: -webkit-gradient(
linear, left top, left bottom, from(#06c), to(#39f)
);
border-radius:4px;
margin:0px;
padding:1em;
color:white;
text-shadow: 3px 3px 3px black;
line-height:1.4;
}
</style>
<h1>Chrome extensions</h1>
<script>
document.querySelector('h1').onclick=function(){
this.innerText = 'Chrome拡張入門';
}
</script>
</html>
パッケージ化
chrome://extensions/ で「拡張機能のパッケージ化」からパッケージを作成できます。
Constellation's crxmake at master - GitHubでCUI環境でもパッケージングができます。
出来上がったcrxファイルをChromeにドロップすると、インストールダイアログが現れて、拡張としてインストールできます。インストールした拡張は(Windowsの場合)以下のパスに保存されています。
//XP
"C:\Documents and Settings\[USERNAME]\Local Settings\Application Data\Google\Chrome\User Data\Default\Extensions"
//7
"C:\Users\[USERNAME]\AppData\Local\Google\Chrome\User Data\Default\Extensions"
インストールした拡張はIDが開発時と変わってしまいます。その代わりにインストール後のmanifest.jsonには"key"というフィールドが追加されています。このkeyがあるとパッケージ化されていない拡張を読み込んだ際もパッケージ化した拡張のIDと同じものが使用できます。
なお、.crxファイルはzip形式なので、解凍することができます。
本格的な拡張の概要
では、少し本格的な拡張を紹介します。今回はChrome Stylistをサンプルとして使用します
- manifest.json
-
{
"name": "Chrome Stylist",
"description": "Chrome Stylist",
"version": "2.0.3",
"permissions": [ "tabs" , "http://*/", "https://*/"],
"update_url": "http://ss-o.net/chrome_extension/ChromeStylist/updates.xml",
"key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6OrLV1xhHM3WMEydnMRoMZzDc1K/BQY7LfsL8qDmmq+XyBCf6vpEI1xSUP23ckblC5rG6baH/sp6gbRnFLNiu+gOT4e9UYgG71o+WSqeAkKO9w0JLZFguX/Ft89858MdRM3zcCORakNYj3Ux9QBcupYKfLTvF6d6PcsdibxenSwIDAQAB",
"background_page": "background.html",
"options_page": "options_page.html",
"icons": {
"128": "stylist128.png",
"48": "stylist48.png"
},
"page_action": {
"default_icon": "stylist16.png",
"default_title": "add new style",
"popup" : "popup.html"
},
"content_scripts": [
{
"js": [
"stylist.js"
],
"matches": [
"http://*/*",
"https://*/*",
"ftp://*/*"
],
"run_at": "document_start",
"all_frames":true
}
]
}
- background.html
- スタイルの定義を変数として保持しておき、URLをContent Scriptから受け取ったらそのURLにマッチするスタイルを返す
スタイルのインストール処理も行う
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<title>Chrome Stylist</title>
<script>
const NONE = 0;
const ALL = 1;
const PART = 2;
this.Stylists = [
{
"name": "demo",
"src": "chrome.stylist.0",
"match": {
"pattern": "^http://ss-o\\.net"
},
"css": "h1:before{content:'hello! ';}"
}
];
this.Config = {
status_icon:PART
}
if (localStorage.Stylists){
// すでに値を持っていたら復元
Stylists = JSON.parse(localStorage.Stylists);
} else {
// 初回の初期化
localStorage.Stylists = JSON.stringify(Stylists);
}
if (localStorage.Config){
Config = JSON.parse(localStorage.Config);
} else {
localStorage.Config = JSON.stringify(Config);
}
// URLにマッチするスタイルを取得
function getStyle(url){
var css = [];
Stylists.forEach(function(style){
if (!style.disabled && new RegExp(style.match.pattern).test(url)) {
css.push(style.css);
}
});
return css.join('\n');
}
// ContentScriptsからのリクエスト処理
chrome.extension.onRequest.addListener(function(request, sender, sendResponse){
if (request.href) {
if (Config.status_icon === ALL){
chrome.pageAction.show(sender.tab.id);
}
var css = getStyle(request.href);
if (css) {
sendResponse({css:css});
if (Config.status_icon === PART){
chrome.pageAction.show(sender.tab.id);
}
}
} else if (request.src) {
var xhr = new XMLHttpRequest();
var src = request.src;
xhr.open('GET', src, true);
xhr.onload = function(){
var css = xhr.responseText;
var res = false;
if (request.type === '.user.css') {
res = usercss_parser(css, src);
} else if (request.type === 'userstyles.org') {
res = userstyles_parser(css, src, request);
}
if (res) {
sendResponse(res);
} else {
sendResponse('Sorry, install was failed.');
}
};
xhr.onerror = function(){
console.error(xhr);
};
xhr.send(null);
}
});
var meta_def = {
"name":1,
"namespace":1,
"match-type":1,
"match-pattern":1,
"src":1,
};
function userstyles_parser(css, src, info){
var __i = Stylists.length;
var isUpdate = false;
while(__i--) {
if (Stylists[__i].src === info.src) {
Stylists.splice(__i, 1);
isUpdate = true;
}
}
var comments = [];
var _css = css.replace(/\/\*[\s\S]*?\*\//g,function(_,_c){
comments.push(_);
return '/*####COMMENT' + (comments.length-1) + '####*/';
});
var parts = _css.split('@-moz-document');
var isInstall = false;
var global_styles = [];
parts.forEach(function(part,index){
if (index === 0){
if (/{[^}]*}/.test(part)) {
global_styles.push(part.replace(/@namespace\s+(\w*\s+)?url\(['"]?([^'")]*)['"]?\);/,''));
}
return;
}
var start = part.indexOf('{');
var end = part.lastIndexOf('}');
var meta_text = part.substring(0, start);
var css_text = part.substring(start+1, end);
var _global = part.substring(end+1).trim();
if (_global && /{[^}]*}/.test(_global)) {
global_styles.push(_global);
}
if (!meta_text || !css_text) return;
var prefixs = [], tmp;
while((tmp = /url-prefix\(['"]?([^'")]*)['"]?\)/g.exec(meta_text))) {
prefixs.push(tmp[1].trim());
}
var domains = [];
while((tmp = /domain\(['"]?([^'")]*)['"]?\)/g.exec(meta_text))) {
domains.push(tmp[1].trim());
}
var urls = [];
while((tmp = /url\(['"]?([^'")]*)['"]?\)/g.exec(meta_text))) {
urls.push(tmp[1].trim());
}
var patterns = [], plains = [];//, match_type = 'prefix';
if (prefixs.length) {
patterns.push('^(' + prefixs.map(function(s){return s.replace(/\W/g,'\\$&')}).join('|') + ')');
prefixs.forEach(function(v){plains.push({type:'prefix',value:v});});
}
if (domains.length) {
patterns.push('^https?://[^/]*' + domains.map(function(s){return s.replace(/\W/g,'\\$&')}).join('|') + '/');
domains.forEach(function(v){plains.push({type:'domain',value:v});});
//match_type = 'domain';
}
if (urls.length) {
patterns.push('^' + urls.map(function(s){return s.replace(/\W/g,'\\$&')}).join('$|^') + '$');
urls.forEach(function(v){plains.push({type:'url',value:v});});
//match_type = 'url';
}
if (!plains.length){
return;
}
var _info = clone(info);
_info.match = {
pattern:'('+patterns.join('|')+')'
,plains:plains
//,type:match_type
};
_info.css = css_text.replace(/\/\*####COMMENT(\d*)####\*\//g,function(_,_s){
return comments[_s] || '';
});
_info.id = Stylists.length++;
Stylists.push(_info);
isInstall = true;
});
if (global_styles.length) {
var _info = clone(info);
_info.match = {
pattern:'.'
,plains:[]
,global:true
};
_info.css = global_styles.join('\n').replace(/\/\*####COMMENT(\d*)####\*\//g,function(_,_s){
return comments[_s] || '';
});
_info.id = Stylists.length;
Stylists.push(_info);
isInstall = true;
}
if (isInstall) {
localStorage.Stylists = JSON.stringify(Stylists);
return isUpdate ? info.name + ' is updated.' : info.name + ' is now installed.';
}
}
function usercss_parser(css, src){
var rows = css.split(/[\n\r]+/g);
var metainf = {}, _begin = false;
rows.some(function(txt){
if (/==UserStyle==/.test(txt)){
_begin = true;
} else if (_begin) {
if (/==\/UserStyle==/.test(txt)){
return true;
} else {
var meta = txt.match(/^\/\/\s*@([-\w]+)\s+(.*)/);
if (meta && meta_def[meta[1]]){
metainf[meta[1]] = meta[2];
}
}
}
});
if (metainf.name){
var check, index = -1;
check = Stylists.some(function(sty,i){
index = i;
return sty.name === metainf.name;
});
var type = 'prefix';
var pattern = '^https?://';
if (metainf['match-type']) {
type = metainf['match-type'];
delete metainf['match-type'];
}
if (metainf['match-pattern']) {
pattern = metainf['match-pattern'];
delete metainf['match-pattern'];
}
if (!metainf['src'] || metainf['src'].indexOf('http') !== 0) {
metainf.src = request.src;
}
if (type === 'prefix') {
pattern = '^' + pattern.replace(/\W/, '\\$&');
}
metainf.match = {
pattern: pattern,
plains:[pattern],
type:type
};
metainf.css = css;
metainf.id = Stylists.length;
if (check) {
Stylists[index] = metainf;
} else {
Stylists.push(metainf);
}
localStorage.Stylists = JSON.stringify(Stylists);
return 'installed';
}
}
function clone(o){
return JSON.parse(JSON.stringify(o));
}
get_manifest(function(manifest){
window.Manifest = manifest;
});
function get_manifest(callback){
var url = './manifest.json';
var xhr = new XMLHttpRequest();
xhr.onload = function(){
callback(JSON.parse(xhr.responseText));
};
xhr.open('GET',url,true);
xhr.send(null);
}
</script>
<body>
</body>
</html>
- options_page.html
- インストール済みスタイルの管理画面
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Chrome Stylist</title>
<script src="x.js"> </script>
<script src="tween2.js"></script>
<script src="stylist.js"></script>
<link href="option_page.css" rel="stylesheet" type="text/css">
<body>
<ul class="tabs" id="menu_tabs">
<li class="basics"><a href="#basics" class="active"><span>Basics</span></a>
<li class="styles"><a href="#styles"><span>Styles</span></a>
<li class="about"><a href="#about"><span>About</span></a>
</ul>
<div id="container">
<div id="inner_container">
<section id="basics" class="content">
<p><img src="stylist19.png">Stylist icon</p>
<p class="sub"><input type="radio" id="status_icon_none" value="0" name="status_icon"><label for="status_icon_none">none</label></p>
<p class="sub"><input type="radio" id="status_icon_part" value="2" name="status_icon"><label for="status_icon_part">matched</label></p>
<p class="sub"><input type="radio" id="status_icon_all" value="1" name="status_icon"><label for="status_icon_all">always</label></p>
</section>
<section id="styles" class="content">
<div id="left_box">
<ul id="style_list">
</ul>
<button id="add_style">Add New Style</button>
<p>search from <a href="http://userstyles.org/">userstyles.org</a></p>
</div>
<div id="right_box">
</div>
</section>
<section id="about" class="content">
<dl>
<dt>Name:<dt>
<dd><a href="http://ss-o.net/chrome_extension/" target="_blank">Chrome Stylist</a></dd>
<dt>Version:<dt>
<dd>ver <span id="ExtensionVersion"></span></dd>
<dt>Author:<dt>
<dd><a href="http://ss-o.net/" target="_blank">os0x</a></dd>
<dt>License:<dt>
<dd><a href="http://creativecommons.org/licenses/MIT/" target="_blank">The MIT license</a></dd>
</dl>
</section>
</div>
</div>
<script>
window.onload = function(){
setTimeout(__onload, 100);
};
function __onload(){
BG = chrome.extension.getBackgroundPage();
Stylists = BG.Stylists;
Config = BG.Config;
indexStylist = BG.indexStylist;
$X('//section/p/input[@type="radio"]').forEach(function(box){
var name = box.name;
var val = Config[name];
if (val === Number(box.value)) {
box.checked = true;
}
box.addEventListener('click',function(){
Config[name] = Number(box.value);
localStorage.Config = JSON.stringify(Config);
},false);
});
var add = document.getElementById('add_style');
var add_style = function(){
var id = ++indexStylist;
BG.indexStylist = indexStylist;
var styl = {
name:'',
match:{
//type:'prefix',
pattern:'',
plains:[{type:'prefix',value:''}]
},
css:'',
src:'chrome.stylist.' + id,
id:id
};
var button = create_style_list(styl, id);
while(right_box.firstChild) right_box.removeChild(right_box.firstChild);
create_style_form(styl, undefined, button);
};
add.addEventListener('click', add_style,false);
var right_box = document.getElementById('right_box');
var style_list = document.getElementById('style_list');
var Dup = {};
Stylists.forEach(create_style_list);
function create_style_list(styl,index){
var button;
if (styl.src && Dup[styl.src]) {
var set = Dup[styl.src];
button = set.button;
set.styles.push(styl);
button.addEventListener('click',function(){
create_style_form(styl, index, button, true);
},false);
return;
}
var li = document.createElement('li');
button = document.createElement('button');
button.textContent = styl.name;
button.id = 'button_' + styl.name.replace(/\W/g,'-');
if (styl.src) {
Dup[styl.src] = {button:button,styles:[styl]};
}
button.addEventListener('click',function(){
while(right_box.firstChild) right_box.removeChild(right_box.firstChild);
create_style_form(styl, index, button);
},false);
li.appendChild(button);
style_list.appendChild(li);
return button;
}
function create_style_form(styl, index, button, notitle){
if (!notitle) {
var h3 = document.createElement('h3');
var name = document.createElement('input');
name.type = 'text';
name.value = styl.name;
name.id = 'current_name';
name.placeholder = 'Style Name';
name.addEventListener('input',function(e){
button.textContent = name.value;
},false)
h3.appendChild(name);
var addset = document.createElement('button');
addset.textContent = 'new style set';
addset.className ='add';
addset.addEventListener('click',function(){
var new_style = clone(styl);
new_style.match = {type:'prefix',pattern:'',plains:['']};
new_style.css = '';
delete new_style.id;
create_style_form(new_style, undefined, button, true);
},false);
var deleteAll = document.createElement('button');
deleteAll.textContent = 'delete ALL';
deleteAll.className = 'top';
deleteAll.addEventListener('click',function(){
if (confirm('Are sure you want to delete this style? There is NO undo!')) {
var src = styl.src;
BG.Stylists = Stylists = Stylists.filter(function(_sty,i){
if (_sty.src === src) {
styl.deleted = true;
return false;
} else {
return true;
}
});
localStorage.Stylists = JSON.stringify(Stylists);
var li = $X('parent::li[parent::ul]', button)[0];
if (li && li.parentNode) {
li.parentNode.removeChild(li);
}
while(right_box.firstChild) right_box.removeChild(right_box.firstChild);
}
},false);
right_box.appendChild(h3);
if (styl.original) {
var source = document.createElement('button');
source.className = 'top';
source.addEventListener('click',function(){
chrome.tabs.create({url:styl.original});
},false);
source.textContent = 'original source';
right_box.appendChild(source);
}
var disable = document.createElement('button');
disable.className = 'top';
disable.addEventListener('click',function(){
Array.prototype.slice.call(right_box.querySelectorAll('input,select,textarea,button:not(.top)')).forEach(function(f){
f.disabled = !styl.disabled;
});
Dup[styl.src].styles.forEach(function(styl){
styl.disabled = !styl.disabled;
});
localStorage.Stylists = JSON.stringify(Stylists);
disable.textContent = styl.disabled ? 'enable' : 'disable';
},false);
disable.textContent = styl.disabled ? 'enable' : 'disable';
right_box.appendChild(disable);
right_box.appendChild(deleteAll);
right_box.appendChild(addset);
}
if (!styl.match.plains) {
styl.match.plains = [{type:'regexp',value:styl.match.pattern}];
}
var field = document.createElement('fieldset');
var h4 = document.createElement('h4');
h4.textContent = 'style set';
field.appendChild(h4);
var div1 = document.createElement('div');
field.appendChild(div1);
var _select = document.createElement('select');
//select.disabled = true;
_select.className = 'match';
var opt_pre = document.createElement('option');
opt_pre.value = 'prefix';
opt_pre.label = 'prefix';
var opt_reg = document.createElement('option');
opt_reg.value = 'regexp';
opt_reg.label = 'regexp';
var opt_dom = document.createElement('option');
opt_dom.value = 'domain';
opt_dom.label = 'domain';
_select.appendChild(opt_pre);
_select.appendChild(opt_dom);
_select.appendChild(opt_reg);
var _plains = styl.match.plains.slice();
var rule_box = document.createElement('div');
field.appendChild(rule_box);
var rules = [];
var LABEL = {prefix:'URL Prefix', domain:'Site Domain', regexp:'URL RegExp', global:''};
field.className = styl.match.type;
var global = document.createElement('input');
global.type = 'checkbox';
global.id = 'global_checkbox' +index;
if (!styl.match.global && styl.match.type === 'global'){
styl.match.global = true;
delete styl.match.type;
}
global.checked = !!styl.match.global;
global.addEventListener('click',function(){
if(global.checked){
rule_box.style.display = 'none';
} else {
rule_box.style.display = 'block';
}
},false);
if(global.checked){
rule_box.style.display = 'none';
} else {
rule_box.style.display = 'block';
}
var global_label = document.createElement('label');
global_label.textContent = 'All site';
global_label.htmlFor = 'global_checkbox' + index;
div1.className = 'global';
div1.appendChild(global);
div1.appendChild(global_label);
var rule_ul = document.createElement('ul');
rule_box.appendChild(rule_ul);
var rule_add = document.createElement('button');
rule_add.textContent = 'add Rule';
rule_add.className = 'patterns addrule';
rule_add.addEventListener('click',function(evt){
var rule = {type:'prefix',value:''};
_plains.push(rule);
create_rule(rule, _plains.length-1);
},false);
rule_box.appendChild(rule_add);
var create_rule = function(plain, _i, plains){
if (plain.del) return;
if (!plain.type) {
var _plain = plain;
plain = {type:'prefix', value:_plain};
plains[_i] = plain;
}
var rule_list = document.createElement('li');
var select = _select.cloneNode(true);
select.addEventListener('change',function(){
pattern.placeholder = LABEL[select.value];
plain.type = select.value;
},false);
select.value = plain.type;
var pattern = document.createElement('input');
if (plain.type === 'global') {
pattern.type = 'hidden';
} else {
pattern.type = 'text';
}
pattern.placeholder = LABEL[plain.type];
pattern.className = 'patterns input';
pattern.value = plain.value;
rule_list.appendChild(select);
rule_list.appendChild(pattern);
pattern.addEventListener('change',function(){
plain.value = pattern.value;
},false);
rules.push({pattern:pattern,type:select,plain:plain});
var del = document.createElement('button');
del.textContent = 'del';
del.className = 'patterns del';
var isDel = false;
rule_list.appendChild(del);
del.addEventListener('click',function(){
isDel = !isDel;
if (isDel) {
pattern.disabled = true;
del.textContent = 'Undo';
} else {
pattern.disabled = false;
del.textContent = 'del';
}
});
rule_ul.appendChild(rule_list);
};
_plains.forEach(create_rule);
var css = document.createElement('textarea');
css.value = styl.css;
css.placeholder = 'Stylesheet Text';
field.appendChild(css);
var save = document.createElement('button');
save.textContent = 'Save';
save.addEventListener('click',function(){
var regs = [], errors = [];
if(global.checked){
regs = ['.'];
_plains = [];
styl.match.global = true;
} else {
styl.match.global = false;
rules.forEach(function(rule,i){
console.log(rule.pattern,rule.plain);
if (rule.pattern.disabled) {
//_plains.splice(i,1);
rule.plain.del = true;
return;
}
rule.plain.del = false;
var plain = rule.plain;
if (!plain.value) {
errors.push({message:'cannot blank pattern',element:rule.pattern});
}
/*if (plain.type === 'regexp' || plain.type === 'global') {
regs.push(plain.value);
} else {
regs.push(plain.value.replace(/\W/g,'\\$&'));
}*/
if (plain.type === 'regexp') {
regs.push(plain.value);
} else if (plain.type === 'prefix') {
regs.push('^(' + plain.value.replace(/\W/g,'\\$&') + ')');
} else if (plain.type === 'domain') {
regs.push('^https?://[^/]*' + plain.value.replace(/\W/g,'\\$&') + '/');
} else if (plain.type === 'url') {
regs.push('^' + plain.value.replace(/\W/g,'\\$&') + '$');
} else if (plain.type === 'global') {
regs.push('.');
}
});
}
if (errors.length) {
errors[0].element.focus();
return;
}
if (!regs.length) {
message.textContent = 'Rule cannot be blank!';
setTimeout(function(){message.textContent='';},3000);
field.querySelector('button.del,button.addrule').focus();
return;
}
styl.match.plains = _plains;
var _name = name || document.getElementById('current_name');
if (!_name.value) {
message.textContent = 'Name cannot be blank!';
setTimeout(function(){message.textContent='';},3000);
_name.focus();
return;
}
styl.css = css.value;
styl.name = _name.value;
//styl.match.type = select.value;
styl.match.pattern = regs.join('|');
if (index === undefined || styl.deleted) {
index = Stylists.length;
styl.id = indexStylist++;
BG.indexStylist = indexStylist;
styl.deleted = false;
Stylists.push(styl);
}
localStorage.Stylists = JSON.stringify(Stylists);
message.textContent = 'Saved!';
setTimeout(function(){message.textContent='';},3000);
},false);
field.appendChild(save);
var del = document.createElement('button');
del.textContent = 'del';
del.addEventListener('click',function(evt){
//if (index >= 0) {
right_box.removeChild(field);
if (!document.querySelector('#right_box > fieldset')) {
var li = $X('parent::li[parent::ul]', button)[0];
if (li && li.parentNode) {
li.parentNode.removeChild(li);
}
while(right_box.firstChild) right_box.removeChild(right_box.firstChild);
}
Stylists.some(function(_sty,i){
if (_sty.id === styl.id) {
Stylists.splice(i, 1);
styl.deleted = true;
return true;
}
});
localStorage.Stylists = JSON.stringify(Stylists);
/*} else {
var li = $X('parent::li[parent::ul]', button)[0];
if (li && li.parentNode) {
li.parentNode.removeChild(li);
}
while(right_box.firstChild) right_box.removeChild(right_box.firstChild);
}*/
});
field.appendChild(del);
var message = document.createElement('span');
field.appendChild(message);
right_box.insertBefore(field, right_box.lastChild);
Array.prototype.slice.call(right_box.querySelectorAll('input,select,textarea,button:not(.top)')).forEach(function(f){
f.disabled = styl.disabled;
});
}
function clone(o){
return JSON.parse(JSON.stringify(o));
}
var WIDTH = 800;
var HEIGHT = Math.max(window.innerHeight - 35, 300);
document.getElementById('ExtensionVersion').textContent = BG.Manifest.version;
var sections = $X('/html/body/div/div/section[contains(@class, "content")]');
var inner_container = document.getElementById('inner_container');
var container = document.getElementById('container');
inner_container.style.width = sections.length * (WIDTH+20) + 'px';
//inner_container.style.height = HEIGHT + 'px';
//container.style.height = HEIGHT + 'px';
container.style.marginTop = '-2px';
sections.forEach(function(section, _i){
section.style.visibility = 'hidden';
section.style.height = '100px';
});
var btns = $X('id("menu_tabs")/li/a');
var default_title = document.title;
btns.forEach(function(btn, i, btns){
btn.addEventListener('click',function(evt){
evt.preventDefault();
btns.forEach(function(btn){btn.className = '';})
btn.className = 'active';
sections[i].style.visibility = 'visible';
sections[i].style.height = 'auto';
new Tween(inner_container.style, {marginLeft:{to:i * -WIDTH,tmpl:'$#px'},time:0.2,onComplete:function(){
document.title = default_title + btn.hash;
location.hash = btn.hash;
window.scrollBy(0, -1000);
sections.forEach(function(section, _i){
if (i !== _i) {
section.style.visibility = 'hidden';
section.style.height = '100px';
}
});
}});
}, false);
});
if (location.hash) {
sections.some(function(section, i){
if ('#' + section.id === location.hash) {
btns.forEach(function(btn){btn.className = '';})
btns[i].className = 'active';
inner_container.style.marginLeft = -WIDTH * i + 'px';
section.style.visibility = 'visible';
section.style.height = 'auto';
document.title = default_title + location.hash;
}
});
} else {
sections[0].style.height = 'auto';
sections[0].style.visibility = 'visible';
document.title = default_title + '#' + sections[0].id;
}
};
</script>
<script type="text/javascript" src="chrome-extension://okneonigbfnolfkmfgjmaeniipdjkgkl/chrome_keyconfig.js"></script>
<script type="text/javascript" src="chrome-extension://jpkfjicglakibpenojifdiepckckakgk/chrome_gestures.user.js"></script>
</body>
</html>
- popup.html
- 現在表示しているページでスタイルを適用しつつ保存もできるインターフェース
- stylist.js
- ContentScriptsとして実行されるJSファイル。表示されたURLをBackgroundに送り、取得したスタイルを適用させる
var style = document.createElement('style');
style.id = 'ChromeStylist-pabfempgigicdjjlccdgnbmeggkbjdhd';
chrome.extension.sendRequest({href:location.href},function(res){
style.appendChild(document.createTextNode(res.css));
(document.head || document.documentElement).appendChild(style);
});
const USER_STYLE_ORG = 'userstyles.org';
if (location.host === USER_STYLE_ORG) {
document.addEventListener('DOMContentLoaded',function(){
var point = document.querySelector('#style-install-chrome > p:last-child');
if (!point) return;
var p = point.cloneNode(false);
point.parentNode.appendChild(p);
var button = document.createElement('button');
button.textContent = 'Install with Stylist';
p.appendChild(button);
button.addEventListener('click',function(evt){
chrome.extension.sendRequest({
type: USER_STYLE_ORG,
src: 'http://' + USER_STYLE_ORG + location.pathname + '.css',
name: document.querySelector('h1').textContent.trim(),
original:location.href
}, install_response);
});
},false);
} else {
document.addEventListener('DOMContentLoaded',function(){
var css = document.querySelectorAll('a[href$=".user.css"]');
for (var i = 0, len = css.length;i < len;i++){
var cs = css[i];
cs.addEventListener('click',function(evt){
if (confirm('Install?')){
evt.preventDefault();
chrome.extension.sendRequest({
type: '.user.css',
src: this.href
}, install_response);
}
},false);
}
},false);
}
function install_response(res){
alert(res);
}
- アイコン
以上のファイルを1つのフォルダに置き、拡張機能ページで「デベロッパーモード」選択し、パッケージ化されてない拡張の読み込みを行います
拡張のデバッグ
Web Inspectorを使用する(参考:続・先取り! Google Chrome Extensions:第6回 Firebug要らずなChromeのWeb Inspector|gihyo.jp … 技術評論社)
Chrome拡張で使えるHTML5
ChromeのHTML5対応:関連API
- canvas
- 対応済み
- video, audio
- 対応済み(対応フォーマットは前述の通り)
- Web Workers
- 対応済み
- Web Storage
- Chrome4ではlocalStorageのみ対応、Chrome5でsessionStorageも対応予定
- コミュニケーションAPI
- 対応済み(ただし、Chrome4ではテキストのみの送受信)
- Web Sockets
- 対応済み(ただし、WokerからのSocketsはChrome5で対応予定)
- HTML Ruby
- 対応済み
- Web Fonts
- 対応済み
- Web SQL Database
- 対応済み(残念ながら標準化はストップしてしまいましたが…)
- Desktop notifications
- Chrome5で正式対応予定
- HTML Forms
- 一部のみ対応、Chrome5ではバリデーションなどを追加、UIはChrome6以降
- Application Cache
- Chrome5で対応予定
- Geolocation API
- Chrome5で対応
- Drag & drop
- 一部実装されているが、Chrome5で完全に対応する予定
- File API
- Chrome5で対応予定
- Indexed Database API
- Chrome6で対応予定、2010/3/11に動作したとの第一報あり
最新の情報は、Web Platform Status (The Chromium Projects)にまとまっています
ChromeのHTML5対応:HTMLパーサー
<p>第1パラグラフ
<section>セクション</section>
<p>第2パラグラフ
わざとらしい例ですが、パーサーがsectionに対応しているか否かで、下記のように解釈が変わります。
| Chrome | Firefox |
| section要素未対応 |  Chrome4 |  Firefox3.6 |
| section要素対応 |  Chrome5 |  Firefox3.6(html5.enable) |
Chrome拡張からHTML5へ
元々、Chrome拡張のAPIの仕様だったDesktop Notificationsが、2010年1月にW3C Editor's Draftとして(Web Notifications)公開されました。
このように、Chrome拡張は各種APIがHTML5をベースとしており、同時に、Chrome拡張からHTML5にフィードバックする、という相互作用の関係があります
HTML5を使いたい!という方は、是非Chrome拡張を!!
Appendix
Chrome Extensionsの参考URL
この資料についてのお話
この資料自体がHTML5のサンプルとなるよう作ってあります。
- HTML5 Validator
- Validator.nu (X)HTML5 Validator
(X)HTML5 validation results for http://ss-o.net/chrome/extension/introduction.html
- HTML5 Outliner
- HTML 5 Outliner
Outline
- Outlineと目次
- HTML5 のセクションアウトラインを生成してみよう! - IT戦記のOutliner.jsを使用して、右上の目次を自動生成しています。

This work is in the Public Domain.