import { Palette } from "./highlighter/palette"; import { Node, Position } from "./highlighter/node"; import { State } from "./highlighter/state"; import { Token, Type } from "./tokenizer/token"; import { TODO, VERIFY, VERIFY_NOT_REACHED } from "../util/assertions.js"; export class Highlighter { private state: State = State.Undefined; private returnState!: State; private currentToken!: Token; private currentNode!: Node; public nodes: Array = new Array(); private pointer: number = 0; public finished: boolean = false; public constructor(private tokens: Array) { } public spin(): void { switch (this.state) { case State.Undefined: { switch (this.consumeNextTokenType()) { case Type.Character: this.reconsumeIn(State.BeforePlain); break; case Type.StartTag: this.reconsumeIn(State.StartTag); break; case Type.EndTag: this.reconsumeIn(State.EndTag); break; case Type.DOCTYPE: this.reconsumeIn(State.DOCTYPE); break; case Type.Comment: this.reconsumeIn(State.Comment); break; case Type.EndOfFile: this.finished = true; break; default: TODO(`Unimplemented token type '${this.currentToken.type}'`); } break; } case State.BeforePlain: { switch (this.consumeNextTokenType()) { case Type.Character: this.createNode({ position: { line: 0, character: 0 }, color: Palette.Plain, content: '' }); this.reconsumeIn(State.Plain); break; default: VERIFY_NOT_REACHED(this.currentToken.type); } break; } case State.Plain: { switch (this.consumeNextTokenType()) { case Type.Character: this.currentNode.content += this.currentTokenOfType(Type.Character).data; break; default: this.emitNode(this.currentNode); this.reconsumeIn(State.Undefined); } break; } case State.StartTag: { switch (this.consumeNextTokenOfType(Type.StartTag).name) { case 'script': this.returnState = State.BeforeScript; break; default: this.returnState = State.Undefined; break; } this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Punctuator, content: `<` }); this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Tag, content: this.currentTokenOfType(Type.StartTag).name }); if (this.currentTokenOfType(Type.StartTag).attributes.nonEmpty()) { this.emitSpace({ line: 0, character: 0 }); this.reconsumeIn(State.Attributes); } this.reconsumeIn(State.AfterAttributes); this.state = this.returnState; break; } case State.EndTag: { this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Punctuator, content: '' }); } else { this.emitSpace({ line: 0, character: 0 }); this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Punctuator, content: '/>' }); } break; case Type.EndTag: this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Punctuator, content: '>' }); break; default: VERIFY_NOT_REACHED(this.currentToken.type); } break; } case State.BeforeScript: { switch (this.consumeNextTokenType()) { case Type.Character: this.createNode({ position: { line: 0, character: 0 }, color: Palette.String, content: '' }); this.reconsumeIn(State.Script); break; case Type.EndTag: this.reconsumeIn(State.EndTag); break; default: VERIFY_NOT_REACHED(this.currentToken.type); } break; } case State.Script: { switch (this.consumeNextTokenType()) { case Type.Character: this.currentNode.content += this.currentTokenOfType(Type.Character).data; break; default: this.emitNode(this.currentNode); this.reconsumeIn(State.Undefined); } break; } case State.DOCTYPE: { const doctype = this.consumeNextTokenOfType(Type.DOCTYPE); this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Punctuator, content: '' }); this.state = State.Undefined; break; } case State.Comment: this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Comment, content: `` }); this.state = State.Undefined; break; default: TODO(`Unimplemented state '${this.state}'`); } } private emitNode(node: Node): void { this.nodes.push(node); } private emitSpace(position: Position): void { this.nodes.push({ position, color: Palette.Plain, content: ' ' }); } private createNode(node: Node): Node { return this.currentNode = node; } private consumeNextTokenOfType(type: T): Token & { type: T } { this.currentToken = this.tokens[this.pointer]; VERIFY(this.currentToken.type === type, `Expected '${type}', got '${this.currentToken.type}' instead`); this.pointer++; return this.currentToken as Token & { type: T }; } private consumeNextTokenOfEitherType(a: T, b: U): Token & { type: T | U } { this.currentToken = this.tokens[this.pointer]; VERIFY(this.currentToken.type === a || this.currentToken.type === b, `Expected '${a}' or '${b}', got '${this.currentToken.type}' instead`); this.pointer++; return this.currentToken as Token & { type: T }; } private consumeNextTokenType(): Type { this.currentToken = this.tokens[this.pointer]; this.pointer++; return this.currentToken?.type; } private consumeNextToken(): Token { this.currentToken = this.tokens[this.pointer]; this.pointer++; return this.currentToken; } private currentTokenOfType(type: T): Token & { type: T } { VERIFY(this.currentToken.type === type, `Expected '${type}', got '${this.currentToken.type}' instead`); return this.currentToken as Token & { type: T }; } private currentTokenOfEitherType(a: T, b: U): Token & { type: T | U } { VERIFY(this.currentToken.type === a || this.currentToken.type === b, `Expected '${a}' or '${b}', got '${this.currentToken.type}' instead`); return this.currentToken as Token & { type: T }; } private reconsumeIn(state: State): void { this.pointer--; this.state = state; this.spin(); } }