nwex.de/html/highlighter.ts

230 lines
No EOL
8.8 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(`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: '</' });
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: 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;
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.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(`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, `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, `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, `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, `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();
}
}