├── .appveyor.yml
├── .editorconfig
├── .gitignore
├── .tape.js
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── index.js
├── package.json
└── test
├── alpha.css
├── basic.css
├── basic.expect.css
├── comment.css
├── sized.css
└── weighted.css
/.appveyor.yml:
--------------------------------------------------------------------------------
1 | # https://www.appveyor.com/docs/appveyor-yml
2 |
3 | environment:
4 | matrix:
5 | - nodejs_version: 4.0
6 |
7 | version: "{build}"
8 | build: off
9 | deploy: off
10 |
11 | install:
12 | - ps: Install-Product node $env:nodejs_version
13 | - npm install --ignore-scripts
14 |
15 | test_script:
16 | - node --version
17 | - npm --version
18 | - cmd: "npm test"
19 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_style = tab
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 |
10 | [*.md]
11 | trim_trailing_whitespace = false
12 |
13 | [*.{json,md,yml}]
14 | indent_size = 2
15 | indent_style = space
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .*
3 | !.appveyor.yml
4 | !.editorconfig
5 | !.gitignore
6 | !.tape.js
7 | !.travis.yml
8 | *.log*
9 | *.result.css
10 |
--------------------------------------------------------------------------------
/.tape.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'postcss-wcag-contrast': {
3 | 'basic': {
4 | message: 'supports basic usage',
5 | warning: 2,
6 | expect: 'basic.css'
7 | },
8 | 'basic:aaa': {
9 | message: 'supports basic usage { compliance "AAA" }',
10 | warning: 4,
11 | options: {
12 | compliance: 'AAA'
13 | },
14 | expect: 'basic.css'
15 | },
16 | 'sized': {
17 | message: 'supports font-size usage',
18 | warning: 3,
19 | expect: 'sized.css'
20 | },
21 | 'sized:aaa': {
22 | message: 'supports font-size usage with { compliance "AAA" }',
23 | warning: 3,
24 | options: {
25 | compliance: 'AAA'
26 | },
27 | expect: 'sized.css'
28 | },
29 | 'weighted': {
30 | message: 'supports font-size & font-weight usage',
31 | warning: 2,
32 | expect: 'weighted.css'
33 | },
34 | 'weighted:aaa': {
35 | message: 'supports font-size & font-weight usage { compliance "AAA" }',
36 | warning: 3,
37 | options: {
38 | compliance: 'AAA'
39 | },
40 | expect: 'weighted.css'
41 | },
42 | 'comment': {
43 | message: 'supports wcag-param usage',
44 | warning: 3,
45 | expect: 'comment.css'
46 | },
47 | 'comment:aaa': {
48 | message: 'supports wcag-param usage',
49 | warning: 4,
50 | options: {
51 | compliance: 'AAA'
52 | },
53 | expect: 'comment.css'
54 | },
55 | 'alpha': {
56 | message: 'supports alpha transparancy',
57 | warning: 4,
58 | expect: 'alpha.css'
59 | },
60 | 'alpha:aaa': {
61 | message: 'supports alpha transparancy',
62 | warning: 6,
63 | options: {
64 | compliance: 'AAA'
65 | },
66 | expect: 'alpha.css'
67 | }
68 | }
69 | };
70 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # https://docs.travis-ci.com/user/travis-lint
2 |
3 | language: node_js
4 |
5 | node_js:
6 | - 4
7 |
8 | install:
9 | - npm install --ignore-scripts
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changes to WCAG Contrast
2 |
3 | ### 2.0.0 (May 30, 2017)
4 |
5 | - Added: Support for PostCSS v6
6 | - Added: Support for Node v4
7 |
8 | ### 1.1.0 (February 4, 2017)
9 |
10 | - Added: Support for alpha transparency
11 |
12 | ### 1.0.0 (February 19, 2016)
13 |
14 | - Added: Initial version
15 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to WCAG Contrast
2 |
3 | You want to help? You rock! Now, take a moment to be sure your contributions
4 | make sense to everyone else.
5 |
6 | ## Reporting Issues
7 |
8 | Found a problem? Want a new feature?
9 |
10 | - See if your issue or idea has [already been reported].
11 | - Provide a [reduced test case] or a [live example].
12 |
13 | Remember, a bug is a _demonstrable problem_ caused by _our_ code.
14 |
15 | ## Submitting Pull Requests
16 |
17 | Pull requests are the greatest contributions, so be sure they are focused in
18 | scope and avoid unrelated commits.
19 |
20 | 1. To begin; [fork this project], clone your fork, and add our upstream.
21 | ```bash
22 | # Clone your fork of the repo into the current directory
23 | git clone git@github.com:YOUR_USER/postcss-wcag-contrast.git
24 |
25 | # Navigate to the newly cloned directory
26 | cd postcss-wcag-contrast
27 |
28 | # Assign the original repo to a remote called "upstream"
29 | git remote add upstream git@github.com:jonathantneal/postcss-wcag-contrast.git
30 |
31 | # Install the tools necessary for testing
32 | npm install
33 | ```
34 |
35 | 2. Create a branch for your feature or fix:
36 | ```bash
37 | # Move into a new branch for your feature
38 | git checkout -b feature/thing
39 | ```
40 | ```bash
41 | # Move into a new branch for your fix
42 | git checkout -b fix/something
43 | ```
44 |
45 | 3. If your code follows our practices, then push your feature branch:
46 | ```bash
47 | # Test current code
48 | npm test
49 | ```
50 | ```bash
51 | # Push the branch for your new feature
52 | git push origin feature/thing
53 | ```
54 | ```bash
55 | # Or, push the branch for your update
56 | git push origin update/something
57 | ```
58 |
59 | That’s it! Now [open a pull request] with a clear title and description.
60 |
61 | [already been reported]: issues
62 | [fork this project]: fork
63 | [live example]: https://codepen.io/pen
64 | [open a pull request]: https://help.github.com/articles/using-pull-requests/
65 | [reduced test case]: https://css-tricks.com/reduced-test-cases/
66 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # CC0 1.0 Universal
2 |
3 | ## Statement of Purpose
4 |
5 | The laws of most jurisdictions throughout the world automatically confer
6 | exclusive Copyright and Related Rights (defined below) upon the creator and
7 | subsequent owner(s) (each and all, an “owner”) of an original work of
8 | authorship and/or a database (each, a “Work”).
9 |
10 | Certain owners wish to permanently relinquish those rights to a Work for the
11 | purpose of contributing to a commons of creative, cultural and scientific works
12 | (“Commons”) that the public can reliably and without fear of later claims of
13 | infringement build upon, modify, incorporate in other works, reuse and
14 | redistribute as freely as possible in any form whatsoever and for any purposes,
15 | including without limitation commercial purposes. These owners may contribute
16 | to the Commons to promote the ideal of a free culture and the further
17 | production of creative, cultural and scientific works, or to gain reputation or
18 | greater distribution for their Work in part through the use and efforts of
19 | others.
20 |
21 | For these and/or other purposes and motivations, and without any expectation of
22 | additional consideration or compensation, the person associating CC0 with a
23 | Work (the “Affirmer”), to the extent that he or she is an owner of Copyright
24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and
25 | publicly distribute the Work under its terms, with knowledge of his or her
26 | Copyright and Related Rights in the Work and the meaning and intended legal
27 | effect of CC0 on those rights.
28 |
29 | 1. Copyright and Related Rights. A Work made available under CC0 may be
30 | protected by copyright and related or neighboring rights (“Copyright and
31 | Related Rights”). Copyright and Related Rights include, but are not limited
32 | to, the following:
33 | 1. the right to reproduce, adapt, distribute, perform, display,
34 | communicate, and translate a Work;
35 | 2. moral rights retained by the original author(s) and/or performer(s);
36 | 3. publicity and privacy rights pertaining to a person’s image or likeness
37 | depicted in a Work;
38 | 4. rights protecting against unfair competition in regards to a Work,
39 | subject to the limitations in paragraph 4(i), below;
40 | 5. rights protecting the extraction, dissemination, use and reuse of data
41 | in a Work;
42 | 6. database rights (such as those arising under Directive 96/9/EC of the
43 | European Parliament and of the Council of 11 March 1996 on the legal
44 | protection of databases, and under any national implementation thereof,
45 | including any amended or successor version of such directive); and
46 | 7. other similar, equivalent or corresponding rights throughout the world
47 | based on applicable law or treaty, and any national implementations
48 | thereof.
49 |
50 | 2. Waiver. To the greatest extent permitted by, but not in contravention of,
51 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
52 | unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright
53 | and Related Rights and associated claims and causes of action, whether now
54 | known or unknown (including existing as well as future claims and causes of
55 | action), in the Work (i) in all territories worldwide, (ii) for the maximum
56 | duration provided by applicable law or treaty (including future time
57 | extensions), (iii) in any current or future medium and for any number of
58 | copies, and (iv) for any purpose whatsoever, including without limitation
59 | commercial, advertising or promotional purposes (the “Waiver”). Affirmer makes
60 | the Waiver for the benefit of each member of the public at large and to the
61 | detriment of Affirmer’s heirs and successors, fully intending that such Waiver
62 | shall not be subject to revocation, rescission, cancellation, termination, or
63 | any other legal or equitable action to disrupt the quiet enjoyment of the Work
64 | by the public as contemplated by Affirmer’s express Statement of Purpose.
65 |
66 | 3. Public License Fallback. Should any part of the Waiver for any reason be
67 | judged legally invalid or ineffective under applicable law, then the Waiver
68 | shall be preserved to the maximum extent permitted taking into account
69 | Affirmer’s express Statement of Purpose. In addition, to the extent the Waiver
70 | is so judged Affirmer hereby grants to each affected person a royalty-free, non
71 | transferable, non sublicensable, non exclusive, irrevocable and unconditional
72 | license to exercise Affirmer’s Copyright and Related Rights in the Work (i) in
73 | all territories worldwide, (ii) for the maximum duration provided by applicable
74 | law or treaty (including future time extensions), (iii) in any current or
75 | future medium and for any number of copies, and (iv) for any purpose
76 | whatsoever, including without limitation commercial, advertising or promotional
77 | purposes (the “License”). The License shall be deemed effective as of the date
78 | CC0 was applied by Affirmer to the Work. Should any part of the License for any
79 | reason be judged legally invalid or ineffective under applicable law, such
80 | partial invalidity or ineffectiveness shall not invalidate the remainder of the
81 | License, and in such case Affirmer hereby affirms that he or she will not (i)
82 | exercise any of his or her remaining Copyright and Related Rights in the Work
83 | or (ii) assert any associated claims and causes of action with respect to the
84 | Work, in either case contrary to Affirmer’s express Statement of Purpose.
85 |
86 | 4. Limitations and Disclaimers.
87 | 1. No trademark or patent rights held by Affirmer are waived, abandoned,
88 | surrendered, licensed or otherwise affected by this document.
89 | 2. Affirmer offers the Work as-is and makes no representations or
90 | warranties of any kind concerning the Work, express, implied, statutory
91 | or otherwise, including without limitation warranties of title,
92 | merchantability, fitness for a particular purpose, non infringement, or
93 | the absence of latent or other defects, accuracy, or the present or
94 | absence of errors, whether or not discoverable, all to the greatest
95 | extent permissible under applicable law.
96 | 3. Affirmer disclaims responsibility for clearing rights of other persons
97 | that may apply to the Work or any use thereof, including without
98 | limitation any person’s Copyright and Related Rights in the Work.
99 | Further, Affirmer disclaims responsibility for obtaining any necessary
100 | consents, permissions or other rights required for any use of the Work.
101 | 4. Affirmer understands and acknowledges that Creative Commons is not a
102 | party to this document and has no duty or obligation with respect to
103 | this CC0 or use of the Work.
104 |
105 | For more information, please see
106 | https://creativecommons.org/publicdomain/zero/1.0/.
107 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WCAG Contrast [
][postcss]
2 |
3 | [![NPM Version][npm-img]][npm-url]
4 | [![Linux Build Status][cli-img]][cli-url]
5 | [![Windows Build Status][win-img]][win-url]
6 | [![Gitter Chat][git-img]][git-url]
7 |
8 | [WCAG Contrast] checks CSS for [color contrast compliance] with
9 | [Web Content Accessibility Guidelines (WCAG) 2.0].
10 |
11 | ```css
12 | .header {
13 | background-color: #444;
14 | color: #000; /* throws a warning for a low contrast of only 2.2 */
15 | }
16 |
17 | .footer {
18 | /* wcag-params: bold 14pt #777 */
19 | color: #000; /* throws no warning when text is bold 14pt and contrast is 4.7 */
20 | }
21 | ```
22 |
23 | ## Usage
24 |
25 | Add [WCAG Contrast] to your build tool:
26 |
27 | ```bash
28 | npm install postcss-wcag-contrast --save-dev
29 | ```
30 |
31 | #### Node
32 |
33 | Use [WCAG Contrast] to process your CSS:
34 |
35 | ```js
36 | require('postcss-wcag-contrast').process(YOUR_CSS);
37 | ```
38 |
39 | #### PostCSS
40 |
41 | Add [PostCSS] to your build tool:
42 |
43 | ```bash
44 | npm install postcss --save-dev
45 | ```
46 |
47 | Use [WCAG Contrast] as a plugin:
48 |
49 | ```js
50 | postcss([
51 | require('postcss-wcag-contrast')()
52 | ]).process(YOUR_CSS);
53 | ```
54 |
55 | #### Gulp
56 |
57 | Add [Gulp PostCSS] to your build tool:
58 |
59 | ```bash
60 | npm install gulp-postcss --save-dev
61 | ```
62 |
63 | Use [WCAG Contrast] in your Gulpfile:
64 |
65 | ```js
66 | var postcss = require('gulp-postcss');
67 |
68 | gulp.task('css', function () {
69 | return gulp.src('./src/*.css').pipe(
70 | postcss([
71 | require('postcss-wcag-contrast')()
72 | ])
73 | ).pipe(
74 | gulp.dest('.')
75 | );
76 | });
77 | ```
78 |
79 | #### Grunt
80 |
81 | Add [Grunt PostCSS] to your build tool:
82 |
83 | ```bash
84 | npm install grunt-postcss --save-dev
85 | ```
86 |
87 | Use [WCAG Contrast] in your Gruntfile:
88 |
89 | ```js
90 | grunt.loadNpmTasks('grunt-postcss');
91 |
92 | grunt.initConfig({
93 | postcss: {
94 | options: {
95 | use: [
96 | require('postcss-wcag-contrast')()
97 | ]
98 | },
99 | dist: {
100 | src: '*.css'
101 | }
102 | }
103 | });
104 | ```
105 |
106 | ## Options
107 |
108 | ### compliance
109 |
110 | Type: `String`
111 | Default: `"AA"`
112 |
113 | The `compliance` option specifies the WCAG compliance the CSS will be evaluated
114 | against.
115 |
116 | ### wcag-params
117 |
118 | Type: `CSS Comment`
119 |
120 | The `wcag-params` specifies additional font size, font weight, background, and
121 | foreground information about the rule.
122 |
123 | [npm-url]: https://www.npmjs.com/package/postcss-wcag-contrast
124 | [npm-img]: https://img.shields.io/npm/v/postcss-wcag-contrast.svg
125 | [cli-url]: https://travis-ci.org/jonathantneal/postcss-wcag-contrast
126 | [cli-img]: https://img.shields.io/travis/jonathantneal/postcss-wcag-contrast.svg
127 | [win-url]: https://ci.appveyor.com/project/jonathantneal/postcss-wcag-contrast
128 | [win-img]: https://img.shields.io/appveyor/ci/jonathantneal/postcss-wcag-contrast.svg
129 | [git-url]: https://gitter.im/postcss/postcss
130 | [git-img]: https://img.shields.io/badge/chat-gitter-blue.svg
131 |
132 | [WCAG Contrast]: https://github.com/jonathantneal/postcss-wcag-contrast
133 | [PostCSS]: https://github.com/postcss/postcss
134 | [Gulp PostCSS]: https://github.com/postcss/gulp-postcss
135 | [Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss
136 |
137 | [color contrast compliance]: https://www.w3.org/TR/WCAG20/#visual-audio-contrast-contrast
138 | [Web Content Accessibility Guidelines (WCAG) 2.0]: https://www.w3.org/TR/WCAG20/
139 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const onecolor = require('onecolor');
4 | const postcss = require('postcss');
5 | const comma = postcss.list.comma;
6 | const space = postcss.list.space;
7 |
8 | const cssColorRegExp = new RegExp('^(#(?:[\\da-f]{3}){1,2}|rgb\\((?:\\d{1,3},\\s*){2}\\d{1,3}\\)|rgba\\((?:\\d{1,3},\\s*){3}\\d*\\.?\\d+\\)|hsl\\(\\d{1,3}(?:,\\s*\\d{1,3}%){2}\\)|hsla\\(\\d{1,3}(?:,\\s*\\d{1,3}%){2},\\s*\\d*\\.?\\d+\\))$');
9 | const cssLengthRegExp = new RegExp('^([-+]?0|[-+]?[0-9]*\.?[0-9]+(%|cm|in|mm|pc|pt|px|rem))$');
10 | const cssWeightRegExp = new RegExp('^([1-9]00|bold|normal)$');
11 | const cssParamsRegExp = new RegExp('^(\s*wcag-params:\s*(.+)\s*)$');
12 |
13 | const cssLengthUnits = {
14 | cm: 0.3937 * 96,
15 | in: 96,
16 | mm: 0.3937 * 96 / 10,
17 | pc: 12 * 96 / 72,
18 | pt: 96 / 72,
19 | rem: 16
20 | };
21 |
22 | module.exports = postcss.plugin('postcss-wcag-contrast', (opts) => {
23 | // set options
24 | const compliance = opts && 'compliance' in opts ? String(opts.compliance).toUpperCase() : 'AA';
25 |
26 | if (compliance !== 'AAA' && compliance !== 'AA') {
27 | throw new Error('The `compliance` option must be "AA" or "AAA"');
28 | }
29 |
30 | // return wcag contrast plugin
31 | return (css, result) => {
32 | // walk each rule
33 | css.walkRules((rule) => {
34 | let fontsize;
35 | let fontweight;
36 |
37 | let foreground;
38 | let background;
39 | let fallbackcolor;
40 | let fallbacksize;
41 | let fallbackweight;
42 |
43 | // for each comment or declration of the rule
44 | rule.nodes.forEach((node) => {
45 | if (node.prop === 'color') {
46 | foreground = node.value;
47 | } else if (node.prop === 'background-color') {
48 | background = node.value;
49 | } else if (node.prop === 'font-size') {
50 | fontsize = getFontSize(node.value);
51 | } else if (node.prop === 'font-weight') {
52 | fontweight = getFontWeight(node.value);
53 | } else if (node.prop === 'font') {
54 | fontweight = 400;
55 |
56 | space(node.value.replace(/\/[^\s]+/, '')).forEach((spaceSplitValue) => {
57 | if (cssWeightRegExp.test(spaceSplitValue)) {
58 | fontweight = getFontWeight(spaceSplitValue);
59 | } else if (cssLengthRegExp.test(spaceSplitValue)) {
60 | fontsize = getFontSize(spaceSplitValue);
61 | }
62 | });
63 | } else if (node.prop === 'background') {
64 | // split background by comma and space
65 | comma(node.value).forEach((commaSplitValue) => {
66 | space(commaSplitValue).forEach((spaceSplitValue) => {
67 | // conditionally set the the background color
68 | if (cssColorRegExp.test(spaceSplitValue)) {
69 | background = spaceSplitValue;
70 | }
71 | });
72 | });
73 | } else if (node.type === 'comment') {
74 | if (cssParamsRegExp.test(node.text)) {
75 | // split comment by space
76 | space(node.text).forEach((spaceSplitValue) => {
77 | // conditionally define fallback values
78 | if (cssColorRegExp.test(spaceSplitValue)) {
79 | fallbackcolor = spaceSplitValue;
80 | } else if (cssWeightRegExp.test(spaceSplitValue)) {
81 | fallbackweight = getFontWeight(spaceSplitValue);
82 | } else if (cssLengthRegExp.test(spaceSplitValue)) {
83 | fallbacksize = getFontSize(spaceSplitValue);
84 | }
85 | });
86 | }
87 | }
88 | });
89 |
90 | // conditionally update foreground or background
91 | if (!foreground) {
92 | foreground = fallbackcolor;
93 | } else if (!background) {
94 | background = fallbackcolor;
95 | }
96 |
97 | // ignore rules without a foreground or background
98 | if (!foreground || !background) {
99 | return;
100 | }
101 |
102 | // conditionally update font size
103 | if (!fontsize) {
104 | fontsize = fallbacksize || 16;
105 | }
106 |
107 | // conditionally update font weight
108 | if (!fontweight) {
109 | fontweight = fallbackweight || 400;
110 | }
111 |
112 | const contrastRatio = getContrastRatio(foreground, background);
113 |
114 | // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#larger-scaledef
115 | const isLargeScale = fontsize >= 24 || fontsize >= 14 * (96 / 72) && fontweight >= 700;
116 |
117 | // conditionally warn of compliance
118 | if (compliance === 'AAA') {
119 | // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast7
120 | const minAAAContrastRatio = isLargeScale ? 4.5 : 7;
121 | const hasAAAContrast = contrastRatio >= minAAAContrastRatio;
122 |
123 | if (!hasAAAContrast) {
124 | rule.warn(result, contrastRatio.toFixed(2) + ':1 is an insufficient contrast ratio (WCAG 2.0 AAA requires ' + minAAAContrastRatio + ':1)');
125 | }
126 | } else if (compliance === 'AA') {
127 | // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast-contrast
128 | const minAAContrastRatio = isLargeScale ? 3 : 4.5;
129 | const hasAAContrast = contrastRatio >= minAAContrastRatio;
130 |
131 | if (!hasAAContrast) {
132 | rule.warn(result, contrastRatio.toFixed(2) + ':1 is an insufficient contrast ratio (WCAG 2.0 AA requires ' + minAAContrastRatio + ':1)');
133 | }
134 | }
135 | });
136 | };
137 | });
138 |
139 | function getRelativeLuminance(cssColor) {
140 | // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
141 | const color = onecolor(cssColor);
142 |
143 | const R = color._red <= 0.03928 ? color._red / 12.92 : Math.pow((color._red + 0.055) / 1.055, 2.4);
144 | const G = color._green <= 0.03928 ? color._green / 12.92 : Math.pow((color._green + 0.055) / 1.055, 2.4);
145 | const B = color._blue <= 0.03928 ? color._blue / 12.92 : Math.pow((color._blue + 0.055) / 1.055, 2.4);
146 |
147 | const L = 0.2126 * R + 0.7152 * G + 0.0722 * B;
148 |
149 | return L;
150 | }
151 |
152 | function alphaBlend(cssForeground, cssBackground) {
153 | const foreground = onecolor(cssForeground);
154 | const background = onecolor(cssBackground);
155 | const result = onecolor('#fff');
156 | const a = foreground.alpha();
157 |
158 | result._red = foreground._red * a + background._red * (1 - a);
159 | result._green = foreground._green * a + background._green * (1 - a);
160 | result._blue = foreground._blue * a + background._blue * (1 - a);
161 |
162 | return result;
163 | }
164 |
165 | function getContrastRatioOpaque(foreground, background) {
166 | const L1 = getRelativeLuminance(background);
167 | const L2 = getRelativeLuminance(alphaBlend(foreground, background));
168 |
169 | // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
170 | return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
171 | }
172 |
173 | function getContrastRatio(foreground, background) {
174 | const backgroundOnWhite = alphaBlend(background, '#fff');
175 | const backgroundOnBlack = alphaBlend(background, '#000');
176 |
177 | const LWhite = getRelativeLuminance(backgroundOnWhite);
178 | const LBlack = getRelativeLuminance(backgroundOnBlack);
179 | const LForeground = getRelativeLuminance(foreground);
180 |
181 | if (LWhite < LForeground) {
182 | return getContrastRatioOpaque(foreground, backgroundOnWhite);
183 | } else if (LBlack > LForeground) {
184 | return getContrastRatioOpaque(foreground, backgroundOnBlack);
185 | } else {
186 | return 1;
187 | }
188 | }
189 |
190 | function getFontSize(cssLength) {
191 | const fontSizeMatch = cssLength.match(cssLengthRegExp);
192 |
193 | const fontSize = fontSizeMatch && parseFloat(fontSizeMatch[0]);
194 | const fontSizeUnit = fontSizeMatch && fontSizeMatch[2];
195 |
196 | const fontSizeScale = fontSizeUnit in cssLengthUnits ? cssLengthUnits[fontSizeUnit] : 1;
197 |
198 | return fontSize ? fontSize * fontSizeScale : 16;
199 | }
200 |
201 | function getFontWeight(cssWeight) {
202 | return cssWeight === 'bold' ? 700 : cssWeight === 'normal' ? 400 : parseInt(cssWeight) || 400;
203 | }
204 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "postcss-wcag-contrast",
3 | "version": "2.0.0",
4 | "description": "Check for WCAG color contrast compliance",
5 | "author": "Jonathan Neal ",
6 | "license": "CC0-1.0",
7 | "repository": "jonathantneal/postcss-wcag-contrast",
8 | "homepage": "https://github.com/jonathantneal/postcss-wcag-contrast#readme",
9 | "bugs": "https://github.com/jonathantneal/postcss-wcag-contrast/issues",
10 | "main": "index.js",
11 | "files": [
12 | "index.js"
13 | ],
14 | "scripts": {
15 | "clean": "git clean -X -d -f",
16 | "prepublish": "npm test",
17 | "test": "echo 'Running tests...'; npm run test:js && npm run test:tape",
18 | "test:js": "eslint *.js --cache --ignore-pattern .gitignore",
19 | "test:tape": "postcss-tape"
20 | },
21 | "engines": {
22 | "node": ">=4.0.0"
23 | },
24 | "dependencies": {
25 | "onecolor": "^3.0.4",
26 | "postcss": "^6.0.1"
27 | },
28 | "devDependencies": {
29 | "eslint": "^3.19.0",
30 | "eslint-config-dev": "2.0.0",
31 | "postcss-tape": "2.0.1",
32 | "pre-commit": "^1.2.2"
33 | },
34 | "eslintConfig": {
35 | "extends": "dev"
36 | },
37 | "keywords": [
38 | "postcss",
39 | "css",
40 | "postcss-plugin",
41 | "aa",
42 | "aaa",
43 | "analyse",
44 | "analyze",
45 | "blues",
46 | "colors",
47 | "compliances",
48 | "contrasts",
49 | "greens",
50 | "guards",
51 | "lints",
52 | "luminances",
53 | "luminosity",
54 | "minimum",
55 | "ratios",
56 | "ratios",
57 | "reds",
58 | "rgbs",
59 | "specifications",
60 | "specs",
61 | "wcag"
62 | ]
63 | }
64 |
--------------------------------------------------------------------------------
/test/alpha.css:
--------------------------------------------------------------------------------
1 | .a {
2 | background-color: rgba(0, 0, 0, 0.2);
3 | color: #fff;
4 | }
5 |
6 | .b {
7 | background-color: rgba(0, 0, 0, 0.5);
8 | color: #fff;
9 | }
10 |
11 | .c {
12 | background-color: rgba(0, 0, 0, 0.6);
13 | color: #fff;
14 | }
15 |
16 | .d {
17 | background-color: rgba(0, 0, 0, 0.8);
18 | color: #fff;
19 | }
20 |
21 | .e {
22 | background-color: #fff;
23 | color: rgba(0, 0, 0, 0.4);
24 | }
25 |
26 | .f {
27 | background-color: #fff;
28 | color: rgba(0, 0, 0, 0.5);
29 | }
30 |
31 | .g {
32 | background-color: #fff;
33 | color: rgba(0, 0, 0, 0.6);
34 | }
35 |
36 | .h {
37 | background-color: #fff;
38 | color: rgba(0, 0, 0, 0.7);
39 | }
40 |
--------------------------------------------------------------------------------
/test/basic.css:
--------------------------------------------------------------------------------
1 | .a {
2 | background-color: #fff;
3 | color: #000;
4 | }
5 |
6 | .b {
7 | background-color: #ccc;
8 | color: #000;
9 | }
10 |
11 | .c {
12 | background-color: #777;
13 | color: #000;
14 | }
15 |
16 | .d {
17 | background-color: #444;
18 | color: #000;
19 | }
20 |
21 | .e {
22 | background-color: #111;
23 | color: #000;
24 | }
25 |
26 | .f {
27 | background: #fff url(kitten.jpg);
28 | color: #000;
29 | }
30 |
31 | .g {
32 | background: #777 url(kitten.jpg);
33 | color: #000;
34 | }
35 |
36 | .h {
37 | /* wcag-params: #eee */
38 | color: #333;
39 | }
40 |
--------------------------------------------------------------------------------
/test/basic.expect.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csstools/postcss-wcag-contrast/1eaf468e349d56521f14dbbf814aac920acb1d37/test/basic.expect.css
--------------------------------------------------------------------------------
/test/comment.css:
--------------------------------------------------------------------------------
1 | .a {
2 | /* wcag-params: #fff */
3 | color: #000;
4 | }
5 |
6 | .b {
7 | /* wcag-params: #777 */
8 | color: #000;
9 | }
10 |
11 | .c {
12 | /* wcag-params: #444 */
13 | color: #000;
14 | }
15 |
16 | .d {
17 | /* wcag-params: 18pt #fff */
18 | color: #000;
19 | }
20 |
21 | .e {
22 | /* wcag-params: 18pt #777 */
23 | color: #000;
24 | }
25 |
26 | .f {
27 | /* wcag-params: 18pt #444 */
28 | color: #000;
29 | }
30 |
31 | .g {
32 | /* wcag-params: bold 14pt #fff */
33 | color: #000;
34 | }
35 |
36 | .h {
37 | /* wcag-params: bold 14pt #777 */
38 | color: #000;
39 | }
40 |
41 | .i {
42 | /* wcag-params: bold 14pt #444 */
43 | color: #000;
44 | }
45 |
46 | .j {
47 | color: #000;
48 | }
49 |
--------------------------------------------------------------------------------
/test/sized.css:
--------------------------------------------------------------------------------
1 | .a {
2 | background-color: #fff;
3 | color: #000;
4 | font-size: 18pt;
5 | }
6 |
7 | .b {
8 | background-color: #ccc;
9 | color: #000;
10 | font-size: 18pt;
11 | }
12 |
13 | .c {
14 | background-color: #777;
15 | color: #000;
16 | font-size: 18pt;
17 | }
18 |
19 | .d {
20 | background-color: #444;
21 | color: #000;
22 | font-size: 18pt;
23 | }
24 |
25 | .e {
26 | background-color: #111;
27 | color: #000;
28 | font-size: 18pt;
29 | }
30 |
31 | .f {
32 | background-color: #111;
33 | color: #000;
34 | font: 18pt sans-serif;
35 | }
36 |
--------------------------------------------------------------------------------
/test/weighted.css:
--------------------------------------------------------------------------------
1 | .a {
2 | background-color: #fff;
3 | color: #000;
4 | font-size: 14pt;
5 | font-weight: bold;
6 | }
7 |
8 | .b {
9 | background-color: #ccc;
10 | color: #000;
11 | font-size: 14pt;
12 | font-weight: bold;
13 | }
14 |
15 | .c {
16 | background-color: #777;
17 | color: #000;
18 | font-size: 14pt;
19 | font-weight: bold;
20 | }
21 |
22 | .d {
23 | background-color: #444;
24 | color: #000;
25 | font-size: 14pt;
26 | font-weight: bold;
27 | }
28 |
29 | .e {
30 | background-color: #111;
31 | color: #000;
32 | font-size: 14pt;
33 | font-weight: bold;
34 | }
35 |
36 | .f {
37 | background-color: #777;
38 | color: #000;
39 | font: 18pt serif;
40 | }
41 |
42 | .g {
43 | background-color: #777;
44 | color: #000;
45 | font: bold 14pt serif;
46 | }
47 |
48 | .h {
49 | background-color: #777;
50 | color: #000;
51 | font: 14pt serif;
52 | }
53 |
--------------------------------------------------------------------------------