Search code examples
javascriptpostcsscss-parsing

How to append attribute in css with postcss?


I have a following css:

a, a::after, p + .selector, .selector > .my-selector, .selector::before {

}

I want to append [data-123] to all of the selectors. So something like:

a[data-123], a::after[data-123], p[data-123] + .selector[data-123], .selector[data-123] > .my-selector[data-123], .selector::before[data-123] {

}

How can I do this with postcss or is there any other way to achieve the result at runtime?


Solution

  • This is how Vue does it:

    • Create a util file:
    //css-parser.js
    
    import { Root } from "postcss"
    import * as postcss from "postcss"
    // postcss-selector-parser does have typings but it's problematic to work with.
    const selectorParser = require("postcss-selector-parser")
    
    export default postcss.plugin("add-id", options => root => {
        const id = options
        const keyframes = Object.create(null)
    
        root.each(function rewriteSelector(node) {
            if (!node.selector) {
                // handle media queries
                if (node.type === "atrule") {
                    if (node.name === "media" || node.name === "supports") {
                        node.each(rewriteSelector)
                    } else if (/-?keyframes$/.test(node.name)) {
                        // register keyframes
                        keyframes[node.params] = node.params =
                            node.params + "-" + id
                    }
                }
                return
            }
            node.selector = selectorParser(selectors => {
                selectors.each(selector => {
                    let node = null
    
                    // find the last child node to insert attribute selector
                    selector.each(n => {
                        // ">>>" combinator
                        // and /deep/ alias for >>>, since >>> doesn't work in SASS
                        if (
                            n.type === "combinator" &&
                            (n.value === ">>>" || n.value === "/deep/")
                        ) {
                            n.value = " "
                            n.spaces.before = n.spaces.after = ""
                            return false
                        }
    
                        // in newer versions of sass, /deep/ support is also dropped, so add a ::v-deep alias
                        if (n.type === "pseudo" && n.value === "::v-deep") {
                            n.value = n.spaces.before = n.spaces.after = ""
                            return false
                        }
    
                        if (n.type !== "pseudo" && n.type !== "combinator") {
                            node = n
                        }
                    })
    
                    if (node) {
                        node.spaces.after = ""
                    } else {
                        // For deep selectors & standalone pseudo selectors,
                        // the attribute selectors are prepended rather than appended.
                        // So all leading spaces must be eliminated to avoid problems.
                        selector.first.spaces.before = ""
                    }
    
                    selector.insertAfter(
                        node,
                        selectorParser.attribute({
                            attribute: id
                        })
                    )
                })
            }).processSync(node.selector)
        })
    
        // If keyframes are found in this <style>, find and rewrite animation names
        // in declarations.
        // Caveat: this only works for keyframes and animation rules in the same
        // <style> element.
        if (Object.keys(keyframes).length) {
            root.walkDecls(decl => {
                // individual animation-name declaration
                if (/^(-\w+-)?animation-name$/.test(decl.prop)) {
                    decl.value = decl.value
                        .split(",")
                        .map(v => keyframes[v.trim()] || v.trim())
                        .join(",")
                }
                // shorthand
                if (/^(-\w+-)?animation$/.test(decl.prop)) {
                    decl.value = decl.value
                        .split(",")
                        .map(v => {
                            const vals = v.trim().split(/\s+/)
                            const i = vals.findIndex(val => keyframes[val])
                            if (i !== -1) {
                                vals.splice(i, 1, keyframes[vals[i]])
                                return vals.join(" ")
                            } else {
                                return v
                            }
                        })
                        .join(",")
                }
            })
        }
    })
    
    
    • Then use it as:
    import hashsum from "hash-sum"
    import cssParser from "@/utils/css-parser"
    
    const uniqueHash = "data-e-" + hashsum("my-random-hash")
    
    const processCss = (css, hash) => {
        const plugins = [cssParser(hash || uniqueHash)]
        const result = postcss(plugins).process(css)
    
        return result.css
    }
    

    It works perfectly.