@markuplint/ml-core 4.13.2 → 5.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.ja.md +524 -0
- package/ARCHITECTURE.md +524 -0
- package/CHANGELOG.md +52 -2
- package/README.md +5 -0
- package/SKILL.md +61 -0
- package/docs/linting-pipeline.ja.md +307 -0
- package/docs/linting-pipeline.md +307 -0
- package/docs/maintenance.ja.md +210 -0
- package/docs/maintenance.md +210 -0
- package/docs/ml-dom/attr.ja.md +103 -0
- package/docs/ml-dom/attr.md +103 -0
- package/docs/ml-dom/block.ja.md +272 -0
- package/docs/ml-dom/block.md +272 -0
- package/docs/ml-dom/document.ja.md +141 -0
- package/docs/ml-dom/document.md +141 -0
- package/docs/ml-dom/element.ja.md +176 -0
- package/docs/ml-dom/element.md +176 -0
- package/docs/ml-dom/helpers.ja.md +203 -0
- package/docs/ml-dom/helpers.md +203 -0
- package/docs/ml-dom/node.ja.md +199 -0
- package/docs/ml-dom/node.md +199 -0
- package/docs/ml-dom/others.ja.md +120 -0
- package/docs/ml-dom/others.md +120 -0
- package/docs/ml-dom/overview.ja.md +102 -0
- package/docs/ml-dom/overview.md +102 -0
- package/docs/ml-dom/pretender.ja.md +269 -0
- package/docs/ml-dom/pretender.md +269 -0
- package/docs/ml-dom/rule-mapping.ja.md +371 -0
- package/docs/ml-dom/rule-mapping.md +371 -0
- package/docs/ml-dom.ja.md +18 -0
- package/docs/ml-dom.md +18 -0
- package/docs/rule-system.ja.md +287 -0
- package/docs/rule-system.md +287 -0
- package/lib/convert-ruleset.d.ts +7 -0
- package/lib/convert-ruleset.js +7 -0
- package/lib/debug.d.ts +4 -0
- package/lib/debug.js +4 -0
- package/lib/index.d.ts +4 -3
- package/lib/index.js +1 -1
- package/lib/ml-core.d.ts +37 -1
- package/lib/ml-core.js +171 -82
- package/lib/ml-dom/helper/accname.d.ts +8 -0
- package/lib/ml-dom/helper/accname.js +71 -55
- package/lib/ml-dom/helper/create-node.js +1 -0
- package/lib/ml-dom/helper/get-indent.d.ts +4 -1
- package/lib/ml-dom/helper/get-indent.js +21 -30
- package/lib/ml-dom/node/attr.d.ts +65 -4
- package/lib/ml-dom/node/attr.js +151 -53
- package/lib/ml-dom/node/block.d.ts +23 -2
- package/lib/ml-dom/node/block.js +24 -1
- package/lib/ml-dom/node/child-node.d.ts +9 -0
- package/lib/ml-dom/node/child-node.js +9 -0
- package/lib/ml-dom/node/comment.d.ts +7 -0
- package/lib/ml-dom/node/comment.js +7 -0
- package/lib/ml-dom/node/document-fragment.d.ts +8 -0
- package/lib/ml-dom/node/document-fragment.js +8 -0
- package/lib/ml-dom/node/document-type.d.ts +22 -0
- package/lib/ml-dom/node/document-type.js +25 -0
- package/lib/ml-dom/node/document.d.ts +88 -7
- package/lib/ml-dom/node/document.js +128 -32
- package/lib/ml-dom/node/dom-token-list.js +17 -30
- package/lib/ml-dom/node/element-close-tag.js +1 -0
- package/lib/ml-dom/node/element.d.ts +151 -5
- package/lib/ml-dom/node/element.js +242 -50
- package/lib/ml-dom/node/node-store.js +6 -15
- package/lib/ml-dom/node/node.d.ts +19 -1
- package/lib/ml-dom/node/node.js +175 -166
- package/lib/ml-dom/node/parent-node.js +14 -30
- package/lib/ml-dom/node/rule-mapper.js +7 -20
- package/lib/ml-dom/node/text.d.ts +19 -0
- package/lib/ml-dom/node/text.js +21 -0
- package/lib/ml-dom/node/types.d.ts +68 -0
- package/lib/ml-dom/token/token.d.ts +42 -0
- package/lib/ml-dom/token/token.js +59 -39
- package/lib/ml-rule/create-rule.d.ts +17 -1
- package/lib/ml-rule/ml-rule-context.js +7 -11
- package/lib/ml-rule/ml-rule.d.ts +66 -1
- package/lib/ml-rule/ml-rule.js +95 -25
- package/lib/ml-rule/types.d.ts +41 -0
- package/lib/plugin/plugin.d.ts +8 -0
- package/lib/plugin/plugin.js +8 -0
- package/lib/plugin/types.d.ts +21 -0
- package/lib/ruleset/index.d.ts +10 -0
- package/lib/ruleset/index.js +13 -0
- package/lib/test/index.d.ts +42 -1
- package/lib/test/index.js +39 -2
- package/lib/types.d.ts +10 -1
- package/lib/violation-collector.d.ts +33 -0
- package/lib/violation-collector.js +48 -28
- package/lib/virtual-rule.d.ts +72 -0
- package/lib/virtual-rule.js +233 -0
- package/package.json +16 -13
package/lib/ml-core.d.ts
CHANGED
|
@@ -2,17 +2,53 @@ import type { MLFabric } from './types.js';
|
|
|
2
2
|
import type { PlainData, RuleConfigValue, Violation } from '@markuplint/ml-config';
|
|
3
3
|
import { ParserError } from '@markuplint/parser-utils';
|
|
4
4
|
import { Document } from './ml-dom/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Parameters for constructing an {@link MLCore} instance.
|
|
7
|
+
* Extends {@link MLFabric} with the source code, filename, and debug flag.
|
|
8
|
+
*/
|
|
5
9
|
export type MLCoreParams = {
|
|
10
|
+
/** The markup source code to lint */
|
|
6
11
|
readonly sourceCode: string;
|
|
12
|
+
/** The filename associated with the source code */
|
|
7
13
|
readonly filename: string;
|
|
14
|
+
/** Whether to enable debug logging */
|
|
8
15
|
readonly debug?: boolean;
|
|
9
16
|
} & MLFabric;
|
|
17
|
+
/**
|
|
18
|
+
* The core linting engine for markuplint.
|
|
19
|
+
*
|
|
20
|
+
* Parses markup source code into an AST, constructs a DOM document,
|
|
21
|
+
* and verifies it against configured rules to produce violations.
|
|
22
|
+
*/
|
|
10
23
|
export declare class MLCore {
|
|
11
24
|
#private;
|
|
12
|
-
constructor({ parser, sourceCode, ruleset, rules, locale, schemas, parserOptions, severity, pretenders, filename, debug, configErrors, }: MLCoreParams);
|
|
25
|
+
constructor({ parser, sourceCode, ruleset, rules, locale, schemas, ruleCommonSettings, parserOptions, severity, pretenders, filename, debug, configErrors, }: MLCoreParams);
|
|
26
|
+
/**
|
|
27
|
+
* The parsed document, or a {@link ParserError} if parsing failed.
|
|
28
|
+
*/
|
|
13
29
|
get document(): ParserError | Document<RuleConfigValue, PlainData>;
|
|
30
|
+
/**
|
|
31
|
+
* Replaces the source code and re-parses the document.
|
|
32
|
+
*
|
|
33
|
+
* @param sourceCode - The new markup source code
|
|
34
|
+
*/
|
|
14
35
|
setCode(sourceCode: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Updates the linting configuration and re-creates the document.
|
|
38
|
+
* Only re-parses if parser options have changed.
|
|
39
|
+
*
|
|
40
|
+
* @param fabric - Partial fabric with the properties to update
|
|
41
|
+
*/
|
|
15
42
|
update({ parser, ruleset, rules, locale, schemas, parserOptions, configErrors }: Partial<MLFabric>): void;
|
|
43
|
+
/**
|
|
44
|
+
* Runs all configured rules against the parsed document and returns violations.
|
|
45
|
+
*
|
|
46
|
+
* If the document failed to parse, a single parse-error violation is returned
|
|
47
|
+
* (unless parse errors are suppressed via severity options).
|
|
48
|
+
*
|
|
49
|
+
* @param fix - Whether to attempt auto-fixing violations
|
|
50
|
+
* @returns An array of violations found during verification
|
|
51
|
+
*/
|
|
16
52
|
verify(fix?: boolean): Promise<Violation[]>;
|
|
17
53
|
private _createDocument;
|
|
18
54
|
private _createParseError;
|
package/lib/ml-core.js
CHANGED
|
@@ -1,100 +1,169 @@
|
|
|
1
|
-
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
-
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
-
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");
|
|
5
|
-
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
-
};
|
|
7
|
-
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
-
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");
|
|
10
|
-
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
-
};
|
|
12
|
-
var _MLCore_ast, _MLCore_document, _MLCore_filename, _MLCore_locale, _MLCore_parser, _MLCore_parserOptions, _MLCore_severity, _MLCore_pretenders, _MLCore_rules, _MLCore_ruleset, _MLCore_schemas, _MLCore_sourceCode, _MLCore_configErrors;
|
|
13
1
|
import { ParserError } from '@markuplint/parser-utils';
|
|
14
2
|
import { log, enableDebug } from './debug.js';
|
|
15
3
|
import { Document } from './ml-dom/index.js';
|
|
4
|
+
import { expandNamedNodeRules, expandNamedRules } from './virtual-rule.js';
|
|
16
5
|
const resultLog = log.extend('result');
|
|
6
|
+
/**
|
|
7
|
+
* The core linting engine for markuplint.
|
|
8
|
+
*
|
|
9
|
+
* Parses markup source code into an AST, constructs a DOM document,
|
|
10
|
+
* and verifies it against configured rules to produce violations.
|
|
11
|
+
*/
|
|
17
12
|
export class MLCore {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
13
|
+
#ast = null;
|
|
14
|
+
#document;
|
|
15
|
+
#filename;
|
|
16
|
+
#locale;
|
|
17
|
+
#parser;
|
|
18
|
+
#parserOptions;
|
|
19
|
+
#severity;
|
|
20
|
+
#pretenders;
|
|
21
|
+
#rules;
|
|
22
|
+
#ruleset;
|
|
23
|
+
#schemas;
|
|
24
|
+
#ruleCommonSettings;
|
|
25
|
+
#sourceCode;
|
|
26
|
+
#configErrors;
|
|
27
|
+
/**
|
|
28
|
+
* Pre-expansion nodeRules preserved for hot-reload.
|
|
29
|
+
* When `update()` is called without a new ruleset, these are used as the
|
|
30
|
+
* source for `expandNamedNodeRules()` instead of the already-transformed
|
|
31
|
+
* `#ruleset.nodeRules` (which has alias keys and no `name` property).
|
|
32
|
+
*/
|
|
33
|
+
#originalNodeRules;
|
|
34
|
+
#originalChildNodeRules;
|
|
35
|
+
/**
|
|
36
|
+
* Pre-computed namespace prefixes from wildcard disable entries.
|
|
37
|
+
* e.g., `rules["a11y/*"]: false` yields `"a11y/"`.
|
|
38
|
+
*/
|
|
39
|
+
#disabledNamespaces;
|
|
40
|
+
constructor({ parser, sourceCode, ruleset, rules, locale, schemas, ruleCommonSettings, parserOptions, severity, pretenders, filename, debug, configErrors, }) {
|
|
32
41
|
if (debug) {
|
|
33
42
|
enableDebug();
|
|
34
43
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
this.#parser = parser;
|
|
45
|
+
this.#sourceCode = sourceCode;
|
|
46
|
+
this.#parserOptions = parserOptions;
|
|
47
|
+
this.#locale = locale;
|
|
48
|
+
this.#schemas = schemas;
|
|
49
|
+
this.#ruleCommonSettings = ruleCommonSettings;
|
|
50
|
+
this.#filename = filename;
|
|
51
|
+
this.#severity = severity;
|
|
52
|
+
this.#pretenders = [...pretenders];
|
|
53
|
+
this.#configErrors = [...(configErrors ?? [])];
|
|
54
|
+
// Preserve pre-expansion nodeRules for hot-reload
|
|
55
|
+
this.#originalNodeRules = ruleset.nodeRules ?? [];
|
|
56
|
+
this.#originalChildNodeRules = ruleset.childNodeRules ?? [];
|
|
57
|
+
// Expand named rule groups in the rules section
|
|
58
|
+
const namedRulesResult = expandNamedRules(ruleset.rules ?? {}, rules);
|
|
59
|
+
// Expand named nodeRules into virtual rules (using expanded rules as base)
|
|
60
|
+
const allRulesForExpansion = [...rules, ...namedRulesResult.virtualRules];
|
|
61
|
+
const nodeRuleResult = expandNamedNodeRules(this.#originalNodeRules, allRulesForExpansion);
|
|
62
|
+
const childNodeRuleResult = expandNamedNodeRules(this.#originalChildNodeRules, allRulesForExpansion);
|
|
63
|
+
const resolvedRules = namedRulesResult.resolvedRules;
|
|
64
|
+
this.#rules = [...allRulesForExpansion, ...nodeRuleResult.virtualRules, ...childNodeRuleResult.virtualRules];
|
|
65
|
+
this.#ruleset = {
|
|
66
|
+
rules: resolvedRules,
|
|
67
|
+
nodeRules: nodeRuleResult.transformedNodeRules,
|
|
68
|
+
childNodeRules: childNodeRuleResult.transformedNodeRules,
|
|
69
|
+
};
|
|
70
|
+
this.#disabledNamespaces = extractDisabledNamespaces(resolvedRules);
|
|
71
|
+
this.#configErrors.push(...namedRulesResult.errors, ...nodeRuleResult.errors, ...childNodeRuleResult.errors);
|
|
50
72
|
this._parse();
|
|
51
73
|
this._createDocument();
|
|
52
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* The parsed document, or a {@link ParserError} if parsing failed.
|
|
77
|
+
*/
|
|
53
78
|
get document() {
|
|
54
|
-
return
|
|
79
|
+
return this.#document;
|
|
55
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Replaces the source code and re-parses the document.
|
|
83
|
+
*
|
|
84
|
+
* @param sourceCode - The new markup source code
|
|
85
|
+
*/
|
|
56
86
|
setCode(sourceCode) {
|
|
57
|
-
|
|
87
|
+
this.#sourceCode = sourceCode;
|
|
58
88
|
this._parse();
|
|
59
89
|
this._createDocument();
|
|
60
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Updates the linting configuration and re-creates the document.
|
|
93
|
+
* Only re-parses if parser options have changed.
|
|
94
|
+
*
|
|
95
|
+
* @param fabric - Partial fabric with the properties to update
|
|
96
|
+
*/
|
|
61
97
|
update({ parser, ruleset, rules, locale, schemas, parserOptions, configErrors }) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
98
|
+
this.#parser = parser ?? this.#parser;
|
|
99
|
+
this.#locale = locale ?? this.#locale;
|
|
100
|
+
this.#schemas = schemas ?? this.#schemas;
|
|
101
|
+
this.#configErrors = [...(configErrors ?? [])];
|
|
102
|
+
const baseRules = rules?.slice() ?? this.#rules.filter(r => !r.baseRuleId);
|
|
103
|
+
// Use pre-expansion originals as fallback when ruleset is not provided
|
|
104
|
+
const incomingNodeRules = ruleset?.nodeRules ?? this.#originalNodeRules;
|
|
105
|
+
const incomingChildNodeRules = ruleset?.childNodeRules ?? this.#originalChildNodeRules;
|
|
106
|
+
// Expand named rule groups in the rules section
|
|
107
|
+
const incomingRules = ruleset?.rules ?? this.#ruleset.rules;
|
|
108
|
+
const namedRulesResult = expandNamedRules(incomingRules, baseRules);
|
|
109
|
+
const allRulesForExpansion = [...baseRules, ...namedRulesResult.virtualRules];
|
|
110
|
+
const nodeRuleResult = expandNamedNodeRules(incomingNodeRules, allRulesForExpansion);
|
|
111
|
+
const childNodeRuleResult = expandNamedNodeRules(incomingChildNodeRules, allRulesForExpansion);
|
|
112
|
+
// Update originals if new data was provided
|
|
113
|
+
if (ruleset?.nodeRules) {
|
|
114
|
+
this.#originalNodeRules = ruleset.nodeRules;
|
|
115
|
+
}
|
|
116
|
+
if (ruleset?.childNodeRules) {
|
|
117
|
+
this.#originalChildNodeRules = ruleset.childNodeRules;
|
|
118
|
+
}
|
|
119
|
+
const resolvedRules = namedRulesResult.resolvedRules;
|
|
120
|
+
this.#rules = [...allRulesForExpansion, ...nodeRuleResult.virtualRules, ...childNodeRuleResult.virtualRules];
|
|
121
|
+
this.#ruleset = {
|
|
122
|
+
rules: resolvedRules,
|
|
123
|
+
nodeRules: nodeRuleResult.transformedNodeRules,
|
|
124
|
+
childNodeRules: childNodeRuleResult.transformedNodeRules,
|
|
125
|
+
};
|
|
126
|
+
this.#disabledNamespaces = extractDisabledNamespaces(resolvedRules);
|
|
127
|
+
this.#configErrors.push(...namedRulesResult.errors, ...nodeRuleResult.errors, ...childNodeRuleResult.errors);
|
|
72
128
|
if (parserOptions &&
|
|
73
|
-
(parserOptions.ignoreFrontMatter !==
|
|
74
|
-
parserOptions.authoredElementName !==
|
|
129
|
+
(parserOptions.ignoreFrontMatter !== this.#parserOptions.ignoreFrontMatter ||
|
|
130
|
+
parserOptions.authoredElementName !== this.#parserOptions.authoredElementName)) {
|
|
75
131
|
this._parse();
|
|
76
132
|
}
|
|
77
133
|
this._createDocument();
|
|
78
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Runs all configured rules against the parsed document and returns violations.
|
|
137
|
+
*
|
|
138
|
+
* If the document failed to parse, a single parse-error violation is returned
|
|
139
|
+
* (unless parse errors are suppressed via severity options).
|
|
140
|
+
*
|
|
141
|
+
* @param fix - Whether to attempt auto-fixing violations
|
|
142
|
+
* @returns An array of violations found during verification
|
|
143
|
+
*/
|
|
79
144
|
async verify(fix = false) {
|
|
80
145
|
log('verify: start');
|
|
81
146
|
const violations = [];
|
|
82
|
-
if (
|
|
83
|
-
const parseError = this._createParseError(
|
|
147
|
+
if (this.#document instanceof ParserError) {
|
|
148
|
+
const parseError = this._createParseError(this.#document.message, this.#document.line, this.#document.col, this.#document.raw);
|
|
84
149
|
if (!parseError) {
|
|
85
150
|
return [];
|
|
86
151
|
}
|
|
87
152
|
violations.push(parseError);
|
|
88
|
-
log('verify: error %o',
|
|
153
|
+
log('verify: error %o', this.#document.message);
|
|
89
154
|
return violations;
|
|
90
155
|
}
|
|
91
|
-
const definedRuleName = new Set(
|
|
156
|
+
const definedRuleName = new Set(this.#rules.map(rule => rule.name));
|
|
92
157
|
const setRuleNames = new Set([
|
|
93
|
-
...Object.keys(
|
|
94
|
-
...
|
|
95
|
-
...
|
|
158
|
+
...Object.keys(this.#ruleset.rules),
|
|
159
|
+
...this.#ruleset.nodeRules.flatMap(nodeRule => Object.keys(nodeRule.rules ?? {})),
|
|
160
|
+
...this.#ruleset.childNodeRules.flatMap(childNodeRule => Object.keys(childNodeRule.rules ?? {})),
|
|
96
161
|
]);
|
|
97
162
|
for (const setRuleName of setRuleNames) {
|
|
163
|
+
// Skip wildcard patterns (e.g., "a11y/*") — they are namespace disable entries, not rule references
|
|
164
|
+
if (setRuleName.endsWith('/*')) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
98
167
|
if (!definedRuleName.has(setRuleName)) {
|
|
99
168
|
violations.push({
|
|
100
169
|
ruleId: 'config-error',
|
|
@@ -106,7 +175,7 @@ export class MLCore {
|
|
|
106
175
|
});
|
|
107
176
|
}
|
|
108
177
|
}
|
|
109
|
-
for (const error of
|
|
178
|
+
for (const error of this.#configErrors) {
|
|
110
179
|
violations.push({
|
|
111
180
|
ruleId: 'config-error',
|
|
112
181
|
severity: 'warning',
|
|
@@ -116,13 +185,25 @@ export class MLCore {
|
|
|
116
185
|
raw: '',
|
|
117
186
|
});
|
|
118
187
|
}
|
|
119
|
-
for (const rule of
|
|
120
|
-
|
|
188
|
+
for (const rule of this.#rules) {
|
|
189
|
+
// For virtual rules, check disable conditions:
|
|
190
|
+
// 1. Exact name match: rules["alias/name"]: false
|
|
191
|
+
// 2. Group disable: rules["groupName"]: false (multi-entry named nodeRules)
|
|
192
|
+
// 3. Namespace wildcard: rules["scope/*"]: false
|
|
193
|
+
// Note: base rule name disable (rules["baseRuleName"]: false) is handled
|
|
194
|
+
// during expandNamedRules for named rule groups in the rules section.
|
|
195
|
+
if (rule.baseRuleId &&
|
|
196
|
+
(this.#ruleset.rules[rule.name] === false ||
|
|
197
|
+
(rule.groupName && this.#ruleset.rules[rule.groupName] === false) ||
|
|
198
|
+
this.#disabledNamespaces.some(ns => rule.name.startsWith(ns)))) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const ruleInfo = rule.getRuleInfo(this.#ruleset, rule.name);
|
|
121
202
|
if (ruleInfo.disabled && ruleInfo.nodeRules.length === 0 && ruleInfo.childNodeRules.length === 0) {
|
|
122
203
|
continue;
|
|
123
204
|
}
|
|
124
205
|
log('%s Rule: verify', rule.name);
|
|
125
|
-
const results = await rule.verify(
|
|
206
|
+
const results = await rule.verify(this.#document, this.#locale, fix).catch(error => {
|
|
126
207
|
if (error instanceof ParserError) {
|
|
127
208
|
return error;
|
|
128
209
|
}
|
|
@@ -159,21 +240,21 @@ export class MLCore {
|
|
|
159
240
|
return violations;
|
|
160
241
|
}
|
|
161
242
|
_createDocument() {
|
|
162
|
-
if (!
|
|
243
|
+
if (!this.#ast) {
|
|
163
244
|
return;
|
|
164
245
|
}
|
|
165
246
|
try {
|
|
166
|
-
|
|
167
|
-
filename:
|
|
168
|
-
endTag:
|
|
169
|
-
booleanish:
|
|
170
|
-
tagNameCaseSensitive:
|
|
171
|
-
pretenders:
|
|
172
|
-
})
|
|
247
|
+
this.#document = new Document(this.#ast, this.#ruleset, this.#schemas, this.#ruleCommonSettings, {
|
|
248
|
+
filename: this.#filename,
|
|
249
|
+
endTag: this.#parser.endTag,
|
|
250
|
+
booleanish: this.#parser.booleanish,
|
|
251
|
+
tagNameCaseSensitive: this.#parser.tagNameCaseSensitive,
|
|
252
|
+
pretenders: this.#pretenders,
|
|
253
|
+
});
|
|
173
254
|
}
|
|
174
255
|
catch (error) {
|
|
175
256
|
if (error instanceof ParserError) {
|
|
176
|
-
|
|
257
|
+
this.#document = error;
|
|
177
258
|
}
|
|
178
259
|
else {
|
|
179
260
|
throw error;
|
|
@@ -181,13 +262,13 @@ export class MLCore {
|
|
|
181
262
|
}
|
|
182
263
|
}
|
|
183
264
|
_createParseError(message, line, col, raw) {
|
|
184
|
-
if (
|
|
265
|
+
if (this.#severity.parseError === false || this.#severity.parseError === 'off') {
|
|
185
266
|
return null;
|
|
186
267
|
}
|
|
187
268
|
// Default severity is 'error'
|
|
188
|
-
const severity =
|
|
269
|
+
const severity = this.#severity.parseError === true || this.#severity.parseError == null
|
|
189
270
|
? 'error'
|
|
190
|
-
:
|
|
271
|
+
: this.#severity.parseError;
|
|
191
272
|
return {
|
|
192
273
|
ruleId: 'parse-error',
|
|
193
274
|
severity,
|
|
@@ -199,13 +280,13 @@ export class MLCore {
|
|
|
199
280
|
}
|
|
200
281
|
_parse() {
|
|
201
282
|
try {
|
|
202
|
-
|
|
283
|
+
this.#ast = this.#parser.parse(this.#sourceCode, this.#parserOptions);
|
|
203
284
|
}
|
|
204
285
|
catch (error) {
|
|
205
286
|
log('Caught the parse error: %O', error);
|
|
206
|
-
|
|
287
|
+
this.#ast = null;
|
|
207
288
|
if (error instanceof ParserError) {
|
|
208
|
-
|
|
289
|
+
this.#document = error;
|
|
209
290
|
}
|
|
210
291
|
else {
|
|
211
292
|
throw error;
|
|
@@ -213,4 +294,12 @@ export class MLCore {
|
|
|
213
294
|
}
|
|
214
295
|
}
|
|
215
296
|
}
|
|
216
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Extracts namespace prefixes from wildcard disable entries in rules.
|
|
299
|
+
* e.g., `{ "a11y/*": false }` yields `["a11y/"]`.
|
|
300
|
+
*/
|
|
301
|
+
function extractDisabledNamespaces(rules) {
|
|
302
|
+
return Object.entries(rules)
|
|
303
|
+
.filter(([key, value]) => key.endsWith('/*') && value === false)
|
|
304
|
+
.map(([key]) => key.slice(0, -1)); // "a11y/*" → "a11y/"
|
|
305
|
+
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import type { MLElement } from '../node/element.js';
|
|
2
2
|
import type { ARIAVersion } from '@markuplint/ml-spec';
|
|
3
|
+
/**
|
|
4
|
+
* Computes the accessible name for an MLElement using the HTML-AAM algorithm.
|
|
5
|
+
* Creates an MLCore-specific resolver that bridges MLElement to the AccnameResolver interface.
|
|
6
|
+
*
|
|
7
|
+
* @param el - The MLElement to compute the accessible name for
|
|
8
|
+
* @param version - The ARIA specification version to use for role resolution
|
|
9
|
+
* @returns The computed accessible name string, or an empty string on error
|
|
10
|
+
*/
|
|
3
11
|
export declare function getAccname(el: MLElement<any, any>, version: ARIAVersion): string;
|
|
@@ -1,71 +1,87 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { computeAccessibleName, escapeCSS, getComputedRole, EMBEDDED_CONTROL_ROLES, isNativeEmbeddedControl, } from '@markuplint/ml-spec';
|
|
2
2
|
import { log } from '../../debug.js';
|
|
3
3
|
const accnameLog = log.extend('accname');
|
|
4
|
+
/**
|
|
5
|
+
* Computes the accessible name for an MLElement using the HTML-AAM algorithm.
|
|
6
|
+
* Creates an MLCore-specific resolver that bridges MLElement to the AccnameResolver interface.
|
|
7
|
+
*
|
|
8
|
+
* @param el - The MLElement to compute the accessible name for
|
|
9
|
+
* @param version - The ARIA specification version to use for role resolution
|
|
10
|
+
* @returns The computed accessible name string, or an empty string on error
|
|
11
|
+
*/
|
|
4
12
|
export function getAccname(
|
|
5
13
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
6
14
|
el, version) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
if (isHidden(el)) {
|
|
16
|
-
return '';
|
|
17
|
-
}
|
|
18
|
-
if (isFromContent(el, version)) {
|
|
19
|
-
return [...el.childNodes]
|
|
20
|
-
.map(child => {
|
|
21
|
-
if (child.is(child.ELEMENT_NODE)) {
|
|
22
|
-
return getAccname(child, version);
|
|
23
|
-
}
|
|
24
|
-
if (child.is(child.TEXT_NODE)) {
|
|
25
|
-
return child.textContent ?? '';
|
|
26
|
-
}
|
|
27
|
-
return '';
|
|
28
|
-
})
|
|
29
|
-
.join('');
|
|
30
|
-
}
|
|
31
|
-
return '';
|
|
32
|
-
}
|
|
33
|
-
function safeGet(
|
|
34
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
35
|
-
el) {
|
|
15
|
+
// AccName computation involves DOM traversal, role resolution, and label
|
|
16
|
+
// lookup via querySelector. These may fail on unexpected DOM states (e.g.,
|
|
17
|
+
// detached nodes, malformed attributes) or incomplete spec data.
|
|
18
|
+
// A single element's failure must not abort the entire linting process,
|
|
19
|
+
// so we catch operational errors and return an empty name (= "unnamed").
|
|
20
|
+
// Programmer errors (TypeError, ReferenceError) are rethrown to avoid
|
|
21
|
+
// masking implementation bugs.
|
|
36
22
|
try {
|
|
37
|
-
const
|
|
38
|
-
|
|
23
|
+
const resolver = createMLCoreResolver(el, version);
|
|
24
|
+
const result = computeAccessibleName(el, resolver);
|
|
25
|
+
return result.name;
|
|
39
26
|
}
|
|
40
27
|
catch (error) {
|
|
28
|
+
if (error instanceof TypeError || error instanceof ReferenceError) {
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
41
31
|
accnameLog('Raw: %s', el.raw);
|
|
42
32
|
accnameLog('Error: %O', error);
|
|
43
33
|
return '';
|
|
44
34
|
}
|
|
45
35
|
}
|
|
46
|
-
function
|
|
47
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
48
|
-
el) {
|
|
49
|
-
if (el.pretenderContext?.type === 'pretender' && el.pretenderContext.aria?.name != null) {
|
|
50
|
-
if (typeof el.pretenderContext.aria.name === 'boolean') {
|
|
51
|
-
return 'some-name(Pretender Options)';
|
|
52
|
-
}
|
|
53
|
-
const attrName = el.pretenderContext.aria.name.fromAttr;
|
|
54
|
-
const attrValue = el.getAttributePretended(attrName);
|
|
55
|
-
if (attrValue) {
|
|
56
|
-
return attrValue;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return '';
|
|
60
|
-
}
|
|
61
|
-
function isHidden(
|
|
62
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
63
|
-
el) {
|
|
64
|
-
return el.getAttribute('aria-hidden') === 'true' || el.hasAttribute('hidden');
|
|
65
|
-
}
|
|
66
|
-
function isFromContent(
|
|
36
|
+
function createMLCoreResolver(
|
|
67
37
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
68
38
|
el, version) {
|
|
69
|
-
const
|
|
70
|
-
return
|
|
39
|
+
const doc = el.ownerMLDocument;
|
|
40
|
+
return {
|
|
41
|
+
getElementById(id) {
|
|
42
|
+
const found = doc.querySelector(`#${escapeCSS(id)}`);
|
|
43
|
+
return found ?? null;
|
|
44
|
+
},
|
|
45
|
+
getLabelsForId(id) {
|
|
46
|
+
const labels = doc.querySelectorAll(`label[for="${escapeCSS(id)}"]`);
|
|
47
|
+
return [...labels];
|
|
48
|
+
},
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
50
|
+
allowsNameFromContent(target) {
|
|
51
|
+
// TODO: Remove cast when getComputedRole accepts AccnameElement (#3178)
|
|
52
|
+
const role = getComputedRole(doc.specs, target, version);
|
|
53
|
+
return !!role.role?.accessibleNameFromContent;
|
|
54
|
+
},
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
56
|
+
isHidden(target) {
|
|
57
|
+
return target.getAttribute('aria-hidden') === 'true' || target.hasAttribute('hidden');
|
|
58
|
+
},
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
60
|
+
isEmbeddedControl(target) {
|
|
61
|
+
// TODO: Remove cast when getComputedRole accepts AccnameElement (#3178)
|
|
62
|
+
const role = getComputedRole(doc.specs, target, version);
|
|
63
|
+
if (role.role?.name) {
|
|
64
|
+
return EMBEDDED_CONTROL_ROLES.has(role.role.name);
|
|
65
|
+
}
|
|
66
|
+
return isNativeEmbeddedControl(target);
|
|
67
|
+
},
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
69
|
+
getPrecomputedName(target) {
|
|
70
|
+
// TODO: Remove cast when AccnameResolver is generic (#3178)
|
|
71
|
+
const targetEl = target;
|
|
72
|
+
if (!targetEl.pretenderContext || targetEl.pretenderContext.type !== 'pretender') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const ariaName = targetEl.pretenderContext.aria?.name;
|
|
76
|
+
if (ariaName == null) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (typeof ariaName === 'boolean') {
|
|
80
|
+
return 'some-name(Pretender Options)';
|
|
81
|
+
}
|
|
82
|
+
const attrName = ariaName.fromAttr;
|
|
83
|
+
const attrValue = targetEl.getAttributePretended(attrName);
|
|
84
|
+
return attrValue || null;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
71
87
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { MLNode } from '../node/node.js';
|
|
2
2
|
import type { MLText } from '../node/text.js';
|
|
3
3
|
/**
|
|
4
|
+
* Computes the indentation preceding the given node by analyzing
|
|
5
|
+
* the whitespace in adjacent text nodes.
|
|
4
6
|
*
|
|
5
7
|
* @deprecated
|
|
6
|
-
* @param node
|
|
8
|
+
* @param node - The node whose indentation to determine
|
|
9
|
+
* @returns An indentation object describing the whitespace, or null if no indentation is found
|
|
7
10
|
*/
|
|
8
11
|
export declare function getIndent(node: MLNode<any, any>): MLDOMIndentation | null;
|
|
9
12
|
declare class MLDOMIndentation {
|