Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: variadic input helpers #284

Merged
merged 7 commits into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -59,24 +54,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))}+`)
Loading