UNPKG

tw-merge

Version:

Merge CSS utility classes without style conflicts - small and zero config

223 lines (180 loc) 5.9 kB
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)]; }