@markuplint/ml-spec 3.0.0-alpha.3 → 3.0.0-alpha.5

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.
@@ -0,0 +1,12 @@
1
+ import type { ARIAVersion, MLMLSpec } from '../types';
2
+ declare type ARIAProps = Record<string, ARIAProp>;
3
+ declare type ARIAProp = {
4
+ name: string;
5
+ value: string | undefined;
6
+ required: boolean;
7
+ deprecated: boolean;
8
+ from: ARIAPropReferenceType;
9
+ };
10
+ declare type ARIAPropReferenceType = 'default' | 'html-attr' | 'aria-attr';
11
+ export declare function getComputedAriaProps(specs: Readonly<MLMLSpec>, el: Element, version: ARIAVersion): ARIAProps;
12
+ export {};
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getComputedAriaProps = void 0;
4
+ const aria_specs_1 = require("../specs/aria-specs");
5
+ const get_computed_role_1 = require("./get-computed-role");
6
+ function getComputedAriaProps(specs, el, version) {
7
+ const ariaSpecs = (0, aria_specs_1.ariaSpecs)(specs, version);
8
+ const { role } = (0, get_computed_role_1.getComputedRole)(specs, el, version);
9
+ if (!role) {
10
+ return {};
11
+ }
12
+ const props = {};
13
+ role.ownedProperties.forEach(ownedProp => {
14
+ var _a;
15
+ const spec = ariaSpecs.props.find(propSpec => propSpec.name === ownedProp.name);
16
+ if (!spec) {
17
+ return;
18
+ }
19
+ if (el.hasAttribute(spec.name)) {
20
+ const attrValue = el.getAttribute(spec.name);
21
+ if (attrValue && isValidAriaValue(spec, role.name, attrValue, spec.enum)) {
22
+ props[ownedProp.name] = {
23
+ name: ownedProp.name,
24
+ value: attrValue,
25
+ deprecated: !!spec.deprecated,
26
+ required: !!ownedProp.required,
27
+ from: 'aria-attr',
28
+ };
29
+ return;
30
+ }
31
+ }
32
+ const equivalentHtmlAttr = (_a = spec.equivalentHtmlAttrs) === null || _a === void 0 ? void 0 : _a[0];
33
+ if (equivalentHtmlAttr && el.hasAttribute(equivalentHtmlAttr.htmlAttrName)) {
34
+ const value = equivalentHtmlAttr.value === null
35
+ ? el.getAttribute(equivalentHtmlAttr.htmlAttrName)
36
+ : equivalentHtmlAttr.value;
37
+ if (value != null) {
38
+ props[ownedProp.name] = {
39
+ name: ownedProp.name,
40
+ value,
41
+ deprecated: !!spec.deprecated,
42
+ required: !!ownedProp.required,
43
+ from: 'html-attr',
44
+ };
45
+ return;
46
+ }
47
+ }
48
+ let defaultValue = spec.defaultValue;
49
+ /**
50
+ * @see https://www.w3.org/TR/html-aria/#el-h1-h6
51
+ */
52
+ if (ownedProp.name === 'aria-level' && /^H[1-6]$/.test(el.nodeName)) {
53
+ defaultValue = el.nodeName.replace('H', '');
54
+ }
55
+ props[ownedProp.name] = {
56
+ name: ownedProp.name,
57
+ value: defaultValue,
58
+ deprecated: !!spec.deprecated,
59
+ required: !!ownedProp.required,
60
+ from: 'default',
61
+ };
62
+ });
63
+ return props;
64
+ }
65
+ exports.getComputedAriaProps = getComputedAriaProps;
66
+ function isValidAriaValue(spec, role, value, enumList) {
67
+ let type = spec.value;
68
+ if (spec.conditionalValue) {
69
+ for (const cond of spec.conditionalValue) {
70
+ if (cond.role.includes(role)) {
71
+ type = cond.value;
72
+ }
73
+ }
74
+ }
75
+ value = (value !== null && value !== void 0 ? value : '').trim();
76
+ switch (type) {
77
+ case 'string': {
78
+ return true;
79
+ }
80
+ case 'ID reference':
81
+ case 'ID reference list':
82
+ case 'URI': {
83
+ return !!value;
84
+ }
85
+ case 'integer':
86
+ case 'number': {
87
+ return !isNaN(parseFloat(value));
88
+ }
89
+ case 'token':
90
+ case 'token list': {
91
+ if (!enumList.length) {
92
+ throw new Error('Need an enum list in token and token list types');
93
+ }
94
+ return enumList.includes(value.toLowerCase());
95
+ }
96
+ case 'tristate': {
97
+ return ['true', 'false', 'mixed'].includes(value.toLowerCase());
98
+ }
99
+ case 'true/false': {
100
+ return ['true', 'false'].includes(value.toLowerCase());
101
+ }
102
+ case 'true/false/undefined': {
103
+ return ['true', 'false', 'undefined'].includes(value.toLowerCase());
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,11 @@
1
+ import type { ARIAVersion, MLMLSpec } from '../types';
2
+ /**
3
+ * Detect including/excluding from the Accessibility Tree
4
+ *
5
+ * @see https://www.w3.org/TR/wai-aria-1.2/#accessibility_tree
6
+ *
7
+ * @param specs
8
+ * @param el
9
+ * @param version
10
+ */
11
+ export declare function isExposed(el: Element, specs: Readonly<MLMLSpec>, version: ARIAVersion): boolean;
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isExposed = void 0;
4
+ const aria_specs_1 = require("../specs/aria-specs");
5
+ const is_presentational_1 = require("../specs/is-presentational");
6
+ const get_computed_role_1 = require("./get-computed-role");
7
+ /**
8
+ * Exposable content models and elements
9
+ *
10
+ * **WARNING**:
11
+ * This implementation is through the author's interpretation.
12
+ * Want a issue request.
13
+ * https://github.com/markuplint/markuplint/issues/new
14
+ *
15
+ * @see exposableModels https://html.spec.whatwg.org/multipage/dom.html#content-models
16
+ * @see exposableElementsThatAreNoBelongingAModel https://html.spec.whatwg.org/multipage/indices.html#elements-3
17
+ */
18
+ const exposableModels = ['#palpable', '#SVGRenderable'];
19
+ const exposableElementsThatAreNoBelongingAModel = [
20
+ 'body',
21
+ 'dd',
22
+ 'dt',
23
+ 'figcaption',
24
+ 'html',
25
+ 'legend',
26
+ 'li',
27
+ 'optgroup',
28
+ 'option',
29
+ 'rp',
30
+ 'rt',
31
+ 'summary',
32
+ 'tbody',
33
+ 'td',
34
+ 'tfoot',
35
+ 'th',
36
+ 'thead',
37
+ 'tr',
38
+ ];
39
+ /**
40
+ * Detect including/excluding from the Accessibility Tree
41
+ *
42
+ * @see https://www.w3.org/TR/wai-aria-1.2/#accessibility_tree
43
+ *
44
+ * @param specs
45
+ * @param el
46
+ * @param version
47
+ */
48
+ function isExposed(el, specs, version) {
49
+ // According to WAI-ARIA
50
+ if (isExcluding(el, specs, version)) {
51
+ return false;
52
+ }
53
+ // According to HTML and SVG Specs with **the author's interpretation**
54
+ {
55
+ const exposableConditions = exposableModels
56
+ .map(model => {
57
+ var _a;
58
+ return ((_a = specs.def['#contentModels'][model]) === null || _a === void 0 ? void 0 : _a.join(',')) || '';
59
+ })
60
+ .concat(exposableElementsThatAreNoBelongingAModel.join(','));
61
+ const exposable = exposableConditions.some(condition => {
62
+ return el.matches(condition);
63
+ });
64
+ if (!exposable) {
65
+ return false;
66
+ }
67
+ }
68
+ // According to WAI-ARIA
69
+ {
70
+ const exposable = isIncluding(el, specs, version);
71
+ if (exposable) {
72
+ return true;
73
+ }
74
+ }
75
+ // Default
76
+ return true;
77
+ }
78
+ exports.isExposed = isExposed;
79
+ /**
80
+ * Excluding Elements from the Accessibility Tree
81
+ *
82
+ * @see https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion
83
+ *
84
+ * @param specs
85
+ * @param el
86
+ * @param version
87
+ */
88
+ function isExcluding(el, specs, version) {
89
+ /**
90
+ * The following elements are not exposed via the accessibility API and
91
+ * user agents MUST NOT include them in the accessibility tree:
92
+ * - Elements, including their descendent elements,
93
+ * that have host language semantics specifying
94
+ * that the element is not displayed, such as CSS display:none,
95
+ * visibility:hidden, or the HTML hidden attribute.
96
+ */
97
+ {
98
+ let currentEl = el;
99
+ while (currentEl) {
100
+ if (hasDisplayNodeOrVisibilityHidden(currentEl) || currentEl.hasAttribute('hidden')) {
101
+ return true;
102
+ }
103
+ currentEl = currentEl.parentElement;
104
+ }
105
+ }
106
+ /**
107
+ * - Elements with none or presentation as the first role in the role attribute.
108
+ * However, their exclusion is conditional. In addition,
109
+ * the element's descendants and text content are generally included.
110
+ * These exceptions and conditions are documented in the presentation (role)
111
+ * section.
112
+ */
113
+ if ((0, is_presentational_1.isPresentational)((el.getAttribute('role') || '').split(/\s+/)[0])) {
114
+ return true;
115
+ }
116
+ /**
117
+ * If not already excluded from the accessibility tree per the above rules,
118
+ * user agents SHOULD NOT include the following elements
119
+ * in the accessibility tree:
120
+ * - Elements, including their descendants, that have aria-hidden set to true.
121
+ * In other words, aria-hidden="true" on a parent overrides aria-hidden="false"
122
+ * on descendants.
123
+ */
124
+ {
125
+ let currentEl = el;
126
+ while (currentEl) {
127
+ if (currentEl.getAttribute('aria-hidden') === 'true') {
128
+ return true;
129
+ }
130
+ currentEl = currentEl.parentElement;
131
+ }
132
+ }
133
+ /**
134
+ * - Any descendants of elements that have the characteristic
135
+ * "Children Presentational: True" unless the descendant
136
+ * is not allowed to be presentational because it meets one of
137
+ * the conditions for exception described in
138
+ * Presentational Roles Conflict Resolution.
139
+ * However, the text content of any excluded descendants is included.
140
+ */
141
+ {
142
+ let currentEl = el.parentElement;
143
+ while (currentEl) {
144
+ const { role } = (0, get_computed_role_1.getComputedRole)(specs, currentEl, version);
145
+ if (role === null || role === void 0 ? void 0 : role.childrenPresentational) {
146
+ return true;
147
+ }
148
+ currentEl = currentEl.parentElement;
149
+ }
150
+ }
151
+ return false;
152
+ }
153
+ /**
154
+ * Including Elements in the Accessibility Tree
155
+ *
156
+ * If not excluded from or marked as hidden in the accessibility tree
157
+ * per the rules above in Excluding Elements in the Accessibility Tree,
158
+ * user agents MUST provide an accessible object in the accessibility tree
159
+ * for DOM elements that meet any of the following criteria:
160
+ *
161
+ * @see https://www.w3.org/TR/wai-aria-1.2/#tree_inclusion
162
+ *
163
+ * @param specs
164
+ * @param el
165
+ * @param version
166
+ */
167
+ function isIncluding(el, specs, version) {
168
+ /**
169
+ * > **meet any** of the following criteria:
170
+ */
171
+ const results = [];
172
+ /**
173
+ * Elements that are not hidden and may fire an accessibility API event,
174
+ * including:
175
+ * - Elements that are currently focused, even if the element or one of
176
+ * its ancestor elements has its aria-hidden attribute set to true.
177
+ */
178
+ // 🚫 Can't detect the element is focused.
179
+ // results.push(true);
180
+ /**
181
+ * - Elements that are a valid target of an aria-activedescendant attribute.
182
+ */
183
+ // TODO: Compute aria-activedescendant.
184
+ // results.push(true);
185
+ /**
186
+ * Elements that have an explicit role or a global WAI-ARIA attribute and
187
+ * do not have aria-hidden set to true.
188
+ * (See Excluding Elements in the Accessibility Tree for
189
+ * additional guidance on aria-hidden.)
190
+ */
191
+ if (el.getAttribute('aria-hidden') !== 'true') {
192
+ const globalAria = (0, aria_specs_1.ariaSpecs)(specs, version).props.filter(prop => prop.isGlobal);
193
+ const { role } = (0, get_computed_role_1.getComputedRole)(specs, el, version);
194
+ // Has an explicit role
195
+ if (role && !role.isImplicit) {
196
+ results.push(true);
197
+ }
198
+ // Has a global WAI-ARIA attribute
199
+ for (const attr of Array.from(el.attributes)) {
200
+ if (globalAria.some(aria => aria.name === attr.localName)) {
201
+ results.push(true);
202
+ break;
203
+ }
204
+ }
205
+ }
206
+ /**
207
+ * Elements that are not hidden and have an ID that is referenced
208
+ * by another element via a WAI-ARIA property.
209
+ */
210
+ // TODO: Compute refering ID.
211
+ // results.push(true);
212
+ return results.includes(true);
213
+ }
214
+ function hasDisplayNodeOrVisibilityHidden(el) {
215
+ const style = el.getAttribute('style');
216
+ if (!style) {
217
+ return false;
218
+ }
219
+ // TODO: Improve accuracy
220
+ return /display\s*:\s*none|visibility\s*:\s*hidden/gi.test(style);
221
+ }
package/lib/index.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  export * from './dom-traverse/accname-computation';
2
2
  export * from './dom-traverse/get-attr-specs';
3
+ export * from './dom-traverse/get-computed-aria-props';
3
4
  export * from './dom-traverse/get-computed-role';
4
5
  export * from './dom-traverse/get-content-model';
5
6
  export * from './dom-traverse/get-implicit-role';
6
7
  export * from './dom-traverse/get-permitted-roles';
7
8
  export * from './dom-traverse/get-spec';
8
9
  export * from './dom-traverse/has-required-owned-elements';
10
+ export * from './dom-traverse/is-exposed';
9
11
  export * from './dom-traverse/may-be-focusable';
10
12
  export * from './specs/aria-specs';
11
13
  export * from './specs/content-model-category-to-tag-names';
package/lib/index.js CHANGED
@@ -3,12 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  tslib_1.__exportStar(require("./dom-traverse/accname-computation"), exports);
5
5
  tslib_1.__exportStar(require("./dom-traverse/get-attr-specs"), exports);
6
+ tslib_1.__exportStar(require("./dom-traverse/get-computed-aria-props"), exports);
6
7
  tslib_1.__exportStar(require("./dom-traverse/get-computed-role"), exports);
7
8
  tslib_1.__exportStar(require("./dom-traverse/get-content-model"), exports);
8
9
  tslib_1.__exportStar(require("./dom-traverse/get-implicit-role"), exports);
9
10
  tslib_1.__exportStar(require("./dom-traverse/get-permitted-roles"), exports);
10
11
  tslib_1.__exportStar(require("./dom-traverse/get-spec"), exports);
11
12
  tslib_1.__exportStar(require("./dom-traverse/has-required-owned-elements"), exports);
13
+ tslib_1.__exportStar(require("./dom-traverse/is-exposed"), exports);
12
14
  tslib_1.__exportStar(require("./dom-traverse/may-be-focusable"), exports);
13
15
  tslib_1.__exportStar(require("./specs/aria-specs"), exports);
14
16
  tslib_1.__exportStar(require("./specs/content-model-category-to-tag-names"), exports);
@@ -6,7 +6,7 @@
6
6
  export declare type PermittedContentPattern = PermittedContentRequire | PermittedContentOptional | PermittedContentOneOrMore | PermittedContentZeroOrMore | PermittedContentChoice | PermittedContentTransparent;
7
7
  export declare type Model = ContentType | ContentType[];
8
8
  export declare type ContentType = string | Category;
9
- export declare type Category = '#text' | '#phrasing' | '#flow' | '#interactive' | '#heading' | '#sectioning' | '#metadata' | '#embedded' | '#palpable' | '#script-supporting' | '#SVGAnimation' | '#SVGBasicShapes' | '#SVGContainer' | '#SVGDescriptive' | '#SVGFilterPrimitive' | '#SVGFont' | '#SVGGradient' | '#SVGGraphics' | '#SVGGraphicsReferencing' | '#SVGLightSource' | '#SVGNeverRendered' | '#SVGNone' | '#SVGPaintServer' | '#SVGRenderable' | '#SVGShape' | '#SVGStructural' | '#SVGStructurallyExternal' | '#SVGTextContent' | '#SVGTextContentChild' | '#SVGOtherXMLNamespace';
9
+ export declare type Category = '#text' | '#phrasing' | '#flow' | '#interactive' | '#heading' | '#sectioning' | '#metadata' | '#embedded' | '#palpable' | '#script-supporting' | '#SVGAnimation' | '#SVGBasicShapes' | '#SVGContainer' | '#SVGDescriptive' | '#SVGFilterPrimitive' | '#SVGFont' | '#SVGGradient' | '#SVGGraphics' | '#SVGGraphicsReferencing' | '#SVGLightSource' | '#SVGNeverRendered' | '#SVGNone' | '#SVGPaintServer' | '#SVGRenderable' | '#SVGShape' | '#SVGStructural' | '#SVGStructurallyExternal' | '#SVGTextContent' | '#SVGTextContentChild';
10
10
  export interface ContentModelsSchema {
11
11
  __contentModel?: ContentModel;
12
12
  }
@@ -6,7 +6,7 @@
6
6
  export declare type PermittedContentPattern = PermittedContentRequire | PermittedContentOptional | PermittedContentOneOrMore | PermittedContentZeroOrMore | PermittedContentChoice | PermittedContentTransparent;
7
7
  export declare type Model = ContentType | ContentType[];
8
8
  export declare type ContentType = string | Category;
9
- export declare type Category = '#text' | '#phrasing' | '#flow' | '#interactive' | '#heading' | '#sectioning' | '#metadata' | '#embedded' | '#palpable' | '#script-supporting' | '#SVGAnimation' | '#SVGBasicShapes' | '#SVGContainer' | '#SVGDescriptive' | '#SVGFilterPrimitive' | '#SVGFont' | '#SVGGradient' | '#SVGGraphics' | '#SVGGraphicsReferencing' | '#SVGLightSource' | '#SVGNeverRendered' | '#SVGNone' | '#SVGPaintServer' | '#SVGRenderable' | '#SVGShape' | '#SVGStructural' | '#SVGStructurallyExternal' | '#SVGTextContent' | '#SVGTextContentChild' | '#SVGOtherXMLNamespace';
9
+ export declare type Category = '#text' | '#phrasing' | '#flow' | '#interactive' | '#heading' | '#sectioning' | '#metadata' | '#embedded' | '#palpable' | '#script-supporting' | '#SVGAnimation' | '#SVGBasicShapes' | '#SVGContainer' | '#SVGDescriptive' | '#SVGFilterPrimitive' | '#SVGFont' | '#SVGGradient' | '#SVGGraphics' | '#SVGGraphicsReferencing' | '#SVGLightSource' | '#SVGNeverRendered' | '#SVGNone' | '#SVGPaintServer' | '#SVGRenderable' | '#SVGShape' | '#SVGStructural' | '#SVGStructurallyExternal' | '#SVGTextContent' | '#SVGTextContentChild';
10
10
  export interface ContentModelsSchema {
11
11
  __contentModel?: ContentModel;
12
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/ml-spec",
3
- "version": "3.0.0-alpha.3",
3
+ "version": "3.0.0-alpha.5",
4
4
  "description": "Types and schema that specs of the Markup languages for markuplint",
5
5
  "repository": "git@github.com:markuplint/markuplint.git",
6
6
  "author": "Yusuke Hirao <yusukehirao@me.com>",
@@ -22,14 +22,14 @@
22
22
  "schema": "run-s schema:*"
23
23
  },
24
24
  "dependencies": {
25
- "@markuplint/ml-ast": "3.0.0-alpha.3",
25
+ "@markuplint/ml-ast": "3.0.0-alpha.5",
26
26
  "dom-accessibility-api": "^0.5.14",
27
27
  "tslib": "^2.4.0"
28
28
  },
29
29
  "devDependencies": {
30
- "@markuplint/test-tools": "3.0.0-alpha.3",
31
- "@markuplint/types": "3.0.0-alpha.3",
30
+ "@markuplint/test-tools": "3.0.0-alpha.5",
31
+ "@markuplint/types": "3.0.0-alpha.5",
32
32
  "json-schema-to-typescript": "^11.0.2"
33
33
  },
34
- "gitHead": "884b1a6a7385e86cca7796b0b927a886ee90140e"
34
+ "gitHead": "d2ee395d917cb69b91dedb0262b1c12ddc81fb58"
35
35
  }
@@ -80,8 +80,7 @@
80
80
  "#SVGStructural",
81
81
  "#SVGStructurallyExternal",
82
82
  "#SVGTextContent",
83
- "#SVGTextContentChild",
84
- "#SVGOtherXMLNamespace"
83
+ "#SVGTextContentChild"
85
84
  ]
86
85
  },
87
86
  "Model": {
@@ -7,7 +7,7 @@ test('Get accessible name', () => {
7
7
  expect(getAccname(c('<div>text</div>'))).toBe('');
8
8
  expect(getAccname(c('<div aria-label="label">text</div>'))).toBe('label');
9
9
  expect(getAccname(c('<span aria-label="label">text</span>'))).toBe('label');
10
- expect(getAccname(c('<img alt="alterative-text" />'))).toBe('alterative-text');
10
+ expect(getAccname(c('<img alt="alternative-text" />'))).toBe('alternative-text');
11
11
  expect(getAccname(c('<img title="title" />'))).toBe('title');
12
12
  expect(getAccname(c('<div><label for="a">label</label><input id="a" /></div>').children[1])).toBe('label');
13
13
  });