"use strict";
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
    if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
    return cooked;
};
var PacketKind;
(function (PacketKind) {
    PacketKind[PacketKind["EOS"] = 0] = "EOS";
    PacketKind[PacketKind["Text"] = 1] = "Text";
    PacketKind[PacketKind["Incomplete"] = 2] = "Incomplete";
    PacketKind[PacketKind["ESC"] = 3] = "ESC";
    PacketKind[PacketKind["Unknown"] = 4] = "Unknown";
    PacketKind[PacketKind["SGR"] = 5] = "SGR";
    PacketKind[PacketKind["OSCURL"] = 6] = "OSCURL";
})(PacketKind || (PacketKind = {}));
export class AnsiUp {
    constructor() {
        this.VERSION = "6.0.1";
        this.setup_palettes();
        this._use_classes = false;
        this.bold = false;
        this.faint = false;
        this.italic = false;
        this.underline = false;
        this.fg = this.bg = null;
        this._buffer = '';
        this._url_allowlist = { 'http': 1, 'https': 1 };
        this._escape_html = true;
        this.boldStyle = 'font-weight:bold';
        this.faintStyle = 'opacity:0.7';
        this.italicStyle = 'font-style:italic';
        this.underlineStyle = 'text-decoration:underline';
    }
    set use_classes(arg) {
        this._use_classes = arg;
    }
    get use_classes() {
        return this._use_classes;
    }
    set url_allowlist(arg) {
        this._url_allowlist = arg;
    }
    get url_allowlist() {
        return this._url_allowlist;
    }
    set escape_html(arg) {
        this._escape_html = arg;
    }
    get escape_html() {
        return this._escape_html;
    }
    set boldStyle(arg) { this._boldStyle = arg; }
    get boldStyle() { return this._boldStyle; }
    set faintStyle(arg) { this._faintStyle = arg; }
    get faintStyle() { return this._faintStyle; }
    set italicStyle(arg) { this._italicStyle = arg; }
    get italicStyle() { return this._italicStyle; }
    set underlineStyle(arg) { this._underlineStyle = arg; }
    get underlineStyle() { return this._underlineStyle; }
    setup_palettes() {
        this.ansi_colors =
            [
                [
                    { rgb: [0, 0, 0], class_name: "ansi-black" },
                    { rgb: [187, 0, 0], class_name: "ansi-red" },
                    { rgb: [0, 187, 0], class_name: "ansi-green" },
                    { rgb: [187, 187, 0], class_name: "ansi-yellow" },
                    { rgb: [0, 0, 187], class_name: "ansi-blue" },
                    { rgb: [187, 0, 187], class_name: "ansi-magenta" },
                    { rgb: [0, 187, 187], class_name: "ansi-cyan" },
                    { rgb: [255, 255, 255], class_name: "ansi-white" }
                ],
                [
                    { rgb: [85, 85, 85], class_name: "ansi-bright-black" },
                    { rgb: [255, 85, 85], class_name: "ansi-bright-red" },
                    { rgb: [0, 255, 0], class_name: "ansi-bright-green" },
                    { rgb: [255, 255, 85], class_name: "ansi-bright-yellow" },
                    { rgb: [85, 85, 255], class_name: "ansi-bright-blue" },
                    { rgb: [255, 85, 255], class_name: "ansi-bright-magenta" },
                    { rgb: [85, 255, 255], class_name: "ansi-bright-cyan" },
                    { rgb: [255, 255, 255], class_name: "ansi-bright-white" }
                ]
            ];
        this.palette_256 = [];
        this.ansi_colors.forEach(palette => {
            palette.forEach(rec => {
                this.palette_256.push(rec);
            });
        });
        let levels = [0, 95, 135, 175, 215, 255];
        for (let r = 0; r < 6; ++r) {
            for (let g = 0; g < 6; ++g) {
                for (let b = 0; b < 6; ++b) {
                    let col = { rgb: [levels[r], levels[g], levels[b]], class_name: 'truecolor' };
                    this.palette_256.push(col);
                }
            }
        }
        let grey_level = 8;
        for (let i = 0; i < 24; ++i, grey_level += 10) {
            let gry = { rgb: [grey_level, grey_level, grey_level], class_name: 'truecolor' };
            this.palette_256.push(gry);
        }
    }
    escape_txt_for_html(txt) {
        if (!this._escape_html)
            return txt;
        return txt.replace(/[&<>"']/gm, (str) => {
            if (str === "&")
                return "&amp;";
            if (str === "<")
                return "&lt;";
            if (str === ">")
                return "&gt;";
            if (str === "\"")
                return "&quot;";
            if (str === "'")
                return "&#x27;";
        });
    }
    append_buffer(txt) {
        var str = this._buffer + txt;
        this._buffer = str;
    }
    get_next_packet() {
        var pkt = {
            kind: PacketKind.EOS,
            text: '',
            url: ''
        };
        var len = this._buffer.length;
        if (len == 0)
            return pkt;
        var pos = this._buffer.indexOf("\x1B");
        if (pos == -1) {
            pkt.kind = PacketKind.Text;
            pkt.text = this._buffer;
            this._buffer = '';
            return pkt;
        }
        if (pos > 0) {
            pkt.kind = PacketKind.Text;
            pkt.text = this._buffer.slice(0, pos);
            this._buffer = this._buffer.slice(pos);
            return pkt;
        }
        if (pos == 0) {
            if (len < 3) {
                pkt.kind = PacketKind.Incomplete;
                return pkt;
            }
            var next_char = this._buffer.charAt(1);
            if ((next_char != '[') && (next_char != ']') && (next_char != '(')) {
                pkt.kind = PacketKind.ESC;
                pkt.text = this._buffer.slice(0, 1);
                this._buffer = this._buffer.slice(1);
                return pkt;
            }
            if (next_char == '[') {
                if (!this._csi_regex) {
                    this._csi_regex = rgx(templateObject_1 || (templateObject_1 = __makeTemplateObject(["\n                        ^                           # beginning of line\n                                                    #\n                                                    # First attempt\n                        (?:                         # legal sequence\n                          \u001B[                      # CSI\n                          ([<-?]?)              # private-mode char\n                          ([d;]*)                    # any digits or semicolons\n                          ([ -/]?               # an intermediate modifier\n                          [@-~])                # the command\n                        )\n                        |                           # alternate (second attempt)\n                        (?:                         # illegal sequence\n                          \u001B[                      # CSI\n                          [ -~]*                # anything legal\n                          ([\0-\u001F:])              # anything illegal\n                        )\n                    "], ["\n                        ^                           # beginning of line\n                                                    #\n                                                    # First attempt\n                        (?:                         # legal sequence\n                          \\x1b\\[                      # CSI\n                          ([\\x3c-\\x3f]?)              # private-mode char\n                          ([\\d;]*)                    # any digits or semicolons\n                          ([\\x20-\\x2f]?               # an intermediate modifier\n                          [\\x40-\\x7e])                # the command\n                        )\n                        |                           # alternate (second attempt)\n                        (?:                         # illegal sequence\n                          \\x1b\\[                      # CSI\n                          [\\x20-\\x7e]*                # anything legal\n                          ([\\x00-\\x1f:])              # anything illegal\n                        )\n                    "])));
                }
                let match = this._buffer.match(this._csi_regex);
                if (match === null) {
                    pkt.kind = PacketKind.Incomplete;
                    return pkt;
                }
                if (match[4]) {
                    pkt.kind = PacketKind.ESC;
                    pkt.text = this._buffer.slice(0, 1);
                    this._buffer = this._buffer.slice(1);
                    return pkt;
                }
                if ((match[1] != '') || (match[3] != 'm'))
                    pkt.kind = PacketKind.Unknown;
                else
                    pkt.kind = PacketKind.SGR;
                pkt.text = match[2];
                var rpos = match[0].length;
                this._buffer = this._buffer.slice(rpos);
                return pkt;
            }
            else if (next_char == ']') {
                if (len < 4) {
                    pkt.kind = PacketKind.Incomplete;
                    return pkt;
                }
                if ((this._buffer.charAt(2) != '8')
                    || (this._buffer.charAt(3) != ';')) {
                    pkt.kind = PacketKind.ESC;
                    pkt.text = this._buffer.slice(0, 1);
                    this._buffer = this._buffer.slice(1);
                    return pkt;
                }
                if (!this._osc_st) {
                    this._osc_st = rgxG(templateObject_2 || (templateObject_2 = __makeTemplateObject(["\n                        (?:                         # legal sequence\n                          (\u001B\\)                    # ESC                           |                           # alternate\n                          (\u0007)                      # BEL (what xterm did)\n                        )\n                        |                           # alternate (second attempt)\n                        (                           # illegal sequence\n                          [\0-\u0006]                 # anything illegal\n                          |                           # alternate\n                          [\b-\u001A]                 # anything illegal\n                          |                           # alternate\n                          [\u001C-\u001F]                 # anything illegal\n                        )\n                    "], ["\n                        (?:                         # legal sequence\n                          (\\x1b\\\\)                    # ESC \\\n                          |                           # alternate\n                          (\\x07)                      # BEL (what xterm did)\n                        )\n                        |                           # alternate (second attempt)\n                        (                           # illegal sequence\n                          [\\x00-\\x06]                 # anything illegal\n                          |                           # alternate\n                          [\\x08-\\x1a]                 # anything illegal\n                          |                           # alternate\n                          [\\x1c-\\x1f]                 # anything illegal\n                        )\n                    "])));
                }
                this._osc_st.lastIndex = 0;
                {
                    let match = this._osc_st.exec(this._buffer);
                    if (match === null) {
                        pkt.kind = PacketKind.Incomplete;
                        return pkt;
                    }
                    if (match[3]) {
                        pkt.kind = PacketKind.ESC;
                        pkt.text = this._buffer.slice(0, 1);
                        this._buffer = this._buffer.slice(1);
                        return pkt;
                    }
                }
                {
                    let match = this._osc_st.exec(this._buffer);
                    if (match === null) {
                        pkt.kind = PacketKind.Incomplete;
                        return pkt;
                    }
                    if (match[3]) {
                        pkt.kind = PacketKind.ESC;
                        pkt.text = this._buffer.slice(0, 1);
                        this._buffer = this._buffer.slice(1);
                        return pkt;
                    }
                }
                if (!this._osc_regex) {
                    this._osc_regex = rgx(templateObject_3 || (templateObject_3 = __makeTemplateObject(["\n                        ^                           # beginning of line\n                                                    #\n                        \u001B]8;                    # OSC Hyperlink\n                        [ -:<-~]*       # params (excluding ;)\n                        ;                           # end of params\n                        ([!-~]{0,512})        # URL capture\n                        (?:                         # ST\n                          (?:\u001B\\)                  # ESC                           |                           # alternate\n                          (?:\u0007)                    # BEL (what xterm did)\n                        )\n                        ([ -~]+)              # TEXT capture\n                        \u001B]8;;                   # OSC Hyperlink End\n                        (?:                         # ST\n                          (?:\u001B\\)                  # ESC                           |                           # alternate\n                          (?:\u0007)                    # BEL (what xterm did)\n                        )\n                    "], ["\n                        ^                           # beginning of line\n                                                    #\n                        \\x1b\\]8;                    # OSC Hyperlink\n                        [\\x20-\\x3a\\x3c-\\x7e]*       # params (excluding ;)\n                        ;                           # end of params\n                        ([\\x21-\\x7e]{0,512})        # URL capture\n                        (?:                         # ST\n                          (?:\\x1b\\\\)                  # ESC \\\n                          |                           # alternate\n                          (?:\\x07)                    # BEL (what xterm did)\n                        )\n                        ([\\x20-\\x7e]+)              # TEXT capture\n                        \\x1b\\]8;;                   # OSC Hyperlink End\n                        (?:                         # ST\n                          (?:\\x1b\\\\)                  # ESC \\\n                          |                           # alternate\n                          (?:\\x07)                    # BEL (what xterm did)\n                        )\n                    "])));
                }
                let match = this._buffer.match(this._osc_regex);
                if (match === null) {
                    pkt.kind = PacketKind.ESC;
                    pkt.text = this._buffer.slice(0, 1);
                    this._buffer = this._buffer.slice(1);
                    return pkt;
                }
                pkt.kind = PacketKind.OSCURL;
                pkt.url = match[1];
                pkt.text = match[2];
                var rpos = match[0].length;
                this._buffer = this._buffer.slice(rpos);
                return pkt;
            }
            else if (next_char == '(') {
                pkt.kind = PacketKind.Unknown;
                this._buffer = this._buffer.slice(3);
                return pkt;
            }
        }
    }
    ansi_to_html(txt) {
        this.append_buffer(txt);
        var blocks = [];
        while (true) {
            var packet = this.get_next_packet();
            if ((packet.kind == PacketKind.EOS)
                || (packet.kind == PacketKind.Incomplete))
                break;
            if ((packet.kind == PacketKind.ESC)
                || (packet.kind == PacketKind.Unknown))
                continue;
            if (packet.kind == PacketKind.Text)
                blocks.push(this.transform_to_html(this.with_state(packet)));
            else if (packet.kind == PacketKind.SGR)
                this.process_ansi(packet);
            else if (packet.kind == PacketKind.OSCURL)
                blocks.push(this.process_hyperlink(packet));
        }
        return blocks.join("");
    }
    with_state(pkt) {
        return { bold: this.bold, faint: this.faint, italic: this.italic, underline: this.underline, fg: this.fg, bg: this.bg, text: pkt.text };
    }
    process_ansi(pkt) {
        let sgr_cmds = pkt.text.split(';');
        while (sgr_cmds.length > 0) {
            let sgr_cmd_str = sgr_cmds.shift();
            let num = parseInt(sgr_cmd_str, 10);
            if (isNaN(num) || num === 0) {
                this.fg = null;
                this.bg = null;
                this.bold = false;
                this.faint = false;
                this.italic = false;
                this.underline = false;
            }
            else if (num === 1) {
                this.bold = true;
            }
            else if (num === 2) {
                this.faint = true;
            }
            else if (num === 3) {
                this.italic = true;
            }
            else if (num === 4) {
                this.underline = true;
            }
            else if (num === 21) {
                this.bold = false;
            }
            else if (num === 22) {
                this.faint = false;
                this.bold = false;
            }
            else if (num === 23) {
                this.italic = false;
            }
            else if (num === 24) {
                this.underline = false;
            }
            else if (num === 39) {
                this.fg = null;
            }
            else if (num === 49) {
                this.bg = null;
            }
            else if ((num >= 30) && (num < 38)) {
                this.fg = this.ansi_colors[0][(num - 30)];
            }
            else if ((num >= 40) && (num < 48)) {
                this.bg = this.ansi_colors[0][(num - 40)];
            }
            else if ((num >= 90) && (num < 98)) {
                this.fg = this.ansi_colors[1][(num - 90)];
            }
            else if ((num >= 100) && (num < 108)) {
                this.bg = this.ansi_colors[1][(num - 100)];
            }
            else if (num === 38 || num === 48) {
                if (sgr_cmds.length > 0) {
                    let is_foreground = (num === 38);
                    let mode_cmd = sgr_cmds.shift();
                    if (mode_cmd === '5' && sgr_cmds.length > 0) {
                        let palette_index = parseInt(sgr_cmds.shift(), 10);
                        if (palette_index >= 0 && palette_index <= 255) {
                            if (is_foreground)
                                this.fg = this.palette_256[palette_index];
                            else
                                this.bg = this.palette_256[palette_index];
                        }
                    }
                    if (mode_cmd === '2' && sgr_cmds.length > 2) {
                        let r = parseInt(sgr_cmds.shift(), 10);
                        let g = parseInt(sgr_cmds.shift(), 10);
                        let b = parseInt(sgr_cmds.shift(), 10);
                        if ((r >= 0 && r <= 255) && (g >= 0 && g <= 255) && (b >= 0 && b <= 255)) {
                            let c = { rgb: [r, g, b], class_name: 'truecolor' };
                            if (is_foreground)
                                this.fg = c;
                            else
                                this.bg = c;
                        }
                    }
                }
            }
        }
    }
    transform_to_html(fragment) {
        let txt = fragment.text;
        if (txt.length === 0)
            return txt;
        txt = this.escape_txt_for_html(txt);
        if (!fragment.bold && !fragment.italic && !fragment.underline && fragment.fg === null && fragment.bg === null)
            return txt;
        let styles = [];
        let classes = [];
        let fg = fragment.fg;
        let bg = fragment.bg;
        if (fragment.bold)
            styles.push(this._boldStyle);
        if (fragment.faint)
            styles.push(this._faintStyle);
        if (fragment.italic)
            styles.push(this._italicStyle);
        if (fragment.underline)
            styles.push(this._underlineStyle);
        if (!this._use_classes) {
            if (fg)
                styles.push(`color:rgb(${fg.rgb.join(',')})`);
            if (bg)
                styles.push(`background-color:rgb(${bg.rgb})`);
        }
        else {
            if (fg) {
                if (fg.class_name !== 'truecolor') {
                    classes.push(`${fg.class_name}-fg`);
                }
                else {
                    styles.push(`color:rgb(${fg.rgb.join(',')})`);
                }
            }
            if (bg) {
                if (bg.class_name !== 'truecolor') {
                    classes.push(`${bg.class_name}-bg`);
                }
                else {
                    styles.push(`background-color:rgb(${bg.rgb.join(',')})`);
                }
            }
        }
        let class_string = '';
        let style_string = '';
        if (classes.length)
            class_string = ` class="${classes.join(' ')}"`;
        if (styles.length)
            style_string = ` style="${styles.join(';')}"`;
        return `<span${style_string}${class_string}>${txt}</span>`;
    }
    ;
    process_hyperlink(pkt) {
        let parts = pkt.url.split(':');
        if (parts.length < 1)
            return '';
        if (!this._url_allowlist[parts[0]])
            return '';
        let result = `<a href="${this.escape_txt_for_html(pkt.url)}">${this.escape_txt_for_html(pkt.text)}</a>`;
        return result;
    }
}
function rgx(tmplObj, ...subst) {
    let regexText = tmplObj.raw[0];
    let wsrgx = /^\s+|\s+\n|\s*#[\s\S]*?\n|\n/gm;
    let txt2 = regexText.replace(wsrgx, '');
    return new RegExp(txt2);
}
function rgxG(tmplObj, ...subst) {
    let regexText = tmplObj.raw[0];
    let wsrgx = /^\s+|\s+\n|\s*#[\s\S]*?\n|\n/gm;
    let txt2 = regexText.replace(wsrgx, '');
    return new RegExp(txt2, 'g');
}
var templateObject_1, templateObject_2, templateObject_3;
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个将 ANSI 转义序列(终端控制符/颜色/样式)转换成 HTML 的 JavaScript/TypeScript 类实现,主要是 AnsiUp 库的源码(版本 6.0.1)。具体功能是:输入包含 ANSI 的文本,解析其中的颜色、粗体、下划线、超链接等控制序列,输出对应的 HTML <span><a> 标签,同时保留普通文本。

我将从整体结构、解析流程、核心逻辑等方面解释这段代码。

1. 整体结构

代码主要包含:

  • PacketKind:枚举,表示解析出的“数据包”类型。
  • AnsiUp 类:核心转换器,保存状态、缓冲区和按钮配置。
  • rgx / rgxG:辅助函数,用于从带注释的多行字符串模板中生成正则表达式。

2. PacketKind 枚举

每种包代表解析缓冲区时得到的不同片段:

含义
EOSEnd of stream,表示缓冲已结束,无更多数据
Text普通文本,不是 ANSI 控制序列
Incomplete缓冲区中的内容不足以构成完整序列,需要等待更多数据
ESC单独的 ESC 字符或非完整的非法转义
Unknown不被支持的 CSI 或其他无法识别的序列
SGRSelect Graphic Rendition,例如 \x1b[31m 设置颜色/样式
OSCURLOSC 超链接序列,例如 \x1b]8;;http://...\x1b\\text\x1b]8;;\x1b\\

3. AnsiUp 核心属性

  • VERSION:版本号。
  • _use_classes:是否用 CSS 类而不是内联样式。默认为 false,即输出用 style="..."
  • boldfaintitalicunderline:文本样式状态。
  • fgbg:前景色和背景色对象,形如 { rgb: [r,g,b], class_name: "ansi-red" }
  • _buffer:内部缓冲区,用于积累尚未解析完整的数据。
  • _url_allowlist:允许生成超链接的 URL 协议白名单,例如 { 'http':1, 'https':1 }
  • _escape_html:是否对 HTML 特殊字符进行转义。
  • _boldStyle 等样式字符串:可自定义输出样式,例如 "font-weight:bold"

4. 调色板初始化 setup_palettes

ansi_colors 是标准的 16 种 ANSI 颜色,分为两行:

  • 第一行:基础色(黑、红、绿、黄、蓝、紫、青、白)。
  • 第二行:亮色(bright 色)。

palette_256 则用于支持 256 色模式,由三部分组成:

  1. 16 种基础/亮色。
  2. 216 种颜色立方体(6×6×6),RGB 值来自 [0, 95, 135, 175, 215, 255]
  3. 24 阶灰度梯度,从 8 开始每次加 10。

每个颜色都有 rgb 数组和 class_name(用于 CSS 类模式)。颜色立方体与灰度的 class_name 为 'truecolor',表示它们无法映射到简单 CSS 类,只能用内联 RGB 样式表示。

5. 缓冲区与分包解析

append_buffer(txt)

将新输入追加到 _buffer 尾部。

get_next_packet()

作用是每次从缓冲区中取出一个最前面的“包”,可能是一个文本块、一个控制序列、一个超链接等。具体逻辑如下:

  • 如果缓冲区为空,返回 EOS 包。
  • 查找 \x1B(ESC 字符)的位置:
    • 如果没有 ESC,说明全是普通文本,返回 Text 包,清空缓冲区。
    • 如果 ESC 不在开头,则先将 ESC 前的普通文本作为一个 Text 包返回,并保留从 ESC 开始的剩余缓冲区。
  • 如果 ESC 在开头:
    • 如果少于 3 个字符,可能是不完整序列,返回 Incomplete
    • 根据 ESC 后的字符判断类型:
      • [ :CSI 序列,例如 \x1b[31m
      • ] :OSC 序列,例如 \x1b]8;;url...
      • ( :字符集选择(较少用),识别后跳过 3 个字符,返回 Unknown
      • 其他:单独的 ESC(比如用户按 ESC 键),返回 ESC 包,并从缓冲区中移除这个字符。

CSI(\x1b[...)的解析

使用 _csi_regex 匹配 CSI 序列:

^\x1b\[([<-?]?)([\d;]*)([ -/]?[@-~])
  • ([<-?]?):私有模式字符(如 >?)。
  • ([\d;]*):参数部分。
  • ([ -/]?[@-~]):中间修饰符和最终字节(命令)。

如果匹配不合法(即出现非法字符),则返回 ESC 包,只吃掉那个 ESC 字符。这是为了避免缓冲区卡死,能逐步跳过损坏的数据。

如果匹配成功但命令不是 m(即不是 SGR),则会返回 Unknown。如果是 m,则认为它是 SGR,包中保存的是参数部分(如 31;1)。随后从缓冲区中移除匹配到的整个序列。

OSC 超链接(\x1b]8;...)的解析

当遇到 \x1b]8; 时,代码会尝试用 _osc_regex 匹配:

\x1b]8;                           # OSC Hyperlink
[\x20-\x3a\x3c-\x7e]*             # 参数(不包含分号)
;                                 # 分隔
([\x21-\x7e]{0,512})              # URL 捕获
(?:\x1b\\|\x07)                   # ST 终止符,ESC \ 或 BEL
([\x20-\x7e]+)                    # 链接显示的文本捕获
\x1b]8;;                          # OSC Hyperlink 结束
(?:\x1b\\|\x07)

匹配成功后返回 OSCURL 包,url 为链接地址,text 为链接显示的文字。

在匹配之前还有一段对 _osc_st 的两次匹配逻辑。_osc_st 用来查找 OSC 的终止符或者非法字符,用于判断当前缓冲区中的 OSC 序列是否完整。如果第一个 _osc_st.exec 找不到,则返回 Incomplete;如果找到了但匹配到的是非法字符(match[3]),则返回 ESC 包,只吃掉第一个字符,从而让程序能继续往前解析。整体上是在应对不完整的 OSC 序列。

6. 主转换方法与流程

ansi_to_html(txt)

核心入口方法:

  1. txt 追加到缓冲区。
  2. 循环调用 get_next_packet() 直到得到 EOSIncomplete
  3. 对于每个包:
    • Text:用当前样式状态包装成 HTML,加入结果数组。
    • SGR:更新内部样式状态(process_ansi)。
    • OSCURL:生成 <a> 超链接。
    • ESC / Unknown:忽略。
  4. 最后把所有 HTML 片段拼接返回。

注意:如果缓冲区末尾还有不完整序列(Incomplete),会留在 _buffer 中,等待下次继续输入。这使得流式数据处理成为可能。

7. SGR 状态处理 process_ansi(pkt)

SGR 包中存有类似 "1;31;44" 的参数串,用分号拆开后逐个处理:

  • 无参数或 0:重置所有样式(前景、背景、粗体、弱化、斜体、下划线)。
  • 1:粗体;2:弱化(faint);3:斜体;4:下划线。
  • 21 ~ 24:关闭对应样式(其中 21 关闭粗体,22 关闭弱化和粗体,23 关闭斜体,24 关闭下划线)。
  • 39:默认前景色;49:默认背景色。
  • 30 ~ 37:前景色(基础色)。
  • 40 ~ 47:背景色(基础色)。
  • 90 ~ 97:亮色前景。
  • 100 ~ 107:亮色背景。
  • 38 / 48:扩展颜色模式,后面可跟:
    • 5;n:256 色模式,n 为 0~255 的索引。
    • 2;r;g;b:24 位真彩色模式。

代码使用 parseInt 解析数值,并查找 this.ansi_colorsthis.palette_256 获得颜色。

8. 文本转换为 HTML transform_to_html(fragment)

根据当前样式状态将普通文本包包装成 HTML:

  • 首先对文本做 HTML 转义(如果没有关闭转义)。
  • 如果没有任何样式且没有颜色,直接返回文本(不包 <span>)。
  • 否则构造 <span>
    • 根据粗体、弱化、斜体、下划线加入相应 CSS 样式。
    • 如果 _use_classes == false:使用内联样式 color:rgb(...)background-color:rgb(...)
    • 如果 _use_classes == true:对 class_name 不是 'truecolor' 时输出类名,如 ansi-red-fg / ansi-red-bg;对 truecolor 仍使用内联 RGB 样式。

这种设计使得用户可以选择使用样式类还是内联样式,方便主题定制。

9. 超链接处理 process_hyperlink(pkt)

  • 将 URL 用 : 分割提取协议名。
  • 如果协议名不在白名单中(如只允许 httphttps),则忽略不生成链接。
  • 否则生成 <a href="...">text</a>,并对 URL 和文本做 HTML 转义。

10. 正则辅助函数

因为源代码中的正则以多行模板字符串(含注释和换行)书写,直接生成 RegExp 时会包含空白和注释,因此使用 rgxrgxG 将它们清理掉:

function rgx(tmplObj, ...subst) {
    let regexText = tmplObj.raw[0];
    let wsrgx = /^\s+|\s+\n|\s*#[\s\S]*?\n|\n/gm;
    let txt2 = regexText.replace(wsrgx, '');
    return new RegExp(txt2);
}

它删除了行首空白、注释、多余换行等,只留下紧凑的正则实体。例如:

^                           # beginning of line
\x1b\[                      # CSI

会变成 ^\x1b\[rgxGrgx 的区别是添加了 g 标志(全局匹配)。

__makeTemplateObject 是 TypeScript 编译用于处理“cooked”和“raw”模板字符串的标准辅助工具。

11. 适用场景

这段代码适用于需要把终端输出(ANSI 颜色/样式/超链接)显示到 Web 页面中的场景,例如:

  • 日志查看器。
  • 终端模拟器 Web 版。
  • CI/CD 构建输出高亮。
  • 远程命令行展示。

它支持:

  • 基础 16 色和亮色。
  • 256 色。
  • 24 位真彩色。
  • 粗体、暗色、斜体、下划线。
  • OSC 8 超链接。
  • HTML 转义。
  • 流式输入(缓冲区缓存不完整序列)。
  • CSS 类模式或内联样式模式。

总结

这段代码是一个比较完整的 ANSI 到 HTML 转换实现。核心思想是利用“缓冲区 + 状态机 + 正则”解析 ANSI 转义序列,将文本按片段拆包,更新样式状态,并为每个片段生成对应的 HTML。整个过程稳定地处理了不完整输入和未知序列,兼顾了灵活性和可用性。

评论加载中...