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
@@ -0,0 +1,304 @@
1
+ import { Grammar, GrammarElement, Terminal } from "./GrammarComposer.js"
2
+
3
+ export function parse(inputString: string, grammar: Grammar<any>) {
4
+ const inputLength = inputString.length
5
+ const maxElementId = grammar.maxElementId
6
+
7
+ let bestFailedMatches: Terminal[] = []
8
+ let bestFailedMatchesOffset = -1
9
+
10
+ function updateBestFailedMatchesIfNeeded(terminal: Terminal, startOffset: number) {
11
+ if (startOffset >= bestFailedMatchesOffset) {
12
+ if (startOffset > bestFailedMatchesOffset) {
13
+ bestFailedMatchesOffset = startOffset
14
+ bestFailedMatches = [terminal]
15
+ } else {
16
+ bestFailedMatches.push(terminal)
17
+ }
18
+ }
19
+ }
20
+
21
+ function tryParse(grammarElement: GrammarElement, startOffset: number): ParseResult | null {
22
+ if (grammarElement.cached === true) {
23
+ return tryParseCached(grammarElement, startOffset)
24
+ } else {
25
+ return tryParseUncached(grammarElement, startOffset)
26
+ }
27
+ }
28
+
29
+ type Slot = Map<GrammarElement, ParseResult | null> | undefined
30
+
31
+ const cachedParseResults: Slot[] = new Array(inputLength)
32
+
33
+ function tryParseCached(grammarElement: GrammarElement, startOffset: number): ParseResult | null {
34
+ let slot = cachedParseResults[startOffset]
35
+
36
+ if (slot === undefined) {
37
+ slot = new Map<GrammarElement, ParseResult | null>()
38
+
39
+ cachedParseResults[startOffset] = slot
40
+ } else {
41
+ const cachedResult = slot.get(grammarElement)
42
+
43
+ if (cachedResult !== undefined) {
44
+ return cachedResult
45
+ }
46
+ }
47
+
48
+ const parseResult = tryParseUncached(grammarElement, startOffset)
49
+
50
+ slot.set(grammarElement, parseResult)
51
+
52
+ return parseResult
53
+ }
54
+
55
+ function tryParseUncached(grammarElement: GrammarElement, startOffset: number): ParseResult | null {
56
+ switch (grammarElement.type) {
57
+ case 'StringTerminal': {
58
+ const target = grammarElement.content
59
+ const endOffset = startOffset + target.length
60
+
61
+ if (endOffset > inputLength) {
62
+ updateBestFailedMatchesIfNeeded(grammarElement, startOffset)
63
+
64
+ return null
65
+ }
66
+
67
+ const substringToMatch = inputString.substring(startOffset, endOffset)
68
+
69
+ if (substringToMatch === target) {
70
+ return {
71
+ endOffset,
72
+ nodes: undefined
73
+ }
74
+ } else {
75
+ updateBestFailedMatchesIfNeeded(grammarElement, startOffset)
76
+
77
+ return null
78
+ }
79
+ }
80
+
81
+ case 'PatternTerminal': {
82
+ if (startOffset >= inputLength) {
83
+ updateBestFailedMatchesIfNeeded(grammarElement, startOffset)
84
+
85
+ return null
86
+ }
87
+
88
+ const substringToMatch = inputString.substring(startOffset)
89
+
90
+ const matchResults = grammarElement.regExp.exec(substringToMatch)
91
+
92
+ if (matchResults === null) {
93
+ updateBestFailedMatchesIfNeeded(grammarElement, startOffset)
94
+
95
+ return null
96
+ }
97
+
98
+ const matchStartOffset = startOffset + matchResults.index
99
+ const matchEndOffset = matchStartOffset + matchResults[0].length
100
+
101
+ let nodes: ParseTreeNode[] | undefined = undefined
102
+
103
+ const groupsIndices = matchResults.indices
104
+
105
+ if (groupsIndices !== undefined) {
106
+ let namedGroupIndicesIdentifiers: string[] | undefined = undefined
107
+
108
+ if (groupsIndices.groups) {
109
+ namedGroupIndicesIdentifiers = Object.keys(groupsIndices.groups)
110
+
111
+ if (namedGroupIndicesIdentifiers.length !== groupsIndices.length - 1) {
112
+ throw new Error(`The regular expression /${grammarElement.regExp.source}/ contains a combination of named and unnamed groups. Due to limitations of the JavaScript RegExp engine, it is impossible to reliably identify the ordering of this combination, please use either all unnamed or named groups, but not both.`)
113
+ }
114
+ }
115
+
116
+ const children: ParseTreeNode[] = []
117
+
118
+ for (let i = 1; i < groupsIndices.length; i++) {
119
+ const groupIndices = groupsIndices[i]
120
+
121
+ if (groupIndices === undefined) {
122
+ continue
123
+ }
124
+
125
+ const groupStartOffset = startOffset + groupIndices[0]
126
+ const groupEndOffset = startOffset + groupIndices[1]
127
+
128
+ children.push({
129
+ name: namedGroupIndicesIdentifiers ? namedGroupIndicesIdentifiers[i - 1] : i.toString(),
130
+ startOffset: groupStartOffset,
131
+ endOffset: groupEndOffset,
132
+ sourceText: inputString.substring(groupStartOffset, groupEndOffset),
133
+ children: undefined
134
+ })
135
+ }
136
+
137
+ nodes = [{
138
+ name: grammarElement.name,
139
+ startOffset: matchStartOffset,
140
+ endOffset: matchEndOffset,
141
+ sourceText: inputString.substring(matchStartOffset, matchEndOffset),
142
+ children
143
+ }]
144
+ }
145
+
146
+ const parseResult: ParseResult = {
147
+ endOffset: matchEndOffset,
148
+ nodes
149
+ }
150
+
151
+ return parseResult
152
+ }
153
+
154
+ case 'Nonterminal': {
155
+ const result = tryParse(grammarElement.content, startOffset)
156
+
157
+ if (result === null) {
158
+ return null
159
+ }
160
+
161
+ let newNode: ParseTreeNode = {
162
+ name: grammarElement.name,
163
+ startOffset,
164
+ endOffset: result.endOffset,
165
+ sourceText: inputString.substring(startOffset, result.endOffset),
166
+ children: result.nodes,
167
+ }
168
+
169
+ const newResult: ParseResult = {
170
+ endOffset: result.endOffset,
171
+ nodes: [newNode]
172
+ }
173
+
174
+ return newResult
175
+ }
176
+
177
+ case 'Sequence': {
178
+ let successfulResults: ParseResult[] = []
179
+ let readOffset = startOffset
180
+
181
+ for (const element of grammarElement.members) {
182
+ const elementResult = tryParse(element, readOffset)
183
+
184
+ if (elementResult !== null) {
185
+ successfulResults.push(elementResult)
186
+
187
+ readOffset = elementResult.endOffset
188
+ } else if (element.optional === false) {
189
+ return null
190
+ }
191
+ }
192
+
193
+ let nodes: ParseTreeNode[] = []
194
+
195
+ for (const result of successfulResults) {
196
+ if (result.nodes !== undefined) {
197
+ nodes.push(...result.nodes)
198
+ }
199
+ }
200
+
201
+ return {
202
+ endOffset: readOffset,
203
+ nodes: nodes.length > 0 ? nodes : undefined
204
+ }
205
+ }
206
+
207
+ case 'Repetition': {
208
+ let readOffset = startOffset
209
+
210
+ const nodes: ParseTreeNode[] = []
211
+
212
+ while (true) {
213
+ const result = tryParse(grammarElement.content, readOffset)
214
+
215
+ if (result === null) {
216
+ break
217
+ }
218
+
219
+ if (result.nodes !== undefined) {
220
+ nodes.push(...result.nodes)
221
+ }
222
+
223
+ readOffset = result.endOffset
224
+ }
225
+
226
+ if (readOffset > startOffset) {
227
+ return {
228
+ endOffset: readOffset,
229
+ nodes: nodes.length > 0 ? nodes : undefined
230
+ }
231
+ } else {
232
+ return null
233
+ }
234
+ }
235
+
236
+ case 'Choice': {
237
+ let bestResult: ParseResult | null = null
238
+
239
+ for (const member of grammarElement.members) {
240
+ const result = tryParse(member, startOffset)
241
+
242
+ if (result !== null && (bestResult === null || result.endOffset > bestResult.endOffset)) {
243
+ bestResult = result
244
+
245
+ if (grammarElement.exhaustive === false) {
246
+ break
247
+ }
248
+ }
249
+ }
250
+
251
+ return bestResult
252
+ }
253
+
254
+ default: {
255
+ throw new Error(`Unsupported grammar element type '${(grammarElement as any).type}'.`)
256
+ }
257
+ }
258
+ }
259
+
260
+ const result = tryParse(grammar.rootElement, 0)
261
+
262
+ const lastNode = result?.nodes?.[result.nodes.length - 1]
263
+
264
+ if (lastNode && lastNode.endOffset >= inputLength) {
265
+ return result.nodes
266
+ } else {
267
+ if (bestFailedMatches.length > 0) {
268
+ const possibleMatches = bestFailedMatches.map(match => {
269
+ if (match.type === 'StringTerminal') {
270
+ return `'${match.content}'`
271
+ } else {
272
+ return match.name
273
+ }
274
+ })
275
+
276
+ const possibleMatchesWithoutDuplicates = [...(new Set(possibleMatches))]
277
+
278
+ let possibleMatchesString: string
279
+
280
+ if (possibleMatchesWithoutDuplicates.length > 1) {
281
+ possibleMatchesString = `any of ${possibleMatchesWithoutDuplicates.join(', ')}`
282
+ } else {
283
+ possibleMatchesString = possibleMatchesWithoutDuplicates[0]
284
+ }
285
+
286
+ throw new Error(`Failed parsing the input text. Expected ${possibleMatchesString} at position ${bestFailedMatchesOffset}.`)
287
+ } else {
288
+ throw new Error(`Failed parsing the input text. Parsed length was ${lastNode?.endOffset ?? 0}. Input length was ${inputLength}.`)
289
+ }
290
+ }
291
+ }
292
+
293
+ export interface ParseResult {
294
+ endOffset: number
295
+ nodes: ParseTreeNode[] | undefined
296
+ }
297
+
298
+ export interface ParseTreeNode {
299
+ name: string
300
+ startOffset: number
301
+ endOffset: number
302
+ sourceText: string
303
+ children: ParseTreeNode[] | undefined
304
+ }