237 lines
9.1 KiB
TypeScript
237 lines
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();
|
||
|
}
|
||
|
}
|