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