update term color render

This commit is contained in:
2026-02-06 22:38:46 +08:00
parent 99577f2482
commit 4329da08cb
7 changed files with 315 additions and 137 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
node_modules/
.idea/
dist/
# ---> macOS # ---> macOS
# General # General
.DS_Store .DS_Store

126
index.js
View File

@@ -1,126 +0,0 @@
// index.ts
function parseColorTokens(message) {
const tokens = [];
if (message) {
let inColorStart = false;
let inColorEnd = false;
let chars = [];
const messageLength = message.length;
for (let i = 0; i < messageLength; i++) {
const c = message.charAt(i);
const nextC = i + 1 < messageLength ? message.charAt(i + 1) : null;
switch (c) {
case "\\":
if (nextC === null) {
chars.push(c);
} else {
chars.push(nextC);
i++;
}
break;
case "[":
if (inColorStart || inColorEnd) {
break;
} else if (nextC == "/") {
inColorEnd = true;
i++;
} else {
inColorStart = true;
}
if (chars.length > 0) {
tokens.push({
type: "text",
content: chars.join("")
});
chars = [];
}
break;
case "]":
if (inColorStart || inColorEnd) {
if (chars.length > 0) {
tokens.push({
type: "color",
colorStart: inColorStart,
color: chars.join("")
});
chars = [];
}
inColorStart = false;
inColorEnd = false;
} else {
chars.push(c);
}
break;
default:
chars.push(c);
break;
}
}
const inColor = inColorStart || inColorEnd;
if (chars.length > 0 && !inColor) {
tokens.push({
type: "text",
content: chars.join("")
});
}
}
return tokens;
}
var COLOR_MAP = {
blink: "5",
bold: "1",
under: "4",
red: "31",
green: "32",
yellow: "33",
blue: "34",
pink: "35",
cyan: "36"
};
function renderColorTokens(tokens) {
const text = [];
const colorMapStack = /* @__PURE__ */ new Map();
for (const token of tokens) {
if (token.type === "color") {
const color = token.color;
if (color) {
const colorCode = COLOR_MAP[color];
if (!colorCode) {
text.push(`[${token.colorStart ? "" : "/"}${color}]`);
continue;
}
const colorStack = colorMapStack.get(color) ?? [];
if (colorStack.length == 0) {
colorMapStack.set(color, colorStack);
}
if (token.colorStart) {
colorStack.push(1);
} else {
colorStack.pop();
text.push("\x1B[0m");
}
const colors = [];
for (const [color2, colorStack2] of colorMapStack) {
if (colorStack2.length > 0) {
colors.push(colorCode);
}
}
if (colors.length > 0) {
text.push(`\x1B[${colors.join(";")}m`);
}
}
} else {
if (token.content) {
text.push(token.content);
}
}
}
text.push("\x1B[0m");
return text.join("");
}
function renderColor(message) {
return renderColorTokens(parseColorTokens(message));
}
export {
renderColor
};

48
package-lock.json generated Normal file
View File

@@ -0,0 +1,48 @@
{
"name": "@hatterjiang/term-color-render",
"version": "1.0.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@hatterjiang/term-color-render",
"version": "1.0.2",
"license": "MIT",
"devDependencies": {
"@types/node": "^25.2.1",
"typescript": "^5.9.3"
}
},
"node_modules/@types/node": {
"version": "25.2.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz",
"integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
}
}
}

View File

@@ -1,12 +1,51 @@
{ {
"name": "@hatterjiang/term-color-render", "name": "@hatterjiang/term-color-render",
"version": "1.0.1", "version": "1.0.2",
"description": "Render color for term use syntax like [red]text[/red]", "description": "Render color for term use syntax like [red]text[/red]",
"license": "MIT", "license": "MIT",
"author": "hatterjiang", "author": "Hatter Jiang",
"type": "commonjs", "type": "module",
"main": "index.ts", "main": "./dist/cjs/index.js",
"module": "./dist/mjs/index.js",
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/types/index.d.ts",
"default": "./dist/mjs/index.js"
},
"require": {
"types": "./dist/types/index.d.ts",
"default": "./dist/cjs/index.js"
}
}
},
"repository": {
"type": "git",
"url": "https://git.hatter.ink/hatter/term-color-render.git"
},
"keywords": [
"terminal",
"term",
"color"
],
"files": [
"dist/**/*",
"README.md",
"LICENSE"
],
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "prebuild": "rm -rf dist",
"build": "npm run build:mjs && npm run build:cjs && npm run build:types",
"build:mjs": "tsc --module ESNext --outDir dist/mjs --moduleSuffixes .mjs",
"build:cjs": "tsc --module CommonJS --outDir dist/cjs --moduleSuffixes .cjs",
"build:types": "tsc --emitDeclarationOnly --outDir dist/types",
"lint": "eslint src --ext .ts",
"test": "jest",
"prepublishOnly": "npm run build"
},
"devDependencies": {
"@types/node": "^25.2.1",
"typescript": "^5.9.3"
} }
} }

152
src/index.js Normal file
View File

@@ -0,0 +1,152 @@
function parseColorTokens(message) {
const tokens = [];
if (message) {
let inColorStart = false;
let inColorEnd = false;
let chars = [];
const messageLength = message.length;
for (let i = 0; i < messageLength; i++) {
const c = message.charAt(i);
const nextC = (i + 1) < messageLength
? message.charAt(i + 1)
: null;
switch (c) {
case "\\":
if (nextC === null) {
chars.push(c);
}
else {
chars.push(nextC);
i++;
}
break;
case "[":
if (inColorStart || inColorEnd) {
// SHOULD NOT HAPPEN
break;
}
else if (nextC == "/") {
inColorEnd = true;
i++;
}
else {
inColorStart = true;
}
if (chars.length > 0) {
tokens.push({
type: "text",
content: chars.join(""),
});
chars = [];
}
break;
case "]":
if (inColorStart || inColorEnd) {
if (chars.length > 0) {
tokens.push({
type: "color",
colorStart: inColorStart,
color: chars.join(""),
});
chars = [];
}
inColorStart = false;
inColorEnd = false;
}
else {
chars.push(c);
}
break;
default:
chars.push(c);
break;
}
}
const inColor = inColorStart || inColorEnd;
if (chars.length > 0 && !inColor) {
tokens.push({
type: "text",
content: chars.join(""),
});
}
}
return tokens;
}
const COLOR_MAP = {
blink: "5",
bold: "1",
under: "4",
red: "31",
green: "32",
yellow: "33",
blue: "34",
pink: "35",
cyan: "36",
};
function renderColorTokens(tokens, renderColor) {
const text = [];
const colorMapStack = new Map();
for (const token of tokens) {
if (token.type === "color") {
const color = token.color;
if (color) {
const colorCode = COLOR_MAP[color];
if (!colorCode) {
text.push(`[${token.colorStart ? "" : "/"}${color}]`);
continue;
}
const colorStack = colorMapStack.get(color) ?? [];
if (colorStack.length == 0) {
colorMapStack.set(color, colorStack);
}
if (token.colorStart) {
colorStack.push(1);
}
else {
colorStack.pop();
if (renderColor) {
text.push("\x1b[0m");
}
}
const colors = [];
for (const [color, colorStack] of colorMapStack) {
if (colorStack.length > 0) {
colors.push(colorCode);
}
}
if (colors.length > 0) {
if (renderColor) {
text.push(`\x1b[${colors.join(";")}m`);
}
}
}
}
else {
if (token.content) {
text.push(token.content);
}
}
}
if (renderColor) {
text.push("\x1b[0m"); // FINALLY END ALL COLOR
}
return text.join("");
}
function supportColor() {
try {
if (process.env.FORCE_COLOR !== undefined) {
return process.env.FORCE_COLOR !== '0';
}
if (process.env.NO_COLOR !== undefined) {
return false;
}
return process.stdout.isTTY && process.stderr.isTTY;
}
catch (e) {
// check color support failed, default false
return false;
}
}
export function renderColor(message, renderColor) {
return renderColorTokens(parseColorTokens(message), renderColor ?? supportColor());
}

View File

@@ -88,7 +88,7 @@ const COLOR_MAP: Record<string, string> = {
cyan: "36", cyan: "36",
}; };
function renderColorTokens(tokens: ColorToken[]): string { function renderColorTokens(tokens: ColorToken[], renderColor: boolean): string {
const text: string[] = []; const text: string[] = [];
const colorMapStack = new Map<string, number[]>(); const colorMapStack = new Map<string, number[]>();
for (const token of tokens) { for (const token of tokens) {
@@ -108,8 +108,10 @@ function renderColorTokens(tokens: ColorToken[]): string {
colorStack.push(1); colorStack.push(1);
} else { } else {
colorStack.pop(); colorStack.pop();
if (renderColor) {
text.push("\x1b[0m"); text.push("\x1b[0m");
} }
}
const colors: string[] = []; const colors: string[] = [];
for (const [color, colorStack] of colorMapStack) { for (const [color, colorStack] of colorMapStack) {
if (colorStack.length > 0) { if (colorStack.length > 0) {
@@ -117,19 +119,38 @@ function renderColorTokens(tokens: ColorToken[]): string {
} }
} }
if (colors.length > 0) { if (colors.length > 0) {
if (renderColor) {
text.push(`\x1b[${colors.join(";")}m`); text.push(`\x1b[${colors.join(";")}m`);
} }
} }
}
} else { } else {
if (token.content) { if (token.content) {
text.push(token.content); text.push(token.content);
} }
} }
} }
if (renderColor) {
text.push("\x1b[0m"); // FINALLY END ALL COLOR text.push("\x1b[0m"); // FINALLY END ALL COLOR
}
return text.join(""); return text.join("");
} }
export function renderColor(message: string): string { function supportColor(): boolean {
return renderColorTokens(parseColorTokens(message)); try {
if (process.env.FORCE_COLOR !== undefined) {
return process.env.FORCE_COLOR !== '0';
}
if (process.env.NO_COLOR !== undefined) {
return false;
}
return process.stdout.isTTY && process.stderr.isTTY;
} catch (e) {
// check color support failed, default false
return false;
}
}
export function renderColor(message: string, renderColor?: boolean): string {
return renderColorTokens(parseColorTokens(message), renderColor ?? supportColor());
} }

41
tsconfig.json Normal file
View File

@@ -0,0 +1,41 @@
{
"ts-node": {
"files": true,
"emit": true,
"compilerHost": true
},
"compilerOptions": {
// 基础设置
"target": "es2015",
"module": "ESNext",
"moduleResolution": "node",
"lib": [
"es2015"
],
// 输出设置
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
// 模块兼容性
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
// 严格模式(推荐)
"strict": true,
"skipLibCheck": true,
// "typeRoots": [
// "./node_modules/@types"
// ],
// "types": [
// "node"
// ]
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}