| @@ -1,5 +1,8 @@ | ||
| 1 | -import { isObject } from '@shared/lib/utils'; | |
| 1 | +import { | |
| 2 | + applyOwnedSection, | |
| 3 | + collectOwnedPaths, | |
| 4 | +} from '@shared/lib/owned-globals'; | |
| 2 | 5 | |
| 3 | 6 | const ownedSettings = ['layout', 'typography', 'custom']; |
| 4 | 7 | // Per-leaf, so clearing a heading size leaves fontFamily to the fonts layer. |
| 5 | 8 | // WordPress reads duotone per block name, never per variation, so blocks is owned here. |
| @@ -4,20 +7,8 @@ | ||
| 4 | 7 | // Per-leaf, so clearing a heading size leaves fontFamily to the fonts layer. |
| 5 | 8 | // WordPress reads duotone per block name, never per variation, so blocks is owned here. |
| 6 | 9 | const ownedStyles = ['typography', 'elements', 'blocks']; |
| 7 | 10 | |
| 8 | -const leafPaths = (value, prefix) => { | |
| 9 | - // PHP serializes an empty object as []; as a leaf it would purge the branch. | |
| 10 | - if (Array.isArray(value) && value.length === 0) return []; | |
| 11 | - if (!isObject(value)) return [prefix]; | |
| 12 | - | |
| 13 | - const entries = Object.entries(value); | |
| 14 | - // Vibes ship `elements: {}` to mean none; as a leaf it would purge fonts too. | |
| 15 | - if (entries.length === 0) return []; | |
| 16 | - | |
| 17 | - return entries.flatMap(([key, child]) => leafPaths(child, [...prefix, key])); | |
| 18 | -}; | |
| 19 | - | |
| 20 | 11 | // The fonts and palette layers own these; a declaring payload is ignored. |
| 21 | 12 | const isExcludedPath = (section, path) => { |
| 22 | 13 | if (path[path.length - 1] === 'fontFamily') return true; |
| 23 | 14 | if (section !== 'settings') return false; |
| @@ -24,89 +15,29 @@ | ||
| 24 | 15 | if (path[0] === 'color') return true; |
| 25 | 16 | return path[0] === 'typography' && path[1] === 'fontFamilies'; |
| 26 | 17 | }; |
| 27 | 18 | |
| 28 | -const collectOwnedPaths = (vibes, section, roots) => { | |
| 29 | - const paths = new Map(); | |
| 30 | - | |
| 31 | - for (const vibe of Object.values(vibes)) { | |
| 32 | - const source = vibe?.[section]; | |
| 33 | - if (!isObject(source)) continue; | |
| 34 | - | |
| 35 | - for (const key of roots) { | |
| 36 | - if (!(key in source)) continue; | |
| 37 | - | |
| 38 | - for (const path of leafPaths(source[key], [key])) { | |
| 39 | - if (isExcludedPath(section, path)) continue; | |
| 40 | - paths.set(path.join('.'), path); | |
| 41 | - } | |
| 42 | - } | |
| 43 | - } | |
| 44 | - | |
| 45 | - return [...paths.values()]; | |
| 46 | -}; | |
| 47 | - | |
| 48 | -const valueAtPath = (source, path) => | |
| 49 | - path.reduce( | |
| 50 | - (value, key) => (isObject(value) ? value[key] : undefined), | |
| 51 | - source, | |
| 52 | - ); | |
| 53 | - | |
| 54 | -const withoutPath = (source, [key, ...rest]) => { | |
| 55 | - if (!isObject(source) || !(key in source)) return source; | |
| 56 | - | |
| 57 | - const updated = { ...source }; | |
| 58 | - | |
| 59 | - if (rest.length === 0) { | |
| 60 | - delete updated[key]; | |
| 61 | - return updated; | |
| 62 | - } | |
| 63 | - | |
| 64 | - const child = withoutPath(updated[key], rest); | |
| 65 | - | |
| 66 | - if (isObject(child) && Object.keys(child).length === 0) { | |
| 67 | - delete updated[key]; | |
| 68 | - } else { | |
| 69 | - updated[key] = child; | |
| 70 | - } | |
| 71 | - | |
| 72 | - return updated; | |
| 73 | -}; | |
| 74 | - | |
| 75 | -const withPath = (source, [key, ...rest], value) => { | |
| 76 | - const updated = isObject(source) ? { ...source } : {}; | |
| 77 | - updated[key] = | |
| 78 | - rest.length === 0 ? value : withPath(updated[key], rest, value); | |
| 79 | - return updated; | |
| 80 | -}; | |
| 81 | - | |
| 82 | -// Every vibe's leaves are purged first, so a switch leaves none of the last one behind. | |
| 83 | -// A vibe that declares nothing lets the theme's own theme.json surface again. | |
| 84 | -const applySection = (current, incoming, ownedPaths) => { | |
| 85 | - const purged = ownedPaths.reduce( | |
| 86 | - (section, path) => withoutPath(section, path), | |
| 87 | - current, | |
| 88 | - ); | |
| 89 | - | |
| 90 | - if (!isObject(incoming)) return purged; | |
| 91 | - | |
| 92 | - return ownedPaths.reduce((section, path) => { | |
| 93 | - const value = valueAtPath(incoming, path); | |
| 94 | - return value === undefined ? section : withPath(section, path, value); | |
| 95 | - }, purged); | |
| 96 | -}; | |
| 97 | - | |
| 98 | 19 | export const ownedSettingPaths = (vibes) => |
| 99 | - collectOwnedPaths(vibes, 'settings', ownedSettings); | |
| 20 | + collectOwnedPaths({ | |
| 21 | + payloads: vibes, | |
| 22 | + section: 'settings', | |
| 23 | + roots: ownedSettings, | |
| 24 | + exclude: isExcludedPath, | |
| 25 | + }); | |
| 100 | 26 | |
| 101 | 27 | export const ownedStylePaths = (vibes) => |
| 102 | - collectOwnedPaths(vibes, 'styles', ownedStyles); | |
| 28 | + collectOwnedPaths({ | |
| 29 | + payloads: vibes, | |
| 30 | + section: 'styles', | |
| 31 | + roots: ownedStyles, | |
| 32 | + exclude: isExcludedPath, | |
| 33 | + }); | |
| 103 | 34 | |
| 104 | 35 | export const applyVibeGlobals = ({ currentSettings, vibeSettings, vibes }) => |
| 105 | - applySection(currentSettings, vibeSettings, ownedSettingPaths(vibes)); | |
| 36 | + applyOwnedSection(currentSettings, vibeSettings, ownedSettingPaths(vibes)); | |
| 106 | 37 | |
| 107 | 38 | export const applyVibeStyles = ({ currentStyles, vibeStyles, vibes }) => |
| 108 | - applySection(currentStyles, vibeStyles, ownedStylePaths(vibes)); | |
| 39 | + applyOwnedSection(currentStyles, vibeStyles, ownedStylePaths(vibes)); | |
| 109 | 40 | |
| 110 | 41 | // A fonts or colors write may not move vibe-owned leaves. |
| 111 | 42 | export const preserveVibeSettings = ({ |
| 112 | 43 | mergedSettings, |
| @@ -111,12 +42,13 @@ | ||
| 111 | 42 | export const preserveVibeSettings = ({ |
| 112 | 43 | mergedSettings, |
| 113 | 44 | currentSettings, |
| 114 | 45 | vibes, |
| 115 | -}) => applySection(mergedSettings, currentSettings, ownedSettingPaths(vibes)); | |
| 46 | +}) => | |
| 47 | + applyOwnedSection(mergedSettings, currentSettings, ownedSettingPaths(vibes)); | |
| 116 | 48 | |
| 117 | 49 | export const preserveVibeStyles = ({ mergedStyles, currentStyles, vibes }) => |
| 118 | - applySection(mergedStyles, currentStyles, ownedStylePaths(vibes)); | |
| 50 | + applyOwnedSection(mergedStyles, currentStyles, ownedStylePaths(vibes)); | |
| 119 | 51 | |
| 120 | 52 | // natural-1 is what the theme ships, so writing it would only restate the theme. |
| 121 | 53 | export const vibeGlobalsEntry = (vibes, selectedVibe) => |
| 122 | 54 | selectedVibe === 'natural-1' ? undefined : vibes?.[selectedVibe]; |