diff --git a/README.md b/README.md index b1874f4..63ce569 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,18 @@ postcss([ This will only merge the ones that are not nested within selectors +### Nest media queries + +Nest compatible top level media queries. + +```js +postcss([ + sortMediaQueries({ + nested: true, + }) +]).process(css); +``` + --- ## Changelog diff --git a/index.js b/index.js index b8f2e17..a1f669d 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,17 @@ -function sortAtRules(queries, sort, sortCSSmq) { - if (typeof sort !== "function") { - sort = sort === "desktop-first" ? sortCSSmq.desktopFirst : sortCSSmq; +function getParams(query) { + return query.split('and').map(q => q.trim()) +} + +function sortParams(params) { + const hasNumber = (param) => /\d/.test(param) + const sort = (a, b) => { + if (hasNumber(a)) return 1; + else if (hasNumber(b)) return -1; + return 0; } + const paramsArr = getParams(params).sort(sort); - return queries.sort(sort); + return paramsArr.join(' and '); } module.exports = (opts = {}) => { @@ -13,6 +21,7 @@ module.exports = (opts = {}) => { configuration: false, onlyTopLevel: false, mergeAtRules: false, + nested: false, }, opts ); @@ -26,6 +35,93 @@ module.exports = (opts = {}) => { postcssPlugin: "postcss-sort-media-queries", OnceExit(root, { AtRule }) { + /** + * Sort atRules + * @param {object} atRulesObject object with atRule params as keys + * @returns Array of atRules + */ + function sortAtRules(atRulesObject) { + let sort = opts.sort; + const queries = Object.keys(atRulesObject); + + if (typeof sort !== "function") { + sort = sort === "desktop-first" ? sortCSSmq.desktopFirst : sortCSSmq; + } + + return queries.sort(sort).map((query) => atRulesObject[query]); + } + + /** + * Nest atRules + * + * @param {array} localAtRules Array of atRules + * @returns Array of nested atRules + */ + function recursivelyNestAtRules(localAtRules) { + let i = 0; + + while (i < localAtRules.length) { + const queryParams = getParams(localAtRules[i].params); + const query = queryParams.shift(); + let atRules = {}; + + if (queryParams.length) { + localAtRules[i] = new AtRule({ + name: localAtRules[i].name, + params: query, + nodes: recursivelyNestAtRules([ + new AtRule({ + name: localAtRules[i].name, + params: queryParams.join(' and '), + nodes: localAtRules[i].nodes, + }) + ]), + }); + + localAtRules = sortAtRules(localAtRules.reduce((acc, current) => { + return Object.assign(acc, { [current.params]: current }); + }, {})); + } else { + localAtRules.forEach(({ params, name, nodes }) => { + if (params.startsWith(query) && params !== query) { + const newQuery = params.split(`${query} and`)[1].trim(); + atRules[newQuery] = new AtRule({ + name: name, + params: newQuery, + nodes: nodes, + }); + } + }); + + if (Object.getOwnPropertyNames(atRules).length) { + atRules = sortAtRules(atRules); + + recursivelyNestAtRules(atRules).forEach((atRule) => { + const existingAtRuleIndex = localAtRules[i].nodes.findIndex(({params}) => params === atRule.params) + if (existingAtRuleIndex > -1) { + if (localAtRules[i].nodes[existingAtRuleIndex].nodes) { + localAtRules[i].nodes[existingAtRuleIndex].nodes.forEach((node) => { + atRule.append(node.clone()); + }); + } + + localAtRules[i].nodes[existingAtRuleIndex].remove(); + } + localAtRules[i].append(atRule); + }); + + localAtRules = localAtRules.filter(({ params }) => { + return !params.startsWith(query) || params === query; + }); + } + + i++; + } + } + + return localAtRules; + } + /** * Recursively merge media queries & layer (optionally) * @@ -35,15 +131,15 @@ module.exports = (opts = {}) => { function recursivelyMergeAtRules(localRoot, match) { let atRules = {}; - localRoot.walkAtRules(match || /(media|container)/mi, (atRule) => { - const query = atRule.params; + localRoot.walkAtRules(match || /(media|container)/im, (atRule) => { + const query = sortParams(atRule.params); if (!match && opts.onlyTopLevel && atRule.parent.type !== "root") return; if (!atRules[query]) { atRules[query] = new AtRule({ name: atRule.name, - params: atRule.params, + params: query, }); } @@ -61,27 +157,36 @@ module.exports = (opts = {}) => { return atRules; } - const atRules = recursivelyMergeAtRules(root); + let atRules = recursivelyMergeAtRules(root); if (opts.mergeAtRules) { - const atRuleRegex = /(layer|scope|supports)/mi; + const atRuleRegex = /(layer|scope|supports)/im; const rootAtRules = recursivelyMergeAtRules(root, atRuleRegex); - Object.keys(rootAtRules).reverse().forEach(nestedAtRuleKey => { - root.prepend(rootAtRules[nestedAtRuleKey]) + Object.keys(rootAtRules) + .reverse() + .forEach((nestedAtRuleKey) => { + root.prepend(rootAtRules[nestedAtRuleKey]); + }); + + Object.keys(atRules).forEach((query) => { + const nestedAtRules = recursivelyMergeAtRules( + atRules[query], + atRuleRegex + ); + Object.keys(nestedAtRules) + .reverse() + .forEach((nestedAtRuleKey) => { + atRules[query].prepend(nestedAtRules[nestedAtRuleKey]); + }); }); - - Object.keys(atRules).forEach(query => { - const nestedAtRules = recursivelyMergeAtRules(atRules[query], atRuleRegex); - Object.keys(nestedAtRules).reverse().forEach(nestedAtRuleKey => { - atRules[query].prepend(nestedAtRules[nestedAtRuleKey]) - }) - }) } if (atRules) { - let sortedAtRules = sortAtRules(Object.keys(atRules), opts.sort, sortCSSmq).map(query => atRules[query]); + atRules = sortAtRules(atRules); + + if (opts.nested) atRules = recursivelyNestAtRules(atRules); - sortedAtRules.forEach((atRule) => { + atRules.forEach((atRule) => { root.append(atRule); }); }