| @@ -1,4 +1,5 @@ | ||
| 1 | +import { RICH_TEXT_ATTRIBUTES } from '@agent/lib/block-schema'; | |
| 1 | 2 | import { getBlockType, parse, serialize } from '@wordpress/blocks'; |
| 2 | 3 | import { colord } from 'colord'; |
| 3 | 4 | |
| 4 | 5 | const isMergeable = (value) => |
| @@ -67,9 +68,21 @@ | ||
| 67 | 68 | ]; |
| 68 | 69 | |
| 69 | 70 | // A known slug maps to a theme preset class; anything else routes to inline |
| 70 | 71 | // style.color.* — so "yellow" (no matching token) never lands on a wrong slug. |
| 71 | -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) => { | |
| 72 | 85 | const slugs = new Set(colorSlugs ?? []); |
| 73 | 86 | const stripped = {}; |
| 74 | 87 | let out = attributes; |
| 75 | 88 | for (const [named, key] of COLOR_CHANNELS) { |
| @@ -74,9 +87,13 @@ | ||
| 74 | 87 | let out = attributes; |
| 75 | 88 | for (const [named, key] of COLOR_CHANNELS) { |
| 76 | 89 | const value = patch?.[named]; |
| 77 | 90 | if (value == null) continue; |
| 78 | - 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); | |
| 79 | 96 | out = unsetPath(out, `style.color.${key}`); |
| 80 | 97 | continue; |
| 81 | 98 | } |
| 82 | 99 | const parsed = colord(value); |
| @@ -162,19 +179,36 @@ | ||
| 162 | 179 | } |
| 163 | 180 | return out; |
| 164 | 181 | }; |
| 165 | 182 | |
| 166 | -// The model always says `text`; blocks whose rich text is `content` need the remap. | |
| 183 | +// The model always says `text`; blocks that keep theirs elsewhere need the remap. | |
| 167 | 184 | const remapText = (block, patch) => { |
| 168 | 185 | if (patch?.text == null) return patch; |
| 169 | 186 | const attributes = getBlockType(block.name)?.attributes ?? {}; |
| 170 | - if (attributes.text || !attributes.content) return patch; | |
| 187 | + const real = RICH_TEXT_ATTRIBUTES.find((name) => attributes[name]); | |
| 188 | + if (!real || real === 'text') return patch; | |
| 171 | 189 | const { text, ...rest } = patch; |
| 172 | - return { ...rest, content: text }; | |
| 190 | + return { ...rest, [real]: text }; | |
| 173 | 191 | }; |
| 174 | 192 | |
| 175 | 193 | // Re-serializing runs the block's own save(), which core/button needs to render |
| 176 | 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 | + | |
| 177 | 211 | export const applyBlockPatch = ( |
| 178 | 212 | serializedBlock, |
| 179 | 213 | patch, |
| 180 | 214 | clear = [], |
| @@ -181,14 +215,22 @@ | ||
| 181 | 215 | presetSlugs = {}, |
| 182 | 216 | ) => { |
| 183 | 217 | const blocks = parse(serializedBlock).map((block) => { |
| 184 | 218 | if (!block.name) return block; |
| 185 | - 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); | |
| 186 | 222 | const routed = routeNamedPresets( |
| 187 | - routeColors(merged, patch, presetSlugs.color), | |
| 188 | - patch, | |
| 223 | + routeColors(merged, filled, presetSlugs.color, presetSlugs.colorValues), | |
| 224 | + filled, | |
| 189 | 225 | presetSlugs, |
| 190 | 226 | ); |
| 191 | - return { ...block, attributes: applyClears(routed, clear ?? []) }; | |
| 227 | + return { | |
| 228 | + ...block, | |
| 229 | + attributes: applyClears(routed, [ | |
| 230 | + ...(clear ?? []), | |
| 231 | + ...emptyLeafPaths(remapped), | |
| 232 | + ]), | |
| 233 | + }; | |
| 192 | 234 | }); |
| 193 | 235 | return serialize(blocks); |
| 194 | 236 | }; |