Skip to content

Commit

Permalink
test: pseudo precedence
Browse files Browse the repository at this point in the history
  • Loading branch information
sastan committed Dec 9, 2020
1 parent f3f48aa commit 7bc9e6f
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 64 deletions.
8 changes: 8 additions & 0 deletions packages/core/src/__tests__/api.json
Expand Up @@ -482,6 +482,14 @@
".group:hover .group-hover\\:text-yellow-600{--tw-text-opacity:1;color:#d97706;color:rgba(217,119,6,var(--tw-text-opacity))}"
]
],
"group-focus:text-red-500 group-active:text-blue-500 group-hover:text-yellow-500": [
"group-focus:text-red-500 group-active:text-blue-500 group-hover:text-yellow-500",
[
".group:hover .group-hover\\:text-yellow-500{--tw-text-opacity:1;color:#f59e0b;color:rgba(245,158,11,var(--tw-text-opacity))}",
".group:focus .group-focus\\:text-red-500{--tw-text-opacity:1;color:#ef4444;color:rgba(239,68,68,var(--tw-text-opacity))}",
".group:active .group-active\\:text-blue-500{--tw-text-opacity:1;color:#3b82f6;color:rgba(59,130,246,var(--tw-text-opacity))}"
]
],
"transform rotate-3 scale-125": [
"transform rotate-3 scale-125",
[
Expand Down
54 changes: 54 additions & 0 deletions packages/core/src/__tests__/api.test.ts
Expand Up @@ -56,6 +56,60 @@ test.each(
expect(injector.target).toStrictEqual(rules)
})

test('variant pseudo presedence', () => {
expect(tw`
active:text-blue-200
empty:text-green-500
group-invalid:text-red-400
checked:text-gray-500
link:text-green-100
disabled:text-blue-300
even:text-gray-300
first:text-gray-50
focus-visible:text-blue-100
focus-within:text-gray-800
focus:text-blue-50
group-focus:text-gray-700
group-hover:text-gray-600
hover:text-gray-900
invalid:text-blue-600
last:text-gray-100
odd:text-gray-200
optional:text-blue-800
read-only:text-blue-700
required:text-blue-400
valid:text-blue-500
visited:text-gray-400
`).toBe(
'active:text-blue-200 empty:text-green-500 group-invalid:text-red-400 checked:text-gray-500 link:text-green-100 disabled:text-blue-300 even:text-gray-300 first:text-gray-50 focus-visible:text-blue-100 focus-within:text-gray-800 focus:text-blue-50 group-focus:text-gray-700 group-hover:text-gray-600 hover:text-gray-900 invalid:text-blue-600 last:text-gray-100 odd:text-gray-200 optional:text-blue-800 read-only:text-blue-700 required:text-blue-400 valid:text-blue-500 visited:text-gray-400',
)

expect(injector.target).toStrictEqual([
'.first\\:text-gray-50:first-child{--tw-text-opacity:1;color:#f9fafb;color:rgba(249,250,251,var(--tw-text-opacity))}',
'.last\\:text-gray-100:last-child{--tw-text-opacity:1;color:#f3f4f6;color:rgba(243,244,246,var(--tw-text-opacity))}',
'.even\\:text-gray-300:nth-child(2n){--tw-text-opacity:1;color:#d1d5db;color:rgba(209,213,219,var(--tw-text-opacity))}',
'.odd\\:text-gray-200:nth-child(odd){--tw-text-opacity:1;color:#e5e7eb;color:rgba(229,231,235,var(--tw-text-opacity))}',
".link\\:text-green-100:link{--tw-text-opacity:1;color:#d1fae5;color:rgba(209,250,229,var(--tw-text-opacity))}",
'.visited\\:text-gray-400:visited{--tw-text-opacity:1;color:#9ca3af;color:rgba(156,163,175,var(--tw-text-opacity))}',
".empty\\:text-green-500:empty{--tw-text-opacity:1;color:#10b981;color:rgba(16,185,129,var(--tw-text-opacity))}",
'.checked\\:text-gray-500:checked{--tw-text-opacity:1;color:#6b7280;color:rgba(107,114,128,var(--tw-text-opacity))}',
'.focus-within\\:text-gray-800:focus-within{--tw-text-opacity:1;color:#1f2937;color:rgba(31,41,55,var(--tw-text-opacity))}',
'.group:hover .group-hover\\:text-gray-600{--tw-text-opacity:1;color:#4b5563;color:rgba(75,85,99,var(--tw-text-opacity))}',
'.hover\\:text-gray-900:hover{--tw-text-opacity:1;color:#111827;color:rgba(17,24,39,var(--tw-text-opacity))}',
'.focus\\:text-blue-50:focus{--tw-text-opacity:1;color:#eff6ff;color:rgba(239,246,255,var(--tw-text-opacity))}',
'.group:focus .group-focus\\:text-gray-700{--tw-text-opacity:1;color:#374151;color:rgba(55,65,81,var(--tw-text-opacity))}',
'.focus-visible\\:text-blue-100:focus-visible{--tw-text-opacity:1;color:#dbeafe;color:rgba(219,234,254,var(--tw-text-opacity))}',
'.active\\:text-blue-200:active{--tw-text-opacity:1;color:#bfdbfe;color:rgba(191,219,254,var(--tw-text-opacity))}',
'.disabled\\:text-blue-300:disabled{--tw-text-opacity:1;color:#93c5fd;color:rgba(147,197,253,var(--tw-text-opacity))}',
'.read-only\\:text-blue-700:read-only{--tw-text-opacity:1;color:#1d4ed8;color:rgba(29,78,216,var(--tw-text-opacity))}',
'.optional\\:text-blue-800:optional{--tw-text-opacity:1;color:#1e40af;color:rgba(30,64,175,var(--tw-text-opacity))}',
'.required\\:text-blue-400:required{--tw-text-opacity:1;color:#60a5fa;color:rgba(96,165,250,var(--tw-text-opacity))}',
".group:invalid .group-invalid\\:text-red-400{--tw-text-opacity:1;color:#f87171;color:rgba(248,113,113,var(--tw-text-opacity))}",
'.invalid\\:text-blue-600:invalid{--tw-text-opacity:1;color:#2563eb;color:rgba(37,99,235,var(--tw-text-opacity))}',
'.valid\\:text-blue-500:valid{--tw-text-opacity:1;color:#3b82f6;color:rgba(59,130,246,var(--tw-text-opacity))}',
])
})

test.each([
[
['bg-white', false && 'rounded'],
Expand Down
73 changes: 51 additions & 22 deletions packages/core/src/internal/presedence.ts
@@ -1,9 +1,13 @@
/* eslint-disable no-return-assign, no-cond-assign, no-implicit-coercion */

// Based on https://github.com/kripod/otion
// License MIT

import type { ThemeResolver } from '@tw-in-js/types'
import { tail, includes } from './util'

// Shared variables
let _: string
let precedence: number
let match: RegExpExecArray | null

Expand All @@ -13,19 +17,14 @@ let match: RegExpExecArray | null
// 1536px -> 9
// 36rem -> 3
// 96rem -> 9
// eslint-disable-next-line no-return-assign
export const responsivePrecedence = (css: string): number =>
// eslint-disable-next-line no-cond-assign
(match = /^(\d+(?:.\d+)?)(p)?/.exec(css))
? +match[1] / (match[2] ? 15 : 1) / 10 // eslint-disable-line no-implicit-coercion
: 0
const responsivePrecedence = (css: string): number =>
(match = /^(\d+(?:.\d+)?)(p)?/.exec(css)) ? +match[1] / (match[2] ? 15 : 1) / 10 : 0

// Colon and dash count of string (ascending): 0 -> 7 => 3 bits
export const seperatorPrecedence = (string: string): number => {
precedence = 0

for (let index = string.length; index--; ) {
// eslint-disable-next-line no-implicit-coercion
if (includes('-:,', string[index])) {
++precedence
}
Expand All @@ -34,10 +33,13 @@ export const seperatorPrecedence = (string: string): number => {
return precedence
}

// Pesude and groupd variant presedence
// Pesudo variant presedence
// Chars 3 - 8: Uniquely identifies a pseudo selector
// represented as a bit set for each relevant value
// 16 bits: one for each variant above plus one for unknown variants
// 17 bits: one for each variant plus one for unknown variants
//
// ':group-*' variants are normalized to their native pseudo class (':group-hover' -> ':hover')
// as they already have a higher selector presedence due to the add '.group' ('.group:hover .group-hover:...')

// Sources:
// - https://bitsofco.de/when-do-the-hover-focus-and-active-pseudo-classes-apply/#orderofstyleshoverthenfocusthenactive
Expand All @@ -54,22 +56,48 @@ const PRECEDENCES_BY_PSEUDO_CLASS = [
/* vi */ 'sited' /* : 5 */,
/* em */ 'pty' /* : 6 */,
/* ch */ 'ecked' /* : 7 */,
/* gr */ 'oup-h' /* over : 8 */,
/* gr */ 'oup-f' /* ocus : 9 */,
/* fo */ 'cus-w' /* ithin : 10 */,
/* ho */ 'ver' /* : 11 */,
/* fo */ 'cus' /* : 12 */,
/* fo */ 'cus-v' /* isible : 13 */,
/* ac */ 'tive' /* : 14 */,
/* di */ 'sable' /* d : 15 */,
/* fo */ 'cus-w' /* ithin : 8 */,
/* ho */ 'ver' /* : 9 */,
/* fo */ 'cus' /* : 10 */,
/* fo */ 'cus-v' /* isible : 11 */,
/* ac */ 'tive' /* : 12 */,
/* di */ 'sable' /* d : 13 */,
/* re */ 'ad-on' /* ly: 14 */,
/* op */ 'tiona' /* l: 15 */,
/* re */ 'quire' /* d: 16 */,
]
/* eslint-enable capitalized-comments */

// eslint-disable-next-line no-return-assign
export const pseudoPrecedence = (pseudoClass: string): number =>
~(precedence = PRECEDENCES_BY_PSEUDO_CLASS.indexOf(pseudoClass.slice(3, 8)))
const pseudoPrecedence = (pseudoClass: string): number =>
~(precedence = PRECEDENCES_BY_PSEUDO_CLASS.indexOf(
pseudoClass.replace(/^:group-/, ':').slice(3, 8),
))
? precedence
: PRECEDENCES_BY_PSEUDO_CLASS.length
: 17

// Variants: 27 bits
export const makeVariantPresedenceCalculator = (
theme: ThemeResolver,
variants: Record<string, string | undefined>,
) => (presedence: number, variant: string): number =>
presedence |
// 5: responsive
((_ = theme('screens', tail(variant), ''))
? // 0=none, 1=sm, 2=md, 3=lg, 4=xl, 5=2xl, 6=??, 7=??
// 0 - 31: 5 bits
// 576px -> 3
// 1536px -> 9
// 36rem -> 3
// 96rem -> 9
(responsivePrecedence(_) & 31) << 22
: // 1: dark mode flag
variant === ':dark'
? 1 << 21
: // 4: precedence of other at-rules
(_ = variants[variant] || variant)[0] === '@'
? (seperatorPrecedence(_) & 15) << 17
: // 17: pseudo and group variants
1 << pseudoPrecedence(variant))

// https://github.com/kripod/otion/blob/main/packages/otion/src/propertyMatchers.ts
// "+1": [
Expand Down Expand Up @@ -114,7 +142,6 @@ const propertyPrecedence = (property: string): number => {

return (
seperatorPrecedence(unprefixedProperty) +
// eslint-disable-next-line no-implicit-coercion
(match ? +!!match[1] /* +1 */ || -!!match[2] /* -1 */ : 0) +
1
)
Expand All @@ -130,3 +157,5 @@ export const descending = (value: number): number => Math.max(0, 15 - value)
// 0 - 15 => 4 bits
export const declarationValuePrecedence = (value: string): number =>
descending(seperatorPrecedence(value))

/* eslint-enable no-return-assign, no-cond-assign, no-implicit-coercion */
48 changes: 6 additions & 42 deletions packages/core/src/process/serialize.ts
Expand Up @@ -2,14 +2,12 @@ import type { Context, CSSRules, Prefixer, Rule } from '@tw-in-js/types'

import * as is from '../internal/is'

import { join, tail, includes, escape, hyphenate } from '../internal/util'
import { join, includes, escape, hyphenate } from '../internal/util'
import {
responsivePrecedence,
pseudoPrecedence,
seperatorPrecedence,
descending,
declarationPropertyPrecedence,
declarationValuePrecedence,
makeVariantPresedenceCalculator,
} from '../internal/presedence'

import { variants } from '../tailwind/variants'
Expand Down Expand Up @@ -157,50 +155,16 @@ export const serialize = (
}
}

const variantPresedence = makeVariantPresedenceCalculator(theme, variants)

return (css, className, rule) => {
rules = []

stringify(
[],

className ? '.' + escape(className) : '',

rule
? // eslint-disable-next-line unicorn/no-reduce
rule.variants.reduceRight((presedence, variant) => {
// Variants: 26 bits

const size = theme('screens', tail(variant), '')

// 5: responsive
if (size) {
// 0=none, 1=sm, 2=md, 3=lg, 4=xl, 5=2xl, 6=??, 7=??
// 0 - 31: 5 bits
// 576px -> 3
// 1536px -> 9
// 36rem -> 3
// 96rem -> 9
return presedence | ((responsivePrecedence(size) & 31) << 22)
}

// 1: dark mode flag
if (variant === ':dark') {
return presedence | (1 << 21)
}

const atRule = variants[variant] || variant

return (
presedence |
(atRule[0] === '@'
? // 4: precedence of other at-rules
(seperatorPrecedence(atRule) & 15) << 17
: // 16: pseudo and group variants
(1 << pseudoPrecedence(variant)) & 0x1ffff)
)
}, 0)
: 0,

// eslint-disable-next-line unicorn/no-reduce
rule ? rule.variants.reduceRight(variantPresedence, 0) : 0,
css,
)

Expand Down

0 comments on commit 7bc9e6f

Please sign in to comment.