Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
"style": "themes/prism.css",
"scripts": {
"test:aliases": "mocha tests/aliases-test.js",
"test:contrast": "mocha tests/contrast-test.js",
"test:core": "mocha tests/core/**/*.js",
"test:dependencies": "mocha tests/dependencies-test.js",
"test:examples": "mocha tests/examples-test.js",
"test:languages": "mocha tests/run.js",
"test:patterns": "mocha tests/pattern-tests.js",
"test:plugins": "mocha tests/plugins/**/*.js",
"test:runner": "mocha tests/testrunner-tests.js",
"test": "npm run test:runner && npm run test:core && npm run test:dependencies && npm run test:languages && npm run test:plugins && npm run test:aliases && npm run test:patterns && npm run test:examples"
"test": "npm run test:runner && npm run test:core && npm run test:dependencies && npm run test:languages && npm run test:plugins && npm run test:aliases && npm run test:patterns && npm run test:examples && npm run test:contrast"
},
"repository": {
"type": "git",
Expand All @@ -31,6 +32,8 @@
},
"devDependencies": {
"chai": "^4.2.0",
"color": "^3.1.2",
"css": "^2.2.4",
"gulp": "^4.0.2",
"gulp-concat": "^2.3.4",
"gulp-header": "^2.0.7",
Expand Down
160 changes: 160 additions & 0 deletions tests/contrast-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
const fs = require('fs');
const css = require('css');
const path = require('path');
const Color = require('color');
const { assert } = require('chai');
const { themes } = require('../components');


/**
* @typedef {import("color")} Color
*/

/**
* Analyzes the given CSS source code to find low-contrast colors and returns an array the found colors, the line they
* occur in, and a suggested high-contrast color with the same hue.
*
* Use `@contrast-background: <color>` on a line in a comment to tell this checker the background color. The background
* color is defined per scope.
*
* Use `@contrast-ignore` on a line in a comment within a rule to ignore the next property.
*
* @param {string} cssCode
* @returns {string[]}
*/
function analyseContrast(cssCode) {
/** @type {string[]} */
const errors = [];

const ast = css.parse(cssCode);
analyseRules(ast.stylesheet.rules);

/**
* @param {string} comment
*/
function parseBackground(comment) {
const colorStr = (/^\s*(?:\*\s*)?@contrast-background:(.*)/m.exec(comment) || [, ''])[1].trim();
if (colorStr) {
return Color(colorStr);
}
}
/**
* @param {string} comment
*/
function parseIgnore(comment) {
return /^\s*(?:\*\s*)?@contrast-ignore\s*$/m.test(comment);
}

/**
* @param {import("css").StyleRules["rules"]} rules
* @param {Color | undefined} [background]
*/
function analyseRules(rules, background) {
for (const element of rules) {
if ("comment" in element) {
background = parseBackground(element.comment) || background;
} else if ("rules" in element) {
analyseRules(element.rules, background);
} else if ("declarations" in element) {
analyseDeclarations(element.declarations, background);
}
}
}
/**
* @param {import("css").Rule["declarations"]} declarations
* @param {Color | undefined} background
*/
function analyseDeclarations(declarations, background) {
let ignore = false;
for (const decl of declarations) {
if ("comment" in decl) {
background = parseBackground(decl.comment) || background;
if (parseIgnore(decl.comment)) {
ignore = true;
}
} else if ("property" in decl) {
if (ignore) {
ignore = false;
continue;
}
if (decl.property !== "color") {
continue;
}

const line = `Line ${decl.position.start.line}`;

if (!background) {
errors.push(`${line}: There is no background defined for the color ${decl.value}.`);
continue;
}

const color = Color(decl.value);
const contrast = color.contrast(background);

if (contrast < 4.5 - 0.01 /* some epsilon for rounding errors */) {
const bg = `Background ${background.hex()}`;
const correctedColor = ensureContrast(color, background, 4.5);
const corrected = `${correctedColor.hex()} (${correctedColor.contrast(background).toFixed(2)})`;
errors.push(`${line}: ${bg}: The color ${decl.value} has a contrast of ${contrast.toFixed(2)} < 4.5. Use ${corrected} instead.`);
}
}
}
}

return errors;
}

/**
* @param {Color} color
* @param {Color} background
* @param {number} contrastGoal
* @returns {Color}
*/
function ensureContrast(color, background, contrastGoal) {
if (color.contrast(background) >= contrastGoal) {
return color.rgb();
}

color = color.lab();
const makeDarker = color.luminosity() < background.luminosity();
const ab = { a: color.object().a, b: color.object().b };

/** @type {number} */
let near = color.lab().object().l;
/** @type {number} */
let far = makeDarker ? 0 : 100;
while (Math.abs(near - far) > 0.00001) {
let middle = (near + far) / 2;
color = Color.lab({ l: middle, ...ab });

if (color.contrast(background) < contrastGoal) {
near = middle;
} else {
far = middle;
}
}

return color.rgb();
}


describe('Contrast', function () {

for (const theme in themes) {
if (theme === 'meta') {
continue;
}

const file = path.join(__dirname, `../themes/${theme}.css`);

it(`- ./themes/${theme}.css`, () => {
const source = fs.readFileSync(file, 'utf8');

const errors = analyseContrast(source);
if (errors.length > 0) {
assert.fail(`There are ${errors.length} contrast issues:\n\n` + errors.join('\n'));
}
});
}

});
5 changes: 5 additions & 0 deletions themes/prism-coy.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* @author Tim Shedor
*/

/* @contrast-background: #fdfdfd */

code[class*="language-"],
pre[class*="language-"] {
color: black;
Expand Down Expand Up @@ -141,6 +143,7 @@ pre[class*="language-"]:after {
.token.entity,
.token.url,
.token.variable {
/* @contrast-background: white */
color: #a67f59;
background: rgba(255, 255, 255, 0.5);
}
Expand All @@ -159,6 +162,7 @@ pre[class*="language-"]:after {

.language-css .token.string,
.style .token.string {
/* @contrast-background: white */
color: #a67f59;
background: rgba(255, 255, 255, 0.5);
}
Expand Down Expand Up @@ -195,6 +199,7 @@ pre[class*="language-"]:after {
.token.tab:not(:empty):before,
.token.cr:before,
.token.lf:before {
/* @contrast-ignore */
color: #e0d7d1;
}

Expand Down
2 changes: 2 additions & 0 deletions themes/prism-dark.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* @author Lea Verou
*/

/* @contrast-background: hsl(30, 20%, 25%) */

code[class*="language-"],
pre[class*="language-"] {
color: white;
Expand Down
2 changes: 2 additions & 0 deletions themes/prism-funky.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* @author Lea Verou
*/

/* @contrast-background: black */

code[class*="language-"],
pre[class*="language-"] {
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
Expand Down
2 changes: 2 additions & 0 deletions themes/prism-okaidia.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* @author ocodia
*/

/* @contrast-background: #272822 */

code[class*="language-"],
pre[class*="language-"] {
color: #f8f8f2;
Expand Down
2 changes: 2 additions & 0 deletions themes/prism-solarizedlight.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ cyan #2aa198
green #859900
*/

/* @contrast-background: #fdf6e3 */

code[class*="language-"],
pre[class*="language-"] {
color: #657b83; /* base00 */
Expand Down
2 changes: 2 additions & 0 deletions themes/prism-tomorrow.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* @author Rose Pritchard
*/

/* @contrast-background: #2d2d2d */

code[class*="language-"],
pre[class*="language-"] {
color: #ccc;
Expand Down
3 changes: 3 additions & 0 deletions themes/prism-twilight.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
* Based (more or less) on the Twilight theme originally of Textmate fame.
* @author Remy Bach
*/

/* @contrast-background: black */

code[class*="language-"],
pre[class*="language-"] {
color: white;
Expand Down
3 changes: 3 additions & 0 deletions themes/prism.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* @author Lea Verou
*/

/* @contrast-background: white */

code[class*="language-"],
pre[class*="language-"] {
color: black;
Expand Down Expand Up @@ -74,6 +76,7 @@ pre[class*="language-"] {
}

.token.punctuation {
/* @contrast-ignore */
color: #999;
}

Expand Down