pug-php/pug-minify

View on GitHub
src/Pug/Keyword/Minify.php

Summary

Maintainability
B
6 hrs
Test Coverage
A
100%
<?php

namespace Pug\Keyword;

use InvalidArgumentException;
use NodejsPhpFallback\Uglify;
use Pug\Keyword\Minify\AssetParser;
use Pug\Keyword\Minify\BlockExtractor;
use Pug\Keyword\Minify\InJsPugParser;
use Pug\Keyword\Minify\Path;
use Pug\Keyword\Minify\ScriptParser;
use Pug\Keyword\Minify\StyleParser;

class Minify
{
    /**
     * @var bool
     */
    protected $dev;

    /**
     * @var array
     */
    protected $assetDirectory;

    /**
     * @var string
     */
    protected $outputDirectory;

    /**
     * @var array
     */
    protected $js;

    /**
     * @var array
     */
    protected $css;

    /**
     * @var array
     */
    protected $events;

    /**
     * @var \Pug\Pug|\Jade\Jade
     */
    protected $pug;

    public function __construct($pug)
    {
        if (!($pug instanceof \Jade\Jade) && !($pug instanceof \Pug\Pug) && !($pug instanceof \Phug\Renderer)) {
            throw new InvalidArgumentException(
                'Allowed pug engine are Jade\\Jade, Pug\\Pug or Phug\\Renderer, ' . get_class($pug) . ' given.'
            );
        }

        $this->pug = $pug;
    }

    protected function prepareDirectory($path)
    {
        $path = (string) $path;
        $directory = dirname($path);

        if (!is_dir($directory)) {
            mkdir($directory, 0777, true);
        }

        return $path;
    }

    protected function parsePugInJs($code, $indent = '', $classAttribute = null)
    {
        $parser = new InJsPugParser(array(
            'classAttribute' => $classAttribute,
            'singleQuote'    => $this->getOption('singleQuote'),
            'prettyprint'    => $this->getOption('prettyprint'),
        ));

        return $parser->parse($code, $indent);
    }

    protected function parsePugInJsx($parameters)
    {
        return $parameters[1] . $this->parsePugInJs(str_replace('``', '`', $parameters[2]), $parameters[1], 'className');
    }

    protected function parsePugInCoffee($parameters)
    {
        return $parameters[1] . '"""' . $this->parsePugInJs($parameters[2], $parameters[1]) . '"""';
    }

    protected function writeWith($parser, $path)
    {
        file_put_contents($path, '');
        $parser->write($path);
    }

    /**
     * @param string      $path
     * @param string      $newExtension
     * @param string|null $relativePath
     *
     * @return string[]
     */
    protected function getPathInfo($path, $newExtension, $relativePath = null)
    {
        $source = $this->getSourcePath($path, $relativePath);
        $extension = pathinfo($path, PATHINFO_EXTENSION);
        $path = substr($path, 0, -strlen($extension)) . $newExtension;
        $destination = $this->prepareDirectory(new Path($this->outputDirectory, $path));

        return array($extension, $path, (string) $source, $destination);
    }

    /**
     * @param string      $path
     * @param string|null $relativePath
     *
     * @return Path
     */
    protected function getSourcePath($path, $relativePath = null)
    {
        if ($relativePath) {
            $relativeSource = new Path($path);
            $relativeSource = $relativeSource->relativeTo($relativePath);

            if (file_exists((string) $relativeSource)) {
                return $relativeSource;
            }
        }

        return new Path($this->assetDirectory, $path);
    }

    protected function needUpdate($source, $destination)
    {
        return !$this->dev || !file_exists($destination) || filemtime($source) >= filemtime($destination);
    }

    protected function prepareSource($params)
    {
        switch ($params->extension) {
            case 'jsxp':
                $params->contents = preg_replace_callback(
                    '/(?<!\s)(\s*)::`(([^`]+|(?<!`)`(?!`))*?)`(?!`)/',
                    array($this, 'parsePugInJsx'),
                    file_get_contents($params->source)
                );
                break;

            case 'cofp':
                $params->contents = preg_replace_callback(
                    '/(?<!\s)(\s*)::"""([\s\S]*?)"""/',
                    array($this, 'parsePugInCoffee'),
                    file_get_contents($params->source)
                );
                break;
        }
    }

    protected function parseScript($path, $relativePath = null)
    {
        list($extension, $path, $source, $destination) = $this->getPathInfo($path, 'js', $relativePath);
        $params = (object) array(
            'extension'   => $extension,
            'type'        => 'script',
            'path'        => $path,
            'source'      => $source,
            'destination' => $destination,
        );

        if ($this->needUpdate($params->source, $params->destination)) {
            $this->prepareSource($params);
            $this->trigger('pre-parse', $params);
            $parser = new ScriptParser($params, $destination);
            $this->handleParsing($params, $parser);
            $this->trigger('post-parse', $params);
        }

        if ($this->dev) {
            return $params->path . '?' . time();
        }

        $this->js[] = $destination;

        return null;
    }

    protected function parseStyle($path, $relativePath = null)
    {
        list($extension, $path, $source, $destination) = $this->getPathInfo($path, 'css', $relativePath);
        $params = (object) array(
            'extension'   => $extension,
            'type'        => 'style',
            'path'        => $path,
            'source'      => $source,
            'destination' => $destination,
        );

        if ($this->needUpdate($params->source, $params->destination)) {
            $this->trigger('pre-parse', $params);
            $this->handleParsing($params, new StyleParser($params));
            $this->trigger('post-parse', $params);
        }

        if ($this->dev) {
            return $params->path . '?' . time();
        }

        $this->css[] = $params->destination;

        return null;
    }

    protected function handleParsing($params, AssetParser $parser)
    {
        $result = $parser->parse();

        if (is_string($result)) {
            copy($result, $params->destination);

            return;
        }

        if (!is_array($result)) {
            $result = array($result, $params->destination);
        }

        $this->writeWith($result[0], $result[1]);
    }

    protected function uglify(&$params)
    {
        $params->concat = true;
        $params->minify = true;
        $language = $params->language;
        $path = $params->path;
        $outputFile = $language . '/' . $path . '.min.' . $language;
        $uglify = new Uglify($this->$language);
        $uglify->setModeFromPath($outputFile);
        $params->outputDirectory = $this->outputDirectory;
        $params->outputFile = $outputFile;
        $params->content = $uglify->getResult();
        $this->trigger('pre-write', $params);
        $path = new Path($params->outputDirectory, $params->outputFile);
        $path = $this->prepareDirectory($path);
        $params->outputPath = $path;
        file_put_contents($path, $params->content);
        $this->trigger('post-write', $params);
    }

    protected function concat(&$params)
    {
        $params->concat = true;
        $params->minify = false;
        $language = $params->language;
        $path = $params->path;
        $outputFile = $language . '/' . $path . '.' . $language;
        $output = '';
        foreach ($this->$language as $path) {
            $output .= file_get_contents($path) . "\n";
        }
        $params->outputDirectory = $this->outputDirectory;
        $params->outputFile = $outputFile;
        $params->content = $output;
        $this->trigger('pre-write', $params);
        $path = new Path($params->outputDirectory, $params->outputFile);
        $path = $this->prepareDirectory($path);
        $params->outputPath = $path;
        file_put_contents($path, $params->content);
        $this->trigger('post-write', $params);
    }

    protected function getOption($option, $defaultValue = null)
    {
        if (method_exists($this->pug, 'hasOption') && !$this->pug->hasOption($option)) {
            return $defaultValue;
        }

        try {
            return $this->pug->getOption($option);
        } catch (InvalidArgumentException $e) {
            return $defaultValue;
        }
    }

    protected function initializeRendering()
    {
        if (is_null($this->dev)) {
            $this->dev = substr($this->getOption('environment'), 0, 3) === 'dev';
        }
        $this->assetDirectory = (array) ($this->assetDirectory ?: $this->getOption('assetDirectory', ''));
        $this->outputDirectory = $this->outputDirectory ?: $this->getOption('outputDirectory', $this->assetDirectory[0]);

        $this->js = array();
        $this->css = array();
    }

    public function on($event, $action)
    {
        $event = strtolower(str_replace('-', '', $event));

        if (!isset($this->events[$event])) {
            $this->events[$event] = array();
        }

        $this->events[$event][] = $action;
    }

    public function trigger($event, &$params = null)
    {
        $event = strtolower(str_replace('-', '', $event));

        if (!is_object($params)) {
            $params = (object) (is_array($params) ? $params : array());
        }

        if (isset($this->events[$event])) {
            $this->triggerEventActions($event, $params);
        }
    }

    private function triggerEventActions($event, &$params = null)
    {
        foreach ($this->events[$event] as $action) {
            if (is_callable($action)) {
                $this->triggerEventAction($action, $event, $params);
            }
        }
    }

    private function triggerEventAction($action, $event, &$params = null)
    {
        $newParams = call_user_func($action, $params, $event, $this);

        if (is_object($newParams)) {
            $params = $newParams;
        }
    }

    public function linkExtractor($href, $rel, $relativePath = null)
    {
        if ($href && $rel === 'stylesheet') {
            $path = $this->parseStyle($href, $relativePath);
            if ($this->dev && $path) {
                return array(
                    'href' => $path,
                );
            }
        }
    }

    public function scriptExtractor($src, $relativePath = null)
    {
        if ($src) {
            $path = $this->parseScript($src, $relativePath);
            if ($this->dev && $path) {
                return array(
                    'src'  => $path,
                    'type' => 'text/javascript',
                );
            }
        }
    }

    protected function renderHtml($renderParams)
    {
        $html = '';

        $compilation = $renderParams->minify ? 'uglify' : 'concat';
        $event = $renderParams->minify ? 'minify' : 'concat';

        if (count($this->js)) {
            $html .= '<script src="' .
                $this->getOutputFile('js', $compilation, $event, $renderParams) .
                '"></script>';
        }

        if (count($this->css)) {
            $html .= '<link rel="stylesheet" href="' .
                $this->getOutputFile('css', $compilation, $event, $renderParams) .
                '">';
        }

        return $html;
    }

    private function getOutputFile($language, $compilation, $event, $renderParams)
    {
        $params = (object) array(
            'language' => $language,
            'path'     => $renderParams->arguments,
        );
        $this->trigger('pre-' . $event, $params);
        $this->$compilation($params);
        $this->trigger('post-' . $event, $params);

        return $params->outputFile;
    }

    public function __invoke($arguments, $block, $keyword)
    {
        $this->initializeRendering();

        if (!is_object($block) || !isset($block->nodes)) {
            return '';
        }

        $renderParams = (object) array(
            'minify'    => !in_array(strtolower(str_replace('-', '', $keyword)), array('concat', 'concatto')),
            'keyword'   => $keyword,
            'arguments' => $arguments,
            'block'     => $block,
        );
        $this->trigger('pre-render', $renderParams);

        $extractor = new BlockExtractor($renderParams->block);
        $extractor->registerTagExtractor('link', array($this, 'linkExtractor'), 'href', 'rel');
        $extractor->registerTagExtractor('script', array($this, 'scriptExtractor'), 'src');
        $extractor->extract();

        $renderParams->html = $this->renderHtml($renderParams);
        $this->trigger('post-render', $renderParams);

        return $renderParams->html;
    }
}