UNPKG

harmonyc

Version:

Harmony Code - model-driven BDD for Vitest

77 lines (76 loc) 5.2 kB
import { alt_sc, apply, expectEOF, expectSingleResult, kright, opt_sc, rep_sc, seq, tok, list_sc, kleft, kmid, fail, nil, unableToConsumeToken, } from 'typescript-parsec'; import { T, lexer } from "./lexer.js"; import { Action, Response, CodeLiteral, StringLiteral, Section, Step, Docstring, Word, Label, ErrorResponse, SaveToVariable, SetVariable, Switch, } from "../model/model.js"; export function parse(input, production = TEST_DESIGN) { const tokens = lexer.parse(input); return expectSingleResult(expectEOF(production.parse(tokens))); } function anythingBut(kind) { return { parse(token) { if (token === undefined) return { successful: false, error: unableToConsumeToken(token) }; if (token.kind === kind) return { successful: false, error: unableToConsumeToken(token) }; return { candidates: [ { firstToken: token, nextToken: token.next, result: token, }, ], successful: true, error: undefined, }; }, }; } export const NEWLINES = list_sc(tok(T.Newline), nil()), WORDS = apply(tok(T.Words), ({ text }) => new Word(text.trimEnd().split(/\s+/).join(' '))), DOUBLE_QUOTE_STRING = alt_sc(apply(tok(T.DoubleQuoteString), ({ text }) => new StringLiteral(JSON.parse(text))), seq(tok(T.UnclosedDoubleQuoteString), fail('unclosed double-quote string'))), BACKTICK_STRING = apply(tok(T.BacktickString), ({ text }) => new CodeLiteral(text.slice(1, -1))), DOCSTRING = kright(opt_sc(NEWLINES), apply(list_sc(tok(T.MultilineString), tok(T.Newline)), (lines) => new Docstring(lines.map(({ text }) => text.slice(2)).join('\n')))), ERROR_MARK = tok(T.ErrorMark), VARIABLE = apply(tok(T.Variable), ({ text }) => text.slice(2, -1)), SIMPLE_PART = alt_sc(WORDS, DOUBLE_QUOTE_STRING, BACKTICK_STRING, DOCSTRING), //REPEATER = apply( //list_sc(rep_sc(SIMPLE_PART), tok(T.And)), //(cs) => new Repeater(cs) //), SWITCH = apply(list_sc(SIMPLE_PART, tok(T.Slash)), (cs) => new Switch(cs)), //ROUTER = apply(list_sc(REPEATER, tok(T.Semicolon)), (cs) => new Router(cs)), BRACES = kmid(tok(T.OpeningBrace), SWITCH, tok(T.ClosingBrace)), PART = alt_sc(SIMPLE_PART, BRACES), PHRASE = rep_sc(PART), ARG = alt_sc(DOUBLE_QUOTE_STRING, BACKTICK_STRING, DOCSTRING), SET_VARIABLE = apply(seq(VARIABLE, ARG), ([variable, value]) => new SetVariable(variable, value)), ACTION = alt_sc(SET_VARIABLE, apply(PHRASE, (parts) => new Action(parts))), RESPONSE = apply(seq(PHRASE, opt_sc(VARIABLE)), ([parts, variable]) => new Response(parts, variable !== undefined ? new SaveToVariable(variable) : undefined)), ERROR_RESPONSE = apply(seq(ERROR_MARK, opt_sc(alt_sc(DOUBLE_QUOTE_STRING, DOCSTRING))), ([, parts]) => new ErrorResponse(parts)), SAVE_TO_VARIABLE = apply(VARIABLE, (variable) => new Response([], new SaveToVariable(variable))), ARROW = kright(opt_sc(NEWLINES), tok(T.ResponseArrow)), RESPONSE_ITEM = kright(ARROW, alt_sc(SAVE_TO_VARIABLE, ERROR_RESPONSE, RESPONSE)), STEP = apply(seq(ACTION, rep_sc(RESPONSE_ITEM)), ([action, responses]) => new Step(action, responses).setFork(true)), LABEL = apply(kleft(list_sc(PART, nil()), tok(T.Colon)), (words) => new Label(words.map((w) => w.toString()).join(' '))), SECTION = apply(LABEL, (text) => new Section(text)), BRANCH = alt_sc(SECTION, STEP), // section first, to make sure there is no colon after step DENTS = apply(alt_sc(tok(T.Plus), tok(T.Minus)), (seqOrFork) => { return { dent: (seqOrFork.text.length - 2) / 2, isFork: seqOrFork.kind === T.Plus, }; }), NODE = apply(seq(DENTS, BRANCH), ([{ dent, isFork }, branch], [start, end]) => ({ dent, branch: branch.setFork(isFork), })), ANYTHING_BUT_NEWLINE = anythingBut(T.Newline), TEXT = apply(seq(tok(T.Words), rep_sc(ANYTHING_BUT_NEWLINE)), () => undefined), LINE = alt_sc(NODE, TEXT), TEST_DESIGN = kmid(rep_sc(NEWLINES), apply(opt_sc(list_sc(apply(LINE, (line, [start, end]) => ({ line, start, end })), NEWLINES)), (lines) => { let dent; const root = new Section(new Label('')); let parent = root; for (const { line, start } of lines !== null && lines !== void 0 ? lines : []) { if (line === undefined) continue; const { dent: d, branch } = line; if (dent === undefined) { if (d !== 0) throw new Error(`invalid indent ${d} at line ${start.pos.rowBegin}: first step must not be indented`); dent = 0; } else if (Math.round(d) !== d) { throw new Error(`invalid odd indent of ${d * 2} at line ${start.pos.rowBegin}`); } else if (d > dent + 1) { throw new Error(`invalid indent ${d} at line ${start.pos.rowBegin}`); } else if (d === dent + 1) { parent = parent.children[parent.children.length - 1]; ++dent; } else while (d < dent) { parent = parent.parent; --dent; } parent.addChild(branch); } return root; }), rep_sc(NEWLINES));