Browse Source

Initial commit

master
Dustin Wilson 5 years ago
commit
bb2c7e27df
  1. 40
      .gitignore
  2. 22
      README.md
  3. 233
      index.js
  4. 14
      package.json
  5. 111
      yarn.lock

40
.gitignore

@ -0,0 +1,40 @@
# npm
node_modules
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# =========================
# Operating System Files
# =========================
# MacOS
# =========================
.DS_Store
.AppleDouble
.LSOverride
# Icon must ends with two \r.
Icon
# Thumbnails
._*
# Files that might appear on external disk
.Spotlight-V100
.Trashes

22
README.md

@ -0,0 +1,22 @@
# postcss-color-contrast-ratio
[a]: https://postcss.org
[b]: https://www.w3.org/TR/WCAG/#contrast-minimum
[PostCSS][a] plugin which allows for creation of colors based upon [WCAG contrast ratios][b].
In:
```css
body {
color: crab(5, 0, 0);
}
```
Out:
```css
body {
color: #717171;
}
```
This readme is a stub. It will contain more information in the future.

233
index.js

@ -0,0 +1,233 @@
const postcss = require('postcss');
const parser = require('postcss-values-parser');
const colorString = require('color-string');
module.exports = postcss.plugin('postcss-color-contrast-ratio', () => {
/* Yes there are color libraries for JavaScript, but haven't found any that does
color conversion from CIELAB to RGB correctly.
CIELAB and CIEXYZ need to use the D50 standard illuminant per the CSS color
specification (and because Photoshop uses it, too), and most libs will use
the illuminant of the input color. In the case of sRGB that'd be D65, and that's
incorrect. */
const lab2xyz = (() => {
const D50 = [ 0.96422, 1, 0.82521 ];
function f(t) {
return (t > 6 / 29) ? t ** 3 : 3 * (6 / 29) ** 2 * (t - 4 / 29);
}
return function lab2xyz([ l, a, b ]) {
const fl = (l + 16) / 116;
const fa = a / 500;
const fb = b / 200;
return [
D50[0] * f(fl + fa),
D50[1] * f(fl),
D50[2] * f(fl - fb)
];
};
})();
const xyz2rgb = (() => {
function sRGBCompanding(v) {
return (v <= 0.0031308) ? 12.92 * v : 1.055 * (v ** (1 / 2.4)) - 0.055;
}
// Conversion matrix from D50 to D65. Normally you'd go through a chromatic
// adaptation algorithm here then convert to RGB, but I pulled the result matrix
// from http://www.brucelindbloom.com/Eqn_RGB_XYZ_Matrix.html instead.
const sRGBD50toD65Matrix = [
3.1338561, -1.6168667, -0.4906146,
-0.9787684, 1.9161415, 0.0334540,
0.0719453, -0.2289914, 1.4052427
];
return function xyz2rgb([ x, y, z ]) {
let lrgb = matrixMultiplyVector(sRGBD50toD65Matrix, [x, y, z]);
return [
Math.min(Math.max(0, sRGBCompanding(lrgb[0])), 1),
Math.min(Math.max(0, sRGBCompanding(lrgb[1])), 1),
Math.min(Math.max(0, sRGBCompanding(lrgb[2])), 1)
];
}
})();
const matrixMultiplyVector = ( [ a, b, c, d, e, f, g, h, i ], [x, y, z]) => {
return [
a * x + b * y + c * z,
d * x + e * y + f * z,
g * x + h * y + i * z
];
};
const relativeLuminance = ([ r, g, b ]) => {
if (r <= 0.03928) {
r = r / 12.92;
} else {
r = Math.pow(((r + 0.055) / 1.055), 2.4);
}
if (g <= 0.03928) {
g = g / 12.92;
} else {
g = Math.pow(((g + 0.055) / 1.055), 2.4);
}
if (b <= 0.03928) {
b = b / 12.92;
} else {
b = Math.pow(((b + 0.055) / 1.055), 2.4);
}
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const RGBtoHex = ([ r, g, b ]) => {
r = Math.round(r * 255).toString(16);
g = Math.round(g * 255).toString(16);
b = Math.round(b * 255).toString(16);
if (r.length % 2) {
r = '0' + r;
}
if (g.length % 2) {
g = '0' + g;
}
if (b.length % 2) {
b = '0' + b;
}
return `#${r}${g}${b}`;
};
const parseCrab = (node) => {
let check = '',
fail = false,
// Weird way to handle argument checking, but bear with me...
arguments = node.nodes.filter((n) => {
// The css value parser includes the parentheses and commas, so filter them out
if (n.type === 'paren' || n.type === 'comma') {
return false;
}
// While doing so add the types to a string to be checked momentarily
check += n.type;
return true;
});
// If the arguments length isn't 3 or 4 then they're invalid.
// If the arguments aren't one of the following then they're invalid:
// number, number, number
// number, number, number, word
// number, number, number, function
if ((arguments.length === 3 || arguments.length === 4) && (check === 'numbernumbernumber' || check === 'numbernumbernumberword' || check === 'numbernumbernumberfunc')) {
let cr = arguments[0].value,
a = arguments[1].value,
b = arguments[2].value,
bg = [ 1, 1, 1 ];
// Normalize the arguments
// WCAG contrast ratio is a value from 1 to 21; starting at 1.4 here
cr = Math.min(Math.max(cr, 1.4), 21);
// a* is a value from -128 to 127
a = Math.min(Math.max(a, -128), 127);
// b* is a value from -128 to 127
b = Math.min(Math.max(b, -128), 127);
// The last argument isn't required, so if it is defined then parse it
if (arguments[3] !== undefined) {
bg = arguments[3];
// If it is another crab function then parse it recursively
if (bg.type ==='func' && bg.value === 'crab') {
bg = parseCrab(bg);
if (bg === null) {
return null;
}
}
// Otherwise, if it is a color constant then try to parse it
// Otherwise, if it's some color function parse that, too
else {
bg = (bg.type === 'word') ? bg.value : bg.toString().trim();
if (bg === null) {
return null;
}
// Change the values to binary notation
bg = colorString.get.rgb(bg).slice(0, 3).map((n) => {
return n / 255;
});
}
// If any of the parsers failed then return null
if (bg === null) {
return null;
}
}
// TODO: Optimize
// Start from 100 L*a*b luminance and go backwards until the color is within the
// desired contrast ratio range and return it
let l = 100,
bgLum = relativeLuminance(bg),
c, cLum, ratio;
while (true) {
c = lab2xyz([ l, a, b ]);
c = xyz2rgb(c);
cLum = relativeLuminance(c);
ratio = (bgLum + 0.05) / (cLum + 0.05);
if (cLum > bgLum) {
ratio = 1 / ratio;
}
if (ratio - 0.1 <= cr && ratio + 0.1 >= cr) {
break;
}
l -= 0.1;
}
return c;
}
return null;
};
return (css) => {
css.walkDecls((decl) => {
// At first at least check if the value might have a crab function
if (!/(^|[^\w-])crab\(/i.test(decl.value)) {
return;
}
// If it might then parse the values and walk through them
let parsedValue = parser(decl.value).parse();
parsedValue.walk((node) => {
if (node.type === 'func' && node.value === 'crab') {
const value = parseCrab(node);
node.removeAll();
node.value = (value !== null) ? RGBtoHex(value) : 'transparent';
return value;
}
});
decl.value = parsedValue.toString();
});
};
});

14
package.json

@ -0,0 +1,14 @@
{
"name": "postcss-color-contrast-ratio",
"version": "0.0.1",
"description": "PostCSS plugin which allows for creation of colors based upon WCAG contrast ratios",
"main": "index.js",
"repository": "https://code.mensbeam.com/MensBeam/postcss-color-contrast-ratio.git",
"author": "Dustin Wilson <dustin@dustinwilson.com>",
"license": "MIT",
"dependencies": {
"color-string": "^1.5.3",
"postcss": "^7.0.7",
"postcss-values-parser": "^2.0.0"
}
}

111
yarn.lock

@ -0,0 +1,111 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
dependencies:
color-convert "^1.9.0"
chalk@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==
dependencies:
ansi-styles "^3.2.1"
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
dependencies:
color-name "1.1.3"
color-name@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
color-name@^1.0.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-string@^1.5.3:
version "1.5.3"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc"
integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==
dependencies:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
flatten@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
integrity sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
indexes-of@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
is-arrayish@^0.3.1:
version "0.3.2"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
postcss-values-parser@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.0.tgz#1ba42cae31367c44f96721cb5eb99462bfb39705"
integrity sha512-cyRdkgbRRefu91ByAlJow4y9w/hnBmmWgLpWmlFQ2bpIy2eKrqowt3VeYcaHQ08otVXmC9V2JtYW1Z/RpvYR8A==
dependencies:
flatten "^1.0.2"
indexes-of "^1.0.1"
uniq "^1.0.1"
postcss@^7.0.7:
version "7.0.7"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.7.tgz#2754d073f77acb4ef08f1235c36c5721a7201614"
integrity sha512-HThWSJEPkupqew2fnuQMEI2YcTj/8gMV3n80cMdJsKxfIh5tHf7nM5JigNX6LxVMqo6zkgQNAI88hyFvBk41Pg==
dependencies:
chalk "^2.4.1"
source-map "^0.6.1"
supports-color "^5.5.0"
simple-swizzle@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
dependencies:
is-arrayish "^0.3.1"
source-map@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
supports-color@^5.3.0, supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
dependencies:
has-flag "^3.0.0"
uniq@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
Loading…
Cancel
Save