@markuplint/rules 5.1.1 → 5.3.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/CHANGELOG.md +47 -0
- package/CLAUDE.md +19 -19
- package/lib/aria-prop-requires-role/index.js +2 -1
- package/lib/attr-check.js +6 -12
- package/lib/attr-eligibility.d.ts +15 -8
- package/lib/attr-eligibility.js +19 -10
- package/lib/attr-order/index.js +24 -18
- package/lib/case-sensitive-attr-name/index.js +24 -5
- package/lib/class-naming/index.js +6 -0
- package/lib/debug.d.ts +8 -1
- package/lib/debug.js +8 -1
- package/lib/element-supports-aria-prop/index.js +2 -1
- package/lib/form-attr-references-form/index.d.ts +6 -3
- package/lib/form-attr-references-form/index.js +6 -3
- package/lib/helpers.d.ts +111 -1
- package/lib/helpers.js +212 -2
- package/lib/input-list-references-datalist/index.d.ts +8 -6
- package/lib/input-list-references-datalist/index.js +8 -6
- package/lib/label-for-references-labelable/index.d.ts +8 -5
- package/lib/label-for-references-labelable/index.js +8 -5
- package/lib/landmark-roles/roles.js +14 -2
- package/lib/map-id-name-match/index.d.ts +3 -3
- package/lib/map-id-name-match/index.js +3 -3
- package/lib/no-ambiguous-navigable-target-names/index.js +2 -1
- package/lib/no-aria-hidden-on-hidden-until-found/index.d.ts +9 -6
- package/lib/no-aria-hidden-on-hidden-until-found/index.js +9 -6
- package/lib/no-aria-hidden-on-hidden-until-found/meta.d.ts +1 -1
- package/lib/no-aria-hidden-on-hidden-until-found/meta.js +1 -1
- package/lib/no-aria-on-unsupported-element/index.js +2 -2
- package/lib/no-aria-on-unsupported-element/meta.d.ts +1 -1
- package/lib/no-aria-on-unsupported-element/meta.js +1 -1
- package/lib/no-boolean-attr-value/index.js +2 -2
- package/lib/no-content-after-body/index.d.ts +2 -2
- package/lib/no-content-after-body/index.js +2 -2
- package/lib/no-contradictory-aria-prop/index.js +2 -1
- package/lib/no-default-aria-value/index.js +2 -1
- package/lib/no-default-value/index.js +2 -2
- package/lib/no-deprecated-aria-prop/index.js +2 -1
- package/lib/no-deprecated-attr/index.js +2 -1
- package/lib/no-disallowed-attr/index.d.ts +7 -1
- package/lib/no-disallowed-attr/index.js +13 -20
- package/lib/no-duplicate-attr/index.js +1 -2
- package/lib/no-duplicate-autofocus/index.js +2 -2
- package/lib/no-duplicate-h1/index.d.ts +1 -1
- package/lib/no-duplicate-h1/index.js +1 -1
- package/lib/no-empty-table-track/index.d.ts +6 -2
- package/lib/no-empty-table-track/index.js +6 -2
- package/lib/no-experimental-features/index.d.ts +5 -1
- package/lib/no-experimental-features/index.js +6 -8
- package/lib/no-hardcoded-id/index.js +1 -1
- package/lib/no-ineffective-attr/index.js +2 -2
- package/lib/no-input-file-value/index.d.ts +10 -4
- package/lib/no-input-file-value/index.js +10 -4
- package/lib/no-invalid-aria-prop-value/index.js +2 -1
- package/lib/no-invalid-attr-value/index.d.ts +3 -0
- package/lib/no-invalid-attr-value/index.js +9 -24
- package/lib/no-nonstandard-features/index.d.ts +5 -1
- package/lib/no-nonstandard-features/index.js +6 -8
- package/lib/no-obsolete-attr/index.js +3 -3
- package/lib/no-prohibited-naming/index.js +2 -1
- package/lib/no-redundant-accessible-name/index.js +17 -1
- package/lib/no-redundant-aria-prop/index.js +2 -1
- package/lib/no-refer-to-non-existent-id/index.js +4 -4
- package/lib/no-restricted-attr/index.d.ts +4 -0
- package/lib/no-restricted-attr/index.js +5 -22
- package/lib/no-stray-head-or-body-tag/index.d.ts +6 -4
- package/lib/no-stray-head-or-body-tag/index.js +11 -6
- package/lib/no-unknown-attr/index.d.ts +7 -1
- package/lib/no-unknown-attr/index.js +11 -25
- package/lib/no-unpaired-srcset-sizes/index.d.ts +3 -2
- package/lib/no-unpaired-srcset-sizes/index.js +23 -17
- package/lib/no-unsupported-browser-features/compat-data.d.ts +15 -2
- package/lib/no-unsupported-browser-features/compat-data.js +48 -3
- package/lib/no-unsupported-browser-features/index.d.ts +5 -1
- package/lib/no-unsupported-browser-features/index.js +6 -7
- package/lib/permitted-contents/choice.js +19 -8
- package/lib/permitted-contents/content-model.d.ts +24 -1
- package/lib/permitted-contents/content-model.js +56 -9
- package/lib/permitted-contents/count-pattern.js +36 -13
- package/lib/permitted-contents/debug.browser.d.ts +18 -0
- package/lib/permitted-contents/debug.browser.js +27 -0
- package/lib/permitted-contents/debug.d.ts +18 -0
- package/lib/permitted-contents/debug.js +27 -0
- package/lib/permitted-contents/index.js +2 -2
- package/lib/permitted-contents/matches-selector.js +40 -18
- package/lib/permitted-contents/order.js +23 -11
- package/lib/permitted-contents/represent-transparent-nodes.js +3 -1
- package/lib/permitted-contents/transparent.js +6 -2
- package/lib/placeholder-label-option/index.d.ts +3 -3
- package/lib/placeholder-label-option/index.js +163 -37
- package/lib/progress-value-bounds/index.d.ts +1 -1
- package/lib/progress-value-bounds/index.js +1 -1
- package/lib/require-attr/index.d.ts +6 -1
- package/lib/require-attr/index.js +13 -5
- package/lib/require-h1/index.d.ts +1 -1
- package/lib/require-h1/index.js +1 -1
- package/lib/role-supports-aria-prop/index.js +2 -1
- package/lib/sizes-auto-requires-lazy-loading/index.d.ts +3 -1
- package/lib/sizes-auto-requires-lazy-loading/index.js +30 -16
- package/lib/srcset-sizes-constraint/parse-srcset.d.ts +6 -5
- package/lib/srcset-sizes-constraint/parse-srcset.js +8 -7
- package/lib/srcset-sizes-constraint/sibling-helpers.d.ts +31 -0
- package/lib/srcset-sizes-constraint/sibling-helpers.js +58 -0
- package/lib/valid-speculation-rules/index.js +1 -1
- package/lib/wai-aria/checkings/contradictory-aria-prop.js +3 -5
- package/lib/wai-aria/checkings/default-value.js +1 -1
- package/lib/wai-aria/checkings/deprecated-props.js +2 -2
- package/lib/wai-aria/checkings/element-supports-aria-prop.js +5 -4
- package/lib/wai-aria/checkings/interaction-in-hidden.d.ts +1 -1
- package/lib/wai-aria/checkings/interaction-in-hidden.js +1 -1
- package/lib/wai-aria/checkings/no-global-prop.js +1 -1
- package/lib/wai-aria/checkings/presentational-children.d.ts +4 -4
- package/lib/wai-aria/checkings/presentational-children.js +6 -5
- package/lib/wai-aria/checkings/prohibited-naming.js +5 -4
- package/lib/wai-aria/checkings/redundant-aria-prop.js +3 -5
- package/lib/wai-aria/checkings/required-prop.js +1 -4
- package/lib/wai-aria/checkings/role-supports-aria-prop.js +4 -3
- package/lib/wai-aria/checkings/tab-requires-tabpanel.d.ts +3 -2
- package/lib/wai-aria/checkings/tab-requires-tabpanel.js +3 -2
- package/lib/wai-aria/checkings/value.d.ts +1 -1
- package/lib/wai-aria/checkings/value.js +2 -2
- package/package.json +19 -19
|
@@ -81,30 +81,77 @@ export function resolveContentModel(
|
|
|
81
81
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
82
82
|
el, rules, specs, mode) {
|
|
83
83
|
if (mode === 'origin' && el.pretenderContext?.type === 'pretender') {
|
|
84
|
-
const userRule = rules.find(r => r
|
|
85
|
-
return userRule
|
|
84
|
+
const userRule = rules.find(r => hasTagRule(el, r));
|
|
85
|
+
return userRule ? contentsOfTagRule(el, userRule) : null;
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
const model = getContentModel(el, specs.specs);
|
|
88
|
+
if (model != null || el.pretenderContext?.type === 'pretender') {
|
|
89
|
+
return model;
|
|
90
|
+
}
|
|
91
|
+
// A rule written for a component under another spelling of its name (`FooBar` for `<foo-bar>`):
|
|
92
|
+
// the lookup of the spec is by the name as written, which the element does not have.
|
|
93
|
+
const userRule = rules.find(r => el.matchesTagNameVariant(r.tag));
|
|
94
|
+
if (!userRule) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return contentsOfTagRule(el, userRule);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The contents of a user's tag rule for the element: those of the first `conditional` entry whose
|
|
101
|
+
* condition the element meets, or else the rule's own, as `getContentModel` of `@markuplint/ml-spec`
|
|
102
|
+
* reads a spec. It is for the lookups that find the rule without `getContentModel`.
|
|
103
|
+
*
|
|
104
|
+
* @param el - The element
|
|
105
|
+
* @param rule - The user's tag rule written for the element
|
|
106
|
+
* @returns The contents that apply to the element
|
|
107
|
+
*/
|
|
108
|
+
function contentsOfTagRule(
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
110
|
+
el, rule) {
|
|
111
|
+
return rule.conditional?.find(({ condition }) => el.matches(condition))?.contents ?? rule.contents;
|
|
88
112
|
}
|
|
89
113
|
/**
|
|
90
|
-
*
|
|
114
|
+
* Whether the user's tag rule is written for the element: the name of the rule is the name
|
|
115
|
+
* the element is written with, or another spelling of it that the document resolves to the
|
|
116
|
+
* same component (see `MLElement#matchesTagNameVariant`).
|
|
117
|
+
*
|
|
118
|
+
* @param el - The element
|
|
119
|
+
* @param rule - The user's tag rule
|
|
120
|
+
* @returns Whether the rule is written for the element
|
|
121
|
+
*/
|
|
122
|
+
export function hasTagRule(
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
124
|
+
el, rule) {
|
|
125
|
+
return rule.tag === el.rawName || el.matchesTagNameVariant(rule.tag);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Cache for merged specs: for each base spec of a document, keyed by the JSON-serialized tag rules.
|
|
91
129
|
* Avoids re-merging the same set of user-defined tag rules on every element check.
|
|
92
130
|
*/
|
|
93
|
-
const caches = new
|
|
131
|
+
const caches = new WeakMap();
|
|
94
132
|
/**
|
|
95
133
|
* Returns a Specs object that merges the base ML spec with user-defined
|
|
96
|
-
* tag rules. Results are cached by the serialized rules to avoid redundant
|
|
134
|
+
* tag rules. Results are cached by the specs of the document and the serialized rules to avoid redundant
|
|
135
|
+
* merging: the specs differ by document (the `specs` of the config is by file), so what is merged
|
|
136
|
+
* for one must not be given to another.
|
|
137
|
+
*
|
|
138
|
+
* Exported for the spec of the cache.
|
|
97
139
|
*
|
|
98
140
|
* @param specs - The base ML spec from the document.
|
|
99
141
|
* @param rules - User-defined tag rules to merge.
|
|
100
142
|
* @returns The merged Specs, either from cache or freshly computed.
|
|
101
143
|
*/
|
|
102
|
-
function cachedSpecs(specs, rules) {
|
|
144
|
+
export function cachedSpecs(specs, rules) {
|
|
103
145
|
if (rules.length === 0) {
|
|
104
146
|
return specs;
|
|
105
147
|
}
|
|
106
148
|
const key = JSON.stringify(rules);
|
|
107
|
-
|
|
149
|
+
let cache = caches.get(specs);
|
|
150
|
+
if (!cache) {
|
|
151
|
+
cache = new Map();
|
|
152
|
+
caches.set(specs, cache);
|
|
153
|
+
}
|
|
154
|
+
const cached = cache.get(key);
|
|
108
155
|
if (cached) {
|
|
109
156
|
return cached;
|
|
110
157
|
}
|
|
@@ -118,6 +165,6 @@ function cachedSpecs(specs, rules) {
|
|
|
118
165
|
})),
|
|
119
166
|
],
|
|
120
167
|
};
|
|
121
|
-
|
|
168
|
+
cache.set(key, merged);
|
|
122
169
|
return merged;
|
|
123
170
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cmLog } from './debug.js';
|
|
1
|
+
import { cmLog, depthLog } from './debug.js';
|
|
2
2
|
import { recursiveBranch } from './recursive-branch.js';
|
|
3
3
|
import { Collection, mergeHints, modelLog, normalizeModel } from './utils.js';
|
|
4
4
|
const cLog = cmLog.extend('countCompereResult');
|
|
@@ -24,21 +24,28 @@ const cLog = cmLog.extend('countCompereResult');
|
|
|
24
24
|
export function countPattern(pattern,
|
|
25
25
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
26
26
|
childNodes, rules, specs, options, depth, mode) {
|
|
27
|
-
const ptLog =
|
|
27
|
+
const ptLog = depthLog('countPattern', depth);
|
|
28
|
+
const ptEnabled = ptLog.enabled;
|
|
28
29
|
const collection = new Collection(childNodes);
|
|
29
30
|
const { model, min, max, repeat, missingType } = normalizeModel(pattern);
|
|
30
|
-
|
|
31
|
+
if (ptEnabled) {
|
|
32
|
+
ptLog('Model:\n RegEx: %s\n Schema: %o', modelLog(model, repeat), pattern);
|
|
33
|
+
}
|
|
31
34
|
let prevResult = null;
|
|
32
35
|
let barelyResult = null;
|
|
33
36
|
let loopCount = 0;
|
|
34
37
|
while (true) {
|
|
35
38
|
loopCount++;
|
|
36
|
-
|
|
39
|
+
if (ptEnabled) {
|
|
40
|
+
ptLog('Check#%s: %s', loopCount, collection);
|
|
41
|
+
}
|
|
37
42
|
const result = recursiveBranch(model, collection.unmatched, rules, specs, options, depth, mode);
|
|
38
43
|
const added = collection.addMatched(result.matched);
|
|
39
44
|
const { matchedCount } = collection;
|
|
40
45
|
if (result.type === 'UNMATCHED_SELECTOR_BUT_MAY_EMPTY') {
|
|
41
|
-
|
|
46
|
+
if (ptEnabled) {
|
|
47
|
+
ptLog('MATCHED_ZERO:\n model: %s\n max: %s\n collection: %s\n matched element: %s', modelLog(model, repeat), max, collection, matchedCount);
|
|
48
|
+
}
|
|
42
49
|
return compereResult({
|
|
43
50
|
type: 'MATCHED_ZERO',
|
|
44
51
|
matched: collection.matched,
|
|
@@ -50,7 +57,9 @@ childNodes, rules, specs, options, depth, mode) {
|
|
|
50
57
|
}
|
|
51
58
|
if (max < collection.matchedCount) {
|
|
52
59
|
collection.max(max);
|
|
53
|
-
|
|
60
|
+
if (ptEnabled) {
|
|
61
|
+
ptLog('UNEXPECTED_EXTRA_NODE:\n model: %s\n max: %s\n collection: %s\n matched element: %s', modelLog(model, repeat), max, collection, matchedCount);
|
|
62
|
+
}
|
|
54
63
|
return compereResult({
|
|
55
64
|
type: 'UNEXPECTED_EXTRA_NODE',
|
|
56
65
|
matched: collection.matched,
|
|
@@ -64,7 +73,9 @@ childNodes, rules, specs, options, depth, mode) {
|
|
|
64
73
|
if (result.type === 'MISSING_NODE_ONE_OR_MORE' ||
|
|
65
74
|
result.type === 'MISSING_NODE_REQUIRED' ||
|
|
66
75
|
result.type === 'TRANSPARENT_MODEL_DISALLOWS') {
|
|
67
|
-
|
|
76
|
+
if (ptEnabled) {
|
|
77
|
+
ptLog('%s(continued): %s; Needs', result.type, collection, result.query);
|
|
78
|
+
}
|
|
68
79
|
return compereResult({
|
|
69
80
|
type: result.type,
|
|
70
81
|
matched: collection.matched,
|
|
@@ -74,7 +85,9 @@ childNodes, rules, specs, options, depth, mode) {
|
|
|
74
85
|
hint: result.hint,
|
|
75
86
|
}, barelyResult);
|
|
76
87
|
}
|
|
77
|
-
|
|
88
|
+
if (ptEnabled) {
|
|
89
|
+
ptLog('%s(continued): %s', prevResult.type, collection);
|
|
90
|
+
}
|
|
78
91
|
return compereResult(prevResult, barelyResult);
|
|
79
92
|
}
|
|
80
93
|
if (added && collection.unmatched.length > 0) {
|
|
@@ -88,7 +101,9 @@ childNodes, rules, specs, options, depth, mode) {
|
|
|
88
101
|
hint: result.hint,
|
|
89
102
|
};
|
|
90
103
|
}
|
|
91
|
-
|
|
104
|
+
if (ptEnabled) {
|
|
105
|
+
ptLog('continue⤴️');
|
|
106
|
+
}
|
|
92
107
|
continue;
|
|
93
108
|
}
|
|
94
109
|
if (collection.matchedCount + (result.zeroMatch ? 1 : 0) < min) {
|
|
@@ -97,7 +112,9 @@ childNodes, rules, specs, options, depth, mode) {
|
|
|
97
112
|
result.type === 'TRANSPARENT_MODEL_DISALLOWS'
|
|
98
113
|
? result.type
|
|
99
114
|
: (missingType ?? 'MISSING_NODE_REQUIRED');
|
|
100
|
-
|
|
115
|
+
if (ptEnabled) {
|
|
116
|
+
ptLog('%s(in %s); Needs %s', resultType, missingType, result.query);
|
|
117
|
+
}
|
|
101
118
|
return compereResult({
|
|
102
119
|
type: resultType,
|
|
103
120
|
matched: collection.matched,
|
|
@@ -129,10 +146,14 @@ childNodes, rules, specs, options, depth, mode) {
|
|
|
129
146
|
};
|
|
130
147
|
if (collection.unmatched.length > 0) {
|
|
131
148
|
prevResult = matchedResult;
|
|
132
|
-
|
|
149
|
+
if (ptEnabled) {
|
|
150
|
+
ptLog('continue⤴️ (add prev)');
|
|
151
|
+
}
|
|
133
152
|
continue;
|
|
134
153
|
}
|
|
135
|
-
|
|
154
|
+
if (ptEnabled) {
|
|
155
|
+
ptLog('%s:\n model: %s\n max: %s\n collection: %s\n matched element: %s', resultType, modelLog(model, repeat), max, collection, matchedCount);
|
|
156
|
+
}
|
|
136
157
|
// When min=0 and no elements were consumed, the pattern legitimately
|
|
137
158
|
// matched zero times — don't propagate the inner sequence's failure.
|
|
138
159
|
// This enables zeroOrMore([oneOrMore dt, oneOrMore dd]) to accept
|
|
@@ -168,7 +189,9 @@ function compereResult(
|
|
|
168
189
|
a,
|
|
169
190
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
170
191
|
b) {
|
|
171
|
-
cLog
|
|
192
|
+
if (cLog.enabled) {
|
|
193
|
+
cLog('current: %s %O\nbarely: %s %O', a.type, a.hint, b?.type, b?.hint);
|
|
194
|
+
}
|
|
172
195
|
if (b == null) {
|
|
173
196
|
return a;
|
|
174
197
|
}
|
|
@@ -5,6 +5,24 @@ import type { Log } from '../debug.js';
|
|
|
5
5
|
* In the browser environment, color helpers are no-op stubs.
|
|
6
6
|
*/
|
|
7
7
|
export declare const cmLog: Log;
|
|
8
|
+
/**
|
|
9
|
+
* Returns the `cmLog` child logger for `<name>#<depth>`, created once per
|
|
10
|
+
* name and depth. `debug`'s `extend()` allocates a logger on every call,
|
|
11
|
+
* which showed up in profiles when it ran per recursion step. Use this instead
|
|
12
|
+
* of calling `cmLog.extend()` for per-depth names.
|
|
13
|
+
*
|
|
14
|
+
* The cache is bounded by the number of distinct name × depth pairs. The returned
|
|
15
|
+
* logger is shared by every caller, so setting `.enabled` or `.log` on it affects
|
|
16
|
+
* all of them.
|
|
17
|
+
*
|
|
18
|
+
* Keep in sync with `debug.ts`: `package.json` swaps this file in for that one in
|
|
19
|
+
* the browser build.
|
|
20
|
+
*
|
|
21
|
+
* @param name - The logger's base name, e.g. `order`.
|
|
22
|
+
* @param depth - The recursion depth the logger belongs to.
|
|
23
|
+
* @returns The shared child logger for `<name>#<depth>`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function depthLog(name: string, depth: number): Log;
|
|
8
26
|
/** No-op color stub for browser environments: green background. */
|
|
9
27
|
export declare const bgGreen: {
|
|
10
28
|
(): void;
|
|
@@ -5,6 +5,33 @@ import { log } from '../debug.js';
|
|
|
5
5
|
* In the browser environment, color helpers are no-op stubs.
|
|
6
6
|
*/
|
|
7
7
|
export const cmLog = log.extend('content-model');
|
|
8
|
+
const depthLogs = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* Returns the `cmLog` child logger for `<name>#<depth>`, created once per
|
|
11
|
+
* name and depth. `debug`'s `extend()` allocates a logger on every call,
|
|
12
|
+
* which showed up in profiles when it ran per recursion step. Use this instead
|
|
13
|
+
* of calling `cmLog.extend()` for per-depth names.
|
|
14
|
+
*
|
|
15
|
+
* The cache is bounded by the number of distinct name × depth pairs. The returned
|
|
16
|
+
* logger is shared by every caller, so setting `.enabled` or `.log` on it affects
|
|
17
|
+
* all of them.
|
|
18
|
+
*
|
|
19
|
+
* Keep in sync with `debug.ts`: `package.json` swaps this file in for that one in
|
|
20
|
+
* the browser build.
|
|
21
|
+
*
|
|
22
|
+
* @param name - The logger's base name, e.g. `order`.
|
|
23
|
+
* @param depth - The recursion depth the logger belongs to.
|
|
24
|
+
* @returns The shared child logger for `<name>#<depth>`.
|
|
25
|
+
*/
|
|
26
|
+
export function depthLog(name, depth) {
|
|
27
|
+
const key = `${name}#${depth}`;
|
|
28
|
+
let logger = depthLogs.get(key);
|
|
29
|
+
if (!logger) {
|
|
30
|
+
logger = cmLog.extend(key);
|
|
31
|
+
depthLogs.set(key, logger);
|
|
32
|
+
}
|
|
33
|
+
return logger;
|
|
34
|
+
}
|
|
8
35
|
const fn = () => { };
|
|
9
36
|
fn.bold = () => { };
|
|
10
37
|
/** No-op color stub for browser environments: green background. */
|
|
@@ -6,6 +6,24 @@ import color from 'ansi-colors';
|
|
|
6
6
|
* for structured debug output during content model evaluation.
|
|
7
7
|
*/
|
|
8
8
|
export declare const cmLog: Log;
|
|
9
|
+
/**
|
|
10
|
+
* Returns the `cmLog` child logger for `<name>#<depth>`, created once per
|
|
11
|
+
* name and depth. `debug`'s `extend()` allocates a logger on every call,
|
|
12
|
+
* which showed up in profiles when it ran per recursion step. Use this instead
|
|
13
|
+
* of calling `cmLog.extend()` for per-depth names.
|
|
14
|
+
*
|
|
15
|
+
* The cache is bounded by the number of distinct name × depth pairs. The returned
|
|
16
|
+
* logger is shared by every caller, so setting `.enabled` or `.log` on it affects
|
|
17
|
+
* all of them.
|
|
18
|
+
*
|
|
19
|
+
* Keep in sync with `debug.browser.ts`: `package.json` swaps that file in for this
|
|
20
|
+
* one in the browser build.
|
|
21
|
+
*
|
|
22
|
+
* @param name - The logger's base name, e.g. `order`.
|
|
23
|
+
* @param depth - The recursion depth the logger belongs to.
|
|
24
|
+
* @returns The shared child logger for `<name>#<depth>`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function depthLog(name: string, depth: number): Log;
|
|
9
27
|
/** ANSI color helper: green background for highlighting locked-and-matched nodes in debug output. */
|
|
10
28
|
export declare const bgGreen: color.StyleFunction;
|
|
11
29
|
/** ANSI color helper: green foreground for highlighting matched nodes in debug output. */
|
|
@@ -6,6 +6,33 @@ import { log } from '../debug.js';
|
|
|
6
6
|
* for structured debug output during content model evaluation.
|
|
7
7
|
*/
|
|
8
8
|
export const cmLog = log.extend('content-model');
|
|
9
|
+
const depthLogs = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* Returns the `cmLog` child logger for `<name>#<depth>`, created once per
|
|
12
|
+
* name and depth. `debug`'s `extend()` allocates a logger on every call,
|
|
13
|
+
* which showed up in profiles when it ran per recursion step. Use this instead
|
|
14
|
+
* of calling `cmLog.extend()` for per-depth names.
|
|
15
|
+
*
|
|
16
|
+
* The cache is bounded by the number of distinct name × depth pairs. The returned
|
|
17
|
+
* logger is shared by every caller, so setting `.enabled` or `.log` on it affects
|
|
18
|
+
* all of them.
|
|
19
|
+
*
|
|
20
|
+
* Keep in sync with `debug.browser.ts`: `package.json` swaps that file in for this
|
|
21
|
+
* one in the browser build.
|
|
22
|
+
*
|
|
23
|
+
* @param name - The logger's base name, e.g. `order`.
|
|
24
|
+
* @param depth - The recursion depth the logger belongs to.
|
|
25
|
+
* @returns The shared child logger for `<name>#<depth>`.
|
|
26
|
+
*/
|
|
27
|
+
export function depthLog(name, depth) {
|
|
28
|
+
const key = `${name}#${depth}`;
|
|
29
|
+
let logger = depthLogs.get(key);
|
|
30
|
+
if (!logger) {
|
|
31
|
+
logger = cmLog.extend(key);
|
|
32
|
+
depthLogs.set(key, logger);
|
|
33
|
+
}
|
|
34
|
+
return logger;
|
|
35
|
+
}
|
|
9
36
|
/** ANSI color helper: green background for highlighting locked-and-matched nodes in debug output. */
|
|
10
37
|
export const bgGreen = color.bgGreen;
|
|
11
38
|
/** ANSI color helper: green foreground for highlighting matched nodes in debug output. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
-
import { contentModel } from './content-model.js';
|
|
2
|
+
import { contentModel, hasTagRule } from './content-model.js';
|
|
3
3
|
import meta from './meta.js';
|
|
4
4
|
import { transparentMode } from './represent-transparent-nodes.js';
|
|
5
5
|
import { getContentOwner, getReportScope, isContentMutable } from './slot-content.js';
|
|
@@ -18,7 +18,7 @@ export default createRule({
|
|
|
18
18
|
// that user-defined custom-tag rules are not silently bypassed by pretending
|
|
19
19
|
// (see issue #3739).
|
|
20
20
|
const modes = ['pretended'];
|
|
21
|
-
if (el.pretenderContext?.type === 'pretender' && el.rule.value.some(r => r
|
|
21
|
+
if (el.pretenderContext?.type === 'pretender' && el.rule.value.some(r => hasTagRule(el, r))) {
|
|
22
22
|
modes.push('origin');
|
|
23
23
|
}
|
|
24
24
|
for (const mode of modes) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { contentModelCategoryToTagNames } from '@markuplint/ml-spec';
|
|
2
|
-
import {
|
|
2
|
+
import { depthLog } from './debug.js';
|
|
3
3
|
import { cleanObject, matches } from './utils.js';
|
|
4
4
|
/**
|
|
5
5
|
* Tests whether a single child node matches a content model query selector.
|
|
@@ -18,11 +18,14 @@ import { cleanObject, matches } from './utils.js';
|
|
|
18
18
|
export function matchesSelector(query,
|
|
19
19
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
20
20
|
childNode, specs, depth, mode) {
|
|
21
|
-
const nodeLog =
|
|
21
|
+
const nodeLog = depthLog('node', depth);
|
|
22
|
+
const nodeEnabled = nodeLog.enabled;
|
|
22
23
|
const { selector, hasText, hasNonEmptyText, hasCustom } = optCondition(query, specs);
|
|
23
24
|
if (childNode == null) {
|
|
24
25
|
if (hasText) {
|
|
25
|
-
|
|
26
|
+
if (nodeEnabled) {
|
|
27
|
+
nodeLog('<#text>.matches(%s) => ""', query);
|
|
28
|
+
}
|
|
26
29
|
return {
|
|
27
30
|
type: 'MATCHED_ZERO',
|
|
28
31
|
matched: [],
|
|
@@ -45,7 +48,9 @@ childNode, specs, depth, mode) {
|
|
|
45
48
|
if (childNode.is(childNode.TEXT_NODE)) {
|
|
46
49
|
if (hasNonEmptyText) {
|
|
47
50
|
if (childNode.isWhitespace()) {
|
|
48
|
-
|
|
51
|
+
if (nodeEnabled) {
|
|
52
|
+
nodeLog('<#nonEmptyText>.matches(%s) => WHITESPACE (rejected)', query);
|
|
53
|
+
}
|
|
49
54
|
return {
|
|
50
55
|
type: 'MISSING_NODE',
|
|
51
56
|
matched: [],
|
|
@@ -55,7 +60,9 @@ childNode, specs, depth, mode) {
|
|
|
55
60
|
hint: {},
|
|
56
61
|
};
|
|
57
62
|
}
|
|
58
|
-
|
|
63
|
+
if (nodeEnabled) {
|
|
64
|
+
nodeLog('<#nonEmptyText>.matches(%s) => "%s"', query, childNode.raw.trim());
|
|
65
|
+
}
|
|
59
66
|
return {
|
|
60
67
|
type: 'MATCHED',
|
|
61
68
|
matched: [childNode],
|
|
@@ -66,7 +73,9 @@ childNode, specs, depth, mode) {
|
|
|
66
73
|
};
|
|
67
74
|
}
|
|
68
75
|
if (hasText) {
|
|
69
|
-
|
|
76
|
+
if (nodeEnabled) {
|
|
77
|
+
nodeLog('<#text>.matches(%s) => "%s"', query, childNode.raw.trim());
|
|
78
|
+
}
|
|
70
79
|
return {
|
|
71
80
|
type: 'MATCHED',
|
|
72
81
|
matched: [childNode],
|
|
@@ -77,7 +86,9 @@ childNode, specs, depth, mode) {
|
|
|
77
86
|
};
|
|
78
87
|
}
|
|
79
88
|
if (childNode.isWhitespace()) {
|
|
80
|
-
|
|
89
|
+
if (nodeEnabled) {
|
|
90
|
+
nodeLog('<#text>.matches(%s) => WHITESPACE', query);
|
|
91
|
+
}
|
|
81
92
|
return {
|
|
82
93
|
type: 'MATCHED',
|
|
83
94
|
matched: [childNode],
|
|
@@ -98,7 +109,9 @@ childNode, specs, depth, mode) {
|
|
|
98
109
|
};
|
|
99
110
|
}
|
|
100
111
|
if (childNode.is(childNode.MARKUPLINT_PREPROCESSOR_BLOCK)) {
|
|
101
|
-
|
|
112
|
+
if (nodeEnabled) {
|
|
113
|
+
nodeLog('%s.matches(%s) => PBlock', childNode.raw, query);
|
|
114
|
+
}
|
|
102
115
|
return {
|
|
103
116
|
type: 'MATCHED',
|
|
104
117
|
matched: [childNode],
|
|
@@ -110,7 +123,9 @@ childNode, specs, depth, mode) {
|
|
|
110
123
|
}
|
|
111
124
|
if (childNode.is(childNode.ELEMENT_NODE)) {
|
|
112
125
|
if (hasCustom && isTreatedAsCustom(childNode, mode)) {
|
|
113
|
-
|
|
126
|
+
if (nodeEnabled) {
|
|
127
|
+
nodeLog('%s.matches(%s) => CustomElement', childNode.raw, query);
|
|
128
|
+
}
|
|
114
129
|
return {
|
|
115
130
|
type: 'MATCHED',
|
|
116
131
|
matched: [childNode],
|
|
@@ -121,7 +136,9 @@ childNode, specs, depth, mode) {
|
|
|
121
136
|
};
|
|
122
137
|
}
|
|
123
138
|
const result = matches(selector, childNode, specs, mode);
|
|
124
|
-
|
|
139
|
+
if (nodeEnabled) {
|
|
140
|
+
nodeLog('%s.matches(%s) => %s', childNode.raw, query, result.matched);
|
|
141
|
+
}
|
|
125
142
|
if (result.matched) {
|
|
126
143
|
return {
|
|
127
144
|
type: 'MATCHED',
|
|
@@ -172,25 +189,30 @@ childNode, specs, depth, mode) {
|
|
|
172
189
|
* pretended, so a component that stands for `<section>` would still look
|
|
173
190
|
* authored and slip through `<p>`, `<span>` and `<button>`. In `'pretended'`
|
|
174
191
|
* mode such an element is judged by what it stands for instead, exactly as if
|
|
175
|
-
* the user had written that element
|
|
176
|
-
*
|
|
177
|
-
*
|
|
192
|
+
* the user had written that element, and that comes first: a parser that cannot
|
|
193
|
+
* tell a component from an element (HTML sees `<MyTabs>` as `html`) still gives a
|
|
194
|
+
* pretender. The pretender target is created with `elementType: 'html'`, so a
|
|
195
|
+
* target that is itself a custom element (its name contains a hyphen) is
|
|
196
|
+
* recognised by name. A target that is a component name (`Tabs.Root`, where
|
|
197
|
+
* the chain of `pretenders.auto` stopped) is `'authored'`, and what that
|
|
198
|
+
* component renders is not known, so it is accepted like one written directly.
|
|
178
199
|
*
|
|
179
200
|
* In `'origin'` mode the pretender context is suppressed while matching (see
|
|
180
201
|
* {@link matches}) and the element is seen under its authored name, so it stays
|
|
181
202
|
* accepted as a custom element.
|
|
203
|
+
*
|
|
204
|
+
* The `'pretended'` criterion is the one `MLElement.isCustomElement` gives a
|
|
205
|
+
* `:model()` selector; change them together.
|
|
182
206
|
*/
|
|
183
207
|
function isTreatedAsCustom(
|
|
184
208
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
185
209
|
element, mode) {
|
|
186
|
-
if (element.elementType === 'html') {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
210
|
const pretenderContext = element.pretenderContext;
|
|
190
211
|
if (mode === 'pretended' && pretenderContext?.type === 'pretender') {
|
|
191
|
-
|
|
212
|
+
const { as } = pretenderContext;
|
|
213
|
+
return as.elementType === 'authored' || as.localName.includes('-');
|
|
192
214
|
}
|
|
193
|
-
return
|
|
215
|
+
return element.elementType !== 'html';
|
|
194
216
|
}
|
|
195
217
|
/**
|
|
196
218
|
* Pre-computed conditions for well-known queries that do not depend on specs.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { deepCopy } from '../helpers.js';
|
|
2
2
|
import { complexBranch } from './complex-branch.js';
|
|
3
|
-
import {
|
|
3
|
+
import { depthLog } from './debug.js';
|
|
4
4
|
import { Collection, mergeHints, modelLog } from './utils.js';
|
|
5
5
|
/**
|
|
6
6
|
* Validates an ordered sequence of content model patterns against a list of child nodes.
|
|
@@ -29,12 +29,16 @@ import { Collection, mergeHints, modelLog } from './utils.js';
|
|
|
29
29
|
export function order(contents,
|
|
30
30
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
31
31
|
childNodes, rules, specs, options, depth, mode) {
|
|
32
|
-
const orderLog =
|
|
33
|
-
const btLog =
|
|
32
|
+
const orderLog = depthLog('order', depth);
|
|
33
|
+
const btLog = depthLog('backtrack', depth);
|
|
34
|
+
const orderEnabled = orderLog.enabled;
|
|
35
|
+
const btEnabled = btLog.enabled;
|
|
34
36
|
const patterns = deepCopy(contents);
|
|
35
37
|
const collection = new Collection(childNodes);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
if (orderEnabled) {
|
|
39
|
+
orderLog('Model:\n RegEx: %s\n Schema: %o', modelLog(patterns, ''), patterns);
|
|
40
|
+
orderLog('Starts: %s', collection);
|
|
41
|
+
}
|
|
38
42
|
let result = undefined;
|
|
39
43
|
let backtrackMode = false;
|
|
40
44
|
let afterBacktrack = false;
|
|
@@ -46,7 +50,9 @@ childNodes, rules, specs, options, depth, mode) {
|
|
|
46
50
|
unmatchedResults.push(result);
|
|
47
51
|
if (backtrackMode) {
|
|
48
52
|
collection.back();
|
|
49
|
-
|
|
53
|
+
if (btEnabled) {
|
|
54
|
+
btLog('🔙◀BACK');
|
|
55
|
+
}
|
|
50
56
|
backtrackMode = false;
|
|
51
57
|
afterBacktrack = true;
|
|
52
58
|
continue;
|
|
@@ -55,9 +61,11 @@ childNodes, rules, specs, options, depth, mode) {
|
|
|
55
61
|
if (!barelyMatchedResult) {
|
|
56
62
|
throw new Error('Unreachable code');
|
|
57
63
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
if (orderEnabled) {
|
|
65
|
+
orderLog('Result (%s): %s%s', result.type, collection.toString(true), barelyMatchedResult.hint.missing?.barelyMatchedElements == null
|
|
66
|
+
? ''
|
|
67
|
+
: `; But ${barelyMatchedResult.hint.missing.barelyMatchedElements} elements hit out of pattern`);
|
|
68
|
+
}
|
|
61
69
|
return {
|
|
62
70
|
type: barelyMatchedResult.type,
|
|
63
71
|
matched: collection.matched,
|
|
@@ -85,7 +93,9 @@ childNodes, rules, specs, options, depth, mode) {
|
|
|
85
93
|
patterns.shift();
|
|
86
94
|
}
|
|
87
95
|
if (collection.unmatched.length > 0) {
|
|
88
|
-
|
|
96
|
+
if (orderEnabled) {
|
|
97
|
+
orderLog('Result (UNEXPECTED_EXTRA_NODE): %s', collection.toString(true));
|
|
98
|
+
}
|
|
89
99
|
return {
|
|
90
100
|
type: 'UNEXPECTED_EXTRA_NODE',
|
|
91
101
|
matched: collection.matched,
|
|
@@ -96,7 +106,9 @@ childNodes, rules, specs, options, depth, mode) {
|
|
|
96
106
|
};
|
|
97
107
|
}
|
|
98
108
|
const resultType = collection.matched.length > 0 ? 'MATCHED' : 'MATCHED_ZERO';
|
|
99
|
-
|
|
109
|
+
if (orderEnabled) {
|
|
110
|
+
orderLog('Result (%s): %s', resultType, collection.toString(true));
|
|
111
|
+
}
|
|
100
112
|
return {
|
|
101
113
|
type: resultType,
|
|
102
114
|
matched: collection.matched,
|
|
@@ -195,7 +195,9 @@ childNodes, rules, specs, options, mode) {
|
|
|
195
195
|
// Cap exceeded (#3895): fall back to the conservative
|
|
196
196
|
// over-approximation documented on MAX_PATTERNS. False negatives
|
|
197
197
|
// are possible beyond this point, so leave a debug trace.
|
|
198
|
-
|
|
198
|
+
if (cmLog.enabled) {
|
|
199
|
+
cmLog('Transparent pattern cap exceeded on <%s> (%d patterns x %d branches > %d): merging all branch children into every pattern; false negatives are possible', childNode.nodeName, patterns.length, branchGroups.length, MAX_PATTERNS);
|
|
200
|
+
}
|
|
199
201
|
const allChildren = branchGroups.flat();
|
|
200
202
|
for (const p of patterns) {
|
|
201
203
|
p.push(childNode, ...allChildren);
|
|
@@ -16,7 +16,9 @@ export function transparent(
|
|
|
16
16
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
17
17
|
childNodes) {
|
|
18
18
|
if (childNodes.length === 0 || childNodes[0]?.parentElement?.parentElement) {
|
|
19
|
-
transparentLog
|
|
19
|
+
if (transparentLog.enabled) {
|
|
20
|
+
transparentLog('Skipped');
|
|
21
|
+
}
|
|
20
22
|
return {
|
|
21
23
|
type: childNodes.length === 0 ? 'MATCHED_ZERO' : 'MATCHED',
|
|
22
24
|
matched: [...childNodes],
|
|
@@ -26,7 +28,9 @@ childNodes) {
|
|
|
26
28
|
hint: {},
|
|
27
29
|
};
|
|
28
30
|
}
|
|
29
|
-
transparentLog
|
|
31
|
+
if (transparentLog.enabled) {
|
|
32
|
+
transparentLog('Transparent model element is component root');
|
|
33
|
+
}
|
|
30
34
|
return {
|
|
31
35
|
type: 'MATCHED',
|
|
32
36
|
matched: [...childNodes],
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* > multiple attribute specified, and has a display size of 1, then the
|
|
6
6
|
* > select element must have a placeholder label option.
|
|
7
7
|
*
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* `needPlaceholderLabelOption` gates whether the requirement applies to this
|
|
9
|
+
* `<select>`; `hasPlaceholderLabelOption` then decides whether an existing
|
|
10
|
+
* option already satisfies it.
|
|
11
11
|
*
|
|
12
12
|
* @see https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element
|
|
13
13
|
*/
|