PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 3.5.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v3.5.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 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 3.5.2, at includes/Utils/CSSGenerator.php

487 lines 13.7 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( $array, function ( $value, $key ) use ( &$initial, $suffix ) {
323 if ( ! is_array( $value ) ) {
324 $_mod_key = $value;
325 } elseif ( is_array( $value ) && ! empty( $value['id'] ) ) {
326 $_mod_key = $value['id'];
327 }
328
329 if ( strpos( $_mod_key, '%' ) !== false ) {
330 $_origin_mod_key = $_mod_key;
331 preg_match_all( '/%([a-zA-Z0-9_]*)%/', $_mod_key, $matches );
332 $_mod_key = $matches[1];
333 }
334
335 if ( is_array( $_mod_key ) ) {
336 $_value = [];
337 foreach ( $_mod_key as $_mod_key_child ) {
338 if ( isset( $this->mods[$_mod_key_child] ) ) {
339 $_value[$_mod_key_child] = $this->mods[$_mod_key_child];
340 } elseif ( ! isset( $this->mods[$_mod_key_child] ) ) {
341 $_value[$_mod_key_child] = $_mod_key_child;
342 }
343 }
344 } else {
345 if ( isset( $this->mods[$_mod_key] ) ) {
346 $_value = $this->mods[$_mod_key];
347 if ( empty( $_value ) && $_value === '' ) {
348 return;
349 }
350 } elseif ( ! isset( $this->mods[$_mod_key] ) ) {
351 $_value = $_mod_key;
352 }
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 return $initial;
366 }
367
368 public function eligible_value( $key, $value, $suffix ) {
369 if ( strpos( $key, 'image' ) !== false ) {
370 $value = 'url("' . $value . '")';
371 }
372
373 $_value = $value;
374 if ( is_string( $value ) ) {
375 $_value = json_decode( $value, true );
376 }
377
378 if ( null != $_value && is_array( $_value ) ) {
379 $_values = [];
380 foreach ( $_value as $single_value ) {
381 $_values[] = $single_value . $suffix;
382 }
383
384 $value = implode( ' ', $_values );
385
386 $suffix = '';
387 }
388
389 if ( ! empty( $suffix ) ) {
390 $value .= $suffix;
391 }
392
393 return $value;
394 }
395
396 public function properties( $properties, $suffix = '' ) {
397 if ( empty( $properties ) ) {
398 return [];
399 }
400
401 $_properties = [];
402 return $this->eligible_properties( $properties, $suffix, $_properties );
403 }
404
405 /**
406 * Escapes a CSS rule selector. Based on https://github.com/mathiasbynens/CSS.escape
407 *
408 * @static
409 * @param string $selector
410 * @return string
411 */
412 public static function escape( $selector ) {
413 if ( '' === $selector ) {
414 return $selector;
415 }
416
417 $length = mb_strlen( $selector );
418 $result = '';
419 $index = -1;
420 $first_char = mb_substr( $selector, 0, 1 );
421 $first_char_code = ord( $first_char );
422
423 if (
424 // If the character is the first character and is a `-` (U+002D), and
425 // there is no second character, […]
426 1 === $length &&
427 0x2D === $first_char_code
428 ) {
429 return '\\' . $selector;
430 }
431 while ( ++$index < $length ) {
432 $char = mb_substr( $selector, $index, 1 );
433 if ( '' === $char ) {
434 continue;
435 }
436
437 $char_code = ord( $char );
438
439 // If the character is NULL
440 if ( 0 === $char_code ) {
441 $result .= "\u{FFFD}";
442 continue;
443 }
444
445 if (
446 // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is U+007F, […]
447 $char_code <= 0x1F || 0x7F === $char_code ||
448 // If the character is the first character and is in the range [0-9]
449 // (U+0030 to U+0039), […]
450 ( 0 === $index && $char_code >= 0x30 && $char_code <= 0x39 ) ||
451 // If the character is the second character and is in the range [0-9]
452 // (U+0030 to U+0039) and the first character is a `-` (U+002D), […]
453 (
454 1 === $index &&
455 $char_code >= 0x0030 && $char_code <= 0x0039 &&
456 0x002D === $first_char_code
457 )
458 ) {
459 // https://drafts.csswg.org/cssom/#escape-a-character-as-code-point
460 $result .= '\\' . dechex( $char_code ) . ' ';
461 continue;
462 }
463
464 if (
465 // If the character is not handled by one of the above rules and is
466 // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or
467 $char_code >= 0x0080 || 0x002D === $char_code || 0x005F === $char_code ||
468 // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to U+005A)
469 $char_code >= 0x0030 && $char_code <= 0x0039 ||
470 $char_code >= 0x0041 && $char_code <= 0x005A ||
471 // , or [a-z] (U+0061 to U+007A), […]
472 $char_code >= 0x0061 && $char_code <= 0x007A
473 ) {
474 // the character itself
475 $result .= $char;
476 continue;
477 }
478
479 // Otherwise, the escaped character.
480 // https://drafts.csswg.org/cssom/#escape-a-character
481 $result .= "\\" . $char;
482 }
483
484 return $result;
485 }
486 }
487