Skip to main content

script/dom/execcommand/contenteditable/
element.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 html5ever::{LocalName, local_name, ns};
6use js::context::JSContext;
7use script_bindings::inheritance::Castable;
8use style::attr::AttrValue;
9use style::properties::{LonghandId, PropertyDeclaration, PropertyDeclarationId, ShorthandId};
10use style::values::specified::TextDecorationLine;
11use style::values::specified::box_::DisplayOutside;
12
13use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
14use crate::dom::bindings::inheritance::{ElementTypeId, HTMLElementTypeId, NodeTypeId};
15use crate::dom::bindings::root::DomRoot;
16use crate::dom::bindings::str::DOMString;
17use crate::dom::element::Element;
18use crate::dom::execcommand::basecommand::{CommandName, CssPropertyName};
19use crate::dom::execcommand::commands::fontsize::font_size_to_css_font;
20use crate::dom::execcommand::contenteditable::node::move_preserving_ranges;
21use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
22use crate::dom::html::htmlfontelement::HTMLFontElement;
23use crate::dom::node::node::{Node, NodeTraits};
24
25impl Element {
26    pub(crate) fn resolved_display_value(&self) -> Option<DisplayOutside> {
27        self.style().map(|style| style.get_box().display.outside())
28    }
29
30    /// <https://w3c.github.io/editing/docs/execCommand/#specified-command-value>
31    pub(crate) fn specified_command_value(&self, command: &CommandName) -> Option<DOMString> {
32        match command {
33            // Step 1. If command is "backColor" or "hiliteColor" and the Element's display property does not have resolved value "inline", return null.
34            CommandName::BackColor | CommandName::HiliteColor
35                if self
36                    .resolved_display_value()
37                    .is_none_or(|display| display != DisplayOutside::Inline) =>
38            {
39                return None;
40            },
41            // Step 2. If command is "createLink" or "unlink":
42            CommandName::CreateLink | CommandName::Unlink => {
43                // Step 2.1. If element is an a element and has an href attribute,
44                // return the value of that attribute.
45                if let Some(anchor) = self.downcast::<HTMLAnchorElement>() {
46                    return anchor
47                        .upcast::<Element>()
48                        .get_attribute_string_value(&local_name!("href"))
49                        .map(|value| value.into());
50                }
51
52                // Step 2.2. Return null.
53                return None;
54            },
55            // Step 3. If command is "subscript" or "superscript":
56            CommandName::Subscript | CommandName::Superscript => {
57                // Step 3.1. If element is a sup, return "superscript".
58                if matches!(*self.local_name(), local_name!("sup")) {
59                    return Some("superscript".into());
60                }
61                // Step 3.2. If element is a sub, return "subscript".
62                if matches!(*self.local_name(), local_name!("sub")) {
63                    return Some("subscript".into());
64                }
65                // Step 3.3. Return null.
66                return None;
67            },
68            CommandName::Strikethrough => {
69                // Step 4. If command is "strikethrough", and element has a style attribute set, and that attribute sets "text-decoration":
70                if let Some(value) = CssPropertyName::TextDecorationLine.value_set_for_style(self) {
71                    // Step 4.1. If element's style attribute sets "text-decoration" to a value containing "line-through", return "line-through".
72                    // Step 4.2. Return null.
73                    return value
74                        .contains("line-through")
75                        .then_some("line-through".into());
76                }
77                // Step 5. If command is "strikethrough" and element is an s or strike element, return "line-through".
78                if matches!(*self.local_name(), local_name!("s") | local_name!("strike")) {
79                    return Some("line-through".into());
80                }
81            },
82            CommandName::Underline => {
83                // Step 6. If command is "underline", and element has a style attribute set, and that attribute sets "text-decoration":
84                if let Some(value) = CssPropertyName::TextDecorationLine.value_set_for_style(self) {
85                    // Step 6.1. If element's style attribute sets "text-decoration" to a value containing "underline", return "underline".
86                    // Step 6.2. Return null.
87                    return value.contains("underline").then_some("underline".into());
88                }
89                // Step 7. If command is "underline" and element is a u element, return "underline".
90                if *self.local_name() == local_name!("u") {
91                    return Some("underline".into());
92                }
93            },
94            _ => {},
95        };
96        // Step 8. Let property be the relevant CSS property for command.
97        // Step 9. If property is null, return null.
98        let property = command.relevant_css_property()?;
99        // Step 10. If element has a style attribute set, and that attribute has the effect of setting property,
100        // return the value that it sets property to.
101        if let Some(value) = property.value_set_for_style(self) {
102            return Some(value);
103        }
104        // Step 11. If element is a font element that has an attribute whose effect is to create a presentational hint for property,
105        // return the value that the hint sets property to. (For a size of 7, this will be the non-CSS value "xxx-large".)
106        if self.is::<HTMLFontElement>() &&
107            let Some(font_size) = self
108                .with_attribute(&ns!(), &local_name!("size"), |attribute| {
109                    if let AttrValue::UInt(_, value) = *attribute.value() {
110                        Some(value)
111                    } else {
112                        None
113                    }
114                })
115                .flatten()
116        {
117            return Some(font_size_to_css_font(&font_size).into());
118        }
119
120        // Step 12. If element is in the following list, and property is equal to the CSS property name listed for it,
121        // return the string listed for it.
122        let element_name = self.local_name();
123        match property {
124            CssPropertyName::FontWeight
125                if element_name == &local_name!("b") || element_name == &local_name!("strong") =>
126            {
127                Some("bold".into())
128            },
129            CssPropertyName::FontStyle
130                if element_name == &local_name!("i") || element_name == &local_name!("em") =>
131            {
132                Some("italic".into())
133            },
134            // Step 13. Return null.
135            _ => None,
136        }
137    }
138
139    /// <https://w3c.github.io/editing/docs/execCommand/#modifiable-element>
140    pub(crate) fn is_modifiable_element(&self) -> bool {
141        let attrs = self.attrs().borrow();
142        let mut attrs = attrs.iter();
143        let type_id = self.upcast::<Node>().type_id();
144
145        // > A modifiable element is a b, em, i, s, span, strike, strong, sub, sup, or u element
146        // > with no attributes except possibly style;
147        if matches!(
148            type_id,
149            NodeTypeId::Element(ElementTypeId::HTMLElement(
150                HTMLElementTypeId::HTMLSpanElement,
151            ))
152        ) || matches!(
153            *self.local_name(),
154            local_name!("b") |
155                local_name!("em") |
156                local_name!("i") |
157                local_name!("s") |
158                local_name!("strike") |
159                local_name!("strong") |
160                local_name!("sub") |
161                local_name!("sup") |
162                local_name!("u")
163        ) {
164            return attrs.all(|attr| attr.local_name() == &local_name!("style"));
165        }
166
167        // > or a font element with no attributes except possibly style, color, face, and/or size;
168        if matches!(
169            type_id,
170            NodeTypeId::Element(ElementTypeId::HTMLElement(
171                HTMLElementTypeId::HTMLFontElement,
172            ))
173        ) {
174            return attrs.all(|attr| {
175                matches!(
176                    *attr.local_name(),
177                    local_name!("style") |
178                        local_name!("color") |
179                        local_name!("face") |
180                        local_name!("size")
181                )
182            });
183        }
184
185        // > or an a element with no attributes except possibly style and/or href.
186        if matches!(
187            type_id,
188            NodeTypeId::Element(ElementTypeId::HTMLElement(
189                HTMLElementTypeId::HTMLAnchorElement,
190            ))
191        ) {
192            return attrs.all(|attr| {
193                matches!(
194                    *attr.local_name(),
195                    local_name!("style") | local_name!("href")
196                )
197            });
198        }
199
200        false
201    }
202
203    pub(crate) fn has_empty_style_attribute(&self) -> bool {
204        let style_attribute = self.style_attribute().borrow();
205        style_attribute.as_ref().is_some_and(|declarations| {
206            let document = self.owner_document();
207            let shared_lock = document.style_shared_author_lock();
208            let read_lock = shared_lock.read();
209            let style = declarations.read_with(&read_lock);
210
211            style.is_empty()
212        })
213    }
214
215    /// <https://w3c.github.io/editing/docs/execCommand/#non-list-single-line-container>
216    pub(crate) fn is_non_list_single_line_container(&self) -> bool {
217        // > A non-list single-line container is an HTML element with local name
218        // > "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "listing", "p", "pre", or "xmp".
219        matches!(
220            *self.local_name(),
221            local_name!("address") |
222                local_name!("div") |
223                local_name!("h1") |
224                local_name!("h2") |
225                local_name!("h3") |
226                local_name!("h4") |
227                local_name!("h5") |
228                local_name!("h6") |
229                local_name!("listing") |
230                local_name!("p") |
231                local_name!("pre") |
232                local_name!("xmp")
233        )
234    }
235
236    /// <https://w3c.github.io/editing/docs/execCommand/#simple-modifiable-element>
237    pub(crate) fn is_simple_modifiable_element(&self) -> bool {
238        let attrs = self.attrs().borrow();
239        let attr_count = attrs.len();
240        let type_id = self.upcast::<Node>().type_id();
241
242        if matches!(
243            type_id,
244            NodeTypeId::Element(ElementTypeId::HTMLElement(
245                HTMLElementTypeId::HTMLAnchorElement,
246            )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
247                HTMLElementTypeId::HTMLFontElement,
248            )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
249                HTMLElementTypeId::HTMLSpanElement,
250            ))
251        ) || matches!(
252            *self.local_name(),
253            local_name!("b") |
254                local_name!("em") |
255                local_name!("i") |
256                local_name!("s") |
257                local_name!("strike") |
258                local_name!("strong") |
259                local_name!("sub") |
260                local_name!("sup") |
261                local_name!("u")
262        ) {
263            // > It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u element with no attributes.
264            if attr_count == 0 {
265                return true;
266            }
267
268            // > It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u element
269            // > with exactly one attribute, which is style,
270            // > which sets no CSS properties (including invalid or unrecognized properties).
271            if attr_count == 1 &&
272                attrs.first().expect("Size is 1").local_name() == &local_name!("style") &&
273                self.has_empty_style_attribute()
274            {
275                return true;
276            }
277        }
278
279        if attr_count != 1 {
280            return false;
281        }
282
283        let first_attr = attrs.first().expect("Size is 1");
284        let only_attribute = first_attr.local_name();
285
286        // > It is an a element with exactly one attribute, which is href.
287        if matches!(
288            type_id,
289            NodeTypeId::Element(ElementTypeId::HTMLElement(
290                HTMLElementTypeId::HTMLAnchorElement,
291            ))
292        ) {
293            return only_attribute == &local_name!("href");
294        }
295
296        // > It is a font element with exactly one attribute, which is either color, face, or size.
297        if matches!(
298            type_id,
299            NodeTypeId::Element(ElementTypeId::HTMLElement(
300                HTMLElementTypeId::HTMLFontElement,
301            ))
302        ) {
303            return only_attribute == &local_name!("color") ||
304                only_attribute == &local_name!("face") ||
305                only_attribute == &local_name!("size");
306        }
307
308        if only_attribute != &local_name!("style") {
309            return false;
310        }
311        let style_attribute = self.style_attribute().borrow();
312        let Some(declarations) = style_attribute.as_ref() else {
313            return false;
314        };
315        let document = self.owner_document();
316        let shared_lock = document.style_shared_author_lock();
317        let read_lock = shared_lock.read();
318        let style = declarations.read_with(&read_lock);
319
320        // > It is a b or strong element with exactly one attribute, which is style,
321        // > and the style attribute sets exactly one CSS property
322        // > (including invalid or unrecognized properties), which is "font-weight".
323        if matches!(*self.local_name(), local_name!("b") | local_name!("strong")) {
324            return style.len() == 1 &&
325                style.contains(PropertyDeclarationId::Longhand(LonghandId::FontWeight));
326        }
327
328        // > It is an i or em element with exactly one attribute, which is style,
329        // > and the style attribute sets exactly one CSS property (including invalid or unrecognized properties),
330        // > which is "font-style".
331        if matches!(*self.local_name(), local_name!("i") | local_name!("em")) {
332            return style.len() == 1 &&
333                style.contains(PropertyDeclarationId::Longhand(LonghandId::FontStyle));
334        }
335
336        let a_font_or_span = matches!(
337            type_id,
338            NodeTypeId::Element(ElementTypeId::HTMLElement(
339                HTMLElementTypeId::HTMLAnchorElement,
340            )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
341                HTMLElementTypeId::HTMLFontElement,
342            )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
343                HTMLElementTypeId::HTMLSpanElement,
344            ))
345        );
346        let s_strike_or_u = matches!(
347            *self.local_name(),
348            local_name!("s") | local_name!("strike") | local_name!("u")
349        );
350        if a_font_or_span || s_strike_or_u {
351            // Note that the shorthand "text-decoration" expands to N longhands (4 at time of writing).
352            // Hence we check if the length is equal to N here, instead of 1.
353            if style.len() == ShorthandId::TextDecoration.longhands().count() &&
354                style
355                    .shorthand_to_css(ShorthandId::TextDecoration, &mut String::new())
356                    .is_ok()
357            {
358                if let Some((text_decoration, _)) = style.get(PropertyDeclarationId::Longhand(
359                    LonghandId::TextDecorationLine,
360                )) {
361                    // > It is an a, font, s, span, strike, or u element with exactly one attribute,
362                    // > which is style, and the style attribute sets exactly one CSS property
363                    // > (including invalid or unrecognized properties), which is "text-decoration",
364                    // > which is set to "line-through" or "underline" or "overline" or "none".
365                    return matches!(
366                        text_decoration,
367                        PropertyDeclaration::TextDecorationLine(
368                            TextDecorationLine::LINE_THROUGH |
369                                TextDecorationLine::UNDERLINE |
370                                TextDecorationLine::OVERLINE |
371                                TextDecorationLine::NONE
372                        )
373                    );
374                }
375            } else if a_font_or_span {
376                // > It is an a, font, or span element with exactly one attribute, which is style,
377                // > and the style attribute sets exactly one CSS property (including invalid or unrecognized properties),
378                // > and that property is not "text-decoration".
379                return style.len() == 1;
380            }
381        }
382
383        false
384    }
385
386    /// <https://w3c.github.io/editing/docs/execCommand/#set-the-tag-name>
387    pub(crate) fn set_the_tag_name(&self, cx: &mut JSContext, new_name: &str) -> DomRoot<Node> {
388        // Step 1. If element is an HTML element with local name equal to new name, return element.
389        if self.local_name() == &LocalName::from(new_name) {
390            return DomRoot::upcast(DomRoot::from_ref(self));
391        }
392        // Step 2. If element's parent is null, return element.
393        let node = self.upcast::<Node>();
394        let Some(parent) = node.GetParentNode() else {
395            return DomRoot::upcast(DomRoot::from_ref(self));
396        };
397        // Step 3. Let replacement element be the result of calling createElement(new name) on the ownerDocument of element.
398        let document = node.owner_document();
399        let replacement = document.create_element(cx, new_name);
400        let replacement_node = replacement.upcast::<Node>();
401        // Step 4. Insert replacement element into element's parent immediately before element.
402        if parent
403            .InsertBefore(cx, replacement_node, Some(node))
404            .is_err()
405        {
406            unreachable!("Must always be able to insert");
407        }
408        // Step 5. Copy all attributes of element to replacement element, in order.
409        self.copy_all_attributes_to_other_element(cx, &replacement);
410        // Step 6. While element has children, append the first child of element as the last child of replacement element, preserving ranges.
411        for child in node.children() {
412            move_preserving_ranges(cx, &child, |cx| replacement_node.AppendChild(cx, &child));
413        }
414        // Step 7. Remove element from its parent.
415        node.remove_self(cx);
416        // Step 8. Return replacement element.
417        DomRoot::upcast(replacement)
418    }
419}