Skip to content

Commit 00dab95

Browse files
authored
feat: variadic input helpers (#284)
1 parent d080898 commit 00dab95

File tree

9 files changed

+304
-132
lines changed

9 files changed

+304
-132
lines changed

docs/content/2.getting-started/2.usage.md

+22-8
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ console.log(regExp)
1515

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

18-
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.
18+
`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.
1919

2020
```js
2121
import { createRegExp, global, multiline, exactly } from 'magic-regexp'
@@ -25,6 +25,16 @@ createRegExp(exactly('foo').or('bar'))
2525
createRegExp('string-to-match', [global, multiline])
2626
// you can also pass flags directly as strings or Sets
2727
createRegExp('string-to-match', ['g', 'm'])
28+
29+
// or pass in multiple `string` and `input patterns`,
30+
// all inputs will be concatenated to one RegExp pattern
31+
createRegExp(
32+
'foo',
33+
maybe('bar').groupedAs('g1'),
34+
'baz',
35+
[global, multiline]
36+
)
37+
// equivalent to /foo(?<g1>(?:bar)?)baz/gm
2838
```
2939

3040
::alert
@@ -38,22 +48,26 @@ There are a range of helpers that can be used to activate pattern matching, and
3848
| | |
3949
| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
4050
| `charIn`, `charNotIn` | this matches or doesn't match any character in the string provided. |
41-
| `anyOf` | this takes an array of inputs and matches any of them. |
51+
| `anyOf` | this takes a variable number of inputs and matches any of them. |
4252
| `char`, `word`, `wordChar`, `wordBoundary`, `digit`, `whitespace`, `letter`, `letter.lowercase`, `letter.uppercase`, `tab`, `linefeed` and `carriageReturn` | these are helpers for specific RegExp characters. |
4353
| `not` | this can prefix `word`, `wordChar`, `wordBoundary`, `digit`, `whitespace`, `letter`, `letter.lowercase`, `letter.uppercase`, `tab`, `linefeed` or `carriageReturn`. For example `createRegExp(not.letter)`. |
44-
| `maybe` | equivalent to `?` - this marks the input as optional. |
45-
| `oneOrMore` | Equivalent to `+` - this marks the input as repeatable, any number of times but at least once. |
46-
| `exactly` | This escapes a string input to match it exactly. |
54+
| `maybe` | equivalent to `?` - this takes a variable number of inputs and marks them as optional. |
55+
| `oneOrMore` | Equivalent to `+` - this takes a variable number of inputs and marks them as repeatable, any number of times but at least once. |
56+
| `exactly` | This takes a variable number of inputs and concatenate their patterns, and escapes string inputs to match it exactly. |
57+
58+
::alert
59+
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'))`.
60+
::
4761

4862
## Chaining inputs
4963

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

5266
| | |
5367
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54-
| `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. |
55-
| `or` | this provides an alternative to the current input. |
56-
| `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). |
68+
| `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. |
69+
| `or` | this takes a variable number of inputs and provides as an alternative to the current input. |
70+
| `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). |
5771
| `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_. |
5872
| `optionally` | this is a function you can call to mark the current input as optional. |
5973
| `as` | alias for `groupedAs` |

docs/content/2.getting-started/3.examples.md

+5-6
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ title: Examples
55
### Quick-and-dirty semver
66

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

1010
createRegExp(
11-
oneOrMore(digit)
12-
.groupedAs('major')
13-
.and('.')
14-
.and(oneOrMore(digit).groupedAs('minor'))
15-
.and(exactly('.').and(oneOrMore(char).groupedAs('patch')).optionally())
11+
oneOrMore(digit).groupedAs('major'),
12+
'.',
13+
oneOrMore(digit).groupedAs('minor'),
14+
maybe('.', oneOrMore(char).groupedAs('patch'))
1615
)
1716
// /(?<major>\d+)\.(?<minor>\d+)(?:\.(?<patch>.+))?/
1817
```

src/core/inputs.ts

+54-35
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
import { createInput, Input } from './internal'
2-
import type { GetValue, EscapeChar } from './types/escape'
2+
import type { EscapeChar } from './types/escape'
33
import type { Join } from './types/join'
4-
import type {
5-
MapToGroups,
6-
MapToValues,
7-
InputSource,
8-
GetGroup,
9-
MapToCapturedGroupsArr,
10-
GetCapturedGroupsArr,
11-
} from './types/sources'
4+
import type { MapToGroups, MapToValues, InputSource, MapToCapturedGroupsArr } from './types/sources'
125
import { IfUnwrapped, wrap } from './wrap'
136

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

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

3429
export const char = createInput('.')
3530
export const word = createInput('\\b\\w+\\b')
@@ -60,24 +55,48 @@ export const not = {
6055
carriageReturn: createInput('[^\\r]'),
6156
}
6257

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

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

77-
/** Equivalent to `+` - this marks the input as repeatable, any number of times but at least once */
78-
export const oneOrMore = <New extends InputSource>(str: New) =>
79-
createInput(`${wrap(exactly(str))}+`) as Input<
80-
IfUnwrapped<GetValue<New>, `(?:${GetValue<New>})+`, `${GetValue<New>}+`>,
81-
GetGroup<New>,
82-
GetCapturedGroupsArr<New>
83-
>
88+
/** Equivalent to `+` - this takes a variable number of inputs and marks them as repeatable, any number of times but at least once
89+
* @example
90+
* oneOrMore('foo', maybe('bar')) // => /(?:foo(?:bar)?)+/
91+
* @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped
92+
*/
93+
export const oneOrMore = <
94+
Inputs extends InputSource[],
95+
Value extends string = Join<MapToValues<Inputs>, '', ''>
96+
>(
97+
...inputs: Inputs
98+
): Input<
99+
IfUnwrapped<Value, `(?:${Value})+`, `${Value}+`>,
100+
MapToGroups<Inputs>,
101+
MapToCapturedGroupsArr<Inputs>
102+
> => createInput(`${wrap(exactly(...inputs))}+`)

0 commit comments

Comments
 (0)