IT戦記

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

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 の文字列を JS の文字列に

こんな感じかな。

JSCSS.CSSValueConverter = {
    _string: function(str) {
        return str.replace(/\\([0-9a-fA-F]{1,6}[ \t\r\n\f]?|\r\n|[ \t\r\n\f]|.)/g, function(str, t) { 
            if (t.match(/^[0-9a-fA-F]{1,6}[ \t\r\n\f]?$/)) {
                return String.fromCharCode(parseInt('0x' + t)); 
            }    
            else if (t.match(/^(\r\n|[ \t\r\n\f])$/)) {
                return '';
            }    
            else {
                return t;
            }    
        });  
    },   
    string: function(str) {
        var m = str.match(/^['"]((?:[\r\n]|.)*?)['"]$/);
        str = m[1];
        return this._string(str);
    },   
    url: function(url) {
        var m = url.match(/^url\([ \n\t\r\f]*["']?[ \n\t\r\f]*((?:[\r\n]|.)*?)[ \n\t\r\f]*["']?[ \n\t\r\f]*\)$/);
        url = m[1];
        return this._string(str);
    }    
};

テスト

alert(JSCSS.CSSValueConverter.string('"\\3042"')); // => あ

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 の定義で複数のパーサを作るためにあったんです><
なるほどおおお

まとめ

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

DOM 2 Style の JavaScript オブジェクト

CSS のデータを扱う JavaScript を書きたくて作った

いろいろと使えそうなので晒しておきます
まだ、インタフェースだけですよ><

var JSCSS = {};

// http://www.w3.org/TR/DOM-Level-2-Style/stylesheets.html#StyleSheets-StyleSheet
JSCSS.StyleSheet = function(type, disabled, ownerNode, parentStyleSheet, href, title, media) {
    this.type = type;
    this.disabled = disabled;
    this.ownerNode = ownerNode;
    this.parentStyleSheet = parentStyleSheet;
    this.href = href;
    this.title = title;
    this.media = media;
};
JSCSS.StyleSheet.prototype = {
    type : null,
    disabled : null,
    ownerNode : null,
    parentStyleSheet : null,
    href : null,
    title : null,
    media : null,
    constructor: JSCSS.StyleSheet
};

// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet
JSCSS.CSSStyleSheet = function(type, disabled, ownerNode, parentStyleSheet, href, title, media, ownerRule, cssRules) {
    JSCSS.StyleSheet.apply(arguments, this);
    this.ownerRule = ownerRule;
    this.cssRules = cssRules;
};
JSCSS.CSSStyleSheet.prototype = new JSCSS.StyleSheet(null, null, null, null, null, null, null);
JSCSS.CSSStyleSheet.prototype.ownerRule = null;
JSCSS.CSSStyleSheet.prototype.cssRules = null;
JSCSS.CSSStyleSheet.prototype.insertRule = function(rule, index) {
    this.cssRules.splice(index, 0, rule);
    return index;
};
JSCSS.CSSStyleSheet.prototype.deleteRule = function(index) {
    this.cssRules.splice(index, 1);
};
JSCSS.CSSStyleSheet.prototype.constructor = JSCSS.CSSStyleSheet;

// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSRuleList
JSCSS.CSSRuleList = function() {
    var self = [];
    self.item = JSCSS.CSSRuleList.prototype.item;
    self.constructor = JSCSS.CSSRuleList;
    return self;
};
JSCSS.CSSRuleList.prototype = {
    item: function(index) {
        return this[index];
    }
}


// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSRule
JSCSS.CSSRule = function(type, cssText, parentStyleSheet, parentRule) {
    this.type = type;
    this.cssText = cssText;
    this.parentStyleSheet = parentStyleSheet;
    this.parentRule = parentRule;
};
JSCSS.CSSRule.prototype = {
    UNKNOWN_RULE: 0,
    STYLE_RULE: 1,
    CHARSET_RULE: 2,
    IMPORT_RULE: 3,
    MEDIA_RULE: 4,
    FONT_FACE_RULE: 5,
    PAGE_RULE: 6,
    type: null,
    cssText: null,
    parentStyleSheet: null,
    parentRule: null,
    constructor: JSCSS.CSSRule
};
JSCSS.CSSRule.UNKNOWN_RULE = JSCSS.CSSRule.prototype.UNKNOWN_RULE;
JSCSS.CSSRule.STYLE_RULE = JSCSS.CSSRule.prototype.STYLE_RULE;
JSCSS.CSSRule.CHARSET_RULE = JSCSS.CSSRule.prototype.CHARSET_RULE;
JSCSS.CSSRule.IMPORT_RULE = JSCSS.CSSRule.prototype.IMPORT_RULE;
JSCSS.CSSRule.MEDIA_RULE = JSCSS.CSSRule.prototype.MEDIA_RULE;
JSCSS.CSSRule.FONT_FACE_RULE = JSCSS.CSSRule.prototype.FONT_FACE_RULE;
JSCSS.CSSRule.PAGE_RULE = JSCSS.CSSRule.prototype.PAGE_RULE;


// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleRule
JSCSS.CSSStyleRule = function(type, cssText, parentStyleSheet, parentRule, selectorText, style) {
    JSCSS.CSSRule.apply(arguments, this);
    this.selectorText = selectorText;
    this.style = style;
};
JSCSS.CSSStyleRule.prototype = new JSCSS.CSSRule(null, null, null, null);
JSCSS.CSSStyleRule.prototype.selectorText = null;
JSCSS.CSSStyleRule.prototype.style = null;
JSCSS.CSSStyleRule.prototype.constructor = JSCSS.CSSStyleRule;
JSCSS.CSSStyleRule.UNKNOWN_RULE = JSCSS.CSSRule.UNKNOWN_RULE;
JSCSS.CSSStyleRule.STYLE_RULE = JSCSS.CSSRule.STYLE_RULE;
JSCSS.CSSStyleRule.CHARSET_RULE = JSCSS.CSSRule.CHARSET_RULE;
JSCSS.CSSStyleRule.IMPORT_RULE = JSCSS.CSSRule.IMPORT_RULE;
JSCSS.CSSStyleRule.MEDIA_RULE = JSCSS.CSSRule.MEDIA_RULE;
JSCSS.CSSStyleRule.FONT_FACE_RULE = JSCSS.CSSRule.FONT_FACE_RULE;
JSCSS.CSSStyleRule.PAGE_RULE = JSCSS.CSSRule.PAGE_RULE;


// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSMediaRule
JSCSS.CSSMediaRule = function(type, cssText, parentStyleSheet, parentRule, media, cssRules) {
    JSCSS.CSSRule.apply(arguments, this);
    this.media = media;
    this.cssRules = cssRules;
};
JSCSS.CSSMediaRule.prototype = new JSCSS.CSSRule(null, null, null, null);
JSCSS.CSSMediaRule.prototype.media = null;
JSCSS.CSSMediaRule.prototype.cssRules = null;
JSCSS.CSSMediaRule.prototype.insertRule = function(rule, index) {
    this.cssRules.splice(index, 0, rule);
    return index;
};
JSCSS.CSSMediaRule.prototype.deleteRule = function(index) {
    this.cssRules.splice(index, 1);
};
JSCSS.CSSMediaRule.prototype.constructor = JSCSS.CSSMediaRule;
JSCSS.CSSMediaRule.UNKNOWN_RULE = JSCSS.CSSRule.UNKNOWN_RULE;
JSCSS.CSSMediaRule.STYLE_RULE = JSCSS.CSSRule.STYLE_RULE;
JSCSS.CSSMediaRule.CHARSET_RULE = JSCSS.CSSRule.CHARSET_RULE;
JSCSS.CSSMediaRule.IMPORT_RULE = JSCSS.CSSRule.IMPORT_RULE;
JSCSS.CSSMediaRule.MEDIA_RULE = JSCSS.CSSRule.MEDIA_RULE;
JSCSS.CSSMediaRule.FONT_FACE_RULE = JSCSS.CSSRule.FONT_FACE_RULE;
JSCSS.CSSMediaRule.PAGE_RULE = JSCSS.CSSRule.PAGE_RULE;


// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSFontFaceRule
JSCSS.CSSFontFaceRule = function(type, cssText, parentStyleSheet, parentRule, style) {
    JSCSS.CSSRule.apply(arguments, this);
    this.style = style;
};
JSCSS.CSSFontFaceRule.prototype = new JSCSS.CSSRule(null, null, null, null);
JSCSS.CSSFontFaceRule.prototype.style = null;
JSCSS.CSSFontFaceRule.prototype.constructor = JSCSS.CSSFontFaceRule;
JSCSS.CSSFontFaceRule.UNKNOWN_RULE = JSCSS.CSSRule.UNKNOWN_RULE;
JSCSS.CSSFontFaceRule.STYLE_RULE = JSCSS.CSSRule.STYLE_RULE;
JSCSS.CSSFontFaceRule.CHARSET_RULE = JSCSS.CSSRule.CHARSET_RULE;
JSCSS.CSSFontFaceRule.IMPORT_RULE = JSCSS.CSSRule.IMPORT_RULE;
JSCSS.CSSFontFaceRule.MEDIA_RULE = JSCSS.CSSRule.MEDIA_RULE;
JSCSS.CSSFontFaceRule.FONT_FACE_RULE = JSCSS.CSSRule.FONT_FACE_RULE;
JSCSS.CSSFontFaceRule.PAGE_RULE = JSCSS.CSSRule.PAGE_RULE;


// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSPageRule
JSCSS.CSSPageRule = function(type, cssText, parentStyleSheet, parentRule, selectorText, style) {
    JSCSS.CSSRule.apply(arguments, this);
    this.selectorText = selectorText;
    this.style = style;
};
JSCSS.CSSPageRule.prototype = new JSCSS.CSSRule(null, null, null, null);
JSCSS.CSSPageRule.prototype.selectorText = null;
JSCSS.CSSPageRule.prototype.style = null;
JSCSS.CSSPageRule.prototype.constructor = JSCSS.CSSPageRule;
JSCSS.CSSPageRule.UNKNOWN_RULE = JSCSS.CSSRule.UNKNOWN_RULE;
JSCSS.CSSPageRule.STYLE_RULE = JSCSS.CSSRule.STYLE_RULE;
JSCSS.CSSPageRule.CHARSET_RULE = JSCSS.CSSRule.CHARSET_RULE;
JSCSS.CSSPageRule.IMPORT_RULE = JSCSS.CSSRule.IMPORT_RULE;
JSCSS.CSSPageRule.MEDIA_RULE = JSCSS.CSSRule.MEDIA_RULE;
JSCSS.CSSPageRule.FONT_FACE_RULE = JSCSS.CSSRule.FONT_FACE_RULE;
JSCSS.CSSPageRule.PAGE_RULE = JSCSS.CSSRule.PAGE_RULE;


// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSImportRule
JSCSS.CSSImportRule = function(type, cssText, parentStyleSheet, parentRule, href, media, styleSheet) {
    JSCSS.CSSRule.apply(arguments, this);
    this.href = href;
    this.media = media;
    this.styleSheet = styleSheet;
};
JSCSS.CSSImportRule.prototype = new JSCSS.CSSRule(null, null, null, null);
JSCSS.CSSImportRule.prototype.href = null;
JSCSS.CSSImportRule.prototype.media = null;
JSCSS.CSSImportRule.prototype.styleSheet = null;
JSCSS.CSSImportRule.prototype.constructor = JSCSS.CSSImportRule;
JSCSS.CSSImportRule.UNKNOWN_RULE = JSCSS.CSSRule.UNKNOWN_RULE;
JSCSS.CSSImportRule.STYLE_RULE = JSCSS.CSSRule.STYLE_RULE;
JSCSS.CSSImportRule.CHARSET_RULE = JSCSS.CSSRule.CHARSET_RULE;
JSCSS.CSSImportRule.IMPORT_RULE = JSCSS.CSSRule.IMPORT_RULE;
JSCSS.CSSImportRule.MEDIA_RULE = JSCSS.CSSRule.MEDIA_RULE;
JSCSS.CSSImportRule.FONT_FACE_RULE = JSCSS.CSSRule.FONT_FACE_RULE;
JSCSS.CSSImportRule.PAGE_RULE = JSCSS.CSSRule.PAGE_RULE;

// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSCharsetRule
JSCSS.CSSCharsetRule = function(type, cssText, parentStyleSheet, parentRule, encoding) {
    JSCSS.CSSRule.apply(arguments, this);
    this.encoding = encoding;
};
JSCSS.CSSCharsetRule.prototype = new JSCSS.CSSRule(null, null, null, null);
JSCSS.CSSCharsetRule.prototype.encoding = null;
JSCSS.CSSCharsetRule.prototype.constructor = JSCSS.CSSCharsetRule;
JSCSS.CSSCharsetRule.UNKNOWN_RULE = JSCSS.CSSRule.UNKNOWN_RULE;
JSCSS.CSSCharsetRule.STYLE_RULE = JSCSS.CSSRule.STYLE_RULE;
JSCSS.CSSCharsetRule.CHARSET_RULE = JSCSS.CSSRule.CHARSET_RULE;
JSCSS.CSSCharsetRule.IMPORT_RULE = JSCSS.CSSRule.IMPORT_RULE;
JSCSS.CSSCharsetRule.MEDIA_RULE = JSCSS.CSSRule.MEDIA_RULE;
JSCSS.CSSCharsetRule.FONT_FACE_RULE = JSCSS.CSSRule.FONT_FACE_RULE;
JSCSS.CSSCharsetRule.PAGE_RULE = JSCSS.CSSRule.PAGE_RULE;


// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSUnknownRule
JSCSS.CSSUnknownRule = function(type, cssText, parentStyleSheet, parentRule) {
    JSCSS.CSSRule.apply(arguments, this);
};
JSCSS.CSSUnknownRule.prototype = new JSCSS.CSSRule(null, null, null, null);
JSCSS.CSSUnknownRule.prototype.constructor = JSCSS.CSSUnknownRule;
JSCSS.CSSUnknownRule.UNKNOWN_RULE = JSCSS.CSSRule.UNKNOWN_RULE;
JSCSS.CSSUnknownRule.STYLE_RULE = JSCSS.CSSRule.STYLE_RULE;
JSCSS.CSSUnknownRule.CHARSET_RULE = JSCSS.CSSRule.CHARSET_RULE;
JSCSS.CSSUnknownRule.IMPORT_RULE = JSCSS.CSSRule.IMPORT_RULE;
JSCSS.CSSUnknownRule.MEDIA_RULE = JSCSS.CSSRule.MEDIA_RULE;
JSCSS.CSSUnknownRule.FONT_FACE_RULE = JSCSS.CSSRule.FONT_FACE_RULE;
JSCSS.CSSUnknownRule.PAGE_RULE = JSCSS.CSSRule.PAGE_RULE;


// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration
JSCSS.CSSStyleDeclaration = function(cssText, parentRule) {
    this.cssText = cssText;
    this.parentRule = parentRule;
};
JSCSS.CSSStyleDeclaration.prototype = {
    cssText: null,
    parentRule: null,
    length: 0,
    item: function(index) {
        return this[index];
    },
    getPropertyValue: function(propertyName) {
        return this[propertyName];
    },
    getPropertyCSSValue: function(propertyName) {
        return this.cssValues[propertyName];
    },
    removePropertyCSSValue: function(propertyName) {
        delete this.cssValue[peropertyName];
        // TODO: length
        // TODO: return 
    },
    getPropertyPriority: function(propertyName) {
        return this.cssPriority[propertyName];
    },
    setPriority: function(propertyName, value, priority) {
        // TODO
    },
    constructor: JSCSS.CSSStyleDeclaration
};


// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSValue
JSCSS.CSSValue = function(cssText, cssValueType) {
    this.cssText = cssText;
    this.cssValueType = cssValueType;
};
JSCSS.CSSValue.prototype = {
    CSS_INHERIT: 0,
    CSS_PRIMITIVE_VALUE: 1,
    CSS_VALUE_LIST: 2,
    CSS_CUSTOM: 3,
    cssText: null,
    cssValueType: null,
    constructor: JSCSS.CSSValue
};
JSCSS.CSSValue.CSS_INHERIT = JSCSS.CSSValue.prototype.CSS_INHERIT
JSCSS.CSSValue.CSS_PRIMITIVE_VALUE = JSCSS.CSSValue.prototype.CSS_PRIMITIVE_VALUE
JSCSS.CSSValue.CSS_VALUE_LIST = JSCSS.CSSValue.prototype.CSS_VALUE_LIST
JSCSS.CSSValue.CSS_CUSTOM = JSCSS.CSSValue.prototype.CSS_CUSTOM


// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSPrimitiveValue
JSCSS.CSSPrimitiveValue = function(cssText, cssValueType, primitiveType) {
    JSCSS.CSSValue.apply(arguments, this);
};
JSCSS.CSSPrimitiveValue.prototype = new JSCSS.CSSValue(null, null);
JSCSS.PrimitiveValue.prorotype.setFloatValue = function(unitType, floatValue) {
    // TODO
};
JSCSS.PrimitiveValue.prorotype.getFloatValue = function(unitType) {
    // TODO
};
JSCSS.PrimitiveValue.prorotype.setStringValue = function(stringType, stringValue) {
    // TODO
};
JSCSS.PrimitiveValue.prorotype.getStringValue = function() {
    // TODO
};
JSCSS.PrimitiveValue.prorotype.getCounterValue = function() {
    // TODO
};
JSCSS.PrimitiveValue.prorotype.getRectValue = function() {
    // TODO
};
JSCSS.PrimitiveValue.prorotype.getRGBColorValue = function() {
    // TODO
};
JSCSS.CSSPrimitiveValue.prototype.primitiveType = null;
JSCSS.CSSPrimitiveValue.prototype.CSS_UNKNOWN = JSCSS.CSSPrimitiveValue.CSS_UNKNOWN = 0;
JSCSS.CSSPrimitiveValue.prototype.CSS_NUMBER = JSCSS.CSSPrimitiveValue.CSS_NUMBER = 1;
JSCSS.CSSPrimitiveValue.prototype.CSS_PERCENTAGE = JSCSS.CSSPrimitiveValue.CSS_PERCENTAGE = 2;
JSCSS.CSSPrimitiveValue.prototype.CSS_EMS = JSCSS.CSSPrimitiveValue.CSS_EMS = 3;
JSCSS.CSSPrimitiveValue.prototype.CSS_EXS = JSCSS.CSSPrimitiveValue.CSS_EXS = 4;
JSCSS.CSSPrimitiveValue.prototype.CSS_PX = JSCSS.CSSPrimitiveValue.CSS_PX = 5;
JSCSS.CSSPrimitiveValue.prototype.CSS_CM = JSCSS.CSSPrimitiveValue.CSS_CM = 6;
JSCSS.CSSPrimitiveValue.prototype.CSS_MM = JSCSS.CSSPrimitiveValue.CSS_MM = 7;
JSCSS.CSSPrimitiveValue.prototype.CSS_IN = JSCSS.CSSPrimitiveValue.CSS_IN = 8;
JSCSS.CSSPrimitiveValue.prototype.CSS_PT = JSCSS.CSSPrimitiveValue.CSS_PT = 9;
JSCSS.CSSPrimitiveValue.prototype.CSS_PC = JSCSS.CSSPrimitiveValue.CSS_PC = 10;
JSCSS.CSSPrimitiveValue.prototype.CSS_DEG = JSCSS.CSSPrimitiveValue.CSS_DEG = 11;
JSCSS.CSSPrimitiveValue.prototype.CSS_RAD = JSCSS.CSSPrimitiveValue.CSS_RAD = 12;
JSCSS.CSSPrimitiveValue.prototype.CSS_GRAD = JSCSS.CSSPrimitiveValue.CSS_GRAD = 13;
JSCSS.CSSPrimitiveValue.prototype.CSS_MS = JSCSS.CSSPrimitiveValue.CSS_MS = 14;
JSCSS.CSSPrimitiveValue.prototype.CSS_S = JSCSS.CSSPrimitiveValue.CSS_S = 15;
JSCSS.CSSPrimitiveValue.prototype.CSS_HZ = JSCSS.CSSPrimitiveValue.CSS_HZ = 16;
JSCSS.CSSPrimitiveValue.prototype.CSS_KHZ = JSCSS.CSSPrimitiveValue.CSS_KHZ = 17;
JSCSS.CSSPrimitiveValue.prototype.CSS_DIMENSION = JSCSS.CSSPrimitiveValue.CSS_DIMENSION = 18;
JSCSS.CSSPrimitiveValue.prototype.CSS_STRING = JSCSS.CSSPrimitiveValue.CSS_STRING = 19;
JSCSS.CSSPrimitiveValue.prototype.CSS_URI = JSCSS.CSSPrimitiveValue.CSS_URI = 20;
JSCSS.CSSPrimitiveValue.prototype.CSS_IDENT = JSCSS.CSSPrimitiveValue.CSS_IDENT = 21;
JSCSS.CSSPrimitiveValue.prototype.CSS_ATTR = JSCSS.CSSPrimitiveValue.CSS_ATTR = 22;
JSCSS.CSSPrimitiveValue.prototype.CSS_COUNTER = JSCSS.CSSPrimitiveValue.CSS_COUNTER = 23;
JSCSS.CSSPrimitiveValue.prototype.CSS_RECT = JSCSS.CSSPrimitiveValue.CSS_RECT = 24;
JSCSS.CSSPrimitiveValue.prototype.CSS_RGBCOLOR = JSCSS.CSSPrimitiveValue.CSS_RGBCOLOR = 25;


// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSValueList
JSCSS.CSSValueList = function(cssText, cssValueType, primitiveType) {
    var self = [];
    JSCSS.CSSValue.apply(arguments, self);
    for (var name in JSCSS.CSSValueList.prototype) {
        self[name] = JSCSS.CSSValueList.prototype[name];
    }
    return self;
};
JSCSS.CSSValueList.prototype = new JSCSS.CSSValue(null, null);
JSCSS.CSSValueList.prototype.item = function(index) {
    return this[index];
};
JSCSS.CSSValueList.prototype.constructor = JSCSS.CSSValueList;
JSCSS.CSSValueList.CSS_INHERIT = JSCSS.CSSValue.CSS_INHERIT
JSCSS.CSSValueList.CSS_PRIMITIVE_VALUE = JSCSS.CSSValue.CSS_PRIMITIVE_VALUE
JSCSS.CSSValueList.CSS_VALUE_LIST = JSCSS.CSSValue.CSS_VALUE_LIST
JSCSS.CSSValueList.CSS_CUSTOM = JSCSS.CSSValue.CSS_CUSTOM

// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-RGBColor
JSCSS.RGBColor = function(red, green, blue) {
    this.red = red;
    this.green = green;
    this.blue = blue;
};
JSCSS.RGBColor.prototype = {
    red: null,
    green: null,
    blue: null,
    constructor: JSCSS.RGBColor
};

// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-Rect
JSCSS.Rect = function(top, right, bottom, left) {
    this.top = top;
    this.right = right;
    this.bottom = bottom;
    this.left = left;
};
JSCSS.Rect.prototype = {
    top: null,
    right: null,
    bottom: null,
    left: null,
    constructor: JSCSS.Rect
};

// http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-Counter
JSCSS.Counter = function() {
    this.identifier = identifier;
    this.listStyle = listStyle;
    this.separator = separator;
};
JSCSS.Counter.prototype = {
    identifier: null,
    listStyle: null,
    separator: null,
    constructor: JSCSS.Counter
};

MediaQuery まとめ

Media Query とは

CSS3 から導入される Media Type を大幅に拡張する仕様です。

そもそも Media Type とは

Media Type とは CSS 2 系に元々ある仕様で CSS を適用するメディア(パソコンの画面、テレビ、プリンタ用紙など)を指定するときに使います。
以下の例だと、 media 属性の中に記述されている all や screen や print が Media Type です。

<link rel="stylesheet" type="text/css" href="共通の.css" media="all" />
<link rel="stylesheet" type="text/css" href="パソコンの画面の.css" media="screen" />
<link rel="stylesheet" type="text/css" href="プリンタ用紙の.css" media="print" />

以下のようにカンマで区切って複数指定することもできます。

<link rel="stylesheet" type="text/css" href="テレビとパソコンの画面の.css" media="tv, screen" />

見たことありますよね!
他にも @media ルールや @import ルールでも使うことができます。

@import url("unko.css") tv, print; /* ← これこれ! */
@media tv, screen { /* ← これこれ! */
}

というわけで、 Media Type の復習でした!
もっと Media Type の細かい仕様を知りたい人は以下をどうぞ
Media types

Media Query は Media Type の拡張

で、 Media Query の話にもどります。 Media Query は Media Type の拡張です。
書ける場所も、 Media Type と同じで、

  • link 要素の media 属性
  • @media ルール
  • @import ルール

です。
Media Type と下位互換性があるので、 Media Type は Media Query の一つと考えることもできます。

Media Query では Media Type と比べて何が出来るようになっているのか

簡単にいうと、 CSS を適用するメディアを Media Type よりも細かく指定できるようになります。
抽象的に言っても分かりにくいので、実際に見てみましょう。

<link rel="stylesheet" type="text/css" href="おっきいウィンドウ用.css" media="screen and (min-height: 600px)" />
<link rel="stylesheet" type="text/css" href="ちっちゃいウィンドウ用.css" media="screen and (max-height: 599px)" />
<link rel="stylesheet" type="text/css" href="カラーディスプレイ.css" media="screen and (color)" />
<link rel="stylesheet" type="text/css" href="白黒ディスプレイ.css" media="screen and (monochrome)" />

こんな感じです。なんとなく、わかりますね^^
and と 括弧の中 が Media Type から Media Query への新しい部分です。

Media Query の文法

では、 Media Query の文法を見てみましょう

カンマと and

まず、 Media Query も Media Type と同じようにカンマ区切りで複数指定できます。

screen and (min-height: 600px) and (min-width: 300px), screen and (min-width: 400px), print

こうすると screen and (min-height: 600px) and (min-width: 300px) と screen and (min-width: 400px) と print の、「どれかの条件を満たすメディア」という意味になります。
つまり or のような働きをする訳ですね。(Media Query には or はありません)
次に、 and で区切られます。

screen and (min-height: 600px) and (min-width: 300px)

これは、さっきの例でも出てきましたが screen で (min-height: 600px) と (min-width: 300px) の、「すべての条件を満たすメディア」という意味になります。
ここまで、カンマと and の区切りを紹介しましたが、これはあくまでも区切りです。
以下のように、ネストさせることはできません。

((A and B), ((C, D) and E))
not と only

カンマで区切られた条件には、 not と only というプリフィックスを付けることができます。

not screen and (min-height: 600px) and (min-width: 300px)

not はその名の通り、それ以降の条件の意味を逆にします。

only screen and (min-height: 600px) and (min-width: 300px)

また、 only は何もしません。ただ単に、Media Query を理解できない UA (ブラウザ)に条件判定を失敗させるためにあります。

Media Type と Media Expression

カンマと and で区切られた条件は、以下の二つの種類があります。

  • Media Type
  • Media Expression

Media Type は、CSS2.1 の時代からあった screen や tv や print や all などです。
Media Type は必ず最初に一つ必要です。つまり、 and で区切られた条件の最初は絶対 Media Type です。
以下に Media Type の一覧を示します。

  • all
  • aural
  • braille
  • handheld
  • print
  • projection
  • screen
  • tty
  • tv
  • embossed

Media Expression はさきほどから出てきている (min-height: 600px) などです。
Media Expression は必ず丸括弧で囲まれています。

Media Feature と 値

Media Expression の丸括弧の中身は、 Media Feature と値がコロンで区切られて一個ずつです。
Media Feature は min-height とかで、値が 600px とかですね。
Media Feature が決まれば、 値がとるべき値の単位や型が決まります。
また、値を省略すると 0 ではない値という意味になります。(つまり、 (color) と書くと「(color:0) 以外」という意味)
以下に Media Feature の一覧を示します。

  • width, min-width, max-width
    • view port の横幅、つまりウィンドウの横幅 (css の長さの単位を使える)
  • height, min-height, max-height
    • view port の高さ、つまりウィンドウの高さ (css の長さの単位を使える)
  • device-width, min-device-width, max-device-width
    • バイスの横幅、つまりディスプレイの横幅 (css の長さの単位を使える)
  • device-height, min-device-height, max-device-height
    • バイスの高さ、つまりディスプレイの高さ(css の長さの単位を使える)
  • device-aspect-ratio, min-device-aspect-ratio, max-device-aspect-ratio
  • color, min-color, max-color
    • 256bit カラーなら 8 、 8bit カラーなら 3 というように bit を二進数で表したときの桁。カラーデバイスじゃないなら 0
  • color-index, min-color-index, max-color-index
    • バイスのカラールックアップ表のエントリーの数(??)
  • monochrome, min-monochrome, max-monochrome
    • 256bit モノクロなら 8 、 8bit モノクロなら 3 というように bit を二進数で表したときの桁。モノクロデバイスじゃないなら 0
  • resolution, min-resolution, max-resolution
    • 解像度(dpi、 dpcm という単位を使う)
  • scan
    • プログレッシブか、インタレースか(テレビだけ)
  • grid
    • グリッドの幅を指定する

ちなみに、 min- max- は以上、以下という意味です。たとえば、 (min-height: 300px) だと「ウィンドウの高さが 300px 以上」という意味。

ブラウザの実装状況

現在、 Media Query は以下の環境で、使うことができます。

Firefox では、 3 系も含めて使えませんでした><
IE でも使えません><

JavaScript での扱い

JavaScript では、 Media Type は MediaList オブジェクトで扱いますが、 Media Query はどのようになっているのでしょうか。
少し調べてみたところ、 DOM 2 Style の次期仕様である CSSOM の Editor's Draft に書きかけの項がありました。
CSS Object Model (CSSOM)
ここを見る限りでは、文字列にシリアライズ する方法はかなり細かく決まっているみたいですね。
今のところは、データ構造としては扱えなくて、MediaList の appendMedium メソッドで文字列を突っ込んだり、item メソッドシリアライズされた文字列を取得したりする感じになるのかな。
ちょっと残念

仕様

詳しい仕様はこちら、
Media Queries

まとめ

Media Query についてまとめてみました。

「Acid2 test」と「マージンの相殺」のちょっとした疑問


マージンの相殺の仕様を読み直していて

Vertical margins may collapse between certain boxes:

Box model

と書いてあることに気がついた。
マージンの相殺はあくまで「may」なのだ。
なので、マージンの相殺をしないブラウザでも CSS 2.1 準拠とうたうことは出来る訳だ。

Acid2 test の場合はマージンの相殺が必須

Acid2 test で表示されるスマイルマークのおでこ部分のボックスと鼻部分のボックスはマージンの相殺が行われてあの距離を保っている。
「may」のものもテストの対象になっているんだなあ。

それってどうなんだろう。

「may」なのも「どうなのそれ?」って思うし
「may」なのにテスト対象なのも「どうなのそれ?」って思う。
でも、 UA 間で仕様が統一されてなかったら、それはそれで仕事が大変なんだろうなって思う
「理想」と「現実」ってとこですか。