Skip to content

Commit 51c2e05

Browse files
committed
a11y checks (#374)
1 parent 66c382a commit 51c2e05

File tree

10 files changed

+105
-26
lines changed

10 files changed

+105
-26
lines changed

src/interfaces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export interface CompileOptions {
5454
cascade?: boolean;
5555
hydratable?: boolean;
5656
legacy?: boolean;
57-
customElement: CustomElementOptions | true;
57+
customElement?: CustomElementOptions | true;
5858

5959
onerror?: (error: Error) => void;
6060
onwarn?: (warning: Warning) => void;

src/validate/html/index.ts

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,24 @@
1-
import * as namespaces from '../../utils/namespaces';
21
import validateElement from './validateElement';
32
import validateWindow from './validateWindow';
43
import fuzzymatch from '../utils/fuzzymatch'
54
import flattenReference from '../../utils/flattenReference';
65
import { Validator } from '../index';
76
import { Node } from '../../interfaces';
87

9-
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|switch|symbol|text|textPath|title|tref|tspan|unknown|use|view|vkern)$/;
10-
118
const meta = new Map([[':Window', validateWindow]]);
129

1310
export default function validateHtml(validator: Validator, html: Node) {
14-
let elementDepth = 0;
15-
1611
const refs = new Map();
1712
const refCallees: Node[] = [];
13+
const elementStack: Node[] = [];
1814

1915
function visit(node: Node) {
2016
if (node.type === 'Element') {
21-
if (
22-
elementDepth === 0 &&
23-
validator.namespace !== namespaces.svg &&
24-
svg.test(node.name)
25-
) {
26-
validator.warn(
27-
`<${node.name}> is an SVG element – did you forget to add { namespace: 'svg' } ?`,
28-
node.start
29-
);
30-
}
31-
3217
if (meta.has(node.name)) {
3318
return meta.get(node.name)(validator, node, refs, refCallees);
3419
}
3520

36-
elementDepth += 1;
37-
38-
validateElement(validator, node, refs, refCallees);
21+
validateElement(validator, node, refs, refCallees, elementStack);
3922
} else if (node.type === 'EachBlock') {
4023
if (validator.helpers.has(node.context)) {
4124
let c = node.expression.end;
@@ -53,16 +36,14 @@ export default function validateHtml(validator: Validator, html: Node) {
5336
}
5437

5538
if (node.children) {
39+
if (node.type === 'Element') elementStack.push(node);
5640
node.children.forEach(visit);
41+
if (node.type === 'Element') elementStack.pop();
5742
}
5843

5944
if (node.else) {
6045
visit(node.else);
6146
}
62-
63-
if (node.type === 'Element') {
64-
elementDepth -= 1;
65-
}
6647
}
6748

6849
html.children.forEach(visit);

src/validate/html/validateElement.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1+
import * as namespaces from '../../utils/namespaces';
12
import validateEventHandler from './validateEventHandler';
23
import { Validator } from '../index';
34
import { Node } from '../../interfaces';
45

5-
export default function validateElement(validator: Validator, node: Node, refs: Map<string, Node[]>, refCallees: Node[]) {
6+
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|switch|symbol|text|textPath|title|tref|tspan|unknown|use|view|vkern)$/;
7+
8+
export default function validateElement(
9+
validator: Validator,
10+
node: Node,
11+
refs: Map<string, Node[]>,
12+
refCallees: Node[],
13+
elementStack: Node[]
14+
) {
615
const isComponent =
716
node.name === ':Self' || validator.components.has(node.name);
817

@@ -11,6 +20,13 @@ export default function validateElement(validator: Validator, node: Node, refs:
1120
validator.warn(`${node.name} component is not defined`, node.start);
1221
}
1322

23+
if (elementStack.length === 0 && validator.namespace !== namespaces.svg && svg.test(node.name)) {
24+
validator.warn(
25+
`<${node.name}> is an SVG element – did you forget to add { namespace: 'svg' } ?`,
26+
node.start
27+
);
28+
}
29+
1430
if (node.name === 'slot') {
1531
const nameAttribute = node.attributes.find((attribute: Node) => attribute.name === 'name');
1632
if (nameAttribute) {
@@ -44,6 +60,8 @@ export default function validateElement(validator: Validator, node: Node, refs:
4460
let hasOutro: boolean;
4561
let hasTransition: boolean;
4662

63+
const attributeMap: Map<string, Node> = new Map();
64+
4765
node.attributes.forEach((attribute: Node) => {
4866
if (attribute.type === 'Ref') {
4967
if (!refs.has(attribute.name)) refs.set(attribute.name, []);
@@ -161,6 +179,8 @@ export default function validateElement(validator: Validator, node: Node, refs:
161179
);
162180
}
163181
} else if (attribute.type === 'Attribute') {
182+
attributeMap.set(attribute.name, attribute);
183+
164184
if (attribute.name === 'value' && node.name === 'textarea') {
165185
if (node.children.length) {
166186
validator.error(
@@ -178,6 +198,29 @@ export default function validateElement(validator: Validator, node: Node, refs:
178198
}
179199
}
180200
});
201+
202+
// a11y
203+
if (node.name === 'a' && !attributeMap.has('href')) {
204+
validator.warn(`A11y: <a> element should have an href attribute`, node.start);
205+
}
206+
207+
if (node.name === 'img' && !attributeMap.has('alt')) {
208+
validator.warn(`A11y: <img> element should have an alt attribute`, node.start);
209+
}
210+
211+
if (node.name === 'figcaption') {
212+
const parent = elementStack[elementStack.length - 1];
213+
if (parent) {
214+
if (parent.name !== 'figure') {
215+
validator.warn(`A11y: <figcaption> must be an immediate child of <figure>`, node.start);
216+
} else {
217+
const index = parent.children.indexOf(node);
218+
if (index !== 0 && index !== parent.children.length - 1) {
219+
validator.warn(`A11y: <figcaption> must be first or last child of <figure>`, node.start);
220+
}
221+
}
222+
}
223+
}
181224
}
182225

183226
function checkTypeAttribute(validator: Validator, node: Node) {

test/validator/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as fs from "fs";
22
import assert from "assert";
33
import { svelte, tryToLoadJson } from "../helpers.js";
44

5-
describe("validate", () => {
5+
describe.only("validate", () => {
66
fs.readdirSync("test/validator/samples").forEach(dir => {
77
if (dir[0] === ".") return;
88

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<a>not actually a link</a>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[{
2+
"message": "A11y: <a> element should have an href attribute",
3+
"loc": {
4+
"line": 1,
5+
"column": 0
6+
},
7+
"pos": 0
8+
}]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<figure>
2+
<img src='foo.jpg' alt='a picture of a foo'>
3+
4+
<figcaption>
5+
a foo in its natural habitat
6+
</figcaption>
7+
8+
<p>this should not be here</p>
9+
</figure>
10+
11+
<figure>
12+
<img src='foo.jpg' alt='a picture of a foo'>
13+
14+
<div class='markup-for-styling'>
15+
<figcaption>
16+
this element should be a child of the figure
17+
</figcaption>
18+
</div>
19+
</figure>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[
2+
{
3+
"message": "A11y: <figcaption> must be first or last child of <figure>",
4+
"loc": {
5+
"line": 4,
6+
"column": 1
7+
},
8+
"pos": 57
9+
},
10+
{
11+
"message": "A11y: <figcaption> must be an immediate child of <figure>",
12+
"loc": {
13+
"line": 15,
14+
"column": 2
15+
},
16+
"pos": 252
17+
}
18+
]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<img src='foo.jpg'>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[{
2+
"message": "A11y: <img> element should have an alt attribute",
3+
"loc": {
4+
"line": 1,
5+
"column": 0
6+
},
7+
"pos": 0
8+
}]

0 commit comments

Comments
 (0)