WebKit: fontショートハンドプロパティのパース

今回はショートハンドプロパティであるfontプロパティのパースについて見ていきます。
ここだけ読んでも、CSS2.1の仕様とWebKitの実装の対応関係がとれているのがなんとなく見えてきます。

15.8 Shorthand font property: the 'font' property

'font'
Value: [ [ <'font-style'> || <'font-variant'> || <'font-weight'> ]? <'font-size'> [ / <'line-height'> ]? <'font-family'> ] | caption | icon | menu | message-box | small-caption | status-bar | inherit
Initial: see individual properties
Applies to: all elements
Inherited: yes
Percentages: see individual properties
Media: visual
Computed value: see individual properties

http://www.w3.org/TR/CSS21/fonts.html#font-shorthand

前回見た通り、プロパティ値のパースはCSSParser::parseValue()が入り口になっている。
fontプロパティに対応するcaseが見つかる。

Source/WebCore/css/CSSParser.cpp

bool CSSParser::parseValue(CSSPropertyID propId, bool important)
{
[...]
    case CSSPropertyFont:
        // [ [ 'font-style' || 'font-variant' || 'font-weight' ]? 'font-size' [ / 'line-height' ]?
        // 'font-family' ] | caption | icon | menu | message-box | small-caption | status-bar | inherit
        if (id >= CSSValueCaption && id <= CSSValueStatusBar)
            validPrimitive = true;
        else
            return parseFont(important);
        break;
[...]

CSSParser::parseFont()を見る。

Source/WebCore/css/CSSParser.cpp

// [ 'font-style' || 'font-variant' || 'font-weight' ]? 'font-size' [ / 'line-height' ]? 'font-family'
bool CSSParser::parseFont(bool important)
{
    // Let's check if there is an inherit or initial somewhere in the shorthand.
    for (unsigned i = 0; i < m_valueList->size(); ++i) {
        if (m_valueList->valueAt(i)->id == CSSValueInherit || m_valueList->valueAt(i)->id == CSSValueInitial)
            return false;
    }

    ShorthandScope scope(this, CSSPropertyFont);
    // Optional font-style, font-variant and font-weight.
    bool fontStyleParsed = false;
    bool fontVariantParsed = false;
    bool fontWeightParsed = false;
    CSSParserValue* value;
    while ((value = m_valueList->current())) {
        if (!fontStyleParsed && isValidKeywordPropertyAndValue(CSSPropertyFontStyle, value->id, m_context)) {
            addProperty(CSSPropertyFontStyle, cssValuePool().createIdentifierValue(value->id), important);
            fontStyleParsed = true;
        } else if (!fontVariantParsed && (value->id == CSSValueNormal || value->id == CSSValueSmallCaps)) {
            // Font variant in the shorthand is particular, it only accepts normal or small-caps.
            addProperty(CSSPropertyFontVariant, cssValuePool().createIdentifierValue(value->id), important);
            fontVariantParsed = true;
        } else if (!fontWeightParsed && parseFontWeight(important))
            fontWeightParsed = true;
        else
            break;
        m_valueList->next();
    }

    if (!value)
        return false;

    if (!fontStyleParsed)
        addProperty(CSSPropertyFontStyle, cssValuePool().createIdentifierValue(CSSValueNormal), important, true);
    if (!fontVariantParsed)
        addProperty(CSSPropertyFontVariant, cssValuePool().createIdentifierValue(CSSValueNormal), important, true);
    if (!fontWeightParsed)
        addProperty(CSSPropertyFontWeight, cssValuePool().createIdentifierValue(CSSValueNormal), important, true);

    // Now a font size _must_ come.
    // <absolute-size> | <relative-size> | <length> | <percentage> | inherit
    if (!parseFontSize(important))
        return false;

    value = m_valueList->current();
    if (!value)
        return false;

    if (isForwardSlashOperator(value)) {
        // The line-height property.
        value = m_valueList->next();
        if (!value)
            return false;
        if (!parseLineHeight(important))
            return false;
    } else
        addProperty(CSSPropertyLineHeight, cssValuePool().createIdentifierValue(CSSValueNormal), important, true);

    // Font family must come now.
    RefPtr<CSSValue> parsedFamilyValue = parseFontFamily();
    if (!parsedFamilyValue)
        return false;

    addProperty(CSSPropertyFontFamily, parsedFamilyValue.release(), important);

    // FIXME: http://www.w3.org/TR/2011/WD-css3-fonts-20110324/#font-prop requires that
    // "font-stretch", "font-size-adjust", and "font-kerning" be reset to their initial values
    // but we don't seem to support them at the moment. They should also be added here once implemented.
    if (m_valueList->current())
        return false;

    return true;
}

CSS2.1の仕様書にもあるBNF記法が、関数の先頭行にコメントとして転記されている。この関数は、このコメントのBNFにそって処理が構成されている。

大きくは3つ。

  • 1. Optional font-style, font-variant and font-weight.
  • 2. Now a font size _must_ come.
  • 3. Font family must come now.

最初のコメントにある「Let's check if there is an inherit or initial somewhere in the shorthand.」ここはあまりよく理解できていない。inheritやinitialがプロパティ値に含まれている場合は、さっさと処理を切り上げているようだ。パースしても無駄になるからだろうか。

ShorthandScopeが目を引いた。ShorthandScopeはスコープをうまく使って、CSSParserでショートハンドプロパティのパースの状態管理を実現しているようだ。ShorthandScopeのローカル変数が作られたタイミングでコンストラクタが走り、スコープを抜けるタイミングでデストラクタが走る。CSSParser::parseFont()はreturnでスコープを抜けるケースが多くあり、手動で状態遷移するよりも、スコープにまかせたほうが漏れなく確実に所望の処理を行える。m_currentShorthandは初期状態がCSSPropertyInvalidなので、ショートハンドプロパティのパース中は、そのプロパティのIDを保持して、パースが終われば、元の状態のCSSPropertyInvalidに戻す、という動作をするのだろう。

class ShorthandScope {
    WTF_MAKE_FAST_ALLOCATED;
public:
    ShorthandScope(CSSParser* parser, CSSPropertyID propId) : m_parser(parser)
    {
        if (!(m_parser->m_inParseShorthand++))
            m_parser->m_currentShorthand = propId;
    }
    ~ShorthandScope()
    {
        if (!(--m_parser->m_inParseShorthand))
            m_parser->m_currentShorthand = CSSPropertyInvalid;
    }

private:
    CSSParser* m_parser;
};

ここからはそれほど面白くはなく、CSSの仕様とWebKitの実装がうまく対応がとれているなあ、といったくらい。

// Optional font-style, font-variant and font-weight. を見る。
font-style、font-variant、font-weightは省略可能なので、あればプロパティとして認識する。
もしも無ければ、「 cssValuePool().createIdentifierValue(CSSValueNormal)」で「normal」(デフォルト値)として認識するわけだろう。

次に // Now a font size _must_ come. でfont sizeをパースする。
「/」(Forward Slash Operator)が見つかったら、line heightをパースする。

最後に、Font family must come now. でfont familyをパースする。