Dev Study
← Rust で小さなブラウザを作る コース

19. カスケードと詳細度 — 当たった宣言を (A,B,C) と出現順で並べ、最後が勝つ

1 つのに複数のルールが当たったとき、どのが勝つかを決めるのがです。CSS Cascade Level 4 §6.1 の並べ替えは、出所と重要度( / ユーザー / 作者、!important)→ 文脈 → → 、の順で比べ、「文書順で最後の宣言が勝つ」と定めています。この実装は !important と文脈を省き、「UA 既定 < 作者」「作者の中は詳細度 → 出現順」だけを実装しています。

詳細度は Selectors Level 4 §17 の 3 つの数です。A = ID の数、B = クラス・・擬似クラスの数、C = 型セレクタ・擬似要素の数。比べ方は「A が大きい方が勝ち。同じなら B、同じなら C」。Rust のタプル (u32, u32, u32) の比較は辞書順なので、specificity() が返すタプルをそのまま sort のキーにできます。#x は (1,0,0)、.c は (0,1,0)、p は (0,0,1)、* は (0,0,0)。

参照実装の compute_style は、当たるルールの宣言を (詳細度, 出現順, 宣言) の組で集め、 で並べ、弱い順に apply_declaration で上書きしていきます。sort は安定ソートなので同じキーの順序は保たれますが、キーに出現順を入れてあるので同点は後の方が勝ちます。学習用の explain_cascade は、同じ並びを文字列で返します。前のステップの実行結果にある「↳」の行がそれで、最後の行がその の勝者です。

UA 既定(ブラウザの)は、仕様上は「UA 由来の通常の宣言」として作者より弱い出所です。参照実装はこれを apply_user_agent_defaults として、を当てる前に反映しています。h1 の 32px、a の青と下線、head / script / style の がここから来ます。作者が a { color: black } と書けば青は負けますが、下線は宣言していないので残ります。

本物のブラウザでは: は ::ResolveStyle() が起点で、ElementRuleCollector が棚から候補を集め、SelectorChecker::MatchSelector() が要素に当たるかを再帰で確かめます。照合は UA・User・Author の 3 つの起源(origin)ごとに MatchAllRules() で行い、集めた宣言を StyleCascade(style_cascade.cc)が起源・重要度・詳細度・順序で決着させます。この回の『(詳細度, 出現順) で安定ソートして後勝ち』は、その決着の作者スタイル部分です。Firefox の は rule tree に照合結果を持ち、rayon で木を並列に走査します(servo/components/style の に rayon、parallel.rs / traversal.rs)。

開発のどこに活かすか: 「効かない CSS」の原因の大半は、詳細度で負けているか、後ろの宣言に上書きされているかです。開発者ツールの Styles パネルで打ち消し線が引かれた宣言は、この並べ替えで負けた宣言そのものです。また、優先順位を (出所, 詳細度, 出現順) のタプルで比べる発想は、設定の多段上書き(既定値 < 変数 < 設定ファイル < コマンドライン引数)の実装にそのまま使えます。

点線の付いた語はタップすると説明が出ます。用語集を開く →

① 1 つの要素に 4 つの宣言が当たったとき

<p id="x" class="c"> に対して、順に p / .c / #x / p と書かれた color の宣言
p { color: blue }(0,0,1) 出現順 1
.c { color: green }(0,1,0) 出現順 2
#x { color: red }(1,0,0) 出現順 3
p { color: gray }(0,0,1) 出現順 4
(詳細度, 出現順) で昇順に並べる
p blue (0,0,1),1
p gray (0,0,1),4
.c green (0,1,0),2
#x red (1,0,0),3
弱い順に上書き
color = red最後に当てた #x が残る
  • ・同じ詳細度 (0,0,1) の blue と gray は出現順で gray が後。しかし .c と #x に負ける
  • ・UA 既定はこの列のさらに前に当てるので、作者の宣言が 1 つでもあれば負ける

並べ替えのキーをタプルにすると、仕様の「A → B → C → 出現順」がコードの 1 行(sort_by_key)になります。

サンプルコード(フレームワーク環境が必要なため表示のみ)

// src/renderer/style.rs の compute_style と、UA 既定・詳細度の部分
pub fn compute_style(el: &Element, sheet: &StyleSheet, parent: Option<&ComputedStyle>) -> ComputedStyle {
    let mut style = ComputedStyle::default();

    if let Some(p) = parent {
        style.color = p.color;
        style.font_size = p.font_size;
        style.text_decoration = p.text_decoration;
    }

    apply_user_agent_defaults(&mut style, &el.tag);

    // (詳細度, 出現順, 宣言) を集めて安定ソートする
    let mut matched: Vec<((u32, u32, u32), usize, &Declaration)> = Vec::new();
    for (order, rule) in sheet.rules.iter().enumerate() {
        // セレクタリストの中で最も詳細度が高いものが、そのルールの詳細度
        let best = rule
            .selectors
            .iter()
            .filter(|s| s.matches(el))
            .map(|s| s.specificity())
            .max();
        if let Some(spec) = best {
            for d in &rule.declarations {
                matched.push((spec, order, d));
            }
        }
    }
    matched.sort_by_key(|(spec, order, _)| (*spec, *order));
    for (_, _, d) in matched {
        apply_declaration(&mut style, d);
    }
    style
}

fn apply_user_agent_defaults(style: &mut ComputedStyle, tag: &str) {
    style.display = match tag {
        "head" | "style" | "script" | "title" | "meta" | "link" => Display::None,
        "html" | "body" | "div" | "p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol" | "li" | "section"
        | "article" | "header" | "footer" | "nav" | "main" | "pre" | "blockquote" | "form" | "table" | "hr" => {
            Display::Block
        }
        _ => Display::Inline,
    };
    match tag {
        "h1" => style.font_size = 32.0, // 2em
        "h2" => style.font_size = 24.0, // 1.5em
        "h3" => style.font_size = 18.72, // 1.17em
        "a" => {
            style.color = Color::BLUE;
            style.text_decoration = TextDecoration::Underline;
        }
        _ => {}
    }
}

// src/css/cssom.rs のセレクタの一致と詳細度
impl Selector {
    /// 要素に一致するか
    pub fn matches(&self, el: &Element) -> bool {
        match self {
            Selector::Universal => true,
            Selector::Type(t) => el.tag == *t,
            Selector::Class(c) => el.classes().contains(&c.as_str()),
            Selector::Id(i) => el.id() == Some(i.as_str()),
            Selector::Unsupported(_) => false,
        }
    }

    /// 詳細度 (A, B, C)。Selectors Level 4 §17:
    /// A = ID セレクタの数、B = クラス・属性・擬似クラスの数、C = 型セレクタ・擬似要素の数。
    /// 比較は A → B → C の順(辞書順)。タプルの比較がそのまま仕様の比較になる
    pub fn specificity(&self) -> (u32, u32, u32) {
        match self {
            Selector::Id(_) => (1, 0, 0),
            Selector::Class(_) => (0, 1, 0),
            Selector::Type(_) => (0, 0, 1),
            Selector::Universal | Selector::Unsupported(_) => (0, 0, 0),
        }
    }

    pub fn to_css(&self) -> String {
        match self {
            Selector::Universal => "*".to_string(),
            Selector::Type(t) => t.clone(),
            Selector::Class(c) => format!(".{c}"),
            Selector::Id(i) => format!("#{i}"),
            Selector::Unsupported(s) => format!("{s} /* 未対応 */"),
        }
    }
}

手元で動かす(配布物)

サンプルコードで身につける(3本) →

学んだ概念を、素朴な例から実務寄りの例まで3つの具体例で確認します。

公式ドキュメントで詳しく ↗