UNPKG

html-validate

Version:

Offline HTML5 validator and linter

607 lines (588 loc) 16.5 kB
import { l as legacyRequire, F as FileSystemConfigLoader, e as esmResolver, H as HtmlValidate } from './core-nodejs.js'; import { g as getFormatter$1, U as UserError, e as ensureError, i as ignore, d as deepmerge, J as engines, B as Reporter } from './core.js'; import path$1 from 'node:path/posix'; import fs from 'fs'; import path from 'node:path'; import { globSync } from 'glob'; import prompts from 'prompts'; import './meta-helper.js'; import fs$1 from 'node:fs'; import betterAjvErrors from '@sidvind/better-ajv-errors'; import kleur from 'kleur'; const DEFAULT_EXTENSIONS = ["html"]; function isDirectory(filename) { const st = fs.statSync(filename); return st.isDirectory(); } function join(stem, filename) { if (path.isAbsolute(filename)) { return path.normalize(filename); } else { return path.normalize(path.join(stem, filename)); } } function directoryPattern(extensions) { switch (extensions.length) { case 0: return "**/*"; case 1: return `**/*.${extensions[0]}`; default: return `**/*.{${extensions.join(",")}}`; } } function expandFiles(patterns, options) { const cwd = options.cwd ?? process.cwd(); const extensions = options.extensions ?? DEFAULT_EXTENSIONS; const files = patterns.reduce((result, pattern) => { if (pattern === "-") { result.push("/dev/stdin"); return result; } for (const filename of globSync(pattern, { cwd })) { const fullpath = join(cwd, filename); if (isDirectory(fullpath)) { const dir = expandFiles([directoryPattern(extensions)], { ...options, cwd: fullpath }); result = result.concat(dir.map((cur) => join(filename, cur))); continue; } result.push(fullpath); } return result.sort((a, b) => { const pa = a.split("/").length; const pb = b.split("/").length; if (pa !== pb) { return pa - pb; } else { return a > b ? 1 : -1; } }); }, []); return Array.from(new Set(files)); } function wrap(formatter, dst) { return (results) => { const output = formatter(results); if (dst) { const dir = path.dirname(dst); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(dst, output, "utf-8"); return ""; } else { return output; } }; } function loadFormatter(name) { const fn = getFormatter$1(name); if (fn) { return fn; } try { return legacyRequire(name); } catch (error) { throw new UserError(`No formatter named "${name}"`, ensureError(error)); } } function getFormatter(formatters) { const fn = formatters.split(",").map((cur) => { const [name, dst] = cur.split("=", 2); const fn2 = loadFormatter(name); return wrap(fn2, dst); }); return (report) => { return fn.map((formatter) => formatter(report.results)).filter(Boolean).join("\n"); }; } class IsIgnored { /** Cache for parsed .htmlvalidateignore files */ cacheIgnore; constructor() { this.cacheIgnore = /* @__PURE__ */ new Map(); } /** * Searches ".htmlvalidateignore" files from filesystem and returns `true` if * one of them contains a pattern matching given filename. */ isIgnored(filename) { return this.match(filename); } /** * Clear cache */ clearCache() { this.cacheIgnore.clear(); } match(target) { let current = path.dirname(target); while (true) { const relative = path.relative(current, target); const filename = path.join(current, ".htmlvalidateignore"); const ig = this.parseFile(filename); if (ig?.ignores(relative)) { return true; } const child = current; current = path.dirname(current); if (current === child) { break; } } return false; } parseFile(filename) { if (this.cacheIgnore.has(filename)) { return this.cacheIgnore.get(filename); } if (!fs.existsSync(filename)) { this.cacheIgnore.set(filename, void 0); return void 0; } const content = fs.readFileSync(filename, "utf-8"); const ig = ignore().add(content); this.cacheIgnore.set(filename, ig); return ig; } } const frameworkConfig = { ["AngularJS" /* angularjs */]: { transform: { "^.*\\.js$": "html-validate-angular/js", "^.*\\.html$": "html-validate-angular/html" } }, ["Vue.js" /* vuejs */]: { plugins: ["html-validate-vue"], extends: ["html-validate-vue:recommended"], transform: { "^.*\\.vue$": "html-validate-vue" } }, ["Markdown" /* markdown */]: { transform: { "^.*\\.md$": "html-validate-markdown" } } }; function addFrameworks(src, frameworks) { let config = src; for (const framework of frameworks) { config = deepmerge(config, frameworkConfig[framework]); } return config; } function writeConfig(dst, config) { return new Promise((resolve, reject) => { fs.writeFile(dst, JSON.stringify(config, null, 2), (err) => { if (err) reject(err); resolve(); }); }); } async function init$1(cwd) { const filename = `${cwd}/.htmlvalidate.json`; const exists = fs.existsSync(filename); const initialConfig = { elements: ["html5"], extends: ["html-validate:recommended"] }; if (exists) { const result = await prompts({ name: "overwrite", type: "confirm", message: "A .htmlvalidate.json file already exists, do you want to overwrite it?" }); if (!result.overwrite) { return Promise.reject(); } } const questions = [ { name: "frameworks", type: "multiselect", choices: [ { title: "AngularJS" /* angularjs */, value: "AngularJS" /* angularjs */ }, { title: "Vue.js" /* vuejs */, value: "Vue.js" /* vuejs */ }, { title: "Markdown" /* markdown */, value: "Markdown" /* markdown */ } ], message: "Support additional frameworks?" } ]; const answers = await prompts(questions); let config = initialConfig; config = addFrameworks(config, answers.frameworks); await writeConfig(filename, config); return { filename }; } function parseSeverity(ruleId, severity) { switch (severity) { case "off": case "0": return "off"; case "warn": case "1": return "warn"; case "error": case "2": return "error"; default: throw new Error(`Invalid severity "${severity}" for rule "${ruleId}"`); } } function parseItem(value) { const [ruleId, severity = "error"] = value.split(":", 2); return { ruleId, severity: parseSeverity(ruleId, severity) }; } function getRuleConfig(values) { if (typeof values === "string") { return getRuleConfig([values]); } return values.reduce((parsedRules, value) => { const { ruleId, severity } = parseItem(value.trim()); return { [ruleId]: severity, ...parsedRules }; }, {}); } const resolver = esmResolver(); function defaultConfig(preset) { const presets = preset.split(",").map((it) => `html-validate:${it}`); return { extends: presets }; } async function getBaseConfig(preset, filename) { if (filename) { const configData = await resolver.resolveConfig(path$1.resolve(filename), { cache: false }); if (!configData) { throw new UserError(`Failed to read configuration from "${filename}"`); } return configData; } else { return defaultConfig(preset ?? "recommended"); } } class CLI { options; config; loader; ignored; /** * Create new CLI helper. * * Can be used to create tooling with similar properties to bundled CLI * script. */ constructor(options) { this.options = options ?? {}; this.config = null; this.loader = null; this.ignored = new IsIgnored(); } /** * Returns list of files matching patterns and are not ignored. Filenames will * have absolute paths. * * @public */ async expandFiles(patterns, options = {}) { const files = expandFiles(patterns, options).filter((filename) => !this.isIgnored(filename)); return Promise.resolve(files); } getFormatter(formatters) { return Promise.resolve(getFormatter(formatters)); } /** * Initialize project with a new configuration. * * A new `.htmlvalidate.json` file will be placed in the path provided by * `cwd`. */ init(cwd) { return init$1(cwd); } /** * Clear cache. * * Previously fetched [[HtmlValidate]] instances must either be fetched again * or call [[HtmlValidate.flushConfigCache]]. */ /* istanbul ignore next: each method is tested separately */ clearCache() { if (this.loader) { this.loader.flushCache(); } this.ignored.clearCache(); return Promise.resolve(); } /** * Get HtmlValidate instance with configuration based on options passed to the * constructor. * * @internal */ async getLoader() { if (!this.loader) { const config = await this.getConfig(); this.loader = new FileSystemConfigLoader([resolver], config); } return this.loader; } /** * Get HtmlValidate instance with configuration based on options passed to the * constructor. * * @public */ async getValidator() { const loader = await this.getLoader(); return new HtmlValidate(loader); } /** * @internal */ async getConfig() { this.config ??= await this.resolveConfig(); return this.config; } /** * Searches ".htmlvalidateignore" files from filesystem and returns `true` if * one of them contains a pattern matching given filename. */ isIgnored(filename) { return this.ignored.isIgnored(filename); } async resolveConfig() { const { options } = this; const havePreset = Boolean(options.preset); const haveConfig = Boolean(options.configFile); const config = await getBaseConfig(options.preset, options.configFile); if (options.rules) { if (havePreset || haveConfig) { config.rules = { ...config.rules, ...getRuleConfig(options.rules) }; } else { config.extends = []; config.rules = getRuleConfig(options.rules); } } return config; } } function prettyError(err) { let json; if (err.filename && fs$1.existsSync(err.filename)) { json = fs$1.readFileSync(err.filename, "utf-8"); } return betterAjvErrors(err.schema, err.obj, err.errors, { format: "cli", indent: 2, json }); } function handleSchemaValidationError(console, err) { if (err.filename) { const filename = path.relative(process.cwd(), err.filename); console.error(kleur.red(`A configuration error was found in "${filename}":`)); } else { console.error(kleur.red(`A configuration error was found:`)); } console.group(); { console.error(prettyError(err)); } console.groupEnd(); } class ImportResolveMissingError extends UserError { constructor() { const message = `import.meta.resolve(..) is not available on this system`; super(message); Error.captureStackTrace(this, ImportResolveMissingError); this.name = ImportResolveMissingError.name; } prettyFormat() { const { message } = this; const currentVersion = process.version; const requiredVersion = engines.node.split("||").map((it) => `v${it.replace(/^[^\d]+/, "").trim()}`); return [ kleur.red(`Error: ${message}.`), "", `Either ensure you are running a supported NodeJS version:`, ` Current: ${currentVersion}`, ` Required: ${requiredVersion.join(", ")} or later`, `Or set NODE_OPTIONS="--experimental-import-meta-resolve"` ].join("\n"); } } var Mode = /* @__PURE__ */ ((Mode2) => { Mode2[Mode2["LINT"] = 0] = "LINT"; Mode2[Mode2["INIT"] = 1] = "INIT"; Mode2[Mode2["DUMP_EVENTS"] = 2] = "DUMP_EVENTS"; Mode2[Mode2["DUMP_TOKENS"] = 3] = "DUMP_TOKENS"; Mode2[Mode2["DUMP_TREE"] = 4] = "DUMP_TREE"; Mode2[Mode2["DUMP_SOURCE"] = 5] = "DUMP_SOURCE"; Mode2[Mode2["PRINT_CONFIG"] = 6] = "PRINT_CONFIG"; return Mode2; })(Mode || {}); function modeToFlag(mode) { switch (mode) { case 0 /* LINT */: return null; case 1 /* INIT */: return "--init"; case 2 /* DUMP_EVENTS */: return "--dump-events"; case 3 /* DUMP_TOKENS */: return "--dump-tokens"; case 4 /* DUMP_TREE */: return "--dump-tree"; case 5 /* DUMP_SOURCE */: return "--dump-source"; case 6 /* PRINT_CONFIG */: return "--print-config"; } } function renameStdin(report, filename) { const stdin = report.results.find((cur) => cur.filePath === "/dev/stdin"); if (stdin) { stdin.filePath = filename; } } async function lint(htmlvalidate, output, files, options) { const reports = []; for (const filename of files) { try { reports.push(await htmlvalidate.validateFile(filename)); } catch (err) { const message = kleur.red(`Validator crashed when parsing "${filename}"`); output.write(`${message} `); throw err; } } const merged = Reporter.merge(reports); if (options.stdinFilename) { renameStdin(merged, options.stdinFilename); } output.write(options.formatter(merged)); if (options.maxWarnings >= 0 && merged.warningCount > options.maxWarnings) { output.write( ` html-validate found too many warnings (maximum: ${String(options.maxWarnings)}). ` ); return false; } return merged.valid; } async function init(cli, output, options) { const result = await cli.init(options.cwd); output.write(`Configuration written to "${result.filename}" `); return true; } async function printConfig(htmlvalidate, output, files) { if (files.length > 1) { output.write(`\`--print-config\` expected a single filename but got multiple: `); for (const filename of files) { output.write(` - ${filename} `); } output.write("\n"); return false; } const config = await htmlvalidate.getConfigFor(files[0]); const json = JSON.stringify(config.getConfigData(), null, 2); output.write(`${json} `); return true; } const jsonIgnored = [ "annotation", "blockedRules", "cache", "closed", "depth", "disabledRules", "nodeType", "unique", "voidElement" ]; const jsonFiltered = [ "childNodes", "children", "data", "meta", "metaElement", "originalData", "parent" ]; function isLocation(key, value) { return Boolean(value && (key === "location" || key.endsWith("Location"))); } function isIgnored(key) { return key.startsWith("_") || jsonIgnored.includes(key); } function isFiltered(key, value) { return Boolean(value && jsonFiltered.includes(key)); } function eventReplacer(key, value) { if (isLocation(key, value)) { const filename = value.filename; const line = String(value.line); const column = String(value.column); return `${filename}:${line}:${column}`; } if (isIgnored(key)) { return void 0; } if (isFiltered(key, value)) { return "[truncated]"; } return value; } function eventFormatter(entry) { const strdata = JSON.stringify(entry.data, eventReplacer, 2); return `${entry.event}: ${strdata}`; } async function dump(htmlvalidate, output, files, mode) { let lines; switch (mode) { case Mode.DUMP_EVENTS: lines = files.map(async (filename) => { const lines2 = await htmlvalidate.dumpEvents(filename); return lines2.map(eventFormatter); }); break; case Mode.DUMP_TOKENS: lines = files.map(async (filename) => { const lines2 = await htmlvalidate.dumpTokens(filename); return lines2.map((entry) => { const data = JSON.stringify(entry.data); return `TOKEN: ${entry.token} Data: ${data} Location: ${entry.location}`; }); }); break; case Mode.DUMP_TREE: lines = files.map((filename) => htmlvalidate.dumpTree(filename)); break; case Mode.DUMP_SOURCE: lines = files.map((filename) => htmlvalidate.dumpSource(filename)); break; default: throw new Error(`Unknown mode "${String(mode)}"`); } const flat = (await Promise.all(lines)).reduce((s, c) => s.concat(c), []); output.write(flat.join("\n")); output.write("\n"); return Promise.resolve(true); } function haveImportMetaResolve() { return "resolve" in import.meta; } export { CLI as C, ImportResolveMissingError as I, Mode as M, handleSchemaValidationError as a, dump as d, haveImportMetaResolve as h, init as i, lint as l, modeToFlag as m, printConfig as p }; //# sourceMappingURL=cli.js.map