@@ -136,44 +136,45 @@ export default class Element extends Node {
136136
137137 this . namespace = get_namespace ( parent as Element , this , component . namespace ) ;
138138
139- if ( this . name === 'textarea' ) {
140- if ( info . children . length > 0 ) {
141- const value_attribute = info . attributes . find ( node => node . name === 'value' ) ;
142- if ( value_attribute ) {
143- component . error ( value_attribute , {
144- code : 'textarea-duplicate-value' ,
145- message : 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
146- } ) ;
147- }
139+ if ( this . namespace !== namespaces . foreign ) {
140+ if ( this . name === 'textarea' ) {
141+ if ( info . children . length > 0 ) {
142+ const value_attribute = info . attributes . find ( node => node . name === 'value' ) ;
143+ if ( value_attribute ) {
144+ component . error ( value_attribute , {
145+ code : 'textarea-duplicate-value' ,
146+ message : 'A <textarea> can have either a value attribute or (equivalently) child content, but not both'
147+ } ) ;
148+ }
148149
149- // this is an egregious hack, but it's the easiest way to get <textarea>
150- // children treated the same way as a value attribute
151- info . attributes . push ( {
152- type : 'Attribute' ,
153- name : 'value' ,
154- value : info . children
155- } ) ;
150+ // this is an egregious hack, but it's the easiest way to get <textarea>
151+ // children treated the same way as a value attribute
152+ info . attributes . push ( {
153+ type : 'Attribute' ,
154+ name : 'value' ,
155+ value : info . children
156+ } ) ;
156157
157- info . children = [ ] ;
158+ info . children = [ ] ;
159+ }
158160 }
159- }
160161
161- if ( this . name === 'option' ) {
162- // Special case — treat these the same way:
163- // <option>{foo}</option>
164- // <option value={foo}>{foo}</option>
165- const value_attribute = info . attributes . find ( attribute => attribute . name === 'value' ) ;
162+ if ( this . name === 'option' ) {
163+ // Special case — treat these the same way:
164+ // <option>{foo}</option>
165+ // <option value={foo}>{foo}</option>
166+ const value_attribute = info . attributes . find ( attribute => attribute . name === 'value' ) ;
166167
167- if ( ! value_attribute ) {
168- info . attributes . push ( {
169- type : 'Attribute' ,
170- name : 'value' ,
171- value : info . children ,
172- synthetic : true
173- } ) ;
168+ if ( ! value_attribute ) {
169+ info . attributes . push ( {
170+ type : 'Attribute' ,
171+ name : 'value' ,
172+ value : info . children ,
173+ synthetic : true
174+ } ) ;
175+ }
174176 }
175177 }
176-
177178 const has_let = info . attributes . some ( node => node . type === 'Let' ) ;
178179 if ( has_let ) {
179180 scope = scope . child ( ) ;
@@ -253,65 +254,83 @@ export default class Element extends Node {
253254 } ) ;
254255 }
255256
256- if ( a11y_distracting_elements . has ( this . name ) ) {
257- // no-distracting-elements
258- this . component . warn ( this , {
259- code : 'a11y-distracting-elements' ,
260- message : `A11y: Avoid <${ this . name } > elements`
261- } ) ;
257+ this . validate_attributes ( ) ;
258+ this . validate_event_handlers ( ) ;
259+ if ( this . namespace === namespaces . foreign ) {
260+ this . validate_bindings_foreign ( ) ;
261+ } else {
262+ this . validate_attributes_a11y ( ) ;
263+ this . validate_special_cases ( ) ;
264+ this . validate_bindings ( ) ;
265+ this . validate_content ( ) ;
262266 }
263267
264- if ( this . name === 'figcaption' ) {
265- let { parent } = this ;
266- let is_figure_parent = false ;
268+ }
267269
268- while ( parent ) {
269- if ( ( parent as Element ) . name === 'figure' ) {
270- is_figure_parent = true ;
271- break ;
272- }
273- if ( parent . type === 'Element' ) {
274- break ;
275- }
276- parent = parent . parent ;
277- }
270+ validate_attributes ( ) {
271+ const { component, parent } = this ;
278272
279- if ( ! is_figure_parent ) {
280- this . component . warn ( this , {
281- code : 'a11y-structure' ,
282- message : 'A11y: <figcaption> must be an immediate child of <figure>'
273+ this . attributes . forEach ( attribute => {
274+ if ( attribute . is_spread ) return ;
275+
276+ const name = attribute . name . toLowerCase ( ) ;
277+
278+ // Errors
279+
280+ if ( / ( ^ [ 0 - 9 - .] ) | [ \^ $ @ % & # ? ! | ( ) [ \] { } ^ * + ~ ; ] / . test ( name ) ) {
281+ component . error ( attribute , {
282+ code : 'illegal-attribute' ,
283+ message : `'${ name } ' is not a valid attribute name`
283284 } ) ;
284285 }
285- }
286286
287- if ( this . name === 'figure' ) {
288- const children = this . children . filter ( node => {
289- if ( node . type === 'Comment' ) return false ;
290- if ( node . type === 'Text' ) return / \S / . test ( node . data ) ;
291- return true ;
292- } ) ;
287+ if ( name === 'slot' ) {
288+ if ( ! attribute . is_static ) {
289+ component . error ( attribute , {
290+ code : 'invalid-slot-attribute' ,
291+ message : 'slot attribute cannot have a dynamic value'
292+ } ) ;
293+ }
293294
294- const index = children . findIndex ( child => ( child as Element ) . name === 'figcaption' ) ;
295+ if ( component . slot_outlets . has ( name ) ) {
296+ component . error ( attribute , {
297+ code : 'duplicate-slot-attribute' ,
298+ message : `Duplicate '${ name } ' slot`
299+ } ) ;
295300
296- if ( index !== - 1 && ( index !== 0 && index !== children . length - 1 ) ) {
297- this . component . warn ( children [ index ] , {
298- code : 'a11y-structure' ,
299- message : 'A11y: <figcaption> must be first or last child of <figure>'
300- } ) ;
301+ component . slot_outlets . add ( name ) ;
302+ }
303+
304+ if ( ! ( parent . type === 'InlineComponent' || within_custom_element ( parent ) ) ) {
305+ component . error ( attribute , {
306+ code : 'invalid-slotted-content' ,
307+ message : 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
308+ } ) ;
309+ }
301310 }
302- }
303311
304- this . validate_attributes ( ) ;
305- this . validate_special_cases ( ) ;
306- this . validate_bindings ( ) ;
307- this . validate_content ( ) ;
308- this . validate_event_handlers ( ) ;
309- }
312+ // Warnings
310313
311- validate_attributes ( ) {
312- const { component, parent } = this ;
314+ if ( this . namespace !== namespaces . foreign ) {
315+ if ( name === 'is' ) {
316+ component . warn ( attribute , {
317+ code : 'avoid-is' ,
318+ message : 'The \'is\' attribute is not supported cross-browser and should be avoided'
319+ } ) ;
320+ }
313321
314- const attribute_map = new Map ( ) ;
322+ if ( react_attributes . has ( attribute . name ) ) {
323+ component . warn ( attribute , {
324+ code : 'invalid-html-attribute' ,
325+ message : `'${ attribute . name } ' is not a valid HTML attribute. Did you mean '${ react_attributes . get ( attribute . name ) } '?`
326+ } ) ;
327+ }
328+ }
329+ } ) ;
330+ }
331+
332+ validate_attributes_a11y ( ) {
333+ const { component } = this ;
315334
316335 this . attributes . forEach ( attribute => {
317336 if ( attribute . is_spread ) return ;
@@ -408,60 +427,13 @@ export default class Element extends Node {
408427 } ) ;
409428 }
410429 }
411-
412-
413- if ( / ( ^ [ 0 - 9 - .] ) | [ \^ $ @ % & # ? ! | ( ) [ \] { } ^ * + ~ ; ] / . test ( name ) ) {
414- component . error ( attribute , {
415- code : 'illegal-attribute' ,
416- message : `'${ name } ' is not a valid attribute name`
417- } ) ;
418- }
419-
420- if ( name === 'slot' ) {
421- if ( ! attribute . is_static ) {
422- component . error ( attribute , {
423- code : 'invalid-slot-attribute' ,
424- message : 'slot attribute cannot have a dynamic value'
425- } ) ;
426- }
427-
428- if ( component . slot_outlets . has ( name ) ) {
429- component . error ( attribute , {
430- code : 'duplicate-slot-attribute' ,
431- message : `Duplicate '${ name } ' slot`
432- } ) ;
433-
434- component . slot_outlets . add ( name ) ;
435- }
436-
437- if ( ! ( parent . type === 'InlineComponent' || within_custom_element ( parent ) ) ) {
438- component . error ( attribute , {
439- code : 'invalid-slotted-content' ,
440- message : 'Element with a slot=\'...\' attribute must be a child of a component or a descendant of a custom element'
441- } ) ;
442- }
443- }
444-
445- if ( name === 'is' ) {
446- component . warn ( attribute , {
447- code : 'avoid-is' ,
448- message : 'The \'is\' attribute is not supported cross-browser and should be avoided'
449- } ) ;
450- }
451-
452- if ( react_attributes . has ( attribute . name ) ) {
453- component . warn ( attribute , {
454- code : 'invalid-html-attribute' ,
455- message : `'${ attribute . name } ' is not a valid HTML attribute. Did you mean '${ react_attributes . get ( attribute . name ) } '?`
456- } ) ;
457- }
458-
459- attribute_map . set ( attribute . name , attribute ) ;
460430 } ) ;
461431 }
462432
433+
463434 validate_special_cases ( ) {
464435 const { component, attributes, handlers } = this ;
436+
465437 const attribute_map = new Map ( ) ;
466438 const handlers_map = new Map ( ) ;
467439
@@ -576,6 +548,63 @@ export default class Element extends Node {
576548 } ) ;
577549 }
578550 }
551+
552+ if ( a11y_distracting_elements . has ( this . name ) ) {
553+ // no-distracting-elements
554+ component . warn ( this , {
555+ code : 'a11y-distracting-elements' ,
556+ message : `A11y: Avoid <${ this . name } > elements`
557+ } ) ;
558+ }
559+
560+ if ( this . name === 'figcaption' ) {
561+ let { parent } = this ;
562+ let is_figure_parent = false ;
563+
564+ while ( parent ) {
565+ if ( ( parent as Element ) . name === 'figure' ) {
566+ is_figure_parent = true ;
567+ break ;
568+ }
569+ if ( parent . type === 'Element' ) {
570+ break ;
571+ }
572+ parent = parent . parent ;
573+ }
574+
575+ if ( ! is_figure_parent ) {
576+ component . warn ( this , {
577+ code : 'a11y-structure' ,
578+ message : 'A11y: <figcaption> must be an immediate child of <figure>'
579+ } ) ;
580+ }
581+ }
582+
583+ if ( this . name === 'figure' ) {
584+ const children = this . children . filter ( node => {
585+ if ( node . type === 'Comment' ) return false ;
586+ if ( node . type === 'Text' ) return / \S / . test ( node . data ) ;
587+ return true ;
588+ } ) ;
589+
590+ const index = children . findIndex ( child => ( child as Element ) . name === 'figcaption' ) ;
591+
592+ if ( index !== - 1 && ( index !== 0 && index !== children . length - 1 ) ) {
593+ component . warn ( children [ index ] , {
594+ code : 'a11y-structure' ,
595+ message : 'A11y: <figcaption> must be first or last child of <figure>'
596+ } ) ;
597+ }
598+ }
599+ }
600+
601+ validate_bindings_foreign ( ) {
602+ this . bindings . forEach ( binding => {
603+ this . component . error ( binding , {
604+ code : 'invalid-binding' ,
605+ message : `'${ binding . name } ' is not a valid binding. Foreign elements only support bind:this`
606+ } ) ;
607+ } ) ;
579608 }
580609
581610 validate_bindings ( ) {
0 commit comments