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