Skip to content
On this page

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 进行替换