1use cssparser::match_ignore_ascii_case;
6use js::context::JSContext;
7use script_bindings::inheritance::Castable;
8
9use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
10use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
11use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
12use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
13use crate::dom::bindings::root::DomRoot;
14use crate::dom::bindings::str::DOMString;
15use crate::dom::comment::Comment;
16use crate::dom::document::Document;
17use crate::dom::event::Event;
18use crate::dom::event::inputevent::InputEvent;
19use crate::dom::execcommand::basecommand::CommandName;
20use crate::dom::execcommand::commands::fontsize::maybe_normalize_pixels;
21use crate::dom::html::htmlelement::HTMLElement;
22use crate::dom::node::Node;
23use crate::dom::processinginstruction::ProcessingInstruction;
24use crate::dom::selection::Selection;
25
26fn is_command_listed_in_miscellaneous_section(command_name: CommandName) -> bool {
28 matches!(
29 command_name,
30 CommandName::DefaultParagraphSeparator |
31 CommandName::Redo |
32 CommandName::SelectAll |
33 CommandName::StyleWithCss |
34 CommandName::Undo |
35 CommandName::Usecss
36 )
37}
38
39fn bump_selection_out_of_invalid_node(cx: &mut JSContext, selection: &Selection) -> Result<(), ()> {
40 let active_range = selection
46 .active_range(cx)
47 .expect("Must always have an active range");
48 if let start_container = active_range.start_container() &&
49 (start_container.is::<Comment>() || start_container.is::<ProcessingInstruction>())
50 {
51 let Some(parent) = start_container.GetParentNode() else {
52 return Err(());
53 };
54 let _ = active_range.SetStart(&parent, start_container.index());
55 }
56 if let end_container = active_range.end_container() &&
57 (end_container.is::<Comment>() || end_container.is::<ProcessingInstruction>())
58 {
59 let Some(parent) = end_container.GetParentNode() else {
60 return Err(());
61 };
62 let _ = active_range.SetEnd(&parent, end_container.index());
63 }
64 Ok(())
65}
66
67fn mapped_value_of_command(command: CommandName) -> DOMString {
69 match command {
70 CommandName::BackColor => "formatBackColor",
71 CommandName::Bold => "formatBold",
72 CommandName::CreateLink => "insertLink",
73 CommandName::Cut => "deleteByCut",
74 CommandName::Delete => "deleteContentBackward",
75 CommandName::FontName => "formatFontName",
76 CommandName::ForeColor => "formatFontColor",
77 CommandName::ForwardDelete => "deleteContentForward",
78 CommandName::Indent => "formatIndent",
79 CommandName::InsertHorizontalRule => "insertHorizontalRule",
80 CommandName::InsertLineBreak => "insertLineBreak",
81 CommandName::InsertOrderedList => "insertOrderedList",
82 CommandName::InsertParagraph => "insertParagraph",
83 CommandName::InsertText => "insertText",
84 CommandName::InsertUnorderedList => "insertUnorderedList",
85 CommandName::JustifyCenter => "formatJustifyCenter",
86 CommandName::JustifyFull => "formatJustifyFull",
87 CommandName::JustifyLeft => "formatJustifyLeft",
88 CommandName::JustifyRight => "formatJustifyRight",
89 CommandName::Outdent => "formatOutdent",
90 CommandName::Paste => "insertFromPaste",
91 CommandName::Redo => "historyRedo",
92 CommandName::Strikethrough => "formatStrikeThrough",
93 CommandName::Superscript => "formatSuperscript",
94 CommandName::Undo => "historyUndo",
95 _ => "",
96 }
97 .into()
98}
99
100impl Node {
101 fn is_in_plaintext_only_state(&self) -> bool {
102 self.downcast::<HTMLElement>()
103 .is_some_and(|el| el.ContentEditable().str() == "plaintext-only")
104 }
105}
106
107impl Document {
108 fn selection_if_command_is_enabled(
110 &self,
111 cx: &mut JSContext,
112 command_name: CommandName,
113 ) -> Option<DomRoot<Selection>> {
114 let selection = self.GetSelection(cx)?;
115 if is_command_listed_in_miscellaneous_section(command_name) {
120 return Some(selection);
121 }
122 let range = selection.active_range(cx)?;
124 let start_container_editing_host = range.start_container().editing_host_of()?;
126 let end_container_editing_host = range.end_container().editing_host_of()?;
130 if !command_name.is_enabled(cx, &range, &start_container_editing_host) {
136 return None;
137 }
138
139 if !command_name.is_enabled_in_plaintext_only_state() &&
141 (start_container_editing_host.is_in_plaintext_only_state() ||
142 end_container_editing_host.is_in_plaintext_only_state())
143 {
144 None
145 } else {
146 Some(selection)
147 }
148 }
149
150 fn command_if_command_is_supported(&self, command_id: &DOMString) -> Option<CommandName> {
152 Some(match_ignore_ascii_case! { &command_id.str(),
155 "backcolor" => CommandName::BackColor,
156 "bold" => CommandName::Bold,
157 "createlink" => CommandName::CreateLink,
158 "delete" => CommandName::Delete,
159 "defaultparagraphseparator" => CommandName::DefaultParagraphSeparator,
160 "fontname" => CommandName::FontName,
161 "fontsize" => CommandName::FontSize,
162 "forecolor" => CommandName::ForeColor,
163 "forwarddelete" => CommandName::ForwardDelete,
164 "hilitecolor" => CommandName::HiliteColor,
165 "indent" => CommandName::Indent,
166 "inserthorizontalrule" => CommandName::InsertHorizontalRule,
167 "insertimage" => CommandName::InsertImage,
168 "insertlinebreak" => CommandName::InsertLineBreak,
169 "insertparagraph" => CommandName::InsertParagraph,
170 "inserttext" => CommandName::InsertText,
171 "italic" => CommandName::Italic,
172 "removeformat" => CommandName::RemoveFormat,
173 "strikethrough" => CommandName::Strikethrough,
174 "stylewithcss" => CommandName::StyleWithCss,
175 "subscript" => CommandName::Subscript,
176 "superscript" => CommandName::Superscript,
177 "underline" => CommandName::Underline,
178 "unlink" => CommandName::Unlink,
179 _ => return None,
180 })
181 }
182}
183
184pub(crate) trait DocumentExecCommandSupport {
185 fn is_command_supported(&self, command_id: DOMString) -> bool;
186 fn is_command_indeterminate(&self, cx: &mut JSContext, command_id: DOMString) -> bool;
187 fn command_state_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> bool;
188 fn command_value_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> DOMString;
189 fn check_support_and_enabled(
190 &self,
191 cx: &mut JSContext,
192 command_id: &DOMString,
193 ) -> Option<(CommandName, DomRoot<Selection>)>;
194 fn exec_command_for_command_id(
195 &self,
196 cx: &mut JSContext,
197 command_id: DOMString,
198 value: DOMString,
199 ) -> bool;
200}
201
202impl DocumentExecCommandSupport for Document {
203 fn is_command_supported(&self, command_id: DOMString) -> bool {
205 self.command_if_command_is_supported(&command_id).is_some()
206 }
207
208 fn is_command_indeterminate(&self, cx: &mut JSContext, command_id: DOMString) -> bool {
210 self.command_if_command_is_supported(&command_id)
213 .is_some_and(|command| command.is_indeterminate(cx, self))
214 }
215
216 fn command_state_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> bool {
218 let Some(command) = self.command_if_command_is_supported(&command_id) else {
220 return false;
221 };
222 let Some(state) = command.current_state(cx, self) else {
223 return false;
224 };
225 self.state_override(&command).unwrap_or(state)
228 }
229
230 fn command_value_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> DOMString {
232 let Some(command) = self.command_if_command_is_supported(&command_id) else {
234 return DOMString::new();
235 };
236 let Some(value) = command.current_value(cx, self) else {
237 return DOMString::new();
238 };
239 self.value_override(&command)
241 .map(|value_override| {
242 if command == CommandName::FontSize {
245 maybe_normalize_pixels(&value_override, self).unwrap_or(value_override)
246 } else {
247 value_override
248 }
249 })
250 .unwrap_or(value)
252 }
253
254 fn check_support_and_enabled(
256 &self,
257 cx: &mut JSContext,
258 command_id: &DOMString,
259 ) -> Option<(CommandName, DomRoot<Selection>)> {
260 let command = self.command_if_command_is_supported(command_id)?;
262 let selection = self.selection_if_command_is_enabled(cx, command)?;
263 Some((command, selection))
264 }
265
266 fn exec_command_for_command_id(
268 &self,
269 cx: &mut JSContext,
270 command_id: DOMString,
271 value: DOMString,
272 ) -> bool {
273 let window = self.window();
274 let Some((command, mut selection)) = self.check_support_and_enabled(cx, &command_id) else {
276 return false;
277 };
278 let affected_editing_host = if !is_command_listed_in_miscellaneous_section(command) {
280 let Some(affected_editing_host) = selection
284 .active_range(cx)
285 .expect("Must always have an active range")
286 .CommonAncestorContainer()
287 .editing_host_of()
288 else {
289 return false;
290 };
291
292 let event = InputEvent::new(
295 cx,
296 window,
297 None,
298 atom!("beforeinput"),
299 true,
300 true,
301 Some(window),
302 0,
303 None,
304 false,
305 "".into(),
306 );
307 let event = event.upcast::<Event>();
308 if !event.fire(cx, affected_editing_host.upcast()) {
310 return false;
311 }
312
313 let Some(new_selection) = self.selection_if_command_is_enabled(cx, command) else {
315 return false;
316 };
317 selection = new_selection;
318
319 selection
323 .active_range(cx)
324 .expect("Must always have an active range")
325 .CommonAncestorContainer()
326 .editing_host_of()
327 } else {
328 None
329 };
330
331 if affected_editing_host.is_some() &&
332 bump_selection_out_of_invalid_node(cx, &selection).is_err()
333 {
334 return false;
335 }
336
337 let result = command.execute(cx, self, &selection, value);
339 if !result {
341 return false;
342 }
343 if let Some(affected_editing_host) = affected_editing_host {
347 let event = InputEvent::new(
348 cx,
349 window,
350 None,
351 atom!("input"),
352 true,
353 false,
354 Some(window),
355 0,
356 None,
357 false,
358 mapped_value_of_command(command),
359 );
360 let event = event.upcast::<Event>();
361 event.set_trusted(true);
362 event.fire(cx, affected_editing_host.upcast());
363 }
364
365 true
367 }
368}