0

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.

Richie Bendall
  • 5,657
  • 3
  • 32
  • 49
  • 1
    The title of your question is misleading, since it isn't about parsing or CSS. What you're asking is how to extract properties from an object and rearrange them into a new object. The keys and values are completely irrelevant. There's a ton of questions already dealing with this. In case you don't know, you can get to the first rule using `astResult.stylesheet.rules[0]` – ChrisG Jan 12 '20 at 14:59
  • Here's one: https://stackoverflow.com/questions/48841569/extract-data-from-javascript-object – ChrisG Jan 12 '20 at 15:03
  • @ChrisG The premise of this problem resides around the fact that the CSS I'm showing in the question is just for example's sake. I need to be able to correctly handle the AST for when **any** valid CSS is provided which requires lots of iterating. I'll add more clarification in my question. – Richie Bendall Jan 12 '20 at 15:05
  • Do you need this? https://jsfiddle.net/khrismuc/y36zfkwg/ If that's not enough, state an example of a more complex stylesheet. – ChrisG Jan 12 '20 at 15:15
  • @ChrisG I've updated the example in my question to show the full application of the functionality I need to create. – Richie Bendall Jan 12 '20 at 15:23
  • Ok, so at this point you're essentially asking for somebody else to write the code for you. What have you tried so far? Where is your code? How did it fail? – ChrisG Jan 12 '20 at 15:30
  • @ChrisG Sure thing! – Richie Bendall Jan 12 '20 at 15:41
  • Wait... are you saying you have working code and you're just asking how to optimize it...? – ChrisG Jan 12 '20 at 16:54
  • @ChrisG I was writing the code and asked the question too early before I had finished. – Richie Bendall Jan 13 '20 at 03:40
  • In that case you might as well remove it, because you'll want to post on [codereview](https://codereview.stackexchange.com/) instead anyway. – ChrisG Jan 13 '20 at 09:48

2 Answers2

0

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);
johannchopin
  • 11,813
  • 8
  • 41
  • 91
0

Solving using reduce:

let 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": []
    }
}

let parsedStyle = styles.stylesheet.rules.reduce((parsed, rule) => {
    parsed[rule['selectors']] = rule['declarations'].reduce((rr, r) => {
    rr[r['property']] = r['value'];return rr
    }, {})
    return parsed
}, {})

console.log(parsedStyle)