8889841cREADME.md000066600000001562150441765100006036 0ustar00# [postcss][postcss]-merge-longhand > Merge longhand properties into shorthand with PostCSS. ## Install With [npm](https://npmjs.org/package/postcss-merge-longhand) do: ``` npm install postcss-merge-longhand --save ``` ## Example Merge longhand properties into shorthand; works with `margin`, `padding` & `border`. For more examples see the [tests](src/__tests__/index.js). ### Input ```css h1 { margin-top: 10px; margin-right: 20px; margin-bottom: 10px; margin-left: 20px; } ``` ### Output ```css h1 { margin: 10px 20px; } ``` ## Usage See the [PostCSS documentation](https://github.com/postcss/postcss#usage) for examples for your environment. ## Contributors See [CONTRIBUTORS.md](https://github.com/cssnano/cssnano/blob/master/CONTRIBUTORS.md). ## License MIT © [Ben Briggs](http://beneb.info) [postcss]: https://github.com/postcss/postcss src/index.js000066600000000732150441765100007011 0ustar00'use strict'; const processors = require('./lib/decl'); /** * @type {import('postcss').PluginCreator} * @return {import('postcss').Plugin} */ function pluginCreator() { return { postcssPlugin: 'postcss-merge-longhand', OnceExit(css) { css.walkRules((rule) => { processors.forEach((p) => { p.explode(rule); p.merge(rule); }); }); }, }; } pluginCreator.postcss = true; module.exports = pluginCreator; src/lib/getValue.js000066600000000233150441765100010220 0ustar00'use strict'; /** * @param {import('postcss').Declaration} arg * @return {string} */ module.exports = function getValue({ value }) { return value; }; src/lib/canExplode.js000066600000000734150441765100010534 0ustar00'use strict'; const isCustomProp = require('./isCustomProp'); const globalKeywords = new Set(['inherit', 'initial', 'unset', 'revert']); /** @type {(prop: import('postcss').Declaration, includeCustomProps?: boolean) => boolean} */ module.exports = (prop, includeCustomProps = true) => { if ( !prop.value || (includeCustomProps && isCustomProp(prop)) || (prop.value && globalKeywords.has(prop.value.toLowerCase())) ) { return false; } return true; }; src/lib/hasAllProps.js000066600000000371150441765100010677 0ustar00'use strict'; /** @type {(rule: import('postcss').Declaration[], ...props: string[]) => boolean} */ module.exports = (rule, ...props) => { return props.every((p) => rule.some((node) => node.prop && node.prop.toLowerCase().includes(p)) ); }; src/lib/mergeRules.js000066600000004122150441765100010557 0ustar00'use strict'; const hasAllProps = require('./hasAllProps.js'); const getDecls = require('./getDecls.js'); const getRules = require('./getRules.js'); /** * @param {import('postcss').Declaration} propA * @param {import('postcss').Declaration} propB * @return {boolean} */ function isConflictingProp(propA, propB) { if ( !propB.prop || propB.important !== propA.important || propA.prop === propB.prop ) { return false; } const partsA = propA.prop.split('-'); const partsB = propB.prop.split('-'); /* Be safe: check that the first part matches. So we don't try to * combine e.g. border-color and color. */ if (partsA[0] !== partsB[0]) { return false; } const partsASet = new Set(partsA); return partsB.every((partB) => partsASet.has(partB)); } /** * @param {import('postcss').Declaration[]} match * @param {import('postcss').Declaration[]} nodes * @return {boolean} */ function hasConflicts(match, nodes) { const firstNode = Math.min(...match.map((n) => nodes.indexOf(n))); const lastNode = Math.max(...match.map((n) => nodes.indexOf(n))); const between = nodes.slice(firstNode + 1, lastNode); return match.some((a) => between.some((b) => isConflictingProp(a, b))); } /** * @param {import('postcss').Rule} rule * @param {string[]} properties * @param {(rules: import('postcss').Declaration[], last: import('postcss').Declaration, props: import('postcss').Declaration[]) => boolean} callback * @return {void} */ module.exports = function mergeRules(rule, properties, callback) { let decls = getDecls(rule, properties); while (decls.length) { const last = decls[decls.length - 1]; const props = decls.filter((node) => node.important === last.important); const rules = getRules(props, properties); if ( hasAllProps(rules, ...properties) && !hasConflicts( rules, /** @type import('postcss').Declaration[]*/ (rule.nodes) ) ) { if (callback(rules, last, props)) { decls = decls.filter((node) => !rules.includes(node)); } } decls = decls.filter((node) => node !== last); } }; src/lib/isCustomProp.js000066600000000225150441765100011114 0ustar00'use strict'; /** @type {(node: import('postcss').Declaration) => boolean} */ module.exports = (node) => node.value.search(/var\s*\(\s*--/i) !== -1; src/lib/getLastNode.js000066600000000444150441765100010661 0ustar00'use strict'; /** @type {(rule: import('postcss').AnyNode[], prop: string) => import('postcss').Declaration} */ module.exports = (rule, prop) => { return /** @type {import('postcss').Declaration} */ ( rule.filter((n) => n.type === 'decl' && n.prop.toLowerCase() === prop).pop() ); }; src/lib/validateWsc.js000066600000002671150441765100010722 0ustar00'use strict'; const colors = require('./colornames.js'); const widths = new Set(['thin', 'medium', 'thick']); const styles = new Set([ 'none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset', ]); /** * @param {string} value * @return {boolean} */ function isStyle(value) { return value !== undefined && styles.has(value.toLowerCase()); } /** * @param {string} value * @return {boolean} */ function isWidth(value) { return ( (value && widths.has(value.toLowerCase())) || /^(\d+(\.\d+)?|\.\d+)(\w+)?$/.test(value) ); } /** * @param {string} value * @return {boolean} */ function isColor(value) { if (!value) { return false; } value = value.toLowerCase(); if (/rgba?\(/.test(value)) { return true; } if (/hsla?\(/.test(value)) { return true; } if (/#([0-9a-z]{6}|[0-9a-z]{3})/.test(value)) { return true; } if (value === 'transparent') { return true; } if (value === 'currentcolor') { return true; } return colors.has(value); } /** * @param {[string, string, string]} wscs * @return {boolean} */ function isValidWsc(wscs) { const validWidth = isWidth(wscs[0]); const validStyle = isStyle(wscs[1]); const validColor = isColor(wscs[2]); return ( (validWidth && validStyle) || (validWidth && validColor) || (validStyle && validColor) ); } module.exports = { isStyle, isWidth, isColor, isValidWsc }; src/lib/parseTrbl.js000066600000000513150441765100010403 0ustar00'use strict'; const { list } = require('postcss'); /** @type {(v: string | string[]) => [string, string, string, string]} */ module.exports = (v) => { const s = typeof v === 'string' ? list.space(v) : v; return [ s[0], // top s[1] || s[0], // right s[2] || s[0], // bottom s[3] || s[1] || s[0], // left ]; }; src/lib/decl/index.js000066600000000333150441765100010463 0ustar00'use strict'; const borders = require('./borders'); const columns = require('./columns'); const margin = require('./margin'); const padding = require('./padding'); module.exports = [borders, columns, margin, padding]; src/lib/decl/margin.js000066600000000126150441765100010631 0ustar00'use strict'; const base = require('./boxBase.js'); module.exports = base('margin'); src/lib/decl/boxBase.js000066600000006365150441765100010752 0ustar00'use strict'; const stylehacks = require('stylehacks'); const canMerge = require('../canMerge.js'); const getDecls = require('../getDecls.js'); const minifyTrbl = require('../minifyTrbl.js'); const parseTrbl = require('../parseTrbl.js'); const insertCloned = require('../insertCloned.js'); const mergeRules = require('../mergeRules.js'); const mergeValues = require('../mergeValues.js'); const trbl = require('../trbl.js'); const isCustomProp = require('../isCustomProp.js'); const canExplode = require('../canExplode.js'); /** * @param {string} prop * @return {{explode: (rule: import('postcss').Rule) => void, merge: (rule: import('postcss').Rule) => void}} */ module.exports = (prop) => { const properties = trbl.map((direction) => `${prop}-${direction}`); /** @type {(rule: import('postcss').Rule) => void} */ const cleanup = (rule) => { let decls = getDecls(rule, [prop].concat(properties)); while (decls.length) { const lastNode = decls[decls.length - 1]; // remove properties of lower precedence const lesser = decls.filter( (node) => !stylehacks.detect(lastNode) && !stylehacks.detect(node) && node !== lastNode && node.important === lastNode.important && lastNode.prop === prop && node.prop !== lastNode.prop ); for (const node of lesser) { node.remove(); } decls = decls.filter((node) => !lesser.includes(node)); // get duplicate properties let duplicates = decls.filter( (node) => !stylehacks.detect(lastNode) && !stylehacks.detect(node) && node !== lastNode && node.important === lastNode.important && node.prop === lastNode.prop && !(!isCustomProp(node) && isCustomProp(lastNode)) ); for (const node of duplicates) { node.remove(); } decls = decls.filter( (node) => node !== lastNode && !duplicates.includes(node) ); } }; const processor = { /** @type {(rule: import('postcss').Rule) => void} */ explode: (rule) => { rule.walkDecls(new RegExp('^' + prop + '$', 'i'), (decl) => { if (!canExplode(decl)) { return; } if (stylehacks.detect(decl)) { return; } const values = parseTrbl(decl.value); trbl.forEach((direction, index) => { insertCloned( /** @type {import('postcss').Rule} */ (decl.parent), decl, { prop: properties[index], value: values[index], } ); }); decl.remove(); }); }, /** @type {(rule: import('postcss').Rule) => void} */ merge: (rule) => { mergeRules(rule, properties, (rules, lastNode) => { if (canMerge(rules) && !rules.some(stylehacks.detect)) { insertCloned( /** @type {import('postcss').Rule} */ (lastNode.parent), lastNode, { prop, value: minifyTrbl(mergeValues(...rules)), } ); for (const node of rules) { node.remove(); } return true; } return false; }); cleanup(rule); }, }; return processor; }; src/lib/decl/padding.js000066600000000124150441765100010760 0ustar00'use strict'; const base = require('./boxBase'); module.exports = base('padding'); src/lib/decl/columns.js000066600000007422150441765100011042 0ustar00'use strict'; const { list } = require('postcss'); const { unit } = require('postcss-value-parser'); const stylehacks = require('stylehacks'); const canMerge = require('../canMerge.js'); const getDecls = require('../getDecls.js'); const getValue = require('../getValue.js'); const mergeRules = require('../mergeRules.js'); const insertCloned = require('../insertCloned.js'); const isCustomProp = require('../isCustomProp.js'); const canExplode = require('../canExplode.js'); const properties = ['column-width', 'column-count']; const auto = 'auto'; const inherit = 'inherit'; /** * Normalize a columns shorthand definition. Both of the longhand * properties' initial values are 'auto', and as per the spec, * omitted values are set to their initial values. Thus, we can * remove any 'auto' definition when there are two values. * * Specification link: https://www.w3.org/TR/css3-multicol/ * * @param {[string, string]} values * @return {string} */ function normalize(values) { if (values[0].toLowerCase() === auto) { return values[1]; } if (values[1].toLowerCase() === auto) { return values[0]; } if ( values[0].toLowerCase() === inherit && values[1].toLowerCase() === inherit ) { return inherit; } return values.join(' '); } /** * @param {import('postcss').Rule} rule * @return {void} */ function explode(rule) { rule.walkDecls(/^columns$/i, (decl) => { if (!canExplode(decl)) { return; } if (stylehacks.detect(decl)) { return; } let values = list.space(decl.value); if (values.length === 1) { values.push(auto); } values.forEach((value, i) => { let prop = properties[1]; const dimension = unit(value); if (value.toLowerCase() === auto) { prop = properties[i]; } else if (dimension && dimension.unit !== '') { prop = properties[0]; } insertCloned(/** @type {import('postcss').Rule} */ (decl.parent), decl, { prop, value, }); }); decl.remove(); }); } /** * @param {import('postcss').Rule} rule * @return {void} */ function cleanup(rule) { let decls = getDecls(rule, ['columns'].concat(properties)); while (decls.length) { const lastNode = decls[decls.length - 1]; // remove properties of lower precedence const lesser = decls.filter( (node) => !stylehacks.detect(lastNode) && !stylehacks.detect(node) && node !== lastNode && node.important === lastNode.important && lastNode.prop === 'columns' && node.prop !== lastNode.prop ); for (const node of lesser) { node.remove(); } decls = decls.filter((node) => !lesser.includes(node)); // get duplicate properties let duplicates = decls.filter( (node) => !stylehacks.detect(lastNode) && !stylehacks.detect(node) && node !== lastNode && node.important === lastNode.important && node.prop === lastNode.prop && !(!isCustomProp(node) && isCustomProp(lastNode)) ); for (const node of duplicates) { node.remove(); } decls = decls.filter( (node) => node !== lastNode && !duplicates.includes(node) ); } } /** * @param {import('postcss').Rule} rule * @return {void} */ function merge(rule) { mergeRules(rule, properties, (rules, lastNode) => { if (canMerge(rules) && !rules.some(stylehacks.detect)) { insertCloned( /** @type {import('postcss').Rule} */ (lastNode.parent), lastNode, { prop: 'columns', value: normalize(/** @type [string, string] */ (rules.map(getValue))), } ); for (const node of rules) { node.remove(); } return true; } return false; }); cleanup(rule); } module.exports = { explode, merge, }; src/lib/decl/borders.js000066600000052312150441765100011020 0ustar00'use strict'; const { list } = require('postcss'); const stylehacks = require('stylehacks'); const insertCloned = require('../insertCloned.js'); const parseTrbl = require('../parseTrbl.js'); const hasAllProps = require('../hasAllProps.js'); const getDecls = require('../getDecls.js'); const getRules = require('../getRules.js'); const getValue = require('../getValue.js'); const mergeRules = require('../mergeRules.js'); const minifyTrbl = require('../minifyTrbl.js'); const minifyWsc = require('../minifyWsc.js'); const canMerge = require('../canMerge.js'); const trbl = require('../trbl.js'); const isCustomProp = require('../isCustomProp.js'); const canExplode = require('../canExplode.js'); const getLastNode = require('../getLastNode.js'); const parseWsc = require('../parseWsc.js'); const { isValidWsc } = require('../validateWsc.js'); const wsc = ['width', 'style', 'color']; const defaults = ['medium', 'none', 'currentcolor']; const colorMightRequireFallback = /(hsla|rgba|color|hwb|lab|lch|oklab|oklch)\(/i; /** * @param {...string} parts * @return {string} */ function borderProperty(...parts) { return `border-${parts.join('-')}`; } /** * @param {string} value * @return {string} */ function mapBorderProperty(value) { return borderProperty(value); } const directions = trbl.map(mapBorderProperty); const properties = wsc.map(mapBorderProperty); /** @type {string[]} */ const directionalProperties = directions.reduce( (prev, curr) => prev.concat(wsc.map((prop) => `${curr}-${prop}`)), /** @type {string[]} */ ([]) ); const precedence = [ ['border'], directions.concat(properties), directionalProperties, ]; const allProperties = precedence.reduce((a, b) => a.concat(b)); /** * @param {string} prop * @return {number | undefined} */ function getLevel(prop) { for (let i = 0; i < precedence.length; i++) { if (precedence[i].includes(prop.toLowerCase())) { return i; } } } /** @type {(value: string) => boolean} */ const isValueCustomProp = (value) => value !== undefined && value.search(/var\s*\(\s*--/i) !== -1; /** * @param {string[]} values * @return {boolean} */ function canMergeValues(values) { return !values.some(isValueCustomProp); } /** * @param {import('postcss').Declaration} decl * @return {string} */ function getColorValue(decl) { if (decl.prop.substr(-5) === 'color') { return decl.value; } return parseWsc(decl.value)[2] || defaults[2]; } /** * @param {[string, string, string]} values * @param {[string, string, string]} nextValues * @return {string[]} */ function diffingProps(values, nextValues) { return wsc.reduce((prev, curr, i) => { if (values[i] === nextValues[i]) { return prev; } return [...prev, curr]; }, /** @type {string[]} */ ([])); } /** * @param {{values: [string, string, string], nextValues: [string, string, string], decl: import('postcss').Declaration, nextDecl: import('postcss').Declaration, index: number}} arg * @return {void} */ function mergeRedundant({ values, nextValues, decl, nextDecl, index }) { if (!canMerge([decl, nextDecl])) { return; } if (stylehacks.detect(decl) || stylehacks.detect(nextDecl)) { return; } const diff = diffingProps(values, nextValues); if (diff.length !== 1) { return; } const prop = /** @type {string} */ (diff.pop()); const position = wsc.indexOf(prop); const prop1 = `${nextDecl.prop}-${prop}`; const prop2 = `border-${prop}`; let props = parseTrbl(values[position]); props[index] = nextValues[position]; const borderValue2 = values.filter((e, i) => i !== position).join(' '); const propValue2 = minifyTrbl(props); const origLength = (minifyWsc(decl.value) + nextDecl.prop + nextDecl.value) .length; const newLength1 = decl.value.length + prop1.length + minifyWsc(nextValues[position]).length; const newLength2 = borderValue2.length + prop2.length + propValue2.length; if (newLength1 < newLength2 && newLength1 < origLength) { nextDecl.prop = prop1; nextDecl.value = nextValues[position]; } if (newLength2 < newLength1 && newLength2 < origLength) { decl.value = borderValue2; nextDecl.prop = prop2; nextDecl.value = propValue2; } } /** * @param {string | string[]} mapped * @return {boolean} */ function isCloseEnough(mapped) { return ( (mapped[0] === mapped[1] && mapped[1] === mapped[2]) || (mapped[1] === mapped[2] && mapped[2] === mapped[3]) || (mapped[2] === mapped[3] && mapped[3] === mapped[0]) || (mapped[3] === mapped[0] && mapped[0] === mapped[1]) ); } /** * @param {string[]} mapped * @return {string[]} */ function getDistinctShorthands(mapped) { return [...new Set(mapped)]; } /** * @param {import('postcss').Rule} rule * @return {void} */ function explode(rule) { rule.walkDecls(/^border/i, (decl) => { if (!canExplode(decl, false)) { return; } if (stylehacks.detect(decl)) { return; } const prop = decl.prop.toLowerCase(); // border -> border-trbl if (prop === 'border') { if (isValidWsc(parseWsc(decl.value))) { directions.forEach((direction) => { insertCloned( /** @type {import('postcss').Rule} */ (decl.parent), decl, { prop: direction } ); }); decl.remove(); } } // border-trbl -> border-trbl-wsc if (directions.some((direction) => prop === direction)) { let values = parseWsc(decl.value); if (isValidWsc(values)) { wsc.forEach((d, i) => { insertCloned( /** @type {import('postcss').Rule} */ (decl.parent), decl, { prop: `${prop}-${d}`, value: values[i] || defaults[i], } ); }); decl.remove(); } } // border-wsc -> border-trbl-wsc wsc.some((style) => { if (prop !== borderProperty(style)) { return false; } if (isCustomProp(decl)) { decl.prop = decl.prop.toLowerCase(); return false; } parseTrbl(decl.value).forEach((value, i) => { insertCloned( /** @type {import('postcss').Rule} */ (decl.parent), decl, { prop: borderProperty(trbl[i], style), value, } ); }); return decl.remove(); }); }); } /** * @param {import('postcss').Rule} rule * @return {void} */ function merge(rule) { // border-trbl-wsc -> border-trbl trbl.forEach((direction) => { const prop = borderProperty(direction); mergeRules( rule, wsc.map((style) => borderProperty(direction, style)), (rules, lastNode) => { if (canMerge(rules, false) && !rules.some(stylehacks.detect)) { insertCloned( /** @type {import('postcss').Rule} */ (lastNode.parent), lastNode, { prop, value: rules.map(getValue).join(' '), } ); for (const node of rules) { node.remove(); } return true; } return false; } ); }); // border-trbl-wsc -> border-wsc wsc.forEach((style) => { const prop = borderProperty(style); mergeRules( rule, trbl.map((direction) => borderProperty(direction, style)), (rules, lastNode) => { if (canMerge(rules) && !rules.some(stylehacks.detect)) { insertCloned( /** @type {import('postcss').Rule} */ (lastNode.parent), lastNode, { prop, value: minifyTrbl(rules.map(getValue).join(' ')), } ); for (const node of rules) { node.remove(); } return true; } return false; } ); }); // border-trbl -> border-wsc mergeRules(rule, directions, (rules, lastNode) => { if (rules.some(stylehacks.detect)) { return false; } const values = rules.map(({ value }) => value); if (!canMergeValues(values)) { return false; } const parsed = values.map((value) => parseWsc(value)); if (!parsed.every(isValidWsc)) { return false; } wsc.forEach((d, i) => { const value = parsed.map((v) => v[i] || defaults[i]); if (canMergeValues(value)) { insertCloned( /** @type {import('postcss').Rule} */ (lastNode.parent), lastNode, { prop: borderProperty(d), value: minifyTrbl( /** @type {[string, string, string, string]} */ (value) ), } ); } else { insertCloned( /** @type {import('postcss').Rule} */ (lastNode.parent), lastNode ); } }); for (const node of rules) { node.remove(); } return true; }); // border-wsc -> border // border-wsc -> border + border-color // border-wsc -> border + border-dir mergeRules(rule, properties, (rules, lastNode) => { if (rules.some(stylehacks.detect)) { return false; } const values = rules.map((node) => parseTrbl(node.value)); const mapped = [0, 1, 2, 3].map((i) => [values[0][i], values[1][i], values[2][i]].join(' ') ); if (!canMergeValues(mapped)) { return false; } const [width, style, color] = rules; const reduced = getDistinctShorthands(mapped); if (isCloseEnough(mapped) && canMerge(rules, false)) { const first = mapped.indexOf(reduced[0]) !== mapped.lastIndexOf(reduced[0]); const border = insertCloned( /** @type {import('postcss').Rule} */ (lastNode.parent), lastNode, { prop: 'border', value: first ? reduced[0] : reduced[1], } ); if (reduced[1]) { const value = first ? reduced[1] : reduced[0]; const prop = borderProperty(trbl[mapped.indexOf(value)]); rule.insertAfter( border, Object.assign(lastNode.clone(), { prop, value, }) ); } for (const node of rules) { node.remove(); } return true; } else if (reduced.length === 1) { rule.insertBefore( color, Object.assign(lastNode.clone(), { prop: 'border', value: [width, style].map(getValue).join(' '), }) ); rules .filter((node) => node.prop.toLowerCase() !== properties[2]) .forEach((node) => node.remove()); return true; } return false; }); // border-wsc -> border + border-trbl mergeRules(rule, properties, (rules, lastNode) => { if (rules.some(stylehacks.detect)) { return false; } const values = rules.map((node) => parseTrbl(node.value)); const mapped = [0, 1, 2, 3].map((i) => [values[0][i], values[1][i], values[2][i]].join(' ') ); const reduced = getDistinctShorthands(mapped); const none = 'medium none currentcolor'; if (reduced.length > 1 && reduced.length < 4 && reduced.includes(none)) { const filtered = mapped.filter((p) => p !== none); const mostCommon = reduced.sort( (a, b) => mapped.filter((v) => v === b).length - mapped.filter((v) => v === a).length )[0]; const borderValue = reduced.length === 2 ? filtered[0] : mostCommon; rule.insertBefore( lastNode, Object.assign(lastNode.clone(), { prop: 'border', value: borderValue, }) ); directions.forEach((dir, i) => { if (mapped[i] !== borderValue) { rule.insertBefore( lastNode, Object.assign(lastNode.clone(), { prop: dir, value: mapped[i], }) ); } }); for (const node of rules) { node.remove(); } return true; } return false; }); // border-trbl -> border // border-trbl -> border + border-trbl mergeRules(rule, directions, (rules, lastNode) => { if (rules.some(stylehacks.detect)) { return false; } const values = rules.map((node) => { const wscValue = parseWsc(node.value); if (!isValidWsc(wscValue)) { return node.value; } return wscValue.map((value, i) => value || defaults[i]).join(' '); }); const reduced = getDistinctShorthands(values); if (isCloseEnough(values)) { const first = values.indexOf(reduced[0]) !== values.lastIndexOf(reduced[0]); rule.insertBefore( lastNode, Object.assign(lastNode.clone(), { prop: 'border', value: minifyWsc(first ? values[0] : values[1]), }) ); if (reduced[1]) { const value = first ? reduced[1] : reduced[0]; const prop = directions[values.indexOf(value)]; rule.insertBefore( lastNode, Object.assign(lastNode.clone(), { prop: prop, value: minifyWsc(value), }) ); } for (const node of rules) { node.remove(); } return true; } return false; }); // border-trbl-wsc + border-trbl (custom prop) -> border-trbl + border-trbl-wsc (custom prop) directions.forEach((direction) => { wsc.forEach((style, i) => { const prop = `${direction}-${style}`; mergeRules(rule, [direction, prop], (rules, lastNode) => { if (lastNode.prop !== direction) { return false; } const values = parseWsc(lastNode.value); if (!isValidWsc(values)) { return false; } const wscProp = rules.filter((r) => r !== lastNode)[0]; if (!isValueCustomProp(values[i]) || isCustomProp(wscProp)) { return false; } const wscValue = values[i]; values[i] = wscProp.value; if (canMerge(rules, false) && !rules.some(stylehacks.detect)) { insertCloned( /** @type {import('postcss').Rule} */ (lastNode.parent), lastNode, { prop, value: wscValue, } ); lastNode.value = minifyWsc(/** @type {any} */ (values)); wscProp.remove(); return true; } return false; }); }); }); // border-wsc + border (custom prop) -> border + border-wsc (custom prop) wsc.forEach((style, i) => { const prop = borderProperty(style); mergeRules(rule, ['border', prop], (rules, lastNode) => { if (lastNode.prop !== 'border') { return false; } const values = parseWsc(lastNode.value); if (!isValidWsc(values)) { return false; } const wscProp = rules.filter((r) => r !== lastNode)[0]; if (!isValueCustomProp(values[i]) || isCustomProp(wscProp)) { return false; } const wscValue = values[i]; values[i] = wscProp.value; if (canMerge(rules, false) && !rules.some(stylehacks.detect)) { insertCloned( /** @type {import('postcss').Rule} */ (lastNode.parent), lastNode, { prop, value: wscValue, } ); lastNode.value = minifyWsc(/** @type {any} */ (values)); wscProp.remove(); return true; } return false; }); }); // optimize border-trbl let decls = getDecls(rule, directions); while (decls.length) { const lastNode = decls[decls.length - 1]; wsc.forEach((d, i) => { const names = directions .filter((name) => name !== lastNode.prop) .map((name) => `${name}-${d}`); let nodes = rule.nodes.slice(0, rule.nodes.indexOf(lastNode)); const border = getLastNode(nodes, 'border'); if (border) { nodes = nodes.slice(nodes.indexOf(border)); } const props = nodes.filter( (node) => node.type === 'decl' && names.includes(node.prop) && node.important === lastNode.important ); const rules = getRules( /** @type {import('postcss').Declaration[]} */ (props), names ); if (hasAllProps(rules, ...names) && !rules.some(stylehacks.detect)) { const values = rules.map((node) => (node ? node.value : null)); const filteredValues = values.filter(Boolean); const lastNodeValue = list.space(lastNode.value)[i]; values[directions.indexOf(lastNode.prop)] = lastNodeValue; let value = minifyTrbl(values.join(' ')); if ( filteredValues[0] === filteredValues[1] && filteredValues[1] === filteredValues[2] ) { value = /** @type {string} */ (filteredValues[0]); } let refNode = props[props.length - 1]; if (value === lastNodeValue) { refNode = lastNode; let valueArray = list.space(lastNode.value); valueArray.splice(i, 1); lastNode.value = valueArray.join(' '); } insertCloned( /** @type {import('postcss').Rule} */ (refNode.parent), /** @type {import('postcss').Declaration} */ (refNode), { prop: borderProperty(d), value, } ); decls = decls.filter((node) => !rules.includes(node)); for (const node of rules) { node.remove(); } } }); decls = decls.filter((node) => node !== lastNode); } rule.walkDecls('border', (decl) => { const nextDecl = decl.next(); if (!nextDecl || nextDecl.type !== 'decl') { return false; } const index = directions.indexOf(nextDecl.prop); if (index === -1) { return; } const values = parseWsc(decl.value); const nextValues = parseWsc(nextDecl.value); if (!isValidWsc(values) || !isValidWsc(nextValues)) { return; } const config = { values, nextValues, decl, nextDecl, index, }; return mergeRedundant(config); }); rule.walkDecls(/^border($|-(top|right|bottom|left)$)/i, (decl) => { let values = parseWsc(decl.value); if (!isValidWsc(values)) { return; } const position = directions.indexOf(decl.prop); let dirs = [...directions]; dirs.splice(position, 1); wsc.forEach((d, i) => { const props = dirs.map((dir) => `${dir}-${d}`); mergeRules(rule, [decl.prop, ...props], (rules) => { if (!rules.includes(decl)) { return false; } const longhands = rules.filter((p) => p !== decl); if ( longhands[0].value.toLowerCase() === longhands[1].value.toLowerCase() && longhands[1].value.toLowerCase() === longhands[2].value.toLowerCase() && values[i] !== undefined && longhands[0].value.toLowerCase() === values[i].toLowerCase() ) { for (const node of longhands) { node.remove(); } insertCloned( /** @type {import('postcss').Rule} */ (decl.parent), decl, { prop: borderProperty(d), value: values[i], } ); /** @type {string|null} */ (values[i]) = null; } return false; }); const newValue = values.join(' '); if (newValue) { decl.value = newValue; } else { decl.remove(); } }); }); // clean-up values rule.walkDecls(/^border($|-(top|right|bottom|left)$)/i, (decl) => { decl.value = minifyWsc(decl.value); }); // border-spacing-hv -> border-spacing rule.walkDecls(/^border-spacing$/i, (decl) => { const value = list.space(decl.value); // merge vertical and horizontal dups if (value.length > 1 && value[0] === value[1]) { decl.value = value.slice(1).join(' '); } }); // clean-up rules decls = getDecls(rule, allProperties); while (decls.length) { const lastNode = decls[decls.length - 1]; const lastPart = lastNode.prop.split('-').pop(); // remove properties of lower precedence const lesser = decls.filter( (node) => !stylehacks.detect(lastNode) && !stylehacks.detect(node) && !isCustomProp(lastNode) && node !== lastNode && node.important === lastNode.important && /** @type {number} */ (getLevel(node.prop)) > /** @type {number} */ (getLevel(lastNode.prop)) && (node.prop.toLowerCase().includes(lastNode.prop) || node.prop.toLowerCase().endsWith(/** @type {string} */ (lastPart))) ); for (const node of lesser) { node.remove(); } decls = decls.filter((node) => !lesser.includes(node)); // get duplicate properties let duplicates = decls.filter( (node) => !stylehacks.detect(lastNode) && !stylehacks.detect(node) && node !== lastNode && node.important === lastNode.important && node.prop === lastNode.prop && !(!isCustomProp(node) && isCustomProp(lastNode)) ); if (duplicates.length) { if (colorMightRequireFallback.test(getColorValue(lastNode))) { const preserve = duplicates .filter( (node) => !colorMightRequireFallback.test(getColorValue(node)) ) .pop(); duplicates = duplicates.filter((node) => node !== preserve); } for (const node of duplicates) { node.remove(); } } decls = decls.filter( (node) => node !== lastNode && !duplicates.includes(node) ); } } module.exports = { explode, merge, }; src/lib/mergeValues.js000066600000000275150441765100010731 0ustar00'use strict'; const getValue = require('./getValue.js'); /** @type {(...rules: import('postcss').Declaration[]) => string} */ module.exports = (...rules) => rules.map(getValue).join(' '); src/lib/canMerge.js000066600000002024150441765100010165 0ustar00'use strict'; const isCustomProp = require('./isCustomProp'); /** @type {(node: import('postcss').Declaration) => boolean} */ const important = (node) => node.important; /** @type {(node: import('postcss').Declaration) => boolean} */ const unimportant = (node) => !node.important; /* Cannot be combined with other values in shorthand https://www.w3.org/TR/css-cascade-5/#shorthand */ const cssWideKeywords = ['inherit', 'initial', 'unset', 'revert']; /** * @type {(props: import('postcss').Declaration[], includeCustomProps?: boolean) => boolean} */ module.exports = (props, includeCustomProps = true) => { const uniqueProps = new Set(props.map((node) => node.value.toLowerCase())); if (uniqueProps.size > 1) { for (const unmergeable of cssWideKeywords) { if (uniqueProps.has(unmergeable)) { return false; } } } if ( includeCustomProps && props.some(isCustomProp) && !props.every(isCustomProp) ) { return false; } return props.every(unimportant) || props.every(important); }; src/lib/parseWsc.js000066600000003527150441765100010244 0ustar00'use strict'; const { list } = require('postcss'); const { isWidth, isStyle, isColor } = require('./validateWsc.js'); const none = /^\s*(none|medium)(\s+none(\s+(none|currentcolor))?)?\s*$/i; /* Approximate https://drafts.csswg.org/css-values-4/#typedef-dashed-ident */ // eslint-disable-next-line no-control-regex const varRE = /--(\w|-|[^\x00-\x7F])+/g; /** @type {(v: string) => string} */ const toLower = (v) => { let match; let lastIndex = 0; let result = ''; varRE.lastIndex = 0; while ((match = varRE.exec(v)) !== null) { if (match.index > lastIndex) { result += v.substring(lastIndex, match.index).toLowerCase(); } result += match[0]; lastIndex = match.index + match[0].length; } if (lastIndex < v.length) { result += v.substring(lastIndex).toLowerCase(); } if (result === '') { return v; } return result; }; /** * @param {string} value * @return {[string, string, string]} */ module.exports = function parseWsc(value) { if (none.test(value)) { return ['medium', 'none', 'currentcolor']; } let width, style, color; const values = list.space(value); if ( values.length > 1 && isStyle(values[1]) && values[0].toLowerCase() === 'none' ) { values.unshift(); width = '0'; } /** @type {string[]} */ const unknown = []; values.forEach((v) => { if (isStyle(v)) { style = toLower(v); } else if (isWidth(v)) { width = toLower(v); } else if (isColor(v)) { color = toLower(v); } else { unknown.push(v); } }); if (unknown.length) { if (!width && style && color) { width = unknown.pop(); } if (width && !style && color) { style = unknown.pop(); } if (width && style && !color) { color = unknown.pop(); } } return /** @type {[string, string, string]} */ ([width, style, color]); }; src/lib/getRules.js000066600000000567150441765100010250 0ustar00'use strict'; const getLastNode = require('./getLastNode.js'); /** * @param {import('postcss').Declaration[]} props * @param {string[]} properties * @return {import('postcss').Declaration[]} */ module.exports = function getRules(props, properties) { return properties .map((property) => { return getLastNode(props, property); }) .filter(Boolean); }; src/lib/trbl.js000066600000000103150441765100007403 0ustar00'use strict'; module.exports = ['top', 'right', 'bottom', 'left']; src/lib/minifyWsc.js000066600000001334150441765100010417 0ustar00'use strict'; const parseWsc = require('./parseWsc.js'); const minifyTrbl = require('./minifyTrbl.js'); const { isValidWsc } = require('./validateWsc.js'); const defaults = ['medium', 'none', 'currentcolor']; /** @type {(v: string) => string} */ module.exports = (v) => { const values = parseWsc(v); if (!isValidWsc(values)) { return minifyTrbl(v); } const value = [...values, ''] .reduceRight((prev, cur, i, arr) => { if ( cur === undefined || (cur.toLowerCase() === defaults[i] && (!i || (arr[i - 1] || '').toLowerCase() !== cur.toLowerCase())) ) { return prev; } return cur + ' ' + prev; }) .trim(); return minifyTrbl(value || 'none'); }; src/lib/minifyTrbl.js000066600000000613150441765100010565 0ustar00'use strict'; const parseTrbl = require('./parseTrbl.js'); /** @type {(v: string | [string, string, string, string]) => string} */ module.exports = (v) => { const value = parseTrbl(v); if (value[3] === value[1]) { value.pop(); if (value[2] === value[0]) { value.pop(); if (value[0] === value[1]) { value.pop(); } } } return value.join(' '); }; src/lib/getDecls.js000066600000000576150441765100010210 0ustar00'use strict'; /** * @param {import('postcss').Rule} rule * @param {string[]} properties * @return {import('postcss').Declaration[]} */ module.exports = function getDecls(rule, properties) { return /** @type {import('postcss').Declaration[]} */ ( rule.nodes.filter( (node) => node.type === 'decl' && properties.includes(node.prop.toLowerCase()) ) ); }; src/lib/insertCloned.js000066600000000603150441765100011076 0ustar00'use strict'; /** * @param {import('postcss').Rule} rule * @param {import('postcss').Declaration} decl * @param {Partial=} props * @return {import('postcss').Declaration} */ module.exports = function insertCloned(rule, decl, props) { const newNode = Object.assign(decl.clone(), props); rule.insertAfter(decl, newNode); return newNode; }; src/lib/colornames.js000066600000004405150441765100010613 0ustar00'use strict'; /* https://www.w3.org/TR/css-color-4/#named-colors */ module.exports = new Set([ 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkgrey', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'greenyellow', 'grey', 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', 'magenta', 'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', 'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen', ]); types/lib/validateWsc.d.ts000066600000000701150441765100011523 0ustar00/** * @param {string} value * @return {boolean} */ export function isStyle(value: string): boolean; /** * @param {string} value * @return {boolean} */ export function isWidth(value: string): boolean; /** * @param {string} value * @return {boolean} */ export function isColor(value: string): boolean; /** * @param {[string, string, string]} wscs * @return {boolean} */ export function isValidWsc(wscs: [string, string, string]): boolean; types/lib/canExplode.d.ts000066600000000173150441765100011342 0ustar00declare const _exports: (prop: import('postcss').Declaration, includeCustomProps?: boolean) => boolean; export = _exports; types/lib/canMerge.d.ts000066600000000176150441765100011004 0ustar00declare const _exports: (props: import('postcss').Declaration[], includeCustomProps?: boolean) => boolean; export = _exports; types/lib/isCustomProp.d.ts000066600000000135150441765100011725 0ustar00declare const _exports: (node: import('postcss').Declaration) => boolean; export = _exports; types/lib/colornames.d.ts000066600000000070150441765100011416 0ustar00declare const _exports: Set; export = _exports; types/lib/remove.d.ts000066600000000144150441765100010553 0ustar00declare function _exports(node: import('postcss').Node): import('postcss').Node; export = _exports; types/lib/mergeValues.d.ts000066600000000142150441765100011533 0ustar00declare const _exports: (...rules: import('postcss').Declaration[]) => string; export = _exports; types/lib/parseWsc.d.ts000066600000000127150441765100011046 0ustar00declare function _exports(value: string): [string, string, string]; export = _exports; types/lib/insertCloned.d.ts000066600000000321150441765100011704 0ustar00declare function _exports(rule: import('postcss').Rule, decl: import('postcss').Declaration, props?: Partial | undefined): import('postcss').Declaration; export = _exports; types/lib/getLastNode.d.ts000066600000000177150441765100011475 0ustar00declare const _exports: (rule: import('postcss').AnyNode[], prop: string) => import('postcss').Declaration; export = _exports; types/lib/decl/boxBase.d.ts000066600000000251150441765100011547 0ustar00declare function _exports(prop: string): { explode: (rule: import('postcss').Rule) => void; merge: (rule: import('postcss').Rule) => void; }; export = _exports; types/lib/decl/columns.d.ts000066600000000374150441765100011652 0ustar00/** * @param {import('postcss').Rule} rule * @return {void} */ export function explode(rule: import('postcss').Rule): void; /** * @param {import('postcss').Rule} rule * @return {void} */ export function merge(rule: import('postcss').Rule): void; types/lib/decl/margin.d.ts000066600000000230150441765100011436 0ustar00declare const _exports: { explode: (rule: import("postcss").Rule) => void; merge: (rule: import("postcss").Rule) => void; }; export = _exports; types/lib/decl/borders.d.ts000066600000000374150441765100011632 0ustar00/** * @param {import('postcss').Rule} rule * @return {void} */ export function explode(rule: import('postcss').Rule): void; /** * @param {import('postcss').Rule} rule * @return {void} */ export function merge(rule: import('postcss').Rule): void; types/lib/decl/padding.d.ts000066600000000230150441765100011567 0ustar00declare const _exports: { explode: (rule: import("postcss").Rule) => void; merge: (rule: import("postcss").Rule) => void; }; export = _exports; types/lib/decl/index.d.ts000066600000000146150441765100011276 0ustar00declare const _exports: (typeof borders)[]; export = _exports; import borders = require("./borders"); types/lib/getRules.d.ts000066600000000215150441765100011047 0ustar00declare function _exports(props: import('postcss').Declaration[], properties: string[]): import('postcss').Declaration[]; export = _exports; types/lib/minifyWsc.d.ts000066600000000102150441765100011220 0ustar00declare const _exports: (v: string) => string; export = _exports; types/lib/mergeRules.d.ts000066600000000364150441765100011374 0ustar00declare function _exports(rule: import('postcss').Rule, properties: string[], callback: (rules: import('postcss').Declaration[], last: import('postcss').Declaration, props: import('postcss').Declaration[]) => boolean): void; export = _exports; types/lib/trbl.d.ts000066600000000065150441765100010223 0ustar00declare const _exports: string[]; export = _exports; types/lib/parseTrbl.d.ts000066600000000147150441765100011217 0ustar00declare const _exports: (v: string | string[]) => [string, string, string, string]; export = _exports; types/lib/minifyTrbl.d.ts000066600000000145150441765100011376 0ustar00declare const _exports: (v: string | [string, string, string, string]) => string; export = _exports; types/lib/getValue.d.ts000066600000000140150441765100011026 0ustar00declare function _exports({ value }: import('postcss').Declaration): string; export = _exports; types/lib/hasAllProps.d.ts000066600000000163150441765100011507 0ustar00declare const _exports: (rule: import('postcss').Declaration[], ...props: string[]) => boolean; export = _exports; types/lib/getDecls.d.ts000066600000000203150441765100011004 0ustar00declare function _exports(rule: import('postcss').Rule, properties: string[]): import('postcss').Declaration[]; export = _exports; types/index.d.ts000066600000000360150441765100007617 0ustar00export = pluginCreator; /** * @type {import('postcss').PluginCreator} * @return {import('postcss').Plugin} */ declare function pluginCreator(): import('postcss').Plugin; declare namespace pluginCreator { const postcss: true; } LICENSE-MIT000066600000002104150441765100006204 0ustar00Copyright (c) Ben Briggs (http://beneb.info) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package.json000066600000001576150441765100007052 0ustar00{ "name": "postcss-merge-longhand", "version": "5.1.7", "description": "Merge longhand properties into shorthand with PostCSS.", "main": "src/index.js", "types": "types/index.d.ts", "files": [ "LICENSE-MIT", "src", "types" ], "keywords": [ "css", "minify", "optimise", "postcss", "postcss-plugin" ], "license": "MIT", "homepage": "https://github.com/cssnano/cssnano", "author": { "name": "Ben Briggs", "email": "beneb.info@gmail.com", "url": "http://beneb.info" }, "repository": "cssnano/cssnano", "dependencies": { "postcss-value-parser": "^4.2.0", "stylehacks": "^5.1.1" }, "bugs": { "url": "https://github.com/cssnano/cssnano/issues" }, "engines": { "node": "^10 || ^12 || >=14.0" }, "devDependencies": { "postcss": "^8.2.15" }, "peerDependencies": { "postcss": "^8.2.15" } }