├── .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 | Option
10 | Values (default in bold)
11 | Description
12 |
13 |
14 | 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 |
18 |
19 | formatter
20 | block | single-line | padded
21 | Set the formatting mode. Overrides minify option if both are set.
22 |
23 |
24 | newlines
25 | use-platform | windows/win | unix
26 | Set the output style of newlines
27 |
28 |
29 | boilerplate
30 | true | false | Path
31 | Prepend a boilerplate to the output file
32 |
33 |
34 | versioning
35 | true | false
36 | Append a timestamped querystring to the output filename
37 |
38 |
39 | 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 |
43 |
44 | cache
45 | true | false
46 | Turn caching on or off.
47 |
48 |
49 | output_dir
50 | Path
51 | Specify an output directory for compiled files. Defaults to the same directory as the host file.
52 |
53 |
54 | output_file
55 | Output filename
56 | Specify an output filename (suffix is added).
57 |
58 |
59 | asset_dir
60 | Path
61 | Directory for SVG and image files generated by plugins (defaults to the main file output directory).
62 |
63 |
64 | stat_dump
65 | false | true | Path
66 | Save compile stats and variables to a file in json format.
67 |
68 |
69 | vendor_target
70 | "all" | "moz", "webkit", ... | Array
71 | Limit aliasing to a specific vendor, or an array of vendors.
72 |
73 |
74 | rewrite_import_urls
75 | true | false | "absolute"
76 | Rewrite relative URLs inside inlined imported files.
77 |
78 |
79 | import_paths
80 | Array
81 | Additional paths to search when resolving relative import URLs.
82 |
83 |
84 | plugins
85 | Array
86 | An array of plugin names to enable.
87 |
88 |
89 | source_map
90 | true | false
91 | Output a source map (compliant with the Source Map v3 proposal).
92 |
93 |
94 | context
95 | Path
96 | Context for importing resources from relative urls (Only applies to `csscrush_string()` and command line utility).
97 |
98 |
99 | 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 |
103 |
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 = "';
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 |
--------------------------------------------------------------------------------