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}