Skip to main content

jsdet_browser/
mutation.rs

1/// MutationObserver simulation.
2///
3/// The sandbox already observes ALL DOM mutations. MutationObserver
4/// exposes that observation stream back to JavaScript — when JS
5/// registers a MutationObserver, we feed it the mutations that
6/// occur during subsequent script execution.
7///
8use crate::dom::NodeId;
9
10/// A registered MutationObserver.
11#[derive(Debug, Clone)]
12pub struct MutationObserverRegistration {
13    /// Target node being observed.
14    pub target: NodeId,
15    /// Observation options.
16    pub options: MutationObserverOptions,
17    /// Callback function reference (opaque ID for JS).
18    pub callback_id: u32,
19}
20
21/// MutationObserver init options.
22#[derive(Debug, Clone, Default)]
23pub struct MutationObserverOptions {
24    pub child_list: bool,
25    pub attributes: bool,
26    pub character_data: bool,
27    pub subtree: bool,
28    pub attribute_old_value: bool,
29    pub character_data_old_value: bool,
30    pub attribute_filter: Vec<String>,
31}
32
33impl MutationObserverOptions {
34    pub fn from_json(json: &str) -> Self {
35        #[derive(serde::Deserialize, Default)]
36        struct Opts {
37            #[serde(default, alias = "childList")]
38            child_list: bool,
39            #[serde(default)]
40            attributes: bool,
41            #[serde(default, alias = "characterData")]
42            character_data: bool,
43            #[serde(default)]
44            subtree: bool,
45            #[serde(default, alias = "attributeOldValue")]
46            attribute_old_value: bool,
47            #[serde(default, alias = "characterDataOldValue")]
48            character_data_old_value: bool,
49            #[serde(default, alias = "attributeFilter")]
50            attribute_filter: Vec<String>,
51        }
52
53        let opts: Opts = serde_json::from_str(json).unwrap_or_default();
54        Self {
55            child_list: opts.child_list,
56            attributes: opts.attributes,
57            character_data: opts.character_data,
58            subtree: opts.subtree,
59            attribute_old_value: opts.attribute_old_value,
60            character_data_old_value: opts.character_data_old_value,
61            attribute_filter: opts.attribute_filter,
62        }
63    }
64}
65
66/// A DOM mutation record.
67#[derive(Debug, Clone, serde::Serialize)]
68pub struct MutationRecord {
69    #[serde(rename = "type")]
70    pub mutation_type: String,
71    pub target: u32,
72    #[serde(skip_serializing_if = "Vec::is_empty")]
73    pub added_nodes: Vec<u32>,
74    #[serde(skip_serializing_if = "Vec::is_empty")]
75    pub removed_nodes: Vec<u32>,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub attribute_name: Option<String>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub old_value: Option<String>,
80}
81
82/// Registry of active MutationObservers.
83#[derive(Debug, Default)]
84pub struct MutationObserverRegistry {
85    observers: Vec<MutationObserverRegistration>,
86    next_id: u32,
87    /// Pending records to deliver on next microtask.
88    pending: Vec<(u32, Vec<MutationRecord>)>,
89}
90
91impl MutationObserverRegistry {
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    /// Register a new observer. Returns callback ID.
97    pub fn observe(&mut self, target: NodeId, options: MutationObserverOptions) -> u32 {
98        let id = self.next_id;
99        self.next_id += 1;
100        self.observers.push(MutationObserverRegistration {
101            target,
102            options,
103            callback_id: id,
104        });
105        id
106    }
107
108    /// Disconnect an observer by callback ID.
109    pub fn disconnect(&mut self, callback_id: u32) {
110        self.observers.retain(|o| o.callback_id != callback_id);
111    }
112
113    /// Notify observers of a child list change.
114    pub fn notify_child_change(&mut self, target: NodeId, added: &[NodeId], removed: &[NodeId]) {
115        for observer in &self.observers {
116            if (observer.target == target || observer.options.subtree)
117                && observer.options.child_list
118            {
119                let record = MutationRecord {
120                    mutation_type: "childList".into(),
121                    target: target.0,
122                    added_nodes: added.iter().map(|n| n.0).collect(),
123                    removed_nodes: removed.iter().map(|n| n.0).collect(),
124                    attribute_name: None,
125                    old_value: None,
126                };
127                self.pending.push((observer.callback_id, vec![record]));
128            }
129        }
130    }
131
132    /// Notify observers of an attribute change.
133    pub fn notify_attribute_change(
134        &mut self,
135        target: NodeId,
136        attribute_name: &str,
137        old_value: Option<&str>,
138    ) {
139        for observer in &self.observers {
140            if (observer.target == target || observer.options.subtree)
141                && observer.options.attributes
142            {
143                if !observer.options.attribute_filter.is_empty()
144                    && !observer
145                        .options
146                        .attribute_filter
147                        .iter()
148                        .any(|f| f == attribute_name)
149                {
150                    continue;
151                }
152                let record = MutationRecord {
153                    mutation_type: "attributes".into(),
154                    target: target.0,
155                    added_nodes: Vec::new(),
156                    removed_nodes: Vec::new(),
157                    attribute_name: Some(attribute_name.to_string()),
158                    old_value: if observer.options.attribute_old_value {
159                        old_value.map(|s| s.to_string())
160                    } else {
161                        None
162                    },
163                };
164                self.pending.push((observer.callback_id, vec![record]));
165            }
166        }
167    }
168
169    /// Drain pending records as JSON to deliver to JS callbacks.
170    pub fn drain_pending(&mut self) -> Vec<(u32, String)> {
171        let pending = std::mem::take(&mut self.pending);
172        pending
173            .into_iter()
174            .map(|(id, records)| {
175                let json = serde_json::to_string(&records).unwrap_or_default();
176                (id, json)
177            })
178            .collect()
179    }
180}