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