1 | import * as base16 from 'base16';
|
2 | import { Base16Theme } from 'base16';
|
3 | import Color from 'color';
|
4 | import * as CSS from 'csstype';
|
5 | import curry from 'lodash.curry';
|
6 | import { Color as ColorTuple, yuv2rgb, rgb2yuv } from './colorConverters';
|
7 | import {
|
8 | Styling,
|
9 | StylingConfig,
|
10 | StylingFunction,
|
11 | StylingValue,
|
12 | StylingValueFunction,
|
13 | Theme,
|
14 | } from './types';
|
15 |
|
16 | const DEFAULT_BASE16 = base16.default;
|
17 |
|
18 | const BASE16_KEYS = Object.keys(DEFAULT_BASE16);
|
19 |
|
20 |
|
21 |
|
22 | const flip = (x: number) => (x < 0.25 ? 1 : x < 0.5 ? 0.9 - x : 1.1 - x);
|
23 |
|
24 | const invertColor = (hexString: string) => {
|
25 | const color = Color(hexString);
|
26 | const [y, u, v] = rgb2yuv(color.array() as ColorTuple);
|
27 | const flippedYuv: ColorTuple = [flip(y), u, v];
|
28 | const rgb = yuv2rgb(flippedYuv);
|
29 | return Color.rgb(rgb).hex();
|
30 | };
|
31 |
|
32 | const merger = (styling: Partial<Styling>) => {
|
33 | return (prevStyling: Partial<Styling>) => ({
|
34 | className: [prevStyling.className, styling.className]
|
35 | .filter(Boolean)
|
36 | .join(' '),
|
37 | style: { ...(prevStyling.style || {}), ...(styling.style || {}) },
|
38 | });
|
39 | };
|
40 |
|
41 | const mergeStyling = (
|
42 | customStyling: StylingValue,
|
43 | defaultStyling: StylingValue
|
44 | ): StylingValue | undefined => {
|
45 | if (customStyling === undefined) {
|
46 | return defaultStyling;
|
47 | }
|
48 | if (defaultStyling === undefined) {
|
49 | return customStyling;
|
50 | }
|
51 |
|
52 | const customType = typeof customStyling;
|
53 | const defaultType = typeof defaultStyling;
|
54 |
|
55 | switch (customType) {
|
56 | case 'string':
|
57 | switch (defaultType) {
|
58 | case 'string':
|
59 | return [defaultStyling, customStyling].filter(Boolean).join(' ');
|
60 | case 'object':
|
61 | return merger({
|
62 | className: customStyling as string,
|
63 | style: defaultStyling as CSS.Properties<string | number>,
|
64 | });
|
65 | case 'function':
|
66 | return (styling: Styling, ...args: unknown[]) =>
|
67 | merger({
|
68 | className: customStyling as string,
|
69 | })((defaultStyling as StylingValueFunction)(styling, ...args));
|
70 | }
|
71 | break;
|
72 | case 'object':
|
73 | switch (defaultType) {
|
74 | case 'string':
|
75 | return merger({
|
76 | className: defaultStyling as string,
|
77 | style: customStyling as CSS.Properties<string | number>,
|
78 | });
|
79 | case 'object':
|
80 | return {
|
81 | ...(defaultStyling as CSS.Properties<string | number>),
|
82 | ...(customStyling as CSS.Properties<string | number>),
|
83 | };
|
84 | case 'function':
|
85 | return (styling: Styling, ...args: unknown[]) =>
|
86 | merger({
|
87 | style: customStyling as CSS.Properties<string | number>,
|
88 | })((defaultStyling as StylingValueFunction)(styling, ...args));
|
89 | }
|
90 | break;
|
91 | case 'function':
|
92 | switch (defaultType) {
|
93 | case 'string':
|
94 | return (styling, ...args) =>
|
95 | (customStyling as StylingValueFunction)(
|
96 | merger(styling)({
|
97 | className: defaultStyling as string,
|
98 | }),
|
99 | ...args
|
100 | );
|
101 | case 'object':
|
102 | return (styling, ...args) =>
|
103 | (customStyling as StylingValueFunction)(
|
104 | merger(styling)({
|
105 | style: defaultStyling as CSS.Properties<string | number>,
|
106 | }),
|
107 | ...args
|
108 | );
|
109 | case 'function':
|
110 | return (styling, ...args) =>
|
111 | (customStyling as StylingValueFunction)(
|
112 | (defaultStyling as StylingValueFunction)(
|
113 | styling,
|
114 | ...args
|
115 | ) as Styling,
|
116 | ...args
|
117 | );
|
118 | }
|
119 | }
|
120 | };
|
121 |
|
122 | const mergeStylings = (
|
123 | customStylings: StylingConfig,
|
124 | defaultStylings: StylingConfig
|
125 | ): StylingConfig => {
|
126 | const keys = Object.keys(defaultStylings);
|
127 | for (const key in customStylings) {
|
128 | if (keys.indexOf(key) === -1) keys.push(key);
|
129 | }
|
130 |
|
131 | return keys.reduce(
|
132 | (mergedStyling, key) => (
|
133 | (mergedStyling[key as keyof StylingConfig] = mergeStyling(
|
134 | customStylings[key] as StylingValue,
|
135 | defaultStylings[key] as StylingValue
|
136 | ) as StylingValue),
|
137 | mergedStyling
|
138 | ),
|
139 | {} as StylingConfig
|
140 | );
|
141 | };
|
142 |
|
143 | const getStylingByKeys = (
|
144 | mergedStyling: StylingConfig,
|
145 | keys: (string | false | undefined) | (string | false | undefined)[],
|
146 | ...args: unknown[]
|
147 | ): Styling => {
|
148 | if (keys === null) {
|
149 | return mergedStyling as unknown as Styling;
|
150 | }
|
151 |
|
152 | if (!Array.isArray(keys)) {
|
153 | keys = [keys];
|
154 | }
|
155 |
|
156 | const styles = keys
|
157 | .map((key) => mergedStyling[key as string])
|
158 | .filter(Boolean);
|
159 |
|
160 | const props = styles.reduce<Styling>(
|
161 | (obj, s) => {
|
162 | if (typeof s === 'string') {
|
163 | obj.className = [obj.className, s].filter(Boolean).join(' ');
|
164 | } else if (typeof s === 'object') {
|
165 | obj.style = { ...obj.style, ...s };
|
166 | } else if (typeof s === 'function') {
|
167 | obj = { ...obj, ...s(obj, ...args) };
|
168 | }
|
169 |
|
170 | return obj;
|
171 | },
|
172 | { className: '', style: {} }
|
173 | );
|
174 |
|
175 | if (!props.className) {
|
176 | delete props.className;
|
177 | }
|
178 |
|
179 | if (Object.keys(props.style!).length === 0) {
|
180 | delete props.style;
|
181 | }
|
182 |
|
183 | return props;
|
184 | };
|
185 |
|
186 | export const invertBase16Theme = (base16Theme: Base16Theme): Base16Theme =>
|
187 | Object.keys(base16Theme).reduce(
|
188 | (t, key) => (
|
189 | (t[key as keyof Base16Theme] = /^base/.test(key)
|
190 | ? invertColor(base16Theme[key as keyof Base16Theme])
|
191 | : key === 'scheme'
|
192 | ? base16Theme[key] + ':inverted'
|
193 | : base16Theme[key as keyof Base16Theme]),
|
194 | t
|
195 | ),
|
196 | {} as Base16Theme
|
197 | );
|
198 |
|
199 | interface Options {
|
200 | defaultBase16?: Base16Theme;
|
201 | base16Themes?: { [themeName: string]: Base16Theme };
|
202 | }
|
203 |
|
204 | export const createStyling = curry<
|
205 | (base16Theme: Base16Theme) => StylingConfig,
|
206 | Options | undefined,
|
207 | Theme | undefined,
|
208 | StylingFunction
|
209 | >(
|
210 | (
|
211 | getStylingFromBase16: (base16Theme: Base16Theme) => StylingConfig,
|
212 | options: Options = {},
|
213 | themeOrStyling: Theme = {},
|
214 | ...args
|
215 | ): StylingFunction => {
|
216 | const { defaultBase16 = DEFAULT_BASE16, base16Themes = null } = options;
|
217 |
|
218 | const base16Theme = getBase16Theme(themeOrStyling, base16Themes);
|
219 | if (base16Theme) {
|
220 | themeOrStyling = {
|
221 | ...base16Theme,
|
222 | ...(themeOrStyling as Base16Theme | StylingConfig),
|
223 | };
|
224 | }
|
225 |
|
226 | const theme = BASE16_KEYS.reduce(
|
227 | (t, key) => (
|
228 | (t[key as keyof Base16Theme] =
|
229 | (themeOrStyling as Base16Theme)[key as keyof Base16Theme] ||
|
230 | defaultBase16[key as keyof Base16Theme]),
|
231 | t
|
232 | ),
|
233 | {} as Base16Theme
|
234 | );
|
235 |
|
236 | const customStyling = Object.keys(themeOrStyling).reduce(
|
237 | (s, key) =>
|
238 | BASE16_KEYS.indexOf(key) === -1
|
239 | ? ((s[key] = (themeOrStyling as StylingConfig)[key]), s)
|
240 | : s,
|
241 | {} as StylingConfig
|
242 | );
|
243 |
|
244 | const defaultStyling = getStylingFromBase16(theme);
|
245 |
|
246 | const mergedStyling = mergeStylings(customStyling, defaultStyling);
|
247 |
|
248 | return curry(getStylingByKeys, 2)(mergedStyling, ...args);
|
249 | },
|
250 | 3
|
251 | );
|
252 |
|
253 | const isStylingConfig = (theme: Theme): theme is StylingConfig =>
|
254 | !!(theme as StylingConfig).extend;
|
255 |
|
256 | export const getBase16Theme = (
|
257 | theme: Theme,
|
258 | base16Themes?: { [themeName: string]: Base16Theme } | null
|
259 | ): Base16Theme | undefined => {
|
260 | if (theme && isStylingConfig(theme) && theme.extend) {
|
261 | theme = theme.extend as string | Base16Theme;
|
262 | }
|
263 |
|
264 | if (typeof theme === 'string') {
|
265 | const [themeName, modifier] = theme.split(':');
|
266 | if (base16Themes) {
|
267 | theme = base16Themes[themeName];
|
268 | } else {
|
269 | theme = base16[themeName as keyof typeof base16];
|
270 | }
|
271 | if (modifier === 'inverted') {
|
272 | theme = invertBase16Theme(theme);
|
273 | }
|
274 | }
|
275 |
|
276 | return theme && Object.prototype.hasOwnProperty.call(theme, 'base00')
|
277 | ? (theme as Base16Theme)
|
278 | : undefined;
|
279 | };
|
280 |
|
281 | export const invertTheme = (theme: Theme | undefined): Theme | undefined => {
|
282 | if (typeof theme === 'string') {
|
283 | return `${theme}:inverted`;
|
284 | }
|
285 |
|
286 | if (theme && isStylingConfig(theme) && theme.extend) {
|
287 | if (typeof theme.extend === 'string') {
|
288 | return { ...theme, extend: `${theme.extend}:inverted` };
|
289 | }
|
290 |
|
291 | return {
|
292 | ...theme,
|
293 | extend: invertBase16Theme(theme.extend as Base16Theme),
|
294 | };
|
295 | }
|
296 |
|
297 | if (theme) {
|
298 | return invertBase16Theme(theme as Base16Theme);
|
299 | }
|
300 |
|
301 | return theme;
|
302 | };
|
303 |
|
304 | export type { Base16Theme };
|
305 | export * from './types';
|