PluginProbe
MaxButtons – Create buttons / 9.7.7
MaxButtons – Create buttons v9.7.7
6.23 6.24 6.25 6.26 6.26.1 6.27 6.28 6.3 6.4 6.5 6.6 6.7 6.8 6.9 7.0 7.1 7.1.1 7.1.2 7.1.3 7.10 7.11 7.13 7.13.1 7.13.2 7.13.3 All 100 releases
maxbuttons / classes / maxCSSParser.php

maxCSSParser.php in MaxButtons – Create buttons 9.7.7, at classes/maxCSSParser.php

744 lines 19.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 declare(strict_types=1);
3 namespace MaxButtons;
4 use MaxButtons\ScssPhp\ScssPhp\Compiler as Compiler;
5
6 defined('ABSPATH') or die('No direct access permitted');
7
8 /* Class to Parse CSS. Load as array with diffent pseudo-types,
9 ability to add nested and new root blocks
10 parses via scss
11 ability to use complicated css stuff via scss mixins parsing like gradient
12 auto-discovery for -unit field types to set units (like px, or %)
13 auto-discovery of fields via domobj.
14
15 */
16
17 use \Exception as Exception;
18
19 class maxCSSParser
20 {
21 protected $struct = array();
22 protected $domObj = '';
23 protected $pseudo = array("hover","active","responsive");
24
25 protected $data;
26 protected $screens;
27 protected $output_css;
28
29 protected $has_compile_error = false;
30 protected $compile_error = array();
31
32 protected $inline = array();
33 protected $responsive = array();
34
35 public $anchor_class = '.maxbutton'; // used for matching buttons in parse_part
36
37 // settings
38 protected $elements_ignore_zero = array(
39 'text-shadow-left',
40 'text_shadow-top',
41 'text-shadow-width',
42 'box-shadow-offset-left',
43 'box-shadow-offset-top',
44 'box-shadow-width',
45 'box-shadow-spread',
46 ); // items to ignore if value is zero, otherwise they become unremovable ( where 0 is still something on display)
47
48 protected $important = false;
49
50 // log possible problems and incidents for debugging;
51 protected $parse_log = array();
52
53 public function __construct()
54 {
55 //$root[] = array("a" => array("hover","active","responsive"));
56 MB()->load_library('scss');
57 }
58
59 public function loadDom($domObj)
60 {
61 $this->domObj = $domObj;
62
63 $root = $domObj->root;
64
65 $struct[$root->tag] = array();
66
67 $children = $root->children();
68
69 if (count($children) > 0)
70 $struct[$root->tag] = $this->loadRecursive(array(), $children);
71
72
73 // find the full and complete statement class defining maxbutton. This is needed for proper parsing on parse_part.
74 $anchor_element = $root->find('.maxbutton', 0);
75 if (! is_null($anchor_element))
76 {
77 $anchor_class = str_replace(' ', '.', $anchor_element->class);
78 $this->anchor_class = $anchor_class;
79 }
80
81 $this->struct = $struct;
82
83 }
84
85
86 protected function loadRecursive($struct, $children)
87 {
88 foreach($children as $domChild)
89 {
90
91 $class = $domChild->class;
92
93 // Class doesn't have to be set / string
94 if (is_string($class))
95 {
96 $class = str_replace(" ",".", $class); // combine seperate classes
97 }
98 $struct[$class]["tag"] = $domChild->tag;
99
100 $child_children = $domChild->children();
101
102 if (count($child_children) > 0)
103 {
104
105 $struct[$class]["children"] = $this->loadRecursive(array(), $child_children);
106 }
107 }
108
109 return $struct;
110 }
111
112 public function setScreens($screens)
113 {
114 $this->screens = $screens;
115 }
116
117 public function parse($data)
118 {
119 $this->clear();
120
121 $struct = $this->struct;
122
123 $this->data = $data;
124
125 if (isset($data["settings"])) // room for settings in parser
126 {
127 $settings = $data["settings"];
128 $this->important = (isset($settings["important"])) ? $settings["important"] : false;
129
130 unset($this->data["settings"]);
131 }
132
133 $elements = array_shift($struct); // first element is a 'stub' root.
134
135 if ( is_null($elements) )
136 return;
137
138 foreach($elements as $el => $el_data)
139 {
140
141 $this->parse_part($el,$el_data);
142 }
143
144 $this->parse_responsive($elements);
145
146 maxUtils::startTime('compile CSS');
147 $css = $this->compile($this->output_css);
148
149 maxUtils::endTime('compile CSS');
150
151 return $css;
152 }
153
154 // reset output values.
155 protected function clear()
156 {
157 $this->data = '';
158 $this->output_css = '';
159 $this->inline = array();
160 $this->responsive = array();
161 }
162
163 public function compile($css)
164 {
165 $scss = new Compiler();
166 $scss->setImportPaths(MB()->get_plugin_path() . "assets/scss");
167
168 $minify = get_option("maxbuttons_minify", 1);
169
170 if ($minify == 1)
171 {
172 $scss->setOutputStyle(\MaxButtons\ScssPhp\ScssPhp\OutputStyle::COMPRESSED);
173 }
174
175 $compile = ' @import "_mixins.scss";' . $css;
176
177 //maxUtils::addTime("CSSParser: Compile start ");
178 try
179 {
180 $css = $scss->compileString($compile)->getCss();
181 } catch (\Exception $e) {
182 $this->has_compile_error = true;
183 $this->compile_error = array('error' => $e,
184 // 'backtrace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 4),
185 'string' => $compile,
186 );
187 $css = $this->output_css;
188 }
189
190 return $css;
191 }
192
193 public function get_compile_errors()
194 {
195 if ($this->has_compile_error)
196 {
197 return $this->compile_error;
198 }
199 return false;
200 }
201
202 // Returns the CSS selector for the responsive screen. Used for custom CSS.
203 public function getResponsiveScreenSelector($screen)
204 {
205 // $deef = $screen->;
206 $def = $this->renderResponsiveDefinition();
207 }
208
209 /** Element is the current element that being parsed. El_add is the parent element that should be put before the subpart
210 * @param $element CSS class Definition
211 * @param $el_data DomDoc Element data of this element.
212 * @param $el_add Indicated child element .css to add to def. to make proper cascase
213 * @param $screenName String Name of the screen when parsing responsive view.
214 */
215 protected function parse_part($element, $el_data, $el_add = '', $screenName = false)
216 {
217 maxUtils::addTime("CSSParser: Parse $element ");
218
219 $data = $this->data;
220 $tag = $el_data["tag"];
221
222 // Element can be unset / int if the tag doesn't have element / tag associated so needs no further parsing (html element)
223 if (! is_string($element))
224 {
225 return;
226 }
227
228 // returns all data from this element.
229 $element_data = $this->findData($data, $element);
230
231 if ($screenName !== false)
232 $element_data = $this->findResponsiveData($element_data, $screenName);
233
234 // not using scss selectors here since there is trouble w/ the :pseudo selector, which should be put on the maxbutton / a tag.
235 if ($element != '')
236 { $el_add .= " ." . $element;
237
238 }
239 if (isset($element_data["responsive"]))
240 {
241 $responsive = $element_data["responsive"]; // doing that at the end
242 unset($element_data["responsive"]);
243
244 $this->responsive[$el_add] = $responsive;
245 }
246
247 foreach($element_data as $pseudo => $values)
248 {
249
250 if ($pseudo != 'normal')
251 {
252 // select the maxbutton case, ending with either space or next class -dot.
253 // Anchor class in default situation should be .maxbutton
254 $anchor_class = $this->anchor_class;
255
256 $count = 0;
257
258 /* If PS Selector replacement doesn't match anchor class selector this probably means the parse is done in a higher level
259 e.g. container level, so no proper will be set. In case 0 count replacement, just put it on current */
260 $ps_selector = preg_replace('/' . $anchor_class . '$|' . $anchor_class . '([.| ])/i',"$anchor_class:$pseudo\$1",$el_add, -1, $count);
261
262 if ($count === 0)
263 {
264 $ps_selector = $el_add . ":" . $pseudo;
265 }
266
267
268 $this->output_css .= "$ps_selector{ ";
269 }
270 else {
271 $this->output_css .= "$el_add{ ";
272 }
273
274 $values = $this->combineStatements($values);
275 $values = $this->doMixins($values);
276
277 $this->inline[$pseudo][$element] = $values;
278
279 foreach($values as $cssTag => $cssVal)
280 {
281
282 $statement = $this->parse_cssline($values,$cssTag,$cssVal); ///"$cssTag $css_sep $cssVal$unit$css_end ";
283
284 if ($statement)
285 {
286 $this->output_css .= $statement ;
287
288 /* if (! isset($this->inline[$pseudo][$element]))
289 $this->inline[$pseudo][$element] = array();
290
291 if (! isset($this->inline[$pseudo][$element][$cssTag]))
292 $this->inline[$pseudo][$element][$cssTag] = '';
293
294 $this->inline[$pseudo][$element][$cssTag] = $cssVal; */
295 }
296 }
297
298 $this->output_css .= "} ";
299 }
300 if (isset($el_data["children"]))
301 {
302 foreach($el_data["children"] as $child_id => $child_data)
303 {
304 $this->parse_part($child_id, $child_data, $el_add, $screenName);
305 }
306 }
307
308
309 }
310
311 protected function parse_cssline($values, $cssTag, $cssVal, $css_end = ';')
312 {
313
314 // unit check - two ways; either unitable items is first or unit declaration.
315 if (isset($values[$cssTag . "_unit"]))
316 {
317 $unit = $values[$cssTag . "_unit"];
318 }
319 elseif(strpos($cssTag, "_unit") !== false)
320 {
321 return false; // no print, should be found under first def.
322 }
323 else $unit = '';
324
325
326 $important = ($this->is_important()) ? " !important" : "";
327 $important = ($cssTag == '@include') ? "" : $important; // mixin's problem, no checking here.
328
329 $css_sep = ($cssTag == '@include') ? $css_sep = '' : ':';
330
331 if ($cssVal == 0 && in_array($cssTag, $this->elements_ignore_zero))
332 return false;
333
334 if($cssVal !== '' && $cssTag !== '')
335 {
336 $statement = "$cssTag $css_sep $cssVal$unit$important$css_end ";
337 return $statement;
338 }
339 return false;
340
341 }
342
343 protected function parse_responsive($elements)
344 {
345
346 $responsive = $this->responsive;
347 if (! is_array($responsive) || count($responsive) == 0)
348 return;
349
350
351 $query_array = array();
352 $screens = $this->screens;
353
354 $definitions = array();
355
356 // Find the responsive definitions and remove that part.
357 foreach($responsive as $part => $screen_ids)
358 {
359 foreach($screen_ids as $screen_id => $screen_data)
360 {
361
362 if (isset($screens[$screen_id]) && $screens[$screen_id]->is_responsive() )
363 {
364 if (isset($screen_data['definition']))
365 {
366 $definitions[$screen_id]['def'] = $this->renderResponsiveDefinition($screen_data['definition']);
367 // Hide screen on this size.
368 if (isset($screen_data['definition']['hide_screen']) && $screen_data['definition']['hide_screen'] == 1)
369 {
370 $this->doHideScreen($screen_id);
371 // This line
372 $screen_data['data'] = array('normal' => array('display' => 'none') );
373 }
374 unset($screen_data['definition']);
375 }
376 if (count($screen_data) > 0)
377 $definitions[$screen_id]['data'][$part] = $screen_data;
378
379
380 }
381 }
382 }
383
384 foreach($definitions as $screen_id => $def_array)
385 {
386 if (isset($def_array['def']) && isset($def_array['data']))
387 {
388 $this->parse_responsive_definition($elements, $def_array['def'], $screen_id);
389 }
390 }
391 }
392
393 /* Hide screen by removing all data and putting display none there */
394 private function doHideScreen($screen_id)
395 {
396 foreach($this->data as $element => $eldata)
397 {
398 if (isset($eldata['responsive']) && isset($eldata['responsive'][$screen_id]))
399 {
400 $this->data[$element]['responsive'][$screen_id] = array('normal' => array('display' => 'none') );
401 }
402 }
403
404 }
405
406 protected function renderResponsiveDefinition($definition)
407 {
408 $def = 'only screen ';
409 if (isset($definition['min_width']))
410 $def .= ' and (min-width: ' . $definition['min_width'] . 'px) ';
411 if (isset($definition['max_width']))
412 $def .= ' and (max-width: ' . $definition['max_width'] . 'px) ';
413
414 return $def;
415 }
416
417 /**
418 * @param $element DOMDoc to feed parse_part with
419 * @param $qdef Query Definition
420 * @param $vdata Data. -probably not needed */
421 protected function parse_responsive_definition($elements, $qdef, $screenName)
422 {
423 if (! isset($qdef) || $qdef == '') {
424
425 return; // no definition.
426 }
427
428 $this->output_css .= "@media ". $qdef . " { ";
429
430 foreach($elements as $el => $el_data)
431 $this->parse_part($el, $el_data, '', $screenName);
432
433 $this->output_css .= "}";
434 }
435
436 private function is_important()
437 {
438
439 if ($this->important == 1)
440 return true;
441 else
442 return false;
443 }
444
445 /* Find Data in the dataset for this specific CSS selector */
446 protected function findData($data, $el)
447 {
448 $classes = explode(".", $el);
449
450 foreach($data as $part => $values)
451 {
452 if (in_array($part, $classes))
453 {
454 return $data[$part];
455 }
456 }
457 return array();
458 }
459
460 /** Filters a Data definition found with FindData for the correct ScreenName and returns that */
461 protected function findResponsiveData($data, $screenName)
462 {
463 if (isset($data["responsive"]) && isset($data['responsive'][$screenName]))
464 {
465 $resp_data = $data['responsive'][$screenName];
466 if (isset($resp_data['definition'])) // don't need the definition, already output that.
467 unset($resp_data['definition']);
468
469 return $resp_data;
470 }
471
472 return array();
473 }
474
475 protected function doMixins($values)
476 {
477 $mixins = array("gradient", "box-shadow", "text-shadow", "keyframes");
478
479 foreach($mixins as $mixin)
480 {
481
482 $results = preg_grep("/^$mixin/i",array_keys($values) );
483 if (count($results) === 0)
484 continue; // no mixins.
485
486 $mixin_array = array();
487 foreach($results as $result)
488 {
489 $mixin_array[$result] = $values[$result];
490 }
491
492 if (count($mixin_array) > 0)
493 {
494 switch($mixin)
495 {
496 case "gradient":
497 $values = $this->mixin_gradient($mixin_array, $values);
498 break;
499 case "box-shadow":
500 $values = $this->mixin_boxshadow($mixin_array, $values);
501 break;
502 case "text-shadow":
503 $values = $this->mixin_textshadow($mixin_array, $values);
504 break;
505 case 'keyframes':
506 $values = $this->mixin_keyframes($mixin_array, $values);
507 break;
508 default:
509 // Do Nothing, just for compat.
510 break;
511 }
512 }
513
514
515 }
516 return $values;
517 }
518
519 /** Put various statements in their one-line shorthand. */
520 protected function combineStatements($values)
521 {
522 $combiners = array(
523 'border' => array('border-width', 'border-style', 'border-color'),
524 'border-radius' => array('border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius'),
525 'margin' => array('margin-top', 'margin-right', 'margin-bottom', 'margin-left'),
526 'padding' => array('padding-top', 'padding-right', 'padding-bottom', 'padding-left'),
527 'background' => array('background-image','background-position','background-size', 'background-repeat'),
528 );
529
530 foreach($combiners as $shorthand => $reqs)
531 {
532 // All fields in combiner must be set. Check this against the keys (fields) in values, to ensure all are there ( results in 0 diff )
533 if (count(array_diff($reqs, array_keys($values) )) == 0)
534 {
535 $values[$shorthand] = '';
536
537 foreach($reqs as $item)
538 {
539 if ($item == 'background-size') // exceptions there must be
540 $values[$shorthand] .= '/ ';
541
542 $values[$shorthand] .= $values[$item] . ' ';
543 unset($values[$item]);
544 }
545
546 }
547 }
548
549 return $values;
550 }
551
552
553 /** Parse the keyframes. Not a real mixin */
554 protected function mixin_keyframes($results, $values)
555 {
556 $keyframes_name = isset($results['keyframes-name']) ? $results['keyframes-name'] : false;
557 $keyframes = isset($results['keyframes']) ? $results['keyframes'] : false;
558
559 $key_output = ' @keyframes ' . $keyframes_name . ' { '.
560 $keyframes . '}';
561
562 $this->output_css = $key_output . $this->output_css;
563
564 $values = array_diff_key($values, $results);
565 return $values;
566 }
567
568 protected function mixin_gradient($results, $values)
569 {
570
571 $background = ( isset($values['background'])) ? $values['background'] : false;
572
573
574 $start = isset($results["gradient-start-color"]) ? $results["gradient-start-color"] : '';
575 $end = isset( $results["gradient-end-color"] ) ? $results["gradient-end-color"] : '';
576 $start_opacity = isset( $results["gradient-start-opacity"] ) ? intval($results["gradient-start-opacity"]) : '';
577 $end_opacity = isset( $results["gradient-end-opacity"] ) ? intval($results["gradient-end-opacity"]) : '';
578 $stop = (isset( $results["gradient-stop"]) && $results["gradient-stop"] != '') ? $results["gradient-stop"] . "%" : '45%';
579 // default to use ( old situation )
580 $use_gradient = (isset($results['gradient-use-gradient']) && $results['gradient-use-gradient'] != '') ? $results['gradient-use-gradient'] : 1;
581
582
583 $start = maxUtils::hex2rgba($start, $start_opacity);
584 $end = maxUtils::hex2rgba($end, $end_opacity);
585
586 $important = ($this->is_important()) ? "!important" : false;
587 //$values = $this->add_include($values, "linear-gradient($start,$end,$stop,$important)");
588
589 if ($use_gradient == 1)
590 {
591 if ($background)
592 unset($values['background']);
593
594 $linear = "linear-gradient($start,$end,$stop";
595 if ($important)
596 $linear .= ','. $important;
597 elseif(! $important && $background)
598 $linear .= ',null,' . $background;
599 elseif ($important && $background)
600 $linear .= ',' . $background;
601
602 $values = $this->add_include($values, $linear . ')');
603 }
604 else {
605 $values['background-color'] = $start;
606 }
607
608 // remove the non-css keys from the value array ( field names )
609 $values = array_diff_key($values, $results);
610
611 return $values;
612
613
614 }
615
616 protected function mixin_boxshadow($results, $values)
617 {
618 $width = isset($results["box-shadow-width"]) ? $results["box-shadow-width"] : 0;
619 $left = isset($results["box-shadow-offset-left"]) ? $results["box-shadow-offset-left"] : 0;
620 $top = isset($results["box-shadow-offset-top"]) ? $results["box-shadow-offset-top"] : 0;
621 $spread = isset($results['box-shadow-spread']) ? $results['box-shadow-spread'] : 0;
622 $color = isset($results["box-shadow-color"]) ? $results["box-shadow-color"] : "rgba(0,0,0,0)";
623
624 $important = ($this->is_important()) ? "!important" : "";
625
626 $values = array_diff_key($values, $results); // always remove these fields from CSS since they are not valid.
627
628 if ($width == 0 && $left == 0 && $top == 0 && $spread == 0)
629 {
630 $values['box-shadow'] = 'none'; // if no box-shadow, prevent it in total
631 return $values;
632 }
633
634 $values = $this->add_include($values, "box-shadow($left, $top, $width, $color,$spread, false, $important) ");
635
636
637 return $values;
638 }
639
640 protected function mixin_textshadow($results, $values)
641 {
642 $width = isset($results["text-shadow-width"]) ? $results["text-shadow-width"] : 0;
643 $left = isset($results["text-shadow-left"]) ? $results["text-shadow-left"] : 0;
644 $top = isset($results["text-shadow-top"]) ? $results["text-shadow-top"] : 0;
645 $color = isset($results["text-shadow-color"]) ? $results["text-shadow-color"] : "rgba(0,0,0,0)";
646 $important = ($this->is_important()) ? "!important" : "";
647
648 if ($width == 0 && $left == 0 && $top == 0)
649 {
650 $values = array_diff_key($values, $results); // remove them from the values, prevent incorrect output.
651 return $values;
652 }
653
654 $values = $this->add_include($values, "text-shadow ($left,$top,$width,$color $important)");
655 $values = array_diff_key($values, $results);
656
657 return $values;
658 }
659
660 private function add_include($values, $include)
661 {
662 if (isset($values["@include"]))
663 $values["@include"] .= "; @include " . $include;
664 else
665 $values["@include"] = $include;
666 return $values;
667 }
668
669 public function outputInline($domObj, $pseudo = 'normal')
670 {
671 $domObj = $domObj->load($domObj->save());
672
673 $inline = $this->inline;
674 // ISSUE #43 Sometimes this breaks
675 if (! isset($inline[$pseudo]))
676 return $domObj;
677
678 $elements = array_keys($inline[$pseudo]);
679
680 if ($pseudo != 'normal') // gather all elements
681 $elements = array_merge($elements, array_keys($inline["normal"]));
682
683 foreach($elements as $element)
684 {
685 $styles = isset($inline[$pseudo][$element]) ? $inline[$pseudo][$element] : array();
686
687 if ($pseudo != 'normal')
688 {
689 $styles = array_merge($inline['normal'][$element],$styles);
690 }
691 $normstyle = '';
692 /* if ($pseudo != 'normal') // parse all possible missing styles from pseudo el
693 {
694
695 $tocompile = $this->dummify($inline['normal'][$element]);
696 $normstyle = $this->compile($tocompile);
697 $normstyle = $this->undummify($normstyle);
698 } */
699 maxUtils::addTime("CSSParser: Parse inline done");
700 foreach($styles as $cssTag => $cssVal)
701 {
702 $normstyle .= $this->parse_cssline($styles, $cssTag,$cssVal);
703 }
704 // add dummy {} here because new scssphp parse doesn't like styles without it. Remove it after compile, since this is inline.
705 $style_output = $this->compile($this->dummify($normstyle));
706 $style_output = $this->undummify($style_output);
707
708 //$styles = $normstyle . $styles;
709
710 $element = trim(str_replace("."," ", $element)); // molten css class, seperator.
711
712 $el = $domObj->find('[class*="' . $element . '"]', 0);
713
714 $el->style = $style_output;
715
716 }
717
718 return $domObj;
719
720 }
721
722 /** Dummyfi, because Scssphp compiler doesn't like CSS without {} */
723 private function dummify($string)
724 {
725 return 'dummy{' . $string . '}';
726 }
727
728 private function undummify($string)
729 {
730 $string = trim(str_replace(array('dummy','{','}'),'', $string)); // remove the dummy
731 if (substr($string,-1) !== ';') // the last ; might not be there, put it so we can glue more CSS there.
732 $string .= ';';
733
734 return $string;
735 }
736
737
738 }
739
740 class compileException extends Exception {
741 protected $code = -1;
742
743 }
744