Skip to content

Instantly share code, notes, and snippets.

@sampart
Last active February 23, 2016 14:57
Show Gist options
  • Save sampart/a40f21767baf3a888787 to your computer and use it in GitHub Desktop.
Save sampart/a40f21767baf3a888787 to your computer and use it in GitHub Desktop.
Assetic filter which incorporates Bless (http://blesscss.com/). Designed for use with Symfony and the AsseticBundle, but should be simple to adapt for other situations.
<?php
namespace YourProj\YourBundle\Assetic\Filter;
use Assetic\Asset\AssetInterface;
use Assetic\Exception\FilterException;
use Assetic\Filter\FilterInterface;
/**
* Bless splits up CSS files to get around the limit of 4096 selectors per file in IE<=9.
* http://blesscss.com/.
*/
class BlessFilter implements FilterInterface
{
const BLESS_OUTPUT_SUBDIR_NAME = 'bless-output';
protected $pathForSplitFiles;
/**
* @param string $pathToWebDir The full path to the app's web directory
*/
public function __construct($pathToWebDir)
{
// ensure that the path ends in a slash
$pathToWebDir = rtrim($pathToWebDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$this->pathForSplitFiles = sprintf(
'%s%s%s',
$pathToWebDir,
self::BLESS_OUTPUT_SUBDIR_NAME,
DIRECTORY_SEPARATOR
);
}
public function filterLoad(AssetInterface $asset)
{
}
/**
* {@inheritdoc}
*/
public function filterDump(AssetInterface $asset)
{
// Set up files and directories
// =============================
$tempInputFile = tempnam(sys_get_temp_dir(), 'input');
// File must have an extension of css for bless to accept it
$inputFile = $tempInputFile . '.css';
rename($tempInputFile, $inputFile);
// We can't simply pass $asset->getSourcePath() to bless - that contains the original
// less file. We need to get the CSS from $asset->getContent().
//
// We write this CSS to a file - it's too much text to simply pipe to bless on the command
// line.
file_put_contents($inputFile, $asset->getContent());
// ensure that the output directory exists
if (!is_dir($this->pathForSplitFiles)) {
mkdir($this->pathForSplitFiles, 0777, true);
}
// The output file isn't actually referenced by the resulting webpage; it's just a place
// to dump the output from bless. Any additional CSS files created will be placed
// in the same directory.
$outputFile = $this->pathForSplitFiles . basename($asset->getTargetPath());
// Build and run the bless command
// ================================
// The resulting asset will reference any "split out" CSS files with @import statements
// In dev, the additional CSS files will be real files but the initial asset (containing
// the imports) will be served by the special Assetic controller and so won't actually
// be a real file on disk. In other environments, the initial asset is also dumped to a
// real file by Assetic.
$command = sprintf(
"blessc %s %s",
$inputFile,
$outputFile
);
$output = [];
$error = 0;
$output = exec($command, $output, $error);
if ($error) {
throw new FilterException(sprintf(
"Error when running blessc: %s",
$output
));
}
// Set the content of the resulting asset, and tidy up
// ====================================================
$initialCSS = file_get_contents($outputFile);
$initialCSS = $this->makeImportPathsAbsolute($initialCSS);
$asset->setContent($initialCSS);
// to avoid confusing people, remove $outputFile as it's not actually used
unlink($outputFile);
// we don't need $inputFile any more either
unlink($inputFile);
}
/**
* Bless creates an output file which references the other CSS files via import statements.
* However, these statements use relative paths - in fact, they just refer to the other files
* by name, and don't include any path information. This means that it is expected that the
* "chunked" files will be in the same folder as the main one.
*
* In dev, you can put the chunked files into web/css and the paths will work - Assetic will
* serve the main file from the virtual directory web/css (in fact, it's served by a special
* Assetic controller, it's not a file on disk).
*
* In test, however, the first CSS file attempts to import the chunked files from
* app_test.php/css/, which doesn't map to the web/css folder and so the files aren't imported.
*
* To avoid this problem, alter the import statements to use absolute paths. This also allows
* us to store the chunked files in a different folder (self::BLESS_OUTPUT_SUBDIR_NAME).
* This can avoid confusion since web/css then isn't used for both real files and "fake" files
* intercepted by the Assetic controller.
*
* @param string $initialCSS
*
* @return string The CSS file with any bless-imports updated to be absolute references to files
* in self::BLESS_OUTPUT_SUBDIR_NAME.
*/
private function makeImportPathsAbsolute($initialCSS)
{
$lines = explode(PHP_EOL, $initialCSS);
$madeAChange = false;
for ($i = 0; $i < count($lines); $i++) {
$line = $lines[$i];
$replaced = 0;
$updatedLine = preg_replace(
'/@import url\(\'([^\']*blessed\d[^\']*)\'\);/',
'@import url(\'/' . self::BLESS_OUTPUT_SUBDIR_NAME . '/$1\');',
$line,
-1,
$replaced
);
if ($replaced) {
$lines[$i] = $updatedLine;
$madeAChange = true;
} else {
break; // stop parsing the lines as soon as we reach one which isn't a bless import
}
}
if ($madeAChange) {
return implode(PHP_EOL, $lines);
} else {
return $initialCSS;
}
}
}
assetic:
debug: %kernel.debug%
use_controller: false
bundles: [ YourBundle ]
filters:
cssrewrite: ~
less:
node: %node_bin%
node_paths: [%node_path%]
apply_to: '\.less$'
# The order here matters - we want to parse the less file after the less filter has been
# applied to it.
#
# Note that, with this order, the bless filter will only be called if the less file hasn't
# already been converted to CSS. Therefore, to ensure the filter runs afresh (e.g. for
# debugging it or if you make changes), you'll need to clear the Symfony cache and your
# browser cache.
bless:
resource: 'assetic.xml' # workaround for assetic to accept our filter - see https://github.com/symfony/AsseticBundle/issues/50
apply_to: '\.less$'
assetic.filter.bless:
class: YourProj\YourBundle\Assetic\Filter\BlessFilter
arguments: [%kernel.root_dir%/../web]
tags:
- { name: assetic.filter, alias: bless }
@sampart
Copy link
Author

sampart commented Aug 19, 2015

If you're running this on Mac rather than Linux, you may need to provide the full path to node and to bless when running blessc. I.e. path/to/node-binary path/to/node/modules/blessc ..... We found that the command returned an error 127 otherwise, even though the command as originally written worked when running from terminal on Mac.

In this case, you'll need to pass in the node binary path and node module path as arguments to the constructor. If you're using the Assetic Less filter, you'll already have these as a variables as that needs them too.

@sampart
Copy link
Author

sampart commented Feb 23, 2016

It looks like the command format has changed in Bless 4, so the code above will need adapting if you're on that version. (The above was written against 3.0.3.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment