Parsing CSS structure into an object

I'm trying to parse the structure of some CSS to use as an input to renderkid in Node.js, kinda (but not the same as) like Parsing CSS in JavaScript / jQuery.

For instance, I want this CSS

body { font-size: 10px; }
html { font-size: 11px; }
html, body { font-size: 12px; }
@media only screen and (max-width: 600px) {
  body {
    background-color: lightblue;
  }
}

to be parsed as this object:

{
  body: {
    "font-size": "12px"
  },
  html: {
    "font-size": "12px"
  },
  "@media only screen and (max-width: 600px)": {
    body: {
      "font-size": "12px"
    },
  }
}

To avoid reinventing the wheel by writing a CSS parser, I'm using the popular css package which returns the AST of the provided CSS which in this case looks like this:

{
    "type": "stylesheet",
    "stylesheet": {
        "rules": [{
            "type": "rule",
            "selectors": ["body"],
            "declarations": [{
                "type": "declaration",
                "property": "font-size",
                "value": "10px",
                "position": {
                    "start": {
                        "line": 1,
                        "column": 8
                    },
                    "end": {
                        "line": 1,
                        "column": 23
                    }
                }
            }],
            "position": {
                "start": {
                    "line": 1,
                    "column": 1
                },
                "end": {
                    "line": 1,
                    "column": 26
                }
            }
        }, {
            "type": "rule",
            "selectors": ["html"],
            "declarations": [{
                "type": "declaration",
                "property": "font-size",
                "value": "11px",
                "position": {
                    "start": {
                        "line": 2,
                        "column": 8
                    },
                    "end": {
                        "line": 2,
                        "column": 23
                    }
                }
            }],
            "position": {
                "start": {
                    "line": 2,
                    "column": 1
                },
                "end": {
                    "line": 2,
                    "column": 26
                }
            }
        }, {
            "type": "rule",
            "selectors": ["html", "body"],
            "declarations": [{
                "type": "declaration",
                "property": "font-size",
                "value": "12px",
                "position": {
                    "start": {
                        "line": 3,
                        "column": 14
                    },
                    "end": {
                        "line": 3,
                        "column": 29
                    }
                }
            }],
            "position": {
                "start": {
                    "line": 3,
                    "column": 1
                },
                "end": {
                    "line": 3,
                    "column": 32
                }
            }
        }, {
            "type": "media",
            "media": "only screen and (max-width: 600px)",
            "rules": [{
                "type": "rule",
                "selectors": ["body"],
                "declarations": [{
                    "type": "declaration",
                    "property": "background-color",
                    "value": "lightblue",
                    "position": {
                        "start": {
                            "line": 6,
                            "column": 5
                        },
                        "end": {
                            "line": 6,
                            "column": 32
                        }
                    }
                }],
                "position": {
                    "start": {
                        "line": 5,
                        "column": 3
                    },
                    "end": {
                        "line": 7,
                        "column": 4
                    }
                }
            }],
            "position": {
                "start": {
                    "line": 4,
                    "column": 1
                },
                "end": {
                    "line": 8,
                    "column": 2
                }
            }
        }],
        "parsingErrors": []
    }
}

At the moment, I've managed to come up with this code:

"use strict"

const {
    parse: parseCSS,
} = require("css")
const _ = require("lodash")

const pickToObject = (array, ...keys) => _.fromPairs(array.map((val) => [val[keys[0]], val[keys[1]]]))

module.exports = (css) => _.merge(...parseCSS(css).stylesheet.rules.map(({
    declarations,
    selectors,
    type,
    media,
    rules,
}) => {
    if (type === "rule") return _.fromPairs(selectors.map((selector) => [selector, pickToObject(declarations, "property", "value")]))
    if (type === "media") {
        return _.fromPairs([
            [`@media ${media}`, _.merge(...rules.map(({
                selectors,
                declarations,
            }) => _.fromPairs(selectors.map((selector) => [selector, pickToObject(declarations, "property", "value")]))))],
        ])
    }

    return undefined
}))

However, I'm not sure how to optimise it further.

Just to clarify: I need to create a canonical function that can handle any valid CSS - meaning simply grabbing values from the AST isn't an option.

1 answer

  • answered 2020-01-12 15:17 johannchopin

    Using javascript it's really simple to parse your given styles object:

    var styles = {
        "type": "stylesheet",
        "stylesheet": {
            "rules": [{
                "type": "rule",
                "selectors": ["body"],
                "declarations": [{
                    "type": "declaration",
                    "property": "background-color",
                    "value": "black",
                    "position": {
                        "start": {
                            "line": 1,
                            "column": 8
                        },
                        "end": {
                            "line": 1,
                            "column": 32
                        }
                    }
                }],
                "position": {
                    "start": {
                        "line": 1,
                        "column": 1
                    },
                    "end": {
                        "line": 1,
                        "column": 33
                    }
                }
            }],
            "parsingErrors": []
        }
    };
    
    var parsedStyles = {};
    
    styles.stylesheet.rules.forEach(rule => {
      parsedStyles[rule['selectors']] = getAllStyles(rule['declarations']);
    })
    
    function getAllStyles(declarations) {
    	var styles = {}
    	declarations.forEach(declaration => {
        styles[declaration['property']] = declaration['value'];
      });
      
      return styles;
    }
    
    console.log(parsedStyles);