"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 "&";
if (str === "<")
return "<";
if (str === ">")
return ">";
if (str === "\"")
return """;
if (str === "'")
return "'";
});
}
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;
这是一个将 ANSI 转义序列(终端控制符/颜色/样式)转换成 HTML 的 JavaScript/TypeScript 类实现,主要是 AnsiUp 库的源码(版本 6.0.1)。具体功能是:输入包含 ANSI 的文本,解析其中的颜色、粗体、下划线、超链接等控制序列,输出对应的 HTML <span> 或 <a> 标签,同时保留普通文本。
我将从整体结构、解析流程、核心逻辑等方面解释这段代码。
1. 整体结构
代码主要包含:
PacketKind:枚举,表示解析出的“数据包”类型。AnsiUp类:核心转换器,保存状态、缓冲区和按钮配置。rgx/rgxG:辅助函数,用于从带注释的多行字符串模板中生成正则表达式。
2. PacketKind 枚举
每种包代表解析缓冲区时得到的不同片段:
| 值 | 含义 |
|---|---|
EOS | End of stream,表示缓冲已结束,无更多数据 |
Text | 普通文本,不是 ANSI 控制序列 |
Incomplete | 缓冲区中的内容不足以构成完整序列,需要等待更多数据 |
ESC | 单独的 ESC 字符或非完整的非法转义 |
Unknown | 不被支持的 CSI 或其他无法识别的序列 |
SGR | Select Graphic Rendition,例如 \x1b[31m 设置颜色/样式 |
OSCURL | OSC 超链接序列,例如 \x1b]8;;http://...\x1b\\text\x1b]8;;\x1b\\ |
3. AnsiUp 核心属性
VERSION:版本号。_use_classes:是否用 CSS 类而不是内联样式。默认为false,即输出用style="..."。bold、faint、italic、underline:文本样式状态。fg、bg:前景色和背景色对象,形如{ 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 色模式,由三部分组成:
- 16 种基础/亮色。
- 216 种颜色立方体(6×6×6),RGB 值来自
[0, 95, 135, 175, 215, 255]。 - 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,说明全是普通文本,返回
- 如果 ESC 在开头:
- 如果少于 3 个字符,可能是不完整序列,返回
Incomplete。 - 根据 ESC 后的字符判断类型:
[:CSI 序列,例如\x1b[31m。]:OSC 序列,例如\x1b]8;;url...。(:字符集选择(较少用),识别后跳过 3 个字符,返回Unknown。- 其他:单独的 ESC(比如用户按 ESC 键),返回
ESC包,并从缓冲区中移除这个字符。
- 如果少于 3 个字符,可能是不完整序列,返回
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)
核心入口方法:
- 将
txt追加到缓冲区。 - 循环调用
get_next_packet()直到得到EOS或Incomplete。 - 对于每个包:
Text:用当前样式状态包装成 HTML,加入结果数组。SGR:更新内部样式状态(process_ansi)。OSCURL:生成<a>超链接。ESC/Unknown:忽略。
- 最后把所有 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_colors 或 this.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 用
:分割提取协议名。 - 如果协议名不在白名单中(如只允许
http、https),则忽略不生成链接。 - 否则生成
<a href="...">text</a>,并对 URL 和文本做 HTML 转义。
10. 正则辅助函数
因为源代码中的正则以多行模板字符串(含注释和换行)书写,直接生成 RegExp 时会包含空白和注释,因此使用 rgx 和 rgxG 将它们清理掉:
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\[。rgxG 与 rgx 的区别是添加了 g 标志(全局匹配)。
__makeTemplateObject 是 TypeScript 编译用于处理“cooked”和“raw”模板字符串的标准辅助工具。
11. 适用场景
这段代码适用于需要把终端输出(ANSI 颜色/样式/超链接)显示到 Web 页面中的场景,例如:
- 日志查看器。
- 终端模拟器 Web 版。
- CI/CD 构建输出高亮。
- 远程命令行展示。
它支持:
- 基础 16 色和亮色。
- 256 色。
- 24 位真彩色。
- 粗体、暗色、斜体、下划线。
- OSC 8 超链接。
- HTML 转义。
- 流式输入(缓冲区缓存不完整序列)。
- CSS 类模式或内联样式模式。
总结
这段代码是一个比较完整的 ANSI 到 HTML 转换实现。核心思想是利用“缓冲区 + 状态机 + 正则”解析 ANSI 转义序列,将文本按片段拆包,更新样式状态,并为每个片段生成对应的 HTML。整个过程稳定地处理了不完整输入和未知序列,兼顾了灵活性和可用性。