comparecells.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. /**
  2. * Try to color the cells of comparison tables based on their contents.
  3. *
  4. * @title Compare cells
  5. */
  6. (function comparecells() {
  7. /* Create a new IFRAME to get a "clean" Window object, so we can use its
  8. * console. Sometimes sites (e.g. Twitter) override console.log and even
  9. * the entire console object. "delete console.log" or "delete console"
  10. * does not always work, and messing with the prototype seemed more
  11. * brittle than this. */
  12. let console = (function () {
  13. let iframe = document.getElementById('xxxJanConsole');
  14. if (!iframe) {
  15. iframe = document.createElementNS('http://www.w3.org/1999/xhtml', 'iframe');
  16. iframe.id = 'xxxJanConsole';
  17. iframe.style.display = 'none';
  18. (document.body || document.documentElement).appendChild(iframe);
  19. }
  20. return iframe && iframe.contentWindow && iframe.contentWindow.console || {
  21. log: function () {}
  22. };
  23. })();
  24. /**
  25. * Get the text content for the given element.
  26. */
  27. function getTextFromElement(element) {
  28. /* TODO: take IMG@alt, BUTTON@value etc. into account */
  29. return element.textContent.trim().toLowerCase();
  30. }
  31. /**
  32. * Get a Uint8Array of the SHA-256 bytes for the given string.
  33. */
  34. async function getSha256Bytes(string) {
  35. try {
  36. if (
  37. typeof crypto === 'object' && typeof crypto.subtle === 'object' && typeof crypto.subtle.digest === 'function'
  38. && typeof Uint8Array === 'function'
  39. && typeof TextEncoder === 'function'
  40. ) {
  41. return new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder('utf-8').encode(string)));
  42. }
  43. } catch (e) {
  44. return null;
  45. }
  46. }
  47. async function getColorsForValue(value) {
  48. /* Cache the calculated values. */
  49. getColorsForValue.cellValuesToRgb = getColorsForValue.cellValuesToRgb || {};
  50. if (!getColorsForValue.cellValuesToRgb[value]) {
  51. let normalizedValue = value.trim().toLowerCase();
  52. let hash;
  53. let yesValues = [
  54. '✔',
  55. 'yes',
  56. 'ja',
  57. 'oui',
  58. 'si',
  59. 'sí'
  60. ];
  61. let noValues = [
  62. 'x',
  63. 'no',
  64. 'nee',
  65. 'neen',
  66. 'nein',
  67. 'non',
  68. 'no'
  69. ];
  70. if (yesValues.indexOf(normalizedValue) > -1) {
  71. /* Make "Yes" cells green. */
  72. getColorsForValue.cellValuesToRgb[value] = [ 150, 255, 32 ];
  73. } else if (noValues.indexOf(normalizedValue) > -1) {
  74. /* Make "No" cells green. */
  75. getColorsForValue.cellValuesToRgb[value] = [ 238, 32, 32 ];
  76. } else if ((shaBytes = await getSha256Bytes(normalizedValue))) {
  77. /* Give other cells a color based on their content’s SHA
  78. * hash to ensure “consistent random colors” every time. */
  79. getColorsForValue.cellValuesToRgb[value] = [
  80. shaBytes[0],
  81. shaBytes[1],
  82. shaBytes[2]
  83. ];
  84. } else {
  85. /* If the SHA hash could not be calculated, just use random
  86. * values. These will change on every execution. */
  87. getColorsForValue.cellValuesToRgb[value] = [
  88. Math.random() * 255,
  89. Math.random() * 255,
  90. Math.random() * 255
  91. ];
  92. }
  93. }
  94. /* Calculate/approximate the lightness (tweaked from “RGB to HSL”) to
  95. * determine whether black or white text is best suited. */
  96. let isLight = 150 < (
  97. getColorsForValue.cellValuesToRgb[value][0] * 0.299
  98. + getColorsForValue.cellValuesToRgb[value][1] * 0.587
  99. + getColorsForValue.cellValuesToRgb[value][2] * 0.114
  100. );
  101. return {
  102. backgroundColor: 'rgb(' + getColorsForValue.cellValuesToRgb[value].join(', ') + ')',
  103. color: isLight
  104. ? 'black'
  105. : 'white',
  106. textShadow: isLight
  107. ? '1px 1px 3px white'
  108. : '1px 1px 3px black'
  109. };
  110. }
  111. /* The main function. */
  112. (function execute(document) {
  113. Array.from(document.querySelectorAll('table')).forEach(table => {
  114. Array.from(table.tBodies).forEach(tBody => {
  115. if (tBody.rows.length < 3) {
  116. console.log('Compare cells: skipping table body ', tBody, ' because it only has ', tBody.rows.length, ' rows');
  117. return;
  118. }
  119. Array.from(tBody.rows).forEach(tr => {
  120. /* Determine the values. */
  121. let cellValues = [];
  122. let uniqueCellValues = new Set();
  123. Array.from(tr.cells).forEach((cell, i) => {
  124. /* Don't take the header cells into account. */
  125. if (cell.tagName.toUpperCase() === 'TH') {
  126. return;
  127. }
  128. /* Assume the first cell is a header cell, even if it is not a TH. */
  129. if (i === 0) {
  130. return;
  131. }
  132. cellValues[i] = getTextFromElement(cell);
  133. uniqueCellValues.add(cellValues[i]);
  134. });
  135. /* Color (or not) the cells based on the values. */
  136. let isFirstValue = true;
  137. let firstValue;
  138. cellValues.forEach(async function(cellValue, i) {
  139. let hasTwoUniqueValues = uniqueCellValues.size == 2;
  140. if (isFirstValue) {
  141. firstValue = cellValue;
  142. isFirstValue = false;
  143. }
  144. let backgroundColor;
  145. let color;
  146. let textShadow;
  147. if (
  148. uniqueCellValues.size == 1 ||
  149. (hasTwoUniqueValues && cellValue === firstValue) ||
  150. cellValue.trim() === ''
  151. ) {
  152. backgroundColor = 'inherit';
  153. color = 'inherit';
  154. textShadow = 'inherit';
  155. } else {
  156. backgroundColor = (await getColorsForValue(cellValue)).backgroundColor;
  157. color = (await getColorsForValue(cellValue)).color;
  158. textShadow = (await getColorsForValue(cellValue)).textShadow;
  159. }
  160. tr.cells[i].style.setProperty('background-color', backgroundColor, 'important');
  161. tr.cells[i].style.setProperty('color', color, 'important');
  162. tr.cells[i].style.setProperty('text-shadow', textShadow, 'important');
  163. });
  164. });
  165. });
  166. });
  167. /* Recurse for frames and iframes. */
  168. try {
  169. Array.from(document.querySelectorAll('frame, iframe, object[type^="text/html"], object[type^="application/xhtml+xml"]')).forEach(function (elem) {
  170. execute(elem.contentDocument);
  171. });
  172. } catch (e) {
  173. /* Catch exceptions for out-of-domain access, but do not do anything with them. */
  174. }
  175. })(document);
  176. })();