Add a CSS fast path parser for hsl() and hsla() colors.

We already had one for rgb()/rgba(), so refactor a few things from it
and use them to build one for hsl()/hsla(). Speeds up MotionMark
multiply by 5–7%.

Change-Id: I5e70e9f8905184d7958e33a02b95c28c587f1208
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3417641
Reviewed-by: Anders Hartvoll Ruud <andruud@chromium.org>
Commit-Queue: Steinar H Gunderson <sesse@chromium.org>
Cr-Commit-Position: refs/heads/main@{#965281}
diff --git a/third_party/blink/renderer/core/css/parser/css_parser_fast_paths.cc b/third_party/blink/renderer/core/css/parser/css_parser_fast_paths.cc
index 593482a..cc3292d 100644
--- a/third_party/blink/renderer/core/css/parser/css_parser_fast_paths.cc
+++ b/third_party/blink/renderer/core/css/parser/css_parser_fast_paths.cc
@@ -178,7 +178,8 @@
     length -= 4;
     unit = CSSPrimitiveValue::UnitType::kTurns;
   } else {
-    // Only valid for zero (we'll check that in the caller).
+    // For rotate: Only valid for zero (we'll check that in the caller).
+    // For hsl(): To be treated as angles (also done in the caller).
     unit = CSSPrimitiveValue::UnitType::kNumber;
   }
 
@@ -240,13 +241,10 @@
   }
 }
 
-// Returns the number of characters which form a valid double
-// and are terminated by the given terminator character
+// Returns the number of initial characters which form a valid double.
 template <typename CharacterType>
-static int CheckForValidDouble(const CharacterType* string,
-                               const CharacterType* end,
-                               const bool terminated_by_space,
-                               const char terminator) {
+static int FindLengthOfValidDouble(const CharacterType* string,
+                                   const CharacterType* end) {
   int length = static_cast<int>(end - string);
   if (length < 1)
     return 0;
@@ -254,17 +252,12 @@
   bool decimal_mark_seen = false;
   int processed_length = 0;
 
-  for (int i = 0; i < length; ++i) {
-    if (string[i] == terminator ||
-        (terminated_by_space && IsHTMLSpace<CharacterType>(string[i]))) {
-      processed_length = i;
-      break;
-    }
+  for (int i = 0; i < length; ++i, ++processed_length) {
     if (!IsASCIIDigit(string[i])) {
       if (!decimal_mark_seen && string[i] == '.')
         decimal_mark_seen = true;
       else
-        return 0;
+        break;
     }
   }
 
@@ -274,17 +267,32 @@
   return processed_length;
 }
 
-// Returns the number of characters consumed for parsing a valid double
-// terminated by the given terminator character
+// If also_accept_whitespace is true: Checks whether string[pos] is the given
+// character, _or_ an HTML space.
+// Otherwise: Checks whether string[pos] is the given character.
+// Returns false if pos is past the end of the string.
+template <typename CharacterType>
+static bool ContainsCharAtPos(const CharacterType* string,
+                              const CharacterType* end,
+                              int pos,
+                              char ch,
+                              bool also_accept_whitespace) {
+  DCHECK_GE(pos, 0);
+  if (pos >= static_cast<int>(end - string)) {
+    return false;
+  }
+  return string[pos] == ch ||
+         (also_accept_whitespace && IsHTMLSpace(string[pos]));
+}
+
+// Returns the number of characters consumed for parsing a valid double,
+// or 0 if the string did not start with a valid double.
 template <typename CharacterType>
 static int ParseDouble(const CharacterType* string,
                        const CharacterType* end,
-                       const char terminator,
-                       const bool terminated_by_space,
                        double& value) {
-  int length =
-      CheckForValidDouble(string, end, terminated_by_space, terminator);
-  if (!length)
+  int length = FindLengthOfValidDouble(string, end);
+  if (length == 0)
     return 0;
 
   int position = 0;
@@ -316,67 +324,136 @@
   return length;
 }
 
+// Parse a float and clamp it upwards to max_value. Optimized for having
+// no decimal part.
 template <typename CharacterType>
-static bool ParseColorNumberOrPercentage(const CharacterType*& string,
-                                         const CharacterType* end,
-                                         const char terminator,
-                                         bool& should_whitespace_terminate,
-                                         bool is_first_value,
-                                         CSSPrimitiveValue::UnitType& expect,
-                                         int& value) {
+static bool ParseFloatWithMaxValue(const CharacterType*& string,
+                                   const CharacterType* end,
+                                   int max_value,
+                                   double& value,
+                                   bool& negative) {
+  value = 0.0;
   const CharacterType* current = string;
-  double local_value = 0;
-  bool negative = false;
   while (current != end && IsHTMLSpace<CharacterType>(*current))
     current++;
   if (current != end && *current == '-') {
     negative = true;
     current++;
+  } else {
+    negative = false;
   }
   if (current == end || !IsASCIIDigit(*current))
     return false;
   while (current != end && IsASCIIDigit(*current)) {
-    double new_value = local_value * 10 + *current++ - '0';
-    if (new_value >= 255) {
-      // Clamp values at 255.
-      local_value = 255;
+    double new_value = value * 10 + *current++ - '0';
+    if (new_value >= max_value) {
+      // Clamp values at 255 or 100 (depending on the caller).
+      value = max_value;
       while (current != end && IsASCIIDigit(*current))
         ++current;
       break;
     }
-    local_value = new_value;
+    value = new_value;
   }
 
   if (current == end)
     return false;
 
-  if (expect == CSSPrimitiveValue::UnitType::kNumber && *current == '%')
-    return false;
-
   if (*current == '.') {
     // We already parsed the integral part, try to parse
     // the fraction part.
     double fractional = 0;
-    int num_characters_parsed =
-        ParseDouble(current, end, '%', false, fractional);
-    if (num_characters_parsed) {
-      // Number is a percent.
-      current += num_characters_parsed;
-      if (*current != '%')
-        return false;
-    } else {
-      // Number is a decimal.
-      num_characters_parsed =
-          ParseDouble(current, end, terminator, true, fractional);
-      if (!num_characters_parsed)
-        return false;
-      current += num_characters_parsed;
+    int num_characters_parsed = ParseDouble(current, end, fractional);
+    if (num_characters_parsed == 0) {
+      return false;
     }
-    local_value += fractional;
+    current += num_characters_parsed;
+    value += fractional;
   }
 
+  string = current;
+  return true;
+}
+
+namespace {
+
+enum TerminatorStatus {
+  // List elements are delimited with whitespace,
+  // e.g., rgb(10 20 30).
+  kMustWhitespaceTerminate,
+
+  // List elements are delimited with a given terminator,
+  // and any whitespace before it should be skipped over,
+  // e.g., rgb(10 , 20,30).
+  kMustCharacterTerminate,
+
+  // We are parsing the first element, so we could do either
+  // variant -- and when it's an in/out argument, we set it
+  // to one of the other values.
+  kCouldWhitespaceTerminate,
+};
+
+}  // namespace
+
+template <typename CharacterType>
+static bool SkipToTerminator(const CharacterType*& string,
+                             const CharacterType* end,
+                             const char terminator,
+                             TerminatorStatus& terminator_status) {
+  const CharacterType* current = string;
+
+  while (current != end && IsHTMLSpace<CharacterType>(*current))
+    current++;
+
+  switch (terminator_status) {
+    case kCouldWhitespaceTerminate:
+      if (current != end && *current == terminator) {
+        terminator_status = kMustCharacterTerminate;
+        ++current;
+        break;
+      }
+      terminator_status = kMustWhitespaceTerminate;
+      [[fallthrough]];
+    case kMustWhitespaceTerminate:
+      // We must have skipped over at least one space before finding
+      // something else (or the end).
+      if (current == string) {
+        return false;
+      }
+      break;
+    case kMustCharacterTerminate:
+      // We must have stopped at the given terminator character.
+      if (current == end || *current != terminator) {
+        return false;
+      }
+      ++current;  // Skip over the terminator.
+      break;
+  }
+
+  string = current;
+  return true;
+}
+
+template <typename CharacterType>
+static bool ParseColorNumberOrPercentage(const CharacterType*& string,
+                                         const CharacterType* end,
+                                         const char terminator,
+                                         TerminatorStatus& terminator_status,
+                                         CSSPrimitiveValue::UnitType& expect,
+                                         int& value) {
+  const CharacterType* current = string;
+  double local_value;
+  bool negative = false;
+  if (!ParseFloatWithMaxValue<CharacterType>(current, end, 255, local_value,
+                                             negative))
+    return false;
+  if (current == end)
+    return false;
+
   if (expect == CSSPrimitiveValue::UnitType::kPercentage && *current != '%')
     return false;
+  if (expect == CSSPrimitiveValue::UnitType::kNumber && *current == '%')
+    return false;
 
   if (*current == '%') {
     expect = CSSPrimitiveValue::UnitType::kPercentage;
@@ -389,22 +466,8 @@
     expect = CSSPrimitiveValue::UnitType::kNumber;
   }
 
-  while (current != end && IsHTMLSpace<CharacterType>(*current))
-    current++;
-
-  if (current == end || *current != terminator) {
-    if (!should_whitespace_terminate ||
-        !IsHTMLSpace<CharacterType>(*(current - 1))) {
-      return false;
-    }
-  } else if (should_whitespace_terminate && is_first_value) {
-    should_whitespace_terminate = false;
-  } else if (should_whitespace_terminate) {
+  if (!SkipToTerminator(current, end, terminator, terminator_status))
     return false;
-  }
-
-  if (!should_whitespace_terminate)
-    current++;
 
   // Clamp negative values at zero.
   value = negative ? 0 : static_cast<int>(round(local_value));
@@ -412,6 +475,38 @@
   return true;
 }
 
+// Parses a percentage (including the % sign), clamps it and converts it to
+// 0.0..1.0.
+template <typename CharacterType>
+static bool ParsePercentage(const CharacterType*& string,
+                            const CharacterType* end,
+                            const char terminator,
+                            TerminatorStatus& terminator_status,
+                            double& value) {
+  const CharacterType* current = string;
+  bool negative = false;
+  if (!ParseFloatWithMaxValue<CharacterType>(current, end, 100, value,
+                                             negative)) {
+    return false;
+  }
+
+  if (*current != '%')
+    return false;
+
+  ++current;
+  if (negative) {
+    value = 0.0;
+  } else {
+    value = std::min(value * 0.01, 1.0);
+  }
+
+  if (!SkipToTerminator(current, end, terminator, terminator_status))
+    return false;
+
+  string = current;
+  return true;
+}
+
 template <typename CharacterType>
 static inline bool IsTenthAlpha(const CharacterType* string,
                                 const wtf_size_t length) {
@@ -452,7 +547,9 @@
     return false;
 
   if (string[0] != '0' && string[0] != '1' && string[0] != '.') {
-    if (CheckForValidDouble(string, end, false, terminator)) {
+    int length = FindLengthOfValidDouble(string, end);
+    if (length > 0 && ContainsCharAtPos(string, end, length, terminator,
+                                        /*also_accept_whitespace=*/false)) {
       value = negative ? 0 : 255;
       string = end;
       return true;
@@ -477,8 +574,11 @@
   }
 
   double alpha = 0;
-  if (!ParseDouble(string, end, terminator, false, alpha))
+  int dbl_length = ParseDouble(string, end, alpha);
+  if (dbl_length == 0 || !ContainsCharAtPos(string, end, dbl_length, terminator,
+                                            /*also_accept_whitespace=*/false)) {
     return false;
+  }
   value = negative ? 0 : static_cast<int>(round(std::min(alpha, 1.0) * 255.0));
   string = end;
   return true;
@@ -498,12 +598,23 @@
 }
 
 template <typename CharacterType>
+static inline bool MightBeHSLOrHSLA(const CharacterType* characters,
+                                    unsigned length) {
+  if (length < 5)
+    return false;
+  return IsASCIIAlphaCaselessEqual(characters[0], 'h') &&
+         IsASCIIAlphaCaselessEqual(characters[1], 's') &&
+         IsASCIIAlphaCaselessEqual(characters[2], 'l') &&
+         (characters[3] == '(' ||
+          (IsASCIIAlphaCaselessEqual(characters[3], 'a') &&
+           characters[4] == '('));
+}
+
+template <typename CharacterType>
 static bool FastParseColorInternal(RGBA32& rgb,
                                    const CharacterType* characters,
                                    unsigned length,
                                    bool quirks_mode) {
-  CSSPrimitiveValue::UnitType expect = CSSPrimitiveValue::UnitType::kUnknown;
-
   if (length >= 4 && characters[0] == '#')
     return Color::ParseHexColor(characters + 1, length - 1, rgb);
 
@@ -512,7 +623,7 @@
       return true;
   }
 
-  // rgb() and rgba() have the same syntax
+  // rgb() and rgba() have the same syntax.
   if (MightBeRGBOrRGBA(characters, length)) {
     int length_to_add = IsASCIIAlphaCaselessEqual(characters[3], 'a') ? 5 : 4;
     const CharacterType* current = characters + length_to_add;
@@ -522,35 +633,35 @@
     int blue;
     int alpha;
     bool should_have_alpha = false;
-    bool should_whitespace_terminate = true;
-    bool no_whitespace_check = false;
 
-    if (!ParseColorNumberOrPercentage(current, end, ',',
-                                      should_whitespace_terminate,
-                                      true /* is_first_value */, expect, red))
+    TerminatorStatus terminator_status = kCouldWhitespaceTerminate;
+    CSSPrimitiveValue::UnitType expect = CSSPrimitiveValue::UnitType::kUnknown;
+    if (!ParseColorNumberOrPercentage(current, end, ',', terminator_status,
+                                      expect, red)) {
       return false;
-    if (!ParseColorNumberOrPercentage(
-            current, end, ',', should_whitespace_terminate,
-            false /* is_first_value */, expect, green))
+    }
+    if (!ParseColorNumberOrPercentage(current, end, ',', terminator_status,
+                                      expect, green)) {
       return false;
+    }
+
+    TerminatorStatus no_whitespace_check = kMustCharacterTerminate;
     if (!ParseColorNumberOrPercentage(current, end, ',', no_whitespace_check,
-                                      false /* is_first_value */, expect,
-                                      blue)) {
-      // Might have slash as separator
+                                      expect, blue)) {
+      // Might have slash as separator.
       if (ParseColorNumberOrPercentage(current, end, '/', no_whitespace_check,
-                                       false /* is_first_value */, expect,
-                                       blue)) {
-        if (!should_whitespace_terminate)
+                                       expect, blue)) {
+        if (terminator_status != kMustWhitespaceTerminate)
           return false;
         should_have_alpha = true;
       }
-      // Might not have alpha
+      // Might not have alpha.
       else if (!ParseColorNumberOrPercentage(
-                   current, end, ')', no_whitespace_check,
-                   false /* is_first_value */, expect, blue))
+                   current, end, ')', no_whitespace_check, expect, blue)) {
         return false;
+      }
     } else {
-      if (should_whitespace_terminate)
+      if (terminator_status != kMustCharacterTerminate)
         return false;
       should_have_alpha = true;
     }
@@ -558,8 +669,6 @@
     if (should_have_alpha) {
       if (!ParseAlphaValue(current, end, ')', alpha))
         return false;
-      if (current != end)
-        return false;
       rgb = MakeRGBA(red, green, blue, alpha);
     } else {
       if (current != end)
@@ -569,6 +678,112 @@
     return true;
   }
 
+  // hsl() and hsla() also have the same syntax:
+  // https://www.w3.org/TR/css-color-4/#the-hsl-notation
+  // Also for legacy reasons, an hsla() function also exists, with an identical
+  // grammar and behavior to hsl().
+
+  if (MightBeHSLOrHSLA(characters, length)) {
+    int length_to_add = IsASCIIAlphaCaselessEqual(characters[3], 'a') ? 5 : 4;
+    const CharacterType* current = characters + length_to_add;
+    const CharacterType* end = characters + length;
+    bool should_have_alpha = false;
+
+    // Skip any whitespace before the hue.
+    while (current != end && IsHTMLSpace(*current))
+      current++;
+
+    // Find the end of the hue. This isn't optimal, but allows us to reuse
+    // ParseAngle() cleanly.
+    const CharacterType* hue_end = current;
+    while (hue_end != end && !IsHTMLSpace(*hue_end) && *hue_end != ',')
+      hue_end++;
+
+    CSSPrimitiveValue::UnitType hue_unit = CSSPrimitiveValue::UnitType::kNumber;
+    double hue;
+    if (!ParseSimpleAngle(current, static_cast<unsigned>(hue_end - current),
+                          hue_unit, hue)) {
+      return false;
+    }
+
+    // We need to convert the hue to the 0..6 scale that MakeRGBAFromHSLA()
+    // expects.
+    switch (hue_unit) {
+      case CSSPrimitiveValue::UnitType::kNumber:
+      case CSSPrimitiveValue::UnitType::kDegrees:
+        // Unitless numbers are to be treated as degrees.
+        hue *= (6.0 / 360.0);
+        break;
+      case CSSPrimitiveValue::UnitType::kRadians:
+        hue = Rad2deg(hue) * (6.0 / 360.0);
+        break;
+      case CSSPrimitiveValue::UnitType::kGradians:
+        hue = Grad2deg(hue) * (6.0 / 360.0);
+        break;
+      case CSSPrimitiveValue::UnitType::kTurns:
+        hue *= 6.0;
+        break;
+      default:
+        NOTREACHED();
+        return false;
+    }
+
+    // Deal with wraparound so that we end up in 0..6,
+    // roughly analogous to the code in ParseHSLParameters().
+    // Taking these branches should be rare.
+    if (hue < 0.0) {
+      hue = fmod(hue, 6.0) + 6.0;
+    } else if (hue > 6.0) {
+      hue = fmod(hue, 6.0);
+    }
+
+    current = hue_end;
+
+    TerminatorStatus terminator_status = kCouldWhitespaceTerminate;
+    if (!SkipToTerminator(current, end, ',', terminator_status))
+      return false;
+
+    // Saturation and lightness must always be percentages.
+    double saturation;
+    if (!ParsePercentage(current, end, ',', terminator_status, saturation))
+      return false;
+
+    TerminatorStatus no_whitespace_check = kMustCharacterTerminate;
+
+    double lightness;
+    if (!ParsePercentage(current, end, ',', no_whitespace_check, lightness)) {
+      // Might have slash as separator.
+      if (ParsePercentage(current, end, '/', no_whitespace_check, lightness)) {
+        if (terminator_status != kMustWhitespaceTerminate)
+          return false;
+        should_have_alpha = true;
+      }
+      // Might not have alpha.
+      else if (!ParsePercentage(current, end, ')', no_whitespace_check,
+                                lightness)) {
+        return false;
+      }
+    } else {
+      if (terminator_status != kMustCharacterTerminate)
+        return false;
+      should_have_alpha = true;
+    }
+
+    if (should_have_alpha) {
+      int alpha;
+      if (!ParseAlphaValue(current, end, ')', alpha))
+        return false;
+      if (current != end)
+        return false;
+      rgb = MakeRGBAFromHSLA(hue, saturation, lightness, alpha * (1.0 / 255.0));
+    } else {
+      if (current != end)
+        return false;
+      rgb = MakeRGBAFromHSLA(hue, saturation, lightness, 1.0);
+    }
+    return true;
+  }
+
   return false;
 }
 
@@ -590,7 +805,7 @@
   bool quirks_mode = IsQuirksModeBehavior(parser_mode) &&
                      ColorPropertyAllowsQuirkyColor(property_id);
 
-  // Fast path for hex colors and rgb()/rgba() colors
+  // Fast path for hex colors and rgb()/rgba()/hsl()/hsla() colors.
   bool parse_result =
       WTF::VisitCharacters(string, [&](const auto* chars, unsigned length) {
         return FastParseColorInternal(color, chars, length, quirks_mode);
diff --git a/third_party/blink/renderer/core/css/parser/css_parser_fast_paths_test.cc b/third_party/blink/renderer/core/css/parser/css_parser_fast_paths_test.cc
index 6530151..dde37e8 100644
--- a/third_party/blink/renderer/core/css/parser/css_parser_fast_paths_test.cc
+++ b/third_party/blink/renderer/core/css/parser/css_parser_fast_paths_test.cc
@@ -175,7 +175,7 @@
 TEST(CSSParserFastPathsTest, ParseColorWithLargeAlpha) {
   CSSValue* value = CSSParserFastPaths::ParseColor("rgba(0,0,0,1893205797.13)",
                                                    kHTMLStandardMode);
-  EXPECT_NE(nullptr, value);
+  ASSERT_NE(nullptr, value);
   EXPECT_TRUE(value->IsColorValue());
   EXPECT_EQ(Color::kBlack, To<cssvalue::CSSColor>(*value).Value());
 }
@@ -183,27 +183,27 @@
 TEST(CSSParserFastPathsTest, ParseColorWithNewSyntax) {
   CSSValue* value =
       CSSParserFastPaths::ParseColor("rgba(0 0 0)", kHTMLStandardMode);
-  EXPECT_NE(nullptr, value);
+  ASSERT_NE(nullptr, value);
   EXPECT_TRUE(value->IsColorValue());
   EXPECT_EQ(Color::kBlack, To<cssvalue::CSSColor>(*value).Value());
 
   value = CSSParserFastPaths::ParseColor("rgba(0 0 0 / 1)", kHTMLStandardMode);
-  EXPECT_NE(nullptr, value);
+  ASSERT_NE(nullptr, value);
   EXPECT_TRUE(value->IsColorValue());
   EXPECT_EQ(Color::kBlack, To<cssvalue::CSSColor>(*value).Value());
 
   value = CSSParserFastPaths::ParseColor("rgba(0, 0, 0, 1)", kHTMLStandardMode);
-  EXPECT_NE(nullptr, value);
+  ASSERT_NE(nullptr, value);
   EXPECT_TRUE(value->IsColorValue());
   EXPECT_EQ(Color::kBlack, To<cssvalue::CSSColor>(*value).Value());
 
   value = CSSParserFastPaths::ParseColor("RGBA(0 0 0 / 1)", kHTMLStandardMode);
-  EXPECT_NE(nullptr, value);
+  ASSERT_NE(nullptr, value);
   EXPECT_TRUE(value->IsColorValue());
   EXPECT_EQ(Color::kBlack, To<cssvalue::CSSColor>(*value).Value());
 
   value = CSSParserFastPaths::ParseColor("RGB(0 0 0 / 1)", kHTMLStandardMode);
-  EXPECT_NE(nullptr, value);
+  ASSERT_NE(nullptr, value);
   EXPECT_TRUE(value->IsColorValue());
   EXPECT_EQ(Color::kBlack, To<cssvalue::CSSColor>(*value).Value());
 
@@ -224,29 +224,148 @@
 TEST(CSSParserFastPathsTest, ParseColorWithDecimal) {
   CSSValue* value = CSSParserFastPaths::ParseColor("rgba(0.0, 0.0, 0.0, 1.0)",
                                                    kHTMLStandardMode);
-  EXPECT_NE(nullptr, value);
+  ASSERT_NE(nullptr, value);
   EXPECT_TRUE(value->IsColorValue());
   EXPECT_EQ(Color::kBlack, To<cssvalue::CSSColor>(*value).Value());
 
   value =
       CSSParserFastPaths::ParseColor("rgb(0.0, 0.0, 0.0)", kHTMLStandardMode);
-  EXPECT_NE(nullptr, value);
+  ASSERT_NE(nullptr, value);
   EXPECT_TRUE(value->IsColorValue());
   EXPECT_EQ(Color::kBlack, To<cssvalue::CSSColor>(*value).Value());
 
   value =
       CSSParserFastPaths::ParseColor("rgb(0.0 , 0.0,0.0)", kHTMLStandardMode);
-  EXPECT_NE(nullptr, value);
+  ASSERT_NE(nullptr, value);
   EXPECT_TRUE(value->IsColorValue());
   EXPECT_EQ(Color::kBlack, To<cssvalue::CSSColor>(*value).Value());
 
   value = CSSParserFastPaths::ParseColor("rgb(254.5, 254.5, 254.5)",
                                          kHTMLStandardMode);
-  EXPECT_NE(nullptr, value);
+  ASSERT_NE(nullptr, value);
   EXPECT_TRUE(value->IsColorValue());
   EXPECT_EQ(Color::kWhite, To<cssvalue::CSSColor>(*value).Value());
 }
 
+TEST(CSSParserFastPathsTest, ParseHSL) {
+  CSSValue* value =
+      CSSParserFastPaths::ParseColor("hsl(90deg, 50%, 25%)", kHTMLStandardMode);
+  ASSERT_NE(nullptr, value);
+  EXPECT_TRUE(value->IsColorValue());
+  EXPECT_EQ("rgb(64, 96, 32)", value->CssText());
+
+  // Implicit “deg” angle.
+  value =
+      CSSParserFastPaths::ParseColor("hsl(180, 50%, 50%)", kHTMLStandardMode);
+  ASSERT_NE(nullptr, value);
+  EXPECT_TRUE(value->IsColorValue());
+  EXPECT_EQ("rgb(64, 191, 191)", value->CssText());
+
+  // turn.
+  value = CSSParserFastPaths::ParseColor("hsl(0.25turn, 25%, 50%)",
+                                         kHTMLStandardMode);
+  ASSERT_NE(nullptr, value);
+  EXPECT_TRUE(value->IsColorValue());
+  EXPECT_EQ("rgb(128, 159, 96)", value->CssText());
+
+  // rad.
+  value = CSSParserFastPaths::ParseColor("hsl(1.0rad, 50%, 50%)",
+                                         kHTMLStandardMode);
+  ASSERT_NE(nullptr, value);
+  EXPECT_TRUE(value->IsColorValue());
+  EXPECT_EQ("rgb(191, 186, 64)", value->CssText());
+
+  // Wraparound.
+  value = CSSParserFastPaths::ParseColor("hsl(450deg, 50%, 50%)",
+                                         kHTMLStandardMode);
+  ASSERT_NE(nullptr, value);
+  EXPECT_TRUE(value->IsColorValue());
+  EXPECT_EQ("rgb(128, 191, 64)", value->CssText());
+
+  // Lots of wraparound.
+  value = CSSParserFastPaths::ParseColor("hsl(4050deg, 50%, 50%)",
+                                         kHTMLStandardMode);
+  ASSERT_NE(nullptr, value);
+  EXPECT_TRUE(value->IsColorValue());
+  EXPECT_EQ("rgb(128, 191, 64)", value->CssText());
+
+  // Negative wraparound.
+  value = CSSParserFastPaths::ParseColor("hsl(-270deg, 50%, 50%)",
+                                         kHTMLStandardMode);
+  ASSERT_NE(nullptr, value);
+  EXPECT_TRUE(value->IsColorValue());
+  EXPECT_EQ("rgb(128, 191, 64)", value->CssText());
+
+  // Saturation clamping.
+  value = CSSParserFastPaths::ParseColor("hsl(45deg, 150%, 50%)",
+                                         kHTMLStandardMode);
+  ASSERT_NE(nullptr, value);
+  EXPECT_TRUE(value->IsColorValue());
+  EXPECT_EQ("rgb(255, 191, 0)", value->CssText());
+
+  // Lightness clamping to negative.
+  value = CSSParserFastPaths::ParseColor("hsl(45deg, 150%, -1000%)",
+                                         kHTMLStandardMode);
+  ASSERT_NE(nullptr, value);
+  EXPECT_TRUE(value->IsColorValue());
+  EXPECT_EQ("rgb(0, 0, 0)", value->CssText());
+
+  // Writing hsla() without alpha.
+  value = CSSParserFastPaths::ParseColor("hsla(45deg, 150%, 50%)",
+                                         kHTMLStandardMode);
+  ASSERT_NE(nullptr, value);
+  EXPECT_TRUE(value->IsColorValue());
+  EXPECT_EQ("rgb(255, 191, 0)", value->CssText());
+}
+
+TEST(CSSParserFastPathsTest, ParseHSLWithAlpha) {
+  // With alpha, using hsl().
+  CSSValue* value = CSSParserFastPaths::ParseColor("hsl(30 , 1%,75%, 0.5)",
+                                                   kHTMLStandardMode);
+  ASSERT_NE(nullptr, value);
+  EXPECT_TRUE(value->IsColorValue());
+  EXPECT_EQ("rgba(192, 191, 191, 0.5)", value->CssText());
+
+  // With alpha, using hsla().
+  value = CSSParserFastPaths::ParseColor("hsla(30 , 1%,75%, 0.5)",
+                                         kHTMLStandardMode);
+  ASSERT_NE(nullptr, value);
+  EXPECT_TRUE(value->IsColorValue());
+  EXPECT_EQ("rgba(192, 191, 191, 0.5)", value->CssText());
+
+  // With alpha, using space-separated syntax.
+  value = CSSParserFastPaths::ParseColor("hsla(30 1% 75% / 0.1)",
+                                         kHTMLStandardMode);
+  ASSERT_NE(nullptr, value);
+  EXPECT_TRUE(value->IsColorValue());
+  EXPECT_EQ("rgba(192, 191, 191, 0.1)", value->CssText());
+
+  // Clamp alpha.
+  value = CSSParserFastPaths::ParseColor("hsla(30 1% 75% / 1.2)",
+                                         kHTMLStandardMode);
+  ASSERT_NE(nullptr, value);
+  EXPECT_TRUE(value->IsColorValue());
+  EXPECT_EQ("rgb(192, 191, 191)", value->CssText());
+}
+
+TEST(CSSParserFastPathsTest, ParseHSLInvalid) {
+  // Invalid unit.
+  EXPECT_EQ(nullptr, CSSParserFastPaths::ParseColor("hsl(20dag, 50%, 20%)",
+                                                    kHTMLStandardMode));
+
+  // Mix of new and old space syntax.
+  EXPECT_EQ(nullptr, CSSParserFastPaths::ParseColor("hsl(0.2, 50%, 20% 0.3)",
+                                                    kHTMLStandardMode));
+  EXPECT_EQ(nullptr, CSSParserFastPaths::ParseColor("hsl(0.2, 50%, 20% / 0.3)",
+                                                    kHTMLStandardMode));
+  EXPECT_EQ(nullptr, CSSParserFastPaths::ParseColor("hsl(0.2 50% 20%, 0.3)",
+                                                    kHTMLStandardMode));
+
+  // Junk after percentage.
+  EXPECT_EQ(nullptr, CSSParserFastPaths::ParseColor(
+                         "hsl(0.2, 50% foo, 20% 0.3)", kHTMLStandardMode));
+}
+
 TEST(CSSParserFastPathsTest, IsValidKeywordPropertyAndValueOverflowClip) {
   {
     ScopedOverflowClipForTest overflow_clip_feature_enabler(false);