Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 21 additions & 9 deletions bin/hihtml.js
Original file line number Diff line number Diff line change
Expand Up @@ -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} */
Expand All @@ -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;
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -220,18 +231,19 @@ 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
|| (report.results.links?.countBroken ?? 0) > 0
|| (report.results.links?.countFileErrors ?? 0) > 0
|| (report.results.minify?.files.some(f => f.error) ?? false);

showQuietHint();
quietHint();
process.exit(hasErrors ? 1 : 0);

} catch (err) {
Expand Down
21 changes: 21 additions & 0 deletions bin/hihtml.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,5 @@
},
"type": "module",
"types": "src/index.d.ts",
"version": "1.3.0-beta"
"version": "1.3.1-beta"
}
41 changes: 25 additions & 16 deletions src/lib/output.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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');
}
Expand All @@ -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 '';

Expand All @@ -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');
}
Expand All @@ -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'));
Expand Down Expand Up @@ -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'
Expand All @@ -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'}`);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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');
}
Expand All @@ -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`;
Expand Down
Loading