regexoid/README.md

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 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:

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