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