@ -136,44 +136,45 @@ export default class Element extends Node {
this . namespace = get_namespace ( parent as Element , this , component . namespace ) ;
if ( this . name === 'textarea' ) {
if ( info . children . length > 0 ) {
const value_attribute = info . attributes . find ( node = > node . name === 'value' ) ;
if ( value_attribute ) {
component . error ( value_attribute , {
code : 'textarea-duplicate-value' ,
message : 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
} ) ;
}
if ( this . namespace !== namespaces . foreign ) {
if ( this . name === 'textarea' ) {
if ( info . children . length > 0 ) {
const value_attribute = info . attributes . find ( node = > node . name === 'value' ) ;
if ( value_attribute ) {
component . error ( value_attribute , {
code : 'textarea-duplicate-value' ,
message : 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
} ) ;
}
// this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute
info . attributes . push ( {
type : 'Attribute' ,
name : 'value' ,
value : info.children
} ) ;
// this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute
info . attributes . push ( {
type : 'Attribute' ,
name : 'value' ,
value : info.children
} ) ;
info . children = [ ] ;
info . children = [ ] ;
}
}
}
if ( this . name === 'option' ) {
// Special case — treat these the same way:
// <option>{foo}</option>
// <option value={foo}>{foo}</option>
const value_attribute = info . attributes . find ( attribute = > attribute . name === 'value' ) ;
if ( this . name === 'option' ) {
// Special case — treat these the same way:
// <option>{foo}</option>
// <option value={foo}>{foo}</option>
const value_attribute = info . attributes . find ( attribute = > attribute . name === 'value' ) ;
if ( ! value_attribute ) {
info . attributes . push ( {
type : 'Attribute' ,
name : 'value' ,
value : info.children ,
synthetic : true
} ) ;
if ( ! value_attribute ) {
info . attributes . push ( {
type : 'Attribute' ,
name : 'value' ,
value : info.children ,
synthetic : true
} ) ;
}
}
}
const has_let = info . attributes . some ( node = > node . type === 'Let' ) ;
if ( has_let ) {
scope = scope . child ( ) ;
@ -253,65 +254,83 @@ export default class Element extends Node {
} ) ;
}
if ( a11y_distracting_elements . has ( this . name ) ) {
// no-distracting-elements
this . component . warn ( this , {
code : 'a11y-distracting-elements' ,
message : ` A11y: Avoid < ${ this . name } > elements `
} ) ;
this . validate_attributes ( ) ;
this . validate_event_handlers ( ) ;
if ( this . namespace === namespaces . foreign ) {
this . validate_bindings_foreign ( ) ;
} else {
this . validate_attributes_a11y ( ) ;
this . validate_special_cases ( ) ;
this . validate_bindings ( ) ;
this . validate_content ( ) ;
}
if ( this . name === 'figcaption' ) {
let { parent } = this ;
let is_figure_parent = false ;
}
while ( parent ) {
if ( ( parent as Element ) . name === 'figure' ) {
is_figure_parent = true ;
break ;
}
if ( parent . type === 'Element' ) {
break ;
}
parent = parent . parent ;
}
validate_attributes() {
const { component , parent } = this ;
if ( ! is_figure_parent ) {
this . component . warn ( this , {
code : 'a11y-structure' ,
message : 'A11y: <figcaption> must be an immediate child of <figure>'
this . attributes . forEach ( attribute = > {
if ( attribute . is_spread ) return ;
const name = attribute . name . toLowerCase ( ) ;
// Errors
if ( /(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/ . test ( name ) ) {
component . error ( attribute , {
code : 'illegal-attribute' ,
message : ` ' ${ name } ' is not a valid attribute name `
} ) ;
}
}
if ( this . name === 'figure' ) {
const children = this . children . filter ( node = > {
if ( node . type === 'Comment' ) return false ;
if ( node . type === 'Text' ) return /\S/ . test ( node . data ) ;
return true ;
} ) ;
if ( name === 'slot' ) {
if ( ! attribute . is_static ) {
component . error ( attribute , {
code : 'invalid-slot-attribute' ,
message : 'slot attribute cannot have a dynamic value'
} ) ;
}
const index = children . findIndex ( child = > ( child as Element ) . name === 'figcaption' ) ;
if ( component . slot_outlets . has ( name ) ) {
component . error ( attribute , {
code : 'duplicate-slot-attribute' ,
message : ` Duplicate ' ${ name } ' slot `
} ) ;
if ( index !== - 1 && ( index !== 0 && index !== children . length - 1 ) ) {
this . component . warn ( children [ index ] , {
code : 'a11y-structure' ,
message : 'A11y: <figcaption> must be first or last child of <figure>'
} ) ;
component . slot_outlets . add ( name ) ;
}
if ( ! ( parent . type === 'InlineComponent' || within_custom_element ( parent ) ) ) {
component . error ( attribute , {
code : 'invalid-slotted-content' ,
message : 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
} ) ;
}
}
}
this . validate_attributes ( ) ;
this . validate_special_cases ( ) ;
this . validate_bindings ( ) ;
this . validate_content ( ) ;
this . validate_event_handlers ( ) ;
}
// Warnings
validate_attributes() {
const { component , parent } = this ;
if ( this . namespace !== namespaces . foreign ) {
if ( name === 'is' ) {
component . warn ( attribute , {
code : 'avoid-is' ,
message : 'The \'is\' attribute is not supported cross-browser and should be avoided'
} ) ;
}
const attribute_map = new Map ( ) ;
if ( react_attributes . has ( attribute . name ) ) {
component . warn ( attribute , {
code : 'invalid-html-attribute' ,
message : ` ' ${ attribute . name } ' is not a valid HTML attribute. Did you mean ' ${ react_attributes . get ( attribute . name ) } '? `
} ) ;
}
}
} ) ;
}
validate_attributes_a11y() {
const { component } = this ;
this . attributes . forEach ( attribute = > {
if ( attribute . is_spread ) return ;
@ -408,60 +427,13 @@ export default class Element extends Node {
} ) ;
}
}
if ( /(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/ . test ( name ) ) {
component . error ( attribute , {
code : 'illegal-attribute' ,
message : ` ' ${ name } ' is not a valid attribute name `
} ) ;
}
if ( name === 'slot' ) {
if ( ! attribute . is_static ) {
component . error ( attribute , {
code : 'invalid-slot-attribute' ,
message : 'slot attribute cannot have a dynamic value'
} ) ;
}
if ( component . slot_outlets . has ( name ) ) {
component . error ( attribute , {
code : 'duplicate-slot-attribute' ,
message : ` Duplicate ' ${ name } ' slot `
} ) ;
component . slot_outlets . add ( name ) ;
}
if ( ! ( parent . type === 'InlineComponent' || within_custom_element ( parent ) ) ) {
component . error ( attribute , {
code : 'invalid-slotted-content' ,
message : 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
} ) ;
}
}
if ( name === 'is' ) {
component . warn ( attribute , {
code : 'avoid-is' ,
message : 'The \'is\' attribute is not supported cross-browser and should be avoided'
} ) ;
}
if ( react_attributes . has ( attribute . name ) ) {
component . warn ( attribute , {
code : 'invalid-html-attribute' ,
message : ` ' ${ attribute . name } ' is not a valid HTML attribute. Did you mean ' ${ react_attributes . get ( attribute . name ) } '? `
} ) ;
}
attribute_map . set ( attribute . name , attribute ) ;
} ) ;
}
validate_special_cases() {
const { component , attributes , handlers } = this ;
const attribute_map = new Map ( ) ;
const handlers_map = new Map ( ) ;
@ -576,6 +548,63 @@ export default class Element extends Node {
} ) ;
}
}
if ( a11y_distracting_elements . has ( this . name ) ) {
// no-distracting-elements
component . warn ( this , {
code : 'a11y-distracting-elements' ,
message : ` A11y: Avoid < ${ this . name } > elements `
} ) ;
}
if ( this . name === 'figcaption' ) {
let { parent } = this ;
let is_figure_parent = false ;
while ( parent ) {
if ( ( parent as Element ) . name === 'figure' ) {
is_figure_parent = true ;
break ;
}
if ( parent . type === 'Element' ) {
break ;
}
parent = parent . parent ;
}
if ( ! is_figure_parent ) {
component . warn ( this , {
code : 'a11y-structure' ,
message : 'A11y: <figcaption> must be an immediate child of <figure>'
} ) ;
}
}
if ( this . name === 'figure' ) {
const children = this . children . filter ( node = > {
if ( node . type === 'Comment' ) return false ;
if ( node . type === 'Text' ) return /\S/ . test ( node . data ) ;
return true ;
} ) ;
const index = children . findIndex ( child = > ( child as Element ) . name === 'figcaption' ) ;
if ( index !== - 1 && ( index !== 0 && index !== children . length - 1 ) ) {
component . warn ( children [ index ] , {
code : 'a11y-structure' ,
message : 'A11y: <figcaption> must be first or last child of <figure>'
} ) ;
}
}
}
validate_bindings_foreign() {
this . bindings . forEach ( binding = > {
this . component . error ( binding , {
code : 'invalid-binding' ,
message : ` ' ${ binding . name } ' is not a valid binding. Foreign elements only support bind:this `
} ) ;
} ) ;
}
validate_bindings() {