From b5bfa7b95ba74c27b363fe7b79a083f7014804a2 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Tue, 25 May 2021 15:46:11 -0400 Subject: [PATCH] JIT: Add support for before/after pseudo-elements --- src/corePlugins.js | 5 +- src/jit/corePlugins.js | 84 ++++++++++++++++++++++------ src/plugins/content.js | 5 ++ src/plugins/index.js | 2 + src/util/pluginUtils.js | 25 +++++++-- stubs/defaultConfig.stub.js | 3 + tests/jit/arbitrary-values.test.css | 3 + tests/jit/arbitrary-values.test.html | 1 + tests/jit/basic-usage.test.css | 3 + tests/jit/basic-usage.test.html | 1 + tests/jit/variants.test.css | 17 ++++++ tests/jit/variants.test.html | 4 ++ 12 files changed, 130 insertions(+), 23 deletions(-) create mode 100644 src/plugins/content.js diff --git a/src/corePlugins.js b/src/corePlugins.js index 10ad57102627..1bafdd3befa1 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -1,8 +1,11 @@ import * as plugins from './plugins/index.js' - import configurePlugins from './util/configurePlugins' +const jitOnlyPlugins = ['content'] + export default function ({ corePlugins: corePluginConfig }) { + corePluginConfig = corePluginConfig.filter((pluginName) => !jitOnlyPlugins.includes(pluginName)) + return configurePlugins(corePluginConfig, Object.keys(plugins)).map((pluginName) => { return plugins[pluginName]() }) diff --git a/src/jit/corePlugins.js b/src/jit/corePlugins.js index 5c4b87051969..f5fd232152db 100644 --- a/src/jit/corePlugins.js +++ b/src/jit/corePlugins.js @@ -12,6 +12,50 @@ import { export default { pseudoClassVariants: function ({ config, addVariant }) { + addVariant( + 'before', + transformAllSelectors( + (selector) => { + return updateAllClasses(selector, (className, { withPseudo }) => { + return withPseudo(`before${config('separator')}${className}`, '::before') + }) + }, + { + withRule: (rule) => { + let foundContent = false + rule.walkDecls('content', () => { + foundContent = true + }) + if (!foundContent) { + rule.prepend(postcss.decl({ prop: 'content', value: '""' })) + } + }, + } + ) + ) + + addVariant( + 'after', + transformAllSelectors( + (selector) => { + return updateAllClasses(selector, (className, { withPseudo }) => { + return withPseudo(`after${config('separator')}${className}`, '::after') + }) + }, + { + withRule: (rule) => { + let foundContent = false + rule.walkDecls('content', () => { + foundContent = true + }) + if (!foundContent) { + rule.prepend(postcss.decl({ prop: 'content', value: '""' })) + } + }, + } + ) + ) + let pseudoVariants = [ ['first', 'first-child'], ['last', 'last-child'], @@ -35,7 +79,7 @@ export default { addVariant( variantName, transformAllClasses((className, { withPseudo }) => { - return withPseudo(`${variantName}${config('separator')}${className}`, state) + return withPseudo(`${variantName}${config('separator')}${className}`, `:${state}`) }) ) } @@ -95,11 +139,13 @@ export default { (className) => { return `motion-safe${config('separator')}${className}` }, - () => - postcss.atRule({ - name: 'media', - params: '(prefers-reduced-motion: no-preference)', - }) + { + wrap: () => + postcss.atRule({ + name: 'media', + params: '(prefers-reduced-motion: no-preference)', + }), + } ) ) @@ -109,11 +155,13 @@ export default { (className) => { return `motion-reduce${config('separator')}${className}` }, - () => - postcss.atRule({ - name: 'media', - params: '(prefers-reduced-motion: reduce)', - }) + { + wrap: () => + postcss.atRule({ + name: 'media', + params: '(prefers-reduced-motion: reduce)', + }), + } ) ) }, @@ -142,11 +190,13 @@ export default { (className) => { return `dark${config('separator')}${className}` }, - () => - postcss.atRule({ - name: 'media', - params: '(prefers-color-scheme: dark)', - }) + { + wrap: () => + postcss.atRule({ + name: 'media', + params: '(prefers-color-scheme: dark)', + }), + } ) ) } @@ -162,7 +212,7 @@ export default { (className) => { return `${screen}${config('separator')}${className}` }, - () => postcss.atRule({ name: 'media', params: query }) + { wrap: () => postcss.atRule({ name: 'media', params: query }) } ) ) } diff --git a/src/plugins/content.js b/src/plugins/content.js new file mode 100644 index 000000000000..cc55ee6ffa35 --- /dev/null +++ b/src/plugins/content.js @@ -0,0 +1,5 @@ +import createUtilityPlugin from '../util/createUtilityPlugin' + +export default function () { + return createUtilityPlugin('content') +} diff --git a/src/plugins/index.js b/src/plugins/index.js index bea0b2a08e05..8107a2eaf9d7 100644 --- a/src/plugins/index.js +++ b/src/plugins/index.js @@ -161,3 +161,5 @@ export { default as transitionProperty } from './transitionProperty' export { default as transitionDelay } from './transitionDelay' export { default as transitionDuration } from './transitionDuration' export { default as transitionTimingFunction } from './transitionTimingFunction' + +export { default as content } from './content' diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js index a37c6e98d7dc..33abffc20c54 100644 --- a/src/util/pluginUtils.js +++ b/src/util/pluginUtils.js @@ -9,7 +9,7 @@ export function updateAllClasses(selectors, updateClass) { selectors.walkClasses((sel) => { let updatedClass = updateClass(sel.value, { withPseudo(className, pseudo) { - sel.parent.insertAfter(sel, selectorParser.pseudo({ value: `:${pseudo}` })) + sel.parent.insertAfter(sel, selectorParser.pseudo({ value: `${pseudo}` })) return className }, }) @@ -36,7 +36,7 @@ export function updateLastClasses(selectors, updateClass) { let updatedClass = updateClass(lastClass.value, { withPseudo(className, pseudo) { - lastClass.parent.insertAfter(lastClass, selectorParser.pseudo({ value: `:${pseudo}` })) + lastClass.parent.insertAfter(lastClass, selectorParser.pseudo({ value: `${pseudo}` })) return className }, }) @@ -51,11 +51,14 @@ export function updateLastClasses(selectors, updateClass) { return result } -export function transformAllSelectors(transformSelector, wrap = null) { +export function transformAllSelectors(transformSelector, { wrap, withRule } = {}) { return ({ container }) => { container.walkRules((rule) => { let transformed = rule.selector.split(',').map(transformSelector).join(',') rule.selector = transformed + if (withRule) { + withRule(rule) + } return rule }) @@ -67,23 +70,35 @@ export function transformAllSelectors(transformSelector, wrap = null) { } } -export function transformAllClasses(transformClass) { +export function transformAllClasses(transformClass, { wrap, withRule } = {}) { return ({ container }) => { container.walkRules((rule) => { let selector = rule.selector let variantSelector = updateAllClasses(selector, transformClass) rule.selector = variantSelector + if (withRule) { + withRule(rule) + } return rule }) + + if (wrap) { + let wrapper = wrap() + wrapper.append(container.nodes) + container.append(wrapper) + } } } -export function transformLastClasses(transformClass, wrap = null) { +export function transformLastClasses(transformClass, { wrap, withRule } = {}) { return ({ container }) => { container.walkRules((rule) => { let selector = rule.selector let variantSelector = updateLastClasses(selector, transformClass) rule.selector = variantSelector + if (withRule) { + withRule(rule) + } return rule }) diff --git a/stubs/defaultConfig.stub.js b/stubs/defaultConfig.stub.js index 29cc4dcb83ac..dcc51b6fbc1a 100644 --- a/stubs/defaultConfig.stub.js +++ b/stubs/defaultConfig.stub.js @@ -175,6 +175,9 @@ module.exports = { 200: '2', }, container: {}, + content: { + none: 'none', + }, cursor: { auto: 'auto', default: 'default', diff --git a/tests/jit/arbitrary-values.test.css b/tests/jit/arbitrary-values.test.css index e648584c26f4..de5344278e98 100644 --- a/tests/jit/arbitrary-values.test.css +++ b/tests/jit/arbitrary-values.test.css @@ -383,6 +383,9 @@ .duration-\[var\(--app-duration\)\] { transition-duration: var(--app-duration); } +.content-\[attr\(content-before\)\] { + content: attr(content-before); +} @media (min-width: 1024px) { .lg\:grid-cols-\[200px\2c repeat\(auto-fill\2c minmax\(15\%\2c 100px\)\)\2c 300px\] { grid-template-columns: 200px repeat(auto-fill, minmax(15%, 100px)) 300px; diff --git a/tests/jit/arbitrary-values.test.html b/tests/jit/arbitrary-values.test.html index 13b2b98b41cb..1811d2bedfa4 100644 --- a/tests/jit/arbitrary-values.test.html +++ b/tests/jit/arbitrary-values.test.html @@ -111,5 +111,6 @@
+
diff --git a/tests/jit/basic-usage.test.css b/tests/jit/basic-usage.test.css index ad799e79c02f..060f1c119cfd 100644 --- a/tests/jit/basic-usage.test.css +++ b/tests/jit/basic-usage.test.css @@ -777,3 +777,6 @@ .ease-in-out { transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } +.content-none { + content: none; +} diff --git a/tests/jit/basic-usage.test.html b/tests/jit/basic-usage.test.html index 95f08a2d2d6f..5c79de934167 100644 --- a/tests/jit/basic-usage.test.html +++ b/tests/jit/basic-usage.test.html @@ -143,5 +143,6 @@
+
diff --git a/tests/jit/variants.test.css b/tests/jit/variants.test.css index cbe8a02b85ee..e85180730c0b 100644 --- a/tests/jit/variants.test.css +++ b/tests/jit/variants.test.css @@ -18,6 +18,23 @@ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } +.before\:block::before { + content: ''; + display: block; +} +.before\:bg-red-500::before { + content: ''; + --tw-bg-opacity: 1; + background-color: rgba(239, 68, 68, var(--tw-bg-opacity)); +} +.after\:flex::after { + content: ''; + display: flex; +} +.after\:uppercase::after { + content: ''; + text-transform: uppercase; +} .first\:shadow-md:first-child { --tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), diff --git a/tests/jit/variants.test.html b/tests/jit/variants.test.html index bb40ebc01c1a..f45a22a18b9a 100644 --- a/tests/jit/variants.test.html +++ b/tests/jit/variants.test.html @@ -25,6 +25,10 @@
+ +
+
+