@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 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.
@@ -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
+ }