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