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 つの宣言が当たったとき
- ・同じ詳細度 (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} /* 未対応 */"),
}
}
}
手元で動かす(配布物)
- ステップ 8 の 1 ファイル版(step08_style.rs)実行すると要素ごとの計算値と、当たった宣言の並びが出る。テスト 38 本
学んだ概念を、素朴な例から実務寄りの例まで3つの具体例で確認します。
公式ドキュメントで詳しく ↗