UNPKG

markuplint

Version:

An HTML linter for all markup developers

381 lines (380 loc) 18.5 kB
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var _MLEngine_configProvider, _MLEngine_core, _MLEngine_file, _MLEngine_options, _MLEngine_watcher; import { ConfigProvider, resolveFiles, resolveParser, resolvePretenders, resolveRules, resolveSpecs, } from '@markuplint/file-resolver'; import { mergeConfig } from '@markuplint/ml-config'; import { MLCore, convertRuleset } from '@markuplint/ml-core'; import { FSWatcher } from 'chokidar'; import { Emitter } from 'strict-event-emitter'; import { log as coreLog, verbosely } from '../debug.js'; import { i18n } from '../i18n.js'; const log = coreLog.extend('ml-engine'); const fileLog = log.extend('file'); const configLog = log.extend('config'); /** * The main markuplint engine that orchestrates file resolution, configuration loading, * parsing, and linting. Supports both single-file and watch-mode operation. * * Emits events at each stage of the linting pipeline for monitoring and debugging. */ export class MLEngine extends Emitter { /** * Creates an MLEngine instance from inline source code. * * @param sourceCode - The markup source code to lint * @param options - Options for configuration, naming, and behavior * @returns A new MLEngine instance ready to lint the provided code */ static async fromCode(sourceCode, options) { if (options?.debug) { verbosely(); } log('[fromCode] Creates: %O', options); const file = await MLEngine.toMLFile({ sourceCode, name: options?.name, workspace: options?.dirname, }); if (!file) { throw new Error('Never reach error'); } log('[fromCode] Created file: %s', file.path); const engine = new MLEngine(file, options); return engine; } /** * Converts a target (file path or inline source) into an MLFile instance. * * @param target - A file path string or inline source code target * @returns The resolved MLFile, or `undefined` if resolution failed */ static async toMLFile(target) { const files = await resolveFiles([target]); return files[0]; } constructor(file, options) { super(); _MLEngine_configProvider.set(this, void 0); _MLEngine_core.set(this, null); _MLEngine_file.set(this, void 0); _MLEngine_options.set(this, void 0); _MLEngine_watcher.set(this, new FSWatcher()); if (__classPrivateFieldGet(this, _MLEngine_options, "f")?.debug) { verbosely(); } __classPrivateFieldSet(this, _MLEngine_file, file, "f"); __classPrivateFieldSet(this, _MLEngine_options, options, "f"); __classPrivateFieldSet(this, _MLEngine_configProvider, new ConfigProvider(), "f"); this.watchMode(!!__classPrivateFieldGet(this, _MLEngine_options, "f")?.watch); log('[MLEngine] Initialized: %s', __classPrivateFieldGet(this, _MLEngine_file, "f").path); } /** * The parsed document, or `null` if not yet set up or if parsing failed. */ get document() { if (__classPrivateFieldGet(this, _MLEngine_core, "f")?.document instanceof Error) { return null; } return __classPrivateFieldGet(this, _MLEngine_core, "f")?.document ?? null; } /** * Closes the engine, removing all event listeners and stopping the file watcher. */ async close() { this.removeAllListeners(); await __classPrivateFieldGet(this, _MLEngine_watcher, "f").close(); } /** * Executes linting on the target file and returns the results. * * Sets up the engine on first call, then verifies the document against all rules. * * @returns The lint result including violations and fixed code, or `null` if setup was skipped */ async exec() { log('exec: start'); const core = await this.setup(); if (!core) { log('exec: cancel (unsetuped yet)'); return null; } const violations = await core.verify(__classPrivateFieldGet(this, _MLEngine_options, "f")?.fix).catch(error => { if (error instanceof Error) { return error; } throw error; }); const sourceCode = await __classPrivateFieldGet(this, _MLEngine_file, "f").getCode(); const fixedCode = core.document.toString(true); if (violations instanceof Error) { this.emit('lint-error', __classPrivateFieldGet(this, _MLEngine_file, "f").path, sourceCode, violations); const errMessage = violations.stack ?? violations.message; log('exec: error %O', errMessage); return { violations: [ { severity: 'error', message: errMessage, ruleId: '@markuplint/ml-core', line: 0, col: 0, raw: '', }, ], filePath: __classPrivateFieldGet(this, _MLEngine_file, "f").path, sourceCode, fixedCode, status: 'processed', }; } const debugMap = 'debugMap' in core.document ? core.document.debugMap() : null; this.emit('lint', __classPrivateFieldGet(this, _MLEngine_file, "f").path, sourceCode, violations, fixedCode, debugMap); log('exec: end'); return { violations, filePath: __classPrivateFieldGet(this, _MLEngine_file, "f").path, sourceCode, fixedCode, status: 'processed', }; } /** * Updates the source code and re-parses the document without re-resolving configuration. * * @param code - The new markup source code */ async setCode(code) { const core = await this.setup(); if (!core) { return; } __classPrivateFieldGet(this, _MLEngine_file, "f").setCode(code); core.setCode(code); } /** * Enables or disables watch mode. When enabled, the engine watches config files * for changes and re-lints automatically. * * @param enable - Whether to enable watch mode */ watchMode(enable) { __classPrivateFieldSet(this, _MLEngine_options, { ...__classPrivateFieldGet(this, _MLEngine_options, "f"), watch: enable, }, "f"); if (enable) { __classPrivateFieldGet(this, _MLEngine_watcher, "f").on('change', this.onChange.bind(this)); } else { __classPrivateFieldGet(this, _MLEngine_watcher, "f").removeAllListeners(); } } async createCore(fabric) { fileLog('Get source code'); const sourceCode = await __classPrivateFieldGet(this, _MLEngine_file, "f").getCode(); fileLog('Source code path: %s', __classPrivateFieldGet(this, _MLEngine_file, "f").path); // cspell: disable-next-line fileLog('Source code size: %dbyte', sourceCode.length); this.emit('code', __classPrivateFieldGet(this, _MLEngine_file, "f").path, sourceCode); const core = new MLCore({ sourceCode, filename: __classPrivateFieldGet(this, _MLEngine_file, "f").path, debug: __classPrivateFieldGet(this, _MLEngine_options, "f")?.debug, ...fabric, }); __classPrivateFieldSet(this, _MLEngine_core, core, "f"); return core; } async i18n() { const i18nSettings = await i18n(__classPrivateFieldGet(this, _MLEngine_options, "f")?.locale); this.emit('i18n', __classPrivateFieldGet(this, _MLEngine_file, "f").path, i18nSettings); return i18nSettings; } async onChange(filePath) { if (!__classPrivateFieldGet(this, _MLEngine_options, "f")?.watch) { return; } this.emit('log', 'watch:onChange', filePath); const fabric = await this.provide(false); if (!fabric) { return; } if (fabric.configErrors) { this.emit('config-errors', __classPrivateFieldGet(this, _MLEngine_file, "f").path, fabric.configErrors); } this.emit('log', 'update:core', __classPrivateFieldGet(this, _MLEngine_file, "f").path); __classPrivateFieldGet(this, _MLEngine_core, "f")?.update(fabric); await this.exec(); } async provide(cache = true) { let configSet; try { configSet = await this.resolveConfig(cache); } catch (error) { if (error instanceof Error) { configSet = { config: {}, plugins: [], files: new Set(), errs: [error], }; } else { throw error; } } fileLog('Fetched Config files: %O', configSet.files); fileLog('Resolved Config: %O', configSet.config); fileLog('Resolved Plugins: %O', configSet.plugins); fileLog('Resolve Errors: %O', configSet.errs); if (!(await __classPrivateFieldGet(this, _MLEngine_file, "f").isFile())) { this.emit('log', 'file-no-exists', `The file doesn't exist or it is not a file: ${__classPrivateFieldGet(this, _MLEngine_file, "f").path}`); fileLog("The file doesn't exist or it is not a file: %s", __classPrivateFieldGet(this, _MLEngine_file, "f").path); return null; } // Exclude const excludeFiles = configSet.config.excludeFiles ?? []; if (__classPrivateFieldGet(this, _MLEngine_file, "f").ignored(excludeFiles)) { fileLog('Excludes the file: %s', __classPrivateFieldGet(this, _MLEngine_file, "f").path); return null; } const { parser, parserOptions, matched } = await this.resolveParser(configSet); const checkingExt = !__classPrivateFieldGet(this, _MLEngine_options, "f")?.ignoreExt; if (checkingExt && !matched) { this.emit('log', 'ext-unmatched', `Avoided linting because a file is unmatched by the extension: ${__classPrivateFieldGet(this, _MLEngine_file, "f").path}`); fileLog('Avoided linting because a file is unmatched by the extension: %s', __classPrivateFieldGet(this, _MLEngine_file, "f").path); return null; } const severity = { ...configSet.config.severity, ...__classPrivateFieldGet(this, _MLEngine_options, "f")?.severity, }; const pretenders = await this.resolvePretenders(configSet); fileLog('Resolved pretenders: %O', pretenders); const ruleset = this.resolveRuleset(configSet); fileLog('Resolved ruleset: %O', ruleset); const schemas = await this.resolveSchemas(configSet); if (fileLog.enabled) { if (schemas[0].cites.length > 0) { const [, ...additionalSpecs] = schemas; fileLog('Resolved schemas: HTML Standard'); for (const additionalSpec of additionalSpecs) { fileLog('Resolved schemas: %O', additionalSpec); } } else { fileLog('Resolved schemas: %O', schemas); } } const rules = await this.resolveRules(configSet.plugins, ruleset); fileLog('Resolved rules: %O', rules); const locale = await i18n(__classPrivateFieldGet(this, _MLEngine_options, "f")?.locale); if (fileLog.enabled) { fileLog('Loaded %d rules: %O', rules.length, rules.map(r => r.name)); } return { parser, parserOptions, severity, pretenders, ruleset, schemas, rules, locale, configErrors: configSet.errs, }; } async resolveConfig(cache) { this.emit('log', 'resolveConfig', JSON.stringify(__classPrivateFieldGet(this, _MLEngine_configProvider, "f"), null, 2)); configLog('configProvider: %s', __classPrivateFieldGet(this, _MLEngine_configProvider, "f")); const defaultConfigKey = __classPrivateFieldGet(this, _MLEngine_options, "f")?.defaultConfig && __classPrivateFieldGet(this, _MLEngine_configProvider, "f").set(mergeConfig(__classPrivateFieldGet(this, _MLEngine_options, "f")?.defaultConfig)); configLog('defaultConfigKey: %s', defaultConfigKey ?? 'N/A'); this.emit('log', 'defaultConfigKey', defaultConfigKey ?? 'N/A'); const targetConfig = await __classPrivateFieldGet(this, _MLEngine_configProvider, "f").search(__classPrivateFieldGet(this, _MLEngine_file, "f")); this.emit('log', 'targetConfig', targetConfig ?? 'N/A'); const configFilePathsFromTarget = __classPrivateFieldGet(this, _MLEngine_options, "f")?.noSearchConfig ? (defaultConfigKey ?? null) : (targetConfig ?? defaultConfigKey); configLog('configFilePathsFromTarget: %s', configFilePathsFromTarget ?? 'N/A'); this.emit('log', 'configFilePathsFromTarget', configFilePathsFromTarget ?? 'N/A'); const configKey = __classPrivateFieldGet(this, _MLEngine_options, "f")?.config && __classPrivateFieldGet(this, _MLEngine_configProvider, "f").set(mergeConfig(__classPrivateFieldGet(this, _MLEngine_options, "f").config)); configLog('option.config: %s', configKey ?? 'N/A'); this.emit('log', 'option.config', configFilePathsFromTarget ?? 'N/A'); let defaultRecommended = null; if (!defaultConfigKey && !configFilePathsFromTarget && !configKey) { // No configured // Default: set recommended defaultRecommended = __classPrivateFieldGet(this, _MLEngine_configProvider, "f").set({ extends: ['markuplint:recommended'] }); } configLog('defaultRecommended: %s', defaultRecommended ?? 'N/A'); this.emit('log', 'defaultRecommended', defaultRecommended ?? 'N/A'); const configSet = await __classPrivateFieldGet(this, _MLEngine_configProvider, "f").resolve(__classPrivateFieldGet(this, _MLEngine_file, "f"), [configFilePathsFromTarget, __classPrivateFieldGet(this, _MLEngine_options, "f")?.configFile, configKey, defaultRecommended], cache); this.emit('config', __classPrivateFieldGet(this, _MLEngine_file, "f").path, configSet); if (__classPrivateFieldGet(this, _MLEngine_options, "f")?.watch) { // It doesn't watch the main HTML file because it may is watched and managed by a language server or text editor or more. __classPrivateFieldGet(this, _MLEngine_watcher, "f").add([...configSet.files]); } return configSet; } async resolveParser( // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types configSet) { const parser = await resolveParser(__classPrivateFieldGet(this, _MLEngine_file, "f"), configSet.config.parser, configSet.config.parserOptions); this.emit('parser', __classPrivateFieldGet(this, _MLEngine_file, "f").path, parser.parserModName); fileLog('Fetched Parser module: %s', parser.parserModName); return parser; } async resolvePretenders( // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types configSet) { const pretenders = await resolvePretenders(configSet.config.pretenders); fileLog('Resolved pretenders: %O', pretenders); return pretenders; } async resolveRules(plugins, ruleset) { const rules = await resolveRules(plugins, ruleset, __classPrivateFieldGet(this, _MLEngine_options, "f")?.importPresetRules ?? true, __classPrivateFieldGet(this, _MLEngine_options, "f")?.autoLoad ?? true); if (__classPrivateFieldGet(this, _MLEngine_options, "f")?.rules) { rules.push(...__classPrivateFieldGet(this, _MLEngine_options, "f").rules); } this.emit('rules', __classPrivateFieldGet(this, _MLEngine_file, "f").path, rules); return rules; } resolveRuleset( // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types configSet) { const ruleset = convertRuleset(configSet.config); this.emit('ruleset', __classPrivateFieldGet(this, _MLEngine_file, "f").path, ruleset); return ruleset; } async resolveSchemas( // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types configSet) { const { schemas } = await resolveSpecs(__classPrivateFieldGet(this, _MLEngine_file, "f").path, configSet.config.specs); this.emit('schemas', __classPrivateFieldGet(this, _MLEngine_file, "f").path, schemas); return schemas; } async setup() { if (__classPrivateFieldGet(this, _MLEngine_core, "f")) { return __classPrivateFieldGet(this, _MLEngine_core, "f"); } const fabric = await this.provide(); if (!fabric) { return null; } if (fabric.configErrors) { this.emit('config-errors', __classPrivateFieldGet(this, _MLEngine_file, "f").path, fabric.configErrors); } return this.createCore(fabric); } } _MLEngine_configProvider = new WeakMap(), _MLEngine_core = new WeakMap(), _MLEngine_file = new WeakMap(), _MLEngine_options = new WeakMap(), _MLEngine_watcher = new WeakMap();