← 解説「CSSOM — ルールは prelude と宣言ブロック、読めないものは捨てて先へ進む」に戻る
サンプルコードで身につける: CSSOM — ルールは prelude と宣言ブロック、読めないものは捨てて先へ進む
解説で学んだ概念を、3つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1qualified rule = prelude + 宣言ブロック、declaration = 名前 + 値
CSS Syntax Level 3 §5 の定義どおり、列を { まで読んだものを ()、{ から } までをのとして に入れます。宣言は「名前 : 値 ;」の繰り返しで、ここでは値を単語 1 つに限った素朴版です。実行すると 3 つのルールについて prelude(p / .note / #main)と、その中の Declaration が = value の形で出ます。
Rust
// CSSOM の形。CSS Syntax Level 3 §5: qualified rule = prelude(ここではセレクタ)+ 宣言のブロック、
// declaration = 名前 + 値。トークン列を "{" まで読めば prelude、"{" から "}" までが宣言のブロック。
// 宣言の中は "名前 : 値 ;" の繰り返し。ここでは値は単語 1 つに限った素朴版。
#[derive(Debug, Clone, PartialEq)]
pub enum CssToken { Ident(String), Hash(String), Delim(char), Colon, Semicolon, OpenCurly, CloseCurly }
#[derive(Debug, PartialEq)]
pub struct Declaration {
pub property: String,
pub value: String,
}
#[derive(Debug, PartialEq)]
pub struct QualifiedRule {
pub prelude: String, // セレクタの文字列(Selector への読み替えは次のサンプルで)
pub declarations: Vec<Declaration>,
}
/// 空白を捨てた素朴なトークン化(識別子・#名前・記号 1 文字)
fn tokenize(css: &str) -> Vec<CssToken> {
let cs: Vec<char> = css.chars().collect();
let (mut i, mut out) = (0, Vec::new());
while i < cs.len() {
let c = cs[i];
i += 1;
if c.is_whitespace() {
continue;
}
if c.is_ascii_alphabetic() || c == '#' {
let start = i;
while i < cs.len() && (cs[i].is_ascii_alphanumeric() || cs[i] == '-') {
i += 1;
}
let name: String = cs[start..i].iter().collect();
out.push(if c == '#' { CssToken::Hash(name) } else { CssToken::Ident(format!("{}{}", c, name)) });
} else {
out.push(match c { ':' => CssToken::Colon, ';' => CssToken::Semicolon, '{' => CssToken::OpenCurly, '}' => CssToken::CloseCurly, o => CssToken::Delim(o) });
}
}
out
}
fn text(t: &CssToken) -> String {
match t { CssToken::Ident(s) => s.clone(), CssToken::Hash(h) => format!("#{}", h), CssToken::Delim(c) => c.to_string(), o => format!("{:?}", o) }
}
pub fn parse(css: &str) -> Vec<QualifiedRule> {
let mut rules = Vec::new();
let mut tokens = tokenize(css).into_iter().peekable();
while tokens.peek().is_some() {
// prelude: "{" が来るまでのトークンをつないだもの
let mut prelude = String::new();
for t in tokens.by_ref() {
if t == CssToken::OpenCurly {
break;
}
prelude.push_str(&text(&t));
}
// 宣言のブロック: "名前 : 値 ;" を "}" まで
let mut declarations = Vec::new();
loop {
match tokens.next() {
None | Some(CssToken::CloseCurly) => break,
Some(CssToken::Semicolon) => continue,
Some(CssToken::Ident(property)) => {
if let (Some(CssToken::Colon), Some(value)) = (tokens.next(), tokens.next()) {
declarations.push(Declaration { property, value: text(&value) });
}
}
Some(_) => {}
}
}
rules.push(QualifiedRule { prelude, declarations });
}
rules
}
fn main() {
let css = "p { color: red; font-weight: bold } .note { background-color: #ffe } #main { display: block; }";
for rule in parse(css) {
println!("QualifiedRule prelude = {:?}", rule.prelude);
for d in &rule.declarations {
println!(" Declaration {} = {}", d.property, d.value);
}
}
}
2読めない宣言は捨ててルールは生かす、at-rule は読み飛ばす
CSS Syntax Level 3 §5 の設計は「壊れていても止まらない」ことです。: が無い宣言は次の ; までを捨てて同じルールの残りの宣言は生かし、@charset や @media のような at-rule は ; か対応する } まで丸ごと読み飛ばし、読めないセレクタ(div p)は Unsupported として残して何にも一致させません。実行すると p ルールは background-color だけが残り、2 つの at-rule を飛ばした記録と、捨てた宣言 "color red" の記録が出ます。
Rust
// 読めないものの捨て方。CSS Syntax Level 3 §5 の設計は「壊れていても止まらない」。壊れた宣言(":" が無い)は
// 次の ";" までを捨てて残りの宣言は生かす。at-rule は ";" か対応する "}" まで読み飛ばす。読めないセレクタは
// Unsupported として残す(どの要素にも当たらない)。1 か所の typo で CSS 全体が死ぬことはない。
#[derive(Debug, Clone, PartialEq)]
pub enum CssToken { Ident(String), Hash(String), AtKeyword(String), Delim(char), Colon, Semicolon, OpenCurly, CloseCurly }
#[derive(Debug, PartialEq)]
pub enum Selector { Type(String), Class(String), Id(String), Unsupported(String) }
#[derive(Debug, PartialEq)]
pub struct Declaration { pub property: String, pub value: String }
#[derive(Debug, PartialEq)]
pub struct QualifiedRule { pub selector: Selector, pub declarations: Vec<Declaration> }
fn text(t: &CssToken) -> String {
match t { CssToken::Ident(s) => s.clone(), CssToken::Hash(h) => format!("#{}", h), CssToken::Delim(c) => c.to_string(), o => format!("{:?}", o) }
}
fn tokenize(css: &str) -> Vec<CssToken> {
let cs: Vec<char> = css.chars().collect();
let (mut i, mut out) = (0, Vec::new());
while i < cs.len() {
let c = cs[i];
i += 1;
if c.is_whitespace() { continue; }
if c.is_ascii_alphabetic() || c == '#' || c == '@' {
let start = i;
while i < cs.len() && (cs[i].is_ascii_alphanumeric() || cs[i] == '-') { i += 1; }
let name: String = cs[start..i].iter().collect();
out.push(match c { '#' => CssToken::Hash(name), '@' => CssToken::AtKeyword(name), _ => CssToken::Ident(format!("{}{}", c, name)) });
} else {
out.push(match c { ':' => CssToken::Colon, ';' => CssToken::Semicolon, '{' => CssToken::OpenCurly, '}' => CssToken::CloseCurly, o => CssToken::Delim(o) });
}
}
out
}
pub fn parse(css: &str) -> (Vec<QualifiedRule>, Vec<String>) {
let (mut rules, mut log) = (Vec::new(), Vec::new());
let toks = tokenize(css);
let mut i = 0;
while i < toks.len() {
if let CssToken::AtKeyword(name) = &toks[i] {
// at-rule: ";" で終わるか、{ } が閉じるまで読み飛ばす
let mut depth = 0;
while i < toks.len() {
match toks[i] { CssToken::OpenCurly => depth += 1, CssToken::CloseCurly => depth -= 1, _ => {} }
i += 1;
if depth == 0 && matches!(toks[i - 1], CssToken::Semicolon | CssToken::CloseCurly) { break; }
}
log.push(format!("@{} を読み飛ばした", name));
continue;
}
let mut prelude = Vec::new();
while i < toks.len() && toks[i] != CssToken::OpenCurly { prelude.push(toks[i].clone()); i += 1; }
i += 1; // "{"
let selector = match prelude.as_slice() {
[CssToken::Ident(t)] => Selector::Type(t.clone()),
[CssToken::Hash(id)] => Selector::Id(id.clone()),
[CssToken::Delim('.'), CssToken::Ident(c)] => Selector::Class(c.clone()),
other => Selector::Unsupported(other.iter().map(text).collect::<Vec<_>>().join(" ")),
};
let mut declarations = Vec::new();
while i < toks.len() && toks[i] != CssToken::CloseCurly {
let start = i;
while i < toks.len() && !matches!(toks[i], CssToken::Semicolon | CssToken::CloseCurly) { i += 1; }
match &toks[start..i] {
[CssToken::Ident(p), CssToken::Colon, rest @ ..] if !rest.is_empty() => {
let value = rest.iter().map(text).collect::<Vec<_>>().join(" ");
declarations.push(Declaration { property: p.clone(), value });
}
[] => {}
broken => log.push(format!("宣言 {:?} は読めない → 捨てた(ルールは残す)", broken.iter().map(text).collect::<Vec<_>>().join(" "))),
}
if i < toks.len() && toks[i] == CssToken::Semicolon { i += 1; }
}
i += 1; // "}"
rules.push(QualifiedRule { selector, declarations });
}
(rules, log)
}
fn main() {
let css = "@charset utf-8; p { color red; background-color: blue } @media screen { p { color: red } } div p { color: red } a { color: navy }";
let (rules, log) = parse(css);
for r in &rules {
println!("{:?} 宣言 {} 個: {:?}", r.selector, r.declarations.len(), r.declarations.iter().map(|d| format!("{}: {}", d.property, d.value)).collect::<Vec<_>>());
}
for l in &log { println!("log: {}", l); }
}
3実務に写す: INI 風の設定を「読めない行は捨て、セクションは生かす」で読む
[section] と key = value からなる設定ファイルを、 と同じ「ルール = 名前 + 宣言の列」の形で持ち、CSS と同じ捨て方をします。= が無い行、名前が空の [] 、セクションの外にある行はそれぞれ記録して捨て、セクション自体は残します。実行すると [server] 2 個・[database] 2 個の項目と、捨てた 3 行の記録が行番号つきで出ます。
Rust
// 実務に写す: INI 風の設定ファイル("[section]" + "key = value")を読む。
// CSSOM と同じ形(ルール = 名前 + 宣言の列)で持ち、CSS と同じ捨て方をする:
// 読めない行は捨てて記録し、セクションは生かす。"#" で始まる行(コメント)は読み飛ばす。
#[derive(Debug, Default, PartialEq)]
pub struct Section {
pub name: String,
pub entries: Vec<(String, String)>, // CSS の Declaration(property, value)に相当
}
pub fn parse_ini(text: &str) -> (Vec<Section>, Vec<String>) {
let mut sections: Vec<Section> = Vec::new();
let mut log = Vec::new();
for (i, raw) in text.lines().enumerate() {
let line = raw.trim();
let lineno = i + 1;
if line.is_empty() || line.starts_with('#') {
continue; // CSS のコメントと同じ: トークンにならない
}
if let Some(inner) = line.strip_prefix('[') {
// "[name]" = 新しいルールの始まり(prelude に相当)
match inner.strip_suffix(']') {
Some(name) if !name.trim().is_empty() => sections.push(Section { name: name.trim().to_string(), ..Default::default() }),
_ => log.push(format!("{} 行目: セクション名が読めない {:?} → 捨てた", lineno, line)),
}
continue;
}
let Some(section) = sections.last_mut() else {
log.push(format!("{} 行目: セクションの外に行がある {:?} → 捨てた", lineno, line));
continue;
};
// "key = value": CSS の "name : value" と同じ。"=" が無ければその行だけ捨てる
match line.split_once('=') {
Some((k, v)) if !k.trim().is_empty() => section.entries.push((k.trim().to_string(), v.trim().to_string())),
_ => log.push(format!("{} 行目: '=' が無い {:?} → 捨てた(セクション [{}] は残す)", lineno, line, section.name)),
}
}
(sections, log)
}
fn main() {
let text = "\
# アプリの設定
orphan = 1
[server]
port = 8080
host localhost
timeout = 30
[]
[database]
url = postgres://db/app
pool = 10
";
let (sections, log) = parse_ini(text);
for s in §ions {
println!("[{}] {} 個: {:?}", s.name, s.entries.len(), s.entries);
}
for l in &log {
println!("log: {}", l);
}
}