11// @ts -check
22let parser = require ( 'postcss-selector-parser' )
33
4+ /** @typedef {import('postcss').Container } Container */
45/** @typedef {import('postcss').ChildNode } ChildNode */
56/** @typedef {import('postcss').Comment } Comment */
67/** @typedef {import('postcss').Declaration } Declaration */
@@ -127,15 +128,18 @@ function createFnAtruleChilds (/** @type {RuleMap} */ bubble) {
127128 * @param {PostcssRule } rule
128129 * @param {AtRule } atrule
129130 * @param {boolean } bubbling
131+ * @param {boolean } [mergeSels]
130132 */
131- return function atruleChilds ( rule , atrule , bubbling ) {
133+ return function atruleChilds ( rule , atrule , bubbling , mergeSels = bubbling ) {
132134 /** @type {Array<ChildNode> } */
133135 let children = [ ]
134136 atrule . each ( child => {
135137 if ( child . type === 'rule' && bubbling ) {
136- child . selectors = mergeSelectors ( rule , child )
138+ if ( mergeSels ) {
139+ child . selectors = mergeSelectors ( rule , child )
140+ }
137141 } else if ( child . type === 'atrule' && child . nodes && bubble [ child . name ] ) {
138- atruleChilds ( rule , child , true )
142+ atruleChilds ( rule , child , mergeSels )
139143 } else {
140144 children . push ( child )
141145 }
@@ -186,6 +190,165 @@ function atruleNames (defaults, custom) {
186190 return list
187191}
188192
193+ /** @typedef {{ type: 'basic', selector?: string, escapeRule?: never } } AtRootBParams */
194+ /** @typedef {{ type: 'withrules', escapeRule: (rule: string) => boolean, selector?: never } } AtRootWParams */
195+ /** @typedef {{ type: 'unknown', selector?: never, escapeRule?: never } } AtRootUParams */
196+ /** @typedef {{ type: 'noop', selector?: never, escapeRule?: never } } AtRootNParams */
197+ /** @typedef {AtRootBParams | AtRootWParams | AtRootNParams | AtRootUParams } AtRootParams */
198+
199+ /** @type {(params: string) => AtRootParams } */
200+ function parseAtRootParams ( params ) {
201+ params = params . trim ( )
202+ let braceBlock = params . match ( / ^ \( ( .* ) \) $ / )
203+ if ( ! braceBlock ) {
204+ return { type : 'basic' , selector : params }
205+ }
206+ let bits = braceBlock [ 1 ] . match ( / ^ ( w i t h (?: o u t ) ? ) : ( .+ ) $ / )
207+ if ( bits ) {
208+ let allowlist = bits [ 1 ] === 'with'
209+ /** @type {RuleMap } */
210+ let rules = Object . fromEntries (
211+ bits [ 2 ]
212+ . trim ( )
213+ . split ( / \s + / )
214+ . map ( name => [ name , true ] )
215+ )
216+ if ( allowlist && rules . all ) {
217+ return { type : 'noop' }
218+ }
219+ return {
220+ type : 'withrules' ,
221+ escapeRule : rules . all
222+ ? ( ) => true
223+ : allowlist
224+ ? rule => rule === 'all' ? false : ! rules [ rule ]
225+ : rule => ! ! rules [ rule ]
226+ }
227+ }
228+ // Unrecognized brace block
229+ return { type : 'unknown' }
230+ }
231+
232+ /**
233+ * @param {AtRule } leaf
234+ * @returns {Array<AtRule> }
235+ */
236+ function getAncestorRules ( leaf ) {
237+ /** @type {Array<AtRule> } */
238+ const lineage = [ ]
239+ /** @type {Container<ChildNode> | ChildNode | Document | undefined} */
240+ let parent
241+ parent = leaf . parent
242+
243+ while ( parent ) {
244+ if ( parent . type === 'atrule' ) {
245+ lineage . push ( /** @type {AtRule } */ ( parent ) )
246+ }
247+ parent = parent . parent
248+ }
249+ return lineage
250+ }
251+
252+
253+ /**
254+ * @param {AtRule } at_root
255+ */
256+ function handleAtRootWithRules ( at_root ) {
257+ const { type, escapeRule } = parseAtRootParams ( at_root . params )
258+ if ( type !== 'withrules' ) {
259+ throw at_root . error ( 'This rule should have been handled during first pass.' )
260+ }
261+
262+ const nodes = at_root . nodes
263+
264+ /** @type {AtRule | undefined } */
265+ let topEscaped
266+ let topEscapedIdx = - 1
267+ /** @type {AtRule | undefined } */
268+ let breakoutLeaf
269+ /** @type {AtRule | undefined } */
270+ let breakoutRoot
271+ /** @type {AtRule | undefined } */
272+ let clone
273+
274+ const lineage = getAncestorRules ( at_root )
275+ lineage . forEach ( ( parent , i ) => {
276+ if ( escapeRule ( parent . name ) ) {
277+ topEscaped = parent
278+ topEscapedIdx = i
279+ breakoutRoot = clone
280+ } else {
281+ const oldClone = clone
282+ clone = parent . clone ( { nodes : [ ] } )
283+ oldClone && clone . append ( oldClone )
284+ breakoutLeaf = breakoutLeaf || clone
285+ }
286+ } )
287+
288+ if ( ! topEscaped ) {
289+ at_root . after ( nodes )
290+ } else if ( ! breakoutRoot ) {
291+ topEscaped . after ( nodes )
292+ } else {
293+ const leaf = /** @type {AtRule } */ ( breakoutLeaf )
294+ leaf . append ( nodes )
295+ topEscaped . after ( breakoutRoot )
296+ }
297+
298+ if ( at_root . next ( ) && topEscaped ) {
299+ /** @type {AtRule | undefined } */
300+ let restRoot
301+ lineage . slice ( 0 , topEscapedIdx + 1 ) . forEach ( ( parent , i , arr ) => {
302+ const oldRoot = restRoot
303+ restRoot = parent . clone ( { nodes : [ ] } )
304+ oldRoot && restRoot . append ( oldRoot )
305+
306+ /** @type {Array<ChildNode>} */
307+ let nextSibs = [ ]
308+ let _child = arr [ i - 1 ] || at_root
309+ let next = _child . next ( )
310+ while ( next ) {
311+ nextSibs . push ( next )
312+ next = next . next ( )
313+ }
314+ restRoot . append ( nextSibs )
315+ } )
316+ restRoot && ( breakoutRoot || nodes [ nodes . length - 1 ] ) . after ( restRoot )
317+ }
318+
319+ at_root . remove ( )
320+ }
321+
322+ /**
323+ * @param {PostcssRule } rule
324+ * @param {AtRule } child
325+ * @param {ChildNode } after
326+ * @param {{ (rule: PostcssRule, atrule: AtRule, bubbling: boolean, mergeSels?: boolean): void; } } atruleChilds
327+ */
328+ function handleAtRoot ( rule , child , after , atruleChilds ) {
329+ let { nodes, params } = child
330+ const { type, selector, escapeRule } = parseAtRootParams ( params )
331+ if ( type === 'withrules' ) {
332+ atruleChilds ( rule , child , true , ! escapeRule ( 'all' ) )
333+ after = breakOut ( child , after )
334+ } else if ( type === 'unknown' ) {
335+ throw rule . error ( `Unknown @at-root parameter ${ JSON . stringify ( params ) } ` )
336+ } else {
337+ if ( selector ) {
338+ // nodes = [new Rule({ selector: selector, nodes })]
339+ nodes = [ rule . clone ( { selector, nodes } ) ]
340+ }
341+ atruleChilds ( rule , child , true , type === 'noop' )
342+ after . after ( nodes )
343+ after = nodes [ nodes . length - 1 ]
344+ child . remove ( )
345+ }
346+ return after
347+ }
348+
349+
350+ // ---------------------------------------------------------------------------
351+
189352/** @type {import('./').Nested } */
190353module . exports = ( opts = { } ) => {
191354 let bubble = atruleNames ( [ 'media' , 'supports' , 'layer' ] , opts . bubble )
@@ -202,8 +365,22 @@ module.exports = (opts = {}) => {
202365 )
203366 let preserveEmpty = opts . preserveEmpty
204367
368+ let hasRootRules = false
369+
205370 return {
206371 postcssPlugin : 'postcss-nested' ,
372+
373+ RootExit ( root , { } ) {
374+ if ( hasRootRules ) {
375+ root . walk ( ( node ) => {
376+ if ( node . type === 'atrule' && node . name === 'at-root' ) {
377+ handleAtRootWithRules ( node )
378+ }
379+ } )
380+ hasRootRules = false
381+ }
382+ } ,
383+
207384 Rule ( rule , { Rule } ) {
208385 let unwrapped = false
209386 /** @type {ChildNode } */
@@ -228,19 +405,10 @@ module.exports = (opts = {}) => {
228405 after = pickDeclarations ( rule . selector , declarations , after , Rule )
229406 declarations = [ ]
230407 }
231-
232408 if ( child . name === 'at-root' ) {
409+ hasRootRules = true
233410 unwrapped = true
234- atruleChilds ( rule , child , false )
235-
236- let nodes = child . nodes
237- if ( child . params ) {
238- nodes = [ new Rule ( { selector : child . params , nodes : child . nodes } ) ]
239- }
240-
241- after . after ( nodes )
242- after = nodes [ nodes . length - 1 ]
243- child . remove ( )
411+ after = handleAtRoot ( rule , child , after , atruleChilds )
244412 } else if ( bubble [ child . name ] ) {
245413 copyDeclarations = true
246414 unwrapped = true
0 commit comments