markuplint
Version:
An HTML linter for all markup developers
381 lines (380 loc) • 18.5 kB
JavaScript
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();