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