362 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			362 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const { AtRule, Rule } = require('postcss')
 | 
						|
let parser = require('postcss-selector-parser')
 | 
						|
 | 
						|
/**
 | 
						|
 * Run a selector string through postcss-selector-parser
 | 
						|
 */
 | 
						|
function parse(rawSelector, rule) {
 | 
						|
  let nodes
 | 
						|
  try {
 | 
						|
    parser(parsed => {
 | 
						|
      nodes = parsed
 | 
						|
    }).processSync(rawSelector)
 | 
						|
  } catch (e) {
 | 
						|
    if (rawSelector.includes(':')) {
 | 
						|
      throw rule ? rule.error('Missed semicolon') : e
 | 
						|
    } else {
 | 
						|
      throw rule ? rule.error(e.message) : e
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return nodes.at(0)
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Replaces the "&" token in a node's selector with the parent selector
 | 
						|
 * similar to what SCSS does.
 | 
						|
 *
 | 
						|
 * Mutates the nodes list
 | 
						|
 */
 | 
						|
function interpolateAmpInSelector(nodes, parent) {
 | 
						|
  let replaced = false
 | 
						|
  nodes.each(node => {
 | 
						|
    if (node.type === 'nesting') {
 | 
						|
      let clonedParent = parent.clone({})
 | 
						|
      if (node.value !== '&') {
 | 
						|
        node.replaceWith(
 | 
						|
          parse(node.value.replace('&', clonedParent.toString()))
 | 
						|
        )
 | 
						|
      } else {
 | 
						|
        node.replaceWith(clonedParent)
 | 
						|
      }
 | 
						|
      replaced = true
 | 
						|
    } else if ('nodes' in node && node.nodes) {
 | 
						|
      if (interpolateAmpInSelector(node, parent)) {
 | 
						|
        replaced = true
 | 
						|
      }
 | 
						|
    }
 | 
						|
  })
 | 
						|
  return replaced
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Combines parent and child selectors, in a SCSS-like way
 | 
						|
 */
 | 
						|
function mergeSelectors(parent, child) {
 | 
						|
  let merged = []
 | 
						|
  parent.selectors.forEach(sel => {
 | 
						|
    let parentNode = parse(sel, parent)
 | 
						|
 | 
						|
    child.selectors.forEach(selector => {
 | 
						|
      if (!selector) {
 | 
						|
        return
 | 
						|
      }
 | 
						|
      let node = parse(selector, child)
 | 
						|
      let replaced = interpolateAmpInSelector(node, parentNode)
 | 
						|
      if (!replaced) {
 | 
						|
        node.prepend(parser.combinator({ value: ' ' }))
 | 
						|
        node.prepend(parentNode.clone({}))
 | 
						|
      }
 | 
						|
      merged.push(node.toString())
 | 
						|
    })
 | 
						|
  })
 | 
						|
  return merged
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Move a child and its preceeding comment(s) to after "after"
 | 
						|
 */
 | 
						|
function breakOut(child, after) {
 | 
						|
  let prev = child.prev()
 | 
						|
  after.after(child)
 | 
						|
  while (prev && prev.type === 'comment') {
 | 
						|
    let nextPrev = prev.prev()
 | 
						|
    after.after(prev)
 | 
						|
    prev = nextPrev
 | 
						|
  }
 | 
						|
  return child
 | 
						|
}
 | 
						|
 | 
						|
function createFnAtruleChilds(bubble) {
 | 
						|
  return function atruleChilds(rule, atrule, bubbling, mergeSels = bubbling) {
 | 
						|
    let children = []
 | 
						|
    atrule.each(child => {
 | 
						|
      if (child.type === 'rule' && bubbling) {
 | 
						|
        if (mergeSels) {
 | 
						|
          child.selectors = mergeSelectors(rule, child)
 | 
						|
        }
 | 
						|
      } else if (child.type === 'atrule' && child.nodes) {
 | 
						|
        if (bubble[child.name]) {
 | 
						|
          atruleChilds(rule, child, mergeSels)
 | 
						|
        } else if (atrule[rootRuleMergeSel] !== false) {
 | 
						|
          children.push(child)
 | 
						|
        }
 | 
						|
      } else {
 | 
						|
        children.push(child)
 | 
						|
      }
 | 
						|
    })
 | 
						|
    if (bubbling) {
 | 
						|
      if (children.length) {
 | 
						|
        let clone = rule.clone({ nodes: [] })
 | 
						|
        for (let child of children) {
 | 
						|
          clone.append(child)
 | 
						|
        }
 | 
						|
        atrule.prepend(clone)
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function pickDeclarations(selector, declarations, after) {
 | 
						|
  let parent = new Rule({
 | 
						|
    nodes: [],
 | 
						|
    selector
 | 
						|
  })
 | 
						|
  parent.append(declarations)
 | 
						|
  after.after(parent)
 | 
						|
  return parent
 | 
						|
}
 | 
						|
 | 
						|
function atruleNames(defaults, custom) {
 | 
						|
  let list = {}
 | 
						|
  for (let name of defaults) {
 | 
						|
    list[name] = true
 | 
						|
  }
 | 
						|
  if (custom) {
 | 
						|
    for (let name of custom) {
 | 
						|
      list[name.replace(/^@/, '')] = true
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return list
 | 
						|
}
 | 
						|
 | 
						|
function parseRootRuleParams(params) {
 | 
						|
  params = params.trim()
 | 
						|
  let braceBlock = params.match(/^\((.*)\)$/)
 | 
						|
  if (!braceBlock) {
 | 
						|
    return { selector: params, type: 'basic' }
 | 
						|
  }
 | 
						|
  let bits = braceBlock[1].match(/^(with(?:out)?):(.+)$/)
 | 
						|
  if (bits) {
 | 
						|
    let allowlist = bits[1] === 'with'
 | 
						|
    let rules = Object.fromEntries(
 | 
						|
      bits[2]
 | 
						|
        .trim()
 | 
						|
        .split(/\s+/)
 | 
						|
        .map(name => [name, true])
 | 
						|
    )
 | 
						|
    if (allowlist && rules.all) {
 | 
						|
      return { type: 'noop' }
 | 
						|
    }
 | 
						|
    let escapes = rule => !!rules[rule]
 | 
						|
    if (rules.all) {
 | 
						|
      escapes = () => true
 | 
						|
    } else if (allowlist) {
 | 
						|
      escapes = rule => (rule === 'all' ? false : !rules[rule])
 | 
						|
    }
 | 
						|
 | 
						|
    return {
 | 
						|
      escapes,
 | 
						|
      type: 'withrules'
 | 
						|
    }
 | 
						|
  }
 | 
						|
  // Unrecognized brace block
 | 
						|
  return { type: 'unknown' }
 | 
						|
}
 | 
						|
 | 
						|
function getAncestorRules(leaf) {
 | 
						|
  let lineage = []
 | 
						|
  let parent = leaf.parent
 | 
						|
 | 
						|
  while (parent && parent instanceof AtRule) {
 | 
						|
    lineage.push(parent)
 | 
						|
    parent = parent.parent
 | 
						|
  }
 | 
						|
  return lineage
 | 
						|
}
 | 
						|
 | 
						|
function unwrapRootRule(rule) {
 | 
						|
  let escapes = rule[rootRuleEscapes]
 | 
						|
 | 
						|
  if (!escapes) {
 | 
						|
    rule.after(rule.nodes)
 | 
						|
  } else {
 | 
						|
    let nodes = rule.nodes
 | 
						|
 | 
						|
    let topEscaped
 | 
						|
    let topEscapedIdx = -1
 | 
						|
    let breakoutLeaf
 | 
						|
    let breakoutRoot
 | 
						|
    let clone
 | 
						|
 | 
						|
    let lineage = getAncestorRules(rule)
 | 
						|
    lineage.forEach((parent, i) => {
 | 
						|
      if (escapes(parent.name)) {
 | 
						|
        topEscaped = parent
 | 
						|
        topEscapedIdx = i
 | 
						|
        breakoutRoot = clone
 | 
						|
      } else {
 | 
						|
        let oldClone = clone
 | 
						|
        clone = parent.clone({ nodes: [] })
 | 
						|
        oldClone && clone.append(oldClone)
 | 
						|
        breakoutLeaf = breakoutLeaf || clone
 | 
						|
      }
 | 
						|
    })
 | 
						|
 | 
						|
    if (!topEscaped) {
 | 
						|
      rule.after(nodes)
 | 
						|
    } else if (!breakoutRoot) {
 | 
						|
      topEscaped.after(nodes)
 | 
						|
    } else {
 | 
						|
      let leaf = breakoutLeaf
 | 
						|
      leaf.append(nodes)
 | 
						|
      topEscaped.after(breakoutRoot)
 | 
						|
    }
 | 
						|
 | 
						|
    if (rule.next() && topEscaped) {
 | 
						|
      let restRoot
 | 
						|
      lineage.slice(0, topEscapedIdx + 1).forEach((parent, i, arr) => {
 | 
						|
        let oldRoot = restRoot
 | 
						|
        restRoot = parent.clone({ nodes: [] })
 | 
						|
        oldRoot && restRoot.append(oldRoot)
 | 
						|
 | 
						|
        let nextSibs = []
 | 
						|
        let _child = arr[i - 1] || rule
 | 
						|
        let next = _child.next()
 | 
						|
        while (next) {
 | 
						|
          nextSibs.push(next)
 | 
						|
          next = next.next()
 | 
						|
        }
 | 
						|
        restRoot.append(nextSibs)
 | 
						|
      })
 | 
						|
      restRoot && (breakoutRoot || nodes[nodes.length - 1]).after(restRoot)
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  rule.remove()
 | 
						|
}
 | 
						|
 | 
						|
const rootRuleMergeSel = Symbol('rootRuleMergeSel')
 | 
						|
const rootRuleEscapes = Symbol('rootRuleEscapes')
 | 
						|
 | 
						|
function normalizeRootRule(rule) {
 | 
						|
  let { params } = rule
 | 
						|
  let { escapes, selector, type } = parseRootRuleParams(params)
 | 
						|
  if (type === 'unknown') {
 | 
						|
    throw rule.error(
 | 
						|
      `Unknown @${rule.name} parameter ${JSON.stringify(params)}`
 | 
						|
    )
 | 
						|
  }
 | 
						|
  if (type === 'basic' && selector) {
 | 
						|
    let selectorBlock = new Rule({ nodes: rule.nodes, selector })
 | 
						|
    rule.removeAll()
 | 
						|
    rule.append(selectorBlock)
 | 
						|
  }
 | 
						|
  rule[rootRuleEscapes] = escapes
 | 
						|
  rule[rootRuleMergeSel] = escapes ? !escapes('all') : type === 'noop'
 | 
						|
}
 | 
						|
 | 
						|
const hasRootRule = Symbol('hasRootRule')
 | 
						|
 | 
						|
module.exports = (opts = {}) => {
 | 
						|
  let bubble = atruleNames(
 | 
						|
    ['media', 'supports', 'layer', 'container', 'starting-style'],
 | 
						|
    opts.bubble
 | 
						|
  )
 | 
						|
  let atruleChilds = createFnAtruleChilds(bubble)
 | 
						|
  let unwrap = atruleNames(
 | 
						|
    [
 | 
						|
      'document',
 | 
						|
      'font-face',
 | 
						|
      'keyframes',
 | 
						|
      '-webkit-keyframes',
 | 
						|
      '-moz-keyframes'
 | 
						|
    ],
 | 
						|
    opts.unwrap
 | 
						|
  )
 | 
						|
  let rootRuleName = (opts.rootRuleName || 'at-root').replace(/^@/, '')
 | 
						|
  let preserveEmpty = opts.preserveEmpty
 | 
						|
 | 
						|
  return {
 | 
						|
    Once(root) {
 | 
						|
      root.walkAtRules(rootRuleName, node => {
 | 
						|
        normalizeRootRule(node)
 | 
						|
        root[hasRootRule] = true
 | 
						|
      })
 | 
						|
    },
 | 
						|
 | 
						|
    postcssPlugin: 'postcss-nested',
 | 
						|
 | 
						|
    RootExit(root) {
 | 
						|
      if (root[hasRootRule]) {
 | 
						|
        root.walkAtRules(rootRuleName, unwrapRootRule)
 | 
						|
        root[hasRootRule] = false
 | 
						|
      }
 | 
						|
    },
 | 
						|
 | 
						|
    Rule(rule) {
 | 
						|
      let unwrapped = false
 | 
						|
      let after = rule
 | 
						|
      let copyDeclarations = false
 | 
						|
      let declarations = []
 | 
						|
 | 
						|
      rule.each(child => {
 | 
						|
        if (child.type === 'rule') {
 | 
						|
          if (declarations.length) {
 | 
						|
            after = pickDeclarations(rule.selector, declarations, after)
 | 
						|
            declarations = []
 | 
						|
          }
 | 
						|
 | 
						|
          copyDeclarations = true
 | 
						|
          unwrapped = true
 | 
						|
          child.selectors = mergeSelectors(rule, child)
 | 
						|
          after = breakOut(child, after)
 | 
						|
        } else if (child.type === 'atrule') {
 | 
						|
          if (declarations.length) {
 | 
						|
            after = pickDeclarations(rule.selector, declarations, after)
 | 
						|
            declarations = []
 | 
						|
          }
 | 
						|
          if (child.name === rootRuleName) {
 | 
						|
            unwrapped = true
 | 
						|
            atruleChilds(rule, child, true, child[rootRuleMergeSel])
 | 
						|
            after = breakOut(child, after)
 | 
						|
          } else if (bubble[child.name]) {
 | 
						|
            copyDeclarations = true
 | 
						|
            unwrapped = true
 | 
						|
            atruleChilds(rule, child, true)
 | 
						|
            after = breakOut(child, after)
 | 
						|
          } else if (unwrap[child.name]) {
 | 
						|
            copyDeclarations = true
 | 
						|
            unwrapped = true
 | 
						|
            atruleChilds(rule, child, false)
 | 
						|
            after = breakOut(child, after)
 | 
						|
          } else if (copyDeclarations) {
 | 
						|
            declarations.push(child)
 | 
						|
          }
 | 
						|
        } else if (child.type === 'decl' && copyDeclarations) {
 | 
						|
          declarations.push(child)
 | 
						|
        }
 | 
						|
      })
 | 
						|
 | 
						|
      if (declarations.length) {
 | 
						|
        after = pickDeclarations(rule.selector, declarations, after)
 | 
						|
      }
 | 
						|
 | 
						|
      if (unwrapped && preserveEmpty !== true) {
 | 
						|
        rule.raws.semicolon = true
 | 
						|
        if (rule.nodes.length === 0) rule.remove()
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
module.exports.postcss = true
 |