source: frontend/node_modules/tailwindcss/src/util/formatVariantSelector.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 9.6 KB
Line 
1import selectorParser from 'postcss-selector-parser'
2import unescape from 'postcss-selector-parser/dist/util/unesc'
3import escapeClassName from '../util/escapeClassName'
4import prefixSelector from '../util/prefixSelector'
5import { movePseudos } from './pseudoElements'
6import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
7
8/** @typedef {import('postcss-selector-parser').Root} Root */
9/** @typedef {import('postcss-selector-parser').Selector} Selector */
10/** @typedef {import('postcss-selector-parser').Pseudo} Pseudo */
11/** @typedef {import('postcss-selector-parser').Node} Node */
12
13/** @typedef {{format: string, respectPrefix: boolean}[]} RawFormats */
14/** @typedef {import('postcss-selector-parser').Root} ParsedFormats */
15/** @typedef {RawFormats | ParsedFormats} AcceptedFormats */
16
17let MERGE = ':merge'
18
19/**
20 * @param {RawFormats} formats
21 * @param {{context: any, candidate: string, base: string | null}} options
22 * @returns {ParsedFormats | null}
23 */
24export function formatVariantSelector(formats, { context, candidate }) {
25 let prefix = context?.tailwindConfig.prefix ?? ''
26
27 // Parse the format selector into an AST
28 let parsedFormats = formats.map((format) => {
29 let ast = selectorParser().astSync(format.format)
30
31 return {
32 ...format,
33 ast: format.respectPrefix ? prefixSelector(prefix, ast) : ast,
34 }
35 })
36
37 // We start with the candidate selector
38 let formatAst = selectorParser.root({
39 nodes: [
40 selectorParser.selector({
41 nodes: [selectorParser.className({ value: escapeClassName(candidate) })],
42 }),
43 ],
44 })
45
46 // And iteratively merge each format selector into the candidate selector
47 for (let { ast } of parsedFormats) {
48 // 1. Handle :merge() special pseudo-class
49 ;[formatAst, ast] = handleMergePseudo(formatAst, ast)
50
51 // 2. Merge the format selector into the current selector AST
52 ast.walkNesting((nesting) => nesting.replaceWith(...formatAst.nodes[0].nodes))
53
54 // 3. Keep going!
55 formatAst = ast
56 }
57
58 return formatAst
59}
60
61/**
62 * Given any node in a selector this gets the "simple" selector it's a part of
63 * A simple selector is just a list of nodes without any combinators
64 * Technically :is(), :not(), :has(), etc… can have combinators but those are nested
65 * inside the relevant node and won't be picked up so they're fine to ignore
66 *
67 * @param {Node} node
68 * @returns {Node[]}
69 **/
70function simpleSelectorForNode(node) {
71 /** @type {Node[]} */
72 let nodes = []
73
74 // Walk backwards until we hit a combinator node (or the start)
75 while (node.prev() && node.prev().type !== 'combinator') {
76 node = node.prev()
77 }
78
79 // Now record all non-combinator nodes until we hit one (or the end)
80 while (node && node.type !== 'combinator') {
81 nodes.push(node)
82 node = node.next()
83 }
84
85 return nodes
86}
87
88/**
89 * Resorts the nodes in a selector to ensure they're in the correct order
90 * Tags go before classes, and pseudo classes go after classes
91 *
92 * @param {Selector} sel
93 * @returns {Selector}
94 **/
95function resortSelector(sel) {
96 sel.sort((a, b) => {
97 if (a.type === 'tag' && b.type === 'class') {
98 return -1
99 } else if (a.type === 'class' && b.type === 'tag') {
100 return 1
101 } else if (a.type === 'class' && b.type === 'pseudo' && b.value.startsWith('::')) {
102 return -1
103 } else if (a.type === 'pseudo' && a.value.startsWith('::') && b.type === 'class') {
104 return 1
105 }
106
107 return sel.index(a) - sel.index(b)
108 })
109
110 return sel
111}
112
113/**
114 * Remove extraneous selectors that do not include the base class/candidate
115 *
116 * Example:
117 * Given the utility `.a, .b { color: red}`
118 * Given the candidate `sm:b`
119 *
120 * The final selector should be `.sm\:b` and not `.a, .sm\:b`
121 *
122 * @param {Selector} ast
123 * @param {string} base
124 */
125export function eliminateIrrelevantSelectors(sel, base) {
126 let hasClassesMatchingCandidate = false
127
128 sel.walk((child) => {
129 if (child.type === 'class' && child.value === base) {
130 hasClassesMatchingCandidate = true
131 return false // Stop walking
132 }
133 })
134
135 if (!hasClassesMatchingCandidate) {
136 sel.remove()
137 }
138
139 // We do NOT recursively eliminate sub selectors that don't have the base class
140 // as this is NOT a safe operation. For example, if we have:
141 // `.space-x-2 > :not([hidden]) ~ :not([hidden])`
142 // We cannot remove the [hidden] from the :not() because it would change the
143 // meaning of the selector.
144
145 // TODO: Can we do this for :matches, :is, and :where?
146}
147
148/**
149 * @param {string} current
150 * @param {AcceptedFormats} formats
151 * @param {{context: any, candidate: string, base: string | null}} options
152 * @returns {string}
153 */
154export function finalizeSelector(current, formats, { context, candidate, base }) {
155 let separator = context?.tailwindConfig?.separator ?? ':'
156
157 // Split by the separator, but ignore the separator inside square brackets:
158 //
159 // E.g.: dark:lg:hover:[paint-order:markers]
160 // ┬ ┬ ┬ ┬
161 // │ │ │ ╰── We will not split here
162 // ╰──┴─────┴─────────────── We will split here
163 //
164 base = base ?? splitAtTopLevelOnly(candidate, separator).pop()
165
166 // Parse the selector into an AST
167 let selector = selectorParser().astSync(current)
168
169 // Normalize escaped classes, e.g.:
170 //
171 // The idea would be to replace the escaped `base` in the selector with the
172 // `format`. However, in css you can escape the same selector in a few
173 // different ways. This would result in different strings and therefore we
174 // can't replace it properly.
175 //
176 // base: bg-[rgb(255,0,0)]
177 // base in selector: bg-\\[rgb\\(255\\,0\\,0\\)\\]
178 // escaped base: bg-\\[rgb\\(255\\2c 0\\2c 0\\)\\]
179 //
180 selector.walkClasses((node) => {
181 if (node.raws && node.value.includes(base)) {
182 node.raws.value = escapeClassName(unescape(node.raws.value))
183 }
184 })
185
186 // Remove extraneous selectors that do not include the base candidate
187 selector.each((sel) => eliminateIrrelevantSelectors(sel, base))
188
189 // If ffter eliminating irrelevant selectors, we end up with nothing
190 // Then the whole "rule" this is associated with does not need to exist
191 // We use `null` as a marker value for that case
192 if (selector.length === 0) {
193 return null
194 }
195
196 // If there are no formats that means there were no variants added to the candidate
197 // so we can just return the selector as-is
198 let formatAst = Array.isArray(formats)
199 ? formatVariantSelector(formats, { context, candidate })
200 : formats
201
202 if (formatAst === null) {
203 return selector.toString()
204 }
205
206 let simpleStart = selectorParser.comment({ value: '/*__simple__*/' })
207 let simpleEnd = selectorParser.comment({ value: '/*__simple__*/' })
208
209 // We can safely replace the escaped base now, since the `base` section is
210 // now in a normalized escaped value.
211 selector.walkClasses((node) => {
212 if (node.value !== base) {
213 return
214 }
215
216 let parent = node.parent
217 let formatNodes = formatAst.nodes[0].nodes
218
219 // Perf optimization: if the parent is a single class we can just replace it and be done
220 if (parent.nodes.length === 1) {
221 node.replaceWith(...formatNodes)
222 return
223 }
224
225 let simpleSelector = simpleSelectorForNode(node)
226 parent.insertBefore(simpleSelector[0], simpleStart)
227 parent.insertAfter(simpleSelector[simpleSelector.length - 1], simpleEnd)
228
229 for (let child of formatNodes) {
230 parent.insertBefore(simpleSelector[0], child.clone())
231 }
232
233 node.remove()
234
235 // Re-sort the simple selector to ensure it's in the correct order
236 simpleSelector = simpleSelectorForNode(simpleStart)
237 let firstNode = parent.index(simpleStart)
238
239 parent.nodes.splice(
240 firstNode,
241 simpleSelector.length,
242 ...resortSelector(selectorParser.selector({ nodes: simpleSelector })).nodes
243 )
244
245 simpleStart.remove()
246 simpleEnd.remove()
247 })
248
249 // Remove unnecessary pseudo selectors that we used as placeholders
250 selector.walkPseudos((p) => {
251 if (p.value === MERGE) {
252 p.replaceWith(p.nodes)
253 }
254 })
255
256 // Move pseudo elements to the end of the selector (if necessary)
257 selector.each((sel) => movePseudos(sel))
258
259 return selector.toString()
260}
261
262/**
263 *
264 * @param {Selector} selector
265 * @param {Selector} format
266 */
267export function handleMergePseudo(selector, format) {
268 /** @type {{pseudo: Pseudo, value: string}[]} */
269 let merges = []
270
271 // Find all :merge() pseudo-classes in `selector`
272 selector.walkPseudos((pseudo) => {
273 if (pseudo.value === MERGE) {
274 merges.push({
275 pseudo,
276 value: pseudo.nodes[0].toString(),
277 })
278 }
279 })
280
281 // Find all :merge() "attachments" in `format` and attach them to the matching selector in `selector`
282 format.walkPseudos((pseudo) => {
283 if (pseudo.value !== MERGE) {
284 return
285 }
286
287 let value = pseudo.nodes[0].toString()
288
289 // Does `selector` contain a :merge() pseudo-class with the same value?
290 let existing = merges.find((merge) => merge.value === value)
291
292 // Nope so there's nothing to do
293 if (!existing) {
294 return
295 }
296
297 // Everything after `:merge()` up to the next combinator is what is attached to the merged selector
298 let attachments = []
299 let next = pseudo.next()
300 while (next && next.type !== 'combinator') {
301 attachments.push(next)
302 next = next.next()
303 }
304
305 let combinator = next
306
307 existing.pseudo.parent.insertAfter(
308 existing.pseudo,
309 selectorParser.selector({ nodes: attachments.map((node) => node.clone()) })
310 )
311
312 pseudo.remove()
313 attachments.forEach((node) => node.remove())
314
315 // What about this case:
316 // :merge(.group):focus > &
317 // :merge(.group):hover &
318 if (combinator && combinator.type === 'combinator') {
319 combinator.remove()
320 }
321 })
322
323 return [selector, format]
324}
Note: See TracBrowser for help on using the repository browser.