@sarahisweird/hmoog
Version:
Out-of-game automation for Hackmud
115 lines (91 loc) • 3.95 kB
text/typescript
import { ColorNode, Node, NodeVisitor, TextNode } from './types.js';
import { ColorDepth, defaultTextColorHex, defaultTextColorRgba, vgaTranslationTable } from './colors.js';
import { corruptionCharReplacements, CorruptionReplacementTable, isCorruptionChar } from './corruption.js';
import { ShellParser } from './parsing.js';
export type AnsiConverterOptions = {
colorDepth: ColorDepth,
replaceCorruption: boolean,
corruptionReplacements: CorruptionReplacementTable,
};
export class AnsiConverter extends NodeVisitor {
private readonly colorDepth: ColorDepth;
private readonly replaceCorruption: boolean;
private readonly corruptionReplacements: CorruptionReplacementTable;
private readonly colorStack: string[] = [];
private result: string = '';
constructor(options?: Partial<AnsiConverterOptions>) {
super();
const defaultedOptions: AnsiConverterOptions = {
colorDepth: ColorDepth.TRUE_COLOR,
replaceCorruption: true,
corruptionReplacements: corruptionCharReplacements,
...options
};
this.colorDepth = defaultedOptions.colorDepth;
this.replaceCorruption = defaultedOptions.replaceCorruption;
this.corruptionReplacements = defaultedOptions.corruptionReplacements;
const defaultColor = this.makeAnsiColor({ colorHex: defaultTextColorHex, colorRgba: defaultTextColorRgba });
this.colorStack.push(defaultColor);
this.result += defaultColor;
}
static convert(nodes: Node[], options?: Partial<AnsiConverterOptions>): string {
const converter = new AnsiConverter(options);
converter.visitAll(nodes);
return converter.getResult();
}
static convertFromShellText(input: string, options?: Partial<AnsiConverterOptions>): string {
const nodes: Node[] = ShellParser.parse(input);
return this.convert(nodes, options);
}
getResult(): string {
if (this.colorDepth !== ColorDepth.NONE) {
this.result += '\x1b[0m';
}
return this.result;
}
visitColor(node: ColorNode): void {
const prevColor: string = this.colorStack[this.colorStack.length - 1];
const newColor: string = this.makeAnsiColor(node);
this.result += newColor;
this.colorStack.push(newColor);
this.visitAll(node.children);
this.colorStack.pop();
this.result += prevColor;
}
visitText(node: TextNode): void {
const text: string = this.replaceCorruption
? this.convertCorruption(node.text)
: node.text;
this.result += text;
}
private convertCorruption(text: string): string {
let newText: string = '';
for (const char of text) {
if (isCorruptionChar(char)) {
newText += this.corruptionReplacements[char];
} else {
newText += char;
}
}
return newText;
}
private makeAnsiColor(node: Pick<ColorNode, 'colorHex' | 'colorRgba'>): string {
switch (this.colorDepth) {
case ColorDepth.NONE:
return '';
case ColorDepth.EIGHT_BIT:
return this.makeEightBitColor(node);
case ColorDepth.TRUE_COLOR:
return this.makeTrueColor(node);
}
}
private makeEightBitColor(node: Pick<ColorNode, 'colorHex'>): string {
const vgaColor: string = vgaTranslationTable[node.colorHex.substring(0, 6)];
if (!vgaColor) throw new Error(`Unknown color: ${node.colorHex}!`);
return `\x1b[${vgaColor}m`;
}
private makeTrueColor(node: Pick<ColorNode, 'colorRgba'>): string {
const color = node.colorRgba;
return `\x1b[38;2;${color[0]};${color[1]};${color[2]}m`;
}
}