script/dom/execcommand/
basecommand.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use js::context::JSContext;
6use script_bindings::inheritance::Castable;
7use style::properties::PropertyDeclarationId;
8use style::properties::generated::{LonghandId, ShorthandId};
9use style::values::specified::text::TextDecorationLine;
10use style_traits::ToCss;
11
12use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
13use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
14use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
15use crate::dom::bindings::codegen::Bindings::HTMLFontElementBinding::HTMLFontElementMethods;
16use crate::dom::bindings::str::DOMString;
17use crate::dom::document::Document;
18use crate::dom::element::Element;
19use crate::dom::execcommand::commands::bold::execute_bold_command;
20use crate::dom::execcommand::commands::defaultparagraphseparator::execute_default_paragraph_separator_command;
21use crate::dom::execcommand::commands::delete::execute_delete_command;
22use crate::dom::execcommand::commands::fontname::execute_fontname_command;
23use crate::dom::execcommand::commands::fontsize::{
24    execute_fontsize_command, font_size_loosely_equivalent, value_for_fontsize_command,
25};
26use crate::dom::execcommand::commands::italic::execute_italic_command;
27use crate::dom::execcommand::commands::strikethrough::execute_strikethrough_command;
28use crate::dom::execcommand::commands::stylewithcss::execute_style_with_css_command;
29use crate::dom::execcommand::commands::underline::execute_underline_command;
30use crate::dom::html::htmlelement::HTMLElement;
31use crate::dom::html::htmlfontelement::HTMLFontElement;
32use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
33use crate::dom::selection::Selection;
34use crate::script_runtime::CanGc;
35
36#[derive(Default, Clone, Copy, MallocSizeOf)]
37pub(crate) enum DefaultSingleLineContainerName {
38    #[default]
39    Div,
40    Paragraph,
41}
42
43impl From<DefaultSingleLineContainerName> for DOMString {
44    fn from(default_single_line_container_name: DefaultSingleLineContainerName) -> Self {
45        match default_single_line_container_name {
46            DefaultSingleLineContainerName::Div => DOMString::from("div"),
47            DefaultSingleLineContainerName::Paragraph => DOMString::from("p"),
48        }
49    }
50}
51
52/// <https://w3c.github.io/editing/docs/execCommand/#relevant-css-property>
53#[derive(Clone, Copy, Eq, PartialEq)]
54#[expect(unused)] // TODO(25005): implement all commands
55pub(crate) enum CssPropertyName {
56    BackgroundColor,
57    FontFamily,
58    FontSize,
59    FontWeight,
60    FontStyle,
61    TextDecoration,
62    TextDecorationLine,
63}
64
65impl CssPropertyName {
66    pub(crate) fn resolved_value_for_node(&self, element: &Element) -> Option<DOMString> {
67        let style = element.style()?;
68
69        Some(
70            match self {
71                CssPropertyName::BackgroundColor => style.clone_background_color().to_css_string(),
72                CssPropertyName::FontFamily => style.clone_font_family().to_css_string(),
73                CssPropertyName::FontSize => {
74                    // Font size is special, in that it can't use the resolved styles to compute
75                    // values. That's because it is influenced by other factors as well, and it
76                    // should also take into account size attributes of font elements.
77                    //
78                    // Therefore, we do a manual traversal up the chain to mimic what style
79                    // resolution would have done. This also allows us to later check for
80                    // loose equivalence for font elements, since we would return the size as an
81                    // integer, without a size indicator (e.g. `px`).
82                    //
83                    // However, if no such relevant declaration exists, then we should fallback
84                    // to pixels after all. For the effective command value, this essentially means
85                    // we will overwrite it. For the value of the "fontsize" command, we would then
86                    // need to convert it using [`legacy_font_size_for`].
87                    return element
88                        .upcast::<Node>()
89                        .inclusive_ancestors(ShadowIncluding::No)
90                        .find_map(|ancestor| {
91                            if let Some(ancestor_font) = ancestor.downcast::<HTMLFontElement>() {
92                                Some(ancestor_font.Size())
93                            } else {
94                                self.value_set_for_style(ancestor.downcast::<Element>()?)
95                            }
96                        })
97                        .or_else(|| {
98                            let pixels = style.get_font().font_size.computed_size().px();
99                            Some(format!("{}px", pixels).into())
100                        });
101                },
102                CssPropertyName::FontWeight => style.clone_font_weight().to_css_string(),
103                CssPropertyName::FontStyle => style.clone_font_style().to_css_string(),
104                CssPropertyName::TextDecoration => unreachable!("Should use longhands instead"),
105                CssPropertyName::TextDecorationLine => {
106                    let text_decoration_line = style.get_text().text_decoration_line;
107                    if text_decoration_line == TextDecorationLine::NONE {
108                        return None;
109                    }
110                    text_decoration_line.to_css_string()
111                },
112            }
113            .into(),
114        )
115    }
116
117    /// Retrieves a respective css longhand value from the style declarations of an
118    /// element. Note that this is different than the computed values, since this is
119    /// only relevant when the author specified rules on the specific element.
120    pub(crate) fn value_set_for_style(&self, element: &Element) -> Option<DOMString> {
121        let style_attribute = element.style_attribute().borrow();
122        let declarations = style_attribute.as_ref()?;
123        let document = element.owner_document();
124        let shared_lock = document.style_shared_lock();
125        let read_lock = shared_lock.read();
126        let style = declarations.read_with(&read_lock);
127
128        let longhand_id = match self {
129            CssPropertyName::BackgroundColor => LonghandId::BackgroundColor,
130            CssPropertyName::FontFamily => LonghandId::FontFamily,
131            CssPropertyName::FontSize => LonghandId::FontSize,
132            CssPropertyName::FontWeight => LonghandId::FontWeight,
133            CssPropertyName::FontStyle => LonghandId::FontStyle,
134            CssPropertyName::TextDecoration => {
135                let mut dest = String::new();
136                style
137                    .shorthand_to_css(ShorthandId::TextDecoration, &mut dest)
138                    .ok()?;
139                return Some(dest.into());
140            },
141            CssPropertyName::TextDecorationLine => LonghandId::TextDecorationLine,
142        };
143        style
144            .get(PropertyDeclarationId::Longhand(longhand_id))
145            .and_then(|value| {
146                let mut dest = String::new();
147                value.0.to_css(&mut dest).ok()?;
148                Some(dest.into())
149            })
150    }
151
152    fn property_name(&self) -> DOMString {
153        match self {
154            CssPropertyName::BackgroundColor => "background-color",
155            CssPropertyName::FontFamily => "font-family",
156            CssPropertyName::FontSize => "font-size",
157            CssPropertyName::FontWeight => "font-weight",
158            CssPropertyName::FontStyle => "font-style",
159            CssPropertyName::TextDecoration => "text-decoration",
160            CssPropertyName::TextDecorationLine => "text-decoration-line",
161        }
162        .into()
163    }
164
165    pub(crate) fn set_for_element(
166        &self,
167        cx: &mut JSContext,
168        element: &HTMLElement,
169        new_value: DOMString,
170    ) {
171        let style = element.Style(CanGc::from_cx(cx));
172
173        let _ = style.SetProperty(cx, self.property_name(), new_value, "".into());
174    }
175
176    pub(crate) fn remove_from_element(&self, cx: &mut JSContext, element: &HTMLElement) {
177        let _ = element
178            .Style(CanGc::from_cx(cx))
179            .RemoveProperty(cx, self.property_name());
180    }
181}
182
183#[derive(Clone, Copy, Eq, Hash, MallocSizeOf, PartialEq)]
184#[expect(unused)] // TODO(25005): implement all commands
185pub(crate) enum CommandName {
186    BackColor,
187    Bold,
188    Copy,
189    CreateLink,
190    Cut,
191    DefaultParagraphSeparator,
192    Delete,
193    FontName,
194    FontSize,
195    ForeColor,
196    FormatBlock,
197    ForwardDelete,
198    HiliteColor,
199    Indent,
200    InsertHorizontalRule,
201    InsertHtml,
202    InsertLineBreak,
203    InsertOrderedList,
204    InsertParagraph,
205    InsertText,
206    InsertUnorderedList,
207    Italic,
208    JustifyCenter,
209    JustifyFull,
210    JustifyLeft,
211    JustifyRight,
212    Outdent,
213    Paste,
214    Redo,
215    SelectAll,
216    Strikethrough,
217    StyleWithCss,
218    Subscript,
219    Superscript,
220    Underline,
221    Undo,
222    Unlink,
223    Usecss,
224}
225
226impl CommandName {
227    /// <https://w3c.github.io/editing/docs/execCommand/#indeterminate>
228    pub(crate) fn is_indeterminate(&self) -> bool {
229        false
230    }
231
232    /// <https://w3c.github.io/editing/docs/execCommand/#state>
233    pub(crate) fn current_state(&self, cx: &mut JSContext, document: &Document) -> Option<bool> {
234        Some(match self {
235            CommandName::StyleWithCss => {
236                // https://w3c.github.io/editing/docs/execCommand/#the-stylewithcss-command
237                // > True if the CSS styling flag is true, otherwise false.
238                document.css_styling_flag()
239            },
240            _ => {
241                // https://w3c.github.io/editing/docs/execCommand/#inline-formatting-command-definitions
242                // > If a command has inline command activated values defined, its state is true if either
243                // > no formattable node is effectively contained in the active range,
244                // > and the active range's start node's effective command value is one of the given values;
245                // > or if there is at least one formattable node effectively contained in the active range,
246                // > and all of them have an effective command value equal to one of the given values.
247                let inline_command_activated_values = self.inline_command_activated_values();
248                if inline_command_activated_values.is_empty() {
249                    return None;
250                }
251                let selection = document.GetSelection(cx)?;
252                let active_range = selection.active_range()?;
253                let mut at_least_one_child_is_formattable = false;
254                let mut all_children_have_matching_command_values = true;
255                active_range.for_each_effectively_contained_child(|node| {
256                    if !node.is_formattable() {
257                        return;
258                    }
259                    at_least_one_child_is_formattable = true;
260                    all_children_have_matching_command_values &= node
261                        .effective_command_value(self)
262                        .is_some_and(|effective_value| {
263                            inline_command_activated_values.contains(&&*effective_value.str())
264                        });
265                });
266                if at_least_one_child_is_formattable {
267                    all_children_have_matching_command_values
268                } else {
269                    active_range
270                        .start_container()
271                        .effective_command_value(self)
272                        .is_some_and(|effective_value| {
273                            inline_command_activated_values.contains(&&*effective_value.str())
274                        })
275                }
276            },
277        })
278    }
279
280    /// <https://w3c.github.io/editing/docs/execCommand/#value>
281    pub(crate) fn current_value(
282        &self,
283        cx: &mut JSContext,
284        document: &Document,
285    ) -> Option<DOMString> {
286        Some(match self {
287            CommandName::DefaultParagraphSeparator => {
288                // https://w3c.github.io/editing/docs/execCommand/#the-defaultparagraphseparator-command
289                // > Return the context object's default single-line container name.
290                document.default_single_line_container_name().into()
291            },
292            CommandName::FontSize => value_for_fontsize_command(cx, document)?,
293            _ => return None,
294        })
295    }
296
297    /// <https://w3c.github.io/editing/docs/execCommand/#equivalent-values>
298    pub(crate) fn are_equivalent_values(
299        &self,
300        first: Option<&DOMString>,
301        second: Option<&DOMString>,
302    ) -> bool {
303        match (first, second) {
304            // > Two quantities are equivalent values for a command if either both are null,
305            (None, None) => true,
306            (Some(first_str), Some(second_str)) => {
307                // > or both are strings and the command defines equivalent values and they match the definition.
308                match self {
309                    CommandName::Bold => {
310                        // https://w3c.github.io/editing/docs/execCommand/#the-bold-command
311                        // > Either the two strings are equal, or one is "bold" and the other is "700",
312                        // > or one is "normal" and the other is "400".
313                        first_str == second_str ||
314                            matches!(
315                                (first_str.str().as_ref(), second_str.str().as_ref()),
316                                ("bold", "700") |
317                                    ("700", "bold") |
318                                    ("normal", "400") |
319                                    ("400", "normal")
320                            )
321                    },
322                    // > or both are strings and they're equal and the command does not define any equivalent values,
323                    _ => first_str == second_str,
324                }
325            },
326            _ => false,
327        }
328    }
329
330    /// <https://w3c.github.io/editing/docs/execCommand/#loosely-equivalent-values>
331    pub(crate) fn are_loosely_equivalent_values(
332        &self,
333        first: Option<&DOMString>,
334        second: Option<&DOMString>,
335    ) -> bool {
336        // > Two quantities are loosely equivalent values for a command if either they are equivalent values for the command,
337        if self.are_equivalent_values(first, second) {
338            return true;
339        }
340        // > or if the command is the fontSize command;
341        // > one of the quantities is one of "x-small", "small", "medium", "large", "x-large", "xx-large", or "xxx-large";
342        // > and the other quantity is the resolved value of "font-size" on a font element whose size attribute
343        // > has the corresponding value set ("1" through "7" respectively).
344        if let (CommandName::FontSize, Some(first), Some(second)) = (self, first, second) {
345            font_size_loosely_equivalent(first, second)
346        } else {
347            false
348        }
349    }
350
351    /// <https://w3c.github.io/editing/docs/execCommand/#relevant-css-property>
352    pub(crate) fn relevant_css_property(&self) -> Option<CssPropertyName> {
353        // > This is defined for certain inline formatting commands, and is used in algorithms specific to those commands.
354        // > It is an implementation detail, and is not exposed to authors.
355        Some(match self {
356            CommandName::Bold => CssPropertyName::FontWeight,
357            CommandName::FontName => CssPropertyName::FontFamily,
358            CommandName::FontSize => CssPropertyName::FontSize,
359            CommandName::Italic => CssPropertyName::FontStyle,
360            // > If a command does not have a relevant CSS property specified, it defaults to null.
361            _ => return None,
362        })
363    }
364
365    pub(crate) fn resolved_value_for_node(&self, element: &Element) -> Option<DOMString> {
366        let property = self.relevant_css_property()?;
367        property.resolved_value_for_node(element)
368    }
369
370    pub(crate) fn is_enabled_in_plaintext_only_state(&self) -> bool {
371        matches!(
372            self,
373            CommandName::Copy |
374                CommandName::Cut |
375                CommandName::DefaultParagraphSeparator |
376                CommandName::FormatBlock |
377                CommandName::ForwardDelete |
378                CommandName::InsertHtml |
379                CommandName::InsertLineBreak |
380                CommandName::InsertParagraph |
381                CommandName::InsertText |
382                CommandName::Paste |
383                CommandName::Redo |
384                CommandName::StyleWithCss |
385                CommandName::Undo |
386                CommandName::Usecss |
387                CommandName::Delete
388        )
389    }
390
391    /// <https://w3c.github.io/editing/docs/execCommand/#action>
392    pub(crate) fn execute(
393        &self,
394        cx: &mut JSContext,
395        document: &Document,
396        selection: &Selection,
397        value: DOMString,
398    ) -> bool {
399        match self {
400            CommandName::Bold => execute_bold_command(cx, document, selection),
401            CommandName::DefaultParagraphSeparator => {
402                execute_default_paragraph_separator_command(document, value)
403            },
404            CommandName::Delete => execute_delete_command(cx, document, selection),
405            CommandName::FontName => execute_fontname_command(cx, document, selection, value),
406            CommandName::FontSize => execute_fontsize_command(cx, document, selection, value),
407            CommandName::Italic => execute_italic_command(cx, document, selection),
408            CommandName::Strikethrough => execute_strikethrough_command(cx, document, selection),
409            CommandName::StyleWithCss => execute_style_with_css_command(document, value),
410            CommandName::Underline => execute_underline_command(cx, document, selection),
411            _ => false,
412        }
413    }
414
415    /// <https://w3c.github.io/editing/docs/execCommand/#inline-command-activated-values>
416    pub(crate) fn inline_command_activated_values(&self) -> Vec<&str> {
417        match self {
418            // https://w3c.github.io/editing/docs/execCommand/#the-bold-command
419            CommandName::Bold => vec!["bold", "600", "700", "800", "900"],
420            // https://w3c.github.io/editing/docs/execCommand/#the-italic-command
421            CommandName::Italic => vec!["italic", "oblique"],
422            // https://w3c.github.io/editing/docs/execCommand/#the-strikethrough-command
423            CommandName::Strikethrough => vec!["line-through"],
424            // https://w3c.github.io/editing/docs/execCommand/#the-subscript-command
425            CommandName::Subscript => vec!["subscript"],
426            // https://w3c.github.io/editing/docs/execCommand/#the-superscript-command
427            CommandName::Superscript => vec!["superscript"],
428            // https://w3c.github.io/editing/docs/execCommand/#the-underline-command
429            CommandName::Underline => vec!["underline"],
430            _ => vec![],
431        }
432    }
433}