@@ -191,15 +191,24 @@ export default function tag(parser) {
191191 } ;
192192 }
193193
194- /** @type {Set< string> } */
195- const unique_names = new Set ( ) ;
194+ /** @type {string[] } */
195+ const unique_names = [ ] ;
196196
197197 const current = parser . current ( ) ;
198198 const is_top_level_script_or_style =
199199 ( name === 'script' || name === 'style' ) && current . type === 'Root' ;
200200
201+ const read = is_top_level_script_or_style ? read_static_attribute : read_attribute ;
202+
201203 let attribute ;
202- while ( ( attribute = read_attribute ( parser , unique_names , is_top_level_script_or_style ) ) ) {
204+ while ( ( attribute = read ( parser ) ) ) {
205+ if (
206+ ( attribute . type === 'Attribute' || attribute . type === 'BindDirective' ) &&
207+ unique_names . includes ( attribute . name )
208+ ) {
209+ error ( attribute . start , 'duplicate-attribute' ) ;
210+ }
211+
203212 element . attributes . push ( attribute ) ;
204213 parser . allow_whitespace ( ) ;
205214 }
@@ -376,22 +385,58 @@ const regex_starts_with_quote_characters = /^["']/;
376385
377386/**
378387 * @param {import('../index.js').Parser } parser
379- * @param {Set<string> } unique_names
380- * @param {boolean } is_static If `true`, `{` and `}` are not treated as delimiters for expressions
381- * @returns {any }
388+ * @returns {import('#compiler').Attribute | null }
382389 */
383- function read_attribute ( parser , unique_names , is_static ) {
390+ function read_static_attribute ( parser ) {
384391 const start = parser . index ;
385392
386- /** @param {string } name */
387- function check_unique ( name ) {
388- if ( unique_names . has ( name ) ) {
389- error ( start , 'duplicate-attribute' ) ;
393+ const name = parser . read_until ( regex_token_ending_character ) ;
394+ if ( ! name ) return null ;
395+
396+ /** @type {true | Array<import('#compiler').Text | import('#compiler').ExpressionTag> } */
397+ let value = true ;
398+
399+ if ( parser . eat ( '=' ) ) {
400+ parser . allow_whitespace ( ) ;
401+ let raw = parser . match_regex ( regex_attribute_value ) ;
402+ if ( ! raw ) {
403+ error ( parser . index , 'missing-attribute-value' ) ;
404+ }
405+
406+ parser . index += raw . length ;
407+
408+ const quoted = raw [ 0 ] === '"' || raw [ 0 ] === "'" ;
409+ if ( quoted ) {
410+ raw = raw . slice ( 1 , - 1 ) ;
390411 }
391- unique_names . add ( name ) ;
412+
413+ value = [
414+ {
415+ start : parser . index - raw . length - ( quoted ? 1 : 0 ) ,
416+ end : quoted ? parser . index - 1 : parser . index ,
417+ type : 'Text' ,
418+ raw : raw ,
419+ data : decode_character_references ( raw , true ) ,
420+ parent : null
421+ }
422+ ] ;
392423 }
393424
394- if ( ! is_static && parser . eat ( '{' ) ) {
425+ if ( parser . match_regex ( regex_starts_with_quote_characters ) ) {
426+ error ( parser . index , 'expected-token' , '=' ) ;
427+ }
428+
429+ return create_attribute ( name , start , parser . index , value ) ;
430+ }
431+
432+ /**
433+ * @param {import('../index.js').Parser } parser
434+ * @returns {import('#compiler').Attribute | import('#compiler').SpreadAttribute | import('#compiler').Directive | null }
435+ */
436+ function read_attribute ( parser ) {
437+ const start = parser . index ;
438+
439+ if ( parser . eat ( '{' ) ) {
395440 parser . allow_whitespace ( ) ;
396441
397442 if ( parser . eat ( '...' ) ) {
@@ -421,8 +466,6 @@ function read_attribute(parser, unique_names, is_static) {
421466 error ( start , 'empty-attribute-shorthand' ) ;
422467 }
423468
424- check_unique ( name ) ;
425-
426469 parser . allow_whitespace ( ) ;
427470 parser . eat ( '}' , true ) ;
428471
@@ -462,25 +505,19 @@ function read_attribute(parser, unique_names, is_static) {
462505 let value = true ;
463506 if ( parser . eat ( '=' ) ) {
464507 parser . allow_whitespace ( ) ;
465- value = read_attribute_value ( parser , is_static ) ;
508+ value = read_attribute_value ( parser ) ;
466509 end = parser . index ;
467510 } else if ( parser . match_regex ( regex_starts_with_quote_characters ) ) {
468511 error ( parser . index , 'expected-token' , '=' ) ;
469512 }
470513
471- if ( ! is_static && type ) {
514+ if ( type ) {
472515 const [ directive_name , ...modifiers ] = name . slice ( colon_index + 1 ) . split ( '|' ) ;
473516
474517 if ( directive_name === '' ) {
475518 error ( start + colon_index + 1 , 'empty-directive-name' , type ) ;
476519 }
477520
478- if ( type === 'BindDirective' && directive_name !== 'this' ) {
479- check_unique ( directive_name ) ;
480- } else if ( type !== 'OnDirective' && type !== 'UseDirective' ) {
481- check_unique ( name ) ;
482- }
483-
484521 if ( type === 'StyleDirective' ) {
485522 return {
486523 start,
@@ -548,8 +585,6 @@ function read_attribute(parser, unique_names, is_static) {
548585 return directive ;
549586 }
550587
551- check_unique ( name ) ;
552-
553588 return create_attribute ( name , start , end , value ) ;
554589}
555590
@@ -573,33 +608,8 @@ const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]))/;
573608
574609/**
575610 * @param {import('../index.js').Parser } parser
576- * @param {boolean } is_static If `true`, `{` and `}` are not treated as delimiters for expressions
577611 */
578- function read_attribute_value ( parser , is_static ) {
579- if ( is_static ) {
580- let value = parser . match_regex ( regex_attribute_value ) ;
581- if ( ! value ) {
582- error ( parser . index , 'missing-attribute-value' ) ;
583- }
584-
585- parser . index += value . length ;
586-
587- const quoted = value [ 0 ] === '"' || value [ 0 ] === "'" ;
588- if ( quoted ) {
589- value = value . slice ( 1 , - 1 ) ;
590- }
591-
592- return [
593- {
594- start : parser . index - value . length - ( quoted ? 1 : 0 ) ,
595- end : quoted ? parser . index - 1 : parser . index ,
596- type : 'Text' ,
597- raw : value ,
598- data : decode_character_references ( value , true )
599- }
600- ] ;
601- }
602-
612+ function read_attribute_value ( parser ) {
603613 const quote_mark = parser . eat ( "'" ) ? "'" : parser . eat ( '"' ) ? '"' : null ;
604614 if ( quote_mark && parser . eat ( quote_mark ) ) {
605615 return [
0 commit comments