scanner
js/parsing/scanner.ts
fino:parsing/scanner - General-purpose scanning infrastructure for binary and text formats.
Supports binary buffer scanning (always), text scanning (opt-in via encoding), and mixed binary/text parsing (common in real protocols: HTTP/1 ASCII headers + binary body, HPACK varint prefixes + UTF-8 strings, ZIP central directory + UTF-8 names).
// Binary protocol
import { Scanner } from 'fino:parsing/scanner';
const s = new Scanner(buffer);
const version = s.readU8();
const length = s.readU32BE();
const payload = s.eatBytes(length);
// Text format
import { Scanner, ParseError } from 'fino:parsing/scanner';
class MyError extends ParseError { name = 'MyError'; }
const s = new Scanner(source, { encoding: 'utf-8', format: 'myformat' });
while (!s.done) {
const tok = s.eatWhile(code => code !== 0x3B); // eat until ';'
if (!s.done) s.expect(';');
}
Types
type Encoding = 'utf-8' | 'ascii' | 'latin1' | 'utf-16le' | 'utf-16be'
Text encodings supported by scanner text operations.
A scanner created from bytes has no text encoding unless one is supplied.
Text operations such as peek(), eat(), and text() require one of these
encodings.
import { Scanner, type Encoding } from 'fino:parsing/scanner';
const encoding: Encoding = 'utf-8';
const scanner = new Scanner(new Uint8Array([0x41]), { encoding });
scanner.eat(); // 'A'
type ScannerSnapshot = ScannerMark
Alias for a saved scanner position.
Snapshots are created with snapshot() and restored with restore(). They
have the same shape as ScannerMark.
import { Scanner, type ScannerSnapshot } from 'fino:parsing/scanner';
const scanner = new Scanner('abc', { encoding: 'utf-8' });
const snapshot: ScannerSnapshot = scanner.snapshot();
scanner.eat();
scanner.restore(snapshot);
Interfaces
interface ScannerOptions {
Options controlling scanner text decoding and error labels.
encoding enables text operations for byte input. format and filename
are used in parse errors and rendered diagnostics.
import { Scanner, type ScannerOptions } from 'fino:parsing/scanner';
const options: ScannerOptions = { encoding: 'utf-8', format: 'json', filename: 'data.json' };
const scanner = new Scanner('{"ok":true}', options);
Properties
encoding?: Encoding
Text encoding used by scanner text operations.
Defaults to 'utf-8' for string input and null for byte input. Without an
encoding, text operations throw and binary operations remain available.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0x68, 0x69]), { encoding: 'ascii' }).eat(2);
format?: string
Human-readable format label used in parse errors.
Defaults to 'parse'. Use a format name such as 'csv' or 'xml' to make
diagnostics clear for callers.
import { Scanner } from 'fino:parsing/scanner';
const error = new Scanner('', { format: 'demo' }).error('missing input');
error.format; // 'demo'
filename?: string
Optional source filename used in rendered diagnostics.
The scanner does not read this file; the value is diagnostic metadata only.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('x', { encoding: 'utf-8', filename: 'input.txt' });
scanner.error('bad token').filename;
interface ScannerMark {
Saved scanner position, including line and column when text tracking is active.
Marks can be passed to text(), bytesSlice(), restore(), or error() to
recover spans and render diagnostics. Byte operations after text scanning can
invalidate line/column tracking; marks captured before that still preserve
their own location metadata.
import { Scanner, type ScannerMark } from 'fino:parsing/scanner';
const scanner = new Scanner('abc', { encoding: 'utf-8' });
const mark: ScannerMark = scanner.mark();
scanner.eat(2);
scanner.text(mark); // 'ab'
Readonly Properties
readonly offset: number
Byte offset from the start of the scanner buffer.
The offset is zero-based and is always present, even for pure binary scanners.
import { Scanner } from 'fino:parsing/scanner';
const mark = new Scanner('abc', { encoding: 'utf-8' }).mark();
mark.offset;
readonly line?: number
One-based line number, when text position tracking is available.
The property is omitted for binary scanners or after byte operations have invalidated text tracking.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('a\nb', { encoding: 'utf-8' });
scanner.eat(2);
scanner.mark().line;
readonly column?: number
One-based column number, when text position tracking is available.
The property is omitted under the same conditions as line.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('abc', { encoding: 'utf-8' });
scanner.eat();
scanner.mark().column;
Classes
class ParseError extends Error {
Parse error with source location and renderable text or binary context.
Format-specific parsers can subclass this error and use Scanner.error() to
create instances with consistent offsets, optional line/column data, and
source snippets. render() returns either a text caret snippet or a binary
hex dump depending on available location metadata.
import { ParseError, Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('bad', { encoding: 'utf-8', format: 'demo' });
const error: ParseError = scanner.error('unexpected token');
console.log(error.render());
Readonly Properties
readonly format: string
Parser format label, such as 'csv', 'xml', or 'parse'.
This is copied from ScannerOptions.format or the explicit constructor
options and appears in rendered diagnostics.
import { Scanner } from 'fino:parsing/scanner';
new Scanner('', { format: 'demo' }).error('missing').format;
readonly filename: string | undefined
Optional filename associated with the source.
The filename is diagnostic metadata and may be undefined.
import { Scanner } from 'fino:parsing/scanner';
new Scanner('x', { encoding: 'utf-8', filename: 'input.txt' }).error('bad').filename;
readonly offset: number
Byte offset where the error starts.
Offsets are zero-based and apply to the scanner's internal byte buffer.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('abc', { encoding: 'utf-8' });
scanner.eat();
scanner.error('bad').offset;
readonly line: number | undefined
One-based line number for text errors.
The value is undefined when no text position is available, for example on
binary-only scanners.
import { Scanner } from 'fino:parsing/scanner';
new Scanner('a\nb', { encoding: 'utf-8' }).error('bad').line;
readonly column: number | undefined
One-based column number for text errors.
The value is undefined when no text position is available.
import { Scanner } from 'fino:parsing/scanner';
new Scanner('abc', { encoding: 'utf-8' }).error('bad').column;
readonly length: number
Highlight length in bytes or text columns, depending on diagnostic mode.
Defaults to 1 when no explicit span is supplied.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('abc', { encoding: 'utf-8' });
const from = scanner.mark();
scanner.eat(2);
scanner.error('bad span', { from, to: scanner.mark() }).length;
Constructors
constructor(message: string, opts: {
detail: string;
format: string;
filename?: string;
offset: number;
line?: number;
column?: number;
length?: number;
source: Uint8Array;
})
Create a parse error with explicit source and location metadata.
Most parsers should call Scanner.error() instead so offsets and line
tracking are filled from the current scanner state. Construct directly when
adapting diagnostics from another parser.
import { ParseError } from 'fino:parsing/scanner';
const error = new ParseError('demo: bad at offset 0', {
detail: 'bad',
format: 'demo',
offset: 0,
source: new Uint8Array([0x62, 0x61, 0x64]),
});
Methods
render(options?: {
color?: boolean;
contextLines?: number;
}): string
Render a diagnostic snippet for humans.
Text errors render line context and a caret. Binary errors render a small
hex dump around the failing byte. color defaults to false and
contextLines defaults to 0.
import { Scanner } from 'fino:parsing/scanner';
const error = new Scanner('a\nb', { encoding: 'utf-8' }).error('bad');
console.log(error.render({ contextLines: 1 }));
class Scanner {
Cursor-based scanner for mixed binary and text parsers.
The scanner owns a byte buffer and advances a cursor through binary and
text-oriented operations. Text operations require an encoding. Byte
operations can invalidate line/column tracking; use mark() or snapshot()
before switching modes when you need to restore text positions.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('name:value\r\n', { encoding: 'utf-8', format: 'header' });
const name = scanner.readToken('header name');
scanner.expect(':');
const value = scanner.eatUntil((code) => code === 0x0D);
Constructors
constructor(source: string | Uint8Array, options?: ScannerOptions)
Create a scanner over string or byte input.
String input is encoded as UTF-8 and defaults to UTF-8 text mode. Byte
input defaults to binary mode unless options.encoding is provided.
import { Scanner } from 'fino:parsing/scanner';
const textScanner = new Scanner('abc', { encoding: 'utf-8' });
const binaryScanner = new Scanner(new Uint8Array([1, 2, 3]));
Getters
get offset(): number
Current byte offset from the start of the scanner buffer.
The value advances as text or byte operations consume input.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('abc', { encoding: 'utf-8' });
scanner.eat();
scanner.offset; // 1
get done(): boolean
Whether the scanner cursor is at or past the end of input.
Use this to drive parser loops without peeking past the buffer.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('a', { encoding: 'utf-8' });
scanner.eat();
scanner.done; // true
get encoding(): Encoding | null
Active text encoding, or null for binary-only scanning.
Text operations throw when this value is null.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0x61])).encoding; // null
get remainingBytes(): number
Number of unread bytes remaining in the buffer.
This is byte-oriented even when text scanning is active.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner(new Uint8Array([1, 2, 3]));
scanner.eatByte();
scanner.remainingBytes; // 2
get line(): number
Current one-based line number for text scanning.
Throws if no encoding is active or if byte operations invalidated text position tracking. Restore a text snapshot to resynchronize.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('a\nb', { encoding: 'utf-8' });
scanner.eat(2);
scanner.line; // 2
get column(): number
Current one-based column number for text scanning.
Throws under the same conditions as line.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('abc', { encoding: 'utf-8' });
scanner.eat();
scanner.column; // 2
Methods
peekByte(at: number = 0): number
Return a byte at the current cursor plus an optional offset without advancing.
Returns -1 when the requested position is beyond the end of input.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner(new Uint8Array([0x41]));
scanner.peekByte(); // 0x41
eatByte(): number
Consume and return one byte.
Throws ParseError at end of input. When text mode is active, this byte
operation invalidates line and column tracking.
import { Scanner } from 'fino:parsing/scanner';
const byte = new Scanner(new Uint8Array([1, 2])).eatByte();
eatBytes(n: number): Uint8Array
Consume n bytes and return a view into the scanner buffer.
Throws ParseError if fewer than n bytes remain. The returned
Uint8Array is a subarray view, not a defensive copy.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner(new Uint8Array([1, 2, 3]));
scanner.eatBytes(2); // Uint8Array [1, 2]
matchBytes(b: Uint8Array | readonly number[]): boolean
Match and consume an exact byte sequence.
Returns true and advances when all bytes match. Returns false and leaves
the cursor unchanged when the sequence does not match or is incomplete.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner(new Uint8Array([0x50, 0x4b]));
scanner.matchBytes([0x50, 0x4b]); // true
eatUntilByte(c: number, max?: number): Uint8Array
Consume bytes until a delimiter byte or optional maximum length is reached.
The delimiter is not consumed. The returned value is a subarray view of the consumed bytes and may be empty when the cursor already points at the delimiter.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner(new Uint8Array([1, 2, 0, 3]));
scanner.eatUntilByte(0); // Uint8Array [1, 2]
bytesSlice(from: ScannerMark, to?: ScannerMark): Uint8Array
Return a byte slice between two marks or from a mark to the current cursor.
The result is a subarray view and does not advance the scanner.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner(new Uint8Array([1, 2, 3]));
const from = scanner.mark();
scanner.eatBytes(2);
scanner.bytesSlice(from); // Uint8Array [1, 2]
readU8(): number
Read an unsigned 8-bit integer and advance by one byte.
Throws ParseError if no byte remains.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([255])).readU8();
readI8(): number
Read a signed 8-bit integer and advance by one byte.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([255])).readI8(); // -1
readU16BE(): number
Read an unsigned big-endian 16-bit integer.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0x01, 0x00])).readU16BE(); // 256
readU16LE(): number
Read an unsigned little-endian 16-bit integer.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0x00, 0x01])).readU16LE(); // 256
readI16BE(): number
Read a signed big-endian 16-bit integer.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0xff, 0xff])).readI16BE(); // -1
readI16LE(): number
Read a signed little-endian 16-bit integer.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0xff, 0xff])).readI16LE(); // -1
readU32BE(): number
Read an unsigned big-endian 32-bit integer.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0, 0, 0, 1])).readU32BE();
readU32LE(): number
Read an unsigned little-endian 32-bit integer.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([1, 0, 0, 0])).readU32LE();
readI32BE(): number
Read a signed big-endian 32-bit integer.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0xff, 0xff, 0xff, 0xff])).readI32BE();
readI32LE(): number
Read a signed little-endian 32-bit integer.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0xff, 0xff, 0xff, 0xff])).readI32LE();
readF32BE(): number
Read a big-endian IEEE 754 32-bit float.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0x3f, 0x80, 0, 0])).readF32BE(); // 1
readF32LE(): number
Read a little-endian IEEE 754 32-bit float.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0, 0, 0x80, 0x3f])).readF32LE(); // 1
readF64BE(): number
Read a big-endian IEEE 754 64-bit float.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0x3f, 0xf0, 0, 0, 0, 0, 0, 0])).readF64BE();
readF64LE(): number
Read a little-endian IEEE 754 64-bit float.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0, 0, 0, 0, 0, 0, 0xf0, 0x3f])).readF64LE();
readU64BE(): bigint
Read an unsigned big-endian 64-bit integer as bigint.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1])).readU64BE();
readU64LE(): bigint
Read an unsigned little-endian 64-bit integer as bigint.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([1, 0, 0, 0, 0, 0, 0, 0])).readU64LE();
readI64BE(): bigint
Read a signed big-endian 64-bit integer as bigint.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])).readI64BE();
readI64LE(): bigint
Read a signed little-endian 64-bit integer as bigint.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])).readI64LE();
readU16BEField(name: string): number
Read a named unsigned big-endian 16-bit field.
The field name appears in the end-of-input error message when not enough bytes remain.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0, 10])).readU16BEField('length');
readU32BEField(name: string): number
Read a named unsigned big-endian 32-bit field.
The field name appears in the end-of-input error message when not enough bytes remain.
import { Scanner } from 'fino:parsing/scanner';
new Scanner(new Uint8Array([0, 0, 0, 10])).readU32BEField('size');
eatText(byteLength: number, encoding?: Encoding): string
Consume a fixed number of bytes and decode them as text.
Uses the supplied encoding, the scanner's active encoding, or UTF-8 by
default. Throws if fewer than byteLength bytes remain.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner(new Uint8Array([0x68, 0x69]));
scanner.eatText(2, 'ascii'); // 'hi'
peek(n: number = 1): string
Peek up to n Unicode code points without advancing.
Requires text mode. Returns fewer characters near end of input and ""
when already done.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('hello', { encoding: 'utf-8' });
scanner.peek(2); // 'he'
peekCode(n: number = 0): number
Peek a Unicode code point without advancing.
n counts code points after the current cursor, not bytes. Returns -1
when the requested character is beyond end of input.
import { Scanner } from 'fino:parsing/scanner';
new Scanner('abc', { encoding: 'utf-8' }).peekCode(); // 0x61
eat(n: number = 1): string
Consume up to n Unicode code points and return them as a string.
Requires text mode. The method stops at end of input without throwing.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('abc', { encoding: 'utf-8' });
scanner.eat(2); // 'ab'
eatChar(s: string): boolean
Match and consume an exact text string.
Returns true on match and false without advancing otherwise. This is an
alias-style helper for single characters or short delimiters.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner(':value', { encoding: 'utf-8' });
scanner.eatChar(':'); // true
match(s: string): boolean
Match and consume an exact text string.
Returns true on match and false without advancing otherwise. Use
expect() when mismatch should throw.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('true', { encoding: 'utf-8' });
scanner.match('true'); // true
eatWhile(pred: (code: number) => boolean): string
Consume characters while a predicate returns true.
The predicate receives Unicode code points. Returns the consumed text and
may return "" when the first character does not match.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('abc123', { encoding: 'utf-8' });
scanner.eatWhile((code) => code >= 0x61 && code <= 0x7a); // 'abc'
eatUntil(pred: (code: number) => boolean): string
Consume characters until a predicate returns true.
The delimiter character is not consumed. This is equivalent to
eatWhile(code => !pred(code)).
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('name:value', { encoding: 'utf-8' });
scanner.eatUntil((code) => code === 0x3a); // 'name'
expect(s: string, message?: string): void
Require an exact text string and consume it.
Throws ParseError with either the supplied message or an automatically
generated expectation message when the text does not match.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('=value', { encoding: 'utf-8' });
scanner.expect('=');
skipSpaceTab(): void
Skip ASCII space and tab characters.
Newlines are not consumed. Requires text mode.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner(' \tvalue', { encoding: 'utf-8' });
scanner.skipSpaceTab();
scanner.peek(); // 'v'
skipWhitespace(): void
Skip ASCII spaces, tabs, carriage returns, and line feeds.
Requires text mode and updates line/column tracking for skipped newlines.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner(' \nvalue', { encoding: 'utf-8' });
scanner.skipWhitespace();
text(from: ScannerMark, to?: ScannerMark): string
Decode text between two marks or from a mark to the current cursor.
Requires text mode. The scanner cursor is not changed.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('abc', { encoding: 'utf-8' });
const from = scanner.mark();
scanner.eat(2);
scanner.text(from); // 'ab'
readLineCRLF(): string
Read one CRLF-terminated text line.
The returned line excludes the \r\n terminator. Bare LF and missing CRLF
before end of input throw ParseError.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('first\r\nsecond\r\n', { encoding: 'utf-8' });
scanner.readLineCRLF(); // 'first'
readHeaderBlock(): string[]
Read CRLF-terminated header lines until an empty line.
Returns header lines without terminators and without the final empty line.
Bare LF or unterminated input throws via readLineCRLF().
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('A: b\r\n\r\n', { encoding: 'utf-8' });
scanner.readHeaderBlock(); // ['A: b']
readAsciiSpanUntilByte(delimiter: number, consumeDelimiter: boolean = false): Uint8Array
Read ASCII bytes until a delimiter byte.
Throws if a consumed byte is non-ASCII. The delimiter is consumed only when
consumeDelimiter is true; otherwise it remains at the cursor.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner(new Uint8Array([0x61, 0x3a, 0x62]));
scanner.readAsciiSpanUntilByte(0x3a, true); // Uint8Array [0x61]
readDelimitedList(delimiter: string): string[]
Read the remaining text as a delimiter-separated list.
ASCII spaces and tabs are trimmed around each part, and empty parts are omitted. Requires text mode and consumes the rest of the input.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('a, b,, c', { encoding: 'utf-8' });
scanner.readDelimitedList(','); // ['a', 'b', 'c']
readToken(name: string = 'token'): string
Read a protocol-style token.
Tokens are visible ASCII characters excluding comma, colon, semicolon, and ASCII control/space characters. Throws when no token is present.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('Content-Type: text/plain', { encoding: 'utf-8' });
scanner.readToken('header name'); // 'Content-Type'
expectToken(expected: string, options?: {
caseInsensitive?: boolean;
name?: string;
}): void
Require a specific protocol token.
Reads one token and throws when it does not match expected. Set
caseInsensitive for ASCII case-insensitive comparisons.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('GET /', { encoding: 'utf-8' });
scanner.expectToken('get', { caseInsensitive: true, name: 'method' });
readStrictInt(options?: {
radix?: 10 | 16;
name?: string;
min?: number;
max?: number;
allowSign?: boolean;
}): number
Read a strictly formatted decimal or hexadecimal integer token.
Defaults to unsigned base-10. Range checks throw ParseError with the
provided field name when the parsed value is outside min or max.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('2a', { encoding: 'utf-8' });
scanner.readStrictInt({ radix: 16, name: 'length', max: 0xff });
subScanner(byteLength: number, options?: ScannerOptions): Scanner
Create a new scanner over the next byteLength bytes.
The parent scanner advances by byteLength. Child options default to the
parent encoding, format, and filename unless overridden.
import { Scanner } from 'fino:parsing/scanner';
const parent = new Scanner(new Uint8Array([0x61, 0x62]), { encoding: 'ascii' });
const child = parent.subScanner(1);
child.eat(); // 'a'
jump(offset: number): void
Move the cursor to an absolute byte offset.
Offsets must be integers between 0 and the buffer length inclusive.
Jumping in text mode invalidates line/column tracking.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('abc', { encoding: 'utf-8' });
scanner.jump(2);
scanner.peek(); // 'c'
mark(): ScannerMark
Save the current scanner position.
In text mode, the mark includes line and column when tracking is still valid. Use marks for span extraction, diagnostics, and manual backtracking.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('abc', { encoding: 'utf-8' });
const start = scanner.mark();
scanner.eat(3);
scanner.text(start); // 'abc'
snapshot(): ScannerSnapshot
Save the current scanner position for later restoration.
This is an alias for mark() with a name that emphasizes backtracking.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('yes', { encoding: 'utf-8' });
const snap = scanner.snapshot();
scanner.eat();
scanner.restore(snap);
restore(s: ScannerSnapshot): void
Restore the scanner to a previous snapshot.
If the snapshot contains line and column metadata, text position tracking is restored too. Otherwise line/column getters remain invalid until another text-synchronized snapshot is restored.
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('abc', { encoding: 'utf-8' });
const snap = scanner.snapshot();
scanner.eat(2);
scanner.restore(snap);
scanner.peek(); // 'a'
error(detail: string, span?: ScannerMark | {
from: ScannerMark;
to: ScannerMark;
}): ParseError
Create a ParseError at the current position or an explicit span.
The error message includes the scanner's format and either line/column or
byte offset. Passing a { from, to } span sets the diagnostic highlight
length. The method does not throw; callers usually throw scanner.error(...).
import { Scanner } from 'fino:parsing/scanner';
const scanner = new Scanner('abc', { encoding: 'utf-8', format: 'demo' });
const from = scanner.mark();
scanner.eat(2);
throw scanner.error('expected digit', { from, to: scanner.mark() });