Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Add recess filter #482

Open
wants to merge 5 commits into from

6 participants

@boteeka

Added the recess filter for *.less files. This filter is based on the standard less filter, but is also able to compile Bootstrap 3.0. As of Bootstrap 3.0 the only officially supported compiler is recess.

@boteeka boteeka referenced this pull request
Open

Recess support #463

@hgfischer

It's surprisingly bad to see things like this being just ignored by the maintainers :(

@boteeka

Well, this is not yet of the highest quality, more like a quick hack to make it work. Currently I am on a vacation, but I am planning to improve on it and add tests as well. Please be patient ;-)

@kriswallsmith

Thanks for this! Tests will need to be fixed before merging this, of course. What other changes do you have in mind, @boteeka?

@boteeka

My current approach is probably only working for bootstrap, which is a one-file-includes-everything situation. I want to make it work with multiple paths as well. The Less filter accepts an array of paths to compile while Recess does not, or not in the same way. So I still have to smooth things out on that front. And the necessary tests, of course :-)

@stof
Collaborator

The Less filter accepts an array of paths to compile while Recess does not, or not in the same way

The LessFilter in Assetic only supports a single input file as well. It runs on the asset content, not on files in the paths given to it. The array of paths is here to let it known from which folder it can import other files (because the asset content itself is not in this folder anymore as it can have been processed by another filter)

boteeka added some commits
@boteeka boteeka Use the includePath option of recess
Use the absolute path of the asset as recess compiler operates only on filepaths, not the content
0a8d13d
@boteeka boteeka Added tests for LessRecessFilter 7083d06
@boteeka

@stof you're absolutely right. Implemented fix to properly use the includePath option of recess.

@hacfi

I’m using recess as a linter..would be great to have assetic use it to compile too.

@boteeka boteeka referenced this pull request in symfony/AsseticBundle
Open

Add config for lessrecess filter (for *.less files) #219

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Aug 8, 2013
  1. @boteeka

    Add recess filter

    boteeka authored
Commits on Aug 9, 2013
  1. @boteeka
  2. @boteeka

    Test for LessRecessFilter

    boteeka authored
Commits on Sep 7, 2013
  1. @boteeka

    Use the includePath option of recess

    boteeka authored
    Use the absolute path of the asset as recess compiler operates only on filepaths, not the content
  2. @boteeka
This page is out of date. Refresh to see the latest.
View
221 src/Assetic/Filter/LessRecessFilter.php
@@ -0,0 +1,221 @@
+<?php
+
+/*
+ * This file is part of the Assetic package, an OpenSky project.
+ *
+ * (c) 2010-2013 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Assetic\Filter;
+
+use Assetic\Asset\AssetInterface;
+use Assetic\Exception\FilterException;
+use Assetic\Factory\AssetFactory;
+use Assetic\Util\LessUtils;
+
+/**
+ * Loads LESS files using the recess linter/compiler
+ *
+ * @link http://lesscss.org/
+ * @link https://github.com/twitter/recess
+ *
+ * @author Botond Szasz <boteeka@gmail.com>
+ */
+class LessRecessFilter extends BaseNodeFilter implements DependencyExtractorInterface
+{
+ private $nodeBin;
+
+ /**
+ * @var array
+ */
+ private $parserOptions;
+
+ /**
+ * Load Paths
+ *
+ * A list of paths which less will search for includes.
+ *
+ * @var array
+ */
+ protected $loadPaths = array();
+
+ /**
+ * Constructor.
+ *
+ * @param string $nodeBin The path to the node binary
+ * @param array $nodePaths An array of node paths
+ */
+ public function __construct($nodeBin = '/usr/bin/node', array $nodePaths = array())
+ {
+ $this->nodeBin = $nodeBin;
+ $this->setNodePaths($nodePaths);
+ $this->parserOptions = array('compile' => true);
+ }
+
+ /**
+ * setCompress
+ *
+ * Sets the option whether to compress the resulting
+ * output or not
+ *
+ * @param bool $compress
+ */
+ public function setCompress($compress)
+ {
+ $this->addParserOption('compress', $compress);
+ }
+
+ /**
+ * addParserOption
+ *
+ * Pass in options to the parser
+ *
+ * @param string $code
+ * @param string $value
+ */
+ public function addParserOption($code, $value)
+ {
+ $this->parserOptions[$code] = $value;
+ }
+
+ /**
+ * setLoadPaths
+ *
+ * Set the include paths where the parser looks for @imported files
+ *
+ * @param array $loadPaths
+ */
+ public function setLoadPaths(array $loadPaths)
+ {
+ $this->loadPaths = $loadPaths;
+ }
+
+ /**
+ * addLoadPath
+ *
+ * Adds a path where less will search for includes
+ *
+ * @param string $path Load path (absolute)
+ */
+ public function addLoadPath($path)
+ {
+ $this->loadPaths[] = $path;
+ }
+
+ /**
+ * filterLoad
+ *
+ * Parses and compiles the asset
+ *
+ * @param AssetInterface $asset
+ */
+ public function filterLoad(AssetInterface $asset)
+ {
+ static $format = <<<'EOF'
+var recess = require('recess');
+var sys = require(process.binding('natives').util ? 'util' : 'sys');
+
+recess(%s, %s, function(err, obj) {
+ if (err) {
+ throw err;
+ process.exit(2);
+ }
+ try {
+ sys.print(obj[0].output);
+ } catch (e) {
+ sys.print(obj[0].errors);
+ process.exit(3);
+ }
+});
+
+EOF;
+
+ $root = $asset->getSourceRoot();
+ $path = $asset->getSourcePath();
+ $paths = array();
+
+ $this->addParserOption('includePath', $this->loadPaths);
+
+ $pb = $this->createProcessBuilder();
+
+ $pb->add($this->nodeBin)->add($input = tempnam(sys_get_temp_dir(), 'assetic_lessrecess'));
+
+ file_put_contents($input, sprintf($format,
+ json_encode($root.'/'.$path),
+ json_encode($this->parserOptions)
+ ));
+
+ $proc = $pb->getProcess();
+ $code = $proc->run();
+ unlink($input);
+
+ if (0 !== $code) {
+ throw FilterException::fromProcess($proc)->setInput($asset->getContent());
+ }
+
+ $asset->setContent($proc->getOutput());
+ }
+
+ /**
+ * filterDump
+ *
+ * @param AssetInterface $asset
+ */
+ public function filterDump(AssetInterface $asset)
+ {
+ }
+
+ /**
+ * getChildren
+ *
+ * Gets all @imported children
+ *
+ * @param AssetFactory $factory
+ * @param string $content
+ * @param string $loadPath
+ *
+ * @return array
+ */
+ public function getChildren(AssetFactory $factory, $content, $loadPath = null)
+ {
+ $loadPaths = $this->loadPaths;
+ if (null !== $loadPath) {
+ $loadPaths[] = $loadPath;
+ }
+
+ if (empty($loadPaths)) {
+ return array();
+ }
+
+ $children = array();
+ foreach (LessUtils::extractImports($content) as $reference) {
+ if ('.css' === substr($reference, -4)) {
+ // skip normal css imports
+ // todo: skip imports with media queries
+ continue;
+ }
+
+ if ('.less' !== substr($reference, -5)) {
+ $reference .= '.less';
+ }
+
+ foreach ($loadPaths as $loadPath) {
+ if (file_exists($file = $loadPath.'/'.$reference)) {
+ $coll = $factory->createAsset($file, array(), array('root' => $loadPath));
+ foreach ($coll as $leaf) {
+ $leaf->ensureFilter($this);
+ $children[] = $leaf;
+ goto next_reference;
+ }
+ }
+ }
+
+ next_reference:
+ }
+
+ return $children;
+ }
+}
View
192 tests/Assetic/Test/Filter/LessRecessFilterTest.php
@@ -0,0 +1,192 @@
+<?php
+
+/*
+ * This file is part of the Assetic package, an OpenSky project.
+ *
+ * (c) 2010-2013 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Assetic\Test\Filter;
+
+use Assetic\Asset\FileAsset;
+use Assetic\Asset\StringAsset;
+use Assetic\Factory\AssetFactory;
+use Assetic\Filter\LessRecessFilter;
+/**
+ * @group integration
+ */
+class LessRecessFilterTest extends FilterTestCase
+{
+ /**
+ * @var LessFilter
+ */
+ protected $filter;
+
+ protected function setUp()
+ {
+ if (!$nodeBin = $this->findExecutable('node', 'NODE_BIN')) {
+ $this->markTestSkipped('Unable to find `node` executable.');
+ }
+
+ if (!$this->checkNodeModule('recess', $nodeBin)) {
+ $this->markTestSkipped('The "recess" module is not installed.');
+ }
+
+ $this->filter = new LessRecessFilter($nodeBin, isset($_SERVER['NODE_PATH']) ? array($_SERVER['NODE_PATH']) : array());
+ }
+
+ public function testFilterLoad()
+ {
+ $asset = new StringAsset('.foo{.bar{width:(1+1);}}');
+ $asset->load();
+
+ $testFilePath = '/tmp/lessrecesstestfile.less';
+ file_put_contents($testFilePath, $asset->getContent());
+
+ $fileAsset = new FileAsset($testFilePath);
+ $fileAsset->load();
+
+ $this->filter->filterLoad($fileAsset);
+
+ $this->assertEquals(".foo .bar {\n width: 2;\n}", $fileAsset->getContent(), '->filterLoad() parses the content');
+ unlink($testFilePath);
+ }
+
+ public function testImport()
+ {
+ $expected = <<<EOF
+.foo {
+ color: blue;
+}
+
+.foo {
+ color: red;
+}
+EOF;
+
+ $asset = new FileAsset(__DIR__.'/fixtures/less/main.less');
+ $asset->load();
+
+ $this->filter->filterLoad($asset);
+
+ $this->assertEquals($expected, $asset->getContent(), '->filterLoad() sets an include path based on source url');
+ }
+
+ public function testCompressImport()
+ {
+ $expected = <<<EOF
+.foo{color:blue}.foo{color:red}
+EOF;
+
+ $asset = new FileAsset(__DIR__.'/fixtures/less/main.less');
+ $asset->load();
+
+ $this->filter->addParserOption('compress', true);
+ $this->filter->filterLoad($asset);
+
+ $this->assertEquals($expected, $asset->getContent(), '->filterLoad() sets an include path based on source url');
+ }
+
+ public function testLoadPath()
+ {
+ $expected = <<<EOF
+.foo {
+ color: blue;
+}
+
+.foo {
+ color: red;
+}
+EOF;
+
+ $this->filter->addLoadPath(__DIR__.'/fixtures/less');
+
+ $asset = new StringAsset('@import "main";');
+ $asset->load();
+
+ $testFilePath = '/tmp/lessrecesstestfile.less';
+ file_put_contents($testFilePath, $asset->getContent());
+
+ $fileAsset = new FileAsset($testFilePath);
+ $fileAsset->load();
+
+ $this->filter->filterLoad($fileAsset);
+
+ $this->assertEquals($expected, $fileAsset->getContent(), '->filterLoad() adds load paths to include paths');
+ }
+
+ public function testSettingLoadPaths()
+ {
+ $expected = <<<EOF
+.foo {
+ color: blue;
+}
+
+.foo {
+ color: red;
+}
+
+.bar {
+ color: #ff0000;
+}
+EOF;
+
+ $this->filter->setLoadPaths(array(
+ __DIR__.'/fixtures/less',
+ __DIR__.'/fixtures/less/import_path',
+ ));
+
+ $asset = new StringAsset('@import "main"; @import "_import"; .bar {color: @red}');
+ $asset->load();
+
+ $testFilePath = '/tmp/lessrecesstestfile.less';
+ file_put_contents($testFilePath, $asset->getContent());
+
+ $fileAsset = new FileAsset($testFilePath);
+ $fileAsset->load();
+
+ $this->filter->filterLoad($fileAsset);
+
+ $this->assertEquals($expected, $fileAsset->getContent(), '->filterLoad() sets load paths to include paths');
+ }
+
+ /**
+ * @dataProvider provideImports
+ */
+ public function testGetChildren($import)
+ {
+ $children = $this->filter->getChildren(new AssetFactory('/'), $import, __DIR__.'/fixtures/less');
+
+ $this->assertCount(1, $children);
+ $this->assertEquals('main.less', $children[0]->getSourcePath());
+ }
+
+ public function provideImports()
+ {
+ return array(
+ array('@import \'main.less\';'),
+ array('@import "main.less";'),
+ array('@import url(\'main.less\');'),
+ array('@import url("main.less");'),
+ array('@import url(main.less);'),
+ array('@import \'main\';'),
+ array('@import "main";'),
+ array('@import url(\'main\');'),
+ array('@import url("main");'),
+ array('@import url(main);'),
+ array('@import-once \'main.less\';'),
+ array('@import-once "main.less";'),
+ array('@import-once url(\'main.less\');'),
+ array('@import-once url("main.less");'),
+ array('@import-once url(main.less);'),
+ array('@import-once \'main\';'),
+ array('@import-once "main";'),
+ array('@import-once url(\'main\');'),
+ array('@import-once url("main");'),
+ array('@import-once url(main);'),
+ );
+ }
+}
Something went wrong with that request. Please try again.