formatVariantSelector.js 11 KB

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