diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49d57b9..70eb58a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,28 @@
# Changelog
-All notable changes to HiHTML are documented in this file, which is (mostly) AI-generated and (always) human-edited. Dependency updates may or may not be called out specifically.
+All notable changes to hihtml are documented in this file, which is (mostly) AI-generated and (always) human-edited. Dependency updates may or may not be called out specifically.
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.0-beta] - 2026-05-13
+
+### Added
+
+* Added string-based functions to programmatic API:
+ - `checkCodeString(content, options?)` validates an HTML string and checks it for deprecated markup, mirroring `checkCode` for string-based pipelines
+ - `checkLinksString(content, options?)` checks all external http/https URLs found in an HTML string, mirroring `checkLinks` for string-based pipelines
+ - `minifyString(content, options?)` minifies an HTML string and returns it, without any file I/O—useful in content-pipeline contexts such as Eleventy transforms, middleware, and SSR handlers
+* Extended URL extraction in link checking to also detect URLs in unquoted attributes (e.g., `href=https://example.com`, which is valid HTML)
+
+### Changed
+
+* Improved performance across several areas:
+ - Directory traversal now fans out subdirectories in parallel (`Promise.all`)
+ - `HtmlValidate` instances are cached per preset, avoiding re-initialization across calls to `validate()`/`checkCode()`
+ - URL-extraction regexes in the link checker are compiled once at module load instead of per-call; extraction now uses `matchAll`
+ - HTML Minifier Next import and preset resolution are cached per preset, avoiding repeated work across calls to `minifyString()`
+ - Ignore-list entries are pre-classified into hostnames (Set) and prefix entries once per `checkLinks()` call, enabling O(1) exact-hostname lookup in the hot path
+
## [1.2.0-beta] - 2026-05-11
### Added
@@ -11,7 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
* Added `validation.ignore`, a list of HTML-validate rule IDs to suppress, mirroring `links.ignore`
- Ignored messages appear in validation output (marked as ignored) but are not counted as errors and do not block minification when using `--all`/`-a`
- Supported in configuration (`.hihtml.json`/`package.json`) and programmatically via `checkCode(files, { ignore: […] })`
- - `ValidationResult` now includes `countIgnored`; `ValidationMessage` now includes `ignored?: boolean`
+ - `ResultCodeValidation` now includes `countIgnored`; `MessageValidation` now includes `ignored?: boolean`
* Added `-s`/`--settings ` flag to load configuration from a specific JSON file, overriding the default CWD config lookup
- Accepts any JSON file, reading the `"hihtml"` key if present (same convention as `package.json`), otherwise using the root object
- `loadConfig()` now accepts an optional `filePath` parameter for the same behavior programmatically
diff --git a/README.md b/README.md
index 36a9038..8c6ae46 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
-# HiHTML, the HTML Processing Supertool (Beta)
+# hihtml, the HTML Processing Supertool (Beta)
[](https://www.npmjs.com/package/hihtml) [](https://github.com/j9t/hihtml/actions) [](https://socket.dev/npm/package/hihtml) [](https://github.com/j9t/hihtml?sponsor=1)
-HiHTML—“High Quality HTML”—bundles key HTML tools into one, making HTML validation and semantics control, link checking, and minification as easy as it gets: [HTML-validate](https://html-validate.org/) for validation, [ObsoHTML](https://github.com/j9t/obsohtml) for deprecated markup detection, Node’s built-in `http`/`https` for link checking, and [HTML Minifier Next](https://github.com/j9t/html-minifier-next) for minification. HiHTML provides a CLI and a programmatic API, and comes with strong defaults but is still highly configurable.
+hihtml—“high-quality HTML”—bundles several key HTML tools into one, making HTML validation and semantics control, link checking, and minification as easy as it gets: [HTML-validate](https://html-validate.org/) for validation, [ObsoHTML](https://github.com/j9t/obsohtml) for deprecated markup detection, Node’s built-in `http`/`https` for link checking, and [HTML Minifier Next](https://github.com/j9t/html-minifier-next) for minification. hihtml provides a CLI and a programmatic API, and comes with strong defaults but is still highly configurable.
## Usage
@@ -14,11 +14,11 @@ HiHTML—“High Quality HTML”—bundles key HTML tools into one, making HTML
npm i hihtml
```
-Recommended: Just run HiHTML via `npx hihtml`.
+Recommended: Just run hihtml via `npx hihtml`.
#### Execution
-Without options, HiHTML validates HTML files and checks for deprecated markup in the current directory. Use flags to control behavior:
+Without options, hihtml validates HTML files and checks for deprecated markup in the current directory. Use flags to control behavior:
| Flag | Description |
|---|---|
@@ -103,7 +103,7 @@ npx hihtml -q -a -i src -o dist
### 2. Programmatic API
```js
-import { checkCode, checkLinks, minify, collect } from 'hihtml';
+import { checkCode, checkCodeString, checkLinks, checkLinksString, minify, minifyString, collect } from 'hihtml';
const files = await collect('./src');
@@ -115,6 +115,11 @@ const links = await checkLinks(files);
const minification = await minify(files, files); // in-place
// { files: [{ path, sizeOriginal, sizeMinified }], saved }
+
+// String variants—same result types, no file I/O
+const minified = await minifyString('Hello world
');
+const codeGate = await checkCodeString('Nope
');
+const linksCleaned = await checkLinksString('Example');
```
#### `collect(dir, extensions?, excludedDirs?)`
@@ -126,37 +131,68 @@ Recursively collects HTML files from `dir`. Returns `Promise`.
#### `checkCode(filePaths, options?)`
-Validates HTML files and checks for deprecated markup. Returns `Promise` with `validation` (HTML-validate result) and `deprecation` (ObsoHTML result) properties.
+Validates HTML files and checks for deprecated markup. Returns `Promise` with `validation` (HTML-validate result) and `deprecation` (ObsoHTML result) properties.
* `options.preset`: HTML-validate preset name (default: `'standard'`)
* `options.ignore`: List of [HTML-validate rule IDs](https://html-validate.org/rules/index.html) to suppress (default: `[]`)
+#### `checkCodeString(content, options?)`
+
+Validates an HTML string and checks for deprecated markup. Returns `Promise`—same shape as `checkCode`. Useful in content-pipeline contexts (Eleventy transforms, middleware, SSR) where HTML is available as a string rather than a file.
+
+* `options.preset`: HTML-validate preset name (default: `'standard'`)
+* `options.ignore`: List of HTML-validate rule IDs to suppress (default: `[]`)
+
+Note: `result.validation.files[0].path` and `result.deprecation.files[0].path` will be `'(string input)'`, not a real file path.
+
#### `checkLinks(filePaths, options?)`
-Checks all external http/https URLs (`href`, `src`, `srcset`, `action` attributes) found in the given HTML files. Each unique URL is checked once; results are mapped back to every file it appears in. Returns `Promise`.
+Checks all external http/https URLs (`href`, `src`, `srcset`, `action` attributes) found in the given HTML files. Each unique URL is checked once; results are mapped back to every file it appears in. Returns `Promise`.
* `options.timeout`: Request timeout in milliseconds (default: `10000`)
* `options.concurrency`: Maximum concurrent requests (default: `8`)
* `options.warnOnPermanentRedirects`: Warn on 301/308 permanent redirects (default: `false`)
* `options.ignore`: List of hostnames or URL prefixes to skip (default: `[]`)
+* `options.onStart`: Called once with the total number of URLs to check
+* `options.onProgress`: Called after each URL is checked
Links are checked via HEAD request, falling back to GET on 405. 4xx and 5xx responses are reported as broken. Skipped URLs (from the ignore list) appear in results with `skipped: true` and are never counted as broken.
+#### `checkLinksString(content, options?)`
+
+Checks all external http/https URLs found in an HTML string. Returns `Promise`—same shape as `checkLinks`. Useful when HTML is available as a string rather than a file, e.g., to check links in a fetched document or API response.
+
+* `options.timeout`: Request timeout in milliseconds (default: `10000`)
+* `options.concurrency`: Maximum concurrent requests (default: `8`)
+* `options.warnOnPermanentRedirects`: Warn on 301/308 permanent redirects (default: `false`)
+* `options.ignore`: List of hostnames or URL prefixes to skip (default: `[]`)
+* `options.onStart`: Called once with the total number of URLs to check
+* `options.onProgress`: Called after each URL is checked
+
+Note: `result.files[0].path` will be `'(string input)'`, not a real file path. `result.countFileErrors` will always be `0`.
+
#### `minify(filePaths, outputPaths, options?)`
-Minifies HTML files using HTML Minifier Next. Returns `Promise`.
+Minifies HTML files using HTML Minifier Next. Returns `Promise`.
* `outputPaths`: Parallel array of output paths; pass the same value as `filePaths` for in-place minification
* `options.preset`: HTML Minifier Next preset name (default: `'comprehensive'`)
* `options.options`: Additional HTML Minifier Next options to merge with the preset
+#### `minifyString(content, options?)`
+
+Minifies an HTML string using HTML Minifier Next. Returns `Promise`. Useful in content-pipeline contexts (Eleventy transforms, middleware, SSR) where HTML is available as a string rather than a file.
+
+* `options.preset`: HTML Minifier Next preset name (default: `'comprehensive'`)
+* `options.options`: Additional HTML Minifier Next options to merge with the preset
+
#### `loadConfig(cwd?, filePath?)`
-Loads HiHTML configuration. When `filePath` is given, only that file is read (no CWD fallback); if it contains a `"hihtml"` key that value is used, otherwise the root object is used. Without `filePath`, reads `.hihtml.json` or the `"hihtml"` key in `package.json` from `cwd`. Returns `Promise`.
+Loads hihtml configuration. When `filePath` is given, only that file is read (no CWD fallback); if it contains a `"hihtml"` key that value is used, otherwise the root object is used. Without `filePath`, reads `.hihtml.json` or the `"hihtml"` key in `package.json` from `cwd`. Returns `Promise`.
## Configuration
-Create a .hihtml.json file in your project root, or add a `"hihtml"` key to package.json. Both use the same format (here showing HiHTML’s defaults):
+Create a .hihtml.json file in your project root, or add a `"hihtml"` key to package.json. Both use the same format (here showing hihtml’s defaults):
```json
{
@@ -200,12 +236,12 @@ If in doubt or in a hurry, [report issues here](https://github.com/j9t/hihtml/is
### What does ObsoHTML do here when HTML-validate already reports on deprecated markup?
-At the moment, ObsoHTML catches some elements and attributes that HTML-validate doesn’t. Once HTML-validate covers everything ObsoHTML covers, ObsoHTML is going to be removed from HiHTML. Note that ObsoHTML is purely informational—it doesn’t prevent minification when used with the `--all`/`-a` flag.
+At the moment, ObsoHTML catches some elements and attributes that HTML-validate doesn’t. Once HTML-validate covers everything ObsoHTML covers, ObsoHTML is going to be removed from hihtml. Note that ObsoHTML is purely informational—it doesn’t prevent minification when used with the `--all`/`-a` flag.
***
You might like some of my other work:
-* Optimization tools: HiHTML (including [HTML Minifier Next](https://github.com/j9t/html-minifier-next) + [ObsoHTML](https://github.com/j9t/obsohtml)) · [Image Guard](https://github.com/j9t/image-guard) · [Compressor.js Next](https://github.com/j9t/compressorjs-next) · [.htaccess Punk](https://github.com/j9t/htaccess-punk)
+* Optimization tools: hihtml (including [HTML Minifier Next](https://github.com/j9t/html-minifier-next) + [ObsoHTML](https://github.com/j9t/obsohtml)) · [Image Guard](https://github.com/j9t/image-guard) · [Compressor.js Next](https://github.com/j9t/compressorjs-next) · [.htaccess Punk](https://github.com/j9t/htaccess-punk)
* Defense tools: [IA Defensa](https://iadefensa.com/solutions/)
* Resources for quality web development: [Articles](https://meiert.com/topics/development/) · [Books](https://meiert.com/topics/books/) (including [_On Web Development_](https://meiert.com/blog/on-web-development-2/)) · [News](https://frontenddogma.com/) · [Terminology](https://webglossary.info/)
\ No newline at end of file
diff --git a/SECURITY.md b/SECURITY.md
index e5bfae8..c95f375 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,7 +2,7 @@
## Supported Versions
-Only the latest and therefore current version of HiHTML is supported. It’s advised to update older versions to the latest version.
+Only the latest and therefore current version of hihtml is supported. It’s advised to update older versions to the latest version.
## Reporting a Vulnerability
diff --git a/bin/hihtml.js b/bin/hihtml.js
old mode 100644
new mode 100755
index 6cf4fb8..1444bb2
--- a/bin/hihtml.js
+++ b/bin/hihtml.js
@@ -267,4 +267,4 @@ async function saveReport(report, fileOpt) {
const reportPath = typeof fileOpt === 'string' ? fileOpt : 'hihtml-report.json';
await fs.promises.writeFile(reportPath, JSON.stringify(report, null, 2), 'utf8');
console.log(`\nReport saved to ${styleText('bold', reportPath)}`);
-}
+}
\ No newline at end of file
diff --git a/bin/hihtml.test.js b/bin/hihtml.test.js
index 772540a..d03ec90 100644
--- a/bin/hihtml.test.js
+++ b/bin/hihtml.test.js
@@ -8,9 +8,9 @@ import assert from 'node:assert';
import { stripVTControlCharacters } from 'node:util';
import { validate } from '../src/adapters/validate.js';
-import { checkCode } from '../src/adapters/check-code.js';
-import { checkLinks } from '../src/adapters/check-links.js';
-import { minify } from '../src/adapters/minify.js';
+import { checkCode, checkCodeString } from '../src/adapters/check-code.js';
+import { checkLinks, checkLinksString } from '../src/adapters/check-links.js';
+import { minify, minifyString } from '../src/adapters/minify.js';
import { collect, read } from '../src/lib/files.js';
import { loadConfig } from '../src/lib/config.js';
@@ -33,9 +33,9 @@ function run(args, stdinInput = '', cwd = undefined) {
// Fixtures
-const CLEAN_HTML = 'TestHello.
';
-const DEPRECATED_HTML = 'TestOld';
-const INVALID_HTML = 'TestBad nesting.
';
+const HTML_CLEAN = 'TestYes
';
+const HTML_DEPRECATED = 'TestNot anymore';
+const HTML_INVALID = 'TestNo
';
/** @type {http.Server} */
let testServer;
@@ -46,9 +46,9 @@ let testServerBase;
before(async () => {
fs.mkdirSync(tempDir, { recursive: true });
- fs.writeFileSync(path.join(tempDir, 'clean.html'), CLEAN_HTML);
- fs.writeFileSync(path.join(tempDir, 'deprecated.html'), DEPRECATED_HTML);
- fs.writeFileSync(path.join(tempDir, 'invalid.html'), INVALID_HTML);
+ fs.writeFileSync(path.join(tempDir, 'clean.html'), HTML_CLEAN);
+ fs.writeFileSync(path.join(tempDir, 'deprecated.html'), HTML_DEPRECATED);
+ fs.writeFileSync(path.join(tempDir, 'invalid.html'), HTML_INVALID);
testServer = await new Promise(resolve => {
const server = http.createServer((req, res) => {
@@ -682,6 +682,43 @@ describe('Check code', () => {
});
});
+// Programmatic API: `checkCodeString`
+
+describe('Check code string', () => {
+ test('Returns expected result shape', async () => {
+ const result = await checkCodeString(HTML_CLEAN);
+ assert.ok('validation' in result);
+ assert.ok('deprecation' in result);
+ assert.ok('countErrors' in result.validation);
+ assert.ok('countIssues' in result.deprecation);
+ });
+
+ test('Clean HTML reports no issues', async () => {
+ const result = await checkCodeString(HTML_CLEAN);
+ assert.strictEqual(result.validation.countErrors, 0);
+ assert.strictEqual(result.deprecation.countIssues, 0);
+ });
+
+ test('Detects deprecated markup', async () => {
+ const result = await checkCodeString(HTML_DEPRECATED);
+ assert.ok(result.deprecation.countIssues > 0);
+ assert.ok(result.deprecation.files[0].elements.includes('center'));
+ });
+
+ test('Detects validation errors', async () => {
+ const result = await checkCodeString(HTML_INVALID);
+ assert.ok(result.validation.countErrors > 0);
+ });
+
+ test('Passes ignore list through to validation result', async () => {
+ const base = await checkCodeString(HTML_INVALID);
+ const ruleIds = [...new Set(base.validation.files[0].messages.map(m => m.ruleId))];
+ const result = await checkCodeString(HTML_INVALID, { ignore: ruleIds });
+ assert.strictEqual(result.validation.countErrors, 0);
+ assert.strictEqual(result.validation.countIgnored, base.validation.files[0].messages.length);
+ });
+});
+
// Programmatic API: `checkLinks`
describe('Check links', () => {
@@ -804,7 +841,7 @@ describe('Check links', () => {
assert.strictEqual(result.countSkipped, 1);
});
- test('Ignored URLs are not counted as broken', async () => {
+ test('Does not count ignored URLs as broken', async () => {
const result = await checkLinks([path.join(tempDir, 'links_mixed.html')], {
ignore: ['127.0.0.1'],
});
@@ -827,6 +864,116 @@ describe('Check links', () => {
});
});
+// Programmatic API: `checkLinksString`
+
+describe('Check links string', () => {
+ test('Returns expected result shape', async () => {
+ const result = await checkLinksString(`TOK`);
+ assert.ok('files' in result);
+ assert.ok('countBroken' in result);
+ assert.ok('countChecked' in result);
+ assert.ok(Array.isArray(result.files));
+ });
+
+ test('Reports ok for 200 response', async () => {
+ const result = await checkLinksString(`TOK`);
+ assert.strictEqual(result.countBroken, 0);
+ assert.strictEqual(result.countChecked, 1);
+ assert.strictEqual(result.files[0].links[0].ok, true);
+ });
+
+ test('Reports broken for 404 response', async () => {
+ const result = await checkLinksString(`TBroken`);
+ assert.strictEqual(result.countBroken, 1);
+ assert.strictEqual(result.files[0].links[0].ok, false);
+ });
+
+ test('No http/https links returns empty result', async () => {
+ const result = await checkLinksString(HTML_CLEAN);
+ assert.strictEqual(result.countBroken, 0);
+ assert.strictEqual(result.countChecked, 0);
+ assert.strictEqual(result.files[0].links.length, 0);
+ });
+});
+
+// Programmatic API: URL extraction (attributes and quote styles)
+
+describe('URL extraction', () => {
+ const ok = () => `${testServerBase}/ok`;
+ const found = async (html) => {
+ const r = await checkLinksString(html);
+ return { checked: r.countChecked, broken: r.countBroken };
+ };
+
+ test('`href` double-quoted', async () => {
+ assert.deepStrictEqual(await found(`L`), { checked: 1, broken: 0 });
+ });
+
+ test('`href` single-quoted', async () => {
+ assert.deepStrictEqual(await found(`L`), { checked: 1, broken: 0 });
+ });
+
+ test('`href` unquoted', async () => {
+ assert.deepStrictEqual(await found(`L`), { checked: 1, broken: 0 });
+ });
+
+ test('`src` double-quoted', async () => {
+ assert.deepStrictEqual(await found(`
`), { checked: 1, broken: 0 });
+ });
+
+ test('`src` single-quoted', async () => {
+ assert.deepStrictEqual(await found(`
`), { checked: 1, broken: 0 });
+ });
+
+ test('`src` unquoted', async () => {
+ assert.deepStrictEqual(await found(`
`), { checked: 1, broken: 0 });
+ });
+
+ test('`action` double-quoted', async () => {
+ assert.deepStrictEqual(await found(``), { checked: 1, broken: 0 });
+ });
+
+ test('`action` single-quoted', async () => {
+ assert.deepStrictEqual(await found(``), { checked: 1, broken: 0 });
+ });
+
+ test('`action` unquoted', async () => {
+ assert.deepStrictEqual(await found(``), { checked: 1, broken: 0 });
+ });
+
+ test('`srcset` double-quoted', async () => {
+ assert.deepStrictEqual(await found(`
`), { checked: 1, broken: 0 });
+ });
+
+ test('`srcset` single-quoted', async () => {
+ assert.deepStrictEqual(await found(`
`), { checked: 1, broken: 0 });
+ });
+
+ test('`href` with spaces around `=`', async () => {
+ assert.deepStrictEqual(await found(`link`), { checked: 1, broken: 0 });
+ });
+
+ test('`srcset` with spaces around `=`', async () => {
+ assert.deepStrictEqual(await found(`
`), { checked: 1, broken: 0 });
+ });
+
+ test('Does not check URLs inside HTML comments', async () => {
+ assert.deepStrictEqual(await found(``), { checked: 0, broken: 0 });
+ });
+
+ test('Does not check URLs inside ``), { checked: 0, broken: 0 });
+ });
+
+ test('Still checks ``), { checked: 1, broken: 0 });
+ });
+
+ test('Does not check URLs inside ``), { checked: 0, broken: 0 });
+ });
+});
+
// Programmatic API: `minify`
describe('Minify files', () => {
@@ -902,6 +1049,31 @@ describe('Minify files', () => {
});
});
+// Programmatic API: `minifyString`
+
+describe('Minify string', () => {
+ test('Returns a string', async () => {
+ const result = await minifyString(HTML_CLEAN);
+ assert.strictEqual(typeof result, 'string');
+ });
+
+ test('Output is not larger than input', async () => {
+ const result = await minifyString(HTML_CLEAN);
+ assert.ok(Buffer.byteLength(result) <= Buffer.byteLength(HTML_CLEAN));
+ });
+
+ test('Collapses whitespace with default preset', async () => {
+ const result = await minifyString('T Hello world
');
+ assert.ok(!result.includes(' Hello'));
+ });
+
+ test('Respects options override', async () => {
+ const loose = 'T Hello world
';
+ const result = await minifyString(loose, { options: { collapseWhitespace: false } });
+ assert.ok(result.includes(' Hello'));
+ });
+});
+
// Programmatic API: `read`
describe('Read files', () => {
@@ -911,7 +1083,7 @@ describe('Read files', () => {
const result = await read([fileClean]);
assert.ok(result instanceof Map);
assert.ok(result.has(fileClean));
- assert.ok(result.get(fileClean).includes('Hello'));
+ assert.ok(result.get(fileClean).includes('Yes'));
});
test('Skips unreadable files gracefully', async () => {
diff --git a/package-lock.json b/package-lock.json
index 0c89197..22e7303 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "hihtml",
- "version": "1.2.0-beta",
+ "version": "1.3.0-beta",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hihtml",
- "version": "1.2.0-beta",
+ "version": "1.3.0-beta",
"license": "MIT",
"dependencies": {
"commander": "^14.0.3",
diff --git a/package.json b/package.json
index af6b183..2df0313 100644
--- a/package.json
+++ b/package.json
@@ -26,13 +26,19 @@
"funding": "https://github.com/j9t/hihtml?sponsor=1",
"homepage": "https://github.com/j9t/hihtml",
"keywords": [
+ "conformance",
"html",
- "minify",
+ "html-minifier",
+ "html-minifier-next",
+ "html-validate",
+ "link-check",
+ "links",
"minification",
+ "minifier",
+ "minify",
+ "quality",
"validate",
- "validation",
- "qa",
- "qc"
+ "validation"
],
"license": "MIT",
"name": "hihtml",
@@ -49,5 +55,5 @@
},
"type": "module",
"types": "src/index.d.ts",
- "version": "1.2.0-beta"
+ "version": "1.3.0-beta"
}
diff --git a/src/adapters/check-code.js b/src/adapters/check-code.js
index 5e05e05..790be77 100644
--- a/src/adapters/check-code.js
+++ b/src/adapters/check-code.js
@@ -5,7 +5,7 @@ import { validate } from './validate.js';
import { read } from '../lib/files.js';
/**
- * @typedef {Object} FileDeprecationResult
+ * @typedef {Object} ResultCodeDeprecationFile
* @property {string} path
* @property {string[]} elements
* @property {string[]} attributes
@@ -13,21 +13,21 @@ import { read } from '../lib/files.js';
*/
/**
- * @typedef {Object} DeprecationResult
- * @property {FileDeprecationResult[]} files
+ * @typedef {Object} ResultCodeDeprecation
+ * @property {ResultCodeDeprecationFile[]} files
* @property {number} countIssues
*/
/**
- * @typedef {Object} CheckResult
- * @property {import('./validate.js').ValidationResult} validation
- * @property {DeprecationResult} deprecation
+ * @typedef {Object} ResultCode
+ * @property {import('./validate.js').ResultCodeValidation} validation
+ * @property {ResultCodeDeprecation} deprecation
*/
/**
* @param {string[]} filePaths
* @param {{ concurrency?: number, contents?: Map }} [options]
- * @returns {Promise}
+ * @returns {Promise}
*/
async function checkDeprecated(filePaths, { concurrency = DEFAULT_CONCURRENCY, contents } = {}) {
const files = await runWithConcurrency(filePaths, concurrency, async (filePath) => {
@@ -37,7 +37,7 @@ async function checkDeprecated(filePaths, { concurrency = DEFAULT_CONCURRENCY, c
try {
content = await fs.promises.readFile(filePath, 'utf8');
} catch (err) {
- return /** @type {FileDeprecationResult} */ ({ path: filePath, elements: [], attributes: [], error: err instanceof Error ? err.message : String(err) });
+ return /** @type {ResultCodeDeprecationFile} */ ({ path: filePath, elements: [], attributes: [], error: err instanceof Error ? err.message : String(err) });
}
}
@@ -48,7 +48,7 @@ async function checkDeprecated(filePaths, { concurrency = DEFAULT_CONCURRENCY, c
throw new Error(`ObsoHTML API error—the package may have breaking changes: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
}
- return /** @type {FileDeprecationResult} */ ({ path: filePath, elements: result.elements, attributes: result.attributes });
+ return /** @type {ResultCodeDeprecationFile} */ ({ path: filePath, elements: result.elements, attributes: result.attributes });
});
const countIssues = files.reduce((acc, f) => acc + f.elements.length + f.attributes.length, 0);
@@ -59,7 +59,7 @@ async function checkDeprecated(filePaths, { concurrency = DEFAULT_CONCURRENCY, c
* Validate HTML files and check for deprecated markup.
* @param {string[]} filePaths
* @param {{ preset?: string, ignore?: string[], concurrency?: number, contents?: Map, onProgress?: () => void }} [options]
- * @returns {Promise}
+ * @returns {Promise}
*/
export async function checkCode(filePaths, { preset = 'standard', ignore = [], concurrency = DEFAULT_CONCURRENCY, contents, onProgress } = {}) {
const resolvedContents = contents ?? await read(filePaths, { concurrency });
@@ -69,3 +69,15 @@ export async function checkCode(filePaths, { preset = 'standard', ignore = [], c
]);
return { validation: validateResult, deprecation: deprecatedResult };
}
+
+const SYNTHETIC_PATH = '(string input)';
+
+/**
+ * Validate an HTML string and check for deprecated markup.
+ * @param {string} content
+ * @param {{ preset?: string, ignore?: string[] }} [options]
+ * @returns {Promise}
+ */
+export async function checkCodeString(content, { preset = 'standard', ignore = [] } = {}) {
+ return checkCode([SYNTHETIC_PATH], { preset, ignore, contents: new Map([[SYNTHETIC_PATH, content]]) });
+}
\ No newline at end of file
diff --git a/src/adapters/check-links.js b/src/adapters/check-links.js
index 5888884..6e825cc 100644
--- a/src/adapters/check-links.js
+++ b/src/adapters/check-links.js
@@ -12,8 +12,11 @@ export const DEFAULT_LINK_TIMEOUT = 10_000;
const USER_AGENT = `hihtml/${version} link-checker`;
+const RE_ATTR = /\b(?:href|src|action)\s*=\s*(?:"(https?:\/\/[^"\s>]+)"|'(https?:\/\/[^'\s>]+)'|(https?:\/\/[^\s"'`=<>]+))/gi;
+const RE_SRCSET = /\bsrcset\s*=\s*(?:"([^"]+)"|'([^']+)')/gi;
+
/**
- * @typedef {Object} LinkResult
+ * @typedef {Object} ResultLinksUrl
* @property {string} url
* @property {number|null} status
* @property {boolean} ok
@@ -24,16 +27,16 @@ const USER_AGENT = `hihtml/${version} link-checker`;
*/
/**
- * @typedef {Object} FileLinkResult
+ * @typedef {Object} ResultLinksFile
* @property {string} path
- * @property {LinkResult[]} links
+ * @property {ResultLinksUrl[]} links
* @property {number} countBroken
* @property {string} [error]
*/
/**
- * @typedef {Object} LinkCheckResult
- * @property {FileLinkResult[]} files
+ * @typedef {Object} ResultLinks
+ * @property {ResultLinksFile[]} files
* @property {number} countBroken
* @property {number} countChecked
* @property {number} countSkipped
@@ -47,17 +50,19 @@ const USER_AGENT = `hihtml/${version} link-checker`;
*/
function extractUrls(content) {
const urls = new Set();
- let m;
- const attrRe = /\b(?:href|src|action)=(?:"(https?:\/\/[^"\s>]+)"|'(https?:\/\/[^'\s>]+)')/gi;
- while ((m = attrRe.exec(content)) !== null) {
- const rawUrl = m[1] ?? m[2];
+ const stripped = content
+ .replace(//g, '')
+ .replace(/(')
+ .replace(/(');
+
+ for (const m of stripped.matchAll(RE_ATTR)) {
+ const rawUrl = m[1] ?? m[2] ?? m[3];
try { urls.add(new URL(rawUrl).href.split('#')[0]); } catch { /* skip malformed URLs */ }
}
- const srcsetRe = /\bsrcset=["']([^"']+)["']/gi;
- while ((m = srcsetRe.exec(content)) !== null) {
- for (const entry of m[1].split(',')) {
+ for (const m of stripped.matchAll(RE_SRCSET)) {
+ for (const entry of (m[1] ?? m[2]).split(',')) {
const candidate = entry.trim().split(/\s+/)[0];
if (candidate.startsWith('http://') || candidate.startsWith('https://')) {
try { urls.add(new URL(candidate).href.split('#')[0]); } catch { /* skip malformed URLs */ }
@@ -106,7 +111,7 @@ function requestSingle(url, method, timeout) {
* Falls back from HEAD to GET on 405. Optionally warns on permanent redirects.
* @param {string} url
* @param {{ timeout?: number, warnOnPermanentRedirects?: boolean }} [options]
- * @returns {Promise}
+ * @returns {Promise}
*/
async function checkUrl(url, { timeout = DEFAULT_LINK_TIMEOUT, warnOnPermanentRedirects = false } = {}) {
let currentUrl = url;
@@ -164,23 +169,43 @@ async function checkUrl(url, { timeout = DEFAULT_LINK_TIMEOUT, warnOnPermanentRe
}
/**
- * Returns true if the URL matches any entry in the ignore list.
- * Entries without a path component are matched by hostname (exact or subdomain).
- * Entries containing a slash are matched as URL prefixes.
- * @param {string} url
+ * @typedef {{ hostnames: Set, prefixes: string[] }} IgnoreList
+ */
+
+/**
+ * Pre-process an ignore list into hostname entries (Set for O(1) lookup) and prefix entries.
+ * Entries containing a slash are treated as URL prefixes; others as hostnames (exact or subdomain).
* @param {string[]} ignore
+ * @returns {IgnoreList}
+ */
+function buildIgnoreList(ignore) {
+ const normalized = ignore.map(e => e.trim().toLowerCase());
+ return {
+ hostnames: new Set(normalized.filter(e => !e.includes('/'))),
+ prefixes: normalized.filter(e => e.includes('/')).map(e => e.replace(/\/+$/, '')),
+ };
+}
+
+/**
+ * Returns true if the URL matches any entry in the pre-processed ignore list.
+ * @param {string} url
+ * @param {IgnoreList} ignoreList
* @returns {boolean}
*/
-function isIgnored(url, ignore) {
- if (ignore.length === 0) return false;
- let hostname;
- try { hostname = new URL(url).hostname; } catch { return false; }
- for (const entry of ignore) {
- if (entry.includes('/')) {
- if (url.startsWith(entry)) return true;
- } else {
- if (hostname === entry || hostname.endsWith(`.${entry}`)) return true;
- }
+function isIgnored(url, { hostnames, prefixes }) {
+ if (hostnames.size === 0 && prefixes.length === 0) return false;
+ let parsed;
+ try { parsed = new URL(url); } catch { return false; }
+ for (const prefix of prefixes) {
+ const target = prefix.startsWith('/')
+ ? parsed.pathname.toLowerCase()
+ : (parsed.origin + parsed.pathname).toLowerCase();
+ if (target === prefix || target.startsWith(prefix + '/')) return true;
+ }
+ const { hostname } = parsed;
+ if (hostnames.has(hostname)) return true;
+ for (const h of hostnames) {
+ if (hostname.endsWith(`.${h}`)) return true;
}
return false;
}
@@ -198,7 +223,7 @@ function isIgnored(url, ignore) {
* onProgress?: () => void,
* onStart?: (total: number) => void,
* }} [options]
- * @returns {Promise}
+ * @returns {Promise}
*/
export async function checkLinks(filePaths, {
concurrency = DEFAULT_LINK_CONCURRENCY,
@@ -230,16 +255,17 @@ export async function checkLinks(filePaths, {
for (const url of urls) allUrls.add(url);
}
+ const ignoreList = buildIgnoreList(ignore);
const toCheck = new Set();
const toSkip = new Set();
for (const url of allUrls) {
- if (isIgnored(url, ignore)) toSkip.add(url);
+ if (isIgnored(url, ignoreList)) toSkip.add(url);
else toCheck.add(url);
}
onStart?.(toCheck.size);
- /** @type {Map} */
+ /** @type {Map} */
const urlResults = new Map();
for (const url of toSkip) {
@@ -253,11 +279,11 @@ export async function checkLinks(filePaths, {
const files = filePaths.map(filePath => {
const data = fileData.get(filePath) ?? { urls: [], error: 'Unknown error' };
- if (data.error) return /** @type {FileLinkResult} */ ({ path: filePath, links: [], countBroken: 0, error: data.error });
+ if (data.error) return /** @type {ResultLinksFile} */ ({ path: filePath, links: [], countBroken: 0, error: data.error });
- const links = data.urls.map(url => /** @type {LinkResult} */ ({ ...urlResults.get(url), url }));
+ const links = data.urls.map(url => /** @type {ResultLinksUrl} */ ({ ...urlResults.get(url), url }));
const countBroken = links.filter(l => !l.ok).length;
- return /** @type {FileLinkResult} */ ({ path: filePath, links, countBroken });
+ return /** @type {ResultLinksFile} */ ({ path: filePath, links, countBroken });
});
const countBroken = [...urlResults.values()].filter(r => !r.ok).length;
@@ -265,3 +291,22 @@ export async function checkLinks(filePaths, {
const countFileErrors = files.filter(f => f.error !== undefined).length;
return { files, countBroken, countChecked: toCheck.size, countSkipped, countFileErrors };
}
+
+const SYNTHETIC_PATH = '(string input)';
+
+/**
+ * Check all external http/https URLs found in an HTML string.
+ * @param {string} content
+ * @param {{
+ * concurrency?: number,
+ * timeout?: number,
+ * warnOnPermanentRedirects?: boolean,
+ * ignore?: string[],
+ * onProgress?: () => void,
+ * onStart?: (total: number) => void,
+ * }} [options]
+ * @returns {Promise}
+ */
+export async function checkLinksString(content, options = {}) {
+ return checkLinks([SYNTHETIC_PATH], { ...options, contents: new Map([[SYNTHETIC_PATH, content]]) });
+}
\ No newline at end of file
diff --git a/src/adapters/minify.js b/src/adapters/minify.js
index 5f536bb..8303fa1 100644
--- a/src/adapters/minify.js
+++ b/src/adapters/minify.js
@@ -3,7 +3,7 @@ import path from 'node:path';
import { DEFAULT_CONCURRENCY, runWithConcurrency } from '../lib/concurrency.js';
/**
- * @typedef {Object} FileMinificationResult
+ * @typedef {Object} ResultMinificationFile
* @property {string} path
* @property {number} sizeOriginal
* @property {number} sizeMinified
@@ -11,34 +11,63 @@ import { DEFAULT_CONCURRENCY, runWithConcurrency } from '../lib/concurrency.js';
*/
/**
- * @typedef {Object} MinificationResult
- * @property {FileMinificationResult[]} files
+ * @typedef {Object} ResultMinification
+ * @property {ResultMinificationFile[]} files
* @property {number} saved
*/
+/** @type {Map }>>} */
+const minifierCache = new Map();
+
+/**
+ * Load HTML Minifier Next and resolve preset and extra options into a merged options object.
+ * The import and preset resolution are cached per preset name.
+ * @param {string} preset
+ * @param {Record} options
+ * @returns {Promise<{ htmlMinify: Function, resolvedOptions: Record }>}
+ */
+async function loadMinifier(preset, options) {
+ if (!minifierCache.has(preset)) {
+ minifierCache.set(preset, (async () => {
+ let htmlMinify, getPreset;
+ try {
+ ({ minify: htmlMinify, getPreset } = await import('html-minifier-next'));
+ } catch {
+ throw new Error('Could not load HTML Minifier Next. Ensure it is installed and check for breaking API changes.');
+ }
+ let presetOptions;
+ try {
+ presetOptions = /** @type {Record} */ (getPreset(preset) ?? {});
+ } catch (err) {
+ throw new Error(`HTML Minifier Next API error—the package may have breaking changes: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
+ }
+ return { htmlMinify, presetOptions };
+ })());
+ }
+ const { htmlMinify, presetOptions } = await /** @type {Promise<{ htmlMinify: Function, presetOptions: Record }>} */ (minifierCache.get(preset));
+ return { htmlMinify, resolvedOptions: { ...presetOptions, ...options } };
+}
+
+/**
+ * Minify an HTML string using HTML Minifier Next.
+ * @param {string} content
+ * @param {{ preset?: string, options?: Record }} [opts]
+ * @returns {Promise}
+ */
+export async function minifyString(content, { preset = 'comprehensive', options = {} } = {}) {
+ const { htmlMinify, resolvedOptions } = await loadMinifier(preset, options);
+ return htmlMinify(content, resolvedOptions);
+}
+
/**
* Minify HTML files using HTML Minifier Next.
* @param {string[]} filePaths - Input file paths
* @param {string[]} outputPaths - Output file paths (parallel to filePaths; same value = in-place)
* @param {{ preset?: string, options?: Record, concurrency?: number, contents?: Map, onProgress?: () => void }} [opts]
- * @returns {Promise}
+ * @returns {Promise}
*/
export async function minify(filePaths, outputPaths, { preset = 'comprehensive', options = {}, concurrency = DEFAULT_CONCURRENCY, contents, onProgress } = {}) {
- let htmlMinify, getPreset;
- try {
- ({ minify: htmlMinify, getPreset } = await import('html-minifier-next'));
- } catch {
- throw new Error('Could not load HTML Minifier Next. Ensure it is installed and check for breaking API changes.');
- }
-
- let presetOptions;
- try {
- presetOptions = getPreset(preset) ?? {};
- } catch (err) {
- throw new Error(`HTML Minifier Next API error—the package may have breaking changes: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
- }
-
- const resolvedOptions = { ...presetOptions, ...options };
+ const { htmlMinify, resolvedOptions } = await loadMinifier(preset, options);
if (outputPaths.length !== filePaths.length) {
throw new Error(`outputPaths length (${outputPaths.length}) must match filePaths length (${filePaths.length})`);
@@ -55,7 +84,7 @@ export async function minify(filePaths, outputPaths, { preset = 'comprehensive',
content = await fs.promises.readFile(filePath, 'utf8');
} catch (err) {
onProgress?.();
- return /** @type {FileMinificationResult} */ ({ path: filePath, sizeOriginal: 0, sizeMinified: 0, error: err instanceof Error ? err.message : String(err) });
+ return /** @type {ResultMinificationFile} */ ({ path: filePath, sizeOriginal: 0, sizeMinified: 0, error: err instanceof Error ? err.message : String(err) });
}
}
@@ -66,7 +95,7 @@ export async function minify(filePaths, outputPaths, { preset = 'comprehensive',
minified = await htmlMinify(content, resolvedOptions);
} catch (err) {
onProgress?.();
- return /** @type {FileMinificationResult} */ ({ path: filePath, sizeOriginal, sizeMinified: 0, error: `Minification error: ${err instanceof Error ? err.message : String(err)}` });
+ return /** @type {ResultMinificationFile} */ ({ path: filePath, sizeOriginal, sizeMinified: 0, error: `Minification error: ${err instanceof Error ? err.message : String(err)}` });
}
const sizeMinified = Buffer.byteLength(minified, 'utf8');
@@ -76,13 +105,13 @@ export async function minify(filePaths, outputPaths, { preset = 'comprehensive',
await fs.promises.writeFile(outputPath, minified, 'utf8');
} catch (err) {
onProgress?.();
- return /** @type {FileMinificationResult} */ ({ path: filePath, sizeOriginal, sizeMinified, error: `Write error: ${err instanceof Error ? err.message : String(err)}` });
+ return /** @type {ResultMinificationFile} */ ({ path: filePath, sizeOriginal, sizeMinified, error: `Write error: ${err instanceof Error ? err.message : String(err)}` });
}
onProgress?.();
- return /** @type {FileMinificationResult} */ ({ path: filePath, sizeOriginal, sizeMinified });
+ return /** @type {ResultMinificationFile} */ ({ path: filePath, sizeOriginal, sizeMinified });
});
const saved = files.reduce((acc, f) => f.error ? acc : acc + Math.max(0, (f.sizeOriginal || 0) - (f.sizeMinified || 0)), 0);
return { files, saved };
-}
+}
\ No newline at end of file
diff --git a/src/adapters/validate.js b/src/adapters/validate.js
index eb1bf7e..6ed922d 100644
--- a/src/adapters/validate.js
+++ b/src/adapters/validate.js
@@ -2,7 +2,7 @@ import fs from 'node:fs';
import { DEFAULT_CONCURRENCY, runWithConcurrency } from '../lib/concurrency.js';
/**
- * @typedef {Object} ValidationMessage
+ * @typedef {Object} MessageValidation
* @property {string} ruleId
* @property {1|2} severity - 1 = warning, 2 = error
* @property {string} message
@@ -12,40 +12,66 @@ import { DEFAULT_CONCURRENCY, runWithConcurrency } from '../lib/concurrency.js';
*/
/**
- * @typedef {Object} FileValidationResult
+ * @typedef {Object} ResultCodeValidationFile
* @property {string} path
- * @property {ValidationMessage[]} messages
+ * @property {MessageValidation[]} messages
*/
/**
- * @typedef {Object} ValidationResult
- * @property {FileValidationResult[]} files
+ * @typedef {Object} ResultCodeValidation
+ * @property {ResultCodeValidationFile[]} files
* @property {number} countErrors
* @property {number} countWarnings
* @property {number} countIgnored
*/
+// Intentionally unbounded: Keyed by preset name, and HTML-validate exposes a
+// fixed small set of presets, so this will never hold more than a handful of entries
+/** @type {Map>} */
+const validatorCache = new Map();
+
+/**
+ * Return a shared promise for a cached HtmlValidate instance for the given preset.
+ * Caching the promise rather than the resolved value means concurrent callers
+ * share a single initialization rather than each racing past the cache check.
+ * @param {string} preset
+ * @returns {Promise}
+ */
+function getValidator(preset) {
+ if (validatorCache.has(preset)) return /** @type {Promise} */ (validatorCache.get(preset));
+
+ const promise = (async () => {
+ let HtmlValidate;
+ try {
+ ({ HtmlValidate } = await import('html-validate'));
+ } catch {
+ throw new Error('Could not load HTML-validate. Ensure it is installed and check for breaking API changes.');
+ }
+
+ let validator;
+ try {
+ validator = new HtmlValidate({ extends: [`html-validate:${preset}`] });
+ } catch (err) {
+ throw new Error(`HTML-validate initialization failed—the package may have breaking changes: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
+ }
+
+ return validator;
+ })();
+
+ promise.catch(() => validatorCache.delete(preset));
+ validatorCache.set(preset, promise);
+ return promise;
+}
+
/**
* Validate HTML files using HTML-validate.
* @param {string[]} filePaths
* @param {{ preset?: string, ignore?: string[], concurrency?: number, contents?: Map, onProgress?: () => void }} [options]
- * @returns {Promise}
+ * @returns {Promise}
*/
export async function validate(filePaths, { preset = 'standard', ignore = [], concurrency = DEFAULT_CONCURRENCY, contents, onProgress } = {}) {
const ignoreSet = new Set(Array.isArray(ignore) ? ignore.map(String) : []);
- let HtmlValidate;
- try {
- ({ HtmlValidate } = await import('html-validate'));
- } catch {
- throw new Error('Could not load HTML-validate. Ensure it is installed and check for breaking API changes.');
- }
-
- let validator;
- try {
- validator = new HtmlValidate({ extends: [`html-validate:${preset}`] });
- } catch (err) {
- throw new Error(`HTML-validate initialization failed—the package may have breaking changes: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
- }
+ const validator = await getValidator(preset);
const files = await runWithConcurrency(filePaths, concurrency, async (filePath) => {
let content = contents?.get(filePath);
@@ -55,7 +81,7 @@ export async function validate(filePaths, { preset = 'standard', ignore = [], co
content = await fs.promises.readFile(filePath, 'utf8');
} catch (err) {
onProgress?.();
- return /** @type {FileValidationResult} */ ({ path: filePath, messages: [{ ruleId: 'io-error', severity: /** @type {2} */ (2), message: err instanceof Error ? err.message : String(err), line: 0, col: 0 }] });
+ return /** @type {ResultCodeValidationFile} */ ({ path: filePath, messages: [{ ruleId: 'io-error', severity: /** @type {2} */ (2), message: err instanceof Error ? err.message : String(err), line: 0, col: 0 }] });
}
}
@@ -67,7 +93,7 @@ export async function validate(filePaths, { preset = 'standard', ignore = [], co
}
const raw = report?.results?.[0]?.messages ?? [];
- /** @type {ValidationMessage[]} */
+ /** @type {MessageValidation[]} */
const messages = raw.map(m => {
const ruleId = String(m.ruleId ?? 'unknown');
return {
@@ -81,7 +107,7 @@ export async function validate(filePaths, { preset = 'standard', ignore = [], co
});
onProgress?.();
- return /** @type {FileValidationResult} */ ({ path: filePath, messages });
+ return /** @type {ResultCodeValidationFile} */ ({ path: filePath, messages });
});
const countErrors = files.reduce((acc, f) => acc + f.messages.filter(m => m.severity === 2 && !m.ignored).length, 0);
@@ -89,4 +115,4 @@ export async function validate(filePaths, { preset = 'standard', ignore = [], co
const countIgnored = files.reduce((acc, f) => acc + f.messages.filter(m => m.ignored).length, 0);
return { files, countErrors, countWarnings, countIgnored };
-}
+}
\ No newline at end of file
diff --git a/src/index.d.ts b/src/index.d.ts
index 5188ece..f612825 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -1,7 +1,7 @@
export declare const HTML_EXTENSIONS: Set;
export declare const EXCLUDED_DIRS: Set;
-export interface ValidationMessage {
+export interface MessageValidation {
ruleId: string;
severity: 1 | 2;
message: string;
@@ -10,36 +10,36 @@ export interface ValidationMessage {
ignored?: boolean;
}
-export interface FileValidationResult {
+export interface ResultCodeValidationFile {
path: string;
- messages: ValidationMessage[];
+ messages: MessageValidation[];
}
-export interface ValidationResult {
- files: FileValidationResult[];
+export interface ResultCodeValidation {
+ files: ResultCodeValidationFile[];
countErrors: number;
countWarnings: number;
countIgnored: number;
}
-export interface FileDeprecationResult {
+export interface ResultCodeDeprecationFile {
path: string;
elements: string[];
attributes: string[];
error?: string;
}
-export interface DeprecationResult {
- files: FileDeprecationResult[];
+export interface ResultCodeDeprecation {
+ files: ResultCodeDeprecationFile[];
countIssues: number;
}
-export interface CheckResult {
- validation: ValidationResult;
- deprecation: DeprecationResult;
+export interface ResultCode {
+ validation: ResultCodeValidation;
+ deprecation: ResultCodeDeprecation;
}
-export interface LinkResult {
+export interface ResultLinksUrl {
url: string;
status: number | null;
ok: boolean;
@@ -49,34 +49,34 @@ export interface LinkResult {
error?: string;
}
-export interface FileLinkResult {
+export interface ResultLinksFile {
path: string;
- links: LinkResult[];
+ links: ResultLinksUrl[];
countBroken: number;
error?: string;
}
-export interface LinkCheckResult {
- files: FileLinkResult[];
+export interface ResultLinks {
+ files: ResultLinksFile[];
countBroken: number;
countChecked: number;
countSkipped: number;
countFileErrors: number;
}
-export interface FileMinificationResult {
+export interface ResultMinificationFile {
path: string;
sizeOriginal: number;
sizeMinified: number;
error?: string;
}
-export interface MinificationResult {
- files: FileMinificationResult[];
+export interface ResultMinification {
+ files: ResultMinificationFile[];
saved: number;
}
-export interface HiHTMLConfig {
+export interface HihtmlConfig {
extensions?: string[];
ignore?: string[];
validation?: { preset?: string; ignore?: string[] };
@@ -100,12 +100,17 @@ export declare function read(
options?: { concurrency?: number; onProgress?: () => void }
): Promise