Skip to main content

atmos/os_lib/
custom_elements.rs

1//! Web Components: Custom Elements 仕様準拠の純粋ロジックモジュール。
2//!
3//! WHATWG HTML Living Standard §4.13 "Custom elements" 準拠。
4//! 外部依存を持たず、ホスト環境 (`tests/`) で完全に単体テスト可能。
5
6use alloc::collections::BTreeMap;
7use alloc::string::String;
8use alloc::vec::Vec;
9
10/// 予約済みのカスタム要素名(HTML 仕様で定義された、ハイフンを含むが登録禁止の名前)。
11const RESERVED_NAMES: &[&str] = &[
12    "annotation-xml",
13    "color-profile",
14    "font-face",
15    "font-face-src",
16    "font-face-uri",
17    "font-face-format",
18    "font-face-name",
19    "missing-glyph",
20];
21
22/// カスタム要素名が WHATWG 仕様に適合しているかを検証する。
23///
24/// 規則:
25/// 1. 先頭は ASCII 小文字 `[a-z]` であること。
26/// 2. 少なくとも 1 つのハイフン `-` を含むこと。
27/// 3. ASCII 大文字を含まないこと(小文字のみ)。
28/// 4. 予約名(`annotation-xml` 等)ではないこと。
29/// 5. 使用可能文字: `[a-z0-9._-]` および特定範囲の Unicode 文字。
30pub fn is_valid_custom_element_name(name: &str) -> bool {
31    if name.is_empty() {
32        return false;
33    }
34    let bytes = name.as_bytes();
35    // 1. 先頭は a-z
36    if !bytes[0].is_ascii_lowercase() {
37        return false;
38    }
39    // 2. ハイフンを含むこと
40    if !name.contains('-') {
41        return false;
42    }
43    // 3. ASCII 大文字の禁止
44    if name.bytes().any(|b| b.is_ascii_uppercase()) {
45        return false;
46    }
47    // 4. 予約名の禁止
48    if RESERVED_NAMES.contains(&name) {
49        return false;
50    }
51    // 5. 文字種チェック: [a-z0-9._-]
52    // (簡略化のため、標準的な ASCII 識別子文字を基本とする)
53    for &b in bytes {
54        let valid = b.is_ascii_lowercase()
55            || b.is_ascii_digit()
56            || b == b'-'
57            || b == b'.'
58            || b == b'_';
59        if !valid {
60            return false;
61        }
62    }
63    true
64}
65
66/// 登録されたカスタム要素の定義。
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct CustomElementDefinition {
69    pub name: String,
70    pub observed_attributes: Vec<String>,
71}
72
73impl CustomElementDefinition {
74    pub fn new(name: String, observed_attributes: Vec<String>) -> Self {
75        Self {
76            name,
77            observed_attributes,
78        }
79    }
80
81    /// 変更通知対象の属性かどうかを判定する。
82    pub fn observes_attribute(&self, attr: &str) -> bool {
83        self.observed_attributes.iter().any(|a| a == attr)
84    }
85}
86
87/// カスタム要素レジストリ(純粋データ構造)。
88#[derive(Debug, Clone, Default)]
89pub struct CustomElementRegistry {
90    definitions: BTreeMap<String, CustomElementDefinition>,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum DefineError {
95    InvalidName,
96    AlreadyDefined,
97}
98
99impl CustomElementRegistry {
100    pub fn new() -> Self {
101        Self {
102            definitions: BTreeMap::new(),
103        }
104    }
105
106    /// カスタム要素を登録する。
107    pub fn define(
108        &mut self,
109        name: &str,
110        observed_attributes: Vec<String>,
111    ) -> Result<(), DefineError> {
112        if !is_valid_custom_element_name(name) {
113            return Err(DefineError::InvalidName);
114        }
115        if self.definitions.contains_key(name) {
116            return Err(DefineError::AlreadyDefined);
117        }
118        self.definitions.insert(
119            String::from(name),
120            CustomElementDefinition::new(String::from(name), observed_attributes),
121        );
122        Ok(())
123    }
124
125    /// 登録済み定義を取得する。
126    pub fn get(&self, name: &str) -> Option<&CustomElementDefinition> {
127        self.definitions.get(name)
128    }
129
130    /// 指定されたタグ名がカスタム要素として登録されているか。
131    pub fn is_defined(&self, name: &str) -> bool {
132        self.definitions.contains_key(name)
133    }
134
135    /// CSS `:defined` 疑似クラスの判定。
136    ///
137    /// - 通常の HTML 組み込み要素(ハイフンを含まない、または組み込みタグ): 常に true
138    /// - カスタム要素(ハイフンを含む有効なカスタム名): define 済みなら true、未定義なら false
139    pub fn matches_defined_pseudo_class(&self, tag_name: &str) -> bool {
140        if !tag_name.contains('-') {
141            // 通常の組み込み要素は常に defined
142            return true;
143        }
144        // ハイフンを含む場合は登録済みかどうか
145        self.is_defined(tag_name)
146    }
147
148    /// 登録数を返す。
149    pub fn len(&self) -> usize {
150        self.definitions.len()
151    }
152
153    pub fn is_empty(&self) -> bool {
154        self.definitions.is_empty()
155    }
156}