svelte/src/validate/html/validateElement.ts

379 lines
12 KiB

import * as namespaces from '../../utils/namespaces';
import validateEventHandler from './validateEventHandler';
import validate, { Validator } from '../index';
8 years ago
import { Node } from '../../interfaces';
import { dimensions } from '../../utils/patterns';
import isVoidElementName from '../../utils/isVoidElementName';
import isValidIdentifier from '../../utils/isValidIdentifier';
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|tref|tspan|unknown|use|view|vkern)$/;
export default function validateElement(
validator: Validator,
node: Node,
refs: Map<string, Node[]>,
refCallees: Node[],
stack: Node[],
elementStack: Node[]
) {
if (elementStack.length === 0 && validator.namespace !== namespaces.svg && svg.test(node.name)) {
validator.warn(node, {
code: `missing-namespace`,
message: `<${node.name}> is an SVG element did you forget to add { namespace: 'svg' } ?`
});
}
8 years ago
if (node.name === 'slot') {
const nameAttribute = node.attributes.find((attribute: Node) => attribute.name === 'name');
if (nameAttribute) {
if (nameAttribute.value.length !== 1 || nameAttribute.value[0].type !== 'Text') {
validator.error(nameAttribute, {
code: `dynamic-slot-name`,
message: `<slot> name cannot be dynamic`
});
8 years ago
}
const slotName = nameAttribute.value[0].data;
if (slotName === 'default') {
validator.error(nameAttribute, {
code: `invalid-slot-name`,
message: `default is a reserved word — it cannot be used as a slot name`
});
8 years ago
}
// TODO should duplicate slots be disallowed? Feels like it's more likely to be a
// bug than anything. Perhaps it should be a warning
// if (validator.slots.has(slotName)) {
// validator.error(`duplicate '${slotName}' <slot> element`, nameAttribute.start);
// }
// validator.slots.add(slotName);
} else {
// if (validator.slots.has('default')) {
// validator.error(`duplicate default <slot> element`, node.start);
// }
// validator.slots.add('default');
}
}
if (node.name === 'title') {
if (node.attributes.length > 0) {
validator.error(node.attributes[0], {
code: `illegal-attribute`,
message: `<title> cannot have attributes`
});
}
node.children.forEach(child => {
if (child.type !== 'Text' && child.type !== 'MustacheTag') {
validator.error(child, {
code: 'illegal-structure',
message: `<title> can only contain text and {tags}`
});
}
});
}
8 years ago
let hasIntro: boolean;
let hasOutro: boolean;
let hasTransition: boolean;
let hasAnimation: boolean;
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Ref') {
if (!isValidIdentifier(attribute.name)) {
const suggestion = attribute.name.replace(/[^_$a-z0-9]/ig, '_').replace(/^\d/, '_$&');
validator.error(attribute, {
code: `invalid-reference-name`,
message: `Reference name '${attribute.name}' is invalid — must be a valid identifier such as ${suggestion}`
});
} else {
if (!refs.has(attribute.name)) refs.set(attribute.name, []);
refs.get(attribute.name).push(node);
}
}
if (attribute.type === 'Binding') {
const { name } = attribute;
if (name === 'value') {
if (
node.name !== 'input' &&
node.name !== 'textarea' &&
node.name !== 'select'
) {
validator.error(attribute, {
code: `invalid-binding`,
message: `'value' is not a valid binding on <${node.name}> elements`
});
}
if (node.name === 'select') {
const attribute = node.attributes.find(
(attribute: Node) => attribute.name === 'multiple'
);
if (attribute && isDynamic(attribute)) {
validator.error(attribute, {
code: `dynamic-multiple-attribute`,
message: `'multiple' attribute cannot be dynamic if select uses two-way binding`
});
}
} else {
checkTypeAttribute(validator, node);
}
} else if (name === 'checked' || name === 'indeterminate') {
if (node.name !== 'input') {
validator.error(attribute, {
code: `invalid-binding`,
message: `'${name}' is not a valid binding on <${node.name}> elements`
});
}
if (checkTypeAttribute(validator, node) !== 'checkbox') {
validator.error(attribute, {
code: `invalid-binding`,
message: `'${name}' binding can only be used with <input type="checkbox">`
});
}
} else if (name === 'group') {
if (node.name !== 'input') {
validator.error(attribute, {
code: `invalid-binding`,
message: `'group' is not a valid binding on <${node.name}> elements`
});
}
const type = checkTypeAttribute(validator, node);
if (type !== 'checkbox' && type !== 'radio') {
validator.error(attribute, {
code: `invalid-binding`,
message: `'checked' binding can only be used with <input type="checkbox"> or <input type="radio">`
});
}
} else if (name == 'files') {
if (node.name !== 'input') {
validator.error(attribute, {
code: `invalid-binding`,
message: `'files' binding acn only be used with <input type="file">`
});
}
const type = checkTypeAttribute(validator, node);
if (type !== 'file') {
validator.error(attribute, {
code: `invalid-binding`,
message: `'files' binding can only be used with <input type="file">`
});
}
} else if (
name === 'currentTime' ||
name === 'duration' ||
name === 'paused' ||
name === 'buffered' ||
name === 'seekable' ||
name === 'played' ||
name === 'volume'
) {
if (node.name !== 'audio' && node.name !== 'video') {
validator.error(attribute, {
code: `invalid-binding`,
message: `'${name}' binding can only be used with <audio> or <video>`
});
}
} else if (dimensions.test(name)) {
if (node.name === 'svg' && (name === 'offsetWidth' || name === 'offsetHeight')) {
validator.error(attribute, {
code: 'invalid-binding',
message: `'${attribute.name}' is not a valid binding on <svg>. Use '${name.replace('offset', 'client')}' instead`
});
} else if (svg.test(node.name)) {
validator.error(attribute, {
code: 'invalid-binding',
message: `'${attribute.name}' is not a valid binding on SVG elements`
});
} else if (isVoidElementName(node.name)) {
validator.error(attribute, {
code: 'invalid-binding',
message: `'${attribute.name}' is not a valid binding on void elements like <${node.name}>. Use a wrapper element instead`
});
}
} else {
validator.error(attribute, {
code: `invalid-binding`,
message: `'${attribute.name}' is not a valid binding`
});
}
} else if (attribute.type === 'EventHandler') {
validator.used.events.add(attribute.name);
validateEventHandler(validator, attribute, refCallees);
} else if (attribute.type === 'Transition') {
validator.used.transitions.add(attribute.name);
const bidi = attribute.intro && attribute.outro;
if (hasTransition) {
if (bidi) {
validator.error(attribute, {
code: `duplicate-transition`,
message: `An element can only have one 'transition' directive`
});
}
validator.error(attribute, {
code: `duplicate-transition`,
message: `An element cannot have both a 'transition' directive and an '${attribute.intro ? 'in' : 'out'}' directive`
});
}
if ((hasIntro && attribute.intro) || (hasOutro && attribute.outro)) {
if (bidi) {
validator.error(attribute, {
code: `duplicate-transition`,
message: `An element cannot have both an '${hasIntro ? 'in' : 'out'}' directive and a 'transition' directive`
});
}
validator.error(attribute, {
code: `duplicate-transition`,
message: `An element can only have one '${hasIntro ? 'in' : 'out'}' directive`
});
}
if (attribute.intro) hasIntro = true;
if (attribute.outro) hasOutro = true;
if (bidi) hasTransition = true;
if (!validator.transitions.has(attribute.name)) {
validator.error(attribute, {
code: `missing-transition`,
message: `Missing transition '${attribute.name}'`
});
}
} else if (attribute.type === 'Animation') {
validator.used.animations.add(attribute.name);
if (hasAnimation) {
validator.error(attribute, {
code: `duplicate-animation`,
message: `An element can only have one 'animate' directive`
});
}
if (!validator.animations.has(attribute.name)) {
validator.error(attribute, {
code: `missing-animation`,
message: `Missing animation '${attribute.name}'`
});
}
const parent = stack[stack.length - 1];
if (!parent || parent.type !== 'EachBlock' || !parent.key) {
// TODO can we relax the 'immediate child' rule?
validator.error(attribute, {
code: `invalid-animation`,
message: `An element that use the animate directive must be the immediate child of a keyed each block`
});
}
if (parent.children.length > 1) {
validator.error(attribute, {
code: `invalid-animation`,
message: `An element that use the animate directive must be the sole child of a keyed each block`
});
}
hasAnimation = true;
} else if (attribute.type === 'Attribute') {
if (attribute.name === 'value' && node.name === 'textarea') {
if (node.children.length) {
validator.error(attribute, {
code: `textarea-duplicate-value`,
message: `A <textarea> can have either a value attribute or (equivalently) child content, but not both`
});
}
}
8 years ago
if (attribute.name === 'slot') {
checkSlotAttribute(validator, node, attribute, stack);
8 years ago
}
Adds actions to components Actions add additional functionality to elements within your component's template that may be difficult to add with other mechanisms. Examples of functionality which actions makes trivial to attach are: * tooltips * image lazy loaders * drag and drop functionality Actions can be added to an element with the `use` directive. ```html <img use:lazyload data-src="giant-photo.jpg> ``` Data may be passed to the action as an object literal (e.g. `use:b="{ setting: true }"`, a literal value (e.g. `use:b="'a string'"`), or a value or function from your component's state (e.g. `add:b="foo"` or `add:b="foo()"`). Actions are defined in a "actions" property on your component definition. ```html <script> export default { actions: { b(node, data) { // do something return { update(data) {}, destroy() {} } } } } </script> ``` A action is a function which receives a reference to an element and optionally the data if it is added in the HTML. This function can then attach listeners or alter the element as needed. The action can optionally return an object with the methods `update(data)` and `destroy()`. When data is added in the HTML and comes from state, the action's `update(data)` will be called if defined whenever the state is changed. When the element is removed from the DOM `destroy()` will be called if provided, allowing for cleanup of event listeners, etc. See https://github.com/sveltejs/svelte/issues/469 for discussion around this feature and more examples of how it could be used.
7 years ago
} else if (attribute.type === 'Action') {
validator.used.actions.add(attribute.name);
if (!validator.actions.has(attribute.name)) {
validator.error(attribute, {
code: `missing-action`,
message: `Missing action '${attribute.name}'`
});
Adds actions to components Actions add additional functionality to elements within your component's template that may be difficult to add with other mechanisms. Examples of functionality which actions makes trivial to attach are: * tooltips * image lazy loaders * drag and drop functionality Actions can be added to an element with the `use` directive. ```html <img use:lazyload data-src="giant-photo.jpg> ``` Data may be passed to the action as an object literal (e.g. `use:b="{ setting: true }"`, a literal value (e.g. `use:b="'a string'"`), or a value or function from your component's state (e.g. `add:b="foo"` or `add:b="foo()"`). Actions are defined in a "actions" property on your component definition. ```html <script> export default { actions: { b(node, data) { // do something return { update(data) {}, destroy() {} } } } } </script> ``` A action is a function which receives a reference to an element and optionally the data if it is added in the HTML. This function can then attach listeners or alter the element as needed. The action can optionally return an object with the methods `update(data)` and `destroy()`. When data is added in the HTML and comes from state, the action's `update(data)` will be called if defined whenever the state is changed. When the element is removed from the DOM `destroy()` will be called if provided, allowing for cleanup of event listeners, etc. See https://github.com/sveltejs/svelte/issues/469 for discussion around this feature and more examples of how it could be used.
7 years ago
}
}
});
}
function checkTypeAttribute(validator: Validator, node: Node) {
const attribute = node.attributes.find(
(attribute: Node) => attribute.name === 'type'
);
if (!attribute) return null;
if (attribute.value === true) {
validator.error(attribute, {
code: `missing-type`,
message: `'type' attribute must be specified`
});
}
8 years ago
if (isDynamic(attribute)) {
validator.error(attribute, {
code: `invalid-type`,
message: `'type' attribute cannot be dynamic if input uses two-way binding`
});
}
return attribute.value[0].data;
}
8 years ago
function checkSlotAttribute(validator: Validator, node: Node, attribute: Node, stack: Node[]) {
if (isDynamic(attribute)) {
validator.error(attribute, {
code: `invalid-slot-attribute`,
message: `slot attribute cannot have a dynamic value`
});
}
let i = stack.length;
while (i--) {
const parent = stack[i];
if (parent.type === 'Component') {
// if we're inside a component or a custom element, gravy
7 years ago
if (parent.name === 'svelte:self' || parent.name === 'svelte:component' || validator.components.has(parent.name)) return;
} else if (parent.type === 'Element') {
if (/-/.test(parent.name)) return;
}
if (parent.type === 'IfBlock' || parent.type === 'EachBlock') {
const message = `Cannot place slotted elements inside an ${parent.type === 'IfBlock' ? 'if' : 'each'}-block`;
validator.error(attribute, {
code: `invalid-slotted-content`,
message
});
}
}
validator.error(attribute, {
code: `invalid-slotted-content`,
message: `Element with a slot='...' attribute must be a descendant of a component or custom element`
});
}
8 years ago
function isDynamic(attribute: Node) {
7 years ago
if (attribute.value === true) return false;
8 years ago
return attribute.value.length > 1 || attribute.value[0].type !== 'Text';
}