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,541 @@
|
|
|
1
|
+
import { Pattern, buildRegExp, inputStart, isPatternOptional } from 'regexp-composer'
|
|
2
|
+
import { isArray, isBoolean, isFunction, isString } from "./utilities/Utilities.js"
|
|
3
|
+
|
|
4
|
+
export * from './TopDownParser.js'
|
|
5
|
+
|
|
6
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
7
|
+
// Grammar builder method
|
|
8
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
9
|
+
export function buildGrammar<T extends { [key: string]: any }>(obj: T | (new () => T), startProductionName: keyof T): Grammar<T> {
|
|
10
|
+
if (isFunction(obj)) {
|
|
11
|
+
obj = new obj()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const nameLookup = new Map<any, keyof T>()
|
|
15
|
+
const nonterminalLookup = new Map<Function, Nonterminal>()
|
|
16
|
+
const optionalNonterminalLookup = new Map<Function, Nonterminal>()
|
|
17
|
+
|
|
18
|
+
for (const key in obj) {
|
|
19
|
+
const objectProperty = obj[key]
|
|
20
|
+
|
|
21
|
+
nameLookup.set(objectProperty, key)
|
|
22
|
+
|
|
23
|
+
if (!isFunction(objectProperty)) {
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const productionContent = objectProperty()
|
|
28
|
+
const normalizedProduction = productionToGrammarElement(productionContent)
|
|
29
|
+
|
|
30
|
+
const newNonterminal = nonterminal(key, normalizedProduction)
|
|
31
|
+
const newOptionalNonterminal = { ...newNonterminal, optional: true }
|
|
32
|
+
|
|
33
|
+
nonterminalLookup.set(objectProperty, newNonterminal)
|
|
34
|
+
optionalNonterminalLookup.set(objectProperty, newOptionalNonterminal)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let uniqueIdCounter = 0
|
|
38
|
+
const uniqueIdSource = () => uniqueIdCounter++
|
|
39
|
+
|
|
40
|
+
for (const [func, nonterminal] of nonterminalLookup) {
|
|
41
|
+
const preparedContent = prepareGrammarElement(nonterminal.content, nameLookup as Map<any, string>, nonterminalLookup, optionalNonterminalLookup, uniqueIdSource)
|
|
42
|
+
|
|
43
|
+
nonterminal.content = preparedContent
|
|
44
|
+
|
|
45
|
+
const optionalNonterminal = optionalNonterminalLookup.get(func)!
|
|
46
|
+
optionalNonterminal.content = preparedContent
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let startNonterminal = nonterminalLookup.get(obj[startProductionName] as Function)
|
|
50
|
+
|
|
51
|
+
if (!startNonterminal) {
|
|
52
|
+
throw new Error(`Couldn't find a start production named '${startProductionName as string}'.`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
detectAndAnnotateOptionalNodes(startNonterminal)
|
|
56
|
+
detectAndErrorOnLeftRecursion(startNonterminal)
|
|
57
|
+
|
|
58
|
+
const nonterminals: { [key in keyof T]: Nonterminal } = {} as any
|
|
59
|
+
|
|
60
|
+
for (const [key, nonterminal] of nonterminalLookup) {
|
|
61
|
+
nonterminals[nameLookup.get(key)!] = nonterminal
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
rootElement: nonterminals[startProductionName],
|
|
66
|
+
productions: nonterminals,
|
|
67
|
+
maxElementId: uniqueIdCounter
|
|
68
|
+
} as Grammar<T>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function prepareGrammarElement(
|
|
72
|
+
rootElement: GrammarElement,
|
|
73
|
+
nameLookup: Map<any, string>,
|
|
74
|
+
nonterminalLookup: Map<Function, Nonterminal>,
|
|
75
|
+
optionalNonterminalLookup: Map<Function, Nonterminal>,
|
|
76
|
+
getUniqueId: () => number
|
|
77
|
+
): GrammarElement {
|
|
78
|
+
function prepare(element: GrammarElement): GrammarElement {
|
|
79
|
+
switch (element.type) {
|
|
80
|
+
case 'StringTerminal':
|
|
81
|
+
case 'Nonterminal': {
|
|
82
|
+
if (element.uniqueId === undefined) {
|
|
83
|
+
element.uniqueId = getUniqueId()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return element
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case 'PatternTerminal': {
|
|
90
|
+
return {
|
|
91
|
+
...element,
|
|
92
|
+
name: nameLookup.get(element) ?? '',
|
|
93
|
+
uniqueId: getUniqueId()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
case 'Repetition': {
|
|
98
|
+
return {
|
|
99
|
+
...element,
|
|
100
|
+
content: prepare(element.content),
|
|
101
|
+
uniqueId: getUniqueId()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case 'Sequence':
|
|
106
|
+
case 'Choice': {
|
|
107
|
+
return {
|
|
108
|
+
...element,
|
|
109
|
+
members: element.members.map(element => prepare(element)),
|
|
110
|
+
uniqueId: getUniqueId()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
case 'NonterminalReference': {
|
|
115
|
+
const reference = element.reference
|
|
116
|
+
|
|
117
|
+
let nonterminal: Nonterminal | undefined
|
|
118
|
+
|
|
119
|
+
if (element.optional) {
|
|
120
|
+
nonterminal = optionalNonterminalLookup.get(reference)
|
|
121
|
+
} else {
|
|
122
|
+
nonterminal = nonterminalLookup.get(reference)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!nonterminal) {
|
|
126
|
+
throw new Error(`Couldn't resolve function reference in grammar element: ${JSON.stringify(element)}`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (nonterminal.uniqueId === undefined) {
|
|
130
|
+
nonterminal.uniqueId = getUniqueId()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return nonterminal
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return prepare(rootElement)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
142
|
+
// Internal static analysis methods
|
|
143
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
144
|
+
function detectAndAnnotateOptionalNodes(rootNode: GrammarElement) {
|
|
145
|
+
const visitedNodes = new Set<GrammarElement>()
|
|
146
|
+
|
|
147
|
+
const resolvedNodes = new Map<GrammarElement, boolean>()
|
|
148
|
+
const unresolvedNodes = new Map<GrammarElement, Set<GrammarElement>>()
|
|
149
|
+
|
|
150
|
+
function processDepthFirst(node: GrammarElement): boolean | undefined {
|
|
151
|
+
if (visitedNodes.has(node)) {
|
|
152
|
+
return resolvedNodes.get(node)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
visitedNodes.add(node)
|
|
156
|
+
|
|
157
|
+
switch (node.type) {
|
|
158
|
+
case 'StringTerminal':
|
|
159
|
+
case 'PatternTerminal': {
|
|
160
|
+
resolvedNodes.set(node, node.optional)
|
|
161
|
+
|
|
162
|
+
return node.optional
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case 'Nonterminal':
|
|
166
|
+
case 'Repetition': {
|
|
167
|
+
const result = processDepthFirst(node.content)
|
|
168
|
+
|
|
169
|
+
if (node.optional) {
|
|
170
|
+
resolvedNodes.set(node, true)
|
|
171
|
+
|
|
172
|
+
return true
|
|
173
|
+
} else if (isBoolean(result)) {
|
|
174
|
+
resolvedNodes.set(node, result)
|
|
175
|
+
|
|
176
|
+
return result
|
|
177
|
+
} else {
|
|
178
|
+
unresolvedNodes.set(node, new Set([node.content]))
|
|
179
|
+
|
|
180
|
+
return undefined
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case 'Sequence':
|
|
185
|
+
case 'Choice': {
|
|
186
|
+
const dependencies = new Set<GrammarElement>()
|
|
187
|
+
|
|
188
|
+
let allResolvedElementsAreOptional = true
|
|
189
|
+
|
|
190
|
+
for (const element of node.members) {
|
|
191
|
+
const result = processDepthFirst(element)
|
|
192
|
+
|
|
193
|
+
if (isBoolean(result)) {
|
|
194
|
+
if (result == false) {
|
|
195
|
+
allResolvedElementsAreOptional = false
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
dependencies.add(element)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (node.optional == true) {
|
|
203
|
+
resolvedNodes.set(node, true)
|
|
204
|
+
|
|
205
|
+
return true
|
|
206
|
+
} else if (dependencies.size == 0 || !allResolvedElementsAreOptional) {
|
|
207
|
+
resolvedNodes.set(node, allResolvedElementsAreOptional)
|
|
208
|
+
|
|
209
|
+
return allResolvedElementsAreOptional
|
|
210
|
+
} else {
|
|
211
|
+
unresolvedNodes.set(node, dependencies)
|
|
212
|
+
|
|
213
|
+
return undefined
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return undefined
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Process depth first to resolve the easy cases, for productions that contain
|
|
222
|
+
// no cyclic references:
|
|
223
|
+
processDepthFirst(rootNode)
|
|
224
|
+
|
|
225
|
+
// Now the remainder consists of nodes containing cyclic references that have not yet been resolved.
|
|
226
|
+
// Use a form of iterative elimination and substitution to resolve them:
|
|
227
|
+
while (unresolvedNodes.size > 0) {
|
|
228
|
+
// This variable tracks whether at least one dependency was resolved, in any node.
|
|
229
|
+
// If it stays false, it means that no improvement was made during the iteration,
|
|
230
|
+
// and we should exit the loop.
|
|
231
|
+
let atLastOneDependencyResolvedInAnyNode = false
|
|
232
|
+
|
|
233
|
+
// Scan the unresolved nodes to locate any new resolved dependencies
|
|
234
|
+
for (const [node, dependencies] of unresolvedNodes) {
|
|
235
|
+
let nonOptionalDependencyFound = false
|
|
236
|
+
|
|
237
|
+
// Iterate over all unresolved dependencies for the node
|
|
238
|
+
for (const dependency of dependencies) {
|
|
239
|
+
// Check if the dependency has been resolved
|
|
240
|
+
const value = resolvedNodes.get(dependency)
|
|
241
|
+
|
|
242
|
+
if (value !== undefined) {
|
|
243
|
+
// If it did, record that some dependencies were resolved
|
|
244
|
+
atLastOneDependencyResolvedInAnyNode = true
|
|
245
|
+
|
|
246
|
+
if (value === false) {
|
|
247
|
+
// If the value was false, then the entire target node must not be optional
|
|
248
|
+
nonOptionalDependencyFound = true
|
|
249
|
+
|
|
250
|
+
break
|
|
251
|
+
} else {
|
|
252
|
+
// If the result was true, remove the dependency from the set
|
|
253
|
+
dependencies.delete(dependency)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// If either a non-optional dependency was found, or all dependencies were resolved,
|
|
259
|
+
// resolve the target node:
|
|
260
|
+
if (nonOptionalDependencyFound || dependencies.size === 0) {
|
|
261
|
+
const isOptional = !nonOptionalDependencyFound
|
|
262
|
+
|
|
263
|
+
resolvedNodes.set(node, isOptional)
|
|
264
|
+
unresolvedNodes.delete(node)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// If not even one dependency was eliminated for any node,
|
|
269
|
+
// it means that only mutually cyclic nodes are left unresolved, so exit the loop.
|
|
270
|
+
if (!atLastOneDependencyResolvedInAnyNode) {
|
|
271
|
+
break
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// All remaining unresolved nodes must now be optional,
|
|
276
|
+
// since they are all mutually cyclic and all their non-cyclic grammar elements are known to be optional.
|
|
277
|
+
for (const node of unresolvedNodes.keys()) {
|
|
278
|
+
resolvedNodes.set(node, true)
|
|
279
|
+
unresolvedNodes.delete(node)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Finally set the 'optional' property of all nodes based on the detected values.
|
|
283
|
+
for (const [node, isOptional] of resolvedNodes) {
|
|
284
|
+
node.optional = isOptional
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function detectAndErrorOnLeftRecursion(rootNode: GrammarElement) {
|
|
289
|
+
const currentlyIteratedNodes = new Set<GrammarElement>()
|
|
290
|
+
|
|
291
|
+
function detect(node: GrammarElement) {
|
|
292
|
+
if (currentlyIteratedNodes.has(node)) {
|
|
293
|
+
if (node.type === 'Nonterminal') {
|
|
294
|
+
throw new Error(`Detected left recursion for nonterminal '${node.name}'.`)
|
|
295
|
+
} else {
|
|
296
|
+
throw new Error(`Detected left recursion for node: ${JSON.stringify(node, undefined, 4)}`)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
currentlyIteratedNodes.add(node)
|
|
301
|
+
|
|
302
|
+
switch (node.type) {
|
|
303
|
+
case 'Nonterminal':
|
|
304
|
+
case 'Repetition': {
|
|
305
|
+
detect(node.content)
|
|
306
|
+
|
|
307
|
+
break
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
case 'Sequence': {
|
|
311
|
+
for (const member of node.members) {
|
|
312
|
+
detect(member)
|
|
313
|
+
|
|
314
|
+
if (!member.optional) {
|
|
315
|
+
break
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
break
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
case 'Choice': {
|
|
323
|
+
for (const member of node.members) {
|
|
324
|
+
detect(member)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
break
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
currentlyIteratedNodes.delete(node)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
detect(rootNode)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
338
|
+
// Exported builder methods
|
|
339
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
340
|
+
export function zeroOrMore(content: Production): Repetition {
|
|
341
|
+
return {
|
|
342
|
+
type: 'Repetition',
|
|
343
|
+
content: productionToGrammarElement(content),
|
|
344
|
+
optional: true
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function oneOrMore(content: Production): Repetition {
|
|
349
|
+
return {
|
|
350
|
+
type: 'Repetition',
|
|
351
|
+
content: productionToGrammarElement(content),
|
|
352
|
+
optional: false
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function anyOf(...members: Production[]): Choice {
|
|
357
|
+
if (members.length == 0) {
|
|
358
|
+
throw new Error(`'anyOf' requires at least one member.`)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const normalizedMembers = members.map(member => productionToGrammarElement(member))
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
type: 'Choice',
|
|
365
|
+
members: normalizedMembers,
|
|
366
|
+
optional: false,
|
|
367
|
+
exhaustive: false
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function bestOf(...members: Production[]): Choice {
|
|
372
|
+
if (members.length == 0) {
|
|
373
|
+
throw new Error(`'bestOf' requires at least one member.`)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const normalizedMembers = members.map(member => productionToGrammarElement(member))
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
type: 'Choice',
|
|
380
|
+
members: normalizedMembers,
|
|
381
|
+
optional: false,
|
|
382
|
+
exhaustive: true
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function possibly<T extends Production>(content: Production): T {
|
|
387
|
+
return { ...productionToGrammarElement(content), optional: true } as T
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function pattern(pattern: Pattern): PatternTerminal {
|
|
391
|
+
if (isArray(pattern)) {
|
|
392
|
+
pattern = [inputStart, ...pattern]
|
|
393
|
+
} else {
|
|
394
|
+
pattern = [inputStart, pattern]
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const regExp = buildRegExp(pattern)
|
|
398
|
+
const optional = isPatternOptional(pattern)
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
type: 'PatternTerminal',
|
|
402
|
+
name: '[Pattern]',
|
|
403
|
+
pattern,
|
|
404
|
+
regExp,
|
|
405
|
+
optional,
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function cached<T extends Production>(content: Production): T {
|
|
410
|
+
return { ...productionToGrammarElement(content), cached: true } as T
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function uncached<T extends Production>(content: Production): T {
|
|
414
|
+
return { ...productionToGrammarElement(content), cached: false } as T
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
418
|
+
// Internal builder methods
|
|
419
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
420
|
+
function stringTerminal(content: string): StringTerminal {
|
|
421
|
+
if (content.length < 1) {
|
|
422
|
+
throw new Error(`A string terminal must have a length of at least 1 character`)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
type: 'StringTerminal',
|
|
427
|
+
content,
|
|
428
|
+
optional: false
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function nonterminal(name: string, content: GrammarElement): Nonterminal {
|
|
433
|
+
if (name.length < 1) {
|
|
434
|
+
throw new Error(`A nonterminal name must include at least 1 character.`)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
type: 'Nonterminal',
|
|
439
|
+
name,
|
|
440
|
+
content,
|
|
441
|
+
optional: false,
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function sequence(members: GrammarElement[]): Sequence {
|
|
446
|
+
return {
|
|
447
|
+
type: 'Sequence',
|
|
448
|
+
members,
|
|
449
|
+
optional: false
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function unresolvedReference(reference: Function): NonterminalReference {
|
|
454
|
+
return {
|
|
455
|
+
type: 'NonterminalReference',
|
|
456
|
+
reference,
|
|
457
|
+
optional: false
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function productionToGrammarElement(production: Production): GrammarElement {
|
|
462
|
+
if (isString(production)) {
|
|
463
|
+
return stringTerminal(production)
|
|
464
|
+
} else if (isArray(production)) {
|
|
465
|
+
const normalizedMembers = production.map(element => productionToGrammarElement(element))
|
|
466
|
+
|
|
467
|
+
return sequence(normalizedMembers)
|
|
468
|
+
} else if (isFunction(production)) {
|
|
469
|
+
return unresolvedReference(production)
|
|
470
|
+
} else {
|
|
471
|
+
return production
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
476
|
+
// Type definitions
|
|
477
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
478
|
+
export interface Grammar<T> {
|
|
479
|
+
rootElement: Nonterminal
|
|
480
|
+
productions: { [key in keyof T]: any }
|
|
481
|
+
maxElementId: number
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export type Production = string | GrammarElement | (() => Production) | Production[]
|
|
485
|
+
|
|
486
|
+
export type GrammarElement =
|
|
487
|
+
StringTerminal |
|
|
488
|
+
PatternTerminal |
|
|
489
|
+
Nonterminal |
|
|
490
|
+
Sequence |
|
|
491
|
+
Repetition |
|
|
492
|
+
Choice |
|
|
493
|
+
NonterminalReference
|
|
494
|
+
|
|
495
|
+
interface GrammarElementBase {
|
|
496
|
+
type: string
|
|
497
|
+
optional: boolean
|
|
498
|
+
uniqueId?: number
|
|
499
|
+
cached?: boolean
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export type Terminal = StringTerminal | PatternTerminal
|
|
503
|
+
|
|
504
|
+
export interface StringTerminal extends GrammarElementBase {
|
|
505
|
+
type: 'StringTerminal'
|
|
506
|
+
content: string
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export interface PatternTerminal extends GrammarElementBase {
|
|
510
|
+
type: 'PatternTerminal'
|
|
511
|
+
name: string
|
|
512
|
+
pattern: Pattern | Pattern[]
|
|
513
|
+
regExp: RegExp
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export interface Nonterminal extends GrammarElementBase {
|
|
517
|
+
type: 'Nonterminal'
|
|
518
|
+
name: string
|
|
519
|
+
content: GrammarElement
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export interface Sequence extends GrammarElementBase {
|
|
523
|
+
type: 'Sequence'
|
|
524
|
+
members: GrammarElement[]
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export interface Repetition extends GrammarElementBase {
|
|
528
|
+
type: 'Repetition'
|
|
529
|
+
content: GrammarElement
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export interface Choice extends GrammarElementBase {
|
|
533
|
+
type: 'Choice'
|
|
534
|
+
members: GrammarElement[]
|
|
535
|
+
exhaustive: boolean
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export interface NonterminalReference extends GrammarElementBase {
|
|
539
|
+
type: 'NonterminalReference'
|
|
540
|
+
reference: Function
|
|
541
|
+
}
|
package/src/Test.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Timer } from "./utilities/Timer.js"
|
|
2
|
+
import { jsonSample2 } from "./test-data/TestData.js"
|
|
3
|
+
import { anyOf, buildGrammar, parse } from "./GrammarComposer.js"
|
|
4
|
+
import { JsonGrammar } from "./test-grammars/JsonGrammar.js"
|
|
5
|
+
import { XmlGrammar } from "./test-grammars/XmlGrammar.js"
|
|
6
|
+
|
|
7
|
+
const log = console.log
|
|
8
|
+
|
|
9
|
+
function testBasic() {
|
|
10
|
+
class MyGrammar {
|
|
11
|
+
p1 = () => ['a', 'b', 'c', anyOf(this.p2, this.p3)]
|
|
12
|
+
|
|
13
|
+
p2 = () => ['x', this.p4, 'z']
|
|
14
|
+
|
|
15
|
+
p3 = () => ['x', this.p4, 'z', 'u']
|
|
16
|
+
|
|
17
|
+
p4 = () => ['y']
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const grammar = buildGrammar(MyGrammar, 'p1')
|
|
21
|
+
|
|
22
|
+
const result = parse('abcxyzu', grammar)
|
|
23
|
+
|
|
24
|
+
console.log(JSON.stringify(result, undefined, 4))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
function testJsonParser() {
|
|
29
|
+
const jsonString = jsonSample2
|
|
30
|
+
|
|
31
|
+
const grammar = buildGrammar(JsonGrammar, 'expression')
|
|
32
|
+
|
|
33
|
+
function run() {
|
|
34
|
+
const timer = new Timer()
|
|
35
|
+
const result1 = parse(jsonString, grammar)
|
|
36
|
+
timer.logAndRestart('Parse')
|
|
37
|
+
|
|
38
|
+
const result2 = JSON.parse(jsonString)
|
|
39
|
+
timer.logAndRestart('JSON.Parse')
|
|
40
|
+
|
|
41
|
+
log(JSON.stringify(result2, undefined, 4))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < 1; i++) {
|
|
45
|
+
run()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function testXmlParser() {
|
|
50
|
+
const xmlString = `
|
|
51
|
+
<!DOCTYPE web-app>
|
|
52
|
+
|
|
53
|
+
<menu>
|
|
54
|
+
<header>Adobe SVG Viewer</header>
|
|
55
|
+
<item action="Open" id="Open">Open</item>
|
|
56
|
+
<item action="OpenNew" id="OpenNew">Open New</item>
|
|
57
|
+
<separator/>
|
|
58
|
+
<item action="ZoomIn" id="ZoomIn">Zoom In</item>
|
|
59
|
+
<item action="ZoomOut" id="ZoomOut">Zoom Out</item>
|
|
60
|
+
<separator/>
|
|
61
|
+
<item action="Quality" id="Quality">Quality</item>
|
|
62
|
+
<item action="Pause" id="Pause">Pause</item>
|
|
63
|
+
<item action="Mute" id="Mute">Mute</item>
|
|
64
|
+
<separator/>
|
|
65
|
+
<item action="Find" id="Find">Find...</item>
|
|
66
|
+
<item action="FindAgain" id="FindAgain">Find Again</item>
|
|
67
|
+
<item action="Copy" id="Copy">Copy</item>
|
|
68
|
+
</menu>
|
|
69
|
+
|
|
70
|
+
`
|
|
71
|
+
// Build the grammar. 'document' is the starting production
|
|
72
|
+
const grammar = buildGrammar(XmlGrammar, 'document')
|
|
73
|
+
|
|
74
|
+
// Parse the XML string
|
|
75
|
+
const parseTree = parse(xmlString, grammar)
|
|
76
|
+
|
|
77
|
+
log(JSON.stringify(parseTree, undefined, 4))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async function testParserError1() {
|
|
82
|
+
const xmlData = `<hello> wo rld <!!! `
|
|
83
|
+
|
|
84
|
+
const grammar = buildGrammar(XmlGrammar, 'document')
|
|
85
|
+
|
|
86
|
+
const result = parse(xmlData, grammar)
|
|
87
|
+
|
|
88
|
+
console.log(JSON.stringify(result, undefined, 4))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function testParserError2() {
|
|
92
|
+
const jsonData = `{ "asdf": 12.5 `
|
|
93
|
+
|
|
94
|
+
const grammar = buildGrammar(JsonGrammar, 'expression')
|
|
95
|
+
|
|
96
|
+
const result = parse(jsonData, grammar)
|
|
97
|
+
|
|
98
|
+
console.log(JSON.stringify(result, undefined, 4))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
testXmlParser()
|