grammar-composer 0.1.0

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.
Files changed (57) hide show
  1. package/LICENSE.md +7 -0
  2. package/README.md +387 -0
  3. package/dist/Exports.d.ts +1 -0
  4. package/dist/Exports.js +2 -0
  5. package/dist/Exports.js.map +1 -0
  6. package/dist/Grammar.d.ts +62 -0
  7. package/dist/Grammar.js +376 -0
  8. package/dist/Grammar.js.map +1 -0
  9. package/dist/GrammarComposer.d.ts +61 -0
  10. package/dist/GrammarComposer.js +383 -0
  11. package/dist/GrammarComposer.js.map +1 -0
  12. package/dist/Grammars/JsonGrammar.d.ts +16 -0
  13. package/dist/Grammars/JsonGrammar.js +74 -0
  14. package/dist/Grammars/JsonGrammar.js.map +1 -0
  15. package/dist/Grammars/XmlGrammar.d.ts +13 -0
  16. package/dist/Grammars/XmlGrammar.js +74 -0
  17. package/dist/Grammars/XmlGrammar.js.map +1 -0
  18. package/dist/ParserTest.d.ts +1 -0
  19. package/dist/ParserTest.js +37 -0
  20. package/dist/ParserTest.js.map +1 -0
  21. package/dist/Test.d.ts +1 -0
  22. package/dist/Test.js +74 -0
  23. package/dist/Test.js.map +1 -0
  24. package/dist/TestData.d.ts +5 -0
  25. package/dist/TestData.js +466 -0
  26. package/dist/TestData.js.map +1 -0
  27. package/dist/TopDownParser.d.ts +13 -0
  28. package/dist/TopDownParser.js +229 -0
  29. package/dist/TopDownParser.js.map +1 -0
  30. package/dist/data/TestData.d.ts +5 -0
  31. package/dist/data/TestData.js +466 -0
  32. package/dist/data/TestData.js.map +1 -0
  33. package/dist/test-data/TestData.d.ts +5 -0
  34. package/dist/test-data/TestData.js +466 -0
  35. package/dist/test-data/TestData.js.map +1 -0
  36. package/dist/test-grammars/JsonGrammar.d.ts +17 -0
  37. package/dist/test-grammars/JsonGrammar.js +74 -0
  38. package/dist/test-grammars/JsonGrammar.js.map +1 -0
  39. package/dist/test-grammars/XmlGrammar.d.ts +14 -0
  40. package/dist/test-grammars/XmlGrammar.js +74 -0
  41. package/dist/test-grammars/XmlGrammar.js.map +1 -0
  42. package/dist/utilities/Timer.d.ts +13 -0
  43. package/dist/utilities/Timer.js +70 -0
  44. package/dist/utilities/Timer.js.map +1 -0
  45. package/dist/utilities/utilities.d.ts +8 -0
  46. package/dist/utilities/utilities.js +26 -0
  47. package/dist/utilities/utilities.js.map +1 -0
  48. package/package.json +40 -0
  49. package/src/GrammarComposer.ts +541 -0
  50. package/src/Test.ts +101 -0
  51. package/src/TopDownParser.ts +304 -0
  52. package/src/test-data/TestData.ts +470 -0
  53. package/src/test-grammars/JsonGrammar.ts +118 -0
  54. package/src/test-grammars/XmlGrammar.ts +141 -0
  55. package/src/utilities/Timer.ts +95 -0
  56. package/src/utilities/Utilities.ts +33 -0
  57. package/tsconfig.json +105 -0
package/LICENSE.md ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2024 Rotem Dan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,387 @@
1
+ # Grammar composer
2
+
3
+ A library to define, build and efficiently parse context-free grammars.
4
+
5
+ * Grammars are defined using TypeScript class declarations
6
+ * No need for separate tokenization step. Tokenization is defined as part of the grammar via embedded `Pattern` objects that are internally processed through the [`regexp-composer`](https://github.com/rotemdan/regexp-composer) regular expression library
7
+ * The generated parser accepts raw characters as input, meaning it's a form of lexer-free, or hybrid parser, supporting contextual tokenization - that is, low-level character patterns can be specialized to different high-level parser contexts, and sub-patterns captured in the low-level regular expressions are directly embedded as part of the resulting parse tree
8
+ * Top-down parsing (roughly equivalent to PEG parsing), with optional "packrat" caching that can be enabled or disabled for individual productions
9
+ * Supports right-recursion, but will currently error when left-recursion is detected
10
+ * Uses sophisticated static analysis to automatically identify and annotate optional productions
11
+ * Provides useful parse-time error reporting, identifying the exact production involved and most likely alternatives at the failed position
12
+ * Fast parsing. Optimized for speed
13
+
14
+ ## Example: XML grammar
15
+
16
+ The grammar is defined within a container class `XmlGrammar`. It contains a mixture of higher-level, context-free productions and lower-level, regular expression productions.
17
+
18
+ * Context-free grammar productions are defined by anonymous functions `() => ...`
19
+ * Regular expression productions are defined by `pattern(...)`
20
+
21
+ In this example, context-free operators are prefixed with `G`, and regular expression operators are prefixed with `R`, to avoid confusion between similarly named operators:
22
+ ```ts
23
+ import * as G from 'grammar-composer'
24
+ import * as R from 'regexp-composer'
25
+
26
+ export class XmlGrammar {
27
+ document = () => [
28
+ G.zeroOrMore(
29
+ G.anyOf(
30
+ this.textFragment,
31
+ this.openingTag,
32
+ this.closingTag,
33
+ this.comment,
34
+ this.declarationTag,
35
+ )
36
+ )
37
+ ]
38
+
39
+ textFragment = G.pattern([
40
+ R.oneOrMore(R.notAnyOfChars('<'))
41
+ ])
42
+
43
+ openingTag = () => [
44
+ this.openingTagStart,
45
+
46
+ G.zeroOrMore(this.attribute),
47
+
48
+ this.tagEnd
49
+ ]
50
+
51
+ openingTagStart = G.pattern([
52
+ '<',
53
+
54
+ R.possibly('?'),
55
+
56
+ R.captureAs('tagName',
57
+ R.oneOrMore(R.notAnyOfChars(R.whitespace, '"', "'", '?', '!', '/', '>'))
58
+ ),
59
+
60
+ R.zeroOrMore(R.whitespace),
61
+ ])
62
+
63
+ tagEnd = G.pattern([
64
+ R.zeroOrMore(R.whitespace),
65
+
66
+ R.possibly(R.anyOf('/', '?')),
67
+
68
+ '>'
69
+ ])
70
+
71
+ attribute = G.pattern([
72
+ R.zeroOrMore(R.whitespace),
73
+
74
+ R.captureAs('attributeName',
75
+ R.oneOrMore(R.notAnyOfChars(R.whitespace, '=', '"', "'", '?', '/', '>'))
76
+ ),
77
+
78
+ R.zeroOrMore(R.whitespace),
79
+
80
+ R.possibly([
81
+ '=',
82
+
83
+ R.zeroOrMore(R.whitespace),
84
+
85
+ quotedString,
86
+
87
+ R.zeroOrMore(R.whitespace),
88
+ ])
89
+ ])
90
+
91
+ closingTag = G.pattern([
92
+ '</',
93
+
94
+ R.zeroOrMore(R.whitespace),
95
+
96
+ R.captureAs('tagName',
97
+ R.oneOrMore(R.notAnyOfChars(R.whitespace, '/', '>'))
98
+ ),
99
+
100
+ R.zeroOrMore(R.whitespace),
101
+
102
+ '>'
103
+ ])
104
+
105
+ declarationTag = () => [
106
+ this.declarationTagOpening,
107
+
108
+ G.zeroOrMore(this.declarationTagAttribute),
109
+
110
+ this.tagEnd
111
+ ]
112
+
113
+ declarationTagOpening = G.pattern([
114
+ '<!',
115
+
116
+ R.captureAs('tagName',
117
+ R.oneOrMore(R.notAnyOfChars(R.whitespace, '"', "'", '/', '>'))
118
+ ),
119
+
120
+ R.zeroOrMore(R.whitespace)
121
+ ])
122
+
123
+ declarationTagAttribute = G.pattern([
124
+ R.zeroOrMore(R.whitespace),
125
+
126
+ R.anyOf(
127
+ R.captureAs('attributeName',
128
+ R.oneOrMore(R.notAnyOfChars(R.whitespace, '"', "'", '/', '!', '?', '>'))
129
+ ),
130
+
131
+ quotedString,
132
+ ),
133
+
134
+ R.zeroOrMore(R.whitespace),
135
+ ])
136
+
137
+ comment = G.pattern([
138
+ '<!--',
139
+
140
+ R.captureAs('commentBody',
141
+ R.zeroOrMoreNonGreedy(R.anyChar)
142
+ ),
143
+
144
+ '-->'
145
+ ])
146
+ }
147
+
148
+ const quotedString = R.anyOf(
149
+ [
150
+ '"',
151
+ R.captureAs('doubleQuotedStringContent',
152
+ R.zeroOrMore(R.notAnyOfChars('"'))
153
+ ),
154
+ '"'
155
+ ],
156
+ [
157
+ "'",
158
+ R.captureAs('singleQuotedStringContent',
159
+ R.zeroOrMore(R.notAnyOfChars("'"))
160
+ ),
161
+ "'"
162
+ ],
163
+ )
164
+ ```
165
+
166
+ Building and parsing using the XML grammar:
167
+
168
+ ```ts
169
+ import { buildGrammar, parse } from 'grammar-composer'
170
+
171
+ const xmlString = `
172
+ <!DOCTYPE web-app>
173
+
174
+ <menu>
175
+ <header>Adobe SVG Viewer</header>
176
+ <item action="Open" id="Open">Open</item>
177
+ <item action="OpenNew" id="OpenNew">Open New</item>
178
+ <separator/>
179
+ <item action="ZoomIn" id="ZoomIn">Zoom In</item>
180
+ <item action="ZoomOut" id="ZoomOut">Zoom Out</item>
181
+ <separator/>
182
+ <item action="Quality" id="Quality">Quality</item>
183
+ <item action="Pause" id="Pause">Pause</item>
184
+ <item action="Mute" id="Mute">Mute</item>
185
+ <separator/>
186
+ <item action="Find" id="Find">Find...</item>
187
+ <item action="FindAgain" id="FindAgain">Find Again</item>
188
+ <item action="Copy" id="Copy">Copy</item>
189
+ </menu>
190
+ `
191
+
192
+ // Build the grammar. 'document' is the starting production.
193
+ //
194
+ // Although `XmlGrammar` is a class, there's no need to instantiatte it,
195
+ // just pass it as it is.
196
+ const grammar = buildGrammar(XmlGrammar, 'document')
197
+
198
+ // Parse the XML string with the built grammar
199
+ const parseTree = parse(xmlString, grammar)
200
+ ```
201
+
202
+ The resulting parse tree looks like:
203
+
204
+ ```ts
205
+ [
206
+ {
207
+ "name": "document",
208
+ "startOffset": 0,
209
+ "endOffset": 644,
210
+ "sourceText": "\n<!DOCTYPE web-app>\n\n<menu>\n <header>Adobe SVG Viewer</header>\n <it
211
+ em action=\"Open\" id=\"Open\">Open</item>\n <item action=\"OpenNew\" id=\"OpenNew\">Open New</ite
212
+ m>\n <separator/>\n <item action=\"ZoomIn\" id=\"ZoomIn\">Zoom In</item>\n <item action=\"Zo
213
+ omOut\" id=\"ZoomOut\">Zoom Out</item>\n <separator/>\n <item action=\"Quality\" id=\"Quality\"
214
+ >Quality</item>\n <item action=\"Pause\" id=\"Pause\">Pause</item>\n <item action=\"Mute\" id=\
215
+ "Mute\">Mute</item>\n <separator/>\n <item action=\"Find\" id=\"Find\">Find...</item>\n <ite
216
+ m action=\"FindAgain\" id=\"FindAgain\">Find Again</item>\n <item action=\"Copy\" id=\"Copy\">Copy
217
+ </item>\n</menu>\n\n",
218
+ "children": [
219
+ {
220
+ "name": "textFragment",
221
+ "startOffset": 0,
222
+ "endOffset": 1,
223
+ "sourceText": "\n",
224
+ "children": []
225
+ },
226
+ {
227
+ "name": "declarationTag",
228
+ "startOffset": 1,
229
+ "endOffset": 19,
230
+ "sourceText": "<!DOCTYPE web-app>",
231
+ "children": [
232
+ {
233
+ "name": "declarationTagOpening",
234
+ "startOffset": 1,
235
+ "endOffset": 11,
236
+ "sourceText": "<!DOCTYPE ",
237
+ "children": [
238
+ {
239
+ "name": "tagName",
240
+ "startOffset": 3,
241
+ "endOffset": 10,
242
+ "sourceText": "DOCTYPE"
243
+ }
244
+ ]
245
+ },
246
+ {
247
+ "name": "declarationTagAttribute",
248
+ "startOffset": 11,
249
+ "endOffset": 18,
250
+ "sourceText": "web-app",
251
+ "children": [
252
+ {
253
+ "name": "attributeName",
254
+ "startOffset": 11,
255
+ "endOffset": 18,
256
+ "sourceText": "web-app"
257
+ }
258
+ ]
259
+ },
260
+ {
261
+ "name": "tagEnd",
262
+ "startOffset": 18,
263
+ "endOffset": 19,
264
+ "sourceText": ">",
265
+ "children": []
266
+ }
267
+ ]
268
+ },
269
+ {
270
+ "name": "textFragment",
271
+ "startOffset": 19,
272
+ "endOffset": 21,
273
+ "sourceText": "\n\n",
274
+ "children": []
275
+ },
276
+ {
277
+ "name": "openingTag",
278
+ "startOffset": 21,
279
+ "endOffset": 27,
280
+ "sourceText": "<menu>",
281
+ "children": [
282
+ {
283
+ "name": "openingTagStart",
284
+ "startOffset": 21,
285
+ "endOffset": 26,
286
+ "sourceText": "<menu",
287
+ "children": [
288
+ {
289
+ "name": "tagName",
290
+ "startOffset": 22,
291
+ "endOffset": 26,
292
+ "sourceText": "menu"
293
+ }
294
+ ]
295
+ },
296
+ {
297
+ "name": "tagEnd",
298
+ "startOffset": 26,
299
+ "endOffset": 27,
300
+ "sourceText": ">",
301
+ "children": []
302
+ }
303
+ ]
304
+ },
305
+ {
306
+ "name": "textFragment",
307
+ "startOffset": 27,
308
+ "endOffset": 32,
309
+ "sourceText": "\n ",
310
+ "children": []
311
+ },
312
+ {
313
+ "name": "openingTag",
314
+ "startOffset": 32,
315
+ "endOffset": 40,
316
+ "sourceText": "<header>",
317
+ "children": [
318
+ {
319
+ "name": "openingTagStart",
320
+ "startOffset": 32,
321
+ "endOffset": 39,
322
+ "sourceText": "<header",
323
+ "children": [
324
+ {
325
+ "name": "tagName",
326
+ "startOffset": 33,
327
+ "endOffset": 39,
328
+ "sourceText": "header"
329
+ }
330
+ ]
331
+ },
332
+ {
333
+ "name": "tagEnd",
334
+ "startOffset": 39,
335
+ "endOffset": 40,
336
+ "sourceText": ">",
337
+ "children": []
338
+ }
339
+ ]
340
+ },
341
+
342
+ ...
343
+ ```
344
+
345
+ ## Operators
346
+
347
+ Context-free operators are mostly named similarly to the ones in [`regexp-composer`](https://github.com/rotemdan/regexp-composer).
348
+
349
+ ### `zeroOrMore(grammarElement)`
350
+
351
+ Match the grammar element zero or more times.
352
+
353
+ ### `oneOrMore(grammarElement)`
354
+
355
+ Match the grammar element one or more times.
356
+
357
+ ### `anyOf(grammarElement1, grammarElement2, grammarElement3, ...)`
358
+
359
+ Match any of the grammar elements. The first successful match, in order, would be accepted without trying subsequent ones.
360
+
361
+ ### `bestOf(grammarElement1, grammarElement2, grammarElement3, ...)`
362
+
363
+ Match the best grammar element. All possibilities would be tried, and the the longest match (in terms of character count) would be chosen.
364
+
365
+ ### `possibly(grammarElement)`
366
+
367
+ Optionally accept the grammar element, or skip if it doesn't match.
368
+
369
+ ### `pattern(regexpPattern)`
370
+
371
+ Accept a regular expression pattern compatible with `regexp-composer` `Pattern` type (either a simple string, pattern object, or array of pattern objects).
372
+
373
+ ### `cached(grammarElement)`
374
+
375
+ Store the result of parsing using this grammar element and reuse when it's subsequently evaluated **at the same text position**.
376
+
377
+ ### `uncached(grammarElement)`
378
+
379
+ Don't cache this grammar element.
380
+
381
+ ## Future
382
+
383
+ * Provide custom parser functions
384
+
385
+ ## License
386
+
387
+ MIT
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=Exports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Exports.js","sourceRoot":"","sources":["../src/Exports.ts"],"names":[],"mappings":""}
@@ -0,0 +1,62 @@
1
+ import { Pattern } from 'regexp-composer';
2
+ export declare function buildGrammar<T extends {
3
+ [key: string]: any;
4
+ }>(obj: T | (new () => T), startProductionName: keyof T): Grammar<T>;
5
+ export declare function detectAndErrorOnLeftRecursion(rootNode: GrammarElement): void;
6
+ export declare function oneOrMore(content: Production): Repetition;
7
+ export declare function zeroOrMore(content: Production): Repetition;
8
+ export declare function anyOf(...members: Production[]): Choice;
9
+ export declare function bestOf(...members: Production[]): Choice;
10
+ export declare function possibly<T extends Production>(content: Production): T;
11
+ export declare function cached<T extends Production>(content: Production): T;
12
+ export declare function uncached<T extends Production>(content: Production): T;
13
+ export declare function pattern(pattern: Pattern): PatternTerminal;
14
+ export interface Grammar<T> {
15
+ rootElement: Nonterminal;
16
+ productions: {
17
+ [key in keyof T]: any;
18
+ };
19
+ maxElementId: number;
20
+ }
21
+ export type Production = string | GrammarElement | (() => Production) | Production[];
22
+ export type GrammarElement = StringTerminal | PatternTerminal | Nonterminal | Sequence | Repetition | Choice | NonterminalReference;
23
+ interface GrammarElementBase {
24
+ type: string;
25
+ optional: boolean;
26
+ uniqueId?: number;
27
+ cached?: boolean;
28
+ }
29
+ export type Terminal = StringTerminal | PatternTerminal;
30
+ export interface StringTerminal extends GrammarElementBase {
31
+ type: 'StringTerminal';
32
+ content: string;
33
+ }
34
+ export interface PatternTerminal extends GrammarElementBase {
35
+ type: 'PatternTerminal';
36
+ name: string;
37
+ pattern: Pattern | Pattern[];
38
+ regExp: RegExp;
39
+ }
40
+ export interface Nonterminal extends GrammarElementBase {
41
+ type: 'Nonterminal';
42
+ name: string;
43
+ content: GrammarElement;
44
+ }
45
+ export interface Sequence extends GrammarElementBase {
46
+ type: 'Sequence';
47
+ members: GrammarElement[];
48
+ }
49
+ export interface Repetition extends GrammarElementBase {
50
+ type: 'Repetition';
51
+ content: GrammarElement;
52
+ }
53
+ export interface Choice extends GrammarElementBase {
54
+ type: 'Choice';
55
+ members: GrammarElement[];
56
+ exhaustive: boolean;
57
+ }
58
+ export interface NonterminalReference extends GrammarElementBase {
59
+ type: 'NonterminalReference';
60
+ reference: Function;
61
+ }
62
+ export {};