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.

http://www.w3.org/TR/CSS21/syndata.html#declaration

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()で少し面白かったのは、

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();
}