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}