Skip to main content

script/dom/execcommand/commands/
removeformat.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::local_name;
6use js::context::JSContext;
7use script_bindings::codegen::GenericBindings::RangeBinding::RangeMethods;
8use script_bindings::inheritance::Castable;
9
10use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
11use crate::dom::bindings::codegen::Bindings::TextBinding::TextMethods;
12use crate::dom::bindings::root::DomRoot;
13use crate::dom::document::Document;
14use crate::dom::element::Element;
15use crate::dom::execcommand::basecommand::CommandName;
16use crate::dom::execcommand::contenteditable::node::{move_preserving_ranges, split_the_parent};
17use crate::dom::html::htmlelement::HTMLElement;
18use crate::dom::selection::Selection;
19use crate::dom::text::Text;
20
21/// <https://w3c.github.io/editing/docs/execCommand/#removeformat-candidate>
22fn is_remove_format_candidate(element: &Element) -> bool {
23    // > A removeFormat candidate is an editable HTML element with local name
24    // > "abbr", "acronym", "b", "bdi", "bdo", "big", "blink", "cite", "code",
25    // > "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q", "s",
26    // > "samp", "small", "span", "strike", "strong", "sub", "sup", "tt", "u", or "var".
27    matches!(
28        *element.local_name(),
29        local_name!("abbr") |
30            local_name!("acronym") |
31            local_name!("b") |
32            local_name!("bdi") |
33            local_name!("bdo") |
34            local_name!("big") |
35            local_name!("blink") |
36            local_name!("cite") |
37            local_name!("code") |
38            local_name!("dfn") |
39            local_name!("em") |
40            local_name!("font") |
41            local_name!("i") |
42            local_name!("ins") |
43            local_name!("kbd") |
44            local_name!("mark") |
45            local_name!("nobr") |
46            local_name!("q") |
47            local_name!("s") |
48            local_name!("samp") |
49            local_name!("small") |
50            local_name!("span") |
51            local_name!("strike") |
52            local_name!("strong") |
53            local_name!("sub") |
54            local_name!("sup") |
55            local_name!("tt") |
56            local_name!("u") |
57            local_name!("var")
58    )
59}
60
61/// <https://w3c.github.io/editing/docs/execCommand/#the-removeformat-command>
62pub(crate) fn execute_removeformat_command(
63    cx: &mut JSContext,
64    document: &Document,
65    selection: &Selection,
66) -> bool {
67    // Step 1. Let elements to remove be a list of every removeFormat candidate effectively contained in the active range.
68    let active_range = selection
69        .active_range(cx)
70        .expect("Must always have an active range");
71    let mut elements = vec![];
72    active_range.for_each_effectively_contained_child(|node| {
73        if let Some(html_element) = node.downcast::<HTMLElement>() &&
74            is_remove_format_candidate(html_element.upcast())
75        {
76            elements.push(DomRoot::from_ref(node));
77        }
78    });
79    // Step 2. For each element in elements to remove:
80    for element in elements {
81        // Step 2.1. While element has children,
82        // insert the first child of element into the parent of element immediately before element, preserving ranges.
83        let parent_element = element.GetParentNode().expect("Must always have a parent");
84        for child in element.children() {
85            move_preserving_ranges(cx, &child, |cx| {
86                parent_element.InsertBefore(cx, &child, Some(&element))
87            });
88        }
89        // Step 2.2. Remove element from its parent.
90        element.remove_self(cx);
91    }
92    // Step 3. If the active range's start node is an editable Text node,
93    // and its start offset is neither zero nor its start node's length,
94    // call splitText() on the active range's start node, with argument equal
95    // to the active range's start offset. Then set the active range's start node
96    // to the result, and its start offset to zero.
97    let start_node = active_range.start_container();
98    let start_offset = active_range.start_offset();
99    if start_node.is_editable() &&
100        start_offset != 0 &&
101        start_offset != start_node.len() &&
102        let Some(start_text) = start_node.downcast::<Text>()
103    {
104        let Ok(start_text) = start_text.SplitText(cx, start_offset) else {
105            unreachable!("Must always be able to split");
106        };
107        let _ = active_range.SetStart(start_text.upcast(), 0);
108    }
109    // Step 4. If the active range's end node is an editable Text node,
110    // and its end offset is neither zero nor its end node's length,
111    // call splitText() on the active range's end node, with argument
112    // equal to the active range's end offset.
113    let end_node = active_range.end_container();
114    let end_offset = active_range.end_offset();
115    if end_node.is_editable() &&
116        end_offset != 0 &&
117        end_offset != end_node.len() &&
118        let Some(end_text) = end_node.downcast::<Text>() &&
119        end_text.SplitText(cx, end_offset).is_err()
120    {
121        unreachable!("Must always be able to split");
122    };
123    // Step 5. Let node list consist of all editable nodes effectively contained in the active range.
124    let mut node_list = vec![];
125    active_range.for_each_effectively_contained_child(|node| {
126        if node.is_editable() {
127            node_list.push(DomRoot::from_ref(node));
128        }
129    });
130    // Step 6. For each node in node list, while node's parent is a removeFormat
131    // candidate in the same editing host as node,
132    // split the parent of the one-node list consisting of node.
133    for node in node_list {
134        while let Some(parent) = node.GetParentElement() {
135            if !is_remove_format_candidate(&parent) {
136                break;
137            }
138            if !node.same_editing_host(parent.upcast()) {
139                break;
140            }
141            split_the_parent(cx, &[&node]);
142        }
143    }
144    // Step 7. For each of the entries in the following list,
145    // in the given order, set the selection's value to null, with command as given.
146    selection.set_the_selection_value(cx, None, CommandName::Subscript, document);
147    selection.set_the_selection_value(cx, None, CommandName::Bold, document);
148    selection.set_the_selection_value(cx, None, CommandName::FontName, document);
149    selection.set_the_selection_value(cx, None, CommandName::FontSize, document);
150    selection.set_the_selection_value(cx, None, CommandName::ForeColor, document);
151    selection.set_the_selection_value(cx, None, CommandName::HiliteColor, document);
152    selection.set_the_selection_value(cx, None, CommandName::Italic, document);
153    selection.set_the_selection_value(cx, None, CommandName::Strikethrough, document);
154    selection.set_the_selection_value(cx, None, CommandName::Underline, document);
155    // Step 8. Return true.
156    true
157}