Skip to main content

kozan_core/dom/
attribute.rs

1// Attribute collection — dedicated type for element attributes.
2//
3// Like Chrome's `Vector<Attribute>` with O(n) linear scan.
4// Most elements have 0–5 attributes, so linear scan beats HashMap.
5// No hardcoded id/class — all attributes are equal.
6
7/// A single attribute (name-value pair).
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct Attribute {
10    name: String,
11    value: String,
12}
13
14impl Attribute {
15    /// Create a new attribute.
16    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
17        Self {
18            name: name.into(),
19            value: value.into(),
20        }
21    }
22
23    /// The attribute name.
24    #[inline]
25    #[must_use]
26    pub fn name(&self) -> &str {
27        &self.name
28    }
29
30    /// The attribute value.
31    #[inline]
32    #[must_use]
33    pub fn value(&self) -> &str {
34        &self.value
35    }
36
37    /// Set the value, returns the old value.
38    pub fn set_value(&mut self, value: impl Into<String>) -> String {
39        core::mem::replace(&mut self.value, value.into())
40    }
41}
42
43/// A collection of attributes.
44///
45/// Linear scan on a flat `Vec<Attribute>`. O(n) lookup is optimal
46/// for the typical 0–5 attributes per element.
47#[derive(Clone, Debug, Default)]
48pub struct AttributeCollection {
49    attrs: Vec<Attribute>,
50}
51
52impl AttributeCollection {
53    /// Create an empty collection.
54    #[must_use]
55    pub fn new() -> Self {
56        Self { attrs: Vec::new() }
57    }
58
59    /// Get an attribute value by name.
60    #[must_use]
61    pub fn get(&self, name: &str) -> Option<&str> {
62        self.attrs
63            .iter()
64            .find(|a| a.name == name)
65            .map(|a| a.value.as_str())
66    }
67
68    /// Set an attribute. If it exists, update its value. Otherwise, add it.
69    pub fn set(&mut self, name: &str, value: impl Into<String>) {
70        if let Some(attr) = self.attrs.iter_mut().find(|a| a.name == name) {
71            attr.value = value.into();
72        } else {
73            self.attrs.push(Attribute::new(name, value));
74        }
75    }
76
77    /// Remove an attribute by name. Returns the old value if it existed.
78    pub fn remove(&mut self, name: &str) -> Option<String> {
79        if let Some(pos) = self.attrs.iter().position(|a| a.name == name) {
80            Some(self.attrs.swap_remove(pos).value)
81        } else {
82            None
83        }
84    }
85
86    /// Check if an attribute exists.
87    #[must_use]
88    pub fn has(&self, name: &str) -> bool {
89        self.attrs.iter().any(|a| a.name == name)
90    }
91
92    /// Number of attributes.
93    #[inline]
94    #[must_use]
95    pub fn len(&self) -> usize {
96        self.attrs.len()
97    }
98
99    /// Is the collection empty?
100    #[inline]
101    #[must_use]
102    pub fn is_empty(&self) -> bool {
103        self.attrs.is_empty()
104    }
105
106    /// Iterate over all attributes.
107    pub fn iter(&self) -> impl Iterator<Item = &Attribute> {
108        self.attrs.iter()
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn empty_collection() {
118        let attrs = AttributeCollection::new();
119        assert!(attrs.is_empty());
120        assert_eq!(attrs.len(), 0);
121        assert!(attrs.get("id").is_none());
122    }
123
124    #[test]
125    fn set_and_get() {
126        let mut attrs = AttributeCollection::new();
127        attrs.set("id", "main");
128        assert_eq!(attrs.get("id"), Some("main"));
129        assert_eq!(attrs.len(), 1);
130    }
131
132    #[test]
133    fn set_overwrites() {
134        let mut attrs = AttributeCollection::new();
135        attrs.set("id", "old");
136        attrs.set("id", "new");
137        assert_eq!(attrs.get("id"), Some("new"));
138        assert_eq!(attrs.len(), 1);
139    }
140
141    #[test]
142    fn remove_returns_old_value() {
143        let mut attrs = AttributeCollection::new();
144        attrs.set("class", "container");
145        let old = attrs.remove("class");
146        assert_eq!(old, Some("container".to_string()));
147        assert!(attrs.is_empty());
148    }
149
150    #[test]
151    fn remove_nonexistent_returns_none() {
152        let mut attrs = AttributeCollection::new();
153        assert_eq!(attrs.remove("missing"), None);
154    }
155
156    #[test]
157    fn has_check() {
158        let mut attrs = AttributeCollection::new();
159        attrs.set("data-x", "1");
160        assert!(attrs.has("data-x"));
161        assert!(!attrs.has("data-y"));
162    }
163
164    #[test]
165    fn multiple_attributes() {
166        let mut attrs = AttributeCollection::new();
167        attrs.set("id", "test");
168        attrs.set("class", "foo bar");
169        attrs.set("data-value", "42");
170        assert_eq!(attrs.len(), 3);
171        assert_eq!(attrs.get("id"), Some("test"));
172        assert_eq!(attrs.get("class"), Some("foo bar"));
173        assert_eq!(attrs.get("data-value"), Some("42"));
174    }
175
176    #[test]
177    fn iterate_attributes() {
178        let mut attrs = AttributeCollection::new();
179        attrs.set("a", "1");
180        attrs.set("b", "2");
181        let names: Vec<&str> = attrs.iter().map(|a| a.name()).collect();
182        assert_eq!(names.len(), 2);
183        assert!(names.contains(&"a"));
184        assert!(names.contains(&"b"));
185    }
186}