1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use js::context::{JSContext, NoGC};
use script_bindings::inheritance::Castable;
use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
use crate::dom::bindings::root::DomRoot;
use crate::dom::document::Document;
use crate::dom::execcommand::basecommand::{
BoolOrOptionalString, CommandName, RecordedStateOfCommand,
};
use crate::dom::execcommand::commands::fontsize::legacy_font_size_for;
use crate::dom::html::htmllielement::HTMLLIElement;
use crate::dom::iterators::ShadowIncluding;
use crate::dom::node::Node;
use crate::dom::range::Range;
use crate::dom::selection::Selection;
use crate::dom::text::Text;
impl Range {
/// <https://w3c.github.io/editing/docs/execCommand/#effectively-contained>
fn is_effectively_contained_node(&self, node: &Node) -> bool {
// > A node node is effectively contained in a range range if range is not collapsed,
if self.collapsed() {
return false;
}
// > and at least one of the following holds:
// > node is range's start node, it is a Text node, and its length is different from range's start offset.
let start_container = self.start_container();
if *start_container == *node && node.is::<Text>() && node.len() != self.start_offset() {
return true;
}
// > node is range's end node, it is a Text node, and range's end offset is not 0.
let end_container = self.end_container();
if *end_container == *node && node.is::<Text>() && self.end_offset() != 0 {
return true;
}
// > node is contained in range.
if self.contains(node) {
return true;
}
// > node has at least one child; and all its children are effectively contained in range;
node.children_count() > 0 && node.children().all(|child| self.is_effectively_contained_node(&child))
// > and either range's start node is not a descendant of node or is not a Text node or range's start offset is zero;
&& (!node.is_ancestor_of(&start_container) || !start_container.is::<Text>() || self.start_offset() == 0)
// > and either range's end node is not a descendant of node or is not a Text node or range's end offset is its end node's length.
&& (!node.is_ancestor_of(&end_container) || !end_container.is::<Text>() || self.end_offset() == end_container.len())
}
/// The definition of "effectively contained" contains the recursion of
/// ancestors of a single fully selected text node. That is to say, that
/// if the selection is a fully selected text node <div>[foobar]</div>,
/// then the div would also be considered effectively contained. As such,
/// we can't use the common ancestor container, since that would be the
/// text node only.
///
/// Instead, we traverse all the way up to the editing host, which we know
/// is sufficient to know to include all contained nodes. That way, we also
/// would traverse ancestors such as the parent div.
fn ancestor_for_effectively_contained(&self) -> DomRoot<Node> {
let ancestor_container = self.CommonAncestorContainer();
ancestor_container
.editing_host_of()
.unwrap_or(ancestor_container)
}
pub(crate) fn first_formattable_contained_node(&self, no_gc: &NoGC) -> Option<DomRoot<Node>> {
if self.collapsed() {
return None;
}
self.ancestor_for_effectively_contained()
.traverse_preorder(ShadowIncluding::No)
.find(|child| child.is_formattable(no_gc) && self.is_effectively_contained_node(child))
}
pub(crate) fn for_each_effectively_contained_child<Callback: FnMut(&Node)>(
&self,
mut callback: Callback,
) {
if self.collapsed() {
return;
}
// Make sure to keep track of the tree nodes before, since `callback` might modify
// the underyling tree and then the iterator would prematurely stop.
let children = self
.ancestor_for_effectively_contained()
.traverse_preorder(ShadowIncluding::No)
.collect::<Vec<DomRoot<Node>>>();
for child in children {
if self.is_effectively_contained_node(&child) {
callback(&child);
}
}
}
/// <https://w3c.github.io/editing/docs/execCommand/#block-extend>
pub(crate) fn block_extend(&self, cx: &mut JSContext, document: &Document) -> DomRoot<Range> {
// Step 1. Let start node, start offset, end node,
// and end offset be the start and end nodes and offsets of range.
let mut start_node = self.start_container();
let mut start_offset = self.start_offset();
let mut end_node = self.end_container();
let mut end_offset = self.end_offset();
// Step 2. If some inclusive ancestor of start node is an li,
// set start offset to the index of the last such li in tree order, and set start node to that li's parent.
if let Some(li_ancestor) = start_node
.inclusive_ancestors_unrooted(cx.no_gc(), ShadowIncluding::No)
.find(|ancestor| ancestor.is::<HTMLLIElement>())
{
start_offset = li_ancestor.index();
start_node = li_ancestor
.GetParentNode()
.expect("Must always have a parent");
}
// Step 3. If (start node, start offset) is not a block start point, repeat the following steps:
if !start_node.is_block_start_point(cx.no_gc(), start_offset as usize) {
loop {
// Step 3.1. If start offset is zero, set it to start node's index, then set start node to its parent.
if start_offset == 0 {
start_offset = start_node.index();
start_node = start_node
.GetParentNode()
.expect("Must always have a parent");
} else {
// Step 3.2. Otherwise, subtract one from start offset.
start_offset -= 1;
}
// Step 3.3. If (start node, start offset) is a block boundary point, break from this loop.
if start_node.is_block_boundary_point(cx.no_gc(), start_offset) {
break;
}
}
}
// Step 4. While start offset is zero and start node's parent is not null,
// set start offset to start node's index, then set start node to its parent.
while start_offset == 0 &&
let Some(parent) = start_node.GetParentNode()
{
start_offset = start_node.index();
start_node = parent;
}
// Step 5. If some inclusive ancestor of end node is an li,
// set end offset to one plus the index of the last such li in tree order,
// and set end node to that li's parent.
if let Some(li_ancestor) = end_node
.inclusive_ancestors_unrooted(cx.no_gc(), ShadowIncluding::No)
.find(|ancestor| ancestor.is::<HTMLLIElement>())
{
end_offset = 1 + li_ancestor.index();
end_node = li_ancestor
.GetParentNode()
.expect("Must always have a parent");
}
// Step 6. If (end node, end offset) is not a block end point, repeat the following steps:
if !end_node.is_block_end_point(end_offset, cx.no_gc()) {
loop {
// Step 6.1. If end offset is end node's length, set it to one plus end node's index, then set end node to its parent.
if end_offset == end_node.len() {
end_offset = 1 + end_node.index();
end_node = end_node.GetParentNode().expect("Must always have a parent");
} else {
// Step 6.2. Otherwise, add one to end offset.
end_offset += 1;
}
// Step 6.3. If (end node, end offset) is a block boundary point, break from this loop.
if end_node.is_block_boundary_point(cx.no_gc(), end_offset) {
break;
}
}
}
// Step 7. While end offset is end node's length and end node's parent is not null,
// set end offset to one plus end node's index, then set end node to its parent.
while end_offset == end_node.len() &&
let Some(parent) = end_node.GetParentNode()
{
end_offset = 1 + end_node.index();
end_node = parent;
}
// Step 8. Let new range be a new range whose start and end nodes and offsets are start node,
// start offset, end node, and end offset.
let new_range = document.CreateRange(cx);
new_range.set_start(&start_node, start_offset);
new_range.set_end(&end_node, end_offset);
// Step 9. Return new range.
new_range
}
/// <https://w3c.github.io/editing/docs/execCommand/#record-current-states-and-values>
pub(crate) fn record_current_states_and_values(
&self,
cx: &mut JSContext,
) -> Vec<RecordedStateOfCommand> {
// Step 1. Let overrides be a list of (string, string or boolean) ordered pairs, initially empty.
//
// We return the vec in one go for the relevant values
// Step 2. Let node be the first formattable node effectively contained in the active range,
// or null if there is none.
let Some(node) = self.first_formattable_contained_node(cx.no_gc()) else {
// Step 3. If node is null, return overrides.
return vec![];
};
// Step 8. Return overrides.
let document = node.owner_doc();
vec![
// Step 4. Add ("createLink", node's effective command value for "createLink") to overrides.
RecordedStateOfCommand::for_command_node(CommandName::CreateLink, &node),
// Step 5. For each command in the list
// "bold", "italic", "strikethrough", "subscript", "superscript", "underline", in order:
// if node's effective command value for command is one of its inline command activated values,
// add (command, true) to overrides, and otherwise add (command, false) to overrides.
RecordedStateOfCommand::for_command_node_with_inline_activated_values(
CommandName::Bold,
&node,
),
RecordedStateOfCommand::for_command_node_with_inline_activated_values(
CommandName::Italic,
&node,
),
RecordedStateOfCommand::for_command_node_with_inline_activated_values(
CommandName::Strikethrough,
&node,
),
RecordedStateOfCommand::for_command_node_with_inline_activated_values(
CommandName::Subscript,
&node,
),
RecordedStateOfCommand::for_command_node_with_inline_activated_values(
CommandName::Superscript,
&node,
),
RecordedStateOfCommand::for_command_node_with_inline_activated_values(
CommandName::Underline,
&node,
),
// Step 6. For each command in the list "fontName", "foreColor", "hiliteColor", in order:
// add (command, command's value) to overrides.
RecordedStateOfCommand::for_command_node_with_value(
cx,
CommandName::FontName,
&document,
),
RecordedStateOfCommand::for_command_node_with_value(
cx,
CommandName::ForeColor,
&document,
),
RecordedStateOfCommand::for_command_node_with_value(
cx,
CommandName::HiliteColor,
&document,
),
// Step 7. Add ("fontSize", node's effective command value for "fontSize") to overrides.
RecordedStateOfCommand::for_command_node(CommandName::FontSize, &node),
]
}
/// <https://w3c.github.io/editing/docs/execCommand/#restore-states-and-values>
pub(crate) fn restore_states_and_values(
&self,
cx: &mut JSContext,
selection: &Selection,
context_object: &Document,
overrides: Vec<RecordedStateOfCommand>,
) {
// Step 1. Let node be the first formattable node effectively contained in the active range,
// or null if there is none.
let mut first_formattable_contained_node =
self.first_formattable_contained_node(cx.no_gc());
for override_state in overrides {
// Step 2. If node is not null, then for each (command, override) pair in overrides, in order:
if let Some(ref node) = first_formattable_contained_node {
match override_state.value {
// Step 2.1. If override is a boolean, and queryCommandState(command)
// returns something different from override, take the action for command,
// with value equal to the empty string.
BoolOrOptionalString::Bool(bool_)
if override_state
.command
.current_state(cx, context_object)
.is_some_and(|value| value != bool_) =>
{
override_state
.command
.execute(cx, context_object, selection, "".into());
},
BoolOrOptionalString::OptionalString(optional_string) => {
match override_state.command {
// Step 2.3. Otherwise, if override is a string; and command is "createLink";
// and either there is a value override for "createLink" that is not equal to override,
// or there is no value override for "createLink" and node's effective command value
// for "createLink" is not equal to override: take the action for "createLink", with value equal to override.
CommandName::CreateLink => {
let value_override =
context_object.value_override(&CommandName::CreateLink);
if value_override != optional_string {
CommandName::CreateLink.execute(
cx,
context_object,
selection,
optional_string.unwrap_or_default(),
);
}
},
// Step 2.4. Otherwise, if override is a string; and command is "fontSize";
// and either there is a value override for "fontSize" that is not equal to override,
// or there is no value override for "fontSize" and node's effective command value for "fontSize"
// is not loosely equivalent to override:
CommandName::FontSize => {
let value_override =
context_object.value_override(&CommandName::FontSize);
if value_override != optional_string ||
(value_override.is_none() &&
!CommandName::FontSize.are_loosely_equivalent_values(
node.effective_command_value(&CommandName::FontSize)
.as_ref(),
optional_string.as_ref(),
))
{
// Step 2.5. Convert override to an integer number of pixels,
// and set override to the legacy font size for the result.
let pixels = optional_string
.and_then(|value| value.parse::<i32>().ok())
.map(|value| {
legacy_font_size_for(value as f32, context_object)
})
.unwrap_or("7".into());
// Step 2.6. Take the action for "fontSize", with value equal to override.
CommandName::FontSize.execute(
cx,
context_object,
selection,
pixels,
);
}
},
// Step 2.2. Otherwise, if override is a string, and command is neither "createLink" nor "fontSize",
// and queryCommandValue(command) returns something not equivalent to override,
// take the action for command, with value equal to override.
command
if command.current_value(cx, context_object) != optional_string =>
{
command.execute(
cx,
context_object,
selection,
optional_string.unwrap_or_default(),
);
},
// Step 2.5. Otherwise, continue this loop from the beginning.
_ => {
continue;
},
}
},
// Step 2.5. Otherwise, continue this loop from the beginning.
_ => {
continue;
},
}
// Step 2.6. Set node to the first formattable node effectively contained in the active range, if there is one.
first_formattable_contained_node =
self.first_formattable_contained_node(cx.no_gc());
} else {
// Step 3. Otherwise, for each (command, override) pair in overrides, in order:
// Step 3.1. If override is a boolean, set the state override for command to override.
match override_state.value {
BoolOrOptionalString::Bool(bool_) => {
context_object.set_state_override(override_state.command, Some(bool_))
},
// Step 3.2. If override is a string, set the value override for command to override.
BoolOrOptionalString::OptionalString(optional_string) => {
context_object.set_value_override(override_state.command, optional_string)
},
}
}
}
}
}