mirror of
https://github.com/hanzoai/esbuild.git
synced 2026-09-24 16:58:50 +00:00
568 lines
17 KiB
Go
568 lines
17 KiB
Go
package css_parser
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/evanw/esbuild/internal/compat"
|
|
"github.com/evanw/esbuild/internal/css_ast"
|
|
"github.com/evanw/esbuild/internal/css_lexer"
|
|
"github.com/evanw/esbuild/internal/logger"
|
|
)
|
|
|
|
func (p *parser) commaToken(loc logger.Loc) css_ast.Token {
|
|
t := css_ast.Token{
|
|
Loc: loc,
|
|
Kind: css_lexer.TComma,
|
|
Text: ",",
|
|
}
|
|
if !p.options.minifyWhitespace {
|
|
t.Whitespace = css_ast.WhitespaceAfter
|
|
}
|
|
return t
|
|
}
|
|
|
|
func expandTokenQuad(tokens []css_ast.Token, allowedIdent string) (result [4]css_ast.Token, ok bool) {
|
|
n := len(tokens)
|
|
if n < 1 || n > 4 {
|
|
return
|
|
}
|
|
|
|
// Don't do this if we encounter any unexpected tokens such as "var()"
|
|
for i := 0; i < n; i++ {
|
|
if t := tokens[i]; !t.Kind.IsNumeric() && (t.Kind != css_lexer.TIdent || allowedIdent == "" || t.Text != allowedIdent) {
|
|
return
|
|
}
|
|
}
|
|
|
|
result[0] = tokens[0]
|
|
if n > 1 {
|
|
result[1] = tokens[1]
|
|
} else {
|
|
result[1] = result[0]
|
|
}
|
|
if n > 2 {
|
|
result[2] = tokens[2]
|
|
} else {
|
|
result[2] = result[0]
|
|
}
|
|
if n > 3 {
|
|
result[3] = tokens[3]
|
|
} else {
|
|
result[3] = result[1]
|
|
}
|
|
|
|
ok = true
|
|
return
|
|
}
|
|
|
|
func compactTokenQuad(a css_ast.Token, b css_ast.Token, c css_ast.Token, d css_ast.Token, minifyWhitespace bool) []css_ast.Token {
|
|
tokens := []css_ast.Token{a, b, c, d}
|
|
if tokens[3].EqualIgnoringWhitespace(tokens[1]) {
|
|
if tokens[2].EqualIgnoringWhitespace(tokens[0]) {
|
|
if tokens[1].EqualIgnoringWhitespace(tokens[0]) {
|
|
tokens = tokens[:1]
|
|
} else {
|
|
tokens = tokens[:2]
|
|
}
|
|
} else {
|
|
tokens = tokens[:3]
|
|
}
|
|
}
|
|
for i := range tokens {
|
|
var whitespace css_ast.WhitespaceFlags
|
|
if !minifyWhitespace || i > 0 {
|
|
whitespace |= css_ast.WhitespaceBefore
|
|
}
|
|
if i+1 < len(tokens) {
|
|
whitespace |= css_ast.WhitespaceAfter
|
|
}
|
|
tokens[i].Whitespace = whitespace
|
|
}
|
|
return tokens
|
|
}
|
|
|
|
func (p *parser) processDeclarations(rules []css_ast.Rule, composesContext *composesContext) (rewrittenRules []css_ast.Rule) {
|
|
margin := boxTracker{key: css_ast.DMargin, keyText: "margin", allowAuto: true}
|
|
padding := boxTracker{key: css_ast.DPadding, keyText: "padding", allowAuto: false}
|
|
inset := boxTracker{key: css_ast.DInset, keyText: "inset", allowAuto: true}
|
|
borderRadius := borderRadiusTracker{}
|
|
rewrittenRules = make([]css_ast.Rule, 0, len(rules))
|
|
didWarnAboutComposes := false
|
|
wouldClipColorFlag := false
|
|
var declarationKeys map[string]struct{}
|
|
|
|
// Don't automatically generate the "inset" property if it's not supported
|
|
if p.options.unsupportedCSSFeatures.Has(compat.InsetProperty) {
|
|
inset.key = css_ast.DUnknown
|
|
inset.keyText = ""
|
|
}
|
|
|
|
// If this is a local class selector, track which CSS properties it declares.
|
|
// This is used to warn when CSS "composes" is used incorrectly.
|
|
if composesContext != nil {
|
|
for _, ref := range composesContext.parentRefs {
|
|
composes, ok := p.composes[ref]
|
|
if !ok {
|
|
composes = &css_ast.Composes{}
|
|
p.composes[ref] = composes
|
|
}
|
|
properties := composes.Properties
|
|
if properties == nil {
|
|
properties = make(map[string]logger.Loc)
|
|
composes.Properties = properties
|
|
}
|
|
for _, rule := range rules {
|
|
if decl, ok := rule.Data.(*css_ast.RDeclaration); ok && decl.Key != css_ast.DComposes {
|
|
properties[decl.KeyText] = decl.KeyRange.Loc
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for i := 0; i < len(rules); i++ {
|
|
rule := rules[i]
|
|
rewrittenRules = append(rewrittenRules, rule)
|
|
decl, ok := rule.Data.(*css_ast.RDeclaration)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// If the previous loop iteration would have clipped a color, we will
|
|
// duplicate it and insert the clipped copy before the unclipped copy
|
|
var wouldClipColor *bool
|
|
if wouldClipColorFlag {
|
|
wouldClipColorFlag = false
|
|
clone := *decl
|
|
clone.Value = css_ast.CloneTokensWithoutImportRecords(clone.Value)
|
|
decl = &clone
|
|
rule.Data = decl
|
|
n := len(rewrittenRules) - 2
|
|
rewrittenRules = append(rewrittenRules[:n], rule, rewrittenRules[n])
|
|
} else {
|
|
wouldClipColor = &wouldClipColorFlag
|
|
}
|
|
|
|
switch decl.Key {
|
|
case css_ast.DComposes:
|
|
// Only process "composes" directives if we're in "local-css" or
|
|
// "global-css" mode. In these cases, "composes" directives will always
|
|
// be removed (because they are being processed) even if they contain
|
|
// errors. Otherwise we leave "composes" directives there untouched and
|
|
// don't check them for errors.
|
|
if p.options.symbolMode != symbolModeDisabled {
|
|
if composesContext == nil {
|
|
if !didWarnAboutComposes {
|
|
didWarnAboutComposes = true
|
|
p.log.AddID(logger.MsgID_CSS_CSSSyntaxError, logger.Warning, &p.tracker, decl.KeyRange, "\"composes\" is not valid here")
|
|
}
|
|
} else if composesContext.problemRange.Len > 0 {
|
|
if !didWarnAboutComposes {
|
|
didWarnAboutComposes = true
|
|
p.log.AddIDWithNotes(logger.MsgID_CSS_CSSSyntaxError, logger.Warning, &p.tracker, decl.KeyRange, "\"composes\" only works inside single class selectors",
|
|
[]logger.MsgData{p.tracker.MsgData(composesContext.problemRange, "The parent selector is not a single class selector because of the syntax here:")})
|
|
}
|
|
} else {
|
|
p.handleComposesPragma(*composesContext, decl.Value)
|
|
}
|
|
rewrittenRules = rewrittenRules[:len(rewrittenRules)-1]
|
|
}
|
|
|
|
case css_ast.DBackground:
|
|
for i, t := range decl.Value {
|
|
t = p.lowerAndMinifyColor(t, wouldClipColor)
|
|
t = p.lowerAndMinifyGradient(t, wouldClipColor)
|
|
decl.Value[i] = t
|
|
}
|
|
|
|
case css_ast.DBackgroundImage,
|
|
css_ast.DBorderImage,
|
|
css_ast.DMaskImage:
|
|
|
|
for i, t := range decl.Value {
|
|
t = p.lowerAndMinifyGradient(t, wouldClipColor)
|
|
decl.Value[i] = t
|
|
}
|
|
|
|
case css_ast.DBackgroundColor,
|
|
css_ast.DBorderBlockEndColor,
|
|
css_ast.DBorderBlockStartColor,
|
|
css_ast.DBorderBottomColor,
|
|
css_ast.DBorderColor,
|
|
css_ast.DBorderInlineEndColor,
|
|
css_ast.DBorderInlineStartColor,
|
|
css_ast.DBorderLeftColor,
|
|
css_ast.DBorderRightColor,
|
|
css_ast.DBorderTopColor,
|
|
css_ast.DCaretColor,
|
|
css_ast.DColor,
|
|
css_ast.DColumnRuleColor,
|
|
css_ast.DFill,
|
|
css_ast.DFloodColor,
|
|
css_ast.DLightingColor,
|
|
css_ast.DOutlineColor,
|
|
css_ast.DStopColor,
|
|
css_ast.DStroke,
|
|
css_ast.DTextDecorationColor,
|
|
css_ast.DTextEmphasisColor:
|
|
|
|
if len(decl.Value) == 1 {
|
|
decl.Value[0] = p.lowerAndMinifyColor(decl.Value[0], wouldClipColor)
|
|
}
|
|
|
|
case css_ast.DTransform:
|
|
if p.options.minifySyntax {
|
|
decl.Value = p.mangleTransforms(decl.Value)
|
|
}
|
|
|
|
case css_ast.DBoxShadow:
|
|
decl.Value = p.lowerAndMangleBoxShadows(decl.Value, wouldClipColor)
|
|
|
|
// Container name
|
|
case css_ast.DContainer:
|
|
p.processContainerShorthand(decl.Value)
|
|
case css_ast.DContainerName:
|
|
p.processContainerName(decl.Value)
|
|
|
|
// Animation name
|
|
case css_ast.DAnimation:
|
|
p.processAnimationShorthand(decl.Value)
|
|
case css_ast.DAnimationName:
|
|
p.processAnimationName(decl.Value)
|
|
|
|
// List style
|
|
case css_ast.DListStyle:
|
|
p.processListStyleShorthand(decl.Value)
|
|
case css_ast.DListStyleType:
|
|
if len(decl.Value) == 1 {
|
|
p.processListStyleType(&decl.Value[0])
|
|
}
|
|
|
|
// Font
|
|
case css_ast.DFont:
|
|
if p.options.minifySyntax {
|
|
decl.Value = p.mangleFont(decl.Value)
|
|
}
|
|
case css_ast.DFontFamily:
|
|
if p.options.minifySyntax {
|
|
if value, ok := p.mangleFontFamily(decl.Value); ok {
|
|
decl.Value = value
|
|
}
|
|
}
|
|
case css_ast.DFontWeight:
|
|
if len(decl.Value) == 1 && p.options.minifySyntax {
|
|
decl.Value[0] = p.mangleFontWeight(decl.Value[0])
|
|
}
|
|
|
|
// Margin
|
|
case css_ast.DMargin:
|
|
if p.options.minifySyntax {
|
|
margin.mangleSides(rewrittenRules, decl, p.options.minifyWhitespace)
|
|
}
|
|
case css_ast.DMarginTop:
|
|
if p.options.minifySyntax {
|
|
margin.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxTop)
|
|
}
|
|
case css_ast.DMarginRight:
|
|
if p.options.minifySyntax {
|
|
margin.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxRight)
|
|
}
|
|
case css_ast.DMarginBottom:
|
|
if p.options.minifySyntax {
|
|
margin.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxBottom)
|
|
}
|
|
case css_ast.DMarginLeft:
|
|
if p.options.minifySyntax {
|
|
margin.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxLeft)
|
|
}
|
|
|
|
// Padding
|
|
case css_ast.DPadding:
|
|
if p.options.minifySyntax {
|
|
padding.mangleSides(rewrittenRules, decl, p.options.minifyWhitespace)
|
|
}
|
|
case css_ast.DPaddingTop:
|
|
if p.options.minifySyntax {
|
|
padding.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxTop)
|
|
}
|
|
case css_ast.DPaddingRight:
|
|
if p.options.minifySyntax {
|
|
padding.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxRight)
|
|
}
|
|
case css_ast.DPaddingBottom:
|
|
if p.options.minifySyntax {
|
|
padding.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxBottom)
|
|
}
|
|
case css_ast.DPaddingLeft:
|
|
if p.options.minifySyntax {
|
|
padding.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxLeft)
|
|
}
|
|
|
|
// Inset
|
|
case css_ast.DInset:
|
|
if p.options.unsupportedCSSFeatures.Has(compat.InsetProperty) {
|
|
if decls, ok := p.lowerInset(rule.Loc, decl); ok {
|
|
rewrittenRules = rewrittenRules[:len(rewrittenRules)-1]
|
|
for i := range decls {
|
|
rewrittenRules = append(rewrittenRules, decls[i])
|
|
if p.options.minifySyntax {
|
|
inset.mangleSide(rewrittenRules, decls[i].Data.(*css_ast.RDeclaration), p.options.minifyWhitespace, i)
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if p.options.minifySyntax {
|
|
inset.mangleSides(rewrittenRules, decl, p.options.minifyWhitespace)
|
|
}
|
|
case css_ast.DTop:
|
|
if p.options.minifySyntax {
|
|
inset.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxTop)
|
|
}
|
|
case css_ast.DRight:
|
|
if p.options.minifySyntax {
|
|
inset.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxRight)
|
|
}
|
|
case css_ast.DBottom:
|
|
if p.options.minifySyntax {
|
|
inset.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxBottom)
|
|
}
|
|
case css_ast.DLeft:
|
|
if p.options.minifySyntax {
|
|
inset.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxLeft)
|
|
}
|
|
|
|
// Border radius
|
|
case css_ast.DBorderRadius:
|
|
if p.options.minifySyntax {
|
|
borderRadius.mangleCorners(rewrittenRules, decl, p.options.minifyWhitespace)
|
|
}
|
|
case css_ast.DBorderTopLeftRadius:
|
|
if p.options.minifySyntax {
|
|
borderRadius.mangleCorner(rewrittenRules, decl, p.options.minifyWhitespace, borderRadiusTopLeft)
|
|
}
|
|
case css_ast.DBorderTopRightRadius:
|
|
if p.options.minifySyntax {
|
|
borderRadius.mangleCorner(rewrittenRules, decl, p.options.minifyWhitespace, borderRadiusTopRight)
|
|
}
|
|
case css_ast.DBorderBottomRightRadius:
|
|
if p.options.minifySyntax {
|
|
borderRadius.mangleCorner(rewrittenRules, decl, p.options.minifyWhitespace, borderRadiusBottomRight)
|
|
}
|
|
case css_ast.DBorderBottomLeftRadius:
|
|
if p.options.minifySyntax {
|
|
borderRadius.mangleCorner(rewrittenRules, decl, p.options.minifyWhitespace, borderRadiusBottomLeft)
|
|
}
|
|
}
|
|
|
|
if prefixes, ok := p.options.cssPrefixData[decl.Key]; ok {
|
|
if declarationKeys == nil {
|
|
// Only generate this map if it's needed
|
|
declarationKeys = make(map[string]struct{})
|
|
for _, rule := range rules {
|
|
if decl, ok := rule.Data.(*css_ast.RDeclaration); ok {
|
|
declarationKeys[decl.KeyText] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
if (prefixes & compat.WebkitPrefix) != 0 {
|
|
rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-webkit-", rule.Loc, decl, declarationKeys)
|
|
}
|
|
if (prefixes & compat.KhtmlPrefix) != 0 {
|
|
rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-khtml-", rule.Loc, decl, declarationKeys)
|
|
}
|
|
if (prefixes & compat.MozPrefix) != 0 {
|
|
rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-moz-", rule.Loc, decl, declarationKeys)
|
|
}
|
|
if (prefixes & compat.MsPrefix) != 0 {
|
|
rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-ms-", rule.Loc, decl, declarationKeys)
|
|
}
|
|
if (prefixes & compat.OPrefix) != 0 {
|
|
rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-o-", rule.Loc, decl, declarationKeys)
|
|
}
|
|
}
|
|
|
|
// If this loop iteration would have clipped a color, the out-of-gamut
|
|
// colors will not be clipped and this flag will be set. We then set up the
|
|
// next iteration of the loop to duplicate this rule and process it again
|
|
// with color clipping enabled.
|
|
if wouldClipColorFlag {
|
|
if p.options.unsupportedCSSFeatures.Has(compat.ColorFunctions) {
|
|
// Only do this if there was no previous instance of that property so
|
|
// we avoid overwriting any manually-specified fallback values
|
|
for j := len(rewrittenRules) - 2; j >= 0; j-- {
|
|
if prev, ok := rewrittenRules[j].Data.(*css_ast.RDeclaration); ok && prev.Key == decl.Key {
|
|
wouldClipColorFlag = false
|
|
break
|
|
}
|
|
}
|
|
if wouldClipColorFlag {
|
|
// If the code above would have clipped a color outside of the sRGB gamut,
|
|
// process this rule again so we can generate the clipped version next time
|
|
i -= 1
|
|
continue
|
|
}
|
|
}
|
|
wouldClipColorFlag = false
|
|
}
|
|
}
|
|
|
|
// Compact removed rules
|
|
if p.options.minifySyntax {
|
|
end := 0
|
|
for _, rule := range rewrittenRules {
|
|
if rule.Data != nil {
|
|
rewrittenRules[end] = rule
|
|
end++
|
|
}
|
|
}
|
|
rewrittenRules = rewrittenRules[:end]
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (p *parser) insertPrefixedDeclaration(rules []css_ast.Rule, prefix string, loc logger.Loc, decl *css_ast.RDeclaration, declarationKeys map[string]struct{}) []css_ast.Rule {
|
|
keyText := prefix + decl.KeyText
|
|
|
|
// Don't insert a prefixed declaration if there already is one
|
|
if _, ok := declarationKeys[keyText]; ok {
|
|
// We found a previous declaration with a matching prefixed property.
|
|
// The value is ignored, which matches the behavior of "autoprefixer".
|
|
return rules
|
|
}
|
|
|
|
// Additional special cases for when the prefix applies
|
|
switch decl.Key {
|
|
case css_ast.DBackgroundClip:
|
|
// The prefix is only needed for "background-clip: text"
|
|
if len(decl.Value) != 1 || decl.Value[0].Kind != css_lexer.TIdent || !strings.EqualFold(decl.Value[0].Text, "text") {
|
|
return rules
|
|
}
|
|
|
|
case css_ast.DPosition:
|
|
// The prefix is only needed for "position: sticky"
|
|
if len(decl.Value) != 1 || decl.Value[0].Kind != css_lexer.TIdent || !strings.EqualFold(decl.Value[0].Text, "sticky") {
|
|
return rules
|
|
}
|
|
|
|
case css_ast.DWidth, css_ast.DMinWidth, css_ast.DMaxWidth,
|
|
css_ast.DHeight, css_ast.DMinHeight, css_ast.DMaxHeight:
|
|
// The prefix is only needed for "width: stretch"
|
|
if len(decl.Value) != 1 || decl.Value[0].Kind != css_lexer.TIdent || !strings.EqualFold(decl.Value[0].Text, "stretch") {
|
|
return rules
|
|
}
|
|
}
|
|
|
|
value := css_ast.CloneTokensWithoutImportRecords(decl.Value)
|
|
|
|
// Additional special cases for how to transform the contents
|
|
switch decl.Key {
|
|
case css_ast.DPosition:
|
|
// The prefix applies to the value, not the property
|
|
keyText = decl.KeyText
|
|
value[0].Text = "-webkit-sticky"
|
|
|
|
case css_ast.DWidth, css_ast.DMinWidth, css_ast.DMaxWidth,
|
|
css_ast.DHeight, css_ast.DMinHeight, css_ast.DMaxHeight:
|
|
// The prefix applies to the value, not the property
|
|
keyText = decl.KeyText
|
|
|
|
// This currently only applies to "stretch" (already checked above)
|
|
switch prefix {
|
|
case "-webkit-":
|
|
value[0].Text = "-webkit-fill-available"
|
|
case "-moz-":
|
|
value[0].Text = "-moz-available"
|
|
}
|
|
|
|
case css_ast.DUserSelect:
|
|
// The prefix applies to the value as well as the property
|
|
if prefix == "-moz-" && len(value) == 1 && value[0].Kind == css_lexer.TIdent && strings.EqualFold(value[0].Text, "none") {
|
|
value[0].Text = "-moz-none"
|
|
}
|
|
|
|
case css_ast.DMaskComposite:
|
|
// WebKit uses different names for these values
|
|
if prefix == "-webkit-" {
|
|
for i, token := range value {
|
|
if token.Kind == css_lexer.TIdent {
|
|
switch token.Text {
|
|
case "add":
|
|
value[i].Text = "source-over"
|
|
case "subtract":
|
|
value[i].Text = "source-out"
|
|
case "intersect":
|
|
value[i].Text = "source-in"
|
|
case "exclude":
|
|
value[i].Text = "xor"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we didn't change the key, manually search for a previous duplicate rule
|
|
if keyText == decl.KeyText {
|
|
for _, rule := range rules {
|
|
if prevDecl, ok := rule.Data.(*css_ast.RDeclaration); ok && prevDecl.KeyText == keyText && css_ast.TokensEqual(prevDecl.Value, value, nil) {
|
|
return rules
|
|
}
|
|
}
|
|
}
|
|
|
|
// Overwrite the latest declaration with the prefixed declaration
|
|
rules[len(rules)-1] = css_ast.Rule{Loc: loc, Data: &css_ast.RDeclaration{
|
|
KeyText: keyText,
|
|
KeyRange: decl.KeyRange,
|
|
Value: value,
|
|
Important: decl.Important,
|
|
}}
|
|
|
|
// Re-add the latest declaration after the inserted declaration
|
|
rules = append(rules, css_ast.Rule{Loc: loc, Data: decl})
|
|
return rules
|
|
}
|
|
|
|
func (p *parser) lowerInset(loc logger.Loc, decl *css_ast.RDeclaration) ([]css_ast.Rule, bool) {
|
|
if tokens, ok := expandTokenQuad(decl.Value, ""); ok {
|
|
mask := ^css_ast.WhitespaceAfter
|
|
if p.options.minifyWhitespace {
|
|
mask = 0
|
|
}
|
|
for i := range tokens {
|
|
tokens[i].Whitespace &= mask
|
|
}
|
|
return []css_ast.Rule{
|
|
{Loc: loc, Data: &css_ast.RDeclaration{
|
|
KeyText: "top",
|
|
KeyRange: decl.KeyRange,
|
|
Key: css_ast.DTop,
|
|
Value: tokens[0:1],
|
|
Important: decl.Important,
|
|
}},
|
|
{Loc: loc, Data: &css_ast.RDeclaration{
|
|
KeyText: "right",
|
|
KeyRange: decl.KeyRange,
|
|
Key: css_ast.DRight,
|
|
Value: tokens[1:2],
|
|
Important: decl.Important,
|
|
}},
|
|
{Loc: loc, Data: &css_ast.RDeclaration{
|
|
KeyText: "bottom",
|
|
KeyRange: decl.KeyRange,
|
|
Key: css_ast.DBottom,
|
|
Value: tokens[2:3],
|
|
Important: decl.Important,
|
|
}},
|
|
{Loc: loc, Data: &css_ast.RDeclaration{
|
|
KeyText: "left",
|
|
KeyRange: decl.KeyRange,
|
|
Key: css_ast.DLeft,
|
|
Value: tokens[3:4],
|
|
Important: decl.Important,
|
|
}},
|
|
}, true
|
|
}
|
|
return nil, false
|
|
}
|