Bug 1273706 - Part 12: Add a new type for computed CSS values. r?heycam draft
authorJonathan Chan <jyc@eqv.io>
Thu, 18 Aug 2016 15:30:36 -0700
changeset 402901 a83885d31250d03e29caadc1c987d298e75c875a
parent 402900 6508bc57aa61423ba1238ccca5c8471bb4964399
child 402902 2bef8f7bc69d1aaf4da3c7d6cc1050478c1b1dc2
push id26775
push userjchan@mozilla.com
push dateThu, 18 Aug 2016 22:38:41 +0000
reviewersheycam
bugs1273706
milestone51.0a1
Bug 1273706 - Part 12: Add a new type for computed CSS values. r?heycam Previously this was not necessary because computed values for various properties were simply stored with the appropriate type in the corresponding style struct. Now we can have typed custom properties whose names and types are not known until run-time. MozReview-Commit-ID: Kc6KrZK2Dn5
layout/style/CSSComputedValue.cpp
layout/style/CSSComputedValue.h
layout/style/CSSValueType.h
layout/style/moz.build
layout/style/nsCSSValue.cpp
new file mode 100644
--- /dev/null
+++ b/layout/style/CSSComputedValue.cpp
@@ -0,0 +1,493 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/CSSComputedValue.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/StyleAnimationValue.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCSSValue.h"
+#include "nsError.h"
+#include "nsIURI.h"
+#include "nsPresContext.h"
+#include "nsROCSSPrimitiveValue.h"
+
+namespace mozilla {
+void SetValueToStyleImage(const nsStyleImage&, nsROCSSPrimitiveValue*);
+} // namespace mozilla
+
+namespace mozilla {
+
+CSSValueType
+CSSComputedValue::SingleTerm::GetType() const
+{
+  return mType;
+}
+
+// SingleTerm: Encoders
+
+bool
+CSSComputedValue::SingleTerm::GetAsAnimationValue(
+                                StyleAnimationValue& aValue) const
+{
+  // For things that don't support interpolation in the spec or in
+  // StyleAnimationValue right now, we call SetUnparsedStringValue.
+  // UnparsedStringValues are not interpolable (StyleAnimationValue::AddWeighted
+  // returns false) so we should end up doing a 50% flip, but they can be
+  // uncomputed:
+  // StyleAnimationValue::UncomputeValue(nsCSSPropertyID,
+  //                                     const StyleAnimationValue&,
+  //                                     nsAString&)
+  // ... just returns the unparsed string.
+  switch (mType) {
+  case CSSValueType::Length:
+  case CSSValueType::Percentage:
+  case CSSValueType::LengthPercentage:
+  case CSSValueType::Integer:
+    return StyleCoordToValue(mData.as<nsStyleCoord>(), aValue);
+  case CSSValueType::Number:
+    aValue.SetFloatValue(mData.as<nsCSSValue>().GetFloatValue());
+    return true;
+  case CSSValueType::Color: {
+    const ColorValue& value = mData.as<ColorValue>();
+    if (value.mValue.GetUnit() == eCSSUnit_EnumColor &&
+        value.mValue.GetIntValue() == NS_COLOR_CURRENTCOLOR) {
+      aValue.SetCurrentColorValue();
+    } else {
+      aValue.SetColorValue(value.mColor);
+    }
+    return true;
+  }
+  case CSSValueType::TransformFunction: {
+    // The list is a list of transform functions.
+    // Each individual transform is an array.
+    nsCSSValueList* list = new nsCSSValueList;
+    // Copy-assign the nsCSSValue, which AddRefs the underlying array.
+    list->mValue = mData.as<nsCSSValue>();
+    list->mNext = nullptr;
+    // <transform-function> is a single transform function, while normally
+    // StyleAnimationValues has lists of transforms. This is why we have the
+    // weird single-element list.
+    RefPtr<nsCSSValueSharedList> sharedList = new nsCSSValueSharedList(list);
+    aValue.SetTransformValue(sharedList);
+    return true;
+  }
+
+  // When Bug 1277433 lands, we should look at updating the properties we store
+  // as UnparsedStringValues (i.e. non-interpolable properties that are not URLs)
+  // as discretely animated nsCSSValues.
+
+  case CSSValueType::Image:
+    // XXXjyc Implement support for CSS Gradients.
+    // CSS gradients are interpolable, but the spec is still in flux / we don't
+    // have an implementation in StyleAnimationValue.
+    if (mData.is<nsStyleImage>()) {
+      nsString string;
+      AppendToString(string);
+      aValue.SetUnparsedStringValue(string);
+      return true;
+    } else {
+      MOZ_ASSERT(mData.is<nsCSSValue>() &&
+                 mData.as<nsCSSValue>().GetUnit() == eCSSUnit_URL);
+      nsCSSValue* value = new nsCSSValue(mData.as<nsCSSValue>());
+      aValue.SetAndAdoptCSSValueValue(value, StyleAnimationValue::eUnit_URL);
+      return true;
+    }
+
+  case CSSValueType::URL: {
+    MOZ_ASSERT(mData.is<nsCSSValue>() &&
+               mData.as<nsCSSValue>().GetUnit() == eCSSUnit_URL);
+    nsCSSValue* value = new nsCSSValue(mData.as<nsCSSValue>());
+    aValue.SetAndAdoptCSSValueValue(value, StyleAnimationValue::eUnit_URL);
+    return true;
+  }
+
+  case CSSValueType::Angle:
+  case CSSValueType::Time:
+  case CSSValueType::Resolution:
+  case CSSValueType::CustomIdent:
+  case CSSValueType::SpecificIdent:
+  case CSSValueType::TokenStream: {
+    nsString string;
+    AppendToString(string);
+    aValue.SetUnparsedStringValue(string);
+    return true;
+  }
+  case CSSValueType::COUNT:
+    MOZ_ASSERT_UNREACHABLE("COUNT is not a real value.");
+  }
+  MOZ_ASSERT_UNREACHABLE("Shouldn't be here.");
+  return false;
+}
+
+void
+CSSComputedValue::SingleTerm::AppendToString(nsAString& aString) const
+{
+  // See: https://drafts.csswg.org/cssom/#serializing-css-values
+  switch (mType) {
+  case CSSValueType::Length:
+  case CSSValueType::Percentage:
+  case CSSValueType::LengthPercentage:
+  case CSSValueType::Integer: {
+    nsCSSValue value;
+    StyleCoordToCSSValue(mData.as<nsStyleCoord>(), value);
+    value.AppendToString(eCSSProperty_UNKNOWN, aString, nsCSSValue::eNormalized);
+    break;
+  }
+  case CSSValueType::Image: {
+    if (mData.is<nsCSSValue>()) {
+      // See the comment for URL for why we don't just call AppendToString.
+      const nsCSSValue& value = mData.as<nsCSSValue>();
+      nsIURI* uri = value.GetURLValue();
+      nsAutoCString spec;
+      MOZ_ALWAYS_TRUE(NS_SUCCEEDED(uri->GetSpec(spec)));
+      aString.AppendLiteral("url(\"");
+      aString.Append(NS_ConvertUTF8toUTF16(spec));
+      aString.AppendLiteral("\")");
+    } else {
+      // Kind of weird that we have to do this, but don't want to rewrite code.
+      RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
+      SetValueToStyleImage(mData.as<nsStyleImage>(), val);
+      nsString text;
+      ErrorResult error;
+      val->GetCssText(text, error);
+      aString = text;
+      MOZ_ASSERT(!error.Failed());
+    }
+    break;
+  }
+  case CSSValueType::URL: {
+    // We can't use AppendToString because nsCSSValue::AppendToString on URLs
+    // just gives us back the original URL, and we are required to output the
+    // absolutized URL as this is a computed value.
+    // We don't just store a nsIURI* because we want to be able to call
+    // StyleAnimationValue::SetAndAdoptCSSValueValue, and we can't construct a
+    // URLValue from a nsIURI.
+    const nsCSSValue& value = mData.as<nsCSSValue>();
+    nsIURI* uri = value.GetURLValue();
+    nsAutoCString spec;
+    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(uri->GetSpec(spec)));
+    aString.AppendLiteral("url(\"");
+    aString.Append(NS_ConvertUTF8toUTF16(spec));
+    aString.AppendLiteral("\")");
+    break;
+  }
+  case CSSValueType::Color:
+    mData.as<ColorValue>().mValue.AppendToString(eCSSProperty_UNKNOWN, aString,
+                                                 nsCSSValue::eNormalized);
+    break;
+  case CSSValueType::Number:
+  case CSSValueType::Angle:
+  case CSSValueType::Time:
+  case CSSValueType::Resolution:
+  case CSSValueType::TransformFunction:
+  case CSSValueType::CustomIdent:
+  case CSSValueType::SpecificIdent:
+  case CSSValueType::TokenStream:
+    mData.as<nsCSSValue>().AppendToString(eCSSProperty_UNKNOWN, aString,
+                                          nsCSSValue::eNormalized);
+    break;
+  case CSSValueType::COUNT:
+    MOZ_ASSERT_UNREACHABLE("COUNT is not a real value.");
+  }
+}
+
+bool
+CSSComputedValue::SingleTerm::operator==(const SingleTerm& aOther) const
+{
+  return mType == aOther.mType && mData == aOther.mData;
+}
+
+// SingleTerm builders
+
+/* static */ CSSComputedValue::SingleTerm
+CSSComputedValue::Length(const nsStyleCoord& aLength)
+{
+  MOZ_ASSERT(aLength.GetUnit() == eStyleUnit_Coord);
+  SingleTerm term;
+  term.mType = CSSValueType::Length;
+  term.mData = SingleTerm::Data(aLength);
+  return term;
+}
+
+/* static */ CSSComputedValue::SingleTerm
+CSSComputedValue::Number(const nsCSSValue& aValue)
+{
+  MOZ_ASSERT(aValue.GetUnit() == eCSSUnit_Number);
+  SingleTerm term;
+  term.mType = CSSValueType::Number;
+  term.mData = SingleTerm::Data(aValue);
+  return term;
+}
+
+/* static */ CSSComputedValue::SingleTerm
+CSSComputedValue::Percentage(const nsStyleCoord& aPercentage)
+{
+  MOZ_ASSERT(aPercentage.GetUnit() == eStyleUnit_Percent);
+  SingleTerm term;
+  term.mType = CSSValueType::Percentage;
+  term.mData = SingleTerm::Data(aPercentage);
+  return term;
+}
+
+/* static */ CSSComputedValue::SingleTerm
+CSSComputedValue::LengthPercentage(const nsStyleCoord& aLengthPercentage)
+{
+  MOZ_ASSERT(aLengthPercentage.GetUnit() == eStyleUnit_Calc ||
+             aLengthPercentage.GetUnit() == eStyleUnit_Percent ||
+             aLengthPercentage.GetUnit() == eStyleUnit_Coord);
+  SingleTerm term;
+  term.mType = CSSValueType::LengthPercentage;
+  term.mData = SingleTerm::Data(aLengthPercentage);
+  return term;
+}
+
+/* static */ CSSComputedValue::SingleTerm
+CSSComputedValue::Color(const nsCSSValue& aValue, nscolor aColor)
+{
+  MOZ_ASSERT(aValue.IsNumericColorUnit() ||
+             aValue.GetUnit() == eCSSUnit_Ident ||
+             aValue.GetUnit() == eCSSUnit_EnumColor);
+  SingleTerm term;
+  term.mType = CSSValueType::Color;
+  term.mData = SingleTerm::Data(SingleTerm::ColorValue(aValue, aColor));
+  return term;
+}
+
+/* static */ CSSComputedValue::SingleTerm
+CSSComputedValue::Image(const nsStyleImage& aImage)
+{
+  SingleTerm term;
+  term.mType = CSSValueType::Image;
+  term.mData = SingleTerm::Data(aImage);
+  return term;
+}
+
+/* static */ CSSComputedValue::SingleTerm
+CSSComputedValue::Image(const nsCSSValue& aValue)
+{
+  MOZ_ASSERT(aValue.GetUnit() == eCSSUnit_URL);
+  SingleTerm term;
+  term.mType = CSSValueType::Image;
+  term.mData = SingleTerm::Data(aValue);
+  return term;
+}
+
+/* static */ CSSComputedValue::SingleTerm
+CSSComputedValue::URL(const nsCSSValue& aValue)
+{
+  MOZ_ASSERT(aValue.GetUnit() == eCSSUnit_URL);
+  SingleTerm term;
+  term.mType = CSSValueType::URL;
+  term.mData = SingleTerm::Data(aValue);
+  return term;
+}
+
+/* static */ CSSComputedValue::SingleTerm
+CSSComputedValue::Integer(const nsStyleCoord& aInteger)
+{
+  MOZ_ASSERT(aInteger.GetUnit() == eStyleUnit_Integer);
+  SingleTerm term;
+  term.mType = CSSValueType::Integer;
+  term.mData = SingleTerm::Data(aInteger);
+  return term;
+}
+
+/* static */ CSSComputedValue::SingleTerm
+CSSComputedValue::Angle(const nsCSSValue& aValue)
+{
+  MOZ_ASSERT(aValue.IsAngularUnit());
+  SingleTerm term;
+  term.mType = CSSValueType::Angle;
+  term.mData = SingleTerm::Data(aValue);
+  return term;
+}
+
+/* static */ CSSComputedValue::SingleTerm
+CSSComputedValue::Time(const nsCSSValue& aValue)
+{
+  MOZ_ASSERT(aValue.IsTimeUnit());
+  SingleTerm term;
+  term.mType = CSSValueType::Time;
+  term.mData = SingleTerm::Data(aValue);
+  return term;
+}
+
+/* static */ CSSComputedValue::SingleTerm
+CSSComputedValue::Resolution(const nsCSSValue& aValue)
+{
+  MOZ_ASSERT(aValue.IsResolutionUnit());
+  SingleTerm term;
+  term.mType = CSSValueType::Resolution;
+  term.mData = SingleTerm::Data(aValue);
+  return term;
+}
+
+/* static */ CSSComputedValue::SingleTerm
+CSSComputedValue::TransformFunction(const nsCSSValue& aValue)
+{
+  MOZ_ASSERT(aValue.GetUnit() == eCSSUnit_Function);
+  SingleTerm term;
+  term.mType = CSSValueType::TransformFunction;
+  term.mData = SingleTerm::Data(aValue);
+  return term;
+}
+
+/* static */ CSSComputedValue::SingleTerm
+CSSComputedValue::CustomIdent(const nsCSSValue& aValue)
+{
+  MOZ_ASSERT(aValue.GetUnit() == eCSSUnit_Ident);
+  SingleTerm term;
+  term.mType = CSSValueType::CustomIdent;
+  term.mData = SingleTerm::Data(aValue);
+  return term;
+}
+
+/* static */ CSSComputedValue::SingleTerm
+CSSComputedValue::SpecificIdent(const nsCSSValue& aValue)
+{
+  MOZ_ASSERT(aValue.GetUnit() == eCSSUnit_Ident);
+  SingleTerm term;
+  term.mType = CSSValueType::SpecificIdent;
+  term.mData = SingleTerm::Data(aValue);
+  return term;
+}
+
+/* static */ CSSComputedValue::SingleTerm
+CSSComputedValue::TokenStream(const nsCSSValue& aValue)
+{
+  MOZ_ASSERT(aValue.GetUnit() == eCSSUnit_TokenStream);
+  SingleTerm term;
+  term.mType = CSSValueType::TokenStream;
+  term.mData = SingleTerm::Data(aValue);
+  return term;
+}
+
+// ListTerm
+//
+/* static */ CSSComputedValue::ListTerm
+CSSComputedValue::List(nsTArray<SingleTerm>&& aTerms)
+{
+  // <syntax>+ guarantees at least one item.
+  // If an empty string was used:
+  //   --property: ;
+  // ... then that would have to have been a token stream.
+  MOZ_ASSERT(!aTerms.IsEmpty());
+  // Assert that the types are all the same.
+  // Then set mType to that type.
+  CSSValueType type = aTerms[0].GetType();
+#ifdef DEBUG
+  for (size_t i = 1; i < aTerms.Length(); i++) {
+    MOZ_ASSERT(aTerms[i].GetType() == type,
+               "All list term types must be the same.");
+  }
+#endif
+
+  ListTerm term;
+  term.mType = type;
+  term.mTerms = aTerms;
+  return term;
+}
+
+CSSValueType
+CSSComputedValue::ListTerm::GetType() const
+{
+  return mType;
+}
+
+const nsTArray<CSSComputedValue::SingleTerm>&
+CSSComputedValue::ListTerm::GetTerms() const
+{
+  return mTerms;
+}
+
+bool
+CSSComputedValue::ListTerm::GetAsAnimationValue(StyleAnimationValue& aValue) const
+{
+  UniquePtr<nsTArray<StyleAnimationValue>> list
+    (new nsTArray<StyleAnimationValue>(mTerms.Length()));
+  MOZ_ASSERT(mTerms.Length() >= 1);
+  for (size_t i = 0; i < mTerms.Length(); i++) {
+    StyleAnimationValue term;
+    if (!mTerms[i].GetAsAnimationValue(term)) {
+      return false;
+    }
+    list->AppendElement(term);
+  }
+  aValue.SetAndAdoptListValue(Move(list));
+  return true;
+}
+
+void
+CSSComputedValue::ListTerm::AppendToString(nsAString& aString) const
+{
+  for (size_t i = 0; i < mTerms.Length(); i++) {
+    mTerms[i].AppendToString(aString);
+    if (i < mTerms.Length() - 1) {
+      aString.AppendLiteral(" ");
+    }
+  }
+}
+
+bool
+CSSComputedValue::ListTerm::operator==(
+                              const CSSComputedValue::ListTerm& aOther) const
+{
+  return mType == aOther.mType && mTerms == aOther.mTerms;
+}
+
+// CSSComputedValue
+CSSComputedValue::CSSComputedValue(SingleTerm&& aTerm)
+  : mData(aTerm)
+{ }
+
+CSSComputedValue::CSSComputedValue(ListTerm&& aTerms)
+  : mData(aTerms)
+{ }
+
+bool
+CSSComputedValue::IsList() const
+{
+  return mData.is<ListTerm>();
+}
+
+const CSSComputedValue::SingleTerm&
+CSSComputedValue::GetSingle() const
+{
+  return mData.as<SingleTerm>();
+}
+
+const CSSComputedValue::ListTerm&
+CSSComputedValue::GetList() const
+{
+  return mData.as<ListTerm>();
+}
+
+bool
+CSSComputedValue::GetAsAnimationValue(StyleAnimationValue& aValue) const
+{
+  if (mData.is<ListTerm>()) {
+    return mData.as<ListTerm>().GetAsAnimationValue(aValue);
+  }
+  return mData.as<SingleTerm>().GetAsAnimationValue(aValue);
+}
+
+void
+CSSComputedValue::AppendToString(nsAString& aString) const
+{
+  if (mData.is<ListTerm>()) {
+    mData.as<ListTerm>().AppendToString(aString);
+  } else {
+    mData.as<SingleTerm>().AppendToString(aString);
+  }
+}
+
+bool
+CSSComputedValue::operator==(const CSSComputedValue& aOther) const
+{
+  return mData == aOther.mData;
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/layout/style/CSSComputedValue.h
@@ -0,0 +1,161 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_CSSComputedValue_h
+#define mozilla_CSSComputedValue_h
+
+#include "mozilla/CSSValueType.h"
+#include "mozilla/Variant.h"
+#include "nsCOMPtr.h"
+#include "nsCSSKeywords.h"
+#include "nsCSSValue.h"
+#include "nsString.h"
+#include "nsStyleCoord.h"
+#include "nsStyleImage.h"
+
+class nsIURI;
+
+namespace mozilla {
+
+class StyleAnimationValue;
+
+/**
+ * A CSSComputedValue contains the computed value of a registered custom
+ * property. It implements a tagged union, as well as methods for conversion to
+ * StyleAnimationValues (for interpolable values) and to strings (for CSSOM).
+ *
+ * A CSSComputedValue can be either a SingleTerm or a ListTerm, corresponding to
+ * the syntaxes "X" and "X+", respectively. A ListTerm is a homogeneous list of
+ * SingleTerms.
+ */
+class CSSComputedValue
+{
+public:
+  class SingleTerm
+  {
+  public:
+    CSSValueType GetType() const;
+
+    bool GetAsAnimationValue(StyleAnimationValue& aValue) const;
+    void AppendToString(nsAString& aString) const;
+
+    bool operator==(const SingleTerm& aOther) const;
+    bool operator!=(const SingleTerm& aOther) const
+      { return !(*this == aOther); }
+
+  private:
+    SingleTerm()
+      : mData(nsCSSValue()) // Just some garbage null value.
+    { }
+
+    // So we can have the nice static constructors below.
+    friend class CSSComputedValue;
+
+    // Colors are stored as ColorValues instead of just nsCSSValues because we
+    // would need a nsPresContext and nsStyleContext to use
+    // nsRuleNode::ComputeColor and get a nscolor, which we need to create the
+    // StyleAnimationValue for non-numeric, non-currentColor colors like 'red'.
+    // Instead, we compute the nscolor in aColor, but keep the nsCSSValue so we
+    // can keep the information necessary to serialize computed colors properly.
+    struct ColorValue
+    {
+      ColorValue(const nsCSSValue& aValue, nscolor aColor)
+        : mValue(aValue)
+        , mColor(aColor)
+      { }
+
+      bool operator==(const ColorValue& aOther) const
+      {
+        return mValue == aOther.mValue && mColor == aOther.mColor;
+      }
+
+      bool operator!=(const ColorValue& aOther) const
+        { return !(*this == aOther); }
+
+      nsCSSValue mValue;
+      nscolor mColor;
+    };
+
+    typedef Variant<
+      // <length>, <percentage>, <length-percentage>, <integer>
+      nsStyleCoord,
+      // computed value ~ specified value
+      // <transform-function> (with elements computed)
+      nsCSSValue,
+      ColorValue,
+      // <image> except for URLs, which are nsCSSValues
+      nsStyleImage
+    > Data;
+
+    CSSValueType mType;
+    Data mData;
+  };
+
+  static SingleTerm Length(const nsStyleCoord& aLength);
+  static SingleTerm Number(const nsCSSValue& aNumber);
+  static SingleTerm Percentage(const nsStyleCoord& aPercentage);
+  static SingleTerm LengthPercentage(const nsStyleCoord& aLengthPercentage);
+  static SingleTerm Color(const nsCSSValue& aValue, nscolor aColor);
+  static SingleTerm Image(const nsStyleImage& aImage);
+  static SingleTerm Image(const nsCSSValue& aValue);
+  static SingleTerm URL(const nsCSSValue& aValue);
+  static SingleTerm Integer(const nsStyleCoord& aInteger);
+  static SingleTerm Angle(const nsCSSValue& aValue);
+  static SingleTerm Time(const nsCSSValue& aValue);
+  static SingleTerm Resolution(const nsCSSValue& aValue);
+  static SingleTerm TransformFunction(const nsCSSValue& aValue);
+  static SingleTerm CustomIdent(const nsCSSValue& aValue);
+  static SingleTerm SpecificIdent(const nsCSSValue& aValue);
+  static SingleTerm TokenStream(const nsCSSValue& aValue);
+
+  class ListTerm
+  {
+  public:
+    CSSValueType GetType() const;
+    const nsTArray<SingleTerm>& GetTerms() const;
+
+    bool GetAsAnimationValue(StyleAnimationValue& aValue) const;
+    void AppendToString(nsAString& aString) const;
+
+    bool operator==(const ListTerm& aOther) const;
+    bool operator!=(const ListTerm& aOther) const
+      { return !(*this == aOther); }
+
+  private:
+    ListTerm() = default;
+
+    friend class CSSComputedValue;
+
+    CSSValueType mType;
+    nsTArray<SingleTerm> mTerms;
+  };
+
+  // Note: the type of all the SingleTerms must be the same!
+  // See + syntax in Properties & Values API.
+  static ListTerm List(nsTArray<SingleTerm>&& aTerms);
+
+  explicit CSSComputedValue(SingleTerm&& aTerm);
+  explicit CSSComputedValue(ListTerm&& aTerms);
+
+  bool IsList() const;
+
+  const SingleTerm& GetSingle() const;
+  const ListTerm& GetList() const;
+
+  bool GetAsAnimationValue(StyleAnimationValue& aValue) const;
+  void AppendToString(nsAString& aString) const;
+
+  bool operator==(const CSSComputedValue& aOther) const;
+  bool operator!=(const CSSComputedValue& aOther) const
+    { return !(*this == aOther); }
+
+private:
+  Variant<SingleTerm, ListTerm> mData;
+};
+
+}
+
+#endif // mozilla_CSSComputedValue_h
new file mode 100644
--- /dev/null
+++ b/layout/style/CSSValueType.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_CSSValueType_h
+#define mozilla_CSSValueType_h
+
+namespace mozilla {
+
+/**
+ * The type of a registered custom property value.
+ * Note that in many cases multiple CSSValueTypes are valid for a given value --
+ * for example, "Mozilla" could be a CustomIdent or SpecificIdent. Which
+ * CSSValueType is valid depends on the custom property registration (in
+ * particular, its syntax.)
+ * CSSValueTypes are used in CSSVariableSyntax for the types of terms and in
+ * CSSComputedValue.
+ */
+enum class CSSValueType
+{
+  // NB: If you add a new value type here that needs a CSSVariableExprContext, make
+  // sure that any relevant code in AnimValuesStyleRule is updated (and maybe
+  // other places!).
+
+  Length,
+  Number,
+  Percentage,
+  LengthPercentage,
+  Color,
+  Image,
+  URL,
+  Integer,
+  Angle,
+  Time,
+  Resolution,
+  TransformFunction,
+  CustomIdent,
+  SpecificIdent,
+
+  // TokenStream isn't a CSSValueType specified by users (see
+  // CSSVariableSyntax). TokenStream happens if the user specifies '*' as the
+  // syntax and we parse it.
+  TokenStream,
+
+  COUNT
+};
+
+} // namespace mozilla
+
+#endif // mozilla_CSSValueType_h
--- a/layout/style/moz.build
+++ b/layout/style/moz.build
@@ -77,19 +77,21 @@ EXPORTS += [
     'nsStyleStructFwd.h',
     'nsStyleStructInlines.h',
     'nsStyleTransformMatrix.h',
     'nsStyleUtil.h',
 ]
 
 EXPORTS.mozilla += [
     'AnimationCollection.h',
+    'CSSComputedValue.h',
     'CSSEnabledState.h',
     'CSSProperty.h',
     'CSSStyleSheet.h',
+    'CSSValueType.h',
     'CSSVariableDeclarations.h',
     'CSSVariableResolver.h',
     'CSSVariableValues.h',
     'HandleRefPtr.h',
     'IncrementalClearCOMRuleArray.h',
     'LayerAnimationInfo.h',
     'RuleNodeCacheConditions.h',
     'RuleProcessorCache.h',
@@ -136,16 +138,17 @@ EXPORTS.mozilla.css += [
     'StyleRule.h',
 ]
 
 UNIFIED_SOURCES += [
     'AnimationCollection.cpp',
     'AnimationCommon.cpp',
     'CounterStyleManager.cpp',
     'CSS.cpp',
+    'CSSComputedValue.cpp',
     'CSSLexer.cpp',
     'CSSRuleList.cpp',
     'CSSStyleSheet.cpp',
     'CSSVariableDeclarations.cpp',
     'CSSVariableResolver.cpp',
     'CSSVariableValues.cpp',
     'Declaration.cpp',
     'ErrorReporter.cpp',
--- a/layout/style/nsCSSValue.cpp
+++ b/layout/style/nsCSSValue.cpp
@@ -2131,17 +2131,18 @@ AppendValueListToString(const nsCSSValue
                         nsCSSValue::Serialization aSerialization)
 {
   for (;;) {
     val->mValue.AppendToString(aProperty, aResult, aSerialization);
     val = val->mNext;
     if (!val)
       break;
 
-    if (nsCSSProps::PropHasFlags(aProperty,
+    if (aProperty != eCSSProperty_UNKNOWN &&
+        nsCSSProps::PropHasFlags(aProperty,
                                  CSS_PROPERTY_VALUE_LIST_USES_COMMAS))
       aResult.Append(char16_t(','));
     aResult.Append(char16_t(' '));
   }
 }
 
 static void
 AppendGridTemplateToString(const nsCSSValueList* val,
@@ -2529,20 +2530,21 @@ nsCSSValuePairList::AppendToString(nsCSS
         item->mYValue.GetUnit() != eCSSUnit_Null) {
       aResult.Append(char16_t(' '));
       item->mYValue.AppendToString(aProperty, aResult, aSerialization);
     }
     item = item->mNext;
     if (!item)
       break;
 
-    if (nsCSSProps::PropHasFlags(aProperty,
+    if (aProperty != eCSSProperty_UNKNOWN &&
+        (nsCSSProps::PropHasFlags(aProperty,
                                  CSS_PROPERTY_VALUE_LIST_USES_COMMAS) ||
-        aProperty == eCSSProperty_clip_path ||
-        aProperty == eCSSProperty_shape_outside)
+         aProperty == eCSSProperty_clip_path ||
+         aProperty == eCSSProperty_shape_outside))
       aResult.Append(char16_t(','));
     aResult.Append(char16_t(' '));
   }
 }
 
 /* static */ bool
 nsCSSValuePairList::Equal(const nsCSSValuePairList* aList1,
                           const nsCSSValuePairList* aList2)