1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.html.impl;
16
17 import java.io.Serializable;
18 import java.util.ArrayList;
19 import java.util.Collections;
20 import java.util.HashSet;
21 import java.util.Iterator;
22 import java.util.List;
23 import java.util.Objects;
24
25 import org.apache.commons.lang3.builder.EqualsBuilder;
26 import org.htmlunit.SgmlPage;
27 import org.htmlunit.html.DomDocumentFragment;
28 import org.htmlunit.html.DomNode;
29 import org.htmlunit.html.DomNodeList;
30 import org.htmlunit.html.DomText;
31 import org.w3c.dom.DOMException;
32 import org.w3c.dom.DocumentFragment;
33 import org.w3c.dom.Node;
34 import org.w3c.dom.NodeList;
35
36
37
38
39
40
41
42
43
44
45 public class SimpleRange implements Serializable {
46
47
48 private DomNode startContainer_;
49
50
51 private DomNode endContainer_;
52
53
54
55
56
57 private int startOffset_;
58
59
60
61
62
63 private int endOffset_;
64
65
66
67
68 public SimpleRange() {
69
70 }
71
72
73
74
75
76
77 public SimpleRange(final DomNode node) {
78 startContainer_ = node;
79 endContainer_ = node;
80 startOffset_ = 0;
81 endOffset_ = getMaxOffset(node);
82 }
83
84
85
86
87
88
89
90 public SimpleRange(final DomNode node, final int offset) {
91 startContainer_ = node;
92 endContainer_ = node;
93 startOffset_ = offset;
94 endOffset_ = offset;
95 }
96
97
98
99
100
101
102
103
104
105 public SimpleRange(final DomNode startNode, final int startOffset, final DomNode endNode, final int endOffset) {
106 startContainer_ = startNode;
107 endContainer_ = endNode;
108 startOffset_ = startOffset;
109 endOffset_ = endOffset;
110 if (startNode == endNode && startOffset > endOffset) {
111 endOffset_ = startOffset;
112 }
113 }
114
115
116
117
118
119
120 public DomDocumentFragment cloneContents() {
121
122 final DomNode ancestor = getCommonAncestorContainer();
123
124 if (ancestor == null) {
125 return new DomDocumentFragment(null);
126 }
127 final DomNode ancestorClone = ancestor.cloneNode(true);
128
129
130 DomNode startClone = null;
131 DomNode endClone = null;
132 final DomNode start = startContainer_;
133 final DomNode end = endContainer_;
134 if (start == ancestor) {
135 startClone = ancestorClone;
136 }
137 if (end == ancestor) {
138 endClone = ancestorClone;
139 }
140 final Iterable<DomNode> descendants = ancestor.getDescendants();
141 if (startClone == null || endClone == null) {
142 final Iterator<DomNode> i = descendants.iterator();
143 final Iterator<DomNode> ci = ancestorClone.getDescendants().iterator();
144 while (i.hasNext()) {
145 final DomNode e = i.next();
146 final DomNode ce = ci.next();
147 if (start == e) {
148 startClone = ce;
149 }
150 else if (end == e) {
151 endClone = ce;
152 break;
153 }
154 }
155 }
156
157
158
159
160 if (endClone == null) {
161 throw new IllegalStateException("Unable to find end node clone.");
162 }
163 deleteAfter(endClone, endOffset_);
164 for (DomNode n = endClone; n != null; n = n.getParentNode()) {
165 while (n.getNextSibling() != null) {
166 n.getNextSibling().remove();
167 }
168 }
169
170
171 if (startClone == null) {
172 throw new IllegalStateException("Unable to find start node clone.");
173 }
174 deleteBefore(startClone, startOffset_);
175 for (DomNode n = startClone; n != null; n = n.getParentNode()) {
176 while (n.getPreviousSibling() != null) {
177 n.getPreviousSibling().remove();
178 }
179 }
180
181 final SgmlPage page = ancestor.getPage();
182 final DomDocumentFragment fragment = new DomDocumentFragment(page);
183 if (start == end) {
184 fragment.appendChild(ancestorClone);
185 }
186 else {
187 for (final DomNode n : ancestorClone.getChildNodes()) {
188 fragment.appendChild(n);
189 }
190 }
191 return fragment;
192 }
193
194
195
196
197
198
199
200 public SimpleRange cloneRange() {
201 return new SimpleRange(startContainer_, startOffset_, endContainer_, endOffset_);
202 }
203
204
205
206
207
208 public void collapse(final boolean toStart) {
209 if (toStart) {
210 endContainer_ = startContainer_;
211 endOffset_ = startOffset_;
212 }
213 else {
214 startContainer_ = endContainer_;
215 startOffset_ = endOffset_;
216 }
217 }
218
219
220
221
222
223
224 public void deleteContents() {
225 final DomNode ancestor = getCommonAncestorContainer();
226 if (ancestor != null) {
227 deleteContents(ancestor);
228 }
229 }
230
231 private void deleteContents(final DomNode ancestor) {
232 final DomNode start;
233 if (isOffsetChars(startContainer_)) {
234 start = startContainer_;
235 String text = getText(start);
236 if (startOffset_ > -1 && startOffset_ < text.length()) {
237 text = text.substring(0, startOffset_);
238 }
239 setText(start, text);
240 }
241 else if (startContainer_.getChildNodes().getLength() > startOffset_) {
242 start = (DomNode) startContainer_.getChildNodes().item(startOffset_);
243 }
244 else {
245 start = startContainer_.getNextSibling();
246 }
247
248 final DomNode end;
249 if (isOffsetChars(endContainer_)) {
250 end = endContainer_;
251 String text = getText(end);
252 if (endOffset_ > -1 && endOffset_ < text.length()) {
253 text = text.substring(endOffset_);
254 }
255 setText(end, text);
256 }
257 else if (endContainer_.getChildNodes().getLength() > endOffset_) {
258 end = (DomNode) endContainer_.getChildNodes().item(endOffset_);
259 }
260 else {
261 end = endContainer_.getNextSibling();
262 }
263
264 boolean foundStart = false;
265 boolean started = false;
266 final Iterator<DomNode> i = ancestor.getDescendants().iterator();
267 while (i.hasNext()) {
268 final DomNode n = i.next();
269 if (n == end) {
270 break;
271 }
272 if (n == start) {
273 foundStart = true;
274 }
275 if (foundStart && (n != start || !isOffsetChars(startContainer_))) {
276 started = true;
277 }
278 if (started && !n.isAncestorOf(end)) {
279 i.remove();
280 }
281 }
282 }
283
284
285
286
287
288
289
290 public DomDocumentFragment extractContents() throws DOMException {
291 final DomDocumentFragment fragment = cloneContents();
292
293
294 deleteContents();
295
296
297 return fragment;
298 }
299
300
301
302
303
304
305
306
307 public boolean isCollapsed() throws DOMException {
308 return startContainer_ == endContainer_ && startOffset_ == endOffset_;
309 }
310
311
312
313
314
315
316
317
318 public DomNode getCommonAncestorContainer() throws DOMException {
319 final HashSet<DomNode> startAncestors = new HashSet<>();
320 DomNode ancestor = startContainer_;
321 while (ancestor != null) {
322 startAncestors.add(ancestor);
323 ancestor = ancestor.getParentNode();
324 }
325
326 ancestor = endContainer_;
327 while (ancestor != null) {
328 if (startAncestors.contains(ancestor)) {
329 return ancestor;
330 }
331 ancestor = ancestor.getParentNode();
332 }
333
334 return null;
335 }
336
337
338
339
340
341
342 public DomNode getEndContainer() {
343 return endContainer_;
344 }
345
346
347
348
349
350
351 public int getEndOffset() {
352 return endOffset_;
353 }
354
355
356
357
358
359
360 public DomNode getStartContainer() {
361 return startContainer_;
362 }
363
364
365
366
367
368
369 public int getStartOffset() {
370 return startOffset_;
371 }
372
373
374
375
376
377
378
379
380
381
382
383
384 public void insertNode(final DomNode newNode) {
385 if (isOffsetChars(startContainer_)) {
386 final DomNode split = startContainer_.cloneNode(false);
387 String text = getText(startContainer_);
388 if (startOffset_ > -1 && startOffset_ < text.length()) {
389 text = text.substring(0, startOffset_);
390 }
391 setText(startContainer_, text);
392 text = getText(split);
393 if (startOffset_ > -1 && startOffset_ < text.length()) {
394 text = text.substring(startOffset_);
395 }
396 setText(split, text);
397 insertNodeOrDocFragment(startContainer_.getParentNode(), split, startContainer_.getNextSibling());
398 insertNodeOrDocFragment(startContainer_.getParentNode(), newNode, split);
399 }
400 else {
401 insertNodeOrDocFragment(startContainer_, newNode,
402 (DomNode) startContainer_.getChildNodes().item(startOffset_));
403 }
404
405 setStart(newNode, 0);
406 }
407
408 private static void insertNodeOrDocFragment(final DomNode parent, final DomNode newNode, final DomNode refNode) {
409 if (newNode instanceof DocumentFragment fragment) {
410
411 final NodeList childNodes = fragment.getChildNodes();
412 while (childNodes.getLength() > 0) {
413 final Node item = childNodes.item(0);
414 parent.insertBefore(item, refNode);
415 }
416 }
417 else {
418 parent.insertBefore(newNode, refNode);
419 }
420 }
421
422
423
424
425
426 public void selectNode(final DomNode node) {
427 startContainer_ = node;
428 startOffset_ = 0;
429 endContainer_ = node;
430 endOffset_ = getMaxOffset(node);
431 }
432
433
434
435
436
437 public void selectNodeContents(final DomNode node) {
438 startContainer_ = node;
439 startOffset_ = 0;
440 endContainer_ = node;
441 endOffset_ = getMaxOffset(node);
442 }
443
444
445
446
447
448
449 public void setEnd(final DomNode refNode, final int offset) {
450 endContainer_ = refNode;
451 endOffset_ = offset;
452 }
453
454
455
456
457
458
459 public void setStart(final DomNode refNode, final int offset) {
460 startContainer_ = refNode;
461 startOffset_ = offset;
462 }
463
464
465
466
467
468
469 public void surroundContents(final DomNode newParent) {
470 newParent.appendChild(extractContents());
471 insertNode(newParent);
472 setStart(newParent, 0);
473 setEnd(newParent, getMaxOffset(newParent));
474 }
475
476
477
478
479 @Override
480 public boolean equals(final Object obj) {
481 if (!(obj instanceof SimpleRange other)) {
482 return false;
483 }
484 return new EqualsBuilder()
485 .append(startContainer_, other.startContainer_)
486 .append(endContainer_, other.endContainer_)
487 .append(startOffset_, other.startOffset_)
488 .append(endOffset_, other.endOffset_).isEquals();
489 }
490
491
492
493
494 @Override
495 public int hashCode() {
496 return Objects.hash(startContainer_, endContainer_, startOffset_, endOffset_);
497 }
498
499
500
501
502 @Override
503 public String toString() {
504 final DomDocumentFragment fragment = cloneContents();
505 if (fragment.getPage() != null) {
506 return fragment.asNormalizedText();
507 }
508 return "";
509 }
510
511 private static boolean isOffsetChars(final DomNode node) {
512 return node instanceof DomText || node instanceof SelectableTextInput;
513 }
514
515 private static String getText(final DomNode node) {
516 if (node instanceof SelectableTextInput input) {
517 return input.getText();
518 }
519 return node.getTextContent();
520 }
521
522 private static void setText(final DomNode node, final String text) {
523 if (node instanceof SelectableTextInput input) {
524 input.setText(text);
525 }
526 else {
527 node.setTextContent(text);
528 }
529 }
530
531 private static void deleteBefore(final DomNode node, int offset) {
532 if (isOffsetChars(node)) {
533 String text = getText(node);
534 if (offset > -1 && offset < text.length()) {
535 text = text.substring(offset);
536 }
537 else {
538 text = "";
539 }
540 setText(node, text);
541 }
542 else {
543 final DomNodeList<DomNode> children = node.getChildNodes();
544 for (int i = 0; i < offset && i < children.getLength(); i++) {
545 final DomNode child = children.get(i);
546 child.remove();
547 i--;
548 offset--;
549 }
550 }
551 }
552
553 private static void deleteAfter(final DomNode node, final int offset) {
554 if (isOffsetChars(node)) {
555 String text = getText(node);
556 if (offset > -1 && offset < text.length()) {
557 text = text.substring(0, offset);
558 setText(node, text);
559 }
560 }
561 else {
562 final DomNodeList<DomNode> children = node.getChildNodes();
563 for (int i = offset; i < children.getLength(); i++) {
564 final DomNode child = children.get(i);
565 child.remove();
566 i--;
567 }
568 }
569 }
570
571 private static int getMaxOffset(final DomNode node) {
572 return isOffsetChars(node) ? getText(node).length() : node.getChildNodes().getLength();
573 }
574
575
576
577
578
579
580 public List<DomNode> containedNodes() {
581 final DomNode ancestor = getCommonAncestorContainer();
582 if (ancestor == null) {
583 return Collections.emptyList();
584 }
585
586
587 if (startContainer_ == endContainer_ && isOffsetChars(startContainer_)) {
588 return Collections.singletonList(startContainer_);
589 }
590
591
592 final DomNode start;
593 if (isOffsetChars(startContainer_)) {
594 start = startContainer_;
595 }
596 else if (startContainer_.getChildNodes().getLength() > startOffset_) {
597 start = (DomNode) startContainer_.getChildNodes().item(startOffset_);
598 }
599 else {
600 start = startContainer_.getNextSibling();
601 }
602
603
604 final DomNode end;
605 if (isOffsetChars(endContainer_)) {
606 end = endContainer_;
607 }
608 else if (endContainer_.getChildNodes().getLength() > endOffset_) {
609 end = (DomNode) endContainer_.getChildNodes().item(endOffset_);
610 }
611 else {
612 end = endContainer_.getNextSibling();
613 }
614
615 boolean foundStart = false;
616 boolean started = false;
617 final List<DomNode> nodes = new ArrayList<>();
618 for (final DomNode n : ancestor.getDescendants()) {
619 if (n == end) {
620 break;
621 }
622 if (n == start) {
623 foundStart = true;
624 }
625 if (foundStart && (n != start || !isOffsetChars(startContainer_))) {
626 started = true;
627 }
628 if (started && !n.isAncestorOf(end)) {
629 nodes.add(n);
630 }
631 }
632 return nodes;
633 }
634 }