2020-08-16 16:00:54 +03:00
|
|
|
import * as base16 from 'base16';
|
|
|
|
import { Base16Theme } from 'base16';
|
|
|
|
import Color from 'color';
|
|
|
|
import * as CSS from 'csstype';
|
|
|
|
import curry from 'lodash.curry';
|
|
|
|
import { Color as ColorTuple, yuv2rgb, rgb2yuv } from './colorConverters';
|
|
|
|
import {
|
|
|
|
Styling,
|
|
|
|
StylingConfig,
|
|
|
|
StylingFunction,
|
|
|
|
StylingValue,
|
|
|
|
StylingValueFunction,
|
|
|
|
Theme,
|
|
|
|
} from './types';
|
|
|
|
|
|
|
|
const DEFAULT_BASE16 = base16.default;
|
|
|
|
|
|
|
|
const BASE16_KEYS = Object.keys(DEFAULT_BASE16);
|
|
|
|
|
|
|
|
// we need a correcting factor, so that a dark, but not black background color
|
|
|
|
// converts to bright enough inversed color
|
|
|
|
const flip = (x: number) => (x < 0.25 ? 1 : x < 0.5 ? 0.9 - x : 1.1 - x);
|
|
|
|
|
|
|
|
const invertColor = (hexString: string) => {
|
|
|
|
const color = Color(hexString);
|
|
|
|
const [y, u, v] = rgb2yuv(color.array() as ColorTuple);
|
|
|
|
const flippedYuv: ColorTuple = [flip(y), u, v];
|
|
|
|
const rgb = yuv2rgb(flippedYuv);
|
|
|
|
return Color.rgb(rgb).hex();
|
|
|
|
};
|
|
|
|
|
2020-08-22 04:51:57 +03:00
|
|
|
const merger = (styling: Partial<Styling>) => {
|
|
|
|
return (prevStyling: Partial<Styling>) => ({
|
2020-08-16 16:00:54 +03:00
|
|
|
className: [prevStyling.className, styling.className]
|
|
|
|
.filter(Boolean)
|
|
|
|
.join(' '),
|
|
|
|
style: { ...(prevStyling.style || {}), ...(styling.style || {}) },
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const mergeStyling = (
|
|
|
|
customStyling: StylingValue,
|
|
|
|
defaultStyling: StylingValue
|
|
|
|
): StylingValue | undefined => {
|
|
|
|
if (customStyling === undefined) {
|
|
|
|
return defaultStyling;
|
|
|
|
}
|
|
|
|
if (defaultStyling === undefined) {
|
|
|
|
return customStyling;
|
|
|
|
}
|
|
|
|
|
|
|
|
const customType = typeof customStyling;
|
|
|
|
const defaultType = typeof defaultStyling;
|
|
|
|
|
|
|
|
switch (customType) {
|
|
|
|
case 'string':
|
|
|
|
switch (defaultType) {
|
|
|
|
case 'string':
|
|
|
|
return [defaultStyling, customStyling].filter(Boolean).join(' ');
|
|
|
|
case 'object':
|
|
|
|
return merger({
|
|
|
|
className: customStyling as string,
|
|
|
|
style: defaultStyling as CSS.Properties<string | number>,
|
|
|
|
});
|
|
|
|
case 'function':
|
|
|
|
return (styling: Styling, ...args: any[]) =>
|
|
|
|
merger({
|
|
|
|
className: customStyling as string,
|
|
|
|
})((defaultStyling as StylingValueFunction)(styling, ...args));
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'object':
|
|
|
|
switch (defaultType) {
|
|
|
|
case 'string':
|
|
|
|
return merger({
|
|
|
|
className: defaultStyling as string,
|
|
|
|
style: customStyling as CSS.Properties<string | number>,
|
|
|
|
});
|
|
|
|
case 'object':
|
|
|
|
return {
|
|
|
|
...(defaultStyling as CSS.Properties<string | number>),
|
|
|
|
...(customStyling as CSS.Properties<string | number>),
|
|
|
|
};
|
|
|
|
case 'function':
|
|
|
|
return (styling: Styling, ...args: any[]) =>
|
|
|
|
merger({
|
|
|
|
style: customStyling as CSS.Properties<string | number>,
|
|
|
|
})((defaultStyling as StylingValueFunction)(styling, ...args));
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'function':
|
|
|
|
switch (defaultType) {
|
|
|
|
case 'string':
|
|
|
|
return (styling, ...args) =>
|
|
|
|
(customStyling as StylingValueFunction)(
|
|
|
|
merger(styling)({
|
|
|
|
className: defaultStyling as string,
|
|
|
|
}),
|
|
|
|
...args
|
|
|
|
);
|
|
|
|
case 'object':
|
|
|
|
return (styling, ...args) =>
|
|
|
|
(customStyling as StylingValueFunction)(
|
|
|
|
merger(styling)({
|
|
|
|
style: defaultStyling as CSS.Properties<string | number>,
|
|
|
|
}),
|
|
|
|
...args
|
|
|
|
);
|
|
|
|
case 'function':
|
|
|
|
return (styling, ...args) =>
|
|
|
|
(customStyling as StylingValueFunction)(
|
2020-08-22 04:51:57 +03:00
|
|
|
(defaultStyling as StylingValueFunction)(
|
|
|
|
styling,
|
|
|
|
...args
|
|
|
|
) as Styling,
|
2020-08-16 16:00:54 +03:00
|
|
|
...args
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const mergeStylings = (
|
|
|
|
customStylings: StylingConfig,
|
|
|
|
defaultStylings: StylingConfig
|
|
|
|
): StylingConfig => {
|
|
|
|
const keys = Object.keys(defaultStylings);
|
|
|
|
for (const key in customStylings) {
|
|
|
|
if (keys.indexOf(key) === -1) keys.push(key);
|
|
|
|
}
|
|
|
|
|
|
|
|
return keys.reduce(
|
|
|
|
(mergedStyling, key) => (
|
|
|
|
(mergedStyling[key as keyof StylingConfig] = mergeStyling(
|
|
|
|
customStylings[key] as StylingValue,
|
|
|
|
defaultStylings[key] as StylingValue
|
|
|
|
) as StylingValue),
|
|
|
|
mergedStyling
|
|
|
|
),
|
|
|
|
{} as StylingConfig
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const getStylingByKeys = (
|
|
|
|
mergedStyling: StylingConfig,
|
2020-08-31 00:49:06 +03:00
|
|
|
keys: (string | false | undefined) | (string | false | undefined)[],
|
2020-08-16 16:00:54 +03:00
|
|
|
...args: any[]
|
|
|
|
): Styling => {
|
|
|
|
if (keys === null) {
|
2020-08-22 04:51:57 +03:00
|
|
|
return (mergedStyling as unknown) as Styling;
|
2020-08-16 16:00:54 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!Array.isArray(keys)) {
|
|
|
|
keys = [keys];
|
|
|
|
}
|
|
|
|
|
2020-08-31 00:49:06 +03:00
|
|
|
const styles = keys
|
|
|
|
.map((key) => mergedStyling[key as string])
|
|
|
|
.filter(Boolean);
|
2020-08-16 16:00:54 +03:00
|
|
|
|
2021-06-16 19:20:20 +03:00
|
|
|
const props = styles.reduce<Styling>(
|
2020-08-16 16:00:54 +03:00
|
|
|
(obj, s) => {
|
|
|
|
if (typeof s === 'string') {
|
|
|
|
obj.className = [obj.className, s].filter(Boolean).join(' ');
|
|
|
|
} else if (typeof s === 'object') {
|
|
|
|
obj.style = { ...obj.style, ...s };
|
|
|
|
} else if (typeof s === 'function') {
|
|
|
|
obj = { ...obj, ...s(obj, ...args) };
|
|
|
|
}
|
|
|
|
|
|
|
|
return obj;
|
|
|
|
},
|
|
|
|
{ className: '', style: {} }
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!props.className) {
|
|
|
|
delete props.className;
|
|
|
|
}
|
|
|
|
|
2021-06-16 19:20:20 +03:00
|
|
|
if (Object.keys(props.style!).length === 0) {
|
2020-08-16 16:00:54 +03:00
|
|
|
delete props.style;
|
|
|
|
}
|
|
|
|
|
|
|
|
return props;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const invertBase16Theme = (base16Theme: Base16Theme): Base16Theme =>
|
|
|
|
Object.keys(base16Theme).reduce(
|
|
|
|
(t, key) => (
|
|
|
|
(t[key as keyof Base16Theme] = /^base/.test(key)
|
|
|
|
? invertColor(base16Theme[key as keyof Base16Theme])
|
|
|
|
: key === 'scheme'
|
|
|
|
? base16Theme[key] + ':inverted'
|
|
|
|
: base16Theme[key as keyof Base16Theme]),
|
|
|
|
t
|
|
|
|
),
|
|
|
|
{} as Base16Theme
|
|
|
|
);
|
|
|
|
|
2021-06-18 04:41:41 +03:00
|
|
|
interface Options {
|
|
|
|
defaultBase16?: Base16Theme;
|
|
|
|
base16Themes?: { [themeName: string]: Base16Theme };
|
|
|
|
}
|
|
|
|
|
|
|
|
export const createStyling = curry<
|
|
|
|
(base16Theme: Base16Theme) => StylingConfig,
|
|
|
|
Options | undefined,
|
|
|
|
Theme | undefined,
|
|
|
|
StylingFunction
|
|
|
|
>(
|
2020-08-16 16:00:54 +03:00
|
|
|
(
|
|
|
|
getStylingFromBase16: (base16Theme: Base16Theme) => StylingConfig,
|
2021-06-18 04:41:41 +03:00
|
|
|
options: Options = {},
|
2020-08-16 16:00:54 +03:00
|
|
|
themeOrStyling: Theme = {},
|
|
|
|
...args
|
|
|
|
): StylingFunction => {
|
|
|
|
const { defaultBase16 = DEFAULT_BASE16, base16Themes = null } = options;
|
|
|
|
|
|
|
|
const base16Theme = getBase16Theme(themeOrStyling, base16Themes);
|
|
|
|
if (base16Theme) {
|
|
|
|
themeOrStyling = {
|
|
|
|
...base16Theme,
|
|
|
|
...(themeOrStyling as Base16Theme | StylingConfig),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const theme = BASE16_KEYS.reduce(
|
|
|
|
(t, key) => (
|
|
|
|
(t[key as keyof Base16Theme] =
|
|
|
|
(themeOrStyling as Base16Theme)[key as keyof Base16Theme] ||
|
|
|
|
defaultBase16[key as keyof Base16Theme]),
|
|
|
|
t
|
|
|
|
),
|
|
|
|
{} as Base16Theme
|
|
|
|
);
|
|
|
|
|
|
|
|
const customStyling = Object.keys(themeOrStyling).reduce(
|
|
|
|
(s, key) =>
|
|
|
|
BASE16_KEYS.indexOf(key) === -1
|
|
|
|
? ((s[key] = (themeOrStyling as StylingConfig)[key]), s)
|
|
|
|
: s,
|
|
|
|
{} as StylingConfig
|
|
|
|
);
|
|
|
|
|
|
|
|
const defaultStyling = getStylingFromBase16(theme);
|
|
|
|
|
|
|
|
const mergedStyling = mergeStylings(customStyling, defaultStyling);
|
|
|
|
|
|
|
|
return curry(getStylingByKeys, 2)(mergedStyling, ...args);
|
|
|
|
},
|
|
|
|
3
|
|
|
|
);
|
|
|
|
|
|
|
|
const isStylingConfig = (theme: Theme): theme is StylingConfig =>
|
|
|
|
!!(theme as StylingConfig).extend;
|
|
|
|
|
|
|
|
export const getBase16Theme = (
|
|
|
|
theme: Theme,
|
|
|
|
base16Themes?: { [themeName: string]: Base16Theme } | null
|
|
|
|
): Base16Theme | undefined => {
|
|
|
|
if (theme && isStylingConfig(theme) && theme.extend) {
|
|
|
|
theme = theme.extend as string | Base16Theme;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof theme === 'string') {
|
|
|
|
const [themeName, modifier] = theme.split(':');
|
|
|
|
if (base16Themes) {
|
|
|
|
theme = base16Themes[themeName];
|
|
|
|
} else {
|
|
|
|
theme = base16[themeName as keyof typeof base16];
|
|
|
|
}
|
|
|
|
if (modifier === 'inverted') {
|
|
|
|
theme = invertBase16Theme(theme);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return theme && Object.prototype.hasOwnProperty.call(theme, 'base00')
|
|
|
|
? (theme as Base16Theme)
|
|
|
|
: undefined;
|
|
|
|
};
|
|
|
|
|
2020-08-22 02:56:29 +03:00
|
|
|
export const invertTheme = (theme: Theme | undefined): Theme | undefined => {
|
2020-08-16 16:00:54 +03:00
|
|
|
if (typeof theme === 'string') {
|
|
|
|
return `${theme}:inverted`;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (theme && isStylingConfig(theme) && theme.extend) {
|
|
|
|
if (typeof theme.extend === 'string') {
|
|
|
|
return { ...theme, extend: `${theme.extend}:inverted` };
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...theme,
|
|
|
|
extend: invertBase16Theme(theme.extend as Base16Theme),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (theme) {
|
|
|
|
return invertBase16Theme(theme as Base16Theme);
|
|
|
|
}
|
|
|
|
|
|
|
|
return theme;
|
|
|
|
};
|
2020-08-22 03:13:24 +03:00
|
|
|
|
|
|
|
export { Base16Theme };
|
|
|
|
export * from './types';
|