From 575d559e5aea5e099ba7784547b1aa359d28fb8c Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Tue, 1 Aug 2017 19:45:35 +0200 Subject: [PATCH 1/1] Import files --- .gitignore | 2 + Controller/DefaultController.php | 13 ++ DependencyInjection/Configuration.php | 29 +++ DependencyInjection/RapsysPackExtension.php | 28 +++ RapsysPackBundle.php | 9 + Resources/config/parameters.yml | 13 ++ Resources/config/services.yml | 15 ++ Twig/PackExtension.php | 50 +++++ Twig/PackNode.php | 31 +++ Twig/PackTokenParser.php | 207 ++++++++++++++++++++ 10 files changed, 397 insertions(+) create mode 100644 .gitignore create mode 100644 Controller/DefaultController.php create mode 100644 DependencyInjection/Configuration.php create mode 100644 DependencyInjection/RapsysPackExtension.php create mode 100644 RapsysPackBundle.php create mode 100644 Resources/config/parameters.yml create mode 100644 Resources/config/services.yml create mode 100644 Twig/PackExtension.php create mode 100644 Twig/PackNode.php create mode 100644 Twig/PackTokenParser.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c17649 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.*.un~ +*~ diff --git a/Controller/DefaultController.php b/Controller/DefaultController.php new file mode 100644 index 0000000..f40c65a --- /dev/null +++ b/Controller/DefaultController.php @@ -0,0 +1,13 @@ +render('RapsysPackBundle:Default:index.html.twig'); + } +} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php new file mode 100644 index 0000000..dd68944 --- /dev/null +++ b/DependencyInjection/Configuration.php @@ -0,0 +1,29 @@ +root('rapsys_pack'); + + // Here you should define the parameters that are allowed to + // configure your bundle. See the documentation linked above for + // more information on that topic. + + return $treeBuilder; + } +} diff --git a/DependencyInjection/RapsysPackExtension.php b/DependencyInjection/RapsysPackExtension.php new file mode 100644 index 0000000..b200abd --- /dev/null +++ b/DependencyInjection/RapsysPackExtension.php @@ -0,0 +1,28 @@ +processConfiguration($configuration, $configs); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('services.yml'); + } +} diff --git a/RapsysPackBundle.php b/RapsysPackBundle.php new file mode 100644 index 0000000..c9e9ea1 --- /dev/null +++ b/RapsysPackBundle.php @@ -0,0 +1,9 @@ +kernel = $kernel; + public function __construct(FileLocator $fileLocator, ContainerInterface $containerInterface) { + //Set file locator + $this->fileLocator = $fileLocator; + //Set container interface + $this->containerInterface = $containerInterface; + + //Set default prefix + $this->prefix = '@RapsysPackBundle/Resources/public/'; + + //Set default coutput + $this->coutput = 'css/*.pack.css'; + //Set default joutput + $this->joutput = 'js/*.pack.js'; + + //Set default cpack + $this->cpack = '/usr/local/bin/cpack'; + //Set default jpack + $this->jpack = '/usr/local/bin/jpack'; + + //Load configuration + if ($containerInterface->hasParameter('rapsys_pack')) { + foreach($containerInterface->getParameter('rapsys_pack') as $k => $v) { + if (isset($this->$k)) { + $this->$k = $v; + } + } + } + + //Fix prefix + $this->prefix = $this->fileLocator->locate($this->prefix); + } + + public function getTokenParsers() { + return array( + new PackTokenParser($this->fileLocator, $this->containerInterface, $this->prefix, 'stylesheets', $this->coutput, $this->cpack), + new PackTokenParser($this->fileLocator, $this->containerInterface, $this->prefix, 'javascripts', $this->joutput, $this->jpack), + #new PackTokenParser($this->fileLocator, $this->containerInterface, $this->prefix, 'image', '*.pack.{tld}'), + ); + } +} diff --git a/Twig/PackNode.php b/Twig/PackNode.php new file mode 100644 index 0000000..4f08be0 --- /dev/null +++ b/Twig/PackNode.php @@ -0,0 +1,31 @@ + $value), array('name' => $name), $line, $tag); + #} + public function __construct(array $nodes = array(), array $attributes = array(), $lineno = 0, $tag = null) { + parent::__construct($nodes, $attributes, $lineno, $tag); + #$this->output = $attributes['output']; + #$this->setAttribute($this->getAttribute('name'), $attributes['name']); + } + + public function compile(\Twig_Compiler $compiler) { + + #header('Content-Type: text/plain'); + #var_dump($this->getNode(0)); + #var_dump($this->attributes); + #var_dump($compiler->subcompile($this->getNode(0))); + #exit; + #$compiler->addDebugInfo($this)->write('echo \'
'.json_encode(array('nodes' => $this->nodes, 'attributes' => $this->attributes)).'
\';'); + $compiler + ->addDebugInfo($this) + ->write('$context[\''.$this->getAttribute('name').'\'] = ') + ->repr($this->getAttribute('output')) + ->raw(";\n") + ->subcompile($this->getNode('value')) + ->raw(";\n"); + } +} diff --git a/Twig/PackTokenParser.php b/Twig/PackTokenParser.php new file mode 100644 index 0000000..8218ee1 --- /dev/null +++ b/Twig/PackTokenParser.php @@ -0,0 +1,207 @@ +fileLocator = $fileLocator; + $this->containerInterface = $containerInterface; + $this->prefix = $prefix; + $this->tag = $tag; + $this->output = $output; + $this->tool = $tool; + } + + public function getTag() { + return $this->tag; + } + + public function parse(\Twig_Token $token) { + $parser = $this->parser; + $stream = $this->parser->getStream(); + + $inputs = array(); + $filters = array(); + $name = 'asset_url'; + $output = $this->output; + + $content = ''; + + while (!$stream->test(\Twig_Token::BLOCK_END_TYPE)) { + if ($stream->test(\Twig_Token::STRING_TYPE)) { + // '@jquery', 'js/src/core/*', 'js/src/extra.js' + $inputs[] = $stream->next()->getValue(); + } elseif ($stream->test(\Twig_Token::NAME_TYPE, 'filter')) { + // filter='yui_js' + $stream->next(); + $stream->expect(\Twig_Token::OPERATOR_TYPE, '='); + $filters = array_merge($filters, array_filter(array_map('trim', explode(',', $stream->expect(\Twig_Token::STRING_TYPE)->getValue())))); + } elseif ($stream->test(\Twig_Token::NAME_TYPE, 'output')) { + // output='js/packed/*.js' OR output='js/core.js' + $stream->next(); + $stream->expect(\Twig_Token::OPERATOR_TYPE, '='); + $output = $stream->expect(\Twig_Token::STRING_TYPE)->getValue(); + } elseif ($stream->test(\Twig_Token::NAME_TYPE, 'name')) { + // name='core_js' + $stream->next(); + $stream->expect(\Twig_Token::OPERATOR_TYPE, '='); + $name = $stream->expect(\Twig_Token::STRING_TYPE)->getValue(); + } else { + $token = $stream->getCurrent(); + throw new \Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s"', \Twig_Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $stream->getFilename()); + } + } + + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + + $body = $this->parser->subparse(array($this, 'testEndTag'), true); + + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + + //Replace star with sha1 + if (($pos = strpos($output, '*')) !== false) { + #XXX: assetic code : substr(sha1(serialize($inputs).serialize($filters).serialize($options)), 0, 7) + $output = substr($output, 0, $pos).sha1(serialize($inputs).serialize($filters)).substr($output, $pos + 1); + } + + //Deal with inputs + for($k = 0; $k < count($inputs); $k++) { + //Deal with generic url + if (strpos($inputs[$k], '//') === 0) { + //TODO: set this as a parameter (scheme) + $inputs[$k] = 'https:'.$inputs[$k]; + //Deal with non url path + } elseif (strpos($inputs[$k], '://') === false) { + //Check if we have a bundle path + if ($inputs[$k][0] == '@') { + if (($pos = strpos($inputs[$k], '/')) === false) { + throw new \Twig_Error_Syntax(sprintf('Invalid input path "%s"', $inputs[$k]), $token->getLine(), $stream->getFilename()); + } + //Extract prefix + #$inputs[$k] = $this->kernel->locateResource(substr($inputs[$k], 0, $pos)).substr($inputs[$k], $pos + 1); + $inputs[$k] = $this->fileLocator->locate(substr($inputs[$k], 0, $pos)).substr($inputs[$k], $pos + 1); + } + //Deal with globs + if (strpos($inputs[$k], '*') !== false || (($a = strpos($inputs[$k], '{')) !== false && ($b = strpos($inputs[$k], ',', $a)) !== false && strpos($inputs[$k], '}', $b) !== false)) { + //Get replacement + $replacement = glob($inputs[$k], GLOB_NOSORT|GLOB_BRACE); + //Check that these are working files + foreach($replacement as $input) { + if (!is_file($input)) { + throw new \Twig_Error_Syntax(sprintf('Input path "%s" from "%s" is not a file', $input, $inputs[$k]), $token->getLine(), $stream->getFilename()); + } + } + //Replace with glob path + array_splice($inputs, $k, 1, $replacement); + //Fix current key + $k += count($replacement) - 1; + //Check that it's a file + } elseif (!is_file($inputs[$k])) { + throw new \Twig_Error_Syntax(sprintf('Input path "%s" is not a file', $inputs[$k]), $token->getLine(), $stream->getFilename()); + } + } + } + + //Retrieve files content + foreach($inputs as $input) { + //Set timeout + $ctx = stream_context_create( + array( + 'http' => array( + 'timeout' => 5 + ) + ) + ); + //Try to retrieve content + if (($data = file_get_contents($input, false, $ctx)) === false) { + throw new \Twig_Error_Syntax(sprintf('Unable to retrieve input path "%s"', $input), $token->getLine(), $stream->getFilename()); + } + //Append content + $content .= $data; + } + + //Use tool + if (!empty($this->tool) && is_executable($this->tool)) { + $descriptorSpec = array( + 0 => array('pipe', 'r'), + 1 => array('pipe', 'w'), + 2 => array('pipe', 'w') + ); + if (is_resource($proc = proc_open($this->tool, $descriptorSpec, $pipes))) { + //Set stderr as non blocking + stream_set_blocking($pipes[2], 0); + //Send content to stdin + fwrite($pipes[0], $content); + //Close stdin + fclose($pipes[0]); + //Read content from stdout + if ($stdout = stream_get_contents($pipes[1])) { + $content = $stdout; + } + //Close stdout + fclose($pipes[1]); + //Read content from stderr + if ($stderr = stream_get_contents($pipes[2])) { + throw new \Twig_Error_Syntax(sprintf('Got unexpected strerr for %s: %s', $this->tool, $stderr), $token->getLine(), $stream->getFilename()); + } + //Close stderr + fclose($pipes[2]); + //Close process + if ($ret = proc_close($proc)) { + throw new \Twig_Error_Syntax(sprintf('Got unexpected non zero return code %s: %d', $this->tool, $ret), $token->getLine(), $stream->getFilename()); + } + } + } + + //Create output dir on demand + if (!is_dir($parent = $dir = dirname($this->prefix.$output))) { + //XXX: set as 0777, symfony umask (0022) will reduce rights (0755) + mkdir($dir, 0777, true); + } + + //Send file content + //TODO: see if atomic rotation is really necessary ? + //XXX: version management is done via rapsys_pack configuration atomic should be useless + //TODO: implement asset versionning or re-use internal functions + if (file_put_contents($this->prefix.$output.'.new', $content) === false) { + throw new \Twig_Error_Syntax(sprintf('Unable to write to: %s', $prefix.$output.'.new'), $token->getLine(), $stream->getFilename()); + } + + //Remove old file + if (is_file($this->prefix.$output) && unlink($this->prefix.$output) === false) { + throw new \Twig_Error_Syntax(sprintf('Unable to unlink: %s', $prefix.$output), $token->getLine(), $stream->getFilename()); + } + + //Rename it + if (rename($this->prefix.$output.'.new', $this->prefix.$output) === false) { + throw new \Twig_Error_Syntax(sprintf('Unable to rename: %s to %s', $prefix.$output.'.new', $prefix.$output), $token->getLine(), $stream->getFilename()); + } + + //Retrieve asset uri + if (($output = $this->containerInterface->get('assets.packages')->getUrl($output, 'rapsys_pack')) === false) { + throw new \Twig_Error_Syntax(sprintf('Unable to get url for asset: %s with package %s', $output, 'rapsys_pack'), $token->getLine(), $stream->getFilename()); + } + + //Send pack node + return new PackNode(array('value' => $body), array('inputs' => $inputs, 'filters' => $filters, 'name' => $name, 'output' => $output), $token->getLine(), $this->getTag()); + } + + public function testEndTag(\Twig_Token $token) { + return $token->test(array('end'.$this->getTag())); + } +} -- 2.41.1