diff --git a/src/interfaces.ts b/src/interfaces.ts index 07bca42ca0..acb9671499 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -54,7 +54,7 @@ export interface CompileOptions { cascade?: boolean; hydratable?: boolean; legacy?: boolean; - customElement: CustomElementOptions | true; + customElement?: CustomElementOptions | true; onerror?: (error: Error) => void; onwarn?: (warning: Warning) => void; diff --git a/src/validate/html/index.ts b/src/validate/html/index.ts index ad653daa63..186181e913 100644 --- a/src/validate/html/index.ts +++ b/src/validate/html/index.ts @@ -1,4 +1,3 @@ -import * as namespaces from '../../utils/namespaces'; import validateElement from './validateElement'; import validateWindow from './validateWindow'; import fuzzymatch from '../utils/fuzzymatch' @@ -6,36 +5,20 @@ import flattenReference from '../../utils/flattenReference'; import { Validator } from '../index'; import { Node } from '../../interfaces'; -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)$/; - const meta = new Map([[':Window', validateWindow]]); export default function validateHtml(validator: Validator, html: Node) { - let elementDepth = 0; - const refs = new Map(); const refCallees: Node[] = []; + const elementStack: Node[] = []; function visit(node: Node) { if (node.type === 'Element') { - if ( - elementDepth === 0 && - validator.namespace !== namespaces.svg && - svg.test(node.name) - ) { - validator.warn( - `<${node.name}> is an SVG element – did you forget to add { namespace: 'svg' } ?`, - node.start - ); - } - if (meta.has(node.name)) { return meta.get(node.name)(validator, node, refs, refCallees); } - elementDepth += 1; - - validateElement(validator, node, refs, refCallees); + validateElement(validator, node, refs, refCallees, elementStack); } else if (node.type === 'EachBlock') { if (validator.helpers.has(node.context)) { let c = node.expression.end; @@ -53,16 +36,14 @@ export default function validateHtml(validator: Validator, html: Node) { } if (node.children) { + if (node.type === 'Element') elementStack.push(node); node.children.forEach(visit); + if (node.type === 'Element') elementStack.pop(); } if (node.else) { visit(node.else); } - - if (node.type === 'Element') { - elementDepth -= 1; - } } html.children.forEach(visit); diff --git a/src/validate/html/validateElement.ts b/src/validate/html/validateElement.ts index 3641280f19..aa396bac0a 100644 --- a/src/validate/html/validateElement.ts +++ b/src/validate/html/validateElement.ts @@ -1,8 +1,17 @@ +import * as namespaces from '../../utils/namespaces'; import validateEventHandler from './validateEventHandler'; import { Validator } from '../index'; import { Node } from '../../interfaces'; -export default function validateElement(validator: Validator, node: Node, refs: Map, refCallees: Node[]) { +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)$/; + +export default function validateElement( + validator: Validator, + node: Node, + refs: Map, + refCallees: Node[], + elementStack: Node[] +) { const isComponent = node.name === ':Self' || validator.components.has(node.name); @@ -11,6 +20,13 @@ export default function validateElement(validator: Validator, node: Node, refs: validator.warn(`${node.name} component is not defined`, node.start); } + if (elementStack.length === 0 && validator.namespace !== namespaces.svg && svg.test(node.name)) { + validator.warn( + `<${node.name}> is an SVG element – did you forget to add { namespace: 'svg' } ?`, + node.start + ); + } + if (node.name === 'slot') { const nameAttribute = node.attributes.find((attribute: Node) => attribute.name === 'name'); if (nameAttribute) { @@ -44,6 +60,8 @@ export default function validateElement(validator: Validator, node: Node, refs: let hasOutro: boolean; let hasTransition: boolean; + const attributeMap: Map = new Map(); + node.attributes.forEach((attribute: Node) => { if (attribute.type === 'Ref') { if (!refs.has(attribute.name)) refs.set(attribute.name, []); @@ -161,6 +179,8 @@ export default function validateElement(validator: Validator, node: Node, refs: ); } } else if (attribute.type === 'Attribute') { + attributeMap.set(attribute.name, attribute); + if (attribute.name === 'value' && node.name === 'textarea') { if (node.children.length) { validator.error( @@ -178,6 +198,29 @@ export default function validateElement(validator: Validator, node: Node, refs: } } }); + + // a11y + if (node.name === 'a' && !attributeMap.has('href')) { + validator.warn(`A11y: element should have an href attribute`, node.start); + } + + if (node.name === 'img' && !attributeMap.has('alt')) { + validator.warn(`A11y: element should have an alt attribute`, node.start); + } + + if (node.name === 'figcaption') { + const parent = elementStack[elementStack.length - 1]; + if (parent) { + if (parent.name !== 'figure') { + validator.warn(`A11y:
must be an immediate child of
`, node.start); + } else { + const index = parent.children.indexOf(node); + if (index !== 0 && index !== parent.children.length - 1) { + validator.warn(`A11y:
must be first or last child of
`, node.start); + } + } + } + } } function checkTypeAttribute(validator: Validator, node: Node) { diff --git a/test/validator/index.js b/test/validator/index.js index 176d060faf..1c718fae80 100644 --- a/test/validator/index.js +++ b/test/validator/index.js @@ -2,7 +2,7 @@ import * as fs from "fs"; import assert from "assert"; import { svelte, tryToLoadJson } from "../helpers.js"; -describe("validate", () => { +describe.only("validate", () => { fs.readdirSync("test/validator/samples").forEach(dir => { if (dir[0] === ".") return; diff --git a/test/validator/samples/a11y-a-without-href/input.html b/test/validator/samples/a11y-a-without-href/input.html new file mode 100644 index 0000000000..f7ebe13def --- /dev/null +++ b/test/validator/samples/a11y-a-without-href/input.html @@ -0,0 +1 @@ +not actually a link \ No newline at end of file diff --git a/test/validator/samples/a11y-a-without-href/warnings.json b/test/validator/samples/a11y-a-without-href/warnings.json new file mode 100644 index 0000000000..bf5f650ba5 --- /dev/null +++ b/test/validator/samples/a11y-a-without-href/warnings.json @@ -0,0 +1,8 @@ +[{ + "message": "A11y: element should have an href attribute", + "loc": { + "line": 1, + "column": 0 + }, + "pos": 0 +}] \ No newline at end of file diff --git a/test/validator/samples/a11y-figcaption-wrong-place/input.html b/test/validator/samples/a11y-figcaption-wrong-place/input.html new file mode 100644 index 0000000000..ffa7dde65d --- /dev/null +++ b/test/validator/samples/a11y-figcaption-wrong-place/input.html @@ -0,0 +1,19 @@ +
+ a picture of a foo + +
+ a foo in its natural habitat +
+ +

this should not be here

+
+ +
+ a picture of a foo + +
+
+ this element should be a child of the figure +
+
+
\ No newline at end of file diff --git a/test/validator/samples/a11y-figcaption-wrong-place/warnings.json b/test/validator/samples/a11y-figcaption-wrong-place/warnings.json new file mode 100644 index 0000000000..0e5e1a1976 --- /dev/null +++ b/test/validator/samples/a11y-figcaption-wrong-place/warnings.json @@ -0,0 +1,18 @@ +[ + { + "message": "A11y:
must be first or last child of
", + "loc": { + "line": 4, + "column": 1 + }, + "pos": 57 + }, + { + "message": "A11y:
must be an immediate child of
", + "loc": { + "line": 15, + "column": 2 + }, + "pos": 252 + } +] diff --git a/test/validator/samples/a11y-img-without-alt/input.html b/test/validator/samples/a11y-img-without-alt/input.html new file mode 100644 index 0000000000..4e524fe107 --- /dev/null +++ b/test/validator/samples/a11y-img-without-alt/input.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/validator/samples/a11y-img-without-alt/warnings.json b/test/validator/samples/a11y-img-without-alt/warnings.json new file mode 100644 index 0000000000..9c48172925 --- /dev/null +++ b/test/validator/samples/a11y-img-without-alt/warnings.json @@ -0,0 +1,8 @@ +[{ + "message": "A11y: element should have an alt attribute", + "loc": { + "line": 1, + "column": 0 + }, + "pos": 0 +}] \ No newline at end of file