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/. */
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"
13 #include "mozilla/dom/Document.h"
14 #include "mozilla/dom/DocumentInlines.h"
15 #include "nsContentUtils.h"
16 #include "nsPresContext.h"
18 #include "nsCSSProps.h"
20 using namespace mozilla
;
21 using namespace mozilla::dom
;
23 namespace mozilla::dom
{
25 NS_IMPL_CYCLE_COLLECTION(ResponsiveImageSelector
, mOwnerNode
)
27 static bool ParseInteger(const nsAString
& aString
, int32_t& aInt
) {
28 nsContentUtils::ParseHTMLIntegerResultFlags parseResult
;
29 aInt
= nsContentUtils::ParseHTMLInteger(aString
, &parseResult
);
30 return !(parseResult
&
31 (nsContentUtils::eParseHTMLInteger_Error
|
32 nsContentUtils::eParseHTMLInteger_DidNotConsumeAllInput
|
33 nsContentUtils::eParseHTMLInteger_NonStandard
));
36 static bool ParseFloat(const nsAString
& aString
, double& aDouble
) {
37 // Check if it is a valid floating-point number first since the result of
38 // nsString.ToDouble() is more lenient than the spec,
39 // https://html.spec.whatwg.org/#valid-floating-point-number
40 nsAString::const_iterator iter
, end
;
41 aString
.BeginReading(iter
);
42 aString
.EndReading(end
);
48 if (*iter
== char16_t('-') && ++iter
== end
) {
52 if (IsAsciiDigit(*iter
)) {
53 for (; iter
!= end
&& IsAsciiDigit(*iter
); ++iter
)
55 } else if (*iter
== char16_t('.')) {
56 // Do nothing, jumps to fraction part
62 if (*iter
== char16_t('.')) {
64 if (iter
== end
|| !IsAsciiDigit(*iter
)) {
65 // U+002E FULL STOP character (.) must be followed by one or more ASCII
70 for (; iter
!= end
&& IsAsciiDigit(*iter
); ++iter
)
74 if (iter
!= end
&& (*iter
== char16_t('e') || *iter
== char16_t('E'))) {
76 if (*iter
== char16_t('-') || *iter
== char16_t('+')) {
80 if (iter
== end
|| !IsAsciiDigit(*iter
)) {
81 // Should have one or more ASCII digits
85 for (; iter
!= end
&& IsAsciiDigit(*iter
); ++iter
)
94 aDouble
= PromiseFlatString(aString
).ToDouble(&rv
);
95 return NS_SUCCEEDED(rv
);
98 ResponsiveImageSelector::ResponsiveImageSelector(nsIContent
* aContent
)
99 : mOwnerNode(aContent
), mSelectedCandidateIndex(-1) {}
101 ResponsiveImageSelector::ResponsiveImageSelector(dom::Document
* aDocument
)
102 : mOwnerNode(aDocument
), mSelectedCandidateIndex(-1) {}
104 ResponsiveImageSelector::~ResponsiveImageSelector() = default;
106 void ResponsiveImageSelector::ParseSourceSet(
107 const nsAString
& aSrcSet
,
108 FunctionRef
<void(ResponsiveImageCandidate
&&)> aCallback
) {
109 nsAString::const_iterator iter
, end
;
110 aSrcSet
.BeginReading(iter
);
111 aSrcSet
.EndReading(end
);
113 // Read URL / descriptor pairs
114 while (iter
!= end
) {
115 nsAString::const_iterator url
, urlEnd
, descriptor
;
117 // Skip whitespace and commas.
118 // Extra commas at this point are a non-fatal syntax error.
119 for (; iter
!= end
&&
120 (nsContentUtils::IsHTMLWhitespace(*iter
) || *iter
== char16_t(','));
131 for (; iter
!= end
&& !nsContentUtils::IsHTMLWhitespace(*iter
); ++iter
)
134 // Omit trailing commas from URL.
135 // Multiple commas are a non-fatal error.
136 while (iter
!= url
) {
137 if (*(--iter
) != char16_t(',')) {
143 const nsDependentSubstring
& urlStr
= Substring(url
, iter
);
145 MOZ_ASSERT(url
!= iter
, "Shouldn't have empty URL at this point");
147 ResponsiveImageCandidate candidate
;
148 if (candidate
.ConsumeDescriptors(iter
, end
)) {
149 candidate
.SetURLSpec(urlStr
);
150 aCallback(std::move(candidate
));
155 // http://www.whatwg.org/specs/web-apps/current-work/#processing-the-image-candidates
156 bool ResponsiveImageSelector::SetCandidatesFromSourceSet(
157 const nsAString
& aSrcSet
, nsIPrincipal
* aTriggeringPrincipal
) {
158 ClearSelectedCandidate();
160 if (!mOwnerNode
|| !mOwnerNode
->GetBaseURI()) {
161 MOZ_ASSERT(false, "Should not be parsing SourceSet without a document");
167 auto eachCandidate
= [&](ResponsiveImageCandidate
&& aCandidate
) {
168 aCandidate
.SetTriggeringPrincipal(
169 nsContentUtils::GetAttrTriggeringPrincipal(
170 Content(), aCandidate
.URLString(), aTriggeringPrincipal
));
171 AppendCandidateIfUnique(std::move(aCandidate
));
174 ParseSourceSet(aSrcSet
, eachCandidate
);
176 bool parsedCandidates
= !mCandidates
.IsEmpty();
178 // Re-add default to end of list
179 MaybeAppendDefaultCandidate();
181 return parsedCandidates
;
184 uint32_t ResponsiveImageSelector::NumCandidates(bool aIncludeDefault
) {
185 uint32_t candidates
= mCandidates
.Length();
187 // If present, the default candidate is the last item
188 if (!aIncludeDefault
&& candidates
&& mCandidates
.LastElement().IsDefault()) {
195 nsIContent
* ResponsiveImageSelector::Content() {
196 return mOwnerNode
->IsContent() ? mOwnerNode
->AsContent() : nullptr;
199 dom::Document
* ResponsiveImageSelector::Document() {
200 return mOwnerNode
->OwnerDoc();
203 void ResponsiveImageSelector::ClearDefaultSource() {
204 ClearSelectedCandidate();
205 // Check if the last element of our candidates is a default
206 if (!mCandidates
.IsEmpty() && mCandidates
.LastElement().IsDefault()) {
207 mCandidates
.RemoveLastElement();
211 void ResponsiveImageSelector::SetDefaultSource(nsIURI
* aURI
,
212 nsIPrincipal
* aPrincipal
) {
213 ClearDefaultSource();
214 mDefaultSourceTriggeringPrincipal
= aPrincipal
;
215 mDefaultSourceURL
= VoidString();
219 CopyUTF8toUTF16(spec
, mDefaultSourceURL
);
221 MaybeAppendDefaultCandidate();
224 void ResponsiveImageSelector::SetDefaultSource(const nsAString
& aURLString
,
225 nsIPrincipal
* aPrincipal
) {
226 ClearDefaultSource();
227 mDefaultSourceTriggeringPrincipal
= aPrincipal
;
228 mDefaultSourceURL
= aURLString
;
229 MaybeAppendDefaultCandidate();
232 void ResponsiveImageSelector::ClearSelectedCandidate() {
233 mSelectedCandidateIndex
= -1;
234 mSelectedCandidateURL
= nullptr;
237 bool ResponsiveImageSelector::SetSizesFromDescriptor(const nsAString
& aSizes
) {
238 ClearSelectedCandidate();
240 NS_ConvertUTF16toUTF8
sizes(aSizes
);
241 mServoSourceSizeList
.reset(Servo_SourceSizeList_Parse(&sizes
));
242 return !!mServoSourceSizeList
;
245 void ResponsiveImageSelector::AppendCandidateIfUnique(
246 ResponsiveImageCandidate
&& aCandidate
) {
247 int numCandidates
= mCandidates
.Length();
249 // With the exception of Default, which should not be added until we are done
250 // building the list.
251 if (aCandidate
.IsDefault()) {
255 // Discard candidates with identical parameters, they will never match
256 for (int i
= 0; i
< numCandidates
; i
++) {
257 if (mCandidates
[i
].HasSameParameter(aCandidate
)) {
262 mCandidates
.AppendElement(std::move(aCandidate
));
265 void ResponsiveImageSelector::MaybeAppendDefaultCandidate() {
266 if (mDefaultSourceURL
.IsEmpty()) {
270 int numCandidates
= mCandidates
.Length();
272 // https://html.spec.whatwg.org/multipage/embedded-content.html#update-the-source-set
274 // If child has a src attribute whose value is not the empty string and source
275 // set does not contain an image source with a density descriptor value of 1,
276 // and no image source with a width descriptor, append child's src attribute
277 // value to source set.
278 for (int i
= 0; i
< numCandidates
; i
++) {
279 if (mCandidates
[i
].IsComputedFromWidth()) {
281 } else if (mCandidates
[i
].Density(this) == 1.0) {
286 ResponsiveImageCandidate defaultCandidate
;
287 defaultCandidate
.SetParameterDefault();
288 defaultCandidate
.SetURLSpec(mDefaultSourceURL
);
289 defaultCandidate
.SetTriggeringPrincipal(mDefaultSourceTriggeringPrincipal
);
290 // We don't use MaybeAppend since we want to keep this even if it can never
291 // match, as it may if the source set changes.
292 mCandidates
.AppendElement(std::move(defaultCandidate
));
295 already_AddRefed
<nsIURI
> ResponsiveImageSelector::GetSelectedImageURL() {
298 nsCOMPtr
<nsIURI
> url
= mSelectedCandidateURL
;
302 bool ResponsiveImageSelector::GetSelectedImageURLSpec(nsAString
& aResult
) {
305 if (mSelectedCandidateIndex
== -1) {
309 aResult
.Assign(mCandidates
[mSelectedCandidateIndex
].URLString());
313 double ResponsiveImageSelector::GetSelectedImageDensity() {
314 int bestIndex
= GetSelectedCandidateIndex();
319 return mCandidates
[bestIndex
].Density(this);
322 nsIPrincipal
* ResponsiveImageSelector::GetSelectedImageTriggeringPrincipal() {
323 int bestIndex
= GetSelectedCandidateIndex();
328 return mCandidates
[bestIndex
].TriggeringPrincipal();
331 bool ResponsiveImageSelector::SelectImage(bool aReselect
) {
332 if (!aReselect
&& mSelectedCandidateIndex
!= -1) {
333 // Already have selection
337 int oldBest
= mSelectedCandidateIndex
;
338 ClearSelectedCandidate();
340 int numCandidates
= mCandidates
.Length();
341 if (!numCandidates
) {
342 return oldBest
!= -1;
345 dom::Document
* doc
= Document();
346 nsPresContext
* pctx
= doc
->GetPresContext();
347 nsCOMPtr
<nsIURI
> baseURI
= mOwnerNode
->GetBaseURI();
349 if (!pctx
|| !baseURI
) {
350 return oldBest
!= -1;
353 double displayDensity
= pctx
->CSSPixelsToDevPixels(1.0f
);
354 double overrideDPPX
= pctx
->GetOverrideDPPX();
356 if (overrideDPPX
> 0) {
357 displayDensity
= overrideDPPX
;
360 // Per spec, "In a UA-specific manner, choose one image source"
361 // - For now, select the lowest density greater than displayDensity, otherwise
362 // the greatest density available
364 // If the list contains computed width candidates, compute the current
365 // effective image width.
366 double computedWidth
= -1;
367 for (int i
= 0; i
< numCandidates
; i
++) {
368 if (mCandidates
[i
].IsComputedFromWidth()) {
369 DebugOnly
<bool> computeResult
=
370 ComputeFinalWidthForCurrentViewport(&computedWidth
);
371 MOZ_ASSERT(computeResult
,
372 "Computed candidates not allowed without sizes data");
378 double bestDensity
= -1.0;
379 for (int i
= 0; i
< numCandidates
; i
++) {
380 double candidateDensity
= (computedWidth
== -1)
381 ? mCandidates
[i
].Density(this)
382 : mCandidates
[i
].Density(computedWidth
);
383 // - If bestIndex is below display density, pick anything larger.
384 // - Otherwise, prefer if less dense than bestDensity but still above
386 if (bestIndex
== -1 ||
387 (bestDensity
< displayDensity
&& candidateDensity
> bestDensity
) ||
388 (candidateDensity
>= displayDensity
&&
389 candidateDensity
< bestDensity
)) {
391 bestDensity
= candidateDensity
;
395 MOZ_ASSERT(bestIndex
>= 0 && bestIndex
< numCandidates
);
399 const nsAString
& urlStr
= mCandidates
[bestIndex
].URLString();
400 nsCOMPtr
<nsIURI
> candidateURL
;
401 rv
= nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(candidateURL
),
402 urlStr
, doc
, baseURI
);
404 mSelectedCandidateURL
= NS_SUCCEEDED(rv
) ? candidateURL
: nullptr;
405 mSelectedCandidateIndex
= bestIndex
;
407 return mSelectedCandidateIndex
!= oldBest
;
410 int ResponsiveImageSelector::GetSelectedCandidateIndex() {
413 return mSelectedCandidateIndex
;
416 bool ResponsiveImageSelector::ComputeFinalWidthForCurrentViewport(
418 dom::Document
* doc
= Document();
419 PresShell
* presShell
= doc
->GetPresShell();
420 nsPresContext
* pctx
= presShell
? presShell
->GetPresContext() : nullptr;
425 nscoord effectiveWidth
=
426 presShell
->StyleSet()->EvaluateSourceSizeList(mServoSourceSizeList
.get());
429 nsPresContext::AppUnitsToDoubleCSSPixels(std::max(effectiveWidth
, 0));
433 ResponsiveImageCandidate::ResponsiveImageCandidate() {
434 mType
= CandidateType::Invalid
;
435 mValue
.mDensity
= 1.0;
438 void ResponsiveImageCandidate::SetURLSpec(const nsAString
& aURLString
) {
439 mURLString
= aURLString
;
442 void ResponsiveImageCandidate::SetTriggeringPrincipal(
443 nsIPrincipal
* aPrincipal
) {
444 mTriggeringPrincipal
= aPrincipal
;
447 void ResponsiveImageCandidate::SetParameterAsComputedWidth(int32_t aWidth
) {
448 mType
= CandidateType::ComputedFromWidth
;
449 mValue
.mWidth
= aWidth
;
452 void ResponsiveImageCandidate::SetParameterDefault() {
453 MOZ_ASSERT(!IsValid(), "double setting candidate type");
455 mType
= CandidateType::Default
;
456 // mValue shouldn't actually be used for this type, but set it to default
458 mValue
.mDensity
= 1.0;
461 void ResponsiveImageCandidate::SetParameterInvalid() {
462 mType
= CandidateType::Invalid
;
463 // mValue shouldn't actually be used for this type, but set it to default
465 mValue
.mDensity
= 1.0;
468 void ResponsiveImageCandidate::SetParameterAsDensity(double aDensity
) {
469 MOZ_ASSERT(!IsValid(), "double setting candidate type");
471 mType
= CandidateType::Density
;
472 mValue
.mDensity
= aDensity
;
475 // Represents all supported descriptors for a ResponsiveImageCandidate, though
476 // there is no candidate type that uses all of these. This should generally
477 // match the mValue union of ResponsiveImageCandidate.
478 struct ResponsiveImageDescriptors
{
479 ResponsiveImageDescriptors() : mInvalid(false){};
481 Maybe
<double> mDensity
;
482 Maybe
<int32_t> mWidth
;
483 // We don't support "h" descriptors yet and they are not spec'd, but the
484 // current spec does specify that they can be silently ignored (whereas
485 // entirely unknown descriptors cause us to invalidate the candidate)
487 // If we ever start honoring them we should serialize them in
488 // AppendDescriptors.
489 Maybe
<int32_t> mFutureCompatHeight
;
490 // If this descriptor set is bogus, e.g. a value was added twice (and thus
491 // dropped) or an unknown descriptor was added.
494 void AddDescriptor(const nsAString
& aDescriptor
);
496 // Use the current set of descriptors to configure a candidate
497 void FillCandidate(ResponsiveImageCandidate
& aCandidate
);
500 // Try to parse a single descriptor from a string. If value already set or
501 // unknown, sets invalid flag.
502 // This corresponds to the descriptor "Descriptor parser" step in:
503 // https://html.spec.whatwg.org/#parse-a-srcset-attribute
504 void ResponsiveImageDescriptors::AddDescriptor(const nsAString
& aDescriptor
) {
505 if (aDescriptor
.IsEmpty()) {
509 // All currently supported descriptors end with an identifying character.
510 nsAString::const_iterator descStart
, descType
;
511 aDescriptor
.BeginReading(descStart
);
512 aDescriptor
.EndReading(descType
);
514 const nsDependentSubstring
& valueStr
= Substring(descStart
, descType
);
515 if (*descType
== char16_t('w')) {
516 int32_t possibleWidth
;
517 // If the value is not a valid non-negative integer, it doesn't match this
518 // descriptor, fall through.
519 if (ParseInteger(valueStr
, possibleWidth
) && possibleWidth
>= 0) {
520 if (possibleWidth
!= 0 && mWidth
.isNothing() && mDensity
.isNothing()) {
521 mWidth
.emplace(possibleWidth
);
523 // Valid width descriptor, but width or density were already seen, sizes
524 // support isn't enabled, or it parsed to 0, which is an error per spec
530 } else if (*descType
== char16_t('h')) {
531 int32_t possibleHeight
;
532 // If the value is not a valid non-negative integer, it doesn't match this
533 // descriptor, fall through.
534 if (ParseInteger(valueStr
, possibleHeight
) && possibleHeight
>= 0) {
535 if (possibleHeight
!= 0 && mFutureCompatHeight
.isNothing() &&
536 mDensity
.isNothing()) {
537 mFutureCompatHeight
.emplace(possibleHeight
);
539 // Valid height descriptor, but height or density were already seen, or
540 // it parsed to zero, which is an error per spec
546 } else if (*descType
== char16_t('x')) {
547 // If the value is not a valid floating point number, it doesn't match this
548 // descriptor, fall through.
549 double possibleDensity
= 0.0;
550 if (ParseFloat(valueStr
, possibleDensity
)) {
551 if (possibleDensity
>= 0.0 && mWidth
.isNothing() &&
552 mDensity
.isNothing() && mFutureCompatHeight
.isNothing()) {
553 mDensity
.emplace(possibleDensity
);
555 // Valid density descriptor, but height or width or density were already
556 // seen, or it parsed to less than zero, which is an error per spec
564 // Matched no known descriptor, mark this descriptor set invalid
568 bool ResponsiveImageDescriptors::Valid() {
569 return !mInvalid
&& !(mFutureCompatHeight
.isSome() && mWidth
.isNothing());
572 void ResponsiveImageDescriptors::FillCandidate(
573 ResponsiveImageCandidate
& aCandidate
) {
575 aCandidate
.SetParameterInvalid();
576 } else if (mWidth
.isSome()) {
577 MOZ_ASSERT(mDensity
.isNothing()); // Shouldn't be valid
579 aCandidate
.SetParameterAsComputedWidth(*mWidth
);
580 } else if (mDensity
.isSome()) {
581 MOZ_ASSERT(mWidth
.isNothing()); // Shouldn't be valid
583 aCandidate
.SetParameterAsDensity(*mDensity
);
585 // A valid set of descriptors with no density nor width (e.g. an empty set)
586 // becomes 1.0 density, per spec
587 aCandidate
.SetParameterAsDensity(1.0);
591 bool ResponsiveImageCandidate::ConsumeDescriptors(
592 nsAString::const_iterator
& aIter
,
593 const nsAString::const_iterator
& aIterEnd
) {
594 nsAString::const_iterator
& iter
= aIter
;
595 const nsAString::const_iterator
& end
= aIterEnd
;
597 bool inParens
= false;
599 ResponsiveImageDescriptors descriptors
;
601 // Parse descriptor list.
602 // This corresponds to the descriptor parsing loop from:
603 // https://html.spec.whatwg.org/#parse-a-srcset-attribute
605 // Skip initial whitespace
606 for (; iter
!= end
&& nsContentUtils::IsHTMLWhitespace(*iter
); ++iter
)
609 nsAString::const_iterator currentDescriptor
= iter
;
613 descriptors
.AddDescriptor(Substring(currentDescriptor
, iter
));
615 } else if (inParens
) {
616 if (*iter
== char16_t(')')) {
620 if (*iter
== char16_t(',')) {
621 // End of descriptors, flush current descriptor and advance past comma
623 descriptors
.AddDescriptor(Substring(currentDescriptor
, iter
));
627 if (nsContentUtils::IsHTMLWhitespace(*iter
)) {
628 // End of current descriptor, consume it, skip spaces
629 // ("After descriptor" state in spec) before continuing
630 descriptors
.AddDescriptor(Substring(currentDescriptor
, iter
));
631 for (; iter
!= end
&& nsContentUtils::IsHTMLWhitespace(*iter
); ++iter
)
636 currentDescriptor
= iter
;
637 // Leave one whitespace so the loop advances to this position next
640 } else if (*iter
== char16_t('(')) {
646 descriptors
.FillCandidate(*this);
651 bool ResponsiveImageCandidate::HasSameParameter(
652 const ResponsiveImageCandidate
& aOther
) const {
653 if (aOther
.mType
!= mType
) {
657 if (mType
== CandidateType::Default
) {
661 if (mType
== CandidateType::Density
) {
662 return aOther
.mValue
.mDensity
== mValue
.mDensity
;
665 if (mType
== CandidateType::Invalid
) {
666 MOZ_ASSERT_UNREACHABLE("Comparing invalid candidates?");
670 if (mType
== CandidateType::ComputedFromWidth
) {
671 return aOther
.mValue
.mWidth
== mValue
.mWidth
;
674 MOZ_ASSERT(false, "Somebody forgot to check for all uses of this enum");
678 double ResponsiveImageCandidate::Density(
679 ResponsiveImageSelector
* aSelector
) const {
680 if (mType
== CandidateType::ComputedFromWidth
) {
682 if (!aSelector
->ComputeFinalWidthForCurrentViewport(&width
)) {
685 return Density(width
);
688 // Other types don't need matching width
689 MOZ_ASSERT(mType
== CandidateType::Default
|| mType
== CandidateType::Density
,
690 "unhandled candidate type");
694 void ResponsiveImageCandidate::AppendDescriptors(
695 nsAString
& aDescriptors
) const {
696 MOZ_ASSERT(IsValid());
698 case CandidateType::Default
:
699 case CandidateType::Invalid
:
701 case CandidateType::ComputedFromWidth
:
702 aDescriptors
.Append(' ');
703 aDescriptors
.AppendInt(mValue
.mWidth
);
704 aDescriptors
.Append('w');
706 case CandidateType::Density
:
707 aDescriptors
.Append(' ');
708 aDescriptors
.AppendFloat(mValue
.mDensity
);
709 aDescriptors
.Append('x');
714 double ResponsiveImageCandidate::Density(double aMatchingWidth
) const {
715 if (mType
== CandidateType::Invalid
) {
716 MOZ_ASSERT(false, "Getting density for uninitialized candidate");
720 if (mType
== CandidateType::Default
) {
724 if (mType
== CandidateType::Density
) {
725 return mValue
.mDensity
;
727 if (mType
== CandidateType::ComputedFromWidth
) {
728 if (aMatchingWidth
< 0) {
731 "Don't expect to have a negative matching width at this point");
734 double density
= double(mValue
.mWidth
) / aMatchingWidth
;
735 MOZ_ASSERT(density
> 0.0);
739 MOZ_ASSERT(false, "Unknown candidate type");
743 } // namespace mozilla::dom