1 /* 2 * Module : injected/prelude.js 3 * Copyright : (c) 2011-2012, Galois, Inc. 4 * 5 * Maintainer : 6 * Stability : Provisional 7 * Portability: Portable 8 * 9 * Licensed under the Apache License, Version 2.0 (the "License"); 10 * you may not use this file except in compliance with the License. 11 * You may obtain a copy of the License at 12 * 13 * http://www.apache.org/licenses/LICENSE-2.0 14 * 15 * Unless required by applicable law or agreed to in writing, software 16 * distributed under the License is distributed on an "AS IS" BASIS, 17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 * See the License for the specific language governing permissions and 19 * limitations under the License. 20 */ 21 22 /*global $5: true, JSON: true */ 23 24 /** 25 * <p>The FiveUI Prelude.</p> 26 * 27 * @description 28 * <p>The prelude provides a collection of utilities to ease the 29 * conversion from human-readable guideline documents (such as the Web 30 * Accessibilty Guidelines, or Apple Human Interface Guidelines) to 31 * executable Rules.</p> 32 * 33 * @namespace 34 */ 35 var fiveui = fiveui || {}; 36 37 /** 38 * A global namespace for statistics collection. 39 * 40 * @namespace 41 */ 42 fiveui.stats = fiveui.stats || {}; 43 /** @global */ 44 fiveui.stats.numElts = 0; 45 /** @const */ 46 fiveui.stats.zero = { numRules: 0, start: 0, end: 0, numElts: 0 }; 47 48 /** DOM Traversal ************************************************************/ 49 50 /** 51 * <p>fiveui.query is a wrapper around the jQuery $() function.</p> 52 * 53 * @description 54 * <p>In most respects fiveui.query behaves just like jQuery. But it 55 * recurses into iframes and frames, whereas $(...) stops 56 * when it encounters internal frames. fiveui.query also skips over 57 * page elements that are added by FiveUI.</p> 58 * 59 * <p>Generally, rules written for FiveUI will want to cover the entire 60 * visible page, and as such, should use fiveui.query; however, $(...) 61 * is available if recursing into frames is not necessary.</p> 62 * 63 * <p>$5() is an alias for fiveui.query.</p> 64 * 65 * @alias $5 66 * 67 * @param {string} sel The jQuery selector string. 68 * @param {Object} [context] The context to run the query within. This is often a DOM object/tree. 69 * @returns {Object} A jQuery object, suitable for chaining. 70 */ 71 fiveui.query = function (sel, context) { 72 var ctx = context || document; 73 var $results = jQuery(sel, ctx); 74 75 jQuery('iframe, frame', ctx) 76 .filter(function(idx, frame) { return sameOrigin(frame); }) 77 .each( 78 function(idx, elt) { 79 var $tempResults; 80 if (elt.contentDocument) { 81 try { 82 $tempResults = fiveui.query(sel, elt.contentDocument); 83 } catch (e) { 84 console.log("encoutered a non-cooperative iframe/frame at " + $(elt).attr("src")); 85 console.log(e.toString()); 86 $tempResults = []; 87 } 88 89 $results = $results.add($tempResults); 90 } 91 } 92 ); 93 94 $filteredResults = $results.not('.fiveui') 95 .not('.fiveui *') 96 .filter(':visible'); 97 98 // update global stats 99 fiveui.stats.numElts += $filteredResults.length; 100 101 return $filteredResults; 102 103 // Frames are considered to be from the same origin if their location 104 // hosts, ports, and schemes are the same. 105 function sameOrigin(frame) { 106 var src = frame.src; 107 var origin = window.location.origin; 108 return src.indexOf(origin) === 0 && src.charAt(origin.length) !== ':'; 109 } 110 }; 111 112 /** 113 * <p>Provide a short alias for fiveui.query along the lines of the 114 * jQuery $ alias.</p> 115 * 116 * @example $5("p").hasText("foo") -> jQuery object containing paragraph elements 117 * containing the text "foo" 118 * 119 * @function 120 * @const 121 * 122 */ 123 var $5 = fiveui.query; 124 125 /** Utilities ****************************************************************/ 126 127 /** 128 * <p>Determine if a given value is a string or not.</p> 129 * 130 * @param {*} [o] A value of some type that may or may not be defined. 131 * @returns {!boolean} true if the object is a defined, non-null string, false 132 * otherwise. 133 */ 134 fiveui.isString = function(o) { 135 return typeof o == 'string'; 136 }; 137 138 139 /** 140 * <p>String-specific utilities.</p> 141 * 142 * @namespace 143 */ 144 fiveui.string = {}; 145 146 /** 147 * <p>Non-destructively removes whitespace from the start and end of a 148 * string.</p> 149 * 150 * @param {string} [s] The string to trim of whitespace. 151 * @returns {string} The input string, without leading or trailing 152 * whitespace. Returns null if you gave it null. 153 */ 154 fiveui.string.trim = function(s) { 155 if (s) { 156 return s.replace(/^\s+|\s+$/g,""); 157 } 158 return s; 159 }; 160 161 /** 162 * <p>Tokenize a string on whitespace.</p> 163 * 164 * @example 165 * var tokens = fiveui.string.tokens('Under the Olive Tree'); 166 * tokens //> [ 'Under', 'the', 'Olive', 'Tree' ] 167 * 168 * @param {!string} s The string to tokenize. 169 * @returns {string[]>} An array of substrings. 170 */ 171 fiveui.string.tokens = function (s) { 172 var posLength = function(ar) { 173 return 1 <= ar.length; 174 }; 175 176 return s.split(/\s/).filter(posLength); 177 }; 178 179 180 /** 181 * <p>A simple heuristic check to see if a string is in Title Case.</p> 182 * 183 * @description 184 * <p>This does not perform an exhaustive grammatical analysis, and as 185 * such, it is prone to generating false-positives in some cases. In 186 * particular, it only has a short 'white list' of articles, 187 * conjections, and prepositions that are allowed to be in lower 188 * case.</p> 189 * 190 * @example 191 * fiveui.string.isTitleCase('Under the Olive Tree') === true 192 * 193 * @param {!string} str The string to check. 194 * @returns {!boolean} true if the string is in title case, false if 195 * it is not. 196 */ 197 fiveui.string.isTitleCase = function(str) { 198 var exception = function(str) { 199 var exceptions = [ 'a', 'an', 'the' // articles 200 , 'and', 'but', 'for', 'not', 'or' // conjuctions 201 , 'in', 'on' // short prepositions 202 , 'to' ]; 203 return exceptions.indexOf(str.toLowerCase()) != -1; 204 }; 205 206 if ( !fiveui.word.capitalized(str[0]) ) { 207 return false; 208 } 209 210 var tokens = fiveui.string.tokens(str); 211 for (var i=1; i < tokens.length; ++i) { 212 if (!exception(tokens[i]) && !fiveui.word.capitalized(tokens[i])) { 213 return false; 214 } 215 } 216 return true; 217 }; 218 219 /** 220 * <p>Utilities for word-specific processing.</p> 221 * 222 * @description 223 * <p>The fiveui.word namespace focuses on tools for working directly 224 * with words in the sense of natural languages, rather than general 225 * strings (as is the case for the fiveui.string namespace).</p> 226 * 227 * @namespace 228 */ 229 fiveui.word = {}; 230 231 /** 232 * <p>Check to see if a sting begins with a capital letter.</p> 233 * 234 * @param {!string} word The string to check for capitalization. 235 * @returns {!boolean} 236 */ 237 fiveui.word.capitalized = function(word) { 238 return fiveui.isString(word) && word.search(/^[A-Z]/, word) >= 0; 239 }; 240 241 /** 242 * <p>Check to see if a sting consists entirely of capital letters.</p> 243 * 244 * @param {!string} word The string to check for capitalization. 245 * @returns {!boolean} 246 */ 247 fiveui.word.allCaps = function(word) { 248 return fiveui.isString(word) 249 && word.search(/^\w/, word) >= 0 250 && (word == word.toUpperCase()); 251 }; 252 253 254 /** 255 * <p>Utilities for dealing with color.</p> 256 * 257 * @namespace 258 */ 259 fiveui.color = {}; 260 261 /** 262 * <p>Color check compiler. It is recommended to use the jQuery plugin 263 * fiveui.jquery.cssIsNot instead.</p> 264 * 265 * @param {!string} selector The HTML element selector to check. 266 * @param {string[]} colorSet An array of strings containing the HEX values of 267 * colors in the desired color set. 268 * @returns {!function()} A function which checks the rule 269 * @see fiveui.jquery.cssIsNot 270 */ 271 fiveui.color.colorCheck = function (selector, colorSet) { 272 var allowable, i, fnStr, forEachFuncStr; 273 allowable = {}; 274 for (i = 0; i < colorSet.length; i += 1) { allowable[colorSet[i]] = true; } 275 276 return function colorCheck() { 277 fiveui.query(selector).each(function(j, elt) { 278 var $elt = $(elt); 279 var color = fiveui.color.colorToHex($elt.css("color")); 280 if (!(color in allowable)) { 281 report("Disallowed color " + color + " in element matching " + selector, $elt); 282 } 283 }); 284 }; 285 }; 286 287 /** 288 * @private 289 */ 290 componentToHex = function (c) { 291 var hex = c.toString(16).toUpperCase(); 292 return hex.length == 1 ? "0" + hex : hex; 293 }; 294 295 /** 296 * @description 297 * <p>Given a hexadecimal color in short form, returns the corresponding 298 * long form.</p> 299 * 300 * @example 301 * shortHexToHex('#fff') === '#ffffff' 302 * 303 * @private 304 */ 305 shortHexToHex = function (color) { 306 var have = color.length - 1; 307 var haveDigits = color.substr(1, color.length); 308 var need = 6 - have; 309 var reps = Math.ceil(need / have); 310 var i, stdColor; 311 for (i = 0, stdColor = color; i < reps; i += 1) { stdColor += haveDigits; } 312 return stdColor.substr(0, 7); 313 }; 314 315 /** 316 * @private 317 */ 318 equalRGBA = function (c1, c2) { 319 return (c1.r == c2.r && 320 c1.g == c2.g && 321 c1.b == c2.b && 322 c1.a == c2.a); 323 }; 324 325 /** 326 * <p>Convert RGB values to Hex.</p> 327 * 328 * @description 329 * <p>Accepts three arguments representing red, green, and blue color 330 * components. Returns a hexadecimal representation of the 331 * corresponding color.</p> 332 * 333 * @example 334 * fiveui.color.rgbToHex(255, 255, 255) === '#FFFFFF' 335 * 336 * @param {!number} red 337 * @param {!number} green 338 * @param {!number} blue 339 * @return {!string} 340 */ 341 fiveui.color.rgbToHex = function (r, g, b) { 342 return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b); 343 }; 344 345 /** 346 * <p>Convert a 3-byte hex value to base-10 RGB</p> 347 * 348 * @description 349 * <p>Given a hexadecimal color code as a string, returns an object with 350 * `r`, `g`, and `b` properties that give the red, green, and blue 351 * components of the color.</p> 352 * 353 * @example 354 * fiveui.color.hexToRGB('#ffffff') //> { r: 255, g: 255, b: 255 } 355 * fiveui.color.hexToRGB('ffffff') //> { r: 255, g: 255, b: 255 } 356 * 357 * @param {!string} color 358 * @return {color} 359 */ 360 fiveui.color.hexToRGB = function (hex) { 361 var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 362 return result ? { 363 r: parseInt(result[1], 16), 364 g: parseInt(result[2], 16), 365 b: parseInt(result[3], 16) 366 } : null; 367 }; 368 369 /** 370 * <p>Covert rgb colors to hex and abreviated hex colors to their full 3 byte 371 * and uppercase normal form.</p> 372 * 373 * @description 374 * <p>Throws an error if the given color cannot be parsed.</p> 375 * 376 * @example 377 * fiveui.color.colorToHex('#ffffff') === "#FFFFFF" 378 * fiveui.color.colorToHex('#fff') === "#FFFFFF" 379 * fiveui.color.colorToHex('rgb(255, 255, 255)') === "#FFFFFF" 380 * fiveui.color.colorToHex('rgba(255, 255, 255)') === "#FFFFFF" 381 * 382 * @param {!string} color The color string to convert. This should be either of the form rgb(...) or #... 383 * @returns {!string} The color string in #XXXXXX form 384 * @throws {ParseError} if the rgb color string cannot be parsed 385 */ 386 fiveui.color.colorToHex = function(color) { 387 if (color.substr(0, 1) === '#') { 388 if (color.length === 7) { 389 return color.toUpperCase(); 390 } 391 else { // deal with #0 or #F7 cases 392 return shortHexToHex(color).toUpperCase(); 393 } 394 } 395 else if (color.substr(0,3) === 'rgb') { 396 var c = fiveui.color.colorToRGB(color); 397 return fiveui.color.rgbToHex(c.r, c.g, c.b); 398 } 399 else { 400 throw new Error('could not convert color string "' + color + '"'); 401 } 402 }; 403 404 /** 405 * <p>Attemps to convert a given string to a hexadecimal color code.</p> 406 * 407 * @description 408 * <p>Behaves like fiveui.color.colorToHex - except that in case there 409 * are parse errors during the conversion, i.e. color values that are 410 * not understood, the input is returned unchanged.</p> 411 * 412 * @param {!string} color The color string to convert. This should be either of the form rgb(...) or #... 413 * @returns {!string} The color string in #XXXXXX form 414 */ 415 fiveui.color.colorToHexWithDefault = function (color) { 416 try { 417 return fiveui.color.colorToHex(color); 418 } 419 catch (e) { 420 console.log(e); 421 return color; 422 } 423 }; 424 425 /** 426 * <p>Covert color to RGB color object.</p> 427 * 428 * @example 429 * fiveui.color.colorToRGB('#ffffff') //> { r: 255, g: 255, b: 255 } 430 * fiveui.color.colorToRGB('#fff') //> { r: 255, g: 255, b: 255 } 431 * fiveui.color.colorToRGB('rgb(255, 255, 255)') //> { r: 255, g: 255, b: 255 } 432 * fiveui.color.colorToRGB('rgba(255, 255, 255)') //> { r: 255, g: 255, b: 255 } 433 * 434 * @param {!string} color The color string to convert. This should be either of the form rgb(...) or #... 435 * @returns {!Object} An RGB color object with attributes: r, g, b, a 436 * @throws {ParseError} if the rgb color string cannot be parsed 437 */ 438 fiveui.color.colorToRGB = function(color) { 439 440 if (color.substr(0, 1) === '#') { 441 return fiveui.color.hexToRGB(fiveui.color.colorToHex(color)); 442 } 443 444 var digits = /rgba?\((\d+), (\d+), (\d+)(, ([-+]?[0-9]*\.?[0-9]+))?/.exec(color); 445 if (!digits) { 446 throw new Error('could not parse color string: "' + color + '"'); 447 } 448 449 var alpha = 1; 450 if (digits[5]) { 451 alpha = parseFloat(digits[5]); 452 } 453 454 return { r: parseInt(digits[1]), 455 g: parseInt(digits[2]), 456 b: parseInt(digits[3]), 457 a: alpha }; 458 }; 459 460 461 /** 462 * <p>Calculate the relative {@link 463 * http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef 464 * luminance} of an sRGB color.</p> 465 * 466 * @description 467 * <p>This function does not account for alpha values that are not 1. 468 * That is, it assumes that the incomming color is fully opaque, or 469 * displayed on a white background.</p> 470 * 471 * @example 472 * fiveui.color.luminance({ r: 255, g: 255, b: 255 }) === 1 473 * 474 * @param {!Object} An RGB color object with attributes: r, g, b, a 475 * @returns {!doubl} A measure of the relative luminance according to 476 * the WCAG 2.0 specification. 477 * 478 */ 479 fiveui.color.luminance = function(color) { 480 var toSRGB = function(c) { 481 return c/255; 482 }; 483 484 var toLumComponent = function(c) { 485 if (c <= 0.03928) { 486 return c / 12.92; 487 } else { 488 return Math.pow((c + 0.055) / 1.055, 2.4); 489 } 490 }; 491 492 return 0.2126 * toLumComponent(toSRGB(color.r)) 493 + 0.7152 * toLumComponent(toSRGB(color.g)) 494 + 0.0722 * toLumComponent(toSRGB(color.b)); 495 }; 496 497 /** 498 * <p>Compute the contrast ratio, according to {@link 499 * http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef 500 * WCAG 2.0}</p> 501 * 502 * @example 503 * var color_a = fiveui.color.colorToRGB('#98BC4B'); 504 * var color_b = fiveui.color.colorToRGB('#88E2CA'); 505 * 506 * var contrast = fiveui.color.contrast( 507 * fiveui.color.luminance(color_a), 508 * fiveui.color.luminance(color_b) 509 * ); 510 * 511 * contrast === 1.4307674054056414 512 * 513 * @param {!double} lum1 The relative luminance of the first color. 514 * @param {!double} lum2 The relative luminance of the second color. 515 * @returns {!double} The contrast ratio. 516 */ 517 fiveui.color.contrast = function(lum1, lum2) { 518 if (lum1 > lum2) { 519 return (lum1 + 0.05) / (lum2 + 0.05); 520 } else { 521 return (lum2 + 0.05) / (lum1 + 0.05); 522 } 523 }; 524 525 /** 526 * <p>Computationally determine the actual displayed background color for 527 * an object. This accounts for parent colors that may appear when 528 * a bg color is unspecified, or fully transparent.</p> 529 * 530 * @description 531 * <p>It does not account for elements that are shifted out of their 532 * parent containers.</p> 533 * 534 * @example 535 * var sidebar = $('div.sidebar'); 536 * fiveui.color.findBGColor(sidebar) //> { r: 136, g: 226, b: 202 } 537 * 538 * @param {!Object} A jquery object. 539 * @returns {color} an RGB color object. (no alpha - this does not 540 * return transparent colors) 541 */ 542 fiveui.color.findBGColor = function(obj) { 543 var fc = fiveui.color; 544 var real = fc.colorToRGB(obj.css('background-color')); 545 var none = fc.colorToRGB('rgba(0, 0, 0, 0)'); 546 var i; 547 548 if (real.a != 1) { 549 550 // find parents with a non-default bg color: 551 var parents = obj.parents().filter( 552 function() { 553 var color = fc.colorToRGB($(this).css('background-color')); 554 return !equalRGBA(color, none); 555 }).map( 556 function(i) { 557 return fc.colorToRGB($(this).css('background-color')); 558 }); 559 560 // push a white element onto the end of parents 561 parents.push({ r: 255, g: 255, b: 255, a: 1}); 562 563 // takeWhile alpha != 1 564 var colors = []; 565 for (i=0; i < parents.length; i++) { 566 colors.push(parents[i]); 567 if (parents[i].a == 1) { 568 break; 569 } 570 } 571 572 // Compose the colors and return. Note that fc.alphaCombine is 573 // neither commutative, nor associative, so we need to be carefull 574 // of the order in which parent colors are combined. 575 var res = real; 576 for (i=0; i < colors.length; i++) { 577 res = fc.alphaCombine(res, colors[i]); 578 } 579 return res; 580 } 581 else { 582 return real; 583 } 584 }; 585 586 /** 587 * <p>Combines two colors, accounting for alpha values less than 1.</p> 588 * 589 * @description 590 * <p>Both colors must specify an alpha value in addition to red, green, 591 * and blue components. Colors are given as objects with `r`, `g`, `b`, 592 * and `a` properties.</p> 593 * 594 * @example 595 * var combined = fiveui.color.alphaCombine( 596 * { r: 152, g: 188, b: 75, a: 0.8 }, 597 * { r: 136, g: 226, b: 202, a: 0.8 } 598 * ); 599 * 600 * combined //> { r: 143, g: 186, b: 92, a: 0.96 } 601 * 602 * @param {color} top The color "on top" 603 * @param {color} bot The color "on bottom" 604 * @return {color} the composite RGBA color. 605 */ 606 fiveui.color.alphaCombine = function(top, bot) { 607 var result = { }; 608 result.r = Math.floor(top.r * top.a + bot.r * bot.a * (1 - top.a)); 609 result.g = Math.floor(top.g * top.a + bot.g * bot.a * (1 - top.a)); 610 result.b = Math.floor(top.b * top.a + bot.b * bot.a * (1 - top.a)); 611 612 result.a = top.a + bot.a * (1 - top.a); 613 614 return result; 615 }; 616 617 /** 618 * <p>Utilities for dealing with fonts.</p> 619 * 620 * @namespace 621 */ 622 fiveui.font = {}; 623 624 /** 625 * <p>Extracts the font-family, font-size (in px, as an int), and font-weight 626 * from a jQuery object.</p> 627 * 628 * @param {!Object} jElt A jQuery object to extract font info from 629 * @returns {!Object} An object with properties: 'family', 'size', and 'weight' 630 * @throws {ParseError} if the font size cannot be parsed 631 */ 632 fiveui.font.getFont = function (jElt) { 633 var res = {}; 634 var size = jElt.css('font-size'); 635 if(size.length > 0) { 636 var psize = /(\d+)/.exec(size); 637 if(!psize) { 638 throw { 639 name: 'ParseError', 640 message: 'Could not parse font size: ' + jElt.css('font-size') 641 }; 642 } 643 else { 644 res.size = psize; 645 } 646 } else { 647 res.size = ''; 648 } 649 res.family = jElt.css('font-family'); 650 res.weight = jElt.css('font-weight').toString(); 651 // normalize reporting of the following two synonyms 652 if (res.weight === '400') { res.weight = 'normal'; } 653 if (res.weight === '700') { res.weight = 'bold'; } 654 return res; 655 }; 656 657 /** 658 * <p>Validate a font property object extracted using fiveui.font.getFont().</p> 659 * 660 * @description 661 * <p>The `allow` parameter should be an object whose top level property names are 662 * (partial) font family names (e.g. 'Verdana'). For each font family name 663 * there should be an object whose properties are font weights (e.g. 'bold'), 664 * and for each font weight there should be an array of allowable sizes 665 * (e.g. [10, 11, 12]).</p> 666 * 667 * <p>The `font` parameter should be an object containing 'family', 'weight', and 668 * 'size' properties. These are returned by @see 669 * fiveui.font.getFont().</p> 670 * 671 * @example > allow = { 'Verdana': { 'bold': [10, 12], 'normal': [10, 12]}}; 672 * > font = { family: 'Verdana Arial sans-serif', size: "10", weight: "normal" }; 673 * > fiveui.font.validate(allow, font) -> true 674 * 675 * @param {!Object} allow Object containing allowable font sets (see description and examples) 676 * @param {!Object} font object to check 677 * @param font.family A partial font family name (e.g. 'Verdana') 678 * @param font.weight A font weight (e.g. 'bold') 679 * @param font.size A font size string (e.g. "10") 680 * @returns {!boolean} 681 */ 682 fiveui.font.validate = function (allow, font) { 683 var x; 684 for (x in allow) { // loop over allowed font family keywords 685 if (font.family.indexOf(x) !== -1) { break; } 686 else { return false; } 687 } 688 return (font.weight in allow[x] && allow[x][font.weight].indexOf(parseInt(font.size)) != -1); 689 }; 690 691 /** 692 * Functions outside the fiveui namespace that can be called during rule 693 * evaluation. 694 */ 695 696 /** 697 * <p>Report a problem to FiveUI.</p> 698 * 699 * @description 700 * <p>report is used to indicate that a guideline has been violated. 701 * Invocations should provide a short (1-line) string description of 702 * the problem, as well as a reference to the element of the DOM that 703 * violated the guideline.</p> 704 * 705 * <p>The second parameter is not strictly required, but we strongly 706 * suggest providing a node if at all possible, as that is used to 707 * visually highlight the problematic areas of the DOM at runtime. 708 * Debugging a guideline violation becomes much more difficult without 709 * the visual indicator.</p> 710 * 711 * @function 712 * @param {!string} desc The description of the problem to report. 713 * @param {?Node} node The node in the DOM that caused the problem. 714 * @name report 715 */ 716