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 +205 -9 2.13.1 → 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,9 +263,9 @@
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 . '}';
178 270
179 271 // A wrapping container's own children must size from their content,
@@ -189,8 +281,9 @@
189 281 // Derived from the pairs rather than stored, so it costs nothing in
190 282 // the bucket tree, and — because this runs after style_hash() has
191 283 // read $rules — the hash in already-saved markup is unaffected.
192 284 $body .= self::wrap_child_css( $pairs, $selector_base . $state );
285 + $body .= self::flex_child_css( $pairs, $selector_base . $state );
193 286
194 287 $css .= ( '' !== $media ) ? $media . '{' . $body . '}' : $body;
195 288 }
196 289 return $css;
@@ -196,8 +289,82 @@
196 289 return $css;
197 290 }
198 291
199 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 + /**
200 367 * The child rule a wrapping container needs, or ''.
201 368 *
202 369 * Scoped to container children only, matching the base stylesheets — a leaf
203 370 * block is sized by its own block, not by the row it sits in. `:where()`
@@ -278,8 +445,20 @@
278 445 && '' === StyleBuckets::device_bucket_key( $bucket['device'] );
279 446 }
280 447
281 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 + /**
282 461 * A bucket that sets a solid background colour and no gradient of its own
283 462 * must clear any gradient inherited from a lower-precedence bucket:
284 463 * `background-color` and `background-image` are separate properties, so the
285 464 * gradient would otherwise stay painted on top of the solid colour.
@@ -312,9 +491,9 @@
312 491 * rather than enumerating props itself, so the JS editor compiler and this
313 492 * one cannot drift on which props exist, what CSS property each maps to, or
314 493 * what order they emit in.
315 494 */
316 - public static function state_declarations( $props ) {
495 + public static function state_declarations( $props, $inherited_border_style = '' ) {
317 496 $css = [];
318 497 if ( ! is_array( $props ) ) {
319 498 return $css;
320 499 }
@@ -335,9 +514,17 @@
335 514
336 515 case 'color':
337 516 $value = self::read_scalar( $props, $prop );
338 517 if ( '' !== $value ) {
339 - $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 + }
340 527 }
341 528 break;
342 529
343 530 case 'scalar':
@@ -377,9 +564,9 @@
377 564 }
378 565 break;
379 566
380 567 case 'border':
381 - $css = array_merge( $css, self::border_css( $props ) );
568 + $css = array_merge( $css, self::border_css( $props, $inherited_border_style ) );
382 569 break;
383 570
384 571 case 'dimensions':
385 572 $css = array_merge( $css, self::spacing_css( $props, $prop ) );
@@ -470,9 +657,9 @@
470 657 * OR only a colour still gets one. Colour-only is the common case — a hover
471 658 * bucket that recolours an existing border — and it rendered nothing before
472 659 * this fallback covered it.
473 660 */
474 - private static function border_css( $props ) {
661 + private static function border_css( $props, $inherited_style = '' ) {
475 662 $css = [];
476 663
477 664 $width = self::read_range( $props, 'borderWidth' );
478 665 $style = self::read_scalar( $props, 'borderStyle' );
@@ -501,9 +688,18 @@
501 688 }
502 689
503 690 // A width on any single side needs a style too, or it paints nothing.
504 691 if ( '' !== $width || $has_side_width || '' !== $color ) {
505 - $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 + }
506 702 } elseif ( '' !== $style ) {
507 703 // A style on its own is meaningful (e.g. `none` to remove a border).
508 704 $css['border-style'] = $style;
509 705 }