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 / font-collector.php

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

354 lines 10.6 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 use ABlocks\Helper;
9
10 /**
11 * Determines which fonts a page actually uses by reading saved block
12 * attributes (deterministic, design-time truth) instead of discovering them
13 * by scanning generated CSS at render time.
14 *
15 * - Per-post fonts are stored in post meta and refreshed on save.
16 * - Global typography fonts are stored in an option and refreshed when the
17 * plugin's global settings are saved.
18 * - Self-hosting (local download) is the default loading model.
19 *
20 * See docs/FONT-MANAGEMENT-PLAN.md.
21 */
22 class FontCollector {
23
24 const POST_META_KEY = '_ablocks_fonts';
25 const GLOBAL_OPTION_NAME = 'ablocks_global_fonts';
26 const SITE_OPTION_NAME = 'ablocks_site_fonts';
27
28 /**
29 * Recursively collect [ family => [weights] ] from a list of parsed blocks.
30 *
31 * @param array $blocks Parsed blocks (from parse_blocks()).
32 * @param array $fonts Accumulator.
33 * @return array
34 */
35 public static function collect_from_blocks( $blocks, &$fonts = [] ) {
36 if ( ! is_array( $blocks ) ) {
37 return $fonts;
38 }
39
40 foreach ( $blocks as $block ) {
41 if ( empty( $block['blockName'] ) ) {
42 continue;
43 }
44
45 // Reusable blocks / synced patterns referenced by id.
46 if ( 'core/block' === $block['blockName'] && ! empty( $block['attrs']['ref'] ) ) {
47 $ref_post = get_post( (int) $block['attrs']['ref'] );
48 if ( $ref_post instanceof \WP_Post ) {
49 self::collect_from_blocks( parse_blocks( $ref_post->post_content ), $fonts );
50 }
51 }
52
53 if ( ! empty( $block['attrs'] ) ) {
54 self::collect_from_attributes( $block['attrs'], $fonts );
55 }
56
57 if ( ! empty( $block['innerBlocks'] ) ) {
58 self::collect_from_blocks( $block['innerBlocks'], $fonts );
59 }
60 }
61
62 return $fonts;
63 }
64
65 /**
66 * Walk an attribute tree and record any typography object that carries a
67 * non-empty fontFamily. Attribute-shape driven, so it works for every block
68 * without a per-block schema (typography attrs are objects with fontFamily).
69 *
70 * @param mixed $node Attributes (array) or sub-node.
71 * @param array $fonts Accumulator.
72 */
73 protected static function collect_from_attributes( $node, &$fonts ) {
74 if ( ! is_array( $node ) ) {
75 return;
76 }
77
78 if ( isset( $node['fontFamily'] ) && is_string( $node['fontFamily'] ) ) {
79 $family = trim( $node['fontFamily'] );
80 // 'Default' sets no font and 'inherit' defers to the theme; neither is
81 // a downloadable family. A stored stack came from a custom value, so
82 // its files are the author's responsibility.
83 if ( '' !== $family && 'Default' !== $family && 'inherit' !== strtolower( $family ) && false === strpos( $family, ',' ) ) {
84 $weight = '400';
85 if ( isset( $node['weight'] ) && '' !== $node['weight'] && 'Default' !== $node['weight'] ) {
86 $weight = (string) $node['weight'];
87 }
88 self::add( $fonts, $family, $weight );
89 }
90 }
91
92 foreach ( $node as $value ) {
93 if ( is_array( $value ) ) {
94 self::collect_from_attributes( $value, $fonts );
95 }
96 }
97 }
98
99 /**
100 * Add a family/weight pair to the accumulator without duplicates.
101 */
102 protected static function add( &$fonts, $family, $weight ) {
103 if ( ! isset( $fonts[ $family ] ) ) {
104 $fonts[ $family ] = [];
105 }
106 if ( ! in_array( $weight, $fonts[ $family ], true ) ) {
107 $fonts[ $family ][] = $weight;
108 }
109 }
110
111 /**
112 * Union two [family => weights] maps.
113 */
114 public static function merge( $a, $b ) {
115 $a = is_array( $a ) ? $a : [];
116 $b = is_array( $b ) ? $b : [];
117 foreach ( $b as $family => $weights ) {
118 foreach ( (array) $weights as $weight ) {
119 self::add( $a, $family, (string) $weight );
120 }
121 }
122 return $a;
123 }
124
125 /* -------------------------------------------------------------------------
126 * Per-post
127 * ---------------------------------------------------------------------- */
128
129 /**
130 * Collect fonts directly from a post's saved content.
131 */
132 public static function collect_from_post( $post_id ) {
133 $post = get_post( $post_id );
134 if ( ! $post instanceof \WP_Post ) {
135 return [];
136 }
137 return self::collect_from_blocks( parse_blocks( $post->post_content ) );
138 }
139
140 /**
141 * Recompute and store a post's fonts, then self-host them. Called on save.
142 */
143 public static function save_post_fonts( $post_id ) {
144 $fonts = self::collect_from_post( $post_id );
145 update_post_meta( $post_id, self::POST_META_KEY, $fonts );
146 self::download( $fonts );
147 return $fonts;
148 }
149
150 /**
151 * Get a post's fonts, computing + caching on first access (lazy backfill for
152 * content saved before this system existed).
153 */
154 public static function get_post_fonts( $post_id ) {
155 $fonts = get_post_meta( $post_id, self::POST_META_KEY, true );
156 if ( ! is_array( $fonts ) ) {
157 $fonts = self::save_post_fonts( $post_id );
158 }
159 return is_array( $fonts ) ? $fonts : [];
160 }
161
162 /* -------------------------------------------------------------------------
163 * Global typography
164 * ---------------------------------------------------------------------- */
165
166 /**
167 * Collect global typography fonts from the plugin's global settings.
168 */
169 public static function collect_from_globals() {
170 $fonts = [];
171 $keys = [
172 'global_typography',
173 'global_body_typography',
174 'global_link_typography',
175 'global_link_hover_typography',
176 'global_h1_typography',
177 'global_h2_typography',
178 'global_h3_typography',
179 'global_h4_typography',
180 'global_h5_typography',
181 'global_h6_typography',
182 ];
183 foreach ( $keys as $key ) {
184 $setting = Helper::get_settings( $key, [] );
185 // Normalise stdClass → array so the recursive walk works uniformly.
186 $normalised = json_decode( wp_json_encode( $setting ), true );
187 self::collect_from_attributes( $normalised, $fonts );
188 }
189 return $fonts;
190 }
191
192 /**
193 * Recompute and store global fonts, then self-host. Called on settings save.
194 */
195 public static function update_global_fonts() {
196 $fonts = self::collect_from_globals();
197 update_option( self::GLOBAL_OPTION_NAME, $fonts );
198 self::download( $fonts );
199 return $fonts;
200 }
201
202 /**
203 * Get global fonts, computing + caching on first access.
204 */
205 public static function get_global_fonts() {
206 $fonts = get_option( self::GLOBAL_OPTION_NAME, null );
207 if ( ! is_array( $fonts ) ) {
208 $fonts = self::update_global_fonts();
209 }
210 return is_array( $fonts ) ? $fonts : [];
211 }
212
213 /* -------------------------------------------------------------------------
214 * Site-wide content (templates, template parts, theme builder layouts)
215 * ---------------------------------------------------------------------- */
216
217 /**
218 * Post types whose content is rendered outside the queried post: block theme
219 * templates and template parts (header/footer/archive markup) and aBlocks
220 * theme builder layouts.
221 *
222 * These are not `is_singular()` on the front end, so their fonts were never
223 * requested — a header built with aBlocks got `font-family: X` in CSS with no
224 * font-face rule and no Google stylesheet behind it.
225 *
226 * @return string[]
227 */
228 public static function get_site_font_post_types() {
229 return (array) apply_filters(
230 'ablocks/site_font_post_types',
231 [ 'wp_template', 'wp_template_part', 'ablocks_tb' ]
232 );
233 }
234
235 /**
236 * Collect fonts from every template / template part / theme builder layout.
237 *
238 * Which template WordPress will render is not known while `wp_head` runs, so
239 * the union of them is used. The result is cached in an option and only
240 * recomputed when one of those posts is saved, so the front end pays a single
241 * option read. Self-hosted faces are lazy — declaring one the page does not
242 * use costs a line of CSS, not a download.
243 */
244 public static function collect_from_site_content() {
245 $fonts = [];
246
247 $post_ids = get_posts(
248 [
249 'post_type' => self::get_site_font_post_types(),
250 'post_status' => [ 'publish', 'draft', 'auto-draft', 'inherit' ],
251 'posts_per_page' => 200,
252 'fields' => 'ids',
253 'no_found_rows' => true,
254 'update_post_meta_cache' => false,
255 'update_post_term_cache' => false,
256 'suppress_filters' => true,
257 ]
258 );
259
260 foreach ( $post_ids as $post_id ) {
261 $fonts = self::merge( $fonts, self::collect_from_post( $post_id ) );
262 }
263
264 return $fonts;
265 }
266
267 /**
268 * Recompute and store the site-wide font set, then self-host it.
269 */
270 public static function update_site_fonts() {
271 $fonts = self::collect_from_site_content();
272 update_option( self::SITE_OPTION_NAME, $fonts );
273 self::download( $fonts );
274 return $fonts;
275 }
276
277 /**
278 * Get the site-wide font set, computing + caching on first access.
279 *
280 * Self-hosting downloads font files over the network, which must not happen
281 * inside a visitor's request. On the front end the set is collected and cached
282 * immediately (so the fonts do load, remotely) and the download is handed to a
283 * one-off cron event.
284 */
285 public static function get_site_fonts() {
286 $fonts = get_option( self::SITE_OPTION_NAME, null );
287 if ( is_array( $fonts ) ) {
288 return $fonts;
289 }
290
291 if ( is_admin() || ( defined( 'WP_CLI' ) && WP_CLI ) ) {
292 return self::update_site_fonts();
293 }
294
295 $fonts = self::collect_from_site_content();
296 update_option( self::SITE_OPTION_NAME, $fonts );
297
298 if ( ! empty( $fonts ) && ! wp_next_scheduled( 'ablocks/download_site_fonts' ) ) {
299 wp_schedule_single_event( time() + 30, 'ablocks/download_site_fonts' );
300 }
301
302 return $fonts;
303 }
304
305 /**
306 * Drop the cached site-wide font set (a template/part/layout changed).
307 */
308 public static function flush_site_fonts() {
309 delete_option( self::SITE_OPTION_NAME );
310 }
311
312 /* -------------------------------------------------------------------------
313 * Frontend
314 * ---------------------------------------------------------------------- */
315
316 /**
317 * The complete set of fonts the current request needs: global typography and
318 * site-wide content (templates, parts, theme builder layouts) on every page,
319 * plus the queried post's own fonts. Cached per request.
320 */
321 public static function get_page_fonts() {
322 static $cache = null;
323 if ( null !== $cache ) {
324 return $cache;
325 }
326
327 $fonts = self::merge( self::get_global_fonts(), self::get_site_fonts() );
328
329 if ( is_singular() ) {
330 $post_id = get_queried_object_id();
331 if ( $post_id ) {
332 $fonts = self::merge( $fonts, self::get_post_fonts( $post_id ) );
333 }
334 }
335
336 $cache = (array) apply_filters( 'ablocks/page_fonts', $fonts );
337 return $cache;
338 }
339
340 /* -------------------------------------------------------------------------
341 * Self-hosting
342 * ---------------------------------------------------------------------- */
343
344 /**
345 * Download any missing font files locally (self-host is the default model).
346 */
347 protected static function download( $fonts ) {
348 if ( empty( $fonts ) ) {
349 return;
350 }
351 ( new FontLoadLocally() )->process_font_queue( $fonts );
352 }
353 }
354