aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/scssphp/source-span/src/SourceFile.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/scssphp/source-span/src/SourceFile.php')
-rw-r--r--vendor/scssphp/source-span/src/SourceFile.php272
1 files changed, 272 insertions, 0 deletions
diff --git a/vendor/scssphp/source-span/src/SourceFile.php b/vendor/scssphp/source-span/src/SourceFile.php
new file mode 100644
index 000000000..33bc5502c
--- /dev/null
+++ b/vendor/scssphp/source-span/src/SourceFile.php
@@ -0,0 +1,272 @@
+<?php
+
+namespace SourceSpan;
+
+use League\Uri\Contracts\UriInterface;
+
+final class SourceFile
+{
+ private readonly string $string;
+
+ private readonly ?UriInterface $sourceUrl;
+
+ /**
+ * @var list<int>
+ */
+ private readonly array $lineStarts;
+
+ /**
+ * The 0-based last line that was returned by {@see getLine}
+ *
+ * This optimizes computation for successive accesses to
+ * the same line or to the next line.
+ * It is stored as 0-based to correspond to the indices
+ * in {@see $lineStarts}.
+ *
+ * @var int|null
+ */
+ private ?int $cachedLine = null;
+
+ public static function fromString(string $content, ?UriInterface $sourceUrl = null): SourceFile
+ {
+ return new SourceFile($content, $sourceUrl);
+ }
+
+ private function __construct(string $content, ?UriInterface $sourceUrl = null)
+ {
+ $this->string = $content;
+ $this->sourceUrl = $sourceUrl;
+
+ // Extract line starts
+ $lineStarts = [0];
+
+ if ($content === '') {
+ $this->lineStarts = $lineStarts;
+ return;
+ }
+
+ $prev = 0;
+
+ while (true) {
+ $crPos = strpos($content, "\r", $prev);
+ $lfPos = strpos($content, "\n", $prev);
+
+ if ($crPos === false && $lfPos === false) {
+ break;
+ }
+
+ if ($crPos !== false) {
+ // Return not followed by newline is treated as a newline
+ if ($lfPos === false || $lfPos > $crPos + 1) {
+ $lineStarts[] = $crPos + 1;
+ $prev = $crPos + 1;
+ continue;
+ }
+ }
+
+ if ($lfPos !== false) {
+ $lineStarts[] = $lfPos + 1;
+ $prev = $lfPos + 1;
+ }
+ }
+
+ $this->lineStarts = $lineStarts;
+ }
+
+ public function getLength(): int
+ {
+ return \strlen($this->string);
+ }
+
+ /**
+ * The number of lines in the file.
+ */
+ public function getLines(): int
+ {
+ return \count($this->lineStarts);
+ }
+
+ public function span(int $start, ?int $end = null): FileSpan
+ {
+ if ($end === null) {
+ $end = \strlen($this->string);
+ }
+
+ return new ConcreteFileSpan($this, $start, $end);
+ }
+
+ public function location(int $offset): FileLocation
+ {
+ if ($offset < 0) {
+ throw new \OutOfRangeException("Offset may not be negative, was $offset.");
+ }
+
+ if ($offset > \strlen($this->string)) {
+ $fileLength = \strlen($this->string);
+
+ throw new \OutOfRangeException("Offset $offset must not be greater than the number of characters in the file, $fileLength.");
+ }
+
+ return new FileLocation($this, $offset);
+ }
+
+ public function getSourceUrl(): ?UriInterface
+ {
+ return $this->sourceUrl;
+ }
+
+ public function getString(): string
+ {
+ return $this->string;
+ }
+
+ /**
+ * The 0-based line corresponding to that offset.
+ */
+ public function getLine(int $offset): int
+ {
+ if ($offset < 0) {
+ throw new \OutOfRangeException('Position cannot be negative');
+ }
+
+ if ($offset > \strlen($this->string)) {
+ throw new \OutOfRangeException('Position cannot be greater than the number of characters in the string.');
+ }
+
+ if ($offset < $this->lineStarts[0]) {
+ return -1;
+ }
+
+ if ($offset >= Util::listLast($this->lineStarts)) {
+ return \count($this->lineStarts) - 1;
+ }
+
+ if ($this->isNearCacheLine($offset)) {
+ assert($this->cachedLine !== null);
+
+ return $this->cachedLine;
+ }
+
+ $this->cachedLine = $this->binarySearch($offset) - 1;
+
+ return $this->cachedLine;
+ }
+
+ /**
+ * Returns `true` if $offset is near {@see $cachedLine}.
+ *
+ * Checks on {@see $cachedLine} and the next line. If it's on the next line, it
+ * updates {@see $cachedLine} to point to that.
+ */
+ private function isNearCacheLine(int $offset): bool
+ {
+ if ($this->cachedLine === null) {
+ return false;
+ }
+
+ if ($offset < $this->lineStarts[$this->cachedLine]) {
+ return false;
+ }
+
+ if (
+ $this->cachedLine >= \count($this->lineStarts) - 1 ||
+ $offset < $this->lineStarts[$this->cachedLine + 1]
+ ) {
+ return true;
+ }
+
+ if (
+ $this->cachedLine >= \count($this->lineStarts) - 2 ||
+ $offset < $this->lineStarts[$this->cachedLine + 2]
+ ) {
+ ++$this->cachedLine;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Binary search through {@see $lineStarts} to find the line containing $offset.
+ *
+ * Returns the index of the line in {@see $lineStarts}.
+ */
+ private function binarySearch(int $offset): int
+ {
+ $min = 0;
+ $max = \count($this->lineStarts) - 1;
+
+ while ($min < $max) {
+ $half = $min + intdiv($max - $min, 2);
+
+ if ($this->lineStarts[$half] > $offset) {
+ $max = $half;
+ } else {
+ $min = $half + 1;
+ }
+ }
+
+ return $max;
+ }
+
+ /**
+ * The 0-based column of that offset.
+ */
+ public function getColumn(int $offset): int
+ {
+ $line = $this->getLine($offset);
+
+ return $offset - $this->lineStarts[$line];
+ }
+
+ /**
+ * Gets the offset for a line and column.
+ */
+ public function getOffset(int $line, int $column = 0): int
+ {
+ if ($line < 0) {
+ throw new \OutOfRangeException('Line may not be negative.');
+ }
+
+ if ($line >= \count($this->lineStarts)) {
+ throw new \OutOfRangeException('Line must be less than the number of lines in the file.');
+ }
+
+ if ($column < 0) {
+ throw new \OutOfRangeException('Column may not be negative.');
+ }
+
+ $result = $this->lineStarts[$line] + $column;
+
+ if ($result > \strlen($this->string) || ($line + 1 < \count($this->lineStarts) && $result >= $this->lineStarts[$line + 1])) {
+ throw new \OutOfRangeException("Line $line doesn't have $column columns.");
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the text of the file from $start to $end (exclusive).
+ *
+ * If $end isn't passed, it defaults to the end of the file.
+ */
+ public function getText(int $start, ?int $end = null): string
+ {
+ if ($end !== null) {
+ if ($end < $start) {
+ throw new \InvalidArgumentException("End $end must come after start $start.");
+ }
+
+ if ($end > $this->getLength()) {
+ throw new \OutOfRangeException("End $end not be greater than the number of characters in the file, {$this->getLength()}.");
+ }
+ }
+
+ if ($start < 0) {
+ throw new \OutOfRangeException("Start may not be negative, was $start.");
+ }
+
+ return Util::substring($this->string, $start, $end);
+ }
+}