@wix/zero-config-implementation 1.98.0 → 1.100.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.
- package/dist/index.d.ts +4 -0
- package/dist/index.js +10442 -10349
- package/package.json +2 -2
- package/src/component-renderer.ts +22 -5
- package/src/converters/css-longhand-resolver.test.ts +35 -0
- package/src/converters/css-longhand-resolver.ts +22 -0
- package/src/converters/states-builder.test.ts +1 -0
- package/src/converters/to-editor-component.test.ts +66 -0
- package/src/converters/to-editor-component.ts +13 -2
- package/src/information-extractors/react/extractors/core/tree-builder.test.ts +186 -0
- package/src/information-extractors/react/extractors/core/tree-builder.ts +41 -5
- package/src/information-extractors/react/extractors/core/types.ts +2 -0
- package/src/information-extractors/react/extractors/css-properties.test.ts +148 -2
- package/src/information-extractors/react/extractors/css-properties.ts +163 -6
- package/src/information-extractors/react/extractors/index.ts +2 -0
- package/src/information-extractors/react/extractors/prop-tracker.test.ts +19 -1
- package/src/information-extractors/react/extractors/prop-tracker.ts +3 -0
- package/src/information-extractors/react/extractors/state-markers.test.ts +1 -0
- package/src/information-extractors/react/index.ts +2 -0
- package/src/manifest-pipeline.ts +3 -1
|
@@ -5,6 +5,7 @@ import type { MatchedCssData } from '../../css/types'
|
|
|
5
5
|
import type { ExtractedElement } from './core/tree-builder'
|
|
6
6
|
import {
|
|
7
7
|
enrichContainerProperties,
|
|
8
|
+
enrichFlexProperties,
|
|
8
9
|
getCssPropertiesForTag,
|
|
9
10
|
getDefaultDisplayForTag,
|
|
10
11
|
hasContainerLikeDisplay,
|
|
@@ -173,8 +174,8 @@ describe('getCssPropertiesForTag', () => {
|
|
|
173
174
|
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
|
|
174
175
|
})
|
|
175
176
|
|
|
176
|
-
it('gives
|
|
177
|
-
for (const tag of ['
|
|
177
|
+
it('gives controls that render their own text both box and text properties', () => {
|
|
178
|
+
for (const tag of ['select', 'textarea']) {
|
|
178
179
|
const relevantProperties = getCssPropertiesForTag(tag)
|
|
179
180
|
|
|
180
181
|
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
|
|
@@ -183,6 +184,16 @@ describe('getCssPropertiesForTag', () => {
|
|
|
183
184
|
}
|
|
184
185
|
})
|
|
185
186
|
|
|
187
|
+
it('treats a button as box-generating but not text-bearing, except for color', () => {
|
|
188
|
+
const relevantProperties = getCssPropertiesForTag('button')
|
|
189
|
+
|
|
190
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
|
|
191
|
+
// `color` resolves `currentColor` in borders, outlines and fills, text or not
|
|
192
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.color)
|
|
193
|
+
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
|
|
194
|
+
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.textAlign)
|
|
195
|
+
})
|
|
196
|
+
|
|
186
197
|
it('treats an input with no type as text-bearing', () => {
|
|
187
198
|
const relevantProperties = getCssPropertiesForTag('input')
|
|
188
199
|
|
|
@@ -242,6 +253,131 @@ describe('getCssPropertiesForTag', () => {
|
|
|
242
253
|
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
|
|
243
254
|
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
|
|
244
255
|
})
|
|
256
|
+
|
|
257
|
+
it('gives block/replaced elements the full margin family regardless of box/text/media semantics', () => {
|
|
258
|
+
for (const tag of ['div', 'p', 'img']) {
|
|
259
|
+
const relevantProperties = getCssPropertiesForTag(tag)
|
|
260
|
+
|
|
261
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginTop)
|
|
262
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginBottom)
|
|
263
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineStart)
|
|
264
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineEnd)
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('gives non-replaced inline elements only the horizontal margins, per CSS margin-on-inline-boxes rules', () => {
|
|
269
|
+
for (const tag of ['span', 'a', 'em', 'b', 'i']) {
|
|
270
|
+
const relevantProperties = getCssPropertiesForTag(tag)
|
|
271
|
+
|
|
272
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineStart)
|
|
273
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineEnd)
|
|
274
|
+
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginTop)
|
|
275
|
+
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginBottom)
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('gives internal table display elements no margin at all, per CSS 2.1 §17.6.1', () => {
|
|
280
|
+
for (const tag of ['tr', 'thead', 'tbody', 'tfoot', 'col', 'colgroup', 'td', 'th']) {
|
|
281
|
+
const relevantProperties = getCssPropertiesForTag(tag)
|
|
282
|
+
|
|
283
|
+
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginTop)
|
|
284
|
+
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginBottom)
|
|
285
|
+
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineStart)
|
|
286
|
+
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineEnd)
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('gives box-generating elements width, height, overflow and mix-blend-mode', () => {
|
|
291
|
+
const relevantProperties = getCssPropertiesForTag('div')
|
|
292
|
+
|
|
293
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.width)
|
|
294
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.height)
|
|
295
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.overflow)
|
|
296
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.mixBlendMode)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('gives media elements sizing, object-fit/position and mix-blend-mode', () => {
|
|
300
|
+
const relevantProperties = getCssPropertiesForTag('img')
|
|
301
|
+
|
|
302
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.width)
|
|
303
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.height)
|
|
304
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.overflow)
|
|
305
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.objectFit)
|
|
306
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.objectPosition)
|
|
307
|
+
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.mixBlendMode)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it('keeps text-only tags without width/height/overflow/object-fit', () => {
|
|
311
|
+
const relevantProperties = getCssPropertiesForTag('p')
|
|
312
|
+
|
|
313
|
+
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.width)
|
|
314
|
+
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.height)
|
|
315
|
+
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.objectFit)
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
describe('enrichFlexProperties', () => {
|
|
320
|
+
it('adds flexDirection/justifyContent/alignItems to a flex container', () => {
|
|
321
|
+
const elements = [createElementWithCssProperties('div', 'display: flex')]
|
|
322
|
+
|
|
323
|
+
const [enrichedElement] = enrichFlexProperties(elements)
|
|
324
|
+
const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
|
|
325
|
+
|
|
326
|
+
expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.flexDirection)
|
|
327
|
+
expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.justifyContent)
|
|
328
|
+
expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignItems)
|
|
329
|
+
expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignSelf)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('does not add flex container properties to a non-flex/grid element', () => {
|
|
333
|
+
const elements = [createElementWithCssProperties('div', 'display: block')]
|
|
334
|
+
|
|
335
|
+
const [enrichedElement] = enrichFlexProperties(elements)
|
|
336
|
+
const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
|
|
337
|
+
|
|
338
|
+
expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.flexDirection)
|
|
339
|
+
expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.justifyContent)
|
|
340
|
+
expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignItems)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('adds alignSelf to direct children of a flex/grid container', () => {
|
|
344
|
+
const child = createElementWithCssProperties('span', '')
|
|
345
|
+
const parent: ExtractedElement = {
|
|
346
|
+
...createElementWithCssProperties('div', 'display: flex'),
|
|
347
|
+
children: [child],
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const [enrichedParent] = enrichFlexProperties([parent])
|
|
351
|
+
const enrichedChild = enrichedParent.children[0]
|
|
352
|
+
const childCssData = enrichedChild.extractorData.get('css-properties') as { relevant: string[] }
|
|
353
|
+
|
|
354
|
+
expect(childCssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignSelf)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('does not add alignSelf to children of a non-flex/grid container', () => {
|
|
358
|
+
const child = createElementWithCssProperties('span', '')
|
|
359
|
+
const parent: ExtractedElement = {
|
|
360
|
+
...createElementWithCssProperties('div', 'display: block'),
|
|
361
|
+
children: [child],
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const [enrichedParent] = enrichFlexProperties([parent])
|
|
365
|
+
const enrichedChild = enrichedParent.children[0]
|
|
366
|
+
const childCssData = enrichedChild.extractorData.get('css-properties') as { relevant: string[] }
|
|
367
|
+
|
|
368
|
+
expect(childCssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignSelf)
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('adds flex container properties to a grid container as well', () => {
|
|
372
|
+
const elements = [createElementWithCssProperties('div', 'display: grid')]
|
|
373
|
+
|
|
374
|
+
const [enrichedElement] = enrichFlexProperties(elements)
|
|
375
|
+
const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
|
|
376
|
+
|
|
377
|
+
expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.flexDirection)
|
|
378
|
+
expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.justifyContent)
|
|
379
|
+
expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignItems)
|
|
380
|
+
})
|
|
245
381
|
})
|
|
246
382
|
|
|
247
383
|
describe('getDefaultDisplayForTag', () => {
|
|
@@ -394,4 +530,14 @@ describe('enrichContainerProperties', () => {
|
|
|
394
530
|
expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
|
|
395
531
|
expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
|
|
396
532
|
})
|
|
533
|
+
|
|
534
|
+
it('adds box properties to span when display is signaled only via the --display root convention, not a literal display declaration', () => {
|
|
535
|
+
const elements = [createElementWithCssProperties('span', '--display: block')]
|
|
536
|
+
|
|
537
|
+
const [enrichedElement] = enrichContainerProperties(elements)
|
|
538
|
+
const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
|
|
539
|
+
|
|
540
|
+
expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
|
|
541
|
+
expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
|
|
542
|
+
})
|
|
397
543
|
})
|
|
@@ -30,7 +30,8 @@ export interface SemanticAttributes {
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Text & Typography - elements where readability and hierarchy are primary.
|
|
33
|
-
*
|
|
33
|
+
* `select`, `textarea`, and `input` render their own text; `input` varies by type
|
|
34
|
+
* (see below). `button` is absent on purpose - see CONTENT_DEPENDENT_TEXT_TAGS.
|
|
34
35
|
*/
|
|
35
36
|
const TEXT_TAGS = new Set([
|
|
36
37
|
'h1',
|
|
@@ -48,11 +49,21 @@ const TEXT_TAGS = new Set([
|
|
|
48
49
|
'em',
|
|
49
50
|
'b',
|
|
50
51
|
'i',
|
|
51
|
-
'button',
|
|
52
52
|
'select',
|
|
53
53
|
'textarea',
|
|
54
54
|
])
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Tags whose text comes from their content rather than their own rendering, so
|
|
58
|
+
* tree building decides once the rendered subtree is known.
|
|
59
|
+
*/
|
|
60
|
+
const CONTENT_DEPENDENT_TEXT_TAGS = new Set(['button'])
|
|
61
|
+
|
|
62
|
+
/** Whether the tag's content, rather than the tag itself, decides typography. */
|
|
63
|
+
export function rendersTextThroughContent(tag: string): boolean {
|
|
64
|
+
return CONTENT_DEPENDENT_TEXT_TAGS.has(tag.toLowerCase())
|
|
65
|
+
}
|
|
66
|
+
|
|
56
67
|
/** Anything else - including a missing or invalid `type` - defaults to text. */
|
|
57
68
|
const NON_TEXT_INPUT_TYPES = new Set(['checkbox', 'radio', 'range', 'color', 'file', 'image', 'hidden'])
|
|
58
69
|
|
|
@@ -88,6 +99,70 @@ const BOX_CSS_PROPERTIES = [
|
|
|
88
99
|
CSS_PROPERTY_TYPE.borderEndStartRadius,
|
|
89
100
|
CSS_PROPERTY_TYPE.borderEndEndRadius,
|
|
90
101
|
CSS_PROPERTY_TYPE.boxShadow,
|
|
102
|
+
CSS_PROPERTY_TYPE.width,
|
|
103
|
+
CSS_PROPERTY_TYPE.height,
|
|
104
|
+
CSS_PROPERTY_TYPE.overflow,
|
|
105
|
+
CSS_PROPERTY_TYPE.mixBlendMode,
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Margin applies to most DOM elements regardless of box/text/media
|
|
110
|
+
* semantics, so it's appended in `getCssPropertiesForTag` rather than gated
|
|
111
|
+
* to one of the trait-based sets above — except for the display-dependent
|
|
112
|
+
* carve-outs in `getMarginPropertiesForTag` below.
|
|
113
|
+
*/
|
|
114
|
+
const MARGIN_CSS_PROPERTIES = [
|
|
115
|
+
CSS_PROPERTY_TYPE.marginTop,
|
|
116
|
+
CSS_PROPERTY_TYPE.marginBottom,
|
|
117
|
+
CSS_PROPERTY_TYPE.marginInlineStart,
|
|
118
|
+
CSS_PROPERTY_TYPE.marginInlineEnd,
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
const HORIZONTAL_MARGIN_CSS_PROPERTIES = [CSS_PROPERTY_TYPE.marginInlineStart, CSS_PROPERTY_TYPE.marginInlineEnd]
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Per CSS 2.1 §17.6.1, margin does not apply to elements with these internal
|
|
125
|
+
* table display values — the table layout algorithm ignores it entirely.
|
|
126
|
+
*/
|
|
127
|
+
const MARGIN_UNSUPPORTED_DISPLAY_VALUES = new Set([
|
|
128
|
+
'table-row',
|
|
129
|
+
'table-row-group',
|
|
130
|
+
'table-header-group',
|
|
131
|
+
'table-footer-group',
|
|
132
|
+
'table-column',
|
|
133
|
+
'table-column-group',
|
|
134
|
+
'table-cell',
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Margin properties relevant to a tag's default display. Internal table
|
|
139
|
+
* display values (`tr`, `thead`/`tbody`/`tfoot`, `col`/`colgroup`, `td`/`th`)
|
|
140
|
+
* don't honor margin at all. Non-replaced inline elements (`span`, `a`, …)
|
|
141
|
+
* only honor the horizontal (inline) margins — `margin-top`/`margin-bottom`
|
|
142
|
+
* have no effect on them. Replaced inline elements (`img`, `video`, …) honor
|
|
143
|
+
* margin on every side, same as a block box.
|
|
144
|
+
*/
|
|
145
|
+
function getMarginPropertiesForTag(tag: string): string[] {
|
|
146
|
+
const normalizedTag = tag.toLowerCase()
|
|
147
|
+
const defaultDisplay = getDefaultDisplayForTag(normalizedTag)
|
|
148
|
+
|
|
149
|
+
if (MARGIN_UNSUPPORTED_DISPLAY_VALUES.has(defaultDisplay)) {
|
|
150
|
+
return []
|
|
151
|
+
}
|
|
152
|
+
if (defaultDisplay === 'inline' && !MEDIA_TAGS.has(normalizedTag)) {
|
|
153
|
+
return HORIZONTAL_MARGIN_CSS_PROPERTIES
|
|
154
|
+
}
|
|
155
|
+
return MARGIN_CSS_PROPERTIES
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Child-layout controls for flex/grid containers, added post-hoc by
|
|
160
|
+
* `enrichFlexProperties` once the element's resolved `display` is known.
|
|
161
|
+
*/
|
|
162
|
+
const FLEX_CONTAINER_CSS_PROPERTIES = [
|
|
163
|
+
CSS_PROPERTY_TYPE.flexDirection,
|
|
164
|
+
CSS_PROPERTY_TYPE.justifyContent,
|
|
165
|
+
CSS_PROPERTY_TYPE.alignItems,
|
|
91
166
|
]
|
|
92
167
|
|
|
93
168
|
/**
|
|
@@ -117,6 +192,12 @@ const MEDIA_CSS_PROPERTIES = [
|
|
|
117
192
|
CSS_PROPERTY_TYPE.borderEndStartRadius,
|
|
118
193
|
CSS_PROPERTY_TYPE.borderEndEndRadius,
|
|
119
194
|
CSS_PROPERTY_TYPE.boxShadow,
|
|
195
|
+
CSS_PROPERTY_TYPE.width,
|
|
196
|
+
CSS_PROPERTY_TYPE.height,
|
|
197
|
+
CSS_PROPERTY_TYPE.overflow,
|
|
198
|
+
CSS_PROPERTY_TYPE.objectFit,
|
|
199
|
+
CSS_PROPERTY_TYPE.objectPosition,
|
|
200
|
+
CSS_PROPERTY_TYPE.mixBlendMode,
|
|
120
201
|
]
|
|
121
202
|
|
|
122
203
|
function hasTextSemantics(tag: string, attributes: SemanticAttributes = {}): boolean {
|
|
@@ -157,6 +238,10 @@ export function getCssPropertiesForTag(tag: string, attributes: SemanticAttribut
|
|
|
157
238
|
|
|
158
239
|
if (hasTextProperties) {
|
|
159
240
|
properties.push(...TEXT_CSS_PROPERTIES)
|
|
241
|
+
} else if (rendersTextThroughContent(tag)) {
|
|
242
|
+
// `color` resolves `currentColor` in borders, outlines and fills even with no
|
|
243
|
+
// text; the rest of the typography waits until tree building sees the content.
|
|
244
|
+
properties.push(CSS_PROPERTY_TYPE.color)
|
|
160
245
|
}
|
|
161
246
|
if (hasMediaProperties) {
|
|
162
247
|
properties.push(...MEDIA_CSS_PROPERTIES)
|
|
@@ -165,6 +250,7 @@ export function getCssPropertiesForTag(tag: string, attributes: SemanticAttribut
|
|
|
165
250
|
properties.push(...BOX_CSS_PROPERTIES)
|
|
166
251
|
}
|
|
167
252
|
|
|
253
|
+
properties.push(...getMarginPropertiesForTag(tag))
|
|
168
254
|
properties.push(CSS_PROPERTY_TYPE.display)
|
|
169
255
|
return [...new Set(properties)]
|
|
170
256
|
}
|
|
@@ -204,6 +290,27 @@ export function addGapProperty(existing: string[]): string[] {
|
|
|
204
290
|
return [...existing, CSS_PROPERTY_TYPE.gap]
|
|
205
291
|
}
|
|
206
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Adds flex/grid container CSS properties to an existing property list without duplicates.
|
|
295
|
+
*/
|
|
296
|
+
export function addFlexContainerProperties(existing: string[]): string[] {
|
|
297
|
+
const result = [...existing]
|
|
298
|
+
for (const property of FLEX_CONTAINER_CSS_PROPERTIES) {
|
|
299
|
+
if (!result.includes(property)) {
|
|
300
|
+
result.push(property)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return result
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Adds the `alignSelf` CSS property to an existing property list if not already present.
|
|
308
|
+
*/
|
|
309
|
+
export function addAlignSelfProperty(existing: string[]): string[] {
|
|
310
|
+
if (existing.includes(CSS_PROPERTY_TYPE.alignSelf)) return existing
|
|
311
|
+
return [...existing, CSS_PROPERTY_TYPE.alignSelf]
|
|
312
|
+
}
|
|
313
|
+
|
|
207
314
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
208
315
|
// Display Value Resolution
|
|
209
316
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -397,7 +504,7 @@ function hasUnresolvableDisplayVar(matcherData: MatchedCssData): boolean {
|
|
|
397
504
|
return resolveCssPropertyValue(displayProperty, matcherData.customProperties) === undefined
|
|
398
505
|
}
|
|
399
506
|
|
|
400
|
-
function
|
|
507
|
+
function hasLiteralDisplayDeclaration(matcherData: MatchedCssData): boolean {
|
|
401
508
|
for (const match of matcherData.matches) {
|
|
402
509
|
for (const property of match.properties) {
|
|
403
510
|
if (property.name === 'display') return true
|
|
@@ -406,14 +513,30 @@ function hasDisplayDeclaration(matcherData: MatchedCssData): boolean {
|
|
|
406
513
|
return false
|
|
407
514
|
}
|
|
408
515
|
|
|
516
|
+
export const ROOT_DISPLAY_CUSTOM_PROPERTY = '--display'
|
|
517
|
+
|
|
518
|
+
function hasEffectiveDisplayDeclaration(matcherData: MatchedCssData): boolean {
|
|
519
|
+
return (
|
|
520
|
+
hasLiteralDisplayDeclaration(matcherData) ||
|
|
521
|
+
matcherData.customProperties[ROOT_DISPLAY_CUSTOM_PROPERTY] !== undefined
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function resolveEffectiveDisplayValue(matcherData: MatchedCssData): string | undefined {
|
|
526
|
+
const resolved = resolveDisplayValue(matcherData)
|
|
527
|
+
if (resolved !== undefined) return resolved
|
|
528
|
+
if (hasLiteralDisplayDeclaration(matcherData)) return undefined
|
|
529
|
+
return matcherData.customProperties[ROOT_DISPLAY_CUSTOM_PROPERTY]
|
|
530
|
+
}
|
|
531
|
+
|
|
409
532
|
export function hasFlexOrGridDisplay(matcherData: MatchedCssData): boolean {
|
|
410
|
-
const displayValue =
|
|
533
|
+
const displayValue = resolveEffectiveDisplayValue(matcherData)
|
|
411
534
|
if (displayValue !== undefined) return displayUsesFlexOrGridLayout(displayValue)
|
|
412
535
|
return hasUnresolvableDisplayVar(matcherData)
|
|
413
536
|
}
|
|
414
537
|
|
|
415
538
|
export function hasContainerLikeDisplay(matcherData: MatchedCssData): boolean {
|
|
416
|
-
const displayValue =
|
|
539
|
+
const displayValue = resolveEffectiveDisplayValue(matcherData)
|
|
417
540
|
if (displayValue !== undefined) return displayCreatesContainerBox(displayValue)
|
|
418
541
|
return hasUnresolvableDisplayVar(matcherData)
|
|
419
542
|
}
|
|
@@ -441,7 +564,7 @@ function hasDefaultBoxProperties(tag: string, role?: string): boolean {
|
|
|
441
564
|
}
|
|
442
565
|
|
|
443
566
|
function hasEffectiveContainerDisplay(tag: string, role?: string, matcherData?: MatchedCssData): boolean {
|
|
444
|
-
if (matcherData &&
|
|
567
|
+
if (matcherData && hasEffectiveDisplayDeclaration(matcherData)) {
|
|
445
568
|
return hasContainerLikeDisplay(matcherData)
|
|
446
569
|
}
|
|
447
570
|
|
|
@@ -473,6 +596,40 @@ export function enrichGapProperties(elements: ExtractedElement[]): ExtractedElem
|
|
|
473
596
|
})
|
|
474
597
|
}
|
|
475
598
|
|
|
599
|
+
/**
|
|
600
|
+
* Walks the element tree and adds flex/grid layout controls to relevant CSS
|
|
601
|
+
* properties: `flexDirection`/`justifyContent`/`alignItems` on elements whose
|
|
602
|
+
* own display is flex|grid, and `alignSelf` on their direct children (the
|
|
603
|
+
* property that actually targets flex/grid items per the CSS spec).
|
|
604
|
+
* Must be called after CSS selector matching (requires css-matcher data).
|
|
605
|
+
*/
|
|
606
|
+
export function enrichFlexProperties(
|
|
607
|
+
elements: ExtractedElement[],
|
|
608
|
+
parentIsFlexOrGridContainer = false,
|
|
609
|
+
): ExtractedElement[] {
|
|
610
|
+
return elements.map((element) => {
|
|
611
|
+
const matcherData = element.extractorData.get('css-matcher') as MatchedCssData | undefined
|
|
612
|
+
const isFlexOrGridContainer = matcherData ? hasFlexOrGridDisplay(matcherData) : false
|
|
613
|
+
const cssData = element.extractorData.get('css-properties') as CssPropertiesData | undefined
|
|
614
|
+
|
|
615
|
+
if (cssData) {
|
|
616
|
+
let relevant = cssData.relevant
|
|
617
|
+
if (isFlexOrGridContainer) {
|
|
618
|
+
relevant = addFlexContainerProperties(relevant)
|
|
619
|
+
}
|
|
620
|
+
if (parentIsFlexOrGridContainer) {
|
|
621
|
+
relevant = addAlignSelfProperty(relevant)
|
|
622
|
+
}
|
|
623
|
+
element.extractorData.set('css-properties', { relevant })
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return {
|
|
627
|
+
...element,
|
|
628
|
+
children: enrichFlexProperties(element.children, isFlexOrGridContainer),
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
}
|
|
632
|
+
|
|
476
633
|
/**
|
|
477
634
|
* Walks the element tree and adds box properties to selected text tags
|
|
478
635
|
* when CSS overrides `display` or default UA display creates a box.
|
|
@@ -28,8 +28,10 @@ export {
|
|
|
28
28
|
createCssPropertiesExtractor,
|
|
29
29
|
enrichGapProperties,
|
|
30
30
|
enrichContainerProperties,
|
|
31
|
+
enrichFlexProperties,
|
|
31
32
|
resolveDisplayValue,
|
|
32
33
|
getDefaultDisplayForTag,
|
|
34
|
+
ROOT_DISPLAY_CUSTOM_PROPERTY,
|
|
33
35
|
} from './css-properties'
|
|
34
36
|
export type { CssPropertiesData } from './css-properties'
|
|
35
37
|
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
|
|
|
2
2
|
import { createRefElementMarker } from '../../../extensions/ref-elements/path-utils'
|
|
3
3
|
import type { RefElementMatch } from '../../../extensions/ref-elements/types'
|
|
4
4
|
import { ExtractorStore } from './core/store'
|
|
5
|
-
import { createPropTrackerExtractor } from './prop-tracker'
|
|
5
|
+
import { type PropTrackerData, createPropTrackerExtractor } from './prop-tracker'
|
|
6
6
|
|
|
7
7
|
function markRefElementComponent(runtimeComponent: (...args: unknown[]) => unknown, refComponentType: string): void {
|
|
8
8
|
Object.defineProperty(
|
|
@@ -248,4 +248,22 @@ describe('createPropTrackerExtractor', () => {
|
|
|
248
248
|
selector: '.card-cta',
|
|
249
249
|
})
|
|
250
250
|
})
|
|
251
|
+
|
|
252
|
+
it('persists the empty-slot signal onto the element data', () => {
|
|
253
|
+
const store = new ExtractorStore()
|
|
254
|
+
const { extractor } = createPropTrackerExtractor()
|
|
255
|
+
|
|
256
|
+
extractor.onCreateElement?.({
|
|
257
|
+
type: 'button',
|
|
258
|
+
isDomElement: true,
|
|
259
|
+
tag: 'button',
|
|
260
|
+
traceId: 'trace-1',
|
|
261
|
+
props: { className: 'trigger' },
|
|
262
|
+
children: [],
|
|
263
|
+
hasEmptySlot: true,
|
|
264
|
+
store,
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
expect(store.get<PropTrackerData>('trace-1', 'prop-tracker')?.hasEmptySlot).toBe(true)
|
|
268
|
+
})
|
|
251
269
|
})
|
|
@@ -26,6 +26,8 @@ export interface PropTrackerData {
|
|
|
26
26
|
concatenatedAttrs: Map<string, string> // attrName → propName (for concatenated values)
|
|
27
27
|
/** on*-handler prop names whose value is a function on this element (e.g. ['onClick', 'onChange']). */
|
|
28
28
|
eventHandlers: string[]
|
|
29
|
+
/** True when a child slot rendered nothing, so the content is unknown. */
|
|
30
|
+
hasEmptySlot: boolean
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export interface PropTrackerExtractorState {
|
|
@@ -170,6 +172,7 @@ export function createPropTrackerExtractor(options?: CreatePropTrackerExtractorO
|
|
|
170
172
|
boundProps: [...boundProps],
|
|
171
173
|
concatenatedAttrs,
|
|
172
174
|
eventHandlers,
|
|
175
|
+
hasEmptySlot: event.hasEmptySlot ?? false,
|
|
173
176
|
} satisfies PropTrackerData)
|
|
174
177
|
|
|
175
178
|
attachMatchedRefElement(traceId, props, store)
|
|
@@ -18,8 +18,10 @@ export {
|
|
|
18
18
|
createCssPropertiesExtractor,
|
|
19
19
|
enrichGapProperties,
|
|
20
20
|
enrichContainerProperties,
|
|
21
|
+
enrichFlexProperties,
|
|
21
22
|
resolveDisplayValue,
|
|
22
23
|
getDefaultDisplayForTag,
|
|
24
|
+
ROOT_DISPLAY_CUSTOM_PROPERTY,
|
|
23
25
|
inferSupportedNativeStates,
|
|
24
26
|
} from './extractors'
|
|
25
27
|
|
package/src/manifest-pipeline.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
createCssPropertiesExtractor,
|
|
11
11
|
createPropTrackerExtractor,
|
|
12
12
|
enrichContainerProperties,
|
|
13
|
+
enrichFlexProperties,
|
|
13
14
|
enrichGapProperties,
|
|
14
15
|
runExtractors,
|
|
15
16
|
} from './information-extractors/react'
|
|
@@ -151,9 +152,10 @@ export function processComponent(
|
|
|
151
152
|
const matchResult = matchCssSelectors(html, extractedElements, css)
|
|
152
153
|
const containerEnrichedElements = enrichContainerProperties(matchResult.elements)
|
|
153
154
|
const gapEnrichedElements = enrichGapProperties(containerEnrichedElements)
|
|
155
|
+
const flexEnrichedElements = enrichFlexProperties(gapEnrichedElements)
|
|
154
156
|
enhancedInfo = {
|
|
155
157
|
...enhancedInfo,
|
|
156
|
-
elements: convertElements(
|
|
158
|
+
elements: convertElements(flexEnrichedElements),
|
|
157
159
|
}
|
|
158
160
|
varUsedByTraceId = matchResult.varUsedByTraceId
|
|
159
161
|
} catch (thrownError) {
|