/src/serenity/Userland/Libraries/LibWeb/DOM/Range.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2020, the SerenityOS developers. |
3 | | * Copyright (c) 2022, Luke Wilde <lukew@serenityos.org> |
4 | | * Copyright (c) 2022-2023, Andreas Kling <kling@serenityos.org> |
5 | | * |
6 | | * SPDX-License-Identifier: BSD-2-Clause |
7 | | */ |
8 | | |
9 | | #include <LibWeb/Bindings/Intrinsics.h> |
10 | | #include <LibWeb/Bindings/RangePrototype.h> |
11 | | #include <LibWeb/DOM/Comment.h> |
12 | | #include <LibWeb/DOM/Document.h> |
13 | | #include <LibWeb/DOM/DocumentFragment.h> |
14 | | #include <LibWeb/DOM/DocumentType.h> |
15 | | #include <LibWeb/DOM/ElementFactory.h> |
16 | | #include <LibWeb/DOM/Event.h> |
17 | | #include <LibWeb/DOM/Node.h> |
18 | | #include <LibWeb/DOM/ProcessingInstruction.h> |
19 | | #include <LibWeb/DOM/Range.h> |
20 | | #include <LibWeb/DOM/Text.h> |
21 | | #include <LibWeb/Geometry/DOMRect.h> |
22 | | #include <LibWeb/Geometry/DOMRectList.h> |
23 | | #include <LibWeb/HTML/HTMLHtmlElement.h> |
24 | | #include <LibWeb/HTML/Window.h> |
25 | | #include <LibWeb/Layout/Viewport.h> |
26 | | #include <LibWeb/Namespace.h> |
27 | | #include <LibWeb/Painting/InlinePaintable.h> |
28 | | #include <LibWeb/Painting/ViewportPaintable.h> |
29 | | |
30 | | namespace Web::DOM { |
31 | | |
32 | | JS_DEFINE_ALLOCATOR(Range); |
33 | | |
34 | | HashTable<Range*>& Range::live_ranges() |
35 | 0 | { |
36 | 0 | static HashTable<Range*> ranges; |
37 | 0 | return ranges; |
38 | 0 | } |
39 | | |
40 | | JS::NonnullGCPtr<Range> Range::create(HTML::Window& window) |
41 | 0 | { |
42 | 0 | return Range::create(window.associated_document()); |
43 | 0 | } |
44 | | |
45 | | JS::NonnullGCPtr<Range> Range::create(Document& document) |
46 | 0 | { |
47 | 0 | auto& realm = document.realm(); |
48 | 0 | return realm.heap().allocate<Range>(realm, document); |
49 | 0 | } |
50 | | |
51 | | JS::NonnullGCPtr<Range> Range::create(Node& start_container, WebIDL::UnsignedLong start_offset, Node& end_container, WebIDL::UnsignedLong end_offset) |
52 | 0 | { |
53 | 0 | auto& realm = start_container.realm(); |
54 | 0 | return realm.heap().allocate<Range>(realm, start_container, start_offset, end_container, end_offset); |
55 | 0 | } |
56 | | |
57 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<Range>> Range::construct_impl(JS::Realm& realm) |
58 | 0 | { |
59 | 0 | auto& window = verify_cast<HTML::Window>(realm.global_object()); |
60 | 0 | return Range::create(window); |
61 | 0 | } |
62 | | |
63 | | Range::Range(Document& document) |
64 | 0 | : Range(document, 0, document, 0) |
65 | 0 | { |
66 | 0 | } |
67 | | |
68 | | Range::Range(Node& start_container, WebIDL::UnsignedLong start_offset, Node& end_container, WebIDL::UnsignedLong end_offset) |
69 | 0 | : AbstractRange(start_container, start_offset, end_container, end_offset) |
70 | 0 | { |
71 | 0 | live_ranges().set(this); |
72 | 0 | } |
73 | | |
74 | | Range::~Range() |
75 | 0 | { |
76 | 0 | live_ranges().remove(this); |
77 | 0 | } |
78 | | |
79 | | void Range::initialize(JS::Realm& realm) |
80 | 0 | { |
81 | 0 | Base::initialize(realm); |
82 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(Range); |
83 | 0 | } |
84 | | |
85 | | void Range::visit_edges(Cell::Visitor& visitor) |
86 | 0 | { |
87 | 0 | Base::visit_edges(visitor); |
88 | 0 | visitor.visit(m_associated_selection); |
89 | 0 | } |
90 | | |
91 | | void Range::set_associated_selection(Badge<Selection::Selection>, JS::GCPtr<Selection::Selection> selection) |
92 | 0 | { |
93 | 0 | m_associated_selection = selection; |
94 | 0 | update_associated_selection(); |
95 | 0 | } |
96 | | |
97 | | void Range::update_associated_selection() |
98 | 0 | { |
99 | 0 | if (auto* viewport = m_start_container->document().paintable()) { |
100 | 0 | viewport->recompute_selection_states(*this); |
101 | 0 | viewport->update_selection(); |
102 | 0 | viewport->set_needs_display(); |
103 | 0 | } |
104 | |
|
105 | 0 | if (!m_associated_selection) |
106 | 0 | return; |
107 | | |
108 | | // https://w3c.github.io/selection-api/#selectionchange-event |
109 | | // When the selection is dissociated with its range, associated with a new range or the associated range's boundary |
110 | | // point is mutated either by the user or the content script, the user agent must queue a task on the user interaction |
111 | | // task source to fire an event named selectionchange, which does not bubble and is not cancelable, at the document |
112 | | // associated with the selection. |
113 | 0 | auto document = m_associated_selection->document(); |
114 | 0 | queue_global_task(HTML::Task::Source::UserInteraction, relevant_global_object(*document), JS::create_heap_function(document->heap(), [document] { |
115 | 0 | EventInit event_init; |
116 | 0 | event_init.bubbles = false; |
117 | 0 | event_init.cancelable = false; |
118 | 0 | auto event = DOM::Event::create(document->realm(), HTML::EventNames::selectionchange, event_init); |
119 | 0 | document->dispatch_event(event); |
120 | 0 | })); |
121 | 0 | } |
122 | | |
123 | | // https://dom.spec.whatwg.org/#concept-range-root |
124 | | Node& Range::root() |
125 | 0 | { |
126 | | // The root of a live range is the root of its start node. |
127 | 0 | return m_start_container->root(); |
128 | 0 | } |
129 | | |
130 | | Node const& Range::root() const |
131 | 0 | { |
132 | 0 | return m_start_container->root(); |
133 | 0 | } |
134 | | |
135 | | // https://dom.spec.whatwg.org/#concept-range-bp-position |
136 | | RelativeBoundaryPointPosition position_of_boundary_point_relative_to_other_boundary_point(Node const& node_a, u32 offset_a, Node const& node_b, u32 offset_b) |
137 | 0 | { |
138 | | // 1. Assert: nodeA and nodeB have the same root. |
139 | 0 | VERIFY(&node_a.root() == &node_b.root()); |
140 | | |
141 | | // 2. If nodeA is nodeB, then return equal if offsetA is offsetB, before if offsetA is less than offsetB, and after if offsetA is greater than offsetB. |
142 | 0 | if (&node_a == &node_b) { |
143 | 0 | if (offset_a == offset_b) |
144 | 0 | return RelativeBoundaryPointPosition::Equal; |
145 | | |
146 | 0 | if (offset_a < offset_b) |
147 | 0 | return RelativeBoundaryPointPosition::Before; |
148 | | |
149 | 0 | return RelativeBoundaryPointPosition::After; |
150 | 0 | } |
151 | | |
152 | | // 3. If nodeA is following nodeB, then if the position of (nodeB, offsetB) relative to (nodeA, offsetA) is before, return after, and if it is after, return before. |
153 | 0 | if (node_a.is_following(node_b)) { |
154 | 0 | auto relative_position = position_of_boundary_point_relative_to_other_boundary_point(node_b, offset_b, node_a, offset_a); |
155 | |
|
156 | 0 | if (relative_position == RelativeBoundaryPointPosition::Before) |
157 | 0 | return RelativeBoundaryPointPosition::After; |
158 | | |
159 | 0 | if (relative_position == RelativeBoundaryPointPosition::After) |
160 | 0 | return RelativeBoundaryPointPosition::Before; |
161 | 0 | } |
162 | | |
163 | | // 4. If nodeA is an ancestor of nodeB: |
164 | 0 | if (node_a.is_ancestor_of(node_b)) { |
165 | | // 1. Let child be nodeB. |
166 | 0 | JS::NonnullGCPtr<Node const> child = node_b; |
167 | | |
168 | | // 2. While child is not a child of nodeA, set child to its parent. |
169 | 0 | while (!node_a.is_parent_of(child)) { |
170 | 0 | auto* parent = child->parent(); |
171 | 0 | VERIFY(parent); |
172 | 0 | child = *parent; |
173 | 0 | } |
174 | | |
175 | | // 3. If child’s index is less than offsetA, then return after. |
176 | 0 | if (child->index() < offset_a) |
177 | 0 | return RelativeBoundaryPointPosition::After; |
178 | 0 | } |
179 | | |
180 | | // 5. Return before. |
181 | 0 | return RelativeBoundaryPointPosition::Before; |
182 | 0 | } |
183 | | |
184 | | WebIDL::ExceptionOr<void> Range::set_start_or_end(Node& node, u32 offset, StartOrEnd start_or_end) |
185 | 0 | { |
186 | | // To set the start or end of a range to a boundary point (node, offset), run these steps: |
187 | | |
188 | | // 1. If node is a doctype, then throw an "InvalidNodeTypeError" DOMException. |
189 | 0 | if (is<DocumentType>(node)) |
190 | 0 | return WebIDL::InvalidNodeTypeError::create(realm(), "Node cannot be a DocumentType."_string); |
191 | | |
192 | | // 2. If offset is greater than node’s length, then throw an "IndexSizeError" DOMException. |
193 | 0 | if (offset > node.length()) |
194 | 0 | return WebIDL::IndexSizeError::create(realm(), MUST(String::formatted("Node does not contain a child at offset {}", offset))); |
195 | | |
196 | | // 3. Let bp be the boundary point (node, offset). |
197 | | |
198 | 0 | if (start_or_end == StartOrEnd::Start) { |
199 | | // -> If these steps were invoked as "set the start" |
200 | | |
201 | | // 1. If range’s root is not equal to node’s root, or if bp is after the range’s end, set range’s end to bp. |
202 | 0 | if (&root() != &node.root() || position_of_boundary_point_relative_to_other_boundary_point(node, offset, m_end_container, m_end_offset) == RelativeBoundaryPointPosition::After) { |
203 | 0 | m_end_container = node; |
204 | 0 | m_end_offset = offset; |
205 | 0 | } |
206 | | |
207 | | // 2. Set range’s start to bp. |
208 | 0 | m_start_container = node; |
209 | 0 | m_start_offset = offset; |
210 | 0 | } else { |
211 | | // -> If these steps were invoked as "set the end" |
212 | 0 | VERIFY(start_or_end == StartOrEnd::End); |
213 | | |
214 | | // 1. If range’s root is not equal to node’s root, or if bp is before the range’s start, set range’s start to bp. |
215 | 0 | if (&root() != &node.root() || position_of_boundary_point_relative_to_other_boundary_point(node, offset, m_start_container, m_start_offset) == RelativeBoundaryPointPosition::Before) { |
216 | 0 | m_start_container = node; |
217 | 0 | m_start_offset = offset; |
218 | 0 | } |
219 | | |
220 | | // 2. Set range’s end to bp. |
221 | 0 | m_end_container = node; |
222 | 0 | m_end_offset = offset; |
223 | 0 | } |
224 | | |
225 | 0 | update_associated_selection(); |
226 | 0 | return {}; |
227 | 0 | } |
228 | | |
229 | | // https://dom.spec.whatwg.org/#concept-range-bp-set |
230 | | WebIDL::ExceptionOr<void> Range::set_start(Node& node, WebIDL::UnsignedLong offset) |
231 | 0 | { |
232 | | // The setStart(node, offset) method steps are to set the start of this to boundary point (node, offset). |
233 | 0 | return set_start_or_end(node, offset, StartOrEnd::Start); |
234 | 0 | } |
235 | | |
236 | | WebIDL::ExceptionOr<void> Range::set_end(Node& node, WebIDL::UnsignedLong offset) |
237 | 0 | { |
238 | | // The setEnd(node, offset) method steps are to set the end of this to boundary point (node, offset). |
239 | 0 | return set_start_or_end(node, offset, StartOrEnd::End); |
240 | 0 | } |
241 | | |
242 | | // https://dom.spec.whatwg.org/#dom-range-setstartbefore |
243 | | WebIDL::ExceptionOr<void> Range::set_start_before(Node& node) |
244 | 0 | { |
245 | | // 1. Let parent be node’s parent. |
246 | 0 | auto* parent = node.parent(); |
247 | | |
248 | | // 2. If parent is null, then throw an "InvalidNodeTypeError" DOMException. |
249 | 0 | if (!parent) |
250 | 0 | return WebIDL::InvalidNodeTypeError::create(realm(), "Given node has no parent."_string); |
251 | | |
252 | | // 3. Set the start of this to boundary point (parent, node’s index). |
253 | 0 | return set_start_or_end(*parent, node.index(), StartOrEnd::Start); |
254 | 0 | } |
255 | | |
256 | | // https://dom.spec.whatwg.org/#dom-range-setstartafter |
257 | | WebIDL::ExceptionOr<void> Range::set_start_after(Node& node) |
258 | 0 | { |
259 | | // 1. Let parent be node’s parent. |
260 | 0 | auto* parent = node.parent(); |
261 | | |
262 | | // 2. If parent is null, then throw an "InvalidNodeTypeError" DOMException. |
263 | 0 | if (!parent) |
264 | 0 | return WebIDL::InvalidNodeTypeError::create(realm(), "Given node has no parent."_string); |
265 | | |
266 | | // 3. Set the start of this to boundary point (parent, node’s index plus 1). |
267 | 0 | return set_start_or_end(*parent, node.index() + 1, StartOrEnd::Start); |
268 | 0 | } |
269 | | |
270 | | // https://dom.spec.whatwg.org/#dom-range-setendbefore |
271 | | WebIDL::ExceptionOr<void> Range::set_end_before(Node& node) |
272 | 0 | { |
273 | | // 1. Let parent be node’s parent. |
274 | 0 | auto* parent = node.parent(); |
275 | | |
276 | | // 2. If parent is null, then throw an "InvalidNodeTypeError" DOMException. |
277 | 0 | if (!parent) |
278 | 0 | return WebIDL::InvalidNodeTypeError::create(realm(), "Given node has no parent."_string); |
279 | | |
280 | | // 3. Set the end of this to boundary point (parent, node’s index). |
281 | 0 | return set_start_or_end(*parent, node.index(), StartOrEnd::End); |
282 | 0 | } |
283 | | |
284 | | // https://dom.spec.whatwg.org/#dom-range-setendafter |
285 | | WebIDL::ExceptionOr<void> Range::set_end_after(Node& node) |
286 | 0 | { |
287 | | // 1. Let parent be node’s parent. |
288 | 0 | auto* parent = node.parent(); |
289 | | |
290 | | // 2. If parent is null, then throw an "InvalidNodeTypeError" DOMException. |
291 | 0 | if (!parent) |
292 | 0 | return WebIDL::InvalidNodeTypeError::create(realm(), "Given node has no parent."_string); |
293 | | |
294 | | // 3. Set the end of this to boundary point (parent, node’s index plus 1). |
295 | 0 | return set_start_or_end(*parent, node.index() + 1, StartOrEnd::End); |
296 | 0 | } |
297 | | |
298 | | // https://dom.spec.whatwg.org/#dom-range-compareboundarypoints |
299 | | WebIDL::ExceptionOr<WebIDL::Short> Range::compare_boundary_points(WebIDL::UnsignedShort how, Range const& source_range) const |
300 | 0 | { |
301 | | // 1. If how is not one of |
302 | | // - START_TO_START, |
303 | | // - START_TO_END, |
304 | | // - END_TO_END, and |
305 | | // - END_TO_START, |
306 | | // then throw a "NotSupportedError" DOMException. |
307 | 0 | if (how != HowToCompareBoundaryPoints::START_TO_START && how != HowToCompareBoundaryPoints::START_TO_END && how != HowToCompareBoundaryPoints::END_TO_END && how != HowToCompareBoundaryPoints::END_TO_START) |
308 | 0 | return WebIDL::NotSupportedError::create(realm(), MUST(String::formatted("Expected 'how' to be one of START_TO_START (0), START_TO_END (1), END_TO_END (2) or END_TO_START (3), got {}", how))); |
309 | | |
310 | | // 2. If this’s root is not the same as sourceRange’s root, then throw a "WrongDocumentError" DOMException. |
311 | 0 | if (&root() != &source_range.root()) |
312 | 0 | return WebIDL::WrongDocumentError::create(realm(), "This range is not in the same tree as the source range."_string); |
313 | | |
314 | 0 | JS::GCPtr<Node> this_point_node; |
315 | 0 | u32 this_point_offset = 0; |
316 | |
|
317 | 0 | JS::GCPtr<Node> other_point_node; |
318 | 0 | u32 other_point_offset = 0; |
319 | | |
320 | | // 3. If how is: |
321 | 0 | switch (how) { |
322 | 0 | case HowToCompareBoundaryPoints::START_TO_START: |
323 | | // -> START_TO_START: |
324 | | // Let this point be this’s start. Let other point be sourceRange’s start. |
325 | 0 | this_point_node = m_start_container; |
326 | 0 | this_point_offset = m_start_offset; |
327 | |
|
328 | 0 | other_point_node = source_range.m_start_container; |
329 | 0 | other_point_offset = source_range.m_start_offset; |
330 | 0 | break; |
331 | 0 | case HowToCompareBoundaryPoints::START_TO_END: |
332 | | // -> START_TO_END: |
333 | | // Let this point be this’s end. Let other point be sourceRange’s start. |
334 | 0 | this_point_node = m_end_container; |
335 | 0 | this_point_offset = m_end_offset; |
336 | |
|
337 | 0 | other_point_node = source_range.m_start_container; |
338 | 0 | other_point_offset = source_range.m_start_offset; |
339 | 0 | break; |
340 | 0 | case HowToCompareBoundaryPoints::END_TO_END: |
341 | | // -> END_TO_END: |
342 | | // Let this point be this’s end. Let other point be sourceRange’s end. |
343 | 0 | this_point_node = m_end_container; |
344 | 0 | this_point_offset = m_end_offset; |
345 | |
|
346 | 0 | other_point_node = source_range.m_end_container; |
347 | 0 | other_point_offset = source_range.m_end_offset; |
348 | 0 | break; |
349 | 0 | case HowToCompareBoundaryPoints::END_TO_START: |
350 | | // -> END_TO_START: |
351 | | // Let this point be this’s start. Let other point be sourceRange’s end. |
352 | 0 | this_point_node = m_start_container; |
353 | 0 | this_point_offset = m_start_offset; |
354 | |
|
355 | 0 | other_point_node = source_range.m_end_container; |
356 | 0 | other_point_offset = source_range.m_end_offset; |
357 | 0 | break; |
358 | 0 | default: |
359 | 0 | VERIFY_NOT_REACHED(); |
360 | 0 | } |
361 | | |
362 | 0 | VERIFY(this_point_node); |
363 | 0 | VERIFY(other_point_node); |
364 | | |
365 | | // 4. If the position of this point relative to other point is |
366 | 0 | auto relative_position = position_of_boundary_point_relative_to_other_boundary_point(*this_point_node, this_point_offset, *other_point_node, other_point_offset); |
367 | 0 | switch (relative_position) { |
368 | 0 | case RelativeBoundaryPointPosition::Before: |
369 | | // -> before |
370 | | // Return −1. |
371 | 0 | return -1; |
372 | 0 | case RelativeBoundaryPointPosition::Equal: |
373 | | // -> equal |
374 | | // Return 0. |
375 | 0 | return 0; |
376 | 0 | case RelativeBoundaryPointPosition::After: |
377 | | // -> after |
378 | | // Return 1. |
379 | 0 | return 1; |
380 | 0 | default: |
381 | 0 | VERIFY_NOT_REACHED(); |
382 | 0 | } |
383 | 0 | } |
384 | | |
385 | | // https://dom.spec.whatwg.org/#concept-range-select |
386 | | WebIDL::ExceptionOr<void> Range::select(Node& node) |
387 | 0 | { |
388 | | // 1. Let parent be node’s parent. |
389 | 0 | auto* parent = node.parent(); |
390 | | |
391 | | // 2. If parent is null, then throw an "InvalidNodeTypeError" DOMException. |
392 | 0 | if (!parent) |
393 | 0 | return WebIDL::InvalidNodeTypeError::create(realm(), "Given node has no parent."_string); |
394 | | |
395 | | // 3. Let index be node’s index. |
396 | 0 | auto index = node.index(); |
397 | | |
398 | | // 4. Set range’s start to boundary point (parent, index). |
399 | 0 | m_start_container = *parent; |
400 | 0 | m_start_offset = index; |
401 | | |
402 | | // 5. Set range’s end to boundary point (parent, index plus 1). |
403 | 0 | m_end_container = *parent; |
404 | 0 | m_end_offset = index + 1; |
405 | |
|
406 | 0 | update_associated_selection(); |
407 | 0 | return {}; |
408 | 0 | } |
409 | | |
410 | | // https://dom.spec.whatwg.org/#dom-range-selectnode |
411 | | WebIDL::ExceptionOr<void> Range::select_node(Node& node) |
412 | 0 | { |
413 | | // The selectNode(node) method steps are to select node within this. |
414 | 0 | return select(node); |
415 | 0 | } |
416 | | |
417 | | // https://dom.spec.whatwg.org/#dom-range-collapse |
418 | | void Range::collapse(bool to_start) |
419 | 0 | { |
420 | | // The collapse(toStart) method steps are to, if toStart is true, set end to start; otherwise set start to end. |
421 | 0 | if (to_start) { |
422 | 0 | m_end_container = m_start_container; |
423 | 0 | m_end_offset = m_start_offset; |
424 | 0 | } else { |
425 | 0 | m_start_container = m_end_container; |
426 | 0 | m_start_offset = m_end_offset; |
427 | 0 | } |
428 | 0 | update_associated_selection(); |
429 | 0 | } |
430 | | |
431 | | // https://dom.spec.whatwg.org/#dom-range-selectnodecontents |
432 | | WebIDL::ExceptionOr<void> Range::select_node_contents(Node& node) |
433 | 0 | { |
434 | | // 1. If node is a doctype, throw an "InvalidNodeTypeError" DOMException. |
435 | 0 | if (is<DocumentType>(node)) |
436 | 0 | return WebIDL::InvalidNodeTypeError::create(realm(), "Node cannot be a DocumentType."_string); |
437 | | |
438 | | // 2. Let length be the length of node. |
439 | 0 | auto length = node.length(); |
440 | | |
441 | | // 3. Set start to the boundary point (node, 0). |
442 | 0 | m_start_container = node; |
443 | 0 | m_start_offset = 0; |
444 | | |
445 | | // 4. Set end to the boundary point (node, length). |
446 | 0 | m_end_container = node; |
447 | 0 | m_end_offset = length; |
448 | |
|
449 | 0 | update_associated_selection(); |
450 | 0 | return {}; |
451 | 0 | } |
452 | | |
453 | | JS::NonnullGCPtr<Range> Range::clone_range() const |
454 | 0 | { |
455 | 0 | return heap().allocate<Range>(shape().realm(), const_cast<Node&>(*m_start_container), m_start_offset, const_cast<Node&>(*m_end_container), m_end_offset); |
456 | 0 | } |
457 | | |
458 | | JS::NonnullGCPtr<Range> Range::inverted() const |
459 | 0 | { |
460 | 0 | return heap().allocate<Range>(shape().realm(), const_cast<Node&>(*m_end_container), m_end_offset, const_cast<Node&>(*m_start_container), m_start_offset); |
461 | 0 | } |
462 | | |
463 | | JS::NonnullGCPtr<Range> Range::normalized() const |
464 | 0 | { |
465 | 0 | if (m_start_container.ptr() == m_end_container.ptr()) { |
466 | 0 | if (m_start_offset <= m_end_offset) |
467 | 0 | return clone_range(); |
468 | | |
469 | 0 | return inverted(); |
470 | 0 | } |
471 | | |
472 | 0 | if (m_start_container->is_before(m_end_container)) |
473 | 0 | return clone_range(); |
474 | | |
475 | 0 | return inverted(); |
476 | 0 | } |
477 | | |
478 | | // https://dom.spec.whatwg.org/#dom-range-commonancestorcontainer |
479 | | JS::NonnullGCPtr<Node> Range::common_ancestor_container() const |
480 | 0 | { |
481 | | // 1. Let container be start node. |
482 | 0 | auto container = m_start_container; |
483 | | |
484 | | // 2. While container is not an inclusive ancestor of end node, let container be container’s parent. |
485 | 0 | while (!container->is_inclusive_ancestor_of(m_end_container)) { |
486 | 0 | VERIFY(container->parent()); |
487 | 0 | container = *container->parent(); |
488 | 0 | } |
489 | | |
490 | | // 3. Return container. |
491 | 0 | return container; |
492 | 0 | } |
493 | | |
494 | | // https://dom.spec.whatwg.org/#dom-range-intersectsnode |
495 | | bool Range::intersects_node(Node const& node) const |
496 | 0 | { |
497 | | // 1. If node’s root is different from this’s root, return false. |
498 | 0 | if (&node.root() != &root()) |
499 | 0 | return false; |
500 | | |
501 | | // 2. Let parent be node’s parent. |
502 | 0 | auto* parent = node.parent(); |
503 | | |
504 | | // 3. If parent is null, return true. |
505 | 0 | if (!parent) |
506 | 0 | return true; |
507 | | |
508 | | // 4. Let offset be node’s index. |
509 | 0 | auto offset = node.index(); |
510 | | |
511 | | // 5. If (parent, offset) is before end and (parent, offset plus 1) is after start, return true |
512 | 0 | auto relative_position_to_end = position_of_boundary_point_relative_to_other_boundary_point(*parent, offset, m_end_container, m_end_offset); |
513 | 0 | auto relative_position_to_start = position_of_boundary_point_relative_to_other_boundary_point(*parent, offset + 1, m_start_container, m_start_offset); |
514 | 0 | if (relative_position_to_end == RelativeBoundaryPointPosition::Before && relative_position_to_start == RelativeBoundaryPointPosition::After) |
515 | 0 | return true; |
516 | | |
517 | | // 6. Return false. |
518 | 0 | return false; |
519 | 0 | } |
520 | | |
521 | | // https://dom.spec.whatwg.org/#dom-range-ispointinrange |
522 | | WebIDL::ExceptionOr<bool> Range::is_point_in_range(Node const& node, WebIDL::UnsignedLong offset) const |
523 | 0 | { |
524 | | // 1. If node’s root is different from this’s root, return false. |
525 | 0 | if (&node.root() != &root()) |
526 | 0 | return false; |
527 | | |
528 | | // 2. If node is a doctype, then throw an "InvalidNodeTypeError" DOMException. |
529 | 0 | if (is<DocumentType>(node)) |
530 | 0 | return WebIDL::InvalidNodeTypeError::create(realm(), "Node cannot be a DocumentType."_string); |
531 | | |
532 | | // 3. If offset is greater than node’s length, then throw an "IndexSizeError" DOMException. |
533 | 0 | if (offset > node.length()) |
534 | 0 | return WebIDL::IndexSizeError::create(realm(), MUST(String::formatted("Node does not contain a child at offset {}", offset))); |
535 | | |
536 | | // 4. If (node, offset) is before start or after end, return false. |
537 | 0 | auto relative_position_to_start = position_of_boundary_point_relative_to_other_boundary_point(node, offset, m_start_container, m_start_offset); |
538 | 0 | auto relative_position_to_end = position_of_boundary_point_relative_to_other_boundary_point(node, offset, m_end_container, m_end_offset); |
539 | 0 | if (relative_position_to_start == RelativeBoundaryPointPosition::Before || relative_position_to_end == RelativeBoundaryPointPosition::After) |
540 | 0 | return false; |
541 | | |
542 | | // 5. Return true. |
543 | 0 | return true; |
544 | 0 | } |
545 | | |
546 | | // https://dom.spec.whatwg.org/#dom-range-comparepoint |
547 | | WebIDL::ExceptionOr<WebIDL::Short> Range::compare_point(Node const& node, WebIDL::UnsignedLong offset) const |
548 | 0 | { |
549 | | // 1. If node’s root is different from this’s root, then throw a "WrongDocumentError" DOMException. |
550 | 0 | if (&node.root() != &root()) |
551 | 0 | return WebIDL::WrongDocumentError::create(realm(), "Given node is not in the same document as the range."_string); |
552 | | |
553 | | // 2. If node is a doctype, then throw an "InvalidNodeTypeError" DOMException. |
554 | 0 | if (is<DocumentType>(node)) |
555 | 0 | return WebIDL::InvalidNodeTypeError::create(realm(), "Node cannot be a DocumentType."_string); |
556 | | |
557 | | // 3. If offset is greater than node’s length, then throw an "IndexSizeError" DOMException. |
558 | 0 | if (offset > node.length()) |
559 | 0 | return WebIDL::IndexSizeError::create(realm(), MUST(String::formatted("Node does not contain a child at offset {}", offset))); |
560 | | |
561 | | // 4. If (node, offset) is before start, return −1. |
562 | 0 | auto relative_position_to_start = position_of_boundary_point_relative_to_other_boundary_point(node, offset, m_start_container, m_start_offset); |
563 | 0 | if (relative_position_to_start == RelativeBoundaryPointPosition::Before) |
564 | 0 | return -1; |
565 | | |
566 | | // 5. If (node, offset) is after end, return 1. |
567 | 0 | auto relative_position_to_end = position_of_boundary_point_relative_to_other_boundary_point(node, offset, m_end_container, m_end_offset); |
568 | 0 | if (relative_position_to_end == RelativeBoundaryPointPosition::After) |
569 | 0 | return 1; |
570 | | |
571 | | // 6. Return 0. |
572 | 0 | return 0; |
573 | 0 | } |
574 | | |
575 | | // https://dom.spec.whatwg.org/#dom-range-stringifier |
576 | | String Range::to_string() const |
577 | 0 | { |
578 | | // 1. Let s be the empty string. |
579 | 0 | StringBuilder builder; |
580 | | |
581 | | // 2. If this’s start node is this’s end node and it is a Text node, |
582 | | // then return the substring of that Text node’s data beginning at this’s start offset and ending at this’s end offset. |
583 | 0 | if (start_container() == end_container() && is<Text>(*start_container())) { |
584 | 0 | auto const& text = static_cast<Text const&>(*start_container()); |
585 | 0 | return MUST(text.substring_data(start_offset(), end_offset() - start_offset())); |
586 | 0 | } |
587 | | |
588 | | // 3. If this’s start node is a Text node, then append the substring of that node’s data from this’s start offset until the end to s. |
589 | 0 | if (is<Text>(*start_container())) { |
590 | 0 | auto const& text = static_cast<Text const&>(*start_container()); |
591 | 0 | builder.append(MUST(text.substring_data(start_offset(), text.length_in_utf16_code_units() - start_offset()))); |
592 | 0 | } |
593 | | |
594 | | // 4. Append the concatenation of the data of all Text nodes that are contained in this, in tree order, to s. |
595 | 0 | for (Node const* node = start_container(); node != end_container()->next_sibling(); node = node->next_in_pre_order()) { |
596 | 0 | if (is<Text>(*node) && contains_node(*node)) |
597 | 0 | builder.append(static_cast<Text const&>(*node).data()); |
598 | 0 | } |
599 | | |
600 | | // 5. If this’s end node is a Text node, then append the substring of that node’s data from its start until this’s end offset to s. |
601 | 0 | if (is<Text>(*end_container())) { |
602 | 0 | auto const& text = static_cast<Text const&>(*end_container()); |
603 | 0 | builder.append(MUST(text.substring_data(0, end_offset()))); |
604 | 0 | } |
605 | | |
606 | | // 6. Return s. |
607 | 0 | return MUST(builder.to_string()); |
608 | 0 | } |
609 | | |
610 | | // https://dom.spec.whatwg.org/#dom-range-extractcontents |
611 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<DocumentFragment>> Range::extract_contents() |
612 | 0 | { |
613 | 0 | return extract(); |
614 | 0 | } |
615 | | |
616 | | // https://dom.spec.whatwg.org/#concept-range-extract |
617 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<DocumentFragment>> Range::extract() |
618 | 0 | { |
619 | | // 1. Let fragment be a new DocumentFragment node whose node document is range’s start node’s node document. |
620 | 0 | auto fragment = heap().allocate<DOM::DocumentFragment>(realm(), const_cast<Document&>(start_container()->document())); |
621 | | |
622 | | // 2. If range is collapsed, then return fragment. |
623 | 0 | if (collapsed()) |
624 | 0 | return fragment; |
625 | | |
626 | | // 3. Let original start node, original start offset, original end node, and original end offset |
627 | | // be range’s start node, start offset, end node, and end offset, respectively. |
628 | 0 | JS::NonnullGCPtr<Node> original_start_node = m_start_container; |
629 | 0 | auto original_start_offset = m_start_offset; |
630 | 0 | JS::NonnullGCPtr<Node> original_end_node = m_end_container; |
631 | 0 | auto original_end_offset = m_end_offset; |
632 | | |
633 | | // 4. If original start node is original end node and it is a CharacterData node, then: |
634 | 0 | if (original_start_node.ptr() == original_end_node.ptr() && is<CharacterData>(*original_start_node)) { |
635 | | // 1. Let clone be a clone of original start node. |
636 | 0 | auto clone = TRY(original_start_node->clone_node()); |
637 | | |
638 | | // 2. Set the data of clone to the result of substringing data with node original start node, |
639 | | // offset original start offset, and count original end offset minus original start offset. |
640 | 0 | auto result = TRY(static_cast<CharacterData const&>(*original_start_node).substring_data(original_start_offset, original_end_offset - original_start_offset)); |
641 | 0 | verify_cast<CharacterData>(*clone).set_data(move(result)); |
642 | | |
643 | | // 3. Append clone to fragment. |
644 | 0 | TRY(fragment->append_child(clone)); |
645 | | |
646 | | // 4. Replace data with node original start node, offset original start offset, count original end offset minus original start offset, and data the empty string. |
647 | 0 | TRY(static_cast<CharacterData&>(*original_start_node).replace_data(original_start_offset, original_end_offset - original_start_offset, String {})); |
648 | | |
649 | | // 5. Return fragment. |
650 | 0 | return fragment; |
651 | 0 | } |
652 | | |
653 | | // 5. Let common ancestor be original start node. |
654 | 0 | JS::NonnullGCPtr<Node> common_ancestor = original_start_node; |
655 | | |
656 | | // 6. While common ancestor is not an inclusive ancestor of original end node, set common ancestor to its own parent. |
657 | 0 | while (!common_ancestor->is_inclusive_ancestor_of(original_end_node)) |
658 | 0 | common_ancestor = *common_ancestor->parent_node(); |
659 | | |
660 | | // 7. Let first partially contained child be null. |
661 | 0 | JS::GCPtr<Node> first_partially_contained_child; |
662 | | |
663 | | // 8. If original start node is not an inclusive ancestor of original end node, |
664 | | // set first partially contained child to the first child of common ancestor that is partially contained in range. |
665 | 0 | if (!original_start_node->is_inclusive_ancestor_of(original_end_node)) { |
666 | 0 | for (auto* child = common_ancestor->first_child(); child; child = child->next_sibling()) { |
667 | 0 | if (partially_contains_node(*child)) { |
668 | 0 | first_partially_contained_child = child; |
669 | 0 | break; |
670 | 0 | } |
671 | 0 | } |
672 | 0 | } |
673 | | |
674 | | // 9. Let last partially contained child be null. |
675 | 0 | JS::GCPtr<Node> last_partially_contained_child; |
676 | | |
677 | | // 10. If original end node is not an inclusive ancestor of original start node, |
678 | | // set last partially contained child to the last child of common ancestor that is partially contained in range. |
679 | 0 | if (!original_end_node->is_inclusive_ancestor_of(original_start_node)) { |
680 | 0 | for (auto* child = common_ancestor->last_child(); child; child = child->previous_sibling()) { |
681 | 0 | if (partially_contains_node(*child)) { |
682 | 0 | last_partially_contained_child = child; |
683 | 0 | break; |
684 | 0 | } |
685 | 0 | } |
686 | 0 | } |
687 | | |
688 | | // 11. Let contained children be a list of all children of common ancestor that are contained in range, in tree order. |
689 | 0 | Vector<JS::NonnullGCPtr<Node>> contained_children; |
690 | 0 | for (Node* node = common_ancestor->first_child(); node; node = node->next_sibling()) { |
691 | 0 | if (contains_node(*node)) |
692 | 0 | contained_children.append(*node); |
693 | 0 | } |
694 | | |
695 | | // 12. If any member of contained children is a doctype, then throw a "HierarchyRequestError" DOMException. |
696 | 0 | for (auto const& child : contained_children) { |
697 | 0 | if (is<DocumentType>(*child)) |
698 | 0 | return WebIDL::HierarchyRequestError::create(realm(), "Contained child is a DocumentType"_string); |
699 | 0 | } |
700 | | |
701 | 0 | JS::GCPtr<Node> new_node; |
702 | 0 | size_t new_offset = 0; |
703 | | |
704 | | // 13. If original start node is an inclusive ancestor of original end node, set new node to original start node and new offset to original start offset. |
705 | 0 | if (original_start_node->is_inclusive_ancestor_of(original_end_node)) { |
706 | 0 | new_node = original_start_node; |
707 | 0 | new_offset = original_start_offset; |
708 | 0 | } |
709 | | // 14. Otherwise: |
710 | 0 | else { |
711 | | // 1. Let reference node equal original start node. |
712 | 0 | JS::GCPtr<Node> reference_node = original_start_node; |
713 | | |
714 | | // 2. While reference node’s parent is not null and is not an inclusive ancestor of original end node, set reference node to its parent. |
715 | 0 | while (reference_node->parent_node() && !reference_node->parent_node()->is_inclusive_ancestor_of(original_end_node)) |
716 | 0 | reference_node = reference_node->parent_node(); |
717 | | |
718 | | // 3. Set new node to the parent of reference node, and new offset to one plus reference node’s index. |
719 | 0 | new_node = reference_node->parent_node(); |
720 | 0 | new_offset = 1 + reference_node->index(); |
721 | 0 | } |
722 | | |
723 | | // 15. If first partially contained child is a CharacterData node, then: |
724 | 0 | if (first_partially_contained_child && is<CharacterData>(*first_partially_contained_child)) { |
725 | | // 1. Let clone be a clone of original start node. |
726 | 0 | auto clone = TRY(original_start_node->clone_node()); |
727 | | |
728 | | // 2. Set the data of clone to the result of substringing data with node original start node, offset original start offset, |
729 | | // and count original start node’s length minus original start offset. |
730 | 0 | auto result = TRY(static_cast<CharacterData const&>(*original_start_node).substring_data(original_start_offset, original_start_node->length() - original_start_offset)); |
731 | 0 | verify_cast<CharacterData>(*clone).set_data(move(result)); |
732 | | |
733 | | // 3. Append clone to fragment. |
734 | 0 | TRY(fragment->append_child(clone)); |
735 | | |
736 | | // 4. Replace data with node original start node, offset original start offset, count original start node’s length minus original start offset, and data the empty string. |
737 | 0 | TRY(static_cast<CharacterData&>(*original_start_node).replace_data(original_start_offset, original_start_node->length() - original_start_offset, String {})); |
738 | 0 | } |
739 | | // 16. Otherwise, if first partially contained child is not null: |
740 | 0 | else if (first_partially_contained_child) { |
741 | | // 1. Let clone be a clone of first partially contained child. |
742 | 0 | auto clone = TRY(first_partially_contained_child->clone_node()); |
743 | | |
744 | | // 2. Append clone to fragment. |
745 | 0 | TRY(fragment->append_child(clone)); |
746 | | |
747 | | // 3. Let subrange be a new live range whose start is (original start node, original start offset) and whose end is (first partially contained child, first partially contained child’s length). |
748 | 0 | auto subrange = Range::create(original_start_node, original_start_offset, *first_partially_contained_child, first_partially_contained_child->length()); |
749 | | |
750 | | // 4. Let subfragment be the result of extracting subrange. |
751 | 0 | auto subfragment = TRY(subrange->extract()); |
752 | | |
753 | | // 5. Append subfragment to clone. |
754 | 0 | TRY(clone->append_child(subfragment)); |
755 | 0 | } |
756 | | |
757 | | // 17. For each contained child in contained children, append contained child to fragment. |
758 | 0 | for (auto& contained_child : contained_children) { |
759 | 0 | TRY(fragment->append_child(contained_child)); |
760 | 0 | } |
761 | | |
762 | | // 18. If last partially contained child is a CharacterData node, then: |
763 | 0 | if (last_partially_contained_child && is<CharacterData>(*last_partially_contained_child)) { |
764 | | // 1. Let clone be a clone of original end node. |
765 | 0 | auto clone = TRY(original_end_node->clone_node()); |
766 | | |
767 | | // 2. Set the data of clone to the result of substringing data with node original end node, offset 0, and count original end offset. |
768 | 0 | auto result = TRY(static_cast<CharacterData const&>(*original_end_node).substring_data(0, original_end_offset)); |
769 | 0 | verify_cast<CharacterData>(*clone).set_data(move(result)); |
770 | | |
771 | | // 3. Append clone to fragment. |
772 | 0 | TRY(fragment->append_child(clone)); |
773 | | |
774 | | // 4. Replace data with node original end node, offset 0, count original end offset, and data the empty string. |
775 | 0 | TRY(verify_cast<CharacterData>(*original_end_node).replace_data(0, original_end_offset, String {})); |
776 | 0 | } |
777 | | // 19. Otherwise, if last partially contained child is not null: |
778 | 0 | else if (last_partially_contained_child) { |
779 | | // 1. Let clone be a clone of last partially contained child. |
780 | 0 | auto clone = TRY(last_partially_contained_child->clone_node()); |
781 | | |
782 | | // 2. Append clone to fragment. |
783 | 0 | TRY(fragment->append_child(clone)); |
784 | | |
785 | | // 3. Let subrange be a new live range whose start is (last partially contained child, 0) and whose end is (original end node, original end offset). |
786 | 0 | auto subrange = Range::create(*last_partially_contained_child, 0, original_end_node, original_end_offset); |
787 | | |
788 | | // 4. Let subfragment be the result of extracting subrange. |
789 | 0 | auto subfragment = TRY(subrange->extract()); |
790 | | |
791 | | // 5. Append subfragment to clone. |
792 | 0 | TRY(clone->append_child(subfragment)); |
793 | 0 | } |
794 | | |
795 | | // 20. Set range’s start and end to (new node, new offset). |
796 | 0 | TRY(set_start(*new_node, new_offset)); |
797 | 0 | TRY(set_end(*new_node, new_offset)); |
798 | | |
799 | | // 21. Return fragment. |
800 | 0 | return fragment; |
801 | 0 | } |
802 | | |
803 | | // https://dom.spec.whatwg.org/#contained |
804 | | bool Range::contains_node(Node const& node) const |
805 | 0 | { |
806 | | // A node node is contained in a live range range if node’s root is range’s root, |
807 | 0 | if (&node.root() != &root()) |
808 | 0 | return false; |
809 | | |
810 | | // and (node, 0) is after range’s start, |
811 | 0 | if (position_of_boundary_point_relative_to_other_boundary_point(node, 0, m_start_container, m_start_offset) != RelativeBoundaryPointPosition::After) |
812 | 0 | return false; |
813 | | |
814 | | // and (node, node’s length) is before range’s end. |
815 | 0 | if (position_of_boundary_point_relative_to_other_boundary_point(node, node.length(), m_end_container, m_end_offset) != RelativeBoundaryPointPosition::Before) |
816 | 0 | return false; |
817 | | |
818 | 0 | return true; |
819 | 0 | } |
820 | | |
821 | | // https://dom.spec.whatwg.org/#partially-contained |
822 | | bool Range::partially_contains_node(Node const& node) const |
823 | 0 | { |
824 | | // A node is partially contained in a live range if it’s an inclusive ancestor of the live range’s start node but not its end node, or vice versa. |
825 | 0 | if (node.is_inclusive_ancestor_of(m_start_container) && &node != m_end_container.ptr()) |
826 | 0 | return true; |
827 | 0 | if (node.is_inclusive_ancestor_of(m_end_container) && &node != m_start_container.ptr()) |
828 | 0 | return true; |
829 | 0 | return false; |
830 | 0 | } |
831 | | |
832 | | // https://dom.spec.whatwg.org/#dom-range-insertnode |
833 | | WebIDL::ExceptionOr<void> Range::insert_node(JS::NonnullGCPtr<Node> node) |
834 | 0 | { |
835 | 0 | return insert(node); |
836 | 0 | } |
837 | | |
838 | | // https://dom.spec.whatwg.org/#concept-range-insert |
839 | | WebIDL::ExceptionOr<void> Range::insert(JS::NonnullGCPtr<Node> node) |
840 | 0 | { |
841 | | // 1. If range’s start node is a ProcessingInstruction or Comment node, is a Text node whose parent is null, or is node, then throw a "HierarchyRequestError" DOMException. |
842 | 0 | if ((is<ProcessingInstruction>(*m_start_container) || is<Comment>(*m_start_container)) |
843 | 0 | || (is<Text>(*m_start_container) && !m_start_container->parent_node()) |
844 | 0 | || m_start_container.ptr() == node.ptr()) { |
845 | 0 | return WebIDL::HierarchyRequestError::create(realm(), "Range has inappropriate start node for insertion"_string); |
846 | 0 | } |
847 | | |
848 | | // 2. Let referenceNode be null. |
849 | 0 | JS::GCPtr<Node> reference_node; |
850 | | |
851 | | // 3. If range’s start node is a Text node, set referenceNode to that Text node. |
852 | 0 | if (is<Text>(*m_start_container)) { |
853 | 0 | reference_node = m_start_container; |
854 | 0 | } |
855 | | // 4. Otherwise, set referenceNode to the child of start node whose index is start offset, and null if there is no such child. |
856 | 0 | else { |
857 | 0 | reference_node = m_start_container->child_at_index(m_start_offset); |
858 | 0 | } |
859 | | |
860 | | // 5. Let parent be range’s start node if referenceNode is null, and referenceNode’s parent otherwise. |
861 | 0 | JS::GCPtr<Node> parent; |
862 | 0 | if (!reference_node) |
863 | 0 | parent = m_start_container; |
864 | 0 | else |
865 | 0 | parent = reference_node->parent(); |
866 | | |
867 | | // 6. Ensure pre-insertion validity of node into parent before referenceNode. |
868 | 0 | TRY(parent->ensure_pre_insertion_validity(node, reference_node)); |
869 | | |
870 | | // 7. If range’s start node is a Text node, set referenceNode to the result of splitting it with offset range’s start offset. |
871 | 0 | if (is<Text>(*m_start_container)) |
872 | 0 | reference_node = TRY(static_cast<Text&>(*m_start_container).split_text(m_start_offset)); |
873 | | |
874 | | // 8. If node is referenceNode, set referenceNode to its next sibling. |
875 | 0 | if (node == reference_node) |
876 | 0 | reference_node = reference_node->next_sibling(); |
877 | | |
878 | | // 9. If node’s parent is non-null, then remove node. |
879 | 0 | if (node->parent()) |
880 | 0 | node->remove(); |
881 | | |
882 | | // 10. Let newOffset be parent’s length if referenceNode is null, and referenceNode’s index otherwise. |
883 | 0 | size_t new_offset = 0; |
884 | 0 | if (!reference_node) |
885 | 0 | new_offset = parent->length(); |
886 | 0 | else |
887 | 0 | new_offset = reference_node->index(); |
888 | | |
889 | | // 11. Increase newOffset by node’s length if node is a DocumentFragment node, and one otherwise. |
890 | 0 | if (is<DocumentFragment>(*node)) |
891 | 0 | new_offset += node->length(); |
892 | 0 | else |
893 | 0 | new_offset += 1; |
894 | | |
895 | | // 12. Pre-insert node into parent before referenceNode. |
896 | 0 | (void)TRY(parent->pre_insert(node, reference_node)); |
897 | | |
898 | | // 13. If range is collapsed, then set range’s end to (parent, newOffset). |
899 | 0 | if (collapsed()) |
900 | 0 | TRY(set_end(*parent, new_offset)); |
901 | |
|
902 | 0 | return {}; |
903 | 0 | } |
904 | | |
905 | | // https://dom.spec.whatwg.org/#dom-range-surroundcontents |
906 | | WebIDL::ExceptionOr<void> Range::surround_contents(JS::NonnullGCPtr<Node> new_parent) |
907 | 0 | { |
908 | | // 1. If a non-Text node is partially contained in this, then throw an "InvalidStateError" DOMException. |
909 | 0 | Node* start_non_text_node = start_container(); |
910 | 0 | if (is<Text>(*start_non_text_node)) |
911 | 0 | start_non_text_node = start_non_text_node->parent_node(); |
912 | 0 | Node* end_non_text_node = end_container(); |
913 | 0 | if (is<Text>(*end_non_text_node)) |
914 | 0 | end_non_text_node = end_non_text_node->parent_node(); |
915 | 0 | if (start_non_text_node != end_non_text_node) |
916 | 0 | return WebIDL::InvalidStateError::create(realm(), "Non-Text node is partially contained in range."_string); |
917 | | |
918 | | // 2. If newParent is a Document, DocumentType, or DocumentFragment node, then throw an "InvalidNodeTypeError" DOMException. |
919 | 0 | if (is<Document>(*new_parent) || is<DocumentType>(*new_parent) || is<DocumentFragment>(*new_parent)) |
920 | 0 | return WebIDL::InvalidNodeTypeError::create(realm(), "Invalid parent node type"_string); |
921 | | |
922 | | // 3. Let fragment be the result of extracting this. |
923 | 0 | auto fragment = TRY(extract()); |
924 | | |
925 | | // 4. If newParent has children, then replace all with null within newParent. |
926 | 0 | if (new_parent->has_children()) |
927 | 0 | new_parent->replace_all(nullptr); |
928 | | |
929 | | // 5. Insert newParent into this. |
930 | 0 | TRY(insert(new_parent)); |
931 | | |
932 | | // 6. Append fragment to newParent. |
933 | 0 | (void)TRY(new_parent->append_child(fragment)); |
934 | | |
935 | | // 7. Select newParent within this. |
936 | 0 | return select(*new_parent); |
937 | 0 | } |
938 | | |
939 | | // https://dom.spec.whatwg.org/#dom-range-clonecontents |
940 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<DocumentFragment>> Range::clone_contents() |
941 | 0 | { |
942 | 0 | return clone_the_contents(); |
943 | 0 | } |
944 | | |
945 | | // https://dom.spec.whatwg.org/#concept-range-clone |
946 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<DocumentFragment>> Range::clone_the_contents() |
947 | 0 | { |
948 | | // 1. Let fragment be a new DocumentFragment node whose node document is range’s start node’s node document. |
949 | 0 | auto fragment = heap().allocate<DOM::DocumentFragment>(realm(), const_cast<Document&>(start_container()->document())); |
950 | | |
951 | | // 2. If range is collapsed, then return fragment. |
952 | 0 | if (collapsed()) |
953 | 0 | return fragment; |
954 | | |
955 | | // 3. Let original start node, original start offset, original end node, and original end offset |
956 | | // be range’s start node, start offset, end node, and end offset, respectively. |
957 | 0 | JS::NonnullGCPtr<Node> original_start_node = m_start_container; |
958 | 0 | auto original_start_offset = m_start_offset; |
959 | 0 | JS::NonnullGCPtr<Node> original_end_node = m_end_container; |
960 | 0 | auto original_end_offset = m_end_offset; |
961 | | |
962 | | // 4. If original start node is original end node and it is a CharacterData node, then: |
963 | 0 | if (original_start_node.ptr() == original_end_node.ptr() && is<CharacterData>(*original_start_node)) { |
964 | | // 1. Let clone be a clone of original start node. |
965 | 0 | auto clone = TRY(original_start_node->clone_node()); |
966 | | |
967 | | // 2. Set the data of clone to the result of substringing data with node original start node, |
968 | | // offset original start offset, and count original end offset minus original start offset. |
969 | 0 | auto result = TRY(static_cast<CharacterData const&>(*original_start_node).substring_data(original_start_offset, original_end_offset - original_start_offset)); |
970 | 0 | verify_cast<CharacterData>(*clone).set_data(move(result)); |
971 | | |
972 | | // 3. Append clone to fragment. |
973 | 0 | TRY(fragment->append_child(clone)); |
974 | | |
975 | | // 4. Return fragment. |
976 | 0 | return fragment; |
977 | 0 | } |
978 | | |
979 | | // 5. Let common ancestor be original start node. |
980 | 0 | JS::NonnullGCPtr<Node> common_ancestor = original_start_node; |
981 | | |
982 | | // 6. While common ancestor is not an inclusive ancestor of original end node, set common ancestor to its own parent. |
983 | 0 | while (!common_ancestor->is_inclusive_ancestor_of(original_end_node)) |
984 | 0 | common_ancestor = *common_ancestor->parent_node(); |
985 | | |
986 | | // 7. Let first partially contained child be null. |
987 | 0 | JS::GCPtr<Node> first_partially_contained_child; |
988 | | |
989 | | // 8. If original start node is not an inclusive ancestor of original end node, |
990 | | // set first partially contained child to the first child of common ancestor that is partially contained in range. |
991 | 0 | if (!original_start_node->is_inclusive_ancestor_of(original_end_node)) { |
992 | 0 | for (auto* child = common_ancestor->first_child(); child; child = child->next_sibling()) { |
993 | 0 | if (partially_contains_node(*child)) { |
994 | 0 | first_partially_contained_child = child; |
995 | 0 | break; |
996 | 0 | } |
997 | 0 | } |
998 | 0 | } |
999 | | |
1000 | | // 9. Let last partially contained child be null. |
1001 | 0 | JS::GCPtr<Node> last_partially_contained_child; |
1002 | | |
1003 | | // 10. If original end node is not an inclusive ancestor of original start node, |
1004 | | // set last partially contained child to the last child of common ancestor that is partially contained in range. |
1005 | 0 | if (!original_end_node->is_inclusive_ancestor_of(original_start_node)) { |
1006 | 0 | for (auto* child = common_ancestor->last_child(); child; child = child->previous_sibling()) { |
1007 | 0 | if (partially_contains_node(*child)) { |
1008 | 0 | last_partially_contained_child = child; |
1009 | 0 | break; |
1010 | 0 | } |
1011 | 0 | } |
1012 | 0 | } |
1013 | | |
1014 | | // 11. Let contained children be a list of all children of common ancestor that are contained in range, in tree order. |
1015 | 0 | Vector<JS::NonnullGCPtr<Node>> contained_children; |
1016 | 0 | for (Node* node = common_ancestor->first_child(); node; node = node->next_sibling()) { |
1017 | 0 | if (contains_node(*node)) |
1018 | 0 | contained_children.append(*node); |
1019 | 0 | } |
1020 | | |
1021 | | // 12. If any member of contained children is a doctype, then throw a "HierarchyRequestError" DOMException. |
1022 | 0 | for (auto const& child : contained_children) { |
1023 | 0 | if (is<DocumentType>(*child)) |
1024 | 0 | return WebIDL::HierarchyRequestError::create(realm(), "Contained child is a DocumentType"_string); |
1025 | 0 | } |
1026 | | |
1027 | | // 13. If first partially contained child is a CharacterData node, then: |
1028 | 0 | if (first_partially_contained_child && is<CharacterData>(*first_partially_contained_child)) { |
1029 | | // 1. Let clone be a clone of original start node. |
1030 | 0 | auto clone = TRY(original_start_node->clone_node()); |
1031 | | |
1032 | | // 2. Set the data of clone to the result of substringing data with node original start node, offset original start offset, |
1033 | | // and count original start node’s length minus original start offset. |
1034 | 0 | auto result = TRY(static_cast<CharacterData const&>(*original_start_node).substring_data(original_start_offset, original_start_node->length() - original_start_offset)); |
1035 | 0 | verify_cast<CharacterData>(*clone).set_data(move(result)); |
1036 | | |
1037 | | // 3. Append clone to fragment. |
1038 | 0 | TRY(fragment->append_child(clone)); |
1039 | 0 | } |
1040 | | // 14. Otherwise, if first partially contained child is not null: |
1041 | 0 | else if (first_partially_contained_child) { |
1042 | | // 1. Let clone be a clone of first partially contained child. |
1043 | 0 | auto clone = TRY(first_partially_contained_child->clone_node()); |
1044 | | |
1045 | | // 2. Append clone to fragment. |
1046 | 0 | TRY(fragment->append_child(clone)); |
1047 | | |
1048 | | // 3. Let subrange be a new live range whose start is (original start node, original start offset) and whose end is (first partially contained child, first partially contained child’s length). |
1049 | 0 | auto subrange = Range::create(original_start_node, original_start_offset, *first_partially_contained_child, first_partially_contained_child->length()); |
1050 | | |
1051 | | // 4. Let subfragment be the result of cloning the contents of subrange. |
1052 | 0 | auto subfragment = TRY(subrange->clone_the_contents()); |
1053 | | |
1054 | | // 5. Append subfragment to clone. |
1055 | 0 | TRY(clone->append_child(subfragment)); |
1056 | 0 | } |
1057 | | |
1058 | | // 15. For each contained child in contained children. |
1059 | 0 | for (auto& contained_child : contained_children) { |
1060 | | // 1. Let clone be a clone of contained child with the clone children flag set. |
1061 | 0 | auto clone = TRY(contained_child->clone_node(nullptr, true)); |
1062 | | |
1063 | | // 2. Append clone to fragment. |
1064 | 0 | TRY(fragment->append_child(move(clone))); |
1065 | 0 | } |
1066 | | |
1067 | | // 16. If last partially contained child is a CharacterData node, then: |
1068 | 0 | if (last_partially_contained_child && is<CharacterData>(*last_partially_contained_child)) { |
1069 | | // 1. Let clone be a clone of original end node. |
1070 | 0 | auto clone = TRY(original_end_node->clone_node()); |
1071 | | |
1072 | | // 2. Set the data of clone to the result of substringing data with node original end node, offset 0, and count original end offset. |
1073 | 0 | auto result = TRY(static_cast<CharacterData const&>(*original_end_node).substring_data(0, original_end_offset)); |
1074 | 0 | verify_cast<CharacterData>(*clone).set_data(move(result)); |
1075 | | |
1076 | | // 3. Append clone to fragment. |
1077 | 0 | TRY(fragment->append_child(clone)); |
1078 | 0 | } |
1079 | | // 17. Otherwise, if last partially contained child is not null: |
1080 | 0 | else if (last_partially_contained_child) { |
1081 | | // 1. Let clone be a clone of last partially contained child. |
1082 | 0 | auto clone = TRY(last_partially_contained_child->clone_node()); |
1083 | | |
1084 | | // 2. Append clone to fragment. |
1085 | 0 | TRY(fragment->append_child(clone)); |
1086 | | |
1087 | | // 3. Let subrange be a new live range whose start is (last partially contained child, 0) and whose end is (original end node, original end offset). |
1088 | 0 | auto subrange = Range::create(*last_partially_contained_child, 0, original_end_node, original_end_offset); |
1089 | | |
1090 | | // 4. Let subfragment be the result of cloning the contents of subrange. |
1091 | 0 | auto subfragment = TRY(subrange->clone_the_contents()); |
1092 | | |
1093 | | // 5. Append subfragment to clone. |
1094 | 0 | TRY(clone->append_child(subfragment)); |
1095 | 0 | } |
1096 | | |
1097 | | // 18. Return fragment. |
1098 | 0 | return fragment; |
1099 | 0 | } |
1100 | | |
1101 | | // https://dom.spec.whatwg.org/#dom-range-deletecontents |
1102 | | WebIDL::ExceptionOr<void> Range::delete_contents() |
1103 | 0 | { |
1104 | | // 1. If this is collapsed, then return. |
1105 | 0 | if (collapsed()) |
1106 | 0 | return {}; |
1107 | | |
1108 | | // 2. Let original start node, original start offset, original end node, and original end offset be this’s start node, start offset, end node, and end offset, respectively. |
1109 | 0 | JS::NonnullGCPtr<Node> original_start_node = m_start_container; |
1110 | 0 | auto original_start_offset = m_start_offset; |
1111 | 0 | JS::NonnullGCPtr<Node> original_end_node = m_end_container; |
1112 | 0 | auto original_end_offset = m_end_offset; |
1113 | | |
1114 | | // 3. If original start node is original end node and it is a CharacterData node, then replace data with node original start node, offset original start offset, |
1115 | | // count original end offset minus original start offset, and data the empty string, and then return. |
1116 | 0 | if (original_start_node.ptr() == original_end_node.ptr() && is<CharacterData>(*original_start_node)) { |
1117 | 0 | TRY(static_cast<CharacterData&>(*original_start_node).replace_data(original_start_offset, original_end_offset - original_start_offset, String {})); |
1118 | 0 | return {}; |
1119 | 0 | } |
1120 | | |
1121 | | // 4. Let nodes to remove be a list of all the nodes that are contained in this, in tree order, omitting any node whose parent is also contained in this. |
1122 | 0 | JS::MarkedVector<Node*> nodes_to_remove(heap()); |
1123 | 0 | for (Node const* node = start_container(); node != end_container()->next_sibling(); node = node->next_in_pre_order()) { |
1124 | 0 | if (contains_node(*node) && (!node->parent_node() || !contains_node(*node->parent_node()))) |
1125 | 0 | nodes_to_remove.append(const_cast<Node*>(node)); |
1126 | 0 | } |
1127 | |
|
1128 | 0 | JS::GCPtr<Node> new_node; |
1129 | 0 | size_t new_offset = 0; |
1130 | | |
1131 | | // 5. If original start node is an inclusive ancestor of original end node, set new node to original start node and new offset to original start offset. |
1132 | 0 | if (original_start_node->is_inclusive_ancestor_of(original_end_node)) { |
1133 | 0 | new_node = original_start_node; |
1134 | 0 | new_offset = original_start_offset; |
1135 | 0 | } |
1136 | | // 6. Otherwise |
1137 | 0 | else { |
1138 | | // 1. Let reference node equal original start node. |
1139 | 0 | auto reference_node = original_start_node; |
1140 | | |
1141 | | // 2. While reference node’s parent is not null and is not an inclusive ancestor of original end node, set reference node to its parent. |
1142 | 0 | while (reference_node->parent_node() && !reference_node->parent_node()->is_inclusive_ancestor_of(original_end_node)) |
1143 | 0 | reference_node = *reference_node->parent_node(); |
1144 | | |
1145 | | // 3. Set new node to the parent of reference node, and new offset to one plus the index of reference node. |
1146 | 0 | new_node = reference_node->parent_node(); |
1147 | 0 | new_offset = 1 + reference_node->index(); |
1148 | 0 | } |
1149 | | |
1150 | | // 7. If original start node is a CharacterData node, then replace data with node original start node, offset original start offset, count original start node’s length minus original start offset, data the empty string. |
1151 | 0 | if (is<CharacterData>(*original_start_node)) |
1152 | 0 | TRY(static_cast<CharacterData&>(*original_start_node).replace_data(original_start_offset, original_start_node->length() - original_start_offset, String {})); |
1153 | | |
1154 | | // 8. For each node in nodes to remove, in tree order, remove node. |
1155 | 0 | for (auto& node : nodes_to_remove) |
1156 | 0 | node->remove(); |
1157 | | |
1158 | | // 9. If original end node is a CharacterData node, then replace data with node original end node, offset 0, count original end offset and data the empty string. |
1159 | 0 | if (is<CharacterData>(*original_end_node)) |
1160 | 0 | TRY(static_cast<CharacterData&>(*original_end_node).replace_data(0, original_end_offset, String {})); |
1161 | | |
1162 | | // 10. Set start and end to (new node, new offset). |
1163 | 0 | TRY(set_start(*new_node, new_offset)); |
1164 | 0 | TRY(set_end(*new_node, new_offset)); |
1165 | 0 | return {}; |
1166 | 0 | } |
1167 | | |
1168 | | // https://drafts.csswg.org/cssom-view/#dom-element-getclientrects |
1169 | | // https://drafts.csswg.org/cssom-view/#extensions-to-the-range-interface |
1170 | | JS::NonnullGCPtr<Geometry::DOMRectList> Range::get_client_rects() |
1171 | 0 | { |
1172 | | // 1. return an empty DOMRectList object if the range is not in the document |
1173 | 0 | if (!start_container()->document().navigable()) |
1174 | 0 | return Geometry::DOMRectList::create(realm(), {}); |
1175 | | |
1176 | 0 | start_container()->document().update_layout(); |
1177 | 0 | update_associated_selection(); |
1178 | 0 | Vector<JS::Handle<Geometry::DOMRect>> rects; |
1179 | | // FIXME: take Range collapsed into consideration |
1180 | | // 2. Iterate the node included in Range |
1181 | 0 | auto start_node = start_container(); |
1182 | 0 | auto end_node = end_container(); |
1183 | 0 | if (!is<DOM::Text>(start_node)) { |
1184 | 0 | start_node = start_node->child_at_index(m_start_offset); |
1185 | 0 | } |
1186 | 0 | if (!is<DOM::Text>(end_node)) { |
1187 | | // end offset shouldn't be 0 |
1188 | 0 | if (m_end_offset == 0) |
1189 | 0 | return Geometry::DOMRectList::create(realm(), {}); |
1190 | 0 | end_node = end_node->child_at_index(m_end_offset - 1); |
1191 | 0 | } |
1192 | 0 | for (Node const* node = start_node; node && node != end_node->next_in_pre_order(); node = node->next_in_pre_order()) { |
1193 | 0 | auto node_type = static_cast<NodeType>(node->node_type()); |
1194 | 0 | if (node_type == NodeType::ELEMENT_NODE) { |
1195 | | // 1. For each element selected by the range, whose parent is not selected by the range, include the border |
1196 | | // areas returned by invoking getClientRects() on the element. |
1197 | 0 | if (contains_node(*node) && !contains_node(*node->parent())) { |
1198 | 0 | auto const& element = static_cast<DOM::Element const&>(*node); |
1199 | 0 | JS::NonnullGCPtr<Geometry::DOMRectList> const element_rects = element.get_client_rects(); |
1200 | 0 | for (u32 i = 0; i < element_rects->length(); i++) { |
1201 | 0 | auto rect = element_rects->item(i); |
1202 | 0 | rects.append(Geometry::DOMRect::create(realm(), |
1203 | 0 | Gfx::FloatRect(rect->x(), rect->y(), rect->width(), rect->height()))); |
1204 | 0 | } |
1205 | 0 | } |
1206 | 0 | } else if (node_type == NodeType::TEXT_NODE) { |
1207 | | // 2. For each Text node selected or partially selected by the range (including when the boundary-points |
1208 | | // are identical), include scaled DOMRect object (for the part that is selected, not the whole line box). |
1209 | 0 | auto const& text = static_cast<DOM::Text const&>(*node); |
1210 | 0 | auto const* paintable = text.paintable(); |
1211 | 0 | if (paintable) { |
1212 | 0 | auto const* containing_block = paintable->containing_block(); |
1213 | 0 | if (is<Painting::PaintableWithLines>(*containing_block)) { |
1214 | 0 | auto const& paintable_lines = static_cast<Painting::PaintableWithLines const&>(*containing_block); |
1215 | 0 | auto fragments = paintable_lines.fragments(); |
1216 | 0 | auto const& font = paintable->layout_node().first_available_font(); |
1217 | 0 | for (auto frag = fragments.begin(); frag != fragments.end(); frag++) { |
1218 | 0 | auto rect = frag->range_rect(font, *this); |
1219 | 0 | if (rect.is_empty()) |
1220 | 0 | continue; |
1221 | 0 | rects.append(Geometry::DOMRect::create(realm(), |
1222 | 0 | Gfx::FloatRect(rect))); |
1223 | 0 | } |
1224 | 0 | } else { |
1225 | 0 | dbgln("FIXME: Failed to get client rects for node {}", node->debug_description()); |
1226 | 0 | } |
1227 | 0 | } |
1228 | 0 | } |
1229 | 0 | } |
1230 | 0 | return Geometry::DOMRectList::create(realm(), move(rects)); |
1231 | 0 | } |
1232 | | |
1233 | | // https://w3c.github.io/csswg-drafts/cssom-view/#dom-range-getboundingclientrect |
1234 | | JS::NonnullGCPtr<Geometry::DOMRect> Range::get_bounding_client_rect() |
1235 | 0 | { |
1236 | | // 1. Let list be the result of invoking getClientRects() on element. |
1237 | 0 | auto list = get_client_rects(); |
1238 | | |
1239 | | // 2. If the list is empty return a DOMRect object whose x, y, width and height members are zero. |
1240 | 0 | if (list->length() == 0) |
1241 | 0 | return Geometry::DOMRect::construct_impl(realm(), 0, 0, 0, 0).release_value_but_fixme_should_propagate_errors(); |
1242 | | |
1243 | | // 3. If all rectangles in list have zero width or height, return the first rectangle in list. |
1244 | 0 | auto all_rectangle_has_zero_width_or_height = true; |
1245 | 0 | for (auto i = 0u; i < list->length(); ++i) { |
1246 | 0 | auto const& rect = list->item(i); |
1247 | 0 | if (rect->width() != 0 && rect->height() != 0) { |
1248 | 0 | all_rectangle_has_zero_width_or_height = false; |
1249 | 0 | break; |
1250 | 0 | } |
1251 | 0 | } |
1252 | 0 | if (all_rectangle_has_zero_width_or_height) |
1253 | 0 | return JS::NonnullGCPtr { *const_cast<Geometry::DOMRect*>(list->item(0)) }; |
1254 | | |
1255 | | // 4. Otherwise, return a DOMRect object describing the smallest rectangle that includes all of the rectangles in |
1256 | | // list of which the height or width is not zero. |
1257 | 0 | auto const* first_rect = list->item(0); |
1258 | 0 | auto bounding_rect = Gfx::Rect { first_rect->x(), first_rect->y(), first_rect->width(), first_rect->height() }; |
1259 | 0 | for (auto i = 1u; i < list->length(); ++i) { |
1260 | 0 | auto const& rect = list->item(i); |
1261 | 0 | if (rect->width() == 0 || rect->height() == 0) |
1262 | 0 | continue; |
1263 | 0 | bounding_rect = bounding_rect.united({ rect->x(), rect->y(), rect->width(), rect->height() }); |
1264 | 0 | } |
1265 | 0 | return Geometry::DOMRect::create(realm(), bounding_rect.to_type<float>()); |
1266 | 0 | } |
1267 | | |
1268 | | // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-range-createcontextualfragment |
1269 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<DocumentFragment>> Range::create_contextual_fragment(String const& string) |
1270 | 0 | { |
1271 | | // FIXME: 1. Let compliantString be the result of invoking the Get Trusted Type compliant string algorithm with TrustedHTML, this's relevant global object, string, "Range createContextualFragment", and "script". |
1272 | | |
1273 | | // 2. Let node be this's start node. |
1274 | 0 | JS::NonnullGCPtr<Node> node = *start_container(); |
1275 | | |
1276 | | // 3. Let element be null. |
1277 | 0 | JS::GCPtr<Element> element = nullptr; |
1278 | |
|
1279 | 0 | auto node_type = static_cast<NodeType>(node->node_type()); |
1280 | | // 4. If node implements Element, set element to node. |
1281 | 0 | if (node_type == NodeType::ELEMENT_NODE) |
1282 | 0 | element = static_cast<DOM::Element&>(*node); |
1283 | | // 5. Otherwise, if node implements Text or Comment node, set element to node's parent element. |
1284 | 0 | else if (first_is_one_of(node_type, NodeType::TEXT_NODE, NodeType::COMMENT_NODE)) |
1285 | 0 | element = node->parent_element(); |
1286 | | |
1287 | | // 6. If either element is null or all of the following are true: |
1288 | | // - element's node document is an HTML document, |
1289 | | // - element's local name is "html"; and |
1290 | | // - element's namespace is the HTML namespace; |
1291 | 0 | if (!element || is<HTML::HTMLHtmlElement>(*element)) { |
1292 | | // then set element to the result of creating an element given this's node document, |
1293 | | // body, and the HTML namespace. |
1294 | 0 | element = TRY(DOM::create_element(node->document(), HTML::TagNames::body, Namespace::HTML)); |
1295 | 0 | } |
1296 | | |
1297 | | // 7. Let fragment node be the result of invoking the fragment parsing algorithm steps with element and compliantString. FIXME: Use compliantString. |
1298 | 0 | auto fragment_node = TRY(element->parse_fragment(string)); |
1299 | | |
1300 | | // 8. For each script of fragment node's script element descendants: |
1301 | 0 | fragment_node->for_each_in_subtree_of_type<HTML::HTMLScriptElement>([&](HTML::HTMLScriptElement& script_element) { |
1302 | | // 8.1 Set scripts already started to false. |
1303 | 0 | script_element.unmark_as_already_started({}); |
1304 | | // 8.2 Set scripts parser document to null. |
1305 | 0 | script_element.unmark_as_parser_inserted({}); |
1306 | 0 | return TraversalDecision::Continue; |
1307 | 0 | }); |
1308 | | |
1309 | | // 5. Return fragment node. |
1310 | 0 | return fragment_node; |
1311 | 0 | } |
1312 | | |
1313 | | void Range::increase_start_offset(Badge<Node>, WebIDL::UnsignedLong count) |
1314 | 0 | { |
1315 | 0 | m_start_offset += count; |
1316 | 0 | } |
1317 | | |
1318 | | void Range::increase_end_offset(Badge<Node>, WebIDL::UnsignedLong count) |
1319 | 0 | { |
1320 | 0 | m_end_offset += count; |
1321 | 0 | } |
1322 | | |
1323 | | void Range::decrease_start_offset(Badge<Node>, WebIDL::UnsignedLong count) |
1324 | 0 | { |
1325 | 0 | m_start_offset -= count; |
1326 | 0 | } |
1327 | | |
1328 | | void Range::decrease_end_offset(Badge<Node>, WebIDL::UnsignedLong count) |
1329 | 0 | { |
1330 | 0 | m_end_offset -= count; |
1331 | 0 | } |
1332 | | |
1333 | | } |