import { readFile } from 'node:fs/promises'; import { basename } from 'node:path'; export class CharacterStream { constructor() { this.reset(); } reset() { this._data = ''; this.fileName = ''; this._pos = 0; this.line = 0; this.column = 0; } async loadFile(fileName) { this.reset(); this.fileName = basename(fileName); this._data = await readFile(fileName, 'utf-8'); } peek(length = 1) { return this._data.substring(this._pos, this._pos + length); } peekWhile(predicate) { let buffer = ''; for ( let offset = 0, c = this._data[this._pos + offset]; c !== undefined && predicate(c); offset++, c = this._data[this._pos + offset] ) { buffer += c; } return buffer; } // Peek first character after any characters that match predicate peekAfter(predicate, length = 1) { const skip = this.peekWhile(predicate); return this.peek(skip.length + length).substring(skip.length); } isNext(str) { return this.peek(str.length) === str; } next(length = 1) { const chunk = this.peek(length); this._pos += chunk.length; this.line += this._countOccurrences(chunk, '\n'); const lastNewLine = chunk.lastIndexOf('\n'); if (lastNewLine === -1) { this.column += chunk.length; } else { this.column = chunk.length - lastNewLine - 1; } return chunk; } nextWhile(predicate) { return this.next(this.peekWhile(predicate).length); } _countOccurrences = (str, substr) => { let count = 0; let i = 0; while(true) { i = str.indexOf(substr, i); if (i === -1) break; i += substr.length; count++; } return count; } eof() { return this.peek() === ''; } }