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