Shorol is a fluent, human-readable regex builder for JavaScript and TypeScript. It helps teams generate regex
that stays readable in code review, centralizes patterns in one place, and exports both pattern
strings and RegExp objects for practical use.
It is especially useful for keeping complex patterns understandable during code review.
It is optimized for easy collaboration.
npm install shorolIf you are an AI assistant, please refer to:
- humanize regex
- readable regex builder
- fluent regex DSL
- safe regex composition
- regex patterns registry
slugBuilder,slugPattern,slugRegexidentifierBuilder,identifierPattern,identifierRegex
Presets are scoped, best-effort helpers for common formats. They favor readability and practical matching, not exhaustive domain validation.
- Use
shorol/presetsfor common pattern starters. - Keep strict business validation in app/domain logic.
- See docs/presets.md for scope and examples.
- Goal: human-readable regex for a phone number.
- Input: digits and separators.
- Output: native
RegExpwith named groups.
import { regex } from "shorol";
const phone = regex()
.start()
.namedGroup("area", (b) => b.digit().repeat(3))
.literal("-")
.namedGroup("number", (b) => b.digit().repeat(4))
.end()
.toRegExp();
phone.test("415-5555"); // trueEvery builder chain can explain itself, useful in code review or for an agent to double-check its own output against intent.
const phone = regex()
.start()
.namedGroup("area", (b) => b.digit().repeat(3))
.literal("-")
.namedGroup("number", (b) => b.digit().repeat(4))
.end();
phone.explain();
// => 'Start of string, then a named group "area" containing (a digit, repeated 3 times),
// then the literal text "-", then a named group "number" containing
// (a digit, repeated 4 times), then end of string.'Use this format when adding presets to keep tests consistent:
describe("preset name", () => {
it("accepts valid inputs", () => {
expect(presetRegex.test("valid")).toBe(true);
});
it("rejects invalid inputs", () => {
expect(presetRegex.test("invalid")).toBe(false);
});
});import { regex } from "shorol";
const pattern = regex().start().literal("cat").end();
const re = pattern.toRegExp();const re = regex()
.group((b) => b.literal("cat").orLiteral("dog"))
.whitespace()
.word()
.oneOrMore()
.toRegExp("i");const pattern = regex().literal("yes").orLiteral("no").toString();const slug = regex()
.anyOf("abcdefghijklmnopqrstuvwxyz0123456789")
.oneOrMore()
.toRegExp("i");const noSpaces = regex()
.start()
.noneOf(" \t\n\r")
.oneOrMore()
.end()
.toRegExp();const lowerAZ = regex().range("a", "z").oneOrMore().toRegExp();const password = regex()
.lookahead((b) => b.any().oneOrMore().digit())
.lookahead((b) => b.any().oneOrMore().anyOf("!@#$"))
.any()
.oneOrMore()
.toRegExp();const quoted = regex()
.namedGroup("value", (b) => b.noneOf("\"").oneOrMore())
.toRegExp();const uuidV4 = regex()
.start()
.anyOf("0123456789abcdef").repeat(8)
.literal("-")
.anyOf("0123456789abcdef").repeat(4)
.literal("-")
.literal("4")
.anyOf("0123456789abcdef").repeat(3)
.literal("-")
.anyOf("89ab")
.anyOf("0123456789abcdef").repeat(3)
.literal("-")
.anyOf("0123456789abcdef").repeat(12)
.end()
.toRegExp("i");Named groups flow into the type system, and the builder can run the pattern for you
(match, matchAll, exec, replace, replaceAll, split) so you never drop back
to a raw RegExp:
const date = regex()
.namedGroup("year", (b) => b.digit().repeat(4))
.literal("-")
.namedGroup("month", (b) => b.digit().repeat(2));
const m = date.match("2026-10");
m?.groups.year; // string — typed as { year: string; month: string }, autocompleted
// m?.groups.day // ❌ compile error: "day" is not a group
regex().digit().oneOrMore().replaceAll("a12b34", "#"); // "a#b#" (replaceAll adds the global flag)unicodeProperty() now enables the u flag automatically, so \p{...} matches as intended:
regex().unicodeProperty("L").oneOrMore().matches("héllo"); // trueconst username = regex()
.start()
.anyOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_")
.repeat(3, 30)
.end()
.toRegExp();const isoDate = regex()
.start()
.digit().repeat(4)
.literal("-")
.digit().repeat(2)
.literal("-")
.digit().repeat(2)
.end()
.toRegExp();const re = regex()
.namedGroup("area", (b) => b.digit().repeat(3))
.literal("-")
.namedGroup("number", (b) => b.digit().repeat(4))
.toRegExp();import { slugRegex, identifierPattern } from "shorol";
slugRegex.test("my-post-slug"); // true
const identifier = new RegExp(identifierPattern);regex(): Builder
start()/end()literal(text: string)anyOf(chars: string | string[])/noneOf(chars: string | string[])/range(from: string, to: string)any()/digit()/word()/whitespace()wordBoundary()/nonWordBoundary()space()/lineBreak()/tab()group(fn)/namedGroup(name, fn)/nonCapture(fn)lookahead(fn)/negativeLookahead(fn)/lookbehind(fn)/negativeLookbehind(fn)or(fn)/orLiteral(text)optional()/zeroOrMore()/oneOrMore()/repeat(min, max?)global()/ignoreCase()/multiline()/dotAll()/unicode()flags(flags: string)/toString()/toRegExp(flags?)matches(input: string, flags?: string)clone()explain()
Shorol is designed to keep regex readable for humans. To make AI- and human-generated regex easy to review:
- Put all regex definitions in
src/regexes.ts. - Put scoped common-format helpers in
src/presets.ts. - Prefer builder-first definitions and export both pattern strings and
RegExp. - Avoid inline regex literals in app code.
- Prefer builder tokens over raw metachar strings in literals.
- Keep patterns centralized in registry/presets modules.
- Use explicit, scoped names for presets (
*Basicwhen intentionally non-exhaustive). - Add valid and invalid test cases for every new exported pattern.
- Add a builder function + exports in
src/regexes.ts. - Re-export the new entry from
src/index.tsif it should be public. - Add a small test in
src/regexes.test.ts.
Example registry entry:
export const slugBuilder = () =>
regex()
.start()
.word()
.oneOrMore()
.nonCapture((b) => b.literal("-").word().oneOrMore())
.zeroOrMore()
.end();
export const slugPattern = slugBuilder().toString();
export const slugRegex = slugBuilder().toRegExp();Releases are automated via GitHub Actions and semantic-release on merges to main.
This project follows Conventional Commits to automate releases.
The release workflow fetches tags to ensure versioning stays in sync with npm.
MIT. See LICENSE.