@ -20,26 +20,35 @@ export default function validateElement(
if ( ! isComponent && /^[A-Z]/ . test ( node . name [ 0 ] ) ) {
if ( ! isComponent && /^[A-Z]/ . test ( node . name [ 0 ] ) ) {
// TODO upgrade to validator.error in v2
// TODO upgrade to validator.error in v2
validator . warn ( ` ${ node . name } component is not defined ` , node ) ;
validator . warn ( node , {
code : ` missing-component ` ,
message : ` ${ node . name } component is not defined `
} ) ;
}
}
if ( elementStack . length === 0 && validator . namespace !== namespaces . svg && svg . test ( node . name ) ) {
if ( elementStack . length === 0 && validator . namespace !== namespaces . svg && svg . test ( node . name ) ) {
validator . warn (
validator . warn ( node , {
` < ${ node . name } > is an SVG element – did you forget to add { namespace: 'svg' } ? ` ,
code : ` missing-namespace ` ,
node
message: ` < ${ node. name } > is an SVG element – did you forget to add { namespace: 'svg' } ? `
);
} );
}
}
if ( node . name === 'slot' ) {
if ( node . name === 'slot' ) {
const nameAttribute = node . attributes . find ( ( attribute : Node ) = > attribute . name === 'name' ) ;
const nameAttribute = node . attributes . find ( ( attribute : Node ) = > attribute . name === 'name' ) ;
if ( nameAttribute ) {
if ( nameAttribute ) {
if ( nameAttribute . value . length !== 1 || nameAttribute . value [ 0 ] . type !== 'Text' ) {
if ( nameAttribute . value . length !== 1 || nameAttribute . value [ 0 ] . type !== 'Text' ) {
validator . error ( ` <slot> name cannot be dynamic ` , nameAttribute ) ;
validator . error ( nameAttribute , {
code : ` dynamic-slot-name ` ,
message : ` <slot> name cannot be dynamic `
} ) ;
}
}
const slotName = nameAttribute . value [ 0 ] . data ;
const slotName = nameAttribute . value [ 0 ] . data ;
if ( slotName === 'default' ) {
if ( slotName === 'default' ) {
validator . error ( ` default is a reserved word — it cannot be used as a slot name ` , nameAttribute ) ;
validator . error ( nameAttribute , {
code : ` invalid-slot-name ` ,
message : ` default is a reserved word — it cannot be used as a slot name `
} ) ;
}
}
// TODO should duplicate slots be disallowed? Feels like it's more likely to be a
// TODO should duplicate slots be disallowed? Feels like it's more likely to be a
@ -61,18 +70,18 @@ export default function validateElement(
if ( node . name === 'title' ) {
if ( node . name === 'title' ) {
if ( node . attributes . length > 0 ) {
if ( node . attributes . length > 0 ) {
validator . error (
validator . error ( node . attributes [ 0 ] , {
` <title> cannot have attributes ` ,
code : ` illegal-attribute ` ,
node. attributes [ 0 ]
message: ` <title> cannot have attributes `
);
} );
}
}
node . children . forEach ( child = > {
node . children . forEach ( child = > {
if ( child . type !== 'Text' && child . type !== 'MustacheTag' ) {
if ( child . type !== 'Text' && child . type !== 'MustacheTag' ) {
validator . error (
validator . error ( child , {
` <title> can only contain text and {{tags}} ` ,
code : 'illegal-structure' ,
child
message: ` <title> can only contain text and {{tags}} `
);
} );
}
}
} ) ;
} ) ;
}
}
@ -96,10 +105,10 @@ export default function validateElement(
node . name !== 'textarea' &&
node . name !== 'textarea' &&
node . name !== 'select'
node . name !== 'select'
) {
) {
validator . error (
validator . error ( attribute , {
` 'value' is not a valid binding on < ${ node . name } > elements ` ,
code : ` invalid-binding ` ,
attribute
message: ` 'value' is not a valid binding on < ${ node . name } > elements `
);
} );
}
}
if ( node . name === 'select' ) {
if ( node . name === 'select' ) {
@ -108,43 +117,43 @@ export default function validateElement(
) ;
) ;
if ( attribute && isDynamic ( attribute ) ) {
if ( attribute && isDynamic ( attribute ) ) {
validator . error (
validator . error ( attribute , {
` 'multiple' attribute cannot be dynamic if select uses two-way binding ` ,
code : ` dynamic-multiple-attribute ` ,
attribute
message: ` 'multiple' attribute cannot be dynamic if select uses two-way binding`
);
} );
}
}
} else {
} else {
checkTypeAttribute ( validator , node ) ;
checkTypeAttribute ( validator , node ) ;
}
}
} else if ( name === 'checked' || name === 'indeterminate' ) {
} else if ( name === 'checked' || name === 'indeterminate' ) {
if ( node . name !== 'input' ) {
if ( node . name !== 'input' ) {
validator . error (
validator . error ( attribute , {
` ' ${ name } ' is not a valid binding on < ${ node . name } > elements ` ,
code : ` invalid-binding ` ,
attribute
message: ` ' ${ name } ' is not a valid binding on < ${ node . name } > elements `
);
} );
}
}
if ( checkTypeAttribute ( validator , node ) !== 'checkbox' ) {
if ( checkTypeAttribute ( validator , node ) !== 'checkbox' ) {
validator . error (
validator . error ( attribute , {
` ' ${ name } ' binding can only be used with <input type="checkbox"> ` ,
code : ` invalid-binding ` ,
attribute
message: ` ' ${ name } ' binding can only be used with <input type="checkbox"> `
);
} );
}
}
} else if ( name === 'group' ) {
} else if ( name === 'group' ) {
if ( node . name !== 'input' ) {
if ( node . name !== 'input' ) {
validator . error (
validator . error ( attribute , {
` 'group' is not a valid binding on < ${ node . name } > elements ` ,
code : ` invalid-binding ` ,
attribute
message: ` 'group' is not a valid binding on < ${ node . name } > elements `
);
} );
}
}
const type = checkTypeAttribute ( validator , node ) ;
const type = checkTypeAttribute ( validator , node ) ;
if ( type !== 'checkbox' && type !== 'radio' ) {
if ( type !== 'checkbox' && type !== 'radio' ) {
validator . error (
validator . error ( attribute , {
` 'checked' binding can only be used with <input type="checkbox"> or <input type="radio"> ` ,
code : ` invalid-binding ` ,
attribute
message: ` 'checked' binding can only be used with <input type="checkbox"> or <input type="radio"> `
);
} );
}
}
} else if (
} else if (
name === 'currentTime' ||
name === 'currentTime' ||
@ -156,23 +165,26 @@ export default function validateElement(
name === 'volume'
name === 'volume'
) {
) {
if ( node . name !== 'audio' && node . name !== 'video' ) {
if ( node . name !== 'audio' && node . name !== 'video' ) {
validator . error (
validator . error ( attribute , {
` ' ${ name } ' binding can only be used with <audio> or <video> ` ,
code : ` invalid-binding ` ,
attribute
message: ` ' ${ name } ' binding can only be used with <audio> or <video> `
);
} );
}
}
} else {
} else {
validator . error (
validator . error ( attribute , {
` ' ${ attribute . name } ' is not a valid binding` ,
code : ` invalid- binding` ,
attribute
message: ` ' ${ attribute. name } ' is not a valid binding `
);
} );
}
}
} else if ( attribute . type === 'EventHandler' ) {
} else if ( attribute . type === 'EventHandler' ) {
validator . used . events . add ( attribute . name ) ;
validator . used . events . add ( attribute . name ) ;
validateEventHandler ( validator , attribute , refCallees ) ;
validateEventHandler ( validator , attribute , refCallees ) ;
} else if ( attribute . type === 'Transition' ) {
} else if ( attribute . type === 'Transition' ) {
if ( isComponent ) {
if ( isComponent ) {
validator . error ( ` Transitions can only be applied to DOM elements, not components ` , attribute ) ;
validator . error ( attribute , {
code : ` invalid-transition ` ,
message : ` Transitions can only be applied to DOM elements, not components `
} ) ;
}
}
validator . used . transitions . add ( attribute . name ) ;
validator . used . transitions . add ( attribute . name ) ;
@ -180,31 +192,31 @@ export default function validateElement(
const bidi = attribute . intro && attribute . outro ;
const bidi = attribute . intro && attribute . outro ;
if ( hasTransition ) {
if ( hasTransition ) {
if ( bidi )
if ( bidi ) {
validator . error (
validator . error ( attribute , {
` An element can only have one 'transition' directive ` ,
code : ` duplicate-transition ` ,
attribu te
mess age: ` An elemen t can only have one ' trans ition' dir ective`
);
} );
validator . error (
}
` An element cannot have both a 'transition' directive and an ' ${ attribute . intro
? 'in'
validator . error ( attribute , {
: 'out' } ' directive ` ,
code : ` duplicate-transition ` ,
attribute
message: ` An element cannot have both a 'transition' directive and an ' ${ attribute. intro ? 'in' : 'out' } ' directive `
);
} );
}
}
if ( ( hasIntro && attribute . intro ) || ( hasOutro && attribute . outro ) ) {
if ( ( hasIntro && attribute . intro ) || ( hasOutro && attribute . outro ) ) {
if ( bidi )
if ( bidi ) {
validator . error (
validator . error ( attribute , {
` An element cannot have both an ' ${ hasIntro
code : ` duplicate-transition ` ,
? 'in'
message : ` An element cannot have both an ' ${ hasIntro ? 'in' : 'out' } ' directive and a 'transition' directive `
: 'out' } ' directive and a ' transition ' directive ` ,
} ) ;
attribute
}
) ;
validator . error (
validator . error ( attribute , {
` An element can only have one ' ${ hasIntro ? 'in' : 'out' } ' directive ` ,
code : ` duplicate-transition ` ,
attribute
message: ` An element can only have one ' ${ hasIntro ? 'in' : 'out' } ' directive `
);
} );
}
}
if ( attribute . intro ) hasIntro = true ;
if ( attribute . intro ) hasIntro = true ;
@ -212,18 +224,18 @@ export default function validateElement(
if ( bidi ) hasTransition = true ;
if ( bidi ) hasTransition = true ;
if ( ! validator . transitions . has ( attribute . name ) ) {
if ( ! validator . transitions . has ( attribute . name ) ) {
validator . error (
validator . error ( attribute , {
` Missing transition ' ${ attribute . name } ' ` ,
code : ` missing-transition ` ,
attribute
message: ` Missing transition ' ${ attribute. name } ' `
);
} );
}
}
} else if ( attribute . type === 'Attribute' ) {
} else if ( attribute . type === 'Attribute' ) {
if ( attribute . name === 'value' && node . name === 'textarea' ) {
if ( attribute . name === 'value' && node . name === 'textarea' ) {
if ( node . children . length ) {
if ( node . children . length ) {
validator . error (
validator . error ( attribute , {
` A <textarea> can have either a value attribute or (equivalently) child content, but not both ` ,
code : ` textarea-duplicate-value ` ,
attribute
message: ` A <textarea> can have either a value attribute or (equivalently) child content, but not both`
);
} );
}
}
}
}
@ -232,16 +244,19 @@ export default function validateElement(
}
}
} else if ( attribute . type === 'Action' ) {
} else if ( attribute . type === 'Action' ) {
if ( isComponent ) {
if ( isComponent ) {
validator . error ( ` Actions can only be applied to DOM elements, not components ` , attribute ) ;
validator . error ( attribute , {
code : ` invalid-action ` ,
message : ` Actions can only be applied to DOM elements, not components `
} ) ;
}
}
validator . used . actions . add ( attribute . name ) ;
validator . used . actions . add ( attribute . name ) ;
if ( ! validator . actions . has ( attribute . name ) ) {
if ( ! validator . actions . has ( attribute . name ) ) {
validator . error (
validator . error ( attribute , {
` Missing action ' ${ attribute . name } ' ` ,
code : ` missing-action ` ,
attribute
message: ` Missing action ' ${ attribute. name } ' `
);
} );
}
}
}
}
} ) ;
} ) ;
@ -254,14 +269,17 @@ function checkTypeAttribute(validator: Validator, node: Node) {
if ( ! attribute ) return null ;
if ( ! attribute ) return null ;
if ( attribute . value === true ) {
if ( attribute . value === true ) {
validator . error ( ` 'type' attribute must be specified ` , attribute ) ;
validator . error ( attribute , {
code : ` missing-type ` ,
message : ` 'type' attribute must be specified `
} ) ;
}
}
if ( isDynamic ( attribute ) ) {
if ( isDynamic ( attribute ) ) {
validator . error (
validator . error ( attribute , {
` 'type' attribute cannot be dynamic if input uses two-way binding ` ,
code : ` invalid-type ` ,
attribute
message: ` 'type' attribute cannot be dynamic if input uses two-way binding`
);
} );
}
}
return attribute . value [ 0 ] . data ;
return attribute . value [ 0 ] . data ;
@ -269,10 +287,10 @@ function checkTypeAttribute(validator: Validator, node: Node) {
function checkSlotAttribute ( validator : Validator , node : Node , attribute : Node , stack : Node [ ] ) {
function checkSlotAttribute ( validator : Validator , node : Node , attribute : Node , stack : Node [ ] ) {
if ( isDynamic ( attribute ) ) {
if ( isDynamic ( attribute ) ) {
validator . error (
validator . error ( attribute , {
` slot attribute cannot have a dynamic valu e` ,
code : ` invalid-slot-attribut e` ,
attribute
message: ` slot attribute cannot have a dynamic value`
);
} );
}
}
let i = stack . length ;
let i = stack . length ;
@ -286,11 +304,17 @@ function checkSlotAttribute(validator: Validator, node: Node, attribute: Node, s
if ( parent . type === 'IfBlock' || parent . type === 'EachBlock' ) {
if ( parent . type === 'IfBlock' || parent . type === 'EachBlock' ) {
const message = ` Cannot place slotted elements inside an ${ parent . type === 'IfBlock' ? 'if' : 'each' } -block ` ;
const message = ` Cannot place slotted elements inside an ${ parent . type === 'IfBlock' ? 'if' : 'each' } -block ` ;
validator . error ( message , attribute ) ;
validator . error ( attribute , {
code : ` invalid-slotted-content ` ,
message
} ) ;
}
}
}
}
validator . error ( ` Element with a slot='...' attribute must be a descendant of a component or custom element ` , attribute ) ;
validator . 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 ) {