Skip to main content

atmos/os_lib/
shadow_dom.rs

1//! Web Components: Shadow DOM 仕様準拠の純粋ロジックモジュール。
2//!
3//! WHATWG DOM Standard §4.4 "Shadow trees" 準拠。
4//! 外部依存を持たず、ホスト環境 (`tests/`) で完全に単体テスト可能。
5
6use alloc::collections::BTreeMap;
7use alloc::string::String;
8use alloc::vec::Vec;
9
10/// ShadowRoot のモード(`open` または `closed`)。
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ShadowRootMode {
13    Open,
14    Closed,
15}
16
17impl ShadowRootMode {
18    pub fn from_str(s: &str) -> Option<Self> {
19        match s {
20            "open" => Some(Self::Open),
21            "closed" => Some(Self::Closed),
22            _ => None,
23        }
24    }
25
26    pub fn as_str(&self) -> &'static str {
27        match self {
28            Self::Open => "open",
29            Self::Closed => "closed",
30        }
31    }
32}
33
34/// 要素に付加された ShadowRoot の情報。
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct ShadowRootData {
37    /// ShadowRoot 自身を表す仮想ルートノードの index(DocumentFragment 相当)。
38    pub root_node_idx: usize,
39    /// カプセル化モード。
40    pub mode: ShadowRootMode,
41    /// ホスト要素の index。
42    pub host_idx: usize,
43}
44
45impl ShadowRootData {
46    pub fn new(root_node_idx: usize, mode: ShadowRootMode, host_idx: usize) -> Self {
47        Self {
48            root_node_idx,
49            mode,
50            host_idx,
51        }
52    }
53
54    /// JS の `element.shadowRoot` ゲッターが返す値(open なら Some、closed なら None)。
55    pub fn is_accessible_via_element(&self) -> bool {
56        self.mode == ShadowRootMode::Open
57    }
58}
59
60/// シャドウツリーの管理レジストリ(DomBridge 等に組み込み可能)。
61#[derive(Debug, Clone, Default)]
62pub struct ShadowTreeRegistry {
63    /// host_idx -> ShadowRootData
64    roots_by_host: BTreeMap<usize, ShadowRootData>,
65    /// root_node_idx -> host_idx
66    hosts_by_root: BTreeMap<usize, usize>,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum AttachShadowError {
71    AlreadyHasShadowRoot,
72    InvalidMode,
73    DisallowedTag,
74}
75
76/// attachShadow が許可されていない予約タグ(HTML 仕様 §4.4.2)。
77const DISALLOWED_TAGS: &[&str] = &[
78    "button", "input", "textarea", "select", "option",
79    "table", "tr", "td", "th", "tbody", "thead", "tfoot",
80    "video", "audio", "canvas", "iframe", "embed", "object",
81    "img", "br", "hr", "wbr",
82];
83
84/// 指定されたタグ名に対して `attachShadow` が許可されているか。
85pub fn is_tag_allowed_for_shadow_root(tag_name: &str) -> bool {
86    let tag = tag_name.to_ascii_lowercase();
87    // カスタム要素(ハイフンを含む)は常に許可
88    if tag.contains('-') {
89        return true;
90    }
91    // 許可された HTML 組み込み要素: article, aside, blockquote, body, div, footer,
92    // h1-h6, header, main, nav, p, section, span
93    match tag.as_str() {
94        "article" | "aside" | "blockquote" | "body" | "div" | "footer" |
95        "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "header" | "main" |
96        "nav" | "p" | "section" | "span" => true,
97        _ => !DISALLOWED_TAGS.contains(&tag.as_str()),
98    }
99}
100
101impl ShadowTreeRegistry {
102    pub fn new() -> Self {
103        Self {
104            roots_by_host: BTreeMap::new(),
105            hosts_by_root: BTreeMap::new(),
106        }
107    }
108
109    /// ホスト要素に ShadowRoot をアタッチする。
110    pub fn attach_shadow(
111        &mut self,
112        host_idx: usize,
113        tag_name: &str,
114        mode: ShadowRootMode,
115        new_root_idx: usize,
116    ) -> Result<ShadowRootData, AttachShadowError> {
117        if !is_tag_allowed_for_shadow_root(tag_name) {
118            return Err(AttachShadowError::DisallowedTag);
119        }
120        if self.roots_by_host.contains_key(&host_idx) {
121            return Err(AttachShadowError::AlreadyHasShadowRoot);
122        }
123        let data = ShadowRootData::new(new_root_idx, mode, host_idx);
124        self.roots_by_host.insert(host_idx, data.clone());
125        self.hosts_by_root.insert(new_root_idx, host_idx);
126        Ok(data)
127    }
128
129    /// ホスト要素の ShadowRoot を取得。
130    pub fn get_by_host(&self, host_idx: usize) -> Option<&ShadowRootData> {
131        self.roots_by_host.get(&host_idx)
132    }
133
134    /// ルートノード index からホスト要素 index を取得。
135    pub fn get_host_by_root(&self, root_idx: usize) -> Option<usize> {
136        self.hosts_by_root.get(&root_idx).copied()
137    }
138
139    /// 指定ノードが ShadowRoot 仮想ノードかどうか。
140    pub fn is_shadow_root(&self, node_idx: usize) -> bool {
141        self.hosts_by_root.contains_key(&node_idx)
142    }
143
144    /// 登録総数。
145    pub fn len(&self) -> usize {
146        self.roots_by_host.len()
147    }
148
149    pub fn is_empty(&self) -> bool {
150        self.roots_by_host.is_empty()
151    }
152}
153
154/// `<slot>` による Light DOM 子ノードの投影(Projection / Flattening)情報。
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct SlotProjectionItem {
157    pub node_idx: usize,
158    pub is_slotted: bool,
159}
160
161/// Shadow DOM と Light DOM のスロット投影解決(純粋アルゴリズム)。
162///
163/// `shadow_children`: Shadow DOM 内部の子ノード一覧 `(idx, is_slot, slot_name)`
164/// `light_children`: ホスト要素の直下の子ノード一覧 `(idx, slot_attr)`
165///
166/// 戻り値: 最終的にレンダリングされる平坦化された子ノード index 一覧。
167pub fn project_slotted_children(
168    shadow_children: &[(usize, bool, Option<String>)],
169    light_children: &[(usize, Option<String>)],
170) -> Vec<usize> {
171    let mut result = Vec::new();
172
173    for &(s_idx, is_slot, ref s_name) in shadow_children {
174        if is_slot {
175            // スロット要素の場合、マッチする Light DOM の子ノードを投影
176            let target_name = s_name.as_deref().unwrap_or("");
177            let mut slotted_any = false;
178            for &(l_idx, ref l_slot_attr) in light_children {
179                let attr_val = l_slot_attr.as_deref().unwrap_or("");
180                if attr_val == target_name {
181                    result.push(l_idx);
182                    slotted_any = true;
183                }
184            }
185            // スロットに一致するノードが無く、かつスロット自身がフォールバック子ノードを持つ場合は
186            // ここでスロット自身を残すことも可能(最小構成では一致したノードを展開)
187            let _ = slotted_any;
188        } else {
189            // 通常の Shadow DOM ノードはそのまま配置
190            result.push(s_idx);
191        }
192    }
193
194    result
195}