182 lines
6.6 KiB
Markdown
182 lines
6.6 KiB
Markdown
# regexoid
|
|
|
|
funny regex thingy
|
|
|
|
this library exposes a singular function called `regexoid`:
|
|
|
|
```js
|
|
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 true
|
|
- `chars.any(...pred)`: each `pred` is an array of the form `['A','Z']` (match a range of characters) or `['abcd']` (match any of specified characters)
|
|
- `chars.none(...pred)`: like `chars.any`, but inverted
|
|
- `seq(...elems)`: match every passed regex element one after another
|
|
- `any(...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. execute `predN` and immediately backtrack; if `predN` matched, run `elemN`, otherwise try the next `pred`/`elem` pair
|
|
- `rep([min,max], ...elems)`: repeat the match at least `min` (or 0) times
|
|
and no more than `max` (or Infinity) times. `min`/`max` can 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 dynamically
|
|
- `rep(lim, ...elems)`: eqv to `rep([lim,lim], ...elems)`
|
|
- `rep.strict`: a non-backtracking version of `rep`
|
|
- `rep.label(label)`: add a label to a `rep`, this is targeted by `markBreak`'s
|
|
- `markBreak(label=null)`: leave a `rep` loop 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 aborts
|
|
- `mark(key, value)`: set a named variable to the specified value
|
|
- `captureMulti`, `markMulti`: same as previous two, but append to array instead of setting the variable
|
|
- `output(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)`: execute `elem` and return any input chunks matched by them during execution. `elem` must never backtrack, or an error will be thrown
|
|
- `map(fn)`: call `fn` passing an object with the following helper functions:
|
|
- `get(k)`: get a value from the captures set (`null` if missing). note that lists/"multi captures"
|
|
make `get` return an iterator from the last capture to the first
|
|
- `set(k,v)`: set a value to the captures set; if any existed it will be replaced. null deletes the value
|
|
- `add(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)
|
|
`fn` may return a value. if the value is `false`, regex matching aborts
|
|
- `abort()`: abort regex matching
|
|
- `limit(n, ...elem)`: execute `elem` but 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: `false` if the regex accepts more data, `true` otherwise
|
|
- 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:
|
|
|
|
```js
|
|
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
|