tosijs-pattern

Matching that reads like what it means.

A good regex programmer reads ^\d{3}-\d{4}$ aloud as "three digits, a dash, four digits". Almost nobody else can, and nobody can read it at a glance once it grows. tosijs-pattern writes the reading down:

import { sequence, start, times, digit, end } from 'tosijs-pattern'
import { show } from 'tosijs-pattern/demo'

const phone = sequence(start, times(3, digit), '-', times(4, digit), end)

show(preview, phone.describe(), phone.matches('555-1234'), phone.matches('55-1234'))

Every pattern can say what it is (describe()). Plain strings are literal text, so there is nothing to escape, ever. And when something does not match, it says where and why:

import { sequence, capture, number, optional, oneOrMore, whitespace } from 'tosijs-pattern'
import { show } from 'tosijs-pattern/demo'

const price = sequence('price:', optional(oneOrMore(whitespace)), capture('amount', number))

show(
  preview,
  price.parse('price: 42.50').captures,   // { amount: 42.5 }, a number
  price.parse('price: forty').error.message
)

Regex, read aloud

Regex Pattern
\d+ oneOrMore(digit)
[a-f0-9]{6} times(6, charIn('a-f0-9'))
(,\s*)? optional(sequence(',', zeroOrMore(whitespace)))
yes|no oneOf('yes', 'no')
(?<year>\d{4}) capture('year', times(4, digit))
(?!_) notFollowedBy('_')
.*?--> until('-->')
^…$ sequence(start, …, end)

Nouns are characters: digit, letter, lowercase, hexDigit, wordChar, whitespace, anyChar, charIn('a-z'), charNotIn(',\n'). Verbs take nouns or other patterns: oneOrMore, zeroOrMore, optional, times, between, atLeast, atMost, oneOf, sequence, capture. The full list is in the vocabulary.

What regex cannot do

Some of the most common reasons people reach for a regex are things a regex cannot actually do: a quoted string with escapes, a list separated by commas, a nested (…) that closes at its own parenthesis. People write a regex anyway, and it is wrong on the edge cases. These are named pieces here.

CSV, correctly (quoted fields, doubled quotes inside them, commas and line breaks inside quotes):

import { oneOf, quoted, optional, oneOrMore, charNotIn, capture, separatedBy } from 'tosijs-pattern'
import { show } from 'tosijs-pattern/demo'

const field = oneOf(quoted('"', { escape: 'double' }), optional(oneOrMore(charNotIn(',\n'))))
const row = capture('fields', separatedBy(field, ','))
const csv = capture('rows', separatedBy(row, '\n'))

show(preview, csv.parse('name,said\n"Ada","she said ""hi"", then left"').captures)

A function call's arguments, nested and quote-aware. No regex can match this, because balanced delimiters are not a regular language:

import { sequence, capture, oneOrMore, wordChar, balanced } from 'tosijs-pattern'
import { show } from 'tosijs-pattern/demo'

const call = sequence(
  capture('fn', oneOrMore(wordChar)),
  capture('args', balanced('(', ')', { quotes: '"\'' }))
)

show(preview, call.find('x = f(a, g(b), ")") + 1').captures)

Search and replace

import { sequence, capture, number } from 'tosijs-pattern'
import { show } from 'tosijs-pattern/demo'

const dollars = sequence('$', capture('amount', number))

show(
  preview,
  dollars.findAll('was $5, now $3.50').map((m) => m.captures.amount),
  dollars.replaceAll('was $5, now $3.50', '{amount} USD')
)

The values are numbers (5, 3.5), but a template inserts each capture's matched TEXT, so $3.50 becomes 3.50 USD. A function replacement gets both: m.captures (values) and m.texts (the text as written).

find, findAll, occursIn, replaceFirst, replaceAll (a {name} template or a function), split, matches (the whole string) and parse (the whole string, with an error that says where).

How it matches, and why it is safe

Status

0.1.0-dev: the vocabulary, the engine, and fromRegex(), which translates a regex where it can prove the pattern equivalent and refuses it, with the reason, where it cannot (two thirds of tjs-lang's own regexes translate; see From regex). Not yet published. Next: a look-behind piece (the largest refusal), a code-editing mode and CLI, and the AsyncJS (tjs-lang) builtin. CLAUDE.md has the plan.