PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 All 128 releases
← All changes | src/Agent/lib/block-patch.js +122 -8 3.2.0 → 3.2.2 View file →
@@ -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);
@@ -94,8 +110,50 @@
94 110 }
95 111 return out;
96 112 };
97 113
114 +// Colours WP stores as a slug attribute beside a custom-value attribute.
115 +const PAIRED_COLORS = [
116 + {
117 + named: 'overlayColor',
118 + custom: 'customOverlayColor',
119 + flag: 'isUserOverlayColor',
120 + },
121 + // The icon renders from the value alone; the slug only adds a class.
122 + { named: 'iconColor', custom: 'iconColorValue', valueForSlug: true },
123 + {
124 + named: 'iconBackgroundColor',
125 + custom: 'iconBackgroundColorValue',
126 + valueForSlug: true,
127 + },
128 +];
129 +
130 +const routePairedColors = (attributes, patch, colorSlugs, colorValues) => {
131 + const slugs = new Set(colorSlugs ?? []);
132 + let out = attributes;
133 + for (const { named, custom, flag, valueForSlug } of PAIRED_COLORS) {
134 + const value = patch?.[named];
135 + if (value == null) continue;
136 + const slug = slugs.has(value) ? value : slugForValue(value, colorValues);
137 + if (slug) {
138 + out = { ...out, [named]: slug };
139 + const slugHex = colorValues?.[slug];
140 + out =
141 + valueForSlug && slugHex
142 + ? { ...out, [custom]: colord(String(slugHex)).toHex() }
143 + : unsetPath(out, custom);
144 + } else {
145 + const parsed = colord(value);
146 + out = unsetPath(
147 + { ...out, [custom]: parsed.isValid() ? parsed.toHex() : value },
148 + named,
149 + );
150 + }
151 + if (flag) out = { ...out, [flag]: true };
152 + }
153 + return out;
154 +};
155 +
98 156 // fontSize / fontFamily mirror the color routing: known slug → named attribute
99 157 // (preset class), anything else → the inline style path.
100 158 const NAMED_PRESETS = [
101 159 {
@@ -144,8 +202,9 @@
144 202 ['backgroundColor', 'style.color.background'],
145 203 ['textColor', 'style.color.text'],
146 204 ['gradient', 'style.color.gradient'],
147 205 ...NAMED_PRESETS.map(({ named, path }) => [named, path]),
206 + ...PAIRED_COLORS.map(({ named, custom }) => [named, custom]),
148 207 ];
149 208 const CLEAR_ALIASES = Object.fromEntries(
150 209 ROUTED_PAIRS.flatMap((pair) => pair.map((p) => [p, pair])),
151 210 );
@@ -175,8 +234,53 @@
175 234 };
176 235
177 236 // Re-serializing runs the block's own save(), which core/button needs to render
178 237 // the color/border styles it marks __experimentalSkipSerialization.
238 +// Merging an empty value leaves a dead key and keeps the preset class.
239 +const emptyLeafPaths = (patch, prefix = '') =>
240 + Object.entries(patch ?? {}).flatMap(([key, value]) => {
241 + const path = prefix ? `${prefix}.${key}` : key;
242 + if (isMergeable(value)) return emptyLeafPaths(value, path);
243 + return value === '' ? [path] : [];
244 + });
245 +
246 +// A block with no style would otherwise save the patch's null and emptied keys.
247 +const withoutEmptyLeaves = (patch) =>
248 + Object.fromEntries(
249 + Object.entries(patch ?? {}).flatMap(([key, value]) => {
250 + if (isMergeable(value)) {
251 + const kept = withoutEmptyLeaves(value);
252 + return Object.keys(kept).length ? [[key, kept]] : [];
253 + }
254 + return value === '' || value == null ? [] : [[key, value]];
255 + }),
256 + );
257 +
258 +// Patch values arrive as strings; the parser drops "30" on a number attribute.
259 +const coerceTypes = (block, patch) => {
260 + const attributes = getBlockType(block.name)?.attributes ?? {};
261 + let out = patch;
262 + for (const [key, value] of Object.entries(patch ?? {})) {
263 + if (typeof value !== 'string') continue;
264 + const type = attributes[key]?.type;
265 + if (type === 'boolean' && ['true', 'false'].includes(value)) {
266 + out = { ...out, [key]: value === 'true' };
267 + continue;
268 + }
269 + if (type !== 'number') continue;
270 + const [, amount, unit] =
271 + value.trim().match(/^(-?\d*\.?\d+)\s*([a-z%]*)$/i) ?? [];
272 + if (amount == null) continue;
273 + out = { ...out, [key]: Number(amount) };
274 + // The model writes lengths like "60vh" into the number field.
275 + const unitKey = `${key}Unit`;
276 + if (unit && attributes[unitKey] && out[unitKey] == null) {
277 + out = { ...out, [unitKey]: unit };
278 + }
279 + }
280 + return out;
281 +};
282 +
179 283 export const applyBlockPatch = (
180 284 serializedBlock,
181 285 patch,
182 286 clear = [],
@@ -183,14 +287,24 @@
183 287 presetSlugs = {},
184 288 ) => {
185 289 const blocks = parse(serializedBlock).map((block) => {
186 290 if (!block.name) return block;
187 - const merged = deepMerge(block.attributes, remapText(block, patch));
188 - const routed = routeNamedPresets(
189 - routeColors(merged, patch, presetSlugs.color),
190 - patch,
191 - presetSlugs,
291 + const remapped = remapText(block, patch);
292 + const filled = coerceTypes(block, withoutEmptyLeaves(remapped));
293 + const merged = deepMerge(block.attributes, filled);
294 + const colored = routePairedColors(
295 + routeColors(merged, filled, presetSlugs.color, presetSlugs.colorValues),
296 + filled,
297 + presetSlugs.color,
298 + presetSlugs.colorValues,
192 299 );
193 - return { ...block, attributes: applyClears(routed, clear ?? []) };
300 + const routed = routeNamedPresets(colored, filled, presetSlugs);
301 + return {
302 + ...block,
303 + attributes: applyClears(routed, [
304 + ...(clear ?? []),
305 + ...emptyLeafPaths(remapped),
306 + ]),
307 + };
194 308 });
195 309 return serialize(blocks);
196 310 };