/// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones import { TokenType, Token, Item, GroupInProgress } from './tokens.js'; import { Pos, advancePos } from './position.js'; export abstract class Scanner implements IterableIterator { readonly pos: Pos; readonly synthetic: boolean | undefined; charBuffer: string | null = null; tokenBuffer: Token | null = null; delimiters = ' \t\n\r\'"`.,;()[]{}/'; constructor(pos: Pos, synthetic?: boolean) { this.pos = { ... pos }; this.synthetic = synthetic; } [Symbol.iterator](): IterableIterator { return this; } abstract _peekChar(): string | null; abstract _dropChar(): void; peekChar(): string | null { if (this.charBuffer !== null) return this.charBuffer; this.charBuffer = this._peekChar(); return this.charBuffer; } dropChar() { if (this.charBuffer === null) this.peekChar(); if (this.charBuffer !== null) { this._dropChar(); advancePos(this.pos, this.charBuffer); this.charBuffer = null; } } shiftChar(): string | null { const ch = this.peekChar(); this.dropChar(); return ch; } makeToken(start: Pos, type: TokenType, text: string): Token { return { type, start, end: this.mark(), text, ... this.synthetic && { synthetic: true } }; } makeGroupInProgress(open: Token, items: Array = []): GroupInProgress { return { start: open.start, open, close: null, items, ... this.synthetic && { synthetic: true } }; } mark(): Pos { return { ... this.pos }; } _while(pred: (ch: string | null) => boolean, f: (ch: string | null) => void) { while (true) { const ch = this.peekChar(); if (!pred(ch)) return; this.dropChar(); f(ch); } } _collectSpace(buf = '', start = this.mark()): Token { this._while(ch => ch !== null && this.isSpace(ch), ch => buf = buf + ch); return this.makeToken(start, TokenType.SPACE, buf); } _punct(type: TokenType): Token { return this.makeToken(this.mark(), type, this.shiftChar()!); } _str(q: string, buf: string, isTemplate: boolean): Token { const start = this.mark(); let ch: string | null; while (true) { ch = this.shiftChar(); if (ch !== null) buf = buf + ch; if (ch === null || ch === q || (isTemplate ? (ch === '$' && this.peekChar() === '{') : (ch === '\n'))) { return this.makeToken(start, TokenType.STRING, buf); } if (ch === '\\') { ch = this.shiftChar(); if (ch === '\n') { // Do nothing. Line continuation. } else if (ch !== null) { buf = buf + ch; } } } } templateConstantFragment(): Token | null { if (this.tokenBuffer !== null) throw new Error("Internal error: templateConstantFragment"); const t = this._str('`', '', true); return (t.text.length === 0) ? null : t; } isSpace(ch: string): boolean { return ' \t\r'.indexOf(ch) >= 0; } isDelimiter(ch: string): boolean { return this.delimiters.indexOf(ch) >= 0; } addDelimiters(newDelimiters: string) { this.delimiters = this.delimiters + newDelimiters; } _atom(start = this.mark(), buf = ''): Token { let ch: string | null; while (true) { ch = this.peekChar(); if (ch === null || this.isDelimiter(ch)) { return this.makeToken(start, TokenType.ATOM, buf); } buf = buf + ch; this.dropChar(); } } _maybeComment(): Token { const start = this.mark(); let buf = this.shiftChar()!; let ch = this.peekChar(); if (ch === null) return this._collectSpace(buf, start); switch (ch) { case '/': // single-line comment. this._while(ch => ch !== null && ch !== '\n', ch => buf = buf + ch); return this._collectSpace(buf, start); case '*': // delimited comment. { let seenStar = false; buf = buf + this.shiftChar(); while (true) { ch = this.shiftChar(); if (ch === null) break; buf = buf + ch; if ((ch === '/') && seenStar) break; seenStar = (ch === '*'); } return this._collectSpace(buf, start); } default: return this._atom(start, buf); } } _peek(): Token | null { let ch = this.peekChar(); if (ch === null) return null; switch (ch) { case ' ': case '\t': case '\r': return this._collectSpace(); case '\n': return this._punct(TokenType.NEWLINE); case '(': case '[': case '{': return this._punct(TokenType.OPEN); case ')': case ']': case '}': return this._punct(TokenType.CLOSE); case '\'': case '"': this.dropChar(); return this._str(ch, ch, false); case '`': this.dropChar(); return this._str(ch, ch, true); case '/': return this._maybeComment(); default: if (this.isDelimiter(ch)) { return this._punct(TokenType.ATOM); } else { return this._atom(this.mark(), this.shiftChar()!); } } } peek(): Token | null { if (this.tokenBuffer === null) this.tokenBuffer = this._peek(); return this.tokenBuffer; } drop() { if (this.tokenBuffer === null) this.peek(); this.tokenBuffer = null; } shift(): Token | null { const t = this.peek(); this.drop(); return t; } next(): IteratorResult { const t = this.shift(); if (t === null) { return { done: true, value: null }; } else { return { done: false, value: t }; } } } export class StringScanner extends Scanner { readonly input: string; index: number; constructor(pos: Pos, input: string, synthetic?: boolean) { super(pos, synthetic); this.input = input; this.index = 0; } _peekChar(): string | null { return this.input[this.index] ?? null; } _dropChar(): void { this.index++; } }