@markuplint/ml-spec 3.0.0-dev.25 → 3.0.0-dev.38
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/lib/dom-traverse/is-exposed.js +8 -0
- package/package.json +4 -4
- package/schemas/aria.schema.json +0 -258
- package/schemas/attributes.schema.json +0 -204
- package/schemas/content-models.schema.json +0 -199
- package/schemas/element.schema.json +0 -11
- package/schemas/global-attributes.schema.json +0 -761
- package/src/dom-traverse/accname-computation.spec.ts +0 -69
- package/src/dom-traverse/accname-computation.ts +0 -5
- package/src/dom-traverse/get-attr-specs.ts +0 -8
- package/src/dom-traverse/get-computed-aria-props.spec.ts +0 -368
- package/src/dom-traverse/get-computed-aria-props.ts +0 -130
- package/src/dom-traverse/get-computed-role.spec.ts +0 -134
- package/src/dom-traverse/get-computed-role.ts +0 -181
- package/src/dom-traverse/get-content-model.ts +0 -32
- package/src/dom-traverse/get-explicit-role.ts +0 -112
- package/src/dom-traverse/get-implicit-role.ts +0 -36
- package/src/dom-traverse/get-non-presentational-ancestor.ts +0 -20
- package/src/dom-traverse/get-permitted-roles.ts +0 -7
- package/src/dom-traverse/get-spec.ts +0 -7
- package/src/dom-traverse/has-required-owned-elements.spec.ts +0 -36
- package/src/dom-traverse/has-required-owned-elements.ts +0 -85
- package/src/dom-traverse/is-exposed.spec.ts +0 -50
- package/src/dom-traverse/is-exposed.ts +0 -196
- package/src/dom-traverse/may-be-focusable.ts +0 -21
- package/src/index.ts +0 -24
- package/src/specs/aria-specs.ts +0 -6
- package/src/specs/content-model-category-to-tag-names.ts +0 -15
- package/src/specs/get-aria.ts +0 -85
- package/src/specs/get-attr-specs.spec.ts +0 -47
- package/src/specs/get-attr-specs.ts +0 -116
- package/src/specs/get-implicit-role.spec.ts +0 -81
- package/src/specs/get-implicit-role.ts +0 -17
- package/src/specs/get-permitted-roles.spec.ts +0 -420
- package/src/specs/get-permitted-roles.ts +0 -87
- package/src/specs/get-role-spec.spec.ts +0 -67
- package/src/specs/get-role-spec.ts +0 -72
- package/src/specs/get-selectors-by-content-model-category.ts +0 -10
- package/src/specs/get-spec-by-tag-name.spec.ts +0 -68
- package/src/specs/get-spec-by-tag-name.ts +0 -20
- package/src/specs/is-nothing-content-model.ts +0 -9
- package/src/specs/is-palpable-elements.ts +0 -55
- package/src/specs/is-presentational.ts +0 -6
- package/src/specs/is-void-element.ts +0 -22
- package/src/specs/resolve-version.ts +0 -20
- package/src/specs/schema-to-spec.spec.ts +0 -39
- package/src/specs/schema-to-spec.ts +0 -103
- package/src/types/aria.ts +0 -153
- package/src/types/attributes.ts +0 -1524
- package/src/types/index.ts +0 -255
- package/src/types/permitted-structres.ts +0 -100
- package/src/types/permitted-structures.ts +0 -96
- package/src/utils/get-ns.ts +0 -18
- package/src/utils/merge-array.ts +0 -35
- package/src/utils/resolve-namespace.spec.ts +0 -37
- package/src/utils/resolve-namespace.ts +0 -40
- package/tsconfig.test.json +0 -3
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import type { ARIAVersion } from '../types';
|
|
2
|
-
|
|
3
|
-
import specs from '@markuplint/html-spec';
|
|
4
|
-
import { createSelector } from '@markuplint/selector';
|
|
5
|
-
import { createJSDOMElement } from '@markuplint/test-tools';
|
|
6
|
-
|
|
7
|
-
import { isExposed } from './is-exposed';
|
|
8
|
-
|
|
9
|
-
function _(html: string, selector?: string) {
|
|
10
|
-
return createJSDOMElement(html, selector, function (selector) {
|
|
11
|
-
// JSDOM supports no level 4 selectors yet.
|
|
12
|
-
return !!createSelector(selector, specs).match(this);
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function x(html: string, selector?: string, version: ARIAVersion = '1.2') {
|
|
17
|
-
const el = _(html, selector);
|
|
18
|
-
return isExposed(el, specs, version);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe('isExposed', () => {
|
|
22
|
-
test('Basic', () => {
|
|
23
|
-
expect(x('<div></div>')).toBe(true);
|
|
24
|
-
expect(x('<div aria-hidden="true"></div>')).toBe(false);
|
|
25
|
-
expect(x('<div role="presentation"></div>')).toBe(false);
|
|
26
|
-
expect(x('<div role="presentation" aria-hidden="true"></div>')).toBe(false);
|
|
27
|
-
expect(x('<div role="presentation"><span></span></div>', 'span')).toBe(true);
|
|
28
|
-
expect(x('<div role="presentation" aria-hidden="true"><span></span></div>', 'span')).toBe(false);
|
|
29
|
-
expect(x('<div style="display: none;"><span></span></div>', 'span')).toBe(false);
|
|
30
|
-
expect(x('<div hidden><span></span></div>', 'span')).toBe(false);
|
|
31
|
-
expect(x('<div hidden="until-found"><span></span></div>', 'span')).toBe(false);
|
|
32
|
-
expect(x('<html><body>content</body></html>', 'body')).toBe(true);
|
|
33
|
-
expect(x('<ul><li></li></ul>', 'li')).toBe(true);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test('Children Presentational: True', () => {
|
|
37
|
-
expect(x('<button></button>')).toBe(true);
|
|
38
|
-
expect(x('<button hidden></button>')).toBe(false);
|
|
39
|
-
expect(x('<button><span></span></button>', 'span')).toBe(false);
|
|
40
|
-
expect(x('<button hidden><span></span></button>', 'span')).toBe(false);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('Hidden elements', () => {
|
|
44
|
-
expect(x('<input type="text">')).toBe(true);
|
|
45
|
-
expect(x('<input type="hidden">')).toBe(false);
|
|
46
|
-
expect(x('<meta>')).toBe(false);
|
|
47
|
-
expect(x('<style>a{}</style>')).toBe(false);
|
|
48
|
-
expect(x('<script>"";</script>')).toBe(false);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import type { ARIAVersion, MLMLSpec } from '../types';
|
|
2
|
-
|
|
3
|
-
import { ariaSpecs } from '../specs/aria-specs';
|
|
4
|
-
import { isPalpableElement } from '../specs/is-palpable-elements';
|
|
5
|
-
import { isPresentational } from '../specs/is-presentational';
|
|
6
|
-
|
|
7
|
-
import { getComputedRole } from './get-computed-role';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Detect including/excluding from the Accessibility Tree
|
|
11
|
-
*
|
|
12
|
-
* @see https://www.w3.org/TR/wai-aria-1.2/#accessibility_tree
|
|
13
|
-
*
|
|
14
|
-
* @param specs
|
|
15
|
-
* @param el
|
|
16
|
-
* @param version
|
|
17
|
-
*/
|
|
18
|
-
export function isExposed(el: Element, specs: Readonly<MLMLSpec>, version: ARIAVersion): boolean {
|
|
19
|
-
// According to WAI-ARIA
|
|
20
|
-
if (isExcluding(el, specs, version)) {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// According to HTML and SVG Specs with **the author's interpretation**
|
|
25
|
-
{
|
|
26
|
-
if (!isPalpableElement(el, specs, { extendsSvg: true, extendsExposableElements: true })) {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// According to WAI-ARIA
|
|
32
|
-
{
|
|
33
|
-
const exposable = isIncluding(el, specs, version);
|
|
34
|
-
if (exposable) {
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Default
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Excluding Elements from the Accessibility Tree
|
|
45
|
-
*
|
|
46
|
-
* @see https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion
|
|
47
|
-
*
|
|
48
|
-
* @param specs
|
|
49
|
-
* @param el
|
|
50
|
-
* @param version
|
|
51
|
-
*/
|
|
52
|
-
function isExcluding(el: Element, specs: Readonly<MLMLSpec>, version: ARIAVersion): boolean {
|
|
53
|
-
/**
|
|
54
|
-
* The following elements are not exposed via the accessibility API and
|
|
55
|
-
* user agents MUST NOT include them in the accessibility tree:
|
|
56
|
-
* - Elements, including their descendent elements,
|
|
57
|
-
* that have host language semantics specifying
|
|
58
|
-
* that the element is not displayed, such as CSS display:none,
|
|
59
|
-
* visibility:hidden, or the HTML hidden attribute.
|
|
60
|
-
*/
|
|
61
|
-
{
|
|
62
|
-
let currentEl: Element | null = el;
|
|
63
|
-
while (currentEl) {
|
|
64
|
-
if (hasDisplayNodeOrVisibilityHidden(currentEl) || currentEl.hasAttribute('hidden')) {
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
currentEl = currentEl.parentElement;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* - Elements with none or presentation as the first role in the role attribute.
|
|
73
|
-
* However, their exclusion is conditional. In addition,
|
|
74
|
-
* the element's descendants and text content are generally included.
|
|
75
|
-
* These exceptions and conditions are documented in the presentation (role)
|
|
76
|
-
* section.
|
|
77
|
-
*/
|
|
78
|
-
if (isPresentational((el.getAttribute('role') || '').split(/\s+/)[0])) {
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* If not already excluded from the accessibility tree per the above rules,
|
|
84
|
-
* user agents SHOULD NOT include the following elements
|
|
85
|
-
* in the accessibility tree:
|
|
86
|
-
* - Elements, including their descendants, that have aria-hidden set to true.
|
|
87
|
-
* In other words, aria-hidden="true" on a parent overrides aria-hidden="false"
|
|
88
|
-
* on descendants.
|
|
89
|
-
*/
|
|
90
|
-
{
|
|
91
|
-
let currentEl: Element | null = el;
|
|
92
|
-
while (currentEl) {
|
|
93
|
-
if (currentEl.getAttribute('aria-hidden') === 'true') {
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
currentEl = currentEl.parentElement;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* - Any descendants of elements that have the characteristic
|
|
102
|
-
* "Children Presentational: True" unless the descendant
|
|
103
|
-
* is not allowed to be presentational because it meets one of
|
|
104
|
-
* the conditions for exception described in
|
|
105
|
-
* Presentational Roles Conflict Resolution.
|
|
106
|
-
* However, the text content of any excluded descendants is included.
|
|
107
|
-
*/
|
|
108
|
-
{
|
|
109
|
-
let currentEl = el.parentElement;
|
|
110
|
-
while (currentEl) {
|
|
111
|
-
const { role } = getComputedRole(specs, currentEl, version);
|
|
112
|
-
if (role?.childrenPresentational) {
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
currentEl = currentEl.parentElement;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Including Elements in the Accessibility Tree
|
|
124
|
-
*
|
|
125
|
-
* If not excluded from or marked as hidden in the accessibility tree
|
|
126
|
-
* per the rules above in Excluding Elements in the Accessibility Tree,
|
|
127
|
-
* user agents MUST provide an accessible object in the accessibility tree
|
|
128
|
-
* for DOM elements that meet any of the following criteria:
|
|
129
|
-
*
|
|
130
|
-
* @see https://www.w3.org/TR/wai-aria-1.2/#tree_inclusion
|
|
131
|
-
*
|
|
132
|
-
* @param specs
|
|
133
|
-
* @param el
|
|
134
|
-
* @param version
|
|
135
|
-
*/
|
|
136
|
-
function isIncluding(el: Element, specs: Readonly<MLMLSpec>, version: ARIAVersion): boolean {
|
|
137
|
-
/**
|
|
138
|
-
* > **meet any** of the following criteria:
|
|
139
|
-
*/
|
|
140
|
-
const results: boolean[] = [];
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Elements that are not hidden and may fire an accessibility API event,
|
|
144
|
-
* including:
|
|
145
|
-
* - Elements that are currently focused, even if the element or one of
|
|
146
|
-
* its ancestor elements has its aria-hidden attribute set to true.
|
|
147
|
-
*/
|
|
148
|
-
// 🚫 Can't detect the element is focused.
|
|
149
|
-
// results.push(true);
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* - Elements that are a valid target of an aria-activedescendant attribute.
|
|
153
|
-
*/
|
|
154
|
-
// TODO: Compute aria-activedescendant.
|
|
155
|
-
// results.push(true);
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Elements that have an explicit role or a global WAI-ARIA attribute and
|
|
159
|
-
* do not have aria-hidden set to true.
|
|
160
|
-
* (See Excluding Elements in the Accessibility Tree for
|
|
161
|
-
* additional guidance on aria-hidden.)
|
|
162
|
-
*/
|
|
163
|
-
if (el.getAttribute('aria-hidden') !== 'true') {
|
|
164
|
-
const globalAria = ariaSpecs(specs, version).props.filter(prop => prop.isGlobal);
|
|
165
|
-
const { role } = getComputedRole(specs, el, version);
|
|
166
|
-
// Has an explicit role
|
|
167
|
-
if (role && !role.isImplicit) {
|
|
168
|
-
results.push(true);
|
|
169
|
-
}
|
|
170
|
-
// Has a global WAI-ARIA attribute
|
|
171
|
-
for (const attr of Array.from(el.attributes)) {
|
|
172
|
-
if (globalAria.some(aria => aria.name === attr.localName)) {
|
|
173
|
-
results.push(true);
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Elements that are not hidden and have an ID that is referenced
|
|
181
|
-
* by another element via a WAI-ARIA property.
|
|
182
|
-
*/
|
|
183
|
-
// TODO: Compute refering ID.
|
|
184
|
-
// results.push(true);
|
|
185
|
-
|
|
186
|
-
return results.includes(true);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function hasDisplayNodeOrVisibilityHidden(el: Element) {
|
|
190
|
-
const style = el.getAttribute('style');
|
|
191
|
-
if (!style) {
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
// TODO: Improve accuracy
|
|
195
|
-
return /display\s*:\s*none|visibility\s*:\s*hidden/gi.test(style);
|
|
196
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { MLMLSpec } from '../types';
|
|
2
|
-
|
|
3
|
-
import { getSelectorsByContentModelCategory } from '../specs/get-selectors-by-content-model-category';
|
|
4
|
-
|
|
5
|
-
export function mayBeFocusable(el: Element, specs: Readonly<MLMLSpec>): boolean {
|
|
6
|
-
return [
|
|
7
|
-
/**
|
|
8
|
-
* Interactive content
|
|
9
|
-
*
|
|
10
|
-
* @see https://html.spec.whatwg.org/multipage/dom.html#interactive-content
|
|
11
|
-
*/
|
|
12
|
-
...getSelectorsByContentModelCategory(specs, '#interactive'),
|
|
13
|
-
/**
|
|
14
|
-
* Interaction
|
|
15
|
-
*
|
|
16
|
-
* @see https://html.spec.whatwg.org/multipage/interaction.html
|
|
17
|
-
*/
|
|
18
|
-
'[tabindex]',
|
|
19
|
-
'[contenteditable]:not([contenteditable="false" i])',
|
|
20
|
-
].some(selector => el.matches(selector));
|
|
21
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export * from './dom-traverse/accname-computation';
|
|
2
|
-
export * from './dom-traverse/get-attr-specs';
|
|
3
|
-
export * from './dom-traverse/get-computed-aria-props';
|
|
4
|
-
export * from './dom-traverse/get-computed-role';
|
|
5
|
-
export * from './dom-traverse/get-content-model';
|
|
6
|
-
export * from './dom-traverse/get-implicit-role';
|
|
7
|
-
export * from './dom-traverse/get-permitted-roles';
|
|
8
|
-
export * from './dom-traverse/get-spec';
|
|
9
|
-
export * from './dom-traverse/has-required-owned-elements';
|
|
10
|
-
export * from './dom-traverse/is-exposed';
|
|
11
|
-
export * from './dom-traverse/may-be-focusable';
|
|
12
|
-
export * from './specs/aria-specs';
|
|
13
|
-
export * from './specs/content-model-category-to-tag-names';
|
|
14
|
-
export * from './specs/get-role-spec';
|
|
15
|
-
export * from './specs/get-spec-by-tag-name';
|
|
16
|
-
export * from './specs/is-nothing-content-model';
|
|
17
|
-
export * from './specs/is-palpable-elements';
|
|
18
|
-
export * from './specs/is-void-element';
|
|
19
|
-
export * from './specs/schema-to-spec';
|
|
20
|
-
export * from './types';
|
|
21
|
-
export * from './types/aria';
|
|
22
|
-
export * from './types/attributes';
|
|
23
|
-
export * from './types/permitted-structures';
|
|
24
|
-
export * from './utils/resolve-namespace';
|
package/src/specs/aria-specs.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { MLMLSpec } from '../types';
|
|
2
|
-
import type { Category } from '../types/permitted-structures';
|
|
3
|
-
|
|
4
|
-
const cache = new Map<Category, ReadonlyArray<string>>();
|
|
5
|
-
|
|
6
|
-
export function contentModelCategoryToTagNames(contentModel: Category, def: MLMLSpec['def']): ReadonlyArray<string> {
|
|
7
|
-
const cached = cache.get(contentModel);
|
|
8
|
-
if (cached) {
|
|
9
|
-
return cached;
|
|
10
|
-
}
|
|
11
|
-
const tags: string[] | undefined = def['#contentModels'][contentModel];
|
|
12
|
-
const sortedTag = Object.freeze(tags && Array.isArray(tags) ? tags.sort() : []);
|
|
13
|
-
cache.set(contentModel, sortedTag);
|
|
14
|
-
return sortedTag;
|
|
15
|
-
}
|
package/src/specs/get-aria.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import type { ARIAVersion, Matches, MLMLSpec } from '../types';
|
|
2
|
-
import type { ARIA, PermittedRoles } from '../types/aria';
|
|
3
|
-
|
|
4
|
-
import { getSpecByTagName } from './get-spec-by-tag-name';
|
|
5
|
-
import { resolveVersion } from './resolve-version';
|
|
6
|
-
|
|
7
|
-
const cache = new Map<string, Omit<ARIA, ARIAVersion> | null>();
|
|
8
|
-
|
|
9
|
-
export function getARIA(
|
|
10
|
-
specs: Readonly<MLMLSpec>,
|
|
11
|
-
localName: string,
|
|
12
|
-
namespace: string | null,
|
|
13
|
-
version: ARIAVersion,
|
|
14
|
-
matches: Matches,
|
|
15
|
-
): Omit<ARIA, ARIAVersion | 'conditions'> | null {
|
|
16
|
-
const aria = getVersionResolvedARIA(specs, localName, namespace, version);
|
|
17
|
-
if (!aria) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
const conditions = aria.conditions;
|
|
21
|
-
if (!conditions) {
|
|
22
|
-
return aria;
|
|
23
|
-
}
|
|
24
|
-
const conditionKeys = Object.keys(conditions);
|
|
25
|
-
let { implicitRole, permittedRoles, implicitProperties, properties, namingProhibited } = aria;
|
|
26
|
-
for (const cond of conditionKeys) {
|
|
27
|
-
if (!matches(cond)) {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
const condARIA = conditions[cond];
|
|
31
|
-
implicitRole = condARIA.implicitRole ?? implicitRole;
|
|
32
|
-
permittedRoles = condARIA.permittedRoles ?? permittedRoles;
|
|
33
|
-
implicitProperties = condARIA.implicitProperties ?? implicitProperties;
|
|
34
|
-
properties = condARIA.properties ?? properties;
|
|
35
|
-
namingProhibited = condARIA.namingProhibited ?? namingProhibited;
|
|
36
|
-
}
|
|
37
|
-
return {
|
|
38
|
-
implicitRole,
|
|
39
|
-
permittedRoles,
|
|
40
|
-
implicitProperties,
|
|
41
|
-
properties,
|
|
42
|
-
namingProhibited,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function getVersionResolvedARIA(
|
|
47
|
-
specs: Readonly<MLMLSpec>,
|
|
48
|
-
localName: string,
|
|
49
|
-
namespace: string | null,
|
|
50
|
-
version: ARIAVersion,
|
|
51
|
-
) {
|
|
52
|
-
const key = localName + namespace + version;
|
|
53
|
-
let aria = cache.get(key);
|
|
54
|
-
if (aria !== undefined) {
|
|
55
|
-
return aria;
|
|
56
|
-
}
|
|
57
|
-
const spec = getSpecByTagName(specs.specs, localName, namespace)?.aria;
|
|
58
|
-
if (!spec) {
|
|
59
|
-
cache.set(key, null);
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
aria = resolveVersion(spec, version);
|
|
63
|
-
if (aria.permittedRoles) {
|
|
64
|
-
aria.permittedRoles = optimizePermittedRoles(aria.permittedRoles);
|
|
65
|
-
}
|
|
66
|
-
cache.set(key, aria);
|
|
67
|
-
return aria;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function optimizePermittedRoles(permittedRoles: PermittedRoles) {
|
|
71
|
-
if (!Array.isArray(permittedRoles)) {
|
|
72
|
-
return permittedRoles;
|
|
73
|
-
}
|
|
74
|
-
const unique = new Set(permittedRoles);
|
|
75
|
-
|
|
76
|
-
// https://www.w3.org/TR/wai-aria-1.2/#note-regarding-the-aria-1-1-none-role
|
|
77
|
-
if (unique.has('presentation')) {
|
|
78
|
-
unique.add('none');
|
|
79
|
-
}
|
|
80
|
-
if (unique.has('none')) {
|
|
81
|
-
unique.add('presentation');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return Array.from(unique).sort();
|
|
85
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import htmlSpec from '@markuplint/html-spec';
|
|
2
|
-
|
|
3
|
-
import { getAttrSpecs } from './get-attr-specs';
|
|
4
|
-
|
|
5
|
-
describe('getSpec', () => {
|
|
6
|
-
test('svg:image[x]', () => {
|
|
7
|
-
const specs = getAttrSpecs('image', 'http://www.w3.org/2000/svg', htmlSpec);
|
|
8
|
-
const x = specs?.find(spec => spec.name === 'x');
|
|
9
|
-
expect(x).toStrictEqual({
|
|
10
|
-
defaultValue: '0',
|
|
11
|
-
name: 'x',
|
|
12
|
-
type: ['<svg-length>', '<percentage>'],
|
|
13
|
-
animatable: true,
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test('svg:foreignObject[x]', () => {
|
|
18
|
-
const specs = getAttrSpecs('foreignObject', 'http://www.w3.org/2000/svg', htmlSpec);
|
|
19
|
-
const x = specs?.find(spec => spec.name === 'x');
|
|
20
|
-
expect(x).toStrictEqual({
|
|
21
|
-
defaultValue: '0',
|
|
22
|
-
description:
|
|
23
|
-
'The x coordinate of the foreignObject. Value type: <length>|<percentage> ; Default value: 0; Animatable: yes',
|
|
24
|
-
name: 'x',
|
|
25
|
-
type: ['<svg-length>', '<percentage>'],
|
|
26
|
-
animatable: true,
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('svg:linearGradient[xlink:href]', () => {
|
|
31
|
-
const specs = getAttrSpecs('linearGradient', 'http://www.w3.org/2000/svg', htmlSpec);
|
|
32
|
-
const x = specs?.find(spec => spec.name === 'xlink:href');
|
|
33
|
-
expect(x).toStrictEqual({
|
|
34
|
-
description:
|
|
35
|
-
'Deprecated: This feature is no longer recommended. Though some browsers might still support it, it may have already been removed from the relevant web standards, may be in the process of being dropped, or may only be kept for compatibility purposes. Avoid using it, and update existing code if possible; see the compatibility table at the bottom of this page to guide your decision. Be aware that this feature may cease to work at any time.An <IRI> reference to another <linearGradient> element that will be used as a template. Value type: <IRI> ; Default value: none; Animatable: yes',
|
|
36
|
-
name: 'xlink:href',
|
|
37
|
-
type: 'URL',
|
|
38
|
-
deprecated: true,
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('img[fetchpriority] (experimental spec)', () => {
|
|
43
|
-
const attrs = getAttrSpecs('img', null, htmlSpec);
|
|
44
|
-
const fetchpriority = attrs?.find(attr => attr.name === 'fetchpriority');
|
|
45
|
-
expect(fetchpriority?.experimental).toBe(true);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import type { MLMLSpec, Attribute } from '../types';
|
|
2
|
-
import type { NamespaceURI } from '@markuplint/ml-ast';
|
|
3
|
-
|
|
4
|
-
import { resolveNamespace } from '../utils/resolve-namespace';
|
|
5
|
-
|
|
6
|
-
const cacheMap = new Map<string, Attribute[] | null>();
|
|
7
|
-
const schemaCache = new WeakSet<MLMLSpec>();
|
|
8
|
-
|
|
9
|
-
export function getAttrSpecs(localName: string, namespace: NamespaceURI | null, schema: MLMLSpec) {
|
|
10
|
-
if (!schemaCache.has(schema)) {
|
|
11
|
-
cacheMap.clear();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const { localNameWithNS } = resolveNamespace(localName, namespace || undefined);
|
|
15
|
-
|
|
16
|
-
const cache = cacheMap.get(localNameWithNS);
|
|
17
|
-
|
|
18
|
-
if (cache !== undefined) {
|
|
19
|
-
return cache;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
schemaCache.add(schema);
|
|
23
|
-
|
|
24
|
-
const elSpec = schema.specs.find(spec => spec.name === localNameWithNS);
|
|
25
|
-
if (!elSpec) {
|
|
26
|
-
cacheMap.set(localNameWithNS, null);
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const globalAttrs = schema.def['#globalAttrs'];
|
|
31
|
-
let attrs: Record<string, Partial<Attribute>> = {};
|
|
32
|
-
|
|
33
|
-
for (const catName in elSpec.globalAttrs) {
|
|
34
|
-
// @ts-ignore
|
|
35
|
-
const catAttrs: boolean | string[] = elSpec.globalAttrs[catName];
|
|
36
|
-
if (!catAttrs) {
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (typeof catAttrs === 'boolean') {
|
|
40
|
-
const global = globalAttrs[catName];
|
|
41
|
-
attrs = {
|
|
42
|
-
...attrs,
|
|
43
|
-
...global,
|
|
44
|
-
};
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
if (Array.isArray(catAttrs)) {
|
|
48
|
-
const global = globalAttrs[catName];
|
|
49
|
-
if (!global) {
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
catAttrs.forEach(selectedName => {
|
|
53
|
-
const selected = global[selectedName];
|
|
54
|
-
attrs[selectedName] = {
|
|
55
|
-
...attrs[selectedName],
|
|
56
|
-
...selected,
|
|
57
|
-
};
|
|
58
|
-
});
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
for (const attrName in elSpec.attributes) {
|
|
64
|
-
const attr = elSpec.attributes[attrName];
|
|
65
|
-
if (!attr) {
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const current = attrs[attrName] as Omit<Attribute, 'name'> | undefined;
|
|
70
|
-
|
|
71
|
-
attrs[attrName] = {
|
|
72
|
-
description: '',
|
|
73
|
-
...current,
|
|
74
|
-
...attr,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const attrList: Attribute[] = Object.keys(attrs).map<Attribute>(name => {
|
|
79
|
-
const attr = attrs[name];
|
|
80
|
-
return { name, type: 'Any', ...attr };
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
for (const attr of attrList) {
|
|
84
|
-
if (!attr.type) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
`The type is empty in the ${attr.name} attribute of the ${localName} element,
|
|
87
|
-
'packages',
|
|
88
|
-
'@markuplint',
|
|
89
|
-
'html-spec',
|
|
90
|
-
'src',
|
|
91
|
-
'attributes',
|
|
92
|
-
nameWithNS.replace(':', '_') + '.json',
|
|
93
|
-
)})`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
attrList.sort(nameCompare);
|
|
99
|
-
|
|
100
|
-
cacheMap.set(localNameWithNS, attrList);
|
|
101
|
-
return attrList;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
type HasName = { name: string };
|
|
105
|
-
|
|
106
|
-
export function nameCompare(a: HasName | string, b: HasName | string) {
|
|
107
|
-
const nameA = typeof a === 'string' ? a : a.name.toUpperCase();
|
|
108
|
-
const nameB = typeof b === 'string' ? b : b.name.toUpperCase();
|
|
109
|
-
if (nameA < nameB) {
|
|
110
|
-
return -1;
|
|
111
|
-
}
|
|
112
|
-
if (nameA > nameB) {
|
|
113
|
-
return 1;
|
|
114
|
-
}
|
|
115
|
-
return 0;
|
|
116
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import type { ARIAVersion } from '../types';
|
|
2
|
-
|
|
3
|
-
import specs from '@markuplint/html-spec';
|
|
4
|
-
import { createSelector } from '@markuplint/selector';
|
|
5
|
-
import { createJSDOMElement } from '@markuplint/test-tools';
|
|
6
|
-
|
|
7
|
-
import { getImplicitRole } from './get-implicit-role';
|
|
8
|
-
|
|
9
|
-
function c(html: string, version: ARIAVersion) {
|
|
10
|
-
const el = createJSDOMElement(html);
|
|
11
|
-
return getImplicitRole(
|
|
12
|
-
specs,
|
|
13
|
-
el.localName,
|
|
14
|
-
el.namespaceURI,
|
|
15
|
-
version,
|
|
16
|
-
selector => !!createSelector(selector, specs).match(el),
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe('getImplicitRole', () => {
|
|
21
|
-
test('the a element', () => {
|
|
22
|
-
expect(c('<a></a>', '1.2')).toBe(false);
|
|
23
|
-
expect(c('<a name="foo"></a>', '1.2')).toBe(false);
|
|
24
|
-
expect(c('<a href></a>', '1.2')).toBe('link');
|
|
25
|
-
expect(c('<a href=""></a>', '1.2')).toBe('link');
|
|
26
|
-
expect(c('<a href="path/to"></a>', '1.2')).toBe('link');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('the area element', () => {
|
|
30
|
-
expect(c('<area />', '1.2')).toBe(false);
|
|
31
|
-
expect(c('<area shape="rect" />', '1.2')).toBe(false);
|
|
32
|
-
expect(c('<area href />', '1.2')).toBe('link');
|
|
33
|
-
expect(c('<area href="" />', '1.2')).toBe('link');
|
|
34
|
-
expect(c('<area href="path/to" />', '1.2')).toBe('link');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('the article element', () => {
|
|
38
|
-
expect(c('<article></article>', '1.2')).toBe('article');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('the aside element', () => {
|
|
42
|
-
expect(c('<aside></aside>', '1.2')).toBe('complementary');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('the audio element', () => {
|
|
46
|
-
expect(c('<audio></audio>', '1.2')).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('the h1 element', () => {
|
|
50
|
-
expect(c('<h1></h1>', '1.2')).toBe('heading');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('the header element', () => {
|
|
54
|
-
expect(c('<header></header>', '1.2')).toBe('banner');
|
|
55
|
-
expect(c('<header><article></article></header>', '1.2')).toBe('generic');
|
|
56
|
-
expect(c('<header><article></article></header>', '1.1')).toBe(false);
|
|
57
|
-
expect(c('<header><div></div></header>', '1.2')).toBe('banner');
|
|
58
|
-
expect(c('<header><div role="article"></div></header>', '1.2')).toBe('generic');
|
|
59
|
-
expect(c('<header><div role="article"></div></header>', '1.1')).toBe(false);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('the img element', () => {
|
|
63
|
-
expect(c('<img />', '1.2')).toBe('img');
|
|
64
|
-
expect(c('<img alt />', '1.2')).toBe('presentation');
|
|
65
|
-
expect(c('<img alt="" />', '1.2')).toBe('presentation');
|
|
66
|
-
expect(c('<img alt="foo" />', '1.2')).toBe('img');
|
|
67
|
-
expect(c('<img aria-label="foo" />', '1.2')).toBe('img');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('the form element', () => {
|
|
71
|
-
expect(c('<form></form>', '1.2')).toBe('form');
|
|
72
|
-
expect(c('<form></form>', '1.1')).toBe(false);
|
|
73
|
-
expect(c('<form aria-label="foo"></form>', '1.2')).toBe('form');
|
|
74
|
-
expect(c('<form aria-label="foo"></form>', '1.1')).toBe('form');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('the section element', () => {
|
|
78
|
-
expect(c('<section></section>', '1.2')).toBe(false);
|
|
79
|
-
expect(c('<section aria-label="foo"></section>', '1.2')).toBe('region');
|
|
80
|
-
});
|
|
81
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { ARIAVersion, Matches, MLMLSpec } from '../types';
|
|
2
|
-
|
|
3
|
-
import { getARIA } from './get-aria';
|
|
4
|
-
|
|
5
|
-
export function getImplicitRole(
|
|
6
|
-
specs: Readonly<MLMLSpec>,
|
|
7
|
-
localName: string,
|
|
8
|
-
namespace: string | null,
|
|
9
|
-
version: ARIAVersion,
|
|
10
|
-
matches: Matches,
|
|
11
|
-
) {
|
|
12
|
-
const aria = getARIA(specs, localName, namespace, version, matches);
|
|
13
|
-
if (!aria) {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
return aria.implicitRole;
|
|
17
|
-
}
|