PluginProbe
WindPress – Tailwind CSS integration for WordPress / 3.2.89
WindPress – Tailwind CSS integration for WordPress v3.2.89
3.2.89 3.2.88 3.2.87 3.2.86 3.2.85 3.2.84 3.2.83 3.2.82 3.2.81 trunk 3.0.0 3.0.1 3.0.10 3.0.11 3.0.12 3.0.13 3.0.14 3.0.15 3.0.16 3.0.17 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 All 143 releases
← All changes | src/Integration/Gutenberg/Editor.php +349 -15 3.0.113.2.89 View file →
@@ -10,11 +10,15 @@
10 10 */
11 11 declare (strict_types=1);
12 12 namespace WindPress\WindPress\Integration\Gutenberg;
13 13
14 +use Exception;
14 15 use WIND_PRESS;
15 16 use WindPress\WindPress\Core\Runtime;
16 -use WindPress\WindPress\Utils\AssetVite;
17 +use WP_Theme_JSON_Data;
18 +use WindPress\WindPress\Core\Cache as CoreCache;
19 +use WindPress\WindPress\Utils\Config;
20 +use WindPress\WindPress\Utils\Vite;
17 21 /**
18 22 * @author Joshua Gugun Siagian <suabahasa@gmail.com>
19 23 */
20 24 class Editor
@@ -20,41 +24,371 @@
20 24 class Editor
21 25 {
22 26 public function __construct()
23 27 {
24 - \add_action('enqueue_block_editor_assets', fn() => $this->enqueue_block_editor_assets());
28 + add_action('enqueue_block_editor_assets', fn() => $this->enqueue_block_editor_assets());
29 + if (Config::get('integration.gutenberg.settings.theme_json', \true)) {
30 + add_filter('wp_theme_json_data_theme', fn($theme_json) => $this->filter_theme_json_data($theme_json, 'theme'), 1000001);
31 + add_filter('wp_theme_json_data_user', fn($theme_json) => $this->filter_theme_json_data($theme_json, 'custom'), 1000001);
32 + }
25 33 }
26 34 public function enqueue_block_editor_assets()
27 35 {
28 - $screen = \get_current_screen();
29 - if (\is_admin() && $screen->is_block_editor()) {
30 - \add_action('admin_head', fn() => $this->admin_head(), 1000001);
36 + $screen = get_current_screen();
37 + if (is_admin() && $screen->is_block_editor()) {
38 + add_action('admin_head', fn() => $this->admin_head(), 1000001);
31 39 }
32 40 }
33 41 public function admin_head()
34 42 {
43 + Runtime::get_instance()->print_windpress_metadata();
35 44 Runtime::get_instance()->enqueue_play_cdn();
36 - if (\strpos($_SERVER['REQUEST_URI'], 'site-editor.php') !== \false) {
45 + if (strpos($_SERVER['REQUEST_URI'], 'site-editor.php') !== \false) {
37 46 // handle the canvas
38 - AssetVite::get_instance()->enqueue_asset('assets/integration/gutenberg/site-editor.js', ['handle' => WIND_PRESS::WP_OPTION . ':integration-gutenberg-site-editor', 'in-footer' => \true]);
47 + Vite::assets()->enqueue('resources/integration/gutenberg/site-editor.js', ['handle' => WIND_PRESS::WP_OPTION . ':integration-gutenberg-site-editor', 'in_footer' => \true]);
39 48 } else {
40 49 // handle the canvas
41 - AssetVite::get_instance()->enqueue_asset('assets/integration/gutenberg/post-editor.js', ['handle' => WIND_PRESS::WP_OPTION . ':integration-gutenberg-post-editor', 'in-footer' => \true]);
50 + Vite::assets()->enqueue('resources/integration/gutenberg/post-editor.js', ['handle' => WIND_PRESS::WP_OPTION . ':integration-gutenberg-post-editor', 'in_footer' => \true]);
42 51 }
43 52 $handle = WIND_PRESS::WP_OPTION . ':integration-gutenberg-block-editor';
44 - AssetVite::get_instance()->enqueue_asset('assets/integration/gutenberg/block-editor.jsx', ['handle' => $handle, 'in-footer' => \true, 'dependencies' => ['wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-hooks', 'wp-i18n', 'react', 'react-dom']]);
45 - \wp_add_inline_script($handle, <<<JS
53 + Vite::assets()->enqueue('resources/integration/gutenberg/block-editor.jsx', ['handle' => $handle, 'in_footer' => \true, 'dependencies' => ['wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-hooks', 'wp-i18n', 'react', 'react-dom', 'react-jsx-runtime']]);
54 + // Enqueue Common Block if enabled
55 + if (Config::get('integration.gutenberg.settings.common_block', \true)) {
56 + Vite::assets()->enqueue('resources/integration/gutenberg/common-block/index.jsx', ['handle' => WIND_PRESS::WP_OPTION . ':integration-gutenberg-common-block', 'in_footer' => \true, 'dependencies' => ['wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-block-editor', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-data', 'react', 'react-dom', 'react-jsx-runtime', $handle]]);
57 + }
58 + // Enqueue Generate Cache module if enabled
59 + if (Config::get('integration.gutenberg.modules.generate_cache', \true)) {
60 + Vite::assets()->enqueue('resources/integration/gutenberg/modules/generate-cache/main.ts', ['handle' => WIND_PRESS::WP_OPTION . ':integration-gutenberg-generate-cache', 'in_footer' => \true, 'dependencies' => ['wp-data', 'wp-preferences', 'wp-components', 'wp-i18n', 'wp-plugins', 'wp-editor', 'wp-element', 'react', 'react-dom', 'react-jsx-runtime', $handle]]);
61 + }
62 + wp_add_inline_script($handle, <<<JS
63 + // add __windpress__disable_playObserver to window if not exists
64 + if (typeof window.__windpress__disable_playObserver === 'undefined') {
65 + window.__windpress__disable_playObserver = true;
66 + }
67 +
46 68 document.addEventListener('DOMContentLoaded', function () {
47 69 wp.hooks.addFilter('windpressgutenberg-autocomplete-items-query', 'windpressgutenberg', async (autocompleteItems, text) => {
48 - if (!window.windpress?.loaded?.module?.autocomplete) {
49 - return autocompleteItems;
50 - }
70 + const windpress_suggestions = await window.windpress.module.autocomplete.query(text);
51 71
52 - const windpress_suggestions = window.wp.hooks.applyFilters('windpress.module.autocomplete', text);
53 -
54 72 return [...windpress_suggestions, ...autocompleteItems];
55 73 });
56 74 });
57 75 JS
58 76 , 'after');
77 + }
78 + /**
79 + * @see https://make.wordpress.org/core/2022/10/10/filters-for-theme-json-data/
80 + * @param WP_Theme_JSON_Data $theme_json
81 + * @return WP_Theme_JSON_Data
82 + */
83 + public function filter_theme_json_data($theme_json, string $origin = 'theme')
84 + {
85 + // Only apply if Tailwind v4 is active
86 + if (Runtime::tailwindcss_version() !== 4) {
87 + return $theme_json;
88 + }
89 + $theme_json_cache_path = CoreCache::get_cache_path(CoreCache::THEME_JSON_FILE);
90 + // Check if WindPress theme.json cache exists
91 + if (!file_exists($theme_json_cache_path)) {
92 + return $theme_json;
93 + }
94 + // Get WindPress theme.json from cache
95 + $windpress_theme_json_content = file_get_contents($theme_json_cache_path);
96 + if ($windpress_theme_json_content === \false) {
97 + return $theme_json;
98 + }
99 + // Decode the cached theme.json
100 + $windpress_theme_data = json_decode($windpress_theme_json_content, \true);
101 + if (!is_array($windpress_theme_data) || !isset($windpress_theme_data['version'])) {
102 + return $theme_json;
103 + }
104 + try {
105 + $current_theme_data = $this->normalize_existing_preset_data($theme_json->get_data());
106 + $prefixed_windpress_theme_data = $this->prefix_windpress_theme_preset_slugs($windpress_theme_data);
107 + $merged_theme_data = $current_theme_data;
108 + if ($origin === 'theme') {
109 + $merged_theme_data = $this->merge_windpress_theme_presets($merged_theme_data, $prefixed_windpress_theme_data, 'theme');
110 + }
111 + if ($origin === 'custom') {
112 + $merged_theme_data = $this->merge_windpress_theme_presets($merged_theme_data, $prefixed_windpress_theme_data, 'theme', \true);
113 + }
114 + // Return updated theme.json data
115 + return $theme_json->update_with($merged_theme_data);
116 + } catch (Exception $e) {
117 + if (\WP_DEBUG) {
118 + error_log('WindPress theme.json integration error: ' . $e->getMessage());
119 + }
120 + return $theme_json;
121 + }
122 + }
123 + /**
124 + * Prefix WindPress preset slugs to avoid collisions with theme/user presets.
125 + */
126 + private function prefix_windpress_theme_preset_slugs(array $windpress_data): array
127 + {
128 + $preset_paths = [['settings', 'color', 'palette'], ['settings', 'typography', 'fontSizes'], ['settings', 'typography', 'fontFamilies'], ['settings', 'spacing', 'spacingSizes'], ['settings', 'border', 'radiusSizes'], ['settings', 'shadow', 'presets']];
129 + foreach ($preset_paths as $path) {
130 + $this->prefix_preset_slugs_at_path($windpress_data, $path);
131 + }
132 + return $windpress_data;
133 + }
134 + private function normalize_existing_preset_data(array $theme_data): array
135 + {
136 + $preset_paths = [['settings', 'color', 'palette'], ['settings', 'typography', 'fontSizes'], ['settings', 'typography', 'fontFamilies'], ['settings', 'spacing', 'spacingSizes'], ['settings', 'border', 'radiusSizes'], ['settings', 'shadow', 'presets']];
137 + foreach ($preset_paths as $path) {
138 + $this->normalize_preset_collection_at_path($theme_data, $path);
139 + }
140 + return $theme_data;
141 + }
142 + /**
143 + * Merge WindPress presets into the selected origin while preserving existing presets.
144 + */
145 + private function merge_windpress_theme_presets(array $current_data, array $windpress_data, string $origin, bool $only_if_origin_exists = \false): array
146 + {
147 + $merged_data = $current_data;
148 + $preset_paths = [['settings', 'color', 'palette'], ['settings', 'typography', 'fontSizes'], ['settings', 'typography', 'fontFamilies'], ['settings', 'spacing', 'spacingSizes'], ['settings', 'border', 'radiusSizes'], ['settings', 'shadow', 'presets']];
149 + foreach ($preset_paths as $path) {
150 + if ($only_if_origin_exists && !$this->has_origin_preset_list_at_path($merged_data, $path, $origin)) {
151 + continue;
152 + }
153 + $windpress_presets = $this->get_preset_list_at_path($windpress_data, $path);
154 + if ($windpress_presets === []) {
155 + continue;
156 + }
157 + $current_presets = $this->get_preset_list_at_path($merged_data, $path, $origin);
158 + $merged_presets = $this->merge_preset_lists($current_presets, $windpress_presets);
159 + $this->set_preset_list_for_origin($merged_data, $path, $origin, $merged_presets);
160 + }
161 + return $merged_data;
162 + }
163 + private function has_origin_preset_list_at_path(array $data, array $path, string $origin): bool
164 + {
165 + $target = $this->get_array_at_path($data, $path);
166 + if ($target === null || !$this->is_origin_preset_map($target)) {
167 + return \false;
168 + }
169 + return isset($target[$origin]) && is_array($target[$origin]);
170 + }
171 + private function prefix_preset_slugs_at_path(array &$data, array $path): void
172 + {
173 + $target =& $data;
174 + foreach ($path as $segment) {
175 + if (!isset($target[$segment]) || !is_array($target[$segment])) {
176 + return;
177 + }
178 + $target =& $target[$segment];
179 + }
180 + if ($this->is_origin_preset_map($target)) {
181 + foreach ($target as $origin => $origin_presets) {
182 + if (!is_array($origin_presets)) {
183 + continue;
184 + }
185 + $normalized_presets = $this->normalize_preset_collection_with_resolved_slugs($origin_presets);
186 + $this->prefix_preset_slugs($normalized_presets);
187 + $target[$origin] = $normalized_presets;
188 + }
189 + return;
190 + }
191 + $normalized_presets = $this->normalize_preset_collection_with_resolved_slugs($target);
192 + $this->prefix_preset_slugs($normalized_presets);
193 + $target = $normalized_presets;
194 + }
195 + private function normalize_preset_collection_at_path(array &$data, array $path): void
196 + {
197 + $target =& $data;
198 + foreach ($path as $segment) {
199 + if (!isset($target[$segment]) || !is_array($target[$segment])) {
200 + return;
201 + }
202 + $target =& $target[$segment];
203 + }
204 + if ($this->is_origin_preset_map($target)) {
205 + foreach ($target as $origin => $origin_presets) {
206 + if (!is_array($origin_presets)) {
207 + continue;
208 + }
209 + $target[$origin] = $this->normalize_preset_collection_with_resolved_slugs($origin_presets);
210 + }
211 + return;
212 + }
213 + $target = $this->normalize_preset_collection_with_resolved_slugs($target);
214 + }
215 + private function prefix_preset_slugs(array &$presets): void
216 + {
217 + foreach ($presets as &$preset) {
218 + if (!is_array($preset)) {
219 + continue;
220 + }
221 + $slug = $this->resolve_preset_slug($preset);
222 + if ($slug === null) {
223 + continue;
224 + }
225 + if (strpos($slug, 'windpress-') !== 0) {
226 + $slug = 'windpress-' . $slug;
227 + }
228 + $preset['slug'] = $slug;
229 + }
230 + unset($preset);
231 + }
232 + private function get_preset_list_at_path(array $data, array $path, ?string $origin = null): array
233 + {
234 + $target = $this->get_array_at_path($data, $path);
235 + if ($target === null) {
236 + return [];
237 + }
238 + if ($origin !== null) {
239 + if (!isset($target[$origin]) || !is_array($target[$origin])) {
240 + return [];
241 + }
242 + return $this->normalize_preset_collection($target[$origin]);
243 + }
244 + if (!$this->is_origin_preset_map($target)) {
245 + return $this->normalize_preset_collection($target);
246 + }
247 + $presets = [];
248 + foreach ($target as $origin_presets) {
249 + if (!is_array($origin_presets)) {
250 + continue;
251 + }
252 + foreach ($this->normalize_preset_collection($origin_presets) as $preset) {
253 + $presets[] = $preset;
254 + }
255 + }
256 + return $presets;
257 + }
258 + private function set_preset_list_for_origin(array &$data, array $path, string $origin, array $presets): void
259 + {
260 + $target =& $data;
261 + foreach ($path as $segment) {
262 + if (!isset($target[$segment]) || !is_array($target[$segment])) {
263 + $target[$segment] = [];
264 + }
265 + $target =& $target[$segment];
266 + }
267 + if (!$this->is_origin_preset_map($target)) {
268 + $target = [$origin => $this->normalize_preset_collection($target)];
269 + }
270 + $target[$origin] = $presets;
271 + }
272 + private function is_origin_preset_map(array $presets): bool
273 + {
274 + $origin_keys = ['default', 'blocks', 'theme', 'custom'];
275 + foreach ($origin_keys as $origin_key) {
276 + if (array_key_exists($origin_key, $presets)) {
277 + return \true;
278 + }
279 + }
280 + return \false;
281 + }
282 + private function normalize_preset_collection(array $presets): array
283 + {
284 + if ($this->is_list_array($presets)) {
285 + return $presets;
286 + }
287 + $normalized_presets = [];
288 + foreach ($presets as $key => $preset) {
289 + if (!is_array($preset)) {
290 + continue;
291 + }
292 + if ((!isset($preset['slug']) || !is_string($preset['slug']) || trim($preset['slug']) === '') && (is_string($key) || is_int($key))) {
293 + $preset['slug'] = (string) $key;
294 + }
295 + $normalized_presets[] = $preset;
296 + }
297 + return $normalized_presets;
298 + }
299 + private function normalize_preset_collection_with_resolved_slugs(array $presets): array
300 + {
301 + $normalized_presets = [];
302 + $seen_slugs = [];
303 + foreach ($this->normalize_preset_collection($presets) as $preset) {
304 + if (!is_array($preset)) {
305 + continue;
306 + }
307 + $normalized_preset = $this->normalize_preset_for_merge($preset);
308 + if ($normalized_preset === null) {
309 + continue;
310 + }
311 + $slug = $normalized_preset['slug'];
312 + if (isset($seen_slugs[$slug])) {
313 + continue;
314 + }
315 + $normalized_presets[] = $normalized_preset;
316 + $seen_slugs[$slug] = \true;
317 + }
318 + return $normalized_presets;
319 + }
320 + private function merge_preset_lists(array $current_presets, array $windpress_presets): array
321 + {
322 + $merged_presets = [];
323 + $seen_slugs = [];
324 + foreach ([$current_presets, $windpress_presets] as $preset_group) {
325 + foreach ($preset_group as $preset) {
326 + if (!is_array($preset)) {
327 + continue;
328 + }
329 + $normalized_preset = $this->normalize_preset_for_merge($preset);
330 + if ($normalized_preset === null) {
331 + continue;
332 + }
333 + $slug = $normalized_preset['slug'];
334 + if (isset($seen_slugs[$slug])) {
335 + continue;
336 + }
337 + $merged_presets[] = $normalized_preset;
338 + $seen_slugs[$slug] = \true;
339 + }
340 + }
341 + return $merged_presets;
342 + }
343 + private function normalize_preset_for_merge(array $preset): ?array
344 + {
345 + $slug = $this->resolve_preset_slug($preset);
346 + if ($slug === null) {
347 + return null;
348 + }
349 + $preset['slug'] = $slug;
350 + return $preset;
351 + }
352 + private function resolve_preset_slug(array $preset): ?string
353 + {
354 + if (isset($preset['slug']) && is_string($preset['slug'])) {
355 + $slug = trim($preset['slug']);
356 + if ($slug !== '') {
357 + return $slug;
358 + }
359 + }
360 + $fallback_keys = ['name', 'fontFamily', 'size', 'color', 'shadow'];
361 + foreach ($fallback_keys as $fallback_key) {
362 + if (!isset($preset[$fallback_key]) || !is_string($preset[$fallback_key])) {
363 + continue;
364 + }
365 + $slug = sanitize_title($preset[$fallback_key]);
366 + if ($slug !== '') {
367 + return $slug;
368 + }
369 + }
370 + return null;
371 + }
372 + private function get_array_at_path(array $data, array $path): ?array
373 + {
374 + $target = $data;
375 + foreach ($path as $segment) {
376 + if (!isset($target[$segment]) || !is_array($target[$segment])) {
377 + return null;
378 + }
379 + $target = $target[$segment];
380 + }
381 + return $target;
382 + }
383 + private function is_list_array(array $array): bool
384 + {
385 + $expected_key = 0;
386 + foreach ($array as $key => $_value) {
387 + if ($key !== $expected_key) {
388 + return \false;
389 + }
390 + $expected_key++;
391 + }
392 + return \true;
59 393 }
60 394 }