a11y validation - accessible-emoji, aria-proptypes, figcaption

pull/3725/head
Tan Li Hau 6 years ago
parent 3ecd3d84b3
commit 3a85d6761a

@ -66,6 +66,7 @@
"code-red": "0.0.17",
"codecov": "^3.5.0",
"css-tree": "1.0.0-alpha22",
"emoji-regex": "^7.0.3",
"eslint": "^6.3.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-svelte3": "^2.7.3",

@ -106,16 +106,16 @@ export default class Attribute extends Node {
return expression;
}
get_static_value() {
if (this.is_spread || this.dependencies.size > 0) return null;
return this.is_true
? true
: this.chunks[0]
? // method should be called only when `is_static = true`
(this.chunks[0] as Text).data
: '';
if (this.is_spread || this.dependencies.size > 0) return undefined;
if (this.is_true) return true;
if (this.is_static) return this.chunks[0] ? (this.chunks[0] as Text).data: '';
if (this.chunks[0]) {
const expression = (this.chunks[0] as Expression).node;
return evaluate_value(expression);
}
return undefined;
}
should_cache() {
@ -127,3 +127,25 @@ export default class Attribute extends Node {
: true;
}
}
function evaluate_value(node) {
switch (node.type) {
case 'Literal':
return node.value;
case 'UnaryExpression':
switch (node.operator) {
case '~':
return ~evaluate_value(node.argument);
case '!':
return !evaluate_value(node.argument);
case '+':
return +evaluate_value(node.argument);
case '-':
return -evaluate_value(node.argument);
}
break;
case 'TemplateLiteral':
return node.quasis[0].value.cooked;
}
return undefined;
}

@ -15,7 +15,7 @@ import list from '../../utils/list';
import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import validateA11y from '../utils/validate-a11y/Element';
import validate_a11y from '../utils/validate-a11y/Element';
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|svg|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/;
@ -180,7 +180,7 @@ export default class Element extends Node {
}
validate() {
validateA11y(this);
validate_a11y(this);
this.validate_attributes();
this.validate_bindings();

@ -2,6 +2,7 @@ import Node from './shared/Node';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import validate_a11y from '../utils/validate-a11y/Text';
export default class Text extends Node {
type: 'Text';
@ -12,5 +13,7 @@ export default class Text extends Node {
super(component, parent, scope, info);
this.data = info.data;
this.synthetic = info.synthetic || false;
validate_a11y(this);
}
}

@ -1,10 +1,11 @@
import Attribute from '../../nodes/Attribute';
import fuzzymatch from '../../../utils/fuzzymatch';
import { array_to_string } from './utils';
const validators = [
no_auto_focus,
unsupported_aria_element,
unknown_aria_attribute,
invalid_aria_attribute,
no_aria_hidden,
no_misplaced_role,
no_unknown_role,
@ -47,26 +48,422 @@ function unsupported_aria_element(attribute: Attribute, name: string) {
}
}
const aria_attributes = 'activedescendant atomic autocomplete busy checked colindex controls current describedby details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowindex selected setsize sort valuemax valuemin valuenow valuetext'.split(
' '
);
// https://github.com/A11yance/aria-query/blob/master/src/ariaPropsMap.js
const aria_attribute_maps = new Map([
['aria-details', { type: 'idlist' }],
[
'aria-activedescendant',
{
type: 'id',
},
],
[
'aria-atomic',
{
type: 'boolean',
},
],
[
'aria-autocomplete',
{
type: 'token',
values: new Set(['inline', 'list', 'both', 'none']),
},
],
[
'aria-busy',
{
type: 'boolean',
},
],
[
'aria-checked',
{
type: 'tristate',
},
],
[
'aria-colcount',
{
type: 'integer',
},
],
[
'aria-colindex',
{
type: 'integer',
},
],
[
'aria-colspan',
{
type: 'integer',
},
],
[
'aria-controls',
{
type: 'idlist',
},
],
[
'aria-current',
{
type: 'token',
values: new Set([
'page',
'step',
'location',
'date',
'time',
'true',
'false',
]),
},
],
[
'aria-describedby',
{
type: 'idlist',
},
],
[
'aria-disabled',
{
type: 'boolean',
},
],
[
'aria-dropeffect',
{
type: 'tokenlist',
values: new Set(['copy', 'move', 'link', 'execute', 'popup', 'none']),
},
],
[
'aria-errormessage',
{
type: 'string',
},
],
[
'aria-expanded',
{
type: 'boolean',
allowundefined: true,
},
],
[
'aria-flowto',
{
type: 'idlist',
},
],
[
'aria-grabbed',
{
type: 'boolean',
allowundefined: true,
},
],
[
'aria-haspopup',
{
type: 'token',
values: new Set([
'false',
'true',
'menu',
'listbox',
'tree',
'grid',
'dialog',
]),
},
],
[
'aria-hidden',
{
type: 'boolean',
},
],
[
'aria-invalid',
{
type: 'token',
values: new Set(['grammar', 'false', 'spelling', 'true']),
},
],
[
'aria-keyshortcuts',
{
type: 'string',
},
],
[
'aria-label',
{
type: 'string',
},
],
[
'aria-labelledby',
{
type: 'idlist',
},
],
[
'aria-level',
{
type: 'integer',
},
],
[
'aria-live',
{
type: 'token',
values: new Set(['off', 'polite', 'assertive']),
},
],
[
'aria-modal',
{
type: 'boolean',
},
],
[
'aria-multiline',
{
type: 'boolean',
},
],
[
'aria-multiselectable',
{
type: 'boolean',
},
],
[
'aria-orientation',
{
type: 'token',
values: new Set(['vertical', 'horizontal']),
},
],
[
'aria-owns',
{
type: 'idlist',
},
],
[
'aria-placeholder',
{
type: 'string',
},
],
[
'aria-posinset',
{
type: 'integer',
},
],
[
'aria-pressed',
{
type: 'tristate',
},
],
[
'aria-readonly',
{
type: 'boolean',
},
],
[
'aria-relevant',
{
type: 'tokenlist',
values: new Set(['additions', 'removals', 'text', 'all']),
},
],
[
'aria-required',
{
type: 'boolean',
},
],
[
'aria-roledescription',
{
type: 'string',
},
],
[
'aria-rowcount',
{
type: 'integer',
},
],
[
'aria-rowindex',
{
type: 'integer',
},
],
[
'aria-rowspan',
{
type: 'integer',
},
],
[
'aria-selected',
{
type: 'boolean',
allowundefined: true,
},
],
[
'aria-setsize',
{
type: 'integer',
},
],
[
'aria-sort',
{
type: 'token',
values: new Set(['ascending', 'descending', 'none', 'other']),
},
],
[
'aria-valuemax',
{
type: 'number',
},
],
[
'aria-valuemin',
{
type: 'number',
},
],
[
'aria-valuenow',
{
type: 'number',
},
],
[
'aria-valuetext',
{
type: 'string',
},
],
]);
const aria_attributes = [...aria_attribute_maps.keys()];
const aria_attribute_set = new Set(aria_attributes);
function unknown_aria_attribute(attribute: Attribute, name: string) {
function invalid_aria_attribute(attribute: Attribute, name: string) {
if (name.startsWith('aria-')) {
const type = name.slice(5);
if (!aria_attribute_set.has(type)) {
const match = fuzzymatch(type, aria_attributes);
let message = `A11y: Unknown aria attribute 'aria-${type}'`;
if (!aria_attribute_set.has(name)) {
const match = fuzzymatch(name, aria_attributes);
let message = `A11y: Unknown aria attribute '${name}'`;
if (match) message += ` (did you mean '${match}'?)`;
attribute.parent.component.warn(attribute, {
code: `a11y-unknown-aria-attribute`,
message,
});
} else {
const value = attribute.get_static_value();
if (value !== undefined) {
const {
type: permitted_type,
values: permitted_values,
} = aria_attribute_maps.get(name);
if (!validate_attribute(value, permitted_type, permitted_values)) {
attribute.parent.component.warn(attribute, {
code: `a11y-invalid-aria-attribute-value`,
message: validate_attribute_error_message(
name,
permitted_type,
permitted_values
),
});
}
}
}
}
}
function validate_attribute(value, expected_types, permitted_values) {
switch (expected_types) {
case 'boolean':
return (
typeof value === 'boolean' || value === 'true' || value === 'false'
);
case 'string':
case 'id':
return typeof value === 'string';
case 'tristate':
return (
validate_attribute(value, 'boolean', undefined) || value === 'mixed'
);
case 'integer':
case 'number':
return (
typeof value === 'number' ||
(typeof value === 'string' && isNaN(Number(value)) === false)
);
case 'token':
return permitted_values.has(
typeof value === 'string' ? value.toLowerCase() : String(value)
);
case 'idlist':
return (
typeof value === 'string' &&
value
.split(' ')
.every(token => validate_attribute(token, 'id', undefined))
);
case 'tokenlist':
return (
typeof value === 'string' &&
value
.split(' ')
.every(token => permitted_values.has(token.toLowerCase()))
);
default:
return false;
}
}
function validate_attribute_error_message(name, type, permitted_values) {
switch (type) {
case 'tristate':
return `The value for ${name} must be a boolean or the string "mixed".`;
case 'token':
return `The value for ${name} must be a single token from the following: ${array_to_string(
Array.from(permitted_values)
)}.`;
case 'tokenlist':
return `The value for ${name} must be a list of one or more tokens from the following: ${array_to_string(
Array.from(permitted_values)
)}.`;
case 'idlist':
return `The value for ${name} must be a list of strings that represent DOM element IDs (idlist)`;
case 'id':
return `The value for ${name} must be a string that represents a DOM element ID`;
case 'boolean':
case 'string':
case 'integer':
case 'number':
default:
return `The value for ${name} must be a ${type}.`;
}
}
function no_aria_hidden(attribute: Attribute, name: string) {
if (name === 'aria-hidden' && /^h[1-6]$/.test(attribute.parent.name)) {
attribute.parent.component.warn(attribute, {

@ -1,6 +1,9 @@
import Element from '../../nodes/Element';
import Attribute from '../../nodes/Attribute';
import EventHandler from '../../nodes/EventHandler';
import emojiRegex from 'emoji-regex';
import Text from '../../nodes/Text';
import { array_to_string } from './utils';
export default function validateA11y(element: Element) {
const attribute_map = new Map();
@ -19,6 +22,7 @@ export default function validateA11y(element: Element) {
required_content(element);
no_missing_handlers(element, handler_map);
img_redundant_alt(element, attribute_map);
accessible_emoji(element, attribute_map);
}
const a11y_distracting_elements = new Set(['blink', 'marquee']);
@ -138,11 +142,7 @@ function no_missing_attribute(
function should_have_attribute(node, attributes: string[], name = node.name) {
const article = /^[aeiou]/.test(attributes[0]) ? 'an' : 'a';
const sequence =
attributes.length > 1
? attributes.slice(0, -1).join(', ') +
` or ${attributes[attributes.length - 1]}`
: attributes[0];
const sequence = array_to_string(attributes);
node.component.warn(node, {
code: `a11y-missing-attribute`,
@ -214,6 +214,22 @@ function img_redundant_alt(
}
}
function accessible_emoji(element: Element, attribute_map: Map<string, Attribute>) {
const has_emoji = element.children.some(child => contain_text(child, emojiRegex()));
if (has_emoji) {
const is_span = element.name === 'span';
const has_label = attribute_map.has('aria-labelledby') ||attribute_map.has('aria-label');
const role = attribute_map.get('role');
const role_value = role && role.chunks[0].type === 'Text' ? (role.chunks[0] as Text).data : null;
if (!has_label || role_value !== 'img' || !is_span) {
element.component.warn(element, {
code: `a11y-accessible-emoji`,
message: `A11y: Emojis should be wrapped in <span>, have role="img", and have an accessible description with aria-label or aria-labelledby.`,
});
}
}
}
function contain_text(node, regex: RegExp) {
switch (node.type) {
case 'Text':

@ -0,0 +1,17 @@
import Text from '../../nodes/Text';
import emojiRegex from 'emoji-regex';
export default function validateA11y(text: Text) {
if (text.parent.type === 'Fragment') {
accessible_emoji(text);
}
}
function accessible_emoji(text: Text) {
if (emojiRegex().test(text.data)) {
text.component.warn(text, {
code: `a11y-accessible-emoji`,
message: `A11y: Emojis should be wrapped in <span>, have role="img", and have an accessible description with aria-label or aria-labelledby.`,
});
}
}

@ -0,0 +1,5 @@
export function array_to_string(values): string {
return values.length > 1
? `${values.slice(0, -1).join(', ')} or ${values[values.length - 1]}`
: values[0];
}

@ -0,0 +1,8 @@
<!-- success -->
<span role="img" aria-label="Snowman">&#9731;</span>
<span role="img" aria-label="Panda">🐼</span>
<span role="img" aria-labelledby="panda1">🐼</span>
<!-- fail -->
<span>🐼</span>
<i role="img" aria-label="Panda">🐼</i>
🐼

@ -0,0 +1,47 @@
[
{
"code": "a11y-accessible-emoji",
"end": {
"character": 197,
"column": 15,
"line": 6
},
"message": "A11y: Emojis should be wrapped in <span>, have role=\"img\", and have an accessible description with aria-label or aria-labelledby.",
"pos": 182,
"start": {
"character": 182,
"column": 0,
"line": 6
}
},
{
"code": "a11y-accessible-emoji",
"end": {
"character": 237,
"column": 39,
"line": 7
},
"message": "A11y: Emojis should be wrapped in <span>, have role=\"img\", and have an accessible description with aria-label or aria-labelledby.",
"pos": 198,
"start": {
"character": 198,
"column": 0,
"line": 7
}
},
{
"code": "a11y-accessible-emoji",
"end": {
"character": 240,
"column": 2,
"line": 8
},
"message": "A11y: Emojis should be wrapped in <span>, have role=\"img\", and have an accessible description with aria-label or aria-labelledby.",
"pos": 237,
"start": {
"character": 237,
"column": 39,
"line": 7
}
}
]

@ -1,7 +1,7 @@
[
{
"code": "a11y-unknown-aria-attribute",
"message": "A11y: Unknown aria attribute 'aria-labeledby' (did you mean 'labelledby'?)",
"message": "A11y: Unknown aria attribute 'aria-labeledby' (did you mean 'aria-labelledby'?)",
"start": {
"line": 1,
"column": 20,

@ -0,0 +1,175 @@
<!-- valid -->
<div aria-hidden={true} />
<div aria-hidden="true" />
<div aria-hidden={"false"} />
<div aria-hidden={!false} />
<div aria-hidden />
<div aria-hidden={false} />
<div aria-hidden={!true} />
<div aria-hidden={!"yes"} />
<div aria-hidden={foo} />
<div aria-hidden={foo.bar} />
<div aria-hidden={undefined} />
<div aria-label="Close" />
<div aria-label={`Close`} />
<div aria-label={foo} />
<div aria-label={foo.bar} />
<div aria-label={undefined} />
<input aria-invalid={error ? "true" : "false"} />
<input aria-invalid={undefined ? "true" : "false"} />
<div aria-checked={true} />
<div aria-checked="true" />
<div aria-checked={"false"} />
<div aria-checked={!false} />
<div aria-checked />
<div aria-checked={false} />
<div aria-checked={!true} />
<div aria-checked={!"yes"} />
<div aria-checked={foo} />
<div aria-checked={foo.bar} />
<div aria-checked="mixed" />
<div aria-checked={`mixed`} />
<div aria-checked={undefined} />
<div aria-level={123} />
<div aria-level={-123} />
<div aria-level={+123} />
<div aria-level={~123} />
<div aria-level={"123"} />
<div aria-level={`123`} />
<div aria-level="123" />
<div aria-level={foo} />
<div aria-level={foo.bar} />
<div aria-level={undefined} />
<div aria-valuemax={123} />
<div aria-valuemax={-123} />
<div aria-valuemax={+123} />
<div aria-valuemax={~123} />
<div aria-valuemax={"123"} />
<div aria-valuemax={`123`} />
<div aria-valuemax="123" />
<div aria-valuemax={foo} />
<div aria-valuemax={foo.bar} />
<div aria-valuemax={undefined} />
<div aria-sort="ascending" />
<div aria-sort="ASCENDING" />
<div aria-sort={"ascending"} />
<div aria-sort={`ascending`} />
<div aria-sort="descending" />
<div aria-sort={"descending"} />
<div aria-sort={`descending`} />
<div aria-sort="none" />
<div aria-sort={"none"} />
<div aria-sort={`none`} />
<div aria-sort="other" />
<div aria-sort={"other"} />
<div aria-sort={`other`} />
<div aria-sort={foo} />
<div aria-sort={foo.bar} />
<div aria-invalid={true} />
<div aria-invalid="true" />
<div aria-invalid={false} />
<div aria-invalid="false" />
<div aria-invalid="grammar" />
<div aria-invalid="spelling" />
<div aria-invalid={undefined} />
<div aria-relevant="additions" />
<div aria-relevant={"additions"} />
<div aria-relevant={`additions`} />
<div aria-relevant="additions removals" />
<div aria-relevant="additions additions" />
<div aria-relevant={"additions removals"} />
<div aria-relevant={`additions removals`} />
<div aria-relevant="additions removals text" />
<div aria-relevant={"additions removals text"} />
<div aria-relevant={`additions removals text`} />
<div aria-relevant="additions removals text all" />
<div aria-relevant={"additions removals text all"} />
<div aria-relevant={`removals additions text all`} />
<div aria-relevant={foo} />
<div aria-relevant={foo.bar} />
<div aria-relevant={undefined} />
<div aria-activedescendant="ascending" />
<div aria-activedescendant="ASCENDING" />
<div aria-activedescendant={"ascending"} />
<div aria-activedescendant={`ascending`} />
<div aria-activedescendant="descending" />
<div aria-activedescendant={"descending"} />
<div aria-activedescendant={`descending`} />
<div aria-activedescendant="none" />
<div aria-activedescendant={"none"} />
<div aria-activedescendant={`none`} />
<div aria-activedescendant="other" />
<div aria-activedescendant={"other"} />
<div aria-activedescendant={`other`} />
<div aria-activedescendant={foo} />
<div aria-activedescendant={foo.bar} />
<div aria-activedescendant={undefined} />
<div aria-labelledby="additions" />
<div aria-labelledby={"additions"} />
<div aria-labelledby={`additions`} />
<div aria-labelledby="additions removals" />
<div aria-labelledby="additions additions" />
<div aria-labelledby={"additions removals"} />
<div aria-labelledby={`additions removals`} />
<div aria-labelledby="additions removals text" />
<div aria-labelledby={"additions removals text"} />
<div aria-labelledby={`additions removals text`} />
<div aria-labelledby="additions removals text all" />
<div aria-labelledby={"additions removals text all"} />
<div aria-labelledby={`removals additions text all`} />
<div aria-labelledby={foo} />
<div aria-labelledby={foo.bar} />
<div aria-labelledby={undefined} />
<!-- invalid -->
<div aria-activedescendant={null} />
<div aria-hidden="yes" />
<div aria-hidden="no" />
<div aria-hidden={1234} />
<div aria-hidden={`${abc}`} />
<div aria-hidden={null} />
<div aria-label />
<div aria-label={true} />
<div aria-label={false} />
<div aria-label={1234} />
<div aria-label={!true} />
<div aria-label={null} />
<div aria-labelledby={null} />
<div aria-checked="yes" />
<div aria-checked="no" />
<div aria-checked={1234} />
<div aria-checked={`${abc}`} />
<div aria-checked={null} />
<div aria-level="yes" />
<div aria-level="no" />
<div aria-level={`abc`} />
<div aria-level={true} />
<div aria-level />
<div aria-level={"false"} />
<div aria-level={!"false"} />
<div aria-level={null} />
<div aria-valuemax="yes" />
<div aria-valuemax="no" />
<div aria-valuemax={`abc`} />
<div aria-valuemax={true} />
<div aria-valuemax />
<div aria-valuemax={"false"} />
<div aria-valuemax={!"false"} />
<div aria-valuemax={null} />
<div aria-sort="" />
<div aria-sort="descnding" />
<div aria-sort />
<div aria-sort={true} />
<div aria-sort={"false"} />
<div aria-sort="ascending descending" />
<div aria-relevant="" />
<div aria-relevant="foobar" />
<div aria-relevant />
<div aria-relevant={true} />
<div aria-relevant={"false"} />
<div aria-relevant="additions removalss" />
<div aria-relevant="additions removalss " />
<div aria-relevant={null} />
<script>
let foo, abc, error;
</script>

@ -0,0 +1,692 @@
[
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4259,
"column": 33,
"line": 124
},
"message": "The value for aria-activedescendant must be a string that represents a DOM element ID",
"pos": 4231,
"start": {
"character": 4231,
"column": 5,
"line": 124
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4285,
"column": 22,
"line": 125
},
"message": "The value for aria-hidden must be a boolean.",
"pos": 4268,
"start": {
"character": 4268,
"column": 5,
"line": 125
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4310,
"column": 21,
"line": 126
},
"message": "The value for aria-hidden must be a boolean.",
"pos": 4294,
"start": {
"character": 4294,
"column": 5,
"line": 126
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4337,
"column": 23,
"line": 127
},
"message": "The value for aria-hidden must be a boolean.",
"pos": 4319,
"start": {
"character": 4319,
"column": 5,
"line": 127
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4395,
"column": 23,
"line": 129
},
"message": "The value for aria-hidden must be a boolean.",
"pos": 4377,
"start": {
"character": 4377,
"column": 5,
"line": 129
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4414,
"column": 15,
"line": 130
},
"message": "The value for aria-label must be a string.",
"pos": 4404,
"start": {
"character": 4404,
"column": 5,
"line": 130
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4440,
"column": 22,
"line": 131
},
"message": "The value for aria-label must be a string.",
"pos": 4423,
"start": {
"character": 4423,
"column": 5,
"line": 131
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4467,
"column": 23,
"line": 132
},
"message": "The value for aria-label must be a string.",
"pos": 4449,
"start": {
"character": 4449,
"column": 5,
"line": 132
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4493,
"column": 22,
"line": 133
},
"message": "The value for aria-label must be a string.",
"pos": 4476,
"start": {
"character": 4476,
"column": 5,
"line": 133
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4520,
"column": 23,
"line": 134
},
"message": "The value for aria-label must be a string.",
"pos": 4502,
"start": {
"character": 4502,
"column": 5,
"line": 134
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4546,
"column": 22,
"line": 135
},
"message": "The value for aria-label must be a string.",
"pos": 4529,
"start": {
"character": 4529,
"column": 5,
"line": 135
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4577,
"column": 27,
"line": 136
},
"message": "The value for aria-labelledby must be a list of strings that represent DOM element IDs (idlist)",
"pos": 4555,
"start": {
"character": 4555,
"column": 5,
"line": 136
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4604,
"column": 23,
"line": 137
},
"message": "The value for aria-checked must be a boolean or the string \"mixed\".",
"pos": 4586,
"start": {
"character": 4586,
"column": 5,
"line": 137
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4630,
"column": 22,
"line": 138
},
"message": "The value for aria-checked must be a boolean or the string \"mixed\".",
"pos": 4613,
"start": {
"character": 4613,
"column": 5,
"line": 138
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4658,
"column": 24,
"line": 139
},
"message": "The value for aria-checked must be a boolean or the string \"mixed\".",
"pos": 4639,
"start": {
"character": 4639,
"column": 5,
"line": 139
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4718,
"column": 24,
"line": 141
},
"message": "The value for aria-checked must be a boolean or the string \"mixed\".",
"pos": 4699,
"start": {
"character": 4699,
"column": 5,
"line": 141
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4743,
"column": 21,
"line": 142
},
"message": "The value for aria-level must be a integer.",
"pos": 4727,
"start": {
"character": 4727,
"column": 5,
"line": 142
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4767,
"column": 20,
"line": 143
},
"message": "The value for aria-level must be a integer.",
"pos": 4752,
"start": {
"character": 4752,
"column": 5,
"line": 143
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4794,
"column": 23,
"line": 144
},
"message": "The value for aria-level must be a integer.",
"pos": 4776,
"start": {
"character": 4776,
"column": 5,
"line": 144
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4820,
"column": 22,
"line": 145
},
"message": "The value for aria-level must be a integer.",
"pos": 4803,
"start": {
"character": 4803,
"column": 5,
"line": 145
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4839,
"column": 15,
"line": 146
},
"message": "The value for aria-level must be a integer.",
"pos": 4829,
"start": {
"character": 4829,
"column": 5,
"line": 146
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4868,
"column": 25,
"line": 147
},
"message": "The value for aria-level must be a integer.",
"pos": 4848,
"start": {
"character": 4848,
"column": 5,
"line": 147
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4898,
"column": 26,
"line": 148
},
"message": "The value for aria-level must be a integer.",
"pos": 4877,
"start": {
"character": 4877,
"column": 5,
"line": 148
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4924,
"column": 22,
"line": 149
},
"message": "The value for aria-level must be a integer.",
"pos": 4907,
"start": {
"character": 4907,
"column": 5,
"line": 149
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4952,
"column": 24,
"line": 150
},
"message": "The value for aria-valuemax must be a number.",
"pos": 4933,
"start": {
"character": 4933,
"column": 5,
"line": 150
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 4979,
"column": 23,
"line": 151
},
"message": "The value for aria-valuemax must be a number.",
"pos": 4961,
"start": {
"character": 4961,
"column": 5,
"line": 151
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5009,
"column": 26,
"line": 152
},
"message": "The value for aria-valuemax must be a number.",
"pos": 4988,
"start": {
"character": 4988,
"column": 5,
"line": 152
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5038,
"column": 25,
"line": 153
},
"message": "The value for aria-valuemax must be a number.",
"pos": 5018,
"start": {
"character": 5018,
"column": 5,
"line": 153
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5060,
"column": 18,
"line": 154
},
"message": "The value for aria-valuemax must be a number.",
"pos": 5047,
"start": {
"character": 5047,
"column": 5,
"line": 154
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5092,
"column": 28,
"line": 155
},
"message": "The value for aria-valuemax must be a number.",
"pos": 5069,
"start": {
"character": 5069,
"column": 5,
"line": 155
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5125,
"column": 29,
"line": 156
},
"message": "The value for aria-valuemax must be a number.",
"pos": 5101,
"start": {
"character": 5101,
"column": 5,
"line": 156
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5154,
"column": 25,
"line": 157
},
"message": "The value for aria-valuemax must be a number.",
"pos": 5134,
"start": {
"character": 5134,
"column": 5,
"line": 157
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5175,
"column": 17,
"line": 158
},
"message": "The value for aria-sort must be a single token from the following: ascending, descending, none or other.",
"pos": 5163,
"start": {
"character": 5163,
"column": 5,
"line": 158
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5205,
"column": 26,
"line": 159
},
"message": "The value for aria-sort must be a single token from the following: ascending, descending, none or other.",
"pos": 5184,
"start": {
"character": 5184,
"column": 5,
"line": 159
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5223,
"column": 14,
"line": 160
},
"message": "The value for aria-sort must be a single token from the following: ascending, descending, none or other.",
"pos": 5214,
"start": {
"character": 5214,
"column": 5,
"line": 160
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5248,
"column": 21,
"line": 161
},
"message": "The value for aria-sort must be a single token from the following: ascending, descending, none or other.",
"pos": 5232,
"start": {
"character": 5232,
"column": 5,
"line": 161
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5276,
"column": 24,
"line": 162
},
"message": "The value for aria-sort must be a single token from the following: ascending, descending, none or other.",
"pos": 5257,
"start": {
"character": 5257,
"column": 5,
"line": 162
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5317,
"column": 37,
"line": 163
},
"message": "The value for aria-sort must be a single token from the following: ascending, descending, none or other.",
"pos": 5285,
"start": {
"character": 5285,
"column": 5,
"line": 163
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5342,
"column": 21,
"line": 164
},
"message": "The value for aria-relevant must be a list of one or more tokens from the following: additions, removals, text or all.",
"pos": 5326,
"start": {
"character": 5326,
"column": 5,
"line": 164
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5373,
"column": 27,
"line": 165
},
"message": "The value for aria-relevant must be a list of one or more tokens from the following: additions, removals, text or all.",
"pos": 5351,
"start": {
"character": 5351,
"column": 5,
"line": 165
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5395,
"column": 18,
"line": 166
},
"message": "The value for aria-relevant must be a list of one or more tokens from the following: additions, removals, text or all.",
"pos": 5382,
"start": {
"character": 5382,
"column": 5,
"line": 166
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5424,
"column": 25,
"line": 167
},
"message": "The value for aria-relevant must be a list of one or more tokens from the following: additions, removals, text or all.",
"pos": 5404,
"start": {
"character": 5404,
"column": 5,
"line": 167
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5456,
"column": 28,
"line": 168
},
"message": "The value for aria-relevant must be a list of one or more tokens from the following: additions, removals, text or all.",
"pos": 5433,
"start": {
"character": 5433,
"column": 5,
"line": 168
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5500,
"column": 40,
"line": 169
},
"message": "The value for aria-relevant must be a list of one or more tokens from the following: additions, removals, text or all.",
"pos": 5465,
"start": {
"character": 5465,
"column": 5,
"line": 169
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5545,
"column": 41,
"line": 170
},
"message": "The value for aria-relevant must be a list of one or more tokens from the following: additions, removals, text or all.",
"pos": 5509,
"start": {
"character": 5509,
"column": 5,
"line": 170
}
},
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 5574,
"column": 25,
"line": 171
},
"message": "The value for aria-relevant must be a list of one or more tokens from the following: additions, removals, text or all.",
"pos": 5554,
"start": {
"character": 5554,
"column": 5,
"line": 171
}
}
]

@ -3,7 +3,7 @@
</script>
<figure>
<img src='foo.jpg' alt='a picture of a foo'>
<img src='foo.jpg' alt='foo'>
{#if caption}
<figcaption>{caption}</figcaption>
{/if}

@ -1,5 +1,5 @@
<figure>
<img src='foo.jpg' alt='a picture of a foo'>
<img src='foo.jpg' alt='foo'>
<figcaption>
a foo in its natural habitat

@ -1,5 +1,5 @@
<figure>
<img src='foo.jpg' alt='a picture of a foo'>
<img src='foo.jpg' alt='foo'>
<figcaption>
a foo in its natural habitat
@ -9,7 +9,7 @@
</figure>
<figure>
<img src='foo.jpg' alt='a picture of a foo'>
<img src='foo.jpg' alt='foo'>
<div class='markup-for-styling'>
<figcaption>

@ -5,14 +5,14 @@
"start": {
"line": 4,
"column": 1,
"character": 57
"character": 42
},
"end": {
"line": 6,
"column": 14,
"character": 115
"character": 100
},
"pos": 57
"pos": 42
},
{
"code": "a11y-structure",
@ -20,13 +20,13 @@
"start": {
"line": 15,
"column": 2,
"character": 252
"character": 222
},
"end": {
"line": 17,
"column": 15,
"character": 328
"character": 298
},
"pos": 252
"pos": 222
}
]

Loading…
Cancel
Save