import { charset, escapedSequences } from './utils/utf-8' import { StringBuilder, NonBufferedString, BufferedString } from './utils/bufferedString' import { TokenType } from './utils/constants' import { JsonPrimitive } from './utils/types' const { LEFT_BRACE, RIGHT_BRACE, LEFT_BRACKET, RIGHT_BRACKET, COLON, COMMA, TRUE, FALSE, NULL, STRING, NUMBER, } = TokenType // Tokenizer States enum TokenizerStates { START, ENDED, ERROR, TRUE1, TRUE2, TRUE3, FALSE1, FALSE2, FALSE3, FALSE4, NULL1, NULL2, NULL3, STRING_DEFAULT, STRING_AFTER_BACKSLASH, STRING_UNICODE_DIGIT_1, STRING_UNICODE_DIGIT_2, STRING_UNICODE_DIGIT_3, STRING_UNICODE_DIGIT_4, STRING_INCOMPLETE_CHAR, NUMBER_AFTER_INITIAL_MINUS, NUMBER_AFTER_INITIAL_ZERO, NUMBER_AFTER_INITIAL_NON_ZERO, NUMBER_AFTER_FULL_STOP, NUMBER_AFTER_DECIMAL, NUMBER_AFTER_E, NUMBER_AFTER_E_AND_SIGN, NUMBER_AFTER_E_AND_DIGIT, SEPARATOR, } export interface TokenizerOptions { stringBufferSize?: number numberBufferSize?: number separator?: string } const defaultOpts: TokenizerOptions = { stringBufferSize: 0, numberBufferSize: 0, separator: undefined, } export class TokenizerError extends Error { constructor(message: string) { super(message) // Typescript is broken. This is a workaround Object.setPrototypeOf(this, TokenizerError.prototype) } } export default class Tokenizer { private state = TokenizerStates.START private separator?: string private separatorBytes?: Uint8Array private separatorIndex = 0 private bufferedString: StringBuilder private bufferedNumber: StringBuilder private unicode: string | undefined = undefined // unicode escapes private highSurrogate: number | undefined = undefined private bytes_remaining = 0 // number of bytes remaining in multi byte utf8 char to read after split boundary private bytes_in_sequence = 0 // bytes in multi byte utf8 char to read private char_split_buffer = new Uint8Array(4) // for rebuilding chars split before boundary is reached private encoder = new TextEncoder() private offset = -1 constructor(opts?: TokenizerOptions) { opts = { ...defaultOpts, ...opts } this.bufferedString = opts.stringBufferSize && opts.stringBufferSize > 4 ? new BufferedString(opts.stringBufferSize) : new NonBufferedString() this.bufferedNumber = opts.numberBufferSize && opts.numberBufferSize > 0 ? new BufferedString(opts.numberBufferSize) : new NonBufferedString() this.separator = opts.separator this.separatorBytes = opts.separator ? this.encoder.encode(opts.separator) : undefined } public get isEnded(): boolean { return this.state === TokenizerStates.ENDED } public write(input: Iterable | string): void { let buffer: Uint8Array if (input instanceof Uint8Array) { buffer = input } else if (typeof input === 'string') { buffer = this.encoder.encode(input) } else if ((typeof input === 'object' && 'buffer' in input) || Array.isArray(input)) { buffer = Uint8Array.from(input) } else { this.error( new TypeError( 'Unexpected type. The `write` function only accepts Arrays, TypedArrays and Strings.' ) ) return } for (let i = 0; i < buffer.length; i += 1) { const n = buffer[i] // get current byte from buffer switch (this.state) { case TokenizerStates.START: this.offset += 1 if (this.separatorBytes && n === this.separatorBytes[0]) { if (this.separatorBytes.length === 1) { this.state = TokenizerStates.START this.onToken( TokenType.SEPARATOR, this.separator as string, this.offset + this.separatorBytes.length - 1 ) continue } this.state = TokenizerStates.SEPARATOR continue } if ( n === charset.SPACE || n === charset.NEWLINE || n === charset.CARRIAGE_RETURN || n === charset.TAB ) { // whitespace continue } if (n === charset.LEFT_CURLY_BRACKET) { this.onToken(LEFT_BRACE, '{', this.offset) continue } if (n === charset.RIGHT_CURLY_BRACKET) { this.onToken(RIGHT_BRACE, '}', this.offset) continue } if (n === charset.LEFT_SQUARE_BRACKET) { this.onToken(LEFT_BRACKET, '[', this.offset) continue } if (n === charset.RIGHT_SQUARE_BRACKET) { this.onToken(RIGHT_BRACKET, ']', this.offset) continue } if (n === charset.COLON) { this.onToken(COLON, ':', this.offset) continue } if (n === charset.COMMA) { this.onToken(COMMA, ',', this.offset) continue } if (n === charset.LATIN_SMALL_LETTER_T) { this.state = TokenizerStates.TRUE1 continue } if (n === charset.LATIN_SMALL_LETTER_F) { this.state = TokenizerStates.FALSE1 continue } if (n === charset.LATIN_SMALL_LETTER_N) { this.state = TokenizerStates.NULL1 continue } if (n === charset.QUOTATION_MARK) { this.bufferedString.reset() this.state = TokenizerStates.STRING_DEFAULT continue } if (n >= charset.DIGIT_ONE && n <= charset.DIGIT_NINE) { this.bufferedNumber.reset() this.bufferedNumber.appendChar(n) this.state = TokenizerStates.NUMBER_AFTER_INITIAL_NON_ZERO continue } if (n === charset.DIGIT_ZERO) { this.bufferedNumber.reset() this.bufferedNumber.appendChar(n) this.state = TokenizerStates.NUMBER_AFTER_INITIAL_ZERO continue } if (n === charset.HYPHEN_MINUS) { this.bufferedNumber.reset() this.bufferedNumber.appendChar(n) this.state = TokenizerStates.NUMBER_AFTER_INITIAL_MINUS continue } break // STRING case TokenizerStates.STRING_DEFAULT: if (n === charset.QUOTATION_MARK) { const string = this.bufferedString.toString() this.state = TokenizerStates.START this.onToken(STRING, string, this.offset) this.offset += this.bufferedString.byteLength + 1 continue } if (n === charset.REVERSE_SOLIDUS) { this.state = TokenizerStates.STRING_AFTER_BACKSLASH continue } if (n >= 128) { // Parse multi byte (>=128) chars one at a time if (n >= 194 && n <= 223) { this.bytes_in_sequence = 2 } else if (n <= 239) { this.bytes_in_sequence = 3 } else { this.bytes_in_sequence = 4 } if (this.bytes_in_sequence <= buffer.length - i) { // if bytes needed to complete char fall outside buffer length, we have a boundary split this.bufferedString.appendBuf(buffer, i, i + this.bytes_in_sequence) i += this.bytes_in_sequence - 1 continue } this.bytes_remaining = i + this.bytes_in_sequence - buffer.length this.char_split_buffer.set(buffer.subarray(i)) i = buffer.length - 1 this.state = TokenizerStates.STRING_INCOMPLETE_CHAR continue } if (n >= charset.SPACE) { this.bufferedString.appendChar(n) continue } break case TokenizerStates.STRING_INCOMPLETE_CHAR: // check for carry over of a multi byte char split between data chunks // & fill temp buffer it with start of this data chunk up to the boundary limit set in the last iteration this.char_split_buffer.set( buffer.subarray(i, i + this.bytes_remaining), this.bytes_in_sequence - this.bytes_remaining ) this.bufferedString.appendBuf(this.char_split_buffer, 0, this.bytes_in_sequence) i = this.bytes_remaining - 1 this.state = TokenizerStates.STRING_DEFAULT continue case TokenizerStates.STRING_AFTER_BACKSLASH: const controlChar = escapedSequences[n] if (controlChar) { this.bufferedString.appendChar(controlChar) this.state = TokenizerStates.STRING_DEFAULT continue } if (n === charset.LATIN_SMALL_LETTER_U) { this.unicode = '' this.state = TokenizerStates.STRING_UNICODE_DIGIT_1 continue } break case TokenizerStates.STRING_UNICODE_DIGIT_1: case TokenizerStates.STRING_UNICODE_DIGIT_2: case TokenizerStates.STRING_UNICODE_DIGIT_3: if ( (n >= charset.DIGIT_ZERO && n <= charset.DIGIT_NINE) || (n >= charset.LATIN_CAPITAL_LETTER_A && n <= charset.LATIN_CAPITAL_LETTER_F) || (n >= charset.LATIN_SMALL_LETTER_A && n <= charset.LATIN_SMALL_LETTER_F) ) { this.unicode += String.fromCharCode(n) this.state += 1 continue } break case TokenizerStates.STRING_UNICODE_DIGIT_4: if ( (n >= charset.DIGIT_ZERO && n <= charset.DIGIT_NINE) || (n >= charset.LATIN_CAPITAL_LETTER_A && n <= charset.LATIN_CAPITAL_LETTER_F) || (n >= charset.LATIN_SMALL_LETTER_A && n <= charset.LATIN_SMALL_LETTER_F) ) { const intVal = parseInt(this.unicode + String.fromCharCode(n), 16) if (this.highSurrogate === undefined) { if (intVal >= 0xd800 && intVal <= 0xdbff) { //<55296,56319> - highSurrogate this.highSurrogate = intVal } else { this.bufferedString.appendBuf( this.encoder.encode(String.fromCharCode(intVal)) ) } } else { if (intVal >= 0xdc00 && intVal <= 0xdfff) { //<56320,57343> - lowSurrogate this.bufferedString.appendBuf( this.encoder.encode( String.fromCharCode(this.highSurrogate, intVal) ) ) } else { this.bufferedString.appendBuf( this.encoder.encode(String.fromCharCode(this.highSurrogate)) ) } this.highSurrogate = undefined } this.state = TokenizerStates.STRING_DEFAULT continue } // Number case TokenizerStates.NUMBER_AFTER_INITIAL_MINUS: if (n === charset.DIGIT_ZERO) { this.bufferedNumber.appendChar(n) this.state = TokenizerStates.NUMBER_AFTER_INITIAL_ZERO continue } if (n >= charset.DIGIT_ONE && n <= charset.DIGIT_NINE) { this.bufferedNumber.appendChar(n) this.state = TokenizerStates.NUMBER_AFTER_INITIAL_NON_ZERO continue } break case TokenizerStates.NUMBER_AFTER_INITIAL_ZERO: if (n === charset.FULL_STOP) { this.bufferedNumber.appendChar(n) this.state = TokenizerStates.NUMBER_AFTER_FULL_STOP continue } if ( n === charset.LATIN_SMALL_LETTER_E || n === charset.LATIN_CAPITAL_LETTER_E ) { this.bufferedNumber.appendChar(n) this.state = TokenizerStates.NUMBER_AFTER_E continue } i -= 1 this.state = TokenizerStates.START this.emitNumber() continue case TokenizerStates.NUMBER_AFTER_INITIAL_NON_ZERO: if (n >= charset.DIGIT_ZERO && n <= charset.DIGIT_NINE) { this.bufferedNumber.appendChar(n) continue } if (n === charset.FULL_STOP) { this.bufferedNumber.appendChar(n) this.state = TokenizerStates.NUMBER_AFTER_FULL_STOP continue } if ( n === charset.LATIN_SMALL_LETTER_E || n === charset.LATIN_CAPITAL_LETTER_E ) { this.bufferedNumber.appendChar(n) this.state = TokenizerStates.NUMBER_AFTER_E continue } i -= 1 this.state = TokenizerStates.START this.emitNumber() continue case TokenizerStates.NUMBER_AFTER_FULL_STOP: if (n >= charset.DIGIT_ZERO && n <= charset.DIGIT_NINE) { this.bufferedNumber.appendChar(n) this.state = TokenizerStates.NUMBER_AFTER_DECIMAL continue } break case TokenizerStates.NUMBER_AFTER_DECIMAL: if (n >= charset.DIGIT_ZERO && n <= charset.DIGIT_NINE) { this.bufferedNumber.appendChar(n) continue } if ( n === charset.LATIN_SMALL_LETTER_E || n === charset.LATIN_CAPITAL_LETTER_E ) { this.bufferedNumber.appendChar(n) this.state = TokenizerStates.NUMBER_AFTER_E continue } i -= 1 this.state = TokenizerStates.START this.emitNumber() continue case TokenizerStates.NUMBER_AFTER_E: if (n === charset.PLUS_SIGN || n === charset.HYPHEN_MINUS) { this.bufferedNumber.appendChar(n) this.state = TokenizerStates.NUMBER_AFTER_E_AND_SIGN continue } // Allow cascading case TokenizerStates.NUMBER_AFTER_E_AND_SIGN: if (n >= charset.DIGIT_ZERO && n <= charset.DIGIT_NINE) { this.bufferedNumber.appendChar(n) this.state = TokenizerStates.NUMBER_AFTER_E_AND_DIGIT continue } break case TokenizerStates.NUMBER_AFTER_E_AND_DIGIT: if (n >= charset.DIGIT_ZERO && n <= charset.DIGIT_NINE) { this.bufferedNumber.appendChar(n) continue } i -= 1 this.state = TokenizerStates.START this.emitNumber() continue // TRUE case TokenizerStates.TRUE1: if (n === charset.LATIN_SMALL_LETTER_R) { this.state = TokenizerStates.TRUE2 continue } break case TokenizerStates.TRUE2: if (n === charset.LATIN_SMALL_LETTER_U) { this.state = TokenizerStates.TRUE3 continue } break case TokenizerStates.TRUE3: if (n === charset.LATIN_SMALL_LETTER_E) { this.state = TokenizerStates.START this.onToken(TRUE, true, this.offset) this.offset += 3 continue } break // FALSE case TokenizerStates.FALSE1: if (n === charset.LATIN_SMALL_LETTER_A) { this.state = TokenizerStates.FALSE2 continue } break case TokenizerStates.FALSE2: if (n === charset.LATIN_SMALL_LETTER_L) { this.state = TokenizerStates.FALSE3 continue } break case TokenizerStates.FALSE3: if (n === charset.LATIN_SMALL_LETTER_S) { this.state = TokenizerStates.FALSE4 continue } break case TokenizerStates.FALSE4: if (n === charset.LATIN_SMALL_LETTER_E) { this.state = TokenizerStates.START this.onToken(FALSE, false, this.offset) this.offset += 4 continue } break // NULL case TokenizerStates.NULL1: if (n === charset.LATIN_SMALL_LETTER_U) { this.state = TokenizerStates.NULL2 continue } break case TokenizerStates.NULL2: if (n === charset.LATIN_SMALL_LETTER_L) { this.state = TokenizerStates.NULL3 continue } break case TokenizerStates.NULL3: if (n === charset.LATIN_SMALL_LETTER_L) { this.state = TokenizerStates.START this.onToken(NULL, null, this.offset) this.offset += 3 continue } break case TokenizerStates.SEPARATOR: this.separatorIndex += 1 if (!this.separatorBytes || n !== this.separatorBytes[this.separatorIndex]) { break } if (this.separatorIndex === this.separatorBytes.length - 1) { this.state = TokenizerStates.START this.onToken( TokenType.SEPARATOR, this.separator as string, this.offset + this.separatorIndex ) this.separatorIndex = 0 } continue case TokenizerStates.ENDED: if ( n === charset.SPACE || n === charset.NEWLINE || n === charset.CARRIAGE_RETURN || n === charset.TAB ) { // whitespace continue } } this.error( new TokenizerError( `Unexpected "${String.fromCharCode(n)}" at position "${i}" in state ${ TokenizerStates[this.state] }` ) ) return } } private emitNumber(): void { this.onToken(NUMBER, this.parseNumber(this.bufferedNumber.toString()), this.offset) this.offset += this.bufferedNumber.byteLength - 1 } protected parseNumber(numberStr: string): number { return Number(numberStr) } public error(err: Error): void { if (this.state !== TokenizerStates.ENDED) { this.state = TokenizerStates.ERROR } this.onError(err) } public end(): void { switch (this.state) { case TokenizerStates.NUMBER_AFTER_INITIAL_ZERO: case TokenizerStates.NUMBER_AFTER_INITIAL_NON_ZERO: case TokenizerStates.NUMBER_AFTER_DECIMAL: case TokenizerStates.NUMBER_AFTER_E_AND_DIGIT: this.state = TokenizerStates.ENDED this.emitNumber() this.onEnd() break case TokenizerStates.START: case TokenizerStates.ERROR: case TokenizerStates.SEPARATOR: this.state = TokenizerStates.ENDED this.onEnd() break default: this.error( new TokenizerError( `Tokenizer ended in the middle of a token (state: ${ TokenizerStates[this.state] }). Either not all the data was received or the data was invalid.` ) ) } } public onToken(token: TokenType.LEFT_BRACE, value: '{', offset: number): void public onToken(token: TokenType.RIGHT_BRACE, value: '}', offset: number): void public onToken(token: TokenType.LEFT_BRACKET, value: '[', offset: number): void public onToken(token: TokenType.RIGHT_BRACKET, value: ']', offset: number): void public onToken(token: TokenType.COLON, value: ':', offset: number): void public onToken(token: TokenType.COMMA, value: ',', offset: number): void public onToken(token: TokenType.TRUE, value: true, offset: number): void public onToken(token: TokenType.FALSE, value: false, offset: number): void public onToken(token: TokenType.NULL, value: null, offset: number): void public onToken(token: TokenType.STRING, value: string, offset: number): void public onToken(token: TokenType.NUMBER, value: number, offset: number): void public onToken(token: TokenType.SEPARATOR, value: string, offset: number): void // eslint-disable-next-line @typescript-eslint/no-unused-vars public onToken(token: TokenType, value: JsonPrimitive, offset: number): void { // Override me throw new TokenizerError( 'Can\'t emit tokens before the "onToken" callback has been set up.' ) } public onError(err: Error): void { // Override me throw err } public onEnd(): void { // Override me } }