202 lines
7.1 KiB
TypeScript
202 lines
7.1 KiB
TypeScript
/// SPDX-License-Identifier: GPL-3.0-or-later
|
|
/// SPDX-FileCopyrightText: Copyright © 2016-2022 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
|
|
import { TokenType, Token, Group, GroupInProgress, Item, Items, finishGroup } from './tokens.js';
|
|
import { Pos, startPos } from './position.js';
|
|
import { Scanner, StringScanner } from './scanner.js';
|
|
|
|
function matchingParen(c: string): string | null {
|
|
switch (c) {
|
|
case ')': return '(';
|
|
case ']': return '[';
|
|
case '}': return '{';
|
|
default: return null;
|
|
}
|
|
}
|
|
|
|
export class LaxReader implements IterableIterator<Item> {
|
|
readonly scanner: Scanner;
|
|
readonly stack: Array<GroupInProgress> = [];
|
|
|
|
constructor(scanner: Scanner) {
|
|
this.scanner = scanner;
|
|
}
|
|
|
|
[Symbol.iterator](): IterableIterator<Item> {
|
|
return this;
|
|
}
|
|
|
|
stackTop(): GroupInProgress | null {
|
|
return this.stack[this.stack.length - 1] ?? null;
|
|
}
|
|
|
|
popUntilMatch(t: Token): Group | 'continue' | 'eof' {
|
|
const m = matchingParen(t.text);
|
|
|
|
if (m !== null && !this.stack.some(g => g.open.text === m)) {
|
|
if (this.stack.length > 0) {
|
|
this.stackTop()!.items.push(t);
|
|
return 'continue';
|
|
}
|
|
} else {
|
|
while (this.stack.length > 0) {
|
|
const inner = finishGroup(this.stack.pop()!, t.end);
|
|
if (inner.open.text === m) {
|
|
inner.close = t;
|
|
}
|
|
|
|
if (this.stack.length === 0) {
|
|
return inner;
|
|
} else {
|
|
const outer = this.stackTop()!;
|
|
outer.items.push(inner);
|
|
if (inner.open.text === m) {
|
|
return 'continue';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return 'eof';
|
|
}
|
|
|
|
_eofClose(): Token {
|
|
return this.scanner.makeToken(this.scanner.mark(), TokenType.CLOSE, '');
|
|
}
|
|
|
|
peek(): Token {
|
|
return this.scanner.peek() ?? this._eofClose();
|
|
}
|
|
|
|
drop() {
|
|
this.scanner.drop();
|
|
}
|
|
|
|
inTemplateString(): boolean {
|
|
const i = this.stackTop();
|
|
return (i !== null) && (i.open.text[0] === '`');
|
|
}
|
|
|
|
expectTemplateVariablePart() {
|
|
const variablePartOpen = this.peek();
|
|
if (variablePartOpen.type !== TokenType.OPEN || variablePartOpen.text !== '{')
|
|
{
|
|
throw new Error("Internal parser error: template string variable part mismatch");
|
|
}
|
|
this.drop();
|
|
this.stack.push(this.scanner.makeGroupInProgress(variablePartOpen));
|
|
}
|
|
|
|
read(): Item | null {
|
|
while (true) {
|
|
if (this.inTemplateString()) {
|
|
const t = this.scanner.templateConstantFragment() ?? this._eofClose();
|
|
switch (t.type) {
|
|
case TokenType.CLOSE: // always means eof, in this case
|
|
return this.popUntilMatch(t) as Group; // will always be a Group
|
|
case TokenType.STRING:
|
|
if (t.text[t.text.length - 1] === '`') {
|
|
const inner = finishGroup(this.stack.pop()!, t.end);
|
|
inner.close = t;
|
|
if (this.stack.length === 0) {
|
|
return inner;
|
|
} else {
|
|
this.stackTop()!.items.push(inner);
|
|
break;
|
|
}
|
|
} else {
|
|
// Another unclosed fragment. Expect {...} again.
|
|
this.stackTop()!.items.push(t);
|
|
this.expectTemplateVariablePart();
|
|
break;
|
|
}
|
|
default:
|
|
throw new Error("Internal error: LaxReader.read()");
|
|
}
|
|
} else {
|
|
let g = this.stackTop();
|
|
const t = this.peek();
|
|
switch (t.type) {
|
|
case TokenType.STRING:
|
|
if (t.text[0] === '`' && t.text[t.text.length - 1] !== '`') {
|
|
// Unclosed template string - so expect {...} followed by more
|
|
// fragments. Encode this as a group with opener the first constant
|
|
// template fragment and closer the last template fragment.
|
|
this.drop();
|
|
this.stack.push(this.scanner.makeGroupInProgress(t));
|
|
// We know, from the implementation of the scanner, that the current
|
|
// character is '{'.
|
|
this.expectTemplateVariablePart();
|
|
break;
|
|
} else {
|
|
// fall through
|
|
}
|
|
case TokenType.SPACE:
|
|
case TokenType.NEWLINE:
|
|
case TokenType.ATOM:
|
|
if (g === null) {
|
|
this.drop();
|
|
return t;
|
|
}
|
|
if (t.text === ';') {
|
|
while ('(['.indexOf(g.open.text) >= 0) {
|
|
this.stack.pop();
|
|
const outer = this.stackTop();
|
|
if (outer === null) {
|
|
// do not drop the semicolon here
|
|
return finishGroup(g, t.start);
|
|
}
|
|
outer.items.push(finishGroup(g, t.start));
|
|
g = outer;
|
|
}
|
|
}
|
|
this.drop();
|
|
g.items.push(t);
|
|
break;
|
|
|
|
case TokenType.OPEN:
|
|
this.drop();
|
|
this.stack.push(this.scanner.makeGroupInProgress(t));
|
|
break;
|
|
|
|
case TokenType.CLOSE: {
|
|
this.drop();
|
|
const i = this.popUntilMatch(t);
|
|
if (i === 'eof') return null;
|
|
if (i === 'continue') break;
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
readToEnd(): Items {
|
|
return Array.from(this);
|
|
}
|
|
|
|
next(): IteratorResult<Item> {
|
|
const i = this.read();
|
|
if (i === null) {
|
|
return { done: true, value: null };
|
|
} else {
|
|
return { done: false, value: i };
|
|
}
|
|
}
|
|
}
|
|
|
|
export interface LaxReadOptions {
|
|
start?: Pos,
|
|
name?: string,
|
|
extraDelimiters?: string,
|
|
synthetic?: boolean,
|
|
}
|
|
|
|
export function laxRead(source: string, options: LaxReadOptions = {}): Items {
|
|
const start = options.start ?? startPos(options.name ?? null);
|
|
const scanner = new StringScanner(start, source, options.synthetic);
|
|
if (options.extraDelimiters) scanner.addDelimiters(options.extraDelimiters);
|
|
const reader = new LaxReader(scanner);
|
|
return reader.readToEnd();
|
|
}
|