| @@ -68,9 +68,21 @@ | ||
| 68 | 68 | ]; |
| 69 | 69 | |
| 70 | 70 | // A known slug maps to a theme preset class; anything else routes to inline |
| 71 | 71 | // style.color.* — so "yellow" (no matching token) never lands on a wrong slug. |
| 72 | -const routeColors = (attributes, patch, colorSlugs) => { | |
| 72 | +// Kept inline, a token's own hex stops the block following the palette. | |
| 73 | +const slugForValue = (value, colorValues) => { | |
| 74 | + const wanted = colord(value); | |
| 75 | + if (!wanted.isValid()) return null; | |
| 76 | + const hex = wanted.toHex(); | |
| 77 | + const match = Object.entries(colorValues ?? {}).find(([, preset]) => { | |
| 78 | + const parsed = colord(String(preset)); | |
| 79 | + return parsed.isValid() && parsed.toHex() === hex; | |
| 80 | + }); | |
| 81 | + return match?.[0] ?? null; | |
| 82 | +}; | |
| 83 | + | |
| 84 | +const routeColors = (attributes, patch, colorSlugs, colorValues) => { | |
| 73 | 85 | const slugs = new Set(colorSlugs ?? []); |
| 74 | 86 | const stripped = {}; |
| 75 | 87 | let out = attributes; |
| 76 | 88 | for (const [named, key] of COLOR_CHANNELS) { |
| @@ -75,9 +87,13 @@ | ||
| 75 | 87 | let out = attributes; |
| 76 | 88 | for (const [named, key] of COLOR_CHANNELS) { |
| 77 | 89 | const value = patch?.[named]; |
| 78 | 90 | if (value == null) continue; |
| 79 | - if (slugs.has(value)) { | |
| 91 | + const named_slug = slugs.has(value) | |
| 92 | + ? value | |
| 93 | + : slugForValue(value, colorValues); | |
| 94 | + if (named_slug) { | |
| 95 | + if (named_slug !== value) out = setPath(out, named, named_slug); | |
| 80 | 96 | out = unsetPath(out, `style.color.${key}`); |
| 81 | 97 | continue; |
| 82 | 98 | } |
| 83 | 99 | const parsed = colord(value); |
| @@ -175,8 +191,24 @@ | ||
| 175 | 191 | }; |
| 176 | 192 | |
| 177 | 193 | // Re-serializing runs the block's own save(), which core/button needs to render |
| 178 | 194 | // the color/border styles it marks __experimentalSkipSerialization. |
| 195 | +// Merging an empty value leaves a dead key and keeps the preset class. | |
| 196 | +const emptyLeafPaths = (patch, prefix = '') => | |
| 197 | + Object.entries(patch ?? {}).flatMap(([key, value]) => { | |
| 198 | + const path = prefix ? `${prefix}.${key}` : key; | |
| 199 | + if (isMergeable(value)) return emptyLeafPaths(value, path); | |
| 200 | + return value === '' ? [path] : []; | |
| 201 | + }); | |
| 202 | + | |
| 203 | +const withoutEmptyLeaves = (patch) => | |
| 204 | + Object.fromEntries( | |
| 205 | + Object.entries(patch ?? {}).flatMap(([key, value]) => { | |
| 206 | + if (isMergeable(value)) return [[key, withoutEmptyLeaves(value)]]; | |
| 207 | + return value === '' ? [] : [[key, value]]; | |
| 208 | + }), | |
| 209 | + ); | |
| 210 | + | |
| 179 | 211 | export const applyBlockPatch = ( |
| 180 | 212 | serializedBlock, |
| 181 | 213 | patch, |
| 182 | 214 | clear = [], |
| @@ -183,14 +215,22 @@ | ||
| 183 | 215 | presetSlugs = {}, |
| 184 | 216 | ) => { |
| 185 | 217 | const blocks = parse(serializedBlock).map((block) => { |
| 186 | 218 | if (!block.name) return block; |
| 187 | - const merged = deepMerge(block.attributes, remapText(block, patch)); | |
| 219 | + const remapped = remapText(block, patch); | |
| 220 | + const filled = withoutEmptyLeaves(remapped); | |
| 221 | + const merged = deepMerge(block.attributes, filled); | |
| 188 | 222 | const routed = routeNamedPresets( |
| 189 | - routeColors(merged, patch, presetSlugs.color), | |
| 190 | - patch, | |
| 223 | + routeColors(merged, filled, presetSlugs.color, presetSlugs.colorValues), | |
| 224 | + filled, | |
| 191 | 225 | presetSlugs, |
| 192 | 226 | ); |
| 193 | - return { ...block, attributes: applyClears(routed, clear ?? []) }; | |
| 227 | + return { | |
| 228 | + ...block, | |
| 229 | + attributes: applyClears(routed, [ | |
| 230 | + ...(clear ?? []), | |
| 231 | + ...emptyLeafPaths(remapped), | |
| 232 | + ]), | |
| 233 | + }; | |
| 194 | 234 | }); |
| 195 | 235 | return serialize(blocks); |
| 196 | 236 | }; |