@markuplint/ml-spec 3.7.0 → 3.9.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.
@@ -0,0 +1,418 @@
1
+ const specs = require('@markuplint/html-spec');
2
+ const { createSelector } = require('@markuplint/selector');
3
+ const { createJSDOMElement } = require('@markuplint/test-tools');
4
+
5
+ const { getPermittedRoles } = require('../../lib/specs/get-permitted-roles');
6
+
7
+ function c(html, version, selector) {
8
+ const el = createJSDOMElement(html, selector);
9
+ const roles = getPermittedRoles(
10
+ specs,
11
+ el.localName,
12
+ el.namespaceURI,
13
+ version,
14
+ selector => createSelector(selector, specs).match(el) !== false,
15
+ );
16
+ if (Array.isArray(roles)) {
17
+ return roles.map(r => r.name);
18
+ }
19
+ return roles;
20
+ }
21
+
22
+ const coreRoles = [
23
+ 'alert',
24
+ 'alertdialog',
25
+ 'application',
26
+ 'article',
27
+ 'banner',
28
+ 'blockquote',
29
+ 'button',
30
+ 'caption',
31
+ 'cell',
32
+ 'checkbox',
33
+ 'code',
34
+ 'columnheader',
35
+ 'combobox',
36
+ 'complementary',
37
+ 'contentinfo',
38
+ 'definition',
39
+ 'deletion',
40
+ 'dialog',
41
+ 'directory',
42
+ 'document',
43
+ 'emphasis',
44
+ 'feed',
45
+ 'figure',
46
+ 'form',
47
+ 'generic',
48
+ 'grid',
49
+ 'gridcell',
50
+ 'group',
51
+ 'heading',
52
+ 'img',
53
+ 'insertion',
54
+ 'link',
55
+ 'list',
56
+ 'listbox',
57
+ 'listitem',
58
+ 'log',
59
+ 'main',
60
+ 'marquee',
61
+ 'math',
62
+ 'menu',
63
+ 'menubar',
64
+ 'menuitem',
65
+ 'menuitemcheckbox',
66
+ 'menuitemradio',
67
+ 'meter',
68
+ 'navigation',
69
+ 'none',
70
+ 'note',
71
+ 'option',
72
+ 'paragraph',
73
+ 'presentation',
74
+ 'progressbar',
75
+ 'radio',
76
+ 'radiogroup',
77
+ 'region',
78
+ 'row',
79
+ 'rowgroup',
80
+ 'rowheader',
81
+ 'scrollbar',
82
+ 'search',
83
+ 'searchbox',
84
+ 'separator',
85
+ 'slider',
86
+ 'spinbutton',
87
+ 'status',
88
+ 'strong',
89
+ 'subscript',
90
+ 'superscript',
91
+ 'switch',
92
+ 'tab',
93
+ 'table',
94
+ 'tablist',
95
+ 'tabpanel',
96
+ 'term',
97
+ 'textbox',
98
+ 'time',
99
+ 'timer',
100
+ 'toolbar',
101
+ 'tooltip',
102
+ 'tree',
103
+ 'treegrid',
104
+ 'treeitem',
105
+ ];
106
+
107
+ describe('getPermittedRoles', () => {
108
+ test('the a element', () => {
109
+ expect(c('<a></a>', '1.2')).toStrictEqual(coreRoles);
110
+ expect(c('<a href="path/to"></a>', '1.2')).toStrictEqual([
111
+ 'link',
112
+ 'button',
113
+ 'checkbox',
114
+ 'menuitem',
115
+ 'menuitemcheckbox',
116
+ 'menuitemradio',
117
+ 'option',
118
+ 'radio',
119
+ 'switch',
120
+ 'tab',
121
+ 'treeitem',
122
+ ]);
123
+ });
124
+
125
+ test('the area element', () => {
126
+ expect(c('<area></area>', '1.2')).toStrictEqual(['generic', 'button', 'link']);
127
+ expect(c('<area></area>', '1.1')).toStrictEqual([]);
128
+ expect(c('<area href="path/to"></area>', '1.2')).toStrictEqual(['link']);
129
+ });
130
+
131
+ test('the figure element', () => {
132
+ expect(c('<figure></figure>', '1.2')).toStrictEqual(coreRoles);
133
+ expect(c('<figure><figcaption></figcaption></figure>', '1.2')).toStrictEqual(['figure']);
134
+ });
135
+
136
+ test('the img element', () => {
137
+ expect(c('<img>', '1.2')).toStrictEqual(['img']);
138
+ expect(c('<img alt="">', '1.2')).toStrictEqual(['none', 'presentation']);
139
+ expect(c('<img alt="photo: something">', '1.2')).toStrictEqual([
140
+ 'img',
141
+ 'button',
142
+ 'checkbox',
143
+ 'link',
144
+ 'menuitem',
145
+ 'menuitemcheckbox',
146
+ 'menuitemradio',
147
+ 'option',
148
+ 'progressbar',
149
+ 'radio',
150
+ 'scrollbar',
151
+ 'separator',
152
+ 'slider',
153
+ 'switch',
154
+ 'tab',
155
+ 'treeitem',
156
+ ]);
157
+ expect(c('<img alt="photo: something">', '1.1')).toStrictEqual([
158
+ 'img',
159
+ 'button',
160
+ 'checkbox',
161
+ 'link',
162
+ 'menuitem',
163
+ 'menuitemcheckbox',
164
+ 'menuitemradio',
165
+ 'option',
166
+ 'progressbar',
167
+ 'scrollbar',
168
+ 'separator',
169
+ 'slider',
170
+ 'switch',
171
+ 'tab',
172
+ 'treeitem',
173
+ ]);
174
+ });
175
+
176
+ test('the input element', () => {
177
+ expect(c('<input>', '1.2')).toStrictEqual(['textbox', 'combobox', 'searchbox', 'spinbutton']);
178
+ expect(c('<input>', '1.1')).toStrictEqual(['textbox', 'combobox', 'searchbox', 'spinbutton']);
179
+ expect(c('<input type="button">', '1.2')).toStrictEqual([
180
+ 'button',
181
+ 'checkbox',
182
+ 'combobox',
183
+ 'link',
184
+ 'menuitem',
185
+ 'menuitemcheckbox',
186
+ 'menuitemradio',
187
+ 'option',
188
+ 'radio',
189
+ 'switch',
190
+ 'tab',
191
+ ]);
192
+ expect(c('<input type="button">', '1.1')).toStrictEqual([
193
+ 'button',
194
+ 'link',
195
+ 'menuitem',
196
+ 'menuitemcheckbox',
197
+ 'menuitemradio',
198
+ 'option',
199
+ 'radio',
200
+ 'switch',
201
+ 'tab',
202
+ ]);
203
+ expect(c('<input type="checkbox" aria-pressed="true">', '1.2')).toStrictEqual(['checkbox', 'button']);
204
+ });
205
+
206
+ test('the img element', () => {
207
+ const imgPermitted = [
208
+ 'img',
209
+ 'button',
210
+ 'checkbox',
211
+ 'link',
212
+ 'menuitem',
213
+ 'menuitemcheckbox',
214
+ 'menuitemradio',
215
+ 'option',
216
+ 'progressbar',
217
+ 'radio',
218
+ 'scrollbar',
219
+ 'separator',
220
+ 'slider',
221
+ 'switch',
222
+ 'tab',
223
+ 'treeitem',
224
+ ];
225
+ expect(c('<img />', '1.2')).toStrictEqual(['img']);
226
+ expect(c('<img alt />', '1.2')).toStrictEqual(['none', 'presentation']);
227
+ expect(c('<img alt="" />', '1.2')).toStrictEqual(['none', 'presentation']);
228
+ expect(c('<img alt="foo" />', '1.2')).toStrictEqual(imgPermitted);
229
+ expect(c('<img aria-label="foo" />', '1.2')).toStrictEqual(imgPermitted);
230
+ });
231
+
232
+ test('the form element', () => {
233
+ expect(c('<form></form>', '1.2')).toStrictEqual(['none', 'presentation', 'search']);
234
+ expect(c('<form></form>', '1.1')).toStrictEqual(['none', 'presentation', 'search']);
235
+ expect(c('<form aria-label="foo"></form>', '1.2')).toStrictEqual(['form', 'none', 'presentation', 'search']);
236
+ expect(c('<form aria-label="foo"></form>', '1.1')).toStrictEqual(['form', 'none', 'presentation', 'search']);
237
+ });
238
+
239
+ test('the svg element', () => {
240
+ expect(c('<svg></svg>', '1.2')).toStrictEqual([
241
+ 'alert',
242
+ 'alertdialog',
243
+ 'application',
244
+ 'article',
245
+ 'banner',
246
+ 'blockquote',
247
+ 'button',
248
+ 'caption',
249
+ 'cell',
250
+ 'checkbox',
251
+ 'code',
252
+ 'columnheader',
253
+ 'combobox',
254
+ 'complementary',
255
+ 'contentinfo',
256
+ 'definition',
257
+ 'deletion',
258
+ 'dialog',
259
+ 'directory',
260
+ 'document',
261
+ 'emphasis',
262
+ 'feed',
263
+ 'figure',
264
+ 'form',
265
+ 'generic',
266
+ 'grid',
267
+ 'gridcell',
268
+ 'group',
269
+ 'heading',
270
+ 'img',
271
+ 'insertion',
272
+ 'link',
273
+ 'list',
274
+ 'listbox',
275
+ 'listitem',
276
+ 'log',
277
+ 'main',
278
+ 'marquee',
279
+ 'math',
280
+ 'menu',
281
+ 'menubar',
282
+ 'menuitem',
283
+ 'menuitemcheckbox',
284
+ 'menuitemradio',
285
+ 'meter',
286
+ 'navigation',
287
+ 'none',
288
+ 'note',
289
+ 'option',
290
+ 'paragraph',
291
+ 'presentation',
292
+ 'progressbar',
293
+ 'radio',
294
+ 'radiogroup',
295
+ 'region',
296
+ 'row',
297
+ 'rowgroup',
298
+ 'rowheader',
299
+ 'scrollbar',
300
+ 'search',
301
+ 'searchbox',
302
+ 'separator',
303
+ 'slider',
304
+ 'spinbutton',
305
+ 'status',
306
+ 'strong',
307
+ 'subscript',
308
+ 'superscript',
309
+ 'switch',
310
+ 'tab',
311
+ 'table',
312
+ 'tablist',
313
+ 'tabpanel',
314
+ 'term',
315
+ 'textbox',
316
+ 'time',
317
+ 'timer',
318
+ 'toolbar',
319
+ 'tooltip',
320
+ 'tree',
321
+ 'treegrid',
322
+ 'treeitem',
323
+ 'graphics-document',
324
+ 'graphics-object',
325
+ 'graphics-symbol',
326
+ ]);
327
+ });
328
+
329
+ test('the rect element', () => {
330
+ expect(c('<svg><rect></rect></svg>', '1.2')).toStrictEqual([
331
+ 'alert',
332
+ 'alertdialog',
333
+ 'application',
334
+ 'article',
335
+ 'banner',
336
+ 'blockquote',
337
+ 'button',
338
+ 'caption',
339
+ 'cell',
340
+ 'checkbox',
341
+ 'code',
342
+ 'columnheader',
343
+ 'combobox',
344
+ 'complementary',
345
+ 'contentinfo',
346
+ 'definition',
347
+ 'deletion',
348
+ 'dialog',
349
+ 'directory',
350
+ 'document',
351
+ 'emphasis',
352
+ 'feed',
353
+ 'figure',
354
+ 'form',
355
+ 'generic',
356
+ 'grid',
357
+ 'gridcell',
358
+ 'group',
359
+ 'heading',
360
+ 'img',
361
+ 'insertion',
362
+ 'link',
363
+ 'list',
364
+ 'listbox',
365
+ 'listitem',
366
+ 'log',
367
+ 'main',
368
+ 'marquee',
369
+ 'math',
370
+ 'menu',
371
+ 'menubar',
372
+ 'menuitem',
373
+ 'menuitemcheckbox',
374
+ 'menuitemradio',
375
+ 'meter',
376
+ 'navigation',
377
+ 'none',
378
+ 'note',
379
+ 'option',
380
+ 'paragraph',
381
+ 'presentation',
382
+ 'progressbar',
383
+ 'radio',
384
+ 'radiogroup',
385
+ 'region',
386
+ 'row',
387
+ 'rowgroup',
388
+ 'rowheader',
389
+ 'scrollbar',
390
+ 'search',
391
+ 'searchbox',
392
+ 'separator',
393
+ 'slider',
394
+ 'spinbutton',
395
+ 'status',
396
+ 'strong',
397
+ 'subscript',
398
+ 'superscript',
399
+ 'switch',
400
+ 'tab',
401
+ 'table',
402
+ 'tablist',
403
+ 'tabpanel',
404
+ 'term',
405
+ 'textbox',
406
+ 'time',
407
+ 'timer',
408
+ 'toolbar',
409
+ 'tooltip',
410
+ 'tree',
411
+ 'treegrid',
412
+ 'treeitem',
413
+ 'graphics-document',
414
+ 'graphics-object',
415
+ 'graphics-symbol',
416
+ ]);
417
+ });
418
+ });
@@ -0,0 +1,67 @@
1
+ const specs = require('@markuplint/html-spec');
2
+
3
+ const { getRoleSpec } = require('../../lib/specs/get-role-spec');
4
+
5
+ describe('getRoleSpec', () => {
6
+ test('the button role', () => {
7
+ const role = getRoleSpec(specs, 'button', 'http://www.w3.org/1999/xhtml', '1.2');
8
+ const superClassRoles = role.superClassRoles.map(r => r.name);
9
+ expect(role.ownedProperties.map(p => p.name + (p.deprecated ? ':deprecated' : ''))).toStrictEqual([
10
+ 'aria-atomic',
11
+ 'aria-busy',
12
+ 'aria-controls',
13
+ 'aria-current',
14
+ 'aria-describedby',
15
+ 'aria-details',
16
+ 'aria-disabled',
17
+ 'aria-dropeffect',
18
+ 'aria-errormessage:deprecated',
19
+ 'aria-expanded',
20
+ 'aria-flowto',
21
+ 'aria-grabbed',
22
+ 'aria-haspopup',
23
+ 'aria-hidden',
24
+ 'aria-invalid:deprecated',
25
+ 'aria-keyshortcuts',
26
+ 'aria-label',
27
+ 'aria-labelledby',
28
+ 'aria-live',
29
+ 'aria-owns',
30
+ 'aria-pressed',
31
+ 'aria-relevant',
32
+ 'aria-roledescription',
33
+ ]);
34
+ expect(role.isAbstract).toBe(false);
35
+ expect(superClassRoles).toStrictEqual(['command', 'widget', 'roletype']);
36
+ });
37
+
38
+ test('the roletype role', () => {
39
+ const role = getRoleSpec(specs, 'roletype', 'http://www.w3.org/1999/xhtml', '1.2');
40
+ const superClassRoles = role.superClassRoles.map(r => r.name);
41
+ expect(role.ownedProperties.map(p => p.name + (p.deprecated ? ':deprecated' : ''))).toStrictEqual([
42
+ 'aria-atomic',
43
+ 'aria-busy',
44
+ 'aria-controls',
45
+ 'aria-current',
46
+ 'aria-describedby',
47
+ 'aria-details',
48
+ 'aria-disabled:deprecated',
49
+ 'aria-dropeffect',
50
+ 'aria-errormessage:deprecated',
51
+ 'aria-flowto',
52
+ 'aria-grabbed',
53
+ 'aria-haspopup:deprecated',
54
+ 'aria-hidden',
55
+ 'aria-invalid:deprecated',
56
+ 'aria-keyshortcuts',
57
+ 'aria-label',
58
+ 'aria-labelledby',
59
+ 'aria-live',
60
+ 'aria-owns',
61
+ 'aria-relevant',
62
+ 'aria-roledescription',
63
+ ]);
64
+ expect(role.isAbstract).toBe(true);
65
+ expect(superClassRoles).toStrictEqual([]);
66
+ });
67
+ });
@@ -0,0 +1,68 @@
1
+ const { specs } = require('@markuplint/html-spec');
2
+
3
+ const { getSpecByTagName } = require('../../lib/specs/get-spec-by-tag-name');
4
+
5
+ const divSpec = {
6
+ conditional: [
7
+ {
8
+ condition: 'dl > div',
9
+ contents: [
10
+ {
11
+ zeroOrMore: ':model(script-supporting)',
12
+ },
13
+ {
14
+ oneOrMore: 'dt',
15
+ },
16
+ {
17
+ zeroOrMore: ':model(script-supporting)',
18
+ },
19
+ {
20
+ oneOrMore: 'dd',
21
+ },
22
+ {
23
+ zeroOrMore: ':model(script-supporting)',
24
+ },
25
+ ],
26
+ },
27
+ ],
28
+ contents: [
29
+ {
30
+ oneOrMore: ':model(flow)',
31
+ },
32
+ ],
33
+ };
34
+
35
+ test('html:div', () => {
36
+ expect(getSpecByTagName(specs, 'div', null)?.contentModel).toStrictEqual(divSpec);
37
+ expect(getSpecByTagName(specs, 'div', 'unknown-namespace')?.contentModel).toStrictEqual(divSpec);
38
+ expect(getSpecByTagName(specs, 'div', 'http://www.w3.org/1999/xhtml')?.contentModel).toStrictEqual(divSpec);
39
+ });
40
+
41
+ test('svg:svg', () => {
42
+ expect(getSpecByTagName(specs, 'svg', 'http://www.w3.org/2000/svg')?.contentModel).toStrictEqual({
43
+ contents: [
44
+ {
45
+ zeroOrMore: [
46
+ ':model(SVGAnimation)',
47
+ ':model(SVGDescriptive)',
48
+ ':model(SVGPaintServer)',
49
+ ':model(SVGShape)',
50
+ ':model(SVGStructural)',
51
+ 'svg|a',
52
+ 'svg|clipPath',
53
+ 'svg|filter',
54
+ 'svg|foreignObject',
55
+ 'svg|image',
56
+ 'svg|marker',
57
+ 'svg|mask',
58
+ 'svg|script',
59
+ 'svg|style',
60
+ 'svg|switch',
61
+ 'svg|text',
62
+ 'svg|view',
63
+ ],
64
+ },
65
+ ],
66
+ });
67
+ expect(getSpecByTagName(specs, 'svg', null)).toBe(null);
68
+ });
@@ -0,0 +1,34 @@
1
+ const { resolveVersion } = require('../../lib/specs/resolve-version');
2
+
3
+ test('output', () => {
4
+ const aria = {
5
+ implicitRole: 'button',
6
+ permittedRoles: false,
7
+ };
8
+ const resolved = resolveVersion(aria, '1.1');
9
+ expect(resolved.implicitRole).toBe('button');
10
+ });
11
+
12
+ test('output the specified version', () => {
13
+ const aria = {
14
+ implicitRole: 'button',
15
+ permittedRoles: false,
16
+ 1.1: {
17
+ implicitRole: 'link',
18
+ },
19
+ };
20
+ const resolved = resolveVersion(aria, '1.1');
21
+ expect(resolved.implicitRole).toBe('link');
22
+ });
23
+
24
+ test('output the latest undefined version', () => {
25
+ const aria = {
26
+ implicitRole: 'button',
27
+ permittedRoles: false,
28
+ 1.1: {
29
+ implicitRole: 'link',
30
+ },
31
+ };
32
+ const resolved = resolveVersion(aria, '1.3');
33
+ expect(resolved.implicitRole).toBe('button');
34
+ });
@@ -0,0 +1,61 @@
1
+ const htmlSpec = require('@markuplint/html-spec');
2
+
3
+ const { schemaToSpec } = require('../../lib/specs/schema-to-spec');
4
+
5
+ describe('schemaToSpec', () => {
6
+ test('specs', () => {
7
+ const exAttr = {
8
+ ref: 'N/A',
9
+ type: 'Boolean',
10
+ description: 'For the unit test.',
11
+ };
12
+ const exAttr2 = {
13
+ ref: 'N/A',
14
+ type: 'Boolean',
15
+ description: 'For the unit test. Override.',
16
+ };
17
+ const mergedSpec = schemaToSpec([
18
+ htmlSpec,
19
+ {
20
+ specs: [
21
+ {
22
+ name: 'a',
23
+ attributes: { 'extended-attr': exAttr },
24
+ },
25
+ ],
26
+ },
27
+ {
28
+ specs: [
29
+ {
30
+ name: 'a',
31
+ attributes: { 'extended-attr': exAttr2 },
32
+ },
33
+ ],
34
+ },
35
+ ]);
36
+ const aElAttrs = mergedSpec.specs.find(el => el.name === 'a').attributes;
37
+ expect(aElAttrs['extended-attr']).toStrictEqual(exAttr2);
38
+ });
39
+
40
+ test('globalAttrs.extends', () => {
41
+ const keyAttr = {
42
+ type: 'NoEmptyAny',
43
+ description: 'A special attribute for list rendering',
44
+ condition: '[v-for]',
45
+ };
46
+ const mergedSpec = schemaToSpec([
47
+ htmlSpec,
48
+ {
49
+ def: {
50
+ '#globalAttrs': {
51
+ '#extends': {
52
+ key: keyAttr,
53
+ },
54
+ },
55
+ },
56
+ },
57
+ ]);
58
+ const key = mergedSpec.def['#globalAttrs']['#HTMLGlobalAttrs']['key'];
59
+ expect(key).toStrictEqual(keyAttr);
60
+ });
61
+ });
@@ -0,0 +1,37 @@
1
+ const { resolveNamespace } = require('../../lib/utils/resolve-namespace');
2
+
3
+ test('tag', () => {
4
+ expect(resolveNamespace('a')).toStrictEqual({
5
+ localName: 'a',
6
+ localNameWithNS: 'a',
7
+ namespace: 'html',
8
+ namespaceURI: 'http://www.w3.org/1999/xhtml',
9
+ });
10
+ expect(resolveNamespace('svg:a')).toStrictEqual({
11
+ localName: 'a',
12
+ localNameWithNS: 'svg:a',
13
+ namespace: 'svg',
14
+ namespaceURI: 'http://www.w3.org/2000/svg',
15
+ });
16
+ });
17
+
18
+ test('attr', () => {
19
+ expect(resolveNamespace('href', 'http://www.w3.org/1999/xhtml')).toStrictEqual({
20
+ localName: 'href',
21
+ localNameWithNS: 'href',
22
+ namespace: 'html',
23
+ namespaceURI: 'http://www.w3.org/1999/xhtml',
24
+ });
25
+ expect(resolveNamespace('href', 'http://www.w3.org/2000/svg')).toStrictEqual({
26
+ localName: 'href',
27
+ localNameWithNS: 'svg:href',
28
+ namespace: 'svg',
29
+ namespaceURI: 'http://www.w3.org/2000/svg',
30
+ });
31
+ expect(resolveNamespace('xlink:href', 'http://www.w3.org/2000/svg')).toStrictEqual({
32
+ localName: 'href',
33
+ localNameWithNS: 'xlink:href',
34
+ namespace: 'xlink',
35
+ namespaceURI: 'http://www.w3.org/1999/xlink',
36
+ });
37
+ });