@markuplint/rules 5.0.0-rc.0 → 5.0.0-rc.2
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 +26 -0
- package/CLAUDE.md +27 -0
- package/SKILL.md +1 -0
- package/lib/attr-check.d.ts +3 -1
- package/lib/attr-check.js +10 -1
- package/lib/helpers.js +16 -1
- package/lib/index.d.ts +25 -7
- package/lib/index.js +36 -0
- package/lib/invalid-attr/index.js +12 -0
- package/lib/no-duplicate-autofocus/index.d.ts +7 -0
- package/lib/no-duplicate-autofocus/index.js +27 -0
- package/lib/no-duplicate-autofocus/meta.d.ts +4 -0
- package/lib/no-duplicate-autofocus/meta.js +3 -0
- package/lib/no-duplicate-visible-main/index.d.ts +8 -0
- package/lib/no-duplicate-visible-main/index.js +28 -0
- package/lib/no-duplicate-visible-main/meta.d.ts +4 -0
- package/lib/no-duplicate-visible-main/meta.js +3 -0
- package/lib/permitted-contents/choice.d.ts +5 -2
- package/lib/permitted-contents/choice.js +5 -2
- package/lib/permitted-contents/complex-branch.d.ts +5 -2
- package/lib/permitted-contents/complex-branch.js +6 -3
- package/lib/permitted-contents/content-model.d.ts +21 -2
- package/lib/permitted-contents/content-model.js +32 -6
- package/lib/permitted-contents/count-pattern.d.ts +5 -2
- package/lib/permitted-contents/count-pattern.js +13 -5
- package/lib/permitted-contents/index.d.ts +7 -1
- package/lib/permitted-contents/index.js +160 -65
- package/lib/permitted-contents/matches-selector.d.ts +2 -2
- package/lib/permitted-contents/matches-selector.js +43 -5
- package/lib/permitted-contents/order.d.ts +9 -2
- package/lib/permitted-contents/order.js +9 -2
- package/lib/permitted-contents/recursive-branch.d.ts +5 -2
- package/lib/permitted-contents/recursive-branch.js +7 -4
- package/lib/permitted-contents/represent-transparent-nodes.d.ts +2 -2
- package/lib/permitted-contents/represent-transparent-nodes.js +6 -6
- package/lib/permitted-contents/start.d.ts +2 -2
- package/lib/permitted-contents/start.js +3 -3
- package/lib/permitted-contents/types.d.ts +14 -0
- package/lib/permitted-contents/utils.d.ts +11 -2
- package/lib/permitted-contents/utils.js +62 -13
- package/lib/wai-aria/checkings/disallowed-prop.d.ts +2 -1
- package/lib/wai-aria/checkings/disallowed-prop.js +27 -5
- package/lib/wai-aria/checkings/permitted-roles.js +16 -3
- package/lib/wai-aria/checkings/required-accessibility-parent-role.js +1 -1
- package/lib/wai-aria/default-options.d.ts +5 -0
- package/lib/wai-aria/default-options.js +18 -0
- package/lib/wai-aria/index.js +8 -19
- package/lib/wai-aria-abstract-role/index.d.ts +4 -0
- package/lib/wai-aria-abstract-role/index.js +22 -0
- package/lib/wai-aria-abstract-role/meta.d.ts +4 -0
- package/lib/wai-aria-abstract-role/meta.js +3 -0
- package/lib/wai-aria-default-value/index.d.ts +4 -0
- package/lib/wai-aria-default-value/index.js +28 -0
- package/lib/wai-aria-default-value/meta.d.ts +5 -0
- package/lib/wai-aria-default-value/meta.js +4 -0
- package/lib/wai-aria-deprecated-props/index.d.ts +4 -0
- package/lib/wai-aria-deprecated-props/index.js +29 -0
- package/lib/wai-aria-deprecated-props/meta.d.ts +5 -0
- package/lib/wai-aria-deprecated-props/meta.js +4 -0
- package/lib/wai-aria-deprecated-role/index.d.ts +4 -0
- package/lib/wai-aria-deprecated-role/index.js +26 -0
- package/lib/wai-aria-deprecated-role/meta.d.ts +4 -0
- package/lib/wai-aria-deprecated-role/meta.js +3 -0
- package/lib/wai-aria-disallowed-props/index.d.ts +4 -0
- package/lib/wai-aria-disallowed-props/index.js +33 -0
- package/lib/wai-aria-disallowed-props/meta.d.ts +5 -0
- package/lib/wai-aria-disallowed-props/meta.js +4 -0
- package/lib/wai-aria-implicit-props/index.d.ts +4 -0
- package/lib/wai-aria-implicit-props/index.js +29 -0
- package/lib/wai-aria-implicit-props/meta.d.ts +5 -0
- package/lib/wai-aria-implicit-props/meta.js +4 -0
- package/lib/wai-aria-implicit-role/index.d.ts +4 -0
- package/lib/wai-aria-implicit-role/index.js +23 -0
- package/lib/wai-aria-implicit-role/meta.d.ts +4 -0
- package/lib/wai-aria-implicit-role/meta.js +3 -0
- package/lib/wai-aria-interaction-in-hidden/index.d.ts +4 -0
- package/lib/wai-aria-interaction-in-hidden/index.js +20 -0
- package/lib/wai-aria-interaction-in-hidden/meta.d.ts +5 -0
- package/lib/wai-aria-interaction-in-hidden/meta.js +4 -0
- package/lib/wai-aria-no-global-prop/index.d.ts +4 -0
- package/lib/wai-aria-no-global-prop/index.js +30 -0
- package/lib/wai-aria-no-global-prop/meta.d.ts +5 -0
- package/lib/wai-aria-no-global-prop/meta.js +4 -0
- package/lib/wai-aria-non-existent-role/index.d.ts +4 -0
- package/lib/wai-aria-non-existent-role/index.js +22 -0
- package/lib/wai-aria-non-existent-role/meta.d.ts +4 -0
- package/lib/wai-aria-non-existent-role/meta.js +3 -0
- package/lib/wai-aria-permitted-roles/index.d.ts +4 -0
- package/lib/wai-aria-permitted-roles/index.js +22 -0
- package/lib/wai-aria-permitted-roles/meta.d.ts +4 -0
- package/lib/wai-aria-permitted-roles/meta.js +3 -0
- package/lib/wai-aria-presentational-children/index.d.ts +4 -0
- package/lib/wai-aria-presentational-children/index.js +20 -0
- package/lib/wai-aria-presentational-children/meta.d.ts +5 -0
- package/lib/wai-aria-presentational-children/meta.js +4 -0
- package/lib/wai-aria-required-owned-elements/index.d.ts +4 -0
- package/lib/wai-aria-required-owned-elements/index.js +23 -0
- package/lib/wai-aria-required-owned-elements/meta.d.ts +4 -0
- package/lib/wai-aria-required-owned-elements/meta.js +3 -0
- package/lib/wai-aria-required-parent-role/index.d.ts +4 -0
- package/lib/wai-aria-required-parent-role/index.js +25 -0
- package/lib/wai-aria-required-parent-role/meta.d.ts +4 -0
- package/lib/wai-aria-required-parent-role/meta.js +3 -0
- package/lib/wai-aria-required-props/index.d.ts +4 -0
- package/lib/wai-aria-required-props/index.js +26 -0
- package/lib/wai-aria-required-props/meta.d.ts +4 -0
- package/lib/wai-aria-required-props/meta.js +3 -0
- package/lib/wai-aria-value/index.d.ts +4 -0
- package/lib/wai-aria-value/index.js +28 -0
- package/lib/wai-aria-value/meta.d.ts +5 -0
- package/lib/wai-aria-value/meta.js +4 -0
- package/package.json +15 -11
- package/schema.json +54 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChildNode, Options, Result, Specs } from './types.js';
|
|
1
|
+
import type { ChildNode, Mode, Options, Result, Specs, TagRule } from './types.js';
|
|
2
2
|
import type { PermittedContentOneOrMore, PermittedContentOptional, PermittedContentRequire, PermittedContentZeroOrMore } from '@markuplint/ml-spec';
|
|
3
3
|
import type { ReadonlyDeep } from 'type-fest';
|
|
4
4
|
/**
|
|
@@ -12,9 +12,12 @@ import type { ReadonlyDeep } from 'type-fest';
|
|
|
12
12
|
*
|
|
13
13
|
* @param pattern - A quantified content model pattern (require, optional, oneOrMore, or zeroOrMore).
|
|
14
14
|
* @param childNodes - The child nodes to validate against the repeated pattern.
|
|
15
|
+
* @param rules - User-defined tag rules. Threaded through for transparent-model recursion;
|
|
16
|
+
* not consulted here directly. See `order` for the rationale.
|
|
15
17
|
* @param specs - The resolved spec data for content model lookups.
|
|
16
18
|
* @param options - Validation behavior options.
|
|
17
19
|
* @param depth - The current recursion depth, used for debug logging and nested evaluation.
|
|
20
|
+
* @param mode - Whether we are evaluating the element's `'origin'` or `'pretended'` identity.
|
|
18
21
|
* @returns A result indicating whether the required count of matches was achieved.
|
|
19
22
|
*/
|
|
20
|
-
export declare function countPattern(pattern: ReadonlyDeep<PermittedContentOneOrMore> | ReadonlyDeep<PermittedContentOptional> | ReadonlyDeep<PermittedContentRequire> | ReadonlyDeep<PermittedContentZeroOrMore>, childNodes: readonly ChildNode[], specs: Specs, options: Options, depth: number): Result;
|
|
23
|
+
export declare function countPattern(pattern: ReadonlyDeep<PermittedContentOneOrMore> | ReadonlyDeep<PermittedContentOptional> | ReadonlyDeep<PermittedContentRequire> | ReadonlyDeep<PermittedContentZeroOrMore>, childNodes: readonly ChildNode[], rules: readonly TagRule[], specs: Specs, options: Options, depth: number, mode: Mode): Result;
|
|
@@ -13,14 +13,17 @@ const cLog = cmLog.extend('countCompereResult');
|
|
|
13
13
|
*
|
|
14
14
|
* @param pattern - A quantified content model pattern (require, optional, oneOrMore, or zeroOrMore).
|
|
15
15
|
* @param childNodes - The child nodes to validate against the repeated pattern.
|
|
16
|
+
* @param rules - User-defined tag rules. Threaded through for transparent-model recursion;
|
|
17
|
+
* not consulted here directly. See `order` for the rationale.
|
|
16
18
|
* @param specs - The resolved spec data for content model lookups.
|
|
17
19
|
* @param options - Validation behavior options.
|
|
18
20
|
* @param depth - The current recursion depth, used for debug logging and nested evaluation.
|
|
21
|
+
* @param mode - Whether we are evaluating the element's `'origin'` or `'pretended'` identity.
|
|
19
22
|
* @returns A result indicating whether the required count of matches was achieved.
|
|
20
23
|
*/
|
|
21
24
|
export function countPattern(pattern,
|
|
22
25
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
23
|
-
childNodes, specs, options, depth) {
|
|
26
|
+
childNodes, rules, specs, options, depth, mode) {
|
|
24
27
|
const ptLog = cmLog.extend(`countPattern#${depth}`);
|
|
25
28
|
const collection = new Collection(childNodes);
|
|
26
29
|
const { model, min, max, repeat, missingType } = normalizeModel(pattern);
|
|
@@ -31,7 +34,7 @@ childNodes, specs, options, depth) {
|
|
|
31
34
|
while (true) {
|
|
32
35
|
loopCount++;
|
|
33
36
|
ptLog('Check#%s: %s', loopCount, collection);
|
|
34
|
-
const result = recursiveBranch(model, collection.unmatched, specs, options, depth);
|
|
37
|
+
const result = recursiveBranch(model, collection.unmatched, rules, specs, options, depth, mode);
|
|
35
38
|
const added = collection.addMatched(result.matched);
|
|
36
39
|
const { matchedCount } = collection;
|
|
37
40
|
if (result.type === 'UNMATCHED_SELECTOR_BUT_MAY_EMPTY') {
|
|
@@ -130,9 +133,14 @@ childNodes, specs, options, depth) {
|
|
|
130
133
|
continue;
|
|
131
134
|
}
|
|
132
135
|
ptLog('%s:\n model: %s\n max: %s\n collection: %s\n matched element: %s', resultType, modelLog(model, repeat), max, collection, matchedCount);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
+
// When min=0 and no elements were consumed, the pattern legitimately
|
|
137
|
+
// matched zero times — don't propagate the inner sequence's failure.
|
|
138
|
+
// This enables zeroOrMore([oneOrMore dt, oneOrMore dd]) to accept
|
|
139
|
+
// empty content (e.g., <dl></dl>). See #3592.
|
|
140
|
+
if ((collection.matchedCount > 0 || min > 0) &&
|
|
141
|
+
(result.type === 'MISSING_NODE_REQUIRED' ||
|
|
142
|
+
result.type === 'MISSING_NODE_ONE_OR_MORE' ||
|
|
143
|
+
result.type === 'TRANSPARENT_MODEL_DISALLOWS')) {
|
|
136
144
|
return compereResult({
|
|
137
145
|
type: result.type,
|
|
138
146
|
matched: collection.matched,
|
|
@@ -9,7 +9,13 @@ import type { Options, TagRule } from './types.js';
|
|
|
9
9
|
* For each element, it resolves the applicable content model (from the HTML spec or
|
|
10
10
|
* user-defined tag rules), evaluates the element's children against that model, and
|
|
11
11
|
* reports violations such as unexpected elements, missing required elements, or
|
|
12
|
-
* disallowed content through transparent models.
|
|
12
|
+
* disallowed content through transparent models. It also checks forbidden ancestor
|
|
13
|
+
* constraints — elements like `<header>`, `<footer>`, `<main>`, and `<address>` must
|
|
14
|
+
* not appear as descendants of certain other elements as defined by the HTML spec.
|
|
15
|
+
* Additionally, it enforces required ancestor constraints (`descendantOf`) — certain
|
|
16
|
+
* elements must appear as descendants of specific other elements. It also validates
|
|
17
|
+
* sibling-unique attribute constraints (`uniqueAttrs`) — certain attributes must not
|
|
18
|
+
* appear on more than one element of the same type within the same parent.
|
|
13
19
|
*/
|
|
14
20
|
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<TagRule[], Options>>;
|
|
15
21
|
export default _default;
|
|
@@ -12,7 +12,13 @@ import { transparentMode } from './represent-transparent-nodes.js';
|
|
|
12
12
|
* For each element, it resolves the applicable content model (from the HTML spec or
|
|
13
13
|
* user-defined tag rules), evaluates the element's children against that model, and
|
|
14
14
|
* reports violations such as unexpected elements, missing required elements, or
|
|
15
|
-
* disallowed content through transparent models.
|
|
15
|
+
* disallowed content through transparent models. It also checks forbidden ancestor
|
|
16
|
+
* constraints — elements like `<header>`, `<footer>`, `<main>`, and `<address>` must
|
|
17
|
+
* not appear as descendants of certain other elements as defined by the HTML spec.
|
|
18
|
+
* Additionally, it enforces required ancestor constraints (`descendantOf`) — certain
|
|
19
|
+
* elements must appear as descendants of specific other elements. It also validates
|
|
20
|
+
* sibling-unique attribute constraints (`uniqueAttrs`) — certain attributes must not
|
|
21
|
+
* appear on more than one element of the same type within the same parent.
|
|
16
22
|
*/
|
|
17
23
|
export default createRule({
|
|
18
24
|
meta: meta,
|
|
@@ -23,83 +29,165 @@ export default createRule({
|
|
|
23
29
|
},
|
|
24
30
|
async verify({ document, report, t }) {
|
|
25
31
|
await document.walkOn('Element', el => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
t('the max number of elements required is {0}', `${hint.max}`);
|
|
34
|
-
}
|
|
35
|
-
switch (type) {
|
|
36
|
-
case 'MATCHED':
|
|
37
|
-
case 'MATCHED_ZERO': {
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
case 'MISSING_NODE_ONE_OR_MORE': {
|
|
41
|
-
if (scope.rule.options.ignoreHasMutableChildren &&
|
|
42
|
-
(!scope.is(scope.ELEMENT_NODE) || scope.hasMutableChildren())) {
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
message =
|
|
46
|
-
message ||
|
|
47
|
-
t('Require {0}', t('one or more elements')) + t('. ') + '(' + t('Need "{0*}"', query) + ')';
|
|
32
|
+
// Check forbidden ancestors
|
|
33
|
+
const elSpec = document.specs.specs.find(s => s.name === el.localName);
|
|
34
|
+
const forbiddenAncestors = elSpec?.contentModel?.forbiddenAncestors;
|
|
35
|
+
if (forbiddenAncestors && forbiddenAncestors.length > 0) {
|
|
36
|
+
let ancestor = el.parentElement;
|
|
37
|
+
while (ancestor) {
|
|
38
|
+
if (forbiddenAncestors.some(selector => ancestor.matches(selector))) {
|
|
48
39
|
report({
|
|
49
|
-
scope,
|
|
50
|
-
message,
|
|
40
|
+
scope: el,
|
|
41
|
+
message: t('{0} must not appear as a descendant of {1}', t('the "{0}" {1}', el.localName, 'element'), t('the "{0}" {1}', ancestor.localName, 'element')),
|
|
51
42
|
});
|
|
52
43
|
break;
|
|
53
44
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
});
|
|
45
|
+
ancestor = ancestor.parentElement;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Check required ancestor (descendantOf)
|
|
49
|
+
const descendantOf = elSpec?.contentModel?.descendantOf;
|
|
50
|
+
if (descendantOf) {
|
|
51
|
+
let ancestor = el.parentElement;
|
|
52
|
+
let found = false;
|
|
53
|
+
while (ancestor) {
|
|
54
|
+
if (ancestor.matches(descendantOf)) {
|
|
55
|
+
found = true;
|
|
66
56
|
break;
|
|
67
57
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
58
|
+
ancestor = ancestor.parentElement;
|
|
59
|
+
}
|
|
60
|
+
if (!found) {
|
|
61
|
+
report({
|
|
62
|
+
scope: el,
|
|
63
|
+
message: t('{0} must appear as a descendant of {1}', t('the "{0}" {1}', el.localName, 'element'), t('the "{0}" {1}', descendantOf, 'element')),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Check unique attributes among siblings of the same type
|
|
68
|
+
const uniqueAttrs = elSpec?.contentModel?.uniqueAttrs;
|
|
69
|
+
if (uniqueAttrs && uniqueAttrs.length > 0) {
|
|
70
|
+
for (const attrName of uniqueAttrs) {
|
|
71
|
+
if (!el.hasAttribute(attrName)) {
|
|
72
|
+
continue;
|
|
80
73
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
report({
|
|
85
|
-
scope: not,
|
|
86
|
-
message: t('{0} is {1} but {2}', name(tp, t), t('a {0}', 'transparent model'), t('also disallows {0} in this context', name(not, t))),
|
|
87
|
-
});
|
|
88
|
-
break;
|
|
74
|
+
const parent = el.parentElement;
|
|
75
|
+
if (!parent) {
|
|
76
|
+
continue;
|
|
89
77
|
}
|
|
90
|
-
|
|
78
|
+
// Only report on the second (and subsequent) element that has the attribute
|
|
79
|
+
const precedingSiblings = [...parent.children];
|
|
80
|
+
const elIndex = precedingSiblings.indexOf(el);
|
|
81
|
+
const hasPrecedingDuplicate = precedingSiblings
|
|
82
|
+
.slice(0, elIndex)
|
|
83
|
+
.some(child => child.localName === el.localName && child.hasAttribute(attrName));
|
|
84
|
+
if (hasPrecedingDuplicate) {
|
|
91
85
|
report({
|
|
92
86
|
scope: el,
|
|
93
|
-
message: t('{0}
|
|
87
|
+
message: t('The "{0}" attribute must not appear on more than one "{1}" element within the same parent', attrName, el.localName),
|
|
94
88
|
});
|
|
95
|
-
break;
|
|
96
89
|
}
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Build the mode list. `'pretended'` always runs and preserves the historical
|
|
93
|
+
// behaviour. `'origin'` additionally fires when the element is pretending and
|
|
94
|
+
// the user has declared a content model keyed on its original AST name, so
|
|
95
|
+
// that user-defined custom-tag rules are not silently bypassed by pretending
|
|
96
|
+
// (see issue #3739).
|
|
97
|
+
const modes = ['pretended'];
|
|
98
|
+
if (el.pretenderContext?.type === 'pretender' && el.rule.value.some(r => r.tag === el.rawName)) {
|
|
99
|
+
modes.push('origin');
|
|
100
|
+
}
|
|
101
|
+
for (const mode of modes) {
|
|
102
|
+
const results = contentModel(el, el.rule.value, el.rule.options, mode);
|
|
103
|
+
for (const { type, scope, query, hint } of results) {
|
|
104
|
+
let message = '';
|
|
105
|
+
if (hint.max != null) {
|
|
106
|
+
message =
|
|
107
|
+
t('there is more content than it needs') +
|
|
108
|
+
t('. ') +
|
|
109
|
+
t('the max number of elements required is {0}', `${hint.max}`);
|
|
110
|
+
}
|
|
111
|
+
switch (type) {
|
|
112
|
+
case 'MATCHED':
|
|
113
|
+
case 'MATCHED_ZERO': {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case 'MISSING_NODE_ONE_OR_MORE': {
|
|
117
|
+
if (scope.rule.options.ignoreHasMutableChildren &&
|
|
118
|
+
(!scope.is(scope.ELEMENT_NODE) || scope.hasMutableChildren())) {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
message =
|
|
122
|
+
message ||
|
|
123
|
+
t('Require {0}', t('one or more elements')) +
|
|
124
|
+
t('. ') +
|
|
125
|
+
'(' +
|
|
126
|
+
t('Need "{0*}"', query) +
|
|
127
|
+
')';
|
|
128
|
+
report({
|
|
129
|
+
scope,
|
|
130
|
+
message,
|
|
131
|
+
});
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case 'MISSING_NODE_REQUIRED': {
|
|
135
|
+
if (scope.rule.options.ignoreHasMutableChildren &&
|
|
136
|
+
(!scope.is(scope.ELEMENT_NODE) || scope.hasMutableChildren())) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
message =
|
|
140
|
+
message ||
|
|
141
|
+
t('Require {0}', t('an {0}', 'element')) +
|
|
142
|
+
t('. ') +
|
|
143
|
+
'(' +
|
|
144
|
+
t('Need "{0*}"', query) +
|
|
145
|
+
')';
|
|
146
|
+
report({
|
|
147
|
+
scope,
|
|
148
|
+
message,
|
|
149
|
+
});
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case 'UNEXPECTED_EXTRA_NODE': {
|
|
153
|
+
const not = hint.not ?? scope;
|
|
154
|
+
message =
|
|
155
|
+
message ||
|
|
156
|
+
(transparentMode.has(scope)
|
|
157
|
+
? t('{0} is not allowed in {1} through the transparent model in this context', name(not, t, mode), name(el, t, mode))
|
|
158
|
+
: t('{0} is not allowed in {1} in this context', name(not, t, mode), name(el, t, mode)));
|
|
159
|
+
report({
|
|
160
|
+
scope: not,
|
|
161
|
+
message,
|
|
162
|
+
});
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case 'TRANSPARENT_MODEL_DISALLOWS': {
|
|
166
|
+
const not = hint.not ?? scope;
|
|
167
|
+
const tp = hint.transparent ?? el;
|
|
168
|
+
report({
|
|
169
|
+
scope: not,
|
|
170
|
+
message: t('{0} is {1} but {2}', name(tp, t, mode), t('a {0}', 'transparent model'), t('also disallows {0} in this context', name(not, t, mode))),
|
|
171
|
+
});
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case 'NOTHING': {
|
|
175
|
+
report({
|
|
176
|
+
scope: el,
|
|
177
|
+
message: t('{0} disallows {1}', t('the {0}', 'element'), 'contents'),
|
|
178
|
+
});
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
default: {
|
|
182
|
+
throw new Error('Unreachable code');
|
|
183
|
+
}
|
|
99
184
|
}
|
|
100
185
|
}
|
|
186
|
+
// Clear the per-element transparent-model tracker between passes so that
|
|
187
|
+
// errors reported in one mode do not carry the "through the transparent
|
|
188
|
+
// model" phrasing into the other mode's diagnostics.
|
|
189
|
+
transparentMode.clear();
|
|
101
190
|
}
|
|
102
|
-
transparentMode.clear();
|
|
103
191
|
});
|
|
104
192
|
},
|
|
105
193
|
});
|
|
@@ -108,15 +196,22 @@ export default createRule({
|
|
|
108
196
|
* Used in error messages to describe the offending node (e.g., 'the "div" element',
|
|
109
197
|
* 'the text node', 'the comment', 'the doctype', 'the code block').
|
|
110
198
|
*
|
|
199
|
+
* In `'origin'` mode, uses {@link MLElement.rawName} so that the diagnostic refers
|
|
200
|
+
* to the original AST identity (e.g. the component name `"Breadcrumbs"`) rather
|
|
201
|
+
* than the pretender target (`"nav"`). `'pretended'` mode preserves the historical
|
|
202
|
+
* behaviour and prints the visible `localName`.
|
|
203
|
+
*
|
|
111
204
|
* @param scope - The child node to generate a name for.
|
|
112
205
|
* @param t - The translator function for localized message formatting.
|
|
206
|
+
* @param mode - Which identity to surface in the message.
|
|
113
207
|
* @returns A localized string describing the node.
|
|
114
208
|
*/
|
|
115
209
|
function name(
|
|
116
210
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
117
|
-
scope, t) {
|
|
211
|
+
scope, t, mode) {
|
|
118
212
|
if (scope.is(scope.ELEMENT_NODE)) {
|
|
119
|
-
|
|
213
|
+
const displayName = mode === 'origin' ? scope.rawName : scope.localName;
|
|
214
|
+
return t('the "{0}" {1}', displayName, 'element');
|
|
120
215
|
}
|
|
121
216
|
if (scope.is(scope.TEXT_NODE)) {
|
|
122
217
|
return t('the {0}', 'text node');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChildNode, Result, Specs } from './types.js';
|
|
1
|
+
import type { ChildNode, Mode, Result, Specs } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Extended result type for selector matching that includes additional
|
|
4
4
|
* intermediate states: a selector that did not match but allows empty content,
|
|
@@ -19,4 +19,4 @@ export type SelectorResult = Result<'UNMATCHED_SELECTOR_BUT_MAY_EMPTY' | 'MISSIN
|
|
|
19
19
|
* @param depth - The current recursion depth, used for debug logging namespacing.
|
|
20
20
|
* @returns A selector result indicating match status with diagnostic hints.
|
|
21
21
|
*/
|
|
22
|
-
export declare function matchesSelector(query: string, childNode: ChildNode | undefined, specs: Specs, depth: number): SelectorResult;
|
|
22
|
+
export declare function matchesSelector(query: string, childNode: ChildNode | undefined, specs: Specs, depth: number, mode: Mode): SelectorResult;
|
|
@@ -17,9 +17,9 @@ import { cleanObject, matches } from './utils.js';
|
|
|
17
17
|
*/
|
|
18
18
|
export function matchesSelector(query,
|
|
19
19
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
20
|
-
childNode, specs, depth) {
|
|
20
|
+
childNode, specs, depth, mode) {
|
|
21
21
|
const nodeLog = cmLog.extend(`node#${depth}`);
|
|
22
|
-
const { selector, hasText, hasCustom } = optCondition(query, specs);
|
|
22
|
+
const { selector, hasText, hasNonEmptyText, hasCustom } = optCondition(query, specs);
|
|
23
23
|
if (childNode == null) {
|
|
24
24
|
if (hasText) {
|
|
25
25
|
nodeLog('<#text>.matches(%s) => ""', query);
|
|
@@ -32,6 +32,7 @@ childNode, specs, depth) {
|
|
|
32
32
|
hint: {},
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
+
// #nonEmptyText requires actual non-empty text — missing node is not acceptable
|
|
35
36
|
return {
|
|
36
37
|
type: 'MISSING_NODE',
|
|
37
38
|
matched: [],
|
|
@@ -42,6 +43,28 @@ childNode, specs, depth) {
|
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
if (childNode.is(childNode.TEXT_NODE)) {
|
|
46
|
+
if (hasNonEmptyText) {
|
|
47
|
+
if (childNode.isWhitespace()) {
|
|
48
|
+
nodeLog('<#nonEmptyText>.matches(%s) => WHITESPACE (rejected)', query);
|
|
49
|
+
return {
|
|
50
|
+
type: 'MISSING_NODE',
|
|
51
|
+
matched: [],
|
|
52
|
+
unmatched: [],
|
|
53
|
+
zeroMatch: false,
|
|
54
|
+
query,
|
|
55
|
+
hint: {},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
nodeLog('<#nonEmptyText>.matches(%s) => "%s"', query, childNode.raw.trim());
|
|
59
|
+
return {
|
|
60
|
+
type: 'MATCHED',
|
|
61
|
+
matched: [childNode],
|
|
62
|
+
unmatched: [],
|
|
63
|
+
zeroMatch: false,
|
|
64
|
+
query,
|
|
65
|
+
hint: {},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
45
68
|
if (hasText) {
|
|
46
69
|
nodeLog('<#text>.matches(%s) => "%s"', query, childNode.raw.trim());
|
|
47
70
|
return {
|
|
@@ -80,7 +103,7 @@ childNode, specs, depth) {
|
|
|
80
103
|
type: 'MATCHED',
|
|
81
104
|
matched: [childNode],
|
|
82
105
|
unmatched: [],
|
|
83
|
-
zeroMatch: !!hasText,
|
|
106
|
+
zeroMatch: !!(hasText || hasNonEmptyText),
|
|
84
107
|
query,
|
|
85
108
|
hint: {},
|
|
86
109
|
};
|
|
@@ -97,7 +120,7 @@ childNode, specs, depth) {
|
|
|
97
120
|
hint: {},
|
|
98
121
|
};
|
|
99
122
|
}
|
|
100
|
-
const result = matches(selector, childNode, specs);
|
|
123
|
+
const result = matches(selector, childNode, specs, mode);
|
|
101
124
|
nodeLog('%s.matches(%s) => %s', childNode.raw, query, result.matched);
|
|
102
125
|
if (result.matched) {
|
|
103
126
|
return {
|
|
@@ -149,11 +172,19 @@ const conditionWithoutSpecs = {
|
|
|
149
172
|
selector: '#custom',
|
|
150
173
|
hasCustom: true,
|
|
151
174
|
hasText: false,
|
|
175
|
+
hasNonEmptyText: false,
|
|
152
176
|
},
|
|
153
177
|
'#text': {
|
|
154
178
|
selector: '#text',
|
|
155
179
|
hasCustom: false,
|
|
156
180
|
hasText: true,
|
|
181
|
+
hasNonEmptyText: false,
|
|
182
|
+
},
|
|
183
|
+
'#nonEmptyText': {
|
|
184
|
+
selector: '#nonEmptyText',
|
|
185
|
+
hasCustom: false,
|
|
186
|
+
hasText: false,
|
|
187
|
+
hasNonEmptyText: true,
|
|
157
188
|
},
|
|
158
189
|
};
|
|
159
190
|
/**
|
|
@@ -185,8 +216,14 @@ function optCondition(query, specs) {
|
|
|
185
216
|
}
|
|
186
217
|
let hasCustom = false;
|
|
187
218
|
let hasText = false;
|
|
219
|
+
let hasNonEmptyText = false;
|
|
188
220
|
const selector = query.replace(/^:model\(([^)]+)\)|^#([a-z-]+)/, (_, $model, _model) => {
|
|
189
|
-
const
|
|
221
|
+
const tag = `#${$model ?? _model}`;
|
|
222
|
+
if (tag === '#nonEmptyText') {
|
|
223
|
+
hasNonEmptyText = true;
|
|
224
|
+
return '';
|
|
225
|
+
}
|
|
226
|
+
const _selectors = contentModelCategoryToTagNames(tag, specs.def);
|
|
190
227
|
if (_selectors.length === 0) {
|
|
191
228
|
throw new Error(`${$model ?? _model} is empty`);
|
|
192
229
|
}
|
|
@@ -208,6 +245,7 @@ function optCondition(query, specs) {
|
|
|
208
245
|
selector,
|
|
209
246
|
hasCustom,
|
|
210
247
|
hasText,
|
|
248
|
+
hasNonEmptyText,
|
|
211
249
|
};
|
|
212
250
|
queryCaches.set(query, result);
|
|
213
251
|
optConditionSpecsBaseCaches.set(specs, queryCaches);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChildNode, Options, Result, Specs } from './types.js';
|
|
1
|
+
import type { ChildNode, Mode, Options, Result, Specs, TagRule } from './types.js';
|
|
2
2
|
import type { PermittedContentPattern } from '@markuplint/ml-spec';
|
|
3
3
|
import type { ReadonlyDeep } from 'type-fest';
|
|
4
4
|
/**
|
|
@@ -13,9 +13,16 @@ import type { ReadonlyDeep } from 'type-fest';
|
|
|
13
13
|
*
|
|
14
14
|
* @param contents - An ordered array of content model patterns to match sequentially.
|
|
15
15
|
* @param childNodes - The child nodes to validate against the patterns.
|
|
16
|
+
* @param rules - User-defined tag rules. `order` does not consult them directly; they are
|
|
17
|
+
* threaded through so nested helpers (especially `representTransparentNodes`)
|
|
18
|
+
* can resolve content models via `resolveContentModel`. Do not remove even if
|
|
19
|
+
* it looks unused here.
|
|
16
20
|
* @param specs - The resolved spec data for content model lookups.
|
|
17
21
|
* @param options - Validation behavior options.
|
|
18
22
|
* @param depth - The current recursion depth, used for debug logging and nested pattern matching.
|
|
23
|
+
* @param mode - Whether we are evaluating the element's `'origin'` or `'pretended'` identity.
|
|
24
|
+
* Propagated unchanged into `complexBranch` so downstream selector matching
|
|
25
|
+
* honors the same view.
|
|
19
26
|
* @returns A result indicating overall match status and the matched/unmatched node partitioning.
|
|
20
27
|
*/
|
|
21
|
-
export declare function order(contents: ReadonlyDeep<PermittedContentPattern[]>, childNodes: readonly ChildNode[], specs: Specs, options: Options, depth: number): Result;
|
|
28
|
+
export declare function order(contents: ReadonlyDeep<PermittedContentPattern[]>, childNodes: readonly ChildNode[], rules: readonly TagRule[], specs: Specs, options: Options, depth: number, mode: Mode): Result;
|
|
@@ -14,14 +14,21 @@ import { Collection, mergeHints, modelLog } from './utils.js';
|
|
|
14
14
|
*
|
|
15
15
|
* @param contents - An ordered array of content model patterns to match sequentially.
|
|
16
16
|
* @param childNodes - The child nodes to validate against the patterns.
|
|
17
|
+
* @param rules - User-defined tag rules. `order` does not consult them directly; they are
|
|
18
|
+
* threaded through so nested helpers (especially `representTransparentNodes`)
|
|
19
|
+
* can resolve content models via `resolveContentModel`. Do not remove even if
|
|
20
|
+
* it looks unused here.
|
|
17
21
|
* @param specs - The resolved spec data for content model lookups.
|
|
18
22
|
* @param options - Validation behavior options.
|
|
19
23
|
* @param depth - The current recursion depth, used for debug logging and nested pattern matching.
|
|
24
|
+
* @param mode - Whether we are evaluating the element's `'origin'` or `'pretended'` identity.
|
|
25
|
+
* Propagated unchanged into `complexBranch` so downstream selector matching
|
|
26
|
+
* honors the same view.
|
|
20
27
|
* @returns A result indicating overall match status and the matched/unmatched node partitioning.
|
|
21
28
|
*/
|
|
22
29
|
export function order(contents,
|
|
23
30
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
24
|
-
childNodes, specs, options, depth) {
|
|
31
|
+
childNodes, rules, specs, options, depth, mode) {
|
|
25
32
|
const orderLog = cmLog.extend(`order#${depth}`);
|
|
26
33
|
const btLog = cmLog.extend(`backtrack#${depth}`);
|
|
27
34
|
const patterns = deepCopy(contents);
|
|
@@ -33,7 +40,7 @@ childNodes, specs, options, depth) {
|
|
|
33
40
|
let afterBacktrack = false;
|
|
34
41
|
const unmatchedResults = [];
|
|
35
42
|
while (patterns.length > 0 && patterns[0]) {
|
|
36
|
-
result = complexBranch(patterns[0], collection.unmatched, specs, options, depth);
|
|
43
|
+
result = complexBranch(patterns[0], collection.unmatched, rules, specs, options, depth, mode);
|
|
37
44
|
collection.addMatched(result.matched);
|
|
38
45
|
if (result.type !== 'UNEXPECTED_EXTRA_NODE' && result.type !== 'MATCHED' && result.type !== 'MATCHED_ZERO') {
|
|
39
46
|
unmatchedResults.push(result);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SelectorResult } from './matches-selector.js';
|
|
2
|
-
import type { ChildNode, Options, Specs } from './types.js';
|
|
2
|
+
import type { ChildNode, Mode, Options, Specs, TagRule } from './types.js';
|
|
3
3
|
import type { PermittedContentPattern, Model } from '@markuplint/ml-spec';
|
|
4
4
|
import type { ReadonlyDeep } from 'type-fest';
|
|
5
5
|
/**
|
|
@@ -14,9 +14,12 @@ import type { ReadonlyDeep } from 'type-fest';
|
|
|
14
14
|
*
|
|
15
15
|
* @param model - Either a terminal model (selector string or array of selector strings) or a nested pattern array.
|
|
16
16
|
* @param childNodes - The child nodes to validate against the model.
|
|
17
|
+
* @param rules - User-defined tag rules. Threaded through for transparent-model recursion;
|
|
18
|
+
* not consulted here directly. See `order` for the rationale.
|
|
17
19
|
* @param specs - The resolved spec data for content model lookups.
|
|
18
20
|
* @param options - Validation behavior options.
|
|
19
21
|
* @param depth - The current recursion depth, used for debug logging and nested evaluation.
|
|
22
|
+
* @param mode - Whether we are evaluating the element's `'origin'` or `'pretended'` identity.
|
|
20
23
|
* @returns A selector result indicating whether the first unmatched child node matches the model.
|
|
21
24
|
*/
|
|
22
|
-
export declare function recursiveBranch(model: ReadonlyDeep<Model | PermittedContentPattern[]>, childNodes: readonly ChildNode[], specs: Specs, options: Options, depth: number): SelectorResult;
|
|
25
|
+
export declare function recursiveBranch(model: ReadonlyDeep<Model | PermittedContentPattern[]>, childNodes: readonly ChildNode[], rules: readonly TagRule[], specs: Specs, options: Options, depth: number, mode: Mode): SelectorResult;
|
|
@@ -13,24 +13,27 @@ import { Collection, isModel } from './utils.js';
|
|
|
13
13
|
*
|
|
14
14
|
* @param model - Either a terminal model (selector string or array of selector strings) or a nested pattern array.
|
|
15
15
|
* @param childNodes - The child nodes to validate against the model.
|
|
16
|
+
* @param rules - User-defined tag rules. Threaded through for transparent-model recursion;
|
|
17
|
+
* not consulted here directly. See `order` for the rationale.
|
|
16
18
|
* @param specs - The resolved spec data for content model lookups.
|
|
17
19
|
* @param options - Validation behavior options.
|
|
18
20
|
* @param depth - The current recursion depth, used for debug logging and nested evaluation.
|
|
21
|
+
* @param mode - Whether we are evaluating the element's `'origin'` or `'pretended'` identity.
|
|
19
22
|
* @returns A selector result indicating whether the first unmatched child node matches the model.
|
|
20
23
|
*/
|
|
21
24
|
export function recursiveBranch(model,
|
|
22
25
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
23
|
-
childNodes, specs, options, depth) {
|
|
26
|
+
childNodes, rules, specs, options, depth, mode) {
|
|
24
27
|
if (!isModel(model)) {
|
|
25
|
-
return order(model, childNodes, specs, options, depth + 1);
|
|
28
|
+
return order(model, childNodes, rules, specs, options, depth + 1, mode);
|
|
26
29
|
}
|
|
27
30
|
if (typeof model === 'string') {
|
|
28
|
-
return matchesSelector(model, childNodes[0], specs, depth);
|
|
31
|
+
return matchesSelector(model, childNodes[0], specs, depth, mode);
|
|
29
32
|
}
|
|
30
33
|
const collection = new Collection(childNodes);
|
|
31
34
|
let lastUnmatched = null;
|
|
32
35
|
for (const query of model) {
|
|
33
|
-
const result = matchesSelector(query, collection.unmatched[0], specs, depth);
|
|
36
|
+
const result = matchesSelector(query, collection.unmatched[0], specs, depth, mode);
|
|
34
37
|
collection.addMatched(result.matched);
|
|
35
38
|
if (result.type === 'MATCHED' || result.type === 'MATCHED_ZERO') {
|
|
36
39
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChildNode, Options, Result, Specs } from './types.js';
|
|
1
|
+
import type { ChildNode, Mode, Options, Result, Specs, TagRule } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* A map tracking child nodes that are currently being evaluated through
|
|
4
4
|
* a transparent content model. Used to prevent infinite recursion and
|
|
@@ -58,5 +58,5 @@ type TransparentNode = {
|
|
|
58
58
|
* @returns An array of possible transparent node resolutions, each with flattened nodes and accumulated errors.
|
|
59
59
|
* @see https://github.com/markuplint/markuplint/issues/3249
|
|
60
60
|
*/
|
|
61
|
-
export declare function representTransparentNodes(childNodes: readonly ChildNode[], specs: Specs, options: Options): TransparentNode[];
|
|
61
|
+
export declare function representTransparentNodes(childNodes: readonly ChildNode[], rules: readonly TagRule[], specs: Specs, options: Options, mode: Mode): TransparentNode[];
|
|
62
62
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveContentModel } from './content-model.js';
|
|
2
2
|
import { order } from './order.js';
|
|
3
3
|
import { Collection, isTransparent, matches } from './utils.js';
|
|
4
4
|
/**
|
|
@@ -82,10 +82,10 @@ const MAX_PATTERNS = 1024;
|
|
|
82
82
|
*/
|
|
83
83
|
export function representTransparentNodes(
|
|
84
84
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
85
|
-
childNodes, specs, options) {
|
|
85
|
+
childNodes, rules, specs, options, mode) {
|
|
86
86
|
const parentElement = childNodes[0]?.parentElement;
|
|
87
87
|
const parentResults = parentElement
|
|
88
|
-
? representTransparentNodes([parentElement], specs, options)
|
|
88
|
+
? representTransparentNodes([parentElement], rules, specs, options, mode)
|
|
89
89
|
: [{ nodes: [], errors: [] }];
|
|
90
90
|
let patterns = [[]];
|
|
91
91
|
for (const childNode of childNodes) {
|
|
@@ -95,7 +95,7 @@ childNodes, specs, options) {
|
|
|
95
95
|
}
|
|
96
96
|
continue;
|
|
97
97
|
}
|
|
98
|
-
const models =
|
|
98
|
+
const models = resolveContentModel(childNode, rules, specs, mode);
|
|
99
99
|
if (models == null || typeof models === 'boolean') {
|
|
100
100
|
for (const p of patterns) {
|
|
101
101
|
p.push(childNode);
|
|
@@ -117,7 +117,7 @@ childNodes, specs, options) {
|
|
|
117
117
|
const collection = new Collection([...branchChildNodes]);
|
|
118
118
|
let unmatched;
|
|
119
119
|
if (noTransparentModels.length > 0) {
|
|
120
|
-
const result = order(noTransparentModels, collection.unmatched, specs, options, Number.POSITIVE_INFINITY);
|
|
120
|
+
const result = order(noTransparentModels, collection.unmatched, rules, specs, options, Number.POSITIVE_INFINITY, mode);
|
|
121
121
|
unmatched = result.unmatched;
|
|
122
122
|
}
|
|
123
123
|
else {
|
|
@@ -135,7 +135,7 @@ childNodes, specs, options) {
|
|
|
135
135
|
}
|
|
136
136
|
transparentMode.set(child, true);
|
|
137
137
|
if (child.is(child.ELEMENT_NODE)) {
|
|
138
|
-
const transparentCondMatched = matches(transparent.transparent, child, specs);
|
|
138
|
+
const transparentCondMatched = matches(transparent.transparent, child, specs, mode);
|
|
139
139
|
if (!transparentCondMatched.matched) {
|
|
140
140
|
branchChildren.push({
|
|
141
141
|
type: 'TRANSPARENT_MODEL_DISALLOWS',
|