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