PluginProbe
WindPress – Tailwind CSS integration for WordPress / trunk
WindPress – Tailwind CSS integration for WordPress vtrunk
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 3.0.7 All 142 releases
windpress / src / Integration / Gutenberg / Editor.php

Editor.php in WindPress – Tailwind CSS integration for WordPress trunk, at src/Integration/Gutenberg/Editor.php

395 lines 16.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /*
4 * This file is part of the WindPress package.
5 *
6 * (c) Joshua Gugun Siagian <suabahasa@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11 declare (strict_types=1);
12 namespace WindPress\WindPress\Integration\Gutenberg;
13
14 use Exception;
15 use WIND_PRESS;
16 use WindPress\WindPress\Core\Runtime;
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;
21 /**
22 * @author Joshua Gugun Siagian <suabahasa@gmail.com>
23 */
24 class Editor
25 {
26 public function __construct()
27 {
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 }
33 }
34 public function enqueue_block_editor_assets()
35 {
36 $screen = get_current_screen();
37 if (is_admin() && $screen->is_block_editor()) {
38 add_action('admin_head', fn() => $this->admin_head(), 1000001);
39 }
40 }
41 public function admin_head()
42 {
43 Runtime::get_instance()->print_windpress_metadata();
44 Runtime::get_instance()->enqueue_play_cdn();
45 if (strpos($_SERVER['REQUEST_URI'], 'site-editor.php') !== \false) {
46 // handle the canvas
47 Vite::assets()->enqueue('resources/integration/gutenberg/site-editor.js', ['handle' => WIND_PRESS::WP_OPTION . ':integration-gutenberg-site-editor', 'in_footer' => \true]);
48 } else {
49 // handle the canvas
50 Vite::assets()->enqueue('resources/integration/gutenberg/post-editor.js', ['handle' => WIND_PRESS::WP_OPTION . ':integration-gutenberg-post-editor', 'in_footer' => \true]);
51 }
52 $handle = WIND_PRESS::WP_OPTION . ':integration-gutenberg-block-editor';
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
68 document.addEventListener('DOMContentLoaded', function () {
69 wp.hooks.addFilter('windpressgutenberg-autocomplete-items-query', 'windpressgutenberg', async (autocompleteItems, text) => {
70 const windpress_suggestions = await window.windpress.module.autocomplete.query(text);
71
72 return [...windpress_suggestions, ...autocompleteItems];
73 });
74 });
75 JS
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;
393 }
394 }
395