// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A `Scanner` walks a fixed text from left to right, each call picking up
/// where the last left off — Python's `regex.Scanner`, and the loop `re`'s
/// `finditer` runs internally. Build one with `Pattern::scanner`.
pub struct Scanner {
priv pattern : Pattern
priv text : String
/// The current scan position, held in a one-element array so the cursor can be
/// advanced through an immutable receiver (arrays are reference-mutable).
priv pos : Array[Int32]
}
///|
/// Bind a `Pattern` to a text so its methods can scan that text incrementally.
pub fn Pattern::scanner(self : Pattern, text : String) -> Scanner {
Scanner::{ pattern: self, text, pos: [0], }
}
///|
/// The next match from the current position, advancing past it; `None` once the
/// text is exhausted — Python's `Scanner.search`.
pub fn Scanner::search(self : Scanner) -> Match? raise @parse.Refused {
match self.pattern.search(self.text, pos=self.pos[0]) {
None => None
Some(m) => {
self.pos[0] = m.end(0)
Some(m)
}
}
}
///|
/// A match anchored at the current position, advancing past it; `None` if the
/// pattern does not match there — Python's `Scanner.match`.
pub fn Scanner::prefix(self : Scanner) -> Match? raise @parse.Refused {
match self.pattern.prefix(self.text, pos=self.pos[0]) {
None => None
Some(m) => {
self.pos[0] = m.end(0)
Some(m)
}
}
}
///|
/// A full match of the region from the current position, advancing past it;
/// `None` otherwise — Python's `Scanner.full`.
pub fn Scanner::full(self : Scanner) -> Match? raise @parse.Refused {
match self.pattern.full(self.text, pos=self.pos[0]) {
None => None
Some(m) => {
self.pos[0] = m.end(0)
Some(m)
}
}
}
///|
/// Every remaining match from the current position, as group strings, advancing
/// to the end of the text — Python's `Scanner.findall`.
pub fn Scanner::findall(
self : Scanner,
group~ : Group = Whole,
overlapped~ : Bool = false,
) -> Array[String?] raise @parse.Refused {
let r = self.pattern.findall(self.text, group~, overlapped~, pos=self.pos[0])
self.pos[0] = cp_length(self.text)
r
}
///|
/// Every remaining match from the current position, as `Match` objects,
/// advancing to the end of the text — Python's `Scanner.finditer`.
pub fn Scanner::finditer(
self : Scanner,
overlapped~ : Bool = false,
) -> Array[Match] raise @parse.Refused {
let r = self.pattern.finditer(self.text, overlapped~, pos=self.pos[0])
self.pos[0] = cp_length(self.text)
r
}
///|
/// Split the remaining text on the pattern, advancing to the end — Python's
/// `Scanner.split`.
pub fn Scanner::split(
self : Scanner,
maxsplit~ : Int32 = 0,
) -> Array[String?] raise @parse.Refused {
let r = self.pattern.split(self.text, maxsplit~, pos=self.pos[0])
self.pos[0] = cp_length(self.text)
r
}
///|
/// Replace every remaining match with `repl`, returning the new text and
/// advancing to the end — Python's `Scanner.subn` / `Scanner.sub`.
pub fn Scanner::subn(
self : Scanner,
repl : String,
count~ : Int32 = 0,
) -> (String, Int32) raise @parse.Refused {
let r = self.pattern.subn(self.text, repl, count~, pos=self.pos[0])
self.pos[0] = cp_length(self.text)
r
}
///|
/// Replace every remaining match with `repl`, returning the new text —
/// Python's `Scanner.sub`.
pub fn Scanner::sub(
self : Scanner,
repl : String,
count~ : Int32 = 0,
) -> String raise @parse.Refused {
self.subn(repl, count~).0
}