import { ConfigData, HtmlValidate, Report, Reporter, } from "../deps/html_validate.ts"; import { merge } from "../core/utils/object.ts"; import { log } from "../core/utils/log.ts"; export interface Options { /** * List of plugins to load * @see https://html-validate.org/usage/#plugins */ plugins?: ConfigData["plugins"]; /** * List of configuration presets to extend. * @see https://html-validate.org/usage/#extends */ extends?: string[]; /** * Rules configuration * @see https://html-validate.org/usage/#rules */ rules?: ConfigData["rules"]; /** Customize the report output */ output?: string | ((report: Report) => void); } export const defaults: Options = { extends: ["html-validate:recommended", "html-validate:document"], rules: { "doctype-style": "off", "attr-quotes": "off", "no-trailing-whitespace": "off", "void-style": "warn", "require-sri": ["error", { target: "crossorigin" }], }, }; export function validateHtml(userOptions?: Options) { const options = merge(defaults, userOptions); const htmlvalidate = new HtmlValidate({ plugins: options.plugins, rules: options.rules, extends: options.extends, }); return (site: Lume.Site) => { let reports: Report | undefined; site.process([".html"], processValidateHtml); function output() { if (!reports) { return; } const { output } = options; if (typeof output === "function") { output(reports); } else if (typeof output === "string") { outputFile(reports, output); } else if (output !== false) { outputConsole(reports); } } site.addEventListener("afterUpdate", output); site.addEventListener("afterBuild", output); async function processValidateHtml(pages: Lume.Page[]) { reports = undefined; const pageReports: Set = new Set(); for (const page of pages) { const report = await htmlvalidate.validateString( page.text, page.data.url, ); pageReports.add(report); } reports = Reporter.merge(Array.from(pageReports.values())); const report = site.debugBar?.collection("HTML validator"); if (report) { report.icon = "file-html"; report.empty = "No HTML errors found! 🎉"; for (const result of reports.results) { report.items.push({ title: result.filePath!, items: Array.from(result.messages).map((message) => { const actions = message.ruleUrl ? [{ text: "Info", href: message.ruleUrl, target: "_blank" }] : []; return { title: `[${message.ruleId}] ${escapeHtml(message.message)}`, details: `Line ${message.line}, Column ${message.column}`, code: message.selector ?? undefined, actions, }; }), actions: [ { text: "Open", href: result.filePath!, }, ], }); } } } }; } function outputFile( reports: Report, file: string, ) { const content = JSON.stringify( reports, null, 2, ); Deno.writeTextFileSync(file, content); if (reports.valid) { log.info("[validate_html plugin] No HTML errors found!"); return; } log.warn( `[validate_html plugin] ${reports.errorCount} HTML error(s) saved to ${file}`, ); } function outputConsole(reports: Report) { if (reports.valid) { log.info("[validate_html plugin] No HTML errors found!"); return; } log.warn( `[validate_html plugin] ${reports.errorCount} HTML error(s) found. Setup an output file or check the debug bar.`, ); } function escapeHtml(text: string) { return text .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">"); } export default validateHtml;