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