UNPKG

@mozaic-ds/postcss-media-queries-packer-on-demand

Version:
253 lines (205 loc) 6.74 kB
const postcss = require('postcss') const list = require('postcss/lib/list') let startPacking = false let queries = {} let queryLists = [] /* eslint-disable */ /** * Parses a media query list string into an array of query objects. * * @param {string} queryList - The media query list string to parse. * @returns {Array<Object>} An array of query objects, where each object represents a media query. * Each query object contains features as keys and their corresponding values as arrays. */ const parseQueryList = (queryList) => { const queries = [] list.comma(queryList).forEach((query) => { const expressions = {} list.space(query).forEach((expression) => { let newExpression = expression.toLowerCase() if (newExpression === 'and') { return } if (/^\w+$/.test(newExpression)) { expressions[newExpression] = true return } newExpression = list.split(newExpression.replace(/^\(|\)$/g, ''), [':']) const [feature, value] = newExpression if (!expressions[feature]) { expressions[feature] = [] } expressions[feature].push(value) }) queries.push(expressions) }) return queries } /** * Converts a CSS length value to a numeric value in pixels. * * @param {string} length - The CSS length value to inspect. Can be in units of 'ch', 'em', 'ex', 'px', or 'rem'. * @returns {number} The numeric value in pixels. Returns 0 if the input is '0'. Returns Number.MAX_VALUE if the input is not a valid CSS length. */ const inspectLength = (length) => { if (length === '0') { return 0 } const matches = /(-?\d*\.?\d+)(ch|em|ex|px|rem)/.exec(length) if (!matches) { return Number.MAX_VALUE } matches.shift() const [num, unit] = matches let newNum = num switch (unit) { case 'ch': newNum = parseFloat(newNum) * 8.8984375 break case 'em': case 'rem': newNum = parseFloat(newNum) * 16 break case 'ex': newNum = parseFloat(newNum) * 8.296875 break case 'px': newNum = parseFloat(newNum) break } return newNum } /** * Picks the minimum 'min-width' value from an array of media feature expressions. * * @param {Array<Object>} expressions - An array of media feature expressions. * @param {Object} expressions[].min-width - The 'min-width' value of the media feature. * @param {boolean} expressions[].not - Indicates if the media feature is negated. * @param {boolean} expressions[].print - Indicates if the media feature is for print. * @returns {number|null} The minimum 'min-width' value, or null if no valid 'min-width' is found. */ const pickMinimumMinWidth = (expressions) => { const minWidths = [] expressions.forEach((feature) => { let minWidth = feature['min-width'] if (!minWidth || feature.not || feature.print) { minWidth = [null] } minWidths.push(minWidth.map(inspectLength).sort((a, b) => b - a)[0]) }) return minWidths.sort((a, b) => a - b)[0] } /** * Sorts an array of media query lists based on the provided sorting criteria. * * @param {Array} queryLists - The array of media query lists to be sorted. * @param {boolean|function} sort - The sorting criteria. If false, no sorting is applied. * If a function is provided, it is used as the comparison function for sorting. * @returns {Array} - The sorted array of media query lists. */ const sortQueryLists = (queryLists, sort) => { const mapQueryLists = [] if (!sort) { return queryLists } if (typeof sort === 'function') { return queryLists.sort(sort) } queryLists.forEach((queryList) => { mapQueryLists.push(parseQueryList(queryList)) }) return mapQueryLists .map((e, i) => ({ index: i, value: pickMinimumMinWidth(e), })) .sort((a, b) => a.value - b.value) .map((e) => queryLists[e.index]) } /** * Adds a node to the appropriate at-rules based on its parent and parameters. * * This function processes a given node and organizes it into at-rules. If the node's * parent is not the root, it creates a new at-rule with the parent's name and parameters, * appends the node's rules to this new at-rule, and then replaces the original node with * the new at-rule. It also manages a list of queries and clones nodes as needed. * * @param {Object} node - The node to be processed and added to at-rules. * @param {Object} node.parent - The parent of the node. * @param {string} node.parent.name - The name of the parent node. * @param {string} node.parent.params - The parameters of the parent node. * @param {string} node.params - The parameters of the node. * @param {Function} node.each - Function to iterate over the node's rules. * @param {Function} node.remove - Function to remove the node. * @param {Function} node.removeAll - Function to remove all child nodes. * @param {Function} node.append - Function to append a new rule to the node. * @param {Function} node.clone - Function to clone the node. */ function addToAtRules(node) { if (node.parent.parent && node.parent.parent.type !== 'root') { return } if (node.parent.type !== 'root') { const newAtRule = postcss.atRule({ name: node.parent.name, params: node.parent.params, }) node.each((rule) => { newAtRule.append(rule) }) node.remove() node.removeAll() node.append(newAtRule) } const queryList = node.params const past = queries[queryList] if (typeof past === 'object') { node.each((rule) => { past.append(rule.clone()) }) } else { queries[queryList] = node.clone() queryLists.push(queryList) } node.remove() } module.exports = (options = {}) => { const opts = { sort: false, ...options, } return { postcssPlugin: 'postcss-media-queries-packer-on-demand', Once(root) { root.each((node) => { if (node.type === 'comment' && node.text === 'mqp:start') { startPacking = true node.remove() } if (node.type === 'comment' && node.text === 'mqp:end') { startPacking = false } if ( node.type === 'atrule' && node.name === 'media' && startPacking === true ) { addToAtRules(node) } if ( node.type === 'comment' && node.text === 'mqp:end' && startPacking === false ) { sortQueryLists(queryLists, opts.sort).forEach((queryList) => { node.before(queries[queryList]) }) node.remove() queries = {} queryLists = [] } }) }, } } module.exports.postcss = true