@hoyrynpm/bsm-kit 0.1.1 → 0.2.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/README.md CHANGED
@@ -2,9 +2,20 @@
2
2
 
3
3
  Pure JS helpers shared by Block Styles Manager and Höyry block plugins.
4
4
 
5
- Phase 0 ships this package from `packages/bsm-kit` inside the BSM repo via a
6
- `file:` dependency so builds work before the private npm publish. After Tuki
7
- publishes `0.1.0` to the `@hoyrynpm` registry, switch BSM to `"@hoyrynpm/bsm-kit": "^0.1.0"`.
5
+ ## 0.2.0
6
+
7
+ New exports (existing exports unchanged; `KIT_API_VERSION` remains `1`):
8
+
9
+ - `buildSchemaCss( schema, settings, derivedFn, getCssVar )` — declarative schema
10
+ CSS loop (rule A + mobile rule M at `schema.viewports.mobile.max` + derived
11
+ parts). Pass the host `getCssVar` (BSM `src/helpers.js`) as the fourth
12
+ argument, or `{ derivedFn, getCssVar }` as the third.
13
+ - `migrateSettings( settings, schema, from )` — JS pair of
14
+ `Style_Settings_Migrator::plan` ops (`rename`, `drop`, `move`, `default`,
15
+ `map_value`); returns `{ settings, orphans, warnings }`.
8
16
 
9
17
  No `@wordpress/*` dependencies. Runtime settings are read from `window.hoyryBsm`
10
18
  at call time.
19
+
20
+ Publish `0.2.0` from the PR branch to the `@hoyrynpm` registry before bumping
21
+ BSM's dependency and lockfile to the published package (phase 0 §4.9).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoyrynpm/bsm-kit",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Shared pure helpers for Höyry Block Styles Manager and block plugins",
5
5
  "license": "GPL-2.0-or-later",
6
6
  "author": "Digi-ja mainostoimisto Höyry",
@@ -12,6 +12,7 @@
12
12
  "./fluid": "./src/fluid.js",
13
13
  "./viewport": "./src/viewport.js",
14
14
  "./schemaCss": "./src/schemaCss.js",
15
+ "./migrateSettings": "./src/migrateSettings.js",
15
16
  "./stampSchemaVersion": "./src/stampSchemaVersion.js"
16
17
  },
17
18
  "files": [
package/src/index.js CHANGED
@@ -20,5 +20,6 @@ export {
20
20
  emitMobileCssVar,
21
21
  emitTabletCssVar,
22
22
  } from './viewport.js';
23
- export { resolveCssValue, cssVarName } from './schemaCss.js';
23
+ export { resolveCssValue, cssVarName, buildSchemaCss } from './schemaCss.js';
24
+ export { migrateSettings } from './migrateSettings.js';
24
25
  export { stampSchemaVersion } from './stampSchemaVersion.js';
@@ -0,0 +1,428 @@
1
+ /**
2
+ * JS pair of Style_Settings_Migrator::plan ops (rename/drop/move/default/map_value).
3
+ * Pure settings transform — no CSS generation, no post writes.
4
+ */
5
+
6
+ /**
7
+ * Apply declarative migrations from `from` toward schema.schemaVersion.
8
+ *
9
+ * @param {Object} settings styleSettings tree.
10
+ * @param {Object} schema Schema document with migrations + groups.
11
+ * @param {string} from Source schema version stamp.
12
+ * @return {{ settings: Object, orphans: Object, warnings: Array<{code:string,message:string}> }}
13
+ */
14
+ export function migrateSettings( settings, schema, from ) {
15
+ const warnings = [];
16
+ const doc = schema && typeof schema === 'object' ? schema : {};
17
+ const to =
18
+ typeof doc.schemaVersion === 'string' ? doc.schemaVersion : '';
19
+ let migrated = deepCopy(
20
+ settings && typeof settings === 'object' ? settings : {}
21
+ );
22
+
23
+ const source = typeof from === 'string' && from !== '' ? from : 'unknown';
24
+
25
+ if ( source === to && source !== 'unknown' ) {
26
+ return {
27
+ settings: migrated,
28
+ orphans: collectOrphans( migrated, doc ),
29
+ warnings,
30
+ };
31
+ }
32
+
33
+ const chain = migrationChain( doc, source, to );
34
+ if ( chain === null ) {
35
+ warnings.push( {
36
+ code: 'migration_chain_incomplete',
37
+ message: `No complete migration path from ${ source } to ${ to }.`,
38
+ } );
39
+ } else {
40
+ for ( const step of chain ) {
41
+ const ops = Array.isArray( step?.ops ) ? step.ops : [];
42
+ for ( const op of ops ) {
43
+ if ( ! op || typeof op !== 'object' ) {
44
+ continue;
45
+ }
46
+ migrated = applyOp( migrated, op, warnings );
47
+ }
48
+ }
49
+ }
50
+
51
+ return {
52
+ settings: migrated,
53
+ orphans: collectOrphans( migrated, doc ),
54
+ warnings,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * @param {Object} schema
60
+ * @param {string} from
61
+ * @param {string} to
62
+ * @return {Array<Object>|null}
63
+ */
64
+ function migrationChain( schema, from, to ) {
65
+ if ( from === to ) {
66
+ return [];
67
+ }
68
+ const migrations = Array.isArray( schema.migrations )
69
+ ? schema.migrations
70
+ : [];
71
+ if ( migrations.length === 0 ) {
72
+ return null;
73
+ }
74
+
75
+ const byFrom = {};
76
+ for ( const step of migrations ) {
77
+ if ( ! step || typeof step !== 'object' ) {
78
+ continue;
79
+ }
80
+ const stepFrom = typeof step.from === 'string' ? step.from : '';
81
+ if ( stepFrom !== '' ) {
82
+ byFrom[ stepFrom ] = step;
83
+ }
84
+ }
85
+
86
+ const chain = [];
87
+ let current = from;
88
+ let guard = 0;
89
+ while ( current !== to && guard < 64 ) {
90
+ guard += 1;
91
+ if ( ! Object.prototype.hasOwnProperty.call( byFrom, current ) ) {
92
+ return null;
93
+ }
94
+ const step = byFrom[ current ];
95
+ chain.push( step );
96
+ current = typeof step.to === 'string' ? step.to : '';
97
+ if ( current === '' ) {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ return current === to ? chain : null;
103
+ }
104
+
105
+ /**
106
+ * @param {Object} settings
107
+ * @param {Object} op
108
+ * @param {Array} warnings
109
+ * @return {Object}
110
+ */
111
+ function applyOp( settings, op, warnings ) {
112
+ if ( op.rename && typeof op.rename === 'object' ) {
113
+ let next = settings;
114
+ for ( const [ fromPath, toPath ] of Object.entries( op.rename ) ) {
115
+ if ( pathExists( next, String( fromPath ) ) ) {
116
+ const value = pathGet( next, String( fromPath ) );
117
+ next = pathSet( next, String( toPath ), value );
118
+ next = pathUnset( next, String( fromPath ) );
119
+ }
120
+ }
121
+ return next;
122
+ }
123
+
124
+ if ( op.move && typeof op.move === 'object' ) {
125
+ const fromPath =
126
+ typeof op.move.from === 'string' ? op.move.from : '';
127
+ const toPath = typeof op.move.to === 'string' ? op.move.to : '';
128
+ if ( fromPath !== '' && toPath !== '' ) {
129
+ const value = pathGet( settings, fromPath );
130
+ let next = pathSet( settings, toPath, value );
131
+ next = pathUnset( next, fromPath );
132
+ return next;
133
+ }
134
+ return settings;
135
+ }
136
+
137
+ if ( Object.prototype.hasOwnProperty.call( op, 'drop' ) ) {
138
+ const paths = [];
139
+ if ( typeof op.drop === 'string' ) {
140
+ paths.push( op.drop );
141
+ } else if ( Array.isArray( op.drop ) ) {
142
+ for ( const path of op.drop ) {
143
+ if ( typeof path === 'string' ) {
144
+ paths.push( path );
145
+ }
146
+ }
147
+ } else if ( op.drop && typeof op.drop === 'object' ) {
148
+ if ( typeof op.drop.path === 'string' ) {
149
+ paths.push( op.drop.path );
150
+ }
151
+ }
152
+ let next = settings;
153
+ for ( const path of paths ) {
154
+ next = pathUnset( next, path );
155
+ }
156
+ return next;
157
+ }
158
+
159
+ if ( op.default && typeof op.default === 'object' ) {
160
+ const path =
161
+ typeof op.default.path === 'string' ? op.default.path : '';
162
+ const value = op.default.value;
163
+ if ( path !== '' && ! pathExists( settings, path ) ) {
164
+ return pathSet( settings, path, value );
165
+ }
166
+ return settings;
167
+ }
168
+
169
+ if ( op.map_value && typeof op.map_value === 'object' ) {
170
+ const path =
171
+ typeof op.map_value.path === 'string' ? op.map_value.path : '';
172
+ const map =
173
+ op.map_value.map && typeof op.map_value.map === 'object'
174
+ ? op.map_value.map
175
+ : {};
176
+ if ( path !== '' && pathExists( settings, path ) ) {
177
+ const current = pathGet( settings, path );
178
+ const key =
179
+ current === null || current === undefined
180
+ ? null
181
+ : typeof current === 'object'
182
+ ? null
183
+ : String( current );
184
+ if ( key !== null && Object.prototype.hasOwnProperty.call( map, key ) ) {
185
+ return pathSet( settings, path, map[ key ] );
186
+ }
187
+ }
188
+ return settings;
189
+ }
190
+
191
+ warnings.push( {
192
+ code: 'unknown_migration_op',
193
+ message: `Unknown migration op keys: ${ Object.keys( op ).join( ',' ) }`,
194
+ } );
195
+ return settings;
196
+ }
197
+
198
+ /**
199
+ * @param {Object} settings
200
+ * @param {Object} schema
201
+ * @return {Object}
202
+ */
203
+ function collectOrphans( settings, schema ) {
204
+ const known = knownSettingKeys( schema );
205
+ const orphans = {};
206
+ walkSettings( settings, '', ( path, value ) => {
207
+ if ( path === '' ) {
208
+ return;
209
+ }
210
+ if ( isPlainObject( value ) && isGroupPath( path, known ) ) {
211
+ return;
212
+ }
213
+ const base = stripViewportStateSuffix( path, known );
214
+ if ( ! Object.prototype.hasOwnProperty.call( known, base ) ) {
215
+ orphans[ path ] = value;
216
+ }
217
+ } );
218
+ return orphans;
219
+ }
220
+
221
+ /**
222
+ * @param {Object} schema
223
+ * @return {Object<string,true>}
224
+ */
225
+ function knownSettingKeys( schema ) {
226
+ const known = {};
227
+ const groups =
228
+ schema.groups && typeof schema.groups === 'object'
229
+ ? schema.groups
230
+ : {};
231
+ for ( const [ group, fields ] of Object.entries( groups ) ) {
232
+ if ( ! fields || typeof fields !== 'object' ) {
233
+ continue;
234
+ }
235
+ for ( const field of Object.keys( fields ) ) {
236
+ known[ `${ group }.${ field }` ] = true;
237
+ }
238
+ }
239
+ return known;
240
+ }
241
+
242
+ /**
243
+ * @param {string} path
244
+ * @param {Object} known
245
+ * @return {boolean}
246
+ */
247
+ function isGroupPath( path, known ) {
248
+ for ( const key of Object.keys( known ) ) {
249
+ if ( key.startsWith( `${ path }.` ) ) {
250
+ return true;
251
+ }
252
+ }
253
+ return ! path.includes( '.' );
254
+ }
255
+
256
+ /**
257
+ * @param {string} path
258
+ * @param {Object} known
259
+ * @return {string}
260
+ */
261
+ function stripViewportStateSuffix( path, known ) {
262
+ if ( Object.prototype.hasOwnProperty.call( known, path ) ) {
263
+ return path;
264
+ }
265
+ const suffixes = [
266
+ 'HoverMobile',
267
+ 'ActiveMobile',
268
+ 'Hover',
269
+ 'Active',
270
+ 'Mobile',
271
+ ];
272
+ for ( const suffix of suffixes ) {
273
+ if ( path.endsWith( suffix ) ) {
274
+ const base = path.slice( 0, -suffix.length );
275
+ if ( Object.prototype.hasOwnProperty.call( known, base ) ) {
276
+ return base;
277
+ }
278
+ }
279
+ }
280
+ return path;
281
+ }
282
+
283
+ /**
284
+ * @param {Object} settings
285
+ * @param {string} prefix
286
+ * @param {Function} cb
287
+ */
288
+ function walkSettings( settings, prefix, cb ) {
289
+ if ( ! settings || typeof settings !== 'object' ) {
290
+ return;
291
+ }
292
+ for ( const [ key, value ] of Object.entries( settings ) ) {
293
+ const path = prefix === '' ? String( key ) : `${ prefix }.${ key }`;
294
+ cb( path, value );
295
+ if ( isPlainObject( value ) && looksLikeGroup( value ) ) {
296
+ walkSettings( value, path, cb );
297
+ }
298
+ }
299
+ }
300
+
301
+ /**
302
+ * @param {*} value
303
+ * @return {boolean}
304
+ */
305
+ function isPlainObject( value ) {
306
+ return (
307
+ value !== null &&
308
+ typeof value === 'object' &&
309
+ ! Array.isArray( value )
310
+ );
311
+ }
312
+
313
+ /**
314
+ * @param {Object} value
315
+ * @return {boolean}
316
+ */
317
+ function looksLikeGroup( value ) {
318
+ const sideKeys = {
319
+ top: true,
320
+ right: true,
321
+ bottom: true,
322
+ left: true,
323
+ topLeft: true,
324
+ topRight: true,
325
+ bottomLeft: true,
326
+ bottomRight: true,
327
+ color: true,
328
+ gradient: true,
329
+ width: true,
330
+ };
331
+ const keys = Object.keys( value );
332
+ if ( keys.length === 0 ) {
333
+ return false;
334
+ }
335
+ let sideHits = 0;
336
+ for ( const key of keys ) {
337
+ if ( sideKeys[ key ] ) {
338
+ sideHits += 1;
339
+ }
340
+ }
341
+ return sideHits < keys.length;
342
+ }
343
+
344
+ /**
345
+ * @param {Object} arr
346
+ * @param {string} path
347
+ * @return {*}
348
+ */
349
+ function pathGet( arr, path ) {
350
+ if ( path === '' ) {
351
+ return undefined;
352
+ }
353
+ const keys = path.split( '.' );
354
+ let cur = arr;
355
+ for ( const key of keys ) {
356
+ if ( ! isPlainObject( cur ) || ! Object.prototype.hasOwnProperty.call( cur, key ) ) {
357
+ return undefined;
358
+ }
359
+ cur = cur[ key ];
360
+ }
361
+ return cur;
362
+ }
363
+
364
+ /**
365
+ * @param {Object} arr
366
+ * @param {string} path
367
+ * @return {boolean}
368
+ */
369
+ function pathExists( arr, path ) {
370
+ const keys = path.split( '.' );
371
+ let cur = arr;
372
+ for ( const key of keys ) {
373
+ if ( ! isPlainObject( cur ) || ! Object.prototype.hasOwnProperty.call( cur, key ) ) {
374
+ return false;
375
+ }
376
+ cur = cur[ key ];
377
+ }
378
+ return true;
379
+ }
380
+
381
+ /**
382
+ * @param {Object} arr
383
+ * @param {string} path
384
+ * @param {*} value
385
+ * @return {Object}
386
+ */
387
+ function pathSet( arr, path, value ) {
388
+ const keys = path.split( '.' );
389
+ const next = deepCopy( arr );
390
+ let ref = next;
391
+ const last = keys.pop();
392
+ for ( const key of keys ) {
393
+ if ( ! isPlainObject( ref[ key ] ) ) {
394
+ ref[ key ] = {};
395
+ }
396
+ ref = ref[ key ];
397
+ }
398
+ ref[ last ] = value;
399
+ return next;
400
+ }
401
+
402
+ /**
403
+ * @param {Object} arr
404
+ * @param {string} path
405
+ * @return {Object}
406
+ */
407
+ function pathUnset( arr, path ) {
408
+ const keys = path.split( '.' );
409
+ const next = deepCopy( arr );
410
+ let ref = next;
411
+ const last = keys.pop();
412
+ for ( const key of keys ) {
413
+ if ( ! isPlainObject( ref[ key ] ) ) {
414
+ return next;
415
+ }
416
+ ref = ref[ key ];
417
+ }
418
+ delete ref[ last ];
419
+ return next;
420
+ }
421
+
422
+ /**
423
+ * @param {*} value
424
+ * @return {*}
425
+ */
426
+ function deepCopy( value ) {
427
+ return JSON.parse( JSON.stringify( value ?? null ) );
428
+ }
package/src/schemaCss.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * JS pair of Schema_Css_Generator declarative css key rules.
2
+ * JS pair of Schema_Css_Generator declarative css key rules + schema CSS loop.
3
3
  */
4
4
 
5
- import { isEmptyViewportValue } from './viewport.js';
5
+ import { isEmptyViewportValue, emitMobileCssVar } from './viewport.js';
6
6
 
7
7
  /**
8
8
  * Resolve a field value: stored → fallbackFrom → default.
@@ -57,6 +57,351 @@ export function cssVarName( prefix, varName, state = '', mobile = false ) {
57
57
  return name;
58
58
  }
59
59
 
60
+ /**
61
+ * Declarative schema CSS parts (rule A + mobile rule M + derived).
62
+ * Same structure as PHP Schema_Css_Generator::from_attrs before wrap_named_css.
63
+ *
64
+ * @param {Object} schema style-schema.json document.
65
+ * @param {Object} [settings] styleSettings.
66
+ * @param {Function|Object|null|undefined} [derivedFn] (settings)=>string[] or
67
+ * { derivedFn, getCssVar }.
68
+ * @param {Function|null|undefined} [getCssVarFn] getCssVar(var, type, value, suffix?).
69
+ * @return {string[]} CSS parts with {{selector}} placeholders.
70
+ */
71
+ export function buildSchemaCss(
72
+ schema,
73
+ settings = {},
74
+ derivedFn = null,
75
+ getCssVarFn = null
76
+ ) {
77
+ let derived = derivedFn;
78
+ let getCssVar = getCssVarFn;
79
+ if ( derived && typeof derived === 'object' && ! Array.isArray( derived ) ) {
80
+ getCssVar = derived.getCssVar ?? getCssVar;
81
+ derived = derived.derivedFn ?? derived.derived ?? null;
82
+ }
83
+ if ( typeof getCssVar !== 'function' ) {
84
+ getCssVar = () => '';
85
+ }
86
+
87
+ const doc = schema && typeof schema === 'object' ? schema : {};
88
+ const bag = settings && typeof settings === 'object' ? settings : {};
89
+ const { desktop, mobile } = buildSchemaDecls( bag, doc, getCssVar );
90
+ const mobileMax = mobileMaxFromSchema( doc );
91
+ const mq = `@media(max-width:${ mobileMax }px)`;
92
+
93
+ const parts = [];
94
+ if ( desktop !== '' ) {
95
+ parts.push( `{{selector}}{${ desktop }}` );
96
+ }
97
+ if ( mobile !== '' ) {
98
+ parts.push( mq + '{{{selector}}{' + mobile + '}}' );
99
+ }
100
+
101
+ if ( typeof derived === 'function' ) {
102
+ const extra = derived( bag, doc );
103
+ if ( Array.isArray( extra ) ) {
104
+ for ( const part of extra ) {
105
+ if ( typeof part === 'string' && part !== '' ) {
106
+ parts.push( part );
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ return parts;
113
+ }
114
+
115
+ /**
116
+ * @param {Object} settings
117
+ * @param {Object} doc
118
+ * @param {Function} getCssVar
119
+ * @return {{ desktop: string, mobile: string }}
120
+ */
121
+ function buildSchemaDecls( settings, doc, getCssVar ) {
122
+ const prefix =
123
+ typeof doc.cssVarPrefix === 'string'
124
+ ? doc.cssVarPrefix
125
+ : doc.slug || '';
126
+ const states = schemaStates( doc );
127
+ const mobileSuffix = mobileSuffixFromSchema( doc );
128
+ let desktopDecls = '';
129
+ let mobileDecls = '';
130
+
131
+ const emitMobile = ( name, cssType, desktopVal, mobileVal ) =>
132
+ emitMobileCssVar( name, cssType, desktopVal, mobileVal, getCssVar );
133
+
134
+ const groups =
135
+ doc.groups && typeof doc.groups === 'object' ? doc.groups : {};
136
+ Object.keys( groups ).forEach( ( groupKey ) => {
137
+ const fields = groups[ groupKey ];
138
+ if ( ! fields || typeof fields !== 'object' ) {
139
+ return;
140
+ }
141
+ Object.keys( fields ).forEach( ( fieldKey ) => {
142
+ const field = fields[ fieldKey ];
143
+ if (
144
+ ! field ||
145
+ typeof field !== 'object' ||
146
+ ! field.css ||
147
+ typeof field.css !== 'object'
148
+ ) {
149
+ return;
150
+ }
151
+ const cssSpec = field.css;
152
+ const varName =
153
+ typeof cssSpec.var === 'string' ? cssSpec.var : '';
154
+ const cssType =
155
+ typeof cssSpec.type === 'string' ? cssSpec.type : '';
156
+ if ( ! varName || ! cssType ) {
157
+ return;
158
+ }
159
+ const baseVar = cssVarName( prefix, varName );
160
+
161
+ const desktopValue = resolveSchemaValue(
162
+ settings,
163
+ groupKey,
164
+ fieldKey,
165
+ '',
166
+ '',
167
+ cssSpec
168
+ );
169
+ if ( ! isEmptyViewportValue( desktopValue ) ) {
170
+ desktopDecls += getCssVar( baseVar, cssType, desktopValue );
171
+ }
172
+
173
+ const fieldStates = Array.isArray( field.states )
174
+ ? field.states
175
+ : null;
176
+ Object.keys( states ).forEach( ( stateName ) => {
177
+ if ( fieldStates && ! fieldStates.includes( stateName ) ) {
178
+ return;
179
+ }
180
+ const storageSuffix =
181
+ states[ stateName ]?.suffix ||
182
+ stateName.charAt( 0 ).toUpperCase() + stateName.slice( 1 );
183
+ const cssState = String( stateName ).toLowerCase();
184
+ const stateValue = resolveSchemaValue(
185
+ settings,
186
+ groupKey,
187
+ fieldKey,
188
+ storageSuffix,
189
+ '',
190
+ cssSpec
191
+ );
192
+ if ( ! isEmptyViewportValue( stateValue ) ) {
193
+ desktopDecls += getCssVar(
194
+ baseVar,
195
+ cssType,
196
+ stateValue,
197
+ `-${ cssState }`
198
+ );
199
+ }
200
+ } );
201
+
202
+ const mobileValue = resolveSchemaValue(
203
+ settings,
204
+ groupKey,
205
+ fieldKey,
206
+ '',
207
+ mobileSuffix,
208
+ cssSpec
209
+ );
210
+ const hasMobile = ! isEmptyViewportValue( mobileValue );
211
+ const inherits = !! cssSpec.mobileInherits;
212
+ if ( hasMobile ) {
213
+ mobileDecls += emitMobile( baseVar, cssType, null, mobileValue );
214
+ } else if (
215
+ inherits &&
216
+ ! isEmptyViewportValue( desktopValue )
217
+ ) {
218
+ mobileDecls += emitMobile(
219
+ baseVar,
220
+ cssType,
221
+ null,
222
+ desktopValue
223
+ );
224
+ }
225
+
226
+ Object.keys( states ).forEach( ( stateName ) => {
227
+ if ( fieldStates && ! fieldStates.includes( stateName ) ) {
228
+ return;
229
+ }
230
+ const storageSuffix =
231
+ states[ stateName ]?.suffix ||
232
+ stateName.charAt( 0 ).toUpperCase() + stateName.slice( 1 );
233
+ const cssState = String( stateName ).toLowerCase();
234
+ const stateMobile = resolveSchemaValue(
235
+ settings,
236
+ groupKey,
237
+ fieldKey,
238
+ storageSuffix,
239
+ mobileSuffix,
240
+ cssSpec
241
+ );
242
+ if ( ! isEmptyViewportValue( stateMobile ) ) {
243
+ mobileDecls += emitMobile(
244
+ `${ baseVar }-${ cssState }`,
245
+ cssType,
246
+ null,
247
+ stateMobile
248
+ );
249
+ }
250
+ } );
251
+ } );
252
+ } );
253
+
254
+ return { desktop: desktopDecls, mobile: mobileDecls };
255
+ }
256
+
257
+ /**
258
+ * Resolve stored → fallbackFrom → css.default (Schema_Css_Generator::resolve_value).
259
+ *
260
+ * @param {Object} settings
261
+ * @param {string} groupKey
262
+ * @param {string} fieldKey
263
+ * @param {string} stateSuffix
264
+ * @param {string} mobileSuffix
265
+ * @param {Object} cssSpec
266
+ * @return {*}
267
+ */
268
+ function resolveSchemaValue(
269
+ settings,
270
+ groupKey,
271
+ fieldKey,
272
+ stateSuffix,
273
+ mobileSuffix,
274
+ cssSpec
275
+ ) {
276
+ const group =
277
+ settings?.[ groupKey ] && typeof settings[ groupKey ] === 'object'
278
+ ? settings[ groupKey ]
279
+ : {};
280
+ const storageKey = `${ fieldKey }${ stateSuffix }${ mobileSuffix }`;
281
+ if (
282
+ Object.prototype.hasOwnProperty.call( group, storageKey ) &&
283
+ ! isEmptyViewportValue( group[ storageKey ] )
284
+ ) {
285
+ return group[ storageKey ];
286
+ }
287
+
288
+ const fallback = cssSpec?.fallbackFrom;
289
+ const paths = Array.isArray( fallback )
290
+ ? fallback
291
+ : fallback
292
+ ? [ fallback ]
293
+ : [];
294
+ for ( const path of paths ) {
295
+ if ( typeof path !== 'string' || path === '' ) {
296
+ continue;
297
+ }
298
+ let value;
299
+ if ( path.includes( '.' ) ) {
300
+ value = getByPath(
301
+ settings,
302
+ `${ path }${ stateSuffix }${ mobileSuffix }`
303
+ );
304
+ if (
305
+ isEmptyViewportValue( value ) &&
306
+ stateSuffix === '' &&
307
+ mobileSuffix === ''
308
+ ) {
309
+ value = getByPath( settings, path );
310
+ }
311
+ } else {
312
+ const fbKey = `${ path }${ stateSuffix }${ mobileSuffix }`;
313
+ value = Object.prototype.hasOwnProperty.call( group, fbKey )
314
+ ? group[ fbKey ]
315
+ : undefined;
316
+ if (
317
+ isEmptyViewportValue( value ) &&
318
+ stateSuffix === '' &&
319
+ mobileSuffix === ''
320
+ ) {
321
+ value = Object.prototype.hasOwnProperty.call( group, path )
322
+ ? group[ path ]
323
+ : undefined;
324
+ }
325
+ }
326
+ if ( ! isEmptyViewportValue( value ) ) {
327
+ return value;
328
+ }
329
+ }
330
+
331
+ if (
332
+ cssSpec &&
333
+ Object.prototype.hasOwnProperty.call( cssSpec, 'default' ) &&
334
+ ! isEmptyViewportValue( cssSpec.default )
335
+ ) {
336
+ if ( stateSuffix === '' && mobileSuffix === '' ) {
337
+ return cssSpec.default;
338
+ }
339
+ }
340
+ return undefined;
341
+ }
342
+
343
+ /**
344
+ * @param {Object} doc
345
+ * @return {Object<string,{suffix?:string}>}
346
+ */
347
+ function schemaStates( doc ) {
348
+ const defaults = {
349
+ hover: { suffix: 'Hover' },
350
+ active: { suffix: 'Active' },
351
+ };
352
+ if (
353
+ ! doc?.states ||
354
+ typeof doc.states !== 'object' ||
355
+ Object.keys( doc.states ).length === 0
356
+ ) {
357
+ return defaults;
358
+ }
359
+ const out = {};
360
+ Object.keys( doc.states ).forEach( ( name ) => {
361
+ const meta = doc.states[ name ];
362
+ out[ name ] =
363
+ meta && typeof meta === 'object'
364
+ ? meta
365
+ : {
366
+ suffix:
367
+ name.charAt( 0 ).toUpperCase() + name.slice( 1 ),
368
+ };
369
+ } );
370
+ return out;
371
+ }
372
+
373
+ /**
374
+ * @param {Object} doc
375
+ * @return {string}
376
+ */
377
+ function mobileSuffixFromSchema( doc ) {
378
+ const mobile = doc?.viewports?.mobile;
379
+ if (
380
+ mobile &&
381
+ typeof mobile === 'object' &&
382
+ typeof mobile.suffix === 'string' &&
383
+ mobile.suffix !== ''
384
+ ) {
385
+ return mobile.suffix;
386
+ }
387
+ return 'Mobile';
388
+ }
389
+
390
+ /**
391
+ * @param {Object} doc
392
+ * @return {number}
393
+ */
394
+ function mobileMaxFromSchema( doc ) {
395
+ const mobile = doc?.viewports?.mobile;
396
+ if ( mobile && typeof mobile === 'object' && Number.isFinite( mobile.max ) ) {
397
+ const max = Number( mobile.max );
398
+ if ( max === 800 || max === 1023 ) {
399
+ return max;
400
+ }
401
+ }
402
+ return 800;
403
+ }
404
+
60
405
  /**
61
406
  * @param {Object|null|undefined} root
62
407
  * @param {string} path