feat: add gradient and theme features with corresponding tests and documentation
This commit is contained in:
parent
aa06bb5ac3
commit
c017dd5b04
8 changed files with 505 additions and 13 deletions
36
source/index.d.ts
vendored
Normal file → Executable file
36
source/index.d.ts
vendored
Normal file → Executable file
|
|
@ -121,6 +121,21 @@ export interface ChalkInstance {
|
|||
*/
|
||||
bgAnsi256: (index: number) => this;
|
||||
|
||||
/**
|
||||
Create a gradient between colors and apply it to the text.
|
||||
|
||||
@param colors - Array of colors (hex strings or RGB arrays) to gradient between.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.gradient('#ff0000', '#0000ff')('Gradient text');
|
||||
chalk.gradient([255, 0, 0], [0, 0, 255])('RGB gradient');
|
||||
```
|
||||
*/
|
||||
gradient: (...colors: Array<string | [number, number, number]>) => this;
|
||||
|
||||
/**
|
||||
Modifier: Reset the current style.
|
||||
*/
|
||||
|
|
@ -228,6 +243,27 @@ export interface ChalkInstance {
|
|||
readonly bgMagentaBright: this;
|
||||
readonly bgCyanBright: this;
|
||||
readonly bgWhiteBright: this;
|
||||
|
||||
/**
|
||||
Create a themed Chalk instance with custom styles.
|
||||
|
||||
@param theme - An object where keys are style names and values are Chalk instances.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
const themedChalk = chalk.theme({
|
||||
error: chalk.red.bold,
|
||||
success: chalk.green,
|
||||
warning: chalk.yellow,
|
||||
});
|
||||
|
||||
console.log(themedChalk.error('This is an error'));
|
||||
console.log(themedChalk.success('This is a success'));
|
||||
```
|
||||
*/
|
||||
theme<T extends Record<string, this>>(theme: T): this & T;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
61
source/index.js
Normal file → Executable file
61
source/index.js
Normal file → Executable file
|
|
@ -3,6 +3,8 @@ import supportsColor from '#supports-color';
|
|||
import { // eslint-disable-line import/order
|
||||
stringReplaceAll,
|
||||
stringEncaseCRLFWithFirstIndex,
|
||||
createGradientStyler,
|
||||
applyGradient,
|
||||
} from './utilities.js';
|
||||
|
||||
const {stdout: stdoutColor, stderr: stderrColor} = supportsColor;
|
||||
|
|
@ -71,6 +73,18 @@ styles.visible = {
|
|||
},
|
||||
};
|
||||
|
||||
styles.theme = {
|
||||
value(theme) {
|
||||
const themed = createBuilder(this, this[STYLER], this[IS_EMPTY]);
|
||||
|
||||
for (const [key, value] of Object.entries(theme)) {
|
||||
Object.defineProperty(themed, key, {value});
|
||||
}
|
||||
|
||||
return themed;
|
||||
},
|
||||
};
|
||||
|
||||
const getModelAnsi = (model, level, type, ...arguments_) => {
|
||||
if (model === 'rgb') {
|
||||
if (level === 'ansi16m') {
|
||||
|
|
@ -116,6 +130,12 @@ for (const model of usedModels) {
|
|||
};
|
||||
}
|
||||
|
||||
styles.gradient = {
|
||||
get() {
|
||||
return (...colors) => createBuilder(this, createGradientStyler(colors), this[IS_EMPTY]);
|
||||
},
|
||||
};
|
||||
|
||||
const proto = Object.defineProperties(() => {}, {
|
||||
...styles,
|
||||
level: {
|
||||
|
|
@ -176,7 +196,39 @@ const applyStyle = (self, string) => {
|
|||
return string;
|
||||
}
|
||||
|
||||
const {openAll, closeAll} = styler;
|
||||
// Find gradient styler in the chain
|
||||
let gradientStyler = null;
|
||||
let current = styler;
|
||||
|
||||
while (current) {
|
||||
if (current.gradient) {
|
||||
gradientStyler = current;
|
||||
break;
|
||||
}
|
||||
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
let openAll;
|
||||
let closeAll;
|
||||
if (gradientStyler) {
|
||||
// Build openAll/closeAll excluding gradient stylers
|
||||
openAll = '';
|
||||
closeAll = '';
|
||||
current = styler;
|
||||
|
||||
while (current) {
|
||||
if (!current.gradient) {
|
||||
openAll = current.open + openAll;
|
||||
closeAll += current.close;
|
||||
}
|
||||
|
||||
current = current.parent;
|
||||
}
|
||||
} else {
|
||||
({openAll, closeAll} = styler);
|
||||
}
|
||||
|
||||
if (string.includes('\u001B')) {
|
||||
while (styler !== undefined) {
|
||||
// Replace any instances already present with a re-opening code
|
||||
|
|
@ -188,6 +240,13 @@ const applyStyle = (self, string) => {
|
|||
}
|
||||
}
|
||||
|
||||
// Apply gradient if present
|
||||
if (gradientStyler) {
|
||||
string = applyGradient(string, gradientStyler.colors, self.level);
|
||||
}
|
||||
|
||||
// We can move both next actions out of loop, because remaining actions in loop won't have
|
||||
|
||||
// We can move both next actions out of loop, because remaining actions in loop won't have
|
||||
// any/visible effect on parts we add here. Close the styling before a linebreak and reopen
|
||||
// after next line to fix a bleed issue on macOS: https://github.com/chalk/chalk/pull/92
|
||||
|
|
|
|||
55
source/utilities.js
Normal file → Executable file
55
source/utilities.js
Normal file → Executable file
|
|
@ -1,3 +1,5 @@
|
|||
import ansiStyles from '#ansi-styles';
|
||||
|
||||
// TODO: When targeting Node.js 16, use `String.prototype.replaceAll`.
|
||||
export function stringReplaceAll(string, substring, replacer) {
|
||||
let index = string.indexOf(substring);
|
||||
|
|
@ -31,3 +33,56 @@ export function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
|
|||
returnValue += string.slice(endIndex);
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
export const interpolateRgb = (color1, color2, factor) => [
|
||||
Math.round(color1[0] + ((color2[0] - color1[0]) * factor)),
|
||||
Math.round(color1[1] + ((color2[1] - color1[1]) * factor)),
|
||||
Math.round(color1[2] + ((color2[2] - color1[2]) * factor)),
|
||||
];
|
||||
|
||||
export const createGradientStyler = colors => ({
|
||||
gradient: true,
|
||||
colors: colors.map(color => {
|
||||
if (typeof color === 'string') {
|
||||
return ansiStyles.hexToRgb(color);
|
||||
}
|
||||
|
||||
return color; // Assume [r, g, b]
|
||||
}),
|
||||
open: '',
|
||||
close: ansiStyles.color.close,
|
||||
openAll: '',
|
||||
closeAll: ansiStyles.color.close,
|
||||
parent: undefined,
|
||||
});
|
||||
|
||||
export const applyGradient = (string, colors, level) => {
|
||||
if (colors.length < 2 || !string) {
|
||||
return string;
|
||||
}
|
||||
|
||||
const chars = [...string];
|
||||
let result = '';
|
||||
const segments = colors.length - 1;
|
||||
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
const factor = i / (chars.length - 1 || 1);
|
||||
const segmentIndex = Math.min(Math.floor(factor * segments), segments - 1);
|
||||
const localFactor = (factor * segments) - segmentIndex;
|
||||
const color = interpolateRgb(colors[segmentIndex], colors[segmentIndex + 1], localFactor);
|
||||
|
||||
let code;
|
||||
if (level === 3) {
|
||||
code = ansiStyles.color.ansi16m(...color);
|
||||
} else if (level === 2) {
|
||||
code = ansiStyles.color.ansi256(ansiStyles.rgbToAnsi256(...color));
|
||||
} else {
|
||||
code = ansiStyles.color.ansi(ansiStyles.rgbToAnsi(...color));
|
||||
}
|
||||
|
||||
result += code + chars[i];
|
||||
}
|
||||
|
||||
result += ansiStyles.color.close;
|
||||
return result;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue