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 の構文解析のお話をしたいと思います。(関係ない)
話は変わって
最近、僕の中では CSS (の実装)ブームです。
前回のパーサブームでは JavaScript で XPath を作ったわけですが、そのときはパーサを手で書いていました。
じゃあ、今回は 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 }
がパースされるってことか!!
まとめ
すっきりした!
すっきりした!
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
- 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
- device-height, min-device-height, max-device-height
- 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 は以下の環境で、使うことができます。
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 間で仕様が統一されてなかったら、それはそれで仕事が大変なんだろうなって思う
「理想」と「現実」ってとこですか。