Skip to content

Commit

Permalink
feat: variadic input helpers (#284)
Browse files Browse the repository at this point in the history
  • Loading branch information
didavid61202 committed Apr 13, 2023
1 parent d080898 commit 00dab95
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 132 deletions.
30 changes: 22 additions & 8 deletions docs/content/2.getting-started/2.usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ console.log(regExp)

Every pattern you create with the library should be wrapped in `createRegExp`, which enables the build-time transform.

The first argument is either a string to match exactly, or an input pattern built up using helpers from `magic-regexp`. It also takes a second argument, which is an array of flags or flags string.
`createRegExp` accepts an arbitrary number of arguments of type `string` or `Input` (built up using helpers from `magic-regexp`), and an optional final argument of an array of flags or a flags string. It creates a `MagicRegExp`, which concatenates all the patterns from the arguments that were passed in.

```js
import { createRegExp, global, multiline, exactly } from 'magic-regexp'
Expand All @@ -25,6 +25,16 @@ createRegExp(exactly('foo').or('bar'))
createRegExp('string-to-match', [global, multiline])
// you can also pass flags directly as strings or Sets
createRegExp('string-to-match', ['g', 'm'])

// or pass in multiple `string` and `input patterns`,
// all inputs will be concatenated to one RegExp pattern
createRegExp(
'foo',
maybe('bar').groupedAs('g1'),
'baz',
[global, multiline]
)
// equivalent to /foo(?<g1>(?:bar)?)baz/gm
```

::alert
Expand All @@ -38,22 +48,26 @@ There are a range of helpers that can be used to activate pattern matching, and
| | |
| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `charIn`, `charNotIn` | this matches or doesn't match any character in the string provided. |
| `anyOf` | this takes an array of inputs and matches any of them. |
| `anyOf` | this takes a variable number of inputs and matches any of them. |
| `char`, `word`, `wordChar`, `wordBoundary`, `digit`, `whitespace`, `letter`, `letter.lowercase`, `letter.uppercase`, `tab`, `linefeed` and `carriageReturn` | these are helpers for specific RegExp characters. |
| `not` | this can prefix `word`, `wordChar`, `wordBoundary`, `digit`, `whitespace`, `letter`, `letter.lowercase`, `letter.uppercase`, `tab`, `linefeed` or `carriageReturn`. For example `createRegExp(not.letter)`. |
| `maybe` | equivalent to `?` - this marks the input as optional. |
| `oneOrMore` | Equivalent to `+` - this marks the input as repeatable, any number of times but at least once. |
| `exactly` | This escapes a string input to match it exactly. |
| `maybe` | equivalent to `?` - this takes a variable number of inputs and marks them as optional. |
| `oneOrMore` | Equivalent to `+` - this takes a variable number of inputs and marks them as repeatable, any number of times but at least once. |
| `exactly` | This takes a variable number of inputs and concatenate their patterns, and escapes string inputs to match it exactly. |

::alert
All helpers that takes `string` and `Input` are variadic functions, so you can pass in one or multiple arguments of `string` or `Input` to them and they will be concatenated to one pattern. for example,s `exactly('foo', maybe('bar'))` is equivalent to `exactly('foo').and(maybe('bar'))`.
::

## Chaining inputs

All of the helpers above return an object of type `Input` that can be chained with the following helpers:

| | |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `and` | this adds a new pattern to the current input, or you can use `and.referenceTo(groupName)` to adds a new pattern referencing to a named group. |
| `or` | this provides an alternative to the current input. |
| `after`, `before`, `notAfter` and `notBefore` | these activate positive/negative lookahead/lookbehinds. Make sure to check [browser support](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#browser_compatibility) as not all browsers support lookbehinds (notably Safari). |
| `and` | this takes a variable number of inputs and adds them as new pattern to the current input, or you can use `and.referenceTo(groupName)` to adds a new pattern referencing to a named group. |
| `or` | this takes a variable number of inputs and provides as an alternative to the current input. |
| `after`, `before`, `notAfter` and `notBefore` | these takes a variable number of inputs and activate positive/negative lookahead/lookbehinds. Make sure to check [browser support](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#browser_compatibility) as not all browsers support lookbehinds (notably Safari). |
| `times` | this is a function you can call directly to repeat the previous pattern an exact number of times, or you can use `times.between(min, max)` to specify a range, `times.atLeast(x)` to indicate it must repeat at least x times, `times.atMost(x)` to indicate it must repeat at most x times or `times.any()` to indicate it can repeat any number of times, _including none_. |
| `optionally` | this is a function you can call to mark the current input as optional. |
| `as` | alias for `groupedAs` |
Expand Down
11 changes: 5 additions & 6 deletions docs/content/2.getting-started/3.examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ title: Examples
### Quick-and-dirty semver

```js
import { createRegExp, exactly, oneOrMore, digit, char } from 'magic-regexp'
import { createRegExp, exactly, maybe, oneOrMore, digit, char } from 'magic-regexp'

createRegExp(
oneOrMore(digit)
.groupedAs('major')
.and('.')
.and(oneOrMore(digit).groupedAs('minor'))
.and(exactly('.').and(oneOrMore(char).groupedAs('patch')).optionally())
oneOrMore(digit).groupedAs('major'),
'.',
oneOrMore(digit).groupedAs('minor'),
maybe('.', oneOrMore(char).groupedAs('patch'))
)
// /(?<major>\d+)\.(?<minor>\d+)(?:\.(?<patch>.+))?/
```
Expand Down
89 changes: 54 additions & 35 deletions src/core/inputs.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import { createInput, Input } from './internal'
import type { GetValue, EscapeChar } from './types/escape'
import type { EscapeChar } from './types/escape'
import type { Join } from './types/join'
import type {
MapToGroups,
MapToValues,
InputSource,
GetGroup,
MapToCapturedGroupsArr,
GetCapturedGroupsArr,
} from './types/sources'
import type { MapToGroups, MapToValues, InputSource, MapToCapturedGroupsArr } from './types/sources'
import { IfUnwrapped, wrap } from './wrap'

export type { Input }
Expand All @@ -23,13 +16,15 @@ export const charIn = <T extends string>(chars: T) =>
export const charNotIn = <T extends string>(chars: T) =>
createInput(`[^${chars.replace(/[-\\^\]]/g, '\\$&')}]`) as Input<`[^${EscapeChar<T>}]`>

/** This takes an array of inputs and matches any of them */
export const anyOf = <New extends InputSource[]>(...args: New) =>
createInput(`(?:${args.map(a => exactly(a)).join('|')})`) as Input<
`(?:${Join<MapToValues<New>>})`,
MapToGroups<New>,
MapToCapturedGroupsArr<New>
>
/** This takes a variable number of inputs and matches any of them
* @example
* anyOf('foo', maybe('bar'), 'baz') // => /(?:foo|(?:bar)?|baz)/
* @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped
*/
export const anyOf = <Inputs extends InputSource[]>(
...inputs: Inputs
): Input<`(?:${Join<MapToValues<Inputs>>})`, MapToGroups<Inputs>, MapToCapturedGroupsArr<Inputs>> =>
createInput(`(?:${inputs.map(a => exactly(a)).join('|')})`)

export const char = createInput('.')
export const word = createInput('\\b\\w+\\b')
Expand Down Expand Up @@ -60,24 +55,48 @@ export const not = {
carriageReturn: createInput('[^\\r]'),
}

/** Equivalent to `?` - this marks the input as optional */
export const maybe = <New extends InputSource>(str: New) =>
createInput(`${wrap(exactly(str))}?`) as Input<
IfUnwrapped<GetValue<New>, `(?:${GetValue<New>})?`, `${GetValue<New>}?`>,
GetGroup<New>,
GetCapturedGroupsArr<New>
>
/** Equivalent to `?` - takes a variable number of inputs and marks them as optional
* @example
* maybe('foo', excatly('ba?r')) // => /(?:fooba\?r)?/
* @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped
*/
export const maybe = <
Inputs extends InputSource[],
Value extends string = Join<MapToValues<Inputs>, '', ''>
>(
...inputs: Inputs
): Input<
IfUnwrapped<Value, `(?:${Value})?`, `${Value}?`>,
MapToGroups<Inputs>,
MapToCapturedGroupsArr<Inputs>
> => createInput(`${wrap(exactly(...inputs))}?`)

/** This escapes a string input to match it exactly */
export const exactly = <New extends InputSource>(
input: New
): Input<GetValue<New>, GetGroup<New>, GetCapturedGroupsArr<New>> =>
typeof input === 'string' ? (createInput(input.replace(ESCAPE_REPLACE_RE, '\\$&')) as any) : input
/** This takes a variable number of inputs and concatenate their patterns, and escapes string inputs to match it exactly
* @example
* exactly('fo?o', maybe('bar')) // => /fo\?o(?:bar)?/
* @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped
*/
export const exactly = <Inputs extends InputSource[]>(
...inputs: Inputs
): Input<Join<MapToValues<Inputs>, '', ''>, MapToGroups<Inputs>, MapToCapturedGroupsArr<Inputs>> =>
createInput(
inputs
.map(input => (typeof input === 'string' ? input.replace(ESCAPE_REPLACE_RE, '\\$&') : input))
.join('')
)

/** Equivalent to `+` - this marks the input as repeatable, any number of times but at least once */
export const oneOrMore = <New extends InputSource>(str: New) =>
createInput(`${wrap(exactly(str))}+`) as Input<
IfUnwrapped<GetValue<New>, `(?:${GetValue<New>})+`, `${GetValue<New>}+`>,
GetGroup<New>,
GetCapturedGroupsArr<New>
>
/** Equivalent to `+` - this takes a variable number of inputs and marks them as repeatable, any number of times but at least once
* @example
* oneOrMore('foo', maybe('bar')) // => /(?:foo(?:bar)?)+/
* @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped
*/
export const oneOrMore = <
Inputs extends InputSource[],
Value extends string = Join<MapToValues<Inputs>, '', ''>
>(
...inputs: Inputs
): Input<
IfUnwrapped<Value, `(?:${Value})+`, `${Value}+`>,
MapToGroups<Inputs>,
MapToCapturedGroupsArr<Inputs>
> => createInput(`${wrap(exactly(...inputs))}+`)

1 comment on commit 00dab95

@vercel
Copy link

@vercel vercel bot commented on 00dab95 Apr 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.