UNPKG

12.1 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", {
3 value: true
4});
5function _export(target, all) {
6 for(var name in all)Object.defineProperty(target, name, {
7 enumerable: true,
8 get: all[name]
9 });
10}
11_export(exports, {
12 selectorFunctions: ()=>selectorFunctions,
13 formatVariantSelector: ()=>formatVariantSelector,
14 finalizeSelector: ()=>finalizeSelector
15});
16const _postcssSelectorParser = /*#__PURE__*/ _interopRequireDefault(require("postcss-selector-parser"));
17const _unesc = /*#__PURE__*/ _interopRequireDefault(require("postcss-selector-parser/dist/util/unesc"));
18const _escapeClassName = /*#__PURE__*/ _interopRequireDefault(require("../util/escapeClassName"));
19const _prefixSelector = /*#__PURE__*/ _interopRequireDefault(require("../util/prefixSelector"));
20function _interopRequireDefault(obj) {
21 return obj && obj.__esModule ? obj : {
22 default: obj
23 };
24}
25var ref;
26let MERGE = ":merge";
27let PARENT = "&";
28let selectorFunctions = new Set([
29 MERGE
30]);
31function formatVariantSelector(current, ...others) {
32 for (let other of others){
33 let incomingValue = resolveFunctionArgument(other, MERGE);
34 if (incomingValue !== null) {
35 let existingValue = resolveFunctionArgument(current, MERGE, incomingValue);
36 if (existingValue !== null) {
37 let existingTarget = `${MERGE}(${incomingValue})`;
38 let splitIdx = other.indexOf(existingTarget);
39 let addition = other.slice(splitIdx + existingTarget.length).split(" ")[0];
40 current = current.replace(existingTarget, existingTarget + addition);
41 continue;
42 }
43 }
44 current = other.replace(PARENT, current);
45 }
46 return current;
47}
48/**
49 * Given any node in a selector this gets the "simple" selector it's a part of
50 * A simple selector is just a list of nodes without any combinators
51 * Technically :is(), :not(), :has(), etc… can have combinators but those are nested
52 * inside the relevant node and won't be picked up so they're fine to ignore
53 *
54 * @param {import('postcss-selector-parser').Node} node
55 * @returns {import('postcss-selector-parser').Node[]}
56 **/ function simpleSelectorForNode(node) {
57 /** @type {import('postcss-selector-parser').Node[]} */ let nodes = [];
58 // Walk backwards until we hit a combinator node (or the start)
59 while(node.prev() && node.prev().type !== "combinator"){
60 node = node.prev();
61 }
62 // Now record all non-combinator nodes until we hit one (or the end)
63 while(node && node.type !== "combinator"){
64 nodes.push(node);
65 node = node.next();
66 }
67 return nodes;
68}
69/**
70 * Resorts the nodes in a selector to ensure they're in the correct order
71 * Tags go before classes, and pseudo classes go after classes
72 *
73 * @param {import('postcss-selector-parser').Selector} sel
74 * @returns {import('postcss-selector-parser').Selector}
75 **/ function resortSelector(sel) {
76 sel.sort((a, b)=>{
77 if (a.type === "tag" && b.type === "class") {
78 return -1;
79 } else if (a.type === "class" && b.type === "tag") {
80 return 1;
81 } else if (a.type === "class" && b.type === "pseudo" && b.value.startsWith("::")) {
82 return -1;
83 } else if (a.type === "pseudo" && a.value.startsWith("::") && b.type === "class") {
84 return 1;
85 }
86 return sel.index(a) - sel.index(b);
87 });
88 return sel;
89}
90function eliminateIrrelevantSelectors(sel, base) {
91 let hasClassesMatchingCandidate = false;
92 sel.walk((child)=>{
93 if (child.type === "class" && child.value === base) {
94 hasClassesMatchingCandidate = true;
95 return false // Stop walking
96 ;
97 }
98 });
99 if (!hasClassesMatchingCandidate) {
100 sel.remove();
101 }
102// We do NOT recursively eliminate sub selectors that don't have the base class
103// as this is NOT a safe operation. For example, if we have:
104// `.space-x-2 > :not([hidden]) ~ :not([hidden])`
105// We cannot remove the [hidden] from the :not() because it would change the
106// meaning of the selector.
107// TODO: Can we do this for :matches, :is, and :where?
108}
109var ref1;
110function finalizeSelector(format, { selector , candidate , context , isArbitraryVariant , // Split by the separator, but ignore the separator inside square brackets:
111//
112// E.g.: dark:lg:hover:[paint-order:markers]
113// ┬ ┬ ┬ ┬
114// │ │ │ ╰── We will not split here
115// ╰──┴─────┴─────────────── We will split here
116//
117base =candidate.split(new RegExp(`\\${(ref1 = context === null || context === void 0 ? void 0 : (ref = context.tailwindConfig) === null || ref === void 0 ? void 0 : ref.separator) !== null && ref1 !== void 0 ? ref1 : ":"}(?![^[]*\\])`)).pop() }) {
118 var ref2;
119 let ast = (0, _postcssSelectorParser.default)().astSync(selector);
120 // We explicitly DO NOT prefix classes in arbitrary variants
121 if ((context === null || context === void 0 ? void 0 : (ref2 = context.tailwindConfig) === null || ref2 === void 0 ? void 0 : ref2.prefix) && !isArbitraryVariant) {
122 format = (0, _prefixSelector.default)(context.tailwindConfig.prefix, format);
123 }
124 format = format.replace(PARENT, `.${(0, _escapeClassName.default)(candidate)}`);
125 let formatAst = (0, _postcssSelectorParser.default)().astSync(format);
126 // Remove extraneous selectors that do not include the base class/candidate being matched against
127 // For example if we have a utility defined `.a, .b { color: red}`
128 // And the formatted variant is sm:b then we want the final selector to be `.sm\:b` and not `.a, .sm\:b`
129 ast.each((sel)=>eliminateIrrelevantSelectors(sel, base));
130 // Normalize escaped classes, e.g.:
131 //
132 // The idea would be to replace the escaped `base` in the selector with the
133 // `format`. However, in css you can escape the same selector in a few
134 // different ways. This would result in different strings and therefore we
135 // can't replace it properly.
136 //
137 // base: bg-[rgb(255,0,0)]
138 // base in selector: bg-\\[rgb\\(255\\,0\\,0\\)\\]
139 // escaped base: bg-\\[rgb\\(255\\2c 0\\2c 0\\)\\]
140 //
141 ast.walkClasses((node)=>{
142 if (node.raws && node.value.includes(base)) {
143 node.raws.value = (0, _escapeClassName.default)((0, _unesc.default)(node.raws.value));
144 }
145 });
146 let simpleStart = _postcssSelectorParser.default.comment({
147 value: "/*__simple__*/"
148 });
149 let simpleEnd = _postcssSelectorParser.default.comment({
150 value: "/*__simple__*/"
151 });
152 // We can safely replace the escaped base now, since the `base` section is
153 // now in a normalized escaped value.
154 ast.walkClasses((node)=>{
155 if (node.value !== base) {
156 return;
157 }
158 let parent = node.parent;
159 let formatNodes = formatAst.nodes[0].nodes;
160 // Perf optimization: if the parent is a single class we can just replace it and be done
161 if (parent.nodes.length === 1) {
162 node.replaceWith(...formatNodes);
163 return;
164 }
165 let simpleSelector = simpleSelectorForNode(node);
166 parent.insertBefore(simpleSelector[0], simpleStart);
167 parent.insertAfter(simpleSelector[simpleSelector.length - 1], simpleEnd);
168 for (let child of formatNodes){
169 parent.insertBefore(simpleSelector[0], child);
170 }
171 node.remove();
172 // Re-sort the simple selector to ensure it's in the correct order
173 simpleSelector = simpleSelectorForNode(simpleStart);
174 let firstNode = parent.index(simpleStart);
175 parent.nodes.splice(firstNode, simpleSelector.length, ...resortSelector(_postcssSelectorParser.default.selector({
176 nodes: simpleSelector
177 })).nodes);
178 simpleStart.remove();
179 simpleEnd.remove();
180 });
181 // This will make sure to move pseudo's to the correct spot (the end for
182 // pseudo elements) because otherwise the selector will never work
183 // anyway.
184 //
185 // E.g.:
186 // - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
187 // - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
188 //
189 // `::before:hover` doesn't work, which means that we can make it work for you by flipping the order.
190 function collectPseudoElements(selector) {
191 let nodes = [];
192 for (let node of selector.nodes){
193 if (isPseudoElement(node)) {
194 nodes.push(node);
195 selector.removeChild(node);
196 }
197 if (node === null || node === void 0 ? void 0 : node.nodes) {
198 nodes.push(...collectPseudoElements(node));
199 }
200 }
201 return nodes;
202 }
203 // Remove unnecessary pseudo selectors that we used as placeholders
204 ast.each((selector)=>{
205 selector.walkPseudos((p)=>{
206 if (selectorFunctions.has(p.value)) {
207 p.replaceWith(p.nodes);
208 }
209 });
210 let pseudoElements = collectPseudoElements(selector);
211 if (pseudoElements.length > 0) {
212 selector.nodes.push(pseudoElements.sort(sortSelector));
213 }
214 });
215 return ast.toString();
216}
217// Note: As a rule, double colons (::) should be used instead of a single colon
218// (:). This distinguishes pseudo-classes from pseudo-elements. However, since
219// this distinction was not present in older versions of the W3C spec, most
220// browsers support both syntaxes for the original pseudo-elements.
221let pseudoElementsBC = [
222 ":before",
223 ":after",
224 ":first-line",
225 ":first-letter"
226];
227// These pseudo-elements _can_ be combined with other pseudo selectors AND the order does matter.
228let pseudoElementExceptions = [
229 "::file-selector-button"
230];
231// This will make sure to move pseudo's to the correct spot (the end for
232// pseudo elements) because otherwise the selector will never work
233// anyway.
234//
235// E.g.:
236// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
237// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
238//
239// `::before:hover` doesn't work, which means that we can make it work
240// for you by flipping the order.
241function sortSelector(a, z) {
242 // Both nodes are non-pseudo's so we can safely ignore them and keep
243 // them in the same order.
244 if (a.type !== "pseudo" && z.type !== "pseudo") {
245 return 0;
246 }
247 // If one of them is a combinator, we need to keep it in the same order
248 // because that means it will start a new "section" in the selector.
249 if (a.type === "combinator" ^ z.type === "combinator") {
250 return 0;
251 }
252 // One of the items is a pseudo and the other one isn't. Let's move
253 // the pseudo to the right.
254 if (a.type === "pseudo" ^ z.type === "pseudo") {
255 return (a.type === "pseudo") - (z.type === "pseudo");
256 }
257 // Both are pseudo's, move the pseudo elements (except for
258 // ::file-selector-button) to the right.
259 return isPseudoElement(a) - isPseudoElement(z);
260}
261function isPseudoElement(node) {
262 if (node.type !== "pseudo") return false;
263 if (pseudoElementExceptions.includes(node.value)) return false;
264 return node.value.startsWith("::") || pseudoElementsBC.includes(node.value);
265}
266function resolveFunctionArgument(haystack, needle, arg) {
267 let startIdx = haystack.indexOf(arg ? `${needle}(${arg})` : needle);
268 if (startIdx === -1) return null;
269 // Start inside the `(`
270 startIdx += needle.length + 1;
271 let target = "";
272 let count = 0;
273 for (let char of haystack.slice(startIdx)){
274 if (char !== "(" && char !== ")") {
275 target += char;
276 } else if (char === "(") {
277 target += char;
278 count++;
279 } else if (char === ")") {
280 if (--count < 0) break; // unbalanced
281 target += char;
282 }
283 }
284 return target;
285}