Skip to content

GJZwiers/regexbuilder

Repository files navigation

build Coverage Status

This module provides two fluent builder interfaces to make regex patterns. RegexBuilder is used for piecewise building of a RegExp, while PatternBuilder is used to create extended regexes from string templates defined by the user.

Table of Contents


RegexBuilder

Start building with Regex.new():

import { Regex } from 'https://deno.land/x/regexbuilder/mod.ts';

Regex.new()
    .add('foo')
    .add('bar')
    .build();       
    
    >> /foobar/

Adding regex literals together is also supported:

    .add(/foo/)
    .add(/bar/)
    .build();       
    
    >> /foobar/

Groups

To add groups either call the specific method or use the more general group method where you provide the content and the group type:

    .capture('foo');    >> /(foo)/

    .noncapture('bar');    >> /(?:bar)/

    .group('bar', 'ncg')    >> /(?:bar)/

Named groups can be added with namedCapture:

    .namedCapture('foo', 'bar');    >> /(?<foo>bar)/

Nesting

A nested structure in the pattern can be started by calling nest for a capturing group or specific calls to nest different groups. Call unnest to finish a nested tier or provide it with an integer to finish multiple tiers at once:

    Regex.new()
        .nest()
        .add('foo')
        .nestNonCapture()
        .add('bar')
        .unnest()   // or call .unnest(2)
        .unnest()
        .build()

        >> /(foo(?:bar))/

This can be shortened by using composite calls such as nestAdd to combine nest and add in one call. When no group type is passed it will default to a capturing group, in other cases you need to provide the group type as the second argument. To nest a named group, call nestNamed.

    Regex.new()
        .nestAdd('foo')
        .nestAdd('bar', 'ncg')
        .unnest(2)
        .build()

        >> /(foo(?:bar))/

Assertions

    .lineStart()
    .add('foo')
    .lineEnd()  
    
    >> /^foo$/

    .startsWith('foo')  >> /^foo/

    .endsWith('bar')    >> /bar$/

    .add('foo')
    .lookahead('bar')
    // or
    .followedBy('bar') >> /foo(?=bar)/

    .boundary()  >> /foo\b/
    .negatedBoundary() >> /foo\B/

Alternation

    .alts(['foo','bar','baz']);
    >> /foo|bar|baz/

    .altGroup(['foo', 'bar', 'baz'], 'ncg')
    >> /(?:foo|bar|baz)/

    .joinGroup(['foo','bar','baz'], '.', 'la');
    >> /(?=foo.bar.baz)/
    .joinGroup(['foo','bar','baz'], '.');   // Same as Array.join
    >> /foo.bar.baz/

Ranges

    .range('abc');
    >> /[abc]/

    .negatedRange('abc');
    >> /[^abc]/

Flags

    .add('foo')
    .flags('g')
    >> /foo/g

    .global() >> /foo/g

    .caseInsensitive() >> /foo/i

    .multiline() >> /foo/m

    .unicode() >> /foo/u

    .sticky() >> /foo/y

    .indices() >> /foo/d

Character Classes

    .digit()        >> /\d/
    
    .word()         >> /\w/

    .whitespace()   >> /\s/

    .nonDigit()    >> /\D/
    
    .any()          >> /./

    .digits(2)      >> /\d{2}/

    .digits(2,3)    >> /\d{2,3}/

    .digits(2,'*')  >> /\d{2,}/

    .linefeed()     >> /\n/
    .lf()           >> /\n/

    .carriageReturn() >> /\r/
    .cr()             >> /\r/

    .ctrlChar('A')  >> /\cA/

    .hex('AA')      >> /\xAA/

    .utf16('AAAA')  >> /\uAAAA/

    .unicodeChar('AAAA')
    .flags('u')     >> /\u{AAAA}/u

Quantifiers

    .add('foo')
    .times(2)
    >> /foo{2}/ // matches fo with 2 more o's

    .between(2, 5)
    >> /foo{2,5}/   // matches fo with 2 to 5 more o's

    .atleast(2)
    >> /foo{2,}/    // matches fo with 2 or more o's

    .zeroPlus()
    >> /foo*/   // matches fo with 0 or more o's

    .onePlus()
    >> /foo+/   // matches fo with 1 or more o's

    .oneZero()
    >> /foo?/   // matches fo with 0 or 1 more o

    .zeroPlus()
    .lazy()
    >> /foo*?/   // matches fo with 0 or more o's lazily (stops at first possible match)

Back References

    .capture('foo')
    .add('[: ]+')
    .ref(1)

    >> /(foo)[: ]+\1/

    .namedCapture('foo', 'bar')
    .add('[: ]+')
    .ref('foo')

    >> /(?<foo>bar)[: ]+\k<foo>/

PatternBuilder

is a methodology for building regexes according to templates and can be used to manage the complexity of handling lengthy patterns.

Start building with Pattern.new:

import { Pattern } from 'https://deno.land/x/regexbuilder/mod.ts';

let pattern = Pattern.new()
    .settings({
        template: '(greetings) (?=regions)',
        flags: 'i'
    })
    .vars({
        greetings: ['hello','good morning','howdy'],
        regions: ['world','new york','{{foo}}']
    })
    .placeholders({ foo: 'bar' })
    .build();

    >> /(hello|good morning|howdy) (?=world|new york|bar)/i

Templates

Templates are useful to separate concerns between a pattern's structure and values. You can name any part of a template string. Any word in it will be substituted with the values of the corresponding key in the data. Arrays in the data will be joined to a sequence of alternates (or a custom sequence if you define a custom separator):

    .settings({
        template: '(?:expiration_statement)[: ]+(day-month-year)'
    })
    .vars({
        expiration_statement: ['best-by','use-by','consume before','expiration date','expiry date'],
        day: '[0-3][0-9]',
        month: ['jan', 'feb', 'mar', /* ... */ 'dec'],
        year: String.raw`(?:19|20)\d{2}\b`  // Note that you will need double backslashes in a normal string 
    })

When the data you plan to match has different structures you can define multiple templates and use buildAll() instead of build(). This will return a list of patterns. For example, if you are matching both American and European date formats:

    .settings({
        template: [
            '(day)-(month)-(year)',
            '(month)-(day)-(year)'
        ]
    })
    .vars({
        day: '[0-3][0-9]',
        month: ['jan', 'feb', 'mar', /* ... */ 'dec'],
        year: String.raw`(?:19|20)\d{2}\b`
    })
    .buildAll();

Choosing a template variable symbol is possible if you want to add more clarity on which parts of a template are variables and which aren't.

When you only want to define a template and don't need to use any settings other, call template():

    .template('(foo)(?!bar)')

Placeholders

Declare a set of placeholder substitutes to reuse them in multiple patterns. Adding placeholders to the data can be done with double curly braces and a name: {{placeholder}}.

The example below shows how to reuse some of the data from the two previous patterns by using placeholders for day, month and year:

const placeholders = {
        day: '[0-3][0-9]',
        month: [ 
        'jan', 'feb', 'mar', 'apr', 'may', 'jun',
        'jul', 'aug', 'sep', 'oct', 'nov', 'dec'
        ],
        year: String.raw`(?:19|20)\d{2}\b`
    };
    
const expirationDate = Pattern.new()
    .settings({
        template: '(?:expiration_statement)[: ]+(day)-(month)-(year)'
    })
    .vars({
        expiration_statement: [
            'best-by','use-by','consume before','expiration date','expiry date'
        ],
        day: '{{day}}',
        month: '{{month}}',
        year: '{{year}}'
    })
    .placeholders(placeholders)
    .build();

    >> /(?:best-by|use-by|consume before|expiration date|expiry date)[: ]+([0-3][0-9])-(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)-((?:19|20)\d{2}\b)/

const calendarDate = Pattern.new()
    .settings({
        template: [
            '(day)-(month)-(year)',
            '(month)-(day)-(year)'
        ]
    })
    .placeholders(placeholders)
    .vars({
        day: '{{day}}',
        month: '{{month}}',
        year: '{{year}}'
    })
    .buildAll();

    >> [
        /([0-3][0-9])-(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)-((?:19|20)\d{2}\b)/,
        /(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)-([0-3][0-9])-((?:19|20)\d{2}\b)/
    ]

Match Maps (Experimental)

Arrays of matches can be mapped to their pattern's template with the matchMap method:

    .settings({ template: '(greeting) (region)' })
    .vars({ greeting: 'hello', region: 'world' })
    .build()

    >> /(hello) (world)/

pattern.matchMap('hello world')

>> { full_match: 'hello world', greeting: 'hello', region: 'world' }

Exceptions (Experimental)

Separate desired and unwanted values with the filter method. Note that this will restructure your template as exclude|({the-rest-of-the-template}) and place any desired full match in capture group 1 while adding unwanted values to group 0 only.

.settings({ template: 'years'})
.vars({ years: String.raw`20\d{2}` })
.filter('2000')

>> /2000|(20\d{2})/

When the pattern above matches 2000 the resulting array of matches will not have an index 1, but it will if anything else like 2001 or 2020 is matched.

Below is another example of filtering that allows matching two digits that represent the days of the month very precisely. The pattern will match any from 01-jan-2020 to 31-jan-2020 with all its capturing groups but 00 or 32-39 are excepted and have no capturing groups, allowing you to check if the pattern matched a valid date or not.

    .settings({
        template: '(day)-(month)-(year)'
    })
    .vars({
        day: '[0-3][0-9]',
        month: ['jan', 'feb', 'mar', /* ... */ 'dec'],
        year: String.raw`(?:19|20)\d{2}\b`
    })
    .filter(['00','3[2-9]'])
    .build();

    >> /00|3[2-9](([0-3][0-9])-(jan|feb|mar|dec)-((?:19|20)\d{2}\b))/

    pattern.match('01-jan-2020')
    // matches: ['01-jan-2020','01-jan-2020','01','jan','2020'], with
    // an index for every capturing group

    pattern.match('32-jan-2020')
    // matches: ['32'], no index 1 or higher

Wildcard Pattern (Experimental)

Add a wildcard to be searched for after a set of known values. Note that this will restructure your template as {the-rest-of-the-template}|(wildcard), adding a capture group but not changing the order of existing ones.

.settings({ template: 'years' })
.vars({ years: ['2018','2019','2020'] })
.wildcard(String.raw`20\d{2}\b`)

>> /2018|2019|2020|(20\d{2}\b)/

Note that with the pattern above any wildcard match will be placed in group 1.

Custom Variable Symbol

If you'd like to use a more explicit notation for the template variables, you can choose from a few symbols by adding a symbol setting when building a Pattern:

.settings({ template: '#foo', symbol: '#' })   // '#' | '%' | '@' | '!'
.vars({ foo: 'bar'})

>> /bar/

If you also need to use a variable name as literal characters escape it with a backslash (keep in mind to use a double backslash in a normal string or String.raw):

.settings({ template: String.raw`\#foo (?=#foo)`, symbol: '#' })
.vars({ foo: 'baz' })

>> /#foo (?=bar)/

Custom Separator Symbol

If you'd like to join the arrays defined in vars with something other than | add the separator setting:

.settings({ template: 'foo (?=bar)', separator: String.raw`\s+`})
.vars({ foo: ['one', 'two', 'three']})
.build()

>> /one\s+two\s+three (?=bar)/