Skip to main content

tachys/
hydration.rs

1use crate::{
2    renderer::{CastFrom, Rndr},
3    view::{Position, PositionState},
4};
5#[cfg(any(debug_assertions, leptos_debuginfo))]
6use std::cell::Cell;
7use std::{cell::RefCell, panic::Location, rc::Rc};
8use web_sys::{Comment, Element, Node, Text};
9
10#[cfg(feature = "mark_branches")]
11const COMMENT_NODE: u16 = 8;
12
13/// Hydration works by walking over the DOM, adding interactivity as needed.
14///
15/// This cursor tracks the location in the DOM that is currently being hydrated. Each that type
16/// implements [`RenderHtml`](crate::view::RenderHtml) knows how to advance the cursor to access
17/// the nodes it needs.
18#[derive(Debug)]
19pub struct Cursor(Rc<RefCell<crate::renderer::types::Node>>);
20
21impl Clone for Cursor {
22    fn clone(&self) -> Self {
23        Self(Rc::clone(&self.0))
24    }
25}
26
27impl Cursor
28where
29    crate::renderer::types::Element: AsRef<crate::renderer::types::Node>,
30{
31    /// Creates a new cursor starting at the root element.
32    pub fn new(root: crate::renderer::types::Element) -> Self {
33        let root = <crate::renderer::types::Element as AsRef<
34            crate::renderer::types::Node,
35        >>::as_ref(&root)
36        .clone();
37        Self(Rc::new(RefCell::new(root)))
38    }
39
40    /// Returns the node at which the cursor is currently located.
41    pub fn current(&self) -> crate::renderer::types::Node {
42        self.0.borrow().clone()
43    }
44
45    /// Advances to the next child of the node at which the cursor is located.
46    ///
47    /// Does nothing if there is no child.
48    pub fn child(&self) {
49        let mut inner = self.0.borrow_mut();
50        if let Some(node) = Rndr::first_child(&inner) {
51            *inner = node;
52        }
53
54        #[cfg(feature = "mark_branches")]
55        {
56            while inner.node_type() == COMMENT_NODE {
57                if let Some(content) = inner.text_content() {
58                    if content.starts_with("bo") || content.starts_with("bc") {
59                        if let Some(sibling) = Rndr::next_sibling(&inner) {
60                            *inner = sibling;
61                            continue;
62                        }
63                    }
64                }
65
66                break;
67            }
68        }
69        // //drop(inner);
70        //crate::log(">> which is ");
71        //Rndr::log_node(&self.current());
72    }
73
74    /// Advances to the next sibling of the node at which the cursor is located.
75    ///
76    /// Does nothing if there is no sibling.
77    pub fn sibling(&self) {
78        let mut inner = self.0.borrow_mut();
79        if let Some(node) = Rndr::next_sibling(&inner) {
80            *inner = node;
81        }
82
83        #[cfg(feature = "mark_branches")]
84        {
85            while inner.node_type() == COMMENT_NODE {
86                if let Some(content) = inner.text_content() {
87                    if content.starts_with("bo") || content.starts_with("bc") {
88                        if let Some(sibling) = Rndr::next_sibling(&inner) {
89                            *inner = sibling;
90                            continue;
91                        }
92                    }
93                }
94                break;
95            }
96        }
97        //drop(inner);
98        //crate::log(">> which is ");
99        //Rndr::log_node(&self.current());
100    }
101
102    /// Moves to the parent of the node at which the cursor is located.
103    ///
104    /// Does nothing if there is no parent.
105    pub fn parent(&self) {
106        let mut inner = self.0.borrow_mut();
107        if let Some(node) = Rndr::get_parent(&inner) {
108            *inner = node;
109        }
110    }
111
112    /// Sets the cursor to some node.
113    pub fn set(&self, node: crate::renderer::types::Node) {
114        *self.0.borrow_mut() = node;
115    }
116
117    /// Advances to the next placeholder node and returns it
118    pub fn next_placeholder(
119        &self,
120        position: &PositionState,
121    ) -> crate::renderer::types::Placeholder {
122        //crate::dom::log("looking for placeholder after");
123        //Rndr::log_node(&self.current());
124        self.advance_to_placeholder(position);
125        let marker = self.current();
126        crate::renderer::types::Placeholder::cast_from(marker.clone())
127            .unwrap_or_else(|| failed_to_cast_marker_node(marker))
128    }
129
130    /// Advances to the next placeholder node.
131    pub fn advance_to_placeholder(&self, position: &PositionState) {
132        if position.get() == Position::FirstChild {
133            self.child();
134        } else {
135            self.sibling();
136        }
137        position.set(Position::NextChild);
138    }
139}
140
141#[cfg(any(debug_assertions, leptos_debuginfo))]
142thread_local! {
143    static CURRENTLY_HYDRATING: Cell<Option<&'static Location<'static>>> = const { Cell::new(None) };
144}
145
146pub(crate) fn set_currently_hydrating(
147    location: Option<&'static Location<'static>>,
148) {
149    #[cfg(any(debug_assertions, leptos_debuginfo))]
150    {
151        CURRENTLY_HYDRATING.set(location);
152    }
153    #[cfg(not(any(debug_assertions, leptos_debuginfo)))]
154    {
155        _ = location;
156    }
157}
158
159pub(crate) fn failed_to_cast_element(tag_name: &str, node: Node) -> Element {
160    #[cfg(not(any(debug_assertions, leptos_debuginfo)))]
161    {
162        _ = node;
163        unreachable!();
164    }
165    #[cfg(any(debug_assertions, leptos_debuginfo))]
166    {
167        let hydrating = CURRENTLY_HYDRATING
168            .take()
169            .map(|n| n.to_string())
170            .unwrap_or_else(|| "{unknown}".to_string());
171        web_sys::console::error_3(
172            &wasm_bindgen::JsValue::from_str(&format!(
173                "A hydration error occurred while trying to hydrate an \
174                 element defined at {hydrating}.\n\nThe framework expected an \
175                 HTML <{tag_name}> element, but found this instead: ",
176            )),
177            &node,
178            &wasm_bindgen::JsValue::from_str(
179                "\n\nThe hydration mismatch may have occurred slightly \
180                 earlier, but this is the first time the framework found a \
181                 node of an unexpected type.",
182            ),
183        );
184        panic!(
185            "Unrecoverable hydration error. Please read the error message \
186             directly above this for more details."
187        );
188    }
189}
190
191pub(crate) fn failed_to_cast_marker_node(node: Node) -> Comment {
192    #[cfg(not(any(debug_assertions, leptos_debuginfo)))]
193    {
194        _ = node;
195        unreachable!();
196    }
197    #[cfg(any(debug_assertions, leptos_debuginfo))]
198    {
199        let hydrating = CURRENTLY_HYDRATING
200            .take()
201            .map(|n| n.to_string())
202            .unwrap_or_else(|| "{unknown}".to_string());
203        web_sys::console::error_3(
204            &wasm_bindgen::JsValue::from_str(&format!(
205                "A hydration error occurred while trying to hydrate an \
206                 element defined at {hydrating}.\n\nThe framework expected a \
207                 marker node, but found this instead: ",
208            )),
209            &node,
210            &wasm_bindgen::JsValue::from_str(
211                "\n\nThe hydration mismatch may have occurred slightly \
212                 earlier, but this is the first time the framework found a \
213                 node of an unexpected type.",
214            ),
215        );
216        panic!(
217            "Unrecoverable hydration error. Please read the error message \
218             directly above this for more details."
219        );
220    }
221}
222
223pub(crate) fn failed_to_cast_text_node(node: Node) -> Text {
224    #[cfg(not(any(debug_assertions, leptos_debuginfo)))]
225    {
226        _ = node;
227        unreachable!();
228    }
229    #[cfg(any(debug_assertions, leptos_debuginfo))]
230    {
231        let hydrating = CURRENTLY_HYDRATING
232            .take()
233            .map(|n| n.to_string())
234            .unwrap_or_else(|| "{unknown}".to_string());
235        web_sys::console::error_3(
236            &wasm_bindgen::JsValue::from_str(&format!(
237                "A hydration error occurred while trying to hydrate an \
238                 element defined at {hydrating}.\n\nThe framework expected a \
239                 text node, but found this instead: ",
240            )),
241            &node,
242            &wasm_bindgen::JsValue::from_str(
243                "\n\nThe hydration mismatch may have occurred slightly \
244                 earlier, but this is the first time the framework found a \
245                 node of an unexpected type.",
246            ),
247        );
248        panic!(
249            "Unrecoverable hydration error. Please read the error message \
250             directly above this for more details."
251        );
252    }
253}