Skip to content

Commit d46b043

Browse files
committed
feat(parsely): Add stringUntil() parser
The counterpart of stringOf(), it reads characters until it matches its parameter.
1 parent 5656d9d commit d46b043

File tree

2 files changed

+49
-1
lines changed

2 files changed

+49
-1
lines changed

packages/phoenix/packages/parsely/exports.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { adapt_parser, VALUE } from './parser.js';
22
import { Discard, FirstMatch, Optional, Repeat, Sequence } from './parsers/combinators.js';
3-
import { Fail, Literal, None, StringOf, Symbol } from './parsers/terminals.js';
3+
import { Fail, Literal, None, StringOf, StringUntil, Symbol } from './parsers/terminals.js';
44

55
class ParserWithAction {
66
#parser;
@@ -89,6 +89,7 @@ export const standard_parsers = () => {
8989
repeat: Repeat,
9090
sequence: Sequence,
9191
stringOf: StringOf,
92+
stringUntil: StringUntil,
9293
symbol: Symbol,
9394
}
9495
}

packages/phoenix/packages/parsely/parsers/terminals.js

+47
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,53 @@ export class StringOf extends Parser {
5353
}
5454
}
5555

56+
/**
57+
* Parses characters into a string, until it encounters the given character, unescaped.
58+
* @param testOrCharacter End of the string. Either a character, or a function that takes a character,
59+
* and returns whether it ends the string.
60+
* @param escapeCharacter Character to use as the escape character. By default, is '\'.
61+
*/
62+
export class StringUntil extends Parser {
63+
_create(testOrCharacter, { escapeCharacter = '\\' } = {}) {
64+
if (typeof testOrCharacter === 'string') {
65+
this.test = (c => c === testOrCharacter);
66+
} else {
67+
this.test = testOrCharacter;
68+
}
69+
this.escapeCharacter = escapeCharacter;
70+
}
71+
72+
_parse(stream) {
73+
const subStream = stream.fork();
74+
let text = '';
75+
let lastWasEscape = false;
76+
77+
while (true) {
78+
let { done, value } = subStream.look();
79+
if ( done ) break;
80+
if ( !lastWasEscape && this.test(value) )
81+
break;
82+
83+
subStream.next();
84+
if (value === this.escapeCharacter) {
85+
lastWasEscape = true;
86+
continue;
87+
}
88+
lastWasEscape = false;
89+
text += value;
90+
}
91+
92+
if (lastWasEscape)
93+
return INVALID;
94+
95+
if (text.length === 0)
96+
return UNRECOGNIZED;
97+
98+
stream.join(subStream);
99+
return { status: VALUE, $: 'stringUntil', value: text };
100+
}
101+
}
102+
56103
/**
57104
* Parses an object defined by the symbol registry.
58105
* @param symbolName The name of the symbol to parse.

0 commit comments

Comments
 (0)