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
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "mozilla/dom/ResponsiveImageSelector.h"
8 #include "mozilla/PresShell.h"
9 #include "mozilla/PresShellInlines.h"
10 #include "mozilla/ServoStyleSetInlines.h"
11 #include "mozilla/TextUtils.h"
12 #include "nsIURI.h"
13 #include "mozilla/dom/Document.h"
14 #include "mozilla/dom/DocumentInlines.h"
15 #include "nsContentUtils.h"
16 #include "nsPresContext.h"
17 
18 #include "nsCSSProps.h"
19 
20 using namespace mozilla;
21 using namespace mozilla::dom;
22 
23 namespace mozilla::dom {
24 
NS_IMPL_CYCLE_COLLECTION(ResponsiveImageSelector,mOwnerNode)25 NS_IMPL_CYCLE_COLLECTION(ResponsiveImageSelector, mOwnerNode)
26 
27 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(ResponsiveImageSelector, AddRef)
28 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(ResponsiveImageSelector, Release)
29 
30 static bool ParseInteger(const nsAString& aString, int32_t& aInt) {
31   nsContentUtils::ParseHTMLIntegerResultFlags parseResult;
32   aInt = nsContentUtils::ParseHTMLInteger(aString, &parseResult);
33   return !(parseResult &
34            (nsContentUtils::eParseHTMLInteger_Error |
35             nsContentUtils::eParseHTMLInteger_DidNotConsumeAllInput |
36             nsContentUtils::eParseHTMLInteger_NonStandard));
37 }
38 
ParseFloat(const nsAString & aString,double & aDouble)39 static bool ParseFloat(const nsAString& aString, double& aDouble) {
40   // Check if it is a valid floating-point number first since the result of
41   // nsString.ToDouble() is more lenient than the spec,
42   // https://html.spec.whatwg.org/#valid-floating-point-number
43   nsAString::const_iterator iter, end;
44   aString.BeginReading(iter);
45   aString.EndReading(end);
46 
47   if (iter == end) {
48     return false;
49   }
50 
51   if (*iter == char16_t('-') && ++iter == end) {
52     return false;
53   }
54 
55   if (IsAsciiDigit(*iter)) {
56     for (; iter != end && IsAsciiDigit(*iter); ++iter)
57       ;
58   } else if (*iter == char16_t('.')) {
59     // Do nothing, jumps to fraction part
60   } else {
61     return false;
62   }
63 
64   // Fraction
65   if (*iter == char16_t('.')) {
66     ++iter;
67     if (iter == end || !IsAsciiDigit(*iter)) {
68       // U+002E FULL STOP character (.) must be followed by one or more ASCII
69       // digits
70       return false;
71     }
72 
73     for (; iter != end && IsAsciiDigit(*iter); ++iter)
74       ;
75   }
76 
77   if (iter != end && (*iter == char16_t('e') || *iter == char16_t('E'))) {
78     ++iter;
79     if (*iter == char16_t('-') || *iter == char16_t('+')) {
80       ++iter;
81     }
82 
83     if (iter == end || !IsAsciiDigit(*iter)) {
84       // Should have one or more ASCII digits
85       return false;
86     }
87 
88     for (; iter != end && IsAsciiDigit(*iter); ++iter)
89       ;
90   }
91 
92   if (iter != end) {
93     return false;
94   }
95 
96   nsresult rv;
97   aDouble = PromiseFlatString(aString).ToDouble(&rv);
98   return NS_SUCCEEDED(rv);
99 }
100 
ResponsiveImageSelector(nsIContent * aContent)101 ResponsiveImageSelector::ResponsiveImageSelector(nsIContent* aContent)
102     : mOwnerNode(aContent), mSelectedCandidateIndex(-1) {}
103 
ResponsiveImageSelector(dom::Document * aDocument)104 ResponsiveImageSelector::ResponsiveImageSelector(dom::Document* aDocument)
105     : mOwnerNode(aDocument), mSelectedCandidateIndex(-1) {}
106 
107 ResponsiveImageSelector::~ResponsiveImageSelector() = default;
108 
ParseSourceSet(const nsAString & aSrcSet,FunctionRef<void (ResponsiveImageCandidate &&)> aCallback)109 void ResponsiveImageSelector::ParseSourceSet(
110     const nsAString& aSrcSet,
111     FunctionRef<void(ResponsiveImageCandidate&&)> aCallback) {
112   nsAString::const_iterator iter, end;
113   aSrcSet.BeginReading(iter);
114   aSrcSet.EndReading(end);
115 
116   // Read URL / descriptor pairs
117   while (iter != end) {
118     nsAString::const_iterator url, urlEnd, descriptor;
119 
120     // Skip whitespace and commas.
121     // Extra commas at this point are a non-fatal syntax error.
122     for (; iter != end &&
123            (nsContentUtils::IsHTMLWhitespace(*iter) || *iter == char16_t(','));
124          ++iter)
125       ;
126 
127     if (iter == end) {
128       break;
129     }
130 
131     url = iter;
132 
133     // Find end of url
134     for (; iter != end && !nsContentUtils::IsHTMLWhitespace(*iter); ++iter)
135       ;
136 
137     // Omit trailing commas from URL.
138     // Multiple commas are a non-fatal error.
139     while (iter != url) {
140       if (*(--iter) != char16_t(',')) {
141         iter++;
142         break;
143       }
144     }
145 
146     const nsDependentSubstring& urlStr = Substring(url, iter);
147 
148     MOZ_ASSERT(url != iter, "Shouldn't have empty URL at this point");
149 
150     ResponsiveImageCandidate candidate;
151     if (candidate.ConsumeDescriptors(iter, end)) {
152       candidate.SetURLSpec(urlStr);
153       aCallback(std::move(candidate));
154     }
155   }
156 }
157 
158 // http://www.whatwg.org/specs/web-apps/current-work/#processing-the-image-candidates
SetCandidatesFromSourceSet(const nsAString & aSrcSet,nsIPrincipal * aTriggeringPrincipal)159 bool ResponsiveImageSelector::SetCandidatesFromSourceSet(
160     const nsAString& aSrcSet, nsIPrincipal* aTriggeringPrincipal) {
161   ClearSelectedCandidate();
162 
163   if (!mOwnerNode || !mOwnerNode->GetBaseURI()) {
164     MOZ_ASSERT(false, "Should not be parsing SourceSet without a document");
165     return false;
166   }
167 
168   mCandidates.Clear();
169 
170   auto eachCandidate = [&](ResponsiveImageCandidate&& aCandidate) {
171     aCandidate.SetTriggeringPrincipal(
172         nsContentUtils::GetAttrTriggeringPrincipal(
173             Content(), aCandidate.URLString(), aTriggeringPrincipal));
174     AppendCandidateIfUnique(std::move(aCandidate));
175   };
176 
177   ParseSourceSet(aSrcSet, eachCandidate);
178 
179   bool parsedCandidates = !mCandidates.IsEmpty();
180 
181   // Re-add default to end of list
182   MaybeAppendDefaultCandidate();
183 
184   return parsedCandidates;
185 }
186 
NumCandidates(bool aIncludeDefault)187 uint32_t ResponsiveImageSelector::NumCandidates(bool aIncludeDefault) {
188   uint32_t candidates = mCandidates.Length();
189 
190   // If present, the default candidate is the last item
191   if (!aIncludeDefault && candidates && mCandidates.LastElement().IsDefault()) {
192     candidates--;
193   }
194 
195   return candidates;
196 }
197 
Content()198 nsIContent* ResponsiveImageSelector::Content() {
199   return mOwnerNode->IsContent() ? mOwnerNode->AsContent() : nullptr;
200 }
201 
Document()202 dom::Document* ResponsiveImageSelector::Document() {
203   return mOwnerNode->OwnerDoc();
204 }
205 
SetDefaultSource(const nsAString & aURLString,nsIPrincipal * aPrincipal)206 void ResponsiveImageSelector::SetDefaultSource(const nsAString& aURLString,
207                                                nsIPrincipal* aPrincipal) {
208   ClearSelectedCandidate();
209 
210   // Check if the last element of our candidates is a default
211   if (!mCandidates.IsEmpty() && mCandidates.LastElement().IsDefault()) {
212     mCandidates.RemoveLastElement();
213   }
214 
215   mDefaultSourceURL = aURLString;
216   mDefaultSourceTriggeringPrincipal = aPrincipal;
217 
218   // Add new default to end of list
219   MaybeAppendDefaultCandidate();
220 }
221 
ClearSelectedCandidate()222 void ResponsiveImageSelector::ClearSelectedCandidate() {
223   mSelectedCandidateIndex = -1;
224   mSelectedCandidateURL = nullptr;
225 }
226 
SetSizesFromDescriptor(const nsAString & aSizes)227 bool ResponsiveImageSelector::SetSizesFromDescriptor(const nsAString& aSizes) {
228   ClearSelectedCandidate();
229 
230   NS_ConvertUTF16toUTF8 sizes(aSizes);
231   mServoSourceSizeList = Servo_SourceSizeList_Parse(&sizes).Consume();
232   return !!mServoSourceSizeList;
233 }
234 
AppendCandidateIfUnique(ResponsiveImageCandidate && aCandidate)235 void ResponsiveImageSelector::AppendCandidateIfUnique(
236     ResponsiveImageCandidate&& aCandidate) {
237   int numCandidates = mCandidates.Length();
238 
239   // With the exception of Default, which should not be added until we are done
240   // building the list.
241   if (aCandidate.IsDefault()) {
242     return;
243   }
244 
245   // Discard candidates with identical parameters, they will never match
246   for (int i = 0; i < numCandidates; i++) {
247     if (mCandidates[i].HasSameParameter(aCandidate)) {
248       return;
249     }
250   }
251 
252   mCandidates.AppendElement(std::move(aCandidate));
253 }
254 
MaybeAppendDefaultCandidate()255 void ResponsiveImageSelector::MaybeAppendDefaultCandidate() {
256   if (mDefaultSourceURL.IsEmpty()) {
257     return;
258   }
259 
260   int numCandidates = mCandidates.Length();
261 
262   // https://html.spec.whatwg.org/multipage/embedded-content.html#update-the-source-set
263   // step 4.1.3:
264   // If child has a src attribute whose value is not the empty string and source
265   // set does not contain an image source with a density descriptor value of 1,
266   // and no image source with a width descriptor, append child's src attribute
267   // value to source set.
268   for (int i = 0; i < numCandidates; i++) {
269     if (mCandidates[i].IsComputedFromWidth()) {
270       return;
271     } else if (mCandidates[i].Density(this) == 1.0) {
272       return;
273     }
274   }
275 
276   ResponsiveImageCandidate defaultCandidate;
277   defaultCandidate.SetParameterDefault();
278   defaultCandidate.SetURLSpec(mDefaultSourceURL);
279   defaultCandidate.SetTriggeringPrincipal(mDefaultSourceTriggeringPrincipal);
280   // We don't use MaybeAppend since we want to keep this even if it can never
281   // match, as it may if the source set changes.
282   mCandidates.AppendElement(std::move(defaultCandidate));
283 }
284 
GetSelectedImageURL()285 already_AddRefed<nsIURI> ResponsiveImageSelector::GetSelectedImageURL() {
286   SelectImage();
287 
288   nsCOMPtr<nsIURI> url = mSelectedCandidateURL;
289   return url.forget();
290 }
291 
GetSelectedImageURLSpec(nsAString & aResult)292 bool ResponsiveImageSelector::GetSelectedImageURLSpec(nsAString& aResult) {
293   SelectImage();
294 
295   if (mSelectedCandidateIndex == -1) {
296     return false;
297   }
298 
299   aResult.Assign(mCandidates[mSelectedCandidateIndex].URLString());
300   return true;
301 }
302 
GetSelectedImageDensity()303 double ResponsiveImageSelector::GetSelectedImageDensity() {
304   int bestIndex = GetSelectedCandidateIndex();
305   if (bestIndex < 0) {
306     return 1.0;
307   }
308 
309   return mCandidates[bestIndex].Density(this);
310 }
311 
GetSelectedImageTriggeringPrincipal()312 nsIPrincipal* ResponsiveImageSelector::GetSelectedImageTriggeringPrincipal() {
313   int bestIndex = GetSelectedCandidateIndex();
314   if (bestIndex < 0) {
315     return nullptr;
316   }
317 
318   return mCandidates[bestIndex].TriggeringPrincipal();
319 }
320 
SelectImage(bool aReselect)321 bool ResponsiveImageSelector::SelectImage(bool aReselect) {
322   if (!aReselect && mSelectedCandidateIndex != -1) {
323     // Already have selection
324     return false;
325   }
326 
327   int oldBest = mSelectedCandidateIndex;
328   ClearSelectedCandidate();
329 
330   int numCandidates = mCandidates.Length();
331   if (!numCandidates) {
332     return oldBest != -1;
333   }
334 
335   dom::Document* doc = Document();
336   nsPresContext* pctx = doc->GetPresContext();
337   nsCOMPtr<nsIURI> baseURI = mOwnerNode->GetBaseURI();
338 
339   if (!pctx || !baseURI) {
340     return oldBest != -1;
341   }
342 
343   double displayDensity = pctx->CSSPixelsToDevPixels(1.0f);
344   double overrideDPPX = pctx->GetOverrideDPPX();
345 
346   if (overrideDPPX > 0) {
347     displayDensity = overrideDPPX;
348   }
349 
350   // Per spec, "In a UA-specific manner, choose one image source"
351   // - For now, select the lowest density greater than displayDensity, otherwise
352   //   the greatest density available
353 
354   // If the list contains computed width candidates, compute the current
355   // effective image width.
356   double computedWidth = -1;
357   for (int i = 0; i < numCandidates; i++) {
358     if (mCandidates[i].IsComputedFromWidth()) {
359       DebugOnly<bool> computeResult =
360           ComputeFinalWidthForCurrentViewport(&computedWidth);
361       MOZ_ASSERT(computeResult,
362                  "Computed candidates not allowed without sizes data");
363       break;
364     }
365   }
366 
367   int bestIndex = -1;
368   double bestDensity = -1.0;
369   for (int i = 0; i < numCandidates; i++) {
370     double candidateDensity = (computedWidth == -1)
371                                   ? mCandidates[i].Density(this)
372                                   : mCandidates[i].Density(computedWidth);
373     // - If bestIndex is below display density, pick anything larger.
374     // - Otherwise, prefer if less dense than bestDensity but still above
375     //   displayDensity.
376     if (bestIndex == -1 ||
377         (bestDensity < displayDensity && candidateDensity > bestDensity) ||
378         (candidateDensity >= displayDensity &&
379          candidateDensity < bestDensity)) {
380       bestIndex = i;
381       bestDensity = candidateDensity;
382     }
383   }
384 
385   MOZ_ASSERT(bestIndex >= 0 && bestIndex < numCandidates);
386 
387   // Resolve URL
388   nsresult rv;
389   const nsAString& urlStr = mCandidates[bestIndex].URLString();
390   nsCOMPtr<nsIURI> candidateURL;
391   rv = nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(candidateURL),
392                                                  urlStr, doc, baseURI);
393 
394   mSelectedCandidateURL = NS_SUCCEEDED(rv) ? candidateURL : nullptr;
395   mSelectedCandidateIndex = bestIndex;
396 
397   return mSelectedCandidateIndex != oldBest;
398 }
399 
GetSelectedCandidateIndex()400 int ResponsiveImageSelector::GetSelectedCandidateIndex() {
401   SelectImage();
402 
403   return mSelectedCandidateIndex;
404 }
405 
ComputeFinalWidthForCurrentViewport(double * aWidth)406 bool ResponsiveImageSelector::ComputeFinalWidthForCurrentViewport(
407     double* aWidth) {
408   dom::Document* doc = Document();
409   PresShell* presShell = doc->GetPresShell();
410   nsPresContext* pctx = presShell ? presShell->GetPresContext() : nullptr;
411 
412   if (!pctx) {
413     return false;
414   }
415   nscoord effectiveWidth =
416       presShell->StyleSet()->EvaluateSourceSizeList(mServoSourceSizeList.get());
417 
418   *aWidth =
419       nsPresContext::AppUnitsToDoubleCSSPixels(std::max(effectiveWidth, 0));
420   return true;
421 }
422 
ResponsiveImageCandidate()423 ResponsiveImageCandidate::ResponsiveImageCandidate() {
424   mType = CandidateType::Invalid;
425   mValue.mDensity = 1.0;
426 }
427 
SetURLSpec(const nsAString & aURLString)428 void ResponsiveImageCandidate::SetURLSpec(const nsAString& aURLString) {
429   mURLString = aURLString;
430 }
431 
SetTriggeringPrincipal(nsIPrincipal * aPrincipal)432 void ResponsiveImageCandidate::SetTriggeringPrincipal(
433     nsIPrincipal* aPrincipal) {
434   mTriggeringPrincipal = aPrincipal;
435 }
436 
SetParameterAsComputedWidth(int32_t aWidth)437 void ResponsiveImageCandidate::SetParameterAsComputedWidth(int32_t aWidth) {
438   mType = CandidateType::ComputedFromWidth;
439   mValue.mWidth = aWidth;
440 }
441 
SetParameterDefault()442 void ResponsiveImageCandidate::SetParameterDefault() {
443   MOZ_ASSERT(!IsValid(), "double setting candidate type");
444 
445   mType = CandidateType::Default;
446   // mValue shouldn't actually be used for this type, but set it to default
447   // anyway
448   mValue.mDensity = 1.0;
449 }
450 
SetParameterInvalid()451 void ResponsiveImageCandidate::SetParameterInvalid() {
452   mType = CandidateType::Invalid;
453   // mValue shouldn't actually be used for this type, but set it to default
454   // anyway
455   mValue.mDensity = 1.0;
456 }
457 
SetParameterAsDensity(double aDensity)458 void ResponsiveImageCandidate::SetParameterAsDensity(double aDensity) {
459   MOZ_ASSERT(!IsValid(), "double setting candidate type");
460 
461   mType = CandidateType::Density;
462   mValue.mDensity = aDensity;
463 }
464 
465 // Represents all supported descriptors for a ResponsiveImageCandidate, though
466 // there is no candidate type that uses all of these. This should generally
467 // match the mValue union of ResponsiveImageCandidate.
468 struct ResponsiveImageDescriptors {
ResponsiveImageDescriptorsmozilla::dom::ResponsiveImageDescriptors469   ResponsiveImageDescriptors() : mInvalid(false){};
470 
471   Maybe<double> mDensity;
472   Maybe<int32_t> mWidth;
473   // We don't support "h" descriptors yet and they are not spec'd, but the
474   // current spec does specify that they can be silently ignored (whereas
475   // entirely unknown descriptors cause us to invalidate the candidate)
476   //
477   // If we ever start honoring them we should serialize them in
478   // AppendDescriptors.
479   Maybe<int32_t> mFutureCompatHeight;
480   // If this descriptor set is bogus, e.g. a value was added twice (and thus
481   // dropped) or an unknown descriptor was added.
482   bool mInvalid;
483 
484   void AddDescriptor(const nsAString& aDescriptor);
485   bool Valid();
486   // Use the current set of descriptors to configure a candidate
487   void FillCandidate(ResponsiveImageCandidate& aCandidate);
488 };
489 
490 // Try to parse a single descriptor from a string. If value already set or
491 // unknown, sets invalid flag.
492 // This corresponds to the descriptor "Descriptor parser" step in:
493 // https://html.spec.whatwg.org/#parse-a-srcset-attribute
AddDescriptor(const nsAString & aDescriptor)494 void ResponsiveImageDescriptors::AddDescriptor(const nsAString& aDescriptor) {
495   if (aDescriptor.IsEmpty()) {
496     return;
497   }
498 
499   // All currently supported descriptors end with an identifying character.
500   nsAString::const_iterator descStart, descType;
501   aDescriptor.BeginReading(descStart);
502   aDescriptor.EndReading(descType);
503   descType--;
504   const nsDependentSubstring& valueStr = Substring(descStart, descType);
505   if (*descType == char16_t('w')) {
506     int32_t possibleWidth;
507     // If the value is not a valid non-negative integer, it doesn't match this
508     // descriptor, fall through.
509     if (ParseInteger(valueStr, possibleWidth) && possibleWidth >= 0) {
510       if (possibleWidth != 0 && mWidth.isNothing() && mDensity.isNothing()) {
511         mWidth.emplace(possibleWidth);
512       } else {
513         // Valid width descriptor, but width or density were already seen, sizes
514         // support isn't enabled, or it parsed to 0, which is an error per spec
515         mInvalid = true;
516       }
517 
518       return;
519     }
520   } else if (*descType == char16_t('h')) {
521     int32_t possibleHeight;
522     // If the value is not a valid non-negative integer, it doesn't match this
523     // descriptor, fall through.
524     if (ParseInteger(valueStr, possibleHeight) && possibleHeight >= 0) {
525       if (possibleHeight != 0 && mFutureCompatHeight.isNothing() &&
526           mDensity.isNothing()) {
527         mFutureCompatHeight.emplace(possibleHeight);
528       } else {
529         // Valid height descriptor, but height or density were already seen, or
530         // it parsed to zero, which is an error per spec
531         mInvalid = true;
532       }
533 
534       return;
535     }
536   } else if (*descType == char16_t('x')) {
537     // If the value is not a valid floating point number, it doesn't match this
538     // descriptor, fall through.
539     double possibleDensity = 0.0;
540     if (ParseFloat(valueStr, possibleDensity)) {
541       if (possibleDensity >= 0.0 && mWidth.isNothing() &&
542           mDensity.isNothing() && mFutureCompatHeight.isNothing()) {
543         mDensity.emplace(possibleDensity);
544       } else {
545         // Valid density descriptor, but height or width or density were already
546         // seen, or it parsed to less than zero, which is an error per spec
547         mInvalid = true;
548       }
549 
550       return;
551     }
552   }
553 
554   // Matched no known descriptor, mark this descriptor set invalid
555   mInvalid = true;
556 }
557 
Valid()558 bool ResponsiveImageDescriptors::Valid() {
559   return !mInvalid && !(mFutureCompatHeight.isSome() && mWidth.isNothing());
560 }
561 
FillCandidate(ResponsiveImageCandidate & aCandidate)562 void ResponsiveImageDescriptors::FillCandidate(
563     ResponsiveImageCandidate& aCandidate) {
564   if (!Valid()) {
565     aCandidate.SetParameterInvalid();
566   } else if (mWidth.isSome()) {
567     MOZ_ASSERT(mDensity.isNothing());  // Shouldn't be valid
568 
569     aCandidate.SetParameterAsComputedWidth(*mWidth);
570   } else if (mDensity.isSome()) {
571     MOZ_ASSERT(mWidth.isNothing());  // Shouldn't be valid
572 
573     aCandidate.SetParameterAsDensity(*mDensity);
574   } else {
575     // A valid set of descriptors with no density nor width (e.g. an empty set)
576     // becomes 1.0 density, per spec
577     aCandidate.SetParameterAsDensity(1.0);
578   }
579 }
580 
ConsumeDescriptors(nsAString::const_iterator & aIter,const nsAString::const_iterator & aIterEnd)581 bool ResponsiveImageCandidate::ConsumeDescriptors(
582     nsAString::const_iterator& aIter,
583     const nsAString::const_iterator& aIterEnd) {
584   nsAString::const_iterator& iter = aIter;
585   const nsAString::const_iterator& end = aIterEnd;
586 
587   bool inParens = false;
588 
589   ResponsiveImageDescriptors descriptors;
590 
591   // Parse descriptor list.
592   // This corresponds to the descriptor parsing loop from:
593   // https://html.spec.whatwg.org/#parse-a-srcset-attribute
594 
595   // Skip initial whitespace
596   for (; iter != end && nsContentUtils::IsHTMLWhitespace(*iter); ++iter)
597     ;
598 
599   nsAString::const_iterator currentDescriptor = iter;
600 
601   for (;; iter++) {
602     if (iter == end) {
603       descriptors.AddDescriptor(Substring(currentDescriptor, iter));
604       break;
605     } else if (inParens) {
606       if (*iter == char16_t(')')) {
607         inParens = false;
608       }
609     } else {
610       if (*iter == char16_t(',')) {
611         // End of descriptors, flush current descriptor and advance past comma
612         // before breaking
613         descriptors.AddDescriptor(Substring(currentDescriptor, iter));
614         iter++;
615         break;
616       }
617       if (nsContentUtils::IsHTMLWhitespace(*iter)) {
618         // End of current descriptor, consume it, skip spaces
619         // ("After descriptor" state in spec) before continuing
620         descriptors.AddDescriptor(Substring(currentDescriptor, iter));
621         for (; iter != end && nsContentUtils::IsHTMLWhitespace(*iter); ++iter)
622           ;
623         if (iter == end) {
624           break;
625         }
626         currentDescriptor = iter;
627         // Leave one whitespace so the loop advances to this position next
628         // iteration
629         iter--;
630       } else if (*iter == char16_t('(')) {
631         inParens = true;
632       }
633     }
634   }
635 
636   descriptors.FillCandidate(*this);
637 
638   return IsValid();
639 }
640 
HasSameParameter(const ResponsiveImageCandidate & aOther) const641 bool ResponsiveImageCandidate::HasSameParameter(
642     const ResponsiveImageCandidate& aOther) const {
643   if (aOther.mType != mType) {
644     return false;
645   }
646 
647   if (mType == CandidateType::Default) {
648     return true;
649   }
650 
651   if (mType == CandidateType::Density) {
652     return aOther.mValue.mDensity == mValue.mDensity;
653   }
654 
655   if (mType == CandidateType::Invalid) {
656     MOZ_ASSERT_UNREACHABLE("Comparing invalid candidates?");
657     return true;
658   }
659 
660   if (mType == CandidateType::ComputedFromWidth) {
661     return aOther.mValue.mWidth == mValue.mWidth;
662   }
663 
664   MOZ_ASSERT(false, "Somebody forgot to check for all uses of this enum");
665   return false;
666 }
667 
Density(ResponsiveImageSelector * aSelector) const668 double ResponsiveImageCandidate::Density(
669     ResponsiveImageSelector* aSelector) const {
670   if (mType == CandidateType::ComputedFromWidth) {
671     double width;
672     if (!aSelector->ComputeFinalWidthForCurrentViewport(&width)) {
673       return 1.0;
674     }
675     return Density(width);
676   }
677 
678   // Other types don't need matching width
679   MOZ_ASSERT(mType == CandidateType::Default || mType == CandidateType::Density,
680              "unhandled candidate type");
681   return Density(-1);
682 }
683 
AppendDescriptors(nsAString & aDescriptors) const684 void ResponsiveImageCandidate::AppendDescriptors(
685     nsAString& aDescriptors) const {
686   MOZ_ASSERT(IsValid());
687   switch (mType) {
688     case CandidateType::Default:
689     case CandidateType::Invalid:
690       return;
691     case CandidateType::ComputedFromWidth:
692       aDescriptors.Append(' ');
693       aDescriptors.AppendInt(mValue.mWidth);
694       aDescriptors.Append('w');
695       return;
696     case CandidateType::Density:
697       aDescriptors.Append(' ');
698       aDescriptors.AppendFloat(mValue.mDensity);
699       aDescriptors.Append('x');
700       return;
701   }
702 }
703 
Density(double aMatchingWidth) const704 double ResponsiveImageCandidate::Density(double aMatchingWidth) const {
705   if (mType == CandidateType::Invalid) {
706     MOZ_ASSERT(false, "Getting density for uninitialized candidate");
707     return 1.0;
708   }
709 
710   if (mType == CandidateType::Default) {
711     return 1.0;
712   }
713 
714   if (mType == CandidateType::Density) {
715     return mValue.mDensity;
716   }
717   if (mType == CandidateType::ComputedFromWidth) {
718     if (aMatchingWidth < 0) {
719       MOZ_ASSERT(
720           false,
721           "Don't expect to have a negative matching width at this point");
722       return 1.0;
723     }
724     double density = double(mValue.mWidth) / aMatchingWidth;
725     MOZ_ASSERT(density > 0.0);
726     return density;
727   }
728 
729   MOZ_ASSERT(false, "Unknown candidate type");
730   return 1.0;
731 }
732 
733 }  // namespace mozilla::dom
734