Skip to main content

domtree/
node.rs

1//! Node types stored in the [`Dom`](crate::Dom) arena.
2//!
3//! Strings (tag names, attribute names/values, text) are **not** stored inline
4//! on each node. They live in a single growable buffer on the [`Dom`](crate::Dom)
5//! and nodes reference them by [`Span`] (a byte range). This replaces thousands
6//! of tiny per-node allocations with appends to one buffer — the main reason
7//! parsing is fast despite producing an owned tree.
8
9use core::fmt;
10use core::num::NonZeroU32;
11use core::ops::Range;
12
13/// A handle to a node inside a [`Dom`](crate::Dom). Cheap (`Copy`) and stable
14/// for the lifetime of the tree.
15//
16// Stored as index + 1 so that `Option<NodeId>` is 4 bytes: every node holds
17// five tree links, so this keeps a node at 52 bytes.
18#[derive(Clone, Copy, PartialEq, Eq, Hash)]
19pub struct NodeId(NonZeroU32);
20
21impl NodeId {
22    /// The id of arena slot `index`. (Saturates past `u32::MAX - 1` nodes,
23    /// which no arena that fits in memory reaches.)
24    #[inline]
25    pub(crate) fn from_index(index: usize) -> Self {
26        let index = u32::try_from(index).unwrap_or(u32::MAX);
27        NodeId(NonZeroU32::MIN.saturating_add(index))
28    }
29
30    /// The node's position in the arena.
31    #[inline]
32    pub fn index(self) -> usize {
33        (self.0.get() - 1) as usize
34    }
35}
36
37impl fmt::Debug for NodeId {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        f.debug_tuple("NodeId").field(&self.index()).finish()
40    }
41}
42
43/// A byte range into the [`Dom`](crate::Dom)'s string arena.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub(crate) struct Span {
46    pub(crate) start: u32,
47    pub(crate) len: u32,
48}
49
50impl Span {
51    pub(crate) const EMPTY: Span = Span { start: 0, len: 0 };
52
53    #[inline]
54    pub(crate) fn range(self) -> Range<usize> {
55        let start = self.start as usize;
56        start..start + self.len as usize
57    }
58}
59
60/// What a node is — a borrowed, `Copy` view returned by
61/// [`NodeRef::kind`](crate::NodeRef::kind).
62///
63/// For elements, the tag name is included here; read attributes via the
64/// [`NodeRef`](crate::NodeRef) accessors (`attr`, `attributes`, `classes`, …).
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum NodeKind<'a> {
67    /// An element, with its lower-cased tag name.
68    Element {
69        /// The lower-cased tag name (e.g. `"div"`).
70        tag: &'a str,
71    },
72    /// A run of text (already entity-decoded, except inside `<script>`/`<style>`).
73    Text(&'a str),
74    /// The body of an HTML comment.
75    Comment(&'a str),
76    /// The body of a `<!doctype …>` declaration.
77    Doctype(&'a str),
78}
79
80/// An element's attributes: a run of `len` consecutive `(name, value)` pairs
81/// starting at `start` in the [`Dom`](crate::Dom)'s attribute arena.
82///
83/// Storing every element's attributes in one shared `Vec` (instead of a `Vec`
84/// per element) saves a heap allocation per element with attributes and
85/// shrinks a node from 64 to 52 bytes. The parser lays the runs out in
86/// document order; editing may move an element's run to the end of the arena
87/// (see `Dom::set_attr`).
88#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
89pub(crate) struct AttrRange {
90    pub(crate) start: u32,
91    pub(crate) len: u32,
92}
93
94impl AttrRange {
95    #[inline]
96    pub(crate) fn range(self) -> Range<usize> {
97        let start = self.start as usize;
98        start..start + self.len as usize
99    }
100}
101
102/// The internal, span-based node kind (not exposed; see [`NodeKind`]).
103#[derive(Clone, Copy)]
104pub(crate) enum RawKind {
105    Element { name: Span, attrs: AttrRange },
106    Text(Span),
107    Comment(Span),
108    Doctype(Span),
109}
110
111/// Where a node came from: a byte range into the *original source* (not the
112/// string arena), or [`SrcRange::NONE`] for nodes created by editing.
113///
114/// Two `u32`s keep the per-node overhead at 8 bytes. The builder records
115/// ranges only when the whole input fits in them (under 4 GiB); for a larger
116/// input every node is `NONE`, never a wrapped offset.
117#[derive(Clone, Copy, PartialEq, Eq, Debug)]
118pub(crate) struct SrcRange {
119    start: u32,
120    end: u32,
121}
122
123impl SrcRange {
124    /// No range. `start == u32::MAX` marks it: real offsets are below that.
125    pub(crate) const NONE: SrcRange = SrcRange {
126        start: u32::MAX,
127        end: u32::MAX,
128    };
129
130    /// A range the caller knows is valid (`start <= end < u32::MAX`).
131    #[inline]
132    pub(crate) fn new(start: u32, end: u32) -> Self {
133        SrcRange { start, end }
134    }
135
136    /// Move the end (an element's range grows when it is closed). A no-op on
137    /// [`SrcRange::NONE`].
138    #[inline]
139    pub(crate) fn set_end(&mut self, end: u32) {
140        if self.start != u32::MAX {
141            self.end = end;
142        }
143    }
144
145    pub(crate) fn get(self) -> Option<Range<usize>> {
146        (self.start != u32::MAX).then_some(self.start as usize..self.end.max(self.start) as usize)
147    }
148}
149
150/// The internal arena record: tree links plus the node's kind.
151#[derive(Clone)]
152pub(crate) struct Node {
153    pub(crate) parent: Option<NodeId>,
154    pub(crate) first_child: Option<NodeId>,
155    pub(crate) last_child: Option<NodeId>,
156    pub(crate) next_sibling: Option<NodeId>,
157    pub(crate) prev_sibling: Option<NodeId>,
158    /// Exactly when the node is **not** reachable from the top level: it was
159    /// removed with [`Dom::remove`](crate::Dom::remove) (or is inside a
160    /// removed subtree), or was created by editing and not yet attached. A
161    /// subtree is always uniformly flagged. It stays in the arena either way.
162    pub(crate) detached: bool,
163    pub(crate) src: SrcRange,
164    pub(crate) raw: RawKind,
165}
166
167impl Node {
168    pub(crate) fn new(raw: RawKind, src: SrcRange) -> Self {
169        Node {
170            parent: None,
171            first_child: None,
172            last_child: None,
173            next_sibling: None,
174            prev_sibling: None,
175            detached: false,
176            src,
177            raw,
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn nodes_stay_small() {
188        // Parse speed is mostly memory traffic: keep nodes small. Five links
189        // (20 bytes), the source range (8), the kind with its two spans (20)
190        // and the detached flag.
191        assert_eq!(core::mem::size_of::<Option<NodeId>>(), 4);
192        assert_eq!(core::mem::size_of::<RawKind>(), 20);
193        assert!(core::mem::size_of::<Node>() <= 52);
194    }
195
196    #[test]
197    fn node_id_round_trips_its_index() {
198        for i in [0, 1, 41, u32::MAX as usize - 1] {
199            assert_eq!(NodeId::from_index(i).index(), i);
200        }
201        assert_eq!(alloc::format!("{:?}", NodeId::from_index(7)), "NodeId(7)");
202    }
203}