123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- 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
|