tawasuyu

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 501HTML 254C 19JavaScript 81414 Sutichaykuna

⎇ devin/1784869041-flex-baseline-container scripts/gentest/test_helper.js JavaScript · 357 siq'ikuna · 11.6 KB Ñawpa willay Pi imata llamkarqan ch'uya

Implement `direction` property (#906) 60601a657 · Kane Wang ·

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