# core/adapters/tool.htmlhint.adapter.ts

> 107 lines of code and 29 definitions.

Tree: GovLab Quality
Language: typescript
Layer: infrastructure
Canonical: https://banes-lab.com/anatomy/quality#file-quality-core-adapters-tool-htmlhint-adapter-ts
Source text: https://banes-lab.com/source/quality/core/adapters/tool.htmlhint.adapter.ts.txt

Listed in [core/adapters](https://banes-lab.com/api/source/quality/core/adapters.md), after [core/adapters/tool.hadolint.adapter.ts](https://banes-lab.com/source/quality/core/adapters/tool.hadolint.adapter.ts.md) and before [core/adapters/tool.jscpd.adapter.ts](https://banes-lab.com/source/quality/core/adapters/tool.jscpd.adapter.ts.md).

## Definitions

- `walk` (lexical_declaration, line 67)
- `isVerify` (lexical_declaration, line 32)
- `isIgnored` (lexical_declaration, line 57)
- `loadVerify` (lexical_declaration, line 36)
- `govlabHtmlhintConfig` (lexical_declaration, line 46, exported)
- `entryTargets` (lexical_declaration, line 90)
- `runHtmlhint` (lexical_declaration, line 98)
- `SECTION` (lexical_declaration, line 18)
- `HTML_EXTENSION` (lexical_declaration, line 19)
- `WILDCARD` (lexical_declaration, line 20)
- `HtmlhintMessage` (interface_declaration, line 22)
- `HtmlhintVerify` (type_alias_declaration, line 30)
- `loaded` (lexical_declaration, line 37)
- `api` (lexical_declaration, line 38)
- `config` (lexical_declaration, line 49, exported)
- `section` (lexical_declaration, line 50, exported)
- `normalized` (lexical_declaration, line 58)
- `Walk` (interface_declaration, line 62)
- `stat` (lexical_declaration, line 72)
- `baseDir` (lexical_declaration, line 83)
- `star` (lexical_declaration, line 84)
- `trimmed` (lexical_declaration, line 85)
- `sep` (lexical_declaration, line 86)
- `dir` (lexical_declaration, line 94)
- `{ rules, ignore }` (lexical_declaration, line 99)
- `scan` (lexical_declaration, line 100)
- `verify` (lexical_declaration, line 101)
- `files` (lexical_declaration, line 102)
- `findings` (lexical_declaration, line 103)

## Contained in

- [core/adapters](https://banes-lab.com/anatomy/quality/folder-quality-core-adapters.md)

## Uses

- [core/factories/finding.factory.ts](https://banes-lab.com/source/quality/core/factories/finding.factory.ts.md)
- [core/loaders/source.loader.ts](https://banes-lab.com/source/quality/core/loaders/source.loader.ts.md)
- [core/selectors/exclusions.selector.ts](https://banes-lab.com/source/quality/core/selectors/exclusions.selector.ts.md)

## Linked from

- [core/adapters](https://banes-lab.com/anatomy/quality/folder-quality-core-adapters.md)
- [core/factories](https://banes-lab.com/anatomy/quality/folder-quality-core-factories.md)
- [core/loaders](https://banes-lab.com/anatomy/quality/folder-quality-core-loaders.md)
- [core/selectors](https://banes-lab.com/anatomy/quality/folder-quality-core-selectors.md)

## Source

```typescript
import type { Finding, RunResult } from "#types/finding.types";
import { existsSync, readFileSync, statSync } from "node:fs";
import { isRecord, stringArrayField } from "#core/selectors/record.selector";
import { safeReaddir, safeStat } from "#core/loaders/source.loader";
import { MISSING_VERIFY } from "#configuration/strings/tool.strings";
import type { PathExclusion } from "#types/exclusions.types";
import type { RunnerContext } from "#types/tool.types";
import { createRequire } from "node:module";
import { defineTool } from "#core/registries/tool.registry";
import { excludeMatcher } from "#core/factories/exclusions.factory";
import { findingsResult } from "#core/factories/finding.factory";
import { loadGovlabConfig } from "#core/loaders/config.loader";
import path from "node:path";
import process from "node:process";
import { sectionOf } from "#core/selectors/config.selector";
import { withMasterExclude } from "#core/selectors/exclusions.selector";

const SECTION = "htmlhint";
const HTML_EXTENSION = ".html";
const WILDCARD = "*";

interface HtmlhintMessage {
    col: number;
    line: number;
    message: string;
    rule: { id: string };
    type: string;
}

type HtmlhintVerify = (html: string, ruleset?: Record<string, unknown>) => HtmlhintMessage[];

const isVerify = function isVerify(value: unknown): value is HtmlhintVerify {
    return typeof value === "function";
};

const loadVerify = function loadVerify(): HtmlhintVerify {
    const loaded: unknown = createRequire(import.meta.url)(SECTION);
    const api = isRecord(loaded) ? loaded["HTMLHint"] : undefined;
    const verify = isRecord(api) ? api["verify"] : undefined;
    if (!isVerify(verify)) {
        throw new Error(MISSING_VERIFY);
    }
    return (html, ruleset) => verify.call(api, html, ruleset);
};

export const govlabHtmlhintConfig = async function govlabHtmlhintConfig(
    consumerRoot: string = process.cwd(),
): Promise<{ rules: Record<string, unknown>; ignore: string[] }> {
    const config = await loadGovlabConfig(consumerRoot);
    const section = sectionOf(config, SECTION);
    return {
        ignore: withMasterExclude(config, stringArrayField(section, "ignore")),
        rules: isRecord(section["rules"]) ? section["rules"] : {},
    };
};

const isIgnored = function isIgnored(file: string, ignore: readonly string[]): boolean {
    const normalized = file.split(path.sep).join("/");
    return ignore.some((entry) => normalized.includes(entry.split(WILDCARD).join("")));
};

interface Walk {
    excluded: PathExclusion;
    ignore: readonly string[];
}

const walk = function walk(dir: string, scan: Walk): string[] {
    return safeReaddir(dir)
        .map((entry) => path.join(dir, entry.name))
        .filter((full) => !scan.excluded(full))
        .flatMap((full): string[] => {
            const stat = safeStat(full);
            if (stat === null) {
                return [];
            }
            if (stat.isDirectory()) {
                return walk(full, scan);
            }
            return full.endsWith(HTML_EXTENSION) && !isIgnored(full, scan.ignore) ? [full] : [];
        });
};

const baseDir = function baseDir(entry: string): string {
    const star = entry.indexOf(WILDCARD);
    const trimmed = star === -1 ? entry : entry.slice(0, star);
    const sep = trimmed.lastIndexOf("/");
    return sep === -1 ? "." : trimmed.slice(0, sep);
};

const entryTargets = function entryTargets(root: string, entry: string, scan: Walk): string[] {
    if (!entry.includes(WILDCARD) && entry.endsWith(HTML_EXTENSION)) {
        return [path.resolve(root, entry)];
    }
    const dir = path.resolve(root, baseDir(entry));
    return existsSync(dir) && statSync(dir).isDirectory() ? walk(dir, scan) : [];
};

const runHtmlhint = async function runHtmlhint(context: RunnerContext): Promise<RunResult> {
    const { rules, ignore } = await govlabHtmlhintConfig(context.root);
    const scan: Walk = { excluded: await excludeMatcher(context.root), ignore };
    const verify = loadVerify();
    const files = [...new Set(context.paths.flatMap((entry) => entryTargets(context.root, entry, scan)))];
    const findings = files.flatMap((file): Finding[] =>
        verify(readFileSync(file, "utf8"), rules).map((message) => ({
            advisory: false,
            column: message.col,
            ecosystem: context.ecosystem,
            file,
            fixable: false,
            line: message.line,
            message: message.message,
            ruleId: message.rule.id,
            severity: "error",
            tool: SECTION,
        })),
    );
    return findingsResult(findings);
};

defineTool({ ecosystems: ["html"], run: runHtmlhint, tool: SECTION });
```
