UNPKG

clarity-pattern-parser

Version:

Parsing Library for Typescript and Javascript.

755 lines (694 loc) 23.3 kB
import { Block } from "./Block"; import { Literal } from "./Literal"; import { Regex } from "./Regex"; import { Optional } from "./Optional"; import { Sequence } from "./Sequence"; import { Cursor } from "./Cursor"; import { Reference } from "./Reference"; describe("Block", () => { describe("simple blocks", () => { test("matches empty block", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const result = block.exec("{}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{}"); expect(result.ast!.children.length).toBe(2); expect(result.ast!.children[0].value).toBe("{"); expect(result.ast!.children[1].value).toBe("}"); }); test("matches block with content", () => { const block = new Block( "braces", new Literal("open", "{"), new Regex("content", "[^{}]+"), new Literal("close", "}") ); const result = block.exec("{hello}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{hello}"); expect(result.ast!.children.length).toBe(3); expect(result.ast!.children[0].value).toBe("{"); expect(result.ast!.children[1].value).toBe("hello"); expect(result.ast!.children[2].value).toBe("}"); }); test("null content skips inner text", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const cursor = new Cursor("{stuff inside}"); const node = block.parse(cursor); expect(node).not.toBeNull(); expect(node!.firstIndex).toBe(0); expect(node!.lastIndex).toBe(13); expect(node!.children.length).toBe(3); expect(node!.children[0].value).toBe("{"); expect(node!.children[1].value).toBe("stuff inside"); expect(node!.children[1].name).toBe("braces-content"); expect(node!.children[2].value).toBe("}"); }); test("null content with empty block", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const result = block.exec("{}"); expect(result.ast).not.toBeNull(); expect(result.ast!.children.length).toBe(2); }); test("null content with nested delimiters", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const cursor = new Cursor("{ { } }"); const node = block.parse(cursor); expect(node).not.toBeNull(); expect(node!.firstIndex).toBe(0); expect(node!.lastIndex).toBe(6); expect(node!.children.length).toBe(3); expect(node!.children[1].value).toBe(" { } "); }); test("matches block with whitespace content", () => { const block = new Block( "braces", new Literal("open", "{"), new Regex("content", "\\s+"), new Literal("close", "}") ); const result = block.exec("{ }"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{ }"); }); test("matches parenthesized block", () => { const block = new Block( "parens", new Literal("open", "("), new Regex("content", "[^()]+"), new Literal("close", ")") ); const result = block.exec("(hello)"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("(hello)"); }); test("matches block with multi-char delimiters", () => { const block = new Block( "begin-end", new Literal("open", "BEGIN"), new Regex("content", "[^BE]+"), new Literal("close", "END") ); const result = block.exec("BEGIN stuff END"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("BEGIN stuff END"); }); }); describe("nested blocks", () => { test("scan finds matching close past nested braces", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const cursor = new Cursor("{ { } }"); const node = block.parse(cursor); expect(node).not.toBeNull(); expect(node!.firstIndex).toBe(0); expect(node!.lastIndex).toBe(6); }); test("counts nesting depth correctly", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const cursor = new Cursor("{ { { } } }"); const node = block.parse(cursor); expect(node).not.toBeNull(); expect(node!.firstIndex).toBe(0); expect(node!.lastIndex).toBe(10); }); test("nested blocks with recursive content", () => { const innerBlock = new Block( "inner", new Literal("open", "{"), null, new Literal("close", "}") ); const ws = new Optional("ws", new Regex("whitespace", "\\s+")); const block = new Block( "outer", new Literal("open", "{"), new Sequence("content", [ws, innerBlock, ws]), new Literal("close", "}") ); const result = block.exec("{ {} }"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{ {} }"); const children = result.ast!.children; expect(children[0].value).toBe("{"); expect(children[children.length - 1].value).toBe("}"); }); test("graceful close when opens outnumber closes", () => { // With null content, the scanner finds the matching close // and the block captures open + close only const block = new Block( "tag", new Literal("open", "<task>"), null, new Literal("close", "</task>") ); const cursor = new Cursor("<task>stuff <task> more stuff</task>"); const node = block.parse(cursor); expect(node).not.toBeNull(); expect(node!.firstIndex).toBe(0); expect(node!.children[0].value).toBe("<task>"); expect(node!.children[node!.children.length - 1].value).toBe("</task>"); }); test("graceful close with multiple unmatched opens", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const cursor = new Cursor("{ { { }"); const node = block.parse(cursor); expect(node).not.toBeNull(); expect(node!.firstIndex).toBe(0); expect(node!.lastIndex).toBe(6); }); test("still fails when no close delimiter exists at all", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const cursor = new Cursor("{ no close here"); const node = block.parse(cursor); expect(node).toBeNull(); }); test("adjacent blocks after first block", () => { const block = new Block( "braces", new Literal("open", "{"), new Regex("content", "[^{}]+"), new Literal("close", "}") ); const cursor = new Cursor("{a}{b}"); const first = block.parse(cursor); expect(first).not.toBeNull(); expect(first!.value).toBe("{a}"); expect(cursor.index).toBe(2); cursor.next(); const second = block.parse(cursor); expect(second).not.toBeNull(); expect(second!.value).toBe("{b}"); }); }); describe("error cases", () => { test("returns null for unmatched open", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const result = block.exec("{"); expect(result.ast).toBeNull(); }); test("returns null when input doesn't start with open", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const result = block.exec("x{}"); expect(result.ast).toBeNull(); }); test("returns null for empty input", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const result = block.exec(""); expect(result.ast).toBeNull(); }); }); describe("content patterns", () => { test("matches sequence content pattern", () => { const block = new Block( "braces", new Literal("open", "{"), new Sequence("content", [ new Regex("name", "[a-z]+"), new Literal("colon", ":"), new Regex("value", "[a-z]+"), ]), new Literal("close", "}") ); const result = block.exec("{foo:bar}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{foo:bar}"); // open + sequence content + close expect(result.ast!.children.length).toBe(3); // The sequence node contains the individual parts const contentNode = result.ast!.children[1]; expect(contentNode.children[0].value).toBe("foo"); expect(contentNode.children[1].value).toBe(":"); expect(contentNode.children[2].value).toBe("bar"); }); test("handles optional content pattern", () => { const block = new Block( "braces", new Literal("open", "{"), new Optional("maybe", new Regex("content", "[a-z]+")), new Literal("close", "}") ); const result = block.exec("{}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{}"); }); test("fails when required content doesn't match the text", () => { const block = new Block( "typed", new Literal("open", "{"), new Regex("digits", "[0-9]+"), new Literal("close", "}") ); const result = block.exec("{abc}"); expect(result.ast).toBeNull(); }); test("fails when content only partially fills the block", () => { const block = new Block( "typed", new Literal("open", "{"), new Regex("alpha", "[a-z]+"), new Literal("close", "}") ); const result = block.exec("{abc 123}"); expect(result.ast).toBeNull(); }); }); describe("used with cursor directly", () => { test("parse advances cursor correctly", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const cursor = new Cursor("{} rest"); const node = block.parse(cursor); expect(node).not.toBeNull(); expect(node!.value).toBe("{}"); expect(cursor.index).toBe(1); }); test("parse restores cursor on failure", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const cursor = new Cursor("hello"); const startIndex = cursor.index; block.parse(cursor); expect(cursor.index).toBe(startIndex); }); }); describe("clone", () => { test("clone creates independent copy", () => { const block = new Block( "braces", new Literal("open", "{"), new Regex("content", "[^{}]+"), new Literal("close", "}") ); const cloned = block.clone("braces-copy") as Block; expect(cloned.name).toBe("braces-copy"); expect(cloned.type).toBe("block"); expect(cloned.id).toBe(block.id); }); }); describe("metadata", () => { test("has correct type", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); expect(block.type).toBe("block"); }); test("has correct name", () => { const block = new Block( "my-block", new Literal("open", "{"), null, new Literal("close", "}") ); expect(block.name).toBe("my-block"); }); test("children includes open, content, and close patterns", () => { const content = new Regex("content", "[^{}]+"); const block = new Block( "braces", new Literal("open", "{"), content, new Literal("close", "}") ); expect(block.children.length).toBe(3); }); test("children without content has open and close only", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); expect(block.children.length).toBe(2); }); test("getTokens returns open pattern tokens", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); expect(block.getTokens()).toEqual(["{"]); }); test("test with other pattern", () => { const anyChar = new Regex("content", "[^{}]*"); const block = new Block( "braces", new Literal("open", "{"), anyChar, new Literal("close", "}") ); const {ast} = block.exec("{text}"); expect(ast?.value).toBe("{text}"); }); test("test with sequence content", () => { const anyChar = new Regex("content", "[^{}}]+"); const refBlock = new Reference("braces"); const block = new Block( "braces", new Literal("open", "{"), new Sequence("content", [anyChar, new Optional("ref", refBlock)]), new Literal("close", "}") ); const {ast} = block.exec("{text{inner}}"); expect(ast?.value).toBe("{text{inner}}"); }); }); describe("recursive self-referencing block", () => { test("empty block as base case", () => { const ref = new Reference("block"); const block = new Block( "block", new Literal("open", "{"), ref, new Literal("close", "}") ); const result = block.exec("{}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{}"); expect(result.ast!.children.length).toBe(2); }); test("one level of nesting", () => { const ref = new Reference("block"); const block = new Block( "block", new Literal("open", "{"), ref, new Literal("close", "}") ); const result = block.exec("{{}}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{{}}"); expect(result.ast!.children.length).toBe(3); expect(result.ast!.children[0].value).toBe("{"); expect(result.ast!.children[1].value).toBe("{}"); expect(result.ast!.children[2].value).toBe("}"); }); test("two levels of nesting", () => { const ref = new Reference("block"); const block = new Block( "block", new Literal("open", "{"), ref, new Literal("close", "}") ); const result = block.exec("{{{}}}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{{{}}}"); const inner = result.ast!.children[1]; expect(inner.value).toBe("{{}}"); expect(inner.children[1].value).toBe("{}"); }); }); describe("boolean test()", () => { test("returns true for matching block", () => { const block = new Block( "braces", new Literal("open", "{"), new Regex("content", "[^{}]+"), new Literal("close", "}") ); expect(block.test("{hello}")).toBe(true); }); test("returns false for non-matching text", () => { const block = new Block( "braces", new Literal("open", "{"), new Regex("content", "[^{}]+"), new Literal("close", "}") ); expect(block.test("no braces here")).toBe(false); }); }); describe("autocomplete introspection", () => { test("suggests content tokens after open delimiter", () => { const block = new Block( "config", new Literal("open", "{"), new Literal("keyword", "key"), new Literal("close", "}") ); const open = block.children[0]; const tokens = block.getTokensAfter(open); expect(tokens).toEqual(["key"]); }); test("suggests close token after content", () => { const block = new Block( "config", new Literal("open", "{"), new Literal("keyword", "key"), new Literal("close", "}") ); const content = block.children[1]; const tokens = block.getTokensAfter(content); expect(tokens).toEqual(["}"]); }); test("suggests both optional content and close after open", () => { const block = new Block( "maybe", new Literal("open", "{"), new Optional("maybe-digits", new Regex("digits", "[0-9]+")), new Literal("close", "}") ); const open = block.children[0]; const patterns = block.getPatternsAfter(open); expect(patterns.length).toBe(2); expect(patterns[0].type).toBe("optional"); expect(patterns[1].name).toBe("close"); }); test("after close delimiter delegates to parent sequence", () => { const block = new Block( "braces", new Literal("open", "{"), new Regex("content", "[^{}]+"), new Literal("close", "}") ); const semi = new Literal("semi", ";"); const seq = new Sequence("statement", [block, semi]); const blockInSeq = seq.children[0]; const close = blockInSeq.children[blockInSeq.children.length - 1]; const patterns = blockInSeq.getPatternsAfter(close); expect(patterns.length).toBe(1); expect(patterns[0].name).toBe("semi"); }); test("block in sequence knows what comes next", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const semi = new Literal("semi", ";"); const seq = new Sequence("statement", [block, semi]); const blockInSeq = seq.children[0]; expect(blockInSeq.getNextTokens()).toEqual([";"]); expect(blockInSeq.getNextPatterns().length).toBe(1); expect(blockInSeq.getNextPatterns()[0].name).toBe("semi"); }); test("standalone block has no next tokens or patterns", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); expect(block.getNextTokens()).toEqual([]); expect(block.getNextPatterns()).toEqual([]); }); test("getPatterns returns the open delimiter patterns", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const patterns = block.getPatterns(); expect(patterns.length).toBe(1); expect(patterns[0].name).toBe("open"); }); test("returns empty for unknown child reference", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const stranger = new Literal("stranger", "x"); expect(block.getPatternsAfter(stranger)).toEqual([]); }); test("find locates a child pattern within the block", () => { const block = new Block( "braces", new Literal("open", "{"), new Regex("content", "[^{}]+"), new Literal("close", "}") ); const found = block.find(p => p.name === "close"); expect(found).not.toBeNull(); expect(found!.type).toBe("literal"); }); }); describe("equality", () => { test("blocks with identical structure are equal", () => { const a = new Block("braces", new Literal("open", "{"), new Regex("content", "[^{}]+"), new Literal("close", "}")); const b = new Block("braces", new Literal("open", "{"), new Regex("content", "[^{}]+"), new Literal("close", "}")); expect(a.isEqual(b)).toBe(true); }); test("blocks with different delimiters are not equal", () => { const braces = new Block("braces", new Literal("open", "{"), null, new Literal("close", "}")); const parens = new Block("parens", new Literal("open", "("), null, new Literal("close", ")")); expect(braces.isEqual(parens)).toBe(false); }); }); describe("unicode and emoji", () => { test("wildcard block with emoji content", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const result = block.exec("{🔴🟢🔵}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{🔴🟢🔵}"); }); test("block with emoji delimiters", () => { const block = new Block( "stars", new Literal("open", "🌟"), new Regex("content", "[^🌟]+"), new Literal("close", "🌟") ); const result = block.exec("🌟hello🌟"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("🌟hello🌟"); }); test("block with multi-char emoji delimiters", () => { const block = new Block( "arrows", new Literal("open", "➡️"), null, new Literal("close", "⬅️") ); const result = block.exec("➡️content here⬅️"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("➡️content here⬅️"); }); test("nested blocks with emoji content between", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const cursor = new Cursor("{🔴{🟢}🔵}"); const node = block.parse(cursor); expect(node).not.toBeNull(); expect(node!.value).toBe("{🔴{🟢}🔵}"); }); test("block with combining characters in content", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); // e + combining acute = é const result = block.exec("{cafe\u0301}"); expect(result.ast).not.toBeNull(); expect(result.ast!.value).toBe("{cafe\u0301}"); }); test("cursor position correct after emoji block", () => { const block = new Block( "braces", new Literal("open", "{"), null, new Literal("close", "}") ); const cursor = new Cursor("{🔴} rest"); const node = block.parse(cursor); expect(node).not.toBeNull(); expect(node!.value).toBe("{🔴}"); // Cursor should be on the close delimiter expect(cursor.currentChar).toBe("}"); }); test("check text with more than one as open and close pattern", () => { const block = new Block( "braces", new Literal("open", "<script"), null, new Literal("close", "</script>") ); const {ast, cursor} = block.exec("<script>Hello</script>"); expect(ast).not.toBeNull(); expect(ast!.value).toBe("<script>Hello</script>"); // Cursor should be on the close delimiter expect(cursor.currentChar).toBe(">"); }); }); });