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.
- package/LICENSE.md +7 -0
- package/README.md +387 -0
- package/dist/Exports.d.ts +1 -0
- package/dist/Exports.js +2 -0
- package/dist/Exports.js.map +1 -0
- package/dist/Grammar.d.ts +62 -0
- package/dist/Grammar.js +376 -0
- package/dist/Grammar.js.map +1 -0
- package/dist/GrammarComposer.d.ts +61 -0
- package/dist/GrammarComposer.js +383 -0
- package/dist/GrammarComposer.js.map +1 -0
- package/dist/Grammars/JsonGrammar.d.ts +16 -0
- package/dist/Grammars/JsonGrammar.js +74 -0
- package/dist/Grammars/JsonGrammar.js.map +1 -0
- package/dist/Grammars/XmlGrammar.d.ts +13 -0
- package/dist/Grammars/XmlGrammar.js +74 -0
- package/dist/Grammars/XmlGrammar.js.map +1 -0
- package/dist/ParserTest.d.ts +1 -0
- package/dist/ParserTest.js +37 -0
- package/dist/ParserTest.js.map +1 -0
- package/dist/Test.d.ts +1 -0
- package/dist/Test.js +74 -0
- package/dist/Test.js.map +1 -0
- package/dist/TestData.d.ts +5 -0
- package/dist/TestData.js +466 -0
- package/dist/TestData.js.map +1 -0
- package/dist/TopDownParser.d.ts +13 -0
- package/dist/TopDownParser.js +229 -0
- package/dist/TopDownParser.js.map +1 -0
- package/dist/data/TestData.d.ts +5 -0
- package/dist/data/TestData.js +466 -0
- package/dist/data/TestData.js.map +1 -0
- package/dist/test-data/TestData.d.ts +5 -0
- package/dist/test-data/TestData.js +466 -0
- package/dist/test-data/TestData.js.map +1 -0
- package/dist/test-grammars/JsonGrammar.d.ts +17 -0
- package/dist/test-grammars/JsonGrammar.js +74 -0
- package/dist/test-grammars/JsonGrammar.js.map +1 -0
- package/dist/test-grammars/XmlGrammar.d.ts +14 -0
- package/dist/test-grammars/XmlGrammar.js +74 -0
- package/dist/test-grammars/XmlGrammar.js.map +1 -0
- package/dist/utilities/Timer.d.ts +13 -0
- package/dist/utilities/Timer.js +70 -0
- package/dist/utilities/Timer.js.map +1 -0
- package/dist/utilities/utilities.d.ts +8 -0
- package/dist/utilities/utilities.js +26 -0
- package/dist/utilities/utilities.js.map +1 -0
- package/package.json +40 -0
- package/src/GrammarComposer.ts +541 -0
- package/src/Test.ts +101 -0
- package/src/TopDownParser.ts +304 -0
- package/src/test-data/TestData.ts +470 -0
- package/src/test-grammars/JsonGrammar.ts +118 -0
- package/src/test-grammars/XmlGrammar.ts +141 -0
- package/src/utilities/Timer.ts +95 -0
- package/src/utilities/Utilities.ts +33 -0
- 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
|
+
}
|