github.com/evanw/esbuild@v0.21.4/internal/css_parser/css_parser_selector.go (about) 1 package css_parser 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/evanw/esbuild/internal/ast" 8 "github.com/evanw/esbuild/internal/css_ast" 9 "github.com/evanw/esbuild/internal/css_lexer" 10 "github.com/evanw/esbuild/internal/logger" 11 ) 12 13 type parseSelectorOpts struct { 14 composesContext *composesContext 15 pseudoClassKind css_ast.PseudoClassKind 16 isDeclarationContext bool 17 stopOnCloseParen bool 18 onlyOneComplexSelector bool 19 noLeadingCombinator bool 20 } 21 22 func (p *parser) parseSelectorList(opts parseSelectorOpts) (list []css_ast.ComplexSelector, ok bool) { 23 // Parse the first selector 24 sel, good := p.parseComplexSelector(parseComplexSelectorOpts{ 25 parseSelectorOpts: opts, 26 isFirst: true, 27 }) 28 if !good { 29 return 30 } 31 list = p.flattenLocalAndGlobalSelectors(list, sel) 32 33 // Parse the remaining selectors 34 if opts.onlyOneComplexSelector { 35 if t := p.current(); t.Kind == css_lexer.TComma { 36 p.prevError = t.Range.Loc 37 kind := fmt.Sprintf(":%s(...)", opts.pseudoClassKind.String()) 38 p.log.AddIDWithNotes(logger.MsgID_CSS_CSSSyntaxError, logger.Warning, &p.tracker, t.Range, 39 fmt.Sprintf("Unexpected \",\" inside %q", kind), 40 []logger.MsgData{{Text: fmt.Sprintf("Different CSS tools behave differently in this case, so esbuild doesn't allow it. "+ 41 "Either remove this comma or split this selector up into multiple comma-separated %q selectors instead.", kind)}}) 42 return 43 } 44 } else { 45 skip: 46 for { 47 p.eat(css_lexer.TWhitespace) 48 if !p.eat(css_lexer.TComma) { 49 break 50 } 51 p.eat(css_lexer.TWhitespace) 52 sel, good := p.parseComplexSelector(parseComplexSelectorOpts{ 53 parseSelectorOpts: opts, 54 }) 55 if !good { 56 return 57 } 58 59 // Omit duplicate selectors 60 if p.options.minifySyntax { 61 for _, existing := range list { 62 if sel.Equal(existing, nil) { 63 continue skip 64 } 65 } 66 } 67 68 list = p.flattenLocalAndGlobalSelectors(list, sel) 69 } 70 } 71 72 if p.options.minifySyntax { 73 for i := 1; i < len(list); i++ { 74 if analyzeLeadingAmpersand(list[i], opts.isDeclarationContext) != cannotRemoveLeadingAmpersand { 75 list[i].Selectors = list[i].Selectors[1:] 76 } 77 } 78 79 switch analyzeLeadingAmpersand(list[0], opts.isDeclarationContext) { 80 case canAlwaysRemoveLeadingAmpersand: 81 list[0].Selectors = list[0].Selectors[1:] 82 83 case canRemoveLeadingAmpersandIfNotFirst: 84 for i := 1; i < len(list); i++ { 85 if sel := list[i].Selectors[0]; !sel.HasNestingSelector() && (sel.Combinator.Byte != 0 || sel.TypeSelector == nil) { 86 list[0].Selectors = list[0].Selectors[1:] 87 list[0], list[i] = list[i], list[0] 88 break 89 } 90 } 91 } 92 } 93 94 ok = true 95 return 96 } 97 98 func mergeCompoundSelectors(target *css_ast.CompoundSelector, source css_ast.CompoundSelector) { 99 // ".foo:local(&)" => "&.foo" 100 if source.HasNestingSelector() && !target.HasNestingSelector() { 101 target.NestingSelectorLoc = source.NestingSelectorLoc 102 } 103 104 if source.TypeSelector != nil { 105 if target.TypeSelector == nil { 106 // ".foo:local(div)" => "div.foo" 107 target.TypeSelector = source.TypeSelector 108 } else { 109 // "div:local(span)" => "div:is(span)" 110 // 111 // Note: All other implementations of this (Lightning CSS, PostCSS, and 112 // Webpack) do something really weird here. They do this instead: 113 // 114 // "div:local(span)" => "divspan" 115 // 116 // But that just seems so obviously wrong that I'm not going to do that. 117 target.SubclassSelectors = append(target.SubclassSelectors, css_ast.SubclassSelector{ 118 Range: source.TypeSelector.Range(), 119 Data: &css_ast.SSPseudoClassWithSelectorList{ 120 Kind: css_ast.PseudoClassIs, 121 Selectors: []css_ast.ComplexSelector{{Selectors: []css_ast.CompoundSelector{{TypeSelector: source.TypeSelector}}}}, 122 }, 123 }) 124 } 125 } 126 127 // ".foo:local(.bar)" => ".foo.bar" 128 target.SubclassSelectors = append(target.SubclassSelectors, source.SubclassSelectors...) 129 } 130 131 func containsLocalOrGlobalSelector(sel css_ast.ComplexSelector) bool { 132 for _, s := range sel.Selectors { 133 for _, ss := range s.SubclassSelectors { 134 switch pseudo := ss.Data.(type) { 135 case *css_ast.SSPseudoClass: 136 if pseudo.Name == "global" || pseudo.Name == "local" { 137 return true 138 } 139 140 case *css_ast.SSPseudoClassWithSelectorList: 141 if pseudo.Kind == css_ast.PseudoClassGlobal || pseudo.Kind == css_ast.PseudoClassLocal { 142 return true 143 } 144 } 145 } 146 } 147 return false 148 } 149 150 // This handles the ":local()" and ":global()" annotations from CSS modules 151 func (p *parser) flattenLocalAndGlobalSelectors(list []css_ast.ComplexSelector, sel css_ast.ComplexSelector) []css_ast.ComplexSelector { 152 // Only do the work to flatten the whole list if there's a ":local" or a ":global" 153 if p.options.symbolMode != symbolModeDisabled && containsLocalOrGlobalSelector(sel) { 154 var selectors []css_ast.CompoundSelector 155 156 for _, s := range sel.Selectors { 157 oldSubclassSelectors := s.SubclassSelectors 158 s.SubclassSelectors = make([]css_ast.SubclassSelector, 0, len(oldSubclassSelectors)) 159 160 for _, ss := range oldSubclassSelectors { 161 switch pseudo := ss.Data.(type) { 162 case *css_ast.SSPseudoClass: 163 if pseudo.Name == "global" || pseudo.Name == "local" { 164 // Remove bare ":global" and ":local" pseudo-classes 165 continue 166 } 167 168 case *css_ast.SSPseudoClassWithSelectorList: 169 if pseudo.Kind == css_ast.PseudoClassGlobal || pseudo.Kind == css_ast.PseudoClassLocal { 170 inner := pseudo.Selectors[0].Selectors 171 172 // Replace this pseudo-class with all inner compound selectors. 173 // The first inner compound selector is merged with the compound 174 // selector before it and the last inner compound selector is 175 // merged with the compound selector after it: 176 // 177 // "div:local(.a .b):hover" => "div.a b:hover" 178 // 179 // This behavior is really strange since this is not how anything 180 // involving pseudo-classes in real CSS works at all. However, all 181 // other implementations (Lightning CSS, PostCSS, and Webpack) are 182 // consistent with this strange behavior, so we do it too. 183 if inner[0].Combinator.Byte == 0 { 184 mergeCompoundSelectors(&s, inner[0]) 185 inner = inner[1:] 186 } else { 187 // "div:local(+ .foo):hover" => "div + .foo:hover" 188 } 189 if n := len(inner); n > 0 { 190 if !s.IsInvalidBecauseEmpty() { 191 // Don't add this selector if it consisted only of a bare ":global" or ":local" 192 selectors = append(selectors, s) 193 } 194 selectors = append(selectors, inner[:n-1]...) 195 s = inner[n-1] 196 } 197 continue 198 } 199 } 200 201 s.SubclassSelectors = append(s.SubclassSelectors, ss) 202 } 203 204 if !s.IsInvalidBecauseEmpty() { 205 // Don't add this selector if it consisted only of a bare ":global" or ":local" 206 selectors = append(selectors, s) 207 } 208 } 209 210 if len(selectors) == 0 { 211 // Treat a bare ":global" or ":local" as a bare "&" nesting selector 212 selectors = append(selectors, css_ast.CompoundSelector{ 213 NestingSelectorLoc: ast.MakeIndex32(uint32(sel.Selectors[0].Range().Loc.Start)), 214 WasEmptyFromLocalOrGlobal: true, 215 }) 216 217 // Make sure we report that nesting is present so that it can be lowered 218 p.nestingIsPresent = true 219 } 220 221 sel.Selectors = selectors 222 } 223 224 return append(list, sel) 225 } 226 227 type leadingAmpersand uint8 228 229 const ( 230 cannotRemoveLeadingAmpersand leadingAmpersand = iota 231 canAlwaysRemoveLeadingAmpersand 232 canRemoveLeadingAmpersandIfNotFirst 233 ) 234 235 func analyzeLeadingAmpersand(sel css_ast.ComplexSelector, isDeclarationContext bool) leadingAmpersand { 236 if len(sel.Selectors) > 1 { 237 if first := sel.Selectors[0]; first.IsSingleAmpersand() { 238 if second := sel.Selectors[1]; second.Combinator.Byte == 0 && second.HasNestingSelector() { 239 // ".foo { & &.bar {} }" => ".foo { & &.bar {} }" 240 } else if second.Combinator.Byte != 0 || second.TypeSelector == nil || !isDeclarationContext { 241 // "& + div {}" => "+ div {}" 242 // "& div {}" => "div {}" 243 // ".foo { & + div {} }" => ".foo { + div {} }" 244 // ".foo { & + &.bar {} }" => ".foo { + &.bar {} }" 245 // ".foo { & :hover {} }" => ".foo { :hover {} }" 246 return canAlwaysRemoveLeadingAmpersand 247 } else { 248 // ".foo { & div {} }" 249 // ".foo { .bar, & div {} }" => ".foo { .bar, div {} }" 250 return canRemoveLeadingAmpersandIfNotFirst 251 } 252 } 253 } else { 254 // "& {}" => "& {}" 255 } 256 return cannotRemoveLeadingAmpersand 257 } 258 259 type parseComplexSelectorOpts struct { 260 parseSelectorOpts 261 isFirst bool 262 } 263 264 func (p *parser) parseComplexSelector(opts parseComplexSelectorOpts) (result css_ast.ComplexSelector, ok bool) { 265 // This is an extension: https://drafts.csswg.org/css-nesting-1/ 266 var combinator css_ast.Combinator 267 if !opts.noLeadingCombinator { 268 combinator = p.parseCombinator() 269 if combinator.Byte != 0 { 270 p.nestingIsPresent = true 271 p.eat(css_lexer.TWhitespace) 272 } 273 } 274 275 // Parent 276 sel, good := p.parseCompoundSelector(parseComplexSelectorOpts{ 277 parseSelectorOpts: opts.parseSelectorOpts, 278 isFirst: opts.isFirst, 279 }) 280 if !good { 281 return 282 } 283 sel.Combinator = combinator 284 result.Selectors = append(result.Selectors, sel) 285 286 stop := css_lexer.TOpenBrace 287 if opts.stopOnCloseParen { 288 stop = css_lexer.TCloseParen 289 } 290 for { 291 p.eat(css_lexer.TWhitespace) 292 if p.peek(css_lexer.TEndOfFile) || p.peek(css_lexer.TComma) || p.peek(stop) { 293 break 294 } 295 296 // Optional combinator 297 combinator := p.parseCombinator() 298 if combinator.Byte != 0 { 299 p.eat(css_lexer.TWhitespace) 300 } 301 302 // Child 303 sel, good := p.parseCompoundSelector(parseComplexSelectorOpts{ 304 parseSelectorOpts: opts.parseSelectorOpts, 305 }) 306 if !good { 307 return 308 } 309 sel.Combinator = combinator 310 result.Selectors = append(result.Selectors, sel) 311 } 312 313 ok = true 314 return 315 } 316 317 func (p *parser) nameToken() css_ast.NameToken { 318 t := p.current() 319 return css_ast.NameToken{ 320 Kind: t.Kind, 321 Range: t.Range, 322 Text: p.decoded(), 323 } 324 } 325 326 func (p *parser) parseCompoundSelector(opts parseComplexSelectorOpts) (sel css_ast.CompoundSelector, ok bool) { 327 startLoc := p.current().Range.Loc 328 329 // This is an extension: https://drafts.csswg.org/css-nesting-1/ 330 hasLeadingNestingSelector := p.peek(css_lexer.TDelimAmpersand) 331 if hasLeadingNestingSelector { 332 p.nestingIsPresent = true 333 sel.NestingSelectorLoc = ast.MakeIndex32(uint32(startLoc.Start)) 334 p.advance() 335 } 336 337 // Parse the type selector 338 typeSelectorLoc := p.current().Range.Loc 339 switch p.current().Kind { 340 case css_lexer.TDelimBar, css_lexer.TIdent, css_lexer.TDelimAsterisk: 341 nsName := css_ast.NamespacedName{} 342 if !p.peek(css_lexer.TDelimBar) { 343 nsName.Name = p.nameToken() 344 p.advance() 345 } else { 346 // Hack: Create an empty "identifier" to represent this 347 nsName.Name.Kind = css_lexer.TIdent 348 } 349 if p.eat(css_lexer.TDelimBar) { 350 if !p.peek(css_lexer.TIdent) && !p.peek(css_lexer.TDelimAsterisk) { 351 p.expect(css_lexer.TIdent) 352 return 353 } 354 prefix := nsName.Name 355 nsName.NamespacePrefix = &prefix 356 nsName.Name = p.nameToken() 357 p.advance() 358 } 359 sel.TypeSelector = &nsName 360 } 361 362 // Parse the subclass selectors 363 subclassSelectors: 364 for { 365 subclassToken := p.current() 366 367 switch subclassToken.Kind { 368 case css_lexer.THash: 369 if (subclassToken.Flags & css_lexer.IsID) == 0 { 370 break subclassSelectors 371 } 372 nameLoc := logger.Loc{Start: subclassToken.Range.Loc.Start + 1} 373 name := p.decoded() 374 sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{ 375 Range: subclassToken.Range, 376 Data: &css_ast.SSHash{ 377 Name: p.symbolForName(nameLoc, name), 378 }, 379 }) 380 p.advance() 381 382 case css_lexer.TDelimDot: 383 p.advance() 384 nameRange := p.current().Range 385 name := p.decoded() 386 sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{ 387 Range: logger.Range{Loc: subclassToken.Range.Loc, Len: nameRange.End() - subclassToken.Range.Loc.Start}, 388 Data: &css_ast.SSClass{ 389 Name: p.symbolForName(nameRange.Loc, name), 390 }, 391 }) 392 if !p.expect(css_lexer.TIdent) { 393 return 394 } 395 396 case css_lexer.TOpenBracket: 397 attr, r := p.parseAttributeSelector() 398 if r.Len == 0 { 399 return 400 } 401 sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{ 402 Range: r, 403 Data: &attr, 404 }) 405 406 case css_lexer.TColon: 407 if p.next().Kind == css_lexer.TColon { 408 // Special-case the start of the pseudo-element selector section 409 for p.current().Kind == css_lexer.TColon { 410 firstColonLoc := p.current().Range.Loc 411 isElement := p.next().Kind == css_lexer.TColon 412 if isElement { 413 p.advance() 414 } 415 pseudo, r := p.parsePseudoClassSelector(firstColonLoc, isElement) 416 417 // https://www.w3.org/TR/selectors-4/#single-colon-pseudos 418 // The four Level 2 pseudo-elements (::before, ::after, ::first-line, 419 // and ::first-letter) may, for legacy reasons, be represented using 420 // the <pseudo-class-selector> grammar, with only a single ":" 421 // character at their start. 422 if p.options.minifySyntax && isElement { 423 if pseudo, ok := pseudo.(*css_ast.SSPseudoClass); ok && len(pseudo.Args) == 0 { 424 switch pseudo.Name { 425 case "before", "after", "first-line", "first-letter": 426 pseudo.IsElement = false 427 } 428 } 429 } 430 431 sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{ 432 Range: r, 433 Data: pseudo, 434 }) 435 } 436 break subclassSelectors 437 } 438 439 pseudo, r := p.parsePseudoClassSelector(subclassToken.Range.Loc, false) 440 sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{ 441 Range: r, 442 Data: pseudo, 443 }) 444 445 case css_lexer.TDelimAmpersand: 446 // This is an extension: https://drafts.csswg.org/css-nesting-1/ 447 p.nestingIsPresent = true 448 sel.NestingSelectorLoc = ast.MakeIndex32(uint32(subclassToken.Range.Loc.Start)) 449 p.advance() 450 451 default: 452 break subclassSelectors 453 } 454 } 455 456 // The compound selector must be non-empty 457 if sel.IsInvalidBecauseEmpty() { 458 p.unexpected() 459 return 460 } 461 462 // Note: "&div {}" was originally valid, but is now an invalid selector: 463 // https://github.com/w3c/csswg-drafts/issues/8662#issuecomment-1514977935. 464 // This is because SASS already uses that syntax to mean something very 465 // different, so that syntax has been removed to avoid mistakes. 466 if hasLeadingNestingSelector && sel.TypeSelector != nil { 467 r := logger.Range{Loc: typeSelectorLoc, Len: p.at(p.index-1).Range.End() - typeSelectorLoc.Start} 468 text := sel.TypeSelector.Name.Text 469 if sel.TypeSelector.NamespacePrefix != nil { 470 text = fmt.Sprintf("%s|%s", sel.TypeSelector.NamespacePrefix.Text, text) 471 } 472 var howToFix string 473 suggestion := p.source.TextForRange(r) 474 if opts.isFirst { 475 suggestion = fmt.Sprintf(":is(%s)", suggestion) 476 howToFix = "You can wrap this selector in \":is(...)\" as a workaround. " 477 } else { 478 r = logger.Range{Loc: startLoc, Len: r.End() - startLoc.Start} 479 suggestion += "&" 480 howToFix = "You can move the \"&\" to the end of this selector as a workaround. " 481 } 482 msg := logger.Msg{ 483 Kind: logger.Warning, 484 Data: p.tracker.MsgData(r, fmt.Sprintf("Cannot use type selector %q directly after nesting selector \"&\"", text)), 485 Notes: []logger.MsgData{{Text: "CSS nesting syntax does not allow the \"&\" selector to come before a type selector. " + 486 howToFix + 487 "This restriction exists to avoid problems with SASS nesting, where the same syntax means something very different " + 488 "that has no equivalent in real CSS (appending a suffix to the parent selector)."}}, 489 } 490 msg.Data.Location.Suggestion = suggestion 491 p.log.AddMsgID(logger.MsgID_CSS_CSSSyntaxError, msg) 492 return 493 } 494 495 // The type selector must always come first 496 switch p.current().Kind { 497 case css_lexer.TDelimBar, css_lexer.TIdent, css_lexer.TDelimAsterisk: 498 p.unexpected() 499 return 500 } 501 502 ok = true 503 return 504 } 505 506 func (p *parser) parseAttributeSelector() (attr css_ast.SSAttribute, r logger.Range) { 507 matchingLoc := p.current().Range.Loc 508 p.advance() 509 510 // Parse the namespaced name 511 switch p.current().Kind { 512 case css_lexer.TDelimBar, css_lexer.TDelimAsterisk: 513 // "[|x]" 514 // "[*|x]" 515 if p.peek(css_lexer.TDelimAsterisk) { 516 prefix := p.nameToken() 517 p.advance() 518 attr.NamespacedName.NamespacePrefix = &prefix 519 } else { 520 // "[|attr]" is equivalent to "[attr]". From the specification: 521 // "In keeping with the Namespaces in the XML recommendation, default 522 // namespaces do not apply to attributes, therefore attribute selectors 523 // without a namespace component apply only to attributes that have no 524 // namespace (equivalent to |attr)." 525 } 526 if !p.expect(css_lexer.TDelimBar) { 527 return 528 } 529 attr.NamespacedName.Name = p.nameToken() 530 if !p.expect(css_lexer.TIdent) { 531 return 532 } 533 534 default: 535 // "[x]" 536 // "[x|y]" 537 attr.NamespacedName.Name = p.nameToken() 538 if !p.expect(css_lexer.TIdent) { 539 return 540 } 541 if p.next().Kind != css_lexer.TDelimEquals && p.eat(css_lexer.TDelimBar) { 542 prefix := attr.NamespacedName.Name 543 attr.NamespacedName.NamespacePrefix = &prefix 544 attr.NamespacedName.Name = p.nameToken() 545 if !p.expect(css_lexer.TIdent) { 546 return 547 } 548 } 549 } 550 551 // Parse the optional matcher operator 552 p.eat(css_lexer.TWhitespace) 553 if p.eat(css_lexer.TDelimEquals) { 554 attr.MatcherOp = "=" 555 } else { 556 switch p.current().Kind { 557 case css_lexer.TDelimTilde: 558 attr.MatcherOp = "~=" 559 case css_lexer.TDelimBar: 560 attr.MatcherOp = "|=" 561 case css_lexer.TDelimCaret: 562 attr.MatcherOp = "^=" 563 case css_lexer.TDelimDollar: 564 attr.MatcherOp = "$=" 565 case css_lexer.TDelimAsterisk: 566 attr.MatcherOp = "*=" 567 } 568 if attr.MatcherOp != "" { 569 p.advance() 570 if !p.expect(css_lexer.TDelimEquals) { 571 return 572 } 573 } 574 } 575 576 // Parse the optional matcher value 577 if attr.MatcherOp != "" { 578 p.eat(css_lexer.TWhitespace) 579 if !p.peek(css_lexer.TString) && !p.peek(css_lexer.TIdent) { 580 p.unexpected() 581 } 582 attr.MatcherValue = p.decoded() 583 p.advance() 584 p.eat(css_lexer.TWhitespace) 585 if p.peek(css_lexer.TIdent) { 586 if modifier := p.decoded(); len(modifier) == 1 { 587 if c := modifier[0]; c == 'i' || c == 'I' || c == 's' || c == 'S' { 588 attr.MatcherModifier = c 589 p.advance() 590 } 591 } 592 } 593 } 594 595 closeRange := p.current().Range 596 if !p.expectWithMatchingLoc(css_lexer.TCloseBracket, matchingLoc) { 597 closeRange.Len = 0 598 } 599 r = logger.Range{Loc: matchingLoc, Len: closeRange.End() - matchingLoc.Start} 600 return 601 } 602 603 func (p *parser) parsePseudoClassSelector(loc logger.Loc, isElement bool) (css_ast.SS, logger.Range) { 604 p.advance() 605 606 if p.peek(css_lexer.TFunction) { 607 text := p.decoded() 608 matchingLoc := logger.Loc{Start: p.current().Range.End() - 1} 609 p.advance() 610 611 // Potentially parse a pseudo-class with a selector list 612 if !isElement { 613 var kind css_ast.PseudoClassKind 614 local := p.makeLocalSymbols 615 ok := true 616 switch text { 617 case "global": 618 kind = css_ast.PseudoClassGlobal 619 if p.options.symbolMode != symbolModeDisabled { 620 local = false 621 } 622 case "has": 623 kind = css_ast.PseudoClassHas 624 case "is": 625 kind = css_ast.PseudoClassIs 626 case "local": 627 kind = css_ast.PseudoClassLocal 628 if p.options.symbolMode != symbolModeDisabled { 629 local = true 630 } 631 case "not": 632 kind = css_ast.PseudoClassNot 633 case "nth-child": 634 kind = css_ast.PseudoClassNthChild 635 case "nth-last-child": 636 kind = css_ast.PseudoClassNthLastChild 637 case "nth-of-type": 638 kind = css_ast.PseudoClassNthOfType 639 case "nth-last-of-type": 640 kind = css_ast.PseudoClassNthLastOfType 641 case "where": 642 kind = css_ast.PseudoClassWhere 643 default: 644 ok = false 645 } 646 if ok { 647 old := p.index 648 if kind.HasNthIndex() { 649 p.eat(css_lexer.TWhitespace) 650 651 // Parse the "An+B" syntax 652 if index, ok := p.parseNthIndex(); ok { 653 var selectors []css_ast.ComplexSelector 654 655 // Parse the optional "of" clause 656 if (kind == css_ast.PseudoClassNthChild || kind == css_ast.PseudoClassNthLastChild) && 657 p.peek(css_lexer.TIdent) && strings.EqualFold(p.decoded(), "of") { 658 p.advance() 659 p.eat(css_lexer.TWhitespace) 660 661 // Contain the effects of ":local" and ":global" 662 oldLocal := p.makeLocalSymbols 663 selectors, ok = p.parseSelectorList(parseSelectorOpts{ 664 stopOnCloseParen: true, 665 noLeadingCombinator: true, 666 }) 667 p.makeLocalSymbols = oldLocal 668 } 669 670 // "2n+0" => "2n" 671 if p.options.minifySyntax { 672 index.Minify() 673 } 674 675 // Match the closing ")" 676 if ok { 677 closeRange := p.current().Range 678 if !p.expectWithMatchingLoc(css_lexer.TCloseParen, matchingLoc) { 679 closeRange.Len = 0 680 } 681 return &css_ast.SSPseudoClassWithSelectorList{Kind: kind, Selectors: selectors, Index: index}, 682 logger.Range{Loc: loc, Len: closeRange.End() - loc.Start} 683 } 684 } 685 } else { 686 p.eat(css_lexer.TWhitespace) 687 688 // ":local" forces local names and ":global" forces global names 689 oldLocal := p.makeLocalSymbols 690 p.makeLocalSymbols = local 691 selectors, ok := p.parseSelectorList(parseSelectorOpts{ 692 pseudoClassKind: kind, 693 stopOnCloseParen: true, 694 onlyOneComplexSelector: kind == css_ast.PseudoClassGlobal || kind == css_ast.PseudoClassLocal, 695 }) 696 p.makeLocalSymbols = oldLocal 697 698 // Match the closing ")" 699 if ok { 700 closeRange := p.current().Range 701 if !p.expectWithMatchingLoc(css_lexer.TCloseParen, matchingLoc) { 702 closeRange.Len = 0 703 } 704 return &css_ast.SSPseudoClassWithSelectorList{Kind: kind, Selectors: selectors}, 705 logger.Range{Loc: loc, Len: closeRange.End() - loc.Start} 706 } 707 } 708 p.index = old 709 } 710 } 711 712 args := p.convertTokens(p.parseAnyValue()) 713 closeRange := p.current().Range 714 if !p.expectWithMatchingLoc(css_lexer.TCloseParen, matchingLoc) { 715 closeRange.Len = 0 716 } 717 return &css_ast.SSPseudoClass{IsElement: isElement, Name: text, Args: args}, 718 logger.Range{Loc: loc, Len: closeRange.End() - loc.Start} 719 } 720 721 nameRange := p.current().Range 722 name := p.decoded() 723 sel := css_ast.SSPseudoClass{IsElement: isElement} 724 if p.expect(css_lexer.TIdent) { 725 sel.Name = name 726 727 // ":local .local_name :global .global_name {}" 728 // ":local { .local_name { :global { .global_name {} } }" 729 if p.options.symbolMode != symbolModeDisabled { 730 switch name { 731 case "local": 732 p.makeLocalSymbols = true 733 case "global": 734 p.makeLocalSymbols = false 735 } 736 } 737 } else { 738 nameRange.Len = 0 739 } 740 return &sel, logger.Range{Loc: loc, Len: nameRange.End() - loc.Start} 741 } 742 743 func (p *parser) parseAnyValue() []css_lexer.Token { 744 // Reference: https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value 745 746 p.stack = p.stack[:0] // Reuse allocated memory 747 start := p.index 748 749 loop: 750 for { 751 switch p.current().Kind { 752 case css_lexer.TCloseParen, css_lexer.TCloseBracket, css_lexer.TCloseBrace: 753 last := len(p.stack) - 1 754 if last < 0 || !p.peek(p.stack[last]) { 755 break loop 756 } 757 p.stack = p.stack[:last] 758 759 case css_lexer.TSemicolon, css_lexer.TDelimExclamation: 760 if len(p.stack) == 0 { 761 break loop 762 } 763 764 case css_lexer.TOpenParen, css_lexer.TFunction: 765 p.stack = append(p.stack, css_lexer.TCloseParen) 766 767 case css_lexer.TOpenBracket: 768 p.stack = append(p.stack, css_lexer.TCloseBracket) 769 770 case css_lexer.TOpenBrace: 771 p.stack = append(p.stack, css_lexer.TCloseBrace) 772 773 case css_lexer.TEndOfFile: 774 break loop 775 } 776 777 p.advance() 778 } 779 780 tokens := p.tokens[start:p.index] 781 if len(tokens) == 0 { 782 p.unexpected() 783 } 784 return tokens 785 } 786 787 func (p *parser) parseCombinator() css_ast.Combinator { 788 t := p.current() 789 790 switch t.Kind { 791 case css_lexer.TDelimGreaterThan: 792 p.advance() 793 return css_ast.Combinator{Loc: t.Range.Loc, Byte: '>'} 794 795 case css_lexer.TDelimPlus: 796 p.advance() 797 return css_ast.Combinator{Loc: t.Range.Loc, Byte: '+'} 798 799 case css_lexer.TDelimTilde: 800 p.advance() 801 return css_ast.Combinator{Loc: t.Range.Loc, Byte: '~'} 802 803 default: 804 return css_ast.Combinator{} 805 } 806 } 807 808 func parseInteger(text string) (string, bool) { 809 n := len(text) 810 if n == 0 { 811 return "", false 812 } 813 814 // Trim leading zeros 815 start := 0 816 for start < n && text[start] == '0' { 817 start++ 818 } 819 820 // Make sure remaining characters are digits 821 if start == n { 822 return "0", true 823 } 824 for i := start; i < n; i++ { 825 if c := text[i]; c < '0' || c > '9' { 826 return "", false 827 } 828 } 829 return text[start:], true 830 } 831 832 func (p *parser) parseNthIndex() (css_ast.NthIndex, bool) { 833 type sign uint8 834 const ( 835 none sign = iota 836 negative 837 positive 838 ) 839 840 // Reference: https://drafts.csswg.org/css-syntax-3/#anb-microsyntax 841 t0 := p.current() 842 text0 := p.decoded() 843 844 // Handle "even" and "odd" 845 if t0.Kind == css_lexer.TIdent && (text0 == "even" || text0 == "odd") { 846 p.advance() 847 p.eat(css_lexer.TWhitespace) 848 return css_ast.NthIndex{B: text0}, true 849 } 850 851 // Handle a single number 852 if t0.Kind == css_lexer.TNumber { 853 bNeg := false 854 if strings.HasPrefix(text0, "-") { 855 bNeg = true 856 text0 = text0[1:] 857 } else { 858 text0 = strings.TrimPrefix(text0, "+") 859 } 860 if b, ok := parseInteger(text0); ok { 861 if bNeg { 862 b = "-" + b 863 } 864 p.advance() 865 p.eat(css_lexer.TWhitespace) 866 return css_ast.NthIndex{B: b}, true 867 } 868 p.unexpected() 869 return css_ast.NthIndex{}, false 870 } 871 872 aSign := none 873 if p.eat(css_lexer.TDelimPlus) { 874 aSign = positive 875 t0 = p.current() 876 text0 = p.decoded() 877 } 878 879 // Everything from here must be able to contain an "n" 880 if t0.Kind != css_lexer.TIdent && t0.Kind != css_lexer.TDimension { 881 p.unexpected() 882 return css_ast.NthIndex{}, false 883 } 884 885 // Check for a leading sign 886 if aSign == none { 887 if strings.HasPrefix(text0, "-") { 888 aSign = negative 889 text0 = text0[1:] 890 } else { 891 text0 = strings.TrimPrefix(text0, "+") 892 } 893 } 894 895 // The string must contain an "n" 896 n := strings.IndexByte(text0, 'n') 897 if n < 0 { 898 p.unexpected() 899 return css_ast.NthIndex{}, false 900 } 901 902 // Parse the number before the "n" 903 var a string 904 if n == 0 { 905 if aSign == negative { 906 a = "-1" 907 } else { 908 a = "1" 909 } 910 } else if aInt, ok := parseInteger(text0[:n]); ok { 911 if aSign == negative { 912 aInt = "-" + aInt 913 } 914 a = aInt 915 } else { 916 p.unexpected() 917 return css_ast.NthIndex{}, false 918 } 919 text0 = text0[n+1:] 920 921 // Parse the stuff after the "n" 922 bSign := none 923 if strings.HasPrefix(text0, "-") { 924 text0 = text0[1:] 925 if b, ok := parseInteger(text0); ok { 926 p.advance() 927 p.eat(css_lexer.TWhitespace) 928 return css_ast.NthIndex{A: a, B: "-" + b}, true 929 } 930 bSign = negative 931 } 932 if text0 != "" { 933 p.unexpected() 934 return css_ast.NthIndex{}, false 935 } 936 p.advance() 937 p.eat(css_lexer.TWhitespace) 938 939 // Parse an optional sign delimiter 940 if bSign == none { 941 if p.eat(css_lexer.TDelimMinus) { 942 bSign = negative 943 p.eat(css_lexer.TWhitespace) 944 } else if p.eat(css_lexer.TDelimPlus) { 945 bSign = positive 946 p.eat(css_lexer.TWhitespace) 947 } 948 } 949 950 // Parse an optional trailing number 951 t1 := p.current() 952 text1 := p.decoded() 953 if t1.Kind == css_lexer.TNumber { 954 if bSign == none { 955 if strings.HasPrefix(text1, "-") { 956 bSign = negative 957 text1 = text1[1:] 958 } else if strings.HasPrefix(text1, "+") { 959 text1 = text1[1:] 960 } 961 } 962 if b, ok := parseInteger(text1); ok { 963 if bSign == negative { 964 b = "-" + b 965 } 966 p.advance() 967 p.eat(css_lexer.TWhitespace) 968 return css_ast.NthIndex{A: a, B: b}, true 969 } 970 } 971 972 // If there is a trailing sign, then there must also be a trailing number 973 if bSign != none { 974 p.expect(css_lexer.TNumber) 975 return css_ast.NthIndex{}, false 976 } 977 978 return css_ast.NthIndex{A: a}, true 979 }