@hoyrynpm/bsm-kit 0.1.0 → 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 +14 -3
- package/package.json +2 -1
- package/src/config.js +5 -2
- package/src/index.js +2 -1
- package/src/migrateSettings.js +428 -0
- package/src/schemaCss.js +347 -2
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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.
|
|
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/config.js
CHANGED
|
@@ -71,9 +71,12 @@ export function getViewportConfig() {
|
|
|
71
71
|
const allowed = Array.isArray( viewports.allowed )
|
|
72
72
|
? viewports.allowed.map( Number ).filter( ( n ) => Number.isFinite( n ) )
|
|
73
73
|
: [ ...DEFAULT_ALLOWED ];
|
|
74
|
+
const rawExceptions = viewports.exceptions;
|
|
74
75
|
const exceptions =
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
rawExceptions &&
|
|
77
|
+
typeof rawExceptions === 'object' &&
|
|
78
|
+
! Array.isArray( rawExceptions )
|
|
79
|
+
? { ...rawExceptions }
|
|
77
80
|
: {};
|
|
78
81
|
return {
|
|
79
82
|
mobileMax:
|
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
|