230 lines
No EOL
8.8 KiB
TypeScript
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();
|
|
}
|
|
} |