cddl 0.13.1 → 0.14.1

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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020 Christian Bromann
3
+ Copyright (c) 2026 Christian Bromann
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- CDDL [![Test](https://github.com/christian-bromann/cddl/actions/workflows/test.yml/badge.svg)](https://github.com/christian-bromann/cddl/actions/workflows/test.yml)
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/christian-bromann/cddl2java) package
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 [`/docs`](/docs/README.md) directory.
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/christian-bromann/cddl/blob/master/CONTRIBUTING.md) to get started.
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.13.1",
3
+ "version": "0.14.1",
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/christian-bromann/cddl#readme",
7
+ "homepage": "https://github.com/webdriverio/cddl#readme",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+ssh://git@github.com/christian-bromann/cddl.git"
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/christian-bromann/cddl/issues"
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": "^24.12.0",
25
+ "@types/node": "^25.5.0",
43
26
  "@types/yargs": "^17.0.35",
44
27
  "@vitest/coverage-v8": "^4.1.0",
45
- "npm-run-all": "^4.1.5",
46
- "release-it": "^19.2.4",
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`
@@ -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
+ }
@@ -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
+ }