6.6 KiB
regexoid
funny regex thingy
this library exposes a singular function called regexoid:
import { regexoid } from 'regexoid';
// var { regexoid } = require('regexoid');
// create a regex factory-function
let regex = regexoid(({ seq, any }) => (
seq(any('Goodbye','Hello'),' ','world!')
// matches either "Goodbye world!" or "Hello world!"
));
// instantiate a regex
let eater = regex()
// process text
console.log(eater('Hello world!'))
// { done: true, match: {}, leftover: '', leftoverCount: 0 }
the regexoid() function calls the user-provided function f with an object
full of helper functions r provided as the first argument. f must return
a "regex element", which is one of:
- a string (matches itself)
- the return value of one of the helper functions:
chars(pred): match a single character if the predicate function returns truechars.any(...pred): eachpredis an array of the form['A','Z'](match a range of characters) or['abcd'](match any of specified characters)chars.none(...pred): likechars.any, but invertedseq(...elems): match every passed regex element one after anotherany(...elems): match any of the regex elements (or rather, the longest matching; if multiple are the same length, first in the list wins)cond([pred1, ...elem1], [pred2, ...elem2], ...): the lookahead operator. executepredNand immediately backtrack; ifpredNmatched, runelemN, otherwise try the nextpred/elempairrep([min,max], ...elems): repeat the match at leastmin(or 0) times and no more thanmax(or Infinity) times.min/maxcan be a number, nullish, or a string/symbol. if strings/symbols are used, the corresponding edge of the limit is looked up in the results table dynamicallyrep(lim, ...elems): eqv torep([lim,lim], ...elems)rep.strict: a non-backtracking version ofreprep.label(label): add a label to arep, this is targeted bymarkBreak'smarkBreak(label=null): leave areploop early. note that this does not cancel the current iteration.capture(key, ...elems): "capture" the enclosed elements into a named variable in the results. if the value already exists, regex matching abortsmark(key, value): set a named variable to the specified valuecaptureMulti,markMulti: same as previous two, but append to array instead of setting the variableoutput(k, v=k): return a value from the result set during incomplete regexoid execution. the regexoid must not backtrack past this operator, otherwise an error will be thrown.captureOutput(name, ...elem): executeelemand return any input chunks matched by them during execution.elemmust never backtrack, or an error will be thrownmap(fn): callfnpassing an object with the following helper functions:get(k): get a value from the captures set (nullif missing). note that lists/"multi captures" makegetreturn an iterator from the last capture to the firstset(k,v): set a value to the captures set; if any existed it will be replaced. null deletes the valueadd(k,v): add a value to a list in the captures set; if the key is already taken by a non-list value the operation fails and returns false (otherwise true)fnmay return a value. if the value isfalse, regex matching aborts
abort(): abort regex matchinglimit(n, ...elem): executeelembut do not allow them to consume more than n characters (this is done with a simulated EOF)
the regex elements may be mixed and matched to produce a complex regex
regexoid() returns a constructor function that can then be used to instantiate
a regex processing stream. the regex instance is also a function, taking
a chunk of text as argument and returning the result of parsing, i.e an object
with the following fields:
- done:
falseif the regex accepts more data,trueotherwise - match: when the regex is done, this will contain any variables captured by the regex.
- leftoverCount: sometimes the regex may finish before reading the whole chunk, or even retroactively reject multiple chunks' worth of text. this property allows you to know how much of the submitted data was left unread.
- leftover: unread data, as a single string
regex matching is always anchored to the beginning. captures are only present in the results table
when the path the capture is in is actually taken and matched; that also means you can
re-use the same name for multiple different captures even without captureMulti/markMulti, as long
as only one of them is matched in any given run (e.g. if they're in distinct branches of an any()).
pass no arguments to the regex object will signal EOF to the regex.
More complex example:
import { regexoid } from 'regexoid';
let o = regexoid(({
seq, any, rep, chars,
mark, markMulti,
capture, captureMulti,
map, abort,
}) => (
(({field, spc}) => (
field = (field => () => any(
field(),
captureMulti('unknownFields',
rep([1,], chars(c => c >= 'a' && c <= 'z'
|| c >= 'A' && c <= 'Z')),
any('',seq('(',rep([],chars(c => c != ')')),')'))
),
))(field),
seq(
spc(), rep([], field(), any(',',';'), spc()), field(), spc()
)
))({
field: () => (any(
...['meow', 'meower', 'sigma', 'susser']
.map(k => (
seq(
seq(
...[...k]
.map(e => [e.toLowerCase(),e.toUpperCase()])
.map(([l,h]) => chars(e => e==l || e==h))
),
any(
mark('value', true),
seq('(',
capture('value',
rep([], chars(c => c != ')'))
)
,
')')
),
map(({ get, set, add }) => {
let v = get('value');
if (v == 'BAD')
return false;
set('value');
add(k, v);
}),
)
))
)),
spc: (lim=[]) => rep(
lim,
chars(c => c==' '
|| c == '\t'
|| c == '\n'
|| c == '\r'))
})
))();
[
o(`
meow(sussy);
Meower, meo`),o(`wer(2), meower(3),
Unknownus(hmm?),
meower(BAD),
sigma(meo`),
o(`wer)`),
o('')
]
.map(e=>console.log('result:',e))
/*
result: { done: false }
result: { done: false }
result: { done: false }
result: {
done: true,
match: {
meow: [ 'sussy' ],
meower: [ true, '2', '3' ],
unknownFields: [ 'Unknownus(hmm?)', 'meower(BAD)' ],
sigma: [ 'meower' ]
},
leftoverCount: 0,
leftover: ''
}
*/
performance
Perhaps
bugs
i hope not