cddl 0.13.1 → 0.14.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/.release-it.json +12 -0
- package/LICENSE +1 -1
- package/README.md +4 -4
- package/package.json +12 -26
- package/src/ast.ts +180 -0
- package/src/cli/commands/repl.ts +33 -0
- package/src/cli/commands/validate.ts +44 -0
- package/src/cli/constants.ts +1 -0
- package/src/cli/index.ts +18 -0
- package/src/constants.ts +16 -0
- package/src/index.ts +11 -0
- package/src/lexer.ts +238 -0
- package/src/parser.ts +967 -0
- package/src/tokens.ts +50 -0
- package/src/utils.ts +37 -0
- package/tests/__snapshots__/complex_types.test.ts.snap +80 -0
- package/tests/__snapshots__/examples.test.ts.snap +1077 -0
- package/tests/__snapshots__/group-choices.test.ts.snap +278 -0
- package/tests/__snapshots__/parser.test.ts.snap +3045 -0
- package/tests/__snapshots__/webdriver-local.test.ts.snap +11192 -0
- package/tests/__snapshots__/webdriver-remote.test.ts.snap +16378 -0
- package/tests/commands/repl.test.ts +52 -0
- package/tests/commands/validate.test.ts +66 -0
- package/tests/complex_types.test.ts +18 -0
- package/tests/examples.test.ts +25 -0
- package/tests/group-choices.test.ts +103 -0
- package/tests/lexer.test.ts +63 -0
- package/tests/module.test.ts +21 -0
- package/tests/parser.test.ts +44 -0
- package/tests/webdriver-local.test.ts +15 -0
- package/tests/webdriver-remote.test.ts +15 -0
- package/tsconfig.json +11 -0
package/.release-it.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"plugins": {
|
|
3
|
+
"release-it-pnpm": {}
|
|
4
|
+
},
|
|
5
|
+
"git": {
|
|
6
|
+
"requireCleanWorkingDir": false,
|
|
7
|
+
"addUntrackedFiles": true,
|
|
8
|
+
"tagName": "cddl-v${version}",
|
|
9
|
+
"commitMessage": "chore(cddl): release v${version}",
|
|
10
|
+
"changelog": "git log --pretty=format:\"* %s (%h)\" ${latestTag ? latestTag + '..HEAD' : ''} -- . ../../tsconfig.json"
|
|
11
|
+
}
|
|
12
|
+
}
|
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
CDDL
|
|
1
|
+
CDDL
|
|
2
2
|
====
|
|
3
3
|
|
|
4
4
|
> Concise data definition language ([RFC 8610](https://tools.ietf.org/html/rfc8610)) implementation and JSON validator in Node.js.
|
|
@@ -10,7 +10,7 @@ There are also CDDL parsers for other languages:
|
|
|
10
10
|
|
|
11
11
|
The package is currently mostly used to help generate typed interfaces for the WebDriver Bidi specification in the following projects:
|
|
12
12
|
- [WebdriverIO](https://webdriver.io) - via the [`cddl2ts`](https://www.npmjs.com/package/cddl2ts) package and [this script](https://github.com/webdriverio/webdriverio/blob/a2ae35332f9b3fc9490136df1ac3d2e14c1e35b6/scripts/bidi/index.ts)
|
|
13
|
-
- [Selenium](https://selenium.dev) - via the [`cddl2java`](https://github.com/
|
|
13
|
+
- [Selenium](https://selenium.dev) - via the [`cddl2java`](https://github.com/webdriverio/cddl2java) package
|
|
14
14
|
|
|
15
15
|
__Note:__ this is __work in progress__, feel free to have a look at the code or contribute but don't use this for anything yet!
|
|
16
16
|
|
|
@@ -66,8 +66,8 @@ console.log(ast)
|
|
|
66
66
|
*/
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
Read the full documentation on this AST in the [
|
|
69
|
+
Read the full documentation on this AST in the [`docs`](./docs/README.md) directory.
|
|
70
70
|
|
|
71
71
|
---
|
|
72
72
|
|
|
73
|
-
If you are interested in this project, please feel free to contribute ideas or code patches. Have a look at our [contributing guidelines](https://github.com/
|
|
73
|
+
If you are interested in this project, please feel free to contribute ideas or code patches. Have a look at our [contributing guidelines](https://github.com/webdriverio/cddl/blob/master/CONTRIBUTING.md) to get started.
|
package/package.json
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cddl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Concise data definition language (RFC 8610) implementation and JSON validator in Node.js",
|
|
5
5
|
"author": "Christian Bromann <mail@bromann.dev>",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"homepage": "https://github.com/
|
|
7
|
+
"homepage": "https://github.com/webdriverio/cddl#readme",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+ssh://git@github.com/
|
|
10
|
+
"url": "git+ssh://git@github.com/webdriverio/cddl.git"
|
|
11
11
|
},
|
|
12
12
|
"keywords": [
|
|
13
13
|
"cddl"
|
|
14
14
|
],
|
|
15
|
-
"engines": {
|
|
16
|
-
"node": ">=20.0.0",
|
|
17
|
-
"npm": ">=9.0.0"
|
|
18
|
-
},
|
|
19
15
|
"bugs": {
|
|
20
|
-
"url": "https://github.com/
|
|
16
|
+
"url": "https://github.com/webdriverio/cddl/issues"
|
|
21
17
|
},
|
|
22
18
|
"type": "module",
|
|
23
19
|
"exports": "./build/index.js",
|
|
@@ -25,29 +21,19 @@
|
|
|
25
21
|
"bin": {
|
|
26
22
|
"cddl": "./bin/cddl.js"
|
|
27
23
|
},
|
|
28
|
-
"scripts": {
|
|
29
|
-
"build": "run-s clean compile",
|
|
30
|
-
"clean": "rm -rf ./build ./coverage",
|
|
31
|
-
"compile": "tsc -p ./tsconfig.json",
|
|
32
|
-
"release": "release-it --github.release",
|
|
33
|
-
"release:ci": "npm run release -- --ci --npm.skipChecks --no-git.requireCleanWorkingDir",
|
|
34
|
-
"release:patch": "npm run release -- patch",
|
|
35
|
-
"release:minor": "npm run release -- minor",
|
|
36
|
-
"release:major": "npm run release -- major",
|
|
37
|
-
"test": "vitest run",
|
|
38
|
-
"checks:all": "npm run compile && npm run test",
|
|
39
|
-
"watch": "tsc --watch"
|
|
40
|
-
},
|
|
41
24
|
"devDependencies": {
|
|
42
|
-
"@types/node": "^
|
|
25
|
+
"@types/node": "^25.5.0",
|
|
43
26
|
"@types/yargs": "^17.0.35",
|
|
44
27
|
"@vitest/coverage-v8": "^4.1.0",
|
|
45
|
-
"npm-run-
|
|
46
|
-
"
|
|
47
|
-
"typescript": "^5.9.3",
|
|
28
|
+
"npm-run-all2": "^8.0.4",
|
|
29
|
+
"typescript": "^6.0.2",
|
|
48
30
|
"vitest": "^4.1.0"
|
|
49
31
|
},
|
|
50
32
|
"dependencies": {
|
|
51
33
|
"yargs": "^18.0.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"release": "release-it --github.release",
|
|
37
|
+
"release:ci": "pnpm release --ci --npm.skipChecks"
|
|
52
38
|
}
|
|
53
|
-
}
|
|
39
|
+
}
|
package/src/ast.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* a group definition
|
|
3
|
+
* ```
|
|
4
|
+
* person = {
|
|
5
|
+
* age: int,
|
|
6
|
+
* name: tstr,
|
|
7
|
+
* employer: tstr,
|
|
8
|
+
* }
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export type Group = {
|
|
12
|
+
Type: 'group'
|
|
13
|
+
Name: string
|
|
14
|
+
IsChoiceAddition: boolean
|
|
15
|
+
Properties: (Property|Property[])[]
|
|
16
|
+
Comments: Comment[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* an array definition
|
|
21
|
+
* ```
|
|
22
|
+
* Geography = [
|
|
23
|
+
* city: tstr
|
|
24
|
+
* ]
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export type Array = {
|
|
28
|
+
Type: 'array'
|
|
29
|
+
Name: string
|
|
30
|
+
Values: (Property|Property[])[]
|
|
31
|
+
Comments: Comment[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* a tag definition
|
|
36
|
+
* ```
|
|
37
|
+
* #6.32(tstr)
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export type Tag = {
|
|
41
|
+
NumericPart: number
|
|
42
|
+
TypePart: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* a variable assignment
|
|
47
|
+
* ```
|
|
48
|
+
* device-address = byte
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export type Variable = {
|
|
52
|
+
Type: 'variable'
|
|
53
|
+
Name: string
|
|
54
|
+
IsChoiceAddition: boolean
|
|
55
|
+
PropertyType: PropertyType | PropertyType[]
|
|
56
|
+
Operator?: Operator
|
|
57
|
+
Comments: Comment[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* a comment statement
|
|
62
|
+
* ```
|
|
63
|
+
* ; This is a comment
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export type Comment = {
|
|
67
|
+
Type: 'comment'
|
|
68
|
+
Content: string
|
|
69
|
+
Leading: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type Occurrence = {
|
|
73
|
+
n: number
|
|
74
|
+
m: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type Property = {
|
|
78
|
+
HasCut: boolean
|
|
79
|
+
Occurrence: Occurrence
|
|
80
|
+
Name: PropertyName
|
|
81
|
+
Type: PropertyType | PropertyType[]
|
|
82
|
+
Comments: Comment[]
|
|
83
|
+
Operator?: Operator
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export enum Type {
|
|
87
|
+
/**
|
|
88
|
+
* any types
|
|
89
|
+
*/
|
|
90
|
+
ANY = 'any',
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* boolean types
|
|
94
|
+
*/
|
|
95
|
+
// Boolean value (major type 7, additional information 20 or 21).
|
|
96
|
+
BOOL = 'bool',
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* numeric types
|
|
100
|
+
*/
|
|
101
|
+
// An unsigned integer or a negative integer.
|
|
102
|
+
INT = 'int',
|
|
103
|
+
// An unsigned integer (major type 0).
|
|
104
|
+
UINT = 'uint',
|
|
105
|
+
// A negative integer (major type 1).
|
|
106
|
+
NINT = 'nint',
|
|
107
|
+
// One of float16, float32, or float64.
|
|
108
|
+
FLOAT = 'float',
|
|
109
|
+
// A number representable as an IEEE 754 half-precision float
|
|
110
|
+
FLOAT16 = 'float16',
|
|
111
|
+
// A number representable as an IEEE 754 single-precision
|
|
112
|
+
FLOAT32 = 'float32',
|
|
113
|
+
// A number representable as an IEEE 754 double-precision
|
|
114
|
+
FLOAT64 = 'float64',
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* string types
|
|
118
|
+
*/
|
|
119
|
+
// A byte string (major type 2).
|
|
120
|
+
BSTR = 'bstr',
|
|
121
|
+
// A byte string (major type 2).
|
|
122
|
+
BYTES = 'bytes',
|
|
123
|
+
// Text string (major type 3)
|
|
124
|
+
TSTR = 'tstr',
|
|
125
|
+
// Text string (major type 3)
|
|
126
|
+
TEXT = 'text',
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* null types
|
|
130
|
+
*/
|
|
131
|
+
NIL = 'nil',
|
|
132
|
+
NULL = 'null'
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* can be a number, e.g. "foo = 0..10"
|
|
137
|
+
* ```
|
|
138
|
+
* {
|
|
139
|
+
* Type: "int",
|
|
140
|
+
* Value: 6
|
|
141
|
+
* }
|
|
142
|
+
* ```
|
|
143
|
+
* or a literal, e.g. "foo = 0..max-byte"
|
|
144
|
+
* ```
|
|
145
|
+
* {
|
|
146
|
+
* Type: "literal",
|
|
147
|
+
* Value: "max-byte"
|
|
148
|
+
* }
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export type RangePropertyReference = number | string
|
|
152
|
+
|
|
153
|
+
export type Range = {
|
|
154
|
+
Min: RangePropertyReference
|
|
155
|
+
Max: RangePropertyReference
|
|
156
|
+
Inclusive: boolean
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export type OperatorType = 'default' | 'size' | 'regexp' | 'bits' | 'and' | 'within' | 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge'
|
|
160
|
+
export interface Operator {
|
|
161
|
+
Type: OperatorType
|
|
162
|
+
Value: PropertyType
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export type PropertyReferenceType = 'literal' | 'group' | 'group_array' | 'array' | 'range' | 'tag'
|
|
166
|
+
export type PropertyReference = {
|
|
167
|
+
Type: PropertyReferenceType
|
|
168
|
+
Value: string | number | boolean | Group | Array | Range | Tag
|
|
169
|
+
Unwrapped: boolean
|
|
170
|
+
Operator?: Operator
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface NativeTypeWithOperator {
|
|
174
|
+
Type: Type | PropertyReference
|
|
175
|
+
Operator?: Operator
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export type Assignment = Group | Array | Variable
|
|
179
|
+
export type PropertyType = Assignment | Array | PropertyReference | string | NativeTypeWithOperator
|
|
180
|
+
export type PropertyName = string
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import repl from 'node:repl'
|
|
2
|
+
import type { Argv } from 'yargs'
|
|
3
|
+
|
|
4
|
+
import { CLI_EPILOGUE } from '../constants.js'
|
|
5
|
+
import Lexer from '../../lexer.js'
|
|
6
|
+
import { Tokens } from '../../tokens.js'
|
|
7
|
+
|
|
8
|
+
export const command = 'repl'
|
|
9
|
+
export const desc = 'Run CDDL repl'
|
|
10
|
+
export const builder = (yargs: Argv<{}>) => {
|
|
11
|
+
return yargs
|
|
12
|
+
.epilogue(CLI_EPILOGUE)
|
|
13
|
+
.help()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const handler = () => {
|
|
17
|
+
repl.start({
|
|
18
|
+
prompt: '> ',
|
|
19
|
+
eval: evaluate
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function evaluate (evalCmd: string, _: any, _file: string, callback: (err: Error | null, result: any) => void) {
|
|
24
|
+
if (!evalCmd) {
|
|
25
|
+
return callback(new Error('No input'), null)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const l = new Lexer(evalCmd)
|
|
29
|
+
for (let tok = l.nextToken(); tok.Type !== Tokens.EOF; tok = l.nextToken()) {
|
|
30
|
+
console.log(tok)
|
|
31
|
+
}
|
|
32
|
+
return callback(null, null)
|
|
33
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import type { Argv, ArgumentsCamelCase } from 'yargs'
|
|
4
|
+
|
|
5
|
+
import { CLI_EPILOGUE } from '../constants.js'
|
|
6
|
+
import { parse } from '../../index.js'
|
|
7
|
+
|
|
8
|
+
interface ValidateArguments {
|
|
9
|
+
filePath: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const command = 'validate <filePath>'
|
|
13
|
+
export const desc = 'Validate a *.cddl file'
|
|
14
|
+
export const builder = (yargs: Argv<{}>) => {
|
|
15
|
+
return yargs
|
|
16
|
+
.epilogue(CLI_EPILOGUE)
|
|
17
|
+
.help()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const handler = (argv: ArgumentsCamelCase<ValidateArguments>) => {
|
|
21
|
+
const filePath = argv.filePath.startsWith('/')
|
|
22
|
+
? argv.filePath
|
|
23
|
+
: path.resolve(process.cwd(), argv.filePath)
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(filePath)) {
|
|
26
|
+
console.error(`Couldn't find CDDL file at ${filePath}`)
|
|
27
|
+
return process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
parse(filePath)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* ToDo check for
|
|
36
|
+
* - missing group declarations
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
console.log('✅ Valid CDDL file!')
|
|
40
|
+
} catch (err: unknown) {
|
|
41
|
+
console.error(`⚠️ Invalid CDDL file (${filePath})\n\n${(err as Error).stack}`)
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CLI_EPILOGUE = `Copyright 2023 - Christian Bromann`
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import yargs from 'yargs/yargs'
|
|
2
|
+
import { hideBin } from 'yargs/helpers'
|
|
3
|
+
|
|
4
|
+
import * as replCommand from './commands/repl.js'
|
|
5
|
+
import * as validateCommand from './commands/validate.js'
|
|
6
|
+
import { CLI_EPILOGUE } from './constants.js'
|
|
7
|
+
|
|
8
|
+
export default async function () {
|
|
9
|
+
const argv = yargs(hideBin(process.argv))
|
|
10
|
+
.command(replCommand)
|
|
11
|
+
.command(validateCommand as any)
|
|
12
|
+
.example('$0 repl', 'Start CDDL repl')
|
|
13
|
+
.epilogue(CLI_EPILOGUE)
|
|
14
|
+
.demandCommand()
|
|
15
|
+
.help()
|
|
16
|
+
|
|
17
|
+
return argv.argv
|
|
18
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const WHITESPACE_CHARACTERS = [' ', '\t', '\n', '\r']
|
|
2
|
+
export const BOOLEAN_LITERALS = ['true', 'false']
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* as defined in Appendix D
|
|
6
|
+
* https://tools.ietf.org/html/draft-ietf-cbor-cddl-08#appendix-D
|
|
7
|
+
*/
|
|
8
|
+
export const PREDEFINED_IDENTIFIER = [
|
|
9
|
+
'any', 'uint', 'nint', 'int', 'bstr', 'bytes', 'tstr', 'text',
|
|
10
|
+
'tdate', 'time', 'number', 'biguint', 'bignint', 'bigint',
|
|
11
|
+
'integer', 'unsigned', 'decfrac', 'bigfloat', 'eb64url',
|
|
12
|
+
'eb64legacy', 'eb16', 'encoded-cbor', 'uri', 'b64url',
|
|
13
|
+
'b64legacy', 'regexp', 'mime-message', 'cbor-any', 'float16',
|
|
14
|
+
'float32', 'float64', 'float16-32', 'float32-64', 'float',
|
|
15
|
+
'false', 'true', 'bool', 'nil', 'null', 'undefined'
|
|
16
|
+
]
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import Lexer from './lexer.js'
|
|
2
|
+
import Parser from './parser.js'
|
|
3
|
+
|
|
4
|
+
export function parse (filePath: string) {
|
|
5
|
+
const parser = new Parser(filePath)
|
|
6
|
+
return parser.parse()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default { parse }
|
|
10
|
+
export { Lexer, Parser }
|
|
11
|
+
export * from './ast.js'
|
package/src/lexer.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { Token, Tokens } from './tokens.js';
|
|
2
|
+
import { isLetter, isAlphabeticCharacter, isDigit, hasSpecialNumberCharacter } from './utils.js'
|
|
3
|
+
import { WHITESPACE_CHARACTERS } from './constants.js';
|
|
4
|
+
|
|
5
|
+
export default class Lexer {
|
|
6
|
+
input: string
|
|
7
|
+
position: number = 0
|
|
8
|
+
readPosition: number = 0
|
|
9
|
+
ch: number = 0
|
|
10
|
+
|
|
11
|
+
constructor (source: string) {
|
|
12
|
+
this.input = source
|
|
13
|
+
|
|
14
|
+
this.readChar()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private readChar (): void {
|
|
18
|
+
if (this.readPosition >= this.input.length) {
|
|
19
|
+
this.ch = 0
|
|
20
|
+
} else {
|
|
21
|
+
this.ch = this.input[this.readPosition].charCodeAt(0)
|
|
22
|
+
}
|
|
23
|
+
this.position = this.readPosition
|
|
24
|
+
this.readPosition++
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getLocation () {
|
|
28
|
+
const position = this.position - 2
|
|
29
|
+
const sourceLines = this.input.split('\n')
|
|
30
|
+
const sourceLineLength = sourceLines.map((l) => l.length)
|
|
31
|
+
let i = 0
|
|
32
|
+
|
|
33
|
+
for (const [line, lineLength] of Object.entries(sourceLineLength)) {
|
|
34
|
+
i += lineLength + 1
|
|
35
|
+
if (i > position) {
|
|
36
|
+
const lineBegin = i - lineLength
|
|
37
|
+
return {
|
|
38
|
+
line: parseInt(line, 10),
|
|
39
|
+
position: position - lineBegin + 1
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { line: 0, position: 0 }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getLine (lineNumber: number) {
|
|
48
|
+
return this.input.split('\n')[lineNumber]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getLocationInfo () {
|
|
52
|
+
const loc = this.getLocation()
|
|
53
|
+
const line = loc ? this.getLine(loc.line) : ''
|
|
54
|
+
let locationInfo = line + '\n'
|
|
55
|
+
locationInfo += ' '.repeat(loc?.position || 0) + '^\n'
|
|
56
|
+
locationInfo += ' '.repeat(loc?.position || 0) + '|\n'
|
|
57
|
+
return locationInfo
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
nextToken (): Token {
|
|
61
|
+
let token: Token
|
|
62
|
+
this.skipWhitespace()
|
|
63
|
+
|
|
64
|
+
const Literal = String.fromCharCode(this.ch)
|
|
65
|
+
switch (this.ch) {
|
|
66
|
+
case '='.charCodeAt(0):
|
|
67
|
+
token = { Type: Tokens.ASSIGN, Literal }
|
|
68
|
+
break
|
|
69
|
+
case '('.charCodeAt(0):
|
|
70
|
+
token = { Type: Tokens.LPAREN, Literal }
|
|
71
|
+
break
|
|
72
|
+
case ')'.charCodeAt(0):
|
|
73
|
+
token = { Type: Tokens.RPAREN, Literal }
|
|
74
|
+
break
|
|
75
|
+
case '{'.charCodeAt(0):
|
|
76
|
+
token = { Type: Tokens.LBRACE, Literal }
|
|
77
|
+
break
|
|
78
|
+
case '}'.charCodeAt(0):
|
|
79
|
+
token = { Type: Tokens.RBRACE, Literal }
|
|
80
|
+
break
|
|
81
|
+
case '['.charCodeAt(0):
|
|
82
|
+
token = { Type: Tokens.LBRACK, Literal }
|
|
83
|
+
break
|
|
84
|
+
case ']'.charCodeAt(0):
|
|
85
|
+
token = { Type: Tokens.RBRACK, Literal }
|
|
86
|
+
break
|
|
87
|
+
case '<'.charCodeAt(0):
|
|
88
|
+
token = { Type: Tokens.LT, Literal }
|
|
89
|
+
break
|
|
90
|
+
case '>'.charCodeAt(0):
|
|
91
|
+
token = { Type: Tokens.GT, Literal }
|
|
92
|
+
break
|
|
93
|
+
case '+'.charCodeAt(0):
|
|
94
|
+
token = { Type: Tokens.PLUS, Literal }
|
|
95
|
+
break
|
|
96
|
+
case ','.charCodeAt(0):
|
|
97
|
+
token = { Type: Tokens.COMMA, Literal }
|
|
98
|
+
break
|
|
99
|
+
case '.'.charCodeAt(0):
|
|
100
|
+
token = { Type: Tokens.DOT, Literal }
|
|
101
|
+
break
|
|
102
|
+
case ':'.charCodeAt(0):
|
|
103
|
+
token = { Type: Tokens.COLON, Literal }
|
|
104
|
+
break
|
|
105
|
+
case '?'.charCodeAt(0):
|
|
106
|
+
token = { Type: Tokens.QUEST, Literal }
|
|
107
|
+
break
|
|
108
|
+
case '/'.charCodeAt(0):
|
|
109
|
+
token = { Type: Tokens.SLASH, Literal }
|
|
110
|
+
break
|
|
111
|
+
case '*'.charCodeAt(0):
|
|
112
|
+
token = { Type: Tokens.ASTERISK, Literal }
|
|
113
|
+
break
|
|
114
|
+
case '^'.charCodeAt(0):
|
|
115
|
+
token = { Type: Tokens.CARET, Literal }
|
|
116
|
+
break
|
|
117
|
+
case '#'.charCodeAt(0):
|
|
118
|
+
token = { Type: Tokens.HASH, Literal }
|
|
119
|
+
break
|
|
120
|
+
case '~'.charCodeAt(0):
|
|
121
|
+
token = { Type: Tokens.TILDE, Literal }
|
|
122
|
+
break
|
|
123
|
+
case '"'.charCodeAt(0):
|
|
124
|
+
token = { Type: Tokens.STRING, Literal: this.readString() }
|
|
125
|
+
break
|
|
126
|
+
case ';'.charCodeAt(0):
|
|
127
|
+
token = { Type: Tokens.COMMENT, Literal: this.readComment() }
|
|
128
|
+
break
|
|
129
|
+
case 0:
|
|
130
|
+
token = { Type: Tokens.EOF, Literal: '' }
|
|
131
|
+
break
|
|
132
|
+
default: {
|
|
133
|
+
if (isAlphabeticCharacter(Literal)) {
|
|
134
|
+
return { Type: Tokens.IDENT, Literal: this.readIdentifier() }
|
|
135
|
+
} else if (
|
|
136
|
+
// positive number
|
|
137
|
+
isDigit(Literal) ||
|
|
138
|
+
// negative number
|
|
139
|
+
(this.ch === Tokens.MINUS.charCodeAt(0) && isDigit(this.input[this.readPosition]))
|
|
140
|
+
) {
|
|
141
|
+
const numberOrFloat = this.readNumberOrFloat()
|
|
142
|
+
return {
|
|
143
|
+
Type: numberOrFloat.includes(Tokens.DOT) ? Tokens.FLOAT : Tokens.NUMBER,
|
|
144
|
+
Literal: numberOrFloat
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
token = { Type: Tokens.ILLEGAL, Literal: '' }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.readChar()
|
|
152
|
+
return token
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private readIdentifier (): string {
|
|
156
|
+
const position = this.position
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* an identifier can contain
|
|
160
|
+
* see https://tools.ietf.org/html/draft-ietf-cbor-cddl-08#section-3.1
|
|
161
|
+
*/
|
|
162
|
+
while (
|
|
163
|
+
// a letter (a-z, A-Z)
|
|
164
|
+
isLetter(String.fromCharCode(this.ch)) ||
|
|
165
|
+
// a digit (0-9)
|
|
166
|
+
isDigit(String.fromCharCode(this.ch)) ||
|
|
167
|
+
// and special characters (-, _, @, ., $)
|
|
168
|
+
[
|
|
169
|
+
Tokens.MINUS.charCodeAt(0),
|
|
170
|
+
Tokens.UNDERSCORE.charCodeAt(0),
|
|
171
|
+
Tokens.ATSIGN.charCodeAt(0),
|
|
172
|
+
Tokens.DOT.charCodeAt(0),
|
|
173
|
+
Tokens.DOLLAR.charCodeAt(0)
|
|
174
|
+
].includes(this.ch)
|
|
175
|
+
) {
|
|
176
|
+
this.readChar()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return this.input.slice(position, this.position)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private readComment (): string {
|
|
183
|
+
const position = this.position
|
|
184
|
+
|
|
185
|
+
while (this.ch && String.fromCharCode(this.ch) !== '\n') {
|
|
186
|
+
this.readChar()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return this.input.slice(position, this.position).trim()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private readString (): string {
|
|
193
|
+
const position = this.position
|
|
194
|
+
|
|
195
|
+
this.readChar() // eat "
|
|
196
|
+
while (this.ch && String.fromCharCode(this.ch) !== Tokens.QUOT) {
|
|
197
|
+
this.readChar() // eat any character until "
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return this.input.slice(position + 1, this.position).trim()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private readNumberOrFloat (): string {
|
|
204
|
+
const position = this.position
|
|
205
|
+
let foundSpecialCharacter = false
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* a number of float can contain
|
|
209
|
+
*/
|
|
210
|
+
while (
|
|
211
|
+
// a number
|
|
212
|
+
isDigit(String.fromCharCode(this.ch)) ||
|
|
213
|
+
// a special character, e.g. ".", "x" and "b"
|
|
214
|
+
hasSpecialNumberCharacter(this.ch)
|
|
215
|
+
) {
|
|
216
|
+
/**
|
|
217
|
+
* ensure we respect ranges, e.g. 0..10
|
|
218
|
+
* so break after the second dot and adjust read position
|
|
219
|
+
*/
|
|
220
|
+
if (hasSpecialNumberCharacter(this.ch) && foundSpecialCharacter) {
|
|
221
|
+
this.position--
|
|
222
|
+
this.readPosition--
|
|
223
|
+
break
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
foundSpecialCharacter = hasSpecialNumberCharacter(this.ch)
|
|
227
|
+
this.readChar() // eat any character until a non digit or a 2nd dot
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return this.input.slice(position, this.position).trim()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private skipWhitespace () {
|
|
234
|
+
while (WHITESPACE_CHARACTERS.includes(String.fromCharCode(this.ch))) {
|
|
235
|
+
this.readChar()
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|