spec-up-t-healthcheck 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,559 @@
1
+ /**
2
+ * @fileoverview Shared result details formatter for health check results
3
+ *
4
+ * This module provides functions to format health check result details into HTML.
5
+ * It works in both Node.js and browser environments, enabling consistent display
6
+ * of health check results in CLI tools, HTML reports, and web applications.
7
+ *
8
+ * The module formats:
9
+ * - Errors array with red styling
10
+ * - Warnings array with yellow/orange styling
11
+ * - Success messages array with green styling
12
+ * - Additional metadata (missingFields, count, packageData)
13
+ *
14
+ * URLs in messages are automatically converted to clickable links.
15
+ *
16
+ * @author spec-up-t-healthcheck
17
+ */
18
+
19
+ /**
20
+ * Escapes HTML special characters to prevent XSS attacks.
21
+ * This is critical for security when displaying user-generated content.
22
+ *
23
+ * @param {string|any} text - Text to escape
24
+ * @returns {string} HTML-escaped text
25
+ *
26
+ * @example
27
+ * ```javascript
28
+ * escapeHtml('<script>alert("xss")</script>');
29
+ * // Returns: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
30
+ * ```
31
+ */
32
+ export function escapeHtml(text) {
33
+ if (typeof text !== 'string') {
34
+ return String(text);
35
+ }
36
+
37
+ const map = {
38
+ '&': '&amp;',
39
+ '<': '&lt;',
40
+ '>': '&gt;',
41
+ '"': '&quot;',
42
+ "'": '&#039;'
43
+ };
44
+
45
+ return text.replace(/[&<>"']/g, (m) => map[m]);
46
+ }
47
+
48
+ /**
49
+ * Converts URLs in text to clickable links that open in a new tab.
50
+ * The text is first escaped for HTML safety, then URLs are converted to links.
51
+ *
52
+ * This function enhances user experience by making URLs in error messages,
53
+ * warnings, and other feedback immediately actionable.
54
+ *
55
+ * @param {string} text - Text potentially containing URLs
56
+ * @returns {string} HTML string with clickable links
57
+ *
58
+ * @example
59
+ * ```javascript
60
+ * linkifyUrls('Check https://example.com for details');
61
+ * // Returns: 'Check <a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a> for details'
62
+ * ```
63
+ */
64
+ export function linkifyUrls(text) {
65
+ if (typeof text !== 'string') {
66
+ return escapeHtml(String(text));
67
+ }
68
+
69
+ // First escape the text for HTML safety
70
+ const escaped = escapeHtml(text);
71
+
72
+ // URL regex pattern that matches http://, https://, and www. URLs
73
+ // Excludes common trailing punctuation like ), >, ", and whitespace
74
+ const urlPattern = /(https?:\/\/[^\s<>"()]+|www\.[^\s<>"()]+)/g;
75
+
76
+ // Replace URLs with clickable links
77
+ return escaped.replace(urlPattern, (url) => {
78
+ // Ensure the URL has a protocol for the href attribute
79
+ const href = url.startsWith('http') ? url : `https://${url}`;
80
+
81
+ // Create an anchor tag that opens in a new tab with security attributes
82
+ return `<a href="${href}" target="_blank" rel="noopener noreferrer" class="text-decoration-underline">${url}</a>`;
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Formats result details into HTML.
88
+ *
89
+ * This is the main formatter function that converts a health check result's
90
+ * details object into a formatted HTML string. It handles:
91
+ * - Errors: Displayed in red with bullet points
92
+ * - Warnings: Displayed in yellow/orange with bullet points
93
+ * - Success: Displayed in green with bullet points (can be hidden for brevity)
94
+ * - Info: Displayed in muted gray with bullet points (contextual information)
95
+ * - Metadata: Missing fields, counts, package data
96
+ * - Console Messages: Special table format for console output (timestamp, type, message, operation, additionalData)
97
+ *
98
+ * All URLs in messages are automatically converted to clickable links.
99
+ *
100
+ * @param {Object} details - The details object from a health check result
101
+ * @param {string[]} [details.errors] - Array of error messages
102
+ * @param {string[]} [details.warnings] - Array of warning messages
103
+ * @param {string[]} [details.success] - Array of success messages
104
+ * @param {string[]} [details.info] - Array of informational messages
105
+ * @param {string[]} [details.missingFields] - Array of missing field names
106
+ * @param {number} [details.count] - Count of items found
107
+ * @param {Object} [details.packageData] - Package metadata
108
+ * @param {string} [details.packageData.name] - Package name
109
+ * @param {string} [details.packageData.version] - Package version
110
+ * @param {Object} [details.analysis] - Console message analysis data
111
+ * @param {Array} [details.allMessages] - All console messages for table display
112
+ * @returns {string} Formatted HTML string
113
+ *
114
+ * @example
115
+ * ```javascript
116
+ * const details = {
117
+ * errors: ['Field "title" is missing'],
118
+ * warnings: ['Field "favicon" is recommended'],
119
+ * success: ['Field "author" exists'],
120
+ * info: ['URL accessibility checks skipped (browser environment)']
121
+ * };
122
+ * const html = formatResultDetails(details);
123
+ * // Returns formatted HTML with error, warning, success, and info lists
124
+ * ```
125
+ */
126
+ export function formatResultDetails(details) {
127
+ let html = '';
128
+
129
+ // Special handling for console-messages check with errors/warnings
130
+ // Show compact error/warning list first, then full messages table
131
+ if (details.analysis && details.allMessages) {
132
+ // Show errors first (compact format for quick scanning)
133
+ if (details.errors && details.errors.length > 0) {
134
+ const isConsoleMessageFormat = typeof details.errors[0] === 'object' && details.errors[0].timestamp;
135
+ if (isConsoleMessageFormat) {
136
+ html += formatConsoleMessageList('Errors', details.errors, 'danger');
137
+ if (details.errorsNote) {
138
+ html += `<div class="mt-1"><small class="text-muted">${escapeHtml(details.errorsNote)}</small></div>`;
139
+ }
140
+ }
141
+ }
142
+
143
+ // Show warnings (compact format)
144
+ if (details.warnings && details.warnings.length > 0) {
145
+ const isConsoleMessageFormat = typeof details.warnings[0] === 'object' && details.warnings[0].timestamp;
146
+ if (isConsoleMessageFormat) {
147
+ html += formatConsoleMessageList('Warnings', details.warnings, 'warning');
148
+ if (details.warningsNote) {
149
+ html += `<div class="mt-1"><small class="text-muted">${escapeHtml(details.warningsNote)}</small></div>`;
150
+ }
151
+ }
152
+ }
153
+
154
+ // Add the full messages table (with all message types)
155
+ html += formatConsoleMessagesTable(details);
156
+ return html;
157
+ }
158
+
159
+ // Special handling for markdown-tables check
160
+ if (details.details && Array.isArray(details.details)) {
161
+ html += formatMarkdownTablesDetails(details);
162
+ return html;
163
+ }
164
+
165
+ // Display errors array with clickable URLs
166
+ // Errors are shown with strong red styling to draw immediate attention
167
+ if (details.errors && details.errors.length > 0) {
168
+ // Check if errors are objects (from console-messages) or strings (from other checks)
169
+ const isConsoleMessageFormat = typeof details.errors[0] === 'object' && details.errors[0].timestamp;
170
+
171
+ if (isConsoleMessageFormat) {
172
+ html += formatConsoleMessageList('Errors', details.errors, 'danger');
173
+ if (details.errorsNote) {
174
+ html += `<small class="text-muted">${escapeHtml(details.errorsNote)}</small>`;
175
+ }
176
+ } else {
177
+ html += `<div class="mt-2 detail-errors"><strong class="text-danger">Errors:</strong><ul class="mb-0 mt-1">`;
178
+ details.errors.forEach(error => {
179
+ html += `<li class="text-danger">${linkifyUrls(error)}</li>`;
180
+ });
181
+ html += `</ul></div>`;
182
+ }
183
+ }
184
+
185
+ // Display warnings array with clickable URLs
186
+ // Warnings indicate potential issues that should be addressed but aren't critical
187
+ if (details.warnings && details.warnings.length > 0) {
188
+ // Check if warnings are objects (from console-messages) or strings (from other checks)
189
+ const isConsoleMessageFormat = typeof details.warnings[0] === 'object' && details.warnings[0].timestamp;
190
+
191
+ if (isConsoleMessageFormat) {
192
+ html += formatConsoleMessageList('Warnings', details.warnings, 'warning');
193
+ if (details.warningsNote) {
194
+ html += `<small class="text-muted">${escapeHtml(details.warningsNote)}</small>`;
195
+ }
196
+ } else {
197
+ html += `<div class="mt-2 detail-warnings"><strong class="text-warning">Warnings:</strong><ul class="mb-0 mt-1">`;
198
+ details.warnings.forEach(warning => {
199
+ html += `<li class="text-warning">${linkifyUrls(warning)}</li>`;
200
+ });
201
+ html += `</ul></div>`;
202
+ }
203
+ }
204
+
205
+ // Display success messages array with clickable URLs
206
+ // Add detail-success class so these can be hidden when "Show passing checks" is disabled
207
+ // This helps users focus on issues while still providing complete information when needed
208
+ if (details.success && details.success.length > 0) {
209
+ html += `<div class="mt-2 detail-success"><strong class="text-success">Success:</strong><ul class="mb-0 mt-1">`;
210
+ details.success.forEach(success => {
211
+ html += `<li class="text-success">${linkifyUrls(success)}</li>`;
212
+ });
213
+ html += `</ul></div>`;
214
+ }
215
+
216
+ // Display informational messages array with clickable URLs
217
+ // Info messages provide additional context without indicating success or failure
218
+ if (details.info && details.info.length > 0) {
219
+ html += `<div class="mt-2 detail-info"><strong class="text-info">Info:</strong><ul class="mb-0 mt-1">`;
220
+ details.info.forEach(info => {
221
+ html += `<li class="text-muted">${linkifyUrls(info)}</li>`;
222
+ });
223
+ html += `</ul></div>`;
224
+ }
225
+
226
+ // Display missing fields (existing functionality for backward compatibility)
227
+ // Provides a quick summary of what's missing in validation results
228
+ if (details.missingFields && details.missingFields.length > 0) {
229
+ html += `<br><small class="text-muted">Missing fields: ${details.missingFields.map(escapeHtml).join(', ')}</small>`;
230
+ }
231
+
232
+ // Display count (existing functionality)
233
+ // Shows the number of items found in checks that count resources
234
+ if (details.count !== undefined) {
235
+ html += `<br><small class="text-muted">Files found: ${details.count}</small>`;
236
+ }
237
+
238
+ // Display package data (existing functionality)
239
+ // Shows package name and version for package.json validation
240
+ if (details.packageData) {
241
+ html += `<br><small class="text-muted">Package: ${escapeHtml(details.packageData.name)}@${escapeHtml(details.packageData.version)}</small>`;
242
+ }
243
+
244
+ return html;
245
+ }
246
+
247
+ /**
248
+ * Formats console messages in a compact list format.
249
+ * This is used when showing errors/warnings from console-messages check.
250
+ *
251
+ * @param {string} title - Title for the list (e.g., "Errors", "Warnings")
252
+ * @param {Array<Object>} messages - Array of console message objects
253
+ * @param {string} colorClass - Bootstrap color class ('danger', 'warning', etc.)
254
+ * @returns {string} HTML string with formatted list
255
+ */
256
+ export function formatConsoleMessageList(title, messages, colorClass) {
257
+ let html = `<div class="mt-2"><strong class="text-${colorClass}">${escapeHtml(title)}:</strong>`;
258
+ html += `<div class="table-responsive mt-2"><table class="table-sm" style="width: 100%; background-color: transparent;">`;
259
+ html += `<thead>`;
260
+ html += `<tr>`;
261
+ // html += `<th style="width: 150px; background-color: transparent">Timestamp</th>`;
262
+ html += `<th class="d-none" style="background-color: transparent">Message</th>`;
263
+ html += `</tr>`;
264
+ html += `</thead>`;
265
+ html += `<tbody>`;
266
+
267
+ messages.forEach(msg => {
268
+ const timestamp = msg.timestamp ? new Date(msg.timestamp).toLocaleString() : 'N/A';
269
+ const message = msg.message || '';
270
+ const additionalData = msg.additionalData ? ` [${Array.isArray(msg.additionalData) ? msg.additionalData.join(', ') : msg.additionalData}]` : '';
271
+
272
+ html += `<tr>`;
273
+ // html += `<td><small>${escapeHtml(timestamp)}</small></td>`;
274
+ html += `<td><small>${linkifyUrls(message + additionalData)}</small></td>`;
275
+ html += `</tr>`;
276
+ });
277
+
278
+ html += `</tbody></table></div></div>`;
279
+ return html;
280
+ }
281
+
282
+ /**
283
+ * Formats all console messages in a detailed table format.
284
+ * This is used when the check includes allMessages for verbose display.
285
+ *
286
+ * @param {Object} details - Details object containing console message data
287
+ * @returns {string} HTML string with formatted table
288
+ */
289
+ export function formatConsoleMessagesTable(details) {
290
+ const { analysis, allMessages, metadata } = details;
291
+
292
+ let html = '<div class="mt-3">';
293
+
294
+ // // Add a clear heading for the full messages table
295
+ // html += `<div class="mb-3">`;
296
+ // html += `<strong class="text-primary">All Console Messages (${allMessages.length}):</strong><br>`;
297
+ // html += `<small class="text-muted">Complete log of all operations with timestamp, type, and details</small>`;
298
+ // html += `</div>`;
299
+
300
+ // // Add summary statistics
301
+ // if (analysis) {
302
+ // html += `<div class="mb-3">`;
303
+ // html += `<strong>Summary:</strong><br>`;
304
+ // html += `<small class="text-muted">`;
305
+ // html += `Total: ${analysis.totalMessages || 0} | `;
306
+ // html += `<span class="text-success">Success: ${analysis.successCount || 0}</span> | `;
307
+ // html += `<span class="text-danger">Errors: ${analysis.errorCount || 0}</span> | `;
308
+ // html += `<span class="text-warning">Warnings: ${analysis.warningCount || 0}</span>`;
309
+ // html += `</small>`;
310
+ // html += `</div>`;
311
+ // }
312
+
313
+ // // Add operations info if available
314
+ // if (metadata && metadata.operations && metadata.operations.length > 0) {
315
+ // html += `<div class="mb-2">`;
316
+ // html += `<small class="text-muted">Operations: ${metadata.operations.map(escapeHtml).join(', ')}</small>`;
317
+ // html += `</div>`;
318
+ // }
319
+
320
+ // Create the messages table (without Operation column)
321
+ html += `<div class="table-responsive">`;
322
+ html += `<table class="table-sm table-striped" style="width: 100%;">`;
323
+ html += `<thead class="table-light">`;
324
+ html += `<tr>`;
325
+ // html += `<th style="width: 150px;">Timestamp</th>`;
326
+ // html += `<th style="width: 80px;">Type</th>`;
327
+ html += `<th class="d-none">Message</th>`;
328
+ // html += `<th style="width: 150px;">Additional Data</th>`;
329
+ html += `</tr>`;
330
+ html += `</thead>`;
331
+ html += `<tbody>`;
332
+
333
+ // Format each message as a table row
334
+ allMessages.forEach(msg => {
335
+ const timestamp = msg.timestamp ? new Date(msg.timestamp).toLocaleString() : 'N/A';
336
+ const type = msg.type || 'info';
337
+ const message = msg.message || '';
338
+ const additionalData = msg.additionalData
339
+ ? (Array.isArray(msg.additionalData) ? msg.additionalData.join(', ') : String(msg.additionalData))
340
+ : '';
341
+
342
+ // Apply color based on message type
343
+ let typeClass = 'text-muted';
344
+ let typeBadge = 'secondary';
345
+ switch (type) {
346
+ case 'error':
347
+ typeClass = 'text-danger';
348
+ typeBadge = 'danger';
349
+ break;
350
+ case 'warn':
351
+ typeClass = 'text-warning';
352
+ typeBadge = 'warning';
353
+ break;
354
+ case 'success':
355
+ typeClass = 'text-success';
356
+ typeBadge = 'success';
357
+ break;
358
+ case 'info':
359
+ typeClass = 'text-info';
360
+ typeBadge = 'info';
361
+ break;
362
+ case 'highlight':
363
+ typeClass = 'text-primary';
364
+ typeBadge = 'primary';
365
+ break;
366
+ }
367
+
368
+ html += `<tr>`;
369
+ // html += `<td><small>${escapeHtml(timestamp)}</small></td>`;
370
+ // html += `<td><span class="badge bg-${typeBadge}">${escapeHtml(type)}</span></td>`;
371
+ html += `<td><small>${linkifyUrls(message)}</small></td>`;
372
+ // html += `<td><small class="text-muted">${escapeHtml(additionalData)}</small></td>`;
373
+ html += `</tr>`;
374
+ });
375
+
376
+ html += `</tbody></table></div></div>`;
377
+
378
+ return html;
379
+ }
380
+
381
+ /**
382
+ * Formats markdown tables details into HTML.
383
+ *
384
+ * @param {Object} details - Details object containing markdown tables data
385
+ * @returns {string} HTML string with formatted table issues
386
+ */
387
+ export function formatMarkdownTablesDetails(details) {
388
+ const { details: fileResults } = details;
389
+
390
+ let html = '<div class="mt-2">';
391
+ html += '<div class="table-responsive">';
392
+ html += '<table class="table table-sm table-striped">';
393
+ html += '<thead class="table-light">';
394
+ html += '<tr>';
395
+ html += '<th>File</th>';
396
+ html += '<th>Line</th>';
397
+ html += '<th>Issue</th>';
398
+ html += '<th>Content</th>';
399
+ html += '</tr>';
400
+ html += '</thead>';
401
+ html += '<tbody>';
402
+
403
+ fileResults.forEach(fileResult => {
404
+ fileResult.tables.forEach(table => {
405
+ table.issues.forEach(issue => {
406
+ const severityClass = issue.severity === 'error' ? 'text-danger' :
407
+ issue.severity === 'warning' ? 'text-warning' : 'text-info';
408
+ const severityIcon = issue.severity === 'error' ? 'bi-exclamation-triangle-fill' :
409
+ issue.severity === 'warning' ? 'bi-exclamation-circle-fill' : 'bi-info-circle-fill';
410
+
411
+ html += '<tr>';
412
+ html += `<td><small><code>${escapeHtml(fileResult.file)}</code></small></td>`;
413
+ html += `<td><small>${issue.line}</small></td>`;
414
+ html += `<td><small class="${severityClass}"><i class="bi ${severityIcon}"></i> ${escapeHtml(issue.message)}</small></td>`;
415
+ html += `<td><small class="text-muted">${escapeHtml(issue.content || '')}</small></td>`;
416
+ html += '</tr>';
417
+ });
418
+ });
419
+ });
420
+
421
+ html += '</tbody>';
422
+ html += '</table>';
423
+ html += '</div>';
424
+ html += '</div>';
425
+
426
+ return html;
427
+ }
428
+
429
+ /**
430
+ * Formats a single health check result into a table row HTML string.
431
+ *
432
+ * This function is useful for building tables of health check results.
433
+ * It includes status badges, check names, messages, and formatted details.
434
+ *
435
+ * @param {Object} result - A health check result object
436
+ * @param {string} result.status - Status of the check ('pass', 'fail', 'warn', 'skip')
437
+ * @param {string} result.check - Name of the health check
438
+ * @param {string} result.message - Primary message describing the result
439
+ * @param {Object} [result.details] - Additional details to format
440
+ * @returns {string} HTML table row string
441
+ *
442
+ * @example
443
+ * ```javascript
444
+ * const result = {
445
+ * status: 'fail',
446
+ * check: 'specs.json validation',
447
+ * message: 'specs.json has 2 error(s)',
448
+ * details: {
449
+ * errors: ['Field "title" is missing', 'Field "author" is empty']
450
+ * }
451
+ * };
452
+ * const rowHtml = formatResultAsTableRow(result);
453
+ * ```
454
+ */
455
+ export function formatResultAsTableRow(result) {
456
+ const { statusClass, statusIcon, statusText } = getStatusDisplay(result);
457
+ const rowClass = getRowClass(result);
458
+
459
+ let detailsHtml = '';
460
+ if (result.details && Object.keys(result.details).length > 0) {
461
+ detailsHtml = formatResultDetails(result.details);
462
+ }
463
+
464
+ return `<tr data-status="${result.status}" class="check-row ${rowClass}">
465
+ <td class="${statusClass} status-badge">
466
+ <i class="bi ${statusIcon} status-icon"></i>
467
+ <span>${statusText}</span>
468
+ </td>
469
+ <td>${escapeHtml(result.check)}</td>
470
+ <td>
471
+ ${escapeHtml(result.message)}
472
+ ${detailsHtml}
473
+ </td>
474
+ </tr>`;
475
+ }
476
+
477
+ /**
478
+ * Gets display properties (class, icon, text) for a result status.
479
+ *
480
+ * This helper function centralizes the mapping between status values
481
+ * and their visual representation, ensuring consistency across the UI.
482
+ *
483
+ * @param {Object} result - The health check result
484
+ * @param {string} result.status - Status value ('pass', 'fail', 'warn', 'skip')
485
+ * @returns {{statusClass: string, statusIcon: string, statusText: string}}
486
+ *
487
+ * @example
488
+ * ```javascript
489
+ * const display = getStatusDisplay({ status: 'fail' });
490
+ * // Returns: {
491
+ * // statusClass: 'text-danger',
492
+ * // statusIcon: 'bi-x-circle-fill',
493
+ * // statusText: 'Fail'
494
+ * // }
495
+ * ```
496
+ */
497
+ export function getStatusDisplay(result) {
498
+ switch (result.status) {
499
+ case 'pass':
500
+ return {
501
+ statusClass: 'text-success',
502
+ statusIcon: 'bi-check-circle-fill',
503
+ statusText: 'Pass'
504
+ };
505
+ case 'fail':
506
+ return {
507
+ statusClass: 'text-danger',
508
+ statusIcon: 'bi-x-circle-fill',
509
+ statusText: 'Fail'
510
+ };
511
+ case 'warn':
512
+ return {
513
+ statusClass: 'text-warning',
514
+ statusIcon: 'bi-exclamation-triangle-fill',
515
+ statusText: 'Warning'
516
+ };
517
+ case 'skip':
518
+ return {
519
+ statusClass: 'text-muted',
520
+ statusIcon: 'bi-dash-circle',
521
+ statusText: 'Skipped'
522
+ };
523
+ default:
524
+ return {
525
+ statusClass: 'text-secondary',
526
+ statusIcon: 'bi-question-circle',
527
+ statusText: 'Unknown'
528
+ };
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Gets the appropriate CSS class for a table row based on the result status.
534
+ *
535
+ * This provides Bootstrap table row styling to visually distinguish
536
+ * different result statuses in tabular displays.
537
+ *
538
+ * @param {Object} result - The health check result
539
+ * @param {string} result.status - Status value ('pass', 'fail', 'warn')
540
+ * @returns {string} CSS class name
541
+ *
542
+ * @example
543
+ * ```javascript
544
+ * const rowClass = getRowClass({ status: 'fail' });
545
+ * // Returns: 'table-danger'
546
+ * ```
547
+ */
548
+ export function getRowClass(result) {
549
+ switch (result.status) {
550
+ case 'fail':
551
+ return 'table-danger';
552
+ case 'warn':
553
+ return 'table-warning';
554
+ case 'pass':
555
+ return 'table-success';
556
+ default:
557
+ return '';
558
+ }
559
+ }
package/lib/formatters.js CHANGED
@@ -98,6 +98,20 @@ export function formatResultsAsText(healthCheckOutput, useColors = false) {
98
98
  if (result.details.packageData) {
99
99
  output.push(` Package: ${result.details.packageData.name}@${result.details.packageData.version}`);
100
100
  }
101
+ // Special handling for markdown-tables check
102
+ if (result.check === 'markdown-tables' && result.details.details) {
103
+ const fileResults = result.details.details;
104
+ fileResults.forEach(fileResult => {
105
+ output.push(` 📄 ${fileResult.file}:`);
106
+ fileResult.tables.forEach(table => {
107
+ output.push(` Table at line ${table.startLine}:`);
108
+ table.issues.forEach(issue => {
109
+ const severityIcon = issue.severity === 'error' ? '❌' : issue.severity === 'warning' ? 'âš ī¸' : 'â„šī¸';
110
+ output.push(` ${severityIcon} Line ${issue.line}: ${issue.message}`);
111
+ });
112
+ });
113
+ });
114
+ }
101
115
  }
102
116
 
103
117
  output.push('');