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(CharacterToken).data; break; default: this.emitNode(this.currentNode); this.reconsumeIn(State.Undefined); } break; } case State.StartTag: { switch (this.consumeNextTokenOfType(StartTagToken).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(StartTagToken).name }); if (this.currentTokenOfType(StartTagToken).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(CharacterToken).data; break; default: this.emitNode(this.currentNode); this.reconsumeIn(State.Undefined); } break; } case State.DOCTYPE: { const doctype = this.consumeNextTokenOfType(DOCTYPEToken); 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: Constructor): T { this.currentToken = this.tokens[this.pointer]; VERIFY(this.currentToken instanceof type, `Expected '${type.name}', got '${this.currentToken.constructor.name}' instead`); this.pointer++; return this.currentToken; } private consumeNextTokenOfEitherType(a: Constructor, b: Constructor): T | U { this.currentToken = this.tokens[this.pointer]; VERIFY(this.currentToken instanceof a || this.currentToken instanceof b, `Expected '${a.name}' or '${b.name}', got '${this.currentToken.constructor.name}' instead`); this.pointer++; return this.currentToken; } 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: Constructor): T { VERIFY(this.currentToken instanceof type, `Expected '${type.name}', got '${this.currentToken.constructor.name}' instead`); return this.currentToken; } private currentTokenOfEitherType(a: Constructor, b: Constructor): T | U { VERIFY(this.currentToken instanceof a || this.currentToken instanceof b, `Expected '${a.name}' or '${b.name}', got '${this.currentToken.constructor.name}' instead`); return this.currentToken; } private reconsumeIn(state: State): void { this.pointer--; this.state = state; this.spin(); } }