/** * @typedef {Command[]} Script * @typedef {Word[]} Command * @typedef {object} Word * @property {string} text */ /** * A Pattern is a function that matches against a string starting at a given index. * * If it matches successfully, it returns some captured value, and the index following the match. * * @template T * @typedef {(source: string, index: number) => ([T, number] | null)} Pattern */ /** * Creates a pattern that wraps another pattern, transforming the returned value on a match * @template T, U * @param {Pattern} pattern * @param {(value: T)=> U} map * @return {Pattern} */ function MapPattern(pattern, map) { return function (source, index) { const match = pattern(source, index); return match ? [map(match[0]), match[1]] : null; }; } /** * Creates a pattern matching a regex & returning any captures. The regex needs to be sticky (using the //y modifier) * @param {RegExp} regex * @return {Pattern} */ function RegexPattern(regex) { return function (source, index) { regex.lastIndex = index; const matches = regex.exec(source); return matches ? [matches, regex.lastIndex] : null; }; } /** * @template T * @param {...Pattern} patterns * @return {Pattern} */ function Choose(...patterns) { return function (source, index) { for (const pattern of patterns) { const match = pattern(source, index); if (match) { return match; } } return null; }; } /** * @template {unknown[]} T * @param {{[K in keyof T]: Pattern}} patterns * @return {Pattern} */ function Sequence(...patterns) { return function (source, index) { const values = /** @type {T} */ (/** @type {unknown} */ ([])); for (const pattern of patterns) { const match = pattern(source, index); if (match == null) { return null; } values.push(match[0]); index = match[1]; } return [values, index]; }; } const InterCommandWhitespace = RegexPattern(/[^\S\n;]*/y); const CommentPattern = RegexPattern(/#.*\n/y); const PreWordWhitespace = RegexPattern(/[^\S\n;]*/y); const BasicWord = MapPattern(RegexPattern(/[^\s;]+/y), ([word]) => ({ text: word, })); const WordPattern = MapPattern( Sequence(PreWordWhitespace, BasicWord), ([_, word]) => word ); /** * Parse out a Notcl script into an easier-to-interpret representation. * No script is actually executed yet. * * @param {string} code * @returns Script */ function parseNotcl(code) { /* Preprocess */ // fold line endings code = code.replace(/(?