PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.2.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.2.0
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 / Utils / CSSGenerator.php

CSSGenerator.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.2.0, at includes/Utils/CSSGenerator.php

488 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs\Utils;
4
5 class CSSGenerator {
6 const VERSION = '1.0.0';
7
8 /**
9 * Stores the global variables
10 *
11 * @var array<string, string|int>
12 */
13 protected $variables = [];
14
15 /**
16 * Store the declared CSS blocks and comments
17 *
18 * @var array<int, array<mixed>>
19 */
20 protected $blocks = [];
21
22 /**
23 * Store the config options
24 *
25 * @var array<string, string|int>
26 */
27 protected $options = [];
28
29 /**
30 * Store the indentation level
31 *
32 * @var int
33 */
34 protected $indent_level = 0;
35
36 /**
37 * @var bool
38 */
39 protected $compress_output;
40
41 /**
42 * Stores the last generated CSS in prettry format (with tabs, spaces and line breaks).
43 *
44 * @var string|null
45 */
46 protected $cache_pretty;
47
48 /**
49 * Stores the last generated CSS in minified format
50 *
51 * @var string|null
52 */
53 protected $cache_compressed;
54
55 /**
56 * Mods Data
57 * @var array $mods
58 */
59 private $mods;
60
61 /**
62 * Class constructor
63 *
64 * @param array $options
65 */
66
67 public function __construct( $mods = [], $options = [] ) {
68 $default_options = [
69 'indent_size' => 4, // only works with indent_style = space
70 'indent_style' => 'space' // or "tab"
71 ];
72 $this->options = array_merge( $default_options, $options );
73 $this->mods = $mods;
74 }
75
76 /**
77 * Returns the generated CSS
78 *
79 * @param boolean $compressed
80 * @return string
81 */
82 public function get_output( $compressed = false ) {
83 $result = '';
84 if ( ! $compressed ) {
85 $result = $this->cache_pretty = $this->generate( false );
86 } else {
87 $result = $this->cache_compressed = $this->generate( true );
88 }
89 return $result;
90 }
91
92 /**
93 * Print anything (be careful)
94 *
95 * @param string $string
96 * @return $this
97 */
98 public function add_raw( $string ) {
99 $this->blocks[] = [
100 'type' => 'raw',
101 'raw' => $string
102 ];
103 $this->clear_cache();
104 return $this;
105 }
106
107 /**
108 * Print a code comment
109 *
110 * @param string $string
111 * @return $this
112 */
113 public function add_comment( $string ) {
114 $this->blocks[] = [
115 'type' => 'comment',
116 'comment' => $string
117 ];
118 $this->clear_cache();
119 return $this;
120 }
121
122 /**
123 * Declares a CSS rule
124 *
125 * @param string|string[] $selectors
126 * @param array<string, string|numeric> $declarations
127 * @return $this
128 */
129 public function add_rule( $selectors, $declarations ) {
130 $selectors = ! is_array( $selectors ) ? [ $selectors ] : $selectors;
131
132 if ( ! empty( $declarations ) ) {
133 $this->blocks[] = [
134 'type' => 'rule',
135 'selectors' => $selectors,
136 'declarations' => $declarations
137 ];
138 }
139
140 $this->clear_cache();
141 return $this;
142 }
143
144 /**
145 * Declares a global variable (in :root)
146 *
147 * @param string $name The variable name
148 * @param string|numeric-string $value The variable value
149 * @return $this
150 */
151 public function root_variable( $name, $value ) {
152 $this->variables[ trim( $name ) ] = trim( strval( $value ) );
153 $this->clear_cache();
154 return $this;
155 }
156
157 /**
158 * Opens a block like @media, @supports, etc.
159 *
160 * @param string $name
161 * @param string $props
162 * @return $this
163 */
164 public function open_block( $name, $props = '' ) {
165 $this->blocks[] = [
166 'type' => 'open',
167 'name' => $name,
168 'props' => $props
169 ];
170
171 $this->clear_cache();
172 return $this;
173 }
174
175 /**
176 * Closes the last opened block
177 *
178 * @return $this
179 */
180 public function close_block() {
181 $this->blocks[] = [
182 'type' => 'close'
183 ];
184 $this->clear_cache();
185 return $this;
186 }
187
188 /**
189 * @return void
190 */
191 public function clear_cache() {
192 $this->cache_compressed = null;
193 $this->cache_pretty = null;
194 }
195
196 /**
197 * Delete all declared blocks and resets the instance.
198 *
199 * @return void
200 */
201 public function reset() {
202 $this->clear_cache();
203 $this->blocks = [];
204 }
205
206 /**
207 * Alias for self::escape
208 *
209 * @param string $selector
210 * @return string
211 */
212 public function esc( $selector ) {
213 return self::escape( $selector );
214 }
215
216 /**
217 * Returns one unit of indentation
218 *
219 * @return string
220 */
221 public function get_indent_unit() {
222 if ( 'space' === $this->options['indent_style'] ) {
223 $size = intval( $this->options['indent_size'] );
224 return str_repeat( ' ', max( $size, 2 ) );
225 }
226 return "\t";
227 }
228
229 /**
230 * Returns the current indentation (if not generating a minified code).
231 *
232 * @return string
233 */
234 protected function tab() {
235 if ( ! $this->compress_output ) {
236 if ( $this->indent_level > 0 ) {
237 return str_repeat( $this->get_indent_unit(), $this->indent_level );
238 }
239 }
240 return '';
241 }
242
243 /**
244 * Build the CSS code
245 *
246 * @param boolean $compressed
247 * @return string
248 */
249 protected function generate( $compressed = false ) {
250 $this->indent_level = 0;
251 $this->compress_output = $compressed;
252
253 $br = $this->compress_output ? '' : "\n";
254 $s = $this->compress_output ? '' : ' ';
255 $open = $s . '{' . $br;
256 $close = '}' . $br;
257 $output = '';
258 $has_variables = count( $this->variables ) > 0;
259
260 if ( $has_variables ) {
261 $output .= ':root' . $open;
262 ++$this->indent_level;
263 foreach ( $this->variables as $name => $value ) {
264 $output .= $this->tab();
265 $output .= "--$name:$s$value;$br";
266 }
267 $output .= $close . $br;
268 --$this->indent_level;
269 }
270
271 foreach ( $this->blocks as $block ) {
272 switch ( $block['type'] ) {
273 case 'comment':
274 if ( ! $compressed ) {
275 $output .= $this->tab();
276 $output .= "/* {$block['comment']} */$br";
277 }
278 break;
279 case 'raw':
280 $output .= $block['raw'];
281 break;
282 case 'rule':
283 $output .= $this->tab();
284 $selectors = array_map( 'trim', $block['selectors'] );
285 $output .= implode( ",$br" . $this->tab(), $selectors );
286 $output .= $open;
287 ++$this->indent_level;
288 foreach ( $block['declarations'] as $key => $value ) {
289 if ( empty( $key ) ) {
290 continue;
291 }
292 $output .= $this->tab();
293 $output .= trim( $key ) . ":$s" . trim( $value ) . ";$br";
294 }
295 --$this->indent_level;
296 $output .= $this->tab() . $close;
297 break;
298 case 'open':
299 $output .= $this->tab();
300 $output .= '@' . $block['name'];
301 $output .= '' !== $block['props'] ? " {$block['props']}" : '';
302 $output .= "$open";
303 ++$this->indent_level;
304 break;
305 case 'close':
306 --$this->indent_level;
307 $output .= $this->tab() . $close;
308 break;
309 default:
310 break;
311 }
312 }
313 while ( $this->indent_level > 0 ) {
314 --$this->indent_level;
315 $output .= $this->tab() . $close;
316 }
317
318 return $output;
319 }
320
321 public function eligible_properties( $array, $suffix = '', &$initial = [] ) {
322 array_walk(
323 $array,
324 function ( $value, $key ) use ( &$initial, $suffix ) {
325 if ( ! is_array( $value ) ) {
326 $_mod_key = $value;
327 } elseif ( is_array( $value ) && ! empty( $value['id'] ) ) {
328 $_mod_key = $value['id'];
329 }
330
331 if ( strpos( $_mod_key, '%' ) !== false ) {
332 $_origin_mod_key = $_mod_key;
333 preg_match_all( '/%([a-zA-Z0-9_]*)%/', $_mod_key, $matches );
334 $_mod_key = $matches[1];
335 }
336
337 if ( is_array( $_mod_key ) ) {
338 $_value = [];
339 foreach ( $_mod_key as $_mod_key_child ) {
340 if ( isset( $this->mods[ $_mod_key_child ] ) ) {
341 $_value[ $_mod_key_child ] = $this->mods[ $_mod_key_child ];
342 } elseif ( ! isset( $this->mods[ $_mod_key_child ] ) ) {
343 $_value[ $_mod_key_child ] = $_mod_key_child;
344 }
345 }
346 } elseif ( isset( $this->mods[ $_mod_key ] ) ) {
347 $_value = $this->mods[ $_mod_key ];
348 if ( empty( $_value ) && $_value === '' ) {
349 return;
350 }
351 } elseif ( ! isset( $this->mods[ $_mod_key ] ) ) {
352 $_value = $_mod_key;
353 }
354
355 if ( ! empty( $matches ) ) {
356 $_value = str_replace( $matches[0], $_value, $_origin_mod_key );
357 }
358
359 $initial[ $key ] = $this->eligible_value( $key, $_value, $suffix );
360
361 if ( is_array( $value ) ) {
362 $initial = array_merge( $initial, $this->eligible_properties( $value['properties'], $suffix, $initial ) );
363 }
364 }
365 );
366 return $initial;
367 }
368
369 public function eligible_value( $key, $value, $suffix ) {
370 if ( strpos( $key, 'image' ) !== false ) {
371 $value = 'url("' . $value . '")';
372 }
373
374 $_value = $value;
375 if ( is_string( $value ) ) {
376 $_value = json_decode( $value, true );
377 }
378
379 if ( null != $_value && is_array( $_value ) ) {
380 $_values = [];
381 foreach ( $_value as $single_value ) {
382 $_values[] = $single_value . $suffix;
383 }
384
385 $value = implode( ' ', $_values );
386
387 $suffix = '';
388 }
389
390 if ( ! empty( $suffix ) ) {
391 $value .= $suffix;
392 }
393
394 return $value;
395 }
396
397 public function properties( $properties, $suffix = '' ) {
398 if ( empty( $properties ) ) {
399 return [];
400 }
401
402 $_properties = [];
403 return $this->eligible_properties( $properties, $suffix, $_properties );
404 }
405
406 /**
407 * Escapes a CSS rule selector. Based on https://github.com/mathiasbynens/CSS.escape
408 *
409 * @static
410 * @param string $selector
411 * @return string
412 */
413 public static function escape( $selector ) {
414 if ( '' === $selector ) {
415 return $selector;
416 }
417
418 $length = mb_strlen( $selector );
419 $result = '';
420 $index = -1;
421 $first_char = mb_substr( $selector, 0, 1 );
422 $first_char_code = ord( $first_char );
423
424 if (
425 // If the character is the first character and is a `-` (U+002D), and
426 // there is no second character, […]
427 1 === $length &&
428 0x2D === $first_char_code
429 ) {
430 return '\\' . $selector;
431 }
432 while ( ++$index < $length ) {
433 $char = mb_substr( $selector, $index, 1 );
434 if ( '' === $char ) {
435 continue;
436 }
437
438 $char_code = ord( $char );
439
440 // If the character is NULL
441 if ( 0 === $char_code ) {
442 $result .= "\u{FFFD}";
443 continue;
444 }
445
446 if (
447 // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is U+007F, […]
448 $char_code <= 0x1F || 0x7F === $char_code ||
449 // If the character is the first character and is in the range [0-9]
450 // (U+0030 to U+0039), […]
451 ( 0 === $index && $char_code >= 0x30 && $char_code <= 0x39 ) ||
452 // If the character is the second character and is in the range [0-9]
453 // (U+0030 to U+0039) and the first character is a `-` (U+002D), […]
454 (
455 1 === $index &&
456 $char_code >= 0x0030 && $char_code <= 0x0039 &&
457 0x002D === $first_char_code
458 )
459 ) {
460 // https://drafts.csswg.org/cssom/#escape-a-character-as-code-point
461 $result .= '\\' . dechex( $char_code ) . ' ';
462 continue;
463 }
464
465 if (
466 // If the character is not handled by one of the above rules and is
467 // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or
468 $char_code >= 0x0080 || 0x002D === $char_code || 0x005F === $char_code ||
469 // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to U+005A)
470 $char_code >= 0x0030 && $char_code <= 0x0039 ||
471 $char_code >= 0x0041 && $char_code <= 0x005A ||
472 // , or [a-z] (U+0061 to U+007A), […]
473 $char_code >= 0x0061 && $char_code <= 0x007A
474 ) {
475 // the character itself
476 $result .= $char;
477 continue;
478 }
479
480 // Otherwise, the escaped character.
481 // https://drafts.csswg.org/cssom/#escape-a-character
482 $result .= '\\' . $char;
483 }
484
485 return $result;
486 }
487 }
488