PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.13.1
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.13.1
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 1.1.1 1.1.2 All 80 releases
ablocks / includes / classes / css-dedupe.php

css-dedupe.php in aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder 2.13.1, at includes/classes/css-dedupe.php

143 lines 4.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace ABlocks\Classes;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * Deduplicate generated per-instance CSS by merging rules that share an
10 * identical declaration body into a single grouped selector
11 * (`.a{X} .b{X}` -> `.a,.b{X}`).
12 *
13 * Safety: aBlocks generates per-instance CSS under a unique
14 * `.ablocks-block-{id}` wrapper, so almost every selector is globally unique
15 * and therefore cascade-independent (nothing else in the sheet targets those
16 * elements). We ONLY merge rules whose selector is globally unique, and we emit
17 * each merged group at the position of its first member — rules with a
18 * duplicated selector (which could carry cascade order) and @media blocks are
19 * left exactly in place. Runs on every generation (it produces byte-for-byte
20 * equivalent styling); disable via the `ablocks/perf/dedupe_css` filter.
21 */
22 class CssDedupe {
23
24 public static function process( $css ) {
25 $css = (string) $css;
26 if ( '' === trim( $css ) ) {
27 return $css;
28 }
29 return self::dedupe_scope( $css );
30 }
31
32 /**
33 * Dedupe one scope (the whole sheet, or the inside of an @media block).
34 * @media/at-rule segments are passed through (their inner rules are deduped
35 * recursively) and never merged with top-level rules.
36 */
37 private static function dedupe_scope( $css ) {
38 $segments = self::parse( $css );
39
40 // Frequency of each selector among mergeable (non-at) rules.
41 $freq = [];
42 foreach ( $segments as $s ) {
43 if ( 'rule' === $s['type'] ) {
44 $freq[ $s['sel'] ] = ( $freq[ $s['sel'] ] ?? 0 ) + 1;
45 }
46 }
47
48 // Group unique-selector rules by identical body; remember first position.
49 $groups = [];
50 foreach ( $segments as $i => $s ) {
51 if ( 'rule' === $s['type'] && 1 === $freq[ $s['sel'] ] && '' !== $s['body'] ) {
52 if ( ! isset( $groups[ $s['body'] ] ) ) {
53 $groups[ $s['body'] ] = [ 'sels' => [], 'first' => $i ];
54 }
55 $groups[ $s['body'] ]['sels'][] = $s['sel'];
56 }
57 }
58
59 $out = '';
60 foreach ( $segments as $i => $s ) {
61 if ( 'at' === $s['type'] ) {
62 $out .= self::dedupe_at_rule( $s['raw'] );
63 continue;
64 }
65 $mergeable = ( 1 === $freq[ $s['sel'] ] && '' !== $s['body'] );
66 if ( $mergeable ) {
67 $group = $groups[ $s['body'] ];
68 if ( $group['first'] === $i ) {
69 $out .= implode( ',', $group['sels'] ) . '{' . $s['body'] . '}';
70 }
71 // Non-first members are folded into the group above — skip.
72 } else {
73 $out .= $s['raw'];
74 }
75 }
76 return $out;
77 }
78
79 /**
80 * Recurse into an @media/@supports block's body; leave keyframes/font-face
81 * untouched (order/steps matter).
82 */
83 private static function dedupe_at_rule( $raw ) {
84 $open = strpos( $raw, '{' );
85 if ( false === $open ) {
86 return $raw;
87 }
88 $prelude = substr( $raw, 0, $open );
89 if ( 0 !== stripos( ltrim( $prelude ), '@media' ) && 0 !== stripos( ltrim( $prelude ), '@supports' ) ) {
90 return $raw; // @keyframes, @font-face, etc.
91 }
92 $inner = substr( $raw, $open + 1, strrpos( $raw, '}' ) - $open - 1 );
93 return $prelude . '{' . self::dedupe_scope( $inner ) . '}';
94 }
95
96 /**
97 * Parse CSS into ordered segments respecting brace depth. Each segment is
98 * either ['type'=>'rule','sel'=>..,'body'=>..,'raw'=>..] or
99 * ['type'=>'at','raw'=>..] for at-rules (prelude starts with '@').
100 */
101 private static function parse( $css ) {
102 $segments = [];
103 $len = strlen( $css );
104 $i = 0;
105 $buffer = '';
106
107 while ( $i < $len ) {
108 $char = $css[ $i ];
109 $buffer .= $char;
110
111 if ( '{' === $char ) {
112 $prelude = substr( $buffer, 0, -1 );
113 $depth = 1;
114 $i++;
115 while ( $i < $len && $depth > 0 ) {
116 $c = $css[ $i ];
117 $buffer .= $c;
118 if ( '{' === $c ) {
119 $depth++;
120 } elseif ( '}' === $c ) {
121 $depth--;
122 }
123 $i++;
124 }
125 $sel = trim( $prelude );
126 if ( '' !== $sel && '@' === $sel[0] ) {
127 $segments[] = [ 'type' => 'at', 'raw' => $buffer ];
128 } else {
129 $body = trim( substr( $buffer, strlen( $prelude ) + 1, -1 ) );
130 $segments[] = [ 'type' => 'rule', 'sel' => $sel, 'body' => $body, 'raw' => $buffer ];
131 }
132 $buffer = '';
133 continue;
134 }
135 $i++;
136 }
137 if ( '' !== trim( $buffer ) ) {
138 $segments[] = [ 'type' => 'at', 'raw' => $buffer ]; // stray text, pass through
139 }
140 return $segments;
141 }
142 }
143