@markuplint/rules 5.4.0 → 5.6.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 +34 -0
- package/CLAUDE.md +4 -3
- package/lib/aria-prop-requires-role/index.js +2 -2
- package/lib/attr-eligibility.d.ts +21 -0
- package/lib/attr-eligibility.js +86 -6
- package/lib/attr-order/index.js +2 -1
- package/lib/attr-value-quotes/index.js +2 -2
- package/lib/consistent-table-row-length/index.js +1 -1
- package/lib/create-message.js +6 -5
- package/lib/element-supports-aria-prop/index.js +2 -2
- package/lib/form-attr-references-form/index.js +3 -3
- package/lib/helpers.d.ts +96 -5
- package/lib/helpers.js +187 -29
- package/lib/input-list-references-datalist/index.js +3 -3
- package/lib/itemprop-requires-itemscope/index.js +28 -11
- package/lib/label-for-references-labelable/index.js +3 -3
- package/lib/label-has-control/index.d.ts +17 -0
- package/lib/label-has-control/index.js +28 -1
- package/lib/label-no-multiple-controls/index.d.ts +4 -5
- package/lib/label-no-multiple-controls/index.js +8 -8
- package/lib/landmark-roles/roles.d.ts +13 -1
- package/lib/landmark-roles/roles.js +22 -0
- package/lib/link-types/index.js +8 -8
- package/lib/map-id-name-match/index.js +1 -1
- package/lib/meter-value-bounds/index.js +3 -3
- package/lib/no-ambiguous-navigable-target-names/index.js +5 -1
- package/lib/no-aria-hidden-on-hidden-until-found/index.js +1 -1
- package/lib/no-aria-on-presentational-children/index.js +4 -0
- package/lib/no-aria-on-unsupported-element/index.js +2 -2
- package/lib/no-boolean-attr-value/index.js +2 -1
- package/lib/no-broken-fragment-link/index.d.ts +5 -0
- package/lib/no-broken-fragment-link/index.js +8 -2
- package/lib/no-consecutive-br/index.js +1 -1
- package/lib/no-content-after-body/index.js +2 -2
- package/lib/no-contradictory-aria-prop/index.js +2 -2
- package/lib/no-default-aria-value/index.js +2 -2
- package/lib/no-default-value/index.js +4 -0
- package/lib/no-deprecated-aria-prop/index.js +2 -2
- package/lib/no-disallowed-ancestor/index.d.ts +16 -1
- package/lib/no-disallowed-ancestor/index.js +68 -5
- package/lib/no-disallowed-attr/index.js +5 -1
- package/lib/no-duplicate-autofocus/index.js +1 -1
- package/lib/no-duplicate-dt/index.js +1 -1
- package/lib/no-duplicate-h1/index.js +18 -14
- package/lib/no-duplicate-id/index.d.ts +9 -0
- package/lib/no-duplicate-id/index.js +23 -4
- package/lib/no-duplicate-sibling-attr/index.js +1 -1
- package/lib/no-duplicate-visible-main/index.js +1 -1
- package/lib/no-empty-table-track/index.js +1 -1
- package/lib/no-extra-selected-options/index.js +1 -1
- package/lib/no-ineffective-attr/index.js +4 -0
- package/lib/no-input-file-value/index.js +1 -1
- package/lib/no-invalid-aria-prop-value/index.js +3 -3
- package/lib/no-invalid-attr-value/index.js +12 -0
- package/lib/no-malformed-character-reference/index.js +1 -1
- package/lib/no-nested-top-level-landmark/index.js +20 -17
- package/lib/no-prohibited-naming/index.js +2 -2
- package/lib/no-redundant-accessible-name/index.js +15 -8
- package/lib/no-redundant-aria-prop/index.js +2 -2
- package/lib/no-refer-to-non-existent-id/index.d.ts +15 -0
- package/lib/no-refer-to-non-existent-id/index.js +47 -16
- package/lib/no-restricted-attr/index.js +2 -1
- package/lib/no-restricted-element/index.js +4 -2
- package/lib/no-skipped-heading-level/index.js +9 -4
- package/lib/no-stray-head-or-body-tag/index.d.ts +2 -1
- package/lib/no-stray-head-or-body-tag/index.js +4 -3
- package/lib/no-table-cell-overlap/index.js +3 -2
- package/lib/no-table-span-overflow/index.js +1 -1
- package/lib/no-translate.d.ts +17 -0
- package/lib/no-translate.js +29 -0
- package/lib/no-unclosed-element-at-eof/index.js +1 -1
- package/lib/no-unescaped-char/index.js +1 -1
- package/lib/no-unsupported-browser-features/index.d.ts +9 -0
- package/lib/no-unsupported-browser-features/index.js +10 -7
- package/lib/permitted-contents/content-model.d.ts +14 -0
- package/lib/permitted-contents/content-model.js +21 -1
- package/lib/permitted-contents/index.js +6 -8
- package/lib/permitted-contents/matches-selector.js +32 -5
- package/lib/permitted-contents/represent-transparent-nodes.js +100 -68
- package/lib/permitted-contents/slot-content.d.ts +18 -1
- package/lib/permitted-contents/slot-content.js +49 -2
- package/lib/permitted-contents/start.d.ts +8 -1
- package/lib/permitted-contents/start.js +39 -6
- package/lib/permitted-contents/utils.d.ts +11 -0
- package/lib/permitted-contents/utils.js +15 -0
- package/lib/placeholder-label-option/index.js +4 -26
- package/lib/progress-value-bounds/index.js +2 -2
- package/lib/require-accessible-name/index.js +2 -2
- package/lib/require-adjacent-popover/index.js +4 -2
- package/lib/require-ancestor/index.d.ts +37 -1
- package/lib/require-ancestor/index.js +82 -9
- package/lib/require-ancestor/meta.d.ts +5 -0
- package/lib/require-ancestor/meta.js +5 -0
- package/lib/require-attr/index.js +4 -1
- package/lib/require-datetime/index.js +3 -2
- package/lib/require-dialog-autofocus/index.js +8 -6
- package/lib/require-element/index.js +8 -2
- package/lib/require-h1/index.js +4 -8
- package/lib/require-landmark-label/index.js +17 -15
- package/lib/role-supports-aria-prop/index.js +2 -2
- package/lib/sizes-auto-requires-lazy-loading/index.d.ts +4 -2
- package/lib/sizes-auto-requires-lazy-loading/index.js +4 -2
- package/lib/usemap-references-map/index.js +12 -3
- package/lib/valid-speculation-rules/index.js +1 -1
- package/lib/wai-aria/checkings/contradictory-aria-prop.js +2 -2
- package/lib/wai-aria/checkings/interaction-in-hidden.js +6 -2
- package/lib/wai-aria/checkings/presentational-children.js +38 -17
- package/lib/wai-aria/checkings/redundant-aria-prop.js +1 -1
- package/lib/wai-aria/checkings/required-accessibility-parent-role.js +4 -2
- package/lib/wai-aria/checkings/required-owned-elements.d.ts +2 -0
- package/lib/wai-aria/checkings/required-owned-elements.js +175 -21
- package/lib/wai-aria/checkings/required-prop.js +2 -1
- package/lib/wai-aria/checkings/tab-requires-tabpanel.js +10 -6
- package/lib/wai-aria/checkings/value.d.ts +1 -4
- package/lib/wai-aria/checkings/value.js +8 -19
- package/package.json +14 -11
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,40 @@
|
|
|
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.6.0](https://github.com/markuplint/markuplint/compare/v5.5.0...v5.6.0) (2026-11-12)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* do not report an area element that has no parent node in the file ([916a166](https://github.com/markuplint/markuplint/commit/916a166916e4efd5292402abcd293927605ff799)), closes [#4528](https://github.com/markuplint/markuplint/issues/4528)
|
|
11
|
+
* **rules:** do not report an area that a component of unknown output may enclose in a map ([91f796a](https://github.com/markuplint/markuplint/commit/91f796acd865c4815c66bf733a80c838b6f43be3)), closes [#4575](https://github.com/markuplint/markuplint/issues/4575)
|
|
12
|
+
* **rules:** do not report what a component that no pretender resolved may render ([ecf810c](https://github.com/markuplint/markuplint/commit/ecf810c021b8fde9a6053b43293c5261f3911a46)), closes [#4572](https://github.com/markuplint/markuplint/issues/4572) [#4064](https://github.com/markuplint/markuplint/issues/4064)
|
|
13
|
+
* **rules:** read the declarative shadow root as owned by its host ([2324117](https://github.com/markuplint/markuplint/commit/2324117dc83627db98f1f2f2b2f6d26d6886708f)), closes [#4527](https://github.com/markuplint/markuplint/issues/4527)
|
|
14
|
+
* **rules:** read what a slot of a shadow root renders ([d3a5c76](https://github.com/markuplint/markuplint/commit/d3a5c76f5293322986ad0a122f13884f971735c5))
|
|
15
|
+
* **rules:** show every message in Japanese and the names in it as written ([9145904](https://github.com/markuplint/markuplint/commit/9145904e5b43fbeb51b5d5efa6e4735eddef7921))
|
|
16
|
+
* **rules:** write "an ambiguous" in the message of no-ambiguous-navigable-target-names ([9be22b3](https://github.com/markuplint/markuplint/commit/9be22b3949e114f08308d068c238666206384a45)), closes [#4585](https://github.com/markuplint/markuplint/issues/4585)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# [5.5.0](https://github.com/markuplint/markuplint/compare/v5.4.0...v5.5.0) (2026-11-10)
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
* **html-spec:** allow script-supporting elements in table sections, colgroup and dl ([197176a](https://github.com/markuplint/markuplint/commit/197176a7aae805e2949f9b96c6a27f39d478d261)), closes [#3591](https://github.com/markuplint/markuplint/issues/3591) [#4492](https://github.com/markuplint/markuplint/issues/4492)
|
|
24
|
+
* report a main element that is not hierarchically correct ([b44c936](https://github.com/markuplint/markuplint/commit/b44c936cf2e40c8142f88fe6b503af6f437cd341))
|
|
25
|
+
* **rules:** accept style as the first child in permitted-contents ([0523872](https://github.com/markuplint/markuplint/commit/05238722e9501b7758035d55293a178ab766001a)), closes [#x20](https://github.com/markuplint/markuplint/issues/x20)
|
|
26
|
+
* **rules:** accept the interactive content that canvas allows ([095eb3a](https://github.com/markuplint/markuplint/commit/095eb3aeb16186b0f5cb3f4a3bd342360dae1c2c)), closes [#4491](https://github.com/markuplint/markuplint/issues/4491)
|
|
27
|
+
* **rules:** count an element inside a conditional block as a label's descendant ([6ca1da5](https://github.com/markuplint/markuplint/commit/6ca1da545a903bec024f4418c3595f16d72365bc))
|
|
28
|
+
* **rules:** do not judge an element that the framework moves to the head by its parent ([db28a20](https://github.com/markuplint/markuplint/commit/db28a20bc562233f0c55594077a15276ec9c113a)), closes [#4484](https://github.com/markuplint/markuplint/issues/4484) [#4484](https://github.com/markuplint/markuplint/issues/4484)
|
|
29
|
+
* **rules:** do not look for an ancestor across the boundary of template content ([b660c8b](https://github.com/markuplint/markuplint/commit/b660c8bc5d2b4cbcc16f592ad536db9a2b8bdaaa))
|
|
30
|
+
* **rules:** do not treat template content as an accessible name ([0d004ef](https://github.com/markuplint/markuplint/commit/0d004efd84e6cd201cdcdc66163b89c4b0a714d2))
|
|
31
|
+
* **rules:** ignore comment nodes in permitted-contents ([38881eb](https://github.com/markuplint/markuplint/commit/38881eb71e80bfef45832a8f2378d336e5bfbe09)), closes [#4488](https://github.com/markuplint/markuplint/issues/4488)
|
|
32
|
+
* **rules:** judge an id and a structure in the tree that it is in ([#4489](https://github.com/markuplint/markuplint/issues/4489)) ([b17db6d](https://github.com/markuplint/markuplint/commit/b17db6d813012e127beb2c2b9e3fc21a393e2d4d))
|
|
33
|
+
* **rules:** require as to satisfy every matched link destination ([b4dbea2](https://github.com/markuplint/markuplint/commit/b4dbea2a08d761c90c98098102d7af40cf485849))
|
|
34
|
+
|
|
35
|
+
### Features
|
|
36
|
+
|
|
37
|
+
* **rules:** judge an attribute by what the framework renders ([3beee5a](https://github.com/markuplint/markuplint/commit/3beee5ad51e96fc3033b7930fec2889ea5bf18e6))
|
|
38
|
+
|
|
39
|
+
|
|
6
40
|
# [5.4.0](https://github.com/markuplint/markuplint/compare/v5.3.0...v5.4.0) (2026-10-29)
|
|
7
41
|
|
|
8
42
|
### Bug Fixes
|
package/CLAUDE.md
CHANGED
|
@@ -65,10 +65,11 @@ broken description and detonate in someone else's PR. Run `yarn site:build` befo
|
|
|
65
65
|
|
|
66
66
|
## Citing a specification
|
|
67
67
|
|
|
68
|
-
A README, a source comment, `meta.ts` and `schema.json` cite the passage that justifies a rule. `yarn spec-citations` checks every citation against the live specification (what it checks and why: the module comment of `scripts/spec-citations/cli.ts`), and `scripts/spec-citations/citations-policy.spec.ts` holds the conventions below offline. After editing a citation, run `yarn spec-citations` (read-only) and fix what it reports. Its lock file is refreshed by the scheduled workflow's PR, never by hand.
|
|
68
|
+
A README, a source comment, `meta.ts` and `schema.json` cite the passage that justifies a rule; so do the comments in the `src` of every package (the algorithms of `ml-spec`, the value grammars of `types`, the DOM of `ml-core`, the declarations of how a framework renders a literal in `*-spec` and `*-parser`), the `html-spec` data and the website's pages. The monitor reads the whole `src` of each package, so a citation that moves with its code to another package stays watched. `yarn spec-citations` checks every citation against the live specification, the documentation or the source of a framework (what it checks and why: the module comment of `scripts/spec-citations/cli.ts`), and `scripts/spec-citations/citations-policy.spec.ts` holds the conventions below offline. After editing a citation, run `yarn spec-citations` (read-only) and fix what it reports. Its lock file is refreshed by the scheduled workflow's PR, never by hand.
|
|
69
69
|
|
|
70
|
-
- Quote a spec sentence verbatim, as a blockquote in a README or as `>` lines in a source comment, and name its source: in a README the `Cite: [..](url)` line after the quote (`[..](url)より引用` in Japanese), in a comment an `@see` URL in the same comment. Never quote inline in double quotes: the monitor cannot check it. `[…]` marks what the quote leaves out.
|
|
71
|
-
- Cite a section by its fragment. A `§4.10.7` beside the link is compared with the section the fragment is in; the number of an ancestor section is accepted.
|
|
70
|
+
- Quote a spec sentence verbatim, as a blockquote in a README or as `>` lines in a source comment, and name its source: in a README the `Cite: [..](url)` line after the quote (`[..](url)より引用` in Japanese), in a comment an `@see` URL in the same comment. A `>` is read only at the start of a comment line (not after a list marker), and each quote is a paragraph of its own. Never quote inline in double quotes: the monitor cannot check it. `[…]` marks what the quote leaves out, and a quote is looked up in the sections the comment cites, in the order that it writes it in.
|
|
71
|
+
- Cite a section by its fragment. A `§4.10.7` beside the link is compared with the section the fragment is in (one before the link belongs to the link that follows it, one after a link on the same line, as in `@see <url> — Name §4.3.2`, to that link); the number of an ancestor section is accepted. Prefer the id of a heading to a text fragment (`#:~:text=`) alone: a text fragment with no `#fragment` is searched in the whole page, and stops matching when the passage is reworded (HTML-AAM's tables were rebuilt into one section per element, and `#:~:text=Comments-,article,-aside` went with them).
|
|
72
|
+
- A source file of a framework is cited by `https://github.com/<owner>/<repo>/blob/<default branch>/<path>#<name>`, where `<name>` is a top-level declaration of the file (a function, a variable, a class, a type, or `exports.<name> =`). It is compared as the declaration alone, so an edit elsewhere in the file reports nothing and a renamed declaration is `anchor-missing`. Only the repositories listed in `scripts/spec-citations/collect.ts` are monitored, and a link without a name or with a line range (`#L11-L52`) is not checked.
|
|
72
73
|
- `README.ja.md` cites the HTML Living Standard through its translation (`https://momdo.github.io/html/…`, the same page and anchor as the English README) and quotes the translation's own text. That text stays English where the translation is behind the original, and the monitor reports it once the translation catches up. For WCAG, the translation is waic.jp's.
|
|
73
74
|
- Cite W3C specs at an edition-less URL (`https://w3c.github.io/accname/`, `https://w3c.github.io/aria/`, `https://www.w3.org/TR/WCAG/`, `https://www.w3.org/TR/html-aria/`). WCAG's Techniques and Understanding pages have no edition-less URL; cite the latest edition (`https://www.w3.org/WAI/WCAG22/…`). Not `w3c.github.io/html-aria/`: that is unrendered ReSpec source whose anchors exist only after a browser builds them.
|
|
74
75
|
|
|
@@ -2,7 +2,7 @@ import { createRule, getComputedRole, ariaSpecs, getSpec } from '@markuplint/ml-
|
|
|
2
2
|
import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
|
|
3
3
|
import { checkingNoGlobalProp } from '../wai-aria/checkings/no-global-prop.js';
|
|
4
4
|
import { defaultOptions } from '../wai-aria/default-options.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getAriaAttrs } from '../helpers.js';
|
|
6
6
|
import meta from './meta.js';
|
|
7
7
|
export default createRule({
|
|
8
8
|
meta,
|
|
@@ -14,7 +14,7 @@ export default createRule({
|
|
|
14
14
|
return;
|
|
15
15
|
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
16
16
|
return;
|
|
17
|
-
const propAttrs = el
|
|
17
|
+
const propAttrs = getAriaAttrs(el);
|
|
18
18
|
if (propAttrs.length === 0)
|
|
19
19
|
return;
|
|
20
20
|
const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
@@ -41,8 +41,29 @@ export type AttrEligibility = {
|
|
|
41
41
|
* @param attr - The attribute that is being resolved
|
|
42
42
|
*/
|
|
43
43
|
export declare function resolveAttrEligibility<T extends RuleConfigValue, O extends PlainData>(name: string, node: Element<T, O>, attrSpecs: readonly Attribute[], attr?: Attr<T, O>): AttrEligibility;
|
|
44
|
+
type PlainEnum = {
|
|
45
|
+
readonly enum: readonly [string, ...string[]];
|
|
46
|
+
readonly caseInsensitive?: boolean;
|
|
47
|
+
readonly disallowToSurroundBySpaces?: boolean;
|
|
48
|
+
readonly invalidValueDefault?: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* `invalidValueDefault` is not carried over: a fallback of one set may be a
|
|
52
|
+
* value another set forbids. A case-sensitive spelling is kept when an earlier
|
|
53
|
+
* entry only has another case. Case and surrounding spaces follow the stricter
|
|
54
|
+
* side, because the value has to satisfy every entry.
|
|
55
|
+
*
|
|
56
|
+
* An empty intersection cannot be an enum (`enum` has at least one token).
|
|
57
|
+
* A pattern that matches nothing keeps every value invalid. That pattern is
|
|
58
|
+
* not a keyword list an author is meant to satisfy.
|
|
59
|
+
*
|
|
60
|
+
* @param enums - The enum entries that matched, in spec order. At least two
|
|
61
|
+
* @returns The intersection, or a pattern that matches nothing when the sets are disjoint
|
|
62
|
+
*/
|
|
63
|
+
export declare function intersectEnums(enums: readonly PlainEnum[]): Attribute['type'];
|
|
44
64
|
/**
|
|
45
65
|
* Formats the "did you mean" suffix shared by `no-unknown-attr`'s two typo
|
|
46
66
|
* paths (spec-driven candidate, case-sensitivity mismatch).
|
|
47
67
|
*/
|
|
48
68
|
export declare function suggestionSuffix(t: Translator, candidate: string | undefined): string;
|
|
69
|
+
export {};
|
package/lib/attr-eligibility.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isConditionalAttributeTypeArray } from '@markuplint/ml-spec';
|
|
2
|
-
import { getCandidate } from '@markuplint/types';
|
|
2
|
+
import { check, getCandidate } from '@markuplint/types';
|
|
3
3
|
import { attrMatches, conditionDependsOnDynamicAttr, findAttrSpec, namespaceKeyOf } from './helpers.js';
|
|
4
4
|
/**
|
|
5
5
|
* Attribute namespaces that are exempt from spec-based name/eligibility
|
|
@@ -43,14 +43,11 @@ attr) {
|
|
|
43
43
|
}
|
|
44
44
|
// Resolve ConditionalAttributeType[] to a concrete type based on element matching (#3598).
|
|
45
45
|
if (isConditionalAttributeTypeArray(spec.type)) {
|
|
46
|
-
const matched = spec.type.
|
|
46
|
+
const matched = spec.type.filter(entry => {
|
|
47
47
|
const cond = typeof entry.condition === 'string' ? entry.condition : entry.condition.join(',');
|
|
48
48
|
return node.matches(cond);
|
|
49
49
|
});
|
|
50
|
-
|
|
51
|
-
// entry (text, search, tel, password, hidden, checkbox, radio, file, submit,
|
|
52
|
-
// image, reset, button) have no value constraints per the HTML spec.
|
|
53
|
-
spec = { ...spec, type: matched ? matched.type : 'Any' };
|
|
50
|
+
spec = { ...spec, type: combineMatchedConditionalTypes(matched) };
|
|
54
51
|
}
|
|
55
52
|
if (spec.condition != null &&
|
|
56
53
|
!node.hasSpreadAttr &&
|
|
@@ -60,6 +57,89 @@ attr) {
|
|
|
60
57
|
}
|
|
61
58
|
return { status: 'ok', spec };
|
|
62
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Enum entries that match together are simultaneous constraints: `link[as]`
|
|
62
|
+
* has to be a preload destination and a module preload destination when `rel`
|
|
63
|
+
* contains both keywords. The message names that intersection and does not
|
|
64
|
+
* offer a keyword another entry forbids. Token order follows the entries:
|
|
65
|
+
* a spelling is kept from the earliest entry whose own token satisfies every
|
|
66
|
+
* entry, which is the first entry's order when that spelling is valid there.
|
|
67
|
+
*
|
|
68
|
+
* A non-enum overlap stays on the first match. Those entries are ordered
|
|
69
|
+
* alternatives: an empty `input[value]` other than `type=range` is
|
|
70
|
+
* unconstrained even though a later type-specific entry also matches, and
|
|
71
|
+
* `email` with `multiple` matches both the address-list type and the
|
|
72
|
+
* single-address type, of which the list is the requirement.
|
|
73
|
+
*/
|
|
74
|
+
function combineMatchedConditionalTypes(matched) {
|
|
75
|
+
const first = matched[0];
|
|
76
|
+
// No condition matched. Input types without an explicit entry (text, search,
|
|
77
|
+
// tel, password, hidden, checkbox, radio, file, submit, image, reset, button)
|
|
78
|
+
// have no value constraints per the HTML spec.
|
|
79
|
+
if (!first) {
|
|
80
|
+
return 'Any';
|
|
81
|
+
}
|
|
82
|
+
const enums = [];
|
|
83
|
+
for (const entry of matched) {
|
|
84
|
+
if (!isPlainEnum(entry.type)) {
|
|
85
|
+
return first.type;
|
|
86
|
+
}
|
|
87
|
+
enums.push(entry.type);
|
|
88
|
+
}
|
|
89
|
+
if (enums.length === 1) {
|
|
90
|
+
return first.type;
|
|
91
|
+
}
|
|
92
|
+
return intersectEnums(enums);
|
|
93
|
+
}
|
|
94
|
+
function isPlainEnum(type) {
|
|
95
|
+
if (typeof type === 'string' || Array.isArray(type)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return 'enum' in type && Array.isArray(type.enum) && type.enum.length > 0;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* `invalidValueDefault` is not carried over: a fallback of one set may be a
|
|
102
|
+
* value another set forbids. A case-sensitive spelling is kept when an earlier
|
|
103
|
+
* entry only has another case. Case and surrounding spaces follow the stricter
|
|
104
|
+
* side, because the value has to satisfy every entry.
|
|
105
|
+
*
|
|
106
|
+
* An empty intersection cannot be an enum (`enum` has at least one token).
|
|
107
|
+
* A pattern that matches nothing keeps every value invalid. That pattern is
|
|
108
|
+
* not a keyword list an author is meant to satisfy.
|
|
109
|
+
*
|
|
110
|
+
* @param enums - The enum entries that matched, in spec order. At least two
|
|
111
|
+
* @returns The intersection, or a pattern that matches nothing when the sets are disjoint
|
|
112
|
+
*/
|
|
113
|
+
export function intersectEnums(enums) {
|
|
114
|
+
const caseInsensitive = enums.every(item => item.caseInsensitive !== false);
|
|
115
|
+
const tokens = [];
|
|
116
|
+
for (const item of enums) {
|
|
117
|
+
for (const token of item.enum) {
|
|
118
|
+
if (!enums.every(other => check(token, other).matched)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (tokens.some(existing => sameToken(existing, token, caseInsensitive))) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
tokens.push(token);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const [head, ...rest] = tokens;
|
|
128
|
+
if (head === undefined) {
|
|
129
|
+
return { pattern: '/(?!)/' };
|
|
130
|
+
}
|
|
131
|
+
const result = {
|
|
132
|
+
enum: [head, ...rest],
|
|
133
|
+
...(caseInsensitive ? {} : { caseInsensitive: false }),
|
|
134
|
+
...(enums.every(item => item.disallowToSurroundBySpaces === false)
|
|
135
|
+
? { disallowToSurroundBySpaces: false }
|
|
136
|
+
: {}),
|
|
137
|
+
};
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
function sameToken(a, b, caseInsensitive) {
|
|
141
|
+
return caseInsensitive ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
142
|
+
}
|
|
63
143
|
/**
|
|
64
144
|
* Formats the "did you mean" suffix shared by `no-unknown-attr`'s two typo
|
|
65
145
|
* paths (spec-driven candidate, case-sensitivity mismatch).
|
package/lib/attr-order/index.js
CHANGED
|
@@ -229,7 +229,8 @@ export default createRule({
|
|
|
229
229
|
let cachedValue;
|
|
230
230
|
let cachedCompiled;
|
|
231
231
|
await document.walkOn('Element', node => {
|
|
232
|
-
|
|
232
|
+
// An attribute that a pretender made is not in the source, so it has no place in the order of it
|
|
233
|
+
const attrs = node.getAttributeTokens().filter(attr => !attr.isPretended);
|
|
233
234
|
if (attrs.length <= 1) {
|
|
234
235
|
return;
|
|
235
236
|
}
|
|
@@ -13,8 +13,8 @@ export default createRule({
|
|
|
13
13
|
defaultValue: 'double',
|
|
14
14
|
async verify({ document, report, t }) {
|
|
15
15
|
await document.walkOn('Attr', attr => {
|
|
16
|
-
const message = t('{0}
|
|
17
|
-
if (attr.
|
|
16
|
+
const message = t('{0} must be quoted with {1}', 'Attribute value', attr.rule.value === 'double' ? 'double quotation marks' : 'single quotation marks');
|
|
17
|
+
if (attr.isExpressionValue || attr.isDirective || !attr.equal || attr.equal?.raw === '') {
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
const quote = attr.startQuote?.raw;
|
|
@@ -18,7 +18,7 @@ export default createRule({
|
|
|
18
18
|
meta,
|
|
19
19
|
defaultSeverity: 'warning',
|
|
20
20
|
verify({ document, report, t }) {
|
|
21
|
-
const tables = document.
|
|
21
|
+
const tables = document.deepQuerySelectorAll('table');
|
|
22
22
|
for (const table of tables) {
|
|
23
23
|
const grid = new Grid(table);
|
|
24
24
|
if (grid.hasOverlapped()) {
|
package/lib/create-message.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isList, isKeyword, isEnum, isNumber, isDirective, isPattern } from '@markuplint/types';
|
|
2
|
+
import { noTranslate } from './no-translate.js';
|
|
2
3
|
export function createMessageValueExpected(t, baseTarget, type, matches) {
|
|
3
4
|
let target = baseTarget;
|
|
4
5
|
let listDescriptionPart;
|
|
@@ -53,7 +54,7 @@ export function __createMessageValueExpected(t, baseTarget, expected, matches) {
|
|
|
53
54
|
}
|
|
54
55
|
case 'extra-token': {
|
|
55
56
|
const token = matches.partName ?? 'token';
|
|
56
|
-
reasonPart = t('Found {0}', t('extra {0}', token)) + ' ' + t([matches.raw]);
|
|
57
|
+
reasonPart = t('Found {0}', t('extra {0}', token)) + ' ' + t([noTranslate(matches.raw)]);
|
|
57
58
|
if (matches.extra) {
|
|
58
59
|
const extra = expectValueToWord(t, matches.extra, 'Any');
|
|
59
60
|
unnecessaryPart = t("It doesn't need {0}", extra);
|
|
@@ -212,11 +213,11 @@ function createExpectedObject(type, matches, t) {
|
|
|
212
213
|
}
|
|
213
214
|
else if (isKeyword(type)) {
|
|
214
215
|
if (type[0] === '<') {
|
|
215
|
-
expectedObject.push(t('the CSS Syntax "{0}"', type));
|
|
216
|
+
expectedObject.push(t('the CSS Syntax "{0*}"', type));
|
|
216
217
|
}
|
|
217
218
|
}
|
|
218
219
|
else if (isEnum(type)) {
|
|
219
|
-
expectedObject.push(...type.enum);
|
|
220
|
+
expectedObject.push(...type.enum.map(noTranslate));
|
|
220
221
|
}
|
|
221
222
|
else if (isNumber(type)) {
|
|
222
223
|
expectedObject.push(createExpectedNumber(t, type));
|
|
@@ -240,7 +241,7 @@ function expectValueToWord(t, expect, type) {
|
|
|
240
241
|
return expect.value;
|
|
241
242
|
}
|
|
242
243
|
case 'const': {
|
|
243
|
-
return expect.value
|
|
244
|
+
return noTranslate(expect.value);
|
|
244
245
|
}
|
|
245
246
|
case 'format': {
|
|
246
247
|
return t('the {0} format', expect.value);
|
|
@@ -250,7 +251,7 @@ function expectValueToWord(t, expect, type) {
|
|
|
250
251
|
}
|
|
251
252
|
case 'syntax': {
|
|
252
253
|
if (isKeyword(type) && type[0] === '<') {
|
|
253
|
-
return t('the CSS Syntax "{0}"', expect.value);
|
|
254
|
+
return t('the CSS Syntax "{0*}"', expect.value);
|
|
254
255
|
}
|
|
255
256
|
return t('{0} syntax', expect.value);
|
|
256
257
|
}
|
|
@@ -2,7 +2,7 @@ import { createRule, getComputedRole, ariaSpecs, getSpec } from '@markuplint/ml-
|
|
|
2
2
|
import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
|
|
3
3
|
import { checkingElementSupportsAriaProp } from '../wai-aria/checkings/element-supports-aria-prop.js';
|
|
4
4
|
import { defaultOptions } from '../wai-aria/default-options.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getAriaAttrs } from '../helpers.js';
|
|
6
6
|
import meta from './meta.js';
|
|
7
7
|
/**
|
|
8
8
|
* Split from the former `wai-aria-disallowed-props` rule (#3989): the ARIA in
|
|
@@ -25,7 +25,7 @@ export default createRule({
|
|
|
25
25
|
return;
|
|
26
26
|
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
27
27
|
return;
|
|
28
|
-
const propAttrs = el
|
|
28
|
+
const propAttrs = getAriaAttrs(el);
|
|
29
29
|
if (propAttrs.length === 0)
|
|
30
30
|
return;
|
|
31
31
|
const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { getElementByIdInTree } from '../helpers.js';
|
|
2
3
|
import meta from './meta.js';
|
|
3
4
|
/**
|
|
4
5
|
* Form-associated elements (HTML LS §4.10.2). Listed elements may carry
|
|
@@ -47,8 +48,7 @@ export default createRule({
|
|
|
47
48
|
return;
|
|
48
49
|
if (formAttr.isDynamicValue)
|
|
49
50
|
return;
|
|
50
|
-
const
|
|
51
|
-
const target = [...document.querySelectorAll('[id]')].find(candidate => candidate.getAttribute('id') === targetId);
|
|
51
|
+
const target = getElementByIdInTree(el, formAttr.value, formAttr.isExpressionValue);
|
|
52
52
|
// Existence is checked by `no-refer-to-non-existent-id`. If the ID does not resolve
|
|
53
53
|
// here, leave that diagnostic to the dedicated rule and stay silent.
|
|
54
54
|
if (!target)
|
|
@@ -59,7 +59,7 @@ export default createRule({
|
|
|
59
59
|
line: formAttr.valueNode?.startLine,
|
|
60
60
|
col: formAttr.valueNode?.startCol,
|
|
61
61
|
raw: formAttr.valueNode?.raw,
|
|
62
|
-
message: t('The "{0}" attribute on a form-associated element must reference a "{1}" element', 'form', 'form'),
|
|
62
|
+
message: t('The "{0*}" attribute on a form-associated element must reference a "{1*}" element', 'form', 'form'),
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
});
|
package/lib/helpers.d.ts
CHANGED
|
@@ -1,7 +1,68 @@
|
|
|
1
1
|
import type { FixToken, IRuleFixer, PlainData, TextEdit } from '@markuplint/ml-config';
|
|
2
|
-
import type { Attr, Element, RuleConfigValue
|
|
2
|
+
import type { Attr, ChildNode, Element, RuleConfigValue } from '@markuplint/ml-core';
|
|
3
3
|
import type { Attribute } from '@markuplint/ml-spec';
|
|
4
4
|
import type { WritableDeep } from 'type-fest';
|
|
5
|
+
import { getDOMValue } from '@markuplint/ml-core';
|
|
6
|
+
export { getDOMValue };
|
|
7
|
+
/**
|
|
8
|
+
* The element that an id reference written in an attribute of `el` resolves to: the first element
|
|
9
|
+
* of the tree `el` is in whose id is the reference. The content of a `template` element and a
|
|
10
|
+
* declarative shadow root are trees of their own, so a reference there does not reach an id of the
|
|
11
|
+
* document, nor the other way around.
|
|
12
|
+
*
|
|
13
|
+
* The reference is compared with the character references in the markup resolved, as the id is.
|
|
14
|
+
*
|
|
15
|
+
* @template T - The rule configuration value type
|
|
16
|
+
* @template O - The rule options type
|
|
17
|
+
* @param el - The element that has the attribute
|
|
18
|
+
* @param reference - The value of the attribute, or one token of it, as written in the markup
|
|
19
|
+
* @param isExpression - Whether the value is written as code, which has no character reference in it
|
|
20
|
+
* @returns The element, or `null` when the tree has none, or `el` is in no tree
|
|
21
|
+
* @see https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute
|
|
22
|
+
*/
|
|
23
|
+
export declare function getElementByIdInTree<T extends RuleConfigValue, O extends PlainData>(el: Element<T, O>, reference: string, isExpression?: boolean): Element<T, O> | null;
|
|
24
|
+
/**
|
|
25
|
+
* Whether the element is not an element of the output, so that what is written in it is rendered in
|
|
26
|
+
* place of it:
|
|
27
|
+
*
|
|
28
|
+
* - a fragment (`<>` of JSX), which `childNodes` already flattens;
|
|
29
|
+
* - a `template` element with a directive attribute (Vue's `<template v-if>`, `v-for`, `v-slot`).
|
|
30
|
+
* It is not an HTML `template` element: the template engine renders its children in place of it
|
|
31
|
+
* and drops it. A spread attribute (`{...props}`) is not such a directive: it sets attributes of
|
|
32
|
+
* the element, which stays.
|
|
33
|
+
*
|
|
34
|
+
* `isTransparent` of `@markuplint/ml-core` (`src/ml-dom/helper/tree-scope.ts`), which is not exported,
|
|
35
|
+
* decides the same of an element for the tree boundaries (`getRootNode()`, the shadow host): a kind
|
|
36
|
+
* added here belongs to it too.
|
|
37
|
+
*
|
|
38
|
+
* @template T - The rule configuration value type
|
|
39
|
+
* @template O - The rule options type
|
|
40
|
+
* @param el - An element
|
|
41
|
+
* @returns `true` if the output has no element for it
|
|
42
|
+
*/
|
|
43
|
+
export declare function isRenderedAway<T extends RuleConfigValue, O extends PlainData>(el: Element<T, O>): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Whether the element is a component whose output is not known, so that it may render an element that
|
|
46
|
+
* a rule looks for among the ancestors (`MLElement#isUnresolvedComponent`). What the parser sees as a
|
|
47
|
+
* component but renders its children in place is not one: what {@link isRenderedAway} says (a
|
|
48
|
+
* fragment such as `React.Fragment` or Astro's `Fragment`, a `template` with a directive).
|
|
49
|
+
*
|
|
50
|
+
* @template T - The rule configuration value type
|
|
51
|
+
* @template O - The rule options type
|
|
52
|
+
* @param el - An ancestor of the element
|
|
53
|
+
* @returns `true` if the element may render what is not known
|
|
54
|
+
*/
|
|
55
|
+
export declare function isUnknownOutput<T extends RuleConfigValue, O extends PlainData>(el: Element<T, O>): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* The elements that match the selector in the tree `el` is in (see {@link getElementByIdInTree}).
|
|
58
|
+
*
|
|
59
|
+
* @template T - The rule configuration value type
|
|
60
|
+
* @template O - The rule options type
|
|
61
|
+
* @param el - An element of the tree
|
|
62
|
+
* @param selectors - A selector
|
|
63
|
+
* @returns The elements in the order of the markup, none when `el` is in no tree
|
|
64
|
+
*/
|
|
65
|
+
export declare function querySelectorAllInTree<T extends RuleConfigValue, O extends PlainData>(el: Element<T, O>, selectors: string): readonly Element<T, O>[];
|
|
5
66
|
/** A `null`/`undefined` condition means the attribute applies unconditionally. */
|
|
6
67
|
export declare function attrMatches<T extends RuleConfigValue, O extends PlainData>(node: Element<T, O>, condition: Attribute['condition']): boolean;
|
|
7
68
|
/**
|
|
@@ -60,6 +121,16 @@ export declare function namespaceKeyOf(attr: Pick<Attr<RuleConfigValue, PlainDat
|
|
|
60
121
|
readonly elementType: string;
|
|
61
122
|
};
|
|
62
123
|
}): string;
|
|
124
|
+
/**
|
|
125
|
+
* The `aria-*` attributes that the element has. An attribute that the framework does not output
|
|
126
|
+
* (`aria-label={null}`, `MLAttr#isAbsent`) is not one of them: the DOM does not have it, so what a rule
|
|
127
|
+
* says of an attribute of the element (it is unsupported, deprecated, redundant, or its value is
|
|
128
|
+
* invalid) is not said of it.
|
|
129
|
+
*
|
|
130
|
+
* @param el - The element
|
|
131
|
+
* @returns The `aria-*` attributes that are output
|
|
132
|
+
*/
|
|
133
|
+
export declare function getAriaAttrs<T extends RuleConfigValue, O extends PlainData>(el: Element<T, O>): Attr<T, O>[];
|
|
63
134
|
/**
|
|
64
135
|
* Whether the attribute is an `aria-*` one, told by {@link namespaceKeyOf}.
|
|
65
136
|
*
|
|
@@ -117,7 +188,28 @@ export declare function match(needle: string, pattern: string): boolean;
|
|
|
117
188
|
*/
|
|
118
189
|
export declare const rePCENChar: string;
|
|
119
190
|
export declare function toNormalizedValue(value: string, spec: Attribute): string;
|
|
120
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Whether a child of an element may stand for children that are not known: a block of a preprocessor
|
|
193
|
+
* that has no `blockBehavior` (an output such as `{value}` or an `include`), a `slot`, or a component
|
|
194
|
+
* whose output is not known (`MLElement#isUnresolvedComponent`).
|
|
195
|
+
*
|
|
196
|
+
* It is for a rule that asks which children an element has (the required children, the elements that
|
|
197
|
+
* satisfy a query). It looks at the child alone: a descendant of the child is not a child of the
|
|
198
|
+
* element, so what it renders cannot become the child that the rule asks for, and the attributes of
|
|
199
|
+
* a child do not decide whether the child exists. `MLElement#hasMutableChildren` looks at every
|
|
200
|
+
* descendant, which is the question for a rule that asks what the descendants are or have.
|
|
201
|
+
* `require-datetime` asks about the text, which any descendant may change, and looks at every
|
|
202
|
+
* descendant for an unresolved component itself.
|
|
203
|
+
*
|
|
204
|
+
* @template T - The rule configuration value type
|
|
205
|
+
* @template O - The rule options type
|
|
206
|
+
* @param node - A child node of the element
|
|
207
|
+
* @param components - Whether a component whose output is not known counts. It does not when the
|
|
208
|
+
* rule evaluates the components by the name that they are written with
|
|
209
|
+
* @returns True if the node may stand for children that are not known
|
|
210
|
+
*/
|
|
211
|
+
export declare function mayRenderUnknownChildren<T extends RuleConfigValue, O extends PlainData>(node: ChildNode<T, O>, components?: boolean): boolean;
|
|
212
|
+
export declare function accnameMayBeMutable(el: Element<any, any>): boolean;
|
|
121
213
|
/**
|
|
122
214
|
* Whether the accessible name of `el` may be rendered by content the pretender
|
|
123
215
|
* does not know: an unknown entry (`{ dynamic: true }`) in the `contents` of
|
|
@@ -129,9 +221,9 @@ export declare function accnameMayBeMutable(el: Element<any, any>, document: Doc
|
|
|
129
221
|
* must stay checked (and reported as redundant by `no-redundant-accessible-name`).
|
|
130
222
|
* Call it only once the static name has been computed and found empty.
|
|
131
223
|
*/
|
|
132
|
-
export declare function accnameMayComeFromUnknownContent(el: Element<any, any
|
|
224
|
+
export declare function accnameMayComeFromUnknownContent(el: Element<any, any>): boolean;
|
|
133
225
|
export declare const labelable: string[];
|
|
134
|
-
export declare function getOwnedLabel<V extends RuleConfigValue, O extends PlainData>(el: Element<V, O
|
|
226
|
+
export declare function getOwnedLabel<V extends RuleConfigValue, O extends PlainData>(el: Element<V, O>): Element<V, O> | null;
|
|
135
227
|
/** `null`/`undefined` items are silently dropped on add. */
|
|
136
228
|
export declare class Collection<T> {
|
|
137
229
|
#private;
|
|
@@ -181,4 +273,3 @@ export declare function createIgnoreFeaturesLookup(tagNameCaseSensitive: boolean
|
|
|
181
273
|
/** Whether the attribute of the element is ignored. */
|
|
182
274
|
isAttrIgnored<T extends RuleConfigValue, O extends PlainData>(elementOption: readonly string[] | undefined, elementName: string, attr: Attr<T, O>): boolean;
|
|
183
275
|
};
|
|
184
|
-
export {};
|