@@ -236,7 +236,7 @@ function expression(allowMarkup: boolean): Model.Expression | Model.Markup {
236236 pos += 1 ; // ':'
237237 annotation = { type : 'function' , name : identifier ( ) } ;
238238 const options_ = options ( ) ;
239- if ( options_ . length ) annotation . options = options_ ;
239+ if ( options_ ) annotation . options = options_ ;
240240 break ;
241241 }
242242 case '#' :
@@ -246,7 +246,7 @@ function expression(allowMarkup: boolean): Model.Expression | Model.Markup {
246246 const kind = sigil === '#' ? 'open' : 'close' ;
247247 markup = { type : 'markup' , kind, name : identifier ( ) } ;
248248 const options_ = options ( ) ;
249- if ( options_ . length ) markup . options = options_ ;
249+ if ( options_ ) markup . options = options_ ;
250250 break ;
251251 }
252252 case '^' :
@@ -267,50 +267,75 @@ function expression(allowMarkup: boolean): Model.Expression | Model.Markup {
267267 throw SyntaxError ( 'parse-error' , pos ) ;
268268 }
269269
270- while ( source [ pos ] === '@' ) attribute ( ) ;
270+ const attributes_ = attributes ( ) ;
271271 if ( markup ?. kind === 'open' && source [ pos ] === '/' ) {
272272 markup . kind = 'standalone' ;
273273 pos += 1 ; // '/'
274274 }
275275 expect ( '}' , true ) ;
276276
277277 if ( annotation ) {
278- return arg
278+ const exp : Model . Expression = arg
279279 ? { type : 'expression' , arg, annotation }
280280 : { type : 'expression' , annotation } ;
281+ if ( attributes_ ) exp . attributes = attributes_ ;
282+ return exp ;
283+ }
284+ if ( markup ) {
285+ if ( attributes_ ) markup . attributes = attributes_ ;
286+ return markup ;
281287 }
282- if ( markup ) return markup ;
283288 if ( ! arg ) throw SyntaxError ( 'empty-token' , start , pos ) ;
284- return { type : 'expression' , arg } ;
289+ return attributes_
290+ ? { type : 'expression' , arg, attributes : attributes_ }
291+ : { type : 'expression' , arg } ;
285292}
286293
287294/** Requires and consumes leading and trailing whitespace. */
288295function options ( ) {
289296 ws ( '/}' ) ;
290- const options : Model . Option [ ] = [ ] ;
297+ const options : Model . Options = new Map ( ) ;
298+ let isEmpty = true ;
291299 while ( pos < source . length ) {
292300 const next = source [ pos ] ;
293301 if ( next === '@' || next === '/' || next === '}' ) break ;
302+ const start = pos ;
294303 const name_ = identifier ( ) ;
304+ if ( options . has ( name_ ) ) {
305+ throw SyntaxError ( 'duplicate-option-name' , start , pos ) ;
306+ }
295307 ws ( ) ;
296308 expect ( '=' , true ) ;
297309 ws ( ) ;
298- options . push ( { name : name_ , value : value ( true ) } ) ;
310+ options . set ( name_ , value ( true ) ) ;
311+ isEmpty = false ;
299312 ws ( '/}' ) ;
300313 }
301- return options ;
314+ return isEmpty ? null : options ;
302315}
303316
304- function attribute ( ) {
305- pos += 1 ; // '@'
306- identifier ( ) ; // name
307- ws ( '=/}' ) ;
308- if ( source [ pos ] === '=' ) {
309- pos += 1 ; // '='
310- ws ( ) ;
311- value ( true ) ; // value
312- ws ( '/}' ) ;
317+ function attributes ( ) {
318+ const attributes : Model . Attributes = new Map ( ) ;
319+ let isEmpty = true ;
320+ while ( source [ pos ] === '@' ) {
321+ const start = pos ;
322+ pos += 1 ; // '@'
323+ const name_ = identifier ( ) ;
324+ if ( attributes . has ( name_ ) ) {
325+ throw SyntaxError ( 'duplicate-attribute' , start , pos ) ;
326+ }
327+ ws ( '=/}' ) ;
328+ if ( source [ pos ] === '=' ) {
329+ pos += 1 ; // '='
330+ ws ( ) ;
331+ attributes . set ( name_ , literal ( true ) ) ;
332+ ws ( '/}' ) ;
333+ } else {
334+ attributes . set ( name_ , true ) ;
335+ }
336+ isEmpty = false ;
313337 }
338+ return isEmpty ? null : attributes ;
314339}
315340
316341// eslint-disable-next-line @typescript-eslint/no-explicit-any
0 commit comments