feat: publish Regex Tools 0.1.0

This commit is contained in:
2026-07-24 18:04:21 +02:00
commit 873b9b218d
136 changed files with 24212 additions and 0 deletions

View File

@@ -0,0 +1,248 @@
import { describe, expect, it } from "vitest";
import { EcmaScriptSyntaxProvider } from "../../src/regex/syntax/providers/ecmascript/EcmaScriptSyntaxProvider";
import { executeEcmaScript } from "../../src/regex/execution/adapters/ecmascript/EcmaScriptEngineAdapter";
const provider = new EcmaScriptSyntaxProvider();
const cases = [
{
name: "named capture",
pattern: "(?<name>\\w+)",
flags: ["u"],
subject: "alice",
value: "alice",
},
{
name: "lookbehind",
pattern: "(?<=€)\\d+",
flags: ["u"],
subject: "€42",
value: "42",
},
{
name: "backreference",
pattern: "(?<word>\\w+)\\s+\\k<word>",
flags: ["u"],
subject: "echo echo",
value: "echo echo",
},
{
name: "Unicode property",
pattern: "\\p{Letter}+",
flags: ["u"],
subject: "Grüße 42",
value: "Grüße",
},
] as const;
describe("project-authored ECMAScript conformance cases", () => {
for (const testCase of cases) {
it(testCase.name, async () => {
const syntax = await provider.parsePattern({
flavour: "ecmascript",
pattern: testCase.pattern,
flags: testCase.flags,
options: {},
});
expect(syntax.accepted).toBe(true);
const result = executeEcmaScript({
flavour: "ecmascript",
pattern: testCase.pattern,
flags: testCase.flags,
subject: testCase.subject,
captureMetadata: syntax.captures,
scanAll: false,
maximumMatches: 100,
maximumCaptureRows: 1_000,
});
expect(result.accepted).toBe(true);
expect(result.matches[0]?.value).toBe(testCase.value);
});
}
it("keeps astral ranges in UTF-16 code units", async () => {
const syntax = await provider.parsePattern({
flavour: "ecmascript",
pattern: "(?<emoji>😀)",
flags: ["u"],
options: {},
});
const result = executeEcmaScript({
flavour: "ecmascript",
pattern: "(?<emoji>😀)",
flags: ["u"],
subject: "x😀y",
captureMetadata: syntax.captures,
scanAll: false,
maximumMatches: 10,
maximumCaptureRows: 10,
});
expect(result.matches[0]?.range).toEqual({
startUtf16: 1,
endUtf16: 3,
});
expect(result.matches[0]?.captures[0]?.nativeRange).toEqual({
start: 1,
end: 3,
unit: "utf16",
});
});
it.each([
{
name: "global iteration",
pattern: "\\d",
flags: ["g"],
subject: "1 2",
values: ["1", "2"],
},
{
name: "sticky iteration",
pattern: "\\w",
flags: ["y"],
subject: "ab",
values: ["a"],
},
{
name: "ignore case",
pattern: "hello",
flags: ["i"],
subject: "HELLO",
values: ["HELLO"],
},
{
name: "multiline anchors",
pattern: "^second$",
flags: ["m"],
subject: "first\nsecond\nthird",
values: ["second"],
},
{
name: "dot all",
pattern: "a.b",
flags: ["s"],
subject: "a\nb",
values: ["a\nb"],
},
{
name: "Unicode sets",
pattern: "[\\p{Letter}&&\\p{ASCII}]+",
flags: ["v"],
subject: "abcä",
values: ["abc"],
},
])("implements $name flag semantics", async (testCase) => {
const syntax = await provider.parsePattern({
flavour: "ecmascript",
pattern: testCase.pattern,
flags: testCase.flags,
options: {},
});
expect(syntax.accepted).toBe(true);
const result = executeEcmaScript({
flavour: "ecmascript",
pattern: testCase.pattern,
flags: testCase.flags,
subject: testCase.subject,
captureMetadata: syntax.captures,
scanAll: false,
maximumMatches: 100,
maximumCaptureRows: 1_000,
});
expect(result.accepted).toBe(true);
expect(result.matches.map((match) => match.value)).toEqual(testCase.values);
});
it("preserves an explicit user indices flag", async () => {
const syntax = await provider.parsePattern({
flavour: "ecmascript",
pattern: "(a)",
flags: ["d"],
options: {},
});
const result = executeEcmaScript({
flavour: "ecmascript",
pattern: "(a)",
flags: ["d"],
subject: "a",
captureMetadata: syntax.captures,
scanAll: false,
maximumMatches: 10,
maximumCaptureRows: 10,
});
expect(result.accepted).toBe(true);
expect(result.flags.userFlags).toBe("d");
expect(result.flags.internallyAddedIndicesFlag).toBe(false);
expect(result.matches[0]?.captures[0]?.range).toEqual({
startUtf16: 0,
endUtf16: 1,
});
});
it("reports malformed syntax from both provider and engine", async () => {
const syntax = await provider.parsePattern({
flavour: "ecmascript",
pattern: "(",
flags: [],
options: {},
});
expect(syntax.accepted).toBe(false);
expect(syntax.diagnostics[0]?.range).toEqual({
startUtf16: 1,
endUtf16: 1,
});
const execution = executeEcmaScript({
flavour: "ecmascript",
pattern: "(",
flags: [],
subject: "",
captureMetadata: [],
scanAll: false,
maximumMatches: 10,
maximumCaptureRows: 10,
});
expect(execution.accepted).toBe(false);
expect(execution.diagnostics[0]?.code).toBe("compile-error");
});
it("records the current runtime's duplicate-name behavior", async () => {
const pattern = "(?<value>a)|(?<value>b)";
const syntax = await provider.parsePattern({
flavour: "ecmascript",
pattern,
flags: [],
options: {},
});
const execution = executeEcmaScript({
flavour: "ecmascript",
pattern,
flags: [],
subject: "b",
captureMetadata: syntax.captures,
scanAll: false,
maximumMatches: 10,
maximumCaptureRows: 10,
});
let runtimeSupportsDuplicateNames = true;
try {
new RegExp(pattern);
} catch {
runtimeSupportsDuplicateNames = false;
}
expect(execution.accepted).toBe(runtimeSupportsDuplicateNames);
if (runtimeSupportsDuplicateNames) {
expect(
execution.matches[0]?.captures.find(
(capture) =>
capture.groupName === "value" &&
capture.status !== "did-not-participate",
)?.value,
).toBe("b");
} else {
expect(execution.diagnostics[0]?.code).toBe("compile-error");
}
});
});