Vocabulary
Every name is chosen by one test: read the pattern aloud, and it should say what it matches.
describe() does exactly that, so any pattern can be checked against its intent.
import { sequence, capture, oneOrMore, letter, wordChar, zeroOrMore, optional, charIn } from 'tosijs-pattern'
import { show } from 'tosijs-pattern/demo'
const identifier = sequence(letter, zeroOrMore(wordChar))
const flag = sequence('--', capture('name', identifier), optional(sequence('=', capture('value', oneOrMore(charIn('a-z0-9'))))))
show(preview, flag.describe(), flag.parse('--level=9').captures)
Nouns: one character
| Name | Matches | Regex |
|---|---|---|
digit |
0-9 | \d |
hexDigit |
0-9, a-f, A-F | [0-9a-fA-F] |
letter |
A-Z, a-z (ASCII for now) | [A-Za-z] |
lowercase, uppercase |
a-z, A-Z | [a-z], [A-Z] |
wordChar |
A-Z, a-z, 0-9, _ | \w |
whitespace |
exactly JavaScript's \s |
\s |
blank |
space or tab | [ \t] |
anyChar |
any character, line breaks included | [^], or . with s |
charIn('a-z_') |
one of a range string | [a-z_] |
charNotIn(',\n') |
anything else | [^,\n] |
A range string is characters and x-y ranges. A - first or last is itself, and \ makes the
next character literal. There are no class escapes inside it: name the noun instead.
Positions: match no characters
| Name | Matches | Regex |
|---|---|---|
start, end |
the start or end of the input | ^, $ |
lineStart, lineEnd |
the start or end of a line | ^, $ with m |
followedBy(p) |
if p comes next |
(?=p) |
notFollowedBy(p) |
if p does not come next |
(?!p) |
Verbs: combine patterns
| Name | Matches | Regex |
|---|---|---|
sequence(a, b, …) |
each in order | ab… |
oneOf(a, b, …) |
the FIRST that matches | a|b |
optional(p) |
p or nothing |
p? |
zeroOrMore(p), oneOrMore(p) |
as many as match | p*, p+ |
times(n, p) |
exactly n | p{n} |
between(m, n, p), atLeast(n, p), atMost(n, p) |
bounded repetition | p{m,n}, p{n,}, p{0,n} |
caseless('text') |
the text in any case | (?i:text) |
capture('name', p) |
p, recording it |
(?<name>p) |
describedAs('an id', p) |
p, named for messages |
(none) |
A plain string anywhere is that literal text.
Structure: what regex cannot express
| Name | Matches |
|---|---|
until(p) |
everything up to the first p (.*? before p, without the retrying) |
quoted('"', { escape }) |
a quoted span; escape is a character or 'double' (CSV's ""); its value is the inside |
balanced('(', ')', { quotes }) |
a nested span that closes at its own delimiter, skipping quoted text; its value is the inside |
separatedBy(item, sep) |
a list; its value is an array |
number, integer |
JSON's number grammar; the value is a number |
Values and captures
A capture records its pattern's VALUE: the text it matched, unless the piece has a better one
(number gives a number, quoted the unescaped inside, separatedBy an array). A capture
inside a repetition is collected by capturing the repetition, one entry per item:
import { capture, oneOrMore, sequence, letter, integer, optional } from 'tosijs-pattern'
import { show } from 'tosijs-pattern/demo'
const pair = sequence(capture('key', oneOrMore(letter)), '=', capture('value', integer), optional(';'))
const settings = capture('settings', oneOrMore(pair))
show(preview, settings.parse('width=3;height=4').captures)
Beside its value, every capture keeps the TEXT it matched, as written: texts on a parse
result or a match. {name} in a replacement template inserts the text (so $3.50 stays
3.50, and a quoted capture keeps its quotes); a replacement function sees both.
A capture that would be silently lost is an error that says what to write instead: inside a
repetition nobody captures, inside followedBy/notFollowedBy/until's stopping point, or a
name used twice where the second would overwrite the first (oneOf options may share one).