augdom/
lib.rs

1//! `augdom` provides an "augmented DOM" implementation that can run almost anywhere Rust can. By
2//! default the `webdom` feature is enabled and this crate is a wrapper around `web-sys` for
3//! creating and manipulating HTML elements. See the [crate::Dom] trait for the provided behavior.
4//!
5//! The `rsdom` feature enables a DOM emulation layer written in pure Rust which can be
6//! used for testing or to render HTML strings.
7//!
8//! # Known Limitations
9//!
10//! As of today the `<web_sys::Element as Dom>::*_attribute` methods will panic if called on a text
11//! node. This cost seems appropriate today because this is a dependency for other crates which
12//! enforce this requirement themselves. `web_sys` enforces this restriction statically.
13#![deny(clippy::all, missing_docs)]
14
15static_assertions::assert_cfg!(
16    any(feature = "webdom", feature = "rsdom"),
17    "At least one DOM implementation's feature must be enabled (`webdom`, `rsdom`)"
18);
19
20#[cfg(feature = "webdom")]
21pub use {wasm_bindgen::JsCast, web_sys as sys};
22
23#[cfg(feature = "rsdom")]
24use {rsdom::VirtNode, std::rc::Rc};
25
26use {
27    quick_xml::Writer as XmlWriter,
28    std::{
29        fmt::{Debug, Display, Formatter, Result as FmtResult},
30        io::{prelude::*, Cursor},
31    },
32};
33
34#[cfg(feature = "rsdom")]
35pub mod rsdom;
36#[cfg(feature = "webdom")]
37pub mod webdom;
38
39pub mod event;
40
41/// Returns the current window. Panics if no window is available.
42#[cfg(feature = "webdom")]
43pub fn window() -> sys::Window {
44    sys::window().expect("must run from within a `window`")
45}
46
47/// Returns the current document. Panics if called outside a web document context.
48#[cfg(feature = "webdom")]
49pub fn document() -> sys::Document {
50    window()
51        .document()
52        .expect("must run from within a `window` with a valid `document`")
53}
54
55/// A value which implements a subset of the web's document object model.
56pub trait Dom: Sized {
57    // TODO is there a way to pass the starting indentation down from a formatter?
58    /// Write this value as XML via the provided writer. Consider using [Xml::inner_html] or
59    /// [Xml::pretty_inner_html] unless you need the performance.
60    fn write_xml<W: Write>(&self, writer: &mut XmlWriter<W>);
61
62    /// Returns a string of serialized XML without newlines or indentation.
63    fn outer_html(&self) -> String {
64        let mut buf: Cursor<Vec<u8>> = Cursor::new(Vec::new());
65        {
66            let mut writer = XmlWriter::new(&mut buf);
67            self.write_xml(&mut writer);
68        }
69        String::from_utf8(buf.into_inner()).unwrap()
70    }
71
72    /// Returns a string of "prettified" serialized XML with the provided indentation.
73    fn pretty_outer_html(&self, indent: usize) -> String {
74        let mut buf: Cursor<Vec<u8>> = Cursor::new(Vec::new());
75        {
76            let mut writer = XmlWriter::new_with_indent(&mut buf, b' ', indent);
77            self.write_xml(&mut writer);
78        }
79        String::from_utf8(buf.into_inner()).unwrap()
80    }
81
82    /// Create a new element within the same tree as the method receiver.
83    fn create_element(&self, ty: &str) -> Self;
84
85    /// Create a new text node within the same tree as the method receiver.
86    fn create_text_node(&self, contents: &str) -> Self;
87
88    /// Set an attribute on this DOM node.
89    fn set_attribute(&self, name: &str, value: &str);
90
91    /// Ensure the provided attribute has been removed from this DOM node.
92    fn remove_attribute(&self, name: &str);
93
94    /// Returns the next child of this node's parent after this node itself.
95    fn next_sibling(&self) -> Option<Self>;
96
97    /// Returns the first child of this node.
98    fn first_child(&self) -> Option<Self>;
99
100    /// Adds a new child to the end of this node's children.
101    fn append_child(&self, child: &Self);
102
103    /// Replaces the provided child of this node with a new one.
104    fn replace_child(&self, new_child: &Self, existing: &Self);
105
106    /// Removes the provided child from this node.
107    fn remove_child(&self, to_remove: &Self) -> Option<Self>;
108}
109
110/// A `Node` in the augmented DOM.
111#[derive(Clone)]
112pub enum Node {
113    /// A handle to a concrete DOM node running in the browser.
114    #[cfg(feature = "webdom")]
115    Concrete(sys::Node),
116
117    /// A handle to a "virtual" DOM node, emulating the web in memory. While this implementation
118    /// lacks many features, it can run on any target that Rust supports.
119    #[cfg(feature = "rsdom")]
120    Virtual(Rc<VirtNode>),
121}
122
123impl Debug for Node {
124    fn fmt(&self, f: &mut Formatter) -> FmtResult {
125        let s = if f.alternate() {
126            self.pretty_outer_html(4)
127        } else {
128            self.outer_html()
129        };
130        f.write_str(&s)
131    }
132}
133
134impl Display for Node {
135    fn fmt(&self, f: &mut Formatter) -> FmtResult {
136        f.write_str(&self.pretty_outer_html(2))
137    }
138}
139
140impl PartialEq for Node {
141    fn eq(&self, other: &Self) -> bool {
142        match (self, other) {
143            #[cfg(feature = "webdom")]
144            (Node::Concrete(s), Node::Concrete(o)) => s.is_same_node(Some(o)),
145
146            #[cfg(feature = "rsdom")]
147            (Node::Virtual(s), Node::Virtual(o)) => Rc::ptr_eq(s, o),
148
149            #[cfg(all(feature = "webdom", feature = "rsdom"))]
150            _ => unreachable!("if moxie-dom is comparing two different types of nodes...uh-oh."),
151        }
152    }
153}
154
155impl Dom for Node {
156    fn write_xml<W: Write>(&self, writer: &mut XmlWriter<W>) {
157        match self {
158            #[cfg(feature = "webdom")]
159            Node::Concrete(n) => {
160                n.write_xml(writer);
161            }
162
163            #[cfg(feature = "rsdom")]
164            Node::Virtual(n) => {
165                n.write_xml(writer);
166            }
167        }
168    }
169
170    fn create_element(&self, ty: &str) -> Self {
171        match self {
172            #[cfg(feature = "webdom")]
173            Node::Concrete(n) => Node::Concrete(n.create_element(ty)),
174
175            #[cfg(feature = "rsdom")]
176            Node::Virtual(n) => Node::Virtual(n.create_element(ty)),
177        }
178    }
179
180    fn create_text_node(&self, contents: &str) -> Self {
181        match self {
182            #[cfg(feature = "webdom")]
183            Node::Concrete(n) => Node::Concrete(n.create_text_node(contents)),
184
185            #[cfg(feature = "rsdom")]
186            Node::Virtual(n) => Node::Virtual(n.create_text_node(contents)),
187        }
188    }
189
190    fn first_child(&self) -> Option<Self> {
191        match self {
192            #[cfg(feature = "webdom")]
193            Node::Concrete(n) => <sys::Node as Dom>::first_child(n).map(Node::Concrete),
194
195            #[cfg(feature = "rsdom")]
196            Node::Virtual(n) => n.first_child().map(Node::Virtual),
197        }
198    }
199
200    fn append_child(&self, child: &Self) {
201        match self {
202            #[cfg(feature = "webdom")]
203            Node::Concrete(n) => {
204                <sys::Node as Dom>::append_child(n, child.expect_concrete());
205            }
206
207            #[cfg(feature = "rsdom")]
208            Node::Virtual(n) => {
209                n.append_child(child.expect_virtual());
210            }
211        }
212    }
213
214    fn next_sibling(&self) -> Option<Self> {
215        match self {
216            #[cfg(feature = "webdom")]
217            Node::Concrete(n) => <sys::Node as Dom>::next_sibling(n).map(Node::Concrete),
218
219            #[cfg(feature = "rsdom")]
220            Node::Virtual(n) => n.next_sibling().map(Node::Virtual),
221        }
222    }
223
224    fn remove_child(&self, to_remove: &Self) -> Option<Self> {
225        match self {
226            #[cfg(feature = "webdom")]
227            Node::Concrete(n) => {
228                <sys::Node as Dom>::remove_child(n, to_remove.expect_concrete()).map(Node::Concrete)
229            }
230
231            #[cfg(feature = "rsdom")]
232            Node::Virtual(n) => n
233                .remove_child(to_remove.expect_virtual())
234                .map(Node::Virtual),
235        }
236    }
237
238    fn replace_child(&self, new_child: &Node, existing: &Node) {
239        match self {
240            #[cfg(feature = "webdom")]
241            Node::Concrete(n) => {
242                <sys::Node as Dom>::replace_child(
243                    n,
244                    new_child.expect_concrete(),
245                    existing.expect_concrete(),
246                );
247            }
248
249            #[cfg(feature = "rsdom")]
250            Node::Virtual(n) => {
251                n.replace_child(new_child.expect_virtual(), existing.expect_virtual());
252            }
253        }
254    }
255
256    fn set_attribute(&self, name: &str, value: &str) {
257        match self {
258            #[cfg(feature = "webdom")]
259            Node::Concrete(n) => <sys::Node as Dom>::set_attribute(n, name, value),
260            #[cfg(feature = "rsdom")]
261            Node::Virtual(n) => n.set_attribute(name, value),
262        }
263    }
264
265    fn remove_attribute(&self, name: &str) {
266        match self {
267            #[cfg(feature = "webdom")]
268            Node::Concrete(n) => <sys::Node as Dom>::remove_attribute(n, name),
269            #[cfg(feature = "rsdom")]
270            Node::Virtual(n) => n.remove_attribute(name),
271        }
272    }
273}