001package org.jsoup.nodes; 002 003import org.jsoup.internal.LineMap; 004import org.jsoup.internal.StringUtil; 005 006import java.util.Arrays; 007import java.util.Objects; 008 009/** 010 A Range tracks the source offsets where a Node starts or ends. Line and column coordinates are derived from the 011 line map retained during parsing. To track these positions, enable {@link org.jsoup.parser.Parser#setTrackPosition(boolean)} 012 before parsing. 013 @see Node#sourceRange() 014 @since 1.15.2 015 */ 016public class Range { 017 // sentinels 018 private static final LineMap UnsetLineMap = new LineMap(); 019 private static final int[] UnsetAttrRanges = new int[0]; 020 private static final Position UntrackedPos = new Position(-1, -1, -1); 021 private static final Range Untracked = new Range(); 022 023 private final LineMap lineMap; 024 private final int startPos; 025 private final int endPos; 026 027 /** 028 Creates the untracked source range sentinel. 029 */ 030 private Range() { 031 lineMap = UnsetLineMap; 032 startPos = -1; 033 endPos = -1; 034 } 035 036 /** 037 Creates a new Range from source offsets. 038 */ 039 private Range(LineMap lineMap, int startPos, int endPos) { 040 this.lineMap = lineMap; 041 if (startPos < 0 || endPos < 0) 042 throw new IllegalArgumentException("Range positions must be non-negative"); 043 this.startPos = startPos; 044 this.endPos = endPos; 045 } 046 047 /** 048 Get the start position of this range, with 1-based line and column coordinates. 049 * @return the start position. 050 */ 051 public Position start() { 052 return startPos == -1 ? UntrackedPos : position(startPos); 053 } 054 055 /** 056 Get the starting source offset of this range. 057 @return the 0-based start source offset. 058 @since 1.17.1 059 */ 060 public int startPos() { 061 return startPos; 062 } 063 064 /** 065 Get the end position of this range, with 1-based line and column coordinates. 066 * @return the end position. 067 */ 068 public Position end() { 069 return endPos == -1 ? UntrackedPos : position(endPos); 070 } 071 072 /** 073 Get the ending source offset of this range. 074 @return the 0-based ending source offset. 075 @since 1.17.1 076 */ 077 public int endPos() { 078 return endPos; 079 } 080 081 /** 082 Test if this range has source offsets available. 083 * @return true if this range has source offsets, false otherwise (and all fields will be {@code -1}). 084 */ 085 public boolean isTracked() { 086 return startPos != -1; 087 } 088 089 /** 090 Checks if the range represents a node that was implicitly created / closed. 091 <p>For example, with HTML of {@code <p>One<p>Two}, both {@code p} elements will have an explicit 092 {@link Element#sourceRange()} but an implicit {@link Element#endSourceRange()} marking the end position, as neither 093 have closing {@code </p>} tags. The TextNodes will have explicit sourceRanges. 094 <p>A range is considered implicit if its start and end positions are the same. 095 @return true if the range is tracked and its start and end positions are the same, false otherwise. 096 @since 1.17.1 097 */ 098 public boolean isImplicit() { 099 return isTracked() && startPos == endPos; 100 } 101 102 /** 103 Creates a Position from a source offset and this Range's line map. 104 */ 105 private Position position(int pos) { 106 return new Position(pos, lineMap.lineNumber(pos), lineMap.columnNumber(pos)); 107 } 108 109 /** 110 Retrieves the start source range for a given Node. 111 * @param node the node to retrieve the position for 112 * @return the Range, or the Untracked (-1) position if tracking is disabled. 113 */ 114 static Range ofStart(Node node) { 115 Range.Spans rangeSpans = node.spans(); 116 return rangeSpans != null ? rangeSpans.sourceRange() : Untracked; 117 } 118 119 /** 120 Retrieves the end source range for a given Element. 121 * @param element the element to retrieve the end tag position for 122 * @return the Range, or the Untracked (-1) position if tracking is disabled. 123 */ 124 static Range ofEnd(Element element) { 125 Range.Spans rangeSpans = element.spans(); 126 return rangeSpans != null ? rangeSpans.endSourceRange() : Untracked; 127 } 128 129 @Override 130 public boolean equals(Object o) { 131 if (this == o) return true; 132 if (o == null || getClass() != o.getClass()) return false; 133 134 Range range = (Range) o; 135 136 return startPos == range.startPos && endPos == range.endPos; 137 } 138 139 @Override 140 public int hashCode() { 141 int result = startPos; 142 result = 31 * result + endPos; 143 return result; 144 } 145 146 /** 147 Gets a String representation of this Range, in the format {@code line,column:pos-line,column:pos}. 148 * @return a String 149 */ 150 @Override 151 public String toString() { 152 StringBuilder sb = StringUtil.borrowBuilder() 153 .append(start()) 154 .append('-') 155 .append(end()); 156 return StringUtil.releaseBuilder(sb); 157 } 158 159 /** 160 A Position describes a source offset and its line and column coordinates. Positions are available when position 161 tracking is enabled with {@link org.jsoup.parser.Parser#setTrackPosition(boolean)} before parsing. 162 @see Node#sourceRange() 163 */ 164 public static class Position { 165 private final int pos, lineNumber, columnNumber; 166 167 /** 168 Creates a position from its source offset and coordinates. 169 */ 170 private Position(int pos, int lineNumber, int columnNumber) { 171 this.pos = pos; 172 this.lineNumber = lineNumber; 173 this.columnNumber = columnNumber; 174 } 175 176 /** 177 Gets the position index (0-based) of the original input source that this Position was read at. This tracks the 178 total number of characters read into the source at this position, regardless of the number of preceding lines. 179 * @return the position, or {@code -1} if untracked. 180 */ 181 public int pos() { 182 return pos; 183 } 184 185 /** 186 Gets the line number (1-based) of the original input source that this Position was read at. 187 * @return the line number, or {@code -1} if untracked. 188 */ 189 public int lineNumber() { 190 return lineNumber; 191 } 192 193 /** 194 Gets the cursor number (1-based) of the original input source that this Position was read at. The cursor number 195 resets to 1 on every new line. 196 * @return the cursor number, or {@code -1} if untracked. 197 */ 198 public int columnNumber() { 199 return columnNumber; 200 } 201 202 /** 203 Test if this position was tracked during parsing. 204 * @return true if this was tracked during parsing, false otherwise (and all fields will be {@code -1}). 205 */ 206 public boolean isTracked() { 207 return pos != -1; 208 } 209 210 /** 211 Gets a String presentation of this Position, in the format {@code line,column:pos}. 212 * @return a String 213 */ 214 @Override 215 public String toString() { 216 StringBuilder sb = StringUtil.borrowBuilder() 217 .append(lineNumber) 218 .append(',') 219 .append(columnNumber) 220 .append(':') 221 .append(pos); 222 return StringUtil.releaseBuilder(sb); 223 } 224 225 @Override 226 public boolean equals(Object o) { 227 if (this == o) return true; 228 if (o == null || getClass() != o.getClass()) return false; 229 Position position = (Position) o; 230 if (pos != position.pos) return false; 231 if (lineNumber != position.lineNumber) return false; 232 return columnNumber == position.columnNumber; 233 } 234 235 @Override 236 public int hashCode() { 237 return Objects.hash(pos, lineNumber, columnNumber); 238 } 239 } 240 241 public static class AttributeRange { 242 static final AttributeRange UntrackedAttr = new AttributeRange(); 243 244 private final LineMap lineMap; 245 private final int nameStartPos, nameEndPos, valueStartPos, valueEndPos; 246 247 /** 248 Creates the untracked attribute source range sentinel. 249 */ 250 private AttributeRange() { 251 lineMap = UnsetLineMap; 252 nameStartPos = -1; 253 nameEndPos = -1; 254 valueStartPos = -1; 255 valueEndPos = -1; 256 } 257 258 /** 259 Creates a new AttributeRange from source offsets. 260 */ 261 private AttributeRange(LineMap lineMap, int nameStartPos, int nameEndPos, int valueStartPos, int valueEndPos) { 262 this.lineMap = lineMap; 263 if (nameStartPos < 0 || nameEndPos < 0 || valueStartPos < 0 || valueEndPos < 0) 264 throw new IllegalArgumentException("Attribute range positions must be non-negative"); 265 this.nameStartPos = nameStartPos; 266 this.nameEndPos = nameEndPos; 267 this.valueStartPos = valueStartPos; 268 this.valueEndPos = valueEndPos; 269 } 270 271 /** Get the source range for the attribute's name. */ 272 public Range nameRange() { 273 return isTracked() ? new Range(lineMap, nameStartPos, nameEndPos) : Range.Untracked; 274 } 275 276 /** Get the source range for the attribute's value. */ 277 public Range valueRange() { 278 return isTracked() ? new Range(lineMap, valueStartPos, valueEndPos) : Range.Untracked; 279 } 280 281 /** 282 Tests if this attribute range has tracked name and value offsets. 283 * @return true if the attribute's name and value ranges were tracked; false otherwise. 284 @since 1.23.1 285 */ 286 public boolean isTracked() { 287 return nameStartPos != -1; 288 } 289 290 /** 291 Get a String representation of this Attribute range, in the form 292 {@code line,column:pos-line,column:pos=line,column:pos-line,column:pos} (name start - name end = val start - val end) 293 */ 294 @Override 295 public String toString() { 296 StringBuilder sb = StringUtil.borrowBuilder() 297 .append(nameRange()) 298 .append('=') 299 .append(valueRange()); 300 return StringUtil.releaseBuilder(sb); 301 } 302 303 @Override public boolean equals(Object o) { 304 if (this == o) return true; 305 if (o == null || getClass() != o.getClass()) return false; 306 307 AttributeRange that = (AttributeRange) o; 308 309 if (nameStartPos != that.nameStartPos) return false; 310 if (nameEndPos != that.nameEndPos) return false; 311 if (valueStartPos != that.valueStartPos) return false; 312 return valueEndPos == that.valueEndPos; 313 } 314 315 @Override public int hashCode() { 316 int result = nameStartPos; 317 result = 31 * result + nameEndPos; 318 result = 31 * result + valueStartPos; 319 result = 31 * result + valueEndPos; 320 return result; 321 } 322 } 323 324 /** 325 Internal range span storage attached to a Node or Attributes object. 326 <p>Unset records use {@code -1}; once written, a node, end-tag, or attribute range record is complete.</p> 327 */ 328 static final class Spans { 329 private static final int AttrRangeWidth = 4; 330 331 private LineMap lineMap = UnsetLineMap; 332 private int nodeStartPos = -1; 333 private int nodeEndPos = -1; 334 private int endTagStartPos = -1; 335 private int endTagEndPos = -1; 336 private int[] attrRanges = UnsetAttrRanges; 337 338 /** 339 Gets the node start source range. 340 */ 341 private Range sourceRange() { 342 return range(nodeStartPos, nodeEndPos); 343 } 344 345 /** 346 Gets the element end tag source range. 347 */ 348 private Range endSourceRange() { 349 return range(endTagStartPos, endTagEndPos); 350 } 351 352 /** 353 Sets the node start source range. 354 */ 355 void sourceRange(LineMap lineMap, int startPos, int endPos) { 356 useLineMap(lineMap); 357 nodeStartPos = startPos; 358 nodeEndPos = endPos; 359 } 360 361 /** 362 Sets the element end tag source range. 363 */ 364 void endSourceRange(LineMap lineMap, int startPos, int endPos) { 365 useLineMap(lineMap); 366 endTagStartPos = startPos; 367 endTagEndPos = endPos; 368 } 369 370 /** Clears the end-tag range when a parser-created element is recreated from an earlier start token. */ 371 void clearEndSourceRange() { 372 endTagStartPos = -1; 373 endTagEndPos = -1; 374 } 375 376 /** 377 Gets the source ranges for an attribute slot. 378 */ 379 Range.AttributeRange attributeRange(int index) { 380 if (index < 0) 381 return Range.AttributeRange.UntrackedAttr; 382 383 int[] ranges = attrRanges; 384 int nameIndex = attrNameStartIndex(index); 385 int valueIndex = attrValueStartIndex(index); 386 int valueEndIndex = valueIndex + 1; 387 if (valueEndIndex >= ranges.length) 388 return Range.AttributeRange.UntrackedAttr; 389 390 int nameStart = ranges[nameIndex]; 391 int nameEnd = ranges[nameIndex + 1]; 392 int valueStart = ranges[valueIndex]; 393 int valueEnd = ranges[valueEndIndex]; 394 if (nameStart == -1) 395 return Range.AttributeRange.UntrackedAttr; 396 397 return new Range.AttributeRange(lineMap, nameStart, nameEnd, valueStart, valueEnd); 398 } 399 400 /** 401 Sets the source ranges for an attribute slot. 402 */ 403 void attributeRange(int index, Range.AttributeRange range) { 404 attributeRange( 405 index, 406 range.lineMap, 407 range.nameStartPos, 408 range.nameEndPos, 409 range.valueStartPos, 410 range.valueEndPos 411 ); 412 } 413 414 /** 415 Sets source range offsets for an attribute slot. 416 */ 417 void attributeRange(int index, LineMap lineMap, int nameStart, int nameEnd, int valueStart, int valueEnd) { 418 if (nameStart < 0 || nameEnd < 0 || valueStart < 0 || valueEnd < 0) 419 throw new IllegalArgumentException("Attribute range positions must be non-negative"); 420 useLineMap(lineMap); 421 int nameIndex = attrNameStartIndex(index); 422 int valueIndex = attrValueStartIndex(index); 423 ensureAttributeCapacity(valueIndex + 2); 424 attrRanges[nameIndex] = nameStart; 425 attrRanges[nameIndex + 1] = nameEnd; 426 attrRanges[valueIndex] = valueStart; 427 attrRanges[valueIndex + 1] = valueEnd; 428 } 429 430 /** Rebases locally parsed attribute ranges into their containing source. */ 431 void rebaseAttributeRanges(Range sourceRange, int offset) { 432 this.lineMap = sourceRange.lineMap; 433 for (int i = 0; i < attrRanges.length; i++) { 434 if (attrRanges[i] >= 0) 435 attrRanges[i] += offset; 436 } 437 } 438 439 /** Copies attribute ranges while retaining this node's other ranges. */ 440 void copyAttributeRanges(Spans source) { 441 if (source.attrRanges.length == 0) 442 return; 443 useLineMap(source.lineMap); 444 attrRanges = source.attrRanges.clone(); 445 } 446 447 /** 448 Retains the first line map and rejects mixed-source ranges. 449 */ 450 private void useLineMap(LineMap lineMap) { 451 if (this.lineMap == UnsetLineMap) { 452 this.lineMap = lineMap; 453 } else if (this.lineMap != lineMap) { 454 throw new IllegalArgumentException("Source ranges must come from the same parse"); 455 } 456 } 457 458 /** 459 Removes an attribute slot and shifts following source ranges. 460 */ 461 void removeAttributeRange(int index) { 462 if (index < 0) return; 463 int[] ranges = attrRanges; 464 int removeIndex = attrNameStartIndex(index); 465 if (removeIndex >= ranges.length) return; 466 467 int nextIndex = removeIndex + AttrRangeWidth; 468 int shifted = ranges.length - nextIndex; 469 if (shifted > 0) 470 System.arraycopy(ranges, nextIndex, ranges, removeIndex, shifted); 471 Arrays.fill(ranges, ranges.length - AttrRangeWidth, ranges.length, -1); 472 } 473 474 /** 475 Returns a copy whose source range arrays can mutate independently. 476 */ 477 Spans copy() { 478 Spans copy = new Spans(); 479 copy.lineMap = lineMap; 480 copy.nodeStartPos = nodeStartPos; 481 copy.nodeEndPos = nodeEndPos; 482 copy.endTagStartPos = endTagStartPos; 483 copy.endTagEndPos = endTagEndPos; 484 copy.attrRanges = attrRanges.length == 0 ? UnsetAttrRanges : attrRanges.clone(); 485 return copy; 486 } 487 488 /** 489 Grows attribute range storage to hold the requested slot count. 490 */ 491 private void ensureAttributeCapacity(int minLength) { 492 if (attrRanges.length >= minLength) return; 493 int oldLength = attrRanges.length; 494 attrRanges = Arrays.copyOf(attrRanges, minLength); 495 Arrays.fill(attrRanges, oldLength, attrRanges.length, -1); 496 } 497 498 /** 499 Creates a Range from stored offsets. 500 */ 501 private Range range(int startPos, int endPos) { 502 if (startPos == -1) 503 return Range.Untracked; 504 return new Range(lineMap, startPos, endPos); 505 } 506 507 /** 508 Maps an attribute slot to its stored name range start slot. 509 */ 510 private static int attrNameStartIndex(int index) { 511 return index * AttrRangeWidth; 512 } 513 514 /** 515 Maps an attribute slot to its stored value range start slot. 516 */ 517 private static int attrValueStartIndex(int index) { 518 return index * AttrRangeWidth + 2; 519 } 520 521 @Override public boolean equals(Object o) { 522 if (this == o) return true; 523 if (o == null || getClass() != o.getClass()) return false; 524 Spans spans = (Spans) o; 525 return nodeStartPos == spans.nodeStartPos && 526 nodeEndPos == spans.nodeEndPos && 527 endTagStartPos == spans.endTagStartPos && 528 endTagEndPos == spans.endTagEndPos && 529 Arrays.equals(attrRanges, spans.attrRanges); 530 } 531 532 @Override public int hashCode() { 533 int result = Objects.hash(nodeStartPos, nodeEndPos, endTagStartPos, endTagEndPos); 534 result = 31 * result + Arrays.hashCode(attrRanges); 535 return result; 536 } 537 } 538}