@@ -303,167 +303,178 @@ function validateRule({ tags, metadata }) {
303303 issues . push ( `metadata.help can not contain the word '${ prohibitedWord } '.` ) ;
304304 }
305305
306- // issues.push(...findTagIssues(tags));
306+ issues . push ( ...findTagIssues ( tags ) ) ;
307307 return issues ;
308308}
309309
310- // const miscTags = ['ACT', 'experimental', 'review-item', 'deprecated'];
311-
312- // const categories = [
313- // 'aria',
314- // 'color',
315- // 'forms',
316- // 'keyboard',
317- // 'language',
318- // 'name-role-value',
319- // 'parsing',
320- // 'semantics',
321- // 'sensory-and-visual-cues',
322- // 'structure',
323- // 'tables',
324- // 'text-alternatives',
325- // 'time-and-media'
326- // ];
327-
328- // const standardsTags = [
329- // {
330- // // Has to be first, as others rely on the WCAG level getting picked up first
331- // name: 'WCAG',
332- // standardRegex: /^wcag2(1|2)?a{1,3}(-obsolete)?$/,
333- // criterionRegex: /^wcag\d{3,4}$/
334- // },
335- // {
336- // name: 'Section 508',
337- // standardRegex: /^section508$/,
338- // criterionRegex: /^section508\.\d{1,2}\.[a-z]$/,
339- // wcagLevelRegex: /^wcag2aa?$/
340- // },
341- // {
342- // name: 'Trusted Tester',
343- // standardRegex: /^TTv5$/,
344- // criterionRegex: /^TT\d{1,3}\.[a-z]$/,
345- // wcagLevelRegex: /^wcag2aa?$/
346- // },
347- // {
348- // name: 'EN 301 549',
349- // standardRegex: /^EN-301-549$/,
350- // criterionRegex: /^EN-9\.[1-4]\.[1-9]\.\d{1,2}$/,
351- // wcagLevelRegex: /^wcag21?aa?$/
352- // }
353- // ];
354-
355- // function findTagIssues(tags) {
356- // const issues = [];
357- // const catTags = tags.filter(tag => tag.startsWith('cat.'));
358- // const bestPracticeTags = tags.filter(tag => tag === 'best-practice');
359-
360- // // Category
361- // if (catTags.length !== 1) {
362- // issues.push(`Must have exactly one cat. tag, got ${catTags.length}`);
363- // }
364- // if (catTags.length && !categories.includes(catTags[0].slice(4))) {
365- // issues.push(`Invalid category tag: ${catTags[0]}`);
366- // }
367- // if (!startsWith(tags, catTags)) {
368- // issues.push(`Tag ${catTags[0]} must be before ${tags[0]}`);
369- // }
370- // tags = removeTags(tags, catTags);
371-
372- // // Best practice
373- // if (bestPracticeTags.length > 1) {
374- // issues.push(
375- // `Only one best-practice tag is allowed, got ${bestPracticeTags.length}`
376- // );
377- // }
378- // if (!startsWith(tags, bestPracticeTags)) {
379- // issues.push(`Tag ${bestPracticeTags[0]} must be before ${tags[0]}`);
380- // }
381- // tags = removeTags(tags, bestPracticeTags);
382-
383- // const standards = {};
384- // // WCAG, Section 508, Trusted Tester, EN 301 549
385- // for (const {
386- // name,
387- // standardRegex,
388- // criterionRegex,
389- // wcagLevelRegex
390- // } of standardsTags) {
391- // const standardTags = tags.filter(tag => tag.match(standardRegex));
392- // const criterionTags = tags.filter(tag => tag.match(criterionRegex));
393- // if (!standardTags.length && !criterionTags.length) {
394- // continue;
395- // }
396-
397- // standards[name] = {
398- // name,
399- // standardTag: standardTags[0] ?? null,
400- // criterionTags
401- // };
402- // if (bestPracticeTags.length !== 0) {
403- // issues.push(`${name} tags cannot be used along side best-practice tag`);
404- // }
405- // if (standardTags.length === 0) {
406- // issues.push(`Expected one ${name} tag, got 0`);
407- // } else if (standardTags.length > 1) {
408- // issues.push(`Expected one ${name} tag, got: ${standardTags.join(', ')}`);
409- // }
410- // if (criterionTags.length === 0) {
411- // issues.push(`Expected at least one ${name} criterion tag, got 0`);
412- // }
413-
414- // if (wcagLevelRegex) {
415- // const wcagLevel = standards.WCAG.standardTag;
416- // if (!wcagLevel.match(wcagLevelRegex)) {
417- // issues.push(`${name} rules not allowed on ${wcagLevel}`);
418- // }
419- // }
420-
421- // // Must have the same criteria listed
422- // if (name === 'EN 301 549') {
423- // const wcagCriteria = standards.WCAG.criterionTags.map(tag =>
424- // tag.slice(4)
425- // );
426- // const enCriteria = criterionTags.map(tag =>
427- // tag.slice(5).replaceAll('.', '')
428- // );
429- // if (
430- // wcagCriteria.length !== enCriteria.length ||
431- // !startsWith(wcagCriteria, enCriteria)
432- // ) {
433- // issues.push(
434- // `Expect WCAG and EN criteria numbers to match: ${wcagCriteria.join(
435- // ', '
436- // )} vs ${enCriteria.join(', ')} }`
437- // );
438- // }
439- // }
440- // tags = removeTags(tags, [...standardTags, ...criterionTags]);
441- // }
442-
443- // // Other tags
444- // const usedMiscTags = miscTags.filter(tag => tags.includes(tag));
445- // const unknownTags = removeTags(tags, usedMiscTags);
446- // if (unknownTags.length) {
447- // issues.push(`Invalid tags: ${unknownTags.join(', ')}`);
448- // }
449-
450- // // At this point only misc tags are left:
451- // tags = removeTags(tags, unknownTags);
452- // if (!startsWith(tags, usedMiscTags)) {
453- // issues.push(
454- // `Tags [${tags.join(', ')}] should be sorted like [${usedMiscTags.join(
455- // ', '
456- // )}]`
457- // );
458- // }
459-
460- // return issues;
461- // }
462-
463- // function startsWith(arr1, arr2) {
464- // return arr2.every((item, i) => item === arr1[i]);
465- // }
466-
467- // function removeTags(tags, tagsToRemove) {
468- // return tags.filter(tag => !tagsToRemove.includes(tag));
469- // }
310+ const miscTags = [
311+ 'ACT' ,
312+ 'experimental' ,
313+ 'review-item' ,
314+ 'deprecated' ,
315+ 'a11y-engine' ,
316+ 'a11y-engine-experimental'
317+ ] ;
318+
319+ const categories = [
320+ 'aria' ,
321+ 'color' ,
322+ 'forms' ,
323+ 'keyboard' ,
324+ 'language' ,
325+ 'name-role-value' ,
326+ 'parsing' ,
327+ 'semantics' ,
328+ 'sensory-and-visual-cues' ,
329+ 'structure' ,
330+ 'tables' ,
331+ 'text-alternatives' ,
332+ 'time-and-media'
333+ ] ;
334+
335+ const standardsTags = [
336+ {
337+ // Has to be first, as others rely on the WCAG level getting picked up first
338+ name : 'WCAG' ,
339+ standardRegex : / ^ w c a g 2 ( 1 | 2 ) ? a { 1 , 3 } ( - o b s o l e t e ) ? $ / ,
340+ criterionRegex : / ^ w c a g \d { 3 , 4 } $ /
341+ } ,
342+ {
343+ name : 'Section 508' ,
344+ standardRegex : / ^ s e c t i o n 5 0 8 $ / ,
345+ criterionRegex : / ^ s e c t i o n 5 0 8 \. \d { 1 , 2 } \. [ a - z ] $ / ,
346+ wcagLevelRegex : / ^ w c a g 2 a a ? $ /
347+ } ,
348+ {
349+ name : 'Trusted Tester' ,
350+ standardRegex : / ^ T T v 5 $ / ,
351+ criterionRegex : / ^ T T \d { 1 , 3 } \. [ a - z ] $ / ,
352+ wcagLevelRegex : / ^ w c a g 2 a a ? $ /
353+ } ,
354+ {
355+ name : 'EN 301 549' ,
356+ standardRegex : / ^ E N - 3 0 1 - 5 4 9 $ / ,
357+ criterionRegex : / ^ E N - 9 \. [ 1 - 4 ] \. [ 1 - 9 ] \. \d { 1 , 2 } $ / ,
358+ wcagLevelRegex : / ^ w c a g 2 1 ? a a ? $ /
359+ }
360+ ] ;
361+
362+ function findTagIssues ( tags ) {
363+ const issues = [ ] ;
364+ const catTags = tags . filter ( tag => tag . startsWith ( 'cat.' ) ) ;
365+ const bestPracticeTags = tags . filter ( tag => tag === 'best-practice' ) ;
366+
367+ // Category
368+ if ( catTags . length !== 1 ) {
369+ issues . push ( `Must have exactly one cat. tag, got ${ catTags . length } ` ) ;
370+ }
371+ if ( catTags . length && ! categories . includes ( catTags [ 0 ] . slice ( 4 ) ) ) {
372+ issues . push ( `Invalid category tag: ${ catTags [ 0 ] } ` ) ;
373+ }
374+ if ( ! startsWith ( tags , catTags ) ) {
375+ issues . push ( `Tag ${ catTags [ 0 ] } must be before ${ tags [ 0 ] } ` ) ;
376+ }
377+ tags = removeTags ( tags , catTags ) ;
378+
379+ // Best practice
380+ if ( bestPracticeTags . length > 1 ) {
381+ issues . push (
382+ `Only one best-practice tag is allowed, got ${ bestPracticeTags . length } `
383+ ) ;
384+ }
385+ if ( ! startsWith ( tags , bestPracticeTags ) ) {
386+ issues . push ( `Tag ${ bestPracticeTags [ 0 ] } must be before ${ tags [ 0 ] } ` ) ;
387+ }
388+ tags = removeTags ( tags , bestPracticeTags ) ;
389+
390+ const standards = { } ;
391+ // WCAG, Section 508, Trusted Tester, EN 301 549
392+ for ( const {
393+ name,
394+ standardRegex,
395+ criterionRegex,
396+ wcagLevelRegex
397+ } of standardsTags ) {
398+ const standardTags = tags . filter ( tag => tag . match ( standardRegex ) ) ;
399+ const criterionTags = tags . filter ( tag => tag . match ( criterionRegex ) ) ;
400+ if ( ! standardTags . length && ! criterionTags . length ) {
401+ continue ;
402+ }
403+
404+ standards [ name ] = {
405+ name,
406+ standardTag : standardTags [ 0 ] ?? null ,
407+ criterionTags
408+ } ;
409+ if ( bestPracticeTags . length !== 0 ) {
410+ issues . push ( `${ name } tags cannot be used along side best-practice tag` ) ;
411+ }
412+ if ( standardTags . length === 0 ) {
413+ issues . push ( `Expected one ${ name } tag, got 0` ) ;
414+ }
415+ // Commented out this part allowing multiple wcag rules.
416+ // This is because we have multiple WCAG rules for different levels.
417+
418+ // else if (standardTags.length > 1) {
419+ // issues.push(`Expected one ${name} tag, got: ${standardTags.join(', ')}`);
420+ // }
421+ if ( criterionTags . length === 0 ) {
422+ issues . push ( `Expected at least one ${ name } criterion tag, got 0` ) ;
423+ }
424+
425+ if ( wcagLevelRegex ) {
426+ const wcagLevel = standards . WCAG . standardTag ;
427+ if ( ! wcagLevel . match ( wcagLevelRegex ) ) {
428+ issues . push ( `${ name } rules not allowed on ${ wcagLevel } ` ) ;
429+ }
430+ }
431+
432+ // Must have the same criteria listed
433+ if ( name === 'EN 301 549' ) {
434+ const wcagCriteria = standards . WCAG . criterionTags . map ( tag =>
435+ tag . slice ( 4 )
436+ ) ;
437+ const enCriteria = criterionTags . map ( tag =>
438+ tag . slice ( 5 ) . replaceAll ( '.' , '' )
439+ ) ;
440+ if (
441+ wcagCriteria . length !== enCriteria . length ||
442+ ! startsWith ( wcagCriteria , enCriteria )
443+ ) {
444+ issues . push (
445+ `Expect WCAG and EN criteria numbers to match: ${ wcagCriteria . join (
446+ ', '
447+ ) } vs ${ enCriteria . join ( ', ' ) } }`
448+ ) ;
449+ }
450+ }
451+ tags = removeTags ( tags , [ ...standardTags , ...criterionTags ] ) ;
452+ }
453+
454+ // Other tags
455+ const usedMiscTags = miscTags . filter ( tag => tags . includes ( tag ) ) ;
456+ const unknownTags = removeTags ( tags , usedMiscTags ) ;
457+ if ( unknownTags . length ) {
458+ issues . push ( `Invalid tags: ${ unknownTags . join ( ', ' ) } ` ) ;
459+ }
460+
461+ // At this point only misc tags are left:
462+ tags = removeTags ( tags , unknownTags ) ;
463+ if ( ! startsWith ( tags , usedMiscTags ) ) {
464+ issues . push (
465+ `Tags [${ tags . join ( ', ' ) } ] should be sorted like [${ usedMiscTags . join (
466+ ', '
467+ ) } ]`
468+ ) ;
469+ }
470+
471+ return issues ;
472+ }
473+
474+ function startsWith ( arr1 , arr2 ) {
475+ return arr2 . every ( ( item , i ) => item === arr1 [ i ] ) ;
476+ }
477+
478+ function removeTags ( tags , tagsToRemove ) {
479+ return tags . filter ( tag => ! tagsToRemove . includes ( tag ) ) ;
480+ }
0 commit comments