2021-10-24 22:36:38 +02:00
|
|
|
import { Palette } from "./highlighter/palette";
|
|
|
|
import { Node, Position } from "./highlighter/node";
|
|
|
|
import { State } from "./highlighter/state";
|
|
|
|
import { Token, Type } from "./tokenizer/token";
|
2021-10-24 23:06:35 +02:00
|
|
|
import { TODO, VERIFY, VERIFY_NOT_REACHED } from "../util/assertions.js";
|
2021-10-24 22:36:38 +02:00
|
|
|
|
|
|
|
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;
|
2021-10-24 23:11:34 +02:00
|
|
|
default: TODO(`Unimplemented token type '${this.currentToken.type}'`);
|
2021-10-24 22:36:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2021-10-25 19:30:21 +02:00
|
|
|
default: VERIFY_NOT_REACHED(this.currentToken.type);
|
2021-10-24 22:36:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case State.Plain: {
|
|
|
|
switch (this.consumeNextTokenType()) {
|
2022-01-04 00:40:12 +01:00
|
|
|
case Type.Character: this.currentNode.content += this.currentTokenOfType(CharacterToken).data; break;
|
2021-10-24 22:36:38 +02:00
|
|
|
default:
|
|
|
|
this.emitNode(this.currentNode);
|
|
|
|
this.reconsumeIn(State.Undefined);
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case State.StartTag: {
|
2022-01-04 00:40:12 +01:00
|
|
|
switch (this.consumeNextTokenOfType(StartTagToken).name) {
|
2021-10-24 22:36:38 +02:00
|
|
|
case 'script': this.returnState = State.BeforeScript; break;
|
|
|
|
default: this.returnState = State.Undefined; break;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Punctuator, content: `<` });
|
2022-01-04 00:40:12 +01:00
|
|
|
this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Tag, content: this.currentTokenOfType(StartTagToken).name });
|
2021-10-24 22:36:38 +02:00
|
|
|
|
2022-01-04 00:40:12 +01:00
|
|
|
if (this.currentTokenOfType(StartTagToken).attributes.nonEmpty()) {
|
2021-10-24 22:36:38 +02:00
|
|
|
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: '</' });
|
2022-01-04 00:40:12 +01:00
|
|
|
this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Tag, content: this.consumeNextTokenOfType(EndTagToken).name });
|
2021-10-24 22:36:38 +02:00
|
|
|
|
|
|
|
this.reconsumeIn(State.AfterAttributes);
|
|
|
|
|
|
|
|
this.state = State.Undefined;
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case State.Attributes: {
|
2022-01-04 00:40:12 +01:00
|
|
|
const attributes = this.consumeNextTokenOfEitherType(StartTagToken, EndTagToken).attributes.list;
|
2021-10-24 22:36:38 +02:00
|
|
|
|
|
|
|
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:
|
2022-01-04 00:40:12 +01:00
|
|
|
// FIXME: StartTagToken does not support selfClosing as of now
|
|
|
|
// if (this.currentTokenOfType(StartTagToken).selfClosing === undefined) {
|
2021-10-24 22:36:38 +02:00
|
|
|
this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Punctuator, content: '>' });
|
2022-01-04 00:40:12 +01:00
|
|
|
// } else {
|
|
|
|
// this.emitSpace({ line: 0, character: 0 });
|
|
|
|
// this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Punctuator, content: '/>' });
|
|
|
|
// }
|
2021-10-24 22:36:38 +02:00
|
|
|
break;
|
|
|
|
case Type.EndTag:
|
|
|
|
this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Punctuator, content: '>' });
|
|
|
|
break;
|
2021-10-25 19:30:21 +02:00
|
|
|
default: VERIFY_NOT_REACHED(this.currentToken.type);
|
2021-10-24 22:36:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2021-10-25 19:33:46 +02:00
|
|
|
case Type.EndTag: this.reconsumeIn(State.EndTag); break;
|
2021-10-25 19:30:21 +02:00
|
|
|
default: VERIFY_NOT_REACHED(this.currentToken.type);
|
2021-10-24 22:36:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case State.Script: {
|
|
|
|
switch (this.consumeNextTokenType()) {
|
2022-01-04 00:40:12 +01:00
|
|
|
case Type.Character: this.currentNode.content += this.currentTokenOfType(CharacterToken).data; break;
|
2021-10-24 22:36:38 +02:00
|
|
|
default:
|
|
|
|
this.emitNode(this.currentNode);
|
|
|
|
this.reconsumeIn(State.Undefined);
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case State.DOCTYPE: {
|
2022-01-04 00:40:12 +01:00
|
|
|
const doctype = this.consumeNextTokenOfType(DOCTYPEToken);
|
2021-10-24 22:36:38 +02:00
|
|
|
|
|
|
|
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:
|
2022-01-04 00:40:12 +01:00
|
|
|
this.emitNode({ position: { line: 0, character: 0 }, color: Palette.Comment, content: `<!--${this.consumeNextTokenOfType(CommentToken).data}-->` });
|
2021-10-24 22:36:38 +02:00
|
|
|
|
|
|
|
this.state = State.Undefined;
|
|
|
|
break;
|
2021-10-24 23:11:34 +02:00
|
|
|
default: TODO(`Unimplemented state '${this.state}'`);
|
2021-10-24 22:36:38 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-01-04 00:40:12 +01:00
|
|
|
private consumeNextTokenOfType<T extends Token>(type: Constructor<T>): T {
|
2021-10-24 22:36:38 +02:00
|
|
|
this.currentToken = this.tokens[this.pointer];
|
|
|
|
|
2022-01-04 00:40:12 +01:00
|
|
|
VERIFY(this.currentToken instanceof type, `Expected '${type.name}', got '${this.currentToken.constructor.name}' instead`);
|
2021-10-24 22:36:38 +02:00
|
|
|
|
|
|
|
this.pointer++;
|
|
|
|
|
2022-01-04 00:40:12 +01:00
|
|
|
return this.currentToken;
|
2021-10-24 22:36:38 +02:00
|
|
|
}
|
|
|
|
|
2022-01-04 00:40:12 +01:00
|
|
|
private consumeNextTokenOfEitherType<T extends Token, U extends Token>(a: Constructor<T>, b: Constructor<U>): T | U {
|
2021-10-24 22:36:38 +02:00
|
|
|
this.currentToken = this.tokens[this.pointer];
|
|
|
|
|
2022-01-04 00:40:12 +01:00
|
|
|
VERIFY(this.currentToken instanceof a || this.currentToken instanceof b, `Expected '${a.name}' or '${b.name}', got '${this.currentToken.constructor.name}' instead`);
|
2021-10-24 22:36:38 +02:00
|
|
|
|
|
|
|
this.pointer++;
|
|
|
|
|
2022-01-04 00:40:12 +01:00
|
|
|
return this.currentToken;
|
2021-10-24 22:36:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-01-04 00:40:12 +01:00
|
|
|
private currentTokenOfType<T extends Token>(type: Constructor<T>): T {
|
|
|
|
VERIFY(this.currentToken instanceof type, `Expected '${type.name}', got '${this.currentToken.constructor.name}' instead`);
|
2021-10-24 22:36:38 +02:00
|
|
|
|
2022-01-04 00:40:12 +01:00
|
|
|
return this.currentToken;
|
2021-10-24 22:36:38 +02:00
|
|
|
}
|
|
|
|
|
2022-01-04 00:40:12 +01:00
|
|
|
private currentTokenOfEitherType<T extends Token, U extends Token>(a: Constructor<T>, b: Constructor<U>): T | U {
|
|
|
|
VERIFY(this.currentToken instanceof a || this.currentToken instanceof b, `Expected '${a.name}' or '${b.name}', got '${this.currentToken.constructor.name}' instead`);
|
2021-10-24 22:36:38 +02:00
|
|
|
|
2022-01-04 00:40:12 +01:00
|
|
|
return this.currentToken;
|
2021-10-24 22:36:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private reconsumeIn(state: State): void {
|
|
|
|
this.pointer--;
|
|
|
|
this.state = state;
|
|
|
|
this.spin();
|
|
|
|
}
|
|
|
|
}
|