PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.4
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.4
4.9.4 4.9.3 4.9.2 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 All 202 releases
← All changes | includes/Modules/StyleHandler.php +554 -322 4.4.0 → 4.9.4 View file →
@@ -1,375 +1,607 @@
1 1 <?php
2 +namespace WPDeveloper\BetterDocs\Modules;
2 3
3 -namespace WPDeveloper\BetterDocs\Modules;
4 +if ( ! defined( 'ABSPATH' ) ) {
5 + exit;
6 +}
4 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
5 17 use WPDeveloper\BetterDocs\Utils\CSSParser;
6 18
7 19 final class StyleHandler {
8 - private static $instance;
20 + private static $instance;
9 21
10 - private $prefix = 'betterdocs-style';
11 - private $style_dir;
12 - private $style_url;
13 - /**
14 - * Holds block styles array
15 - *
16 - * @var array
17 - */
18 - public static $_block_styles = [];
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( );
19 31
20 - public static function init() {
21 - if ( null === self::$instance ) {
22 - self::$instance = new self();
23 - }
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( );
24 40
25 - return self::$instance;
26 - }
41 + public static function init() {
42 + if ( null === self::$instance ) {
43 + self::$instance = new self();
44 + }
27 45
28 - public function __construct() {
29 - $upload_dir = wp_upload_dir();
46 + return self::$instance;
47 + }
30 48
31 - $this->style_dir = $upload_dir['basedir'] . '/' . $this->prefix . DIRECTORY_SEPARATOR;
32 - $this->style_url = set_url_scheme( $upload_dir['baseurl'] ) . '/' . $this->prefix . '/';
49 + public function __construct() {
50 + $upload_dir = wp_upload_dir();
33 51
34 - add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_frontend_assets' ], 99 );
35 - add_action( 'save_post', [ $this, 'on_save_post' ], 10, 3 );
36 - add_action( 'wp', [ $this, 'generate_post_content' ] );
37 - add_action( 'rest_after_save_widget', [ $this, 'after_save_widget' ], 10, 4 );
52 + $this->style_dir = $upload_dir[ 'basedir' ] . '/' . $this->prefix . DIRECTORY_SEPARATOR;
53 + $this->style_url = set_url_scheme( $upload_dir[ 'baseurl' ] ) . '/' . $this->prefix . '/';
38 54
39 - // FSE assets generation
40 - add_action(
41 - 'init',
42 - function () {
43 - if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
44 - add_filter( '404_template', [ $this, 'fse_assets_generation' ], 99, 3 );
45 - add_filter( 'archive_template', [ $this, 'fse_assets_generation' ], 99, 3 );
46 - add_filter( 'category_template', [ $this, 'fse_assets_generation' ], 99, 3 );
47 - add_filter( 'frontpage_template', [ $this, 'fse_assets_generation' ], 99, 3 );
48 - add_filter( 'home_template', [ $this, 'fse_assets_generation' ], 99, 3 );
49 - add_filter( 'index_template', [ $this, 'fse_assets_generation' ], 99, 3 );
50 - add_filter( 'page_template', [ $this, 'fse_assets_generation' ], 99, 3 );
51 - add_filter( 'search_template', [ $this, 'fse_assets_generation' ], 99, 3 );
52 - add_filter( 'single_template', [ $this, 'fse_assets_generation' ], 99, 3 );
53 - add_filter( 'singular_template', [ $this, 'fse_assets_generation' ], 99, 3 );
54 - add_filter( 'tag_template', [ $this, 'fse_assets_generation' ], 99, 3 );
55 - add_filter( 'taxonomy_template', [ $this, 'fse_assets_generation' ], 99, 3 );
56 - }
57 - },
58 - 999
59 - );
60 - }
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' ) );
61 58
62 - /**
63 - * Generate FSE Assets
64 - */
65 - public function fse_assets_generation( $template, $type, $templates ) {
66 - $block_template = resolve_block_template( $type, $templates, $template );
67 - if ( ! empty( $block_template ) ) {
68 - $parsed_content = parse_blocks( $block_template->content );
69 - if ( is_array( $parsed_content ) && ! empty( $parsed_content ) ) {
70 - foreach ( $parsed_content as $content ) {
71 - if ( ( 'core/template-part' == $content['blockName'] ) || ( 'core/template' == $content['blockName'] ) ) {
72 - $post_ids = isset( $content['attrs']['slug'] ) ? self::betterdocs_get_post_content_by_post_name( $content['attrs']['slug'] ) : [];
59 + // Synced patterns in block widgets render on every page; track them
60 + // once the widget/sidebar changes of the request are all saved.
61 + add_action( 'rest_after_save_widget', array( $this, 'schedule_widget_patterns_refresh' ) );
62 + add_action( 'rest_delete_widget', array( $this, 'schedule_widget_patterns_refresh' ) );
63 + add_action( 'rest_save_sidebar', array( $this, 'schedule_widget_patterns_refresh' ) );
64 + add_action( 'after_switch_theme', array( $this, 'schedule_widget_patterns_refresh' ) );
65 + add_action( 'rest_after_save_widget', array( $this, 'after_save_widget' ), 10, 4 );
66 + add_action( 'rest_delete_widget', array( $this, 'after_save_widget' ) );
67 + add_action( 'rest_save_sidebar', array( $this, 'after_save_widget' ) );
68 + add_action( 'after_switch_theme', array( $this, 'after_save_widget' ) );
73 69
74 - if ( ! empty( $post_ids ) ) {
75 - foreach ( $post_ids as $id ) {
76 - $post_id = (int) $id['ID'];
77 - $post = get_post( $post_id );
78 - $parsed_content = parse_blocks( $post->post_content );
79 - $this->write_css_from_content( $post, $post_id, $parsed_content );
80 - }
81 - }
82 - } else {
83 - $post_ids = self::betterdocs_get_post_content_by_post_name( $block_template->slug );
84 - if ( ! empty( $post_ids ) ) {
85 - foreach ( $post_ids as $id ) {
86 - $post_id = (int) $id['ID'];
87 - $post = get_post( $post_id );
88 - $parsed_content = parse_blocks( $post->post_content );
89 - $this->write_css_from_content( $post, $post_id, $parsed_content );
90 - }
91 - }
92 - }
93 - }
94 - }
95 - }
70 + // FSE assets generation
71 + add_action(
72 + 'init',
73 + function () {
74 + if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
75 + add_filter( '404_template', array( $this, 'fse_assets_generation' ), 99, 3 );
76 + add_filter( 'archive_template', array( $this, 'fse_assets_generation' ), 99, 3 );
77 + add_filter( 'category_template', array( $this, 'fse_assets_generation' ), 99, 3 );
78 + add_filter( 'frontpage_template', array( $this, 'fse_assets_generation' ), 99, 3 );
79 + add_filter( 'home_template', array( $this, 'fse_assets_generation' ), 99, 3 );
80 + add_filter( 'index_template', array( $this, 'fse_assets_generation' ), 99, 3 );
81 + add_filter( 'page_template', array( $this, 'fse_assets_generation' ), 99, 3 );
82 + add_filter( 'search_template', array( $this, 'fse_assets_generation' ), 99, 3 );
83 + add_filter( 'single_template', array( $this, 'fse_assets_generation' ), 99, 3 );
84 + add_filter( 'singular_template', array( $this, 'fse_assets_generation' ), 99, 3 );
85 + add_filter( 'tag_template', array( $this, 'fse_assets_generation' ), 99, 3 );
86 + add_filter( 'taxonomy_template', array( $this, 'fse_assets_generation' ), 99, 3 );
87 + }
88 + },
89 + 999
90 + );
91 + }
96 92
97 - return $template;
98 - }
93 + /**
94 + * Generate FSE Assets
95 + */
96 + public function fse_assets_generation( $template, $type, $templates ) {
97 + $block_template = resolve_block_template( $type, $templates, $template );
98 + if ( ! empty( $block_template ) ) {
99 + $parsed_content = parse_blocks( $block_template->content );
100 + if ( is_array( $parsed_content ) && ! empty( $parsed_content ) ) {
101 + foreach ( $parsed_content as $content ) {
102 + if ( ( 'core/template-part' == $content[ 'blockName' ] ) || ( 'core/template' == $content[ 'blockName' ] ) ) {
103 + $post_ids = isset( $content[ 'attrs' ][ 'slug' ] ) ? self::betterdocs_get_post_content_by_post_name( $content[ 'attrs' ][ 'slug' ] ) : array( );
99 104
100 - /**
101 - * Write CSS
102 - */
103 - public function write_css_from_content( $post, $post_id, $parsed_content ) {
104 - $betterdocs_blocks = [];
105 - $recursive_response = CSSParser::betterdocs_block_style_recursive( $parsed_content, $betterdocs_blocks );
106 - $reusable_Blocks = ! empty( $recursive_response['reusableBlocks'] ) ? $recursive_response['reusableBlocks'] : [];
107 - // remove empty reusable blocks
108 - $reusable_Blocks = array_filter(
109 - $reusable_Blocks,
110 - function ( $v ) {
111 - return ! empty( $v );
112 - }
113 - );
114 - unset( $recursive_response['reusableBlocks'] );
115 - $style = CSSParser::blocks_to_style_array( $recursive_response );
116 - $reusableIds = $reusable_Blocks ? array_keys( $reusable_Blocks ) : [];
117 - if ( ! empty( $reusableIds ) ) {
118 - update_option( '_betterdocs_reusable_block_ids', $reusableIds );
119 - }
120 - update_post_meta( $post_id, '_betterdocs_reusable_block_ids', $reusableIds );
121 - $this->write_block_css( $style, $post ); //Write CSS file for this page
105 + if ( ! empty( $post_ids ) ) {
106 + foreach ( $post_ids as $id ) {
107 + $post_id = (int) $id[ 'ID' ];
108 + self::$_fse_template_post_ids[ ] = $post_id;
109 + $post = get_post( $post_id );
110 + $parsed_content = parse_blocks( $post->post_content );
111 + $this->write_css_from_content( $post, $post_id, $parsed_content );
112 + }
113 + }
114 + } else {
115 + $post_ids = self::betterdocs_get_post_content_by_post_name( $block_template->slug );
116 + if ( ! empty( $post_ids ) ) {
117 + foreach ( $post_ids as $id ) {
118 + $post_id = (int) $id[ 'ID' ];
119 + self::$_fse_template_post_ids[ ] = $post_id;
120 + $post = get_post( $post_id );
121 + $parsed_content = parse_blocks( $post->post_content );
122 + $this->write_css_from_content( $post, $post_id, $parsed_content );
123 + }
124 + }
125 + }
126 + }
127 + }
128 + }
122 129
123 - if ( ! empty( $reusable_Blocks ) ) {
124 - foreach ( $reusable_Blocks as $blockId => $block ) {
125 - $style = CSSParser::blocks_to_style_array( $block );
126 - $this->write_reusable_block_css( $style, $blockId );
127 - }
128 - }
129 - }
130 + return $template;
131 + }
130 132
131 - /**
132 - * Save Widget CSS when Widget is saved
133 - * @return void
134 - * @since 3.5.3
135 - */
136 - public function after_save_widget( $id, $sidebar_id, $request, $creating ) {
137 - $parsed_content = isset( $request['instance']['raw']['content'] ) ? parse_blocks( $request['instance']['raw']['content'] ) : [];
138 - if ( is_array( $parsed_content ) && ! empty( $parsed_content ) ) {
139 - $betterdocs_blocks = [];
140 - $recursive_response = CSSParser::betterdocs_block_style_recursive( $parsed_content, $betterdocs_blocks );
141 - unset( $recursive_response['reusableBlocks'] );
142 - $style = CSSParser::blocks_to_style_array( $recursive_response );
143 - //Write CSS file for Widget
144 - $this->single_file_css_generator( $style, $this->style_dir, $this->prefix . '-widget.min.css' );
145 - }
146 - }
133 + /**
134 + * Write CSS
135 + */
136 + public function write_css_from_content( $post, $post_id, $parsed_content ) {
137 + $betterdocs_blocks = array( );
138 + $recursive_response = CSSParser::betterdocs_block_style_recursive( $parsed_content, $betterdocs_blocks );
139 + $all_reusable_blocks = ! empty( $recursive_response[ 'reusableBlocks' ] ) ? $recursive_response[ 'reusableBlocks' ] : array( );
140 + // remove empty reusable blocks
141 + $reusable_Blocks = array_filter(
142 + $all_reusable_blocks,
143 + function ( $v ) {
144 + return ! empty( $v );
145 + }
146 + );
147 + unset( $recursive_response[ 'reusableBlocks' ] );
148 + $style = CSSParser::blocks_to_style_array( $recursive_response );
149 + $reusableIds = $reusable_Blocks ? array_keys( $reusable_Blocks ) : array( );
150 + // The option is merged into every page's enqueue, so only FSE
151 + // templates / template parts (rendered around every page) may set
152 + // it. Ordinary posts keep their ids in post meta below; they used to
153 + // overwrite the option too, so whatever the last post with synced
154 + // patterns referenced was loaded site-wide.
155 + if ( ! empty( $reusableIds ) && $this->is_fse_template( $post ) ) {
156 + update_option( '_betterdocs_reusable_block_ids', $reusableIds );
157 + }
158 + update_post_meta( $post_id, '_betterdocs_reusable_block_ids', $reusableIds );
159 + $this->write_block_css( $style, $post ); //Write CSS file for this page
147 160
148 - /**
149 - * Load Dependencies
150 - */
151 - private function load_style_handler_dependencies() {
152 - require_once plugin_dir_path( __FILE__ ) . 'includes/class-parse-css.php';
153 - }
161 + // Every referenced reusable block, including those with no BetterDocs
162 + // styles, so their stale files (e.g. pre-#174 copies of Essential
163 + // Blocks' CSS) are removed.
164 + foreach ( $all_reusable_blocks as $blockId => $block ) {
165 + $style = CSSParser::blocks_to_style_array( $block );
166 + $this->write_reusable_block_css( $style, $blockId );
167 + }
168 + }
154 169
155 - /**
156 - * Enqueue frontend css for post if have one
157 - * @return void
158 - * @since 1.0.2
159 - */
160 - public function enqueue_frontend_assets() {
161 - global $post;
162 - $deps = apply_filters( 'betterdocs_generated_css_frontend_deps', [] );
170 + /**
171 + * Whether a post is an FSE template or template part.
172 + *
173 + * @param \WP_Post|null $post Post.
174 + * @return bool
175 + */
176 + private function is_fse_template( $post ) {
177 + return isset( $post->post_type ) && ( 'wp_template_part' === $post->post_type || 'wp_template' === $post->post_type );
178 + }
163 179
164 - // generatepress elements
165 - if ( in_array( 'gp-premium/gp-premium.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) {
166 - $gp_elements = get_posts( [ 'post_type' => 'gp_elements' ] );
167 - if ( is_array( $gp_elements ) && ! empty( $gp_elements ) ) {
168 - foreach ( $gp_elements as $element ) {
169 - if ( file_exists( $this->style_dir . $this->prefix . '-' . $element->ID . '.min.css' ) ) {
170 - wp_enqueue_style( 'betterdocs-block-style-' . $element->ID, $this->style_url . $this->prefix . '-' . $element->ID . '.min.css', $deps, substr( md5( microtime( true ) ), 0, 10 ) );
171 - }
172 - }
173 - }
174 - }
180 + /**
181 + * Rebuild the widget CSS when a widget or sidebar is saved or a widget is
182 + * deleted — once, at the end of the request: core fires
183 + * rest_after_save_widget before it assigns the widget to its sidebar.
184 + * @return void
185 + * @since 3.5.3
186 + */
187 + public function after_save_widget() {
188 + if ( false === has_action( 'shutdown', array( $this, 'rebuild_widget_css' ) ) ) {
189 + add_action( 'shutdown', array( $this, 'rebuild_widget_css' ) );
190 + }
191 + }
175 192
176 - if ( ! empty( $post ) && ! empty( $post->ID ) ) {
177 - //Page/Post Style Enqueue
178 - if ( file_exists( $this->style_dir . $this->prefix . '-' . $post->ID . '.min.css' ) ) {
179 - wp_enqueue_style( 'betterdocs-block-style-' . $post->ID, $this->style_url . $this->prefix . '-' . $post->ID . '.min.css', $deps, substr( md5( microtime( true ) ), 0, 10 ) );
180 - }
193 + /**
194 + * Write the widget CSS file from every active block widget's BetterDocs
195 + * blocks, or remove it when there are none. It used to hold only the
196 + * widget saved last, and was never removed (e.g. stale Essential Blocks
197 + * CSS collected before #174 stayed loaded on every page).
198 + * @return void
199 + */
200 + public function rebuild_widget_css() {
201 + $styles = array( );
202 + foreach ( $this->active_block_widget_contents() as $content ) {
203 + if ( false === strpos( $content, '<!-- wp:betterdocs/' ) ) {
204 + continue;
205 + }
206 + $betterdocs_blocks = array( );
207 + $recursive_response = CSSParser::betterdocs_block_style_recursive( parse_blocks( $content ), $betterdocs_blocks );
208 + unset( $recursive_response[ 'reusableBlocks' ] );
209 + $styles = array_merge( $styles, CSSParser::blocks_to_style_array( $recursive_response ) );
210 + }
181 211
182 - // Reusable block Style Enqueues
183 - $reusableIds = get_post_meta( $post->ID, '_betterdocs_reusable_block_ids', true );
184 - $reusableIds = ! empty( $reusableIds ) ? $reusableIds : [];
185 - $templateReusableIds = get_option( '_betterdocs_reusable_block_ids', [] );
186 - $reusableIds = array_unique( array_merge( $reusableIds, $templateReusableIds ) );
187 - if ( ! empty( $reusableIds ) ) {
188 - foreach ( $reusableIds as $reusableId ) {
189 - if ( file_exists( $this->style_dir . 'reusable-blocks/betterdocs-reusable-' . $reusableId . '.min.css' ) ) {
190 - 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 ) );
191 - }
192 - }
193 - }
194 - }
212 + $file = $this->style_dir . $this->prefix . '-widget.min.css';
213 + $css = empty( $styles ) ? '' : CSSParser::build_css( $styles );
214 + if ( empty( $css ) ) {
215 + if ( file_exists( $file ) ) {
216 + wp_delete_file( $file );
217 + }
218 + return;
219 + }
195 220
196 - //Widget Style Enqueue
197 - if ( file_exists( $this->style_dir . $this->prefix . '-widget.min.css' ) ) {
198 - wp_enqueue_style( 'betterdocs-widget-style', $this->style_url . $this->prefix . '-widget.min.css', $deps, substr( md5( microtime( true ) ), 0, 10 ) );
199 - }
221 + // Unchanged CSS: leave the file (and its mtime-based version) alone.
222 + if ( file_exists( $file ) && file_get_contents( $file ) === $css ) {
223 + return;
224 + }
200 225
201 - //FSE Style Enqueue
202 - if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() && file_exists( $this->style_dir . $this->prefix . '-edit-site.min.css' ) && betterdocs()->helper->is_templates() ) {
203 - wp_enqueue_style( 'betterdocs-fullsite-style', $this->style_url . $this->prefix . '-edit-site.min.css', $deps, substr( md5( microtime( true ) ), 0, 10 ) );
204 - }
226 + if ( ! file_exists( $this->style_dir ) ) {
227 + wp_mkdir_p( $this->style_dir );
228 + }
229 + file_put_contents( $file, $css );
230 + }
205 231
206 - /**
207 - * Hooks assets for enqueue in frontend
208 - *
209 - * @param $path string
210 - * @param $url string
211 - *
212 - * @since 3.0.0
213 - */
214 - do_action( 'betterdocs_frontend_assets', $this->style_dir, $this->style_url );
215 - }
232 + /**
233 + * Content of the block widgets placed in a sidebar (not inactive).
234 + * @return string[]
235 + */
236 + private function active_block_widget_contents() {
237 + $contents = array( );
238 + $instances = get_option( 'widget_block', array( ) );
239 + foreach ( wp_get_sidebars_widgets() as $sidebar_id => $widget_ids ) {
240 + if ( 'wp_inactive_widgets' === $sidebar_id || ! is_array( $widget_ids ) ) {
241 + continue;
242 + }
243 + foreach ( $widget_ids as $widget_id ) {
244 + if ( preg_match( '/^block-(\d+)$/', (string) $widget_id, $matches ) && ! empty( $instances[ $matches[1] ]['content'] ) ) {
245 + $contents[] = (string) $instances[ $matches[1] ]['content'];
246 + }
247 + }
248 + }
249 + return $contents;
250 + }
216 251
217 - /**
218 - * Get post content when page is saved
219 - */
220 - public function on_save_post( $post_id, $post, $update ) {
221 - $post_type = get_post_type( $post_id );
252 + /**
253 + * Load Dependencies
254 + */
255 + private function load_style_handler_dependencies() {
256 + require_once plugin_dir_path( __FILE__ ) . 'includes/class-parse-css.php';
257 + }
222 258
223 - //If This page is draft, return
224 - if ( isset( $post->post_status ) && 'auto-draft' == $post->post_status ) {
225 - return;
226 - }
259 + /**
260 + * Enqueue frontend css for post if have one
261 + * @return void
262 + * @since 1.0.2
263 + */
264 + public function enqueue_frontend_assets() {
265 + global $post;
266 + $deps = apply_filters( 'betterdocs_generated_css_frontend_deps', array( ) );
227 267
228 - // Autosave, do nothing
229 - if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
230 - return;
231 - }
268 + // generatepress elements
269 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WP core 'active_plugins' filter, applied to detect GeneratePress Premium presence on multisite.
270 + if ( in_array( 'gp-premium/gp-premium.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) {
271 + $gp_elements = get_posts( array( 'post_type' => 'gp_elements' ) );
272 + if ( is_array( $gp_elements ) && ! empty( $gp_elements ) ) {
273 + foreach ( $gp_elements as $element ) {
274 + if ( file_exists( $this->style_dir . $this->prefix . '-' . $element->ID . '.min.css' ) ) {
275 + wp_enqueue_style( 'betterdocs-block-style-' . $element->ID, $this->style_url . $this->prefix . '-' . $element->ID . '.min.css', $deps, $this->file_version( $this->style_dir . $this->prefix . '-' . $element->ID . '.min.css' ) );
276 + }
277 + }
278 + }
279 + }
232 280
233 - // Return if it's a post revision
234 - if ( false !== wp_is_post_revision( $post_id ) ) {
235 - return;
236 - }
281 + if ( ! empty( $post ) && ! empty( $post->ID ) ) {
282 + //Page/Post Style Enqueue — only for posts that hold BetterDocs
283 + // blocks. Until #174 the parser also collected Essential Blocks'
284 + // blocks, so files written then for pages without BetterDocs
285 + // blocks are stale duplicates of Essential Blocks' CSS; they are
286 + // skipped here and removed on the post's next save (#174).
287 + $post_css = $this->style_dir . $this->prefix . '-' . $post->ID . '.min.css';
288 + if ( file_exists( $post_css ) && false !== strpos( (string) $post->post_content, '<!-- wp:betterdocs/' ) ) {
289 + wp_enqueue_style( 'betterdocs-block-style-' . $post->ID, $this->style_url . $this->prefix . '-' . $post->ID . '.min.css', $deps, $this->file_version( $post_css ) );
290 + }
237 291
238 - $parsed_content = $this->get_parsed_content( $post_id, $post, $post_type );
292 + // Reusable block Style Enqueues
293 + $reusableIds = get_post_meta( $post->ID, '_betterdocs_reusable_block_ids', true );
294 + $reusableIds = ! empty( $reusableIds ) ? $reusableIds : array( );
295 + $templateReusableIds = get_option( '_betterdocs_reusable_block_ids', array( ) );
296 + $reusableIds = array_unique( array_merge( $reusableIds, $templateReusableIds ) );
297 + foreach ( $reusableIds as $reusableId ) {
298 + $this->enqueue_reusable_block_style( $reusableId, $deps );
299 + }
300 + }
239 301
240 - if ( is_array( $parsed_content ) && ! empty( $parsed_content ) ) {
241 - $this->write_css_from_content( $post, $post_id, $parsed_content );
242 - }
243 - }
302 + //Widget Style Enqueue — only while an active block widget holds
303 + // BetterDocs blocks, so a stale file (see rebuild_widget_css()) is
304 + // not loaded site-wide until the next widget save removes it.
305 + if ( file_exists( $this->style_dir . $this->prefix . '-widget.min.css' ) && false !== strpos( implode( '', $this->active_block_widget_contents() ), '<!-- wp:betterdocs/' ) ) {
306 + wp_enqueue_style( 'betterdocs-widget-style', $this->style_url . $this->prefix . '-widget.min.css', $deps, $this->file_version( $this->style_dir . $this->prefix . '-widget.min.css' ) );
307 + }
244 308
245 - private function get_parsed_content( $post_id, $post, $post_type ) {
246 - if ( $post_type === 'wp_template_part' || $post_type === 'wp_template' ) {
247 - $post = get_post( $post_id );
248 - }
309 + // Synced patterns in block widgets, on every page. Built once on
310 + // sites whose widgets were saved before this list existed.
311 + $widgetReusableIds = get_option( '_betterdocs_widget_reusable_block_ids', null );
312 + if ( null === $widgetReusableIds ) {
313 + $widgetReusableIds = $this->refresh_widget_patterns();
314 + }
315 + foreach ( (array) $widgetReusableIds as $reusableId ) {
316 + $this->enqueue_reusable_block_style( $reusableId, $deps );
317 + }
249 318
250 - $parsed_content = parse_blocks( $post->post_content );
319 + //FSE Style Enqueue — one file per FSE template/template-part post so
320 + // styles do not bleed across templates (issue #53).
321 + if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() && betterdocs()->helper->is_templates() && ! empty( self::$_fse_template_post_ids ) ) {
322 + foreach ( array_unique( self::$_fse_template_post_ids ) as $template_post_id ) {
323 + $file_name = $this->prefix . '-edit-site-' . abs( $template_post_id ) . '.min.css';
324 + if ( file_exists( $this->style_dir . $file_name ) ) {
325 + wp_enqueue_style(
326 + 'betterdocs-fullsite-style-' . abs( $template_post_id ),
327 + $this->style_url . $file_name,
328 + $deps,
329 + $this->file_version( $this->style_dir . $file_name )
330 + );
331 + }
332 + }
333 + }
251 334
252 - if ( empty( $parsed_content ) ) {
253 - delete_post_meta( $post_id, '_betterdocs_reusable_block_ids' );
254 - }
335 + /**
336 + * Hooks assets for enqueue in frontend
337 + *
338 + * @param $path string
339 + * @param $url string
340 + *
341 + * @since 3.0.0
342 + */
343 + do_action( 'betterdocs_frontend_assets', $this->style_dir, $this->style_url );
344 + }
255 345
256 - return $parsed_content;
257 - }
346 + /**
347 + * Get post content when page is saved
348 + */
349 + public function on_save_post( $post_id, $post, $update ) {
350 + $post_type = get_post_type( $post_id );
258 351
259 - /**
260 - * Get post content when page is load in frontend
261 - */
262 - public function generate_post_content() {
263 - $post_id = get_the_ID();
264 - if ( $post_id ) {
265 - $post_type = get_post_type( $post_id );
266 - $post = get_post( $post_id );
267 - //If This page is draft, return
268 - if ( isset( $post->post_status ) && 'auto-draft' == $post->post_status ) {
269 - return;
270 - }
352 + //If This page is draft, return
353 + if ( isset( $post->post_status ) && 'auto-draft' == $post->post_status ) {
354 + return;
355 + }
271 356
272 - // Return if it's a post revision
273 - if ( false !== wp_is_post_revision( $post_id ) ) {
274 - return null;
275 - }
357 + // Autosave, do nothing
358 + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
359 + return;
360 + }
276 361
277 - $parsed_content = $this->get_parsed_content( $post_id, $post, $post_type );
362 + // Return if it's a post revision
363 + if ( false !== wp_is_post_revision( $post_id ) ) {
364 + return;
365 + }
278 366
279 - if ( is_array( $parsed_content ) && ! empty( $parsed_content ) ) {
280 - $this->write_css_from_content( $post, $post_id, $parsed_content );
281 - }
282 - }
283 - }
367 + $parsed_content = $this->get_parsed_content( $post_id, $post, $post_type );
284 368
285 - /**
286 - * Ajax callback to write css in upload directory
287 - * @retun void
288 - * @since 1.0.2
289 - */
290 - private function write_block_css( $block_styles, $post ) {
291 - //Write CSS for FSE
292 - if ( isset( $post->post_type ) && ( $post->post_type === 'wp_template_part' || $post->post_type === 'wp_template' && ! empty( $block_styles ) ) ) {
293 - $this->single_file_css_generator( $block_styles, $this->style_dir, $this->prefix . '-edit-site.min.css' );
294 - } // Write CSS for Page/Posts
295 - elseif ( ! empty( $css = CSSParser::build_css( $block_styles ) ) ) {
296 - if ( ! file_exists( $this->style_dir ) ) {
297 - mkdir( $this->style_dir );
298 - }
299 - file_put_contents( $this->style_dir . $this->prefix . '-' . abs( $post->ID ) . '.min.css', $css );
300 - }
301 - }
369 + // A synced pattern is styled through its reusable-block file, which
370 + // pages referencing it enqueue. Refresh (or remove) that file here so
371 + // edits to the pattern take effect without re-saving those pages.
372 + if ( 'wp_block' === $post_type ) {
373 + $betterdocs_blocks = array( );
374 + $recursive_response = is_array( $parsed_content ) ? CSSParser::betterdocs_block_style_recursive( $parsed_content, $betterdocs_blocks ) : array( );
375 + $this->write_reusable_block_css( CSSParser::blocks_to_style_array( $recursive_response ), $post_id );
376 + return;
377 + }
302 378
303 - /**
304 - * Write css for Reusable block
305 - * @retun void
306 - * @since 3.4.0
307 - */
308 - private function write_reusable_block_css( $block_styles, $id ) {
309 - if ( isset( $block_styles ) && is_array( $block_styles ) ) {
310 - if ( ! empty( $css = CSSParser::build_css( $block_styles ) ) ) {
311 - $upload_dir = $this->style_dir . 'reusable-blocks/';
312 - if ( ! file_exists( $upload_dir ) ) {
313 - mkdir( $upload_dir, 0777, true );
314 - }
315 - file_put_contents( $upload_dir . '/' . 'betterdocs-reusable-' . abs( $id ) . '.min.css', $css );
316 - }
317 - }
318 - }
379 + if ( is_array( $parsed_content ) && ! empty( $parsed_content ) ) {
380 + $this->write_css_from_content( $post, $post_id, $parsed_content );
381 + }
382 + }
319 383
320 - /**
321 - * Single file css generator
322 - * @retun void
323 - * @since 3.5.3
324 - */
325 - private function single_file_css_generator( $block_styles, $upload_dir, $filename ) {
326 - $editSiteCssPath = $upload_dir . $filename;
327 - if ( file_exists( $editSiteCssPath ) ) {
328 - $existingCss = file_get_contents( $editSiteCssPath );
329 - $pattern = '~\/\*(.*?)\*\/~';
330 - preg_match_all( $pattern, $existingCss, $result, PREG_PATTERN_ORDER );
331 - $allComments = $result[0];
332 - $seperatedIds = [];
333 - foreach ( $allComments as $comment ) {
334 - $id = preg_replace( '/[^A-Za-z0-9\-]|Ends|Starts/', '', $comment );
384 + private function get_parsed_content( $post_id, $post, $post_type ) {
385 + if ( 'wp_template_part' === $post_type || 'wp_template' === $post_type ) {
386 + $post = get_post( $post_id );
387 + }
335 388
336 - if ( strpos( $comment, 'Starts' ) ) {
337 - $seperatedIds[ $id ]['start'] = $comment;
338 - } elseif ( strpos( $comment, 'Ends' ) ) {
339 - $seperatedIds[ $id ]['end'] = $comment;
340 - }
341 - }
389 + if ( ! $post ) {
390 + return array( );
391 + }
342 392
343 - $seperateStyles = [];
344 - foreach ( $seperatedIds as $key => $ids ) {
345 - $seperateStyles[][ $key ] = isset( $block_styles[ $key ] ) ? $block_styles[ $key ] : [];
346 - }
393 + $parsed_content = parse_blocks( $post->post_content );
347 394
348 - self::$_block_styles = array_merge( self::$_block_styles, $block_styles );
395 + if ( empty( $parsed_content ) ) {
396 + delete_post_meta( $post_id, '_betterdocs_reusable_block_ids' );
397 + }
349 398
350 - if ( ! empty( $css = CSSParser::build_css( self::$_block_styles ) ) ) {
351 - if ( ! file_exists( $upload_dir ) ) {
352 - mkdir( $upload_dir );
353 - }
399 + return $parsed_content;
400 + }
354 401
355 - file_put_contents( $editSiteCssPath, $css );
356 - }
357 - } elseif ( ! empty( $css = CSSParser::build_css( $block_styles ) ) ) {
358 - if ( ! file_exists( $this->style_dir ) ) {
359 - mkdir( $this->style_dir );
360 - }
402 + /**
403 + * Get post content when page is load in frontend
404 + */
405 + /**
406 + * Cheap pre-check: can this post's content hold BetterDocs blocks,
407 + * directly or through a reusable block (`core/block` ref)?
408 + *
409 + * @param \WP_Post|null $post Post.
410 + * @return bool
411 + */
412 + public static function may_have_betterdocs_blocks( $post ) {
413 + $content = isset( $post->post_content ) ? (string) $post->post_content : '';
414 + return false !== strpos( $content, '<!-- wp:betterdocs/' ) || false !== strpos( $content, '<!-- wp:block ' );
415 + }
361 416
362 - file_put_contents( $editSiteCssPath, $css );
363 - }
364 - }
417 + /**
418 + * Whether a synced pattern (wp_block) holds BetterDocs blocks.
419 + * get_post() is object-cached, so this is cheap per enqueue.
420 + *
421 + * @param int $id wp_block post ID.
422 + * @return bool
423 + */
424 + private function reusable_block_has_betterdocs_blocks( $id ) {
425 + $block = get_post( (int) $id );
426 + return isset( $block->post_content ) && false !== strpos( (string) $block->post_content, '<!-- wp:betterdocs/' );
427 + }
365 428
366 - /**
367 - * Get post id by post_name for template
368 - */
369 - public static function betterdocs_get_post_content_by_post_name( $post_name ) {
370 - global $wpdb;
371 - $sql = $wpdb->prepare( "SELECT ID FROM {$wpdb->prefix}posts WHERE post_name = %s", $post_name );
429 + /**
430 + * Refresh the widget pattern list at the end of the request: core fires
431 + * rest_after_save_widget before it assigns the widget to its sidebar.
432 + * @return void
433 + */
434 + public function schedule_widget_patterns_refresh() {
435 + if ( false === has_action( 'shutdown', array( $this, 'refresh_widget_patterns' ) ) ) {
436 + add_action( 'shutdown', array( $this, 'refresh_widget_patterns' ) );
437 + }
438 + }
372 439
373 - return $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
374 - }
440 + /**
441 + * Write the CSS of the synced patterns placed in active block widgets and
442 + * record their ids, which every page enqueues.
443 + * @return array Pattern ids.
444 + */
445 + public function refresh_widget_patterns() {
446 + $ids = array( );
447 + $instances = get_option( 'widget_block', array( ) );
448 + foreach ( wp_get_sidebars_widgets() as $sidebar_id => $widget_ids ) {
449 + if ( 'wp_inactive_widgets' === $sidebar_id || ! is_array( $widget_ids ) ) {
450 + continue;
451 + }
452 + foreach ( $widget_ids as $widget_id ) {
453 + if ( ! preg_match( '/^block-(\d+)$/', (string) $widget_id, $matches ) || empty( $instances[ $matches[1] ]['content'] ) ) {
454 + continue;
455 + }
456 + $betterdocs_blocks = array( );
457 + $recursive_response = CSSParser::betterdocs_block_style_recursive( parse_blocks( $instances[ $matches[1] ]['content'] ), $betterdocs_blocks );
458 + $reusable_blocks = ! empty( $recursive_response['reusableBlocks'] ) ? $recursive_response['reusableBlocks'] : array( );
459 + foreach ( $reusable_blocks as $block_id => $block ) {
460 + $this->write_reusable_block_css( CSSParser::blocks_to_style_array( $block ), $block_id );
461 + if ( ! empty( $block ) ) {
462 + $ids[] = (int) $block_id;
463 + }
464 + }
465 + }
466 + }
467 +
468 + $ids = array_values( array_unique( $ids ) );
469 + update_option( '_betterdocs_widget_reusable_block_ids', $ids );
470 + return $ids;
471 + }
472 +
473 + /**
474 + * Enqueue a synced pattern's CSS file, if it holds BetterDocs blocks.
475 + * @return void
476 + */
477 + private function enqueue_reusable_block_style( $reusableId, $deps ) {
478 + // Only for synced patterns that hold BetterDocs blocks: files written
479 + // before #174 for patterns built with other plugins' blocks are stale
480 + // duplicates of their CSS (#174).
481 + $file = $this->style_dir . 'reusable-blocks/betterdocs-reusable-' . $reusableId . '.min.css';
482 + if ( file_exists( $file ) && $this->reusable_block_has_betterdocs_blocks( $reusableId ) ) {
483 + wp_enqueue_style( 'betterdocs-reusable-block-style-' . $reusableId, $this->style_url . 'reusable-blocks/betterdocs-reusable-' . $reusableId . '.min.css', $deps, $this->file_version( $file ) );
484 + }
485 + }
486 +
487 + public function generate_post_content() {
488 + $post_id = get_the_ID();
489 + if ( $post_id ) {
490 + $post_type = get_post_type( $post_id );
491 + $post = get_post( $post_id );
492 + //If This page is draft, return
493 + if ( isset( $post->post_status ) && 'auto-draft' == $post->post_status ) {
494 + return;
495 + }
496 +
497 + // Return if it's a post revision
498 + if ( false !== wp_is_post_revision( $post_id ) ) {
499 + return null;
500 + }
501 +
502 + // This runs on every uncached front-end request. Skip the parse
503 + // and the file/meta/option writes for content with nothing for
504 + // BetterDocs to style — on a large page built with other blocks
505 + // that was a full parse_blocks() plus a CSS rewrite per view.
506 + if ( ! self::may_have_betterdocs_blocks( $post ) ) {
507 + return;
508 + }
509 +
510 + $parsed_content = $this->get_parsed_content( $post_id, $post, $post_type );
511 +
512 + if ( is_array( $parsed_content ) && ! empty( $parsed_content ) ) {
513 + $this->write_css_from_content( $post, $post_id, $parsed_content );
514 + }
515 + }
516 + }
517 +
518 + /**
519 + * Cache-busting version for a generated stylesheet: its modification time.
520 + * Was a random value per render, which stopped browsers caching the file.
521 + *
522 + * @param string $path File path.
523 + * @return string|false
524 + */
525 + private function file_version( $path ) {
526 + $mtime = @filemtime( $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
527 + return $mtime ? (string) $mtime : false;
528 + }
529 +
530 + /**
531 + * Ajax callback to write css in upload directory
532 + * @retun void
533 + * @since 1.0.2
534 + */
535 + private function write_block_css( $block_styles, $post ) {
536 + // Write CSS for FSE templates / template parts — one file per post so
537 + // styles from different templates do not overwrite each other in a
538 + // shared file (issue #53).
539 + if ( isset( $post->post_type ) && ( 'wp_template_part' === $post->post_type || 'wp_template' === $post->post_type ) ) {
540 + $file = $this->style_dir . $this->prefix . '-edit-site-' . abs( $post->ID ) . '.min.css';
541 + } else { // Page/Posts
542 + $file = $this->style_dir . $this->prefix . '-' . abs( $post->ID ) . '.min.css';
543 + }
544 +
545 + $css = empty( $block_styles ) ? '' : CSSParser::build_css( $block_styles );
546 + if ( empty( $css ) ) {
547 + // No BetterDocs blocks left: drop the file so a stale one is not
548 + // enqueued forever (#174).
549 + if ( file_exists( $file ) ) {
550 + wp_delete_file( $file );
551 + }
552 + return;
553 + }
554 +
555 + // Unchanged CSS: leave the file (and its mtime-based version) alone,
556 + // so browsers keep their cached copy.
557 + if ( file_exists( $file ) && file_get_contents( $file ) === $css ) {
558 + return;
559 + }
560 +
561 + if ( ! file_exists( $this->style_dir ) ) {
562 + wp_mkdir_p( $this->style_dir );
563 + }
564 + file_put_contents( $file, $css );
565 + }
566 +
567 + /**
568 + * Write css for Reusable block
569 + * @retun void
570 + * @since 3.4.0
571 + */
572 + private function write_reusable_block_css( $block_styles, $id ) {
573 + $upload_dir = $this->style_dir . 'reusable-blocks/';
574 + $file = $upload_dir . 'betterdocs-reusable-' . abs( $id ) . '.min.css';
575 +
576 + $css = empty( $block_styles ) || ! is_array( $block_styles ) ? '' : CSSParser::build_css( $block_styles );
577 + if ( empty( $css ) ) {
578 + // No BetterDocs blocks in this synced pattern: drop the file so a
579 + // stale one is not enqueued forever (#174).
580 + if ( file_exists( $file ) ) {
581 + wp_delete_file( $file );
582 + }
583 + return;
584 + }
585 +
586 + // Unchanged CSS: leave the file (and its mtime-based version) alone.
587 + if ( file_exists( $file ) && file_get_contents( $file ) === $css ) {
588 + return;
589 + }
590 +
591 + if ( ! file_exists( $upload_dir ) ) {
592 + wp_mkdir_p( $upload_dir );
593 + }
594 + file_put_contents( $file, $css );
595 + }
596 +
597 + /**
598 + * Get post id by post_name for template
599 + */
600 + public static function betterdocs_get_post_content_by_post_name( $post_name ) {
601 + global $wpdb;
602 + $sql = $wpdb->prepare( "SELECT ID FROM {$wpdb->prefix}posts WHERE post_name = %s", $post_name );
603 +
604 + // 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.
605 + return $wpdb->get_results( $sql, ARRAY_A );
606 + }
375 607 }