@v0idd0/regexlab 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/bin/regexlab.js +47 -0
- package/package.json +44 -0
- package/src/index.js +240 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vøiddo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# regexlab
|
|
2
|
+
|
|
3
|
+
Explain regex patterns in plain english. Read the manual.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
$ regexlab '\d{3}-\d{4}'
|
|
7
|
+
pattern: \d{3}-\d{4}
|
|
8
|
+
|
|
9
|
+
\d{3} → a digit (0–9) exactly 3 times
|
|
10
|
+
- → the literal character "-"
|
|
11
|
+
\d{4} → a digit (0–9) exactly 4 times
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g rtfm-regexlab
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Explain a bare pattern
|
|
24
|
+
regexlab '\d{3}-\d{4}'
|
|
25
|
+
|
|
26
|
+
# Explain with flags
|
|
27
|
+
regexlab '/foo|bar/gi'
|
|
28
|
+
|
|
29
|
+
# JSON output for piping
|
|
30
|
+
regexlab --json '^[a-z]+$' | jq '.tokens'
|
|
31
|
+
|
|
32
|
+
# Help
|
|
33
|
+
regexlab --help
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## What it handles
|
|
37
|
+
|
|
38
|
+
- Anchors: `^`, `$`, `\b`, `\B`
|
|
39
|
+
- Character classes: `\d`, `\D`, `\w`, `\W`, `\s`, `\S`, `[...]`, `[^...]`
|
|
40
|
+
- Quantifiers: `*`, `+`, `?`, `{n}`, `{n,}`, `{n,m}`
|
|
41
|
+
- Groups: capturing `(...)`, non-capturing `(?:...)`, named `(?<name>...)`
|
|
42
|
+
- Lookarounds: `(?=...)`, `(?!...)`, `(?<=...)`, `(?<!...)`
|
|
43
|
+
- Alternation: `|`
|
|
44
|
+
- Escapes: `\.`, `\\`, `\(`, etc.
|
|
45
|
+
- Backreferences: `\1` through `\9`
|
|
46
|
+
- Flags: `g`, `i`, `m`, `s`, `u`, `y`
|
|
47
|
+
|
|
48
|
+
## Why
|
|
49
|
+
|
|
50
|
+
You wrote a regex six months ago. You don't remember what it does. The grep manual is hostile. So is regex101 if you're offline. This explains it locally.
|
|
51
|
+
|
|
52
|
+
## Programmatic API
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
import { explain, format } from 'rtfm-regexlab';
|
|
56
|
+
|
|
57
|
+
const result = explain('/^\\d{3}-\\d{4}$/g');
|
|
58
|
+
console.log(format(result));
|
|
59
|
+
|
|
60
|
+
// Or get structured output:
|
|
61
|
+
console.log(result.tokens);
|
|
62
|
+
// [{ token: '\\d{3}', explanation: '...' }, ...]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT — part of the [vøiddo](https://voiddo.com) tools collection.
|
package/bin/regexlab.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// regexlab CLI entry point.
|
|
3
|
+
|
|
4
|
+
import { explain, format } from '../src/index.js';
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
|
|
8
|
+
function help() {
|
|
9
|
+
console.log(`
|
|
10
|
+
regexlab — explain regex patterns in plain english.
|
|
11
|
+
|
|
12
|
+
regexlab '<pattern>' explain pattern (wrap in single quotes)
|
|
13
|
+
regexlab '/<pattern>/<flags>' explain with flags
|
|
14
|
+
regexlab --json '<pattern>' output as JSON
|
|
15
|
+
regexlab -h, --help show this
|
|
16
|
+
|
|
17
|
+
examples:
|
|
18
|
+
regexlab '\\\\d{3}-\\\\d{4}'
|
|
19
|
+
regexlab '^[a-zA-Z0-9._-]+@example\\\\.com\$'
|
|
20
|
+
regexlab '/foo|bar/gi'
|
|
21
|
+
`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
|
|
25
|
+
help();
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const wantJson = args.includes('--json');
|
|
30
|
+
const pattern = args.find(a => !a.startsWith('--'));
|
|
31
|
+
|
|
32
|
+
if (!pattern) {
|
|
33
|
+
console.error('error: no pattern given. try `regexlab --help`.');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const result = explain(pattern);
|
|
39
|
+
if (wantJson) {
|
|
40
|
+
console.log(JSON.stringify(result, null, 2));
|
|
41
|
+
} else {
|
|
42
|
+
console.log(format(result));
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error(`regexlab: ${err.message}`);
|
|
46
|
+
process.exit(2);
|
|
47
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@v0idd0/regexlab",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "regexlab \u2014 explain regex patterns in plain English. Token-by-token breakdown: anchors, character classes, quantifiers, groups, lookarounds, flags. CLI + library. Free forever from v\u00f8iddo.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"regexlab": "./bin/regexlab.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node test.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"regex",
|
|
15
|
+
"regular-expressions",
|
|
16
|
+
"explain",
|
|
17
|
+
"cli",
|
|
18
|
+
"developer-tools",
|
|
19
|
+
"voiddo"
|
|
20
|
+
],
|
|
21
|
+
"author": "v\u00f8iddo <support@voiddo.com> (https://voiddo.com)",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"homepage": "https://tools.voiddo.com/regexlab/",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/voidd0/regexlab.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/voidd0/regexlab/issues",
|
|
30
|
+
"email": "support@voiddo.com"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=14"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"bin",
|
|
40
|
+
"src",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
]
|
|
44
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// regexlab — explain regex patterns in plain english.
|
|
2
|
+
//
|
|
3
|
+
// pure parser. no external deps. handles common ECMAScript regex constructs:
|
|
4
|
+
// anchors, character classes, quantifiers, groups, alternation, backreferences,
|
|
5
|
+
// lookarounds, escape sequences. unknown constructs degrade to "unexplained".
|
|
6
|
+
|
|
7
|
+
const NAMED_CHARCLASSES = {
|
|
8
|
+
'\\d': 'a digit (0–9)',
|
|
9
|
+
'\\D': 'a non-digit',
|
|
10
|
+
'\\w': 'a word character (letter, digit, or underscore)',
|
|
11
|
+
'\\W': 'a non-word character',
|
|
12
|
+
'\\s': 'a whitespace character',
|
|
13
|
+
'\\S': 'a non-whitespace character',
|
|
14
|
+
'\\b': 'a word boundary',
|
|
15
|
+
'\\B': 'a non-word boundary',
|
|
16
|
+
'\\n': 'a newline',
|
|
17
|
+
'\\r': 'a carriage return',
|
|
18
|
+
'\\t': 'a tab',
|
|
19
|
+
'\\0': 'a null character',
|
|
20
|
+
'.': 'any character (except newline)',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const ANCHORS = {
|
|
24
|
+
'^': 'the start of the string',
|
|
25
|
+
'$': 'the end of the string',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function parseQuantifier(src, i) {
|
|
29
|
+
if (i >= src.length) return null;
|
|
30
|
+
const ch = src[i];
|
|
31
|
+
if (ch === '*') return { kind: 'q', text: '*', meaning: 'zero or more times', advance: 1 };
|
|
32
|
+
if (ch === '+') return { kind: 'q', text: '+', meaning: 'one or more times', advance: 1 };
|
|
33
|
+
if (ch === '?') return { kind: 'q', text: '?', meaning: 'optional (zero or one time)', advance: 1 };
|
|
34
|
+
if (ch === '{') {
|
|
35
|
+
const end = src.indexOf('}', i);
|
|
36
|
+
if (end === -1) return null;
|
|
37
|
+
const body = src.slice(i + 1, end);
|
|
38
|
+
if (/^\d+$/.test(body)) {
|
|
39
|
+
return { kind: 'q', text: `{${body}}`, meaning: `exactly ${body} times`, advance: end - i + 1 };
|
|
40
|
+
}
|
|
41
|
+
const m = body.match(/^(\d+),(\d*)$/);
|
|
42
|
+
if (m) {
|
|
43
|
+
const lo = m[1], hi = m[2];
|
|
44
|
+
const meaning = hi === ''
|
|
45
|
+
? `at least ${lo} times`
|
|
46
|
+
: `between ${lo} and ${hi} times`;
|
|
47
|
+
return { kind: 'q', text: `{${body}}`, meaning, advance: end - i + 1 };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseCharClass(src, i) {
|
|
54
|
+
if (src[i] !== '[') return null;
|
|
55
|
+
let end = i + 1;
|
|
56
|
+
if (src[end] === '^') end++;
|
|
57
|
+
while (end < src.length && src[end] !== ']') {
|
|
58
|
+
if (src[end] === '\\') end += 2;
|
|
59
|
+
else end++;
|
|
60
|
+
}
|
|
61
|
+
if (end >= src.length) return null;
|
|
62
|
+
const body = src.slice(i + 1, end);
|
|
63
|
+
const negated = body.startsWith('^');
|
|
64
|
+
const inside = negated ? body.slice(1) : body;
|
|
65
|
+
const parts = [];
|
|
66
|
+
let j = 0;
|
|
67
|
+
while (j < inside.length) {
|
|
68
|
+
if (inside[j] === '\\') {
|
|
69
|
+
const esc = inside.slice(j, j + 2);
|
|
70
|
+
parts.push(NAMED_CHARCLASSES[esc] || `the character "${esc}"`);
|
|
71
|
+
j += 2;
|
|
72
|
+
} else if (inside[j + 1] === '-' && j + 2 < inside.length) {
|
|
73
|
+
parts.push(`characters from "${inside[j]}" to "${inside[j + 2]}"`);
|
|
74
|
+
j += 3;
|
|
75
|
+
} else {
|
|
76
|
+
parts.push(`the character "${inside[j]}"`);
|
|
77
|
+
j += 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const meaning = (negated ? 'any character except: ' : 'one of: ') + parts.join(', ');
|
|
81
|
+
return { kind: 'cc', text: src.slice(i, end + 1), meaning, advance: end - i + 1 };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseGroup(src, i) {
|
|
85
|
+
if (src[i] !== '(') return null;
|
|
86
|
+
// Match closing paren with depth.
|
|
87
|
+
let depth = 1;
|
|
88
|
+
let j = i + 1;
|
|
89
|
+
while (j < src.length && depth > 0) {
|
|
90
|
+
if (src[j] === '\\') { j += 2; continue; }
|
|
91
|
+
if (src[j] === '(') depth++;
|
|
92
|
+
else if (src[j] === ')') depth--;
|
|
93
|
+
if (depth === 0) break;
|
|
94
|
+
j++;
|
|
95
|
+
}
|
|
96
|
+
if (depth !== 0) return null;
|
|
97
|
+
const inner = src.slice(i + 1, j);
|
|
98
|
+
let kind = 'capturing group';
|
|
99
|
+
let consumeStart = 0;
|
|
100
|
+
if (inner.startsWith('?:')) { kind = 'non-capturing group'; consumeStart = 2; }
|
|
101
|
+
else if (inner.startsWith('?=')) { kind = 'positive lookahead'; consumeStart = 2; }
|
|
102
|
+
else if (inner.startsWith('?!')) { kind = 'negative lookahead'; consumeStart = 2; }
|
|
103
|
+
else if (inner.startsWith('?<=')) { kind = 'positive lookbehind'; consumeStart = 3; }
|
|
104
|
+
else if (inner.startsWith('?<!')) { kind = 'negative lookbehind'; consumeStart = 3; }
|
|
105
|
+
else if (inner.startsWith('?<')) {
|
|
106
|
+
const close = inner.indexOf('>');
|
|
107
|
+
if (close > 0) {
|
|
108
|
+
const name = inner.slice(2, close);
|
|
109
|
+
kind = `named capturing group "${name}"`;
|
|
110
|
+
consumeStart = close + 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const body = inner.slice(consumeStart);
|
|
114
|
+
return { kind: 'g', subKind: kind, text: src.slice(i, j + 1), inner: body, advance: j - i + 1 };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function explain(pattern) {
|
|
118
|
+
// Strip leading/trailing slash and trailing flags if present (e.g. /foo/i).
|
|
119
|
+
let src = pattern;
|
|
120
|
+
let flags = '';
|
|
121
|
+
const m = src.match(/^\/(.*)\/([gimsuy]*)$/);
|
|
122
|
+
if (m) { src = m[1]; flags = m[2]; }
|
|
123
|
+
|
|
124
|
+
const out = [];
|
|
125
|
+
let i = 0;
|
|
126
|
+
while (i < src.length) {
|
|
127
|
+
const ch = src[i];
|
|
128
|
+
|
|
129
|
+
// Anchors
|
|
130
|
+
if (ANCHORS[ch] && (i === 0 || ch === '$')) {
|
|
131
|
+
out.push({ token: ch, explanation: `match ${ANCHORS[ch]}` });
|
|
132
|
+
i += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Alternation
|
|
137
|
+
if (ch === '|') {
|
|
138
|
+
out.push({ token: '|', explanation: 'OR — match either side of this' });
|
|
139
|
+
i += 1;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Escape
|
|
144
|
+
if (ch === '\\') {
|
|
145
|
+
const esc = src.slice(i, i + 2);
|
|
146
|
+
let meaning = NAMED_CHARCLASSES[esc] || `the literal character "${esc[1]}"`;
|
|
147
|
+
// Backref \1 .. \9
|
|
148
|
+
if (/^\\[1-9]$/.test(esc)) {
|
|
149
|
+
meaning = `back-reference to capturing group ${esc[1]}`;
|
|
150
|
+
}
|
|
151
|
+
const q = parseQuantifier(src, i + 2);
|
|
152
|
+
out.push({ token: esc, explanation: q ? `${meaning} ${q.meaning}` : meaning });
|
|
153
|
+
i += 2 + (q ? q.advance : 0);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Character class
|
|
158
|
+
if (ch === '[') {
|
|
159
|
+
const cc = parseCharClass(src, i);
|
|
160
|
+
if (cc) {
|
|
161
|
+
const q = parseQuantifier(src, i + cc.advance);
|
|
162
|
+
const meaning = q ? `${cc.meaning}, ${q.meaning}` : cc.meaning;
|
|
163
|
+
out.push({ token: cc.text + (q ? q.text : ''), explanation: meaning });
|
|
164
|
+
i += cc.advance + (q ? q.advance : 0);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Group
|
|
170
|
+
if (ch === '(') {
|
|
171
|
+
const g = parseGroup(src, i);
|
|
172
|
+
if (g) {
|
|
173
|
+
const inner = explain(g.inner);
|
|
174
|
+
const q = parseQuantifier(src, i + g.advance);
|
|
175
|
+
const innerSummary = inner.tokens.map(t => t.token).join('');
|
|
176
|
+
let meaning = `${g.subKind} containing: ${innerSummary || '(empty)'}`;
|
|
177
|
+
if (q) meaning += `, ${q.meaning}`;
|
|
178
|
+
out.push({
|
|
179
|
+
token: g.text + (q ? q.text : ''),
|
|
180
|
+
explanation: meaning,
|
|
181
|
+
children: inner.tokens,
|
|
182
|
+
});
|
|
183
|
+
i += g.advance + (q ? q.advance : 0);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Dot
|
|
189
|
+
if (ch === '.') {
|
|
190
|
+
const q = parseQuantifier(src, i + 1);
|
|
191
|
+
const meaning = q ? `${NAMED_CHARCLASSES['.']}, ${q.meaning}` : NAMED_CHARCLASSES['.'];
|
|
192
|
+
out.push({ token: ch + (q ? q.text : ''), explanation: meaning });
|
|
193
|
+
i += 1 + (q ? q.advance : 0);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Literal
|
|
198
|
+
const q = parseQuantifier(src, i + 1);
|
|
199
|
+
const meaning = q
|
|
200
|
+
? `the literal character "${ch}", ${q.meaning}`
|
|
201
|
+
: `the literal character "${ch}"`;
|
|
202
|
+
out.push({ token: ch + (q ? q.text : ''), explanation: meaning });
|
|
203
|
+
i += 1 + (q ? q.advance : 0);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const flagMeanings = {
|
|
207
|
+
g: 'global (match all occurrences, not just the first)',
|
|
208
|
+
i: 'case-insensitive',
|
|
209
|
+
m: 'multiline (^/$ match line boundaries)',
|
|
210
|
+
s: 'dotall (. also matches newlines)',
|
|
211
|
+
u: 'unicode',
|
|
212
|
+
y: 'sticky (match starting from lastIndex)',
|
|
213
|
+
};
|
|
214
|
+
const flagsExplained = flags
|
|
215
|
+
? flags.split('').map(f => `${f} = ${flagMeanings[f] || 'unknown flag'}`)
|
|
216
|
+
: [];
|
|
217
|
+
|
|
218
|
+
return { source: pattern, flags, tokens: out, flagsExplained };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function format(result, indent = 0) {
|
|
222
|
+
const pad = ' '.repeat(indent * 2);
|
|
223
|
+
const lines = [];
|
|
224
|
+
if (indent === 0) {
|
|
225
|
+
lines.push(`pattern: ${result.source}`);
|
|
226
|
+
if (result.flagsExplained.length > 0) {
|
|
227
|
+
lines.push(`flags: ${result.flagsExplained.join('; ')}`);
|
|
228
|
+
}
|
|
229
|
+
lines.push('');
|
|
230
|
+
}
|
|
231
|
+
for (const tok of result.tokens) {
|
|
232
|
+
lines.push(`${pad}${tok.token.padEnd(12)} → ${tok.explanation}`);
|
|
233
|
+
if (tok.children) {
|
|
234
|
+
for (const c of tok.children) {
|
|
235
|
+
lines.push(`${pad} ${c.token.padEnd(10)} ↳ ${c.explanation}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return lines.join('\n');
|
|
240
|
+
}
|