UNPKG

@taml/encoder

Version:

Convert ANSI escape sequences to TAML (Terminal ANSI Markup Language) tags

105 lines (93 loc) 3.12 kB
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(/&amp;/g, "&amp;amp;") // First escape existing &amp; .replace(/&lt;/g, "&amp;lt;") // Then escape existing &lt; .replace(/</g, "&lt;"); // 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; }