@wix/zero-config-implementation 1.99.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.js +9295 -9216
- package/package.json +2 -2
- package/src/converters/css-longhand-resolver.test.ts +35 -0
- package/src/converters/css-longhand-resolver.ts +22 -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/css-properties.test.ts +136 -0
- package/src/information-extractors/react/extractors/css-properties.ts +146 -4
- package/src/information-extractors/react/extractors/index.ts +2 -0
- package/src/information-extractors/react/index.ts +2 -0
- package/src/manifest-pipeline.ts +3 -1
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"registry": "https://registry.npmjs.org/",
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.
|
|
7
|
+
"version": "1.100.0",
|
|
8
8
|
"description": "Core library for extracting component manifests from JS and CSS files",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"main": "dist/index.js",
|
|
@@ -106,5 +106,5 @@
|
|
|
106
106
|
]
|
|
107
107
|
}
|
|
108
108
|
},
|
|
109
|
-
"falconPackageHash": "
|
|
109
|
+
"falconPackageHash": "80ef17ffc1549b6c4d3f51396955cb0517fe8eceb7b20eed65fc7e7d"
|
|
110
110
|
}
|
|
@@ -131,6 +131,41 @@ describe('resolveLonghand', () => {
|
|
|
131
131
|
expect(resolveLonghand('paddingTop', new Map())).toBeUndefined()
|
|
132
132
|
})
|
|
133
133
|
|
|
134
|
+
describe('margin family', () => {
|
|
135
|
+
it('marginTop falls back through marginBlockStart → marginBlock → margin', () => {
|
|
136
|
+
expect(resolveLonghand('marginTop', new Map([['marginBlockStart', '1px']]))).toBe('1px')
|
|
137
|
+
expect(resolveLonghand('marginTop', new Map([['marginBlock', '4px 8px']]))).toBe('4px')
|
|
138
|
+
expect(resolveLonghand('marginTop', new Map([['margin', '1rem 2rem']]))).toBe('1rem')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('marginBottom falls back through marginBlockEnd → marginBlock → margin', () => {
|
|
142
|
+
expect(resolveLonghand('marginBottom', new Map([['marginBlockEnd', '2px']]))).toBe('2px')
|
|
143
|
+
expect(resolveLonghand('marginBottom', new Map([['marginBlock', '4px 8px']]))).toBe('8px')
|
|
144
|
+
expect(resolveLonghand('marginBottom', new Map([['margin', '1rem 2rem']]))).toBe('1rem')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('marginInlineStart falls back through marginLeft → marginInline → margin', () => {
|
|
148
|
+
expect(resolveLonghand('marginInlineStart', new Map([['marginLeft', '7px']]))).toBe('7px')
|
|
149
|
+
expect(resolveLonghand('marginInlineStart', new Map([['marginInline', '12px']]))).toBe('12px')
|
|
150
|
+
expect(resolveLonghand('marginInlineStart', new Map([['marginInline', '12px 24px']]))).toBe('12px')
|
|
151
|
+
expect(resolveLonghand('marginInlineStart', new Map([['margin', '1px 2px 3px 4px']]))).toBe('4px')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('marginInlineEnd falls back through marginRight → marginInline → margin', () => {
|
|
155
|
+
expect(resolveLonghand('marginInlineEnd', new Map([['marginRight', '9px']]))).toBe('9px')
|
|
156
|
+
expect(resolveLonghand('marginInlineEnd', new Map([['marginInline', '12px 24px']]))).toBe('24px')
|
|
157
|
+
expect(resolveLonghand('marginInlineEnd', new Map([['margin', '1px 2px 3px 4px']]))).toBe('2px')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('prefers the literal longhand over any fallback', () => {
|
|
161
|
+
const values = new Map([
|
|
162
|
+
['marginTop', '5px'],
|
|
163
|
+
['margin', '10px'],
|
|
164
|
+
])
|
|
165
|
+
expect(resolveLonghand('marginTop', values)).toBe('5px')
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
134
169
|
describe('padding family', () => {
|
|
135
170
|
it('paddingTop falls back through paddingBlockStart → paddingBlock → padding', () => {
|
|
136
171
|
expect(resolveLonghand('paddingTop', new Map([['paddingBlockStart', '1px']]))).toBe('1px')
|
|
@@ -215,6 +215,18 @@ const fromTextDecorationLine: Fallback = (values) => {
|
|
|
215
215
|
return lineTokens.join(' ')
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* `flex-flow: <flex-direction> || <flex-wrap>` — order is free and either
|
|
220
|
+
* part may be omitted. Pick out just the direction keyword for the schema.
|
|
221
|
+
*/
|
|
222
|
+
const FLEX_DIRECTION_KEYWORDS = new Set(['row', 'row-reverse', 'column', 'column-reverse'])
|
|
223
|
+
|
|
224
|
+
const fromFlexFlowDirection: Fallback = (values) => {
|
|
225
|
+
const shorthand = values.get('flexFlow')
|
|
226
|
+
if (shorthand === undefined) return undefined
|
|
227
|
+
return splitTopLevelTokens(shorthand).find((token) => FLEX_DIRECTION_KEYWORDS.has(token.toLowerCase()))
|
|
228
|
+
}
|
|
229
|
+
|
|
218
230
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
219
231
|
// Resolution table
|
|
220
232
|
//
|
|
@@ -233,6 +245,12 @@ const FALLBACKS: Partial<Record<CssPropertyName, Fallback[]>> = {
|
|
|
233
245
|
paddingInlineStart: [fromKey('paddingLeft'), fromAxis('paddingInline', 'start'), fromBox('padding', 'left')],
|
|
234
246
|
paddingInlineEnd: [fromKey('paddingRight'), fromAxis('paddingInline', 'end'), fromBox('padding', 'right')],
|
|
235
247
|
|
|
248
|
+
// Margin — same block/inline convention as padding above.
|
|
249
|
+
marginTop: [fromKey('marginBlockStart'), fromAxis('marginBlock', 'start'), fromBox('margin', 'top')],
|
|
250
|
+
marginBottom: [fromKey('marginBlockEnd'), fromAxis('marginBlock', 'end'), fromBox('margin', 'bottom')],
|
|
251
|
+
marginInlineStart: [fromKey('marginLeft'), fromAxis('marginInline', 'start'), fromBox('margin', 'left')],
|
|
252
|
+
marginInlineEnd: [fromKey('marginRight'), fromAxis('marginInline', 'end'), fromBox('margin', 'right')],
|
|
253
|
+
|
|
236
254
|
// Border radius — schema uses all-logical corner names; physical names map under LTR/horizontal-tb.
|
|
237
255
|
borderStartStartRadius: [fromKey('borderTopLeftRadius'), fromCorner('borderRadius', 'tl')],
|
|
238
256
|
borderStartEndRadius: [fromKey('borderTopRightRadius'), fromCorner('borderRadius', 'tr')],
|
|
@@ -255,6 +273,10 @@ const FALLBACKS: Partial<Record<CssPropertyName, Fallback[]>> = {
|
|
|
255
273
|
// and color in one declaration. Pick out just the line tokens for the schema.
|
|
256
274
|
textDecorationLine: [fromTextDecorationLine],
|
|
257
275
|
|
|
276
|
+
// Flex — `flex-flow: column wrap` carries both direction and wrap; pick out
|
|
277
|
+
// just the direction keyword for the schema.
|
|
278
|
+
flexDirection: [fromFlexFlowDirection],
|
|
279
|
+
|
|
258
280
|
// Shorthand targets (e.g. `font`) live in css-shorthand-composer.ts and are
|
|
259
281
|
// merged in here — composer-driven, conceptually the inverse of the rows above.
|
|
260
282
|
...SHORTHAND_FALLBACKS,
|
|
@@ -346,6 +346,72 @@ describe('toEditorReactComponent — cssProperties defaultValue', () => {
|
|
|
346
346
|
defaultValue: 'var(--wst-primary-background-color)',
|
|
347
347
|
})
|
|
348
348
|
})
|
|
349
|
+
|
|
350
|
+
it('excludes width/height from the root element (sizing is handled via layout config)', () => {
|
|
351
|
+
const extractorData = new Map<string, unknown>()
|
|
352
|
+
extractorData.set('css-matcher', matcherDataFromCss('width: 100px; height: 50px; overflow: hidden;'))
|
|
353
|
+
extractorData.set('css-properties', {
|
|
354
|
+
relevant: [
|
|
355
|
+
CSS_PROPERTIES.CSS_PROPERTY_TYPE.width,
|
|
356
|
+
CSS_PROPERTIES.CSS_PROPERTY_TYPE.height,
|
|
357
|
+
CSS_PROPERTIES.CSS_PROPERTY_TYPE.overflow,
|
|
358
|
+
],
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
const rootElement: ExtractedElement = {
|
|
362
|
+
traceId: 'trace-root',
|
|
363
|
+
name: 'root',
|
|
364
|
+
tag: 'div',
|
|
365
|
+
attributes: {},
|
|
366
|
+
extractorData,
|
|
367
|
+
children: [],
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const result = toEditorReactComponent(createComponent(rootElement))
|
|
371
|
+
|
|
372
|
+
expect(result.editorElement?.cssProperties?.width).toBeUndefined()
|
|
373
|
+
expect(result.editorElement?.cssProperties?.height).toBeUndefined()
|
|
374
|
+
expect(result.editorElement?.cssProperties?.overflow).toEqual({ defaultValue: 'hidden' })
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('keeps width/height on inner (non-root) elements', () => {
|
|
378
|
+
const rootExtractorData = new Map<string, unknown>()
|
|
379
|
+
rootExtractorData.set('css-matcher', matcherDataFromCss(''))
|
|
380
|
+
rootExtractorData.set('css-properties', { relevant: [] })
|
|
381
|
+
|
|
382
|
+
const childExtractorData = new Map<string, unknown>()
|
|
383
|
+
childExtractorData.set('css-matcher', matcherDataFromCss('width: 24px; height: 24px;'))
|
|
384
|
+
childExtractorData.set('css-properties', {
|
|
385
|
+
relevant: [CSS_PROPERTIES.CSS_PROPERTY_TYPE.width, CSS_PROPERTIES.CSS_PROPERTY_TYPE.height],
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
const childElement: ExtractedElement = {
|
|
389
|
+
traceId: 'trace-icon',
|
|
390
|
+
name: 'icon',
|
|
391
|
+
tag: 'img',
|
|
392
|
+
attributes: {},
|
|
393
|
+
extractorData: childExtractorData,
|
|
394
|
+
children: [],
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const rootElement: ExtractedElement = {
|
|
398
|
+
traceId: 'trace-root',
|
|
399
|
+
name: 'root',
|
|
400
|
+
tag: 'div',
|
|
401
|
+
attributes: {},
|
|
402
|
+
extractorData: rootExtractorData,
|
|
403
|
+
children: [childElement],
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const result = toEditorReactComponent(createComponent(rootElement))
|
|
407
|
+
|
|
408
|
+
expect(result.editorElement?.elements?.icon?.inlineElement?.cssProperties?.width).toEqual({
|
|
409
|
+
defaultValue: '24px',
|
|
410
|
+
})
|
|
411
|
+
expect(result.editorElement?.elements?.icon?.inlineElement?.cssProperties?.height).toEqual({
|
|
412
|
+
defaultValue: '24px',
|
|
413
|
+
})
|
|
414
|
+
})
|
|
349
415
|
})
|
|
350
416
|
|
|
351
417
|
describe('toEditorReactComponent selector conversion', () => {
|
|
@@ -25,7 +25,11 @@ import type {
|
|
|
25
25
|
ExtractedElement,
|
|
26
26
|
TrackingStores,
|
|
27
27
|
} from '../information-extractors/react'
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
ROOT_DISPLAY_CUSTOM_PROPERTY,
|
|
30
|
+
getDefaultDisplayForTag,
|
|
31
|
+
resolveDisplayValue,
|
|
32
|
+
} from '../information-extractors/react'
|
|
29
33
|
import { findPreferredSemanticClass, normalizeClassNames } from '../utils/css-class'
|
|
30
34
|
import { buildActiveItemDisplayGroups, buildActiveItemIndexDisplayFilters } from './active-item-index-builder'
|
|
31
35
|
import { resolveLonghand } from './css-longhand-resolver'
|
|
@@ -307,7 +311,7 @@ function buildNearestCommonAncestorCustomProps(
|
|
|
307
311
|
const nearestCommonAncestorCustomProps = new Map<string, Record<string, CssCustomPropertyItem>>()
|
|
308
312
|
|
|
309
313
|
for (const varName of varNames) {
|
|
310
|
-
if (varName ===
|
|
314
|
+
if (varName === ROOT_DISPLAY_CUSTOM_PROPERTY) continue
|
|
311
315
|
|
|
312
316
|
const registration = registeredByVar.get(varName)
|
|
313
317
|
// Prefer a concrete declared value; fall back to the @property initial-value so a
|
|
@@ -563,6 +567,10 @@ function toDisplayEnumValue(cssValue: string): DisplayEnumValue {
|
|
|
563
567
|
return (CSS_DISPLAY_TO_ENUM[cssValue] ?? cssValue) as DisplayEnumValue
|
|
564
568
|
}
|
|
565
569
|
|
|
570
|
+
// Root sizing is handled separately via layout config, so `width`/`height`
|
|
571
|
+
// are excluded from the root element's own `cssProperties`.
|
|
572
|
+
const ROOT_EXCLUDED_CSS_PROPERTIES = new Set<string>([CSS_PROPERTY_TYPE.width, CSS_PROPERTY_TYPE.height])
|
|
573
|
+
|
|
566
574
|
function buildCssProperties(
|
|
567
575
|
element: ExtractedElement | undefined,
|
|
568
576
|
isRootElement: boolean,
|
|
@@ -576,6 +584,9 @@ function buildCssProperties(
|
|
|
576
584
|
}
|
|
577
585
|
const cssPropertyValues = element ? getMatchedPropertyValues(element) : new Map<string, string>()
|
|
578
586
|
for (const propName of decidedProperties) {
|
|
587
|
+
if (isRootElement && ROOT_EXCLUDED_CSS_PROPERTIES.has(propName)) {
|
|
588
|
+
continue
|
|
589
|
+
}
|
|
579
590
|
if (propName === CSS_PROPERTY_TYPE.display && element) {
|
|
580
591
|
if (!isRootElement) {
|
|
581
592
|
result[propName] = buildDisplayProperty(element)
|
|
@@ -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,
|
|
@@ -252,6 +253,131 @@ describe('getCssPropertiesForTag', () => {
|
|
|
252
253
|
expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
|
|
253
254
|
expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
|
|
254
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
|
+
})
|
|
255
381
|
})
|
|
256
382
|
|
|
257
383
|
describe('getDefaultDisplayForTag', () => {
|
|
@@ -404,4 +530,14 @@ describe('enrichContainerProperties', () => {
|
|
|
404
530
|
expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
|
|
405
531
|
expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
|
|
406
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
|
+
})
|
|
407
543
|
})
|
|
@@ -99,6 +99,70 @@ const BOX_CSS_PROPERTIES = [
|
|
|
99
99
|
CSS_PROPERTY_TYPE.borderEndStartRadius,
|
|
100
100
|
CSS_PROPERTY_TYPE.borderEndEndRadius,
|
|
101
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,
|
|
102
166
|
]
|
|
103
167
|
|
|
104
168
|
/**
|
|
@@ -128,6 +192,12 @@ const MEDIA_CSS_PROPERTIES = [
|
|
|
128
192
|
CSS_PROPERTY_TYPE.borderEndStartRadius,
|
|
129
193
|
CSS_PROPERTY_TYPE.borderEndEndRadius,
|
|
130
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,
|
|
131
201
|
]
|
|
132
202
|
|
|
133
203
|
function hasTextSemantics(tag: string, attributes: SemanticAttributes = {}): boolean {
|
|
@@ -180,6 +250,7 @@ export function getCssPropertiesForTag(tag: string, attributes: SemanticAttribut
|
|
|
180
250
|
properties.push(...BOX_CSS_PROPERTIES)
|
|
181
251
|
}
|
|
182
252
|
|
|
253
|
+
properties.push(...getMarginPropertiesForTag(tag))
|
|
183
254
|
properties.push(CSS_PROPERTY_TYPE.display)
|
|
184
255
|
return [...new Set(properties)]
|
|
185
256
|
}
|
|
@@ -219,6 +290,27 @@ export function addGapProperty(existing: string[]): string[] {
|
|
|
219
290
|
return [...existing, CSS_PROPERTY_TYPE.gap]
|
|
220
291
|
}
|
|
221
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
|
+
|
|
222
314
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
223
315
|
// Display Value Resolution
|
|
224
316
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -412,7 +504,7 @@ function hasUnresolvableDisplayVar(matcherData: MatchedCssData): boolean {
|
|
|
412
504
|
return resolveCssPropertyValue(displayProperty, matcherData.customProperties) === undefined
|
|
413
505
|
}
|
|
414
506
|
|
|
415
|
-
function
|
|
507
|
+
function hasLiteralDisplayDeclaration(matcherData: MatchedCssData): boolean {
|
|
416
508
|
for (const match of matcherData.matches) {
|
|
417
509
|
for (const property of match.properties) {
|
|
418
510
|
if (property.name === 'display') return true
|
|
@@ -421,14 +513,30 @@ function hasDisplayDeclaration(matcherData: MatchedCssData): boolean {
|
|
|
421
513
|
return false
|
|
422
514
|
}
|
|
423
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
|
+
|
|
424
532
|
export function hasFlexOrGridDisplay(matcherData: MatchedCssData): boolean {
|
|
425
|
-
const displayValue =
|
|
533
|
+
const displayValue = resolveEffectiveDisplayValue(matcherData)
|
|
426
534
|
if (displayValue !== undefined) return displayUsesFlexOrGridLayout(displayValue)
|
|
427
535
|
return hasUnresolvableDisplayVar(matcherData)
|
|
428
536
|
}
|
|
429
537
|
|
|
430
538
|
export function hasContainerLikeDisplay(matcherData: MatchedCssData): boolean {
|
|
431
|
-
const displayValue =
|
|
539
|
+
const displayValue = resolveEffectiveDisplayValue(matcherData)
|
|
432
540
|
if (displayValue !== undefined) return displayCreatesContainerBox(displayValue)
|
|
433
541
|
return hasUnresolvableDisplayVar(matcherData)
|
|
434
542
|
}
|
|
@@ -456,7 +564,7 @@ function hasDefaultBoxProperties(tag: string, role?: string): boolean {
|
|
|
456
564
|
}
|
|
457
565
|
|
|
458
566
|
function hasEffectiveContainerDisplay(tag: string, role?: string, matcherData?: MatchedCssData): boolean {
|
|
459
|
-
if (matcherData &&
|
|
567
|
+
if (matcherData && hasEffectiveDisplayDeclaration(matcherData)) {
|
|
460
568
|
return hasContainerLikeDisplay(matcherData)
|
|
461
569
|
}
|
|
462
570
|
|
|
@@ -488,6 +596,40 @@ export function enrichGapProperties(elements: ExtractedElement[]): ExtractedElem
|
|
|
488
596
|
})
|
|
489
597
|
}
|
|
490
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
|
+
|
|
491
633
|
/**
|
|
492
634
|
* Walks the element tree and adds box properties to selected text tags
|
|
493
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
|
|
|
@@ -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) {
|