PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.11.0
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.11.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 1.1.1 1.1.2 All 80 releases
ablocks / includes / classes / scanner / design-repeats.php

design-repeats.php in aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder 2.11.0, at includes/classes/scanner/design-repeats.php

260 lines 6.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace ABlocks\Classes\Scanner;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 use ABlocks\Classes\DesignAudit;
9 use ABlocks\Helper;
10
11 /**
12 * Find design values typed in over and over instead of being made a global.
13 *
14 * {@see DesignAudit} answers "which blocks carry a hand-picked value?" — useful
15 * for the design-system lock, which only cares whether a violation exists. This
16 * answers a different question: *which value*, and *how often*.
17 *
18 * That difference is the whole point. One block with a custom colour is a
19 * choice. The same hex typed into thirty blocks is an unnamed design token: it
20 * behaves like a brand colour, it just cannot be changed in one place. Finding
21 * those is what turns "you have custom colours" into "#3858e9 appears 30 times
22 * and is not one of your presets — make it one".
23 *
24 * A value is only counted when the block has no matching `<name>Global`
25 * sibling, so anything already wired to a preset is invisible here.
26 */
27 class DesignRepeats {
28
29 /**
30 * Times a value must appear before it is worth naming.
31 *
32 * Below this it is genuinely a one-off; above it, changing your mind means
33 * editing every occurrence by hand.
34 */
35 const MIN_REPEATS = 3;
36
37 /**
38 * Posts examined. Bounded because this parses block content.
39 */
40 const MAX_POSTS = 300;
41
42 /**
43 * Scan site content for repeated colours and typography.
44 *
45 * @return array{colors:array, fonts:array, posts:int}
46 */
47 public static function scan() {
48 $post_ids = get_posts(
49 [
50 'post_type' => array_merge(
51 [ 'post', 'page' ],
52 \ABlocks\Classes\FontCollector::get_site_font_post_types()
53 ),
54 'post_status' => [ 'publish', 'draft', 'pending', 'private' ],
55 'posts_per_page' => self::MAX_POSTS,
56 'fields' => 'ids',
57 'no_found_rows' => true,
58 'update_post_meta_cache' => false,
59 'update_post_term_cache' => false,
60 ]
61 );
62
63 $colors = [];
64 $fonts = [];
65 $seen = 0;
66
67 foreach ( $post_ids as $post_id ) {
68 $post = get_post( $post_id );
69 if ( ! $post instanceof \WP_Post || false === strpos( (string) $post->post_content, 'wp:ablocks' ) ) {
70 continue;
71 }
72
73 $seen++;
74 self::walk( parse_blocks( $post->post_content ), $colors, $fonts );
75 }
76
77 return [
78 'colors' => self::rank( $colors ),
79 'fonts' => self::rank( $fonts ),
80 'posts' => $seen,
81 ];
82 }
83
84 /**
85 * Walk parsed blocks, tallying literal values.
86 *
87 * @param array $blocks Parsed blocks.
88 * @param array $colors Colour tally, by reference.
89 * @param array $fonts Font tally, by reference.
90 */
91 private static function walk( $blocks, &$colors, &$fonts ) {
92 if ( ! is_array( $blocks ) ) {
93 return;
94 }
95
96 foreach ( $blocks as $block ) {
97 if ( empty( $block['blockName'] ) ) {
98 continue;
99 }
100
101 if ( 0 === strpos( $block['blockName'], 'ablocks/' ) && ! empty( $block['attrs'] ) ) {
102 self::tally( $block['attrs'], $colors, $fonts );
103 }
104
105 if ( ! empty( $block['innerBlocks'] ) ) {
106 self::walk( $block['innerBlocks'], $colors, $fonts );
107 }
108 }
109 }
110
111 /**
112 * Tally one block's attributes.
113 *
114 * @param array $attrs Block attributes.
115 * @param array $colors Colour tally, by reference.
116 * @param array $fonts Font tally, by reference.
117 */
118 private static function tally( $attrs, &$colors, &$fonts ) {
119 foreach ( $attrs as $name => $value ) {
120 // A global reference is the thing we want people to have, not a finding.
121 if ( self::ends_with( $name, 'Global' ) ) {
122 continue;
123 }
124 if ( ! empty( $attrs[ $name . 'Global' ] ) ) {
125 continue;
126 }
127
128 if ( DesignAudit::is_typography( $value ) ) {
129 $key = self::font_key( $value );
130 if ( $key ) {
131 $fonts[ $key ] = isset( $fonts[ $key ] ) ? $fonts[ $key ] + 1 : 1;
132 }
133 continue;
134 }
135
136 if ( DesignAudit::is_custom_color( $value ) ) {
137 $key = self::color_key( $value );
138 if ( $key ) {
139 $colors[ $key ] = isset( $colors[ $key ] ) ? $colors[ $key ] + 1 : 1;
140 }
141 }
142 }//end foreach
143 }
144
145 /**
146 * Normalise a colour so the same colour written differently counts once.
147 *
148 * `#FFF`, `#ffffff` and `#FFFFFF` are one colour to a designer and three
149 * strings to a computer; counting them separately would hide exactly the
150 * repetition this is looking for.
151 *
152 * @param mixed $value Attribute value.
153 * @return string
154 */
155 private static function color_key( $value ) {
156 if ( ! is_string( $value ) ) {
157 return '';
158 }
159
160 $value = strtolower( trim( $value ) );
161
162 if ( preg_match( '/^#([0-9a-f]{3})$/', $value, $m ) ) {
163 $value = '#' . $m[1][0] . $m[1][0] . $m[1][1] . $m[1][1] . $m[1][2] . $m[1][2];
164 }
165
166 // Whitespace inside rgb()/rgba() is meaningless, so remove it.
167 $value = preg_replace( '/\s+/', '', $value );
168
169 return $value;
170 }
171
172 /**
173 * Describe a typography object by the parts a person would recognise.
174 *
175 * Deliberately ignores units and responsive variants: two headings differing
176 * only in tablet line-height are the same decision, and splitting them would
177 * bury the repeat.
178 *
179 * @param array $value Typography attribute.
180 * @return string
181 */
182 private static function font_key( $value ) {
183 $family = isset( $value['fontFamily'] ) ? trim( (string) $value['fontFamily'] ) : '';
184 $size = isset( $value['fontSize'] ) ? trim( (string) $value['fontSize'] ) : '';
185 $weight = isset( $value['weight'] ) ? trim( (string) $value['weight'] ) : '';
186
187 if ( 'Default' === $family ) {
188 $family = '';
189 }
190
191 $parts = array_filter( [ $family, $size ? $size . 'px' : '', $weight ] );
192
193 return empty( $parts ) ? '' : implode( ' · ', $parts );
194 }
195
196 /**
197 * Keep values seen often enough to be worth naming, biggest first.
198 *
199 * @param array $tally value => count.
200 * @return array
201 */
202 private static function rank( $tally ) {
203 $min = (int) apply_filters( 'ablocks/scanner/min_repeats', self::MIN_REPEATS );
204
205 $tally = array_filter(
206 $tally,
207 function ( $count ) use ( $min ) {
208 return $count >= $min;
209 }
210 );
211
212 arsort( $tally );
213
214 $out = [];
215 foreach ( array_slice( $tally, 0, 10, true ) as $value => $count ) {
216 $out[] = [
217 'value' => (string) $value,
218 'count' => (int) $count,
219 ];
220 }
221
222 return $out;
223 }
224
225 /**
226 * Colours already defined as global presets, normalised for comparison.
227 *
228 * @return string[]
229 */
230 public static function global_colors() {
231 $presets = Helper::get_settings( 'global_color', [] );
232 $out = [];
233
234 foreach ( (array) $presets as $preset ) {
235 $value = is_array( $preset ) && isset( $preset['value'] ) ? $preset['value'] : null;
236 if ( ! is_string( $value ) ) {
237 continue;
238 }
239 $key = self::color_key( $value );
240 if ( $key ) {
241 $out[] = $key;
242 }
243 }
244
245 return $out;
246 }
247
248 /**
249 * Does a string end with a suffix?
250 *
251 * @param string $haystack Subject.
252 * @param string $needle Suffix.
253 * @return bool
254 */
255 private static function ends_with( $haystack, $needle ) {
256 $length = strlen( $needle );
257 return 0 !== $length && substr( $haystack, -$length ) === $needle;
258 }
259 }
260