networkException
586546ee57
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.
237 lines
No EOL
9.1 KiB
TypeScript
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();
|
|
}
|
|
} |