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    /**
099     @deprecated no-op; internal method moved out of visibility
100     */
101    @Deprecated
102    public void track(Node node, boolean start) {}
103
104    @Override
105    public boolean equals(Object o) {
106        if (this == o) return true;
107        if (o == null || getClass() != o.getClass()) return false;
108
109        Range range = (Range) o;
110
111        if (!start.equals(range.start)) return false;
112        return end.equals(range.end);
113    }
114
115    @Override
116    public int hashCode() {
117        int result = start.hashCode();
118        result = 31 * result + end.hashCode();
119        return result;
120    }
121
122    /**
123     Gets a String presentation of this Range, in the format {@code line,column:pos-line,column:pos}.
124     * @return a String
125     */
126    @Override
127    public String toString() {
128        return start + "-" + end;
129    }
130
131    /**
132     A Position object tracks the character position in the original input source where a Node starts or ends. If you want to
133     track these positions, tracking must be enabled in the Parser with
134     {@link org.jsoup.parser.Parser#setTrackPosition(boolean)}.
135     @see Node#sourceRange()
136     */
137    public static class Position {
138        private final int pos, lineNumber, columnNumber;
139
140        /**
141         Create a new Position object. Called by the TreeBuilder if source position tracking is on.
142         * @param pos position index
143         * @param lineNumber line number
144         * @param columnNumber column number
145         */
146        public Position(int pos, int lineNumber, int columnNumber) {
147            this.pos = pos;
148            this.lineNumber = lineNumber;
149            this.columnNumber = columnNumber;
150        }
151
152        /**
153         Gets the position index (0-based) of the original input source that this Position was read at. This tracks the
154         total number of characters read into the source at this position, regardless of the number of preceding lines.
155         * @return the position, or {@code -1} if untracked.
156         */
157        public int pos() {
158            return pos;
159        }
160
161        /**
162         Gets the line number (1-based) of the original input source that this Position was read at.
163         * @return the line number, or {@code -1} if untracked.
164         */
165        public int lineNumber() {
166            return lineNumber;
167        }
168
169        /**
170         Gets the cursor number (1-based) of the original input source that this Position was read at. The cursor number
171         resets to 1 on every new line.
172         * @return the cursor number, or {@code -1} if untracked.
173         */
174        public int columnNumber() {
175            return columnNumber;
176        }
177
178        /**
179         Test if this position was tracked during parsing.
180         * @return true if this was tracked during parsing, false otherwise (and all fields will be {@code -1}).
181         */
182        public boolean isTracked() {
183            return this != UntrackedPos;
184        }
185
186        /**
187         Gets a String presentation of this Position, in the format {@code line,column:pos}.
188         * @return a String
189         */
190        @Override
191        public String toString() {
192            return lineNumber + "," + columnNumber + ":" + pos;
193        }
194
195        @Override
196        public boolean equals(Object o) {
197            if (this == o) return true;
198            if (o == null || getClass() != o.getClass()) return false;
199            Position position = (Position) o;
200            if (pos != position.pos) return false;
201            if (lineNumber != position.lineNumber) return false;
202            return columnNumber == position.columnNumber;
203        }
204
205        @Override
206        public int hashCode() {
207            int result = pos;
208            result = 31 * result + lineNumber;
209            result = 31 * result + columnNumber;
210            return result;
211        }
212    }
213
214    public static class AttributeRange {
215        static final AttributeRange UntrackedAttr = new AttributeRange(Range.Untracked, Range.Untracked);
216
217        private final Range nameRange;
218        private final Range valueRange;
219
220        /** Creates a new AttributeRange. Called during parsing by Token.StartTag. */
221        public AttributeRange(Range nameRange, Range valueRange) {
222            this.nameRange = nameRange;
223            this.valueRange = valueRange;
224        }
225
226        /** Get the source range for the attribute's name. */
227        public Range nameRange() {
228            return nameRange;
229        }
230
231        /** Get the source range for the attribute's value. */
232        public Range valueRange() {
233            return valueRange;
234        }
235
236        /** Get a String presentation of this Attribute range, in the form
237         {@code line,column:pos-line,column:pos=line,column:pos-line,column:pos} (name start - name end = val start - val end).
238         . */
239        @Override public String toString() {
240            return nameRange().toString() + "=" + valueRange().toString();
241        }
242
243        @Override public boolean equals(Object o) {
244            if (this == o) return true;
245            if (o == null || getClass() != o.getClass()) return false;
246
247            AttributeRange that = (AttributeRange) o;
248
249            if (!nameRange.equals(that.nameRange)) return false;
250            return valueRange.equals(that.valueRange);
251        }
252
253        @Override public int hashCode() {
254            int result = nameRange.hashCode();
255            result = 31 * result + valueRange.hashCode();
256            return result;
257        }
258    }
259}