ink-gradient
Version:
Gradient color component for Ink
133 lines (131 loc) • 5.04 kB
JavaScript
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
import { Children, isValidElement, cloneElement, } from 'react';
import { Box, Transform, Text } from 'ink';
import gradientString from 'gradient-string';
import stripAnsi from 'strip-ansi';
/**
@example
```
import React from 'react';
import {render} from 'ink';
import Gradient from 'ink-gradient';
import BigText from 'ink-big-text';
render(
<Gradient name="rainbow">
<BigText text="unicorns"/>
</Gradient>
);
```
*/
const Gradient = props => {
if (props.name && props.colors) {
throw new Error('The `name` and `colors` props are mutually exclusive');
}
let gradient;
if (props.name) {
gradient = gradientString[props.name];
}
else if (props.colors) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
gradient = gradientString(props.colors); // Note: `gradient-string` types are too loose to express this safely.
}
else {
throw new Error('Either `name` or `colors` prop must be provided');
}
const applyGradient = (text) => gradient.multiline(stripAnsi(text));
const containsBoxDescendant = (nodeChildren) => {
let hasBox = false;
const search = (value) => {
Children.forEach(value, child => {
if (hasBox) {
return;
}
if (!isValidElement(child)) {
return;
}
if (child.type === Box) {
hasBox = true;
return;
}
const childProps = child.props;
if (Object.hasOwn(childProps, 'children')) {
search(childProps['children']);
}
});
};
search(nodeChildren);
return hasBox;
};
const hasChildrenProp = (props) => Object.hasOwn(props, 'children');
const isPlainTextNode = (node) => typeof node === 'string' || typeof node === 'number';
const isNonRenderableChild = (node) => node === null || node === undefined || typeof node === 'boolean';
const childrenCount = Children.count(props.children);
// Check if children is just a string/number (simple case)
if (isPlainTextNode(props.children)) {
return _jsx(Transform, { transform: applyGradient, children: props.children });
}
if (childrenCount === 1 && !containsBoxDescendant(props.children)) {
return _jsx(Transform, { transform: applyGradient, children: props.children });
}
// For complex children (components), apply gradient to text nodes directly
const applyGradientToChildren = (children) => {
const nodes = [];
let bufferedText = '';
let nodeIndex = 0;
const createKey = () => `gradient-node-${nodeIndex++}`;
const pushTransformed = (node, key) => {
nodes.push(_jsx(Transform, { transform: applyGradient, children: node }, key));
};
const flushText = () => {
if (bufferedText === '') {
return;
}
const text = bufferedText;
bufferedText = '';
pushTransformed(_jsx(Text, { children: text }), createKey());
};
Children.forEach(children, child => {
if (isNonRenderableChild(child)) {
return;
}
if (isPlainTextNode(child)) {
bufferedText += String(child);
return;
}
flushText();
if (isValidElement(child)) {
const childKey = child.key ?? createKey();
const childProps = child.props;
if (child.type === Text) {
pushTransformed(child, childKey);
return;
}
if (child.type === Box) {
if (hasChildrenProp(childProps)) {
const childChildren = childProps['children'];
nodes.push(cloneElement(child, { key: childKey }, applyGradientToChildren(childChildren)));
return;
}
nodes.push(cloneElement(child, { key: childKey }));
return;
}
if (hasChildrenProp(childProps)) {
const childChildren = childProps['children'];
if (!containsBoxDescendant(childChildren)) {
pushTransformed(child, childKey);
return;
}
nodes.push(cloneElement(child, { key: childKey }, applyGradientToChildren(childChildren)));
return;
}
pushTransformed(child, childKey);
return;
}
nodes.push(child);
});
flushText();
return nodes;
};
return _jsx(_Fragment, { children: applyGradientToChildren(props.children) });
};
export default Gradient;