<?php
/**
* CSS Injector
* Helper object for including dynamic styles into head of the document,
* with possibilities of extending it.
*
* @copyright 2019-present Creative Themes
* @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
* @package Blocksy
*/
class Blocksy_Css_Injector {
/**
* Temporary CSS attributes.
*
* @var array $attr Attributes.
*/
private $attr = [];
private $additional_symbols = [];
private $selector_prefix = null;
private $fonts_manager = null;
/**
* Keyword that allows skiping a certain CSS rule from getting in the output.
*/
public static function get_skip_rule_keyword($suffix = '') {
return 'CT_CSS_SKIP_RULE' . $suffix;
}
public static function get_inline_keyword($suffix = '') {
return 'CT_CSS_INLINE_CSS' . $suffix;
}
/**
* Injector constructor.
*/
public function __construct($args = []) {
$args = wp_parse_args(
$args,
[
'selector_prefix' => '',
'fonts_manager' => null
]
);
if (! empty($args['selector_prefix'])) {
$this->selector_prefix = $args['selector_prefix'];
}
$this->additional_symbols = ['-', '%', 'px', 's'];
if ($args['fonts_manager']) {
$this->fonts_manager = $args['fonts_manager'];
}
}
public function process_matching_typography($value) {
if (! $this->fonts_manager) {
return;
}
$this->fonts_manager->process_matching_typography($value);
}
/**
* Parse each temporary structure and transform it into actual CSS.
*/
public function build_css_structure() {
$content = '';
if (isset($this->attr[Blocksy_Css_Injector::get_inline_keyword()])) {
$content .= implode('', $this->attr[Blocksy_Css_Injector::get_inline_keyword()]);
unset($this->attr[Blocksy_Css_Injector::get_inline_keyword()]);
}
if (count($this->attr)) {
$content .= "\n" . $this->convert_to_css();
}
$content = $this->css_minify($content);
return $content;
}
public function get_wp_style_engine_rules($args = []) {
$args = wp_parse_args(
$args,
[
'device' => 'desktop'
]
);
$media_queries = [
'tablet' => '@media (max-width: 999.98px)',
'mobile' => '@media (max-width: 689.98px)'
];
$rules = [];
foreach ($this->attr as $selector => $lines) {
$declarations = [];
foreach ($lines as $line) {
$line = trim($line);
if (! $line) {
continue;
}
$parts = explode(':', $line);
if (count($parts) <= 1) {
continue;
}
$declarations[trim($parts[0])] = trim($parts[1]);
}
$rule = [
'selector' => $selector,
'declarations' => $declarations
];
if (isset($media_queries[$args['device']])) {
$rule['rules_group'] = $media_queries[$args['device']];
}
$rules[] = $rule;
}
return $rules;
}
/**
* Add new line in CSS structure.
*
* @param string|array $selector CSS class, id, tag.
* @param string|array $rules CSS syntax.
*/
public function put($selector, $rules) {
$normalized = $this->normalize_inputs($selector, $rules);
if (! $normalized) {
return;
}
$selector = $normalized['selector'];
$rules = $normalized['rules'];
if (! isset($this->attr[$selector])) {
$this->attr[$selector] = [];
}
foreach ($rules as $line) {
$line = trim($line);
if (
! $line
||
in_array($line, $this->attr[$selector], true)
) {
continue;
}
if (strpos($line, self::get_skip_rule_keyword()) !== false) {
continue;
}
$this->attr[$selector][] = $line;
}
}
private function normalize_inputs($selector, $preliminary_rules) {
if (is_string($preliminary_rules) && trim($preliminary_rules) === '') {
return false;
}
if (is_array($selector)) {
$selector = implode(",\n", $selector);
}
$rules = [];
if ($selector === Blocksy_Css_Injector::get_inline_keyword()) {
$rules = is_array($preliminary_rules) ? $preliminary_rules : [
$preliminary_rules
];
} else {
// Convert string to array.
if (! is_array($preliminary_rules)) {
$rules = explode(';', $preliminary_rules);
} else {
// Support nested rules.
foreach ($preliminary_rules as $maybe_rule) {
$current_rules = explode(';', $maybe_rule);
foreach ($current_rules as $current_rule) {
$rules[] = $current_rule;
}
}
}
}
$prefix = '';
if (! empty($this->selector_prefix)) {
$prefix = $this->selector_prefix . ' ';
}
return [
'selector' => $prefix . $selector,
'rules' => $rules
];
}
/**
* Merge selectors that have the same CSS. This has the effect of increasing
* the weight of the selectors.
*/
private function merge_class_with_the_same_css() {
return;
$new_names = [];
$used = [];
foreach ($this->attr as $key => $values) {
if (isset($used[$key])) {
continue;
}
foreach ($this->attr as $sub_key => $sub_values) {
if ($sub_key !== $key && $values === $sub_values) {
$used[$sub_key] = 1;
$new_names[$key][] = $sub_key;
$used[$key] = 1;
}
}
}
// Merge classes.
foreach ($new_names as $parent => $childs) {
$class_name = $parent . ",\n" . join(",\n", $childs);
$this->attr[$class_name] = $this->attr[$parent];
// Remove CSS from main structure.
if (isset($this->attr[$parent])) {
unset($this->attr[$parent]);
}
// Remove all childs css.
foreach ($childs as $child_class) {
if (isset($this->attr[$child_class])) {
unset($this->attr[$child_class]);
}
}
}
}
/**
* Convert this->attr to a CSS string.
*/
private function convert_to_css() {
$css = '';
$this->merge_class_with_the_same_css();
foreach ($this->attr as $key => $values) {
$section = '';
$section .= $key . " {\n";
$content = '';
foreach ($values as $line) {
$line = trim($line);
if (! $this->is_empty_style($line)) {
if (strpos($key, '@media') === false) {
$line = str_replace(';', '', $line);
}
$content .= " {$line}";
if (strpos($key, '@media') === false) {
$content .= ";\n";
}
}
}
// CSS is not empty.
if ($content) {
$section .= $content;
} else {
continue;
}
$section .= "}\n\n";
$css .= $section;
}
// Erase structure.
$this->attr = [];
return $css;
}
/**
* Check if a CSS rule is empty.
*
* @param string $line Single rule.
*/
private function is_empty_style($line) {
$parts = explode(':', $line);
if (count($parts) <= 1) {
return false;
}
if (! isset($parts[1])) {
return true;
}
$parts[1] = str_replace($this->additional_symbols, '', $parts[1]);
return strlen(trim($parts[1])) === 0;
}
/**
* Very rudimentary CSS minifier.
*
* @param string $minify CSS to be minified.
*/
private function css_minify($minify) {
if (defined('WP_DEBUG') && WP_DEBUG) {
// return $minify;
}
// return $minify;
/* remove comments */
$minify = preg_replace( '!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $minify );
/* remove tabs, spaces, newlines, etc. */
$minify = str_replace( array( "\r\n", "\r", "\n", "\t", ' ', ' ', ' ' ), '', $minify );
/* remove space after colons */
$minify = str_replace( ': ', ':', $minify );
$minify = str_replace( '}[', '} [', $minify );
return $minify;
}
}
|