commit bb2c7e27df90a6bba0d44b37516125a0b56ee3a9 Author: Dustin Wilson Date: Thu Jan 3 10:46:21 2019 -0600 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5f93eb --- /dev/null +++ b/.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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d29a80 --- /dev/null +++ b/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. \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..117666f --- /dev/null +++ b/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(); + }); + }; +}); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..cb8f87b --- /dev/null +++ b/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 ", + "license": "MIT", + "dependencies": { + "color-string": "^1.5.3", + "postcss": "^7.0.7", + "postcss-values-parser": "^2.0.0" + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..901e816 --- /dev/null +++ b/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=