PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.15.0
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.15.0
2.15.0 2.14.0 2.13.0 2.13.1 2.12.0 2.11.1 2.11.0 2.10.0 2.9.0 2.7.4 2.7.5 2.7.6 2.7.7 2.8.0 2.8.1 2.9.1 trunk 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.0.1 1.0.2 1.0.3 1.1.0 All 82 releases
← All changes | includes/classes/atomic-styles.php +300 -12 2.12.0 → 2.15.0 View file →
@@ -22,8 +22,29 @@
22 22 */
23 23 class AtomicStyles {
24 24
25 25 /**
26 + * Revision of the CSS this compiler emits. Bump it whenever the emitted
27 + * output changes for the same stored styles.
28 + *
29 + * Front-end pages do not compile per request: each page is baked once
30 + * into an uploads stylesheet, and Assets::build_revision() decides when
31 + * that file is stale. It was keyed on ABLOCKS_VERSION alone, so an
32 + * emission change with no version bump never reached pages baked before
33 + * it. The centring-margin fix (emitted_value() / flex_child_css()) showed
34 + * up in the editor, which compiles live, while every existing page kept
35 + * serving the old `margin-left:auto` and its wide flex-row gap.
36 + *
37 + * 2: centring margins resolved per parent layout.
38 + * 3: a bucket that sets a border width/colour but no type keeps the type it
39 + * inherits instead of `solid`.
40 + * 4: Atomic Text's static top-margin reset yields to WordPress's layout
41 + * block gap. Baked pages embed each block's static style.css as well,
42 + * so a change there needs this bump just like a compiler change does.
43 + */
44 + const OUTPUT_REVISION = 4;
45 +
46 + /**
26 47 * Compile a full bucket tree for one selector base into CSS.
27 48 *
28 49 * The stored state key IS the pseudo-selector, so it is appended directly;
29 50 * the breakpoint comes from the bucket's device via the one media-query
@@ -52,11 +73,14 @@
52 73 $rules = [];
53 74 $devices = Helper::get_responsive_devices();
54 75 $has_styles = StyleBuckets::has_schema_version( $styles );
55 76
56 - foreach ( $devices as $device ) {
77 + $devices = array_values( $devices );
78 +
79 + foreach ( $devices as $index => $device ) {
57 80 $media = Helper::breakpoint_media_query( $device );
58 81 $bucket_key = StyleBuckets::device_bucket_key( $device );
82 + $cascade = self::cascade_devices( $devices, $index );
59 83
60 84 foreach ( StyleBuckets::state_keys() as $state ) {
61 85 $declarations = [];
62 86
@@ -62,10 +86,14 @@
62 86
63 87 if ( $has_styles ) {
64 88 $props = StyleBuckets::read_bucket( $styles, $bucket_key, $state );
65 89 if ( ! empty( $props ) ) {
66 - $declarations = self::apply_background_reset(
67 - self::state_declarations( $props ),
90 + // The border type in force from the rest of the cascade,
91 + // so a bucket that only changes width/colour keeps it
92 + // rather than falling back to `solid`.
93 + $inherited_style = StyleBuckets::inherited_prop( $styles, $bucket_key, $state, 'borderStyle', $cascade );
94 + $declarations = self::apply_background_reset(
95 + self::state_declarations( $props, is_scalar( $inherited_style ) ? (string) $inherited_style : '' ),
68 96 '' === $state && '' === $bucket_key
69 97 );
70 98 }
71 99 }
@@ -100,8 +128,35 @@
100 128 return $rules;
101 129 }
102 130
103 131 /**
132 + * The devices a device's rules cascade from on the page: those that also
133 + * apply at its width and are emitted before it, widest-first, ending at the
134 + * device itself. Mirror of the JS `cascadeDevices()` (atomic-shared/hash.js),
135 + * which builds it from `devicesApplyingAt( representativeWidth() )`.
136 + *
137 + * @param array $devices Ordered device list (widest-first, base first).
138 + * @param int $index The device's position in it.
139 + * @return array Devices to inherit from.
140 + */
141 + private static function cascade_devices( $devices, $index ) {
142 + $own = (array) $devices[ $index ];
143 + // A device's representative width: its upper bound, else its lower
144 + // bound, else "very wide" for the base device.
145 + $width = ! empty( $own['max'] ) ? (int) $own['max'] : ( ! empty( $own['min'] ) ? (int) $own['min'] : 99999 );
146 +
147 + $out = [];
148 + foreach ( array_slice( $devices, 0, $index + 1 ) as $device ) {
149 + list( $min, $max ) = Helper::breakpoint_bounds( $device );
150 + if ( ( $min > 0 && $width < $min ) || ( $max > 0 && $width > $max ) ) {
151 + continue;
152 + }
153 + $out[] = $device;
154 + }
155 + return $out;
156 + }
157 +
158 + /**
104 159 * FNV-1a 32-bit over the normalised rule list.
105 160 *
106 161 * Deliberately not md5: the editor has to compute the identical hash at save
107 162 * time, and FNV-1a is a handful of lines in both languages rather than a
@@ -159,8 +214,45 @@
159 214
160 215 return [ 'class' => $class, 'css' => self::rules_to_css( $rules, '.' . $class . '.' . $class ) ];
161 216 }
162 217
218 + /**
219 + * Point a block's saved markup at its current style class.
220 + *
221 + * The editor stamps `ablocks-s-{hash}` into the markup at save time, but the
222 + * front end emits rules for the hash of what the compiler produces NOW. When
223 + * the compiled output changes for the same stored styles (OUTPUT_REVISION),
224 + * a post saved before carries a class no rule targets any more and the
225 + * block renders unstyled until someone re-saves it. Only the block's own
226 + * opening tag is touched — inner blocks are rendered, and fixed, on their
227 + * own — and only when it lacks the current class.
228 + *
229 + * @param string $content The block's saved markup.
230 + * @param array $styles The block's styles attribute.
231 + * @param array $alignment The block's alignment attribute.
232 + * @return string The markup, with a stale style class replaced.
233 + */
234 + public static function refresh_style_class( $content, $styles, $alignment = [] ) {
235 + if ( ! is_string( $content ) || false === strpos( $content, 'ablocks-s-' ) ) {
236 + return $content;
237 + }
238 + if ( ! preg_match( '/^\s*<[a-zA-Z][^>]*>/', $content, $tag ) ) {
239 + return $content;
240 + }
241 + $open = $tag[0];
242 + if ( ! preg_match( '/(?<=[\s"\'])ablocks-s-[0-9a-z]+(?=[\s"\'])/', $open, $stale ) ) {
243 + return $content;
244 + }
245 +
246 + $current = self::style_class( $styles, $alignment );
247 + if ( '' === $current || preg_match( '/(?<=[\s"\'])' . preg_quote( $current, '/' ) . '(?=[\s"\'])/', $open ) ) {
248 + return $content;
249 + }
250 +
251 + $fixed = preg_replace( '/(?<=[\s"\'])' . preg_quote( $stale[0], '/' ) . '(?=[\s"\'])/', $current, $open, 1 );
252 + return substr_replace( $content, $fixed, strpos( $content, $open ), strlen( $open ) );
253 + }
254 +
163 255 /** Render a normalised rule list against a selector base. */
164 256 public static function rules_to_css( $rules, $selector_base ) {
165 257 $css = '';
166 258 foreach ( $rules as $rule ) {
@@ -171,16 +263,183 @@
171 263 // — scalar, range, colour, typography, effect, overlay — passes
172 264 // through one guard on its way out. This runs after style_hash()
173 265 // has already read $rules, so the hash the editor writes into
174 266 // the markup is unaffected.
175 - $declarations .= Helper::esc_css_value( $pair[0] ) . ':' . Helper::esc_css_value( $pair[1] ) . ';';
267 + $declarations .= Helper::esc_css_value( $pair[0] ) . ':' . Helper::esc_css_value( self::emitted_value( $pair[0], $pair[1] ) ) . ';';
176 268 }
177 269 $body = $selector_base . $state . '{' . $declarations . '}';
270 +
271 + // A wrapping container's own children must size from their content,
272 + // or the line can never be over-subscribed and `flex-wrap` never
273 + // breaks one. That is a statement about THIS container's children,
274 + // so it is emitted as a child rule here rather than as an inherited
275 + // custom property: a custom property inherits down the whole tree,
276 + // so a wrapping container silently re-sized the children of every
277 + // non-wrapping container nested inside it — measured, a
278 + // non-wrapping inner container's children came out `flex-basis:
279 + // auto` (content-sized) instead of `0%` (equal share).
280 + //
281 + // Derived from the pairs rather than stored, so it costs nothing in
282 + // the bucket tree, and — because this runs after style_hash() has
283 + // read $rules — the hash in already-saved markup is unaffected.
284 + $body .= self::wrap_child_css( $pairs, $selector_base . $state );
285 + $body .= self::flex_child_css( $pairs, $selector_base . $state );
286 +
178 287 $css .= ( '' !== $media ) ? $media . '{' . $body . '}' : $body;
179 288 }
180 289 return $css;
181 290 }
182 291
292 + /**
293 + * The value a declaration is emitted with, which is its compiled value
294 + * except for the centring guard's auto margins (see state_declarations()).
295 + *
296 + * Those centre a width-capped box in normal flow, but in a flex row an
297 + * auto margin swallows the free space on the main axis and overrides the
298 + * parent's justify-content: two 180px children of a centred, 24px-gap row
299 + * came out ~116px apart, the leftover space split into all four margins.
300 + * Emitted through a custom property instead, which the parent resolves
301 + * for its own children (flex_child_css()) and which falls back to the
302 + * same `auto` everywhere else. An `auto` here can only be the guard's: the
303 + * Dimensions control always appends a unit, and the guard stands down
304 + * when the author set a horizontal margin.
305 + *
306 + * Rewritten at emission, after style_hash() has read the pairs, so the
307 + * class in already-saved markup is unaffected.
308 + *
309 + * Mirrors emittedValue() in atomic-shared/styles.js.
310 + *
311 + * @param string $property The CSS property.
312 + * @param string $value The compiled value.
313 + * @return string The value to emit.
314 + */
315 + public static function emitted_value( $property, $value ) {
316 + if ( 'auto' === $value && ( 'margin-left' === $property || 'margin-right' === $property ) ) {
317 + return 'var(--ablocks-center-margin,auto)';
318 + }
319 + return $value;
320 + }
321 +
322 + /**
323 + * The child rule a bucket that sets `display` or `flex-direction` needs,
324 + * or ''. Resolves --ablocks-center-margin (see emitted_value()) for this
325 + * container's own children: 0 across a flex row's main axis, the `auto`
326 + * fallback in a flex column (where horizontal is the cross axis and
327 + * centring cannot open a gap) and in any other display.
328 + *
329 + * Display and direction are separate variables because they may come
330 + * from different breakpoints — a row at Desktop turned into a column at
331 + * Tablet only compiles `flex-direction` there — and the cascade has to
332 + * combine them. The inner var() is substituted on the child itself, so
333 + * it reads the direction set for that same child.
334 + *
335 + * Custom properties inherit, so the grandchildren are reset to the
336 + * guaranteed-invalid value; `:where()` keeps that at 0-0-0, below any
337 + * nested container's own child rule. Grid is left alone on purpose: an
338 + * auto margin there centres an item in its own cell and opens no gap.
339 + *
340 + * Mirrors flexChildCss() in atomic-shared/styles.js.
341 + *
342 + * @param array $pairs The bucket's declaration pairs.
343 + * @param string $selector The already-composed selector for this bucket.
344 + * @return string A CSS rule, or ''.
345 + */
346 + public static function flex_child_css( $pairs, $selector ) {
347 + $declarations = '';
348 + foreach ( $pairs as $pair ) {
349 + if ( 'display' === $pair[0] ) {
350 + $declarations .= ( 'flex' === $pair[1] || 'inline-flex' === $pair[1] )
351 + ? '--ablocks-center-margin:var(--ablocks-column-margin,0);'
352 + : '--ablocks-center-margin:initial;';
353 + } elseif ( 'flex-direction' === $pair[0] ) {
354 + $declarations .= ( 'column' === $pair[1] || 'column-reverse' === $pair[1] )
355 + ? '--ablocks-column-margin:auto;'
356 + : '--ablocks-column-margin:initial;';
357 + }
358 + }
359 + if ( '' === $declarations ) {
360 + return '';
361 + }
362 + return $selector . '>*{' . $declarations . '}'
363 + . ':where(' . $selector . '>*>*){--ablocks-center-margin:initial;--ablocks-column-margin:initial;}';
364 + }
365 +
366 + /**
367 + * The child rule a wrapping container needs, or ''.
368 + *
369 + * Scoped to container children only, matching the base stylesheets — a leaf
370 + * block is sized by its own block, not by the row it sits in. `:where()`
371 + * keeps the selector at the same specificity as those base rules, and this
372 + * <style> is injected after them, so it wins on order alone.
373 + *
374 + * Mirrors wrapChildCss() in atomic-shared/styles.js.
375 + *
376 + * @param array $pairs The bucket's declaration pairs.
377 + * @param string $selector The already-composed selector for this bucket.
378 + * @return string A CSS rule, or ''.
379 + */
380 + public static function wrap_child_css( $pairs, $selector ) {
381 + $wraps = false;
382 + foreach ( $pairs as $pair ) {
383 + if ( 'flex-wrap' === $pair[0]
384 + && ( 'wrap' === $pair[1] || 'wrap-reverse' === $pair[1] ) ) {
385 + $wraps = true;
386 + }
387 + }
388 + if ( ! $wraps ) {
389 + return '';
390 + }
391 + return $selector . self::WRAP_CHILD_SELECTOR . '{flex-basis:auto;}';
392 + }
393 +
394 + /** The child combinator both compilers append for a wrapping container. */
395 + const WRAP_CHILD_SELECTOR = '>:where(.ablocks-atomic-div,.ablocks-atomic-flex,.ablocks-atomic-grid)';
396 +
397 + /**
398 + * A block-specific rule emitted only for the buckets that compile a given
399 + * CSS property — media query and state preserved.
400 + *
401 + * Lets one block react to a declaration the shared compiler produced
402 + * without that reaction leaking to every other atomic block, and without a
403 + * second copy of the bucket/breakpoint walk. Used by the SVG block, whose
404 + * graphic must stop filling its wrapper once the author has asked for the
405 + * wrapper to position it.
406 + *
407 + * @param array $styles The block's styles object.
408 + * @param string $selector_base The block's own selector.
409 + * @param string $property The compiled CSS property to look for.
410 + * @param string $suffix Appended to the selector (e.g. ' svg').
411 + * @param string $declarations The declarations to emit.
412 + * @param string|null $unless_property Skip a bucket that ALSO compiles this
413 + * property — an explicit value there is
414 + * more specific than the reaction being
415 + * conditioned on, and must win outright
416 + * rather than being overridden by it.
417 + * @return string CSS, or ''.
418 + */
419 + public static function conditional_rules( $styles, $selector_base, $property, $suffix, $declarations, $unless_property = null ) {
420 + $css = '';
421 + foreach ( self::compile_rules( $styles ) as $rule ) {
422 + list( $media, $state, $pairs ) = $rule;
423 + $found = false;
424 + $skip = false;
425 + foreach ( $pairs as $pair ) {
426 + if ( $pair[0] === $property ) {
427 + $found = true;
428 + }
429 + if ( null !== $unless_property && $pair[0] === $unless_property ) {
430 + $skip = true;
431 + }
432 + }
433 + if ( ! $found || $skip ) {
434 + continue;
435 + }
436 + $body = $selector_base . $state . $suffix . '{' . $declarations . '}';
437 + $css .= ( '' !== $media ) ? $media . '{' . $body . '}' : $body;
438 + }
439 + return $css;
440 + }
441 +
183 442 /** Whether a bucket is the base one (base device, normal state). */
184 443 public static function is_base_bucket( $bucket ) {
185 444 return '' === $bucket['state']
186 445 && '' === StyleBuckets::device_bucket_key( $bucket['device'] );
@@ -186,8 +445,20 @@
186 445 && '' === StyleBuckets::device_bucket_key( $bucket['device'] );
187 446 }
188 447
189 448 /**
449 + * Whether a backgroundColor value is itself a gradient function (the
450 + * Background tab's Color control can now produce one — see
451 + * ABlocksColorControl's `isGradient` picker). Such a value is not valid
452 + * CSS under `background-color`; it belongs under `background-image`
453 + * instead, the same property the legacy `backgroundGradient` scalar and
454 + * `backgroundOverlay` layers already use. Mirrors JS `isGradientValue()`.
455 + */
456 + public static function is_gradient_value( $value ) {
457 + return is_string( $value ) && ( 0 === strpos( $value, 'linear-gradient(' ) || 0 === strpos( $value, 'radial-gradient(' ) );
458 + }
459 +
460 + /**
190 461 * A bucket that sets a solid background colour and no gradient of its own
191 462 * must clear any gradient inherited from a lower-precedence bucket:
192 463 * `background-color` and `background-image` are separate properties, so the
193 464 * gradient would otherwise stay painted on top of the solid colour.
@@ -220,9 +491,9 @@
220 491 * rather than enumerating props itself, so the JS editor compiler and this
221 492 * one cannot drift on which props exist, what CSS property each maps to, or
222 493 * what order they emit in.
223 494 */
224 - public static function state_declarations( $props ) {
495 + public static function state_declarations( $props, $inherited_border_style = '' ) {
225 496 $css = [];
226 497 if ( ! is_array( $props ) ) {
227 498 return $css;
228 499 }
@@ -243,9 +514,17 @@
243 514
244 515 case 'color':
245 516 $value = self::read_scalar( $props, $prop );
246 517 if ( '' !== $value ) {
247 - $css[ $entry['css'] ] = Color::get_css( $value );
518 + $css_value = Color::get_css( $value );
519 + // backgroundColor is the one colour prop whose value can be
520 + // a gradient function; every other colour prop (textColor,
521 + // borderColor) keeps writing its own CSS property as before.
522 + if ( 'backgroundColor' === $prop && self::is_gradient_value( $css_value ) ) {
523 + $css['background-image'] = $css_value;
524 + } else {
525 + $css[ $entry['css'] ] = $css_value;
526 + }
248 527 }
249 528 break;
250 529
251 530 case 'scalar':
@@ -285,9 +564,9 @@
285 564 }
286 565 break;
287 566
288 567 case 'border':
289 - $css = array_merge( $css, self::border_css( $props ) );
568 + $css = array_merge( $css, self::border_css( $props, $inherited_border_style ) );
290 569 break;
291 570
292 571 case 'dimensions':
293 572 $css = array_merge( $css, self::spacing_css( $props, $prop ) );
@@ -305,11 +584,11 @@
305 584 $has_width = '' !== self::read_range( $props, 'width' );
306 585 $has_max_width = '' !== self::read_range( $props, 'maxWidth' );
307 586
308 587 if ( $has_width ) {
309 - if ( '' === self::read_scalar( $props, 'flexShrink' ) ) {
310 - $css['flex-shrink'] = '0';
311 - }
588 + // No `flex-shrink: 0` — see the JS note. Pinning shrink to 0 is
589 + // what let a child escape its parent, and `flex-basis: auto` below
590 + // already holds the width whenever the row has room for it.
312 591 if ( '' === self::read_scalar( $props, 'flexGrow' ) ) {
313 592 $css['flex-grow'] = '0';
314 593 }
315 594 if ( '' === self::read_scalar( $props, 'flexBasis' ) ) {
@@ -378,9 +657,9 @@
378 657 * OR only a colour still gets one. Colour-only is the common case — a hover
379 658 * bucket that recolours an existing border — and it rendered nothing before
380 659 * this fallback covered it.
381 660 */
382 - private static function border_css( $props ) {
661 + private static function border_css( $props, $inherited_style = '' ) {
383 662 $css = [];
384 663
385 664 $width = self::read_range( $props, 'borderWidth' );
386 665 $style = self::read_scalar( $props, 'borderStyle' );
@@ -409,9 +688,18 @@
409 688 }
410 689
411 690 // A width on any single side needs a style too, or it paints nothing.
412 691 if ( '' !== $width || $has_side_width || '' !== $color ) {
413 - $css['border-style'] = '' !== $style ? $style : 'solid';
692 + // Without a type of its own the bucket carries the one it inherits:
693 + // this rule's border-style would otherwise override a wider
694 + // device's (or the normal state's) `dashed` with a `solid` nobody
695 + // chose. `solid` is only the default when nothing up the cascade
696 + // sets a type either.
697 + if ( '' !== $style ) {
698 + $css['border-style'] = $style;
699 + } else {
700 + $css['border-style'] = '' !== $inherited_style ? $inherited_style : 'solid';
701 + }
414 702 } elseif ( '' !== $style ) {
415 703 // A style on its own is meaningful (e.g. `none` to remove a border).
416 704 $css['border-style'] = $style;
417 705 }