diff --git a/README.md b/README.md index 7c9a8fd..3d60881 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ It also allows for Spatial Tracking or various sub-regions within the source. It - `SourceLocation` is basically a smart 2D coordinate equivalent to `(line, col)` (but also tracks `CodePointIndex`) - `Span` an interval determined by `start` and `end` SourceLocations +# Source Cursor +- `SourceCursor` is a mutable cursor over `SourceRegion`. Primarily useful to build parsers on top of `SourceRegion`. It is line-aware. # Rendering CLI Errors Secondary functionality is `function renderSpan(region: SourceRegion, span: Span, contextLines = 1): LineView[]` which is able to render spans of source-code as follows diff --git a/src/index.ts b/src/index.ts index 82ed23a..e030add 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ export const UPPERCASE_F: CodePoint = char('F'); export const LOWERCASE_z: CodePoint = char('z'); export const UPPERCASE_Z: CodePoint = char('Z'); +// === Predicates === export function isBetween(a: CodePoint, x: CodePoint, b: CodePoint): boolean { return a <= x && x <= b; @@ -46,6 +47,17 @@ export function isAsciiAlphanumeric(x: CodePoint): boolean { return isAsciiAlpha(x) || isDigit(x); } +export function isAsciiWhitespace(cp: CodePoint): boolean { + return cp === SPACE + || cp === TAB + || cp === NEW_LINE + || cp === CARRIAGE_RETURN; +} + +export function isAsciiInlineWhitespace(cp: CodePoint): boolean { + return cp === SPACE || cp === TAB; +} + export type CodePointRef = { char: CodePoint, offset: StringIndex, @@ -366,6 +378,75 @@ export type SourceLocation = { column: number; // 1-based } +export class SourceCursor { + private index: CodePointIndex; + + constructor(public readonly region: SourceRegion) { + this.index = region.span.start.index; + } + + current(): CodePointIndex { + return this.index; + } + + checkpoint(): CodePointIndex { + return this.index; + } + + restore(index: CodePointIndex) { + this.index = index; + } + + peek(): CodePoint | undefined { + if (this.index >= this.region.span.end.index) return undefined; + return this.region.codePointAt(this.index); + } + + advance(): CodePoint | undefined { + const cp = this.peek(); + if (cp === undefined) return undefined; + this.index += 1; + return cp; + } + + isAtEnd(): boolean { + return this.index >= this.region.span.end.index; + } + + spanFrom(start: CodePointIndex): CodePointSpan { + return rawSpan(start, this.index); + } + + currentSpan(): CodePointSpan { + return this.isAtEnd() + ? pointSpan(this.index) + : rawSpan(this.index, this.index + 1); + } + + eofSpan(): CodePointSpan { + return pointSpan(this.region.span.end.index); + } + + slice(span: CodePointSpan): string { + return this.region.slice(span); + } + + moveToNextLineStart(): void { + const loc = this.region.source.getLocation(this.index); + const nextLine = loc.line + 1; + + if (nextLine > this.region.span.end.line) { + this.index = this.region.span.end.index; + return; + } + + const range = this.region.source.getLineRange(nextLine); + this.index = Math.min(range.start, this.region.span.end.index); + } +} + + + // === Rendering Utilities === export type LineView = {