Skip to main content

bliss_dom/
dom_control.rs

1//! DomController implementation for BaseDocument
2//!
3//! Provides a stable API for querying and mutating the DOM from outside the rendering engine.
4
5use bliss_traits::dom_control::{
6    DomControlError, DomControlResult, DomController, NodeId, NodeInfo,
7};
8use markup5ever::local_name;
9
10use crate::{BaseDocument, NodeData};
11
12impl DomController for BaseDocument {
13    fn query_selector(&self, selector: &str) -> DomControlResult<Option<NodeId>> {
14        self.query_selector(selector)
15            .map_err(|e| DomControlError::InvalidSelector(format!("{:?}", e)))
16    }
17
18    fn query_selector_all(&self, selector: &str) -> DomControlResult<Vec<NodeId>> {
19        self.query_selector_all(selector)
20            .map_err(|e| DomControlError::InvalidSelector(format!("{:?}", e)))
21            .map(|smallvec| smallvec.into_vec())
22    }
23
24    fn get_element_by_id(&self, id: &str) -> Option<NodeId> {
25        self.get_element_by_id(id)
26    }
27
28    fn get_node_info(&self, node_id: NodeId) -> DomControlResult<NodeInfo> {
29        let node = self
30            .get_node(node_id)
31            .ok_or(DomControlError::NodeNotFound(node_id))?;
32
33        let tag_name = node.element_data().map(|el| el.name.local.to_string());
34
35        let text_content = match &node.data {
36            NodeData::Text(text_data) => Some(text_data.content.clone()),
37            _ => {
38                // For elements, collect text from all child text nodes
39                let mut text = String::new();
40                for child_id in &node.children {
41                    if let Some(child) = self.get_node(*child_id) {
42                        if let NodeData::Text(text_data) = &child.data {
43                            text.push_str(&text_data.content);
44                        }
45                    }
46                }
47                if text.is_empty() { None } else { Some(text) }
48            }
49        };
50
51        let attributes = node
52            .element_data()
53            .map(|el| {
54                el.attrs()
55                    .iter()
56                    .map(|attr| (attr.name.local.to_string(), attr.value.clone()))
57                    .collect()
58            })
59            .unwrap_or_default();
60
61        Ok(NodeInfo {
62            id: node_id,
63            tag_name,
64            text_content,
65            attributes,
66        })
67    }
68
69    fn set_attribute(&mut self, node_id: NodeId, name: &str, value: &str) -> DomControlResult<()> {
70        let node = self
71            .get_node_mut(node_id)
72            .ok_or(DomControlError::NodeNotFound(node_id))?;
73
74        let element = node.element_data_mut().ok_or_else(|| {
75            DomControlError::InvalidMutation(format!("Node {} is not an element", node_id))
76        })?;
77
78        // Check if this is the id attribute - need to update nodes_to_id map
79        if name == "id" {
80            // Note: We can't update nodes_to_id here because self is already mutably borrowed
81            // This would need to be handled at a higher level or with interior mutability
82            // For now, just update the element's id
83            element.id = Some(style::Atom::from(value));
84        }
85
86        // Find and update or add the attribute
87        let local_name = local_name!(name);
88        if let Some(attr) = element
89            .attrs
90            .iter_mut()
91            .find(|a| a.name.local == local_name)
92        {
93            attr.value = value.to_string();
94        } else {
95            use crate::Attribute;
96            use crate::qual_name;
97            element.attrs.push(Attribute {
98                name: qual_name!(name),
99                value: value.to_string(),
100            });
101        }
102
103        // Mark node for restyling if needed
104        node.set_restyle_hint(
105            style::invalidation::element::restyle_hints::RestyleHint::restyle_subtree(),
106        );
107
108        // Update nodes_to_id after releasing the mutable borrow
109        if name == "id" {
110            self.nodes_to_id.insert(value.to_string(), node_id);
111        }
112
113        Ok(())
114    }
115
116    fn remove_attribute(&mut self, node_id: NodeId, name: &str) -> DomControlResult<()> {
117        let node = self
118            .get_node_mut(node_id)
119            .ok_or(DomControlError::NodeNotFound(node_id))?;
120
121        let element = node.element_data_mut().ok_or_else(|| {
122            DomControlError::InvalidMutation(format!("Node {} is not an element", node_id))
123        })?;
124
125        // Check if this is the id attribute
126        let is_id = name == "id";
127        let old_id = if is_id { element.id.clone() } else { None };
128
129        let local_name = local_name!(name);
130        element.attrs.retain(|attr| attr.name.local != local_name);
131
132        if is_id {
133            element.id = None;
134        }
135
136        // Mark node for restyling
137        node.set_restyle_hint(
138            style::invalidation::element::restyle_hints::RestyleHint::restyle_subtree(),
139        );
140
141        // Update nodes_to_id after releasing the mutable borrow
142        if is_id {
143            if let Some(id) = old_id {
144                self.nodes_to_id.remove(&id.to_string());
145            }
146        }
147
148        Ok(())
149    }
150
151    fn set_text_content(&mut self, node_id: NodeId, text: &str) -> DomControlResult<()> {
152        let node = self
153            .get_node_mut(node_id)
154            .ok_or(DomControlError::NodeNotFound(node_id))?;
155
156        match &mut node.data {
157            NodeData::Text(text_data) => {
158                text_data.content = text.to_string();
159            }
160            NodeData::Element(_) | NodeData::AnonymousBlock(_) => {
161                // For elements, clear children and add a single text node
162                // Note: This is a simplified implementation - a full implementation
163                // would need to handle this more carefully
164                return Err(DomControlError::InvalidMutation(
165                    "set_text_content on elements requires child node manipulation".to_string(),
166                ));
167            }
168            _ => {
169                return Err(DomControlError::InvalidMutation(
170                    "Cannot set text content on this node type".to_string(),
171                ));
172            }
173        }
174
175        // Mark node for restyle
176        node.set_restyle_hint(
177            style::invalidation::element::restyle_hints::RestyleHint::restyle_subtree(),
178        );
179
180        Ok(())
181    }
182
183    fn set_style_property(
184        &mut self,
185        node_id: NodeId,
186        property: &str,
187        value: &str,
188    ) -> DomControlResult<()> {
189        // Clone needed data first to avoid borrow issues
190        let guard = self.guard.clone();
191        let url_extra_data = self.url.url_extra_data().clone();
192
193        let node = self
194            .get_node_mut(node_id)
195            .ok_or(DomControlError::NodeNotFound(node_id))?;
196
197        let element = node.element_data_mut().ok_or_else(|| {
198            DomControlError::InvalidMutation(format!("Node {} is not an element", node_id))
199        })?;
200
201        element.set_style_property(property, value, &guard, url_extra_data);
202
203        // Mark node for restyle
204        node.set_restyle_hint(
205            style::invalidation::element::restyle_hints::RestyleHint::restyle_subtree(),
206        );
207
208        Ok(())
209    }
210
211    fn remove_style_property(&mut self, node_id: NodeId, property: &str) -> DomControlResult<()> {
212        // Clone needed data first to avoid borrow issues
213        let guard = self.guard.clone();
214        let url_extra_data = self.url.url_extra_data().clone();
215
216        let node = self
217            .get_node_mut(node_id)
218            .ok_or(DomControlError::NodeNotFound(node_id))?;
219
220        let element = node.element_data_mut().ok_or_else(|| {
221            DomControlError::InvalidMutation(format!("Node {} is not an element", node_id))
222        })?;
223
224        element.remove_style_property(property, &guard, url_extra_data);
225
226        // Mark node for restyle
227        node.set_restyle_hint(
228            style::invalidation::element::restyle_hints::RestyleHint::restyle_subtree(),
229        );
230
231        Ok(())
232    }
233
234    fn set_inner_html(&mut self, node_id: NodeId, html: &str) -> DomControlResult<()> {
235        // Ensure node exists and is an element
236        let _ = self
237            .get_node(node_id)
238            .and_then(|node| node.element_data())
239            .ok_or(DomControlError::NodeNotFound(node_id))?;
240
241        let parser = self.html_parser_provider.clone();
242        let mut mutr = self.mutate();
243        parser.parse_inner_html(&mut mutr, node_id, html);
244        Ok(())
245    }
246
247    fn add_event_listener(
248        &mut self,
249        node_id: NodeId,
250        event: &str,
251        handler_id: u64,
252    ) -> DomControlResult<()> {
253        // Validate the node exists
254        if self.get_node(node_id).is_none() {
255            return Err(DomControlError::NodeNotFound(node_id));
256        }
257
258        self.event_listeners
259            .entry((node_id, event.to_string()))
260            .or_default()
261            .push(handler_id);
262
263        Ok(())
264    }
265
266    fn remove_event_listener(
267        &mut self,
268        node_id: NodeId,
269        event: &str,
270        handler_id: u64,
271    ) -> DomControlResult<()> {
272        // Validate the node exists
273        if self.get_node(node_id).is_none() {
274            return Err(DomControlError::NodeNotFound(node_id));
275        }
276
277        if let Some(handlers) = self.event_listeners.get_mut(&(node_id, event.to_string())) {
278            handlers.retain(|&id| id != handler_id);
279            if handlers.is_empty() {
280                self.event_listeners.remove(&(node_id, event.to_string()));
281            }
282        }
283
284        Ok(())
285    }
286}
287
288/// A wrapper that combines BaseDocument with a policy for capability-based control
289pub struct PolicyDomController<'a> {
290    doc: &'a mut BaseDocument,
291    policy: std::sync::Arc<dyn bliss_traits::dom_control::DomCapabilityPolicy>,
292    doc_id: usize,
293}
294
295impl<'a> PolicyDomController<'a> {
296    pub fn new(
297        doc: &'a mut BaseDocument,
298        policy: std::sync::Arc<dyn bliss_traits::dom_control::DomCapabilityPolicy>,
299        doc_id: usize,
300    ) -> Self {
301        Self {
302            doc,
303            policy,
304            doc_id,
305        }
306    }
307}
308
309impl<'a> DomController for PolicyDomController<'a> {
310    fn query_selector(&self, selector: &str) -> DomControlResult<Option<NodeId>> {
311        if !self.policy.allow_query(self.doc_id, selector) {
312            return Err(DomControlError::PermissionDenied(format!(
313                "Query not allowed: {}",
314                selector
315            )));
316        }
317        self.doc
318            .query_selector(selector)
319            .map_err(|e| DomControlError::InvalidSelector(format!("{:?}", e)))
320    }
321
322    fn query_selector_all(&self, selector: &str) -> DomControlResult<Vec<NodeId>> {
323        if !self.policy.allow_query(self.doc_id, selector) {
324            return Err(DomControlError::PermissionDenied(format!(
325                "Query not allowed: {}",
326                selector
327            )));
328        }
329        self.doc
330            .query_selector_all(selector)
331            .map_err(|e| DomControlError::InvalidSelector(format!("{:?}", e)))
332            .map(|smallvec| smallvec.into_vec())
333    }
334
335    fn get_element_by_id(&self, id: &str) -> Option<NodeId> {
336        // ID lookup is generally safe, but could be policy-gated if needed
337        self.doc.get_element_by_id(id)
338    }
339
340    fn get_node_info(&self, node_id: NodeId) -> DomControlResult<NodeInfo> {
341        // Node info is read-only, generally safe
342        self.doc.get_node_info(node_id)
343    }
344
345    fn set_attribute(&mut self, node_id: NodeId, name: &str, value: &str) -> DomControlResult<()> {
346        if !self
347            .policy
348            .allow_mutation(self.doc_id, node_id, "set_attribute")
349        {
350            return Err(DomControlError::PermissionDenied(
351                "set_attribute not allowed".to_string(),
352            ));
353        }
354        self.doc.set_attribute(node_id, name, value)
355    }
356
357    fn remove_attribute(&mut self, node_id: NodeId, name: &str) -> DomControlResult<()> {
358        if !self
359            .policy
360            .allow_mutation(self.doc_id, node_id, "remove_attribute")
361        {
362            return Err(DomControlError::PermissionDenied(
363                "remove_attribute not allowed".to_string(),
364            ));
365        }
366        self.doc.remove_attribute(node_id, name)
367    }
368
369    fn set_text_content(&mut self, node_id: NodeId, text: &str) -> DomControlResult<()> {
370        if !self
371            .policy
372            .allow_mutation(self.doc_id, node_id, "set_text_content")
373        {
374            return Err(DomControlError::PermissionDenied(
375                "set_text_content not allowed".to_string(),
376            ));
377        }
378        self.doc.set_text_content(node_id, text)
379    }
380
381    fn set_style_property(
382        &mut self,
383        node_id: NodeId,
384        property: &str,
385        value: &str,
386    ) -> DomControlResult<()> {
387        if !self
388            .policy
389            .allow_mutation(self.doc_id, node_id, "set_style_property")
390        {
391            return Err(DomControlError::PermissionDenied(
392                "set_style_property not allowed".to_string(),
393            ));
394        }
395        self.doc.set_style_property(node_id, property, value);
396        Ok(())
397    }
398
399    fn remove_style_property(&mut self, node_id: NodeId, property: &str) -> DomControlResult<()> {
400        if !self
401            .policy
402            .allow_mutation(self.doc_id, node_id, "remove_style_property")
403        {
404            return Err(DomControlError::PermissionDenied(
405                "remove_style_property not allowed".to_string(),
406            ));
407        }
408        self.doc.remove_style_property(node_id, property);
409        Ok(())
410    }
411
412    fn set_inner_html(&mut self, node_id: NodeId, html: &str) -> DomControlResult<()> {
413        if !self
414            .policy
415            .allow_mutation(self.doc_id, node_id, "set_inner_html")
416        {
417            return Err(DomControlError::PermissionDenied(
418                "set_inner_html not allowed".to_string(),
419            ));
420        }
421        self.doc.set_inner_html(node_id, html)
422    }
423
424    fn add_event_listener(
425        &mut self,
426        node_id: NodeId,
427        event: &str,
428        handler_id: u64,
429    ) -> DomControlResult<()> {
430        if !self
431            .policy
432            .allow_event_listener(self.doc_id, node_id, event)
433        {
434            return Err(DomControlError::PermissionDenied(format!(
435                "add_event_listener not allowed for {}",
436                event
437            )));
438        }
439        self.doc.add_event_listener(node_id, event, handler_id)
440    }
441
442    fn remove_event_listener(
443        &mut self,
444        node_id: NodeId,
445        event: &str,
446        handler_id: u64,
447    ) -> DomControlResult<()> {
448        if !self
449            .policy
450            .allow_event_listener(self.doc_id, node_id, event)
451        {
452            return Err(DomControlError::PermissionDenied(format!(
453                "remove_event_listener not allowed for {}",
454                event
455            )));
456        }
457        self.doc.remove_event_listener(node_id, event, handler_id)
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464    use crate::{DocumentConfig, ElementData, NodeData, qual_name};
465
466    #[test]
467    fn test_dom_controller_query_selector() {
468        let mut doc = BaseDocument::new(DocumentConfig::default());
469
470        // Create a simple DOM: <div id="test"><span class="item">Hello</span></div>
471        let div = doc.create_node(NodeData::Element(ElementData::new(
472            qual_name!("div"),
473            vec![],
474        )));
475        let span = doc.create_node(NodeData::Element(ElementData::new(
476            qual_name!("span"),
477            vec![],
478        )));
479        let text = doc.create_text_node("Hello");
480
481        // Build tree structure - connect div to document root first
482        doc.root_node_mut().children.push(div);
483        doc.get_node_mut(div).unwrap().parent = Some(0); // Document root is at index 0
484
485        // Connect span to div
486        doc.get_node_mut(div).unwrap().children.push(span);
487        doc.get_node_mut(span).unwrap().parent = Some(div);
488
489        // Connect text to span
490        doc.get_node_mut(span).unwrap().children.push(text);
491        doc.get_node_mut(text).unwrap().parent = Some(span);
492
493        // Test query_selector
494        let result = doc.query_selector("span").unwrap();
495        assert_eq!(result, Some(span));
496
497        let result = doc.query_selector(".nonexistent").unwrap();
498        assert_eq!(result, None);
499    }
500
501    #[test]
502    fn test_dom_controller_get_node_info() {
503        let mut doc = BaseDocument::new(DocumentConfig::default());
504
505        let div = doc.create_node(NodeData::Element(ElementData::new(
506            qual_name!("div"),
507            vec![],
508        )));
509
510        let info = doc.get_node_info(div).unwrap();
511        assert_eq!(info.id, div);
512        assert_eq!(info.tag_name, Some("div".to_string()));
513    }
514}