Skip to main content

fhp_tree/
arena.rs

1//! Arena allocator for DOM nodes, text, and attributes.
2//!
3//! All nodes live in a single contiguous `Vec<Node>`, giving cache-friendly
4//! traversal. Text content and attributes are stored in separate slabs,
5//! referenced by offset+length from each [`Node`](crate::node::Node).
6
7use fhp_core::hash::{class_bloom_bit, selector_hash};
8use fhp_core::tag::Tag;
9
10use crate::node::{Node, NodeFlags, NodeId};
11
12/// A compact attribute stored in the attribute slab.
13///
14/// Names and values are stored as offsets into `Arena::attr_str_slab` rather
15/// than separate heap allocations. Use [`Arena::attr_name`] and
16/// [`Arena::attr_value`] to access the strings.
17#[derive(Clone, Debug)]
18pub struct Attribute {
19    name_offset: u32,
20    name_len: u16,
21    value_offset: u32,
22    value_len: u16,
23}
24
25/// Number of tag index buckets (Tag is `repr(u8)`, 256 possible values).
26const TAG_INDEX_SIZE: usize = 256;
27
28/// Arena-based storage for all DOM nodes, text content, and attributes.
29///
30/// Nodes are stored in a contiguous `Vec<Node>` for cache-line-friendly access.
31/// Text and attributes are stored in separate slabs and referenced by
32/// offset+length from each node.
33pub struct Arena {
34    /// All nodes in insertion order.
35    pub(crate) nodes: Vec<Node>,
36    /// All text content concatenated (for entity-decoded or owned text).
37    pub(crate) text_slab: Vec<u8>,
38    /// All attributes in insertion order.
39    pub(crate) attr_slab: Vec<Attribute>,
40    /// All attribute name and value bytes concatenated.
41    pub(crate) attr_str_slab: Vec<u8>,
42    /// Owned copy of the original input source.
43    ///
44    /// Text nodes that reference entity-free (borrowed) regions of the input
45    /// store offsets into this buffer via [`NodeFlags::IS_TEXT_FROM_SOURCE`].
46    /// Empty for streaming parsers.
47    pub(crate) source: Vec<u8>,
48    /// Pre-built tag → NodeId index, populated during tree construction.
49    ///
50    /// Indexed by `Tag as u8`. Each bucket contains NodeIds of elements with
51    /// that tag, in document order. Built inline during `open_tag` to avoid
52    /// a separate DFS pass.
53    pub(crate) tag_index: Option<Box<[Vec<NodeId>; TAG_INDEX_SIZE]>>,
54}
55
56impl Arena {
57    /// Create a new empty arena.
58    pub fn new() -> Self {
59        Self {
60            nodes: Vec::new(),
61            text_slab: Vec::new(),
62            attr_slab: Vec::new(),
63            attr_str_slab: Vec::new(),
64            source: Vec::new(),
65            tag_index: None,
66        }
67    }
68
69    /// Create a new arena with pre-allocated capacity.
70    pub fn with_capacity(node_cap: usize, text_cap: usize, attr_cap: usize) -> Self {
71        Self {
72            nodes: Vec::with_capacity(node_cap),
73            text_slab: Vec::with_capacity(text_cap),
74            attr_slab: Vec::with_capacity(attr_cap),
75            attr_str_slab: Vec::with_capacity(attr_cap * 32),
76            source: Vec::new(),
77            tag_index: None,
78        }
79    }
80
81    /// Enable the inline tag index.
82    ///
83    /// Once enabled, every [`Arena::new_element`] call appends the node id to
84    /// the corresponding tag bucket. Consumers can retrieve the index via
85    /// [`Arena::tag_index`].
86    pub fn enable_tag_index(&mut self) {
87        if self.tag_index.is_none() {
88            // Use a boxed array to avoid 256 * 24 = 6 KB on the stack.
89            self.tag_index = Some(Box::new(std::array::from_fn(|_| Vec::new())));
90        }
91    }
92
93    /// Get the pre-built tag index, if it was enabled during construction.
94    pub fn tag_index(&self) -> Option<&[Vec<NodeId>; TAG_INDEX_SIZE]> {
95        self.tag_index.as_deref()
96    }
97
98    /// Allocate a new element node and return its id.
99    pub fn new_element(&mut self, tag: Tag, depth: u16) -> NodeId {
100        let id = NodeId(self.nodes.len() as u32);
101        self.nodes.push(Node::new_element(tag, depth));
102        // Populate inline tag index if enabled.
103        if let Some(ref mut idx) = self.tag_index {
104            idx[tag as u8 as usize].push(id);
105        }
106        id
107    }
108
109    /// Store the original tag name for an unknown/custom element.
110    pub fn set_unknown_tag_name(&mut self, node: NodeId, tag_name: &str) {
111        if tag_name.is_empty() || self.nodes[node.index()].tag != Tag::Unknown {
112            return;
113        }
114        let offset = self.text_slab.len() as u32;
115        let len = tag_name.len() as u32;
116        self.text_slab.extend_from_slice(tag_name.as_bytes());
117        let n = &mut self.nodes[node.index()];
118        n.text_offset = offset;
119        n.text_len = len;
120    }
121
122    /// Allocate a new text node, storing content in the text slab.
123    pub fn new_text(&mut self, depth: u16, text: &str) -> NodeId {
124        let offset = self.text_slab.len() as u32;
125        let len = text.len() as u32;
126        self.text_slab.extend_from_slice(text.as_bytes());
127        let id = NodeId(self.nodes.len() as u32);
128        self.nodes.push(Node::new_text(depth, offset, len));
129        id
130    }
131
132    /// Allocate a text node that references a region of the original source.
133    ///
134    /// Instead of copying content to the text slab, this stores a
135    /// `(source_offset, len)` pair and sets [`NodeFlags::IS_TEXT_FROM_SOURCE`].
136    /// The source must have been set via [`Arena::set_source`] before calling
137    /// this method.
138    pub fn new_text_ref(&mut self, depth: u16, source_offset: u32, len: u32) -> NodeId {
139        let id = NodeId(self.nodes.len() as u32);
140        let mut node = Node::new_text(depth, source_offset, len);
141        node.flags.set(NodeFlags::IS_TEXT_FROM_SOURCE);
142        self.nodes.push(node);
143        id
144    }
145
146    /// Store an owned copy of the input source for source-backed text nodes.
147    pub fn set_source(&mut self, input: &str) {
148        self.source = input.as_bytes().to_vec();
149    }
150
151    /// Transfer an already-owned `String` as the source buffer (zero copy).
152    ///
153    /// When the caller owns the input `String` (e.g., from an HTTP response),
154    /// this avoids the memcpy that [`set_source`](Arena::set_source) performs.
155    pub fn set_source_owned(&mut self, source: String) {
156        self.source = source.into_bytes();
157    }
158
159    /// Allocate a new comment node, storing content in the text slab.
160    pub fn new_comment(&mut self, depth: u16, text: &str) -> NodeId {
161        let offset = self.text_slab.len() as u32;
162        let len = text.len() as u32;
163        self.text_slab.extend_from_slice(text.as_bytes());
164        let id = NodeId(self.nodes.len() as u32);
165        self.nodes.push(Node::new_comment(depth, offset, len));
166        id
167    }
168
169    /// Allocate a new doctype node, storing content in the text slab.
170    pub fn new_doctype(&mut self, depth: u16, text: &str) -> NodeId {
171        let offset = self.text_slab.len() as u32;
172        let len = text.len() as u32;
173        self.text_slab.extend_from_slice(text.as_bytes());
174        let id = NodeId(self.nodes.len() as u32);
175        self.nodes.push(Node::new_doctype(depth, offset, len));
176        id
177    }
178
179    /// Set attributes for a node from tokenizer attributes.
180    pub fn set_attrs(&mut self, node: NodeId, attrs: &[fhp_tokenizer::token::Attribute<'_>]) {
181        if attrs.is_empty() {
182            return;
183        }
184        let offset = self.attr_slab.len() as u32;
185        let count = attrs.len().min(u16::MAX as usize) as u16;
186
187        for attr in &attrs[..count as usize] {
188            let name_offset = self.attr_str_slab.len() as u32;
189            self.attr_str_slab.extend_from_slice(attr.name.as_bytes());
190            let name_len = attr.name.len() as u16;
191
192            let (value_offset, value_len) = if let Some(ref v) = attr.value {
193                let vo = self.attr_str_slab.len() as u32;
194                self.attr_str_slab.extend_from_slice(v.as_bytes());
195                (vo, v.len() as u16)
196            } else {
197                (0, 0)
198            };
199
200            self.attr_slab.push(Attribute {
201                name_offset,
202                name_len,
203                value_offset,
204                value_len,
205            });
206        }
207
208        let n = &mut self.nodes[node.index()];
209        n.attr_offset = offset;
210        n.attr_count = count;
211        n.flags.set(NodeFlags::HAS_ATTRS);
212
213        // Compute class_hash and id_hash from the just-added attributes.
214        self.compute_node_hashes(node, offset, count);
215    }
216
217    /// Parse attributes directly from a raw attribute region into the slab.
218    ///
219    /// Skips all intermediate `Vec<Attribute>` allocation — names and values
220    /// are written directly to `attr_str_slab` and compact `Attribute` structs
221    /// are pushed to `attr_slab` in a single pass.
222    pub fn set_attrs_from_raw(&mut self, node: NodeId, attr_raw: &str) {
223        let bytes = attr_raw.as_bytes();
224        let end = bytes.len();
225        if end == 0 {
226            return;
227        }
228
229        let slab_offset = self.attr_slab.len() as u32;
230        let mut count: u16 = 0;
231        let mut pos = 0;
232
233        loop {
234            // Skip whitespace using fast byte scan.
235            pos += bytes[pos..end]
236                .iter()
237                .position(|&b| !is_attr_whitespace(b))
238                .unwrap_or(end - pos);
239            if pos >= end || count == u16::MAX {
240                break;
241            }
242
243            // Attribute name.
244            let name_start = pos;
245            while pos < end && !is_attr_name_end(bytes[pos]) {
246                pos += 1;
247            }
248            if name_start == pos {
249                // Not a valid name char — skip it.
250                pos += 1;
251                continue;
252            }
253
254            let name_slab_offset = self.attr_str_slab.len() as u32;
255            self.attr_str_slab
256                .extend_from_slice(&bytes[name_start..pos]);
257            let name_len = (pos - name_start) as u16;
258
259            // Skip whitespace using fast byte scan.
260            pos += bytes[pos..end]
261                .iter()
262                .position(|&b| !is_attr_whitespace(b))
263                .unwrap_or(end - pos);
264
265            // Check for `=`.
266            if pos < end && bytes[pos] == b'=' {
267                pos += 1;
268
269                // Skip whitespace using fast byte scan.
270                pos += bytes[pos..end]
271                    .iter()
272                    .position(|&b| !is_attr_whitespace(b))
273                    .unwrap_or(end - pos);
274
275                // Parse value.
276                if pos < end && (bytes[pos] == b'"' || bytes[pos] == b'\'') {
277                    // Quoted value — use memchr for SIMD-accelerated scan.
278                    let quote = bytes[pos];
279                    pos += 1;
280                    let val_start = pos;
281                    if let Some(found) = memchr::memchr(quote, &bytes[pos..end]) {
282                        pos += found;
283                    } else {
284                        pos = end;
285                    }
286                    let val_end = pos;
287                    if pos < end {
288                        pos += 1; // skip closing quote
289                    }
290                    let raw_value = &attr_raw[val_start..val_end];
291                    let (value_offset, value_len) = self.push_attr_value(raw_value);
292                    self.attr_slab.push(Attribute {
293                        name_offset: name_slab_offset,
294                        name_len,
295                        value_offset,
296                        value_len,
297                    });
298                } else {
299                    // Unquoted value.
300                    let val_start = pos;
301                    while pos < end && !is_attr_whitespace(bytes[pos]) && bytes[pos] != b'>' {
302                        pos += 1;
303                    }
304                    let raw_value = &attr_raw[val_start..pos];
305                    let (value_offset, value_len) = self.push_attr_value(raw_value);
306                    self.attr_slab.push(Attribute {
307                        name_offset: name_slab_offset,
308                        name_len,
309                        value_offset,
310                        value_len,
311                    });
312                }
313            } else {
314                // Boolean attribute (no value).
315                self.attr_slab.push(Attribute {
316                    name_offset: name_slab_offset,
317                    name_len,
318                    value_offset: 0,
319                    value_len: 0,
320                });
321            }
322
323            count += 1;
324        }
325
326        if count > 0 {
327            let n = &mut self.nodes[node.index()];
328            n.attr_offset = slab_offset;
329            n.attr_count = count;
330            n.flags.set(NodeFlags::HAS_ATTRS);
331
332            // Compute class_hash (bloom) and id_hash (exact) from parsed attrs.
333            self.compute_node_hashes(node, slab_offset, count);
334        }
335    }
336
337    /// Compute class bloom hash and id hash from a node's just-parsed attributes.
338    ///
339    /// Scans the attribute slab for `class` and `id` attributes and stores
340    /// the computed hashes on the node for fast selector rejection.
341    fn compute_node_hashes(&mut self, node: NodeId, slab_offset: u32, count: u16) {
342        let mut class_hash: u64 = 0;
343        let mut id_hash: u32 = 0;
344        let start = slab_offset as usize;
345        let end = start + count as usize;
346
347        for i in start..end {
348            let attr = &self.attr_slab[i];
349            let name_start = attr.name_offset as usize;
350            let name_end = name_start + attr.name_len as usize;
351            let name_bytes = &self.attr_str_slab[name_start..name_end];
352
353            if name_bytes.eq_ignore_ascii_case(b"class") && attr.value_len > 0 {
354                let val_start = attr.value_offset as usize;
355                let val_end = val_start + attr.value_len as usize;
356                let val = &self.attr_str_slab[val_start..val_end];
357                // Build bloom: OR in a bit for each whitespace-separated token.
358                let mut pos = 0;
359                while pos < val.len() {
360                    // Skip whitespace.
361                    while pos < val.len() && val[pos].is_ascii_whitespace() {
362                        pos += 1;
363                    }
364                    let token_start = pos;
365                    while pos < val.len() && !val[pos].is_ascii_whitespace() {
366                        pos += 1;
367                    }
368                    if pos > token_start {
369                        class_hash |= class_bloom_bit(&val[token_start..pos]);
370                    }
371                }
372            } else if name_bytes.eq_ignore_ascii_case(b"id") && attr.value_len > 0 {
373                let val_start = attr.value_offset as usize;
374                let val_end = val_start + attr.value_len as usize;
375                id_hash = selector_hash(&self.attr_str_slab[val_start..val_end]);
376            }
377        }
378
379        let n = &mut self.nodes[node.index()];
380        n.class_hash = class_hash;
381        n.id_hash = id_hash;
382    }
383
384    /// Write an attribute value to the string slab, with optional entity decoding.
385    #[cfg(feature = "entity-decode")]
386    fn push_attr_value(&mut self, raw_value: &str) -> (u32, u16) {
387        let offset = self.attr_str_slab.len() as u32;
388        let decoded = fhp_tokenizer::entity::decode_entities(raw_value);
389        self.attr_str_slab.extend_from_slice(decoded.as_bytes());
390        (offset, decoded.len() as u16)
391    }
392
393    /// Write an attribute value to the string slab (no entity decoding).
394    #[cfg(not(feature = "entity-decode"))]
395    fn push_attr_value(&mut self, raw_value: &str) -> (u32, u16) {
396        let offset = self.attr_str_slab.len() as u32;
397        self.attr_str_slab.extend_from_slice(raw_value.as_bytes());
398        (offset, raw_value.len() as u16)
399    }
400
401    /// Set the 1-based element sibling index for a node.
402    ///
403    /// Called by [`TreeBuilder`](crate::builder::TreeBuilder) after appending an element child.
404    #[inline]
405    pub fn set_element_index(&mut self, node: NodeId, index: u16) {
406        self.nodes[node.index()].element_index = index;
407    }
408
409    /// Set the self-closing flag on a node.
410    pub fn set_self_closing(&mut self, node: NodeId) {
411        self.nodes[node.index()]
412            .flags
413            .set(NodeFlags::IS_SELF_CLOSING);
414    }
415
416    /// Append `child` as the last child of `parent`.
417    ///
418    /// Updates all tree links: parent, first_child, last_child, prev_sibling,
419    /// next_sibling. Uses unchecked indexing since NodeIds are always valid
420    /// indices created by this arena.
421    ///
422    /// `child` must be a freshly allocated, not-yet-linked node, distinct from
423    /// `parent` (and therefore from `parent`'s current last child). Internal
424    /// callers always satisfy this; the debug assertions below catch misuse.
425    pub fn append_child(&mut self, parent: NodeId, child: NodeId) {
426        debug_assert!(
427            parent != child,
428            "append_child: parent and child must be distinct nodes (aliasing &mut)"
429        );
430        let nodes = self.nodes.as_mut_ptr();
431        // SAFETY: parent and child indices were created by this arena via
432        // new_element/new_text/etc., so they are always in bounds. The three
433        // `&mut` references taken below (parent, child, and parent.last_child)
434        // never alias: `parent != child` (asserted), and `last` is a previously
435        // appended child, so it differs from both the pre-existing `parent` and
436        // the freshly allocated `child`.
437        unsafe {
438            let p = &mut *nodes.add(parent.index());
439            let c = &mut *nodes.add(child.index());
440            let last = p.last_child;
441            c.parent = parent;
442            if last.is_null() {
443                p.first_child = child;
444            } else {
445                debug_assert!(
446                    last != parent && last != child,
447                    "append_child: parent.last_child must differ from parent and child"
448                );
449                (*nodes.add(last.index())).next_sibling = child;
450                c.prev_sibling = last;
451            }
452            p.last_child = child;
453            p.flags.set(NodeFlags::HAS_CHILDREN);
454        }
455    }
456
457    /// Get the name of an attribute.
458    #[inline]
459    pub fn attr_name(&self, attr: &Attribute) -> &str {
460        let start = attr.name_offset as usize;
461        let end = start + attr.name_len as usize;
462        // SAFETY: attr names are sourced from tokenizer `&str` slices (valid UTF-8).
463        unsafe { std::str::from_utf8_unchecked(&self.attr_str_slab[start..end]) }
464    }
465
466    /// Get the value of an attribute, or `None` for boolean attributes.
467    #[inline]
468    pub fn attr_value(&self, attr: &Attribute) -> Option<&str> {
469        if attr.value_len == 0 {
470            return None;
471        }
472        let start = attr.value_offset as usize;
473        let end = start + attr.value_len as usize;
474        // SAFETY: attr values are sourced from tokenizer `&str` slices (valid UTF-8).
475        Some(unsafe { std::str::from_utf8_unchecked(&self.attr_str_slab[start..end]) })
476    }
477
478    /// Get the attributes for a node (read-only, requires prior parse).
479    ///
480    /// If the node has lazy (unparsed) attributes, returns an empty slice.
481    /// Call [`Arena::ensure_attrs_parsed`] first to guarantee parsing.
482    #[inline]
483    pub fn attrs(&self, node: NodeId) -> &[Attribute] {
484        let n = &self.nodes[node.index()];
485        if n.attr_count == 0 {
486            return &[];
487        }
488        let start = n.attr_offset as usize;
489        let end = start + n.attr_count as usize;
490        &self.attr_slab[start..end]
491    }
492
493    /// Get the text content for a node (direct text, not recursive).
494    #[inline]
495    pub fn text(&self, node: NodeId) -> &str {
496        let n = &self.nodes[node.index()];
497        if n.text_len == 0 {
498            return "";
499        }
500        let start = n.text_offset as usize;
501        let end = start + n.text_len as usize;
502        if n.flags.has(NodeFlags::IS_TEXT) && n.flags.has(NodeFlags::IS_TEXT_FROM_SOURCE) {
503            // SAFETY: source is a copy of the input &str (valid UTF-8).
504            unsafe { std::str::from_utf8_unchecked(&self.source[start..end]) }
505        } else {
506            // SAFETY: text slab is always valid UTF-8 (we only write str bytes).
507            unsafe { std::str::from_utf8_unchecked(&self.text_slab[start..end]) }
508        }
509    }
510
511    /// Get the preserved name for an unknown/custom element.
512    #[inline]
513    pub fn unknown_tag_name(&self, node: NodeId) -> Option<&str> {
514        let n = &self.nodes[node.index()];
515        if n.tag != Tag::Unknown || n.text_len == 0 {
516            return None;
517        }
518        let start = n.text_offset as usize;
519        let end = start + n.text_len as usize;
520        // SAFETY: the tag name is sourced from tokenizer `&str` slices.
521        Some(unsafe { std::str::from_utf8_unchecked(&self.text_slab[start..end]) })
522    }
523
524    /// Get a reference to a node by id.
525    #[inline]
526    pub fn get(&self, id: NodeId) -> &Node {
527        &self.nodes[id.index()]
528    }
529
530    /// Get a mutable reference to a node by id.
531    #[inline]
532    pub fn get_mut(&mut self, id: NodeId) -> &mut Node {
533        &mut self.nodes[id.index()]
534    }
535
536    /// Total number of nodes in the arena.
537    #[inline]
538    pub fn len(&self) -> usize {
539        self.nodes.len()
540    }
541
542    /// Returns `true` if the arena contains no nodes.
543    #[inline]
544    pub fn is_empty(&self) -> bool {
545        self.nodes.is_empty()
546    }
547}
548
549impl Default for Arena {
550    fn default() -> Self {
551        Self::new()
552    }
553}
554
555/// Check if a byte is ASCII whitespace (for attribute parsing).
556#[inline(always)]
557fn is_attr_whitespace(b: u8) -> bool {
558    matches!(b, b' ' | b'\t' | b'\n' | b'\r')
559}
560
561/// Check if a byte terminates an attribute name.
562#[inline(always)]
563fn is_attr_name_end(b: u8) -> bool {
564    matches!(b, b' ' | b'\t' | b'\n' | b'\r' | b'=' | b'/' | b'>')
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570    use std::borrow::Cow;
571
572    #[test]
573    fn new_element_and_get() {
574        let mut arena = Arena::new();
575        let id = arena.new_element(Tag::Div, 0);
576        assert_eq!(id, NodeId(0));
577        assert_eq!(arena.get(id).tag, Tag::Div);
578        assert_eq!(arena.get(id).depth, 0);
579    }
580
581    #[test]
582    fn new_text_and_read() {
583        let mut arena = Arena::new();
584        let id = arena.new_text(1, "hello world");
585        assert!(arena.get(id).flags.has(NodeFlags::IS_TEXT));
586        assert_eq!(arena.text(id), "hello world");
587    }
588
589    #[test]
590    fn append_child_single() {
591        let mut arena = Arena::new();
592        let parent = arena.new_element(Tag::Div, 0);
593        let child = arena.new_element(Tag::Span, 1);
594        arena.append_child(parent, child);
595
596        assert_eq!(arena.get(parent).first_child, child);
597        assert_eq!(arena.get(parent).last_child, child);
598        assert_eq!(arena.get(child).parent, parent);
599        assert!(arena.get(child).next_sibling.is_null());
600        assert!(arena.get(child).prev_sibling.is_null());
601    }
602
603    #[test]
604    fn append_child_multiple() {
605        let mut arena = Arena::new();
606        let parent = arena.new_element(Tag::Div, 0);
607        let c1 = arena.new_element(Tag::Span, 1);
608        let c2 = arena.new_element(Tag::P, 1);
609        let c3 = arena.new_element(Tag::A, 1);
610
611        arena.append_child(parent, c1);
612        arena.append_child(parent, c2);
613        arena.append_child(parent, c3);
614
615        assert_eq!(arena.get(parent).first_child, c1);
616        assert_eq!(arena.get(parent).last_child, c3);
617
618        assert_eq!(arena.get(c1).next_sibling, c2);
619        assert!(arena.get(c1).prev_sibling.is_null());
620
621        assert_eq!(arena.get(c2).prev_sibling, c1);
622        assert_eq!(arena.get(c2).next_sibling, c3);
623
624        assert_eq!(arena.get(c3).prev_sibling, c2);
625        assert!(arena.get(c3).next_sibling.is_null());
626    }
627
628    #[test]
629    fn attrs_roundtrip() {
630        use fhp_tokenizer::token::Attribute as TokAttr;
631
632        let mut arena = Arena::new();
633        let id = arena.new_element(Tag::A, 0);
634
635        let tok_attrs = vec![
636            TokAttr {
637                name: Cow::Borrowed("href"),
638                value: Some(Cow::Borrowed("https://example.com")),
639            },
640            TokAttr {
641                name: Cow::Borrowed("class"),
642                value: Some(Cow::Borrowed("link")),
643            },
644        ];
645        arena.set_attrs(id, &tok_attrs);
646
647        let attrs = arena.attrs(id);
648        assert_eq!(attrs.len(), 2);
649        assert_eq!(arena.attr_name(&attrs[0]), "href");
650        assert_eq!(arena.attr_value(&attrs[0]), Some("https://example.com"));
651        assert_eq!(arena.attr_name(&attrs[1]), "class");
652        assert_eq!(arena.attr_value(&attrs[1]), Some("link"));
653    }
654
655    #[test]
656    fn empty_attrs() {
657        let mut arena = Arena::new();
658        let id = arena.new_element(Tag::Div, 0);
659        assert!(arena.attrs(id).is_empty());
660    }
661
662    #[test]
663    fn arena_len() {
664        let mut arena = Arena::new();
665        assert!(arena.is_empty());
666        arena.new_element(Tag::Div, 0);
667        arena.new_text(1, "hi");
668        assert_eq!(arena.len(), 2);
669    }
670}