@markuplint/rules 5.0.0-rc.1 → 5.0.0-rc.4

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.
Files changed (113) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/CLAUDE.md +27 -0
  3. package/SKILL.md +1 -0
  4. package/lib/attr-check.d.ts +3 -1
  5. package/lib/attr-check.js +10 -1
  6. package/lib/helpers.js +16 -1
  7. package/lib/index.d.ts +18 -0
  8. package/lib/index.js +36 -0
  9. package/lib/invalid-attr/index.js +12 -0
  10. package/lib/no-duplicate-autofocus/index.d.ts +7 -0
  11. package/lib/no-duplicate-autofocus/index.js +27 -0
  12. package/lib/no-duplicate-autofocus/meta.d.ts +4 -0
  13. package/lib/no-duplicate-autofocus/meta.js +3 -0
  14. package/lib/no-duplicate-visible-main/index.d.ts +8 -0
  15. package/lib/no-duplicate-visible-main/index.js +28 -0
  16. package/lib/no-duplicate-visible-main/meta.d.ts +4 -0
  17. package/lib/no-duplicate-visible-main/meta.js +3 -0
  18. package/lib/permitted-contents/choice.d.ts +5 -2
  19. package/lib/permitted-contents/choice.js +5 -2
  20. package/lib/permitted-contents/complex-branch.d.ts +5 -2
  21. package/lib/permitted-contents/complex-branch.js +6 -3
  22. package/lib/permitted-contents/content-model.d.ts +21 -2
  23. package/lib/permitted-contents/content-model.js +32 -6
  24. package/lib/permitted-contents/count-pattern.d.ts +5 -2
  25. package/lib/permitted-contents/count-pattern.js +13 -5
  26. package/lib/permitted-contents/index.d.ts +7 -1
  27. package/lib/permitted-contents/index.js +160 -65
  28. package/lib/permitted-contents/matches-selector.d.ts +2 -2
  29. package/lib/permitted-contents/matches-selector.js +43 -5
  30. package/lib/permitted-contents/order.d.ts +9 -2
  31. package/lib/permitted-contents/order.js +9 -2
  32. package/lib/permitted-contents/recursive-branch.d.ts +5 -2
  33. package/lib/permitted-contents/recursive-branch.js +7 -4
  34. package/lib/permitted-contents/represent-transparent-nodes.d.ts +2 -2
  35. package/lib/permitted-contents/represent-transparent-nodes.js +6 -6
  36. package/lib/permitted-contents/start.d.ts +2 -2
  37. package/lib/permitted-contents/start.js +3 -3
  38. package/lib/permitted-contents/types.d.ts +14 -0
  39. package/lib/permitted-contents/utils.d.ts +11 -2
  40. package/lib/permitted-contents/utils.js +62 -13
  41. package/lib/wai-aria/checkings/disallowed-prop.d.ts +2 -1
  42. package/lib/wai-aria/checkings/disallowed-prop.js +27 -5
  43. package/lib/wai-aria/checkings/permitted-roles.js +16 -3
  44. package/lib/wai-aria/checkings/required-accessibility-parent-role.js +1 -1
  45. package/lib/wai-aria/default-options.d.ts +5 -0
  46. package/lib/wai-aria/default-options.js +18 -0
  47. package/lib/wai-aria/index.js +8 -19
  48. package/lib/wai-aria-abstract-role/index.d.ts +4 -0
  49. package/lib/wai-aria-abstract-role/index.js +22 -0
  50. package/lib/wai-aria-abstract-role/meta.d.ts +4 -0
  51. package/lib/wai-aria-abstract-role/meta.js +3 -0
  52. package/lib/wai-aria-default-value/index.d.ts +4 -0
  53. package/lib/wai-aria-default-value/index.js +28 -0
  54. package/lib/wai-aria-default-value/meta.d.ts +5 -0
  55. package/lib/wai-aria-default-value/meta.js +4 -0
  56. package/lib/wai-aria-deprecated-props/index.d.ts +4 -0
  57. package/lib/wai-aria-deprecated-props/index.js +29 -0
  58. package/lib/wai-aria-deprecated-props/meta.d.ts +5 -0
  59. package/lib/wai-aria-deprecated-props/meta.js +4 -0
  60. package/lib/wai-aria-deprecated-role/index.d.ts +4 -0
  61. package/lib/wai-aria-deprecated-role/index.js +26 -0
  62. package/lib/wai-aria-deprecated-role/meta.d.ts +4 -0
  63. package/lib/wai-aria-deprecated-role/meta.js +3 -0
  64. package/lib/wai-aria-disallowed-props/index.d.ts +4 -0
  65. package/lib/wai-aria-disallowed-props/index.js +33 -0
  66. package/lib/wai-aria-disallowed-props/meta.d.ts +5 -0
  67. package/lib/wai-aria-disallowed-props/meta.js +4 -0
  68. package/lib/wai-aria-implicit-props/index.d.ts +4 -0
  69. package/lib/wai-aria-implicit-props/index.js +29 -0
  70. package/lib/wai-aria-implicit-props/meta.d.ts +5 -0
  71. package/lib/wai-aria-implicit-props/meta.js +4 -0
  72. package/lib/wai-aria-implicit-role/index.d.ts +4 -0
  73. package/lib/wai-aria-implicit-role/index.js +23 -0
  74. package/lib/wai-aria-implicit-role/meta.d.ts +4 -0
  75. package/lib/wai-aria-implicit-role/meta.js +3 -0
  76. package/lib/wai-aria-interaction-in-hidden/index.d.ts +4 -0
  77. package/lib/wai-aria-interaction-in-hidden/index.js +20 -0
  78. package/lib/wai-aria-interaction-in-hidden/meta.d.ts +5 -0
  79. package/lib/wai-aria-interaction-in-hidden/meta.js +4 -0
  80. package/lib/wai-aria-no-global-prop/index.d.ts +4 -0
  81. package/lib/wai-aria-no-global-prop/index.js +30 -0
  82. package/lib/wai-aria-no-global-prop/meta.d.ts +5 -0
  83. package/lib/wai-aria-no-global-prop/meta.js +4 -0
  84. package/lib/wai-aria-non-existent-role/index.d.ts +4 -0
  85. package/lib/wai-aria-non-existent-role/index.js +22 -0
  86. package/lib/wai-aria-non-existent-role/meta.d.ts +4 -0
  87. package/lib/wai-aria-non-existent-role/meta.js +3 -0
  88. package/lib/wai-aria-permitted-roles/index.d.ts +4 -0
  89. package/lib/wai-aria-permitted-roles/index.js +22 -0
  90. package/lib/wai-aria-permitted-roles/meta.d.ts +4 -0
  91. package/lib/wai-aria-permitted-roles/meta.js +3 -0
  92. package/lib/wai-aria-presentational-children/index.d.ts +4 -0
  93. package/lib/wai-aria-presentational-children/index.js +20 -0
  94. package/lib/wai-aria-presentational-children/meta.d.ts +5 -0
  95. package/lib/wai-aria-presentational-children/meta.js +4 -0
  96. package/lib/wai-aria-required-owned-elements/index.d.ts +4 -0
  97. package/lib/wai-aria-required-owned-elements/index.js +23 -0
  98. package/lib/wai-aria-required-owned-elements/meta.d.ts +4 -0
  99. package/lib/wai-aria-required-owned-elements/meta.js +3 -0
  100. package/lib/wai-aria-required-parent-role/index.d.ts +4 -0
  101. package/lib/wai-aria-required-parent-role/index.js +25 -0
  102. package/lib/wai-aria-required-parent-role/meta.d.ts +4 -0
  103. package/lib/wai-aria-required-parent-role/meta.js +3 -0
  104. package/lib/wai-aria-required-props/index.d.ts +4 -0
  105. package/lib/wai-aria-required-props/index.js +26 -0
  106. package/lib/wai-aria-required-props/meta.d.ts +4 -0
  107. package/lib/wai-aria-required-props/meta.js +3 -0
  108. package/lib/wai-aria-value/index.d.ts +4 -0
  109. package/lib/wai-aria-value/index.js +28 -0
  110. package/lib/wai-aria-value/meta.d.ts +5 -0
  111. package/lib/wai-aria-value/meta.js +4 -0
  112. package/package.json +14 -10
  113. package/schema.json +54 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,36 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [5.0.0-rc.4](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.3...v5.0.0-rc.4) (2026-04-19)
7
+
8
+ **Note:** Version bump only for package @markuplint/rules
9
+
10
+ # [5.0.0-rc.3](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.2...v5.0.0-rc.3) (2026-04-19)
11
+
12
+ **Note:** Version bump only for package @markuplint/rules
13
+
14
+ # [5.0.0-rc.2](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.1...v5.0.0-rc.2) (2026-04-15)
15
+
16
+ ### Bug Fixes
17
+
18
+ - **rules:** allow zeroOrMore to match zero times in countPattern ([f5da340](https://github.com/markuplint/markuplint/commit/f5da340ac264c831ba0c74adfd039a1d0b6ff3fb)), closes [#3592](https://github.com/markuplint/markuplint/issues/3592)
19
+ - **rules:** apply permitted-contents user rule on pretended elements ([04c42d1](https://github.com/markuplint/markuplint/commit/04c42d165aabde754d6544486ca5d39ef4a868bc)), closes [#3739](https://github.com/markuplint/markuplint/issues/3739)
20
+ - **rules:** enforce descendantOf constraint in permitted-contents rule ([5375207](https://github.com/markuplint/markuplint/commit/5375207f169d26c65be6510f28c904d39454d08e))
21
+ - **rules:** preserve role name in permitted-roles message, dedupe with implicit-role ([3123cd6](https://github.com/markuplint/markuplint/commit/3123cd690e140e142a94c09d6948bb9cf0dfd23e)), closes [#3641](https://github.com/markuplint/markuplint/issues/3641) [#3641](https://github.com/markuplint/markuplint/issues/3641) [#3641](https://github.com/markuplint/markuplint/issues/3641)
22
+ - **types:** reject zero and negative srcset descriptors ([5584d20](https://github.com/markuplint/markuplint/commit/5584d2089fe1dfe508d43601fa14aeff4e08b265))
23
+
24
+ ### Features
25
+
26
+ - **ml-spec:** add ConditionalAttributeType to Attribute.type union ([#3685](https://github.com/markuplint/markuplint/issues/3685)) ([a619a07](https://github.com/markuplint/markuplint/commit/a619a071d93566dd8aa7ab8dee2ed751c2e8756c)), closes [#3598](https://github.com/markuplint/markuplint/issues/3598) [#3189](https://github.com/markuplint/markuplint/issues/3189)
27
+ - **rules:** add #nonEmptyText matching to permitted-contents engine ([4b91cf3](https://github.com/markuplint/markuplint/commit/4b91cf37cb8d54b86e8bfe125276251f2d8cb97b))
28
+ - **rules:** add document uniqueness rules (autofocus, visible-main, charset) ([1befb20](https://github.com/markuplint/markuplint/commit/1befb2051015df7669fbd07da3fea3a0b6542a90))
29
+ - **rules:** check forbiddenAncestors in permitted-contents rule ([0633c05](https://github.com/markuplint/markuplint/commit/0633c05df91a2f51361a72efe8ebb2e604a53918))
30
+ - **rules:** check uniqueAttrs constraint in permitted-contents rule ([6b2cf6b](https://github.com/markuplint/markuplint/commit/6b2cf6b9fc7d5320543acdc9271de12e46677b9f))
31
+ - **rules:** disallow is attribute on autonomous custom elements ([ed7ec78](https://github.com/markuplint/markuplint/commit/ed7ec78c058030caacc8bd23c90c489c77c40b27))
32
+ - **rules:** enforce ARIA naming prohibition on elements without role ([f67bd5d](https://github.com/markuplint/markuplint/commit/f67bd5dcb33f28ef3daf3ecd3b5d30ad4ced7814)), closes [#3630](https://github.com/markuplint/markuplint/issues/3630)
33
+ - **rules:** implement conditional type resolution for input value ([#3598](https://github.com/markuplint/markuplint/issues/3598)) ([d478ce6](https://github.com/markuplint/markuplint/commit/d478ce669387c868dd2e4b81cad32bcccddef7f2))
34
+ - **rules:** split wai-aria into 16 granular sub-rules ([1aded91](https://github.com/markuplint/markuplint/commit/1aded912ca839a75b89d14f3ae6ba30c2701a61b)), closes [#3645](https://github.com/markuplint/markuplint/issues/3645)
35
+
6
36
  # [5.0.0-rc.1](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.0...v5.0.0-rc.1) (2026-03-27)
7
37
 
8
38
  **Note:** Version bump only for package @markuplint/rules
package/CLAUDE.md ADDED
@@ -0,0 +1,27 @@
1
+ # @markuplint/rules
2
+
3
+ ## Test ID Convention (MANDATORY)
4
+
5
+ Every `test()` block in `src/**/*.spec.ts` MUST have a unique ID prefix:
6
+
7
+ ```
8
+ [rule-name-category-NNN] description
9
+ ```
10
+
11
+ ### Categories
12
+
13
+ | Category | When to use |
14
+ | ------------ | --------------------------------------------------------- |
15
+ | `valid` | Test expects 0 violations |
16
+ | `invalid` | Test expects 1+ violations |
17
+ | `fix` | Test checks `fixedCode` or auto-fix behavior |
18
+ | `parser` | Test uses a non-default parser (Vue, React, Pug, etc.) |
19
+ | `issue-NNNN` | Regression test for a GitHub issue (use the issue number) |
20
+
21
+ ### Rules
22
+
23
+ - IDs go on `test()` blocks only (not `describe()`)
24
+ - Numbers are sequential per category per file, starting at `001`
25
+ - Multiple tests for the same issue: `[rule-name-issue-NNNN-001]`, `[rule-name-issue-NNNN-002]`
26
+ - When adding a new test, assign the next available number in the appropriate category
27
+ - Run `/list-rule-test` (or `node .claude/commands/scripts/list-rule-test.mjs --no-id`) to check for missing IDs
package/SKILL.md CHANGED
@@ -58,6 +58,7 @@ const { violations } = await mlRuleTest(rule, '<button role="separator" ...>', {
58
58
  - `toStrictEqual` with exact `{ severity, line, col, message, raw }` — **never** loose assertions
59
59
  - Always include both valid and invalid cases
60
60
  - Some roles need ARIA attributes in HTML: `separator` → `aria-valuenow`, `meter` → `aria-valuenow`
61
+ - **Every `test()` MUST have a unique ID prefix** — see `CLAUDE.md` in this package for the full convention
61
62
 
62
63
  ## Task: update-test-expectations
63
64
 
@@ -34,7 +34,9 @@ type Loc = {
34
34
  * 2. Verifies the attribute exists in the spec
35
35
  * 3. Checks case-sensitive name matching
36
36
  * 4. Checks whether the attribute is marked as `noUse` (disallowed)
37
- * 5. Validates the attribute value against all declared types
37
+ * 5. Safety-net fallback for `ConditionalAttributeType[]` (#3685); in normal flow,
38
+ * `isValidAttr()` resolves conditional types before calling this function (#3598)
39
+ * 6. Validates the attribute value against all declared types
38
40
  *
39
41
  * @param t - The i18n translator for generating localized error messages
40
42
  * @param name - The attribute name to check
package/lib/attr-check.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isConditionalAttributeTypeArray } from '@markuplint/ml-spec';
1
2
  import { toNonNullableArrayFromItemOrArray } from '@markuplint/shared';
2
3
  import { check, getCandidate } from '@markuplint/types';
3
4
  import { createMessageValueExpected } from './create-message.js';
@@ -11,7 +12,9 @@ import { log } from './debug.js';
11
12
  * 2. Verifies the attribute exists in the spec
12
13
  * 3. Checks case-sensitive name matching
13
14
  * 4. Checks whether the attribute is marked as `noUse` (disallowed)
14
- * 5. Validates the attribute value against all declared types
15
+ * 5. Safety-net fallback for `ConditionalAttributeType[]` (#3685); in normal flow,
16
+ * `isValidAttr()` resolves conditional types before calling this function (#3598)
17
+ * 6. Validates the attribute value against all declared types
15
18
  *
16
19
  * @param t - The i18n translator for generating localized error messages
17
20
  * @param name - The attribute name to check
@@ -72,6 +75,12 @@ export function attrCheck(t, name, value, isCustomRule, spec, allAttrNames) {
72
75
  message: t('{0} is {1:c}', t('the "{0*}" {1}', name, 'attribute'), 'disallowed'),
73
76
  };
74
77
  }
78
+ // Safety net: In normal flow, `isValidAttr()` resolves ConditionalAttributeType[]
79
+ // before calling this function (#3598). This guard handles direct calls.
80
+ if (isConditionalAttributeTypeArray(spec.type)) {
81
+ log('The "%s" attribute uses ConditionalAttributeType[] — unresolved, treating as valid', name);
82
+ return false;
83
+ }
75
84
  const types = toNonNullableArrayFromItemOrArray(spec.type);
76
85
  const invalidMap = new Map();
77
86
  for (const type of types) {
package/lib/helpers.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isConditionalAttributeTypeArray } from '@markuplint/ml-spec';
1
2
  import { attrCheck } from './attr-check.js';
2
3
  /**
3
4
  * Tests whether an element matches the condition specified in an attribute spec.
@@ -93,8 +94,20 @@ export function isValidAttr(t, name, value, isDynamicValue,
93
94
  node, attrSpecs,
94
95
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
95
96
  log) {
96
- const spec = attrSpecs.find(s => s.name.toLowerCase() === name.toLowerCase());
97
+ let spec = attrSpecs.find(s => s.name.toLowerCase() === name.toLowerCase());
97
98
  log?.('Spec of the %s attr: %o', name, spec);
99
+ // Resolve ConditionalAttributeType[] to a concrete type based on element matching (#3598).
100
+ if (spec && isConditionalAttributeTypeArray(spec.type)) {
101
+ const matched = spec.type.find(entry => {
102
+ const cond = typeof entry.condition === 'string' ? entry.condition : entry.condition.join(',');
103
+ return node.matches(cond);
104
+ });
105
+ log?.('ConditionalAttributeType resolution for %s: %o', name, matched);
106
+ // Fallback to 'Any' when no condition matches: input types without an explicit
107
+ // entry (text, search, tel, password, hidden, checkbox, radio, file, submit,
108
+ // image, reset, button) have no value constraints per the HTML spec.
109
+ spec = { ...spec, type: matched ? matched.type : 'Any' };
110
+ }
98
111
  const allAttrNames = attrSpecs.map(s => s.name);
99
112
  let invalid = attrCheck(t, name, value, false, spec, allAttrNames);
100
113
  if (invalid === false &&
@@ -127,6 +140,8 @@ export function toNormalizedValue(value, spec) {
127
140
  if (!spec.caseSensitive) {
128
141
  normalized = normalized.toLowerCase();
129
142
  }
143
+ // When spec.type is an array (AttributeType[] or ConditionalAttributeType[]),
144
+ // type-specific normalization is skipped — only caseSensitive applies above.
130
145
  if (typeof spec.type === 'string') {
131
146
  if (spec.type[0] === '<') {
132
147
  normalized = normalized.toLowerCase().trim().replaceAll(/\s+/g, ' ');
package/lib/index.d.ts CHANGED
@@ -64,7 +64,9 @@ declare const rules: {
64
64
  readonly 'no-boolean-attr-value': Readonly<import("@markuplint/ml-core").RuleSeed<import("@markuplint/ml-core").RuleConfigValue, undefined>>;
65
65
  readonly 'no-consecutive-br': Readonly<import("@markuplint/ml-core").RuleSeed<import("@markuplint/ml-core").RuleConfigValue, undefined>>;
66
66
  readonly 'no-default-value': Readonly<import("@markuplint/ml-core").RuleSeed<import("@markuplint/ml-core").RuleConfigValue, undefined>>;
67
+ readonly 'no-duplicate-autofocus': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, null>>;
67
68
  readonly 'no-duplicate-dt': Readonly<import("@markuplint/ml-core").RuleSeed<import("@markuplint/ml-core").RuleConfigValue, undefined>>;
69
+ readonly 'no-duplicate-visible-main': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, null>>;
68
70
  readonly 'no-empty-palpable-content': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, {
69
71
  extendsExposableElements?: boolean;
70
72
  ignoreIfAriaBusy?: boolean;
@@ -120,5 +122,21 @@ declare const rules: {
120
122
  prevCodeBlock?: boolean;
121
123
  }>>;
122
124
  readonly 'wai-aria': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
125
+ readonly 'wai-aria-abstract-role': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
126
+ readonly 'wai-aria-default-value': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
127
+ readonly 'wai-aria-deprecated-props': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
128
+ readonly 'wai-aria-deprecated-role': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
129
+ readonly 'wai-aria-disallowed-props': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
130
+ readonly 'wai-aria-implicit-props': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
131
+ readonly 'wai-aria-implicit-role': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
132
+ readonly 'wai-aria-interaction-in-hidden': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
133
+ readonly 'wai-aria-no-global-prop': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
134
+ readonly 'wai-aria-non-existent-role': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
135
+ readonly 'wai-aria-permitted-roles': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
136
+ readonly 'wai-aria-presentational-children': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
137
+ readonly 'wai-aria-required-owned-elements': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
138
+ readonly 'wai-aria-required-parent-role': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
139
+ readonly 'wai-aria-required-props': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
140
+ readonly 'wai-aria-value': Readonly<import("@markuplint/ml-core").RuleSeed<boolean, import("./wai-aria/types.js").Options>>;
123
141
  };
124
142
  export default rules;
package/lib/index.js CHANGED
@@ -32,7 +32,9 @@ import NoAmbiguousNavigableTargetNames from './no-ambiguous-navigable-target-nam
32
32
  import NoBooleanAttrValue from './no-boolean-attr-value/index.js';
33
33
  import NoConsecutiveBr from './no-consecutive-br/index.js';
34
34
  import NoDefaultValue from './no-default-value/index.js';
35
+ import NoDuplicateAutofocus from './no-duplicate-autofocus/index.js';
35
36
  import NoDuplicateDt from './no-duplicate-dt/index.js';
37
+ import NoDuplicateVisibleMain from './no-duplicate-visible-main/index.js';
36
38
  import NoEmptyPalpableContent from './no-empty-palpable-content/index.js';
37
39
  import NoHardCodeId from './no-hard-code-id/index.js';
38
40
  import NoOrphanedEndTag from './no-orphaned-end-tag/index.js';
@@ -52,6 +54,22 @@ import RequiredH1 from './required-h1/index.js';
52
54
  import TableRowColumnAlignment from './table-row-column-alignment/index.js';
53
55
  import UseList from './use-list/index.js';
54
56
  import WaiAria from './wai-aria/index.js';
57
+ import WaiAriaAbstractRole from './wai-aria-abstract-role/index.js';
58
+ import WaiAriaDefaultValue from './wai-aria-default-value/index.js';
59
+ import WaiAriaDeprecatedProps from './wai-aria-deprecated-props/index.js';
60
+ import WaiAriaDeprecatedRole from './wai-aria-deprecated-role/index.js';
61
+ import WaiAriaDisallowedProps from './wai-aria-disallowed-props/index.js';
62
+ import WaiAriaImplicitProps from './wai-aria-implicit-props/index.js';
63
+ import WaiAriaImplicitRole from './wai-aria-implicit-role/index.js';
64
+ import WaiAriaInteractionInHidden from './wai-aria-interaction-in-hidden/index.js';
65
+ import WaiAriaNoGlobalProp from './wai-aria-no-global-prop/index.js';
66
+ import WaiAriaNonExistentRole from './wai-aria-non-existent-role/index.js';
67
+ import WaiAriaPermittedRoles from './wai-aria-permitted-roles/index.js';
68
+ import WaiAriaPresentationalChildren from './wai-aria-presentational-children/index.js';
69
+ import WaiAriaRequiredOwnedElements from './wai-aria-required-owned-elements/index.js';
70
+ import WaiAriaRequiredParentRole from './wai-aria-required-parent-role/index.js';
71
+ import WaiAriaRequiredProps from './wai-aria-required-props/index.js';
72
+ import WaiAriaValue from './wai-aria-value/index.js';
55
73
  /**
56
74
  * Registry of all built-in markuplint rules, mapping rule names to their seed definitions.
57
75
  * Used by the markuplint core to initialize rule instances during linting.
@@ -83,7 +101,9 @@ const rules = {
83
101
  'no-boolean-attr-value': NoBooleanAttrValue,
84
102
  'no-consecutive-br': NoConsecutiveBr,
85
103
  'no-default-value': NoDefaultValue,
104
+ 'no-duplicate-autofocus': NoDuplicateAutofocus,
86
105
  'no-duplicate-dt': NoDuplicateDt,
106
+ 'no-duplicate-visible-main': NoDuplicateVisibleMain,
87
107
  'no-empty-palpable-content': NoEmptyPalpableContent,
88
108
  'no-hard-code-id': NoHardCodeId,
89
109
  'no-orphaned-end-tag': NoOrphanedEndTag,
@@ -103,5 +123,21 @@ const rules = {
103
123
  'table-row-column-alignment': TableRowColumnAlignment,
104
124
  'use-list': UseList,
105
125
  'wai-aria': WaiAria,
126
+ 'wai-aria-abstract-role': WaiAriaAbstractRole,
127
+ 'wai-aria-default-value': WaiAriaDefaultValue,
128
+ 'wai-aria-deprecated-props': WaiAriaDeprecatedProps,
129
+ 'wai-aria-deprecated-role': WaiAriaDeprecatedRole,
130
+ 'wai-aria-disallowed-props': WaiAriaDisallowedProps,
131
+ 'wai-aria-implicit-props': WaiAriaImplicitProps,
132
+ 'wai-aria-implicit-role': WaiAriaImplicitRole,
133
+ 'wai-aria-interaction-in-hidden': WaiAriaInteractionInHidden,
134
+ 'wai-aria-no-global-prop': WaiAriaNoGlobalProp,
135
+ 'wai-aria-non-existent-role': WaiAriaNonExistentRole,
136
+ 'wai-aria-permitted-roles': WaiAriaPermittedRoles,
137
+ 'wai-aria-presentational-children': WaiAriaPresentationalChildren,
138
+ 'wai-aria-required-owned-elements': WaiAriaRequiredOwnedElements,
139
+ 'wai-aria-required-parent-role': WaiAriaRequiredParentRole,
140
+ 'wai-aria-required-props': WaiAriaRequiredProps,
141
+ 'wai-aria-value': WaiAriaValue,
106
142
  };
107
143
  export default rules;
@@ -25,6 +25,18 @@ export default createRule({
25
25
  if (attr.isDirective) {
26
26
  return;
27
27
  }
28
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#attr-is
29
+ // "The is attribute must not be specified on an autonomous custom element"
30
+ if (attr.name === 'is' && attr.ownerElement.elementType === 'web-component') {
31
+ report({
32
+ scope: attr,
33
+ line: attr.nameNode?.startLine,
34
+ col: attr.nameNode?.startCol,
35
+ raw: attr.nameNode?.raw,
36
+ message: t('The "{0}" attribute must not be specified on an autonomous custom element', 'is'),
37
+ });
38
+ return;
39
+ }
28
40
  const attrSpecs = getAttrSpecs(attr.ownerElement, document.specs);
29
41
  const attrName = attr.nameNode;
30
42
  const name = attr.name;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Ensures no more than one element in a document has the `autofocus` attribute.
3
+ *
4
+ * @see https://html.spec.whatwg.org/multipage/interaction.html#the-autofocus-attribute
5
+ */
6
+ declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, null>>;
7
+ export default _default;
@@ -0,0 +1,27 @@
1
+ import { createRule } from '@markuplint/ml-core';
2
+ import meta from './meta.js';
3
+ /**
4
+ * Ensures no more than one element in a document has the `autofocus` attribute.
5
+ *
6
+ * @see https://html.spec.whatwg.org/multipage/interaction.html#the-autofocus-attribute
7
+ */
8
+ export default createRule({
9
+ meta: meta,
10
+ defaultValue: true,
11
+ defaultOptions: null,
12
+ async verify({ document, report, t }) {
13
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
14
+ const autofocusElements = [];
15
+ await document.walkOn('Element', el => {
16
+ if (el.hasAttribute('autofocus')) {
17
+ autofocusElements.push(el);
18
+ }
19
+ });
20
+ for (const el of autofocusElements.slice(1)) {
21
+ report({
22
+ scope: el,
23
+ message: t('The "{0}" attribute must be unique in the document', 'autofocus'),
24
+ });
25
+ }
26
+ },
27
+ });
@@ -0,0 +1,4 @@
1
+ declare const _default: {
2
+ readonly category: "validation";
3
+ };
4
+ export default _default;
@@ -0,0 +1,3 @@
1
+ export default {
2
+ category: 'validation',
3
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Ensures no more than one visible `<main>` element exists in a document.
3
+ * A `<main>` element with the `hidden` attribute is not considered visible.
4
+ *
5
+ * @see https://html.spec.whatwg.org/multipage/grouping-content.html#the-main-element
6
+ */
7
+ declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, null>>;
8
+ export default _default;
@@ -0,0 +1,28 @@
1
+ import { createRule } from '@markuplint/ml-core';
2
+ import meta from './meta.js';
3
+ /**
4
+ * Ensures no more than one visible `<main>` element exists in a document.
5
+ * A `<main>` element with the `hidden` attribute is not considered visible.
6
+ *
7
+ * @see https://html.spec.whatwg.org/multipage/grouping-content.html#the-main-element
8
+ */
9
+ export default createRule({
10
+ meta: meta,
11
+ defaultValue: true,
12
+ defaultOptions: null,
13
+ async verify({ document, report, t }) {
14
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
15
+ const visibleMains = [];
16
+ await document.walkOn('Element', el => {
17
+ if (el.localName === 'main' && !el.hasAttribute('hidden')) {
18
+ visibleMains.push(el);
19
+ }
20
+ });
21
+ for (const el of visibleMains.slice(1)) {
22
+ report({
23
+ scope: el,
24
+ message: t('There must not be more than one visible "{0}" element in a document', 'main'),
25
+ });
26
+ }
27
+ },
28
+ });
@@ -0,0 +1,4 @@
1
+ declare const _default: {
2
+ readonly category: "validation";
3
+ };
4
+ export default _default;
@@ -0,0 +1,3 @@
1
+ export default {
2
+ category: 'validation',
3
+ };
@@ -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 { PermittedContentChoice } from '@markuplint/ml-spec';
3
3
  import type { ReadonlyDeep } from 'type-fest';
4
4
  /**
@@ -13,9 +13,12 @@ import type { ReadonlyDeep } from 'type-fest';
13
13
  *
14
14
  * @param pattern - The choice pattern containing multiple alternative content model branches.
15
15
  * @param childNodes - The child nodes to validate against the choice branches.
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 from the best-matching branch, or the branch that came closest to matching.
20
23
  */
21
- export declare function choice(pattern: ReadonlyDeep<PermittedContentChoice>, childNodes: readonly ChildNode[], specs: Specs, options: Options, depth: number): Result;
24
+ export declare function choice(pattern: ReadonlyDeep<PermittedContentChoice>, childNodes: readonly ChildNode[], rules: readonly TagRule[], specs: Specs, options: Options, depth: number, mode: Mode): Result;
@@ -18,21 +18,24 @@ const indexes = new WeakMap();
18
18
  *
19
19
  * @param pattern - The choice pattern containing multiple alternative content model branches.
20
20
  * @param childNodes - The child nodes to validate against the choice branches.
21
+ * @param rules - User-defined tag rules. Threaded through for transparent-model recursion;
22
+ * not consulted here directly. See `order` for the rationale.
21
23
  * @param specs - The resolved spec data for content model lookups.
22
24
  * @param options - Validation behavior options.
23
25
  * @param depth - The current recursion depth, used for debug logging and nested evaluation.
26
+ * @param mode - Whether we are evaluating the element's `'origin'` or `'pretended'` identity.
24
27
  * @returns A result from the best-matching branch, or the branch that came closest to matching.
25
28
  */
26
29
  export function choice(pattern,
27
30
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
28
- childNodes, specs, options, depth) {
31
+ childNodes, rules, specs, options, depth, mode) {
29
32
  const choiceLog = cmLog.extend(`choice#${depth}`);
30
33
  const collection = new Collection(childNodes);
31
34
  const unmatchedResults = [];
32
35
  let i = 0;
33
36
  for (const some of pattern.choice) {
34
37
  choiceLog('Patterns[%s]: %s', i, modelLog(some, ''));
35
- const result = order(some, collection.unmatched, specs, options, depth + 1);
38
+ const result = order(some, collection.unmatched, rules, specs, options, depth + 1, mode);
36
39
  if (result.type === 'MATCHED' ||
37
40
  result.type === 'MATCHED_ZERO' ||
38
41
  (result.type === 'UNEXPECTED_EXTRA_NODE' && result.matched.length > 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 { PermittedContentPattern } from '@markuplint/ml-spec';
3
3
  import type { ReadonlyDeep } from 'type-fest';
4
4
  /**
@@ -10,9 +10,12 @@ import type { ReadonlyDeep } from 'type-fest';
10
10
  *
11
11
  * @param pattern - A single content model pattern to evaluate.
12
12
  * @param childNodes - The child nodes to validate against the pattern.
13
+ * @param rules - User-defined tag rules. Threaded through for transparent-model recursion;
14
+ * not consulted here directly. See `order` for the rationale.
13
15
  * @param specs - The resolved spec data for content model lookups.
14
16
  * @param options - Validation behavior options.
15
17
  * @param depth - The current recursion depth, used for debug logging and nested evaluation.
18
+ * @param mode - Whether we are evaluating the element's `'origin'` or `'pretended'` identity.
16
19
  * @returns A result indicating whether the child nodes match the pattern.
17
20
  */
18
- export declare function complexBranch(pattern: ReadonlyDeep<PermittedContentPattern>, childNodes: readonly ChildNode[], specs: Specs, options: Options, depth: number): Result;
21
+ export declare function complexBranch(pattern: ReadonlyDeep<PermittedContentPattern>, childNodes: readonly ChildNode[], rules: readonly TagRule[], specs: Specs, options: Options, depth: number, mode: Mode): Result;
@@ -11,19 +11,22 @@ import { isChoice, isTransparent } from './utils.js';
11
11
  *
12
12
  * @param pattern - A single content model pattern to evaluate.
13
13
  * @param childNodes - The child nodes to validate against the pattern.
14
+ * @param rules - User-defined tag rules. Threaded through for transparent-model recursion;
15
+ * not consulted here directly. See `order` for the rationale.
14
16
  * @param specs - The resolved spec data for content model lookups.
15
17
  * @param options - Validation behavior options.
16
18
  * @param depth - The current recursion depth, used for debug logging and nested evaluation.
19
+ * @param mode - Whether we are evaluating the element's `'origin'` or `'pretended'` identity.
17
20
  * @returns A result indicating whether the child nodes match the pattern.
18
21
  */
19
22
  export function complexBranch(pattern,
20
23
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
21
- childNodes, specs, options, depth) {
24
+ childNodes, rules, specs, options, depth, mode) {
22
25
  if (isChoice(pattern)) {
23
- return choice(pattern, childNodes, specs, options, depth);
26
+ return choice(pattern, childNodes, rules, specs, options, depth, mode);
24
27
  }
25
28
  if (isTransparent(pattern)) {
26
29
  return transparent(childNodes);
27
30
  }
28
- return countPattern(pattern, childNodes, specs, options, depth);
31
+ return countPattern(pattern, childNodes, rules, specs, options, depth, mode);
29
32
  }
@@ -1,4 +1,6 @@
1
- import type { ContentModelResult, Element, Options, TagRule } from './types.js';
1
+ import type { ContentModelResult, Element, Mode, Options, Specs, TagRule } from './types.js';
2
+ import type { ContentModel } from '@markuplint/ml-spec';
3
+ import type { ReadonlyDeep } from 'type-fest';
2
4
  /**
3
5
  * Top-level entry point for content model validation of a single element.
4
6
  * Resolves the element's content model from the spec (possibly augmented
@@ -8,6 +10,23 @@ import type { ContentModelResult, Element, Options, TagRule } from './types.js';
8
10
  * @param el - The element whose children are to be validated against its content model.
9
11
  * @param rules - User-defined tag rules that can override or extend built-in content models.
10
12
  * @param options - Validation behavior options (e.g., whether to ignore mutable children).
13
+ * @param mode - Which identity to evaluate the element as (`'origin'` consults user
14
+ * rules keyed on the pre-pretender AST name; `'pretended'` uses the
15
+ * HTML spec for the pretender target).
11
16
  * @returns An array of content model results, one per child node issue found (empty if all valid).
12
17
  */
13
- export declare function contentModel(el: Element, rules: readonly TagRule[], options: Options): ContentModelResult[];
18
+ export declare function contentModel(el: Element, rules: readonly TagRule[], options: Options, mode: Mode): ContentModelResult[];
19
+ /**
20
+ * Resolves the content model for an element while honoring the current
21
+ * {@link Mode}. In `'origin'` mode for a pretendered element, the lookup
22
+ * first consults user-defined tag rules keyed on the element's original AST
23
+ * name (`rawName`); if no such rule exists the returned model is `null`,
24
+ * which signals the caller to skip validation for this mode. In all other
25
+ * cases the standard spec lookup (`getContentModel`) is used, which keys on
26
+ * the visible `localName` — i.e. the pretender target when pretending is
27
+ * active.
28
+ *
29
+ * Exported so that {@link representTransparentNodes} can reuse the same
30
+ * resolution logic when recursing into child content models.
31
+ */
32
+ export declare function resolveContentModel(el: Element, rules: readonly TagRule[], specs: Specs, mode: Mode): ReadonlyDeep<ContentModel['contents']> | null;
@@ -9,12 +9,15 @@ import { start } from './start.js';
9
9
  * @param el - The element whose children are to be validated against its content model.
10
10
  * @param rules - User-defined tag rules that can override or extend built-in content models.
11
11
  * @param options - Validation behavior options (e.g., whether to ignore mutable children).
12
+ * @param mode - Which identity to evaluate the element as (`'origin'` consults user
13
+ * rules keyed on the pre-pretender AST name; `'pretended'` uses the
14
+ * HTML spec for the pretender target).
12
15
  * @returns An array of content model results, one per child node issue found (empty if all valid).
13
16
  */
14
17
  export function contentModel(
15
18
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
16
- el, rules, options) {
17
- const { model, specs } = createModel(el, rules);
19
+ el, rules, options, mode) {
20
+ const { model, specs } = createModel(el, rules, mode);
18
21
  if (model == null) {
19
22
  return [
20
23
  {
@@ -25,28 +28,51 @@ el, rules, options) {
25
28
  },
26
29
  ];
27
30
  }
28
- const result = start(model, el, specs, options);
31
+ const result = start(model, el, rules, specs, options, mode);
29
32
  return result;
30
33
  }
31
34
  /**
32
35
  * Builds the content model and merged specs for a given element.
33
36
  * Combines the element's document-level specs with any user-defined
34
- * tag rules, then looks up the content model for the element.
37
+ * tag rules, then resolves the content model for the current {@link Mode}.
35
38
  *
36
39
  * @param el - The element to look up the content model for.
37
40
  * @param rules - User-defined tag rules to merge into the spec.
41
+ * @param mode - Which identity to resolve against.
38
42
  * @returns An object containing the resolved content model and merged specs.
39
43
  */
40
44
  function createModel(
41
45
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
42
- el, rules) {
46
+ el, rules, mode) {
43
47
  const specs = cachedSpecs(el.ownerMLDocument.specs, rules);
44
- const model = getContentModel(el, specs.specs);
48
+ const model = resolveContentModel(el, rules, specs, mode);
45
49
  return {
46
50
  model,
47
51
  specs,
48
52
  };
49
53
  }
54
+ /**
55
+ * Resolves the content model for an element while honoring the current
56
+ * {@link Mode}. In `'origin'` mode for a pretendered element, the lookup
57
+ * first consults user-defined tag rules keyed on the element's original AST
58
+ * name (`rawName`); if no such rule exists the returned model is `null`,
59
+ * which signals the caller to skip validation for this mode. In all other
60
+ * cases the standard spec lookup (`getContentModel`) is used, which keys on
61
+ * the visible `localName` — i.e. the pretender target when pretending is
62
+ * active.
63
+ *
64
+ * Exported so that {@link representTransparentNodes} can reuse the same
65
+ * resolution logic when recursing into child content models.
66
+ */
67
+ export function resolveContentModel(
68
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
69
+ el, rules, specs, mode) {
70
+ if (mode === 'origin' && el.pretenderContext?.type === 'pretender') {
71
+ const userRule = rules.find(r => r.tag === el.rawName);
72
+ return userRule?.contents ?? null;
73
+ }
74
+ return getContentModel(el, specs.specs);
75
+ }
50
76
  /**
51
77
  * Cache for merged specs keyed by the JSON-serialized tag rules.
52
78
  * Avoids re-merging the same set of user-defined tag rules on every element check.
@@ -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;