PluginProbe ʕ •ᴥ•ʔ
FrontBlocks for Gutenberg/GeneratePress / ci-artifacts
FrontBlocks for Gutenberg/GeneratePress vci-artifacts
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 month ago ContainerEdgeAlignment.php 1 month ago Counter.php 1 month ago Events.php 7 months ago FaqSchema.php 1 month ago FluidTypography.php 4 months ago Gallery.php 8 months ago GravityFormsInline.php 1 month ago Headline.php 1 month ago InsertPost.php 1 month ago ProductCategories.php 8 months ago ReadingProgress.php 7 months ago ReadingTime.php 8 months ago ShapeAnimations.php 1 month ago StackedImages.php 4 months ago StickyColumn.php 1 month ago Testimonials.php 8 months ago TextAnimation.php 1 month ago
FluidTypography.php
235 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 // Check if module is enabled.
27 if ( ! $this->is_enabled() ) {
28 return;
29 }
30
31 $this->init_hooks();
32 }
33
34 /**
35 * Check if module is enabled.
36 *
37 * @return bool
38 */
39 private function is_enabled() {
40 $options = get_option( 'frontblocks_settings', array() );
41 return ! empty( $options['enable_fluid_typography'] );
42 }
43
44 /**
45 * Initialize hooks.
46 *
47 * @return void
48 */
49 private function init_hooks() {
50 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_fluid_typography' ), 999 );
51 }
52
53 /**
54 * Enqueue fluid typography styles.
55 *
56 * @return void
57 */
58 public function enqueue_fluid_typography() {
59 // Get GeneratePress dynamic CSS output.
60 $dynamic_css = get_option( 'generate_dynamic_css_output', '' );
61
62 if ( empty( $dynamic_css ) ) {
63 return;
64 }
65
66 // Generate fluid typography CSS from dynamic CSS.
67 $fluid_css = $this->convert_to_fluid_typography( $dynamic_css );
68
69 // DEBUG: Show generated CSS as HTML comment for admins.
70 if ( current_user_can( 'manage_options' ) && isset( $_GET['frbl_debug'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
71 add_action(
72 'wp_head',
73 function () use ( $fluid_css ) {
74 echo "\n<!-- FRBL Fluid Typography DEBUG:\n" . esc_html( $fluid_css ) . "\n-->\n";
75 }
76 );
77 }
78
79 if ( ! empty( $fluid_css ) ) {
80 wp_add_inline_style( 'generate-style', $fluid_css );
81 }
82 }
83
84 /**
85 * Convert GeneratePress static CSS to fluid typography.
86 *
87 * @param string $css Dynamic CSS from GeneratePress.
88 * @return string Fluid typography CSS.
89 */
90 private function convert_to_fluid_typography( $css ) {
91 $fluid_css = '';
92
93 // Typography selectors to process - starting with simple ones that definitely work.
94 $selectors = array(
95 'body', // Body text (paragraphs inherit from this).
96 'h1', // Heading 1.
97 'h2', // Heading 2.
98 'h3', // Heading 3.
99 'h4', // Heading 4.
100 'h5', // Heading 5.
101 'h6', // Heading 6.
102 );
103
104 foreach ( $selectors as $selector ) {
105 $fluid_rule = $this->extract_and_convert_selector( $css, $selector );
106
107 if ( ! empty( $fluid_rule ) ) {
108 $fluid_css .= $fluid_rule . "\n";
109 }
110 }
111
112 return $fluid_css;
113 }
114
115 /**
116 * Extract font sizes from CSS and convert to fluid typography.
117 *
118 * @param string $css CSS content.
119 * @param string $selector CSS selector (e.g., 'h1').
120 * @return string Fluid CSS rule or empty string.
121 */
122 private function extract_and_convert_selector( $css, $selector ) {
123 $sizes = array(
124 'desktop' => null,
125 'tablet' => null,
126 'mobile' => null,
127 );
128
129 // Use different regex patterns depending on the selector.
130 // Body is in a multiple selector (body, button, input...), headings are standalone.
131 $is_multiple_selector = ( 'body' === $selector );
132
133 // Extract base/desktop font-size.
134 if ( $is_multiple_selector ) {
135 $pattern = '/(?:^|,|\})\s*[^{]*\b' . preg_quote( $selector, '/' ) . '\b[^{]*\{[^}]*font-size:\s*([0-9.]+)(px|rem|em)/i';
136 } else {
137 $pattern = '/' . preg_quote( $selector, '/' ) . '\s*\{[^}]*font-size:\s*([0-9.]+)(px|rem|em)/i';
138 }
139
140 if ( preg_match( $pattern, $css, $matches ) ) {
141 $sizes['desktop'] = array(
142 'value' => floatval( $matches[1] ),
143 'unit' => $matches[2],
144 );
145 }
146
147 // Extract tablet font-size from @media (max-width: 1024px).
148 if ( $is_multiple_selector ) {
149 // Pattern for body in media query.
150 $pattern_tablet = '/@media[^{]*max-width:\s*1024px[^{]*\{[^@]*\b' . preg_quote( $selector, '/' ) . '\b[^{]*\{[^}]*font-size:\s*([0-9.]+)(px|rem|em)/is';
151 } else {
152 $pattern_tablet = '/@media[^{]*max-width:\s*1024px[^{]*\{[^}]*' . preg_quote( $selector, '/' ) . '\s*\{[^}]*font-size:\s*([0-9.]+)(px|rem|em)/is';
153 }
154
155 if ( preg_match( $pattern_tablet, $css, $matches ) ) {
156 $sizes['tablet'] = array(
157 'value' => floatval( $matches[1] ),
158 'unit' => $matches[2],
159 );
160 }
161
162 // Extract mobile font-size from @media (max-width: 768px).
163 if ( $is_multiple_selector ) {
164 // Pattern for body in media query.
165 $pattern_mobile = '/@media[^{]*max-width:\s*768px[^{]*\{[^@]*\b' . preg_quote( $selector, '/' ) . '\b[^{]*\{[^}]*font-size:\s*([0-9.]+)(px|rem|em)/is';
166 } else {
167 $pattern_mobile = '/@media[^{]*max-width:\s*768px[^{]*\{[^}]*' . preg_quote( $selector, '/' ) . '\s*\{[^}]*font-size:\s*([0-9.]+)(px|rem|em)/is';
168 }
169
170 if ( preg_match( $pattern_mobile, $css, $matches ) ) {
171 $sizes['mobile'] = array(
172 'value' => floatval( $matches[1] ),
173 'unit' => $matches[2],
174 );
175 }
176
177 // If we don't have enough data, skip.
178 if ( ! $sizes['desktop'] || ! $sizes['mobile'] ) {
179 return '';
180 }
181
182 // Ensure all units are the same.
183 if ( $sizes['desktop']['unit'] !== $sizes['mobile']['unit'] ) {
184 return '';
185 }
186
187 $min_size = $sizes['mobile']['value'];
188 $max_size = $sizes['desktop']['value'];
189 $unit = $sizes['desktop']['unit'];
190
191 // Skip if same values.
192 if ( $min_size === $max_size ) {
193 return '';
194 }
195
196 // Generate fluid typography with clamp().
197 // Viewport range: 320px (mobile) to 1440px (desktop).
198 $viewport_start = 320;
199 $viewport_end = 1440;
200 $viewport_diff = $viewport_end - $viewport_start;
201
202 // Build clamp() formula.
203 $fluid_calc = sprintf(
204 'calc(%1$s%2$s + (%3$s - %1$s) * ((100vw - %4$spx) / %5$s))',
205 $min_size,
206 $unit,
207 $max_size,
208 $viewport_start,
209 $viewport_diff
210 );
211
212 $clamp_rule = sprintf(
213 'clamp(%1$s%2$s, %3$s, %4$s%2$s)',
214 $min_size,
215 $unit,
216 $fluid_calc,
217 $max_size
218 );
219
220 // Generate CSS rule with appropriate specificity.
221 if ( in_array( $selector, array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ), true ) ) {
222 // For headings, use high specificity to ensure they always win over body classes.
223 $rule = sprintf( "body %s,\nbody %s.gb-headline {\n\tfont-size: %s !important;\n}", $selector, $selector, $clamp_rule );
224 } else {
225 // For body, apply to body and paragraphs with GB classes.
226 $rule = sprintf( "%s {\n\tfont-size: %s !important;\n}", $selector, $clamp_rule );
227 if ( 'body' === $selector ) {
228 $rule .= sprintf( "\np.gb-headline-text {\n\tfont-size: %s !important;\n}", $clamp_rule );
229 }
230 }
231
232 return $rule;
233 }
234 }
235