← 解説「箱の木 — block・inline・text の 3 種類と、箱にならないもの」に戻る
サンプルコードで身につける: 箱の木 — block・inline・text の 3 種類と、箱にならないもの
解説で学んだ概念を、3つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1箱の 3 種と display: none
をにする最小の build_layout_tree です。は display の値で か のに、文字は text の箱になり、 の要素は None を返すので子孫を見に行きません。実行すると h1 と p の 2 つだけが body の子として出て、3 つ目の p が子孫ごと消えたことが最後の行で分かります。
Rust
// 箱の木(box tree): 要素は display の値に従って block / inline の箱に、文字は Text の箱になる。
// display: none の要素は箱を作らない。その子孫も丸ごと消える。
#[derive(Clone, Copy)]
enum Display { Block, Inline, None }
/// 最小の DOM。要素は (タグ, display, 子)、文字はそのまま
enum Dom {
Element { tag: &'static str, display: Display, children: Vec<Dom> },
Text(&'static str),
}
enum BoxKind { Block, Inline, Text(String) }
struct LayoutBox { kind: BoxKind, tag: String, children: Vec<LayoutBox> }
/// DOM → 箱。display: none なら None を返し、親はその子を積まない
fn build_layout_tree(node: &Dom) -> Option<LayoutBox> {
match node {
Dom::Element { tag, display, children } => {
let kind = match display {
Display::None => return None, // ここで子孫ごと消える(子を見に行かない)
Display::Block => BoxKind::Block,
Display::Inline => BoxKind::Inline,
};
let mut b = LayoutBox { kind, tag: tag.to_string(), children: Vec::new() };
for c in children {
if let Some(cb) = build_layout_tree(c) {
b.children.push(cb);
}
}
Some(b)
}
Dom::Text(t) => Some(LayoutBox { kind: BoxKind::Text(t.to_string()), tag: "#text".to_string(), children: Vec::new() }),
}
}
fn dump(b: &LayoutBox, depth: usize) {
let kind = match &b.kind {
BoxKind::Block => "block".to_string(),
BoxKind::Inline => "inline".to_string(),
BoxKind::Text(t) => format!("text {:?}", t),
};
println!("{}{:<12} <{}>", " ".repeat(depth), kind, b.tag);
for c in &b.children {
dump(c, depth + 1);
}
}
fn el(tag: &'static str, display: Display, children: Vec<Dom>) -> Dom {
Dom::Element { tag, display, children }
}
fn main() {
// <body><h1>Hello</h1><p>It has <a>a link</a></p><p class="gone">You should not see <b>this</b></p></body>
// UA の既定で body h1 p は block、a b は inline。.gone { display: none } が当たっている想定
let body = el("body", Display::Block, vec![
el("h1", Display::Block, vec![Dom::Text("Hello")]),
el("p", Display::Block, vec![Dom::Text("It has "), el("a", Display::Inline, vec![Dom::Text("a link")])]),
el("p", Display::None, vec![Dom::Text("You should not see "), el("b", Display::Inline, vec![Dom::Text("this")])]),
]);
let root = build_layout_tree(&body).expect("body は箱になる");
dump(&root, 0);
println!("body の子の箱: {} 個(DOM の子は 3 個。3 つ目の <p> は子孫ごと消えた)", root.children.len());
}
2空白の折りたたみと「箱にならない文字」
の中身を で 1 つの空白にまとめ、空白だけなら箱を作らない text_box です。タグの間の改行や字下げ("\n ")はここで消え、文字の前後の空白 1 つは残ります(隣の文字との間隔になる)。実行すると 6 個の Text ノードのうち箱になるのは 3 個と出ます。
Rust
// 文字の箱を作る前に、連続する空白(改行・タブ含む)を 1 つの空白に折りたたむ。
// 折りたたんだ結果が空白だけなら箱を作らない。タグとタグの間の改行や字下げは、これで画面から消える。
fn collapse_whitespace(text: &str) -> String {
let mut out = String::new();
let mut in_space = false;
for c in text.chars() {
if c.is_whitespace() {
if !in_space {
out.push(' ');
in_space = true;
}
} else {
out.push(c);
in_space = false;
}
}
out
}
/// Text ノードの中身 → 文字の箱の中身。箱を作らないなら None
fn text_box(text: &str) -> Option<String> {
let collapsed = collapse_whitespace(text);
if collapsed.trim().is_empty() {
return None;
}
Some(collapsed)
}
fn main() {
// <body> の直下にある Text ノードを、HTML に書かれた順に並べたもの
let text_nodes = [
"\n ", // <body> と <h1> の間
"Hello, tinybrowser", // <h1> の中
"\n ", // <h1> と <p> の間
"This page is\n rendered by\ta browser.", // <p> の中(改行して字下げした分)
" a link ", // <a> の中。前後の空白は残る(隣の文字との間隔)
"\t\t\n", // 最後の改行
];
let mut boxes = 0;
for t in text_nodes.iter() {
match text_box(t) {
Some(s) => {
boxes += 1;
println!("{:<48} -> text {:?}", format!("{:?}", t), s);
}
None => println!("{:<48} -> (箱を作らない)", format!("{:?}", t)),
}
}
println!("Text ノード {} 個のうち、箱になるのは {} 個", text_nodes.len(), boxes);
}
3実務に写す: 権限で消えるメニューの木
設定画面のメニュー定義から「表示する項目だけの木」を作る build_view_tree です。hidden の項目は display: none と同じく子孫ごと落とし、子が全部落ちたグループも(空白だけの文字が箱にならないのと同じ理屈で)落とします。実行すると「管理」と「実験的な機能」が消え、ルート直下が 4 個から 2 個になります。
Rust
// 実務に写す: 設定画面のメニュー定義(木)から「表示する項目だけの木」を作る。
// display: none と同じく、hidden の項目は子孫ごと落とす。
// 子が全部落ちて空になったグループは、空白だけの文字が箱にならないのと同じ理屈で落とす。
struct MenuItem { label: &'static str, hidden: bool, children: Vec<MenuItem> }
struct ViewItem { label: String, children: Vec<ViewItem> }
/// 定義の木 → 表示の木。表示しないなら None
fn build_view_tree(item: &MenuItem) -> Option<ViewItem> {
if item.hidden {
return None; // 子孫を見に行かない
}
let children: Vec<ViewItem> = item.children.iter().filter_map(build_view_tree).collect();
if !item.children.is_empty() && children.is_empty() {
return None; // グループなのに中身が残らなかった
}
Some(ViewItem { label: item.label.to_string(), children })
}
fn dump(v: &ViewItem, depth: usize) {
println!("{}- {}", " ".repeat(depth), v.label);
for c in &v.children {
dump(c, depth + 1);
}
}
fn item(label: &'static str, hidden: bool) -> MenuItem {
MenuItem { label, hidden, children: Vec::new() }
}
fn group(label: &'static str, hidden: bool, children: Vec<MenuItem>) -> MenuItem {
MenuItem { label, hidden, children }
}
fn main() {
// 一般ユーザーの権限で見たメニュー。管理系は hidden、ベータ機能のグループも hidden
let menu = group("設定", false, vec![
group("アカウント", false, vec![item("プロフィール", false), item("パスワード", false)]),
group("管理", false, vec![item("ユーザー一覧", true), item("監査ログ", true)]),
group("実験的な機能", true, vec![item("新しいエディタ", false)]),
item("ヘルプ", false),
]);
let view = build_view_tree(&menu).expect("ルートは表示される");
dump(&view, 0);
println!("定義の木のルート直下 {} 個 -> 表示の木のルート直下 {} 個", menu.children.len(), view.children.len());
}