Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/UtilityParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ import pointerEvents from './resolve/pointer-events';
import userSelect from './resolve/user-select';
import textDecorationStyle from './resolve/text-decoration-style';
import { outlineOffset, outlineStyle, outlineWidth } from './resolve/outline';
import {
filterBrightness,
filterContrast,
filterGrayscale,
filterHueRotate,
filterInvert,
filterSaturate,
filterSepia,
} from './resolve/filter';

export default class UtilityParser {
private position = 0;
Expand Down Expand Up @@ -376,6 +385,41 @@ export default class UtilityParser {
if (style) return style;
}

if (this.consumePeeked(`brightness-`)) {
style = filterBrightness(this.rest, this.context, theme?.brightness);
if (style) return style;
}

if (this.consumePeeked(`contrast-`)) {
style = filterContrast(this.rest, this.context, theme?.contrast);
if (style) return style;
}

if (this.consumePeeked(`saturate-`)) {
style = filterSaturate(this.rest, this.context, theme?.saturate);
if (style) return style;
}

if (this.consumePeeked(`hue-rotate-`)) {
style = filterHueRotate(this.rest, this.context, theme?.hueRotate);
if (style) return style;
}

if (this.consumePeeked(`grayscale`)) {
style = filterGrayscale(this.rest, this.context, theme?.grayscale);
if (style) return style;
}

if (this.consumePeeked(`invert`)) {
style = filterInvert(this.rest, this.context, theme?.invert);
if (style) return style;
}

if (this.consumePeeked(`sepia`)) {
style = filterSepia(this.rest, this.context, theme?.sepia);
if (style) return style;
}

h.warn(`\`${this.isNegative ? `-` : ``}${this.rest}\` unknown or invalid utility`);
return null;
}
Expand Down
56 changes: 56 additions & 0 deletions src/__tests__/filter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, test, expect } from '@jest/globals';
import { create } from '../';

describe(`filter utilities`, () => {
let tw = create();
beforeEach(() => (tw = create()));

const cases: Array<[string, Record<string, Record<string, string | number>[]>]> = [
// grayscale
[`grayscale`, { filter: [{ grayscale: 1 }] }],
[`grayscale-0`, { filter: [{ grayscale: 0 }] }],
[`grayscale-[50%]`, { filter: [{ grayscale: 0.5 }] }],
// invert
[`invert`, { filter: [{ invert: 1 }] }],
[`invert-0`, { filter: [{ invert: 0 }] }],
[`invert-[25%]`, { filter: [{ invert: 0.25 }] }],
// sepia
[`sepia`, { filter: [{ sepia: 1 }] }],
[`sepia-0`, { filter: [{ sepia: 0 }] }],
[`sepia-[0.75]`, { filter: [{ sepia: 0.75 }] }],
// contrast
[`contrast-125`, { filter: [{ contrast: 1.25 }] }],
[`contrast-[2.5]`, { filter: [{ contrast: 2.5 }] }],
// brightness
[`brightness-75`, { filter: [{ brightness: 0.75 }] }],
[`brightness-110`, { filter: [{ brightness: 1.1 }] }],
[`brightness-[1.75]`, { filter: [{ brightness: 1.75 }] }],
// saturate
[`saturate-0`, { filter: [{ saturate: 0 }] }],
[`saturate-[.75]`, { filter: [{ saturate: 0.75 }] }],
// hue rotate
[`hue-rotate-90`, { filter: [{ hueRotate: `90deg` }] }],
[`hue-rotate-[27deg]`, { filter: [{ hueRotate: `27deg` }] }],
[`hue-rotate-[3rad]`, { filter: [{ hueRotate: `3rad` }] }],
[`hue-rotate-[-270deg]`, { filter: [{ hueRotate: `-270deg` }] }],
// all values mix
[
`grayscale contrast-25 brightness-25 invert sepia-25 saturate-75 hue-rotate-90`,
{
filter: [
{ grayscale: 1 },
{ contrast: 0.25 },
{ brightness: 0.25 },
{ invert: 1 },
{ sepia: 0.25 },
{ saturate: 0.75 },
{ hueRotate: `90deg` },
],
},
],
];

test.each(cases)(`tw\`%s\` -> %s`, (utility, expected) => {
expect(tw.style(utility)).toEqual(expected);
});
});
4 changes: 4 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ function unconfiggedStyleVal(
return toStyleVal(number, unit, context);
}

export function isArbitraryValue(value: string): boolean {
return value.startsWith(`[`) && value.endsWith(`]`);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we not already have a function like this? or are there some obvious other places we should use it instead for clarity?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I have extracted the helper from transforms resolver to avoid duplicating the code. After your comment I have found few more places where it can be used, replaced inline checks and made sure that all tests are still passing. Let me know if this approach make sense!

}

function consoleWarn(...args: any[]): void {
console.warn(...args); // eslint-disable-line no-console
}
Expand Down
4 changes: 2 additions & 2 deletions src/resolve/color.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ColorStyleType, Style, StyleIR } from '../types';
import type { TwColors } from '../tw-config';
import { isObject, isString } from '../types';
import { warn } from '../helpers';
import { isArbitraryValue, warn } from '../helpers';

export function color(
type: ColorStyleType,
Expand All @@ -23,7 +23,7 @@ export function color(
if (value.startsWith(`[#`) || value.startsWith(`[rgb`) || value.startsWith(`[hsl`)) {
color = value.slice(1, -1);
// arbitrary named colors: `bg-[lemonchiffon]`
} else if (value.startsWith(`[`) && value.slice(1, -1).match(/^[a-z]{3,}$/)) {
} else if (isArbitraryValue(value) && value.slice(1, -1).match(/^[a-z]{3,}$/)) {
color = value.slice(1, -1);
} else {
color = configColor(value, config) ?? ``;
Expand Down
160 changes: 160 additions & 0 deletions src/resolve/filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import type { ParseContext, Style, StyleIR } from '../types';
import type { TwTheme } from '../tw-config';
import { isArbitraryValue, parseNumericValue, parseStyleVal } from '../helpers';

export function filterBrightness(
value: string,
context: ParseContext = {},
config?: TwTheme['brightness'],
): StyleIR | null {
const styleVal = getBaseFilterStyleValue(value, context, config?.[value]);

return createStyle(`brightness`, styleVal);
}

export function filterContrast(
value: string,
context: ParseContext = {},
config?: TwTheme['contrast'],
): StyleIR | null {
const styleVal = getBaseFilterStyleValue(value, context, config?.[value]);

return createStyle(`contrast`, styleVal);
}

export function filterSaturate(
value: string,
context: ParseContext = {},
config?: TwTheme['saturate'],
): StyleIR | null {
const styleVal = getBaseFilterStyleValue(value, context, config?.[value]);

return createStyle(`saturate`, styleVal);
}

export function filterGrayscale(
value: string,
context: ParseContext = {},
config?: TwTheme['grayscale'],
): StyleIR | null {
const parsed = value.startsWith(`-`) ? value.slice(1) : `100`;
const styleVal = getPercentageFilterStyleValue(parsed, context, config?.[value]);

return createStyle(`grayscale`, styleVal);
}

export function filterInvert(
value: string,
context: ParseContext = {},
config?: TwTheme['invert'],
): StyleIR | null {
const parsed = value.startsWith(`-`) ? value.slice(1) : `100`;
const styleVal = getPercentageFilterStyleValue(parsed, context, config?.[value]);

return createStyle(`invert`, styleVal);
}

export function filterSepia(
value: string,
context: ParseContext = {},
config?: TwTheme['sepia'],
): StyleIR | null {
const parsed = value.startsWith(`-`) ? value.slice(1) : `100`;
const styleVal = getPercentageFilterStyleValue(parsed, context, config?.[value]);

return createStyle(`sepia`, styleVal);
}

export function filterHueRotate(
value: string,
context: ParseContext = {},
config?: TwTheme['hueRotate'],
): StyleIR | null {
const configValue = config?.[value];
let styleVal: string | number | null;
if (configValue) {
styleVal = parseStyleVal(configValue, context);
} else if (isArbitraryValue(value)) {
const parsed = parseNumericValue(value.slice(1, -1));
styleVal = parsed ? `${parsed[0]}${parsed[1]}` : null;
} else {
const parsed = parseNumericValue(value);
styleVal = parsed ? `${parsed[0]}${parsed[1]}` : null;
}

return createStyle(`hueRotate`, styleVal);
}

function getBaseFilterStyleValue(
value: string,
context: ParseContext = {},
configValue?: string,
): string | number | null {
if (configValue) {
return parseStyleVal(configValue, context);
} else if (isArbitraryValue(value)) {
const parsed = parseNumericValue(value.slice(1, -1));
return parsed ? parsed[0] : null;
} else {
const parsed = parseNumericValue(value);
return parsed ? parsed[0] / 100 : null;
}
}

function getPercentageFilterStyleValue(
value: string,
context: ParseContext = {},
configValue?: string,
): string | number | null {
if (configValue) {
const parsed = parseStyleVal(configValue, context)?.toString().slice(0, -1);
return parsed ? parseInt(parsed) / 100 : null;
} else if (isArbitraryValue(value)) {
const parsed = parseNumericValue(value.slice(1, -1));
if (parsed === null) {
return null;
}
if (Number.isInteger(parsed[0])) {
return parsed[0] / 100;
}
return parsed[0];
} else {
const parsed = parseNumericValue(value);
if (parsed === null) {
return null;
}
if (Number.isInteger(parsed[0])) {
return parsed[0] / 100;
}
return parsed[0];
}
}

function createStyle(
filterType: string,
styleVal: string | number | null,
): StyleIR | null {
return {
kind: `dependent`,
complete(style) {
updateFilterStyle(style, filterType, styleVal);
},
};
}

function updateFilterStyle(
style: Style,
key: string,
styleVal: string | number | null,
): void {
if (styleVal === null) {
return;
}

const existingFilter = (style.filter || []) as Style[];
if (Array.isArray(existingFilter) && existingFilter) {
style.filter = [...existingFilter, { [key]: styleVal }];
} else {
style.filter = existingFilter;
}
}
10 changes: 8 additions & 2 deletions src/resolve/flex.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import type { TwTheme } from '../tw-config';
import type { ParseContext, StyleIR } from '../types';
import { getCompleteStyle, complete, parseStyleVal, unconfiggedStyle } from '../helpers';
import {
getCompleteStyle,
complete,
parseStyleVal,
unconfiggedStyle,
isArbitraryValue,
} from '../helpers';

export function flexGrowShrink(
type: 'Grow' | 'Shrink',
value: string,
config?: TwTheme['flexGrow'] | TwTheme['flexShrink'],
): StyleIR | null {
value = value.replace(/^-/, ``);
if (value[0] === `[` && value.endsWith(`]`)) {
if (isArbitraryValue(value)) {
value = value.slice(1, -1);
}
const configKey = value === `` ? `DEFAULT` : value;
Expand Down
4 changes: 2 additions & 2 deletions src/resolve/line-height.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { TwTheme } from '../tw-config';
import type { StyleIR } from '../types';
import { Unit } from '../types';
import { parseNumericValue, complete, toStyleVal } from '../helpers';
import { parseNumericValue, complete, toStyleVal, isArbitraryValue } from '../helpers';

export default function lineHeight(
value: string,
config?: TwTheme['lineHeight'],
): StyleIR | null {
const parseValue =
config?.[value] ?? (value.startsWith(`[`) ? value.slice(1, -1) : value);
config?.[value] ?? (isArbitraryValue(value) ? value.slice(1, -1) : value);

const parsed = parseNumericValue(parseValue);
if (!parsed) {
Expand Down
9 changes: 7 additions & 2 deletions src/resolve/spacing.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import type { TwTheme } from '../tw-config';
import type { Direction, ParseContext, StyleIR } from '../types';
import { Unit } from '../types';
import { parseNumericValue, parseUnconfigged, toStyleVal } from '../helpers';
import {
isArbitraryValue,
parseNumericValue,
parseUnconfigged,
toStyleVal,
} from '../helpers';

export default function spacing(
type: 'margin' | 'padding',
Expand All @@ -11,7 +16,7 @@ export default function spacing(
config?: TwTheme['margin'] | TwTheme['padding'],
): StyleIR | null {
let numericValue = ``;
if (value[0] === `[`) {
if (isArbitraryValue(value)) {
numericValue = value.slice(1, -1);
} else {
const configValue = config?.[value];
Expand Down
5 changes: 1 addition & 4 deletions src/resolve/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { DependentStyle, ParseContext, Style, StyleIR } from '../types';
import { isString, Unit } from '../types';
import {
complete,
isArbitraryValue,
parseNumericValue,
parseStyleVal,
parseUnconfigged,
Expand Down Expand Up @@ -246,10 +247,6 @@ function createStyle(
};
}

function isArbitraryValue(value: string): boolean {
return value.startsWith(`[`) && value.endsWith(`]`);
}

function parseOriginValue(
value: string | undefined,
allowedPositions: OriginPosition[],
Expand Down
Loading