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