⎇ ae87fd0283feff32a5ceffe4c77394ff6ca88d39
scripts/gentest/test_helper.js
History
Who touched what
raw
1
2class TrackSizingParser {
3 static INITIAL_CHAR_REGEX = /[a-z-A-Z0-9]/;
4 static TOKEN_CHAR_REGEX = /[-\.a-z-A-Z0-9%]/;
5
6 constructor(input, options = { allowFrUnits: true }) {
7 this.input = input;
8 this.index = 0;
9 this.options = options;
10 }
11
12 parseList() {
13 return this._parseItemList(' ', null);
14 }
15
16 parseSingleItem() {
17 return this.parseItem();
18 }
19
20 _parseItemList(separator, terminator = null) {
21 if (!separator) throw new Error('No terminator passed');
22 let tokenList = [];
23 // console.debug('Parse List', this.index, this.input.slice(this.index));
24
25 while (this.index < this.input.length) {
26 const char = this.input[this.index];
27 // console.debug(this.index, char);
28
29 // Skip whitespace
30 if (char === ' ') { this.index++; continue;}
31
32 if (TrackSizingParser.INITIAL_CHAR_REGEX.test(char)) {
33 const token = this._parseItem();
34 tokenList.push(token);
35
36 const nextChar = this.input[this.index];
37 if ((terminator && nextChar === terminator) || !terminator && !nextChar) {
38 return tokenList;
39 } else {
40 this.index++;
41 continue;
42 }
43 }
44
45 throw new Error (`Invalid start of token ${char}`);
46 }
47 }
48
49 _parseItem() {
50 let token = '';
51 // console.debug('Parse Item', this.index, this.input.slice(this.index));
52
53 while (this.index < this.input.length) {
54 const char = this.input[this.index];
55 // console.debug(this.index, char);
56
57 if (TrackSizingParser.TOKEN_CHAR_REGEX.test(char)) {
58 token += char;
59 this.index++;
60 continue;
61 }
62
63 if (char === '(') {
64 this.index++;
65 const args = this._parseItemList(',', ')');
66 this.index++;
67 return { kind: 'function', name: token, arguments: args };
68 }
69
70 return { kind: 'scalar', ...this._parseScalarItem(token) };
71 }
72 return { kind: 'scalar', ...this._parseScalarItem(token) };
73
74 }
75
76 _parseScalarItem(item) {
77 const res = parseRepetition(item) || parseDimension(item, { allowFrUnits: this.options.allowFrUnits });
78 if (!res) throw new Error(`Invalid scalar grid track sizing function ${item}`);
79 return res;
80 }
81
82}
83
84function parseViewportConstraint(e) {
85 if (e.parentNode.classList.contains('viewport')) {
86 return {
87 width: parseDimension(e.parentNode.style.width || 'max-content'),
88 height: parseDimension(e.parentNode.style.height || 'max-content'),
89 }
90 } else {
91 return {
92 width: { unit: 'max-content' },
93 height: { unit: 'max-content' },
94 }
95 }
96}
97
98function parseRepetition(input) {
99 if (input === "auto-fill") return { unit: 'auto-fill' };
100 if (input === "auto-fit") return { unit: 'auto-fit' };
101 if (/^[0-9]*$/.test(input)) return { 'unit': 'integer', value: parseInt(input, 10) };
102 return undefined;
103}
104
105function parseDimension(input, options = { allowFrUnits: false }) {
106 if (options.allowFrUnits && input.endsWith('fr')) return { unit: 'fraction', value: parseFloat(input.replace('fr','')) };
107 if (input.endsWith('px')) return { unit: 'points', value: parseFloat(input.replace('px','')) };
108 if (input.endsWith('%')) return { unit: 'percent', value: parseFloat(input.replace('%','')) / 100 };
109 if (input === 'auto') return { unit: 'auto' };
110 if (input === 'min-content') return { unit: 'min-content' };
111 if (input === 'max-content') return { unit: 'max-content' };
112 return undefined;
113}
114
115function parseNumber(input) {
116 if (input === '' || isNaN(input)) return undefined;
117 return Number(input);
118}
119
120function parseRatio(input) {
121 if (!input) return undefined;
122
123 if (input.includes('/')) {
124 let [width, height] = input.split("/").map(part => parseFloat(part.trim()));
125 if (!width || width < 0 || !height || height <= 0) return undefined;
126 return width / height;
127 }
128
129 let ratio = parseFloat(input);
130 if (!ratio || ratio < 0) return undefined;
131 return ratio;
132}
133
134function parseEnum(input) {
135 if (input) return input;
136 return undefined;
137}
138
139function parseEdges(edges) {
140 const left = parseDimension(edges.left);
141 const right = parseDimension(edges.right);
142 const top = parseDimension(edges.top);
143 const bottom = parseDimension(edges.bottom);
144
145 if (!left && !right && !top && !bottom) return undefined;
146 return { left, right, top, bottom };
147}
148
149function parseSize(size) {
150 const width = parseDimension(size.width);
151 const height = parseDimension(size.height);
152
153 if (!width && !height) return undefined;
154 return { width, height };
155}
156
157function parseGaps(style) {
158 if (style.gap) {
159 const gaps = style.gap.trim().split(/\s+/).map(part => parseDimension(part));
160 return { row: gaps[0], column: gaps[1] ?? gaps[0] };
161 }
162 if (style.rowGap || style.columnGap) {
163 return { row: parseDimension(style.rowGap), column: parseDimension(style.columnGap) }
164 }
165 return undefined;
166}
167
168
169function parseGridTrackDefinitions(input) {
170 if (input === '') return undefined;
171 return new TrackSizingParser(input).parseList();
172}
173
174function parseGridAutoFlow(input) {
175 if (!/column/.test(input) && !/row/.test(input) && !/dense/.test(input)) return undefined;
176 const direction = /column/.test(input) ? 'column' : 'row';
177 const algorithm = /dense/.test(input) ? 'dense' : 'sparse';
178 return { direction, algorithm };
179}
180
181function parseGridPosition(input) {
182 if (input === 'auto') return { kind: 'auto' }
183 if (/^span +\d+$/.test(input)) return { kind: 'span', value: parseInt(input.replace(/[^\d]/g, ''), 10)}
184 if (/^-?\d+$/.test(input)) return { kind: 'line', value: parseInt(input, 10)}
185 return undefined;
186}
187
188function describeElement(e) {
189
190 // Get precise, unrounded dimensions for the current element and it's parent
191 let boundingRect = e.getBoundingClientRect();
192 let parentBoundingRect = e.parentNode.getBoundingClientRect();
193
194 return {
195 style: {
196 display: parseEnum(e.style.display),
197
198 position: parseEnum(e.style.position),
199 direction: parseEnum(e.style.direction),
200 flexDirection: parseEnum(e.style.flexDirection),
201
202 writingMode: parseEnum(e.style.writingMode),
203
204 flexWrap: parseEnum(e.style.flexWrap),
205 overflow: parseEnum(e.style.overflow),
206
207 alignItems: parseEnum(e.style.alignItems),
208 alignSelf: parseEnum(e.style.alignSelf),
209 justifyItems: parseEnum(e.style.justifyItems),
210 justifySelf: parseEnum(e.style.justifySelf),
211
212 alignContent: parseEnum(e.style.alignContent),
213 justifyContent: parseEnum(e.style.justifyContent),
214
215 flexGrow: parseNumber(e.style.flexGrow),
216 flexShrink: parseNumber(e.style.flexShrink),
217 flexBasis: parseDimension(e.style.flexBasis),
218
219 gridTemplateRows: parseGridTrackDefinitions(e.style.gridTemplateRows),
220 gridTemplateColumns: parseGridTrackDefinitions(e.style.gridTemplateColumns),
221 gridAutoRows: parseGridTrackDefinitions(e.style.gridAutoRows),
222 gridAutoColumns: parseGridTrackDefinitions(e.style.gridAutoColumns),
223 gridAutoFlow: parseGridAutoFlow(e.style.gridAutoFlow),
224
225 gridRowStart: parseGridPosition(e.style.gridRowStart),
226 gridRowEnd: parseGridPosition(e.style.gridRowEnd),
227 gridColumnStart: parseGridPosition(e.style.gridColumnStart),
228 gridColumnEnd: parseGridPosition(e.style.gridColumnEnd),
229
230 gap: parseGaps(e.style),
231
232 size: parseSize({width: e.style.width, height: e.style.height}),
233 minSize: parseSize({width: e.style.minWidth, height: e.style.minHeight}),
234 maxSize: parseSize({width: e.style.maxWidth, height: e.style.maxHeight}),
235 aspectRatio: parseRatio(e.style.aspectRatio),
236
237 margin: parseEdges({
238 left: e.style.marginLeft,
239 right: e.style.marginRight,
240 top: e.style.marginTop,
241 bottom: e.style.marginBottom,
242 }),
243
244 padding: parseEdges({
245 left: e.style.paddingLeft,
246 right: e.style.paddingRight,
247 top: e.style.paddingTop,
248 bottom: e.style.paddingBottom,
249 }),
250
251 border: parseEdges({
252 left: e.style.borderLeftWidth,
253 right: e.style.borderRightWidth,
254 top: e.style.borderTopWidth,
255 bottom: e.style.borderBottomWidth,
256 }),
257
258 inset: parseEdges({
259 left: e.style.left,
260 right: e.style.right,
261 top: e.style.top,
262 bottom: e.style.bottom,
263 }),
264 },
265
266 // The textContent is used for generating intrinsic sizing measure funcs
267 // So we're only interested in the text content of leaf nodes
268 textContent: e.childElementCount === 0 && e.textContent.length && e.textContent !== "\n" ? e.textContent : undefined,
269
270 // The layout of the node in full precision (floating-point)
271 unroundedLayout: {
272 width: boundingRect.width,
273 height: boundingRect.height,
274 x: boundingRect.x - parentBoundingRect.x,
275 y: boundingRect.y - parentBoundingRect.y,
276 },
277
278 // The naively rounded layout of the node. This is equivalent to calling Math.round() on
279 // each value in the unrounded layout individually
280 naivelyRoundedLayout: {
281 width: e.offsetWidth,
282 height: e.offsetHeight,
283 x: e.offsetLeft + e.parentNode.clientLeft,
284 y: e.offsetTop + e.parentNode.clientTop,
285 },
286
287 // The naive rounding can result in 1px gaps in the layout, so Taffy uses a smarter algorithm to avoid this.
288 // Chrome also uses a smarter algorithm, but it doesn't expose the output of that rounding.
289 // So we just emulate Taffy's computation here.
290 smartRoundedLayout: {
291 width: Math.round(boundingRect.right) - Math.round(boundingRect.left),
292 height: Math.round(boundingRect.bottom) - Math.round(boundingRect.top),
293 x: Math.round(boundingRect.x - parentBoundingRect.x),
294 y: Math.round(boundingRect.y - parentBoundingRect.y),
295 },
296
297 // Whether the test should enable rounding
298 useRounding: e.getAttribute("data-test-rounding") !== "false",
299
300 viewport: parseViewportConstraint(e),
301
302 children: Array.from(e.children).map(c => describeElement(c)),
303 }
304}
305
306// Useful when developing this script. Logs the parsed style to the console when any test fixture is opened in a browser.
307window.onload = function () {
308 try {
309 console.log(describeElement(document.getElementById('test-root')));
310 } catch (e) {
311 console.error(e);
312 }
313}