├── .eslintrc.cjs ├── .github └── workflows │ └── php.yml ├── CHANGELOG.md ├── CssCrush.php ├── LICENSE.txt ├── README.md ├── aliases.ini ├── bin └── csscrush ├── boilerplate.txt ├── cli.php ├── composer.json ├── docs ├── README.md ├── api │ ├── functions.md │ └── options.md ├── core │ ├── abstract.md │ ├── auto-prefixing.md │ ├── direct-import.md │ ├── fragments.md │ ├── functions │ │ ├── a-adjust.md │ │ ├── data-uri.md │ │ ├── h-adjust.md │ │ ├── hsl-adjust.md │ │ ├── hsla-adjust.md │ │ ├── l-adjust.md │ │ ├── math.md │ │ ├── query.md │ │ ├── s-adjust.md │ │ └── this.md │ ├── inheritance.md │ ├── loop.md │ ├── mixins.md │ ├── nesting.md │ ├── selector-aliases.md │ ├── selector-grouping.md │ └── variables.md ├── getting-started │ ├── js.md │ └── php.md └── plugins │ ├── aria.md │ ├── canvas.md │ ├── ease.md │ ├── forms.md │ ├── hocus-pocus.md │ ├── property-sorter.md │ ├── svg-gradients.md │ └── svg.md ├── js ├── index.d.ts └── index.js ├── jsconfig.json ├── lib ├── CssCrush │ ├── BalancedMatch.php │ ├── Collection.php │ ├── Color.php │ ├── Crush.php │ ├── Declaration.php │ ├── DeclarationList.php │ ├── EventEmitter.php │ ├── ExtendArg.php │ ├── File.php │ ├── Fragment.php │ ├── Functions.php │ ├── IO.php │ ├── IO │ │ └── Watch.php │ ├── Importer.php │ ├── Iterator.php │ ├── Logger.php │ ├── Mixin.php │ ├── Options.php │ ├── PostAliasFix.php │ ├── Process.php │ ├── Regex.php │ ├── Rule.php │ ├── Selector.php │ ├── SelectorAlias.php │ ├── SelectorList.php │ ├── StringObject.php │ ├── Template.php │ ├── Tokens.php │ ├── Url.php │ ├── Util.php │ └── Version.php └── functions.php ├── misc ├── color-keywords.ini ├── formatters.php └── property-sorting.ini ├── package-lock.json ├── package.json ├── phpstan.neon └── plugins ├── aria.php ├── canvas.php ├── ease.php ├── forms.php ├── hocus-pocus.php ├── property-sorter.php └── svg.php /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /*eslint sort-keys: 2*/ 2 | /*eslint object-property-newline: 2*/ 3 | /*eslint quote-props: [2, "consistent"]*/ 4 | 5 | module.exports = { 6 | env: { 7 | es6: true, 8 | node: true, 9 | }, 10 | extends: 'eslint:recommended', 11 | parserOptions: { 12 | ecmaVersion: 'latest', 13 | sourceType: 'module', 14 | }, 15 | rules: { 16 | 'array-bracket-spacing': [2, 'never'], 17 | 'array-element-newline': [2, 'consistent'], 18 | 'arrow-parens': [2, 'as-needed'], 19 | 'arrow-spacing': 2, 20 | 'brace-style': [2, 'stroustrup'], 21 | 'camelcase': [2, { 22 | ignoreDestructuring: true, 23 | properties: 'never', 24 | }], 25 | 'comma-dangle': [2, { 26 | arrays: 'always-multiline', 27 | functions: 'never', 28 | objects: 'always-multiline', 29 | }], 30 | 'comma-spacing': [2, { 31 | after: true, 32 | before: false, 33 | }], 34 | 'curly': 2, 35 | 'eol-last': [2, 'always'], 36 | 'eqeqeq': 2, 37 | 'key-spacing': [2, { 38 | afterColon: true, 39 | beforeColon: false, 40 | }], 41 | 'keyword-spacing': 2, 42 | 'linebreak-style': [2, 'unix'], 43 | 'multiline-comment-style': [2, 'starred-block'], 44 | 'no-console': 2, 45 | 'no-dupe-keys': 2, 46 | 'no-else-return': 2, 47 | 'no-empty': [2, { 48 | allowEmptyCatch: true, 49 | }], 50 | 'no-lonely-if': 2, 51 | 'no-multi-spaces': 2, 52 | 'no-multiple-empty-lines': [2, { 53 | max: 2, 54 | maxBOF: 1, 55 | maxEOF: 1, 56 | }], 57 | 'no-new-object': 2, 58 | 'no-template-curly-in-string': 2, 59 | 'no-tabs': 2, 60 | 'no-throw-literal': 2, 61 | 'no-trailing-spaces': 2, 62 | 'no-unneeded-ternary': 2, 63 | 'no-unused-expressions': [2, {allowShortCircuit: true}], 64 | 'no-unused-vars': [2, { 65 | args: 'all', 66 | argsIgnorePattern: '^(req|res|next)$|^_', 67 | varsIgnorePattern: '^_$', 68 | }], 69 | 'no-useless-call': 2, 70 | 'no-useless-concat': 2, 71 | 'no-useless-return': 2, 72 | 'no-var': 2, 73 | 'object-curly-newline': [2, {consistent: true}], 74 | 'object-curly-spacing': [2, 'never'], 75 | 'object-shorthand': [2, 'properties'], 76 | 'operator-linebreak': [2, 'before', { 77 | overrides: { 78 | ':': 'ignore', 79 | '?': 'ignore', 80 | }, 81 | }], 82 | 'prefer-arrow-callback': 2, 83 | 'prefer-const': [2, {destructuring: 'all'}], 84 | 'prefer-destructuring': [2, { 85 | array: false, 86 | object: true, 87 | }], 88 | 'prefer-object-spread': 2, 89 | 'quote-props': [2, 'as-needed'], 90 | 'quotes': [2, 'single', { 91 | allowTemplateLiterals: true, 92 | avoidEscape: true, 93 | }], 94 | 'semi': [2, 'always'], 95 | 'space-before-blocks': 2, 96 | 'space-before-function-paren': [2, { 97 | anonymous: 'always', 98 | asyncArrow: 'always', 99 | named: 'never', 100 | }], 101 | 'space-unary-ops': [2, { 102 | nonwords: false, 103 | overrides: {'!': true}, 104 | words: true, 105 | }], 106 | 'yoda': 2, 107 | }, 108 | }; 109 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | 10 | php-api: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | php-versions: ['8.1', '8.4'] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: ${{ matrix.php-versions }} 22 | tools: composer:v2 23 | - name: Validate composer.json and composer.lock 24 | run: composer validate --strict 25 | - name: Install dependencies 26 | run: composer install --prefer-dist --no-progress 27 | - name: Lint 28 | run: composer run-script lint 29 | - name: Test 30 | run: composer run-script test 31 | 32 | js-api: 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | - uses: actions/setup-node@v3 38 | with: 39 | node-version: 18 40 | - name: Install dependencies 41 | run: npm install 42 | - name: Lint 43 | run: npm run lint 44 | - name: Check JS 45 | run: npm run types 46 | - name: Test 47 | run: npm run test 48 | -------------------------------------------------------------------------------- /CssCrush.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | A CSS preprocessor designed to enable a modern and uncluttered CSS workflow. 6 | 7 | * Automatic vendor prefixing 8 | * Variables 9 | * Import inlining 10 | * Nesting 11 | * Functions (color manipulation, math, data-uris etc.) 12 | * Rule inheritance (@extends) 13 | * Mixins 14 | * Minification 15 | * Lightweight plugin system 16 | * Source maps 17 | 18 | See the [docs](http://the-echoplex.net/csscrush) for full details. 19 | 20 | ******************************** 21 | 22 | ## Setup (PHP) 23 | 24 | If you're using [Composer](http://getcomposer.org) you can use Crush in your project with the following line in your terminal: 25 | 26 | ```shell 27 | composer require css-crush/css-crush:dev-master 28 | ``` 29 | 30 | If you're not using Composer yet just download the library into a convenient location and require the bootstrap file: 31 | 32 | ```php 33 | 34 | ``` 35 | 36 | ## Basic usage (PHP) 37 | 38 | ```php 39 | 44 | ``` 45 | 46 | Compiles the CSS file and outputs the following link tag: 47 | 48 | ```html 49 | 50 | ``` 51 | 52 | There are several other [functions](http://the-echoplex.net/csscrush#api) for working with files and strings of CSS: 53 | 54 | * `csscrush_file($file, $options)` - Returns a URL of the compiled file. 55 | * `csscrush_string($css, $options)` - Compiles a raw string of css and returns the resulting css. 56 | * `csscrush_inline($file, $options, $tag_attributes)` - Returns compiled css in an inline style tag. 57 | 58 | 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. 59 | 60 | ******************************** 61 | 62 | ## Setup (JS) 63 | 64 | ```shell 65 | npm install csscrush 66 | ``` 67 | 68 | ## Basic usage (JS) 69 | 70 | ```js 71 | // All methods can take the standard options (camelCase) as the second argument. 72 | const csscrush = require('csscrush'); 73 | 74 | // Compile. Returns promise. 75 | csscrush.file('./styles.css', {sourceMap: true}); 76 | 77 | // Compile string of CSS. Returns promise. 78 | csscrush.string('* {box-sizing: border-box;}'); 79 | 80 | // Compile and watch file. Returns event emitter (triggers 'data' on compile). 81 | csscrush.watch('./styles.css'); 82 | ``` 83 | 84 | ******************************** 85 | 86 | ## Contributing 87 | 88 | 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. 89 | 90 | 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. 91 | 92 | [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)). 93 | 94 | 95 | ## Licence 96 | 97 | MIT 98 | -------------------------------------------------------------------------------- /aliases.ini: -------------------------------------------------------------------------------- 1 | ;---------------------------------------------------------------- 2 | ; 3 | ; Sources: 4 | ; http://developer.mozilla.org/en-US/docs/CSS/CSS_Reference 5 | ; http://caniuse.com/#cats=CSS 6 | ; 7 | ;---------------------------------------------------------------- 8 | ; Property aliases. 9 | 10 | [properties] 11 | 12 | ; Animations. 13 | animation[] = -webkit-animation 14 | animation-delay[] = -webkit-animation-delay 15 | animation-direction[] = -webkit-animation-direction 16 | animation-duration[] = -webkit-animation-duration 17 | animation-fill-mode[] = -webkit-animation-fill-mode 18 | animation-iteration-count[] = -webkit-animation-iteration-count 19 | animation-name[] = -webkit-animation-name 20 | animation-play-state[] = -webkit-animation-play-state 21 | animation-timing-function[] = -webkit-animation-timing-function 22 | 23 | ; Backdrop filter. 24 | backdrop-filter[] = -webkit-backdrop-filter 25 | 26 | ; Backface visibility. 27 | backface-visibility[] = -webkit-backface-visibility 28 | 29 | ; Border-image. 30 | border-image[] = -webkit-border-image 31 | 32 | ; Box decoration. 33 | box-decoration-break[] = -webkit-box-decoration-break 34 | 35 | ; Filter. 36 | filter[] = -webkit-filter 37 | 38 | ; Hyphens. 39 | hyphens[] = -webkit-hyphens 40 | 41 | ; Tab size. 42 | tab-size[] = -moz-tab-size 43 | tab-size[] = -o-tab-size 44 | 45 | ; User select (non standard). 46 | user-select[] = -webkit-user-select 47 | user-select[] = -moz-user-select 48 | 49 | 50 | ;---------------------------------------------------------------- 51 | ; Declaration aliases. 52 | 53 | [declarations] 54 | 55 | ; Experimental width values. 56 | width:max-content[] = width:intrinsic 57 | width:max-content[] = width:-webkit-max-content 58 | width:max-content[] = width:-moz-max-content 59 | width:min-content[] = width:-webkit-min-content 60 | width:min-content[] = width:-moz-min-content 61 | width:available[] = width:-webkit-available 62 | width:available[] = width:-moz-available 63 | width:fit-content[] = width:-webkit-fit-content 64 | width:fit-content[] = width:-moz-fit-content 65 | 66 | max-width:max-content[] = max-width:intrinsic 67 | max-width:max-content[] = max-width:-webkit-max-content 68 | max-width:max-content[] = max-width:-moz-max-content 69 | max-width:min-content[] = max-width:-webkit-min-content 70 | max-width:min-content[] = max-width:-moz-min-content 71 | max-width:available[] = max-width:-webkit-available 72 | max-width:available[] = max-width:-moz-available 73 | max-width:fit-content[] = max-width:-webkit-fit-content 74 | max-width:fit-content[] = max-width:-moz-fit-content 75 | 76 | min-width:max-content[] = min-width:intrinsic 77 | min-width:max-content[] = min-width:-webkit-max-content 78 | min-width:max-content[] = min-width:-moz-max-content 79 | min-width:min-content[] = min-width:-webkit-min-content 80 | min-width:min-content[] = min-width:-moz-min-content 81 | min-width:available[] = min-width:-webkit-available 82 | min-width:available[] = min-width:-moz-available 83 | min-width:fit-content[] = min-width:-webkit-fit-content 84 | min-width:fit-content[] = min-width:-moz-fit-content 85 | 86 | ; Appearance (non-standard). 87 | appearance:none[] = -webkit-appearance:none 88 | appearance:none[] = -moz-appearance:none 89 | 90 | position:sticky[] = position:-webkit-sticky 91 | 92 | 93 | ;---------------------------------------------------------------- 94 | ; Function aliases. 95 | 96 | [functions] 97 | 98 | 99 | ;---------------------------------------------------------------- 100 | ; @rule aliases. 101 | 102 | [at-rules] 103 | -------------------------------------------------------------------------------- /bin/csscrush: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | =8.1" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "9.6.23", 27 | "psr/log": "1.0.*@dev", 28 | "phpstan/phpstan": "^1.10", 29 | "twig/twig": "3.11.3" 30 | }, 31 | "bin": [ 32 | "bin/csscrush" 33 | ], 34 | "autoload": { 35 | "psr-0": { "CssCrush": "lib/" }, 36 | "files": [ "lib/functions.php" ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # CSS-Crush Documentation 2 | 3 | Rendered online at http://the-echoplex.net/csscrush 4 | -------------------------------------------------------------------------------- /docs/api/functions.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## csscrush_file() 8 | 9 | Process CSS file and return the compiled file URL. 10 | 11 | csscrush_file( string $file [, array [$options](#api--options) ] ) 12 | 13 | 14 | *************** 15 | 16 | ## csscrush_tag() 17 | 18 | Process CSS file and return an html `link` tag with populated href. 19 | 20 | csscrush_tag( string $file [, array [$options](#api--options) [, array $tag\_attributes ]] ) 21 | 22 | 23 | *************** 24 | 25 | ## csscrush_inline() 26 | 27 | Process CSS file and return CSS as text wrapped in html `style` tags. 28 | 29 | csscrush_inline( string $file [, array [$options](#api--options) [, array $tag\_attributes ]] ) 30 | 31 | 32 | *************** 33 | 34 | ## csscrush_string() 35 | 36 | Compile a raw string of CSS string and return it. 37 | 38 | csscrush_string( string $string [, array [$options](#api--options) ] ) 39 | 40 | 41 | *************** 42 | 43 | ## csscrush_get() 44 | 45 | Retrieve a config setting or option default. 46 | 47 | `csscrush_get( string $object_name, string $property )` 48 | 49 | ### Parameters 50 | 51 | * `$object_name` Name of object you want to inspect: 'config' or 'options'. 52 | * `$property` 53 | 54 | 55 | *************** 56 | 57 | ## csscrush_set() 58 | 59 | Set a config setting or option default. 60 | 61 | `csscrush_set( string $object_name, mixed $settings )` 62 | 63 | ### Parameters 64 | 65 | * `$object_name` Name of object you want to modify: 'config' or 'options'. 66 | * `$settings` Associative array of keys and values to set, or callable which argument is the object specified in `$object_name`. 67 | 68 | 69 | *************** 70 | 71 | ## csscrush_plugin() 72 | 73 | Register a plugin. 74 | 75 | `csscrush_plugin( string $name, callable $callback )` 76 | 77 | 78 | *************** 79 | 80 | ## csscrush_stat() 81 | 82 | Get compilation stats from the most recent compiled file. 83 | 84 | `csscrush_stat()` 85 | -------------------------------------------------------------------------------- /docs/api/options.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 13 | 14 | 18 | 19 | 23 | 24 | 28 | 29 | 33 | 34 | 38 | 39 | 43 | 44 | 48 | 49 | 53 | 54 | 58 | 59 | 63 | 64 | 68 | 69 | 73 | 74 | 78 | 79 | 83 | 84 | 88 | 89 | 93 | 94 | 98 | 99 | 103 |
Option 10 | Values (default in bold) 11 | Description 12 |
minify 15 | true | false | Array 16 | 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. 17 |
formatter 20 | block | single-line | padded 21 | Set the formatting mode. Overrides minify option if both are set. 22 |
newlines 25 | use-platform | windows/win | unix 26 | Set the output style of newlines 27 |
boilerplate 30 | true | false | Path 31 | Prepend a boilerplate to the output file 32 |
versioning 35 | true | false 36 | Append a timestamped querystring to the output filename 37 |
vars 40 | Array 41 | An associative array of CSS variables to be applied at runtime. These will override variables declared globally or in the CSS. 42 |
cache 45 | true | false 46 | Turn caching on or off. 47 |
output_dir 50 | Path 51 | Specify an output directory for compiled files. Defaults to the same directory as the host file. 52 |
output_file 55 | Output filename 56 | Specify an output filename (suffix is added). 57 |
asset_dir 60 | Path 61 | Directory for SVG and image files generated by plugins (defaults to the main file output directory). 62 |
stat_dump 65 | false | true | Path 66 | Save compile stats and variables to a file in json format. 67 |
vendor_target 70 | "all" | "moz", "webkit", ... | Array 71 | Limit aliasing to a specific vendor, or an array of vendors. 72 |
rewrite_import_urls 75 | true | false | "absolute" 76 | Rewrite relative URLs inside inlined imported files. 77 |
import_paths 80 | Array 81 | Additional paths to search when resolving relative import URLs. 82 |
plugins 85 | Array 86 | An array of plugin names to enable. 87 |
source_map 90 | true | false 91 | Output a source map (compliant with the Source Map v3 proposal). 92 |
context 95 | Path 96 | Context for importing resources from relative urls (Only applies to `csscrush_string()` and command line utility). 97 |
doc_root 100 | Path 101 | Specify an alternative server document root for situations where the CSS is being served behind an alias or url rewritten path. 102 |
104 | -------------------------------------------------------------------------------- /docs/core/abstract.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 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. 8 | 9 | ```crush 10 | @abstract ellipsis { 11 | white-space: nowrap; 12 | overflow: hidden; 13 | text-overflow: ellipsis; 14 | } 15 | @abstract heading { 16 | font: bold 1rem serif; 17 | letter-spacing: .1em; 18 | } 19 | 20 | .foo { 21 | @extend ellipsis; 22 | display: block; 23 | } 24 | .bar { 25 | @extend ellipsis; 26 | @include heading; 27 | } 28 | ``` 29 | 30 | ```css 31 | .foo, 32 | .bar { 33 | white-space: nowrap; 34 | overflow: hidden; 35 | text-overflow: ellipsis; 36 | } 37 | .foo { 38 | display: block; 39 | } 40 | .bar { 41 | font: bold 1rem serif; 42 | letter-spacing: .1em; 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/core/auto-prefixing.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 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. 8 | 9 | 10 | ```crush 11 | .foo { 12 | background: linear-gradient(to right, red, white); 13 | } 14 | ``` 15 | 16 | ```css 17 | .foo { 18 | background: -webkit-linear-gradient(to right, red, white); 19 | background: linear-gradient(to right, red, white); 20 | } 21 | ``` 22 | 23 | 24 | ```crush 25 | @keyframes bounce { 26 | 50% { transform: scale(1.4); } 27 | } 28 | ``` 29 | 30 | ```css 31 | @-webkit-keyframes bounce { 32 | 50% {-webkit-transform: scale(1.4); 33 | transform: scale(1.4);} 34 | } 35 | @keyframes bounce { 36 | 50% {-webkit-transform: scale(1.4); 37 | transform: scale(1.4);} 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/core/direct-import.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 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. 8 | 9 | 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. 10 | 11 | 12 | ```crush 13 | /* Standard CSS @import statements */ 14 | @import "print.css" print; 15 | @import url( "small-screen.css" ) screen and ( max-width: 500px ); 16 | ``` 17 | 18 | ```css 19 | @media print { 20 | /* Contents of print.css */ 21 | } 22 | @media screen and ( max-width: 500px ) { 23 | /* Contents of small-screen.css */ 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/core/fragments.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Fragments – defined and invoked with the @fragment directive – work in a similar way to [mixins](#core--mixins), except that they work at block level: 8 | 9 | ```crush 10 | @fragment input-placeholder { 11 | #(1)::-webkit-input-placeholder { color: #(0); } 12 | #(1):-moz-placeholder { color: #(0); } 13 | #(1)::placeholder { color: #(0); } 14 | #(1).placeholder-state { color: #(0); } 15 | } 16 | 17 | @fragment input-placeholder(#777, textarea); 18 | ``` 19 | 20 | ```css 21 | textarea::-webkit-input-placeholder { color: #777; } 22 | textarea:-moz-placeholder { color: #777; } 23 | textarea::placeholder { color: #777; } 24 | textarea.placeholder-state { color: #777; } 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/core/functions/a-adjust.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Manipulate the opacity (alpha channel) of a color value. 8 | 9 | a-adjust( *color*, *offset* ) 10 | 11 | ## Parameters 12 | 13 | * *`color`* Any valid CSS color value 14 | * *`offset`* The percentage to offset the color opacity 15 | 16 | ## Returns 17 | 18 | The modified color value 19 | 20 | 21 | ## Examples 22 | 23 | ```css 24 | /* Reduce color opacity by 10% */ 25 | color: a-adjust( rgb(50,50,0) -10 ); 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/core/functions/data-uri.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Create a data-uri. 8 | 9 | data-uri( *url* ) 10 | 11 | ## Parameters 12 | 13 | * *`url`* URL of an asset 14 | 15 | `url` cannot be external, and must not be written with an http protocol prefix. 16 | 17 | The following file extensions are supported: jpg, jpeg, gif, png, svg, svgz, ttf, woff 18 | 19 | 20 | ## Returns 21 | 22 | The created data-uri as a string inside a CSS url(). 23 | 24 | 25 | ## Examples 26 | 27 | ```crush 28 | background: silver data-uri(../images/stripe.png); 29 | ``` 30 | 31 | ```css 32 | background: silver url(data:); 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/core/functions/h-adjust.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Adjust the hue of a color value. 8 | 9 | h-adjust( *color*, *offset* ) 10 | 11 | ## Parameters 12 | 13 | * *`color`* Any valid CSS color value 14 | * *`offset`* The percentage to offset the color hue (percent mark optional) 15 | 16 | ## Returns 17 | 18 | The modified color value. 19 | 20 | ## Examples 21 | 22 | ```css 23 | color: h-adjust( deepskyblue -10 ); 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/core/functions/hsl-adjust.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Manipulate the hue, saturation and lightness of a color value 8 | 9 | hsl-adjust( *color*, *hue-offset*, *saturation-offset*, *lightness-offset* ) 10 | 11 | ## Parameters 12 | 13 | * *`color`* Any valid CSS color value 14 | * *`hue-offset`* The percentage to offset the color hue 15 | * *`saturation-offset`* The percentage to offset the color saturation 16 | * *`lightness-offset`* The percentage to offset the color lightness 17 | 18 | ## Returns 19 | 20 | The modified color value 21 | 22 | ## Examples 23 | 24 | ```css 25 | /* Lighten and increase saturation */ 26 | color: hsl-adjust( red 0 5 5 ); 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/core/functions/hsla-adjust.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Manipulate the hue, saturation, lightness and opacity of a color value. 8 | 9 | hsla-adjust( *color*, *hue-offset*, *saturation-offset*, *lightness-offset*, *alpha-offset* ) 10 | 11 | ## Parameters 12 | 13 | * *`color`* Any valid CSS color value 14 | * *`hue-offset`* The percentage to offset the color hue 15 | * *`saturation-offset`* The percentage to offset the color saturation 16 | * *`lightness-offset`* The percentage to offset the color lightness 17 | * *`alpha-offset`* The percentage to offset the color opacity 18 | 19 | ## Returns 20 | 21 | The modified color value. 22 | 23 | ## Examples 24 | 25 | ```css 26 | color: hsla-adjust( #f00 0 5 5 -10 ); 27 | ``` -------------------------------------------------------------------------------- /docs/core/functions/l-adjust.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Adjust the lightness of a color value. 8 | 9 | l-adjust( *color*, *offset* ) 10 | 11 | ## Parameters 12 | 13 | * *`color`* Any valid CSS color value 14 | * *`offset`* The percentage to offset the color hue (percent mark optional) 15 | 16 | ## Returns 17 | 18 | The modified color value. 19 | 20 | ## Examples 21 | 22 | ```css 23 | color: l-adjust( deepskyblue 10 ); 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/core/functions/math.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Evaluate a raw mathematical expression. 8 | 9 | math( *expression* [, *unit*] ) 10 | 11 | ## Examples 12 | 13 | ```crush 14 | font-size: math( 12 / 16, em ); 15 | ``` 16 | 17 | ```css 18 | font-size: 0.75em; 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/core/functions/query.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Copy a value from another rule. 8 | 9 | query( *target* [, *property-name* = default] [, *fallback*] ) 10 | 11 | ## Parameters 12 | 13 | * *`target`* A rule selector, an abstract rule name or context keyword: `previous`, `next` (also `parent` and `top` within nested structures) 14 | * *`property-name`* The CSS property name to copy, or just `default` to pass over. Defaults to the calling property 15 | * *`fallback`* A CSS value to use if the target property does not exist 16 | 17 | 18 | ## Returns 19 | 20 | The referenced property value, or the fallback if it has not been set. 21 | 22 | 23 | ## Examples 24 | 25 | 26 | ```css 27 | .foo { 28 | width: 40em; 29 | height: 100em; 30 | } 31 | 32 | .bar { 33 | width: query( .foo ); /* 40em */ 34 | margin-top: query( .foo, height ); /* 100em */ 35 | margin-bottom: query( .foo, default, 3em ); /* 3em */ 36 | } 37 | ``` 38 | 39 | Using context keywords: 40 | 41 | ```css 42 | .foo { 43 | width: 40em; 44 | .bar { 45 | width: 30em; 46 | .baz: { 47 | width: query( parent ); /* 30em */ 48 | .qux { 49 | width: query( top ); /* 40em */ 50 | } 51 | } 52 | } 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/core/functions/s-adjust.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Adjust the saturation of a color value. 8 | 9 | s-adjust( *color*, *offset* ) 10 | 11 | ## Parameters 12 | 13 | * *`color`* Any valid CSS color value 14 | * *`offset`* The percentage to offset the color hue (percent mark optional) 15 | 16 | ## Returns 17 | 18 | The modified color value. 19 | 20 | ## Examples 21 | 22 | ```css 23 | /* Desaturate */ 24 | color: s-adjust( deepskyblue -100 ); 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/core/functions/this.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Reference another property value from the same containing block. 8 | 9 | Restricted to referencing properties that don't already reference other properties. 10 | 11 | this( *property-name*, *fallback* ) 12 | 13 | ## Parameters 14 | 15 | * *`property-name`* Property name 16 | * *`fallback`* A CSS value 17 | 18 | ## Returns 19 | 20 | The referenced property value, or the fallback if it has not been set. 21 | 22 | ## Examples 23 | 24 | ```css 25 | .foo { 26 | width: this( height ); 27 | height: 100em; 28 | } 29 | ``` 30 | 31 | ******** 32 | 33 | ```css 34 | /* The following both fail because they create circular references. */ 35 | .bar { 36 | height: this( width ); 37 | width: this( height ); 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/core/inheritance.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 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. 8 | 9 | [Abstract rules](#core--abstract) can be used if you just need to extend a generic set of declarations. 10 | 11 | ```crush 12 | .negative-text { 13 | overflow: hidden; 14 | text-indent: -9999px; 15 | } 16 | 17 | .sidebar-headline { 18 | @extend .negative-text; 19 | background: url( headline.png ) no-repeat; 20 | } 21 | ``` 22 | 23 | ```css 24 | .negative-text, 25 | .sidebar-headline { 26 | overflow: hidden; 27 | text-indent: -9999px; 28 | } 29 | 30 | .sidebar-headline { 31 | background: url( headline.png ) no-repeat; 32 | } 33 | ``` 34 | 35 | Inheritance is recursive: 36 | 37 | ```crush 38 | .one { color: pink; } 39 | .two { @extend .one; } 40 | .three { @extend .two; } 41 | .four { @extend .three; } 42 | ``` 43 | 44 | ```css 45 | .one, .two, .three, .four { color: pink; } 46 | ``` 47 | 48 | ## Referencing by name 49 | 50 | If you want to reference a rule without being concerned about later changes to the identifying selector use the `@name` directive: 51 | 52 | ```crush 53 | .foo123 { 54 | @name foo; 55 | text-decoration: underline; 56 | } 57 | 58 | .bar { 59 | @include foo; 60 | } 61 | .baz { 62 | @extend foo; 63 | } 64 | ``` 65 | 66 | 67 | ## Extending with pseudo classes/elements 68 | 69 | `@extend` arguments can adopt pseudo classes/elements by appending an exclamation mark: 70 | 71 | ```crush 72 | .link-base { 73 | color: #bada55; 74 | text-decoration: underline; 75 | } 76 | .link-base:hover, 77 | .link-base:focus { 78 | text-decoration: none; 79 | } 80 | 81 | .link-footer { 82 | @extend .link-base, .link-base:hover!, .link-base:focus!; 83 | color: blue; 84 | } 85 | ``` 86 | 87 | ```css 88 | .link-base, 89 | .link-footer { 90 | color: #bada55; 91 | text-decoration: underline; 92 | } 93 | 94 | .link-base:hover, 95 | .link-base:focus, 96 | .link-footer:hover, 97 | .link-footer:focus { 98 | text-decoration: none; 99 | } 100 | 101 | .link-footer { 102 | color: blue; 103 | } 104 | ``` 105 | 106 | The same outcome can also be achieved with an [Abstract rule](#core--abstract) wrapper to simplify repeated use: 107 | 108 | ```crush 109 | .link-base { 110 | color: #bada55; 111 | text-decoration: underline; 112 | } 113 | .link-base:hover, 114 | .link-base:focus { 115 | text-decoration: none; 116 | } 117 | 118 | @abstract link-base { 119 | @extend .link-base, .link-base:hover!, .link-base:focus!; 120 | } 121 | 122 | .link-footer { 123 | @extend link-base; 124 | color: blue; 125 | } 126 | ``` 127 | 128 | -------------------------------------------------------------------------------- /docs/core/loop.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | For...in loops with lists and generator functions. 8 | 9 | ```crush 10 | @for fruit in apple, orange, pear { 11 | .#(fruit) { 12 | background-image: url("images/#(fruit).jpg"); 13 | } 14 | } 15 | ``` 16 | 17 | ```css 18 | .apple { background-image: url(images/apple.jpg); } 19 | .orange { background-image: url(images/orange.jpg); } 20 | .pear { background-image: url(images/pear.jpg); } 21 | ``` 22 | 23 | ```crush 24 | @for base in range(2, 24) { 25 | @for i in range(1, #(base)) { 26 | .grid-#(i)-of-#(base) { 27 | width: math(#(i) / #(base) * 100, %); 28 | } 29 | } 30 | } 31 | ``` 32 | 33 | ```css 34 | .grid-1-of-2 { width: 50%; } 35 | .grid-2-of-2 { width: 100%; } 36 | /* 37 | Intermediate steps ommited. 38 | */ 39 | .grid-23-of-24 { width: 95.83333%; } 40 | .grid-24-of-24 { width: 100%; } 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/core/mixins.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Mixins make reusing small snippets of CSS much simpler. You define them with the `@mixin` directive. 8 | 9 | Positional arguments via the argument function `#()` extend the capability of mixins for repurposing in different contexts. 10 | 11 | ```crush 12 | @mixin display-font { 13 | font-family: "Arial Black", sans-serif; 14 | font-size: #(0); 15 | letter-spacing: #(1); 16 | } 17 | 18 | /* Another mixin with default arguments */ 19 | @mixin blue-theme { 20 | color: #(0 navy); 21 | background-image: url("images/#(1 cross-hatch).png"); 22 | } 23 | 24 | /* Applying the mixins */ 25 | .foo { 26 | @include display-font(100%, .1em), blue-theme; 27 | } 28 | ``` 29 | 30 | ```css 31 | .foo { 32 | font-family: "Arial Black", sans-serif; 33 | font-size: 100%; 34 | letter-spacing: .1em; 35 | color: navy; 36 | background-image: url("images/cross-hatch.png"); 37 | } 38 | ``` 39 | 40 | ## Skipping arguments 41 | 42 | Mixin arguments can be skipped by using the **default** keyword: 43 | 44 | ```crush 45 | @mixin display-font { 46 | font-size: #(0 100%); 47 | letter-spacing: #(1); 48 | } 49 | 50 | /* Applying the mixin skipping the first argument so the 51 | default value is used instead */ 52 | #foo { 53 | @include display-font(default, .3em); 54 | } 55 | ``` 56 | 57 | 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: 58 | 59 | ```crush 60 | @mixin square { 61 | width: #(0 10px); 62 | height: #(0); 63 | } 64 | 65 | .foo { 66 | @include square; 67 | } 68 | ``` 69 | 70 | ```css 71 | #foo { 72 | width: 10px; 73 | height: 10px; 74 | } 75 | ``` 76 | 77 | 78 | ## Mixing-in from other sources 79 | 80 | Normal rules and [abstract rules](#core--abstract) can also be used as static mixins without arguments: 81 | 82 | ```crush 83 | @abstract negative-text { 84 | text-indent: -9999px; 85 | overflow: hidden; 86 | } 87 | 88 | #main-content .theme-border { 89 | border: 1px solid maroon; 90 | } 91 | 92 | .foo { 93 | @include negative-text, #main-content .theme-border; 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /docs/core/nesting.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Rules can be nested to avoid repetitive typing when scoping to a common parent selector. 8 | 9 | ```crush 10 | .homepage { 11 | color: #333; 12 | background: white; 13 | .content { 14 | p { 15 | font-size: 110%; 16 | } 17 | } 18 | } 19 | ``` 20 | 21 | ```css 22 | .homepage { 23 | color: #333; 24 | background: white; 25 | } 26 | .homepage .content p { 27 | font-size: 110%; 28 | } 29 | ``` 30 | 31 | ## Parent referencing 32 | 33 | You can use the parent reference symbol `&` for placing the parent selector explicitly. 34 | 35 | ```crush 36 | .homepage { 37 | .no-js & { 38 | p { 39 | font-size: 110%; 40 | } 41 | } 42 | } 43 | ``` 44 | 45 | ```css 46 | .no-js .homepage p { 47 | font-size: 110%; 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/core/selector-aliases.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Selector aliases can be useful for grouping together common selector chains for reuse. 8 | 9 | They're defined with the `@selector` directive, and can be used anywhere you might use a pseudo class. 10 | 11 | 12 | ```crush 13 | @selector heading :any(h1, h2, h3, h4, h5, h6); 14 | @selector radio input[type="radio"]; 15 | @selector hocus :any(:hover, :focus); 16 | 17 | /* Selector aliases with arguments */ 18 | @selector class-prefix :any([class^="#(0)"], [class*=" #(0)"]); 19 | @selector col :class-prefix(-col); 20 | 21 | .sidebar :heading { 22 | color: honeydew; 23 | } 24 | 25 | :radio { 26 | margin-right: 4px; 27 | } 28 | 29 | :col { 30 | float: left; 31 | } 32 | 33 | p a:hocus { 34 | text-decoration: none; 35 | } 36 | ``` 37 | 38 | ```css 39 | .sidebar h1, .sidebar h2, 40 | .sidebar h3, .sidebar h4, 41 | .sidebar h5, .sidebar h6 { 42 | color: honeydew; 43 | } 44 | 45 | input[type="radio"] { 46 | margin-right: 4px; 47 | } 48 | 49 | [class^="col-"], 50 | [class*=" col-"] { 51 | border: 1px solid rgba(0,0,0,.5); 52 | } 53 | 54 | p a:hover, 55 | p a:focus { 56 | text-decoration: none; 57 | } 58 | ``` 59 | 60 | ## Selector splatting 61 | 62 | Selector splats are a special kind of selector alias that expand using passed arguments. 63 | 64 | ```crush 65 | @selector-splat input input[type="#(text)"]; 66 | 67 | form :input(time, text, url, email, number) { 68 | border: 1px solid; 69 | } 70 | ``` 71 | 72 | ```css 73 | form input[type="time"], 74 | form input[type="text"], 75 | form input[type="url"], 76 | form input[type="email"], 77 | form input[type="number"] { 78 | border: 1px solid; 79 | } 80 | ``` 81 | -------------------------------------------------------------------------------- /docs/core/selector-grouping.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Selector grouping with the `:any` pseudo class (modelled after CSS4 :matches) simplifies the creation of complex selector chains. 8 | 9 | ```crush 10 | :any( .sidebar, .block ) a:any( :hover, :focus ) { 11 | color: lemonchiffon; 12 | } 13 | ``` 14 | 15 | ```css 16 | .block a:hover, 17 | .block a:focus, 18 | .sidebar a:hover, 19 | .sidebar a:focus { 20 | color: lemonchiffon; 21 | } 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/core/variables.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Declare variables in your CSS with a `@set` directive and use them with the `$()` function. 8 | 9 | Variables can also be injected at runtime with the [vars option](#api--options). 10 | 11 | 12 | ```crush 13 | /* Defining variables */ 14 | @set { 15 | dark: #333; 16 | light: #F4F2E2; 17 | smaller-screen: screen and (max-width: 800px); 18 | } 19 | 20 | /* Using variables */ 21 | @media $(smaller-screen) { 22 | ul, p { 23 | color: $(dark); 24 | /* Using a fallback value with an undefined variable */ 25 | background-color: $(accent-color, #ff0); 26 | } 27 | } 28 | ``` 29 | 30 | ******* 31 | 32 | ```css 33 | /* Interpolation */ 34 | .username::before { 35 | content: "$(greeting)"; 36 | } 37 | ``` 38 | 39 | ## Conditionals 40 | 41 | Sections of CSS can be included and excluded on the basis of variable existence with the `@ifset` directive: 42 | 43 | ```crush 44 | @set foo #f00; 45 | @set bar true; 46 | 47 | @ifset foo { 48 | p { 49 | color: $(foo); 50 | } 51 | } 52 | 53 | p { 54 | font-size: 12px; 55 | @ifset not foo { 56 | line-height: 1.5; 57 | } 58 | @ifset bar(true) { 59 | margin-bottom: 5px; 60 | } 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/getting-started/js.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | This preprocessor is written in PHP, so as prerequisite you will need to have PHP installed on your system to use the JS api. 8 | 9 | ```shell 10 | npm install csscrush 11 | ``` 12 | 13 | All methods can take the standard options (camelCase) as the second argument. 14 | 15 | ```php 16 | const csscrush = require('csscrush'); 17 | 18 | // Compile. Returns promise. 19 | csscrush.file('./styles.css', {sourceMap: true}); 20 | 21 | // Compile string of CSS. Returns promise. 22 | csscrush.string('* {box-sizing: border-box;}'); 23 | 24 | // Compile and watch file. Returns event emitter (triggers 'data' on compile). 25 | csscrush.watch('./styles.css'); 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/getting-started/php.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | If you're using [Composer](http://getcomposer.org) you can use Crush in your project with the following line in your terminal: 8 | 9 | ```shell 10 | composer require css-crush/css-crush 11 | ``` 12 | 13 | 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: 14 | 15 | ```php 16 | 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/plugins/aria.md: -------------------------------------------------------------------------------- 1 | 2 | Pseudo classes for working with ARIA roles, states and properties. 3 | 4 | * [ARIA roles spec](http://www.w3.org/TR/wai-aria/roles) 5 | * [ARIA states and properties spec](http://www.w3.org/TR/wai-aria/states_and_properties) 6 | 7 | ````crush 8 | :role(tablist) {...} 9 | :aria-expanded {...} 10 | :aria-expanded(false) {...} 11 | :aria-label {...} 12 | :aria-label(foobarbaz) {...} 13 | ```` 14 | 15 | ````css 16 | [role="tablist"] {...} 17 | [aria-expanded="true"] {...} 18 | [aria-expanded="false"] {...} 19 | [aria-label] {...} 20 | [aria-label="foobarbaz"] {...} 21 | ```` 22 | -------------------------------------------------------------------------------- /docs/plugins/canvas.md: -------------------------------------------------------------------------------- 1 | 2 | Bitmap image generator. 3 | 4 | Requires the GD image library bundled with PHP. 5 | 6 | ```crush 7 | /* Create square semi-opaque png. */ 8 | @canvas foo { 9 | width: 50; 10 | height: 50; 11 | fill: rgba(255, 0, 0, .5); 12 | } 13 | 14 | body { 15 | background: white canvas(foo); 16 | } 17 | ``` 18 | 19 | ***** 20 | 21 | ```crush 22 | /* White to transparent east facing gradient with 10px 23 | margin and background fill. */ 24 | @canvas horz-gradient { 25 | width: #(0); 26 | height: 150; 27 | fill: canvas-linear-gradient(to right, #(1 white), #(2 rgba(255,255,255,0))); 28 | background-fill: powderblue; 29 | margin: 10; 30 | } 31 | 32 | /* Rectangle 300x150. */ 33 | body { 34 | background: canvas(horz-gradient, 300); 35 | } 36 | /* Flipped gradient, using canvas-data() to generate a data URI. */ 37 | .bar { 38 | background: canvas-data(horz-gradient, 100, rgba(255,255,255,0), white); 39 | } 40 | ``` 41 | 42 | ***** 43 | 44 | ```crush 45 | /* Google logo resized to 400px width and given a sepia effect. */ 46 | @canvas sepia { 47 | src: url(http://www.google.com/images/logo.png); 48 | width: 400; 49 | canvas-filter: greyscale() colorize(45, 45, 0); 50 | } 51 | 52 | .bar { 53 | background: canvas(sepia); 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/plugins/ease.md: -------------------------------------------------------------------------------- 1 | 2 | Expanded easing keywords for transitions. 3 | 4 | * ease-in-out-back 5 | * ease-in-out-circ 6 | * ease-in-out-expo 7 | * ease-in-out-sine 8 | * ease-in-out-quint 9 | * ease-in-out-quart 10 | * ease-in-out-cubic 11 | * ease-in-out-quad 12 | * ease-out-back 13 | * ease-out-circ 14 | * ease-out-expo 15 | * ease-out-sine 16 | * ease-out-quint 17 | * ease-out-quart 18 | * ease-out-cubic 19 | * ease-out-quad 20 | * ease-in-back 21 | * ease-in-circ 22 | * ease-in-expo 23 | * ease-in-sine 24 | * ease-in-quint 25 | * ease-in-quart 26 | * ease-in-cubic 27 | * ease-in-quad 28 | 29 | See [easing demos](http://easings.net) for live examples. 30 | 31 | ```crush 32 | transition: .2s ease-in-quad; 33 | ``` 34 | 35 | ```css 36 | transition: .2s cubic-bezier(.550,.085,.680,.530); 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/plugins/forms.md: -------------------------------------------------------------------------------- 1 | 2 | Pseudo classes for working with forms. 3 | 4 | ```crush 5 | :input(date, search, email) {...} 6 | :checkbox {...} 7 | :radio {...} 8 | :text {...} 9 | ``` 10 | 11 | ```css 12 | input[type="date"], input[type="search"], input[type="email"] {...} 13 | input[type="checkbox"] {...} 14 | input[type="radio"] {...} 15 | input[type="text"] {...} 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/plugins/hocus-pocus.md: -------------------------------------------------------------------------------- 1 | 2 | Composite :hover/:focus/:active pseudo classes. 3 | 4 | ```crush 5 | a:hocus { color: red; } 6 | a:pocus { color: red; } 7 | ``` 8 | 9 | ```css 10 | a:hover, a:focus { color: red; } 11 | a:hover, a:focus, a:active { color: red; } 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/plugins/property-sorter.md: -------------------------------------------------------------------------------- 1 | 2 | Property sorting. 3 | 4 | Examples use the predefined property sorting table. To define a custom sorting order pass an array to `csscrush_set_property_sort_order()` 5 | 6 | 7 | ```crush 8 | color: red; 9 | background: #000; 10 | opacity: .5; 11 | display: block; 12 | position: absolute; 13 | ``` 14 | 15 | ```css 16 | position: absolute; 17 | display: block; 18 | opacity: .5; 19 | color: red; 20 | background: #000; 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/plugins/svg-gradients.md: -------------------------------------------------------------------------------- 1 | 2 | Functions for creating SVG gradients with a CSS gradient like syntax. 3 | 4 | Primarily useful for supporting Internet Explorer 9. 5 | 6 | ## svg-linear-gradent() 7 | 8 | Syntax is the same as [linear-gradient()](http://dev.w3.org/csswg/css3-images/#linear-gradient) 9 | 10 | ```syntax 11 | svg-linear-gradent( [ | to ,]? [, ]+ ) 12 | ``` 13 | 14 | ### Returns 15 | 16 | A base64 encoded svg data-uri. 17 | 18 | ### Known issues 19 | 20 | Color stops can only take percentage value offsets. 21 | 22 | ```css 23 | background-image: svg-linear-gradient( to top left, #fff, rgba(255,255,255,0) 80% ); 24 | background-image: svg-linear-gradient( 35deg, red, gold 20%, powderblue ); 25 | ``` 26 | 27 | 28 | ## svg-radial-gradent() 29 | 30 | Syntax is similar to but more limited than [radial-gradient()](http://dev.w3.org/csswg/css3-images/#radial-gradient) 31 | 32 | ```syntax 33 | svg-radial-gradent( [ | at ,]? [, ]+ ) 34 | ``` 35 | 36 | ### Returns 37 | 38 | A base64 encoded svg data-uri. 39 | 40 | ### Known issues 41 | 42 | Color stops can only take percentage value offsets. 43 | No control over shape - only circular gradients - however, the generated image can be stretched with background-size. 44 | 45 | ```css 46 | background-image: svg-radial-gradient( at center, red, blue 50%, yellow ); 47 | background-image: svg-radial-gradient( 100% 50%, rgba(255,255,255,.5), rgba(255,255,255,0) ); 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/plugins/svg.md: -------------------------------------------------------------------------------- 1 | 2 | Define and embed simple SVG elements, paths and effects inside CSS 3 | 4 | 5 | ```crush 6 | @svg foo { 7 | type: star; 8 | star-points: #(0 5); 9 | radius: 100 50; 10 | margin: 20; 11 | stroke: black; 12 | fill: red; 13 | fill-opacity: .5; 14 | } 15 | 16 | /* Embed SVG with svg() function (generates an svg file). */ 17 | body { 18 | background: svg(foo); 19 | } 20 | /* As above but a 3 point star creating a data URI instead of a file. */ 21 | body { 22 | background: svg-data(foo, 3); 23 | } 24 | ``` 25 | 26 | ******* 27 | 28 | ```crush 29 | /* Using path data and stroke styles to create a plus sign. */ 30 | @svg plus { 31 | d: "M0,5 h10 M5,0 v10"; 32 | width: 10; 33 | height: 10; 34 | stroke: white; 35 | stroke-linecap: round; 36 | stroke-width: 2; 37 | } 38 | ``` 39 | 40 | 41 | ******* 42 | 43 | ```crush 44 | /* Skewed circle with radial gradient fill and drop shadow. */ 45 | @svg circle { 46 | type: circle; 47 | transform: skewX(30); 48 | diameter: 60; 49 | margin: 20; 50 | fill: svg-radial-gradient(at top right, gold 50%, red); 51 | drop-shadow: 2 2 0 rgba(0,0,0,1); 52 | } 53 | ``` 54 | 55 | ******* 56 | 57 | ```crush 58 | /* 8-sided polygon with an image fill. 59 | Note: images usually have to be converted to data URIs, see known issues below. */ 60 | @svg pattern { 61 | type: polygon; 62 | sides: 8; 63 | diameter: 180; 64 | margin: 20; 65 | fill: pattern(data-uri(kitten.jpg), scale(1) translate(-100 0)); 66 | fill-opacity: .8; 67 | } 68 | ``` 69 | 70 | 71 | ### Known issues 72 | 73 | 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. 74 | 75 | -------------------------------------------------------------------------------- /js/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} file - CSS file path 3 | * @param {CSSCrushOptions} [options] 4 | * @returns {CSSCrushProcess} 5 | */ 6 | export function watch(file: string, options?: CSSCrushOptions): CSSCrushProcess; 7 | /** 8 | * @param {string} file - CSS file path 9 | * @param {CSSCrushOptions} [options] 10 | * @returns {Promise} 11 | */ 12 | export function file(file: string, options?: CSSCrushOptions): Promise; 13 | /** 14 | * @param {string} string - CSS text 15 | * @param {CSSCrushOptions} [options] 16 | * @returns {Promise} 17 | */ 18 | export function string(string: string, options?: CSSCrushOptions): Promise; 19 | declare namespace _default { 20 | export { watch }; 21 | export { file }; 22 | export { string }; 23 | } 24 | export default _default; 25 | export type CSSCrushOptions = { 26 | sourceMap?: boolean; 27 | boilerplate?: boolean; 28 | minify?: boolean; 29 | vendorTarget?: ('all' | 'none' | 'moz' | 'ms' | 'webkit'); 30 | plugins?: string | [string]; 31 | importPath?: string | [string]; 32 | newlines?: ('use-platform' | 'windows' | 'unix'); 33 | formatter?: ('block' | 'single-line' | 'padded'); 34 | input?: string; 35 | context?: string; 36 | output?: string; 37 | vars?: object; 38 | }; 39 | export type CSSCrushProcessOptions = CSSCrushOptions & { 40 | stdIn?: string; 41 | watch?: boolean; 42 | }; 43 | /** 44 | * @typedef {object} CSSCrushOptions 45 | * @property {boolean} [sourceMap] 46 | * @property {boolean} [boilerplate] 47 | * @property {boolean} [minify=true] 48 | * @property {('all' | 'none' | 'moz' | 'ms' | 'webkit')} [vendorTarget='all'] 49 | * @property {string | [string]} [plugins] 50 | * @property {string | [string]} [importPath] 51 | * @property {('use-platform' | 'windows' | 'unix')} [newlines='use-platform'] 52 | * @property {('block' | 'single-line' | 'padded')} [formatter] 53 | * @property {string} [input] 54 | * @property {string} [context] 55 | * @property {string} [output] 56 | * @property {object} [vars] 57 | */ 58 | /** 59 | * @typedef {CSSCrushOptions & { 60 | * stdIn?: string; 61 | * watch?: boolean; 62 | * }} CSSCrushProcessOptions 63 | */ 64 | declare class CSSCrushProcess extends EventEmitter { 65 | /** 66 | * @param {CSSCrushProcessOptions} options 67 | * @returns {Promise} 68 | */ 69 | exec(options: CSSCrushProcessOptions): Promise; 70 | /** 71 | * @param {CSSCrushProcessOptions} options 72 | * @returns {CSSCrushProcess} 73 | */ 74 | watch(options: CSSCrushProcessOptions): CSSCrushProcess; 75 | kill(): void; 76 | #private; 77 | } 78 | import { EventEmitter } from 'node:events'; 79 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | /*eslint no-control-regex: 0*/ 2 | import os from 'node:os'; 3 | import fs from 'node:fs'; 4 | import pathUtil from 'node:path'; 5 | import {fileURLToPath} from 'node:url'; 6 | import querystring from 'node:querystring'; 7 | import {EventEmitter} from 'node:events'; 8 | import {exec} from 'node:child_process'; 9 | import {createHash} from 'node:crypto'; 10 | import glob from 'glob'; 11 | 12 | const cliPath = pathUtil 13 | .resolve(pathUtil 14 | .dirname(fileURLToPath(import.meta.url)), '../cli.php'); 15 | 16 | const processes = []; 17 | 18 | for (const event of [ 19 | 'exit', 20 | 'SIGINT', 21 | 'SIGTERM', 22 | 'SIGUSR1', 23 | 'SIGUSR2', 24 | 'uncaughtException', 25 | ]) { 26 | process.on(event, exit); 27 | } 28 | 29 | /** 30 | * @typedef {object} CSSCrushOptions 31 | * @property {boolean} [sourceMap] 32 | * @property {boolean} [boilerplate] 33 | * @property {boolean} [minify=true] 34 | * @property {('all' | 'none' | 'moz' | 'ms' | 'webkit')} [vendorTarget='all'] 35 | * @property {string | [string]} [plugins] 36 | * @property {string | [string]} [importPath] 37 | * @property {('use-platform' | 'windows' | 'unix')} [newlines='use-platform'] 38 | * @property {('block' | 'single-line' | 'padded')} [formatter] 39 | * @property {string} [input] 40 | * @property {string} [context] 41 | * @property {string} [output] 42 | * @property {object} [vars] 43 | */ 44 | /** 45 | * @typedef {CSSCrushOptions & { 46 | * stdIn?: string; 47 | * watch?: boolean; 48 | * }} CSSCrushProcessOptions 49 | */ 50 | 51 | class CSSCrushProcess extends EventEmitter { 52 | 53 | #process; 54 | 55 | /** 56 | * @param {CSSCrushProcessOptions} options 57 | * @returns {Promise} 58 | */ 59 | exec(options) { 60 | return new Promise(resolve => { 61 | let command = this.#assembleCommand(options); 62 | const {stdIn} = options; 63 | if (stdIn) { 64 | command = `echo '${stdIn.replace(/'/g, "\\'")}' | ${command}`; 65 | } 66 | processExec(command, (error, stdout, stderr) => { 67 | process.stderr.write(stderr.toString()); 68 | if (error) { 69 | return resolve(false); 70 | } 71 | const stdOut = stdout.toString(); 72 | if (stdIn) { 73 | process.stdout.write(stdOut); 74 | } 75 | return resolve(stdOut || true); 76 | }); 77 | }); 78 | } 79 | 80 | /** 81 | * @param {CSSCrushProcessOptions} options 82 | * @returns {CSSCrushProcess} 83 | */ 84 | watch(options) { 85 | options.watch = true; 86 | const command = this.#assembleCommand(options); 87 | this.#process = processExec(command); 88 | 89 | /* 90 | * Emitting 'error' events from EventEmitter without 91 | * any error listener will throw uncaught exception. 92 | */ 93 | this.on('error', () => {}); 94 | 95 | this.#process.stderr.on('data', msg => { 96 | msg = msg.toString(); 97 | process.stderr.write(msg); 98 | msg = msg.replace(/\x1B\[[^m]*m/g, '').trim(); 99 | 100 | const [, signal, detail] = /^([A-Z]+):\s*(.+)/i.exec(msg) || []; 101 | const {input, output} = options; 102 | const eventData = { 103 | signal, 104 | options: { 105 | input: input ? pathUtil.resolve(input) : null, 106 | output: output ? pathUtil.resolve(output) : null, 107 | }, 108 | }; 109 | 110 | if (/^(WARNING|ERROR)$/.test(signal)) { 111 | const error = new Error(detail); 112 | Object.assign(error, eventData, {severity: signal.toLowerCase()}); 113 | this.emit('error', error); 114 | } 115 | else { 116 | this.emit('data', {message: detail, ...eventData}); 117 | } 118 | }); 119 | 120 | this.#process.on('exit', exit); 121 | 122 | return this; 123 | } 124 | 125 | kill() { 126 | this.#process?.kill(); 127 | } 128 | 129 | #assembleCommand(options) { 130 | return `${process.env.CSSCRUSH_PHP_BIN || 'php'} ${cliPath} ${this.#stringifyOptions(options)}`; 131 | } 132 | 133 | #stringifyOptions(options) { 134 | const args = []; 135 | options = {...options}; 136 | for (let name in options) { 137 | // Normalize to hypenated case. 138 | const cssCase = name.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`); 139 | if (name !== cssCase) { 140 | options[cssCase] = options[name]; 141 | delete options[name]; 142 | name = cssCase; 143 | } 144 | let value = options[name]; 145 | switch (name) { 146 | // Booleans. 147 | case 'watch': 148 | case 'source-map': 149 | case 'boilerplate': 150 | if (value) { 151 | args.push(`--${name}`); 152 | } 153 | else if (value === false) { 154 | args.push(`--${name}=false`); 155 | } 156 | break; 157 | case 'minify': 158 | if (! value) { 159 | args.push(`--pretty`); 160 | } 161 | break; 162 | // Array/list values. 163 | case 'vendor-target': 164 | case 'plugins': 165 | case 'import-path': 166 | if (value) { 167 | value = (Array.isArray(value) ? value : [value]).join(','); 168 | args.push(`--${name}="${value}"`); 169 | } 170 | break; 171 | // String values. 172 | case 'newlines': 173 | case 'formatter': 174 | case 'input': 175 | case 'context': 176 | case 'output': 177 | if (value) { 178 | args.push(`--${name}="${value}"`); 179 | } 180 | break; 181 | case 'vars': 182 | args.push(`--${name}="${querystring.stringify(value)}"`); 183 | break; 184 | } 185 | } 186 | 187 | return args.join(' '); 188 | } 189 | } 190 | 191 | export default { 192 | watch, 193 | file, 194 | string, 195 | }; 196 | 197 | /** 198 | * @param {string} file - CSS file path 199 | * @param {CSSCrushOptions} [options] 200 | * @returns {CSSCrushProcess} 201 | */ 202 | export function watch(file, options={}) { 203 | ({file: options.input, context: options.context} = resolveFile(file, {watch: true})); 204 | return (new CSSCrushProcess()).watch(options); 205 | } 206 | 207 | /** 208 | * @param {string} file - CSS file path 209 | * @param {CSSCrushOptions} [options] 210 | * @returns {Promise} 211 | */ 212 | export function file(file, options={}) { 213 | ({file: options.input, context: options.context} = resolveFile(file)); 214 | return (new CSSCrushProcess()).exec(options); 215 | } 216 | 217 | /** 218 | * @param {string} string - CSS text 219 | * @param {CSSCrushOptions} [options] 220 | * @returns {Promise} 221 | */ 222 | export function string(string, options={}) { 223 | 224 | /** @type {CSSCrushProcessOptions} */ (options).stdIn = string; 225 | return (new CSSCrushProcess()).exec(options); 226 | } 227 | 228 | /** 229 | * @param {string} input 230 | * @param {object} [options] 231 | * @param {boolean} [options.watch] 232 | */ 233 | function resolveFile(input, {watch}={}) { 234 | 235 | if (Array.isArray(input)) { 236 | 237 | let initial; 238 | let previous; 239 | 240 | /* 241 | * Generate temporary file containing entrypoints. 242 | * Poll to update on additions and deletions. 243 | */ 244 | const poller = () => { 245 | const result = resolveInputs(input); 246 | 247 | if (result.fingerprint !== previous?.fingerprint) { 248 | fs.writeFileSync(initial?.file || result.file, result.content, { 249 | mode: 0o777, 250 | }); 251 | } 252 | 253 | initial ||= result; 254 | previous = result; 255 | 256 | if (watch) { 257 | setTimeout(poller, 2000); 258 | } 259 | 260 | return result; 261 | }; 262 | 263 | return poller(); 264 | } 265 | 266 | return { 267 | file: input, 268 | }; 269 | } 270 | 271 | function resolveInputs(fileGlobs) { 272 | 273 | const result = {}; 274 | 275 | /** @type {Set | array} */ 276 | let files = new Set(); 277 | 278 | for (const it of fileGlobs) { 279 | for (const path of (glob.sync(it) || []).sort()) { 280 | files.add(path); 281 | } 282 | } 283 | 284 | if (! files.size) { 285 | return result; 286 | } 287 | 288 | files = [...files]; 289 | 290 | const rootPath = files 291 | .shift(); 292 | const context = pathUtil 293 | .dirname(rootPath); 294 | const rootFile = pathUtil 295 | .basename(rootPath); 296 | 297 | const content = [rootFile] 298 | .concat(files 299 | .map(it => pathUtil 300 | .relative(context, it))) 301 | .map(it => `@import "./${it}";`) 302 | .join('\n'); 303 | 304 | const fingerprint = createHash('md5') 305 | .update(content) 306 | .digest('hex'); 307 | 308 | const outputDir = `${os.tmpdir()}/csscrush`; 309 | if (! fs.existsSync(outputDir)) { 310 | fs.mkdirSync(outputDir, { 311 | mode: 0o777, 312 | }); 313 | } 314 | 315 | return Object 316 | .assign(result, { 317 | context, 318 | content, 319 | fingerprint, 320 | file: `${outputDir}/${fingerprint}.css`, 321 | }); 322 | } 323 | 324 | function processExec(command, done) { 325 | processes.push(exec(command, done)); 326 | return processes.at(-1); 327 | } 328 | 329 | function exit() { 330 | let proc; 331 | while ((proc = processes.pop())) { 332 | proc?.kill(); 333 | } 334 | process.exit(); 335 | } 336 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./js/index.js" 4 | ], 5 | "exclude": [ 6 | "node_modules", 7 | ], 8 | "compilerOptions": { 9 | "allowSyntheticDefaultImports": true, 10 | "baseUrl": ".", 11 | "checkJs": true, 12 | "declaration": true, 13 | "emitDeclarationOnly": true, 14 | "maxNodeModuleJsDepth": 0, 15 | "module": "esnext", 16 | "moduleResolution": "nodenext", 17 | "noEmit": false, 18 | "outDir": "./js", 19 | "target": "esnext", 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /lib/CssCrush/BalancedMatch.php: -------------------------------------------------------------------------------- 1 | string = $string; 23 | $this->offset = $offset; 24 | $this->match = null; 25 | $this->length = 0; 26 | 27 | list($opener, $closer) = str_split($brackets, 1); 28 | 29 | if (strpos($string->raw, $opener, $this->offset) === false) { 30 | 31 | return; 32 | } 33 | 34 | if (substr_count($string->raw, $opener) !== substr_count($string->raw, $closer)) { 35 | $sample = substr($string->raw, $this->offset, 25); 36 | warning("Unmatched token near '$sample'."); 37 | 38 | return; 39 | } 40 | 41 | $patt = ($opener === '{') ? Regex::$patt->block : Regex::$patt->parens; 42 | 43 | if (preg_match($patt, $string->raw, $m, PREG_OFFSET_CAPTURE, $this->offset)) { 44 | 45 | $this->match = $m; 46 | $this->matchLength = strlen($m[0][0]); 47 | $this->matchStart = $m[0][1]; 48 | $this->matchEnd = $this->matchStart + $this->matchLength; 49 | $this->length = $this->matchEnd - $this->offset; 50 | } 51 | else { 52 | warning("Could not match '$opener'. Exiting."); 53 | } 54 | } 55 | 56 | public function inside() 57 | { 58 | return $this->match[2][0]; 59 | } 60 | 61 | public function whole() 62 | { 63 | return substr($this->string->raw, $this->offset, $this->length); 64 | } 65 | 66 | public function replace($replacement) 67 | { 68 | $this->string->splice($replacement, $this->offset, $this->length); 69 | } 70 | 71 | public function unWrap() 72 | { 73 | $this->string->splice($this->inside(), $this->offset, $this->length); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/CssCrush/Collection.php: -------------------------------------------------------------------------------- 1 | store = $store; 14 | } 15 | 16 | public function get($index = null) 17 | { 18 | return is_int($index) ? $this->store[$index] : $this->store; 19 | } 20 | 21 | static public function value($item, $property) 22 | { 23 | if (strpos($property, '|') !== false) { 24 | $filters = explode('|', $property); 25 | $property = array_shift($filters); 26 | $value = $item->$property; 27 | foreach ($filters as $filter) { 28 | switch ($filter) { 29 | case 'lower': 30 | $value = strtolower($value); 31 | break; 32 | } 33 | } 34 | return $value; 35 | } 36 | return $item->$property; 37 | } 38 | 39 | public function filter($filterer, $op = '===') 40 | { 41 | if (is_array($filterer)) { 42 | 43 | $ops = [ 44 | '===' => function ($item) use ($filterer) { 45 | foreach ($filterer as $property => $value) { 46 | if (Collection::value($item, $property) !== $value) { 47 | return false; 48 | } 49 | } 50 | return true; 51 | }, 52 | '!==' => function ($item) use ($filterer) { 53 | foreach ($filterer as $property => $value) { 54 | if (Collection::value($item, $property) === $value) { 55 | return false; 56 | } 57 | } 58 | return true; 59 | }, 60 | ]; 61 | 62 | $callback = $ops[$op]; 63 | } 64 | elseif (is_callable($filterer)) { 65 | $callback = $filterer; 66 | } 67 | 68 | if (isset($callback)) { 69 | $this->store = array_filter($this->store, $callback); 70 | } 71 | 72 | return $this; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/CssCrush/Crush.php: -------------------------------------------------------------------------------- 1 | pluginDirs = [self::$dir . '/plugins']; 27 | self::$config->scriptDir = dirname(realpath($_SERVER['SCRIPT_FILENAME'])); 28 | self::$config->docRoot = self::resolveDocRoot(); 29 | self::$config->logger = new Logger(); 30 | self::$config->io = 'CssCrush\IO'; 31 | 32 | // Shared resources. 33 | self::$config->vars = []; 34 | self::$config->aliasesFile = self::$dir . '/aliases.ini'; 35 | self::$config->aliases = []; 36 | self::$config->bareAliases = [ 37 | 'properties' => [], 38 | 'functions' => [], 39 | 'function_groups' => [], 40 | 'declarations' => [], 41 | 'at-rules' => [], 42 | ]; 43 | self::$config->options = new Options(); 44 | 45 | require_once self::$dir . '/misc/formatters.php'; 46 | } 47 | 48 | static protected function resolveDocRoot($doc_root = null) 49 | { 50 | // Get document_root reference 51 | // $_SERVER['DOCUMENT_ROOT'] is unreliable in certain CGI/Apache/IIS setups 52 | 53 | if (! $doc_root) { 54 | 55 | $script_filename = $_SERVER['SCRIPT_FILENAME']; 56 | $script_name = $_SERVER['SCRIPT_NAME']; 57 | 58 | if ($script_filename && $script_name) { 59 | 60 | $len_diff = strlen($script_filename) - strlen($script_name); 61 | 62 | // We're comparing the two strings so normalize OS directory separators 63 | $script_filename = str_replace('\\', '/', $script_filename); 64 | $script_name = str_replace('\\', '/', $script_name); 65 | 66 | // Check $script_filename ends with $script_name 67 | if (substr($script_filename, $len_diff) === $script_name) { 68 | 69 | $path = substr($script_filename, 0, $len_diff); 70 | $doc_root = realpath($path); 71 | } 72 | } 73 | 74 | if (! $doc_root) { 75 | $doc_root = realpath($_SERVER['DOCUMENT_ROOT']); 76 | } 77 | 78 | if (! $doc_root) { 79 | warning("Could not get a valid DOCUMENT_ROOT reference."); 80 | } 81 | } 82 | 83 | return Util::normalizePath($doc_root); 84 | } 85 | 86 | public static function loadAssets() 87 | { 88 | static $called; 89 | if ($called) { 90 | return; 91 | } 92 | $called = true; 93 | 94 | if (! self::$config->aliases) { 95 | $aliases = self::parseAliasesFile(self::$config->aliasesFile); 96 | self::$config->aliases = $aliases ?: self::$config->bareAliases; 97 | } 98 | } 99 | 100 | public static function plugin($name = null, ?callable $callback = null) 101 | { 102 | static $plugins = []; 103 | 104 | if (! $callback) { 105 | return isset($plugins[$name]) ? $plugins[$name] : null; 106 | } 107 | 108 | $plugins[$name] = $callback; 109 | } 110 | 111 | public static function enablePlugin($name) 112 | { 113 | $plugin = self::plugin($name); 114 | if (! $plugin) { 115 | $path = self::$dir . "/plugins/$name.php"; 116 | if (! file_exists($path)) { 117 | notice("Plugin '$name' not found."); 118 | return; 119 | } 120 | require_once $path; 121 | $plugin = self::plugin($name); 122 | } 123 | 124 | $plugin(self::$process); 125 | } 126 | 127 | public static function parseAliasesFile($file) 128 | { 129 | if (! ($tree = Util::parseIni($file, true))) { 130 | 131 | return false; 132 | } 133 | 134 | $regex = Regex::$patt; 135 | 136 | // Some alias groups need further parsing to unpack useful information into the tree. 137 | foreach ($tree as $section => $items) { 138 | 139 | if ($section === 'declarations') { 140 | 141 | $store = []; 142 | foreach ($items as $prop_val => $aliases) { 143 | 144 | list($prop, $value) = array_map('trim', explode(':', $prop_val)); 145 | 146 | foreach ($aliases as &$alias) { 147 | 148 | list($p, $v) = explode(':', $alias); 149 | $vendor = null; 150 | 151 | // Try to detect the vendor from property and value in turn. 152 | if ( 153 | preg_match($regex->vendorPrefix, $p, $m) 154 | || preg_match($regex->vendorPrefix, $v, $m) 155 | ) { 156 | $vendor = $m[1]; 157 | } 158 | $alias = [$p, $v, $vendor]; 159 | } 160 | $store[$prop][$value] = $aliases; 161 | } 162 | $tree['declarations'] = $store; 163 | } 164 | 165 | // Function groups. 166 | elseif (strpos($section, 'functions.') === 0) { 167 | 168 | $group = substr($section, strlen('functions')); 169 | 170 | $vendor_grouped_aliases = []; 171 | foreach ($items as $func_name => $aliases) { 172 | 173 | // Assign group name to the aliasable function. 174 | $tree['functions'][$func_name] = $group; 175 | 176 | foreach ($aliases as $alias_func) { 177 | 178 | // Only supporting vendor prefixed aliases, for now. 179 | if (preg_match($regex->vendorPrefix, $alias_func, $m)) { 180 | 181 | // We'll cache the function matching regex here. 182 | $vendor_grouped_aliases[$m[1]]['find'][] = Regex::make("~{{ LB }}$func_name(?=\()~iS"); 183 | $vendor_grouped_aliases[$m[1]]['replace'][] = $alias_func; 184 | } 185 | } 186 | } 187 | $tree['function_groups'][$group] = $vendor_grouped_aliases; 188 | unset($tree[$section]); 189 | } 190 | } 191 | 192 | $tree += self::$config->bareAliases; 193 | 194 | // Persisting dummy aliases for testing purposes. 195 | $tree['properties']['foo'] = 196 | $tree['at-rules']['foo'] = 197 | $tree['functions']['foo'] = ['-webkit-foo', '-moz-foo', '-ms-foo']; 198 | 199 | return $tree; 200 | } 201 | 202 | ############################# 203 | # Logging and stats. 204 | 205 | public static function printLog() 206 | { 207 | if (! empty(self::$process->debugLog)) { 208 | 209 | if (PHP_SAPI !== 'cli') { 210 | $out = []; 211 | foreach (self::$process->debugLog as $item) { 212 | $out[] = '
' . htmlspecialchars($item) . '
'; 213 | } 214 | echo implode('
', $out); 215 | } 216 | else { 217 | echo implode(PHP_EOL, self::$process->debugLog), PHP_EOL; 218 | } 219 | } 220 | } 221 | 222 | public static function runStat() 223 | { 224 | $process = Crush::$process; 225 | 226 | foreach (func_get_args() as $stat_name) { 227 | 228 | switch ($stat_name) { 229 | case 'paths': 230 | $process->stat['input_filename'] = $process->input->filename; 231 | $process->stat['input_path'] = $process->input->path; 232 | $process->stat['output_filename'] = $process->output->filename; 233 | $process->stat['output_path'] = $process->output->dir . '/' . $process->output->filename; 234 | break; 235 | 236 | case 'vars': 237 | $process->stat['vars'] = array_map(function ($item) use ($process) { 238 | return $process->tokens->restore($process->functions->apply($item), ['s', 'u', 'p']); 239 | }, $process->vars); 240 | break; 241 | 242 | case 'compile_time': 243 | $process->stat['compile_time'] = microtime(true) - $process->stat['compile_start_time']; 244 | unset($process->stat['compile_start_time']); 245 | break; 246 | 247 | case 'selector_count': 248 | $process->stat['selector_count'] = 0; 249 | foreach ($process->tokens->store->r as $rule) { 250 | $process->stat['selector_count'] += count($rule->selectors); 251 | } 252 | break; 253 | 254 | case 'rule_count': 255 | $process->stat['rule_count'] = count($process->tokens->store->r); 256 | break; 257 | } 258 | } 259 | } 260 | } 261 | 262 | function warning($message, $context = []) { 263 | Crush::$process->errors[] = $message; 264 | $logger = Crush::$config->logger; 265 | if ($logger instanceof Logger) { 266 | $message = "[CssCrush] $message"; 267 | } 268 | $logger->warning($message, $context); 269 | } 270 | 271 | function notice($message, $context = []) { 272 | Crush::$process->warnings[] = $message; 273 | $logger = Crush::$config->logger; 274 | if ($logger instanceof Logger) { 275 | $message = "[CssCrush] $message"; 276 | } 277 | $logger->notice($message, $context); 278 | } 279 | 280 | function debug($message, $context = []) { 281 | Crush::$config->logger->debug($message, $context); 282 | } 283 | 284 | function log($message, $context = [], $type = 'debug') { 285 | Crush::$config->logger->$type($message, $context); 286 | } 287 | 288 | // Compat with PHP < 7.2. 289 | if (! defined('PREG_UNMATCHED_AS_NULL')) { 290 | define('PREG_UNMATCHED_AS_NULL', null); 291 | } 292 | 293 | Crush::init(); 294 | -------------------------------------------------------------------------------- /lib/CssCrush/Declaration.php: -------------------------------------------------------------------------------- 1 | custom = true; 27 | $this->skip = true; 28 | } 29 | else { 30 | $property = strtolower($property); 31 | } 32 | 33 | if ($this->skip = strpos($property, '~') === 0) { 34 | $property = substr($property, 1); 35 | } 36 | 37 | // Store the canonical property name. 38 | // Store the vendor mark if one is present. 39 | if (preg_match(Regex::$patt->vendorPrefix, $property, $vendor)) { 40 | $canonical_property = $vendor[2]; 41 | $vendor = $vendor[1]; 42 | } 43 | else { 44 | $vendor = null; 45 | $canonical_property = $property; 46 | } 47 | 48 | // Check for !important. 49 | if (($important = stripos($value, '!important')) !== false) { 50 | $value = rtrim(substr($value, 0, $important)); 51 | $this->important = true; 52 | } 53 | 54 | Crush::$process->emit('declaration_preprocess', ['property' => &$property, 'value' => &$value]); 55 | 56 | // Reject declarations with empty CSS values. 57 | if ($value === false || $value === '') { 58 | $this->valid = false; 59 | } 60 | 61 | $this->property = $property; 62 | $this->canonicalProperty = $canonical_property; 63 | $this->vendor = $vendor; 64 | $this->index = $contextIndex; 65 | $this->value = $value; 66 | } 67 | 68 | public function __toString() 69 | { 70 | if (Crush::$process->minifyOutput) { 71 | $whitespace = ''; 72 | } 73 | else { 74 | $whitespace = ' '; 75 | } 76 | $important = $this->important ? "$whitespace!important" : ''; 77 | 78 | return "$this->property:$whitespace$this->value$important"; 79 | } 80 | 81 | /* 82 | Execute functions on value. 83 | Index functions. 84 | */ 85 | public function process($parentRule) 86 | { 87 | static $thisFunction; 88 | if (! $thisFunction) { 89 | $thisFunction = new Functions(['this' => 'CssCrush\fn__this']); 90 | } 91 | 92 | if (! $this->skip) { 93 | 94 | // this() function needs to be called exclusively because it is self referencing. 95 | $context = (object) [ 96 | 'rule' => $parentRule 97 | ]; 98 | $this->value = $thisFunction->apply($this->value, $context); 99 | 100 | if (isset($parentRule->declarations->data)) { 101 | $parentRule->declarations->data += [$this->property => $this->value]; 102 | } 103 | 104 | $context = (object) [ 105 | 'rule' => $parentRule, 106 | 'property' => $this->property 107 | ]; 108 | $this->value = Crush::$process->functions->apply($this->value, $context); 109 | } 110 | 111 | // Whitespace may have been introduced by functions. 112 | $this->value = trim($this->value); 113 | 114 | if ($this->value === '') { 115 | $this->valid = false; 116 | return; 117 | } 118 | 119 | $parentRule->declarations->queryData[$this->property] = $this->value; 120 | 121 | $this->indexFunctions(); 122 | } 123 | 124 | public function indexFunctions() 125 | { 126 | // Create an index of all regular functions in the value. 127 | $functions = []; 128 | if (preg_match_all(Regex::$patt->functionTest, $this->value, $m)) { 129 | foreach ($m['func_name'] as $fn_name) { 130 | $functions[strtolower($fn_name)] = true; 131 | } 132 | } 133 | $this->functions = $functions; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/CssCrush/EventEmitter.php: -------------------------------------------------------------------------------- 1 | eventEmitterStorage[$event])) { 17 | $this->eventEmitterStorage[$event] = []; 18 | } 19 | 20 | $id = ++$this->eventEmitterUid; 21 | $this->eventEmitterStorage[$event][$id] = $function; 22 | 23 | return function () use ($event, $id) { 24 | unset($this->eventEmitterStorage[$event][$id]); 25 | }; 26 | } 27 | 28 | public function emit($event, $data = null) 29 | { 30 | if (isset($this->eventEmitterStorage[$event])) { 31 | foreach ($this->eventEmitterStorage[$event] as $function) { 32 | $function($data); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/CssCrush/ExtendArg.php: -------------------------------------------------------------------------------- 1 | name = 19 | $this->raw = $name; 20 | 21 | if (! preg_match(Regex::$patt->rooted_ident, $this->name)) { 22 | 23 | // Not a regular name: Some kind of selector so normalize it for later comparison. 24 | $this->name = 25 | $this->raw = Selector::makeReadable($this->name); 26 | 27 | // If applying the pseudo on output store. 28 | if (substr($this->name, -1) === '!') { 29 | 30 | $this->name = rtrim($this->name, ' !'); 31 | if (preg_match('~\:\:?[\w-]+$~', $this->name, $m)) { 32 | $this->pseudo = $m[0]; 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/CssCrush/File.php: -------------------------------------------------------------------------------- 1 | process = $process; 18 | $io = $process->io; 19 | 20 | Crush::runStat('paths'); 21 | 22 | if ($process->options->cache) { 23 | $process->cacheData = $io->getCacheData(); 24 | if ($io->validateCache()) { 25 | $this->url = $io->getOutputUrl(); 26 | $this->path = $io->getOutputDir() . '/' . $io->getOutputFilename(); 27 | $process->release(); 28 | 29 | return; 30 | } 31 | } 32 | 33 | $string = $process->compile(); 34 | 35 | if ($io->write($string)) { 36 | $this->url = $io->getOutputUrl(); 37 | $this->path = $io->getOutputDir() . '/' . $io->getOutputFilename(); 38 | } 39 | } 40 | 41 | public function __toString() 42 | { 43 | return $this->url; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/CssCrush/Fragment.php: -------------------------------------------------------------------------------- 1 | name = $options['name']; 17 | } 18 | 19 | public function __invoke(?array $args = null, $str = null) 20 | { 21 | $str = parent::__invoke($args); 22 | 23 | // Flatten all fragment calls within the template string. 24 | while (preg_match(Regex::$patt->fragmentInvoke, $str, $m, PREG_OFFSET_CAPTURE)) { 25 | 26 | $name = strtolower($m['name'][0]); 27 | $fragment = isset(Crush::$process->fragments[$name]) ? Crush::$process->fragments[$name] : null; 28 | 29 | $replacement = ''; 30 | $start = $m[0][1]; 31 | $length = strlen($m[0][0]); 32 | 33 | // Skip over same named fragments to avoid infinite recursion. 34 | if ($fragment && $name !== $this->name) { 35 | $args = []; 36 | if (isset($m['parens'][1])) { 37 | $args = Functions::parseArgs($m['parens_content'][0]); 38 | } 39 | $replacement = $fragment($args); 40 | } 41 | $str = substr_replace($str, $replacement, $start, $length); 42 | } 43 | 44 | return $str; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/CssCrush/Functions.php: -------------------------------------------------------------------------------- 1 | 'CssCrush\fn__query', 15 | 16 | // These functions can be any order. 17 | 'math' => 'CssCrush\fn__math', 18 | 'hsla-adjust' => 'CssCrush\fn__hsla_adjust', 19 | 'hsl-adjust' => 'CssCrush\fn__hsl_adjust', 20 | 'h-adjust' => 'CssCrush\fn__h_adjust', 21 | 's-adjust' => 'CssCrush\fn__s_adjust', 22 | 'l-adjust' => 'CssCrush\fn__l_adjust', 23 | 'a-adjust' => 'CssCrush\fn__a_adjust', 24 | ]; 25 | 26 | public $register = []; 27 | 28 | protected $pattern; 29 | 30 | protected $patternOptions; 31 | 32 | public function __construct($register = []) 33 | { 34 | $this->register = $register; 35 | } 36 | 37 | public function add($name, $callback) 38 | { 39 | $this->register[$name] = $callback; 40 | } 41 | 42 | public function remove($name) 43 | { 44 | unset($this->register[$name]); 45 | } 46 | 47 | public function setPattern($useAll = false) 48 | { 49 | if ($useAll) { 50 | $this->register = self::$builtins + $this->register; 51 | } 52 | 53 | $this->pattern = Functions::makePattern(array_keys($this->register)); 54 | } 55 | 56 | public function apply($str, ?\stdClass $context = null) 57 | { 58 | if (strpos($str, '(') === false) { 59 | return $str; 60 | } 61 | 62 | if (! $this->pattern) { 63 | $this->setPattern(); 64 | } 65 | 66 | if (! preg_match($this->pattern, $str)) { 67 | return $str; 68 | } 69 | 70 | $matches = Regex::matchAll($this->pattern, $str); 71 | 72 | while ($match = array_pop($matches)) { 73 | 74 | if (isset($match['function']) && $match['function'][1] !== -1) { 75 | list($function, $offset) = $match['function']; 76 | } 77 | else { 78 | list($function, $offset) = $match['simple_function']; 79 | } 80 | 81 | if (! preg_match(Regex::$patt->parens, $str, $parens, PREG_OFFSET_CAPTURE, $offset)) { 82 | continue; 83 | } 84 | 85 | $openingParen = $parens[0][1]; 86 | $closingParen = $openingParen + strlen($parens[0][0]); 87 | $rawArgs = trim($parens['parens_content'][0]); 88 | 89 | // Update the context function identifier. 90 | if ($context) { 91 | $context->function = $function; 92 | } 93 | 94 | $returns = ''; 95 | if (isset($this->register[$function])) { 96 | $fn = $this->register[$function]; 97 | if (is_array($fn) && !empty($fn['parse_args'])) { 98 | $returns = $fn['callback'](self::parseArgs($rawArgs), $context); 99 | } 100 | else { 101 | $returns = $fn($rawArgs, $context); 102 | } 103 | } 104 | 105 | if (! is_null($returns)) { 106 | $str = substr_replace($str, $returns, $offset, $closingParen - $offset); 107 | } 108 | } 109 | 110 | return $str; 111 | } 112 | 113 | 114 | ############################# 115 | # API and helpers. 116 | 117 | public static function parseArgs($input, $allowSpaceDelim = false) 118 | { 119 | $options = []; 120 | if ($allowSpaceDelim) { 121 | $options['regex'] = Regex::$patt->argListSplit; 122 | } 123 | 124 | return Util::splitDelimList($input, $options); 125 | } 126 | 127 | /* 128 | Quick argument list parsing for functions that take 1 or 2 arguments 129 | with the proviso the first argument is an ident. 130 | */ 131 | public static function parseArgsSimple($input) 132 | { 133 | return preg_split(Regex::$patt->argListSplit, $input, 2); 134 | } 135 | 136 | public static function makePattern($functionNames) 137 | { 138 | $idents = []; 139 | $nonIdents = []; 140 | 141 | foreach ($functionNames as $functionName) { 142 | if (preg_match(Regex::$patt->ident, $functionName[0])) { 143 | $idents[] = preg_quote($functionName); 144 | } 145 | else { 146 | $nonIdents[] = preg_quote($functionName); 147 | } 148 | } 149 | 150 | if ($idents) { 151 | $idents = '{{ LB }}-?(?' . implode('|', $idents) . ')'; 152 | } 153 | if ($nonIdents) { 154 | $nonIdents = '(?' . implode('|', $nonIdents) . ')'; 155 | } 156 | 157 | if ($idents && $nonIdents) { 158 | $patt = "(?:$idents|$nonIdents)"; 159 | } 160 | elseif ($idents) { 161 | $patt = $idents; 162 | } 163 | elseif ($nonIdents) { 164 | $patt = $nonIdents; 165 | } 166 | 167 | return Regex::make("~$patt\(~iS"); // @phpstan-ignore-line variable.undefined 168 | } 169 | } 170 | 171 | 172 | ############################# 173 | # Stock CSS functions. 174 | 175 | function fn__math($input) { 176 | 177 | list($expression, $unit) = array_pad(Functions::parseArgs($input), 2, ''); 178 | 179 | // Swap in math constants. 180 | $expression = preg_replace( 181 | ['~\bpi\b~i'], 182 | [M_PI], 183 | $expression); 184 | 185 | // If no unit is specified scan expression. 186 | if (! $unit) { 187 | $numPatt = Regex::$classes->number; 188 | if (preg_match("~\b{$numPatt}(?[A-Za-z]{2,4}\b|%)~", $expression, $m)) { 189 | $unit = $m['unit']; 190 | } 191 | } 192 | 193 | // Filter expression so it's just characters necessary for simple math. 194 | $expression = preg_replace("~[^.0-9/*()+-]~S", '', $expression); 195 | 196 | $evalExpression = "return $expression;"; 197 | $result = false; 198 | 199 | if (class_exists('\\ParseError')) { 200 | try { 201 | $result = @eval($evalExpression); 202 | } 203 | catch (\Error $e) {} 204 | } 205 | else { 206 | $result = @eval($evalExpression); 207 | } 208 | 209 | return ($result === false ? 0 : round($result, 5)) . $unit; 210 | } 211 | 212 | function fn__hsla_adjust($input) { 213 | list($color, $h, $s, $l, $a) = array_pad(Functions::parseArgs($input, true), 5, 0); 214 | return Color::test($color) ? Color::colorAdjust($color, [$h, $s, $l, $a]) : ''; 215 | } 216 | 217 | function fn__hsl_adjust($input) { 218 | list($color, $h, $s, $l) = array_pad(Functions::parseArgs($input, true), 4, 0); 219 | return Color::test($color) ? Color::colorAdjust($color, [$h, $s, $l, 0]) : ''; 220 | } 221 | 222 | function fn__h_adjust($input) { 223 | list($color, $h) = array_pad(Functions::parseArgs($input, true), 2, 0); 224 | return Color::test($color) ? Color::colorAdjust($color, [$h, 0, 0, 0]) : ''; 225 | } 226 | 227 | function fn__s_adjust($input) { 228 | list($color, $s) = array_pad(Functions::parseArgs($input, true), 2, 0); 229 | return Color::test($color) ? Color::colorAdjust($color, [0, $s, 0, 0]) : ''; 230 | } 231 | 232 | function fn__l_adjust($input) { 233 | list($color, $l) = array_pad(Functions::parseArgs($input, true), 2, 0); 234 | return Color::test($color) ? Color::colorAdjust($color, [0, 0, $l, 0]) : ''; 235 | } 236 | 237 | function fn__a_adjust($input) { 238 | list($color, $a) = array_pad(Functions::parseArgs($input, true), 2, 0); 239 | return Color::test($color) ? Color::colorAdjust($color, [0, 0, 0, $a]) : ''; 240 | } 241 | 242 | function fn__this($input, $context) { 243 | 244 | $args = Functions::parseArgsSimple($input); 245 | $property = $args[0]; 246 | 247 | // Function relies on a context rule, bail if none. 248 | if (! isset($context->rule)) { 249 | return ''; 250 | } 251 | $rule = $context->rule; 252 | 253 | $rule->declarations->expandData('data', $property); 254 | 255 | if (isset($rule->declarations->data[$property])) { 256 | 257 | return $rule->declarations->data[$property]; 258 | } 259 | 260 | // Fallback value. 261 | elseif (isset($args[1])) { 262 | 263 | return $args[1]; 264 | } 265 | 266 | return ''; 267 | } 268 | 269 | function fn__query($input, $context) { 270 | 271 | $args = Functions::parseArgs($input); 272 | 273 | // Context property is required. 274 | if (! count($args) || ! isset($context->property)) { 275 | return ''; 276 | } 277 | 278 | list($target, $property, $fallback) = $args + [null, $context->property, null]; 279 | 280 | if (strtolower($property) === 'default') { 281 | $property = $context->property; 282 | } 283 | 284 | if (! preg_match(Regex::$patt->rooted_ident, $target)) { 285 | $target = Selector::makeReadable($target); 286 | } 287 | 288 | $targetRule = null; 289 | $references =& Crush::$process->references; 290 | 291 | switch (strtolower($target)) { 292 | case 'parent': 293 | $targetRule = $context->rule->parent; 294 | break; 295 | case 'previous': 296 | $targetRule = $context->rule->previous; 297 | break; 298 | case 'next': 299 | $targetRule = $context->rule->next; 300 | break; 301 | case 'top': 302 | $targetRule = $context->rule->parent; 303 | while ($targetRule && $targetRule->parent && $targetRule = $targetRule->parent); 304 | break; 305 | default: 306 | if (isset($references[$target])) { 307 | $targetRule = $references[$target]; 308 | } 309 | break; 310 | } 311 | 312 | $result = ''; 313 | if ($targetRule) { 314 | $targetRule->declarations->process(); 315 | $targetRule->declarations->expandData('queryData', $property); 316 | if (isset($targetRule->declarations->queryData[$property])) { 317 | $result = $targetRule->declarations->queryData[$property]; 318 | } 319 | } 320 | 321 | if ($result === '' && isset($fallback)) { 322 | $result = $fallback; 323 | } 324 | 325 | return $result; 326 | } 327 | -------------------------------------------------------------------------------- /lib/CssCrush/IO.php: -------------------------------------------------------------------------------- 1 | process = $process; 16 | } 17 | 18 | public function init() 19 | { 20 | $this->process->cacheFile = "{$this->process->output->dir}/.csscrush"; 21 | } 22 | 23 | public function getOutputDir() 24 | { 25 | $outputDir = $this->process->options->output_dir; 26 | 27 | return $outputDir ? $outputDir : $this->process->input->dir; 28 | } 29 | 30 | public function getOutputFilename() 31 | { 32 | $options = $this->process->options; 33 | 34 | $inputBasename = $this->process->input->filename 35 | ? basename($this->process->input->filename, '.css') 36 | : 'styles'; 37 | 38 | $outputBasename = $inputBasename; 39 | 40 | if (! empty($options->output_file)) { 41 | $outputBasename = basename($options->output_file, '.css'); 42 | } 43 | 44 | if ($this->process->input->dir === $this->getOutputDir() && $inputBasename === $outputBasename) { 45 | $outputBasename .= '.crush'; 46 | } 47 | 48 | return "$outputBasename.css"; 49 | } 50 | 51 | public function getOutputUrl() 52 | { 53 | $process = $this->process; 54 | $options = $process->options; 55 | $filename = $process->output->filename; 56 | 57 | $url = $process->output->dirUrl . '/' . $filename; 58 | 59 | // Make URL relative if the input path was relative. 60 | $input_path = new Url($process->input->raw); 61 | if ($input_path->isRelative) { 62 | $url = Util::getLinkBetweenPaths(Crush::$config->scriptDir, $process->output->dir) . $filename; 63 | } 64 | 65 | // Optional query-string timestamp. 66 | if ($options->versioning !== false) { 67 | $url .= '?'; 68 | if (isset($process->cacheData[$filename]['datem_sum'])) { 69 | $url .= $process->cacheData[$filename]['datem_sum']; 70 | } 71 | else { 72 | $url .= time(); 73 | } 74 | } 75 | 76 | return $url; 77 | } 78 | 79 | public function validateCache() 80 | { 81 | $process = $this->process; 82 | $options = $process->options; 83 | $input = $process->input; 84 | 85 | $dir = $this->getOutputDir(); 86 | $filename = $this->getOutputFilename(); 87 | $path = "$dir/$filename"; 88 | 89 | if (! file_exists($path)) { 90 | debug("File '$path' not cached."); 91 | 92 | return false; 93 | } 94 | 95 | if (! isset($process->cacheData[$filename])) { 96 | debug('Cached file exists but is not registered.'); 97 | 98 | return false; 99 | } 100 | 101 | $data =& $process->cacheData[$filename]; 102 | 103 | // Make stack of file mtimes starting with the input file. 104 | $file_sums = [$input->mtime]; 105 | foreach ($data['imports'] as $import_file) { 106 | 107 | // Check if this is docroot relative or input dir relative. 108 | $root = strpos($import_file, '/') === 0 ? $process->docRoot : $input->dir; 109 | $import_filepath = realpath($root) . "/$import_file"; 110 | 111 | if (file_exists($import_filepath)) { 112 | $file_sums[] = filemtime($import_filepath); 113 | } 114 | else { 115 | // File has been moved, remove old file and skip to compile. 116 | debug('Recompiling - an import file has been moved.'); 117 | 118 | return false; 119 | } 120 | } 121 | 122 | $files_changed = $data['datem_sum'] != array_sum($file_sums); 123 | if ($files_changed) { 124 | debug('Files have been modified. Recompiling.'); 125 | } 126 | 127 | // Compare runtime options and cached options for differences. 128 | // Cast because the cached options may be a \stdClass if an IO adapter has been used. 129 | $options_changed = false; 130 | $cached_options = (array) $data['options']; 131 | $active_options = $options->get(); 132 | foreach ($cached_options as $key => &$value) { 133 | if (isset($active_options[$key]) && $active_options[$key] !== $value) { 134 | debug('Options have been changed. Recompiling.'); 135 | $options_changed = true; 136 | break; 137 | } 138 | } 139 | 140 | if (! $options_changed && ! $files_changed) { 141 | debug("Files and options have not been modified, returning cached file."); 142 | 143 | return true; 144 | } 145 | else { 146 | $data['datem_sum'] = array_sum($file_sums); 147 | 148 | return false; 149 | } 150 | } 151 | 152 | public function getCacheData() 153 | { 154 | $process = $this->process; 155 | 156 | if (file_exists($process->cacheFile) && $process->cacheData) { 157 | 158 | // Already loaded and config file exists in the current directory 159 | return; 160 | } 161 | 162 | $cache_data_exists = file_exists($process->cacheFile); 163 | $cache_data_file_is_writable = $cache_data_exists ? is_writable($process->cacheFile) : false; 164 | $cache_data = []; 165 | 166 | if ( 167 | $cache_data_exists && 168 | $cache_data_file_is_writable && 169 | $cache_data = json_decode(file_get_contents($process->cacheFile), true) 170 | ) { 171 | // Successfully loaded config file. 172 | debug('Cache data loaded.'); 173 | } 174 | else { 175 | // Config file may exist but not be writable (may not be visible in some ftp situations?) 176 | if ($cache_data_exists) { 177 | if (! @unlink($process->cacheFile)) { 178 | notice('Could not delete cache data file.'); 179 | } 180 | } 181 | else { 182 | debug('Creating cache data file.'); 183 | } 184 | Util::filePutContents($process->cacheFile, json_encode([])); 185 | } 186 | 187 | return $cache_data; 188 | } 189 | 190 | public function saveCacheData() 191 | { 192 | $process = $this->process; 193 | 194 | debug('Saving config.'); 195 | 196 | Util::filePutContents($process->cacheFile, json_encode($process->cacheData, JSON_PRETTY_PRINT)); 197 | } 198 | 199 | public function write(StringObject $string) 200 | { 201 | $process = $this->process; 202 | 203 | $dir = $this->getOutputDir(); 204 | $filename = $this->getOutputFilename(); 205 | $sourcemapFilename = "$filename.map"; 206 | 207 | if ($process->sourceMap) { 208 | $string->append($process->newline . "/*# sourceMappingURL=$sourcemapFilename */"); 209 | } 210 | 211 | if (Util::filePutContents("$dir/$filename", $string)) { 212 | 213 | if ($process->sourceMap) { 214 | Util::filePutContents("$dir/$sourcemapFilename", 215 | json_encode($process->sourceMap, JSON_PRETTY_PRINT)); 216 | } 217 | 218 | if ($process->options->stat_dump) { 219 | $statFile = is_string($process->options->stat_dump) ? 220 | $process->options->stat_dump : "$dir/$filename.json"; 221 | 222 | $GLOBALS['CSSCRUSH_STAT_FILE'] = $statFile; 223 | Util::filePutContents($statFile, json_encode(csscrush_stat(), JSON_PRETTY_PRINT)); 224 | } 225 | 226 | return true; 227 | } 228 | 229 | return false; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /lib/CssCrush/IO/Watch.php: -------------------------------------------------------------------------------- 1 | process; 19 | $options = $process->options; 20 | 21 | $input_basename = $output_basename = basename($process->input->filename, '.css'); 22 | 23 | if (! empty($options->output_file)) { 24 | $output_basename = basename($options->output_file, '.css'); 25 | } 26 | 27 | $suffix = '.crush'; 28 | if (($process->input->dir !== $process->output->dir) || ($input_basename !== $output_basename)) { 29 | $suffix = ''; 30 | } 31 | 32 | return "$output_basename$suffix.css"; 33 | } 34 | 35 | public function getCacheData() 36 | { 37 | // Clear results from earlier processes. 38 | clearstatcache(); 39 | $this->process->cacheData = []; 40 | 41 | return self::$cacheData; 42 | } 43 | 44 | public function saveCacheData() 45 | { 46 | self::$cacheData = $this->process->cacheData; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/CssCrush/Importer.php: -------------------------------------------------------------------------------- 1 | process = $process; 16 | } 17 | 18 | public function collate() 19 | { 20 | $process = $this->process; 21 | $options = $process->options; 22 | $regex = Regex::$patt; 23 | $input = $process->input; 24 | 25 | $str = ''; 26 | 27 | // Keep track of all import file info for cache data. 28 | $mtimes = []; 29 | $filenames = []; 30 | 31 | // Resolve main input; a string of css or a file. 32 | if (isset($input->string)) { 33 | $str .= $input->string; 34 | $process->sources[] = 'Inline CSS'; 35 | } 36 | else { 37 | $str .= file_get_contents($input->path); 38 | $process->sources[] = $input->path; 39 | } 40 | 41 | // If there's a parsing error go no further. 42 | if (! $this->prepareImport($str)) { 43 | 44 | return $str; 45 | } 46 | 47 | // This may be set non-zero during the script if an absolute @import URL is encountered. 48 | $search_offset = 0; 49 | 50 | // Recurses until the nesting heirarchy is flattened and all import files are inlined. 51 | while (preg_match($regex->import, $str, $match, PREG_OFFSET_CAPTURE, $search_offset)) { 52 | 53 | $match_len = strlen($match[0][0]); 54 | $match_start = $match[0][1]; 55 | 56 | $import = new \stdClass(); 57 | $import->url = $process->tokens->get($match[1][0]); 58 | $import->media = trim($match[2][0]); 59 | 60 | // Protocoled import urls are not processed. Stash for prepending to output. 61 | if ($import->url->protocol) { 62 | $str = substr_replace($str, '', $match_start, $match_len); 63 | $process->absoluteImports[] = $import; 64 | continue; 65 | } 66 | 67 | // Resolve import path information. 68 | $import->path = null; 69 | if ($import->url->isRooted) { 70 | $import->path = realpath($process->docRoot . $import->url->value); 71 | } 72 | else { 73 | $url =& $import->url; 74 | $candidates = ["$input->dir/$url->value"]; 75 | 76 | // If `import_path` option is set implicit relative urls 77 | // are additionally searched under specified import path(s). 78 | if (is_array($options->import_path) && $url->isRelativeImplicit()) { 79 | foreach ($options->import_path as $importPath) { 80 | $candidates[] = "$importPath/$url->originalValue"; 81 | } 82 | } 83 | foreach ($candidates as $candidate) { 84 | if (file_exists($candidate)) { 85 | $import->path = realpath($candidate); 86 | break; 87 | } 88 | } 89 | } 90 | 91 | // If unsuccessful getting import contents continue with the import line removed. 92 | $import->content = $import->path ? @file_get_contents($import->path) : false; 93 | if ($import->content === false) { 94 | $errDesc = 'was not found'; 95 | if ($import->path && ! is_readable($import->path)) { 96 | $errDesc = 'is not readable'; 97 | } 98 | if (! empty($process->sources)) { 99 | $errDesc .= " (from within {$process->input->dir})"; 100 | } 101 | notice("@import '{$import->url->value}' $errDesc"); 102 | $str = substr_replace($str, '', $match_start, $match_len); 103 | continue; 104 | } 105 | 106 | $import->dir = dirname($import->path); 107 | $import->relativeDir = Util::getLinkBetweenPaths($input->dir, $import->dir); 108 | 109 | // Import file exists so register it. 110 | $process->sources[] = $import->path; 111 | $mtimes[] = filemtime($import->path); 112 | $filenames[] = $import->relativeDir . basename($import->path); 113 | 114 | // If the import content doesn't pass syntax validation skip to next import. 115 | if (! $this->prepareImport($import->content)) { 116 | 117 | $str = substr_replace($str, '', $match_start, $match_len); 118 | continue; 119 | } 120 | 121 | // Alter all embedded import URLs to be relative to the host-file. 122 | foreach (Regex::matchAll($regex->import, $import->content) as $m) { 123 | 124 | $nested_url = $process->tokens->get($m[1][0]); 125 | 126 | // Resolve rooted paths. 127 | if ($nested_url->isRooted) { 128 | $link = Util::getLinkBetweenPaths(dirname($nested_url->getAbsolutePath()), $import->dir); 129 | $nested_url->update($link . basename($nested_url->value)); 130 | } 131 | elseif (strlen($import->relativeDir)) { 132 | $nested_url->prepend("$import->relativeDir/"); 133 | } 134 | } 135 | 136 | // Optionally rewrite relative url and custom function data-uri references. 137 | if ($options->rewrite_import_urls) { 138 | $this->rewriteImportedUrls($import); 139 | } 140 | 141 | if ($import->media) { 142 | $import->content = "@media $import->media {{$import->content}}"; 143 | } 144 | 145 | $str = substr_replace($str, $import->content, $match_start, $match_len); 146 | } 147 | 148 | // Save only if caching is on and the hostfile object is associated with a real file. 149 | if ($input->path && $options->cache) { 150 | 151 | $process->cacheData[$process->output->filename] = [ 152 | 'imports' => $filenames, 153 | 'datem_sum' => array_sum($mtimes) + $input->mtime, 154 | 'options' => $options->get(), 155 | ]; 156 | $process->io->saveCacheData(); 157 | } 158 | 159 | return $str; 160 | } 161 | 162 | protected function rewriteImportedUrls($import) 163 | { 164 | $link = Util::getLinkBetweenPaths($this->process->input->dir, dirname($import->path)); 165 | 166 | if (empty($link)) { 167 | return; 168 | } 169 | 170 | // Match all urls that are not imports. 171 | preg_match_all(Regex::make('~(?content, $matches); 172 | 173 | foreach ($matches[0] as $token) { 174 | 175 | $url = $this->process->tokens->get($token); 176 | 177 | if ($url->isRelative) { 178 | $url->prepend($link); 179 | } 180 | } 181 | } 182 | 183 | protected function prepareImport(&$str) 184 | { 185 | $regex = Regex::$patt; 186 | $process = $this->process; 187 | $tokens = $process->tokens; 188 | 189 | // Convert all EOL to unix style. 190 | $str = preg_replace('~\r\n?~', "\n", $str); 191 | 192 | // Trimming to reduce regex backtracking. 193 | $str = rtrim($this->captureCommentAndString(rtrim($str))); 194 | 195 | if (! $this->syntaxCheck($str)) { 196 | 197 | $str = ''; 198 | return false; 199 | } 200 | 201 | // Normalize double-colon pseudo elements for backwards compatability. 202 | $str = preg_replace('~::(after|before|first-(?:letter|line))~iS', ':$1', $str); 203 | 204 | // Store @charset if set. 205 | if (preg_match($regex->charset, $str, $m)) { 206 | $replace = ''; 207 | if (! $process->charset) { 208 | // Keep track of newlines for line numbering. 209 | $replace = str_repeat("\n", substr_count($m[0], "\n")); 210 | $process->charset = trim($tokens->get($m[1]), '"\''); 211 | } 212 | $str = preg_replace($regex->charset, $replace, $str); 213 | } 214 | 215 | $str = $tokens->captureUrls($str, true); 216 | 217 | $this->addMarkers($str); 218 | 219 | $str = Util::normalizeWhiteSpace($str); 220 | 221 | return true; 222 | } 223 | 224 | protected function syntaxCheck(&$str) 225 | { 226 | // Catch obvious typing errors. 227 | $errors = false; 228 | $current_file = 'file://' . end($this->process->sources); 229 | $balanced_parens = substr_count($str, "(") === substr_count($str, ")"); 230 | $balanced_curlies = substr_count($str, "{") === substr_count($str, "}"); 231 | 232 | $validate_pairings = function ($str, $pairing) use ($current_file) 233 | { 234 | if ($pairing === '{}') { 235 | $opener_patt = '~\{~'; 236 | $balancer_patt = Regex::make('~^{{block}}~'); 237 | } 238 | else { 239 | $opener_patt = '~\(~'; 240 | $balancer_patt = Regex::make('~^{{parens}}~'); 241 | } 242 | 243 | // Find unbalanced opening brackets. 244 | preg_match_all($opener_patt, $str, $matches, PREG_OFFSET_CAPTURE); 245 | foreach ($matches[0] as $m) { 246 | $offset = $m[1]; 247 | if (! preg_match($balancer_patt, substr($str, $offset), $m)) { 248 | $substr = substr($str, 0, $offset); 249 | $line = substr_count($substr, "\n") + 1; 250 | $column = strlen($substr) - strrpos($substr, "\n"); 251 | return "Unbalanced '{$pairing[0]}' in $current_file, Line $line, Column $column."; 252 | } 253 | } 254 | 255 | // Reverse the string (and brackets) to find stray closing brackets. 256 | $str = strtr(strrev($str), $pairing, strrev($pairing)); 257 | 258 | preg_match_all($opener_patt, $str, $matches, PREG_OFFSET_CAPTURE); 259 | foreach ($matches[0] as $m) { 260 | $offset = $m[1]; 261 | $substr = substr($str, $offset); 262 | if (! preg_match($balancer_patt, $substr, $m)) { 263 | $line = substr_count($substr, "\n") + 1; 264 | $column = strpos($substr, "\n"); 265 | return "Stray '{$pairing[1]}' in $current_file, Line $line, Column $column."; 266 | } 267 | } 268 | 269 | return false; 270 | }; 271 | 272 | if (! $balanced_curlies) { 273 | $errors = true; 274 | warning($validate_pairings($str, '{}') ?: "Unbalanced '{' in $current_file."); 275 | } 276 | if (! $balanced_parens) { 277 | $errors = true; 278 | warning($validate_pairings($str, '()') ?: "Unbalanced '(' in $current_file."); 279 | } 280 | 281 | return $errors ? false : true; 282 | } 283 | 284 | protected function addMarkers(&$str) 285 | { 286 | $process = $this->process; 287 | $currentFileIndex = count($process->sources) - 1; 288 | 289 | static $patt; 290 | if (! $patt) { 291 | $patt = Regex::make('~ 292 | (?:^|(?<=[;{}])) 293 | (? 294 | (?: \s | {{c_token}} )* 295 | ) 296 | (? 297 | (?: 298 | # Some @-rules are treated like standard rule blocks. 299 | @(?: (?i)page|abstract|font-face(?-i) ) {{RB}} [^{]* 300 | | 301 | [^@;{}]+ 302 | ) 303 | ) 304 | \{ 305 | ~xS'); 306 | } 307 | 308 | $count = preg_match_all($patt, $str, $matches, PREG_OFFSET_CAPTURE); 309 | while ($count--) { 310 | 311 | $selectorOffset = $matches['selector'][$count][1]; 312 | 313 | $line = 0; 314 | $before = substr($str, 0, $selectorOffset); 315 | if ($selectorOffset) { 316 | $line = substr_count($before, "\n"); 317 | } 318 | 319 | $pointData = [$currentFileIndex, $line]; 320 | 321 | // Source maps require column index too. 322 | if ($process->generateMap) { 323 | $pointData[] = strlen($before) - (strrpos($before, "\n") ?: 0); 324 | } 325 | 326 | // Splice in marker token (packing point_data into string is more memory efficient). 327 | $str = substr_replace( 328 | $str, 329 | $process->tokens->add(implode(',', $pointData), 't'), 330 | $selectorOffset, 331 | 0); 332 | } 333 | } 334 | 335 | protected function captureCommentAndString($str) 336 | { 337 | $process = $this->process; 338 | $callback = function ($m) use ($process) { 339 | 340 | $fullMatch = $m[0]; 341 | 342 | if (strpos($fullMatch, '/*') === 0) { 343 | 344 | // Bail without storing comment if output is minified or a private comment. 345 | if ($process->minifyOutput || strpos($fullMatch, '/*$') === 0) { 346 | 347 | $label = ''; 348 | } 349 | else { 350 | // Fix broken comments as they will break any subsquent 351 | // imported files that are inlined. 352 | if (! preg_match('~\*/$~', $fullMatch)) { 353 | $fullMatch .= '*/'; 354 | } 355 | $label = $process->tokens->add($fullMatch, 'c'); 356 | } 357 | } 358 | else { 359 | // Fix broken strings as they will break any subsquent 360 | // imported files that are inlined. 361 | if ($fullMatch[0] !== $fullMatch[strlen($fullMatch)-1]) { 362 | $fullMatch .= $fullMatch[0]; 363 | } 364 | 365 | // Backticked literals may have been used for custom property values. 366 | if ($fullMatch[0] === '`') { 367 | $fullMatch = preg_replace('~\x5c`~', '`', trim($fullMatch, '`')); 368 | } 369 | 370 | $label = $process->tokens->add($fullMatch, 's'); 371 | } 372 | 373 | return $process->generateMap ? Tokens::pad($label, $fullMatch) : $label; 374 | }; 375 | 376 | return preg_replace_callback(Regex::$patt->commentAndString, $callback, $str); 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /lib/CssCrush/Iterator.php: -------------------------------------------------------------------------------- 1 | store = $items; 16 | } 17 | 18 | /* 19 | IteratorAggregate implementation. 20 | */ 21 | public function getIterator(): \Traversable 22 | { 23 | return new \ArrayIterator($this->store); 24 | } 25 | 26 | /* 27 | ArrayAccess implementation. 28 | */ 29 | public function offsetExists($index): bool 30 | { 31 | return array_key_exists($index, $this->store); 32 | } 33 | 34 | public function offsetGet($index): mixed 35 | { 36 | return isset($this->store[$index]) ? $this->store[$index] : null; 37 | } 38 | 39 | public function offsetSet($index, $value): void 40 | { 41 | $this->store[$index] = $value; 42 | } 43 | 44 | public function offsetUnset($index): void 45 | { 46 | unset($this->store[$index]); 47 | } 48 | 49 | public function getContents() 50 | { 51 | return $this->store; 52 | } 53 | 54 | /* 55 | Countable implementation. 56 | */ 57 | public function count(): int 58 | { 59 | return count($this->store); 60 | } 61 | 62 | /* 63 | Collection interface. 64 | */ 65 | public function filter($filterer, $op = '===') 66 | { 67 | $collection = new Collection($this->store); 68 | return $collection->filter($filterer, $op); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/CssCrush/Logger.php: -------------------------------------------------------------------------------- 1 | error($message, $context); 21 | } 22 | 23 | /** 24 | * Action must be taken immediately. 25 | * 26 | * Example: Entire website down, database unavailable, etc. This should 27 | * trigger the SMS alerts and wake you up. 28 | * 29 | * @param string $message 30 | * @param array $context 31 | * @return null 32 | */ 33 | public function alert($message, array $context = []) 34 | { 35 | $this->error($message, $context); 36 | } 37 | 38 | /** 39 | * Critical conditions. 40 | * 41 | * Example: Application component unavailable, unexpected exception. 42 | * 43 | * @param string $message 44 | * @param array $context 45 | * @return null 46 | */ 47 | public function critical($message, array $context = []) 48 | { 49 | $this->error($message, $context); 50 | } 51 | 52 | /** 53 | * Runtime errors that do not require immediate action but should typically 54 | * be logged and monitored. 55 | * 56 | * @param string $message 57 | * @param array $context 58 | * @return null 59 | */ 60 | public function error($message, array $context = []) 61 | { 62 | trigger_error($message, E_USER_ERROR); 63 | } 64 | 65 | /** 66 | * Exceptional occurrences that are not errors. 67 | * 68 | * Example: Use of deprecated APIs, poor use of an API, undesirable things 69 | * that are not necessarily wrong. 70 | * 71 | * @param string $message 72 | * @param array $context 73 | * @return null 74 | */ 75 | public function warning($message, array $context = []) 76 | { 77 | trigger_error($message, E_USER_WARNING); 78 | } 79 | 80 | /** 81 | * Normal but significant events. 82 | * 83 | * @param string $message 84 | * @param array $context 85 | * @return null 86 | */ 87 | public function notice($message, array $context = []) 88 | { 89 | trigger_error($message, E_USER_NOTICE); 90 | } 91 | 92 | /** 93 | * Interesting events. 94 | * 95 | * Example: User logs in, SQL logs. 96 | * 97 | * @param string $message 98 | * @param array $context 99 | * @return null 100 | */ 101 | public function info($message, array $context = []) 102 | { 103 | $this->debug($message, $context); 104 | } 105 | 106 | /** 107 | * Detailed debug information. 108 | * 109 | * @param string $message 110 | * @param array $context 111 | * @return null 112 | */ 113 | public function debug($message, array $context = []) 114 | { 115 | if (! empty($context['label'])) { 116 | $label = $context['label']; 117 | $label = PHP_EOL . "$label" . PHP_EOL . str_repeat('=', strlen($label)) . PHP_EOL; 118 | } 119 | else { 120 | $label = ''; 121 | } 122 | 123 | if (is_string($message)) { 124 | Crush::$process->debugLog[] = "$label$message"; 125 | } 126 | else { 127 | ob_start(); 128 | ! empty($context['var_dump']) ? var_dump($message) : print_r($message); 129 | Crush::$process->debugLog[] = $label . ob_get_clean(); 130 | } 131 | } 132 | 133 | /** 134 | * Logs with an arbitrary level. 135 | * 136 | * @param mixed $level 137 | * @param string $message 138 | * @param array $context 139 | * @return null 140 | */ 141 | public function log($level, $message, array $context = []) 142 | { 143 | $log_levels = array_flip(get_class_methods(__CLASS__)); 144 | unset($log_levels['log']); 145 | 146 | if (isset($log_levels[$level])) { 147 | return $this->$level($message, $context); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /lib/CssCrush/Mixin.php: -------------------------------------------------------------------------------- 1 | template = new Template($block); 16 | } 17 | 18 | public static function call($message, $context = null) 19 | { 20 | $process = Crush::$process; 21 | $mixable = null; 22 | $message = trim($message); 23 | 24 | // Test for mixin or abstract rule. e.g: 25 | // named-mixin( 50px, rgba(0,0,0,0), left 100% ) 26 | // abstract-rule 27 | if (preg_match(Regex::make('~^(?{{ident}}) {{parens}}?~xS'), $message, $message_match)) { 28 | 29 | $name = $message_match['name']; 30 | 31 | if (isset($process->mixins[$name])) { 32 | 33 | $mixable = $process->mixins[$name]; 34 | } 35 | elseif (isset($process->references[$name])) { 36 | 37 | $mixable = $process->references[$name]; 38 | } 39 | } 40 | 41 | // If no mixin or abstract rule matched, look for matching selector 42 | if (! $mixable) { 43 | 44 | $selector_test = Selector::makeReadable($message); 45 | 46 | if (isset($process->references[$selector_test])) { 47 | $mixable = $process->references[$selector_test]; 48 | } 49 | } 50 | 51 | // Avoid infinite recursion. 52 | if (! $mixable || $mixable === $context) { 53 | 54 | return false; 55 | } 56 | elseif ($mixable instanceof Mixin) { 57 | 58 | $args = []; 59 | $raw_args = isset($message_match['parens_content']) ? trim($message_match['parens_content']) : null; 60 | if ($raw_args) { 61 | $args = Util::splitDelimList($raw_args); 62 | } 63 | 64 | return DeclarationList::parse($mixable->template->__invoke($args), [ 65 | 'flatten' => true, 66 | 'context' => $mixable, 67 | ]); 68 | } 69 | elseif ($mixable instanceof Rule) { 70 | 71 | return $mixable->declarations->store; 72 | } 73 | } 74 | 75 | public static function merge(array $input, $message_list, $options = []) 76 | { 77 | $context = isset($options['context']) ? $options['context'] : null; 78 | 79 | $mixables = []; 80 | foreach (Util::splitDelimList($message_list) as $message) { 81 | if ($result = self::call($message, $context)) { 82 | $mixables = array_merge($mixables, $result); 83 | } 84 | } 85 | 86 | while ($mixable = array_shift($mixables)) { 87 | if ($mixable instanceof Declaration) { 88 | $input[] = $mixable; 89 | } 90 | else { 91 | list($property, $value) = $mixable; 92 | if ($property === 'mixin') { 93 | $input = Mixin::merge($input, $value, $options); 94 | } 95 | elseif (! empty($options['keyed'])) { 96 | $input[$property] = $value; 97 | } 98 | else { 99 | $input[] = [$property, $value]; 100 | } 101 | } 102 | } 103 | 104 | return $input; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/CssCrush/Options.php: -------------------------------------------------------------------------------- 1 | true, 16 | 'formatter' => null, 17 | 'versioning' => true, 18 | 'boilerplate' => true, 19 | 'vars' => [], 20 | 'cache' => true, 21 | 'context' => null, 22 | 'import_path' => null, 23 | 'output_file' => null, 24 | 'output_dir' => null, 25 | 'asset_dir' => null, 26 | 'doc_root' => null, 27 | 'vendor_target' => 'all', 28 | 'rewrite_import_urls' => true, 29 | 'plugins' => null, 30 | 'settings' => [], 31 | 'stat_dump' => false, 32 | 'source_map' => false, 33 | 'newlines' => 'use-platform', 34 | ]; 35 | 36 | public function __construct(array $options = [], ?Options $defaults = null) 37 | { 38 | $options = array_change_key_case($options, CASE_LOWER); 39 | 40 | if ($defaults) { 41 | $options += $defaults->get(); 42 | } 43 | 44 | if (! empty($options['enable'])) { 45 | if (empty($options['plugins'])) { 46 | $options['plugins'] = $options['enable']; 47 | } 48 | unset($options['enable']); 49 | } 50 | 51 | foreach ($options + self::$standardOptions as $name => $value) { 52 | $this->__set($name, $value); 53 | } 54 | } 55 | 56 | public function __set($name, $value) 57 | { 58 | $this->inputOptions[$name] = $value; 59 | 60 | switch ($name) { 61 | 62 | case 'formatter': 63 | if (is_string($value) && isset(Crush::$config->formatters[$value])) { 64 | $value = Crush::$config->formatters[$value]; 65 | } 66 | if (! is_callable($value)) { 67 | $value = null; 68 | } 69 | break; 70 | 71 | // Path options. 72 | case 'boilerplate': 73 | if (is_string($value)) { 74 | $value = Util::resolveUserPath($value); 75 | } 76 | break; 77 | 78 | case 'stat_dump': 79 | if (is_string($value)) { 80 | $value = Util::resolveUserPath($value, function ($path) { 81 | touch($path); 82 | return $path; 83 | }); 84 | } 85 | break; 86 | 87 | case 'output_dir': 88 | case 'asset_dir': 89 | if (is_string($value)) { 90 | $value = Util::resolveUserPath($value, function ($path) use ($name) { 91 | if (! @mkdir($path, 0755, true)) { 92 | warning("Could not create directory $path (setting `$name` option)."); 93 | } 94 | else { 95 | debug("Created directory $path (setting `$name` option)."); 96 | } 97 | return $path; 98 | }); 99 | } 100 | break; 101 | 102 | // Path options that only accept system paths. 103 | case 'context': 104 | case 'doc_root': 105 | if (is_string($value)) { 106 | $value = Util::normalizePath(realpath($value)); 107 | } 108 | break; 109 | 110 | case 'import_path': 111 | if ($value) { 112 | if (is_string($value)) { 113 | $value = preg_split('~\s*,\s*~', trim($value)); 114 | } 115 | $value = array_filter(array_map(function ($path) { 116 | return Util::normalizePath(realpath($path)); 117 | }, $value)); 118 | } 119 | break; 120 | 121 | // Options used internally as arrays. 122 | case 'plugins': 123 | $value = (array) $value; 124 | break; 125 | } 126 | 127 | $this->computedOptions[$name] = $value; 128 | } 129 | 130 | public function __get($name) 131 | { 132 | switch ($name) { 133 | case 'newlines': 134 | switch ($this->inputOptions[$name]) { 135 | case 'windows': 136 | case 'win': 137 | return "\r\n"; 138 | case 'unix': 139 | return "\n"; 140 | case 'use-platform': 141 | default: 142 | return PHP_EOL; 143 | } 144 | break; 145 | 146 | case 'minify': 147 | if (isset($this->computedOptions['formatter'])) { 148 | return false; 149 | } 150 | break; 151 | 152 | case 'formatter': 153 | if (empty($this->inputOptions['minify'])) { 154 | return isset($this->computedOptions['formatter']) ? 155 | $this->computedOptions['formatter'] : 'CssCrush\fmtr_block'; 156 | } 157 | } 158 | 159 | return isset($this->computedOptions[$name]) ? $this->computedOptions[$name] : null; 160 | } 161 | 162 | public function __isset($name) 163 | { 164 | return isset($this->inputOptions[$name]); 165 | } 166 | 167 | public function get($computed = false) 168 | { 169 | return $computed ? $this->computedOptions : self::filter($this->inputOptions); 170 | } 171 | 172 | public static function filter(?array $optionsArray = null) 173 | { 174 | return $optionsArray ? array_intersect_key($optionsArray, self::$standardOptions) : self::$standardOptions; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /lib/CssCrush/PostAliasFix.php: -------------------------------------------------------------------------------- 1 | ident = '[a-zA-Z0-9_-]+'; 24 | $classes->number = '[+-]?\d*\.?\d+'; 25 | $classes->percentage = $classes->number . '%'; 26 | $classes->length_unit = '(?i)(?:e[mx]|c[hm]|rem|v[hwm]|in|p[tcx])(?-i)'; 27 | $classes->length = $classes->number . $classes->length_unit; 28 | $classes->color_hex = '#[[:xdigit:]]{3}(?:[[:xdigit:]]{3})?'; 29 | 30 | // Tokens. 31 | $classes->token_id = '[0-9a-z]+'; 32 | $classes->c_token = '\?c' . $classes->token_id . '\?'; // Comments. 33 | $classes->s_token = '\?s' . $classes->token_id . '\?'; // Strings. 34 | $classes->r_token = '\?r' . $classes->token_id . '\?'; // Rules. 35 | $classes->u_token = '\?u' . $classes->token_id . '\?'; // URLs. 36 | $classes->t_token = '\?t' . $classes->token_id . '\?'; // Traces. 37 | $classes->a_token = '\?a(' . $classes->token_id . ')\?'; // Args. 38 | 39 | // Boundries. 40 | $classes->LB = '(?RB = '(?![\w-])'; // Right ident boundry. 42 | 43 | // Recursive block matching. 44 | $classes->block = '(?\{\s*(?(?:(?>[^{}]+)|(?&block))*)\})'; 45 | $classes->parens = '(?\(\s*(?(?:(?>[^()]+)|(?&parens))*)\))'; 46 | 47 | // Misc. 48 | $classes->vendor = '-[a-zA-Z]+-'; 49 | $classes->hex = '[[:xdigit:]]'; 50 | $classes->newline = '(\r\n?|\n)'; 51 | 52 | // Create standalone class patterns, add classes as class swaps. 53 | foreach ($classes as $name => $class) { 54 | $patt->{$name} = '~' . $class . '~S'; 55 | } 56 | 57 | // Rooted classes. 58 | $patt->rooted_ident = '~^' . $classes->ident . '$~'; 59 | $patt->rooted_number = '~^' . $classes->number . '$~'; 60 | 61 | // @-rules. 62 | $patt->import = Regex::make('~@import \s+ ({{u_token}}) \s? ([^;]*);~ixS'); 63 | $patt->charset = Regex::make('~@charset \s+ ({{s_token}}) \s*;~ixS'); 64 | $patt->mixin = Regex::make('~@mixin \s+ (?{{ident}}) \s* {{block}}~ixS'); 65 | $patt->fragmentCapture = Regex::make('~@fragment \s+ (?{{ident}}) \s* {{block}}~ixS'); 66 | $patt->fragmentInvoke = Regex::make('~@fragment \s+ (?{{ident}}) {{parens}}? \s* ;~ixS'); 67 | $patt->abstract = Regex::make('~^@abstract \s+ (?{{ident}})~ixS'); 68 | 69 | // Functions. 70 | $patt->functionTest = Regex::make('~{{ LB }} (?{{ ident }}) \(~xS'); 71 | $patt->thisFunction = Functions::makePattern(['this']); 72 | 73 | // Strings and comments. 74 | $patt->string = '~(\'|")(?:\\\\\1|[^\1])*?\1~xS'; 75 | $patt->commentAndString = '~ 76 | # Quoted string (to EOF if unmatched). 77 | (\'|"|`)(?:\\\\\1|[^\1])*?(?:\1|$) 78 | | 79 | # Block comment (to EOF if unmatched). 80 | /\*(?:[^*]*\*+(?:[^/*][^*]*\*+)*/|.*) 81 | ~xsS'; 82 | 83 | // Misc. 84 | $patt->vendorPrefix = '~^-([a-z]+)-([a-z-]+)~iS'; 85 | $patt->ruleDirective = '~^(?:(@include)|(@extends?)|(@name))[\s]+~iS'; 86 | $patt->argListSplit = '~\s*[,\s]\s*~S'; 87 | $patt->cruftyHex = Regex::make('~\#({{hex}})\1({{hex}})\2({{hex}})\3~S'); 88 | $patt->token = Regex::make('~^ \? (?[a-zA-Z]) {{token_id}} \? $~xS'); 89 | } 90 | 91 | public static function make($pattern) 92 | { 93 | static $cache = []; 94 | 95 | if (isset($cache[$pattern])) { 96 | return $cache[$pattern]; 97 | } 98 | 99 | return $cache[$pattern] = preg_replace_callback('~\{\{ *(?\w+) *\}\}~S', function ($m) { 100 | return Regex::$classes->{ $m['name'] }; 101 | }, $pattern); 102 | } 103 | 104 | public static function matchAll($patt, $subject, $offset = 0) 105 | { 106 | $count = preg_match_all($patt, $subject, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER, $offset); 107 | 108 | return $count ? $matches : []; 109 | } 110 | } 111 | 112 | Regex::init(); 113 | -------------------------------------------------------------------------------- /lib/CssCrush/Rule.php: -------------------------------------------------------------------------------- 1 | label = $process->tokens->createLabel('r'); 33 | $this->marker = $process->generateMap ? $traceToken : null; 34 | $this->selectors = new SelectorList($selectorString, $this); 35 | $this->declarations = new DeclarationList($declarationsString, $this); 36 | } 37 | 38 | public function __toString() 39 | { 40 | $process = Crush::$process; 41 | 42 | // Merge the extend selectors. 43 | $this->selectors->store += $this->extendSelectors; 44 | 45 | // Dereference and return empty string if there are no selectors or declarations. 46 | if (empty($this->selectors->store) || empty($this->declarations->store)) { 47 | $process->tokens->pop($this->label); 48 | 49 | return ''; 50 | } 51 | 52 | $stub = $this->marker; 53 | 54 | if ($process->minifyOutput) { 55 | return "$stub{$this->selectors->join()}{{$this->declarations->join()}}"; 56 | } 57 | else { 58 | return $stub . call_user_func($process->ruleFormatter, $this); 59 | } 60 | } 61 | 62 | public function __clone() 63 | { 64 | $this->selectors = clone $this->selectors; 65 | $this->declarations = clone $this->declarations; 66 | } 67 | 68 | 69 | ############################# 70 | # Rule inheritance. 71 | 72 | public function addExtendSelectors($rawValue) 73 | { 74 | foreach (Util::splitDelimList($rawValue) as $arg) { 75 | $extendArg = new ExtendArg($arg); 76 | $this->extendArgs[$extendArg->raw] = $extendArg; 77 | } 78 | } 79 | 80 | public function resolveExtendables() 81 | { 82 | if (! $this->extendArgs) { 83 | 84 | return false; 85 | } 86 | elseif (! $this->resolvedExtendables) { 87 | 88 | $references =& Crush::$process->references; 89 | 90 | // Filter the extendArgs list to usable references. 91 | $filtered = []; 92 | foreach ($this->extendArgs as $extendArg) { 93 | 94 | if (isset($references[$extendArg->name])) { 95 | $parentRule = $references[$extendArg->name]; 96 | $parentRule->resolveExtendables(); 97 | $extendArg->pointer = $parentRule; 98 | $filtered[$parentRule->label] = $extendArg; 99 | } 100 | } 101 | 102 | $this->resolvedExtendables = true; 103 | $this->extendArgs = $filtered; 104 | } 105 | 106 | return true; 107 | } 108 | 109 | public function applyExtendables() 110 | { 111 | if (! $this->resolveExtendables()) { 112 | 113 | return; 114 | } 115 | 116 | // Create a stack of all parent rule args. 117 | $parentExtendArgs = []; 118 | foreach ($this->extendArgs as $extendArg) { 119 | $parentExtendArgs += $extendArg->pointer->extendArgs; 120 | } 121 | 122 | // Merge this rule's extendArgs with parent extendArgs. 123 | $this->extendArgs += $parentExtendArgs; 124 | 125 | // Add this rule's selectors to all extendArgs. 126 | foreach ($this->extendArgs as $extendArg) { 127 | 128 | $ancestor = $extendArg->pointer; 129 | 130 | $extendSelectors = $this->selectors->store; 131 | 132 | // If there is a pseudo class extension create a new set accordingly. 133 | if ($extendArg->pseudo) { 134 | 135 | $extendSelectors = []; 136 | foreach ($this->selectors->store as $selector) { 137 | $newSelector = clone $selector; 138 | $newReadable = $newSelector->appendPseudo($extendArg->pseudo); 139 | $extendSelectors[$newReadable] = $newSelector; 140 | } 141 | } 142 | $ancestor->extendSelectors += $extendSelectors; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /lib/CssCrush/Selector.php: -------------------------------------------------------------------------------- 1 | allowPrefix = false; 21 | } 22 | 23 | $this->readableValue = Selector::makeReadable($rawSelector); 24 | 25 | $this->value = Selector::expandAliases($rawSelector); 26 | } 27 | 28 | public function __toString() 29 | { 30 | if (Crush::$process->minifyOutput) { 31 | // Trim whitespace around selector combinators. 32 | $this->value = preg_replace('~ ?([>\~+]) ?~S', '$1', $this->value); 33 | } 34 | else { 35 | $this->value = Selector::normalizeWhiteSpace($this->value); 36 | } 37 | return $this->value; 38 | } 39 | 40 | public function appendPseudo($pseudo) 41 | { 42 | // Check to avoid doubling-up. 43 | if (! StringObject::endsWith($this->readableValue, $pseudo)) { 44 | 45 | $this->readableValue .= $pseudo; 46 | $this->value .= $pseudo; 47 | } 48 | return $this->readableValue; 49 | } 50 | 51 | public static function normalizeWhiteSpace($str) 52 | { 53 | // Create space around combinators, then normalize whitespace. 54 | return Util::normalizeWhiteSpace(preg_replace('~([>+]|\~(?!=))~S', ' $1 ', $str)); 55 | } 56 | 57 | public static function makeReadable($str) 58 | { 59 | $str = Selector::normalizeWhiteSpace($str); 60 | 61 | // Quick test for string tokens. 62 | if (strpos($str, '?s') !== false) { 63 | $str = Crush::$process->tokens->restore($str, 's'); 64 | } 65 | 66 | return $str; 67 | } 68 | 69 | public static function expandAliases($str) 70 | { 71 | $process = Crush::$process; 72 | 73 | if (! $process->selectorAliases || ! preg_match($process->selectorAliasesPatt, $str)) { 74 | return $str; 75 | } 76 | 77 | while (preg_match_all($process->selectorAliasesPatt, $str, $m, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { 78 | 79 | $alias_call = end($m); 80 | $alias_name = strtolower($alias_call[1][0]); 81 | 82 | $start = $alias_call[0][1]; 83 | $length = strlen($alias_call[0][0]); 84 | $args = []; 85 | 86 | // It's a function alias if a start paren is matched. 87 | if (isset($alias_call[2])) { 88 | 89 | // Parse argument list. 90 | if (preg_match(Regex::$patt->parens, $str, $parens, PREG_OFFSET_CAPTURE, $start)) { 91 | $args = Functions::parseArgs($parens[2][0]); 92 | 93 | // Amend offsets. 94 | $paren_start = $parens[0][1]; 95 | $paren_len = strlen($parens[0][0]); 96 | $length = ($paren_start + $paren_len) - $start; 97 | } 98 | } 99 | 100 | $str = substr_replace($str, $process->selectorAliases[$alias_name]($args), $start, $length); 101 | } 102 | 103 | return $str; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/CssCrush/SelectorAlias.php: -------------------------------------------------------------------------------- 1 | handler = $handler; 17 | $this->type = $type; 18 | 19 | switch ($this->type) { 20 | case 'alias': 21 | $this->handler = new Template($handler); 22 | break; 23 | } 24 | } 25 | 26 | public function __invoke($args) 27 | { 28 | $handler = $this->handler; 29 | $tokens = Crush::$process->tokens; 30 | 31 | $splat_arg_patt = Regex::make('~#\((?{{ ident }})?\)~'); 32 | 33 | switch ($this->type) { 34 | case 'alias': 35 | return $handler($args); 36 | case 'callback': 37 | $template = new Template($handler($args)); 38 | return $template($args); 39 | case 'splat': 40 | $handler = $tokens->restore($handler, 's'); 41 | if ($args) { 42 | $list = []; 43 | foreach ($args as $arg) { 44 | $list[] = SelectorAlias::wrap( 45 | $tokens->capture(preg_replace($splat_arg_patt, $arg, $handler), 's') 46 | ); 47 | } 48 | $handler = implode(',', $list); 49 | } 50 | else { 51 | $handler = $tokens->capture(preg_replace_callback($splat_arg_patt, function ($m) { 52 | return $m['fallback']; 53 | }, $handler), 's'); 54 | } 55 | return SelectorAlias::wrap($handler); 56 | } 57 | } 58 | 59 | public static function wrap($str) 60 | { 61 | return strpos($str, ',') !== false ? ":any($str)" : $str; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/CssCrush/SelectorList.php: -------------------------------------------------------------------------------- 1 | abstract, $selector, $m)) { 20 | $rule->name = strtolower($m['name']); 21 | $rule->isAbstract = true; 22 | } 23 | else { 24 | $this->add(new Selector($selector)); 25 | } 26 | } 27 | } 28 | 29 | public function add(Selector $selector) 30 | { 31 | $this->store[$selector->readableValue] = $selector; 32 | } 33 | 34 | public function join($glue = ',') 35 | { 36 | return implode($glue, $this->store); 37 | } 38 | 39 | public function expand() 40 | { 41 | static $grouping_patt, $expand, $expandSelector; 42 | if (! $grouping_patt) { 43 | 44 | $grouping_patt = Regex::make('~\:any{{ parens }}~iS'); 45 | 46 | $expand = function ($selector_string) use ($grouping_patt) 47 | { 48 | if (preg_match($grouping_patt, $selector_string, $m, PREG_OFFSET_CAPTURE)) { 49 | 50 | list($full_match, $full_match_offset) = $m[0]; 51 | $before = substr($selector_string, 0, $full_match_offset); 52 | $after = substr($selector_string, strlen($full_match) + $full_match_offset); 53 | $selectors = []; 54 | 55 | // Allowing empty strings for more expansion possibilities. 56 | foreach (Util::splitDelimList($m['parens_content'][0], ['allow_empty_strings' => true]) as $segment) { 57 | if ($selector = trim("$before$segment$after")) { 58 | $selectors[$selector] = true; 59 | } 60 | } 61 | 62 | return $selectors; 63 | } 64 | 65 | return false; 66 | }; 67 | 68 | $expandSelector = function ($selector_string) use ($expand) 69 | { 70 | if ($running_stack = $expand($selector_string)) { 71 | 72 | $flattened_stack = []; 73 | do { 74 | $loop_stack = []; 75 | foreach ($running_stack as $selector => $bool) { 76 | $selectors = $expand($selector); 77 | if (! $selectors) { 78 | $flattened_stack += [$selector => true]; 79 | } 80 | else { 81 | $loop_stack += $selectors; 82 | } 83 | } 84 | $running_stack = $loop_stack; 85 | 86 | } while ($loop_stack); 87 | 88 | return $flattened_stack; 89 | } 90 | 91 | return [$selector_string => true]; 92 | }; 93 | } 94 | 95 | $expanded_set = []; 96 | 97 | foreach ($this->store as $original_selector) { 98 | if (stripos($original_selector->value, ':any(') !== false) { 99 | foreach ($expandSelector($original_selector->value) as $selector_string => $bool) { 100 | $new = new Selector($selector_string); 101 | $expanded_set[$new->readableValue] = $new; 102 | } 103 | } 104 | else { 105 | $expanded_set[$original_selector->readableValue] = $original_selector; 106 | } 107 | } 108 | 109 | $this->store = $expanded_set; 110 | } 111 | 112 | public function merge($rawSelectors) 113 | { 114 | $stack = []; 115 | 116 | foreach ($rawSelectors as $rawParentSelector) { 117 | foreach ($this->store as $selector) { 118 | 119 | $useParentSymbol = strpos($selector->value, '&') !== false; 120 | 121 | if (! $selector->allowPrefix && ! $useParentSymbol) { 122 | $stack[$selector->readableValue] = $selector; 123 | } 124 | elseif ($useParentSymbol) { 125 | $new = new Selector(str_replace('&', $rawParentSelector, $selector->value)); 126 | $stack[$new->readableValue] = $new; 127 | } 128 | else { 129 | $new = new Selector("$rawParentSelector {$selector->value}"); 130 | $stack[$new->readableValue] = $new; 131 | } 132 | } 133 | } 134 | $this->store = $stack; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /lib/CssCrush/StringObject.php: -------------------------------------------------------------------------------- 1 | raw = $str; 16 | } 17 | 18 | public function __toString() 19 | { 20 | return $this->raw; 21 | } 22 | 23 | public static function endsWith($haystack, $needle) 24 | { 25 | return substr($haystack, -strlen($needle)) === $needle; 26 | } 27 | 28 | public function update($str) 29 | { 30 | $this->raw = $str; 31 | 32 | return $this; 33 | } 34 | 35 | public function substr($start, $length = null) 36 | { 37 | if (! isset($length)) { 38 | 39 | return substr($this->raw, $start); 40 | } 41 | else { 42 | 43 | return substr($this->raw, $start, $length); 44 | } 45 | } 46 | 47 | public function matchAll($patt, $offset = 0) 48 | { 49 | return Regex::matchAll($patt, $this->raw, $offset); 50 | } 51 | 52 | public function replaceHash($replacements) 53 | { 54 | if ($replacements) { 55 | $this->raw = str_replace( 56 | array_keys($replacements), 57 | array_values($replacements), 58 | $this->raw); 59 | } 60 | return $this; 61 | } 62 | 63 | public function pregReplaceHash($replacements) 64 | { 65 | if ($replacements) { 66 | $this->raw = preg_replace( 67 | array_keys($replacements), 68 | array_values($replacements), 69 | $this->raw); 70 | } 71 | return $this; 72 | } 73 | 74 | public function pregReplaceCallback($patt, $callback) 75 | { 76 | $this->raw = preg_replace_callback($patt, $callback, $this->raw); 77 | return $this; 78 | } 79 | 80 | public function append($append) 81 | { 82 | $this->raw .= $append; 83 | return $this; 84 | } 85 | 86 | public function prepend($prepend) 87 | { 88 | $this->raw = $prepend . $this->raw; 89 | return $this; 90 | } 91 | 92 | public function splice($replacement, $offset, $length = null) 93 | { 94 | $this->raw = substr_replace($this->raw, $replacement, $offset, $length); 95 | return $this; 96 | } 97 | 98 | public function trim() 99 | { 100 | $this->raw = trim($this->raw); 101 | return $this; 102 | } 103 | 104 | public function rTrim() 105 | { 106 | $this->raw = rtrim($this->raw); 107 | return $this; 108 | } 109 | 110 | public function lTrim() 111 | { 112 | $this->raw = ltrim($this->raw); 113 | return $this; 114 | } 115 | 116 | public function restore($types, $release = false, $callback = null) 117 | { 118 | $this->raw = Crush::$process->tokens->restore($this->raw, $types, $release, $callback); 119 | 120 | return $this; 121 | } 122 | 123 | public function captureDirectives($directive, $parse_options = []) 124 | { 125 | if (is_array($directive)) { 126 | $directive = '(?:' . implode('|', $directive) . ')'; 127 | } 128 | 129 | $parse_options += [ 130 | 'keyed' => true, 131 | 'lowercase_keys' => true, 132 | 'ignore_directives' => true, 133 | 'singles' => false, 134 | 'flatten' => false, 135 | ]; 136 | 137 | if ($parse_options['singles']) { 138 | $patt = Regex::make('~@(?i)' . $directive . '(?-i)(?:\s*{{ block }}|\s+(?{{ ident }})\s+(?[^;]+)\s*;)~S'); 139 | } 140 | else { 141 | $patt = Regex::make('~@(?i)' . $directive . '(?-i)\s*{{ block }}~S'); 142 | } 143 | 144 | $captured_directives = []; 145 | $this->pregReplaceCallback($patt, function ($m) use (&$captured_directives, $parse_options) { 146 | if (isset($m['name'])) { 147 | $name = $parse_options['lowercase_keys'] ? strtolower($m['name']) : $m['name']; 148 | $captured_directives[$name] = $m['value']; 149 | } 150 | else { 151 | $captured_directives = DeclarationList::parse($m['block_content'], $parse_options) + $captured_directives; 152 | } 153 | return ''; 154 | }); 155 | 156 | return $captured_directives; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /lib/CssCrush/Template.php: -------------------------------------------------------------------------------- 1 | defaults[$position] = $defaultValue; 49 | } 50 | 51 | // Update argument count. 52 | $argNumber = ((int) $position) + 1; 53 | $self->argCount = max($self->argCount, $argNumber); 54 | 55 | return "?a$position?"; 56 | }; 57 | 58 | $templateFunctions->register['#'] = $captureCallback; 59 | 60 | $this->string = $templateFunctions->apply($str); 61 | } 62 | 63 | public function __invoke(?array $args = null, $str = null) 64 | { 65 | $str = isset($str) ? $str : $this->string; 66 | 67 | // Apply passed arguments as priority. 68 | if (isset($args)) { 69 | 70 | list($find, $replace) = $this->prepare($args, false); 71 | } 72 | 73 | // Secondly use prepared substitutions if available. 74 | elseif ($this->substitutions) { 75 | 76 | list($find, $replace) = $this->substitutions; 77 | } 78 | 79 | // Apply substitutions. 80 | if (isset($find) && isset($replace)) { 81 | $str = str_replace($find, $replace, $str); 82 | } 83 | 84 | return Template::tokenize($str); 85 | } 86 | 87 | public function getArgValue($index, &$args) 88 | { 89 | // First lookup a passed value. 90 | if (isset($args[$index]) && $args[$index] !== 'default') { 91 | 92 | return $args[$index]; 93 | } 94 | 95 | // Get a default value. 96 | $default = isset($this->defaults[$index]) ? $this->defaults[$index] : ''; 97 | 98 | // Recurse for nested arg() calls. 99 | while (preg_match(Regex::$patt->a_token, $default, $m)) { 100 | $default = str_replace( 101 | $m[0], 102 | $this->getArgValue((int) $m[1], $args), 103 | $default); 104 | } 105 | 106 | return $default; 107 | } 108 | 109 | public function prepare(array $args, $persist = true) 110 | { 111 | // Create table of substitutions. 112 | $find = []; 113 | $replace = []; 114 | 115 | if ($this->argCount) { 116 | 117 | $argIndexes = range(0, $this->argCount-1); 118 | 119 | foreach ($argIndexes as $index) { 120 | $find[] = "?a$index?"; 121 | $replace[] = $this->getArgValue($index, $args); 122 | } 123 | } 124 | 125 | $substitutions = [$find, $replace]; 126 | 127 | // Persist substitutions by default. 128 | if ($persist) { 129 | $this->substitutions = $substitutions; 130 | } 131 | 132 | return $substitutions; 133 | } 134 | 135 | public static function tokenize($str) 136 | { 137 | $str = Crush::$process->tokens->capture($str, 's'); 138 | $str = Crush::$process->tokens->capture($str, 'u'); 139 | 140 | return $str; 141 | } 142 | 143 | public static function unTokenize($str) 144 | { 145 | $str = Crush::$process->tokens->restore($str, ['u', 's']); 146 | 147 | return $str; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lib/CssCrush/Tokens.php: -------------------------------------------------------------------------------- 1 | store = new \stdClass; 25 | $this->ids = new \stdClass; 26 | 27 | foreach ($types as $type) { 28 | $this->store->$type = []; 29 | $this->ids->$type = 0; 30 | } 31 | } 32 | 33 | public function get($label) 34 | { 35 | $path =& $this->store->{$label[1]}; 36 | 37 | return isset($path[$label]) ? $path[$label] : null; 38 | } 39 | 40 | public function pop($label) 41 | { 42 | $value = $this->get($label); 43 | if (isset($value)) { 44 | unset($this->store->{$label[1]}[$label]); 45 | } 46 | 47 | return $value; 48 | } 49 | 50 | public function add($value, $type = null, $existing_label = null) 51 | { 52 | if ($value instanceof Url) { 53 | $type = 'u'; 54 | } 55 | elseif ($value instanceof Rule) { 56 | $type = 'r'; 57 | } 58 | $label = $existing_label ? $existing_label : $this->createLabel($type); 59 | $this->store->{$type}[$label] = $value; 60 | 61 | return $label; 62 | } 63 | 64 | public function createLabel($type) 65 | { 66 | $counter = base_convert(++$this->ids->$type, 10, 36); 67 | 68 | return "?$type$counter?"; 69 | } 70 | 71 | public function restore($str, $types, $release = false, $callback = null) 72 | { 73 | $types = implode('', (array) $types); 74 | $patt = Regex::make("~\?[$types]{{ token_id }}\?~S"); 75 | $tokens = $this; 76 | $callback = $callback ?: function ($m) use ($tokens, $release) { 77 | return $release ? $tokens->pop($m[0]) : $tokens->get($m[0]); 78 | }; 79 | 80 | return preg_replace_callback($patt, $callback, $str); 81 | } 82 | 83 | public function capture($str, $type) 84 | { 85 | switch ($type) { 86 | case 'u': 87 | return $this->captureUrls($str); 88 | break; 89 | case 's': 90 | return preg_replace_callback(Regex::$patt->string, function ($m) { 91 | return Crush::$process->tokens->add($m[0], 's'); 92 | }, $str); 93 | } 94 | } 95 | 96 | public function captureUrls($str, $add_padding = false) 97 | { 98 | $count = preg_match_all( 99 | Regex::make('~@import \s+ (?{{s_token}}) | {{LB}} (?url|data-uri) {{parens}}~ixS'), 100 | $str, 101 | $m, 102 | PREG_OFFSET_CAPTURE); 103 | 104 | while ($count--) { 105 | 106 | list($full_text, $full_offset) = $m[0][$count]; 107 | list($import_text, $import_offset) = $m['import'][$count]; 108 | 109 | // @import directive. 110 | if ($import_offset !== -1) { 111 | 112 | $label = $this->add(new Url(trim($import_text))); 113 | $str = str_replace($import_text, $add_padding ? str_pad($label, strlen($import_text)) : $label, $str); 114 | } 115 | 116 | // A URL function. 117 | else { 118 | $func_name = strtolower($m['func'][$count][0]); 119 | 120 | $url = new Url(trim($m['parens_content'][$count][0])); 121 | $url->convertToData = 'data-uri' === $func_name; 122 | $label = $this->add($url); 123 | $str = substr_replace( 124 | $str, 125 | $add_padding ? Tokens::pad($label, $full_text) : $label, 126 | $full_offset, 127 | strlen($full_text)); 128 | } 129 | } 130 | 131 | return $str; 132 | } 133 | 134 | public static function pad($label, $replaced_text) 135 | { 136 | // Padding token labels to maintain whitespace and newlines. 137 | if (($last_newline_pos = strrpos($replaced_text, "\n")) !== false) { 138 | $label .= str_repeat("\n", substr_count($replaced_text, "\n")) . str_repeat(' ', strlen(substr($replaced_text, $last_newline_pos))-1); 139 | } 140 | else { 141 | $label = str_pad($label, strlen($replaced_text)); 142 | } 143 | 144 | return $label; 145 | } 146 | 147 | public static function is($label, $of_type) 148 | { 149 | if (preg_match(Regex::$patt->token, $label, $m)) { 150 | 151 | return $of_type ? ($of_type === $m['type']) : true; 152 | } 153 | 154 | return false; 155 | } 156 | 157 | public static function test($value) 158 | { 159 | return preg_match(Regex::$patt->token, $value, $m) ? $m['type'] : false; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /lib/CssCrush/Url.php: -------------------------------------------------------------------------------- 1 | s_token, $raw_value)) { 26 | $this->value = trim(Crush::$process->tokens->pop($raw_value), '\'"'); 27 | } 28 | else { 29 | $this->value = $raw_value; 30 | } 31 | 32 | $this->originalValue = $this->value; 33 | $this->evaluate(); 34 | } 35 | 36 | public function __toString() 37 | { 38 | if ($this->convertToData) { 39 | $this->toData(); 40 | } 41 | 42 | if ($this->isRelative || $this->isRooted) { 43 | $this->simplify(); 44 | } 45 | 46 | if ($this->isData) { 47 | return 'url("' . preg_replace('~(?value) . '")'; 48 | } 49 | 50 | // Only wrap url with quotes if it contains tricky characters. 51 | $quote = ''; 52 | if (preg_match('~[()*\s]~S', $this->value)) { 53 | $quote = '"'; 54 | } 55 | 56 | return "url($quote$this->value$quote)"; 57 | } 58 | 59 | public function update($new_value) 60 | { 61 | $this->value = $new_value; 62 | 63 | return $this->evaluate(); 64 | } 65 | 66 | public function evaluate() 67 | { 68 | // Protocol, protocol-relative (//) or fragment URL. 69 | if (preg_match('~^(?: (?[a-z]+)\: | \/{2} | \# )~ix', $this->value, $m)) { 70 | 71 | $this->protocol = ! empty($m['protocol']) ? strtolower($m['protocol']) : 'relative'; 72 | 73 | switch ($this->protocol) { 74 | case 'data': 75 | $type = 'data'; 76 | break; 77 | default: 78 | $type = 'absolute'; 79 | break; 80 | } 81 | } 82 | // Relative and rooted URLs. 83 | else { 84 | $type = 'relative'; 85 | $leading_variable = strpos($this->value, '$(') === 0; 86 | 87 | // Normalize './' led paths. 88 | $this->value = preg_replace('~^\.\/+~i', '', $this->value); 89 | 90 | if ($leading_variable || ($this->value !== '' && $this->value[0] === '/')) { 91 | $type = 'rooted'; 92 | } 93 | 94 | // Normalize slashes. 95 | $this->value = rtrim(preg_replace('~[\\\\/]+~', '/', $this->value), '/'); 96 | } 97 | 98 | $this->setType($type); 99 | 100 | return $this; 101 | } 102 | 103 | public function isRelativeImplicit() 104 | { 105 | return $this->isRelative && preg_match('~^([\w$-]|\.[^\/.])~', $this->originalValue); 106 | } 107 | 108 | public function getAbsolutePath() 109 | { 110 | $path = false; 111 | if ($this->protocol) { 112 | $path = $this->value; 113 | } 114 | elseif ($this->isRelative || $this->isRooted) { 115 | $path = Crush::$process->docRoot . 116 | ($this->isRelative ? $this->toRoot()->simplify()->value : $this->value); 117 | } 118 | return $path; 119 | } 120 | 121 | public function prepend($path_fragment) 122 | { 123 | if ($this->isRelative) { 124 | $this->value = rtrim($path_fragment, DIRECTORY_SEPARATOR) 125 | . DIRECTORY_SEPARATOR 126 | . ltrim($this->value, DIRECTORY_SEPARATOR); 127 | } 128 | 129 | return $this; 130 | } 131 | 132 | public function toRoot() 133 | { 134 | if ($this->isRelative) { 135 | $this->prepend(Crush::$process->input->dirUrl . '/'); 136 | $this->setType('rooted'); 137 | } 138 | 139 | return $this; 140 | } 141 | 142 | public function toData() 143 | { 144 | // Only make one conversion attempt. 145 | $this->convertToData = false; 146 | 147 | $file = Crush::$process->docRoot . $this->toRoot()->value; 148 | 149 | // File not found. 150 | if (! file_exists($file)) { 151 | 152 | return $this; 153 | } 154 | 155 | $file_ext = pathinfo($file, PATHINFO_EXTENSION); 156 | 157 | // Only allow certain extensions 158 | static $allowed_file_extensions = [ 159 | 'woff' => 'application/x-font-woff;charset=utf-8', 160 | 'ttf' => 'font/truetype;charset=utf-8', 161 | 'svg' => 'image/svg+xml', 162 | 'svgz' => 'image/svg+xml', 163 | 'gif' => 'image/gif', 164 | 'jpeg' => 'image/jpg', 165 | 'jpg' => 'image/jpg', 166 | 'png' => 'image/png', 167 | ]; 168 | 169 | if (! isset($allowed_file_extensions[$file_ext])) { 170 | 171 | return $this; 172 | } 173 | 174 | $mime_type = $allowed_file_extensions[$file_ext]; 175 | $file_contents = file_get_contents($file); 176 | 177 | if ($file_ext === 'svg') { 178 | $string = preg_replace('/\R/', '%0A', trim($file_contents)); 179 | $this->value = "data:$mime_type;utf8,$string"; 180 | } 181 | else { 182 | $base64 = base64_encode($file_contents); 183 | $this->value = "data:$mime_type;base64,$base64"; 184 | } 185 | 186 | $this->setType('data')->protocol = 'data'; 187 | 188 | return $this; 189 | } 190 | 191 | public function setType($type = 'absolute') 192 | { 193 | $this->isAbsolute = false; 194 | $this->isRooted = false; 195 | $this->isRelative = false; 196 | $this->isData = false; 197 | 198 | switch ($type) { 199 | case 'absolute': 200 | $this->isAbsolute = true; 201 | break; 202 | case 'relative': 203 | $this->isRelative = true; 204 | break; 205 | case 'rooted': 206 | $this->isRooted = true; 207 | break; 208 | case 'data': 209 | $this->isData = true; 210 | $this->convertToData = false; 211 | break; 212 | } 213 | 214 | return $this; 215 | } 216 | 217 | public function simplify() 218 | { 219 | if ($this->isRelative || $this->isRooted) { 220 | $this->value = Util::simplifyPath($this->value); 221 | } 222 | return $this; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /lib/CssCrush/Util.php: -------------------------------------------------------------------------------- 1 | $b_index ? 1 : -1; 26 | } 27 | elseif ($a_found && ! $b_found) { 28 | return -1; 29 | } 30 | elseif ($b_found && ! $a_found) { 31 | return 1; 32 | } 33 | 34 | return strcmp($a, $b); 35 | }); 36 | } 37 | 38 | $str = ''; 39 | foreach ($attributes as $name => $value) { 40 | $value = htmlspecialchars($value, ENT_COMPAT, 'UTF-8', false); 41 | $str .= " $name=\"$value\""; 42 | } 43 | return $str; 44 | } 45 | 46 | public static function normalizePath($path, $strip_drive_letter = false) 47 | { 48 | if (! $path) { 49 | return ''; 50 | } 51 | 52 | if ($strip_drive_letter) { 53 | $path = preg_replace('~^[a-z]\:~i', '', $path); 54 | } 55 | 56 | // Backslashes and repeat slashes to a single forward slash. 57 | $path = rtrim(preg_replace('~[\\\\/]+~', '/', $path), '/'); 58 | 59 | // Removing redundant './'. 60 | $path = str_replace('/./', '/', $path); 61 | if (strpos($path, './') === 0) { 62 | $path = substr($path, 2); 63 | } 64 | 65 | return Util::simplifyPath($path); 66 | } 67 | 68 | public static function simplifyPath($path) 69 | { 70 | // Reduce redundant path segments. e.g 'foo/../bar' => 'bar' 71 | $patt = '~[^/.]+/\.\./~S'; 72 | while (preg_match($patt, $path)) { 73 | $path = preg_replace($patt, '', $path); 74 | } 75 | return $path; 76 | } 77 | 78 | public static function resolveUserPath($path, ?callable $recovery = null, $docRoot = null) 79 | { 80 | // System path. 81 | if ($realpath = realpath($path)) { 82 | $path = $realpath; 83 | } 84 | else { 85 | if (! $docRoot) { 86 | $docRoot = isset(Crush::$process->docRoot) ? Crush::$process->docRoot : Crush::$config->docRoot; 87 | } 88 | 89 | // Absolute path. 90 | if (strpos($path, '/') === 0) { 91 | // If $path is not doc_root based assume it's doc_root relative and prepend doc_root. 92 | if (strpos($path, $docRoot) !== 0) { 93 | $path = $docRoot . $path; 94 | } 95 | } 96 | // Relative path. Try resolving based on the directory of the executing script. 97 | else { 98 | $path = Crush::$config->scriptDir . '/' . $path; 99 | } 100 | 101 | if (! file_exists($path) && $recovery) { 102 | $path = $recovery($path); 103 | } 104 | $path = realpath($path); 105 | } 106 | 107 | return $path ? Util::normalizePath($path) : false; 108 | } 109 | 110 | public static function stripCommentTokens($str) 111 | { 112 | return preg_replace(Regex::$patt->c_token, '', $str); 113 | } 114 | 115 | public static function normalizeWhiteSpace($str) 116 | { 117 | static $find, $replace; 118 | if (! $find) { 119 | $replacements = [ 120 | // Convert all whitespace sequences to a single space. 121 | '~\s+~S' => ' ', 122 | // Trim bracket whitespace where it's safe to do it. 123 | '~([\[(]) | ([\])])| ?([{}]) ?~S' => '${1}${2}${3}', 124 | // Trim whitespace around delimiters and special characters. 125 | '~ ?([;,]) ?~S' => '$1', 126 | ]; 127 | $find = array_keys($replacements); 128 | $replace = array_values($replacements); 129 | } 130 | 131 | return preg_replace($find, $replace, $str); 132 | } 133 | 134 | public static function splitDelimList($str, $options = []) 135 | { 136 | extract($options + [ 137 | 'delim' => ',', 138 | 'regex' => false, 139 | 'allow_empty_strings' => false, 140 | ]); 141 | 142 | $str = trim($str); 143 | 144 | if (! $regex && strpos($str, $delim) === false) { // @phpstan-ignore-line variable.undefined 145 | return ! $allow_empty_strings && ! strlen($str) ? [] : [$str]; // @phpstan-ignore-line variable.undefined 146 | } 147 | 148 | if ($match_count = preg_match_all(Regex::$patt->parens, $str, $matches)) { 149 | $keys = []; 150 | foreach ($matches[0] as $index => &$value) { 151 | $keys[] = "?$index?"; 152 | } 153 | $str = str_replace($matches[0], $keys, $str); 154 | } 155 | 156 | // @phpstan-ignore-next-line variable.undefined 157 | $list = $regex ? preg_split($regex, $str) : explode($delim, $str); 158 | 159 | if ($match_count) { 160 | foreach ($list as &$value) { 161 | $value = str_replace($keys, $matches[0], $value); 162 | } 163 | } 164 | 165 | $list = array_map('trim', $list); 166 | 167 | // @phpstan-ignore-next-line variable.undefined 168 | return ! $allow_empty_strings ? array_filter($list, 'strlen') : $list; 169 | } 170 | 171 | public static function getLinkBetweenPaths($path1, $path2, $directories = true) 172 | { 173 | $path1 = trim(Util::normalizePath($path1, true), '/'); 174 | $path2 = trim(Util::normalizePath($path2, true), '/'); 175 | 176 | $link = ''; 177 | 178 | if ($path1 != $path2) { 179 | 180 | // Split the directory paths into arrays so we can compare segment by segment. 181 | $path1_segs = explode('/', $path1); 182 | $path2_segs = explode('/', $path2); 183 | 184 | // Shift the segments until they are on different branches. 185 | while (isset($path1_segs[0]) && isset($path2_segs[0]) && ($path1_segs[0] === $path2_segs[0])) { 186 | array_shift($path1_segs); 187 | array_shift($path2_segs); 188 | } 189 | 190 | $link = str_repeat('../', count($path1_segs)) . implode('/', $path2_segs); 191 | } 192 | 193 | $link = $link !== '' ? rtrim($link, '/') : ''; 194 | 195 | // Append end slash if getting a link between directories. 196 | if ($link && $directories) { 197 | $link .= '/'; 198 | } 199 | 200 | return $link; 201 | } 202 | 203 | public static function filePutContents($file, $str) 204 | { 205 | if ($stream = fopen($file, 'w')) { 206 | fwrite($stream, $str); 207 | fclose($stream); 208 | 209 | return true; 210 | } 211 | 212 | warning("Could not write file '$file'."); 213 | 214 | return false; 215 | } 216 | 217 | public static function parseIni($path, $sections = false) 218 | { 219 | if (! ($result = @parse_ini_file($path, $sections))) { 220 | notice("Ini file '$path' could not be parsed."); 221 | 222 | return false; 223 | } 224 | return $result; 225 | } 226 | 227 | public static function readConfigFile($path) 228 | { 229 | require_once $path; 230 | return Options::filter(get_defined_vars()); 231 | } 232 | 233 | /* 234 | * Get raw value (useful if testing values that may or may not be a token). 235 | */ 236 | public static function rawValue($value) 237 | { 238 | if ($tokenType = Tokens::test($value)) { 239 | if ($tokenType == 'u') { 240 | $value = Crush::$process->tokens->get($value)->value; 241 | } 242 | elseif ($tokenType == 's') { 243 | $value = Crush::$process->tokens->get($value); 244 | } 245 | } 246 | 247 | return $value; 248 | } 249 | 250 | /* 251 | * Encode integer to Base64 VLQ. 252 | */ 253 | public static function vlqEncode($value) 254 | { 255 | static $VLQ_BASE_SHIFT, $VLQ_BASE, $VLQ_BASE_MASK, $VLQ_CONTINUATION_BIT, $BASE64_MAP; 256 | if (! $VLQ_BASE_SHIFT) { 257 | $VLQ_BASE_SHIFT = 5; 258 | $VLQ_BASE = 1 << $VLQ_BASE_SHIFT; 259 | $VLQ_BASE_MASK = $VLQ_BASE - 1; 260 | $VLQ_CONTINUATION_BIT = $VLQ_BASE; 261 | $BASE64_MAP = str_split('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'); 262 | } 263 | 264 | $vlq = $value < 0 ? ((-$value) << 1) + 1 : ($value << 1) + 0; 265 | 266 | $encoded = ""; 267 | do { 268 | $digit = $vlq & $VLQ_BASE_MASK; 269 | $vlq >>= $VLQ_BASE_SHIFT; 270 | if ($vlq > 0) { 271 | $digit |= $VLQ_CONTINUATION_BIT; 272 | } 273 | $encoded .= $BASE64_MAP[$digit]; 274 | 275 | } while ($vlq > 0); 276 | 277 | return $encoded; 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /lib/CssCrush/Version.php: -------------------------------------------------------------------------------- 1 | \d+) 23 | (?:\.(?\d+))? 24 | (?:\.(?\d+))? 25 | (?:-(?.+))? 26 | $~ix', 27 | $version_string, 28 | $version); 29 | 30 | if ($version) { 31 | $this->major = (int) $version['major']; 32 | $this->minor = isset($version['minor']) ? (int) $version['minor'] : 0; 33 | $this->patch = isset($version['patch']) ? (int) $version['patch'] : 0; 34 | $this->extra = isset($version['extra']) ? $version['extra'] : null; 35 | } 36 | } 37 | 38 | public function __toString() 39 | { 40 | $out = (string) $this->major; 41 | 42 | if (isset($this->minor)) { 43 | $out .= ".$this->minor"; 44 | } 45 | if (isset($this->patch)) { 46 | $out .= ".$this->patch"; 47 | } 48 | if (isset($this->extra)) { 49 | $out .= "-$this->extra"; 50 | } 51 | 52 | return "v$out"; 53 | } 54 | 55 | public function compare($version_string) 56 | { 57 | $LESS = -1; 58 | $MORE = 1; 59 | $EQUAL = 0; 60 | 61 | $test = new Version($version_string); 62 | 63 | foreach (['major', 'minor', 'patch'] as $level) { 64 | 65 | if ($this->{$level} < $test->{$level}) { 66 | 67 | return $LESS; 68 | } 69 | elseif ($this->{$level} > $test->{$level}) { 70 | 71 | return $MORE; 72 | } 73 | } 74 | 75 | return $EQUAL; 76 | } 77 | 78 | public static function detect() { 79 | return self::gitDescribe() ?: self::packageDescribe(); 80 | } 81 | 82 | public static function gitDescribe() 83 | { 84 | static $attempted, $version; 85 | if (! $attempted && file_exists(Crush::$dir . '/.git')) { 86 | $attempted = true; 87 | $command = 'cd ' . escapeshellarg(Crush::$dir) . ' && git describe --tag --long'; 88 | @exec($command, $lines); 89 | if ($lines) { 90 | $version = new Version(trim($lines[0])); 91 | if (is_null($version->major)) { 92 | $version = null; 93 | } 94 | } 95 | } 96 | 97 | return $version; 98 | } 99 | 100 | public static function packageDescribe() 101 | { 102 | static $attempted, $version; 103 | if (! $attempted && file_exists(Crush::$dir . '/package.json')) { 104 | $attempted = true; 105 | $package = json_decode(file_get_contents(Crush::$dir . '/package.json')); 106 | if ($package->version) { 107 | $version = new Version($package->version); 108 | if (is_null($version->major)) { 109 | $version = null; 110 | } 111 | } 112 | } 113 | 114 | return $version; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/functions.php: -------------------------------------------------------------------------------- 1 | 'file', 'data' => $file]); 18 | } 19 | catch (\Exception $e) { 20 | CssCrush\warning($e->getMessage()); 21 | 22 | return ''; 23 | } 24 | 25 | return new CssCrush\File(Crush::$process); 26 | } 27 | 28 | 29 | /** 30 | * Process CSS file and return an HTML link tag with populated href. 31 | * 32 | * @see docs/api/functions.md 33 | */ 34 | function csscrush_tag($file, $options = [], $tag_attributes = []) { 35 | 36 | $file = csscrush_file($file, $options); 37 | if ($file && $file->url) { 38 | $tag_attributes['href'] = $file->url; 39 | $tag_attributes += [ 40 | 'rel' => 'stylesheet', 41 | 'media' => 'all', 42 | ]; 43 | $attrs = CssCrush\Util::htmlAttributes($tag_attributes, ['rel', 'href', 'media']); 44 | 45 | return "\n"; 46 | } 47 | } 48 | 49 | 50 | /** 51 | * Process CSS file and return CSS as text wrapped in html style tags. 52 | * 53 | * @see docs/api/functions.md 54 | */ 55 | function csscrush_inline($file, $options = [], $tag_attributes = []) { 56 | 57 | if (! is_array($options)) { 58 | $options = []; 59 | } 60 | if (! isset($options['boilerplate'])) { 61 | $options['boilerplate'] = false; 62 | } 63 | 64 | $file = csscrush_file($file, $options); 65 | if ($file && $file->path) { 66 | $tagOpen = ''; 67 | $tagClose = ''; 68 | if (is_array($tag_attributes)) { 69 | $attrs = CssCrush\Util::htmlAttributes($tag_attributes); 70 | $tagOpen = ""; 71 | $tagClose = ''; 72 | } 73 | return $tagOpen . file_get_contents($file->path) . $tagClose . "\n"; 74 | } 75 | } 76 | 77 | 78 | /** 79 | * Compile a raw string of CSS string and return it. 80 | * 81 | * @see docs/api/functions.md 82 | */ 83 | function csscrush_string($string, $options = []) { 84 | 85 | if (! isset($options['boilerplate'])) { 86 | $options['boilerplate'] = false; 87 | } 88 | 89 | Crush::$process = new CssCrush\Process($options, ['type' => 'filter', 'data' => $string]); 90 | 91 | return Crush::$process->compile()->__toString(); 92 | } 93 | 94 | 95 | /** 96 | * Set default options and config settings. 97 | * 98 | * @see docs/api/functions.md 99 | */ 100 | function csscrush_set($object_name, $modifier) { 101 | 102 | if (in_array($object_name, ['options', 'config'])) { 103 | 104 | $pointer = $object_name === 'options' ? Crush::$config->options : Crush::$config; 105 | 106 | if (is_callable($modifier)) { 107 | $modifier($pointer); 108 | } 109 | elseif (is_array($modifier)) { 110 | foreach ($modifier as $key => $value) { 111 | $pointer->{$key} = $value; 112 | } 113 | } 114 | } 115 | } 116 | 117 | 118 | /** 119 | * Get default options and config settings. 120 | * 121 | * @see docs/api/functions.md 122 | */ 123 | function csscrush_get($object_name, $property = null) { 124 | 125 | if (in_array($object_name, ['options', 'config'])) { 126 | 127 | $pointer = $object_name === 'options' ? Crush::$config->options : Crush::$config; 128 | 129 | if (! isset($property)) { 130 | return $pointer; 131 | } 132 | else { 133 | return isset($pointer->{$property}) ? $pointer->{$property} : null; 134 | } 135 | } 136 | return null; 137 | } 138 | 139 | 140 | /** 141 | * Add plugin. 142 | * 143 | * @see docs/api/functions.md 144 | */ 145 | function csscrush_plugin($name, callable $callback) { 146 | 147 | Crush::plugin($name, $callback); 148 | } 149 | 150 | 151 | /** 152 | * Get stats from most recent compile. 153 | * 154 | * @see docs/api/functions.md 155 | */ 156 | function csscrush_stat() { 157 | 158 | $process = Crush::$process; 159 | $stats = $process->stat; 160 | 161 | // Get logged errors as late as possible. 162 | $stats['errors'] = $process->errors; 163 | $stats['warnings'] = $process->warnings; 164 | $stats += ['compile_time' => 0]; 165 | 166 | return $stats; 167 | } 168 | -------------------------------------------------------------------------------- /misc/color-keywords.ini: -------------------------------------------------------------------------------- 1 | ; Sources: 2 | ; http://www.w3.org/TR/css3-color 3 | 4 | aliceblue = "240,248,255" 5 | antiquewhite = "250,235,215" 6 | aqua = "0,255,255" 7 | aquamarine = "127,255,212" 8 | azure = "240,255,255" 9 | beige = "245,245,220" 10 | bisque = "255,228,196" 11 | black = "0,0,0" 12 | blanchedalmond = "255,235,205" 13 | blue = "0,0,255" 14 | blueviolet = "138,43,226" 15 | brown = "165,42,42" 16 | burlywood = "222,184,135" 17 | cadetblue = "95,158,160" 18 | chartreuse = "127,255,0" 19 | chocolate = "210,105,30" 20 | coral = "255,127,80" 21 | cornflowerblue = "100,149,237" 22 | cornsilk = "255,248,220" 23 | crimson = "220,20,60" 24 | cyan = "0,255,255" 25 | darkblue = "0,0,139" 26 | darkcyan = "0,139,139" 27 | darkgoldenrod = "184,134,11" 28 | darkgray = "169,169,169" 29 | darkgreen = "0,100,0" 30 | darkgrey = "169,169,169" 31 | darkkhaki = "189,183,107" 32 | darkmagenta = "139,0,139" 33 | darkolivegreen = "85,107,47" 34 | darkorange = "255,140,0" 35 | darkorchid = "153,50,204" 36 | darkred = "139,0,0" 37 | darksalmon = "233,150,122" 38 | darkseagreen = "143,188,143" 39 | darkslateblue = "72,61,139" 40 | darkslategray = "47,79,79" 41 | darkslategrey = "47,79,79" 42 | darkturquoise = "0,206,209" 43 | darkviolet = "148,0,211" 44 | deeppink = "255,20,147" 45 | deepskyblue = "0,191,255" 46 | dimgray = "105,105,105" 47 | dimgrey = "105,105,105" 48 | dodgerblue = "30,144,255" 49 | firebrick = "178,34,34" 50 | floralwhite = "255,250,240" 51 | forestgreen = "34,139,34" 52 | fuchsia = "255,0,255" 53 | gainsboro = "220,220,220" 54 | ghostwhite = "248,248,255" 55 | gold = "255,215,0" 56 | goldenrod = "218,165,32" 57 | gray = "128,128,128" 58 | green = "0,128,0" 59 | greenyellow = "173,255,47" 60 | grey = "128,128,128" 61 | honeydew = "240,255,240" 62 | hotpink = "255,105,180" 63 | indianred = "205,92,92" 64 | indigo = "75,0,130" 65 | ivory = "255,255,240" 66 | khaki = "240,230,140" 67 | lavender = "230,230,250" 68 | lavenderblush = "255,240,245" 69 | lawngreen = "124,252,0" 70 | lemonchiffon = "255,250,205" 71 | lightblue = "173,216,230" 72 | lightcoral = "240,128,128" 73 | lightcyan = "224,255,255" 74 | lightgoldenrodyellow = "250,250,210" 75 | lightgray = "211,211,211" 76 | lightgreen = "144,238,144" 77 | lightgrey = "211,211,211" 78 | lightpink = "255,182,193" 79 | lightsalmon = "255,160,122" 80 | lightseagreen = "32,178,170" 81 | lightskyblue = "135,206,250" 82 | lightslategray = "119,136,153" 83 | lightslategrey = "119,136,153" 84 | lightsteelblue = "176,196,222" 85 | lightyellow = "255,255,224" 86 | lime = "0,255,0" 87 | limegreen = "50,205,50" 88 | linen = "250,240,230" 89 | magenta = "255,0,255" 90 | maroon = "128,0,0" 91 | mediumaquamarine = "102,205,170" 92 | mediumblue = "0,0,205" 93 | mediumorchid = "186,85,211" 94 | mediumpurple = "147,112,219" 95 | mediumseagreen = "60,179,113" 96 | mediumslateblue = "123,104,238" 97 | mediumspringgreen = "0,250,154" 98 | mediumturquoise = "72,209,204" 99 | mediumvioletred = "199,21,133" 100 | midnightblue = "25,25,112" 101 | mintcream = "245,255,250" 102 | mistyrose = "255,228,225" 103 | moccasin = "255,228,181" 104 | navajowhite = "255,222,173" 105 | navy = "0,0,128" 106 | oldlace = "253,245,230" 107 | olive = "128,128,0" 108 | olivedrab = "107,142,35" 109 | orange = "255,165,0" 110 | orangered = "255,69,0" 111 | orchid = "218,112,214" 112 | palegoldenrod = "238,232,170" 113 | palegreen = "152,251,152" 114 | paleturquoise = "175,238,238" 115 | palevioletred = "219,112,147" 116 | papayawhip = "255,239,213" 117 | peachpuff = "255,218,185" 118 | peru = "205,133,63" 119 | pink = "255,192,203" 120 | plum = "221,160,221" 121 | powderblue = "176,224,230" 122 | purple = "128,0,128" 123 | rebeccapurple = "102,51,153" 124 | red = "255,0,0" 125 | rosybrown = "188,143,143" 126 | royalblue = "65,105,225" 127 | saddlebrown = "139,69,19" 128 | salmon = "250,128,114" 129 | sandybrown = "244,164,96" 130 | seagreen = "46,139,87" 131 | seashell = "255,245,238" 132 | sienna = "160,82,45" 133 | silver = "192,192,192" 134 | skyblue = "135,206,235" 135 | slateblue = "106,90,205" 136 | slategray = "112,128,144" 137 | slategrey = "112,128,144" 138 | snow = "255,250,250" 139 | springgreen = "0,255,127" 140 | steelblue = "70,130,180" 141 | tan = "210,180,140" 142 | teal = "0,128,128" 143 | thistle = "216,191,216" 144 | tomato = "255,99,71" 145 | turquoise = "64,224,208" 146 | violet = "238,130,238" 147 | wheat = "245,222,179" 148 | white = "255,255,255" 149 | whitesmoke = "245,245,245" 150 | yellow = "255,255,0" 151 | yellowgreen = "154,205,50" 152 | -------------------------------------------------------------------------------- /misc/formatters.php: -------------------------------------------------------------------------------- 1 | formatters = [ 10 | 'single-line' => 'CssCrush\fmtr_single', 11 | 'padded' => 'CssCrush\fmtr_padded', 12 | 'block' => 'CssCrush\fmtr_block', 13 | ]; 14 | 15 | function fmtr_single($rule) { 16 | 17 | $EOL = Crush::$process->newline; 18 | 19 | $selectors = $rule->selectors->join(', '); 20 | $block = $rule->declarations->join('; '); 21 | return "$selectors { $block; }$EOL"; 22 | } 23 | 24 | function fmtr_padded($rule, $padding = 40) { 25 | 26 | $EOL = Crush::$process->newline; 27 | 28 | $selectors = $rule->selectors->join(', '); 29 | $block = $rule->declarations->join('; '); 30 | 31 | if (strlen($selectors) > $padding) { 32 | $padding = str_repeat(' ', $padding); 33 | return "$selectors$EOL$padding { $block; }$EOL"; 34 | } 35 | else { 36 | $selectors = str_pad($selectors, $padding); 37 | return "$selectors { $block; }$EOL"; 38 | } 39 | } 40 | 41 | function fmtr_block($rule, $indent = ' ') { 42 | 43 | $EOL = Crush::$process->newline; 44 | 45 | $selectors = $rule->selectors->join(",$EOL"); 46 | $block = $rule->declarations->join(";$EOL$indent"); 47 | return "$selectors {{$EOL}$indent$block;$EOL$indent}$EOL"; 48 | } 49 | -------------------------------------------------------------------------------- /misc/property-sorting.ini: -------------------------------------------------------------------------------- 1 | ; Table for property sorting. 2 | ; Vendor prefixes are added at runtime. 3 | 4 | ; Generated content 5 | content 6 | quotes 7 | 8 | ; Positioning 9 | position 10 | z-index 11 | top 12 | right 13 | bottom 14 | left 15 | 16 | ; Display 17 | visibility 18 | opacity 19 | display 20 | overflow 21 | overflow-x 22 | overflow-y 23 | vertical-align 24 | 25 | ; Floats 26 | float 27 | clear 28 | 29 | ; Transforms 30 | transform 31 | transform-style 32 | perspective 33 | perspective-origin 34 | backface-visibility 35 | 36 | ; Box-model: dimensions 37 | box-sizing 38 | width 39 | height 40 | min-width 41 | max-width 42 | min-height 43 | max-height 44 | 45 | ; Box-model: padding 46 | padding 47 | padding-top 48 | padding-right 49 | padding-bottom 50 | padding-left 51 | 52 | ; Box-model: margins 53 | margin 54 | margin-top 55 | margin-right 56 | margin-bottom 57 | margin-left 58 | 59 | ; Box-model: borders 60 | border 61 | border-color 62 | border-image 63 | border-radius 64 | border-style 65 | border-width 66 | border-top 67 | border-top-color 68 | border-top-left-radius 69 | border-top-right-radius 70 | border-top-style 71 | border-top-width 72 | border-right 73 | border-right-color 74 | border-right-style 75 | border-right-width 76 | border-bottom 77 | border-bottom-color 78 | border-bottom-style 79 | border-bottom-left-radius 80 | border-bottom-right-radius 81 | border-bottom-width 82 | border-left 83 | border-left-color 84 | border-left-style 85 | border-left-width 86 | 87 | ; Box-model: effects 88 | box-shadow 89 | 90 | ; Counters 91 | counter-increment 92 | counter-reset 93 | 94 | ; Foreground color 95 | color 96 | 97 | ; Background 98 | background 99 | background-attachment 100 | background-clip 101 | background-color 102 | background-image 103 | background-origin 104 | background-position 105 | background-position-x 106 | background-position-y 107 | background-repeat 108 | background-size 109 | 110 | ; Text 111 | direction 112 | text-align 113 | text-align-last 114 | text-decoration 115 | text-decoration-color 116 | text-decoration-line 117 | text-decoration-style 118 | text-indent 119 | text-overflow 120 | text-shadow 121 | text-transform 122 | 123 | ; Fonts: general 124 | font 125 | font-family 126 | font-size 127 | font-style 128 | font-weight 129 | font-variant 130 | line-height 131 | 132 | ; Fonts: spacing and behaviour 133 | letter-spacing 134 | white-space 135 | word-break 136 | word-spacing 137 | word-wrap 138 | hyphens 139 | orphans 140 | 141 | ; Outlines 142 | outline 143 | outline-color 144 | outline-offset 145 | outline-style 146 | outline-width 147 | 148 | ; Animations 149 | animation 150 | animation-delay 151 | animation-direction 152 | animation-duration 153 | animation-fill-mode 154 | animation-iteration-count 155 | animation-name 156 | animation-play-state 157 | animation-timing-function 158 | 159 | ; Transitions 160 | transition 161 | transition-delay 162 | transition-duration 163 | transition-property 164 | transition-timing-function 165 | 166 | ; Tables specific 167 | table-layout 168 | border-collapse 169 | caption-side 170 | empty-cells 171 | 172 | ; Lists specific 173 | list-style 174 | list-style-image 175 | list-style-position 176 | list-style-type 177 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csscrush", 3 | "version": "4.2.0", 4 | "description": "CSS-Crush, CSS preprocessor", 5 | "main": "./js/index.js", 6 | "types": "./js/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/peteboere/css-crush.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/peteboere/css-crush/issues" 13 | }, 14 | "bin": { 15 | "csscrush": "./bin/csscrush" 16 | }, 17 | "scripts": { 18 | "lint": "eslint --fix ./js/index.js", 19 | "types": "npx -p typescript tsc -p jsconfig.json", 20 | "test": "node ./js/tests/test.js" 21 | }, 22 | "homepage": "http://the-echoplex.net/csscrush", 23 | "license": "MIT", 24 | "devDependencies": { 25 | "@types/node": "~20.4.9", 26 | "eslint": "~8.16.0", 27 | "normalize.css": "7.0.0", 28 | "typescript": "~5.1.6" 29 | }, 30 | "dependencies": { 31 | "glob": "~8.0.3" 32 | }, 33 | "type": "module", 34 | "engines": { 35 | "node": ">=18" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 1 3 | paths: 4 | - lib 5 | - plugins 6 | -------------------------------------------------------------------------------- /plugins/aria.php: -------------------------------------------------------------------------------- 1 | $handler) { 11 | $type = is_callable($handler) ? 'callback' : 'alias'; 12 | $process->addSelectorAlias($name, $handler, $type); 13 | } 14 | }); 15 | 16 | function aria() { 17 | 18 | static $aria, $optional_value; 19 | if (! $aria) { 20 | $optional_value = function ($property) { 21 | return function ($args) use ($property) { 22 | return $args ? "[$property=\"#(0)\"]" : "[$property]"; 23 | }; 24 | }; 25 | $aria = [ 26 | 27 | // Roles. 28 | 'role' => $optional_value('role'), 29 | 30 | // States and properties. 31 | 'aria-activedescendant' => $optional_value('aria-activedescendant'), 32 | 'aria-atomic' => '[aria-atomic="#(0 true)"]', 33 | 'aria-autocomplete' => $optional_value('aria-autocomplete'), 34 | 'aria-busy' => '[aria-busy="#(0 true)"]', 35 | 'aria-checked' => '[aria-checked="#(0 true)"]', 36 | 'aria-controls' => $optional_value('aria-controls'), 37 | 'aria-describedby' => $optional_value('aria-describedby'), 38 | 'aria-disabled' => '[aria-disabled="#(0 true)"]', 39 | 'aria-dropeffect' => $optional_value('aria-dropeffect'), 40 | 'aria-expanded' => '[aria-expanded="#(0 true)"]', 41 | 'aria-flowto' => $optional_value('aria-flowto'), 42 | 'aria-grabbed' => '[aria-grabbed="#(0 true)"]', 43 | 'aria-haspopup' => '[aria-haspopup="#(0 true)"]', 44 | 'aria-hidden' => '[aria-hidden="#(0 true)"]', 45 | 'aria-invalid' => '[aria-invalid="#(0 true)"]', 46 | 'aria-label' => $optional_value('aria-label'), 47 | 'aria-labelledby' => $optional_value('aria-labelledby'), 48 | 'aria-level' => $optional_value('aria-level'), 49 | 'aria-live' => $optional_value('aria-live'), 50 | 'aria-multiline' => '[aria-multiline="#(0 true)"]', 51 | 'aria-multiselectable' => '[aria-multiselectable="#(0 true)"]', 52 | 'aria-orientation' => $optional_value('aria-orientation'), 53 | 'aria-owns' => $optional_value('aria-owns'), 54 | 'aria-posinset' => $optional_value('aria-posinset'), 55 | 'aria-pressed' => '[aria-pressed="#(0 true)"]', 56 | 'aria-readonly' => '[aria-readonly="#(0 true)"]', 57 | 'aria-relevant' => $optional_value('aria-relevant'), 58 | 'aria-required' => '[aria-required="#(0 true)"]', 59 | 'aria-selected' => '[aria-selected="#(0 true)"]', 60 | 'aria-setsize' => $optional_value('aria-setsize'), 61 | 'aria-sort' => $optional_value('aria-sort'), 62 | 'aria-valuemax' => $optional_value('aria-valuemax'), 63 | 'aria-valuemin' => $optional_value('aria-valuemin'), 64 | 'aria-valuenow' => $optional_value('aria-valuenow'), 65 | 'aria-valuetext' => $optional_value('aria-valuetext'), 66 | ]; 67 | } 68 | 69 | return $aria; 70 | } 71 | -------------------------------------------------------------------------------- /plugins/ease.php: -------------------------------------------------------------------------------- 1 | on('rule_prealias', 'CssCrush\ease'); 11 | }); 12 | 13 | function ease(Rule $rule) { 14 | 15 | static $find, $replace, $easing_properties; 16 | if (! $find) { 17 | $easings = [ 18 | 'ease-in-out-back' => 'cubic-bezier(.680,-0.550,.265,1.550)', 19 | 'ease-in-out-circ' => 'cubic-bezier(.785,.135,.150,.860)', 20 | 'ease-in-out-expo' => 'cubic-bezier(1,0,0,1)', 21 | 'ease-in-out-sine' => 'cubic-bezier(.445,.050,.550,.950)', 22 | 'ease-in-out-quint' => 'cubic-bezier(.860,0,.070,1)', 23 | 'ease-in-out-quart' => 'cubic-bezier(.770,0,.175,1)', 24 | 'ease-in-out-cubic' => 'cubic-bezier(.645,.045,.355,1)', 25 | 'ease-in-out-quad' => 'cubic-bezier(.455,.030,.515,.955)', 26 | 'ease-out-back' => 'cubic-bezier(.175,.885,.320,1.275)', 27 | 'ease-out-circ' => 'cubic-bezier(.075,.820,.165,1)', 28 | 'ease-out-expo' => 'cubic-bezier(.190,1,.220,1)', 29 | 'ease-out-sine' => 'cubic-bezier(.390,.575,.565,1)', 30 | 'ease-out-quint' => 'cubic-bezier(.230,1,.320,1)', 31 | 'ease-out-quart' => 'cubic-bezier(.165,.840,.440,1)', 32 | 'ease-out-cubic' => 'cubic-bezier(.215,.610,.355,1)', 33 | 'ease-out-quad' => 'cubic-bezier(.250,.460,.450,.940)', 34 | 'ease-in-back' => 'cubic-bezier(.600,-0.280,.735,.045)', 35 | 'ease-in-circ' => 'cubic-bezier(.600,.040,.980,.335)', 36 | 'ease-in-expo' => 'cubic-bezier(.950,.050,.795,.035)', 37 | 'ease-in-sine' => 'cubic-bezier(.470,0,.745,.715)', 38 | 'ease-in-quint' => 'cubic-bezier(.755,.050,.855,.060)', 39 | 'ease-in-quart' => 'cubic-bezier(.895,.030,.685,.220)', 40 | 'ease-in-cubic' => 'cubic-bezier(.550,.055,.675,.190)', 41 | 'ease-in-quad' => 'cubic-bezier(.550,.085,.680,.530)', 42 | ]; 43 | 44 | $easing_properties = [ 45 | 'transition' => true, 46 | 'transition-timing-function' => true, 47 | ]; 48 | 49 | foreach ($easings as $property => $value) { 50 | $patt = Regex::make("~{{ LB }}$property{{ RB }}~i"); 51 | $find[] = $patt; 52 | $replace[] = $value; 53 | } 54 | } 55 | 56 | if (! array_intersect_key($rule->declarations->canonicalProperties, $easing_properties)) { 57 | return; 58 | } 59 | 60 | foreach ($rule->declarations->filter(['skip' => false]) as $declaration) { 61 | if (isset($easing_properties[$declaration->canonicalProperty])) { 62 | $declaration->value = preg_replace($find, $replace, $declaration->value); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /plugins/forms.php: -------------------------------------------------------------------------------- 1 | $handler) { 11 | if (is_array($handler)) { 12 | $type = $handler['type']; 13 | $handler = $handler['handler']; 14 | } 15 | $process->addSelectorAlias($name, $handler, $type); // @phpstan-ignore-line variable.undefined 16 | } 17 | }); 18 | 19 | function forms() { 20 | return [ 21 | 'input' => [ 22 | 'type' => 'splat', 23 | 'handler' => 'input[type=#(text)]', 24 | ], 25 | 'checkbox' => 'input[type="checkbox"]', 26 | 'radio' => 'input[type="radio"]', 27 | 'file' => 'input[type="file"]', 28 | 'image' => 'input[type="image"]', 29 | 'password' => 'input[type="password"]', 30 | 'submit' => 'input[type="submit"]', 31 | 'text' => 'input[type="text"]', 32 | ]; 33 | } 34 | -------------------------------------------------------------------------------- /plugins/hocus-pocus.php: -------------------------------------------------------------------------------- 1 | addSelectorAlias('hocus', ':any(:hover,:focus)'); 9 | $process->addSelectorAlias('pocus', ':any(:hover,:focus,:active)'); 10 | }); 11 | -------------------------------------------------------------------------------- /plugins/property-sorter.php: -------------------------------------------------------------------------------- 1 | on('rule_prealias', 'CssCrush\property_sorter'); 11 | }); 12 | 13 | function property_sorter(Rule $rule) { 14 | 15 | usort($rule->declarations->store, 'CssCrush\property_sorter_callback'); 16 | } 17 | 18 | 19 | /* 20 | Callback for sorting. 21 | */ 22 | function property_sorter_callback($a, $b) { 23 | 24 | $map =& property_sorter_get_table(); 25 | $a_prop =& $a->canonicalProperty; 26 | $b_prop =& $b->canonicalProperty; 27 | $a_listed = isset($map[$a_prop]); 28 | $b_listed = isset($map[$b_prop]); 29 | 30 | // If the properties are identical we need to flag for an index comparison. 31 | $compare_indexes = false; 32 | 33 | // If the 'canonical' properties are identical we need to flag for a vendor comparison. 34 | $compare_vendor = false; 35 | 36 | // If both properties are listed. 37 | if ($a_listed && $b_listed) { 38 | 39 | if ($a_prop === $b_prop) { 40 | if ($a->vendor || $b->vendor) { 41 | $compare_vendor = true; 42 | } 43 | else { 44 | $compare_indexes = true; 45 | } 46 | } 47 | else { 48 | // Table comparison. 49 | return $map[$a_prop] > $map[$b_prop] ? 1 : -1; 50 | } 51 | } 52 | 53 | // If one property is listed it always takes higher priority. 54 | elseif ($a_listed && ! $b_listed) { 55 | return -1; 56 | } 57 | elseif ($b_listed && ! $a_listed) { 58 | return 1; 59 | } 60 | 61 | // If neither property is listed. 62 | else { 63 | 64 | if ($a_prop === $b_prop) { 65 | if ($a->vendor || $b->vendor) { 66 | $compare_vendor = true; 67 | } 68 | else { 69 | $compare_indexes = true; 70 | } 71 | } 72 | else { 73 | // Regular sort. 74 | return $a_prop > $b_prop ? 1 : -1; 75 | } 76 | } 77 | 78 | // Comparing by index. 79 | if ($compare_indexes ) { 80 | return $a->index > $b->index ? 1 : -1; 81 | } 82 | 83 | // Comparing by vendor mark. 84 | if ($compare_vendor) { 85 | if (! $a->vendor && $b->vendor) { 86 | return 1; 87 | } 88 | elseif ($a->vendor && ! $b->vendor) { 89 | return -1; 90 | } 91 | else { 92 | // If both have a vendor mark compare vendor name length. 93 | return strlen($b->vendor) > strlen($a->vendor) ? 1 : -1; 94 | } 95 | } 96 | } 97 | 98 | 99 | /* 100 | Cache for the table of values to compare against. 101 | */ 102 | function &property_sorter_get_table () { 103 | 104 | // Check for cached table. 105 | if (isset($GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER_CACHE'])) { 106 | return $GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER_CACHE']; 107 | } 108 | 109 | $table = []; 110 | 111 | // Nothing cached, check for a user-defined table. 112 | if (isset($GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER'])) { 113 | $table = (array) $GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER']; 114 | } 115 | 116 | // No user-defined table, use pre-defined. 117 | else { 118 | 119 | // Load from property-sorting.ini. 120 | $sorting_file_contents = file_get_contents(Crush::$dir . '/misc/property-sorting.ini'); 121 | if ($sorting_file_contents !== false) { 122 | 123 | $sorting_file_contents = preg_replace('~;[^\r\n]*~', '', $sorting_file_contents); 124 | $table = preg_split('~\s+~', trim($sorting_file_contents)); 125 | } 126 | else { 127 | notice("Property sorting file not found."); 128 | } 129 | 130 | // Store to the global variable. 131 | $GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER'] = $table; 132 | } 133 | 134 | // Cache the table (and flip it). 135 | $GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER_CACHE'] = array_flip($table); 136 | 137 | return $GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER_CACHE']; 138 | } 139 | 140 | } 141 | 142 | namespace { 143 | 144 | /* 145 | Get the current sorting table. 146 | */ 147 | function csscrush_get_property_sort_order() { 148 | CssCrush\property_sorter_get_table(); 149 | return $GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER']; 150 | } 151 | 152 | 153 | /* 154 | Set a custom sorting table. 155 | */ 156 | function csscrush_set_property_sort_order(array $new_order) { 157 | unset($GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER_CACHE']); 158 | $GLOBALS['CSSCRUSH_PROPERTY_SORT_ORDER'] = $new_order; 159 | } 160 | } 161 | --------------------------------------------------------------------------------