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}