PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.5.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.5.5
4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
betterdocs / includes / Modules / StyleHandler.php

StyleHandler.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.5.5, at includes/Modules/StyleHandler.php

426 lines 18.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace WPDeveloper\BetterDocs\Modules;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8
9
10 // Doc/category-listing primitives (meta_query/tax_query, post__not_in/exclude)
11 // are intrinsic to BetterDocs' KB / category / FAQ filters and intentional.
12 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
13 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
14 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_tax_query
15 // phpcs:disable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
16 // phpcs:disable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
17 use WPDeveloper\BetterDocs\Utils\CSSParser;
18
19 final class StyleHandler {
20 private static $instance;
21
22 private $prefix = 'betterdocs-style';
23 private $style_dir;
24 private $style_url;
25 /**
26 * Holds block styles array
27 *
28 * @var array
29 */
30 public static $_block_styles = array( );
31
32 /**
33 * Post IDs of FSE templates / template parts whose CSS was generated
34 * during the current request. Used to enqueue the matching per-template
35 * CSS files on the frontend.
36 *
37 * @var int[]
38 */
39 public static $_fse_template_post_ids = array( );
40
41 public static function init() {
42 if ( null === self::$instance ) {
43 self::$instance = new self();
44 }
45
46 return self::$instance;
47 }
48
49 public function __construct() {
50 $upload_dir = wp_upload_dir();
51
52 $this->style_dir = $upload_dir[ 'basedir' ] . '/' . $this->prefix . DIRECTORY_SEPARATOR;
53 $this->style_url = set_url_scheme( $upload_dir[ 'baseurl' ] ) . '/' . $this->prefix . '/';
54
55 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ), 99 );
56 add_action( 'save_post', array( $this, 'on_save_post' ), 10, 3 );
57 add_action( 'wp', array( $this, 'generate_post_content' ) );
58 add_action( 'rest_after_save_widget', array( $this, 'after_save_widget' ), 10, 4 );
59
60 // FSE assets generation
61 add_action(
62 'init',
63 function () {
64 if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
65 add_filter( '404_template', array( $this, 'fse_assets_generation' ), 99, 3 );
66 add_filter( 'archive_template', array( $this, 'fse_assets_generation' ), 99, 3 );
67 add_filter( 'category_template', array( $this, 'fse_assets_generation' ), 99, 3 );
68 add_filter( 'frontpage_template', array( $this, 'fse_assets_generation' ), 99, 3 );
69 add_filter( 'home_template', array( $this, 'fse_assets_generation' ), 99, 3 );
70 add_filter( 'index_template', array( $this, 'fse_assets_generation' ), 99, 3 );
71 add_filter( 'page_template', array( $this, 'fse_assets_generation' ), 99, 3 );
72 add_filter( 'search_template', array( $this, 'fse_assets_generation' ), 99, 3 );
73 add_filter( 'single_template', array( $this, 'fse_assets_generation' ), 99, 3 );
74 add_filter( 'singular_template', array( $this, 'fse_assets_generation' ), 99, 3 );
75 add_filter( 'tag_template', array( $this, 'fse_assets_generation' ), 99, 3 );
76 add_filter( 'taxonomy_template', array( $this, 'fse_assets_generation' ), 99, 3 );
77 }
78 },
79 999
80 );
81 }
82
83 /**
84 * Generate FSE Assets
85 */
86 public function fse_assets_generation( $template, $type, $templates ) {
87 $block_template = resolve_block_template( $type, $templates, $template );
88 if ( ! empty( $block_template ) ) {
89 $parsed_content = parse_blocks( $block_template->content );
90 if ( is_array( $parsed_content ) && ! empty( $parsed_content ) ) {
91 foreach ( $parsed_content as $content ) {
92 if ( ( 'core/template-part' == $content[ 'blockName' ] ) || ( 'core/template' == $content[ 'blockName' ] ) ) {
93 $post_ids = isset( $content[ 'attrs' ][ 'slug' ] ) ? self::betterdocs_get_post_content_by_post_name( $content[ 'attrs' ][ 'slug' ] ) : array( );
94
95 if ( ! empty( $post_ids ) ) {
96 foreach ( $post_ids as $id ) {
97 $post_id = (int) $id[ 'ID' ];
98 self::$_fse_template_post_ids[ ] = $post_id;
99 $post = get_post( $post_id );
100 $parsed_content = parse_blocks( $post->post_content );
101 $this->write_css_from_content( $post, $post_id, $parsed_content );
102 }
103 }
104 } else {
105 $post_ids = self::betterdocs_get_post_content_by_post_name( $block_template->slug );
106 if ( ! empty( $post_ids ) ) {
107 foreach ( $post_ids as $id ) {
108 $post_id = (int) $id[ 'ID' ];
109 self::$_fse_template_post_ids[ ] = $post_id;
110 $post = get_post( $post_id );
111 $parsed_content = parse_blocks( $post->post_content );
112 $this->write_css_from_content( $post, $post_id, $parsed_content );
113 }
114 }
115 }
116 }
117 }
118 }
119
120 return $template;
121 }
122
123 /**
124 * Write CSS
125 */
126 public function write_css_from_content( $post, $post_id, $parsed_content ) {
127 $betterdocs_blocks = array( );
128 $recursive_response = CSSParser::betterdocs_block_style_recursive( $parsed_content, $betterdocs_blocks );
129 $reusable_Blocks = ! empty( $recursive_response[ 'reusableBlocks' ] ) ? $recursive_response[ 'reusableBlocks' ] : array( );
130 // remove empty reusable blocks
131 $reusable_Blocks = array_filter(
132 $reusable_Blocks,
133 function ( $v ) {
134 return ! empty( $v );
135 }
136 );
137 unset( $recursive_response[ 'reusableBlocks' ] );
138 $style = CSSParser::blocks_to_style_array( $recursive_response );
139 $reusableIds = $reusable_Blocks ? array_keys( $reusable_Blocks ) : array( );
140 if ( ! empty( $reusableIds ) ) {
141 update_option( '_betterdocs_reusable_block_ids', $reusableIds );
142 }
143 update_post_meta( $post_id, '_betterdocs_reusable_block_ids', $reusableIds );
144 $this->write_block_css( $style, $post ); //Write CSS file for this page
145
146 if ( ! empty( $reusable_Blocks ) ) {
147 foreach ( $reusable_Blocks as $blockId => $block ) {
148 $style = CSSParser::blocks_to_style_array( $block );
149 $this->write_reusable_block_css( $style, $blockId );
150 }
151 }
152 }
153
154 /**
155 * Save Widget CSS when Widget is saved
156 * @return void
157 * @since 3.5.3
158 */
159 public function after_save_widget( $id, $sidebar_id, $request, $creating ) {
160 $parsed_content = isset( $request[ 'instance' ][ 'raw' ][ 'content' ] ) ? parse_blocks( $request[ 'instance' ][ 'raw' ][ 'content' ] ) : array( );
161 if ( is_array( $parsed_content ) && ! empty( $parsed_content ) ) {
162 $betterdocs_blocks = array( );
163 $recursive_response = CSSParser::betterdocs_block_style_recursive( $parsed_content, $betterdocs_blocks );
164 unset( $recursive_response[ 'reusableBlocks' ] );
165 $style = CSSParser::blocks_to_style_array( $recursive_response );
166 //Write CSS file for Widget
167 $this->single_file_css_generator( $style, $this->style_dir, $this->prefix . '-widget.min.css' );
168 }
169 }
170
171 /**
172 * Load Dependencies
173 */
174 private function load_style_handler_dependencies() {
175 require_once plugin_dir_path( __FILE__ ) . 'includes/class-parse-css.php';
176 }
177
178 /**
179 * Enqueue frontend css for post if have one
180 * @return void
181 * @since 1.0.2
182 */
183 public function enqueue_frontend_assets() {
184 global $post;
185 $deps = apply_filters( 'betterdocs_generated_css_frontend_deps', array( ) );
186
187 // generatepress elements
188 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WP core 'active_plugins' filter, applied to detect GeneratePress Premium presence on multisite.
189 if ( in_array( 'gp-premium/gp-premium.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) {
190 $gp_elements = get_posts( array( 'post_type' => 'gp_elements' ) );
191 if ( is_array( $gp_elements ) && ! empty( $gp_elements ) ) {
192 foreach ( $gp_elements as $element ) {
193 if ( file_exists( $this->style_dir . $this->prefix . '-' . $element->ID . '.min.css' ) ) {
194 wp_enqueue_style( 'betterdocs-block-style-' . $element->ID, $this->style_url . $this->prefix . '-' . $element->ID . '.min.css', $deps, substr( md5( microtime( true ) ), 0, 10 ) );
195 }
196 }
197 }
198 }
199
200 if ( ! empty( $post ) && ! empty( $post->ID ) ) {
201 //Page/Post Style Enqueue
202 if ( file_exists( $this->style_dir . $this->prefix . '-' . $post->ID . '.min.css' ) ) {
203 wp_enqueue_style( 'betterdocs-block-style-' . $post->ID, $this->style_url . $this->prefix . '-' . $post->ID . '.min.css', $deps, substr( md5( microtime( true ) ), 0, 10 ) );
204 }
205
206 // Reusable block Style Enqueues
207 $reusableIds = get_post_meta( $post->ID, '_betterdocs_reusable_block_ids', true );
208 $reusableIds = ! empty( $reusableIds ) ? $reusableIds : array( );
209 $templateReusableIds = get_option( '_betterdocs_reusable_block_ids', array( ) );
210 $reusableIds = array_unique( array_merge( $reusableIds, $templateReusableIds ) );
211 if ( ! empty( $reusableIds ) ) {
212 foreach ( $reusableIds as $reusableId ) {
213 if ( file_exists( $this->style_dir . 'reusable-blocks/betterdocs-reusable-' . $reusableId . '.min.css' ) ) {
214 wp_enqueue_style( 'betterdocs-reusable-block-style-' . $reusableId, $this->style_url . 'reusable-blocks/betterdocs-reusable-' . $reusableId . '.min.css', $deps, substr( md5( microtime( true ) ), 0, 10 ) );
215 }
216 }
217 }
218 }
219
220 //Widget Style Enqueue
221 if ( file_exists( $this->style_dir . $this->prefix . '-widget.min.css' ) ) {
222 wp_enqueue_style( 'betterdocs-widget-style', $this->style_url . $this->prefix . '-widget.min.css', $deps, substr( md5( microtime( true ) ), 0, 10 ) );
223 }
224
225 //FSE Style Enqueue — one file per FSE template/template-part post so
226 // styles do not bleed across templates (issue #53).
227 if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() && betterdocs()->helper->is_templates() && ! empty( self::$_fse_template_post_ids ) ) {
228 foreach ( array_unique( self::$_fse_template_post_ids ) as $template_post_id ) {
229 $file_name = $this->prefix . '-edit-site-' . abs( $template_post_id ) . '.min.css';
230 if ( file_exists( $this->style_dir . $file_name ) ) {
231 wp_enqueue_style(
232 'betterdocs-fullsite-style-' . abs( $template_post_id ),
233 $this->style_url . $file_name,
234 $deps,
235 substr( md5( microtime( true ) ), 0, 10 )
236 );
237 }
238 }
239 }
240
241 /**
242 * Hooks assets for enqueue in frontend
243 *
244 * @param $path string
245 * @param $url string
246 *
247 * @since 3.0.0
248 */
249 do_action( 'betterdocs_frontend_assets', $this->style_dir, $this->style_url );
250 }
251
252 /**
253 * Get post content when page is saved
254 */
255 public function on_save_post( $post_id, $post, $update ) {
256 $post_type = get_post_type( $post_id );
257
258 //If This page is draft, return
259 if ( isset( $post->post_status ) && 'auto-draft' == $post->post_status ) {
260 return;
261 }
262
263 // Autosave, do nothing
264 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
265 return;
266 }
267
268 // Return if it's a post revision
269 if ( false !== wp_is_post_revision( $post_id ) ) {
270 return;
271 }
272
273 $parsed_content = $this->get_parsed_content( $post_id, $post, $post_type );
274
275 if ( is_array( $parsed_content ) && ! empty( $parsed_content ) ) {
276 $this->write_css_from_content( $post, $post_id, $parsed_content );
277 }
278 }
279
280 private function get_parsed_content( $post_id, $post, $post_type ) {
281 if ( 'wp_template_part' === $post_type || 'wp_template' === $post_type ) {
282 $post = get_post( $post_id );
283 }
284
285 if ( ! $post ) {
286 return array( );
287 }
288
289 $parsed_content = parse_blocks( $post->post_content );
290
291 if ( empty( $parsed_content ) ) {
292 delete_post_meta( $post_id, '_betterdocs_reusable_block_ids' );
293 }
294
295 return $parsed_content;
296 }
297
298 /**
299 * Get post content when page is load in frontend
300 */
301 public function generate_post_content() {
302 $post_id = get_the_ID();
303 if ( $post_id ) {
304 $post_type = get_post_type( $post_id );
305 $post = get_post( $post_id );
306 //If This page is draft, return
307 if ( isset( $post->post_status ) && 'auto-draft' == $post->post_status ) {
308 return;
309 }
310
311 // Return if it's a post revision
312 if ( false !== wp_is_post_revision( $post_id ) ) {
313 return null;
314 }
315
316 $parsed_content = $this->get_parsed_content( $post_id, $post, $post_type );
317
318 if ( is_array( $parsed_content ) && ! empty( $parsed_content ) ) {
319 $this->write_css_from_content( $post, $post_id, $parsed_content );
320 }
321 }
322 }
323
324 /**
325 * Ajax callback to write css in upload directory
326 * @retun void
327 * @since 1.0.2
328 */
329 private function write_block_css( $block_styles, $post ) {
330 // Write CSS for FSE templates / template parts — one file per post so
331 // styles from different templates do not overwrite each other in a
332 // shared file (issue #53).
333 if ( isset( $post->post_type ) && ( 'wp_template_part' === $post->post_type || 'wp_template' === $post->post_type ) ) {
334 if ( empty( $block_styles ) ) {
335 return;
336 }
337 if ( ! empty( $css = CSSParser::build_css( $block_styles ) ) ) {
338 if ( ! file_exists( $this->style_dir ) ) {
339 wp_mkdir_p( $this->style_dir );
340 }
341 file_put_contents( $this->style_dir . $this->prefix . '-edit-site-' . abs( $post->ID ) . '.min.css', $css );
342 }
343 } // Write CSS for Page/Posts
344 elseif ( ! empty( $css = CSSParser::build_css( $block_styles ) ) ) {
345 if ( ! file_exists( $this->style_dir ) ) {
346 wp_mkdir_p( $this->style_dir );
347 }
348 file_put_contents( $this->style_dir . $this->prefix . '-' . abs( $post->ID ) . '.min.css', $css );
349 }
350 }
351
352 /**
353 * Write css for Reusable block
354 * @retun void
355 * @since 3.4.0
356 */
357 private function write_reusable_block_css( $block_styles, $id ) {
358 if ( isset( $block_styles ) && is_array( $block_styles ) ) {
359 if ( ! empty( $css = CSSParser::build_css( $block_styles ) ) ) {
360 $upload_dir = $this->style_dir . 'reusable-blocks/';
361 if ( ! file_exists( $upload_dir ) ) {
362 wp_mkdir_p( $upload_dir );
363 }
364 file_put_contents( $upload_dir . '/' . 'betterdocs-reusable-' . abs( $id ) . '.min.css', $css );
365 }
366 }
367 }
368
369 /**
370 * Single file css generator
371 * @retun void
372 * @since 3.5.3
373 */
374 private function single_file_css_generator( $block_styles, $upload_dir, $filename ) {
375 $editSiteCssPath = $upload_dir . $filename;
376 if ( file_exists( $editSiteCssPath ) ) {
377 $existingCss = file_get_contents( $editSiteCssPath );
378 $pattern = '~\/\*(.*?)\*\/~';
379 preg_match_all( $pattern, $existingCss, $result, PREG_PATTERN_ORDER );
380 $allComments = $result[ 0 ];
381 $seperatedIds = array( );
382 foreach ( $allComments as $comment ) {
383 $id = preg_replace( '/[^A-Za-z0-9\-]|Ends|Starts/', '', $comment );
384
385 if ( strpos( $comment, 'Starts' ) ) {
386 $seperatedIds[ $id ][ 'start' ] = $comment;
387 } elseif ( strpos( $comment, 'Ends' ) ) {
388 $seperatedIds[ $id ][ 'end' ] = $comment;
389 }
390 }
391
392 $seperateStyles = array( );
393 foreach ( $seperatedIds as $key => $ids ) {
394 $seperateStyles[ ][ $key ] = isset( $block_styles[ $key ] ) ? $block_styles[ $key ] : array( );
395 }
396
397 self::$_block_styles = array_merge( self::$_block_styles, $block_styles );
398
399 if ( ! empty( $css = CSSParser::build_css( self::$_block_styles ) ) ) {
400 if ( ! file_exists( $upload_dir ) ) {
401 wp_mkdir_p( $upload_dir );
402 }
403
404 file_put_contents( $editSiteCssPath, $css );
405 }
406 } elseif ( ! empty( $css = CSSParser::build_css( $block_styles ) ) ) {
407 if ( ! file_exists( $this->style_dir ) ) {
408 wp_mkdir_p( $this->style_dir );
409 }
410
411 file_put_contents( $editSiteCssPath, $css );
412 }
413 }
414
415 /**
416 * Get post id by post_name for template
417 */
418 public static function betterdocs_get_post_content_by_post_name( $post_name ) {
419 global $wpdb;
420 $sql = $wpdb->prepare( "SELECT ID FROM {$wpdb->prefix}posts WHERE post_name = %s", $post_name );
421
422 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- one-off lookup of FSE template posts by post_name during CSS generation; no caching layer applies.
423 return $wpdb->get_results( $sql, ARRAY_A );
424 }
425 }
426