@wix/zero-config-implementation 1.105.0 → 1.107.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 +29 -17
- package/dist/index.js +8019 -7895
- package/package.json +2 -2
- package/src/converters/states-default-values.test.ts +308 -0
- package/src/converters/to-editor-component.test.ts +0 -85
- package/src/converters/to-editor-component.ts +225 -105
- package/src/information-extractors/css/parse.test.ts +49 -0
- package/src/information-extractors/css/parse.ts +113 -24
- package/src/information-extractors/css/selector-matcher.ts +83 -28
- package/src/information-extractors/css/types.ts +29 -17
- package/src/manifest-pipeline.ts +8 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CssCustomPropertyItem,
|
|
3
|
-
CssNumber,
|
|
4
3
|
CssPropertyItem,
|
|
5
4
|
CustomPropertyEnum,
|
|
6
5
|
DataItem,
|
|
@@ -17,7 +16,7 @@ import { buildRefElementManifestKey } from '../extensions/ref-elements/path-util
|
|
|
17
16
|
import type { RefElementMatch } from '../extensions/ref-elements/types'
|
|
18
17
|
import type { ComponentInfoWithCss } from '../index'
|
|
19
18
|
import { cssPropertyTypeFromUsages, inferCssDataType, stripVarFallback } from '../information-extractors/css/parse'
|
|
20
|
-
import type { MatchedCssData, RegisteredCustomProperty } from '../information-extractors/css/types'
|
|
19
|
+
import type { CssSelectorMatch, MatchedCssData, RegisteredCustomProperty } from '../information-extractors/css/types'
|
|
21
20
|
import type {
|
|
22
21
|
CoupledComponentInfo,
|
|
23
22
|
CoupledProp,
|
|
@@ -78,15 +77,59 @@ function collectStateClassTriggers(component: ComponentInfoWithCss): StateClassT
|
|
|
78
77
|
const EXCLUDED_PROPS = new Set(['id', 'className', 'elementProps', 'wix'])
|
|
79
78
|
|
|
80
79
|
export function toEditorReactComponent(component: ComponentInfoWithCss): EditorReactComponent {
|
|
81
|
-
const
|
|
80
|
+
const statesByTraceId = buildStatesByTraceId(component)
|
|
81
|
+
const nearestCommonAncestorCustomProps = buildNearestCommonAncestorCustomProps(component, statesByTraceId)
|
|
82
82
|
return {
|
|
83
|
-
editorElement: buildEditorElement(component, nearestCommonAncestorCustomProps),
|
|
83
|
+
editorElement: buildEditorElement(component, nearestCommonAncestorCustomProps, statesByTraceId),
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** The `states` block of every emitted element, keyed by traceId. */
|
|
88
|
+
type StatesByTraceId = Map<string, States>
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Builds every element's `states` block up front, so CSS defaults can be split per state
|
|
92
|
+
* before the element entries are assembled. Native states need a semantic block name to
|
|
93
|
+
* synthesize `<block>--<state>`; custom class triggers don't (they carry the className
|
|
94
|
+
* verbatim and match the element directly), so they're built even when the element
|
|
95
|
+
* resolves no semantic class. Prop triggers (`ElementState<>`) are component-level and
|
|
96
|
+
* attach to the root only. Ref elements (and their subtrees) emit no states.
|
|
97
|
+
*/
|
|
98
|
+
function buildStatesByTraceId(component: ComponentInfoWithCss): StatesByTraceId {
|
|
99
|
+
const statesByTraceId: StatesByTraceId = new Map()
|
|
100
|
+
const rootElement = component.elements[0]
|
|
101
|
+
if (!rootElement) return statesByTraceId
|
|
102
|
+
|
|
103
|
+
const stateClassTriggers = collectStateClassTriggers(component)
|
|
104
|
+
const rootPropTriggers = Object.values(component.props).filter((prop) => prop.isStateTrigger)
|
|
105
|
+
|
|
106
|
+
const visit = (element: ExtractedElement, isRootElement: boolean): void => {
|
|
107
|
+
if (!isRootElement && element.extractorData.get('ref-element')) return
|
|
108
|
+
const semanticClass = getSemanticBlockName(element)
|
|
109
|
+
const states = mergeStates(
|
|
110
|
+
semanticClass ? buildStatesBlock(element, semanticClass, stateClassTriggers.focus) : undefined,
|
|
111
|
+
buildCustomStatesBlock({
|
|
112
|
+
element,
|
|
113
|
+
propTriggers: isRootElement ? rootPropTriggers : [],
|
|
114
|
+
classTriggers: stateClassTriggers.custom,
|
|
115
|
+
}),
|
|
116
|
+
)
|
|
117
|
+
if (states) statesByTraceId.set(element.traceId, states)
|
|
118
|
+
for (const child of element.children) visit(child, false)
|
|
119
|
+
}
|
|
120
|
+
visit(rootElement, true)
|
|
121
|
+
|
|
122
|
+
return statesByTraceId
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function stateNamesOf(statesByTraceId: StatesByTraceId, traceId: string): string[] {
|
|
126
|
+
return Object.keys(statesByTraceId.get(traceId) ?? {})
|
|
127
|
+
}
|
|
128
|
+
|
|
87
129
|
function buildEditorElement(
|
|
88
130
|
component: ComponentInfoWithCss,
|
|
89
131
|
nearestCommonAncestorCustomProps: Map<string, Record<string, CssCustomPropertyItem>>,
|
|
132
|
+
statesByTraceId: StatesByTraceId,
|
|
90
133
|
): EditorElement {
|
|
91
134
|
const rootElement = component.elements[0]
|
|
92
135
|
if (!rootElement) {
|
|
@@ -98,23 +141,9 @@ function buildEditorElement(
|
|
|
98
141
|
const childElements = rootElement.children
|
|
99
142
|
const rootCustomProps = nearestCommonAncestorCustomProps.get(rootElement.traceId) ?? {}
|
|
100
143
|
|
|
101
|
-
const stateClassTriggers = collectStateClassTriggers(component)
|
|
102
|
-
|
|
103
|
-
// Native states need a semantic block name to synthesize `<block>--<state>`; custom
|
|
104
|
-
// class triggers don't (they carry the className verbatim and match the element
|
|
105
|
-
// directly), so they're built even when the element resolves no semantic class.
|
|
106
144
|
const rootSemanticClass = getSemanticBlockName(rootElement)
|
|
107
145
|
const rootSemanticClasses = rootSemanticClass ? [rootSemanticClass] : []
|
|
108
|
-
const rootStates = rootElement
|
|
109
|
-
? mergeStates(
|
|
110
|
-
rootSemanticClass ? buildStatesBlock(rootElement, rootSemanticClass, stateClassTriggers.focus) : undefined,
|
|
111
|
-
buildCustomStatesBlock({
|
|
112
|
-
element: rootElement,
|
|
113
|
-
propTriggers: Object.values(component.props).filter((prop) => prop.isStateTrigger),
|
|
114
|
-
classTriggers: stateClassTriggers.custom,
|
|
115
|
-
}),
|
|
116
|
-
)
|
|
117
|
-
: undefined
|
|
146
|
+
const rootStates = statesByTraceId.get(rootElement.traceId)
|
|
118
147
|
|
|
119
148
|
const displayGroups = buildActiveItemDisplayGroups(component.props)
|
|
120
149
|
const displayFilters = buildActiveItemIndexDisplayFilters(component.props)
|
|
@@ -126,12 +155,12 @@ function buildEditorElement(
|
|
|
126
155
|
elements: buildElements(
|
|
127
156
|
childElements,
|
|
128
157
|
nearestCommonAncestorCustomProps,
|
|
129
|
-
|
|
158
|
+
statesByTraceId,
|
|
130
159
|
rootSemanticClasses,
|
|
131
160
|
component.innerElementProps,
|
|
132
161
|
component.propUsages,
|
|
133
162
|
),
|
|
134
|
-
cssProperties: buildCssProperties(rootElement, true),
|
|
163
|
+
cssProperties: buildCssProperties(rootElement, true, stateNamesOf(statesByTraceId, rootElement.traceId)),
|
|
135
164
|
cssCustomProperties: rootCustomProps,
|
|
136
165
|
...(rootStates && { states: rootStates }),
|
|
137
166
|
...(displayGroups && { displayGroups }),
|
|
@@ -202,7 +231,7 @@ function buildData(
|
|
|
202
231
|
function buildElements(
|
|
203
232
|
elements: ExtractedElement[],
|
|
204
233
|
nearestCommonAncestorCustomProps: Map<string, Record<string, CssCustomPropertyItem>>,
|
|
205
|
-
|
|
234
|
+
statesByTraceId: StatesByTraceId,
|
|
206
235
|
ancestorSemanticClasses: string[],
|
|
207
236
|
innerElementProps?: CoupledComponentInfo['innerElementProps'],
|
|
208
237
|
propUsages?: TrackingStores['propUsages'],
|
|
@@ -228,15 +257,9 @@ function buildElements(
|
|
|
228
257
|
const semanticClass = getSemanticBlockName(element)
|
|
229
258
|
|
|
230
259
|
const data = innerEntry && propUsages ? buildData(innerEntry.props, propUsages) : undefined
|
|
231
|
-
const cssProps = buildCssProperties(element, false)
|
|
260
|
+
const cssProps = buildCssProperties(element, false, stateNamesOf(statesByTraceId, element.traceId))
|
|
232
261
|
const cssCustomProps = nearestCommonAncestorCustomProps.get(element.traceId) ?? {}
|
|
233
|
-
|
|
234
|
-
// Inner-element custom states come from custom-class triggers only; prop
|
|
235
|
-
// triggers (`ElementState<>`) are component-level and attach to the root.
|
|
236
|
-
const states = mergeStates(
|
|
237
|
-
semanticClass ? buildStatesBlock(element, semanticClass, stateClassTriggers.focus) : undefined,
|
|
238
|
-
buildCustomStatesBlock({ element, propTriggers: [], classTriggers: stateClassTriggers.custom }),
|
|
239
|
-
)
|
|
262
|
+
const states = statesByTraceId.get(element.traceId)
|
|
240
263
|
|
|
241
264
|
result[element.name] = {
|
|
242
265
|
elementType: ELEMENTS.ELEMENT_TYPE.inlineElement,
|
|
@@ -257,7 +280,7 @@ function buildElements(
|
|
|
257
280
|
? buildElements(
|
|
258
281
|
element.children,
|
|
259
282
|
nearestCommonAncestorCustomProps,
|
|
260
|
-
|
|
283
|
+
statesByTraceId,
|
|
261
284
|
semanticClass ? [...ancestorSemanticClasses, semanticClass] : ancestorSemanticClasses,
|
|
262
285
|
innerElementProps,
|
|
263
286
|
propUsages,
|
|
@@ -283,8 +306,10 @@ function buildElements(
|
|
|
283
306
|
*/
|
|
284
307
|
function buildNearestCommonAncestorCustomProps(
|
|
285
308
|
component: ComponentInfoWithCss,
|
|
309
|
+
statesByTraceId: StatesByTraceId,
|
|
286
310
|
): Map<string, Record<string, CssCustomPropertyItem>> {
|
|
287
311
|
const parentMap = buildParentMap(component.elements)
|
|
312
|
+
const elementsByTraceId = buildElementsByTraceId(component.elements)
|
|
288
313
|
const rootTraceId = component.elements[0]?.traceId
|
|
289
314
|
|
|
290
315
|
// Custom properties registered via the standard @property at-rule — the declared
|
|
@@ -307,7 +332,13 @@ function buildNearestCommonAncestorCustomProps(
|
|
|
307
332
|
|
|
308
333
|
// Emit every declared variable plus every @property registration — a registered var
|
|
309
334
|
// may have no element-level declaration yet still needs a typed control.
|
|
310
|
-
|
|
335
|
+
// Variables declared only under a state (`.root:hover { --x: … }`) have no regular default
|
|
336
|
+
// but still get a control on their declaring element, carrying the state default.
|
|
337
|
+
const varNames = new Set<string>([
|
|
338
|
+
...allCustomPropertyValues.keys(),
|
|
339
|
+
...registeredByVar.keys(),
|
|
340
|
+
...collectStateDeclaredVarNames(component.elements, statesByTraceId),
|
|
341
|
+
])
|
|
311
342
|
|
|
312
343
|
const nearestCommonAncestorCustomProps = new Map<string, Record<string, CssCustomPropertyItem>>()
|
|
313
344
|
|
|
@@ -320,7 +351,7 @@ function buildNearestCommonAncestorCustomProps(
|
|
|
320
351
|
const defaultValue = allCustomPropertyValues.get(varName) ?? registration?.defaultValue
|
|
321
352
|
|
|
322
353
|
// Declaration wins over usage; multiple declaring elements resolve to their NCA.
|
|
323
|
-
const definingTraceIds = findDefiningElements(component.elements, varName)
|
|
354
|
+
const definingTraceIds = findDefiningElements(component.elements, varName, statesByTraceId)
|
|
324
355
|
|
|
325
356
|
let nearestCommonAncestorTraceId: string | undefined
|
|
326
357
|
if (definingTraceIds.size > 0) {
|
|
@@ -342,21 +373,28 @@ function buildNearestCommonAncestorCustomProps(
|
|
|
342
373
|
|
|
343
374
|
if (!nearestCommonAncestorTraceId) continue
|
|
344
375
|
|
|
345
|
-
|
|
376
|
+
const statesDefaultValues = buildCustomPropertyStatesDefaultValues(
|
|
377
|
+
elementsByTraceId.get(nearestCommonAncestorTraceId),
|
|
378
|
+
stateNamesOf(statesByTraceId, nearestCommonAncestorTraceId),
|
|
379
|
+
varName,
|
|
380
|
+
defaultValue,
|
|
381
|
+
)
|
|
382
|
+
// A @property registration is the top-priority type source; otherwise infer from usage,
|
|
383
|
+
// then from the value — a state value when the variable has no regular declaration.
|
|
384
|
+
const valueForTypeInference = defaultValue ?? Object.values(statesDefaultValues ?? {})[0] ?? ''
|
|
346
385
|
const cssPropertyType = registration
|
|
347
386
|
? registration.cssPropertyType
|
|
348
|
-
: getVarPropertyTypeFromCssInfos(varName,
|
|
387
|
+
: getVarPropertyTypeFromCssInfos(varName, valueForTypeInference, component.css)
|
|
349
388
|
const customEnum = buildCustomEnum(registration)
|
|
350
|
-
const numberLimits = buildCssNumber(registration, cssPropertyType)
|
|
351
389
|
const cleanVarName = varName.startsWith('--') ? varName.slice(2) : varName
|
|
352
390
|
|
|
353
391
|
const existingProps = nearestCommonAncestorCustomProps.get(nearestCommonAncestorTraceId) ?? {}
|
|
354
392
|
existingProps[cleanVarName] = {
|
|
355
393
|
displayName: formatDisplayName(cleanVarName),
|
|
356
394
|
...(defaultValue !== undefined && { defaultValue }),
|
|
395
|
+
...(statesDefaultValues && { statesDefaultValues }),
|
|
357
396
|
...(cssPropertyType !== undefined && { cssPropertyType }),
|
|
358
397
|
...(customEnum && { customEnum }),
|
|
359
|
-
...(numberLimits && { number: numberLimits }),
|
|
360
398
|
}
|
|
361
399
|
nearestCommonAncestorCustomProps.set(nearestCommonAncestorTraceId, existingProps)
|
|
362
400
|
}
|
|
@@ -364,6 +402,65 @@ function buildNearestCommonAncestorCustomProps(
|
|
|
364
402
|
return nearestCommonAncestorCustomProps
|
|
365
403
|
}
|
|
366
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Reads the per-state values a variable is declared with on its placement element. Only the
|
|
407
|
+
* placement element's own state declarations count: a value declared under `:hover` on a
|
|
408
|
+
* descendant belongs to that descendant's state, not to the ancestor the variable was placed on.
|
|
409
|
+
* Values equal to the regular default are omitted.
|
|
410
|
+
*/
|
|
411
|
+
function buildCustomPropertyStatesDefaultValues(
|
|
412
|
+
placementElement: ExtractedElement | undefined,
|
|
413
|
+
stateNames: string[],
|
|
414
|
+
varName: string,
|
|
415
|
+
defaultValue: string | undefined,
|
|
416
|
+
): Record<string, string> | undefined {
|
|
417
|
+
const matcherData = placementElement?.extractorData.get('css-matcher') as MatchedCssData | undefined
|
|
418
|
+
const statesDefaultValues: Record<string, string> = {}
|
|
419
|
+
for (const stateName of stateNames) {
|
|
420
|
+
const stateValue = matcherData?.stateCustomProperties?.[stateName]?.[varName]
|
|
421
|
+
if (stateValue !== undefined && stateValue !== defaultValue) statesDefaultValues[stateName] = stateValue
|
|
422
|
+
}
|
|
423
|
+
return Object.keys(statesDefaultValues).length > 0 ? statesDefaultValues : undefined
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* The custom properties an element declares under each state it exposes. Declarations under a
|
|
428
|
+
* CSS state the element has no `states` entry for are left out — there is nowhere to attach them.
|
|
429
|
+
*/
|
|
430
|
+
function exposedStateCustomProperties(
|
|
431
|
+
element: ExtractedElement,
|
|
432
|
+
statesByTraceId: StatesByTraceId,
|
|
433
|
+
): Record<string, string>[] {
|
|
434
|
+
const matcherData = element.extractorData.get('css-matcher') as MatchedCssData | undefined
|
|
435
|
+
return stateNamesOf(statesByTraceId, element.traceId).flatMap((stateName) => {
|
|
436
|
+
const stateCustomProperties = matcherData?.stateCustomProperties?.[stateName]
|
|
437
|
+
return stateCustomProperties ? [stateCustomProperties] : []
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Collects every variable declared under an exposed state anywhere in the element tree. */
|
|
442
|
+
function collectStateDeclaredVarNames(elements: ExtractedElement[], statesByTraceId: StatesByTraceId): Set<string> {
|
|
443
|
+
const varNames = new Set<string>()
|
|
444
|
+
for (const element of elements) {
|
|
445
|
+
for (const stateCustomProperties of exposedStateCustomProperties(element, statesByTraceId)) {
|
|
446
|
+
for (const varName of Object.keys(stateCustomProperties)) varNames.add(varName)
|
|
447
|
+
}
|
|
448
|
+
for (const varName of collectStateDeclaredVarNames(element.children, statesByTraceId)) varNames.add(varName)
|
|
449
|
+
}
|
|
450
|
+
return varNames
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function buildElementsByTraceId(elements: ExtractedElement[]): Map<string, ExtractedElement> {
|
|
454
|
+
const elementsByTraceId = new Map<string, ExtractedElement>()
|
|
455
|
+
for (const element of elements) {
|
|
456
|
+
elementsByTraceId.set(element.traceId, element)
|
|
457
|
+
for (const [traceId, descendant] of buildElementsByTraceId(element.children)) {
|
|
458
|
+
elementsByTraceId.set(traceId, descendant)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return elementsByTraceId
|
|
462
|
+
}
|
|
463
|
+
|
|
367
464
|
/**
|
|
368
465
|
* Merges @property registrations across all of the component's CSS files, keyed by the
|
|
369
466
|
* `--`-prefixed variable name. The first file to register a variable wins.
|
|
@@ -393,42 +490,6 @@ function buildCustomEnum(registration: RegisteredCustomProperty | undefined): Cu
|
|
|
393
490
|
}
|
|
394
491
|
}
|
|
395
492
|
|
|
396
|
-
function parseFiniteDescriptor(rawValue: string | undefined): number | undefined {
|
|
397
|
-
const trimmed = rawValue?.trim()
|
|
398
|
-
if (!trimmed) return undefined
|
|
399
|
-
const parsed = Number(trimmed)
|
|
400
|
-
return Number.isFinite(parsed) ? parsed : undefined
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Builds the clamp the panel's number control applies, from an @property's range
|
|
405
|
-
* descriptors. Anything unusable is dropped rather than passed through.
|
|
406
|
-
*/
|
|
407
|
-
function buildCssNumber(
|
|
408
|
-
registration: RegisteredCustomProperty | undefined,
|
|
409
|
-
cssPropertyType: CssCustomPropertyItem['cssPropertyType'],
|
|
410
|
-
): CssNumber | undefined {
|
|
411
|
-
if (!registration?.range || cssPropertyType !== CSS_PROPERTIES.CSS_DATA_TYPE.number) return undefined
|
|
412
|
-
|
|
413
|
-
let min = parseFiniteDescriptor(registration.range.min)
|
|
414
|
-
let max = parseFiniteDescriptor(registration.range.max)
|
|
415
|
-
if (min !== undefined && max !== undefined && min >= max) {
|
|
416
|
-
min = undefined
|
|
417
|
-
max = undefined
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const step = parseFiniteDescriptor(registration.range.step)
|
|
421
|
-
const multiplier = step !== undefined && step > 0 ? step : undefined
|
|
422
|
-
|
|
423
|
-
if (min === undefined && max === undefined && multiplier === undefined) return undefined
|
|
424
|
-
|
|
425
|
-
return {
|
|
426
|
-
...(min !== undefined && { min: String(min) }),
|
|
427
|
-
...(max !== undefined && { max: String(max) }),
|
|
428
|
-
...(multiplier !== undefined && { multiplier: String(multiplier) }),
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
493
|
function getVarPropertyTypeFromCssInfos(
|
|
433
494
|
varName: string,
|
|
434
495
|
defaultValue: string,
|
|
@@ -516,14 +577,24 @@ function findNearestCommonAncestor(traceIds: Set<string>, parentMap: Map<string,
|
|
|
516
577
|
}
|
|
517
578
|
|
|
518
579
|
/**
|
|
519
|
-
* Finds every surviving element whose matched CSS declares the given custom property
|
|
580
|
+
* Finds every surviving element whose matched CSS declares the given custom property,
|
|
581
|
+
* in its regular appearance or in any state it exposes.
|
|
520
582
|
*/
|
|
521
|
-
function findDefiningElements(
|
|
583
|
+
function findDefiningElements(
|
|
584
|
+
elements: ExtractedElement[],
|
|
585
|
+
varName: string,
|
|
586
|
+
statesByTraceId: StatesByTraceId,
|
|
587
|
+
): Set<string> {
|
|
522
588
|
const definingTraceIds = new Set<string>()
|
|
523
589
|
for (const element of elements) {
|
|
524
590
|
const matcherData = element.extractorData.get('css-matcher') as MatchedCssData | undefined
|
|
525
|
-
|
|
526
|
-
|
|
591
|
+
const declaresInState = exposedStateCustomProperties(element, statesByTraceId).some(
|
|
592
|
+
(stateCustomProperties) => stateCustomProperties[varName] !== undefined,
|
|
593
|
+
)
|
|
594
|
+
if (matcherData?.customProperties[varName] !== undefined || declaresInState) definingTraceIds.add(element.traceId)
|
|
595
|
+
for (const traceId of findDefiningElements(element.children, varName, statesByTraceId)) {
|
|
596
|
+
definingTraceIds.add(traceId)
|
|
597
|
+
}
|
|
527
598
|
}
|
|
528
599
|
return definingTraceIds
|
|
529
600
|
}
|
|
@@ -533,22 +604,39 @@ function findDefiningElements(elements: ExtractedElement[], varName: string): Se
|
|
|
533
604
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
534
605
|
|
|
535
606
|
/**
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
* When multiple rules match (e.g. .foo and .foo.dark), later match wins for same property.
|
|
607
|
+
* The CSS property values (camelCased names) declared by one layer of matches — the regular
|
|
608
|
+
* matches or one state's matches — split by whether each declaration sits inside an at-rule.
|
|
539
609
|
*/
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
610
|
+
interface LayerValues {
|
|
611
|
+
topLevel: Map<string, string>
|
|
612
|
+
inAtRule: Map<string, string>
|
|
613
|
+
}
|
|
544
614
|
|
|
545
|
-
|
|
546
|
-
|
|
615
|
+
/**
|
|
616
|
+
* Collects one layer's property values. When multiple rules match (e.g. .foo and .foo.dark),
|
|
617
|
+
* later match wins for same property within each half of the layer.
|
|
618
|
+
*/
|
|
619
|
+
function collectPropertyValues(matches: CssSelectorMatch[] | undefined): LayerValues {
|
|
620
|
+
const layerValues: LayerValues = { topLevel: new Map(), inAtRule: new Map() }
|
|
621
|
+
for (const match of matches ?? []) {
|
|
547
622
|
for (const prop of match.properties) {
|
|
548
|
-
|
|
623
|
+
const targetValues = prop.inAtRule ? layerValues.inAtRule : layerValues.topLevel
|
|
624
|
+
targetValues.set(camelCase(prop.name), prop.value)
|
|
549
625
|
}
|
|
550
626
|
}
|
|
551
|
-
return
|
|
627
|
+
return layerValues
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Resolves a manifest default from one layer, ignoring any `var()` fallback. Top-level
|
|
632
|
+
* declarations are resolved first, so a conditional at-rule (`@media`) value — even a longhand
|
|
633
|
+
* under a top-level shorthand — only fills in what the top level leaves undeclared: the
|
|
634
|
+
* manifest default describes the unconditional appearance.
|
|
635
|
+
*/
|
|
636
|
+
function resolveLayerDefault(propName: string, layerValues: LayerValues): string | undefined {
|
|
637
|
+
const resolvedValue =
|
|
638
|
+
resolveLonghand(propName, layerValues.topLevel) ?? resolveLonghand(propName, layerValues.inAtRule)
|
|
639
|
+
return resolvedValue !== undefined ? stripVarFallback(resolvedValue) : undefined
|
|
552
640
|
}
|
|
553
641
|
|
|
554
642
|
const { CSS_PROPERTY_TYPE, DISPLAY_VALUE } = CSS_PROPERTIES
|
|
@@ -594,6 +682,7 @@ const ROOT_EXCLUDED_CSS_PROPERTIES = new Set<string>([
|
|
|
594
682
|
function buildCssProperties(
|
|
595
683
|
element: ExtractedElement | undefined,
|
|
596
684
|
isRootElement: boolean,
|
|
685
|
+
stateNames: string[],
|
|
597
686
|
): Record<string, CssPropertyItem> {
|
|
598
687
|
const result: Record<string, CssPropertyItem> = {}
|
|
599
688
|
|
|
@@ -602,47 +691,78 @@ function buildCssProperties(
|
|
|
602
691
|
if (!decidedProperties || decidedProperties.length === 0) {
|
|
603
692
|
return result
|
|
604
693
|
}
|
|
605
|
-
const
|
|
694
|
+
const matcherData = element?.extractorData.get('css-matcher') as MatchedCssData | undefined
|
|
695
|
+
const cssPropertyValues = collectPropertyValues(matcherData?.matches)
|
|
696
|
+
// Each state layer is resolved on its own — merging it over the regular values would let
|
|
697
|
+
// a regular longhand (`padding-top`) beat a state shorthand (`padding`).
|
|
698
|
+
const stateLayers = stateNames.flatMap((stateName) => {
|
|
699
|
+
const stateMatches = matcherData?.stateMatches?.[stateName]
|
|
700
|
+
return stateMatches ? [{ stateName, values: collectPropertyValues(stateMatches) }] : []
|
|
701
|
+
})
|
|
606
702
|
for (const propName of decidedProperties) {
|
|
607
703
|
if (isRootElement && ROOT_EXCLUDED_CSS_PROPERTIES.has(propName)) {
|
|
608
704
|
continue
|
|
609
705
|
}
|
|
610
706
|
if (propName === CSS_PROPERTY_TYPE.display && element) {
|
|
611
707
|
if (!isRootElement) {
|
|
612
|
-
result[propName] = buildDisplayProperty(element)
|
|
708
|
+
result[propName] = buildDisplayProperty(element, stateNames)
|
|
613
709
|
}
|
|
614
710
|
continue
|
|
615
711
|
}
|
|
616
|
-
const
|
|
617
|
-
const
|
|
712
|
+
const defaultValue = resolveLayerDefault(propName, cssPropertyValues)
|
|
713
|
+
const statesDefaultValues: Record<string, string> = {}
|
|
714
|
+
for (const stateLayer of stateLayers) {
|
|
715
|
+
const stateDefaultValue = resolveLayerDefault(propName, stateLayer.values)
|
|
716
|
+
if (stateDefaultValue !== undefined && stateDefaultValue !== defaultValue) {
|
|
717
|
+
statesDefaultValues[stateLayer.stateName] = stateDefaultValue
|
|
718
|
+
}
|
|
719
|
+
}
|
|
618
720
|
result[propName] = {
|
|
619
721
|
...(defaultValue !== undefined && { defaultValue }),
|
|
722
|
+
...(Object.keys(statesDefaultValues).length > 0 && { statesDefaultValues }),
|
|
620
723
|
}
|
|
621
724
|
}
|
|
622
725
|
|
|
623
726
|
return result
|
|
624
727
|
}
|
|
625
728
|
|
|
626
|
-
function buildDisplayProperty(element: ExtractedElement): CssPropertyItem {
|
|
729
|
+
function buildDisplayProperty(element: ExtractedElement, stateNames: string[]): CssPropertyItem {
|
|
627
730
|
const matcherData = element.extractorData.get('css-matcher') as MatchedCssData | undefined
|
|
628
731
|
const resolvedFromCss = matcherData ? resolveDisplayValue(matcherData) : undefined
|
|
629
732
|
const hasExplicitDisplayDeclaration =
|
|
630
733
|
matcherData?.matches.some((match) => match.properties.some((property) => property.name === 'display')) ?? false
|
|
631
734
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
735
|
+
// An explicit but unresolvable regular display leaves only `none` as a known option.
|
|
736
|
+
const isRegularDisplayUnresolved = hasExplicitDisplayDeclaration && resolvedFromCss === undefined
|
|
737
|
+
const currentEnumValue = isRegularDisplayUnresolved
|
|
738
|
+
? undefined
|
|
739
|
+
: toDisplayEnumValue(resolvedFromCss ?? getDefaultDisplayForTag(element.tag))
|
|
740
|
+
const displayValues: DisplayEnumValue[] = currentEnumValue
|
|
741
|
+
? [DISPLAY_VALUE.none, currentEnumValue]
|
|
742
|
+
: [DISPLAY_VALUE.none]
|
|
743
|
+
|
|
744
|
+
// Each state's effective display becomes its state default, and is offered as an option too.
|
|
745
|
+
// The state is resolved over the regular matches with its custom properties merged in, so a
|
|
746
|
+
// state that only changes the variable a regular `display: var(--layout)` reads still counts;
|
|
747
|
+
// a state's own `display` wins on specificity (and, on a tie, by coming later).
|
|
748
|
+
const statesDefaultValues: Record<string, DisplayEnumValue> = {}
|
|
749
|
+
for (const stateName of stateNames) {
|
|
750
|
+
const stateMatches = matcherData?.stateMatches?.[stateName]
|
|
751
|
+
const stateCustomProperties = matcherData?.stateCustomProperties?.[stateName]
|
|
752
|
+
if (!stateMatches && !stateCustomProperties) continue
|
|
753
|
+
const stateDisplayValue = resolveDisplayValue({
|
|
754
|
+
matches: [...(matcherData?.matches ?? []), ...(stateMatches ?? [])],
|
|
755
|
+
customProperties: { ...matcherData?.customProperties, ...stateCustomProperties },
|
|
756
|
+
})
|
|
757
|
+
if (stateDisplayValue === undefined) continue
|
|
758
|
+
const stateEnumValue = toDisplayEnumValue(stateDisplayValue)
|
|
759
|
+
if (stateEnumValue === currentEnumValue) continue
|
|
760
|
+
statesDefaultValues[stateName] = stateEnumValue
|
|
761
|
+
if (!displayValues.includes(stateEnumValue)) displayValues.push(stateEnumValue)
|
|
638
762
|
}
|
|
639
763
|
|
|
640
|
-
const currentValue = resolvedFromCss ?? getDefaultDisplayForTag(element.tag)
|
|
641
|
-
const currentEnumValue = toDisplayEnumValue(currentValue)
|
|
642
|
-
|
|
643
764
|
return {
|
|
644
|
-
display: {
|
|
645
|
-
|
|
646
|
-
},
|
|
765
|
+
display: { displayValues },
|
|
766
|
+
...(Object.keys(statesDefaultValues).length > 0 && { statesDefaultValues }),
|
|
647
767
|
}
|
|
648
768
|
}
|
|
@@ -121,6 +121,55 @@ describe('parseCss — getStateClasses', () => {
|
|
|
121
121
|
})
|
|
122
122
|
})
|
|
123
123
|
|
|
124
|
+
describe('parseCss — getSelectorStateScope', () => {
|
|
125
|
+
// Selectors are keyed by css-tree's generated form, so look the single rule up by its key.
|
|
126
|
+
function scopeOf(selector: string) {
|
|
127
|
+
const api = parseCss(`${selector} { color: red; }`)
|
|
128
|
+
const [generatedSelector] = api.getAllProperties().keys()
|
|
129
|
+
return api.getSelectorStateScope(generatedSelector)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
it.each([
|
|
133
|
+
['.root', { kind: 'regular' }],
|
|
134
|
+
['.root:hover', { kind: 'state', stateName: 'hover' }],
|
|
135
|
+
['.root:global(.button--hover)', { kind: 'state', stateName: 'hover' }],
|
|
136
|
+
['.root:global(.button--active)', { kind: 'state', stateName: 'active' }],
|
|
137
|
+
['.root:active', { kind: 'state', stateName: 'active' }],
|
|
138
|
+
['.root:focus-visible', { kind: 'state', stateName: 'focus' }],
|
|
139
|
+
['.root:global(.button--featured)', { kind: 'state', stateName: 'featured' }],
|
|
140
|
+
['.root:global(.button--hover):hover', { kind: 'state', stateName: 'hover' }],
|
|
141
|
+
['.root:not([dir])', { kind: 'regular' }],
|
|
142
|
+
['.root:not(:disabled)', { kind: 'regular' }],
|
|
143
|
+
['.root:first-child', { kind: 'regular' }],
|
|
144
|
+
['.root:hover .label', { kind: 'unmappedState' }],
|
|
145
|
+
['.root:global(.button--active):hover', { kind: 'unmappedState' }],
|
|
146
|
+
['.root:hover:focus-visible', { kind: 'unmappedState' }],
|
|
147
|
+
['.root:is(:hover, :focus)', { kind: 'unmappedState' }],
|
|
148
|
+
['.root:is(:global(.button--selected))', { kind: 'unmappedState' }],
|
|
149
|
+
['.root:where(:global(.button--selected))', { kind: 'unmappedState' }],
|
|
150
|
+
])('classifies %s', (selector, expectedScope) => {
|
|
151
|
+
expect(scopeOf(selector)).toEqual(expectedScope)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('treats a global ancestor theme class as regular (no local base to modify)', () => {
|
|
155
|
+
expect(scopeOf(':global(.dark) .root')).toEqual({ kind: 'regular' })
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('classifies each selector of a selector list on its own', () => {
|
|
159
|
+
const api = parseCss('.root:global(.button--hover), .root:hover, .root { color: red; }')
|
|
160
|
+
expect(api.getSelectorStateScope('.root:global(.button--hover)')).toEqual({ kind: 'state', stateName: 'hover' })
|
|
161
|
+
expect(api.getSelectorStateScope('.root:hover')).toEqual({ kind: 'state', stateName: 'hover' })
|
|
162
|
+
expect(api.getSelectorStateScope('.root')).toEqual({ kind: 'regular' })
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('flags declarations inside a conditional at-rule', () => {
|
|
166
|
+
const api = parseCss('.root { color: red; } @media (min-width: 600px) { .root { background: blue; } }')
|
|
167
|
+
const properties = api.getPropertiesForSelector('.root')
|
|
168
|
+
expect(properties.find((property) => property.name === 'color')?.inAtRule).toBeUndefined()
|
|
169
|
+
expect(properties.find((property) => property.name === 'background')?.inAtRule).toBe(true)
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
124
173
|
describe('parseCss — @property registration', () => {
|
|
125
174
|
it('maps each syntax type to its cssPropertyType and reads the initial-value default', () => {
|
|
126
175
|
const registered = parseCss(`
|