tw-merge
Version:
Merge CSS utility classes without style conflicts - small and zero config
223 lines (180 loc) • 5.9 kB
text/typescript
import { isNumericValue } from "./lib/utils";
export type Handler<T = any> = (
memory: T,
matches: NonNullable<RegExpMatchArray["groups"]>
) => boolean | "c"; // keep class | continue to next rule
export type Rule = [string, Handler];
export type RuleSet = Rule[];
export const TRAILING_SLASH_REGEXP = "(\\/\\d+)?";
export const VALUE_REGEXP = `(-(?<v>.+?)${TRAILING_SLASH_REGEXP})?`;
// simple rule
// -----------
export type SimpleHandlerOptions = { byType?: boolean };
export function createSimpleHandler({ byType }: SimpleHandlerOptions = {}) {
const simpleHandler: Handler<
Record<string, Partial<Record<"number" | "other", boolean>>>
> = (memory, { v: value, t: target }) => {
const type = byType && isNumericValue(value) ? "number" : "other";
const mem = (memory[target!] ??= {});
// seen before
if (mem[type]) return false;
// never seen
return (mem[type] = true);
};
return simpleHandler;
}
export type SimpleRuleOptions = SimpleHandlerOptions;
export function simpleRule(
target: string,
{ byType }: SimpleRuleOptions = {}
): Rule {
const regExp = `(?<t>${target})${VALUE_REGEXP}$`;
return [regExp, createSimpleHandler({ byType })];
}
// cardinal rule
// -------------
export type CardinalHandlerOptions = {
byType?: boolean;
};
type Direction = string;
const CARDINAL_OVERRIDES: Record<string, string> = {
t: ",y,tl,tr",
r: ",x,tr,br",
b: ",y,br,bl",
l: ",x,bl,tl",
x: "",
y: "",
s: "",
e: "",
ss: ",e,s",
se: ",e,s",
es: ",e,s",
ee: ",e,s",
};
const CARDINAL_DIRECTIONS =
Object.keys(CARDINAL_OVERRIDES).join("|") + "|tl|tr|br|bl";
export function createCardinalHandler({ byType }: CardinalHandlerOptions = {}) {
const cardinalHandler: Handler<
Partial<Record<Direction, Partial<Record<"number" | "other", boolean>>>> & {
_?: Partial<Record<"number" | "other", Set<string>>>;
}
> = (memory, { v: value, d: direction = "" }) => {
const type = byType && isNumericValue(value) ? "number" : "other";
const mem = (memory[direction] ??= {});
// seen before
if (mem[type]) return false;
// apply override
const memOverriders = ((memory._ ??= {})[type] ??= new Set());
if (
CARDINAL_OVERRIDES[direction]
?.split(",")
.some((dir) => memOverriders.has(dir))
)
return false;
// remember overrider
memOverriders.add(direction);
// never seen
mem[type] = true;
return true;
};
return cardinalHandler;
}
export type CardinalRuleOptions = {
/**
* Whether the direction is dash-separated (e.g. `border-t-2`)
* @default true
*/
dash?: boolean;
} & CardinalHandlerOptions;
export function cardinalRule(
target: string,
{ dash = true, byType }: CardinalRuleOptions = {}
): Rule {
const _target = `${target}(${dash ? "-" : ""}(?<d>${CARDINAL_DIRECTIONS}))?`;
const regExp = `${_target}${VALUE_REGEXP}$`;
return [regExp, createCardinalHandler({ byType })];
}
export function cardinalRules(targets: string, options?: CardinalRuleOptions) {
const _targets = targets.split("|");
return _targets.map((target) => cardinalRule(target, options));
}
// unique rule
// -----------
export function createUniqueHandler() {
const uniqueHandler: Handler<Record<string, boolean>> = (memory, groups) => {
const key = Object.entries(groups).find((x) => x[1])![0];
return memory[key] ? false : (memory[key] = true);
};
return uniqueHandler;
}
export type UniqueRuleOptions = { prefix?: string; def?: boolean };
export function uniqueRule(targets: (string | string[])[]): Rule {
const regExp = `(${targets
.map((target, targetI) =>
Array.isArray(target)
? target
.slice(1)
.map(
(subtarget, subtargetI) =>
`(?<i${targetI}_${subtargetI}>${`${target[0]}-(${subtarget})`})`
)
: `(?<i${targetI}>${target})`
)
.flat()
.join("|")})${TRAILING_SLASH_REGEXP}$`;
return [regExp, createUniqueHandler()];
}
// arbitrary rule
// --------------
export function createArbitraryHandler() {
const arbitraryHandler: Handler<Record<string, { done?: boolean }>> = (
memory,
{ p: property }
) => {
const mem = (memory[property!] ??= {});
// seen before
if (mem.done) return false;
// never seen
return (mem.done = true);
};
return arbitraryHandler;
}
export function arbitraryRule(): Rule {
return [`\\[(?<p>.+?):.*\\]$`, createArbitraryHandler()];
}
// conflict rule
// -------------
export type ConflictRuleTargets = Record<string, string>;
export function createConflictHandler(targets: ConflictRuleTargets) {
const overridableMap: Record<string, string[]> = {};
Object.entries(targets).forEach(([overridingUtility, overridableUtilities]) =>
overridableUtilities.split("|").forEach((value) => {
overridableMap[value] ??= [];
overridableMap[value]!.push(overridingUtility);
})
);
const conflictHandler: Handler<Record<string, boolean>> = (
memory,
{ u: utility }
) => {
// is overridable utility and overriding utility has been seen
const skipClass = Boolean(
utility! in overridableMap &&
overridableMap[utility!]!.some((u) => memory[u])
);
if (skipClass) return false;
// is overriding utility
if (utility! in targets) memory[utility!] = true;
// continue evaluating other rules
return "c";
};
return conflictHandler;
}
export function conflictRule(targets: ConflictRuleTargets): Rule {
const overridingUtilities = Object.keys(targets);
const overridableUtilities = Object.values(targets).join("|").split("|");
const matchingClasses = [...overridingUtilities, ...overridableUtilities];
const utility = `(?<u>${matchingClasses.join("|")})`;
const regExp = `${utility}${VALUE_REGEXP}$`;
return [regExp, createConflictHandler(targets)];
}