html.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. /**
  2. * @fileoverview HTML reporter
  3. * @author Julian Laval
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Helpers
  8. //------------------------------------------------------------------------------
  9. const encodeHTML = (function() {
  10. const encodeHTMLRules = {
  11. "&": "&",
  12. "<": "&#60;",
  13. ">": "&#62;",
  14. '"': "&#34;",
  15. "'": "&#39;"
  16. };
  17. const matchHTML = /[&<>"']/ug;
  18. return function(code) {
  19. return code
  20. ? code.toString().replace(matchHTML, m => encodeHTMLRules[m] || m)
  21. : "";
  22. };
  23. }());
  24. /**
  25. * Get the final HTML document.
  26. * @param {Object} it data for the document.
  27. * @returns {string} HTML document.
  28. */
  29. function pageTemplate(it) {
  30. const { reportColor, reportSummary, date, results } = it;
  31. return `
  32. <!DOCTYPE html>
  33. <html>
  34. <head>
  35. <meta charset="UTF-8">
  36. <title>ESLint Report</title>
  37. <style>
  38. body {
  39. font-family:Arial, "Helvetica Neue", Helvetica, sans-serif;
  40. font-size:16px;
  41. font-weight:normal;
  42. margin:0;
  43. padding:0;
  44. color:#333
  45. }
  46. #overview {
  47. padding:20px 30px
  48. }
  49. td, th {
  50. padding:5px 10px
  51. }
  52. h1 {
  53. margin:0
  54. }
  55. table {
  56. margin:30px;
  57. width:calc(100% - 60px);
  58. max-width:1000px;
  59. border-radius:5px;
  60. border:1px solid #ddd;
  61. border-spacing:0px;
  62. }
  63. th {
  64. font-weight:400;
  65. font-size:medium;
  66. text-align:left;
  67. cursor:pointer
  68. }
  69. td.clr-1, td.clr-2, th span {
  70. font-weight:700
  71. }
  72. th span {
  73. float:right;
  74. margin-left:20px
  75. }
  76. th span:after {
  77. content:"";
  78. clear:both;
  79. display:block
  80. }
  81. tr:last-child td {
  82. border-bottom:none
  83. }
  84. tr td:first-child, tr td:last-child {
  85. color:#9da0a4
  86. }
  87. #overview.bg-0, tr.bg-0 th {
  88. color:#468847;
  89. background:#dff0d8;
  90. border-bottom:1px solid #d6e9c6
  91. }
  92. #overview.bg-1, tr.bg-1 th {
  93. color:#f0ad4e;
  94. background:#fcf8e3;
  95. border-bottom:1px solid #fbeed5
  96. }
  97. #overview.bg-2, tr.bg-2 th {
  98. color:#b94a48;
  99. background:#f2dede;
  100. border-bottom:1px solid #eed3d7
  101. }
  102. td {
  103. border-bottom:1px solid #ddd
  104. }
  105. td.clr-1 {
  106. color:#f0ad4e
  107. }
  108. td.clr-2 {
  109. color:#b94a48
  110. }
  111. td a {
  112. color:#3a33d1;
  113. text-decoration:none
  114. }
  115. td a:hover {
  116. color:#272296;
  117. text-decoration:underline
  118. }
  119. </style>
  120. </head>
  121. <body>
  122. <div id="overview" class="bg-${reportColor}">
  123. <h1>ESLint Report</h1>
  124. <div>
  125. <span>${reportSummary}</span> - Generated on ${date}
  126. </div>
  127. </div>
  128. <table>
  129. <tbody>
  130. ${results}
  131. </tbody>
  132. </table>
  133. <script type="text/javascript">
  134. var groups = document.querySelectorAll("tr[data-group]");
  135. for (i = 0; i < groups.length; i++) {
  136. groups[i].addEventListener("click", function() {
  137. var inGroup = document.getElementsByClassName(this.getAttribute("data-group"));
  138. this.innerHTML = (this.innerHTML.indexOf("+") > -1) ? this.innerHTML.replace("+", "-") : this.innerHTML.replace("-", "+");
  139. for (var j = 0; j < inGroup.length; j++) {
  140. inGroup[j].style.display = (inGroup[j].style.display !== "none") ? "none" : "table-row";
  141. }
  142. });
  143. }
  144. </script>
  145. </body>
  146. </html>
  147. `.trimLeft();
  148. }
  149. /**
  150. * Given a word and a count, append an s if count is not one.
  151. * @param {string} word A word in its singular form.
  152. * @param {int} count A number controlling whether word should be pluralized.
  153. * @returns {string} The original word with an s on the end if count is not one.
  154. */
  155. function pluralize(word, count) {
  156. return (count === 1 ? word : `${word}s`);
  157. }
  158. /**
  159. * Renders text along the template of x problems (x errors, x warnings)
  160. * @param {string} totalErrors Total errors
  161. * @param {string} totalWarnings Total warnings
  162. * @returns {string} The formatted string, pluralized where necessary
  163. */
  164. function renderSummary(totalErrors, totalWarnings) {
  165. const totalProblems = totalErrors + totalWarnings;
  166. let renderedText = `${totalProblems} ${pluralize("problem", totalProblems)}`;
  167. if (totalProblems !== 0) {
  168. renderedText += ` (${totalErrors} ${pluralize("error", totalErrors)}, ${totalWarnings} ${pluralize("warning", totalWarnings)})`;
  169. }
  170. return renderedText;
  171. }
  172. /**
  173. * Get the color based on whether there are errors/warnings...
  174. * @param {string} totalErrors Total errors
  175. * @param {string} totalWarnings Total warnings
  176. * @returns {int} The color code (0 = green, 1 = yellow, 2 = red)
  177. */
  178. function renderColor(totalErrors, totalWarnings) {
  179. if (totalErrors !== 0) {
  180. return 2;
  181. }
  182. if (totalWarnings !== 0) {
  183. return 1;
  184. }
  185. return 0;
  186. }
  187. /**
  188. * Get HTML (table row) describing a single message.
  189. * @param {Object} it data for the message.
  190. * @returns {string} HTML (table row) describing the message.
  191. */
  192. function messageTemplate(it) {
  193. const {
  194. parentIndex,
  195. lineNumber,
  196. columnNumber,
  197. severityNumber,
  198. severityName,
  199. message,
  200. ruleUrl,
  201. ruleId
  202. } = it;
  203. return `
  204. <tr style="display:none" class="f-${parentIndex}">
  205. <td>${lineNumber}:${columnNumber}</td>
  206. <td class="clr-${severityNumber}">${severityName}</td>
  207. <td>${encodeHTML(message)}</td>
  208. <td>
  209. <a href="${ruleUrl ? ruleUrl : ""}" target="_blank" rel="noopener noreferrer">${ruleId ? ruleId : ""}</a>
  210. </td>
  211. </tr>
  212. `.trimLeft();
  213. }
  214. /**
  215. * Get HTML (table rows) describing the messages.
  216. * @param {Array} messages Messages.
  217. * @param {int} parentIndex Index of the parent HTML row.
  218. * @param {Object} rulesMeta Dictionary containing metadata for each rule executed by the analysis.
  219. * @returns {string} HTML (table rows) describing the messages.
  220. */
  221. function renderMessages(messages, parentIndex, rulesMeta) {
  222. /**
  223. * Get HTML (table row) describing a message.
  224. * @param {Object} message Message.
  225. * @returns {string} HTML (table row) describing a message.
  226. */
  227. return messages.map(message => {
  228. const lineNumber = message.line || 0;
  229. const columnNumber = message.column || 0;
  230. let ruleUrl;
  231. if (rulesMeta) {
  232. const meta = rulesMeta[message.ruleId];
  233. if (meta && meta.docs && meta.docs.url) {
  234. ruleUrl = meta.docs.url;
  235. }
  236. }
  237. return messageTemplate({
  238. parentIndex,
  239. lineNumber,
  240. columnNumber,
  241. severityNumber: message.severity,
  242. severityName: message.severity === 1 ? "Warning" : "Error",
  243. message: message.message,
  244. ruleId: message.ruleId,
  245. ruleUrl
  246. });
  247. }).join("\n");
  248. }
  249. /**
  250. * Get HTML (table row) describing the result for a single file.
  251. * @param {Object} it data for the file.
  252. * @returns {string} HTML (table row) describing the result for the file.
  253. */
  254. function resultTemplate(it) {
  255. const { color, index, filePath, summary } = it;
  256. return `
  257. <tr class="bg-${color}" data-group="f-${index}">
  258. <th colspan="4">
  259. [+] ${encodeHTML(filePath)}
  260. <span>${encodeHTML(summary)}</span>
  261. </th>
  262. </tr>
  263. `.trimLeft();
  264. }
  265. // eslint-disable-next-line jsdoc/require-description
  266. /**
  267. * @param {Array} results Test results.
  268. * @param {Object} rulesMeta Dictionary containing metadata for each rule executed by the analysis.
  269. * @returns {string} HTML string describing the results.
  270. */
  271. function renderResults(results, rulesMeta) {
  272. return results.map((result, index) => resultTemplate({
  273. index,
  274. color: renderColor(result.errorCount, result.warningCount),
  275. filePath: result.filePath,
  276. summary: renderSummary(result.errorCount, result.warningCount)
  277. }) + renderMessages(result.messages, index, rulesMeta)).join("\n");
  278. }
  279. //------------------------------------------------------------------------------
  280. // Public Interface
  281. //------------------------------------------------------------------------------
  282. module.exports = function(results, data) {
  283. let totalErrors,
  284. totalWarnings;
  285. const metaData = data ? data.rulesMeta : {};
  286. totalErrors = 0;
  287. totalWarnings = 0;
  288. // Iterate over results to get totals
  289. results.forEach(result => {
  290. totalErrors += result.errorCount;
  291. totalWarnings += result.warningCount;
  292. });
  293. return pageTemplate({
  294. date: new Date(),
  295. reportColor: renderColor(totalErrors, totalWarnings),
  296. reportSummary: renderSummary(totalErrors, totalWarnings),
  297. results: renderResults(results, metaData)
  298. });
  299. };