@taml/encoder
Version:
Convert ANSI escape sequences to TAML (Terminal ANSI Markup Language) tags
105 lines (93 loc) • 3.12 kB
text/typescript
import {
applyAnsiSequence,
createInitialAnsiState,
getActiveStyleTags,
tokenizeAnsiText,
} from "./ansi-parser.js";
/**
* Escapes special characters in a string for TAML.
* @param text The input string.
* @returns The escaped string.
*/
function escapeText(text: string): string {
return text
.replace(/&/g, "&") // First escape existing &
.replace(/</g, "&lt;") // Then escape existing <
.replace(/</g, "<"); // Finally escape remaining <
}
/**
* Encode ANSI escape sequences to TAML (Terminal ANSI Markup Language) tags
* @param ansiText Input text containing ANSI escape sequences
* @returns Text with ANSI sequences converted to TAML tags
*/
export function encode(ansiText: string): string {
if (!ansiText) {
return "";
}
const tokens = tokenizeAnsiText(ansiText);
let result = "";
let currentState = createInitialAnsiState();
const tagStack: string[] = [];
for (const token of tokens) {
if (token.type === "text") {
// Add text content with current formatting
result += escapeText(token.content);
} else if (token.type === "sequence" && token.sequence) {
const newState = applyAnsiSequence(currentState, token.sequence);
// Handle state changes
if (token.sequence.type === "reset") {
// Close all open tags when reset
while (tagStack.length > 0) {
const tag = tagStack.pop();
if (tag) {
result += `</${tag}>`;
}
}
currentState = newState;
} else {
// Get the difference between old and new states
const oldTags = getActiveStyleTags(currentState);
const newTags = getActiveStyleTags(newState);
// Close tags that are no longer active
const tagsToClose = oldTags.filter((tag) => !newTags.includes(tag));
for (let i = tagStack.length - 1; i >= 0; i--) {
const tag = tagStack[i];
if (tag && tagsToClose.includes(tag)) {
// Close this tag and all tags after it
const closedTags = tagStack.splice(i);
for (let j = closedTags.length - 1; j >= 0; j--) {
const closedTag = closedTags[j];
if (closedTag) {
result += `</${closedTag}>`;
}
}
// Reopen tags that should still be active
const tagsToReopen = closedTags.filter(
(t) => t && newTags.includes(t),
);
for (const reopenTag of tagsToReopen) {
result += `<${reopenTag}>`;
tagStack.push(reopenTag);
}
break;
}
}
// Open new tags
const tagsToOpen = newTags.filter((tag) => !oldTags.includes(tag));
for (const tag of tagsToOpen) {
result += `<${tag}>`;
tagStack.push(tag);
}
currentState = newState;
}
}
}
// Close any remaining open tags at the end
while (tagStack.length > 0) {
const tag = tagStack.pop();
if (tag) {
result += `</${tag}>`;
}
}
return result;
}