cddl 0.16.0 → 0.18.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 (44) hide show
  1. package/LICENSE +1 -1
  2. package/package.json +5 -1
  3. package/.release-it.ts +0 -10
  4. package/docs/README.md +0 -47
  5. package/docs/arrays.md +0 -356
  6. package/docs/ast-structure.md +0 -166
  7. package/docs/basic-types.md +0 -243
  8. package/docs/cddl-cheatsheet.md +0 -231
  9. package/docs/comments.md +0 -307
  10. package/docs/groups.md +0 -351
  11. package/docs/operators.md +0 -466
  12. package/docs/properties.md +0 -334
  13. package/docs/ranges.md +0 -339
  14. package/docs/references.md +0 -340
  15. package/docs/variables.md +0 -335
  16. package/src/ast.ts +0 -180
  17. package/src/cli/commands/repl.ts +0 -33
  18. package/src/cli/commands/validate.ts +0 -44
  19. package/src/cli/constants.ts +0 -1
  20. package/src/cli/index.ts +0 -18
  21. package/src/constants.ts +0 -16
  22. package/src/index.ts +0 -12
  23. package/src/lexer.ts +0 -238
  24. package/src/parser.ts +0 -993
  25. package/src/tokens.ts +0 -50
  26. package/src/utils.ts +0 -96
  27. package/tests/__snapshots__/complex_types.test.ts.snap +0 -80
  28. package/tests/__snapshots__/examples.test.ts.snap +0 -1077
  29. package/tests/__snapshots__/group-choices.test.ts.snap +0 -403
  30. package/tests/__snapshots__/parser.test.ts.snap +0 -3045
  31. package/tests/__snapshots__/webdriver-local.test.ts.snap +0 -11192
  32. package/tests/__snapshots__/webdriver-remote.test.ts.snap +0 -16350
  33. package/tests/commands/repl.test.ts +0 -52
  34. package/tests/commands/validate.test.ts +0 -66
  35. package/tests/complex_types.test.ts +0 -18
  36. package/tests/examples.test.ts +0 -25
  37. package/tests/group-choices.test.ts +0 -132
  38. package/tests/lexer.test.ts +0 -63
  39. package/tests/module.test.ts +0 -21
  40. package/tests/parser.test.ts +0 -63
  41. package/tests/utils.test.ts +0 -211
  42. package/tests/webdriver-local.test.ts +0 -15
  43. package/tests/webdriver-remote.test.ts +0 -15
  44. package/tsconfig.json +0 -11
package/src/parser.ts DELETED
@@ -1,993 +0,0 @@
1
- import fs from 'node:fs'
2
-
3
- import Lexer from './lexer.js'
4
- import { Property } from './ast.js'
5
- import { Token, Tokens } from './tokens.js';
6
- import { PREDEFINED_IDENTIFIER, BOOLEAN_LITERALS } from './constants.js'
7
- import { parseNumberValue } from './utils.js'
8
- import {
9
- Type, PropertyName, PropertyType, PropertyReferenceType,
10
- Variable, RangePropertyReference, Occurrence, Assignment,
11
- Comment, Group, OperatorType, NativeTypeWithOperator, Operator
12
- } from './ast.js'
13
-
14
- const NIL_TOKEN: Token = { Type: Tokens.ILLEGAL, Literal: '' }
15
- const DEFAULT_OCCURRENCE: Occurrence = { n: 1, m: 1 } // exactly one time
16
- const OPERATORS: OperatorType[] = ['default', 'size', 'regexp', 'bits', 'and', 'within', 'eq', 'ne', 'lt', 'le', 'gt', 'ge']
17
- const OPERATORS_EXPECTING_VALUES: Record<OperatorType, PropertyReferenceType[] | undefined> = {
18
- default: undefined,
19
- size: ['literal', 'range'],
20
- regexp: ['literal'],
21
- bits: ['group'],
22
- and: ['group'],
23
- within: ['group'],
24
- eq: ['group'],
25
- ne: ['group'],
26
- lt: ['group'],
27
- le: ['group'],
28
- gt: ['group'],
29
- ge: ['group'],
30
- }
31
-
32
- export default class Parser {
33
- #filePath: string
34
- l: Lexer;
35
-
36
- curToken: Token = NIL_TOKEN;
37
- peekToken: Token = NIL_TOKEN;
38
- peekBelowToken: Token = NIL_TOKEN;
39
-
40
- constructor (filePath: string) {
41
- this.#filePath = filePath
42
- this.l = new Lexer(fs.readFileSync(filePath, 'utf-8'))
43
-
44
- this.nextToken()
45
- this.nextToken()
46
- this.nextToken()
47
- }
48
-
49
- private nextToken () {
50
- this.curToken = this.peekToken
51
- this.peekToken = this.peekBelowToken
52
- this.peekBelowToken = this.l.nextToken()
53
- return true
54
- }
55
-
56
- private parseAssignments (): Assignment {
57
- const comments: Comment[] = []
58
- while (this.curToken.Type === Tokens.COMMENT) {
59
- const comment = this.parseComment()
60
- if (comment) {
61
- comments.push(comment)
62
- }
63
- }
64
-
65
- /**
66
- * expect group identifier, e.g.
67
- * groupName =
68
- * groupName /=
69
- * groupName //=
70
- */
71
- if (this.curToken.Type !== Tokens.IDENT || !(this.peekToken.Type === Tokens.ASSIGN || this.peekToken.Type === Tokens.SLASH)) {
72
- throw this.parserError(`group identifier expected, received "${JSON.stringify(this.curToken)}"`)
73
- }
74
-
75
- let isChoiceAddition = false
76
- const groupName = this.curToken.Literal
77
- this.nextToken() // eat group identifier
78
-
79
- // @ts-ignore
80
- if (this.curToken.Type === Tokens.SLASH) {
81
- isChoiceAddition = true
82
- this.nextToken() // eat `/`
83
- }
84
-
85
- // @ts-ignore
86
- if (this.curToken.Type === Tokens.SLASH) {
87
- this.nextToken() // eat `/`
88
- }
89
-
90
- this.nextToken() // eat `=`
91
- const assignmentValue = this.parseAssignmentValue(groupName, isChoiceAddition) as Assignment
92
-
93
- // @ts-expect-error curToken can be changed by now but TS doesn't understand this
94
- while (this.curToken.Type === Tokens.COMMENT) {
95
- const comment = this.parseComment()
96
- comment && comments.push(comment)
97
- }
98
- assignmentValue.Comments = comments
99
- return assignmentValue
100
- }
101
-
102
- private parseAssignmentValue (groupName?: string, isChoiceAddition = false): Assignment | PropertyType[] {
103
- let isChoice = false
104
- const valuesOrProperties: (Property | Property[])[] = []
105
- const closingTokens = this.openSegment()
106
-
107
- /**
108
- * if no group segment was opened we have a variable assignment
109
- * and can return immediatelly, e.g.
110
- *
111
- * attire = "bow tie" / "necktie" / "Internet attire"
112
- *
113
- */
114
- if (closingTokens.length === 0) {
115
- if (groupName) {
116
- const variable: Variable = {
117
- Type: 'variable',
118
- Name: groupName,
119
- IsChoiceAddition: isChoiceAddition,
120
- PropertyType: this.parsePropertyTypes(),
121
- Comments: []
122
- }
123
-
124
- return variable
125
- }
126
-
127
- return this.parsePropertyTypes()
128
- }
129
-
130
- /**
131
- * type or group choices can be wrapped within `(` and `)`, e.g.
132
- *
133
- * attireBlock = (
134
- * "bow tie" /
135
- * "necktie" /
136
- * "Internet attire"
137
- * )
138
- * attireGroup = (
139
- * attire //
140
- * attireBlock
141
- * )
142
- */
143
- if (
144
- closingTokens.includes(Tokens.RPAREN) &&
145
- this.peekToken.Type === Tokens.SLASH &&
146
- this.peekBelowToken.Type !== Tokens.SLASH &&
147
- !(this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH)
148
- ) {
149
- const propertyType: PropertyType[] = []
150
- while (!closingTokens.includes(this.curToken.Type)) {
151
- propertyType.push(...this.parsePropertyTypes())
152
- if (closingTokens.includes(this.curToken.Type)) {
153
- this.nextToken()
154
- break
155
- }
156
-
157
- this.nextToken()
158
-
159
- if (this.curToken.Type === Tokens.SLASH) {
160
- this.nextToken()
161
- }
162
- }
163
- if (this.curToken.Type === Tokens.RPAREN) {
164
- this.nextToken();
165
- }
166
- if (groupName) {
167
- const variable: Variable = {
168
- Type: 'variable',
169
- Name: groupName,
170
- IsChoiceAddition: isChoiceAddition,
171
- PropertyType: propertyType,
172
- Comments: []
173
- }
174
-
175
- if (this.isOperator()) {
176
- variable.Operator = this.parseOperator()
177
- }
178
-
179
- return variable
180
- }
181
-
182
- return propertyType
183
- }
184
-
185
- /**
186
- * parse operator assignments, e.g. `ip4 = (float .ge 0.0) .default 1.0`
187
- */
188
- if (closingTokens.length === 1 && this.peekToken.Type === Tokens.DOT) {
189
- const propertyType = this.parsePropertyType()
190
- const operator = this.isOperator() ? this.parseOperator() : undefined
191
- const prop: PropertyType = {
192
- Type: propertyType,
193
- ...(operator ? { Operator: operator } : {})
194
- } as NativeTypeWithOperator
195
-
196
- this.nextToken() // eat closing token
197
- if (groupName) {
198
- const variable: Variable = {
199
- Type: 'variable',
200
- Name: groupName,
201
- IsChoiceAddition: isChoiceAddition,
202
- PropertyType: prop,
203
- Operator: this.parseOperator(),
204
- Comments: []
205
- }
206
-
207
- return variable
208
- }
209
-
210
- return [prop]
211
- }
212
-
213
- while (!closingTokens.includes(this.curToken.Type)) {
214
- /**
215
- * check if we have a group choice instead of an assignment
216
- */
217
- if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) {
218
- if (valuesOrProperties.length === 0) {
219
- throw this.parserError('Unexpected group choice operator "//" at start of group')
220
- }
221
-
222
- if (!isChoice) {
223
- const last = valuesOrProperties.pop() as Property
224
- valuesOrProperties.push([last])
225
- isChoice = true
226
- }
227
- this.nextToken()
228
- this.nextToken()
229
- continue
230
- }
231
-
232
- const propertyType: PropertyType[] = []
233
- const comments: Comment[] = []
234
- let isUnwrapped = false
235
- let hasCut = false
236
- let propertyName = ''
237
-
238
- const leadingComment = this.parseComment(true)
239
- leadingComment && comments.push(leadingComment)
240
-
241
- const occurrence = this.parseOccurrences()
242
-
243
- /**
244
- * check if variable name is unwrapped
245
- */
246
- if (this.curToken.Literal === Tokens.TILDE) {
247
- isUnwrapped = true
248
- this.nextToken() // eat ~
249
- }
250
-
251
- /**
252
- * parse assignment within array, e.g.
253
- * ```
254
- * ActionsPerformActionsParameters = [1* {
255
- * type: "key",
256
- * id: text,
257
- * actions: ActionItems,
258
- * *text => any
259
- * }]
260
- * ```
261
- * or
262
- * ```
263
- * script.MappingRemoteValue = [*[(script.RemoteValue / text), script.RemoteValue]];
264
- * ```
265
- */
266
- if (
267
- this.curToken.Literal === Tokens.LBRACE ||
268
- this.curToken.Literal === Tokens.LBRACK ||
269
- this.curToken.Literal === Tokens.LPAREN
270
- ) {
271
- const innerGroup = this.parseAssignmentValue() as Group
272
- const prop = {
273
- HasCut: false,
274
- Occurrence: occurrence,
275
- Name: '',
276
- Type: innerGroup,
277
- Comments: []
278
- }
279
-
280
- if (isChoice) {
281
- (valuesOrProperties[valuesOrProperties.length - 1] as Property[]).push(prop)
282
- } else {
283
- valuesOrProperties.push(prop)
284
- }
285
-
286
- if (this.curToken.Type === Tokens.COMMA) {
287
- this.nextToken()
288
- isChoice = false
289
- }
290
-
291
- if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type !== Tokens.SLASH) {
292
- if (!isChoice) {
293
- const last = valuesOrProperties.pop() as Property
294
- valuesOrProperties.push([last])
295
- isChoice = true
296
- }
297
- this.nextToken()
298
- }
299
- continue
300
- }
301
-
302
- /**
303
- * check if we are in an array and a new item is indicated
304
- */
305
- if (this.curToken.Literal === Tokens.COMMA && closingTokens[0] === Tokens.RBRACK) {
306
- this.nextToken()
307
- continue
308
- }
309
-
310
- propertyName = this.parsePropertyName()
311
-
312
- /**
313
- * if `,` is found we have a group reference and jump to the next line
314
- */
315
- if (this.curToken.Type === Tokens.COMMA || closingTokens.includes(this.curToken.Type)) {
316
- const tokenType = this.curToken.Type
317
- let parsedComments = false
318
- let comment: Comment | undefined
319
-
320
- /**
321
- * check if line has a comment
322
- */
323
- if (this.curToken.Type === Tokens.COMMA && this.peekToken.Type === Tokens.COMMENT) {
324
- this.nextToken()
325
- comment = this.parseComment()
326
- parsedComments = true
327
- }
328
-
329
- valuesOrProperties.push({
330
- HasCut: hasCut,
331
- Occurrence: occurrence,
332
- Name: '',
333
- Type: PREDEFINED_IDENTIFIER.includes(propertyName)
334
- ? propertyName
335
- : [{
336
- Type: 'group' as PropertyReferenceType,
337
- Value: propertyName,
338
- Unwrapped: isUnwrapped
339
- }],
340
- Comments: comment ? [comment] : []
341
- })
342
-
343
- if (this.curToken.Literal === Tokens.COMMA || this.curToken.Literal === closingTokens[0]) {
344
- if (this.curToken.Literal === Tokens.COMMA) {
345
- this.nextToken()
346
- }
347
- continue
348
- }
349
-
350
- if (!parsedComments) {
351
- this.nextToken()
352
- }
353
-
354
- /**
355
- * only continue if next token contains a comma
356
- */
357
- if (tokenType === Tokens.COMMA) {
358
- continue
359
- }
360
-
361
- /**
362
- * otherwise break
363
- */
364
- break
365
- }
366
-
367
- /**
368
- * check if property has cut, which happens if a property is described as
369
- * - `? "optional-key" ^ => int,`
370
- * - `? optional-key: int,` - since the colon shortcut includes cuts
371
- */
372
- if (this.curToken.Type === Tokens.CARET || this.curToken.Type === Tokens.COLON) {
373
- hasCut = true
374
-
375
- if (this.curToken.Type === Tokens.CARET) {
376
- this.nextToken() // eat ^
377
- }
378
- }
379
-
380
- /**
381
- * check if we have a group choice instead of an assignment
382
- */
383
- if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) {
384
- const prop: Property = {
385
- HasCut: hasCut,
386
- Occurrence: occurrence,
387
- Name: '',
388
- Type: {
389
- Type: 'group',
390
- Value: propertyName,
391
- Unwrapped: isUnwrapped
392
- },
393
- Comments: comments
394
- }
395
-
396
- if (isChoice) {
397
- /**
398
- * if we already in a choice just push into it
399
- */
400
- (valuesOrProperties[valuesOrProperties.length - 1] as Property[]).push(prop)
401
- } else {
402
- /**
403
- * otherwise create a new one
404
- */
405
- isChoice = true
406
- valuesOrProperties.push([prop])
407
- }
408
-
409
- this.nextToken() // eat /
410
- this.nextToken() // eat /
411
- continue
412
- }
413
-
414
- /**
415
- * else if no colon was found, throw
416
- */
417
- if (!this.isPropertyValueSeparator()) {
418
- throw this.parserError('Expected ":" or "=>"')
419
- }
420
-
421
- this.nextToken() // eat :
422
-
423
- /**
424
- * parse property value
425
- */
426
- const props = this.parseAssignmentValue()
427
- let operator = this.isOperator() ? this.parseOperator() : undefined
428
- if (!isChoice && this.curToken.Type === Tokens.SLASH && this.peekToken.Type !== Tokens.SLASH) {
429
- this.nextToken()
430
- const nextType = this.parsePropertyType()
431
- if (Array.isArray(props)) {
432
- /**
433
- * property has not yet been flagged as a choice, but is part
434
- * of one, e.g. `(float .ge 1.0) / null`
435
- */
436
- props.push(nextType)
437
- if (!this.isOperator()) {
438
- this.nextToken()
439
- }
440
- }
441
- }
442
- if (this.isOperator()) {
443
- operator = this.parseOperator()
444
- }
445
- if (Array.isArray(props)) {
446
- /**
447
- * property has multiple types (e.g. `float / tstr / int`)
448
- */
449
- propertyType.push(...props)
450
- } else {
451
- propertyType.push(props)
452
- }
453
-
454
- /**
455
- * advance comma
456
- */
457
- let flipIsChoice = false
458
- if (this.curToken.Type === Tokens.COMMA) {
459
- /**
460
- * if we are in a choice, we leave it here
461
- */
462
- flipIsChoice = true
463
-
464
- this.nextToken() // eat ,
465
- }
466
-
467
- const trailingComment = this.parseComment()
468
- trailingComment && comments.push(trailingComment)
469
-
470
- const prop = {
471
- HasCut: hasCut,
472
- Occurrence: occurrence,
473
- Name: propertyName,
474
- Type: propertyType,
475
- Comments: comments,
476
- ...(operator ? { Operator: operator } : {})
477
- }
478
-
479
- if (isChoice) {
480
- (valuesOrProperties[valuesOrProperties.length - 1] as Property[]).push(prop)
481
- } else {
482
- valuesOrProperties.push(prop)
483
- }
484
-
485
- if (flipIsChoice) {
486
- isChoice = false
487
- }
488
-
489
- /**
490
- * if `}` is found we are at the end of the group
491
- */
492
- if (closingTokens.includes(this.curToken.Type)) {
493
- /**
494
- * Handle the case where a group is followed by an inclusion operator, e.g.
495
- *
496
- * group1 = {
497
- * name: tstr,
498
- * age: number,
499
- * }
500
- *
501
- * group2 = {
502
- * handle: tstr
503
- * } .and group1
504
- *
505
- */
506
- while (this.peekToken.Type === Tokens.DOT) {
507
- this.nextToken()
508
- if (this.isOperator()) {
509
- valuesOrProperties.push({
510
- Name: '',
511
- Type: 'group',
512
- Occurrence: DEFAULT_OCCURRENCE,
513
- Operator: this.parseOperator(),
514
- Comments: [],
515
- HasCut: false,
516
- })
517
- }
518
- }
519
- break
520
- }
521
-
522
- /**
523
- * eat // if we are in a choice
524
- */
525
- if (isChoice) {
526
- this.nextToken() // eat /
527
- this.nextToken() // eat /
528
- continue
529
- }
530
- }
531
-
532
- /**
533
- * close segment
534
- */
535
- if (this.curToken.Type === [...closingTokens].shift()) {
536
- this.nextToken()
537
- }
538
-
539
- /**
540
- * if last closing token is "]" we have an array
541
- */
542
- if (closingTokens[closingTokens.length - 1] === Tokens.RBRACK) {
543
- return {
544
- Type: 'array',
545
- Name: groupName || '',
546
- Values: valuesOrProperties,
547
- Comments: []
548
- }
549
- }
550
-
551
- /**
552
- * simplify wrapped types, e.g. from
553
- * {
554
- * "Type": "group",
555
- * "Name": "",
556
- * "Properties": [
557
- * {
558
- * "HasCut": false,
559
- * "Occurrence": {
560
- * "n": 1,
561
- * "m": 1
562
- * },
563
- * "Name": "",
564
- * "Type": "bool",
565
- * "Comment": ""
566
- * }
567
- * ],
568
- * "IsChoiceAddition": false
569
- * }
570
- * back to:
571
- * bool
572
- */
573
- if (!groupName && valuesOrProperties.length === 1 && PREDEFINED_IDENTIFIER.includes((valuesOrProperties[0] as Property).Type as string)) {
574
- return (valuesOrProperties[0] as Property).Type as Assignment
575
- }
576
-
577
- /**
578
- * otherwise a group
579
- */
580
- return {
581
- Type: 'group',
582
- Name: groupName || '',
583
- Properties: valuesOrProperties,
584
- IsChoiceAddition: isChoiceAddition,
585
- Comments: []
586
- }
587
- }
588
-
589
- private isPropertyValueSeparator () {
590
- if (this.curToken.Type === Tokens.COLON) {
591
- return true
592
- }
593
-
594
- if (this.curToken.Type === Tokens.ASSIGN && this.peekToken.Type === Tokens.GT) {
595
- this.nextToken() // eat <
596
- return true
597
- }
598
-
599
- return false
600
- }
601
-
602
- /**
603
- * checks if group segment is opened and forwards to beginning of
604
- * first property declaration
605
- * @returns {String[]} closing tokens for group (either `}`, `)` or both)
606
- */
607
- private openSegment (): string[] {
608
- if (this.curToken.Type === Tokens.LBRACE) {
609
- this.nextToken()
610
- return [Tokens.RBRACE]
611
- } else if (this.curToken.Type === Tokens.LPAREN) {
612
- this.nextToken()
613
- return [Tokens.RPAREN]
614
- } else if (this.curToken.Type === Tokens.LBRACK) {
615
- this.nextToken()
616
- return [Tokens.RBRACK]
617
- }
618
-
619
- return []
620
- }
621
-
622
- private parsePropertyName (): PropertyName {
623
- /**
624
- * property name without quotes
625
- */
626
- if (this.curToken.Type === Tokens.IDENT || this.curToken.Type === Tokens.STRING) {
627
- const name = this.curToken.Literal
628
- this.nextToken()
629
- return name
630
- }
631
-
632
- throw this.parserError(`Expected property name, received ${this.curToken.Type}(${this.curToken.Literal}), ${this.peekToken.Type}(${this.peekToken.Literal})`)
633
- }
634
-
635
- private parsePropertyType (): PropertyType {
636
- let type: PropertyType | undefined = undefined
637
- let isUnwrapped = false
638
- let isGroupedRange = false
639
-
640
- /**
641
- * check if variable name is unwrapped
642
- */
643
- if (this.curToken.Literal === Tokens.TILDE) {
644
- isUnwrapped = true
645
- this.nextToken() // eat ~
646
- }
647
-
648
- switch (this.curToken.Literal) {
649
- case Type.ANY:
650
- case Type.BOOL:
651
- case Type.INT:
652
- case Type.UINT:
653
- case Type.NINT:
654
- case Type.FLOAT:
655
- case Type.FLOAT16:
656
- case Type.FLOAT32:
657
- case Type.FLOAT64:
658
- case Type.BSTR:
659
- case Type.BYTES:
660
- case Type.TSTR:
661
- case Type.TEXT:
662
- case Type.NIL:
663
- case Type.NULL:
664
- type = this.curToken.Literal
665
- break
666
- default: {
667
- if (BOOLEAN_LITERALS.includes(this.curToken.Literal)) {
668
- type = {
669
- Type: 'literal' as PropertyReferenceType,
670
- Value: this.curToken.Literal === 'true',
671
- Unwrapped: isUnwrapped
672
- }
673
- } else if (this.curToken.Literal === Tokens.LBRACE || this.curToken.Literal === Tokens.LBRACK) {
674
- const val = this.parseAssignmentValue()
675
- if (Array.isArray(val)) {
676
- throw new Error('Unexpected array in property type parsing')
677
- }
678
- type = val
679
- } else if (this.curToken.Type === Tokens.IDENT) {
680
- type = {
681
- Type: 'group' as PropertyReferenceType,
682
- Value: this.curToken.Literal,
683
- Unwrapped: isUnwrapped
684
- }
685
- } else if (this.curToken.Type === Tokens.STRING) {
686
- type = {
687
- Type: 'literal' as PropertyReferenceType,
688
- Value: this.curToken.Literal,
689
- Unwrapped: isUnwrapped
690
- }
691
- } else if (this.curToken.Type === Tokens.NUMBER || this.curToken.Type === Tokens.FLOAT) {
692
- type = {
693
- Type: 'literal' as PropertyReferenceType,
694
- Value: parseNumberValue(this.curToken),
695
- Unwrapped: isUnwrapped
696
- }
697
- } else if (this.curToken.Type === Tokens.HASH) {
698
- this.nextToken()
699
- const n = parseNumberValue(this.curToken)
700
- this.nextToken() // eat numeric value
701
- this.nextToken() // eat (
702
- const t = this.parsePropertyType()
703
- this.nextToken() // eat )
704
- type = {
705
- Type: 'tag',
706
- Value: {
707
- NumericPart: n as number,
708
- TypePart: t as string
709
- },
710
- Unwrapped: isUnwrapped
711
- }
712
- } else if (this.curToken.Literal === Tokens.LPAREN && this.peekToken.Type === Tokens.NUMBER) {
713
- this.nextToken()
714
- type = {
715
- Type: 'literal' as PropertyReferenceType,
716
- Value: parseNumberValue(this.curToken),
717
- Unwrapped: isUnwrapped
718
- }
719
- isGroupedRange = true
720
- } else {
721
- throw this.parserError(`Invalid property type "${this.curToken.Literal}"`)
722
- }
723
- }
724
- }
725
-
726
- /**
727
- * check if type continue as a range
728
- */
729
- if (
730
- this.peekToken.Type === Tokens.DOT &&
731
- this.nextToken() &&
732
- this.peekToken.Type === Tokens.DOT
733
- ) {
734
- this.nextToken()
735
- let Inclusive = true
736
-
737
- /**
738
- * check if range excludes upper bound
739
- */
740
- if (this.peekToken.Type === Tokens.DOT) {
741
- Inclusive = false
742
- this.nextToken()
743
- }
744
-
745
- this.nextToken()
746
-
747
- if (!type || (typeof type === 'object' && !('Value' in type))) {
748
- throw new Error('Invalid type for range definition')
749
- }
750
-
751
- const Min: RangePropertyReference = typeof type === 'string' || typeof type.Value === 'number'
752
- ? type as string
753
- : type.Value as (number | string)
754
- type = {
755
- Type: 'range' as PropertyReferenceType,
756
- Value: {
757
- Inclusive,
758
- Min,
759
- Max: this.parsePropertyType() as RangePropertyReference
760
- },
761
- Unwrapped: isUnwrapped
762
- }
763
-
764
- if (!isGroupedRange && this.peekToken.Literal === Tokens.RPAREN) {
765
- /**
766
- * If we are at the end of a grouped range, and this was called
767
- * on the first item of the range as opposed to the opening
768
- * parenthesis, isGroupedRange will not be set to true at this
769
- * point. We need to advance to the closing parenthesis, and if
770
- * the next token is an operator, we need to advance to the dot
771
- * so that parseOperator will work properly.
772
- * e.g.
773
- *
774
- * ```
775
- * (1.0..2.0) .default 1.5
776
- * ```
777
- *
778
- * This will be called on the `1.0` and then the `2.0` will be parsed
779
- * as a grouped range.
780
- */
781
- this.nextToken()
782
- if (this.isOperator()) {
783
- isGroupedRange = true
784
- }
785
- }
786
-
787
- if (isGroupedRange) {
788
- this.nextToken() // eat ")"
789
- }
790
- }
791
-
792
- if (!type) {
793
- const { line, position: column } = this.l.getLocation()
794
- throw new Error(`Unexpected type: ${this.curToken.Type} at line ${line} column ${column}`)
795
- }
796
-
797
- return type
798
- }
799
-
800
- private parseOperator (): Operator {
801
- const type = this.peekToken.Literal as OperatorType
802
- if (this.curToken.Literal !== Tokens.DOT || !OPERATORS.includes(this.peekToken.Literal as OperatorType)) {
803
- throw new Error(`Operator ".${type}", expects a ${OPERATORS_EXPECTING_VALUES[type]!.join(' or ')} property, but found ${this.peekToken.Literal}!`)
804
- }
805
-
806
- this.nextToken() // eat "."
807
- this.nextToken() // eat operator type
808
- const value = this.parsePropertyType() as PropertyReferenceType
809
- this.nextToken() // eat operator value
810
- return {
811
- Type: type,
812
- Value: value
813
- }
814
- }
815
-
816
- private isOperator () {
817
- return this.curToken.Literal === Tokens.DOT && OPERATORS.includes(this.peekToken.Literal as OperatorType)
818
- }
819
-
820
- private parsePropertyTypes (): PropertyType[] {
821
- const propertyTypes: PropertyType[] = []
822
-
823
- let prop: PropertyType = this.parsePropertyType()
824
- if (this.isOperator()) {
825
- prop = {
826
- Type: prop,
827
- Operator: this.parseOperator()
828
- } as NativeTypeWithOperator
829
- } else if (this.curToken.Type !== Tokens.SLASH) {
830
- this.nextToken() // eat Property if not already consumed (e.g. by Group parsing)
831
- }
832
-
833
- propertyTypes.push(prop)
834
-
835
- /**
836
- * ignore comments between type choice members, e.g.
837
- * ```
838
- * Foo = int ; comment
839
- * / text
840
- * ```
841
- * or
842
- * ```
843
- * Foo = int / ; comment
844
- * text
845
- * ```
846
- */
847
- while (this.curToken.Type === Tokens.COMMENT && this.peekToken.Type === Tokens.SLASH) {
848
- this.parseComment()
849
- }
850
-
851
- /**
852
- * ensure we don't go into the next choice, e.g.:
853
- * ```
854
- * delivery = (
855
- * city // lala: tstr / bool // per-pickup: true,
856
- * )
857
- */
858
- if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) {
859
- return propertyTypes
860
- }
861
-
862
- /**
863
- * capture more if available (e.g. `tstr / float / boolean`)
864
- */
865
- while (this.curToken.Type === Tokens.SLASH) {
866
- this.nextToken() // eat `/`
867
- while ([Tokens.COMMENT].includes(this.curToken.Type)) {
868
- this.parseComment()
869
- }
870
- propertyTypes.push(this.parsePropertyType())
871
- if (!this.isOperator() && this.curToken.Type !== Tokens.SLASH) {
872
- /**
873
- * If we are not parsing an operator, we need to eat the next token;
874
- * otherwise, the operator will be parsed by the caller
875
- */
876
- this.nextToken()
877
- }
878
-
879
- while ([Tokens.COMMENT].includes(this.curToken.Type) && this.peekToken.Type === Tokens.SLASH) {
880
- this.parseComment()
881
- }
882
-
883
- /**
884
- * ensure we don't go into the next choice, e.g.:
885
- * ```
886
- * delivery = (
887
- * city // lala: tstr / bool // per-pickup: true,
888
- * )
889
- */
890
- if (this.curToken.Type === Tokens.SLASH && this.peekToken.Type === Tokens.SLASH) {
891
- break
892
- }
893
- }
894
-
895
- return propertyTypes
896
- }
897
-
898
- private parseOccurrences () {
899
- let occurrence = DEFAULT_OCCURRENCE
900
-
901
- /**
902
- * check for non-numbered occurrence indicator, e.g.
903
- * ```
904
- * * bedroom: size,
905
- * ```
906
- * which is the same as:
907
- * ```
908
- * ? bedroom: size,
909
- * ```
910
- * or have miniumum of 1 occurrence
911
- * ```
912
- * + bedroom: size,
913
- * ```
914
- */
915
- if (this.curToken.Type === Tokens.QUEST || this.curToken.Type === Tokens.ASTERISK || this.curToken.Type === Tokens.PLUS) {
916
- const n = this.curToken.Type === Tokens.PLUS ? 1 : 0
917
- let m = Infinity
918
-
919
- /**
920
- * check if there is a max definition
921
- */
922
- if (this.peekToken.Type === Tokens.NUMBER) {
923
- m = parseInt(this.peekToken.Literal, 10)
924
- this.nextToken()
925
- }
926
-
927
- occurrence = { n, m }
928
- this.nextToken()
929
- /**
930
- * numbered occurrence indicator, e.g.
931
- * ```
932
- * 1*10 bedroom: size,
933
- * ```
934
- */
935
- } else if (
936
- this.curToken.Type === Tokens.NUMBER &&
937
- this.peekToken.Type === Tokens.ASTERISK
938
- ) {
939
- const n = parseInt(this.curToken.Literal, 10)
940
- let m = Infinity
941
- this.nextToken() // eat "n"
942
- this.nextToken() // eat "*"
943
-
944
- /**
945
- * check if there is a max definition
946
- */
947
- if (this.curToken.Type === Tokens.NUMBER) {
948
- m = parseInt(this.curToken.Literal, 10)
949
- this.nextToken()
950
- }
951
-
952
- occurrence = { n, m }
953
- }
954
-
955
- return occurrence
956
- }
957
-
958
- /**
959
- * check if line has a comment
960
- */
961
- private parseComment (isLeading?: boolean): Comment | undefined {
962
- if (this.curToken.Type !== Tokens.COMMENT) {
963
- return
964
- }
965
- const comment = this.curToken.Literal.replace(/^;(\s*)/, '')
966
- this.nextToken()
967
-
968
- if (comment.trim().length === 0) {
969
- return
970
- }
971
-
972
- return { Type: 'comment', Content: comment, Leading: Boolean(isLeading) }
973
- }
974
-
975
- parse () {
976
- const definition: Assignment[] = []
977
-
978
- while (this.curToken.Type !== Tokens.EOF) {
979
- const group = this.parseAssignments()
980
- if (group) {
981
- definition.push(group)
982
- }
983
- }
984
-
985
- return definition
986
- }
987
-
988
- private parserError (message: string) {
989
- const location = this.l.getLocation()
990
- const locInfo = this.l.getLocationInfo()
991
- return new Error(`${this.#filePath.replace(process.cwd(), '')}:${location.line + 1}:${location.position} - error: ${message}\n\n${locInfo}`)
992
- }
993
- }