colorette
轻松设置终端文本的颜色和样式
- 无依赖
- 自动颜色支持识别
- 比替代品快2倍
ts
支持- 对
NO_COLOR
友好 Node >= 10
NO_COLOR
是什么
简单来说, NO_COLOR
是一个是否需要颜色和样式而产生的规范, 其主要定义于环境变量, 可以使得满足规范的终端在获取到该环境变量时, 输出文本不带有颜色和样式
ts
//* 极少用到的nodejs内置包,主要用来判断是否是tty
import * as tty from "tty"
const {
env = {},
argv = [],
platform = "",
} = typeof process === "undefined" ? {} : process
//* 是否禁用, 根据 NO_COLOR 环境变量, 这里还支持了 命令行参数
const isDisabled = "NO_COLOR" in env || argv.includes("--no-color")
//* 同理, 也可以设置成强制使用颜色输出, 方式与 NO_COLOR 一样
const isForced = "FORCE_COLOR" in env || argv.includes("--color")
//* 是否是 windows 系统
const isWindows = platform === "win32"
//* 是否是静默终端, 这里还没明白是什么意思, 其是读取的环境变量
const isDumbTerminal = env.TERM === "dumb"
//* 便携式终端, 也没明白哈
const isCompatibleTerminal =
tty && tty.isatty && tty.isatty(1) && env.TERM && !isDumbTerminal
//* 是否是CI环境, 比如 github actions, gitlab ci, circle ci
const isCI =
"CI" in env &&
("GITHUB_ACTIONS" in env || "GITLAB_CI" in env || "CIRCLECI" in env)
//* 识别颜色支持
export const isColorSupported =
!isDisabled &&
(isForced || (isWindows && !isDumbTerminal) || isCompatibleTerminal || isCI)
//* 用户输入文本和当前结束标签冲突
//* 进行结束标签替换
const replaceClose = (
index,
string,
close,
replace,
//* 第一个结束标签前 + 替换标签
head = string.substring(0, index) + replace,
//* 结束标签后面的内容
tail = string.substring(index + close.length),
//* 结束标签后面是否还有结束标签
next = tail.indexOf(close)
//* 没有next: 第一个结束标签前+替换标签+结束标签后面的内容
//* 比如用红色包裹如下中间带有绿色的字符串
//* red \x1b[32m green \x1b[39m red => \x1b[31m red \x1b[32m green \x1b[31m red \x1b[39m
//* 有next: 则next后面的再递归替换
) => head + (next < 0 ? tail : replaceClose(next, tail, close, replace))
const clearBleed = (index, string, open, close, replace) =>
index < 0
//* 用户输入与当前没有冲突, 直接把用户字符串放到开闭标签中间即可
? open + string + close
//* 用户输入与当前有冲突或者说是标签嵌套
//* 则替换结束标签
: open + replaceClose(index, string, close, replace) + close
//* 高阶函数, 闭包
const filterEmpty =
//* 默认用开始标签替换
//* 为什么? 为什么不是用结束标签替换?
//* 下文有简略说明
(open, close, replace = open, at = open.length + 1) =>
//* 供外部调用的函数
(string) =>
string || !(string === "" || string === undefined)
? clearBleed(
//* 可能用户自定义了颜色或样式, 则可能和内部的有冲突
//* 判断是否有冲突, 从开始标签后开始查找第一个结束标签
("" + string).indexOf(close, at),
string,
open,
close,
replace
)
: ""
const init = (open, close, replace) =>
filterEmpty(`\x1b[${open}m`, `\x1b[${close}m`, replace)
const colors = {
reset: init(0, 0),
bold: init(1, 22, "\x1b[22m\x1b[1m"),
dim: init(2, 22, "\x1b[22m\x1b[2m"),
italic: init(3, 23),
underline: init(4, 24),
inverse: init(7, 27),
hidden: init(8, 28),
strikethrough: init(9, 29),
black: init(30, 39),
red: init(31, 39),
green: init(32, 39),
//- 只截取部分
}
export const createColors = ({ useColor = isColorSupported } = {}) =>
useColor
? colors
: Object.keys(colors).reduce(
//* 无颜色时, 使用 String 进行处理
(colors, key) => ({ ...colors, [key]: String }),
{}
)
export const {
reset,
bold,
dim,
italic,
underline,
inverse,
hidden,
strikethrough,
black,
red,
green,
//- 只截取部分
} = createColors()
标签替换说明
与
HTML
的开始标签和结束标签配对的实现方式不同
开闭标签配对, 但结果错误
console.log('\x1b[31m A \x1b[32m green \x1b[39m B \x1b[39m');
输出 A green B开闭标签未配对, 结果正确
console.log('\x1b[31m A \x1b[32m green \x1b[31m B \x1b[39m');
输出 A green B
这就是为什么上面源码中 replace
默认使用 close
进行替换