Skip to content

fix(lstsv): emit regex() as regexMatch('/pat/', x) - #245

Merged
jstet merged 1 commit into
mainfrom
fix/regexmatch-218
Nov 4, 2026
Merged

jstet merged 1 commit into
mainfrom
fix/regexmatch-218

Conversation

@jstet

@jstet jstet commented Nov 4, 2026

Copy link
Copy Markdown
Member

Fixes #218.

LimeSurvey's `regexMatch(pattern, input)` takes the pattern first and needs it delimited (`/…/`). We emitted `regexMatch(self, 'pat')`, so the constraint was wrong for every input. Checked against the LimeSurvey source: PHP `exprmgr_regexMatch` runs `preg_match($pattern . 'u', $input)`, and JS `LEMregexMatch` strips `/…/flags` before `RegExp.test`.

  • Order and delimiters: `regex(x, 'pat')` becomes `regexMatch('/pat/', x)`. A `/` in the pattern is escaped as `\/` (not already-escaped ones). The quote style of the XPath literal is kept; quotes need nothing else because XPath has no escapes.
  • Anchoring: ODK `regex()` matches the whole value, while both LimeSurvey implementations search anywhere in it. So an unanchored pattern is wrapped as `^(?:pat)$`; a pattern that already starts with `^` and ends with an unescaped `$` (and has no `|`) is left alone. Without this, `regex(., '[0-9]{5}')` would accept `abc12345xyz`. A computed (non-literal) pattern is delimited and anchored at run time by string concatenation.
  • Escape hatch: an author-written `regexMatch("pat", .)` in a constraint gets delimiters (and `/` escaping) but no anchoring, as it is already LimeSurvey syntax; an already delimited pattern is kept.
  • Reverse (lstsv2xlsform): `regexMatch('/pat/', x)` reads back to `regex(x, 'pat')`, undoing the delimiters, the `/` escaping and the `^(?:…)$` wrapper. The old field-first `regexMatch(x, 'pat')` is still accepted. LimeSurvey-native unanchored patterns come back unchanged (so ODK will match the whole value), and flags such as `/pat/i` have no ODK form and are kept as text. The README's former known loss 7 (argument order) is replaced accordingly.
  • `convention:logicMapping` documents the mapping; `src/generated/` is regenerated.

Before:
```
regex(., '^[0-9]{5}$') -> regexMatch(self, '^[0-9]{5}$')
regex(., '[0-9]{5}') -> regexMatch(self, '[0-9]{5}')
```

After:
```
regex(., '^[0-9]{5}$') -> regexMatch('/^[0-9]{5}$/', self)
regex(., '[0-9]{5}') -> regexMatch('/^(?:[0-9]{5})$/', self)
```

The `testB` snapshot changed only in its two email-regex constraint rows (blessed and diff inspected). Not run against a live LimeSurvey here; the issue's live valid/invalid test is still worth doing.

Tests

  • Forward: delimiters, anchoring, alternation grouping, `/` escaping, quote styles, escaped `$`, computed pattern, escape-hatch delimiting.
  • Reverse: delimited, anchored, escaped, native-unanchored, flags, and the old field-first form; forward-then-reverse restores the original XPath.
  • `npm test`: 108 files and 1794 tests pass (1773 before). `npm run validate` (lint, types, format, knip, dup, arch, markdownlint) is clean.

🤖 Generated with Claude Code

… delimited

LimeSurvey's regexMatch(pattern, input) needs a delimited pattern (PHP
preg_match, JS RegExp). regex(x, 'pat') now becomes
regexMatch('/^(?:pat)$/', x): `/` is escaped, and the pattern is anchored
because ODK matches the whole value while LimeSurvey searches. The reverse
reads this back to regex(x, 'pat') and still accepts the old field-first
form. The bare-regexMatch escape hatch delimits its pattern too.

Fixes #218.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jstet
jstet force-pushed the fix/regexmatch-218 branch from 9cee121 to fbd410c Compare November 4, 2026 20:10
@jstet
jstet merged commit f7a182e into main Nov 4, 2026
@jstet jstet mentioned this pull request Nov 4, 2026
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.

xlsform2lstsv: regex() is emitted as regexMatch(self, 'pat'), but LimeSurvey expects regexMatch('/pat/', self)

1 participant