diff --git a/packages/phoenix/packages/parsely/exports.js b/packages/phoenix/packages/parsely/exports.js index 29f956b9..85551cc7 100644 --- a/packages/phoenix/packages/parsely/exports.js +++ b/packages/phoenix/packages/parsely/exports.js @@ -1,6 +1,6 @@ import { adapt_parser, VALUE } from './parser.js'; import { Discard, FirstMatch, Optional, Repeat, Sequence } from './parsers/combinators.js'; -import { Fail, Literal, None, StringOf, Symbol } from './parsers/terminals.js'; +import { Fail, Literal, None, StringOf, StringUntil, Symbol } from './parsers/terminals.js'; class ParserWithAction { #parser; @@ -89,6 +89,7 @@ export const standard_parsers = () => { repeat: Repeat, sequence: Sequence, stringOf: StringOf, + stringUntil: StringUntil, symbol: Symbol, } } diff --git a/packages/phoenix/packages/parsely/parsers/terminals.js b/packages/phoenix/packages/parsely/parsers/terminals.js index 62c19df6..a7afaaa8 100644 --- a/packages/phoenix/packages/parsely/parsers/terminals.js +++ b/packages/phoenix/packages/parsely/parsers/terminals.js @@ -53,6 +53,53 @@ export class StringOf extends Parser { } } +/** + * Parses characters into a string, until it encounters the given character, unescaped. + * @param testOrCharacter End of the string. Either a character, or a function that takes a character, + * and returns whether it ends the string. + * @param escapeCharacter Character to use as the escape character. By default, is '\'. + */ +export class StringUntil extends Parser { + _create(testOrCharacter, { escapeCharacter = '\\' } = {}) { + if (typeof testOrCharacter === 'string') { + this.test = (c => c === testOrCharacter); + } else { + this.test = testOrCharacter; + } + this.escapeCharacter = escapeCharacter; + } + + _parse(stream) { + const subStream = stream.fork(); + let text = ''; + let lastWasEscape = false; + + while (true) { + let { done, value } = subStream.look(); + if ( done ) break; + if ( !lastWasEscape && this.test(value) ) + break; + + subStream.next(); + if (value === this.escapeCharacter) { + lastWasEscape = true; + continue; + } + lastWasEscape = false; + text += value; + } + + if (lastWasEscape) + return INVALID; + + if (text.length === 0) + return UNRECOGNIZED; + + stream.join(subStream); + return { status: VALUE, $: 'stringUntil', value: text }; + } +} + /** * Parses an object defined by the symbol registry. * @param symbolName The name of the symbol to parse.