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