サンプルコードで身につける: カスケードと詳細度 — 当たった宣言を (A,B,C) と出現順で並べ、最後が勝つ
解説で学んだ概念を、3つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1詳細度 (A, B, C) はタプル比較、同点なら出現順
Selectors Level 4 §17 のは A = ID の数、B = クラスなどの数、C = 型の数の 3 つ組で、A → B → C の順に比べます。Rust のタプル (u32, u32, u32) の比較は左から順に決まるので、そのまま仕様の比較になります。実行すると (1,0,0) > (0,99,99) が true になること、同じに当たる 4 つのルールを (詳細度, ) で並べると同点の p は後に書いた navy が blue の上に来て、最後に残る #x の red が勝つことが出ます。
// 詳細度(specificity)は 3 つの数 (A, B, C)。Selectors Level 4 §17:
// A = ID セレクタの数、B = クラス・属性・擬似クラスの数、C = 型セレクタ・擬似要素の数。
// 比べるときは A → B → C の順(辞書順)で、Rust のタプル比較 (u32, u32, u32) がそのまま仕様の比較になる。
// 同点なら後に書いた方(出現順が大きい方)が勝つ(CSS Cascade Level 4 §6.1 "Order of Appearance")。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Selector {
Universal,
Type(String),
Class(String),
Id(String),
}
impl Selector {
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 => (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),
}
}
}
fn main() {
// タプルの比較は左から順に決まる。(1,0,0) は B や C がいくら大きくても (0,99,99) より大きい
println!("(1,0,0) > (0,99,99) : {}", (1u32, 0u32, 0u32) > (0, 99, 99));
println!("(0,1,0) > (0,0,5) : {}", (0u32, 1u32, 0u32) > (0, 0, 5));
println!("(0,0,1) > (0,0,0) : {}", (0u32, 0u32, 1u32) > (0, 0, 0));
println!();
// 同じ要素 <p id="x" class="c"> に当たる 4 つのルールを、書いた順に並べる
let rules = vec![
(Selector::Type("p".into()), "blue"),
(Selector::Id("x".into()), "red"),
(Selector::Class("c".into()), "green"),
(Selector::Type("p".into()), "navy"), // p が 2 回。同点なので後の方が勝つ
];
// (詳細度, 出現順) をキーにして並べ替えると、最後に残ったものが勝者
let mut ordered: Vec<((u32, u32, u32), usize, &Selector, &str)> =
rules.iter().enumerate().map(|(i, (s, v))| (s.specificity(), i, s, *v)).collect();
ordered.sort_by_key(|(spec, order, _, _)| (*spec, *order));
println!("弱い順に並べる:");
for (spec, order, sel, value) in &ordered {
println!(" {:<4} 詳細度 {:?} 出現順 {} color: {}", sel.to_css(), spec, order + 1, value);
}
let (_, _, sel, value) = ordered.last().unwrap();
println!("勝者: {} → color: {}", sel.to_css(), value);
}
2UA 既定 → 作者の宣言を詳細度と出現順で順に当てる
CSS Cascade Level 4 §6 の順(出所 → 詳細度 → 出現順)で 1 要素のスタイルを決めます。まず (ブラウザ既定)のスタイルを当て、次に一致するを (詳細度, 出現順) でして順に上書きするので、最後に当てたが勝ちます。実行すると <a> は a ルールの navy、<a class="c"> は .c の purple、<a id="x" class="c"> は #x の red になり、どの宣言がどの順で当たったかの記録が出ます。
// カスケードを 1 要素ぶん最後まで回す。CSS Cascade Level 4 §6 の順: 出所(UA 既定 < 作者)→ 詳細度 → 出現順。
// 1. UA(ブラウザ既定)のスタイルを当てる 2. 一致する作者の宣言を (詳細度, 出現順) で安定ソートして順に当てる
// 後に当てたものが前を上書きするので、最後に当てた宣言 = 勝者。参照実装の compute_style と同じ流れ。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Selector { Type(String), Class(String), Id(String) }
pub struct Element { pub tag: String, pub id: Option<String>, pub classes: Vec<String> }
pub struct Declaration { pub property: String, pub value: String }
pub struct QualifiedRule { pub selector: Selector, pub declarations: Vec<Declaration> }
impl Selector {
pub fn matches(&self, el: &Element) -> bool {
match self {
Selector::Type(t) => el.tag == *t,
Selector::Class(c) => el.classes.contains(c),
Selector::Id(i) => el.id.as_deref() == Some(i.as_str()),
}
}
pub fn specificity(&self) -> (u32, u32, u32) {
match self { Selector::Id(_) => (1, 0, 0), Selector::Class(_) => (0, 1, 0), Selector::Type(_) => (0, 0, 1) }
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ComputedStyle { pub color: String, pub text_decoration: String }
fn user_agent_defaults(tag: &str) -> ComputedStyle {
match tag {
"a" => ComputedStyle { color: "blue".into(), text_decoration: "underline".into() },
_ => ComputedStyle { color: "black".into(), text_decoration: "none".into() },
}
}
pub fn compute_style(el: &Element, sheet: &[QualifiedRule]) -> (ComputedStyle, Vec<String>) {
let mut style = user_agent_defaults(&el.tag);
let mut log = vec![format!("UA 既定 → color: {}, text-decoration: {}", style.color, style.text_decoration)];
let mut matched: Vec<((u32, u32, u32), usize, &Declaration)> = Vec::new();
for (order, rule) in sheet.iter().enumerate() {
if rule.selector.matches(el) {
for d in &rule.declarations {
matched.push((rule.selector.specificity(), order, d));
}
}
}
matched.sort_by_key(|(spec, order, _)| (*spec, *order)); // 安定ソート: 同点は出現順が保たれる
for (spec, order, d) in matched {
match d.property.as_str() {
"color" => style.color = d.value.clone(),
"text-decoration" => style.text_decoration = d.value.clone(),
_ => {}
}
log.push(format!("詳細度 {:?} 出現順 {} → {}: {}", spec, order + 1, d.property, d.value));
}
(style, log)
}
fn main() {
let rule = |selector: Selector, decls: &[(&str, &str)]| QualifiedRule {
selector,
declarations: decls.iter().map(|(p, v)| Declaration { property: p.to_string(), value: v.to_string() }).collect(),
};
let sheet = vec![
rule(Selector::Type("a".into()), &[("color", "green"), ("text-decoration", "none")]),
rule(Selector::Id("x".into()), &[("color", "red")]),
rule(Selector::Class("c".into()), &[("color", "purple")]),
rule(Selector::Type("a".into()), &[("color", "navy")]),
];
let targets = [
("<a>", Element { tag: "a".into(), id: None, classes: vec![] }),
("<a class=\"c\">", Element { tag: "a".into(), id: None, classes: vec!["c".into()] }),
("<a id=\"x\" class=\"c\">", Element { tag: "a".into(), id: Some("x".into()), classes: vec!["c".into()] }),
];
for (label, el) in &targets {
let (style, log) = compute_style(el, &sheet);
println!("{}", label);
for l in &log {
println!(" {}", l);
}
println!(" => {:?}\n", style);
}
}
3実務に写す: 組み込み既定 < 設定ファイル < 環境変数 < コマンドライン
アプリ設定の優先順位は CSS のと同じ形です。出所(Origin)に順位を付け、(順位, 出現順) で並べて順に上書きすれば最後の値が勝ち、負けた値を記録しておけば「なぜこの値か」を説明できます。実行すると log_level は CommandLine の debug、 は EnvVar の 9000、workers は同じ ConfigFile で 2 回書かれたうち後の 16 が勝ち、それぞれ負けた値が添えられて出ます。
// 実務に写す: アプリ設定の優先順位(組み込み既定 < 設定ファイル < 環境変数 < コマンドライン引数)。
// CSS のカスケードと同じ形: 出所に順位を付け、(順位, 出現順) で並べて順に上書きすれば、最後の値が勝つ。
// どこから来た値かを記録しておくと「なぜこの値になったか」を説明できる(ブラウザの開発者ツールと同じ)。
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Origin {
BuiltinDefault, // UA 既定に相当
ConfigFile,
EnvVar,
CommandLine,
}
pub struct Setting {
pub origin: Origin,
pub key: String,
pub value: String,
}
/// 勝った値と、負けた値の一覧を返す
pub fn cascade(settings: &[Setting]) -> BTreeMap<String, (String, Origin, Vec<String>)> {
let mut ordered: Vec<(Origin, usize, &Setting)> = settings.iter().enumerate().map(|(i, s)| (s.origin, i, s)).collect();
ordered.sort_by_key(|(origin, order, _)| (*origin, *order)); // 出所 → 出現順
let mut result: BTreeMap<String, (String, Origin, Vec<String>)> = BTreeMap::new();
for (origin, _, s) in ordered {
let entry = result.entry(s.key.clone()).or_insert_with(|| (String::new(), origin, Vec::new()));
if !entry.0.is_empty() {
entry.2.push(format!("{:?}={}", entry.1, entry.0)); // 上書きされた側を記録
}
entry.0 = s.value.clone();
entry.1 = origin;
}
result
}
fn main() {
let s = |origin: Origin, key: &str, value: &str| Setting { origin, key: key.to_string(), value: value.to_string() };
// 書いた順はばらばら。出所の順位で並べ替えるので、並び順には依存しない
let settings = vec![
s(Origin::EnvVar, "port", "9000"),
s(Origin::BuiltinDefault, "port", "8080"),
s(Origin::BuiltinDefault, "log_level", "info"),
s(Origin::BuiltinDefault, "workers", "4"),
s(Origin::ConfigFile, "port", "8081"),
s(Origin::ConfigFile, "log_level", "warn"),
s(Origin::CommandLine, "log_level", "debug"),
s(Origin::ConfigFile, "workers", "8"),
s(Origin::ConfigFile, "workers", "16"), // 同じ出所で 2 回 → 後の方が勝つ(出現順)
];
for (key, (value, origin, overridden)) in cascade(&settings) {
println!("{:<10} = {:<6} ({:?})", key, value, origin);
if !overridden.is_empty() {
println!("{:<10} 負けた値: {}", "", overridden.join(", "));
}
}
}