import { Palette } from "./highlighter/palette"; import { Node, Position } from "./highlighter/node"; import { State } from "./highlighter/state"; import { Token, Type } from "./tokenizer/token"; 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: throw new Error(`FIXME (Highlighter#spin, 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: throw new Error('BeforePlain') } 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: throw new Error('AfterAttributes got ' + JSON.stringify(this.currentToken)); } 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; default: throw new Error('BeforeScript') } 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: throw new Error(`FIXME (Highlighter#iterate, 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]; console.assert(this.currentToken.type === type, { message: `Highlighter#consumeNextOfType: 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]; console.assert(this.currentToken.type === a || this.currentToken.type === b, { message: `Highlighter#consumeNextTokenOfEitherType: 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 } { console.assert(this.currentToken.type === type, { message: `Highlighter#currentTokenOfType: Expected '${type}', got '${this.currentToken.type}' instead` }); return this.currentToken as Token & { type: T }; } private currentTokenOfEitherType(a: T, b: U): Token & { type: T | U } { console.assert(this.currentToken.type === a || this.currentToken.type === b, { message: `Highlighter#currentTokenOfEitherType: 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(); } }