index.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. const { AtRule, Rule } = require('postcss')
  2. let parser = require('postcss-selector-parser')
  3. /**
  4. * Run a selector string through postcss-selector-parser
  5. */
  6. function parse(rawSelector, rule) {
  7. let nodes
  8. try {
  9. parser(parsed => {
  10. nodes = parsed
  11. }).processSync(rawSelector)
  12. } catch (e) {
  13. if (rawSelector.includes(':')) {
  14. throw rule ? rule.error('Missed semicolon') : e
  15. } else {
  16. throw rule ? rule.error(e.message) : e
  17. }
  18. }
  19. return nodes.at(0)
  20. }
  21. /**
  22. * Replaces the "&" token in a node's selector with the parent selector
  23. * similar to what SCSS does.
  24. *
  25. * Mutates the nodes list
  26. */
  27. function interpolateAmpInSelector(nodes, parent) {
  28. let replaced = false
  29. nodes.each(node => {
  30. if (node.type === 'nesting') {
  31. let clonedParent = parent.clone({})
  32. if (node.value !== '&') {
  33. node.replaceWith(
  34. parse(node.value.replace('&', clonedParent.toString()))
  35. )
  36. } else {
  37. node.replaceWith(clonedParent)
  38. }
  39. replaced = true
  40. } else if ('nodes' in node && node.nodes) {
  41. if (interpolateAmpInSelector(node, parent)) {
  42. replaced = true
  43. }
  44. }
  45. })
  46. return replaced
  47. }
  48. /**
  49. * Combines parent and child selectors, in a SCSS-like way
  50. */
  51. function mergeSelectors(parent, child) {
  52. let merged = []
  53. parent.selectors.forEach(sel => {
  54. let parentNode = parse(sel, parent)
  55. child.selectors.forEach(selector => {
  56. if (!selector) {
  57. return
  58. }
  59. let node = parse(selector, child)
  60. let replaced = interpolateAmpInSelector(node, parentNode)
  61. if (!replaced) {
  62. node.prepend(parser.combinator({ value: ' ' }))
  63. node.prepend(parentNode.clone({}))
  64. }
  65. merged.push(node.toString())
  66. })
  67. })
  68. return merged
  69. }
  70. /**
  71. * Move a child and its preceeding comment(s) to after "after"
  72. */
  73. function breakOut(child, after) {
  74. let prev = child.prev()
  75. after.after(child)
  76. while (prev && prev.type === 'comment') {
  77. let nextPrev = prev.prev()
  78. after.after(prev)
  79. prev = nextPrev
  80. }
  81. return child
  82. }
  83. function createFnAtruleChilds(bubble) {
  84. return function atruleChilds(rule, atrule, bubbling, mergeSels = bubbling) {
  85. let children = []
  86. atrule.each(child => {
  87. if (child.type === 'rule' && bubbling) {
  88. if (mergeSels) {
  89. child.selectors = mergeSelectors(rule, child)
  90. }
  91. } else if (child.type === 'atrule' && child.nodes) {
  92. if (bubble[child.name]) {
  93. atruleChilds(rule, child, mergeSels)
  94. } else if (atrule[rootRuleMergeSel] !== false) {
  95. children.push(child)
  96. }
  97. } else {
  98. children.push(child)
  99. }
  100. })
  101. if (bubbling) {
  102. if (children.length) {
  103. let clone = rule.clone({ nodes: [] })
  104. for (let child of children) {
  105. clone.append(child)
  106. }
  107. atrule.prepend(clone)
  108. }
  109. }
  110. }
  111. }
  112. function pickDeclarations(selector, declarations, after) {
  113. let parent = new Rule({
  114. nodes: [],
  115. selector
  116. })
  117. parent.append(declarations)
  118. after.after(parent)
  119. return parent
  120. }
  121. function atruleNames(defaults, custom) {
  122. let list = {}
  123. for (let name of defaults) {
  124. list[name] = true
  125. }
  126. if (custom) {
  127. for (let name of custom) {
  128. list[name.replace(/^@/, '')] = true
  129. }
  130. }
  131. return list
  132. }
  133. function parseRootRuleParams(params) {
  134. params = params.trim()
  135. let braceBlock = params.match(/^\((.*)\)$/)
  136. if (!braceBlock) {
  137. return { selector: params, type: 'basic' }
  138. }
  139. let bits = braceBlock[1].match(/^(with(?:out)?):(.+)$/)
  140. if (bits) {
  141. let allowlist = bits[1] === 'with'
  142. let rules = Object.fromEntries(
  143. bits[2]
  144. .trim()
  145. .split(/\s+/)
  146. .map(name => [name, true])
  147. )
  148. if (allowlist && rules.all) {
  149. return { type: 'noop' }
  150. }
  151. let escapes = rule => !!rules[rule]
  152. if (rules.all) {
  153. escapes = () => true
  154. } else if (allowlist) {
  155. escapes = rule => (rule === 'all' ? false : !rules[rule])
  156. }
  157. return {
  158. escapes,
  159. type: 'withrules'
  160. }
  161. }
  162. // Unrecognized brace block
  163. return { type: 'unknown' }
  164. }
  165. function getAncestorRules(leaf) {
  166. let lineage = []
  167. let parent = leaf.parent
  168. while (parent && parent instanceof AtRule) {
  169. lineage.push(parent)
  170. parent = parent.parent
  171. }
  172. return lineage
  173. }
  174. function unwrapRootRule(rule) {
  175. let escapes = rule[rootRuleEscapes]
  176. if (!escapes) {
  177. rule.after(rule.nodes)
  178. } else {
  179. let nodes = rule.nodes
  180. let topEscaped
  181. let topEscapedIdx = -1
  182. let breakoutLeaf
  183. let breakoutRoot
  184. let clone
  185. let lineage = getAncestorRules(rule)
  186. lineage.forEach((parent, i) => {
  187. if (escapes(parent.name)) {
  188. topEscaped = parent
  189. topEscapedIdx = i
  190. breakoutRoot = clone
  191. } else {
  192. let oldClone = clone
  193. clone = parent.clone({ nodes: [] })
  194. oldClone && clone.append(oldClone)
  195. breakoutLeaf = breakoutLeaf || clone
  196. }
  197. })
  198. if (!topEscaped) {
  199. rule.after(nodes)
  200. } else if (!breakoutRoot) {
  201. topEscaped.after(nodes)
  202. } else {
  203. let leaf = breakoutLeaf
  204. leaf.append(nodes)
  205. topEscaped.after(breakoutRoot)
  206. }
  207. if (rule.next() && topEscaped) {
  208. let restRoot
  209. lineage.slice(0, topEscapedIdx + 1).forEach((parent, i, arr) => {
  210. let oldRoot = restRoot
  211. restRoot = parent.clone({ nodes: [] })
  212. oldRoot && restRoot.append(oldRoot)
  213. let nextSibs = []
  214. let _child = arr[i - 1] || rule
  215. let next = _child.next()
  216. while (next) {
  217. nextSibs.push(next)
  218. next = next.next()
  219. }
  220. restRoot.append(nextSibs)
  221. })
  222. restRoot && (breakoutRoot || nodes[nodes.length - 1]).after(restRoot)
  223. }
  224. }
  225. rule.remove()
  226. }
  227. const rootRuleMergeSel = Symbol('rootRuleMergeSel')
  228. const rootRuleEscapes = Symbol('rootRuleEscapes')
  229. function normalizeRootRule(rule) {
  230. let { params } = rule
  231. let { escapes, selector, type } = parseRootRuleParams(params)
  232. if (type === 'unknown') {
  233. throw rule.error(
  234. `Unknown @${rule.name} parameter ${JSON.stringify(params)}`
  235. )
  236. }
  237. if (type === 'basic' && selector) {
  238. let selectorBlock = new Rule({ nodes: rule.nodes, selector })
  239. rule.removeAll()
  240. rule.append(selectorBlock)
  241. }
  242. rule[rootRuleEscapes] = escapes
  243. rule[rootRuleMergeSel] = escapes ? !escapes('all') : type === 'noop'
  244. }
  245. const hasRootRule = Symbol('hasRootRule')
  246. module.exports = (opts = {}) => {
  247. let bubble = atruleNames(
  248. ['media', 'supports', 'layer', 'container', 'starting-style'],
  249. opts.bubble
  250. )
  251. let atruleChilds = createFnAtruleChilds(bubble)
  252. let unwrap = atruleNames(
  253. [
  254. 'document',
  255. 'font-face',
  256. 'keyframes',
  257. '-webkit-keyframes',
  258. '-moz-keyframes'
  259. ],
  260. opts.unwrap
  261. )
  262. let rootRuleName = (opts.rootRuleName || 'at-root').replace(/^@/, '')
  263. let preserveEmpty = opts.preserveEmpty
  264. return {
  265. Once(root) {
  266. root.walkAtRules(rootRuleName, node => {
  267. normalizeRootRule(node)
  268. root[hasRootRule] = true
  269. })
  270. },
  271. postcssPlugin: 'postcss-nested',
  272. RootExit(root) {
  273. if (root[hasRootRule]) {
  274. root.walkAtRules(rootRuleName, unwrapRootRule)
  275. root[hasRootRule] = false
  276. }
  277. },
  278. Rule(rule) {
  279. let unwrapped = false
  280. let after = rule
  281. let copyDeclarations = false
  282. let declarations = []
  283. rule.each(child => {
  284. if (child.type === 'rule') {
  285. if (declarations.length) {
  286. after = pickDeclarations(rule.selector, declarations, after)
  287. declarations = []
  288. }
  289. copyDeclarations = true
  290. unwrapped = true
  291. child.selectors = mergeSelectors(rule, child)
  292. after = breakOut(child, after)
  293. } else if (child.type === 'atrule') {
  294. if (declarations.length) {
  295. after = pickDeclarations(rule.selector, declarations, after)
  296. declarations = []
  297. }
  298. if (child.name === rootRuleName) {
  299. unwrapped = true
  300. atruleChilds(rule, child, true, child[rootRuleMergeSel])
  301. after = breakOut(child, after)
  302. } else if (bubble[child.name]) {
  303. copyDeclarations = true
  304. unwrapped = true
  305. atruleChilds(rule, child, true)
  306. after = breakOut(child, after)
  307. } else if (unwrap[child.name]) {
  308. copyDeclarations = true
  309. unwrapped = true
  310. atruleChilds(rule, child, false)
  311. after = breakOut(child, after)
  312. } else if (copyDeclarations) {
  313. declarations.push(child)
  314. }
  315. } else if (child.type === 'decl' && copyDeclarations) {
  316. declarations.push(child)
  317. }
  318. })
  319. if (declarations.length) {
  320. after = pickDeclarations(rule.selector, declarations, after)
  321. }
  322. if (unwrapped && preserveEmpty !== true) {
  323. rule.raws.semicolon = true
  324. if (rule.nodes.length === 0) rule.remove()
  325. }
  326. }
  327. }
  328. }
  329. module.exports.postcss = true