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