View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript.host.dom;
16  
17  import java.util.HashSet;
18  
19  import org.apache.commons.logging.LogFactory;
20  import org.htmlunit.SgmlPage;
21  import org.htmlunit.WebClient;
22  import org.htmlunit.html.DomDocumentFragment;
23  import org.htmlunit.html.DomNode;
24  import org.htmlunit.html.impl.SimpleRange;
25  import org.htmlunit.javascript.HtmlUnitScriptable;
26  import org.htmlunit.javascript.JavaScriptEngine;
27  import org.htmlunit.javascript.configuration.JsxClass;
28  import org.htmlunit.javascript.configuration.JsxConstant;
29  import org.htmlunit.javascript.configuration.JsxConstructor;
30  import org.htmlunit.javascript.configuration.JsxFunction;
31  import org.htmlunit.javascript.configuration.JsxGetter;
32  import org.htmlunit.javascript.host.DOMRect;
33  import org.htmlunit.javascript.host.DOMRectList;
34  import org.htmlunit.javascript.host.html.HTMLElement;
35  
36  /**
37   * The JavaScript object that represents a Range.
38   *
39   * @see <a href="http://www.xulplanet.com/references/objref/Range.html">XULPlanet</a>
40   * @see <a href="http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html">DOM-Level-2-Traversal-Range</a>
41   * @author Marc Guillemot
42   * @author Ahmed Ashour
43   * @author Daniel Gredler
44   * @author James Phillpotts
45   * @author Ronald Brill
46   */
47  @JsxClass
48  public class Range extends AbstractRange {
49  
50      /** Comparison mode for compareBoundaryPoints. */
51      @JsxConstant
52      public static final int START_TO_START = 0;
53  
54      /** Comparison mode for compareBoundaryPoints. */
55      @JsxConstant
56      public static final int START_TO_END = 1;
57  
58      /** Comparison mode for compareBoundaryPoints. */
59      @JsxConstant
60      public static final int END_TO_END = 2;
61  
62      /** Comparison mode for compareBoundaryPoints. */
63      @JsxConstant
64      public static final int END_TO_START = 3;
65  
66      /**
67       * Creates an instance.
68       */
69      public Range() {
70          super();
71      }
72  
73      /**
74       * JavaScript constructor.
75       */
76      @Override
77      @JsxConstructor
78      public void jsConstructor() {
79          super.jsConstructor();
80      }
81  
82      /**
83       * Creates a new instance.
84       * @param document the HTML document creating the range
85       */
86      public Range(final Document document) {
87          super(document, document, 0, 0);
88      }
89  
90      Range(final SimpleRange simpleRange) {
91          super(simpleRange.getStartContainer().getScriptableObject(),
92                  simpleRange.getEndContainer().getScriptableObject(),
93                  simpleRange.getStartOffset(),
94                  simpleRange.getEndOffset());
95      }
96  
97      /**
98       * Sets the attributes describing the start of a Range.
99       * @param refNode the reference node
100      * @param offset the offset value within the node
101      */
102     @JsxFunction
103     public void setStart(final Node refNode, final int offset) {
104         if (refNode == null) {
105             throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setStart() with a null node.");
106         }
107         internSetStartContainer(refNode);
108         internSetStartOffset(offset);
109     }
110 
111     /**
112      * Sets the start of the range to be after the node.
113      * @param refNode the reference node
114      */
115     @JsxFunction
116     public void setStartAfter(final Node refNode) {
117         if (refNode == null) {
118             throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setStartAfter() with a null node.");
119         }
120         internSetStartContainer(refNode.getParent());
121         internSetStartOffset(getPositionInContainer(refNode) + 1);
122     }
123 
124     /**
125      * Sets the start of the range to be before the node.
126      * @param refNode the reference node
127      */
128     @JsxFunction
129     public void setStartBefore(final Node refNode) {
130         if (refNode == null) {
131             throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setStartBefore() with a null node.");
132         }
133         internSetStartContainer(refNode.getParent());
134         internSetStartOffset(getPositionInContainer(refNode));
135     }
136 
137     private static int getPositionInContainer(final Node refNode) {
138         int i = 0;
139         Node node = refNode;
140         while (node.getPreviousSibling() != null) {
141             node = node.getPreviousSibling();
142             i++;
143         }
144         return i;
145     }
146 
147     /**
148      * Sets the attributes describing the end of a Range.
149      * @param refNode the reference node
150      * @param offset the offset value within the node
151      */
152     @JsxFunction
153     public void setEnd(final Node refNode, final int offset) {
154         if (refNode == null) {
155             throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setEnd() with a null node.");
156         }
157         internSetEndContainer(refNode);
158         internSetEndOffset(offset);
159     }
160 
161     /**
162      * Sets the end of the range to be after the node.
163      * @param refNode the reference node
164      */
165     @JsxFunction
166     public void setEndAfter(final Node refNode) {
167         if (refNode == null) {
168             throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setEndAfter() with a null node.");
169         }
170         internSetEndContainer(refNode.getParent());
171         internSetEndOffset(getPositionInContainer(refNode) + 1);
172     }
173 
174     /**
175      * Sets the end of the range to be before the node.
176      * @param refNode the reference node
177      */
178     @JsxFunction
179     public void setEndBefore(final Node refNode) {
180         if (refNode == null) {
181             throw JavaScriptEngine.reportRuntimeError("It is illegal to call Range.setEndBefore() with a null node.");
182         }
183         internSetStartContainer(refNode.getParent());
184         internSetStartOffset(getPositionInContainer(refNode));
185     }
186 
187     /**
188      * Select the contents within a node.
189      * @param refNode Node to select from
190      */
191     @JsxFunction
192     public void selectNodeContents(final Node refNode) {
193         internSetStartContainer(refNode);
194         internSetStartOffset(0);
195         internSetEndContainer(refNode);
196         internSetEndOffset(refNode.getChildNodes().getLength());
197     }
198 
199     /**
200      * Selects a node and its contents.
201      * @param refNode the node to select
202      */
203     @JsxFunction
204     public void selectNode(final Node refNode) {
205         setStartBefore(refNode);
206         setEndAfter(refNode);
207     }
208 
209     /**
210      * Collapse a Range onto one of its boundaries.
211      * @param toStart if {@code true}, collapses the Range onto its start; else collapses it onto its end
212      */
213     @JsxFunction
214     public void collapse(final boolean toStart) {
215         if (toStart) {
216             internSetEndContainer(internGetStartContainer());
217             internSetEndOffset(internGetStartOffset());
218         }
219         else {
220             internSetStartContainer(internGetEndContainer());
221             internSetStartOffset(internGetEndOffset());
222         }
223     }
224 
225     /**
226      * Returns the deepest common ancestor container of the Range's two boundary points.
227      * @return the deepest common ancestor container of the Range's two boundary points
228      */
229     @JsxGetter
230     public Object getCommonAncestorContainer() {
231         final HashSet<Node> startAncestors = new HashSet<>();
232         Node ancestor = internGetStartContainer();
233         while (ancestor != null) {
234             startAncestors.add(ancestor);
235             ancestor = ancestor.getParent();
236         }
237 
238         ancestor = internGetEndContainer();
239         while (ancestor != null) {
240             if (startAncestors.contains(ancestor)) {
241                 return ancestor;
242             }
243             ancestor = ancestor.getParent();
244         }
245 
246         return JavaScriptEngine.UNDEFINED;
247     }
248 
249     /**
250      * Parses an HTML snippet.
251      * @param valueAsString text that contains text and tags to be converted to a document fragment
252      * @return a document fragment
253      * @see <a href="https://developer.mozilla.org/en-US/docs/DOM/range.createContextualFragment">Mozilla
254      *     documentation</a>
255      */
256     @JsxFunction
257     public HtmlUnitScriptable createContextualFragment(final String valueAsString) {
258         final SgmlPage page = internGetStartContainer().getDomNodeOrDie().getPage();
259         final DomDocumentFragment fragment = new DomDocumentFragment(page);
260         try {
261             final WebClient webClient = page.getWebClient();
262             webClient.getPageCreator().getHtmlParser()
263                     .parseFragment(webClient, fragment,
264                             internGetStartContainer().getDomNodeOrDie(), valueAsString, false);
265         }
266         catch (final Exception e) {
267             LogFactory.getLog(Range.class).error("Unexpected exception occurred in createContextualFragment", e);
268             throw JavaScriptEngine.reportRuntimeError("Unexpected exception occurred in createContextualFragment: "
269                     + e.getMessage());
270         }
271 
272         return fragment.getScriptableObject();
273     }
274 
275     /**
276      * Moves this range's contents from the document tree into a document fragment.
277      * @return the new document fragment containing the range contents
278      */
279     @JsxFunction
280     public HtmlUnitScriptable extractContents() {
281         try {
282             return getSimpleRange().extractContents().getScriptableObject();
283         }
284         catch (final IllegalStateException e) {
285             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
286         }
287     }
288 
289     /**
290      * Compares the boundary points of two Ranges.
291      * @param how a constant describing the comparison method
292      * @param sourceRange the Range to compare boundary points with this range
293      * @return -1, 0, or 1, indicating whether the corresponding boundary-point of range is respectively before,
294      *         equal to, or after the corresponding boundary-point of sourceRange.
295      */
296     @JsxFunction
297     public int compareBoundaryPoints(final int how, final Range sourceRange) {
298         final Node nodeForThis;
299         final int offsetForThis;
300         final int containingMoficator;
301         if (START_TO_START == how || END_TO_START == how) {
302             nodeForThis = internGetStartContainer();
303             offsetForThis = internGetStartOffset();
304             containingMoficator = 1;
305         }
306         else {
307             nodeForThis = internGetEndContainer();
308             offsetForThis = internGetEndOffset();
309             containingMoficator = -1;
310         }
311 
312         final Node nodeForOther;
313         final int offsetForOther;
314         if (START_TO_END == how || START_TO_START == how) {
315             nodeForOther = sourceRange.internGetStartContainer();
316             offsetForOther = sourceRange.internGetStartOffset();
317         }
318         else {
319             nodeForOther = sourceRange.internGetEndContainer();
320             offsetForOther = sourceRange.internGetEndOffset();
321         }
322 
323         if (nodeForThis == nodeForOther) {
324             if (offsetForThis < offsetForOther) {
325                 return -1;
326             }
327             else if (offsetForThis > offsetForOther) {
328                 return 1;
329             }
330             return 0;
331         }
332 
333         final byte nodeComparision = (byte) nodeForThis.compareDocumentPosition(nodeForOther);
334         if ((nodeComparision & Node.DOCUMENT_POSITION_CONTAINED_BY) != 0) {
335             return -1 * containingMoficator;
336         }
337         else if ((nodeComparision & Node.DOCUMENT_POSITION_PRECEDING) != 0) {
338             return -1;
339         }
340 
341         // TODO: handle other cases
342         return 1;
343     }
344 
345     /**
346      * Returns a clone of the range in a document fragment.
347      * @return a clone
348      */
349     @JsxFunction
350     public HtmlUnitScriptable cloneContents() {
351         try {
352             return getSimpleRange().cloneContents().getScriptableObject();
353         }
354         catch (final IllegalStateException e) {
355             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
356         }
357     }
358 
359     /**
360      * Deletes the contents of the range.
361      */
362     @JsxFunction
363     public void deleteContents() {
364         try {
365             getSimpleRange().deleteContents();
366         }
367         catch (final IllegalStateException e) {
368             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
369         }
370     }
371 
372     /**
373      * Inserts a new node at the beginning of the range. If the range begins at an offset, the node is split.
374      * @param newNode The node to insert
375      * @see <a href="https://developer.mozilla.org/en/DOM/range">https://developer.mozilla.org/en/DOM/range</a>
376      */
377     @JsxFunction
378     public void insertNode(final Node newNode) {
379         try {
380             getSimpleRange().insertNode(newNode.getDomNodeOrDie());
381         }
382         catch (final IllegalStateException e) {
383             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
384         }
385     }
386 
387     /**
388      * Surrounds the contents of the range in a new node.
389      * @param newNode The node to surround the range in
390      */
391     @JsxFunction
392     public void surroundContents(final Node newNode) {
393         try {
394             getSimpleRange().surroundContents(newNode.getDomNodeOrDie());
395         }
396         catch (final IllegalStateException e) {
397             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
398         }
399     }
400 
401     /**
402      * Returns a clone of the range.
403      * @return a clone of the range
404      */
405     @JsxFunction
406     public Range cloneRange() {
407         try {
408             return new Range(getSimpleRange().cloneRange());
409         }
410         catch (final IllegalStateException e) {
411             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
412         }
413     }
414 
415     /**
416      * Releases Range from use to improve performance.
417      */
418     @JsxFunction
419     public void detach() {
420         // Java garbage collection should take care of this for us
421     }
422 
423     /**
424      * Returns the text of the Range.
425      * @return the text
426      */
427     @JsxFunction(functionName = "toString")
428     public String jsToString() {
429         try {
430             return getSimpleRange().toString();
431         }
432         catch (final IllegalStateException e) {
433             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
434         }
435     }
436 
437     /**
438      * Retrieves a collection of rectangles that describes the layout of the contents of an object
439      * or range within the client. Each rectangle describes a single line.
440      * @return a collection of rectangles that describes the layout of the contents
441      */
442     @JsxFunction
443     public DOMRectList getClientRects() {
444         final DOMRectList rectList = new DOMRectList();
445         rectList.setParentScope(getParentScope());
446         rectList.setPrototype(getPrototype(rectList.getClass()));
447 
448         try {
449             // simple impl for now
450             for (final DomNode node : getSimpleRange().containedNodes()) {
451                 final HtmlUnitScriptable scriptable = node.getScriptableObject();
452                 if (scriptable instanceof HTMLElement) {
453                     final DOMRect rect = new DOMRect(0, 0, 1, 1);
454                     rect.setParentScope(getParentScope());
455                     rect.setPrototype(getPrototype(rect.getClass()));
456                     rectList.add(rect);
457                 }
458             }
459 
460             return rectList;
461         }
462         catch (final IllegalStateException e) {
463             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
464         }
465     }
466 
467     /**
468      * Returns an object that bounds the contents of the range.
469      * this a rectangle enclosing the union of the bounding rectangles for all the elements in the range.
470      * @return an object the bounds the contents of the range
471      */
472     @JsxFunction
473     public DOMRect getBoundingClientRect() {
474         final DOMRect rect = new DOMRect();
475         rect.setParentScope(getParentScope());
476         rect.setPrototype(getPrototype(rect.getClass()));
477 
478         try {
479             // simple impl for now
480             for (final DomNode node : getSimpleRange().containedNodes()) {
481                 final HtmlUnitScriptable scriptable = node.getScriptableObject();
482                 if (scriptable instanceof HTMLElement element) {
483                     final DOMRect childRect = element.getBoundingClientRect();
484                     rect.setY(Math.min(rect.getX(), childRect.getX()));
485                     rect.setY(Math.min(rect.getY(), childRect.getY()));
486                     rect.setWidth(Math.max(rect.getWidth(), childRect.getWidth()));
487                     rect.setHeight(Math.max(rect.getHeight(), childRect.getHeight()));
488                 }
489             }
490 
491             return rect;
492         }
493         catch (final IllegalStateException e) {
494             throw JavaScriptEngine.reportRuntimeError(e.getMessage());
495         }
496     }
497 }