import { escapeHtml } from "./helpers"; import { AtLeast, Choose, End, Pattern, Regex, Sequence, Use } from "./peg"; import { Script, Word as WordType, TextWord, EnchantedWord as EnchantedWordType, SimplifyWord, InterpolatedPiece, } from "./words"; const Comment = Regex(/#[^\n]*/y) .expects("#") .map(() => []); const PreWordWhitespace = Regex(/[^\S\n;]+/y).expects("whitespace"); const EnchantedWord = Regex(/[^\]\[\}\{$\\";\s]+(?=[\s;]|$)/y) .map(([enchanted]) => ({ enchanted } as EnchantedWordType)) .expects("ENCHANTED_WORD"); const BackslashEscape = Regex(/\\(.)/y) .expects("\\") .map(([, char]) => ({ text: char })); const BARE_WORD_CHAR = /[^\s\\;]+/y; function bareWordTmpl(charRegex: RegExp) { return Sequence( AtLeast( 1, Choose( BackslashEscape, Regex(charRegex) .expects("CHAR") .map(([text]) => ({ text })) ) ) ).map(([pieces]) => SimplifyWord(pieces)); } const QuotedWord = Sequence( Regex(/"/y).expects('"'), AtLeast( 0, Choose( BackslashEscape, Regex(/[^"\\]+/y) .expects("CHAR") .map(([text]) => ({ text })) ) ), Regex(/"/y).expects('"') ).map(([, pieces]) => SimplifyWord(pieces)); const Brace: Pattern = Sequence( Regex(/\{/y).expects("{"), AtLeast( 0, Choose( Use(() => Brace) .expects("{") .map((text) => `{${text}}`), Regex(/\\./y) .expects("\\") .map(([escape]) => escape), Regex(/[^\\{}]+/y) .expects("text") .map(([text]) => text) ) ), Regex(/\}/y).expects("}") ).map(([, fragments]) => fragments.join("")); function wordTmpl(bareWordCharRegex: RegExp): Pattern { return Choose( EnchantedWord, Brace.map((text) => ({ text } as TextWord)), QuotedWord, bareWordTmpl(bareWordCharRegex) ); } const CommandTerminator = Regex(/[\n;]/y) .expects("NEWLINE | ;") .map(() => true); function commandTmpl(bareWordCharRegex: RegExp) { const word = wordTmpl(bareWordCharRegex); return Sequence( word, AtLeast( 0, Sequence(PreWordWhitespace, word).map(([, word]) => word) ), AtLeast(0, PreWordWhitespace) ).map(([word, moreWords]) => [word].concat(moreWords)); } function scriptTmpl(bareWordCharRegex: RegExp, endPattern: Pattern) { return Sequence( AtLeast( 0, Choose( PreWordWhitespace.map(() => []), CommandTerminator.map(() => []), Sequence(Comment, Choose(CommandTerminator, endPattern)).map(() => []), Sequence( commandTmpl(bareWordCharRegex), Choose(CommandTerminator, endPattern) ).map(([words]) => words) ) ), endPattern ).map(([commands]) => commands.filter((command) => command.length > 0)); } const Script = scriptTmpl(BARE_WORD_CHAR, End()); const ERROR_CONTEXT = /(?<=([^\n]{0,50}))([^\n]{0,50})/y; /** * Parse out a Notcl script into an easier-to-interpret representation. * No script is actually executed yet. * * @param - code to parse * @returns - parsed list of commands, or error message on failure */ export function parse(code: string): [true, Script] | [false, string] { /* Preprocess */ // fold line endings code = code.replace(/(?Error at position ${errorPos} ${escapeHtml(before + "" + after)} ${"-".repeat(before.length)}^ Expected: ${escapeHtml(expected)}`, ]; } }