Skip to content

Commit 0b539e1

Browse files
authored
feat: support majority of filter style properties (#379)
* feat: support majority of `filter` style properties * use extracted helper in few more places * fix formatting
1 parent a164648 commit 0b539e1

10 files changed

Lines changed: 295 additions & 14 deletions

File tree

src/UtilityParser.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ import pointerEvents from './resolve/pointer-events';
2828
import userSelect from './resolve/user-select';
2929
import textDecorationStyle from './resolve/text-decoration-style';
3030
import { outlineOffset, outlineStyle, outlineWidth } from './resolve/outline';
31+
import {
32+
filterBrightness,
33+
filterContrast,
34+
filterGrayscale,
35+
filterHueRotate,
36+
filterInvert,
37+
filterSaturate,
38+
filterSepia,
39+
} from './resolve/filter';
3140

3241
export default class UtilityParser {
3342
private position = 0;
@@ -376,6 +385,41 @@ export default class UtilityParser {
376385
if (style) return style;
377386
}
378387

388+
if (this.consumePeeked(`brightness-`)) {
389+
style = filterBrightness(this.rest, this.context, theme?.brightness);
390+
if (style) return style;
391+
}
392+
393+
if (this.consumePeeked(`contrast-`)) {
394+
style = filterContrast(this.rest, this.context, theme?.contrast);
395+
if (style) return style;
396+
}
397+
398+
if (this.consumePeeked(`saturate-`)) {
399+
style = filterSaturate(this.rest, this.context, theme?.saturate);
400+
if (style) return style;
401+
}
402+
403+
if (this.consumePeeked(`hue-rotate-`)) {
404+
style = filterHueRotate(this.rest, this.context, theme?.hueRotate);
405+
if (style) return style;
406+
}
407+
408+
if (this.consumePeeked(`grayscale`)) {
409+
style = filterGrayscale(this.rest, this.context, theme?.grayscale);
410+
if (style) return style;
411+
}
412+
413+
if (this.consumePeeked(`invert`)) {
414+
style = filterInvert(this.rest, this.context, theme?.invert);
415+
if (style) return style;
416+
}
417+
418+
if (this.consumePeeked(`sepia`)) {
419+
style = filterSepia(this.rest, this.context, theme?.sepia);
420+
if (style) return style;
421+
}
422+
379423
h.warn(`\`${this.isNegative ? `-` : ``}${this.rest}\` unknown or invalid utility`);
380424
return null;
381425
}

src/__tests__/filter.spec.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, test, expect } from '@jest/globals';
2+
import { create } from '../';
3+
4+
describe(`filter utilities`, () => {
5+
let tw = create();
6+
beforeEach(() => (tw = create()));
7+
8+
const cases: Array<[string, Record<string, Record<string, string | number>[]>]> = [
9+
// grayscale
10+
[`grayscale`, { filter: [{ grayscale: 1 }] }],
11+
[`grayscale-0`, { filter: [{ grayscale: 0 }] }],
12+
[`grayscale-[50%]`, { filter: [{ grayscale: 0.5 }] }],
13+
// invert
14+
[`invert`, { filter: [{ invert: 1 }] }],
15+
[`invert-0`, { filter: [{ invert: 0 }] }],
16+
[`invert-[25%]`, { filter: [{ invert: 0.25 }] }],
17+
// sepia
18+
[`sepia`, { filter: [{ sepia: 1 }] }],
19+
[`sepia-0`, { filter: [{ sepia: 0 }] }],
20+
[`sepia-[0.75]`, { filter: [{ sepia: 0.75 }] }],
21+
// contrast
22+
[`contrast-125`, { filter: [{ contrast: 1.25 }] }],
23+
[`contrast-[2.5]`, { filter: [{ contrast: 2.5 }] }],
24+
// brightness
25+
[`brightness-75`, { filter: [{ brightness: 0.75 }] }],
26+
[`brightness-110`, { filter: [{ brightness: 1.1 }] }],
27+
[`brightness-[1.75]`, { filter: [{ brightness: 1.75 }] }],
28+
// saturate
29+
[`saturate-0`, { filter: [{ saturate: 0 }] }],
30+
[`saturate-[.75]`, { filter: [{ saturate: 0.75 }] }],
31+
// hue rotate
32+
[`hue-rotate-90`, { filter: [{ hueRotate: `90deg` }] }],
33+
[`hue-rotate-[27deg]`, { filter: [{ hueRotate: `27deg` }] }],
34+
[`hue-rotate-[3rad]`, { filter: [{ hueRotate: `3rad` }] }],
35+
[`hue-rotate-[-270deg]`, { filter: [{ hueRotate: `-270deg` }] }],
36+
// all values mix
37+
[
38+
`grayscale contrast-25 brightness-25 invert sepia-25 saturate-75 hue-rotate-90`,
39+
{
40+
filter: [
41+
{ grayscale: 1 },
42+
{ contrast: 0.25 },
43+
{ brightness: 0.25 },
44+
{ invert: 1 },
45+
{ sepia: 0.25 },
46+
{ saturate: 0.75 },
47+
{ hueRotate: `90deg` },
48+
],
49+
},
50+
],
51+
];
52+
53+
test.each(cases)(`tw\`%s\` -> %s`, (utility, expected) => {
54+
expect(tw.style(utility)).toEqual(expected);
55+
});
56+
});

src/helpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,10 @@ function unconfiggedStyleVal(
226226
return toStyleVal(number, unit, context);
227227
}
228228

229+
export function isArbitraryValue(value: string): boolean {
230+
return value.startsWith(`[`) && value.endsWith(`]`);
231+
}
232+
229233
function consoleWarn(...args: any[]): void {
230234
console.warn(...args); // eslint-disable-line no-console
231235
}

src/resolve/color.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ColorStyleType, Style, StyleIR } from '../types';
22
import type { TwColors } from '../tw-config';
33
import { isObject, isString } from '../types';
4-
import { warn } from '../helpers';
4+
import { isArbitraryValue, warn } from '../helpers';
55

66
export function color(
77
type: ColorStyleType,
@@ -23,7 +23,7 @@ export function color(
2323
if (value.startsWith(`[#`) || value.startsWith(`[rgb`) || value.startsWith(`[hsl`)) {
2424
color = value.slice(1, -1);
2525
// arbitrary named colors: `bg-[lemonchiffon]`
26-
} else if (value.startsWith(`[`) && value.slice(1, -1).match(/^[a-z]{3,}$/)) {
26+
} else if (isArbitraryValue(value) && value.slice(1, -1).match(/^[a-z]{3,}$/)) {
2727
color = value.slice(1, -1);
2828
} else {
2929
color = configColor(value, config) ?? ``;

src/resolve/filter.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import type { ParseContext, Style, StyleIR } from '../types';
2+
import type { TwTheme } from '../tw-config';
3+
import { isArbitraryValue, parseNumericValue, parseStyleVal } from '../helpers';
4+
5+
export function filterBrightness(
6+
value: string,
7+
context: ParseContext = {},
8+
config?: TwTheme['brightness'],
9+
): StyleIR | null {
10+
const styleVal = getBaseFilterStyleValue(value, context, config?.[value]);
11+
12+
return createStyle(`brightness`, styleVal);
13+
}
14+
15+
export function filterContrast(
16+
value: string,
17+
context: ParseContext = {},
18+
config?: TwTheme['contrast'],
19+
): StyleIR | null {
20+
const styleVal = getBaseFilterStyleValue(value, context, config?.[value]);
21+
22+
return createStyle(`contrast`, styleVal);
23+
}
24+
25+
export function filterSaturate(
26+
value: string,
27+
context: ParseContext = {},
28+
config?: TwTheme['saturate'],
29+
): StyleIR | null {
30+
const styleVal = getBaseFilterStyleValue(value, context, config?.[value]);
31+
32+
return createStyle(`saturate`, styleVal);
33+
}
34+
35+
export function filterGrayscale(
36+
value: string,
37+
context: ParseContext = {},
38+
config?: TwTheme['grayscale'],
39+
): StyleIR | null {
40+
const parsed = value.startsWith(`-`) ? value.slice(1) : `100`;
41+
const styleVal = getPercentageFilterStyleValue(parsed, context, config?.[value]);
42+
43+
return createStyle(`grayscale`, styleVal);
44+
}
45+
46+
export function filterInvert(
47+
value: string,
48+
context: ParseContext = {},
49+
config?: TwTheme['invert'],
50+
): StyleIR | null {
51+
const parsed = value.startsWith(`-`) ? value.slice(1) : `100`;
52+
const styleVal = getPercentageFilterStyleValue(parsed, context, config?.[value]);
53+
54+
return createStyle(`invert`, styleVal);
55+
}
56+
57+
export function filterSepia(
58+
value: string,
59+
context: ParseContext = {},
60+
config?: TwTheme['sepia'],
61+
): StyleIR | null {
62+
const parsed = value.startsWith(`-`) ? value.slice(1) : `100`;
63+
const styleVal = getPercentageFilterStyleValue(parsed, context, config?.[value]);
64+
65+
return createStyle(`sepia`, styleVal);
66+
}
67+
68+
export function filterHueRotate(
69+
value: string,
70+
context: ParseContext = {},
71+
config?: TwTheme['hueRotate'],
72+
): StyleIR | null {
73+
const configValue = config?.[value];
74+
let styleVal: string | number | null;
75+
if (configValue) {
76+
styleVal = parseStyleVal(configValue, context);
77+
} else if (isArbitraryValue(value)) {
78+
const parsed = parseNumericValue(value.slice(1, -1));
79+
styleVal = parsed ? `${parsed[0]}${parsed[1]}` : null;
80+
} else {
81+
const parsed = parseNumericValue(value);
82+
styleVal = parsed ? `${parsed[0]}${parsed[1]}` : null;
83+
}
84+
85+
return createStyle(`hueRotate`, styleVal);
86+
}
87+
88+
function getBaseFilterStyleValue(
89+
value: string,
90+
context: ParseContext = {},
91+
configValue?: string,
92+
): string | number | null {
93+
if (configValue) {
94+
return parseStyleVal(configValue, context);
95+
} else if (isArbitraryValue(value)) {
96+
const parsed = parseNumericValue(value.slice(1, -1));
97+
return parsed ? parsed[0] : null;
98+
} else {
99+
const parsed = parseNumericValue(value);
100+
return parsed ? parsed[0] / 100 : null;
101+
}
102+
}
103+
104+
function getPercentageFilterStyleValue(
105+
value: string,
106+
context: ParseContext = {},
107+
configValue?: string,
108+
): string | number | null {
109+
if (configValue) {
110+
const parsed = parseStyleVal(configValue, context)?.toString().slice(0, -1);
111+
return parsed ? parseInt(parsed) / 100 : null;
112+
} else if (isArbitraryValue(value)) {
113+
const parsed = parseNumericValue(value.slice(1, -1));
114+
if (parsed === null) {
115+
return null;
116+
}
117+
if (Number.isInteger(parsed[0])) {
118+
return parsed[0] / 100;
119+
}
120+
return parsed[0];
121+
} else {
122+
const parsed = parseNumericValue(value);
123+
if (parsed === null) {
124+
return null;
125+
}
126+
if (Number.isInteger(parsed[0])) {
127+
return parsed[0] / 100;
128+
}
129+
return parsed[0];
130+
}
131+
}
132+
133+
function createStyle(
134+
filterType: string,
135+
styleVal: string | number | null,
136+
): StyleIR | null {
137+
return {
138+
kind: `dependent`,
139+
complete(style) {
140+
updateFilterStyle(style, filterType, styleVal);
141+
},
142+
};
143+
}
144+
145+
function updateFilterStyle(
146+
style: Style,
147+
key: string,
148+
styleVal: string | number | null,
149+
): void {
150+
if (styleVal === null) {
151+
return;
152+
}
153+
154+
const existingFilter = (style.filter || []) as Style[];
155+
if (Array.isArray(existingFilter) && existingFilter) {
156+
style.filter = [...existingFilter, { [key]: styleVal }];
157+
} else {
158+
style.filter = existingFilter;
159+
}
160+
}

src/resolve/flex.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import type { TwTheme } from '../tw-config';
22
import type { ParseContext, StyleIR } from '../types';
3-
import { getCompleteStyle, complete, parseStyleVal, unconfiggedStyle } from '../helpers';
3+
import {
4+
getCompleteStyle,
5+
complete,
6+
parseStyleVal,
7+
unconfiggedStyle,
8+
isArbitraryValue,
9+
} from '../helpers';
410

511
export function flexGrowShrink(
612
type: 'Grow' | 'Shrink',
713
value: string,
814
config?: TwTheme['flexGrow'] | TwTheme['flexShrink'],
915
): StyleIR | null {
1016
value = value.replace(/^-/, ``);
11-
if (value[0] === `[` && value.endsWith(`]`)) {
17+
if (isArbitraryValue(value)) {
1218
value = value.slice(1, -1);
1319
}
1420
const configKey = value === `` ? `DEFAULT` : value;

src/resolve/line-height.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type { TwTheme } from '../tw-config';
22
import type { StyleIR } from '../types';
33
import { Unit } from '../types';
4-
import { parseNumericValue, complete, toStyleVal } from '../helpers';
4+
import { parseNumericValue, complete, toStyleVal, isArbitraryValue } from '../helpers';
55

66
export default function lineHeight(
77
value: string,
88
config?: TwTheme['lineHeight'],
99
): StyleIR | null {
1010
const parseValue =
11-
config?.[value] ?? (value.startsWith(`[`) ? value.slice(1, -1) : value);
11+
config?.[value] ?? (isArbitraryValue(value) ? value.slice(1, -1) : value);
1212

1313
const parsed = parseNumericValue(parseValue);
1414
if (!parsed) {

src/resolve/spacing.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import type { TwTheme } from '../tw-config';
22
import type { Direction, ParseContext, StyleIR } from '../types';
33
import { Unit } from '../types';
4-
import { parseNumericValue, parseUnconfigged, toStyleVal } from '../helpers';
4+
import {
5+
isArbitraryValue,
6+
parseNumericValue,
7+
parseUnconfigged,
8+
toStyleVal,
9+
} from '../helpers';
510

611
export default function spacing(
712
type: 'margin' | 'padding',
@@ -11,7 +16,7 @@ export default function spacing(
1116
config?: TwTheme['margin'] | TwTheme['padding'],
1217
): StyleIR | null {
1318
let numericValue = ``;
14-
if (value[0] === `[`) {
19+
if (isArbitraryValue(value)) {
1520
numericValue = value.slice(1, -1);
1621
} else {
1722
const configValue = config?.[value];

src/resolve/transform.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { DependentStyle, ParseContext, Style, StyleIR } from '../types';
33
import { isString, Unit } from '../types';
44
import {
55
complete,
6+
isArbitraryValue,
67
parseNumericValue,
78
parseStyleVal,
89
parseUnconfigged,
@@ -246,10 +247,6 @@ function createStyle(
246247
};
247248
}
248249

249-
function isArbitraryValue(value: string): boolean {
250-
return value.startsWith(`[`) && value.endsWith(`]`);
251-
}
252-
253250
function parseOriginValue(
254251
value: string | undefined,
255252
allowedPositions: OriginPosition[],

0 commit comments

Comments
 (0)