WP-SCSS - Version 2.3.0

Version Description

  • Update src to use ScssPHP github repo at 1.5.2
    • Update deprecated setFormatter to setOutputStyle and provide db migration shadoath
Download this release

Release Info

Developer Sky Bolt
Plugin Icon wp plugin WP-SCSS
Version 2.3.0
Comparing to
See all releases

Code changes from version 2.2.0 to 2.3.0

Files changed (49) hide show
  1. class/class-wp-scss.php +13 -20
  2. options.php +10 -16
  3. readme.md +8 -2
  4. readme.txt +7 -3
  5. scssphp/LICENSE.md +20 -0
  6. scssphp/README.md +71 -0
  7. scssphp/bin/pscss +71 -35
  8. scssphp/composer.json +89 -0
  9. scssphp/phpcs.xml.dist +12 -0
  10. scssphp/scss.inc.php +14 -29
  11. scssphp/src/Base/Range.php +9 -0
  12. scssphp/src/Block.php +4 -2
  13. scssphp/src/Cache.php +41 -9
  14. scssphp/src/Colors.php +7 -5
  15. scssphp/src/CompilationResult.php +69 -0
  16. scssphp/src/Compiler.php +2605 -1185
  17. scssphp/src/Compiler/CachedResult.php +77 -0
  18. scssphp/src/Compiler/Environment.php +4 -2
  19. scssphp/src/Exception/CompilerException.php +2 -0
  20. scssphp/src/Exception/ParserException.php +2 -0
  21. scssphp/src/Exception/RangeException.php +2 -0
  22. scssphp/src/Exception/SassScriptException.php +32 -0
  23. scssphp/src/Exception/ServerException.php +4 -0
  24. scssphp/src/Formatter.php +45 -14
  25. scssphp/src/Formatter/Compact.php +6 -0
  26. scssphp/src/Formatter/Compressed.php +4 -1
  27. scssphp/src/Formatter/Crunched.php +8 -1
  28. scssphp/src/Formatter/Debug.php +6 -1
  29. scssphp/src/Formatter/Expanded.php +2 -1
  30. scssphp/src/Formatter/Nested.php +6 -1
  31. scssphp/src/Formatter/OutputBlock.php +9 -7
  32. scssphp/src/Logger/LoggerInterface.php +48 -0
  33. scssphp/src/Logger/QuietLogger.php +27 -0
  34. scssphp/src/Logger/StreamLogger.php +60 -0
  35. scssphp/src/Node.php +4 -2
  36. scssphp/src/Node/Number.php +545 -140
  37. scssphp/src/OutputStyle.php +9 -0
  38. scssphp/src/Parser.php +325 -113
  39. scssphp/src/SourceMap/Base64.php +4 -2
  40. scssphp/src/SourceMap/Base64VLQ.php +2 -2
  41. scssphp/src/SourceMap/Base64VLQEncoder.php +0 -217
  42. scssphp/src/SourceMap/SourceMapGenerator.php +39 -7
  43. scssphp/src/Type.php +4 -0
  44. scssphp/src/Util.php +35 -11
  45. scssphp/src/Util/Path.php +77 -0
  46. scssphp/src/ValueConverter.php +95 -0
  47. scssphp/src/Version.php +1 -1
  48. scssphp/src/Warn.php +84 -0
  49. wp-scss.php +9 -5
class/class-wp-scss.php CHANGED
@@ -5,36 +5,28 @@ include_once(WPSCSS_PLUGIN_DIR . '/scssphp/scss.inc.php');
5
  use ScssPhp\ScssPhp\Compiler;
6
 
7
  class Wp_Scss {
8
- /**
9
- * Compiling preferences properites
10
- *
11
- * @var string
12
- * @access private
13
- */
14
- private $scss_dir, $css_dir, $compile_method, $scssc, $compile_errors, $sourcemaps, $cache;
15
 
16
  /**
17
  * Set values for Wp_Scss::properties
18
  *
19
  * @param string scss_dir - path to source directory for scss files
20
  * @param string css_dir - path to output directory for css files
21
- * @param string method - type of compile (compressed, expanded, etc)
22
  *
23
  * @var object scssc - instantiate the compiling object.
24
  *
25
  * @var array compile_errors - catches errors from compile
26
  */
27
  public function __construct ($scss_dir, $css_dir, $compile_method, $sourcemaps) {
28
-
29
  $this->scss_dir = $scss_dir;
30
  $this->css_dir = $css_dir;
31
- $this->compile_method = $compile_method;
32
  $this->compile_errors = array();
33
  $this->scssc = new Compiler();
34
 
35
  $this->cache = WPSCSS_PLUGIN_DIR . '/cache/';
36
 
37
- $this->scssc->setFormatter( $compile_method );
38
  $this->scssc->setImportPaths( $this->scss_dir );
39
 
40
  $this->sourcemaps = $sourcemaps;
@@ -89,7 +81,7 @@ class Wp_Scss {
89
  * @access public
90
  */
91
  public function compile() {
92
-
93
  $input_files = array();
94
 
95
  // Loop through directory and get .scss file that do not start with '_'
@@ -105,7 +97,7 @@ class Wp_Scss {
105
  $outputName = preg_replace("/\.[^$]*/", ".css", $scss_file);
106
  $output = $this->css_dir . $outputName;
107
 
108
- $this->compiler($input, $output, $this);
109
  }
110
 
111
  if (count($this->compile_errors) < 1) {
@@ -141,7 +133,7 @@ class Wp_Scss {
141
  * Puts error in 'compile_errors' property
142
  * @access public
143
  */
144
- private function compiler($in, $out, $instance) {
145
 
146
  if (!file_exists($this->cache)) {
147
  mkdir($this->cache, 0644);
@@ -149,15 +141,16 @@ class Wp_Scss {
149
  if (is_writable($this->cache)) {
150
  try {
151
  $map = basename($out) . '.map';
152
- $this->scssc->setSourceMap(constant('ScssPhp\ScssPhp\Compiler::' . $instance->sourcemaps));
153
  $this->scssc->setSourceMapOptions(array(
154
- 'sourceMapWriteTo' => $instance->css_dir . $map, // absolute path to a file to write the map to
155
  'sourceMapURL' => $map, // url of the map
156
  'sourceMapBasepath' => rtrim(ABSPATH, '/'), // base path for filename normalization
157
  'sourceRoot' => home_url('/'), // This value is prepended to the individual entries in the 'source' field.
158
  ));
159
 
160
- $css = $this->scssc->compile(file_get_contents($in), $in);
 
161
 
162
  file_put_contents($this->cache . basename($out), $css);
163
  } catch (Exception $e) {
@@ -165,14 +158,14 @@ class Wp_Scss {
165
  'file' => basename($in),
166
  'message' => $e->getMessage(),
167
  );
168
- array_push($instance->compile_errors, $errors);
169
  }
170
  } else {
171
  $errors = array (
172
  'file' => $this->cache,
173
  'message' => "File Permission Error, permission denied. Please make the cache directory writable."
174
  );
175
- array_push($instance->compile_errors, $errors);
176
  }
177
  }
178
 
@@ -283,6 +276,6 @@ class Wp_Scss {
283
 
284
  public function set_variables(array $variables) {
285
 
286
- $this->scssc->setVariables($variables);
287
  }
288
  } // End Wp_Scss Class
5
  use ScssPhp\ScssPhp\Compiler;
6
 
7
  class Wp_Scss {
 
 
 
 
 
 
 
8
 
9
  /**
10
  * Set values for Wp_Scss::properties
11
  *
12
  * @param string scss_dir - path to source directory for scss files
13
  * @param string css_dir - path to output directory for css files
14
+ * @param string compile_method - type of compile (compressed or expanded)
15
  *
16
  * @var object scssc - instantiate the compiling object.
17
  *
18
  * @var array compile_errors - catches errors from compile
19
  */
20
  public function __construct ($scss_dir, $css_dir, $compile_method, $sourcemaps) {
21
+
22
  $this->scss_dir = $scss_dir;
23
  $this->css_dir = $css_dir;
 
24
  $this->compile_errors = array();
25
  $this->scssc = new Compiler();
26
 
27
  $this->cache = WPSCSS_PLUGIN_DIR . '/cache/';
28
 
29
+ $this->scssc->setOutputStyle( $compile_method );
30
  $this->scssc->setImportPaths( $this->scss_dir );
31
 
32
  $this->sourcemaps = $sourcemaps;
81
  * @access public
82
  */
83
  public function compile() {
84
+
85
  $input_files = array();
86
 
87
  // Loop through directory and get .scss file that do not start with '_'
97
  $outputName = preg_replace("/\.[^$]*/", ".css", $scss_file);
98
  $output = $this->css_dir . $outputName;
99
 
100
+ $this->compiler($input, $output);
101
  }
102
 
103
  if (count($this->compile_errors) < 1) {
133
  * Puts error in 'compile_errors' property
134
  * @access public
135
  */
136
+ private function compiler($in, $out) {
137
 
138
  if (!file_exists($this->cache)) {
139
  mkdir($this->cache, 0644);
141
  if (is_writable($this->cache)) {
142
  try {
143
  $map = basename($out) . '.map';
144
+ $this->scssc->setSourceMap(constant('ScssPhp\ScssPhp\Compiler::' . $this->sourcemaps));
145
  $this->scssc->setSourceMapOptions(array(
146
+ 'sourceMapWriteTo' => $this->css_dir . $map, // absolute path to a file to write the map to
147
  'sourceMapURL' => $map, // url of the map
148
  'sourceMapBasepath' => rtrim(ABSPATH, '/'), // base path for filename normalization
149
  'sourceRoot' => home_url('/'), // This value is prepended to the individual entries in the 'source' field.
150
  ));
151
 
152
+ $compilationResult = $this->scssc->compileString(file_get_contents($in), $in);
153
+ $css = $compilationResult->getCss();
154
 
155
  file_put_contents($this->cache . basename($out), $css);
156
  } catch (Exception $e) {
158
  'file' => basename($in),
159
  'message' => $e->getMessage(),
160
  );
161
+ array_push($this->compile_errors, $errors);
162
  }
163
  } else {
164
  $errors = array (
165
  'file' => $this->cache,
166
  'message' => "File Permission Error, permission denied. Please make the cache directory writable."
167
  );
168
+ array_push($this->compile_errors, $errors);
169
  }
170
  }
171
 
276
 
277
  public function set_variables(array $variables) {
278
 
279
+ $this->scssc->addVariables(array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables));
280
  }
281
  } // End Wp_Scss Class
options.php CHANGED
@@ -1,6 +1,8 @@
1
  <?php
2
- class Wp_Scss_Settings
3
- {
 
 
4
  /**
5
  * Holds the values to be used in the fields callbacks
6
  */
@@ -9,8 +11,7 @@ class Wp_Scss_Settings
9
  /**
10
  * Start up
11
  */
12
- public function __construct()
13
- {
14
  add_action( 'admin_menu', array( $this, 'add_plugin_page' ) );
15
  add_action( 'admin_init', array( $this, 'page_init' ) );
16
  }
@@ -18,8 +19,7 @@ class Wp_Scss_Settings
18
  /**
19
  * Add options page
20
  */
21
- public function add_plugin_page()
22
- {
23
  // This page will be under "Settings"
24
  add_options_page(
25
  'Settings Admin',
@@ -33,8 +33,7 @@ class Wp_Scss_Settings
33
  /**
34
  * Options page callback
35
  */
36
- public function create_admin_page()
37
- {
38
  // Set class property
39
  $this->options = get_option( 'wpscss_options' );
40
  ?>
@@ -62,8 +61,7 @@ class Wp_Scss_Settings
62
  /**
63
  * Register and add settings
64
  */
65
- public function page_init()
66
- {
67
  register_setting(
68
  'wpscss_options_group', // Option group
69
  'wpscss_options', // Option name
@@ -137,12 +135,8 @@ class Wp_Scss_Settings
137
  'name' => 'compiling_options',
138
  'type' => apply_filters( 'wp_scss_compiling_modes',
139
  array(
140
- 'ScssPhp\ScssPhp\Formatter\Expanded' => 'Expanded',
141
- 'ScssPhp\ScssPhp\Formatter\Nested' => 'Nested',
142
- 'ScssPhp\ScssPhp\Formatter\Compressed' => 'Compressed',
143
- 'ScssPhp\ScssPhp\Formatter\Compact' => 'Compact',
144
- 'ScssPhp\ScssPhp\Formatter\Crunched' => 'Crunched',
145
- 'ScssPhp\ScssPhp\Formatter\Debug' => 'Debug'
146
  )
147
  )
148
  )
1
  <?php
2
+
3
+ use ScssPhp\ScssPhp\OutputStyle;
4
+
5
+ class Wp_Scss_Settings {
6
  /**
7
  * Holds the values to be used in the fields callbacks
8
  */
11
  /**
12
  * Start up
13
  */
14
+ public function __construct() {
 
15
  add_action( 'admin_menu', array( $this, 'add_plugin_page' ) );
16
  add_action( 'admin_init', array( $this, 'page_init' ) );
17
  }
19
  /**
20
  * Add options page
21
  */
22
+ public function add_plugin_page() {
 
23
  // This page will be under "Settings"
24
  add_options_page(
25
  'Settings Admin',
33
  /**
34
  * Options page callback
35
  */
36
+ public function create_admin_page() {
 
37
  // Set class property
38
  $this->options = get_option( 'wpscss_options' );
39
  ?>
61
  /**
62
  * Register and add settings
63
  */
64
+ public function page_init() {
 
65
  register_setting(
66
  'wpscss_options_group', // Option group
67
  'wpscss_options', // Option name
135
  'name' => 'compiling_options',
136
  'type' => apply_filters( 'wp_scss_compiling_modes',
137
  array(
138
+ OutputStyle::COMPRESSED => ucfirst(OutputStyle::COMPRESSED),
139
+ OutputStyle::EXPANDED => ucfirst(OutputStyle::EXPANDED),
 
 
 
 
140
  )
141
  )
142
  )
readme.md CHANGED
@@ -24,11 +24,14 @@ Ideally you should setup a scss folder and a css folder within your theme. This
24
 
25
  #### Compiling Mode
26
 
27
- Compiling comes in five modes:
28
 
 
29
  - Expanded - Full open css. One line per property. Brackets close on their own line.
 
 
 
30
  - Nested - Lightly compressed css. Brackets close with css block. Indents to match scss nesting.
31
- - Compressed - More compressed css. Entire rule block on one line. No indentation.
32
  - Compact - Removes all line breaks, unnecessary whitespace, and single-line comments.
33
  - Crunched - Same as Compressed, but also removes multi-line comments.
34
 
@@ -104,6 +107,9 @@ This plugin will only work with .scss format.
104
 
105
  ## Changelog
106
 
 
 
 
107
  - 2.2.0
108
  - Updates to allow compile() from outside the plugin [niaccurshi](https://github.com/ConnectThink/WP-SCSS/pull/190)
109
  - Update src to use [ScssPHP github repo at 1.2.1](https://github.com/scssphp/scssphp/releases/tag/1.2.1)
24
 
25
  #### Compiling Mode
26
 
27
+ Compiling comes in two modes:
28
 
29
+ - Compressed - More compressed css. Entire rule block on one line. No indentation.
30
  - Expanded - Full open css. One line per property. Brackets close on their own line.
31
+
32
+ **Removed** compiling modes
33
+
34
  - Nested - Lightly compressed css. Brackets close with css block. Indents to match scss nesting.
 
35
  - Compact - Removes all line breaks, unnecessary whitespace, and single-line comments.
36
  - Crunched - Same as Compressed, but also removes multi-line comments.
37
 
107
 
108
  ## Changelog
109
 
110
+ - 2.3.0
111
+ - Update src to use [ScssPHP github repo at 1.5.2](https://github.com/scssphp/scssphp/releases/tag/1.5.2)
112
+ - Update deprecated setFormatter to setOutputStyle and provide db migration [shadoath](https://github.com/ConnectThink/WP-SCSS/pull/195)
113
  - 2.2.0
114
  - Updates to allow compile() from outside the plugin [niaccurshi](https://github.com/ConnectThink/WP-SCSS/pull/190)
115
  - Update src to use [ScssPHP github repo at 1.2.1](https://github.com/scssphp/scssphp/releases/tag/1.2.1)
readme.txt CHANGED
@@ -5,7 +5,7 @@ Plugin URI: https://github.com/ConnectThink/WP-SCSS
5
  Requires at least: 3.0.1
6
  Tested up to: 5.7.1
7
  Requires PHP: 5.6
8
- Stable tag: 2.2.0
9
  License: GPLv3 or later
10
  License URI: http://www.gnu.org/copyleft/gpl.html
11
 
@@ -61,7 +61,7 @@ Alternatively, you can include [Bourbon](https://github.com/thoughtbot/bourbon)
61
 
62
  This plugin will only work with .scss format.
63
 
64
-
65
  = It's not updating my css, what's happening? =
66
 
67
  Do you have errors printing to the front end? If not, check your log file in your scss directory. The css will not be updated if there are errors in your sass file(s).
@@ -76,10 +76,14 @@ If you are having issues with the plugin, create an issue on [github](https://gi
76
 
77
  == Changelog ==
78
 
 
 
 
 
79
  = 2.2.0 =
80
  - Updates to allow compile() from outside the plugin [niaccurshi](https://github.com/ConnectThink/WP-SCSS/pull/190)
81
  - Update src to use [ScssPHP github repo at 1.2.1](https://github.com/scssphp/scssphp/releases/tag/1.2.1)
82
-
83
  = 2.1.6 =
84
  - When enqueueing CSS files Defer to WordPress for URLs instead of trying to guess them. Change by [mmcev106](https://github.com/ConnectThink/WP-SCSS/pull/185)
85
  - Allow setting Base Directory to Parent theme folder. [Shadoath](https://github.com/ConnectThink/WP-SCSS/issues/178)
5
  Requires at least: 3.0.1
6
  Tested up to: 5.7.1
7
  Requires PHP: 5.6
8
+ Stable tag: 2.3.0
9
  License: GPLv3 or later
10
  License URI: http://www.gnu.org/copyleft/gpl.html
11
 
61
 
62
  This plugin will only work with .scss format.
63
 
64
+
65
  = It's not updating my css, what's happening? =
66
 
67
  Do you have errors printing to the front end? If not, check your log file in your scss directory. The css will not be updated if there are errors in your sass file(s).
76
 
77
  == Changelog ==
78
 
79
+ = 2.3.0 =
80
+ - Update src to use [ScssPHP github repo at 1.5.2](https://github.com/scssphp/scssphp/releases/tag/1.5.2)
81
+ - Update deprecated setFormatter to setOutputStyle and provide db migration [shadoath](https://github.com/ConnectThink/WP-SCSS/pull/195)
82
+
83
  = 2.2.0 =
84
  - Updates to allow compile() from outside the plugin [niaccurshi](https://github.com/ConnectThink/WP-SCSS/pull/190)
85
  - Update src to use [ScssPHP github repo at 1.2.1](https://github.com/scssphp/scssphp/releases/tag/1.2.1)
86
+
87
  = 2.1.6 =
88
  - When enqueueing CSS files Defer to WordPress for URLs instead of trying to guess them. Change by [mmcev106](https://github.com/ConnectThink/WP-SCSS/pull/185)
89
  - Allow setting Base Directory to Parent theme folder. [Shadoath](https://github.com/ConnectThink/WP-SCSS/issues/178)
scssphp/LICENSE.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright (c) 2015 Leaf Corcoran, http://scssphp.github.io/scssphp
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
scssphp/README.md ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # scssphp
2
+ ### <https://scssphp.github.io/scssphp>
3
+
4
+ ![Build](https://github.com/scssphp/scssphp/workflows/CI/badge.svg)
5
+ [![License](https://poser.pugx.org/scssphp/scssphp/license)](https://packagist.org/packages/scssphp/scssphp)
6
+
7
+ `scssphp` is a compiler for SCSS written in PHP.
8
+
9
+ Checkout the homepage, <https://scssphp.github.io/scssphp>, for directions on how to use.
10
+
11
+ ## Running Tests
12
+
13
+ `scssphp` uses [PHPUnit](https://github.com/sebastianbergmann/phpunit) for testing.
14
+
15
+ Run the following command from the root directory to run every test:
16
+
17
+ vendor/bin/phpunit tests
18
+
19
+ There are several tests in the `tests/` directory:
20
+
21
+ * `ApiTest.php` contains various unit tests that test the PHP interface.
22
+ * `ExceptionTest.php` contains unit tests that test for exceptions thrown by the parser and compiler.
23
+ * `FailingTest.php` contains tests reported in Github issues that demonstrate compatibility bugs.
24
+ * `InputTest.php` compiles every `.scss` file in the `tests/inputs` directory
25
+ then compares to the respective `.css` file in the `tests/outputs` directory.
26
+ * `SassSpecTest.php` extracts tests from the `sass/sass-spec` repository.
27
+
28
+ When changing any of the tests in `tests/inputs`, the tests will most likely
29
+ fail because the output has changed. Once you verify that the output is correct
30
+ you can run the following command to rebuild all the tests:
31
+
32
+ BUILD=1 vendor/bin/phpunit tests
33
+
34
+ This will compile all the tests, and save results into `tests/outputs`. It also
35
+ updates the list of excluded specs from sass-spec.
36
+
37
+ To enable the full `sass-spec` compatibility tests:
38
+
39
+ TEST_SASS_SPEC=1 vendor/bin/phpunit tests
40
+
41
+ ## Coding Standard
42
+
43
+ `scssphp` source conforms to [PSR12](https://www.php-fig.org/psr/psr-12/).
44
+
45
+ Run the following command from the root directory to check the code for "sniffs".
46
+
47
+ vendor/bin/phpcs --standard=PSR12 --extensions=php bin src tests *.php
48
+
49
+ ## Static Analysis
50
+
51
+ `scssphp` uses [phpstan](https://phpstan.org/) for static analysis.
52
+
53
+ Run the following command from the root directory to analyse the codebase:
54
+
55
+ make phpstan
56
+
57
+ As most of the codebase is composed of legacy code which cannot be type-checked
58
+ fully, the setup contains a baseline file with all errors we want to ignore. In
59
+ particular, we ignore all errors related to not specifying the types inside arrays
60
+ when these arrays correspond to the representation of Sass values and Sass AST nodes
61
+ in the parser and compiler.
62
+ When contributing, the proper process to deal with static analysis is the following:
63
+
64
+ 1. Make your change in the codebase
65
+ 2. Run `make phpstan`
66
+ 3. Fix errors reported by phpstan when possible
67
+ 4. Repeat step 2 and 3 until nothing gets fixed anymore at step 3
68
+ 5. Run `make phpstan-baseline` to regenerate the phpstan baseline
69
+
70
+ Additions to the baseline will be reviewed to avoid ignoring errors that should have
71
+ been fixed.
scssphp/bin/pscss CHANGED
@@ -20,6 +20,8 @@ if (version_compare(PHP_VERSION, '5.6') < 0) {
20
  include __DIR__ . '/../scss.inc.php';
21
 
22
  use ScssPhp\ScssPhp\Compiler;
 
 
23
  use ScssPhp\ScssPhp\Parser;
24
  use ScssPhp\ScssPhp\Version;
25
 
@@ -28,10 +30,10 @@ $loadPaths = null;
28
  $dumpTree = false;
29
  $inputFile = null;
30
  $changeDir = false;
31
- $debugInfo = false;
32
- $lineNumbers = false;
33
  $encoding = false;
34
  $sourceMap = false;
 
 
35
 
36
  /**
37
  * Parse argument
@@ -60,25 +62,29 @@ function parseArgument(&$i, $options) {
60
  }
61
  }
62
 
 
 
63
  for ($i = 1; $i < $argc; $i++) {
64
  if ($argv[$i] === '-?' || $argv[$i] === '-h' || $argv[$i] === '--help') {
65
  $exe = $argv[0];
66
 
67
  $HELP = <<<EOT
68
- Usage: $exe [options] [input-file]
69
 
70
  Options include:
71
 
72
  --help Show this message [-h, -?]
73
  --continue-on-error [deprecated] Ignored
74
- --debug-info Annotate selectors with CSS referring to the source file and line number [-g]
75
- --dump-tree Dump formatted parse tree [-T]
76
  --iso8859-1 Use iso8859-1 encoding instead of default utf-8
77
- --line-numbers Annotate selectors with comments referring to the source file and line number [--line-comments]
78
  --load-path=PATH Set import path [-I]
79
  --precision=N [deprecated] Ignored. (default 10) [-p]
80
  --sourcemap Create source map file
81
- --style=FORMAT Set the output format (compact, compressed, crunched, expanded, or nested) [-s, -t]
 
 
82
  --version Print the version [-v]
83
 
84
  EOT;
@@ -95,8 +101,9 @@ EOT;
95
  continue;
96
  }
97
 
 
98
  if ($argv[$i] === '-g' || $argv[$i] === '--debug-info') {
99
- $debugInfo = true;
100
  continue;
101
  }
102
 
@@ -105,8 +112,9 @@ EOT;
105
  continue;
106
  }
107
 
 
108
  if ($argv[$i] === '--line-numbers' || $argv[$i] === '--line-comments') {
109
- $lineNumbers = true;
110
  continue;
111
  }
112
 
@@ -115,6 +123,16 @@ EOT;
115
  continue;
116
  }
117
 
 
 
 
 
 
 
 
 
 
 
118
  if ($argv[$i] === '-T' || $argv[$i] === '--dump-tree') {
119
  $dumpTree = true;
120
  continue;
@@ -142,23 +160,13 @@ EOT;
142
  continue;
143
  }
144
 
145
- if (file_exists($argv[$i])) {
146
- $inputFile = $argv[$i];
147
- continue;
148
- }
149
  }
150
 
151
 
152
- if ($inputFile) {
 
153
  $data = file_get_contents($inputFile);
154
-
155
- $newWorkingDir = dirname(realpath($inputFile));
156
- $oldWorkingDir = getcwd();
157
-
158
- if ($oldWorkingDir !== $newWorkingDir) {
159
- $changeDir = chdir($newWorkingDir);
160
- $inputFile = basename($inputFile);
161
- }
162
  } else {
163
  $data = '';
164
 
@@ -172,37 +180,65 @@ if ($dumpTree) {
172
 
173
  print_r(json_decode(json_encode($parser->parse($data)), true));
174
 
 
 
175
  exit();
176
  }
177
 
178
  $scss = new Compiler();
179
 
180
- if ($debugInfo) {
181
- $scss->setLineNumberStyle(Compiler::DEBUG_INFO);
182
- }
183
-
184
- if ($lineNumbers) {
185
- $scss->setLineNumberStyle(Compiler::LINE_COMMENTS);
186
- }
187
-
188
  if ($loadPaths) {
189
  $scss->setImportPaths(explode(PATH_SEPARATOR, $loadPaths));
190
  }
191
 
192
  if ($style) {
193
- $scss->setFormatter('ScssPhp\\ScssPhp\\Formatter\\' . ucfirst($style));
 
 
 
 
 
194
  }
195
 
 
 
 
196
  if ($sourceMap) {
197
- $scss->setSourceMap(Compiler::SOURCE_MAP_INLINE);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  }
199
 
200
  if ($encoding) {
201
  $scss->setEncoding($encoding);
202
  }
203
 
204
- echo $scss->compile($data, $inputFile);
 
 
 
 
 
 
 
 
205
 
206
- if ($changeDir) {
207
- chdir($oldWorkingDir);
 
 
 
208
  }
20
  include __DIR__ . '/../scss.inc.php';
21
 
22
  use ScssPhp\ScssPhp\Compiler;
23
+ use ScssPhp\ScssPhp\Exception\SassException;
24
+ use ScssPhp\ScssPhp\OutputStyle;
25
  use ScssPhp\ScssPhp\Parser;
26
  use ScssPhp\ScssPhp\Version;
27
 
30
  $dumpTree = false;
31
  $inputFile = null;
32
  $changeDir = false;
 
 
33
  $encoding = false;
34
  $sourceMap = false;
35
+ $embedSources = false;
36
+ $embedSourceMap = false;
37
 
38
  /**
39
  * Parse argument
62
  }
63
  }
64
 
65
+ $arguments = [];
66
+
67
  for ($i = 1; $i < $argc; $i++) {
68
  if ($argv[$i] === '-?' || $argv[$i] === '-h' || $argv[$i] === '--help') {
69
  $exe = $argv[0];
70
 
71
  $HELP = <<<EOT
72
+ Usage: $exe [options] [input-file] [output-file]
73
 
74
  Options include:
75
 
76
  --help Show this message [-h, -?]
77
  --continue-on-error [deprecated] Ignored
78
+ --debug-info [deprecated] Ignored [-g]
79
+ --dump-tree [deprecated] Dump formatted parse tree [-T]
80
  --iso8859-1 Use iso8859-1 encoding instead of default utf-8
81
+ --line-numbers [deprecated] Ignored [--line-comments]
82
  --load-path=PATH Set import path [-I]
83
  --precision=N [deprecated] Ignored. (default 10) [-p]
84
  --sourcemap Create source map file
85
+ --embed-sources Embed source file contents in source maps
86
+ --embed-source-map Embed the source map contents in CSS (default if writing to stdout)
87
+ --style=FORMAT Set the output style (compressed or expanded) [-s, -t]
88
  --version Print the version [-v]
89
 
90
  EOT;
101
  continue;
102
  }
103
 
104
+ // Keep parsing it to avoid BC breaks for scripts using it
105
  if ($argv[$i] === '-g' || $argv[$i] === '--debug-info') {
106
+ // TODO report it as a warning ?
107
  continue;
108
  }
109
 
112
  continue;
113
  }
114
 
115
+ // Keep parsing it to avoid BC breaks for scripts using it
116
  if ($argv[$i] === '--line-numbers' || $argv[$i] === '--line-comments') {
117
+ // TODO report it as a warning ?
118
  continue;
119
  }
120
 
123
  continue;
124
  }
125
 
126
+ if ($argv[$i] === '--embed-sources') {
127
+ $embedSources = true;
128
+ continue;
129
+ }
130
+
131
+ if ($argv[$i] === '--embed-source-map') {
132
+ $embedSourceMap = true;
133
+ continue;
134
+ }
135
+
136
  if ($argv[$i] === '-T' || $argv[$i] === '--dump-tree') {
137
  $dumpTree = true;
138
  continue;
160
  continue;
161
  }
162
 
163
+ $arguments[] = $argv[$i];
 
 
 
164
  }
165
 
166
 
167
+ if (isset($arguments[0]) && file_exists($arguments[0])) {
168
+ $inputFile = $arguments[0];
169
  $data = file_get_contents($inputFile);
 
 
 
 
 
 
 
 
170
  } else {
171
  $data = '';
172
 
180
 
181
  print_r(json_decode(json_encode($parser->parse($data)), true));
182
 
183
+ fwrite(STDERR, 'Warning: the --dump-tree option is deprecated. Use proper debugging tools instead.');
184
+
185
  exit();
186
  }
187
 
188
  $scss = new Compiler();
189
 
 
 
 
 
 
 
 
 
190
  if ($loadPaths) {
191
  $scss->setImportPaths(explode(PATH_SEPARATOR, $loadPaths));
192
  }
193
 
194
  if ($style) {
195
+ if ($style === OutputStyle::COMPRESSED || $style === OutputStyle::EXPANDED) {
196
+ $scss->setOutputStyle($style);
197
+ } else {
198
+ fwrite(STDERR, "WARNING: the $style style is deprecated.\n");
199
+ $scss->setFormatter('ScssPhp\\ScssPhp\\Formatter\\' . ucfirst($style));
200
+ }
201
  }
202
 
203
+ $outputFile = isset($arguments[1]) ? $arguments[1] : null;
204
+ $sourceMapFile = null;
205
+
206
  if ($sourceMap) {
207
+ $sourceMapOptions = array(
208
+ 'outputSourceFiles' => $embedSources,
209
+ );
210
+ if ($embedSourceMap || $outputFile === null) {
211
+ $scss->setSourceMap(Compiler::SOURCE_MAP_INLINE);
212
+ } else {
213
+ $sourceMapFile = $outputFile . '.map';
214
+ $sourceMapOptions['sourceMapWriteTo'] = $sourceMapFile;
215
+ $sourceMapOptions['sourceMapURL'] = basename($sourceMapFile);
216
+ $sourceMapOptions['sourceMapBasepath'] = getcwd();
217
+ $sourceMapOptions['sourceMapFilename'] = basename($outputFile);
218
+
219
+ $scss->setSourceMap(Compiler::SOURCE_MAP_FILE);
220
+ }
221
+
222
+ $scss->setSourceMapOptions($sourceMapOptions);
223
  }
224
 
225
  if ($encoding) {
226
  $scss->setEncoding($encoding);
227
  }
228
 
229
+ try {
230
+ $result = $scss->compileString($data, $inputFile);
231
+ } catch (SassException $e) {
232
+ fwrite(STDERR, 'Error: '.$e->getMessage()."\n");
233
+ exit(1);
234
+ }
235
+
236
+ if ($outputFile) {
237
+ file_put_contents($outputFile, $result->getCss());
238
 
239
+ if ($sourceMapFile !== null && $result->getSourceMap() !== null) {
240
+ file_put_contents($sourceMapFile, $result->getSourceMap());
241
+ }
242
+ } else {
243
+ echo $result->getCss();
244
  }
scssphp/composer.json ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "scssphp/scssphp",
3
+ "type": "library",
4
+ "description": "scssphp is a compiler for SCSS written in PHP.",
5
+ "keywords": ["css", "stylesheet", "scss", "sass", "less"],
6
+ "homepage": "http://scssphp.github.io/scssphp/",
7
+ "license": [
8
+ "MIT"
9
+ ],
10
+ "authors": [
11
+ {
12
+ "name": "Anthon Pang",
13
+ "email": "apang@softwaredevelopment.ca",
14
+ "homepage": "https://github.com/robocoder"
15
+ },
16
+ {
17
+ "name": "Cédric Morin",
18
+ "email": "cedric@yterium.com",
19
+ "homepage": "https://github.com/Cerdic"
20
+ }
21
+ ],
22
+ "autoload": {
23
+ "psr-4": { "ScssPhp\\ScssPhp\\": "src/" }
24
+ },
25
+ "autoload-dev": {
26
+ "psr-4": { "ScssPhp\\ScssPhp\\Tests\\": "tests/" }
27
+ },
28
+ "require": {
29
+ "php": ">=5.6.0",
30
+ "ext-json": "*",
31
+ "ext-ctype": "*"
32
+ },
33
+ "suggest": {
34
+ "ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv",
35
+ "ext-iconv": "Can be used as fallback when ext-mbstring is not available"
36
+ },
37
+ "require-dev": {
38
+ "bamarni/composer-bin-plugin": "^1.4",
39
+ "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4",
40
+ "sass/sass-spec": "*",
41
+ "squizlabs/php_codesniffer": "~3.5",
42
+ "symfony/phpunit-bridge": "^5.1",
43
+ "twbs/bootstrap": "~5.0",
44
+ "twbs/bootstrap4": "4.6.0",
45
+ "zurb/foundation": "~6.5"
46
+ },
47
+ "repositories": [
48
+ {
49
+ "type": "package",
50
+ "package": {
51
+ "name": "sass/sass-spec",
52
+ "version": "2021.05.10",
53
+ "source": {
54
+ "type": "git",
55
+ "url": "https://github.com/sass/sass-spec.git",
56
+ "reference": "b9bf24a936528f333fb30ee59ca550c6da551c11"
57
+ },
58
+ "dist": {
59
+ "type": "zip",
60
+ "url": "https://api.github.com/repos/sass/sass-spec/zipball/b9bf24a936528f333fb30ee59ca550c6da551c11",
61
+ "reference": "b9bf24a936528f333fb30ee59ca550c6da551c11",
62
+ "shasum": ""
63
+ }
64
+ }
65
+ },
66
+ {
67
+ "type": "package",
68
+ "package": {
69
+ "name": "twbs/bootstrap4",
70
+ "version": "v4.6.0",
71
+ "source": {
72
+ "type": "git",
73
+ "url": "https://github.com/twbs/bootstrap.git",
74
+ "reference": "6ffb0b48e455430f8a5359ed689ad64c1143fac2"
75
+ },
76
+ "dist": {
77
+ "type": "zip",
78
+ "url": "https://api.github.com/repos/twbs/bootstrap/zipball/6ffb0b48e455430f8a5359ed689ad64c1143fac2",
79
+ "reference": "6ffb0b48e455430f8a5359ed689ad64c1143fac2",
80
+ "shasum": ""
81
+ }
82
+ }
83
+ }
84
+ ],
85
+ "bin": ["bin/pscss"],
86
+ "config": {
87
+ "sort-packages": true
88
+ }
89
+ }
scssphp/phpcs.xml.dist ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <ruleset name="PSR12 (adapted for PHP 5.6+)">
3
+ <rule ref="PSR12">
4
+ <!-- Ignore this PHP 7.1+ sniff as long as we support PHP 5.6+ -->
5
+ <exclude name="PSR12.Properties.ConstantVisibility.NotFound"/>
6
+
7
+ <!-- This sniff doesn't ignore comment blocks -->
8
+ <!--
9
+ <exclude name="Generic.Files.LineLength"/>
10
+ -->
11
+ </rule>
12
+ </ruleset>
scssphp/scss.inc.php CHANGED
@@ -4,33 +4,18 @@ if (version_compare(PHP_VERSION, '5.6') < 0) {
4
  throw new \Exception('scssphp requires PHP 5.6 or above');
5
  }
6
 
7
- if (! class_exists('ScssPhp\ScssPhp\Version', false)) {
8
- include_once __DIR__ . '/src/Base/Range.php';
9
- include_once __DIR__ . '/src/Block.php';
10
- include_once __DIR__ . '/src/Cache.php';
11
- include_once __DIR__ . '/src/Colors.php';
12
- include_once __DIR__ . '/src/Compiler.php';
13
- include_once __DIR__ . '/src/Compiler/Environment.php';
14
- include_once __DIR__ . '/src/Exception/SassException.php';
15
- include_once __DIR__ . '/src/Exception/CompilerException.php';
16
- include_once __DIR__ . '/src/Exception/ParserException.php';
17
- include_once __DIR__ . '/src/Exception/RangeException.php';
18
- include_once __DIR__ . '/src/Exception/ServerException.php';
19
- include_once __DIR__ . '/src/Formatter.php';
20
- include_once __DIR__ . '/src/Formatter/Compact.php';
21
- include_once __DIR__ . '/src/Formatter/Compressed.php';
22
- include_once __DIR__ . '/src/Formatter/Crunched.php';
23
- include_once __DIR__ . '/src/Formatter/Debug.php';
24
- include_once __DIR__ . '/src/Formatter/Expanded.php';
25
- include_once __DIR__ . '/src/Formatter/Nested.php';
26
- include_once __DIR__ . '/src/Formatter/OutputBlock.php';
27
- include_once __DIR__ . '/src/Node.php';
28
- include_once __DIR__ . '/src/Node/Number.php';
29
- include_once __DIR__ . '/src/Parser.php';
30
- include_once __DIR__ . '/src/SourceMap/Base64.php';
31
- include_once __DIR__ . '/src/SourceMap/Base64VLQ.php';
32
- include_once __DIR__ . '/src/SourceMap/SourceMapGenerator.php';
33
- include_once __DIR__ . '/src/Type.php';
34
- include_once __DIR__ . '/src/Util.php';
35
- include_once __DIR__ . '/src/Version.php';
36
  }
4
  throw new \Exception('scssphp requires PHP 5.6 or above');
5
  }
6
 
7
+ if (! class_exists('ScssPhp\ScssPhp\Version')) {
8
+ spl_autoload_register(function ($class) {
9
+ if (0 !== strpos($class, 'ScssPhp\ScssPhp\\')) {
10
+ // Not a ScssPhp class
11
+ return;
12
+ }
13
+
14
+ $subClass = substr($class, strlen('ScssPhp\ScssPhp\\'));
15
+ $path = __DIR__ . '/src/' . str_replace('\\', '/', $subClass) . '.php';
16
+
17
+ if (file_exists($path)) {
18
+ require $path;
19
+ }
20
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
scssphp/src/Base/Range.php CHANGED
@@ -16,10 +16,19 @@ namespace ScssPhp\ScssPhp\Base;
16
  * Range
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
 
 
19
  */
20
  class Range
21
  {
 
 
 
22
  public $first;
 
 
 
 
23
  public $last;
24
 
25
  /**
16
  * Range
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
19
+ *
20
+ * @internal
21
  */
22
  class Range
23
  {
24
+ /**
25
+ * @var float|int
26
+ */
27
  public $first;
28
+
29
+ /**
30
+ * @var float|int
31
+ */
32
  public $last;
33
 
34
  /**
scssphp/src/Block.php CHANGED
@@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp;
16
  * Block
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
 
 
19
  */
20
  class Block
21
  {
@@ -50,7 +52,7 @@ class Block
50
  public $sourceColumn;
51
 
52
  /**
53
- * @var array
54
  */
55
  public $selectors;
56
 
@@ -65,7 +67,7 @@ class Block
65
  public $children;
66
 
67
  /**
68
- * @var \ScssPhp\ScssPhp\Block
69
  */
70
  public $selfParent;
71
  }
16
  * Block
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
19
+ *
20
+ * @internal
21
  */
22
  class Block
23
  {
52
  public $sourceColumn;
53
 
54
  /**
55
+ * @var array|null
56
  */
57
  public $selectors;
58
 
67
  public $children;
68
 
69
  /**
70
+ * @var \ScssPhp\ScssPhp\Block|null
71
  */
72
  public $selfParent;
73
  }
scssphp/src/Cache.php CHANGED
@@ -13,6 +13,7 @@
13
  namespace ScssPhp\ScssPhp;
14
 
15
  use Exception;
 
16
 
17
  /**
18
  * The scss cache manager.
@@ -29,30 +30,54 @@ use Exception;
29
  * SCSS cache
30
  *
31
  * @author Cedric Morin <cedric@yterium.com>
 
 
32
  */
33
  class Cache
34
  {
35
  const CACHE_VERSION = 1;
36
 
37
- // directory used for storing data
 
 
 
 
38
  public static $cacheDir = false;
39
 
40
- // prefix for the storing data
 
 
 
 
41
  public static $prefix = 'scssphp_';
42
 
43
- // force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit
 
 
 
 
44
  public static $forceRefresh = false;
45
 
46
- // specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up
 
 
 
 
47
  public static $gcLifetime = 604800;
48
 
49
- // array of already refreshed cache if $forceRefresh==='once'
 
 
 
 
50
  protected static $refreshed = [];
51
 
52
  /**
53
  * Constructor
54
  *
55
  * @param array $options
 
 
56
  */
57
  public function __construct($options)
58
  {
@@ -84,10 +109,10 @@ class Cache
84
  * Get the cached result of $operation on $what,
85
  * which is known as dependant from the content of $options
86
  *
87
- * @param string $operation parse, compile...
88
- * @param mixed $what content key (e.g., filename to be treated)
89
- * @param array $options any option that affect the operation result on the content
90
- * @param integer $lastModified last modified timestamp
91
  *
92
  * @return mixed
93
  *
@@ -127,6 +152,8 @@ class Cache
127
  * @param mixed $what
128
  * @param mixed $value
129
  * @param array $options
 
 
130
  */
131
  public function setCache($operation, $what, $value, $options = [])
132
  {
@@ -156,6 +183,7 @@ class Cache
156
  {
157
  $t = [
158
  'version' => self::CACHE_VERSION,
 
159
  'operation' => $operation,
160
  'what' => $what,
161
  'options' => $options
@@ -172,6 +200,8 @@ class Cache
172
  /**
173
  * Check that the cache dir exists and is writeable
174
  *
 
 
175
  * @throws \Exception
176
  */
177
  public static function checkCacheDir()
@@ -190,6 +220,8 @@ class Cache
190
 
191
  /**
192
  * Delete unused cached files
 
 
193
  */
194
  public static function cleanCache()
195
  {
13
  namespace ScssPhp\ScssPhp;
14
 
15
  use Exception;
16
+ use ScssPhp\ScssPhp\Version;
17
 
18
  /**
19
  * The scss cache manager.
30
  * SCSS cache
31
  *
32
  * @author Cedric Morin <cedric@yterium.com>
33
+ *
34
+ * @internal
35
  */
36
  class Cache
37
  {
38
  const CACHE_VERSION = 1;
39
 
40
+ /**
41
+ * directory used for storing data
42
+ *
43
+ * @var string|false
44
+ */
45
  public static $cacheDir = false;
46
 
47
+ /**
48
+ * prefix for the storing data
49
+ *
50
+ * @var string
51
+ */
52
  public static $prefix = 'scssphp_';
53
 
54
+ /**
55
+ * force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit
56
+ *
57
+ * @var bool|string
58
+ */
59
  public static $forceRefresh = false;
60
 
61
+ /**
62
+ * specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up
63
+ *
64
+ * @var int
65
+ */
66
  public static $gcLifetime = 604800;
67
 
68
+ /**
69
+ * array of already refreshed cache if $forceRefresh==='once'
70
+ *
71
+ * @var array<string, bool>
72
+ */
73
  protected static $refreshed = [];
74
 
75
  /**
76
  * Constructor
77
  *
78
  * @param array $options
79
+ *
80
+ * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string} $options
81
  */
82
  public function __construct($options)
83
  {
109
  * Get the cached result of $operation on $what,
110
  * which is known as dependant from the content of $options
111
  *
112
+ * @param string $operation parse, compile...
113
+ * @param mixed $what content key (e.g., filename to be treated)
114
+ * @param array $options any option that affect the operation result on the content
115
+ * @param int|null $lastModified last modified timestamp
116
  *
117
  * @return mixed
118
  *
152
  * @param mixed $what
153
  * @param mixed $value
154
  * @param array $options
155
+ *
156
+ * @return void
157
  */
158
  public function setCache($operation, $what, $value, $options = [])
159
  {
183
  {
184
  $t = [
185
  'version' => self::CACHE_VERSION,
186
+ 'scssphpVersion' => Version::VERSION,
187
  'operation' => $operation,
188
  'what' => $what,
189
  'options' => $options
200
  /**
201
  * Check that the cache dir exists and is writeable
202
  *
203
+ * @return void
204
+ *
205
  * @throws \Exception
206
  */
207
  public static function checkCacheDir()
220
 
221
  /**
222
  * Delete unused cached files
223
+ *
224
+ * @return void
225
  */
226
  public static function cleanCache()
227
  {
scssphp/src/Colors.php CHANGED
@@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp;
16
  * CSS Colors
17
  *
18
  * @author Leaf Corcoran <leafot@gmail.com>
 
 
19
  */
20
  class Colors
21
  {
@@ -24,13 +26,13 @@ class Colors
24
  *
25
  * @see http://www.w3.org/TR/css3-color
26
  *
27
- * @var array
28
  */
29
  protected static $cssColors = [
30
  'aliceblue' => '240,248,255',
31
  'antiquewhite' => '250,235,215',
32
- 'cyan' => '0,255,255',
33
  'aqua' => '0,255,255',
 
34
  'aquamarine' => '127,255,212',
35
  'azure' => '240,255,255',
36
  'beige' => '245,245,220',
@@ -75,8 +77,8 @@ class Colors
75
  'firebrick' => '178,34,34',
76
  'floralwhite' => '255,250,240',
77
  'forestgreen' => '34,139,34',
78
- 'magenta' => '255,0,255',
79
  'fuchsia' => '255,0,255',
 
80
  'gainsboro' => '220,220,220',
81
  'ghostwhite' => '248,248,255',
82
  'gold' => '255,215,0',
@@ -183,7 +185,7 @@ class Colors
183
  *
184
  * @param string $colorName
185
  *
186
- * @return array|null
187
  */
188
  public static function colorNameToRGBa($colorName)
189
  {
@@ -205,7 +207,7 @@ class Colors
205
  * @param integer $r
206
  * @param integer $g
207
  * @param integer $b
208
- * @param integer $a
209
  *
210
  * @return string|null
211
  */
16
  * CSS Colors
17
  *
18
  * @author Leaf Corcoran <leafot@gmail.com>
19
+ *
20
+ * @internal
21
  */
22
  class Colors
23
  {
26
  *
27
  * @see http://www.w3.org/TR/css3-color
28
  *
29
+ * @var array<string, string>
30
  */
31
  protected static $cssColors = [
32
  'aliceblue' => '240,248,255',
33
  'antiquewhite' => '250,235,215',
 
34
  'aqua' => '0,255,255',
35
+ 'cyan' => '0,255,255',
36
  'aquamarine' => '127,255,212',
37
  'azure' => '240,255,255',
38
  'beige' => '245,245,220',
77
  'firebrick' => '178,34,34',
78
  'floralwhite' => '255,250,240',
79
  'forestgreen' => '34,139,34',
 
80
  'fuchsia' => '255,0,255',
81
+ 'magenta' => '255,0,255',
82
  'gainsboro' => '220,220,220',
83
  'ghostwhite' => '248,248,255',
84
  'gold' => '255,215,0',
185
  *
186
  * @param string $colorName
187
  *
188
+ * @return int[]|null
189
  */
190
  public static function colorNameToRGBa($colorName)
191
  {
207
  * @param integer $r
208
  * @param integer $g
209
  * @param integer $b
210
+ * @param integer|float $a
211
  *
212
  * @return string|null
213
  */
scssphp/src/CompilationResult.php ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * SCSSPHP
5
+ *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
+ *
8
+ * @license http://opensource.org/licenses/MIT MIT
9
+ *
10
+ * @link http://scssphp.github.io/scssphp
11
+ */
12
+
13
+ namespace ScssPhp\ScssPhp;
14
+
15
+ class CompilationResult
16
+ {
17
+ /**
18
+ * @var string
19
+ */
20
+ private $css;
21
+
22
+ /**
23
+ * @var string|null
24
+ */
25
+ private $sourceMap;
26
+
27
+ /**
28
+ * @var string[]
29
+ */
30
+ private $includedFiles;
31
+
32
+ /**
33
+ * @param string $css
34
+ * @param string|null $sourceMap
35
+ * @param string[] $includedFiles
36
+ */
37
+ public function __construct($css, $sourceMap, array $includedFiles)
38
+ {
39
+ $this->css = $css;
40
+ $this->sourceMap = $sourceMap;
41
+ $this->includedFiles = $includedFiles;
42
+ }
43
+
44
+ /**
45
+ * @return string
46
+ */
47
+ public function getCss()
48
+ {
49
+ return $this->css;
50
+ }
51
+
52
+ /**
53
+ * @return string[]
54
+ */
55
+ public function getIncludedFiles()
56
+ {
57
+ return $this->includedFiles;
58
+ }
59
+
60
+ /**
61
+ * The sourceMap content, if it was generated
62
+ *
63
+ * @return null|string
64
+ */
65
+ public function getSourceMap()
66
+ {
67
+ return $this->sourceMap;
68
+ }
69
+ }
scssphp/src/Compiler.php CHANGED
@@ -13,17 +13,20 @@
13
  namespace ScssPhp\ScssPhp;
14
 
15
  use ScssPhp\ScssPhp\Base\Range;
16
- use ScssPhp\ScssPhp\Block;
17
- use ScssPhp\ScssPhp\Cache;
18
- use ScssPhp\ScssPhp\Colors;
19
  use ScssPhp\ScssPhp\Compiler\Environment;
20
  use ScssPhp\ScssPhp\Exception\CompilerException;
 
 
 
 
 
21
  use ScssPhp\ScssPhp\Formatter\OutputBlock;
22
- use ScssPhp\ScssPhp\Node;
 
 
23
  use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
24
- use ScssPhp\ScssPhp\Type;
25
- use ScssPhp\ScssPhp\Parser;
26
- use ScssPhp\ScssPhp\Util;
27
 
28
  /**
29
  * The scss compiler and parser.
@@ -56,15 +59,35 @@ use ScssPhp\ScssPhp\Util;
56
  * SCSS compiler
57
  *
58
  * @author Leaf Corcoran <leafot@gmail.com>
 
 
59
  */
60
  class Compiler
61
  {
 
 
 
62
  const LINE_COMMENTS = 1;
 
 
 
63
  const DEBUG_INFO = 2;
64
 
 
 
 
65
  const WITH_RULE = 1;
 
 
 
66
  const WITH_MEDIA = 2;
 
 
 
67
  const WITH_SUPPORTS = 4;
 
 
 
68
  const WITH_ALL = 7;
69
 
70
  const SOURCE_MAP_NONE = 0;
@@ -72,7 +95,7 @@ class Compiler
72
  const SOURCE_MAP_FILE = 2;
73
 
74
  /**
75
- * @var array
76
  */
77
  protected static $operatorNames = [
78
  '+' => 'add',
@@ -88,11 +111,10 @@ class Compiler
88
 
89
  '<=' => 'lte',
90
  '>=' => 'gte',
91
- '<=>' => 'cmp',
92
  ];
93
 
94
  /**
95
- * @var array
96
  */
97
  protected static $namespaces = [
98
  'special' => '%',
@@ -102,7 +124,9 @@ class Compiler
102
 
103
  public static $true = [Type::T_KEYWORD, 'true'];
104
  public static $false = [Type::T_KEYWORD, 'false'];
 
105
  public static $NaN = [Type::T_KEYWORD, 'NaN'];
 
106
  public static $Infinity = [Type::T_KEYWORD, 'Infinity'];
107
  public static $null = [Type::T_NULL];
108
  public static $nullString = [Type::T_STRING, '', []];
@@ -114,79 +138,220 @@ class Compiler
114
  public static $with = [Type::T_KEYWORD, 'with'];
115
  public static $without = [Type::T_KEYWORD, 'without'];
116
 
117
- protected $importPaths = [''];
 
 
 
 
 
 
118
  protected $importCache = [];
 
 
 
 
119
  protected $importedFiles = [];
 
 
 
 
 
120
  protected $userFunctions = [];
 
 
 
121
  protected $registeredVars = [];
 
 
 
122
  protected $registeredFeatures = [
123
  'extend-selector-pseudoclass' => false,
124
  'at-error' => true,
125
- 'units-level-3' => false,
126
  'global-variable-shadowing' => false,
127
  ];
128
 
 
 
 
129
  protected $encoding = null;
 
 
 
 
130
  protected $lineNumberStyle = null;
131
 
 
 
 
 
132
  protected $sourceMap = self::SOURCE_MAP_NONE;
 
 
 
 
 
133
  protected $sourceMapOptions = [];
134
 
135
  /**
136
  * @var string|\ScssPhp\ScssPhp\Formatter
137
  */
138
- protected $formatter = 'ScssPhp\ScssPhp\Formatter\Nested';
139
 
 
 
 
140
  protected $rootEnv;
 
 
 
141
  protected $rootBlock;
142
 
143
  /**
144
  * @var \ScssPhp\ScssPhp\Compiler\Environment
145
  */
146
  protected $env;
 
 
 
147
  protected $scope;
 
 
 
148
  protected $storeEnv;
 
 
 
149
  protected $charsetSeen;
 
 
 
150
  protected $sourceNames;
151
 
 
 
 
152
  protected $cache;
153
 
 
 
 
 
 
 
 
 
154
  protected $indentLevel;
 
 
 
155
  protected $extends;
 
 
 
156
  protected $extendsMap;
157
- protected $parsedFiles;
 
 
 
 
 
 
 
 
158
  protected $parser;
 
 
 
159
  protected $sourceIndex;
 
 
 
160
  protected $sourceLine;
 
 
 
161
  protected $sourceColumn;
162
- protected $stderr;
 
 
163
  protected $shouldEvaluate;
 
 
 
 
164
  protected $ignoreErrors;
 
 
 
165
  protected $ignoreCallStackMessage = false;
166
 
 
 
 
167
  protected $callStack = [];
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  /**
170
  * Constructor
171
  *
172
  * @param array|null $cacheOptions
 
173
  */
174
  public function __construct($cacheOptions = null)
175
  {
176
- $this->parsedFiles = [];
177
  $this->sourceNames = [];
178
 
179
  if ($cacheOptions) {
180
  $this->cache = new Cache($cacheOptions);
 
 
 
181
  }
182
 
183
- $this->stderr = fopen('php://stderr', 'w');
184
  }
185
 
186
  /**
187
  * Get compiler options
188
  *
189
- * @return array
 
 
190
  */
191
  public function getCompileOptions()
192
  {
@@ -198,53 +363,96 @@ class Compiler
198
  'sourceMap' => serialize($this->sourceMap),
199
  'sourceMapOptions' => $this->sourceMapOptions,
200
  'formatter' => $this->formatter,
 
201
  ];
202
 
203
  return $options;
204
  }
205
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  /**
207
  * Set an alternative error output stream, for testing purpose only
208
  *
209
  * @param resource $handle
 
 
 
 
210
  */
211
  public function setErrorOuput($handle)
212
  {
213
- $this->stderr = $handle;
 
 
214
  }
215
 
216
  /**
217
  * Compile scss
218
  *
219
- * @api
220
- *
221
- * @param string $code
222
- * @param string $path
223
  *
224
  * @return string
 
 
 
 
225
  */
226
  public function compile($code, $path = null)
227
  {
228
- if ($this->cache) {
229
- $cacheKey = ($path ? $path : '(stdin)') . ':' . md5($code);
230
- $compileOptions = $this->getCompileOptions();
231
- $cache = $this->cache->getCache('compile', $cacheKey, $compileOptions);
232
 
233
- if (\is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) {
234
- // check if any dependency file changed before accepting the cache
235
- foreach ($cache['dependencies'] as $file => $mtime) {
236
- if (! is_file($file) || filemtime($file) !== $mtime) {
237
- unset($cache);
238
- break;
239
- }
240
- }
241
 
242
- if (isset($cache)) {
243
- return $cache['out'];
244
- }
 
 
 
 
 
245
  }
246
  }
247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
249
  $this->indentLevel = -1;
250
  $this->extends = [];
@@ -258,65 +466,149 @@ class Compiler
258
  $this->charsetSeen = null;
259
  $this->shouldEvaluate = null;
260
  $this->ignoreCallStackMessage = false;
 
 
 
261
 
262
- $this->parser = $this->parserFactory($path);
263
- $tree = $this->parser->parse($code);
264
- $this->parser = null;
 
 
 
 
 
265
 
266
- $this->formatter = new $this->formatter();
267
- $this->rootBlock = null;
268
- $this->rootEnv = $this->pushEnv($tree);
 
269
 
270
- $this->injectVariables($this->registeredVars);
271
- $this->compileRoot($tree);
272
- $this->popEnv();
273
 
274
- $sourceMapGenerator = null;
 
 
 
275
 
276
- if ($this->sourceMap) {
277
- if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) {
278
- $sourceMapGenerator = $this->sourceMap;
279
- $this->sourceMap = self::SOURCE_MAP_FILE;
280
- } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
281
- $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
282
  }
283
- }
284
 
285
- $out = $this->formatter->format($this->scope, $sourceMapGenerator);
 
 
 
 
 
 
 
 
 
286
 
287
- if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) {
288
- $sourceMap = $sourceMapGenerator->generateJson();
289
- $sourceMapUrl = null;
290
 
291
- switch ($this->sourceMap) {
292
- case self::SOURCE_MAP_INLINE:
293
- $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap));
294
- break;
295
 
296
- case self::SOURCE_MAP_FILE:
297
- $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap);
298
- break;
 
 
299
  }
300
 
301
- $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  }
303
 
 
 
304
  if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
305
- $v = [
306
- 'dependencies' => $this->getParsedFiles(),
307
- 'out' => &$out,
308
- ];
309
 
310
- $this->cache->setCache('compile', $cacheKey, $v, $compileOptions);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  }
312
 
313
- return $out;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  }
315
 
316
  /**
317
  * Instantiate parser
318
  *
319
- * @param string $path
320
  *
321
  * @return \ScssPhp\ScssPhp\Parser
322
  */
@@ -329,11 +621,11 @@ class Compiler
329
  // Otherwise, the CSS will be rendered as-is. It can even be extended!
330
  $cssOnly = false;
331
 
332
- if (substr($path, '-4') === '.css') {
333
  $cssOnly = true;
334
  }
335
 
336
- $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly);
337
 
338
  $this->sourceNames[] = $path;
339
  $this->addParsedFile($path);
@@ -366,6 +658,8 @@ class Compiler
366
  * @param array $target
367
  * @param array $origin
368
  * @param array|null $block
 
 
369
  */
370
  protected function pushExtends($target, $origin, $block)
371
  {
@@ -384,8 +678,8 @@ class Compiler
384
  /**
385
  * Make output block
386
  *
387
- * @param string $type
388
- * @param array $selectors
389
  *
390
  * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
391
  */
@@ -416,6 +710,8 @@ class Compiler
416
  * Compile root
417
  *
418
  * @param \ScssPhp\ScssPhp\Block $rootBlock
 
 
419
  */
420
  protected function compileRoot(Block $rootBlock)
421
  {
@@ -428,6 +724,8 @@ class Compiler
428
 
429
  /**
430
  * Report missing selectors
 
 
431
  */
432
  protected function missingSelectors()
433
  {
@@ -456,6 +754,8 @@ class Compiler
456
  *
457
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
458
  * @param string $parentKey
 
 
459
  */
460
  protected function flattenSelectors(OutputBlock $block, $parentKey = null)
461
  {
@@ -511,7 +811,7 @@ class Compiler
511
  }
512
 
513
  /**
514
- * Glue parts of :not( or :nth-child( ... that are in general splitted in selectors parts
515
  *
516
  * @param array $parts
517
  *
@@ -552,6 +852,8 @@ class Compiler
552
  * @param array $out
553
  * @param integer $from
554
  * @param boolean $initial
 
 
555
  */
556
  protected function matchExtends($selector, &$out, $from = 0, $initial = true)
557
  {
@@ -705,6 +1007,8 @@ class Compiler
705
  *
706
  * @param array $out
707
  * @param array $extended
 
 
708
  */
709
  protected function pushOrMergeExtentedSelector(&$out, $extended)
710
  {
@@ -796,7 +1100,7 @@ class Compiler
796
  $buffer = $matches[2];
797
  $parser = $this->parserFactory(__METHOD__);
798
 
799
- if ($parser->parseSelector($buffer, $subSelectors)) {
800
  foreach ($subSelectors as $ksub => $subSelector) {
801
  $subExtended = [];
802
  $this->matchExtends($subSelector, $subExtended, 0, false);
@@ -956,6 +1260,8 @@ class Compiler
956
  * Compile media
957
  *
958
  * @param \ScssPhp\ScssPhp\Block $media
 
 
959
  */
960
  protected function compileMedia(Block $media)
961
  {
@@ -963,7 +1269,7 @@ class Compiler
963
 
964
  $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env));
965
 
966
- if (! empty($mediaQueries) && $mediaQueries) {
967
  $previousScope = $this->scope;
968
  $parentScope = $this->mediaParent($this->scope);
969
 
@@ -1003,30 +1309,6 @@ class Compiler
1003
  $wrapped->children = $media->children;
1004
 
1005
  $media->children = [[Type::T_BLOCK, $wrapped]];
1006
-
1007
- if (isset($this->lineNumberStyle)) {
1008
- $annotation = $this->makeOutputBlock(Type::T_COMMENT);
1009
- $annotation->depth = 0;
1010
-
1011
- $file = $this->sourceNames[$media->sourceIndex];
1012
- $line = $media->sourceLine;
1013
-
1014
- switch ($this->lineNumberStyle) {
1015
- case static::LINE_COMMENTS:
1016
- $annotation->lines[] = '/* line ' . $line
1017
- . ($file ? ', ' . $file : '')
1018
- . ' */';
1019
- break;
1020
-
1021
- case static::DEBUG_INFO:
1022
- $annotation->lines[] = '@media -sass-debug-info{'
1023
- . ($file ? 'filename{font-family:"' . $file . '"}' : '')
1024
- . 'line{font-family:' . $line . '}}';
1025
- break;
1026
- }
1027
-
1028
- $this->scope->children[] = $annotation;
1029
- }
1030
  }
1031
 
1032
  $this->compileChildrenNoReturn($media->children, $this->scope);
@@ -1060,8 +1342,10 @@ class Compiler
1060
  /**
1061
  * Compile directive
1062
  *
1063
- * @param \ScssPhp\ScssPhp\Block|array $block
1064
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
 
 
1065
  */
1066
  protected function compileDirective($directive, OutputBlock $out)
1067
  {
@@ -1103,7 +1387,7 @@ class Compiler
1103
  * directive names can include some interpolation
1104
  *
1105
  * @param string|array $directiveName
1106
- * @return array|string
1107
  * @throws CompilerException
1108
  */
1109
  protected function compileDirectiveName($directiveName)
@@ -1119,6 +1403,8 @@ class Compiler
1119
  * Compile at-root
1120
  *
1121
  * @param \ScssPhp\ScssPhp\Block $block
 
 
1122
  */
1123
  protected function compileAtRoot(Block $block)
1124
  {
@@ -1144,9 +1430,10 @@ class Compiler
1144
  }
1145
 
1146
  $selfParent = $block->selfParent;
 
1147
 
1148
  if (
1149
- ! $block->selfParent->selectors &&
1150
  isset($block->parent) && $block->parent &&
1151
  isset($block->parent->selectors) && $block->parent->selectors
1152
  ) {
@@ -1175,19 +1462,19 @@ class Compiler
1175
  * @param array $with
1176
  * @param array $without
1177
  *
1178
- * @return mixed
1179
  */
1180
  protected function filterScopeWithWithout($scope, $with, $without)
1181
  {
1182
  $filteredScopes = [];
1183
  $childStash = [];
1184
 
1185
- if ($scope->type === TYPE::T_ROOT) {
1186
  return $scope;
1187
  }
1188
 
1189
  // start from the root
1190
- while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) {
1191
  array_unshift($childStash, $scope);
1192
  $scope = $scope->parent;
1193
  }
@@ -1248,7 +1535,7 @@ class Compiler
1248
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1249
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope
1250
  *
1251
- * @return mixed
1252
  */
1253
  protected function completeScope($scope, $previousScope)
1254
  {
@@ -1315,7 +1602,7 @@ class Compiler
1315
  }
1316
  }
1317
 
1318
- if ($this->libMapHasKey([$withCondition, static::$with])) {
1319
  $without = []; // cancel the default
1320
  $list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
1321
 
@@ -1326,7 +1613,7 @@ class Compiler
1326
  }
1327
  }
1328
 
1329
- if ($this->libMapHasKey([$withCondition, static::$without])) {
1330
  $without = []; // cancel the default
1331
  $list = $this->coerceList($this->libMapGet([$withCondition, static::$without]));
1332
 
@@ -1344,11 +1631,13 @@ class Compiler
1344
  /**
1345
  * Filter env stack
1346
  *
1347
- * @param array $envs
1348
  * @param array $with
1349
  * @param array $without
1350
  *
1351
- * @return \ScssPhp\ScssPhp\Compiler\Environment
 
 
1352
  */
1353
  protected function filterWithWithout($envs, $with, $without)
1354
  {
@@ -1403,7 +1692,7 @@ class Compiler
1403
  $s = reset($s);
1404
  }
1405
 
1406
- if (\is_object($s) && $s instanceof Node\Number) {
1407
  return $this->testWithWithout('keyframes', $with, $without);
1408
  }
1409
  }
@@ -1440,7 +1729,9 @@ class Compiler
1440
  * Compile keyframe block
1441
  *
1442
  * @param \ScssPhp\ScssPhp\Block $block
1443
- * @param array $selectors
 
 
1444
  */
1445
  protected function compileKeyframeBlock(Block $block, $selectors)
1446
  {
@@ -1469,6 +1760,8 @@ class Compiler
1469
  *
1470
  * @param \ScssPhp\ScssPhp\Block $block
1471
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
 
 
1472
  */
1473
  protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out)
1474
  {
@@ -1502,7 +1795,9 @@ class Compiler
1502
  * Compile nested block
1503
  *
1504
  * @param \ScssPhp\ScssPhp\Block $block
1505
- * @param array $selectors
 
 
1506
  */
1507
  protected function compileNestedBlock(Block $block, $selectors)
1508
  {
@@ -1564,6 +1859,8 @@ class Compiler
1564
  * @see Compiler::compileChild()
1565
  *
1566
  * @param \ScssPhp\ScssPhp\Block $block
 
 
1567
  */
1568
  protected function compileBlock(Block $block)
1569
  {
@@ -1572,30 +1869,6 @@ class Compiler
1572
 
1573
  $out = $this->makeOutputBlock(null);
1574
 
1575
- if (isset($this->lineNumberStyle) && \count($env->selectors) && \count($block->children)) {
1576
- $annotation = $this->makeOutputBlock(Type::T_COMMENT);
1577
- $annotation->depth = 0;
1578
-
1579
- $file = $this->sourceNames[$block->sourceIndex];
1580
- $line = $block->sourceLine;
1581
-
1582
- switch ($this->lineNumberStyle) {
1583
- case static::LINE_COMMENTS:
1584
- $annotation->lines[] = '/* line ' . $line
1585
- . ($file ? ', ' . $file : '')
1586
- . ' */';
1587
- break;
1588
-
1589
- case static::DEBUG_INFO:
1590
- $annotation->lines[] = '@media -sass-debug-info{'
1591
- . ($file ? 'filename{font-family:"' . $file . '"}' : '')
1592
- . 'line{font-family:' . $line . '}}';
1593
- break;
1594
- }
1595
-
1596
- $this->scope->children[] = $annotation;
1597
- }
1598
-
1599
  $this->scope->children[] = $out;
1600
 
1601
  if (\count($block->children)) {
@@ -1627,7 +1900,7 @@ class Compiler
1627
  * @param array $value
1628
  * @param boolean $pushEnv
1629
  *
1630
- * @return array|mixed|string
1631
  */
1632
  protected function compileCommentValue($value, $pushEnv = false)
1633
  {
@@ -1638,17 +1911,16 @@ class Compiler
1638
  $this->pushEnv();
1639
  }
1640
 
1641
- $ignoreCallStackMessage = $this->ignoreCallStackMessage;
1642
- $this->ignoreCallStackMessage = true;
1643
-
1644
  try {
1645
  $c = $this->compileValue($value[2]);
1646
- } catch (\Exception $e) {
 
 
 
 
1647
  // ignore error in comment compilation which are only interpolation
1648
  }
1649
 
1650
- $this->ignoreCallStackMessage = $ignoreCallStackMessage;
1651
-
1652
  if ($pushEnv) {
1653
  $this->popEnv();
1654
  }
@@ -1661,6 +1933,8 @@ class Compiler
1661
  * Compile root level comment
1662
  *
1663
  * @param array $block
 
 
1664
  */
1665
  protected function compileComment($block)
1666
  {
@@ -1689,7 +1963,13 @@ class Compiler
1689
  $buffer = $this->collapseSelectors($selectors);
1690
  $parser = $this->parserFactory(__METHOD__);
1691
 
1692
- if ($parser->parseSelector($buffer, $newSelectors)) {
 
 
 
 
 
 
1693
  $selectors = array_map([$this, 'evalSelector'], $newSelectors);
1694
  }
1695
  }
@@ -1722,8 +2002,8 @@ class Compiler
1722
  if (\is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
1723
  $p = $this->compileValue($p);
1724
 
1725
- // force re-evaluation
1726
- if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
1727
  $this->shouldEvaluate = true;
1728
  }
1729
  } elseif (
@@ -1741,20 +2021,16 @@ class Compiler
1741
  /**
1742
  * Collapse selectors
1743
  *
1744
- * @param array $selectors
1745
- * @param boolean $selectorFormat
1746
- * if false return a collapsed string
1747
- * if true return an array description of a structured selector
1748
  *
1749
  * @return string
1750
  */
1751
- protected function collapseSelectors($selectors, $selectorFormat = false)
1752
  {
1753
  $parts = [];
1754
 
1755
  foreach ($selectors as $selector) {
1756
  $output = [];
1757
- $glueNext = false;
1758
 
1759
  foreach ($selector as $node) {
1760
  $compound = '';
@@ -1766,48 +2042,71 @@ class Compiler
1766
  }
1767
  );
1768
 
1769
- if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) {
1770
- if (\count($output)) {
1771
- $output[\count($output) - 1] .= ' ' . $compound;
1772
- } else {
1773
- $output[] = $compound;
1774
- }
1775
-
1776
- $glueNext = true;
1777
- } elseif ($glueNext) {
1778
- $output[\count($output) - 1] .= ' ' . $compound;
1779
- $glueNext = false;
1780
- } else {
1781
- $output[] = $compound;
1782
- }
1783
  }
1784
 
1785
- if ($selectorFormat) {
1786
- foreach ($output as &$o) {
1787
- $o = [Type::T_STRING, '', [$o]];
1788
- }
1789
 
1790
- $output = [Type::T_LIST, ' ', $output];
1791
- } else {
1792
- $output = implode(' ', $output);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1793
  }
1794
 
1795
- $parts[] = $output;
1796
- }
 
1797
 
1798
- if ($selectorFormat) {
1799
- $parts = [Type::T_LIST, ',', $parts];
1800
- } else {
1801
- $parts = implode(', ', $parts);
1802
  }
1803
 
1804
- return $parts;
1805
  }
1806
 
1807
  /**
1808
  * Parse down the selector and revert [self] to "&" before a reparsing
1809
  *
1810
- * @param array $selectors
 
1811
  *
1812
  * @return array
1813
  */
@@ -1935,6 +2234,11 @@ class Compiler
1935
  return false;
1936
  }
1937
 
 
 
 
 
 
1938
  protected function pushCallStack($name = '')
1939
  {
1940
  $this->callStack[] = [
@@ -1954,6 +2258,9 @@ class Compiler
1954
  }
1955
  }
1956
 
 
 
 
1957
  protected function popCallStack()
1958
  {
1959
  array_pop($this->callStack);
@@ -1966,7 +2273,7 @@ class Compiler
1966
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1967
  * @param string $traceName
1968
  *
1969
- * @return array|null
1970
  */
1971
  protected function compileChildren($stms, OutputBlock $out, $traceName = '')
1972
  {
@@ -1988,13 +2295,15 @@ class Compiler
1988
  }
1989
 
1990
  /**
1991
- * Compile children and throw exception if unexpected @return
1992
  *
1993
  * @param array $stms
1994
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1995
  * @param \ScssPhp\ScssPhp\Block $selfParent
1996
  * @param string $traceName
1997
  *
 
 
1998
  * @throws \Exception
1999
  */
2000
  protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '')
@@ -2006,7 +2315,7 @@ class Compiler
2006
  $stm[1]->selfParent = $selfParent;
2007
  $ret = $this->compileChild($stm, $out);
2008
  $stm[1]->selfParent = null;
2009
- } elseif ($selfParent && \in_array($stm[0], [TYPE::T_INCLUDE, TYPE::T_EXTEND])) {
2010
  $stm['selfParent'] = $selfParent;
2011
  $ret = $this->compileChild($stm, $out);
2012
  unset($stm['selfParent']);
@@ -2024,7 +2333,7 @@ class Compiler
2024
 
2025
 
2026
  /**
2027
- * evaluate media query : compile internal value keeping the structure inchanged
2028
  *
2029
  * @param array $queryList
2030
  *
@@ -2094,7 +2403,7 @@ class Compiler
2094
  *
2095
  * @param array $queryList
2096
  *
2097
- * @return array
2098
  */
2099
  protected function compileMediaQuery($queryList)
2100
  {
@@ -2332,10 +2641,12 @@ class Compiler
2332
  if ($rawPath[0] === Type::T_STRING) {
2333
  $path = $this->compileStringContent($rawPath);
2334
 
2335
- if (strpos($path, 'url(') !== 0 && $path = $this->findImport($path)) {
2336
- if (! $once || ! \in_array($path, $this->importedFiles)) {
2337
- $this->importFile($path, $out);
2338
- $this->importedFiles[] = $path;
 
 
2339
  }
2340
 
2341
  return true;
@@ -2373,7 +2684,7 @@ class Compiler
2373
  }
2374
 
2375
  /**
2376
- * @param $rawPath
2377
  * @return string
2378
  * @throws CompilerException
2379
  */
@@ -2381,12 +2692,12 @@ class Compiler
2381
  {
2382
  $path = $this->compileValue($rawPath);
2383
 
2384
- // case url() without quotes : supress \r \n remaining in the path
2385
  // if this is a real string there can not be CR or LF char
2386
  if (strpos($path, 'url(') === 0) {
2387
  $path = str_replace(array("\r", "\n"), array('', ' '), $path);
2388
  } else {
2389
- // if this is a file name in a string, spaces shoudl be escaped
2390
  $path = $this->reduce($rawPath);
2391
  $path = $this->escapeImportPathString($path);
2392
  $path = $this->compileValue($path);
@@ -2424,9 +2735,11 @@ class Compiler
2424
  * Append a root directive like @import or @charset as near as the possible from the source code
2425
  * (keeping before comments, @import and @charset coming before in the source code)
2426
  *
2427
- * @param string $line
2428
- * @param @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2429
- * @param array $allowed
 
 
2430
  */
2431
  protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT])
2432
  {
@@ -2474,7 +2787,9 @@ class Compiler
2474
  *
2475
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2476
  * @param string $type
2477
- * @param string|mixed $line
 
 
2478
  */
2479
  protected function appendOutputLine(OutputBlock $out, $type, $line)
2480
  {
@@ -2509,7 +2824,7 @@ class Compiler
2509
  * @param array $child
2510
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2511
  *
2512
- * @return array
2513
  */
2514
  protected function compileChild($child, OutputBlock $out)
2515
  {
@@ -2523,12 +2838,13 @@ class Compiler
2523
  $this->sourceColumn = $child[1]->sourceColumn;
2524
  } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
2525
  $this->sourceLine = $out->sourceLine;
2526
- $this->sourceIndex = array_search($out->sourceName, $this->sourceNames);
2527
  $this->sourceColumn = $out->sourceColumn;
2528
 
2529
- if ($this->sourceIndex === false) {
2530
- $this->sourceIndex = null;
2531
  }
 
2532
  }
2533
 
2534
  switch ($child[0]) {
@@ -2653,7 +2969,7 @@ class Compiler
2653
  $divider = $this->reduce($divider, true);
2654
  }
2655
 
2656
- if (\intval($divider->dimension) && ! \count($divider->units)) {
2657
  $revert = false;
2658
  }
2659
  }
@@ -2677,7 +2993,7 @@ class Compiler
2677
  $divider = $this->reduce($divider, true);
2678
  }
2679
 
2680
- if (\intval($divider->dimension) && ! \count($divider->units)) {
2681
  $revert = false;
2682
  }
2683
  }
@@ -2735,12 +3051,21 @@ class Compiler
2735
 
2736
  case Type::T_EXTEND:
2737
  foreach ($child[1] as $sel) {
2738
- $sel = $this->replaceSelfSelector($sel);
 
 
 
 
 
2739
  $results = $this->evalSelectors([$sel]);
2740
 
2741
  foreach ($results as $result) {
 
 
 
 
2742
  // only use the first one
2743
- $result = current($result);
2744
  $selectors = $out->selectors;
2745
 
2746
  if (! $selectors && isset($child['selfParent'])) {
@@ -2790,17 +3115,11 @@ class Compiler
2790
  $ret = $this->compileChildren($each->children, $out);
2791
 
2792
  if ($ret) {
2793
- if ($ret[0] !== Type::T_CONTROL) {
2794
- $store = $this->env->store;
2795
- $this->popEnv();
2796
- $this->backPropagateEnv($store, $each->vars);
2797
-
2798
- return $ret;
2799
- }
2800
 
2801
- if ($ret[1]) {
2802
- break;
2803
- }
2804
  }
2805
  }
2806
  $store = $this->env->store;
@@ -2816,13 +3135,7 @@ class Compiler
2816
  $ret = $this->compileChildren($while->children, $out);
2817
 
2818
  if ($ret) {
2819
- if ($ret[0] !== Type::T_CONTROL) {
2820
- return $ret;
2821
- }
2822
-
2823
- if ($ret[1]) {
2824
- break;
2825
- }
2826
  }
2827
  }
2828
  break;
@@ -2830,24 +3143,15 @@ class Compiler
2830
  case Type::T_FOR:
2831
  list(, $for) = $child;
2832
 
2833
- $start = $this->reduce($for->start, true);
2834
- $end = $this->reduce($for->end, true);
2835
-
2836
- if (! $start instanceof Node\Number) {
2837
- throw $this->error('%s is not a number', $start[0]);
2838
- }
2839
 
2840
- if (! $end instanceof Node\Number) {
2841
- throw $this->error('%s is not a number', $end[0]);
2842
- }
2843
 
2844
- if (! ($start[2] == $end[2] || $end->unitless())) {
2845
- throw $this->error('Incompatible units: "%s" && "%s".', $start->unitStr(), $end->unitStr());
2846
- }
2847
 
2848
- $unit = $start[2];
2849
- $start = $start[1];
2850
- $end = $end[1];
2851
 
2852
  $d = $start < $end ? 1 : -1;
2853
 
@@ -2861,23 +3165,17 @@ class Compiler
2861
  break;
2862
  }
2863
 
2864
- $this->set($for->var, new Node\Number($start, $unit));
2865
  $start += $d;
2866
 
2867
  $ret = $this->compileChildren($for->children, $out);
2868
 
2869
  if ($ret) {
2870
- if ($ret[0] !== Type::T_CONTROL) {
2871
- $store = $this->env->store;
2872
- $this->popEnv();
2873
- $this->backPropagateEnv($store, [$for->var]);
2874
-
2875
- return $ret;
2876
- }
2877
 
2878
- if ($ret[1]) {
2879
- break;
2880
- }
2881
  }
2882
  }
2883
 
@@ -2887,12 +3185,6 @@ class Compiler
2887
 
2888
  break;
2889
 
2890
- case Type::T_BREAK:
2891
- return [Type::T_CONTROL, true];
2892
-
2893
- case Type::T_CONTINUE:
2894
- return [Type::T_CONTROL, false];
2895
-
2896
  case Type::T_RETURN:
2897
  return $this->reduce($child[1], true);
2898
 
@@ -3007,35 +3299,32 @@ class Compiler
3007
  case Type::T_DEBUG:
3008
  list(, $value) = $child;
3009
 
3010
- $fname = $this->sourceNames[$this->sourceIndex];
3011
  $line = $this->sourceLine;
3012
  $value = $this->compileDebugValue($value);
3013
 
3014
- fwrite($this->stderr, "$fname:$line DEBUG: $value\n");
3015
  break;
3016
 
3017
  case Type::T_WARN:
3018
  list(, $value) = $child;
3019
 
3020
- $fname = $this->sourceNames[$this->sourceIndex];
3021
  $line = $this->sourceLine;
3022
  $value = $this->compileDebugValue($value);
3023
 
3024
- fwrite($this->stderr, "WARNING: $value\n on line $line of $fname\n\n");
3025
  break;
3026
 
3027
  case Type::T_ERROR:
3028
  list(, $value) = $child;
3029
 
3030
- $fname = $this->sourceNames[$this->sourceIndex];
3031
  $line = $this->sourceLine;
3032
  $value = $this->compileValue($this->reduce($value, true));
3033
 
3034
  throw $this->error("File $fname on line $line ERROR: $value\n");
3035
 
3036
- case Type::T_CONTROL:
3037
- throw $this->error('@break/@continue not permitted in this scope');
3038
-
3039
  default:
3040
  throw $this->error("unknown child type: $child[0]");
3041
  }
@@ -3045,7 +3334,7 @@ class Compiler
3045
  * Reduce expression to string
3046
  *
3047
  * @param array $exp
3048
- * @param true $keepParens
3049
  *
3050
  * @return array
3051
  */
@@ -3083,11 +3372,11 @@ class Compiler
3083
  /**
3084
  * Is truthy?
3085
  *
3086
- * @param array $value
3087
  *
3088
  * @return boolean
3089
  */
3090
- protected function isTruthy($value)
3091
  {
3092
  return $value !== static::$false && $value !== static::$null;
3093
  }
@@ -3131,15 +3420,15 @@ class Compiler
3131
  /**
3132
  * Reduce value
3133
  *
3134
- * @param array $value
3135
  * @param boolean $inExp
3136
  *
3137
- * @return null|string|array|\ScssPhp\ScssPhp\Node\Number
3138
  */
3139
  protected function reduce($value, $inExp = false)
3140
  {
3141
- if (\is_null($value)) {
3142
- return null;
3143
  }
3144
 
3145
  switch ($value[0]) {
@@ -3157,8 +3446,8 @@ class Compiler
3157
 
3158
  // special case: looks like css shorthand
3159
  if (
3160
- $opName == 'div' && ! $inParens && ! $inExp && isset($right[2]) &&
3161
- (($right[0] !== Type::T_NUMBER && $right[2] != '') ||
3162
  ($right[0] === Type::T_NUMBER && ! $right->unitless()))
3163
  ) {
3164
  return $this->expToString($value);
@@ -3188,50 +3477,6 @@ class Compiler
3188
  \is_callable([$this, $fn]) &&
3189
  $genOp = true)
3190
  ) {
3191
- $coerceUnit = false;
3192
-
3193
- if (
3194
- ! isset($genOp) &&
3195
- $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER
3196
- ) {
3197
- $coerceUnit = true;
3198
-
3199
- switch ($opName) {
3200
- case 'mul':
3201
- $targetUnit = $left[2];
3202
-
3203
- foreach ($right[2] as $unit => $exp) {
3204
- $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp;
3205
- }
3206
- break;
3207
-
3208
- case 'div':
3209
- $targetUnit = $left[2];
3210
-
3211
- foreach ($right[2] as $unit => $exp) {
3212
- $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) - $exp;
3213
- }
3214
- break;
3215
-
3216
- case 'mod':
3217
- $targetUnit = $left[2];
3218
- break;
3219
-
3220
- default:
3221
- $targetUnit = $left->unitless() ? $right[2] : $left[2];
3222
- }
3223
-
3224
- $baseUnitLeft = $left->isNormalizable();
3225
- $baseUnitRight = $right->isNormalizable();
3226
-
3227
- if ($baseUnitLeft && $baseUnitRight && $baseUnitLeft === $baseUnitRight) {
3228
- $left = $left->normalize();
3229
- $right = $right->normalize();
3230
- } elseif ($coerceUnit) {
3231
- $left = new Node\Number($left[1], []);
3232
- }
3233
- }
3234
-
3235
  $shouldEval = $inParens || $inExp;
3236
 
3237
  if (isset($passOp)) {
@@ -3241,10 +3486,6 @@ class Compiler
3241
  }
3242
 
3243
  if (isset($out)) {
3244
- if ($coerceUnit && $out[0] === Type::T_NUMBER) {
3245
- $out = $out->coerce($targetUnit);
3246
- }
3247
-
3248
  return $out;
3249
  }
3250
  }
@@ -3257,13 +3498,13 @@ class Compiler
3257
  $inExp = $inExp || $this->shouldEval($exp);
3258
  $exp = $this->reduce($exp);
3259
 
3260
- if ($exp[0] === Type::T_NUMBER) {
3261
  switch ($op) {
3262
  case '+':
3263
- return new Node\Number($exp[1], $exp[2]);
3264
 
3265
  case '-':
3266
- return new Node\Number(-$exp[1], $exp[2]);
3267
  }
3268
  }
3269
 
@@ -3288,6 +3529,14 @@ class Compiler
3288
  foreach ($value[2] as &$item) {
3289
  $item = $this->reduce($item);
3290
  }
 
 
 
 
 
 
 
 
3291
 
3292
  return $value;
3293
 
@@ -3304,7 +3553,7 @@ class Compiler
3304
 
3305
  case Type::T_STRING:
3306
  foreach ($value[2] as &$item) {
3307
- if (\is_array($item) || $item instanceof \ArrayAccess) {
3308
  $item = $this->reduce($item);
3309
  }
3310
  }
@@ -3315,7 +3564,7 @@ class Compiler
3315
  $value[1] = $this->reduce($value[1]);
3316
 
3317
  if ($inExp) {
3318
- return $value[1];
3319
  }
3320
 
3321
  return $value;
@@ -3326,7 +3575,7 @@ class Compiler
3326
  case Type::T_SELF:
3327
  $selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null;
3328
  $selfSelector = $this->multiplySelectors($this->env, $selfParent);
3329
- $selfSelector = $this->collapseSelectors($selfSelector, true);
3330
 
3331
  return $selfSelector;
3332
 
@@ -3338,10 +3587,10 @@ class Compiler
3338
  /**
3339
  * Function caller
3340
  *
3341
- * @param string $name
3342
- * @param array $argValues
3343
  *
3344
- * @return array|null
3345
  */
3346
  protected function fncall($functionReference, $argValues)
3347
  {
@@ -3386,7 +3635,7 @@ class Compiler
3386
 
3387
  // special cases of css valid functions min/max
3388
  $name = strtolower($name);
3389
- if (\in_array($name, ['min', 'max'])) {
3390
  $cssFunction = $this->cssValidArg(
3391
  [Type::T_FUNCTION_CALL, $name, $argValues],
3392
  ['min', 'max', 'calc', 'env', 'var']
@@ -3408,8 +3657,19 @@ class Compiler
3408
  }
3409
  }
3410
 
 
 
 
 
 
 
 
3411
  protected function cssValidArg($arg, $allowed_function = [], $inFunction = false)
3412
  {
 
 
 
 
3413
  switch ($arg[0]) {
3414
  case Type::T_INTERPOLATE:
3415
  return [Type::T_KEYWORD, $this->CompileValue($arg)];
@@ -3454,9 +3714,6 @@ class Compiler
3454
  }
3455
  return $this->stringifyFncallArgs($arg);
3456
 
3457
- case Type::T_NUMBER:
3458
- return $this->stringifyFncallArgs($arg);
3459
-
3460
  case Type::T_LIST:
3461
  if (!$inFunction) {
3462
  return false;
@@ -3494,11 +3751,16 @@ class Compiler
3494
 
3495
  /**
3496
  * Reformat fncall arguments to proper css function output
3497
- * @param $arg
3498
- * @return array|\ArrayAccess|Node\Number|string|null
 
 
3499
  */
3500
  protected function stringifyFncallArgs($arg)
3501
  {
 
 
 
3502
 
3503
  switch ($arg[0]) {
3504
  case Type::T_LIST:
@@ -3563,6 +3825,21 @@ class Compiler
3563
  $libName = $f[1];
3564
  $prototype = isset(static::$$libName) ? static::$$libName : null;
3565
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3566
  return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype];
3567
  }
3568
 
@@ -3585,14 +3862,20 @@ class Compiler
3585
  /**
3586
  * Normalize value
3587
  *
3588
- * @param array $value
3589
  *
3590
- * @return array
 
 
3591
  */
3592
  public function normalizeValue($value)
3593
  {
3594
  $value = $this->coerceForExpression($this->reduce($value));
3595
 
 
 
 
 
3596
  switch ($value[0]) {
3597
  case Type::T_LIST:
3598
  $value = $this->extractInterpolation($value);
@@ -3614,9 +3897,6 @@ class Compiler
3614
  case Type::T_STRING:
3615
  return [$value[0], '"', [$this->compileStringContent($value)]];
3616
 
3617
- case Type::T_NUMBER:
3618
- return $value->normalize();
3619
-
3620
  case Type::T_INTERPOLATE:
3621
  return [Type::T_KEYWORD, $this->compileValue($value)];
3622
 
@@ -3628,74 +3908,66 @@ class Compiler
3628
  /**
3629
  * Add numbers
3630
  *
3631
- * @param array $left
3632
- * @param array $right
3633
  *
3634
- * @return \ScssPhp\ScssPhp\Node\Number
3635
  */
3636
- protected function opAddNumberNumber($left, $right)
3637
  {
3638
- return new Node\Number($left[1] + $right[1], $left[2]);
3639
  }
3640
 
3641
  /**
3642
  * Multiply numbers
3643
  *
3644
- * @param array $left
3645
- * @param array $right
3646
  *
3647
- * @return \ScssPhp\ScssPhp\Node\Number
3648
  */
3649
- protected function opMulNumberNumber($left, $right)
3650
  {
3651
- return new Node\Number($left[1] * $right[1], $left[2]);
3652
  }
3653
 
3654
  /**
3655
  * Subtract numbers
3656
  *
3657
- * @param array $left
3658
- * @param array $right
3659
  *
3660
- * @return \ScssPhp\ScssPhp\Node\Number
3661
  */
3662
- protected function opSubNumberNumber($left, $right)
3663
  {
3664
- return new Node\Number($left[1] - $right[1], $left[2]);
3665
  }
3666
 
3667
  /**
3668
  * Divide numbers
3669
  *
3670
- * @param array $left
3671
- * @param array $right
3672
  *
3673
- * @return array|\ScssPhp\ScssPhp\Node\Number
3674
  */
3675
- protected function opDivNumberNumber($left, $right)
3676
  {
3677
- if ($right[1] == 0) {
3678
- return ($left[1] == 0) ? static::$NaN : static::$Infinity;
3679
- }
3680
-
3681
- return new Node\Number($left[1] / $right[1], $left[2]);
3682
  }
3683
 
3684
  /**
3685
  * Mod numbers
3686
  *
3687
- * @param array $left
3688
- * @param array $right
3689
  *
3690
- * @return \ScssPhp\ScssPhp\Node\Number
3691
  */
3692
- protected function opModNumberNumber($left, $right)
3693
  {
3694
- if ($right[1] == 0) {
3695
- return static::$NaN;
3696
- }
3697
-
3698
- return new Node\Number($left[1] % $right[1], $left[2]);
3699
  }
3700
 
3701
  /**
@@ -3734,11 +4006,11 @@ class Compiler
3734
  /**
3735
  * Boolean and
3736
  *
3737
- * @param array $left
3738
- * @param array $right
3739
  * @param boolean $shouldEval
3740
  *
3741
- * @return array|null
3742
  */
3743
  protected function opAnd($left, $right, $shouldEval)
3744
  {
@@ -3762,11 +4034,11 @@ class Compiler
3762
  /**
3763
  * Boolean or
3764
  *
3765
- * @param array $left
3766
- * @param array $right
3767
  * @param boolean $shouldEval
3768
  *
3769
- * @return array|null
3770
  */
3771
  protected function opOr($left, $right, $shouldEval)
3772
  {
@@ -3798,6 +4070,15 @@ class Compiler
3798
  */
3799
  protected function opColorColor($op, $left, $right)
3800
  {
 
 
 
 
 
 
 
 
 
3801
  $out = [Type::T_COLOR];
3802
 
3803
  foreach ([1, 2, 3] as $i) {
@@ -3858,13 +4139,21 @@ class Compiler
3858
  *
3859
  * @param string $op
3860
  * @param array $left
3861
- * @param array $right
3862
  *
3863
  * @return array
3864
  */
3865
- protected function opColorNumber($op, $left, $right)
3866
  {
3867
- $value = $right[1];
 
 
 
 
 
 
 
 
3868
 
3869
  return $this->opColorColor(
3870
  $op,
@@ -3877,14 +4166,22 @@ class Compiler
3877
  * Compare number and color
3878
  *
3879
  * @param string $op
3880
- * @param array $left
3881
  * @param array $right
3882
  *
3883
  * @return array
3884
  */
3885
- protected function opNumberColor($op, $left, $right)
3886
  {
3887
- $value = $left[1];
 
 
 
 
 
 
 
 
3888
 
3889
  return $this->opColorColor(
3890
  $op,
@@ -3896,8 +4193,8 @@ class Compiler
3896
  /**
3897
  * Compare number1 == number2
3898
  *
3899
- * @param array $left
3900
- * @param array $right
3901
  *
3902
  * @return array
3903
  */
@@ -3917,8 +4214,8 @@ class Compiler
3917
  /**
3918
  * Compare number1 != number2
3919
  *
3920
- * @param array $left
3921
- * @param array $right
3922
  *
3923
  * @return array
3924
  */
@@ -3936,70 +4233,81 @@ class Compiler
3936
  }
3937
 
3938
  /**
3939
- * Compare number1 >= number2
3940
  *
3941
- * @param array $left
3942
- * @param array $right
3943
  *
3944
  * @return array
3945
  */
3946
- protected function opGteNumberNumber($left, $right)
3947
  {
3948
- return $this->toBool($left[1] >= $right[1]);
3949
  }
3950
 
3951
  /**
3952
- * Compare number1 > number2
3953
  *
3954
- * @param array $left
3955
- * @param array $right
3956
  *
3957
  * @return array
3958
  */
3959
- protected function opGtNumberNumber($left, $right)
3960
  {
3961
- return $this->toBool($left[1] > $right[1]);
3962
  }
3963
 
3964
  /**
3965
- * Compare number1 <= number2
3966
  *
3967
- * @param array $left
3968
- * @param array $right
3969
  *
3970
  * @return array
3971
  */
3972
- protected function opLteNumberNumber($left, $right)
3973
  {
3974
- return $this->toBool($left[1] <= $right[1]);
3975
  }
3976
 
3977
  /**
3978
- * Compare number1 < number2
3979
  *
3980
- * @param array $left
3981
- * @param array $right
3982
  *
3983
  * @return array
3984
  */
3985
- protected function opLtNumberNumber($left, $right)
3986
  {
3987
- return $this->toBool($left[1] < $right[1]);
3988
  }
3989
 
3990
  /**
3991
- * Three-way comparison, aka spaceship operator
3992
  *
3993
- * @param array $left
3994
- * @param array $right
3995
  *
3996
- * @return \ScssPhp\ScssPhp\Node\Number
3997
  */
3998
- protected function opCmpNumberNumber($left, $right)
3999
  {
4000
- $n = $left[1] - $right[1];
 
4001
 
4002
- return new Node\Number($n ? $n / abs($n) : 0, '');
 
 
 
 
 
 
 
 
 
 
4003
  }
4004
 
4005
  /**
@@ -4007,7 +4315,7 @@ class Compiler
4007
  *
4008
  * @api
4009
  *
4010
- * @param mixed $thing
4011
  *
4012
  * @return array
4013
  */
@@ -4016,6 +4324,53 @@ class Compiler
4016
  return $thing ? static::$true : static::$false;
4017
  }
4018
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4019
  /**
4020
  * Compiles a primitive value into a CSS property value.
4021
  *
@@ -4029,17 +4384,22 @@ class Compiler
4029
  *
4030
  * @api
4031
  *
4032
- * @param array $value
 
4033
  *
4034
- * @return string|array
4035
  */
4036
- public function compileValue($value)
4037
  {
4038
  $value = $this->reduce($value);
4039
 
 
 
 
 
4040
  switch ($value[0]) {
4041
  case Type::T_KEYWORD:
4042
- return $value[1];
4043
 
4044
  case Type::T_COLOR:
4045
  // [1] - red component (either number for a %)
@@ -4063,7 +4423,7 @@ class Compiler
4063
  }
4064
 
4065
  if (is_numeric($alpha)) {
4066
- $a = new Node\Number($alpha, '');
4067
  } else {
4068
  $a = $alpha;
4069
  }
@@ -4091,32 +4451,35 @@ class Compiler
4091
 
4092
  return $h;
4093
 
4094
- case Type::T_NUMBER:
4095
- return $value->output($this);
4096
-
4097
  case Type::T_STRING:
4098
- $content = $this->compileStringContent($value);
 
 
 
 
 
4099
 
4100
- if ($value[1]) {
4101
  // force double quote as string quote for the output in certain cases
4102
  if (
4103
  $value[1] === "'" &&
4104
- strpos($content, '"') === false &&
4105
- strpbrk($content, '{}') !== false
4106
  ) {
4107
  $value[1] = '"';
 
 
 
 
 
4108
  }
4109
- $content = str_replace(
4110
- array('\\a', "\n", "\f" , '\\' , "\r" , $value[1]),
4111
- array("\r" , ' ' , '\\f', '\\\\', '\\a', '\\' . $value[1]),
4112
- $content
4113
- );
4114
  }
4115
 
4116
  return $value[1] . $content . $value[1];
4117
 
4118
  case Type::T_FUNCTION:
4119
- $args = ! empty($value[2]) ? $this->compileValue($value[2]) : '';
4120
 
4121
  return "$value[1]($args)";
4122
 
@@ -4129,7 +4492,7 @@ class Compiler
4129
  $value = $this->extractInterpolation($value);
4130
 
4131
  if ($value[0] !== Type::T_LIST) {
4132
- return $this->compileValue($value);
4133
  }
4134
 
4135
  list(, $delim, $items) = $value;
@@ -4161,12 +4524,28 @@ class Compiler
4161
 
4162
  $filtered = [];
4163
 
 
4164
  foreach ($items as $item) {
 
 
 
 
 
 
 
 
 
 
 
 
4165
  if ($item[0] === Type::T_NULL) {
4166
  continue;
4167
  }
 
 
 
4168
 
4169
- $compiled = $this->compileValue($item);
4170
 
4171
  if ($prefix_value && \strlen($compiled)) {
4172
  $compiled = $prefix_value . $compiled;
@@ -4183,7 +4562,7 @@ class Compiler
4183
  $filtered = [];
4184
 
4185
  for ($i = 0, $s = \count($keys); $i < $s; $i++) {
4186
- $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
4187
  }
4188
 
4189
  array_walk($filtered, function (&$value, $key) {
@@ -4204,7 +4583,7 @@ class Compiler
4204
  }
4205
 
4206
  $left = \count($left[2]) > 0
4207
- ? $this->compileValue($left) . $delim . $whiteLeft
4208
  : '';
4209
 
4210
  $delim = $right[1];
@@ -4214,14 +4593,18 @@ class Compiler
4214
  }
4215
 
4216
  $right = \count($right[2]) > 0 ?
4217
- $whiteRight . $delim . $this->compileValue($right) : '';
4218
 
4219
- return $left . $this->compileValue($interpolate) . $right;
4220
 
4221
  case Type::T_INTERPOLATE:
4222
  // strip quotes if it's a string
4223
  $reduced = $this->reduce($value[1]);
4224
 
 
 
 
 
4225
  switch ($reduced[0]) {
4226
  case Type::T_LIST:
4227
  $reduced = $this->extractInterpolation($reduced);
@@ -4243,14 +4626,12 @@ class Compiler
4243
  continue;
4244
  }
4245
 
4246
- $temp = $this->compileValue([Type::T_KEYWORD, $item]);
4247
-
4248
- if ($temp[0] === Type::T_STRING) {
4249
- $filtered[] = $this->compileStringContent($temp);
4250
- } elseif ($temp[0] === Type::T_KEYWORD) {
4251
- $filtered[] = $temp[1];
4252
  } else {
4253
- $filtered[] = $this->compileValue($temp);
4254
  }
4255
  }
4256
 
@@ -4258,14 +4639,14 @@ class Compiler
4258
  break;
4259
 
4260
  case Type::T_STRING:
4261
- $reduced = [Type::T_KEYWORD, $this->compileStringContent($reduced)];
4262
  break;
4263
 
4264
  case Type::T_NULL:
4265
  $reduced = [Type::T_KEYWORD, ''];
4266
  }
4267
 
4268
- return $this->compileValue($reduced);
4269
 
4270
  case Type::T_NULL:
4271
  return 'null';
@@ -4279,14 +4660,18 @@ class Compiler
4279
  }
4280
 
4281
  /**
4282
- * @param array $value
4283
  *
4284
- * @return array|string
4285
  */
4286
  protected function compileDebugValue($value)
4287
  {
4288
  $value = $this->reduce($value, true);
4289
 
 
 
 
 
4290
  switch ($value[0]) {
4291
  case Type::T_STRING:
4292
  return $this->compileStringContent($value);
@@ -4302,28 +4687,52 @@ class Compiler
4302
  * @param array $list
4303
  *
4304
  * @return string
 
 
4305
  */
4306
  protected function flattenList($list)
4307
  {
 
 
4308
  return $this->compileValue($list);
4309
  }
4310
 
4311
  /**
4312
- * Compile string content
4313
  *
4314
- * @param array $string
 
 
 
4315
  *
4316
  * @return string
4317
  */
4318
- protected function compileStringContent($string)
4319
  {
4320
- $parts = [];
 
 
4321
 
4322
- foreach ($string[2] as $part) {
4323
- if (\is_array($part) || $part instanceof \ArrayAccess) {
4324
- $parts[] = $this->compileValue($part);
4325
- } else {
4326
- $parts[] = $part;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4327
  }
4328
  }
4329
 
@@ -4533,9 +4942,11 @@ class Compiler
4533
  /**
4534
  * Convert env linked list to stack
4535
  *
4536
- * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4537
  *
4538
- * @return array
 
 
4539
  */
4540
  protected function compactEnv(Environment $env)
4541
  {
@@ -4549,9 +4960,11 @@ class Compiler
4549
  /**
4550
  * Convert env stack to singly linked list
4551
  *
4552
- * @param array $envs
4553
  *
4554
- * @return \ScssPhp\ScssPhp\Compiler\Environment
 
 
4555
  */
4556
  protected function extractEnv($envs)
4557
  {
@@ -4587,6 +5000,8 @@ class Compiler
4587
 
4588
  /**
4589
  * Pop environment
 
 
4590
  */
4591
  protected function popEnv()
4592
  {
@@ -4597,8 +5012,10 @@ class Compiler
4597
  /**
4598
  * Propagate vars from a just poped Env (used in @each and @for)
4599
  *
4600
- * @param array $store
4601
- * @param null|array $excludedVars
 
 
4602
  */
4603
  protected function backPropagateEnv($store, $excludedVars = null)
4604
  {
@@ -4627,6 +5044,8 @@ class Compiler
4627
  * @param boolean $shadow
4628
  * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4629
  * @param mixed $valueUnreduced
 
 
4630
  */
4631
  protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null)
4632
  {
@@ -4650,6 +5069,8 @@ class Compiler
4650
  * @param mixed $value
4651
  * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4652
  * @param mixed $valueUnreduced
 
 
4653
  */
4654
  protected function setExisting($name, $value, Environment $env, $valueUnreduced = null)
4655
  {
@@ -4708,6 +5129,8 @@ class Compiler
4708
  * @param mixed $value
4709
  * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4710
  * @param mixed $valueUnreduced
 
 
4711
  */
4712
  protected function setRaw($name, $value, Environment $env, $valueUnreduced = null)
4713
  {
@@ -4721,7 +5144,7 @@ class Compiler
4721
  /**
4722
  * Get variable
4723
  *
4724
- * @api
4725
  *
4726
  * @param string $name
4727
  * @param boolean $shouldThrow
@@ -4804,6 +5227,8 @@ class Compiler
4804
  * Inject variables
4805
  *
4806
  * @param array $args
 
 
4807
  */
4808
  protected function injectVariables(array $args)
4809
  {
@@ -4818,7 +5243,7 @@ class Compiler
4818
  $name = substr($name, 1);
4819
  }
4820
 
4821
- if (! $parser->parseValue($strValue, $value)) {
4822
  $value = $this->coerceValue($strValue);
4823
  }
4824
 
@@ -4826,16 +5251,59 @@ class Compiler
4826
  }
4827
  }
4828
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4829
  /**
4830
  * Set variables
4831
  *
4832
  * @api
4833
  *
4834
  * @param array $variables
 
 
 
 
4835
  */
4836
  public function setVariables(array $variables)
4837
  {
4838
- $this->registeredVars = array_merge($this->registeredVars, $variables);
 
 
4839
  }
4840
 
4841
  /**
@@ -4844,6 +5312,8 @@ class Compiler
4844
  * @api
4845
  *
4846
  * @param string $name
 
 
4847
  */
4848
  public function unsetVariable($name)
4849
  {
@@ -4865,13 +5335,15 @@ class Compiler
4865
  /**
4866
  * Adds to list of parsed files
4867
  *
4868
- * @api
4869
  *
4870
- * @param string $path
 
 
4871
  */
4872
  public function addParsedFile($path)
4873
  {
4874
- if (isset($path) && is_file($path)) {
4875
  $this->parsedFiles[realpath($path)] = filemtime($path);
4876
  }
4877
  }
@@ -4879,12 +5351,12 @@ class Compiler
4879
  /**
4880
  * Returns list of parsed files
4881
  *
4882
- * @api
4883
- *
4884
- * @return array
4885
  */
4886
  public function getParsedFiles()
4887
  {
 
4888
  return $this->parsedFiles;
4889
  }
4890
 
@@ -4894,6 +5366,8 @@ class Compiler
4894
  * @api
4895
  *
4896
  * @param string|callable $path
 
 
4897
  */
4898
  public function addImportPath($path)
4899
  {
@@ -4907,11 +5381,24 @@ class Compiler
4907
  *
4908
  * @api
4909
  *
4910
- * @param string|array $path
 
 
4911
  */
4912
  public function setImportPaths($path)
4913
  {
4914
- $this->importPaths = (array) $path;
 
 
 
 
 
 
 
 
 
 
 
4915
  }
4916
 
4917
  /**
@@ -4921,6 +5408,8 @@ class Compiler
4921
  *
4922
  * @param integer $numberPrecision
4923
  *
 
 
4924
  * @deprecated The number precision is not configurable anymore. The default is enough for all browsers.
4925
  */
4926
  public function setNumberPrecision($numberPrecision)
@@ -4929,15 +5418,51 @@ class Compiler
4929
  . 'The default is enough for all browsers.', E_USER_DEPRECATED);
4930
  }
4931
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4932
  /**
4933
  * Set formatter
4934
  *
4935
  * @api
4936
  *
4937
  * @param string $formatterName
 
 
 
 
4938
  */
4939
  public function setFormatter($formatterName)
4940
  {
 
 
 
 
 
4941
  $this->formatter = $formatterName;
4942
  }
4943
 
@@ -4947,10 +5472,15 @@ class Compiler
4947
  * @api
4948
  *
4949
  * @param string $lineNumberStyle
 
 
 
 
4950
  */
4951
  public function setLineNumberStyle($lineNumberStyle)
4952
  {
4953
- $this->lineNumberStyle = $lineNumberStyle;
 
4954
  }
4955
 
4956
  /**
@@ -4959,6 +5489,10 @@ class Compiler
4959
  * @api
4960
  *
4961
  * @param integer $sourceMap
 
 
 
 
4962
  */
4963
  public function setSourceMap($sourceMap)
4964
  {
@@ -4971,6 +5505,10 @@ class Compiler
4971
  * @api
4972
  *
4973
  * @param array $sourceMapOptions
 
 
 
 
4974
  */
4975
  public function setSourceMapOptions($sourceMapOptions)
4976
  {
@@ -4982,13 +5520,23 @@ class Compiler
4982
  *
4983
  * @api
4984
  *
4985
- * @param string $name
4986
- * @param callable $func
4987
- * @param array $prototype
 
 
4988
  */
4989
- public function registerFunction($name, $func, $prototype = null)
4990
  {
4991
- $this->userFunctions[$this->normalizeName($name)] = [$func, $prototype];
 
 
 
 
 
 
 
 
4992
  }
4993
 
4994
  /**
@@ -4997,6 +5545,8 @@ class Compiler
4997
  * @api
4998
  *
4999
  * @param string $name
 
 
5000
  */
5001
  public function unregisterFunction($name)
5002
  {
@@ -5009,9 +5559,15 @@ class Compiler
5009
  * @api
5010
  *
5011
  * @param string $name
 
 
 
 
5012
  */
5013
  public function addFeature($name)
5014
  {
 
 
5015
  $this->registeredFeatures[$name] = true;
5016
  }
5017
 
@@ -5020,13 +5576,24 @@ class Compiler
5020
  *
5021
  * @param string $path
5022
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
 
 
5023
  */
5024
  protected function importFile($path, OutputBlock $out)
5025
  {
5026
- $this->pushCallStack('import ' . $path);
5027
  // see if tree is cached
5028
  $realPath = realpath($path);
5029
 
 
 
 
 
 
 
 
 
 
5030
  if (isset($this->importCache[$realPath])) {
5031
  $this->handleImportLoop($realPath);
5032
 
@@ -5039,78 +5606,109 @@ class Compiler
5039
  $this->importCache[$realPath] = $tree;
5040
  }
5041
 
5042
- $pi = pathinfo($path);
 
5043
 
5044
- array_unshift($this->importPaths, $pi['dirname']);
5045
  $this->compileChildrenNoReturn($tree->children, $out);
5046
- array_shift($this->importPaths);
5047
  $this->popCallStack();
5048
  }
5049
 
5050
  /**
5051
- * Return the file path for an import url if it exists
5052
  *
5053
- * @api
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5054
  *
5055
  * @param string $url
5056
  *
5057
- * @return string|null
5058
  */
5059
- public function findImport($url)
5060
  {
5061
- $urls = [];
 
5062
 
5063
- $hasExtension = preg_match('/[.]s?css$/', $url);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5064
 
5065
- // for "normal" scss imports (ignore vanilla css and external requests)
5066
- if (! preg_match('~\.css$|^https?://|^//~', $url)) {
5067
- $isPartial = (strpos(basename($url), '_') === 0);
5068
 
5069
- // try both normal and the _partial filename
5070
- $urls = [$url . ($hasExtension ? '' : '.scss')];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5071
 
5072
- if (! $isPartial) {
5073
- $urls[] = preg_replace('~[^/]+$~', '_\0', $url) . ($hasExtension ? '' : '.scss');
 
5074
  }
 
 
5075
 
5076
- if (! $hasExtension) {
5077
- $urls[] = "$url/index.scss";
5078
- // allow to find a plain css file, *if* no scss or partial scss is found
5079
- $urls[] .= $url . '.css';
 
5080
  }
5081
  }
5082
 
5083
  foreach ($this->importPaths as $dir) {
5084
  if (\is_string($dir)) {
5085
- // check urls for normal import paths
5086
- foreach ($urls as $full) {
5087
- $found = [];
5088
- $separator = (
5089
- ! empty($dir) &&
5090
- substr($dir, -1) !== '/' &&
5091
- substr($full, 0, 1) !== '/'
5092
- ) ? '/' : '';
5093
- $full = $dir . $separator . $full;
5094
-
5095
- if (is_file($file = $full)) {
5096
- $found[] = $file;
5097
- }
5098
- if (! $isPartial) {
5099
- $full = dirname($full) . '/_' . basename($full);
5100
- if (is_file($file = $full)) {
5101
- $found[] = $file;
5102
- }
5103
- }
5104
- if ($found) {
5105
- if (\count($found) === 1) {
5106
- return reset($found);
5107
- }
5108
- if (\count($found) > 1) {
5109
- throw $this->error(
5110
- "Error: It's not clear which file to import. Found: " . implode(', ', $found)
5111
- );
5112
- }
5113
- }
5114
  }
5115
  } elseif (\is_callable($dir)) {
5116
  // check custom callback for import path
@@ -5122,13 +5720,147 @@ class Compiler
5122
  }
5123
  }
5124
 
5125
- if ($urls) {
5126
- if (! $hasExtension || preg_match('/[.]scss$/', $url)) {
5127
- throw $this->error("`$url` file not found for @import");
 
 
 
 
5128
  }
5129
  }
5130
 
5131
- return null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5132
  }
5133
 
5134
  /**
@@ -5136,10 +5868,20 @@ class Compiler
5136
  *
5137
  * @api
5138
  *
5139
- * @param string $encoding
 
 
 
 
5140
  */
5141
  public function setEncoding($encoding)
5142
  {
 
 
 
 
 
 
5143
  $this->encoding = $encoding;
5144
  }
5145
 
@@ -5167,9 +5909,13 @@ class Compiler
5167
  * @api
5168
  *
5169
  * @return array
 
 
5170
  */
5171
  public function getSourcePosition()
5172
  {
 
 
5173
  $sourceFile = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : '';
5174
 
5175
  return [$sourceFile, $this->sourceLine, $this->sourceColumn];
@@ -5199,7 +5945,7 @@ class Compiler
5199
  /**
5200
  * Build an error (exception)
5201
  *
5202
- * @api
5203
  *
5204
  * @param string $msg Message with optional sprintf()-style vararg parameters
5205
  *
@@ -5212,23 +5958,35 @@ class Compiler
5212
  }
5213
 
5214
  if (! $this->ignoreCallStackMessage) {
5215
- $line = $this->sourceLine;
5216
- $column = $this->sourceColumn;
 
 
 
 
 
 
 
 
 
 
 
 
 
5217
 
5218
- $loc = isset($this->sourceNames[$this->sourceIndex])
5219
- ? $this->sourceNames[$this->sourceIndex] . " on line $line, at column $column"
5220
- : "line: $line, column: $column";
5221
 
5222
- $msg = "$msg: $loc";
5223
 
5224
- $callStackMsg = $this->callStackMessage();
5225
 
5226
- if ($callStackMsg) {
5227
- $msg .= "\nCall Stack:\n" . $callStackMsg;
5228
- }
5229
  }
5230
 
5231
- return new CompilerException($msg);
5232
  }
5233
 
5234
  /**
@@ -5236,9 +5994,13 @@ class Compiler
5236
  * @param array $ExpectedArgs
5237
  * @param int $nbActual
5238
  * @return CompilerException
 
 
5239
  */
5240
  public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual)
5241
  {
 
 
5242
  $nbExpected = \count($ExpectedArgs);
5243
 
5244
  if ($nbActual > $nbExpected) {
@@ -5267,8 +6029,8 @@ class Compiler
5267
  /**
5268
  * Beautify call stack for output
5269
  *
5270
- * @param boolean $all
5271
- * @param null $limit
5272
  *
5273
  * @return string
5274
  */
@@ -5282,7 +6044,7 @@ class Compiler
5282
  if ($all || (isset($call['n']) && $call['n'])) {
5283
  $msg = '#' . $ncall++ . ' ' . $call['n'] . ' ';
5284
  $msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]])
5285
- ? $this->sourceNames[$call[Parser::SOURCE_INDEX]]
5286
  : '(unknown file)');
5287
  $msg .= ' on line ' . $call[Parser::SOURCE_LINE];
5288
 
@@ -5314,6 +6076,10 @@ class Compiler
5314
 
5315
  $file = $this->sourceNames[$env->block->sourceIndex];
5316
 
 
 
 
 
5317
  if (realpath($file) === $name) {
5318
  throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file));
5319
  }
@@ -5326,7 +6092,7 @@ class Compiler
5326
  * @param Object $func
5327
  * @param array $argValues
5328
  *
5329
- * @return array $returnValue
5330
  */
5331
  protected function callScssFunction($func, $argValues)
5332
  {
@@ -5366,11 +6132,11 @@ class Compiler
5366
  * Call built-in and registered (PHP) functions
5367
  *
5368
  * @param string $name
5369
- * @param string|array $function
5370
  * @param array $prototype
5371
  * @param array $args
5372
  *
5373
- * @return array
5374
  */
5375
  protected function callNativeFunction($name, $function, $prototype, $args)
5376
  {
@@ -5382,15 +6148,11 @@ class Compiler
5382
  }
5383
  @list($sorted, $kwargs) = $sorted_kwargs;
5384
 
5385
- if ($name !== 'if' && $name !== 'call') {
5386
- $inExp = true;
5387
-
5388
- if ($name === 'join') {
5389
- $inExp = false;
5390
- }
5391
-
5392
  foreach ($sorted as &$val) {
5393
- $val = $this->reduce($val, $inExp);
 
 
5394
  }
5395
  }
5396
 
@@ -5400,7 +6162,13 @@ class Compiler
5400
  return null;
5401
  }
5402
 
5403
- return $this->coerceValue($returnValue);
 
 
 
 
 
 
5404
  }
5405
 
5406
  /**
@@ -5418,7 +6186,11 @@ class Compiler
5418
 
5419
  /**
5420
  * Normalize native function name
5421
- * @param $name
 
 
 
 
5422
  * @return string
5423
  */
5424
  public static function normalizeNativeFunctionName($name)
@@ -5436,7 +6208,11 @@ class Compiler
5436
 
5437
  /**
5438
  * Check if a function is a native built-in scss function, for css parsing
5439
- * @param $name
 
 
 
 
5440
  * @return bool
5441
  */
5442
  public static function isNativeFunction($name)
@@ -5448,7 +6224,7 @@ class Compiler
5448
  * Sorts keyword arguments
5449
  *
5450
  * @param string $functionName
5451
- * @param array $prototypes
5452
  * @param array $args
5453
  *
5454
  * @return array|null
@@ -5484,229 +6260,352 @@ class Compiler
5484
  // notation 100 127 255 / 0 is in fact a simple list of 4 values
5485
  foreach ($args as $k => $arg) {
5486
  if ($arg[1][0] === Type::T_LIST && \count($arg[1][2]) === 3) {
5487
- $last = end($arg[1][2]);
5488
-
5489
- if ($last[0] === Type::T_EXPRESSION && $last[1] === '/') {
5490
- array_pop($arg[1][2]);
5491
- $arg[1][2][] = $last[2];
5492
- $arg[1][2][] = $last[3];
5493
- $args[$k] = $arg;
5494
- }
5495
  }
5496
  }
5497
  }
5498
 
5499
- $finalArgs = [];
5500
 
5501
  if (! \is_array(reset($prototypes))) {
5502
  $prototypes = [$prototypes];
5503
  }
5504
 
 
 
 
 
 
 
 
 
 
5505
  $keyArgs = [];
5506
 
5507
- // trying each prototypes
5508
- $prototypeHasMatch = false;
5509
- $exceptionMessage = '';
5510
 
5511
- foreach ($prototypes as $prototype) {
5512
- $argDef = [];
 
 
 
5513
 
5514
- foreach ($prototype as $i => $p) {
5515
- $default = null;
5516
- $p = explode(':', $p, 2);
5517
- $name = array_shift($p);
5518
 
5519
- if (\count($p)) {
5520
- $p = trim(reset($p));
 
5521
 
5522
- if ($p === 'null') {
5523
- // differentiate this null from the static::$null
5524
- $default = [Type::T_KEYWORD, 'null'];
5525
- } else {
5526
- if (\is_null($parser)) {
5527
- $parser = $this->parserFactory(__METHOD__);
5528
- }
5529
 
5530
- $parser->parseValue($p, $default);
5531
- }
5532
- }
5533
 
5534
- $isVariable = false;
 
5535
 
5536
- if (substr($name, -3) === '...') {
5537
- $isVariable = true;
5538
- $name = substr($name, 0, -3);
5539
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5540
 
5541
- $argDef[] = [$name, $default, $isVariable];
 
 
5542
  }
5543
 
5544
- $ignoreCallStackMessage = $this->ignoreCallStackMessage;
5545
- $this->ignoreCallStackMessage = true;
 
5546
 
5547
- try {
5548
- if (\count($args) > \count($argDef)) {
5549
- $lastDef = end($argDef);
5550
 
5551
- // check that last arg is not a ...
5552
- if (empty($lastDef[2])) {
5553
- throw $this->errorArgsNumber($functionName, $argDef, \count($args));
 
 
 
5554
  }
 
 
5555
  }
5556
- $vars = $this->applyArguments($argDef, $args, false, false);
5557
 
5558
- // ensure all args are populated
5559
- foreach ($prototype as $i => $p) {
5560
- $name = explode(':', $p)[0];
 
 
 
5561
 
5562
- if (! isset($finalArgs[$i])) {
5563
- $finalArgs[$i] = null;
5564
- }
5565
- }
 
5566
 
5567
- // apply positional args
5568
- foreach (array_values($vars) as $i => $val) {
5569
- $finalArgs[$i] = $val;
5570
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5571
 
5572
- $keyArgs = array_merge($keyArgs, $vars);
5573
- $prototypeHasMatch = true;
 
 
 
5574
 
5575
- // overwrite positional args with keyword args
5576
- foreach ($prototype as $i => $p) {
5577
- $name = explode(':', $p)[0];
5578
 
5579
- if (isset($keyArgs[$name])) {
5580
- $finalArgs[$i] = $keyArgs[$name];
5581
- }
 
5582
 
5583
- // special null value as default: translate to real null here
5584
- if ($finalArgs[$i] === [Type::T_KEYWORD, 'null']) {
5585
- $finalArgs[$i] = null;
5586
- }
5587
  }
5588
- // should we break if this prototype seems fulfilled?
5589
- } catch (CompilerException $e) {
5590
- $exceptionMessage = $e->getMessage();
5591
  }
5592
- $this->ignoreCallStackMessage = $ignoreCallStackMessage;
 
 
5593
  }
5594
 
5595
- if ($exceptionMessage && ! $prototypeHasMatch) {
5596
- if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) {
5597
- // if var() or calc() is used as an argument, return as a css function
5598
- foreach ($args as $arg) {
5599
- if ($arg[1][0] === Type::T_FUNCTION_CALL && in_array($arg[1][1], ['var'])) {
5600
- return null;
5601
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5602
  }
 
 
 
 
5603
  }
 
5604
 
5605
- throw $this->error($exceptionMessage);
 
5606
  }
5607
 
5608
- return [$finalArgs, $keyArgs];
 
 
 
 
 
 
 
 
5609
  }
5610
 
5611
  /**
5612
- * Apply argument values per definition
5613
  *
5614
- * @param array $argDef
5615
- * @param array $argValues
5616
- * @param boolean $storeInEnv
5617
- * @param boolean $reduce
5618
- * only used if $storeInEnv = false
5619
  *
5620
- * @return array
5621
  *
5622
- * @throws \Exception
 
 
5623
  */
5624
- protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true)
5625
  {
5626
- $output = [];
5627
 
5628
- if (\is_array($argValues) && \count($argValues) && end($argValues) === static::$null) {
5629
- array_pop($argValues);
5630
- }
5631
 
5632
- if ($storeInEnv) {
5633
- $storeEnv = $this->getStoreEnv();
 
 
 
 
 
 
 
 
5634
 
5635
- $env = new Environment();
5636
- $env->store = $storeEnv->store;
5637
  }
5638
 
5639
- $hasVariable = false;
5640
- $args = [];
 
 
 
 
 
 
 
 
 
 
5641
 
5642
- foreach ($argDef as $i => $arg) {
5643
- list($name, $default, $isVariable) = $argDef[$i];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5644
 
5645
- $args[$name] = [$i, $name, $default, $isVariable];
5646
- $hasVariable |= $isVariable;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5647
  }
5648
 
5649
- $splatSeparator = null;
5650
- $keywordArgs = [];
5651
- $deferredKeywordArgs = [];
5652
- $deferredNamedKeywordArgs = [];
5653
- $remaining = [];
5654
- $hasKeywordArgument = false;
5655
 
5656
- // assign the keyword args
5657
- foreach ((array) $argValues as $arg) {
5658
- if (! empty($arg[0])) {
5659
  $hasKeywordArgument = true;
5660
 
5661
- $name = $arg[0][1];
 
5662
 
5663
- if (! isset($args[$name])) {
5664
- foreach (array_keys($args) as $an) {
5665
- if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
5666
- $name = $an;
5667
- break;
5668
- }
5669
- }
5670
  }
5671
 
5672
- if (! isset($args[$name]) || $args[$name][3]) {
5673
- if ($hasVariable) {
5674
- $deferredNamedKeywordArgs[$name] = $arg[1];
5675
- } else {
5676
- throw $this->error("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
5677
- }
5678
- } elseif ($args[$name][0] < \count($remaining)) {
5679
- throw $this->error("The argument $%s was passed both by position and by name.", $arg[0][1]);
5680
- } else {
5681
- $keywordArgs[$name] = $arg[1];
5682
- }
5683
  } elseif (! empty($arg[2])) {
5684
  // $arg[2] means a var followed by ... in the arg ($list... )
5685
  $val = $this->reduce($arg[1], true);
 
5686
 
5687
  if ($val[0] === Type::T_LIST) {
5688
- foreach ($val[2] as $name => $item) {
5689
- if (! is_numeric($name)) {
5690
- if (! isset($args[$name])) {
5691
- foreach (array_keys($args) as $an) {
5692
- if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
5693
- $name = $an;
5694
- break;
5695
- }
5696
- }
5697
- }
5698
 
5699
- if ($hasVariable) {
5700
- $deferredKeywordArgs[$name] = $item;
5701
- } else {
5702
- $keywordArgs[$name] = $item;
5703
- }
5704
- } else {
5705
- if (\is_null($splatSeparator)) {
5706
- $splatSeparator = $val[1];
 
 
 
5707
  }
5708
 
5709
- $remaining[] = $item;
 
 
5710
  }
5711
  }
5712
  } elseif ($val[0] === Type::T_MAP) {
@@ -5715,72 +6614,122 @@ class Compiler
5715
  $item = $val[2][$i];
5716
 
5717
  if (! is_numeric($name)) {
5718
- if (! isset($args[$name])) {
5719
- foreach (array_keys($args) as $an) {
5720
- if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
5721
- $name = $an;
5722
- break;
5723
- }
5724
- }
5725
- }
5726
 
5727
- if ($hasVariable) {
5728
- $deferredKeywordArgs[$name] = $item;
5729
- } else {
5730
- $keywordArgs[$name] = $item;
5731
  }
 
 
 
 
5732
  } else {
5733
  if (\is_null($splatSeparator)) {
5734
  $splatSeparator = $val[1];
5735
  }
5736
 
5737
- $remaining[] = $item;
5738
  }
5739
  }
5740
- } else {
5741
- $remaining[] = $val;
5742
  }
5743
  } elseif ($hasKeywordArgument) {
5744
- throw $this->error('Positional arguments must come before keyword arguments.');
5745
  } else {
5746
- $remaining[] = $arg[1];
5747
  }
5748
  }
5749
 
5750
- foreach ($args as $arg) {
5751
- list($i, $name, $default, $isVariable) = $arg;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5752
 
5753
  if ($isVariable) {
5754
- // only if more than one arg : can not be passed as position and value
5755
- // see https://github.com/sass/libsass/issues/2927
5756
- if (count($args) > 1) {
5757
- if (isset($remaining[$i]) && isset($deferredNamedKeywordArgs[$name])) {
5758
- throw $this->error("The argument $%s was passed both by position and by name.", $name);
5759
- }
5760
- }
5761
 
5762
- $val = [Type::T_LIST, \is_null($splatSeparator) ? ',' : $splatSeparator , [], $isVariable];
5763
 
5764
- for ($count = \count($remaining); $i < $count; $i++) {
5765
- $val[2][] = $remaining[$i];
5766
- }
5767
 
5768
- foreach ($deferredKeywordArgs as $itemName => $item) {
5769
- $val[2][$itemName] = $item;
5770
- }
5771
 
5772
- foreach ($deferredNamedKeywordArgs as $itemName => $item) {
5773
- $val[2][$itemName] = $item;
5774
- }
5775
- } elseif (isset($remaining[$i])) {
5776
- $val = $remaining[$i];
5777
- } elseif (isset($keywordArgs[$name])) {
5778
- $val = $keywordArgs[$name];
5779
- } elseif (! empty($default)) {
5780
  continue;
 
 
 
 
 
 
5781
  } else {
5782
- throw $this->error("Missing argument $name");
5783
  }
 
 
 
 
 
 
5784
 
5785
  if ($storeInEnv) {
5786
  $this->set($name, $this->reduce($val, true), true, $env);
@@ -5793,12 +6742,13 @@ class Compiler
5793
  $storeEnv->store = $env->store;
5794
  }
5795
 
5796
- foreach ($args as $arg) {
5797
- list($i, $name, $default, $isVariable) = $arg;
5798
 
5799
- if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) {
5800
  continue;
5801
  }
 
5802
 
5803
  if ($storeInEnv) {
5804
  $this->set($name, $this->reduce($default, true), true);
@@ -5810,16 +6760,77 @@ class Compiler
5810
  return $output;
5811
  }
5812
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5813
  /**
5814
  * Coerce a php value into a scss one
5815
  *
5816
  * @param mixed $value
5817
  *
5818
- * @return array|\ScssPhp\ScssPhp\Node\Number
5819
  */
5820
  protected function coerceValue($value)
5821
  {
5822
- if (\is_array($value) || $value instanceof \ArrayAccess) {
5823
  return $value;
5824
  }
5825
 
@@ -5832,7 +6843,7 @@ class Compiler
5832
  }
5833
 
5834
  if (is_numeric($value)) {
5835
- return new Node\Number($value, '');
5836
  }
5837
 
5838
  if ($value === '') {
@@ -5852,9 +6863,9 @@ class Compiler
5852
  /**
5853
  * Coerce something to map
5854
  *
5855
- * @param array $item
5856
  *
5857
- * @return array
5858
  */
5859
  protected function coerceMap($item)
5860
  {
@@ -5863,9 +6874,8 @@ class Compiler
5863
  }
5864
 
5865
  if (
5866
- $item[0] === static::$emptyList[0] &&
5867
- $item[1] === static::$emptyList[1] &&
5868
- $item[2] === static::$emptyList[2]
5869
  ) {
5870
  return static::$emptyMap;
5871
  }
@@ -5876,15 +6886,19 @@ class Compiler
5876
  /**
5877
  * Coerce something to list
5878
  *
5879
- * @param array $item
5880
- * @param string $delim
5881
- * @param boolean $removeTrailingNull
5882
  *
5883
  * @return array
5884
  */
5885
  protected function coerceList($item, $delim = ',', $removeTrailingNull = false)
5886
  {
5887
- if (isset($item) && $item[0] === Type::T_LIST) {
 
 
 
 
5888
  // remove trailing null from the list
5889
  if ($removeTrailingNull && end($item[2]) === static::$null) {
5890
  array_pop($item[2]);
@@ -5893,7 +6907,7 @@ class Compiler
5893
  return $item;
5894
  }
5895
 
5896
- if (isset($item) && $item[0] === Type::T_MAP) {
5897
  $keys = $item[1];
5898
  $values = $item[2];
5899
  $list = [];
@@ -5924,15 +6938,15 @@ class Compiler
5924
  return [Type::T_LIST, ',', $list];
5925
  }
5926
 
5927
- return [Type::T_LIST, $delim, ! isset($item) ? [] : [$item]];
5928
  }
5929
 
5930
  /**
5931
  * Coerce color for expression
5932
  *
5933
- * @param array $value
5934
  *
5935
- * @return array|null
5936
  */
5937
  protected function coerceForExpression($value)
5938
  {
@@ -5946,12 +6960,17 @@ class Compiler
5946
  /**
5947
  * Coerce value to color
5948
  *
5949
- * @param array $value
 
5950
  *
5951
  * @return array|null
5952
  */
5953
  protected function coerceColor($value, $inRGBFunction = false)
5954
  {
 
 
 
 
5955
  switch ($value[0]) {
5956
  case Type::T_COLOR:
5957
  for ($i = 1; $i <= 3; $i++) {
@@ -6037,7 +7056,7 @@ class Compiler
6037
  if ($color[3] === 255) {
6038
  $color[3] = 1; // fully opaque
6039
  } else {
6040
- $color[3] = round($color[3] / 255, Node\Number::PRECISION);
6041
  }
6042
  }
6043
 
@@ -6060,8 +7079,8 @@ class Compiler
6060
  }
6061
 
6062
  /**
6063
- * @param integer|\ScssPhp\ScssPhp\Node\Number $value
6064
- * @param boolean $isAlpha
6065
  *
6066
  * @return integer|mixed
6067
  */
@@ -6079,36 +7098,27 @@ class Compiler
6079
  * @param integer|float $min
6080
  * @param integer|float $max
6081
  * @param boolean $isInt
6082
- * @param boolean $clamp
6083
- * @param boolean $modulo
6084
  *
6085
  * @return integer|mixed
6086
  */
6087
- protected function compileColorPartValue($value, $min, $max, $isInt = true, $clamp = true, $modulo = false)
6088
  {
6089
  if (! is_numeric($value)) {
6090
  if (\is_array($value)) {
6091
  $reduced = $this->reduce($value);
6092
 
6093
- if (\is_object($reduced) && $value->type === Type::T_NUMBER) {
6094
  $value = $reduced;
6095
  }
6096
  }
6097
 
6098
- if (\is_object($value) && $value->type === Type::T_NUMBER) {
6099
- $num = $value->dimension;
6100
-
6101
- if (\count($value->units)) {
6102
- $unit = array_keys($value->units);
6103
- $unit = reset($unit);
6104
-
6105
- switch ($unit) {
6106
- case '%':
6107
- $num *= $max / 100;
6108
- break;
6109
- default:
6110
- break;
6111
- }
6112
  }
6113
 
6114
  $value = $num;
@@ -6122,18 +7132,7 @@ class Compiler
6122
  $value = round($value);
6123
  }
6124
 
6125
- if ($clamp) {
6126
- $value = min($max, max($min, $value));
6127
- }
6128
-
6129
- if ($modulo) {
6130
- $value = $value % $max;
6131
-
6132
- // still negative?
6133
- while ($value < $min) {
6134
- $value += $max;
6135
- }
6136
- }
6137
 
6138
  return $value;
6139
  }
@@ -6144,9 +7143,9 @@ class Compiler
6144
  /**
6145
  * Coerce value to string
6146
  *
6147
- * @param array $value
6148
  *
6149
- * @return array|null
6150
  */
6151
  protected function coerceString($value)
6152
  {
@@ -6158,16 +7157,21 @@ class Compiler
6158
  }
6159
 
6160
  /**
6161
- * Assert value is a string (or keyword)
 
 
 
 
 
6162
  *
6163
  * @api
6164
  *
6165
- * @param array $value
6166
- * @param string $varName
6167
  *
6168
  * @return array
6169
  *
6170
- * @throws \Exception
6171
  */
6172
  public function assertString($value, $varName = null)
6173
  {
@@ -6178,30 +7182,27 @@ class Compiler
6178
 
6179
  if (! \in_array($value[0], [Type::T_STRING, Type::T_KEYWORD])) {
6180
  $value = $this->compileValue($value);
6181
- $var_display = ($varName ? " \${$varName}:" : '');
6182
- throw $this->error("Error:{$var_display} $value is not a string.");
6183
  }
6184
 
6185
- $value = $this->coerceString($value);
6186
-
6187
- return $value;
6188
  }
6189
 
6190
  /**
6191
  * Coerce value to a percentage
6192
  *
6193
- * @param array $value
6194
  *
6195
  * @return integer|float
6196
  */
6197
  protected function coercePercent($value)
6198
  {
6199
- if ($value[0] === Type::T_NUMBER) {
6200
- if (! empty($value[2]['%'])) {
6201
- return $value[1] / 100;
6202
  }
6203
 
6204
- return $value[1];
6205
  }
6206
 
6207
  return 0;
@@ -6212,18 +7213,21 @@ class Compiler
6212
  *
6213
  * @api
6214
  *
6215
- * @param array $value
 
6216
  *
6217
  * @return array
6218
  *
6219
- * @throws \Exception
6220
  */
6221
- public function assertMap($value)
6222
  {
6223
  $value = $this->coerceMap($value);
6224
 
6225
  if ($value[0] !== Type::T_MAP) {
6226
- throw $this->error('expecting map, %s received', $value[0]);
 
 
6227
  }
6228
 
6229
  return $value;
@@ -6234,7 +7238,7 @@ class Compiler
6234
  *
6235
  * @api
6236
  *
6237
- * @param array $value
6238
  *
6239
  * @return array
6240
  *
@@ -6249,24 +7253,48 @@ class Compiler
6249
  return $value;
6250
  }
6251
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6252
  /**
6253
  * Assert value is a color
6254
  *
6255
  * @api
6256
  *
6257
- * @param array $value
 
6258
  *
6259
  * @return array
6260
  *
6261
- * @throws \Exception
6262
  */
6263
- public function assertColor($value)
6264
  {
6265
  if ($color = $this->coerceColor($value)) {
6266
  return $color;
6267
  }
6268
 
6269
- throw $this->error('expecting color, %s received', $value[0]);
 
 
6270
  }
6271
 
6272
  /**
@@ -6274,22 +7302,21 @@ class Compiler
6274
  *
6275
  * @api
6276
  *
6277
- * @param array $value
6278
- * @param string $varName
6279
  *
6280
- * @return integer|float
6281
  *
6282
- * @throws \Exception
6283
  */
6284
  public function assertNumber($value, $varName = null)
6285
  {
6286
- if ($value[0] !== Type::T_NUMBER) {
6287
  $value = $this->compileValue($value);
6288
- $var_display = ($varName ? " \${$varName}:" : '');
6289
- throw $this->error("Error:{$var_display} $value is not a number.");
6290
  }
6291
 
6292
- return $value[1];
6293
  }
6294
 
6295
  /**
@@ -6297,25 +7324,41 @@ class Compiler
6297
  *
6298
  * @api
6299
  *
6300
- * @param array $value
6301
- * @param string $varName
6302
  *
6303
- * @return integer|float
6304
  *
6305
- * @throws \Exception
6306
  */
6307
  public function assertInteger($value, $varName = null)
6308
  {
6309
-
6310
- $value = $this->assertNumber($value, $varName);
6311
- if (round($value - \intval($value), Node\Number::PRECISION) > 0) {
6312
- $var_display = ($varName ? " \${$varName}:" : '');
6313
- throw $this->error("Error:{$var_display} $value is not an integer.");
6314
  }
6315
 
6316
  return intval($value);
6317
  }
6318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6319
 
6320
  /**
6321
  * Make sure a color's components don't go out of bounds
@@ -6334,6 +7377,10 @@ class Compiler
6334
  if ($c[$i] > 255) {
6335
  $c[$i] = 255;
6336
  }
 
 
 
 
6337
  }
6338
 
6339
  return $c;
@@ -6342,7 +7389,7 @@ class Compiler
6342
  /**
6343
  * Convert RGB to HSL
6344
  *
6345
- * @api
6346
  *
6347
  * @param integer $red
6348
  * @param integer $green
@@ -6414,11 +7461,11 @@ class Compiler
6414
  /**
6415
  * Convert HSL to RGB
6416
  *
6417
- * @api
6418
  *
6419
- * @param integer $hue H from 0 to 360
6420
- * @param integer $saturation S from 0 to 100
6421
- * @param integer $lightness L from 0 to 100
6422
  *
6423
  * @return array
6424
  */
@@ -6444,19 +7491,87 @@ class Compiler
6444
  return $out;
6445
  }
6446
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6447
  // Built in functions
6448
 
6449
  protected static $libCall = ['function', 'args...'];
6450
- protected function libCall($args, $kwargs)
6451
  {
6452
- $functionReference = $this->reduce(array_shift($args), true);
6453
 
6454
  if (in_array($functionReference[0], [Type::T_STRING, Type::T_KEYWORD])) {
6455
- $name = $this->compileStringContent($this->coerceString($this->reduce($functionReference, true)));
6456
- $warning = "DEPRECATION WARNING: Passing a string to call() is deprecated and will be illegal\n"
6457
  . "in Sass 4.0. Use call(function-reference($name)) instead.";
6458
- fwrite($this->stderr, "$warning\n\n");
6459
- $functionReference = $this->libGetFunction([$functionReference]);
6460
  }
6461
 
6462
  if ($functionReference === static::$null) {
@@ -6467,18 +7582,9 @@ class Compiler
6467
  throw $this->error('Function reference expected, got ' . $functionReference[0]);
6468
  }
6469
 
6470
- $callArgs = [];
6471
-
6472
- // $kwargs['args'] is [Type::T_LIST, ',', [..]]
6473
- foreach ($kwargs['args'][2] as $varname => $arg) {
6474
- if (is_numeric($varname)) {
6475
- $varname = null;
6476
- } else {
6477
- $varname = [ 'var', $varname];
6478
- }
6479
-
6480
- $callArgs[] = [$varname, $arg, false];
6481
- }
6482
 
6483
  return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]);
6484
  }
@@ -6490,11 +7596,11 @@ class Compiler
6490
  ];
6491
  protected function libGetFunction($args)
6492
  {
6493
- $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
6494
  $isCss = false;
6495
 
6496
  if (count($args)) {
6497
- $isCss = $this->reduce(array_shift($args), true);
6498
  $isCss = (($isCss === static::$true) ? true : false);
6499
  }
6500
 
@@ -6535,15 +7641,32 @@ class Compiler
6535
  return static::$null;
6536
  }
6537
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6538
  $values = [];
6539
 
 
6540
  foreach ($list[2] as $item) {
6541
  $values[] = $this->normalizeValue($item);
6542
  }
6543
 
6544
  $key = array_search($this->normalizeValue($value), $values);
6545
 
6546
- return false === $key ? static::$null : $key + 1;
6547
  }
6548
 
6549
  protected static $libRgb = [
@@ -6610,32 +7733,133 @@ class Compiler
6610
  return $this->libRgb($args, $kwargs, 'rgba');
6611
  }
6612
 
6613
- // helper function for adjust_color, change_color, and scale_color
6614
- protected function alterColor($args, $fn)
6615
- {
6616
- $color = $this->assertColor($args[0]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6617
 
6618
- foreach ([1 => 1, 2 => 2, 3 => 3, 7 => 4] as $iarg => $irgba) {
6619
- if (isset($args[$iarg])) {
6620
- $val = $this->assertNumber($args[$iarg]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6621
 
6622
- if (! isset($color[$irgba])) {
6623
- $color[$irgba] = (($irgba < 4) ? 0 : 1);
6624
- }
 
6625
 
6626
- $color[$irgba] = \call_user_func($fn, $color[$irgba], $val, $iarg);
6627
- }
6628
  }
6629
 
6630
- if (! empty($args[4]) || ! empty($args[5]) || ! empty($args[6])) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6631
  $hsl = $this->toHSL($color[1], $color[2], $color[3]);
6632
 
6633
- foreach ([4 => 1, 5 => 2, 6 => 3] as $iarg => $ihsl) {
6634
- if (! empty($args[$iarg])) {
6635
- $val = $this->assertNumber($args[$iarg]);
6636
- $hsl[$ihsl] = \call_user_func($fn, $hsl[$ihsl], $val, $iarg);
6637
- }
6638
  }
 
 
6639
 
6640
  $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
6641
 
@@ -6646,58 +7870,54 @@ class Compiler
6646
  $color = $rgb;
6647
  }
6648
 
 
 
 
 
 
6649
  return $color;
6650
  }
6651
 
6652
- protected static $libAdjustColor = [
6653
- 'color', 'red:null', 'green:null', 'blue:null',
6654
- 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
6655
- ];
6656
  protected function libAdjustColor($args)
6657
  {
6658
- return $this->alterColor($args, function ($base, $alter, $i) {
6659
- return $base + $alter;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6660
  });
6661
  }
6662
 
6663
- protected static $libChangeColor = [
6664
- 'color', 'red:null', 'green:null', 'blue:null',
6665
- 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
6666
- ];
6667
  protected function libChangeColor($args)
6668
  {
6669
- return $this->alterColor($args, function ($base, $alter, $i) {
 
 
 
 
6670
  return $alter;
6671
  });
6672
  }
6673
 
6674
- protected static $libScaleColor = [
6675
- 'color', 'red:null', 'green:null', 'blue:null',
6676
- 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
6677
- ];
6678
  protected function libScaleColor($args)
6679
  {
6680
- return $this->alterColor($args, function ($base, $scale, $i) {
6681
- // 1, 2, 3 - rgb
6682
- // 4, 5, 6 - hsl
6683
- // 7 - a
6684
- switch ($i) {
6685
- case 1:
6686
- case 2:
6687
- case 3:
6688
- $max = 255;
6689
- break;
6690
-
6691
- case 4:
6692
- $max = 360;
6693
- break;
6694
-
6695
- case 7:
6696
- $max = 1;
6697
- break;
6698
-
6699
- default:
6700
- $max = 100;
6701
  }
6702
 
6703
  $scale = $scale / 100;
@@ -6716,7 +7936,7 @@ class Compiler
6716
  $color = $this->coerceColor($args[0]);
6717
 
6718
  if (\is_null($color)) {
6719
- $this->throwError('Error: argument `$color` of `ie-hex-str($color)` must be a color');
6720
  }
6721
 
6722
  $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
@@ -6730,10 +7950,10 @@ class Compiler
6730
  $color = $this->coerceColor($args[0]);
6731
 
6732
  if (\is_null($color)) {
6733
- $this->throwError('Error: argument `$color` of `red($color)` must be a color');
6734
  }
6735
 
6736
- return $color[1];
6737
  }
6738
 
6739
  protected static $libGreen = ['color'];
@@ -6742,10 +7962,10 @@ class Compiler
6742
  $color = $this->coerceColor($args[0]);
6743
 
6744
  if (\is_null($color)) {
6745
- $this->throwError('Error: argument `$color` of `green($color)` must be a color');
6746
  }
6747
 
6748
- return $color[2];
6749
  }
6750
 
6751
  protected static $libBlue = ['color'];
@@ -6754,17 +7974,17 @@ class Compiler
6754
  $color = $this->coerceColor($args[0]);
6755
 
6756
  if (\is_null($color)) {
6757
- $this->throwError('Error: argument `$color` of `blue($color)` must be a color');
6758
  }
6759
 
6760
- return $color[3];
6761
  }
6762
 
6763
  protected static $libAlpha = ['color'];
6764
  protected function libAlpha($args)
6765
  {
6766
  if ($color = $this->coerceColor($args[0])) {
6767
- return isset($color[4]) ? $color[4] : 1;
6768
  }
6769
 
6770
  // this might be the IE function, so return value unchanged
@@ -6776,7 +7996,7 @@ class Compiler
6776
  {
6777
  $value = $args[0];
6778
 
6779
- if ($value[0] === Type::T_NUMBER) {
6780
  return null;
6781
  }
6782
 
@@ -6792,14 +8012,9 @@ class Compiler
6792
  {
6793
  list($first, $second, $weight) = $args;
6794
 
6795
- $first = $this->assertColor($first);
6796
- $second = $this->assertColor($second);
6797
-
6798
- if (! isset($weight)) {
6799
- $weight = 0.5;
6800
- } else {
6801
- $weight = $this->coercePercent($weight);
6802
- }
6803
 
6804
  $firstAlpha = isset($first[4]) ? $first[4] : 1;
6805
  $secondAlpha = isset($second[4]) ? $second[4] : 1;
@@ -6825,6 +8040,7 @@ class Compiler
6825
 
6826
  protected static $libHsl = [
6827
  ['channels'],
 
6828
  ['hue', 'saturation', 'lightness'],
6829
  ['hue', 'saturation', 'lightness', 'alpha'] ];
6830
  protected function libHsl($args, $kwargs, $funcName = 'hsl')
@@ -6840,24 +8056,30 @@ class Compiler
6840
  $args_to_check = $kwargs['channels'][2];
6841
  }
6842
 
6843
- $hue = $this->compileColorPartValue($args[0], 0, 360, false, false, true);
6844
- $saturation = $this->compileColorPartValue($args[1], 0, 100, false);
6845
- $lightness = $this->compileColorPartValue($args[2], 0, 100, false);
 
 
 
 
 
 
 
6846
 
6847
  foreach ($kwargs as $k => $arg) {
6848
- if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
6849
  return null;
6850
  }
6851
  }
6852
 
6853
  foreach ($args_to_check as $k => $arg) {
6854
- if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
6855
  if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) {
6856
  return null;
6857
  }
6858
 
6859
  $args[$k] = $this->stringifyFncallArgs($arg);
6860
- $hue = '';
6861
  }
6862
 
6863
  if (
@@ -6869,22 +8091,31 @@ class Compiler
6869
  }
6870
  }
6871
 
 
 
 
6872
  $alpha = null;
6873
 
6874
  if (\count($args) === 4) {
6875
  $alpha = $this->compileColorPartValue($args[3], 0, 100, false);
6876
 
6877
- if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness) || ! is_numeric($alpha)) {
6878
  return [Type::T_STRING, '',
6879
  [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
6880
  }
6881
  } else {
6882
- if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness)) {
6883
  return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
6884
  }
6885
  }
6886
 
6887
- $color = $this->toRGB($hue, $saturation, $lightness);
 
 
 
 
 
 
6888
 
6889
  if (! \is_null($alpha)) {
6890
  $color[4] = $alpha;
@@ -6895,6 +8126,7 @@ class Compiler
6895
 
6896
  protected static $libHsla = [
6897
  ['channels'],
 
6898
  ['hue', 'saturation', 'lightness'],
6899
  ['hue', 'saturation', 'lightness', 'alpha']];
6900
  protected function libHsla($args, $kwargs)
@@ -6905,30 +8137,156 @@ class Compiler
6905
  protected static $libHue = ['color'];
6906
  protected function libHue($args)
6907
  {
6908
- $color = $this->assertColor($args[0]);
6909
  $hsl = $this->toHSL($color[1], $color[2], $color[3]);
6910
 
6911
- return new Node\Number($hsl[1], 'deg');
6912
  }
6913
 
6914
  protected static $libSaturation = ['color'];
6915
  protected function libSaturation($args)
6916
  {
6917
- $color = $this->assertColor($args[0]);
6918
  $hsl = $this->toHSL($color[1], $color[2], $color[3]);
6919
 
6920
- return new Node\Number($hsl[2], '%');
6921
  }
6922
 
6923
  protected static $libLightness = ['color'];
6924
  protected function libLightness($args)
6925
  {
6926
- $color = $this->assertColor($args[0]);
6927
  $hsl = $this->toHSL($color[1], $color[2], $color[3]);
6928
 
6929
- return new Node\Number($hsl[3], '%');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6930
  }
6931
 
 
 
 
 
 
 
 
 
 
 
6932
  protected function adjustHsl($color, $idx, $amount)
6933
  {
6934
  $hsl = $this->toHSL($color[1], $color[2], $color[3]);
@@ -6945,8 +8303,8 @@ class Compiler
6945
  protected static $libAdjustHue = ['color', 'degrees'];
6946
  protected function libAdjustHue($args)
6947
  {
6948
- $color = $this->assertColor($args[0]);
6949
- $degrees = $this->assertNumber($args[1]);
6950
 
6951
  return $this->adjustHsl($color, 1, $degrees);
6952
  }
@@ -6954,7 +8312,7 @@ class Compiler
6954
  protected static $libLighten = ['color', 'amount'];
6955
  protected function libLighten($args)
6956
  {
6957
- $color = $this->assertColor($args[0]);
6958
  $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
6959
 
6960
  return $this->adjustHsl($color, 3, $amount);
@@ -6963,7 +8321,7 @@ class Compiler
6963
  protected static $libDarken = ['color', 'amount'];
6964
  protected function libDarken($args)
6965
  {
6966
- $color = $this->assertColor($args[0]);
6967
  $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
6968
 
6969
  return $this->adjustHsl($color, 3, -$amount);
@@ -6974,17 +8332,14 @@ class Compiler
6974
  {
6975
  $value = $args[0];
6976
 
6977
- if ($value[0] === Type::T_NUMBER) {
6978
- return null;
6979
- }
6980
-
6981
  if (count($args) === 1) {
6982
- $val = $this->compileValue($value);
6983
- throw $this->error("\$amount: $val is not a number");
 
6984
  }
6985
 
6986
- $color = $this->assertColor($value);
6987
- $amount = 100 * $this->coercePercent($args[1]);
6988
 
6989
  return $this->adjustHsl($color, 2, $amount);
6990
  }
@@ -6992,8 +8347,8 @@ class Compiler
6992
  protected static $libDesaturate = ['color', 'amount'];
6993
  protected function libDesaturate($args)
6994
  {
6995
- $color = $this->assertColor($args[0]);
6996
- $amount = 100 * $this->coercePercent($args[1]);
6997
 
6998
  return $this->adjustHsl($color, 2, -$amount);
6999
  }
@@ -7003,42 +8358,38 @@ class Compiler
7003
  {
7004
  $value = $args[0];
7005
 
7006
- if ($value[0] === Type::T_NUMBER) {
7007
  return null;
7008
  }
7009
 
7010
- return $this->adjustHsl($this->assertColor($value), 2, -100);
7011
  }
7012
 
7013
  protected static $libComplement = ['color'];
7014
  protected function libComplement($args)
7015
  {
7016
- return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
7017
  }
7018
 
7019
  protected static $libInvert = ['color', 'weight:1'];
7020
  protected function libInvert($args)
7021
  {
7022
- list($value, $weight) = $args;
7023
-
7024
- if (! isset($weight)) {
7025
- $weight = 1;
7026
- } else {
7027
- $weight = $this->coercePercent($weight);
7028
- }
7029
 
7030
- if ($value[0] === Type::T_NUMBER) {
7031
  return null;
7032
  }
7033
 
7034
- $color = $this->assertColor($value);
 
 
7035
  $inverted = $color;
7036
  $inverted[1] = 255 - $inverted[1];
7037
  $inverted[2] = 255 - $inverted[2];
7038
  $inverted[3] = 255 - $inverted[3];
7039
 
7040
  if ($weight < 1) {
7041
- return $this->libMix([$inverted, $color, [Type::T_NUMBER, $weight]]);
7042
  }
7043
 
7044
  return $inverted;
@@ -7048,8 +8399,8 @@ class Compiler
7048
  protected static $libOpacify = ['color', 'amount'];
7049
  protected function libOpacify($args)
7050
  {
7051
- $color = $this->assertColor($args[0]);
7052
- $amount = $this->coercePercent($args[1]);
7053
 
7054
  $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
7055
  $color[4] = min(1, max(0, $color[4]));
@@ -7067,8 +8418,8 @@ class Compiler
7067
  protected static $libTransparentize = ['color', 'amount'];
7068
  protected function libTransparentize($args)
7069
  {
7070
- $color = $this->assertColor($args[0]);
7071
- $amount = $this->coercePercent($args[1]);
7072
 
7073
  $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
7074
  $color[4] = min(1, max(0, $color[4]));
@@ -7085,140 +8436,121 @@ class Compiler
7085
  protected static $libUnquote = ['string'];
7086
  protected function libUnquote($args)
7087
  {
7088
- $str = $args[0];
 
 
 
 
 
 
 
 
7089
 
7090
- if ($str[0] === Type::T_STRING) {
7091
- $str[1] = '';
 
7092
  }
7093
 
 
 
7094
  return $str;
7095
  }
7096
 
7097
  protected static $libQuote = ['string'];
7098
  protected function libQuote($args)
7099
  {
7100
- $value = $args[0];
7101
 
7102
- if ($value[0] === Type::T_STRING && ! empty($value[1])) {
7103
- return $value;
7104
- }
7105
 
7106
- return [Type::T_STRING, '"', [$value]];
7107
  }
7108
 
7109
  protected static $libPercentage = ['number'];
7110
  protected function libPercentage($args)
7111
  {
7112
- return new Node\Number($this->coercePercent($args[0]) * 100, '%');
 
 
 
7113
  }
7114
 
7115
  protected static $libRound = ['number'];
7116
  protected function libRound($args)
7117
  {
7118
- $num = $args[0];
7119
 
7120
- return new Node\Number(round($num[1]), $num[2]);
7121
  }
7122
 
7123
  protected static $libFloor = ['number'];
7124
  protected function libFloor($args)
7125
  {
7126
- $num = $args[0];
7127
 
7128
- return new Node\Number(floor($num[1]), $num[2]);
7129
  }
7130
 
7131
  protected static $libCeil = ['number'];
7132
  protected function libCeil($args)
7133
  {
7134
- $num = $args[0];
7135
 
7136
- return new Node\Number(ceil($num[1]), $num[2]);
7137
  }
7138
 
7139
  protected static $libAbs = ['number'];
7140
  protected function libAbs($args)
7141
  {
7142
- $num = $args[0];
7143
 
7144
- return new Node\Number(abs($num[1]), $num[2]);
7145
  }
7146
 
 
7147
  protected function libMin($args)
7148
  {
7149
- $numbers = $this->getNormalizedNumbers($args);
7150
- $minOriginal = null;
7151
- $minNormalized = null;
 
7152
 
7153
- foreach ($numbers as $key => $pair) {
7154
- list($original, $normalized) = $pair;
7155
 
7156
- if (\is_null($normalized) || \is_null($minNormalized)) {
7157
- if (\is_null($minOriginal) || $original[1] <= $minOriginal[1]) {
7158
- $minOriginal = $original;
7159
- $minNormalized = $normalized;
7160
- }
7161
- } elseif ($normalized[1] <= $minNormalized[1]) {
7162
- $minOriginal = $original;
7163
- $minNormalized = $normalized;
7164
  }
7165
  }
7166
 
7167
- return $minOriginal;
7168
- }
7169
-
7170
- protected function libMax($args)
7171
- {
7172
- $numbers = $this->getNormalizedNumbers($args);
7173
- $maxOriginal = null;
7174
- $maxNormalized = null;
7175
-
7176
- foreach ($numbers as $key => $pair) {
7177
- list($original, $normalized) = $pair;
7178
-
7179
- if (\is_null($normalized) || \is_null($maxNormalized)) {
7180
- if (\is_null($maxOriginal) || $original[1] >= $maxOriginal[1]) {
7181
- $maxOriginal = $original;
7182
- $maxNormalized = $normalized;
7183
- }
7184
- } elseif ($normalized[1] >= $maxNormalized[1]) {
7185
- $maxOriginal = $original;
7186
- $maxNormalized = $normalized;
7187
- }
7188
  }
7189
 
7190
- return $maxOriginal;
7191
  }
7192
 
7193
- /**
7194
- * Helper to normalize args containing numbers
7195
- *
7196
- * @param array $args
7197
- *
7198
- * @return array
7199
- */
7200
- protected function getNormalizedNumbers($args)
7201
  {
7202
- $unit = null;
7203
- $originalUnit = null;
7204
- $numbers = [];
7205
-
7206
- foreach ($args as $key => $item) {
7207
- $this->assertNumber($item);
7208
 
7209
- $number = $item->normalize();
 
7210
 
7211
- if (empty($unit)) {
7212
- $unit = $number[2];
7213
- $originalUnit = $item->unitStr();
7214
- } elseif ($number[1] && $unit !== $number[2] && ! empty($number[2])) {
7215
- throw $this->error('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
7216
  }
 
7217
 
7218
- $numbers[$key] = [$args[$key], empty($number[2]) ? null : $number];
 
7219
  }
7220
 
7221
- return $numbers;
7222
  }
7223
 
7224
  protected static $libLength = ['list'];
@@ -7226,38 +8558,34 @@ class Compiler
7226
  {
7227
  $list = $this->coerceList($args[0], ',', true);
7228
 
7229
- return \count($list[2]);
7230
  }
7231
 
7232
- //protected static $libListSeparator = ['list...'];
7233
  protected function libListSeparator($args)
7234
  {
7235
- if (\count($args) > 1) {
7236
- return 'comma';
7237
- }
7238
-
7239
  if (! \in_array($args[0][0], [Type::T_LIST, Type::T_MAP])) {
7240
- return 'space';
7241
  }
7242
 
7243
  $list = $this->coerceList($args[0]);
7244
 
7245
  if (\count($list[2]) <= 1 && empty($list['enclosing'])) {
7246
- return 'space';
7247
  }
7248
 
7249
  if ($list[1] === ',') {
7250
- return 'comma';
7251
  }
7252
 
7253
- return 'space';
7254
  }
7255
 
7256
  protected static $libNth = ['list', 'n'];
7257
  protected function libNth($args)
7258
  {
7259
  $list = $this->coerceList($args[0], ',', false);
7260
- $n = $this->assertNumber($args[1]);
7261
 
7262
  if ($n > 0) {
7263
  $n--;
@@ -7272,7 +8600,7 @@ class Compiler
7272
  protected function libSetNth($args)
7273
  {
7274
  $list = $this->coerceList($args[0]);
7275
- $n = $this->assertNumber($args[1]);
7276
 
7277
  if ($n > 0) {
7278
  $n--;
@@ -7292,7 +8620,7 @@ class Compiler
7292
  protected static $libMapGet = ['map', 'key'];
7293
  protected function libMapGet($args)
7294
  {
7295
- $map = $this->assertMap($args[0]);
7296
  $key = $args[1];
7297
 
7298
  if (! \is_null($key)) {
@@ -7311,7 +8639,7 @@ class Compiler
7311
  protected static $libMapKeys = ['map'];
7312
  protected function libMapKeys($args)
7313
  {
7314
- $map = $this->assertMap($args[0]);
7315
  $keys = $map[1];
7316
 
7317
  return [Type::T_LIST, ',', $keys];
@@ -7320,21 +8648,28 @@ class Compiler
7320
  protected static $libMapValues = ['map'];
7321
  protected function libMapValues($args)
7322
  {
7323
- $map = $this->assertMap($args[0]);
7324
  $values = $map[2];
7325
 
7326
  return [Type::T_LIST, ',', $values];
7327
  }
7328
 
7329
- protected static $libMapRemove = ['map', 'key...'];
 
 
 
7330
  protected function libMapRemove($args)
7331
  {
7332
- $map = $this->assertMap($args[0]);
7333
- $keyList = $this->assertList($args[1]);
 
 
 
7334
 
7335
  $keys = [];
 
7336
 
7337
- foreach ($keyList[2] as $key) {
7338
  $keys[] = $this->compileStringContent($this->coerceString($key));
7339
  }
7340
 
@@ -7351,8 +8686,19 @@ class Compiler
7351
  protected static $libMapHasKey = ['map', 'key'];
7352
  protected function libMapHasKey($args)
7353
  {
7354
- $map = $this->assertMap($args[0]);
7355
- $key = $this->compileStringContent($this->coerceString($args[1]));
 
 
 
 
 
 
 
 
 
 
 
7356
 
7357
  for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
7358
  if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
@@ -7369,8 +8715,8 @@ class Compiler
7369
  ];
7370
  protected function libMapMerge($args)
7371
  {
7372
- $map1 = $this->assertMap($args[0]);
7373
- $map2 = $this->assertMap($args[1]);
7374
 
7375
  foreach ($map2[1] as $i2 => $key2) {
7376
  $key = $this->compileStringContent($this->coerceString($key2));
@@ -7392,12 +8738,18 @@ class Compiler
7392
  protected static $libKeywords = ['args'];
7393
  protected function libKeywords($args)
7394
  {
7395
- $this->assertList($args[0]);
 
 
 
 
 
 
7396
 
7397
  $keys = [];
7398
  $values = [];
7399
 
7400
- foreach ($args[0][2] as $name => $arg) {
7401
  $keys[] = [Type::T_KEYWORD, $name];
7402
  $values[] = $arg;
7403
  }
@@ -7412,12 +8764,19 @@ class Compiler
7412
  $this->coerceList($list, ' ');
7413
 
7414
  if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') {
7415
- return true;
7416
  }
7417
 
7418
- return false;
7419
  }
7420
 
 
 
 
 
 
 
 
7421
  protected function listSeparatorForJoin($list1, $sep)
7422
  {
7423
  if (! isset($sep)) {
@@ -7499,21 +8858,23 @@ class Compiler
7499
  return $res;
7500
  }
7501
 
 
7502
  protected function libZip($args)
7503
  {
7504
- foreach ($args as $key => $arg) {
7505
- $args[$key] = $this->coerceList($arg);
 
7506
  }
7507
 
7508
  $lists = [];
7509
- $firstList = array_shift($args);
7510
 
7511
  $result = [Type::T_LIST, ',', $lists];
7512
  if (! \is_null($firstList)) {
7513
  foreach ($firstList[2] as $key => $item) {
7514
  $list = [Type::T_LIST, '', [$item]];
7515
 
7516
- foreach ($args as $arg) {
7517
  if (isset($arg[2][$key])) {
7518
  $list[2][] = $arg[2][$key];
7519
  } else {
@@ -7537,6 +8898,16 @@ class Compiler
7537
  {
7538
  $value = $args[0];
7539
 
 
 
 
 
 
 
 
 
 
 
7540
  switch ($value[0]) {
7541
  case Type::T_KEYWORD:
7542
  if ($value === static::$true || $value === static::$false) {
@@ -7555,7 +8926,7 @@ class Compiler
7555
  return 'function';
7556
 
7557
  case Type::T_LIST:
7558
- if (isset($value[3]) && $value[3]) {
7559
  return 'arglist';
7560
  }
7561
 
@@ -7568,21 +8939,17 @@ class Compiler
7568
  protected static $libUnit = ['number'];
7569
  protected function libUnit($args)
7570
  {
7571
- $num = $args[0];
7572
-
7573
- if ($num[0] === Type::T_NUMBER) {
7574
- return [Type::T_STRING, '"', [$num->unitStr()]];
7575
- }
7576
 
7577
- return '';
7578
  }
7579
 
7580
  protected static $libUnitless = ['number'];
7581
  protected function libUnitless($args)
7582
  {
7583
- $value = $args[0];
7584
 
7585
- return $value[0] === Type::T_NUMBER && $value->unitless();
7586
  }
7587
 
7588
  protected static $libComparable = [
@@ -7594,16 +8961,13 @@ class Compiler
7594
  list($number1, $number2) = $args;
7595
 
7596
  if (
7597
- ! isset($number1[0]) || $number1[0] !== Type::T_NUMBER ||
7598
- ! isset($number2[0]) || $number2[0] !== Type::T_NUMBER
7599
  ) {
7600
  throw $this->error('Invalid argument(s) for "comparable"');
7601
  }
7602
 
7603
- $number1 = $number1->normalize();
7604
- $number2 = $number2->normalize();
7605
-
7606
- return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless();
7607
  }
7608
 
7609
  protected static $libStrIndex = ['string', 'substring'];
@@ -7618,10 +8982,10 @@ class Compiler
7618
  if (! \strlen($substringContent)) {
7619
  $result = 0;
7620
  } else {
7621
- $result = strpos($stringContent, $substringContent);
7622
  }
7623
 
7624
- return $result === false ? static::$null : new Node\Number($result + 1, '');
7625
  }
7626
 
7627
  protected static $libStrInsert = ['string', 'insert', 'index'];
@@ -7656,31 +9020,43 @@ class Compiler
7656
  $string = $this->assertString($args[0], 'string');
7657
  $stringContent = $this->compileStringContent($string);
7658
 
7659
- return new Node\Number(Util::mbStrlen($stringContent), '');
7660
  }
7661
 
7662
  protected static $libStrSlice = ['string', 'start-at', 'end-at:-1'];
7663
  protected function libStrSlice($args)
7664
  {
7665
- if (isset($args[2]) && ! $args[2][1]) {
7666
- return static::$nullString;
 
 
 
 
 
 
 
 
 
 
7667
  }
7668
 
7669
- $string = $this->coerceString($args[0]);
7670
- $stringContent = $this->compileStringContent($string);
 
7671
 
7672
- $start = (int) $args[1][1];
 
 
 
 
7673
 
7674
- if ($start > 0) {
7675
- $start--;
7676
  }
7677
 
7678
- $end = isset($args[2]) ? (int) $args[2][1] : -1;
7679
- $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end);
7680
 
7681
- $string[2] = $length
7682
- ? [substr($stringContent, $start, $length)]
7683
- : [substr($stringContent, $start)];
7684
 
7685
  return $string;
7686
  }
@@ -7688,10 +9064,10 @@ class Compiler
7688
  protected static $libToLowerCase = ['string'];
7689
  protected function libToLowerCase($args)
7690
  {
7691
- $string = $this->coerceString($args[0]);
7692
  $stringContent = $this->compileStringContent($string);
7693
 
7694
- $string[2] = [\function_exists('mb_strtolower') ? mb_strtolower($stringContent) : strtolower($stringContent)];
7695
 
7696
  return $string;
7697
  }
@@ -7699,18 +9075,45 @@ class Compiler
7699
  protected static $libToUpperCase = ['string'];
7700
  protected function libToUpperCase($args)
7701
  {
7702
- $string = $this->coerceString($args[0]);
7703
  $stringContent = $this->compileStringContent($string);
7704
 
7705
- $string[2] = [\function_exists('mb_strtoupper') ? mb_strtoupper($stringContent) : strtoupper($stringContent)];
7706
 
7707
  return $string;
7708
  }
7709
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7710
  protected static $libFeatureExists = ['feature'];
7711
  protected function libFeatureExists($args)
7712
  {
7713
- $string = $this->coerceString($args[0]);
7714
  $name = $this->compileStringContent($string);
7715
 
7716
  return $this->toBool(
@@ -7721,18 +9124,18 @@ class Compiler
7721
  protected static $libFunctionExists = ['name'];
7722
  protected function libFunctionExists($args)
7723
  {
7724
- $string = $this->coerceString($args[0]);
7725
  $name = $this->compileStringContent($string);
7726
 
7727
  // user defined functions
7728
  if ($this->has(static::$namespaces['function'] . $name)) {
7729
- return true;
7730
  }
7731
 
7732
  $name = $this->normalizeName($name);
7733
 
7734
  if (isset($this->userFunctions[$name])) {
7735
- return true;
7736
  }
7737
 
7738
  // built-in functions
@@ -7744,30 +9147,31 @@ class Compiler
7744
  protected static $libGlobalVariableExists = ['name'];
7745
  protected function libGlobalVariableExists($args)
7746
  {
7747
- $string = $this->coerceString($args[0]);
7748
  $name = $this->compileStringContent($string);
7749
 
7750
- return $this->has($name, $this->rootEnv);
7751
  }
7752
 
7753
  protected static $libMixinExists = ['name'];
7754
  protected function libMixinExists($args)
7755
  {
7756
- $string = $this->coerceString($args[0]);
7757
  $name = $this->compileStringContent($string);
7758
 
7759
- return $this->has(static::$namespaces['mixin'] . $name);
7760
  }
7761
 
7762
  protected static $libVariableExists = ['name'];
7763
  protected function libVariableExists($args)
7764
  {
7765
- $string = $this->coerceString($args[0]);
7766
  $name = $this->compileStringContent($string);
7767
 
7768
- return $this->has($name);
7769
  }
7770
 
 
7771
  /**
7772
  * Workaround IE7's content counter bug.
7773
  *
@@ -7777,7 +9181,7 @@ class Compiler
7777
  */
7778
  protected function libCounter($args)
7779
  {
7780
- $list = array_map([$this, 'compileValue'], $args);
7781
 
7782
  return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']];
7783
  }
@@ -7785,24 +9189,21 @@ class Compiler
7785
  protected static $libRandom = ['limit:null'];
7786
  protected function libRandom($args)
7787
  {
7788
- if (isset($args[0]) & $args[0] !== static::$null) {
7789
- $n = $this->assertNumber($args[0]);
7790
 
7791
  if ($n < 1) {
7792
- throw $this->error("\$limit must be greater than or equal to 1");
7793
- }
7794
-
7795
- if (round($n - \intval($n), Node\Number::PRECISION) > 0) {
7796
- throw $this->error("Expected \$limit to be an integer but got $n for `random`");
7797
  }
7798
 
7799
- return new Node\Number(mt_rand(1, \intval($n)), '');
7800
  }
7801
 
7802
  $max = mt_getrandmax();
7803
- return new Node\Number(mt_rand(0, $max - 1) / $max, '');
7804
  }
7805
 
 
7806
  protected function libUniqueId()
7807
  {
7808
  static $id;
@@ -7818,6 +9219,12 @@ class Compiler
7818
  return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
7819
  }
7820
 
 
 
 
 
 
 
7821
  protected function inspectFormatValue($value, $force_enclosing_display = false)
7822
  {
7823
  if ($value === static::$null) {
@@ -7826,6 +9233,10 @@ class Compiler
7826
 
7827
  $stringValue = [$value];
7828
 
 
 
 
 
7829
  if ($value[0] === Type::T_LIST) {
7830
  if (end($value[2]) === static::$null) {
7831
  array_pop($value[2]);
@@ -7864,9 +9275,11 @@ class Compiler
7864
  /**
7865
  * Preprocess selector args
7866
  *
7867
- * @param array $arg
 
 
7868
  *
7869
- * @return array|boolean
7870
  */
7871
  protected function getSelectorArg($arg, $varname = null, $allowParent = false)
7872
  {
@@ -7877,18 +9290,19 @@ class Compiler
7877
  }
7878
 
7879
  if (! $this->checkSelectorArgType($arg)) {
7880
- $var_display = ($varname ? ' $' . $varname . ':' : '');
7881
  $var_value = $this->compileValue($arg);
7882
- throw $this->error("Error:{$var_display} $var_value is not a valid selector: it must be a string,"
7883
- . " a list of strings, or a list of lists of strings");
7884
  }
7885
 
7886
- $arg = $this->libUnquote([$arg]);
 
 
 
7887
  $arg = $this->compileValue($arg);
7888
 
7889
  $parsedSelector = [];
7890
 
7891
- if ($parser->parseSelector($arg, $parsedSelector)) {
7892
  $selector = $this->evalSelectors($parsedSelector);
7893
  $gluedSelector = $this->glueFunctionSelectors($selector);
7894
 
@@ -7896,8 +9310,7 @@ class Compiler
7896
  foreach ($gluedSelector as $selector) {
7897
  foreach ($selector as $s) {
7898
  if (in_array(static::$selfSelector, $s)) {
7899
- $var_display = ($varname ? ' $' . $varname . ':' : '');
7900
- throw $this->error("Error:{$var_display} Parent selectors aren't allowed here.");
7901
  }
7902
  }
7903
  }
@@ -7906,8 +9319,7 @@ class Compiler
7906
  return $gluedSelector;
7907
  }
7908
 
7909
- $var_display = ($varname ? ' $' . $varname . ':' : '');
7910
- throw $this->error("Error:{$var_display} expected more input, invalid selector.");
7911
  }
7912
 
7913
  /**
@@ -7937,11 +9349,11 @@ class Compiler
7937
  *
7938
  * @param array $selectors
7939
  *
7940
- * @return string
7941
  */
7942
  protected function formatOutputSelector($selectors)
7943
  {
7944
- $selectors = $this->collapseSelectors($selectors, true);
7945
 
7946
  return $selectors;
7947
  }
@@ -7954,7 +9366,7 @@ class Compiler
7954
  $super = $this->getSelectorArg($super, 'super');
7955
  $sub = $this->getSelectorArg($sub, 'sub');
7956
 
7957
- return $this->isSuperSelector($super, $sub);
7958
  }
7959
 
7960
  /**
@@ -8197,6 +9609,10 @@ class Compiler
8197
  $this->extendsMap = [];
8198
 
8199
  foreach ($extendee as $es) {
 
 
 
 
8200
  // only use the first one
8201
  $this->pushExtends(reset($es), $extender, null);
8202
  }
@@ -8297,7 +9713,7 @@ class Compiler
8297
  * @param array $compound1
8298
  * @param array $compound2
8299
  *
8300
- * @return array|mixed
8301
  */
8302
  protected function unifyCompoundSelectors($compound1, $compound2)
8303
  {
@@ -8413,7 +9829,7 @@ class Compiler
8413
  * @param array $part
8414
  * @param array $compound
8415
  *
8416
- * @return array|boolean
8417
  */
8418
  protected function matchPartInCompound($part, $compound)
8419
  {
@@ -8508,7 +9924,7 @@ class Compiler
8508
  * @param string $tag1
8509
  * @param string $tag2
8510
  *
8511
- * @return array|boolean
8512
  */
8513
  protected function checkCompatibleTags($tag1, $tag2)
8514
  {
@@ -8531,9 +9947,9 @@ class Compiler
8531
  /**
8532
  * Find the html tag name in a selector parts list
8533
  *
8534
- * @param array $parts
8535
  *
8536
- * @return mixed|string
8537
  */
8538
  protected function findTagName($parts)
8539
  {
@@ -8570,7 +9986,11 @@ class Compiler
8570
  protected static $libScssphpGlob = ['pattern'];
8571
  protected function libScssphpGlob($args)
8572
  {
8573
- $string = $this->coerceString($args[0]);
 
 
 
 
8574
  $pattern = $this->compileStringContent($string);
8575
  $matches = glob($pattern);
8576
  $listParts = [];
13
  namespace ScssPhp\ScssPhp;
14
 
15
  use ScssPhp\ScssPhp\Base\Range;
16
+ use ScssPhp\ScssPhp\Compiler\CachedResult;
 
 
17
  use ScssPhp\ScssPhp\Compiler\Environment;
18
  use ScssPhp\ScssPhp\Exception\CompilerException;
19
+ use ScssPhp\ScssPhp\Exception\ParserException;
20
+ use ScssPhp\ScssPhp\Exception\SassException;
21
+ use ScssPhp\ScssPhp\Exception\SassScriptException;
22
+ use ScssPhp\ScssPhp\Formatter\Compressed;
23
+ use ScssPhp\ScssPhp\Formatter\Expanded;
24
  use ScssPhp\ScssPhp\Formatter\OutputBlock;
25
+ use ScssPhp\ScssPhp\Logger\LoggerInterface;
26
+ use ScssPhp\ScssPhp\Logger\StreamLogger;
27
+ use ScssPhp\ScssPhp\Node\Number;
28
  use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
29
+ use ScssPhp\ScssPhp\Util\Path;
 
 
30
 
31
  /**
32
  * The scss compiler and parser.
59
  * SCSS compiler
60
  *
61
  * @author Leaf Corcoran <leafot@gmail.com>
62
+ *
63
+ * @final Extending the Compiler is deprecated
64
  */
65
  class Compiler
66
  {
67
+ /**
68
+ * @deprecated
69
+ */
70
  const LINE_COMMENTS = 1;
71
+ /**
72
+ * @deprecated
73
+ */
74
  const DEBUG_INFO = 2;
75
 
76
+ /**
77
+ * @deprecated
78
+ */
79
  const WITH_RULE = 1;
80
+ /**
81
+ * @deprecated
82
+ */
83
  const WITH_MEDIA = 2;
84
+ /**
85
+ * @deprecated
86
+ */
87
  const WITH_SUPPORTS = 4;
88
+ /**
89
+ * @deprecated
90
+ */
91
  const WITH_ALL = 7;
92
 
93
  const SOURCE_MAP_NONE = 0;
95
  const SOURCE_MAP_FILE = 2;
96
 
97
  /**
98
+ * @var array<string, string>
99
  */
100
  protected static $operatorNames = [
101
  '+' => 'add',
111
 
112
  '<=' => 'lte',
113
  '>=' => 'gte',
 
114
  ];
115
 
116
  /**
117
+ * @var array<string, string>
118
  */
119
  protected static $namespaces = [
120
  'special' => '%',
124
 
125
  public static $true = [Type::T_KEYWORD, 'true'];
126
  public static $false = [Type::T_KEYWORD, 'false'];
127
+ /** @deprecated */
128
  public static $NaN = [Type::T_KEYWORD, 'NaN'];
129
+ /** @deprecated */
130
  public static $Infinity = [Type::T_KEYWORD, 'Infinity'];
131
  public static $null = [Type::T_NULL];
132
  public static $nullString = [Type::T_STRING, '', []];
138
  public static $with = [Type::T_KEYWORD, 'with'];
139
  public static $without = [Type::T_KEYWORD, 'without'];
140
 
141
+ /**
142
+ * @var array<int, string|callable>
143
+ */
144
+ protected $importPaths = [];
145
+ /**
146
+ * @var array<string, Block>
147
+ */
148
  protected $importCache = [];
149
+
150
+ /**
151
+ * @var string[]
152
+ */
153
  protected $importedFiles = [];
154
+
155
+ /**
156
+ * @var array
157
+ * @phpstan-var array<string, array{0: callable, 1: array|null}>
158
+ */
159
  protected $userFunctions = [];
160
+ /**
161
+ * @var array<string, mixed>
162
+ */
163
  protected $registeredVars = [];
164
+ /**
165
+ * @var array<string, bool>
166
+ */
167
  protected $registeredFeatures = [
168
  'extend-selector-pseudoclass' => false,
169
  'at-error' => true,
170
+ 'units-level-3' => true,
171
  'global-variable-shadowing' => false,
172
  ];
173
 
174
+ /**
175
+ * @var string|null
176
+ */
177
  protected $encoding = null;
178
+ /**
179
+ * @var null
180
+ * @deprecated
181
+ */
182
  protected $lineNumberStyle = null;
183
 
184
+ /**
185
+ * @var int|SourceMapGenerator
186
+ * @phpstan-var self::SOURCE_MAP_*|SourceMapGenerator
187
+ */
188
  protected $sourceMap = self::SOURCE_MAP_NONE;
189
+
190
+ /**
191
+ * @var array
192
+ * @phpstan-var array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string}
193
+ */
194
  protected $sourceMapOptions = [];
195
 
196
  /**
197
  * @var string|\ScssPhp\ScssPhp\Formatter
198
  */
199
+ protected $formatter = Expanded::class;
200
 
201
+ /**
202
+ * @var Environment
203
+ */
204
  protected $rootEnv;
205
+ /**
206
+ * @var OutputBlock|null
207
+ */
208
  protected $rootBlock;
209
 
210
  /**
211
  * @var \ScssPhp\ScssPhp\Compiler\Environment
212
  */
213
  protected $env;
214
+ /**
215
+ * @var OutputBlock|null
216
+ */
217
  protected $scope;
218
+ /**
219
+ * @var Environment|null
220
+ */
221
  protected $storeEnv;
222
+ /**
223
+ * @var bool|null
224
+ */
225
  protected $charsetSeen;
226
+ /**
227
+ * @var array<int, string|null>
228
+ */
229
  protected $sourceNames;
230
 
231
+ /**
232
+ * @var Cache|null
233
+ */
234
  protected $cache;
235
 
236
+ /**
237
+ * @var bool
238
+ */
239
+ protected $cacheCheckImportResolutions = false;
240
+
241
+ /**
242
+ * @var int
243
+ */
244
  protected $indentLevel;
245
+ /**
246
+ * @var array[]
247
+ */
248
  protected $extends;
249
+ /**
250
+ * @var array<string, int[]>
251
+ */
252
  protected $extendsMap;
253
+
254
+ /**
255
+ * @var array<string, int>
256
+ */
257
+ protected $parsedFiles = [];
258
+
259
+ /**
260
+ * @var Parser|null
261
+ */
262
  protected $parser;
263
+ /**
264
+ * @var int|null
265
+ */
266
  protected $sourceIndex;
267
+ /**
268
+ * @var int|null
269
+ */
270
  protected $sourceLine;
271
+ /**
272
+ * @var int|null
273
+ */
274
  protected $sourceColumn;
275
+ /**
276
+ * @var bool|null
277
+ */
278
  protected $shouldEvaluate;
279
+ /**
280
+ * @var null
281
+ * @deprecated
282
+ */
283
  protected $ignoreErrors;
284
+ /**
285
+ * @var bool
286
+ */
287
  protected $ignoreCallStackMessage = false;
288
 
289
+ /**
290
+ * @var array[]
291
+ */
292
  protected $callStack = [];
293
 
294
+ /**
295
+ * @var array
296
+ * @phpstan-var list<array{currentDir: string|null, path: string, filePath: string}>
297
+ */
298
+ private $resolvedImports = [];
299
+
300
+ /**
301
+ * The directory of the currently processed file
302
+ *
303
+ * @var string|null
304
+ */
305
+ private $currentDirectory;
306
+
307
+ /**
308
+ * The directory of the input file
309
+ *
310
+ * @var string
311
+ */
312
+ private $rootDirectory;
313
+
314
+ /**
315
+ * @var bool
316
+ */
317
+ private $legacyCwdImportPath = true;
318
+
319
+ /**
320
+ * @var LoggerInterface
321
+ */
322
+ private $logger;
323
+
324
+ /**
325
+ * @var array<string, bool>
326
+ */
327
+ private $warnedChildFunctions = [];
328
+
329
  /**
330
  * Constructor
331
  *
332
  * @param array|null $cacheOptions
333
+ * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string, checkImportResolutions?: bool}|null $cacheOptions
334
  */
335
  public function __construct($cacheOptions = null)
336
  {
 
337
  $this->sourceNames = [];
338
 
339
  if ($cacheOptions) {
340
  $this->cache = new Cache($cacheOptions);
341
+ if (!empty($cacheOptions['checkImportResolutions'])) {
342
+ $this->cacheCheckImportResolutions = true;
343
+ }
344
  }
345
 
346
+ $this->logger = new StreamLogger(fopen('php://stderr', 'w'), true);
347
  }
348
 
349
  /**
350
  * Get compiler options
351
  *
352
+ * @return array<string, mixed>
353
+ *
354
+ * @internal
355
  */
356
  public function getCompileOptions()
357
  {
363
  'sourceMap' => serialize($this->sourceMap),
364
  'sourceMapOptions' => $this->sourceMapOptions,
365
  'formatter' => $this->formatter,
366
+ 'legacyImportPath' => $this->legacyCwdImportPath,
367
  ];
368
 
369
  return $options;
370
  }
371
 
372
+ /**
373
+ * Sets an alternative logger.
374
+ *
375
+ * Changing the logger in the middle of the compilation is not
376
+ * supported and will result in an undefined behavior.
377
+ *
378
+ * @param LoggerInterface $logger
379
+ *
380
+ * @return void
381
+ */
382
+ public function setLogger(LoggerInterface $logger)
383
+ {
384
+ $this->logger = $logger;
385
+ }
386
+
387
  /**
388
  * Set an alternative error output stream, for testing purpose only
389
  *
390
  * @param resource $handle
391
+ *
392
+ * @return void
393
+ *
394
+ * @deprecated Use {@see setLogger} instead
395
  */
396
  public function setErrorOuput($handle)
397
  {
398
+ @trigger_error('The method "setErrorOuput" is deprecated. Use "setLogger" instead.', E_USER_DEPRECATED);
399
+
400
+ $this->logger = new StreamLogger($handle);
401
  }
402
 
403
  /**
404
  * Compile scss
405
  *
406
+ * @param string $code
407
+ * @param string|null $path
 
 
408
  *
409
  * @return string
410
+ *
411
+ * @throws SassException when the source fails to compile
412
+ *
413
+ * @deprecated Use {@see compileString} instead.
414
  */
415
  public function compile($code, $path = null)
416
  {
417
+ @trigger_error(sprintf('The "%s" method is deprecated. Use "compileString" instead.', __METHOD__), E_USER_DEPRECATED);
 
 
 
418
 
419
+ $result = $this->compileString($code, $path);
 
 
 
 
 
 
 
420
 
421
+ $sourceMap = $result->getSourceMap();
422
+
423
+ if ($sourceMap !== null) {
424
+ if ($this->sourceMap instanceof SourceMapGenerator) {
425
+ $this->sourceMap->saveMap($sourceMap);
426
+ } elseif ($this->sourceMap === self::SOURCE_MAP_FILE) {
427
+ $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
428
+ $sourceMapGenerator->saveMap($sourceMap);
429
  }
430
  }
431
 
432
+ return $result->getCss();
433
+ }
434
+
435
+ /**
436
+ * Compile scss
437
+ *
438
+ * @param string $source
439
+ * @param string|null $path
440
+ *
441
+ * @return CompilationResult
442
+ *
443
+ * @throws SassException when the source fails to compile
444
+ */
445
+ public function compileString($source, $path = null)
446
+ {
447
+ if ($this->cache) {
448
+ $cacheKey = ($path ? $path : '(stdin)') . ':' . md5($source);
449
+ $compileOptions = $this->getCompileOptions();
450
+ $cachedResult = $this->cache->getCache('compile', $cacheKey, $compileOptions);
451
+
452
+ if ($cachedResult instanceof CachedResult && $this->isFreshCachedResult($cachedResult)) {
453
+ return $cachedResult->getResult();
454
+ }
455
+ }
456
 
457
  $this->indentLevel = -1;
458
  $this->extends = [];
466
  $this->charsetSeen = null;
467
  $this->shouldEvaluate = null;
468
  $this->ignoreCallStackMessage = false;
469
+ $this->parsedFiles = [];
470
+ $this->importedFiles = [];
471
+ $this->resolvedImports = [];
472
 
473
+ if (!\is_null($path) && is_file($path)) {
474
+ $path = realpath($path) ?: $path;
475
+ $this->currentDirectory = dirname($path);
476
+ $this->rootDirectory = $this->currentDirectory;
477
+ } else {
478
+ $this->currentDirectory = null;
479
+ $this->rootDirectory = getcwd();
480
+ }
481
 
482
+ try {
483
+ $this->parser = $this->parserFactory($path);
484
+ $tree = $this->parser->parse($source);
485
+ $this->parser = null;
486
 
487
+ $this->formatter = new $this->formatter();
488
+ $this->rootBlock = null;
489
+ $this->rootEnv = $this->pushEnv($tree);
490
 
491
+ $warnCallback = function ($message, $deprecation) {
492
+ $this->logger->warn($message, $deprecation);
493
+ };
494
+ $previousWarnCallback = Warn::setCallback($warnCallback);
495
 
496
+ try {
497
+ $this->injectVariables($this->registeredVars);
498
+ $this->compileRoot($tree);
499
+ $this->popEnv();
500
+ } finally {
501
+ Warn::setCallback($previousWarnCallback);
502
  }
 
503
 
504
+ $sourceMapGenerator = null;
505
+
506
+ if ($this->sourceMap) {
507
+ if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) {
508
+ $sourceMapGenerator = $this->sourceMap;
509
+ $this->sourceMap = self::SOURCE_MAP_FILE;
510
+ } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
511
+ $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
512
+ }
513
+ }
514
 
515
+ $out = $this->formatter->format($this->scope, $sourceMapGenerator);
 
 
516
 
517
+ $prefix = '';
 
 
 
518
 
519
+ if (!$this->charsetSeen) {
520
+ if (strlen($out) !== Util::mbStrlen($out)) {
521
+ $prefix = '@charset "UTF-8";' . "\n";
522
+ $out = $prefix . $out;
523
+ }
524
  }
525
 
526
+ $sourceMap = null;
527
+
528
+ if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) {
529
+ $sourceMap = $sourceMapGenerator->generateJson($prefix);
530
+ $sourceMapUrl = null;
531
+
532
+ switch ($this->sourceMap) {
533
+ case self::SOURCE_MAP_INLINE:
534
+ $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap));
535
+ break;
536
+
537
+ case self::SOURCE_MAP_FILE:
538
+ if (isset($this->sourceMapOptions['sourceMapURL'])) {
539
+ $sourceMapUrl = $this->sourceMapOptions['sourceMapURL'];
540
+ }
541
+ break;
542
+ }
543
+
544
+ if ($sourceMapUrl !== null) {
545
+ $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
546
+ }
547
+ }
548
+ } catch (SassScriptException $e) {
549
+ throw new CompilerException($this->addLocationToMessage($e->getMessage()), 0, $e);
550
+ }
551
+
552
+ $includedFiles = [];
553
+
554
+ foreach ($this->resolvedImports as $resolvedImport) {
555
+ $includedFiles[$resolvedImport['filePath']] = $resolvedImport['filePath'];
556
  }
557
 
558
+ $result = new CompilationResult($out, $sourceMap, array_values($includedFiles));
559
+
560
  if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
561
+ $this->cache->setCache('compile', $cacheKey, new CachedResult($result, $this->parsedFiles, $this->resolvedImports), $compileOptions);
562
+ }
 
 
563
 
564
+ // Reset state to free memory
565
+ // TODO in 2.0, reset parsedFiles as well when the getter is removed.
566
+ $this->resolvedImports = [];
567
+ $this->importedFiles = [];
568
+
569
+ return $result;
570
+ }
571
+
572
+ /**
573
+ * @param CachedResult $result
574
+ *
575
+ * @return bool
576
+ */
577
+ private function isFreshCachedResult(CachedResult $result)
578
+ {
579
+ // check if any dependency file changed since the result was compiled
580
+ foreach ($result->getParsedFiles() as $file => $mtime) {
581
+ if (! is_file($file) || filemtime($file) !== $mtime) {
582
+ return false;
583
+ }
584
  }
585
 
586
+ if ($this->cacheCheckImportResolutions) {
587
+ $resolvedImports = [];
588
+
589
+ foreach ($result->getResolvedImports() as $import) {
590
+ $currentDir = $import['currentDir'];
591
+ $path = $import['path'];
592
+ // store the check across all the results in memory to avoid multiple findImport() on the same path
593
+ // with same context.
594
+ // this is happening in a same hit with multiple compilations (especially with big frameworks)
595
+ if (empty($resolvedImports[$currentDir][$path])) {
596
+ $resolvedImports[$currentDir][$path] = $this->findImport($path, $currentDir);
597
+ }
598
+
599
+ if ($resolvedImports[$currentDir][$path] !== $import['filePath']) {
600
+ return false;
601
+ }
602
+ }
603
+ }
604
+
605
+ return true;
606
  }
607
 
608
  /**
609
  * Instantiate parser
610
  *
611
+ * @param string|null $path
612
  *
613
  * @return \ScssPhp\ScssPhp\Parser
614
  */
621
  // Otherwise, the CSS will be rendered as-is. It can even be extended!
622
  $cssOnly = false;
623
 
624
+ if ($path !== null && substr($path, -4) === '.css') {
625
  $cssOnly = true;
626
  }
627
 
628
+ $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly, $this->logger);
629
 
630
  $this->sourceNames[] = $path;
631
  $this->addParsedFile($path);
658
  * @param array $target
659
  * @param array $origin
660
  * @param array|null $block
661
+ *
662
+ * @return void
663
  */
664
  protected function pushExtends($target, $origin, $block)
665
  {
678
  /**
679
  * Make output block
680
  *
681
+ * @param string|null $type
682
+ * @param string[]|null $selectors
683
  *
684
  * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
685
  */
710
  * Compile root
711
  *
712
  * @param \ScssPhp\ScssPhp\Block $rootBlock
713
+ *
714
+ * @return void
715
  */
716
  protected function compileRoot(Block $rootBlock)
717
  {
724
 
725
  /**
726
  * Report missing selectors
727
+ *
728
+ * @return void
729
  */
730
  protected function missingSelectors()
731
  {
754
  *
755
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
756
  * @param string $parentKey
757
+ *
758
+ * @return void
759
  */
760
  protected function flattenSelectors(OutputBlock $block, $parentKey = null)
761
  {
811
  }
812
 
813
  /**
814
+ * Glue parts of :not( or :nth-child( ... that are in general split in selectors parts
815
  *
816
  * @param array $parts
817
  *
852
  * @param array $out
853
  * @param integer $from
854
  * @param boolean $initial
855
+ *
856
+ * @return void
857
  */
858
  protected function matchExtends($selector, &$out, $from = 0, $initial = true)
859
  {
1007
  *
1008
  * @param array $out
1009
  * @param array $extended
1010
+ *
1011
+ * @return void
1012
  */
1013
  protected function pushOrMergeExtentedSelector(&$out, $extended)
1014
  {
1100
  $buffer = $matches[2];
1101
  $parser = $this->parserFactory(__METHOD__);
1102
 
1103
+ if ($parser->parseSelector($buffer, $subSelectors, false)) {
1104
  foreach ($subSelectors as $ksub => $subSelector) {
1105
  $subExtended = [];
1106
  $this->matchExtends($subSelector, $subExtended, 0, false);
1260
  * Compile media
1261
  *
1262
  * @param \ScssPhp\ScssPhp\Block $media
1263
+ *
1264
+ * @return void
1265
  */
1266
  protected function compileMedia(Block $media)
1267
  {
1269
 
1270
  $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env));
1271
 
1272
+ if (! empty($mediaQueries)) {
1273
  $previousScope = $this->scope;
1274
  $parentScope = $this->mediaParent($this->scope);
1275
 
1309
  $wrapped->children = $media->children;
1310
 
1311
  $media->children = [[Type::T_BLOCK, $wrapped]];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1312
  }
1313
 
1314
  $this->compileChildrenNoReturn($media->children, $this->scope);
1342
  /**
1343
  * Compile directive
1344
  *
1345
+ * @param \ScssPhp\ScssPhp\Block|array $directive
1346
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1347
+ *
1348
+ * @return void
1349
  */
1350
  protected function compileDirective($directive, OutputBlock $out)
1351
  {
1387
  * directive names can include some interpolation
1388
  *
1389
  * @param string|array $directiveName
1390
+ * @return string
1391
  * @throws CompilerException
1392
  */
1393
  protected function compileDirectiveName($directiveName)
1403
  * Compile at-root
1404
  *
1405
  * @param \ScssPhp\ScssPhp\Block $block
1406
+ *
1407
+ * @return void
1408
  */
1409
  protected function compileAtRoot(Block $block)
1410
  {
1430
  }
1431
 
1432
  $selfParent = $block->selfParent;
1433
+ assert($selfParent !== null, 'at-root blocks must have a selfParent set.');
1434
 
1435
  if (
1436
+ ! $selfParent->selectors &&
1437
  isset($block->parent) && $block->parent &&
1438
  isset($block->parent->selectors) && $block->parent->selectors
1439
  ) {
1462
  * @param array $with
1463
  * @param array $without
1464
  *
1465
+ * @return OutputBlock
1466
  */
1467
  protected function filterScopeWithWithout($scope, $with, $without)
1468
  {
1469
  $filteredScopes = [];
1470
  $childStash = [];
1471
 
1472
+ if ($scope->type === Type::T_ROOT) {
1473
  return $scope;
1474
  }
1475
 
1476
  // start from the root
1477
+ while ($scope->parent && $scope->parent->type !== Type::T_ROOT) {
1478
  array_unshift($childStash, $scope);
1479
  $scope = $scope->parent;
1480
  }
1535
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1536
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope
1537
  *
1538
+ * @return OutputBlock
1539
  */
1540
  protected function completeScope($scope, $previousScope)
1541
  {
1602
  }
1603
  }
1604
 
1605
+ if ($this->mapHasKey($withCondition, static::$with)) {
1606
  $without = []; // cancel the default
1607
  $list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
1608
 
1613
  }
1614
  }
1615
 
1616
+ if ($this->mapHasKey($withCondition, static::$without)) {
1617
  $without = []; // cancel the default
1618
  $list = $this->coerceList($this->libMapGet([$withCondition, static::$without]));
1619
 
1631
  /**
1632
  * Filter env stack
1633
  *
1634
+ * @param Environment[] $envs
1635
  * @param array $with
1636
  * @param array $without
1637
  *
1638
+ * @return Environment
1639
+ *
1640
+ * @phpstan-param non-empty-array<Environment> $envs
1641
  */
1642
  protected function filterWithWithout($envs, $with, $without)
1643
  {
1692
  $s = reset($s);
1693
  }
1694
 
1695
+ if (\is_object($s) && $s instanceof Number) {
1696
  return $this->testWithWithout('keyframes', $with, $without);
1697
  }
1698
  }
1729
  * Compile keyframe block
1730
  *
1731
  * @param \ScssPhp\ScssPhp\Block $block
1732
+ * @param string[] $selectors
1733
+ *
1734
+ * @return void
1735
  */
1736
  protected function compileKeyframeBlock(Block $block, $selectors)
1737
  {
1760
  *
1761
  * @param \ScssPhp\ScssPhp\Block $block
1762
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1763
+ *
1764
+ * @return void
1765
  */
1766
  protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out)
1767
  {
1795
  * Compile nested block
1796
  *
1797
  * @param \ScssPhp\ScssPhp\Block $block
1798
+ * @param string[] $selectors
1799
+ *
1800
+ * @return void
1801
  */
1802
  protected function compileNestedBlock(Block $block, $selectors)
1803
  {
1859
  * @see Compiler::compileChild()
1860
  *
1861
  * @param \ScssPhp\ScssPhp\Block $block
1862
+ *
1863
+ * @return void
1864
  */
1865
  protected function compileBlock(Block $block)
1866
  {
1869
 
1870
  $out = $this->makeOutputBlock(null);
1871
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1872
  $this->scope->children[] = $out;
1873
 
1874
  if (\count($block->children)) {
1900
  * @param array $value
1901
  * @param boolean $pushEnv
1902
  *
1903
+ * @return string
1904
  */
1905
  protected function compileCommentValue($value, $pushEnv = false)
1906
  {
1911
  $this->pushEnv();
1912
  }
1913
 
 
 
 
1914
  try {
1915
  $c = $this->compileValue($value[2]);
1916
+ } catch (SassScriptException $e) {
1917
+ $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $this->addLocationToMessage($e->getMessage()), true);
1918
+ // ignore error in comment compilation which are only interpolation
1919
+ } catch (SassException $e) {
1920
+ $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $e->getMessage(), true);
1921
  // ignore error in comment compilation which are only interpolation
1922
  }
1923
 
 
 
1924
  if ($pushEnv) {
1925
  $this->popEnv();
1926
  }
1933
  * Compile root level comment
1934
  *
1935
  * @param array $block
1936
+ *
1937
+ * @return void
1938
  */
1939
  protected function compileComment($block)
1940
  {
1963
  $buffer = $this->collapseSelectors($selectors);
1964
  $parser = $this->parserFactory(__METHOD__);
1965
 
1966
+ try {
1967
+ $isValid = $parser->parseSelector($buffer, $newSelectors, true);
1968
+ } catch (ParserException $e) {
1969
+ throw $this->error($e->getMessage());
1970
+ }
1971
+
1972
+ if ($isValid) {
1973
  $selectors = array_map([$this, 'evalSelector'], $newSelectors);
1974
  }
1975
  }
2002
  if (\is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
2003
  $p = $this->compileValue($p);
2004
 
2005
+ // force re-evaluation if self char or non standard char
2006
+ if (preg_match(',[^\w-],', $p)) {
2007
  $this->shouldEvaluate = true;
2008
  }
2009
  } elseif (
2021
  /**
2022
  * Collapse selectors
2023
  *
2024
+ * @param array $selectors
 
 
 
2025
  *
2026
  * @return string
2027
  */
2028
+ protected function collapseSelectors($selectors)
2029
  {
2030
  $parts = [];
2031
 
2032
  foreach ($selectors as $selector) {
2033
  $output = [];
 
2034
 
2035
  foreach ($selector as $node) {
2036
  $compound = '';
2042
  }
2043
  );
2044
 
2045
+ $output[] = $compound;
 
 
 
 
 
 
 
 
 
 
 
 
 
2046
  }
2047
 
2048
+ $parts[] = implode(' ', $output);
2049
+ }
 
 
2050
 
2051
+ return implode(', ', $parts);
2052
+ }
2053
+
2054
+ /**
2055
+ * Collapse selectors
2056
+ *
2057
+ * @param array $selectors
2058
+ *
2059
+ * @return array
2060
+ */
2061
+ private function collapseSelectorsAsList($selectors)
2062
+ {
2063
+ $parts = [];
2064
+
2065
+ foreach ($selectors as $selector) {
2066
+ $output = [];
2067
+ $glueNext = false;
2068
+
2069
+ foreach ($selector as $node) {
2070
+ $compound = '';
2071
+
2072
+ array_walk_recursive(
2073
+ $node,
2074
+ function ($value, $key) use (&$compound) {
2075
+ $compound .= $value;
2076
+ }
2077
+ );
2078
+
2079
+ if ($this->isImmediateRelationshipCombinator($compound)) {
2080
+ if (\count($output)) {
2081
+ $output[\count($output) - 1] .= ' ' . $compound;
2082
+ } else {
2083
+ $output[] = $compound;
2084
+ }
2085
+
2086
+ $glueNext = true;
2087
+ } elseif ($glueNext) {
2088
+ $output[\count($output) - 1] .= ' ' . $compound;
2089
+ $glueNext = false;
2090
+ } else {
2091
+ $output[] = $compound;
2092
+ }
2093
  }
2094
 
2095
+ foreach ($output as &$o) {
2096
+ $o = [Type::T_STRING, '', [$o]];
2097
+ }
2098
 
2099
+ $parts[] = [Type::T_LIST, ' ', $output];
 
 
 
2100
  }
2101
 
2102
+ return [Type::T_LIST, ',', $parts];
2103
  }
2104
 
2105
  /**
2106
  * Parse down the selector and revert [self] to "&" before a reparsing
2107
  *
2108
+ * @param array $selectors
2109
+ * @param string|null $replace
2110
  *
2111
  * @return array
2112
  */
2234
  return false;
2235
  }
2236
 
2237
+ /**
2238
+ * @param string $name
2239
+ *
2240
+ * @return void
2241
+ */
2242
  protected function pushCallStack($name = '')
2243
  {
2244
  $this->callStack[] = [
2258
  }
2259
  }
2260
 
2261
+ /**
2262
+ * @return void
2263
+ */
2264
  protected function popCallStack()
2265
  {
2266
  array_pop($this->callStack);
2273
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2274
  * @param string $traceName
2275
  *
2276
+ * @return array|Number|null
2277
  */
2278
  protected function compileChildren($stms, OutputBlock $out, $traceName = '')
2279
  {
2295
  }
2296
 
2297
  /**
2298
+ * Compile children and throw exception if unexpected `@return`
2299
  *
2300
  * @param array $stms
2301
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2302
  * @param \ScssPhp\ScssPhp\Block $selfParent
2303
  * @param string $traceName
2304
  *
2305
+ * @return void
2306
+ *
2307
  * @throws \Exception
2308
  */
2309
  protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '')
2315
  $stm[1]->selfParent = $selfParent;
2316
  $ret = $this->compileChild($stm, $out);
2317
  $stm[1]->selfParent = null;
2318
+ } elseif ($selfParent && \in_array($stm[0], [Type::T_INCLUDE, Type::T_EXTEND])) {
2319
  $stm['selfParent'] = $selfParent;
2320
  $ret = $this->compileChild($stm, $out);
2321
  unset($stm['selfParent']);
2333
 
2334
 
2335
  /**
2336
+ * evaluate media query : compile internal value keeping the structure unchanged
2337
  *
2338
  * @param array $queryList
2339
  *
2403
  *
2404
  * @param array $queryList
2405
  *
2406
+ * @return string[]
2407
  */
2408
  protected function compileMediaQuery($queryList)
2409
  {
2641
  if ($rawPath[0] === Type::T_STRING) {
2642
  $path = $this->compileStringContent($rawPath);
2643
 
2644
+ if (strpos($path, 'url(') !== 0 && $filePath = $this->findImport($path, $this->currentDirectory)) {
2645
+ $this->registerImport($this->currentDirectory, $path, $filePath);
2646
+
2647
+ if (! $once || ! \in_array($filePath, $this->importedFiles)) {
2648
+ $this->importFile($filePath, $out);
2649
+ $this->importedFiles[] = $filePath;
2650
  }
2651
 
2652
  return true;
2684
  }
2685
 
2686
  /**
2687
+ * @param array $rawPath
2688
  * @return string
2689
  * @throws CompilerException
2690
  */
2692
  {
2693
  $path = $this->compileValue($rawPath);
2694
 
2695
+ // case url() without quotes : suppress \r \n remaining in the path
2696
  // if this is a real string there can not be CR or LF char
2697
  if (strpos($path, 'url(') === 0) {
2698
  $path = str_replace(array("\r", "\n"), array('', ' '), $path);
2699
  } else {
2700
+ // if this is a file name in a string, spaces should be escaped
2701
  $path = $this->reduce($rawPath);
2702
  $path = $this->escapeImportPathString($path);
2703
  $path = $this->compileValue($path);
2735
  * Append a root directive like @import or @charset as near as the possible from the source code
2736
  * (keeping before comments, @import and @charset coming before in the source code)
2737
  *
2738
+ * @param string $line
2739
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2740
+ * @param array $allowed
2741
+ *
2742
+ * @return void
2743
  */
2744
  protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT])
2745
  {
2787
  *
2788
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2789
  * @param string $type
2790
+ * @param string $line
2791
+ *
2792
+ * @return void
2793
  */
2794
  protected function appendOutputLine(OutputBlock $out, $type, $line)
2795
  {
2824
  * @param array $child
2825
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2826
  *
2827
+ * @return array|Number|null
2828
  */
2829
  protected function compileChild($child, OutputBlock $out)
2830
  {
2838
  $this->sourceColumn = $child[1]->sourceColumn;
2839
  } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
2840
  $this->sourceLine = $out->sourceLine;
2841
+ $sourceIndex = array_search($out->sourceName, $this->sourceNames);
2842
  $this->sourceColumn = $out->sourceColumn;
2843
 
2844
+ if ($sourceIndex === false) {
2845
+ $sourceIndex = null;
2846
  }
2847
+ $this->sourceIndex = $sourceIndex;
2848
  }
2849
 
2850
  switch ($child[0]) {
2969
  $divider = $this->reduce($divider, true);
2970
  }
2971
 
2972
+ if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) {
2973
  $revert = false;
2974
  }
2975
  }
2993
  $divider = $this->reduce($divider, true);
2994
  }
2995
 
2996
+ if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) {
2997
  $revert = false;
2998
  }
2999
  }
3051
 
3052
  case Type::T_EXTEND:
3053
  foreach ($child[1] as $sel) {
3054
+ $replacedSel = $this->replaceSelfSelector($sel);
3055
+
3056
+ if ($replacedSel !== $sel) {
3057
+ throw $this->error('Parent selectors aren\'t allowed here.');
3058
+ }
3059
+
3060
  $results = $this->evalSelectors([$sel]);
3061
 
3062
  foreach ($results as $result) {
3063
+ if (\count($result) !== 1) {
3064
+ throw $this->error('complex selectors may not be extended.');
3065
+ }
3066
+
3067
  // only use the first one
3068
+ $result = $result[0];
3069
  $selectors = $out->selectors;
3070
 
3071
  if (! $selectors && isset($child['selfParent'])) {
3115
  $ret = $this->compileChildren($each->children, $out);
3116
 
3117
  if ($ret) {
3118
+ $store = $this->env->store;
3119
+ $this->popEnv();
3120
+ $this->backPropagateEnv($store, $each->vars);
 
 
 
 
3121
 
3122
+ return $ret;
 
 
3123
  }
3124
  }
3125
  $store = $this->env->store;
3135
  $ret = $this->compileChildren($while->children, $out);
3136
 
3137
  if ($ret) {
3138
+ return $ret;
 
 
 
 
 
 
3139
  }
3140
  }
3141
  break;
3143
  case Type::T_FOR:
3144
  list(, $for) = $child;
3145
 
3146
+ $startNumber = $this->assertNumber($this->reduce($for->start, true));
3147
+ $endNumber = $this->assertNumber($this->reduce($for->end, true));
 
 
 
 
3148
 
3149
+ $start = $this->assertInteger($startNumber);
 
 
3150
 
3151
+ $numeratorUnits = $startNumber->getNumeratorUnits();
3152
+ $denominatorUnits = $startNumber->getDenominatorUnits();
 
3153
 
3154
+ $end = $this->assertInteger($endNumber->coerce($numeratorUnits, $denominatorUnits));
 
 
3155
 
3156
  $d = $start < $end ? 1 : -1;
3157
 
3165
  break;
3166
  }
3167
 
3168
+ $this->set($for->var, new Number($start, $numeratorUnits, $denominatorUnits));
3169
  $start += $d;
3170
 
3171
  $ret = $this->compileChildren($for->children, $out);
3172
 
3173
  if ($ret) {
3174
+ $store = $this->env->store;
3175
+ $this->popEnv();
3176
+ $this->backPropagateEnv($store, [$for->var]);
 
 
 
 
3177
 
3178
+ return $ret;
 
 
3179
  }
3180
  }
3181
 
3185
 
3186
  break;
3187
 
 
 
 
 
 
 
3188
  case Type::T_RETURN:
3189
  return $this->reduce($child[1], true);
3190
 
3299
  case Type::T_DEBUG:
3300
  list(, $value) = $child;
3301
 
3302
+ $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
3303
  $line = $this->sourceLine;
3304
  $value = $this->compileDebugValue($value);
3305
 
3306
+ $this->logger->debug("$fname:$line DEBUG: $value");
3307
  break;
3308
 
3309
  case Type::T_WARN:
3310
  list(, $value) = $child;
3311
 
3312
+ $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
3313
  $line = $this->sourceLine;
3314
  $value = $this->compileDebugValue($value);
3315
 
3316
+ $this->logger->warn("$value\n on line $line of $fname");
3317
  break;
3318
 
3319
  case Type::T_ERROR:
3320
  list(, $value) = $child;
3321
 
3322
+ $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
3323
  $line = $this->sourceLine;
3324
  $value = $this->compileValue($this->reduce($value, true));
3325
 
3326
  throw $this->error("File $fname on line $line ERROR: $value\n");
3327
 
 
 
 
3328
  default:
3329
  throw $this->error("unknown child type: $child[0]");
3330
  }
3334
  * Reduce expression to string
3335
  *
3336
  * @param array $exp
3337
+ * @param bool $keepParens
3338
  *
3339
  * @return array
3340
  */
3372
  /**
3373
  * Is truthy?
3374
  *
3375
+ * @param array|Number $value
3376
  *
3377
  * @return boolean
3378
  */
3379
+ public function isTruthy($value)
3380
  {
3381
  return $value !== static::$false && $value !== static::$null;
3382
  }
3420
  /**
3421
  * Reduce value
3422
  *
3423
+ * @param array|Number $value
3424
  * @param boolean $inExp
3425
  *
3426
+ * @return array|Number
3427
  */
3428
  protected function reduce($value, $inExp = false)
3429
  {
3430
+ if ($value instanceof Number) {
3431
+ return $value;
3432
  }
3433
 
3434
  switch ($value[0]) {
3446
 
3447
  // special case: looks like css shorthand
3448
  if (
3449
+ $opName == 'div' && ! $inParens && ! $inExp &&
3450
+ (($right[0] !== Type::T_NUMBER && isset($right[2]) && $right[2] != '') ||
3451
  ($right[0] === Type::T_NUMBER && ! $right->unitless()))
3452
  ) {
3453
  return $this->expToString($value);
3477
  \is_callable([$this, $fn]) &&
3478
  $genOp = true)
3479
  ) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3480
  $shouldEval = $inParens || $inExp;
3481
 
3482
  if (isset($passOp)) {
3486
  }
3487
 
3488
  if (isset($out)) {
 
 
 
 
3489
  return $out;
3490
  }
3491
  }
3498
  $inExp = $inExp || $this->shouldEval($exp);
3499
  $exp = $this->reduce($exp);
3500
 
3501
+ if ($exp instanceof Number) {
3502
  switch ($op) {
3503
  case '+':
3504
+ return $exp;
3505
 
3506
  case '-':
3507
+ return $exp->unaryMinus();
3508
  }
3509
  }
3510
 
3529
  foreach ($value[2] as &$item) {
3530
  $item = $this->reduce($item);
3531
  }
3532
+ unset($item);
3533
+
3534
+ if (isset($value[3]) && \is_array($value[3])) {
3535
+ foreach ($value[3] as &$item) {
3536
+ $item = $this->reduce($item);
3537
+ }
3538
+ unset($item);
3539
+ }
3540
 
3541
  return $value;
3542
 
3553
 
3554
  case Type::T_STRING:
3555
  foreach ($value[2] as &$item) {
3556
+ if (\is_array($item) || $item instanceof Number) {
3557
  $item = $this->reduce($item);
3558
  }
3559
  }
3564
  $value[1] = $this->reduce($value[1]);
3565
 
3566
  if ($inExp) {
3567
+ return [Type::T_KEYWORD, $this->compileValue($value, false)];
3568
  }
3569
 
3570
  return $value;
3575
  case Type::T_SELF:
3576
  $selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null;
3577
  $selfSelector = $this->multiplySelectors($this->env, $selfParent);
3578
+ $selfSelector = $this->collapseSelectorsAsList($selfSelector);
3579
 
3580
  return $selfSelector;
3581
 
3587
  /**
3588
  * Function caller
3589
  *
3590
+ * @param string|array $functionReference
3591
+ * @param array $argValues
3592
  *
3593
+ * @return array|Number
3594
  */
3595
  protected function fncall($functionReference, $argValues)
3596
  {
3635
 
3636
  // special cases of css valid functions min/max
3637
  $name = strtolower($name);
3638
+ if (\in_array($name, ['min', 'max']) && count($argValues) >= 1) {
3639
  $cssFunction = $this->cssValidArg(
3640
  [Type::T_FUNCTION_CALL, $name, $argValues],
3641
  ['min', 'max', 'calc', 'env', 'var']
3657
  }
3658
  }
3659
 
3660
+ /**
3661
+ * @param array|Number $arg
3662
+ * @param string[] $allowed_function
3663
+ * @param bool $inFunction
3664
+ *
3665
+ * @return array|Number|false
3666
+ */
3667
  protected function cssValidArg($arg, $allowed_function = [], $inFunction = false)
3668
  {
3669
+ if ($arg instanceof Number) {
3670
+ return $this->stringifyFncallArgs($arg);
3671
+ }
3672
+
3673
  switch ($arg[0]) {
3674
  case Type::T_INTERPOLATE:
3675
  return [Type::T_KEYWORD, $this->CompileValue($arg)];
3714
  }
3715
  return $this->stringifyFncallArgs($arg);
3716
 
 
 
 
3717
  case Type::T_LIST:
3718
  if (!$inFunction) {
3719
  return false;
3751
 
3752
  /**
3753
  * Reformat fncall arguments to proper css function output
3754
+ *
3755
+ * @param array|Number $arg
3756
+ *
3757
+ * @return array|Number
3758
  */
3759
  protected function stringifyFncallArgs($arg)
3760
  {
3761
+ if ($arg instanceof Number) {
3762
+ return $arg;
3763
+ }
3764
 
3765
  switch ($arg[0]) {
3766
  case Type::T_LIST:
3825
  $libName = $f[1];
3826
  $prototype = isset(static::$$libName) ? static::$$libName : null;
3827
 
3828
+ if (\get_class($this) !== __CLASS__ && !isset($this->warnedChildFunctions[$libName])) {
3829
+ $r = new \ReflectionMethod($this, $libName);
3830
+ $declaringClass = $r->getDeclaringClass()->name;
3831
+
3832
+ $needsWarning = $this->warnedChildFunctions[$libName] = $declaringClass !== __CLASS__;
3833
+
3834
+ if ($needsWarning) {
3835
+ if (method_exists(__CLASS__, $libName)) {
3836
+ @trigger_error(sprintf('Overriding the "%s" core function by extending the Compiler is deprecated and will be unsupported in 2.0. Remove the "%s::%s" method.', $normalizedName, $declaringClass, $libName), E_USER_DEPRECATED);
3837
+ } else {
3838
+ @trigger_error(sprintf('Registering custom functions by extending the Compiler and using the lib* discovery mechanism is deprecated and will be removed in 2.0. Replace the "%s::%s" method with registering the "%s" function through "Compiler::registerFunction".', $declaringClass, $libName, $normalizedName), E_USER_DEPRECATED);
3839
+ }
3840
+ }
3841
+ }
3842
+
3843
  return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype];
3844
  }
3845
 
3862
  /**
3863
  * Normalize value
3864
  *
3865
+ * @internal
3866
  *
3867
+ * @param array|Number $value
3868
+ *
3869
+ * @return array|Number
3870
  */
3871
  public function normalizeValue($value)
3872
  {
3873
  $value = $this->coerceForExpression($this->reduce($value));
3874
 
3875
+ if ($value instanceof Number) {
3876
+ return $value;
3877
+ }
3878
+
3879
  switch ($value[0]) {
3880
  case Type::T_LIST:
3881
  $value = $this->extractInterpolation($value);
3897
  case Type::T_STRING:
3898
  return [$value[0], '"', [$this->compileStringContent($value)]];
3899
 
 
 
 
3900
  case Type::T_INTERPOLATE:
3901
  return [Type::T_KEYWORD, $this->compileValue($value)];
3902
 
3908
  /**
3909
  * Add numbers
3910
  *
3911
+ * @param Number $left
3912
+ * @param Number $right
3913
  *
3914
+ * @return Number
3915
  */
3916
+ protected function opAddNumberNumber(Number $left, Number $right)
3917
  {
3918
+ return $left->plus($right);
3919
  }
3920
 
3921
  /**
3922
  * Multiply numbers
3923
  *
3924
+ * @param Number $left
3925
+ * @param Number $right
3926
  *
3927
+ * @return Number
3928
  */
3929
+ protected function opMulNumberNumber(Number $left, Number $right)
3930
  {
3931
+ return $left->times($right);
3932
  }
3933
 
3934
  /**
3935
  * Subtract numbers
3936
  *
3937
+ * @param Number $left
3938
+ * @param Number $right
3939
  *
3940
+ * @return Number
3941
  */
3942
+ protected function opSubNumberNumber(Number $left, Number $right)
3943
  {
3944
+ return $left->minus($right);
3945
  }
3946
 
3947
  /**
3948
  * Divide numbers
3949
  *
3950
+ * @param Number $left
3951
+ * @param Number $right
3952
  *
3953
+ * @return Number
3954
  */
3955
+ protected function opDivNumberNumber(Number $left, Number $right)
3956
  {
3957
+ return $left->dividedBy($right);
 
 
 
 
3958
  }
3959
 
3960
  /**
3961
  * Mod numbers
3962
  *
3963
+ * @param Number $left
3964
+ * @param Number $right
3965
  *
3966
+ * @return Number
3967
  */
3968
+ protected function opModNumberNumber(Number $left, Number $right)
3969
  {
3970
+ return $left->modulo($right);
 
 
 
 
3971
  }
3972
 
3973
  /**
4006
  /**
4007
  * Boolean and
4008
  *
4009
+ * @param array|Number $left
4010
+ * @param array|Number $right
4011
  * @param boolean $shouldEval
4012
  *
4013
+ * @return array|Number|null
4014
  */
4015
  protected function opAnd($left, $right, $shouldEval)
4016
  {
4034
  /**
4035
  * Boolean or
4036
  *
4037
+ * @param array|Number $left
4038
+ * @param array|Number $right
4039
  * @param boolean $shouldEval
4040
  *
4041
+ * @return array|Number|null
4042
  */
4043
  protected function opOr($left, $right, $shouldEval)
4044
  {
4070
  */
4071
  protected function opColorColor($op, $left, $right)
4072
  {
4073
+ if ($op !== '==' && $op !== '!=') {
4074
+ $warning = "Color arithmetic is deprecated and will be an error in future versions.\n"
4075
+ . "Consider using Sass's color functions instead.";
4076
+ $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
4077
+ $line = $this->sourceLine;
4078
+
4079
+ Warn::deprecation("$warning\n on line $line of $fname");
4080
+ }
4081
+
4082
  $out = [Type::T_COLOR];
4083
 
4084
  foreach ([1, 2, 3] as $i) {
4139
  *
4140
  * @param string $op
4141
  * @param array $left
4142
+ * @param Number $right
4143
  *
4144
  * @return array
4145
  */
4146
+ protected function opColorNumber($op, $left, Number $right)
4147
  {
4148
+ if ($op === '==') {
4149
+ return static::$false;
4150
+ }
4151
+
4152
+ if ($op === '!=') {
4153
+ return static::$true;
4154
+ }
4155
+
4156
+ $value = $right->getDimension();
4157
 
4158
  return $this->opColorColor(
4159
  $op,
4166
  * Compare number and color
4167
  *
4168
  * @param string $op
4169
+ * @param Number $left
4170
  * @param array $right
4171
  *
4172
  * @return array
4173
  */
4174
+ protected function opNumberColor($op, Number $left, $right)
4175
  {
4176
+ if ($op === '==') {
4177
+ return static::$false;
4178
+ }
4179
+
4180
+ if ($op === '!=') {
4181
+ return static::$true;
4182
+ }
4183
+
4184
+ $value = $left->getDimension();
4185
 
4186
  return $this->opColorColor(
4187
  $op,
4193
  /**
4194
  * Compare number1 == number2
4195
  *
4196
+ * @param array|Number $left
4197
+ * @param array|Number $right
4198
  *
4199
  * @return array
4200
  */
4214
  /**
4215
  * Compare number1 != number2
4216
  *
4217
+ * @param array|Number $left
4218
+ * @param array|Number $right
4219
  *
4220
  * @return array
4221
  */
4233
  }
4234
 
4235
  /**
4236
+ * Compare number1 == number2
4237
  *
4238
+ * @param Number $left
4239
+ * @param Number $right
4240
  *
4241
  * @return array
4242
  */
4243
+ protected function opEqNumberNumber(Number $left, Number $right)
4244
  {
4245
+ return $this->toBool($left->equals($right));
4246
  }
4247
 
4248
  /**
4249
+ * Compare number1 != number2
4250
  *
4251
+ * @param Number $left
4252
+ * @param Number $right
4253
  *
4254
  * @return array
4255
  */
4256
+ protected function opNeqNumberNumber(Number $left, Number $right)
4257
  {
4258
+ return $this->toBool(!$left->equals($right));
4259
  }
4260
 
4261
  /**
4262
+ * Compare number1 >= number2
4263
  *
4264
+ * @param Number $left
4265
+ * @param Number $right
4266
  *
4267
  * @return array
4268
  */
4269
+ protected function opGteNumberNumber(Number $left, Number $right)
4270
  {
4271
+ return $this->toBool($left->greaterThanOrEqual($right));
4272
  }
4273
 
4274
  /**
4275
+ * Compare number1 > number2
4276
  *
4277
+ * @param Number $left
4278
+ * @param Number $right
4279
  *
4280
  * @return array
4281
  */
4282
+ protected function opGtNumberNumber(Number $left, Number $right)
4283
  {
4284
+ return $this->toBool($left->greaterThan($right));
4285
  }
4286
 
4287
  /**
4288
+ * Compare number1 <= number2
4289
  *
4290
+ * @param Number $left
4291
+ * @param Number $right
4292
  *
4293
+ * @return array
4294
  */
4295
+ protected function opLteNumberNumber(Number $left, Number $right)
4296
  {
4297
+ return $this->toBool($left->lessThanOrEqual($right));
4298
+ }
4299
 
4300
+ /**
4301
+ * Compare number1 < number2
4302
+ *
4303
+ * @param Number $left
4304
+ * @param Number $right
4305
+ *
4306
+ * @return array
4307
+ */
4308
+ protected function opLtNumberNumber(Number $left, Number $right)
4309
+ {
4310
+ return $this->toBool($left->lessThan($right));
4311
  }
4312
 
4313
  /**
4315
  *
4316
  * @api
4317
  *
4318
+ * @param bool $thing
4319
  *
4320
  * @return array
4321
  */
4324
  return $thing ? static::$true : static::$false;
4325
  }
4326
 
4327
+ /**
4328
+ * Escape non printable chars in strings output as in dart-sass
4329
+ *
4330
+ * @internal
4331
+ *
4332
+ * @param string $string
4333
+ * @param bool $inKeyword
4334
+ *
4335
+ * @return string
4336
+ */
4337
+ public function escapeNonPrintableChars($string, $inKeyword = false)
4338
+ {
4339
+ static $replacement = [];
4340
+ if (empty($replacement[$inKeyword])) {
4341
+ for ($i = 0; $i < 32; $i++) {
4342
+ if ($i !== 9 || $inKeyword) {
4343
+ $replacement[$inKeyword][chr($i)] = '\\' . dechex($i) . ($inKeyword ? ' ' : chr(0));
4344
+ }
4345
+ }
4346
+ }
4347
+ $string = str_replace(array_keys($replacement[$inKeyword]), array_values($replacement[$inKeyword]), $string);
4348
+ // chr(0) is not a possible char from the input, so any chr(0) comes from our escaping replacement
4349
+ if (strpos($string, chr(0)) !== false) {
4350
+ if (substr($string, -1) === chr(0)) {
4351
+ $string = substr($string, 0, -1);
4352
+ }
4353
+ $string = str_replace(
4354
+ [chr(0) . '\\',chr(0) . ' '],
4355
+ [ '\\', ' '],
4356
+ $string
4357
+ );
4358
+ if (strpos($string, chr(0)) !== false) {
4359
+ $parts = explode(chr(0), $string);
4360
+ $string = array_shift($parts);
4361
+ while (count($parts)) {
4362
+ $next = array_shift($parts);
4363
+ if (strpos("0123456789abcdefABCDEF" . chr(9), $next[0]) !== false) {
4364
+ $string .= " ";
4365
+ }
4366
+ $string .= $next;
4367
+ }
4368
+ }
4369
+ }
4370
+
4371
+ return $string;
4372
+ }
4373
+
4374
  /**
4375
  * Compiles a primitive value into a CSS property value.
4376
  *
4384
  *
4385
  * @api
4386
  *
4387
+ * @param array|Number $value
4388
+ * @param bool $quote
4389
  *
4390
+ * @return string
4391
  */
4392
+ public function compileValue($value, $quote = true)
4393
  {
4394
  $value = $this->reduce($value);
4395
 
4396
+ if ($value instanceof Number) {
4397
+ return $value->output($this);
4398
+ }
4399
+
4400
  switch ($value[0]) {
4401
  case Type::T_KEYWORD:
4402
+ return $this->escapeNonPrintableChars($value[1], true);
4403
 
4404
  case Type::T_COLOR:
4405
  // [1] - red component (either number for a %)
4423
  }
4424
 
4425
  if (is_numeric($alpha)) {
4426
+ $a = new Number($alpha, '');
4427
  } else {
4428
  $a = $alpha;
4429
  }
4451
 
4452
  return $h;
4453
 
 
 
 
4454
  case Type::T_STRING:
4455
+ $content = $this->compileStringContent($value, $quote);
4456
+
4457
+ if ($value[1] && $quote) {
4458
+ $content = str_replace('\\', '\\\\', $content);
4459
+
4460
+ $content = $this->escapeNonPrintableChars($content);
4461
 
 
4462
  // force double quote as string quote for the output in certain cases
4463
  if (
4464
  $value[1] === "'" &&
4465
+ (strpos($content, '"') === false or strpos($content, "'") !== false) &&
4466
+ strpbrk($content, '{}\\\'') !== false
4467
  ) {
4468
  $value[1] = '"';
4469
+ } elseif (
4470
+ $value[1] === '"' &&
4471
+ (strpos($content, '"') !== false and strpos($content, "'") === false)
4472
+ ) {
4473
+ $value[1] = "'";
4474
  }
4475
+
4476
+ $content = str_replace($value[1], '\\' . $value[1], $content);
 
 
 
4477
  }
4478
 
4479
  return $value[1] . $content . $value[1];
4480
 
4481
  case Type::T_FUNCTION:
4482
+ $args = ! empty($value[2]) ? $this->compileValue($value[2], $quote) : '';
4483
 
4484
  return "$value[1]($args)";
4485
 
4492
  $value = $this->extractInterpolation($value);
4493
 
4494
  if ($value[0] !== Type::T_LIST) {
4495
+ return $this->compileValue($value, $quote);
4496
  }
4497
 
4498
  list(, $delim, $items) = $value;
4524
 
4525
  $filtered = [];
4526
 
4527
+ $same_string_quote = null;
4528
  foreach ($items as $item) {
4529
+ if (\is_null($same_string_quote)) {
4530
+ $same_string_quote = false;
4531
+ if ($item[0] === Type::T_STRING) {
4532
+ $same_string_quote = $item[1];
4533
+ foreach ($items as $ii) {
4534
+ if ($ii[0] !== Type::T_STRING) {
4535
+ $same_string_quote = false;
4536
+ break;
4537
+ }
4538
+ }
4539
+ }
4540
+ }
4541
  if ($item[0] === Type::T_NULL) {
4542
  continue;
4543
  }
4544
+ if ($same_string_quote === '"' && $item[0] === Type::T_STRING && $item[1]) {
4545
+ $item[1] = $same_string_quote;
4546
+ }
4547
 
4548
+ $compiled = $this->compileValue($item, $quote);
4549
 
4550
  if ($prefix_value && \strlen($compiled)) {
4551
  $compiled = $prefix_value . $compiled;
4562
  $filtered = [];
4563
 
4564
  for ($i = 0, $s = \count($keys); $i < $s; $i++) {
4565
+ $filtered[$this->compileValue($keys[$i], $quote)] = $this->compileValue($values[$i], $quote);
4566
  }
4567
 
4568
  array_walk($filtered, function (&$value, $key) {
4583
  }
4584
 
4585
  $left = \count($left[2]) > 0
4586
+ ? $this->compileValue($left, $quote) . $delim . $whiteLeft
4587
  : '';
4588
 
4589
  $delim = $right[1];
4593
  }
4594
 
4595
  $right = \count($right[2]) > 0 ?
4596
+ $whiteRight . $delim . $this->compileValue($right, $quote) : '';
4597
 
4598
+ return $left . $this->compileValue($interpolate, $quote) . $right;
4599
 
4600
  case Type::T_INTERPOLATE:
4601
  // strip quotes if it's a string
4602
  $reduced = $this->reduce($value[1]);
4603
 
4604
+ if ($reduced instanceof Number) {
4605
+ return $this->compileValue($reduced, $quote);
4606
+ }
4607
+
4608
  switch ($reduced[0]) {
4609
  case Type::T_LIST:
4610
  $reduced = $this->extractInterpolation($reduced);
4626
  continue;
4627
  }
4628
 
4629
+ if ($item[0] === Type::T_STRING) {
4630
+ $filtered[] = $this->compileStringContent($item, $quote);
4631
+ } elseif ($item[0] === Type::T_KEYWORD) {
4632
+ $filtered[] = $item[1];
 
 
4633
  } else {
4634
+ $filtered[] = $this->compileValue($item, $quote);
4635
  }
4636
  }
4637
 
4639
  break;
4640
 
4641
  case Type::T_STRING:
4642
+ $reduced = [Type::T_STRING, '', [$this->compileStringContent($reduced)]];
4643
  break;
4644
 
4645
  case Type::T_NULL:
4646
  $reduced = [Type::T_KEYWORD, ''];
4647
  }
4648
 
4649
+ return $this->compileValue($reduced, $quote);
4650
 
4651
  case Type::T_NULL:
4652
  return 'null';
4660
  }
4661
 
4662
  /**
4663
+ * @param array|Number $value
4664
  *
4665
+ * @return string
4666
  */
4667
  protected function compileDebugValue($value)
4668
  {
4669
  $value = $this->reduce($value, true);
4670
 
4671
+ if ($value instanceof Number) {
4672
+ return $this->compileValue($value);
4673
+ }
4674
+
4675
  switch ($value[0]) {
4676
  case Type::T_STRING:
4677
  return $this->compileStringContent($value);
4687
  * @param array $list
4688
  *
4689
  * @return string
4690
+ *
4691
+ * @deprecated
4692
  */
4693
  protected function flattenList($list)
4694
  {
4695
+ @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
4696
+
4697
  return $this->compileValue($list);
4698
  }
4699
 
4700
  /**
4701
+ * Gets the text of a Sass string
4702
  *
4703
+ * Calling this method on anything else than a SassString is unsupported. Use {@see assertString} first
4704
+ * to ensure that the value is indeed a string.
4705
+ *
4706
+ * @param array $value
4707
  *
4708
  * @return string
4709
  */
4710
+ public function getStringText(array $value)
4711
  {
4712
+ if ($value[0] !== Type::T_STRING) {
4713
+ throw new \InvalidArgumentException('The argument is not a sass string. Did you forgot to use "assertString"?');
4714
+ }
4715
 
4716
+ return $this->compileStringContent($value);
4717
+ }
4718
+
4719
+ /**
4720
+ * Compile string content
4721
+ *
4722
+ * @param array $string
4723
+ * @param bool $quote
4724
+ *
4725
+ * @return string
4726
+ */
4727
+ protected function compileStringContent($string, $quote = true)
4728
+ {
4729
+ $parts = [];
4730
+
4731
+ foreach ($string[2] as $part) {
4732
+ if (\is_array($part) || $part instanceof Number) {
4733
+ $parts[] = $this->compileValue($part, $quote);
4734
+ } else {
4735
+ $parts[] = $part;
4736
  }
4737
  }
4738
 
4942
  /**
4943
  * Convert env linked list to stack
4944
  *
4945
+ * @param Environment $env
4946
  *
4947
+ * @return Environment[]
4948
+ *
4949
+ * @phpstan-return non-empty-array<Environment>
4950
  */
4951
  protected function compactEnv(Environment $env)
4952
  {
4960
  /**
4961
  * Convert env stack to singly linked list
4962
  *
4963
+ * @param Environment[] $envs
4964
  *
4965
+ * @return Environment
4966
+ *
4967
+ * @phpstan-param non-empty-array<Environment> $envs
4968
  */
4969
  protected function extractEnv($envs)
4970
  {
5000
 
5001
  /**
5002
  * Pop environment
5003
+ *
5004
+ * @return void
5005
  */
5006
  protected function popEnv()
5007
  {
5012
  /**
5013
  * Propagate vars from a just poped Env (used in @each and @for)
5014
  *
5015
+ * @param array $store
5016
+ * @param null|string[] $excludedVars
5017
+ *
5018
+ * @return void
5019
  */
5020
  protected function backPropagateEnv($store, $excludedVars = null)
5021
  {
5044
  * @param boolean $shadow
5045
  * @param \ScssPhp\ScssPhp\Compiler\Environment $env
5046
  * @param mixed $valueUnreduced
5047
+ *
5048
+ * @return void
5049
  */
5050
  protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null)
5051
  {
5069
  * @param mixed $value
5070
  * @param \ScssPhp\ScssPhp\Compiler\Environment $env
5071
  * @param mixed $valueUnreduced
5072
+ *
5073
+ * @return void
5074
  */
5075
  protected function setExisting($name, $value, Environment $env, $valueUnreduced = null)
5076
  {
5129
  * @param mixed $value
5130
  * @param \ScssPhp\ScssPhp\Compiler\Environment $env
5131
  * @param mixed $valueUnreduced
5132
+ *
5133
+ * @return void
5134
  */
5135
  protected function setRaw($name, $value, Environment $env, $valueUnreduced = null)
5136
  {
5144
  /**
5145
  * Get variable
5146
  *
5147
+ * @internal
5148
  *
5149
  * @param string $name
5150
  * @param boolean $shouldThrow
5227
  * Inject variables
5228
  *
5229
  * @param array $args
5230
+ *
5231
+ * @return void
5232
  */
5233
  protected function injectVariables(array $args)
5234
  {
5243
  $name = substr($name, 1);
5244
  }
5245
 
5246
+ if (!\is_string($strValue) || ! $parser->parseValue($strValue, $value)) {
5247
  $value = $this->coerceValue($strValue);
5248
  }
5249
 
5251
  }
5252
  }
5253
 
5254
+ /**
5255
+ * Replaces variables.
5256
+ *
5257
+ * @param array<string, mixed> $variables
5258
+ *
5259
+ * @return void
5260
+ */
5261
+ public function replaceVariables(array $variables)
5262
+ {
5263
+ $this->registeredVars = [];
5264
+ $this->addVariables($variables);
5265
+ }
5266
+
5267
+ /**
5268
+ * Replaces variables.
5269
+ *
5270
+ * @param array<string, mixed> $variables
5271
+ *
5272
+ * @return void
5273
+ */
5274
+ public function addVariables(array $variables)
5275
+ {
5276
+ $triggerWarning = false;
5277
+
5278
+ foreach ($variables as $name => $value) {
5279
+ if (!$value instanceof Number && !\is_array($value)) {
5280
+ $triggerWarning = true;
5281
+ }
5282
+
5283
+ $this->registeredVars[$name] = $value;
5284
+ }
5285
+
5286
+ if ($triggerWarning) {
5287
+ @trigger_error('Passing raw values to as custom variables to the Compiler is deprecated. Use "\ScssPhp\ScssPhp\ValueConverter::parseValue" or "\ScssPhp\ScssPhp\ValueConverter::fromPhp" to convert them instead.', E_USER_DEPRECATED);
5288
+ }
5289
+ }
5290
+
5291
  /**
5292
  * Set variables
5293
  *
5294
  * @api
5295
  *
5296
  * @param array $variables
5297
+ *
5298
+ * @return void
5299
+ *
5300
+ * @deprecated Use "addVariables" or "replaceVariables" instead.
5301
  */
5302
  public function setVariables(array $variables)
5303
  {
5304
+ @trigger_error('The method "setVariables" of the Compiler is deprecated. Use the "addVariables" method for the equivalent behavior or "replaceVariables" if merging with previous variables was not desired.');
5305
+
5306
+ $this->addVariables($variables);
5307
  }
5308
 
5309
  /**
5312
  * @api
5313
  *
5314
  * @param string $name
5315
+ *
5316
+ * @return void
5317
  */
5318
  public function unsetVariable($name)
5319
  {
5335
  /**
5336
  * Adds to list of parsed files
5337
  *
5338
+ * @internal
5339
  *
5340
+ * @param string|null $path
5341
+ *
5342
+ * @return void
5343
  */
5344
  public function addParsedFile($path)
5345
  {
5346
+ if (! \is_null($path) && is_file($path)) {
5347
  $this->parsedFiles[realpath($path)] = filemtime($path);
5348
  }
5349
  }
5351
  /**
5352
  * Returns list of parsed files
5353
  *
5354
+ * @deprecated
5355
+ * @return array<string, int>
 
5356
  */
5357
  public function getParsedFiles()
5358
  {
5359
+ @trigger_error('The method "getParsedFiles" of the Compiler is deprecated. Use the "getIncludedFiles" method on the CompilationResult instance returned by compileString() instead. Be careful that the signature of the method is different.', E_USER_DEPRECATED);
5360
  return $this->parsedFiles;
5361
  }
5362
 
5366
  * @api
5367
  *
5368
  * @param string|callable $path
5369
+ *
5370
+ * @return void
5371
  */
5372
  public function addImportPath($path)
5373
  {
5381
  *
5382
  * @api
5383
  *
5384
+ * @param string|array<string|callable> $path
5385
+ *
5386
+ * @return void
5387
  */
5388
  public function setImportPaths($path)
5389
  {
5390
+ $paths = (array) $path;
5391
+ $actualImportPaths = array_filter($paths, function ($path) {
5392
+ return $path !== '';
5393
+ });
5394
+
5395
+ $this->legacyCwdImportPath = \count($actualImportPaths) !== \count($paths);
5396
+
5397
+ if ($this->legacyCwdImportPath) {
5398
+ @trigger_error('Passing an empty string in the import paths to refer to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be used directly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compileString()" instead.', E_USER_DEPRECATED);
5399
+ }
5400
+
5401
+ $this->importPaths = $actualImportPaths;
5402
  }
5403
 
5404
  /**
5408
  *
5409
  * @param integer $numberPrecision
5410
  *
5411
+ * @return void
5412
+ *
5413
  * @deprecated The number precision is not configurable anymore. The default is enough for all browsers.
5414
  */
5415
  public function setNumberPrecision($numberPrecision)
5418
  . 'The default is enough for all browsers.', E_USER_DEPRECATED);
5419
  }
5420
 
5421
+ /**
5422
+ * Sets the output style.
5423
+ *
5424
+ * @api
5425
+ *
5426
+ * @param string $style One of the OutputStyle constants
5427
+ *
5428
+ * @return void
5429
+ *
5430
+ * @phpstan-param OutputStyle::* $style
5431
+ */
5432
+ public function setOutputStyle($style)
5433
+ {
5434
+ switch ($style) {
5435
+ case OutputStyle::EXPANDED:
5436
+ $this->formatter = Expanded::class;
5437
+ break;
5438
+
5439
+ case OutputStyle::COMPRESSED:
5440
+ $this->formatter = Compressed::class;
5441
+ break;
5442
+
5443
+ default:
5444
+ throw new \InvalidArgumentException(sprintf('Invalid output style "%s".', $style));
5445
+ }
5446
+ }
5447
+
5448
  /**
5449
  * Set formatter
5450
  *
5451
  * @api
5452
  *
5453
  * @param string $formatterName
5454
+ *
5455
+ * @return void
5456
+ *
5457
+ * @deprecated Use {@see setOutputStyle} instead.
5458
  */
5459
  public function setFormatter($formatterName)
5460
  {
5461
+ if (!\in_array($formatterName, [Expanded::class, Compressed::class], true)) {
5462
+ @trigger_error('Formatters other than Expanded and Compressed are deprecated.', E_USER_DEPRECATED);
5463
+ }
5464
+ @trigger_error('The method "setFormatter" is deprecated. Use "setOutputStyle" instead.', E_USER_DEPRECATED);
5465
+
5466
  $this->formatter = $formatterName;
5467
  }
5468
 
5472
  * @api
5473
  *
5474
  * @param string $lineNumberStyle
5475
+ *
5476
+ * @return void
5477
+ *
5478
+ * @deprecated The line number output is not supported anymore. Use source maps instead.
5479
  */
5480
  public function setLineNumberStyle($lineNumberStyle)
5481
  {
5482
+ @trigger_error('The line number output is not supported anymore. '
5483
+ . 'Use source maps instead.', E_USER_DEPRECATED);
5484
  }
5485
 
5486
  /**
5489
  * @api
5490
  *
5491
  * @param integer $sourceMap
5492
+ *
5493
+ * @return void
5494
+ *
5495
+ * @phpstan-param self::SOURCE_MAP_* $sourceMap
5496
  */
5497
  public function setSourceMap($sourceMap)
5498
  {
5505
  * @api
5506
  *
5507
  * @param array $sourceMapOptions
5508
+ *
5509
+ * @phpstan-param array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} $sourceMapOptions
5510
+ *
5511
+ * @return void
5512
  */
5513
  public function setSourceMapOptions($sourceMapOptions)
5514
  {
5520
  *
5521
  * @api
5522
  *
5523
+ * @param string $name
5524
+ * @param callable $callback
5525
+ * @param string[]|null $argumentDeclaration
5526
+ *
5527
+ * @return void
5528
  */
5529
+ public function registerFunction($name, $callback, $argumentDeclaration = null)
5530
  {
5531
+ if (self::isNativeFunction($name)) {
5532
+ @trigger_error(sprintf('The "%s" function is a core sass function. Overriding it with a custom implementation through "%s" is deprecated and won\'t be supported in ScssPhp 2.0 anymore.', $name, __METHOD__), E_USER_DEPRECATED);
5533
+ }
5534
+
5535
+ if ($argumentDeclaration === null) {
5536
+ @trigger_error('Omitting the argument declaration when registering custom function is deprecated and won\'t be supported in ScssPhp 2.0 anymore.', E_USER_DEPRECATED);
5537
+ }
5538
+
5539
+ $this->userFunctions[$this->normalizeName($name)] = [$callback, $argumentDeclaration];
5540
  }
5541
 
5542
  /**
5545
  * @api
5546
  *
5547
  * @param string $name
5548
+ *
5549
+ * @return void
5550
  */
5551
  public function unregisterFunction($name)
5552
  {
5559
  * @api
5560
  *
5561
  * @param string $name
5562
+ *
5563
+ * @return void
5564
+ *
5565
+ * @deprecated Registering additional features is deprecated.
5566
  */
5567
  public function addFeature($name)
5568
  {
5569
+ @trigger_error('Registering additional features is deprecated.', E_USER_DEPRECATED);
5570
+
5571
  $this->registeredFeatures[$name] = true;
5572
  }
5573
 
5576
  *
5577
  * @param string $path
5578
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
5579
+ *
5580
+ * @return void
5581
  */
5582
  protected function importFile($path, OutputBlock $out)
5583
  {
5584
+ $this->pushCallStack('import ' . $this->getPrettyPath($path));
5585
  // see if tree is cached
5586
  $realPath = realpath($path);
5587
 
5588
+ if (substr($path, -5) === '.sass') {
5589
+ $this->sourceIndex = \count($this->sourceNames);
5590
+ $this->sourceNames[] = $path;
5591
+ $this->sourceLine = 1;
5592
+ $this->sourceColumn = 1;
5593
+
5594
+ throw $this->error('The Sass indented syntax is not implemented.');
5595
+ }
5596
+
5597
  if (isset($this->importCache[$realPath])) {
5598
  $this->handleImportLoop($realPath);
5599
 
5606
  $this->importCache[$realPath] = $tree;
5607
  }
5608
 
5609
+ $currentDirectory = $this->currentDirectory;
5610
+ $this->currentDirectory = dirname($path);
5611
 
 
5612
  $this->compileChildrenNoReturn($tree->children, $out);
5613
+ $this->currentDirectory = $currentDirectory;
5614
  $this->popCallStack();
5615
  }
5616
 
5617
  /**
5618
+ * Save the imported files with their resolving path context
5619
  *
5620
+ * @param string|null $currentDirectory
5621
+ * @param string $path
5622
+ * @param string $filePath
5623
+ *
5624
+ * @return void
5625
+ */
5626
+ private function registerImport($currentDirectory, $path, $filePath)
5627
+ {
5628
+ $this->resolvedImports[] = ['currentDir' => $currentDirectory, 'path' => $path, 'filePath' => $filePath];
5629
+ }
5630
+
5631
+ /**
5632
+ * Detects whether the import is a CSS import.
5633
+ *
5634
+ * For legacy reasons, custom importers are called for those, allowing them
5635
+ * to replace them with an actual Sass import. However this behavior is
5636
+ * deprecated. Custom importers are expected to return null when they receive
5637
+ * a CSS import.
5638
  *
5639
  * @param string $url
5640
  *
5641
+ * @return bool
5642
  */
5643
+ public static function isCssImport($url)
5644
  {
5645
+ return 1 === preg_match('~\.css$|^https?://|^//~', $url);
5646
+ }
5647
 
5648
+ /**
5649
+ * Return the file path for an import url if it exists
5650
+ *
5651
+ * @internal
5652
+ *
5653
+ * @param string $url
5654
+ * @param string|null $currentDir
5655
+ *
5656
+ * @return string|null
5657
+ */
5658
+ public function findImport($url, $currentDir = null)
5659
+ {
5660
+ // Vanilla css and external requests. These are not meant to be Sass imports.
5661
+ // Callback importers are still called for BC.
5662
+ if (self::isCssImport($url)) {
5663
+ foreach ($this->importPaths as $dir) {
5664
+ if (\is_string($dir)) {
5665
+ continue;
5666
+ }
5667
 
5668
+ if (\is_callable($dir)) {
5669
+ // check custom callback for import path
5670
+ $file = \call_user_func($dir, $url);
5671
 
5672
+ if (! \is_null($file)) {
5673
+ if (\is_array($dir)) {
5674
+ $callableDescription = (\is_object($dir[0]) ? \get_class($dir[0]) : $dir[0]).'::'.$dir[1];
5675
+ } elseif ($dir instanceof \Closure) {
5676
+ $r = new \ReflectionFunction($dir);
5677
+ if (false !== strpos($r->name, '{closure}')) {
5678
+ $callableDescription = sprintf('closure{%s:%s}', $r->getFileName(), $r->getStartLine());
5679
+ } elseif ($class = $r->getClosureScopeClass()) {
5680
+ $callableDescription = $class->name.'::'.$r->name;
5681
+ } else {
5682
+ $callableDescription = $r->name;
5683
+ }
5684
+ } elseif (\is_object($dir)) {
5685
+ $callableDescription = \get_class($dir) . '::__invoke';
5686
+ } else {
5687
+ $callableDescription = 'callable'; // Fallback if we don't have a dedicated description
5688
+ }
5689
+ @trigger_error(sprintf('Returning a file to import for CSS or external references in custom importer callables is deprecated and will not be supported anymore in ScssPhp 2.0. This behavior is not compliant with the Sass specification. Update your "%s" importer.', $callableDescription), E_USER_DEPRECATED);
5690
 
5691
+ return $file;
5692
+ }
5693
+ }
5694
  }
5695
+ return null;
5696
+ }
5697
 
5698
+ if (!\is_null($currentDir)) {
5699
+ $relativePath = $this->resolveImportPath($url, $currentDir);
5700
+
5701
+ if (!\is_null($relativePath)) {
5702
+ return $relativePath;
5703
  }
5704
  }
5705
 
5706
  foreach ($this->importPaths as $dir) {
5707
  if (\is_string($dir)) {
5708
+ $path = $this->resolveImportPath($url, $dir);
5709
+
5710
+ if (!\is_null($path)) {
5711
+ return $path;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5712
  }
5713
  } elseif (\is_callable($dir)) {
5714
  // check custom callback for import path
5720
  }
5721
  }
5722
 
5723
+ if ($this->legacyCwdImportPath) {
5724
+ $path = $this->resolveImportPath($url, getcwd());
5725
+
5726
+ if (!\is_null($path)) {
5727
+ @trigger_error('Resolving imports relatively to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be added as an import path explicitly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compileString()" instead.', E_USER_DEPRECATED);
5728
+
5729
+ return $path;
5730
  }
5731
  }
5732
 
5733
+ throw $this->error("`$url` file not found for @import");
5734
+ }
5735
+
5736
+ /**
5737
+ * @param string $url
5738
+ * @param string $baseDir
5739
+ *
5740
+ * @return string|null
5741
+ */
5742
+ private function resolveImportPath($url, $baseDir)
5743
+ {
5744
+ $path = Path::join($baseDir, $url);
5745
+
5746
+ $hasExtension = preg_match('/.s[ac]ss$/', $url);
5747
+
5748
+ if ($hasExtension) {
5749
+ return $this->checkImportPathConflicts($this->tryImportPath($path));
5750
+ }
5751
+
5752
+ $result = $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path));
5753
+
5754
+ if (!\is_null($result)) {
5755
+ return $result;
5756
+ }
5757
+
5758
+ return $this->tryImportPathAsDirectory($path);
5759
+ }
5760
+
5761
+ /**
5762
+ * @param string[] $paths
5763
+ *
5764
+ * @return string|null
5765
+ */
5766
+ private function checkImportPathConflicts(array $paths)
5767
+ {
5768
+ if (\count($paths) === 0) {
5769
+ return null;
5770
+ }
5771
+
5772
+ if (\count($paths) === 1) {
5773
+ return $paths[0];
5774
+ }
5775
+
5776
+ $formattedPrettyPaths = [];
5777
+
5778
+ foreach ($paths as $path) {
5779
+ $formattedPrettyPaths[] = ' ' . $this->getPrettyPath($path);
5780
+ }
5781
+
5782
+ throw $this->error("It's not clear which file to import. Found:\n" . implode("\n", $formattedPrettyPaths));
5783
+ }
5784
+
5785
+ /**
5786
+ * @param string $path
5787
+ *
5788
+ * @return string[]
5789
+ */
5790
+ private function tryImportPathWithExtensions($path)
5791
+ {
5792
+ $result = array_merge(
5793
+ $this->tryImportPath($path.'.sass'),
5794
+ $this->tryImportPath($path.'.scss')
5795
+ );
5796
+
5797
+ if ($result) {
5798
+ return $result;
5799
+ }
5800
+
5801
+ return $this->tryImportPath($path.'.css');
5802
+ }
5803
+
5804
+ /**
5805
+ * @param string $path
5806
+ *
5807
+ * @return string[]
5808
+ */
5809
+ private function tryImportPath($path)
5810
+ {
5811
+ $partial = dirname($path).'/_'.basename($path);
5812
+
5813
+ $candidates = [];
5814
+
5815
+ if (is_file($partial)) {
5816
+ $candidates[] = $partial;
5817
+ }
5818
+
5819
+ if (is_file($path)) {
5820
+ $candidates[] = $path;
5821
+ }
5822
+
5823
+ return $candidates;
5824
+ }
5825
+
5826
+ /**
5827
+ * @param string $path
5828
+ *
5829
+ * @return string|null
5830
+ */
5831
+ private function tryImportPathAsDirectory($path)
5832
+ {
5833
+ if (!is_dir($path)) {
5834
+ return null;
5835
+ }
5836
+
5837
+ return $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path.'/index'));
5838
+ }
5839
+
5840
+ /**
5841
+ * @param string|null $path
5842
+ *
5843
+ * @return string
5844
+ */
5845
+ private function getPrettyPath($path)
5846
+ {
5847
+ if ($path === null) {
5848
+ return '(unknown file)';
5849
+ }
5850
+
5851
+ $normalizedPath = $path;
5852
+ $normalizedRootDirectory = $this->rootDirectory.'/';
5853
+
5854
+ if (\DIRECTORY_SEPARATOR === '\\') {
5855
+ $normalizedRootDirectory = str_replace('\\', '/', $normalizedRootDirectory);
5856
+ $normalizedPath = str_replace('\\', '/', $path);
5857
+ }
5858
+
5859
+ if (0 === strpos($normalizedPath, $normalizedRootDirectory)) {
5860
+ return substr($normalizedPath, \strlen($normalizedRootDirectory));
5861
+ }
5862
+
5863
+ return $path;
5864
  }
5865
 
5866
  /**
5868
  *
5869
  * @api
5870
  *
5871
+ * @param string|null $encoding
5872
+ *
5873
+ * @return void
5874
+ *
5875
+ * @deprecated Non-compliant support for other encodings than UTF-8 is deprecated.
5876
  */
5877
  public function setEncoding($encoding)
5878
  {
5879
+ if (!$encoding || strtolower($encoding) === 'utf-8') {
5880
+ @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
5881
+ } else {
5882
+ @trigger_error(sprintf('The "%s" method is deprecated. Parsing will only support UTF-8 in ScssPhp 2.0. The non-UTF-8 parsing of ScssPhp 1.x is not spec compliant.', __METHOD__), E_USER_DEPRECATED);
5883
+ }
5884
+
5885
  $this->encoding = $encoding;
5886
  }
5887
 
5909
  * @api
5910
  *
5911
  * @return array
5912
+ *
5913
+ * @deprecated
5914
  */
5915
  public function getSourcePosition()
5916
  {
5917
+ @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
5918
+
5919
  $sourceFile = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : '';
5920
 
5921
  return [$sourceFile, $this->sourceLine, $this->sourceColumn];
5945
  /**
5946
  * Build an error (exception)
5947
  *
5948
+ * @internal
5949
  *
5950
  * @param string $msg Message with optional sprintf()-style vararg parameters
5951
  *
5958
  }
5959
 
5960
  if (! $this->ignoreCallStackMessage) {
5961
+ $msg = $this->addLocationToMessage($msg);
5962
+ }
5963
+
5964
+ return new CompilerException($msg);
5965
+ }
5966
+
5967
+ /**
5968
+ * @param string $msg
5969
+ *
5970
+ * @return string
5971
+ */
5972
+ private function addLocationToMessage($msg)
5973
+ {
5974
+ $line = $this->sourceLine;
5975
+ $column = $this->sourceColumn;
5976
 
5977
+ $loc = isset($this->sourceNames[$this->sourceIndex])
5978
+ ? $this->getPrettyPath($this->sourceNames[$this->sourceIndex]) . " on line $line, at column $column"
5979
+ : "line: $line, column: $column";
5980
 
5981
+ $msg = "$msg: $loc";
5982
 
5983
+ $callStackMsg = $this->callStackMessage();
5984
 
5985
+ if ($callStackMsg) {
5986
+ $msg .= "\nCall Stack:\n" . $callStackMsg;
 
5987
  }
5988
 
5989
+ return $msg;
5990
  }
5991
 
5992
  /**
5994
  * @param array $ExpectedArgs
5995
  * @param int $nbActual
5996
  * @return CompilerException
5997
+ *
5998
+ * @deprecated
5999
  */
6000
  public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual)
6001
  {
6002
+ @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
6003
+
6004
  $nbExpected = \count($ExpectedArgs);
6005
 
6006
  if ($nbActual > $nbExpected) {
6029
  /**
6030
  * Beautify call stack for output
6031
  *
6032
+ * @param boolean $all
6033
+ * @param int|null $limit
6034
  *
6035
  * @return string
6036
  */
6044
  if ($all || (isset($call['n']) && $call['n'])) {
6045
  $msg = '#' . $ncall++ . ' ' . $call['n'] . ' ';
6046
  $msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]])
6047
+ ? $this->getPrettyPath($this->sourceNames[$call[Parser::SOURCE_INDEX]])
6048
  : '(unknown file)');
6049
  $msg .= ' on line ' . $call[Parser::SOURCE_LINE];
6050
 
6076
 
6077
  $file = $this->sourceNames[$env->block->sourceIndex];
6078
 
6079
+ if ($file === null) {
6080
+ continue;
6081
+ }
6082
+
6083
  if (realpath($file) === $name) {
6084
  throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file));
6085
  }
6092
  * @param Object $func
6093
  * @param array $argValues
6094
  *
6095
+ * @return array|Number
6096
  */
6097
  protected function callScssFunction($func, $argValues)
6098
  {
6132
  * Call built-in and registered (PHP) functions
6133
  *
6134
  * @param string $name
6135
+ * @param callable $function
6136
  * @param array $prototype
6137
  * @param array $args
6138
  *
6139
+ * @return array|Number|null
6140
  */
6141
  protected function callNativeFunction($name, $function, $prototype, $args)
6142
  {
6148
  }
6149
  @list($sorted, $kwargs) = $sorted_kwargs;
6150
 
6151
+ if ($name !== 'if') {
 
 
 
 
 
 
6152
  foreach ($sorted as &$val) {
6153
+ if ($val !== null) {
6154
+ $val = $this->reduce($val, true);
6155
+ }
6156
  }
6157
  }
6158
 
6162
  return null;
6163
  }
6164
 
6165
+ if (\is_array($returnValue) || $returnValue instanceof Number) {
6166
+ return $returnValue;
6167
+ }
6168
+
6169
+ @trigger_error(sprintf('Returning a PHP value from the "%s" custom function is deprecated. A sass value must be returned instead.', $name), E_USER_DEPRECATED);
6170
+
6171
+ return $this->coerceValue($returnValue);
6172
  }
6173
 
6174
  /**
6186
 
6187
  /**
6188
  * Normalize native function name
6189
+ *
6190
+ * @internal
6191
+ *
6192
+ * @param string $name
6193
+ *
6194
  * @return string
6195
  */
6196
  public static function normalizeNativeFunctionName($name)
6208
 
6209
  /**
6210
  * Check if a function is a native built-in scss function, for css parsing
6211
+ *
6212
+ * @internal
6213
+ *
6214
+ * @param string $name
6215
+ *
6216
  * @return bool
6217
  */
6218
  public static function isNativeFunction($name)
6224
  * Sorts keyword arguments
6225
  *
6226
  * @param string $functionName
6227
+ * @param array|null $prototypes
6228
  * @param array $args
6229
  *
6230
  * @return array|null
6260
  // notation 100 127 255 / 0 is in fact a simple list of 4 values
6261
  foreach ($args as $k => $arg) {
6262
  if ($arg[1][0] === Type::T_LIST && \count($arg[1][2]) === 3) {
6263
+ $args[$k][1][2] = $this->extractSlashAlphaInColorFunction($arg[1][2]);
 
 
 
 
 
 
 
6264
  }
6265
  }
6266
  }
6267
 
6268
+ list($positionalArgs, $namedArgs, $names, $separator, $hasSplat) = $this->evaluateArguments($args, false);
6269
 
6270
  if (! \is_array(reset($prototypes))) {
6271
  $prototypes = [$prototypes];
6272
  }
6273
 
6274
+ $parsedPrototypes = array_map([$this, 'parseFunctionPrototype'], $prototypes);
6275
+ assert(!empty($parsedPrototypes));
6276
+ $matchedPrototype = $this->selectFunctionPrototype($parsedPrototypes, \count($positionalArgs), $names);
6277
+
6278
+ $this->verifyPrototype($matchedPrototype, \count($positionalArgs), $names, $hasSplat);
6279
+
6280
+ $vars = $this->applyArgumentsToDeclaration($matchedPrototype, $positionalArgs, $namedArgs, $separator);
6281
+
6282
+ $finalArgs = [];
6283
  $keyArgs = [];
6284
 
6285
+ foreach ($matchedPrototype['arguments'] as $argument) {
6286
+ list($normalizedName, $originalName, $default) = $argument;
 
6287
 
6288
+ if (isset($vars[$normalizedName])) {
6289
+ $value = $vars[$normalizedName];
6290
+ } else {
6291
+ $value = $default;
6292
+ }
6293
 
6294
+ // special null value as default: translate to real null here
6295
+ if ($value === [Type::T_KEYWORD, 'null']) {
6296
+ $value = null;
6297
+ }
6298
 
6299
+ $finalArgs[] = $value;
6300
+ $keyArgs[$originalName] = $value;
6301
+ }
6302
 
6303
+ if ($matchedPrototype['rest_argument'] !== null) {
6304
+ $value = $vars[$matchedPrototype['rest_argument']];
 
 
 
 
 
6305
 
6306
+ $finalArgs[] = $value;
6307
+ $keyArgs[$matchedPrototype['rest_argument']] = $value;
6308
+ }
6309
 
6310
+ return [$finalArgs, $keyArgs];
6311
+ }
6312
 
6313
+ /**
6314
+ * Parses a function prototype to the internal representation of arguments.
6315
+ *
6316
+ * The input is an array of strings describing each argument, as supported
6317
+ * in {@see registerFunction}. Argument names don't include the `$`.
6318
+ * The output contains the list of positional argument, with their normalized
6319
+ * name (underscores are replaced by dashes), their original name (to be used
6320
+ * in case of error reporting) and their default value. The output also contains
6321
+ * the normalized name of the rest argument, or null if the function prototype
6322
+ * is not variadic.
6323
+ *
6324
+ * @param string[] $prototype
6325
+ *
6326
+ * @return array
6327
+ * @phpstan-return array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null}
6328
+ */
6329
+ private function parseFunctionPrototype(array $prototype)
6330
+ {
6331
+ static $parser = null;
6332
+
6333
+ $arguments = [];
6334
+ $restArgument = null;
6335
 
6336
+ foreach ($prototype as $p) {
6337
+ if (null !== $restArgument) {
6338
+ throw new \InvalidArgumentException('The argument declaration is invalid. The rest argument must be the last one.');
6339
  }
6340
 
6341
+ $default = null;
6342
+ $p = explode(':', $p, 2);
6343
+ $name = str_replace('_', '-', $p[0]);
6344
 
6345
+ if (isset($p[1])) {
6346
+ $defaultSource = trim($p[1]);
 
6347
 
6348
+ if ($defaultSource === 'null') {
6349
+ // differentiate this null from the static::$null
6350
+ $default = [Type::T_KEYWORD, 'null'];
6351
+ } else {
6352
+ if (\is_null($parser)) {
6353
+ $parser = $this->parserFactory(__METHOD__);
6354
  }
6355
+
6356
+ $parser->parseValue($defaultSource, $default);
6357
  }
6358
+ }
6359
 
6360
+ if (substr($name, -3) === '...') {
6361
+ $restArgument = substr($name, 0, -3);
6362
+ } else {
6363
+ $arguments[] = [$name, $p[0], $default];
6364
+ }
6365
+ }
6366
 
6367
+ return [
6368
+ 'arguments' => $arguments,
6369
+ 'rest_argument' => $restArgument,
6370
+ ];
6371
+ }
6372
 
6373
+ /**
6374
+ * Returns the function prototype for the given positional and named arguments.
6375
+ *
6376
+ * If no exact match is found, finds the closest approximation. Note that this
6377
+ * doesn't guarantee that $positional and $names are valid for the returned
6378
+ * prototype.
6379
+ *
6380
+ * @param array[] $prototypes
6381
+ * @param int $positional
6382
+ * @param array<string, string> $names A set of names, as both keys and values
6383
+ *
6384
+ * @return array
6385
+ *
6386
+ * @phpstan-param non-empty-list<array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null}> $prototypes
6387
+ * @phpstan-return array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null}
6388
+ */
6389
+ private function selectFunctionPrototype(array $prototypes, $positional, array $names)
6390
+ {
6391
+ $fuzzyMatch = null;
6392
+ $minMismatchDistance = null;
6393
 
6394
+ foreach ($prototypes as $prototype) {
6395
+ // Ideally, find an exact match.
6396
+ if ($this->checkPrototypeMatches($prototype, $positional, $names)) {
6397
+ return $prototype;
6398
+ }
6399
 
6400
+ $mismatchDistance = \count($prototype['arguments']) - $positional;
 
 
6401
 
6402
+ if ($minMismatchDistance !== null) {
6403
+ if (abs($mismatchDistance) > abs($minMismatchDistance)) {
6404
+ continue;
6405
+ }
6406
 
6407
+ // If two overloads have the same mismatch distance, favor the overload
6408
+ // that has more arguments.
6409
+ if (abs($mismatchDistance) === abs($minMismatchDistance) && $mismatchDistance < 0) {
6410
+ continue;
6411
  }
 
 
 
6412
  }
6413
+
6414
+ $minMismatchDistance = $mismatchDistance;
6415
+ $fuzzyMatch = $prototype;
6416
  }
6417
 
6418
+ return $fuzzyMatch;
6419
+ }
6420
+
6421
+ /**
6422
+ * Checks whether the argument invocation matches the callable prototype.
6423
+ *
6424
+ * The rules are similar to {@see verifyPrototype}. The boolean return value
6425
+ * avoids the overhead of building and catching exceptions when the reason of
6426
+ * not matching the prototype does not need to be known.
6427
+ *
6428
+ * @param array $prototype
6429
+ * @param int $positional
6430
+ * @param array<string, string> $names
6431
+ *
6432
+ * @return bool
6433
+ *
6434
+ * @phpstan-param array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null} $prototype
6435
+ */
6436
+ private function checkPrototypeMatches(array $prototype, $positional, array $names)
6437
+ {
6438
+ $nameUsed = 0;
6439
+
6440
+ foreach ($prototype['arguments'] as $i => $argument) {
6441
+ list ($name, $originalName, $default) = $argument;
6442
+
6443
+ if ($i < $positional) {
6444
+ if (isset($names[$name])) {
6445
+ return false;
6446
  }
6447
+ } elseif (isset($names[$name])) {
6448
+ $nameUsed++;
6449
+ } elseif ($default === null) {
6450
+ return false;
6451
  }
6452
+ }
6453
 
6454
+ if ($prototype['rest_argument'] !== null) {
6455
+ return true;
6456
  }
6457
 
6458
+ if ($positional > \count($prototype['arguments'])) {
6459
+ return false;
6460
+ }
6461
+
6462
+ if ($nameUsed < \count($names)) {
6463
+ return false;
6464
+ }
6465
+
6466
+ return true;
6467
  }
6468
 
6469
  /**
6470
+ * Verifies that the argument invocation is valid for the callable prototype.
6471
  *
6472
+ * @param array $prototype
6473
+ * @param int $positional
6474
+ * @param array<string, string> $names
6475
+ * @param bool $hasSplat
 
6476
  *
6477
+ * @return void
6478
  *
6479
+ * @throws SassScriptException
6480
+ *
6481
+ * @phpstan-param array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null} $prototype
6482
  */
6483
+ private function verifyPrototype(array $prototype, $positional, array $names, $hasSplat)
6484
  {
6485
+ $nameUsed = 0;
6486
 
6487
+ foreach ($prototype['arguments'] as $i => $argument) {
6488
+ list ($name, $originalName, $default) = $argument;
 
6489
 
6490
+ if ($i < $positional) {
6491
+ if (isset($names[$name])) {
6492
+ throw new SassScriptException(sprintf('Argument $%s was passed both by position and by name.', $originalName));
6493
+ }
6494
+ } elseif (isset($names[$name])) {
6495
+ $nameUsed++;
6496
+ } elseif ($default === null) {
6497
+ throw new SassScriptException(sprintf('Missing argument $%s', $originalName));
6498
+ }
6499
+ }
6500
 
6501
+ if ($prototype['rest_argument'] !== null) {
6502
+ return;
6503
  }
6504
 
6505
+ if ($positional > \count($prototype['arguments'])) {
6506
+ $message = sprintf(
6507
+ 'Only %d %sargument%s allowed, but %d %s passed.',
6508
+ \count($prototype['arguments']),
6509
+ empty($names) ? '' : 'positional ',
6510
+ \count($prototype['arguments']) === 1 ? '' : 's',
6511
+ $positional,
6512
+ $positional === 1 ? 'was' : 'were'
6513
+ );
6514
+ if (!$hasSplat) {
6515
+ throw new SassScriptException($message);
6516
+ }
6517
 
6518
+ $message = $this->addLocationToMessage($message);
6519
+ $message .= "\nThis will be an error in future versions of Sass.";
6520
+ $this->logger->warn($message, true);
6521
+ }
6522
+
6523
+ if ($nameUsed < \count($names)) {
6524
+ $unknownNames = array_values(array_diff($names, array_column($prototype['arguments'], 0)));
6525
+ $lastName = array_pop($unknownNames);
6526
+ $message = sprintf(
6527
+ 'No argument%s named $%s%s.',
6528
+ $unknownNames ? 's' : '',
6529
+ $unknownNames ? implode(', $', $unknownNames) . ' or $' : '',
6530
+ $lastName
6531
+ );
6532
+ throw new SassScriptException($message);
6533
+ }
6534
+ }
6535
 
6536
+ /**
6537
+ * Evaluates the argument from the invocation.
6538
+ *
6539
+ * This returns several things about this invocation:
6540
+ * - the list of positional arguments
6541
+ * - the map of named arguments, indexed by normalized names
6542
+ * - the set of names used in the arguments (that's an array using the normalized names as keys for O(1) access)
6543
+ * - the separator used by the list using the splat operator, if any
6544
+ * - a boolean indicator whether any splat argument (list or map) was used, to support the incomplete error reporting.
6545
+ *
6546
+ * @param array[] $args
6547
+ * @param bool $reduce Whether arguments should be reduced to their value
6548
+ *
6549
+ * @return array
6550
+ *
6551
+ * @throws SassScriptException
6552
+ *
6553
+ * @phpstan-return array{0: list<array|Number>, 1: array<string, array|Number>, 2: array<string, string>, 3: string|null, 4: bool}
6554
+ */
6555
+ private function evaluateArguments(array $args, $reduce = true)
6556
+ {
6557
+ // this represents trailing commas
6558
+ if (\count($args) && end($args) === static::$null) {
6559
+ array_pop($args);
6560
  }
6561
 
6562
+ $splatSeparator = null;
6563
+ $keywordArgs = [];
6564
+ $names = [];
6565
+ $positionalArgs = [];
6566
+ $hasKeywordArgument = false;
6567
+ $hasSplat = false;
6568
 
6569
+ foreach ($args as $arg) {
6570
+ if (!empty($arg[0])) {
 
6571
  $hasKeywordArgument = true;
6572
 
6573
+ assert(\is_string($arg[0][1]));
6574
+ $name = str_replace('_', '-', $arg[0][1]);
6575
 
6576
+ if (isset($keywordArgs[$name])) {
6577
+ throw new SassScriptException(sprintf('Duplicate named argument $%s.', $arg[0][1]));
 
 
 
 
 
6578
  }
6579
 
6580
+ $keywordArgs[$name] = $this->maybeReduce($reduce, $arg[1]);
6581
+ $names[$name] = $name;
 
 
 
 
 
 
 
 
 
6582
  } elseif (! empty($arg[2])) {
6583
  // $arg[2] means a var followed by ... in the arg ($list... )
6584
  $val = $this->reduce($arg[1], true);
6585
+ $hasSplat = true;
6586
 
6587
  if ($val[0] === Type::T_LIST) {
6588
+ foreach ($val[2] as $item) {
6589
+ if (\is_null($splatSeparator)) {
6590
+ $splatSeparator = $val[1];
6591
+ }
 
 
 
 
 
 
6592
 
6593
+ $positionalArgs[] = $this->maybeReduce($reduce, $item);
6594
+ }
6595
+
6596
+ if (isset($val[3]) && \is_array($val[3])) {
6597
+ foreach ($val[3] as $name => $item) {
6598
+ assert(\is_string($name));
6599
+
6600
+ $normalizedName = str_replace('_', '-', $name);
6601
+
6602
+ if (isset($keywordArgs[$normalizedName])) {
6603
+ throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name));
6604
  }
6605
 
6606
+ $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item);
6607
+ $names[$normalizedName] = $normalizedName;
6608
+ $hasKeywordArgument = true;
6609
  }
6610
  }
6611
  } elseif ($val[0] === Type::T_MAP) {
6614
  $item = $val[2][$i];
6615
 
6616
  if (! is_numeric($name)) {
6617
+ $normalizedName = str_replace('_', '-', $name);
 
 
 
 
 
 
 
6618
 
6619
+ if (isset($keywordArgs[$normalizedName])) {
6620
+ throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name));
 
 
6621
  }
6622
+
6623
+ $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item);
6624
+ $names[$normalizedName] = $normalizedName;
6625
+ $hasKeywordArgument = true;
6626
  } else {
6627
  if (\is_null($splatSeparator)) {
6628
  $splatSeparator = $val[1];
6629
  }
6630
 
6631
+ $positionalArgs[] = $this->maybeReduce($reduce, $item);
6632
  }
6633
  }
6634
+ } elseif ($val[0] !== Type::T_NULL) { // values other than null are treated a single-element lists, while null is the empty list
6635
+ $positionalArgs[] = $this->maybeReduce($reduce, $val);
6636
  }
6637
  } elseif ($hasKeywordArgument) {
6638
+ throw new SassScriptException('Positional arguments must come before keyword arguments.');
6639
  } else {
6640
+ $positionalArgs[] = $this->maybeReduce($reduce, $arg[1]);
6641
  }
6642
  }
6643
 
6644
+ return [$positionalArgs, $keywordArgs, $names, $splatSeparator, $hasSplat];
6645
+ }
6646
+
6647
+ /**
6648
+ * @param bool $reduce
6649
+ * @param array|Number $value
6650
+ *
6651
+ * @return array|Number
6652
+ */
6653
+ private function maybeReduce($reduce, $value)
6654
+ {
6655
+ if ($reduce) {
6656
+ return $this->reduce($value, true);
6657
+ }
6658
+
6659
+ return $value;
6660
+ }
6661
+
6662
+ /**
6663
+ * Apply argument values per definition
6664
+ *
6665
+ * @param array[] $argDef
6666
+ * @param array|null $argValues
6667
+ * @param boolean $storeInEnv
6668
+ * @param boolean $reduce
6669
+ * only used if $storeInEnv = false
6670
+ *
6671
+ * @return array<string, array|Number>
6672
+ *
6673
+ * @phpstan-param list<array{0: string, 1: array|Number|null, 2: bool}> $argDef
6674
+ *
6675
+ * @throws \Exception
6676
+ */
6677
+ protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true)
6678
+ {
6679
+ $output = [];
6680
+
6681
+ if (\is_null($argValues)) {
6682
+ $argValues = [];
6683
+ }
6684
+
6685
+ if ($storeInEnv) {
6686
+ $storeEnv = $this->getStoreEnv();
6687
+
6688
+ $env = new Environment();
6689
+ $env->store = $storeEnv->store;
6690
+ }
6691
+
6692
+ $prototype = ['arguments' => [], 'rest_argument' => null];
6693
+ $originalRestArgumentName = null;
6694
+
6695
+ foreach ($argDef as $i => $arg) {
6696
+ list($name, $default, $isVariable) = $arg;
6697
+ $normalizedName = str_replace('_', '-', $name);
6698
 
6699
  if ($isVariable) {
6700
+ $originalRestArgumentName = $name;
6701
+ $prototype['rest_argument'] = $normalizedName;
6702
+ } else {
6703
+ $prototype['arguments'][] = [$normalizedName, $name, !empty($default) ? $default : null];
6704
+ }
6705
+ }
 
6706
 
6707
+ list($positionalArgs, $namedArgs, $names, $splatSeparator, $hasSplat) = $this->evaluateArguments($argValues, $reduce);
6708
 
6709
+ $this->verifyPrototype($prototype, \count($positionalArgs), $names, $hasSplat);
 
 
6710
 
6711
+ $vars = $this->applyArgumentsToDeclaration($prototype, $positionalArgs, $namedArgs, $splatSeparator);
 
 
6712
 
6713
+ foreach ($prototype['arguments'] as $argument) {
6714
+ list($normalizedName, $name) = $argument;
6715
+
6716
+ if (!isset($vars[$normalizedName])) {
 
 
 
 
6717
  continue;
6718
+ }
6719
+
6720
+ $val = $vars[$normalizedName];
6721
+
6722
+ if ($storeInEnv) {
6723
+ $this->set($name, $this->reduce($val, true), true, $env);
6724
  } else {
6725
+ $output[$name] = ($reduce ? $this->reduce($val, true) : $val);
6726
  }
6727
+ }
6728
+
6729
+ if ($prototype['rest_argument'] !== null) {
6730
+ assert($originalRestArgumentName !== null);
6731
+ $name = $originalRestArgumentName;
6732
+ $val = $vars[$prototype['rest_argument']];
6733
 
6734
  if ($storeInEnv) {
6735
  $this->set($name, $this->reduce($val, true), true, $env);
6742
  $storeEnv->store = $env->store;
6743
  }
6744
 
6745
+ foreach ($prototype['arguments'] as $argument) {
6746
+ list($normalizedName, $name, $default) = $argument;
6747
 
6748
+ if (isset($vars[$normalizedName])) {
6749
  continue;
6750
  }
6751
+ assert($default !== null);
6752
 
6753
  if ($storeInEnv) {
6754
  $this->set($name, $this->reduce($default, true), true);
6760
  return $output;
6761
  }
6762
 
6763
+ /**
6764
+ * Apply argument values per definition.
6765
+ *
6766
+ * This method assumes that the arguments are valid for the provided prototype.
6767
+ * The validation with {@see verifyPrototype} must have been run before calling
6768
+ * it.
6769
+ * Arguments are returned as a map from the normalized argument names to the
6770
+ * value. Additional arguments are collected in a sass argument list available
6771
+ * under the name of the rest argument in the result.
6772
+ *
6773
+ * Defaults are not applied as they are resolved in a different environment.
6774
+ *
6775
+ * @param array $prototype
6776
+ * @param array<array|Number> $positionalArgs
6777
+ * @param array<string, array|Number> $namedArgs
6778
+ * @param string|null $splatSeparator
6779
+ *
6780
+ * @return array<string, array|Number>
6781
+ *
6782
+ * @phpstan-param array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null} $prototype
6783
+ */
6784
+ private function applyArgumentsToDeclaration(array $prototype, array $positionalArgs, array $namedArgs, $splatSeparator)
6785
+ {
6786
+ $output = [];
6787
+ $minLength = min(\count($positionalArgs), \count($prototype['arguments']));
6788
+
6789
+ for ($i = 0; $i < $minLength; $i++) {
6790
+ list($name) = $prototype['arguments'][$i];
6791
+ $val = $positionalArgs[$i];
6792
+
6793
+ $output[$name] = $val;
6794
+ }
6795
+
6796
+ $restNamed = $namedArgs;
6797
+
6798
+ for ($i = \count($positionalArgs); $i < \count($prototype['arguments']); $i++) {
6799
+ $argument = $prototype['arguments'][$i];
6800
+ list($name) = $argument;
6801
+
6802
+ if (isset($namedArgs[$name])) {
6803
+ $val = $namedArgs[$name];
6804
+ unset($restNamed[$name]);
6805
+ } else {
6806
+ continue;
6807
+ }
6808
+
6809
+ $output[$name] = $val;
6810
+ }
6811
+
6812
+ if ($prototype['rest_argument'] !== null) {
6813
+ $name = $prototype['rest_argument'];
6814
+ $rest = array_values(array_slice($positionalArgs, \count($prototype['arguments'])));
6815
+
6816
+ $val = [Type::T_LIST, \is_null($splatSeparator) ? ',' : $splatSeparator , $rest, $restNamed];
6817
+
6818
+ $output[$name] = $val;
6819
+ }
6820
+
6821
+ return $output;
6822
+ }
6823
+
6824
  /**
6825
  * Coerce a php value into a scss one
6826
  *
6827
  * @param mixed $value
6828
  *
6829
+ * @return array|Number
6830
  */
6831
  protected function coerceValue($value)
6832
  {
6833
+ if (\is_array($value) || $value instanceof Number) {
6834
  return $value;
6835
  }
6836
 
6843
  }
6844
 
6845
  if (is_numeric($value)) {
6846
+ return new Number($value, '');
6847
  }
6848
 
6849
  if ($value === '') {
6863
  /**
6864
  * Coerce something to map
6865
  *
6866
+ * @param array|Number $item
6867
  *
6868
+ * @return array|Number
6869
  */
6870
  protected function coerceMap($item)
6871
  {
6874
  }
6875
 
6876
  if (
6877
+ $item[0] === Type::T_LIST &&
6878
+ $item[2] === []
 
6879
  ) {
6880
  return static::$emptyMap;
6881
  }
6886
  /**
6887
  * Coerce something to list
6888
  *
6889
+ * @param array|Number $item
6890
+ * @param string $delim
6891
+ * @param boolean $removeTrailingNull
6892
  *
6893
  * @return array
6894
  */
6895
  protected function coerceList($item, $delim = ',', $removeTrailingNull = false)
6896
  {
6897
+ if ($item instanceof Number) {
6898
+ return [Type::T_LIST, $delim, [$item]];
6899
+ }
6900
+
6901
+ if ($item[0] === Type::T_LIST) {
6902
  // remove trailing null from the list
6903
  if ($removeTrailingNull && end($item[2]) === static::$null) {
6904
  array_pop($item[2]);
6907
  return $item;
6908
  }
6909
 
6910
+ if ($item[0] === Type::T_MAP) {
6911
  $keys = $item[1];
6912
  $values = $item[2];
6913
  $list = [];
6938
  return [Type::T_LIST, ',', $list];
6939
  }
6940
 
6941
+ return [Type::T_LIST, $delim, [$item]];
6942
  }
6943
 
6944
  /**
6945
  * Coerce color for expression
6946
  *
6947
+ * @param array|Number $value
6948
  *
6949
+ * @return array|Number
6950
  */
6951
  protected function coerceForExpression($value)
6952
  {
6960
  /**
6961
  * Coerce value to color
6962
  *
6963
+ * @param array|Number $value
6964
+ * @param bool $inRGBFunction
6965
  *
6966
  * @return array|null
6967
  */
6968
  protected function coerceColor($value, $inRGBFunction = false)
6969
  {
6970
+ if ($value instanceof Number) {
6971
+ return null;
6972
+ }
6973
+
6974
  switch ($value[0]) {
6975
  case Type::T_COLOR:
6976
  for ($i = 1; $i <= 3; $i++) {
7056
  if ($color[3] === 255) {
7057
  $color[3] = 1; // fully opaque
7058
  } else {
7059
+ $color[3] = round($color[3] / 255, Number::PRECISION);
7060
  }
7061
  }
7062
 
7079
  }
7080
 
7081
  /**
7082
+ * @param integer|Number $value
7083
+ * @param boolean $isAlpha
7084
  *
7085
  * @return integer|mixed
7086
  */
7098
  * @param integer|float $min
7099
  * @param integer|float $max
7100
  * @param boolean $isInt
 
 
7101
  *
7102
  * @return integer|mixed
7103
  */
7104
+ protected function compileColorPartValue($value, $min, $max, $isInt = true)
7105
  {
7106
  if (! is_numeric($value)) {
7107
  if (\is_array($value)) {
7108
  $reduced = $this->reduce($value);
7109
 
7110
+ if ($reduced instanceof Number) {
7111
  $value = $reduced;
7112
  }
7113
  }
7114
 
7115
+ if ($value instanceof Number) {
7116
+ if ($value->unitless()) {
7117
+ $num = $value->getDimension();
7118
+ } elseif ($value->hasUnit('%')) {
7119
+ $num = $max * $value->getDimension() / 100;
7120
+ } else {
7121
+ throw $this->error('Expected %s to have no units or "%%".', $value);
 
 
 
 
 
 
 
7122
  }
7123
 
7124
  $value = $num;
7132
  $value = round($value);
7133
  }
7134
 
7135
+ $value = min($max, max($min, $value));
 
 
 
 
 
 
 
 
 
 
 
7136
 
7137
  return $value;
7138
  }
7143
  /**
7144
  * Coerce value to string
7145
  *
7146
+ * @param array|Number $value
7147
  *
7148
+ * @return array
7149
  */
7150
  protected function coerceString($value)
7151
  {
7157
  }
7158
 
7159
  /**
7160
+ * Assert value is a string
7161
+ *
7162
+ * This method deals with internal implementation details of the value
7163
+ * representation where unquoted strings can sometimes be stored under
7164
+ * other types.
7165
+ * The returned value is always using the T_STRING type.
7166
  *
7167
  * @api
7168
  *
7169
+ * @param array|Number $value
7170
+ * @param string|null $varName
7171
  *
7172
  * @return array
7173
  *
7174
+ * @throws SassScriptException
7175
  */
7176
  public function assertString($value, $varName = null)
7177
  {
7182
 
7183
  if (! \in_array($value[0], [Type::T_STRING, Type::T_KEYWORD])) {
7184
  $value = $this->compileValue($value);
7185
+ throw SassScriptException::forArgument("$value is not a string.", $varName);
 
7186
  }
7187
 
7188
+ return $this->coerceString($value);
 
 
7189
  }
7190
 
7191
  /**
7192
  * Coerce value to a percentage
7193
  *
7194
+ * @param array|Number $value
7195
  *
7196
  * @return integer|float
7197
  */
7198
  protected function coercePercent($value)
7199
  {
7200
+ if ($value instanceof Number) {
7201
+ if ($value->hasUnit('%')) {
7202
+ return $value->getDimension() / 100;
7203
  }
7204
 
7205
+ return $value->getDimension();
7206
  }
7207
 
7208
  return 0;
7213
  *
7214
  * @api
7215
  *
7216
+ * @param array|Number $value
7217
+ * @param string|null $varName
7218
  *
7219
  * @return array
7220
  *
7221
+ * @throws SassScriptException
7222
  */
7223
+ public function assertMap($value, $varName = null)
7224
  {
7225
  $value = $this->coerceMap($value);
7226
 
7227
  if ($value[0] !== Type::T_MAP) {
7228
+ $value = $this->compileValue($value);
7229
+
7230
+ throw SassScriptException::forArgument("$value is not a map.", $varName);
7231
  }
7232
 
7233
  return $value;
7238
  *
7239
  * @api
7240
  *
7241
+ * @param array|Number $value
7242
  *
7243
  * @return array
7244
  *
7253
  return $value;
7254
  }
7255
 
7256
+ /**
7257
+ * Gets the keywords of an argument list.
7258
+ *
7259
+ * Keys in the returned array are normalized names (underscores are replaced with dashes)
7260
+ * without the leading `$`.
7261
+ * Calling this helper with anything that an argument list received for a rest argument
7262
+ * of the function argument declaration is not supported.
7263
+ *
7264
+ * @param array|Number $value
7265
+ *
7266
+ * @return array<string, array|Number>
7267
+ */
7268
+ public function getArgumentListKeywords($value)
7269
+ {
7270
+ if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) {
7271
+ throw new \InvalidArgumentException('The argument is not a sass argument list.');
7272
+ }
7273
+
7274
+ return $value[3];
7275
+ }
7276
+
7277
  /**
7278
  * Assert value is a color
7279
  *
7280
  * @api
7281
  *
7282
+ * @param array|Number $value
7283
+ * @param string|null $varName
7284
  *
7285
  * @return array
7286
  *
7287
+ * @throws SassScriptException
7288
  */
7289
+ public function assertColor($value, $varName = null)
7290
  {
7291
  if ($color = $this->coerceColor($value)) {
7292
  return $color;
7293
  }
7294
 
7295
+ $value = $this->compileValue($value);
7296
+
7297
+ throw SassScriptException::forArgument("$value is not a color.", $varName);
7298
  }
7299
 
7300
  /**
7302
  *
7303
  * @api
7304
  *
7305
+ * @param array|Number $value
7306
+ * @param string|null $varName
7307
  *
7308
+ * @return Number
7309
  *
7310
+ * @throws SassScriptException
7311
  */
7312
  public function assertNumber($value, $varName = null)
7313
  {
7314
+ if (!$value instanceof Number) {
7315
  $value = $this->compileValue($value);
7316
+ throw SassScriptException::forArgument("$value is not a number.", $varName);
 
7317
  }
7318
 
7319
+ return $value;
7320
  }
7321
 
7322
  /**
7324
  *
7325
  * @api
7326
  *
7327
+ * @param array|Number $value
7328
+ * @param string|null $varName
7329
  *
7330
+ * @return integer
7331
  *
7332
+ * @throws SassScriptException
7333
  */
7334
  public function assertInteger($value, $varName = null)
7335
  {
7336
+ $value = $this->assertNumber($value, $varName)->getDimension();
7337
+ if (round($value - \intval($value), Number::PRECISION) > 0) {
7338
+ throw SassScriptException::forArgument("$value is not an integer.", $varName);
 
 
7339
  }
7340
 
7341
  return intval($value);
7342
  }
7343
 
7344
+ /**
7345
+ * Extract the ... / alpha on the last argument of channel arg
7346
+ * in color functions
7347
+ *
7348
+ * @param array $args
7349
+ * @return array
7350
+ */
7351
+ private function extractSlashAlphaInColorFunction($args)
7352
+ {
7353
+ $last = end($args);
7354
+ if (\count($args) === 3 && $last[0] === Type::T_EXPRESSION && $last[1] === '/') {
7355
+ array_pop($args);
7356
+ $args[] = $last[2];
7357
+ $args[] = $last[3];
7358
+ }
7359
+ return $args;
7360
+ }
7361
+
7362
 
7363
  /**
7364
  * Make sure a color's components don't go out of bounds
7377
  if ($c[$i] > 255) {
7378
  $c[$i] = 255;
7379
  }
7380
+
7381
+ if (!\is_int($c[$i])) {
7382
+ $c[$i] = round($c[$i]);
7383
+ }
7384
  }
7385
 
7386
  return $c;
7389
  /**
7390
  * Convert RGB to HSL
7391
  *
7392
+ * @internal
7393
  *
7394
  * @param integer $red
7395
  * @param integer $green
7461
  /**
7462
  * Convert HSL to RGB
7463
  *
7464
+ * @internal
7465
  *
7466
+ * @param int|float $hue H from 0 to 360
7467
+ * @param int|float $saturation S from 0 to 100
7468
+ * @param int|float $lightness L from 0 to 100
7469
  *
7470
  * @return array
7471
  */
7491
  return $out;
7492
  }
7493
 
7494
+ /**
7495
+ * Convert HWB to RGB
7496
+ * https://www.w3.org/TR/css-color-4/#hwb-to-rgb
7497
+ *
7498
+ * @api
7499
+ *
7500
+ * @param integer $hue H from 0 to 360
7501
+ * @param integer $whiteness W from 0 to 100
7502
+ * @param integer $blackness B from 0 to 100
7503
+ *
7504
+ * @return array
7505
+ */
7506
+ private function HWBtoRGB($hue, $whiteness, $blackness)
7507
+ {
7508
+ $w = min(100, max(0, $whiteness)) / 100;
7509
+ $b = min(100, max(0, $blackness)) / 100;
7510
+
7511
+ $sum = $w + $b;
7512
+ if ($sum > 1.0) {
7513
+ $w = $w / $sum;
7514
+ $b = $b / $sum;
7515
+ }
7516
+ $b = min(1.0 - $w, $b);
7517
+
7518
+ $rgb = $this->toRGB($hue, 100, 50);
7519
+ for($i = 1; $i < 4; $i++) {
7520
+ $rgb[$i] *= (1.0 - $w - $b);
7521
+ $rgb[$i] = round($rgb[$i] + 255 * $w + 0.0001);
7522
+ }
7523
+
7524
+ return $rgb;
7525
+ }
7526
+
7527
+ /**
7528
+ * Convert RGB to HWB
7529
+ *
7530
+ * @api
7531
+ *
7532
+ * @param integer $red
7533
+ * @param integer $green
7534
+ * @param integer $blue
7535
+ *
7536
+ * @return array
7537
+ */
7538
+ private function RGBtoHWB($red, $green, $blue)
7539
+ {
7540
+ $min = min($red, $green, $blue);
7541
+ $max = max($red, $green, $blue);
7542
+
7543
+ $d = $max - $min;
7544
+
7545
+ if ((int) $d === 0) {
7546
+ $h = 0;
7547
+ } else {
7548
+
7549
+ if ($red == $max) {
7550
+ $h = 60 * ($green - $blue) / $d;
7551
+ } elseif ($green == $max) {
7552
+ $h = 60 * ($blue - $red) / $d + 120;
7553
+ } elseif ($blue == $max) {
7554
+ $h = 60 * ($red - $green) / $d + 240;
7555
+ }
7556
+ }
7557
+
7558
+ return [Type::T_HWB, fmod($h, 360), $min / 255 * 100, 100 - $max / 255 *100];
7559
+ }
7560
+
7561
+
7562
  // Built in functions
7563
 
7564
  protected static $libCall = ['function', 'args...'];
7565
+ protected function libCall($args)
7566
  {
7567
+ $functionReference = $args[0];
7568
 
7569
  if (in_array($functionReference[0], [Type::T_STRING, Type::T_KEYWORD])) {
7570
+ $name = $this->compileStringContent($this->coerceString($functionReference));
7571
+ $warning = "Passing a string to call() is deprecated and will be illegal\n"
7572
  . "in Sass 4.0. Use call(function-reference($name)) instead.";
7573
+ Warn::deprecation($warning);
7574
+ $functionReference = $this->libGetFunction([$this->assertString($functionReference, 'function')]);
7575
  }
7576
 
7577
  if ($functionReference === static::$null) {
7582
  throw $this->error('Function reference expected, got ' . $functionReference[0]);
7583
  }
7584
 
7585
+ $callArgs = [
7586
+ [null, $args[1], true]
7587
+ ];
 
 
 
 
 
 
 
 
 
7588
 
7589
  return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]);
7590
  }
7596
  ];
7597
  protected function libGetFunction($args)
7598
  {
7599
+ $name = $this->compileStringContent($this->assertString(array_shift($args), 'name'));
7600
  $isCss = false;
7601
 
7602
  if (count($args)) {
7603
+ $isCss = array_shift($args);
7604
  $isCss = (($isCss === static::$true) ? true : false);
7605
  }
7606
 
7641
  return static::$null;
7642
  }
7643
 
7644
+ // Numbers are represented with value objects, for which the PHP equality operator does not
7645
+ // match the Sass rules (and we cannot overload it). As they are the only type of values
7646
+ // represented with a value object for now, they require a special case.
7647
+ if ($value instanceof Number) {
7648
+ $key = 0;
7649
+ foreach ($list[2] as $item) {
7650
+ $key++;
7651
+ $itemValue = $this->normalizeValue($item);
7652
+
7653
+ if ($itemValue instanceof Number && $value->equals($itemValue)) {
7654
+ return new Number($key, '');
7655
+ }
7656
+ }
7657
+ return static::$null;
7658
+ }
7659
+
7660
  $values = [];
7661
 
7662
+
7663
  foreach ($list[2] as $item) {
7664
  $values[] = $this->normalizeValue($item);
7665
  }
7666
 
7667
  $key = array_search($this->normalizeValue($value), $values);
7668
 
7669
+ return false === $key ? static::$null : new Number($key + 1, '');
7670
  }
7671
 
7672
  protected static $libRgb = [
7733
  return $this->libRgb($args, $kwargs, 'rgba');
7734
  }
7735
 
7736
+ /**
7737
+ * Helper function for adjust_color, change_color, and scale_color
7738
+ *
7739
+ * @param array<array|Number> $args
7740
+ * @param string $operation
7741
+ * @param callable $fn
7742
+ *
7743
+ * @return array
7744
+ *
7745
+ * @phpstan-param callable(float|int, float|int|null, float|int): (float|int) $fn
7746
+ */
7747
+ protected function alterColor(array $args, $operation, $fn)
7748
+ {
7749
+ $color = $this->assertColor($args[0], 'color');
7750
+
7751
+ if ($args[1][2]) {
7752
+ throw new SassScriptException('Only one positional argument is allowed. All other arguments must be passed by name.');
7753
+ }
7754
+
7755
+ $kwargs = $this->getArgumentListKeywords($args[1]);
7756
+
7757
+ $scale = $operation === 'scale';
7758
+ $change = $operation === 'change';
7759
+
7760
+ /**
7761
+ * @param string $name
7762
+ * @param float|int $max
7763
+ * @param bool $checkPercent
7764
+ * @param bool $assertPercent
7765
+ *
7766
+ * @return float|int|null
7767
+ */
7768
+ $getParam = function ($name, $max, $checkPercent = false, $assertPercent = false) use (&$kwargs, $scale, $change) {
7769
+ if (!isset($kwargs[$name])) {
7770
+ return null;
7771
+ }
7772
+
7773
+ $number = $this->assertNumber($kwargs[$name], $name);
7774
+ unset($kwargs[$name]);
7775
+
7776
+ if (!$scale && $checkPercent) {
7777
+ if (!$number->hasUnit('%')) {
7778
+ $warning = $this->error("{$name} Passing a number `$number` without unit % is deprecated.");
7779
+ $this->logger->warn($warning->getMessage(), true);
7780
+ }
7781
+ }
7782
+
7783
+ if ($scale || $assertPercent) {
7784
+ $number->assertUnit('%', $name);
7785
+ }
7786
+
7787
+ if ($scale) {
7788
+ $max = 100;
7789
+ }
7790
+
7791
+ return $number->valueInRange($change ? 0 : -$max, $max, $name);
7792
+ };
7793
+
7794
+ $alpha = $getParam('alpha', 1);
7795
+ $red = $getParam('red', 255);
7796
+ $green = $getParam('green', 255);
7797
+ $blue = $getParam('blue', 255);
7798
 
7799
+ if ($scale || !isset($kwargs['hue'])) {
7800
+ $hue = null;
7801
+ } else {
7802
+ $hueNumber = $this->assertNumber($kwargs['hue'], 'hue');
7803
+ unset($kwargs['hue']);
7804
+ $hue = $hueNumber->getDimension();
7805
+ }
7806
+ $saturation = $getParam('saturation', 100, true);
7807
+ $lightness = $getParam('lightness', 100, true);
7808
+ $whiteness = $getParam('whiteness', 100, false, true);
7809
+ $blackness = $getParam('blackness', 100, false, true);
7810
+
7811
+ if (!empty($kwargs)) {
7812
+ $unknownNames = array_keys($kwargs);
7813
+ $lastName = array_pop($unknownNames);
7814
+ $message = sprintf(
7815
+ 'No argument%s named $%s%s.',
7816
+ $unknownNames ? 's' : '',
7817
+ $unknownNames ? implode(', $', $unknownNames) . ' or $' : '',
7818
+ $lastName
7819
+ );
7820
+ throw new SassScriptException($message);
7821
+ }
7822
 
7823
+ $hasRgb = $red !== null || $green !== null || $blue !== null;
7824
+ $hasSL = $saturation !== null || $lightness !== null;
7825
+ $hasWB = $whiteness !== null || $blackness !== null;
7826
+ $found = false;
7827
 
7828
+ if ($hasRgb && ($hasSL || $hasWB || $hue !== null)) {
7829
+ throw new SassScriptException(sprintf('RGB parameters may not be passed along with %s parameters.', $hasWB ? 'HWB' : 'HSL'));
7830
  }
7831
 
7832
+ if ($hasWB && $hasSL) {
7833
+ throw new SassScriptException('HSL parameters may not be passed along with HWB parameters.');
7834
+ }
7835
+
7836
+ if ($hasRgb) {
7837
+ $color[1] = round($fn($color[1], $red, 255));
7838
+ $color[2] = round($fn($color[2], $green, 255));
7839
+ $color[3] = round($fn($color[3], $blue, 255));
7840
+ } elseif ($hasWB) {
7841
+ $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]);
7842
+ if ($hue !== null) {
7843
+ $hwb[1] = $change ? $hue : $hwb[1] + $hue;
7844
+ }
7845
+ $hwb[2] = $fn($hwb[2], $whiteness, 100);
7846
+ $hwb[3] = $fn($hwb[3], $blackness, 100);
7847
+
7848
+ $rgb = $this->HWBtoRGB($hwb[1], $hwb[2], $hwb[3]);
7849
+
7850
+ if (isset($color[4])) {
7851
+ $rgb[4] = $color[4];
7852
+ }
7853
+
7854
+ $color = $rgb;
7855
+ } elseif ($hue !== null || $hasSL) {
7856
  $hsl = $this->toHSL($color[1], $color[2], $color[3]);
7857
 
7858
+ if ($hue !== null) {
7859
+ $hsl[1] = $change ? $hue : $hsl[1] + $hue;
 
 
 
7860
  }
7861
+ $hsl[2] = $fn($hsl[2], $saturation, 100);
7862
+ $hsl[3] = $fn($hsl[3], $lightness, 100);
7863
 
7864
  $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
7865
 
7870
  $color = $rgb;
7871
  }
7872
 
7873
+ if ($alpha !== null) {
7874
+ $existingAlpha = isset($color[4]) ? $color[4] : 1;
7875
+ $color[4] = $fn($existingAlpha, $alpha, 1);
7876
+ }
7877
+
7878
  return $color;
7879
  }
7880
 
7881
+ protected static $libAdjustColor = ['color', 'kwargs...'];
 
 
 
7882
  protected function libAdjustColor($args)
7883
  {
7884
+ return $this->alterColor($args, 'adjust', function ($base, $alter, $max) {
7885
+ if ($alter === null) {
7886
+ return $base;
7887
+ }
7888
+
7889
+ $new = $base + $alter;
7890
+
7891
+ if ($new < 0) {
7892
+ return 0;
7893
+ }
7894
+
7895
+ if ($new > $max) {
7896
+ return $max;
7897
+ }
7898
+
7899
+ return $new;
7900
  });
7901
  }
7902
 
7903
+ protected static $libChangeColor = ['color', 'kwargs...'];
 
 
 
7904
  protected function libChangeColor($args)
7905
  {
7906
+ return $this->alterColor($args,'change', function ($base, $alter, $max) {
7907
+ if ($alter === null) {
7908
+ return $base;
7909
+ }
7910
+
7911
  return $alter;
7912
  });
7913
  }
7914
 
7915
+ protected static $libScaleColor = ['color', 'kwargs...'];
 
 
 
7916
  protected function libScaleColor($args)
7917
  {
7918
+ return $this->alterColor($args, 'scale', function ($base, $scale, $max) {
7919
+ if ($scale === null) {
7920
+ return $base;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7921
  }
7922
 
7923
  $scale = $scale / 100;
7936
  $color = $this->coerceColor($args[0]);
7937
 
7938
  if (\is_null($color)) {
7939
+ throw $this->error('Error: argument `$color` of `ie-hex-str($color)` must be a color');
7940
  }
7941
 
7942
  $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
7950
  $color = $this->coerceColor($args[0]);
7951
 
7952
  if (\is_null($color)) {
7953
+ throw $this->error('Error: argument `$color` of `red($color)` must be a color');
7954
  }
7955
 
7956
+ return new Number((int) $color[1], '');
7957
  }
7958
 
7959
  protected static $libGreen = ['color'];
7962
  $color = $this->coerceColor($args[0]);
7963
 
7964
  if (\is_null($color)) {
7965
+ throw $this->error('Error: argument `$color` of `green($color)` must be a color');
7966
  }
7967
 
7968
+ return new Number((int) $color[2], '');
7969
  }
7970
 
7971
  protected static $libBlue = ['color'];
7974
  $color = $this->coerceColor($args[0]);
7975
 
7976
  if (\is_null($color)) {
7977
+ throw $this->error('Error: argument `$color` of `blue($color)` must be a color');
7978
  }
7979
 
7980
+ return new Number((int) $color[3], '');
7981
  }
7982
 
7983
  protected static $libAlpha = ['color'];
7984
  protected function libAlpha($args)
7985
  {
7986
  if ($color = $this->coerceColor($args[0])) {
7987
+ return new Number(isset($color[4]) ? $color[4] : 1, '');
7988
  }
7989
 
7990
  // this might be the IE function, so return value unchanged
7996
  {
7997
  $value = $args[0];
7998
 
7999
+ if ($value instanceof Number) {
8000
  return null;
8001
  }
8002
 
8012
  {
8013
  list($first, $second, $weight) = $args;
8014
 
8015
+ $first = $this->assertColor($first, 'color1');
8016
+ $second = $this->assertColor($second, 'color2');
8017
+ $weight = $this->coercePercent($this->assertNumber($weight, 'weight'));
 
 
 
 
 
8018
 
8019
  $firstAlpha = isset($first[4]) ? $first[4] : 1;
8020
  $secondAlpha = isset($second[4]) ? $second[4] : 1;
8040
 
8041
  protected static $libHsl = [
8042
  ['channels'],
8043
+ ['hue', 'saturation'],
8044
  ['hue', 'saturation', 'lightness'],
8045
  ['hue', 'saturation', 'lightness', 'alpha'] ];
8046
  protected function libHsl($args, $kwargs, $funcName = 'hsl')
8056
  $args_to_check = $kwargs['channels'][2];
8057
  }
8058
 
8059
+ if (\count($args) === 2) {
8060
+ // if var() is used as an argument, return as a css function
8061
+ foreach ($args as $arg) {
8062
+ if ($arg[0] === Type::T_FUNCTION && in_array($arg[1], ['var'])) {
8063
+ return null;
8064
+ }
8065
+ }
8066
+
8067
+ throw new SassScriptException('Missing argument $lightness.');
8068
+ }
8069
 
8070
  foreach ($kwargs as $k => $arg) {
8071
+ if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) {
8072
  return null;
8073
  }
8074
  }
8075
 
8076
  foreach ($args_to_check as $k => $arg) {
8077
+ if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) {
8078
  if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) {
8079
  return null;
8080
  }
8081
 
8082
  $args[$k] = $this->stringifyFncallArgs($arg);
 
8083
  }
8084
 
8085
  if (
8091
  }
8092
  }
8093
 
8094
+ $hue = $this->reduce($args[0]);
8095
+ $saturation = $this->reduce($args[1]);
8096
+ $lightness = $this->reduce($args[2]);
8097
  $alpha = null;
8098
 
8099
  if (\count($args) === 4) {
8100
  $alpha = $this->compileColorPartValue($args[3], 0, 100, false);
8101
 
8102
+ if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number || ! is_numeric($alpha)) {
8103
  return [Type::T_STRING, '',
8104
  [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
8105
  }
8106
  } else {
8107
+ if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number) {
8108
  return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
8109
  }
8110
  }
8111
 
8112
+ $hueValue = $hue->getDimension() % 360;
8113
+
8114
+ while ($hueValue < 0) {
8115
+ $hueValue += 360;
8116
+ }
8117
+
8118
+ $color = $this->toRGB($hueValue, max(0, min($saturation->getDimension(), 100)), max(0, min($lightness->getDimension(), 100)));
8119
 
8120
  if (! \is_null($alpha)) {
8121
  $color[4] = $alpha;
8126
 
8127
  protected static $libHsla = [
8128
  ['channels'],
8129
+ ['hue', 'saturation'],
8130
  ['hue', 'saturation', 'lightness'],
8131
  ['hue', 'saturation', 'lightness', 'alpha']];
8132
  protected function libHsla($args, $kwargs)
8137
  protected static $libHue = ['color'];
8138
  protected function libHue($args)
8139
  {
8140
+ $color = $this->assertColor($args[0], 'color');
8141
  $hsl = $this->toHSL($color[1], $color[2], $color[3]);
8142
 
8143
+ return new Number($hsl[1], 'deg');
8144
  }
8145
 
8146
  protected static $libSaturation = ['color'];
8147
  protected function libSaturation($args)
8148
  {
8149
+ $color = $this->assertColor($args[0], 'color');
8150
  $hsl = $this->toHSL($color[1], $color[2], $color[3]);
8151
 
8152
+ return new Number($hsl[2], '%');
8153
  }
8154
 
8155
  protected static $libLightness = ['color'];
8156
  protected function libLightness($args)
8157
  {
8158
+ $color = $this->assertColor($args[0], 'color');
8159
  $hsl = $this->toHSL($color[1], $color[2], $color[3]);
8160
 
8161
+ return new Number($hsl[3], '%');
8162
+ }
8163
+
8164
+ /*
8165
+ * Todo : a integrer dans le futur module color
8166
+ protected static $libHwb = [
8167
+ ['channels'],
8168
+ ['hue', 'whiteness', 'blackness'],
8169
+ ['hue', 'whiteness', 'blackness', 'alpha'] ];
8170
+ protected function libHwb($args, $kwargs, $funcName = 'hwb')
8171
+ {
8172
+ $args_to_check = $args;
8173
+
8174
+ if (\count($args) == 1) {
8175
+ if ($args[0][0] !== Type::T_LIST) {
8176
+ throw $this->error("Missing elements \$whiteness and \$blackness");
8177
+ }
8178
+
8179
+ if (\trim($args[0][1])) {
8180
+ throw $this->error("\$channels must be a space-separated list.");
8181
+ }
8182
+
8183
+ if (! empty($args[0]['enclosing'])) {
8184
+ throw $this->error("\$channels must be an unbracketed list.");
8185
+ }
8186
+
8187
+ $args = $args[0][2];
8188
+ if (\count($args) > 3) {
8189
+ throw $this->error("hwb() : Only 3 elements are allowed but ". \count($args) . "were passed");
8190
+ }
8191
+
8192
+ $args_to_check = $this->extractSlashAlphaInColorFunction($kwargs['channels'][2]);
8193
+ if (\count($args_to_check) !== \count($kwargs['channels'][2])) {
8194
+ $args = $args_to_check;
8195
+ }
8196
+ }
8197
+
8198
+ if (\count($args_to_check) < 2) {
8199
+ throw $this->error("Missing elements \$whiteness and \$blackness");
8200
+ }
8201
+ if (\count($args_to_check) < 3) {
8202
+ throw $this->error("Missing element \$blackness");
8203
+ }
8204
+ if (\count($args_to_check) > 4) {
8205
+ throw $this->error("hwb() : Only 4 elements are allowed but ". \count($args) . "were passed");
8206
+ }
8207
+
8208
+ foreach ($kwargs as $k => $arg) {
8209
+ if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
8210
+ return null;
8211
+ }
8212
+ }
8213
+
8214
+ foreach ($args_to_check as $k => $arg) {
8215
+ if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
8216
+ if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) {
8217
+ return null;
8218
+ }
8219
+
8220
+ $args[$k] = $this->stringifyFncallArgs($arg);
8221
+ }
8222
+
8223
+ if (
8224
+ $k >= 2 && count($args) === 4 &&
8225
+ in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) &&
8226
+ in_array($arg[1], ['calc','env'])
8227
+ ) {
8228
+ return null;
8229
+ }
8230
+ }
8231
+
8232
+ $hue = $this->reduce($args[0]);
8233
+ $whiteness = $this->reduce($args[1]);
8234
+ $blackness = $this->reduce($args[2]);
8235
+ $alpha = null;
8236
+
8237
+ if (\count($args) === 4) {
8238
+ $alpha = $this->compileColorPartValue($args[3], 0, 1, false);
8239
+
8240
+ if (! \is_numeric($alpha)) {
8241
+ $val = $this->compileValue($args[3]);
8242
+ throw $this->error("\$alpha: $val is not a number");
8243
+ }
8244
+ }
8245
+
8246
+ $this->assertNumber($hue, 'hue');
8247
+ $this->assertUnit($whiteness, ['%'], 'whiteness');
8248
+ $this->assertUnit($blackness, ['%'], 'blackness');
8249
+
8250
+ $this->assertRange($whiteness, 0, 100, "0% and 100%", "whiteness");
8251
+ $this->assertRange($blackness, 0, 100, "0% and 100%", "blackness");
8252
+
8253
+ $w = $whiteness->getDimension();
8254
+ $b = $blackness->getDimension();
8255
+
8256
+ $hueValue = $hue->getDimension() % 360;
8257
+
8258
+ while ($hueValue < 0) {
8259
+ $hueValue += 360;
8260
+ }
8261
+
8262
+ $color = $this->HWBtoRGB($hueValue, $w, $b);
8263
+
8264
+ if (! \is_null($alpha)) {
8265
+ $color[4] = $alpha;
8266
+ }
8267
+
8268
+ return $color;
8269
+ }
8270
+
8271
+ protected static $libWhiteness = ['color'];
8272
+ protected function libWhiteness($args, $kwargs, $funcName = 'whiteness') {
8273
+
8274
+ $color = $this->assertColor($args[0]);
8275
+ $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]);
8276
+
8277
+ return new Number($hwb[2], '%');
8278
  }
8279
 
8280
+ protected static $libBlackness = ['color'];
8281
+ protected function libBlackness($args, $kwargs, $funcName = 'blackness') {
8282
+
8283
+ $color = $this->assertColor($args[0]);
8284
+ $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]);
8285
+
8286
+ return new Number($hwb[3], '%');
8287
+ }
8288
+ */
8289
+
8290
  protected function adjustHsl($color, $idx, $amount)
8291
  {
8292
  $hsl = $this->toHSL($color[1], $color[2], $color[3]);
8303
  protected static $libAdjustHue = ['color', 'degrees'];
8304
  protected function libAdjustHue($args)
8305
  {
8306
+ $color = $this->assertColor($args[0], 'color');
8307
+ $degrees = $this->assertNumber($args[1], 'degrees')->getDimension();
8308
 
8309
  return $this->adjustHsl($color, 1, $degrees);
8310
  }
8312
  protected static $libLighten = ['color', 'amount'];
8313
  protected function libLighten($args)
8314
  {
8315
+ $color = $this->assertColor($args[0], 'color');
8316
  $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
8317
 
8318
  return $this->adjustHsl($color, 3, $amount);
8321
  protected static $libDarken = ['color', 'amount'];
8322
  protected function libDarken($args)
8323
  {
8324
+ $color = $this->assertColor($args[0], 'color');
8325
  $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
8326
 
8327
  return $this->adjustHsl($color, 3, -$amount);
8332
  {
8333
  $value = $args[0];
8334
 
 
 
 
 
8335
  if (count($args) === 1) {
8336
+ $this->assertNumber($args[0], 'amount');
8337
+
8338
+ return null;
8339
  }
8340
 
8341
+ $color = $this->assertColor($value, 'color');
8342
+ $amount = 100 * $this->coercePercent($this->assertNumber($args[1], 'amount'));
8343
 
8344
  return $this->adjustHsl($color, 2, $amount);
8345
  }
8347
  protected static $libDesaturate = ['color', 'amount'];
8348
  protected function libDesaturate($args)
8349
  {
8350
+ $color = $this->assertColor($args[0], 'color');
8351
+ $amount = 100 * $this->coercePercent($this->assertNumber($args[1], 'amount'));
8352
 
8353
  return $this->adjustHsl($color, 2, -$amount);
8354
  }
8358
  {
8359
  $value = $args[0];
8360
 
8361
+ if ($value instanceof Number) {
8362
  return null;
8363
  }
8364
 
8365
+ return $this->adjustHsl($this->assertColor($value, 'color'), 2, -100);
8366
  }
8367
 
8368
  protected static $libComplement = ['color'];
8369
  protected function libComplement($args)
8370
  {
8371
+ return $this->adjustHsl($this->assertColor($args[0], 'color'), 1, 180);
8372
  }
8373
 
8374
  protected static $libInvert = ['color', 'weight:1'];
8375
  protected function libInvert($args)
8376
  {
8377
+ $value = $args[0];
 
 
 
 
 
 
8378
 
8379
+ if ($value instanceof Number) {
8380
  return null;
8381
  }
8382
 
8383
+ $weight = $this->coercePercent($this->assertNumber($args[1], 'weight'));
8384
+
8385
+ $color = $this->assertColor($value, 'color');
8386
  $inverted = $color;
8387
  $inverted[1] = 255 - $inverted[1];
8388
  $inverted[2] = 255 - $inverted[2];
8389
  $inverted[3] = 255 - $inverted[3];
8390
 
8391
  if ($weight < 1) {
8392
+ return $this->libMix([$inverted, $color, new Number($weight, '')]);
8393
  }
8394
 
8395
  return $inverted;
8399
  protected static $libOpacify = ['color', 'amount'];
8400
  protected function libOpacify($args)
8401
  {
8402
+ $color = $this->assertColor($args[0], 'color');
8403
+ $amount = $this->coercePercent($this->assertNumber($args[1], 'amount'));
8404
 
8405
  $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
8406
  $color[4] = min(1, max(0, $color[4]));
8418
  protected static $libTransparentize = ['color', 'amount'];
8419
  protected function libTransparentize($args)
8420
  {
8421
+ $color = $this->assertColor($args[0], 'color');
8422
+ $amount = $this->coercePercent($this->assertNumber($args[1], 'amount'));
8423
 
8424
  $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
8425
  $color[4] = min(1, max(0, $color[4]));
8436
  protected static $libUnquote = ['string'];
8437
  protected function libUnquote($args)
8438
  {
8439
+ try {
8440
+ $str = $this->assertString($args[0], 'string');
8441
+ } catch (SassScriptException $e) {
8442
+ $value = $this->compileValue($args[0]);
8443
+ $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
8444
+ $line = $this->sourceLine;
8445
+
8446
+ $message = "Passing $value, a non-string value, to unquote()
8447
+ will be an error in future versions of Sass.\n on line $line of $fname";
8448
 
8449
+ $this->logger->warn($message, true);
8450
+
8451
+ return $args[0];
8452
  }
8453
 
8454
+ $str[1] = '';
8455
+
8456
  return $str;
8457
  }
8458
 
8459
  protected static $libQuote = ['string'];
8460
  protected function libQuote($args)
8461
  {
8462
+ $value = $this->assertString($args[0], 'string');
8463
 
8464
+ $value[1] = '"';
 
 
8465
 
8466
+ return $value;
8467
  }
8468
 
8469
  protected static $libPercentage = ['number'];
8470
  protected function libPercentage($args)
8471
  {
8472
+ $num = $this->assertNumber($args[0], 'number');
8473
+ $num->assertNoUnits('number');
8474
+
8475
+ return new Number($num->getDimension() * 100, '%');
8476
  }
8477
 
8478
  protected static $libRound = ['number'];
8479
  protected function libRound($args)
8480
  {
8481
+ $num = $this->assertNumber($args[0], 'number');
8482
 
8483
+ return new Number(round($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
8484
  }
8485
 
8486
  protected static $libFloor = ['number'];
8487
  protected function libFloor($args)
8488
  {
8489
+ $num = $this->assertNumber($args[0], 'number');
8490
 
8491
+ return new Number(floor($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
8492
  }
8493
 
8494
  protected static $libCeil = ['number'];
8495
  protected function libCeil($args)
8496
  {
8497
+ $num = $this->assertNumber($args[0], 'number');
8498
 
8499
+ return new Number(ceil($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
8500
  }
8501
 
8502
  protected static $libAbs = ['number'];
8503
  protected function libAbs($args)
8504
  {
8505
+ $num = $this->assertNumber($args[0], 'number');
8506
 
8507
+ return new Number(abs($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
8508
  }
8509
 
8510
+ protected static $libMin = ['numbers...'];
8511
  protected function libMin($args)
8512
  {
8513
+ /**
8514
+ * @var Number|null
8515
+ */
8516
+ $min = null;
8517
 
8518
+ foreach ($args[0][2] as $arg) {
8519
+ $number = $this->assertNumber($arg);
8520
 
8521
+ if (\is_null($min) || $min->greaterThan($number)) {
8522
+ $min = $number;
 
 
 
 
 
 
8523
  }
8524
  }
8525
 
8526
+ if (!\is_null($min)) {
8527
+ return $min;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8528
  }
8529
 
8530
+ throw $this->error('At least one argument must be passed.');
8531
  }
8532
 
8533
+ protected static $libMax = ['numbers...'];
8534
+ protected function libMax($args)
 
 
 
 
 
 
8535
  {
8536
+ /**
8537
+ * @var Number|null
8538
+ */
8539
+ $max = null;
 
 
8540
 
8541
+ foreach ($args[0][2] as $arg) {
8542
+ $number = $this->assertNumber($arg);
8543
 
8544
+ if (\is_null($max) || $max->lessThan($number)) {
8545
+ $max = $number;
 
 
 
8546
  }
8547
+ }
8548
 
8549
+ if (!\is_null($max)) {
8550
+ return $max;
8551
  }
8552
 
8553
+ throw $this->error('At least one argument must be passed.');
8554
  }
8555
 
8556
  protected static $libLength = ['list'];
8558
  {
8559
  $list = $this->coerceList($args[0], ',', true);
8560
 
8561
+ return new Number(\count($list[2]), '');
8562
  }
8563
 
8564
+ protected static $libListSeparator = ['list'];
8565
  protected function libListSeparator($args)
8566
  {
 
 
 
 
8567
  if (! \in_array($args[0][0], [Type::T_LIST, Type::T_MAP])) {
8568
+ return [Type::T_KEYWORD, 'space'];
8569
  }
8570
 
8571
  $list = $this->coerceList($args[0]);
8572
 
8573
  if (\count($list[2]) <= 1 && empty($list['enclosing'])) {
8574
+ return [Type::T_KEYWORD, 'space'];
8575
  }
8576
 
8577
  if ($list[1] === ',') {
8578
+ return [Type::T_KEYWORD, 'comma'];
8579
  }
8580
 
8581
+ return [Type::T_KEYWORD, 'space'];
8582
  }
8583
 
8584
  protected static $libNth = ['list', 'n'];
8585
  protected function libNth($args)
8586
  {
8587
  $list = $this->coerceList($args[0], ',', false);
8588
+ $n = $this->assertNumber($args[1])->getDimension();
8589
 
8590
  if ($n > 0) {
8591
  $n--;
8600
  protected function libSetNth($args)
8601
  {
8602
  $list = $this->coerceList($args[0]);
8603
+ $n = $this->assertNumber($args[1])->getDimension();
8604
 
8605
  if ($n > 0) {
8606
  $n--;
8620
  protected static $libMapGet = ['map', 'key'];
8621
  protected function libMapGet($args)
8622
  {
8623
+ $map = $this->assertMap($args[0], 'map');
8624
  $key = $args[1];
8625
 
8626
  if (! \is_null($key)) {
8639
  protected static $libMapKeys = ['map'];
8640
  protected function libMapKeys($args)
8641
  {
8642
+ $map = $this->assertMap($args[0], 'map');
8643
  $keys = $map[1];
8644
 
8645
  return [Type::T_LIST, ',', $keys];
8648
  protected static $libMapValues = ['map'];
8649
  protected function libMapValues($args)
8650
  {
8651
+ $map = $this->assertMap($args[0], 'map');
8652
  $values = $map[2];
8653
 
8654
  return [Type::T_LIST, ',', $values];
8655
  }
8656
 
8657
+ protected static $libMapRemove = [
8658
+ ['map'],
8659
+ ['map', 'key', 'keys...'],
8660
+ ];
8661
  protected function libMapRemove($args)
8662
  {
8663
+ $map = $this->assertMap($args[0], 'map');
8664
+
8665
+ if (\count($args) === 1) {
8666
+ return $map;
8667
+ }
8668
 
8669
  $keys = [];
8670
+ $keys[] = $this->compileStringContent($this->coerceString($args[1]));
8671
 
8672
+ foreach ($args[2][2] as $key) {
8673
  $keys[] = $this->compileStringContent($this->coerceString($key));
8674
  }
8675
 
8686
  protected static $libMapHasKey = ['map', 'key'];
8687
  protected function libMapHasKey($args)
8688
  {
8689
+ $map = $this->assertMap($args[0], 'map');
8690
+
8691
+ return $this->toBool($this->mapHasKey($map, $args[1]));
8692
+ }
8693
+
8694
+ /**
8695
+ * @param array|Number $keyValue
8696
+ *
8697
+ * @return bool
8698
+ */
8699
+ private function mapHasKey(array $map, $keyValue)
8700
+ {
8701
+ $key = $this->compileStringContent($this->coerceString($keyValue));
8702
 
8703
  for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
8704
  if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
8715
  ];
8716
  protected function libMapMerge($args)
8717
  {
8718
+ $map1 = $this->assertMap($args[0], 'map1');
8719
+ $map2 = $this->assertMap($args[1], 'map2');
8720
 
8721
  foreach ($map2[1] as $i2 => $key2) {
8722
  $key = $this->compileStringContent($this->coerceString($key2));
8738
  protected static $libKeywords = ['args'];
8739
  protected function libKeywords($args)
8740
  {
8741
+ $value = $args[0];
8742
+
8743
+ if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) {
8744
+ $compiledValue = $this->compileValue($value);
8745
+
8746
+ throw SassScriptException::forArgument($compiledValue . ' is not an argument list.', 'args');
8747
+ }
8748
 
8749
  $keys = [];
8750
  $values = [];
8751
 
8752
+ foreach ($this->getArgumentListKeywords($value) as $name => $arg) {
8753
  $keys[] = [Type::T_KEYWORD, $name];
8754
  $values[] = $arg;
8755
  }
8764
  $this->coerceList($list, ' ');
8765
 
8766
  if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') {
8767
+ return self::$true;
8768
  }
8769
 
8770
+ return self::$false;
8771
  }
8772
 
8773
+ /**
8774
+ * @param array $list1
8775
+ * @param array|Number|null $sep
8776
+ *
8777
+ * @return string
8778
+ * @throws CompilerException
8779
+ */
8780
  protected function listSeparatorForJoin($list1, $sep)
8781
  {
8782
  if (! isset($sep)) {
8858
  return $res;
8859
  }
8860
 
8861
+ protected static $libZip = ['lists...'];
8862
  protected function libZip($args)
8863
  {
8864
+ $argLists = [];
8865
+ foreach ($args[0][2] as $arg) {
8866
+ $argLists[] = $this->coerceList($arg);
8867
  }
8868
 
8869
  $lists = [];
8870
+ $firstList = array_shift($argLists);
8871
 
8872
  $result = [Type::T_LIST, ',', $lists];
8873
  if (! \is_null($firstList)) {
8874
  foreach ($firstList[2] as $key => $item) {
8875
  $list = [Type::T_LIST, '', [$item]];
8876
 
8877
+ foreach ($argLists as $arg) {
8878
  if (isset($arg[2][$key])) {
8879
  $list[2][] = $arg[2][$key];
8880
  } else {
8898
  {
8899
  $value = $args[0];
8900
 
8901
+ return [Type::T_KEYWORD, $this->getTypeOf($value)];
8902
+ }
8903
+
8904
+ /**
8905
+ * @param array|Number $value
8906
+ *
8907
+ * @return string
8908
+ */
8909
+ private function getTypeOf($value)
8910
+ {
8911
  switch ($value[0]) {
8912
  case Type::T_KEYWORD:
8913
  if ($value === static::$true || $value === static::$false) {
8926
  return 'function';
8927
 
8928
  case Type::T_LIST:
8929
+ if (isset($value[3]) && \is_array($value[3])) {
8930
  return 'arglist';
8931
  }
8932
 
8939
  protected static $libUnit = ['number'];
8940
  protected function libUnit($args)
8941
  {
8942
+ $num = $this->assertNumber($args[0], 'number');
 
 
 
 
8943
 
8944
+ return [Type::T_STRING, '"', [$num->unitStr()]];
8945
  }
8946
 
8947
  protected static $libUnitless = ['number'];
8948
  protected function libUnitless($args)
8949
  {
8950
+ $value = $this->assertNumber($args[0], 'number');
8951
 
8952
+ return $this->toBool($value->unitless());
8953
  }
8954
 
8955
  protected static $libComparable = [
8961
  list($number1, $number2) = $args;
8962
 
8963
  if (
8964
+ ! $number1 instanceof Number ||
8965
+ ! $number2 instanceof Number
8966
  ) {
8967
  throw $this->error('Invalid argument(s) for "comparable"');
8968
  }
8969
 
8970
+ return $this->toBool($number1->isComparableTo($number2));
 
 
 
8971
  }
8972
 
8973
  protected static $libStrIndex = ['string', 'substring'];
8982
  if (! \strlen($substringContent)) {
8983
  $result = 0;
8984
  } else {
8985
+ $result = Util::mbStrpos($stringContent, $substringContent);
8986
  }
8987
 
8988
+ return $result === false ? static::$null : new Number($result + 1, '');
8989
  }
8990
 
8991
  protected static $libStrInsert = ['string', 'insert', 'index'];
9020
  $string = $this->assertString($args[0], 'string');
9021
  $stringContent = $this->compileStringContent($string);
9022
 
9023
+ return new Number(Util::mbStrlen($stringContent), '');
9024
  }
9025
 
9026
  protected static $libStrSlice = ['string', 'start-at', 'end-at:-1'];
9027
  protected function libStrSlice($args)
9028
  {
9029
+ $string = $this->assertString($args[0], 'string');
9030
+ $stringContent = $this->compileStringContent($string);
9031
+
9032
+ $start = $this->assertNumber($args[1], 'start-at');
9033
+ $start->assertNoUnits('start-at');
9034
+ $startInt = $this->assertInteger($start, 'start-at');
9035
+ $end = $this->assertNumber($args[2], 'end-at');
9036
+ $end->assertNoUnits('end-at');
9037
+ $endInt = $this->assertInteger($end, 'end-at');
9038
+
9039
+ if ($endInt === 0) {
9040
+ return [Type::T_STRING, $string[1], []];
9041
  }
9042
 
9043
+ if ($startInt > 0) {
9044
+ $startInt--;
9045
+ }
9046
 
9047
+ if ($endInt < 0) {
9048
+ $endInt = Util::mbStrlen($stringContent) + $endInt;
9049
+ } else {
9050
+ $endInt--;
9051
+ }
9052
 
9053
+ if ($endInt < $startInt) {
9054
+ return [Type::T_STRING, $string[1], []];
9055
  }
9056
 
9057
+ $length = $endInt - $startInt + 1; // The end of the slice is inclusive
 
9058
 
9059
+ $string[2] = [Util::mbSubstr($stringContent, $startInt, $length)];
 
 
9060
 
9061
  return $string;
9062
  }
9064
  protected static $libToLowerCase = ['string'];
9065
  protected function libToLowerCase($args)
9066
  {
9067
+ $string = $this->assertString($args[0], 'string');
9068
  $stringContent = $this->compileStringContent($string);
9069
 
9070
+ $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtolower')];
9071
 
9072
  return $string;
9073
  }
9075
  protected static $libToUpperCase = ['string'];
9076
  protected function libToUpperCase($args)
9077
  {
9078
+ $string = $this->assertString($args[0], 'string');
9079
  $stringContent = $this->compileStringContent($string);
9080
 
9081
+ $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtoupper')];
9082
 
9083
  return $string;
9084
  }
9085
 
9086
+ /**
9087
+ * Apply a filter on a string content, only on ascii chars
9088
+ * let extended chars untouched
9089
+ *
9090
+ * @param string $stringContent
9091
+ * @param callable $filter
9092
+ * @return string
9093
+ */
9094
+ protected function stringTransformAsciiOnly($stringContent, $filter)
9095
+ {
9096
+ $mblength = Util::mbStrlen($stringContent);
9097
+ if ($mblength === strlen($stringContent)) {
9098
+ return $filter($stringContent);
9099
+ }
9100
+ $filteredString = "";
9101
+ for ($i = 0; $i < $mblength; $i++) {
9102
+ $char = Util::mbSubstr($stringContent, $i, 1);
9103
+ if (strlen($char) > 1) {
9104
+ $filteredString .= $char;
9105
+ } else {
9106
+ $filteredString .= $filter($char);
9107
+ }
9108
+ }
9109
+
9110
+ return $filteredString;
9111
+ }
9112
+
9113
  protected static $libFeatureExists = ['feature'];
9114
  protected function libFeatureExists($args)
9115
  {
9116
+ $string = $this->assertString($args[0], 'feature');
9117
  $name = $this->compileStringContent($string);
9118
 
9119
  return $this->toBool(
9124
  protected static $libFunctionExists = ['name'];
9125
  protected function libFunctionExists($args)
9126
  {
9127
+ $string = $this->assertString($args[0], 'name');
9128
  $name = $this->compileStringContent($string);
9129
 
9130
  // user defined functions
9131
  if ($this->has(static::$namespaces['function'] . $name)) {
9132
+ return self::$true;
9133
  }
9134
 
9135
  $name = $this->normalizeName($name);
9136
 
9137
  if (isset($this->userFunctions[$name])) {
9138
+ return self::$true;
9139
  }
9140
 
9141
  // built-in functions
9147
  protected static $libGlobalVariableExists = ['name'];
9148
  protected function libGlobalVariableExists($args)
9149
  {
9150
+ $string = $this->assertString($args[0], 'name');
9151
  $name = $this->compileStringContent($string);
9152
 
9153
+ return $this->toBool($this->has($name, $this->rootEnv));
9154
  }
9155
 
9156
  protected static $libMixinExists = ['name'];
9157
  protected function libMixinExists($args)
9158
  {
9159
+ $string = $this->assertString($args[0], 'name');
9160
  $name = $this->compileStringContent($string);
9161
 
9162
+ return $this->toBool($this->has(static::$namespaces['mixin'] . $name));
9163
  }
9164
 
9165
  protected static $libVariableExists = ['name'];
9166
  protected function libVariableExists($args)
9167
  {
9168
+ $string = $this->assertString($args[0], 'name');
9169
  $name = $this->compileStringContent($string);
9170
 
9171
+ return $this->toBool($this->has($name));
9172
  }
9173
 
9174
+ protected static $libCounter = ['args...'];
9175
  /**
9176
  * Workaround IE7's content counter bug.
9177
  *
9181
  */
9182
  protected function libCounter($args)
9183
  {
9184
+ $list = array_map([$this, 'compileValue'], $args[0][2]);
9185
 
9186
  return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']];
9187
  }
9189
  protected static $libRandom = ['limit:null'];
9190
  protected function libRandom($args)
9191
  {
9192
+ if (isset($args[0]) && $args[0] !== static::$null) {
9193
+ $n = $this->assertInteger($args[0], 'limit');
9194
 
9195
  if ($n < 1) {
9196
+ throw new SassScriptException("\$limit: Must be greater than 0, was $n.");
 
 
 
 
9197
  }
9198
 
9199
+ return new Number(mt_rand(1, $n), '');
9200
  }
9201
 
9202
  $max = mt_getrandmax();
9203
+ return new Number(mt_rand(0, $max - 1) / $max, '');
9204
  }
9205
 
9206
+ protected static $libUniqueId = [];
9207
  protected function libUniqueId()
9208
  {
9209
  static $id;
9219
  return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
9220
  }
9221
 
9222
+ /**
9223
+ * @param array|Number $value
9224
+ * @param bool $force_enclosing_display
9225
+ *
9226
+ * @return array
9227
+ */
9228
  protected function inspectFormatValue($value, $force_enclosing_display = false)
9229
  {
9230
  if ($value === static::$null) {
9233
 
9234
  $stringValue = [$value];
9235
 
9236
+ if ($value instanceof Number) {
9237
+ return [Type::T_STRING, '', $stringValue];
9238
+ }
9239
+
9240
  if ($value[0] === Type::T_LIST) {
9241
  if (end($value[2]) === static::$null) {
9242
  array_pop($value[2]);
9275
  /**
9276
  * Preprocess selector args
9277
  *
9278
+ * @param array $arg
9279
+ * @param string|null $varname
9280
+ * @param bool $allowParent
9281
  *
9282
+ * @return array
9283
  */
9284
  protected function getSelectorArg($arg, $varname = null, $allowParent = false)
9285
  {
9290
  }
9291
 
9292
  if (! $this->checkSelectorArgType($arg)) {
 
9293
  $var_value = $this->compileValue($arg);
9294
+ throw SassScriptException::forArgument("$var_value is not a valid selector: it must be a string, a list of strings, or a list of lists of strings", $varname);
 
9295
  }
9296
 
9297
+
9298
+ if ($arg[0] === Type::T_STRING) {
9299
+ $arg[1] = '';
9300
+ }
9301
  $arg = $this->compileValue($arg);
9302
 
9303
  $parsedSelector = [];
9304
 
9305
+ if ($parser->parseSelector($arg, $parsedSelector, true)) {
9306
  $selector = $this->evalSelectors($parsedSelector);
9307
  $gluedSelector = $this->glueFunctionSelectors($selector);
9308
 
9310
  foreach ($gluedSelector as $selector) {
9311
  foreach ($selector as $s) {
9312
  if (in_array(static::$selfSelector, $s)) {
9313
+ throw SassScriptException::forArgument("Parent selectors aren't allowed here.", $varname);
 
9314
  }
9315
  }
9316
  }
9319
  return $gluedSelector;
9320
  }
9321
 
9322
+ throw SassScriptException::forArgument("expected more input, invalid selector.", $varname);
 
9323
  }
9324
 
9325
  /**
9349
  *
9350
  * @param array $selectors
9351
  *
9352
+ * @return array
9353
  */
9354
  protected function formatOutputSelector($selectors)
9355
  {
9356
+ $selectors = $this->collapseSelectorsAsList($selectors);
9357
 
9358
  return $selectors;
9359
  }
9366
  $super = $this->getSelectorArg($super, 'super');
9367
  $sub = $this->getSelectorArg($sub, 'sub');
9368
 
9369
+ return $this->toBool($this->isSuperSelector($super, $sub));
9370
  }
9371
 
9372
  /**
9609
  $this->extendsMap = [];
9610
 
9611
  foreach ($extendee as $es) {
9612
+ if (\count($es) !== 1) {
9613
+ throw $this->error('Can\'t extend complex selector.');
9614
+ }
9615
+
9616
  // only use the first one
9617
  $this->pushExtends(reset($es), $extender, null);
9618
  }
9713
  * @param array $compound1
9714
  * @param array $compound2
9715
  *
9716
+ * @return array
9717
  */
9718
  protected function unifyCompoundSelectors($compound1, $compound2)
9719
  {
9829
  * @param array $part
9830
  * @param array $compound
9831
  *
9832
+ * @return array|false
9833
  */
9834
  protected function matchPartInCompound($part, $compound)
9835
  {
9924
  * @param string $tag1
9925
  * @param string $tag2
9926
  *
9927
+ * @return array|false
9928
  */
9929
  protected function checkCompatibleTags($tag1, $tag2)
9930
  {
9947
  /**
9948
  * Find the html tag name in a selector parts list
9949
  *
9950
+ * @param string[] $parts
9951
  *
9952
+ * @return string
9953
  */
9954
  protected function findTagName($parts)
9955
  {
9986
  protected static $libScssphpGlob = ['pattern'];
9987
  protected function libScssphpGlob($args)
9988
  {
9989
+ @trigger_error(sprintf('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0. Register your own alternative through "%s::registerFunction', __CLASS__), E_USER_DEPRECATED);
9990
+
9991
+ $this->logger->warn('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0.', true);
9992
+
9993
+ $string = $this->assertString($args[0], 'pattern');
9994
  $pattern = $this->compileStringContent($string);
9995
  $matches = glob($pattern);
9996
  $listParts = [];
scssphp/src/Compiler/CachedResult.php ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * SCSSPHP
5
+ *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
+ *
8
+ * @license http://opensource.org/licenses/MIT MIT
9
+ *
10
+ * @link http://scssphp.github.io/scssphp
11
+ */
12
+
13
+ namespace ScssPhp\ScssPhp\Compiler;
14
+
15
+ use ScssPhp\ScssPhp\CompilationResult;
16
+
17
+ /**
18
+ * @internal
19
+ */
20
+ class CachedResult
21
+ {
22
+ /**
23
+ * @var CompilationResult
24
+ */
25
+ private $result;
26
+
27
+ /**
28
+ * @var array<string, int>
29
+ */
30
+ private $parsedFiles;
31
+
32
+ /**
33
+ * @var array
34
+ * @phpstan-var list<array{currentDir: string|null, path: string, filePath: string}>
35
+ */
36
+ private $resolvedImports;
37
+
38
+ /**
39
+ * @param CompilationResult $result
40
+ * @param array<string, int> $parsedFiles
41
+ * @param array $resolvedImports
42
+ *
43
+ * @phpstan-param list<array{currentDir: string|null, path: string, filePath: string}> $resolvedImports
44
+ */
45
+ public function __construct(CompilationResult $result, array $parsedFiles, array $resolvedImports)
46
+ {
47
+ $this->result = $result;
48
+ $this->parsedFiles = $parsedFiles;
49
+ $this->resolvedImports = $resolvedImports;
50
+ }
51
+
52
+ /**
53
+ * @return CompilationResult
54
+ */
55
+ public function getResult()
56
+ {
57
+ return $this->result;
58
+ }
59
+
60
+ /**
61
+ * @return array<string, int>
62
+ */
63
+ public function getParsedFiles()
64
+ {
65
+ return $this->parsedFiles;
66
+ }
67
+
68
+ /**
69
+ * @return array
70
+ *
71
+ * @phpstan-return list<array{currentDir: string|null, path: string, filePath: string}>
72
+ */
73
+ public function getResolvedImports()
74
+ {
75
+ return $this->resolvedImports;
76
+ }
77
+ }
scssphp/src/Compiler/Environment.php CHANGED
@@ -16,16 +16,18 @@ namespace ScssPhp\ScssPhp\Compiler;
16
  * Compiler environment
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
 
 
19
  */
20
  class Environment
21
  {
22
  /**
23
- * @var \ScssPhp\ScssPhp\Block
24
  */
25
  public $block;
26
 
27
  /**
28
- * @var \ScssPhp\ScssPhp\Compiler\Environment
29
  */
30
  public $parent;
31
 
16
  * Compiler environment
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
19
+ *
20
+ * @internal
21
  */
22
  class Environment
23
  {
24
  /**
25
+ * @var \ScssPhp\ScssPhp\Block|null
26
  */
27
  public $block;
28
 
29
  /**
30
+ * @var \ScssPhp\ScssPhp\Compiler\Environment|null
31
  */
32
  public $parent;
33
 
scssphp/src/Exception/CompilerException.php CHANGED
@@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp\Exception;
16
  * Compiler exception
17
  *
18
  * @author Oleksandr Savchenko <traveltino@gmail.com>
 
 
19
  */
20
  class CompilerException extends \Exception implements SassException
21
  {
16
  * Compiler exception
17
  *
18
  * @author Oleksandr Savchenko <traveltino@gmail.com>
19
+ *
20
+ * @internal
21
  */
22
  class CompilerException extends \Exception implements SassException
23
  {
scssphp/src/Exception/ParserException.php CHANGED
@@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp\Exception;
16
  * Parser Exception
17
  *
18
  * @author Oleksandr Savchenko <traveltino@gmail.com>
 
 
19
  */
20
  class ParserException extends \Exception implements SassException
21
  {
16
  * Parser Exception
17
  *
18
  * @author Oleksandr Savchenko <traveltino@gmail.com>
19
+ *
20
+ * @internal
21
  */
22
  class ParserException extends \Exception implements SassException
23
  {
scssphp/src/Exception/RangeException.php CHANGED
@@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp\Exception;
16
  * Range exception
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
 
 
19
  */
20
  class RangeException extends \Exception implements SassException
21
  {
16
  * Range exception
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
19
+ *
20
+ * @internal
21
  */
22
  class RangeException extends \Exception implements SassException
23
  {
scssphp/src/Exception/SassScriptException.php ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace ScssPhp\ScssPhp\Exception;
4
+
5
+ /**
6
+ * An exception thrown by SassScript.
7
+ *
8
+ * This class does not implement SassException on purpose, as it should
9
+ * never be returned to the outside code. The compilation will catch it
10
+ * and replace it with a SassException reporting the location of the
11
+ * error.
12
+ */
13
+ class SassScriptException extends \Exception
14
+ {
15
+ /**
16
+ * Creates a SassScriptException with support for an argument name.
17
+ *
18
+ * This helper ensures a consistent handling of argument names in the
19
+ * error message, without duplicating it.
20
+ *
21
+ * @param string $message
22
+ * @param string|null $name The argument name, without $
23
+ *
24
+ * @return SassScriptException
25
+ */
26
+ public static function forArgument($message, $name = null)
27
+ {
28
+ $varDisplay = !\is_null($name) ? "\${$name}: " : '';
29
+
30
+ return new self($varDisplay . $message);
31
+ }
32
+ }
scssphp/src/Exception/ServerException.php CHANGED
@@ -12,10 +12,14 @@
12
 
13
  namespace ScssPhp\ScssPhp\Exception;
14
 
 
 
15
  /**
16
  * Server Exception
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
 
 
19
  */
20
  class ServerException extends \Exception implements SassException
21
  {
12
 
13
  namespace ScssPhp\ScssPhp\Exception;
14
 
15
+ @trigger_error(sprintf('The "%s" class is deprecated.', ServerException::class), E_USER_DEPRECATED);
16
+
17
  /**
18
  * Server Exception
19
  *
20
  * @author Anthon Pang <anthon.pang@gmail.com>
21
+ *
22
+ * @deprecated The Scssphp server should define its own exception instead.
23
  */
24
  class ServerException extends \Exception implements SassException
25
  {
scssphp/src/Formatter.php CHANGED
@@ -19,6 +19,8 @@ use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
19
  * Base formatter
20
  *
21
  * @author Leaf Corcoran <leafot@gmail.com>
 
 
22
  */
23
  abstract class Formatter
24
  {
@@ -78,7 +80,7 @@ abstract class Formatter
78
  protected $currentColumn;
79
 
80
  /**
81
- * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator
82
  */
83
  protected $sourceMapGenerator;
84
 
@@ -139,6 +141,8 @@ abstract class Formatter
139
  * Output lines inside a block
140
  *
141
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
 
 
142
  */
143
  protected function blockLines(OutputBlock $block)
144
  {
@@ -156,9 +160,13 @@ abstract class Formatter
156
  * Output block selectors
157
  *
158
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
 
 
159
  */
160
  protected function blockSelectors(OutputBlock $block)
161
  {
 
 
162
  $inner = $this->indentStr();
163
 
164
  $this->write($inner
@@ -170,6 +178,8 @@ abstract class Formatter
170
  * Output block children
171
  *
172
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
 
 
173
  */
174
  protected function blockChildren(OutputBlock $block)
175
  {
@@ -182,6 +192,8 @@ abstract class Formatter
182
  * Output non-empty block
183
  *
184
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
 
 
185
  */
186
  protected function block(OutputBlock $block)
187
  {
@@ -285,6 +297,8 @@ abstract class Formatter
285
  * Output content
286
  *
287
  * @param string $str
 
 
288
  */
289
  protected function write($str)
290
  {
@@ -310,22 +324,39 @@ abstract class Formatter
310
  }
311
 
312
  if ($this->sourceMapGenerator) {
313
- $this->sourceMapGenerator->addMapping(
314
- $this->currentLine,
315
- $this->currentColumn,
316
- $this->currentBlock->sourceLine,
317
- //columns from parser are off by one
318
- $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0,
319
- $this->currentBlock->sourceName
320
- );
321
-
322
  $lines = explode("\n", $str);
323
- $lineCount = \count($lines);
324
- $this->currentLine += $lineCount - 1;
325
-
326
  $lastLine = array_pop($lines);
327
 
328
- $this->currentColumn = ($lineCount === 1 ? $this->currentColumn : 0) + \strlen($lastLine);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  }
330
 
331
  echo $str;
19
  * Base formatter
20
  *
21
  * @author Leaf Corcoran <leafot@gmail.com>
22
+ *
23
+ * @internal
24
  */
25
  abstract class Formatter
26
  {
80
  protected $currentColumn;
81
 
82
  /**
83
+ * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null
84
  */
85
  protected $sourceMapGenerator;
86
 
141
  * Output lines inside a block
142
  *
143
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
144
+ *
145
+ * @return void
146
  */
147
  protected function blockLines(OutputBlock $block)
148
  {
160
  * Output block selectors
161
  *
162
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
163
+ *
164
+ * @return void
165
  */
166
  protected function blockSelectors(OutputBlock $block)
167
  {
168
+ assert(! empty($block->selectors));
169
+
170
  $inner = $this->indentStr();
171
 
172
  $this->write($inner
178
  * Output block children
179
  *
180
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
181
+ *
182
+ * @return void
183
  */
184
  protected function blockChildren(OutputBlock $block)
185
  {
192
  * Output non-empty block
193
  *
194
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
195
+ *
196
+ * @return void
197
  */
198
  protected function block(OutputBlock $block)
199
  {
297
  * Output content
298
  *
299
  * @param string $str
300
+ *
301
+ * @return void
302
  */
303
  protected function write($str)
304
  {
324
  }
325
 
326
  if ($this->sourceMapGenerator) {
 
 
 
 
 
 
 
 
 
327
  $lines = explode("\n", $str);
 
 
 
328
  $lastLine = array_pop($lines);
329
 
330
+ foreach ($lines as $line) {
331
+ // If the written line starts is empty, adding a mapping would add it for
332
+ // a non-existent column as we are at the end of the line
333
+ if ($line !== '') {
334
+ $this->sourceMapGenerator->addMapping(
335
+ $this->currentLine,
336
+ $this->currentColumn,
337
+ $this->currentBlock->sourceLine,
338
+ //columns from parser are off by one
339
+ $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0,
340
+ $this->currentBlock->sourceName
341
+ );
342
+ }
343
+
344
+ $this->currentLine++;
345
+ $this->currentColumn = 0;
346
+ }
347
+
348
+ if ($lastLine !== '') {
349
+ $this->sourceMapGenerator->addMapping(
350
+ $this->currentLine,
351
+ $this->currentColumn,
352
+ $this->currentBlock->sourceLine,
353
+ //columns from parser are off by one
354
+ $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0,
355
+ $this->currentBlock->sourceName
356
+ );
357
+ }
358
+
359
+ $this->currentColumn += \strlen($lastLine);
360
  }
361
 
362
  echo $str;
scssphp/src/Formatter/Compact.php CHANGED
@@ -18,6 +18,10 @@ use ScssPhp\ScssPhp\Formatter;
18
  * Compact formatter
19
  *
20
  * @author Leaf Corcoran <leafot@gmail.com>
 
 
 
 
21
  */
22
  class Compact extends Formatter
23
  {
@@ -26,6 +30,8 @@ class Compact extends Formatter
26
  */
27
  public function __construct()
28
  {
 
 
29
  $this->indentLevel = 0;
30
  $this->indentChar = '';
31
  $this->break = '';
18
  * Compact formatter
19
  *
20
  * @author Leaf Corcoran <leafot@gmail.com>
21
+ *
22
+ * @deprecated since 1.4.0. Use the Compressed formatter instead.
23
+ *
24
+ * @internal
25
  */
26
  class Compact extends Formatter
27
  {
30
  */
31
  public function __construct()
32
  {
33
+ @trigger_error('The Compact formatter is deprecated since 1.4.0. Use the Compressed formatter instead.', E_USER_DEPRECATED);
34
+
35
  $this->indentLevel = 0;
36
  $this->indentChar = '';
37
  $this->break = '';
scssphp/src/Formatter/Compressed.php CHANGED
@@ -13,12 +13,13 @@
13
  namespace ScssPhp\ScssPhp\Formatter;
14
 
15
  use ScssPhp\ScssPhp\Formatter;
16
- use ScssPhp\ScssPhp\Formatter\OutputBlock;
17
 
18
  /**
19
  * Compressed formatter
20
  *
21
  * @author Leaf Corcoran <leafot@gmail.com>
 
 
22
  */
23
  class Compressed extends Formatter
24
  {
@@ -68,6 +69,8 @@ class Compressed extends Formatter
68
  */
69
  protected function blockSelectors(OutputBlock $block)
70
  {
 
 
71
  $inner = $this->indentStr();
72
 
73
  $this->write(
13
  namespace ScssPhp\ScssPhp\Formatter;
14
 
15
  use ScssPhp\ScssPhp\Formatter;
 
16
 
17
  /**
18
  * Compressed formatter
19
  *
20
  * @author Leaf Corcoran <leafot@gmail.com>
21
+ *
22
+ * @internal
23
  */
24
  class Compressed extends Formatter
25
  {
69
  */
70
  protected function blockSelectors(OutputBlock $block)
71
  {
72
+ assert(! empty($block->selectors));
73
+
74
  $inner = $this->indentStr();
75
 
76
  $this->write(
scssphp/src/Formatter/Crunched.php CHANGED
@@ -13,12 +13,15 @@
13
  namespace ScssPhp\ScssPhp\Formatter;
14
 
15
  use ScssPhp\ScssPhp\Formatter;
16
- use ScssPhp\ScssPhp\Formatter\OutputBlock;
17
 
18
  /**
19
  * Crunched formatter
20
  *
21
  * @author Anthon Pang <anthon.pang@gmail.com>
 
 
 
 
22
  */
23
  class Crunched extends Formatter
24
  {
@@ -27,6 +30,8 @@ class Crunched extends Formatter
27
  */
28
  public function __construct()
29
  {
 
 
30
  $this->indentLevel = 0;
31
  $this->indentChar = ' ';
32
  $this->break = '';
@@ -66,6 +71,8 @@ class Crunched extends Formatter
66
  */
67
  protected function blockSelectors(OutputBlock $block)
68
  {
 
 
69
  $inner = $this->indentStr();
70
 
71
  $this->write(
13
  namespace ScssPhp\ScssPhp\Formatter;
14
 
15
  use ScssPhp\ScssPhp\Formatter;
 
16
 
17
  /**
18
  * Crunched formatter
19
  *
20
  * @author Anthon Pang <anthon.pang@gmail.com>
21
+ *
22
+ * @deprecated since 1.4.0. Use the Compressed formatter instead.
23
+ *
24
+ * @internal
25
  */
26
  class Crunched extends Formatter
27
  {
30
  */
31
  public function __construct()
32
  {
33
+ @trigger_error('The Crunched formatter is deprecated since 1.4.0. Use the Compressed formatter instead.', E_USER_DEPRECATED);
34
+
35
  $this->indentLevel = 0;
36
  $this->indentChar = ' ';
37
  $this->break = '';
71
  */
72
  protected function blockSelectors(OutputBlock $block)
73
  {
74
+ assert(! empty($block->selectors));
75
+
76
  $inner = $this->indentStr();
77
 
78
  $this->write(
scssphp/src/Formatter/Debug.php CHANGED
@@ -13,12 +13,15 @@
13
  namespace ScssPhp\ScssPhp\Formatter;
14
 
15
  use ScssPhp\ScssPhp\Formatter;
16
- use ScssPhp\ScssPhp\Formatter\OutputBlock;
17
 
18
  /**
19
  * Debug formatter
20
  *
21
  * @author Anthon Pang <anthon.pang@gmail.com>
 
 
 
 
22
  */
23
  class Debug extends Formatter
24
  {
@@ -27,6 +30,8 @@ class Debug extends Formatter
27
  */
28
  public function __construct()
29
  {
 
 
30
  $this->indentLevel = 0;
31
  $this->indentChar = '';
32
  $this->break = "\n";
13
  namespace ScssPhp\ScssPhp\Formatter;
14
 
15
  use ScssPhp\ScssPhp\Formatter;
 
16
 
17
  /**
18
  * Debug formatter
19
  *
20
  * @author Anthon Pang <anthon.pang@gmail.com>
21
+ *
22
+ * @deprecated since 1.4.0.
23
+ *
24
+ * @internal
25
  */
26
  class Debug extends Formatter
27
  {
30
  */
31
  public function __construct()
32
  {
33
+ @trigger_error('The Debug formatter is deprecated since 1.4.0.', E_USER_DEPRECATED);
34
+
35
  $this->indentLevel = 0;
36
  $this->indentChar = '';
37
  $this->break = "\n";
scssphp/src/Formatter/Expanded.php CHANGED
@@ -13,12 +13,13 @@
13
  namespace ScssPhp\ScssPhp\Formatter;
14
 
15
  use ScssPhp\ScssPhp\Formatter;
16
- use ScssPhp\ScssPhp\Formatter\OutputBlock;
17
 
18
  /**
19
  * Expanded formatter
20
  *
21
  * @author Leaf Corcoran <leafot@gmail.com>
 
 
22
  */
23
  class Expanded extends Formatter
24
  {
13
  namespace ScssPhp\ScssPhp\Formatter;
14
 
15
  use ScssPhp\ScssPhp\Formatter;
 
16
 
17
  /**
18
  * Expanded formatter
19
  *
20
  * @author Leaf Corcoran <leafot@gmail.com>
21
+ *
22
+ * @internal
23
  */
24
  class Expanded extends Formatter
25
  {
scssphp/src/Formatter/Nested.php CHANGED
@@ -13,13 +13,16 @@
13
  namespace ScssPhp\ScssPhp\Formatter;
14
 
15
  use ScssPhp\ScssPhp\Formatter;
16
- use ScssPhp\ScssPhp\Formatter\OutputBlock;
17
  use ScssPhp\ScssPhp\Type;
18
 
19
  /**
20
  * Nested formatter
21
  *
22
  * @author Leaf Corcoran <leafot@gmail.com>
 
 
 
 
23
  */
24
  class Nested extends Formatter
25
  {
@@ -33,6 +36,8 @@ class Nested extends Formatter
33
  */
34
  public function __construct()
35
  {
 
 
36
  $this->indentLevel = 0;
37
  $this->indentChar = ' ';
38
  $this->break = "\n";
13
  namespace ScssPhp\ScssPhp\Formatter;
14
 
15
  use ScssPhp\ScssPhp\Formatter;
 
16
  use ScssPhp\ScssPhp\Type;
17
 
18
  /**
19
  * Nested formatter
20
  *
21
  * @author Leaf Corcoran <leafot@gmail.com>
22
+ *
23
+ * @deprecated since 1.4.0. Use the Expanded formatter instead.
24
+ *
25
+ * @internal
26
  */
27
  class Nested extends Formatter
28
  {
36
  */
37
  public function __construct()
38
  {
39
+ @trigger_error('The Nested formatter is deprecated since 1.4.0. Use the Expanded formatter instead.', E_USER_DEPRECATED);
40
+
41
  $this->indentLevel = 0;
42
  $this->indentChar = ' ';
43
  $this->break = "\n";
scssphp/src/Formatter/OutputBlock.php CHANGED
@@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp\Formatter;
16
  * Output block
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
 
 
19
  */
20
  class OutputBlock
21
  {
@@ -30,37 +32,37 @@ class OutputBlock
30
  public $depth;
31
 
32
  /**
33
- * @var array
34
  */
35
  public $selectors;
36
 
37
  /**
38
- * @var array
39
  */
40
  public $lines;
41
 
42
  /**
43
- * @var array
44
  */
45
  public $children;
46
 
47
  /**
48
- * @var \ScssPhp\ScssPhp\Formatter\OutputBlock
49
  */
50
  public $parent;
51
 
52
  /**
53
- * @var string
54
  */
55
  public $sourceName;
56
 
57
  /**
58
- * @var integer
59
  */
60
  public $sourceLine;
61
 
62
  /**
63
- * @var integer
64
  */
65
  public $sourceColumn;
66
  }
16
  * Output block
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
19
+ *
20
+ * @internal
21
  */
22
  class OutputBlock
23
  {
32
  public $depth;
33
 
34
  /**
35
+ * @var array|null
36
  */
37
  public $selectors;
38
 
39
  /**
40
+ * @var string[]
41
  */
42
  public $lines;
43
 
44
  /**
45
+ * @var OutputBlock[]
46
  */
47
  public $children;
48
 
49
  /**
50
+ * @var OutputBlock|null
51
  */
52
  public $parent;
53
 
54
  /**
55
+ * @var string|null
56
  */
57
  public $sourceName;
58
 
59
  /**
60
+ * @var integer|null
61
  */
62
  public $sourceLine;
63
 
64
  /**
65
+ * @var integer|null
66
  */
67
  public $sourceColumn;
68
  }
scssphp/src/Logger/LoggerInterface.php ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * SCSSPHP
5
+ *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
+ *
8
+ * @license http://opensource.org/licenses/MIT MIT
9
+ *
10
+ * @link http://scssphp.github.io/scssphp
11
+ */
12
+
13
+ namespace ScssPhp\ScssPhp\Logger;
14
+
15
+ /**
16
+ * Interface implemented by loggers for warnings and debug messages.
17
+ *
18
+ * The official Sass implementation recommends that loggers report the
19
+ * messages immediately rather than waiting for the end of the
20
+ * compilation, to provide a better debugging experience when the
21
+ * compilation does not end (error or infinite loop after the warning
22
+ * for instance).
23
+ */
24
+ interface LoggerInterface
25
+ {
26
+ /**
27
+ * Emits a warning with the given message.
28
+ *
29
+ * If $deprecation is true, it indicates that this is a deprecation
30
+ * warning. Implementations should surface all this information to
31
+ * the end user.
32
+ *
33
+ * @param string $message
34
+ * @param bool $deprecation
35
+ *
36
+ * @return void
37
+ */
38
+ public function warn($message, $deprecation = false);
39
+
40
+ /**
41
+ * Emits a debugging message.
42
+ *
43
+ * @param string $message
44
+ *
45
+ * @return void
46
+ */
47
+ public function debug($message);
48
+ }
scssphp/src/Logger/QuietLogger.php ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * SCSSPHP
5
+ *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
+ *
8
+ * @license http://opensource.org/licenses/MIT MIT
9
+ *
10
+ * @link http://scssphp.github.io/scssphp
11
+ */
12
+
13
+ namespace ScssPhp\ScssPhp\Logger;
14
+
15
+ /**
16
+ * A logger that silently ignores all messages.
17
+ */
18
+ class QuietLogger implements LoggerInterface
19
+ {
20
+ public function warn($message, $deprecation = false)
21
+ {
22
+ }
23
+
24
+ public function debug($message)
25
+ {
26
+ }
27
+ }
scssphp/src/Logger/StreamLogger.php ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * SCSSPHP
5
+ *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
+ *
8
+ * @license http://opensource.org/licenses/MIT MIT
9
+ *
10
+ * @link http://scssphp.github.io/scssphp
11
+ */
12
+
13
+ namespace ScssPhp\ScssPhp\Logger;
14
+
15
+ /**
16
+ * A logger that prints to a PHP stream (for instance stderr)
17
+ */
18
+ class StreamLogger implements LoggerInterface
19
+ {
20
+ private $stream;
21
+ private $closeOnDestruct;
22
+
23
+ /**
24
+ * @param resource $stream A stream resource
25
+ * @param bool $closeOnDestruct If true, takes ownership of the stream and close it on destruct to avoid leaks.
26
+ */
27
+ public function __construct($stream, $closeOnDestruct = false)
28
+ {
29
+ $this->stream = $stream;
30
+ $this->closeOnDestruct = $closeOnDestruct;
31
+ }
32
+
33
+ /**
34
+ * @internal
35
+ */
36
+ public function __destruct()
37
+ {
38
+ if ($this->closeOnDestruct) {
39
+ fclose($this->stream);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * @inheritDoc
45
+ */
46
+ public function warn($message, $deprecation = false)
47
+ {
48
+ $prefix = ($deprecation ? 'DEPRECATION ' : '') . 'WARNING: ';
49
+
50
+ fwrite($this->stream, $prefix . $message . "\n\n");
51
+ }
52
+
53
+ /**
54
+ * @inheritDoc
55
+ */
56
+ public function debug($message)
57
+ {
58
+ fwrite($this->stream, $message . "\n");
59
+ }
60
+ }
scssphp/src/Node.php CHANGED
@@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp;
16
  * Base node
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
 
 
19
  */
20
  abstract class Node
21
  {
@@ -30,12 +32,12 @@ abstract class Node
30
  public $sourceIndex;
31
 
32
  /**
33
- * @var integer
34
  */
35
  public $sourceLine;
36
 
37
  /**
38
- * @var integer
39
  */
40
  public $sourceColumn;
41
  }
16
  * Base node
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
19
+ *
20
+ * @internal
21
  */
22
  abstract class Node
23
  {
32
  public $sourceIndex;
33
 
34
  /**
35
+ * @var int|null
36
  */
37
  public $sourceLine;
38
 
39
  /**
40
+ * @var int|null
41
  */
42
  public $sourceColumn;
43
  }
scssphp/src/Node/Number.php CHANGED
@@ -12,9 +12,13 @@
12
 
13
  namespace ScssPhp\ScssPhp\Node;
14
 
 
15
  use ScssPhp\ScssPhp\Compiler;
 
 
16
  use ScssPhp\ScssPhp\Node;
17
  use ScssPhp\ScssPhp\Type;
 
18
 
19
  /**
20
  * Dimension + optional units
@@ -26,6 +30,8 @@ use ScssPhp\ScssPhp\Type;
26
  * }}
27
  *
28
  * @author Anthon Pang <anthon.pang@gmail.com>
 
 
29
  */
30
  class Number extends Node implements \ArrayAccess
31
  {
@@ -41,6 +47,7 @@ class Number extends Node implements \ArrayAccess
41
  * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/
42
  *
43
  * @var array
 
44
  */
45
  protected static $unitTable = [
46
  'in' => [
@@ -76,74 +83,67 @@ class Number extends Node implements \ArrayAccess
76
  /**
77
  * @var integer|float
78
  */
79
- public $dimension;
80
 
81
  /**
82
- * @var array
 
83
  */
84
- public $units;
85
 
86
  /**
87
- * Initialize number
88
- *
89
- * @param mixed $dimension
90
- * @param mixed $initialUnit
91
  */
92
- public function __construct($dimension, $initialUnit)
93
- {
94
- $this->type = Type::T_NUMBER;
95
- $this->dimension = $dimension;
96
- $this->units = \is_array($initialUnit)
97
- ? $initialUnit
98
- : ($initialUnit ? [$initialUnit => 1]
99
- : []);
100
- }
101
 
102
  /**
103
- * Coerce number to target units
104
  *
105
- * @param array $units
 
 
106
  *
107
- * @return \ScssPhp\ScssPhp\Node\Number
 
108
  */
109
- public function coerce($units)
110
  {
111
- if ($this->unitless()) {
112
- return new Number($this->dimension, $units);
 
 
 
 
113
  }
114
 
115
- $dimension = $this->dimension;
116
-
117
- if (\count($units)) {
118
- $baseUnit = array_keys($units);
119
- $baseUnit = reset($baseUnit);
120
- $baseUnit = $this->findBaseUnit($baseUnit);
121
- if ($baseUnit && isset(static::$unitTable[$baseUnit])) {
122
- foreach (static::$unitTable[$baseUnit] as $unit => $conv) {
123
- $from = isset($this->units[$unit]) ? $this->units[$unit] : 0;
124
- $to = isset($units[$unit]) ? $units[$unit] : 0;
125
- $factor = pow($conv, $from - $to);
126
- $dimension /= $factor;
127
- }
128
- }
129
- }
130
-
131
- return new Number($dimension, $units);
132
  }
133
 
134
  /**
135
- * Normalize number
136
- *
137
- * @return \ScssPhp\ScssPhp\Node\Number
138
  */
139
- public function normalize()
140
  {
141
- $dimension = $this->dimension;
142
- $units = [];
143
 
144
- $this->normalizeUnits($dimension, $units);
 
 
 
 
 
 
145
 
146
- return new Number($dimension, $units);
 
 
 
 
 
147
  }
148
 
149
  /**
@@ -187,13 +187,13 @@ class Number extends Node implements \ArrayAccess
187
  return $this->sourceIndex;
188
 
189
  case 0:
190
- return $this->type;
191
 
192
  case 1:
193
  return $this->dimension;
194
 
195
  case 2:
196
- return $this->units;
197
  }
198
  }
199
 
@@ -202,17 +202,7 @@ class Number extends Node implements \ArrayAccess
202
  */
203
  public function offsetSet($offset, $value)
204
  {
205
- if ($offset === 1) {
206
- $this->dimension = $value;
207
- } elseif ($offset === 2) {
208
- $this->units = $value;
209
- } elseif ($offset == -1) {
210
- $this->sourceIndex = $value;
211
- } elseif ($offset == -2) {
212
- $this->sourceLine = $value;
213
- } elseif ($offset == -3) {
214
- $this->sourceColumn = $value;
215
- }
216
  }
217
 
218
  /**
@@ -220,17 +210,7 @@ class Number extends Node implements \ArrayAccess
220
  */
221
  public function offsetUnset($offset)
222
  {
223
- if ($offset === 1) {
224
- $this->dimension = null;
225
- } elseif ($offset === 2) {
226
- $this->units = null;
227
- } elseif ($offset === -1) {
228
- $this->sourceIndex = null;
229
- } elseif ($offset === -2) {
230
- $this->sourceLine = null;
231
- } elseif ($offset === -3) {
232
- $this->sourceColumn = null;
233
- }
234
  }
235
 
236
  /**
@@ -240,61 +220,311 @@ class Number extends Node implements \ArrayAccess
240
  */
241
  public function unitless()
242
  {
243
- return ! array_sum($this->units);
244
  }
245
 
246
  /**
247
- * Test if a number can be normalized in a base unit
248
- * ie if its units are homogeneous
249
  *
250
- * @return boolean
 
 
 
 
 
 
 
 
 
 
 
 
251
  */
252
- public function isNormalizable()
253
  {
254
  if ($this->unitless()) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  return false;
256
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
 
258
- $baseUnit = null;
 
 
 
 
 
 
259
 
260
- foreach ($this->units as $unit => $exp) {
261
- $b = $this->findBaseUnit($unit);
 
 
 
 
 
 
 
 
 
 
 
262
 
263
- if (\is_null($baseUnit)) {
264
- $baseUnit = $b;
265
  }
266
 
267
- if (\is_null($b) or $b !== $baseUnit) {
268
- return false;
269
  }
270
- }
271
 
272
- return $baseUnit;
 
273
  }
274
 
275
  /**
276
- * Returns unit(s) as the product of numerator units divided by the product of denominator units
277
  *
278
- * @return string
279
  */
280
- public function unitStr()
281
  {
282
- $numerators = [];
283
- $denominators = [];
284
 
285
- foreach ($this->units as $unit => $unitSize) {
286
- if ($unitSize > 0) {
287
- $numerators = array_pad($numerators, \count($numerators) + $unitSize, $unit);
288
- continue;
 
 
 
 
 
 
 
 
 
 
289
  }
 
 
 
290
 
291
- if ($unitSize < 0) {
292
- $denominators = array_pad($denominators, \count($denominators) - $unitSize, $unit);
293
- continue;
294
- }
 
 
 
 
 
 
 
 
 
295
  }
296
 
297
- return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : '');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  }
299
 
300
  /**
@@ -308,35 +538,29 @@ class Number extends Node implements \ArrayAccess
308
  {
309
  $dimension = round($this->dimension, self::PRECISION);
310
 
311
- $units = array_filter($this->units, function ($unitSize) {
312
- return $unitSize;
313
- });
314
-
315
- if (\count($units) > 1 && array_sum($units) === 0) {
316
- $dimension = $this->dimension;
317
- $units = [];
318
-
319
- $this->normalizeUnits($dimension, $units);
320
 
321
- $dimension = round($dimension, self::PRECISION);
322
- $units = array_filter($units, function ($unitSize) {
323
- return $unitSize;
324
- });
325
  }
326
 
327
- $unitSize = array_sum($units);
 
 
328
 
329
- if ($compiler && ($unitSize > 1 || $unitSize < 0 || \count($units) > 1)) {
330
- $this->units = $units;
331
  $unit = $this->unitStr();
 
 
332
  } else {
333
- reset($units);
334
- $unit = key($units);
335
  }
336
 
337
  $dimension = number_format($dimension, self::PRECISION, '.', '');
338
 
339
- return (self::PRECISION ? rtrim(rtrim($dimension, '0'), '.') : $dimension) . $unit;
340
  }
341
 
342
  /**
@@ -348,48 +572,229 @@ class Number extends Node implements \ArrayAccess
348
  }
349
 
350
  /**
351
- * Normalize units
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  *
353
- * @param integer|float $dimension
354
- * @param array $units
355
- * @param string $baseUnit
 
 
 
356
  */
357
- private function normalizeUnits(&$dimension, &$units, $baseUnit = null)
358
  {
359
- $dimension = $this->dimension;
360
- $units = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
 
362
- foreach ($this->units as $unit => $exp) {
363
- if (! $baseUnit) {
364
- $baseUnit = $this->findBaseUnit($unit);
365
  }
366
 
367
- if ($baseUnit && isset(static::$unitTable[$baseUnit][$unit])) {
368
- $factor = pow(static::$unitTable[$baseUnit][$unit], $exp);
 
 
 
 
 
 
 
 
 
 
369
 
370
- $unit = $baseUnit;
371
- $dimension /= $factor;
 
 
 
 
 
372
  }
373
 
374
- $units[$unit] = $exp + (isset($units[$unit]) ? $units[$unit] : 0);
 
 
 
 
 
 
 
 
 
 
 
 
375
  }
 
 
376
  }
377
 
378
  /**
379
- * Find the base unit family for a given unit
 
 
 
 
380
  *
381
- * @param string $unit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  *
383
- * @return string|null
 
 
 
384
  */
385
- private function findBaseUnit($unit)
386
  {
387
- foreach (static::$unitTable as $baseUnit => $unitVariants) {
388
- if (isset($unitVariants[$unit])) {
389
- return $baseUnit;
 
 
 
 
390
  }
391
  }
392
 
393
  return null;
394
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  }
12
 
13
  namespace ScssPhp\ScssPhp\Node;
14
 
15
+ use ScssPhp\ScssPhp\Base\Range;
16
  use ScssPhp\ScssPhp\Compiler;
17
+ use ScssPhp\ScssPhp\Exception\RangeException;
18
+ use ScssPhp\ScssPhp\Exception\SassScriptException;
19
  use ScssPhp\ScssPhp\Node;
20
  use ScssPhp\ScssPhp\Type;
21
+ use ScssPhp\ScssPhp\Util;
22
 
23
  /**
24
  * Dimension + optional units
30
  * }}
31
  *
32
  * @author Anthon Pang <anthon.pang@gmail.com>
33
+ *
34
+ * @template-implements \ArrayAccess<int, mixed>
35
  */
36
  class Number extends Node implements \ArrayAccess
37
  {
47
  * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/
48
  *
49
  * @var array
50
+ * @phpstan-var array<string, array<string, float|int>>
51
  */
52
  protected static $unitTable = [
53
  'in' => [
83
  /**
84
  * @var integer|float
85
  */
86
+ private $dimension;
87
 
88
  /**
89
+ * @var string[]
90
+ * @phpstan-var list<string>
91
  */
92
+ private $numeratorUnits;
93
 
94
  /**
95
+ * @var string[]
96
+ * @phpstan-var list<string>
 
 
97
  */
98
+ private $denominatorUnits;
 
 
 
 
 
 
 
 
99
 
100
  /**
101
+ * Initialize number
102
  *
103
+ * @param integer|float $dimension
104
+ * @param string[]|string $numeratorUnits
105
+ * @param string[] $denominatorUnits
106
  *
107
+ * @phpstan-param list<string>|string $numeratorUnits
108
+ * @phpstan-param list<string> $denominatorUnits
109
  */
110
+ public function __construct($dimension, $numeratorUnits, array $denominatorUnits = [])
111
  {
112
+ if (is_string($numeratorUnits)) {
113
+ $numeratorUnits = $numeratorUnits ? [$numeratorUnits] : [];
114
+ } elseif (isset($numeratorUnits['numerator_units'], $numeratorUnits['denominator_units'])) {
115
+ // TODO get rid of this once `$number[2]` is not used anymore
116
+ $denominatorUnits = $numeratorUnits['denominator_units'];
117
+ $numeratorUnits = $numeratorUnits['numerator_units'];
118
  }
119
 
120
+ $this->dimension = $dimension;
121
+ $this->numeratorUnits = $numeratorUnits;
122
+ $this->denominatorUnits = $denominatorUnits;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  }
124
 
125
  /**
126
+ * @return float|int
 
 
127
  */
128
+ public function getDimension()
129
  {
130
+ return $this->dimension;
131
+ }
132
 
133
+ /**
134
+ * @return string[]
135
+ */
136
+ public function getNumeratorUnits()
137
+ {
138
+ return $this->numeratorUnits;
139
+ }
140
 
141
+ /**
142
+ * @return string[]
143
+ */
144
+ public function getDenominatorUnits()
145
+ {
146
+ return $this->denominatorUnits;
147
  }
148
 
149
  /**
187
  return $this->sourceIndex;
188
 
189
  case 0:
190
+ return Type::T_NUMBER;
191
 
192
  case 1:
193
  return $this->dimension;
194
 
195
  case 2:
196
+ return array('numerator_units' => $this->numeratorUnits, 'denominator_units' => $this->denominatorUnits);
197
  }
198
  }
199
 
202
  */
203
  public function offsetSet($offset, $value)
204
  {
205
+ throw new \BadMethodCallException('Number is immutable');
 
 
 
 
 
 
 
 
 
 
206
  }
207
 
208
  /**
210
  */
211
  public function offsetUnset($offset)
212
  {
213
+ throw new \BadMethodCallException('Number is immutable');
 
 
 
 
 
 
 
 
 
 
214
  }
215
 
216
  /**
220
  */
221
  public function unitless()
222
  {
223
+ return \count($this->numeratorUnits) === 0 && \count($this->denominatorUnits) === 0;
224
  }
225
 
226
  /**
227
+ * Checks whether the number has exactly this unit
 
228
  *
229
+ * @param string $unit
230
+ *
231
+ * @return bool
232
+ */
233
+ public function hasUnit($unit)
234
+ {
235
+ return \count($this->numeratorUnits) === 1 && \count($this->denominatorUnits) === 0 && $this->numeratorUnits[0] === $unit;
236
+ }
237
+
238
+ /**
239
+ * Returns unit(s) as the product of numerator units divided by the product of denominator units
240
+ *
241
+ * @return string
242
  */
243
+ public function unitStr()
244
  {
245
  if ($this->unitless()) {
246
+ return '';
247
+ }
248
+
249
+ return self::getUnitString($this->numeratorUnits, $this->denominatorUnits);
250
+ }
251
+
252
+ /**
253
+ * @param float|int $min
254
+ * @param float|int $max
255
+ * @param string|null $name
256
+ *
257
+ * @return float|int
258
+ * @throws SassScriptException
259
+ */
260
+ public function valueInRange($min, $max, $name = null)
261
+ {
262
+ try {
263
+ return Util::checkRange('', new Range($min, $max), $this);
264
+ } catch (RangeException $e) {
265
+ throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s', $this, $min, $this->unitStr(), $max), $name);
266
+ }
267
+ }
268
+
269
+ /**
270
+ * @param string|null $varName
271
+ *
272
+ * @return void
273
+ */
274
+ public function assertNoUnits($varName = null)
275
+ {
276
+ if ($this->unitless()) {
277
+ return;
278
+ }
279
+
280
+ throw SassScriptException::forArgument(sprintf('Expected %s to have no units.', $this), $varName);
281
+ }
282
+
283
+ /**
284
+ * @param string $unit
285
+ * @param string|null $varName
286
+ *
287
+ * @return void
288
+ */
289
+ public function assertUnit($unit, $varName = null)
290
+ {
291
+ if ($this->hasUnit($unit)) {
292
+ return;
293
+ }
294
+
295
+ throw SassScriptException::forArgument(sprintf('Expected %s to have unit "%s".', $this, $unit), $varName);
296
+ }
297
+
298
+ /**
299
+ * @param Number $other
300
+ *
301
+ * @return void
302
+ */
303
+ public function assertSameUnitOrUnitless(Number $other)
304
+ {
305
+ if ($other->unitless()) {
306
+ return;
307
+ }
308
+
309
+ if ($this->numeratorUnits === $other->numeratorUnits && $this->denominatorUnits === $other->denominatorUnits) {
310
+ return;
311
+ }
312
+
313
+ throw new SassScriptException(sprintf(
314
+ 'Incompatible units %s and %s.',
315
+ self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
316
+ self::getUnitString($other->numeratorUnits, $other->denominatorUnits)
317
+ ));
318
+ }
319
+
320
+ /**
321
+ * Returns a copy of this number, converted to the units represented by $newNumeratorUnits and $newDenominatorUnits.
322
+ *
323
+ * This does not throw an error if this number is unitless and
324
+ * $newNumeratorUnits/$newDenominatorUnits are not empty, or vice versa. Instead,
325
+ * it treats all unitless numbers as convertible to and from all units without
326
+ * changing the value.
327
+ *
328
+ * @param string[] $newNumeratorUnits
329
+ * @param string[] $newDenominatorUnits
330
+ *
331
+ * @return Number
332
+ *
333
+ * @phpstan-param list<string> $newNumeratorUnits
334
+ * @phpstan-param list<string> $newDenominatorUnits
335
+ *
336
+ * @throws SassScriptException if this number's units are not compatible with $newNumeratorUnits and $newDenominatorUnits
337
+ */
338
+ public function coerce(array $newNumeratorUnits, array $newDenominatorUnits)
339
+ {
340
+ return new Number($this->valueInUnits($newNumeratorUnits, $newDenominatorUnits), $newNumeratorUnits, $newDenominatorUnits);
341
+ }
342
+
343
+ /**
344
+ * @param Number $other
345
+ *
346
+ * @return bool
347
+ */
348
+ public function isComparableTo(Number $other)
349
+ {
350
+ if ($this->unitless() || $other->unitless()) {
351
+ return true;
352
+ }
353
+
354
+ try {
355
+ $this->greaterThan($other);
356
+ return true;
357
+ } catch (SassScriptException $e) {
358
  return false;
359
  }
360
+ }
361
+
362
+ /**
363
+ * @param Number $other
364
+ *
365
+ * @return bool
366
+ */
367
+ public function lessThan(Number $other)
368
+ {
369
+ return $this->coerceUnits($other, function ($num1, $num2) {
370
+ return $num1 < $num2;
371
+ });
372
+ }
373
+
374
+ /**
375
+ * @param Number $other
376
+ *
377
+ * @return bool
378
+ */
379
+ public function lessThanOrEqual(Number $other)
380
+ {
381
+ return $this->coerceUnits($other, function ($num1, $num2) {
382
+ return $num1 <= $num2;
383
+ });
384
+ }
385
+
386
+ /**
387
+ * @param Number $other
388
+ *
389
+ * @return bool
390
+ */
391
+ public function greaterThan(Number $other)
392
+ {
393
+ return $this->coerceUnits($other, function ($num1, $num2) {
394
+ return $num1 > $num2;
395
+ });
396
+ }
397
+
398
+ /**
399
+ * @param Number $other
400
+ *
401
+ * @return bool
402
+ */
403
+ public function greaterThanOrEqual(Number $other)
404
+ {
405
+ return $this->coerceUnits($other, function ($num1, $num2) {
406
+ return $num1 >= $num2;
407
+ });
408
+ }
409
+
410
+ /**
411
+ * @param Number $other
412
+ *
413
+ * @return Number
414
+ */
415
+ public function plus(Number $other)
416
+ {
417
+ return $this->coerceNumber($other, function ($num1, $num2) {
418
+ return $num1 + $num2;
419
+ });
420
+ }
421
+
422
+ /**
423
+ * @param Number $other
424
+ *
425
+ * @return Number
426
+ */
427
+ public function minus(Number $other)
428
+ {
429
+ return $this->coerceNumber($other, function ($num1, $num2) {
430
+ return $num1 - $num2;
431
+ });
432
+ }
433
 
434
+ /**
435
+ * @return Number
436
+ */
437
+ public function unaryMinus()
438
+ {
439
+ return new Number(-$this->dimension, $this->numeratorUnits, $this->denominatorUnits);
440
+ }
441
 
442
+ /**
443
+ * @param Number $other
444
+ *
445
+ * @return Number
446
+ */
447
+ public function modulo(Number $other)
448
+ {
449
+ return $this->coerceNumber($other, function ($num1, $num2) {
450
+ if ($num2 == 0) {
451
+ return NAN;
452
+ }
453
+
454
+ $result = fmod($num1, $num2);
455
 
456
+ if ($result == 0) {
457
+ return 0;
458
  }
459
 
460
+ if ($num2 < 0 xor $num1 < 0) {
461
+ $result += $num2;
462
  }
 
463
 
464
+ return $result;
465
+ });
466
  }
467
 
468
  /**
469
+ * @param Number $other
470
  *
471
+ * @return Number
472
  */
473
+ public function times(Number $other)
474
  {
475
+ return $this->multiplyUnits($this->dimension * $other->dimension, $this->numeratorUnits, $this->denominatorUnits, $other->numeratorUnits, $other->denominatorUnits);
476
+ }
477
 
478
+ /**
479
+ * @param Number $other
480
+ *
481
+ * @return Number
482
+ */
483
+ public function dividedBy(Number $other)
484
+ {
485
+ if ($other->dimension == 0) {
486
+ if ($this->dimension == 0) {
487
+ $value = NAN;
488
+ } elseif ($this->dimension > 0) {
489
+ $value = INF;
490
+ } else {
491
+ $value = -INF;
492
  }
493
+ } else {
494
+ $value = $this->dimension / $other->dimension;
495
+ }
496
 
497
+ return $this->multiplyUnits($value, $this->numeratorUnits, $this->denominatorUnits, $other->denominatorUnits, $other->numeratorUnits);
498
+ }
499
+
500
+ /**
501
+ * @param Number $other
502
+ *
503
+ * @return bool
504
+ */
505
+ public function equals(Number $other)
506
+ {
507
+ // Unitless numbers are convertable to unit numbers, but not equal, so we special-case unitless here.
508
+ if ($this->unitless() !== $other->unitless()) {
509
+ return false;
510
  }
511
 
512
+ // In Sass, neither NaN nor Infinity are equal to themselves, while PHP defines INF==INF
513
+ if (is_nan($this->dimension) || is_nan($other->dimension) || !is_finite($this->dimension) || !is_finite($other->dimension)) {
514
+ return false;
515
+ }
516
+
517
+ if ($this->unitless()) {
518
+ return round($this->dimension, self::PRECISION) == round($other->dimension, self::PRECISION);
519
+ }
520
+
521
+ try {
522
+ return $this->coerceUnits($other, function ($num1, $num2) {
523
+ return round($num1,self::PRECISION) == round($num2, self::PRECISION);
524
+ });
525
+ } catch (SassScriptException $e) {
526
+ return false;
527
+ }
528
  }
529
 
530
  /**
538
  {
539
  $dimension = round($this->dimension, self::PRECISION);
540
 
541
+ if (is_nan($dimension)) {
542
+ return 'NaN';
543
+ }
 
 
 
 
 
 
544
 
545
+ if ($dimension === INF) {
546
+ return 'Infinity';
 
 
547
  }
548
 
549
+ if ($dimension === -INF) {
550
+ return '-Infinity';
551
+ }
552
 
553
+ if ($compiler) {
 
554
  $unit = $this->unitStr();
555
+ } elseif (isset($this->numeratorUnits[0])) {
556
+ $unit = $this->numeratorUnits[0];
557
  } else {
558
+ $unit = '';
 
559
  }
560
 
561
  $dimension = number_format($dimension, self::PRECISION, '.', '');
562
 
563
+ return rtrim(rtrim($dimension, '0'), '.') . $unit;
564
  }
565
 
566
  /**
572
  }
573
 
574
  /**
575
+ * @param Number $other
576
+ * @param callable $operation
577
+ *
578
+ * @return Number
579
+ *
580
+ * @phpstan-param callable(int|float, int|float): (int|float) $operation
581
+ */
582
+ private function coerceNumber(Number $other, $operation)
583
+ {
584
+ $result = $this->coerceUnits($other, $operation);
585
+
586
+ if (!$this->unitless()) {
587
+ return new Number($result, $this->numeratorUnits, $this->denominatorUnits);
588
+ }
589
+
590
+ return new Number($result, $other->numeratorUnits, $other->denominatorUnits);
591
+ }
592
+
593
+ /**
594
+ * @param Number $other
595
+ * @param callable $operation
596
+ *
597
+ * @return mixed
598
+ *
599
+ * @phpstan-template T
600
+ * @phpstan-param callable(int|float, int|float): T $operation
601
+ * @phpstan-return T
602
+ */
603
+ private function coerceUnits(Number $other, $operation)
604
+ {
605
+ if (!$this->unitless()) {
606
+ $num1 = $this->dimension;
607
+ $num2 = $other->valueInUnits($this->numeratorUnits, $this->denominatorUnits);
608
+ } else {
609
+ $num1 = $this->valueInUnits($other->numeratorUnits, $other->denominatorUnits);
610
+ $num2 = $other->dimension;
611
+ }
612
+
613
+ return \call_user_func($operation, $num1, $num2);
614
+ }
615
+
616
+ /**
617
+ * @param string[] $numeratorUnits
618
+ * @param string[] $denominatorUnits
619
  *
620
+ * @return int|float
621
+ *
622
+ * @phpstan-param list<string> $numeratorUnits
623
+ * @phpstan-param list<string> $denominatorUnits
624
+ *
625
+ * @throws SassScriptException if this number's units are not compatible with $numeratorUnits and $denominatorUnits
626
  */
627
+ private function valueInUnits(array $numeratorUnits, array $denominatorUnits)
628
  {
629
+ if (
630
+ $this->unitless()
631
+ || (\count($numeratorUnits) === 0 && \count($denominatorUnits) === 0)
632
+ || ($this->numeratorUnits === $numeratorUnits && $this->denominatorUnits === $denominatorUnits)
633
+ ) {
634
+ return $this->dimension;
635
+ }
636
+
637
+ $value = $this->dimension;
638
+ $oldNumerators = $this->numeratorUnits;
639
+
640
+ foreach ($numeratorUnits as $newNumerator) {
641
+ foreach ($oldNumerators as $key => $oldNumerator) {
642
+ $conversionFactor = self::getConversionFactor($newNumerator, $oldNumerator);
643
+
644
+ if (\is_null($conversionFactor)) {
645
+ continue;
646
+ }
647
 
648
+ $value *= $conversionFactor;
649
+ unset($oldNumerators[$key]);
650
+ continue 2;
651
  }
652
 
653
+ throw new SassScriptException(sprintf(
654
+ 'Incompatible units %s and %s.',
655
+ self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
656
+ self::getUnitString($numeratorUnits, $denominatorUnits)
657
+ ));
658
+ }
659
+
660
+ $oldDenominators = $this->denominatorUnits;
661
+
662
+ foreach ($denominatorUnits as $newDenominator) {
663
+ foreach ($oldDenominators as $key => $oldDenominator) {
664
+ $conversionFactor = self::getConversionFactor($newDenominator, $oldDenominator);
665
 
666
+ if (\is_null($conversionFactor)) {
667
+ continue;
668
+ }
669
+
670
+ $value /= $conversionFactor;
671
+ unset($oldDenominators[$key]);
672
+ continue 2;
673
  }
674
 
675
+ throw new SassScriptException(sprintf(
676
+ 'Incompatible units %s and %s.',
677
+ self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
678
+ self::getUnitString($numeratorUnits, $denominatorUnits)
679
+ ));
680
+ }
681
+
682
+ if (\count($oldNumerators) || \count($oldDenominators)) {
683
+ throw new SassScriptException(sprintf(
684
+ 'Incompatible units %s and %s.',
685
+ self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
686
+ self::getUnitString($numeratorUnits, $denominatorUnits)
687
+ ));
688
  }
689
+
690
+ return $value;
691
  }
692
 
693
  /**
694
+ * @param int|float $value
695
+ * @param string[] $numerators1
696
+ * @param string[] $denominators1
697
+ * @param string[] $numerators2
698
+ * @param string[] $denominators2
699
  *
700
+ * @return Number
701
+ *
702
+ * @phpstan-param list<string> $numerators1
703
+ * @phpstan-param list<string> $denominators1
704
+ * @phpstan-param list<string> $numerators2
705
+ * @phpstan-param list<string> $denominators2
706
+ */
707
+ private function multiplyUnits($value, array $numerators1, array $denominators1, array $numerators2, array $denominators2)
708
+ {
709
+ $newNumerators = array();
710
+
711
+ foreach ($numerators1 as $numerator) {
712
+ foreach ($denominators2 as $key => $denominator) {
713
+ $conversionFactor = self::getConversionFactor($numerator, $denominator);
714
+
715
+ if (\is_null($conversionFactor)) {
716
+ continue;
717
+ }
718
+
719
+ $value /= $conversionFactor;
720
+ unset($denominators2[$key]);
721
+ continue 2;
722
+ }
723
+
724
+ $newNumerators[] = $numerator;
725
+ }
726
+
727
+ foreach ($numerators2 as $numerator) {
728
+ foreach ($denominators1 as $key => $denominator) {
729
+ $conversionFactor = self::getConversionFactor($numerator, $denominator);
730
+
731
+ if (\is_null($conversionFactor)) {
732
+ continue;
733
+ }
734
+
735
+ $value /= $conversionFactor;
736
+ unset($denominators1[$key]);
737
+ continue 2;
738
+ }
739
+
740
+ $newNumerators[] = $numerator;
741
+ }
742
+
743
+ $newDenominators = array_values(array_merge($denominators1, $denominators2));
744
+
745
+ return new Number($value, $newNumerators, $newDenominators);
746
+ }
747
+
748
+ /**
749
+ * Returns the number of [unit1]s per [unit2].
750
+ *
751
+ * Equivalently, `1unit1 * conversionFactor(unit1, unit2) = 1unit2`.
752
  *
753
+ * @param string $unit1
754
+ * @param string $unit2
755
+ *
756
+ * @return float|int|null
757
  */
758
+ private static function getConversionFactor($unit1, $unit2)
759
  {
760
+ if ($unit1 === $unit2) {
761
+ return 1;
762
+ }
763
+
764
+ foreach (static::$unitTable as $unitVariants) {
765
+ if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) {
766
+ return $unitVariants[$unit1] / $unitVariants[$unit2];
767
  }
768
  }
769
 
770
  return null;
771
  }
772
+
773
+ /**
774
+ * Returns unit(s) as the product of numerator units divided by the product of denominator units
775
+ *
776
+ * @param string[] $numerators
777
+ * @param string[] $denominators
778
+ *
779
+ * @phpstan-param list<string> $numerators
780
+ * @phpstan-param list<string> $denominators
781
+ *
782
+ * @return string
783
+ */
784
+ private static function getUnitString(array $numerators, array $denominators)
785
+ {
786
+ if (!\count($numerators)) {
787
+ if (\count($denominators) === 0) {
788
+ return 'no units';
789
+ }
790
+
791
+ if (\count($denominators) === 1) {
792
+ return $denominators[0] . '^-1';
793
+ }
794
+
795
+ return '(' . implode('*', $denominators) . ')^-1';
796
+ }
797
+
798
+ return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : '');
799
+ }
800
  }
scssphp/src/OutputStyle.php ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace ScssPhp\ScssPhp;
4
+
5
+ final class OutputStyle
6
+ {
7
+ const EXPANDED = 'expanded';
8
+ const COMPRESSED = 'compressed';
9
+ }
scssphp/src/Parser.php CHANGED
@@ -12,17 +12,16 @@
12
 
13
  namespace ScssPhp\ScssPhp;
14
 
15
- use ScssPhp\ScssPhp\Block;
16
- use ScssPhp\ScssPhp\Cache;
17
- use ScssPhp\ScssPhp\Compiler;
18
  use ScssPhp\ScssPhp\Exception\ParserException;
19
- use ScssPhp\ScssPhp\Node;
20
- use ScssPhp\ScssPhp\Type;
21
 
22
  /**
23
  * Parser
24
  *
25
  * @author Leaf Corcoran <leafot@gmail.com>
 
 
26
  */
27
  class Parser
28
  {
@@ -31,7 +30,7 @@ class Parser
31
  const SOURCE_COLUMN = -3;
32
 
33
  /**
34
- * @var array
35
  */
36
  protected static $precedence = [
37
  '=' => 0,
@@ -39,7 +38,6 @@ class Parser
39
  'and' => 2,
40
  '==' => 3,
41
  '!=' => 3,
42
- '<=>' => 3,
43
  '<=' => 4,
44
  '>=' => 4,
45
  '<' => 4,
@@ -51,41 +49,89 @@ class Parser
51
  '%' => 6,
52
  ];
53
 
 
 
 
54
  protected static $commentPattern;
 
 
 
55
  protected static $operatorPattern;
 
 
 
56
  protected static $whitePattern;
57
 
 
 
 
58
  protected $cache;
59
 
60
  private $sourceName;
61
  private $sourceIndex;
 
 
 
62
  private $sourcePositions;
 
 
 
63
  private $charset;
 
 
 
 
 
64
  private $count;
 
 
 
65
  private $env;
 
 
 
66
  private $inParens;
 
 
 
67
  private $eatWhiteDefault;
 
 
 
68
  private $discardComments;
69
  private $allowVars;
 
 
 
70
  private $buffer;
71
  private $utf8;
 
 
 
72
  private $encoding;
73
  private $patternModifiers;
74
  private $commentsSeen;
75
 
76
  private $cssOnly;
77
 
 
 
 
 
 
78
  /**
79
  * Constructor
80
  *
81
  * @api
82
  *
83
- * @param string $sourceName
84
- * @param integer $sourceIndex
85
- * @param string $encoding
86
- * @param \ScssPhp\ScssPhp\Cache $cache
 
 
87
  */
88
- public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', $cache = null, $cssOnly = false)
89
  {
90
  $this->sourceName = $sourceName ?: '(stdin)';
91
  $this->sourceIndex = $sourceIndex;
@@ -96,9 +142,10 @@ class Parser
96
  $this->commentsSeen = [];
97
  $this->allowVars = true;
98
  $this->cssOnly = $cssOnly;
 
99
 
100
  if (empty(static::$operatorPattern)) {
101
- static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
102
 
103
  $commentSingle = '\/\/';
104
  $commentMultiLeft = '\/\*';
@@ -110,9 +157,7 @@ class Parser
110
  : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS';
111
  }
112
 
113
- if ($cache) {
114
- $this->cache = $cache;
115
- }
116
  }
117
 
118
  /**
@@ -134,9 +179,32 @@ class Parser
134
  *
135
  * @param string $msg
136
  *
137
- * @throws \ScssPhp\ScssPhp\Exception\ParserException
 
 
 
 
138
  */
139
  public function throwParseError($msg = 'parse error')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  {
141
  list($line, $column) = $this->getSourcePosition($this->count);
142
 
@@ -150,7 +218,7 @@ class Parser
150
  $e = new ParserException("$msg: failed at `$m[1]` $loc");
151
  $e->setSourcePosition([$this->sourceName, $line, $column]);
152
 
153
- throw $e;
154
  }
155
 
156
  $this->restoreEncoding();
@@ -158,7 +226,7 @@ class Parser
158
  $e = new ParserException("$msg: $loc");
159
  $e->setSourcePosition([$this->sourceName, $line, $column]);
160
 
161
- throw $e;
162
  }
163
 
164
  /**
@@ -168,7 +236,7 @@ class Parser
168
  *
169
  * @param string $buffer
170
  *
171
- * @return \ScssPhp\ScssPhp\Block
172
  */
173
  public function parse($buffer)
174
  {
@@ -209,11 +277,11 @@ class Parser
209
  }
210
 
211
  if ($this->count !== \strlen($this->buffer)) {
212
- $this->throwParseError();
213
  }
214
 
215
  if (! empty($this->env->parent)) {
216
- $this->throwParseError('unclosed block');
217
  }
218
 
219
  if ($this->charset) {
@@ -248,6 +316,7 @@ class Parser
248
  $this->buffer = (string) $buffer;
249
 
250
  $this->saveEncoding();
 
251
 
252
  $list = $this->valueList($out);
253
 
@@ -263,10 +332,11 @@ class Parser
263
  *
264
  * @param string $buffer
265
  * @param string|array $out
 
266
  *
267
  * @return boolean
268
  */
269
- public function parseSelector($buffer, &$out)
270
  {
271
  $this->count = 0;
272
  $this->env = null;
@@ -275,11 +345,21 @@ class Parser
275
  $this->buffer = (string) $buffer;
276
 
277
  $this->saveEncoding();
 
 
 
 
 
 
278
 
279
  $selector = $this->selectors($out);
280
 
281
  $this->restoreEncoding();
282
 
 
 
 
 
283
  return $selector;
284
  }
285
 
@@ -302,6 +382,7 @@ class Parser
302
  $this->buffer = (string) $buffer;
303
 
304
  $this->saveEncoding();
 
305
 
306
  $isMediaQuery = $this->mediaQueryList($out);
307
 
@@ -446,6 +527,10 @@ class Parser
446
  ) {
447
  ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
448
 
 
 
 
 
449
  $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s);
450
 
451
  return true;
@@ -460,7 +545,7 @@ class Parser
460
  $this->end()
461
  ) {
462
  if ($this->cssOnly) {
463
- $this->assertPlainCssValid($importPath, $s);
464
  $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]);
465
  return true;
466
  }
@@ -478,7 +563,7 @@ class Parser
478
  $this->end()
479
  ) {
480
  if ($this->cssOnly) {
481
- $this->assertPlainCssValid($importPath, $s);
482
  $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]);
483
  return true;
484
  }
@@ -523,32 +608,6 @@ class Parser
523
 
524
  $this->seek($s);
525
 
526
- if (
527
- $this->literal('@break', 6) &&
528
- $this->end()
529
- ) {
530
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
531
-
532
- $this->append([Type::T_BREAK], $s);
533
-
534
- return true;
535
- }
536
-
537
- $this->seek($s);
538
-
539
- if (
540
- $this->literal('@continue', 9) &&
541
- $this->end()
542
- ) {
543
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
544
-
545
- $this->append([Type::T_CONTINUE], $s);
546
-
547
- return true;
548
- }
549
-
550
- $this->seek($s);
551
-
552
  if (
553
  $this->literal('@return', 7) &&
554
  ($this->valueList($retVal) || true) &&
@@ -723,8 +782,7 @@ class Parser
723
  $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
724
  } elseif (
725
  $this->literal('if', 2) &&
726
- $this->valueList($cond) &&
727
- $this->matchChar('{', false)
728
  ) {
729
  $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
730
  $else->cond = $cond;
@@ -824,7 +882,7 @@ class Parser
824
  ! \in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA])
825
  ) {
826
  $plain = \trim(\substr($this->buffer, $s, $this->count - $s));
827
- $this->throwParseError(
828
  "Unknown directive `{$plain}` not allowed in `" . $this->env->type . "` block"
829
  );
830
  }
@@ -932,11 +990,6 @@ class Parser
932
 
933
  $this->seek($s);
934
 
935
- // misc
936
- if ($this->literal('-->', 3)) {
937
- return true;
938
- }
939
-
940
  // opening css block
941
  if (
942
  $this->selectors($selectors) &&
@@ -965,7 +1018,7 @@ class Parser
965
 
966
  if ($this->valueList($value)) {
967
  if (empty($this->env->parent)) {
968
- $this->throwParseError('expected "{"');
969
  }
970
 
971
  $this->append([Type::T_ASSIGN, $name, $value], $s);
@@ -1024,10 +1077,7 @@ class Parser
1024
  }
1025
 
1026
  // extra stuff
1027
- if (
1028
- $this->matchChar(';') ||
1029
- $this->literal('<!--', 4)
1030
- ) {
1031
  return true;
1032
  }
1033
 
@@ -1037,10 +1087,10 @@ class Parser
1037
  /**
1038
  * Push block onto parse tree
1039
  *
1040
- * @param array $selectors
1041
  * @param integer $pos
1042
  *
1043
- * @return \ScssPhp\ScssPhp\Block
1044
  */
1045
  protected function pushBlock($selectors, $pos = 0)
1046
  {
@@ -1086,7 +1136,7 @@ class Parser
1086
  * @param string $type
1087
  * @param integer $pos
1088
  *
1089
- * @return \ScssPhp\ScssPhp\Block
1090
  */
1091
  protected function pushSpecialBlock($type, $pos)
1092
  {
@@ -1099,7 +1149,7 @@ class Parser
1099
  /**
1100
  * Pop scope and return last block
1101
  *
1102
- * @return \ScssPhp\ScssPhp\Block
1103
  *
1104
  * @throws \Exception
1105
  */
@@ -1115,7 +1165,7 @@ class Parser
1115
  $block = $this->env;
1116
 
1117
  if (empty($block->parent)) {
1118
- $this->throwParseError('unexpected }');
1119
  }
1120
 
1121
  if ($block->type == Type::T_AT_ROOT) {
@@ -1146,7 +1196,7 @@ class Parser
1146
  }
1147
 
1148
  $r = '/' . $regex . '/' . $this->patternModifiers;
1149
- $result = preg_match($r, $this->buffer, $out, null, $from);
1150
 
1151
  return $result;
1152
  }
@@ -1164,7 +1214,7 @@ class Parser
1164
  /**
1165
  * Assert a parsed part is plain CSS Valid
1166
  *
1167
- * @param array $parsed
1168
  * @param int $startPos
1169
  * @throws ParserException
1170
  */
@@ -1185,7 +1235,7 @@ class Parser
1185
  if ($type) {
1186
  $message .= " ($type)";
1187
  }
1188
- $this->throwParseError($message);
1189
  }
1190
 
1191
  return $parsed;
@@ -1198,6 +1248,11 @@ class Parser
1198
  */
1199
  protected function isPlainCssValidElement($parsed, $allowExpression = false)
1200
  {
 
 
 
 
 
1201
  if (
1202
  \in_array($parsed[0], [Type::T_FUNCTION, Type::T_FUNCTION_CALL]) &&
1203
  !\in_array($parsed[1], [
@@ -1209,6 +1264,7 @@ class Parser
1209
  'grayscale',
1210
  'hsl',
1211
  'hsla',
 
1212
  'invert',
1213
  'linear-gradient',
1214
  'min',
@@ -1232,6 +1288,7 @@ class Parser
1232
  case Type::T_KEYWORD:
1233
  case Type::T_NULL:
1234
  case Type::T_NUMBER:
 
1235
  return $parsed;
1236
 
1237
  case Type::T_COMMENT:
@@ -1250,6 +1307,16 @@ class Parser
1250
 
1251
  return $parsed;
1252
 
 
 
 
 
 
 
 
 
 
 
1253
  case Type::T_STRING:
1254
  foreach ($parsed[2] as $k => $substr) {
1255
  if (\is_array($substr)) {
@@ -1261,6 +1328,18 @@ class Parser
1261
  }
1262
  return $parsed;
1263
 
 
 
 
 
 
 
 
 
 
 
 
 
1264
  case Type::T_ASSIGN:
1265
  foreach ([1, 2, 3] as $k) {
1266
  if (! empty($parsed[$k])) {
@@ -1297,6 +1376,7 @@ class Parser
1297
  ]
1298
  ];
1299
 
 
1300
  case Type::T_UNARY:
1301
  $parsed[2] = $this->isPlainCssValidElement($parsed[2]);
1302
  if (! $parsed[2]) {
@@ -1389,7 +1469,7 @@ class Parser
1389
  {
1390
  $r = '/' . $regex . '/' . $this->patternModifiers;
1391
 
1392
- if (! preg_match($r, $this->buffer, $out, null, $this->count)) {
1393
  return false;
1394
  }
1395
 
@@ -1470,7 +1550,7 @@ class Parser
1470
  {
1471
  $gotWhite = false;
1472
 
1473
- while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) {
1474
  if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
1475
  // comment that are kept in the output CSS
1476
  $comment = [];
@@ -1498,6 +1578,9 @@ class Parser
1498
 
1499
  $comment[] = [Type::T_COMMENT, substr($this->buffer, $p, $this->count - $p), $out];
1500
  } else {
 
 
 
1501
  $comment[] = substr($this->buffer, $this->count, 2);
1502
 
1503
  $this->count += 2;
@@ -1515,7 +1598,14 @@ class Parser
1515
  } else {
1516
  $comment[] = $c;
1517
  $staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount);
1518
- $this->appendComment([Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]]);
 
 
 
 
 
 
 
1519
  }
1520
 
1521
  $this->commentsSeen[$startCommentCount] = true;
@@ -1550,7 +1640,7 @@ class Parser
1550
  /**
1551
  * Append statement to current block
1552
  *
1553
- * @param array $statement
1554
  * @param integer $pos
1555
  */
1556
  protected function append($statement, $pos = null)
@@ -1885,7 +1975,7 @@ class Parser
1885
 
1886
  /**
1887
  * Check if a generic directive is known to be able to allow almost any syntax or not
1888
- * @param $directiveName
1889
  * @return bool
1890
  */
1891
  protected function isKnownGenericDirective($directiveName)
@@ -2063,10 +2153,10 @@ class Parser
2063
  /**
2064
  * Parse generic list
2065
  *
2066
- * @param array $out
2067
- * @param callable $parseItem
2068
- * @param string $delim
2069
- * @param boolean $flatten
2070
  *
2071
  * @return boolean
2072
  */
@@ -2755,7 +2845,7 @@ class Parser
2755
  $sss = $this->count;
2756
 
2757
  if (! $this->matchChar(')')) {
2758
- $this->throwParseError('... has to be after the final argument');
2759
  }
2760
 
2761
  $arg[2] = true;
@@ -2917,7 +3007,7 @@ class Parser
2917
  $content[] = '#{'; // ignore it
2918
  }
2919
  } elseif ($m[2] === "\r") {
2920
- $content[] = '\\a';
2921
  // TODO : warning
2922
  # DEPRECATION WARNING on line x, column y of zzz:
2923
  # Unescaped multiline strings are deprecated and will be removed in a future version of Sass.
@@ -2936,7 +3026,7 @@ class Parser
2936
  } elseif ($this->matchEscapeCharacter($c)) {
2937
  $content[] = $c;
2938
  } else {
2939
- $this->throwParseError('Unterminated escape sequence');
2940
  }
2941
  } else {
2942
  $this->count -= \strlen($delim);
@@ -2961,8 +3051,14 @@ class Parser
2961
  return false;
2962
  }
2963
 
2964
- protected function matchEscapeCharacter(&$out)
 
 
 
 
 
2965
  {
 
2966
  if ($this->match('[a-f0-9]', $m, false)) {
2967
  $hex = $m[0];
2968
 
@@ -2974,10 +3070,16 @@ class Parser
2974
  }
2975
  }
2976
 
 
 
 
 
2977
  $value = hexdec($hex);
2978
 
2979
- if ($value == 0 || ($value >= 0xD800 && $value <= 0xDFFF) || $value >= 0x10FFFF) {
2980
- $out = "\u{FFFD}";
 
 
2981
  } else {
2982
  $out = Util::mbChr($value);
2983
  }
@@ -2986,6 +3088,10 @@ class Parser
2986
  }
2987
 
2988
  if ($this->match('.', $m, false)) {
 
 
 
 
2989
  $out = $m[0];
2990
 
2991
  return true;
@@ -3217,7 +3323,7 @@ class Parser
3217
  }
3218
 
3219
  // match comment hack
3220
- if (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) {
3221
  if (! empty($m[0])) {
3222
  $parts[] = $m[0];
3223
  $this->count += \strlen($m[0]);
@@ -3293,8 +3399,8 @@ class Parser
3293
  /**
3294
  * Parse comma separated selector list
3295
  *
3296
- * @param array $out
3297
- * @param boolean $subSelector
3298
  *
3299
  * @return boolean
3300
  */
@@ -3329,8 +3435,8 @@ class Parser
3329
  /**
3330
  * Parse whitespace separated selector list
3331
  *
3332
- * @param array $out
3333
- * @param boolean $subSelector
3334
  *
3335
  * @return boolean
3336
  */
@@ -3338,6 +3444,9 @@ class Parser
3338
  {
3339
  $selector = [];
3340
 
 
 
 
3341
  for (;;) {
3342
  $s = $this->count;
3343
 
@@ -3355,18 +3464,15 @@ class Parser
3355
 
3356
  if ($this->selectorSingle($part, $subSelector)) {
3357
  $selector[] = $part;
3358
- $this->match('\s+', $m);
3359
- continue;
3360
- }
3361
-
3362
- if ($this->match('\/[^\/]+\/', $m, true)) {
3363
- $selector[] = [$m[0]];
3364
  continue;
3365
  }
3366
 
3367
  break;
3368
  }
3369
 
 
 
3370
  if (! $selector) {
3371
  return false;
3372
  }
@@ -3376,6 +3482,55 @@ class Parser
3376
  return true;
3377
  }
3378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3379
  /**
3380
  * Parse the parts that make up a selector
3381
  *
@@ -3383,8 +3538,8 @@ class Parser
3383
  * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
3384
  * }}
3385
  *
3386
- * @param array $out
3387
- * @param boolean $subSelector
3388
  *
3389
  * @return boolean
3390
  */
@@ -3436,9 +3591,14 @@ class Parser
3436
  continue 2;
3437
  }
3438
 
3439
- if ($char === '\\' && $this->match('\\\\\S', $m)) {
3440
- $parts[] = $m[0];
3441
- continue;
 
 
 
 
 
3442
  }
3443
 
3444
  if ($char === '%') {
@@ -3524,7 +3684,7 @@ class Parser
3524
  $this->seek($ss);
3525
  }
3526
  } elseif (
3527
- $this->matchChar('(') &&
3528
  ($this->openString(')', $str, '(') || true) &&
3529
  $this->matchChar(')')
3530
  ) {
@@ -3581,7 +3741,7 @@ class Parser
3581
  continue;
3582
  }
3583
 
3584
- if ($this->restrictedKeyword($name)) {
3585
  $parts[] = $name;
3586
  continue;
3587
  }
@@ -3634,22 +3794,62 @@ class Parser
3634
  *
3635
  * @param string $word
3636
  * @param boolean $eatWhitespace
 
3637
  *
3638
  * @return boolean
3639
  */
3640
- protected function keyword(&$word, $eatWhitespace = null)
3641
  {
 
3642
  $match = $this->match(
3643
  $this->utf8
3644
- ? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|[\\\\].)*)'
3645
- : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
3646
  $m,
3647
- $eatWhitespace
3648
  );
3649
 
3650
  if ($match) {
3651
  $word = $m[1];
3652
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3653
  return true;
3654
  }
3655
 
@@ -3661,14 +3861,15 @@ class Parser
3661
  *
3662
  * @param string $word
3663
  * @param boolean $eatWhitespace
 
3664
  *
3665
  * @return boolean
3666
  */
3667
- protected function restrictedKeyword(&$word, $eatWhitespace = null)
3668
  {
3669
  $s = $this->count;
3670
 
3671
- if ($this->keyword($word, $eatWhitespace) && (\ord($word[0]) > 57 || \ord($word[0]) < 48)) {
3672
  return true;
3673
  }
3674
 
@@ -3893,11 +4094,20 @@ class Parser
3893
  }
3894
 
3895
  /**
3896
- * Save internal encoding
 
 
 
 
 
 
 
 
 
3897
  */
3898
  private function saveEncoding()
3899
  {
3900
- if (\extension_loaded('mbstring')) {
3901
  $this->encoding = mb_internal_encoding();
3902
 
3903
  mb_internal_encoding('iso-8859-1');
@@ -3906,6 +4116,8 @@ class Parser
3906
 
3907
  /**
3908
  * Restore internal encoding
 
 
3909
  */
3910
  private function restoreEncoding()
3911
  {
12
 
13
  namespace ScssPhp\ScssPhp;
14
 
 
 
 
15
  use ScssPhp\ScssPhp\Exception\ParserException;
16
+ use ScssPhp\ScssPhp\Logger\LoggerInterface;
17
+ use ScssPhp\ScssPhp\Logger\QuietLogger;
18
 
19
  /**
20
  * Parser
21
  *
22
  * @author Leaf Corcoran <leafot@gmail.com>
23
+ *
24
+ * @internal
25
  */
26
  class Parser
27
  {
30
  const SOURCE_COLUMN = -3;
31
 
32
  /**
33
+ * @var array<string, int>
34
  */
35
  protected static $precedence = [
36
  '=' => 0,
38
  'and' => 2,
39
  '==' => 3,
40
  '!=' => 3,
 
41
  '<=' => 4,
42
  '>=' => 4,
43
  '<' => 4,
49
  '%' => 6,
50
  ];
51
 
52
+ /**
53
+ * @var string
54
+ */
55
  protected static $commentPattern;
56
+ /**
57
+ * @var string
58
+ */
59
  protected static $operatorPattern;
60
+ /**
61
+ * @var string
62
+ */
63
  protected static $whitePattern;
64
 
65
+ /**
66
+ * @var Cache|null
67
+ */
68
  protected $cache;
69
 
70
  private $sourceName;
71
  private $sourceIndex;
72
+ /**
73
+ * @var array<int, int>
74
+ */
75
  private $sourcePositions;
76
+ /**
77
+ * @var array|null
78
+ */
79
  private $charset;
80
+ /**
81
+ * The current offset in the buffer
82
+ *
83
+ * @var int
84
+ */
85
  private $count;
86
+ /**
87
+ * @var Block|null
88
+ */
89
  private $env;
90
+ /**
91
+ * @var bool
92
+ */
93
  private $inParens;
94
+ /**
95
+ * @var bool
96
+ */
97
  private $eatWhiteDefault;
98
+ /**
99
+ * @var bool
100
+ */
101
  private $discardComments;
102
  private $allowVars;
103
+ /**
104
+ * @var string
105
+ */
106
  private $buffer;
107
  private $utf8;
108
+ /**
109
+ * @var string|null
110
+ */
111
  private $encoding;
112
  private $patternModifiers;
113
  private $commentsSeen;
114
 
115
  private $cssOnly;
116
 
117
+ /**
118
+ * @var LoggerInterface
119
+ */
120
+ private $logger;
121
+
122
  /**
123
  * Constructor
124
  *
125
  * @api
126
  *
127
+ * @param string|null $sourceName
128
+ * @param integer $sourceIndex
129
+ * @param string|null $encoding
130
+ * @param Cache|null $cache
131
+ * @param bool $cssOnly
132
+ * @param LoggerInterface|null $logger
133
  */
134
+ public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache $cache = null, $cssOnly = false, LoggerInterface $logger = null)
135
  {
136
  $this->sourceName = $sourceName ?: '(stdin)';
137
  $this->sourceIndex = $sourceIndex;
142
  $this->commentsSeen = [];
143
  $this->allowVars = true;
144
  $this->cssOnly = $cssOnly;
145
+ $this->logger = $logger ?: new QuietLogger();
146
 
147
  if (empty(static::$operatorPattern)) {
148
+ static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)';
149
 
150
  $commentSingle = '\/\/';
151
  $commentMultiLeft = '\/\*';
157
  : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS';
158
  }
159
 
160
+ $this->cache = $cache;
 
 
161
  }
162
 
163
  /**
179
  *
180
  * @param string $msg
181
  *
182
+ * @phpstan-return never-return
183
+ *
184
+ * @throws ParserException
185
+ *
186
+ * @deprecated use "parseError" and throw the exception in the caller instead.
187
  */
188
  public function throwParseError($msg = 'parse error')
189
+ {
190
+ @trigger_error(
191
+ 'The method "throwParseError" is deprecated. Use "parseError" and throw the exception in the caller instead',
192
+ E_USER_DEPRECATED
193
+ );
194
+
195
+ throw $this->parseError($msg);
196
+ }
197
+
198
+ /**
199
+ * Creates a parser error
200
+ *
201
+ * @api
202
+ *
203
+ * @param string $msg
204
+ *
205
+ * @return ParserException
206
+ */
207
+ public function parseError($msg = 'parse error')
208
  {
209
  list($line, $column) = $this->getSourcePosition($this->count);
210
 
218
  $e = new ParserException("$msg: failed at `$m[1]` $loc");
219
  $e->setSourcePosition([$this->sourceName, $line, $column]);
220
 
221
+ return $e;
222
  }
223
 
224
  $this->restoreEncoding();
226
  $e = new ParserException("$msg: $loc");
227
  $e->setSourcePosition([$this->sourceName, $line, $column]);
228
 
229
+ return $e;
230
  }
231
 
232
  /**
236
  *
237
  * @param string $buffer
238
  *
239
+ * @return Block
240
  */
241
  public function parse($buffer)
242
  {
277
  }
278
 
279
  if ($this->count !== \strlen($this->buffer)) {
280
+ throw $this->parseError();
281
  }
282
 
283
  if (! empty($this->env->parent)) {
284
+ throw $this->parseError('unclosed block');
285
  }
286
 
287
  if ($this->charset) {
316
  $this->buffer = (string) $buffer;
317
 
318
  $this->saveEncoding();
319
+ $this->extractLineNumbers($this->buffer);
320
 
321
  $list = $this->valueList($out);
322
 
332
  *
333
  * @param string $buffer
334
  * @param string|array $out
335
+ * @param bool $shouldValidate
336
  *
337
  * @return boolean
338
  */
339
+ public function parseSelector($buffer, &$out, $shouldValidate = true)
340
  {
341
  $this->count = 0;
342
  $this->env = null;
345
  $this->buffer = (string) $buffer;
346
 
347
  $this->saveEncoding();
348
+ $this->extractLineNumbers($this->buffer);
349
+
350
+ // discard space/comments at the start
351
+ $this->discardComments = true;
352
+ $this->whitespace();
353
+ $this->discardComments = false;
354
 
355
  $selector = $this->selectors($out);
356
 
357
  $this->restoreEncoding();
358
 
359
+ if ($shouldValidate && $this->count !== strlen($buffer)) {
360
+ throw $this->parseError("`" . substr($buffer, $this->count) . "` is not a valid Selector in `$buffer`");
361
+ }
362
+
363
  return $selector;
364
  }
365
 
382
  $this->buffer = (string) $buffer;
383
 
384
  $this->saveEncoding();
385
+ $this->extractLineNumbers($this->buffer);
386
 
387
  $isMediaQuery = $this->mediaQueryList($out);
388
 
527
  ) {
528
  ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
529
 
530
+ list($line, $column) = $this->getSourcePosition($s);
531
+ $file = $this->sourceName;
532
+ $this->logger->warn("The \"@scssphp-import-once\" directive is deprecated and will be removed in ScssPhp 2.0, in \"$file\", line $line, column $column.", true);
533
+
534
  $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s);
535
 
536
  return true;
545
  $this->end()
546
  ) {
547
  if ($this->cssOnly) {
548
+ $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s);
549
  $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]);
550
  return true;
551
  }
563
  $this->end()
564
  ) {
565
  if ($this->cssOnly) {
566
+ $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s);
567
  $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]);
568
  return true;
569
  }
608
 
609
  $this->seek($s);
610
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
611
  if (
612
  $this->literal('@return', 7) &&
613
  ($this->valueList($retVal) || true) &&
782
  $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
783
  } elseif (
784
  $this->literal('if', 2) &&
785
+ $this->functionCallArgumentsList($cond, false, '{', false)
 
786
  ) {
787
  $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
788
  $else->cond = $cond;
882
  ! \in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA])
883
  ) {
884
  $plain = \trim(\substr($this->buffer, $s, $this->count - $s));
885
+ throw $this->parseError(
886
  "Unknown directive `{$plain}` not allowed in `" . $this->env->type . "` block"
887
  );
888
  }
990
 
991
  $this->seek($s);
992
 
 
 
 
 
 
993
  // opening css block
994
  if (
995
  $this->selectors($selectors) &&
1018
 
1019
  if ($this->valueList($value)) {
1020
  if (empty($this->env->parent)) {
1021
+ throw $this->parseError('expected "{"');
1022
  }
1023
 
1024
  $this->append([Type::T_ASSIGN, $name, $value], $s);
1077
  }
1078
 
1079
  // extra stuff
1080
+ if ($this->matchChar(';')) {
 
 
 
1081
  return true;
1082
  }
1083
 
1087
  /**
1088
  * Push block onto parse tree
1089
  *
1090
+ * @param array|null $selectors
1091
  * @param integer $pos
1092
  *
1093
+ * @return Block
1094
  */
1095
  protected function pushBlock($selectors, $pos = 0)
1096
  {
1136
  * @param string $type
1137
  * @param integer $pos
1138
  *
1139
+ * @return Block
1140
  */
1141
  protected function pushSpecialBlock($type, $pos)
1142
  {
1149
  /**
1150
  * Pop scope and return last block
1151
  *
1152
+ * @return Block
1153
  *
1154
  * @throws \Exception
1155
  */
1165
  $block = $this->env;
1166
 
1167
  if (empty($block->parent)) {
1168
+ throw $this->parseError('unexpected }');
1169
  }
1170
 
1171
  if ($block->type == Type::T_AT_ROOT) {
1196
  }
1197
 
1198
  $r = '/' . $regex . '/' . $this->patternModifiers;
1199
+ $result = preg_match($r, $this->buffer, $out, 0, $from);
1200
 
1201
  return $result;
1202
  }
1214
  /**
1215
  * Assert a parsed part is plain CSS Valid
1216
  *
1217
+ * @param array|false $parsed
1218
  * @param int $startPos
1219
  * @throws ParserException
1220
  */
1235
  if ($type) {
1236
  $message .= " ($type)";
1237
  }
1238
+ throw $this->parseError($message);
1239
  }
1240
 
1241
  return $parsed;
1248
  */
1249
  protected function isPlainCssValidElement($parsed, $allowExpression = false)
1250
  {
1251
+ // keep string as is
1252
+ if (is_string($parsed)) {
1253
+ return $parsed;
1254
+ }
1255
+
1256
  if (
1257
  \in_array($parsed[0], [Type::T_FUNCTION, Type::T_FUNCTION_CALL]) &&
1258
  !\in_array($parsed[1], [
1264
  'grayscale',
1265
  'hsl',
1266
  'hsla',
1267
+ 'hwb',
1268
  'invert',
1269
  'linear-gradient',
1270
  'min',
1288
  case Type::T_KEYWORD:
1289
  case Type::T_NULL:
1290
  case Type::T_NUMBER:
1291
+ case Type::T_MEDIA:
1292
  return $parsed;
1293
 
1294
  case Type::T_COMMENT:
1307
 
1308
  return $parsed;
1309
 
1310
+ case Type::T_IMPORT:
1311
+ if ($parsed[1][0] === Type::T_LIST) {
1312
+ return false;
1313
+ }
1314
+ $parsed[1] = $this->isPlainCssValidElement($parsed[1]);
1315
+ if ($parsed[1] === false) {
1316
+ return false;
1317
+ }
1318
+ return $parsed;
1319
+
1320
  case Type::T_STRING:
1321
  foreach ($parsed[2] as $k => $substr) {
1322
  if (\is_array($substr)) {
1328
  }
1329
  return $parsed;
1330
 
1331
+ case Type::T_LIST:
1332
+ if (!empty($parsed['enclosing'])) {
1333
+ return false;
1334
+ }
1335
+ foreach ($parsed[2] as $k => $listElement) {
1336
+ $parsed[2][$k] = $this->isPlainCssValidElement($listElement);
1337
+ if (! $parsed[2][$k]) {
1338
+ return false;
1339
+ }
1340
+ }
1341
+ return $parsed;
1342
+
1343
  case Type::T_ASSIGN:
1344
  foreach ([1, 2, 3] as $k) {
1345
  if (! empty($parsed[$k])) {
1376
  ]
1377
  ];
1378
 
1379
+ case Type::T_CUSTOM_PROPERTY:
1380
  case Type::T_UNARY:
1381
  $parsed[2] = $this->isPlainCssValidElement($parsed[2]);
1382
  if (! $parsed[2]) {
1469
  {
1470
  $r = '/' . $regex . '/' . $this->patternModifiers;
1471
 
1472
+ if (! preg_match($r, $this->buffer, $out, 0, $this->count)) {
1473
  return false;
1474
  }
1475
 
1550
  {
1551
  $gotWhite = false;
1552
 
1553
+ while (preg_match(static::$whitePattern, $this->buffer, $m, 0, $this->count)) {
1554
  if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
1555
  // comment that are kept in the output CSS
1556
  $comment = [];
1578
 
1579
  $comment[] = [Type::T_COMMENT, substr($this->buffer, $p, $this->count - $p), $out];
1580
  } else {
1581
+ list($line, $column) = $this->getSourcePosition($this->count);
1582
+ $file = $this->sourceName;
1583
+ $this->logger->warn("Unterminated interpolations in multiline comments are deprecated and will be removed in ScssPhp 2.0, in \"$file\", line $line, column $column.", true);
1584
  $comment[] = substr($this->buffer, $this->count, 2);
1585
 
1586
  $this->count += 2;
1598
  } else {
1599
  $comment[] = $c;
1600
  $staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount);
1601
+ $commentStatement = [Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]];
1602
+
1603
+ list($line, $column) = $this->getSourcePosition($startCommentCount);
1604
+ $commentStatement[self::SOURCE_LINE] = $line;
1605
+ $commentStatement[self::SOURCE_COLUMN] = $column;
1606
+ $commentStatement[self::SOURCE_INDEX] = $this->sourceIndex;
1607
+
1608
+ $this->appendComment($commentStatement);
1609
  }
1610
 
1611
  $this->commentsSeen[$startCommentCount] = true;
1640
  /**
1641
  * Append statement to current block
1642
  *
1643
+ * @param array|null $statement
1644
  * @param integer $pos
1645
  */
1646
  protected function append($statement, $pos = null)
1975
 
1976
  /**
1977
  * Check if a generic directive is known to be able to allow almost any syntax or not
1978
+ * @param mixed $directiveName
1979
  * @return bool
1980
  */
1981
  protected function isKnownGenericDirective($directiveName)
2153
  /**
2154
  * Parse generic list
2155
  *
2156
+ * @param array $out
2157
+ * @param string $parseItem The name of the method used to parse items
2158
+ * @param string $delim
2159
+ * @param boolean $flatten
2160
  *
2161
  * @return boolean
2162
  */
2845
  $sss = $this->count;
2846
 
2847
  if (! $this->matchChar(')')) {
2848
+ throw $this->parseError('... has to be after the final argument');
2849
  }
2850
 
2851
  $arg[2] = true;
3007
  $content[] = '#{'; // ignore it
3008
  }
3009
  } elseif ($m[2] === "\r") {
3010
+ $content[] = chr(10);
3011
  // TODO : warning
3012
  # DEPRECATION WARNING on line x, column y of zzz:
3013
  # Unescaped multiline strings are deprecated and will be removed in a future version of Sass.
3026
  } elseif ($this->matchEscapeCharacter($c)) {
3027
  $content[] = $c;
3028
  } else {
3029
+ throw $this->parseError('Unterminated escape sequence');
3030
  }
3031
  } else {
3032
  $this->count -= \strlen($delim);
3051
  return false;
3052
  }
3053
 
3054
+ /**
3055
+ * @param string $out
3056
+ * @param bool $inKeywords
3057
+ * @return bool
3058
+ */
3059
+ protected function matchEscapeCharacter(&$out, $inKeywords = false)
3060
  {
3061
+ $s = $this->count;
3062
  if ($this->match('[a-f0-9]', $m, false)) {
3063
  $hex = $m[0];
3064
 
3070
  }
3071
  }
3072
 
3073
+ // CSS allows Unicode escape sequences to be followed by a delimiter space
3074
+ // (necessary in some cases for shorter sequences to disambiguate their end)
3075
+ $this->matchChar(' ', false);
3076
+
3077
  $value = hexdec($hex);
3078
 
3079
+ if (!$inKeywords && ($value == 0 || ($value >= 0xD800 && $value <= 0xDFFF) || $value >= 0x10FFFF)) {
3080
+ $out = "\xEF\xBF\xBD"; // "\u{FFFD}" but with a syntax supported on PHP 5
3081
+ } elseif ($value < 0x20) {
3082
+ $out = Util::mbChr($value);
3083
  } else {
3084
  $out = Util::mbChr($value);
3085
  }
3088
  }
3089
 
3090
  if ($this->match('.', $m, false)) {
3091
+ if ($inKeywords && in_array($m[0], ["'",'"','@','&',' ','\\',':','/','%'])) {
3092
+ $this->seek($s);
3093
+ return false;
3094
+ }
3095
  $out = $m[0];
3096
 
3097
  return true;
3323
  }
3324
 
3325
  // match comment hack
3326
+ if (preg_match(static::$whitePattern, $this->buffer, $m, 0, $this->count)) {
3327
  if (! empty($m[0])) {
3328
  $parts[] = $m[0];
3329
  $this->count += \strlen($m[0]);
3399
  /**
3400
  * Parse comma separated selector list
3401
  *
3402
+ * @param array $out
3403
+ * @param string|boolean $subSelector
3404
  *
3405
  * @return boolean
3406
  */
3435
  /**
3436
  * Parse whitespace separated selector list
3437
  *
3438
+ * @param array $out
3439
+ * @param string|boolean $subSelector
3440
  *
3441
  * @return boolean
3442
  */
3444
  {
3445
  $selector = [];
3446
 
3447
+ $discardComments = $this->discardComments;
3448
+ $this->discardComments = true;
3449
+
3450
  for (;;) {
3451
  $s = $this->count;
3452
 
3464
 
3465
  if ($this->selectorSingle($part, $subSelector)) {
3466
  $selector[] = $part;
3467
+ $this->whitespace();
 
 
 
 
 
3468
  continue;
3469
  }
3470
 
3471
  break;
3472
  }
3473
 
3474
+ $this->discardComments = $discardComments;
3475
+
3476
  if (! $selector) {
3477
  return false;
3478
  }
3482
  return true;
3483
  }
3484
 
3485
+ /**
3486
+ * parsing escaped chars in selectors:
3487
+ * - escaped single chars are kept escaped in the selector but in a normalized form
3488
+ * (if not in 0-9a-f range as this would be ambigous)
3489
+ * - other escaped sequences (multibyte chars or 0-9a-f) are kept in their initial escaped form,
3490
+ * normalized to lowercase
3491
+ *
3492
+ * TODO: this is a fallback solution. Ideally escaped chars in selectors should be encoded as the genuine chars,
3493
+ * and escaping added when printing in the Compiler, where/if it's mandatory
3494
+ * - but this require a better formal selector representation instead of the array we have now
3495
+ *
3496
+ * @param string $out
3497
+ * @param bool $keepEscapedNumber
3498
+ * @return bool
3499
+ */
3500
+ protected function matchEscapeCharacterInSelector(&$out, $keepEscapedNumber = false)
3501
+ {
3502
+ $s_escape = $this->count;
3503
+ if ($this->match('\\\\', $m)) {
3504
+ $out = '\\' . $m[0];
3505
+ return true;
3506
+ }
3507
+
3508
+ if ($this->matchEscapeCharacter($escapedout, true)) {
3509
+ if (strlen($escapedout) === 1) {
3510
+ if (!preg_match(",\w,", $escapedout)) {
3511
+ $out = '\\' . $escapedout;
3512
+ return true;
3513
+ } elseif (! $keepEscapedNumber || ! \is_numeric($escapedout)) {
3514
+ $out = $escapedout;
3515
+ return true;
3516
+ }
3517
+ }
3518
+ $escape_sequence = rtrim(substr($this->buffer, $s_escape, $this->count - $s_escape));
3519
+ if (strlen($escape_sequence) < 6) {
3520
+ $escape_sequence .= ' ';
3521
+ }
3522
+ $out = '\\' . strtolower($escape_sequence);
3523
+ return true;
3524
+ }
3525
+ if ($this->match('\\S', $m)) {
3526
+ $out = '\\' . $m[0];
3527
+ return true;
3528
+ }
3529
+
3530
+
3531
+ return false;
3532
+ }
3533
+
3534
  /**
3535
  * Parse the parts that make up a selector
3536
  *
3538
  * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
3539
  * }}
3540
  *
3541
+ * @param array $out
3542
+ * @param string|boolean $subSelector
3543
  *
3544
  * @return boolean
3545
  */
3591
  continue 2;
3592
  }
3593
 
3594
+ // handling of escaping in selectors : get the escaped char
3595
+ if ($char === '\\') {
3596
+ $this->count++;
3597
+ if ($this->matchEscapeCharacterInSelector($escaped, true)) {
3598
+ $parts[] = $escaped;
3599
+ continue;
3600
+ }
3601
+ $this->count--;
3602
  }
3603
 
3604
  if ($char === '%') {
3684
  $this->seek($ss);
3685
  }
3686
  } elseif (
3687
+ $this->matchChar('(', true) &&
3688
  ($this->openString(')', $str, '(') || true) &&
3689
  $this->matchChar(')')
3690
  ) {
3741
  continue;
3742
  }
3743
 
3744
+ if ($this->restrictedKeyword($name, false, true)) {
3745
  $parts[] = $name;
3746
  continue;
3747
  }
3794
  *
3795
  * @param string $word
3796
  * @param boolean $eatWhitespace
3797
+ * @param boolean $inSelector
3798
  *
3799
  * @return boolean
3800
  */
3801
+ protected function keyword(&$word, $eatWhitespace = null, $inSelector = false)
3802
  {
3803
+ $s = $this->count;
3804
  $match = $this->match(
3805
  $this->utf8
3806
+ ? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)'
3807
+ : '(([\w_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\w\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)',
3808
  $m,
3809
+ false
3810
  );
3811
 
3812
  if ($match) {
3813
  $word = $m[1];
3814
 
3815
+ // handling of escaping in keyword : get the escaped char
3816
+ if (strpos($word, '\\') !== false) {
3817
+ $send = $this->count;
3818
+ $escapedWord = [];
3819
+ $this->seek($s);
3820
+ $previousEscape = false;
3821
+ while ($this->count < $send) {
3822
+ $char = $this->buffer[$this->count];
3823
+ $this->count++;
3824
+ if (
3825
+ $this->count < $send
3826
+ && $char === '\\'
3827
+ && !$previousEscape
3828
+ && (
3829
+ $inSelector ?
3830
+ $this->matchEscapeCharacterInSelector($out)
3831
+ :
3832
+ $this->matchEscapeCharacter($out, true)
3833
+ )
3834
+ ) {
3835
+ $escapedWord[] = $out;
3836
+ } else {
3837
+ if ($previousEscape) {
3838
+ $previousEscape = false;
3839
+ } elseif ($char === '\\') {
3840
+ $previousEscape = true;
3841
+ }
3842
+ $escapedWord[] = $char;
3843
+ }
3844
+ }
3845
+
3846
+ $word = implode('', $escapedWord);
3847
+ }
3848
+
3849
+ if (is_null($eatWhitespace) ? $this->eatWhiteDefault : $eatWhitespace) {
3850
+ $this->whitespace();
3851
+ }
3852
+
3853
  return true;
3854
  }
3855
 
3861
  *
3862
  * @param string $word
3863
  * @param boolean $eatWhitespace
3864
+ * @param boolean $inSelector
3865
  *
3866
  * @return boolean
3867
  */
3868
+ protected function restrictedKeyword(&$word, $eatWhitespace = null, $inSelector = false)
3869
  {
3870
  $s = $this->count;
3871
 
3872
+ if ($this->keyword($word, $eatWhitespace, $inSelector) && (\ord($word[0]) > 57 || \ord($word[0]) < 48)) {
3873
  return true;
3874
  }
3875
 
4094
  }
4095
 
4096
  /**
4097
+ * Save internal encoding of mbstring
4098
+ *
4099
+ * When mbstring.func_overload is used to replace the standard PHP string functions,
4100
+ * this method configures the internal encoding to a single-byte one so that the
4101
+ * behavior matches the normal behavior of PHP string functions while using the parser.
4102
+ * The existing internal encoding is saved and will be restored when calling {@see restoreEncoding}.
4103
+ *
4104
+ * If mbstring.func_overload is not used (or does not override string functions), this method is a no-op.
4105
+ *
4106
+ * @return void
4107
  */
4108
  private function saveEncoding()
4109
  {
4110
+ if (\PHP_VERSION_ID < 80000 && \extension_loaded('mbstring') && (2 & (int) ini_get('mbstring.func_overload')) > 0) {
4111
  $this->encoding = mb_internal_encoding();
4112
 
4113
  mb_internal_encoding('iso-8859-1');
4116
 
4117
  /**
4118
  * Restore internal encoding
4119
+ *
4120
+ * @return void
4121
  */
4122
  private function restoreEncoding()
4123
  {
scssphp/src/SourceMap/Base64.php CHANGED
@@ -16,11 +16,13 @@ namespace ScssPhp\ScssPhp\SourceMap;
16
  * Base 64 Encode/Decode
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
 
 
19
  */
20
  class Base64
21
  {
22
  /**
23
- * @var array
24
  */
25
  private static $encodingMap = [
26
  0 => 'A',
@@ -90,7 +92,7 @@ class Base64
90
  ];
91
 
92
  /**
93
- * @var array
94
  */
95
  private static $decodingMap = [
96
  'A' => 0,
16
  * Base 64 Encode/Decode
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
19
+ *
20
+ * @internal
21
  */
22
  class Base64
23
  {
24
  /**
25
+ * @var array<int, string>
26
  */
27
  private static $encodingMap = [
28
  0 => 'A',
92
  ];
93
 
94
  /**
95
+ * @var array<string|int, int>
96
  */
97
  private static $decodingMap = [
98
  'A' => 0,
scssphp/src/SourceMap/Base64VLQ.php CHANGED
@@ -12,8 +12,6 @@
12
 
13
  namespace ScssPhp\ScssPhp\SourceMap;
14
 
15
- use ScssPhp\ScssPhp\SourceMap\Base64;
16
-
17
  /**
18
  * Base 64 VLQ
19
  *
@@ -36,6 +34,8 @@ use ScssPhp\ScssPhp\SourceMap\Base64;
36
  *
37
  * @author John Lenz <johnlenz@google.com>
38
  * @author Anthon Pang <anthon.pang@gmail.com>
 
 
39
  */
40
  class Base64VLQ
41
  {
12
 
13
  namespace ScssPhp\ScssPhp\SourceMap;
14
 
 
 
15
  /**
16
  * Base 64 VLQ
17
  *
34
  *
35
  * @author John Lenz <johnlenz@google.com>
36
  * @author Anthon Pang <anthon.pang@gmail.com>
37
+ *
38
+ * @internal
39
  */
40
  class Base64VLQ
41
  {
scssphp/src/SourceMap/Base64VLQEncoder.php DELETED
@@ -1,217 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2019 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://scssphp.github.io/scssphp
10
- */
11
-
12
- namespace ScssPhp\ScssPhp\SourceMap;
13
-
14
- /**
15
- * Base64 VLQ Encoder
16
- *
17
- * {@internal Derivative of oyejorge/less.php's lib/SourceMap/Base64VLQ.php, relicensed with permission. }}
18
- *
19
- * @author Josh Schmidt <oyejorge@gmail.com>
20
- * @author Nicolas FRANÇOIS <nicolas.francois@frog-labs.com>
21
- */
22
- class Base64VLQEncoder
23
- {
24
- /**
25
- * Shift
26
- *
27
- * @var integer
28
- */
29
- private $shift = 5;
30
-
31
- /**
32
- * Mask
33
- *
34
- * @var integer
35
- */
36
- private $mask = 0x1F; // == (1 << shift) == 0b00011111
37
-
38
- /**
39
- * Continuation bit
40
- *
41
- * @var integer
42
- */
43
- private $continuationBit = 0x20; // == (mask - 1 ) == 0b00100000
44
-
45
- /**
46
- * Char to integer map
47
- *
48
- * @var array
49
- */
50
- private $charToIntMap = [
51
- 'A' => 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6, 'H' => 7,
52
- 'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13, 'O' => 14, 'P' => 15,
53
- 'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20, 'V' => 21, 'W' => 22, 'X' => 23,
54
- 'Y' => 24, 'Z' => 25, 'a' => 26, 'b' => 27, 'c' => 28, 'd' => 29, 'e' => 30, 'f' => 31,
55
- 'g' => 32, 'h' => 33, 'i' => 34, 'j' => 35, 'k' => 36, 'l' => 37, 'm' => 38, 'n' => 39,
56
- 'o' => 40, 'p' => 41, 'q' => 42, 'r' => 43, 's' => 44, 't' => 45, 'u' => 46, 'v' => 47,
57
- 'w' => 48, 'x' => 49, 'y' => 50, 'z' => 51, 0 => 52, 1 => 53, 2 => 54, 3 => 55,
58
- 4 => 56, 5 => 57, 6 => 58, 7 => 59, 8 => 60, 9 => 61, '+' => 62, '/' => 63,
59
- ];
60
-
61
- /**
62
- * Integer to char map
63
- *
64
- * @var array
65
- */
66
- private $intToCharMap = [
67
- 0 => 'A', 1 => 'B', 2 => 'C', 3 => 'D', 4 => 'E', 5 => 'F', 6 => 'G', 7 => 'H',
68
- 8 => 'I', 9 => 'J', 10 => 'K', 11 => 'L', 12 => 'M', 13 => 'N', 14 => 'O', 15 => 'P',
69
- 16 => 'Q', 17 => 'R', 18 => 'S', 19 => 'T', 20 => 'U', 21 => 'V', 22 => 'W', 23 => 'X',
70
- 24 => 'Y', 25 => 'Z', 26 => 'a', 27 => 'b', 28 => 'c', 29 => 'd', 30 => 'e', 31 => 'f',
71
- 32 => 'g', 33 => 'h', 34 => 'i', 35 => 'j', 36 => 'k', 37 => 'l', 38 => 'm', 39 => 'n',
72
- 40 => 'o', 41 => 'p', 42 => 'q', 43 => 'r', 44 => 's', 45 => 't', 46 => 'u', 47 => 'v',
73
- 48 => 'w', 49 => 'x', 50 => 'y', 51 => 'z', 52 => '0', 53 => '1', 54 => '2', 55 => '3',
74
- 56 => '4', 57 => '5', 58 => '6', 59 => '7', 60 => '8', 61 => '9', 62 => '+', 63 => '/',
75
- ];
76
-
77
- /**
78
- * Constructor
79
- */
80
- public function __construct()
81
- {
82
- // I leave it here for future reference
83
- // foreach (str_split('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/') as $i => $char)
84
- // {
85
- // $this->charToIntMap[$char] = $i;
86
- // $this->intToCharMap[$i] = $char;
87
- // }
88
- }
89
-
90
- /**
91
- * Convert from a two-complement value to a value where the sign bit is
92
- * is placed in the least significant bit. For example, as decimals:
93
- * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
94
- * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
95
- * We generate the value for 32 bit machines, hence -2147483648 becomes 1, not 4294967297,
96
- * even on a 64 bit machine.
97
- *
98
- * @param string $aValue
99
- */
100
- public function toVLQSigned($aValue)
101
- {
102
- return 0xffffffff & ($aValue < 0 ? ((-$aValue) << 1) + 1 : ($aValue << 1) + 0);
103
- }
104
-
105
- /**
106
- * Convert to a two-complement value from a value where the sign bit is
107
- * is placed in the least significant bit. For example, as decimals:
108
- * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1
109
- * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2
110
- * We assume that the value was generated with a 32 bit machine in mind.
111
- * Hence
112
- * 1 becomes -2147483648
113
- * even on a 64 bit machine.
114
- *
115
- * @param integer $aValue
116
- */
117
- public function fromVLQSigned($aValue)
118
- {
119
- return $aValue & 1 ? $this->zeroFill(~$aValue + 2, 1) | (-1 - 0x7fffffff) : $this->zeroFill($aValue, 1);
120
- }
121
-
122
- /**
123
- * Return the base 64 VLQ encoded value.
124
- *
125
- * @param string $aValue The value to encode
126
- *
127
- * @return string The encoded value
128
- */
129
- public function encode($aValue)
130
- {
131
- $encoded = '';
132
- $vlq = $this->toVLQSigned($aValue);
133
-
134
- do {
135
- $digit = $vlq & $this->mask;
136
- $vlq = $this->zeroFill($vlq, $this->shift);
137
-
138
- if ($vlq > 0) {
139
- $digit |= $this->continuationBit;
140
- }
141
-
142
- $encoded .= $this->base64Encode($digit);
143
- } while ($vlq > 0);
144
-
145
- return $encoded;
146
- }
147
-
148
- /**
149
- * Return the value decoded from base 64 VLQ.
150
- *
151
- * @param string $encoded The encoded value to decode
152
- *
153
- * @return integer The decoded value
154
- */
155
- public function decode($encoded)
156
- {
157
- $vlq = 0;
158
- $i = 0;
159
-
160
- do {
161
- $digit = $this->base64Decode($encoded[$i]);
162
- $vlq |= ($digit & $this->mask) << ($i * $this->shift);
163
- $i++;
164
- } while ($digit & $this->continuationBit);
165
-
166
- return $this->fromVLQSigned($vlq);
167
- }
168
-
169
- /**
170
- * Right shift with zero fill.
171
- *
172
- * @param integer $a number to shift
173
- * @param integer $b number of bits to shift
174
- *
175
- * @return integer
176
- */
177
- public function zeroFill($a, $b)
178
- {
179
- return ($a >= 0) ? ($a >> $b) : ($a >> $b) & (PHP_INT_MAX >> ($b - 1));
180
- }
181
-
182
- /**
183
- * Encode single 6-bit digit as base64.
184
- *
185
- * @param integer $number
186
- *
187
- * @return string
188
- *
189
- * @throws \Exception If the number is invalid
190
- */
191
- public function base64Encode($number)
192
- {
193
- if ($number < 0 || $number > 63) {
194
- throw new \Exception(sprintf('Invalid number "%s" given. Must be between 0 and 63.', $number));
195
- }
196
-
197
- return $this->intToCharMap[$number];
198
- }
199
-
200
- /**
201
- * Decode single 6-bit digit from base64
202
- *
203
- * @param string $char
204
- *
205
- * @return integer
206
- *
207
- * @throws \Exception If the number is invalid
208
- */
209
- public function base64Decode($char)
210
- {
211
- if (! array_key_exists($char, $this->charToIntMap)) {
212
- throw new \Exception(sprintf('Invalid base 64 digit "%s" given.', $char));
213
- }
214
-
215
- return $this->charToIntMap[$char];
216
- }
217
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scssphp/src/SourceMap/SourceMapGenerator.php CHANGED
@@ -21,6 +21,8 @@ use ScssPhp\ScssPhp\Exception\CompilerException;
21
  *
22
  * @author Josh Schmidt <oyejorge@gmail.com>
23
  * @author Nicolas FRANÇOIS <nicolas.francois@frog-labs.com>
 
 
24
  */
25
  class SourceMapGenerator
26
  {
@@ -33,6 +35,7 @@ class SourceMapGenerator
33
  * Array of default options
34
  *
35
  * @var array
 
36
  */
37
  protected $defaultOptions = [
38
  // an optional source root, useful for relocating source files
@@ -70,6 +73,7 @@ class SourceMapGenerator
70
  * Array of mappings
71
  *
72
  * @var array
 
73
  */
74
  protected $mappings = [];
75
 
@@ -83,16 +87,24 @@ class SourceMapGenerator
83
  /**
84
  * File to content map
85
  *
86
- * @var array
87
  */
88
  protected $sources = [];
 
 
 
 
89
  protected $sourceKeys = [];
90
 
91
  /**
92
  * @var array
 
93
  */
94
  private $options;
95
 
 
 
 
96
  public function __construct(array $options = [])
97
  {
98
  $this->options = array_merge($this->defaultOptions, $options);
@@ -107,6 +119,8 @@ class SourceMapGenerator
107
  * @param integer $originalLine The line number in original file
108
  * @param integer $originalColumn The column number in original file
109
  * @param string $sourceFile The original source file
 
 
110
  */
111
  public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $sourceFile)
112
  {
@@ -129,6 +143,7 @@ class SourceMapGenerator
129
  * @return string
130
  *
131
  * @throws \ScssPhp\ScssPhp\Exception\CompilerException If the file could not be saved
 
132
  */
133
  public function saveMap($content)
134
  {
@@ -154,14 +169,16 @@ class SourceMapGenerator
154
  /**
155
  * Generates the JSON source map
156
  *
 
 
157
  * @return string
158
  *
159
  * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#
160
  */
161
- public function generateJson()
162
  {
163
  $sourceMap = [];
164
- $mappings = $this->generateMappings();
165
 
166
  // File version (always the first entry in the object) and must be a positive integer.
167
  $sourceMap['version'] = self::VERSION;
@@ -212,7 +229,7 @@ class SourceMapGenerator
212
  /**
213
  * Returns the sources contents
214
  *
215
- * @return array|null
216
  */
217
  protected function getSourcesContent()
218
  {
@@ -232,14 +249,21 @@ class SourceMapGenerator
232
  /**
233
  * Generates the mappings string
234
  *
 
 
235
  * @return string
236
  */
237
- public function generateMappings()
238
  {
239
  if (! \count($this->mappings)) {
240
  return '';
241
  }
242
 
 
 
 
 
 
243
  $this->sourceKeys = array_flip(array_keys($this->sources));
244
 
245
  // group mappings by generated line number.
@@ -254,6 +278,12 @@ class SourceMapGenerator
254
  $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
255
 
256
  foreach ($groupedMap as $lineNumber => $lineMap) {
 
 
 
 
 
 
257
  while (++$lastGeneratedLine < $lineNumber) {
258
  $groupedMapEncoded[] = ';';
259
  }
@@ -262,8 +292,10 @@ class SourceMapGenerator
262
  $lastGeneratedColumn = 0;
263
 
264
  foreach ($lineMap as $m) {
265
- $mapEncoded = $this->encoder->encode($m['generated_column'] - $lastGeneratedColumn);
266
- $lastGeneratedColumn = $m['generated_column'];
 
 
267
 
268
  // find the index
269
  if ($m['source_file']) {
21
  *
22
  * @author Josh Schmidt <oyejorge@gmail.com>
23
  * @author Nicolas FRANÇOIS <nicolas.francois@frog-labs.com>
24
+ *
25
+ * @internal
26
  */
27
  class SourceMapGenerator
28
  {
35
  * Array of default options
36
  *
37
  * @var array
38
+ * @phpstan-var array{sourceRoot: string, sourceMapFilename: string|null, sourceMapURL: string|null, sourceMapWriteTo: string|null, outputSourceFiles: bool, sourceMapRootpath: string, sourceMapBasepath: string}
39
  */
40
  protected $defaultOptions = [
41
  // an optional source root, useful for relocating source files
73
  * Array of mappings
74
  *
75
  * @var array
76
+ * @phpstan-var list<array{generated_line: int, generated_column: int, original_line: int, original_column: int, source_file: string}>
77
  */
78
  protected $mappings = [];
79
 
87
  /**
88
  * File to content map
89
  *
90
+ * @var array<string, string>
91
  */
92
  protected $sources = [];
93
+
94
+ /**
95
+ * @var array<string, int>
96
+ */
97
  protected $sourceKeys = [];
98
 
99
  /**
100
  * @var array
101
+ * @phpstan-var array{sourceRoot: string, sourceMapFilename: string|null, sourceMapURL: string|null, sourceMapWriteTo: string|null, outputSourceFiles: bool, sourceMapRootpath: string, sourceMapBasepath: string}
102
  */
103
  private $options;
104
 
105
+ /**
106
+ * @phpstan-param array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} $options
107
+ */
108
  public function __construct(array $options = [])
109
  {
110
  $this->options = array_merge($this->defaultOptions, $options);
119
  * @param integer $originalLine The line number in original file
120
  * @param integer $originalColumn The column number in original file
121
  * @param string $sourceFile The original source file
122
+ *
123
+ * @return void
124
  */
125
  public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $sourceFile)
126
  {
143
  * @return string
144
  *
145
  * @throws \ScssPhp\ScssPhp\Exception\CompilerException If the file could not be saved
146
+ * @deprecated
147
  */
148
  public function saveMap($content)
149
  {
169
  /**
170
  * Generates the JSON source map
171
  *
172
+ * @param string $prefix A prefix added in the output file, which needs to shift mappings
173
+ *
174
  * @return string
175
  *
176
  * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#
177
  */
178
+ public function generateJson($prefix = '')
179
  {
180
  $sourceMap = [];
181
+ $mappings = $this->generateMappings($prefix);
182
 
183
  // File version (always the first entry in the object) and must be a positive integer.
184
  $sourceMap['version'] = self::VERSION;
229
  /**
230
  * Returns the sources contents
231
  *
232
+ * @return string[]|null
233
  */
234
  protected function getSourcesContent()
235
  {
249
  /**
250
  * Generates the mappings string
251
  *
252
+ * @param string $prefix A prefix added in the output file, which needs to shift mappings
253
+ *
254
  * @return string
255
  */
256
+ public function generateMappings($prefix = '')
257
  {
258
  if (! \count($this->mappings)) {
259
  return '';
260
  }
261
 
262
+ $prefixLines = substr_count($prefix, "\n");
263
+ $lastPrefixNewLine = strrpos($prefix, "\n");
264
+ $lastPrefixLineStart = false === $lastPrefixNewLine ? 0 : $lastPrefixNewLine + 1;
265
+ $prefixColumn = strlen($prefix) - $lastPrefixLineStart;
266
+
267
  $this->sourceKeys = array_flip(array_keys($this->sources));
268
 
269
  // group mappings by generated line number.
278
  $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
279
 
280
  foreach ($groupedMap as $lineNumber => $lineMap) {
281
+ if ($lineNumber > 1) {
282
+ // The prefix only impacts the column for the first line of the original output
283
+ $prefixColumn = 0;
284
+ }
285
+ $lineNumber += $prefixLines;
286
+
287
  while (++$lastGeneratedLine < $lineNumber) {
288
  $groupedMapEncoded[] = ';';
289
  }
292
  $lastGeneratedColumn = 0;
293
 
294
  foreach ($lineMap as $m) {
295
+ $generatedColumn = $m['generated_column'] + $prefixColumn;
296
+
297
+ $mapEncoded = $this->encoder->encode($generatedColumn - $lastGeneratedColumn);
298
+ $lastGeneratedColumn = $generatedColumn;
299
 
300
  // find the index
301
  if ($m['source_file']) {
scssphp/src/Type.php CHANGED
@@ -22,11 +22,14 @@ class Type
22
  const T_ASSIGN = 'assign';
23
  const T_AT_ROOT = 'at-root';
24
  const T_BLOCK = 'block';
 
25
  const T_BREAK = 'break';
26
  const T_CHARSET = 'charset';
27
  const T_COLOR = 'color';
28
  const T_COMMENT = 'comment';
 
29
  const T_CONTINUE = 'continue';
 
30
  const T_CONTROL = 'control';
31
  const T_CUSTOM_PROPERTY = 'custom';
32
  const T_DEBUG = 'debug';
@@ -42,6 +45,7 @@ class Type
42
  const T_FUNCTION_REFERENCE = 'function-reference';
43
  const T_FUNCTION_CALL = 'fncall';
44
  const T_HSL = 'hsl';
 
45
  const T_IF = 'if';
46
  const T_IMPORT = 'import';
47
  const T_INCLUDE = 'include';
22
  const T_ASSIGN = 'assign';
23
  const T_AT_ROOT = 'at-root';
24
  const T_BLOCK = 'block';
25
+ /** @deprecated */
26
  const T_BREAK = 'break';
27
  const T_CHARSET = 'charset';
28
  const T_COLOR = 'color';
29
  const T_COMMENT = 'comment';
30
+ /** @deprecated */
31
  const T_CONTINUE = 'continue';
32
+ /** @deprecated */
33
  const T_CONTROL = 'control';
34
  const T_CUSTOM_PROPERTY = 'custom';
35
  const T_DEBUG = 'debug';
45
  const T_FUNCTION_REFERENCE = 'function-reference';
46
  const T_FUNCTION_CALL = 'fncall';
47
  const T_HSL = 'hsl';
48
+ const T_HWB = 'hwb';
49
  const T_IF = 'if';
50
  const T_IMPORT = 'import';
51
  const T_INCLUDE = 'include';
scssphp/src/Util.php CHANGED
@@ -14,11 +14,14 @@ namespace ScssPhp\ScssPhp;
14
 
15
  use ScssPhp\ScssPhp\Base\Range;
16
  use ScssPhp\ScssPhp\Exception\RangeException;
 
17
 
18
  /**
19
- * Utilty functions
20
  *
21
  * @author Anthon Pang <anthon.pang@gmail.com>
 
 
22
  */
23
  class Util
24
  {
@@ -26,10 +29,10 @@ class Util
26
  * Asserts that `value` falls within `range` (inclusive), leaving
27
  * room for slight floating-point errors.
28
  *
29
- * @param string $name The name of the value. Used in the error message.
30
- * @param \ScssPhp\ScssPhp\Base\Range $range Range of values.
31
- * @param array $value The value to check.
32
- * @param string $unit The unit of the value. Used in error reporting.
33
  *
34
  * @return mixed `value` adjusted to fall within range, if it was outside by a floating-point margin.
35
  *
@@ -82,8 +85,8 @@ class Util
82
  */
83
  public static function mbChr($code)
84
  {
85
- // Use the native implementation if available.
86
- if (\function_exists('mb_chr')) {
87
  return mb_chr($code, 'UTF-8');
88
  }
89
 
@@ -105,7 +108,7 @@ class Util
105
  * mb_strlen() wrapper
106
  *
107
  * @param string $string
108
- * @return false|int
109
  */
110
  public static function mbStrlen($string)
111
  {
@@ -115,10 +118,10 @@ class Util
115
  }
116
 
117
  if (\function_exists('iconv_strlen')) {
118
- return @iconv_strlen($string, 'UTF-8');
119
  }
120
 
121
- return strlen($string);
122
  }
123
 
124
  /**
@@ -155,6 +158,27 @@ class Util
155
  return (string)iconv_substr($string, $start, $length, 'UTF-8');
156
  }
157
 
158
- return substr($string, $start, $length);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  }
160
  }
14
 
15
  use ScssPhp\ScssPhp\Base\Range;
16
  use ScssPhp\ScssPhp\Exception\RangeException;
17
+ use ScssPhp\ScssPhp\Node\Number;
18
 
19
  /**
20
+ * Utility functions
21
  *
22
  * @author Anthon Pang <anthon.pang@gmail.com>
23
+ *
24
+ * @internal
25
  */
26
  class Util
27
  {
29
  * Asserts that `value` falls within `range` (inclusive), leaving
30
  * room for slight floating-point errors.
31
  *
32
+ * @param string $name The name of the value. Used in the error message.
33
+ * @param Range $range Range of values.
34
+ * @param array|Number $value The value to check.
35
+ * @param string $unit The unit of the value. Used in error reporting.
36
  *
37
  * @return mixed `value` adjusted to fall within range, if it was outside by a floating-point margin.
38
  *
85
  */
86
  public static function mbChr($code)
87
  {
88
+ // Use the native implementation if available, but not on PHP 7.2 as mb_chr(0) is buggy there
89
+ if (\PHP_VERSION_ID > 70300 && \function_exists('mb_chr')) {
90
  return mb_chr($code, 'UTF-8');
91
  }
92
 
108
  * mb_strlen() wrapper
109
  *
110
  * @param string $string
111
+ * @return int
112
  */
113
  public static function mbStrlen($string)
114
  {
118
  }
119
 
120
  if (\function_exists('iconv_strlen')) {
121
+ return (int) @iconv_strlen($string, 'UTF-8');
122
  }
123
 
124
+ throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.');
125
  }
126
 
127
  /**
158
  return (string)iconv_substr($string, $start, $length, 'UTF-8');
159
  }
160
 
161
+ throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.');
162
+ }
163
+
164
+ /**
165
+ * mb_strpos wrapper
166
+ * @param string $haystack
167
+ * @param string $needle
168
+ * @param int $offset
169
+ *
170
+ * @return int|false
171
+ */
172
+ public static function mbStrpos($haystack, $needle, $offset = 0)
173
+ {
174
+ if (\function_exists('mb_strpos')) {
175
+ return mb_strpos($haystack, $needle, $offset, 'UTF-8');
176
+ }
177
+
178
+ if (\function_exists('iconv_strpos')) {
179
+ return iconv_strpos($haystack, $needle, $offset, 'UTF-8');
180
+ }
181
+
182
+ throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.');
183
  }
184
  }
scssphp/src/Util/Path.php ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace ScssPhp\ScssPhp\Util;
4
+
5
+ /**
6
+ * @internal
7
+ */
8
+ class Path
9
+ {
10
+ /**
11
+ * @param string $path
12
+ *
13
+ * @return bool
14
+ */
15
+ public static function isAbsolute($path)
16
+ {
17
+ if ($path === '') {
18
+ return false;
19
+ }
20
+
21
+ if ($path[0] === '/') {
22
+ return true;
23
+ }
24
+
25
+ if (\DIRECTORY_SEPARATOR !== '\\') {
26
+ return false;
27
+ }
28
+
29
+ if ($path[0] === '\\') {
30
+ return true;
31
+ }
32
+
33
+ if (\strlen($path) < 3) {
34
+ return false;
35
+ }
36
+
37
+ if ($path[1] !== ':') {
38
+ return false;
39
+ }
40
+
41
+ if ($path[2] !== '/' && $path[2] !== '\\') {
42
+ return false;
43
+ }
44
+
45
+ if (!preg_match('/^[A-Za-z]$/', $path[0])) {
46
+ return false;
47
+ }
48
+
49
+ return true;
50
+ }
51
+
52
+ /**
53
+ * @param string $part1
54
+ * @param string $part2
55
+ *
56
+ * @return string
57
+ */
58
+ public static function join($part1, $part2)
59
+ {
60
+ if ($part1 === '' || self::isAbsolute($part2)) {
61
+ return $part2;
62
+ }
63
+
64
+ if ($part2 === '') {
65
+ return $part1;
66
+ }
67
+
68
+ $last = $part1[\strlen($part1) - 1];
69
+ $separator = \DIRECTORY_SEPARATOR;
70
+
71
+ if ($last === '/' || $last === \DIRECTORY_SEPARATOR) {
72
+ $separator = '';
73
+ }
74
+
75
+ return $part1 . $separator . $part2;
76
+ }
77
+ }
scssphp/src/ValueConverter.php ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * SCSSPHP
5
+ *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
+ *
8
+ * @license http://opensource.org/licenses/MIT MIT
9
+ *
10
+ * @link http://scssphp.github.io/scssphp
11
+ */
12
+
13
+ namespace ScssPhp\ScssPhp;
14
+
15
+ use ScssPhp\ScssPhp\Node\Number;
16
+
17
+ final class ValueConverter
18
+ {
19
+ // Prevent instantiating it
20
+ private function __construct()
21
+ {
22
+ }
23
+
24
+ /**
25
+ * Parses a value from a Scss source string.
26
+ *
27
+ * The returned value is guaranteed to be supported by the
28
+ * Compiler methods for registering custom variables. No other
29
+ * guarantee about it is provided. It should be considered
30
+ * opaque values by the caller.
31
+ *
32
+ * @param string $source
33
+ *
34
+ * @return mixed
35
+ */
36
+ public static function parseValue($source)
37
+ {
38
+ $parser = new Parser(__CLASS__);
39
+
40
+ if (!$parser->parseValue($source, $value)) {
41
+ throw new \InvalidArgumentException(sprintf('Invalid value source "%s".', $source));
42
+ }
43
+
44
+ return $value;
45
+ }
46
+
47
+ /**
48
+ * Converts a PHP value to a Sass value
49
+ *
50
+ * The returned value is guaranteed to be supported by the
51
+ * Compiler methods for registering custom variables. No other
52
+ * guarantee about it is provided. It should be considered
53
+ * opaque values by the caller.
54
+ *
55
+ * @param mixed $value
56
+ *
57
+ * @return mixed
58
+ */
59
+ public static function fromPhp($value)
60
+ {
61
+ if ($value instanceof Number) {
62
+ return $value;
63
+ }
64
+
65
+ if (is_array($value) && isset($value[0]) && \in_array($value[0], [Type::T_NULL, Type::T_COLOR, Type::T_KEYWORD, Type::T_LIST, Type::T_MAP, Type::T_STRING])) {
66
+ return $value;
67
+ }
68
+
69
+ if ($value === null) {
70
+ return Compiler::$null;
71
+ }
72
+
73
+ if ($value === true) {
74
+ return Compiler::$true;
75
+ }
76
+
77
+ if ($value === false) {
78
+ return Compiler::$false;
79
+ }
80
+
81
+ if ($value === '') {
82
+ return Compiler::$emptyString;
83
+ }
84
+
85
+ if (\is_int($value) || \is_float($value)) {
86
+ return new Number($value, '');
87
+ }
88
+
89
+ if (\is_string($value)) {
90
+ return [Type::T_STRING, '"', [$value]];
91
+ }
92
+
93
+ throw new \InvalidArgumentException(sprintf('Cannot convert the value of type "%s" to a Sass value.', gettype($value)));
94
+ }
95
+ }
scssphp/src/Version.php CHANGED
@@ -19,5 +19,5 @@ namespace ScssPhp\ScssPhp;
19
  */
20
  class Version
21
  {
22
- const VERSION = '1.2.1';
23
  }
19
  */
20
  class Version
21
  {
22
+ const VERSION = '1.5.2';
23
  }
scssphp/src/Warn.php ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * SCSSPHP
5
+ *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
+ *
8
+ * @license http://opensource.org/licenses/MIT MIT
9
+ *
10
+ * @link http://scssphp.github.io/scssphp
11
+ */
12
+
13
+ namespace ScssPhp\ScssPhp;
14
+
15
+ final class Warn
16
+ {
17
+ /**
18
+ * @var callable|null
19
+ * @phpstan-var (callable(string, bool): void)|null
20
+ */
21
+ private static $callback;
22
+
23
+ /**
24
+ * Prints a warning message associated with the current `@import` or function call.
25
+ *
26
+ * This may only be called within a custom function or importer callback.
27
+ *
28
+ * @param string $message
29
+ *
30
+ * @return void
31
+ */
32
+ public static function warning($message)
33
+ {
34
+ self::reportWarning($message, false);
35
+ }
36
+
37
+ /**
38
+ * Prints a deprecation warning message associated with the current `@import` or function call.
39
+ *
40
+ * This may only be called within a custom function or importer callback.
41
+ *
42
+ * @param string $message
43
+ *
44
+ * @return void
45
+ */
46
+ public static function deprecation($message)
47
+ {
48
+ self::reportWarning($message, true);
49
+ }
50
+
51
+ /**
52
+ * @param callable|null $callback
53
+ *
54
+ * @return callable|null The previous warn callback
55
+ *
56
+ * @phpstan-param (callable(string, bool): void)|null $callback
57
+ *
58
+ * @phpstan-return (callable(string, bool): void)|null
59
+ *
60
+ * @internal
61
+ */
62
+ public static function setCallback(callable $callback = null)
63
+ {
64
+ $previousCallback = self::$callback;
65
+ self::$callback = $callback;
66
+
67
+ return $previousCallback;
68
+ }
69
+
70
+ /**
71
+ * @param string $message
72
+ * @param bool $deprecation
73
+ *
74
+ * @return void
75
+ */
76
+ private static function reportWarning($message, $deprecation)
77
+ {
78
+ if (self::$callback === null) {
79
+ throw new \BadMethodCallException('The warning Reporter may only be called within a custom function or importer callback.');
80
+ }
81
+
82
+ \call_user_func(self::$callback, $message, $deprecation);
83
+ }
84
+ }
wp-scss.php CHANGED
@@ -3,7 +3,7 @@
3
  * Plugin Name: WP-SCSS
4
  * Plugin URI: https://github.com/ConnectThink/WP-SCSS
5
  * Description: Compiles scss files live on WordPress.
6
- * Version: 2.2.0
7
  * Author: Connect Think
8
  * Author URI: http://connectthink.com
9
  * License: GPLv3
@@ -44,7 +44,7 @@ if (!defined('WPSCSS_VERSION_KEY'))
44
  define('WPSCSS_VERSION_KEY', 'wpscss_version');
45
 
46
  if (!defined('WPSCSS_VERSION_NUM'))
47
- define('WPSCSS_VERSION_NUM', '2.2.0');
48
 
49
  // Add version to options table
50
  if ( get_option( WPSCSS_VERSION_KEY ) !== false ) {
@@ -111,7 +111,12 @@ function wpscss_plugin_action_links($links, $file) {
111
 
112
  add_filter('option_wpscss_options', 'wpscss_plugin_db_cleanup');
113
  function wpscss_plugin_db_cleanup($option_values){
114
- $option_values['compiling_options'] = str_replace("Leafo", "ScssPhp", $option_values['compiling_options']);
 
 
 
 
 
115
  return $option_values;
116
  }
117
 
@@ -161,7 +166,7 @@ if( $scss_dir_setting == false || $css_dir_setting == false ) {
161
  $wpscss_settings = array(
162
  'scss_dir' => $base_compiling_folder . $scss_dir_setting,
163
  'css_dir' => $base_compiling_folder . $css_dir_setting,
164
- 'compiling' => isset($wpscss_options['compiling_options']) ? $wpscss_options['compiling_options'] : 'ScssPhp\ScssPhp\Formatter\Expanded',
165
  'always_recompile' => isset($wpscss_options['always_recompile']) ? $wpscss_options['always_recompile'] : false,
166
  'errors' => isset($wpscss_options['errors']) ? $wpscss_options['errors'] : 'show',
167
  'sourcemaps' => isset($wpscss_options['sourcemap_options']) ? $wpscss_options['sourcemap_options'] : 'SOURCE_MAP_NONE',
@@ -194,7 +199,6 @@ function wp_scss_needs_compiling() {
194
  wpscss_handle_errors();
195
  }
196
  }
197
-
198
  add_action('wp_loaded', 'wp_scss_needs_compiling');
199
 
200
  function wp_scss_compile() {
3
  * Plugin Name: WP-SCSS
4
  * Plugin URI: https://github.com/ConnectThink/WP-SCSS
5
  * Description: Compiles scss files live on WordPress.
6
+ * Version: 2.3.0
7
  * Author: Connect Think
8
  * Author URI: http://connectthink.com
9
  * License: GPLv3
44
  define('WPSCSS_VERSION_KEY', 'wpscss_version');
45
 
46
  if (!defined('WPSCSS_VERSION_NUM'))
47
+ define('WPSCSS_VERSION_NUM', '2.3.0');
48
 
49
  // Add version to options table
50
  if ( get_option( WPSCSS_VERSION_KEY ) !== false ) {
111
 
112
  add_filter('option_wpscss_options', 'wpscss_plugin_db_cleanup');
113
  function wpscss_plugin_db_cleanup($option_values){
114
+ $compiling_options = str_replace("Leafo", "ScssPhp", $option_values['compiling_options']);
115
+ $compiling_options = str_replace("ScssPhp\\ScssPhp\\Formatter\\", "", $compiling_options);
116
+ $compiling_options = str_replace(["Compact", "Crunched"], "compressed", $compiling_options);
117
+ $compiling_options = str_replace("Nested", "expanded", $compiling_options);
118
+ $compiling_options = strtolower($compiling_options);
119
+ $option_values['compiling_options'] = $compiling_options;
120
  return $option_values;
121
  }
122
 
166
  $wpscss_settings = array(
167
  'scss_dir' => $base_compiling_folder . $scss_dir_setting,
168
  'css_dir' => $base_compiling_folder . $css_dir_setting,
169
+ 'compiling' => isset($wpscss_options['compiling_options']) ? $wpscss_options['compiling_options'] : 'compressed',
170
  'always_recompile' => isset($wpscss_options['always_recompile']) ? $wpscss_options['always_recompile'] : false,
171
  'errors' => isset($wpscss_options['errors']) ? $wpscss_options['errors'] : 'show',
172
  'sourcemaps' => isset($wpscss_options['sourcemap_options']) ? $wpscss_options['sourcemap_options'] : 'SOURCE_MAP_NONE',
199
  wpscss_handle_errors();
200
  }
201
  }
 
202
  add_action('wp_loaded', 'wp_scss_needs_compiling');
203
 
204
  function wp_scss_compile() {