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