WebKit: CSSプロパティと値の組(宣言)のパース
今回はCSSのプロパティと値の組(宣言)についてWebKitの実装を読んでみる。
つまり、CSSの以下の仕様。
4.1.8 Declarations and properties
A declaration is either empty or consists of a property name, followed by a colon (:), followed by a property value. Around each of these there may be white space.
WebKitではCSSのパースはbisonで実現されている。
実装を見てみる。
Source/WebCore/css/CSSGrammar.y.in
declaration: [...] | property ':' maybe_space expr prio { $$ = false; bool isPropertyParsed = false; if ($1 && $4) { parser->m_valueList = parser->sinkFloatingValueList($4); int oldParsedProperties = parser->m_parsedProperties.size(); $$ = parser->parseValue(static_cast<CSSPropertyID>($1), $5); if (!$$) parser->rollbackLastProperties(parser->m_parsedProperties.size() - oldParsedProperties); else isPropertyParsed = true; parser->m_valueList = nullptr; } parser->markPropertyEnd($5, isPropertyParsed); } [...] property: IDENT maybe_space { $$ = cssPropertyID($1); } ; prio: IMPORTANT_SYM maybe_space { $$ = true; } | /* empty */ { $$ = false; } ; expr: term { $$ = parser->createFloatingValueList(); $$->addValue(parser->sinkFloatingValue($1)); } | expr operator term { $$ = $1; if ($$) { if ($2) { CSSParserValue v; v.id = 0; v.unit = CSSParserValue::Operator; v.iValue = $2; $$->addValue(v); } $$->addValue(parser->sinkFloatingValue($3)); } } | expr invalid_block_list { $$ = 0; } | expr invalid_block_list error { $$ = 0; } | expr error { $$ = 0; } ; [...] operator: '/' maybe_space { $$ = '/'; } | ',' maybe_space { $$ = ','; } | /* empty */ { $$ = 0; } ;
「property ':' maybe_space expr prio」がポイントで、プロパティと値の組(宣言)を表している。
例えば、
h1 { font-family: sans-serif !important }
があった場合、propertyは「font-family」、maybe_spaceは「半角スペース」、exprは「sans-serif」、prioは「true」になる。
表にまとめると、
規則 | 説明 | バインド先 |
---|---|---|
property | プロパティ名 | $1 |
: | コロン | $2 |
maybe_space | 省略可能なスペース | $3 |
expr | プロパティ値(式) | $4 |
prio | !important宣言の有無 | $5 |
sinkFloatingValueList($4)でパース中のプロパティ値リスト($4)をCSSParserのm_valueListに一時保存する。
一時保存されたプロパティ値リストは、プロパティ値のパース中に参照される。
パースが済んだら、一時保存した値リストを白紙に戻す。
ちなみに、プロパティ値リストの要素数は大抵の場合、1つになりそうだが、font-familyプロパティのようなもので活躍するのだろう。
15.3 Font family: the 'font-family' property
The property value is a prioritized list of font family names and/or generic family names. Unlike most other CSS properties, component values are separated by a comma to indicate that they are alternatives:
body { font-family: Gill, Helvetica, sans-serif }
http://www.w3.org/TR/CSS21/fonts.html#font-family-prop
CSSParserは、パース済みのプロパティーリストを保持している。
もしプロパティ値のパースに失敗したら、パース中に追加されたプロパティーを無かったことに(ロールバック)する。
rollbackLastProperties()は巻き戻すプロパティ数を引数に取る。これから、パース済のプロパティリストは順序付のデータ構造であることが読み取れる。実際に実装を確認すると、Vectorで実現されていた。
Source/WebCore/css/CSSParser.h
class CSSParser {
[...]
ParsedPropertyVector m_parsedProperties;
少し戻ってproperty規則を見てみる。
property規則のアクションでcssPropertyID()が呼ばれている。これは、プロパティ値(文字列)からハッシュ表をもとにID(整数)に変換しているだけの処理。目的は高速化で、CSSのパース中やその後の処理で文字列よりも軽量な整数IDに変換しておいたほうが、速度面で有利だから。
cssPropertyID()で少し面白かったのは、
- -apple-や-khtml-といったレガシーなベンダープレフィックスを-webkit-に置換するコンパイルオプションの存在
- CSSに含まれる-webkit-ベンダープレフィックスの利用頻度の統計情報を取得できそうなフックの存在
Source/WebCore/css/CSSParser.cpp
template <typename CharacterType> static CSSPropertyID cssPropertyID(const CharacterType* propertyName, unsigned length) { char buffer[maxCSSPropertyNameLength + 1 + 1]; // 1 to turn "apple"/"khtml" into "webkit", 1 for null character for (unsigned i = 0; i != length; ++i) { CharacterType c = propertyName[i]; if (c == 0 || c >= 0x7F) return CSSPropertyInvalid; // illegal character buffer[i] = toASCIILower(c); } buffer[length] = '\0'; const char* name = buffer; if (buffer[0] == '-') { #if ENABLE(LEGACY_CSS_VENDOR_PREFIXES) // If the prefix is -apple- or -khtml-, change it to -webkit-. // This makes the string one character longer. if (hasPrefix(buffer, length, "-apple-") || hasPrefix(buffer, length, "-khtml-")) { memmove(buffer + 7, buffer + 6, length + 1 - 6); memcpy(buffer, "-webkit", 7); ++length; } #endif #if PLATFORM(IOS) cssPropertyNameIOSAliasing(buffer, name, length); #endif } const Property* hashTableEntry = findProperty(name, length); const CSSPropertyID propertyID = hashTableEntry ? static_cast<CSSPropertyID>(hashTableEntry->id) : CSSPropertyInvalid; static const int cssPropertyHistogramSize = numCSSProperties; if (hasPrefix(buffer, length, "-webkit-") && propertyID != CSSPropertyInvalid) { int histogramValue = propertyID - firstCSSProperty; ASSERT(0 <= histogramValue && histogramValue < cssPropertyHistogramSize); HistogramSupport::histogramEnumeration("CSS.PrefixUsage", histogramValue, cssPropertyHistogramSize); } return propertyID; }
ちなみに、ハッシュテーブルは、Source/WebCore/css/makeprop.plのperlスクリプトからCSSPropertyNames.inを読み込んで生成される。
Source/WebCore/css/CSSPropertyNames.in
// // CSS property names // // Some properties are used internally, but are not part of CSS. They are used to get // HTML4 compatibility in the rendering engine. // // Microsoft extensions are documented here: // http://msdn.microsoft.com/workshop/author/css/reference/attributes.asp // // high-priority property names have to be listed first, to simplify the check // for applying them first. color direction display font font-family font-size [...]
CSSParser::parseValue()にもぐってみる。CSSPropertyIDに対応するswitch文が大量に存在することがわかる。ここではfont-familyのパースを追いかけてみる。
先ほど見たプロパティ値リストであるm_valueListが早速登場した。
m_valueList->current()で先頭を取得して、値が無くなるまでwhile (value)でループしてるのがわかる。
実装詳細はまだ読み解けないが、コンマ区切りのリスト構造を構築していることはわかる。
Source/WebCore/css/CSSParser.cpp
bool CSSParser::parseValue(CSSPropertyID propId, bool important) { [...] case CSSPropertyFontFamily: // [[ <family-name> | <generic-family> ],]* [<family-name> | <generic-family>] | inherit { parsedValue = parseFontFamily(); break; } [...] PassRefPtr<CSSValueList> CSSParser::parseFontFamily() { RefPtr<CSSValueList> list = CSSValueList::createCommaSeparated(); CSSParserValue* value = m_valueList->current(); FontFamilyValueBuilder familyBuilder(list.get()); bool inFamily = false; while (value) { if (value->id == CSSValueInitial || value->id == CSSValueInherit || value->id == CSSValueDefault) return 0; CSSParserValue* nextValue = m_valueList->next(); bool nextValBreaksFont = !nextValue || (nextValue->unit == CSSParserValue::Operator && nextValue->iValue == ','); bool nextValIsFontName = nextValue && ((nextValue->id >= CSSValueSerif && nextValue->id <= CSSValueWebkitBody) || (nextValue->unit == CSSPrimitiveValue::CSS_STRING || nextValue->unit == CSSPrimitiveValue::CSS_IDENT)); if (value->id >= CSSValueSerif && value->id <= CSSValueWebkitBody) { if (inFamily) familyBuilder.add(value->string); else if (nextValBreaksFont || !nextValIsFontName) list->append(cssValuePool().createIdentifierValue(value->id)); else { familyBuilder.commit(); familyBuilder.add(value->string); inFamily = true; } } else if (value->unit == CSSPrimitiveValue::CSS_STRING) { // Strings never share in a family name. inFamily = false; familyBuilder.commit(); list->append(cssValuePool().createFontFamilyValue(value->string)); } else if (value->unit == CSSPrimitiveValue::CSS_IDENT) { if (inFamily) familyBuilder.add(value->string); else if (nextValBreaksFont || !nextValIsFontName) list->append(cssValuePool().createFontFamilyValue(value->string)); else { familyBuilder.commit(); familyBuilder.add(value->string); inFamily = true; } } else { break; } if (!nextValue) break; if (nextValBreaksFont) { value = m_valueList->next(); familyBuilder.commit(); inFamily = false; } else if (nextValIsFontName) value = nextValue; else break; } familyBuilder.commit(); if (!list->length()) list = 0; return list.release(); }