/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://docs.moderne.io/licensing/moderne-source-available-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ExecutionContext} from "./execution";
import {SourceFile} from "./tree";
import fs, {readFileSync} from "node:fs";
import {isAbsolute, relative} from "path";
import {ParseError, ParseErrorKind} from "./parse-error";
import {markers, MarkersKind, ParseExceptionResult} from "./markers";
import {randomId} from "./uuid";
export type SourcePath = string
export type ParserInput = SourcePath | { text: string, sourcePath: string }
export function parserInputRead(input: ParserInput): string {
if (typeof input === "object") {
return input.text;
}
return fs.readFileSync(input).toString()
}
export function parserInputFile(input: ParserInput): string {
if (typeof input === "object") {
return input.sourcePath;
}
return input;
}
export interface ParserOptions {
ctx?: ExecutionContext;
relativeTo?: string;
}
export abstract class Parser {
constructor({
ctx = new ExecutionContext(),
relativeTo
}: ParserOptions = {}) {
this.ctx = ctx;
this.relativeTo = relativeTo;
}
protected ctx: ExecutionContext;
protected readonly relativeTo?: string;
abstract parse(...sourcePaths: ParserInput[]): AsyncGenerator
/**
* Parses a single input and returns the first source file.
* This is a convenience method for when you know you're parsing exactly one input.
*
* @param input The input to parse
* @returns The parsed source file
* @throws Error if the parser produces no results
*/
async parseOne(input: ParserInput): Promise {
const result = await this.parse(input).next();
if (result.done) {
const sourcePath = typeof input === 'string' ? input : input.sourcePath;
throw new Error(`Parser produced no results for: ${sourcePath}`);
}
return result.value;
}
protected relativePath(sourcePath: ParserInput): string {
const path = typeof sourcePath === "string" ? sourcePath : sourcePath.sourcePath;
return isAbsolute(path) && this.relativeTo ? relative(this.relativeTo, path) : path;
}
protected error(input: ParserInput, e: Error): ParseError {
return {
kind: ParseErrorKind,
id: randomId(),
markers: markers({
kind: MarkersKind.ParseExceptionResult,
id: randomId(),
parserType: this.constructor.name,
exceptionType: e.name,
message: e.message + ':\n' + e.stack,
} satisfies ParseExceptionResult as ParseExceptionResult),
text: parserInputRead(input),
sourcePath: this.relativePath(input),
}
}
}
/**
* A source reader that can be used by parsers to incrementally parse a source, keeping
* track of the position of the parsing.
*/
export class ParserSourceReader {
readonly source: string
cursor: number = 0;
constructor(public readonly sourcePath: ParserInput) {
this.source = readSourceSync(sourcePath)
}
whitespace(): string {
function isWhitespace(char: string): boolean {
const code = char.charCodeAt(0);
return code === 32 || (code >= 9 && code <= 13);
}
const start = this.cursor;
while (this.cursor < this.source.length && isWhitespace(this.source[this.cursor])) {
this.cursor++;
}
return this.source.slice(start, this.cursor);
}
sourceBefore(token: string): string {
const start = this.cursor;
// increment cursor until we find the token
while (this.cursor < this.source.length && !this.source.startsWith(token, this.cursor++)) {
}
return this.source.slice(start, this.cursor - token.length)
}
/**
* Used in debugging parsers during development to see where the
* cursor is in the source.
*/
// noinspection JSUnusedGlobalSymbols
get afterCursor(): string {
return this.source.slice(this.cursor);
}
}
export function readSourceSync(sourcePath: ParserInput) {
if (typeof sourcePath === "string") {
return readFileSync(sourcePath).toString();
}
return sourcePath.text;
}
type ParserConstructor = new (options?: ParserOptions) => T;
export type ParserType = "javascript" | "packageJson" | "json" | "yaml" | "plainText";
export class Parsers {
private static registry = new Map>();
static registerParser(
name: ParserType,
parserClass: ParserConstructor
): void {
Parsers.registry.set(name, parserClass as ParserConstructor);
}
static createParser(name: ParserType, options?: ParserOptions): Parser {
const ParserClass = Parsers.registry.get(name);
if (!ParserClass) {
throw new Error(`No parser registered with name: ${name}`);
}
return new ParserClass(options);
}
}