github.com/evanw/esbuild@v0.21.4/internal/css_parser/css_decls_font_family.go (about)

     1  package css_parser
     2  
     3  import (
     4  	"strings"
     5  
     6  	"github.com/evanw/esbuild/internal/css_ast"
     7  	"github.com/evanw/esbuild/internal/css_lexer"
     8  )
     9  
    10  // These keywords usually require special handling when parsing.
    11  
    12  // Declaring a property to have these values explicitly specifies a particular
    13  // defaulting behavior instead of setting the property to that identifier value.
    14  // As specified in CSS Values and Units Level 3, all CSS properties can accept
    15  // these values.
    16  //
    17  // For example, "font-family: 'inherit'" sets the font family to the font named
    18  // "inherit" while "font-family: inherit" sets the font family to the inherited
    19  // value.
    20  //
    21  // Note that other CSS specifications can define additional CSS-wide keywords,
    22  // which we should copy here whenever new ones are created so we can quote those
    23  // identifiers to avoid collisions with any newly-created CSS-wide keywords.
    24  var cssWideAndReservedKeywords = map[string]bool{
    25  	// CSS Values and Units Level 3: https://drafts.csswg.org/css-values-3/#common-keywords
    26  	"initial": true, // CSS-wide keyword
    27  	"inherit": true, // CSS-wide keyword
    28  	"unset":   true, // CSS-wide keyword
    29  	"default": true, // CSS reserved keyword
    30  
    31  	// CSS Cascading and Inheritance Level 5: https://drafts.csswg.org/css-cascade-5/#defaulting-keywords
    32  	"revert":       true, // Cascade-dependent keyword
    33  	"revert-layer": true, // Cascade-dependent keyword
    34  }
    35  
    36  // Font family names that happen to be the same as a keyword value must be
    37  // quoted to prevent confusion with the keywords with the same names. UAs must
    38  // not consider these keywords as matching the <family-name> type.
    39  // Specification: https://drafts.csswg.org/css-fonts/#generic-font-families
    40  var genericFamilyNames = map[string]bool{
    41  	"serif":         true,
    42  	"sans-serif":    true,
    43  	"cursive":       true,
    44  	"fantasy":       true,
    45  	"monospace":     true,
    46  	"system-ui":     true,
    47  	"emoji":         true,
    48  	"math":          true,
    49  	"fangsong":      true,
    50  	"ui-serif":      true,
    51  	"ui-sans-serif": true,
    52  	"ui-monospace":  true,
    53  	"ui-rounded":    true,
    54  }
    55  
    56  // Specification: https://drafts.csswg.org/css-fonts/#font-family-prop
    57  func (p *parser) mangleFontFamily(tokens []css_ast.Token) ([]css_ast.Token, bool) {
    58  	result, rest, ok := p.mangleFamilyNameOrGenericName(nil, tokens)
    59  	if !ok {
    60  		return nil, false
    61  	}
    62  
    63  	for len(rest) > 0 && rest[0].Kind == css_lexer.TComma {
    64  		result, rest, ok = p.mangleFamilyNameOrGenericName(append(result, rest[0]), rest[1:])
    65  		if !ok {
    66  			return nil, false
    67  		}
    68  	}
    69  
    70  	if len(rest) > 0 {
    71  		return nil, false
    72  	}
    73  
    74  	return result, true
    75  }
    76  
    77  func (p *parser) mangleFamilyNameOrGenericName(result []css_ast.Token, tokens []css_ast.Token) ([]css_ast.Token, []css_ast.Token, bool) {
    78  	if len(tokens) > 0 {
    79  		t := tokens[0]
    80  
    81  		// Handle <generic-family>
    82  		if t.Kind == css_lexer.TIdent && genericFamilyNames[t.Text] {
    83  			return append(result, t), tokens[1:], true
    84  		}
    85  
    86  		// Handle <family-name>
    87  		if t.Kind == css_lexer.TString {
    88  			// "If a sequence of identifiers is given as a <family-name>, the computed
    89  			// value is the name converted to a string by joining all the identifiers
    90  			// in the sequence by single spaces."
    91  			//
    92  			// More information: https://mathiasbynens.be/notes/unquoted-font-family
    93  			names := strings.Split(t.Text, " ")
    94  			for _, name := range names {
    95  				if !isValidCustomIdent(name, genericFamilyNames) {
    96  					return append(result, t), tokens[1:], true
    97  				}
    98  			}
    99  			for i, name := range names {
   100  				var whitespace css_ast.WhitespaceFlags
   101  				if i != 0 || !p.options.minifyWhitespace {
   102  					whitespace = css_ast.WhitespaceBefore
   103  				}
   104  				result = append(result, css_ast.Token{
   105  					Loc:        t.Loc,
   106  					Kind:       css_lexer.TIdent,
   107  					Text:       name,
   108  					Whitespace: whitespace,
   109  				})
   110  			}
   111  			return result, tokens[1:], true
   112  		}
   113  
   114  		// "Font family names other than generic families must either be given
   115  		// quoted as <string>s, or unquoted as a sequence of one or more
   116  		// <custom-ident>."
   117  		if t.Kind == css_lexer.TIdent {
   118  			for {
   119  				if !isValidCustomIdent(t.Text, genericFamilyNames) {
   120  					return nil, nil, false
   121  				}
   122  				result = append(result, t)
   123  				tokens = tokens[1:]
   124  				if len(tokens) == 0 || tokens[0].Kind != css_lexer.TIdent {
   125  					break
   126  				}
   127  				t = tokens[0]
   128  			}
   129  			return result, tokens, true
   130  		}
   131  	}
   132  
   133  	// Anything other than the cases listed above causes us to bail
   134  	return nil, nil, false
   135  }
   136  
   137  // Specification: https://drafts.csswg.org/css-values-4/#custom-idents
   138  func isValidCustomIdent(text string, predefinedKeywords map[string]bool) bool {
   139  	loweredText := strings.ToLower(text)
   140  
   141  	if predefinedKeywords[loweredText] {
   142  		return false
   143  	}
   144  	if cssWideAndReservedKeywords[loweredText] {
   145  		return false
   146  	}
   147  	if loweredText == "" {
   148  		return false
   149  	}
   150  
   151  	// validate if it contains characters which needs to be escaped
   152  	if !css_lexer.WouldStartIdentifierWithoutEscapes(text) {
   153  		return false
   154  	}
   155  	for _, c := range text {
   156  		if !css_lexer.IsNameContinue(c) {
   157  			return false
   158  		}
   159  	}
   160  
   161  	return true
   162  }