formatVariantSelector.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. function _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. formatVariantSelector: ()=>formatVariantSelector,
  13. eliminateIrrelevantSelectors: ()=>eliminateIrrelevantSelectors,
  14. finalizeSelector: ()=>finalizeSelector,
  15. handleMergePseudo: ()=>handleMergePseudo
  16. });
  17. const _postcssSelectorParser = /*#__PURE__*/ _interopRequireDefault(require("postcss-selector-parser"));
  18. const _unesc = /*#__PURE__*/ _interopRequireDefault(require("postcss-selector-parser/dist/util/unesc"));
  19. const _escapeClassName = /*#__PURE__*/ _interopRequireDefault(require("../util/escapeClassName"));
  20. const _prefixSelector = /*#__PURE__*/ _interopRequireDefault(require("../util/prefixSelector"));
  21. function _interopRequireDefault(obj) {
  22. return obj && obj.__esModule ? obj : {
  23. default: obj
  24. };
  25. }
  26. /** @typedef {import('postcss-selector-parser').Root} Root */ /** @typedef {import('postcss-selector-parser').Selector} Selector */ /** @typedef {import('postcss-selector-parser').Pseudo} Pseudo */ /** @typedef {import('postcss-selector-parser').Node} Node */ /** @typedef {{format: string, isArbitraryVariant: boolean}[]} RawFormats */ /** @typedef {import('postcss-selector-parser').Root} ParsedFormats */ /** @typedef {RawFormats | ParsedFormats} AcceptedFormats */ let MERGE = ":merge";
  27. function formatVariantSelector(formats, { context , candidate }) {
  28. var _context_tailwindConfig_prefix;
  29. let prefix = (_context_tailwindConfig_prefix = context === null || context === void 0 ? void 0 : context.tailwindConfig.prefix) !== null && _context_tailwindConfig_prefix !== void 0 ? _context_tailwindConfig_prefix : "";
  30. // Parse the format selector into an AST
  31. let parsedFormats = formats.map((format)=>{
  32. let ast = (0, _postcssSelectorParser.default)().astSync(format.format);
  33. return {
  34. ...format,
  35. ast: format.isArbitraryVariant ? ast : (0, _prefixSelector.default)(prefix, ast)
  36. };
  37. });
  38. // We start with the candidate selector
  39. let formatAst = _postcssSelectorParser.default.root({
  40. nodes: [
  41. _postcssSelectorParser.default.selector({
  42. nodes: [
  43. _postcssSelectorParser.default.className({
  44. value: (0, _escapeClassName.default)(candidate)
  45. })
  46. ]
  47. })
  48. ]
  49. });
  50. // And iteratively merge each format selector into the candidate selector
  51. for (let { ast } of parsedFormats){
  52. [formatAst, ast] = handleMergePseudo(formatAst, ast);
  53. // 2. Merge the format selector into the current selector AST
  54. ast.walkNesting((nesting)=>nesting.replaceWith(...formatAst.nodes[0].nodes));
  55. // 3. Keep going!
  56. formatAst = ast;
  57. }
  58. return formatAst;
  59. }
  60. /**
  61. * Given any node in a selector this gets the "simple" selector it's a part of
  62. * A simple selector is just a list of nodes without any combinators
  63. * Technically :is(), :not(), :has(), etc… can have combinators but those are nested
  64. * inside the relevant node and won't be picked up so they're fine to ignore
  65. *
  66. * @param {Node} node
  67. * @returns {Node[]}
  68. **/ function simpleSelectorForNode(node) {
  69. /** @type {Node[]} */ let nodes = [];
  70. // Walk backwards until we hit a combinator node (or the start)
  71. while(node.prev() && node.prev().type !== "combinator"){
  72. node = node.prev();
  73. }
  74. // Now record all non-combinator nodes until we hit one (or the end)
  75. while(node && node.type !== "combinator"){
  76. nodes.push(node);
  77. node = node.next();
  78. }
  79. return nodes;
  80. }
  81. /**
  82. * Resorts the nodes in a selector to ensure they're in the correct order
  83. * Tags go before classes, and pseudo classes go after classes
  84. *
  85. * @param {Selector} sel
  86. * @returns {Selector}
  87. **/ function resortSelector(sel) {
  88. sel.sort((a, b)=>{
  89. if (a.type === "tag" && b.type === "class") {
  90. return -1;
  91. } else if (a.type === "class" && b.type === "tag") {
  92. return 1;
  93. } else if (a.type === "class" && b.type === "pseudo" && b.value.startsWith("::")) {
  94. return -1;
  95. } else if (a.type === "pseudo" && a.value.startsWith("::") && b.type === "class") {
  96. return 1;
  97. }
  98. return sel.index(a) - sel.index(b);
  99. });
  100. return sel;
  101. }
  102. function eliminateIrrelevantSelectors(sel, base) {
  103. let hasClassesMatchingCandidate = false;
  104. sel.walk((child)=>{
  105. if (child.type === "class" && child.value === base) {
  106. hasClassesMatchingCandidate = true;
  107. return false // Stop walking
  108. ;
  109. }
  110. });
  111. if (!hasClassesMatchingCandidate) {
  112. sel.remove();
  113. }
  114. // We do NOT recursively eliminate sub selectors that don't have the base class
  115. // as this is NOT a safe operation. For example, if we have:
  116. // `.space-x-2 > :not([hidden]) ~ :not([hidden])`
  117. // We cannot remove the [hidden] from the :not() because it would change the
  118. // meaning of the selector.
  119. // TODO: Can we do this for :matches, :is, and :where?
  120. }
  121. function finalizeSelector(current, formats, { context , candidate , base }) {
  122. var _context_tailwindConfig;
  123. var _context_tailwindConfig_separator;
  124. let separator = (_context_tailwindConfig_separator = context === null || context === void 0 ? void 0 : (_context_tailwindConfig = context.tailwindConfig) === null || _context_tailwindConfig === void 0 ? void 0 : _context_tailwindConfig.separator) !== null && _context_tailwindConfig_separator !== void 0 ? _context_tailwindConfig_separator : ":";
  125. // Split by the separator, but ignore the separator inside square brackets:
  126. //
  127. // E.g.: dark:lg:hover:[paint-order:markers]
  128. // ┬ ┬ ┬ ┬
  129. // │ │ │ ╰── We will not split here
  130. // ╰──┴─────┴─────────────── We will split here
  131. //
  132. base = base !== null && base !== void 0 ? base : candidate.split(new RegExp(`\\${separator}(?![^[]*\\])`)).pop();
  133. // Parse the selector into an AST
  134. let selector = (0, _postcssSelectorParser.default)().astSync(current);
  135. // Normalize escaped classes, e.g.:
  136. //
  137. // The idea would be to replace the escaped `base` in the selector with the
  138. // `format`. However, in css you can escape the same selector in a few
  139. // different ways. This would result in different strings and therefore we
  140. // can't replace it properly.
  141. //
  142. // base: bg-[rgb(255,0,0)]
  143. // base in selector: bg-\\[rgb\\(255\\,0\\,0\\)\\]
  144. // escaped base: bg-\\[rgb\\(255\\2c 0\\2c 0\\)\\]
  145. //
  146. selector.walkClasses((node)=>{
  147. if (node.raws && node.value.includes(base)) {
  148. node.raws.value = (0, _escapeClassName.default)((0, _unesc.default)(node.raws.value));
  149. }
  150. });
  151. // Remove extraneous selectors that do not include the base candidate
  152. selector.each((sel)=>eliminateIrrelevantSelectors(sel, base));
  153. // If there are no formats that means there were no variants added to the candidate
  154. // so we can just return the selector as-is
  155. let formatAst = Array.isArray(formats) ? formatVariantSelector(formats, {
  156. context,
  157. candidate
  158. }) : formats;
  159. if (formatAst === null) {
  160. return selector.toString();
  161. }
  162. let simpleStart = _postcssSelectorParser.default.comment({
  163. value: "/*__simple__*/"
  164. });
  165. let simpleEnd = _postcssSelectorParser.default.comment({
  166. value: "/*__simple__*/"
  167. });
  168. // We can safely replace the escaped base now, since the `base` section is
  169. // now in a normalized escaped value.
  170. selector.walkClasses((node)=>{
  171. if (node.value !== base) {
  172. return;
  173. }
  174. let parent = node.parent;
  175. let formatNodes = formatAst.nodes[0].nodes;
  176. // Perf optimization: if the parent is a single class we can just replace it and be done
  177. if (parent.nodes.length === 1) {
  178. node.replaceWith(...formatNodes);
  179. return;
  180. }
  181. let simpleSelector = simpleSelectorForNode(node);
  182. parent.insertBefore(simpleSelector[0], simpleStart);
  183. parent.insertAfter(simpleSelector[simpleSelector.length - 1], simpleEnd);
  184. for (let child of formatNodes){
  185. parent.insertBefore(simpleSelector[0], child.clone());
  186. }
  187. node.remove();
  188. // Re-sort the simple selector to ensure it's in the correct order
  189. simpleSelector = simpleSelectorForNode(simpleStart);
  190. let firstNode = parent.index(simpleStart);
  191. parent.nodes.splice(firstNode, simpleSelector.length, ...resortSelector(_postcssSelectorParser.default.selector({
  192. nodes: simpleSelector
  193. })).nodes);
  194. simpleStart.remove();
  195. simpleEnd.remove();
  196. });
  197. // Remove unnecessary pseudo selectors that we used as placeholders
  198. selector.walkPseudos((p)=>{
  199. if (p.value === MERGE) {
  200. p.replaceWith(p.nodes);
  201. }
  202. });
  203. // Move pseudo elements to the end of the selector (if necessary)
  204. selector.each((sel)=>{
  205. let pseudoElements = collectPseudoElements(sel);
  206. if (pseudoElements.length > 0) {
  207. sel.nodes.push(pseudoElements.sort(sortSelector));
  208. }
  209. });
  210. return selector.toString();
  211. }
  212. function handleMergePseudo(selector, format) {
  213. /** @type {{pseudo: Pseudo, value: string}[]} */ let merges = [];
  214. // Find all :merge() pseudo-classes in `selector`
  215. selector.walkPseudos((pseudo)=>{
  216. if (pseudo.value === MERGE) {
  217. merges.push({
  218. pseudo,
  219. value: pseudo.nodes[0].toString()
  220. });
  221. }
  222. });
  223. // Find all :merge() "attachments" in `format` and attach them to the matching selector in `selector`
  224. format.walkPseudos((pseudo)=>{
  225. if (pseudo.value !== MERGE) {
  226. return;
  227. }
  228. let value = pseudo.nodes[0].toString();
  229. // Does `selector` contain a :merge() pseudo-class with the same value?
  230. let existing = merges.find((merge)=>merge.value === value);
  231. // Nope so there's nothing to do
  232. if (!existing) {
  233. return;
  234. }
  235. // Everything after `:merge()` up to the next combinator is what is attached to the merged selector
  236. let attachments = [];
  237. let next = pseudo.next();
  238. while(next && next.type !== "combinator"){
  239. attachments.push(next);
  240. next = next.next();
  241. }
  242. let combinator = next;
  243. existing.pseudo.parent.insertAfter(existing.pseudo, _postcssSelectorParser.default.selector({
  244. nodes: attachments.map((node)=>node.clone())
  245. }));
  246. pseudo.remove();
  247. attachments.forEach((node)=>node.remove());
  248. // What about this case:
  249. // :merge(.group):focus > &
  250. // :merge(.group):hover &
  251. if (combinator && combinator.type === "combinator") {
  252. combinator.remove();
  253. }
  254. });
  255. return [
  256. selector,
  257. format
  258. ];
  259. }
  260. // Note: As a rule, double colons (::) should be used instead of a single colon
  261. // (:). This distinguishes pseudo-classes from pseudo-elements. However, since
  262. // this distinction was not present in older versions of the W3C spec, most
  263. // browsers support both syntaxes for the original pseudo-elements.
  264. let pseudoElementsBC = [
  265. ":before",
  266. ":after",
  267. ":first-line",
  268. ":first-letter"
  269. ];
  270. // These pseudo-elements _can_ be combined with other pseudo selectors AND the order does matter.
  271. let pseudoElementExceptions = [
  272. "::file-selector-button",
  273. // Webkit scroll bar pseudo elements can be combined with user-action pseudo classes
  274. "::-webkit-scrollbar",
  275. "::-webkit-scrollbar-button",
  276. "::-webkit-scrollbar-thumb",
  277. "::-webkit-scrollbar-track",
  278. "::-webkit-scrollbar-track-piece",
  279. "::-webkit-scrollbar-corner",
  280. "::-webkit-resizer"
  281. ];
  282. /**
  283. * This will make sure to move pseudo's to the correct spot (the end for
  284. * pseudo elements) because otherwise the selector will never work
  285. * anyway.
  286. *
  287. * E.g.:
  288. * - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
  289. * - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
  290. *
  291. * `::before:hover` doesn't work, which means that we can make it work for you by flipping the order.
  292. *
  293. * @param {Selector} selector
  294. **/ function collectPseudoElements(selector) {
  295. /** @type {Node[]} */ let nodes = [];
  296. for (let node of selector.nodes){
  297. if (isPseudoElement(node)) {
  298. nodes.push(node);
  299. selector.removeChild(node);
  300. }
  301. if (node === null || node === void 0 ? void 0 : node.nodes) {
  302. nodes.push(...collectPseudoElements(node));
  303. }
  304. }
  305. return nodes;
  306. }
  307. // This will make sure to move pseudo's to the correct spot (the end for
  308. // pseudo elements) because otherwise the selector will never work
  309. // anyway.
  310. //
  311. // E.g.:
  312. // - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
  313. // - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
  314. //
  315. // `::before:hover` doesn't work, which means that we can make it work
  316. // for you by flipping the order.
  317. function sortSelector(a, z) {
  318. // Both nodes are non-pseudo's so we can safely ignore them and keep
  319. // them in the same order.
  320. if (a.type !== "pseudo" && z.type !== "pseudo") {
  321. return 0;
  322. }
  323. // If one of them is a combinator, we need to keep it in the same order
  324. // because that means it will start a new "section" in the selector.
  325. if (a.type === "combinator" ^ z.type === "combinator") {
  326. return 0;
  327. }
  328. // One of the items is a pseudo and the other one isn't. Let's move
  329. // the pseudo to the right.
  330. if (a.type === "pseudo" ^ z.type === "pseudo") {
  331. return (a.type === "pseudo") - (z.type === "pseudo");
  332. }
  333. // Both are pseudo's, move the pseudo elements (except for
  334. // ::file-selector-button) to the right.
  335. return isPseudoElement(a) - isPseudoElement(z);
  336. }
  337. function isPseudoElement(node) {
  338. if (node.type !== "pseudo") return false;
  339. if (pseudoElementExceptions.includes(node.value)) return false;
  340. return node.value.startsWith("::") || pseudoElementsBC.includes(node.value);
  341. }