UNPKG

8.41 kBPlain TextView Raw
1import * as base16 from 'base16';
2import { Base16Theme } from 'base16';
3import Color from 'color';
4import * as CSS from 'csstype';
5import curry from 'lodash.curry';
6import { Color as ColorTuple, yuv2rgb, rgb2yuv } from './colorConverters';
7import {
8 Styling,
9 StylingConfig,
10 StylingFunction,
11 StylingValue,
12 StylingValueFunction,
13 Theme,
14} from './types';
15
16const DEFAULT_BASE16 = base16.default;
17
18const BASE16_KEYS = Object.keys(DEFAULT_BASE16);
19
20// we need a correcting factor, so that a dark, but not black background color
21// converts to bright enough inversed color
22const flip = (x: number) => (x < 0.25 ? 1 : x < 0.5 ? 0.9 - x : 1.1 - x);
23
24const 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
32const 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
41const 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
122const 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
143const 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
186export 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
199interface Options {
200 defaultBase16?: Base16Theme;
201 base16Themes?: { [themeName: string]: Base16Theme };
202}
203
204export 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
253const isStylingConfig = (theme: Theme): theme is StylingConfig =>
254 !!(theme as StylingConfig).extend;
255
256export 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
281export 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
304export type { Base16Theme };
305export * from './types';