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