XeNote/.yarn/unplugged/@ampproject-toolbox-optimizer-virtual-871245ee15/node_modules/@ampproject/toolbox-optimizer/lib/parseSrcSet.js

309 lines
8.4 KiB
JavaScript
Raw Normal View History

2021-05-17 01:16:19 +00:00
/**
* Copyright 2015 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Credits: ported from https://github.com/ampproject/amphtml/blob/1098976cdb0bb73ad89f3287185df992bb29f69c/src/srcset.js
*/
/**
* A single source within a srcset. Only one: width or DPR can be specified at
* a time.
* @typedef {{
* url: string,
* width: (number|undefined),
* dpr: (number|undefined)
* }}
*/
// eslint-disable-next-line
let SrcsetSourceDef;
/**
* General grammar: (URL [NUM[w|x]],)*
* Example 1: "image1.png 100w, image2.png 50w"
* Example 2: "image1.png 2x, image2.png"
* Example 3: "image1,100w.png 100w, image2.png 50w"
*/
const srcsetRegex = /(\S+)(?:\s+(?:(-?\d+(?:\.\d+)?)([a-zA-Z]*)))?\s*(?:,|$)/g;
/**
* Parses the text representation of srcset into Srcset object.
* See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#Attributes.
* See http://www.w3.org/html/wg/drafts/html/master/semantics.html#attr-img-srcset.
* @param {string} s
* @return {Srcset}
*/
function parseSrcset(s) {
const sources = [];
let match;
while ((match = srcsetRegex.exec(s))) {
const url = match[1];
let width, dpr;
if (match[2]) {
const type = match[3].toLowerCase();
if (type == 'w') {
width = parseInt(match[2], 10);
} else if (type == 'x') {
dpr = parseFloat(match[2]);
} else {
continue;
}
} else {
// If no "w" or "x" specified, we assume it's "1x".
dpr = 1;
}
sources.push({url, width, dpr});
}
return new Srcset(sources);
}
/**
* A srcset object contains one or more sources.
*
* There are two types of sources: width-based and DPR-based. Only one type
* of sources allowed to be specified within a single srcset. Depending on a
* usecase, the components are free to choose any source that best corresponds
* to the required rendering quality and network and CPU conditions. See
* "select" method for details on how this selection is performed.
*
* See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#Attributes
*/
class Srcset {
/**
* @param {!Array<!SrcsetSourceDef>} sources
*/
constructor(sources) {
assert(sources.length > 0, 'Srcset must have at least one source');
/** @private @const {!Array<!SrcsetSourceDef>} */
this.sources_ = sources;
// Only one type of source specified can be used - width or DPR.
let hasWidth = false;
let hasDpr = false;
for (let i = 0; i < sources.length; i++) {
const source = sources[i];
hasWidth = hasWidth || !!source.width;
hasDpr = hasDpr || !!source.dpr;
}
assert(!!(hasWidth ^ hasDpr), 'Srcset must have width or dpr sources, but not both');
// Source and assert duplicates.
sources.sort(hasWidth ? sortByWidth : sortByDpr);
/** @private @const {boolean} */
this.widthBased_ = hasWidth;
}
/**
* Performs selection for specified width and DPR. Here, width is the width
* in screen pixels and DPR is the device-pixel-ratio or pixel density of
* the device. Depending on the circumstances, such as low network conditions,
* it's possible to manipulate the result of this method by passing a lower
* DPR value.
*
* The source selection depends on whether this is width-based or DPR-based
* srcset.
*
* In a width-based source, the source's width is the physical width of a
* resource (e.g. an image). Depending on the provided DPR, this width is
* converted to the screen pixels as following:
* pixelWidth = sourceWidth / DPR
*
* Then, the source closest to the requested "width" is selected using
* the "pixelWidth". The slight preference is given to the bigger sources to
* ensure the most optimal quality.
*
* In a DPR-based source, the source's DPR is used to return the source that
* is closest to the requested DPR.
*
* Based on
* http://www.w3.org/html/wg/drafts/html/master/semantics.html#attr-img-srcset.
* @param {number} width
* @param {number} dpr
* @return {string}
*/
select(width, dpr) {
assert(width, 'width=%s', width);
assert(dpr, 'dpr=%s', dpr);
let index = 0;
if (this.widthBased_) {
index = this.selectByWidth_(width * dpr);
} else {
index = this.selectByDpr_(dpr);
}
return this.sources_[index].url;
}
/**
* @param {number} width
* @return {number}
* @private
*/
selectByWidth_(width) {
const sources = this.sources_;
let minIndex = 0;
let minScore = Infinity;
let minWidth = Infinity;
for (let i = 0; i < sources.length; i++) {
const sWidth = sources[i].width;
const score = Math.abs(sWidth - width);
// Select the one that is closer with a slight preference toward larger
// widths. If smaller size is closer, enforce minimum ratio to ensure
// image isn't too distorted.
if (score <= minScore * 1.1 || width / minWidth > 1.2) {
minIndex = i;
minScore = score;
minWidth = sWidth;
} else {
break;
}
}
return minIndex;
}
/**
* @param {number} dpr
* @return {number}
* @private
*/
selectByDpr_(dpr) {
const sources = this.sources_;
let minIndex = 0;
let minScore = Infinity;
for (let i = 0; i < sources.length; i++) {
const score = Math.abs(sources[i].dpr - dpr);
if (score <= minScore) {
minIndex = i;
minScore = score;
} else {
break;
}
}
return minIndex;
}
/**
* Returns all URLs in the srcset.
* @return {!Array<string>}
*/
getUrls() {
return this.sources_.map((s) => s.url);
}
/**
* Reconstructs the string expression for this srcset.
* @param {function(string):string=} opt_mapper
* @return {string}
*/
stringify(opt_mapper) {
const res = [];
const sources = this.sources_;
for (let i = 0; i < sources.length; i++) {
const source = sources[i];
let src = source.url;
if (opt_mapper) {
src = opt_mapper(src);
}
if (this.widthBased_) {
src += ` ${source.width}w`;
} else {
src += ` ${source.dpr}x`;
}
res.push(src);
}
return res.join(', ');
}
}
/**
* Sorts by width
*
* @param {number} s1
* @param {number} s2
* @return {number}
*/
function sortByWidth(s1, s2) {
assert(s1.width != s2.width, 'Duplicate width: %s', s1.width);
return s1.width - s2.width;
}
/**
* Sorts by dpr
*
* @param {!Object} s1
* @param {!Object} s2
* @return {number}
*/
function sortByDpr(s1, s2) {
assert(s1.dpr != s2.dpr, 'Duplicate dpr: %s', s1.dpr);
return s1.dpr - s2.dpr;
}
function assert(shouldBeTrueish, opt_message) {
let firstElement;
if (!shouldBeTrueish) {
const message = opt_message || 'Assertion failed';
const splitMessage = message.split('%s');
const first = splitMessage.shift();
let formatted = first;
const messageArray = [];
let i = 2;
pushIfNonEmpty(messageArray, first);
while (splitMessage.length > 0) {
const nextConstant = splitMessage.shift();
const val = arguments[i++];
if (val && val.tagName) {
firstElement = val;
}
messageArray.push(val);
pushIfNonEmpty(messageArray, nextConstant.trim());
formatted += stringOrElementString(val) + nextConstant;
}
const e = new Error(formatted);
e.fromAssert = true;
e.associatedElement = firstElement;
e.messageArray = messageArray;
throw e;
}
return shouldBeTrueish;
}
/**
* @param {string|!Element} val
* @return {string}
*/
const stringOrElementString = (val) => /** @type {string} */ elementStringOrPassthru(val);
/**
* @param {*} val
* @return {*}
*/
function elementStringOrPassthru(val) {
// Do check equivalent to `val instanceof Element` without cross-window bug
if (val && val.nodeType == 1) {
return val.tagName.toLowerCase() + (val.id ? '#' + val.id : '');
}
return val;
}
function pushIfNonEmpty(array, val) {
if (val != '') {
array.push(val);
}
}
module.exports = parseSrcset;