clarity-pattern-parser
Version:
Parsing Library for Typescript and Javascript.
1,285 lines (1,044 loc) • 43.6 kB
text/typescript
import { Sequence } from "../patterns/Sequence";
import { Literal } from "../patterns/Literal";
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 { AutoComplete, AutoCompleteOptions } from "./AutoComplete";
import { Optional } from "../patterns/Optional";
import { SuggestionOption } from "./SuggestionOption";
interface ExpectedOption {
text: string;
startIndex: number;
subElements: ExpectedSubElement[];
}
interface ExpectedSubElement {
text: string;
// maps to the name of the pattern
pattern: string;
}
function generateFlagFromList(flagNames: string[]) {
return flagNames.map(flagName => {
return new Literal('flag-name', flagName);
});
}
function generateFlagPattern(flagNames: string[]): Pattern {
const singleFlagOption = flagNames.length === 1 && flagNames[0];
if (singleFlagOption) {
return new Literal('flag-name', singleFlagOption);
}
const flagPattern = new Options('flags', generateFlagFromList(flagNames));
return flagPattern;
}
function optionsMatchExpected(resultOptions: SuggestionOption[], expectedOptions: ExpectedOption[]) {
const expectedOptionsPatternNames = expectedOptions.map(e => e.subElements.map(s => s.pattern));
const resultOptionsPatternNames = resultOptions.map(r => r.suggestionSequence.map(s => s.pattern.name));
expect(resultOptions.length).toBe(expectedOptions.length);
resultOptions.forEach((resultOption, index) => {
expect(resultOption.text).toBe(expectedOptions[index].text);
expect(resultOption.startIndex).toBe(expectedOptions[index].startIndex);
expect(resultOption.suggestionSequence.length).toBe(expectedOptions[index].subElements.length);
expect(expectedOptionsPatternNames).toEqual(resultOptionsPatternNames);
});
}
export function generateExpression(flagNames: string[]): Repeat {
if (flagNames.length === 0) {
// regex is purposefully impossible to satisfy
const noValidOptionsRegex = new Regex('[No Valid Options Exist]', '(?=a)^(?!a)');
// returning a "Repeat" so as to not break current implementations relying on a Repeat return
const invalidInputExpression = new Repeat(
'impossible_expression',
noValidOptionsRegex
);
return invalidInputExpression;
}
const openParen = new Literal('open-paren', '(');
const closeParen = new Literal('close-paren', ')');
const space = new Regex('[space]', '\\s');
const and = new Literal('sequence-literal', 'AND');
const or = new Literal('options-literal', 'OR');
const not = new Literal('not', 'NOT ');
const booleanOperator = new Options('booleanOperator', [and, or]);
const operatorWithSpaces = new Sequence('operator-with-spaces', [
space,
booleanOperator,
space,
]);
const flag = generateFlagPattern(flagNames);
const group = new Sequence('group', [
openParen,
new Reference('flag-expression'),
closeParen,
]);
const flagOptionsGroup = new Options('flag-or-group', [flag, group]);
const expressionBody = new Sequence('flag-body', [
new Optional("optional-not", not),
flagOptionsGroup,
]);
const flagExpression = new Repeat(
'flag-expression',
expressionBody,
{ divider: operatorWithSpaces, trimDivider: true }
);
return flagExpression;
}
describe("AutoComplete", () => {
test("No Text", () => {
const name = new Literal("name", "Name");
const autoComplete = new AutoComplete(name);
let result = autoComplete.suggestFor("");
expect(result.options[0].text).toBe("Name");
expect(result.options[0].startIndex).toBe(0);
expect(result.errorAtIndex).toBe(0);
expect(result.isComplete).toBeFalsy();
});
test("Full Pattern Match Simple", () => {
const john = new Literal("john", "John");
const space = new Literal("space", " ");
const doe = new Literal("doe", "Doe");
const smith = new Literal("smith", "Smith");
const name = new Sequence("name", [john, space, new Options("last-name", [smith, doe])]);
const autoComplete = new AutoComplete(name);
const result = autoComplete.suggestFor("John Doe");
expect(result.ast?.value).toBe("John Doe");
expect(result.options.length).toBe(0);
expect(result.errorAtIndex).toBeNull();
expect(result.isComplete).toBeTruthy();
expect(result.cursor).not.toBeNull();
});
test("More Than One Option", () => {
const john = new Literal("john", "John");
const space = new Literal("space", " ");
const doe = new Literal("doe", "Doe");
const smith = new Literal("smith", "Smith");
const lastNameOptions = new Options("last-name", [smith, doe]);
const name = new Sequence("name", [john, space, lastNameOptions]);
const autoComplete = new AutoComplete(name);
const text = "John "
const result = autoComplete.suggestFor(text);
const expectedOptions = [
{
text: "Doe",
startIndex: 5,
subElements: [{
text: "Doe",
pattern: 'doe'
}]
},
{
text: "Smith",
startIndex: 5,
subElements: [{
text: "Smith",
pattern: smith.name
}]
}
];
expect(result.ast).toBeNull();
optionsMatchExpected(result.options, expectedOptions);
expect(result.errorAtIndex).toBe(text.length);
expect(result.isComplete).toBeFalsy();
expect(result.cursor).not.toBeNull();
});
test("Option should error at furthest match index", () => {
const john = new Literal("john", "John");
const space = new Literal("space", " ");
const doe = new Literal("doe", "Doe");
const smith = new Literal("smith", "Smith");
const lastNameOptions = new Options("last-name", [smith, doe]);
const name = new Sequence("name", [john, space, lastNameOptions]);
const text = "John Smi"
const autoComplete = new AutoComplete(name);
const result = autoComplete.suggestFor(text);
const expectedOptions = [{
text: "th",
startIndex: 8,
subElements: [{
text: "Smith",
pattern: smith.name
}]
}];
expect(result.ast).toBeNull();
optionsMatchExpected(result.options, expectedOptions);
expect(result.errorAtIndex).toBe(text.length - 1);
expect(result.isComplete).toBeFalsy();
expect(result.cursor).not.toBeNull();
});
test("Root Regex Pattern suggests customTokens", () => {
const freeTextPattern = new Regex(
`free-text`,
'[(\\w)\\s]+'
);
const customTokensMap:Record<string, string[]> = {
'free-text': ['luke',"leia skywalker",'luke skywalker']
}
const autoComplete = new AutoComplete(freeTextPattern,{
customTokens:customTokensMap
});
const result = autoComplete.suggestFor("luke");
const expectedOptions = [{
text: " skywalker",
startIndex: 4,
subElements: [{
text: " skywalker",
pattern: freeTextPattern.name
}]
}];
expect(result.ast?.value).toBe("luke");
optionsMatchExpected(result.options, expectedOptions);
expect(result.errorAtIndex).toBeNull()
expect(result.isComplete).toBeTruthy();
expect(result.cursor).not.toBeNull();
});
test("Sequence Regex Pattern suggests customTokens", () => {
const jediLiteral = new Literal("jedi", "jedi ");
const freeTextPattern = new Regex(
`free-text`,
'[(\\w)\\s]+'
);
const sequence = new Sequence('sequence', [jediLiteral,freeTextPattern])
const customTokensMap:Record<string, string[]> = {
'free-text': ['luke',"leia skywalker",'luke skywalker']
}
const autoComplete = new AutoComplete(sequence,{
customTokens:customTokensMap
});
const result = autoComplete.suggestFor("jedi luke sky");
const expected = [
{ text: "walker", startIndex: 13, subElements: [{
text: "walker",
startIndex: 13,
pattern: freeTextPattern.name
}] },
];
expect(result.ast?.value).toBe("jedi luke sky");
optionsMatchExpected(result.options, expected);
expect(result.errorAtIndex).toBeNull()
expect(result.isComplete).toBeTruthy();
expect(result.cursor).not.toBeNull();
});
test("Full Pattern Match With Root Repeat", () => {
const john = new Literal("john", "John");
const space = new Literal("space", " ");
const doe = new Literal("doe", "Doe");
const smith = new Literal("smith", "Smith");
const name = new Sequence("name", [john, space, new Options("last-name", [smith, doe])]);
const divider = new Regex("divider", "\\s+,\\s+");
divider.setTokens([", "])
const text = "John Doe";
const repeat = new Repeat("last-names", name, { divider });
const autoComplete = new AutoComplete(repeat);
const result = autoComplete.suggestFor(text);
const expectedOptions = [{
text: ", ",
startIndex: 8,
subElements: [{
text: ", ",
pattern: 'divider'
}]
}];
expect(result.ast?.value).toBe(text);
optionsMatchExpected(result.options, expectedOptions);
expect(result.errorAtIndex).toBeNull()
expect(result.isComplete).toBeTruthy();
expect(result.cursor).not.toBeNull();
});
test("Partial Simple", () => {
const name = new Literal("name", "Name");
const autoComplete = new AutoComplete(name);
// Use deprecated suggest for code coverage.
const result = autoComplete.suggestFor("Na");
const expectedOptions = [{
text: "me",
startIndex: 2,
subElements: [{
text: "me",
pattern: 'name'
}]
}];
expect(result.ast).toBeNull();
optionsMatchExpected(result.options, expectedOptions);
expect(result.errorAtIndex).toBe(1);
expect(result.isComplete).toBeFalsy();
expect(result.cursor).not.toBeNull();
});
test("Partial Match With Bad Characters", () => {
const name = new Literal("name", "Name");
const autoComplete = new AutoComplete(name);
const result = autoComplete.suggestFor("Ni");
const expectedOptions = [{
text: "ame",
startIndex: 1,
subElements: [{
text: "ame",
pattern: 'name'
}]
}];
expect(result.ast).toBeNull();
optionsMatchExpected(result.options, expectedOptions);
expect(result.errorAtIndex).toBe(1);
expect(result.isComplete).toBeFalsy();
expect(result.cursor).not.toBeNull();
});
test("Complete", () => {
const name = new Literal("name", "Name");
const autoComplete = new AutoComplete(name);
const text = "Name"
const result = autoComplete.suggestFor(text);
expect(result.ast?.value).toBe(text);
optionsMatchExpected(result.options, []);
expect(result.errorAtIndex).toBeNull();
expect(result.isComplete).toBeTruthy();
expect(result.cursor).not.toBeNull();
});
test("Options AutoComplete on Composing Pattern", () => {
const autoCompleteOptions: AutoCompleteOptions = {
greedyPatternNames: ["space"],
customTokens: {
"last-name": ["Sparrow"]
}
};
const jack = new Literal("jack", "Jack");
const john = new Literal("john", "John");
const space = new Literal("space", " ");
const doe = new Literal("doe", "Doe");
const smith = new Literal("smith", "Smith");
const firstName = new Options("first-name", [jack, john]);
const lastName = new Options("last-name", [doe, smith]);
const fullName = new Sequence("full-name", [firstName, space, lastName]);
const text = "Jack";
const autoComplete = new AutoComplete(fullName, autoCompleteOptions);
const result = autoComplete.suggestFor(text);
const expectedOptions:ExpectedOption[] = [
{ text: " Doe", startIndex: 4, subElements: [{
text: " ",
pattern: space.name
},{
text: "Doe",
pattern: doe.name
}] },
{ text: " Smith", startIndex: 4, subElements: [{
text: " ",
pattern: space.name
},{
text: "Smith",
pattern: smith.name
}] },
{ text: " Sparrow", startIndex: 4, subElements: [{
text: " ",
pattern: space.name
},{
text: "Sparrow",
pattern: "last-name"
}] },
];
expect(result.ast).toBeNull();
expect(result.errorAtIndex).toBe(4);
optionsMatchExpected(result.options, expectedOptions);
});
test("Options AutoComplete on Root Pattern", () => {
const jack = new Literal("first-name", "Jack");
const john = new Literal("first-name", "John");
const names = new Options('names', [jack,john]);
const divider = new Literal('divider', ', ');
const repeat = new Repeat('name-list', names, { divider, trimDivider: true });
const autoCompleteOptions: AutoCompleteOptions = {
customTokens: {
'first-name': ["James"]
},
disableDedupe: true
};
const autoComplete = new AutoComplete(repeat,autoCompleteOptions);
const text = ''
const results = autoComplete.suggestFor(text)
const expectedOptions:ExpectedOption[] = [
{ text: "Jack", startIndex: 0, subElements: [{
text: "Jack",
pattern: jack.name
}] },
{ text: "James", startIndex: 0, subElements: [{
text: "James",
pattern: 'first-name'
}] },
{ text: "James", startIndex: 0, subElements: [{
text: "James",
pattern: 'first-name'
}] },
{ text: "John", startIndex: 0, subElements: [{
text: "John",
pattern: john.name
}] },
];
optionsMatchExpected(results.options, expectedOptions);
// because autoCompleteOptions specifies "last-name" which is shared by two literals, we get two suggestions each mapping to a respective pattern of that name
expect(results.options[0].suggestionSequence[0].pattern.id).toBe(jack.id);
expect(results.options[2].suggestionSequence[0].pattern.id).toBe(john.id);
})
test("Options AutoComplete On Leaf Pattern", () => {
const autoCompleteOptions: AutoCompleteOptions = {
greedyPatternNames: ["space"],
customTokens: {
"space": [" "]
}
};
const jack = new Literal("jack", "Jack");
const john = new Literal("john", "John");
const space = new Literal("space", " ");
const doe = new Literal("doe", "Doe");
const smith = new Literal("smith", "Smith");
const firstName = new Options("first-name", [jack, john]);
const lastName = new Options("last-name", [doe, smith]);
const fullName = new Sequence("full-name", [firstName, space, lastName]);
const text = "Jack";
const autoComplete = new AutoComplete(fullName, autoCompleteOptions);
const results = autoComplete.suggestFor(text);
const expectedOptions:ExpectedOption[] = [
{ text: " Doe", startIndex: 4, subElements: [{
text: " ",
pattern: space.name
},{
text: "Doe",
pattern: doe.name
}] },
{ text: " Smith", startIndex: 4, subElements: [{
text: " ",
pattern: space.name
},{
text: "Smith",
pattern: smith.name
}] },
{ text: " Doe", startIndex: 4, subElements: [{
text: " ",
pattern: space.name
},{
text: "Doe",
pattern: doe.name
}] },
{ text: " Smith", startIndex: 4, subElements: [{
text: " ",
pattern: space.name
},{
text: "Smith",
pattern: smith.name
}] },
];
expect(results.ast).toBeNull();
expect(results.errorAtIndex).toBe(4);
optionsMatchExpected(results.options, expectedOptions);
});
test("Match On Different Pattern Roots", () => {
const start = new Literal("start", "John went to");
const a = new Literal("a", "a bank.");
const the = new Literal("the", "the store.");
const first = new Sequence("first", [start, a]);
const second = new Sequence("second", [start, the]);
const both = new Options("both", [first, second]);
const autoComplete = new AutoComplete(both);
const text = "John went to a gas station.";
const result = autoComplete.suggestFor(text);
const expected:ExpectedOption[] = [
{ text: "the store.", startIndex: 12, subElements: [{
text: "the",
pattern: the.name
},] },
{ text: "a bank.", startIndex: 12, subElements: [{
text: "a",
pattern: a.name
}] }
];
optionsMatchExpected(result.options, expected);
});
test("Options on errors because of string ending, with match", () => {
const large = new Literal("large", "kahnnnnnn");
const medium = new Literal("medium", "kahnnnnn");
const small = new Literal("small", "kahn");
const smalls = new Options("kahns", [
large,
medium,
small,
]);
const autoComplete = new AutoComplete(smalls);
const result = autoComplete.suggestFor("kahn");
const expected:ExpectedOption[] = [
{ text: "nnnnn", startIndex: 4, subElements: [{
text: "nnnnn",
pattern: large.name
}] },
{ text: "nnnn", startIndex: 4, subElements: [{
text: "nnnn",
pattern: medium.name
}] }
];
optionsMatchExpected(result.options, expected);
expect(result.isComplete).toBeTruthy();
});
test("Options on errors because of string ending, between matches", () => {
const large = new Literal("large", "kahnnnnnn");
const medium = new Literal("medium", "kahnnnnn");
const small = new Literal("small", "kahn");
const smalls = new Options("kahns", [
large,
medium,
small,
]);
const autoComplete = new AutoComplete(smalls);
const result = autoComplete.suggestFor("kahnn");
const expected:ExpectedOption[] = [
{ text: "nnnn", startIndex: 5, subElements: [{
text: "nnnn",
pattern: large.name
}] },
{ text: "nnn", startIndex: 5, subElements: [{
text: "nnn",
pattern: medium.name
}] }
];
optionsMatchExpected(result.options, expected);
expect(result.isComplete).toBeFalsy();
});
test("Options on errors because of string ending, match middle", () => {
const large = new Literal("large", "kahnnnnnn");
const medium = new Literal("medium", "kahnnnnn");
const small = new Literal("small", "kahn");
const smalls = new Options("kahns", [
large,
medium,
small,
]);
const autoComplete = new AutoComplete(smalls);
const result = autoComplete.suggestFor("kahnnnnn");
const expected:ExpectedOption[] = [
{ text: "n", startIndex: 8, subElements: [{
text: "n",
pattern: large.name
}] },
];
optionsMatchExpected(result.options, expected);
expect(result.isComplete).toBeTruthy();
});
test("Options on errors because of string ending on a variety, with match", () => {
const different3 = new Literal("different-3", "kahnnnnnnn3");
const different21 = new Literal("different-21", "kahnnnnnn21");
const different22 = new Literal("different-22", "kahnnnnnn22");
const different2 = new Literal("different-2", "kahnnnnnn2");
const different1 = new Literal("different", "kahnnnnnn1");
const large = new Literal("large", "kahnnnnnn");
const small = new Literal("small", "kahn");
const medium = new Literal("medium", "kahnnnnn");
const smalls = new Options("kahns", [
different3,
different21,
different22,
different2,
different1,
large,
medium,
small,
]);
const autoComplete = new AutoComplete(smalls);
const result = autoComplete.suggestFor("kahnnnnn");
const expected:ExpectedOption[] = [
{ text: "nn3", startIndex: 8, subElements: [{
text: "nn3",
pattern: different3.name
}] },
{ text: "n21", startIndex: 8, subElements: [{
text: "n21",
pattern: different21.name
}] },
{ text: "n22", startIndex: 8 , subElements: [{
text: "n22",
pattern: different22.name
}] },
{ text: "n2", startIndex: 8, subElements: [{
text: "n2",
pattern: different2.name
}] },
{ text: "n1", startIndex: 8, subElements: [{
text: "n1",
pattern: different1.name
}] },
{ text: "n", startIndex: 8, subElements: [{
text: "n",
pattern: large.name
}] },
];
optionsMatchExpected(result.options, expected);
expect(result.isComplete).toBeTruthy();
});
test("Options on errors because of string ending on different branches, with match", () => {
const smalls = new Options("kahns", [
new Sequence("kahn-combo-3", [new Literal("kah", "kah"), new Sequence('partial', [new Literal("n", "n"), new Literal("three", "3")])]),
new Sequence("kahn-combo", [new Literal("kahn", "kahn"), new Literal("one", "1")]),
new Sequence("kahn-combo-2", [new Literal("kahn", "kahn"), new Literal("two", "2")]),
new Literal("small", "kahn"),
]);
const autoComplete = new AutoComplete(smalls);
const result = autoComplete.suggestFor("kahn");
const expected:ExpectedOption[] = [
{ text: "2", startIndex: 4, subElements: [{
text: "2",
pattern: "two"
}] },
{ text: "1", startIndex: 4, subElements: [{
text: "1",
pattern: "one"
}] },
{ text: "3", startIndex: 4, subElements: [{
text: "3",
pattern: "three"
}] },
];
optionsMatchExpected(result.options, expected);
expect(result.isComplete).toBeTruthy();
});
test("Remove Trailing Divider", () => {
const repeat = new Repeat("repeat", new Literal("a", "a"), { divider: new Literal("pipe", "|"), trimDivider: true });
const autoComplete = new AutoComplete(repeat);
const result = autoComplete.suggestFor("a|a|");
const expected:ExpectedOption[] = [
{ text: "a", startIndex: 4, subElements: [{
text: "a",
pattern: "a"
}] },
];
optionsMatchExpected(result.options, expected);
});
test("Remove options divider", () => {
const jediLuke = new Literal(`jedi`, 'luke');
const names = new Options('names', [jediLuke]);
const literalA = new Literal('literal-a', 'a');
const literalB = new Literal('literal-b', 'b');
const optionsDivider = new Options('options-divider', [literalA, literalB]);
// control to prove the pattern works without trimDivider
const controlPattern = new Repeat('name-list', names, { divider: optionsDivider });
const controlAutoComplete = new AutoComplete(controlPattern);
const controlResult = controlAutoComplete.suggestFor('lukea');
expect(controlResult.isComplete).toEqual(true);
const trimPattern = new Repeat('name-list', names, { divider: optionsDivider, trimDivider: true });
const trimAutoComplete = new AutoComplete(trimPattern);
const trimResult = trimAutoComplete.suggestFor('lukea');
expect(trimResult.isComplete).toEqual(false);
})
test("Expect Divider", () => {
const repeat = new Repeat("repeat", new Literal("a", "a"), { divider: new Literal("pipe", "|") });
const autoComplete = new AutoComplete(repeat);
const result = autoComplete.suggestFor("a|a");
const expected:ExpectedOption[] = [
{ text: '|', startIndex: 3, subElements: [{
text: '|',
pattern: 'pipe'
}] }
];
optionsMatchExpected(result.options, expected);
});
test("Repeat with bad second repeat", () => {
const repeat = new Repeat("repeat", new Literal("a", "a"), { divider: new Literal("pipe", "|"), trimDivider: true });
const autoComplete = new AutoComplete(repeat);
const result = autoComplete.suggestFor("a|b");
const expected:ExpectedOption[] = [
{ text: "a", startIndex: 2, subElements: [{
text: "a",
pattern: "a"
}] },
];
optionsMatchExpected(result.options, expected);
});
test("Repeat with bad trailing content", () => {
const flags = ["FlagA", "FlagB", "FlagC"];
const pattern = generateExpression(flags);
const result = new AutoComplete(pattern).suggestFor("FlagA AND FlagAlkjhgB");
expect(result.options).toEqual([]);
expect(result.ast?.value).toBe("FlagA AND FlagA");
expect(result.errorAtIndex).toBe(15);
});
test("Greedy Options", () => {
const john = new Literal("john", "John");
const doe = new Literal("doe", "Doe");
const jane = new Literal("jane", "Jane");
const smith = new Literal("smith", "Smith");
const space = new Literal("space", " ");
const firstName = new Options("first-name", [john, jane], true);
const lastName = new Options("last-name", [doe, smith], true);
const johnJohnson = new Literal("john-johnson", "John Johnson");
const johnStockton = new Literal("john-stockton", "John Stockton");
const fullName = new Sequence("full-name", [firstName, space, lastName]);
const names = new Options("names", [johnStockton, johnJohnson, fullName], true);
const autoComplete = new AutoComplete(names);
const results = autoComplete.suggestFor("John ");
const expected:ExpectedOption[] = [
{
text: "Doe",
startIndex: 5,
subElements: [{
text: "Doe",
pattern: "doe"
}]
},
{
text: "Smith",
startIndex: 5,
subElements: [{
text: "Smith",
pattern: "smith"
}]
},
{
text: "Stockton",
startIndex: 5,
subElements: [{
text: "Stockton",
pattern: "john-stockton"
}]
},
{
text: "Johnson",
startIndex: 5,
subElements: [{
text: "Johnson",
pattern: "john-johnson"
}]
},
];
optionsMatchExpected(results.options, expected);
expect(results.ast).toBeNull();
expect(results.errorAtIndex).toBe(5);
});
test("Dedup suggestions", () => {
const branchOne = new Sequence("branch-1", [new Literal("space", " "), new Literal("A", "A")]);
const branchTwo = new Sequence("branch-2", [new Literal("space", " "), new Literal("B", "B")]);
const eitherBranch = new Options("either-branch", [branchOne, branchTwo]);
const autoComplete = new AutoComplete(eitherBranch);
const results = autoComplete.suggestFor("");
const expected:ExpectedOption[] = [{
startIndex: 0,
text: " ",
subElements: [{
text: " ",
pattern: "space"
}]
}];
optionsMatchExpected(results.options, expected);
});
test("Multiple Complex Branches", () => {
const branchOne = new Sequence("branch-1", [
new Literal("space-1-1", " "),
new Literal("space-1-2", " "),
new Options('branch-1-options', [
new Literal("AA", "AA"),
new Literal("AB", "AB"),
new Literal("BC", "BC"),
])
]);
const branchTwo = new Sequence("branch-2", [
new Literal("space-2-1", " "),
new Literal("space-2-2", " "),
new Options('branch-2-options', [
new Literal("BA", "BA"),
new Literal("BB", "BB")
])
]);
const eitherBranch = new Options("either-branch", [branchOne, branchTwo]);
const autoComplete = new AutoComplete(eitherBranch);
const results = autoComplete.suggestFor(" B");
const expected:ExpectedOption[] = [
{ startIndex: 3, text: "A", subElements: [{
text: "A",
pattern: "BA"
}] },
{ startIndex: 3, text: "B", subElements: [{
text: "B",
pattern: "BB"
}] },
{ startIndex: 3, text: "C", subElements: [{
text: "C",
pattern: "BC"
}] },
];
optionsMatchExpected(results.options, expected);
});
test("Recursion With Or", () => {
const ref = new Reference("names");
const names = new Options("names", [
ref,
new Literal("john", "John"),
new Literal("jane", "Jane")
]);
const autoComplete = new AutoComplete(names);
const suggestion = autoComplete.suggestFor("Jo");
const expected:ExpectedOption[] = [
{ text: 'hn', startIndex: 2, subElements: [{
text: 'hn',
pattern: "john"
}] },
];
optionsMatchExpected(suggestion.options, expected);
expect(suggestion.error?.lastIndex).toBe(1);
});
test("Recursion With And", () => {
const firstNames = new Options("first-names", [
new Literal("john", "John"),
new Literal("jane", "Jane"),
]);
const lastNames = new Options("last-names", [
new Literal("doe", "Doe"),
new Literal("smith", "Smith"),
]);
const fullName = new Sequence("full-name", [
firstNames,
new Literal("space", " "),
lastNames
]);
const ref = new Reference("names");
const names = new Sequence("names", [
fullName,
ref,
lastNames,
]);
const autoComplete = new AutoComplete(names, {
greedyPatternNames: ["space"]
});
const suggestion = autoComplete.suggestFor("John");
const expected:ExpectedOption[] = [
{ text: " Doe", startIndex: 4, subElements: [{
text: " ",
pattern: "space"
},{
text: "Doe",
pattern: "doe"
}] },
{ text: " Smith", startIndex: 4, subElements: [{
text: " ",
pattern: "space"
},{
text: "Smith",
pattern: "smith"
}] },
];
optionsMatchExpected(suggestion.options, expected);
});
test("Repeat With Options", () => {
const john = new Literal("john", "John");
const jane = new Literal("jane", "Jane");
const names = new Options("names", [john, jane]);
const comma = new Regex("comma", "\\s*,\\s*");
comma.setTokens([", "]);
const list = new Repeat("names-list", names, {
divider: comma
});
const autoComplete = new AutoComplete(list);
const suggestion = autoComplete.suggestFor("John, ");
expect(suggestion).toBe(suggestion);
});
test("Repeat With Options With Options", () => {
const john = new Literal("john", "John");
const jane = new Literal("jane", "Jane");
const jack = new Literal("jack", "Jack");
const jill = new Literal("jill", "Jill");
const otherNames = new Options("names", [jack, jill]);
const names = new Options("names", [john, jane, otherNames]);
const comma = new Regex("comma", "\\s*,\\s*");
comma.setTokens([", "]);
const list = new Repeat("names-list", names, {
divider: comma,
trimDivider: true
});
const autoComplete = new AutoComplete(list, { greedyPatternNames: ["comma"] });
const suggestion = autoComplete.suggestFor("John");
expect(suggestion).toBe(suggestion);
});
test("Mid sequence suggestion", () => {
const operator = new Options("operator", [
new Literal("==", "=="),
new Literal("!=", "!="),
]);
const leftOperands = new Options("left-operands", [
new Literal("variable-1", "variable1"),
new Literal("variable-2", "variable2"),
]);
const rightOperands = new Options("right-operands", [
new Literal("variable-3", "variable3"),
new Literal("variable-4", "variable4"),
]);
const optionalSpace = new Optional("optional-sapce", new Regex("space", "\\s+"));
const statement = new Sequence("statement", [leftOperands, optionalSpace, operator, optionalSpace, rightOperands]);
const autoComplete = new AutoComplete(statement, { greedyPatternNames: ["space", "operator"], customTokens: { space: [" "] } }
);
const result = autoComplete.suggestFor("variable1 ==");
const expected:ExpectedOption[] = [
{ text: " variable3", startIndex: 12, subElements: [{
text: " ",
pattern: "space"
},{
text: "variable3",
pattern: "variable-3"
}] },
{ text: " variable4", startIndex: 12, subElements: [{
text: " ",
pattern: "space"
},{
text: "variable4",
pattern: "variable-4"
}] },
{ text: "variable3", startIndex: 12, subElements: [{
text: "variable3",
pattern: "variable-3"
}] },
{ text: "variable4", startIndex: 12, subElements: [{
text: "variable4",
pattern: "variable-4"
}] },
];
optionsMatchExpected(result.options, expected);
});
test("Calling AutoComplete -> _getAllOptions Does not mutate customTokensMap", () => {
const jediLuke = new Literal(`jedi`, 'luke');
const names = new Options('names', [jediLuke]);
const divider = new Literal('divider', ', ');
const pattern = new Repeat('name-list', names, { divider, trimDivider: true });
const customTokensMap:Record<string, string[]> = {
'jedi': ["leia"]
}
const copiedCustomTokensMap:Record<string, string[]> = JSON.parse(JSON.stringify(customTokensMap));
const autoCompleteOptions: AutoCompleteOptions = {
greedyPatternNames:['jedi'],
customTokens: customTokensMap
};
const autoComplete = new AutoComplete(pattern,autoCompleteOptions);
// provide a non-empty input to trigger the logic flow that hits _getAllOptions
autoComplete.suggestFor('l')
expect(customTokensMap).toEqual(copiedCustomTokensMap)
})
test("Suggests entire greedy node, with appended successive nodes", () => {
const start = new Literal("start", "start");
const separator = new Literal("separator", '==');
const aLiteral = new Literal("letter", "A");
const bLiteral = new Literal("letter", "B");
const cLiteral = new Literal("letter", "C");
const abcOptions = new Options("letters-options", [aLiteral, bLiteral, cLiteral]);
const fullSequence = new Sequence("full-sequence", [start, separator, abcOptions]);
const autoCompleteOptions: AutoCompleteOptions = {
greedyPatternNames: ["separator"],
};
const autoComplete = new AutoComplete(fullSequence, autoCompleteOptions);
const text = "start";
const results = autoComplete.suggestFor(text);
const expectedOptions:ExpectedOption[] = [
{ text: "==A", startIndex: 5, subElements: [{
text: "==",
pattern: separator.name
},{
text: "A",
pattern: aLiteral.name
}] },
{ text: "==B", startIndex: 5, subElements: [{
text: "==",
pattern: separator.name
},{
text: "B",
pattern: bLiteral.name
}] },
{ text: "==C", startIndex: 5, subElements: [{
text: "==",
pattern: separator.name
},{
text: "C",
pattern: cLiteral.name
}] },
];
optionsMatchExpected(results.options, expectedOptions);
});
test("incomplete greedy text, suggests node completion with appended successive nodes", () => {
const start = new Literal("start", "start");
const separator = new Literal("separator", '==');
const aLiteral = new Literal("letter", "A");
const bLiteral = new Literal("letter", "B");
const cLiteral = new Literal("letter", "C");
const abcOptions = new Options("letters-options", [aLiteral, bLiteral, cLiteral]);
const fullSequence = new Sequence("full-sequence", [start, separator, abcOptions]);
const autoCompleteOptions: AutoCompleteOptions = {
greedyPatternNames: ["separator"],
};
const autoComplete = new AutoComplete(fullSequence, autoCompleteOptions);
const text = "start=";
const results = autoComplete.suggestFor(text);
const expectedOptions:ExpectedOption[] = [
{ text: "=A", startIndex: 6, subElements: [{
text: "=",
pattern: separator.name
},{
text: "A",
pattern: aLiteral.name
}] },
{ text: "=B", startIndex: 6, subElements: [{
text: "=",
pattern: separator.name
},{
text: "B",
pattern: bLiteral.name
}] },
{ text: "=C", startIndex: 6, subElements: [{
text: "=",
pattern: separator.name
},{
text: "C",
pattern: cLiteral.name
}] },
];
optionsMatchExpected(results.options, expectedOptions);
});
test("greedy root node", () => {
const separator = new Literal("separator", '==');
const aLiteral = new Literal("letter", "A");
const bLiteral = new Literal("letter", "B");
const cLiteral = new Literal("letter", "C");
const abcOptions = new Options("letters-options", [aLiteral, bLiteral, cLiteral]);
const fullSequence = new Sequence("full-sequence", [separator, abcOptions]);
const autoCompleteOptions: AutoCompleteOptions = {
greedyPatternNames: ["separator"],
};
const autoComplete = new AutoComplete(fullSequence, autoCompleteOptions);
const text = "";
const results = autoComplete.suggestFor(text);
const expectedOptions:ExpectedOption[] = [
{ text: "==A", startIndex: 0, subElements: [{
text: "==",
pattern: separator.name
},{
text: "A",
pattern: aLiteral.name
}] },
{ text: "==B", startIndex: 0, subElements: [{
text: "==",
pattern: separator.name
},{
text: "B",
pattern: bLiteral.name
}] },
{ text: "==C", startIndex: 0, subElements: [{
text: "==",
pattern: separator.name
},{
text: "C",
pattern: cLiteral.name
}] },
];
optionsMatchExpected(results.options, expectedOptions);
});
test('options return farthest match when encountering options with shared initial literals', () => {
const startBracket = new Literal('shared-starting-literal', '{');
const abcLiteral = new Literal('abc', 'abc');
const xyzLiteral = new Literal('xyz', 'xyz');
const sequenceOne = new Sequence('sequence-one', [startBracket, abcLiteral]);
const sequenceTwo = new Sequence('sequence-two', [startBracket, xyzLiteral]);
// create options with sequences that have matching initial literals
const options = new Options('options', [sequenceOne, sequenceTwo]);
const autoComplete = new AutoComplete(options);
const results = autoComplete.suggestFor('{a');
const expectedOptions: ExpectedOption[] = [
{ text: 'bc', startIndex: 2, subElements: [{ text: 'abc', pattern: abcLiteral.name }] },
];
optionsMatchExpected(results.options, expectedOptions);
});
});