From c7e0fac35a50170bb5a5b4f322ff00a4a0f2dad9 Mon Sep 17 00:00:00 2001 From: Buchholz Date: Tue, 19 Oct 2021 15:27:42 +0200 Subject: [PATCH] Css-Crush preprocessor --- include/thirdparty/css-crush/CssCrush.php | 19 + include/thirdparty/css-crush/LICENSE.txt | 19 + include/thirdparty/css-crush/README.md | 97 ++ include/thirdparty/css-crush/aliases.ini | 248 ++++ include/thirdparty/css-crush/bin/csscrush | 8 + include/thirdparty/css-crush/cli.php | 637 ++++++++++ include/thirdparty/css-crush/docs/README.md | 3 + .../css-crush/docs/api/functions.md | 84 ++ .../thirdparty/css-crush/docs/api/options.md | 103 ++ .../css-crush/docs/core/abstract.md | 44 + .../css-crush/docs/core/auto-prefixing.md | 39 + .../css-crush/docs/core/direct-import.md | 25 + .../css-crush/docs/core/fragments.md | 25 + .../css-crush/docs/core/functions/a-adjust.md | 26 + .../css-crush/docs/core/functions/data-uri.md | 33 + .../css-crush/docs/core/functions/h-adjust.md | 24 + .../docs/core/functions/hsl-adjust.md | 27 + .../docs/core/functions/hsla-adjust.md | 27 + .../css-crush/docs/core/functions/l-adjust.md | 24 + .../css-crush/docs/core/functions/math.md | 19 + .../css-crush/docs/core/functions/query.md | 54 + .../css-crush/docs/core/functions/s-adjust.md | 25 + .../css-crush/docs/core/functions/this.md | 39 + .../css-crush/docs/core/inheritance.md | 127 ++ .../thirdparty/css-crush/docs/core/loop.md | 41 + .../thirdparty/css-crush/docs/core/mixins.md | 95 ++ .../thirdparty/css-crush/docs/core/nesting.md | 49 + .../css-crush/docs/core/selector-aliases.md | 80 ++ .../css-crush/docs/core/selector-grouping.md | 22 + .../css-crush/docs/core/variables.md | 62 + .../css-crush/docs/getting-started/js.md | 26 + .../css-crush/docs/getting-started/php.md | 17 + .../thirdparty/css-crush/docs/plugins/aria.md | 21 + .../css-crush/docs/plugins/canvas.md | 55 + .../thirdparty/css-crush/docs/plugins/ease.md | 37 + .../css-crush/docs/plugins/forms.md | 16 + .../css-crush/docs/plugins/hocus-pocus.md | 12 + .../css-crush/docs/plugins/property-sorter.md | 21 + .../css-crush/docs/plugins/svg-gradients.md | 48 + .../thirdparty/css-crush/docs/plugins/svg.md | 74 ++ include/thirdparty/css-crush/index.js | 153 +++ .../css-crush/lib/CssCrush/BalancedMatch.php | 66 + .../css-crush/lib/CssCrush/Collection.php | 74 ++ .../css-crush/lib/CssCrush/Color.php | 472 +++++++ .../css-crush/lib/CssCrush/Crush.php | 289 +++++ .../css-crush/lib/CssCrush/Declaration.php | 135 ++ .../lib/CssCrush/DeclarationList.php | 598 +++++++++ .../css-crush/lib/CssCrush/EventEmitter.php | 36 + .../css-crush/lib/CssCrush/ExtendArg.php | 37 + .../css-crush/lib/CssCrush/File.php | 45 + .../css-crush/lib/CssCrush/Fragment.php | 46 + .../css-crush/lib/CssCrush/Functions.php | 324 +++++ .../thirdparty/css-crush/lib/CssCrush/IO.php | 228 ++++ .../css-crush/lib/CssCrush/IO/Watch.php | 48 + .../css-crush/lib/CssCrush/Importer.php | 378 ++++++ .../css-crush/lib/CssCrush/Iterator.php | 70 ++ .../css-crush/lib/CssCrush/Logger.php | 149 +++ .../css-crush/lib/CssCrush/Mixin.php | 106 ++ .../css-crush/lib/CssCrush/Options.php | 176 +++ .../css-crush/lib/CssCrush/PostAliasFix.php | 26 + .../css-crush/lib/CssCrush/Process.php | 1102 ++++++++++++++++ .../css-crush/lib/CssCrush/Regex.php | 112 ++ .../css-crush/lib/CssCrush/Rule.php | 145 +++ .../css-crush/lib/CssCrush/Selector.php | 105 ++ .../css-crush/lib/CssCrush/SelectorAlias.php | 63 + .../css-crush/lib/CssCrush/SelectorList.php | 136 ++ .../css-crush/lib/CssCrush/StringObject.php | 156 +++ .../css-crush/lib/CssCrush/Template.php | 147 +++ .../css-crush/lib/CssCrush/Tokens.php | 161 +++ .../thirdparty/css-crush/lib/CssCrush/Url.php | 214 ++++ .../css-crush/lib/CssCrush/Util.php | 277 +++++ .../css-crush/lib/CssCrush/Version.php | 116 ++ .../thirdparty/css-crush/lib/functions.php | 167 +++ .../css-crush/misc/color-keywords.ini | 151 +++ .../thirdparty/css-crush/misc/formatters.php | 48 + .../css-crush/misc/property-sorting.ini | 176 +++ include/thirdparty/css-crush/plugins/aria.php | 70 ++ .../thirdparty/css-crush/plugins/canvas.php | 652 ++++++++++ include/thirdparty/css-crush/plugins/ease.php | 65 + .../thirdparty/css-crush/plugins/forms.php | 33 + .../css-crush/plugins/hocus-pocus.php | 10 + .../css-crush/plugins/property-sorter.php | 160 +++ include/thirdparty/css-crush/plugins/svg.php | 1105 +++++++++++++++++ 83 files changed, 11279 insertions(+) create mode 100644 include/thirdparty/css-crush/CssCrush.php create mode 100644 include/thirdparty/css-crush/LICENSE.txt create mode 100644 include/thirdparty/css-crush/README.md create mode 100644 include/thirdparty/css-crush/aliases.ini create mode 100644 include/thirdparty/css-crush/bin/csscrush create mode 100644 include/thirdparty/css-crush/cli.php create mode 100644 include/thirdparty/css-crush/docs/README.md create mode 100644 include/thirdparty/css-crush/docs/api/functions.md create mode 100644 include/thirdparty/css-crush/docs/api/options.md create mode 100644 include/thirdparty/css-crush/docs/core/abstract.md create mode 100644 include/thirdparty/css-crush/docs/core/auto-prefixing.md create mode 100644 include/thirdparty/css-crush/docs/core/direct-import.md create mode 100644 include/thirdparty/css-crush/docs/core/fragments.md create mode 100644 include/thirdparty/css-crush/docs/core/functions/a-adjust.md create mode 100644 include/thirdparty/css-crush/docs/core/functions/data-uri.md create mode 100644 include/thirdparty/css-crush/docs/core/functions/h-adjust.md create mode 100644 include/thirdparty/css-crush/docs/core/functions/hsl-adjust.md create mode 100644 include/thirdparty/css-crush/docs/core/functions/hsla-adjust.md create mode 100644 include/thirdparty/css-crush/docs/core/functions/l-adjust.md create mode 100644 include/thirdparty/css-crush/docs/core/functions/math.md create mode 100644 include/thirdparty/css-crush/docs/core/functions/query.md create mode 100644 include/thirdparty/css-crush/docs/core/functions/s-adjust.md create mode 100644 include/thirdparty/css-crush/docs/core/functions/this.md create mode 100644 include/thirdparty/css-crush/docs/core/inheritance.md create mode 100644 include/thirdparty/css-crush/docs/core/loop.md create mode 100644 include/thirdparty/css-crush/docs/core/mixins.md create mode 100644 include/thirdparty/css-crush/docs/core/nesting.md create mode 100644 include/thirdparty/css-crush/docs/core/selector-aliases.md create mode 100644 include/thirdparty/css-crush/docs/core/selector-grouping.md create mode 100644 include/thirdparty/css-crush/docs/core/variables.md create mode 100644 include/thirdparty/css-crush/docs/getting-started/js.md create mode 100644 include/thirdparty/css-crush/docs/getting-started/php.md create mode 100644 include/thirdparty/css-crush/docs/plugins/aria.md create mode 100644 include/thirdparty/css-crush/docs/plugins/canvas.md create mode 100644 include/thirdparty/css-crush/docs/plugins/ease.md create mode 100644 include/thirdparty/css-crush/docs/plugins/forms.md create mode 100644 include/thirdparty/css-crush/docs/plugins/hocus-pocus.md create mode 100644 include/thirdparty/css-crush/docs/plugins/property-sorter.md create mode 100644 include/thirdparty/css-crush/docs/plugins/svg-gradients.md create mode 100644 include/thirdparty/css-crush/docs/plugins/svg.md create mode 100644 include/thirdparty/css-crush/index.js create mode 100644 include/thirdparty/css-crush/lib/CssCrush/BalancedMatch.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Collection.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Color.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Crush.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Declaration.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/DeclarationList.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/EventEmitter.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/ExtendArg.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/File.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Fragment.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Functions.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/IO.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/IO/Watch.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Importer.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Iterator.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Logger.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Mixin.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Options.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/PostAliasFix.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Process.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Regex.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Rule.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Selector.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/SelectorAlias.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/SelectorList.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/StringObject.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Template.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Tokens.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Url.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Util.php create mode 100644 include/thirdparty/css-crush/lib/CssCrush/Version.php create mode 100644 include/thirdparty/css-crush/lib/functions.php create mode 100644 include/thirdparty/css-crush/misc/color-keywords.ini create mode 100644 include/thirdparty/css-crush/misc/formatters.php create mode 100644 include/thirdparty/css-crush/misc/property-sorting.ini create mode 100644 include/thirdparty/css-crush/plugins/aria.php create mode 100644 include/thirdparty/css-crush/plugins/canvas.php create mode 100644 include/thirdparty/css-crush/plugins/ease.php create mode 100644 include/thirdparty/css-crush/plugins/forms.php create mode 100644 include/thirdparty/css-crush/plugins/hocus-pocus.php create mode 100644 include/thirdparty/css-crush/plugins/property-sorter.php create mode 100644 include/thirdparty/css-crush/plugins/svg.php diff --git a/include/thirdparty/css-crush/CssCrush.php b/include/thirdparty/css-crush/CssCrush.php new file mode 100644 index 0000000..94e1497 --- /dev/null +++ b/include/thirdparty/css-crush/CssCrush.php @@ -0,0 +1,19 @@ + + +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 + +``` + +## Basic usage (PHP) + +```php + +``` + +Compiles the CSS file and outputs the following link tag: + +```html + +``` + +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 diff --git a/include/thirdparty/css-crush/aliases.ini b/include/thirdparty/css-crush/aliases.ini new file mode 100644 index 0000000..8e797d1 --- /dev/null +++ b/include/thirdparty/css-crush/aliases.ini @@ -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 , + ; and . These properties are available in both 2012 implementations via + ; 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 diff --git a/include/thirdparty/css-crush/bin/csscrush b/include/thirdparty/css-crush/bin/csscrush new file mode 100644 index 0000000..559fb84 --- /dev/null +++ b/include/thirdparty/css-crush/bin/csscrush @@ -0,0 +1,8 @@ +#!/usr/bin/env php +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 = [ + '' => "\033[0;30m", + '' => "\033[0;31m", + '' => "\033[0;32m", + '' => "\033[0;33m", + '' => "\033[0;34m", + '' => "\033[0;35m", + '' => "\033[0;36m", + '' => "\033[0;37m", + + '' => "\033[1;30m", + '' => "\033[1;31m", + '' => "\033[1;32m", + '' => "\033[1;33m", + '' => "\033[1;34m", + '' => "\033[1;35m", + '' => "\033[1;36m", + '' => "\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 = <<USAGE: + csscrush [OPTIONS] [input-file] [output-file] + +OPTIONS: + -i, --input + Input file. If omitted takes input from STDIN. + + -o, --output + Output file. If omitted prints to STDOUT. + + -p, --pretty + Formatted, un-minified output. + + -w, --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. + + -E, --plugins + List of plugins (comma separated) to enable. + + --boilerplate + Whether or not to output a boilerplate. Optionally accepts filepath + to a custom boilerplate template. + + --context + Filepath context for resolving relative import URLs. + Only meaningful when taking raw input from STDIN. + + --import-path + Comma separated list of additional paths to search when resolving + relative import URLs. + + --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. + + --help + Display this help message. + + --newlines + Force newline style on output css. Defaults to the current platform + newline. Possible values: 'windows' (or 'win'), 'unix', 'use-platform'. + + --source-map + Create a source map file (compliant with the Source Map v3 proposal). + + --stats + Display post-compile stats. + + --vars + Map of variable names in an http query string format. + + --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). + + --version + Display version number. + +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); +} diff --git a/include/thirdparty/css-crush/docs/README.md b/include/thirdparty/css-crush/docs/README.md new file mode 100644 index 0000000..9c284db --- /dev/null +++ b/include/thirdparty/css-crush/docs/README.md @@ -0,0 +1,3 @@ +# CSS-Crush Documentation + +Rendered online at http://the-echoplex.net/csscrush diff --git a/include/thirdparty/css-crush/docs/api/functions.md b/include/thirdparty/css-crush/docs/api/functions.md new file mode 100644 index 0000000..639f9fb --- /dev/null +++ b/include/thirdparty/css-crush/docs/api/functions.md @@ -0,0 +1,84 @@ + + +## csscrush_file() + +Process CSS file and return the compiled file URL. + +csscrush_file( string $file [, array [$options](#api--options) ] ) + + +*************** + +## csscrush_tag() + +Process CSS file and return an html `link` tag with populated href. + +csscrush_tag( string $file [, array [$options](#api--options) [, array $tag\_attributes ]] ) + + +*************** + +## csscrush_inline() + +Process CSS file and return CSS as text wrapped in html `style` tags. + +csscrush_inline( string $file [, array [$options](#api--options) [, array $tag\_attributes ]] ) + + +*************** + +## csscrush_string() + +Compile a raw string of CSS string and return it. + +csscrush_string( string $string [, array [$options](#api--options) ] ) + + +*************** + +## 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()` diff --git a/include/thirdparty/css-crush/docs/api/options.md b/include/thirdparty/css-crush/docs/api/options.md new file mode 100644 index 0000000..1aa3fb2 --- /dev/null +++ b/include/thirdparty/css-crush/docs/api/options.md @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Option + Values (default in bold) + Description +
minify + true | false | Array + 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. +
formatter + block | single-line | padded + Set the formatting mode. Overrides minify option if both are set. +
newlines + use-platform | windows/win | unix + Set the output style of newlines +
boilerplate + true | false | Path + Prepend a boilerplate to the output file +
versioning + true | false + Append a timestamped querystring to the output filename +
vars + Array + An associative array of CSS variables to be applied at runtime. These will override variables declared globally or in the CSS. +
cache + true | false + Turn caching on or off. +
output_dir + Path + Specify an output directory for compiled files. Defaults to the same directory as the host file. +
output_file + Output filename + Specify an output filename (suffix is added). +
asset_dir + Path + Directory for SVG and image files generated by plugins (defaults to the main file output directory). +
stat_dump + false | true | Path + Save compile stats and variables to a file in json format. +
vendor_target + "all" | "moz", "webkit", ... | Array +Limit aliasing to a specific vendor, or an array of vendors. +
rewrite_import_urls + true | false | "absolute" + Rewrite relative URLs inside inlined imported files. +
import_paths + Array + Additional paths to search when resolving relative import URLs. +
plugins + Array + An array of plugin names to enable. +
source_map + true | false + Output a source map (compliant with the Source Map v3 proposal). +
context + Path + Context for importing resources from relative urls (Only applies to `csscrush_string()` and command line utility). +
doc_root + Path + Specify an alternative server document root for situations where the CSS is being served behind an alias or url rewritten path. +
diff --git a/include/thirdparty/css-crush/docs/core/abstract.md b/include/thirdparty/css-crush/docs/core/abstract.md new file mode 100644 index 0000000..4936aa8 --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/abstract.md @@ -0,0 +1,44 @@ + + +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; +} +``` diff --git a/include/thirdparty/css-crush/docs/core/auto-prefixing.md b/include/thirdparty/css-crush/docs/core/auto-prefixing.md new file mode 100644 index 0000000..02c67b6 --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/auto-prefixing.md @@ -0,0 +1,39 @@ + + +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);} +} +``` diff --git a/include/thirdparty/css-crush/docs/core/direct-import.md b/include/thirdparty/css-crush/docs/core/direct-import.md new file mode 100644 index 0000000..fea9d69 --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/direct-import.md @@ -0,0 +1,25 @@ + + +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 */ +} +``` diff --git a/include/thirdparty/css-crush/docs/core/fragments.md b/include/thirdparty/css-crush/docs/core/fragments.md new file mode 100644 index 0000000..2d3d693 --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/fragments.md @@ -0,0 +1,25 @@ + + +Fragments – defined and invoked with the @fragment 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; } +``` diff --git a/include/thirdparty/css-crush/docs/core/functions/a-adjust.md b/include/thirdparty/css-crush/docs/core/functions/a-adjust.md new file mode 100644 index 0000000..59d2c93 --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/functions/a-adjust.md @@ -0,0 +1,26 @@ + + +Manipulate the opacity (alpha channel) of a color value. + +a-adjust( *color*, *offset* ) + +## 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 ); +``` diff --git a/include/thirdparty/css-crush/docs/core/functions/data-uri.md b/include/thirdparty/css-crush/docs/core/functions/data-uri.md new file mode 100644 index 0000000..32b0459 --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/functions/data-uri.md @@ -0,0 +1,33 @@ + + +Create a data-uri. + +data-uri( *url* ) + +## 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:); +``` diff --git a/include/thirdparty/css-crush/docs/core/functions/h-adjust.md b/include/thirdparty/css-crush/docs/core/functions/h-adjust.md new file mode 100644 index 0000000..98605c9 --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/functions/h-adjust.md @@ -0,0 +1,24 @@ + + +Adjust the hue of a color value. + +h-adjust( *color*, *offset* ) + +## 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 ); +``` diff --git a/include/thirdparty/css-crush/docs/core/functions/hsl-adjust.md b/include/thirdparty/css-crush/docs/core/functions/hsl-adjust.md new file mode 100644 index 0000000..bdee0fd --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/functions/hsl-adjust.md @@ -0,0 +1,27 @@ + + +Manipulate the hue, saturation and lightness of a color value + +hsl-adjust( *color*, *hue-offset*, *saturation-offset*, *lightness-offset* ) + +## 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 ); +``` diff --git a/include/thirdparty/css-crush/docs/core/functions/hsla-adjust.md b/include/thirdparty/css-crush/docs/core/functions/hsla-adjust.md new file mode 100644 index 0000000..cc05b4b --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/functions/hsla-adjust.md @@ -0,0 +1,27 @@ + + +Manipulate the hue, saturation, lightness and opacity of a color value. + +hsla-adjust( *color*, *hue-offset*, *saturation-offset*, *lightness-offset*, *alpha-offset* ) + +## 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 ); +``` \ No newline at end of file diff --git a/include/thirdparty/css-crush/docs/core/functions/l-adjust.md b/include/thirdparty/css-crush/docs/core/functions/l-adjust.md new file mode 100644 index 0000000..28ac701 --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/functions/l-adjust.md @@ -0,0 +1,24 @@ + + +Adjust the lightness of a color value. + +l-adjust( *color*, *offset* ) + +## 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 ); +``` diff --git a/include/thirdparty/css-crush/docs/core/functions/math.md b/include/thirdparty/css-crush/docs/core/functions/math.md new file mode 100644 index 0000000..2d65b40 --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/functions/math.md @@ -0,0 +1,19 @@ + + +Evaluate a raw mathematical expression. + +math( *expression* [, *unit*] ) + +## Examples + +```crush +font-size: math( 12 / 16, em ); +``` + +```css +font-size: 0.75em; +``` diff --git a/include/thirdparty/css-crush/docs/core/functions/query.md b/include/thirdparty/css-crush/docs/core/functions/query.md new file mode 100644 index 0000000..c9d5240 --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/functions/query.md @@ -0,0 +1,54 @@ + + +Copy a value from another rule. + +query( *target* [, *property-name* = default] [, *fallback*] ) + +## 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 */ + } + } + } +} +``` diff --git a/include/thirdparty/css-crush/docs/core/functions/s-adjust.md b/include/thirdparty/css-crush/docs/core/functions/s-adjust.md new file mode 100644 index 0000000..8d2cce1 --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/functions/s-adjust.md @@ -0,0 +1,25 @@ + + +Adjust the saturation of a color value. + +s-adjust( *color*, *offset* ) + +## 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 ); +``` diff --git a/include/thirdparty/css-crush/docs/core/functions/this.md b/include/thirdparty/css-crush/docs/core/functions/this.md new file mode 100644 index 0000000..12029b6 --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/functions/this.md @@ -0,0 +1,39 @@ + + +Reference another property value from the same containing block. + +Restricted to referencing properties that don't already reference other properties. + +this( *property-name*, *fallback* ) + +## 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 ); +} +``` diff --git a/include/thirdparty/css-crush/docs/core/inheritance.md b/include/thirdparty/css-crush/docs/core/inheritance.md new file mode 100644 index 0000000..4473813 --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/inheritance.md @@ -0,0 +1,127 @@ + + +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; +} +``` + diff --git a/include/thirdparty/css-crush/docs/core/loop.md b/include/thirdparty/css-crush/docs/core/loop.md new file mode 100644 index 0000000..66ae6f7 --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/loop.md @@ -0,0 +1,41 @@ + + +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%; } +``` diff --git a/include/thirdparty/css-crush/docs/core/mixins.md b/include/thirdparty/css-crush/docs/core/mixins.md new file mode 100644 index 0000000..1348ff8 --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/mixins.md @@ -0,0 +1,95 @@ + + +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; +} +``` diff --git a/include/thirdparty/css-crush/docs/core/nesting.md b/include/thirdparty/css-crush/docs/core/nesting.md new file mode 100644 index 0000000..999be4f --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/nesting.md @@ -0,0 +1,49 @@ + + +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%; +} +``` diff --git a/include/thirdparty/css-crush/docs/core/selector-aliases.md b/include/thirdparty/css-crush/docs/core/selector-aliases.md new file mode 100644 index 0000000..ea9296d --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/selector-aliases.md @@ -0,0 +1,80 @@ + + +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; +} +``` diff --git a/include/thirdparty/css-crush/docs/core/selector-grouping.md b/include/thirdparty/css-crush/docs/core/selector-grouping.md new file mode 100644 index 0000000..0cafa1f --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/selector-grouping.md @@ -0,0 +1,22 @@ + + +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; +} +``` diff --git a/include/thirdparty/css-crush/docs/core/variables.md b/include/thirdparty/css-crush/docs/core/variables.md new file mode 100644 index 0000000..117f53f --- /dev/null +++ b/include/thirdparty/css-crush/docs/core/variables.md @@ -0,0 +1,62 @@ + + +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; + } +} +``` diff --git a/include/thirdparty/css-crush/docs/getting-started/js.md b/include/thirdparty/css-crush/docs/getting-started/js.md new file mode 100644 index 0000000..605727f --- /dev/null +++ b/include/thirdparty/css-crush/docs/getting-started/js.md @@ -0,0 +1,26 @@ + + +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'); +``` diff --git a/include/thirdparty/css-crush/docs/getting-started/php.md b/include/thirdparty/css-crush/docs/getting-started/php.md new file mode 100644 index 0000000..1c6f836 --- /dev/null +++ b/include/thirdparty/css-crush/docs/getting-started/php.md @@ -0,0 +1,17 @@ + + +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 + +``` diff --git a/include/thirdparty/css-crush/docs/plugins/aria.md b/include/thirdparty/css-crush/docs/plugins/aria.md new file mode 100644 index 0000000..c00608d --- /dev/null +++ b/include/thirdparty/css-crush/docs/plugins/aria.md @@ -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"] {...} +```` diff --git a/include/thirdparty/css-crush/docs/plugins/canvas.md b/include/thirdparty/css-crush/docs/plugins/canvas.md new file mode 100644 index 0000000..df6994d --- /dev/null +++ b/include/thirdparty/css-crush/docs/plugins/canvas.md @@ -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); +} +``` diff --git a/include/thirdparty/css-crush/docs/plugins/ease.md b/include/thirdparty/css-crush/docs/plugins/ease.md new file mode 100644 index 0000000..37ce5f9 --- /dev/null +++ b/include/thirdparty/css-crush/docs/plugins/ease.md @@ -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); +``` diff --git a/include/thirdparty/css-crush/docs/plugins/forms.md b/include/thirdparty/css-crush/docs/plugins/forms.md new file mode 100644 index 0000000..3f4fcbc --- /dev/null +++ b/include/thirdparty/css-crush/docs/plugins/forms.md @@ -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"] {...} +``` diff --git a/include/thirdparty/css-crush/docs/plugins/hocus-pocus.md b/include/thirdparty/css-crush/docs/plugins/hocus-pocus.md new file mode 100644 index 0000000..b8f3efa --- /dev/null +++ b/include/thirdparty/css-crush/docs/plugins/hocus-pocus.md @@ -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; } +``` diff --git a/include/thirdparty/css-crush/docs/plugins/property-sorter.md b/include/thirdparty/css-crush/docs/plugins/property-sorter.md new file mode 100644 index 0000000..cd17817 --- /dev/null +++ b/include/thirdparty/css-crush/docs/plugins/property-sorter.md @@ -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; +``` diff --git a/include/thirdparty/css-crush/docs/plugins/svg-gradients.md b/include/thirdparty/css-crush/docs/plugins/svg-gradients.md new file mode 100644 index 0000000..66ff410 --- /dev/null +++ b/include/thirdparty/css-crush/docs/plugins/svg-gradients.md @@ -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( [ | to ,]? [, ]+ ) +``` + +### 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( [ | at ,]? [, ]+ ) +``` + +### 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) ); +``` diff --git a/include/thirdparty/css-crush/docs/plugins/svg.md b/include/thirdparty/css-crush/docs/plugins/svg.md new file mode 100644 index 0000000..facec0d --- /dev/null +++ b/include/thirdparty/css-crush/docs/plugins/svg.md @@ -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. + diff --git a/include/thirdparty/css-crush/index.js b/include/thirdparty/css-crush/index.js new file mode 100644 index 0000000..925f6c5 --- /dev/null +++ b/include/thirdparty/css-crush/index.js @@ -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); +}; diff --git a/include/thirdparty/css-crush/lib/CssCrush/BalancedMatch.php b/include/thirdparty/css-crush/lib/CssCrush/BalancedMatch.php new file mode 100644 index 0000000..e7ac516 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/BalancedMatch.php @@ -0,0 +1,66 @@ +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); + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Collection.php b/include/thirdparty/css-crush/lib/CssCrush/Collection.php new file mode 100644 index 0000000..d811e0f --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Collection.php @@ -0,0 +1,74 @@ +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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Color.php b/include/thirdparty/css-crush/lib/CssCrush/Color.php new file mode 100644 index 0000000..048ddda --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Color.php @@ -0,0 +1,472 @@ + $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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Crush.php b/include/thirdparty/css-crush/lib/CssCrush/Crush.php new file mode 100644 index 0000000..f3b103e --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Crush.php @@ -0,0 +1,289 @@ +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[] = '
' . htmlspecialchars($item) . '
'; + } + echo implode('
', $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(); diff --git a/include/thirdparty/css-crush/lib/CssCrush/Declaration.php b/include/thirdparty/css-crush/lib/CssCrush/Declaration.php new file mode 100644 index 0000000..34cdf18 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Declaration.php @@ -0,0 +1,135 @@ +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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/DeclarationList.php b/include/thirdparty/css-crush/lib/CssCrush/DeclarationList.php new file mode 100644 index 0000000..c61c649 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/DeclarationList.php @@ -0,0 +1,598 @@ +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]]; + } + } + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/EventEmitter.php b/include/thirdparty/css-crush/lib/CssCrush/EventEmitter.php new file mode 100644 index 0000000..2ab4452 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/EventEmitter.php @@ -0,0 +1,36 @@ +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); + } + } + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/ExtendArg.php b/include/thirdparty/css-crush/lib/CssCrush/ExtendArg.php new file mode 100644 index 0000000..eef511e --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/ExtendArg.php @@ -0,0 +1,37 @@ +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]; + } + } + } + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/File.php b/include/thirdparty/css-crush/lib/CssCrush/File.php new file mode 100644 index 0000000..28403d8 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/File.php @@ -0,0 +1,45 @@ +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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Fragment.php b/include/thirdparty/css-crush/lib/CssCrush/Fragment.php new file mode 100644 index 0000000..0f4b6e3 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Fragment.php @@ -0,0 +1,46 @@ +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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Functions.php b/include/thirdparty/css-crush/lib/CssCrush/Functions.php new file mode 100644 index 0000000..999937c --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Functions.php @@ -0,0 +1,324 @@ + '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 }}-?(?' . implode('|', $idents) . ')'; + } + if ($nonIdents) { + $nonIdents = '(?' . 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}(?[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; +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/IO.php b/include/thirdparty/css-crush/lib/CssCrush/IO.php new file mode 100644 index 0000000..cba1611 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/IO.php @@ -0,0 +1,228 @@ +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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/IO/Watch.php b/include/thirdparty/css-crush/lib/CssCrush/IO/Watch.php new file mode 100644 index 0000000..1604ecd --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/IO/Watch.php @@ -0,0 +1,48 @@ +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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Importer.php b/include/thirdparty/css-crush/lib/CssCrush/Importer.php new file mode 100644 index 0000000..bed7a23 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Importer.php @@ -0,0 +1,378 @@ +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('~(?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('~ + (?:^|(?<=[;{}])) + (? + (?: \s | {{c_token}} )* + ) + (? + (?: + # 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); + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Iterator.php b/include/thirdparty/css-crush/lib/CssCrush/Iterator.php new file mode 100644 index 0000000..dfba721 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Iterator.php @@ -0,0 +1,70 @@ +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); + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Logger.php b/include/thirdparty/css-crush/lib/CssCrush/Logger.php new file mode 100644 index 0000000..8f2228d --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Logger.php @@ -0,0 +1,149 @@ +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); + } + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Mixin.php b/include/thirdparty/css-crush/lib/CssCrush/Mixin.php new file mode 100644 index 0000000..b4df1a4 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Mixin.php @@ -0,0 +1,106 @@ +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('~^(?{{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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Options.php b/include/thirdparty/css-crush/lib/CssCrush/Options.php new file mode 100644 index 0000000..0081e10 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Options.php @@ -0,0 +1,176 @@ + 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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/PostAliasFix.php b/include/thirdparty/css-crush/lib/CssCrush/PostAliasFix.php new file mode 100644 index 0000000..2b7b61e --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/PostAliasFix.php @@ -0,0 +1,26 @@ +cacheData = []; + $this->mixins = []; + $this->fragments = []; + $this->references = []; + $this->absoluteImports = []; + $this->charset = null; + $this->sources = []; + $this->vars = []; + $this->plugins = []; + $this->misc = new \stdClass(); + $this->input = new \stdClass(); + $this->output = new \stdClass(); + $this->tokens = new Tokens(); + $this->functions = new Functions(); + $this->sourceMap = null; + $this->selectorAliases = []; + $this->selectorAliasesPatt = null; + $this->io = new Crush::$config->io($this); + + $this->errors = []; + $this->warnings = []; + $this->debugLog = []; + $this->stat = []; + + // Copy config values. + $this->aliases = $config->aliases; + + // Options. + $this->options = new Options($user_options, $config->options); + + // Context options. + $context += ['type' => 'filter', 'data' => '']; + $this->ioContext = $context['type']; + + // Keep track of global vars to maintain cache integrity. + $this->options->global_vars = $config->vars; + + // Shortcut commonly used options to avoid __get() overhead. + $this->docRoot = isset($this->options->doc_root) ? $this->options->doc_root : $config->docRoot; + $this->generateMap = $this->ioContext === 'file' && $this->options->__get('source_map'); + $this->ruleFormatter = $this->options->__get('formatter'); + $this->minifyOutput = $this->options->__get('minify'); + $this->newline = $this->options->__get('newlines'); + + if ($context['type'] === 'file') { + $file = $context['data']; + $this->input->raw = $file; + if (! ($inputFile = Util::resolveUserPath($file, null, $this->docRoot))) { + throw new \Exception('Input file \'' . basename($file) . '\' not found.'); + } + $this->resolveContext(dirname($inputFile), $inputFile); + } + elseif ($context['type'] === 'filter') { + if (! empty($this->options->context)) { + $this->resolveContext($this->options->context); + } + else { + $this->resolveContext(); + } + $this->input->string = $context['data']; + } + } + + public function release() + { + unset( + $this->tokens, + $this->mixins, + $this->references, + $this->cacheData, + $this->misc, + $this->plugins, + $this->aliases, + $this->selectorAliases + ); + } + + public function resolveContext($input_dir = null, $input_file = null) + { + if ($input_file) { + $this->input->path = $input_file; + $this->input->filename = basename($input_file); + $this->input->mtime = filemtime($input_file); + } + else { + $this->input->path = null; + $this->input->filename = null; + } + + $this->input->dir = $input_dir ?: $this->docRoot; + $this->input->dirUrl = substr($input_dir, strlen($this->docRoot)); + + $this->output->dir = $this->io->getOutputDir(); + $this->output->filename = $this->io->getOutputFileName(); + $this->output->dirUrl = substr($this->output->dir, strlen($this->docRoot)); + + $context_resolved = true; + if ($input_file) { + $output_dir = $this->output->dir; + + if (! file_exists($output_dir)) { + warning("Output directory '$output_dir' doesn't exist."); + $context_resolved = false; + } + elseif (! is_writable($output_dir)) { + + debug('Attempting to change permissions.'); + + if (! @chmod($output_dir, 0755)) { + warning("Output directory '$output_dir' is unwritable."); + $context_resolved = false; + } + else { + debug('Permissions updated.'); + } + } + } + + $this->io->init(); + + return $context_resolved; + } + + + ############################# + # Boilerplate. + + protected function getBoilerplate() + { + $file = false; + $boilerplateOption = $this->options->boilerplate; + + if ($boilerplateOption === true) { + $file = Crush::$dir . '/boilerplate.txt'; + } + elseif (is_string($boilerplateOption)) { + if (file_exists($boilerplateOption)) { + $file = $boilerplateOption; + } + } + + // Return an empty string if no file is found. + if (! $file) { + return ''; + } + + $boilerplate = file_get_contents($file); + + // Substitute any tags + if (preg_match_all('~\{\{([^}]+)\}\}~', $boilerplate, $boilerplateMatches)) { + + // Command line arguments (if any). + $commandArgs = 'n/a'; + if (isset($_SERVER['argv'])) { + $argv = $_SERVER['argv']; + array_shift($argv); + $commandArgs = 'csscrush ' . implode(' ', $argv); + } + + $tags = [ + 'datetime' => @date('Y-m-d H:i:s O'), + 'year' => @date('Y'), + 'command' => $commandArgs, + 'plugins' => implode(',', $this->plugins), + 'version' => function () { + return Version::detect(); + }, + 'compile_time' => function () { + $now = microtime(true) - Crush::$process->stat['compile_start_time']; + return round($now, 4) . ' seconds'; + }, + ]; + + foreach (array_keys($boilerplateMatches[0]) as $index) { + $tagName = trim($boilerplateMatches[1][$index]); + $replacement = '?'; + if (isset($tags[$tagName])) { + $replacement = is_callable($tags[$tagName]) ? $tags[$tagName]() : $tags[$tagName]; + } + $replacements[] = $replacement; + } + $boilerplate = str_replace($boilerplateMatches[0], $replacements, $boilerplate); + } + + // Pretty print. + $EOL = $this->newline; + $boilerplate = preg_split('~[\t]*'. Regex::$classes->newline . '[\t]*~', trim($boilerplate)); + $boilerplate = array_map('trim', $boilerplate); + $boilerplate = "$EOL * " . implode("$EOL * ", $boilerplate); + + return "/*$boilerplate$EOL */$EOL"; + } + + + ############################# + # Selector aliases. + + protected function resolveSelectorAliases() + { + $this->string->pregReplaceCallback( + Regex::make('~@selector(?:-(?alias|splat))? +\:?(?{{ident}}) +(?[^;]+) *;~iS'), + function ($m) { + $name = strtolower($m['name']); + $type = ! empty($m['type']) ? strtolower($m['type']) : 'alias'; + $handler = Util::stripCommentTokens($m['handler']); + Crush::$process->selectorAliases[$name] = new SelectorAlias($handler, $type); + }); + + // Create the selector aliases pattern and store it. + if ($this->selectorAliases) { + $names = implode('|', array_keys($this->selectorAliases)); + $this->selectorAliasesPatt + = Regex::make('~\:(' . $names . '){{RB}}(\()?~iS'); + } + } + + public function addSelectorAlias($name, $handler, $type = 'alias') + { + if ($type != 'callback') { + $handler = $this->tokens->capture($handler, 's'); + } + $this->selectorAliases[$name] = new SelectorAlias($handler, $type); + } + + + ############################# + # Aliases. + + protected function filterAliases() + { + // If a vendor target is given, we prune the aliases array. + $vendors = $this->options->vendor_target; + + // Default vendor argument, so use all aliases as normal. + if ('all' === $vendors) { + + return; + } + + // For expicit 'none' argument turn off aliases. + if ('none' === $vendors) { + $this->aliases = Crush::$config->bareAliases; + + return; + } + + // Normalize vendor names and create regex patt. + $vendor_names = (array) $vendors; + foreach ($vendor_names as &$vendor_name) { + $vendor_name = trim($vendor_name, '-'); + } + $vendor_patt = '~^\-(' . implode($vendor_names, '|') . ')\-~i'; + + + // Loop the aliases array, filter down to the target vendor. + foreach ($this->aliases as $section => $group_array) { + + // Declarations aliases. + if ($section === 'declarations') { + + foreach ($group_array as $property => $values) { + foreach ($values as $value => $prefix_values) { + foreach ($prefix_values as $index => $declaration) { + + if (in_array($declaration[2], $vendor_names)) { + continue; + } + + // Unset uneeded aliases. + unset($this->aliases[$section][$property][$value][$index]); + + if (empty($this->aliases[$section][$property][$value])) { + unset($this->aliases[$section][$property][$value]); + } + if (empty($this->aliases[$section][$property])) { + unset($this->aliases[$section][$property]); + } + } + } + } + } + + // Function group aliases. + elseif ($section === 'function_groups') { + + foreach ($group_array as $func_group => $vendors) { + foreach (array_keys($vendors) as $vendor) { + if (! in_array($vendor, $vendor_names)) { + unset($this->aliases['function_groups'][$func_group][$vendor]); + } + } + } + } + + // Everything else. + else { + foreach ($group_array as $alias_keyword => $prefix_array) { + + // Skip over pointers to function groups. + if ($prefix_array[0] === '.') { + continue; + } + + $result = []; + + foreach ($prefix_array as $prefix) { + if (preg_match($vendor_patt, $prefix)) { + $result[] = $prefix; + } + } + + // Prune the whole alias keyword if there is no result. + if (empty($result)) { + unset($this->aliases[$section][$alias_keyword]); + } + else { + $this->aliases[$section][$alias_keyword] = $result; + } + } + } + } + } + + + ############################# + # Plugins. + + protected function filterPlugins() + { + $this->plugins = array_unique($this->options->plugins); + + foreach ($this->plugins as $plugin) { + Crush::enablePlugin($plugin); + } + } + + + ############################# + # Variables. + + protected function captureVars() + { + Crush::$process->vars = Crush::$process->string->captureDirectives(['set', 'define'], [ + 'singles' => true, + 'lowercase_keys' => false, + ]) + Crush::$process->vars; + + // For convenience adding a runtime variable for cache busting linked resources. + $this->vars['timestamp'] = (int) $this->stat['compile_start_time']; + + // In-file variables override global variables. + $this->vars += Crush::$config->vars; + + // Runtime variables override in-file variables. + if (! empty($this->options->vars)) { + $this->vars = $this->options->vars + $this->vars; + } + + // Place variables referenced inside variables. + foreach ($this->vars as &$value) { + $this->placeVars($value); + } + } + + protected function placeAllVars() + { + $this->placeVars($this->string->raw); + + $rawTokens =& $this->tokens->store; + + // Repeat above steps for variables embedded in string tokens. + foreach ($rawTokens->s as $label => &$value) { + $this->placeVars($value); + } + + // Repeat above steps for variables embedded in URL tokens. + foreach ($rawTokens->u as $label => $url) { + if (! $url->isData && $this->placeVars($url->value)) { + // Re-evaluate $url->value if anything has been interpolated. + $url->evaluate(); + } + } + } + + protected function placeVars(&$value) + { + static $varFunction, $varFunctionSimple; + if (! $varFunction) { + $varFunctionSimple = Regex::make('~\$\( \s* ({{ ident }}) \s* \)~xS'); + $varFunction = new Functions(['$' => function ($rawArgs) { + $args = Functions::parseArgsSimple($rawArgs); + if (isset(Crush::$process->vars[$args[0]])) { + return Crush::$process->vars[$args[0]]; + } + else { + return isset($args[1]) ? $args[1] : ''; + } + }]); + } + + // Variables with no default value. + $value = preg_replace_callback($varFunctionSimple, function ($m) { + $varName = $m[1]; + if (isset(Crush::$process->vars[$varName])) { + return Crush::$process->vars[$varName]; + } + }, $value, -1, $varsPlaced); + + // Variables with default value. + if (strpos($value, '$(') !== false) { + + // Assume at least one replace. + $varsPlaced = true; + + // Variables may be nested so need to apply full function parsing. + $value = $varFunction->apply($value); + } + + // If we know replacements have been made we may want to update $value. e.g URL tokens. + return $varsPlaced; + } + + ############################# + # @for..in blocks. + + protected function resolveLoops() + { + $LOOP_VAR_PATT = '~\#\( \s* (?[a-zA-Z][\.a-zA-Z0-9-_]*) \s* \)~x'; + $LOOP_PATT = Regex::make('~ + (? + @for \s+ (?{{ident}}) \s+ in \s+ (?[^{]+) + ) \s* + {{ block }} + ~xiS'); + + $apply_scope = function ($str, $context) use ($LOOP_VAR_PATT, $LOOP_PATT) { + // Need to temporarily hide child block scopes. + $child_scopes = []; + $str = preg_replace_callback($LOOP_PATT, function ($m) use (&$child_scopes) { + $label = '?B' . count($child_scopes) . '?'; + $child_scopes[$label] = $m['block']; + return $m['expression'] . $label; + }, $str); + + $str = preg_replace_callback($LOOP_VAR_PATT, function ($m) use ($context) { + // Normalize casing of built-in loop variables. + // User variables are case-sensitive. + $arg = preg_replace_callback('~^loop\.(parent\.)?counter0?$~i', function ($m) { + return strtolower($m[0]); + }, $m['arg']); + + return isset($context[$arg]) ? $context[$arg] : ''; + }, $str); + + return str_replace(array_keys($child_scopes), array_values($child_scopes), $str); + }; + + $resolve_list = function ($list) { + // Resolve the list of items for iteration. + // Either a generator function or a plain list. + $items = []; + $this->placeVars($list); + $list = $this->functions->apply($list); + if (preg_match(Regex::make('~(?range){{ parens }}~ix'), $list, $m)) { + $func = strtolower($m['func']); + $args = Functions::parseArgs($m['parens_content']); + switch ($func) { + case 'range': + $items = range(...$args); + break; + } + } + else { + $items = Util::splitDelimList($list); + } + + return $items; + }; + + $unroll = function ($str, $context = []) use (&$unroll, $LOOP_PATT, $apply_scope, $resolve_list) { + $str = $apply_scope($str, $context); + while (preg_match($LOOP_PATT, $str, $m, PREG_OFFSET_CAPTURE)) { + $str = substr_replace($str, '', $m[0][1], strlen($m[0][0])); + $context['loop.parent.counter'] = isset($context['loop.counter']) ? $context['loop.counter'] : -1; + $context['loop.parent.counter0'] = isset($context['loop.counter0']) ? $context['loop.counter0'] : -1; + foreach ($resolve_list($m['list'][0]) as $index => $value) { + $str .= $unroll($m['block_content'][0], [ + $m['var'][0] => $value, + 'loop.counter' => $index + 1, + 'loop.counter0' => $index, + ] + $context); + } + } + + return $str; + }; + + $this->string->pregReplaceCallback($LOOP_PATT, function ($m) use ($unroll) { + return Template::tokenize($unroll(Template::unTokenize($m[0]))); + }); + } + + ############################# + # @ifdefine blocks. + + protected function resolveIfDefines() + { + $ifdefinePatt = Regex::make('~@if(?:set|define) \s+ (?not \s+)? (?{{ ident }}) \s* {{ parens }}? \s* \{~ixS'); + + $matches = $this->string->matchAll($ifdefinePatt); + + while ($match = array_pop($matches)) { + + $curlyMatch = new BalancedMatch($this->string, $match[0][1]); + + if (! $curlyMatch->match) { + continue; + } + + $negate = $match['negate'][1] != -1; + $nameDefined = isset($this->vars[$match['name'][0]]); + + $valueDefined = isset($match['parens_content'][0]); + $valueMatch = false; + if ($nameDefined && $valueDefined) { + $testValue = Util::rawValue(trim($match['parens_content'][0])); + $varValue = Util::rawValue($this->vars[$match['name'][0]]); + $valueMatch = $varValue == $testValue; + } + + if ( + ( $valueDefined && !$negate && $valueMatch ) + || ( $valueDefined && $negate && !$valueMatch ) + || ( !$valueDefined && !$negate && $nameDefined ) + || ( !$valueDefined && $negate && !$nameDefined ) + ) { + $curlyMatch->unWrap(); + } + else { + $curlyMatch->replace(''); + } + } + } + + + ############################# + # Mixins. + + protected function captureMixins() + { + $this->string->pregReplaceCallback(Regex::$patt->mixin, function ($m) { + Crush::$process->mixins[$m['name']] = new Mixin($m['block_content']); + }); + } + + + ############################# + # Fragments. + + protected function resolveFragments() + { + $fragments =& Crush::$process->fragments; + + $this->string->pregReplaceCallback(Regex::$patt->fragmentCapture, function ($m) use (&$fragments) { + $fragments[$m['name']] = new Fragment( + $m['block_content'], + ['name' => strtolower($m['name'])] + ); + return ''; + }); + + $this->string->pregReplaceCallback(Regex::$patt->fragmentInvoke, function ($m) use (&$fragments) { + $fragment = isset($fragments[$m['name']]) ? $fragments[$m['name']] : null; + if ($fragment) { + $args = []; + if (isset($m['parens'])) { + $args = Functions::parseArgs($m['parens_content']); + } + return $fragment($args); + } + return ''; + }); + } + + + ############################# + # Rules. + + public function captureRules() + { + $tokens = $this->tokens; + + $rulePatt = Regex::make('~ + (? {{ t_token }}) + \s* + (? [^{]+) + \s* + {{ block }} + ~xiS'); + $rulesAndMediaPatt = Regex::make('~{{ r_token }}|@media[^\{]+{{ block }}~iS'); + + $count = preg_match_all(Regex::$patt->t_token, $this->string->raw, $traceMatches, PREG_OFFSET_CAPTURE); + while ($count--) { + + $traceOffset = $traceMatches[0][$count][1]; + + preg_match($rulePatt, $this->string->raw, $ruleMatch, null, $traceOffset); + + $selector = trim($ruleMatch['selector']); + $block = trim($ruleMatch['block_content']); + $replace = ''; + + // If rules are nested inside we set their parent property. + if (preg_match_all(Regex::$patt->r_token, $block, $childMatches)) { + + $block = preg_replace_callback($rulesAndMediaPatt, function ($m) use (&$replace) { + $replace .= $m[0]; + return ''; + }, $block); + + $rule = new Rule($selector, $block, $ruleMatch['trace_token']); + foreach ($childMatches[0] as $childToken) { + $childRule = $tokens->get($childToken); + if (! $childRule->parent) { + $childRule->parent = $rule; + } + } + } + else { + $rule = new Rule($selector, $block, $ruleMatch['trace_token']); + } + + $replace = $tokens->add($rule, 'r', $rule->label) . $replace; + + $this->string->splice($replace, $traceOffset, strlen($ruleMatch[0])); + } + + // Flip, since we just captured rules in reverse order. + $tokens->store->r = array_reverse($tokens->store->r); + + foreach ($tokens->store->r as $rule) { + if ($rule->parent) { + $rule->selectors->merge(array_keys($rule->parent->selectors->store)); + } + } + + // Cleanup unusable rules. + $this->string->pregReplaceCallback(Regex::$patt->r_token, function ($m) use ($tokens) { + $ruleToken = $m[0]; + $rule = $tokens->store->r[$ruleToken]; + if (empty($rule->declarations->store) && ! $rule->extendArgs) { + unset($tokens->store->r[$ruleToken]); + return ''; + } + return $ruleToken; + }); + } + + protected function processRules() + { + // Create table of name/selector to rule references. + $namedReferences = []; + + $previousRule = null; + foreach ($this->tokens->store->r as $rule) { + if ($rule->name) { + $namedReferences[$rule->name] = $rule; + } + foreach ($rule->selectors as $selector) { + $this->references[$selector->readableValue] = $rule; + } + if ($previousRule) { + $rule->previous = $previousRule; + $previousRule->next = $rule; + } + $previousRule = $rule; + } + + // Explicit named references take precedence. + $this->references = $namedReferences + $this->references; + + foreach ($this->tokens->store->r as $rule) { + + $rule->declarations->flatten(); + $rule->declarations->process(); + + $this->emit('rule_prealias', $rule); + + $rule->declarations->aliasProperties($rule->vendorContext); + $rule->declarations->aliasFunctions($rule->vendorContext); + $rule->declarations->aliasDeclarations($rule->vendorContext); + + $this->emit('rule_postalias', $rule); + + $rule->selectors->expand(); + $rule->applyExtendables(); + + $this->emit('rule_postprocess', $rule); + } + } + + + ############################# + # @-rule aliasing. + + protected function aliasAtRules() + { + if (empty($this->aliases['at-rules'])) { + + return; + } + + $aliases = $this->aliases['at-rules']; + $regex = Regex::$patt; + + foreach ($aliases as $at_rule => $at_rule_aliases) { + + $matches = $this->string->matchAll("~@$at_rule" . '[\s{]~i'); + + // Find at-rules that we want to alias. + while ($match = array_pop($matches)) { + + $curly_match = new BalancedMatch($this->string, $match[0][1]); + + if (! $curly_match->match) { + // Couldn't match the block. + continue; + } + + // Build up string with aliased blocks for splicing. + $original_block = $curly_match->whole(); + $new_blocks = []; + + foreach ($at_rule_aliases as $alias) { + + // Copy original block, replacing at-rule with alias name. + $copy_block = str_replace("@$at_rule", "@$alias", $original_block); + + // Aliases are nearly always prefixed, capture the current vendor name. + preg_match($regex->vendorPrefix, $alias, $vendor); + + $vendor = $vendor ? $vendor[1] : null; + + // Duplicate rules. + if (preg_match_all($regex->r_token, $copy_block, $copy_matches)) { + + $originals = []; + $replacements = []; + + foreach ($copy_matches[0] as $rule_label) { + + // Clone the matched rule. + $originals[] = $rule_label; + $clone_rule = clone $this->tokens->get($rule_label); + + $clone_rule->vendorContext = $vendor; + + // Store the clone. + $replacements[] = $this->tokens->add($clone_rule); + } + + // Finally replace the original labels with the cloned rule labels. + $copy_block = str_replace($originals, $replacements, $copy_block); + } + + // Add the copied block to the stack. + $new_blocks[] = $copy_block; + } + + // The original version is always pushed last in the list. + $new_blocks[] = $original_block; + + // Splice in the blocks. + $curly_match->replace(implode("\n", $new_blocks)); + } + } + } + + + ############################# + # Compile / collate. + + protected function collate() + { + $options = $this->options; + $minify = $options->minify; + $EOL = $this->newline; + + // Formatting replacements. + // Strip newlines added during processing. + $regex_replacements = []; + $regex_replacements['~\n+~'] = ''; + + if ($minify) { + // Strip whitespace around colons used in @-rule arguments. + $regex_replacements['~ ?\: ?~'] = ':'; + } + else { + // Pretty printing. + $regex_replacements['~}~'] = "$0$EOL$EOL"; + $regex_replacements['~([^\s])\{~'] = "$1 {"; + $regex_replacements['~ ?(@[^{]+\{)~'] = "$1$EOL"; + $regex_replacements['~ ?(@[^;]+\;)~'] = "$1$EOL"; + + // Trim leading spaces on @-rules and some tokens. + $regex_replacements[Regex::make('~ +([@}]|\?[rc]{{token_id}}\?)~S')] = "$1"; + + // Additional newline between adjacent rules and comments. + $regex_replacements[Regex::make('~({{r_token}}) (\s*) ({{c_token}})~xS')] = "$1$EOL$2$3"; + } + + // Apply all formatting replacements. + $this->string->pregReplaceHash($regex_replacements)->lTrim(); + + $this->string->restore('r'); + + // Record stats then drop rule objects to reclaim memory. + Crush::runStat('selector_count', 'rule_count', 'vars'); + $this->tokens->store->r = []; + + // If specified, apply advanced minification. + if (is_array($minify)) { + if (in_array('colors', $minify)) { + $this->minifyColors(); + } + } + + $this->decruft(); + + if (! $minify) { + // Add newlines after comments. + foreach ($this->tokens->store->c as $token => &$comment) { + $comment .= $EOL; + } + + // Insert comments and do final whitespace cleanup. + $this->string + ->restore('c') + ->trim() + ->append($EOL); + } + + // Insert URLs. + $urls = $this->tokens->store->u; + if ($urls) { + + $link = Util::getLinkBetweenPaths($this->output->dir, $this->input->dir); + $make_urls_absolute = $options->rewrite_import_urls === 'absolute'; + + foreach ($urls as $token => $url) { + + if ($url->isRelative && ! $url->noRewrite) { + if ($make_urls_absolute) { + $url->toRoot(); + } + // If output dir is different to input dir prepend a link between the two. + elseif ($link && $options->rewrite_import_urls) { + $url->prepend($link); + } + } + } + } + + if ($this->absoluteImports) { + $absoluteImports = ''; + $closing = $minify ? ';' : ";$EOL"; + foreach ($this->absoluteImports as $import) { + $absoluteImports .= "@import $import->url" . ($import->media ? " $import->media" : '') . $closing; + } + $this->string->prepend($absoluteImports); + } + + if ($options->boilerplate) { + $this->string->prepend($this->getBoilerplate()); + } + + if ($this->charset) { + $this->string->prepend("@charset \"$this->charset\";$EOL"); + } + + $this->string->restore(['u', 's']); + + if ($this->generateMap) { + $this->generateSourceMap(); + } + } + + private $iniOriginal = []; + public function preCompile() + { + foreach ([ + 'pcre.backtrack_limit' => 1000000, + 'pcre.jit' => 0, // Have run into PREG_JIT_STACKLIMIT_ERROR (issue #82). + 'memory_limit' => '128M', + ] as $name => $value) { + $this->iniOriginal[$name] = ini_get($name); + ini_set($name, $value); + } + + $this->filterPlugins(); + $this->filterAliases(); + + $this->functions->setPattern(true); + + $this->stat['compile_start_time'] = microtime(true); + } + + public function postCompile() + { + $this->release(); + + Crush::runStat('compile_time'); + + foreach ($this->iniOriginal as $name => $value) { + ini_set($name, $value); + } + } + + public function compile() + { + $this->preCompile(); + + $importer = new Importer($this); + $this->string = new StringObject($importer->collate()); + + // Capture phase 0 hook: Before all variables have resolved. + $this->emit('capture_phase0', $this); + + $this->captureVars(); + + $this->resolveIfDefines(); + + $this->resolveLoops(); + + $this->placeAllVars(); + + // Capture phase 1 hook: After all variables have resolved. + $this->emit('capture_phase1', $this); + + $this->resolveSelectorAliases(); + + $this->captureMixins(); + + $this->resolveFragments(); + + // Capture phase 2 hook: After most built-in directives have resolved. + $this->emit('capture_phase2', $this); + + $this->captureRules(); + + // Calling functions on media query lists. + $process = $this; + $this->string->pregReplaceCallback('~@media\s+(?[^{]+)\{~i', function ($m) use (&$process) { + return "@media {$process->functions->apply($m['media_list'])}{"; + }); + + $this->aliasAtRules(); + + $this->processRules(); + + $this->collate(); + + $this->postCompile(); + + return $this->string; + } + + + ############################# + # Source maps. + + public function generateSourceMap() + { + $this->sourceMap = [ + 'version' => 3, + 'file' => $this->output->filename, + 'sources' => [], + ]; + foreach ($this->sources as $source) { + $this->sourceMap['sources'][] = Util::getLinkBetweenPaths($this->output->dir, $source, false); + } + + $token_patt = Regex::make('~\?[tm]{{token_id}}\?~S'); + $mappings = []; + $lines = preg_split(Regex::$patt->newline, $this->string->raw); + $tokens =& $this->tokens->store; + + // All mappings are calculated as delta values. + $previous_dest_col = 0; + $previous_src_file = 0; + $previous_src_line = 0; + $previous_src_col = 0; + + foreach ($lines as &$line_text) { + + $line_segments = []; + + while (preg_match($token_patt, $line_text, $m, PREG_OFFSET_CAPTURE)) { + + list($token, $dest_col) = $m[0]; + $token_type = $token[1]; + + if (isset($tokens->{$token_type}[$token])) { + + list($src_file, $src_line, $src_col) = explode(',', $tokens->{$token_type}[$token]); + $line_segments[] = + Util::vlqEncode($dest_col - $previous_dest_col) . + Util::vlqEncode($src_file - $previous_src_file) . + Util::vlqEncode($src_line - $previous_src_line) . + Util::vlqEncode($src_col - $previous_src_col); + + $previous_dest_col = $dest_col; + $previous_src_file = $src_file; + $previous_src_line = $src_line; + $previous_src_col = $src_col; + } + $line_text = substr_replace($line_text, '', $dest_col, strlen($token)); + } + + $mappings[] = implode(',', $line_segments); + } + + $this->string->raw = implode($this->newline, $lines); + $this->sourceMap['mappings'] = implode(';', $mappings); + } + + + ############################# + # Decruft. + + protected function decruft() + { + return $this->string->pregReplaceHash([ + + // Strip leading zeros on floats. + '~([: \(,])(-?)0(\.\d+)~S' => '$1$2$3', + + // Strip unnecessary units on zero values for length types. + '~([: \(,])\.?0' . Regex::$classes->length_unit . '~iS' => '${1}0', + + // Collapse zero lists. + '~(\: *)(?:0 0 0|0 0 0 0) *([;}])~S' => '${1}0$2', + + // Collapse zero lists 2nd pass. + '~(padding|margin|border-radius) ?(\: *)0 0 *([;}])~iS' => '${1}${2}0$3', + + // Dropping redundant trailing zeros on TRBL lists. + '~(\: *)(-?(?:\d+)?\.?\d+[a-z]{1,4}) 0 0 0 *([;}])~iS' => '$1$2 0 0$3', + '~(\: *)0 0 (-?(?:\d+)?\.?\d+[a-z]{1,4}) 0 *([;}])~iS' => '${1}0 0 $2$3', + + // Compress hex codes. + Regex::$patt->cruftyHex => '#$1$2$3', + ]); + } + + + ############################# + # Advanced minification. + + protected function minifyColors() + { + static $keywords_patt, $functions_patt; + + $minified_keywords = Color::getMinifyableKeywords(); + + if (! $keywords_patt) { + $keywords_patt = '~(?string->pregReplaceCallback($keywords_patt, function ($m) use ($minified_keywords) { + return $minified_keywords[strtolower($m[0])]; + }); + + $this->string->pregReplaceCallback($functions_patt, function ($m) { + $args = Functions::parseArgs(trim($m[2])); + if (stripos($m[1], 'hsl') === 0) { + $args = Color::cssHslToRgb($args); + } + return Color::rgbToHex($args); + }); + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Regex.php b/include/thirdparty/css-crush/lib/CssCrush/Regex.php new file mode 100644 index 0000000..deba844 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Regex.php @@ -0,0 +1,112 @@ +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 = '(?RB = '(?![\w-])'; // Right ident boundry. + + // Recursive block matching. + $classes->block = '(?\{\s*(?(?:(?>[^{}]+)|(?&block))*)\})'; + $classes->parens = '(?\(\s*(?(?:(?>[^()]+)|(?&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+ (?{{ident}}) \s* {{block}}~ixS'); + $patt->fragmentCapture = Regex::make('~@fragment \s+ (?{{ident}}) \s* {{block}}~ixS'); + $patt->fragmentInvoke = Regex::make('~@fragment \s+ (?{{ident}}) {{parens}}? \s* ;~ixS'); + $patt->abstract = Regex::make('~^@abstract \s+ (?{{ident}})~ixS'); + + // Functions. + $patt->functionTest = Regex::make('~{{ LB }} (?{{ 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('~^ \? (?[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('~\{\{ *(?\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(); diff --git a/include/thirdparty/css-crush/lib/CssCrush/Rule.php b/include/thirdparty/css-crush/lib/CssCrush/Rule.php new file mode 100644 index 0000000..2e899a5 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Rule.php @@ -0,0 +1,145 @@ +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; + } + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Selector.php b/include/thirdparty/css-crush/lib/CssCrush/Selector.php new file mode 100644 index 0000000..108ceb3 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Selector.php @@ -0,0 +1,105 @@ +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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/SelectorAlias.php b/include/thirdparty/css-crush/lib/CssCrush/SelectorAlias.php new file mode 100644 index 0000000..8189d62 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/SelectorAlias.php @@ -0,0 +1,63 @@ +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('~#\((?{{ 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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/SelectorList.php b/include/thirdparty/css-crush/lib/CssCrush/SelectorList.php new file mode 100644 index 0000000..87ec177 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/SelectorList.php @@ -0,0 +1,136 @@ +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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/StringObject.php b/include/thirdparty/css-crush/lib/CssCrush/StringObject.php new file mode 100644 index 0000000..18b4eec --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/StringObject.php @@ -0,0 +1,156 @@ +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+(?{{ ident }})\s+(?[^;]+)\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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Template.php b/include/thirdparty/css-crush/lib/CssCrush/Template.php new file mode 100644 index 0000000..034ed27 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Template.php @@ -0,0 +1,147 @@ +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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Tokens.php b/include/thirdparty/css-crush/lib/CssCrush/Tokens.php new file mode 100644 index 0000000..7726955 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Tokens.php @@ -0,0 +1,161 @@ +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+ (?{{s_token}}) | {{LB}} (?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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Url.php b/include/thirdparty/css-crush/lib/CssCrush/Url.php new file mode 100644 index 0000000..0ed604d --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Url.php @@ -0,0 +1,214 @@ +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('~(?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('~^(?: (?[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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Util.php b/include/thirdparty/css-crush/lib/CssCrush/Util.php new file mode 100644 index 0000000..f0743e2 --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Util.php @@ -0,0 +1,277 @@ + $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; + } +} diff --git a/include/thirdparty/css-crush/lib/CssCrush/Version.php b/include/thirdparty/css-crush/lib/CssCrush/Version.php new file mode 100644 index 0000000..f15d05b --- /dev/null +++ b/include/thirdparty/css-crush/lib/CssCrush/Version.php @@ -0,0 +1,116 @@ +\d+) + (?:\.(?\d+))? + (?:\.(?\d+))? + (?:-(?.+))? + $~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; + } +} diff --git a/include/thirdparty/css-crush/lib/functions.php b/include/thirdparty/css-crush/lib/functions.php new file mode 100644 index 0000000..c8abc02 --- /dev/null +++ b/include/thirdparty/css-crush/lib/functions.php @@ -0,0 +1,167 @@ + '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 "\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 = ""; + $tagClose = ''; + } + 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; +} diff --git a/include/thirdparty/css-crush/misc/color-keywords.ini b/include/thirdparty/css-crush/misc/color-keywords.ini new file mode 100644 index 0000000..e8ec1e0 --- /dev/null +++ b/include/thirdparty/css-crush/misc/color-keywords.ini @@ -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" diff --git a/include/thirdparty/css-crush/misc/formatters.php b/include/thirdparty/css-crush/misc/formatters.php new file mode 100644 index 0000000..2117837 --- /dev/null +++ b/include/thirdparty/css-crush/misc/formatters.php @@ -0,0 +1,48 @@ +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"; +} diff --git a/include/thirdparty/css-crush/misc/property-sorting.ini b/include/thirdparty/css-crush/misc/property-sorting.ini new file mode 100644 index 0000000..3dfab1c --- /dev/null +++ b/include/thirdparty/css-crush/misc/property-sorting.ini @@ -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 diff --git a/include/thirdparty/css-crush/plugins/aria.php b/include/thirdparty/css-crush/plugins/aria.php new file mode 100644 index 0000000..e8408ed --- /dev/null +++ b/include/thirdparty/css-crush/plugins/aria.php @@ -0,0 +1,70 @@ + $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; +} diff --git a/include/thirdparty/css-crush/plugins/canvas.php b/include/thirdparty/css-crush/plugins/canvas.php new file mode 100644 index 0000000..85cdc10 --- /dev/null +++ b/include/thirdparty/css-crush/plugins/canvas.php @@ -0,0 +1,652 @@ +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+(?{{ 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(?$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; +} diff --git a/include/thirdparty/css-crush/plugins/ease.php b/include/thirdparty/css-crush/plugins/ease.php new file mode 100644 index 0000000..fce8ad6 --- /dev/null +++ b/include/thirdparty/css-crush/plugins/ease.php @@ -0,0 +1,65 @@ +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); + } + } +} diff --git a/include/thirdparty/css-crush/plugins/forms.php b/include/thirdparty/css-crush/plugins/forms.php new file mode 100644 index 0000000..8d4321b --- /dev/null +++ b/include/thirdparty/css-crush/plugins/forms.php @@ -0,0 +1,33 @@ + $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"]', + ]; +} diff --git a/include/thirdparty/css-crush/plugins/hocus-pocus.php b/include/thirdparty/css-crush/plugins/hocus-pocus.php new file mode 100644 index 0000000..a7cda1d --- /dev/null +++ b/include/thirdparty/css-crush/plugins/hocus-pocus.php @@ -0,0 +1,10 @@ +addSelectorAlias('hocus', ':any(:hover,:focus)'); + $process->addSelectorAlias('pocus', ':any(:hover,:focus,:active)'); +}); diff --git a/include/thirdparty/css-crush/plugins/property-sorter.php b/include/thirdparty/css-crush/plugins/property-sorter.php new file mode 100644 index 0000000..8930d66 --- /dev/null +++ b/include/thirdparty/css-crush/plugins/property-sorter.php @@ -0,0 +1,160 @@ +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; + } +} diff --git a/include/thirdparty/css-crush/plugins/svg.php b/include/thirdparty/css-crush/plugins/svg.php new file mode 100644 index 0000000..3eb74f7 --- /dev/null +++ b/include/thirdparty/css-crush/plugins/svg.php @@ -0,0 +1,1105 @@ +on('capture_phase2', 'CssCrush\svg_capture'); + $process->functions->add('svg', 'CssCrush\fn__svg'); + $process->functions->add('svg-data', 'CssCrush\fn__svg_data'); + $process->functions->add('svg-linear-gradient', 'CssCrush\fn__svg_linear_gradient'); + $process->functions->add('svg-radial-gradient', 'CssCrush\fn__svg_radial_gradient'); +}); + +function fn__svg($input) { + + return svg_generator($input, 'svg'); +} + +function fn__svg_data($input) { + + return svg_generator($input, 'svg-data'); +} + +function svg_capture($process) { + + $process->string->pregReplaceCallback( + Regex::make('~@svg\s+(?{{ ident }})\s*{{ block }}~iS'), + function ($m) { + Crush::$process->misc->svg_defs[strtolower($m['name'])] = new Template($m['block_content']); + return ''; + }); +} + +function svg_generator($input, $fn_name) { + + $process = Crush::$process; + + $cache_key = $fn_name . $input; + if (isset($process->misc->svg_cache[$cache_key])) { + + return $process->misc->svg_cache[$cache_key]; + } + + // Map types to element names. + static $schemas; + if (! $schemas) { + $schemas = [ + 'circle' => [ + 'tag' => 'circle', + 'attrs' => 'cx cy r', + ], + 'ellipse' => [ + 'tag' => 'ellipse', + 'attrs' => 'cx cy rx ry', + ], + 'rect' => [ + 'tag' => 'rect', + 'attrs' => 'x y rx ry width height', + ], + 'polygon' => [ + 'tag' => 'polygon', + 'attrs' => 'points', + ], + 'line' => [ + 'tag' => 'line', + 'attrs' => 'x1 y1 x2 y2', + ], + 'polyline' => [ + 'tag' => 'polyline', + 'attrs' => 'points', + ], + 'path' => [ + 'tag' => 'path', + 'attrs' => 'd', + ], + 'star' => [ + 'tag' => 'path', + 'attrs' => '', + ], + 'text' => [ + 'tag' => 'text', + 'attrs' => 'x y dx dy rotate', + ], + ]; + + // Convert attributes to keyed array. + // Add global attributes. + foreach ($schemas as $type => &$schema) { + $schema['attrs'] = array_flip(explode(' ', $schema['attrs'])) + + ['transform' => true]; + } + } + + // Non standard attributes. + static $custom_attrs = [ + 'type' => true, + 'data' => true, + 'twist' => true, + 'diameter' => true, + 'corner-radius' => true, + 'star-points' => true, + 'margin' => true, + 'drop-shadow' => true, + 'sides' => true, + 'text' => true, + 'width' => true, + 'height' => true, + ]; + + // Bail if no args. + $args = Functions::parseArgs($input); + if (! isset($args[0])) { + + return ''; + } + + $name = strtolower(array_shift($args)); + + // Bail if no SVG registered by this name. + $svg_defs =& $process->misc->svg_defs; + if (! isset($svg_defs[$name])) { + + return ''; + } + + // Apply args to template. + $block = $svg_defs[$name]($args); + + $raw_data = DeclarationList::parse($block, [ + 'keyed' => true, + 'lowercase_keys' => true, + 'flatten' => true, + 'apply_hooks' => true, + ]); + + // Resolve the type. + // Bail if type not recognised. + $type = isset($raw_data['type']) ? strtolower($raw_data['type']) : 'path'; + if (! isset($schemas[$type])) { + + return ''; + } + + // Create element object for attaching all required rendering data. + $element = (object) [ + 'tag' => $schemas[$type]['tag'], + 'fills' => [ + 'gradients' => [], + 'patterns' => [], + ], + 'filters' => [], + 'data' => [], + 'attrs' => [], + 'styles' => [], + 'svg_attrs' => [ + 'xmlns' => 'http://www.w3.org/2000/svg', + ], + 'svg_styles' => [], + 'face_styles' => [], + ]; + + // Filter off prefixed properties that are for the svg element or @font-face. + foreach ($raw_data as $property => $value) { + if (strpos($property, 'svg-') === 0) { + $element->svg_styles[substr($property, 4)] = $value; + unset($raw_data[$property]); + } + elseif (strpos($property, 'face-') === 0) { + $element->face_styles[substr($property, 5)] = $value; + unset($raw_data[$property]); + } + } + + svg_apply_css_funcs($element, $raw_data); + + // Initialize element attributes. + $element->attrs = array_intersect_key($raw_data, $schemas[$type]['attrs']); + $element->data = array_intersect_key($raw_data, $custom_attrs); + + // Everything else is treated as CSS. + $element->styles = array_diff_key($raw_data, $custom_attrs, $schemas[$type]['attrs']); + + // Pre-populate common attributes. + svg_preprocess($element); + + // Filters. + svg_apply_filters($element); + + // Apply element type callback. + call_user_func("CssCrush\svg_$type", $element); + + // Apply optimizations. + svg_compress($element); + + // Build markup. + $svg = svg_render($element); + + // Debugging... + // $code = implode("\n", $svg); + // $test = '
' . htmlspecialchars($code) . '
'; + // echo $test; + + // Either write to a file. + if ($fn_name === 'svg' && $process->ioContext === 'file') { + + $flattened_svg = implode("\n", $svg); + + // Create fingerprint for the created file. + $fingerprint = substr(md5($flattened_svg), 0, 7); + $generated_filename = "svg-$name-$fingerprint.svg"; + + 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; + } + + Util::filePutContents($generated_filepath, $flattened_svg, __METHOD__); + + $url = new Url($generated_url); + $url->noRewrite = true; + } + // Or create data uri. + else { + $url = new Url('data:image/svg+xml;base64,' . base64_encode(implode('', $svg))); + } + + // Cache the output URL. + $label = $process->tokens->add($url); + $process->misc->svg_cache[$cache_key] = $label; + + return $label; +} + + +/* + Circle callback. +*/ +function svg_circle($element) { + + // Ensure required attributes have defaults set. + $element->data += [ + 'diameter' => 50, + ]; + + list($margin_top, $margin_right, $margin_bottom, $margin_left) = $element->data['margin']; + + $element->attrs['r'] = + $radius = svg_ifset($element->attrs['r'], $element->data['diameter'] / 2); + + $diameter = $radius * 2; + + $element->attrs['cx'] = svg_ifset($element->attrs['cx'], $margin_left + $radius); + $element->attrs['cy'] = svg_ifset($element->attrs['cy'], $margin_top + $radius); + + $element->svg_attrs['width'] = $margin_left + $diameter + $margin_right; + $element->svg_attrs['height'] = $margin_top + $diameter + $margin_bottom; +} + +/* + Rect callback. +*/ +function svg_rect($element) { + + $element->data += [ + 'width' => 50, + 'height' => 50, + ]; + + list($margin_top, $margin_right, $margin_bottom, $margin_left) = $element->data['margin']; + + $element->attrs['x'] = $margin_left; + $element->attrs['y'] = $margin_top; + $element->attrs['width'] = $element->data['width']; + $element->attrs['height'] = $element->data['height']; + + if (isset($element->data['corner-radius'])) { + $args = svg_parselist($element->data['corner-radius']); + $element->attrs['rx'] = isset($args[0]) ? $args[0] : 0; + $element->attrs['ry'] = isset($args[1]) ? $args[1] : $args[0]; + } + + $element->svg_attrs['width'] = $margin_left + $element->data['width'] + $margin_right; + $element->svg_attrs['height'] = $margin_top + $element->data['height'] + $margin_bottom; +} + +/* + Ellipse callback. +*/ +function svg_ellipse($element) { + + $element->data += [ + 'diameter' => '100 50', + ]; + + if (! isset($element->attrs['rx']) && ! isset($element->attrs['ry'])) { + $diameter = svg_parselist($element->data['diameter']); + $element->attrs['rx'] = $diameter[0] / 2; + $element->attrs['ry'] = isset($diameter[1]) ? $diameter[1] / 2 : $diameter[0] / 2; + } + + list($margin_top, $margin_right, $margin_bottom, $margin_left) = $element->data['margin']; + + $element->attrs['cx'] = $margin_left + $element->attrs['rx']; + $element->attrs['cy'] = $margin_top + $element->attrs['ry']; + + $element->svg_attrs['width'] = $margin_left + ($element->attrs['rx'] * 2) + $margin_right; + $element->svg_attrs['height'] = $margin_top + ($element->attrs['ry'] * 2) + $margin_bottom; +} + +/* + Path callback. +*/ +function svg_path($element) { + + // Ensure minimum required attributes have defaults set. + $element->data += [ + 'd' => 'M 10,10 l 10,0 l 0,10 l 10,0 l 0,10', + ]; + + // Unclosed paths have implicit fill. + $element->styles += [ + 'fill' => 'none', + ]; +} + +/* + Polyline callback. +*/ +function svg_polyline($element) { + + // Ensure required attributes have defaults set. + $element->data += [ + 'points' => '20,20 40,20 40,40 60,40 60,60', + ]; + + // Polylines have implicit fill. + $element->styles += [ + 'fill' => 'none', + ]; +} + +/* + Line callback. +*/ +function svg_line($element) { + + // Set a default stroke. + $element->styles += [ + 'stroke' => '#000', + ]; + + $element->attrs += [ + 'x1' => 0, + 'x2' => 0, + 'y1' => 0, + 'y2' => 0, + ]; +} + +/* + Polygon callback. +*/ +function svg_polygon($element) { + + if (! isset($element->attrs['points'])) { + + // Switch to path element. + $element->tag = 'path'; + + $element->data += [ + 'sides' => 3, + 'diameter' => 100, + ]; + + list($margin_top, $margin_right, $margin_bottom, $margin_left) = $element->data['margin']; + + $diameter = svg_parselist($element->data['diameter']); + $diameter = $diameter[0]; + $radius = $diameter / 2; + + $cx = $radius + $margin_left; + $cy = $radius + $margin_top; + $sides = $element->data['sides']; + + $element->attrs['d'] = svg_starpath($cx, $cy, $sides, $radius); + + $element->svg_attrs['width'] = $diameter + $margin_left + $margin_right; + $element->svg_attrs['height'] = $diameter + $margin_top + $margin_bottom; + } +} + +/* + Star callback. +*/ +function svg_star($element) { + + // Minimum required attributes have defaults. + $element->data += [ + 'star-points' => 4, + 'diameter' => '50 30', + 'twist' => 0, + ]; + + list($margin_top, $margin_right, $margin_bottom, $margin_left) = $element->data['margin']; + + $diameter = svg_parselist($element->data['diameter']); + if (! isset($diameter[1])) { + $diameter[1] = ($diameter[0] / 2); + } + $outer_r = $diameter[0] / 2; + $inner_r = $diameter[1] / 2; + + $cx = $outer_r + $margin_left; + $cy = $outer_r + $margin_top; + $points = $element->data['star-points']; + $twist = $element->data['twist'] * 10; + + $element->attrs['d'] = svg_starpath($cx, $cy, $points, $outer_r, $inner_r, $twist); + + $element->svg_attrs['width'] = $margin_left + ($outer_r * 2) + $margin_left; + $element->svg_attrs['height'] = $margin_top + ($outer_r * 2) + $margin_bottom; +} + +/* + Text callback. + Warning: Very limited for svg-as-image situations. +*/ +function svg_text($element) { + + // Minimum required attributes have defaults. + $element->data += [ + 'x' => 0, + 'y' => 0, + 'width' => 100, + 'height' => 100, + 'text' => '', + ]; + + $text = Crush::$process->tokens->restore($element->data['text'], 's', true); + + // Remove open and close quotes. + $text = substr($text, 1, strlen($text) - 2); + + // Convert CSS unicode sequences to XML unicode. + $text = preg_replace('~\\\\([[:xdigit:]]{2,6})~', '&#x$1;', $text); + + // Remove excape slashes and encode meta entities. + $text = htmlentities(stripslashes($text), ENT_QUOTES, 'UTF-8', false); + $element->data['text'] = $text; + + $element->svg_attrs['width'] = $element->data['width']; + $element->svg_attrs['height'] = $element->data['height']; +} + + + +/* + Star/polygon path builder. + + Adapted from http://svg-whiz.com/svg/StarMaker.svg by Doug Schepers. +*/ +function svg_starpath($cx, $cy, $points, $outer_r, $inner_r = null, $twist = 0, $orient = 'point') { + + $d = []; + + // Enforce minimum number of points. + $points = max(3, $points); + + for ($s = 0; $points >= $s; $s++) { + + // Outer angle. + $outer_angle = 2.0 * M_PI * ($s / $points); + + if ($orient === 'point') { + $outer_angle -= (M_PI / 2); + } + elseif ($orient === 'edge') { + $outer_angle = ($outer_angle + (M_PI / $points)) - (M_PI / 2); + } + + // Outer point based on outer angle. + $x = ( $outer_r * cos($outer_angle) ) + $cx; + $y = ( $outer_r * sin($outer_angle) ) + $cy; + + if ($points != $s) { + $d[] = "$x $y"; + } + + // If star shape is required need inner angles too. + if ($inner_r != null && $points != $s) { + + $inner_angle = (2 * M_PI * ($s / $points)) + (M_PI / $points); + + if ($orient === 'point') { + $inner_angle -= (M_PI / 2); + } + $inner_angle += $twist; + + $ix = ( $inner_r * cos($inner_angle) ) + $cx; + $iy = ( $inner_r * sin($inner_angle) ) + $cy; + + $d[] = "$ix $iy"; + } + } + + return 'M' . implode('L', $d) . 'Z'; +} + +function svg_apply_filters($element) { + + if (isset($element->data['drop-shadow'])) { + + $parts = svg_parselist($element->data['drop-shadow'], false); + + list($ds_x, $ds_y, $ds_strength, $ds_color) = $parts += [ + 2, // x offset. + 2, // y offset. + 2, // strength. + 'black', // color. + ]; + + // Opacity. + $drop_shadow_opacity = null; + if ($color_components = Color::colorSplit($ds_color)) { + list($ds_color, $drop_shadow_opacity) = $color_components; + } + + $filter = ''; + $filter .= ""; + $filter .= ""; + $filter .= ""; + $filter .= ""; + if (isset($drop_shadow_opacity)) { + $filter .= ''; + $filter .= ""; + $filter .= ''; + } + $filter .= ''; + $filter .= ''; + $filter .= ''; + $filter .= ''; + $filter .= ''; + + $element->styles['filter'] = 'url(#f)'; + $element->filters[] = $filter; + } +} + +function svg_preprocess($element) { + + if (isset($element->data['margin'])) { + + $margin =& $element->data['margin']; + + $parts = svg_parselist($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 { + $element->data['margin'] = [0, 0, 0, 0]; + } + + // 'Unzip' string tokens on data attributes. + foreach (['points', 'd'] as $point_data_attr) { + + if (isset($element->attrs[$point_data_attr])) { + + $value = $element->attrs[$point_data_attr]; + + if (Tokens::is($value, 's')) { + $element->attrs[$point_data_attr] = + trim(Crush::$process->tokens->get($value), '"\'');; + } + } + } + + if (isset($element->data['width'])) { + $element->svg_attrs['width'] = $element->data['width']; + } + if (isset($element->data['height'])) { + $element->svg_attrs['height'] = $element->data['height']; + } +} + +function svg_apply_css_funcs($element, &$raw_data) { + + // Setup functions for using on values. + // Note using custom versions of svg-*-gradient(). + static $functions; + if (! $functions) { + $functions = new \stdClass(); + $functions->fill = new Functions([ + 'svg-linear-gradient' => 'CssCrush\svg_fn_linear_gradient', + 'svg-radial-gradient' => 'CssCrush\svg_fn_radial_gradient', + 'pattern' => 'CssCrush\svg_fn_pattern', + ]); + + $functions->generic = new Functions(array_diff_key(Crush::$process->functions->register, $functions->fill->register)); + } + + foreach ($raw_data as $property => &$value) { + $value = $functions->generic->apply($value); + + // Only capturing fills for fill and stoke properties. + if ($property === 'fill' || $property === 'stroke') { + $value = $functions->fill->apply($value, $element); + + // If the value is a color with alpha component we split the color + // and set the corresponding *-opacity property because Webkit doesn't + // support rgba()/hsla() in SVG. + if ($components = Color::colorSplit($value)) { + list($color, $opacity) = $components; + $raw_data[$property] = $color; + if ($opacity < 1) { + $raw_data += ["$property-opacity" => $opacity]; + } + } + } + } +} + +function svg_compress($element) { + + foreach ($element->attrs as $key => &$value) { + + // Compress numbers on data attributes. + if (in_array($key, ['points', 'd'])) { + $value = preg_replace_callback( + Regex::$patt->number, + function ($m) { return round($m[0], 2); }, + $value); + } + } +} + +function svg_render($element) { + + // Flatten styles. + $styles = ''; + $styles_data = [ + '@font-face' => $element->face_styles, + 'svg' => $element->svg_styles, + ]; + foreach ($styles_data as $selector => $declarations) { + if ($declarations) { + $out = []; + foreach ($declarations as $property => $value) { + $out[] = "$property:$value"; + } + $styles .= $selector . '{' . implode(';', $out) . '}'; + } + } + $styles = Crush::$process->tokens->restore($styles, ['u', 's'], true); + + // Add element styles as attributes which tend to work better with svg2png converters. + $attrs = Util::htmlAttributes($element->attrs + $element->styles); + + // Add viewbox to help IE scale correctly. + if (isset($element->svg_attrs['width']) && isset($element->svg_attrs['height'])) { + $element->svg_attrs += [ + 'viewbox' => implode(' ', [ + 0, + 0, + $element->svg_attrs['width'], + $element->svg_attrs['height'] + ]), + ]; + } + $svg_attrs = Util::htmlAttributes($element->svg_attrs); + + // Markup. + $svg[] = ""; + + if ( + $element->fills['gradients'] || + $element->fills['patterns'] || + $element->filters || + $styles + ) { + $svg[] = ''; + $svg[] = implode($element->fills['gradients']); + $svg[] = implode($element->fills['patterns']); + $svg[] = implode($element->filters); + if ($styles) { + $cdata = preg_match('~[<>&]~', $styles); + $svg[] = ''; + } + $svg[] = ''; + } + + if ($element->tag === 'text') { + $svg[] = "{$element->data['text']}"; + } + else { + $svg[] = "<{$element->tag}$attrs/>"; + } + $svg[] = ''; + + return array_filter($svg, 'strlen'); +} + + +/* + Custom versions of svg-*-gradient() for integrating. +*/ +function svg_fn_linear_gradient($input, $element) { + + $generated_gradient = create_svg_linear_gradient($input); + $element->fills['gradients'][] = reset($generated_gradient); + + return 'url(#' . key($generated_gradient) . ')'; +} + +function svg_fn_radial_gradient($input, $element) { + + $generated_gradient = create_svg_radial_gradient($input); + $element->fills['gradients'][] = reset($generated_gradient); + + return 'url(#' . key($generated_gradient) . ')'; +} + +function svg_fn_pattern($input, $element) { + + $pid = 'p'; + + // Get args in order with defaults. + list($url, $transform_list, $width, $height, $x, $y) = + Functions::parseArgs($input) + + ['', '', 0, 0, 0, 0]; + + $url = Crush::$process->tokens->get($url); + if (! $url) { + return ''; + } + + // If $width or $height is not specified get image dimensions the slow way. + if (! $width || ! $height) { + $file = $url->getAbsolutePath(); + list($width, $height) = getimagesize($file); + } + + // If a data-uri function has been used. + if ($url->convertToData) { + $url->toData(); + } + + $transform_list = $transform_list ? " patternTransform=\"$transform_list\"" : ''; + $generated_pattern = ""; + $generated_pattern .= "value}\" x=\"$x\" y=\"$y\" width=\"$width\" height=\"$height\"/>"; + $generated_pattern .= ''; + + $element->fills['patterns'][] = $generated_pattern; + $element->svg_attrs['xmlns:xlink'] = "http://www.w3.org/1999/xlink"; + + return 'url(#' . $pid . ')'; +} + + +/* + Helpers. +*/ +function svg_parselist($str, $numbers = true) { + $list = preg_split('~ +~', trim($str)); + return $numbers ? array_map('floatval', $list) : $list; +} + +function svg_ifset(&$var, $fallback = null) { + if (isset($var)) { + return $var; + } + return $fallback; +} + + +/* + SVG gradients. +*/ +function fn__svg_linear_gradient($input) { + + $gradient = create_svg_linear_gradient($input); + $gradient_markup = reset($gradient); + $gradient_id = key($gradient); + + $svg = ''; + $svg .= ''; + $svg .= $gradient_markup; + $svg .= ''; + $svg .= ""; + $svg .= ''; + + return Crush::$process->tokens->add(new Url('data:image/svg+xml;base64,' . base64_encode($svg))); +} + + +function fn__svg_radial_gradient($input) { + + $gradient = create_svg_radial_gradient($input); + $gradient_markup = reset($gradient); + $gradient_id = key($gradient); + + $svg = ''; + $svg .= ''; + $svg .= $gradient_markup; + $svg .= ''; + $svg .= ""; + $svg .= ''; + + return Crush::$process->tokens->add(new Url('data:image/svg+xml;base64,' . base64_encode($svg))); +} + + +function create_svg_linear_gradient($input) { + + static $angle_keywords, $deg_patt; + if (! $angle_keywords) { + $angle_keywords = [ + 'to top' => 180, + 'to right' => 270, + 'to bottom' => 0, + 'to left' => 90, + // Not very magic corners. + 'to top right' => [[0, 100], [100, 0]], + 'to top left' => [[100, 100], [0, 0]], + 'to bottom right' => [[0, 0], [100, 100]], + 'to bottom left' => [[100, 0], [0, 100]], + ]; + $angle_keywords['to right top'] = $angle_keywords['to top right']; + $angle_keywords['to left top'] = $angle_keywords['to top left']; + $angle_keywords['to right bottom'] = $angle_keywords['to bottom right']; + $angle_keywords['to left bottom'] = $angle_keywords['to bottom left']; + + $deg_patt = Regex::make('~^{{number}}deg$~i'); + } + + $args = Functions::parseArgs($input); + + // If no angle argument is passed the default. + $angle = 0; + + // Parse starting and ending coordinates from the first argument if it's an angle. + $coords = null; + $first_arg = $args[0]; + $first_arg_is_angle = false; + + // Try to parse an angle value. + if (preg_match($deg_patt, $first_arg)) { + $angle = floatval($first_arg); + + // Quick fix to match standard linear-gradient() angle. + $angle += 180; + $first_arg_is_angle = true; + } + elseif (isset($angle_keywords[$first_arg])) { + if (is_array($angle_keywords[$first_arg])) { + $coords = $angle_keywords[$first_arg]; + } + else { + $angle = $angle_keywords[$first_arg]; + } + $first_arg_is_angle = true; + } + + // Shift off the first argument if it has been recognised as an angle. + if ($first_arg_is_angle) { + array_shift($args); + } + + // If not using a magic corner, create start/end coordinates from the angle. + if (! $coords) { + + // Normalize the angle. + $angle = fmod($angle, 360); + if ($angle < 0) { + $angle = 360 + $angle; + } + $angle = round($angle, 2); + + $start_x = 0; + $end_x = 0; + $start_y = 0; + $end_y = 100; + + if ($angle >= 0 && $angle <= 45) { + $start_x = (($angle / 45) * 50) + 50; + $end_x = 100 - $start_x; + $start_y = 0; + $end_y = 100; + } + elseif ($angle > 45 && $angle <= 135) { + $angle_delta = $angle - 45; + $start_x = 100; + $end_x = 0; + $start_y = ($angle_delta / 90) * 100; + $end_y = 100 - $start_y; + } + elseif ($angle > 135 && $angle <= 225) { + $angle_delta = $angle - 135; + $start_x = 100 - (($angle_delta / 90) * 100); + $end_x = 100 - $start_x; + $start_y = 100; + $end_y = 0; + } + elseif ($angle > 225 && $angle <= 315) { + $angle_delta = $angle - 225; + $start_x = 0; + $end_x = 100; + $start_y = 100 - (($angle_delta / 90) * 100); + $end_y = 100 - $start_y; + } + elseif ($angle > 315 && $angle <= 360) { + $angle_delta = $angle - 315; + $start_x = ($angle_delta / 90) * 100; + $end_x = 100 - $start_x; + $start_y = 0; + $end_y = 100; + } + $coords = [ + [round($start_x, 1), round($start_y, 1)], + [round($end_x, 1), round($end_y, 1)], + ]; + } + + // The remaining arguments are treated as color stops. + // - Capture their color values and if specified color offset percentages. + // - Only percentages are supported as SVG gradients to accept other length values + // for color stop offsets. + $color_stops = parse_gradient_color_stops($args); + + $gradient_id = "lg"; + $gradient = ""; + $gradient .= $color_stops; + $gradient .= ''; + + return [$gradient_id => $gradient]; +} + + +function create_svg_radial_gradient($input) { + + static $position_keywords, $origin_patt; + if (! $position_keywords) { + $position_keywords = [ + 'at top' => ['50%', '0%'], + 'at right' => ['100%', '50%'], + 'at bottom' => ['50%', '100%'], + 'at left' => ['0%', '50%'], + 'at center' => ['50%', '50%'], + // Not very magic corners. + 'at top right' => ['100%', '0%'], + 'at top left' => ['0%', '0%'], + 'at bottom right' => ['100%', '100%'], + 'at bottom left' => ['0%', '100%'], + ]; + $position_keywords['at right top'] = $position_keywords['at top right']; + $position_keywords['at left top'] = $position_keywords['at top left']; + $position_keywords['at right bottom'] = $position_keywords['at bottom right']; + $position_keywords['at left bottom'] = $position_keywords['at bottom left']; + + $origin_patt = Regex::make('~^({{number}}%?) +({{number}}%?)$~'); + } + + $args = Functions::parseArgs($input); + + // Default origin, + $position = $position_keywords['at center']; + + // Parse origin coordinates from the first argument if it's an origin. + $first_arg = $args[0]; + $first_arg_is_position = false; + + // Try to parse an origin value. + if (preg_match($origin_patt, $first_arg, $m)) { + $position = [$m[1], $m[2]]; + $first_arg_is_position = true; + } + elseif (isset($position_keywords[$first_arg])) { + $position = $position_keywords[$first_arg]; + $first_arg_is_position = true; + } + + // Shift off the first argument if it has been recognised as an origin. + if ($first_arg_is_position) { + array_shift($args); + } + + // The remaining arguments are treated as color stops. + // - Capture their color values and if specified color offset percentages. + // - Only percentages are supported as SVG gradients to accept other length values + // for color stop offsets. + $color_stops = parse_gradient_color_stops($args); + + $gradient_id = "rg"; + $gradient = ""; + $gradient .= $color_stops; + $gradient .= ''; + + return [$gradient_id => $gradient]; +} + + +function parse_gradient_color_stops(array $color_stop_args) { + + $offsets = []; + $colors = []; + $offset_patt = '~ +([\d\.]+%)$~'; + $last_index = count($color_stop_args) - 1; + + foreach ($color_stop_args as $index => $color_arg) { + + if (preg_match($offset_patt, $color_arg, $m)) { + $offsets[] = floatval($m[1]); + $color = preg_replace($offset_patt, '', $color_arg); + } + else { + if ($index === 0) { + $offsets[] = 0; + } + elseif ($index === $last_index) { + $offsets[] = 100; + } + else { + $offsets[] = null; + } + $color = $color_arg; + } + + // For hsla()/rgba() extract alpha component from color values and + // convert to hsl()/rgb(). + // Webkit doesn't support them for SVG colors. + $colors[] = Color::colorSplit($color); + } + + // For unspecified color offsets fill in the blanks. + $next_index_not_null = 0; + $prev_index_not_null = 0; + $n = count($offsets); + + foreach ($offsets as $index => $offset) { + + if (! isset($offset)) { + + // Scan for next non-null offset. + for ($i = $index; $i < $n; $i++) { + if (isset($offsets[$i])) { + $next_index_not_null = $i; + break; + } + } + + // Get the difference between previous 'not null' offset and the next 'not null' offset. + // Divide by the number of null offsets to get a value for padding between them. + $padding_increment = + ($offsets[$next_index_not_null] - $offsets[$prev_index_not_null]) / + ($next_index_not_null - $index + 1); + $padding = $padding_increment; + + for ($i = $index; $i < $n; $i++) { + if (isset($offsets[$i])) { + break; + } + // Replace the null offset with the new padded value. + $offsets[$i] = $offsets[$prev_index_not_null] + $padding; + // Bump the padding for the next null offset. + $padding += $padding_increment; + } + } + else { + $prev_index_not_null = $index; + } + } + + $stops = ''; + foreach (array_combine($offsets, $colors) as $offset => $color) { + list($color_value, $opacity) = $color; + $stop_opacity = $opacity < 1 ? " stop-opacity=\"$opacity\"" : ''; + $stops .= ""; + } + + return $stops; +}