サンプルコードで身につける: 継承と初期値 — 宣言が無いとき、値はどこから来るか
解説で学んだ概念を、3つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1color / font-size は親の計算値、background-color は初期値
CSS Cascade Level 4 §7: が無いは、プロパティなら親の、そうでなければになります。inherit_from は子の計算値の出発点を作る関数で、color と font-size は親からもらい、background-color は初期値(None = transparent)にします。実行すると div の red / 20px / Some("yellow") に対して、宣言の無い span が red / 20px / None になることが出ます。
// 継承と初期値。CSS Cascade Level 4 §7: 宣言が無いプロパティは、継承プロパティなら親の計算値
// (inherited value)、そうでなければ初期値(initial value)になる。
// color / font-size は継承する。background-color は継承しない(初期値 transparent のまま)。
#[derive(Debug, Clone, PartialEq)]
pub struct ComputedStyle {
pub color: String,
pub font_size: f32, // px
pub background_color: Option<String>, // None = transparent(初期値)
}
impl Default for ComputedStyle {
/// 初期値。どの要素でもない「素の」値
fn default() -> Self {
ComputedStyle { color: "black".into(), font_size: 16.0, background_color: None }
}
}
/// 子の計算値の出発点を作る: 継承するものは親からもらい、しないものは初期値
pub fn inherit_from(parent: &ComputedStyle) -> ComputedStyle {
let initial = ComputedStyle::default();
ComputedStyle {
color: parent.color.clone(), // 継承する
font_size: parent.font_size, // 継承する
background_color: initial.background_color, // 継承しない → 初期値
}
}
fn main() {
// <div style="color: red; font-size: 20px; background-color: yellow"><span>text</span></div>
let mut div = ComputedStyle::default();
div.color = "red".into();
div.font_size = 20.0;
div.background_color = Some("yellow".into());
let span = inherit_from(&div); // span には宣言が 1 つも無い
println!("div = {:?}", div);
println!("span = {:?}", span);
println!();
println!("color : 親の値をもらった → {}", span.color);
println!("font-size : 親の値をもらった → {}px", span.font_size);
println!("background-color : 継承しないので初期値 → {:?} (transparent)", span.background_color);
}
2指定値 → 計算値: em と % は親の計算値に対する比率
計算値はを「プロパティ定義表の Computed Value の行に従って」解決した結果です(CSS Cascade Level 4 §4.4)。font-size なら はそのまま、em と % は親の計算値に対する比率、large などのは表の値で、子に継承されるのは計算値(px)です。実行すると html 16 → body 20px → div 1.5em = 30 → span 50% = 15 → b(宣言なし)15 → i large = 18 と、指定値と計算値の対応が 1 行ずつ出ます。
// 指定値(specified value)→ 計算値(computed value)。CSS Cascade Level 4 §4.4: 計算値は指定値を
// 「プロパティ定義表の Computed Value の行に従って」解決した結果。font-size なら、em と % は
// 親の計算値に対する比率、キーワード(large など)は表の値、px はそのまま。
// 継承で子に渡るのは「計算値」なので、親の "1.5em" は子には px になって届く。
#[derive(Debug, Clone, PartialEq)]
pub enum CssToken { Ident(String), Dimension(f64, String), Percentage(f64) }
#[derive(Debug, Clone, PartialEq)]
pub struct ComputedStyle {
pub font_size: f32, // 計算値は常に px
}
pub const INITIAL_FONT_SIZE: f32 = 16.0;
/// 指定値(トークン)を、親の計算値を使って計算値にする
pub fn compute_font_size(specified: Option<&CssToken>, parent: Option<&ComputedStyle>) -> f32 {
let inherited = parent.map(|p| p.font_size).unwrap_or(INITIAL_FONT_SIZE);
match specified {
None => inherited, // 宣言なし: 継承プロパティなので親の計算値
Some(CssToken::Dimension(n, unit)) if unit == "px" => *n as f32,
Some(CssToken::Dimension(n, unit)) if unit == "em" => inherited * *n as f32, // 親の計算値 × 倍率
Some(CssToken::Percentage(p)) => inherited * (*p as f32) / 100.0,
Some(CssToken::Ident(k)) => match k.as_str() {
"inherit" => inherited,
"initial" => INITIAL_FONT_SIZE,
"small" => 13.0, // 絶対サイズのキーワード(CSS Fonts の absolute-size)。medium = 16px とした比
"medium" => 16.0,
"large" => 18.0,
"x-large" => 24.0,
_ => inherited, // 読めない値は無視 = 宣言が無かったのと同じ
},
Some(_) => inherited,
}
}
fn main() {
// html > body > div > span > b それぞれの font-size の指定値
let chain: Vec<(&str, Option<CssToken>)> = vec![
("html", None),
("body", Some(CssToken::Dimension(20.0, "px".into()))),
("div", Some(CssToken::Dimension(1.5, "em".into()))),
("span", Some(CssToken::Percentage(50.0))),
("b", None),
("i", Some(CssToken::Ident("large".into()))),
];
let mut parent: Option<ComputedStyle> = None;
println!("{:<6} {:<10} {}", "tag", "specified", "computed(px)");
for (tag, specified) in &chain {
let font_size = compute_font_size(specified.as_ref(), parent.as_ref());
let spec_text = match specified {
None => "(none)".to_string(), // 宣言なし → 継承
Some(CssToken::Dimension(n, u)) => format!("{}{}", n, u),
Some(CssToken::Percentage(p)) => format!("{}%", p),
Some(CssToken::Ident(k)) => k.clone(),
};
println!("{:<6} {:<10} {}", tag, spec_text, font_size);
parent = Some(ComputedStyle { font_size });
}
}
3実務に写す: 組織 → チーム → プロジェクトの設定で継承する項目としない項目
階層をもつ設定にも「継承する項目」と「しない項目」があります。ログレベル・タイムゾーン・保持日数は上の階層の値を引き継ぎ、名前や担当者は引き継がず無ければ初期値、と項目ごとに決めて resolve(親の計算値, この階層の宣言) で解決します。実行すると timezone が org から 2 段継承され、owner は team / project で None、log_level は project 自身の debug が勝つ、という 3 階層の表が出ます。
// 実務に写す: 階層をもつ設定(組織 → チーム → プロジェクト)の「継承する項目」と「しない項目」。
// ログレベルやタイムゾーンは上から継承し、名前や担当者は継承しない(無ければ初期値)。
// CSS と同じく、宣言が無いときに「親の計算値をもらう」か「初期値にする」かを項目ごとに決めておく。
#[derive(Debug, Clone, PartialEq)]
pub struct Resolved {
pub name: String, // 継承しない(初期値: "(unnamed)")
pub owner: Option<String>, // 継承しない(初期値: None)
pub log_level: String, // 継承する(根の初期値: "info")
pub timezone: String, // 継承する(根の初期値: "UTC")
pub retention_days: u32, // 継承する(根の初期値: 30)
}
/// 1 階層ぶんの「宣言」。None は「この階層では指定していない」
#[derive(Debug, Default)]
pub struct Declared {
pub name: Option<String>,
pub owner: Option<String>,
pub log_level: Option<String>,
pub timezone: Option<String>,
pub retention_days: Option<u32>,
}
impl Resolved {
/// 根の既定(CSS の初期値に相当)
pub fn initial() -> Self {
Resolved { name: "(unnamed)".into(), owner: None, log_level: "info".into(), timezone: "UTC".into(), retention_days: 30 }
}
}
/// 親の計算値 + この階層の宣言 → この階層の計算値
pub fn resolve(parent: &Resolved, d: &Declared) -> Resolved {
let initial = Resolved::initial();
Resolved {
// 継承しない項目: 宣言が無ければ初期値
name: d.name.clone().unwrap_or(initial.name),
owner: d.owner.clone().or(initial.owner),
// 継承する項目: 宣言が無ければ親の計算値
log_level: d.log_level.clone().unwrap_or_else(|| parent.log_level.clone()),
timezone: d.timezone.clone().unwrap_or_else(|| parent.timezone.clone()),
retention_days: d.retention_days.unwrap_or(parent.retention_days),
}
}
fn main() {
let org = Declared { name: Some("ACME".into()), owner: Some("cto".into()), timezone: Some("Asia/Tokyo".into()), ..Default::default() };
let team = Declared { name: Some("platform".into()), log_level: Some("warn".into()), retention_days: Some(90), ..Default::default() };
let project = Declared { name: Some("billing".into()), log_level: Some("debug".into()), ..Default::default() };
let root = Resolved::initial();
let org_r = resolve(&root, &org);
let team_r = resolve(&org_r, &team);
let project_r = resolve(&team_r, &project);
for (label, r) in [("org", &org_r), ("team", &team_r), ("project", &project_r)] {
println!("{:<8} name={:<10} owner={:<12} log_level={:<6} timezone={:<11} retention={}",
label, r.name, format!("{:?}", r.owner), r.log_level, r.timezone, r.retention_days);
}
println!();
println!("owner は継承しないので team / project では None。timezone は org から 2 段継承。");
println!("retention は team の 90 が project に継承され、log_level は project 自身の debug が勝つ。");
}