pull/1721/head
Rich Harris 7 years ago
parent 8a46bc2e82
commit 49ed03c361

@ -11,6 +11,15 @@ export default class Action extends Node {
this.name = info.name; this.name = info.name;
component.used.actions.add(this.name);
if (!component.actions.has(this.name)) {
component.error(this, {
code: `missing-action`,
message: `Missing action '${this.name}'`
});
}
this.expression = info.expression this.expression = info.expression
? new Expression(component, this, scope, info.expression) ? new Expression(component, this, scope, info.expression)
: null; : null;

@ -64,6 +64,8 @@ const booleanAttributes = new Set([
'translate' 'translate'
]); ]);
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)$/;
const ariaAttributes = 'activedescendant atomic autocomplete busy checked 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 selected setsize sort valuemax valuemin valuenow valuetext'.split(' '); const ariaAttributes = 'activedescendant atomic autocomplete busy checked 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 selected setsize sort valuemax valuemin valuenow valuetext'.split(' ');
const ariaAttributeSet = new Set(ariaAttributes); const ariaAttributeSet = new Set(ariaAttributes);
@ -131,6 +133,13 @@ export default class Element extends Node {
namespaces.svg : namespaces.svg :
parentElement ? parentElement.namespace : this.component.namespace; parentElement ? parentElement.namespace : this.component.namespace;
if (!this.namespace && svg.test(this.name)) {
this.component.warn(this, {
code: `missing-namespace`,
message: `<${this.name}> is an SVG element did you forget to add { namespace: 'svg' } ?`
});
}
this.attributes = []; this.attributes = [];
this.actions = []; this.actions = [];
this.bindings = []; this.bindings = [];
@ -234,6 +243,7 @@ export default class Element extends Node {
} }
this.validateAttributes(); this.validateAttributes();
this.validateBindings();
this.validateContent(); this.validateContent();
} }
@ -326,6 +336,38 @@ export default class Element extends Node {
} }
} }
if (name === 'slot') {
if (attribute.isDynamic) {
component.error(attribute, {
code: `invalid-slot-attribute`,
message: `slot attribute cannot have a dynamic value`
});
}
let ancestor = parent;
do {
if (ancestor.type === 'InlineComponent') break;
if (ancestor.type === 'Element' && /-/.test(ancestor.name)) break;
if (ancestor.type === 'IfBlock' || ancestor.type === 'EachBlock') {
const type = ancestor.type === 'IfBlock' ? 'if' : 'each';
const message = `Cannot place slotted elements inside an ${type}-block`;
component.error(attribute, {
code: `invalid-slotted-content`,
message
});
}
} while (ancestor);
if (!ancestor) {
component.error(attribute, {
code: `invalid-slotted-content`,
message: `Element with a slot='...' attribute must be a descendant of a component or custom element`
});
}
}
attributeMap.set(attribute.name, attribute); attributeMap.set(attribute.name, attribute);
}); });
@ -362,6 +404,151 @@ export default class Element extends Node {
} }
} }
validateBindings() {
const { component } = this;
const checkTypeAttribute = () => {
const attribute = this.attributes.find(
(attribute: Attribute) => attribute.name === 'type'
);
if (!attribute) return null;
if (attribute.isDynamic) {
component.error(attribute, {
code: `invalid-type`,
message: `'type' attribute cannot be dynamic if input uses two-way binding`
});
}
const value = attribute.getStaticValue();
if (value === true) {
component.error(attribute, {
code: `missing-type`,
message: `'type' attribute must be specified`
});
}
return value;
};
this.bindings.forEach(binding => {
const { name } = binding;
if (name === 'value') {
if (
this.name !== 'input' &&
this.name !== 'textarea' &&
this.name !== 'select'
) {
component.error(binding, {
code: `invalid-binding`,
message: `'value' is not a valid binding on <${this.name}> elements`
});
}
if (this.name === 'select') {
const attribute = this.attributes.find(
(attribute: Attribute) => attribute.name === 'multiple'
);
if (attribute && attribute.isDynamic) {
component.error(attribute, {
code: `dynamic-multiple-attribute`,
message: `'multiple' attribute cannot be dynamic if select uses two-way binding`
});
}
} else {
checkTypeAttribute();
}
} else if (name === 'checked' || name === 'indeterminate') {
if (this.name !== 'input') {
component.error(binding, {
code: `invalid-binding`,
message: `'${name}' is not a valid binding on <${this.name}> elements`
});
}
if (checkTypeAttribute() !== 'checkbox') {
component.error(binding, {
code: `invalid-binding`,
message: `'${name}' binding can only be used with <input type="checkbox">`
});
}
} else if (name === 'group') {
if (this.name !== 'input') {
component.error(binding, {
code: `invalid-binding`,
message: `'group' is not a valid binding on <${this.name}> elements`
});
}
const type = checkTypeAttribute();
if (type !== 'checkbox' && type !== 'radio') {
component.error(binding, {
code: `invalid-binding`,
message: `'checked' binding can only be used with <input type="checkbox"> or <input type="radio">`
});
}
} else if (name == 'files') {
if (this.name !== 'input') {
component.error(binding, {
code: `invalid-binding`,
message: `'files' binding acn only be used with <input type="file">`
});
}
const type = checkTypeAttribute();
if (type !== 'file') {
component.error(binding, {
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 (this.name !== 'audio' && this.name !== 'video') {
component.error(binding, {
code: `invalid-binding`,
message: `'${name}' binding can only be used with <audio> or <video>`
});
}
} else if (dimensions.test(name)) {
if (this.name === 'svg' && (name === 'offsetWidth' || name === 'offsetHeight')) {
component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on <svg>. Use '${name.replace('offset', 'client')}' instead`
});
} else if (svg.test(this.name)) {
component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on SVG elements`
});
} else if (isVoidElementName(this.name)) {
component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on void elements like <${this.name}>. Use a wrapper element instead`
});
}
} else {
component.error(binding, {
code: `invalid-binding`,
message: `'${binding.name}' is not a valid binding`
});
}
});
}
validateContent() { validateContent() {
if (!a11yRequiredContent.has(this.name)) return; if (!a11yRequiredContent.has(this.name)) return;

@ -16,15 +16,6 @@ export default function validateElement(
stack: Node[], stack: Node[],
elementStack: Node[] elementStack: Node[]
) { ) {
if (elementStack.length === 0 && component.namespace !== namespaces.svg && svg.test(node.name)) {
component.warn(node, {
code: `missing-namespace`,
message: `<${node.name}> is an SVG element did you forget to add { namespace: 'svg' } ?`
});
}
let hasAnimation: boolean;
node.attributes.forEach((attribute: Node) => { node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Binding') { if (attribute.type === 'Binding') {
const { name } = attribute; const { name } = attribute;
@ -148,19 +139,6 @@ export default function validateElement(
}); });
} }
} }
if (attribute.name === 'slot') {
checkSlotAttribute(component, node, attribute, stack);
}
} else if (attribute.type === 'Action') {
component.used.actions.add(attribute.name);
if (!component.actions.has(attribute.name)) {
component.error(attribute, {
code: `missing-action`,
message: `Missing action '${attribute.name}'`
});
}
} }
}); });
} }
@ -188,40 +166,6 @@ function checkTypeAttribute(component: Component, node: Node) {
return attribute.value[0].data; return attribute.value[0].data;
} }
function checkSlotAttribute(component: Component, node: Node, attribute: Node, stack: Node[]) {
if (isDynamic(attribute)) {
component.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 === 'InlineComponent') {
// if we're inside a component or a custom element, gravy
if (parent.name === 'svelte:self' || parent.name === 'svelte:component' || component.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`;
component.error(attribute, {
code: `invalid-slotted-content`,
message
});
}
}
component.error(attribute, {
code: `invalid-slotted-content`,
message: `Element with a slot='...' attribute must be a descendant of a component or custom element`
});
}
function isDynamic(attribute: Node) { function isDynamic(attribute: Node) {
if (attribute.value === true) return false; if (attribute.value === true) return false;
return attribute.value.length > 1 || attribute.value[0].type !== 'Text'; return attribute.value.length > 1 || attribute.value[0].type !== 'Text';

Loading…
Cancel
Save