PluginProbe ʕ •ᴥ•ʔ
FrontBlocks for Gutenberg/GeneratePress / trunk
FrontBlocks for Gutenberg/GeneratePress vtrunk
trunk 0.2.0 0.2.1 0.2.2 0.2.3 0.2.4 0.2.5 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.2.0 1.2.1 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 ci-artifacts
frontblocks / includes / Frontend / FluidTypography.php
frontblocks / includes / Frontend Last commit date
Animations.php 1 month ago BackButton.php 7 months ago BeforeAfter.php 1 month ago BlockPatterns.php 4 months ago Carousel.php 1 week ago ColumnsSameHeight.php 1 week ago ContainerEdgeAlignment.php 4 weeks ago Counter.php 1 month ago DownloadButton.php 1 week ago Events.php 4 weeks ago FaqSchema.php 1 week ago FluidTypography.php 4 weeks ago Gallery.php 8 months ago GravityFormsInline.php 1 month ago Headline.php 4 weeks ago InsertPost.php 1 month ago ProductCategories.php 4 weeks ago ReadingProgress.php 7 months ago ReadingTime.php 8 months ago ShapeAnimations.php 4 weeks ago StackedImages.php 4 months ago StickyColumn.php 4 weeks ago SvgUpload.php 4 weeks ago Testimonials.php 8 months ago TextAnimation.php 1 month ago UserText.php 4 weeks ago
FluidTypography.php
279 lines
1 <?php
2 /**
3 * Fluid Typography module for FrontBlocks.
4 *
5 * @package FrontBlocks
6 * @author David Perez <david@close.technology>
7 * @copyright 2025 Closemarketing
8 * @version 1.0
9 */
10
11 namespace FrontBlocks\Frontend;
12
13 defined( 'ABSPATH' ) || exit;
14
15 /**
16 * FluidTypography class.
17 *
18 * @since 1.0.0
19 */
20 class FluidTypography {
21
22 /**
23 * Constructor.
24 */
25 public function __construct() {
26 if ( ! $this->is_enabled() ) {
27 return;
28 }
29
30 $this->init_hooks();
31 }
32
33 /**
34 * Check if module is enabled.
35 *
36 * @return bool
37 */
38 private function is_enabled() {
39 $options = get_option( 'frontblocks_settings', array() );
40 return (bool) ( $options['enable_fluid_typography'] ?? true );
41 }
42
43 /**
44 * Initialize hooks.
45 *
46 * @return void
47 */
48 private function init_hooks() {
49 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_fluid_typography' ), 999 );
50 }
51
52 /**
53 * Enqueue fluid typography styles.
54 *
55 * @return void
56 */
57 public function enqueue_fluid_typography() {
58 $source_css = $this->get_source_css();
59
60 if ( empty( $source_css ) ) {
61 return;
62 }
63
64 $fluid_css = $this->convert_to_fluid_typography( $source_css );
65
66 if ( current_user_can( 'manage_options' ) && isset( $_GET['frbl_debug'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
67 add_action(
68 'wp_head',
69 function () use ( $fluid_css ) {
70 echo "\n<!-- FRBL Fluid Typography DEBUG:\n" . esc_html( $fluid_css ) . "\n-->\n";
71 }
72 );
73 }
74
75 if ( empty( $fluid_css ) ) {
76 return;
77 }
78
79 $style_handle = $this->get_style_handle();
80 wp_add_inline_style( $style_handle, $fluid_css );
81 }
82
83 /**
84 * Get CSS source depending on active theme.
85 * Priority: GeneratePress → theme.json global stylesheet → theme stylesheet.
86 *
87 * @return string
88 */
89 private function get_source_css(): string {
90 // 1. GeneratePress — use its pre-compiled dynamic CSS cache.
91 if ( function_exists( 'generate_get_default_color_palettes' ) ) {
92 $gp_css = get_option( 'generate_dynamic_css_output', '' );
93 if ( ! empty( $gp_css ) ) {
94 return $gp_css;
95 }
96 }
97
98 // 2. Block/FSE themes — compile from theme.json via WP core.
99 if ( function_exists( 'wp_get_global_stylesheet' ) ) {
100 $global_css = wp_get_global_stylesheet();
101 if ( ! empty( $global_css ) ) {
102 return $global_css;
103 }
104 }
105
106 // 3. Classic themes — read the theme's style.css.
107 $stylesheet = get_stylesheet_directory() . '/style.css';
108 if ( file_exists( $stylesheet ) ) {
109 $css = file_get_contents( $stylesheet ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
110 if ( ! empty( $css ) ) {
111 return $css;
112 }
113 }
114
115 return '';
116 }
117
118 /**
119 * Return the registered style handle to attach the fluid CSS to.
120 * Falls back to 'wp-block-library' (always enqueued) if the theme handle isn't found.
121 *
122 * @return string
123 */
124 private function get_style_handle(): string {
125 // GeneratePress.
126 if ( wp_style_is( 'generate-style', 'enqueued' ) ) {
127 return 'generate-style';
128 }
129
130 // Common theme handles.
131 $candidates = array( 'parent-style', 'child-style', 'theme-style', 'main-style', get_stylesheet() );
132 foreach ( $candidates as $handle ) {
133 if ( wp_style_is( $handle, 'enqueued' ) ) {
134 return $handle;
135 }
136 }
137
138 // Universal fallback — always present when blocks are used.
139 return 'wp-block-library';
140 }
141
142 /**
143 * Convert source CSS to fluid typography using clamp().
144 *
145 * @param string $css Source CSS.
146 * @return string Fluid typography CSS.
147 */
148 private function convert_to_fluid_typography( $css ) {
149 $fluid_css = '';
150 $selectors = array( 'body', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' );
151
152 $is_generateblocks = class_exists( 'GenerateBlocks' );
153
154 foreach ( $selectors as $selector ) {
155 $fluid_rule = $this->extract_and_convert_selector( $css, $selector, $is_generateblocks );
156
157 if ( ! empty( $fluid_rule ) ) {
158 $fluid_css .= $fluid_rule . "\n";
159 }
160 }
161
162 return $fluid_css;
163 }
164
165 /**
166 * Extract font sizes from CSS and convert to fluid typography.
167 *
168 * @param string $css Source CSS.
169 * @param string $selector CSS selector (e.g. 'h1').
170 * @param bool $is_generateblocks Whether GenerateBlocks is active.
171 * @return string Fluid CSS rule or empty string.
172 */
173 private function extract_and_convert_selector( $css, $selector, $is_generateblocks = false ) {
174 $sizes = array(
175 'desktop' => null,
176 'tablet' => null,
177 'mobile' => null,
178 );
179
180 $is_multiple_selector = ( 'body' === $selector );
181
182 // Desktop font-size.
183 if ( $is_multiple_selector ) {
184 $pattern = '/(?:^|,|\})\s*[^{]*\b' . preg_quote( $selector, '/' ) . '\b[^{]*\{[^}]*font-size:\s*([0-9.]+)(px|rem|em)/i';
185 } else {
186 $pattern = '/' . preg_quote( $selector, '/' ) . '\s*\{[^}]*font-size:\s*([0-9.]+)(px|rem|em)/i';
187 }
188
189 if ( preg_match( $pattern, $css, $matches ) ) {
190 $sizes['desktop'] = array(
191 'value' => floatval( $matches[1] ),
192 'unit' => $matches[2],
193 );
194 }
195
196 // Tablet font-size — @media (max-width: 1024px).
197 if ( $is_multiple_selector ) {
198 $pattern_tablet = '/@media[^{]*max-width:\s*1024px[^{]*\{[^@]*\b' . preg_quote( $selector, '/' ) . '\b[^{]*\{[^}]*font-size:\s*([0-9.]+)(px|rem|em)/is';
199 } else {
200 $pattern_tablet = '/@media[^{]*max-width:\s*1024px[^{]*\{[^}]*' . preg_quote( $selector, '/' ) . '\s*\{[^}]*font-size:\s*([0-9.]+)(px|rem|em)/is';
201 }
202
203 if ( preg_match( $pattern_tablet, $css, $matches ) ) {
204 $sizes['tablet'] = array(
205 'value' => floatval( $matches[1] ),
206 'unit' => $matches[2],
207 );
208 }
209
210 // Mobile font-size — @media (max-width: 768px).
211 if ( $is_multiple_selector ) {
212 $pattern_mobile = '/@media[^{]*max-width:\s*768px[^{]*\{[^@]*\b' . preg_quote( $selector, '/' ) . '\b[^{]*\{[^}]*font-size:\s*([0-9.]+)(px|rem|em)/is';
213 } else {
214 $pattern_mobile = '/@media[^{]*max-width:\s*768px[^{]*\{[^}]*' . preg_quote( $selector, '/' ) . '\s*\{[^}]*font-size:\s*([0-9.]+)(px|rem|em)/is';
215 }
216
217 if ( preg_match( $pattern_mobile, $css, $matches ) ) {
218 $sizes['mobile'] = array(
219 'value' => floatval( $matches[1] ),
220 'unit' => $matches[2],
221 );
222 }
223
224 if ( ! $sizes['desktop'] || ! $sizes['mobile'] ) {
225 return '';
226 }
227
228 if ( $sizes['desktop']['unit'] !== $sizes['mobile']['unit'] ) {
229 return '';
230 }
231
232 $min_size = $sizes['mobile']['value'];
233 $max_size = $sizes['desktop']['value'];
234 $unit = $sizes['desktop']['unit'];
235
236 if ( $min_size === $max_size ) {
237 return '';
238 }
239
240 $viewport_start = 320;
241 $viewport_end = 1440;
242 $viewport_diff = $viewport_end - $viewport_start;
243
244 $fluid_calc = sprintf(
245 'calc(%1$s%2$s + (%3$s - %1$s) * ((100vw - %4$spx) / %5$s))',
246 $min_size,
247 $unit,
248 $max_size,
249 $viewport_start,
250 $viewport_diff
251 );
252
253 $clamp_rule = sprintf(
254 'clamp(%1$s%2$s, %3$s, %4$s%2$s)',
255 $min_size,
256 $unit,
257 $fluid_calc,
258 $max_size
259 );
260
261 if ( in_array( $selector, array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ), true ) ) {
262 $rule = sprintf( "body %s {\n\tfont-size: %s !important;\n}", $selector, $clamp_rule );
263
264 // Add GenerateBlocks-specific selector only when GB is active.
265 if ( $is_generateblocks ) {
266 $rule .= sprintf( "\nbody %s.gb-headline {\n\tfont-size: %s !important;\n}", $selector, $clamp_rule );
267 }
268 } else {
269 $rule = sprintf( "%s {\n\tfont-size: %s !important;\n}", $selector, $clamp_rule );
270
271 if ( 'body' === $selector && $is_generateblocks ) {
272 $rule .= sprintf( "\np.gb-headline-text {\n\tfont-size: %s !important;\n}", $clamp_rule );
273 }
274 }
275
276 return $rule;
277 }
278 }
279