この記事で取り上げているAPIは現在と使い方が異なっていたり、
特にToolstrips APIは最新のChromeでは使用できなくなっています。詳しくは
前回はExtensionsの作り方からドキュメント、
前回の復習とバグフィックス
前回作成したSBMカウンタは、
一つ目のバグは、
このとき、
chrome.tabs.onSelectionChanged.addListener(function(tabid){
  chrome.tabs.get(tabid, function(tab){
    if (tab.windowId === current_window.id) {
      run_on_tab(tab);
    }
  });
});
続いて、
chrome.tabs.onUpdated.addListener(function(tabid, inf){
  if (inf.status === 'loading') {
    chrome.tabs.getSelected(current_window.id, function(tab){
      if (tab.id === tabid) {
        run_on_tab(tab, true);
      } else {
        run_on_tab(tab, false, true);
      }
    });
  }
});
なお、
Page Actions と Background Pages
続いて、
  "page_actions": [
    {
      "id": "hatena",
      "name": "add hatena bookmark",
      "icons": [
        "hatena.favicon.gif"
      ]
    },
    {
      "id": "delicious",
      "name": "add delicious",
      "icons": [
        "delicious.small.gif"
      ]
    }
  ],
idはそのpage_
続いて、
  "background_page": "background.html",
では、
var PageActionServices = [
   {
      "id":"hatena",
      "title":"add hatena bookmark",
      "url":"http://b.hatena.ne.jp/add?&url=#{encoded_url}&title=#{title}"
   },
   {
      "id":"delicious",
      "title":"add delicious",
      "url":"http://delicious.com/save?v=5&jump=close&url=#{encoded_url}&title=#{title}"
   }
];
このサービスの定義はToolstripsで使用している定義と共有する形にしてもよいのですが、
続いて、
chrome.tabs.onUpdated.addListener(function(tabid, inf){
  if (inf.status !== 'loading') return;
  chrome.tabs.get(tabid, function(tab){
    if (!/^http/.test(tab.url)) return;
    PageActionServices.forEach(function(service){
      var opt = {
        tabId:  tab.id,
        url:    tab.url,
        title:  service.title,
        iconId: 0
      };
      chrome.pageActions.enableForTab(service.id, opt);
    });
  });
});
ここでもTabs APIのonUpdatedイベントでタブを取得し、
これでアドレスバー
PageActionServices.forEach(function(service){
  chrome.pageActions[service.id].addListener(function(id, tabinf){
    if (!tabinf){//for Chrom 3
      tabinf = id.data;
      id = id.pageActionId;
    }
    chrome.tabs.get(tabinf.tabId,function(tab){
      var inf = {
        encoded_url:encodeURIComponent(tab.url),
        title:tab.title
      };
      var opt = {
        url: fill(service.url, inf),
        selected: true
      };
      chrome.tabs.update(tab.id,opt);
    });
  });
});
function fill(str, opt){
  function replacer(_, _$){
    return opt[_$] || '';
  }
  return str.replace(/#\{([^}]+)\}/g, replacer);
}
chrome.
あとはタブIDからTabオブジェクトを取得し、
Content Scripts API
続いて、
まず、
  "content_scripts": [
    {
      "js": [
        "x.js",
        "bookmark.js"
      ],
      "css": [
        "bookmark.css"
      ],
      "matches": [
        "http://*/*"
      ]
    }
  ],必須のプロパティはmatchesのみです。どれも文字列の配列で、
matches の http://*/* という記述は、
続いて、
XPathはCSSセレクタ以上に柔軟に指定の要素を選択できるので、
では、
それでは、
var connection = chrome.extension.connect();
connection.onMessage.addListener(function(info, con){
  console.log(info, con);
});
connection.postMessage({url:location.href});chrome.
また、
Background Pageの応用
Background PageはChromeが起動時に読み込まれ、
では、
var Siteinfo = [
  'http://b.st-hatena.com/file/HatenaBookmarkUsersCount.items.json',
  'http://ss-o.net/json/wedataLDRize.json'
];
var bookinfo = [];
(function getSiteinfo(url){
  var xhr = new XMLHttpRequest;
  xhr.open('GET',url,true);
  xhr.onload = function(){
    try {
      var i = JSON.parse(xhr.responseText);
      i.forEach(function(s){
        bookinfo.push(s.data);
      });
    } catch (e) {
      console.error(e);
    }
    if (Siteinfo.length) {
      getSiteinfo(Siteinfo.shift());
    }
  };
  xhr.onerror = function(){
    if (Siteinfo.length) {
      getSiteinfo(Siteinfo.shift());
    }
  };
  xhr.send(null);
})(Siteinfo.shift());取得先のURLを配列で定義し、
続いて、
chrome.self.onConnect.addListener(function(port,name) {
  port.onMessage.addListener(function(info,con){
    SeachSiteinfo(info, con);
  });
});まずchrome.
function SeachSiteinfo(info, con){
  var _info = [], url = info.url;
  for (var i = 0,len = bookinfo.length;i < len;i++){
    var inf = bookinfo[i], matcher = null;
    if (inf.domain) {
      try {
        matcher = new RegExp(inf.domain);
      } catch(e){}
    }
    if (matcher && matcher.test(url)){
      if (inf.disable){
        return;
      }
      if (inf.paragraph && inf.link) {
        _info.push(inf);
      }
    } else if (matcher === null) {
    }
  }
  if (_info.length) {
    con.postMessage({"matched":_info});
  }
}定義データのdomainを正規表現オブジェクトに変換してURLとの一致をチェックしています。URLとマッチした時にdisableがtrueである場合は処理を終了します。そうなければ、
定義データを受け取ったContent Script側では、
var siteinfo = [];
var CountItem = {};
connection.onMessage.addListener(function(info){
  siteinfo = info.matched;
  SeachInlineLinks(document);
});
function SeachInlineLinks(doc){
  var links = [];
  siteinfo.forEach(function(info){
    $X(info.paragraph, doc).forEach(function(paragraph){
      var link = $X(info.link, paragraph)[0];
      if (!link) return;
      if (!CountItem[link.href]) {
        links.push(link.href);
        CountItem[link.href] = [link];
      } else {
        CountItem[link.href].push(link);
      }
    });
  });
  connection.postMessage({links:links});
}CountItemは同じURLが複数あった場合に1つにまとめる役割を担っています。そうして抽出されたURLを再度Background Pageに渡します。
chrome.self.onConnect.addListener(function(port,name) {
  port.onMessage.addListener(function(info,con){
    if (info.url) {
      SeachSiteinfo(info, con);
    } else if (info.links) {
      GetLinkCount(info, con);
    }
  });
});
function GetLinkCount(info, con) {
  Services.forEach(function(service){
    service.link_count_request(info, con, service);
  });
}先ほどは受信したメッセージを無条件でSeachSiteinfo関数に渡していましたが、
複数のURLに対してブックマーク数を取得するには、
では、
var parser = new DOMParser(), text = xhr.responseText;
var doc = parser.parseFromString(text, 'text/xml');
var res = [];
$X('//member',doc).forEach(function(member,i){
  var vals = $X('*',member);
  res.push({
    url: vals[0].textContent,
    total_posts: vals[1].textContent
  });
});
con.postMessage({
  "count":{
    "id": "hatena",
    "items": res,
    "api_link": service.api_link
  }
});はてなブックマーク件数取得APIで取得したXMLをパースし、
var api = 'http://feeds.delicious.com/v2/json/urlinfo';
var param = 'hash=' + info.links.map(function(url){
  return encodeURIComponent(md5.hex(url));
}).join('&hash=');
var xhr = new XMLHttpRequest();
xhr.open('post',api + '?' + param, true);
xhr.onload = function(){
  var res = JSON.parse(xhr.responseText);
  con.postMessage({
    "count":{
      "id": "delicious",
      "items": res,
      "api_link": service.api_link
    }
  });
};
xhr.send(null);deliciousのAPIはURLをmd5に変換する必要があるものの、
connection.onMessage.addListener(function(info){
  if (info.matched) {
    siteinfo = info.matched;
    SeachInlineLinks(document);
  } else if (info.count) {
    Counter(info.count);
  }
});
function Counter(data){
  var id = data.id, api_link = data.api_link;
  var class_name = 'sso_'+id+'_bookmark_counter_element';
  data.items.forEach(function(item){
    if (item.total_posts < 1) return;
    var links = CountItem[item.url];
    links.forEach(function(link){
      if (link.nextSibling &&
        class_name === link.nextSibling.className){
        return;
      }
      var parent = link.parentNode;
      var a = document.createElement('a');
      a.href = api_link.replace(/#\{([^}]+)\}/g,
          function(_, _$){return item[_$] || '';});
      a.className = class_name;
      a.textContent = item.total_posts + ' user' +
          (item.total_posts === '1' ? '' : 's');
      parent.insertBefore(a, link.nextSibling);
      CountedItems.push(link);
    });
  });
}メッセージを受け取るContent Script側は、
Node.parentNode.insertBefore(NewNode, Node.nextSibling)これは特定のノードの後ろに新しいノードを追加するときに使用するイディオムとして覚えておいて損はないでしょう
最後になりましたが、
a.sso_hatena_bookmark_counter_element,
a.sso_delicious_bookmark_counter_element{
  font-weight:bold !important;
  padding:0 2px !important;
  display:inline !important;
  font-size:10px !important;
}
a.sso_hatena_bookmark_counter_element{
  background:#ffffff !important;
}
a.sso_delicious_bookmark_counter_element{
  background:#0066cc !important;
}
a.sso_hatena_bookmark_counter_element:link,
a.sso_hatena_bookmark_counter_element:visited{
  color:#ff8888 !important;
  text-decoration:none !important;
}
a.sso_delicious_bookmark_counter_element:link,
a.sso_delicious_bookmark_counter_element:visited{
  color:white !important;
  text-decoration:none !important;
}
a.sso_hatena_bookmark_counter_element:hover{
  color:#ff6666 !important;
  text-decoration:none !important;
}
a.sso_delicious_bookmark_counter_element:hover{
  color:white !important;
  background:#0099cc !important;
  text-decoration:none !important;
}ページ側のスタイルより優先されるように!importantを付けています
以上をパッケージしたファイルは下記にあります。前回とは別ファイルになっています。
まとめ
今回は前回に続いてExtension APIの使い方をContent ScriptとBackground Pageの連携部分を中心に実習しました。次回はChrome 4での新機能やExtensionsの今後について、
