import cx from 'classnames' import { mergeBEMLike, } from '../../tailwind/classNames' import { escape, unescape, } from '../../tailwind/parser' import type { TailwindFeature, } from '../../types' import type { tBlockAttributes, } from '@ska/shared' import type { TailwindFeatureAttribute, } from '../../tailwind/attributes' import { type SelectorsOptionsValue, type SelectorsValue, } from '.' /** * Convert: * { * sm: { * value, * skaBlocksSelectors: { * lg: { * value, * } * }, * }, * } * to: * { * sm: { * value, * }, * lg:sm: { * value, * } * } */ export const flattenSelectors = (selectors: SelectorsValue = {}, parent: string = ''): SelectorsValue => { return Object.keys(selectors).reduce((acc, cur) => { const { skaBlocksSelectors = {}, ...features } = selectors[cur] const selector = [parent, cur].filter(v => v).join(':') // TW 4 order swap return { ...acc, [selector]: { ...features, }, ...(Object.keys(skaBlocksSelectors).length > 0 && { ...flattenSelectors(skaBlocksSelectors, selector), }), } }, {} as SelectorsValue) } const SHIFT_TO_LEFT = '<' const SHIFT_TO_TOP = '^' const shiftToLeft = (className: string): string => { /** Nothing to shift. */ if(className.indexOf(`:${SHIFT_TO_LEFT}`) === -1) { /** Can't shift first variant, but get rid of `<`-s. */ if(className.startsWith(SHIFT_TO_LEFT)) { let leftTrimmed = className.substring(1) while(leftTrimmed.startsWith(SHIFT_TO_LEFT)) { leftTrimmed = className.substring(1) } return leftTrimmed } return className } const arr = className .split(`:${SHIFT_TO_LEFT}`) .filter(v => v) // A variant was `<` - noop, equal to `[&]` variant .reduce((acc, cur, i) => { return [ ...acc, /** * Split the rest of the class to variants by escaping first and unescaping. * `i > 0` means the part had a shifter so put it back for detection below. */ ...escape(`${i > 0 ? SHIFT_TO_LEFT : ''}${cur}`).split(':').map(unescape), ] }, [] as string[]) let shifted = true while(shifted) { shifted = false for(let i = 1; i < arr.length; i++) { if(arr[i].startsWith(SHIFT_TO_LEFT)) { /** Shift the variant in the array and strip shifter. */ [arr[i - 1], arr[i]] = [arr[i].slice(1), arr[i - 1]] shifted = true } } } return arr.join(':') } const shiftToTop = (className: string): string => { /** Nothing to shift. */ if(className.indexOf(SHIFT_TO_TOP) === -1) { return className } const arr = className .split(':') .filter(v => v !== SHIFT_TO_TOP) // A variant was `^` - noop, equal to `[&]` variant return arr .filter(x => x.startsWith(SHIFT_TO_TOP)) .map(x => x.slice(1)) .concat(arr.filter(x => !x.startsWith(SHIFT_TO_TOP))) .join(':') } const APPLY_SELECTOR = [ mergeBEMLike, shiftToLeft, shiftToTop, ] /** Add selector to a Tailwind class name. */ export const applySelector = (selector: string, className: string): string => { return APPLY_SELECTOR.reduce((acc, fn) => fn(acc), `${selector}:${className}`) } export const getSelectorDisplayName = (selector: string) => { return selector.replaceAll('\\', '') } export const isOptionSelector = (selector: string): boolean => { return selector[0] === '{' && selector[selector.length - 1] === '}' } export const unwrapOptionSelector = (selector: string): string => { if(!isOptionSelector(selector)) { return selector } return selector.substring(1, selector.length - 1) } export const sanitizeOptionSelectorLabel = (selector: string) => { return selector .replaceAll('!', '') .replaceAll('_', ' ') } export const getOptionSelectorLabel = (s: string): string => { const selector = unwrapOptionSelector(s) if(selector.indexOf('=') === -1) { return sanitizeOptionSelectorLabel(selector) } const [label, _value] = selector.split('=') return sanitizeOptionSelectorLabel(label) } export const isOptionSelectorActive = (s: string, skaBlocksOptions: SelectorsOptionsValue): boolean => { const label = getOptionSelectorLabel(s) const selector = unwrapOptionSelector(s) if(selector.indexOf('=') === -1) { if(selector.indexOf('!') === 0) { return !skaBlocksOptions[label] } return skaBlocksOptions[label] === true } const [_rawLabel = '', value = ''] = selector.split('=') return skaBlocksOptions[label] === value } export const withSelectorsClassNames = ( classNames: string, feature: TailwindFeature, attributes: tBlockAttributes, buildClassNames: (value: TailwindFeatureAttribute | undefined, feature: TailwindFeature) => string ): string => { const { skaBlocksSelectors: selectorsValue = {}, skaBlocksOptions = {}, } = attributes const skaBlocksSelectors = flattenSelectors(selectorsValue) const selectors = Object.keys(skaBlocksSelectors) if(!selectors.length) { return classNames } return cx(classNames, selectors.map(selector => { const {[selector]: selectorValue = {}} = skaBlocksSelectors const {[feature.id]: featureValue} = selectorValue if(isOptionSelector(selector)) { return isOptionSelectorActive(selector, skaBlocksOptions) ? buildClassNames(featureValue, feature).split(' ').filter(v => v).join(' ') : '' } return buildClassNames(featureValue, feature).split(' ').filter(v => v).map(className => applySelector(selector, className)).join(' ') })) }