tawasuyu

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 1077HTML 1008Markdown 12TOML 83209 Definitions

⎇ e8253b1ae8ad9e95777144ce91aa6fb1ec8d4e32 scripts/gentest/test_helper.js JavaScript · 297 lines · 9.6 KB History Who touched what raw

Fix 1px wide gaps between adjacent UI nodes (#374) e8253b1ae · Nico Burns ·

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 parseRepetition(input) {
85  if (input === "auto-fill") return { unit: 'auto-fill' };
86  if (input === "auto-fit") return { unit: 'auto-fit' };
87  if (/^[0-9]*$/.test(input)) return { 'unit': 'integer', value: parseInt(input, 10) };
88  return undefined;
89}
90
91function parseDimension(input, options = { allowFrUnits: false }) {
92  if (options.allowFrUnits && input.endsWith('fr')) return { unit: 'fraction', value: parseFloat(input.replace('fr','')) };
93  if (input.endsWith('px')) return { unit: 'points',   value: parseFloat(input.replace('px','')) };
94  if (input.endsWith('%')) return { unit: 'percent',  value: parseFloat(input.replace('%','')) / 100 };
95  if (input === 'auto') return { unit: 'auto' };
96  if (input === 'min-content') return { unit: 'min-content' };
97  if (input === 'max-content') return { unit: 'max-content' };
98  return undefined;
99}
100
101function parseNumber(input) {
102  if (input === '' || isNaN(input)) return undefined;
103  return Number(input);
104}
105
106function parseRatio(input) {
107  if (!input) return undefined;
108
109  if (input.includes('/')) {
110    let [width, height] = input.split("/").map(part => parseFloat(part.trim()));
111    if (!width || width < 0 || !height || height <= 0) return undefined;
112    return width / height;
113  }
114
115  let ratio = parseFloat(input);
116  if (!ratio || ratio < 0) return undefined;
117  return ratio;
118}
119
120function parseEnum(input) {
121  if (input) return input;
122  return undefined;
123}
124
125function parseEdges(edges) {
126  const left = parseDimension(edges.left);
127  const right = parseDimension(edges.right);
128  const top = parseDimension(edges.top);
129  const bottom = parseDimension(edges.bottom);
130  
131  if (!left && !right && !top && !bottom) return undefined;
132  return { left, right, top, bottom };
133}
134
135function parseSize(size) {
136  const width = parseDimension(size.width);
137  const height = parseDimension(size.height);
138  
139  if (!width && !height) return undefined;
140  return { width, height };
141}
142
143function parseGaps(style) {
144  if (style.gap) {
145    const gaps = style.gap.trim().split(/\s+/).map(part => parseDimension(part));
146    return { row: gaps[0], column: gaps[1] ?? gaps[0] };
147  }
148  if (style.rowGap || style.columnGap) {
149    return { row: parseDimension(style.rowGap), column: parseDimension(style.columnGap) }
150  }
151  return undefined;
152}
153
154
155function parseGridTrackDefinitions(input) {
156  if (input === '') return undefined;
157  return new TrackSizingParser(input).parseList();
158}
159
160function parseGridAutoFlow(input) {
161  if (!/column/.test(input) && !/row/.test(input) && !/dense/.test(input)) return undefined;
162  const direction = /column/.test(input) ? 'column' : 'row';
163  const algorithm = /dense/.test(input) ? 'dense' : 'sparse';
164  return { direction, algorithm };
165}
166
167function parseGridPosition(input) {
168  if (input === 'auto') return { kind: 'auto' }
169  if (/^span +\d+$/.test(input)) return { kind: 'span', value: parseInt(input.replace(/[^\d]/g, ''), 10)}
170  if (/^-?\d+$/.test(input)) return { kind: 'line', value: parseInt(input, 10)}
171  return undefined;
172}
173
174function describeElement(e) {
175
176  // Get precise, unrounded dimensions for the current element and it's parent
177  let boundingRect = e.getBoundingClientRect();
178  let parentBoundingRect = e.parentNode.getBoundingClientRect();
179
180  return {
181    style: {
182      display: parseEnum(e.style.display),
183
184      position: parseEnum(e.style.position),
185      direction: parseEnum(e.style.direction),
186      flexDirection: parseEnum(e.style.flexDirection),
187
188      writingMode: parseEnum(e.style.writingMode),
189
190      flexWrap: parseEnum(e.style.flexWrap),
191      overflow: parseEnum(e.style.overflow),
192
193      alignItems: parseEnum(e.style.alignItems),
194      alignSelf: parseEnum(e.style.alignSelf),
195      justifyItems: parseEnum(e.style.justifyItems),
196      justifySelf: parseEnum(e.style.justifySelf),
197
198      alignContent: parseEnum(e.style.alignContent),
199      justifyContent: parseEnum(e.style.justifyContent),
200
201      flexGrow: parseNumber(e.style.flexGrow),
202      flexShrink: parseNumber(e.style.flexShrink),
203      flexBasis: parseDimension(e.style.flexBasis),
204
205      gridTemplateRows: parseGridTrackDefinitions(e.style.gridTemplateRows),
206      gridTemplateColumns: parseGridTrackDefinitions(e.style.gridTemplateColumns),
207      gridAutoRows: parseGridTrackDefinitions(e.style.gridAutoRows),
208      gridAutoColumns: parseGridTrackDefinitions(e.style.gridAutoColumns),
209      gridAutoFlow: parseGridAutoFlow(e.style.gridAutoFlow),
210
211      gridRowStart: parseGridPosition(e.style.gridRowStart),
212      gridRowEnd: parseGridPosition(e.style.gridRowEnd),
213      gridColumnStart: parseGridPosition(e.style.gridColumnStart),
214      gridColumnEnd: parseGridPosition(e.style.gridColumnEnd),
215
216      gap: parseGaps(e.style),
217
218      size: parseSize({width: e.style.width, height: e.style.height}),
219      minSize: parseSize({width: e.style.minWidth, height: e.style.minHeight}),
220      maxSize: parseSize({width: e.style.maxWidth, height: e.style.maxHeight}),
221      aspectRatio: parseRatio(e.style.aspectRatio),
222
223      margin: parseEdges({
224        left: e.style.marginLeft,
225        right: e.style.marginRight,
226        top: e.style.marginTop,
227        bottom: e.style.marginBottom,
228      }),
229
230      padding: parseEdges({
231        left: e.style.paddingLeft,
232        right: e.style.paddingRight,
233        top: e.style.paddingTop,
234        bottom: e.style.paddingBottom,
235      }),
236
237      border: parseEdges({
238        left: e.style.borderLeftWidth,
239        right: e.style.borderRightWidth,
240        top: e.style.borderTopWidth,
241        bottom: e.style.borderBottomWidth,
242      }),
243
244      inset: parseEdges({
245        left: e.style.left,
246        right: e.style.right,
247        top: e.style.top,
248        bottom: e.style.bottom,
249      }),
250    },
251
252    // The textContent is used for generating intrinsic sizing measure funcs
253    // So we're only interested in the text content of leaf nodes
254    textContent: e.childElementCount === 0 && e.textContent.length && e.textContent !== "\n" ? e.textContent : undefined,
255
256    // The layout of the node in full precision (floating-point)
257    unroundedLayout: {
258      width: boundingRect.width,
259      height: boundingRect.height,
260      x: boundingRect.x - parentBoundingRect.x,
261      y: boundingRect.y - parentBoundingRect.y,
262    },
263
264    // The naively rounded layout of the node. This is equivalent to calling Math.round() on
265    // each value in the unrounded layout individually
266    naivelyRoundedLayout: {
267      width: e.offsetWidth,
268      height: e.offsetHeight,
269      x: e.offsetLeft + e.parentNode.clientLeft,
270      y: e.offsetTop + e.parentNode.clientTop,
271    },
272
273    // The naive rounding can result in 1px gaps in the layout, so Taffy uses a smarter algorithm to avoid this.
274    // Chrome also uses a smarter algorithm, but it doesn't expose the output of that rounding.
275    // So we just emulate Taffy's computation here.
276    smartRoundedLayout: {
277      width: Math.round(boundingRect.right) - Math.round(boundingRect.left),
278      height: Math.round(boundingRect.bottom) - Math.round(boundingRect.top),
279      x: Math.round(boundingRect.x - parentBoundingRect.x),
280      y: Math.round(boundingRect.y - parentBoundingRect.y),
281    },
282
283    // Whether the test should enable rounding
284    useRounding: e.getAttribute("data-test-rounding") !== "false",
285
286    children: Array.from(e.children).map(c => describeElement(c)),
287  }
288}
289
290// Useful when developing this script. Logs the parsed style to the console when any test fixture is opened in a browser.
291window.onload = function () {
292  try {
293    console.log(describeElement(document.getElementById('test-root')));
294  } catch (e) {
295    console.error(e);
296  }
297}