|
1 | 1 | import { writeFileSync } from 'fs'
|
2 | 2 | import { FormatterCallback } from '../formatter'
|
3 | 3 |
|
| 4 | +/** |
| 5 | + * Escapes HTML characters for safe display in HTML |
| 6 | + * @param message The message that might contain HTML code |
| 7 | + * @returns Escaped message |
| 8 | + */ |
| 9 | +const formatMessage = (message: string): string => |
| 10 | + message |
| 11 | + .replace(/&/g, '&') |
| 12 | + .replace(/</g, '<') |
| 13 | + .replace(/>/g, '>') |
| 14 | + .replace(/"/g, '"') |
| 15 | + .replace(/'/g, ''') |
| 16 | + |
4 | 17 | const htmlFormatter: FormatterCallback = function (formatter) {
|
5 | 18 | formatter.on('end', (event) => {
|
6 |
| - let fileContent = '<!DOCTYPE html><html lang="en">' |
7 |
| - fileContent += '\n<head>' |
8 |
| - fileContent += '\n<meta charset="UTF-8">' |
| 19 | + let fileContent = '<!DOCTYPE html>\n' |
| 20 | + fileContent += '<html lang="en">\n' |
| 21 | + fileContent += '<head>\n' |
| 22 | + fileContent += '<meta charset="utf-8">\n' |
9 | 23 | fileContent +=
|
10 |
| - '\n<meta name="viewport" content="width=device-width, initial-scale=1">' |
11 |
| - fileContent += '\n<title>HTML Hint Violation Report</title>' |
12 |
| - fileContent += '\n<meta name="generator" content="HTMLHint">' |
| 24 | + '<meta name="viewport" content="width=device-width, initial-scale=1">\n' |
| 25 | + fileContent += '<title>HTML Hint Violation Report</title>\n' |
| 26 | + fileContent += '<meta name="generator" content="HTMLHint">\n' |
13 | 27 | fileContent +=
|
14 |
| - '\n<style>body{font-family:Arial,helvetica,sans-serif;} footer{margin-top:20px;text-align:center;opacity:0.5;}</style>' |
| 28 | + '<style>body{font-family:Arial,helvetica,sans-serif;} footer{margin-top:20px;text-align:center;opacity:0.5;}</style>\n' |
15 | 29 | fileContent +=
|
16 |
| - '\n<style>table{border-collapse:collapse;width:100%;} th,td{border:1px solid rgb(128,128,128,0.4);padding:8px;text-align:left;} th{background-color:rgb(128,128,128,0.2);}</style>' |
| 30 | + '<style>table{border-collapse:collapse;width:100%;} th,td{border:1px solid rgb(128,128,128,0.4);padding:8px;text-align:left;} th{background-color:rgb(128,128,128,0.2);}</style>\n' |
17 | 31 | fileContent +=
|
18 |
| - '\n<style>@media (prefers-color-scheme: dark) {body {background-color:#333;color:#fff;}}</style>' |
19 |
| - fileContent += '\n</head>' |
20 |
| - fileContent += '\n<body>' |
21 |
| - fileContent += '\n<h1>Violation Report</h1>' |
22 |
| - fileContent += '\n<main>' |
23 |
| - fileContent += '\n<table>' |
| 32 | + '<style>@media (prefers-color-scheme: dark) {body {background-color:#333;color:#fff;}}</style>\n' |
| 33 | + fileContent += '</head>\n' |
| 34 | + fileContent += '<body>\n' |
| 35 | + fileContent += '<h1>Violation Report</h1>\n' |
| 36 | + fileContent += '<main>\n' |
| 37 | + fileContent += '<table>\n' |
24 | 38 | fileContent +=
|
25 |
| - '\n<tr><th>Number#</th><th>File Name</th><th>Line Number</th><th>Message</th></tr>' |
| 39 | + '<tr><th>Number#</th><th>File Name</th><th>Line Number</th><th>Message</th></tr>\n' |
| 40 | + |
| 41 | + let totalMessages = 0 |
| 42 | + for (const { messages } of event.arrAllMessages) { |
| 43 | + totalMessages += messages.length |
| 44 | + } |
26 | 45 |
|
| 46 | + let messageCount = 0 |
27 | 47 | for (const { file, messages } of event.arrAllMessages) {
|
28 |
| - fileContent += messages |
29 |
| - .map( |
30 |
| - ({ line, message }, i) => |
31 |
| - `\n<tr><td>${ |
32 |
| - i + 1 |
33 |
| - }</td><td>${file}</td><td>${line}</td><td>${message}</td></tr>` |
34 |
| - ) |
35 |
| - .join('') |
| 48 | + messages.forEach(({ line, message }) => { |
| 49 | + messageCount++ |
| 50 | + const isLastMessage = messageCount === totalMessages |
| 51 | + |
| 52 | + if (isLastMessage) { |
| 53 | + // Last message - add the table closing tag right after it (no newline) |
| 54 | + fileContent += `<tr><td>${messageCount}</td><td>${file}</td><td>${line}</td><td>${formatMessage(message)}</td></tr></table>\n` |
| 55 | + } else { |
| 56 | + fileContent += `<tr><td>${messageCount}</td><td>${file}</td><td>${line}</td><td>${formatMessage(message)}</td></tr>\n` |
| 57 | + } |
| 58 | + }) |
36 | 59 | }
|
37 | 60 |
|
38 |
| - fileContent += '</table>' |
| 61 | + // Table closing tag is now included with the last message |
| 62 | + // fileContent += '</table>\n' |
39 | 63 | fileContent +=
|
40 |
| - '\n<footer><small>Generated by <a href="https://htmlhint.com" target="_blank" rel="noopener">HTMLHint</a></small></footer>' |
41 |
| - fileContent += '\n</main>' |
42 |
| - fileContent += '\n</body>' |
| 64 | + '<footer><small>Generated by <a href="https://htmlhint.com" target="_blank" rel="noopener">HTMLHint</a></small></footer>\n' |
| 65 | + fileContent += '</main>\n' |
| 66 | + fileContent += '</body>\n' |
43 | 67 | fileContent += '</html>'
|
44 | 68 | console.log(fileContent)
|
45 | 69 | writeFileSync('report.html', fileContent)
|
|
0 commit comments