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