Css-Crush preprocessor

This commit is contained in:
Buchholz 2021-10-19 15:27:42 +02:00
parent 9a1f42908e
commit c7e0fac35a
83 changed files with 11279 additions and 0 deletions

View file

@ -0,0 +1,19 @@
<?php
/**
*
* Bootstrap file with autoloader.
*
*/
spl_autoload_register(function ($class) {
if (stripos($class, 'csscrush') !== 0) {
return;
}
$class = str_ireplace('csscrush', 'CssCrush', $class);
$subpath = implode('/', array_map('ucfirst', explode('\\', $class)));
require_once __DIR__ . "/lib/$subpath.php";
});
require_once 'lib/functions.php';

View file

@ -0,0 +1,19 @@
Copyright (c) 2010-2015 Pete Boere
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

97
include/thirdparty/css-crush/README.md vendored Normal file
View file

@ -0,0 +1,97 @@
[![Build Status](https://travis-ci.org/peteboere/css-crush.svg)](https://travis-ci.org/peteboere/css-crush)
<img src="http://the-echoplex.net/csscrush/images/css-crush-external.svg?v=2" alt="Logo"/>
A CSS preprocessor designed to enable a modern and uncluttered CSS workflow.
* Automatic vendor prefixing
* Variables
* Import inlining
* Nesting
* Functions (color manipulation, math, data-uris etc.)
* Rule inheritance (@extends)
* Mixins
* Minification
* Lightweight plugin system
* Source maps
See the [docs](http://the-echoplex.net/csscrush) for full details.
********************************
## Setup (PHP)
If you're using [Composer](http://getcomposer.org) you can use Crush in your project with the following line in your terminal:
```shell
composer require css-crush/css-crush:dev-master
```
If you're not using Composer yet just download the library into a convenient location and require the bootstrap file:
```php
<?php require_once 'path/to/CssCrush.php'; ?>
```
## Basic usage (PHP)
```php
<?php
echo csscrush_tag('css/styles.css');
?>
```
Compiles the CSS file and outputs the following link tag:
```html
<link rel="stylesheet" href="css/styles.crush.css" media="all" />
```
There are several other [functions](http://the-echoplex.net/csscrush#api) for working with files and strings of CSS:
* `csscrush_file($file, $options)` - Returns a URL of the compiled file.
* `csscrush_string($css, $options)` - Compiles a raw string of css and returns the resulting css.
* `csscrush_inline($file, $options, $tag_attributes)` - Returns compiled css in an inline style tag.
There are a number of [options](http://the-echoplex.net/csscrush#api--options) available for tailoring the output, and a collection of bundled [plugins](http://the-echoplex.net/csscrush#plugins) that cover many workflow issues in contemporary CSS development.
********************************
## Setup (JS)
```shell
npm install csscrush
```
## Basic usage (JS)
```js
// All methods can take the standard options (camelCase) as the second argument.
const csscrush = require('csscrush');
// Compile. Returns promise.
csscrush.file('./styles.css', {sourceMap: true});
// Compile string of CSS. Returns promise.
csscrush.string('* {box-sizing: border-box;}');
// Compile and watch file. Returns event emitter (triggers 'data' on compile).
csscrush.watch('./styles.css');
```
********************************
## Contributing
If you think you've found a bug please create an [issue](https://github.com/peteboere/css-crush/issues) explaining the problem and expected result.
Likewise, if you'd like to request a feature please create an [issue](https://github.com/peteboere/css-crush/issues) with some explanation of the requested feature and use-cases.
[Pull requests](https://help.github.com/articles/using-pull-requests) are welcome, though please keep coding style consistent with the project (which is based on [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)).
## Licence
MIT

248
include/thirdparty/css-crush/aliases.ini vendored Normal file
View file

@ -0,0 +1,248 @@
;----------------------------------------------------------------
;
; Add or delete aliases to suit your needs.
;
; Sources:
; http://developer.mozilla.org/en-US/docs/CSS/CSS_Reference
; http://caniuse.com/#cats=CSS
;
;----------------------------------------------------------------
; Property aliases.
[properties]
; Animations.
animation[] = -webkit-animation
animation-delay[] = -webkit-animation-delay
animation-direction[] = -webkit-animation-direction
animation-duration[] = -webkit-animation-duration
animation-fill-mode[] = -webkit-animation-fill-mode
animation-iteration-count[] = -webkit-animation-iteration-count
animation-name[] = -webkit-animation-name
animation-play-state[] = -webkit-animation-play-state
animation-timing-function[] = -webkit-animation-timing-function
; Backface visibility.
backface-visibility[] = -webkit-backface-visibility
; Border-image.
border-image[] = -webkit-border-image
; Box decoration.
box-decoration-break[] = -webkit-box-decoration-break
; Box shadow.
box-shadow[] = -webkit-box-shadow
; Box sizing.
box-sizing[] = -webkit-box-sizing
box-sizing[] = -moz-box-sizing
; Columns.
columns[] = -webkit-columns
column-count[] = -webkit-column-count
column-fill[] = -webkit-column-fill
column-gap[] = -webkit-column-gap
column-rule[] = -webkit-column-rule
column-rule-style[] = -webkit-column-rule-style
column-rule-width[] = -webkit-column-rule-width
column-rule-style[] = -webkit-column-rule-style
column-rule-color[] = -webkit-column-rule-color
column-span[] = -webkit-column-span
column-width[] = -webkit-column-width
; Filter.
filter[] = -webkit-filter
; Flexbox (2012).
;
; Merges two similar versions of the flexbox spec:
; - September 2012 (for non IE): http://www.w3.org/TR/2012/CR-css3-flexbox-20120918
; - March 2012 (for IE10): http://www.w3.org/TR/2012/WD-css3-flexbox-20120322
;
; The early 2012 spec mostly differs only in syntax to the later one, with the notable
; exception of not supporting seperate properties for <flex-grow>, <flex-shrink>
; and <flex-basis>. These properties are available in both 2012 implementations via
; <flex> shorthand.
;
; Support for the early 2012 syntax implemented in IE10 is achieved here in part with
; property aliases, and in part with declaration aliases later in this file.
;
align-content[] = -webkit-align-content
align-items[] = -webkit-align-items
align-self[] = -webkit-align-self
flex[] = -webkit-flex
flex[] = -ms-flex
flex-basis[] = -webkit-flex-basis
flex-direction[] = -webkit-flex-direction
flex-direction[] = -ms-flex-direction
flex-flow[] = -webkit-flex-flow
flex-flow[] = -ms-flex-flow
flex-grow[] = -webkit-flex-grow
flex-shrink[] = -webkit-flex-shrink
flex-wrap[] = -webkit-flex-wrap
flex-wrap[] = -ms-flex-wrap
justify-content[] = -webkit-justify-content
order[] = -webkit-order
order[] = -ms-flex-order
; Hyphens.
hyphens[] = -webkit-hyphens
hyphens[] = -ms-hyphens
; Outline radius.
outline-radius[] = -moz-outline-radius
outline-top-left-radius[] = -moz-outline-radius-topleft
outline-top-right-radius[] = -moz-outline-radius-topright
outline-bottom-left-radius[] = -moz-outline-radius-bottomleft
outline-bottom-right-radius[] = -moz-outline-radius-bottomright
; Perspective.
perspective[] = -webkit-perspective
perspective-origin[] = -webkit-perspective-origin
; Shapes
shape-image-threshold[] = -webkit-shape-image-threshold
shape-outside[] = -webkit-shape-outside
shape-margin[] = -webkit-shape-margin
; Tab size.
tab-size[] = -moz-tab-size
tab-size[] = -o-tab-size
; Text decoration.
text-decoration-color[] = -webkit-text-decoration-color
text-decoration-line[] = -webkit-text-decoration-line
text-decoration-style[] = -webkit-text-decoration-style
; Transforms.
transform[] = -webkit-transform
transform[] = -ms-transform
transform-origin[] = -webkit-transform-origin
transform-origin[] = -ms-transform-origin
transform-style[] = -webkit-transform-style
transform-style[] = -ms-transform-style
; Transitions.
transition[] = -webkit-transition
transition-delay[] = -webkit-transition-delay
transition-duration[] = -webkit-transition-duration
transition-property[] = -webkit-transition-property
transition-timing-function[] = -webkit-transition-timing-function
; User select (non standard).
user-select[] = -webkit-user-select
user-select[] = -moz-user-select
user-select[] = -ms-user-select
;----------------------------------------------------------------
; Declaration aliases.
[declarations]
; Flexbox (2012).
display:flex[] = display:-ms-flexbox
display:flex[] = display:-webkit-flex
display:inline-flex[] = display:-ms-inline-flexbox
display:inline-flex[] = display:-webkit-inline-flex
; Flexbox (early 2012).
align-content:flex-start[] = -ms-flex-line-pack:start
align-content:flex-end[] = -ms-flex-line-pack:end
align-content:center[] = -ms-flex-line-pack:center
align-content:space-between[] = -ms-flex-line-pack:justify
align-content:space-around[] = -ms-flex-line-pack:distribute
align-content:stretch[] = -ms-flex-line-pack:stretch
align-items:flex-start[] = -ms-flex-align:start
align-items:flex-end[] = -ms-flex-align:end
align-items:center[] = -ms-flex-align:center
align-items:baseline[] = -ms-flex-align:baseline
align-items:stretch[] = -ms-flex-align:stretch
align-self:auto[] = -ms-flex-item-align:auto
align-self:flex-start[] = -ms-flex-item-align:start
align-self:flex-end[] = -ms-flex-item-align:end
align-self:center[] = -ms-flex-item-align:center
align-self:baseline[] = -ms-flex-item-align:baseline
align-self:stretch[] = -ms-flex-item-align:stretch
justify-content:flex-start[] = -ms-flex-pack:start
justify-content:flex-end[] = -ms-flex-pack:end
justify-content:center[] = -ms-flex-pack:center
justify-content:space-between[] = -ms-flex-pack:justify
justify-content:space-around[] = -ms-flex-pack:distribute
; Cursor values (non-standard).
cursor:zoom-in[] = cursor:-webkit-zoom-in
cursor:zoom-out[] = cursor:-webkit-zoom-out
cursor:grab[] = cursor:-webkit-grab
cursor:grabbing[] = cursor:-webkit-grabbing
; Experimental width values.
width:max-content[] = width:intrinsic
width:max-content[] = width:-webkit-max-content
width:max-content[] = width:-moz-max-content
width:min-content[] = width:-webkit-min-content
width:min-content[] = width:-moz-min-content
width:available[] = width:-webkit-available
width:available[] = width:-moz-available
width:fit-content[] = width:-webkit-fit-content
width:fit-content[] = width:-moz-fit-content
max-width:max-content[] = max-width:intrinsic
max-width:max-content[] = max-width:-webkit-max-content
max-width:max-content[] = max-width:-moz-max-content
max-width:min-content[] = max-width:-webkit-min-content
max-width:min-content[] = max-width:-moz-min-content
max-width:available[] = max-width:-webkit-available
max-width:available[] = max-width:-moz-available
max-width:fit-content[] = max-width:-webkit-fit-content
max-width:fit-content[] = max-width:-moz-fit-content
min-width:max-content[] = min-width:intrinsic
min-width:max-content[] = min-width:-webkit-max-content
min-width:max-content[] = min-width:-moz-max-content
min-width:min-content[] = min-width:-webkit-min-content
min-width:min-content[] = min-width:-moz-min-content
min-width:available[] = min-width:-webkit-available
min-width:available[] = min-width:-moz-available
min-width:fit-content[] = min-width:-webkit-fit-content
min-width:fit-content[] = min-width:-moz-fit-content
; Appearance (non-standard).
appearance:none[] = -webkit-appearance:none
appearance:none[] = -moz-appearance:none
position:sticky[] = position:-webkit-sticky
;----------------------------------------------------------------
; Function aliases.
[functions]
; Calc.
calc[] = -webkit-calc
[functions.gradients]
; Gradients.
linear-gradient[] = -webkit-linear-gradient
radial-gradient[] = -webkit-radial-gradient
; Repeating gradients.
repeating-linear-gradient[] = -webkit-repeating-linear-gradient
repeating-radial-gradient[] = -webkit-repeating-radial-gradient
;----------------------------------------------------------------
; @rule aliases.
[at-rules]
; Keyframes.
keyframes[] = -webkit-keyframes

View file

@ -0,0 +1,8 @@
#!/usr/bin/env php
<?php
/**
*
* Alias for package managers.
*
*/
require __DIR__ . '/../cli.php';

637
include/thirdparty/css-crush/cli.php vendored Normal file
View file

@ -0,0 +1,637 @@
<?php
/**
*
* Command line utility.
*
*/
require_once 'CssCrush.php';
define('STATUS_OK', 0);
define('STATUS_ERROR', 1);
$version = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;
$requiredVersion = 5.6;
if ($version < $requiredVersion) {
stderr(["PHP version $requiredVersion or higher is required to use this tool.",
"You are currently running PHP $version"]);
exit(STATUS_ERROR);
}
try {
$args = parse_args();
}
catch (Exception $ex) {
stderr(message($ex->getMessage(), ['type'=>'error']));
exit($ex->getCode());
}
##################################################################
## Information options.
if ($args->version) {
stdout((string) CssCrush\Version::detect());
exit(STATUS_OK);
}
elseif ($args->help) {
stdout(manpage());
exit(STATUS_OK);
}
##################################################################
## Resolve input.
$input = null;
if ($args->input_file) {
$input = file_get_contents($args->input_file);
}
elseif ($stdin = get_stdin_contents()) {
$input = $stdin;
}
else {
stdout(manpage());
exit(STATUS_OK);
}
if ($args->watch && ! $args->input_file) {
stderr(message('Watch mode requires an input file.', ['type'=>'error']));
exit(STATUS_ERROR);
}
##################################################################
## Resolve process options.
$configFile = 'crushfile.php';
if (file_exists($configFile)) {
$options = CssCrush\Util::readConfigFile($configFile);
}
else {
$options = [];
}
if ($args->pretty) {
$options['minify'] = false;
}
foreach (['boilerplate', 'formatter', 'newlines',
'stat_dump', 'source_map', 'import_path'] as $option) {
if ($args->$option) {
$options[$option] = $args->$option;
}
}
if ($args->enable_plugins) {
$options['plugins'] = parse_list($args->enable_plugins);
}
if ($args->vendor_target) {
$options['vendor_target'] = parse_list($args->vendor_target);
}
if ($args->vars) {
parse_str($args->vars, $in_vars);
$options['vars'] = $in_vars;
}
if ($args->output_file) {
$options['output_dir'] = dirname($args->output_file);
$options['output_file'] = basename($args->output_file);
}
$options += [
'doc_root' => getcwd(),
'context' => $args->context,
];
##################################################################
## Output.
error_reporting(0);
if ($args->watch) {
csscrush_set('config', ['io' => 'CssCrush\IO\Watch']);
stdout('CONTROL-C to quit.');
$outstandingErrors = false;
while (true) {
csscrush_file($args->input_file, $options);
$stats = csscrush_stat();
$changed = $stats['compile_time'] && ! $stats['errors'];
$errors = $stats['errors'];
$warnings = $stats['warnings'];
$showErrors = $errors && (! $outstandingErrors || ($outstandingErrors != $errors));
if ($errors) {
if ($showErrors) {
$outstandingErrors = $errors;
stderr(message($errors, ['type'=>'error']));
}
}
elseif ($changed) {
$outstandingErrors = false;
stderr(message(fmt_fileinfo($stats, 'output'), ['type'=>'write']));
}
if (($showErrors || $changed) && $warnings) {
stderr(message($warnings, ['type'=>'warning']));
}
if ($changed && $args->stats) {
stderr(message($stats, ['type'=>'stats']));
}
sleep(1);
}
}
else {
$stdOutput = null;
if ($args->input_file && isset($options['output_dir'])) {
$options['cache'] = false;
csscrush_file($args->input_file, $options);
}
else {
$stdOutput = csscrush_string($input, $options);
}
$stats = csscrush_stat();
$errors = $stats['errors'];
$warnings = $stats['warnings'];
if ($errors) {
stderr(message($errors, ['type'=>'error']));
exit(STATUS_ERROR);
}
elseif ($args->input_file && ! empty($stats['output_filename'])) {
stderr(message(fmt_fileinfo($stats, 'output'), ['type'=>'write']));
}
if ($warnings) {
stderr(message($warnings, ['type'=>'warning']));
}
if ($args->stats) {
stderr(message($stats, ['type'=>'stats']));
}
if ($stdOutput) {
stdout($stdOutput);
}
exit(STATUS_OK);
}
##################################################################
## Helpers.
function stderr($lines, $closing_newline = true) {
$out = implode(PHP_EOL, (array) $lines) . ($closing_newline ? PHP_EOL : '');
fwrite(defined('TESTMODE') && TESTMODE ? STDOUT : STDERR, $out);
}
function stdout($lines, $closing_newline = true) {
$out = implode(PHP_EOL, (array) $lines) . ($closing_newline ? PHP_EOL : '');
fwrite(STDOUT, $out);
}
function get_stdin_contents() {
stream_set_blocking(STDIN, 0);
$contents = stream_get_contents(STDIN);
stream_set_blocking(STDIN, 1);
return $contents;
}
function parse_list(array $option) {
$out = [];
foreach ($option as $arg) {
if (is_string($arg)) {
foreach (preg_split('~\s*,\s*~', $arg) as $item) {
$out[] = $item;
}
}
else {
$out[] = $arg;
}
}
return $out;
}
function message($messages, $options = []) {
$defaults = [
'color' => 'b',
'label' => null,
'indent' => false,
'format_label' => false,
];
$preset = ! empty($options['type']) ? $options['type'] : null;
switch ($preset) {
case 'error':
$defaults['color'] = 'r';
$defaults['label'] = 'ERROR';
break;
case 'warning':
$defaults['color'] = 'y';
$defaults['label'] = 'WARNING';
break;
case 'write':
$defaults['color'] = 'g';
$defaults['label'] = 'WRITE';
break;
case 'stats':
// Making stats concise and readable.
$messages['input_file'] = $messages['input_path'];
$messages['compile_time'] = round($messages['compile_time'], 5) . ' seconds';
foreach (['input_filename', 'input_path', 'output_filename',
'output_path', 'vars', 'errors', 'warnings'] as $key) {
unset($messages[$key]);
}
ksort($messages);
$defaults['indent'] = true;
$defaults['format_label'] = true;
break;
}
extract($options + $defaults);
$out = [];
foreach ((array) $messages as $_label => $value) {
$_label = $label ?: $_label;
if ($format_label) {
$_label = ucfirst(str_replace('_', ' ', $_label));
}
$prefix = $indent ? '└── ' : '';
$colorUp = strtoupper($color);
if (is_scalar($value)) {
$out[] = colorize("<$color>$prefix<$colorUp>$_label:<$color> $value</>");
}
}
return implode(PHP_EOL, $out);
}
function fmt_fileinfo($stats, $type) {
$time = round($stats['compile_time'], 3);
return $stats[$type . '_path'] . " ({$time}s)";
}
function pick(array &$arr) {
$args = func_get_args();
array_shift($args);
foreach ($args as $key) {
if (isset($arr[$key])) {
// Optional values return false but we want true is argument is present.
return is_bool($arr[$key]) ? true : $arr[$key];
}
}
return null;
}
function colorize($str) {
static $color_support;
static $tags = [
'<b>' => "\033[0;30m",
'<r>' => "\033[0;31m",
'<g>' => "\033[0;32m",
'<y>' => "\033[0;33m",
'<b>' => "\033[0;34m",
'<v>' => "\033[0;35m",
'<c>' => "\033[0;36m",
'<w>' => "\033[0;37m",
'<B>' => "\033[1;30m",
'<R>' => "\033[1;31m",
'<G>' => "\033[1;32m",
'<Y>' => "\033[1;33m",
'<B>' => "\033[1;34m",
'<V>' => "\033[1;35m",
'<C>' => "\033[1;36m",
'<W>' => "\033[1;37m",
'</>' => "\033[m",
];
if (! isset($color_support)) {
$color_support = defined('TESTMODE') && TESTMODE ? false : true;
if (DIRECTORY_SEPARATOR == '\\') {
$color_support = false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI');
}
}
$find = array_keys($tags);
$replace = $color_support ? array_values($tags) : '';
return str_replace($find, $replace, $str);
}
function get_trailing_io_args($required_value_opts) {
$trailing_input_file = null;
$trailing_output_file = null;
// Get raw script args, shift off calling scriptname and reduce to last three.
$trailing_args = $GLOBALS['argv'];
array_shift($trailing_args);
$trailing_args = array_slice($trailing_args, -3);
// Create patterns for detecting options.
$required_values = implode('|', $required_value_opts);
$value_opt_patt = "~^-{1,2}($required_values)$~";
$other_opt_patt = "~^-{1,2}([a-z0-9\-]+)?(=|$)~ix";
// Step through the args.
$filtered = [];
for ($i = 0; $i < count($trailing_args); $i++) {
$current = $trailing_args[$i];
// If tests as a required value option, reset and skip next.
if (preg_match($value_opt_patt, $current)) {
$filtered = [];
$i++;
}
// If it looks like any other kind of flag, or optional value option, reset.
elseif (preg_match($other_opt_patt, $current)) {
$filtered = [];
}
else {
$filtered[] = $current;
}
}
// We're only interested in the last two values.
$filtered = array_slice($filtered, -2);
switch (count($filtered)) {
case 1:
$trailing_input_file = $filtered[0];
break;
case 2:
$trailing_input_file = $filtered[0];
$trailing_output_file = $filtered[1];
break;
}
return [$trailing_input_file, $trailing_output_file];
}
function parse_args() {
$required_value_opts = [
'i|input|f|file', // Input file. Defaults to STDIN.
'o|output', // Output file. Defaults to STDOUT.
'E|enable|plugins',
'D|disable',
'vars|variables',
'formatter',
'vendor-target',
'context',
'import-path',
'newlines',
];
$optional_value_opts = [
'b|boilerplate',
'stat-dump',
];
$flag_opts = [
'p|pretty',
'w|watch',
'help',
'version',
'source-map',
'stats',
'test',
];
// Create option strings for getopt().
$short_opts = [];
$long_opts = [];
$join_opts = function ($opts_list, $modifier) use (&$short_opts, &$long_opts) {
foreach ($opts_list as $opt) {
foreach (explode('|', $opt) as $arg) {
if (strlen($arg) === 1) {
$short_opts[] = "$arg$modifier";
}
else {
$long_opts[] = "$arg$modifier";
}
}
}
};
$join_opts($required_value_opts, ':');
$join_opts($optional_value_opts, '::');
$join_opts($flag_opts, '');
$opts = getopt(implode($short_opts), $long_opts);
$args = new stdClass();
// Information options.
$args->help = isset($opts['h']) ?: isset($opts['help']);
$args->version = isset($opts['version']);
// File arguments.
$args->input_file = pick($opts, 'i', 'input', 'f', 'file');
$args->output_file = pick($opts, 'o', 'output');
$args->context = pick($opts, 'context');
// Flags.
$args->pretty = isset($opts['p']) ?: isset($opts['pretty']);
$args->watch = isset($opts['w']) ?: isset($opts['watch']);
$args->source_map = isset($opts['source-map']);
$args->stats = pick($opts, 'stats');
define('TESTMODE', isset($opts['test']));
// Arguments that optionally accept a single value.
$args->boilerplate = pick($opts, 'b', 'boilerplate');
$args->stat_dump = pick($opts, 'stat-dump');
// Arguments that require a single value.
$args->formatter = pick($opts, 'formatter');
$args->vars = pick($opts, 'vars', 'variables');
$args->newlines = pick($opts, 'newlines');
// Arguments that require a value but accept multiple values.
$args->enable_plugins = pick($opts, 'E', 'enable', 'plugins');
$args->vendor_target = pick($opts, 'vendor-target');
$args->import_path = pick($opts, 'import-path');
// Run multiple value arguments through array cast.
foreach (['enable_plugins', 'vendor_target'] as $arg) {
if ($args->$arg) {
$args->$arg = (array) $args->$arg;
}
}
// Detect trailing IO files from raw script arguments.
list($trailing_input_file, $trailing_output_file) = get_trailing_io_args($required_value_opts);
// If detected apply, not overriding explicit IO file options.
if (! $args->input_file && $trailing_input_file) {
$args->input_file = $trailing_input_file;
}
if (! $args->output_file && $trailing_output_file) {
$args->output_file = $trailing_output_file;
}
if ($args->input_file) {
$inputFile = $args->input_file;
if (! ($args->input_file = realpath($args->input_file))) {
throw new Exception("Input file '$inputFile' does not exist.", STATUS_ERROR);
}
}
if ($args->output_file) {
$outDir = dirname($args->output_file);
if (! realpath($outDir) && ! @mkdir($outDir, 0755, true)) {
throw new Exception('Output directory does not exist and could not be created.', STATUS_ERROR);
}
$args->output_file = realpath($outDir) . '/' . basename($args->output_file);
}
if ($args->context) {
if (! ($args->context = realpath($args->context))) {
throw new Exception('Context path does not exist.', STATUS_ERROR);
}
}
else {
$args->context = $args->input_file ? dirname($args->input_file) : getcwd();
}
if (is_string($args->boilerplate)) {
if (! ($args->boilerplate = realpath($args->boilerplate))) {
throw new Exception('Boilerplate file does not exist.', STATUS_ERROR);
}
}
return $args;
}
function manpage() {
$manpage = <<<TPL
<B>USAGE:</>
<B>csscrush <G>[OPTIONS] <g>[input-file] [output-file]
<B>OPTIONS:</>
<G>-i<g>, --input</>
Input file. If omitted takes input from STDIN.
<G>-o<g>, --output</>
Output file. If omitted prints to STDOUT.
<G>-p<g>, --pretty</>
Formatted, un-minified output.
<G>-w<g>, --watch</>
Watch input file for changes.
Writes to file specified with -o option or to the input file
directory with a '.crush.css' file extension.
<G>-E<g>, --plugins</>
List of plugins (comma separated) to enable.
<g>--boilerplate</>
Whether or not to output a boilerplate. Optionally accepts filepath
to a custom boilerplate template.
<g>--context</>
Filepath context for resolving relative import URLs.
Only meaningful when taking raw input from STDIN.
<g>--import-path</>
Comma separated list of additional paths to search when resolving
relative import URLs.
<g>--formatter</>
Possible values:
'block' (default)
Rules are block formatted.
'single-line'
Rules are printed in single lines.
'padded'
Rules are printed in single lines with right padded selectors.
<g>--help</>
Display this help message.
<g>--newlines</>
Force newline style on output css. Defaults to the current platform
newline. Possible values: 'windows' (or 'win'), 'unix', 'use-platform'.
<g>--source-map</>
Create a source map file (compliant with the Source Map v3 proposal).
<g>--stats</>
Display post-compile stats.
<g>--vars</>
Map of variable names in an http query string format.
<g>--vendor-target</>
Possible values:
'all'
For all vendor prefixes (default).
'none'
For no vendor prefixing.
'moz', 'webkit', 'ms' etc.
Limit to a specific vendor prefix (or comma separated list).
<g>--version</>
Display version number.
<B>EXAMPLES:</>
# Restrict vendor prefixing.
csscrush --pretty --vendor-target webkit -i styles.css
# Piped input.
cat styles.css | csscrush --vars 'foo=black&bar=white' > alt-styles.css
# Linting.
csscrush --pretty -E property-sorter -i styles.css -o linted.css
# Watch mode.
csscrush --watch -i styles.css -o compiled/styles.css
# Using custom boilerplate template.
csscrush --boilerplate=css/boilerplate.txt css/styles.css
TPL;
return colorize($manpage);
}

View file

@ -0,0 +1,3 @@
# CSS-Crush Documentation
Rendered online at http://the-echoplex.net/csscrush

View file

@ -0,0 +1,84 @@
<!--{
"title": "API functions"
}-->
## csscrush_file()
Process CSS file and return the compiled file URL.
<code>csscrush_file( string $file [, array [$options](#api--options) ] )</code>
***************
## csscrush_tag()
Process CSS file and return an html `link` tag with populated href.
<code>csscrush_tag( string $file [, array [$options](#api--options) [, array $tag\_attributes ]] )</code>
***************
## csscrush_inline()
Process CSS file and return CSS as text wrapped in html `style` tags.
<code>csscrush_inline( string $file [, array [$options](#api--options) [, array $tag\_attributes ]] )</code>
***************
## csscrush_string()
Compile a raw string of CSS string and return it.
<code>csscrush_string( string $string [, array [$options](#api--options) ] )</code>
***************
## csscrush_get()
Retrieve a config setting or option default.
`csscrush_get( string $object_name, string $property )`
### Parameters
* `$object_name` Name of object you want to inspect: 'config' or 'options'.
* `$property`
***************
## csscrush_set()
Set a config setting or option default.
`csscrush_set( string $object_name, mixed $settings )`
### Parameters
* `$object_name` Name of object you want to modify: 'config' or 'options'.
* `$settings` Associative array of keys and values to set, or callable which argument is the object specified in `$object_name`.
***************
## csscrush_plugin()
Register a plugin.
`csscrush_plugin( string $name, callable $callback )`
***************
## csscrush_stat()
Get compilation stats from the most recent compiled file.
`csscrush_stat()`

View file

@ -0,0 +1,103 @@
<!--{
"title": "Options"
}-->
<table>
<tr>
<th class="option">Option
<th class="values">Values (default in bold)
<th>Description
</tr>
<tr>
<td class="option">minify
<td class="values"><b>true</b> | false | Array
<td>Enable or disable minification. Optionally specify an array of advanced minification parameters. Currently the only advanced option is 'colors', which will compress all color values in any notation.
</tr>
<tr>
<td class="option">formatter
<td class="values"><b>block</b> | single-line | padded
<td>Set the formatting mode. Overrides minify option if both are set.
</tr>
<tr>
<td class="option">newlines
<td class="values"><b>use-platform</b> | windows/win | unix
<td>Set the output style of newlines
</tr>
<tr>
<td class="option">boilerplate
<td class="values"><b>true</b> | false | Path
<td>Prepend a boilerplate to the output file
</tr>
<tr>
<td class="option">versioning
<td class="values"><b>true</b> | false
<td>Append a timestamped querystring to the output filename
</tr>
<tr>
<td class="option">vars
<td class="values">Array
<td>An associative array of CSS variables to be applied at runtime. These will override variables declared globally or in the CSS.
</tr>
<tr>
<td class="option">cache
<td class="values"><b>true</b> | false
<td>Turn caching on or off.
</tr>
<tr>
<td class="option">output_dir
<td class="values">Path
<td>Specify an output directory for compiled files. Defaults to the same directory as the host file.
</tr>
<tr>
<td class="option">output_file
<td class="values">Output filename
<td>Specify an output filename (suffix is added).
</tr>
<tr>
<td class="option">asset_dir
<td class="values">Path
<td>Directory for SVG and image files generated by plugins (defaults to the main file output directory).
</tr>
<tr>
<td class="option">stat_dump
<td class="values"><b>false</b> | true | Path
<td>Save compile stats and variables to a file in json format.
</tr>
<tr>
<td class="option">vendor_target
<td class="values"><b>"all"</b> | "moz", "webkit", ... | Array
<td>Limit aliasing to a specific vendor, or an array of vendors.
</tr>
<tr>
<td class="option">rewrite_import_urls
<td class="values"><b>true</b> | false | "absolute"
<td>Rewrite relative URLs inside inlined imported files.
</tr>
<tr>
<td class="option">import_paths
<td class="values">Array
<td>Additional paths to search when resolving relative import URLs.
</tr>
<tr>
<td class="option">plugins
<td class="values">Array
<td>An array of plugin names to enable.
</tr>
<tr>
<td class="option">source_map
<td class="values">true | <b>false</b>
<td>Output a source map (compliant with the Source Map v3 proposal).
</tr>
<tr>
<td class="option">context
<td class="values">Path
<td>Context for importing resources from relative urls (Only applies to `csscrush_string()` and command line utility).
</tr>
<tr>
<td class="option">doc_root
<td class="values">Path
<td>Specify an alternative server document root for situations where the CSS is being served behind an alias or url rewritten path.
</tr>
</table>

View file

@ -0,0 +1,44 @@
<!--{
"title": "Abstract rules"
}-->
Abstract rules are generic rules that can be [extended](#core--inheritance) with the `@extend` directive or mixed in (without arguments) like regular [mixins](#core--mixins) with the `@include` directive.
```crush
@abstract ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@abstract heading {
font: bold 1rem serif;
letter-spacing: .1em;
}
.foo {
@extend ellipsis;
display: block;
}
.bar {
@extend ellipsis;
@include heading;
}
```
```css
.foo,
.bar {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.foo {
display: block;
}
.bar {
font: bold 1rem serif;
letter-spacing: .1em;
}
```

View file

@ -0,0 +1,39 @@
<!--{
"title": "Auto prefixing"
}-->
Vendor prefixes for properties, functions, @-rules and declarations are **automatically generated** based on [trusted](http://caniuse.com) [sources](http://developer.mozilla.org/en-US/docs/CSS/CSS_Reference) so you can maintain cross-browser support while keeping your source code clean and easy to maintain.
```crush
.foo {
background: linear-gradient(to right, red, white);
}
```
```css
.foo {
background: -webkit-linear-gradient(to right, red, white);
background: linear-gradient(to right, red, white);
}
```
```crush
@keyframes bounce {
50% { transform: scale(1.4); }
}
```
```css
@-webkit-keyframes bounce {
50% {-webkit-transform: scale(1.4);
transform: scale(1.4);}
}
@keyframes bounce {
50% {-webkit-transform: scale(1.4);
transform: scale(1.4);}
}
```

View file

@ -0,0 +1,25 @@
<!--{
"title": "Direct @import"
}-->
Files referenced with the `@import` directive are inlined directly to save on http requests. Relative URL paths in the CSS are also updated if necessary.
If you specify a media designation following the import URL — as per the CSS standard — the imported file content is wrapped in a `@media` block.
```crush
/* Standard CSS @import statements */
@import "print.css" print;
@import url( "small-screen.css" ) screen and ( max-width: 500px );
```
```css
@media print {
/* Contents of print.css */
}
@media screen and ( max-width: 500px ) {
/* Contents of small-screen.css */
}
```

View file

@ -0,0 +1,25 @@
<!--{
"title": "Fragments"
}-->
Fragments defined and invoked with the <code>@fragment</code> directive work in a similar way to [mixins](#core--mixins), except that they work at block level:
```crush
@fragment input-placeholder {
#(1)::-webkit-input-placeholder { color: #(0); }
#(1):-moz-placeholder { color: #(0); }
#(1)::placeholder { color: #(0); }
#(1).placeholder-state { color: #(0); }
}
@fragment input-placeholder(#777, textarea);
```
```css
textarea::-webkit-input-placeholder { color: #777; }
textarea:-moz-placeholder { color: #777; }
textarea::placeholder { color: #777; }
textarea.placeholder-state { color: #777; }
```

View file

@ -0,0 +1,26 @@
<!--{
"title": "a-adjust()"
}-->
Manipulate the opacity (alpha channel) of a color value.
<code>a-adjust( *color*, *offset* )</code>
## Parameters
* *`color`* Any valid CSS color value
* *`offset`* The percentage to offset the color opacity
## Returns
The modified color value
## Examples
```css
/* Reduce color opacity by 10% */
color: a-adjust( rgb(50,50,0) -10 );
```

View file

@ -0,0 +1,33 @@
<!--{
"title": "data-uri()"
}-->
Create a data-uri.
<code>data-uri( *url* )</code>
## Parameters
* *`url`* URL of an asset
`url` cannot be external, and must not be written with an http protocol prefix.
The following file extensions are supported: jpg, jpeg, gif, png, svg, svgz, ttf, woff
## Returns
The created data-uri as a string inside a CSS url().
## Examples
```crush
background: silver data-uri(../images/stripe.png);
```
```css
background: silver url(data:<img-data>);
```

View file

@ -0,0 +1,24 @@
<!--{
"title": "h-adjust()"
}-->
Adjust the hue of a color value.
<code>h-adjust( *color*, *offset* )</code>
## Parameters
* *`color`* Any valid CSS color value
* *`offset`* The percentage to offset the color hue (percent mark optional)
## Returns
The modified color value.
## Examples
```css
color: h-adjust( deepskyblue -10 );
```

View file

@ -0,0 +1,27 @@
<!--{
"title": "hsl-adjust()"
}-->
Manipulate the hue, saturation and lightness of a color value
<code>hsl-adjust( *color*, *hue-offset*, *saturation-offset*, *lightness-offset* )</code>
## Parameters
* *`color`* Any valid CSS color value
* *`hue-offset`* The percentage to offset the color hue
* *`saturation-offset`* The percentage to offset the color saturation
* *`lightness-offset`* The percentage to offset the color lightness
## Returns
The modified color value
## Examples
```css
/* Lighten and increase saturation */
color: hsl-adjust( red 0 5 5 );
```

View file

@ -0,0 +1,27 @@
<!--{
"title": "hsla-adjust()"
}-->
Manipulate the hue, saturation, lightness and opacity of a color value.
<code>hsla-adjust( *color*, *hue-offset*, *saturation-offset*, *lightness-offset*, *alpha-offset* )</code>
## Parameters
* *`color`* Any valid CSS color value
* *`hue-offset`* The percentage to offset the color hue
* *`saturation-offset`* The percentage to offset the color saturation
* *`lightness-offset`* The percentage to offset the color lightness
* *`alpha-offset`* The percentage to offset the color opacity
## Returns
The modified color value.
## Examples
```css
color: hsla-adjust( #f00 0 5 5 -10 );
```

View file

@ -0,0 +1,24 @@
<!--{
"title": "l-adjust()"
}-->
Adjust the lightness of a color value.
<code>l-adjust( *color*, *offset* )</code>
## Parameters
* *`color`* Any valid CSS color value
* *`offset`* The percentage to offset the color hue (percent mark optional)
## Returns
The modified color value.
## Examples
```css
color: l-adjust( deepskyblue 10 );
```

View file

@ -0,0 +1,19 @@
<!--{
"title": "math()"
}-->
Evaluate a raw mathematical expression.
<code>math( *expression* [, *unit*] )</code>
## Examples
```crush
font-size: math( 12 / 16, em );
```
```css
font-size: 0.75em;
```

View file

@ -0,0 +1,54 @@
<!--{
"title": "query()"
}-->
Copy a value from another rule.
<code>query( *target* [, *property-name* = default] [, *fallback*] )</code>
## Parameters
* *`target`* A rule selector, an abstract rule name or context keyword: `previous`, `next` (also `parent` and `top` within nested structures)
* *`property-name`* The CSS property name to copy, or just `default` to pass over. Defaults to the calling property
* *`fallback`* A CSS value to use if the target property does not exist
## Returns
The referenced property value, or the fallback if it has not been set.
## Examples
```css
.foo {
width: 40em;
height: 100em;
}
.bar {
width: query( .foo ); /* 40em */
margin-top: query( .foo, height ); /* 100em */
margin-bottom: query( .foo, default, 3em ); /* 3em */
}
```
Using context keywords:
```css
.foo {
width: 40em;
.bar {
width: 30em;
.baz: {
width: query( parent ); /* 30em */
.qux {
width: query( top ); /* 40em */
}
}
}
}
```

View file

@ -0,0 +1,25 @@
<!--{
"title": "s-adjust()"
}-->
Adjust the saturation of a color value.
<code>s-adjust( *color*, *offset* )</code>
## Parameters
* *`color`* Any valid CSS color value
* *`offset`* The percentage to offset the color hue (percent mark optional)
## Returns
The modified color value.
## Examples
```css
/* Desaturate */
color: s-adjust( deepskyblue -100 );
```

View file

@ -0,0 +1,39 @@
<!--{
"title": "this()"
}-->
Reference another property value from the same containing block.
Restricted to referencing properties that don't already reference other properties.
<code>this( *property-name*, *fallback* )</code>
## Parameters
* *`property-name`* Property name
* *`fallback`* A CSS value
## Returns
The referenced property value, or the fallback if it has not been set.
## Examples
```css
.foo {
width: this( height );
height: 100em;
}
```
********
```css
/* The following both fail because they create circular references. */
.bar {
height: this( width );
width: this( height );
}
```

View file

@ -0,0 +1,127 @@
<!--{
"title": "Rule inheritance"
}-->
By using the `@extend` directive and passing it a named ruleset or selector from any other rule you can share styles more effectively across a stylesheet.
[Abstract rules](#core--abstract) can be used if you just need to extend a generic set of declarations.
```crush
.negative-text {
overflow: hidden;
text-indent: -9999px;
}
.sidebar-headline {
@extend .negative-text;
background: url( headline.png ) no-repeat;
}
```
```css
.negative-text,
.sidebar-headline {
overflow: hidden;
text-indent: -9999px;
}
.sidebar-headline {
background: url( headline.png ) no-repeat;
}
```
Inheritance is recursive:
```crush
.one { color: pink; }
.two { @extend .one; }
.three { @extend .two; }
.four { @extend .three; }
```
```css
.one, .two, .three, .four { color: pink; }
```
## Referencing by name
If you want to reference a rule without being concerned about later changes to the identifying selector use the `@name` directive:
```crush
.foo123 {
@name foo;
text-decoration: underline;
}
.bar {
@include foo;
}
.baz {
@extend foo;
}
```
## Extending with pseudo classes/elements
`@extend` arguments can adopt pseudo classes/elements by appending an exclamation mark:
```crush
.link-base {
color: #bada55;
text-decoration: underline;
}
.link-base:hover,
.link-base:focus {
text-decoration: none;
}
.link-footer {
@extend .link-base, .link-base:hover!, .link-base:focus!;
color: blue;
}
```
```css
.link-base,
.link-footer {
color: #bada55;
text-decoration: underline;
}
.link-base:hover,
.link-base:focus,
.link-footer:hover,
.link-footer:focus {
text-decoration: none;
}
.link-footer {
color: blue;
}
```
The same outcome can also be achieved with an [Abstract rule](#core--abstract) wrapper to simplify repeated use:
```crush
.link-base {
color: #bada55;
text-decoration: underline;
}
.link-base:hover,
.link-base:focus {
text-decoration: none;
}
@abstract link-base {
@extend .link-base, .link-base:hover!, .link-base:focus!;
}
.link-footer {
@extend link-base;
color: blue;
}
```

View file

@ -0,0 +1,41 @@
<!--{
"title": "Loops"
}-->
For...in loops with lists and generator functions.
```crush
@for fruit in apple, orange, pear {
.#(fruit) {
background-image: url("images/#(fruit).jpg");
}
}
```
```css
.apple { background-image: url(images/apple.jpg); }
.orange { background-image: url(images/orange.jpg); }
.pear { background-image: url(images/pear.jpg); }
```
```crush
@for base in range(2, 24) {
@for i in range(1, #(base)) {
.grid-#(i)-of-#(base) {
width: math(#(i) / #(base) * 100, %);
}
}
}
```
```css
.grid-1-of-2 { width: 50%; }
.grid-2-of-2 { width: 100%; }
/*
Intermediate steps ommited.
*/
.grid-23-of-24 { width: 95.83333%; }
.grid-24-of-24 { width: 100%; }
```

View file

@ -0,0 +1,95 @@
<!--{
"title": "Mixins"
}-->
Mixins make reusing small snippets of CSS much simpler. You define them with the `@mixin` directive.
Positional arguments via the argument function `#()` extend the capability of mixins for repurposing in different contexts.
```crush
@mixin display-font {
font-family: "Arial Black", sans-serif;
font-size: #(0);
letter-spacing: #(1);
}
/* Another mixin with default arguments */
@mixin blue-theme {
color: #(0 navy);
background-image: url("images/#(1 cross-hatch).png");
}
/* Applying the mixins */
.foo {
@include display-font(100%, .1em), blue-theme;
}
```
```css
.foo {
font-family: "Arial Black", sans-serif;
font-size: 100%;
letter-spacing: .1em;
color: navy;
background-image: url("images/cross-hatch.png");
}
```
## Skipping arguments
Mixin arguments can be skipped by using the **default** keyword:
```crush
@mixin display-font {
font-size: #(0 100%);
letter-spacing: #(1);
}
/* Applying the mixin skipping the first argument so the
default value is used instead */
#foo {
@include display-font(default, .3em);
}
```
Sometimes you may need to use the same positional argument more than once. In this case the default value only needs to be specified once:
```crush
@mixin square {
width: #(0 10px);
height: #(0);
}
.foo {
@include square;
}
```
```css
#foo {
width: 10px;
height: 10px;
}
```
## Mixing-in from other sources
Normal rules and [abstract rules](#core--abstract) can also be used as static mixins without arguments:
```crush
@abstract negative-text {
text-indent: -9999px;
overflow: hidden;
}
#main-content .theme-border {
border: 1px solid maroon;
}
.foo {
@include negative-text, #main-content .theme-border;
}
```

View file

@ -0,0 +1,49 @@
<!--{
"title": "Nesting"
}-->
Rules can be nested to avoid repetitive typing when scoping to a common parent selector.
```crush
.homepage {
color: #333;
background: white;
.content {
p {
font-size: 110%;
}
}
}
```
```css
.homepage {
color: #333;
background: white;
}
.homepage .content p {
font-size: 110%;
}
```
## Parent referencing
You can use the parent reference symbol `&` for placing the parent selector explicitly.
```crush
.homepage {
.no-js & {
p {
font-size: 110%;
}
}
}
```
```css
.no-js .homepage p {
font-size: 110%;
}
```

View file

@ -0,0 +1,80 @@
<!--{
"title": "Selector aliases"
}-->
Selector aliases can be useful for grouping together common selector chains for reuse.
They're defined with the `@selector` directive, and can be used anywhere you might use a pseudo class.
```crush
@selector heading :any(h1, h2, h3, h4, h5, h6);
@selector radio input[type="radio"];
@selector hocus :any(:hover, :focus);
/* Selector aliases with arguments */
@selector class-prefix :any([class^="#(0)"], [class*=" #(0)"]);
@selector col :class-prefix(-col);
.sidebar :heading {
color: honeydew;
}
:radio {
margin-right: 4px;
}
:col {
float: left;
}
p a:hocus {
text-decoration: none;
}
```
```css
.sidebar h1, .sidebar h2,
.sidebar h3, .sidebar h4,
.sidebar h5, .sidebar h6 {
color: honeydew;
}
input[type="radio"] {
margin-right: 4px;
}
[class^="col-"],
[class*=" col-"] {
border: 1px solid rgba(0,0,0,.5);
}
p a:hover,
p a:focus {
text-decoration: none;
}
```
## Selector splatting
Selector splats are a special kind of selector alias that expand using passed arguments.
```crush
@selector-splat input input[type="#(text)"];
form :input(time, text, url, email, number) {
border: 1px solid;
}
```
```css
form input[type="time"],
form input[type="text"],
form input[type="url"],
form input[type="email"],
form input[type="number"] {
border: 1px solid;
}
```

View file

@ -0,0 +1,22 @@
<!--{
"title": "Selector grouping"
}-->
Selector grouping with the `:any` pseudo class (modelled after CSS4 :matches) simplifies the creation of complex selector chains.
```crush
:any( .sidebar, .block ) a:any( :hover, :focus ) {
color: lemonchiffon;
}
```
```css
.block a:hover,
.block a:focus,
.sidebar a:hover,
.sidebar a:focus {
color: lemonchiffon;
}
```

View file

@ -0,0 +1,62 @@
<!--{
"title": "Variables"
}-->
Declare variables in your CSS with a `@set` directive and use them with the `$()` function.
Variables can also be injected at runtime with the [vars option](#api--options).
```crush
/* Defining variables */
@set {
dark: #333;
light: #F4F2E2;
smaller-screen: screen and (max-width: 800px);
}
/* Using variables */
@media $(smaller-screen) {
ul, p {
color: $(dark);
/* Using a fallback value with an undefined variable */
background-color: $(accent-color, #ff0);
}
}
```
*******
```css
/* Interpolation */
.username::before {
content: "$(greeting)";
}
```
## Conditionals
Sections of CSS can be included and excluded on the basis of variable existence with the `@ifset` directive:
```crush
@set foo #f00;
@set bar true;
@ifset foo {
p {
color: $(foo);
}
}
p {
font-size: 12px;
@ifset not foo {
line-height: 1.5;
}
@ifset bar(true) {
margin-bottom: 5px;
}
}
```

View file

@ -0,0 +1,26 @@
<!--{
"title": "JavaScript"
}-->
This preprocessor is written in PHP, so as prerequisite you will need to have PHP installed on your system to use the JS api.
```shell
npm install csscrush
```
All methods can take the standard options (camelCase) as the second argument.
```php
const csscrush = require('csscrush');
// Compile. Returns promise.
csscrush.file('./styles.css', {sourceMap: true});
// Compile string of CSS. Returns promise.
csscrush.string('* {box-sizing: border-box;}');
// Compile and watch file. Returns event emitter (triggers 'data' on compile).
csscrush.watch('./styles.css');
```

View file

@ -0,0 +1,17 @@
<!--{
"title": "PHP"
}-->
If you're using [Composer](http://getcomposer.org) you can use Crush in your project with the following line in your terminal:
```shell
composer require css-crush/css-crush
```
If you're not using Composer yet just download the library ([zip](http://github.com/peteboere/css-crush/zipball/master) or [tar](http://github.com/peteboere/css-crush/tarball/master)) into a convenient location and require the bootstrap file:
```php
<?php require_once 'path/to/CssCrush.php'; ?>
```

View file

@ -0,0 +1,21 @@
Pseudo classes for working with ARIA roles, states and properties.
* [ARIA roles spec](http://www.w3.org/TR/wai-aria/roles)
* [ARIA states and properties spec](http://www.w3.org/TR/wai-aria/states_and_properties)
````crush
:role(tablist) {...}
:aria-expanded {...}
:aria-expanded(false) {...}
:aria-label {...}
:aria-label(foobarbaz) {...}
````
````css
[role="tablist"] {...}
[aria-expanded="true"] {...}
[aria-expanded="false"] {...}
[aria-label] {...}
[aria-label="foobarbaz"] {...}
````

View file

@ -0,0 +1,55 @@
Bitmap image generator.
Requires the GD image library bundled with PHP.
```crush
/* Create square semi-opaque png. */
@canvas foo {
width: 50;
height: 50;
fill: rgba(255, 0, 0, .5);
}
body {
background: white canvas(foo);
}
```
*****
```crush
/* White to transparent east facing gradient with 10px
margin and background fill. */
@canvas horz-gradient {
width: #(0);
height: 150;
fill: canvas-linear-gradient(to right, #(1 white), #(2 rgba(255,255,255,0)));
background-fill: powderblue;
margin: 10;
}
/* Rectangle 300x150. */
body {
background: canvas(horz-gradient, 300);
}
/* Flipped gradient, using canvas-data() to generate a data URI. */
.bar {
background: canvas-data(horz-gradient, 100, rgba(255,255,255,0), white);
}
```
*****
```crush
/* Google logo resized to 400px width and given a sepia effect. */
@canvas sepia {
src: url(http://www.google.com/images/logo.png);
width: 400;
canvas-filter: greyscale() colorize(45, 45, 0);
}
.bar {
background: canvas(sepia);
}
```

View file

@ -0,0 +1,37 @@
Expanded easing keywords for transitions.
* ease-in-out-back
* ease-in-out-circ
* ease-in-out-expo
* ease-in-out-sine
* ease-in-out-quint
* ease-in-out-quart
* ease-in-out-cubic
* ease-in-out-quad
* ease-out-back
* ease-out-circ
* ease-out-expo
* ease-out-sine
* ease-out-quint
* ease-out-quart
* ease-out-cubic
* ease-out-quad
* ease-in-back
* ease-in-circ
* ease-in-expo
* ease-in-sine
* ease-in-quint
* ease-in-quart
* ease-in-cubic
* ease-in-quad
See [easing demos](http://easings.net) for live examples.
```crush
transition: .2s ease-in-quad;
```
```css
transition: .2s cubic-bezier(.550,.085,.680,.530);
```

View file

@ -0,0 +1,16 @@
Pseudo classes for working with forms.
```crush
:input(date, search, email) {...}
:checkbox {...}
:radio {...}
:text {...}
```
```css
input[type="date"], input[type="search"], input[type="email"] {...}
input[type="checkbox"] {...}
input[type="radio"] {...}
input[type="text"] {...}
```

View file

@ -0,0 +1,12 @@
Composite :hover/:focus/:active pseudo classes.
```crush
a:hocus { color: red; }
a:pocus { color: red; }
```
```css
a:hover, a:focus { color: red; }
a:hover, a:focus, a:active { color: red; }
```

View file

@ -0,0 +1,21 @@
Property sorting.
Examples use the predefined property sorting table. To define a custom sorting order pass an array to `csscrush_set_property_sort_order()`
```crush
color: red;
background: #000;
opacity: .5;
display: block;
position: absolute;
```
```css
position: absolute;
display: block;
opacity: .5;
color: red;
background: #000;
```

View file

@ -0,0 +1,48 @@
Functions for creating SVG gradients with a CSS gradient like syntax.
Primarily useful for supporting Internet Explorer 9.
## svg-linear-gradent()
Syntax is the same as [linear-gradient()](http://dev.w3.org/csswg/css3-images/#linear-gradient)
```syntax
svg-linear-gradent( [ <angle> | to <side-or-corner> ,]? <color-stop> [, <color-stop>]+ )
```
### Returns
A base64 encoded svg data-uri.
### Known issues
Color stops can only take percentage value offsets.
```css
background-image: svg-linear-gradient( to top left, #fff, rgba(255,255,255,0) 80% );
background-image: svg-linear-gradient( 35deg, red, gold 20%, powderblue );
```
## svg-radial-gradent()
Syntax is similar to but more limited than [radial-gradient()](http://dev.w3.org/csswg/css3-images/#radial-gradient)
```syntax
svg-radial-gradent( [ <origin> | at <position> ,]? <color-stop> [, <color-stop>]+ )
```
### Returns
A base64 encoded svg data-uri.
### Known issues
Color stops can only take percentage value offsets.
No control over shape - only circular gradients - however, the generated image can be stretched with background-size.
```css
background-image: svg-radial-gradient( at center, red, blue 50%, yellow );
background-image: svg-radial-gradient( 100% 50%, rgba(255,255,255,.5), rgba(255,255,255,0) );
```

View file

@ -0,0 +1,74 @@
Define and embed simple SVG elements, paths and effects inside CSS
```crush
@svg foo {
type: star;
star-points: #(0 5);
radius: 100 50;
margin: 20;
stroke: black;
fill: red;
fill-opacity: .5;
}
/* Embed SVG with svg() function (generates an svg file). */
body {
background: svg(foo);
}
/* As above but a 3 point star creating a data URI instead of a file. */
body {
background: svg-data(foo, 3);
}
```
*******
```crush
/* Using path data and stroke styles to create a plus sign. */
@svg plus {
d: "M0,5 h10 M5,0 v10";
width: 10;
height: 10;
stroke: white;
stroke-linecap: round;
stroke-width: 2;
}
```
*******
```crush
/* Skewed circle with radial gradient fill and drop shadow. */
@svg circle {
type: circle;
transform: skewX(30);
diameter: 60;
margin: 20;
fill: svg-radial-gradient(at top right, gold 50%, red);
drop-shadow: 2 2 0 rgba(0,0,0,1);
}
```
*******
```crush
/* 8-sided polygon with an image fill.
Note: images usually have to be converted to data URIs, see known issues below. */
@svg pattern {
type: polygon;
sides: 8;
diameter: 180;
margin: 20;
fill: pattern(data-uri(kitten.jpg), scale(1) translate(-100 0));
fill-opacity: .8;
}
```
### Known issues
Firefox [does not allow linked images](https://bugzilla.mozilla.org/show_bug.cgi?id=628747#c0) (or other svg) when svg is in "svg as image" mode.

153
include/thirdparty/css-crush/index.js vendored Normal file
View file

@ -0,0 +1,153 @@
/*eslint no-control-regex: 0*/
const path = require('path');
const querystring = require('querystring');
const {EventEmitter} = require('events');
const cliPath = path.resolve(__dirname, './cli.php');
const processes = [];
const processExec = (...args) => {
processes.push(require('child_process').exec(...args));
return processes[processes.length-1];
};
process.on('exit', () => {
processes.filter(it => it).forEach(proc => proc.kill());
});
const self = module.exports = {};
class Process extends EventEmitter {
exec(options) {
return new Promise(resolve => {
let command = this.assembleCommand(options);
const {stdIn} = options;
if (stdIn) {
command = `echo '${stdIn.replace(/'/g, "\\'")}' | ${command}`;
}
processExec(command, (error, stdout, stderr) => {
process.stderr.write(stderr.toString());
if (error) {
return resolve(false);
}
const stdOut = stdout.toString();
if (stdIn) {
process.stdout.write(stdOut);
}
return resolve(stdOut || true);
});
});
}
watch(options) {
options.watch = true;
const command = this.assembleCommand(options);
const proc = processExec(command);
/*
* Emitting 'error' events from EventEmitter without
* any error listener will throw uncaught exception.
*/
this.on('error', () => {});
proc.stderr.on('data', msg => {
msg = msg.toString();
process.stderr.write(msg);
msg = msg.replace(/\x1B\[[^m]*m/g, '').trim();
const [, signal, detail] = /^([A-Z]+):\s*(.+)/i.exec(msg) || [];
const {input, output} = options;
const eventData = {
signal,
options: {
input: input ? path.resolve(input) : null,
output: output ? path.resolve(output) : null,
},
};
if (/^(WARNING|ERROR)$/.test(signal)) {
const error = new Error(detail);
Object.assign(error, eventData, {severity: signal.toLowerCase()});
this.emit('error', error);
}
else {
this.emit('data', {message: detail, ...eventData});
}
});
return this;
}
assembleCommand(options) {
return `${self.phpBin || 'php'} ${cliPath} ${this.stringifyOptions(options)}`;
}
stringifyOptions(options) {
const args = [];
options = {...options};
for (let name in options) {
// Normalize to hypenated case.
const cssCase = name.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);
if (name !== cssCase) {
options[cssCase] = options[name];
delete options[name];
name = cssCase;
}
let value = options[name];
switch (name) {
// Booleans.
case 'watch': // fallthrough
case 'source-map': // fallthrough
case 'boilerplate': // fallthrough
if (value) {
args.push(`--${name}`);
}
break;
case 'minify':
if (! value) {
args.push(`--pretty`);
}
break;
// Array/list values.
case 'vendor-target': // fallthrough
case 'plugins': // fallthrough
case 'import-path':
if (value) {
value = (Array.isArray(value) ? value : [value]).join(',');
args.push(`--${name}="${value}"`);
}
break;
// String values.
case 'newlines': // fallthrough
case 'formatter': // fallthrough
case 'input': // fallthrough
case 'context': // fallthrough
case 'output':
if (value) {
args.push(`--${name}="${value}"`);
}
break;
case 'vars':
args.push(`--${name}="${querystring.stringify(value)}"`);
break;
}
}
return args.join(' ');
}
}
self.watch = (file, options={}) => {
options.input = file;
return (new Process()).watch(options);
};
self.file = (file, options={}) => {
options.input = file;
return (new Process()).exec(options);
};
self.string = (string, options={}) => {
options.stdIn = string;
return (new Process()).exec(options);
};

View file

@ -0,0 +1,66 @@
<?php
/**
*
* Balanced bracket matching on string objects.
*
*/
namespace CssCrush;
class BalancedMatch
{
public function __construct(StringObject $string, $offset, $brackets = '{}')
{
$this->string = $string;
$this->offset = $offset;
$this->match = null;
$this->length = 0;
list($opener, $closer) = str_split($brackets, 1);
if (strpos($string->raw, $opener, $this->offset) === false) {
return;
}
if (substr_count($string->raw, $opener) !== substr_count($string->raw, $closer)) {
$sample = substr($string->raw, $this->offset, 25);
warning("Unmatched token near '$sample'.");
return;
}
$patt = ($opener === '{') ? Regex::$patt->block : Regex::$patt->parens;
if (preg_match($patt, $string->raw, $m, PREG_OFFSET_CAPTURE, $this->offset)) {
$this->match = $m;
$this->matchLength = strlen($m[0][0]);
$this->matchStart = $m[0][1];
$this->matchEnd = $this->matchStart + $this->matchLength;
$this->length = $this->matchEnd - $this->offset;
}
else {
warning("Could not match '$opener'. Exiting.");
}
}
public function inside()
{
return $this->match[2][0];
}
public function whole()
{
return substr($this->string->raw, $this->offset, $this->length);
}
public function replace($replacement)
{
$this->string->splice($replacement, $this->offset, $this->length);
}
public function unWrap()
{
$this->string->splice($this->inside(), $this->offset, $this->length);
}
}

View file

@ -0,0 +1,74 @@
<?php
/*
*/
namespace CssCrush;
class Collection extends Iterator
{
public $store;
public function __construct(array $store)
{
$this->store = $store;
}
public function get($index = null)
{
return is_int($index) ? $this->store[$index] : $this->store;
}
static public function value($item, $property)
{
if (strpos($property, '|') !== false) {
$filters = explode('|', $property);
$property = array_shift($filters);
$value = $item->$property;
foreach ($filters as $filter) {
switch ($filter) {
case 'lower':
$value = strtolower($value);
break;
}
}
return $value;
}
return $item->$property;
}
public function filter($filterer, $op = '===')
{
if (is_array($filterer)) {
$ops = [
'===' => function ($item) use ($filterer) {
foreach ($filterer as $property => $value) {
if (Collection::value($item, $property) !== $value) {
return false;
}
}
return true;
},
'!==' => function ($item) use ($filterer) {
foreach ($filterer as $property => $value) {
if (Collection::value($item, $property) === $value) {
return false;
}
}
return true;
},
];
$callback = $ops[$op];
}
elseif (is_callable($filterer)) {
$callback = $filterer;
}
if (isset($callback)) {
$this->store = array_filter($this->store, $callback);
}
return $this;
}
}

View file

@ -0,0 +1,472 @@
<?php
/**
*
* Colour parsing and conversion.
*
*/
namespace CssCrush;
class Color
{
protected static $minifyableKeywords;
public static function getKeywords()
{
static $namedColors;
if (! isset($namedColors)) {
if ($colors = Util::parseIni(Crush::$dir . '/misc/color-keywords.ini')) {
foreach ($colors as $name => $rgb) {
$namedColors[$name] = array_map('floatval', explode(',', $rgb)) + [0, 0, 0, 1];
}
}
}
return isset(Crush::$process->colorKeywords) ? Crush::$process->colorKeywords : $namedColors;
}
public static function getMinifyableKeywords()
{
if (! isset(self::$minifyableKeywords)) {
// If color name is longer than 4 and less than 8 test to see if its hex
// representation could be shortened.
$keywords = self::getKeywords();
foreach ($keywords as $name => $rgba) {
$name_len = strlen($name);
if ($name_len < 5) {
continue;
}
$hex = self::rgbToHex($rgba);
if ($name_len > 7) {
self::$minifyableKeywords[$name] = $hex;
}
else {
if (preg_match(Regex::$patt->cruftyHex, $hex)) {
self::$minifyableKeywords[$name] = $hex;
}
}
}
}
return self::$minifyableKeywords;
}
public static function parse($str)
{
if ($test = Color::test($str)) {
$color = $test['value'];
$type = $test['type'];
}
else {
return false;
}
$rgba = false;
switch ($type) {
case 'hex':
$rgba = Color::hexToRgb($color);
break;
case 'rgb':
case 'rgba':
case 'hsl':
case 'hsla':
$function = $type;
$vals = substr($color, strlen($function) + 1); // Trim function name and start paren.
$vals = substr($vals, 0, strlen($vals) - 1); // Trim end paren.
$vals = array_map('trim', explode(',', $vals)); // Explode to array of arguments.
// Always set the alpha channel.
$vals[3] = isset($vals[3]) ? floatval($vals[3]) : 1;
if (strpos($function, 'rgb') === 0) {
$rgba = Color::normalizeCssRgb($vals);
}
else {
$rgba = Color::cssHslToRgb($vals);
}
break;
case 'keyword':
$keywords = self::getKeywords();
$rgba = $keywords[$color];
break;
}
return $rgba;
}
public static function test($str)
{
static $color_patt;
if (! $color_patt) {
$color_patt = Regex::make('~^(
\#(?={{hex}}{3}) |
\#(?={{hex}}{6}) |
rgba?(?=\() |
hsla?(?=\()
)~ixS');
}
$color_test = [];
$str = strtolower(trim($str));
// First match a hex value or the start of a function.
if (preg_match($color_patt, $str, $m)) {
$type_match = $m[1];
switch ($type_match) {
case '#':
$color_test['type'] = 'hex';
break;
case 'hsl':
case 'hsla':
case 'rgb':
case 'rgba':
$color_test['type'] = $type_match;
break;
}
}
// Secondly try to match a color keyword.
else {
$keywords = self::getKeywords();
if (isset($keywords[$str])) {
$color_test['type'] = 'keyword';
}
}
if ($color_test) {
$color_test['value'] = $str;
}
return $color_test ? $color_test : false;
}
/**
* http://mjijackson.com/2008/02/
* rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
*
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*/
public static function rgbToHsl(array $rgba)
{
list($r, $g, $b, $a) = $rgba;
$r /= 255;
$g /= 255;
$b /= 255;
$max = max($r, $g, $b);
$min = min($r, $g, $b);
$h = 0;
$s = 0;
$l = ($max + $min) / 2;
if ($max == $min) {
$h = $s = 0;
}
else {
$d = $max - $min;
$s = $l > 0.5 ? $d / (2 - $max - $min) : $d / ($max + $min);
switch($max) {
case $r:
$h = ($g - $b) / $d + ($g < $b ? 6 : 0);
break;
case $g:
$h = ($b - $r) / $d + 2;
break;
case $b:
$h = ($r - $g) / $d + 4;
break;
}
$h /= 6;
}
return [$h, $s, $l, $a];
}
/**
* http://mjijackson.com/2008/02/
* rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
*
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*/
public static function hslToRgb(array $hsla)
{
// Populate unspecified alpha value.
if (! isset($hsla[3])) {
$hsla[3] = 1;
}
list($h, $s, $l, $a) = $hsla;
$r = 0;
$g = 0;
$b = 0;
if ($s == 0) {
$r = $g = $b = $l;
}
else {
$q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s;
$p = 2 * $l - $q;
$r = self::hueToRgb($p, $q, $h + 1 / 3);
$g = self::hueToRgb($p, $q, $h);
$b = self::hueToRgb($p, $q, $h - 1 / 3);
}
return [round($r * 255), round($g * 255), round($b * 255), $a];
}
// Convert percentages to points (0-255).
public static function normalizeCssRgb(array $rgba)
{
foreach ($rgba as &$val) {
if (strpos($val, '%') !== false) {
$val = str_replace('%', '', $val);
$val = round($val * 2.55);
}
}
return $rgba;
}
public static function cssHslToRgb(array $hsla)
{
// Populate unspecified alpha value.
if (! isset($hsla[3])) {
$hsla[3] = 1;
}
// Alpha is carried over.
$a = array_pop($hsla);
// Normalize the hue degree value then convert to float.
$h = array_shift($hsla);
$h = $h % 360;
if ($h < 0) {
$h = 360 + $h;
}
$h = $h / 360;
// Convert saturation and lightness to floats.
foreach ($hsla as &$val) {
$val = str_replace('%', '', $val);
$val /= 100;
}
list($s, $l) = $hsla;
return self::hslToRgb([$h, $s, $l, $a]);
}
public static function hueToRgb($p, $q, $t)
{
if ($t < 0) $t += 1;
if ($t > 1) $t -= 1;
if ($t < 1/6) return $p + ($q - $p) * 6 * $t;
if ($t < 1/2) return $q;
if ($t < 2/3) return $p + ($q - $p) * (2 / 3 - $t) * 6;
return $p;
}
public static function rgbToHex(array $rgba)
{
// Drop alpha component.
if (isset($rgba[3])) {
array_pop($rgba);
}
$hex_out = '#';
foreach ($rgba as $val) {
$hex_out .= str_pad(dechex($val), 2, '0', STR_PAD_LEFT);
}
return $hex_out;
}
public static function hexToRgb($hex)
{
$hex = substr($hex, 1);
// Handle shortened format.
if (strlen($hex) === 3) {
$long_hex = [];
foreach (str_split($hex) as $val) {
$long_hex[] = $val . $val;
}
$hex = $long_hex;
}
else {
$hex = str_split($hex, 2);
}
// Return RGBa
$rgba = array_map('hexdec', $hex);
$rgba[] = 1;
return $rgba;
}
public static function colorAdjust($str, array $adjustments)
{
$hsla = new Color($str, true);
// On failure to parse return input.
return $hsla->isValid ? $hsla->adjust($adjustments)->__toString() : $str;
}
public static function colorSplit($str)
{
if ($test = Color::test($str)) {
$color = $test['value'];
$type = $test['type'];
}
else {
return false;
}
// If non-alpha color return early.
if (! in_array($type, ['hsla', 'rgba'])) {
return [$color, 1];
}
// Strip all whitespace.
$color = preg_replace('~\s+~', '', $color);
// Extract alpha component if one is matched.
$opacity = 1;
if (preg_match(
Regex::make('~^(rgb|hsl)a\(({{number}}%?,{{number}}%?,{{number}}%?),({{number}})\)$~i'),
$color,
$m)
) {
$opacity = floatval($m[3]);
$color = "$m[1]($m[2])";
}
return [$color, $opacity];
}
#############################
# Instances.
protected $value;
protected $hslColorSpace;
protected $namedComponents = [
'red' => 0,
'green' => 1,
'blue' => 2,
'alpha' => 3,
];
public $isValid;
public function __construct($color, $useHslColorSpace = false)
{
$this->value = is_array($color) ? $color : self::parse($color);
$this->isValid = ! empty($this->value);
if ($useHslColorSpace && $this->isValid) {
$this->toHsl();
}
}
public function __toString()
{
// For opaque colors return hex notation as it's the most compact.
if ($this->getComponent('alpha') == 1) {
return $this->getHex();
}
// R, G and B components must be integers.
$components = [];
foreach (($this->hslColorSpace ? $this->getRgb() : $this->value) as $index => $component) {
$components[] = ($index === 3) ? $component : min(round($component), 255);
}
return 'rgba(' . implode(',', $components) . ')';
}
public function toRgb()
{
if ($this->hslColorSpace) {
$this->hslColorSpace = false;
$this->value = self::hslToRgb($this->value);
}
return $this;
}
public function toHsl()
{
if (! $this->hslColorSpace) {
$this->hslColorSpace = true;
$this->value = self::rgbToHsl($this->value);
}
return $this;
}
public function getHex()
{
return self::rgbToHex($this->getRgb());
}
public function getHsl()
{
return ! $this->hslColorSpace ? self::rgbToHsl($this->value) : $this->value;
}
public function getRgb()
{
return $this->hslColorSpace ? self::hslToRgb($this->value) : $this->value;
}
public function getComponent($index)
{
$index = isset($this->namedComponents[$index]) ? $this->namedComponents[$index] : $index;
return $this->value[$index];
}
public function setComponent($index, $newComponentValue)
{
$index = isset($this->namedComponents[$index]) ? $this->namedComponents[$index] : $index;
$this->value[$index] = is_numeric($newComponentValue) ? $newComponentValue : 0;
}
public function adjust(array $adjustments)
{
$wasHslColor = $this->hslColorSpace;
$this->toHsl();
// Normalize percentage adjustment parameters to floating point numbers.
foreach ($adjustments as $index => $val) {
// Normalize argument.
$val = $val ? trim(str_replace('%', '', $val)) : 0;
if ($val) {
// Reduce value to float.
$val /= 100;
// Update the color component.
$this->setComponent($index, max(0, min(1, $this->getComponent($index) + $val)));
}
}
return ! $wasHslColor ? $this->toRgb() : $this;
}
}

View file

@ -0,0 +1,289 @@
<?php
/**
*
* Core public API.
*
*/
namespace CssCrush;
class Crush
{
// Global settings.
public static $config;
// The current active process.
public static $process;
// Library root directory.
public static $dir;
public static function init()
{
self::$dir = dirname(dirname(__DIR__));
self::$config = new \stdClass();
self::$config->pluginDirs = [self::$dir . '/plugins'];
self::$config->scriptDir = dirname(realpath($_SERVER['SCRIPT_FILENAME']));
self::$config->docRoot = self::resolveDocRoot();
self::$config->logger = new Logger();
self::$config->io = 'CssCrush\IO';
// Shared resources.
self::$config->vars = [];
self::$config->aliasesFile = self::$dir . '/aliases.ini';
self::$config->aliases = [];
self::$config->bareAliases = [
'properties' => [],
'functions' => [],
'function_groups' => [],
'declarations' => [],
'at-rules' => [],
];
self::$config->options = new Options();
require_once self::$dir . '/misc/formatters.php';
}
static protected function resolveDocRoot($doc_root = null)
{
// Get document_root reference
// $_SERVER['DOCUMENT_ROOT'] is unreliable in certain CGI/Apache/IIS setups
if (! $doc_root) {
$script_filename = $_SERVER['SCRIPT_FILENAME'];
$script_name = $_SERVER['SCRIPT_NAME'];
if ($script_filename && $script_name) {
$len_diff = strlen($script_filename) - strlen($script_name);
// We're comparing the two strings so normalize OS directory separators
$script_filename = str_replace('\\', '/', $script_filename);
$script_name = str_replace('\\', '/', $script_name);
// Check $script_filename ends with $script_name
if (substr($script_filename, $len_diff) === $script_name) {
$path = substr($script_filename, 0, $len_diff);
$doc_root = realpath($path);
}
}
if (! $doc_root) {
$doc_root = realpath($_SERVER['DOCUMENT_ROOT']);
}
if (! $doc_root) {
warning("Could not get a valid DOCUMENT_ROOT reference.");
}
}
return Util::normalizePath($doc_root);
}
public static function loadAssets()
{
static $called;
if ($called) {
return;
}
$called = true;
if (! self::$config->aliases) {
$aliases = self::parseAliasesFile(self::$config->aliasesFile);
self::$config->aliases = $aliases ?: self::$config->bareAliases;
}
}
public static function plugin($name = null, callable $callback = null)
{
static $plugins = [];
if (! $callback) {
return isset($plugins[$name]) ? $plugins[$name] : null;
}
$plugins[$name] = $callback;
}
public static function enablePlugin($name)
{
$plugin = self::plugin($name);
if (! $plugin) {
$path = self::$dir . "/plugins/$name.php";
if (! file_exists($path)) {
notice("Plugin '$name' not found.");
return;
}
require_once $path;
$plugin = self::plugin($name);
}
$plugin(self::$process);
}
public static function parseAliasesFile($file)
{
if (! ($tree = Util::parseIni($file, true))) {
return false;
}
$regex = Regex::$patt;
// Some alias groups need further parsing to unpack useful information into the tree.
foreach ($tree as $section => $items) {
if ($section === 'declarations') {
$store = [];
foreach ($items as $prop_val => $aliases) {
list($prop, $value) = array_map('trim', explode(':', $prop_val));
foreach ($aliases as &$alias) {
list($p, $v) = explode(':', $alias);
$vendor = null;
// Try to detect the vendor from property and value in turn.
if (
preg_match($regex->vendorPrefix, $p, $m)
|| preg_match($regex->vendorPrefix, $v, $m)
) {
$vendor = $m[1];
}
$alias = [$p, $v, $vendor];
}
$store[$prop][$value] = $aliases;
}
$tree['declarations'] = $store;
}
// Function groups.
elseif (strpos($section, 'functions.') === 0) {
$group = substr($section, strlen('functions'));
$vendor_grouped_aliases = [];
foreach ($items as $func_name => $aliases) {
// Assign group name to the aliasable function.
$tree['functions'][$func_name] = $group;
foreach ($aliases as $alias_func) {
// Only supporting vendor prefixed aliases, for now.
if (preg_match($regex->vendorPrefix, $alias_func, $m)) {
// We'll cache the function matching regex here.
$vendor_grouped_aliases[$m[1]]['find'][] = Regex::make("~{{ LB }}$func_name(?=\()~iS");
$vendor_grouped_aliases[$m[1]]['replace'][] = $alias_func;
}
}
}
$tree['function_groups'][$group] = $vendor_grouped_aliases;
unset($tree[$section]);
}
}
$tree += self::$config->bareAliases;
// Persisting dummy aliases for testing purposes.
$tree['properties']['foo'] =
$tree['at-rules']['foo'] =
$tree['functions']['foo'] = ['-webkit-foo', '-moz-foo', '-ms-foo'];
return $tree;
}
#############################
# Logging and stats.
public static function printLog()
{
if (! empty(self::$process->debugLog)) {
if (PHP_SAPI !== 'cli') {
$out = [];
foreach (self::$process->debugLog as $item) {
$out[] = '<pre>' . htmlspecialchars($item) . '</pre>';
}
echo implode('<hr>', $out);
}
else {
echo implode(PHP_EOL, self::$process->debugLog), PHP_EOL;
}
}
}
public static function runStat()
{
$process = Crush::$process;
foreach (func_get_args() as $stat_name) {
switch ($stat_name) {
case 'paths':
$process->stat['input_filename'] = $process->input->filename;
$process->stat['input_path'] = $process->input->path;
$process->stat['output_filename'] = $process->output->filename;
$process->stat['output_path'] = $process->output->dir . '/' . $process->output->filename;
break;
case 'vars':
$process->stat['vars'] = array_map(function ($item) use ($process) {
return $process->tokens->restore($process->functions->apply($item), ['s', 'u', 'p']);
}, $process->vars);
break;
case 'compile_time':
$process->stat['compile_time'] = microtime(true) - $process->stat['compile_start_time'];
unset($process->stat['compile_start_time']);
break;
case 'selector_count':
$process->stat['selector_count'] = 0;
foreach ($process->tokens->store->r as $rule) {
$process->stat['selector_count'] += count($rule->selectors);
}
break;
case 'rule_count':
$process->stat['rule_count'] = count($process->tokens->store->r);
break;
}
}
}
}
function warning($message, $context = []) {
Crush::$process->errors[] = $message;
$logger = Crush::$config->logger;
if ($logger instanceof Logger) {
$message = "[CssCrush] $message";
}
$logger->warning($message, $context);
}
function notice($message, $context = []) {
Crush::$process->warnings[] = $message;
$logger = Crush::$config->logger;
if ($logger instanceof Logger) {
$message = "[CssCrush] $message";
}
$logger->notice($message, $context);
}
function debug($message, $context = []) {
Crush::$config->logger->debug($message, $context);
}
function log($message, $context = [], $type = 'debug') {
Crush::$config->logger->$type($message, $context);
}
Crush::init();

View file

@ -0,0 +1,135 @@
<?php
/**
*
* Declaration objects.
*
*/
namespace CssCrush;
class Declaration
{
public $property;
public $canonicalProperty;
public $vendor;
public $functions;
public $value;
public $index;
public $skip = false;
public $important = false;
public $custom = false;
public $valid = true;
public function __construct($property, $value, $contextIndex = 0)
{
// Normalize, but preserve case if a custom property.
if (strpos($property, '--') === 0) {
$this->custom = true;
$this->skip = true;
}
else {
$property = strtolower($property);
}
if ($this->skip = strpos($property, '~') === 0) {
$property = substr($property, 1);
}
// Store the canonical property name.
// Store the vendor mark if one is present.
if (preg_match(Regex::$patt->vendorPrefix, $property, $vendor)) {
$canonical_property = $vendor[2];
$vendor = $vendor[1];
}
else {
$vendor = null;
$canonical_property = $property;
}
// Check for !important.
if (($important = stripos($value, '!important')) !== false) {
$value = rtrim(substr($value, 0, $important));
$this->important = true;
}
Crush::$process->emit('declaration_preprocess', ['property' => &$property, 'value' => &$value]);
// Reject declarations with empty CSS values.
if ($value === false || $value === '') {
$this->valid = false;
}
$this->property = $property;
$this->canonicalProperty = $canonical_property;
$this->vendor = $vendor;
$this->index = $contextIndex;
$this->value = $value;
}
public function __toString()
{
if (Crush::$process->minifyOutput) {
$whitespace = '';
}
else {
$whitespace = ' ';
}
$important = $this->important ? "$whitespace!important" : '';
return "$this->property:$whitespace$this->value$important";
}
/*
Execute functions on value.
Index functions.
*/
public function process($parentRule)
{
static $thisFunction;
if (! $thisFunction) {
$thisFunction = new Functions(['this' => 'CssCrush\fn__this']);
}
if (! $this->skip) {
// this() function needs to be called exclusively because it is self referencing.
$context = (object) [
'rule' => $parentRule
];
$this->value = $thisFunction->apply($this->value, $context);
if (isset($parentRule->declarations->data)) {
$parentRule->declarations->data += [$this->property => $this->value];
}
$context = (object) [
'rule' => $parentRule,
'property' => $this->property
];
$this->value = Crush::$process->functions->apply($this->value, $context);
}
// Whitespace may have been introduced by functions.
$this->value = trim($this->value);
if ($this->value === '') {
$this->valid = false;
return;
}
$parentRule->declarations->queryData[$this->property] = $this->value;
$this->indexFunctions();
}
public function indexFunctions()
{
// Create an index of all regular functions in the value.
$functions = [];
if (preg_match_all(Regex::$patt->functionTest, $this->value, $m)) {
foreach ($m['func_name'] as $fn_name) {
$functions[strtolower($fn_name)] = true;
}
}
$this->functions = $functions;
}
}

View file

@ -0,0 +1,598 @@
<?php
/**
*
* Declaration lists.
*
*/
namespace CssCrush;
class DeclarationList extends Iterator
{
public $flattened = true;
public $processed = false;
protected $rule;
public $properties = [];
public $canonicalProperties = [];
// Declarations hash table for inter-rule this() referencing.
public $data = [];
// Declarations hash table for external query() referencing.
public $queryData = [];
public function __construct($declarationsString, Rule $rule)
{
parent::__construct();
$this->rule = $rule;
$pairs = DeclarationList::parse($declarationsString);
foreach ($pairs as $index => $pair) {
list($prop, $value) = $pair;
// Directives.
if ($prop === 'extends') {
$this->rule->addExtendSelectors($value);
unset($pairs[$index]);
}
elseif ($prop === 'name') {
if (! $this->rule->name) {
$this->rule->name = $value;
}
unset($pairs[$index]);
}
}
// Build declaration list.
foreach ($pairs as $index => &$pair) {
list($prop, $value) = $pair;
if (trim($value) !== '') {
if ($prop === 'mixin') {
$this->flattened = false;
$this->store[] = $pair;
}
else {
// Only store to $this->data if the value does not itself make a
// this() call to avoid circular references.
if (! preg_match(Regex::$patt->thisFunction, $value)) {
$this->data[strtolower($prop)] = $value;
}
$this->add($prop, $value, $index);
}
}
}
}
public function add($property, $value, $contextIndex = 0)
{
$declaration = new Declaration($property, $value, $contextIndex);
if ($declaration->valid) {
$this->index($declaration);
$this->store[] = $declaration;
return $declaration;
}
return false;
}
public function reset(array $declaration_stack)
{
$this->store = $declaration_stack;
$this->updateIndex();
}
public function index($declaration)
{
$property = $declaration->property;
if (isset($this->properties[$property])) {
$this->properties[$property]++;
}
else {
$this->properties[$property] = 1;
}
$this->canonicalProperties[$declaration->canonicalProperty] = true;
}
public function updateIndex()
{
$this->properties = [];
$this->canonicalProperties = [];
foreach ($this->store as $declaration) {
$this->index($declaration);
}
}
public function propertyCount($property)
{
return isset($this->properties[$property]) ? $this->properties[$property] : 0;
}
public function join($glue = ';')
{
return implode($glue, $this->store);
}
/*
Aliasing.
*/
public function aliasProperties($vendor_context = null)
{
$aliased_properties =& Crush::$process->aliases['properties'];
// Bail early if nothing doing.
if (! array_intersect_key($aliased_properties, $this->properties)) {
return;
}
$stack = [];
$rule_updated = false;
$regex = Regex::$patt;
foreach ($this->store as $declaration) {
// Check declaration against vendor context.
if ($vendor_context && $declaration->vendor && $declaration->vendor !== $vendor_context) {
continue;
}
if ($declaration->skip) {
$stack[] = $declaration;
continue;
}
// Shim in aliased properties.
if (isset($aliased_properties[$declaration->property])) {
foreach ($aliased_properties[$declaration->property] as $prop_alias) {
// If an aliased version already exists do not create one.
if ($this->propertyCount($prop_alias)) {
continue;
}
// Get property alias vendor.
preg_match($regex->vendorPrefix, $prop_alias, $alias_vendor);
// Check against vendor context.
if ($vendor_context && $alias_vendor && $alias_vendor[1] !== $vendor_context) {
continue;
}
// Create the aliased declaration.
$copy = clone $declaration;
$copy->property = $prop_alias;
// Set the aliased declaration vendor property.
$copy->vendor = null;
if ($alias_vendor) {
$copy->vendor = $alias_vendor[1];
}
$stack[] = $copy;
$rule_updated = true;
}
}
// Un-aliased property or a property alias that has been manually set.
$stack[] = $declaration;
}
// Re-assign if any updates have been made.
if ($rule_updated) {
$this->reset($stack);
}
}
public function aliasFunctions($vendor_context = null)
{
$function_aliases =& Crush::$process->aliases['functions'];
$function_alias_groups =& Crush::$process->aliases['function_groups'];
// The new modified set of declarations.
$new_set = [];
$rule_updated = false;
// Shim in aliased functions.
foreach ($this->store as $declaration) {
// No functions, bail.
if (! $declaration->functions || $declaration->skip) {
$new_set[] = $declaration;
continue;
}
// Get list of functions used in declaration that are alias-able, bail if none.
$intersect = array_intersect_key($declaration->functions, $function_aliases);
if (! $intersect) {
$new_set[] = $declaration;
continue;
}
// Keep record of which groups have been applied.
$processed_groups = [];
foreach (array_keys($intersect) as $fn_name) {
// Store for all the duplicated declarations.
$prefixed_copies = [];
// Grouped function aliases.
if ($function_aliases[$fn_name][0] === '.') {
$group_id = $function_aliases[$fn_name];
// If this group has been applied we can skip over.
if (isset($processed_groups[$group_id])) {
continue;
}
// Mark group as applied.
$processed_groups[$group_id] = true;
$groups =& $function_alias_groups[$group_id];
foreach ($groups as $group_key => $replacements) {
// If the declaration is vendor specific only create aliases for the same vendor.
if (
($declaration->vendor && $group_key !== $declaration->vendor) ||
($vendor_context && $group_key !== $vendor_context)
) {
continue;
}
$copy = clone $declaration;
// Make swaps.
$copy->value = preg_replace(
$replacements['find'],
$replacements['replace'],
$copy->value
);
$prefixed_copies[] = $copy;
$rule_updated = true;
}
// Post fixes.
if (isset(PostAliasFix::$functions[$group_id])) {
call_user_func(PostAliasFix::$functions[$group_id], $prefixed_copies, $group_id);
}
}
// Single function aliases.
else {
foreach ($function_aliases[$fn_name] as $fn_alias) {
// If the declaration is vendor specific only create aliases for the same vendor.
if ($declaration->vendor) {
preg_match(Regex::$patt->vendorPrefix, $fn_alias, $m);
if (
$m[1] !== $declaration->vendor ||
($vendor_context && $m[1] !== $vendor_context)
) {
continue;
}
}
$copy = clone $declaration;
// Make swaps.
$copy->value = preg_replace(
Regex::make("~{{ LB }}$fn_name(?=\()~iS"),
$fn_alias,
$copy->value
);
$prefixed_copies[] = $copy;
$rule_updated = true;
}
// Post fixes.
if (isset(PostAliasFix::$functions[$fn_name])) {
call_user_func(PostAliasFix::$functions[$fn_name], $prefixed_copies, $fn_name);
}
}
$new_set = array_merge($new_set, $prefixed_copies);
}
$new_set[] = $declaration;
}
// Re-assign if any updates have been made.
if ($rule_updated) {
$this->reset($new_set);
}
}
public function aliasDeclarations($vendor_context = null)
{
$declaration_aliases =& Crush::$process->aliases['declarations'];
// First test for the existence of any aliased properties.
if (! ($intersect = array_intersect_key($declaration_aliases, $this->properties))) {
return;
}
$intersect = array_flip(array_keys($intersect));
$new_set = [];
$rule_updated = false;
foreach ($this->store as $declaration) {
// Check the current declaration property is actually aliased.
if (isset($intersect[$declaration->property]) && ! $declaration->skip) {
// Iterate on the current declaration property for value matches.
foreach ($declaration_aliases[$declaration->property] as $value_match => $replacements) {
// Create new alias declaration if the property and value match.
if ($declaration->value === $value_match) {
foreach ($replacements as $values) {
// Check the vendor against context.
if ($vendor_context && $vendor_context !== $values[2]) {
continue;
}
// If the replacement property is null use the original declaration property.
$new = new Declaration(
! empty($values[0]) ? $values[0] : $declaration->property,
$values[1]
);
$new->important = $declaration->important;
$new_set[] = $new;
$rule_updated = true;
}
}
}
}
$new_set[] = $declaration;
}
// Re-assign if any updates have been made.
if ($rule_updated) {
$this->reset($new_set);
}
}
public static function parse($str, $options = [])
{
$str = Util::stripCommentTokens($str);
$lines = preg_split('~\s*;\s*~', $str, null, PREG_SPLIT_NO_EMPTY);
$options += [
'keyed' => false,
'ignore_directives' => false,
'lowercase_keys' => false,
'context' => null,
'flatten' => false,
'apply_hooks' => false,
];
$pairs = [];
foreach ($lines as $line) {
if (! $options['ignore_directives'] && preg_match(Regex::$patt->ruleDirective, $line, $m)) {
if (! empty($m[1])) {
$property = 'mixin';
}
elseif (! empty($m[2])) {
$property = 'extends';
}
else {
$property = 'name';
}
$value = trim(substr($line, strlen($m[0])));
}
elseif (($colon_pos = strpos($line, ':')) !== false) {
$property = trim(substr($line, 0, $colon_pos));
$value = trim(substr($line, $colon_pos + 1));
if ($options['lowercase_keys']) {
$property = strtolower($property);
}
if ($options['apply_hooks']) {
Crush::$process->emit('declaration_preprocess', [
'property' => &$property,
'value' => &$value,
]);
}
}
else {
continue;
}
if ($property === '' || $value === '') {
continue;
}
if ($property === 'mixin' && $options['flatten']) {
$pairs = Mixin::merge($pairs, $value, [
'keyed' => $options['keyed'],
'context' => $options['context'],
]);
}
elseif ($options['keyed']) {
$pairs[$property] = $value;
}
else {
$pairs[] = [$property, $value];
}
}
return $pairs;
}
public function flatten()
{
if ($this->flattened) {
return;
}
$newSet = [];
foreach ($this->store as $declaration) {
if (is_array($declaration) && $declaration[0] === 'mixin') {
foreach (Mixin::merge([], $declaration[1], ['context' => $this->rule]) as $mixable) {
if ($mixable instanceof Declaration) {
$clone = clone $mixable;
$clone->index = count($newSet);
$newSet[] = $clone;
}
elseif ($mixable[0] === 'extends') {
$this->rule->addExtendSelectors($mixable[1]);
}
else {
$newSet[] = new Declaration($mixable[0], $mixable[1], count($newSet));
}
}
}
else {
$declaration->index = count($newSet);
$newSet[] = $declaration;
}
}
$this->reset($newSet);
$this->flattened = true;
}
public function process()
{
if ($this->processed) {
return;
}
foreach ($this->store as $index => $declaration) {
// Execute functions, store as data etc.
$declaration->process($this->rule);
// Drop declaration if value is now empty.
if (! $declaration->valid) {
unset($this->store[$index]);
}
}
// data is done with, reclaim memory.
unset($this->data);
$this->processed = true;
}
public function expandData($dataset, $property)
{
// Expand shorthand properties to make them available
// as data for this() and query().
static $expandables = [
'margin-top' => 'margin',
'margin-right' => 'margin',
'margin-bottom' => 'margin',
'margin-left' => 'margin',
'padding-top' => 'padding',
'padding-right' => 'padding',
'padding-bottom' => 'padding',
'padding-left' => 'padding',
'border-top-width' => 'border-width',
'border-right-width' => 'border-width',
'border-bottom-width' => 'border-width',
'border-left-width' => 'border-width',
'border-top-left-radius' => 'border-radius',
'border-top-right-radius' => 'border-radius',
'border-bottom-right-radius' => 'border-radius',
'border-bottom-left-radius' => 'border-radius',
'border-top-color' => 'border-color',
'border-right-color' => 'border-color',
'border-bottom-color' => 'border-color',
'border-left-color' => 'border-color',
];
$dataset =& $this->{$dataset};
$property_group = isset($expandables[$property]) ? $expandables[$property] : null;
// Bail if property non-expandable or already set.
if (! $property_group || isset($dataset[$property]) || ! isset($dataset[$property_group])) {
return;
}
// Get the expandable property value.
$value = $dataset[$property_group];
// Top-Right-Bottom-Left "trbl" expandable properties.
$trbl_fmt = null;
switch ($property_group) {
case 'margin':
$trbl_fmt = 'margin-%s';
break;
case 'padding':
$trbl_fmt = 'padding-%s';
break;
case 'border-width':
$trbl_fmt = 'border-%s-width';
break;
case 'border-radius':
$trbl_fmt = 'border-%s-radius';
break;
case 'border-color':
$trbl_fmt = 'border-%s-color';
break;
}
if ($trbl_fmt) {
$parts = explode(' ', $value);
$placeholders = [];
// 4 values.
if (isset($parts[3])) {
$placeholders = $parts;
}
// 3 values.
elseif (isset($parts[2])) {
$placeholders = [$parts[0], $parts[1], $parts[2], $parts[1]];
}
// 2 values.
elseif (isset($parts[1])) {
$placeholders = [$parts[0], $parts[1], $parts[0], $parts[1]];
}
// 1 value.
else {
$placeholders = array_pad($placeholders, 4, $parts[0]);
}
// Set positional variants.
if ($property_group === 'border-radius') {
$positions = [
'top-left',
'top-right',
'bottom-right',
'bottom-left',
];
}
else {
$positions = [
'top',
'right',
'bottom',
'left',
];
}
foreach ($positions as $index => $position) {
$prop = sprintf($trbl_fmt, $position);
$dataset += [$prop => $placeholders[$index]];
}
}
}
}

View file

@ -0,0 +1,36 @@
<?php
/**
*
* Event Emitter trait.
*
*/
namespace CssCrush;
trait EventEmitter {
private $eventEmitterStorage = [];
private $eventEmitterUid = 0;
public function on($event, callable $function)
{
if (! isset($this->eventEmitterStorage[$event])) {
$this->eventEmitterStorage[$event] = [];
}
$id = ++$this->eventEmitterUid;
$this->eventEmitterStorage[$event][$id] = $function;
return function () use ($event, $id) {
unset($this->eventEmitterStorage[$event][$id]);
};
}
public function emit($event, $data = null)
{
if (isset($this->eventEmitterStorage[$event])) {
foreach ($this->eventEmitterStorage[$event] as $function) {
$function($data);
}
}
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
*
* Extend argument objects.
*
*/
namespace CssCrush;
class ExtendArg
{
public $pointer;
public $name;
public $raw;
public $pseudo;
public function __construct($name)
{
$this->name =
$this->raw = $name;
if (! preg_match(Regex::$patt->rooted_ident, $this->name)) {
// Not a regular name: Some kind of selector so normalize it for later comparison.
$this->name =
$this->raw = Selector::makeReadable($this->name);
// If applying the pseudo on output store.
if (substr($this->name, -1) === '!') {
$this->name = rtrim($this->name, ' !');
if (preg_match('~\:\:?[\w-]+$~', $this->name, $m)) {
$this->pseudo = $m[0];
}
}
}
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
*
* Output file resources.
*
*/
namespace CssCrush;
class File
{
public $url;
public $path;
public $process;
public function __construct(Process $process)
{
$this->process = $process;
$io = $process->io;
Crush::runStat('paths');
if ($process->options->cache) {
$process->cacheData = $io->getCacheData();
if ($io->validateCache()) {
$this->url = $io->getOutputUrl();
$this->path = $io->getOutputDir() . '/' . $io->getOutputFilename();
$process->release();
return;
}
}
$string = $process->compile();
if ($io->write($string)) {
$this->url = $io->getOutputUrl();
$this->path = $io->getOutputDir() . '/' . $io->getOutputFilename();
}
}
public function __toString()
{
return $this->url;
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
*
* Fragments.
*
*/
namespace CssCrush;
class Fragment extends Template
{
public $name;
public function __construct($str, $options = [])
{
parent::__construct($str, $options);
$this->name = $options['name'];
}
public function __invoke(array $args = null, $str = null)
{
$str = parent::__invoke($args);
// Flatten all fragment calls within the template string.
while (preg_match(Regex::$patt->fragmentInvoke, $str, $m, PREG_OFFSET_CAPTURE)) {
$name = strtolower($m['name'][0]);
$fragment = isset(Crush::$process->fragments[$name]) ? Crush::$process->fragments[$name] : null;
$replacement = '';
$start = $m[0][1];
$length = strlen($m[0][0]);
// Skip over same named fragments to avoid infinite recursion.
if ($fragment && $name !== $this->name) {
$args = [];
if (isset($m['parens'][1])) {
$args = Functions::parseArgs($m['parens_content'][0]);
}
$replacement = $fragment($args);
}
$str = substr_replace($str, $replacement, $start, $length);
}
return $str;
}
}

View file

@ -0,0 +1,324 @@
<?php
/**
*
* Custom CSS functions
*
*/
namespace CssCrush;
class Functions
{
protected static $builtins = [
// These functions must come first in this order.
'query' => 'CssCrush\fn__query',
// These functions can be any order.
'math' => 'CssCrush\fn__math',
'hsla-adjust' => 'CssCrush\fn__hsla_adjust',
'hsl-adjust' => 'CssCrush\fn__hsl_adjust',
'h-adjust' => 'CssCrush\fn__h_adjust',
's-adjust' => 'CssCrush\fn__s_adjust',
'l-adjust' => 'CssCrush\fn__l_adjust',
'a-adjust' => 'CssCrush\fn__a_adjust',
];
public $register = [];
protected $pattern;
protected $patternOptions;
public function __construct($register = [])
{
$this->register = $register;
}
public function add($name, $callback)
{
$this->register[$name] = $callback;
}
public function remove($name)
{
unset($this->register[$name]);
}
public function setPattern($useAll = false)
{
if ($useAll) {
$this->register = self::$builtins + $this->register;
}
$this->pattern = Functions::makePattern(array_keys($this->register));
}
public function apply($str, \stdClass $context = null)
{
if (strpos($str, '(') === false) {
return $str;
}
if (! $this->pattern) {
$this->setPattern();
}
if (! preg_match($this->pattern, $str)) {
return $str;
}
$matches = Regex::matchAll($this->pattern, $str);
while ($match = array_pop($matches)) {
if (isset($match['function']) && $match['function'][1] !== -1) {
list($function, $offset) = $match['function'];
}
else {
list($function, $offset) = $match['simple_function'];
}
if (! preg_match(Regex::$patt->parens, $str, $parens, PREG_OFFSET_CAPTURE, $offset)) {
continue;
}
$openingParen = $parens[0][1];
$closingParen = $openingParen + strlen($parens[0][0]);
$rawArgs = trim($parens['parens_content'][0]);
// Update the context function identifier.
if ($context) {
$context->function = $function;
}
$returns = '';
if (isset($this->register[$function])) {
$fn = $this->register[$function];
if (is_array($fn) && !empty($fn['parse_args'])) {
$returns = $fn['callback'](self::parseArgs($rawArgs), $context);
}
else {
$returns = $fn($rawArgs, $context);
}
}
$str = substr_replace($str, $returns, $offset, $closingParen - $offset);
}
return $str;
}
#############################
# API and helpers.
public static function parseArgs($input, $allowSpaceDelim = false)
{
$options = [];
if ($allowSpaceDelim) {
$options['regex'] = Regex::$patt->argListSplit;
}
return Util::splitDelimList($input, $options);
}
/*
Quick argument list parsing for functions that take 1 or 2 arguments
with the proviso the first argument is an ident.
*/
public static function parseArgsSimple($input)
{
return preg_split(Regex::$patt->argListSplit, $input, 2);
}
public static function makePattern($functionNames)
{
$idents = [];
$nonIdents = [];
foreach ($functionNames as $functionName) {
if (preg_match(Regex::$patt->ident, $functionName[0])) {
$idents[] = preg_quote($functionName);
}
else {
$nonIdents[] = preg_quote($functionName);
}
}
if ($idents) {
$idents = '{{ LB }}-?(?<function>' . implode('|', $idents) . ')';
}
if ($nonIdents) {
$nonIdents = '(?<simple_function>' . implode('|', $nonIdents) . ')';
}
if ($idents && $nonIdents) {
$patt = "(?:$idents|$nonIdents)";
}
elseif ($idents) {
$patt = $idents;
}
elseif ($nonIdents) {
$patt = $nonIdents;
}
return Regex::make("~$patt\(~iS");
}
}
#############################
# Stock CSS functions.
function fn__math($input) {
list($expression, $unit) = array_pad(Functions::parseArgs($input), 2, '');
// Swap in math constants.
$expression = preg_replace(
['~\bpi\b~i'],
[M_PI],
$expression);
// If no unit is specified scan expression.
if (! $unit) {
$numPatt = Regex::$classes->number;
if (preg_match("~\b{$numPatt}(?<unit>[A-Za-z]{2,4}\b|%)~", $expression, $m)) {
$unit = $m['unit'];
}
}
// Filter expression so it's just characters necessary for simple math.
$expression = preg_replace("~[^.0-9/*()+-]~S", '', $expression);
$evalExpression = "return $expression;";
$result = false;
if (class_exists('\\ParseError')) {
try {
$result = @eval($evalExpression);
}
catch (\Error $e) {}
}
else {
$result = @eval($evalExpression);
}
return ($result === false ? 0 : round($result, 5)) . $unit;
}
function fn__hsla_adjust($input) {
list($color, $h, $s, $l, $a) = array_pad(Functions::parseArgs($input, true), 5, 0);
return Color::test($color) ? Color::colorAdjust($color, [$h, $s, $l, $a]) : '';
}
function fn__hsl_adjust($input) {
list($color, $h, $s, $l) = array_pad(Functions::parseArgs($input, true), 4, 0);
return Color::test($color) ? Color::colorAdjust($color, [$h, $s, $l, 0]) : '';
}
function fn__h_adjust($input) {
list($color, $h) = array_pad(Functions::parseArgs($input, true), 2, 0);
return Color::test($color) ? Color::colorAdjust($color, [$h, 0, 0, 0]) : '';
}
function fn__s_adjust($input) {
list($color, $s) = array_pad(Functions::parseArgs($input, true), 2, 0);
return Color::test($color) ? Color::colorAdjust($color, [0, $s, 0, 0]) : '';
}
function fn__l_adjust($input) {
list($color, $l) = array_pad(Functions::parseArgs($input, true), 2, 0);
return Color::test($color) ? Color::colorAdjust($color, [0, 0, $l, 0]) : '';
}
function fn__a_adjust($input) {
list($color, $a) = array_pad(Functions::parseArgs($input, true), 2, 0);
return Color::test($color) ? Color::colorAdjust($color, [0, 0, 0, $a]) : '';
}
function fn__this($input, $context) {
$args = Functions::parseArgsSimple($input);
$property = $args[0];
// Function relies on a context rule, bail if none.
if (! isset($context->rule)) {
return '';
}
$rule = $context->rule;
$rule->declarations->expandData('data', $property);
if (isset($rule->declarations->data[$property])) {
return $rule->declarations->data[$property];
}
// Fallback value.
elseif (isset($args[1])) {
return $args[1];
}
return '';
}
function fn__query($input, $context) {
$args = Functions::parseArgs($input);
// Context property is required.
if (! count($args) || ! isset($context->property)) {
return '';
}
list($target, $property, $fallback) = $args + [null, $context->property, null];
if (strtolower($property) === 'default') {
$property = $context->property;
}
if (! preg_match(Regex::$patt->rooted_ident, $target)) {
$target = Selector::makeReadable($target);
}
$targetRule = null;
$references =& Crush::$process->references;
switch (strtolower($target)) {
case 'parent':
$targetRule = $context->rule->parent;
break;
case 'previous':
$targetRule = $context->rule->previous;
break;
case 'next':
$targetRule = $context->rule->next;
break;
case 'top':
$targetRule = $context->rule->parent;
while ($targetRule && $targetRule->parent && $targetRule = $targetRule->parent);
break;
default:
if (isset($references[$target])) {
$targetRule = $references[$target];
}
break;
}
$result = '';
if ($targetRule) {
$targetRule->declarations->process();
$targetRule->declarations->expandData('queryData', $property);
if (isset($targetRule->declarations->queryData[$property])) {
$result = $targetRule->declarations->queryData[$property];
}
}
if ($result === '' && isset($fallback)) {
$result = $fallback;
}
return $result;
}

View file

@ -0,0 +1,228 @@
<?php
/**
*
* Interface for writing files, retrieving files and checking caches
*
*/
namespace CssCrush;
class IO
{
protected $process;
public function __construct(Process $process)
{
$this->process = $process;
}
public function init()
{
$this->process->cacheFile = "{$this->process->output->dir}/.csscrush";
}
public function getOutputDir()
{
$outputDir = $this->process->options->output_dir;
return $outputDir ? $outputDir : $this->process->input->dir;
}
public function getOutputFilename()
{
$options = $this->process->options;
$inputBasename = basename($this->process->input->filename, '.css');
$outputBasename = $inputBasename;
if (! empty($options->output_file)) {
$outputBasename = basename($options->output_file, '.css');
}
if ($this->process->input->dir === $this->getOutputDir() && $inputBasename === $outputBasename) {
$outputBasename .= '.crush';
}
return "$outputBasename.css";
}
public function getOutputUrl()
{
$process = $this->process;
$options = $process->options;
$filename = $process->output->filename;
$url = $process->output->dirUrl . '/' . $filename;
// Make URL relative if the input path was relative.
$input_path = new Url($process->input->raw);
if ($input_path->isRelative) {
$url = Util::getLinkBetweenPaths(Crush::$config->scriptDir, $process->output->dir) . $filename;
}
// Optional query-string timestamp.
if ($options->versioning !== false) {
$url .= '?';
if (isset($process->cacheData[$filename]['datem_sum'])) {
$url .= $process->cacheData[$filename]['datem_sum'];
}
else {
$url .= time();
}
}
return $url;
}
public function validateCache()
{
$process = $this->process;
$options = $process->options;
$input = $process->input;
$dir = $this->getOutputDir();
$filename = $this->getOutputFilename();
$path = "$dir/$filename";
if (! file_exists($path)) {
debug("File '$path' not cached.");
return false;
}
if (! isset($process->cacheData[$filename])) {
debug('Cached file exists but is not registered.');
return false;
}
$data =& $process->cacheData[$filename];
// Make stack of file mtimes starting with the input file.
$file_sums = [$input->mtime];
foreach ($data['imports'] as $import_file) {
// Check if this is docroot relative or input dir relative.
$root = strpos($import_file, '/') === 0 ? $process->docRoot : $input->dir;
$import_filepath = realpath($root) . "/$import_file";
if (file_exists($import_filepath)) {
$file_sums[] = filemtime($import_filepath);
}
else {
// File has been moved, remove old file and skip to compile.
debug('Recompiling - an import file has been moved.');
return false;
}
}
$files_changed = $data['datem_sum'] != array_sum($file_sums);
if ($files_changed) {
debug('Files have been modified. Recompiling.');
}
// Compare runtime options and cached options for differences.
// Cast because the cached options may be a \stdClass if an IO adapter has been used.
$options_changed = false;
$cached_options = (array) $data['options'];
$active_options = $options->get();
foreach ($cached_options as $key => &$value) {
if (isset($active_options[$key]) && $active_options[$key] !== $value) {
debug('Options have been changed. Recompiling.');
$options_changed = true;
break;
}
}
if (! $options_changed && ! $files_changed) {
debug("Files and options have not been modified, returning cached file.");
return true;
}
else {
$data['datem_sum'] = array_sum($file_sums);
return false;
}
}
public function getCacheData()
{
$process = $this->process;
if (file_exists($process->cacheFile) && $process->cacheData) {
// Already loaded and config file exists in the current directory
return;
}
$cache_data_exists = file_exists($process->cacheFile);
$cache_data_file_is_writable = $cache_data_exists ? is_writable($process->cacheFile) : false;
$cache_data = [];
if (
$cache_data_exists &&
$cache_data_file_is_writable &&
$cache_data = json_decode(file_get_contents($process->cacheFile), true)
) {
// Successfully loaded config file.
debug('Cache data loaded.');
}
else {
// Config file may exist but not be writable (may not be visible in some ftp situations?)
if ($cache_data_exists) {
if (! @unlink($process->cacheFile)) {
notice('Could not delete cache data file.');
}
}
else {
debug('Creating cache data file.');
}
Util::filePutContents($process->cacheFile, json_encode([]), __METHOD__);
}
return $cache_data;
}
public function saveCacheData()
{
$process = $this->process;
debug('Saving config.');
Util::filePutContents($process->cacheFile, json_encode($process->cacheData, JSON_PRETTY_PRINT), __METHOD__);
}
public function write(StringObject $string)
{
$process = $this->process;
$dir = $this->getOutputDir();
$filename = $this->getOutputFilename();
$sourcemapFilename = "$filename.map";
if ($process->sourceMap) {
$string->append($process->newline . "/*# sourceMappingURL=$sourcemapFilename */");
}
if (Util::filePutContents("$dir/$filename", $string, __METHOD__)) {
if ($process->sourceMap) {
Util::filePutContents("$dir/$sourcemapFilename",
json_encode($process->sourceMap, JSON_PRETTY_PRINT), __METHOD__);
}
if ($process->options->stat_dump) {
$statFile = is_string($process->options->stat_dump) ?
$process->options->stat_dump : "$dir/$filename.json";
$GLOBALS['CSSCRUSH_STAT_FILE'] = $statFile;
Util::filePutContents($statFile, json_encode(csscrush_stat(), JSON_PRETTY_PRINT), __METHOD__);
}
return true;
}
return false;
}
}

View file

@ -0,0 +1,48 @@
<?php
/**
*
* IO class for command line file watching.
*
*/
namespace CssCrush\IO;
use CssCrush\Crush;
use CssCrush\IO;
class Watch extends IO
{
public static $cacheData = [];
public function getOutputFileName()
{
$process = $this->process;
$options = $process->options;
$input_basename = $output_basename = basename($process->input->filename, '.css');
if (! empty($options->output_file)) {
$output_basename = basename($options->output_file, '.css');
}
$suffix = '.crush';
if (($process->input->dir !== $process->output->dir) || ($input_basename !== $output_basename)) {
$suffix = '';
}
return "$output_basename$suffix.css";
}
public function getCacheData()
{
// Clear results from earlier processes.
clearstatcache();
$this->process->cacheData = [];
return self::$cacheData;
}
public function saveCacheData()
{
self::$cacheData = $this->process->cacheData;
}
}

View file

@ -0,0 +1,378 @@
<?php
/**
*
* Recursive file importing
*
*/
namespace CssCrush;
class Importer
{
protected $process;
public function __construct(Process $process)
{
$this->process = $process;
}
public function collate()
{
$process = $this->process;
$options = $process->options;
$regex = Regex::$patt;
$input = $process->input;
$str = '';
// Keep track of all import file info for cache data.
$mtimes = [];
$filenames = [];
// Resolve main input; a string of css or a file.
if (isset($input->string)) {
$str .= $input->string;
$process->sources[] = 'Inline CSS';
}
else {
$str .= file_get_contents($input->path);
$process->sources[] = $input->path;
}
// If there's a parsing error go no further.
if (! $this->prepareImport($str)) {
return $str;
}
// This may be set non-zero during the script if an absolute @import URL is encountered.
$search_offset = 0;
// Recurses until the nesting heirarchy is flattened and all import files are inlined.
while (preg_match($regex->import, $str, $match, PREG_OFFSET_CAPTURE, $search_offset)) {
$match_len = strlen($match[0][0]);
$match_start = $match[0][1];
$import = new \stdClass();
$import->url = $process->tokens->get($match[1][0]);
$import->media = trim($match[2][0]);
// Protocoled import urls are not processed. Stash for prepending to output.
if ($import->url->protocol) {
$str = substr_replace($str, '', $match_start, $match_len);
$process->absoluteImports[] = $import;
continue;
}
// Resolve import path information.
$import->path = null;
if ($import->url->isRooted) {
$import->path = realpath($process->docRoot . $import->url->value);
}
else {
$url =& $import->url;
$candidates = ["$input->dir/$url->value"];
// If `import_path` option is set implicit relative urls
// are additionally searched under specified import path(s).
if (is_array($options->import_path) && $url->isRelativeImplicit()) {
foreach ($options->import_path as $importPath) {
$candidates[] = "$importPath/$url->originalValue";
}
}
foreach ($candidates as $candidate) {
if (file_exists($candidate)) {
$import->path = realpath($candidate);
break;
}
}
}
// If unsuccessful getting import contents continue with the import line removed.
$import->content = $import->path ? @file_get_contents($import->path) : false;
if ($import->content === false) {
$errDesc = 'was not found';
if ($import->path && ! is_readable($import->path)) {
$errDesc = 'is not readable';
}
if (! empty($process->sources)) {
$errDesc .= " (from within {$process->sources[0]})";
}
notice("@import '{$import->url->value}' $errDesc");
$str = substr_replace($str, '', $match_start, $match_len);
continue;
}
$import->dir = dirname($import->path);
$import->relativeDir = Util::getLinkBetweenPaths($input->dir, $import->dir);
// Import file exists so register it.
$process->sources[] = $import->path;
$mtimes[] = filemtime($import->path);
$filenames[] = $import->relativeDir . basename($import->path);
// If the import content doesn't pass syntax validation skip to next import.
if (! $this->prepareImport($import->content)) {
$str = substr_replace($str, '', $match_start, $match_len);
continue;
}
// Alter all embedded import URLs to be relative to the host-file.
foreach (Regex::matchAll($regex->import, $import->content) as $m) {
$nested_url = $process->tokens->get($m[1][0]);
// Resolve rooted paths.
if ($nested_url->isRooted) {
$link = Util::getLinkBetweenPaths(dirname($nested_url->getAbsolutePath()), $import->dir);
$nested_url->update($link . basename($nested_url->value));
}
elseif (strlen($import->relativeDir)) {
$nested_url->prepend("$import->relativeDir/");
}
}
// Optionally rewrite relative url and custom function data-uri references.
if ($options->rewrite_import_urls) {
$this->rewriteImportedUrls($import);
}
if ($import->media) {
$import->content = "@media $import->media {{$import->content}}";
}
$str = substr_replace($str, $import->content, $match_start, $match_len);
}
// Save only if caching is on and the hostfile object is associated with a real file.
if ($input->path && $options->cache) {
$process->cacheData[$process->output->filename] = [
'imports' => $filenames,
'datem_sum' => array_sum($mtimes) + $input->mtime,
'options' => $options->get(),
];
$process->io->saveCacheData();
}
return $str;
}
protected function rewriteImportedUrls($import)
{
$link = Util::getLinkBetweenPaths($this->process->input->dir, dirname($import->path));
if (empty($link)) {
return;
}
// Match all urls that are not imports.
preg_match_all(Regex::make('~(?<!@import ){{u_token}}~iS'), $import->content, $matches);
foreach ($matches[0] as $token) {
$url = $this->process->tokens->get($token);
if ($url->isRelative) {
$url->prepend($link);
}
}
}
protected function prepareImport(&$str)
{
$regex = Regex::$patt;
$process = $this->process;
$tokens = $process->tokens;
// Convert all EOL to unix style.
$str = preg_replace('~\r\n?~', "\n", $str);
// Trimming to reduce regex backtracking.
$str = rtrim($this->captureCommentAndString(rtrim($str)));
if (! $this->syntaxCheck($str)) {
$str = '';
return false;
}
// Normalize double-colon pseudo elements for backwards compatability.
$str = preg_replace('~::(after|before|first-(?:letter|line))~iS', ':$1', $str);
// Store @charset if set.
if (preg_match($regex->charset, $str, $m)) {
$replace = '';
if (! $process->charset) {
// Keep track of newlines for line numbering.
$replace = str_repeat("\n", substr_count($m[0], "\n"));
$process->charset = trim($tokens->get($m[1]), '"\'');
}
$str = preg_replace($regex->charset, $replace, $str);
}
$str = $tokens->captureUrls($str, true);
$this->addMarkers($str);
$str = Util::normalizeWhiteSpace($str);
return true;
}
protected function syntaxCheck(&$str)
{
// Catch obvious typing errors.
$errors = false;
$current_file = 'file://' . end($this->process->sources);
$balanced_parens = substr_count($str, "(") === substr_count($str, ")");
$balanced_curlies = substr_count($str, "{") === substr_count($str, "}");
$validate_pairings = function ($str, $pairing) use ($current_file)
{
if ($pairing === '{}') {
$opener_patt = '~\{~';
$balancer_patt = Regex::make('~^{{block}}~');
}
else {
$opener_patt = '~\(~';
$balancer_patt = Regex::make('~^{{parens}}~');
}
// Find unbalanced opening brackets.
preg_match_all($opener_patt, $str, $matches, PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $m) {
$offset = $m[1];
if (! preg_match($balancer_patt, substr($str, $offset), $m)) {
$substr = substr($str, 0, $offset);
$line = substr_count($substr, "\n") + 1;
$column = strlen($substr) - strrpos($substr, "\n");
return "Unbalanced '{$pairing[0]}' in $current_file, Line $line, Column $column.";
}
}
// Reverse the string (and brackets) to find stray closing brackets.
$str = strtr(strrev($str), $pairing, strrev($pairing));
preg_match_all($opener_patt, $str, $matches, PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $m) {
$offset = $m[1];
$substr = substr($str, $offset);
if (! preg_match($balancer_patt, $substr, $m)) {
$line = substr_count($substr, "\n") + 1;
$column = strpos($substr, "\n");
return "Stray '{$pairing[1]}' in $current_file, Line $line, Column $column.";
}
}
return false;
};
if (! $balanced_curlies) {
$errors = true;
warning($validate_pairings($str, '{}') ?: "Unbalanced '{' in $current_file.");
}
if (! $balanced_parens) {
$errors = true;
warning($validate_pairings($str, '()') ?: "Unbalanced '(' in $current_file.");
}
return $errors ? false : true;
}
protected function addMarkers(&$str)
{
$process = $this->process;
$currentFileIndex = count($process->sources) - 1;
static $patt;
if (! $patt) {
$patt = Regex::make('~
(?:^|(?<=[;{}]))
(?<before>
(?: \s | {{c_token}} )*
)
(?<selector>
(?:
# Some @-rules are treated like standard rule blocks.
@(?: (?i)page|abstract|font-face(?-i) ) {{RB}} [^{]*
|
[^@;{}]+
)
)
\{
~xS');
}
$count = preg_match_all($patt, $str, $matches, PREG_OFFSET_CAPTURE);
while ($count--) {
$selectorOffset = $matches['selector'][$count][1];
$line = 0;
$before = substr($str, 0, $selectorOffset);
if ($selectorOffset) {
$line = substr_count($before, "\n");
}
$pointData = [$currentFileIndex, $line];
// Source maps require column index too.
if ($process->generateMap) {
$pointData[] = strlen($before) - (strrpos($before, "\n") ?: 0);
}
// Splice in marker token (packing point_data into string is more memory efficient).
$str = substr_replace(
$str,
$process->tokens->add(implode(',', $pointData), 't'),
$selectorOffset,
0);
}
}
protected function captureCommentAndString($str)
{
$process = $this->process;
$callback = function ($m) use ($process) {
$fullMatch = $m[0];
if (strpos($fullMatch, '/*') === 0) {
// Bail without storing comment if output is minified or a private comment.
if ($process->minifyOutput || strpos($fullMatch, '/*$') === 0) {
$label = '';
}
else {
// Fix broken comments as they will break any subsquent
// imported files that are inlined.
if (! preg_match('~\*/$~', $fullMatch)) {
$fullMatch .= '*/';
}
$label = $process->tokens->add($fullMatch, 'c');
}
}
else {
// Fix broken strings as they will break any subsquent
// imported files that are inlined.
if ($fullMatch[0] !== $fullMatch[strlen($fullMatch)-1]) {
$fullMatch .= $fullMatch[0];
}
// Backticked literals may have been used for custom property values.
if ($fullMatch[0] === '`') {
$fullMatch = preg_replace('~\x5c`~', '`', trim($fullMatch, '`'));
}
$label = $process->tokens->add($fullMatch, 's');
}
return $process->generateMap ? Tokens::pad($label, $fullMatch) : $label;
};
return preg_replace_callback(Regex::$patt->commentAndString, $callback, $str);
}
}

View file

@ -0,0 +1,70 @@
<?php
/**
*
* Base iterator for Declaration and Selector lists.
*
*/
namespace CssCrush;
class Iterator implements \IteratorAggregate, \ArrayAccess, \Countable
{
public $store;
public function __construct($items = [])
{
$this->store = $items;
}
/*
IteratorAggregate implementation.
*/
public function getIterator()
{
return new \ArrayIterator($this->store);
}
/*
ArrayAccess implementation.
*/
public function offsetExists($index)
{
return array_key_exists($index, $this->store);
}
public function offsetGet($index)
{
return isset($this->store[$index]) ? $this->store[$index] : null;
}
public function offsetSet($index, $value)
{
$this->store[$index] = $value;
}
public function offsetUnset($index)
{
unset($this->store[$index]);
}
public function getContents()
{
return $this->store;
}
/*
Countable implementation.
*/
public function count()
{
return count($this->store);
}
/*
Collection interface.
*/
public function filter($filterer, $op = '===')
{
$collection = new Collection($this->store);
return $collection->filter($filterer, $op);
}
}

View file

@ -0,0 +1,149 @@
<?php
/**
*
* PSR-3 compatible logger.
*
*/
namespace CssCrush;
class Logger
{
/**
* System is unusable.
*
* @param string $message
* @param array $context
* @return null
*/
public function emergency($message, array $context = [])
{
$this->error($message, $context);
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param array $context
* @return null
*/
public function alert($message, array $context = [])
{
$this->error($message, $context);
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
* @return null
*/
public function critical($message, array $context = [])
{
$this->error($message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param array $context
* @return null
*/
public function error($message, array $context = [])
{
trigger_error($message, E_USER_ERROR);
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param array $context
* @return null
*/
public function warning($message, array $context = [])
{
trigger_error($message, E_USER_WARNING);
}
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
* @return null
*/
public function notice($message, array $context = [])
{
trigger_error($message, E_USER_NOTICE);
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
* @return null
*/
public function info($message, array $context = [])
{
$this->debug($message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
* @return null
*/
public function debug($message, array $context = [])
{
if (! empty($context['label'])) {
$label = PHP_EOL . "$label" . PHP_EOL . str_repeat('=', strlen($label)) . PHP_EOL;
}
else {
$label = '';
}
if (is_string($message)) {
Crush::$process->debugLog[] = "$label$message";
}
else {
ob_start();
! empty($context['var_dump']) ? var_dump($message) : print_r($message);
Crush::$process->debugLog[] = $label . ob_get_clean();
}
}
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
* @return null
*/
public function log($level, $message, array $context = [])
{
$log_levels = array_flip(get_class_methods(__CLASS__));
unset($log_levels['log']);
if (isset($log_levels[$level])) {
return $this->$level($message, $context);
}
}
}

View file

@ -0,0 +1,106 @@
<?php
/**
*
* Mixin objects.
*
*/
namespace CssCrush;
class Mixin
{
public $template;
public function __construct($block)
{
$this->template = new Template($block);
}
public static function call($message, $context = null)
{
$process = Crush::$process;
$mixable = null;
$message = trim($message);
// Test for mixin or abstract rule. e.g:
// named-mixin( 50px, rgba(0,0,0,0), left 100% )
// abstract-rule
if (preg_match(Regex::make('~^(?<name>{{ident}}) {{parens}}?~xS'), $message, $message_match)) {
$name = $message_match['name'];
if (isset($process->mixins[$name])) {
$mixable = $process->mixins[$name];
}
elseif (isset($process->references[$name])) {
$mixable = $process->references[$name];
}
}
// If no mixin or abstract rule matched, look for matching selector
if (! $mixable) {
$selector_test = Selector::makeReadable($message);
if (isset($process->references[$selector_test])) {
$mixable = $process->references[$selector_test];
}
}
// Avoid infinite recursion.
if (! $mixable || $mixable === $context) {
return false;
}
elseif ($mixable instanceof Mixin) {
$args = [];
$raw_args = isset($message_match['parens_content']) ? trim($message_match['parens_content']) : null;
if ($raw_args) {
$args = Util::splitDelimList($raw_args);
}
return DeclarationList::parse($mixable->template->__invoke($args), [
'flatten' => true,
'context' => $mixable,
]);
}
elseif ($mixable instanceof Rule) {
return $mixable->declarations->store;
}
}
public static function merge(array $input, $message_list, $options = [])
{
$context = isset($options['context']) ? $options['context'] : null;
$mixables = [];
foreach (Util::splitDelimList($message_list) as $message) {
if ($result = self::call($message, $context)) {
$mixables = array_merge($mixables, $result);
}
}
while ($mixable = array_shift($mixables)) {
if ($mixable instanceof Declaration) {
$input[] = $mixable;
}
else {
list($property, $value) = $mixable;
if ($property === 'mixin') {
$input = Mixin::merge($input, $value, $options);
}
elseif (! empty($options['keyed'])) {
$input[$property] = $value;
}
else {
$input[] = [$property, $value];
}
}
}
return $input;
}
}

View file

@ -0,0 +1,176 @@
<?php
/**
*
* Options handling.
*
*/
namespace CssCrush;
class Options
{
protected $computedOptions = [];
protected $inputOptions = [];
protected static $standardOptions = [
'minify' => true,
'formatter' => null,
'versioning' => true,
'boilerplate' => true,
'vars' => [],
'cache' => true,
'context' => null,
'import_path' => null,
'output_file' => null,
'output_dir' => null,
'asset_dir' => null,
'doc_root' => null,
'vendor_target' => 'all',
'rewrite_import_urls' => true,
'plugins' => null,
'settings' => [],
'stat_dump' => false,
'source_map' => false,
'newlines' => 'use-platform',
];
public function __construct(array $options = [], Options $defaults = null)
{
$options = array_change_key_case($options, CASE_LOWER);
if ($defaults) {
$options += $defaults->get();
}
if (! empty($options['enable'])) {
if (empty($options['plugins'])) {
$options['plugins'] = $options['enable'];
}
unset($options['enable']);
}
foreach ($options + self::$standardOptions as $name => $value) {
$this->__set($name, $value);
}
}
public function __set($name, $value)
{
$this->inputOptions[$name] = $value;
switch ($name) {
case 'formatter':
if (is_string($value) && isset(Crush::$config->formatters[$value])) {
$value = Crush::$config->formatters[$value];
}
if (! is_callable($value)) {
$value = null;
}
break;
// Path options.
case 'boilerplate':
if (is_string($value)) {
$value = Util::resolveUserPath($value);
}
break;
case 'stat_dump':
if (is_string($value)) {
$value = Util::resolveUserPath($value, function ($path) {
touch($path);
return $path;
});
}
break;
case 'output_dir':
case 'asset_dir':
if (is_string($value)) {
$value = Util::resolveUserPath($value, function ($path) use ($name) {
if (! @mkdir($path, 0755, true)) {
warning("Could not create directory $path (setting `$name` option).");
}
else {
debug("Created directory $path (setting `$name` option).");
}
return $path;
});
}
break;
// Path options that only accept system paths.
case 'context':
case 'doc_root':
if (is_string($value)) {
$value = Util::normalizePath(realpath($value));
}
break;
case 'import_path':
if ($value) {
if (is_string($value)) {
$value = preg_split('~\s*,\s*~', trim($value));
}
$value = array_filter(array_map(function ($path) {
return Util::normalizePath(realpath($path));
}, $value));
}
break;
// Options used internally as arrays.
case 'plugins':
$value = (array) $value;
break;
}
$this->computedOptions[$name] = $value;
}
public function __get($name)
{
switch ($name) {
case 'newlines':
switch ($this->inputOptions[$name]) {
case 'windows':
case 'win':
return "\r\n";
case 'unix':
return "\n";
case 'use-platform':
default:
return PHP_EOL;
}
break;
case 'minify':
if (isset($this->computedOptions['formatter'])) {
return false;
}
break;
case 'formatter':
if (empty($this->inputOptions['minify'])) {
return isset($this->computedOptions['formatter']) ?
$this->computedOptions['formatter'] : 'CssCrush\fmtr_block';
}
}
return isset($this->computedOptions[$name]) ? $this->computedOptions[$name] : null;
}
public function __isset($name)
{
return isset($this->inputOptions[$name]);
}
public function get($computed = false)
{
return $computed ? $this->computedOptions : self::filter($this->inputOptions);
}
public static function filter(array $optionsArray = null)
{
return $optionsArray ? array_intersect_key($optionsArray, self::$standardOptions) : self::$standardOptions;
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
*
* Fixes for aliasing to legacy syntaxes.
*
*/
namespace CssCrush;
class PostAliasFix
{
public static $functions = [];
public static function add($alias_type, $key, $callback)
{
if ($alias_type === 'function') {
self::$functions[$key] = $callback;
}
}
public static function remove($alias_type, $key)
{
if ($alias_type === 'function') {
unset(self::$functions[$key]);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,112 @@
<?php
/**
*
* Regex management.
*
*/
namespace CssCrush;
class Regex
{
// Patterns.
public static $patt;
// Character classes.
public static $classes;
public static function init()
{
self::$patt = $patt = new \stdClass();
self::$classes = $classes = new \stdClass();
// CSS type classes.
$classes->ident = '[a-zA-Z0-9_-]+';
$classes->number = '[+-]?\d*\.?\d+';
$classes->percentage = $classes->number . '%';
$classes->length_unit = '(?i)(?:e[mx]|c[hm]|rem|v[hwm]|in|p[tcx])(?-i)';
$classes->length = $classes->number . $classes->length_unit;
$classes->color_hex = '#[[:xdigit:]]{3}(?:[[:xdigit:]]{3})?';
// Tokens.
$classes->token_id = '[0-9a-z]+';
$classes->c_token = '\?c' . $classes->token_id . '\?'; // Comments.
$classes->s_token = '\?s' . $classes->token_id . '\?'; // Strings.
$classes->r_token = '\?r' . $classes->token_id . '\?'; // Rules.
$classes->u_token = '\?u' . $classes->token_id . '\?'; // URLs.
$classes->t_token = '\?t' . $classes->token_id . '\?'; // Traces.
$classes->a_token = '\?a(' . $classes->token_id . ')\?'; // Args.
// Boundries.
$classes->LB = '(?<![\w-])'; // Left ident boundry.
$classes->RB = '(?![\w-])'; // Right ident boundry.
// Recursive block matching.
$classes->block = '(?<block>\{\s*(?<block_content>(?:(?>[^{}]+)|(?&block))*)\})';
$classes->parens = '(?<parens>\(\s*(?<parens_content>(?:(?>[^()]+)|(?&parens))*)\))';
// Misc.
$classes->vendor = '-[a-zA-Z]+-';
$classes->hex = '[[:xdigit:]]';
$classes->newline = '(\r\n?|\n)';
// Create standalone class patterns, add classes as class swaps.
foreach ($classes as $name => $class) {
$patt->{$name} = '~' . $class . '~S';
}
// Rooted classes.
$patt->rooted_ident = '~^' . $classes->ident . '$~';
$patt->rooted_number = '~^' . $classes->number . '$~';
// @-rules.
$patt->import = Regex::make('~@import \s+ ({{u_token}}) \s? ([^;]*);~ixS');
$patt->charset = Regex::make('~@charset \s+ ({{s_token}}) \s*;~ixS');
$patt->mixin = Regex::make('~@mixin \s+ (?<name>{{ident}}) \s* {{block}}~ixS');
$patt->fragmentCapture = Regex::make('~@fragment \s+ (?<name>{{ident}}) \s* {{block}}~ixS');
$patt->fragmentInvoke = Regex::make('~@fragment \s+ (?<name>{{ident}}) {{parens}}? \s* ;~ixS');
$patt->abstract = Regex::make('~^@abstract \s+ (?<name>{{ident}})~ixS');
// Functions.
$patt->functionTest = Regex::make('~{{ LB }} (?<func_name>{{ ident }}) \(~xS');
$patt->thisFunction = Functions::makePattern(['this']);
// Strings and comments.
$patt->string = '~(\'|")(?:\\\\\1|[^\1])*?\1~xS';
$patt->commentAndString = '~
# Quoted string (to EOF if unmatched).
(\'|"|`)(?:\\\\\1|[^\1])*?(?:\1|$)
|
# Block comment (to EOF if unmatched).
/\*(?:[^*]*\*+(?:[^/*][^*]*\*+)*/|.*)
~xsS';
// Misc.
$patt->vendorPrefix = '~^-([a-z]+)-([a-z-]+)~iS';
$patt->ruleDirective = '~^(?:(@include)|(@extends?)|(@name))[\s]+~iS';
$patt->argListSplit = '~\s*[,\s]\s*~S';
$patt->cruftyHex = Regex::make('~\#({{hex}})\1({{hex}})\2({{hex}})\3~S');
$patt->token = Regex::make('~^ \? (?<type>[a-zA-Z]) {{token_id}} \? $~xS');
}
public static function make($pattern)
{
static $cache = [];
if (isset($cache[$pattern])) {
return $cache[$pattern];
}
return $cache[$pattern] = preg_replace_callback('~\{\{ *(?<name>\w+) *\}\}~S', function ($m) {
return Regex::$classes->{ $m['name'] };
}, $pattern);
}
public static function matchAll($patt, $subject, $offset = 0)
{
$count = preg_match_all($patt, $subject, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER, $offset);
return $count ? $matches : [];
}
}
Regex::init();

View file

@ -0,0 +1,145 @@
<?php
/**
*
* CSS rule API.
*
*/
namespace CssCrush;
class Rule
{
public $vendorContext;
public $label;
public $marker;
public $name;
public $isAbstract;
public $resolvedExtendables;
public $parent;
public $previous;
public $next;
public $selectors;
public $declarations;
// Arugments passed via @extend.
public $extendArgs = [];
public $extendSelectors = [];
public function __construct($selectorString, $declarationsString, $traceToken = null)
{
$process = Crush::$process;
$this->label = $process->tokens->createLabel('r');
$this->marker = $process->generateMap ? $traceToken : null;
$this->selectors = new SelectorList($selectorString, $this);
$this->declarations = new DeclarationList($declarationsString, $this);
}
public function __toString()
{
$process = Crush::$process;
// Merge the extend selectors.
$this->selectors->store += $this->extendSelectors;
// Dereference and return empty string if there are no selectors or declarations.
if (empty($this->selectors->store) || empty($this->declarations->store)) {
$process->tokens->pop($this->label);
return '';
}
$stub = $this->marker;
if ($process->minifyOutput) {
return "$stub{$this->selectors->join()}{{$this->declarations->join()}}";
}
else {
return $stub . call_user_func($process->ruleFormatter, $this);
}
}
public function __clone()
{
$this->selectors = clone $this->selectors;
$this->declarations = clone $this->declarations;
}
#############################
# Rule inheritance.
public function addExtendSelectors($rawValue)
{
foreach (Util::splitDelimList($rawValue) as $arg) {
$extendArg = new ExtendArg($arg);
$this->extendArgs[$extendArg->raw] = $extendArg;
}
}
public function resolveExtendables()
{
if (! $this->extendArgs) {
return false;
}
elseif (! $this->resolvedExtendables) {
$references =& Crush::$process->references;
// Filter the extendArgs list to usable references.
$filtered = [];
foreach ($this->extendArgs as $extendArg) {
if (isset($references[$extendArg->name])) {
$parentRule = $references[$extendArg->name];
$parentRule->resolveExtendables();
$extendArg->pointer = $parentRule;
$filtered[$parentRule->label] = $extendArg;
}
}
$this->resolvedExtendables = true;
$this->extendArgs = $filtered;
}
return true;
}
public function applyExtendables()
{
if (! $this->resolveExtendables()) {
return;
}
// Create a stack of all parent rule args.
$parentExtendArgs = [];
foreach ($this->extendArgs as $extendArg) {
$parentExtendArgs += $extendArg->pointer->extendArgs;
}
// Merge this rule's extendArgs with parent extendArgs.
$this->extendArgs += $parentExtendArgs;
// Add this rule's selectors to all extendArgs.
foreach ($this->extendArgs as $extendArg) {
$ancestor = $extendArg->pointer;
$extendSelectors = $this->selectors->store;
// If there is a pseudo class extension create a new set accordingly.
if ($extendArg->pseudo) {
$extendSelectors = [];
foreach ($this->selectors->store as $selector) {
$newSelector = clone $selector;
$newReadable = $newSelector->appendPseudo($extendArg->pseudo);
$extendSelectors[$newReadable] = $newSelector;
}
}
$ancestor->extendSelectors += $extendSelectors;
}
}
}

View file

@ -0,0 +1,105 @@
<?php
/**
*
* Selector objects.
*
*/
namespace CssCrush;
class Selector
{
public $value;
public $readableValue;
public $allowPrefix = true;
public function __construct($rawSelector)
{
// Look for rooting prefix.
if (strpos($rawSelector, '^') === 0) {
$rawSelector = ltrim($rawSelector, "^ \n\r\t");
$this->allowPrefix = false;
}
$this->readableValue = Selector::makeReadable($rawSelector);
$this->value = Selector::expandAliases($rawSelector);
}
public function __toString()
{
if (Crush::$process->minifyOutput) {
// Trim whitespace around selector combinators.
$this->value = preg_replace('~ ?([>\~+]) ?~S', '$1', $this->value);
}
else {
$this->value = Selector::normalizeWhiteSpace($this->value);
}
return $this->value;
}
public function appendPseudo($pseudo)
{
// Check to avoid doubling-up.
if (! StringObject::endsWith($this->readableValue, $pseudo)) {
$this->readableValue .= $pseudo;
$this->value .= $pseudo;
}
return $this->readableValue;
}
public static function normalizeWhiteSpace($str)
{
// Create space around combinators, then normalize whitespace.
return Util::normalizeWhiteSpace(preg_replace('~([>+]|\~(?!=))~S', ' $1 ', $str));
}
public static function makeReadable($str)
{
$str = Selector::normalizeWhiteSpace($str);
// Quick test for string tokens.
if (strpos($str, '?s') !== false) {
$str = Crush::$process->tokens->restore($str, 's');
}
return $str;
}
public static function expandAliases($str)
{
$process = Crush::$process;
if (! $process->selectorAliases || ! preg_match($process->selectorAliasesPatt, $str)) {
return $str;
}
while (preg_match_all($process->selectorAliasesPatt, $str, $m, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
$alias_call = end($m);
$alias_name = strtolower($alias_call[1][0]);
$start = $alias_call[0][1];
$length = strlen($alias_call[0][0]);
$args = [];
// It's a function alias if a start paren is matched.
if (isset($alias_call[2])) {
// Parse argument list.
if (preg_match(Regex::$patt->parens, $str, $parens, PREG_OFFSET_CAPTURE, $start)) {
$args = Functions::parseArgs($parens[2][0]);
// Amend offsets.
$paren_start = $parens[0][1];
$paren_len = strlen($parens[0][0]);
$length = ($paren_start + $paren_len) - $start;
}
}
$str = substr_replace($str, $process->selectorAliases[$alias_name]($args), $start, $length);
}
return $str;
}
}

View file

@ -0,0 +1,63 @@
<?php
/**
*
* Selector aliases.
*
*/
namespace CssCrush;
class SelectorAlias
{
protected $type;
protected $handler;
public function __construct($handler, $type = 'alias')
{
$this->handler = $handler;
$this->type = $type;
switch ($this->type) {
case 'alias':
$this->handler = new Template($handler);
break;
}
}
public function __invoke($args)
{
$handler = $this->handler;
$tokens = Crush::$process->tokens;
$splat_arg_patt = Regex::make('~#\((?<fallback>{{ ident }})?\)~');
switch ($this->type) {
case 'alias':
return $handler($args);
case 'callback':
$template = new Template($handler($args));
return $template($args);
case 'splat':
$handler = $tokens->restore($handler, 's');
if ($args) {
$list = [];
foreach ($args as $arg) {
$list[] = SelectorAlias::wrap(
$tokens->capture(preg_replace($splat_arg_patt, $arg, $handler), 's')
);
}
$handler = implode(',', $list);
}
else {
$handler = $tokens->capture(preg_replace_callback($splat_arg_patt, function ($m) {
return $m['fallback'];
}, $handler), 's');
}
return SelectorAlias::wrap($handler);
}
}
public static function wrap($str)
{
return strpos($str, ',') !== false ? ":any($str)" : $str;
}
}

View file

@ -0,0 +1,136 @@
<?php
/**
*
* Selector lists.
*
*/
namespace CssCrush;
class SelectorList extends Iterator
{
public function __construct($selectorString, Rule $rule)
{
parent::__construct();
$selectorString = trim(Util::stripCommentTokens($selectorString));
foreach (Util::splitDelimList($selectorString) as $selector) {
if (preg_match(Regex::$patt->abstract, $selector, $m)) {
$rule->name = strtolower($m['name']);
$rule->isAbstract = true;
}
else {
$this->add(new Selector($selector));
}
}
}
public function add(Selector $selector)
{
$this->store[$selector->readableValue] = $selector;
}
public function join($glue = ',')
{
return implode($glue, $this->store);
}
public function expand()
{
static $grouping_patt, $expand, $expandSelector;
if (! $grouping_patt) {
$grouping_patt = Regex::make('~\:any{{ parens }}~iS');
$expand = function ($selector_string) use ($grouping_patt)
{
if (preg_match($grouping_patt, $selector_string, $m, PREG_OFFSET_CAPTURE)) {
list($full_match, $full_match_offset) = $m[0];
$before = substr($selector_string, 0, $full_match_offset);
$after = substr($selector_string, strlen($full_match) + $full_match_offset);
$selectors = [];
// Allowing empty strings for more expansion possibilities.
foreach (Util::splitDelimList($m['parens_content'][0], ['allow_empty_strings' => true]) as $segment) {
if ($selector = trim("$before$segment$after")) {
$selectors[$selector] = true;
}
}
return $selectors;
}
return false;
};
$expandSelector = function ($selector_string) use ($expand)
{
if ($running_stack = $expand($selector_string)) {
$flattened_stack = [];
do {
$loop_stack = [];
foreach ($running_stack as $selector => $bool) {
$selectors = $expand($selector);
if (! $selectors) {
$flattened_stack += [$selector => true];
}
else {
$loop_stack += $selectors;
}
}
$running_stack = $loop_stack;
} while ($loop_stack);
return $flattened_stack;
}
return [$selector_string => true];
};
}
$expanded_set = [];
foreach ($this->store as $original_selector) {
if (stripos($original_selector->value, ':any(') !== false) {
foreach ($expandSelector($original_selector->value) as $selector_string => $bool) {
$new = new Selector($selector_string);
$expanded_set[$new->readableValue] = $new;
}
}
else {
$expanded_set[$original_selector->readableValue] = $original_selector;
}
}
$this->store = $expanded_set;
}
public function merge($rawSelectors)
{
$stack = [];
foreach ($rawSelectors as $rawParentSelector) {
foreach ($this->store as $selector) {
$useParentSymbol = strpos($selector->value, '&') !== false;
if (! $selector->allowPrefix && ! $useParentSymbol) {
$stack[$selector->readableValue] = $selector;
}
elseif ($useParentSymbol) {
$new = new Selector(str_replace('&', $rawParentSelector, $selector->value));
$stack[$new->readableValue] = $new;
}
else {
$new = new Selector("$rawParentSelector {$selector->value}");
$stack[$new->readableValue] = $new;
}
}
}
$this->store = $stack;
}
}

View file

@ -0,0 +1,156 @@
<?php
/**
*
* String sugar.
*
*/
namespace CssCrush;
class StringObject
{
public function __construct($str)
{
$this->raw = $str;
}
public function __toString()
{
return $this->raw;
}
public static function endsWith($haystack, $needle)
{
return substr($haystack, -strlen($needle)) === $needle;
}
public function update($str)
{
$this->raw = $str;
return $this;
}
public function substr($start, $length = null)
{
if (! isset($length)) {
return substr($this->raw, $start);
}
else {
return substr($this->raw, $start, $length);
}
}
public function matchAll($patt, $offset = 0)
{
return Regex::matchAll($patt, $this->raw, $offset);
}
public function replaceHash($replacements)
{
if ($replacements) {
$this->raw = str_replace(
array_keys($replacements),
array_values($replacements),
$this->raw);
}
return $this;
}
public function pregReplaceHash($replacements)
{
if ($replacements) {
$this->raw = preg_replace(
array_keys($replacements),
array_values($replacements),
$this->raw);
}
return $this;
}
public function pregReplaceCallback($patt, $callback)
{
$this->raw = preg_replace_callback($patt, $callback, $this->raw);
return $this;
}
public function append($append)
{
$this->raw .= $append;
return $this;
}
public function prepend($prepend)
{
$this->raw = $prepend . $this->raw;
return $this;
}
public function splice($replacement, $offset, $length = null)
{
$this->raw = substr_replace($this->raw, $replacement, $offset, $length);
return $this;
}
public function trim()
{
$this->raw = trim($this->raw);
return $this;
}
public function rTrim()
{
$this->raw = rtrim($this->raw);
return $this;
}
public function lTrim()
{
$this->raw = ltrim($this->raw);
return $this;
}
public function restore($types, $release = false, $callback = null)
{
$this->raw = Crush::$process->tokens->restore($this->raw, $types, $release, $callback);
return $this;
}
public function captureDirectives($directive, $parse_options = [])
{
if (is_array($directive)) {
$directive = '(?:' . implode('|', $directive) . ')';
}
$parse_options += [
'keyed' => true,
'lowercase_keys' => true,
'ignore_directives' => true,
'singles' => false,
'flatten' => false,
];
if ($parse_options['singles']) {
$patt = Regex::make('~@(?i)' . $directive . '(?-i)(?:\s*{{ block }}|\s+(?<name>{{ ident }})\s+(?<value>[^;]+)\s*;)~S');
}
else {
$patt = Regex::make('~@(?i)' . $directive . '(?-i)\s*{{ block }}~S');
}
$captured_directives = [];
$this->pregReplaceCallback($patt, function ($m) use (&$captured_directives, $parse_options) {
if (isset($m['name'])) {
$name = $parse_options['lowercase_keys'] ? strtolower($m['name']) : $m['name'];
$captured_directives[$name] = $m['value'];
}
else {
$captured_directives = DeclarationList::parse($m['block_content'], $parse_options) + $captured_directives;
}
return '';
});
return $captured_directives;
}
}

View file

@ -0,0 +1,147 @@
<?php
/**
*
* Generalized 'in CSS' templating.
*
*/
namespace CssCrush;
class Template
{
// Positional argument default values.
public $defaults = [];
// The number of expected arguments.
public $argCount = 0;
public $substitutions;
// The string passed in with arg calls replaced by tokens.
public $string;
public function __construct($str)
{
static $templateFunctions;
if (! $templateFunctions) {
$templateFunctions = new Functions();
}
$str = Template::unTokenize($str);
// Parse all arg function calls in the passed string,
// callback creates default values.
$self = $this;
$captureCallback = function ($str) use (&$self)
{
$args = Functions::parseArgsSimple($str);
$position = array_shift($args);
// Match the argument index integer.
if (! isset($position) || ! ctype_digit($position)) {
return '';
}
// Store the default value.
$defaultValue = isset($args[0]) ? $args[0] : null;
if (isset($defaultValue)) {
$self->defaults[$position] = $defaultValue;
}
// Update argument count.
$argNumber = ((int) $position) + 1;
$self->argCount = max($self->argCount, $argNumber);
return "?a$position?";
};
$templateFunctions->register['#'] = $captureCallback;
$this->string = $templateFunctions->apply($str);
}
public function __invoke(array $args = null, $str = null)
{
$str = isset($str) ? $str : $this->string;
// Apply passed arguments as priority.
if (isset($args)) {
list($find, $replace) = $this->prepare($args, false);
}
// Secondly use prepared substitutions if available.
elseif ($this->substitutions) {
list($find, $replace) = $this->substitutions;
}
// Apply substitutions.
$str = isset($find) ? str_replace($find, $replace, $str) : $str;
return Template::tokenize($str);
}
public function getArgValue($index, &$args)
{
// First lookup a passed value.
if (isset($args[$index]) && $args[$index] !== 'default') {
return $args[$index];
}
// Get a default value.
$default = isset($this->defaults[$index]) ? $this->defaults[$index] : '';
// Recurse for nested arg() calls.
while (preg_match(Regex::$patt->a_token, $default, $m)) {
$default = str_replace(
$m[0],
$this->getArgValue((int) $m[1], $args),
$default);
}
return $default;
}
public function prepare(array $args, $persist = true)
{
// Create table of substitutions.
$find = [];
$replace = [];
if ($this->argCount) {
$argIndexes = range(0, $this->argCount-1);
foreach ($argIndexes as $index) {
$find[] = "?a$index?";
$replace[] = $this->getArgValue($index, $args);
}
}
$substitutions = [$find, $replace];
// Persist substitutions by default.
if ($persist) {
$this->substitutions = $substitutions;
}
return $substitutions;
}
public static function tokenize($str)
{
$str = Crush::$process->tokens->capture($str, 's');
$str = Crush::$process->tokens->capture($str, 'u');
return $str;
}
public static function unTokenize($str)
{
$str = Crush::$process->tokens->restore($str, ['u', 's']);
return $str;
}
}

View file

@ -0,0 +1,161 @@
<?php
/**
*
* Token API.
*
*/
namespace CssCrush;
class Tokens
{
public $store;
protected $ids;
public function __construct(array $types = null)
{
$types = $types ?: [
's', // strings.
'c', // comments.
'r', // rules.
'u', // URLs.
't', // traces.
];
$this->store = new \stdClass;
$this->ids = new \stdClass;
foreach ($types as $type) {
$this->store->$type = [];
$this->ids->$type = 0;
}
}
public function get($label)
{
$path =& $this->store->{$label[1]};
return isset($path[$label]) ? $path[$label] : null;
}
public function pop($label)
{
$value = $this->get($label);
if (isset($value)) {
unset($this->store->{$label[1]}[$label]);
}
return $value;
}
public function add($value, $type = null, $existing_label = null)
{
if ($value instanceof Url) {
$type = 'u';
}
elseif ($value instanceof Rule) {
$type = 'r';
}
$label = $existing_label ? $existing_label : $this->createLabel($type);
$this->store->{$type}[$label] = $value;
return $label;
}
public function createLabel($type)
{
$counter = base_convert(++$this->ids->$type, 10, 36);
return "?$type$counter?";
}
public function restore($str, $types, $release = false, $callback = null)
{
$types = implode('', (array) $types);
$patt = Regex::make("~\?[$types]{{ token_id }}\?~S");
$tokens = $this;
$callback = $callback ?: function ($m) use ($tokens, $release) {
return $release ? $tokens->pop($m[0]) : $tokens->get($m[0]);
};
return preg_replace_callback($patt, $callback, $str);
}
public function capture($str, $type)
{
switch ($type) {
case 'u':
return $this->captureUrls($str);
break;
case 's':
return preg_replace_callback(Regex::$patt->string, function ($m) {
return Crush::$process->tokens->add($m[0], 's');
}, $str);
}
}
public function captureUrls($str, $add_padding = false)
{
$count = preg_match_all(
Regex::make('~@import \s+ (?<import>{{s_token}}) | {{LB}} (?<func>url|data-uri) {{parens}}~ixS'),
$str,
$m,
PREG_OFFSET_CAPTURE);
while ($count--) {
list($full_text, $full_offset) = $m[0][$count];
list($import_text, $import_offset) = $m['import'][$count];
// @import directive.
if ($import_offset !== -1) {
$label = $this->add(new Url(trim($import_text)));
$str = str_replace($import_text, $add_padding ? str_pad($label, strlen($import_text)) : $label, $str);
}
// A URL function.
else {
$func_name = strtolower($m['func'][$count][0]);
$url = new Url(trim($m['parens_content'][$count][0]));
$url->convertToData = 'data-uri' === $func_name;
$label = $this->add($url);
$str = substr_replace(
$str,
$add_padding ? Tokens::pad($label, $full_text) : $label,
$full_offset,
strlen($full_text));
}
}
return $str;
}
public static function pad($label, $replaced_text)
{
// Padding token labels to maintain whitespace and newlines.
if (($last_newline_pos = strrpos($replaced_text, "\n")) !== false) {
$label .= str_repeat("\n", substr_count($replaced_text, "\n")) . str_repeat(' ', strlen(substr($replaced_text, $last_newline_pos))-1);
}
else {
$label = str_pad($label, strlen($replaced_text));
}
return $label;
}
public static function is($label, $of_type)
{
if (preg_match(Regex::$patt->token, $label, $m)) {
return $of_type ? ($of_type === $m['type']) : true;
}
return false;
}
public static function test($value)
{
return preg_match(Regex::$patt->token, $value, $m) ? $m['type'] : false;
}
}

View file

@ -0,0 +1,214 @@
<?php
/**
*
* URL tokens.
*
*/
namespace CssCrush;
class Url
{
public $protocol;
public $isAbsolute;
public $isRelative;
public $isRooted;
public $isData;
public $noRewrite;
public $convertToData;
public $value;
public $originalValue;
public function __construct($raw_value)
{
if (preg_match(Regex::$patt->s_token, $raw_value)) {
$this->value = trim(Crush::$process->tokens->pop($raw_value), '\'"');
}
else {
$this->value = $raw_value;
}
$this->originalValue = $this->value;
$this->evaluate();
}
public function __toString()
{
if ($this->convertToData) {
$this->toData();
}
if ($this->isRelative || $this->isRooted) {
$this->simplify();
}
if ($this->isData) {
return 'url("' . preg_replace('~(?<!\x5c)"~', '\\"', $this->value) . '")';
}
// Only wrap url with quotes if it contains tricky characters.
$quote = '';
if (preg_match('~[()*\s]~S', $this->value)) {
$quote = '"';
}
return "url($quote$this->value$quote)";
}
public function update($new_value)
{
$this->value = $new_value;
return $this->evaluate();
}
public function evaluate()
{
// Protocol, protocol-relative (//) or fragment URL.
if (preg_match('~^(?: (?<protocol>[a-z]+)\: | \/{2} | \# )~ix', $this->value, $m)) {
$this->protocol = ! empty($m['protocol']) ? strtolower($m['protocol']) : 'relative';
switch ($this->protocol) {
case 'data':
$type = 'data';
break;
default:
$type = 'absolute';
break;
}
}
// Relative and rooted URLs.
else {
$type = 'relative';
$leading_variable = strpos($this->value, '$(') === 0;
// Normalize './' led paths.
$this->value = preg_replace('~^\.\/+~i', '', $this->value);
if ($leading_variable || ($this->value !== '' && $this->value[0] === '/')) {
$type = 'rooted';
}
// Normalize slashes.
$this->value = rtrim(preg_replace('~[\\\\/]+~', '/', $this->value), '/');
}
$this->setType($type);
return $this;
}
public function isRelativeImplicit()
{
return $this->isRelative && preg_match('~^([\w$-]|\.[^\/.])~', $this->originalValue);
}
public function getAbsolutePath()
{
$path = false;
if ($this->protocol) {
$path = $this->value;
}
elseif ($this->isRelative || $this->isRooted) {
$path = Crush::$process->docRoot .
($this->isRelative ? $this->toRoot()->simplify()->value : $this->value);
}
return $path;
}
public function prepend($path_fragment)
{
if ($this->isRelative) {
$this->value = $path_fragment . $this->value;
}
return $this;
}
public function toRoot()
{
if ($this->isRelative) {
$this->prepend(Crush::$process->input->dirUrl . '/');
$this->setType('rooted');
}
return $this;
}
public function toData()
{
// Only make one conversion attempt.
$this->convertToData = false;
$file = Crush::$process->docRoot . $this->toRoot()->value;
// File not found.
if (! file_exists($file)) {
return $this;
}
$file_ext = pathinfo($file, PATHINFO_EXTENSION);
// Only allow certain extensions
static $allowed_file_extensions = [
'woff' => 'application/x-font-woff;charset=utf-8',
'ttf' => 'font/truetype;charset=utf-8',
'svg' => 'image/svg+xml',
'svgz' => 'image/svg+xml',
'gif' => 'image/gif',
'jpeg' => 'image/jpg',
'jpg' => 'image/jpg',
'png' => 'image/png',
];
if (! isset($allowed_file_extensions[$file_ext])) {
return $this;
}
$mime_type = $allowed_file_extensions[$file_ext];
$base64 = base64_encode(file_get_contents($file));
$this->value = "data:$mime_type;base64,$base64";
$this->setType('data')->protocol = 'data';
return $this;
}
public function setType($type = 'absolute')
{
$this->isAbsolute = false;
$this->isRooted = false;
$this->isRelative = false;
$this->isData = false;
switch ($type) {
case 'absolute':
$this->isAbsolute = true;
break;
case 'relative':
$this->isRelative = true;
break;
case 'rooted':
$this->isRooted = true;
break;
case 'data':
$this->isData = true;
$this->convertToData = false;
break;
}
return $this;
}
public function simplify()
{
if ($this->isRelative || $this->isRooted) {
$this->value = Util::simplifyPath($this->value);
}
return $this;
}
}

View file

@ -0,0 +1,277 @@
<?php
/**
*
* General utilities.
*
*/
namespace CssCrush;
class Util
{
public static function htmlAttributes(array $attributes, array $sort_order = null)
{
// Optionally sort attributes (for better readability).
if ($sort_order) {
uksort($attributes, function ($a, $b) use ($sort_order) {
$a_index = array_search($a, $sort_order);
$b_index = array_search($b, $sort_order);
$a_found = is_int($a_index);
$b_found = is_int($b_index);
if ($a_found && $b_found) {
if ($a_index == $b_index) {
return 0;
}
return $a_index > $b_index ? 1 : -1;
}
elseif ($a_found && ! $b_found) {
return -1;
}
elseif ($b_found && ! $a_found) {
return 1;
}
return strcmp($a, $b);
});
}
$str = '';
foreach ($attributes as $name => $value) {
$value = htmlspecialchars($value, ENT_COMPAT, 'UTF-8', false);
$str .= " $name=\"$value\"";
}
return $str;
}
public static function normalizePath($path, $strip_drive_letter = false)
{
if (! $path) {
return '';
}
if ($strip_drive_letter) {
$path = preg_replace('~^[a-z]\:~i', '', $path);
}
// Backslashes and repeat slashes to a single forward slash.
$path = rtrim(preg_replace('~[\\\\/]+~', '/', $path), '/');
// Removing redundant './'.
$path = str_replace('/./', '/', $path);
if (strpos($path, './') === 0) {
$path = substr($path, 2);
}
return Util::simplifyPath($path);
}
public static function simplifyPath($path)
{
// Reduce redundant path segments. e.g 'foo/../bar' => 'bar'
$patt = '~[^/.]+/\.\./~S';
while (preg_match($patt, $path)) {
$path = preg_replace($patt, '', $path);
}
return $path;
}
public static function resolveUserPath($path, callable $recovery = null, $docRoot = null)
{
// System path.
if ($realpath = realpath($path)) {
$path = $realpath;
}
else {
if (! $docRoot) {
$docRoot = isset(Crush::$process->docRoot) ? Crush::$process->docRoot : Crush::$config->docRoot;
}
// Absolute path.
if (strpos($path, '/') === 0) {
// If $path is not doc_root based assume it's doc_root relative and prepend doc_root.
if (strpos($path, $docRoot) !== 0) {
$path = $docRoot . $path;
}
}
// Relative path. Try resolving based on the directory of the executing script.
else {
$path = Crush::$config->scriptDir . '/' . $path;
}
if (! file_exists($path) && $recovery) {
$path = $recovery($path);
}
$path = realpath($path);
}
return $path ? Util::normalizePath($path) : false;
}
public static function stripCommentTokens($str)
{
return preg_replace(Regex::$patt->c_token, '', $str);
}
public static function normalizeWhiteSpace($str)
{
static $find, $replace;
if (! $find) {
$replacements = [
// Convert all whitespace sequences to a single space.
'~\s+~S' => ' ',
// Trim bracket whitespace where it's safe to do it.
'~([\[(]) | ([\])])| ?([{}]) ?~S' => '${1}${2}${3}',
// Trim whitespace around delimiters and special characters.
'~ ?([;,]) ?~S' => '$1',
];
$find = array_keys($replacements);
$replace = array_values($replacements);
}
return preg_replace($find, $replace, $str);
}
public static function splitDelimList($str, $options = [])
{
extract($options + [
'delim' => ',',
'regex' => false,
'allow_empty_strings' => false,
]);
$str = trim($str);
if (! $regex && strpos($str, $delim) === false) {
return ! $allow_empty_strings && ! strlen($str) ? [] : [$str];
}
if ($match_count = preg_match_all(Regex::$patt->parens, $str, $matches)) {
$keys = [];
foreach ($matches[0] as $index => &$value) {
$keys[] = "?$index?";
}
$str = str_replace($matches[0], $keys, $str);
}
$list = $regex ? preg_split($regex, $str) : explode($delim, $str);
if ($match_count) {
foreach ($list as &$value) {
$value = str_replace($keys, $matches[0], $value);
}
}
$list = array_map('trim', $list);
return ! $allow_empty_strings ? array_filter($list, 'strlen') : $list;
}
public static function getLinkBetweenPaths($path1, $path2, $directories = true)
{
$path1 = trim(Util::normalizePath($path1, true), '/');
$path2 = trim(Util::normalizePath($path2, true), '/');
$link = '';
if ($path1 != $path2) {
// Split the directory paths into arrays so we can compare segment by segment.
$path1_segs = explode('/', $path1);
$path2_segs = explode('/', $path2);
// Shift the segments until they are on different branches.
while (isset($path1_segs[0]) && isset($path2_segs[0]) && ($path1_segs[0] === $path2_segs[0])) {
array_shift($path1_segs);
array_shift($path2_segs);
}
$link = str_repeat('../', count($path1_segs)) . implode('/', $path2_segs);
}
$link = $link !== '' ? rtrim($link, '/') : '';
// Append end slash if getting a link between directories.
if ($link && $directories) {
$link .= '/';
}
return $link;
}
public static function filePutContents($file, $str)
{
if ($stream = fopen($file, 'w')) {
fwrite($stream, $str);
fclose($stream);
return true;
}
warning("Could not write file '$file'.");
return false;
}
public static function parseIni($path, $sections = false)
{
if (! ($result = @parse_ini_file($path, $sections))) {
notice("Ini file '$path' could not be parsed.");
return false;
}
return $result;
}
public static function readConfigFile($path)
{
require_once $path;
return Options::filter(get_defined_vars());
}
/*
* Get raw value (useful if testing values that may or may not be a token).
*/
public static function rawValue($value)
{
if ($tokenType = Tokens::test($value)) {
if ($tokenType == 'u') {
$value = Crush::$process->tokens->get($value)->value;
}
elseif ($tokenType == 's') {
$value = Crush::$process->tokens->get($value);
}
}
return $value;
}
/*
* Encode integer to Base64 VLQ.
*/
public static function vlqEncode($value)
{
static $VLQ_BASE_SHIFT, $VLQ_BASE, $VLQ_BASE_MASK, $VLQ_CONTINUATION_BIT, $BASE64_MAP;
if (! $VLQ_BASE_SHIFT) {
$VLQ_BASE_SHIFT = 5;
$VLQ_BASE = 1 << $VLQ_BASE_SHIFT;
$VLQ_BASE_MASK = $VLQ_BASE - 1;
$VLQ_CONTINUATION_BIT = $VLQ_BASE;
$BASE64_MAP = str_split('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/');
}
$vlq = $value < 0 ? ((-$value) << 1) + 1 : ($value << 1) + 0;
$encoded = "";
do {
$digit = $vlq & $VLQ_BASE_MASK;
$vlq >>= $VLQ_BASE_SHIFT;
if ($vlq > 0) {
$digit |= $VLQ_CONTINUATION_BIT;
}
$encoded .= $BASE64_MAP[$digit];
} while ($vlq > 0);
return $encoded;
}
}

View file

@ -0,0 +1,116 @@
<?php
/**
*
* Version string.
*
*/
namespace CssCrush;
class Version
{
public $major;
public $minor;
public $patch;
public $extra;
public function __construct($version_string)
{
// Ideally expecting `git describe --long` (e.g. v2.0.0-5-gb28cdb5)
// but also accepting simpler formats.
preg_match('~^
v?
(?<major>\d+)
(?:\.(?<minor>\d+))?
(?:\.(?<patch>\d+))?
(?:-(?<extra>.+))?
$~ix',
$version_string,
$version);
if ($version) {
$this->major = (int) $version['major'];
$this->minor = isset($version['minor']) ? (int) $version['minor'] : 0;
$this->patch = isset($version['patch']) ? (int) $version['patch'] : 0;
$this->extra = isset($version['extra']) ? $version['extra'] : null;
}
}
public function __toString()
{
$out = (string) $this->major;
if (isset($this->minor)) {
$out .= ".$this->minor";
}
if (isset($this->patch)) {
$out .= ".$this->patch";
}
if (isset($this->extra)) {
$out .= "-$this->extra";
}
return "v$out";
}
public function compare($version_string)
{
$LESS = -1;
$MORE = 1;
$EQUAL = 0;
$test = new Version($version_string);
foreach (['major', 'minor', 'patch'] as $level) {
if ($this->{$level} < $test->{$level}) {
return $LESS;
}
elseif ($this->{$level} > $test->{$level}) {
return $MORE;
}
}
return $EQUAL;
}
public static function detect() {
return self::gitDescribe() ?: self::packageDescribe();
}
public static function gitDescribe()
{
static $attempted, $version;
if (! $attempted && file_exists(Crush::$dir . '/.git')) {
$attempted = true;
$command = 'cd ' . escapeshellarg(Crush::$dir) . ' && git describe --tag --long';
@exec($command, $lines);
if ($lines) {
$version = new Version(trim($lines[0]));
if (is_null($version->major)) {
$version = null;
}
}
}
return $version;
}
public static function packageDescribe()
{
static $attempted, $version;
if (! $attempted && file_exists(Crush::$dir . '/package.json')) {
$attempted = true;
$package = json_decode(file_get_contents(Crush::$dir . '/package.json'));
if ($package->version) {
$version = new Version($package->version);
if (is_null($version->major)) {
$version = null;
}
}
}
return $version;
}
}

View file

@ -0,0 +1,167 @@
<?php
/**
*
* Public API.
*
*/
use CssCrush\Crush;
/**
* Process CSS file and return a new compiled file.
*
* @see docs/api/functions.md
*/
function csscrush_file($file, $options = []) {
try {
Crush::$process = new CssCrush\Process($options, ['type' => 'file', 'data' => $file]);
}
catch (\Exception $e) {
CssCrush\warning($e->getMessage());
return '';
}
return new CssCrush\File(Crush::$process);
}
/**
* Process CSS file and return an HTML link tag with populated href.
*
* @see docs/api/functions.md
*/
function csscrush_tag($file, $options = [], $tag_attributes = []) {
$file = csscrush_file($file, $options);
if ($file && $file->url) {
$tag_attributes['href'] = $file->url;
$tag_attributes += [
'rel' => 'stylesheet',
'media' => 'all',
];
$attrs = CssCrush\Util::htmlAttributes($tag_attributes, ['rel', 'href', 'media']);
return "<link$attrs />\n";
}
}
/**
* Process CSS file and return CSS as text wrapped in html style tags.
*
* @see docs/api/functions.md
*/
function csscrush_inline($file, $options = [], $tag_attributes = []) {
if (! is_array($options)) {
$options = [];
}
if (! isset($options['boilerplate'])) {
$options['boilerplate'] = false;
}
$file = csscrush_file($file, $options);
if ($file && $file->path) {
$tagOpen = '';
$tagClose = '';
if (is_array($tag_attributes)) {
$attrs = CssCrush\Util::htmlAttributes($tag_attributes);
$tagOpen = "<style$attrs>";
$tagClose = '</style>';
}
return $tagOpen . file_get_contents($file->path) . $tagClose . "\n";
}
}
/**
* Compile a raw string of CSS string and return it.
*
* @see docs/api/functions.md
*/
function csscrush_string($string, $options = []) {
if (! isset($options['boilerplate'])) {
$options['boilerplate'] = false;
}
Crush::$process = new CssCrush\Process($options, ['type' => 'filter', 'data' => $string]);
return Crush::$process->compile()->__toString();
}
/**
* Set default options and config settings.
*
* @see docs/api/functions.md
*/
function csscrush_set($object_name, $modifier) {
if (in_array($object_name, ['options', 'config'])) {
$pointer = $object_name === 'options' ? Crush::$config->options : Crush::$config;
if (is_callable($modifier)) {
$modifier($pointer);
}
elseif (is_array($modifier)) {
foreach ($modifier as $key => $value) {
$pointer->{$key} = $value;
}
}
}
}
/**
* Get default options and config settings.
*
* @see docs/api/functions.md
*/
function csscrush_get($object_name, $property = null) {
if (in_array($object_name, ['options', 'config'])) {
$pointer = $object_name === 'options' ? Crush::$config->options : Crush::$config;
if (! isset($property)) {
return $pointer;
}
else {
return isset($pointer->{$property}) ? $pointer->{$property} : null;
}
}
return null;
}
/**
* Add plugin.
*
* @see docs/api/functions.md
*/
function csscrush_plugin($name, callable $callback) {
Crush::plugin($name, $callback);
}
/**
* Get stats from most recent compile.
*
* @see docs/api/functions.md
*/
function csscrush_stat() {
$process = Crush::$process;
$stats = $process->stat;
// Get logged errors as late as possible.
$stats['errors'] = $process->errors;
$stats['warnings'] = $process->warnings;
$stats += ['compile_time' => 0];
return $stats;
}

View file

@ -0,0 +1,151 @@
; Sources:
; http://www.w3.org/TR/css3-color
aliceblue = "240,248,255"
antiquewhite = "250,235,215"
aqua = "0,255,255"
aquamarine = "127,255,212"
azure = "240,255,255"
beige = "245,245,220"
bisque = "255,228,196"
black = "0,0,0"
blanchedalmond = "255,235,205"
blue = "0,0,255"
blueviolet = "138,43,226"
brown = "165,42,42"
burlywood = "222,184,135"
cadetblue = "95,158,160"
chartreuse = "127,255,0"
chocolate = "210,105,30"
coral = "255,127,80"
cornflowerblue = "100,149,237"
cornsilk = "255,248,220"
crimson = "220,20,60"
cyan = "0,255,255"
darkblue = "0,0,139"
darkcyan = "0,139,139"
darkgoldenrod = "184,134,11"
darkgray = "169,169,169"
darkgreen = "0,100,0"
darkgrey = "169,169,169"
darkkhaki = "189,183,107"
darkmagenta = "139,0,139"
darkolivegreen = "85,107,47"
darkorange = "255,140,0"
darkorchid = "153,50,204"
darkred = "139,0,0"
darksalmon = "233,150,122"
darkseagreen = "143,188,143"
darkslateblue = "72,61,139"
darkslategray = "47,79,79"
darkslategrey = "47,79,79"
darkturquoise = "0,206,209"
darkviolet = "148,0,211"
deeppink = "255,20,147"
deepskyblue = "0,191,255"
dimgray = "105,105,105"
dimgrey = "105,105,105"
dodgerblue = "30,144,255"
firebrick = "178,34,34"
floralwhite = "255,250,240"
forestgreen = "34,139,34"
fuchsia = "255,0,255"
gainsboro = "220,220,220"
ghostwhite = "248,248,255"
gold = "255,215,0"
goldenrod = "218,165,32"
gray = "128,128,128"
green = "0,128,0"
greenyellow = "173,255,47"
grey = "128,128,128"
honeydew = "240,255,240"
hotpink = "255,105,180"
indianred = "205,92,92"
indigo = "75,0,130"
ivory = "255,255,240"
khaki = "240,230,140"
lavender = "230,230,250"
lavenderblush = "255,240,245"
lawngreen = "124,252,0"
lemonchiffon = "255,250,205"
lightblue = "173,216,230"
lightcoral = "240,128,128"
lightcyan = "224,255,255"
lightgoldenrodyellow = "250,250,210"
lightgray = "211,211,211"
lightgreen = "144,238,144"
lightgrey = "211,211,211"
lightpink = "255,182,193"
lightsalmon = "255,160,122"
lightseagreen = "32,178,170"
lightskyblue = "135,206,250"
lightslategray = "119,136,153"
lightslategrey = "119,136,153"
lightsteelblue = "176,196,222"
lightyellow = "255,255,224"
lime = "0,255,0"
limegreen = "50,205,50"
linen = "250,240,230"
magenta = "255,0,255"
maroon = "128,0,0"
mediumaquamarine = "102,205,170"
mediumblue = "0,0,205"
mediumorchid = "186,85,211"
mediumpurple = "147,112,219"
mediumseagreen = "60,179,113"
mediumslateblue = "123,104,238"
mediumspringgreen = "0,250,154"
mediumturquoise = "72,209,204"
mediumvioletred = "199,21,133"
midnightblue = "25,25,112"
mintcream = "245,255,250"
mistyrose = "255,228,225"
moccasin = "255,228,181"
navajowhite = "255,222,173"
navy = "0,0,128"
oldlace = "253,245,230"
olive = "128,128,0"
olivedrab = "107,142,35"
orange = "255,165,0"
orangered = "255,69,0"
orchid = "218,112,214"
palegoldenrod = "238,232,170"
palegreen = "152,251,152"
paleturquoise = "175,238,238"
palevioletred = "219,112,147"
papayawhip = "255,239,213"
peachpuff = "255,218,185"
peru = "205,133,63"
pink = "255,192,203"
plum = "221,160,221"
powderblue = "176,224,230"
purple = "128,0,128"
rebeccapurple = "102,51,153"
red = "255,0,0"
rosybrown = "188,143,143"
royalblue = "65,105,225"
saddlebrown = "139,69,19"
salmon = "250,128,114"
sandybrown = "244,164,96"
seagreen = "46,139,87"
seashell = "255,245,238"
sienna = "160,82,45"
silver = "192,192,192"
skyblue = "135,206,235"
slateblue = "106,90,205"
slategray = "112,128,144"
slategrey = "112,128,144"
snow = "255,250,250"
springgreen = "0,255,127"
steelblue = "70,130,180"
tan = "210,180,140"
teal = "0,128,128"
thistle = "216,191,216"
tomato = "255,99,71"
turquoise = "64,224,208"
violet = "238,130,238"
wheat = "245,222,179"
white = "255,255,255"
whitesmoke = "245,245,245"
yellow = "255,255,0"
yellowgreen = "154,205,50"

View file

@ -0,0 +1,48 @@
<?php
/**
*
* Formatter callbacks.
*
*/
namespace CssCrush;
Crush::$config->formatters = [
'single-line' => 'CssCrush\fmtr_single',
'padded' => 'CssCrush\fmtr_padded',
'block' => 'CssCrush\fmtr_block',
];
function fmtr_single($rule) {
$EOL = Crush::$process->newline;
$selectors = $rule->selectors->join(', ');
$block = $rule->declarations->join('; ');
return "$selectors { $block; }$EOL";
}
function fmtr_padded($rule, $padding = 40) {
$EOL = Crush::$process->newline;
$selectors = $rule->selectors->join(', ');
$block = $rule->declarations->join('; ');
if (strlen($selectors) > $padding) {
$padding = str_repeat(' ', $padding);
return "$selectors$EOL$padding { $block; }$EOL";
}
else {
$selectors = str_pad($selectors, $padding);
return "$selectors { $block; }$EOL";
}
}
function fmtr_block($rule, $indent = ' ') {
$EOL = Crush::$process->newline;
$selectors = $rule->selectors->join(",$EOL");
$block = $rule->declarations->join(";$EOL$indent");
return "$selectors {{$EOL}$indent$block;$EOL$indent}$EOL";
}

View file

@ -0,0 +1,176 @@
; Table for property sorting.
; Vendor prefixes are added at runtime.
; Generated content
content
quotes
; Positioning
position
z-index
top
right
bottom
left
; Display
visibility
opacity
display
overflow
overflow-x
overflow-y
vertical-align
; Floats
float
clear
; Transforms
transform
transform-style
perspective
perspective-origin
backface-visibility
; Box-model: dimensions
box-sizing
width
height
min-width
max-width
min-height
max-height
; Box-model: padding
padding
padding-top
padding-right
padding-bottom
padding-left
; Box-model: margins
margin
margin-top
margin-right
margin-bottom
margin-left
; Box-model: borders
border
border-color
border-image
border-radius
border-style
border-width
border-top
border-top-color
border-top-left-radius
border-top-right-radius
border-top-style
border-top-width
border-right
border-right-color
border-right-style
border-right-width
border-bottom
border-bottom-color
border-bottom-style
border-bottom-left-radius
border-bottom-right-radius
border-bottom-width
border-left
border-left-color
border-left-style
border-left-width
; Box-model: effects
box-shadow
; Counters
counter-increment
counter-reset
; Foreground color
color
; Background
background
background-attachment
background-clip
background-color
background-image
background-origin
background-position
background-position-x
background-position-y
background-repeat
background-size
; Text
direction
text-align
text-align-last
text-decoration
text-decoration-color
text-decoration-line
text-decoration-style
text-indent
text-overflow
text-shadow
text-transform
; Fonts: general
font
font-family
font-size
font-style
font-weight
font-variant
line-height
; Fonts: spacing and behaviour
letter-spacing
white-space
word-break
word-spacing
word-wrap
hyphens
orphans
; Outlines
outline
outline-color
outline-offset
outline-style
outline-width
; Animations
animation
animation-delay
animation-direction
animation-duration
animation-fill-mode
animation-iteration-count
animation-name
animation-play-state
animation-timing-function
; Transitions
transition
transition-delay
transition-duration
transition-property
transition-timing-function
; Tables specific
table-layout
border-collapse
caption-side
empty-cells
; Lists specific
list-style
list-style-image
list-style-position
list-style-type

View file

@ -0,0 +1,70 @@
<?php
/**
* Pseudo classes for working with ARIA roles, states and properties
*
* @see docs/plugins/aria.md
*/
namespace CssCrush;
\csscrush_plugin('aria', function ($process) {
foreach (aria() as $name => $handler) {
$type = is_callable($handler) ? 'callback' : 'alias';
$process->addSelectorAlias($name, $handler, $type);
}
});
function aria() {
static $aria, $optional_value;
if (! $aria) {
$optional_value = function ($property) {
return function ($args) use ($property) {
return $args ? "[$property=\"#(0)\"]" : "[$property]";
};
};
$aria = [
// Roles.
'role' => $optional_value('role'),
// States and properties.
'aria-activedescendant' => $optional_value('aria-activedescendant'),
'aria-atomic' => '[aria-atomic="#(0 true)"]',
'aria-autocomplete' => $optional_value('aria-autocomplete'),
'aria-busy' => '[aria-busy="#(0 true)"]',
'aria-checked' => '[aria-checked="#(0 true)"]',
'aria-controls' => $optional_value('aria-controls'),
'aria-describedby' => $optional_value('aria-describedby'),
'aria-disabled' => '[aria-disabled="#(0 true)"]',
'aria-dropeffect' => $optional_value('aria-dropeffect'),
'aria-expanded' => '[aria-expanded="#(0 true)"]',
'aria-flowto' => $optional_value('aria-flowto'),
'aria-grabbed' => '[aria-grabbed="#(0 true)"]',
'aria-haspopup' => '[aria-haspopup="#(0 true)"]',
'aria-hidden' => '[aria-hidden="#(0 true)"]',
'aria-invalid' => '[aria-invalid="#(0 true)"]',
'aria-label' => $optional_value('aria-label'),
'aria-labelledby' => $optional_value('aria-labelledby'),
'aria-level' => $optional_value('aria-level'),
'aria-live' => $optional_value('aria-live'),
'aria-multiline' => '[aria-multiline="#(0 true)"]',
'aria-multiselectable' => '[aria-multiselectable="#(0 true)"]',
'aria-orientation' => $optional_value('aria-orientation'),
'aria-owns' => $optional_value('aria-owns'),
'aria-posinset' => $optional_value('aria-posinset'),
'aria-pressed' => '[aria-pressed="#(0 true)"]',
'aria-readonly' => '[aria-readonly="#(0 true)"]',
'aria-relevant' => $optional_value('aria-relevant'),
'aria-required' => '[aria-required="#(0 true)"]',
'aria-selected' => '[aria-selected="#(0 true)"]',
'aria-setsize' => $optional_value('aria-setsize'),
'aria-sort' => $optional_value('aria-sort'),
'aria-valuemax' => $optional_value('aria-valuemax'),
'aria-valuemin' => $optional_value('aria-valuemin'),
'aria-valuenow' => $optional_value('aria-valuenow'),
'aria-valuetext' => $optional_value('aria-valuetext'),
];
}
return $aria;
}

View file

@ -0,0 +1,652 @@
<?php
/**
* Bitmap image generator
*
* @see docs/plugins/canvas.md
*/
namespace CssCrush;
use stdClass;
\csscrush_plugin('canvas', function ($process) {
$process->on('capture_phase2', 'CssCrush\canvas_capture');
$process->functions->add('canvas', 'CssCrush\canvas_generator');
$process->functions->add('canvas-data', 'CssCrush\canvas_generator');
});
function canvas_capture($process) {
$process->string->pregReplaceCallback(
Regex::make('~@canvas\s+(?<name>{{ ident }})\s*{{ block }}~iS'),
function ($m) {
Crush::$process->misc->canvas_defs[strtolower($m['name'])] = new Template($m['block_content']);
return '';
});
}
function canvas_generator($input, $context) {
$process = Crush::$process;
// Check GD requirements are met.
static $requirements;
if (! isset($requirements)) {
$requirements = canvas_requirements();
}
if ($requirements === false) {
return '';
}
// Check process cache.
$cache_key = $context->function . $input;
if (isset($process->misc->canvas_cache[$cache_key])) {
return $process->misc->canvas_cache[$cache_key];
}
// Parse args, bail if none.
$args = Functions::parseArgs($input);
if (! isset($args[0])) {
return '';
}
$name = strtolower(array_shift($args));
// Bail if name not registered.
$canvas_defs =& $process->misc->canvas_defs;
if (! isset($canvas_defs[$name])) {
return '';
}
// Apply args to template.
$block = $canvas_defs[$name]($args);
$raw = DeclarationList::parse($block, [
'keyed' => true,
'lowercase_keys' => true,
'flatten' => true,
'apply_hooks' => true,
]);
// Create canvas object.
$canvas = new Canvas();
// Parseable canvas attributes with default values.
static $schema = [
'fill' => null,
'background-fill' => null,
'src' => null,
'canvas-filter' => null,
'width' => null,
'height' => null,
'margin' => 0,
];
// Resolve properties, set defaults if not present.
$canvas->raw = array_intersect_key($raw, $schema) + $schema;
// Pre-populate.
canvas_preprocess($canvas);
// Apply functions.
canvas_apply_css_funcs($canvas);
// debug($canvas);
// Create fingerprint for this canvas based on canvas object.
$fingerprint = substr(md5(serialize($canvas)), 0, 7);
$generated_filename = "cnv-$name-$fingerprint.png";
if (! empty($process->options->asset_dir)) {
$generated_filepath = $process->options->asset_dir . '/' . $generated_filename;
$generated_url = Util::getLinkBetweenPaths(
$process->output->dir, $process->options->asset_dir) . $generated_filename;
}
else {
$generated_filepath = $process->output->dir . '/' . $generated_filename;
$generated_url = $generated_filename;
}
$cached_file = file_exists($generated_filepath);
// $cached_file = false;
if (! $cached_file) {
// Source arguments take priority.
if ($src = canvas_fetch_src($canvas->raw['src'])) {
// Resolve the src image dimensions and positioning.
$dst_w = $src->width;
$dst_h = $src->height;
if (isset($canvas->width) && isset($canvas->height)) {
$dst_w = $canvas->width;
$dst_h = $canvas->height;
}
elseif (isset($canvas->width)) {
$dst_w = $canvas->width;
$dst_h = ($src->height/$src->width) * $canvas->width;
}
elseif (isset($canvas->height)) {
$dst_w = ($src->width/$src->height) * $canvas->height;
$dst_h = $canvas->height;
}
// Update the canvas height and width based on the src.
$canvas->width = $dst_w;
$canvas->height = $dst_h;
// Create base.
canvas_create($canvas);
// Apply background layer.
canvas_fill($canvas, 'background-fill');
// Filters.
canvas_apply_filters($canvas, $src);
// Place the src image on the base canvas image.
imagecopyresized(
$canvas->image, // dest_img
$src->image, // src_img
$canvas->margin->left, // dst_x
$canvas->margin->top, // dst_y
0, // src_x
0, // src_y
$dst_w, // dst_w
$dst_h, // dst_h
$src->width, // src_w
$src->height // src_h
);
imagedestroy($src->image);
}
else {
// Set defaults.
$canvas->width = isset($canvas->width) ? intval($canvas->width) : 100;
$canvas->height = isset($canvas->height) ? intval($canvas->height) : 100;
$canvas->fills += ['fill' => 'black'];
// Create base.
canvas_create($canvas);
// Apply background layer.
canvas_fill($canvas, 'background-fill');
canvas_fill($canvas, 'fill');
}
}
else {
// debug('file cached');
}
// Either write to a file.
if ($context->function === 'canvas' && $process->ioContext === 'file') {
if (! $cached_file) {
imagepng($canvas->image, $generated_filepath);
}
$url = new Url($generated_url);
$url->noRewrite = true;
}
// Or create data uri.
else {
if (! $cached_file) {
ob_start();
imagepng($canvas->image);
$data = ob_get_clean();
}
else {
$data = file_get_contents($generated_filepath);
}
$url = new Url('data:image/png;base64,' . base64_encode($data));
}
$label = $process->tokens->add($url);
// Cache the output URL.
$process->misc->canvas_cache[$cache_key] = $label;
return $label;
}
function canvas_fn_linear_gradient($input, $context) {
$args = Functions::parseArgs($input) + ['white', 'black'];
$first_arg = strtolower($args[0]);
static $directions = [
'to top' => ['vertical', true],
'to right' => ['horizontal', false],
'to bottom' => ['vertical', false],
'to left' => ['horizontal', true],
];
if (isset($directions[$first_arg])) {
list($direction, $flip) = $directions[$first_arg];
array_shift($args);
}
else {
list($direction, $flip) = $directions['to bottom'];
}
// Create fill object.
$fill = new stdClass();
$fill->stops = [];
$fill->direction = $direction;
canvas_set_fill_dims($fill, $context->canvas);
// Start color.
$color = Color::parse($args[0]);
$fill->stops[] = $color ? $color : [0, 0, 0, 1];
// End color.
$color = Color::parse($args[1]);
$fill->stops[] = $color ? $color : [255, 255, 255, 1];
if ($flip) {
$fill->stops = array_reverse($fill->stops);
}
$context->canvas->fills[$context->currentProperty] = $fill;
}
function canvas_fn_filter($input, $context) {
$args = Functions::parseArgs($input);
array_unshift($context->canvas->filters, [$context->function, $args]);
}
function canvas_apply_filters($canvas, $src) {
foreach ($canvas->filters as $filter) {
list($name, $args) = $filter;
switch ($name) {
case 'greyscale':
case 'grayscale':
imagefilter($src->image, IMG_FILTER_GRAYSCALE);
break;
case 'invert':
imagefilter($src->image, IMG_FILTER_NEGATE);
break;
case 'opacity':
canvas_fade($src, floatval($args[0]));
break;
case 'colorize':
$rgb = $args + ['black'];
if (count($rgb) === 1) {
// If only one argument parse it as a CSS color value.
$rgb = Color::parse($rgb[0]);
if (! $rgb) {
$rgb = [0, 0, 0];
}
}
imagefilter($src->image, IMG_FILTER_COLORIZE, $rgb[0], $rgb[1], $rgb[2]);
break;
case 'blur':
$level = 1;
if (isset($args[0])) {
// Allow multiple blurs for a stronger effect.
// Set hard limit.
$level = min(max(intval($args[0]), 1), 20);
}
while ($level--) {
imagefilter($src->image, IMG_FILTER_GAUSSIAN_BLUR);
}
break;
case 'contrast':
if (isset($args[0])) {
// By default it works like this:
// (max) -100 <- 0 -> +100 (min)
// But we're flipping the polarity to be more predictable:
// (min) -100 <- 0 -> +100 (max)
$level = intval($args[0]) * -1;
}
imagefilter($src->image, IMG_FILTER_CONTRAST, $level);
break;
case 'brightness':
if (isset($args[0])) {
// -255 <- 0 -> +255
$level = intval($args[0]);
}
imagefilter($src->image, IMG_FILTER_BRIGHTNESS, $level);
break;
}
}
}
function canvas_apply_css_funcs($canvas) {
static $functions;
if (! $functions) {
$functions = new stdClass();
$functions->fill = new Functions(['canvas-linear-gradient' => 'CssCrush\canvas_fn_linear_gradient']);
$functions->generic = new Functions(array_diff_key(Crush::$process->functions->register, $functions->fill->register));
$functions->filter = new Functions([
'contrast' => 'CssCrush\canvas_fn_filter',
'opacity' => 'CssCrush\canvas_fn_filter',
'colorize' => 'CssCrush\canvas_fn_filter',
'grayscale' => 'CssCrush\canvas_fn_filter',
'greyscale' => 'CssCrush\canvas_fn_filter',
'brightness' => 'CssCrush\canvas_fn_filter',
'invert' => 'CssCrush\canvas_fn_filter',
'blur' => 'CssCrush\canvas_fn_filter',
]);
}
$context = new stdClass();
foreach ($canvas->raw as $property => &$value) {
if (! is_string($value)) {
continue;
}
$value = $functions->generic->apply($value);
$context->canvas = $canvas;
if (in_array($property, ['fill', 'background-fill'])) {
$context->currentProperty = $property;
$value = $functions->fill->apply($value, $context);
}
elseif ($property === 'canvas-filter') {
$value = $functions->filter->apply($value, $context);
}
}
}
function canvas_preprocess($canvas) {
if (isset($canvas->raw['margin'])) {
$parts = canvas_parselist($canvas->raw['margin']);
$count = count($parts);
if ($count === 1) {
$margin = [$parts[0], $parts[0], $parts[0], $parts[0]];
}
elseif ($count === 2) {
$margin = [$parts[0], $parts[1], $parts[0], $parts[1]];
}
elseif ($count === 3) {
$margin = [$parts[0], $parts[1], $parts[2], $parts[1]];
}
else {
$margin = $parts;
}
}
else {
$margin = [0, 0, 0, 0];
}
foreach (['fill', 'background-fill'] as $fill_name) {
if (isset($canvas->raw[$fill_name])) {
$canvas->fills[$fill_name] = $canvas->raw[$fill_name];
}
}
$canvas->margin = (object) [
'top' => $margin[0],
'right' => $margin[1],
'bottom' => $margin[2],
'left' => $margin[3],
];
$canvas->width = $canvas->raw['width'];
$canvas->height = $canvas->raw['height'];
}
function canvas_fetch_src($url_token) {
if ($url_token && $url = Crush::$process->tokens->get($url_token)) {
$file = $url->getAbsolutePath();
// Testing the image availability and getting info.
if ($info = @getimagesize($file)) {
$image = null;
// If image is available copy it.
switch ($info['mime']) {
case 'image/png':
$image = imagecreatefrompng($file);
break;
case 'image/jpg':
case 'image/jpeg':
$image = imagecreatefromjpeg($file);
break;
case 'image/gif':
$image = imagecreatefromgif($file);
break;
case 'image/webp':
$image = imagecreatefromwebp($file);
break;
}
if ($image) {
return (object) [
'file' => $file,
'info' => $info,
'width' => $info[0],
'height' => $info[1],
'image' => $image,
];
}
}
}
return false;
}
/*
Adapted from GD Gradient Fill by Ozh (http://planetozh.com):
http://planetozh.com/blog/my-projects/images-php-gd-gradient-fill
*/
function canvas_gradient($canvas, $fill) {
$image = $canvas->image;
// Resolve drawing direction.
if ($fill->direction === 'horizontal') {
$line_numbers = $fill->x2 - $fill->x1;
}
else {
$line_numbers = $fill->y2 - $fill->y1;
}
list($r1, $g1, $b1, $a1) = $fill->stops[0];
list($r2, $g2, $b2, $a2) = $fill->stops[1];
$r = $g = $b = $a = -1;
for ($line = 0; $line < $line_numbers; $line++) {
$last = "$r,$g,$b,$a";
$r = $r2 - $r1 ? intval($r1 + ($r2 - $r1) * ($line / $line_numbers)): $r1;
$g = $g2 - $g1 ? intval($g1 + ($g2 - $g1) * ($line / $line_numbers)): $g1;
$b = $b2 - $b1 ? intval($b1 + ($b2 - $b1) * ($line / $line_numbers)): $b1;
$a = $a2 - $a1 ? ($a1 + ($a2 - $a1) * ($line / $line_numbers)) : $a1;
$a = canvas_opacity($a);
if ($last != "$r,$g,$b,$a") {
$color = imagecolorallocatealpha($image, $r, $g, $b, $a);
}
switch($fill->direction) {
case 'horizontal':
imagefilledrectangle($image,
$fill->x1 + $line,
$fill->y1,
$fill->x1 + $line,
$fill->y2,
$color);
break;
case 'vertical':
default:
imagefilledrectangle($image,
$fill->x1,
$fill->y1 + $line,
$fill->x2,
$fill->y1 + $line,
$color);
break;
}
imagealphablending($image, true);
}
}
function canvas_create($canvas) {
$margin = $canvas->margin;
$width = $canvas->width + $margin->right + $margin->left;
$height = $canvas->height + $margin->top + $margin->bottom;
// Create image object.
$canvas->image = canvas_create_transparent($width, $height);
}
function canvas_create_transparent($width, $height) {
$image = imagecreatetruecolor($width, $height);
// Set transparent canvas background.
imagealphablending($image, false);
imagesavealpha($image, true);
imagefill($image, 0, 0, imagecolorallocatealpha($image, 0, 0, 0, 127));
return $image;
}
function canvas_fade($src, $opacity) {
$width = imagesx($src->image);
$height = imagesy($src->image);
$new_image = canvas_create_transparent($width, $height);
$opacity = canvas_opacity($opacity);
// Perform pixel-based alpha map application
for ($x = 0; $x < $width; $x++) {
for ($y = 0; $y < $height; $y++) {
$colors = imagecolorsforindex($src->image, imagecolorat($src->image, $x, $y));
imagesetpixel($new_image, $x, $y, imagecolorallocatealpha(
$new_image, $colors['red'], $colors['green'], $colors['blue'], $opacity));
}
}
imagedestroy($src->image);
$src->image = $new_image;
}
function canvas_fill($canvas, $property) {
if (! isset($canvas->fills[$property])) {
return false;
}
$fill = $canvas->fills[$property];
// Gradient fill.
if (is_object($fill)) {
canvas_gradient($canvas, $fill);
}
// Solid color fill.
elseif ($solid = Color::parse($fill)) {
list($r, $g, $b, $a) = $solid;
$color = imagecolorallocatealpha($canvas->image, $r, $g, $b, canvas_opacity($a));
$fill = new stdClass();
$canvas->currentProperty = $property;
canvas_set_fill_dims($fill, $canvas);
imagefilledrectangle($canvas->image, $fill->x1, $fill->y1, $fill->x2, $fill->y2, $color);
imagealphablending($canvas->image, true);
}
// Can't parse.
else {
return false;
}
}
function canvas_set_fill_dims($fill, $canvas) {
// Resolve fill dimensions and coordinates.
$margin = $canvas->margin;
$fill->x1 = 0;
$fill->y1 = 0;
$fill->x2 = $canvas->width + $margin->right + $margin->left;
$fill->y2 = $canvas->height + $margin->top + $margin->bottom;
if (isset($canvas->currentProperty) && $canvas->currentProperty === 'fill') {
$fill->x1 = $margin->left;
$fill->y1 = $margin->top;
$fill->x2 = $canvas->width + $fill->x1 - 1;
$fill->y2 = $canvas->height + $fill->y1 - 1;
}
}
function canvas_requirements() {
$requirements_met = true;
if (! extension_loaded('gd')) {
$requirements_met = false;
warning('GD extension not available.');
}
else {
$gd_info = implode('|', array_keys(array_filter(gd_info())));
foreach (['jpe?g' => 'JPG', 'png' => 'PNG'] as $file_ext_patt => $file_ext) {
if (! preg_match("~\b(?<ext>$file_ext_patt) support\b~i", $gd_info)) {
$requirements_met = false;
warning("GD extension has no $file_ext support.");
}
}
}
return $requirements_met;
}
/*
Canvas object.
*/
class Canvas
{
public $image, $fills = [], $filters = [];
public function __destruct()
{
if (isset($this->image)) {
imagedestroy($this->image);
}
}
}
/*
Helpers.
*/
function canvas_opacity($float) {
return 127 - max(min(round($float * 127), 127), 0);
}
function canvas_parselist($str, $numbers = true) {
$list = preg_split('~ +~', trim($str));
return $numbers ? array_map('floatval', $list) : $list;
}

View file

@ -0,0 +1,65 @@
<?php
/**
* Expanded easing keywords for transitions
*
* @see docs/plugins/ease.md
*/
namespace CssCrush;
\csscrush_plugin('ease', function ($process) {
$process->on('rule_prealias', 'CssCrush\ease');
});
function ease(Rule $rule) {
static $find, $replace, $easing_properties;
if (! $find) {
$easings = [
'ease-in-out-back' => 'cubic-bezier(.680,-0.550,.265,1.550)',
'ease-in-out-circ' => 'cubic-bezier(.785,.135,.150,.860)',
'ease-in-out-expo' => 'cubic-bezier(1,0,0,1)',
'ease-in-out-sine' => 'cubic-bezier(.445,.050,.550,.950)',
'ease-in-out-quint' => 'cubic-bezier(.860,0,.070,1)',
'ease-in-out-quart' => 'cubic-bezier(.770,0,.175,1)',
'ease-in-out-cubic' => 'cubic-bezier(.645,.045,.355,1)',
'ease-in-out-quad' => 'cubic-bezier(.455,.030,.515,.955)',
'ease-out-back' => 'cubic-bezier(.175,.885,.320,1.275)',
'ease-out-circ' => 'cubic-bezier(.075,.820,.165,1)',
'ease-out-expo' => 'cubic-bezier(.190,1,.220,1)',
'ease-out-sine' => 'cubic-bezier(.390,.575,.565,1)',
'ease-out-quint' => 'cubic-bezier(.230,1,.320,1)',
'ease-out-quart' => 'cubic-bezier(.165,.840,.440,1)',
'ease-out-cubic' => 'cubic-bezier(.215,.610,.355,1)',
'ease-out-quad' => 'cubic-bezier(.250,.460,.450,.940)',
'ease-in-back' => 'cubic-bezier(.600,-0.280,.735,.045)',
'ease-in-circ' => 'cubic-bezier(.600,.040,.980,.335)',
'ease-in-expo' => 'cubic-bezier(.950,.050,.795,.035)',
'ease-in-sine' => 'cubic-bezier(.470,0,.745,.715)',
'ease-in-quint' => 'cubic-bezier(.755,.050,.855,.060)',
'ease-in-quart' => 'cubic-bezier(.895,.030,.685,.220)',
'ease-in-cubic' => 'cubic-bezier(.550,.055,.675,.190)',
'ease-in-quad' => 'cubic-bezier(.550,.085,.680,.530)',
];
$easing_properties = [
'transition' => true,
'transition-timing-function' => true,
];
foreach ($easings as $property => $value) {
$patt = Regex::make("~{{ LB }}$property{{ RB }}~i");
$find[] = $patt;
$replace[] = $value;
}
}
if (! array_intersect_key($rule->declarations->canonicalProperties, $easing_properties)) {
return;
}
foreach ($rule->declarations->filter(['skip' => false]) as $declaration) {
if (isset($easing_properties[$declaration->canonicalProperty])) {
$declaration->value = preg_replace($find, $replace, $declaration->value);
}
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* Pseudo classes for working with forms
*
* @see docs/plugins/forms.md
*/
namespace CssCrush;
\csscrush_plugin('forms', function ($process) {
foreach (forms() as $name => $handler) {
if (is_array($handler)) {
$type = $handler['type'];
$handler = $handler['handler'];
}
$process->addSelectorAlias($name, $handler, $type);
}
});
function forms() {
return [
'input' => [
'type' => 'splat',
'handler' => 'input[type=#(text)]',
],
'checkbox' => 'input[type="checkbox"]',
'radio' => 'input[type="radio"]',
'file' => 'input[type="file"]',
'image' => 'input[type="image"]',
'password' => 'input[type="password"]',
'submit' => 'input[type="submit"]',
'text' => 'input[type="text"]',
];
}

View file

@ -0,0 +1,10 @@
<?php
/**
* :hover/:focus and :hover/:focus/:active composite pseudo classes
*
* @see docs/plugins/hocus-pocus.md
*/
csscrush_plugin('hocus-pocus', function ($process) {
$process->addSelectorAlias('hocus', ':any(:hover,:focus)');
$process->addSelectorAlias('pocus', ':any(:hover,:focus,:active)');
});

View file

@ -0,0 +1,160 @@
<?php
/**
* Customizable property sorting
*
* @see docs/plugins/property-sorter.md
*/
namespace CssCrush {
\csscrush_plugin('property-sorter', function ($process) {
$process->on('rule_prealias', 'CssCrush\property_sorter');
});
function property_sorter(Rule $rule) {
usort($rule->declarations->store, 'CssCrush\property_sorter_callback');
}
/*
Callback for sorting.
*/
function property_sorter_callback($a, $b) {
$map =& property_sorter_get_table();
$a_prop =& $a->canonicalProperty;
$b_prop =& $b->canonicalProperty;
$a_listed = isset($map[$a_prop]);
$b_listed = isset($map[$b_prop]);
// If the properties are identical we need to flag for an index comparison.
$compare_indexes = false;
// If the 'canonical' properties are identical we need to flag for a vendor comparison.
$compare_vendor = false;
// If both properties are listed.
if ($a_listed && $b_listed) {
if ($a_prop === $b_prop) {
if ($a->vendor || $b->vendor) {
$compare_vendor = true;
}
else {
$compare_indexes = true;
}
}
else {
// Table comparison.
return $map[$a_prop] > $map[$b_prop] ? 1 : -1;
}
}
// If one property is listed it always takes higher priority.
elseif ($a_listed && ! $b_listed) {
return -1;
}
elseif ($b_listed && ! $a_listed) {
return 1;
}
// If neither property is listed.
else {
if ($a_prop === $b_prop) {
if ($a->vendor || $b->vendor) {
$compare_vendor = true;
}
else {
$compare_indexes = true;
}
}
else {
// Regular sort.
return $a_prop > $b_prop ? 1 : -1;
}
}
// Comparing by index.
if ($compare_indexes ) {
return $a->index > $b->index ? 1 : -1;
}
// Comparing by vendor mark.
if ($compare_vendor) {
if (! $a->vendor && $b->vendor) {
return 1;
}
elseif ($a->vendor && ! $b->vendor) {
return -1;
}
else {
// If both have a vendor mark compare vendor name length.
return strlen($b->vendor) > strlen($a->vendor) ? 1 : -1;
}
}
}
/*
Cache for the table of values to compare against.
*/
function &property_sorter_get_table () {
// Check for cached table.
if (isset($GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER_CACHE'])) {
return $GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER_CACHE'];
}
$table = [];
// Nothing cached, check for a user-defined table.
if (isset($GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER'])) {
$table = (array) $GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER'];
}
// No user-defined table, use pre-defined.
else {
// Load from property-sorting.ini.
$sorting_file_contents = file_get_contents(Crush::$dir . '/misc/property-sorting.ini');
if ($sorting_file_contents !== false) {
$sorting_file_contents = preg_replace('~;[^\r\n]*~', '', $sorting_file_contents);
$table = preg_split('~\s+~', trim($sorting_file_contents));
}
else {
notice("Property sorting file not found.");
}
// Store to the global variable.
$GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER'] = $table;
}
// Cache the table (and flip it).
$GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER_CACHE'] = array_flip($table);
return $GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER_CACHE'];
}
}
namespace {
/*
Get the current sorting table.
*/
function csscrush_get_property_sort_order() {
CssCrush\property_sorter_get_table();
return $GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER'];
}
/*
Set a custom sorting table.
*/
function csscrush_set_property_sort_order(array $new_order) {
unset($GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER_CACHE']);
$GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER'] = $new_order;
}
}

File diff suppressed because it is too large Load diff