Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(postcss-colormin): Output 4 and 8 digit hex colors #1109

Merged
merged 13 commits into from May 28, 2021
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -79,6 +79,9 @@
"chrome58": [
"Chrome 58"
],
"chrome62": [
"Chrome 62"
],
"edge15": [
"Edge 15"
],
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/postcss-colormin/package.json
Expand Up @@ -31,6 +31,7 @@
"repository": "cssnano/cssnano",
"dependencies": {
"browserslist": "^4.16.6",
"caniuse-api": "^3.0.0",
"colord": "^2.0.0",
"postcss-value-parser": "^4.1.0"
},
Expand Down
20 changes: 17 additions & 3 deletions packages/postcss-colormin/src/__tests__/index.js
Expand Up @@ -204,7 +204,7 @@ test(
test(
'should not mangle percentage based rgba values',
processCSS(
'h1{color:rgba(50%,50%,50%,0.5)}',
'h1{color:rgba(50%, 50%, 50%, 0.5)}',
'h1{color:hsla(0, 0%, 50%, 0.5)}'
)
);
Expand All @@ -223,15 +223,15 @@ test(
'should add extra spaces when converting rgb',
processCSS(
'h1{background:linear-gradient(rgb(50, 50, 50)0%,blue 100%)}',
'h1{background:linear-gradient(#323232 0%,#00f 100%)}'
'h1{background:linear-gradient(#323232 0%,blue 100%)}'
)
);

test(
'should add extra spaces when converting rgb (2)',
processCSS(
'h1{background:linear-gradient(rgba(0,0,0,0)0%, blue 100%)}',
'h1{background:linear-gradient(transparent 0%, #00f 100%)}'
'h1{background:linear-gradient(transparent 0%, blue 100%)}'
)
);

Expand Down Expand Up @@ -321,3 +321,17 @@ test(
'should respect CSS variables',
passthroughCSS('div{background-color:rgba(51,153,255,var(--tw-bg-opacity))}')
);

test(
'should convert long color to 8-digit hex when supported',
processCSS('h1{color:rgba(100% 50% 0% / 50%)}', 'h1{color:#ff800080}', {
env: 'chrome62',
})
);

test(
'should convert long color to 4-digit hex when supported',
processCSS('h1{color:hsla(0 100% 50% / 40%)}', 'h1{color:#f006}', {
env: 'chrome62',
})
);
@@ -1,4 +1,4 @@
import min from '../colours';
import min from '../minifyColor';

function isEqual(input, output) {
return () => expect(min(input)).toBe(output);
Expand All @@ -20,7 +20,7 @@ test(
test('should convert hsl to keyword', isEqual('hsl(0, 100%, 50%)', 'red'));

test(
'should convert fully oqaque hsl to keyword',
'should convert fully opaque hsl to keyword',
isEqual('hsla(0, 100%, 50%, 1)', 'red')
);

Expand Down Expand Up @@ -94,7 +94,7 @@ test(

test(
'should convert signed numbers (2)',
isEqual('hsla(-400,50%,10%,.5)', 'rgba(38, 13, 30, 0.5)')
isEqual('hsla(-400, 50%, 10%, 0.5)', 'rgba(38, 13, 30, 0.5)')
);

test(
Expand All @@ -104,7 +104,7 @@ test(

test(
'should convert percentage based rgba values (2)',
isEqual('rgba(50%,50%,50%,0.5)', 'hsla(0, 0%, 50%, 0.5)')
isEqual('rgba(50%, 50%, 50%, 0.5)', 'hsla(0, 0%, 50%, 0.5)')
);

test(
Expand All @@ -119,7 +119,7 @@ test(

test(
'should convert percentage based rgba values (5)',
isEqual('rgba(100%,64.7%,0%,.5)', 'rgba(255, 165, 0, 0.5)')
isEqual('rgba(100%, 64.7%, 0%, .5)', 'rgba(255, 165, 0, 0.5)')
);

test(
Expand All @@ -144,3 +144,19 @@ test('should pass through if not recognised', () => {
expect(min('Unrecognised')).toBe('Unrecognised');
expect(min('inherit')).toBe('inherit');
});

test('should convert to hex4', () => {
expect(min('#aabbcc33', { supportsAlphaHex: true })).toBe('#abc3');
expect(min('transparent', { supportsAlphaHex: true })).toBe('#0000');
expect(min('rgb(119,119,119,0.2)', { supportsAlphaHex: true })).toBe('#7773');
expect(min('hsla(0,0%,100%,.4)', { supportsAlphaHex: true })).toBe('#fff6');
});

test('should convert to hex8', () => {
expect(min('rgba(128, 128, 128, 0.5)', { supportsAlphaHex: true })).toBe(
'#80808080'
);
expect(min('hsla(180, 100%, 50%, 0.5)', { supportsAlphaHex: true })).toBe(
'#00ffff80'
);
});
45 changes: 0 additions & 45 deletions packages/postcss-colormin/src/colours.js

This file was deleted.

20 changes: 13 additions & 7 deletions packages/postcss-colormin/src/index.js
@@ -1,6 +1,7 @@
import browserslist from 'browserslist';
import { isSupported } from 'caniuse-api';
import valueParser, { stringify } from 'postcss-value-parser';
import colormin from './colours';
import minifyColor from './minifyColor';

function walk(parent, callback) {
parent.nodes.forEach((node, index) => {
Expand Down Expand Up @@ -31,15 +32,15 @@ function isMathFunctionNode(node) {
return ['calc', 'min', 'max', 'clamp'].includes(node.value.toLowerCase());
}

function transform(value, isLegacy) {
function transform(value, options) {
const parsed = valueParser(value);

walk(parsed, (node, index, parent) => {
if (node.type === 'function') {
if (/^(rgb|hsl)a?$/i.test(node.value)) {
const { value: originalValue } = node;

node.value = colormin(stringify(node), isLegacy);
node.value = minifyColor(stringify(node), options);
node.type = 'word';

const next = parent.nodes[index + 1];
Expand All @@ -58,7 +59,7 @@ function transform(value, isLegacy) {
return false;
}
} else if (node.type === 'word') {
node.value = colormin(node.value, isLegacy);
node.value = minifyColor(node.value, options);
}
});

Expand All @@ -76,7 +77,12 @@ function pluginCreator() {
path: __dirname,
env: resultOpts.env,
});
const isLegacy = browsers.some(hasTransparentBug);

const options = {
supportsTransparent: browsers.some(hasTransparentBug) === false,
supportsAlphaHex: isSupported('css-rrggbbaa', browsers),
};

const cache = {};

return {
Expand All @@ -96,15 +102,15 @@ function pluginCreator() {
return;
}

const cacheKey = value + '|' + isLegacy;
const cacheKey = JSON.stringify({ value, options, browsers });

if (cache[cacheKey]) {
decl.value = cache[cacheKey];

return;
}

const newValue = transform(value, isLegacy);
const newValue = transform(value, options);

decl.value = newValue;
cache[cacheKey] = newValue;
Expand Down
67 changes: 67 additions & 0 deletions packages/postcss-colormin/src/lib/color.js
@@ -0,0 +1,67 @@
import { colord, extend, getFormat } from 'colord';
import namesPlugin from 'colord/plugins/names';
import getShortestString from './getShortestString';

let minifierPlugin = (Colord) => {
/**
* Shortens a color to 3 or 4 digit hexadecimal string if it's possible.
* Returns the original (6 or 8 digit) hex if the it can't be shortened.
*/
Colord.prototype.toShortHex = function ({ formatAlpha }) {
omgovich marked this conversation as resolved.
Show resolved Hide resolved
let hex = this.toHex();
let [, r1, r2, g1, g2, b1, b2, a1, a2] = hex.split('');

// Check if the string can be shorten
if (r1 === r2 && g1 === g2 && b1 === b2) {
if (this.alpha() === 1) {
// Express as 3 digit hexadecimal string if the color doesn't have an alpha channel
return '#' + r1 + g1 + b1;
} else if (formatAlpha && a1 === a2) {
// Format 4 digit hex
return '#' + r1 + g1 + b1 + a1;
}
}

return hex;
};

/**
* Returns the shortest representation of a color.
*/
Colord.prototype.toShortString = function ({
supportsTransparent,
supportsAlphaHex,
}) {
let { r, g, b, a } = this.toRgb();

// RGB[A] and HSL[A] functional notations
let options = [
this.toRgbString(), // e.g. "rgb(128, 128, 128)" or "rgba(128, 128, 128, 0.5)"
this.toHslString(), // e.g. "hsl(180, 50%, 50%)" or "hsla(180, 50%, 50%, 0.5)"
];

// Hexadecimal notations
if (supportsAlphaHex && a < 1) {
options.push(this.toShortHex({ formatAlpha: true })); // e.g. "#7777" or "#80808080"
} else if (a === 1) {
options.push(this.toShortHex({ formatAlpha: false })); // e.g. "#777" or "#808080"
}

// CSS keyword
if (supportsTransparent && r === 0 && g === 0 && b === 0 && a === 0) {
options.push('transparent');
} else if (a === 1) {
let name = this.toName(); // e.g. "gray"
if (name) {
options.push(name);
}
}

// Find the shortest option available
return getShortestString(options);
};
};

extend([namesPlugin, minifierPlugin]);

export { colord as process, getFormat };
16 changes: 16 additions & 0 deletions packages/postcss-colormin/src/lib/getShortestString.js
@@ -0,0 +1,16 @@
/**
* Returns the shortest string in array
*/
const getShortestString = (strings) => {
let shortest = null;

for (let string of strings) {
if (shortest === null || string.length < shortest.length) {
shortest = string;
}
}

return shortest;
};

export default getShortestString;
7 changes: 0 additions & 7 deletions packages/postcss-colormin/src/lib/toShorthand.js

This file was deleted.

31 changes: 31 additions & 0 deletions packages/postcss-colormin/src/minifyColor.js
@@ -0,0 +1,31 @@
import { process } from './lib/color';
import getShortestString from './lib/getShortestString';

/**
* Performs color value minification
*
* @param {string} input - CSS value
* @param {boolean} options.supportsAlphaHex - Does the browser support 4 & 8 character hex notation
* @param {boolean} options.supportsTransparent – Does the browser support "transparent" value properly
*/
export default function minifyColor(input, options = {}) {
const settings = {
supportsAlphaHex: false,
supportsTransparent: true,
...options,
};

const instance = process(input);

if (instance.isValid()) {
// Try to shorten the string if it is a valid CSS color value.
// Fall back to the original input if it's smaller or has equal length/
return getShortestString([
input.toLowerCase(),
instance.toShortString(settings),
]);
} else {
// Possibly malformed, so pass through
return input;
}
}
22 changes: 21 additions & 1 deletion packages/postcss-colormin/yarn.lock
Expand Up @@ -13,7 +13,17 @@ browserslist@^4.16.6:
escalade "^3.1.1"
node-releases "^1.1.71"

caniuse-lite@^1.0.30001219:
caniuse-api@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
dependencies:
browserslist "^4.0.0"
caniuse-lite "^1.0.0"
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"

caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001219:
version "1.0.30001230"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz#8135c57459854b2240b57a4a6786044bdc5a9f71"
integrity sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ==
Expand All @@ -38,6 +48,16 @@ escalade@^3.1.1:
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==

lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=

lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=

nanoid@^3.1.23:
version "3.1.23"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
Expand Down