Skip to main content

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::attr::parse_legacy_color;
8use style::color::ColorFlags;
9use style::properties::PropertyDeclarationId;
10use style::properties::generated::{LonghandId, ShorthandId};
11use style::values::specified::text::TextDecorationLine;
12use style_traits::ToCss;
13
14use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
15use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
16use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
17use crate::dom::bindings::codegen::Bindings::HTMLFontElementBinding::HTMLFontElementMethods;
18use crate::dom::bindings::str::DOMString;
19use crate::dom::document::Document;
20use crate::dom::element::Element;
21use crate::dom::execcommand::commands::backcolor::execute_backcolor_command;
22use crate::dom::execcommand::commands::bold::execute_bold_command;
23use crate::dom::execcommand::commands::createlink::execute_createlink_command;
24use crate::dom::execcommand::commands::defaultparagraphseparator::execute_default_paragraph_separator_command;
25use crate::dom::execcommand::commands::delete::execute_delete_command;
26use crate::dom::execcommand::commands::fontname::execute_fontname_command;
27use crate::dom::execcommand::commands::fontsize::{
28    execute_fontsize_command, font_size_loosely_equivalent, value_for_fontsize_command,
29};
30use crate::dom::execcommand::commands::forecolor::execute_forecolor_command;
31use crate::dom::execcommand::commands::forwarddelete::execute_forward_delete_command;
32use crate::dom::execcommand::commands::hilitecolor::execute_hilitecolor_command;
33use crate::dom::execcommand::commands::indent::execute_indent_command;
34use crate::dom::execcommand::commands::inserthorizontalrule::execute_insert_horizontal_rule_command;
35use crate::dom::execcommand::commands::insertimage::execute_insert_image_command;
36use crate::dom::execcommand::commands::insertlinebreak::execute_insert_line_break_command;
37use crate::dom::execcommand::commands::insertparagraph::execute_insert_paragraph_command;
38use crate::dom::execcommand::commands::inserttext::execute_insert_text_command;
39use crate::dom::execcommand::commands::italic::execute_italic_command;
40use crate::dom::execcommand::commands::removeformat::execute_removeformat_command;
41use crate::dom::execcommand::commands::strikethrough::execute_strikethrough_command;
42use crate::dom::execcommand::commands::stylewithcss::execute_style_with_css_command;
43use crate::dom::execcommand::commands::subscript::execute_subscript_command;
44use crate::dom::execcommand::commands::superscript::execute_superscript_command;
45use crate::dom::execcommand::commands::underline::execute_underline_command;
46use crate::dom::execcommand::commands::unlink::execute_unlink_command;
47use crate::dom::html::htmlelement::HTMLElement;
48use crate::dom::html::htmlfontelement::HTMLFontElement;
49use crate::dom::iterators::ShadowIncluding;
50use crate::dom::node::{Node, NodeTraits};
51use crate::dom::range::Range;
52use crate::dom::selection::Selection;
53
54#[derive(Default, Clone, Copy, MallocSizeOf)]
55pub(crate) enum DefaultSingleLineContainerName {
56    #[default]
57    Div,
58    Paragraph,
59}
60
61impl DefaultSingleLineContainerName {
62    pub(crate) fn str(&self) -> &str {
63        match self {
64            DefaultSingleLineContainerName::Div => "div",
65            DefaultSingleLineContainerName::Paragraph => "p",
66        }
67    }
68}
69
70impl From<DefaultSingleLineContainerName> for DOMString {
71    fn from(default_single_line_container_name: DefaultSingleLineContainerName) -> Self {
72        match default_single_line_container_name {
73            DefaultSingleLineContainerName::Div => DOMString::from_static("div"),
74            DefaultSingleLineContainerName::Paragraph => DOMString::from_static("p"),
75        }
76    }
77}
78
79pub(crate) enum BoolOrOptionalString {
80    Bool(bool),
81    OptionalString(Option<DOMString>),
82}
83
84impl From<Option<DOMString>> for BoolOrOptionalString {
85    fn from(optional_string: Option<DOMString>) -> Self {
86        Self::OptionalString(optional_string)
87    }
88}
89
90impl From<bool> for BoolOrOptionalString {
91    fn from(bool_: bool) -> Self {
92        Self::Bool(bool_)
93    }
94}
95
96pub(crate) struct RecordedStateOfCommand {
97    pub(crate) command: CommandName,
98    pub(crate) value: BoolOrOptionalString,
99}
100
101impl RecordedStateOfCommand {
102    pub(crate) fn for_command_node(command: CommandName, node: &Node) -> Self {
103        let value = node.effective_command_value(&command).into();
104        Self { command, value }
105    }
106
107    pub(crate) fn for_command_node_with_inline_activated_values(
108        command: CommandName,
109        node: &Node,
110    ) -> Self {
111        let effective_command_value = node.effective_command_value(&command);
112        let value = effective_command_value
113            .is_some_and(|effective_command_value| {
114                command
115                    .inline_command_activated_values()
116                    .contains(&effective_command_value.str().as_ref())
117            })
118            .into();
119        Self { command, value }
120    }
121
122    pub(crate) fn for_command_node_with_value(
123        cx: &mut JSContext,
124        command: CommandName,
125        document: &Document,
126    ) -> Self {
127        let value = command.current_value(cx, document).into();
128        Self { command, value }
129    }
130
131    fn for_command_state_override(command: CommandName, document: &Document) -> Option<Self> {
132        let value = document.state_override(&command)?.into();
133        Some(Self { command, value })
134    }
135
136    fn for_command_value_override(command: CommandName, document: &Document) -> Option<Self> {
137        let value_override = document.value_override(&command)?;
138        let value = Some(value_override).into();
139        Some(Self { command, value })
140    }
141}
142
143/// <https://w3c.github.io/editing/docs/execCommand/#relevant-css-property>
144#[derive(Clone, Copy, Eq, PartialEq)]
145pub(crate) enum CssPropertyName {
146    BackgroundColor,
147    Color,
148    FontFamily,
149    FontSize,
150    FontWeight,
151    FontStyle,
152    TextDecoration,
153    TextDecorationLine,
154}
155
156impl CssPropertyName {
157    pub(crate) fn resolved_value_for_node(&self, element: &Element) -> Option<DOMString> {
158        let style = element.style()?;
159
160        Some(
161            match self {
162                CssPropertyName::BackgroundColor => {
163                    let background_color = style.clone_background_color();
164                    if let Some(absolute_color) = background_color.as_absolute() {
165                        // Used as an early-exit when figuring out on which element to resolve
166                        // the style in `effective_command_value`
167                        if absolute_color.is_transparent() {
168                            return None;
169                        }
170                        // Requires legacy SRGB syntax, which is what all tests expect.
171                        // E.g. it should use `rgba()` instead of `rgb()`, even if the alpha
172                        // is zero.
173                        let mut absolute_color = *absolute_color;
174                        absolute_color.flags.insert(ColorFlags::IS_LEGACY_SRGB);
175                        return Some(absolute_color.to_css_string().into());
176                    }
177                    background_color.to_css_string()
178                },
179                CssPropertyName::Color => {
180                    // Detached font elements (e.g. does created with `document.createElement`
181                    // and not yet present in DOM) dont have a computed style for `color`.
182                    // Since we create detached parent elements and compute "effective command
183                    // value" for these elements, we need to special case this. Otherwise, we
184                    // would add both a `color` attribute and `color` style declaration
185                    // to a parent font element.
186                    if let Some(ancestor_font) = element.downcast::<HTMLFontElement>() {
187                        let color = ancestor_font.Color();
188                        if !color.is_empty() {
189                            return Some(color);
190                        }
191                    }
192                    style.clone_color().to_css_string()
193                },
194                CssPropertyName::FontFamily => {
195                    // Detached font elements (e.g. does created with `document.createElement`
196                    // and not yet present in DOM) dont have a computed style for `fontFamily`.
197                    // Since we create detached parent elements and compute "effective command
198                    // value" for these elements, we need to special case this. Otherwise, we
199                    // would add both a `face` attribute and `font-family` style declaration
200                    // to a parent font element.
201                    if let Some(ancestor_font) = element.downcast::<HTMLFontElement>() {
202                        let face = ancestor_font.Face();
203                        if !face.is_empty() {
204                            return Some(face);
205                        }
206                    }
207                    style.clone_font_family().to_css_string()
208                },
209                CssPropertyName::FontSize => {
210                    // Font size is special, in that it can't use the resolved styles to compute
211                    // values. That's because it is influenced by other factors as well, and it
212                    // should also take into account size attributes of font elements.
213                    //
214                    // Therefore, we do a manual traversal up the chain to mimic what style
215                    // resolution would have done. This also allows us to later check for
216                    // loose equivalence for font elements, since we would return the size as an
217                    // integer, without a size indicator (e.g. `px`).
218                    //
219                    // However, if no such relevant declaration exists, then we should fallback
220                    // to pixels after all. For the effective command value, this essentially means
221                    // we will overwrite it. For the value of the "fontsize" command, we would then
222                    // need to convert it using [`legacy_font_size_for`].
223                    return element
224                        .upcast::<Node>()
225                        .inclusive_ancestors(ShadowIncluding::No)
226                        .find_map(|ancestor| {
227                            if let Some(ancestor_font) = ancestor.downcast::<HTMLFontElement>() {
228                                Some(ancestor_font.Size())
229                            } else {
230                                self.value_set_for_style(ancestor.downcast::<Element>()?)
231                            }
232                        })
233                        .or_else(|| {
234                            let pixels = style.get_font().font_size.computed_size().px();
235                            Some(format!("{}px", pixels).into())
236                        });
237                },
238                CssPropertyName::FontWeight => style.clone_font_weight().to_css_string(),
239                CssPropertyName::FontStyle => style.clone_font_style().to_css_string(),
240                CssPropertyName::TextDecoration => unreachable!("Should use longhands instead"),
241                CssPropertyName::TextDecorationLine => {
242                    let text_decoration_line = style.get_text().text_decoration_line;
243                    if text_decoration_line == TextDecorationLine::NONE {
244                        return None;
245                    }
246                    text_decoration_line.to_css_string()
247                },
248            }
249            .into(),
250        )
251    }
252
253    /// Retrieves a respective css longhand value from the style declarations of an
254    /// element. Note that this is different than the computed values, since this is
255    /// only relevant when the author specified rules on the specific element.
256    pub(crate) fn value_set_for_style(&self, element: &Element) -> Option<DOMString> {
257        let style_attribute = element.style_attribute().borrow();
258        let declarations = style_attribute.as_ref()?;
259        let document = element.owner_document();
260        let shared_lock = document.style_shared_author_lock();
261        let read_lock = shared_lock.read();
262        let style = declarations.read_with(&read_lock);
263
264        let longhand_id = match self {
265            CssPropertyName::BackgroundColor => LonghandId::BackgroundColor,
266            CssPropertyName::Color => LonghandId::Color,
267            CssPropertyName::FontFamily => LonghandId::FontFamily,
268            CssPropertyName::FontSize => LonghandId::FontSize,
269            CssPropertyName::FontWeight => LonghandId::FontWeight,
270            CssPropertyName::FontStyle => LonghandId::FontStyle,
271            CssPropertyName::TextDecoration => {
272                let mut dest = String::new();
273                style
274                    .shorthand_to_css(ShorthandId::TextDecoration, &mut dest)
275                    .ok()?;
276                return Some(dest.into());
277            },
278            CssPropertyName::TextDecorationLine => LonghandId::TextDecorationLine,
279        };
280        style
281            .get(PropertyDeclarationId::Longhand(longhand_id))
282            .and_then(|value| {
283                let mut dest = String::new();
284                value.0.to_css(&mut dest).ok()?;
285                Some(dest.into())
286            })
287    }
288
289    fn property_name(&self) -> DOMString {
290        match self {
291            CssPropertyName::BackgroundColor => "background-color",
292            CssPropertyName::Color => "color",
293            CssPropertyName::FontFamily => "font-family",
294            CssPropertyName::FontSize => "font-size",
295            CssPropertyName::FontWeight => "font-weight",
296            CssPropertyName::FontStyle => "font-style",
297            CssPropertyName::TextDecoration => "text-decoration",
298            CssPropertyName::TextDecorationLine => "text-decoration-line",
299        }
300        .into()
301    }
302
303    pub(crate) fn set_for_element(
304        &self,
305        cx: &mut JSContext,
306        element: &HTMLElement,
307        new_value: DOMString,
308    ) {
309        let style = element.Style(cx);
310
311        let _ = style.SetProperty(cx, self.property_name(), new_value, "".into());
312    }
313
314    pub(crate) fn remove_from_element(&self, cx: &mut JSContext, element: &HTMLElement) {
315        let _ = element.Style(cx).RemoveProperty(cx, self.property_name());
316    }
317}
318
319#[derive(Clone, Copy, Eq, Hash, MallocSizeOf, PartialEq)]
320#[expect(unused)] // TODO(25005): implement all commands
321pub(crate) enum CommandName {
322    BackColor,
323    Bold,
324    Copy,
325    CreateLink,
326    Cut,
327    DefaultParagraphSeparator,
328    Delete,
329    FontName,
330    FontSize,
331    ForeColor,
332    FormatBlock,
333    ForwardDelete,
334    HiliteColor,
335    Indent,
336    InsertHorizontalRule,
337    InsertHtml,
338    InsertImage,
339    InsertLineBreak,
340    InsertOrderedList,
341    InsertParagraph,
342    InsertText,
343    InsertUnorderedList,
344    Italic,
345    JustifyCenter,
346    JustifyFull,
347    JustifyLeft,
348    JustifyRight,
349    Outdent,
350    Paste,
351    Redo,
352    RemoveFormat,
353    SelectAll,
354    Strikethrough,
355    StyleWithCss,
356    Subscript,
357    Superscript,
358    Underline,
359    Undo,
360    Unlink,
361    Usecss,
362}
363
364impl CommandName {
365    /// <https://w3c.github.io/editing/docs/execCommand/#indeterminate>
366    pub(crate) fn is_indeterminate(&self, cx: &mut JSContext, document: &Document) -> bool {
367        if !self.is_standard_inline_value_command() {
368            return false;
369        }
370        // https://w3c.github.io/editing/docs/execCommand/#standard-inline-value-command
371        // > it is indeterminate if among formattable nodes that are effectively contained in the active range,
372        // > there are two that have distinct effective command values.
373        let Some(selection) = document.GetSelection(cx) else {
374            return false;
375        };
376        let Some(active_range) = selection.active_range(cx) else {
377            return false;
378        };
379        let mut at_least_two_different_effective_values = false;
380        let mut previous_effective_value: Option<DOMString> = None;
381        active_range.for_each_effectively_contained_child(|node| {
382            if at_least_two_different_effective_values || !node.is_formattable(cx.no_gc()) {
383                return;
384            }
385            if let Some(effective_command_value) = node.effective_command_value(self) {
386                // https://w3c.github.io/editing/docs/execCommand/#the-subscript-command
387                // https://w3c.github.io/editing/docs/execCommand/#the-superscript-command
388                // > or if there is some formattable node effectively contained in
389                // > the active range with effective command value "mixed".
390                if matches!(self, CommandName::Subscript | CommandName::Superscript) &&
391                    effective_command_value == "mixed"
392                {
393                    at_least_two_different_effective_values = true;
394                }
395                if let Some(previous_effective_value) = &previous_effective_value {
396                    if &effective_command_value != previous_effective_value {
397                        at_least_two_different_effective_values = true;
398                    }
399                } else {
400                    previous_effective_value = Some(effective_command_value);
401                }
402            }
403        });
404        at_least_two_different_effective_values
405    }
406
407    /// <https://w3c.github.io/editing/docs/execCommand/#state>
408    pub(crate) fn current_state(&self, cx: &mut JSContext, document: &Document) -> Option<bool> {
409        Some(match self {
410            CommandName::StyleWithCss => {
411                // https://w3c.github.io/editing/docs/execCommand/#the-stylewithcss-command
412                // > True if the CSS styling flag is true, otherwise false.
413                document.css_styling_flag()
414            },
415            _ => {
416                // https://w3c.github.io/editing/docs/execCommand/#inline-formatting-command-definitions
417                // > If a command has inline command activated values defined, its state is true if either
418                // > no formattable node is effectively contained in the active range,
419                // > and the active range's start node's effective command value is one of the given values;
420                // > or if there is at least one formattable node effectively contained in the active range,
421                // > and all of them have an effective command value equal to one of the given values.
422                let inline_command_activated_values = self.inline_command_activated_values();
423                if inline_command_activated_values.is_empty() {
424                    return None;
425                }
426                let selection = document.GetSelection(cx)?;
427                let active_range = selection.active_range(cx)?;
428                let mut at_least_one_child_is_formattable = false;
429                let mut all_children_have_matching_command_values = true;
430                active_range.for_each_effectively_contained_child(|node| {
431                    if !node.is_formattable(cx.no_gc()) {
432                        return;
433                    }
434                    at_least_one_child_is_formattable = true;
435                    all_children_have_matching_command_values &= node
436                        .effective_command_value(self)
437                        .is_some_and(|effective_value| {
438                            inline_command_activated_values.contains(&&*effective_value.str())
439                        });
440                });
441                if at_least_one_child_is_formattable {
442                    all_children_have_matching_command_values
443                } else {
444                    active_range
445                        .start_container()
446                        .effective_command_value(self)
447                        .is_some_and(|effective_value| {
448                            inline_command_activated_values.contains(&&*effective_value.str())
449                        })
450                }
451            },
452        })
453    }
454
455    /// <https://w3c.github.io/editing/docs/execCommand/#value>
456    pub(crate) fn current_value(
457        &self,
458        cx: &mut JSContext,
459        document: &Document,
460    ) -> Option<DOMString> {
461        Some(match self {
462            CommandName::DefaultParagraphSeparator => {
463                // https://w3c.github.io/editing/docs/execCommand/#the-defaultparagraphseparator-command
464                // > Return the context object's default single-line container name.
465                document.default_single_line_container_name().into()
466            },
467            CommandName::FontSize => value_for_fontsize_command(cx, document)?,
468            _ if self.is_standard_inline_value_command() => {
469                // https://w3c.github.io/editing/docs/execCommand/#standard-inline-value-command
470                // > Its value is the effective command value of the first formattable node that
471                // > is effectively contained in the active range; or if there is no such node,
472                // > the effective command value of the active range's start node;
473                // > or if that is null, the empty string.
474                let selection = document.GetSelection(cx)?;
475                let active_range = selection.active_range(cx)?;
476
477                active_range
478                    .first_formattable_contained_node(cx.no_gc())
479                    .unwrap_or_else(|| active_range.start_container())
480                    .effective_command_value(self)
481                    .unwrap_or_default()
482            },
483            _ => return None,
484        })
485    }
486
487    /// <https://w3c.github.io/editing/docs/execCommand/#equivalent-values>
488    pub(crate) fn are_equivalent_values(
489        &self,
490        first: Option<&DOMString>,
491        second: Option<&DOMString>,
492    ) -> bool {
493        match (first, second) {
494            // > Two quantities are equivalent values for a command if either both are null,
495            (None, None) => true,
496            (Some(first_str), Some(second_str)) => {
497                // > or both are strings and the command defines equivalent values and they match the definition.
498                match self {
499                    CommandName::Bold => {
500                        // https://w3c.github.io/editing/docs/execCommand/#the-bold-command
501                        // > Either the two strings are equal, or one is "bold" and the other is "700",
502                        // > or one is "normal" and the other is "400".
503                        first_str == second_str ||
504                            matches!(
505                                (first_str.str().as_ref(), second_str.str().as_ref()),
506                                ("bold", "700") |
507                                    ("700", "bold") |
508                                    ("normal", "400") |
509                                    ("400", "normal")
510                            )
511                    },
512                    CommandName::BackColor | CommandName::ForeColor | CommandName::HiliteColor => {
513                        // https://w3c.github.io/editing/docs/execCommand/#the-backcolor-command
514                        // https://w3c.github.io/editing/docs/execCommand/#the-forecolor-command
515                        // https://w3c.github.io/editing/docs/execCommand/#the-hilitecolor-command
516                        // > Either both strings are valid CSS colors and have the same red, green, blue, and alpha components,
517                        // > or neither string is a valid CSS color.
518                        match (
519                            parse_legacy_color(&first_str.str()),
520                            parse_legacy_color(&second_str.str()),
521                        ) {
522                            (Ok(first_legacy_color), Ok(second_legacy_color)) => {
523                                first_legacy_color == second_legacy_color
524                            },
525                            (Err(_), Err(_)) => true,
526                            _ => false,
527                        }
528                    },
529                    // > or both are strings and they're equal and the command does not define any equivalent values,
530                    _ => first_str == second_str,
531                }
532            },
533            _ => false,
534        }
535    }
536
537    /// <https://w3c.github.io/editing/docs/execCommand/#loosely-equivalent-values>
538    pub(crate) fn are_loosely_equivalent_values(
539        &self,
540        first: Option<&DOMString>,
541        second: Option<&DOMString>,
542    ) -> bool {
543        // > Two quantities are loosely equivalent values for a command if either they are equivalent values for the command,
544        if self.are_equivalent_values(first, second) {
545            return true;
546        }
547        // > or if the command is the fontSize command;
548        // > one of the quantities is one of "x-small", "small", "medium", "large", "x-large", "xx-large", or "xxx-large";
549        // > and the other quantity is the resolved value of "font-size" on a font element whose size attribute
550        // > has the corresponding value set ("1" through "7" respectively).
551        if let (CommandName::FontSize, Some(first), Some(second)) = (self, first, second) {
552            font_size_loosely_equivalent(first, second)
553        } else {
554            false
555        }
556    }
557
558    /// <https://w3c.github.io/editing/docs/execCommand/#record-current-overrides>
559    pub(crate) fn record_current_overrides(document: &Document) -> Vec<RecordedStateOfCommand> {
560        // Step 1. Let overrides be a list of (string, string or boolean) ordered pairs, initially empty.
561        let mut overrides = vec![];
562        // Step 2. If there is a value override for "createLink",
563        // add ("createLink", value override for "createLink") to overrides.
564        if let Some(value_override) =
565            RecordedStateOfCommand::for_command_value_override(CommandName::CreateLink, document)
566        {
567            overrides.push(value_override);
568        }
569        // Step 3. For each command in the list "bold", "italic", "strikethrough",
570        // "subscript", "superscript", "underline", in order:
571        // if there is a state override for command, add (command, command's state override) to overrides.
572        for command in [
573            CommandName::Bold,
574            CommandName::Italic,
575            CommandName::Strikethrough,
576            CommandName::Subscript,
577            CommandName::Superscript,
578            CommandName::Underline,
579        ] {
580            if let Some(state_override) =
581                RecordedStateOfCommand::for_command_state_override(command, document)
582            {
583                overrides.push(state_override);
584            }
585        }
586        // Step 4. For each command in the list "fontName", "fontSize", "foreColor", "hiliteColor",
587        // in order: if there is a value override for command,
588        // add (command, command's value override) to overrides.
589        for command in [
590            CommandName::FontName,
591            CommandName::FontSize,
592            CommandName::ForeColor,
593            CommandName::HiliteColor,
594        ] {
595            if let Some(value_override) =
596                RecordedStateOfCommand::for_command_value_override(command, document)
597            {
598                overrides.push(value_override);
599            }
600        }
601        // Step 5. Return overrides.
602        overrides
603    }
604
605    /// <https://w3c.github.io/editing/docs/execCommand/#relevant-css-property>
606    pub(crate) fn relevant_css_property(&self) -> Option<CssPropertyName> {
607        // > This is defined for certain inline formatting commands, and is used in algorithms specific to those commands.
608        // > It is an implementation detail, and is not exposed to authors.
609        Some(match self {
610            CommandName::BackColor => CssPropertyName::BackgroundColor,
611            CommandName::Bold => CssPropertyName::FontWeight,
612            CommandName::FontName => CssPropertyName::FontFamily,
613            CommandName::FontSize => CssPropertyName::FontSize,
614            CommandName::ForeColor => CssPropertyName::Color,
615            CommandName::HiliteColor => CssPropertyName::BackgroundColor,
616            CommandName::Italic => CssPropertyName::FontStyle,
617            // > If a command does not have a relevant CSS property specified, it defaults to null.
618            _ => return None,
619        })
620    }
621
622    pub(crate) fn resolved_value_for_node(&self, element: &Element) -> Option<DOMString> {
623        let property = self.relevant_css_property()?;
624        property.resolved_value_for_node(element)
625    }
626
627    /// <https://w3c.github.io/editing/docs/execCommand/#standard-inline-value-command>
628    pub(crate) fn is_standard_inline_value_command(&self) -> bool {
629        matches!(
630            self,
631            CommandName::BackColor |
632                CommandName::FontName |
633                CommandName::ForeColor |
634                CommandName::HiliteColor
635        )
636    }
637
638    pub(crate) fn is_enabled(&self, cx: &JSContext, range: &Range, editing_host: &Node) -> bool {
639        match self {
640            // The delete command is not enabled in the situation that the cursor is inside the
641            // editing host at the start, where a backspace would do nothing. However, if the
642            // editing host itself is selected then it is enabled.
643            //
644            // Therefore, start at the start_container and traverse its ancestors up to editing
645            // host. If the index remains 0, then there is no effective character to delete and
646            // the command is disabled.
647            CommandName::Delete => {
648                if !range.collapsed() {
649                    return true;
650                }
651                let start_container = range.start_container();
652                if *start_container == *editing_host {
653                    // TODO: This should return true. However, that crashes deletes at the start
654                    // of the editing host. There currently is no way to distinguish between a
655                    // range that is collapsed to a full node and set to before a node. Chromium
656                    // tracks this with a concept of "anchor position before/after node":
657                    // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/position.h;l=39;drc=a5c6d7b223bfc6028ecae4b1f17d711374238c0d
658                    return false;
659                }
660                let mut current_offset = range.start_offset();
661                for current_ancestor in
662                    start_container.inclusive_ancestors_unrooted(cx, ShadowIncluding::Yes)
663                {
664                    if current_offset != 0 {
665                        return true;
666                    }
667                    if *current_ancestor == editing_host {
668                        return false;
669                    }
670                    current_offset = current_ancestor.index();
671                }
672                false
673            },
674            _ => true,
675        }
676    }
677
678    pub(crate) fn is_enabled_in_plaintext_only_state(&self) -> bool {
679        matches!(
680            self,
681            CommandName::Copy |
682                CommandName::Cut |
683                CommandName::DefaultParagraphSeparator |
684                CommandName::FormatBlock |
685                CommandName::ForwardDelete |
686                CommandName::InsertHtml |
687                CommandName::InsertLineBreak |
688                CommandName::InsertParagraph |
689                CommandName::InsertText |
690                CommandName::Paste |
691                CommandName::Redo |
692                CommandName::StyleWithCss |
693                CommandName::Undo |
694                CommandName::Usecss |
695                CommandName::Delete
696        )
697    }
698
699    /// <https://w3c.github.io/editing/docs/execCommand/#preserves-overrides>
700    fn preserves_overrides(&self) -> bool {
701        matches!(
702            self,
703            CommandName::Delete |
704                CommandName::FormatBlock |
705                CommandName::ForwardDelete |
706                CommandName::Indent |
707                CommandName::InsertHorizontalRule |
708                CommandName::InsertHtml |
709                CommandName::InsertImage |
710                CommandName::InsertLineBreak |
711                CommandName::InsertOrderedList |
712                CommandName::InsertParagraph |
713                CommandName::InsertUnorderedList |
714                CommandName::JustifyCenter |
715                CommandName::JustifyFull |
716                CommandName::JustifyLeft |
717                CommandName::JustifyRight |
718                CommandName::Outdent
719        )
720    }
721
722    /// <https://w3c.github.io/editing/docs/execCommand/#action>
723    pub(crate) fn execute(
724        &self,
725        cx: &mut JSContext,
726        document: &Document,
727        selection: &Selection,
728        value: DOMString,
729    ) -> bool {
730        // https://w3c.github.io/editing/docs/execCommand/#preserves-overrides
731        // > If a command preserves overrides, then before taking its action,
732        // > the user agent must record current overrides.
733        let overrides = if self.preserves_overrides() {
734            Self::record_current_overrides(document)
735        } else {
736            vec![]
737        };
738        let result = match self {
739            CommandName::BackColor => execute_backcolor_command(cx, document, selection, value),
740            CommandName::Bold => execute_bold_command(cx, document, selection),
741            CommandName::CreateLink => execute_createlink_command(cx, document, selection, value),
742            CommandName::DefaultParagraphSeparator => {
743                execute_default_paragraph_separator_command(document, value)
744            },
745            CommandName::Delete => execute_delete_command(cx, document, selection),
746            CommandName::FontName => execute_fontname_command(cx, document, selection, value),
747            CommandName::FontSize => execute_fontsize_command(cx, document, selection, value),
748            CommandName::ForeColor => execute_forecolor_command(cx, document, selection, value),
749            CommandName::ForwardDelete => execute_forward_delete_command(cx, document, selection),
750            CommandName::HiliteColor => execute_hilitecolor_command(cx, document, selection, value),
751            CommandName::Indent => execute_indent_command(cx, document, selection),
752            CommandName::InsertHorizontalRule => {
753                execute_insert_horizontal_rule_command(cx, document, selection)
754            },
755            CommandName::InsertImage => {
756                execute_insert_image_command(cx, document, selection, value)
757            },
758            CommandName::InsertLineBreak => {
759                execute_insert_line_break_command(cx, document, selection)
760            },
761            CommandName::InsertParagraph => {
762                execute_insert_paragraph_command(cx, document, selection)
763            },
764            CommandName::InsertText => execute_insert_text_command(cx, document, selection, value),
765            CommandName::Italic => execute_italic_command(cx, document, selection),
766            CommandName::RemoveFormat => execute_removeformat_command(cx, document, selection),
767            CommandName::Strikethrough => execute_strikethrough_command(cx, document, selection),
768            CommandName::StyleWithCss => execute_style_with_css_command(document, value),
769            CommandName::Subscript => execute_subscript_command(cx, document, selection),
770            CommandName::Superscript => execute_superscript_command(cx, document, selection),
771            CommandName::Underline => execute_underline_command(cx, document, selection),
772            CommandName::Unlink => execute_unlink_command(cx, selection),
773            _ => false,
774        };
775
776        // https://w3c.github.io/editing/docs/execCommand/#preserves-overrides
777        // > After taking the action, if the active range is collapsed,
778        // > it must restore states and values from the recorded list.
779        if let Some(active_range) = selection
780            .active_range(cx)
781            .filter(|active_range| active_range.collapsed())
782        {
783            active_range.restore_states_and_values(cx, selection, document, overrides);
784        }
785
786        result
787    }
788
789    /// <https://w3c.github.io/editing/docs/execCommand/#inline-command-activated-values>
790    pub(crate) fn inline_command_activated_values(&self) -> Vec<&str> {
791        match self {
792            // https://w3c.github.io/editing/docs/execCommand/#the-bold-command
793            CommandName::Bold => vec!["bold", "600", "700", "800", "900"],
794            // https://w3c.github.io/editing/docs/execCommand/#the-italic-command
795            CommandName::Italic => vec!["italic", "oblique"],
796            // https://w3c.github.io/editing/docs/execCommand/#the-strikethrough-command
797            CommandName::Strikethrough => vec!["line-through"],
798            // https://w3c.github.io/editing/docs/execCommand/#the-subscript-command
799            CommandName::Subscript => vec!["subscript"],
800            // https://w3c.github.io/editing/docs/execCommand/#the-superscript-command
801            CommandName::Superscript => vec!["superscript"],
802            // https://w3c.github.io/editing/docs/execCommand/#the-underline-command
803            CommandName::Underline => vec!["underline"],
804            _ => vec![],
805        }
806    }
807}