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}