Skip to content

docs(router): note that constraint regexes use single backslashes in CFML - #4529

Merged
bpamiri merged 1 commit into
developfrom
peter/router-constraint-regex-note
Oct 9, 2026
Merged

bpamiri merged 1 commit into
developfrom
peter/router-constraint-regex-note

Conversation

@bpamiri

@bpamiri bpamiri commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

What

Adds a short caution to the Constrain pattern placeholders section of the v4-2-0 routing guide: a route constraints value is a plain CFML string, and CFML strings have no backslash escapes, so a digit class is written "\d{4}", not "\\d{4}".

A doubled backslash reaches the regex engine as a literal backslash (\\d = "a backslash, then d"), so "\\d{4}" never matches and the route silently falls through to the next one. The same applies to \w, \s, \. and any other escape.

Why it's correct

The router inserts a constraint value into the route's regex verbatim — Mapper.$patternToRegex() replaces each placeholder with (<constraint>) (inner capturing groups rewritten to non-capturing), so the constraint string is the regex. The framework's own route specs confirm the single-backslash form works: they declare constraints = {year = "\d{4}", month = "\d{1,2}"} and compile to a ^archive\/(\d{4})\/(\d{2})\/?$ regex that matches archive/2024/05.

The example in that section already uses the correct form (year="\d{4}"); this just makes the rule explicit so readers don't reach for "\\d" out of habit from escape-processing languages.

Scope

  • The consumer-ai routing quick reference (docs/consumer-ai/.ai/routing.md) does not document constraints, so it's left unchanged; its template copy stays identical (ship-consumer-docs.sh check passes).

Checks

  • pnpm --filter @wheels-dev/site-guides build — green (854 pages).
  • pnpm verify:docs on the page — 8 passed, 0 failed.
  • tools/build/scripts/ship-consumer-docs.sh check — OK.

🤖 Generated with Claude Code

…CFML

A route `constraints` value is a plain CFML string that the router drops
into the route regex verbatim (`$patternToRegex` wraps each in a group).
CFML strings have no backslash escapes, so a digit class is written
`"\d{4}"`, not `"\\d{4}"` — a doubled backslash reaches the regex as a
literal backslash and the pattern never matches, so the route silently
falls through. Add a short caution to the "Constrain pattern
placeholders" section (the example there already uses the correct form).

Verified against the router source and the framework's own passing
route specs, which declare `constraints = {year = "\d{4}", ...}` and
compile to a `\d{4}` route regex that matches a numeric path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri

bpamiri commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

rev1-r3 review @ 4660584: APPROVE

The new caution is accurate.

Against source:

  • Mapper.$patternToRegex() (vendor/wheels/Mapper.cfc:92) puts each constraint into the route regex with ReReplaceNoCase(rv, ":::<key>:::", "(" & $nonCapturingConstraint(constraint) & ")", "all"). The constraint's characters are not escaped or rewritten.
  • The only change is that capturing groups become non-capturing ($nonCapturingConstraint), and the whole constraint is wrapped in one group.
  • whereMatch() goes through the same function (mapper/matching.cfc:671).
  • CFML string literals have no backslash escapes, so "\d{4}" is the regex \d{4}.

Behaviour, probed directly: the constraint is passed as the replacement argument of ReReplaceNoCase, where some engines give backslashes a meaning, so I checked that it survives unchanged. I ran ReReplaceNoCase("^:::year:::$", ":::year:::", "(<constraint>)", "all") and matched the result against 2026:

Engine "\d{4}" → regex / matches 2026 "\\d{4}" → regex / matches 2026
Lucee 7 (LuCLI 0.6.2.3) ^(\d{4})$ / yes ^(\\d{4})$ / no (matches the literal \dddd)
BoxLang (ortussolutions/boxlang:cli) ^(\d{4})$ / yes ^(\\d{4})$ / no

I didn't get a standalone Adobe probe running. The single-backslash form is covered on Adobe by the core suite, which the matrix runs on Adobe 2023 and 2025: mapperModernSpec "whereMatch applies a custom regex" uses "20\d{2}" and asserts archive/2024 matches and archive/1999 doesn't.

Wording nit, not blocking: "drops it into the route's regex verbatim" is true of the backslashes, which is the point of the caution. Strictly, the constraint is wrapped in a group and its capturing groups are made non-capturing. Fine to leave as is.

PR CI: 20 pass, 4 skipping, 0 fail (including Deploy guides, so the MDX and the Aside import build). MERGEABLE.

@bpamiri
bpamiri merged commit a8443c3 into develop Oct 9, 2026
24 checks passed
@bpamiri
bpamiri deleted the peter/router-constraint-regex-note branch October 9, 2026 14:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant