001package org.jsoup.nodes;
002
003import org.jsoup.internal.StringUtil;
004
005import java.util.Objects;
006
007import static org.jsoup.internal.SharedConstants.*;
008
009/**
010 A Range object tracks the character positions in the original input source where a Node starts or ends. If you want to
011 track these positions, tracking must be enabled in the Parser with
012 {@link org.jsoup.parser.Parser#setTrackPosition(boolean)}.
013 @see Node#sourceRange()
014 @since 1.15.2
015 */
016public class Range {
017    private static final Position UntrackedPos = new Position(-1, -1, -1);
018    private final Position start, end;
019
020    /** An untracked source range. */
021    static final Range Untracked = new Range(UntrackedPos, UntrackedPos);
022
023    /**
024     Creates a new Range with start and end Positions. Called by TreeBuilder when position tracking is on.
025     * @param start the start position
026     * @param end the end position
027     */
028    public Range(Position start, Position end) {
029        this.start = start;
030        this.end = end;
031    }
032
033    /**
034     Get the start position of this node.
035     * @return the start position
036     */
037    public Position start() {
038        return start;
039    }
040
041    /**
042     Get the starting cursor position of this range.
043     @return the 0-based start cursor position.
044     @since 1.17.1
045     */
046    public int startPos() {
047        return start.pos;
048    }
049
050    /**
051     Get the end position of this node.
052     * @return the end position
053     */
054    public Position end() {
055        return end;
056    }
057
058    /**
059     Get the ending cursor position of this range.
060     @return the 0-based ending cursor position.
061     @since 1.17.1
062     */
063    public int endPos() {
064        return end.pos;
065    }
066
067    /**
068     Test if this source range was tracked during parsing.
069     * @return true if this was tracked during parsing, false otherwise (and all fields will be {@code -1}).
070     */
071    public boolean isTracked() {
072        return this != Untracked;
073    }
074
075    /**
076     Checks if the range represents a node that was implicitly created / closed.
077     <p>For example, with HTML of {@code <p>One<p>Two}, both {@code p} elements will have an explicit
078     {@link Element#sourceRange()} but an implicit {@link Element#endSourceRange()} marking the end position, as neither
079     have closing {@code </p>} tags. The TextNodes will have explicit sourceRanges.
080     <p>A range is considered implicit if its start and end positions are the same.
081     @return true if the range is tracked and its start and end positions are the same, false otherwise.
082     @since 1.17.1
083     */
084    public boolean isImplicit() {
085        if (!isTracked()) return false;
086        return start.equals(end);
087    }
088
089    /**
090     Retrieves the source range for a given Node.
091     * @param node the node to retrieve the position for
092     * @param start if this is the starting range. {@code false} for Element end tags.
093     * @return the Range, or the Untracked (-1) position if tracking is disabled.
094     */
095    static Range of(Node node, boolean start) {
096        final String key = start ? RangeKey : EndRangeKey;
097        if (!node.hasAttributes()) return Untracked;
098        Object range = node.attributes().userData(key);
099        return range != null ? (Range) range : Untracked;
100    }
101
102    @Override
103    public boolean equals(Object o) {
104        if (this == o) return true;
105        if (o == null || getClass() != o.getClass()) return false;
106
107        Range range = (Range) o;
108
109        if (!start.equals(range.start)) return false;
110        return end.equals(range.end);
111    }
112
113    @Override
114    public int hashCode() {
115        return Objects.hash(start, end);
116    }
117
118    /**
119     Gets a String presentation of this Range, in the format {@code line,column:pos-line,column:pos}.
120     * @return a String
121     */
122    @Override
123    public String toString() {
124        return start + "-" + end;
125    }
126
127    /**
128     A Position object tracks the character position in the original input source where a Node starts or ends. If you want to
129     track these positions, tracking must be enabled in the Parser with
130     {@link org.jsoup.parser.Parser#setTrackPosition(boolean)}.
131     @see Node#sourceRange()
132     */
133    public static class Position {
134        private final int pos, lineNumber, columnNumber;
135
136        /**
137         Create a new Position object. Called by the TreeBuilder if source position tracking is on.
138         * @param pos position index
139         * @param lineNumber line number
140         * @param columnNumber column number
141         */
142        public Position(int pos, int lineNumber, int columnNumber) {
143            this.pos = pos;
144            this.lineNumber = lineNumber;
145            this.columnNumber = columnNumber;
146        }
147
148        /**
149         Gets the position index (0-based) of the original input source that this Position was read at. This tracks the
150         total number of characters read into the source at this position, regardless of the number of preceding lines.
151         * @return the position, or {@code -1} if untracked.
152         */
153        public int pos() {
154            return pos;
155        }
156
157        /**
158         Gets the line number (1-based) of the original input source that this Position was read at.
159         * @return the line number, or {@code -1} if untracked.
160         */
161        public int lineNumber() {
162            return lineNumber;
163        }
164
165        /**
166         Gets the cursor number (1-based) of the original input source that this Position was read at. The cursor number
167         resets to 1 on every new line.
168         * @return the cursor number, or {@code -1} if untracked.
169         */
170        public int columnNumber() {
171            return columnNumber;
172        }
173
174        /**
175         Test if this position was tracked during parsing.
176         * @return true if this was tracked during parsing, false otherwise (and all fields will be {@code -1}).
177         */
178        public boolean isTracked() {
179            return this != UntrackedPos;
180        }
181
182        /**
183         Gets a String presentation of this Position, in the format {@code line,column:pos}.
184         * @return a String
185         */
186        @Override
187        public String toString() {
188            return lineNumber + "," + columnNumber + ":" + pos;
189        }
190
191        @Override
192        public boolean equals(Object o) {
193            if (this == o) return true;
194            if (o == null || getClass() != o.getClass()) return false;
195            Position position = (Position) o;
196            if (pos != position.pos) return false;
197            if (lineNumber != position.lineNumber) return false;
198            return columnNumber == position.columnNumber;
199        }
200
201        @Override
202        public int hashCode() {
203            return Objects.hash(pos, lineNumber, columnNumber);
204        }
205    }
206
207    public static class AttributeRange {
208        static final AttributeRange UntrackedAttr = new AttributeRange(Range.Untracked, Range.Untracked);
209
210        private final Range nameRange;
211        private final Range valueRange;
212
213        /** Creates a new AttributeRange. Called during parsing by Token.StartTag. */
214        public AttributeRange(Range nameRange, Range valueRange) {
215            this.nameRange = nameRange;
216            this.valueRange = valueRange;
217        }
218
219        /** Get the source range for the attribute's name. */
220        public Range nameRange() {
221            return nameRange;
222        }
223
224        /** Get the source range for the attribute's value. */
225        public Range valueRange() {
226            return valueRange;
227        }
228
229        /** Get a String presentation of this Attribute range, in the form
230         {@code line,column:pos-line,column:pos=line,column:pos-line,column:pos} (name start - name end = val start - val end).
231         . */
232        @Override public String toString() {
233            StringBuilder sb = StringUtil.borrowBuilder()
234                .append(nameRange)
235                .append('=')
236                .append(valueRange);
237            return StringUtil.releaseBuilder(sb);
238        }
239
240        @Override public boolean equals(Object o) {
241            if (this == o) return true;
242            if (o == null || getClass() != o.getClass()) return false;
243
244            AttributeRange that = (AttributeRange) o;
245
246            if (!nameRange.equals(that.nameRange)) return false;
247            return valueRange.equals(that.valueRange);
248        }
249
250        @Override public int hashCode() {
251            return Objects.hash(nameRange, valueRange);
252        }
253    }
254}