Add a `SpanScanner.spanFromPosition()` method (dart-lang/string_scanner#78)

Tracking raw ints can be more efficient than tracking
`LineScannerState` objects, and allows users to do small manual
manipulations on the resulting positions.
diff --git a/pkgs/string_scanner/CHANGELOG.md b/pkgs/string_scanner/CHANGELOG.md
index 386a55b..a4c17b6 100644
--- a/pkgs/string_scanner/CHANGELOG.md
+++ b/pkgs/string_scanner/CHANGELOG.md
@@ -1,7 +1,10 @@
-## 1.2.1-wip
+## 1.3.0
 
 * Require Dart 3.1.0
 
+* Add a `SpanScanner.spanFromPosition()` method which takes raw code units
+  rather than `SpanScanner.spanFrom()`'s `LineScannerState`s.
+
 ## 1.2.0
 
 * Require Dart 2.18.0
diff --git a/pkgs/string_scanner/lib/src/relative_span_scanner.dart b/pkgs/string_scanner/lib/src/relative_span_scanner.dart
index 150d507..cd9af0e 100644
--- a/pkgs/string_scanner/lib/src/relative_span_scanner.dart
+++ b/pkgs/string_scanner/lib/src/relative_span_scanner.dart
@@ -79,6 +79,18 @@
   }
 
   @override
+  FileSpan spanFromPosition(int startPosition, [int? endPosition]) {
+    RangeError.checkValidRange(
+        startPosition,
+        endPosition,
+        _sourceFile.length - _startLocation.offset,
+        'startPosition',
+        'endPosition');
+    return _sourceFile.span(_startLocation.offset + startPosition,
+        _startLocation.offset + (endPosition ?? position));
+  }
+
+  @override
   bool matches(Pattern pattern) {
     if (!super.matches(pattern)) {
       _lastSpan = null;
diff --git a/pkgs/string_scanner/lib/src/span_scanner.dart b/pkgs/string_scanner/lib/src/span_scanner.dart
index 413a433..509cf60 100644
--- a/pkgs/string_scanner/lib/src/span_scanner.dart
+++ b/pkgs/string_scanner/lib/src/span_scanner.dart
@@ -91,6 +91,17 @@
     return _sourceFile.span(startState.position, endPosition);
   }
 
+  /// Creates a [FileSpan] representing the source range between [startPosition]
+  /// and [endPosition], or the current position if [endPosition] is null.
+  ///
+  /// Each position should be a code unit offset into the string being scanned,
+  /// with the same conventions as [StringScanner.position].
+  ///
+  /// Throws a [RangeError] if [startPosition] or [endPosition] aren't within
+  /// this source file.
+  FileSpan spanFromPosition(int startPosition, [int? endPosition]) =>
+      _sourceFile.span(startPosition, endPosition ?? position);
+
   @override
   bool matches(Pattern pattern) {
     if (!super.matches(pattern)) {
diff --git a/pkgs/string_scanner/pubspec.yaml b/pkgs/string_scanner/pubspec.yaml
index eea570a..b858538 100644
--- a/pkgs/string_scanner/pubspec.yaml
+++ b/pkgs/string_scanner/pubspec.yaml
@@ -1,5 +1,5 @@
 name: string_scanner
-version: 1.2.1-wip
+version: 1.3.0
 description: A class for parsing strings using a sequence of patterns.
 repository: https://github.com/dart-lang/string_scanner
 
diff --git a/pkgs/string_scanner/test/span_scanner_test.dart b/pkgs/string_scanner/test/span_scanner_test.dart
index 0e20c36..93d9c47 100644
--- a/pkgs/string_scanner/test/span_scanner_test.dart
+++ b/pkgs/string_scanner/test/span_scanner_test.dart
@@ -75,6 +75,16 @@
       expect(span.text, equals('o\nbar\nba'));
     });
 
+    test('.spanFromPosition() returns a span from a previous state', () {
+      scanner.scan('fo');
+      final start = scanner.position;
+      scanner.scan('o\nba');
+      scanner.scan('r\nba');
+
+      final span = scanner.spanFromPosition(start + 2, start + 5);
+      expect(span.text, equals('bar'));
+    });
+
     test('.emptySpan returns an empty span at the current location', () {
       scanner.scan('foo\nba');
 
@@ -139,6 +149,16 @@
       expect(span.text, equals('o\nbar\nba'));
     });
 
+    test('.spanFromPosition() returns a span from a previous state', () {
+      scanner.scan('fo');
+      final start = scanner.position;
+      scanner.scan('o\nba');
+      scanner.scan('r\nba');
+
+      final span = scanner.spanFromPosition(start + 2, start + 5);
+      expect(span.text, equals('bar'));
+    });
+
     test('.emptySpan returns an empty span at the current location', () {
       scanner.scan('foo\nba');