|
import cx from "classnames"; |
|
|
|
export interface MatcherConstructor { |
|
(modifiers: string, candidate: string): RegExp | null; |
|
} |
|
|
|
const memo = new Map<string, string>(); |
|
const regexpMemo = new Map<string, RegExp>(); |
|
|
|
const regexp = (...args: Parameters<typeof String.raw>) => { |
|
const pattern = String.raw(...args); |
|
if (regexpMemo.has(pattern)) { |
|
return regexpMemo.get(pattern)!; |
|
} |
|
|
|
const newRegexp = new RegExp(pattern); |
|
regexpMemo.set(pattern, newRegexp); |
|
return newRegexp; |
|
}; |
|
|
|
const spacingRegExp = /[\s\t ]+/g; |
|
const modifiersAndRestRegExp = /([^\s]*?)((?:[-\w]*|(?:::))*)$/; |
|
|
|
const colorsMatcher = |
|
"transparent|current|black|white|(?:(?:gray|red|yellow|pink|indigo|purple)-(?:50|(?:[1-9]00)))"; |
|
const percentageMatcher = "0|(?:[27]{0,1}5)|(?:[1-9]0)|100"; |
|
const dirsMatcher = "t|tr|r|br|b|bl|l|tl"; |
|
const directionsMatcher = |
|
"bottom|center|left|left-bottom|left-top|right|right-bottom|right-top|top"; |
|
const fractionsUpTo4 = `((1/[2-4])|(2/[34])|(3/4))`; |
|
const fractionsUpTo6 = `((1/([2-6]|12))|(2/[3-6])|(3/[4-6])|(4/[56])|(5/6))`; |
|
const zTo12 = "([0-9]|(1[0-2]))"; |
|
const numbers = "([0-3](.5)?)|([4-9])|([1-9][24680])"; |
|
|
|
export const groups = [ |
|
"bg-(?:auto|cover|contain)", |
|
"bg-(?:clip-border|clip-padding|clip-content|clip-text)", |
|
"bg-(?:fixed|local|scroll)", |
|
"bg-(?:origin-border|origin-padding|origin-content)", |
|
"bg-(?:repeat|no-repeat|repeat-x|repeat-y|repeat-round|repeat-space)", |
|
"isolate|isolation-auto", |
|
"overscroll-(auto|contain|none|y-auto|y-contain|y-none|x-auto|x-contain|x-none)", |
|
"static|fixed|absolute|relative|sticky", |
|
`-?space-x-(${numbers}|px|reverse)`, |
|
`-?space-y-(${numbers}|px|reverse)`, |
|
`(in)?visible`, |
|
`(ring-offset-[0248])|(shadow(-(sm|md|lg|xl|2xl|inner|none)))`, |
|
`auto-cols-(auto|min|max|fr)`, |
|
`auto-rows-(auto|min|max|fr)`, |
|
`bg-(?:${colorsMatcher})`, |
|
`bg-(?:${directionsMatcher})`, |
|
`bg-(?:none|(?:gradient-to-(?:${dirsMatcher})))`, |
|
`bg-opacity-(?:${percentageMatcher})`, |
|
`block|contents|flex|flow-root|grid|hidden|(?:inline(?:-[block|flex|grid|table])?)|list-item|table|(?:table-(?:caption|cell|column|row|(?:(?:(?:header|footer|column)-)?group)))`, |
|
`border-(?:${colorsMatcher})`, |
|
`border-(solid|dashed|dotted|double|none)`, |
|
`border-opacity-(?:${percentageMatcher})`, |
|
`box-(?:border|content)`, |
|
`clear-(?:left|right|both|none)`, |
|
`col-(auto|((span|start)-(${zTo12}|auto|full)))`, |
|
`content-(center|start|end|between|around|evenly)`, |
|
`decoration-(?:slice|clone)`, |
|
`divide-(?:${colorsMatcher})`, |
|
`divide-(solid|dashed|dotted|double|none)`, |
|
`divide-opacity-(?:${percentageMatcher})`, |
|
`divide-x(-([0248]|reverse))?`, |
|
`divide-y(-([0248]|reverse))?`, |
|
`flex-((row(-reverse)?)|(col(-reverse)?))`, |
|
`flex-(1|auto|initial|none)`, |
|
`flex-(wrap(-reverse)?|nowrap)`, |
|
`flex-grow(-0)?`, |
|
`flex-shrink(-0)?`, |
|
`float-(?:right|left|none)`, |
|
`from-(?:${colorsMatcher})`, |
|
`gap(-[xy])?(-(px|${numbers}))?`, |
|
`grid-cols-(${zTo12}|none)`, |
|
`grid-flow-(row|col|row-dense|col-dense)`, |
|
`grid-rows-(${zTo12}|first|last|none)`, |
|
`h-(full|screen|auto|${numbers}|((1/([2-6]|12))|()))`, |
|
`items-(start|end|center|baseline|stretch)`, |
|
`justify-(start|end|center|between|around|evenly)`, |
|
`justify-items-(start|end|center|stretch)`, |
|
`justify-self-(auto|start|end|center|stretch)`, |
|
`max-w-(0|none|xs|((screen-)?(sm|md|lg|[23]?xl|full|none))|[4-7]xl|full|min|max|prose)`, |
|
`min-w-(0|full|min|max)`, |
|
`object-(?:${directionsMatcher})`, |
|
`object-(?:contain|cover|fill|none|scale-down)`, |
|
`opacity-(?:${percentageMatcher})`, |
|
`order-(${zTo12}|first|last|none)`, |
|
`overflow-(?:auto|hidden|visible|scroll|(?:[xy]-(?:auto|hidden|visible|scroll)))`, |
|
`place-(start|end|center|stretch)`, |
|
`place-content-(center|start|end|between|around|evenly|stretch)`, |
|
`place-self-(auto|start|end|center|stretch)`, |
|
`ring-(?:${colorsMatcher})`, |
|
`ring-offset-(?:${colorsMatcher})`, |
|
`ring-opacity-(?:${percentageMatcher})`, |
|
`ring(-([0248]|inset))?`, |
|
`row-(auto|((span|start)-(${zTo12}|auto|full)))`, |
|
`self-(auto|start|end|center|stretch|baseline)`, |
|
`to-(?:${colorsMatcher})`, |
|
`via-(?:${colorsMatcher})`, |
|
`w-(full|screen|min|max|auto|${numbers}|${fractionsUpTo6})`, |
|
`z-(0|([1-5]0)|auto)`, |
|
]; |
|
|
|
export function getGroupsMatcher(modifiers: string, candidate: string) { |
|
for (const groupRules of groups) { |
|
const matcher = regexp`(\s|^)${modifiers}(?:${groupRules})($|\s)`; |
|
|
|
if (matcher.test(candidate)) { |
|
return matcher; |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
export function getBoxMatcher(modifiers: string, candidate: string) { |
|
let [matched, sign, property, mp, mpXY, insetXY, amount] = |
|
candidate.match( |
|
regexp`(?:^|\s)(-?)((m|p)(?:([xy])|(?:[trbl]))?)-(.*?)($|\s)` |
|
) || []; |
|
|
|
if (!matched) return null; |
|
|
|
const rule = (...args: Parameters<typeof String.raw>) => |
|
regexp`(^|\s)${modifiers}(-)?(${String.raw(...args)})(-(.*?))($|\s)`; |
|
|
|
switch (property) { |
|
case "mx": |
|
case "my": |
|
case "px": |
|
case "py": |
|
return rule`${property}|${mp}`; |
|
|
|
case "pt": |
|
case "pb": |
|
case "mt": |
|
case "mb": |
|
return rule`${property}|(${mp}y?)`; |
|
|
|
case "pr": |
|
case "pl": |
|
case "mr": |
|
case "ml": |
|
return rule`${property}|(${mp}x?)`; |
|
|
|
case "p": |
|
case "m": |
|
return rule`${property}`; |
|
|
|
default: |
|
return null; |
|
} |
|
} |
|
|
|
export function getOffsetMatcher(modifiers: string, candidate: string) { |
|
let [matched, sign, property, mp, mpXY, insetXY, amount] = |
|
candidate.match( |
|
regexp`(?:^|\s)(-?)(top|right|bottom|left|(inset(?:-(x|y))?))-(.*?)($|\s)` |
|
) || []; |
|
|
|
if (!matched) return null; |
|
|
|
const rule = (...args: Parameters<typeof String.raw>) => |
|
regexp`(^|\s)${modifiers}(-)?(${String.raw(...args)})(-(.*?))($|\s)`; |
|
|
|
switch (property) { |
|
case "inset-x": |
|
case "inset-y": |
|
return rule`${property}|inset`; |
|
|
|
case "top": |
|
case "bottom": |
|
return rule`${property}|(inset-y?)`; |
|
|
|
case "right": |
|
case "left": |
|
return rule`${property}|(inset-x?)`; |
|
|
|
case "inset": |
|
return rule`${property}`; |
|
|
|
default: |
|
return null; |
|
} |
|
} |
|
|
|
export function getRoundMatcher(modifiers: string, candidate: string) { |
|
const [match, direction, y, x] = |
|
candidate.match( |
|
regexp`(?:^|\s)rounded(?:-(([tb]?)([lr]?)))?(-([^\s]+))?(?:$|\s)` |
|
) || []; |
|
|
|
if (!match) return null; |
|
|
|
const rule = (...args: Parameters<typeof String.raw>) => |
|
regexp`(^|\s)${modifiers}rounded(-${String.raw( |
|
...args |
|
)})?(-([^-]*))?($|\s)`; |
|
|
|
switch (direction) { |
|
case "t": |
|
case "b": |
|
return rule`${y}`; |
|
|
|
case "r": |
|
case "l": |
|
return rule`${x}`; |
|
|
|
case "tr": |
|
case "tl": |
|
case "br": |
|
case "bl": |
|
return rule`(${y}${x}|${x}|${y})`; |
|
|
|
default: |
|
return rule`((?![tb]?[lr]?).*?)`; |
|
} |
|
} |
|
|
|
export function getBorderWidthMatcher(modifiers: string, candidate: string) { |
|
const [match, direction, y, x] = |
|
candidate.match( |
|
regexp`(?:^|\s)border(?:-([trbl]))?(?:-(?:[^\s]+))?(?:$|\s)` |
|
) || []; |
|
|
|
if (!match) return null; |
|
|
|
const rule = (...args: Parameters<typeof String.raw>) => |
|
regexp`(^|\s)${modifiers}border(-${String.raw( |
|
...args |
|
)})?(-((?![tblr]-)[^-]*))?($|\s)`; |
|
|
|
switch (direction) { |
|
case "t": |
|
case "b": |
|
case "r": |
|
case "l": |
|
return rule`${direction}`; |
|
|
|
default: |
|
return rule`(?![tblr])`; |
|
} |
|
} |
|
|
|
function matchCandidate( |
|
matchers: MatcherConstructor[], |
|
{ |
|
modifiers, |
|
classNames, |
|
candidate, |
|
}: { |
|
modifiers: string; |
|
classNames: string; |
|
candidate: string; |
|
} |
|
) { |
|
for (const getMatcher of matchers) { |
|
let matcher = getMatcher(modifiers, candidate); |
|
if (matcher) { |
|
console.log({ |
|
matcher: matcher.source, |
|
classNames, |
|
candidate, |
|
modifiers, |
|
result: classNames.match(matcher), |
|
}); |
|
return !matcher.test(classNames); |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
export const createResolver = ( |
|
matchers: MatcherConstructor[] = [ |
|
getBoxMatcher, |
|
getOffsetMatcher, |
|
getRoundMatcher, |
|
getBorderWidthMatcher, |
|
getGroupsMatcher, |
|
] |
|
) => { |
|
const canAddCandidate = matchCandidate.bind(null, matchers); |
|
return (...classNames: Parameters<typeof cx>) => { |
|
const key = cx(classNames).replace(spacingRegExp, " ").trim(); |
|
if (memo.has(key)) { |
|
return memo.get(key); |
|
} |
|
|
|
const state = {}; |
|
const candidates = key.split(spacingRegExp).reverse(); |
|
let resultingClassName = candidates.shift() || ""; |
|
for (const candidate of candidates) { |
|
const [, modifiers, className] = candidate.match( |
|
modifiersAndRestRegExp |
|
) || ["", ""]; |
|
|
|
if ( |
|
canAddCandidate({ |
|
classNames: resultingClassName, |
|
candidate: className, |
|
modifiers, |
|
}) |
|
) |
|
resultingClassName = `${candidate} ${resultingClassName}`; |
|
} |
|
|
|
memo.set(key, resultingClassName); |
|
return resultingClassName; |
|
}; |
|
}; |
|
|
|
export const tw = createResolver(); |