diff --git a/Changelog.md b/Changelog.md index 7819da6d..94712386 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,10 @@ +## 2.8.0-rc.0 - 2015-07-10 + +# Enhancements: +* Pull color code into separate file. +* Improve color suggestion algorithm. +* Descend into iframes when collecting matching elements. + ## 2.7.1 - 2015-06-30 ## 2.7.1-rc.1 - 2015-06-23 diff --git a/Gruntfile.js b/Gruntfile.js index bb877489..90b1be86 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -18,6 +18,7 @@ module.exports = function(grunt) { './src/js/axs.js', './src/js/BrowserUtils.js', './src/js/Constants.js', + './src/js/Color.js', './src/js/AccessibilityUtils.js', './src/js/Properties.js', './src/js/AuditRule.js', @@ -122,6 +123,7 @@ module.exports = function(grunt) { grunt.registerTask('changelog', function(type) { grunt.task.requires('bump-only:' + type); + var dryRun = grunt.option('dry-run'); var config = { data: { version: grunt.config.get('pkg.version'), @@ -137,6 +139,7 @@ module.exports = function(grunt) { var headerTpl = "## <%= version %> - <%= releaseDate %>\n\n"; var header = grunt.template.process(headerTpl, config); + grunt.log.ok('changelog: Extracting release notes.'); if (contents.length > 0) { if ((stopIndex = contents.search(stopRegex)) !== -1) { releaseNotes = contents.slice(0, stopIndex); @@ -145,8 +148,15 @@ module.exports = function(grunt) { grunt.config.set("gh-release.release-notes", releaseNotes); - grunt.file.write(dest, "" + header + contents); - grunt.log.ok("Changelog updated, and release notes extracted."); + if (dryRun) { + grunt.log.ok('changelog (dry): Prepending header to ' + dest); + grunt.log.writeln(header); + } else { + grunt.file.write(dest, "" + header + contents); + } + + grunt.log.writeln("Release Notes:\n" + releaseNotes); + grunt.log.ok('changelog: Task completed.'); }); grunt.registerTask('gh-release', function() { @@ -154,6 +164,7 @@ module.exports = function(grunt) { grunt.task.requires('coffee:compile'); var GHRepo = require('./.tmp/util/gh_repo'); + var dryRun = grunt.option('dry-run'); var done = this.async(); var config = grunt.config.get('gh-release'); var pkg = grunt.config.get('pkg'); @@ -169,20 +180,30 @@ module.exports = function(grunt) { draft: true }; - grunt.log.writeln("Searching for existing GH release:", nextRelease); + grunt.log.writeln("gh-release: Searching for existing Github release:", nextRelease); repo.getReleaseByName(nextRelease) .then(function(release) { if (release) { - payload.body += "\n" + release.body; - repo.updateRelease(release, payload).then(function() { - grunt.log.ok('Github release ' + nextRelease + ' updated successfully.'); + if (dryRun) { + grunt.log.ok('gh-release (dry): Updating existing Github release: ' + nextRelease); done(); - }); + } else { + payload.body += "\n" + release.body; + repo.updateRelease(release, payload).then(function() { + grunt.log.ok('Github release ' + nextRelease + ' updated successfully.'); + done(); + }); + } } else { - repo.createRelease(payload).then(function() { - grunt.log.ok('Github release ' + nextRelease + ' created successfully'); + if (dryRun) { + grunt.log.ok('gh-release (dry): Creating new Github release: ' + nextRelease); done(); - }); + } else { + repo.createRelease(payload).then(function() { + grunt.log.ok('Github release ' + nextRelease + ' created successfully'); + done(); + }); + } } }) .catch(function(err) { @@ -217,18 +238,26 @@ module.exports = function(grunt) { grunt.fail.fatal('You must specify a release type. i.e. grunt release:prerelease'); } - grunt.task.run([ - 'prompt:gh-release', - 'build', + var dryRun = grunt.option('dry-run'); + + var tasks = ['prompt:gh-release']; + + if (dryRun) { + grunt.log.ok('Skipping build, clean:dist and copy:dist tasks in dry-run'); + } else { + tasks.push('build', 'clean:dist', 'copy:dist'); + } + + tasks = tasks.concat([ 'test:unit', - 'clean:dist', - 'copy:dist', 'bump-only:' + releaseType, 'changelog:' + releaseType, 'bump-commit', 'coffee:compile', 'gh-release' ]); + + grunt.task.run(tasks); }); grunt.registerTask('save-revision', function() { diff --git a/bower.json b/bower.json index 603827af..e14e95cc 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "accessibility-developer-tools", - "version": "2.7.1", + "version": "2.8.0-rc.0", "homepage": "https://github.com/GoogleChrome/accessibility-developer-tools", "authors": [ "Google" diff --git a/dist/js/axs_testing.js b/dist/js/axs_testing.js index 1f1603e8..fb1671e2 100644 --- a/dist/js/axs_testing.js +++ b/dist/js/axs_testing.js @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * Generated from http://github.com/GoogleChrome/accessibility-developer-tools/tree/ef4fec4549616cd69a0f0f732d43c838d8ab17eb + * Generated from http://github.com/GoogleChrome/accessibility-developer-tools/tree/407f318f682c802645c3ef867ed97854de4b655b * * See project README for build steps. */ @@ -523,25 +523,166 @@ reserved:!0}], MATH:[{role:"", allowed:["presentation"]}], MENU:[{role:"toolbar" allowed:["application", "document", "img", "presentation"]}], OL:[{role:"list", allowed:"directory group listbox menu menubar tablist toolbar tree presentation".split(" ")}], OPTGROUP:[{role:"", allowed:["presentation"]}], OPTION:[{role:"option"}], OUTPUT:[{role:"status", allowed:["*"]}], PARAM:[{role:"", reserved:!0}], PICTURE:[{role:"", reserved:!0}], PROGRESS:[{role:"progressbar", allowed:["presentation"]}], SCRIPT:[{role:"", reserved:!0}], SECTION:[{role:"region", allowed:"alert alertdialog application contentinfo dialog document log marquee search status presentation".split(" ")}], SELECT:[{role:"listbox"}], SOURCE:[{role:"", reserved:!0}], SPAN:[{role:"", allowed:["*"]}], STYLE:[{role:"", reserved:!0}], SVG:[{role:"", allowed:["application", "document", "img", "presentation"]}], SUMMARY:[{role:"", allowed:["presentation"]}], TABLE:[{role:"", allowed:["*"]}], TEMPLATE:[{role:"", reserved:!0}], TEXTAREA:[{role:"textbox"}], TBODY:[{role:"rowgroup", allowed:["*"]}], THEAD:[{role:"rowgroup", allowed:["*"]}], TFOOT:[{role:"rowgroup", allowed:["*"]}], TITLE:[{role:"", reserved:!0}], TD:[{role:"", allowed:["*"]}], TH:[{role:"", allowed:["*"]}], TR:[{role:"", allowed:["*"]}], TRACK:[{role:"", reserved:!0}], UL:[{role:"list", allowed:"directory group listbox menu menubar tablist toolbar tree presentation".split(" ")}], VIDEO:[{role:"", allowed:["application", "presentation"]}]}; -axs.utils = {}; -axs.utils.FOCUSABLE_ELEMENTS_SELECTOR = "input:not([type=hidden]):not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),a[href],iframe,[tabindex]"; -axs.utils.Color = function(a, b, c, d) { +axs.color = {}; +axs.color.Color = function(a, b, c, d) { this.red = a; this.green = b; this.blue = c; this.alpha = d; }; -axs.utils.calculateContrastRatio = function(a, b) { - if (!a || !b) { - return null; - } - 1 > a.alpha && (a = axs.utils.flattenColors(a, b)); - var c = axs.utils.calculateLuminance(a), d = axs.utils.calculateLuminance(b); +axs.color.YCbCr = function(a) { + this.luma = this.z = a[0]; + this.Cb = this.x = a[1]; + this.Cr = this.y = a[2]; +}; +axs.color.YCbCr.prototype = {multiply:function(a) { + return new axs.color.YCbCr([this.luma * a, this.Cb * a, this.Cr * a]); +}, add:function(a) { + return new axs.color.YCbCr([this.luma + a.luma, this.Cb + a.Cb, this.Cr + a.Cr]); +}, subtract:function(a) { + return new axs.color.YCbCr([this.luma - a.luma, this.Cb - a.Cb, this.Cr - a.Cr]); +}}; +axs.color.calculateContrastRatio = function(a, b) { + 1 > a.alpha && (a = axs.color.flattenColors(a, b)); + var c = axs.color.calculateLuminance(a), d = axs.color.calculateLuminance(b); return (Math.max(c, d) + .05) / (Math.min(c, d) + .05); }; -axs.utils.luminanceRatio = function(a, b) { +axs.color.calculateLuminance = function(a) { + return axs.color.toYCbCr(a).luma; +}; +axs.color.luminanceRatio = function(a, b) { return (Math.max(a, b) + .05) / (Math.min(a, b) + .05); }; +axs.color.parseColor = function(a) { + var b = a.match(/^rgb\((\d+), (\d+), (\d+)\)$/); + if (b) { + a = parseInt(b[1], 10); + var c = parseInt(b[2], 10), d = parseInt(b[3], 10); + return new axs.color.Color(a, c, d, 1); + } + return (b = a.match(/^rgba\((\d+), (\d+), (\d+), (\d*(\.\d+)?)\)/)) ? (a = parseInt(b[1], 10), c = parseInt(b[2], 10), d = parseInt(b[3], 10), b = parseFloat(b[4]), new axs.color.Color(a, c, d, b)) : null; +}; +axs.color.colorChannelToString = function(a) { + a = Math.round(a); + return 15 >= a ? "0" + a.toString(16) : a.toString(16); +}; +axs.color.colorToString = function(a) { + return 1 == a.alpha ? "#" + axs.color.colorChannelToString(a.red) + axs.color.colorChannelToString(a.green) + axs.color.colorChannelToString(a.blue) : "rgba(" + [a.red, a.green, a.blue, a.alpha].join() + ")"; +}; +axs.color.luminanceFromContrastRatio = function(a, b, c) { + return c ? (a + .05) * b - .05 : (a + .05) / b - .05; +}; +axs.color.translateColor = function(a, b) { + for (var c = b > a.luma ? axs.color.WHITE_YCC : axs.color.BLACK_YCC, d = c == axs.color.WHITE_YCC ? axs.color.YCC_CUBE_FACES_WHITE : axs.color.YCC_CUBE_FACES_BLACK, e = new axs.color.YCbCr([0, a.Cb, a.Cr]), f = new axs.color.YCbCr([1, a.Cb, a.Cr]), f = {a:e, b:f}, e = null, g = 0;g < d.length && !(e = axs.color.findIntersection(f, d[g]), 0 <= e.z && 1 >= e.z);g++) { + } + if (!e) { + throw "Couldn't find intersection with YCbCr color cube for Cb=" + a.Cb + ", Cr=" + a.Cr + "."; + } + if (e.x != a.x || e.y != a.y) { + throw "Intersection has wrong Cb/Cr values."; + } + if (Math.abs(c.luma - e.luma) < Math.abs(c.luma - b)) { + return c = [b, a.Cb, a.Cr], axs.color.fromYCbCrArray(c); + } + c = (b - e.luma) / (c.luma - e.luma); + c = [b, e.Cb - e.Cb * c, e.Cr - e.Cr * c]; + return axs.color.fromYCbCrArray(c); +}; +axs.color.suggestColors = function(a, b, c) { + var d = {}, e = axs.color.calculateLuminance(a), f = axs.color.calculateLuminance(b), g = f > e, h = axs.color.toYCbCr(b), k = axs.color.toYCbCr(a), m; + for (m in c) { + var l = c[m], n = axs.color.luminanceFromContrastRatio(e, l + .02, g); + if (1 >= n && 0 <= n) { + var p = axs.color.translateColor(h, n), l = axs.color.calculateContrastRatio(p, a), n = {}; + n.fg = axs.color.colorToString(p); + n.bg = axs.color.colorToString(a); + n.contrast = l.toFixed(2); + d[m] = n; + } else { + l = axs.color.luminanceFromContrastRatio(f, l + .02, !g), 1 >= l && 0 <= l && (p = axs.color.translateColor(k, l), l = axs.color.calculateContrastRatio(b, p), n = {}, n.bg = axs.color.colorToString(p), n.fg = axs.color.colorToString(b), n.contrast = l.toFixed(2), d[m] = n); + } + } + return d; +}; +axs.color.flattenColors = function(a, b) { + var c = a.alpha; + return new axs.color.Color((1 - c) * b.red + c * a.red, (1 - c) * b.green + c * a.green, (1 - c) * b.blue + c * a.blue, a.alpha + b.alpha * (1 - a.alpha)); +}; +axs.color.multiplyMatrixVector = function(a, b) { + var c = b[0], d = b[1], e = b[2]; + return [a[0][0] * c + a[0][1] * d + a[0][2] * e, a[1][0] * c + a[1][1] * d + a[1][2] * e, a[2][0] * c + a[2][1] * d + a[2][2] * e]; +}; +axs.color.toYCbCr = function(a) { + var b = a.red / 255, c = a.green / 255; + a = a.blue / 255; + b = .03928 >= b ? b / 12.92 : Math.pow((b + .055) / 1.055, 2.4); + c = .03928 >= c ? c / 12.92 : Math.pow((c + .055) / 1.055, 2.4); + a = .03928 >= a ? a / 12.92 : Math.pow((a + .055) / 1.055, 2.4); + return new axs.color.YCbCr(axs.color.multiplyMatrixVector(axs.color.YCC_MATRIX, [b, c, a])); +}; +axs.color.fromYCbCr = function(a) { + return axs.color.fromYCbCrArray([a.luma, a.Cb, a.Cr]); +}; +axs.color.fromYCbCrArray = function(a) { + var b = axs.color.multiplyMatrixVector(axs.color.INVERTED_YCC_MATRIX, a), c = b[0]; + a = b[1]; + b = b[2]; + c = .00303949 >= c ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - .055; + a = .00303949 >= a ? 12.92 * a : 1.055 * Math.pow(a, 1 / 2.4) - .055; + b = .00303949 >= b ? 12.92 * b : 1.055 * Math.pow(b, 1 / 2.4) - .055; + c = Math.min(Math.max(Math.round(255 * c), 0), 255); + a = Math.min(Math.max(Math.round(255 * a), 0), 255); + b = Math.min(Math.max(Math.round(255 * b), 0), 255); + return new axs.color.Color(c, a, b, 1); +}; +axs.color.RGBToYCbCrMatrix = function(a, b) { + return [[a, 1 - a - b, b], [-a / (2 - 2 * b), (a + b - 1) / (2 - 2 * b), (1 - b) / (2 - 2 * b)], [(1 - a) / (2 - 2 * a), (a + b - 1) / (2 - 2 * a), -b / (2 - 2 * a)]]; +}; +axs.color.invert3x3Matrix = function(a) { + var b = a[0][0], c = a[0][1], d = a[0][2], e = a[1][0], f = a[1][1], g = a[1][2], h = a[2][0], k = a[2][1]; + a = a[2][2]; + return axs.color.scalarMultiplyMatrix([[f * a - g * k, d * k - c * a, c * g - d * f], [g * h - e * a, b * a - d * h, d * e - b * g], [e * k - f * h, h * c - b * k, b * f - c * e]], 1 / (b * (f * a - g * k) - c * (a * e - g * h) + d * (e * k - f * h))); +}; +axs.color.findIntersection = function(a, b) { + var c = [a.a.x - b.p0.x, a.a.y - b.p0.y, a.a.z - b.p0.z], d = axs.color.invert3x3Matrix([[a.a.x - a.b.x, b.p1.x - b.p0.x, b.p2.x - b.p0.x], [a.a.y - a.b.y, b.p1.y - b.p0.y, b.p2.y - b.p0.y], [a.a.z - a.b.z, b.p1.z - b.p0.z, b.p2.z - b.p0.z]]), c = axs.color.multiplyMatrixVector(d, c)[0]; + return a.a.add(a.b.subtract(a.a).multiply(c)); +}; +axs.color.scalarMultiplyMatrix = function(a, b) { + for (var c = [], d = 0;3 > d;d++) { + c[d] = axs.color.scalarMultiplyVector(a[d], b); + } + return c; +}; +axs.color.scalarMultiplyVector = function(a, b) { + for (var c = [], d = 0;d < a.length;d++) { + c[d] = a[d] * b; + } + return c; +}; +axs.color.kR = .2126; +axs.color.kB = .0722; +axs.color.YCC_MATRIX = axs.color.RGBToYCbCrMatrix(axs.color.kR, axs.color.kB); +axs.color.INVERTED_YCC_MATRIX = axs.color.invert3x3Matrix(axs.color.YCC_MATRIX); +axs.color.BLACK = new axs.color.Color(0, 0, 0, 1); +axs.color.BLACK_YCC = axs.color.toYCbCr(axs.color.BLACK); +axs.color.WHITE = new axs.color.Color(255, 255, 255, 1); +axs.color.WHITE_YCC = axs.color.toYCbCr(axs.color.WHITE); +axs.color.RED = new axs.color.Color(255, 0, 0, 1); +axs.color.RED_YCC = axs.color.toYCbCr(axs.color.RED); +axs.color.GREEN = new axs.color.Color(0, 255, 0, 1); +axs.color.GREEN_YCC = axs.color.toYCbCr(axs.color.GREEN); +axs.color.BLUE = new axs.color.Color(0, 0, 255, 1); +axs.color.BLUE_YCC = axs.color.toYCbCr(axs.color.BLUE); +axs.color.CYAN = new axs.color.Color(0, 255, 255, 1); +axs.color.CYAN_YCC = axs.color.toYCbCr(axs.color.CYAN); +axs.color.MAGENTA = new axs.color.Color(255, 0, 255, 1); +axs.color.MAGENTA_YCC = axs.color.toYCbCr(axs.color.MAGENTA); +axs.color.YELLOW = new axs.color.Color(255, 255, 0, 1); +axs.color.YELLOW_YCC = axs.color.toYCbCr(axs.color.YELLOW); +axs.color.YCC_CUBE_FACES_BLACK = [{p0:axs.color.BLACK_YCC, p1:axs.color.RED_YCC, p2:axs.color.GREEN_YCC}, {p0:axs.color.BLACK_YCC, p1:axs.color.GREEN_YCC, p2:axs.color.BLUE_YCC}, {p0:axs.color.BLACK_YCC, p1:axs.color.BLUE_YCC, p2:axs.color.RED_YCC}]; +axs.color.YCC_CUBE_FACES_WHITE = [{p0:axs.color.WHITE_YCC, p1:axs.color.CYAN_YCC, p2:axs.color.MAGENTA_YCC}, {p0:axs.color.WHITE_YCC, p1:axs.color.MAGENTA_YCC, p2:axs.color.YELLOW_YCC}, {p0:axs.color.WHITE_YCC, p1:axs.color.YELLOW_YCC, p2:axs.color.CYAN_YCC}]; +axs.utils = {}; +axs.utils.FOCUSABLE_ELEMENTS_SELECTOR = "input:not([type=hidden]):not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),a[href],iframe,[tabindex]"; axs.utils.parentElement = function(a) { if (!a) { return null; @@ -669,7 +810,7 @@ axs.utils.isLargeFont = function(a) { return !1; }; axs.utils.getBgColor = function(a, b) { - var c = axs.utils.parseColor(a.backgroundColor); + var c = axs.color.parseColor(a.backgroundColor); if (!c) { return null; } @@ -679,7 +820,7 @@ axs.utils.getBgColor = function(a, b) { if (null == d) { return null; } - c = axs.utils.flattenColors(c, d); + c = axs.color.flattenColors(c, d); } return c; }; @@ -689,146 +830,28 @@ axs.utils.getParentBgColor = function(a) { for (var c = null;b = axs.utils.parentElement(b);) { var d = window.getComputedStyle(b, null); if (d) { - var e = axs.utils.parseColor(d.backgroundColor); + var e = axs.color.parseColor(d.backgroundColor); if (e && (1 > d.opacity && (e.alpha *= d.opacity), 0 != e.alpha && (a.push(e), 1 == e.alpha))) { c = !0; break; } } } - c || a.push(new axs.utils.Color(255, 255, 255, 1)); + c || a.push(new axs.color.Color(255, 255, 255, 1)); for (b = a.pop();a.length;) { - c = a.pop(), b = axs.utils.flattenColors(c, b); + c = a.pop(), b = axs.color.flattenColors(c, b); } return b; }; axs.utils.getFgColor = function(a, b, c) { - var d = axs.utils.parseColor(a.color); + var d = axs.color.parseColor(a.color); if (!d) { return null; } - 1 > d.alpha && (d = axs.utils.flattenColors(d, c)); - 1 > a.opacity && (b = axs.utils.getParentBgColor(b), d.alpha *= a.opacity, d = axs.utils.flattenColors(d, b)); + 1 > d.alpha && (d = axs.color.flattenColors(d, c)); + 1 > a.opacity && (b = axs.utils.getParentBgColor(b), d.alpha *= a.opacity, d = axs.color.flattenColors(d, b)); return d; }; -axs.utils.parseColor = function(a) { - var b = a.match(/^rgb\((\d+), (\d+), (\d+)\)$/); - if (b) { - a = parseInt(b[1], 10); - var c = parseInt(b[2], 10), d = parseInt(b[3], 10); - return new axs.utils.Color(a, c, d, 1); - } - return (b = a.match(/^rgba\((\d+), (\d+), (\d+), (\d*(\.\d+)?)\)/)) ? (a = parseInt(b[1], 10), c = parseInt(b[2], 10), d = parseInt(b[3], 10), b = parseFloat(b[4]), new axs.utils.Color(a, c, d, b)) : null; -}; -axs.utils.colorChannelToString = function(a) { - a = Math.round(a); - return 15 >= a ? "0" + a.toString(16) : a.toString(16); -}; -axs.utils.colorToString = function(a) { - return 1 == a.alpha ? "#" + axs.utils.colorChannelToString(a.red) + axs.utils.colorChannelToString(a.green) + axs.utils.colorChannelToString(a.blue) : "rgba(" + [a.red, a.green, a.blue, a.alpha].join() + ")"; -}; -axs.utils.luminanceFromContrastRatio = function(a, b, c) { - return c ? (a + .05) * b - .05 : (a + .05) / b - .05; -}; -axs.utils.translateColor = function(a, b) { - var c = a[0], c = (b - c) / ((c > b ? 0 : 1) - c); - return axs.utils.fromYCC([b, a[1] - a[1] * c, a[2] - a[2] * c]); -}; -axs.utils.suggestColors = function(a, b, c, d) { - if (!axs.utils.isLowContrast(c, d, !0)) { - return null; - } - var e = {}, f = axs.utils.calculateLuminance(a), g = axs.utils.calculateLuminance(b), h = axs.utils.isLargeFont(d) ? 3 : 4.5, k = axs.utils.isLargeFont(d) ? 4.5 : 7, m = g > f, l = axs.utils.luminanceFromContrastRatio(f, h + .02, m), n = axs.utils.luminanceFromContrastRatio(f, k + .02, m), p = axs.utils.toYCC(b); - if (axs.utils.isLowContrast(c, d, !1) && 1 >= l && 0 <= l) { - var q = axs.utils.translateColor(p, l), l = axs.utils.calculateContrastRatio(q, a), f = {}; - f.fg = axs.utils.colorToString(q); - f.bg = axs.utils.colorToString(a); - f.contrast = l.toFixed(2); - e.AA = f; - } - axs.utils.isLowContrast(c, d, !0) && 1 >= n && 0 <= n && (n = axs.utils.translateColor(p, n), l = axs.utils.calculateContrastRatio(n, a), f = {}, f.fg = axs.utils.colorToString(n), f.bg = axs.utils.colorToString(a), f.contrast = l.toFixed(2), e.AAA = f); - h = axs.utils.luminanceFromContrastRatio(g, h + .02, !m); - g = axs.utils.luminanceFromContrastRatio(g, k + .02, !m); - a = axs.utils.toYCC(a); - !("AA" in e) && axs.utils.isLowContrast(c, d, !1) && 1 >= h && 0 <= h && (k = axs.utils.translateColor(a, h), l = axs.utils.calculateContrastRatio(b, k), f = {}, f.bg = axs.utils.colorToString(k), f.fg = axs.utils.colorToString(b), f.contrast = l.toFixed(2), e.AA = f); - !("AAA" in e) && axs.utils.isLowContrast(c, d, !0) && 1 >= g && 0 <= g && (c = axs.utils.translateColor(a, g), l = axs.utils.calculateContrastRatio(b, c), f = {}, f.bg = axs.utils.colorToString(c), f.fg = axs.utils.colorToString(b), f.contrast = l.toFixed(2), e.AAA = f); - return e; -}; -axs.utils.flattenColors = function(a, b) { - var c = a.alpha; - return new axs.utils.Color((1 - c) * b.red + c * a.red, (1 - c) * b.green + c * a.green, (1 - c) * b.blue + c * a.blue, a.alpha + b.alpha * (1 - a.alpha)); -}; -axs.utils.calculateLuminance = function(a) { - return axs.utils.toYCC(a)[0]; -}; -axs.utils.RGBToYCCMatrix = function(a, b) { - return [[a, 1 - a - b, b], [-a / (2 - 2 * b), (a + b - 1) / (2 - 2 * b), (1 - b) / (2 - 2 * b)], [(1 - a) / (2 - 2 * a), (a + b - 1) / (2 - 2 * a), -b / (2 - 2 * a)]]; -}; -axs.utils.invert3x3Matrix = function(a) { - var b = a[0][0], c = a[0][1], d = a[0][2], e = a[1][0], f = a[1][1], g = a[1][2], h = a[2][0], k = a[2][1]; - a = a[2][2]; - return axs.utils.scalarMultiplyMatrix([[f * a - g * k, d * k - c * a, c * g - d * f], [g * h - e * a, b * a - d * h, d * e - b * g], [e * k - f * h, h * c - b * k, b * f - c * e]], 1 / (b * (f * a - g * k) - c * (a * e - g * h) + d * (e * k - f * h))); -}; -axs.utils.scalarMultiplyMatrix = function(a, b) { - for (var c = [[], [], []], d = 0;3 > d;d++) { - for (var e = 0;3 > e;e++) { - c[d][e] = a[d][e] * b; - } - } - return c; -}; -axs.utils.kR = .2126; -axs.utils.kB = .0722; -axs.utils.YCC_MATRIX = axs.utils.RGBToYCCMatrix(axs.utils.kR, axs.utils.kB); -axs.utils.INVERTED_YCC_MATRIX = axs.utils.invert3x3Matrix(axs.utils.YCC_MATRIX); -axs.utils.convertColor = function(a, b) { - var c = b[0], d = b[1], e = b[2]; - return [a[0][0] * c + a[0][1] * d + a[0][2] * e, a[1][0] * c + a[1][1] * d + a[1][2] * e, a[2][0] * c + a[2][1] * d + a[2][2] * e]; -}; -axs.utils.multiplyMatrices = function(a, b) { - for (var c = [[], [], []], d = 0;3 > d;d++) { - for (var e = 0;3 > e;e++) { - c[d][e] = a[d][0] * b[0][e] + a[d][1] * b[1][e] + a[d][2] * b[2][e]; - } - } - return c; -}; -axs.utils.toYCC = function(a) { - var b = a.red / 255, c = a.green / 255; - a = a.blue / 255; - b = .03928 >= b ? b / 12.92 : Math.pow((b + .055) / 1.055, 2.4); - c = .03928 >= c ? c / 12.92 : Math.pow((c + .055) / 1.055, 2.4); - a = .03928 >= a ? a / 12.92 : Math.pow((a + .055) / 1.055, 2.4); - return axs.utils.convertColor(axs.utils.YCC_MATRIX, [b, c, a]); -}; -axs.utils.fromYCC = function(a) { - var b = axs.utils.convertColor(axs.utils.INVERTED_YCC_MATRIX, a), c = b[0]; - a = b[1]; - b = b[2]; - c = .00303949 >= c ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - .055; - a = .00303949 >= a ? 12.92 * a : 1.055 * Math.pow(a, 1 / 2.4) - .055; - b = .00303949 >= b ? 12.92 * b : 1.055 * Math.pow(b, 1 / 2.4) - .055; - c = Math.min(Math.max(Math.round(255 * c), 0), 255); - a = Math.min(Math.max(Math.round(255 * a), 0), 255); - b = Math.min(Math.max(Math.round(255 * b), 0), 255); - return new axs.utils.Color(c, a, b, 1); -}; -axs.utils.scalarMultiplyMatrix = function(a, b) { - for (var c = [[], [], []], d = 0;3 > d;d++) { - for (var e = 0;3 > e;e++) { - c[d][e] = a[d][e] * b; - } - } - return c; -}; -axs.utils.multiplyMatrices = function(a, b) { - for (var c = [[], [], []], d = 0;3 > d;d++) { - for (var e = 0;3 > e;e++) { - c[d][e] = a[d][0] * b[0][e] + a[d][1] * b[1][e] + a[d][2] * b[2][e]; - } - } - return c; -}; axs.utils.getContrastRatioForElement = function(a) { var b = window.getComputedStyle(a, null); return axs.utils.getContrastRatioForElementWithComputedStyle(b, a); @@ -842,7 +865,7 @@ axs.utils.getContrastRatioForElementWithComputedStyle = function(a, b) { return null; } var d = axs.utils.getFgColor(a, b, c); - return d ? axs.utils.calculateContrastRatio(d, c) : null; + return d ? axs.color.calculateContrastRatio(d, c) : null; }; axs.utils.isNativeTextElement = function(a) { var b = a.tagName.toLowerCase(); @@ -1189,16 +1212,22 @@ axs.properties.getContrastRatioProperties = function(a) { if (!d) { return null; } - b.backgroundColor = axs.utils.colorToString(d); + b.backgroundColor = axs.color.colorToString(d); var e = axs.utils.getFgColor(c, a, d); - b.foregroundColor = axs.utils.colorToString(e); + b.foregroundColor = axs.color.colorToString(e); a = axs.utils.getContrastRatioForElementWithComputedStyle(c, a); if (!a) { return null; } b.value = a.toFixed(2); axs.utils.isLowContrast(a, c) && (b.alert = !0); - (c = axs.utils.suggestColors(d, e, a, c)) && Object.keys(c).length && (b.suggestedColors = c); + var f = axs.utils.isLargeFont(c) ? 3 : 4.5, c = axs.utils.isLargeFont(c) ? 4.5 : 7, g = {}; + f > a && (g.AA = f); + c > a && (g.AAA = c); + if (!Object.keys(g).length) { + return b; + } + (d = axs.color.suggestColors(d, e, g)) && Object.keys(d).length && (b.suggestedColors = d); return b; }; axs.properties.findTextAlternatives = function(a, b, c, d) { @@ -1527,19 +1556,20 @@ axs.AuditRule.collectMatchingElements = function(a, b, c, d) { } } if (e && "content" == e.localName) { - for (e = e.getDistributedNodes(), f = 0;f < e.length;f++) { - axs.AuditRule.collectMatchingElements(e[f], b, c, d); + for (var f = e.getDistributedNodes(), g = 0;g < f.length;g++) { + axs.AuditRule.collectMatchingElements(f[g], b, c, d); } } else { if (e && "shadow" == e.localName) { if (f = e, d) { - for (e = f.getDistributedNodes(), f = 0;f < e.length;f++) { - axs.AuditRule.collectMatchingElements(e[f], b, c, d); + for (f = f.getDistributedNodes(), g = 0;g < f.length;g++) { + axs.AuditRule.collectMatchingElements(f[g], b, c, d); } } else { console.warn("ShadowRoot not provided for", e); } } + e && "iframe" == e.localName && e.contentDocument && axs.AuditRule.collectMatchingElements(e.contentDocument, b, c, d); for (a = a.firstChild;null != a;) { axs.AuditRule.collectMatchingElements(a, b, c, d), a = a.nextSibling; } @@ -1574,15 +1604,17 @@ axs.AuditRule.prototype.run = function(a) { axs.AuditRules = {}; (function() { var a = {}, b = {}; + axs.AuditRules.specs = {}; axs.AuditRules.addRule = function(c) { - c = new axs.AuditRule(c); - if (c.code in b) { - throw Error('Can not add audit rule with same code: "' + c.code + '"'); + var d = new axs.AuditRule(c); + if (d.code in b) { + throw Error('Can not add audit rule with same code: "' + d.code + '"'); } - if (c.name in a) { - throw Error('Can not add audit rule with same name: "' + c.name + '"'); + if (d.name in a) { + throw Error('Can not add audit rule with same name: "' + d.name + '"'); } - a[c.name] = b[c.code] = c; + a[d.name] = b[d.code] = d; + axs.AuditRules.specs[c.name] = c; }; axs.AuditRules.getRule = function(c) { return a[c] || b[c] || null; diff --git a/package.json b/package.json index 121ce66f..8f072e16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "accessibility-developer-tools", - "version": "2.7.1", + "version": "2.8.0-rc.0", "repository": { "type": "git", "url": "http://github.com/GoogleChrome/accessibility-developer-tools" diff --git a/src/js/AccessibilityUtils.js b/src/js/AccessibilityUtils.js index 8f457bad..ad5db16d 100644 --- a/src/js/AccessibilityUtils.js +++ b/src/js/AccessibilityUtils.js @@ -13,9 +13,10 @@ // limitations under the License. goog.require('axs.browserUtils'); +goog.require('axs.color'); +goog.require('axs.color.Color'); goog.require('axs.constants'); goog.provide('axs.utils'); -goog.provide('axs.utils.Color'); /** * @const @@ -30,54 +31,6 @@ axs.utils.FOCUSABLE_ELEMENTS_SELECTOR = 'iframe,' + '[tabindex]'; -/** - * @constructor - * @param {number} red - * @param {number} green - * @param {number} blue - * @param {number} alpha - */ -axs.utils.Color = function(red, green, blue, alpha) { - /** @type {number} */ - this.red = red; - - /** @type {number} */ - this.green = green; - - /** @type {number} */ - this.blue = blue; - - /** @type {number} */ - this.alpha = alpha; -}; - -/** - * Calculate the contrast ratio between the two given colors. Returns the ratio - * to 1, for example for two two colors with a contrast ratio of 21:1, this - * function will return 21. - * @param {axs.utils.Color} fgColor - * @param {axs.utils.Color} bgColor - * @return {?number} - */ -axs.utils.calculateContrastRatio = function(fgColor, bgColor) { - if (!fgColor || !bgColor) - return null; - - if (fgColor.alpha < 1) - fgColor = axs.utils.flattenColors(fgColor, bgColor); - - var fgLuminance = axs.utils.calculateLuminance(fgColor); - var bgLuminance = axs.utils.calculateLuminance(bgColor); - var contrastRatio = (Math.max(fgLuminance, bgLuminance) + 0.05) / - (Math.min(fgLuminance, bgLuminance) + 0.05); - return contrastRatio; -}; - -axs.utils.luminanceRatio = function(luminance1, luminance2) { - return (Math.max(luminance1, luminance2) + 0.05) / - (Math.min(luminance1, luminance2) + 0.05); -}; - /** * Returns the nearest ancestor which is an Element. * @param {Node} node @@ -403,11 +356,11 @@ axs.utils.isLargeFont = function(style) { /** * @param {CSSStyleDeclaration} style * @param {Element} element - * @return {?axs.utils.Color} + * @return {?axs.color.Color} */ axs.utils.getBgColor = function(style, element) { var bgColorString = style.backgroundColor; - var bgColor = axs.utils.parseColor(bgColorString); + var bgColor = axs.color.parseColor(bgColorString); if (!bgColor) return null; @@ -419,7 +372,7 @@ axs.utils.getBgColor = function(style, element) { if (parentBg == null) return null; - bgColor = axs.utils.flattenColors(bgColor, parentBg); + bgColor = axs.color.flattenColors(bgColor, parentBg); } return bgColor; }; @@ -427,7 +380,7 @@ axs.utils.getBgColor = function(style, element) { /** * Gets the effective background color of the parent of |element|. * @param {Element} element - * @return {?axs.utils.Color} + * @return {?axs.color.Color} */ axs.utils.getParentBgColor = function(element) { /** @type {Element} */ var parent = element; @@ -438,7 +391,7 @@ axs.utils.getParentBgColor = function(element) { if (!computedStyle) continue; - var parentBg = axs.utils.parseColor(computedStyle.backgroundColor); + var parentBg = axs.color.parseColor(computedStyle.backgroundColor); if (!parentBg) continue; @@ -457,12 +410,12 @@ axs.utils.getParentBgColor = function(element) { } if (!foundSolidColor) - bgStack.push(new axs.utils.Color(255, 255, 255, 1)); + bgStack.push(new axs.color.Color(255, 255, 255, 1)); var bg = bgStack.pop(); while (bgStack.length) { var fg = bgStack.pop(); - bg = axs.utils.flattenColors(fg, bg); + bg = axs.color.flattenColors(fg, bg); } return bg; }; @@ -470,407 +423,29 @@ axs.utils.getParentBgColor = function(element) { /** * @param {CSSStyleDeclaration} style * @param {Element} element - * @param {axs.utils.Color} bgColor The background color, which may come from + * @param {axs.color.Color} bgColor The background color, which may come from * another element (such as a parent element), for flattening into the * foreground color. - * @return {?axs.utils.Color} + * @return {?axs.color.Color} */ axs.utils.getFgColor = function(style, element, bgColor) { var fgColorString = style.color; - var fgColor = axs.utils.parseColor(fgColorString); + var fgColor = axs.color.parseColor(fgColorString); if (!fgColor) return null; if (fgColor.alpha < 1) - fgColor = axs.utils.flattenColors(fgColor, bgColor); + fgColor = axs.color.flattenColors(fgColor, bgColor); if (style.opacity < 1) { var parentBg = axs.utils.getParentBgColor(element); fgColor.alpha = fgColor.alpha * style.opacity; - fgColor = axs.utils.flattenColors(fgColor, parentBg); + fgColor = axs.color.flattenColors(fgColor, parentBg); } return fgColor; }; -/** - * @param {string} colorString The color string from CSS. - * @return {?axs.utils.Color} - */ -axs.utils.parseColor = function(colorString) { - var rgbRegex = /^rgb\((\d+), (\d+), (\d+)\)$/; - var match = colorString.match(rgbRegex); - - if (match) { - var r = parseInt(match[1], 10); - var g = parseInt(match[2], 10); - var b = parseInt(match[3], 10); - var a = 1; - return new axs.utils.Color(r, g, b, a); - } - - var rgbaRegex = /^rgba\((\d+), (\d+), (\d+), (\d*(\.\d+)?)\)/; - match = colorString.match(rgbaRegex); - if (match) { - var r = parseInt(match[1], 10); - var g = parseInt(match[2], 10); - var b = parseInt(match[3], 10); - var a = parseFloat(match[4]); - return new axs.utils.Color(r, g, b, a); - } - - return null; -}; - -/** - * @param {number} value The value of a color channel, 0 <= value <= 0xFF - * @return {string} - */ -axs.utils.colorChannelToString = function(value) { - value = Math.round(value); - if (value <= 0xF) - return '0' + value.toString(16); - return value.toString(16); -}; - -/** - * @param {axs.utils.Color} color - * @return {string} - */ -axs.utils.colorToString = function(color) { - if (color.alpha == 1) { - return '#' + axs.utils.colorChannelToString(color.red) + - axs.utils.colorChannelToString(color.green) + axs.utils.colorChannelToString(color.blue); - } - else - return 'rgba(' + [color.red, color.green, color.blue, color.alpha].join(',') + ')'; -}; - -axs.utils.luminanceFromContrastRatio = function(luminance, contrast, higher) { - if (higher) { - var newLuminance = (luminance + 0.05) * contrast - 0.05; - return newLuminance; - } else { - var newLuminance = (luminance + 0.05) / contrast - 0.05; - return newLuminance; - } -}; - -axs.utils.translateColor = function(ycc, luminance) { - var oldLuminance = ycc[0]; - if (oldLuminance > luminance) - var endpoint = 0; - else - var endpoint = 1; - - var d = luminance - oldLuminance; - var scale = d / (endpoint - oldLuminance); - - /** @type {Array.} */ var translatedColor = [ luminance, - ycc[1] - ycc[1] * scale, - ycc[2] - ycc[2] * scale ]; - var rgb = axs.utils.fromYCC(translatedColor); - return rgb; -}; - -/** - * @param {axs.utils.Color} bgColor - * @param {axs.utils.Color} fgColor - * @param {number} contrastRatio - * @param {CSSStyleDeclaration} style - * @return {Object} - */ -axs.utils.suggestColors = function(bgColor, fgColor, contrastRatio, style) { - if (!axs.utils.isLowContrast(contrastRatio, style, true)) - return null; - var colors = {}; - var bgLuminance = axs.utils.calculateLuminance(bgColor); - var fgLuminance = axs.utils.calculateLuminance(fgColor); - - var levelAAContrast = axs.utils.isLargeFont(style) ? 3.0 : 4.5; - var levelAAAContrast = axs.utils.isLargeFont(style) ? 4.5 : 7.0; - var fgLuminanceIsHigher = fgLuminance > bgLuminance; - var desiredFgLuminanceAA = axs.utils.luminanceFromContrastRatio(bgLuminance, levelAAContrast + 0.02, fgLuminanceIsHigher); - var desiredFgLuminanceAAA = axs.utils.luminanceFromContrastRatio(bgLuminance, levelAAAContrast + 0.02, fgLuminanceIsHigher); - var fgYCC = axs.utils.toYCC(fgColor); - - if (axs.utils.isLowContrast(contrastRatio, style, false) && - desiredFgLuminanceAA <= 1 && desiredFgLuminanceAA >= 0) { - var newFgColorAA = axs.utils.translateColor(fgYCC, desiredFgLuminanceAA); - var newContrastRatioAA = axs.utils.calculateContrastRatio(newFgColorAA, bgColor); - var suggestedColorsAA = {}; - suggestedColorsAA['fg'] = axs.utils.colorToString(newFgColorAA); - suggestedColorsAA['bg'] = axs.utils.colorToString(bgColor); - suggestedColorsAA['contrast'] = newContrastRatioAA.toFixed(2); - colors['AA'] = suggestedColorsAA; - } - if (axs.utils.isLowContrast(contrastRatio, style, true) && - desiredFgLuminanceAAA <= 1 && desiredFgLuminanceAAA >= 0) { - var newFgColorAAA = axs.utils.translateColor(fgYCC, desiredFgLuminanceAAA); - var newContrastRatioAAA = axs.utils.calculateContrastRatio(newFgColorAAA, bgColor); - var suggestedColorsAAA = {}; - suggestedColorsAAA['fg'] = axs.utils.colorToString(newFgColorAAA); - suggestedColorsAAA['bg'] = axs.utils.colorToString(bgColor); - suggestedColorsAAA['contrast'] = newContrastRatioAAA.toFixed(2); - colors['AAA'] = suggestedColorsAAA; - } - var desiredBgLuminanceAA = axs.utils.luminanceFromContrastRatio(fgLuminance, levelAAContrast + 0.02, !fgLuminanceIsHigher); - var desiredBgLuminanceAAA = axs.utils.luminanceFromContrastRatio(fgLuminance, levelAAAContrast + 0.02, !fgLuminanceIsHigher); - var bgYCC = axs.utils.toYCC(bgColor); - - if (!('AA' in colors) && axs.utils.isLowContrast(contrastRatio, style, false) && - desiredBgLuminanceAA <= 1 && desiredBgLuminanceAA >= 0) { - var newBgColorAA = axs.utils.translateColor(bgYCC, desiredBgLuminanceAA); - var newContrastRatioAA = axs.utils.calculateContrastRatio(fgColor, newBgColorAA); - var suggestedColorsAA = {}; - suggestedColorsAA['bg'] = axs.utils.colorToString(newBgColorAA); - suggestedColorsAA['fg'] = axs.utils.colorToString(fgColor); - suggestedColorsAA['contrast'] = newContrastRatioAA.toFixed(2); - colors['AA'] = suggestedColorsAA; - } - if (!('AAA' in colors) && axs.utils.isLowContrast(contrastRatio, style, true) && - desiredBgLuminanceAAA <= 1 && desiredBgLuminanceAAA >= 0) { - var newBgColorAAA = axs.utils.translateColor(bgYCC, desiredBgLuminanceAAA); - var newContrastRatioAAA = axs.utils.calculateContrastRatio(fgColor, newBgColorAAA); - var suggestedColorsAAA = {}; - suggestedColorsAAA['bg'] = axs.utils.colorToString(newBgColorAAA); - suggestedColorsAAA['fg'] = axs.utils.colorToString(fgColor); - suggestedColorsAAA['contrast'] = newContrastRatioAAA.toFixed(2); - colors['AAA'] = suggestedColorsAAA; - } - return colors; -}; - -/** - * Combine the two given color according to alpha blending. - * @param {axs.utils.Color} fgColor - * @param {axs.utils.Color} bgColor - * @return {axs.utils.Color} - */ -axs.utils.flattenColors = function(fgColor, bgColor) { - var alpha = fgColor.alpha; - var r = ((1 - alpha) * bgColor.red) + (alpha * fgColor.red); - var g = ((1 - alpha) * bgColor.green) + (alpha * fgColor.green); - var b = ((1 - alpha) * bgColor.blue) + (alpha * fgColor.blue); - var a = fgColor.alpha + (bgColor.alpha * (1 - fgColor.alpha)); - - return new axs.utils.Color(r, g, b, a); -}; - -/** - * Calculate the luminance of the given color using the WCAG algorithm. - * @param {axs.utils.Color} color - * @return {number} - */ -axs.utils.calculateLuminance = function(color) { -/* var rSRGB = color.red / 255; - var gSRGB = color.green / 255; - var bSRGB = color.blue / 255; - - var r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow(((rSRGB + 0.055)/1.055), 2.4); - var g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow(((gSRGB + 0.055)/1.055), 2.4); - var b = bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow(((bSRGB + 0.055)/1.055), 2.4); - - return 0.2126 * r + 0.7152 * g + 0.0722 * b; */ - var ycc = axs.utils.toYCC(color); - return ycc[0]; -}; - -/** - * Returns an RGB to YCC conversion matrix for the given kR, kB constants. - * @param {number} kR - * @param {number} kB - * @return {Array.>} - */ -axs.utils.RGBToYCCMatrix = function(kR, kB) { - return [ - [ - kR, - (1 - kR - kB), - kB - ], - [ - -kR/(2 - 2*kB), - (kR + kB - 1)/(2 - 2*kB), - (1 - kB)/(2 - 2*kB) - ], - [ - (1 - kR)/(2 - 2*kR), - (kR + kB - 1)/(2 - 2*kR), - -kB/(2 - 2*kR) - ] - ]; -}; - -/** - * Return the inverse of the given 3x3 matrix. - * @param {Array.>} matrix - * @return Array.> The inverse of the given matrix. - */ -axs.utils.invert3x3Matrix = function(matrix) { - var a = matrix[0][0]; - var b = matrix[0][1]; - var c = matrix[0][2]; - var d = matrix[1][0]; - var e = matrix[1][1]; - var f = matrix[1][2]; - var g = matrix[2][0]; - var h = matrix[2][1]; - var k = matrix[2][2]; - - var A = (e*k - f*h); - var B = (f*g - d*k); - var C = (d*h - e*g); - var D = (c*h - b*k); - var E = (a*k - c*g); - var F = (g*b - a*h); - var G = (b*f - c*e); - var H = (c*d - a*f); - var K = (a*e - b*d); - - var det = a * (e*k - f*h) - b * (k*d - f*g) + c * (d*h - e*g); - var z = 1/det; - - return axs.utils.scalarMultiplyMatrix([ - [ A, D, G ], - [ B, E, H ], - [ C, F, K ] - ], z); -}; - -axs.utils.scalarMultiplyMatrix = function(matrix, scalar) { - var result = []; - result[0] = []; - result[1] = []; - result[2] = []; - - for (var i = 0; i < 3; i++) { - for (var j = 0; j < 3; j++) { - result[i][j] = matrix[i][j] * scalar; - } - } - - return result; -}; - -axs.utils.kR = 0.2126; -axs.utils.kB = 0.0722; -axs.utils.YCC_MATRIX = axs.utils.RGBToYCCMatrix(axs.utils.kR, axs.utils.kB); -axs.utils.INVERTED_YCC_MATRIX = axs.utils.invert3x3Matrix(axs.utils.YCC_MATRIX); - -/** - * Multiply the given color vector by the given transformation matrix. - * @param {Array.>} matrix A 3x3 conversion matrix - * @param {Array.} vector A 3-element color vector - * @return {Array.} A 3-element color vector - */ -axs.utils.convertColor = function(matrix, vector) { - var a = matrix[0][0]; - var b = matrix[0][1]; - var c = matrix[0][2]; - var d = matrix[1][0]; - var e = matrix[1][1]; - var f = matrix[1][2]; - var g = matrix[2][0]; - var h = matrix[2][1]; - var k = matrix[2][2]; - - var x = vector[0]; - var y = vector[1]; - var z = vector[2]; - - return [ - a*x + b*y + c*z, - d*x + e*y + f*z, - g*x + h*y + k*z - ]; -}; - -axs.utils.multiplyMatrices = function(matrix1, matrix2) { - var result = []; - result[0] = []; - result[1] = []; - result[2] = []; - - for (var i = 0; i < 3; i++) { - for (var j = 0; j < 3; j++) { - result[i][j] = matrix1[i][0] * matrix2[0][j] + - matrix1[i][1] * matrix2[1][j] + - matrix1[i][2] * matrix2[2][j]; - } - } - return result; -}; - -/** - * Convert a given RGB color to YCC. - * @param {axs.utils.Color} color - */ -axs.utils.toYCC = function(color) { - var rSRGB = color.red / 255; - var gSRGB = color.green / 255; - var bSRGB = color.blue / 255; - - var r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow(((rSRGB + 0.055)/1.055), 2.4); - var g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow(((gSRGB + 0.055)/1.055), 2.4); - var b = bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow(((bSRGB + 0.055)/1.055), 2.4); - - return axs.utils.convertColor(axs.utils.YCC_MATRIX, [r, g, b]); -}; - -/** - * Convert a color from a YCC color (as a vector) to an RGB color - * @param {Array.} yccColor - * @return {axs.utils.Color} - */ -axs.utils.fromYCC = function(yccColor) { - var rgb = axs.utils.convertColor(axs.utils.INVERTED_YCC_MATRIX, yccColor); - - var r = rgb[0]; - var g = rgb[1]; - var b = rgb[2]; - var rSRGB = r <= 0.00303949 ? (r * 12.92) : (Math.pow(r, (1/2.4)) * 1.055) - 0.055; - var gSRGB = g <= 0.00303949 ? (g * 12.92) : (Math.pow(g, (1/2.4)) * 1.055) - 0.055; - var bSRGB = b <= 0.00303949 ? (b * 12.92) : (Math.pow(b, (1/2.4)) * 1.055) - 0.055; - - var red = Math.min(Math.max(Math.round(rSRGB * 255), 0), 255); - var green = Math.min(Math.max(Math.round(gSRGB * 255), 0), 255); - var blue = Math.min(Math.max(Math.round(bSRGB * 255), 0), 255); - - return new axs.utils.Color(red, green, blue, 1); -}; - -axs.utils.scalarMultiplyMatrix = function(matrix, scalar) { - var result = []; - result[0] = []; - result[1] = []; - result[2] = []; - - for (var i = 0; i < 3; i++) { - for (var j = 0; j < 3; j++) { - result[i][j] = matrix[i][j] * scalar; - } - } - - return result; -}; - -axs.utils.multiplyMatrices = function(matrix1, matrix2) { - var result = []; - result[0] = []; - result[1] = []; - result[2] = []; - - for (var i = 0; i < 3; i++) { - for (var j = 0; j < 3; j++) { - result[i][j] = matrix1[i][0] * matrix2[0][j] + - matrix1[i][1] * matrix2[1][j] + - matrix1[i][2] * matrix2[2][j]; - } - } - return result; -}; - /** * @param {Element} element * @return {?number} @@ -897,7 +472,7 @@ axs.utils.getContrastRatioForElementWithComputedStyle = function(style, element) if (!fgColor) return null; - return axs.utils.calculateContrastRatio(fgColor, bgColor); + return axs.color.calculateContrastRatio(fgColor, bgColor); }; /** diff --git a/src/js/AuditRule.js b/src/js/AuditRule.js index a6ef9594..275444cd 100644 --- a/src/js/AuditRule.js +++ b/src/js/AuditRule.js @@ -182,6 +182,14 @@ axs.AuditRule.collectMatchingElements = function(node, matcher, collection, } } } + + //If it is a iframe, get the contentDocument + if (element && element.localName == 'iframe' && element.contentDocument) { + axs.AuditRule.collectMatchingElements(element.contentDocument, + matcher, + collection, + opt_shadowRoot); + } // If it is neither the parent of a ShadowRoot, a element, nor // a element recurse normally. var child = node.firstChild; diff --git a/src/js/AuditRules.js b/src/js/AuditRules.js index 4c8f904d..fe9d497c 100644 --- a/src/js/AuditRules.js +++ b/src/js/AuditRules.js @@ -20,6 +20,9 @@ goog.provide('axs.AuditRules'); var auditRulesByName = {}; var auditRulesByCode = {}; + /** @type {Object.} */ + axs.AuditRules.specs = {}; + /** * Instantiates and registers an audit rule. * If a conflicting rule is already registered then the new rule will not be added. @@ -27,7 +30,6 @@ goog.provide('axs.AuditRules'); * @throws {Error} If the rule duplicates properties that must be unique. */ axs.AuditRules.addRule = function(spec) { - // axs.AuditRule.specs[spec.name] = spec; // This would add backwards compatibility // create the auditRule before checking props as we can expect the constructor to perform the // first layer of sanity checking. var auditRule = new axs.AuditRule(spec); @@ -36,6 +38,7 @@ goog.provide('axs.AuditRules'); if (auditRule.name in auditRulesByName) throw new Error('Can not add audit rule with same name: "' + auditRule.name + '"'); auditRulesByName[auditRule.name] = auditRulesByCode[auditRule.code] = auditRule; + axs.AuditRules.specs[spec.name] = spec; }; /** diff --git a/src/js/Color.js b/src/js/Color.js new file mode 100644 index 00000000..e7e77955 --- /dev/null +++ b/src/js/Color.js @@ -0,0 +1,537 @@ +// Copyright 2015 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +goog.provide('axs.color'); +goog.provide('axs.color.Color'); + +/** + * @constructor + * @param {number} red + * @param {number} green + * @param {number} blue + * @param {number} alpha + */ +axs.color.Color = function(red, green, blue, alpha) { + /** @type {number} */ + this.red = red; + + /** @type {number} */ + this.green = green; + + /** @type {number} */ + this.blue = blue; + + /** @type {number} */ + this.alpha = alpha; +}; + +/** + * @constructor + * See https://en.wikipedia.org/wiki/YCbCr for more information. + * @param {Array.} coords The YCbCr values as a 3 element array, in the order [luma, Cb, Cr]. + * All numbers are in the range [0, 1]. + */ +axs.color.YCbCr = function(coords) { + /** @type {number} */ + this.luma = this.z = coords[0]; + + /** @type {number} */ + this.Cb = this.x = coords[1]; + + /** @type {number} */ + this.Cr = this.y = coords[2]; +}; + +axs.color.YCbCr.prototype = { + /** + * @param {number} scalar + * @return {axs.color.YCbCr} This color multiplied by the given scalar + */ + multiply: function(scalar) { + var result = [ this.luma * scalar, this.Cb * scalar, this.Cr * scalar ]; + return new axs.color.YCbCr(result); + }, + + /** + * @param {axs.color.YCbCr} other + * @return {axs.color.YCbCr} This plus other + */ + add: function(other) { + var result = [ this.luma + other.luma, this.Cb + other.Cb, this.Cr + other.Cr ]; + return new axs.color.YCbCr(result); + }, + + /** + * @param {axs.color.YCbCr} other + * @return {axs.color.YCbCr} This minus other + */ + subtract: function(other) { + var result = [ this.luma - other.luma, this.Cb - other.Cb, this.Cr - other.Cr ]; + return new axs.color.YCbCr(result); + } + +}; + + +/** + * Calculate the contrast ratio between the two given colors. Returns the ratio + * to 1, for example for two two colors with a contrast ratio of 21:1, this + * function will return 21. + * @param {axs.color.Color} fgColor + * @param {axs.color.Color} bgColor + * @return {!number} + */ +axs.color.calculateContrastRatio = function(fgColor, bgColor) { + if (fgColor.alpha < 1) + fgColor = axs.color.flattenColors(fgColor, bgColor); + + var fgLuminance = axs.color.calculateLuminance(fgColor); + var bgLuminance = axs.color.calculateLuminance(bgColor); + var contrastRatio = (Math.max(fgLuminance, bgLuminance) + 0.05) / + (Math.min(fgLuminance, bgLuminance) + 0.05); + return contrastRatio; +}; + +/** + * Calculate the luminance of the given color using the WCAG algorithm. + * @param {axs.color.Color} color + * @return {number} + */ +axs.color.calculateLuminance = function(color) { +/* var rSRGB = color.red / 255; + var gSRGB = color.green / 255; + var bSRGB = color.blue / 255; + + var r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow(((rSRGB + 0.055)/1.055), 2.4); + var g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow(((gSRGB + 0.055)/1.055), 2.4); + var b = bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow(((bSRGB + 0.055)/1.055), 2.4); + + return 0.2126 * r + 0.7152 * g + 0.0722 * b; */ + var ycc = axs.color.toYCbCr(color); + return ycc.luma; +}; + +/** + * Compute the luminance ratio between two luminance values. + * @param {number} luminance1 + * @param {number} luminance2 + */ +axs.color.luminanceRatio = function(luminance1, luminance2) { + return (Math.max(luminance1, luminance2) + 0.05) / + (Math.min(luminance1, luminance2) + 0.05); +}; + +/** + * @param {string} colorString The color string from CSS. + * @return {?axs.color.Color} + */ +axs.color.parseColor = function(colorString) { + var rgbRegex = /^rgb\((\d+), (\d+), (\d+)\)$/; + var match = colorString.match(rgbRegex); + + if (match) { + var r = parseInt(match[1], 10); + var g = parseInt(match[2], 10); + var b = parseInt(match[3], 10); + var a = 1; + return new axs.color.Color(r, g, b, a); + } + + var rgbaRegex = /^rgba\((\d+), (\d+), (\d+), (\d*(\.\d+)?)\)/; + match = colorString.match(rgbaRegex); + if (match) { + var r = parseInt(match[1], 10); + var g = parseInt(match[2], 10); + var b = parseInt(match[3], 10); + var a = parseFloat(match[4]); + return new axs.color.Color(r, g, b, a); + } + + return null; +}; + +/** + * @param {number} value The value of a color channel, 0 <= value <= 0xFF + * @return {!string} + */ +axs.color.colorChannelToString = function(value) { + value = Math.round(value); + if (value <= 0xF) + return '0' + value.toString(16); + return value.toString(16); +}; + +/** + * @param {axs.color.Color} color + * @return {!string} + */ +axs.color.colorToString = function(color) { + if (color.alpha == 1) { + return '#' + axs.color.colorChannelToString(color.red) + + axs.color.colorChannelToString(color.green) + axs.color.colorChannelToString(color.blue); + } + else + return 'rgba(' + [color.red, color.green, color.blue, color.alpha].join(',') + ')'; +}; + +/** + * Compute a desired luminance given a given luminance and a desired contrast ratio. + * @param {number} luminance The given luminance. + * @param {number} contrast The desired contrast ratio. + * @param {boolean} higher Whether the desired luminance is higher or lower than the given luminance. + * @return {number} The desired luminance. + */ +axs.color.luminanceFromContrastRatio = function(luminance, contrast, higher) { + if (higher) { + var newLuminance = (luminance + 0.05) * contrast - 0.05; + return newLuminance; + } else { + var newLuminance = (luminance + 0.05) / contrast - 0.05; + return newLuminance; + } +}; + +/** + * Given a color in YCbCr format and a desired luminance, pick a new color with the desired luminance which is + * as close as possible to the original color. + * @param {axs.color.YCbCr} ycc The original color in YCbCr form. + * @param {number} luma The desired luminance + * @return {!axs.color.Color} A new color in RGB. + */ +axs.color.translateColor = function(ycc, luma) { + var endpoint = (luma > ycc.luma) ? axs.color.WHITE_YCC : axs.color.BLACK_YCC; + var cubeFaces = (endpoint == axs.color.WHITE_YCC) ? axs.color.YCC_CUBE_FACES_WHITE + : axs.color.YCC_CUBE_FACES_BLACK; + + var a = new axs.color.YCbCr([0, ycc.Cb, ycc.Cr]); + var b = new axs.color.YCbCr([1, ycc.Cb, ycc.Cr]); + var line = { a: a, b: b }; + + var intersection = null; + for (var i = 0; i < cubeFaces.length; i++) { + var cubeFace = cubeFaces[i]; + intersection = axs.color.findIntersection(line, cubeFace); + // If intersection within [0, 1] in Z axis, it is within the cube. + if (intersection.z >= 0 && intersection.z <= 1) + break; + } + if (!intersection) { + // Should never happen + throw "Couldn't find intersection with YCbCr color cube for Cb=" + ycc.Cb + ", Cr=" + ycc.Cr + "."; + } + if (intersection.x != ycc.x || intersection.y != ycc.y) { + // Should never happen + throw "Intersection has wrong Cb/Cr values."; + } + + // If intersection.luma is closer to endpoint than desired luma, then luma is inside cube + // and we can immediately return new value. + if (Math.abs(endpoint.luma - intersection.luma) < Math.abs(endpoint.luma - luma)) { + var translatedColor = [luma, ycc.Cb, ycc.Cr]; + return axs.color.fromYCbCrArray(translatedColor); + } + + // Otherwise, translate from intersection towards white/black such that luma is correct. + var dLuma = luma - intersection.luma; + var scale = dLuma / (endpoint.luma - intersection.luma); + var translatedColor = [ luma, + intersection.Cb - (intersection.Cb * scale), + intersection.Cr - (intersection.Cr * scale) ]; + + return axs.color.fromYCbCrArray(translatedColor); +}; + +/** @typedef {{fg: string, bg: string, contrast: string}} */ +axs.color.SuggestedColors; + +/** + * @param {axs.color.Color} bgColor + * @param {axs.color.Color} fgColor + * @param {Object.} desiredContrastRatios A map of label to desired contrast ratio. + * @return {Object.} + */ +axs.color.suggestColors = function(bgColor, fgColor, desiredContrastRatios) { + var colors = {}; + var bgLuminance = axs.color.calculateLuminance(bgColor); + var fgLuminance = axs.color.calculateLuminance(fgColor); + + var fgLuminanceIsHigher = fgLuminance > bgLuminance; + var fgYCbCr = axs.color.toYCbCr(fgColor); + var bgYCbCr = axs.color.toYCbCr(bgColor); + for (var desiredLabel in desiredContrastRatios) { + var desiredContrast = desiredContrastRatios[desiredLabel]; + + var desiredFgLuminance = axs.color.luminanceFromContrastRatio(bgLuminance, desiredContrast + 0.02, fgLuminanceIsHigher); + if (desiredFgLuminance <= 1 && desiredFgLuminance >= 0) { + var newFgColor = axs.color.translateColor(fgYCbCr, desiredFgLuminance); + var newContrastRatio = axs.color.calculateContrastRatio(newFgColor, bgColor); + var suggestedColors = {}; + suggestedColors.fg = /** @type {!string} */ (axs.color.colorToString(newFgColor)); + suggestedColors.bg = /** @type {!string} */ (axs.color.colorToString(bgColor)); + suggestedColors.contrast = /** @type {!string} */ (newContrastRatio.toFixed(2)); + colors[desiredLabel] = /** @type {axs.color.SuggestedColors} */ (suggestedColors); + continue; + } + + var desiredBgLuminance = axs.color.luminanceFromContrastRatio(fgLuminance, desiredContrast + 0.02, !fgLuminanceIsHigher); + if (desiredBgLuminance <= 1 && desiredBgLuminance >= 0) { + var newBgColor = axs.color.translateColor(bgYCbCr, desiredBgLuminance); + var newContrastRatio = axs.color.calculateContrastRatio(fgColor, newBgColor); + var suggestedColors = {}; + suggestedColors.bg = /** @type {!string} */ (axs.color.colorToString(newBgColor)); + suggestedColors.fg = /** @type {!string} */ (axs.color.colorToString(fgColor)); + suggestedColors.contrast = /** @type {!string} */ (newContrastRatio.toFixed(2)); + colors[desiredLabel] = /** @type {axs.color.SuggestedColors} */ (suggestedColors); + } + } + return colors; +}; + +/** + * Combine the two given color according to alpha blending. + * @param {axs.color.Color} fgColor + * @param {axs.color.Color} bgColor + * @return {axs.color.Color} + */ +axs.color.flattenColors = function(fgColor, bgColor) { + var alpha = fgColor.alpha; + var r = ((1 - alpha) * bgColor.red) + (alpha * fgColor.red); + var g = ((1 - alpha) * bgColor.green) + (alpha * fgColor.green); + var b = ((1 - alpha) * bgColor.blue) + (alpha * fgColor.blue); + var a = fgColor.alpha + (bgColor.alpha * (1 - fgColor.alpha)); + + return new axs.color.Color(r, g, b, a); +}; + +/** + * Multiply the given vector by the given matrix. + * @param {Array.>} matrix A 3x3 matrix + * @param {Array.} vector A 3-element vector + * @return {Array.} A 3-element vector + */ +axs.color.multiplyMatrixVector = function(matrix, vector) { + var a = matrix[0][0]; + var b = matrix[0][1]; + var c = matrix[0][2]; + var d = matrix[1][0]; + var e = matrix[1][1]; + var f = matrix[1][2]; + var g = matrix[2][0]; + var h = matrix[2][1]; + var k = matrix[2][2]; + + var x = vector[0]; + var y = vector[1]; + var z = vector[2]; + + return [ + a*x + b*y + c*z, + d*x + e*y + f*z, + g*x + h*y + k*z + ]; +}; + +/** + * Convert a given RGB color to YCbCr. + * @param {axs.color.Color} color + * @return {axs.color.YCbCr} + */ +axs.color.toYCbCr = function(color) { + var rSRGB = color.red / 255; + var gSRGB = color.green / 255; + var bSRGB = color.blue / 255; + + var r = rSRGB <= 0.03928 ? rSRGB / 12.92 : Math.pow(((rSRGB + 0.055)/1.055), 2.4); + var g = gSRGB <= 0.03928 ? gSRGB / 12.92 : Math.pow(((gSRGB + 0.055)/1.055), 2.4); + var b = bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow(((bSRGB + 0.055)/1.055), 2.4); + + return new axs.color.YCbCr(axs.color.multiplyMatrixVector(axs.color.YCC_MATRIX, [r, g, b])); +}; + +/** + * @param {axs.color.YCbCr} ycc + * @return {!axs.color.Color} + */ +axs.color.fromYCbCr = function(ycc) { + return axs.color.fromYCbCrArray([ycc.luma, ycc.Cb, ycc.Cr]); +}; + +/** + * Convert a color from a YCbCr color (as a vector) to an RGB color + * @param {Array.} yccArray + * @return {!axs.color.Color} + */ +axs.color.fromYCbCrArray = function(yccArray) { + var rgb = axs.color.multiplyMatrixVector(axs.color.INVERTED_YCC_MATRIX, yccArray); + + var r = rgb[0]; + var g = rgb[1]; + var b = rgb[2]; + var rSRGB = r <= 0.00303949 ? (r * 12.92) : (Math.pow(r, (1/2.4)) * 1.055) - 0.055; + var gSRGB = g <= 0.00303949 ? (g * 12.92) : (Math.pow(g, (1/2.4)) * 1.055) - 0.055; + var bSRGB = b <= 0.00303949 ? (b * 12.92) : (Math.pow(b, (1/2.4)) * 1.055) - 0.055; + + var red = Math.min(Math.max(Math.round(rSRGB * 255), 0), 255); + var green = Math.min(Math.max(Math.round(gSRGB * 255), 0), 255); + var blue = Math.min(Math.max(Math.round(bSRGB * 255), 0), 255); + + return new axs.color.Color(red, green, blue, 1); +}; + +/** + * Returns an RGB to YCbCr conversion matrix for the given kR, kB constants. + * @param {number} kR + * @param {number} kB + * @return {Array.>} + */ +axs.color.RGBToYCbCrMatrix = function(kR, kB) { + return [ + [ + kR, + (1 - kR - kB), + kB + ], + [ + -kR/(2 - 2*kB), + (kR + kB - 1)/(2 - 2*kB), + (1 - kB)/(2 - 2*kB) + ], + [ + (1 - kR)/(2 - 2*kR), + (kR + kB - 1)/(2 - 2*kR), + -kB/(2 - 2*kR) + ] + ]; +}; + +/** + * Return the inverse of the given 3x3 matrix. + * @param {Array.>} matrix + * @return Array.> The inverse of the given matrix. + */ +axs.color.invert3x3Matrix = function(matrix) { + var a = matrix[0][0]; + var b = matrix[0][1]; + var c = matrix[0][2]; + var d = matrix[1][0]; + var e = matrix[1][1]; + var f = matrix[1][2]; + var g = matrix[2][0]; + var h = matrix[2][1]; + var k = matrix[2][2]; + + var A = (e*k - f*h); + var B = (f*g - d*k); + var C = (d*h - e*g); + var D = (c*h - b*k); + var E = (a*k - c*g); + var F = (g*b - a*h); + var G = (b*f - c*e); + var H = (c*d - a*f); + var K = (a*e - b*d); + + var det = a * (e*k - f*h) - b * (k*d - f*g) + c * (d*h - e*g); + var z = 1/det; + + return axs.color.scalarMultiplyMatrix([ + [ A, D, G ], + [ B, E, H ], + [ C, F, K ] + ], z); +}; + +/** @typedef {{ a: axs.color.YCbCr, b: axs.color.YCbCr }} */ +axs.color.Line; + +/** @typedef {{ p0: axs.color.YCbCr, p1: axs.color.YCbCr, p2: axs.color.YCbCr }} */ +axs.color.Plane; + +/** + * Find the intersection between a line and a plane using + * http://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection#Parametric_form + * @param {axs.color.Line} l + * @param {axs.color.Plane} p + * @return {axs.color.YCbCr} + */ +axs.color.findIntersection = function(l, p) { + var lhs = [ l.a.x - p.p0.x, l.a.y - p.p0.y, l.a.z - p.p0.z ]; + + var matrix = [ [ l.a.x - l.b.x, p.p1.x - p.p0.x, p.p2.x - p.p0.x ], + [ l.a.y - l.b.y, p.p1.y - p.p0.y, p.p2.y - p.p0.y ], + [ l.a.z - l.b.z, p.p1.z - p.p0.z, p.p2.z - p.p0.z ] ]; + var invertedMatrix = axs.color.invert3x3Matrix(matrix); + + var tuv = axs.color.multiplyMatrixVector(invertedMatrix, lhs); + var t = tuv[0]; + + var result = l.a.add(l.b.subtract(l.a).multiply(t)); + return result; +}; + +/** + * Multiply a matrix by a scalar. + * @param {Array.>} matrix A 3x3 matrix. + * @param {number} scalar + * @return {Array.>} + */ +axs.color.scalarMultiplyMatrix = function(matrix, scalar) { + var result = []; + + for (var i = 0; i < 3; i++) + result[i] = axs.color.scalarMultiplyVector(matrix[i], scalar); + + return result; +}; + +/** + * Multiply a vector by a scalar. + * @param {Array.} vector + * @param {number} scalar + * @return {Array.} vector + */ +axs.color.scalarMultiplyVector = function(vector, scalar) { + var result = [] + for (var i = 0; i < vector.length; i++) + result[i] = vector[i] * scalar; + return result; +}; + +axs.color.kR = 0.2126; +axs.color.kB = 0.0722; +axs.color.YCC_MATRIX = axs.color.RGBToYCbCrMatrix(axs.color.kR, axs.color.kB); +axs.color.INVERTED_YCC_MATRIX = axs.color.invert3x3Matrix(axs.color.YCC_MATRIX); + +axs.color.BLACK = new axs.color.Color(0, 0, 0, 1.0); +axs.color.BLACK_YCC = axs.color.toYCbCr(axs.color.BLACK); +axs.color.WHITE = new axs.color.Color(255, 255, 255, 1.0); +axs.color.WHITE_YCC = axs.color.toYCbCr(axs.color.WHITE); +axs.color.RED = new axs.color.Color(255, 0, 0, 1.0); +axs.color.RED_YCC = axs.color.toYCbCr(axs.color.RED); +axs.color.GREEN = new axs.color.Color(0, 255, 0, 1.0); +axs.color.GREEN_YCC = axs.color.toYCbCr(axs.color.GREEN); +axs.color.BLUE = new axs.color.Color(0, 0, 255, 1.0); +axs.color.BLUE_YCC = axs.color.toYCbCr(axs.color.BLUE); +axs.color.CYAN = new axs.color.Color(0, 255, 255, 1.0); +axs.color.CYAN_YCC = axs.color.toYCbCr(axs.color.CYAN); +axs.color.MAGENTA = new axs.color.Color(255, 0, 255, 1.0); +axs.color.MAGENTA_YCC = axs.color.toYCbCr(axs.color.MAGENTA); +axs.color.YELLOW = new axs.color.Color(255, 255, 0, 1.0); +axs.color.YELLOW_YCC = axs.color.toYCbCr(axs.color.YELLOW); + +axs.color.YCC_CUBE_FACES_BLACK = [ { p0: axs.color.BLACK_YCC, p1: axs.color.RED_YCC, p2: axs.color.GREEN_YCC }, + { p0: axs.color.BLACK_YCC, p1: axs.color.GREEN_YCC, p2: axs.color.BLUE_YCC }, + { p0: axs.color.BLACK_YCC, p1: axs.color.BLUE_YCC, p2: axs.color.RED_YCC } ]; +axs.color.YCC_CUBE_FACES_WHITE = [ { p0: axs.color.WHITE_YCC, p1: axs.color.CYAN_YCC, p2: axs.color.MAGENTA_YCC }, + { p0: axs.color.WHITE_YCC, p1: axs.color.MAGENTA_YCC, p2: axs.color.YELLOW_YCC }, + { p0: axs.color.WHITE_YCC, p1: axs.color.YELLOW_YCC, p2: axs.color.CYAN_YCC } ]; diff --git a/src/js/Properties.js b/src/js/Properties.js index a6a40ed9..bc8e3901 100644 --- a/src/js/Properties.js +++ b/src/js/Properties.js @@ -13,6 +13,7 @@ // limitations under the License. goog.require('axs.browserUtils'); +goog.require('axs.color'); goog.require('axs.utils'); goog.provide('axs.properties'); @@ -198,16 +199,28 @@ axs.properties.getContrastRatioProperties = function(element) { if (!bgColor) return null; - contrastRatioProperties['backgroundColor'] = axs.utils.colorToString(bgColor); + contrastRatioProperties['backgroundColor'] = axs.color.colorToString(bgColor); var fgColor = axs.utils.getFgColor(style, element, bgColor); - contrastRatioProperties['foregroundColor'] = axs.utils.colorToString(fgColor); - var value = axs.utils.getContrastRatioForElementWithComputedStyle(style, element); - if (!value) + contrastRatioProperties['foregroundColor'] = axs.color.colorToString(fgColor); + var contrast = axs.utils.getContrastRatioForElementWithComputedStyle(style, element); + if (!contrast) return null; - contrastRatioProperties['value'] = value.toFixed(2); - if (axs.utils.isLowContrast(value, style)) + contrastRatioProperties['value'] = contrast.toFixed(2); + if (axs.utils.isLowContrast(contrast, style)) contrastRatioProperties['alert'] = true; - var suggestedColors = axs.utils.suggestColors(bgColor, fgColor, value, style); + + var levelAAContrast = axs.utils.isLargeFont(style) ? 3.0 : 4.5; + var levelAAAContrast = axs.utils.isLargeFont(style) ? 4.5 : 7.0; + var desiredContrastRatios = {}; + if (levelAAContrast > contrast) + desiredContrastRatios['AA'] = levelAAContrast; + if (levelAAAContrast > contrast) + desiredContrastRatios['AAA'] = levelAAAContrast; + + if (!Object.keys(desiredContrastRatios).length) + return contrastRatioProperties; + + var suggestedColors = axs.color.suggestColors(bgColor, fgColor, desiredContrastRatios); if (suggestedColors && Object.keys(suggestedColors).length) contrastRatioProperties['suggestedColors'] = suggestedColors; return contrastRatioProperties; diff --git a/test/index.html b/test/index.html index f08f42d6..b16fc580 100644 --- a/test/index.html +++ b/test/index.html @@ -9,6 +9,7 @@ + @@ -46,6 +47,7 @@ + diff --git a/test/js/audit-rule-test.js b/test/js/audit-rule-test.js index 57562c91..1770594e 100644 --- a/test/js/audit-rule-test.js +++ b/test/js/audit-rule-test.js @@ -41,6 +41,16 @@ equal(matched.length, DIV_COUNT); }); + test("Iframe with simple DOM", function () { + var ifrm = document.createElement("IFRAME"); + var container = document.getElementById('qunit-fixture'); + container.appendChild(ifrm); + ifrm.contentDocument.body.appendChild(buildTestDom()); + var matched = []; + axs.AuditRule.collectMatchingElements(container, matcher, matched); + equal(matched.length, DIV_COUNT); + }); + test("With shadow DOM with no content insertion point", function () { var container = document.getElementById('qunit-fixture'); container.appendChild(buildTestDom()); diff --git a/test/js/color-test.js b/test/js/color-test.js new file mode 100644 index 00000000..72f897df --- /dev/null +++ b/test/js/color-test.js @@ -0,0 +1,53 @@ +// Copyright 2012 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +module("Contrast Ratio", { + setup: function () { + var fixture = document.createElement('div'); + document.getElementById('qunit-fixture').appendChild(fixture); + this.fixture_ = fixture; + this.black_ = {"red": 0, "green": 0, "blue": 0, "alpha": 1}; + this.white_ = {"red": 255, "green": 255, "blue": 255, "alpha": 1}; + } +}); +test("Black and white.", function () { + equal(axs.color.calculateContrastRatio(this.white_, this.black_), 21); + equal(axs.color.calculateContrastRatio(this.black_, this.white_), 21); +}); +test("Same color === no contrast.", function () { + equal(axs.color.calculateContrastRatio(this.white_, this.white_), 1); + equal(axs.color.calculateContrastRatio(this.black_, this.black_), 1); +}); +test("Transparent foreground === no contrast.", function () { + equal(axs.color.calculateContrastRatio({"red": 0, "green": 0, "blue": 0, "alpha": 0}, this.white_), 1); +}); + +module("parseColor"); +test("parses alpha values correctly", function() { + var colorString = 'rgba(255, 255, 255, .47)'; + var color = axs.color.parseColor(colorString); + equal(color.red, 255); + equal(color.blue, 255); + equal(color.green, 255); + equal(color.alpha, .47); +}); + +module("suggestColors"); +test("suggests correct grey values", function() { + var white = new axs.color.Color(255, 255, 255, 1) + var desiredContrastRatios = { AA: 4.5, AAA: 7.0 }; + var suggestions = axs.color.suggestColors(white, white, desiredContrastRatios); + deepEqual(suggestions, { AA: { bg: "#ffffff", contrast: "4.54", fg: "#767676" }, + AAA: { bg: "#ffffff", contrast: "7.00", fg: "#595959" } }); +}); diff --git a/test/js/utils-test.js b/test/js/utils-test.js index 93c0a185..574dbff8 100644 --- a/test/js/utils-test.js +++ b/test/js/utils-test.js @@ -12,27 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -module("Contrast Ratio", { - setup: function () { - var fixture = document.createElement('div'); - document.getElementById('qunit-fixture').appendChild(fixture); - this.fixture_ = fixture; - this.black_ = {"red": 0, "green": 0, "blue": 0, "alpha": 1}; - this.white_ = {"red": 255, "green": 255, "blue": 255, "alpha": 1}; - } -}); -test("Black and white.", function () { - equal(axs.utils.calculateContrastRatio(this.white_, this.black_), 21); - equal(axs.utils.calculateContrastRatio(this.black_, this.white_), 21); -}); -test("Same color === no contrast.", function () { - equal(axs.utils.calculateContrastRatio(this.white_, this.white_), 1); - equal(axs.utils.calculateContrastRatio(this.black_, this.black_), 1); -}); -test("Transparent foreground === no contrast.", function () { - equal(axs.utils.calculateContrastRatio({"red": 0, "green": 0, "blue": 0, "alpha": 0}, this.white_), 1); -}); - module("Zero Area", { setup: function () { var fixture = document.createElement('div'); @@ -143,16 +122,6 @@ test("nth-of-type does not refer to a selector but a tagName", function() { 'selector "' + selector + '" does not match element'); }); -module("parseColor"); -test("parses alpha values correctly", function() { - var colorString = 'rgba(255, 255, 255, .47)'; - var color = axs.utils.parseColor(colorString); - equal(color.red, 255); - equal(color.blue, 255); - equal(color.green, 255); - equal(color.alpha, .47); -}); - module("getIdReferrers", { setup: function () { this.fixture_ = document.getElementById('qunit-fixture'); diff --git a/tools/runner/audit.js b/tools/runner/audit.js index 9e01b439..0bfabe0d 100644 --- a/tools/runner/audit.js +++ b/tools/runner/audit.js @@ -16,6 +16,9 @@ var page = require('webpage').create(), system = require('system'), url; +// disabling so we can get the document root from iframes (http -> https) +page.settings.webSecurityEnabled = false; + if (system.args.length !== 2) { console.log('Usage: phantomjs audit.js URL'); phantom.exit();