001package org.jsoup.nodes;
002
003import org.jsoup.internal.LineMap;
004import org.jsoup.internal.StringUtil;
005
006import java.util.Arrays;
007import java.util.Objects;
008
009/**
010 A Range tracks the source offsets where a Node starts or ends. Line and column coordinates are derived from the
011 line map retained during parsing. To track these positions, enable {@link org.jsoup.parser.Parser#setTrackPosition(boolean)}
012 before parsing.
013 @see Node#sourceRange()
014 @since 1.15.2
015 */
016public class Range {
017    // sentinels
018    private static final LineMap UnsetLineMap  = new LineMap();
019    private static final int[] UnsetAttrRanges = new int[0];
020    private static final Position UntrackedPos = new Position(-1, -1, -1);
021    private static final Range Untracked       = new Range();
022
023    private final LineMap lineMap;
024    private final int startPos;
025    private final int endPos;
026
027    /**
028     Creates the untracked source range sentinel.
029     */
030    private Range() {
031        lineMap = UnsetLineMap;
032        startPos = -1;
033        endPos = -1;
034    }
035
036    /**
037     Creates a new Range from source offsets.
038     */
039    private Range(LineMap lineMap, int startPos, int endPos) {
040        this.lineMap = lineMap;
041        if (startPos < 0 || endPos < 0)
042            throw new IllegalArgumentException("Range positions must be non-negative");
043        this.startPos = startPos;
044        this.endPos = endPos;
045    }
046
047    /**
048     Get the start position of this range, with 1-based line and column coordinates.
049     * @return the start position.
050     */
051    public Position start() {
052        return startPos == -1 ? UntrackedPos : position(startPos);
053    }
054
055    /**
056     Get the starting source offset of this range.
057     @return the 0-based start source offset.
058     @since 1.17.1
059     */
060    public int startPos() {
061        return startPos;
062    }
063
064    /**
065     Get the end position of this range, with 1-based line and column coordinates.
066     * @return the end position.
067     */
068    public Position end() {
069        return endPos == -1 ? UntrackedPos : position(endPos);
070    }
071
072    /**
073     Get the ending source offset of this range.
074     @return the 0-based ending source offset.
075     @since 1.17.1
076     */
077    public int endPos() {
078        return endPos;
079    }
080
081    /**
082     Test if this range has source offsets available.
083     * @return true if this range has source offsets, false otherwise (and all fields will be {@code -1}).
084     */
085    public boolean isTracked() {
086        return startPos != -1;
087    }
088
089    /**
090     Checks if the range represents a node that was implicitly created / closed.
091     <p>For example, with HTML of {@code <p>One<p>Two}, both {@code p} elements will have an explicit
092     {@link Element#sourceRange()} but an implicit {@link Element#endSourceRange()} marking the end position, as neither
093     have closing {@code </p>} tags. The TextNodes will have explicit sourceRanges.
094     <p>A range is considered implicit if its start and end positions are the same.
095     @return true if the range is tracked and its start and end positions are the same, false otherwise.
096     @since 1.17.1
097     */
098    public boolean isImplicit() {
099        return isTracked() && startPos == endPos;
100    }
101
102    /**
103     Creates a Position from a source offset and this Range's line map.
104     */
105    private Position position(int pos) {
106        return new Position(pos, lineMap.lineNumber(pos), lineMap.columnNumber(pos));
107    }
108
109    /**
110     Retrieves the start source range for a given Node.
111     * @param node the node to retrieve the position for
112     * @return the Range, or the Untracked (-1) position if tracking is disabled.
113     */
114    static Range ofStart(Node node) {
115        Range.Spans rangeSpans = node.spans();
116        return rangeSpans != null ? rangeSpans.sourceRange() : Untracked;
117    }
118
119    /**
120     Retrieves the end source range for a given Element.
121     * @param element the element to retrieve the end tag position for
122     * @return the Range, or the Untracked (-1) position if tracking is disabled.
123     */
124    static Range ofEnd(Element element) {
125        Range.Spans rangeSpans = element.spans();
126        return rangeSpans != null ? rangeSpans.endSourceRange() : Untracked;
127    }
128
129    @Override
130    public boolean equals(Object o) {
131        if (this == o) return true;
132        if (o == null || getClass() != o.getClass()) return false;
133
134        Range range = (Range) o;
135
136        return startPos == range.startPos && endPos == range.endPos;
137    }
138
139    @Override
140    public int hashCode() {
141        int result = startPos;
142        result = 31 * result + endPos;
143        return result;
144    }
145
146    /**
147     Gets a String representation of this Range, in the format {@code line,column:pos-line,column:pos}.
148     * @return a String
149     */
150    @Override
151    public String toString() {
152        StringBuilder sb = StringUtil.borrowBuilder()
153            .append(start())
154            .append('-')
155            .append(end());
156        return StringUtil.releaseBuilder(sb);
157    }
158
159    /**
160     A Position describes a source offset and its line and column coordinates. Positions are available when position
161     tracking is enabled with {@link org.jsoup.parser.Parser#setTrackPosition(boolean)} before parsing.
162     @see Node#sourceRange()
163     */
164    public static class Position {
165        private final int pos, lineNumber, columnNumber;
166
167        /**
168         Creates a position from its source offset and coordinates.
169         */
170        private Position(int pos, int lineNumber, int columnNumber) {
171            this.pos = pos;
172            this.lineNumber = lineNumber;
173            this.columnNumber = columnNumber;
174        }
175
176        /**
177         Gets the position index (0-based) of the original input source that this Position was read at. This tracks the
178         total number of characters read into the source at this position, regardless of the number of preceding lines.
179         * @return the position, or {@code -1} if untracked.
180         */
181        public int pos() {
182            return pos;
183        }
184
185        /**
186         Gets the line number (1-based) of the original input source that this Position was read at.
187         * @return the line number, or {@code -1} if untracked.
188         */
189        public int lineNumber() {
190            return lineNumber;
191        }
192
193        /**
194         Gets the cursor number (1-based) of the original input source that this Position was read at. The cursor number
195         resets to 1 on every new line.
196         * @return the cursor number, or {@code -1} if untracked.
197         */
198        public int columnNumber() {
199            return columnNumber;
200        }
201
202        /**
203         Test if this position was tracked during parsing.
204         * @return true if this was tracked during parsing, false otherwise (and all fields will be {@code -1}).
205         */
206        public boolean isTracked() {
207            return pos != -1;
208        }
209
210        /**
211         Gets a String presentation of this Position, in the format {@code line,column:pos}.
212         * @return a String
213         */
214        @Override
215        public String toString() {
216            StringBuilder sb = StringUtil.borrowBuilder()
217                .append(lineNumber)
218                .append(',')
219                .append(columnNumber)
220                .append(':')
221                .append(pos);
222            return StringUtil.releaseBuilder(sb);
223        }
224
225        @Override
226        public boolean equals(Object o) {
227            if (this == o) return true;
228            if (o == null || getClass() != o.getClass()) return false;
229            Position position = (Position) o;
230            if (pos != position.pos) return false;
231            if (lineNumber != position.lineNumber) return false;
232            return columnNumber == position.columnNumber;
233        }
234
235        @Override
236        public int hashCode() {
237            return Objects.hash(pos, lineNumber, columnNumber);
238        }
239    }
240
241    public static class AttributeRange {
242        static final AttributeRange UntrackedAttr = new AttributeRange();
243
244        private final LineMap lineMap;
245        private final int nameStartPos, nameEndPos, valueStartPos, valueEndPos;
246
247        /**
248         Creates the untracked attribute source range sentinel.
249         */
250        private AttributeRange() {
251            lineMap         = UnsetLineMap;
252            nameStartPos    = -1;
253            nameEndPos      = -1;
254            valueStartPos   = -1;
255            valueEndPos     = -1;
256        }
257
258        /**
259         Creates a new AttributeRange from source offsets.
260         */
261        private AttributeRange(LineMap lineMap, int nameStartPos, int nameEndPos, int valueStartPos, int valueEndPos) {
262            this.lineMap = lineMap;
263            if (nameStartPos < 0 || nameEndPos < 0 || valueStartPos < 0 || valueEndPos < 0)
264                throw new IllegalArgumentException("Attribute range positions must be non-negative");
265            this.nameStartPos = nameStartPos;
266            this.nameEndPos = nameEndPos;
267            this.valueStartPos = valueStartPos;
268            this.valueEndPos = valueEndPos;
269        }
270
271        /** Get the source range for the attribute's name. */
272        public Range nameRange() {
273            return isTracked() ? new Range(lineMap, nameStartPos, nameEndPos) : Range.Untracked;
274        }
275
276        /** Get the source range for the attribute's value. */
277        public Range valueRange() {
278            return isTracked() ? new Range(lineMap, valueStartPos, valueEndPos) : Range.Untracked;
279        }
280
281        /**
282         Tests if this attribute range has tracked name and value offsets.
283         * @return true if the attribute's name and value ranges were tracked; false otherwise.
284         @since 1.23.1
285         */
286        public boolean isTracked() {
287            return nameStartPos != -1;
288        }
289
290        /**
291         Get a String representation of this Attribute range, in the form
292         {@code line,column:pos-line,column:pos=line,column:pos-line,column:pos} (name start - name end = val start - val end)
293         */
294        @Override
295        public String toString() {
296            StringBuilder sb = StringUtil.borrowBuilder()
297                    .append(nameRange())
298                    .append('=')
299                    .append(valueRange());
300            return StringUtil.releaseBuilder(sb);
301        }
302
303        @Override public boolean equals(Object o) {
304            if (this == o) return true;
305            if (o == null || getClass() != o.getClass()) return false;
306
307            AttributeRange that = (AttributeRange) o;
308
309            if (nameStartPos    != that.nameStartPos) return false;
310            if (nameEndPos      != that.nameEndPos) return false;
311            if (valueStartPos   != that.valueStartPos) return false;
312            return valueEndPos  == that.valueEndPos;
313        }
314
315        @Override public int hashCode() {
316            int result = nameStartPos;
317            result = 31 * result + nameEndPos;
318            result = 31 * result + valueStartPos;
319            result = 31 * result + valueEndPos;
320            return result;
321        }
322    }
323
324    /**
325     Internal range span storage attached to a Node or Attributes object.
326     <p>Unset records use {@code -1}; once written, a node, end-tag, or attribute range record is complete.</p>
327     */
328    static final class Spans {
329        private static final int AttrRangeWidth = 4;
330
331        private LineMap lineMap     = UnsetLineMap;
332        private int nodeStartPos    = -1;
333        private int nodeEndPos      = -1;
334        private int endTagStartPos  = -1;
335        private int endTagEndPos    = -1;
336        private int[] attrRanges    = UnsetAttrRanges;
337
338        /**
339         Gets the node start source range.
340         */
341        private Range sourceRange() {
342            return range(nodeStartPos, nodeEndPos);
343        }
344
345        /**
346         Gets the element end tag source range.
347         */
348        private Range endSourceRange() {
349            return range(endTagStartPos, endTagEndPos);
350        }
351
352        /**
353         Sets the node start source range.
354         */
355        void sourceRange(LineMap lineMap, int startPos, int endPos) {
356            useLineMap(lineMap);
357            nodeStartPos = startPos;
358            nodeEndPos = endPos;
359        }
360
361        /**
362         Sets the element end tag source range.
363         */
364        void endSourceRange(LineMap lineMap, int startPos, int endPos) {
365            useLineMap(lineMap);
366            endTagStartPos = startPos;
367            endTagEndPos = endPos;
368        }
369
370        /** Clears the end-tag range when a parser-created element is recreated from an earlier start token. */
371        void clearEndSourceRange() {
372            endTagStartPos = -1;
373            endTagEndPos = -1;
374        }
375
376        /**
377         Gets the source ranges for an attribute slot.
378         */
379        Range.AttributeRange attributeRange(int index) {
380            if (index < 0)
381                return Range.AttributeRange.UntrackedAttr;
382
383            int[] ranges = attrRanges;
384            int nameIndex = attrNameStartIndex(index);
385            int valueIndex = attrValueStartIndex(index);
386            int valueEndIndex = valueIndex + 1;
387            if (valueEndIndex >= ranges.length)
388                return Range.AttributeRange.UntrackedAttr;
389
390            int nameStart = ranges[nameIndex];
391            int nameEnd = ranges[nameIndex + 1];
392            int valueStart = ranges[valueIndex];
393            int valueEnd = ranges[valueEndIndex];
394            if (nameStart == -1)
395                return Range.AttributeRange.UntrackedAttr;
396
397            return new Range.AttributeRange(lineMap, nameStart, nameEnd, valueStart, valueEnd);
398        }
399
400        /**
401         Sets the source ranges for an attribute slot.
402         */
403        void attributeRange(int index, Range.AttributeRange range) {
404            attributeRange(
405                index,
406                range.lineMap,
407                range.nameStartPos,
408                range.nameEndPos,
409                range.valueStartPos,
410                range.valueEndPos
411            );
412        }
413
414        /**
415         Sets source range offsets for an attribute slot.
416         */
417        void attributeRange(int index, LineMap lineMap, int nameStart, int nameEnd, int valueStart, int valueEnd) {
418            if (nameStart < 0 || nameEnd < 0 || valueStart < 0 || valueEnd < 0)
419                throw new IllegalArgumentException("Attribute range positions must be non-negative");
420            useLineMap(lineMap);
421            int nameIndex = attrNameStartIndex(index);
422            int valueIndex = attrValueStartIndex(index);
423            ensureAttributeCapacity(valueIndex + 2);
424            attrRanges[nameIndex] = nameStart;
425            attrRanges[nameIndex + 1] = nameEnd;
426            attrRanges[valueIndex] = valueStart;
427            attrRanges[valueIndex + 1] = valueEnd;
428        }
429
430        /** Rebases locally parsed attribute ranges into their containing source. */
431        void rebaseAttributeRanges(Range sourceRange, int offset) {
432            this.lineMap = sourceRange.lineMap;
433            for (int i = 0; i < attrRanges.length; i++) {
434                if (attrRanges[i] >= 0)
435                    attrRanges[i] += offset;
436            }
437        }
438
439        /** Copies attribute ranges while retaining this node's other ranges. */
440        void copyAttributeRanges(Spans source) {
441            if (source.attrRanges.length == 0)
442                return;
443            useLineMap(source.lineMap);
444            attrRanges = source.attrRanges.clone();
445        }
446
447        /**
448         Retains the first line map and rejects mixed-source ranges.
449         */
450        private void useLineMap(LineMap lineMap) {
451            if (this.lineMap == UnsetLineMap) {
452                this.lineMap = lineMap;
453            } else if (this.lineMap != lineMap) {
454                throw new IllegalArgumentException("Source ranges must come from the same parse");
455            }
456        }
457
458        /**
459         Removes an attribute slot and shifts following source ranges.
460         */
461        void removeAttributeRange(int index) {
462            if (index < 0) return;
463            int[] ranges = attrRanges;
464            int removeIndex = attrNameStartIndex(index);
465            if (removeIndex >= ranges.length) return;
466
467            int nextIndex = removeIndex + AttrRangeWidth;
468            int shifted = ranges.length - nextIndex;
469            if (shifted > 0)
470                System.arraycopy(ranges, nextIndex, ranges, removeIndex, shifted);
471            Arrays.fill(ranges, ranges.length - AttrRangeWidth, ranges.length, -1);
472        }
473
474        /**
475         Returns a copy whose source range arrays can mutate independently.
476         */
477        Spans copy() {
478            Spans copy = new Spans();
479            copy.lineMap = lineMap;
480            copy.nodeStartPos = nodeStartPos;
481            copy.nodeEndPos = nodeEndPos;
482            copy.endTagStartPos = endTagStartPos;
483            copy.endTagEndPos = endTagEndPos;
484            copy.attrRanges = attrRanges.length == 0 ? UnsetAttrRanges : attrRanges.clone();
485            return copy;
486        }
487
488        /**
489         Grows attribute range storage to hold the requested slot count.
490         */
491        private void ensureAttributeCapacity(int minLength) {
492            if (attrRanges.length >= minLength) return;
493            int oldLength = attrRanges.length;
494            attrRanges = Arrays.copyOf(attrRanges, minLength);
495            Arrays.fill(attrRanges, oldLength, attrRanges.length, -1);
496        }
497
498        /**
499         Creates a Range from stored offsets.
500         */
501        private Range range(int startPos, int endPos) {
502            if (startPos == -1)
503                return Range.Untracked;
504            return new Range(lineMap, startPos, endPos);
505        }
506
507        /**
508         Maps an attribute slot to its stored name range start slot.
509         */
510        private static int attrNameStartIndex(int index) {
511            return index * AttrRangeWidth;
512        }
513
514        /**
515         Maps an attribute slot to its stored value range start slot.
516         */
517        private static int attrValueStartIndex(int index) {
518            return index * AttrRangeWidth + 2;
519        }
520
521        @Override public boolean equals(Object o) {
522            if (this == o) return true;
523            if (o == null || getClass() != o.getClass()) return false;
524            Spans spans = (Spans) o;
525            return nodeStartPos == spans.nodeStartPos &&
526                nodeEndPos == spans.nodeEndPos &&
527                endTagStartPos == spans.endTagStartPos &&
528                endTagEndPos == spans.endTagEndPos &&
529                Arrays.equals(attrRanges, spans.attrRanges);
530        }
531
532        @Override public int hashCode() {
533            int result = Objects.hash(nodeStartPos, nodeEndPos, endTagStartPos, endTagEndPos);
534            result = 31 * result + Arrays.hashCode(attrRanges);
535            return result;
536        }
537    }
538}