Sindbad~EG File Manager

Current Path : /proc/self/cwd/common/Admin/Appearance/
Upload File :
Current File : //proc/self/cwd/common/Admin/Appearance/CssThemeGenerator.php

<?php namespace Common\Admin\Appearance;

use File;
use Leafo\ScssPhp\Block;
use Leafo\ScssPhp\Parser;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Common\Settings\Settings;

class CssThemeGenerator
{
    /**
     * @var Settings
     */
    private $settings;

    private $variables;

    private $matches = [];

    private $cache = [];

    /**
     * CssThemeGenerator constructor.
     * @param Settings $settings
     */
    public function __construct(Settings $settings)
    {
        $this->settings = $settings;
        $this->variables = config('common.appearance.variables');
    }

    public function generate()
    {
        $dirs = [
            base_path('../client/src/app'),
            base_path('../client/src/scss'),
            base_path('../client/src/common')
        ];

        $parser = new Parser(null);

        $files = iterator_to_array(Finder::create()->name('*.scss')->files()->in($dirs), false);

        foreach ($files as $file) {
            $tree = $parser->parse(file_get_contents($file->getRealPath()));
            $this->parseSassBlock($tree);
        }

        $this->generateCss();
    }

    /**
     * Generate a valid css file from sass files of components that contain variables.
     *
     * @return string
     */
    private function generateCss()
    {
        $grouped = $this->groupMatchesByVariable();
        $css = '';

        foreach ($grouped as $variable => $group) {
            foreach ($group as $property => $matches) {
                //remove "::webkit" stuff as it breaks css in other browsers
                $selectors = array_filter($matches, function($match) {
                    return ! str_contains($match['selector'], '::-webkit');
                });

                //prepend '#theme' to all selectors
                $selectors = array_map(function($match) {
                    return $this->prependThemePrefixToSelector($match['selector']);
                }, $selectors);

                $selectors = join($selectors, ",\n");

                $css .= "$selectors\n{\n\t$property: var(--$variable);\n}\n\n";
            }
        }

        //TEMP: TODO webkit scrollbars
        $css .= $this->getWebkitScrollbarsCss();

        $rootBlock = $this->generateCssVariablesRootBlock();

        $css = $rootBlock . $css;

        File::put(resource_path('editable-theme.css'), $css);

        return $css;
    }

    /**
     * Generate css :root block containing variables.
     *
     * @return string
     */
    private function generateCssVariablesRootBlock()
    {
        $sassVars = $this->getSassVariableFileContents();

        $variables = collect($this->variables)->mapWithKeys(function($variable) use($sassVars) {
            return [$variable => $this->extractVariableValue($sassVars, $variable)];
        })->map(function($value, $name) {
            return "\t--$name: $value;";
        })->implode("\n");

        return ":root {\n$variables\n}\n\n";
    }

    /**
     * Get contents of all _variables.scss files.
     *
     * @return string
     */
    private function getSassVariableFileContents()
    {
        if (isset($this->cache['variableFileContents'])) return $this->cache['variableFileContents'];

        $dirs = [base_path('../client/src/'), base_path('../client/src/common')];

        $files = iterator_to_array(Finder::create()->name('*_variables.scss')->files()->in($dirs), false);

        $contents = collect($files)->map(function(SplFileInfo $file) {
            return File::get($file->getRealPath());
        })->implode('');

        $this->cache['variableFileContents'] = $contents;

        return $this->cache['variableFileContents'];
    }

    /**
     * Extract specified sass variable value recursively.
     *
     * @param string $sass
     * @param string $variable
     * @param bool $recursive
     * @return string
     */
    private function extractVariableValue($sass, $variable, $recursive = true)
    {
        //extract value from sass map
        if (str_contains($variable, 'map_get')) {
            $materialSass = file_get_contents(base_path('../client/node_modules/@angular/material/_theming.scss'));
            preg_match("/map_get\((.+?),.([0-9]+)\)/", $variable, $matches);

            $mapName = '\\'.$matches[1];
            $varName = $matches[2];

            preg_match("/$mapName:.\(.+?$varName:.(#[a-z0-9]+),/s", $materialSass, $matches);

            return $matches[1];
        }

        $variable = str_replace('$', '', $variable);
        $variable = '\$'.$variable;
        preg_match("/$variable:(.+?);/", $sass, $matches);

        try {
            $value = trim(str_replace('!default', '', $matches[1]));
        } catch (\Exception $e) {
            $value = $variable;
        }

        if (str_contains($value, '$') && $recursive) {
            $value = $this->extractVariableValue($sass, $value);
        }

        return $value;
    }

    /**
     * Group all matches by variable name and then by css property name.
     *
     * @return array
     */
    private function groupMatchesByVariable() {
        $grouped = [];

        //group matches by variable, example "site-bg-color-400"
        foreach ($this->matches as $match) {
            $grouped[$match['variable']][] = $match;
        }

        //group each variable group by property name, example: "border-color"
        foreach ($grouped as $groupName => $group) {
            foreach ($group as $matchKey => $match) {
                $grouped[$groupName][$match['property']][] = $match;
                unset($grouped[$groupName][$matchKey]);
            }
        }

        return $this->addMaterialSelectorsToGroupedVariables($grouped);
    }

    /**
     * @param Block $block
     */
    private function parseSassBlock(Block $block)
    {
        foreach ($block->children as $child) {
            $childType = $child[0];
            $childBlock = $child[1];

            if ($childType === 'block') {
                if ($block->selectors) $childBlock->parent = $block;
                $this->getSelectorsFromBlock($childBlock);
                $this->parseSassBlock($childBlock);
            }
        }
    }

    /**
     * Extract css selectors that contain variables from specified sass block.
     *
     * @param Block $block
     */
    private function getSelectorsFromBlock(Block $block) {
        foreach ($block->children as $child) {
            $childType = $child[0];

            if ($childType === 'assign') {
                $assignKey = $child[1][2][0];
                $assignValueType = $child[2][0];
                $variableName = $this->getVariableNameFromSassValue($child);

                $variableIndex = array_search($variableName, $this->variables);

                if ($variableIndex === false) continue;

                if ($assignValueType === 'list' && str_contains($assignKey, 'border')) {
                    $this->addMatch($block, 'border-color', $variableIndex);
                }

                else if ($assignValueType === 'var' && $variableIndex > -1) {
                    $this->addMatch($block, $assignKey, $variableIndex);
                } else if ($assignValueType === 'fncall') {
                    // TODO: parse and use function properly instead
                    // of just overriding it with a static variable
                    //$this->addMatch($block, $assignKey, $variableIndex);
                }
            }
        }
    }

    /**
     * Extract variable name from block child.
     *
     * @param array $blockChild
     * @return string|null;
     */
    private function getVariableNameFromSassValue($blockChild)
    {
        if ( ! is_array($blockChild[2])) return null;

        $flattened = array_flatten($blockChild[2]);

        foreach ($flattened as $key => $value) {
            if ($value === 'var') {
                return $flattened[$key+1];
            }
        }

        return null;
    }

    /**
     * @param Block $block
     * @param $assignKey
     * @param $variableIndex
     */
    private function addMatch(Block $block, $assignKey, $variableIndex)
    {
        $parents = $this->makeParentSelector($block);
        $child  = $this->makeSelectorString($block->selectors);
        $children = explode(',', $child);

        $final = [];

        // merge all parent and child selectors
        // [parent1, parent2], [child1, child2] => [parent1 child1, parent1 child2, parent2 child1, parent2 child2]
        foreach ($parents as $parent) {
            foreach ($children as $child) {
                // convert sass '&' symbols into css, by removing them
                // selector&.child => selector.child
                if (str_contains($child, '&') || str_contains($parent, '&')) {
                    $final[] = str_replace([' &', '& ', '&'], '', $parent.$child);
                } else {
                    $final[] = $parent ? $parent.' '.$child : $child;
                }
            }
        }

        $selector = implode(', ', $final);
        $selector = preg_replace('!\s+!', ' ', $selector);

        //.selector>.child => .selector > .child
        $selector = preg_replace("/([a-z])>/", "$1 >", $selector);

        $this->matches[] = [
            'selector' => $selector,
            'property' => $assignKey,
            'variable' => $this->variables[$variableIndex],
        ];
    }

    /**
     * Generate selectors string from specified sass selectors array.
     *
     * @param array $selectors
     * @return string
     */
    private function makeSelectorString($selectors)
    {
        $string = '';

        foreach ($selectors as $key => $selector) {
            foreach ($selector as $selectorParts) {
                $self = isset($selectorParts[0][0]) && $selectorParts[0][0] === 'self';
                //start space between selectors
                if (! $self) $string .= ' ';

                foreach ($selectorParts as $innerPart) {
                    if (is_string($innerPart)) {
                        $string .= $innerPart;

                        if ($innerPart === '>') {
                            $string .= ' ';
                        }
                    } else if ($innerPart[0] === 'self') {
                        $string .= '&';
                    } else if ($innerPart[0] === 'string' && isset($innerPart[2])) {
                        $string .= $innerPart[2][0];
                    }
                }
            }

            if (isset($selectors[$key + 1])) {
                $string .= ', ';
            }
        }

        return $string;
    }

    /**
     * Generate parent selector to the root parent for specified block.
     *
     * @param Block $block
     * @return array
     */
    private function makeParentSelector(Block $block)
    {
        $parent = isset($block->parent) ? $block->parent : null;
        $parentSelector = [];

        while ($parent) {
            $parentSelector[] = $this->makeSelectorString($parent->selectors);
            $parent = isset($parent->parent) ? $parent->parent : null;
        }

        //order array from parent to child
        $parentSelector = array_reverse($parentSelector);

        $selectors = [''];

        foreach ($parentSelector as $selectorPart) {
            //multiple selectors, will need to prepend previous parents to all of them
            if (str_contains($selectorPart, ',')) {
                $parts = explode(',', $selectorPart);

                $selectors = array_map(function($selector) use($selectors) {
                    return implode(' ', $selectors) . trim($selector);
                }, $parts);
            } else {
                foreach ($selectors as $key => $selector) {
                    $selectors[$key] = $selectors[$key] . ' ' . $selectorPart . ' ';
                }
            }
        }

        $selectors = array_map(function($selector) {
            return trim(str_replace('  ', ' ', $selector));
        }, $selectors);

        return $selectors;
    }

    /**
     * Prepend "#theme" prefix to all specified selector parts.
     *
     * @param string $selector
     * @return string
     */
    private function prependThemePrefixToSelector($selector)
    {
        $parts = explode(',', $selector);

        $parts = array_map(function($selectorPart) {
            $trimmed = trim($selectorPart);
            //remove > so, selectors are never direct child of #theme
            $normalized = preg_replace("/^> /", '', $trimmed);
            return '#theme '.trim($normalized);
        }, $parts);

        return implode(', ', $parts);
    }

    /**
     * Add angular material specific accent selectors to the theme.
     *
     * @param array $grouped
     * @return array
     */
    private function addMaterialSelectorsToGroupedVariables($grouped)
    {
        $grouped['site-accent-color']['background-color'][] = [
            'selector' => '.mat-raised-button.mat-accent:not([disabled]), .mat-fab.mat-accent, .mat-mini-fab.mat-accent',
        ];

        $grouped['site-accent-color']['background-color'][] = [
            'selector' => '.mat-flat-button.mat-accent:not([disabled]), .mat-fab.mat-accent, .mat-mini-fab.mat-accent',
        ];

        $grouped['site-accent-color']['background-color'][] = [
            'selector' => '.mat-checkbox-checked.mat-accent .mat-checkbox-background, .mat-checkbox-indeterminate.mat-accent .mat-checkbox-background',
        ];

        $grouped['site-accent-color']['color'][] = [
            'selector' => ' .mat-button.mat-accent, .mat-icon-button.mat-accent'
        ];

        $grouped['site-primary-color-100']['background-color'][] = [
            'selector' => ' .mat-progress-bar-buffer'
        ];

        $grouped['site-primary-color-200']['background-color'][] = [
            'selector' => ' .mat-raised-button.mat-primary, .mat-icon-button.mat-primary, .mat-progress-bar-fill::after',
        ];

        $grouped['site-primary-color-200']['background-color'][] = [
            'selector' => '.mat-checkbox-checked.mat-primary .mat-checkbox-background, .mat-checkbox-indeterminate.mat-primary .mat-checkbox-background',
        ];

        $grouped['site-primary-color-200']['background-color'][] = [
            'selector' => '.mat-radio-button.mat-primary .mat-radio-inner-circle',
        ];

        $grouped['site-primary-color-200']['border-color'][] = [
            'selector' => '.mat-radio-button.mat-primary.mat-radio-checked .mat-radio-outer-circle',
        ];

        $grouped['site-primary-color-200']['border-color'][] = [
            'selector' => '.mat-tab-group.mat-primary .mat-ink-bar, .mat-tab-nav-bar.mat-primary .mat-ink-bar',
        ];

        $grouped['site-primary-color-200']['background-color'][] = [
            'selector' => '.mat-step-header .mat-step-icon',
        ];

        $grouped['site-primary-color-200']['color'][] = [
            'selector' => '.la-ball-spin-clockwise',
        ];

        $grouped['site-bg-color-100']['background-color'][] = [
            'selector' => ' .mat-menu-panel'
        ];

        $grouped['site-text-color-400']['color'][] = [
            'selector' => ' .mat-menu-item'
        ];

        // TODO: TEMP FOR BEMUSIC
        if (array_search('wp-bg-color-500', $this->variables) > -1) {
            $grouped['wp-bg-color-600']['background-color'][] = [
                'selector' => '.auth-page .auth-panel, account-settings .panel',
            ];

            $grouped['wp-bg-color-500']['background'][] = [
                'selector' => '.auth-page, account-settings, .fullscreen-overlay.maximized',
            ];

            $grouped['wp-bg-color-400']['background-color'][] = [
                'selector' => '.auth-page .auth-panel, .auth-page > .auth-panel .input-container > input, account-settings input, account-settings select, .web-player-theme .mat-dialog-container',
            ];

            $grouped['wp-border-color-200']['border-color'][] = [
                'selector' => 'account-settings .panel, account-settings input, account-settings select',
            ];

            $grouped['wp-bg-color-300']['background-color'][] = [
                'selector' => '.auth-page > .auth-panel .input-container > input',
            ];

            $grouped['wp-text-color-200']['color'][] = [
                'selector' => '.auth-page',
            ];

            $grouped['site-accent-color']['color'][] = [
                'selector' => '.auth-page a',
            ];
        }

        return $grouped;
    }

    private function getWebkitScrollbarsCss() {
        $selector = 'web-player ::-webkit-scrollbar-thumb, .web-player-theme ::-webkit-scrollbar-thumb';
        $css = "$selector\n{\n\tbackground: var(--wp-bg-color-300);\n}\n\n";
        $selector2 = 'web-player ::-webkit-scrollbar-thumb:hover, .web-player-theme ::-webkit-scrollbar-thumb:hover';
        $css.="\n$selector2\n{\n\tbackground: var(--wp-bg-color-200);\n}\n\n";
        return $css;
    }
}

Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists