nwex.de/html/highlighter.ts
networkException 5afae11193 Everywhere: Replace calls to console.assert and throws with assertions
This patch replaces console.assert calls and random throw new Error
calls to crash on undefined behavior with predefined assertion
functions from utils.
2021-10-24 23:06:35 +02:00

232 lines
No EOL
9 KiB
TypeScript

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<Node> = new Array<Node>();
private pointer: number = 0;
public finished: boolean = false;
public constructor(private tokens: Array<Token>) {
}
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(`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: VERIFY_NOT_REACHED();
}
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: '</' });
this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Tag, content: this.consumeNextTokenOfType(Type.EndTag).name });
this.reconsumeIn(State.AfterAttributes);
this.state = State.Undefined;
break;
}
case State.Attributes: {
const attributes = this.consumeNextTokenOfEitherType(Type.StartTag, Type.EndTag).attributes.list;
for (let i = 0; i < attributes.length; i++) {
const attribute = attributes[i];
this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Attribute, content: attribute.name });
this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Punctuator, content: '=' });
this.emitNode({ position: { line: 0, character: 0 }, color: Palette.String, content: `"${attribute.value}"` });
if (i !== attributes.length - 1) this.emitSpace({ line: 0, character: 0 });
}
break;
}
case State.AfterAttributes: {
switch (this.consumeNextTokenType()) {
case Type.StartTag:
if (this.currentTokenOfType(Type.StartTag).selfClosing === undefined) {
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.emitNode({ position: { line: 0, character: 0 }, color: Palette.Tag, content: 'DOCTYPE' });
this.emitSpace({ line: 0, character: 0 });
// FIXME: Implement more doctype values
if (doctype.name !== undefined) this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Attribute, content: doctype.name })
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.consumeNextTokenOfType(Type.Comment).data}-->` });
this.state = State.Undefined;
break;
default: TODO(`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<T extends Type>(type: T): Token & { type: T } {
this.currentToken = this.tokens[this.pointer];
VERIFY(this.currentToken.type === type, `Highlighter#consumeNextOfType: Expected '${type}', got '${this.currentToken.type}' instead`);
this.pointer++;
return this.currentToken as Token & { type: T };
}
private consumeNextTokenOfEitherType<T extends Type, U extends Type>(a: T, b: U): Token & { type: T | U } {
this.currentToken = this.tokens[this.pointer];
VERIFY(this.currentToken.type === a || this.currentToken.type === b,
`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<T extends Type>(type: T): Token & { type: T } {
VERIFY(this.currentToken.type === type, `Highlighter#currentTokenOfType: Expected '${type}', got '${this.currentToken.type}' instead`);
return this.currentToken as Token & { type: T };
}
private currentTokenOfEitherType<T extends Type, U extends Type>(a: T, b: U): Token & { type: T | U } {
VERIFY(this.currentToken.type === a || this.currentToken.type === b,
`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();
}
}