@wix/zero-config-implementation 1.72.0 → 1.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,11 @@
2
2
  * CSS Properties Extractor
3
3
  *
4
4
  * Determines which CSS properties are relevant to each element
5
- * based on its tag, role, and whether it has text content.
5
+ * based on semantic traits and effective display behavior.
6
6
  */
7
7
 
8
8
  import { CSS_PROPERTIES } from '@wix/react-component-schema'
9
+ import { resolveCssPropertyValue } from '../../css/parse'
9
10
  import type { MatchedCssData } from '../../css/types'
10
11
  import type { ExtractedElement } from './core/tree-builder'
11
12
  import type { CreateElementEvent, ReactExtractor } from './core/types'
@@ -19,14 +20,9 @@ export interface CssPropertiesData {
19
20
  }
20
21
 
21
22
  // ─────────────────────────────────────────────────────────────────────────────
22
- // Element Categories
23
+ // Semantic Traits
23
24
  // ─────────────────────────────────────────────────────────────────────────────
24
25
 
25
- /**
26
- * Layout Containers - structural boxes focused on spacing and positioning
27
- */
28
- const LAYOUT_CONTAINER_TAGS = new Set(['div', 'section', 'article', 'header', 'footer', 'main', 'nav', 'aside'])
29
-
30
26
  /**
31
27
  * Text & Typography - elements where readability and hierarchy are primary
32
28
  */
@@ -53,21 +49,17 @@ const TEXT_TAGS = new Set([
53
49
  */
54
50
  const MEDIA_TAGS = new Set(['img', 'video', 'canvas', 'picture'])
55
51
 
56
- /**
57
- * Form Inputs & Interactive Elements - require state-based styling
58
- */
59
- const INTERACTIVE_TAGS = new Set(['button', 'input', 'textarea', 'select'])
60
-
61
52
  // ─────────────────────────────────────────────────────────────────────────────
62
53
  // CSS Property Sets (using logical properties)
63
54
  // ─────────────────────────────────────────────────────────────────────────────
64
55
 
65
56
  /**
66
- * CSS properties for Layout Containers and Interactive Elements
57
+ * CSS properties for box-generating elements. These are visual box styles,
58
+ * not child-layout controls such as `gap`.
67
59
  */
68
60
  const { CSS_PROPERTY_TYPE } = CSS_PROPERTIES
69
61
 
70
- const CONTAINER_CSS_PROPERTIES = [
62
+ const BOX_CSS_PROPERTIES = [
71
63
  CSS_PROPERTY_TYPE.background,
72
64
  CSS_PROPERTY_TYPE.borderTop,
73
65
  CSS_PROPERTY_TYPE.borderBottom,
@@ -113,81 +105,52 @@ const MEDIA_CSS_PROPERTIES = [
113
105
  CSS_PROPERTY_TYPE.boxShadow,
114
106
  ]
115
107
 
116
- // ─────────────────────────────────────────────────────────────────────────────
117
- // Category Detection
118
- // ─────────────────────────────────────────────────────────────────────────────
119
-
120
- type ElementCategory = 'layout' | 'text' | 'media' | 'interactive' | 'unknown'
121
-
122
- /**
123
- * Determines the category of an element based on its tag and role
124
- */
125
- function getElementCategory(tag: string, role?: string): ElementCategory {
108
+ function hasTextSemantics(tag: string, role?: string): boolean {
126
109
  const normalizedTag = tag.toLowerCase()
127
-
128
- // Check role-based overrides first
129
110
  if (role) {
130
111
  const normalizedRole = role.toLowerCase()
131
- if (normalizedRole === 'button' || normalizedRole === 'textbox') {
132
- return 'interactive'
133
- }
134
112
  if (normalizedRole === 'heading' || normalizedRole === 'paragraph') {
135
- return 'text'
136
- }
137
- if (normalizedRole === 'img' || normalizedRole === 'figure') {
138
- return 'media'
113
+ return true
139
114
  }
140
115
  }
116
+ return TEXT_TAGS.has(normalizedTag)
117
+ }
141
118
 
142
- // Check tag-based categories
143
- if (INTERACTIVE_TAGS.has(normalizedTag)) {
144
- return 'interactive'
145
- }
146
- if (TEXT_TAGS.has(normalizedTag)) {
147
- return 'text'
148
- }
149
- if (MEDIA_TAGS.has(normalizedTag)) {
150
- return 'media'
151
- }
152
- if (LAYOUT_CONTAINER_TAGS.has(normalizedTag)) {
153
- return 'layout'
119
+ function hasMediaSemantics(tag: string, role?: string): boolean {
120
+ const normalizedTag = tag.toLowerCase()
121
+ if (role) {
122
+ const normalizedRole = role.toLowerCase()
123
+ if (normalizedRole === 'img' || normalizedRole === 'figure') {
124
+ return true
125
+ }
154
126
  }
155
-
156
- return 'unknown'
127
+ return MEDIA_TAGS.has(normalizedTag)
157
128
  }
158
129
 
159
130
  /**
160
- * Gets CSS properties relevant to an element based on tag and role.
131
+ * Gets CSS properties relevant to an element from composable traits:
132
+ * text semantics, media semantics, and box-generating display.
161
133
  * Note: hasTextContent determination is deferred to tree building since
162
134
  * we can't know during createElement if a child will have text content.
163
135
  */
164
136
  export function getCssPropertiesForTag(tag: string, role?: string): string[] {
165
- const category = getElementCategory(tag, role)
166
137
  const properties: string[] = []
138
+ const hasTextProperties = hasTextSemantics(tag, role)
139
+ const hasMediaProperties = hasMediaSemantics(tag, role)
140
+ const hasBoxProperties = hasContainerLikeDefaultDisplay(tag) && !hasMediaProperties
167
141
 
168
- switch (category) {
169
- case 'layout':
170
- case 'interactive':
171
- properties.push(...CONTAINER_CSS_PROPERTIES)
172
- break
173
-
174
- case 'text':
175
- properties.push(...TEXT_CSS_PROPERTIES)
176
- break
177
-
178
- case 'media':
179
- properties.push(...MEDIA_CSS_PROPERTIES)
180
- break
181
-
182
- case 'unknown':
183
- // For unknown elements, default to container properties
184
- // (can be refined during tree building based on children)
185
- properties.push(...CONTAINER_CSS_PROPERTIES)
186
- break
142
+ if (hasTextProperties) {
143
+ properties.push(...TEXT_CSS_PROPERTIES)
144
+ }
145
+ if (hasMediaProperties) {
146
+ properties.push(...MEDIA_CSS_PROPERTIES)
147
+ }
148
+ if (hasBoxProperties) {
149
+ properties.push(...BOX_CSS_PROPERTIES)
187
150
  }
188
151
 
189
152
  properties.push(CSS_PROPERTY_TYPE.display)
190
- return properties
153
+ return [...new Set(properties)]
191
154
  }
192
155
 
193
156
  /**
@@ -204,6 +167,19 @@ export function addTextProperties(existing: string[]): string[] {
204
167
  return result
205
168
  }
206
169
 
170
+ /**
171
+ * Adds box CSS properties to an existing property list without duplicates.
172
+ */
173
+ export function addBoxProperties(existing: string[]): string[] {
174
+ const result = [...existing]
175
+ for (const property of BOX_CSS_PROPERTIES) {
176
+ if (!result.includes(property)) {
177
+ result.push(property)
178
+ }
179
+ }
180
+ return result
181
+ }
182
+
207
183
  /**
208
184
  * Adds gap CSS property to an existing property list if not already present.
209
185
  */
@@ -216,21 +192,91 @@ export function addGapProperty(existing: string[]): string[] {
216
192
  // Display Value Resolution
217
193
  // ─────────────────────────────────────────────────────────────────────────────
218
194
 
195
+ // Based on WebKit and Blink UA stylesheets. The map is intentionally limited
196
+ // to element defaults rather than pseudo-elements or internal control parts.
219
197
  const TAG_DEFAULT_DISPLAY: Record<string, string> = {
220
- span: 'inline',
221
198
  a: 'inline',
222
- strong: 'inline',
223
- em: 'inline',
199
+ address: 'block',
200
+ area: 'none',
201
+ article: 'block',
202
+ aside: 'block',
203
+ audio: 'inline',
224
204
  b: 'inline',
205
+ base: 'none',
206
+ basefont: 'none',
207
+ blockquote: 'block',
208
+ body: 'block',
209
+ button: 'inline-block',
210
+ canvas: 'inline',
211
+ caption: 'table-caption',
212
+ center: 'block',
213
+ col: 'table-column',
214
+ colgroup: 'table-column-group',
215
+ datalist: 'none',
216
+ dd: 'block',
217
+ dir: 'block',
218
+ div: 'block',
219
+ dl: 'block',
220
+ dt: 'block',
221
+ em: 'inline',
222
+ embed: 'inline',
223
+ fieldset: 'block',
224
+ figcaption: 'block',
225
+ figure: 'block',
226
+ footer: 'block',
227
+ form: 'block',
228
+ h1: 'block',
229
+ h2: 'block',
230
+ h3: 'block',
231
+ h4: 'block',
232
+ h5: 'block',
233
+ h6: 'block',
234
+ head: 'none',
235
+ header: 'block',
236
+ hgroup: 'block',
237
+ hr: 'block',
238
+ html: 'block',
225
239
  i: 'inline',
226
- label: 'inline',
240
+ iframe: 'inline',
227
241
  img: 'inline',
228
- input: 'inline',
229
- select: 'inline',
230
- textarea: 'inline',
231
- button: 'inline-block',
242
+ input: 'inline-block',
243
+ label: 'inline',
244
+ legend: 'block',
232
245
  li: 'list-item',
246
+ link: 'none',
247
+ main: 'block',
248
+ map: 'inline',
249
+ marquee: 'inline-block',
250
+ menu: 'block',
251
+ meta: 'none',
252
+ nav: 'block',
253
+ noembed: 'none',
254
+ noframes: 'none',
255
+ object: 'inline',
256
+ ol: 'block',
257
+ p: 'block',
258
+ param: 'none',
259
+ q: 'inline',
260
+ rp: 'none',
261
+ script: 'none',
262
+ search: 'block',
263
+ section: 'block',
264
+ select: 'inline-block',
265
+ span: 'inline',
266
+ strong: 'inline',
267
+ style: 'none',
233
268
  table: 'table',
269
+ tbody: 'table-row-group',
270
+ td: 'table-cell',
271
+ template: 'none',
272
+ textarea: 'inline-block',
273
+ tfoot: 'table-footer-group',
274
+ th: 'table-cell',
275
+ thead: 'table-header-group',
276
+ title: 'none',
277
+ tr: 'table-row',
278
+ ul: 'block',
279
+ video: 'inline',
234
280
  }
235
281
 
236
282
  export function getDefaultDisplayForTag(tag: string): string {
@@ -238,27 +284,44 @@ export function getDefaultDisplayForTag(tag: string): string {
238
284
  }
239
285
 
240
286
  /**
241
- * Resolves the current `display` value from matched CSS data, handling both
242
- * literal values and CSS variable references.
243
- * Returns the resolved value, or `undefined` if `display` is not declared
244
- * or uses a variable that cannot be resolved.
287
+ * Resolves the current `display` value from matched CSS data using the
288
+ * CSS-layer value resolver, including var() fallback handling.
245
289
  */
246
- export function resolveDisplayValue(matcherData: MatchedCssData): string | undefined {
290
+ function getDisplayPropertyMatch(matcherData: MatchedCssData) {
291
+ let matchedDisplayProperty: (typeof matcherData.matches)[number]['properties'][number] | undefined
292
+ let matchedDisplaySpecificity: [number, number, number] | undefined
293
+
247
294
  for (const match of matcherData.matches) {
248
295
  for (const property of match.properties) {
249
- if (property.name !== 'display') continue
250
-
251
- if (!property.varRefs || property.varRefs.length === 0) {
252
- return property.value
296
+ if (property.name === 'display') {
297
+ if (!matchedDisplaySpecificity || compareSpecificity(match.specificity, matchedDisplaySpecificity) >= 0) {
298
+ matchedDisplayProperty = property
299
+ matchedDisplaySpecificity = match.specificity
300
+ }
253
301
  }
302
+ }
303
+ }
304
+ return matchedDisplayProperty
305
+ }
254
306
 
255
- for (const varName of property.varRefs) {
256
- const resolvedValue = matcherData.customProperties[varName]
257
- if (resolvedValue !== undefined) return resolvedValue
258
- }
307
+ function compareSpecificity(
308
+ leftSpecificity: [number, number, number],
309
+ rightSpecificity: [number, number, number],
310
+ ): number {
311
+ for (let specificityIndex = 0; specificityIndex < leftSpecificity.length; specificityIndex += 1) {
312
+ const difference = leftSpecificity[specificityIndex] - rightSpecificity[specificityIndex]
313
+ if (difference !== 0) {
314
+ return difference
259
315
  }
260
316
  }
261
- return undefined
317
+
318
+ return 0
319
+ }
320
+
321
+ export function resolveDisplayValue(matcherData: MatchedCssData): string | undefined {
322
+ const displayProperty = getDisplayPropertyMatch(matcherData)
323
+ if (!displayProperty) return undefined
324
+ return resolveCssPropertyValue(displayProperty, matcherData.customProperties)
262
325
  }
263
326
 
264
327
  // ─────────────────────────────────────────────────────────────────────────────
@@ -266,20 +329,62 @@ export function resolveDisplayValue(matcherData: MatchedCssData): string | undef
266
329
  // ─────────────────────────────────────────────────────────────────────────────
267
330
 
268
331
  const FLEX_GRID_DISPLAY_VALUES = new Set(['flex', 'grid', 'inline-flex', 'inline-grid'])
332
+ const NON_CONTAINER_DISPLAY_VALUES = new Set(['inline', 'contents', 'none'])
333
+ const BOX_GENERATING_DISPLAY_VALUES = new Set([
334
+ 'block',
335
+ 'flex',
336
+ 'grid',
337
+ 'flow-root',
338
+ 'inline-block',
339
+ 'inline-flex',
340
+ 'inline-grid',
341
+ 'inline-table',
342
+ 'table',
343
+ 'list-item',
344
+ 'table-caption',
345
+ 'table-cell',
346
+ 'table-column',
347
+ 'table-column-group',
348
+ 'table-footer-group',
349
+ 'table-header-group',
350
+ 'table-row',
351
+ 'table-row-group',
352
+ ])
353
+
354
+ function normalizeDisplayValue(displayValue: string): string {
355
+ return displayValue.trim().toLowerCase().replace(/\s+/g, ' ')
356
+ }
357
+
358
+ function displayCreatesContainerBox(displayValue: string): boolean {
359
+ const normalizedDisplayValue = normalizeDisplayValue(displayValue)
360
+ if (NON_CONTAINER_DISPLAY_VALUES.has(normalizedDisplayValue)) return false
361
+
362
+ const displayTokens = normalizedDisplayValue.split(' ')
363
+
364
+ return displayTokens.some((displayToken) => BOX_GENERATING_DISPLAY_VALUES.has(displayToken))
365
+ }
366
+
367
+ function displayUsesFlexOrGridLayout(displayValue: string): boolean {
368
+ const normalizedDisplayValue = normalizeDisplayValue(displayValue)
369
+ const displayTokens = normalizedDisplayValue.split(' ')
370
+
371
+ return displayTokens.some((displayToken) => FLEX_GRID_DISPLAY_VALUES.has(displayToken))
372
+ }
269
373
 
270
374
  /**
271
375
  * Returns true if display uses a CSS variable that cannot be resolved --
272
376
  * we optimistically assume it could be flex/grid.
273
377
  */
274
378
  function hasUnresolvableDisplayVar(matcherData: MatchedCssData): boolean {
379
+ const displayProperty = getDisplayPropertyMatch(matcherData)
380
+ if (!displayProperty?.varRefs || displayProperty.varRefs.length === 0) return false
381
+ return resolveCssPropertyValue(displayProperty, matcherData.customProperties) === undefined
382
+ }
383
+
384
+ function hasDisplayDeclaration(matcherData: MatchedCssData): boolean {
275
385
  for (const match of matcherData.matches) {
276
386
  for (const property of match.properties) {
277
- if (property.name !== 'display') continue
278
- if (property.varRefs && property.varRefs.length > 0) {
279
- for (const varName of property.varRefs) {
280
- if (matcherData.customProperties[varName] === undefined) return true
281
- }
282
- }
387
+ if (property.name === 'display') return true
283
388
  }
284
389
  }
285
390
  return false
@@ -287,10 +392,28 @@ function hasUnresolvableDisplayVar(matcherData: MatchedCssData): boolean {
287
392
 
288
393
  export function hasFlexOrGridDisplay(matcherData: MatchedCssData): boolean {
289
394
  const displayValue = resolveDisplayValue(matcherData)
290
- if (displayValue !== undefined) return FLEX_GRID_DISPLAY_VALUES.has(displayValue)
395
+ if (displayValue !== undefined) return displayUsesFlexOrGridLayout(displayValue)
291
396
  return hasUnresolvableDisplayVar(matcherData)
292
397
  }
293
398
 
399
+ export function hasContainerLikeDisplay(matcherData: MatchedCssData): boolean {
400
+ const displayValue = resolveDisplayValue(matcherData)
401
+ if (displayValue !== undefined) return displayCreatesContainerBox(displayValue)
402
+ return hasUnresolvableDisplayVar(matcherData)
403
+ }
404
+
405
+ function hasContainerLikeDefaultDisplay(tag: string): boolean {
406
+ return displayCreatesContainerBox(getDefaultDisplayForTag(tag))
407
+ }
408
+
409
+ function hasEffectiveContainerDisplay(tag: string, matcherData?: MatchedCssData): boolean {
410
+ if (matcherData && hasDisplayDeclaration(matcherData)) {
411
+ return hasContainerLikeDisplay(matcherData)
412
+ }
413
+
414
+ return hasContainerLikeDefaultDisplay(tag)
415
+ }
416
+
294
417
  /**
295
418
  * Walks the element tree and adds `gap` to relevant CSS properties
296
419
  * for elements that have display: flex|grid and more than 1 child.
@@ -316,6 +439,31 @@ export function enrichGapProperties(elements: ExtractedElement[]): ExtractedElem
316
439
  })
317
440
  }
318
441
 
442
+ /**
443
+ * Walks the element tree and adds box properties to selected text tags
444
+ * when CSS overrides `display` or default UA display creates a box.
445
+ */
446
+ export function enrichContainerProperties(elements: ExtractedElement[]): ExtractedElement[] {
447
+ return elements.map((element) => {
448
+ const role = element.attributes.role
449
+ if (hasTextSemantics(element.tag, role)) {
450
+ const matcherData = element.extractorData.get('css-matcher') as MatchedCssData | undefined
451
+ const cssData = element.extractorData.get('css-properties') as CssPropertiesData | undefined
452
+
453
+ if (cssData && hasEffectiveContainerDisplay(element.tag, matcherData)) {
454
+ element.extractorData.set('css-properties', {
455
+ relevant: addBoxProperties(cssData.relevant),
456
+ })
457
+ }
458
+ }
459
+
460
+ return {
461
+ ...element,
462
+ children: enrichContainerProperties(element.children),
463
+ }
464
+ })
465
+ }
466
+
319
467
  // ─────────────────────────────────────────────────────────────────────────────
320
468
  // Factory
321
469
  // ─────────────────────────────────────────────────────────────────────────────
@@ -27,6 +27,7 @@ export type { PropTrackerData, PropTrackerExtractorState } from './prop-tracker'
27
27
  export {
28
28
  createCssPropertiesExtractor,
29
29
  enrichGapProperties,
30
+ enrichContainerProperties,
30
31
  resolveDisplayValue,
31
32
  getDefaultDisplayForTag,
32
33
  } from './css-properties'
@@ -17,6 +17,7 @@ export {
17
17
  createPropTrackerExtractor,
18
18
  createCssPropertiesExtractor,
19
19
  enrichGapProperties,
20
+ enrichContainerProperties,
20
21
  resolveDisplayValue,
21
22
  getDefaultDisplayForTag,
22
23
  inferSupportedNativeStates,
@@ -9,6 +9,7 @@ import {
9
9
  type RunExtractorsOptions,
10
10
  createCssPropertiesExtractor,
11
11
  createPropTrackerExtractor,
12
+ enrichContainerProperties,
12
13
  enrichGapProperties,
13
14
  runExtractors,
14
15
  } from './information-extractors/react'
@@ -163,7 +164,8 @@ export function processComponent(
163
164
  if (html && extractedElements.length > 0 && css.length > 0) {
164
165
  try {
165
166
  const matchResult = matchCssSelectors(html, extractedElements, css)
166
- const gapEnrichedElements = enrichGapProperties(matchResult.elements)
167
+ const containerEnrichedElements = enrichContainerProperties(matchResult.elements)
168
+ const gapEnrichedElements = enrichGapProperties(containerEnrichedElements)
167
169
  enhancedInfo = {
168
170
  ...enhancedInfo,
169
171
  elements: convertElements(gapEnrichedElements),