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