-
Notifications
You must be signed in to change notification settings - Fork 4
/
parse-sizes.js
214 lines (191 loc) · 7.43 KB
/
parse-sizes.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/*
* 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";
}