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