diff --git a/core/src/ansi/ansiColors.ts b/core/src/ansi/ansiColors.ts
new file mode 100644
index 0000000..6dc0f51
--- /dev/null
+++ b/core/src/ansi/ansiColors.ts
@@ -0,0 +1,67 @@
+/*
+ * @nevware21/chromacon
+ * https://github.com/nevware21/chromacon
+ *
+ * Copyright (c) 2025 NevWare21 Solutions LLC
+ * Licensed under the MIT license.
+ */
+
+import { mathFloor, mathMax, mathMin } from "@nevware21/ts-utils";
+import { AnsiColor } from "./parseAnsiStyles";
+
+/**
+ * The default palette (xterm) used for the colors 0-15.
+ * @since 0.1.5
+ */
+export const DEFAULT_ANSI_PALETTE: string[] = [
+ "#000000", "#cd0000", "#00cd00", "#cdcd00", "#0000ee", "#cd00cd", "#00cdcd", "#e5e5e5",
+ "#7f7f7f", "#ff0000", "#00ff00", "#ffff00", "#5c5cff", "#ff00ff", "#00ffff", "#ffffff"
+];
+
+function _level(n: number): number {
+ return n ? 55 + n * 40 : 0;
+}
+
+function _hexToRgb(hex: string): { r: number; g: number; b: number } {
+ let h = hex.replace("#", "");
+ if (h.length === 3) {
+ h = h.charAt(0) + h.charAt(0) + h.charAt(1) + h.charAt(1) + h.charAt(2) + h.charAt(2);
+ }
+
+ return { r: parseInt(h.substring(0, 2), 16), g: parseInt(h.substring(2, 4), 16), b: parseInt(h.substring(4, 6), 16) };
+}
+
+/**
+ * Convert an xterm 256 color index to RGB.
+ * @param index - The color index (0-255).
+ * @param palette - Optional hex colors (`#rrggbb`) overriding the default colors 0-15.
+ * @since 0.1.5
+ */
+export function ansi256ToRgb(index: number, palette?: string[]): { r: number; g: number; b: number } {
+ index = mathMax(0, mathMin(255, mathFloor(index) || 0));
+ if (index < 16) {
+ return _hexToRgb((palette && palette[index]) || DEFAULT_ANSI_PALETTE[index]);
+ }
+
+ if (index < 232) {
+ // 6x6x6 color cube, levels are 0, 95, 135, 175, 215, 255
+ const n = index - 16;
+ return { r: _level(mathFloor(n / 36)), g: _level(mathFloor(n / 6) % 6), b: _level(n % 6) };
+ }
+
+ // 24 step greyscale ramp
+ const v = 8 + (index - 232) * 10;
+ return { r: v, g: v, b: v };
+}
+
+/**
+ * Convert an {@link AnsiColor} to a CSS `rgb(...)` string.
+ * @param color - The color to convert.
+ * @param palette - Optional hex colors (`#rrggbb`) used for the `basic` colors.
+ * @since 0.1.5
+ */
+export function ansiColorToCss(color: AnsiColor, palette?: string[]): string {
+ const rgb = color.type === "rgb" ? color : ansi256ToRgb(color.index, palette);
+
+ return "rgb(" + rgb.r + ", " + rgb.g + ", " + rgb.b + ")";
+}
diff --git a/core/src/ansi/ansiToHtml.ts b/core/src/ansi/ansiToHtml.ts
new file mode 100644
index 0000000..c9bdfe5
--- /dev/null
+++ b/core/src/ansi/ansiToHtml.ts
@@ -0,0 +1,95 @@
+/*
+ * @nevware21/chromacon
+ * https://github.com/nevware21/chromacon
+ *
+ * Copyright (c) 2025 NevWare21 Solutions LLC
+ * Licensed under the MIT license.
+ */
+
+import { ansiColorToCss } from "./ansiColors";
+import { AnsiColor, IAnsiStyle, parseAnsiStyles } from "./parseAnsiStyles";
+
+/**
+ * Options for {@link ansiToHtml}.
+ * @since 0.1.5
+ */
+export interface IAnsiToHtmlOptions {
+ /** Emit inline styles instead of classes. Default false. */
+ inlineStyles?: boolean;
+ /** Class prefix when using classes, default `ansi-`. */
+ classPrefix?: string;
+ /** Hex palette for the colors 0-15. */
+ palette?: string[];
+ /** Set to false to not emit OSC 8 links as anchors. Default true. */
+ links?: boolean;
+}
+
+function _escape(text: string): string {
+ return text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'");
+}
+
+function _colorClass(prefix: string, kind: string, c: AnsiColor): string {
+ return prefix + kind + "-" + (c.type === "rgb" ? c.r + "-" + c.g + "-" + c.b : (c.type === "basic" ? "" : "256-") + c.index);
+}
+
+function _safeLink(url: string): boolean {
+ return !/^\s*(?:javascript|data|vbscript):/i.test(url);
+}
+
+/**
+ * Convert a string containing ANSI SGR sequences to HTML.
+ * @param value - The string to convert.
+ * @param options - Optional settings.
+ * @since 0.1.5
+ */
+export function ansiToHtml(value: string, options?: IAnsiToHtmlOptions): string {
+ const opts = options || {};
+ const prefix = opts.classPrefix === undefined ? "ansi-" : opts.classPrefix;
+ let out = "";
+
+ parseAnsiStyles(value).forEach((run) => {
+ const s: IAnsiStyle = run.style;
+ const classes: string[] = [];
+ const css: string[] = [];
+ const flags: [string, boolean, string][] = [
+ ["bold", !!s.bold, "font-weight:bold"], ["dim", !!s.dim, "opacity:0.6"], ["italic", !!s.italic, "font-style:italic"],
+ ["underline", !!s.underline, "text-decoration:underline"], ["blink", !!s.blink, ""], ["inverse", !!s.inverse, ""],
+ ["hidden", !!s.hidden, "visibility:hidden"], ["strikethrough", !!s.strikethrough, "text-decoration:line-through"],
+ ["overlined", !!s.overlined, "text-decoration:overline"]
+ ];
+ flags.forEach((f) => {
+ if (f[1]) {
+ classes.push(prefix + f[0]);
+ if (f[2]) {
+ css.push(f[2]);
+ }
+ }
+ });
+ if (s.underline === "double") {
+ classes.push(prefix + "double-underline");
+ }
+ if (s.fg) {
+ classes.push(_colorClass(prefix, "fg", s.fg));
+ css.push("color:" + ansiColorToCss(s.fg, opts.palette));
+ }
+ if (s.bg) {
+ classes.push(_colorClass(prefix, "bg", s.bg));
+ css.push("background-color:" + ansiColorToCss(s.bg, opts.palette));
+ }
+
+ let html = _escape(run.text);
+ if (classes.length) {
+ html = opts.inlineStyles
+ ? (css.length ? "" + html + "" : html)
+ : "" + html + "";
+ }
+
+ if (s.link && opts.links !== false && _safeLink(s.link)) {
+ html = "" + html + "";
+ }
+
+ out += html;
+ });
+
+ return out;
+}
diff --git a/core/src/ansi/parseAnsiStyles.ts b/core/src/ansi/parseAnsiStyles.ts
new file mode 100644
index 0000000..6f4e888
--- /dev/null
+++ b/core/src/ansi/parseAnsiStyles.ts
@@ -0,0 +1,264 @@
+/*
+ * @nevware21/chromacon
+ * https://github.com/nevware21/chromacon
+ *
+ * Copyright (c) 2025 NevWare21 Solutions LLC
+ * Licensed under the MIT license.
+ */
+
+import { isNullOrUndefined } from "@nevware21/ts-utils";
+
+/**
+ * A color resolved from an SGR sequence.
+ * @since 0.1.5
+ */
+export type AnsiColor =
+ | { type: "basic"; index: number }
+ | { type: "ansi256"; index: number }
+ | { type: "rgb"; r: number; g: number; b: number };
+
+/**
+ * The style resolved from SGR codes, only the attributes that are on are present.
+ * @since 0.1.5
+ */
+export interface IAnsiStyle {
+ fg?: AnsiColor;
+ bg?: AnsiColor;
+ ul?: AnsiColor;
+ bold?: boolean;
+ dim?: boolean;
+ italic?: boolean;
+ underline?: boolean | "double";
+ blink?: boolean;
+ inverse?: boolean;
+ hidden?: boolean;
+ strikethrough?: boolean;
+ overlined?: boolean;
+ link?: string;
+}
+
+/**
+ * A run of text with the style that applies to it.
+ * @since 0.1.5
+ */
+export interface IAnsiStyledRun {
+ text: string;
+ style: IAnsiStyle;
+}
+
+// 1: CSI params (7-bit or 8-bit introducer), 2: CSI final byte, 3: OSC payload, 4: other escape
+const TOKEN_REGEXP = "(?:\\x1b\\[|\\x9b)([0-?]*)[ -\\/]*([@-~])|(?:\\x1b\\]|\\x9d)([^\\x07\\x1b\\x9c]*)(?:\\x07|\\x1b\\\\|\\x9c)|\\x1b[ -\\/]*[0-~]";
+
+function _copy(src: T): T {
+ const result: any = {};
+ Object.keys(src).forEach((k) => {
+ result[k] = (src as any)[k];
+ });
+
+ return result;
+}
+
+function _cloneStyle(style: IAnsiStyle): IAnsiStyle {
+ const result: any = _copy(style);
+ ["fg", "bg", "ul"].forEach((key) => {
+ if (result[key]) {
+ result[key] = _copy(result[key]);
+ }
+ });
+
+ return result;
+}
+
+function _sameColor(a?: AnsiColor, b?: AnsiColor): boolean {
+ if (!a || !b) {
+ return a === b;
+ }
+
+ return a.type === b.type && (a as any).index === (b as any).index &&
+ (a as any).r === (b as any).r && (a as any).g === (b as any).g && (a as any).b === (b as any).b;
+}
+
+function _sameStyle(a: IAnsiStyle, b: IAnsiStyle): boolean {
+ return _sameColor(a.fg, b.fg) && _sameColor(a.bg, b.bg) && _sameColor(a.ul, b.ul) &&
+ a.bold === b.bold && a.dim === b.dim && a.italic === b.italic && a.underline === b.underline &&
+ a.blink === b.blink && a.inverse === b.inverse && a.hidden === b.hidden &&
+ a.strikethrough === b.strikethrough && a.overlined === b.overlined && a.link === b.link;
+}
+
+function _toNum(value: string | undefined): number {
+ return value === undefined || value === "" ? NaN : +value;
+}
+
+function _inRange(value: number, max: number): boolean {
+ return value === value && value >= 0 && value <= max && value % 1 === 0;
+}
+
+function _makeColor(mode: number, args: number[]): AnsiColor | undefined {
+ if (mode === 5) {
+ const n = args[0];
+ if (args.length >= 1 && _inRange(n, 255)) {
+ return n < 16 ? { type: "basic", index: n } : { type: "ansi256", index: n };
+ }
+ } else if (mode === 2 && args.length >= 3 && _inRange(args[0], 255) && _inRange(args[1], 255) && _inRange(args[2], 255)) {
+ return { type: "rgb", r: args[0], g: args[1], b: args[2] };
+ }
+
+ return undefined;
+}
+
+const SGR_ON: { [code: number]: string } = {
+ 1: "bold", 2: "dim", 3: "italic", 5: "blink", 6: "blink", 7: "inverse", 8: "hidden",
+ 9: "strikethrough", 21: "underline", 53: "overlined"
+};
+
+const SGR_OFF: { [code: number]: string } = {
+ 22: "bold,dim", 23: "italic", 24: "underline", 25: "blink", 27: "inverse", 28: "hidden",
+ 29: "strikethrough", 39: "fg", 49: "bg", 55: "overlined", 59: "ul"
+};
+
+function _off(style: IAnsiStyle, key: string) {
+ delete (style as any)[key];
+}
+
+function _applySgr(style: IAnsiStyle, paramText: string) {
+ const params = paramText === "" ? ["0"] : paramText.split(";");
+ for (let i = 0; i < params.length; i++) {
+ const sub = params[i].split(":");
+ const code = sub[0] === "" ? 0 : +sub[0];
+ if (code !== code) {
+ continue;
+ }
+
+ if (code === 38 || code === 48 || code === 58) {
+ let args: number[];
+ let mode: number;
+ if (sub.length > 1) {
+ mode = _toNum(sub[1]);
+ // 38:2::r:g:b or 38:2:r:g:b
+ const rest = sub.slice(2);
+ args = (mode === 2 && rest.length >= 4 ? rest.slice(1) : rest).map(_toNum);
+ } else {
+ mode = _toNum(params[i + 1]);
+ const count = mode === 5 ? 1 : (mode === 2 ? 3 : 0);
+ args = [];
+ for (let j = 0; j < count && i + 2 + j < params.length; j++) {
+ args.push(_toNum(params[i + 2 + j]));
+ }
+ i += 1 + args.length;
+ }
+
+ const color = _makeColor(mode, args);
+ if (color) {
+ (style as any)[code === 38 ? "fg" : (code === 48 ? "bg" : "ul")] = color;
+ }
+ } else if (code === 0) {
+ const link = style.link;
+ Object.keys(style).forEach((k) => _off(style, k));
+ if (link !== undefined) {
+ style.link = link;
+ }
+ } else if (code === 4) {
+ const u = sub.length > 1 ? _toNum(sub[1]) : 1;
+ if (u === 0) {
+ _off(style, "underline");
+ } else {
+ style.underline = u === 2 ? "double" : true;
+ }
+ } else if (SGR_ON[code]) {
+ (style as any)[SGR_ON[code]] = code === 21 ? "double" : true;
+ } else if (SGR_OFF[code]) {
+ SGR_OFF[code].split(",").forEach((k) => _off(style, k));
+ } else if (code <= 107 && ((code % 60 >= 30 && code % 60 <= 37) || (code % 60 >= 40 && code % 60 <= 47))) {
+ // 30-37 / 40-47 (basic) and 90-97 / 100-107 (bright, +8)
+ const bright = code >= 90 ? 8 : 0;
+ const base = code % 60;
+ (style as any)[base < 40 ? "fg" : "bg"] = { type: "basic", index: (base % 10) + bright };
+ }
+ }
+}
+
+function _applyOsc(style: IAnsiStyle, payload: string) {
+ // OSC 8 ; params ; uri
+ if (payload.substring(0, 2) === "8;") {
+ const idx = payload.indexOf(";", 2);
+ if (idx >= 0) {
+ const uri = payload.substring(idx + 1);
+ if (uri) {
+ style.link = uri;
+ } else {
+ _off(style, "link");
+ }
+ }
+ }
+}
+
+function _parse(value: string, style: IAnsiStyle): IAnsiStyledRun[] {
+ const runs: IAnsiStyledRun[] = [];
+ if (isNullOrUndefined(value)) {
+ return runs;
+ }
+
+ const regexp = new RegExp(TOKEN_REGEXP, "g");
+ let lastIndex = 0;
+ let match: RegExpExecArray | null;
+
+ const addText = (text: string) => {
+ if (text) {
+ const last = runs.length ? runs[runs.length - 1] : null;
+ if (last && _sameStyle(last.style, style)) {
+ last.text += text;
+ } else {
+ runs.push({ text, style: _cloneStyle(style) });
+ }
+ }
+ };
+
+ while ((match = regexp.exec(value)) !== null) {
+ addText(value.substring(lastIndex, match.index));
+ lastIndex = match.index + match[0].length;
+
+ if (match[2] !== undefined) {
+ if (match[2] === "m" && !/[<=>?]/.test(match[1])) {
+ _applySgr(style, match[1]);
+ }
+ } else if (match[3] !== undefined) {
+ _applyOsc(style, match[3]);
+ }
+ }
+
+ addText(value.substring(lastIndex));
+
+ return runs;
+}
+
+/**
+ * Parse a string containing ANSI SGR escape sequences into runs of text, each with the style
+ * resolved from the sequences preceding it. Non-SGR sequences are dropped, OSC 8 hyperlinks
+ * are reported via the `link` style.
+ * @param value - The string to parse.
+ * @param state - Optional style to start with, allowing the style to be carried between calls.
+ * @returns The styled runs; adjacent runs with the same style are merged and empty runs omitted.
+ * @since 0.1.5
+ * @example
+ * ```ts
+ * parseAnsiStyles("\u001b[31mred\u001b[0m plain");
+ * //=> [{ text: "red", style: { fg: { type: "basic", index: 1 } } }, { text: " plain", style: {} }]
+ * ```
+ */
+export function parseAnsiStyles(value: string, state?: IAnsiStyle): IAnsiStyledRun[] {
+ return _parse(value, state ? _cloneStyle(state) : {});
+}
+
+/**
+ * Create a stateful parser that carries the style from one chunk to the next.
+ * @returns An object with a `push` method that returns the runs for the chunk and a `state` method.
+ * @since 0.1.5
+ */
+export function createAnsiStyleParser(): { push(chunk: string): IAnsiStyledRun[]; state(): IAnsiStyle } {
+ const current: IAnsiStyle = {};
+
+ return {
+ push: (chunk: string) => _parse(chunk, current),
+ state: () => _cloneStyle(current)
+ };
+}
diff --git a/core/src/ansi/sgrCodes.ts b/core/src/ansi/sgrCodes.ts
new file mode 100644
index 0000000..25bb44c
--- /dev/null
+++ b/core/src/ansi/sgrCodes.ts
@@ -0,0 +1,51 @@
+/*
+ * @nevware21/chromacon
+ * https://github.com/nevware21/chromacon
+ *
+ * Copyright (c) 2025 NevWare21 Solutions LLC
+ * Licensed under the MIT license.
+ */
+
+const SGR_NAMES: { [code: number]: string } = {
+ 0: "reset", 1: "bold", 2: "dim", 3: "italic", 4: "underline", 5: "blink", 6: "rapidBlink",
+ 7: "inverse", 8: "hidden", 9: "strikethrough", 21: "doubleUnderline", 22: "normal",
+ 23: "notItalic", 24: "notUnderline", 25: "notBlink", 27: "notInverse", 28: "notHidden",
+ 29: "notStrikethrough", 38: "fgExtended", 39: "fgDefault", 48: "bgExtended", 49: "bgDefault",
+ 53: "overlined", 55: "notOverlined", 58: "ulExtended", 59: "ulDefault"
+};
+
+const BASIC = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"];
+
+/**
+ * Get the name of an SGR code at runtime.
+ * @param code - The SGR code.
+ * @returns The name or undefined if the code is not known.
+ * @since 0.1.5
+ */
+export function getSgrName(code: number): string | undefined {
+ if (SGR_NAMES.hasOwnProperty(code)) {
+ return SGR_NAMES[code];
+ }
+
+ if (code >= 30 && code <= 37) {
+ return "fg" + _cap(BASIC[code - 30]);
+ }
+
+ if (code >= 40 && code <= 47) {
+ return "bg" + _cap(BASIC[code - 40]);
+ }
+
+ if (code >= 90 && code <= 97) {
+ return "fgBright" + _cap(BASIC[code - 90]);
+ }
+
+ if (code >= 100 && code <= 107) {
+ return "bgBright" + _cap(BASIC[code - 100]);
+ }
+
+ return undefined;
+}
+
+function _cap(value: string) {
+ return value.charAt(0).toUpperCase() + value.substring(1);
+}
diff --git a/core/src/index.ts b/core/src/index.ts
index 7f80710..a9f91d4 100644
--- a/core/src/index.ts
+++ b/core/src/index.ts
@@ -17,6 +17,10 @@ export { stripAnsi, replaceAnsi, ReplacerFn } from "./ansi/stripAnsi";
export { matchAnsi } from "./ansi/matchAnsi";
export { parseAnsi } from "./ansi/parseAnsi";
export { escapeAnsi } from "./ansi/escapeAnsi";
+export { parseAnsiStyles, createAnsiStyleParser, AnsiColor, IAnsiStyle, IAnsiStyledRun } from "./ansi/parseAnsiStyles";
+export { ansi256ToRgb, ansiColorToCss, DEFAULT_ANSI_PALETTE } from "./ansi/ansiColors";
+export { getSgrName } from "./ansi/sgrCodes";
+export { ansiToHtml, IAnsiToHtmlOptions } from "./ansi/ansiToHtml";
export {
setColorDetector, setColorLevel, getColorLevel,
diff --git a/core/test/src/ansi/parseAnsiStyles.test.ts b/core/test/src/ansi/parseAnsiStyles.test.ts
new file mode 100644
index 0000000..15828b4
--- /dev/null
+++ b/core/test/src/ansi/parseAnsiStyles.test.ts
@@ -0,0 +1,70 @@
+/*
+ * @nevware21/chromacon
+ * https://github.com/nevware21/chromacon
+ *
+ * Copyright (c) 2025 NevWare21 Solutions LLC
+ * Licensed under the MIT license.
+ */
+
+import { assert } from "@nevware21/tripwire";
+import { parseAnsiStyles, createAnsiStyleParser } from "../../../src/ansi/parseAnsiStyles";
+import { ansi256ToRgb, ansiColorToCss } from "../../../src/ansi/ansiColors";
+import { getSgrName } from "../../../src/ansi/sgrCodes";
+import { ansiToHtml } from "../../../src/ansi/ansiToHtml";
+
+describe("parseAnsiStyles", () => {
+ const b = (i: number) => ({ type: "basic", index: i });
+
+ it("plain and resets", () => {
+ assert.deepEqual(parseAnsiStyles("plain"), [{ text: "plain", style: {} }]);
+ assert.deepEqual(parseAnsiStyles("\x1b[1;31mFAIL\x1b[22m still red\x1b[0m plain"), [
+ { text: "FAIL", style: { bold: true, fg: b(1) } },
+ { text: " still red", style: { fg: b(1) } },
+ { text: " plain", style: {} }
+ ]);
+ });
+
+ it("extended colors", () => {
+ assert.deepEqual(parseAnsiStyles("\x1b[38;5;9mx"), [{ text: "x", style: { fg: b(9) } }]);
+ assert.deepEqual(parseAnsiStyles("\x1b[38;5;208mx\x1b[48;5;244my"), [
+ { text: "x", style: { fg: { type: "ansi256", index: 208 } } },
+ { text: "y", style: { fg: { type: "ansi256", index: 208 }, bg: { type: "ansi256", index: 244 } } }
+ ]);
+ assert.deepEqual(parseAnsiStyles("\x1b[38:2::10:20:30mx"), [{ text: "x", style: { fg: { type: "rgb", r: 10, g: 20, b: 30 } } }]);
+ assert.deepEqual(parseAnsiStyles("\x1b[38;2;10;20;30mx")[0].style, { fg: { type: "rgb", r: 10, g: 20, b: 30 } });
+ assert.deepEqual(parseAnsiStyles("\x1b[32m\x1b[38;5mx"), [{ text: "x", style: { fg: b(2) } }]);
+ assert.deepEqual(parseAnsiStyles("\x1b[38;2;300;0;0mx"), [{ text: "x", style: {} }]);
+ });
+
+ it("other sequences", () => {
+ assert.deepEqual(parseAnsiStyles("\x1b[2K\x1b[1G\x1b[?25lline"), [{ text: "line", style: {} }]);
+ assert.deepEqual(parseAnsiStyles("\x9b31mx"), [{ text: "x", style: { fg: b(1) } }]);
+ assert.deepEqual(parseAnsiStyles("see \x1b]8;;https://x.test\x07docs\x1b]8;;\x07 now"), [
+ { text: "see ", style: {} },
+ { text: "docs", style: { link: "https://x.test" } },
+ { text: " now", style: {} }
+ ]);
+ });
+
+ it("merging and state", () => {
+ assert.deepEqual(parseAnsiStyles("\x1b[31mone \x1b[31mtwo"), [{ text: "one two", style: { fg: b(1) } }]);
+ assert.deepEqual(parseAnsiStyles("b", { fg: b(1) } as any), [{ text: "b", style: { fg: b(1) } }]);
+ const p = createAnsiStyleParser();
+ p.push("\x1b[31ma");
+ assert.deepEqual(p.push("b"), [{ text: "b", style: { fg: b(1) } }]);
+ });
+
+ it("helpers", () => {
+ assert.deepEqual(ansi256ToRgb(208), { r: 255, g: 135, b: 0 });
+ assert.deepEqual(ansi256ToRgb(244), { r: 128, g: 128, b: 128 });
+ assert.equal(ansiColorToCss({ type: "rgb", r: 1, g: 2, b: 3 }), "rgb(1, 2, 3)");
+ assert.equal(getSgrName(1), "bold");
+ assert.equal(getSgrName(31), "fgRed");
+ assert.equal(getSgrName(999), undefined);
+ });
+
+ it("ansiToHtml", () => {
+ assert.equal(ansiToHtml("\x1b[1;31m\x1b[0m"), "<a>");
+ assert.equal(ansiToHtml("\x1b]8;;https://x.test\x07d\x1b]8;;\x07"), "d");
+ });
+});