clarity-pattern-parser 8.4.9 → 8.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -271,9 +271,9 @@ describe("Grammar", () => {
271
271
  test("Bad Grammar At Beginning", () => {
272
272
 
273
273
  expect(() => {
274
- const expression = `Just Junk`;
274
+ const expression = `//`;
275
275
  Grammar.parseString(expression);
276
- }).toThrowError("[Parse Error] Found: 'Just Junk', expected: ' ='.");
276
+ }).toThrow();
277
277
 
278
278
  });
279
279
 
@@ -296,9 +296,9 @@ describe("Grammar", () => {
296
296
  space = " "
297
297
  full-name = first-name & space & last-name
298
298
  `
299
- function resolveImport(path: string) {
300
- expect(path).toBe("some/path/to/file.cpat");
301
- return Promise.resolve({ expression: importExpression, path });
299
+ function resolveImport(resource: string) {
300
+ expect(resource).toBe("some/path/to/file.cpat");
301
+ return Promise.resolve({ expression: importExpression, resource });
302
302
  }
303
303
 
304
304
  const patterns = await Grammar.parse(expression, { resolveImport });
@@ -323,8 +323,8 @@ describe("Grammar", () => {
323
323
  last-name = "Doe"
324
324
  full-name = first-name & space & last-name
325
325
  `
326
- function resolveImport(path: string) {
327
- return Promise.resolve({ expression: pathMap[path], path });
326
+ function resolveImport(resource: string) {
327
+ return Promise.resolve({ expression: pathMap[resource], resource });
328
328
  }
329
329
 
330
330
  const patterns = await Grammar.parse(expression, { resolveImport });
@@ -333,4 +333,93 @@ describe("Grammar", () => {
333
333
  expect(result?.ast?.value).toBe("John Doe");
334
334
  });
335
335
 
336
+ test("Imports with Params", async () => {
337
+ const importExpression = `first-name = "John"`;
338
+ const spaceExpression = `
339
+ use params { custom-space }
340
+ space = custom-space
341
+ `
342
+ const expression = `
343
+ import { first-name } from "first-name.cpat"
344
+ import { space } from "space.cpat" with params { custom-space = " " }
345
+ last-name = "Doe"
346
+ full-name = first-name & space & last-name
347
+ `
348
+
349
+ const pathMap: Record<string, string> = {
350
+ "space.cpat": spaceExpression,
351
+ "first-name.cpat": importExpression
352
+ };
353
+
354
+ function resolveImport(resource: string) {
355
+ return Promise.resolve({ expression: pathMap[resource], resource });
356
+ }
357
+
358
+ const patterns = await Grammar.parse(expression, { resolveImport });
359
+ const fullname = patterns.get("full-name") as Pattern;
360
+ const result = fullname.exec("John Doe");
361
+ expect(result?.ast?.value).toBe("John Doe");
362
+ });
363
+
364
+ test("Export Name", async () => {
365
+ const expression = `
366
+ import { use-this } from "resource1"
367
+ import {name} from "resource2" with params {
368
+ use-this
369
+ }
370
+
371
+ name
372
+ `
373
+
374
+ const resource1 = `
375
+ use-this = "Use This"
376
+ `;
377
+
378
+ const resource2 = `
379
+ use params {
380
+ use-this
381
+ }
382
+ name = use-this
383
+ `;
384
+
385
+ const pathMap: Record<string, string> = {
386
+ "resource1": resource1,
387
+ "resource2": resource2
388
+ };
389
+
390
+ function resolveImport(resource: string) {
391
+ return Promise.resolve({ expression: pathMap[resource], resource });
392
+ }
393
+ const patterns = await Grammar.parse(expression, { resolveImport });
394
+ const pattern = patterns.get("name") as Literal;
395
+
396
+ const result = pattern.exec("Use This");
397
+
398
+ expect(result.ast?.value).toBe("Use This");
399
+ });
400
+
401
+ test("Import Alias", async () => {
402
+ const expression = `
403
+ import { value as alias } from "resource1"
404
+ name = alias
405
+ `
406
+
407
+ const resource1 = `
408
+ value = "Value"
409
+ `;
410
+
411
+
412
+ const pathMap: Record<string, string> = {
413
+ "resource1": resource1,
414
+ };
415
+
416
+ function resolveImport(resource: string) {
417
+ return Promise.resolve({ expression: pathMap[resource], resource });
418
+ }
419
+ const patterns = await Grammar.parse(expression, { resolveImport });
420
+ const pattern = patterns.get("name") as Literal;
421
+
422
+ const result = pattern.exec("Value");
423
+ expect(result.ast?.value).toBe("Value");
424
+ });
336
425
  });
@@ -11,38 +11,41 @@ import { Repeat, RepeatOptions } from "../patterns/Repeat";
11
11
  import { AutoComplete } from "../intellisense/AutoComplete";
12
12
 
13
13
  class ParseContext {
14
+ constructor(params: Pattern[]) {
15
+ params.forEach(p => this.paramsByName.set(p.name, p));
16
+ }
14
17
  patternsByName = new Map<string, Pattern>();
15
18
  importedPatternsByName = new Map<string, Pattern>();
19
+ paramsByName = new Map<string, Pattern>();
16
20
  }
17
21
 
18
22
  function defaultImportResolver(_path: string, _basePath: string | null): Promise<GrammarFile> {
19
23
  throw new Error("No import resolver supplied.");
20
24
  }
21
25
 
22
- export interface GrammarMeta {
23
- originPath?: string;
24
- }
25
-
26
26
  export interface GrammarFile {
27
- path: string;
27
+ resource: string;
28
28
  expression: string;
29
29
  }
30
30
 
31
31
  export interface GrammarOptions {
32
- resolveImport?: (path: string, originPath: string | null) => Promise<GrammarFile>;
33
- meta?: GrammarMeta | null;
32
+ resolveImport?: (resource: string, originResource: string | null) => Promise<GrammarFile>;
33
+ originResource?: string | null;
34
+ params?: Pattern[];
34
35
  }
35
36
 
36
37
  export class Grammar {
37
- private _meta: GrammarMeta | null;
38
- private _resolveImport: (path: string, basePath: string | null) => Promise<GrammarFile>;
38
+ private _params: Pattern[];
39
+ private _originResource?: string | null;
40
+ private _resolveImport: (resource: string, originResource: string | null) => Promise<GrammarFile>;
39
41
  private _parseContext: ParseContext;
40
42
  private _autoComplete: AutoComplete;
41
43
 
42
44
  constructor(options: GrammarOptions = {}) {
43
- this._meta = options.meta == null ? null : options.meta;
45
+ this._params = options?.params == null ? [] : options.params;
46
+ this._originResource = options?.originResource == null ? null : options.originResource;
44
47
  this._resolveImport = options.resolveImport == null ? defaultImportResolver : options.resolveImport;
45
- this._parseContext = new ParseContext();
48
+ this._parseContext = new ParseContext(this._params);
46
49
  this._autoComplete = new AutoComplete(grammar, {
47
50
  greedyPatternNames: ["spaces", "optional-spaces", "whitespace", "new-line"],
48
51
  customTokens: {
@@ -56,13 +59,17 @@ export class Grammar {
56
59
 
57
60
  async import(path: string) {
58
61
  const grammarFile = await this._resolveImport(path, null);
59
- const grammar = new Grammar({ resolveImport: this._resolveImport, meta: { originPath: grammarFile.path } });
62
+ const grammar = new Grammar({
63
+ resolveImport: this._resolveImport,
64
+ originResource: grammarFile.resource,
65
+ params: this._params
66
+ });
60
67
 
61
68
  return grammar.parse(grammarFile.expression);
62
69
  }
63
70
 
64
71
  async parse(expression: string) {
65
- this._parseContext = new ParseContext();
72
+ this._parseContext = new ParseContext(this._params);
66
73
  const ast = this._tryToParse(expression);
67
74
 
68
75
  await this._resolveImports(ast);
@@ -72,7 +79,7 @@ export class Grammar {
72
79
  }
73
80
 
74
81
  parseString(expression: string) {
75
- this._parseContext = new ParseContext();
82
+ this._parseContext = new ParseContext(this._params);
76
83
  const ast = this._tryToParse(expression);
77
84
 
78
85
  if (this._hasImports(ast)) {
@@ -112,9 +119,15 @@ export class Grammar {
112
119
  }
113
120
 
114
121
  private _buildPatterns(ast: Node) {
115
- ast.children.forEach((n) => {
122
+ const body = ast.find(n => n.name === "body");
123
+
124
+ if (body == null) {
125
+ return;
126
+ }
127
+
128
+ body.findAll(n => n.name === "assign-statement" || n.name === "export-name").forEach((n) => {
116
129
  const typeNode = n.find(n => n.name.includes("literal"));
117
- const type = typeNode?.name || "unknown";
130
+ const type = n.name === "export-name" ? "export-name" : typeNode?.name || "unknown";
118
131
 
119
132
  switch (type) {
120
133
  case "literal": {
@@ -141,6 +154,11 @@ export class Grammar {
141
154
  this._buildAlias(n)
142
155
  break;
143
156
  }
157
+ case "export-name": {
158
+ const pattern = this._getPattern(n.value);
159
+ this._parseContext.patternsByName.set(n.value, pattern);
160
+ break;
161
+ }
144
162
  default: {
145
163
  break;
146
164
  }
@@ -150,39 +168,94 @@ export class Grammar {
150
168
 
151
169
  private async _resolveImports(ast: Node) {
152
170
  const parseContext = this._parseContext;
153
- const importStatements = ast.findAll(n => n.name === "import-statement");
171
+ const importStatements = ast.findAll(n => n.name === "import-from");
154
172
 
155
173
  for (const importStatement of importStatements) {
156
- const urlNode = importStatement.find(n => n.name === "url") as Node;
157
-
158
- const url = urlNode.value.slice(1, -1);
159
- const grammarFile = await this._resolveImport(url, this._meta?.originPath || null);
160
- const grammar = new Grammar({ resolveImport: this._resolveImport, meta: { originPath: grammarFile.path } });
174
+ const resourceNode = importStatement.find(n => n.name === "resource") as Node;
175
+ const params = this._getParams(importStatement);
176
+ const resource = resourceNode.value.slice(1, -1);
177
+ const grammarFile = await this._resolveImport(resource, this._originResource || null);
178
+ const grammar = new Grammar({
179
+ resolveImport: this._resolveImport,
180
+ originResource: grammarFile.resource,
181
+ params
182
+ });
161
183
 
162
184
  try {
163
185
  const patterns = await grammar.parse(grammarFile.expression);
164
- const importNames = importStatement.findAll(n => n.name === "import-name").map(n => n.value);
165
-
166
- importNames.forEach((importName) => {
167
- if (parseContext.importedPatternsByName.has(importName)) {
168
- throw new Error(`'${importName}' was already used within another import.`);
186
+ const importStatements = importStatement.findAll(n => n.name === "import-name" || n.name === "import-alias");
187
+
188
+ importStatements.forEach((node) => {
189
+ if (node.name === "import-name") {
190
+ const importName = node.value;
191
+
192
+ if (parseContext.importedPatternsByName.has(importName)) {
193
+ throw new Error(`'${importName}' was already used within another import.`);
194
+ }
195
+
196
+ const pattern = patterns.get(importName);
197
+ if (pattern == null) {
198
+ throw new Error(`Couldn't find pattern with name: ${importName}, from import: ${resource}.`);
199
+ }
200
+
201
+ parseContext.importedPatternsByName.set(importName, pattern);
202
+ } else {
203
+ const importNameNode = node.find(n => n.name === "import-name") as Node;
204
+ const importName = importNameNode.value;
205
+ const aliasNode = node.find(n => n.name === "import-name-alias") as Node;
206
+ const alias = aliasNode.value;
207
+
208
+ if (parseContext.importedPatternsByName.has(alias)) {
209
+ throw new Error(`'${alias}' was already used within another import.`);
210
+ }
211
+
212
+ const pattern = patterns.get(importName);
213
+ if (pattern == null) {
214
+ throw new Error(`Couldn't find pattern with name: ${importName}, from import: ${resource}.`);
215
+ }
216
+
217
+ parseContext.importedPatternsByName.set(alias, pattern);
169
218
  }
219
+ });
170
220
 
171
- const pattern = patterns.get(importName);
172
- if (pattern == null) {
173
- throw new Error(`Couldn't find pattern with name: ${importName}, from import: ${url}.`);
174
- }
175
221
 
176
- parseContext.importedPatternsByName.set(importName, pattern);
177
- })
178
222
 
179
223
  } catch (e: any) {
180
- throw new Error(`Failed loading expression from: "${url}". Error details: "${e.message}"`);
224
+ throw new Error(`Failed loading expression from: "${resource}". Error details: "${e.message}"`);
181
225
  }
182
226
 
183
227
  }
184
228
  }
185
229
 
230
+ private _getParams(importStatement: Node) {
231
+ let params: Pattern[] = [];
232
+ const paramsStatement = importStatement.find(n => n.name === "with-params-statement");
233
+
234
+ if (paramsStatement != null) {
235
+ const statements = paramsStatement.find(n => n.name === "with-params-body");
236
+
237
+ if (statements != null) {
238
+ const expression = statements.toString();
239
+ const importedValues = Array.from(this
240
+ ._parseContext
241
+ .importedPatternsByName
242
+ .values()
243
+ )
244
+
245
+ const grammar = new Grammar({
246
+ params: importedValues,
247
+ originResource: this._originResource,
248
+ resolveImport: this._resolveImport
249
+ });
250
+
251
+ const patterns = grammar.parseString(expression);
252
+ params = Array.from(patterns.values());
253
+ }
254
+ }
255
+
256
+ return params;
257
+ }
258
+
186
259
  private _buildLiteral(statementNode: Node) {
187
260
  const nameNode = statementNode.find(n => n.name === "name") as Node;
188
261
  const literalNode = statementNode.find(n => n.name === "literal") as Node;
@@ -223,6 +296,10 @@ export class Grammar {
223
296
  pattern = this._parseContext.importedPatternsByName.get(name);
224
297
  }
225
298
 
299
+ if (pattern == null) {
300
+ pattern = this._parseContext.paramsByName.get(name);
301
+ }
302
+
226
303
  if (pattern == null) {
227
304
  return new Reference(name);
228
305
  }
@@ -334,8 +411,8 @@ export class Grammar {
334
411
  return grammar.import(path);
335
412
  }
336
413
 
337
- static parseString(expression: string) {
338
- const grammar = new Grammar();
414
+ static parseString(expression: string, options?: GrammarOptions) {
415
+ const grammar = new Grammar(options);
339
416
  return grammar.parseString(expression);
340
417
  }
341
418
 
@@ -0,0 +1,19 @@
1
+ import { And } from "../../patterns/And";
2
+ import { Or } from "../../patterns/Or";
3
+ import { Repeat } from "../../patterns/Repeat";
4
+ import { comment } from "./comment";
5
+ import { lineSpaces, newLine } from "./spaces";
6
+ import { statement } from "./statement";
7
+
8
+ const bodyLineContent = new Or("body-line-content", [
9
+ comment,
10
+ statement
11
+ ]);
12
+
13
+ const bodyLine = new And("body-line", [
14
+ lineSpaces.clone("line-spaces", true),
15
+ bodyLineContent,
16
+ lineSpaces.clone("line-spaces", true),
17
+ ]);
18
+
19
+ export const body = new Repeat("body", bodyLine, {divider: newLine, min: 0});
@@ -2,21 +2,38 @@ import { Or } from "../../patterns/Or";
2
2
  import { Regex } from "../../patterns/Regex";
3
3
  import { Repeat } from "../../patterns/Repeat";
4
4
  import { comment } from "./comment";
5
- import { statement } from "./statement";
6
5
  import { importStatement } from './import';
6
+ import { And } from "../../patterns/And";
7
+ import { allSpaces } from "./spaces";
8
+ import { body } from "./body";
7
9
 
8
- const whitespace = new Regex("whitespace", "[ \\t]+((\\r?\\n)+)?");
10
+ const tabs = new Regex("tabs", "\\t+");
11
+ const spaces = new Regex("spaces", "[ ]+");
9
12
  const newLine = new Regex("new-line", "(\\r?\\n)+");
10
13
 
11
- whitespace.setTokens([" "]);
14
+ spaces.setTokens([" "]);
15
+ tabs.setTokens(["\t"]);
12
16
  newLine.setTokens(["\n"]);
13
17
 
14
- const line = new Or("line", [
15
- newLine,
16
- whitespace,
18
+ const lineSpaces = new Repeat("line-spaces", new Or("line-space", [tabs, spaces]));
19
+
20
+ const headLineContent = new Or("head-line-content", [
17
21
  comment,
18
- importStatement,
19
- statement
22
+ importStatement
23
+ ]);
24
+
25
+ const headLine = new And("head-line-content", [
26
+ lineSpaces.clone("line-spaces", true),
27
+ headLineContent,
28
+ lineSpaces.clone("line-spaces", true),
20
29
  ]);
21
30
 
22
- export const grammar = new Repeat("grammer", line);
31
+ const head = new Repeat("head", headLine, { divider: newLine, min: 0 });
32
+
33
+ export const grammar = new And("grammar", [
34
+ allSpaces,
35
+ head,
36
+ allSpaces,
37
+ body,
38
+ allSpaces
39
+ ]);
@@ -3,27 +3,68 @@ import { Repeat } from "../../patterns/Repeat";
3
3
  import { Literal } from "../../patterns/Literal";
4
4
  import { Regex } from "../../patterns/Regex";
5
5
  import { literal } from "./literal";
6
+ import { Or } from "../../patterns/Or";
7
+ import { body } from "./body";
8
+ import { allSpaces, lineSpaces } from "./spaces";
9
+
10
+ const optionalSpaces = allSpaces.clone("optional-spaces", true);
11
+ const optionalLineSpaces = lineSpaces.clone("options-line-spaces", true);
6
12
 
7
- const spaces = new Regex("spaces", "\\s+", true);
8
13
  const importNameDivider = new Regex("import-name-divider", "(\\s+)?,(\\s+)?");
9
14
  const importKeyword = new Literal("import", "import");
15
+ const useParamsKeyword = new Literal("use-params", "use params");
16
+ const asKeyword = new Literal("as", "as");
10
17
  const fromKeyword = new Literal("from", "from");
11
18
  const openBracket = new Literal("open-bracket", "{");
12
19
  const closeBracket = new Literal("close-bracket", "}");
13
20
  const name = new Regex("import-name", "[^}\\s,]+");
14
- const importedNames = new Repeat("imported-names", name, { divider: importNameDivider });
15
- const optionalSpaces = spaces.clone("optional-spaces", true);
21
+ const importNameAlias = name.clone("import-name-alias");
22
+ const importAlias = new And("import-alias", [name, lineSpaces, asKeyword, lineSpaces, importNameAlias]);
23
+ const importedNames = new Repeat("imported-names", new Or("import-names", [importAlias, name]), { divider: importNameDivider });
24
+ const paramName = name.clone("param-name");
25
+ const paramNames = new Repeat("param-names", paramName, { divider: importNameDivider });
26
+ const resource = literal.clone("resource");
16
27
 
17
- export const importStatement = new And("import-statement", [
18
- importKeyword,
28
+ const useParams = new And("import-params", [
29
+ useParamsKeyword,
30
+ optionalLineSpaces,
31
+ openBracket,
32
+ optionalSpaces,
33
+ paramNames,
34
+ optionalSpaces,
35
+ closeBracket
36
+ ]);
37
+
38
+ const withParamsKeyword = new Literal("with-params", "with params");
39
+ const withParamsStatement = new And("with-params-statement", [
40
+ withParamsKeyword,
41
+ optionalLineSpaces,
42
+ openBracket,
19
43
  optionalSpaces,
44
+ body.clone("with-params-body"),
45
+ optionalSpaces,
46
+ closeBracket
47
+ ], true);
48
+
49
+ const importFromStatement = new And("import-from", [
50
+ importKeyword,
51
+ optionalLineSpaces,
20
52
  openBracket,
21
53
  optionalSpaces,
22
54
  importedNames,
23
55
  optionalSpaces,
24
56
  closeBracket,
25
- optionalSpaces,
57
+ optionalLineSpaces,
26
58
  fromKeyword,
27
- spaces,
28
- literal.clone("url"),
59
+ optionalLineSpaces,
60
+ resource,
61
+ optionalLineSpaces,
62
+ withParamsStatement
29
63
  ]);
64
+
65
+ export const importStatement = new Or("import-statement", [
66
+ useParams,
67
+ importFromStatement
68
+ ]);
69
+
70
+
@@ -1,4 +1,14 @@
1
+ import { Or } from "../../patterns/Or";
1
2
  import { Regex } from "../../patterns/Regex";
3
+ import { Repeat } from "../../patterns/Repeat";
2
4
 
3
- export const spaces = new Regex("spaces", "[ \\t]+");
4
- spaces.setTokens([" "]);
5
+ export const tabs = new Regex("tabs", "\\t+");
6
+ export const spaces = new Regex("spaces", "[ ]+");
7
+ export const newLine = new Regex("new-line", "(\\r?\\n)+");
8
+
9
+ spaces.setTokens([" "]);
10
+ tabs.setTokens(["\t"]);
11
+ newLine.setTokens(["\n"]);
12
+
13
+ export const lineSpaces = new Repeat("line-spaces", new Or("line-space", [tabs, spaces]));
14
+ export const allSpaces = new Regex("all-spaces", "\\s+", true);
@@ -8,11 +8,9 @@ import { regexLiteral } from "./regexLiteral";
8
8
  import { repeatLiteral } from "./repeatLiteral";
9
9
  import { spaces } from "./spaces";
10
10
  import { literal } from "./literal";
11
- import { comment } from "./comment";
12
11
 
13
12
  const optionalSpaces = spaces.clone("optional-spaces", true);
14
13
  const assignOperator = new Literal("assign-operator", "=");
15
- const optionalComment = comment.clone("inline-comment", true);
16
14
 
17
15
  const statements = new Or("statements", [
18
16
  literal,
@@ -23,14 +21,13 @@ const statements = new Or("statements", [
23
21
  name.clone("alias-literal"),
24
22
  ]);
25
23
 
26
- export const statement = new And("statement", [
24
+ const assignStatement = new And("assign-statement", [
27
25
  optionalSpaces,
28
26
  name,
29
27
  optionalSpaces,
30
28
  assignOperator,
31
29
  optionalSpaces,
32
- statements,
33
- optionalSpaces,
34
- optionalComment,
35
- optionalSpaces,
36
- ]);
30
+ statements
31
+ ]);
32
+
33
+ export const statement = new Or("statement", [assignStatement, name.clone("export-name")]);
@@ -164,3 +164,27 @@ import { last-name } from "https://some.cdn.com/some/last-name.cpat"
164
164
 
165
165
  full-name = first-name & spaces & last-name
166
166
  ```
167
+
168
+ ### Import Params
169
+ This allows you to inject patterns within another pattern.
170
+ ```ts
171
+ const firstName = new Literal("first-name", "Jared");
172
+ const grammar = Grammar.import('file.cpat', {params: [firstName, LastName]})
173
+ ```
174
+ file.cpat
175
+ ```
176
+ import params { first-names, last-names }
177
+
178
+ full-name = first-names & spaces & last-names
179
+ ```
180
+
181
+ ### Imports with Params
182
+ ```
183
+ import params { other-pattern }
184
+ import { first-names, last-names } from "some-file.cpat" with params {
185
+ some-pattern = "Some Pattern"
186
+ some-pattern2 = other-pattern
187
+ }
188
+
189
+ full-name = first-names & spaces & last-names
190
+ ```