diff --git a/CHANGELOG.md b/CHANGELOG.md index 70eb58a..081edd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to hihtml are documented in this file, which is (mostly) AI- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.1-beta] - 2026-05-14 + +### Changed + +* Improved end-of-section summaries across all check types: + - Validation and deprecated markup sections now end with a line like “30 files validated · 17 with issues, 13 clean” (previously only the clean-file count was shown) + - Link check summary now includes the per-file breakdown (e.g., “19 with issues, 11 clean”) inline, replacing the separate floating “_x_ files: no issues” line; also fixed “3 broken” → “3 broken links” +* Numbered output sections when more than one is shown (e.g., “1. Validation”, “2. Deprecated markup”), with the number repeated on the summary line for easier scanning in long output + ## [1.3.0-beta] - 2026-05-13 ### Added diff --git a/bin/hihtml.js b/bin/hihtml.js index 1444bb2..8fe810a 100755 --- a/bin/hihtml.js +++ b/bin/hihtml.js @@ -132,9 +132,19 @@ function makeProgress(label, total, { leadingNewline = false } = {}) { readProg.complete(); } - let quietHadOutput = false; - const showQuietHint = () => { - if (opts.quiet && quietHadOutput) console.log('\n(Use `-r` for a full report, or run without `-q` for inline output.)'); + const sections = []; + const sectionsPrint = () => { + const numbered = sections.length > 1; + sections.forEach((out, i) => { + if (!numbered) { console.log('\n' + out); return; } + const num = `${i + 1}. `; + const withTitle = num + out; + const withSummary = withTitle.replace(/\n(\n {2})([^\n]+)$/, `\n$1${num}$2`); + console.log('\n' + withSummary); + }); + }; + const quietHint = () => { + if (opts.quiet && sections.length > 0) console.log('\n(Use `-r` for a full report, or run without `-q` for inline output)'); }; /** @type {import('../src/adapters/check-code.js').CheckResult | undefined} */ @@ -151,9 +161,9 @@ function makeProgress(label, total, { leadingNewline = false } = {}) { ); const valOut = formatValidationResult(checkResult.validation, opts.quiet); - if (valOut) { console.log('\n' + valOut); quietHadOutput = true; } + if (valOut) sections.push(valOut); const depOut = formatDeprecationResult(checkResult.deprecation, opts.quiet); - if (depOut) { console.log('\n' + depOut); quietHadOutput = true; } + if (depOut) sections.push(depOut); report.results.checkCode = checkResult; } @@ -176,16 +186,17 @@ function makeProgress(label, total, { leadingNewline = false } = {}) { ); const linkOut = formatLinkCheckResult(linkResult, opts.quiet); - if (linkOut) { console.log('\n' + linkOut); quietHadOutput = true; } + if (linkOut) sections.push(linkOut); report.results.links = linkResult; } if (opts.all && checkResult?.validation.countErrors > 0) { + sectionsPrint(); console.error( '\n' + style.error(`${checkResult.validation.countErrors} validation ${checkResult.validation.countErrors === 1 ? 'error' : 'errors'} found—skipping minification`) + '\n' + '(Fix validation issues first or define HTML-validate rule IDs to ignore)' ); - showQuietHint(); + quietHint(); if (opts.report !== undefined) await saveReport(report, opts.report); process.exit(1); } @@ -220,10 +231,11 @@ function makeProgress(label, total, { leadingNewline = false } = {}) { minifyProg.complete(minifyResult.files.some(f => f.error)); const minOut = formatMinificationResult(minifyResult, opts.quiet); - if (minOut) { console.log('\n' + minOut); quietHadOutput = true; } + if (minOut) sections.push(minOut); report.results.minify = minifyResult; } + sectionsPrint(); if (opts.report !== undefined) await saveReport(report, opts.report); const hasErrors = (report.results.checkCode?.validation.countErrors ?? 0) > 0 @@ -231,7 +243,7 @@ function makeProgress(label, total, { leadingNewline = false } = {}) { || (report.results.links?.countFileErrors ?? 0) > 0 || (report.results.minify?.files.some(f => f.error) ?? false); - showQuietHint(); + quietHint(); process.exit(hasErrors ? 1 : 0); } catch (err) { diff --git a/bin/hihtml.test.js b/bin/hihtml.test.js index d03ec90..17b7d49 100644 --- a/bin/hihtml.test.js +++ b/bin/hihtml.test.js @@ -217,6 +217,12 @@ describe('CLI `--check-code`', () => { const { stdout } = run(['-i', path.join(tempDir, 'clean.html')]); assert.ok(stdout.includes('Validation')); }); + + test('Numbers sections when multiple are shown', () => { + const { stdout } = run(['-c', '-i', path.join(tempDir, 'clean.html')]); + assert.ok(stdout.includes('1. ')); + assert.ok(stdout.includes('2. ')); + }); }); // CLI: Check links @@ -308,6 +314,11 @@ describe('CLI `--check-links`', () => { assert.strictEqual(report.command, 'check-code+check-links'); fs.unlinkSync(reportPath); }); + + test('Does not number section when only one check is shown', () => { + const { stdout } = run(['-l', '-i', path.join(tempDir, 'links_none.html')]); + assert.ok(!stdout.includes('1. ')); + }); }); // CLI: Minify @@ -396,6 +407,16 @@ describe('CLI `--all`', () => { fs.rmSync(srcDir, { recursive: true, force: true }); fs.rmSync(outDir, { recursive: true, force: true }); }); + + test('Numbers all sections', () => { + const outDir = path.join(tempDir, 'all_numbered_out'); + const { stdout } = run(['-a', '-i', path.join(tempDir, 'clean.html'), '-o', outDir]); + assert.ok(stdout.includes('1. ')); + assert.ok(stdout.includes('2. ')); + assert.ok(stdout.includes('3. ')); + assert.ok(stdout.includes('4. ')); + fs.rmSync(outDir, { recursive: true, force: true }); + }); }); // CLI: Report diff --git a/package-lock.json b/package-lock.json index 22e7303..486e7d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hihtml", - "version": "1.3.0-beta", + "version": "1.3.1-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hihtml", - "version": "1.3.0-beta", + "version": "1.3.1-beta", "license": "MIT", "dependencies": { "commander": "^14.0.3", diff --git a/package.json b/package.json index 2df0313..ee97955 100644 --- a/package.json +++ b/package.json @@ -55,5 +55,5 @@ }, "type": "module", "types": "src/index.d.ts", - "version": "1.3.0-beta" + "version": "1.3.1-beta" } diff --git a/src/lib/output.js b/src/lib/output.js index f3fd68c..78f95d9 100644 --- a/src/lib/output.js +++ b/src/lib/output.js @@ -19,7 +19,6 @@ export { s as style }; */ export function formatValidationResult(result, quiet = false) { const withIssues = result.files.filter(f => f.messages.length > 0); - const cleanCount = result.files.length - withIssues.length; if (quiet && result.countErrors === 0 && result.countWarnings === 0) { if (result.countIgnored > 0) @@ -51,9 +50,8 @@ export function formatValidationResult(result, quiet = false) { } } - if (!quiet && cleanCount > 0) { - lines.push(` ${s.success(`${cleanCount} ${cleanCount === 1 ? 'file' : 'files'}: no issues`)}`); - } + if (!quiet && result.files.length > 0) + lines.push(filesSummary(result.files.length, withIssues.length, 'validated')); return lines.join('\n'); } @@ -65,7 +63,6 @@ export function formatValidationResult(result, quiet = false) { */ export function formatDeprecationResult(result, quiet = false) { const withIssues = result.files.filter(f => f.error || f.elements.length > 0 || f.attributes.length > 0); - const cleanCount = result.files.length - withIssues.length; if (quiet && withIssues.length === 0) return ''; @@ -84,9 +81,8 @@ export function formatDeprecationResult(result, quiet = false) { } } - if (!quiet && cleanCount > 0) { - lines.push(` ${s.success(`${cleanCount} ${cleanCount === 1 ? 'file' : 'files'}: no issues`)}`); - } + if (!quiet && result.files.length > 0) + lines.push(filesSummary(result.files.length, withIssues.length, 'checked for deprecated markup')); return lines.join('\n'); } @@ -100,7 +96,10 @@ export function formatLinkCheckResult(result, quiet = false) { const withIssues = result.files.filter(f => f.error || f.links.some(l => !l.ok || l.skipped || l.warning === 'permanent-redirect') ); - const cleanCount = result.files.length - withIssues.length; + const countIssues = withIssues.filter(f => + f.error || f.links.some(l => !l.ok || l.warning === 'permanent-redirect') + ).length; + const countClean = result.files.length - withIssues.length; const hasRealIssues = result.countBroken > 0 || result.countFileErrors > 0 || result.files.some(f => f.links.some(l => l.warning === 'permanent-redirect')); @@ -142,10 +141,6 @@ export function formatLinkCheckResult(result, quiet = false) { } } - if (!quiet && cleanCount > 0 && (withIssues.length > 0 || result.countChecked > 0 || result.countSkipped > 0)) { - lines.push(` ${s.success(`${cleanCount} ${cleanCount === 1 ? 'file' : 'files'}: no issues`)}`); - } - const total = result.countChecked; const summaryCount = total === 0 && result.countSkipped === 0 ? 'no http/https links' @@ -158,11 +153,14 @@ export function formatLinkCheckResult(result, quiet = false) { const summaryFileErrors = result.countFileErrors > 0 ? `, ${s.error(`${result.countFileErrors} file ${result.countFileErrors === 1 ? 'error' : 'errors'}`)}` : ''; + const fileParts = []; + if (countIssues > 0) fileParts.push(s.warning(`${countIssues} with issues`)); + if (countClean > 0) fileParts.push(s.success(`${countClean} clean`)); const summaryBroken = result.countBroken === 0 && result.countFileErrors === 0 ? s.success('no broken links') - : s.warning(`${result.countBroken} broken`); + : s.warning(`${result.countBroken} broken ${result.countBroken === 1 ? 'link' : 'links'}`); - lines.push(`\n ${result.files.length} ${result.files.length === 1 ? 'file' : 'files'} · ${summaryCount}${summarySkipped}${summaryFileErrors} · ${summaryBroken}`); + lines.push(`\n ${filesCount(result.files.length)} · ${summaryCount}${summarySkipped}${summaryFileErrors} · ${fileParts.join(', ')} · ${summaryBroken}`); return lines.join('\n'); } @@ -189,12 +187,23 @@ export function formatMinificationResult(result, quiet = false) { if (!quiet) { const successCount = result.files.filter(f => !f.error).length; - lines.push(`\n ${s.success(`${successCount} ${successCount === 1 ? 'file' : 'files'} minified, ${formatBytes(saved)} saved`)}`); + lines.push(`\n ${s.success(`${filesCount(successCount)} minified, ${formatBytes(saved)} saved`)}`); } return lines.join('\n'); } +/** @param {number} n @returns {string} */ +function filesCount(n) { return `${n} ${n === 1 ? 'file' : 'files'}`; } + +/** @param {number} total @param {number} issueCount @param {string} label @returns {string} */ +function filesSummary(total, issueCount, label) { + const parts = []; + if (issueCount > 0) parts.push(s.warning(`${issueCount} with issues`)); + if (total - issueCount > 0) parts.push(s.success(`${total - issueCount} clean`)); + return `\n ${filesCount(total)} ${label} · ${parts.join(', ')}`; +} + /** @param {number} bytes @returns {string} */ function formatBytes(bytes) { if (bytes < 1024) return `${bytes} B`;