UNPKG

clarity-pattern-parser

Version:

Parsing Library for Typescript and Javascript.

1,467 lines (1,223 loc) 48.4 kB
import { Sequence } from "../patterns/Sequence"; import { Literal } from "../patterns/Literal"; import { Not } from "../patterns/Not"; import { Options } from "../patterns/Options"; import { Pattern } from "../patterns/Pattern"; import { Reference } from "../patterns/Reference"; import { Regex } from "../patterns/Regex"; import { Repeat } from "../patterns/Repeat"; import { Grammar } from "./Grammar"; import { Optional } from "../patterns/Optional"; import { Context } from "../patterns/Context"; import { createPatternsTemplate, patterns } from "./patterns"; import { Expression } from "../patterns/Expression"; import { Cursor } from "../patterns/Cursor"; describe("Grammar", () => { test("Literal", () => { const expression = ` name = "John" `; const patterns = Grammar.parseString(expression); const namePattern = patterns["name"]; const name = new Literal("name", "John"); const expected = new Context("name", name); expect(namePattern.isEqual(expected)).toBeTruthy(); }); test("Literal With Escaped Characters", () => { const expression = ` chars = "\\n\\r\\t\\b\\f\\v\\0\\x00\\u0000\\"\\\\" `; const patterns = Grammar.parseString(expression); const namePattern = patterns["chars"]; const chars = new Literal("chars", "\n\r\t\b\f\v\0\x00\u0000\"\\"); const expected = new Context('chars', chars); expect(namePattern.isEqual(expected)).toBeTruthy(); }); test("Literal With Escaped Quotes", () => { const expression = ` content = "With Con\\"tent" `; const patterns = Grammar.parseString(expression); const namePattern = patterns["content"]; const content = new Literal("content", "With Con\"tent"); const expected = new Context(`content`, content); expect(namePattern.isEqual(expected)).toBeTruthy(); }); test("Regex", () => { const expression = ` name = /\\w/ `; const patterns = Grammar.parseString(expression); const pattern = patterns["name"]; const name = new Regex("name", "\\w"); const expected = new Context(`name`, name); expect(pattern.isEqual(expected)).toBeTruthy(); }); test("Or", () => { const expression = ` john = "John" jane = "Jane" names = john | jane `; const patterns = Grammar.parseString(expression); const pattern = patterns["names"]; const john = new Literal("john", "John"); const jane = new Literal("jane", "Jane"); const names = new Options("names", [john, jane], true); const expected = new Context("names", names, [john, jane]); expect(pattern.isEqual(expected)).toBeTruthy(); }); test("And", () => { const expression = ` space = " " first-name = /\\w/ last-name = /\\w/ full-name = first-name + space + last-name `; const patterns = Grammar.parseString(expression); const pattern = patterns["full-name"]; const space = new Literal("space", " "); const firstName = new Regex("first-name", "\\w"); const lastName = new Regex("last-name", "\\w"); const fullName = new Sequence("full-name", [firstName, space, lastName]); const expected = new Context("full-name", fullName, [space, firstName, lastName]); expect(pattern.isEqual(expected)).toBeTruthy(); }); test("And With Optional Pattern", () => { const expression = ` space = " " first-name = /\\w/ last-name = /\\w/ middle-name = /\\w/ middle-name-with-space = middle-name + space full-name = first-name + space + middle-name-with-space? + last-name `; const patterns = Grammar.parseString(expression); const pattern = patterns["full-name"]; const space = new Literal("space", " "); const firstName = new Regex("first-name", "\\w"); const lastName = new Regex("last-name", "\\w"); const middleName = new Regex("middle-name", "\\w"); const middleNameWithSpace = new Sequence("middle-name-with-space", [middleName, space]); const optionalMiddleNameWithSpace = new Optional("optional-middle-name-with-space", middleNameWithSpace); const fullName = new Sequence("full-name", [firstName, space, optionalMiddleNameWithSpace, lastName]); const expected = new Context("full-name", fullName, [space, firstName, lastName, middleName, middleNameWithSpace]); expect(pattern.isEqual(expected)).toBeTruthy(); }); test("And With Not Pattern", () => { const expression = ` space = " " first-name = /\\w/ last-name = /\\w/ middle-name = /\\w/ jack = "Jack" middle-name-with-space = middle-name + space full-name = !jack + first-name + space + middle-name-with-space? + last-name `; const patterns = Grammar.parseString(expression); const pattern = patterns["full-name"]; const space = new Literal("space", " "); const firstName = new Regex("first-name", "\\w"); const lastName = new Regex("last-name", "\\w"); const middleName = new Regex("middle-name", "\\w"); const jack = new Literal("jack", "Jack"); const middleNameWithSpace = new Sequence("middle-name-with-space", [middleName, space]); const optionalMiddleNameWithSpace = new Optional("optional-middle-name-with-space", middleNameWithSpace); const fullName = new Sequence("full-name", [new Not("not-jack", jack), firstName, space, optionalMiddleNameWithSpace, lastName]); const expected = new Context("full-name", fullName, [space, firstName, lastName, middleName, jack, middleNameWithSpace]); expect(pattern.isEqual(expected)).toBeTruthy(); }); test("Repeat", () => { const expression = ` digit = /\\d/ digits = (digit)+ `; const patterns = Grammar.parseString(expression); const pattern = patterns["digits"]; const digit = new Regex("digit", "\\d"); const digits = new Repeat("digits", digit); const expected = new Context("digits", digits, [digit]); expect(pattern.isEqual(expected)).toBeTruthy(); }); test("Repeat Zero Or More", () => { const expression = ` digit = /\\d/ digits = (digit)* `; const patterns = Grammar.parseString(expression); const pattern = patterns["digits"]; const digit = new Regex("digit", "\\d"); const digits = new Optional("optional-digits", new Repeat("digits", digit, { min: 0 })); const expected = new Context("optional-digits", digits, [digit]); debugger; expect(pattern.isEqual(expected)).toBeTruthy(); }); test("Repeat Lower Limit", () => { const expression = ` digit = /\\d+/ digits = (digit){1,} `; const patterns = Grammar.parseString(expression); const pattern = patterns["digits"]; const digit = new Regex("digit", "\\d+"); const digits = new Repeat("digits", digit, { min: 1 }); const expected = new Context("digits", digits, [digit]); expect(pattern.isEqual(expected)).toBeTruthy(); }); test("Repeat Bounded", () => { const expression = ` digit = /\\d+/ digits = (digit){1,3} `; const patterns = Grammar.parseString(expression); const pattern = patterns["digits"]; const digit = new Regex("digit", "\\d+"); const digits = new Repeat("digits", digit, { min: 1, max: 3 }); const expected = new Context("digits", digits, [digit]); expect(pattern.isEqual(expected)).toBeTruthy(); }); test("Repeat Upper Limit", () => { const expression = ` digit = /\\d+/ digits = (digit){,3} `; const patterns = Grammar.parseString(expression); const pattern = patterns["digits"]; const digit = new Regex("digit", "\\d+"); const digits = new Repeat("digits", digit, { min: 0, max: 3 }); const expected = new Context("digits", digits, [digit]); expect(pattern.isEqual(expected)).toBeTruthy(); }); test("Repeat Exact", () => { const expression = ` digit = /\\d+/ digits = (digit){3} `; const patterns = Grammar.parseString(expression); const pattern = patterns["digits"]; const digit = new Regex("digit", "\\d+"); const digits = new Repeat("digits", digit, { min: 3, max: 3 }); const expected = new Context("digits", digits, [digit]); expect(pattern.isEqual(expected)).toBeTruthy(); }); test("Repeat Divider", () => { const expression = ` digit = /\\d+/ comma = "," digits = (digit, comma){3} `; const patterns = Grammar.parseString(expression); const pattern = patterns["digits"]; const digit = new Regex("digit", "\\d+"); const comma = new Literal("comma", ","); const digits = new Repeat("digits", digit, { divider: comma, min: 3, max: 3 }); const expected = new Context("digits", digits, [digit, comma]); expect(pattern.isEqual(expected)).toBeTruthy(); }); test("Repeat Divider With Trim Divider", () => { const expression = ` digit = /\\d+/ comma = "," digits = (digit, comma trim)+ `; const patterns = Grammar.parseString(expression); const pattern = patterns["digits"]; const digit = new Regex("digit", "\\d+"); const comma = new Literal("comma", ","); const digits = new Repeat("digits", digit, { divider: comma, min: 1, trimDivider: true }); const expected = new Context("digits", digits, [digit, comma]); expect(pattern.isEqual(expected)).toBeTruthy(); }); test("Repeat Divider With Trim Divider And Bounds", () => { const expression = ` digit = /\\d+/ comma = "," digits = (digit, comma trim){3, 3} `; const patterns = Grammar.parseString(expression); const pattern = patterns["digits"]; const digit = new Regex("digit", "\\d+"); const divider = new Literal("comma", ","); const digits = new Repeat("digits", digit, { divider, min: 3, max: 3, trimDivider: true }); const expected = new Context("digits", digits, [digit, divider]); expect(pattern.isEqual(expected)).toBeTruthy(); }); test("Reference", () => { const expression = ` digit = /\\d+/ divider = /\\s*,\\s*/ open-bracket = "[" close-bracket = "]" spaces = /\\s+/ items = digit | array array-items = (items, divider trim)* array = open-bracket + spaces? + array-items + spaces? + close-bracket `; const patterns = Grammar.parseString(expression); const pattern = patterns["array"] as Pattern; let text = "[1, []]"; let result = pattern.exec(text, true); expect(result.ast?.value).toEqual("[1, []]"); }); test("Alias", () => { const expression = ` name = /regex/ alias = name `; const patterns = Grammar.parseString(expression); const name = patterns["name"]; const expectedName = new Regex("name", "regex"); const alias = patterns["alias"]; const expectedAlias = new Regex("alias", "regex"); const contextualAlias = new Context("alias", expectedAlias, [expectedName]); const contextualName = new Context("name", expectedName, [expectedAlias]); expect(name.isEqual(contextualName)).toBeTruthy(); expect(alias.isEqual(contextualAlias)).toBeTruthy(); }); test("Bad Grammar At Beginning", () => { expect(() => { const expression = `//`; Grammar.parseString(expression); }).toThrow(); }); test("Bad Grammar Further In", () => { expect(() => { const expression = `name = /\\w/ age = /() `; Grammar.parseString(expression); }).toThrow(); }); test("Import", async () => { const importExpression = `first-name = "John"`; const expression = ` import { first-name } from "some/path/to/file.cpat" last-name = "Doe" space = " " full-name = first-name + space + last-name `; function resolveImport(resource: string) { expect(resource).toBe("some/path/to/file.cpat"); return Promise.resolve({ expression: importExpression, resource }); } const patterns = await Grammar.parse(expression, { resolveImport }); const fullname = patterns["full-name"] as Pattern; const result = fullname.exec("John Doe"); expect(result?.ast?.value).toBe("John Doe"); }); test("Imports", async () => { const importExpression = `first-name = "John"`; const spaceExpression = `space = " "`; const pathMap: Record<string, string> = { "space.cpat": spaceExpression, "first-name.cpat": importExpression }; const expression = ` import { first-name } from "first-name.cpat" import { space } from "space.cpat" last-name = "Doe" full-name = first-name + space + last-name `; function resolveImport(resource: string) { return Promise.resolve({ expression: pathMap[resource], resource }); } const patterns = await Grammar.parse(expression, { resolveImport }); const fullname = patterns["full-name"] as Pattern; const result = fullname.exec("John Doe"); expect(result?.ast?.value).toBe("John Doe"); }); test("Imports with Params", async () => { const importExpression = `first-name = "John"`; const spaceExpression = ` use params { custom-space } space = custom-space `; const expression = ` import { first-name } from "first-name.cpat" import { space } from "space.cpat" with params { custom-space = " " } last-name = "Doe" full-name = first-name + space + last-name `; const pathMap: Record<string, string> = { "space.cpat": spaceExpression, "first-name.cpat": importExpression }; function resolveImport(resource: string) { return Promise.resolve({ expression: pathMap[resource], resource }); } const patterns = await Grammar.parse(expression, { resolveImport }); const fullname = patterns["full-name"] as Pattern; const result = fullname.exec("John Doe"); expect(result?.ast?.value).toBe("John Doe"); }); test("Export Name", async () => { const expression = ` import { use-this } from "resource1" import {name} from "resource2" with params { use-this } name `; const resource1 = ` use-this = "Use This" `; const resource2 = ` use params { use-this } name = use-this `; const pathMap: Record<string, string> = { "resource1": resource1, "resource2": resource2 }; function resolveImport(resource: string) { return Promise.resolve({ expression: pathMap[resource], resource }); } const patterns = await Grammar.parse(expression, { resolveImport }); const pattern = patterns["name"] as Literal; const result = pattern.exec("Use This"); expect(result.ast?.value).toBe("Use This"); }); test("Import Alias", async () => { const expression = ` import { value as alias } from "resource1" import { export-value } from "resource2" with params { param = alias } name = export-value `; const resource1 = ` value = "Value" `; const resource2 = ` use params { param } export-value = param `; const pathMap: Record<string, string> = { "resource1": resource1, "resource2": resource2 }; function resolveImport(resource: string) { return Promise.resolve({ expression: pathMap[resource], resource }); } const patterns = await Grammar.parse(expression, { resolveImport }); const pattern = patterns["name"] as Literal; const result = pattern.exec("Value"); expect(result.ast?.value).toBe("Value"); }); test("Default Params Resolves to Default Value", async () => { const expression = ` use params { value = default-value } default-value = "DefaultValue" alias = value `; function resolveImport(_: string) { return Promise.reject(new Error("No Import")); } const patterns = await Grammar.parse(expression, { resolveImport }); const pattern = patterns["alias"] as Literal; const result = pattern.exec("DefaultValue"); expect(result.ast?.value).toBe("DefaultValue"); }); test("Default Params Resolves to params imported", async () => { const expression = ` use params { value = default-value } default-value = "DefaultValue" alias = value `; function resolveImport(_: string) { return Promise.reject(new Error("No Import")); } const patterns = await Grammar.parse(expression, { resolveImport, params: [new Literal("value", "Value")] }); const pattern = patterns["alias"] as Literal; const result = pattern.exec("Value"); expect(result.ast?.value).toBe("Value"); }); test("Default Params Resolves to imported default value", async () => { const expression = ` import { my-value as default-value } from "resource1" use params { value = default-value } default-value = "DefaultValue" alias = value `; const resource1 = ` my-value = "MyValue" `; const pathMap: Record<string, string> = { "resource1": resource1, }; function resolveImport(resource: string) { return Promise.resolve({ expression: pathMap[resource], resource }); } const patterns = await Grammar.parse(expression, { resolveImport }); const pattern = patterns["alias"] as Literal; const result = pattern.exec("MyValue"); expect(result.ast?.value).toBe("MyValue"); }); test("Anonymous Patterns", () => { const expression = ` complex-expression = !"NOT_THIS" + "Text"? + /regex/ + ("Text" <|> /regex/ <|> (pattern)+) + (pattern | pattern) `; const patterns = Grammar.parseString(expression); const expected = new Context("complex-expression", new Sequence("complex-expression", [ new Not("not-NOT_THIS", new Literal("NOT_THIS", "NOT_THIS")), new Optional("Text", new Literal("Text", "Text")), new Regex("regex", "regex"), new Options("anonymous", [ new Literal("Text", "Text"), new Regex("regex", "regex"), new Repeat("anonymous", new Reference("pattern")), ], true ), new Expression("anonymous", [ new Reference("pattern"), new Reference("pattern") ]) ])); expect(patterns["complex-expression"].isEqual(expected)).toBeTruthy(); }); test("Grammar With Spaces", () => { const expression = ` john = "John" jane = "Jane" `; const patterns = Grammar.parseString(expression); expect(patterns.john).not.toBeNull(); expect(patterns.jane).not.toBeNull(); }); test("Grammar Import", async () => { const importExpression = `first-name = "John"`; const spaceExpression = ` use params { custom-space } space = custom-space `; const expression = ` use params { custom-space } import { first-name } from "first-name.cpat" import { space } from "space.cpat" with params { custom-space = custom-space } last-name = "Doe" full-name = first-name + space + last-name `; const pathMap: Record<string, string> = { "space.cpat": spaceExpression, "first-name.cpat": importExpression, "root.cpat": expression }; function resolveImport(resource: string) { return Promise.resolve({ expression: pathMap[resource], resource }); } const patterns = await Grammar.import("root.cpat", { resolveImport, params: [new Literal("custom-space", " ")] }); const fullname = patterns["full-name"] as Pattern; const result = fullname.exec("John Doe"); expect(result?.ast?.value).toBe("John Doe"); }); test("Expression Pattern", () => { const { expression } = patterns` variables = "a" | "b" | "c" ternary = expression + " ? " + expression + " : " + expression expression = ternary | variables bad-ternary = bad-expression + " ? " + bad-expression + " : " + bad-expression bad-expression = bad-ternary | bad-ternary `; let result = expression.exec("a ? b : c"); expect(result).toBe(result); }); test("Expression Pattern With Right Association", () => { const { expression } = patterns` variables = "a" | "b" | "c" | "d" | "e" ternary = expression + " ? " + expression + " : " + expression expression = ternary right | variables `; let result = expression.exec("a ? b : c ? d : e"); debugger; expect(result).toBe(result); }); test("Decorators", () => { const { spaces } = patterns` @tokens([" "]) spaces = /\\s+/ `; expect(spaces.getTokens()).toEqual([" "]); }); test("Decorators No Args", () => { const { spaces } = patterns` @tokens() spaces = /\\s+/ `; expect(spaces.getTokens()).toEqual([]); }); test("Decorators Bad Args", () => { const { spaces } = patterns` @tokens("Bad") spaces = /\\s+/ `; expect(spaces.getTokens()).toEqual([]); }); test("Decorators Bad Decorator", () => { const { spaces } = patterns` @bad-decorator() spaces = /\\s+/ `; expect(spaces.getTokens()).toEqual([]); }); test("Decorators On Multiple Patterns", () => { const { spaces, digits } = patterns` @tokens([" "]) spaces = /\\s+/ @tokens(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]) digits = /\\d+/ `; expect(spaces.getTokens()).toEqual([" "]); expect(digits.getTokens()).toEqual(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]); }); test("Decorators On Multiple Patterns And Comments", () => { const { spaces, digits } = patterns` #Comment @tokens([" "]) spaces = /\\s+/ #Comment @tokens(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]) #Comment digits = /\\d+/ `; expect(spaces.getTokens()).toEqual([" "]); expect(digits.getTokens()).toEqual(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]); }); test("Custom Named Decorator", () => { const allRecordedPatterns: string[] = []; const patterns = createPatternsTemplate({ decorators: { record: (pattern) => { allRecordedPatterns.push(pattern.name); } } }); patterns` @record spaces = /\\s+/ @record digits = /\\d+/ `; expect(allRecordedPatterns).toEqual(["spaces", "digits"]); }); test("Decorator With Empty Object Literal", () => { patterns` @method({}) spaces = /\\s+/ `; }); test("Decorator With Object Literal", () => { patterns` @method({"prop": 2}) spaces = /\\s+/ `; }); test("Take Until", () => { const { scriptText } = patterns` script-text = ?->| "</script" `; const result = scriptText.parse(new Cursor("function(){}</script")); expect(result?.value).toBe("function(){}"); }); test("Import Sync", () => { function resolveImportSync(path: string, importer: string | null) { return { expression: pathMap[path], resource: path, } } const rootExpression = ` import { name } from "first-name.cpat" full-name = name `; const firstNameExpression = ` import { last-name } from "last-name.cpat" first-name = "John" name = first-name + " " + last-name `; const lastNameExpression = ` last-name = "Doe" `; const pathMap: Record<string, string> = { "first-name.cpat": firstNameExpression, "last-name.cpat": lastNameExpression, }; const patterns = Grammar.parseString(rootExpression, { resolveImportSync: resolveImportSync, originResource: "/root.cpat", }); expect(patterns["full-name"]).not.toBeNull(); const result = patterns["full-name"].exec("John Doe"); expect(result?.ast?.value).toBe("John Doe"); }); // --- Greedy vs Non-Greedy Options --- test("greedy options picks longest match", () => { const { greeting } = patterns` hi = "hi" hiya = "hiya" greeting = hi <|> hiya `; expect(greeting.exec("hiya").ast?.value).toBe("hiya"); }); test("non-greedy options picks first match", () => { const { greeting } = patterns` hi = "hi" hiya = "hiya" greeting = hi | hiya `; expect(greeting.exec("hi").ast?.value).toBe("hi"); }); // --- Default Params --- test("default param resolves to locally defined pattern when no param supplied", () => { const p = Grammar.parseString(` use params { value = default-value } default-value = "DefaultValue" alias = value `); expect(p["alias"].exec("DefaultValue").ast?.value).toBe("DefaultValue"); }); test("default param is overridden when param is supplied", () => { const p = Grammar.parseString(` use params { value = default-value } default-value = "DefaultValue" alias = value `, { params: [new Literal("value", "Supplied")] }); expect(p["alias"].exec("Supplied").ast?.value).toBe("Supplied"); }); test("default param resolves to imported pattern", async () => { const p = await Grammar.parse(` import { my-value as default-value } from "r1" use params { value = default-value } alias = value `, { resolveImport: () => Promise.resolve({ expression: `my-value = "MyValue"`, resource: "r1" }) }); expect(p["alias"].exec("MyValue").ast?.value).toBe("MyValue"); }); test("use params without default declares param available from options", () => { const p = Grammar.parseString(` use params { value } alias = value `, { params: [new Literal("value", "Provided")] }); expect(p["alias"].exec("Provided").ast?.value).toBe("Provided"); }); // --- Expression Precedence --- test("expression respects declaration-order precedence for arithmetic", () => { const { expr } = patterns` number = /\\d+/ mul-expr = expr + " * " + expr add-expr = expr + " + " + expr expr = mul-expr | add-expr | number `; const result = expr.exec("1 + 2 * 3"); expect(result.ast?.value).toBe("1 + 2 * 3"); expect(result.ast!.value).toContain("+"); }); test("expression handles prefix operator", () => { const { expr } = patterns` number = /\\d+/ neg = "-" + expr expr = neg | number `; expect(expr.exec("-42").ast?.value).toBe("-42"); }); test("expression handles postfix operator", () => { const { expr } = patterns` number = /\\d+/ factorial = expr + "!" expr = factorial | number `; expect(expr.exec("5!").ast?.value).toBe("5!"); }); // --- Forward References --- test("forward reference resolves pattern defined later", () => { const p = Grammar.parseString(` greeting = "Hello " + name name = /\\w+/ `); expect(p["greeting"].exec("Hello World").ast?.value).toBe("Hello World"); }); // --- Import Errors --- test("duplicate import name throws", async () => { await expect(Grammar.parse(` import { value } from "r1" import { value } from "r2" `, { resolveImport: (r) => Promise.resolve({ expression: `value = "A"`, resource: r }) })).rejects.toThrow("already used"); }); test("importing nonexistent pattern throws", async () => { await expect(Grammar.parse(` import { nonexistent } from "r1" `, { resolveImport: () => Promise.resolve({ expression: `value = "A"`, resource: "r1" }) })).rejects.toThrow("Couldn't find pattern"); }); // --- Nested Groups --- test("nested parenthesized groups unwrap correctly", () => { const p = Grammar.parseString(` word = "hello" inner = ((word)) `); expect(p["inner"].exec("hello").ast?.value).toBe("hello"); }); // --- Alias Chain --- test("alias chain resolves through multiple levels", () => { const p = Grammar.parseString(` original = "value" alias1 = original alias2 = alias1 `); expect(p["alias2"].exec("value").ast?.value).toBe("value"); }); // --- Multiple Decorators --- test("multiple decorators applied to same pattern", () => { let callOrder: string[] = []; const customPatterns = createPatternsTemplate({ decorators: { first: (pattern) => { callOrder.push("first:" + pattern.name); }, second: (pattern) => { callOrder.push("second:" + pattern.name); } } }); customPatterns` @first @second name = /\\w+/ `; expect(callOrder).toContain("first:name"); expect(callOrder).toContain("second:name"); }); // --- Decorator JSON Arg Types --- test("decorator receives number argument", () => { let received: any; const cp = createPatternsTemplate({ decorators: { config: (_p, arg) => { received = arg; } } }); cp`@config(42)\nname = /\\w+/`; expect(received).toBe(42); }); test("decorator receives nested array argument", () => { let received: any; const cp = createPatternsTemplate({ decorators: { config: (_p, arg) => { received = arg; } } }); cp`@config([1, 2, [3]])\nname = /\\w+/`; expect(received).toEqual([1, 2, [3]]); }); test("decorator receives null argument", () => { let received: any = "not-null"; const cp = createPatternsTemplate({ decorators: { config: (_p, arg) => { received = arg; } } }); cp`@config(null)\nname = /\\w+/`; expect(received).toBeNull(); }); test("decorator receives string argument", () => { let received: any; const cp = createPatternsTemplate({ decorators: { config: (_p, arg) => { received = arg; } } }); cp`@config("hello")\nname = /\\w+/`; expect(received).toBe("hello"); }); // --- Patterns Template --- test("patterns template converts kebab-case to camelCase", () => { const result = patterns`my-long-name = "hello"`; expect(result["myLongName"]).toBeDefined(); expect(result["myLongName"].exec("hello").ast?.value).toBe("hello"); }); // --- Edge Case Grammars --- test("empty grammar throws", () => { expect(() => Grammar.parseString("")).toThrow("[Invalid Grammar]"); }); test("grammar with only comments produces no patterns", () => { const p = Grammar.parseString("# just a comment"); expect(Object.keys(p)).toHaveLength(0); }); test("grammar with syntax version and no patterns", () => { const p = Grammar.parseString("syntax 1.0"); expect(Object.keys(p)).toHaveLength(0); }); // --- Inline Anonymous Patterns --- test("repeat with inline literal pattern", () => { const p = Grammar.parseString(`items = ("item")+`); expect(p["items"].exec("itemitemitem").ast?.value).toBe("itemitemitem"); }); test("repeat with inline regex pattern", () => { const p = Grammar.parseString(`digits = (/\\d/)+`); expect(p["digits"].exec("123").ast?.value).toBe("123"); }); test("inline literals use their value as the pattern name", () => { const p = Grammar.parseString(`greeting = "Hello" + " " + /\\w+/`); const ast = p["greeting"].exec("Hello World").ast!; expect(ast.children[0].name).toBe("Hello"); expect(ast.children[1].name).toBe(" "); expect(ast.children[2].name).toBe("\\w+"); }); // --- Flattening --- test("five-way sequence flattens correctly", () => { const p = Grammar.parseString(`result = "a" + "b" + "c" + "d" + "e"`); expect(p["result"].exec("abcde").ast?.value).toBe("abcde"); }); test("four-way options all reachable", () => { const p = Grammar.parseString(`letter = "a" | "b" | "c" | "d"`); expect(p["letter"].exec("a").ast?.value).toBe("a"); expect(p["letter"].exec("d").ast?.value).toBe("d"); }); test("mixed greedy options flatten correctly", () => { const p = Grammar.parseString(`item = "ab" <|> "abc" <|> "a"`); expect(p["item"].exec("abc").ast?.value).toBe("abc"); }); // --- Complex Composition --- test("sequence mixing references, optionals, nots, and inline patterns", () => { const p = Grammar.parseString(` keyword = "let" ws = /\\s+/ ident = /[a-z]+/ eq = "=" digit = /\\d+/ let-stmt = keyword + ws + !digit + ident + ws? + eq + ws? + digit `); expect(p["let-stmt"].exec("let x=42").ast?.value).toBe("let x=42"); expect(p["let-stmt"].exec("let 9=42").ast).toBeNull(); }); test("recursive list with nested expressions", () => { const p = Grammar.parseString(` digit = /\\d+/ comma = ", " open = "(" close = ")" item = digit | list items = (item, comma trim)+ list = open + items + close `); expect(p["list"].exec("(1, (2, 3), 4)").ast?.value).toBe("(1, (2, 3), 4)"); }); // --- AST Structure --- test("sequence children reflect pattern names in AST", () => { const { fullName } = patterns` first = /[A-Z][a-z]+/ space = " " last = /[A-Z][a-z]+/ full-name = first + space + last `; const ast = fullName.exec("John Doe").ast!; expect(ast.children.length).toBe(3); expect(ast.children[0].name).toBe("first"); expect(ast.children[0].value).toBe("John"); expect(ast.children[2].name).toBe("last"); expect(ast.children[2].value).toBe("Doe"); }); test("repeat AST contains repeated children", () => { const { digits } = patterns` digit = /\\d/ digits = (digit)+ `; const ast = digits.exec("123").ast!; expect(ast.children.length).toBe(3); expect(ast.children[0].value).toBe("1"); expect(ast.children[1].value).toBe("2"); expect(ast.children[2].value).toBe("3"); }); test("options AST returns winning alternative directly", () => { const { choice } = patterns` a = "aaa" b = "bbb" choice = a | b `; const ast = choice.exec("bbb").ast!; expect(ast.name).toBe("b"); expect(ast.value).toBe("bbb"); }); test("expression AST wraps in expression node", () => { const { expr } = patterns` num = /\\d+/ add = expr + " + " + expr expr = add | num `; expect(expr.exec("1 + 2").ast!.type).toBe("expression"); }); // --- Multiple Names in One Import --- test("import multiple patterns from same resource", async () => { const p = await Grammar.parse(` import { first, last } from "names.cpat" full = first + " " + last `, { resolveImport: () => Promise.resolve({ expression: `first = "John"\nlast = "Doe"`, resource: "names.cpat" }) }); expect(p["full"].exec("John Doe").ast?.value).toBe("John Doe"); }); test("import multiple patterns with alias from same resource", async () => { const p = await Grammar.parse(` import { first, last as surname } from "names.cpat" full = first + " " + surname `, { resolveImport: () => Promise.resolve({ expression: `first = "John"\nlast = "Doe"`, resource: "names.cpat" }) }); expect(p["full"].exec("John Doe").ast?.value).toBe("John Doe"); }); // --- Repeat with Inline Complex Pattern --- test("repeat with inline sequence as repeated item", () => { const p = Grammar.parseString(` pairs = ("(" + /\\w+/ + ")")+ `); expect(p["pairs"].exec("(a)(b)(c)").ast?.value).toBe("(a)(b)(c)"); }); test("repeat with inline options as repeated item", () => { const p = Grammar.parseString(` bits = ("0" | "1")+ `); expect(p["bits"].exec("1010").ast?.value).toBe("1010"); }); // --- TakeUntil with Regex Terminator --- test("take until with regex terminator", () => { const { content } = patterns` content = ?->| /[<]/ `; const result = content.parse(new Cursor("hello world<")); expect(result?.value).toBe("hello world"); }); // --- Inline Not and Optional with Literals --- test("inline not with literal in sequence", () => { const p = Grammar.parseString(` word = /\\w+/ not-hello = !"hello" + word `); expect(p["not-hello"].exec("world").ast?.value).toBe("world"); expect(p["not-hello"].exec("hello").ast).toBeNull(); }); test("inline optional with literal in sequence", () => { const p = Grammar.parseString(` greeting = "hello" + " world"? `); expect(p["greeting"].exec("hello world").ast?.value).toBe("hello world"); expect(p["greeting"].exec("hello").ast?.value).toBe("hello"); }); // --- Syntax Version Combined with Patterns --- test("syntax version followed by patterns parses correctly", () => { const p = Grammar.parseString(` syntax 1.0 name = "hello" `); expect(p["name"].exec("hello").ast?.value).toBe("hello"); }); // --- Empty String Literal --- test("empty string literal throws", () => { expect(() => Grammar.parseString(`empty = ""`)).toThrow("Value Cannot be empty"); }); // --- Block Pattern --- test("Block with wildcard content", () => { const p = Grammar.parseString(` braces = ["{"] ... ["}"] `); const result = p["braces"].exec("{}"); expect(result.ast).not.toBeNull(); expect(result.ast!.type).toBe("block"); }); test("Block without content requires ...", () => { expect(() => Grammar.parseString(` braces = ["{"] ["}"] `)).toThrow(); }); test("Block with content pattern", () => { const p = Grammar.parseString(` content = /[^{}]+/ braces = ["{"] content ["}"] `); const result = p["braces"].exec("{hello}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{hello}"); }); test("Block with multiple content patterns", () => { const p = Grammar.parseString(` key = /[a-z]+/ value = /[a-z]+/ pair = ["{"] key + ":" + value ["}"] `); const result = p["pair"].exec("{foo:bar}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{foo:bar}"); }); test("Block handles nesting", () => { const p = Grammar.parseString(` braces = ["{"] ... ["}"] `); const cursor = new Cursor("{ { } }"); const ctx = p["braces"] as Context; const result = ctx.find(p => p.type === "block")!.parse(cursor); expect(result).not.toBeNull(); expect(result!.firstIndex).toBe(0); expect(result!.lastIndex).toBe(6); }); test("Block with inline literal content", () => { const p = Grammar.parseString(` parens = ["("] /[^()]+/ [")"] `); const result = p["parens"].exec("(hello)"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("(hello)"); }); test("Block wildcard matches any content", () => { const p = Grammar.parseString(` braces = ["{"] ... ["}"] `); const result = p["braces"].exec("{anything goes here}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{anything goes here}"); }); test("Block with inline regex content", () => { const p = Grammar.parseString(` braces = ["{"] /[^{}]*/ ["}"] `); const result = p["braces"].exec("{hello world}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{hello world}"); }); test("Block with inline literal content", () => { const p = Grammar.parseString(` parens = ["("] "hello" [")"] `); const result = p["parens"].exec("(hello)"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("(hello)"); }); test("Block with pattern reference content", () => { const p = Grammar.parseString(` word = /[a-z]+/ braces = ["{"] word ["}"] `); const result = p["braces"].exec("{hello}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{hello}"); }); test("Block with repeat content", () => { const p = Grammar.parseString(` item = /[a-z]+/ braces = ["{"] (item, ",")+ ["}"] `); const result = p["braces"].exec("{a,b,c}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{a,b,c}"); }); test("Block with grouped options content", () => { const p = Grammar.parseString(` braces = ["{"] ("yes" | "no") ["}"] `); expect(p["braces"].exec("{yes}").ast?.value).toBe("{yes}"); expect(p["braces"].exec("{no}").ast?.value).toBe("{no}"); }); test("Block with inline sequence content", () => { const p = Grammar.parseString(` key = /[a-z]+/ value = /[0-9]+/ braces = ["{"] key + "=" + value ["}"] `); const result = p["braces"].exec("{x=42}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{x=42}"); }); test("Block with three-part inline sequence content", () => { const p = Grammar.parseString(` a = "a" b = "b" c = "c" braces = ["{"] a + b + c ["}"] `); const result = p["braces"].exec("{abc}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{abc}"); }); test("Block with ungrouped options content", () => { const p = Grammar.parseString(` braces = ["{"] "yes" | "no" ["}"] `); expect(p["braces"].exec("{yes}").ast?.value).toBe("{yes}"); expect(p["braces"].exec("{no}").ast?.value).toBe("{no}"); }); // --- Import Without Resolver --- test("import without resolver (sync) throws", () => { expect(() => Grammar.parseString(` import { value } from "file.cpat" name = value `)).toThrow("No import resolver supplied."); }); test("import without resolver (async) throws", async () => { await expect(Grammar.parse(` import { value } from "file.cpat" name = value `)).rejects.toThrow("No import resolver supplied."); }); // --- Sync Import Error Wrapping --- test("sync import wraps error from bad imported grammar", () => { expect(() => Grammar.parseString(` import { value } from "bad.cpat" name = value `, { resolveImportSync: () => ({ expression: "///bad grammar///", resource: "bad.cpat" }) })).toThrow(/Failed loading expression from: "bad\.cpat"/); }); // --- Async Import Error Wrapping --- test("async import wraps error from bad imported grammar", async () => { await expect(Grammar.parse(` import { value } from "bad.cpat" name = value `, { resolveImport: () => Promise.resolve({ expression: "///bad grammar///", resource: "bad.cpat" }) })).rejects.toThrow(/Failed loading expression from: "bad\.cpat"/); }); // --- Import Alias Errors --- test("duplicate import alias throws", async () => { await expect(Grammar.parse(` import { a as alias } from "r1" import { b as alias } from "r2" `, { resolveImport: (r) => Promise.resolve({ expression: r === "r1" ? `a = "A"` : `b = "B"`, resource: r }) })).rejects.toThrow("already used"); }); test("importing nonexistent pattern via alias throws", async () => { await expect(Grammar.parse(` import { nonexistent as alias } from "r1" `, { resolveImport: () => Promise.resolve({ expression: `value = "A"`, resource: "r1" }) })).rejects.toThrow("Couldn't find pattern"); }); // --- Export Pattern (sync path) --- test("export pattern promotes previously defined pattern", () => { const p = Grammar.parseString(` import { value } from "r1" value `, { resolveImportSync: () => ({ expression: `value = "Hello"`, resource: "r1" }) }); expect(p["value"].exec("Hello").ast?.value).toBe("Hello"); }); });