tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

KeyframeUtils.cpp (45345B)


      1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 #include "mozilla/KeyframeUtils.h"
      8 
      9 #include <algorithm>  // For std::stable_sort, std::min
     10 #include <utility>
     11 
     12 #include "js/ForOfIterator.h"  // For JS::ForOfIterator
     13 #include "js/PropertyAndElement.h"  // JS_Enumerate, JS_GetProperty, JS_GetPropertyById
     14 #include "jsapi.h"                  // For most JSAPI
     15 #include "mozilla/CSSPropertyId.h"
     16 #include "mozilla/ComputedStyle.h"
     17 #include "mozilla/ErrorResult.h"
     18 #include "mozilla/ServoBindingTypes.h"
     19 #include "mozilla/ServoBindings.h"
     20 #include "mozilla/ServoCSSParser.h"
     21 #include "mozilla/StaticPrefs_dom.h"
     22 #include "mozilla/StyleAnimationValue.h"
     23 #include "mozilla/TimingParams.h"
     24 #include "mozilla/dom/BaseKeyframeTypesBinding.h"  // For FastBaseKeyframe etc.
     25 #include "mozilla/dom/BindingCallContext.h"
     26 #include "mozilla/dom/Element.h"
     27 #include "mozilla/dom/KeyframeEffect.h"  // For PropertyValuesPair etc.
     28 #include "mozilla/dom/KeyframeEffectBinding.h"
     29 #include "mozilla/dom/Nullable.h"
     30 #include "nsCSSPropertyIDSet.h"
     31 #include "nsCSSProps.h"
     32 #include "nsCSSPseudoElements.h"  // For PseudoStyleType
     33 #include "nsClassHashtable.h"
     34 #include "nsContentUtils.h"  // For GetContextForContent
     35 #include "nsIScriptError.h"
     36 #include "nsPresContextInlines.h"
     37 #include "nsString.h"
     38 #include "nsTArray.h"
     39 
     40 using mozilla::dom::Nullable;
     41 
     42 namespace mozilla {
     43 
     44 // ------------------------------------------------------------------
     45 //
     46 // Internal data types
     47 //
     48 // ------------------------------------------------------------------
     49 
     50 // For the aAllowList parameter of AppendStringOrStringSequence and
     51 // GetPropertyValuesPairs.
     52 enum class ListAllowance { eDisallow, eAllow };
     53 
     54 /**
     55 * A property-values pair obtained from the open-ended properties
     56 * discovered on a regular keyframe or property-indexed keyframe object.
     57 *
     58 * Single values (as required by a regular keyframe, and as also supported
     59 * on property-indexed keyframes) are stored as the only element in
     60 * mValues.
     61 */
     62 struct PropertyValuesPair {
     63  PropertyValuesPair() : mProperty(eCSSProperty_UNKNOWN) {}
     64 
     65  CSSPropertyId mProperty;
     66  nsTArray<nsCString> mValues;
     67 };
     68 
     69 /**
     70 * An additional property (for a property-values pair) found on a
     71 * BaseKeyframe or BasePropertyIndexedKeyframe object.
     72 */
     73 struct AdditionalProperty {
     74  CSSPropertyId mProperty;
     75  size_t mJsidIndex = 0;  // Index into |ids| in GetPropertyValuesPairs.
     76 
     77  struct PropertyComparator {
     78    bool Equals(const AdditionalProperty& aLhs,
     79                const AdditionalProperty& aRhs) const {
     80      return aLhs.mProperty == aRhs.mProperty;
     81    }
     82    bool LessThan(const AdditionalProperty& aLhs,
     83                  const AdditionalProperty& aRhs) const {
     84      bool customLhs = aLhs.mProperty.mId ==
     85                       NonCustomCSSPropertyId::eCSSPropertyExtra_variable;
     86      bool customRhs = aRhs.mProperty.mId ==
     87                       NonCustomCSSPropertyId::eCSSPropertyExtra_variable;
     88      if (!customLhs && !customRhs) {
     89        // Compare by IDL names.
     90        return nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty.mId) <
     91               nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty.mId);
     92      }
     93      if (customLhs && customRhs) {
     94        // Compare by custom property names.
     95        return nsDependentAtomString(aLhs.mProperty.mCustomName) <
     96               nsDependentAtomString(aRhs.mProperty.mCustomName);
     97      }
     98      // Custom properties should be ordered before normal CSS properties, as if
     99      // the custom property name starts with `--`.
    100      // <https://drafts.csswg.org/web-animations-1/#idl-attribute-name-to-animation-property-name>
    101      return !customLhs && customRhs;
    102    }
    103  };
    104 };
    105 
    106 /**
    107 * Data for a segment in a keyframe animation of a given property
    108 * whose value is a StyleAnimationValue.
    109 *
    110 * KeyframeValueEntry is used in GetAnimationPropertiesFromKeyframes
    111 * to gather data for each individual segment.
    112 */
    113 struct KeyframeValueEntry {
    114  KeyframeValueEntry()
    115      : mProperty(eCSSProperty_UNKNOWN), mOffset(), mComposite() {}
    116 
    117  CSSPropertyId mProperty;
    118  AnimationValue mValue;
    119 
    120  float mOffset;
    121  Maybe<StyleComputedTimingFunction> mTimingFunction;
    122  dom::CompositeOperation mComposite;
    123 
    124  struct PropertyOffsetComparator {
    125    static bool Equals(const KeyframeValueEntry& aLhs,
    126                       const KeyframeValueEntry& aRhs) {
    127      return aLhs.mProperty == aRhs.mProperty && aLhs.mOffset == aRhs.mOffset;
    128    }
    129    static bool LessThan(const KeyframeValueEntry& aLhs,
    130                         const KeyframeValueEntry& aRhs) {
    131      // First, sort by property name.
    132      bool customLhs = aLhs.mProperty.mId ==
    133                       NonCustomCSSPropertyId::eCSSPropertyExtra_variable;
    134      bool customRhs = aRhs.mProperty.mId ==
    135                       NonCustomCSSPropertyId::eCSSPropertyExtra_variable;
    136      if (!customLhs && !customRhs) {
    137        // Compare by IDL names.
    138        int32_t order =
    139            nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty.mId) -
    140            nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty.mId);
    141        if (order != 0) {
    142          return order < 0;
    143        }
    144      } else if (customLhs && customRhs) {
    145        // Compare by custom property names.
    146        int order = Compare(nsDependentAtomString(aLhs.mProperty.mCustomName),
    147                            nsDependentAtomString(aRhs.mProperty.mCustomName));
    148        if (order != 0) {
    149          return order < 0;
    150        }
    151      } else {
    152        return !customLhs && customRhs;
    153      }
    154 
    155      // Then, by offset.
    156      return aLhs.mOffset < aRhs.mOffset;
    157    }
    158  };
    159 };
    160 
    161 class ComputedOffsetComparator {
    162 public:
    163  static bool Equals(const Keyframe& aLhs, const Keyframe& aRhs) {
    164    return aLhs.mComputedOffset == aRhs.mComputedOffset;
    165  }
    166 
    167  static bool LessThan(const Keyframe& aLhs, const Keyframe& aRhs) {
    168    return aLhs.mComputedOffset < aRhs.mComputedOffset;
    169  }
    170 };
    171 
    172 // ------------------------------------------------------------------
    173 //
    174 // Internal helper method declarations
    175 //
    176 // ------------------------------------------------------------------
    177 
    178 static void GetKeyframeListFromKeyframeSequence(
    179    JSContext* aCx, dom::Document* aDocument, JS::ForOfIterator& aIterator,
    180    nsTArray<Keyframe>& aResult, const char* aContext, ErrorResult& aRv);
    181 
    182 static bool ConvertKeyframeSequence(JSContext* aCx, dom::Document* aDocument,
    183                                    JS::ForOfIterator& aIterator,
    184                                    const char* aContext,
    185                                    nsTArray<Keyframe>& aResult);
    186 
    187 static bool GetPropertyValuesPairs(JSContext* aCx,
    188                                   JS::Handle<JSObject*> aObject,
    189                                   ListAllowance aAllowLists,
    190                                   nsTArray<PropertyValuesPair>& aResult);
    191 
    192 static bool AppendStringOrStringSequenceToArray(JSContext* aCx,
    193                                                JS::Handle<JS::Value> aValue,
    194                                                ListAllowance aAllowLists,
    195                                                nsTArray<nsCString>& aValues);
    196 
    197 static bool AppendValueAsString(JSContext* aCx, nsTArray<nsCString>& aValues,
    198                                JS::Handle<JS::Value> aValue);
    199 
    200 static Maybe<PropertyValuePair> MakePropertyValuePair(
    201    const CSSPropertyId& aProperty, const nsACString& aStringValue,
    202    dom::Document* aDocument);
    203 
    204 static bool HasValidOffsets(const nsTArray<Keyframe>& aKeyframes);
    205 
    206 #ifdef DEBUG
    207 static void MarkAsComputeValuesFailureKey(PropertyValuePair& aPair);
    208 
    209 #endif
    210 
    211 static nsTArray<ComputedKeyframeValues> GetComputedKeyframeValues(
    212    const nsTArray<Keyframe>& aKeyframes, dom::Element* aElement,
    213    const PseudoStyleRequest& aPseudoRequest,
    214    const ComputedStyle* aComputedValues);
    215 
    216 static void BuildSegmentsFromValueEntries(
    217    nsTArray<KeyframeValueEntry>& aEntries,
    218    nsTArray<AnimationProperty>& aResult);
    219 
    220 static void GetKeyframeListFromPropertyIndexedKeyframe(
    221    JSContext* aCx, dom::Document* aDocument, JS::Handle<JS::Value> aValue,
    222    nsTArray<Keyframe>& aResult, ErrorResult& aRv);
    223 
    224 static void DistributeRange(const Range<Keyframe>& aRange);
    225 
    226 // ------------------------------------------------------------------
    227 //
    228 // Public API
    229 //
    230 // ------------------------------------------------------------------
    231 
    232 /* static */
    233 nsTArray<Keyframe> KeyframeUtils::GetKeyframesFromObject(
    234    JSContext* aCx, dom::Document* aDocument, JS::Handle<JSObject*> aFrames,
    235    const char* aContext, ErrorResult& aRv) {
    236  MOZ_ASSERT(!aRv.Failed());
    237 
    238  nsTArray<Keyframe> keyframes;
    239 
    240  if (!aFrames) {
    241    // The argument was explicitly null meaning no keyframes.
    242    return keyframes;
    243  }
    244 
    245  // At this point we know we have an object. We try to convert it to a
    246  // sequence of keyframes first, and if that fails due to not being iterable,
    247  // we try to convert it to a property-indexed keyframe.
    248  JS::Rooted<JS::Value> objectValue(aCx, JS::ObjectValue(*aFrames));
    249  JS::ForOfIterator iter(aCx);
    250  if (!iter.init(objectValue, JS::ForOfIterator::AllowNonIterable)) {
    251    aRv.Throw(NS_ERROR_FAILURE);
    252    return keyframes;
    253  }
    254 
    255  if (iter.valueIsIterable()) {
    256    GetKeyframeListFromKeyframeSequence(aCx, aDocument, iter, keyframes,
    257                                        aContext, aRv);
    258  } else {
    259    GetKeyframeListFromPropertyIndexedKeyframe(aCx, aDocument, objectValue,
    260                                               keyframes, aRv);
    261  }
    262 
    263  if (aRv.Failed()) {
    264    MOZ_ASSERT(keyframes.IsEmpty(),
    265               "Should not set any keyframes when there is an error");
    266    return keyframes;
    267  }
    268 
    269  return keyframes;
    270 }
    271 
    272 /* static */
    273 void KeyframeUtils::DistributeKeyframes(nsTArray<Keyframe>& aKeyframes) {
    274  if (aKeyframes.IsEmpty()) {
    275    return;
    276  }
    277 
    278  // If the first keyframe has an unspecified offset, fill it in with 0%.
    279  // If there is only a single keyframe, then it gets 100%.
    280  if (aKeyframes.Length() > 1) {
    281    Keyframe& firstElement = aKeyframes[0];
    282    firstElement.mComputedOffset = firstElement.mOffset.valueOr(0.0);
    283    // We will fill in the last keyframe's offset below
    284  } else {
    285    Keyframe& lastElement = aKeyframes.LastElement();
    286    lastElement.mComputedOffset = lastElement.mOffset.valueOr(1.0);
    287  }
    288 
    289  // Fill in remaining missing offsets.
    290  const Keyframe* const last = &aKeyframes.LastElement();
    291  const RangedPtr<Keyframe> begin(aKeyframes.Elements(), aKeyframes.Length());
    292  RangedPtr<Keyframe> keyframeA = begin;
    293  while (keyframeA != last) {
    294    // Find keyframe A and keyframe B *between* which we will apply spacing.
    295    RangedPtr<Keyframe> keyframeB = keyframeA + 1;
    296    while (keyframeB->mOffset.isNothing() && keyframeB != last) {
    297      ++keyframeB;
    298    }
    299    keyframeB->mComputedOffset = keyframeB->mOffset.valueOr(1.0);
    300 
    301    // Fill computed offsets in (keyframe A, keyframe B).
    302    DistributeRange(Range<Keyframe>(keyframeA, keyframeB + 1));
    303    keyframeA = keyframeB;
    304  }
    305 }
    306 
    307 /* static */
    308 nsTArray<AnimationProperty> KeyframeUtils::GetAnimationPropertiesFromKeyframes(
    309    const nsTArray<Keyframe>& aKeyframes, dom::Element* aElement,
    310    const PseudoStyleRequest& aPseudoRequest, const ComputedStyle* aStyle,
    311    dom::CompositeOperation aEffectComposite) {
    312  nsTArray<AnimationProperty> result;
    313 
    314  const nsTArray<ComputedKeyframeValues> computedValues =
    315      GetComputedKeyframeValues(aKeyframes, aElement, aPseudoRequest, aStyle);
    316  if (computedValues.IsEmpty()) {
    317    // In rare cases GetComputedKeyframeValues might fail and return an empty
    318    // array, in which case we likewise return an empty array from here.
    319    return result;
    320  }
    321 
    322  MOZ_ASSERT(aKeyframes.Length() == computedValues.Length(),
    323             "Array length mismatch");
    324 
    325  nsTArray<KeyframeValueEntry> entries(aKeyframes.Length());
    326 
    327  const size_t len = aKeyframes.Length();
    328  for (size_t i = 0; i < len; ++i) {
    329    const Keyframe& frame = aKeyframes[i];
    330    for (auto& value : computedValues[i]) {
    331      MOZ_ASSERT(frame.mComputedOffset != Keyframe::kComputedOffsetNotSet,
    332                 "Invalid computed offset");
    333      KeyframeValueEntry* entry = entries.AppendElement();
    334      entry->mOffset = frame.mComputedOffset;
    335      entry->mProperty = value.mProperty;
    336      entry->mValue = value.mValue;
    337      entry->mTimingFunction = frame.mTimingFunction;
    338      // The following assumes that CompositeOperation is a strict subset of
    339      // CompositeOperationOrAuto.
    340      entry->mComposite =
    341          frame.mComposite == dom::CompositeOperationOrAuto::Auto
    342              ? aEffectComposite
    343              : static_cast<dom::CompositeOperation>(frame.mComposite);
    344    }
    345  }
    346 
    347  BuildSegmentsFromValueEntries(entries, result);
    348  return result;
    349 }
    350 
    351 /* static */
    352 bool KeyframeUtils::IsAnimatableProperty(const CSSPropertyId& aProperty) {
    353  // Regardless of the backend type, treat the 'display' property as not
    354  // animatable. (Servo will report it as being animatable, since it is
    355  // in fact animatable by SMIL.)
    356  if (aProperty.mId == eCSSProperty_display) {
    357    return false;
    358  }
    359  return Servo_Property_IsAnimatable(&aProperty);
    360 }
    361 
    362 // ------------------------------------------------------------------
    363 //
    364 // Internal helpers
    365 //
    366 // ------------------------------------------------------------------
    367 
    368 /**
    369 * Converts a JS object to an IDL sequence<Keyframe>.
    370 *
    371 * @param aCx The JSContext corresponding to |aIterator|.
    372 * @param aDocument The document to use when parsing CSS properties.
    373 * @param aIterator An already-initialized ForOfIterator for the JS
    374 *   object to iterate over as a sequence.
    375 * @param aResult The array into which the resulting Keyframe objects will be
    376 *   appended.
    377 * @param aContext The context string to prepend to thrown exceptions.
    378 * @param aRv Out param to store any errors thrown by this function.
    379 */
    380 static void GetKeyframeListFromKeyframeSequence(
    381    JSContext* aCx, dom::Document* aDocument, JS::ForOfIterator& aIterator,
    382    nsTArray<Keyframe>& aResult, const char* aContext, ErrorResult& aRv) {
    383  MOZ_ASSERT(!aRv.Failed());
    384  MOZ_ASSERT(aResult.IsEmpty());
    385 
    386  // Convert the object in aIterator to a sequence of keyframes producing
    387  // an array of Keyframe objects.
    388  if (!ConvertKeyframeSequence(aCx, aDocument, aIterator, aContext, aResult)) {
    389    aResult.Clear();
    390    aRv.NoteJSContextException(aCx);
    391    return;
    392  }
    393 
    394  // If the sequence<> had zero elements, we won't generate any
    395  // keyframes.
    396  if (aResult.IsEmpty()) {
    397    return;
    398  }
    399 
    400  // Check that the keyframes are loosely sorted and with values all
    401  // between 0% and 100%.
    402  if (!HasValidOffsets(aResult)) {
    403    aResult.Clear();
    404    aRv.ThrowTypeError<dom::MSG_INVALID_KEYFRAME_OFFSETS>();
    405    return;
    406  }
    407 }
    408 
    409 /**
    410 * Converts a JS object wrapped by the given JS::ForIfIterator to an
    411 * IDL sequence<Keyframe> and stores the resulting Keyframe objects in
    412 * aResult.
    413 */
    414 static bool ConvertKeyframeSequence(JSContext* aCx, dom::Document* aDocument,
    415                                    JS::ForOfIterator& aIterator,
    416                                    const char* aContext,
    417                                    nsTArray<Keyframe>& aResult) {
    418  JS::Rooted<JS::Value> value(aCx);
    419  // Parsing errors should only be reported after we have finished iterating
    420  // through all values. If we have any early returns while iterating, we should
    421  // ignore parsing errors.
    422  IgnoredErrorResult parseEasingResult;
    423 
    424  for (;;) {
    425    bool done;
    426    if (!aIterator.next(&value, &done)) {
    427      return false;
    428    }
    429    if (done) {
    430      break;
    431    }
    432    // Each value found when iterating the object must be an object
    433    // or null/undefined (which gets treated as a default {} dictionary
    434    // value).
    435    if (!value.isObject() && !value.isNullOrUndefined()) {
    436      dom::ThrowErrorMessage<dom::MSG_NOT_OBJECT>(
    437          aCx, aContext, "Element of sequence<Keyframe> argument");
    438      return false;
    439    }
    440 
    441    // Convert the JS value into a BaseKeyframe dictionary value.
    442    dom::binding_detail::FastBaseKeyframe keyframeDict;
    443    dom::BindingCallContext callCx(aCx, aContext);
    444    if (!keyframeDict.Init(callCx, value,
    445                           "Element of sequence<Keyframe> argument")) {
    446      // This may happen if the value type of the member of BaseKeyframe is
    447      // invalid. e.g. `offset` only accept a double value, so if we provide a
    448      // string, we enter this branch.
    449      // Besides, keyframeDict.Init() should throw a Type Error message already,
    450      // so we don't have to do it again.
    451      return false;
    452    }
    453 
    454    Keyframe* keyframe = aResult.AppendElement(fallible);
    455    if (!keyframe) {
    456      return false;
    457    }
    458 
    459    if (!keyframeDict.mOffset.IsNull()) {
    460      keyframe->mOffset.emplace(keyframeDict.mOffset.Value());
    461    }
    462 
    463    keyframe->mComposite = keyframeDict.mComposite;
    464 
    465    // Look for additional property-values pairs on the object.
    466    nsTArray<PropertyValuesPair> propertyValuePairs;
    467    if (value.isObject()) {
    468      JS::Rooted<JSObject*> object(aCx, &value.toObject());
    469      if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eDisallow,
    470                                  propertyValuePairs)) {
    471        return false;
    472      }
    473    }
    474 
    475    if (!parseEasingResult.Failed()) {
    476      keyframe->mTimingFunction =
    477          TimingParams::ParseEasing(keyframeDict.mEasing, parseEasingResult);
    478      // Even if the above fails, we still need to continue reading off all the
    479      // properties since checking the validity of easing should be treated as
    480      // a separate step that happens *after* all the other processing in this
    481      // loop since (since it is never likely to be handled by WebIDL unlike the
    482      // rest of this loop).
    483    }
    484 
    485    for (PropertyValuesPair& pair : propertyValuePairs) {
    486      MOZ_ASSERT(pair.mValues.Length() == 1);
    487 
    488      Maybe<PropertyValuePair> valuePair =
    489          MakePropertyValuePair(pair.mProperty, pair.mValues[0], aDocument);
    490      if (!valuePair) {
    491        continue;
    492      }
    493      keyframe->mPropertyValues.AppendElement(std::move(valuePair.ref()));
    494 
    495 #ifdef DEBUG
    496      // When we go to convert keyframes into arrays of property values we
    497      // call StyleAnimation::ComputeValues. This should normally return true
    498      // but in order to test the case where it does not, BaseKeyframeDict
    499      // includes a chrome-only member that can be set to indicate that
    500      // ComputeValues should fail for shorthand property values on that
    501      // keyframe.
    502      if (nsCSSProps::IsShorthand(pair.mProperty.mId) &&
    503          keyframeDict.mSimulateComputeValuesFailure) {
    504        MarkAsComputeValuesFailureKey(keyframe->mPropertyValues.LastElement());
    505      }
    506 #endif
    507    }
    508  }
    509 
    510  // Throw any errors we encountered while parsing 'easing' properties.
    511  if (parseEasingResult.MaybeSetPendingException(aCx)) {
    512    return false;
    513  }
    514 
    515  return true;
    516 }
    517 
    518 /**
    519 * Reads the property-values pairs from the specified JS object.
    520 *
    521 * @param aObject The JS object to look at.
    522 * @param aAllowLists If eAllow, values will be converted to
    523 *   (DOMString or sequence<DOMString); if eDisallow, values
    524 *   will be converted to DOMString.
    525 * @param aResult The array into which the enumerated property-values
    526 *   pairs will be stored.
    527 * @return false on failure or JS exception thrown while interacting
    528 *   with aObject; true otherwise.
    529 */
    530 static bool GetPropertyValuesPairs(JSContext* aCx,
    531                                   JS::Handle<JSObject*> aObject,
    532                                   ListAllowance aAllowLists,
    533                                   nsTArray<PropertyValuesPair>& aResult) {
    534  nsTArray<AdditionalProperty> properties;
    535 
    536  // Iterate over all the properties on aObject and append an
    537  // entry to properties for them.
    538  //
    539  // We don't compare the jsids that we encounter with those for
    540  // the explicit dictionary members, since we know that none
    541  // of the CSS property IDL names clash with them.
    542  JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
    543  if (!JS_Enumerate(aCx, aObject, &ids)) {
    544    return false;
    545  }
    546  for (size_t i = 0, n = ids.length(); i < n; i++) {
    547    nsAutoJSCString propName;
    548    if (!propName.init(aCx, ids[i])) {
    549      return false;
    550    }
    551 
    552    // Basically, we have to handle "cssOffset" and "cssFloat" specially here:
    553    // "cssOffset" => eCSSProperty_offset
    554    // "cssFloat"  => eCSSProperty_float
    555    // This means if the attribute is the string "cssOffset"/"cssFloat", we use
    556    // CSS "offset"/"float" property.
    557    // https://drafts.csswg.org/web-animations/#property-name-conversion
    558    NonCustomCSSPropertyId propertyId =
    559        NonCustomCSSPropertyId::eCSSProperty_UNKNOWN;
    560    if (nsCSSProps::IsCustomPropertyName(propName)) {
    561      propertyId = eCSSPropertyExtra_variable;
    562    } else if (propName.EqualsLiteral("cssOffset")) {
    563      propertyId = NonCustomCSSPropertyId::eCSSProperty_offset;
    564    } else if (propName.EqualsLiteral("cssFloat")) {
    565      propertyId = NonCustomCSSPropertyId::eCSSProperty_float;
    566    } else if (!propName.EqualsLiteral("offset") &&
    567               !propName.EqualsLiteral("float")) {
    568      propertyId = nsCSSProps::LookupPropertyByIDLName(
    569          propName, CSSEnabledState::ForAllContent);
    570    }
    571 
    572    auto property = CSSPropertyId::FromIdOrCustomProperty(propertyId, propName);
    573 
    574    if (KeyframeUtils::IsAnimatableProperty(property)) {
    575      properties.AppendElement(AdditionalProperty{std::move(property), i});
    576    }
    577  }
    578 
    579  // Sort the entries by IDL name and then get each value and
    580  // convert it either to a DOMString or to a
    581  // (DOMString or sequence<DOMString>), depending on aAllowLists,
    582  // and build up aResult.
    583  properties.Sort(AdditionalProperty::PropertyComparator());
    584 
    585  for (AdditionalProperty& p : properties) {
    586    JS::Rooted<JS::Value> value(aCx);
    587    if (!JS_GetPropertyById(aCx, aObject, ids[p.mJsidIndex], &value)) {
    588      return false;
    589    }
    590    PropertyValuesPair* pair = aResult.AppendElement();
    591    pair->mProperty = p.mProperty;
    592    if (!AppendStringOrStringSequenceToArray(aCx, value, aAllowLists,
    593                                             pair->mValues)) {
    594      return false;
    595    }
    596  }
    597 
    598  return true;
    599 }
    600 
    601 /**
    602 * Converts aValue to DOMString, if aAllowLists is eDisallow, or
    603 * to (DOMString or sequence<DOMString>) if aAllowLists is aAllow.
    604 * The resulting strings are appended to aValues.
    605 */
    606 static bool AppendStringOrStringSequenceToArray(JSContext* aCx,
    607                                                JS::Handle<JS::Value> aValue,
    608                                                ListAllowance aAllowLists,
    609                                                nsTArray<nsCString>& aValues) {
    610  if (aAllowLists == ListAllowance::eAllow && aValue.isObject()) {
    611    // The value is an object, and we want to allow lists; convert
    612    // aValue to (DOMString or sequence<DOMString>).
    613    JS::ForOfIterator iter(aCx);
    614    if (!iter.init(aValue, JS::ForOfIterator::AllowNonIterable)) {
    615      return false;
    616    }
    617    if (iter.valueIsIterable()) {
    618      // If the object is iterable, convert it to sequence<DOMString>.
    619      JS::Rooted<JS::Value> element(aCx);
    620      for (;;) {
    621        bool done;
    622        if (!iter.next(&element, &done)) {
    623          return false;
    624        }
    625        if (done) {
    626          break;
    627        }
    628        if (!AppendValueAsString(aCx, aValues, element)) {
    629          return false;
    630        }
    631      }
    632      return true;
    633    }
    634  }
    635 
    636  // Either the object is not iterable, or aAllowLists doesn't want
    637  // a list; convert it to DOMString.
    638  if (!AppendValueAsString(aCx, aValues, aValue)) {
    639    return false;
    640  }
    641 
    642  return true;
    643 }
    644 
    645 /**
    646 * Converts aValue to DOMString and appends it to aValues.
    647 */
    648 static bool AppendValueAsString(JSContext* aCx, nsTArray<nsCString>& aValues,
    649                                JS::Handle<JS::Value> aValue) {
    650  return ConvertJSValueToString(aCx, aValue, dom::eStringify, dom::eStringify,
    651                                *aValues.AppendElement());
    652 }
    653 
    654 static void ReportInvalidPropertyValueToConsole(
    655    const CSSPropertyId& aProperty, const nsACString& aInvalidPropertyValue,
    656    dom::Document* aDoc) {
    657  AutoTArray<nsString, 2> params;
    658  params.AppendElement(NS_ConvertUTF8toUTF16(aInvalidPropertyValue));
    659  aProperty.ToString(*params.AppendElement());
    660  nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Animation"_ns,
    661                                  aDoc, nsContentUtils::eDOM_PROPERTIES,
    662                                  "InvalidKeyframePropertyValue", params);
    663 }
    664 
    665 /**
    666 * Construct a PropertyValuePair parsing the given string into a suitable
    667 * nsCSSValue object.
    668 *
    669 * @param aProperty The CSS property.
    670 * @param aStringValue The property value to parse.
    671 * @param aDocument The document to use when parsing.
    672 * @return The constructed PropertyValuePair, or Nothing() if |aStringValue| is
    673 *   an invalid property value.
    674 */
    675 static Maybe<PropertyValuePair> MakePropertyValuePair(
    676    const CSSPropertyId& aProperty, const nsACString& aStringValue,
    677    dom::Document* aDocument) {
    678  MOZ_ASSERT(aDocument);
    679  Maybe<PropertyValuePair> result;
    680 
    681  ServoCSSParser::ParsingEnvironment env =
    682      ServoCSSParser::GetParsingEnvironment(aDocument);
    683  RefPtr<StyleLockedDeclarationBlock> servoDeclarationBlock =
    684      ServoCSSParser::ParseProperty(aProperty, aStringValue, env,
    685                                    StyleParsingMode::DEFAULT);
    686 
    687  if (servoDeclarationBlock) {
    688    result.emplace(aProperty, std::move(servoDeclarationBlock));
    689  } else {
    690    ReportInvalidPropertyValueToConsole(aProperty, aStringValue, aDocument);
    691  }
    692  return result;
    693 }
    694 
    695 /**
    696 * Checks that the given keyframes are loosely ordered (each keyframe's
    697 * offset that is not null is greater than or equal to the previous
    698 * non-null offset) and that all values are within the range [0.0, 1.0].
    699 *
    700 * @return true if the keyframes' offsets are correctly ordered and
    701 *   within range; false otherwise.
    702 */
    703 static bool HasValidOffsets(const nsTArray<Keyframe>& aKeyframes) {
    704  double offset = 0.0;
    705  for (const Keyframe& keyframe : aKeyframes) {
    706    if (keyframe.mOffset) {
    707      double thisOffset = keyframe.mOffset.value();
    708      if (thisOffset < offset || thisOffset > 1.0f) {
    709        return false;
    710      }
    711      offset = thisOffset;
    712    }
    713  }
    714  return true;
    715 }
    716 
    717 #ifdef DEBUG
    718 /**
    719 * Takes a property-value pair for a shorthand property and modifies the
    720 * value to indicate that when we call StyleAnimationValue::ComputeValues on
    721 * that value we should behave as if that function had failed.
    722 *
    723 * @param aPair The PropertyValuePair to modify. |aPair.mProperty| must be
    724 *              a shorthand property.
    725 */
    726 static void MarkAsComputeValuesFailureKey(PropertyValuePair& aPair) {
    727  MOZ_ASSERT(nsCSSProps::IsShorthand(aPair.mProperty.mId),
    728             "Only shorthand property values can be marked as failure values");
    729 
    730  aPair.mSimulateComputeValuesFailure = true;
    731 }
    732 
    733 #endif
    734 
    735 /**
    736 * The variation of the above function. This is for Servo backend.
    737 */
    738 static nsTArray<ComputedKeyframeValues> GetComputedKeyframeValues(
    739    const nsTArray<Keyframe>& aKeyframes, dom::Element* aElement,
    740    const PseudoStyleRequest& aPseudoRequest,
    741    const ComputedStyle* aComputedStyle) {
    742  MOZ_ASSERT(aElement);
    743 
    744  nsTArray<ComputedKeyframeValues> result;
    745 
    746  nsPresContext* presContext = nsContentUtils::GetContextForContent(aElement);
    747  if (!presContext) {
    748    // This has been reported to happen with some combinations of content
    749    // (particularly involving resize events and layout flushes? See bug 1407898
    750    // and bug 1408420) but no reproducible steps have been found.
    751    // For now we just return an empty array.
    752    return result;
    753  }
    754 
    755  result = presContext->StyleSet()->GetComputedKeyframeValuesFor(
    756      aKeyframes, aElement, aPseudoRequest, aComputedStyle);
    757  return result;
    758 }
    759 
    760 static void AppendInitialSegment(AnimationProperty* aAnimationProperty,
    761                                 const KeyframeValueEntry& aFirstEntry) {
    762  AnimationPropertySegment* segment =
    763      aAnimationProperty->mSegments.AppendElement();
    764  segment->mFromKey = 0.0f;
    765  segment->mToKey = aFirstEntry.mOffset;
    766  segment->mToValue = aFirstEntry.mValue;
    767  segment->mToComposite = aFirstEntry.mComposite;
    768 }
    769 
    770 static void AppendFinalSegment(AnimationProperty* aAnimationProperty,
    771                               const KeyframeValueEntry& aLastEntry) {
    772  AnimationPropertySegment* segment =
    773      aAnimationProperty->mSegments.AppendElement();
    774  segment->mFromKey = aLastEntry.mOffset;
    775  segment->mFromValue = aLastEntry.mValue;
    776  segment->mFromComposite = aLastEntry.mComposite;
    777  segment->mToKey = 1.0f;
    778  segment->mTimingFunction = aLastEntry.mTimingFunction;
    779 }
    780 
    781 // Returns a newly created AnimationProperty if one was created to fill-in the
    782 // missing keyframe, nullptr otherwise (if we decided not to fill the keyframe
    783 // becase we don't support implicit keyframes).
    784 static AnimationProperty* HandleMissingInitialKeyframe(
    785    nsTArray<AnimationProperty>& aResult, const KeyframeValueEntry& aEntry) {
    786  MOZ_ASSERT(aEntry.mOffset != 0.0f,
    787             "The offset of the entry should not be 0.0");
    788 
    789  AnimationProperty* result = aResult.AppendElement();
    790  result->mProperty = aEntry.mProperty;
    791 
    792  AppendInitialSegment(result, aEntry);
    793 
    794  return result;
    795 }
    796 
    797 static void HandleMissingFinalKeyframe(
    798    nsTArray<AnimationProperty>& aResult, const KeyframeValueEntry& aEntry,
    799    AnimationProperty* aCurrentAnimationProperty) {
    800  MOZ_ASSERT(aEntry.mOffset != 1.0f,
    801             "The offset of the entry should not be 1.0");
    802 
    803  // If |aCurrentAnimationProperty| is nullptr, that means this is the first
    804  // entry for the property, we have to append a new AnimationProperty for this
    805  // property.
    806  if (!aCurrentAnimationProperty) {
    807    aCurrentAnimationProperty = aResult.AppendElement();
    808    aCurrentAnimationProperty->mProperty = aEntry.mProperty;
    809 
    810    // If we have only one entry whose offset is neither 1 nor 0 for this
    811    // property, we need to append the initial segment as well.
    812    if (aEntry.mOffset != 0.0f) {
    813      AppendInitialSegment(aCurrentAnimationProperty, aEntry);
    814    }
    815  }
    816  AppendFinalSegment(aCurrentAnimationProperty, aEntry);
    817 }
    818 
    819 /**
    820 * Builds an array of AnimationProperty objects to represent the keyframe
    821 * animation segments in aEntries.
    822 */
    823 static void BuildSegmentsFromValueEntries(
    824    nsTArray<KeyframeValueEntry>& aEntries,
    825    nsTArray<AnimationProperty>& aResult) {
    826  if (aEntries.IsEmpty()) {
    827    return;
    828  }
    829 
    830  // Sort the KeyframeValueEntry objects so that all entries for a given
    831  // property are together, and the entries are sorted by offset otherwise.
    832  std::stable_sort(aEntries.begin(), aEntries.end(),
    833                   &KeyframeValueEntry::PropertyOffsetComparator::LessThan);
    834 
    835  // For a given index i, we want to generate a segment from aEntries[i]
    836  // to aEntries[j], if:
    837  //
    838  //   * j > i,
    839  //   * aEntries[i + 1]'s offset/property is different from aEntries[i]'s, and
    840  //   * aEntries[j - 1]'s offset/property is different from aEntries[j]'s.
    841  //
    842  // That will eliminate runs of same offset/property values where there's no
    843  // point generating zero length segments in the middle of the animation.
    844  //
    845  // Additionally we need to generate a zero length segment at offset 0 and at
    846  // offset 1, if we have multiple values for a given property at that offset,
    847  // since we need to retain the very first and very last value so they can
    848  // be used for reverse and forward filling.
    849  //
    850  // Typically, for each property in |aEntries|, we expect there to be at least
    851  // one KeyframeValueEntry with offset 0.0, and at least one with offset 1.0.
    852  // However, since it is possible that when building |aEntries|, the call to
    853  // StyleAnimationValue::ComputeValues might fail, this can't be guaranteed.
    854  // Furthermore, if additive animation is disabled, the following loop takes
    855  // care to identify properties that lack a value at offset 0.0/1.0 and drops
    856  // those properties from |aResult|.
    857 
    858  CSSPropertyId lastProperty(eCSSProperty_UNKNOWN);
    859  AnimationProperty* animationProperty = nullptr;
    860 
    861  size_t i = 0, n = aEntries.Length();
    862 
    863  while (i < n) {
    864    // If we've reached the end of the array of entries, synthesize a final (and
    865    // initial) segment if necessary.
    866    if (i + 1 == n) {
    867      if (aEntries[i].mOffset != 1.0f) {
    868        HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty);
    869      } else if (aEntries[i].mOffset == 1.0f && !animationProperty) {
    870        // If the last entry with offset 1 and no animation property, that means
    871        // it is the only entry for this property so append a single segment
    872        // from 0 offset to |aEntry[i].offset|.
    873        (void)HandleMissingInitialKeyframe(aResult, aEntries[i]);
    874      }
    875      animationProperty = nullptr;
    876      break;
    877    }
    878 
    879    MOZ_ASSERT(
    880        aEntries[i].mProperty.IsValid() && aEntries[i + 1].mProperty.IsValid(),
    881        "Each entry should specify a valid property");
    882 
    883    // No keyframe for this property at offset 0.
    884    if (aEntries[i].mProperty != lastProperty && aEntries[i].mOffset != 0.0f) {
    885      // If we don't support additive animation we can't fill in the missing
    886      // keyframes and we should just skip this property altogether. Since the
    887      // entries are sorted by offset for a given property, and since we don't
    888      // update |lastProperty|, we will keep hitting this condition until we
    889      // change property.
    890      animationProperty = HandleMissingInitialKeyframe(aResult, aEntries[i]);
    891      if (animationProperty) {
    892        lastProperty = aEntries[i].mProperty;
    893      } else {
    894        // Skip this entry if we did not handle the missing entry.
    895        ++i;
    896        continue;
    897      }
    898    }
    899 
    900    // Skip this entry if the next entry has the same offset except for initial
    901    // and final ones. We will handle missing keyframe in the next loop
    902    // if the property is changed on the next entry.
    903    if (aEntries[i].mProperty == aEntries[i + 1].mProperty &&
    904        aEntries[i].mOffset == aEntries[i + 1].mOffset &&
    905        aEntries[i].mOffset != 1.0f && aEntries[i].mOffset != 0.0f) {
    906      ++i;
    907      continue;
    908    }
    909 
    910    // No keyframe for this property at offset 1.
    911    if (aEntries[i].mProperty != aEntries[i + 1].mProperty &&
    912        aEntries[i].mOffset != 1.0f) {
    913      HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty);
    914      // Move on to new property.
    915      animationProperty = nullptr;
    916      ++i;
    917      continue;
    918    }
    919 
    920    // Starting from i + 1, determine the next [i, j] interval from which to
    921    // generate a segment. Basically, j is i + 1, but there are some special
    922    // cases for offset 0 and 1, so we need to handle them specifically.
    923    // Note: From this moment, we make sure [i + 1] is valid and
    924    //       there must be an initial entry (i.e. mOffset = 0.0) and
    925    //       a final entry (i.e. mOffset = 1.0). Besides, all the entries
    926    //       with the same offsets except for initial/final ones are filtered
    927    //       out already.
    928    size_t j = i + 1;
    929    if (aEntries[i].mOffset == 0.0f && aEntries[i + 1].mOffset == 0.0f) {
    930      // We need to generate an initial zero-length segment.
    931      MOZ_ASSERT(aEntries[i].mProperty == aEntries[i + 1].mProperty);
    932      while (j + 1 < n && aEntries[j + 1].mOffset == 0.0f &&
    933             aEntries[j + 1].mProperty == aEntries[j].mProperty) {
    934        ++j;
    935      }
    936    } else if (aEntries[i].mOffset == 1.0f) {
    937      if (aEntries[i + 1].mOffset == 1.0f &&
    938          aEntries[i + 1].mProperty == aEntries[i].mProperty) {
    939        // We need to generate a final zero-length segment.
    940        while (j + 1 < n && aEntries[j + 1].mOffset == 1.0f &&
    941               aEntries[j + 1].mProperty == aEntries[j].mProperty) {
    942          ++j;
    943        }
    944      } else {
    945        // New property.
    946        MOZ_ASSERT(aEntries[i].mProperty != aEntries[i + 1].mProperty);
    947        animationProperty = nullptr;
    948        ++i;
    949        continue;
    950      }
    951    }
    952 
    953    // If we've moved on to a new property, create a new AnimationProperty
    954    // to insert segments into.
    955    if (aEntries[i].mProperty != lastProperty) {
    956      MOZ_ASSERT(aEntries[i].mOffset == 0.0f);
    957      MOZ_ASSERT(!animationProperty);
    958      animationProperty = aResult.AppendElement();
    959      animationProperty->mProperty = aEntries[i].mProperty;
    960      lastProperty = aEntries[i].mProperty;
    961    }
    962 
    963    MOZ_ASSERT(animationProperty, "animationProperty should be valid pointer.");
    964 
    965    // Now generate the segment.
    966    AnimationPropertySegment* segment =
    967        animationProperty->mSegments.AppendElement();
    968    segment->mFromKey = aEntries[i].mOffset;
    969    segment->mToKey = aEntries[j].mOffset;
    970    segment->mFromValue = aEntries[i].mValue;
    971    segment->mToValue = aEntries[j].mValue;
    972    segment->mTimingFunction = aEntries[i].mTimingFunction;
    973    segment->mFromComposite = aEntries[i].mComposite;
    974    segment->mToComposite = aEntries[j].mComposite;
    975 
    976    i = j;
    977  }
    978 }
    979 
    980 /**
    981 * Converts a JS object representing a property-indexed keyframe into
    982 * an array of Keyframe objects.
    983 *
    984 * @param aCx The JSContext for |aValue|.
    985 * @param aDocument The document to use when parsing CSS properties.
    986 * @param aValue The JS object.
    987 * @param aResult The array into which the resulting AnimationProperty
    988 *   objects will be appended.
    989 * @param aRv Out param to store any errors thrown by this function.
    990 */
    991 static void GetKeyframeListFromPropertyIndexedKeyframe(
    992    JSContext* aCx, dom::Document* aDocument, JS::Handle<JS::Value> aValue,
    993    nsTArray<Keyframe>& aResult, ErrorResult& aRv) {
    994  MOZ_ASSERT(aValue.isObject());
    995  MOZ_ASSERT(aResult.IsEmpty());
    996  MOZ_ASSERT(!aRv.Failed());
    997 
    998  // Convert the object to a property-indexed keyframe dictionary to
    999  // get its explicit dictionary members.
   1000  dom::binding_detail::FastBasePropertyIndexedKeyframe keyframeDict;
   1001  // XXXbz Pass in the method name from callers and set up a BindingCallContext?
   1002  if (!keyframeDict.Init(aCx, aValue, "BasePropertyIndexedKeyframe argument")) {
   1003    aRv.Throw(NS_ERROR_FAILURE);
   1004    return;
   1005  }
   1006 
   1007  // Get all the property--value-list pairs off the object.
   1008  JS::Rooted<JSObject*> object(aCx, &aValue.toObject());
   1009  nsTArray<PropertyValuesPair> propertyValuesPairs;
   1010  if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eAllow,
   1011                              propertyValuesPairs)) {
   1012    aRv.Throw(NS_ERROR_FAILURE);
   1013    return;
   1014  }
   1015 
   1016  // Create a set of keyframes for each property.
   1017  nsTHashMap<nsFloatHashKey, Keyframe> processedKeyframes;
   1018  for (const PropertyValuesPair& pair : propertyValuesPairs) {
   1019    size_t count = pair.mValues.Length();
   1020    if (count == 0) {
   1021      // No animation values for this property.
   1022      continue;
   1023    }
   1024 
   1025    size_t n = pair.mValues.Length() - 1;
   1026    size_t i = 0;
   1027 
   1028    for (const nsCString& stringValue : pair.mValues) {
   1029      // For single-valued lists, the single value should be added to a
   1030      // keyframe with offset 1.
   1031      double offset = n ? i++ / double(n) : 1;
   1032      Keyframe& keyframe = processedKeyframes.LookupOrInsert(offset);
   1033      if (keyframe.mPropertyValues.IsEmpty()) {
   1034        keyframe.mComputedOffset = offset;
   1035      }
   1036 
   1037      Maybe<PropertyValuePair> valuePair =
   1038          MakePropertyValuePair(pair.mProperty, stringValue, aDocument);
   1039      if (!valuePair) {
   1040        continue;
   1041      }
   1042      keyframe.mPropertyValues.AppendElement(std::move(valuePair.ref()));
   1043    }
   1044  }
   1045 
   1046  aResult.SetCapacity(processedKeyframes.Count());
   1047  std::transform(processedKeyframes.begin(), processedKeyframes.end(),
   1048                 MakeBackInserter(aResult), [](auto& entry) {
   1049                   return std::move(*entry.GetModifiableData());
   1050                 });
   1051 
   1052  aResult.Sort(ComputedOffsetComparator());
   1053 
   1054  // Fill in any specified offsets
   1055  //
   1056  // This corresponds to step 5, "Otherwise," branch, substeps 5-6 of
   1057  // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
   1058  const FallibleTArray<Nullable<double>>* offsets = nullptr;
   1059  AutoTArray<Nullable<double>, 1> singleOffset;
   1060  auto& offset = keyframeDict.mOffset;
   1061  if (offset.IsDouble()) {
   1062    singleOffset.AppendElement(offset.GetAsDouble());
   1063    // dom::Sequence is a fallible but AutoTArray is infallible and we need to
   1064    // point to one or the other. Fortunately, fallible and infallible array
   1065    // types can be implicitly converted provided they are const.
   1066    const FallibleTArray<Nullable<double>>& asFallibleArray = singleOffset;
   1067    offsets = &asFallibleArray;
   1068  } else if (offset.IsDoubleOrNullSequence()) {
   1069    offsets = &offset.GetAsDoubleOrNullSequence();
   1070  }
   1071  // If offset.IsNull() is true, then we want to leave the mOffset member of
   1072  // each keyframe with its initialized value of null. By leaving |offsets|
   1073  // as nullptr here, we skip updating mOffset below.
   1074 
   1075  size_t offsetsToFill =
   1076      offsets ? std::min(offsets->Length(), aResult.Length()) : 0;
   1077  for (size_t i = 0; i < offsetsToFill; i++) {
   1078    if (!offsets->ElementAt(i).IsNull()) {
   1079      aResult[i].mOffset.emplace(offsets->ElementAt(i).Value());
   1080    }
   1081  }
   1082 
   1083  // Check that the keyframes are loosely sorted and that any specified offsets
   1084  // are between 0.0 and 1.0 inclusive.
   1085  //
   1086  // This corresponds to steps 6-7 of
   1087  // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
   1088  //
   1089  // In the spec, TypeErrors arising from invalid offsets and easings are thrown
   1090  // at the end of the procedure since it assumes we initially store easing
   1091  // values as strings and then later parse them.
   1092  //
   1093  // However, we will parse easing members immediately when we process them
   1094  // below. In order to maintain the relative order in which TypeErrors are
   1095  // thrown according to the spec, namely exceptions arising from invalid
   1096  // offsets are thrown before exceptions arising from invalid easings, we check
   1097  // the offsets here.
   1098  if (!HasValidOffsets(aResult)) {
   1099    aResult.Clear();
   1100    aRv.ThrowTypeError<dom::MSG_INVALID_KEYFRAME_OFFSETS>();
   1101    return;
   1102  }
   1103 
   1104  // Fill in any easings.
   1105  //
   1106  // This corresponds to step 5, "Otherwise," branch, substeps 7-11 of
   1107  // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
   1108  FallibleTArray<Maybe<StyleComputedTimingFunction>> easings;
   1109  auto parseAndAppendEasing = [&](const nsACString& easingString,
   1110                                  ErrorResult& aRv) {
   1111    auto easing = TimingParams::ParseEasing(easingString, aRv);
   1112    if (!aRv.Failed() && !easings.AppendElement(std::move(easing), fallible)) {
   1113      aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
   1114    }
   1115  };
   1116 
   1117  auto& easing = keyframeDict.mEasing;
   1118  if (easing.IsUTF8String()) {
   1119    parseAndAppendEasing(easing.GetAsUTF8String(), aRv);
   1120    if (aRv.Failed()) {
   1121      aResult.Clear();
   1122      return;
   1123    }
   1124  } else {
   1125    for (const auto& easingString : easing.GetAsUTF8StringSequence()) {
   1126      parseAndAppendEasing(easingString, aRv);
   1127      if (aRv.Failed()) {
   1128        aResult.Clear();
   1129        return;
   1130      }
   1131    }
   1132  }
   1133 
   1134  // If |easings| is empty, then we are supposed to fill it in with the value
   1135  // "linear" and then repeat the list as necessary.
   1136  //
   1137  // However, for Keyframe.mTimingFunction we represent "linear" as a None
   1138  // value. Since we have not assigned 'mTimingFunction' for any of the
   1139  // keyframes in |aResult| they will already have their initial None value
   1140  // (i.e. linear). As a result, if |easings| is empty, we don't need to do
   1141  // anything.
   1142  if (!easings.IsEmpty()) {
   1143    for (size_t i = 0; i < aResult.Length(); i++) {
   1144      aResult[i].mTimingFunction = easings[i % easings.Length()];
   1145    }
   1146  }
   1147 
   1148  // Fill in any composite operations.
   1149  //
   1150  // This corresponds to step 5, "Otherwise," branch, substep 12 of
   1151  // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
   1152  const FallibleTArray<dom::CompositeOperationOrAuto>* compositeOps = nullptr;
   1153  AutoTArray<dom::CompositeOperationOrAuto, 1> singleCompositeOp;
   1154  auto& composite = keyframeDict.mComposite;
   1155  if (composite.IsCompositeOperationOrAuto()) {
   1156    singleCompositeOp.AppendElement(composite.GetAsCompositeOperationOrAuto());
   1157    const FallibleTArray<dom::CompositeOperationOrAuto>& asFallibleArray =
   1158        singleCompositeOp;
   1159    compositeOps = &asFallibleArray;
   1160  } else if (composite.IsCompositeOperationOrAutoSequence()) {
   1161    compositeOps = &composite.GetAsCompositeOperationOrAutoSequence();
   1162  }
   1163 
   1164  // Fill in and repeat as needed.
   1165  if (compositeOps && !compositeOps->IsEmpty()) {
   1166    size_t length = compositeOps->Length();
   1167    for (size_t i = 0; i < aResult.Length(); i++) {
   1168      aResult[i].mComposite = compositeOps->ElementAt(i % length);
   1169    }
   1170  }
   1171 }
   1172 
   1173 /**
   1174 * Distribute the offsets of all keyframes in between the endpoints of the
   1175 * given range.
   1176 *
   1177 * @param aRange The sequence of keyframes between whose endpoints we should
   1178 * distribute offsets.
   1179 */
   1180 static void DistributeRange(const Range<Keyframe>& aRange) {
   1181  const Range<Keyframe> rangeToAdjust =
   1182      Range<Keyframe>(aRange.begin() + 1, aRange.end() - 1);
   1183  const size_t n = aRange.length() - 1;
   1184  const double startOffset = aRange[0].mComputedOffset;
   1185  const double diffOffset = aRange[n].mComputedOffset - startOffset;
   1186  for (auto iter = rangeToAdjust.begin(); iter != rangeToAdjust.end(); ++iter) {
   1187    size_t index = iter - aRange.begin();
   1188    iter->mComputedOffset = startOffset + double(index) / n * diffOffset;
   1189  }
   1190 }
   1191 
   1192 }  // namespace mozilla