IT戦記

プログラミング、起業などについて書いているプログラマーのブログです😚

サーバー起動用に JavaScript が 1 秒以上実行されなくて alert しないブラウザを作る


はじめに

サムネイルサーバーのようなサービスを作るときには、ウェブサーバー上にブラウザを乗せる必要があります。

問題

ただ、そういった用途でブラウザを使う場合。
JavaScript が無限ループに落ち入らないように配慮する必要があります。
ほとんどの場合は、 JavaScript 自体をオフにすることが多いのですが JavaScript を実行したいような場合もあるでしょう。

解決方法

今回は、 WebKit でその解決方法を紹介します。

1. まず WebKitソースコードを取得する
svn co http://svn.webkit.org/repository/webkit/trunk WebKit
2. 次に、タイマーの時間を短くする

WebCore/bindings/js/JSDOMWindowBase.cpp を編集

JSDOMWindowBase::JSDOMWindowBase(PassRefPtr<StructureID> structure, PassRefPtr<DOMWindow> window, JSDOMWindowShell* shell)
    : JSGlobalObject(structure, new JSDOMWindowBaseData(window, shell), shell)
{
    // Time in milliseconds before the script timeout handler kicks in.
    // ■ ここを 10000 から 1000 にする
    setTimeoutTime(1000);

    GlobalPropertyInfo staticGlobals[] = {
        GlobalPropertyInfo(Identifier(globalExec(), "document"), jsNull(), DontDelete | ReadOnly),
        GlobalPropertyInfo(Identifier(globalExec(), "window"), d()->shell, DontDelete | ReadOnly)
    };   
    
    addStaticGlobals(staticGlobals, sizeof(staticGlobals) / sizeof(GlobalPropertyInfo));
}
3. 次に、 JavaScript が 1 秒以上実行されると、無条件(確認なし)で終了

WebCore/page/Chrome.cpp を編集

bool Chrome::shouldInterruptJavaScript()
{
    // ■ この行を追加
    return true;

    // Defer loads in case the client method runs a new event loop that would 
    // otherwise cause the load to continue while we're in the middle of executing JavaScript.
    PageGroupLoadDeferrer deferrer(m_page, true);

    return m_client->shouldInterruptJavaScript();
}
4. alert, confirm, prompt を切る

WebCore/page/DOMWindow.cpp を編集

void DOMWindow::alert(const String& message)
{
    // ■ この行を追加   
    return;

    if (!m_frame)
        return;
    
    Document* doc = m_frame->document();
    ASSERT(doc);
    if (doc)
        doc->updateRendering();
    
    Page* page = m_frame->page();
    if (!page)
        return;

    page->chrome()->runJavaScriptAlert(m_frame, message);
}   

bool DOMWindow::confirm(const String& message)
{
    // ■ この行を追加   
    return false;

    if (!m_frame)
        return false;

    Document* doc = m_frame->document();
    ASSERT(doc);
    if (doc)
        doc->updateRendering();

    Page* page = m_frame->page();
    if (!page)
        return false;

    return page->chrome()->runJavaScriptConfirm(m_frame, message);
}

String DOMWindow::prompt(const String& message, const String& defaultValue)
{
    // ■ この行を追加   
    return String();

    if (!m_frame)
        return String();

    Document* doc = m_frame->document();
    ASSERT(doc);
    if (doc)
        doc->updateRendering();

    Page* page = m_frame->page();
    if (!page)
        return String();

    String returnValue;
    if (page->chrome()->runJavaScriptPrompt(m_frame, message, defaultValue, returnValue))
        return returnValue;

    return String();
}
5. window.open, window.showModalDialog によるポップアップを切る

WebCore/bindings/js/JSDOMWindowBase.cpp を編集

static bool allowPopUp(ExecState* exec)
{
    // ■ この行を追加
    return false;

    Frame* frame = asJSDOMWindow(exec->dynamicGlobalObject())->impl()->frame();

    ASSERT(frame);
    if (frame->script()->processingUserGesture())
        return true;
    Settings* settings = frame->settings();
    return settings && settings->JavaScriptCanOpenWindowsAutomatically();
}
補足:ここまでの diff です
Index: WebCore/bindings/js/JSDOMWindowBase.cpp
===================================================================
--- WebCore/bindings/js/JSDOMWindowBase.cpp	(revision 37703)
+++ WebCore/bindings/js/JSDOMWindowBase.cpp	(working copy)
@@ -175,7 +175,7 @@
     : JSGlobalObject(structure, new JSDOMWindowBaseData(window, shell), shell)
 {
     // Time in milliseconds before the script timeout handler kicks in.
-    setTimeoutTime(10000);
+    setTimeoutTime(1000);
 
     GlobalPropertyInfo staticGlobals[] = {
         GlobalPropertyInfo(Identifier(globalExec(), "document"), jsNull(), DontDelete | ReadOnly),
@@ -223,6 +223,8 @@
 
 static bool allowPopUp(ExecState* exec)
 {
+    return false;
+
     Frame* frame = asJSDOMWindow(exec->dynamicGlobalObject())->impl()->frame();
 
     ASSERT(frame);
Index: WebCore/page/DOMWindow.cpp
===================================================================
--- WebCore/page/DOMWindow.cpp	(revision 37703)
+++ WebCore/page/DOMWindow.cpp	(working copy)
@@ -469,6 +469,8 @@
 
 void DOMWindow::alert(const String& message)
 {
+    return;
+
     if (!m_frame)
         return;
 
@@ -486,6 +488,8 @@
 
 bool DOMWindow::confirm(const String& message)
 {
+    return false;
+
     if (!m_frame)
         return false;
 
@@ -503,6 +507,8 @@
 
 String DOMWindow::prompt(const String& message, const String& defaultValue)
 {
+    return String();
+
     if (!m_frame)
         return String();
 
Index: WebCore/page/Chrome.cpp
===================================================================
--- WebCore/page/Chrome.cpp	(revision 37703)
+++ WebCore/page/Chrome.cpp	(working copy)
@@ -302,6 +302,8 @@
 
 bool Chrome::shouldInterruptJavaScript()
 {
+    return true;
+
     // Defer loads in case the client method runs a new event loop that would 
     // otherwise cause the load to continue while we're in the middle of executing JavaScript.
     PageGroupLoadDeferrer deferrer(m_page, true);
6. ビルドします

今回は Gtk を使う例です。

$ cd WebKit
$ WebKitTools/Scripts/update-webkit
$ WebKitTools/Scripts/set-webkit-configuration --release
$ WebKitTools/Scripts/build-webkit --gtk

Configure の途中で「bison がないよ><」とか「flex がないよ><」とかいろいろ言われると思いますので、その都度インストールしてくださいね。

7. 起動

さあ、完成です。簡単ですね!
さっそく起動しましょう。

$ WebKitBuild/Release/Programs/GtkLaucher

起動しました!
このブラウザで様々なページを見てみてください!
どんなに重いページでも JavaScript が 1 秒以上連続で実行されませんね。
さらに様々なポップアップや alert 系のウィンドウも出ません。
ばっちりですね!

その他のこと

ただ、これだけで安心という訳ではありません。
いちおう Cookie や Storage 関係も切っておいたほうがいいでしょう。
Storage は build-webkit で --no-storage とか指定すればできます。

まとめ

今日は初めてブラウザのコードに手を入れた記念日。

Xvfb と WebKit と Gtk と楽しい!


おおおおお
めもめも

$ export DISPLAY="localhost:1.0"
$ Xvfb :1 -screen 0 1024x768x16 &
$ WebKitBuild/Debug/Programs/GtkLaunchaer
$ xwd > /tmp/hoge.wd && convert /tmp/hoge.wd hoge.gif


コンテンツ部分だけー

おおおお

$ xwininfo -tree -root

xwininfo: Window id: 0x41 (the root window) (has no name)

  Root window id: 0x41 (the root window) (has no name)
  Parent window id: 0x0 (none)
     3 children:
     0x20001d "Google - WebKit Launcher": ("lt-GtkLauncher" "Lt-GtkLauncher")  800x600+0+0  +0+0
        8 children:
        0x200029 (has no name): ()  18x18+782+582  +782+582
        0x200028 (has no name): ()  800x541+0+38  +0+38
        0x200027 (has no name): ()  34x34+764+2  +764+2
        0x200023 (has no name): ()  694x25+70+6  +70+6
           1 child:
           0x200026 (has no name): ()  690x21+2+2  +72+8
        0x200022 (has no name): ()  34x34+36+2  +36+2
        0x200021 (has no name): ()  34x34+2+2  +2+2
        0x200020 (has no name): ()  800x38+0+0  +0+0
        0x20001e (has no name): ()  1x1+-1+-1  +-1+-1
     0x200003 "lt-GtkLauncher": ("lt-GtkLauncher" "Lt-GtkLauncher")  200x200+0+0  +0+0
        5 children:
        0x200056 (has no name): ()  1x1+-1+-1  +-1+-1
        0x200040 (has no name): ()  1x1+-1+-1  +-1+-1
        0x20001c (has no name): ()  1x1+-1+-1  +-1+-1
        0x20001b (has no name): ()  1x1+-1+-1  +-1+-1
        0x200004 (has no name): ()  1x1+-1+-1  +-1+-1
     0x200001 "lt-GtkLauncher": ("lt-GtkLauncher" "Lt-GtkLauncher")  10x10+10+10  +10+10
        1 child:
        0x200002 (has no name): ()  1x1+-1+-1  +9+9

$ xwd -id 0x200028 > /tmp/hoge.wd && convert /tmp/hoge.wd hoge.gif

おおおおおお
フォントがないよ!ふぉんと(ホント)だ!

フォント入れた!
自分のブログを取ってみた

Safari4 と IE8 で実装された DOM Storage とは何か

はじめに

皆様 JavaScript のスピード競争が激化し、 ECMAScript 3.1 の仕様の策定が進むなど、激動の JavaScript 時代をいかがお過ごしでしょうか。
さて今日は、今、ちまたで大ブレイクの兆しを見せている DOM Storage という仕様を紹介したいと思います。

DOM Storage とは何か

まず、 DOM Storage とはどんなものなのでしょうか。
とても簡単に言ってしまえば、とてもたくさんのデータが保存できてサーバーに自動で送られない Cookie みたいなものです。
さらに、 Cookie とは違って JavaScript からとても扱い易く作られています。
では、この DOMStorage の具体的なソースコードを見てみましょう。

<!DOCTYPE html>
<html>
<head><title>DOMStorage の使い方</title></head>
<body><script>

// DOM Storage は localStorage というオブジェクトを経由してアクセスされます。
if (!localStorage.name) {

    // 使い方はただ代入するだけ
    localStorage.name = prompt('初めまして>< 名前を教えてください')
}

// 簡単にデータを取り出せます
document.write(localStorage.name + 'さんこんにちは!');
</script></body>
</html>

たった 4 行です。
この HTML を DOM Storage に対応しているブラウザで見ると、最初の一回だけ名前を聞かれて以降は聞かれないという挙動をします。
サンプルはこちら
簡単ですね!

よく分からない><

もう少し説明します。
当たり前のことですが、普通の JavaScript のオブジェクトってページ遷移すると値を失いますよね。

object.count = object.count || 0;
object.count++;
alert(object.count);

でも、 localStorage というオブジェクトのプロパティに値を代入しておくと、ページ遷移後でも値が保存されたままになるのです。

localStorage.count = localStorage.count || 0;
localStorage.count = parseInt(localStorage) + 1; // localStorage には文字列として保存されるので、 parseInt が必要
alert(localStorage.count);

それが DOM Storage です。

オブジェクトとかもぶち込めるの?

ぶち込めることはぶち込めるのですが、データはすべて文字列化されて保存されます。
つまり、"[object Object]" というような文字列が保存されたりします。

具体的にはどう嬉しいの?

今までの Web アプリケーションでは、クライアントデータの保存はほとんどサーバサイドで行われていました。
DOM Storage を使うと、ローカルのデータはローカルに保存するという当たり前のことが出来るようになります。
使われ方としては、

  • 設定や状態の保存
  • 大きなデータの一時的な保存
  • HTML や JSON などのキャッシュ

メリットとしては

  • サーバ資源の節約
  • パフォーマンスの改善

などがあげられます。

どのブラウザで使えるの?

DOM Storage は以下のブラウザで使うことができます。

新しいブラウザでは、軒並み使えるようになる予定ですので、当然 Opera でも対応してくることになると思います。

新しい仕様と古い仕様

ただ一つ、注意する点は DOM Storage には古い仕様と新しい仕様があって、

  • 古い仕様を実装しているブラウザ
    • Firefox2
    • Firefox3
  • 新しい仕様を実装しているブラウザ

となっています。
一番最初に書いた例は、新しい仕様での書き方なので、実際には古い仕様でも動くように改造する必要があります。
改造と言っても単純で、

<!DOCTYPE html>
<html>
<head><title>DOM Storage の使い方</title></head>
<body><script>
// *** この 1 行を追加するだけ
window.localStorage = window.localStorage || window.globalStorage[location.hostname];
// *** この 1 行を追加するだけ

if (!localStorage.name) {
    localStorage.name = prompt('初めまして>< 名前を教えてください')
}
document.write(localStorage.name + 'さんこんにちは!');
</script></body>
</html>

上の例のように 1 行を追加するだけです。

具体的に保存できる容量は?

容量は仕様には定義されていませんが、例えば IE8 では 10 MB の大容量を扱うことができます。
Safari4Firefox の容量を知っている人がいれば教えてください><

仕様の全貌

この DOM Storage というものの仕様は HTML5 という仕様の一部で、Structured client-side storageという章で定義されています。

localStorage と sessionStorage

ここまで、 localStorage しか紹介してきませんでしたが、 DOM Storage には以下の二つのオブジェクトがあります。

  • localStorage
  • sessionStorage

localStorage はブラウザを落としても、データが残るオブジェクトで、 sessionStorage はそのタブ(ウィンドウ)が起動している間だけデータを保持します。

データの追加、書き換え

ここまでの例では、代入によるデータの追加方法だけを紹介していましたが、 setItem 関数によるデータの追加方法もあります。

localStorage.setItem('hoge', 'fuga'); // localStorage.hoge = 'fuga'; と同じ意味
sessionStorage.setItem('hoge', 'fuga'); // sessionStorage.hoge = 'fuga'; と同じ意味
データの参照

データの参照方法はプロパティとしての参照方法以外に、 getItem 関数による参照方法があります。

alert(localStorage.getItem('hoge')); // alert(localStorage.hoge); と同じ意味
alert(sessionStorage.getItem('hoge')); // alert(sessionStorage.hoge); と同じ意味
データの削除

データの削除方法は delete 演算子、または、 removeItem を使います。

delete localStorage.hoge;
localStorage.removeItem('hoge');
データの列挙

保存されたデータの列挙方法は、 length プロパティと key 関数を使います。

for (var i = 0; i < localStorage.length; i ++) {
  var key = localStorage.key(i);
  var data = localStorage.getItem(key);
}

length プロパティは保存されたデータの個数を表し、 key 関数は数値を受け取りデータのプロパティ名(キー)を返します。

データが共有される範囲

データは同じ origin (プロトコル + ドメイン + ポート)で共有されます。
例えば、

storage イベント

DOM Storage は同じドメインなら複数のドキュメント間で共有できるのが便利ですが、別のページから突如書き換えられてしまう可能性があります。
そのような場合を細かく制御するために、 storage イベントというものが存在します。
使い方は、以下のようになります。

document.onstorage = function(e) {
  e = e || envet; // for IE

  if (e.window != window) { 
    alert(
      '別のドキュメントによって' +
      e.key + 'が' +
      e.oldValue + 'から' +
      e.newValue + 'に書き換えられました!');
  }
};

上の例のようにイベントオブジェクト e の

  • e.window には、書き換えたドキュメントの window オブジェクトが、
  • e.key には、追加したデータのプロパティ名(キー)が、
  • e.oldValue には、書き換えられる前のデータが、
  • e.newValue には、書き換えられた後のデータが、

入っています。

でも IE6, IE7 ユーザーが・・・そんなあなたに

「一番ユーザが多いブラウザ(IE6 と IE7)で使えないんじゃ、いくら便利なものとは言え使えないよ><」というのはもっともだと思います。
でも、そこで諦める必要はありません。 IE 独自の機能を使って、IE6, IE7 でも DOM Storage が使えるようになってしまう夢のようなライブラリ「Ex DOM Storage」が今日リリースされました。
これで、思う存分 DOM Storage を使い倒すことができます。素晴らしい!

簡単な使い方

以下のように、 script タグを読み込んでやるだけです!簡単!

<html><head>
  <title>hoge</title>
  <script src="http://svn.coderepos.org/share/lang/javascript/exdomstorage/trunk/src/exdomstorage.js"></script>
</head>
<body>...</body>
</html>

ただし、自分のサーバーに置くときは、この js ファイル以外にも以下のリンク先にあるファイルをすべて置いて、適切な mime-type を設定しなければなりません。
http://svn.coderepos.org/share/lang/javascript/exdomstorage/trunk/src/

さらに詳細な情報は

作者様のページをどうぞ

まとめ

  • IE8 や Safari4 からは、 DOM Storage が使えるようになる。
  • Firefox では古い仕様だが、簡単に新しい仕様と同じように使うことができる。
  • IE6 や IE7DOM Storage を使えるようにするためのライブラリがリリースされた。

という訳で、 DOM Storage は未来の技術から、現在使える技術へと姿を変えました。皆様も DOM Storage を使って、クライアントサイドのパワーをフルにいかした Web アプリケーションを作ってみてはいかがでしょうか。

Mac や Linux で Google Chrome を動かそう!「CrossOver Chromium」を試してみた。

CrossOver Chromium とは

簡単に言えば Windows 用の Google Chrome (正確には Chromium)を wine というソフトウェアを使って MacLinux 上で動かすものです。

インストール手順

使うための手順を書いておきます。(Mac の例です。)

まず、ダウンロード

以下のサイトの右側からダウンロードします。
screenshot
http://www.codeweavers.com/services/ports/chromium/

dmg をマウントして Application ディレクトリにドラッグ&ドロップ

アイコンをドラッグします

で、起動

初回起動時は時間がかかります。。。

待つと、起動します。

おおおおー。動いてるー。簡単だー

色々試してみよう

起動したので、ちゃんと動くか試してみました。

ブラウザとして


多少不安定ですが、フォント以外は全然問題なくレンダリングされています。(フォントはしょうがないっすね)

タスクマネージャは


全然機能してない見たいです。。

Web Inspector (JavaScript コンソール)は

ちゃんと動いてます。

おおお。やるー

Gears は


動いてないですねー><

全体的な印象

wine 経由でもかなり軽いです。(IEs for Mac とかと比べると幸せになれます。)
ただ、まだまだ不安定でたまに落ちます。
でも、 Google Chromeレンダリングを軽く確認したいときには使えそうな感じですね。手元に Windows が無いときも多いので、僕はもうちょっと使ってみたいと思います。
というわけで
最後は CrossOver Chromium さんのおちゃめな NG シーンでお別れしましょう。さよーならー

bison の error トークンで行き詰まる

このゴールデンウィークWebKit の CSSGrammar.y を写経していて bison では error トークンという特別なトークンが扱えることを知った。
kmyacc には、ない。
行き詰まった><

  • yacc の仕組みをちゃんとを勉強する
  • error トークンと同等のことを行う方法を考える

WebKit の CSS の字句解析部分を JavaScript に移植しました

ポイント

それなりに汎用的な Flex みたいなものを作ったので、その部分は CSS 以外にも使えると思います。
あと、定義を文字列で書かずに正規表現オブジェクトで書くのでバックスラッシュをエスケープせずに書けます。ですので、ほとんどの箇所は WebKit の tokenizer の定義をコピーするだけで済みました。
その辺のアイデア
JavaScript で構文解析: Days on the Moon
を参考にしました

あと

http://svn.coderepos.org/share/lang/actionscript/ascss/src/css/CSSLexer.as
id:gyuque さんの ASCSS もいっぱい参考にしました。

ソース

トークンの値は、 kmyacc で生成しています。

JSCSS = {};
JSCSS.tokens = {};

JSCSS.tokens.YYERRTOK = 256;
JSCSS.tokens.UNIMPORTANT_TOK = 257;
JSCSS.tokens.WHITESPACE = 258;
JSCSS.tokens.SGML_CD = 259;
JSCSS.tokens.INCLUDES = 260;
JSCSS.tokens.DASHMATCH = 261;
JSCSS.tokens.BEGINSWITH = 262;
JSCSS.tokens.ENDSWITH = 263;
JSCSS.tokens.CONTAINS = 264;
JSCSS.tokens.STRING = 265;
JSCSS.tokens.IDENT = 266;
JSCSS.tokens.NTH = 267;
JSCSS.tokens.HEX = 268;
JSCSS.tokens.IDSEL = 269;
JSCSS.tokens.IMPORT_SYM = 270;
JSCSS.tokens.PAGE_SYM = 271;
JSCSS.tokens.MEDIA_SYM = 272;
JSCSS.tokens.FONT_FACE_SYM = 273;
JSCSS.tokens.CHARSET_SYM = 274;
JSCSS.tokens.NAMESPACE_SYM = 275;
JSCSS.tokens.WEBKIT_RULE_SYM = 276;
JSCSS.tokens.WEBKIT_DECLS_SYM = 277;
JSCSS.tokens.WEBKIT_VALUE_SYM = 278;
JSCSS.tokens.WEBKIT_MEDIAQUERY_SYM = 279;
JSCSS.tokens.IMPORTANT_SYM = 280;
JSCSS.tokens.MEDIA_ONLY = 281;
JSCSS.tokens.MEDIA_NOT = 282;
JSCSS.tokens.MEDIA_AND = 283;
JSCSS.tokens.QEMS = 284;
JSCSS.tokens.EMS = 285;
JSCSS.tokens.EXS = 286;
JSCSS.tokens.PXS = 287;
JSCSS.tokens.CMS = 288;
JSCSS.tokens.MMS = 289;
JSCSS.tokens.INS = 290;
JSCSS.tokens.PTS = 291;
JSCSS.tokens.PCS = 292;
JSCSS.tokens.DEGS = 293;
JSCSS.tokens.RADS = 294;
JSCSS.tokens.GRADS = 295;
JSCSS.tokens.MSECS = 296;
JSCSS.tokens.SECS = 297;
JSCSS.tokens.HERZ = 298;
JSCSS.tokens.KHERZ = 299;
JSCSS.tokens.DIMEN = 300; 
JSCSS.tokens.PERCENTAGE = 301;
JSCSS.tokens.FLOATTOKEN = 302;
JSCSS.tokens.INTEGER = 303;
JSCSS.tokens.URI = 304; 
JSCSS.tokens.FUNCTION = 305;
JSCSS.tokens.NOTFUNCTION = 306;
JSCSS.tokens.UNICODERANGE = 307;

JSCSS.CSSLexer = function(source) {
    this.source = source;
    this.reset();
};

(function() {
    var lex = {
        defs: [
            'h',          /[0-9a-fA-F]/,
            'nonascii',   /[^\0-\177]/,
            'unicode',    /\\{h}{1,6}[ \t\r\n\f]?/,
            'escape',     /{unicode}|\\[ -~]|\\[^\0-\177]/,
            'nmstart',    /[_a-zA-Z]|{nonascii}|{escape}/,
            'nmchar',     /[_a-zA-Z0-9-]|{nonascii}|{escape}/,
            'hexcolor',   /{h}{3}|{h}{6}/,
            'ident',      /-?{nmstart}{nmchar}*/,
            'name',       /{nmchar}+/,
            'num',        /[0-9]+|[0-9]*\.[0-9]+/,
            'intnum',     /[0-9]+/,
            'url',        /([!#$%&*-~]|{nonascii}|{escape})*/,
            'w',          /[ \t\r\n\f]*/,
            'nl',         /\n|\r\n|\r|\f/,
            'string1',    /\"([\t !#$%&(-~]|\\{nl}|\'|{nonascii}|{escape})*\"/,
            'string2',    /\'([\t !#$%&(-~]|\\{nl}|\"|{nonascii}|{escape})*\'/,
            'string',     /{string1}|{string2}/,
            'range',      /\?{1,6}|{h}(\?{0,5}|{h}(\?{0,4}|{h}(\?{0,3}|{h}(\?{0,2}|{h}(\??|{h})))))/,
            'nth',        /(-?[0-9]*n[\+-][0-9]+)|(-?[0-9]*n)/
        ],
        rules: [
            'COMMENT',                [/\/\*[^*]*\*+([^/*][^*]*\*+)*\//],
            'WHITESPACE',             [/[ \t\r\n\f]+/],
            'SGML_CD',                [/<!--|-->/],
            'INCLUDES',               ["~="],
            'DASHMATCH',              ["|="],
            'BEGINSWITH',             ["^="],
            'ENDSWITH',               ["$="],
            'CONTAINS',               ["*="],
            'MEDIA_NOT',              ["not",                             'mediaquery'],
            'MEDIA_ONLY',             ["only",                            'mediaquery'],
            'MEDIA_AND',              ["and",                             'mediaquery'],
            'STRING',                 [/{string}/],
            'IDENT',                  [/{ident}/],
            'NTH',                    [/{nth}/],
            'HEX',                    [/#{hexcolor}/],
            'IDSEL',                  [/#{ident}/],
            'IMPORT_SYM',             ["@import",                         undefined,          'mediaquery'],
            'PAGE_SYM',               ["@page"],
            'MEDIA_SYM',              ["@media",                          undefined,          'mediaquery'],
            'FONT_FACE_SYM',          ["@font-face"],
            'CHARSET_SYM',            ["@charset"],
            'NAMESPACE_SYM',          ["@namespace"],
            'WEBKIT_RULE_SYM',        ["@-webkit-rule"],
            'WEBKIT_DECLS_SYM',       ["@-webkit-decls"],
            'WEBKIT_VALUE_SYM',       ["@-webkit-value"],
            'WEBKIT_MEDIAQUERY_SYM',  ["@-webkit-mediaquery",             undefined,          'mediaquery'],
            'IMPORTANT_SYM',          [/!{w}important/],
            'EMS',                    [/{num}em/],
            'QEMS',                   [/{num}__qem/],
            'EXS',                    [/{num}ex/],
            'PXS',                    [/{num}px/],
            'CMS',                    [/{num}cm/],
            'MMS',                    [/{num}mm/],
            'INS',                    [/{num}in/],
            'PTS',                    [/{num}pt/],
            'PCS',                    [/{num}pc/],
            'DEGS',                   [/{num}deg/],
            'RADS',                   [/{num}rad/],
            'GRADS',                  [/{num}grad/],
            'MSECS',                  [/{num}ms/],
            'SECS',                   [/{num}s/],
            'HERZ',                   [/{num}Hz/],
            'KHERZ',                  [/{num}kHz/],
            'DIMEN',                  [/{num}{ident}/],
            'PERCENTAGE',             [/{num}%+/],
            'INTEGER',                [/{intnum}/],
            'FLOATTOKEN',             [/{num}/],
            'NOTFUNCTION',            ["not("],
            'URI',                    [/(?:url\({w}{string}{w}\))|(?:url\({w}{url}{w}\))/],
            'FUNCTION',               [/{ident}\(/],
            'UNICODERANGE',           [/(?:U\+{range})|(?:U\+{h}{1,6}-{h}{1,6})/],
            'MEDIAQUERY_END',         [/{|;/,                             'mediaquery',       'INITIAL',      true],
            'ALSO',                   [/./,                               undefined,          undefined,      true]
        ]
    };

    var defs = {};
    for (var i = 0; i < lex.defs.length; i += 2) {
        var n = lex.defs[i];
        var def = lex.defs[i + 1] + '';
        def = def.substring(1, def.length - 1);
        for (var o in defs) {
            def = def.replace(new RegExp('{' + o + '}', 'g'), defs[o]);
        }
        defs[n] = '(?:' + def + ')'; 
    }

    var rules = lex.rules;
    for (var i = 0; i < rules.length; i += 2) {
        var n = rules[i];
        var rule = rules[i + 1];
        var reg = rule[0];
        if (reg instanceof RegExp) {
            reg += '';
            reg = reg.substring(1, reg.length - 1);
            for (var n in defs) {
                reg = reg.replace(new RegExp('{' + n + '}', 'g'), defs[n]);
            }
            rule[0] = new RegExp('^(?:' + reg + ')');
        }
    }

    JSCSS.CSSLexer.rules = rules;
})();

JSCSS.CSSLexer.prototype = {
    state: 'INITIAL',
    source: null,
    tokneBody: null,
    finished: false,
    next: function() {
        var rules = JSCSS.CSSLexer.rules;

        var m, matches = [];
        for (var i = 0; i < rules.length; i += 2) {
            var n = rules[i];
            var rule = rules[i + 1];
            var reg = rule[0];
            if (rule[1] == undefined || rule[1] == this.state) {
                if (reg instanceof RegExp) {
                    if (m = reg.exec(this.cur)) {
                        matches.push([m[0].length, n, m[0], rule[2], rule[3]])
                    }
                }
                else {
                    if (this.cur.indexOf(reg) == 0) {
                        matches.push([reg.length, n, reg, rule[2], rule[3]]);
                    }
                }
            }
        }

        var length = 0;
        var token = 0;
        var tokenBody = null;
        var state = undefined;
        var literal = false;
        for (var i = 0, l = matches.length; i < l; i++) {
            var match = matches[i];
            if (length < match[0]) {
                length = match[0];
                token = match[1];
                tokenBody = match[2];
                state = match[3];
                literal = match[4];
            }
        }

        if (state != undefined) {
            this.state = state;
        }
        this.tokenBody = tokenBody;
        this.cur = this.cur.substring(length);
        if (token == 0) {
            return 0;
        }
        return literal ? tokenBody.charCodeAt(0) : JSCSS.tokens[token];
    },
    reset: function() {
        this.cur = this.source;
    },
    constructor: JSCSS.CSSLexer
};

(追記)

ちょこっと修正、ハッシュを配列にした

CSS の構文解析と @-webkit-*** ルールの意味

はじめに

あのDan Kogai 氏にも好評な噂の名著「Jythonプログラミング」がついに発売されるわけですが、皆様いかがお過ごしでしょうか。今日は Jython にちなんで、 CSS構文解析のお話をしたいと思います。(関係ない)

知ってますか?

Safari(などの WebKit を使ったブラウザ)にある以下のルールを

これらのルールの使い方を知っていますか?
僕は、今日まで使い方が分からなくて困っていたのです><

話は変わって

最近、僕の中では CSS (の実装)ブームです。
前回のパーサブームでは JavaScriptXPath を作ったわけですが、そのときはパーサを手で書いていました。
じゃあ、今回は yacc でも使ってみるかと鼻息荒めで挑んでみたのですが、少し困ったことがありました。
手書きパーサだと、パーサ全体の一部分だけを使ったパーサを簡単に作ることができるじゃないですか><
たとえば、

// ↓ こういうケースで、文法全体の一部分だけでパースしたいってことあるよねん><

// 宣言だけパースしたい><
element.style.cssText = 'background: red; color: blue';

// Media Query だけパースしたい><
linkElement.media.appendMedium('print and (min-device-width: 800px)');

// 色だけパースしたい><
element.style.color = 'rgb(100, 0, 100)';

でも、 yacc を使うとそれができないんです>< 困った/(^o^)\
じゃあ、何個もパーサ作りますか?
やだやだ!めんどいめどい><!!
1つの文法定義ファイルでぜーんぶやりたいです><

じゃあ、実際ブラウザはどうやってるのですか><

WebKit のソースを見てみました。
まず、 yacc の定義を探してみます。
http://svn.webkit.org/repository/webkit/trunk/WebCore/css/CSSGrammar.y
これ一個しか yacc の文法定義っぽいファイルはないなあ><
じゃあ WebKit では、この 1 ファイルだけを使ってやってるのかなあ。
さらに追いかけてみる

JS から CSS の値が設定された時、何が起こるか

element.style.color = 'red';

まずは、 CSSStyleDeclaration の setProperty が呼ばれる
http://svn.webkit.org/repository/webkit/trunk/WebCore/css/CSSStyleDeclaration.cpp

void CSSStyleDeclaration::setProperty(const String& propertyName, const String& value, ExceptionCode& ec) 
{
    int important = value.find("!important", 0, false);
    if (important == -1) 
        setProperty(propertyName, value, "", ec);
    else
        setProperty(propertyName, value.left(important - 1), "important", ec);
}

void CSSStyleDeclaration::setProperty(const String& propertyName, const String& value, const String& priority, ExceptionCode& ec)
{
    int propID = cssPropertyID(propertyName);
    if (!propID) {
        // FIXME: Should we raise an exception here?
        return;
    }   
    bool important = priority.find("important", 0, false) != -1;
    setProperty(propID, value, important, ec);
}

さらに、今度は CSSStyleDeclaration#setProperty からサブクラスの CSSMutableStyleDeclaration#setProperty が呼び出される
http://svn.webkit.org/repository/webkit/trunk/WebCore/css/CSSMutableStyleDeclaration.cpp

void CSSMutableStyleDeclaration::setProperty(int propertyID, const String& value, bool important, ExceptionCode& ec)
{
    setProperty(propertyID, value, important, true, ec);
}

bool CSSMutableStyleDeclaration::setProperty(int propertyID, const String& value, bool important, bool notifyChanged, Exceptio
nCode& ec)
{
    ec = 0;

    if (value.isEmpty()) {
        removeProperty(propertyID, notifyChanged, false, ec);
        return ec == 0;
    }

    // ■■ わー!ここで与えられた文字列をパースしようとしてる><
    CSSParser parser(useStrictParsing());
    bool success = parser.parseValue(this, propertyID, value, important);

    if (!success) {
    } else if (notifyChanged)
        setChanged();
    ASSERT(!ec);
    return success;
}

というわけで、 CSSParser オブジェクトの中身に答えがありそうですね。
http://svn.webkit.org/repository/webkit/trunk/WebCore/css/CSSParser.cpp

bool CSSParser::parseValue(CSSMutableStyleDeclaration *declaration, int _id, const String &string, bool _important)
{
    styleElement = declaration->stylesheet();

    // ■■ これが答えだ!!
    setupParser("@-webkit-value{", string, "} ");

    id = _id;
    important = _important;
    
    CSSParser* old = currentParser;
    currentParser = this;

    // ここで yacc で作った関数を呼ぶ
    cssyyparse(this);
    currentParser = old;
    
    rule = 0;

    bool ok = false;
    if (numParsedProperties) {
        ok = true;
        declaration->addParsedProperties(parsedProperties, numParsedProperties);
        clearProperties();
    }

    return ok;
}

きた!そういうことか!と思った!
つまり、 JS で

element.style.color = 'red';

とすると

@-webkit-value { red }

がパースされるってことか!!

つまり

最初に紹介した

という webkit の拡張ルールは、 CSS の機能を拡張する訳でもなんでもなくて一つの yacc の定義で複数のパーサを作るためにあったんです><
なるほどおおお

まとめ

すっきりした!
すっきりした!