Skip to content

Instantly share code, notes, and snippets.

@OliverJAsh
Created August 31, 2017 12:10
Show Gist options
  • Save OliverJAsh/977a82eaeb59ac5ba8dfacf526563681 to your computer and use it in GitHub Desktop.
Save OliverJAsh/977a82eaeb59ac5ba8dfacf526563681 to your computer and use it in GitHub Desktop.
Debug responsive images
// On resize and img load events, logs:
// - `currentSrc` width (selected from `srcset`)
// - element width
// - parsed size value (selected from `sizes`)
// - a boolean indicating if the parsed size matches the element width
{
// There seem to be issues with `HTMLImageElement.natural{Width,Height}`, so we have to rely on this
// instead.
// https://bugs.chromium.org/p/chromium/issues/detail?id=760612
const getSrcWidth = (src) => {
const t = new Image();
t.src = src;
return t.width;
}
const evalCalc = calcStr => {
const node = document.createElement('div')
node.style.width = calcStr;
document.body.appendChild(node);
const { width } = window.getComputedStyle(node);
document.body.removeChild(node);
return width;
}
window.observe = imgEl => {
const log = () => {
const currentSrcWidth = getSrcWidth(imgEl.currentSrc);
const elementWidth = imgEl.width;
const parsedSizeRaw = parseSizes(imgEl.sizes);
const parsedSize = evalCalc(parsedSizeRaw);
const doesParsedSizeMatchElementWidth = Math.ceil(parseFloat(parsedSize)) === elementWidth;
console.log({ currentSrcWidth, elementWidth, parsedSize, doesParsedSizeMatchElementWidth })
}
log();
imgEl.addEventListener('load', log)
window.addEventListener('resize', log)
}
// https://raw.githubusercontent.com/albell/parse-sizes/master/parse-sizes.js
/*
* Sizes Parser
*
* By Alex Bell | MIT License
*
* Non-strict but accurate and lightweight JS Parser for the string value <img sizes="here">
*
* Reference algorithm at:
* https://html.spec.whatwg.org/multipage/embedded-content.html#parse-a-sizes-attribute
*
* Most comments are copied in directly from the spec
* (except for comments in parens).
*
* Grammar is:
* <source-size-list> = <source-size># [ , <source-size-value> ]? | <source-size-value>
* <source-size> = <media-condition> <source-size-value>
* <source-size-value> = <length>
* http://www.w3.org/html/wg/drafts/html/master/embedded-content.html#attr-img-sizes
*
* E.g. "(max-width: 30em) 100vw, (max-width: 50em) 70vw, 100vw"
* or "(min-width: 30em), calc(30em - 15px)" or just "30vw"
*
* Returns the first valid <css-length> with a media condition that evaluates to true,
* or "100vw" if all valid media conditions evaluate to false.
*
*/
function parseSizes(strValue) {
// (Percentage CSS lengths are not allowed in this case, to avoid confusion:
// https://html.spec.whatwg.org/multipage/embedded-content.html#valid-source-size-list
// CSS allows a single optional plus or minus sign:
// http://www.w3.org/TR/CSS2/syndata.html#numbers
// CSS is ASCII case-insensitive:
// http://www.w3.org/TR/CSS2/syndata.html#characters )
// Spec allows exponential notation for <number> type:
// http://dev.w3.org/csswg/css-values/#numbers
var regexCssLengthWithUnits = /^(?:[+-]?[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?(?:ch|cm|em|ex|in|mm|pc|pt|px|rem|vh|vmin|vmax|vw)$/i,
// (This is a quick and lenient test. Because of optional unlimited-depth internal
// grouping parens and strict spacing rules, this could get very complicated.)
regexCssCalc = /^calc\((?:[0-9a-z \.\+\-\*\/\(\)]+)\)$/i,
i, unparsedSizesList, unparsedSizesListLength, unparsedSize, lastComponentValue, size;
// UTILITY FUNCTIONS
// ( Manual is faster than RegEx.)
// http://jsperf.com/whitespace-character/5
function isSpace(c) {
return (c === "\u0020" || // space
c === "\u0009" || // horizontal tab
c === "\u000A" || // new line
c === "\u000C" || // form feed
c === "\u000D"); // carriage return
}
// (Toy CSS parser. The goals here are:
// 1) expansive test coverage without the weight of a full CSS parser.
// 2) Avoiding regex wherever convenient.
// Quick tests: http://jsfiddle.net/gtntL4gr/3/
// Returns an array of arrays.)
function parseComponentValues(str) {
var chrctr,
component = "",
componentArray = [],
listArray = [],
parenDepth = 0,
pos = 0,
inComment = false;
function pushComponent() {
if (component) {
componentArray.push(component);
component = "";
}
}
function pushComponentArray() {
if (componentArray[ 0 ]) {
listArray.push(componentArray);
componentArray = [];
}
}
// (Loop forwards from the beginning of the string.)
while (true) {
chrctr = str.charAt(pos);
if (chrctr === "") { // ( End of string reached.)
pushComponent();
pushComponentArray();
return listArray;
} else if (inComment) {
if ( (chrctr === "*") && (str.charAt(pos + 1) === "/") ) { // (At end of a comment.)
inComment = false;
pos += 2;
pushComponent();
continue;
} else {
pos += 1; // (Skip all characters inside comments.)
continue;
}
} else if (isSpace(chrctr)) {
// (If previous character in loop was also a space, or if
// at the beginning of the string, do not add space char to
// component.)
if ((str.charAt(pos - 1) && isSpace(str.charAt(pos - 1) ) ) || (!component)) {
pos += 1;
continue;
} else if (parenDepth === 0) {
pushComponent();
pos += 1;
continue;
} else {
// (Replace any space character with a plain space for legibility.)
chrctr = " ";
}
} else if (chrctr === "(") {
parenDepth += 1;
} else if (chrctr === ")") {
parenDepth -= 1;
} else if (chrctr === ",") {
pushComponent()
pushComponentArray();
pos += 1;
continue;
} else if ( (chrctr === "/") && (str.charAt( pos + 1 ) === "*") ) {
inComment = true;
pos += 2;
continue;
}
component = component + chrctr;
pos += 1;
}
}
function isValidNonNegativeSourceSizeValue(s) {
if (regexCssLengthWithUnits.test(s) && (parseFloat(s) >= 0)) {return true;}
if (regexCssCalc.test(s)) {return true;}
// ( http://www.w3.org/TR/CSS2/syndata.html#numbers says:
// "-0 is equivalent to 0 and is not a negative number." which means that
// unitless zero and unitless negative zero must be accepted as special cases.)
if ((s === "0") || (s === "-0") || (s === "+0")) {return true;}
return false;
}
// When asked to parse a sizes attribute from an element, parse a
// comma-separated list of component values from the value of the element's
// sizes attribute (or the empty string, if the attribute is absent), and let
// unparsed sizes list be the result.
// http://dev.w3.org/csswg/css-syntax/#parse-comma-separated-list-of-component-values
unparsedSizesList = parseComponentValues(strValue);
unparsedSizesListLength = unparsedSizesList.length;
// For each unparsed size in unparsed sizes list:
for (i = 0; i < unparsedSizesListLength; i++) {
unparsedSize = unparsedSizesList[ i ];
// 1. Remove all consecutive <whitespace-token>s from the end of unparsed size.
// ( parseComponentValues() already omits spaces outside of parens. )
// If unparsed size is now empty, that is a parse error; continue to the next
// iteration of this algorithm.
// ( parseComponentValues() won't push an empty array. )
// 2. If the last component value in unparsed size is a valid non-negative
// <source-size-value>, let size be its value and remove the component value
// from unparsed size. Any CSS function other than the calc() function is
// invalid. Otherwise, there is a parse error; continue to the next iteration
// of this algorithm.
// http://dev.w3.org/csswg/css-syntax/#parse-component-value
lastComponentValue = unparsedSize[ unparsedSize.length - 1 ];
if (isValidNonNegativeSourceSizeValue(lastComponentValue)) {
size = lastComponentValue;
unparsedSize.pop();
} else if (window.console && console.log) {
console.log("Parse error: " + strValue);
continue;
}
// 3. Remove all consecutive <whitespace-token>s from the end of unparsed
// size. If unparsed size is now empty, return size and exit this algorithm.
// If this was not the last item in unparsed sizes list, that is a parse error.
if (unparsedSize.length === 0) {
if ((i !== unparsedSizesListLength - 1) && window.console && console.log) {
console.log("Parse error: " + strValue);
}
return size;
}
// 4. Parse the remaining component values in unparsed size as a
// <media-condition>. If it does not parse correctly, or it does parse
// correctly but the <media-condition> evaluates to false, continue to the
// next iteration of this algorithm.
// (Parsing all possible compound media conditions in JS is heavy, complicated,
// and the payoff is unclear. Is there ever an situation where the
// media condition parses incorrectly but still somehow evaluates to true?
// Can we just rely on the browser/polyfill to do it?)
unparsedSize = unparsedSize.join(" ");
if (!(window.matchMedia(unparsedSize).matches) ) {
continue;
}
// 5. Return size and exit this algorithm.
return size;
}
// If the above algorithm exhausts unparsed sizes list without returning a
// size value, return 100vw.
return "100vw";
}
}
// Whilst we debug image sizes, we want to ignore scroll bars as may not be accounted for in our
// `sizes` attribute.
document.body.style.overflow = 'hidden';
observe($('img'))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment