nwex.de/html/highlighter.ts
networkException 586546ee57 Everywhere: Rework website concept completely
This patch removes the Next.js React project that was contained
by this repository previously. The replacement is a vanilla HTML
page with TypeScript that parses it's own HTML source and highlights
it using on load.

The concept will be iterated on in following commits, planned are
on hover tooltips showing metadata about HTML tokens as well as
tokenizing (perhaps parsing) of JavaScript and CSS to be able to
highlight those sections as well. To properly determent the range
of script and style sections it might be required to also implement
HTML tree building, however on read execution of JavaScript or
on the fly parsing as well as fragment parsing is not required for
the site.

This commit merely represents a start and is made to better track
the progress of changes.
2021-10-24 22:36:38 +02:00

237 lines
No EOL
9.1 KiB
TypeScript

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<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: 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: '</' });
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: 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<T extends Type>(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<T extends Type, U extends Type>(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<T extends Type>(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<T extends Type, U extends Type>(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();
}
}