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