WP-SCSS - Version 2.2.0

Version Description

  • Updates to allow compile() from outside the plugin niaccurshi
    • Update src to use ScssPHP github repo at 1.2.1
Download this release

Release Info

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

Code changes from version 2.1.6 to 2.2.0

class/class-wp-scss.php CHANGED
@@ -9,9 +9,9 @@ class Wp_Scss {
9
  * Compiling preferences properites
10
  *
11
  * @var string
12
- * @access public
13
  */
14
- public $scss_dir, $css_dir, $compile_method, $scssc, $compile_errors, $sourcemaps;
15
 
16
  /**
17
  * Set values for Wp_Scss::properties
@@ -25,27 +25,62 @@ class Wp_Scss {
25
  * @var array compile_errors - catches errors from compile
26
  */
27
  public function __construct ($scss_dir, $css_dir, $compile_method, $sourcemaps) {
28
- global $scssc;
29
  $this->scss_dir = $scss_dir;
30
  $this->css_dir = $css_dir;
31
  $this->compile_method = $compile_method;
32
  $this->compile_errors = array();
33
- $scssc = new Compiler();
 
 
34
 
35
- $scssc->setFormatter( $compile_method );
36
- $scssc->setImportPaths( $this->scss_dir );
37
 
38
  $this->sourcemaps = $sourcemaps;
39
  }
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  /**
42
  * METHOD COMPILE
43
  * Loops through scss directory and compilers files that end
44
  * with .scss and do not have '_' in front.
45
  *
46
- * @function compiler - passes input content through scssphp,
47
- * puts compiled css into cache file
48
- *
49
  * @var array input_files - array of .scss files with no '_' in front
50
  * @var array sdir_arr - an array of all the files in the scss directory
51
  *
@@ -54,91 +89,93 @@ class Wp_Scss {
54
  * @access public
55
  */
56
  public function compile() {
57
- global $scssc, $cache;
58
- $cache = WPSCSS_PLUGIN_DIR . '/cache/';
 
 
 
 
 
 
 
59
 
60
- //Compiler - Takes scss $in and writes compiled css to $out file
61
- // catches errors and puts them the object's compiled_errors property
62
- if (!function_exists( 'compiler' )) {
63
- function compiler($in, $out, $instance) {
64
- global $scssc, $cache;
65
 
66
- if (!file_exists($cache)) {
67
- mkdir($cache, 0644);
68
- }
69
- if (is_writable($cache)) {
70
- try {
71
- $map = basename($out) . '.map';
72
- $scssc->setSourceMap(constant('ScssPhp\ScssPhp\Compiler::' . $instance->sourcemaps));
73
- $scssc->setSourceMapOptions(array(
74
- 'sourceMapWriteTo' => $instance->css_dir . $map, // absolute path to a file to write the map to
75
- 'sourceMapURL' => $map, // url of the map
76
- 'sourceMapBasepath' => rtrim(ABSPATH, '/'), // base path for filename normalization
77
- 'sourceRoot' => home_url('/'), // This value is prepended to the individual entries in the 'source' field.
78
- ));
79
-
80
- $css = $scssc->compile(file_get_contents($in), $in);
81
-
82
- file_put_contents($cache.basename($out), $css);
83
- } catch (Exception $e) {
84
- $errors = array (
85
- 'file' => basename($in),
86
- 'message' => $e->getMessage(),
87
- );
88
- array_push($instance->compile_errors, $errors);
89
  }
90
- } else {
91
- $errors = array (
92
- 'file' => $cache,
93
- 'message' => "File Permission Error, permission denied. Please make the cache directory writable."
94
- );
95
- array_push($instance->compile_errors, $errors);
96
  }
 
 
 
 
 
 
97
  }
 
 
98
 
99
- $input_files = array();
100
- // Loop through directory and get .scss file that do not start with '_'
101
- foreach(new DirectoryIterator($this->scss_dir) as $file) {
102
- if (substr($file, 0, 1) != "_" && pathinfo($file->getFilename(), PATHINFO_EXTENSION) == 'scss') {
103
- array_push($input_files, $file->getFilename());
104
- }
105
- }
 
 
 
 
 
 
 
 
 
106
 
107
- // For each input file, find matching css file and compile
108
- foreach ($input_files as $scss_file) {
109
- $input = $this->scss_dir.$scss_file;
110
- $outputName = preg_replace("/\.[^$]*/",".css", $scss_file);
111
- $output = $this->css_dir.$outputName;
 
 
 
 
 
 
 
 
112
 
113
- compiler($input, $output, $this);
114
- }
115
 
116
- if (count($this->compile_errors) < 1) {
117
- if ( is_writable($this->css_dir) ) {
118
- foreach (new DirectoryIterator($cache) as $cache_file) {
119
- if ( pathinfo($cache_file->getFilename(), PATHINFO_EXTENSION) == 'css') {
120
- file_put_contents($this->css_dir.$cache_file, file_get_contents($cache.$cache_file));
121
- unlink($cache.$cache_file->getFilename()); // Delete file on successful write
122
- }
123
- }
124
- } else {
125
- $errors = array(
126
- 'file' => 'CSS Directory',
127
- 'message' => "File Permissions Error, permission denied. Please make your CSS directory writable."
128
- );
129
- array_push($this->compile_errors, $errors);
130
- }
131
  }
132
- }else{
133
  $errors = array (
134
- 'file' => 'SCSS compiler',
135
- 'message' => "Compiling Error, function 'compiler' already exists."
136
  );
137
- array_push($this->compile_errors, $errors);
138
  }
139
  }
140
 
141
-
142
  /**
143
  * METHOD NEEDS_COMPILING
144
  * Gets the most recently modified file in the scss directory
@@ -159,7 +196,7 @@ class Wp_Scss {
159
  */
160
  public function needs_compiling() {
161
  global $wpscss_settings;
162
- if (defined('WP_SCSS_ALWAYS_RECOMPILE') && WP_SCSS_ALWAYS_RECOMPILE || $wpscss_settings['always_recompile'] === "1") {
163
  return true;
164
  }
165
 
@@ -245,7 +282,7 @@ class Wp_Scss {
245
  }
246
 
247
  public function set_variables(array $variables) {
248
- global $scssc;
249
- $scssc->setVariables($variables);
250
  }
251
  } // End Wp_Scss Class
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
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;
41
  }
42
 
43
+ /**
44
+ * METHOD GET SCSS DIRECTORY
45
+ * Returns the stored SCSS directory for this compile instance
46
+ *
47
+ * @return string - Value of the SCSS directory
48
+ *
49
+ * @access public
50
+ */
51
+ public function get_scss_dir() {
52
+ return $this->scss_dir;
53
+ }
54
+
55
+ /**
56
+ * METHOD GET CSS DIRECTORY
57
+ * Returns the stored CSS directory for this compile instance
58
+ *
59
+ * @return string - Value of the CSS directory
60
+ *
61
+ * @access public
62
+ */
63
+ public function get_css_dir() {
64
+ return $this->css_dir;
65
+ }
66
+
67
+ /**
68
+ * METHOD GET CSS DIRECTORY
69
+ * Returns the stored CSS directory for this compile instance
70
+ *
71
+ * @return array - List of errors from the compile process, if any
72
+ *
73
+ * @access public
74
+ */
75
+ public function get_compile_errors() {
76
+ return $this->compile_errors;
77
+ }
78
+
79
  /**
80
  * METHOD COMPILE
81
  * Loops through scss directory and compilers files that end
82
  * with .scss and do not have '_' in front.
83
  *
 
 
 
84
  * @var array input_files - array of .scss files with no '_' in front
85
  * @var array sdir_arr - an array of all the files in the scss directory
86
  *
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 '_'
96
+ foreach(new DirectoryIterator($this->scss_dir) as $file) {
97
+ if (substr($file, 0, 1) != "_" && pathinfo($file->getFilename(), PATHINFO_EXTENSION) == 'scss') {
98
+ array_push($input_files, $file->getFilename());
99
+ }
100
+ }
101
 
102
+ // For each input file, find matching css file and compile
103
+ foreach ($input_files as $scss_file) {
104
+ $input = $this->scss_dir . $scss_file;
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) {
112
+ if ( is_writable($this->css_dir) ) {
113
+ foreach (new DirectoryIterator($this->cache) as $this->cache_file) {
114
+ if ( pathinfo($this->cache_file->getFilename(), PATHINFO_EXTENSION) == 'css') {
115
+ file_put_contents($this->css_dir . $this->cache_file, file_get_contents($this->cache . $this->cache_file));
116
+ unlink($this->cache . $this->cache_file->getFilename()); // Delete file on successful write
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  }
 
 
 
 
 
 
118
  }
119
+ } else {
120
+ $errors = array(
121
+ 'file' => 'CSS Directory',
122
+ 'message' => "File Permissions Error, permission denied. Please make your CSS directory writable."
123
+ );
124
+ array_push($this->compile_errors, $errors);
125
  }
126
+ }
127
+ }
128
 
129
+ /**
130
+ * METHOD COMPILER
131
+ * Takes scss $in and writes compiled css to $out file
132
+ * catches errors and puts them the object's compiled_errors property
133
+ *
134
+ * @function compiler - passes input content through scssphp,
135
+ * puts compiled css into cache file
136
+ *
137
+ * @var array input_files - array of .scss files with no '_' in front
138
+ * @var array sdir_arr - an array of all the files in the scss directory
139
+ *
140
+ * @return nothing - Puts successfully compiled css into appropriate location
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);
148
+ }
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) {
164
+ $errors = array (
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
 
 
179
  /**
180
  * METHOD NEEDS_COMPILING
181
  * Gets the most recently modified file in the scss directory
196
  */
197
  public function needs_compiling() {
198
  global $wpscss_settings;
199
+ if (defined('WP_SCSS_ALWAYS_RECOMPILE') && WP_SCSS_ALWAYS_RECOMPILE || isset($wpscss_settings['always_recompile']) ? $wpscss_settings['always_recompile'] === "1" : false) {
200
  return true;
201
  }
202
 
282
  }
283
 
284
  public function set_variables(array $variables) {
285
+
286
+ $this->scssc->setVariables($variables);
287
  }
288
  } // End Wp_Scss Class
composer.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ConnectThink/WP-SCSS",
3
+ "description": "Compiles .scss files on your wordpress install using lefo's scssphp. Includes settings page for configuring directories, error reporting, compiling options, and auto enqueuing.",
4
+ "keywords": ["wordpress", "plugin"],
5
+ "type": "wordpress-plugin",
6
+ "repositories": [
7
+ {
8
+ "type": "vcs",
9
+ "url": "git@github.com:ConnectThink/WP-SCSS.git"
10
+ }
11
+ ]
12
+ }
13
+
readme.md CHANGED
@@ -104,6 +104,9 @@ This plugin will only work with .scss format.
104
 
105
  ## Changelog
106
 
 
 
 
107
  - 2.1.6
108
  - 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)
109
  - Allow setting Base Directory to Parent theme folder. [Shadoath](https://github.com/ConnectThink/WP-SCSS/issues/178)
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)
110
  - 2.1.6
111
  - 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)
112
  - Allow setting Base Directory to Parent theme folder. [Shadoath](https://github.com/ConnectThink/WP-SCSS/issues/178)
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.1.6
9
  License: GPLv3 or later
10
  License URI: http://www.gnu.org/copyleft/gpl.html
11
 
@@ -76,6 +76,10 @@ If you are having issues with the plugin, create an issue on [github](https://gi
76
 
77
  == Changelog ==
78
 
 
 
 
 
79
  = 2.1.6 =
80
  - 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)
81
  - 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.2.0
9
  License: GPLv3 or later
10
  License URI: http://www.gnu.org/copyleft/gpl.html
11
 
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)
scssphp/bin/pscss CHANGED
@@ -1,9 +1,10 @@
1
  #!/usr/bin/env php
2
  <?php
 
3
  /**
4
  * SCSSPHP
5
  *
6
- * @copyright 2012-2019 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
@@ -24,13 +25,11 @@ use ScssPhp\ScssPhp\Version;
24
 
25
  $style = null;
26
  $loadPaths = null;
27
- $precision = null;
28
  $dumpTree = false;
29
  $inputFile = null;
30
  $changeDir = false;
31
  $debugInfo = false;
32
  $lineNumbers = false;
33
- $ignoreErrors = false;
34
  $encoding = false;
35
  $sourceMap = false;
36
 
@@ -62,7 +61,7 @@ function parseArgument(&$i, $options) {
62
  }
63
 
64
  for ($i = 1; $i < $argc; $i++) {
65
- if ($argv[$i] === '-h' || $argv[$i] === '--help') {
66
  $exe = $argv[0];
67
 
68
  $HELP = <<<EOT
@@ -70,17 +69,17 @@ Usage: $exe [options] [input-file]
70
 
71
  Options include:
72
 
73
- -h, --help Show this message
74
- --continue-on-error Continue compilation (as best as possible) when error encountered
75
- --debug-info Annotate selectors with CSS referring to the source file and line number
76
- -f=format Set the output format (compact, compressed, crunched, expanded, or nested)
77
- -i=path Set import path
78
- --iso8859-1 Use iso8859-1 encoding instead of utf-8 (default utf-8)
79
- --line-numbers Annotate selectors with comments referring to the source file and line number
80
- -p=precision Set decimal number precision (default 10)
81
  --sourcemap Create source map file
82
- -T Dump formatted parse tree
83
- -v, --version Print the version
84
 
85
  EOT;
86
  exit($HELP);
@@ -90,12 +89,13 @@ EOT;
90
  exit(Version::VERSION . "\n");
91
  }
92
 
 
93
  if ($argv[$i] === '--continue-on-error') {
94
- $ignoreErrors = true;
95
  continue;
96
  }
97
 
98
- if ($argv[$i] === '--debug-info') {
99
  $debugInfo = true;
100
  continue;
101
  }
@@ -115,29 +115,30 @@ EOT;
115
  continue;
116
  }
117
 
118
- if ($argv[$i] === '-T') {
119
  $dumpTree = true;
120
  continue;
121
  }
122
 
123
- $value = parseArgument($i, array('-f', '--style'));
124
 
125
  if (isset($value)) {
126
  $style = $value;
127
  continue;
128
  }
129
 
130
- $value = parseArgument($i, array('-i', '--load_paths'));
131
 
132
  if (isset($value)) {
133
  $loadPaths = $value;
134
  continue;
135
  }
136
 
 
137
  $value = parseArgument($i, array('-p', '--precision'));
138
 
139
  if (isset($value)) {
140
- $precision = $value;
141
  continue;
142
  }
143
 
@@ -184,18 +185,10 @@ if ($lineNumbers) {
184
  $scss->setLineNumberStyle(Compiler::LINE_COMMENTS);
185
  }
186
 
187
- if ($ignoreErrors) {
188
- $scss->setIgnoreErrors($ignoreErrors);
189
- }
190
-
191
  if ($loadPaths) {
192
  $scss->setImportPaths(explode(PATH_SEPARATOR, $loadPaths));
193
  }
194
 
195
- if ($precision) {
196
- $scss->setNumberPrecision($precision);
197
- }
198
-
199
  if ($style) {
200
  $scss->setFormatter('ScssPhp\\ScssPhp\\Formatter\\' . ucfirst($style));
201
  }
1
  #!/usr/bin/env php
2
  <?php
3
+
4
  /**
5
  * SCSSPHP
6
  *
7
+ * @copyright 2012-2020 Leaf Corcoran
8
  *
9
  * @license http://opensource.org/licenses/MIT MIT
10
  *
25
 
26
  $style = null;
27
  $loadPaths = null;
 
28
  $dumpTree = false;
29
  $inputFile = null;
30
  $changeDir = false;
31
  $debugInfo = false;
32
  $lineNumbers = false;
 
33
  $encoding = false;
34
  $sourceMap = false;
35
 
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
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;
85
  exit($HELP);
89
  exit(Version::VERSION . "\n");
90
  }
91
 
92
+ // Keep parsing --continue-on-error to avoid BC breaks for scripts using it
93
  if ($argv[$i] === '--continue-on-error') {
94
+ // TODO report it as a warning ?
95
  continue;
96
  }
97
 
98
+ if ($argv[$i] === '-g' || $argv[$i] === '--debug-info') {
99
  $debugInfo = true;
100
  continue;
101
  }
115
  continue;
116
  }
117
 
118
+ if ($argv[$i] === '-T' || $argv[$i] === '--dump-tree') {
119
  $dumpTree = true;
120
  continue;
121
  }
122
 
123
+ $value = parseArgument($i, array('-t', '-s', '--style'));
124
 
125
  if (isset($value)) {
126
  $style = $value;
127
  continue;
128
  }
129
 
130
+ $value = parseArgument($i, array('-I', '--load-path'));
131
 
132
  if (isset($value)) {
133
  $loadPaths = $value;
134
  continue;
135
  }
136
 
137
+ // Keep parsing --precision to avoid BC breaks for scripts using it
138
  $value = parseArgument($i, array('-p', '--precision'));
139
 
140
  if (isset($value)) {
141
+ // TODO report it as a warning ?
142
  continue;
143
  }
144
 
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
  }
scssphp/scss.inc.php CHANGED
@@ -1,34 +1,36 @@
1
  <?php
 
2
  if (version_compare(PHP_VERSION, '5.6') < 0) {
3
- throw new \Exception('scssphp requires PHP 5.6 or above');
4
  }
5
 
6
  if (! class_exists('ScssPhp\ScssPhp\Version', false)) {
7
- include_once __DIR__ . '/src/Base/Range.php';
8
- include_once __DIR__ . '/src/Block.php';
9
- include_once __DIR__ . '/src/Cache.php';
10
- include_once __DIR__ . '/src/Colors.php';
11
- include_once __DIR__ . '/src/Compiler.php';
12
- include_once __DIR__ . '/src/Compiler/Environment.php';
13
- include_once __DIR__ . '/src/Exception/CompilerException.php';
14
- include_once __DIR__ . '/src/Exception/ParserException.php';
15
- include_once __DIR__ . '/src/Exception/RangeException.php';
16
- include_once __DIR__ . '/src/Exception/ServerException.php';
17
- include_once __DIR__ . '/src/Formatter.php';
18
- include_once __DIR__ . '/src/Formatter/Compact.php';
19
- include_once __DIR__ . '/src/Formatter/Compressed.php';
20
- include_once __DIR__ . '/src/Formatter/Crunched.php';
21
- include_once __DIR__ . '/src/Formatter/Debug.php';
22
- include_once __DIR__ . '/src/Formatter/Expanded.php';
23
- include_once __DIR__ . '/src/Formatter/Nested.php';
24
- include_once __DIR__ . '/src/Formatter/OutputBlock.php';
25
- include_once __DIR__ . '/src/Node.php';
26
- include_once __DIR__ . '/src/Node/Number.php';
27
- include_once __DIR__ . '/src/Parser.php';
28
- include_once __DIR__ . '/src/SourceMap/Base64.php';
29
- include_once __DIR__ . '/src/SourceMap/Base64VLQ.php';
30
- include_once __DIR__ . '/src/SourceMap/SourceMapGenerator.php';
31
- include_once __DIR__ . '/src/Type.php';
32
- include_once __DIR__ . '/src/Util.php';
33
- include_once __DIR__ . '/src/Version.php';
 
34
  }
1
  <?php
2
+
3
  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
  }
scssphp/src/Base/Range.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2015-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2015-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
scssphp/src/Block.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
scssphp/src/Cache.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -22,13 +23,12 @@ use Exception;
22
  * taking in account options that affects the result
23
  *
24
  * The cache manager is agnostic about data format and only the operation is expected to be described by string
25
- *
26
  */
27
 
28
  /**
29
  * SCSS cache
30
  *
31
- * @author Cedric Morin
32
  */
33
  class Cache
34
  {
@@ -57,12 +57,12 @@ class Cache
57
  public function __construct($options)
58
  {
59
  // check $cacheDir
60
- if (isset($options['cache_dir'])) {
61
- self::$cacheDir = $options['cache_dir'];
62
  }
63
 
64
  if (empty(self::$cacheDir)) {
65
- throw new Exception('cache_dir not set');
66
  }
67
 
68
  if (isset($options['prefix'])) {
@@ -74,7 +74,7 @@ class Cache
74
  }
75
 
76
  if (isset($options['forceRefresh'])) {
77
- self::$forceRefresh = $options['force_refresh'];
78
  }
79
 
80
  self::checkCacheDir();
@@ -97,18 +97,20 @@ class Cache
97
  {
98
  $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
99
 
100
- if ((! self::$forceRefresh || (self::$forceRefresh === 'once' &&
 
101
  isset(self::$refreshed[$fileCache]))) && file_exists($fileCache)
102
  ) {
103
  $cacheTime = filemtime($fileCache);
104
 
105
- if ((is_null($lastModified) || $cacheTime > $lastModified) &&
 
106
  $cacheTime + self::$gcLifetime > time()
107
  ) {
108
  $c = file_get_contents($fileCache);
109
  $c = unserialize($c);
110
 
111
- if (is_array($c) && isset($c['value'])) {
112
  return $c['value'];
113
  }
114
  }
@@ -132,6 +134,7 @@ class Cache
132
 
133
  $c = ['value' => $value];
134
  $c = serialize($c);
 
135
  file_put_contents($fileCache, $c);
136
 
137
  if (self::$forceRefresh === 'once') {
@@ -176,13 +179,11 @@ class Cache
176
  self::$cacheDir = str_replace('\\', '/', self::$cacheDir);
177
  self::$cacheDir = rtrim(self::$cacheDir, '/') . '/';
178
 
179
- if (! file_exists(self::$cacheDir)) {
180
- if (! mkdir(self::$cacheDir)) {
181
- throw new Exception('Cache directory couldn\'t be created: ' . self::$cacheDir);
182
- }
183
- } elseif (! is_dir(self::$cacheDir)) {
184
  throw new Exception('Cache directory doesn\'t exist: ' . self::$cacheDir);
185
- } elseif (! is_writable(self::$cacheDir)) {
 
 
186
  throw new Exception('Cache directory isn\'t writable: ' . self::$cacheDir);
187
  }
188
  }
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
23
  * taking in account options that affects the result
24
  *
25
  * The cache manager is agnostic about data format and only the operation is expected to be described by string
 
26
  */
27
 
28
  /**
29
  * SCSS cache
30
  *
31
+ * @author Cedric Morin <cedric@yterium.com>
32
  */
33
  class Cache
34
  {
57
  public function __construct($options)
58
  {
59
  // check $cacheDir
60
+ if (isset($options['cacheDir'])) {
61
+ self::$cacheDir = $options['cacheDir'];
62
  }
63
 
64
  if (empty(self::$cacheDir)) {
65
+ throw new Exception('cacheDir not set');
66
  }
67
 
68
  if (isset($options['prefix'])) {
74
  }
75
 
76
  if (isset($options['forceRefresh'])) {
77
+ self::$forceRefresh = $options['forceRefresh'];
78
  }
79
 
80
  self::checkCacheDir();
97
  {
98
  $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
99
 
100
+ if (
101
+ ((self::$forceRefresh === false) || (self::$forceRefresh === 'once' &&
102
  isset(self::$refreshed[$fileCache]))) && file_exists($fileCache)
103
  ) {
104
  $cacheTime = filemtime($fileCache);
105
 
106
+ if (
107
+ (\is_null($lastModified) || $cacheTime > $lastModified) &&
108
  $cacheTime + self::$gcLifetime > time()
109
  ) {
110
  $c = file_get_contents($fileCache);
111
  $c = unserialize($c);
112
 
113
+ if (\is_array($c) && isset($c['value'])) {
114
  return $c['value'];
115
  }
116
  }
134
 
135
  $c = ['value' => $value];
136
  $c = serialize($c);
137
+
138
  file_put_contents($fileCache, $c);
139
 
140
  if (self::$forceRefresh === 'once') {
179
  self::$cacheDir = str_replace('\\', '/', self::$cacheDir);
180
  self::$cacheDir = rtrim(self::$cacheDir, '/') . '/';
181
 
182
+ if (! is_dir(self::$cacheDir)) {
 
 
 
 
183
  throw new Exception('Cache directory doesn\'t exist: ' . self::$cacheDir);
184
+ }
185
+
186
+ if (! is_writable(self::$cacheDir)) {
187
  throw new Exception('Cache directory isn\'t writable: ' . self::$cacheDir);
188
  }
189
  }
scssphp/src/Colors.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -25,9 +26,10 @@ class Colors
25
  *
26
  * @var array
27
  */
28
- public static $cssColors = [
29
  'aliceblue' => '240,248,255',
30
  'antiquewhite' => '250,235,215',
 
31
  'aqua' => '0,255,255',
32
  'aquamarine' => '127,255,212',
33
  'azure' => '240,255,255',
@@ -46,13 +48,12 @@ class Colors
46
  'cornflowerblue' => '100,149,237',
47
  'cornsilk' => '255,248,220',
48
  'crimson' => '220,20,60',
49
- 'cyan' => '0,255,255',
50
  'darkblue' => '0,0,139',
51
  'darkcyan' => '0,139,139',
52
  'darkgoldenrod' => '184,134,11',
53
  'darkgray' => '169,169,169',
54
- 'darkgreen' => '0,100,0',
55
  'darkgrey' => '169,169,169',
 
56
  'darkkhaki' => '189,183,107',
57
  'darkmagenta' => '139,0,139',
58
  'darkolivegreen' => '85,107,47',
@@ -74,15 +75,16 @@ class Colors
74
  'firebrick' => '178,34,34',
75
  'floralwhite' => '255,250,240',
76
  'forestgreen' => '34,139,34',
 
77
  'fuchsia' => '255,0,255',
78
  'gainsboro' => '220,220,220',
79
  'ghostwhite' => '248,248,255',
80
  'gold' => '255,215,0',
81
  'goldenrod' => '218,165,32',
82
  'gray' => '128,128,128',
 
83
  'green' => '0,128,0',
84
  'greenyellow' => '173,255,47',
85
- 'grey' => '128,128,128',
86
  'honeydew' => '240,255,240',
87
  'hotpink' => '255,105,180',
88
  'indianred' => '205,92,92',
@@ -98,8 +100,8 @@ class Colors
98
  'lightcyan' => '224,255,255',
99
  'lightgoldenrodyellow' => '250,250,210',
100
  'lightgray' => '211,211,211',
101
- 'lightgreen' => '144,238,144',
102
  'lightgrey' => '211,211,211',
 
103
  'lightpink' => '255,182,193',
104
  'lightsalmon' => '255,160,122',
105
  'lightseagreen' => '32,178,170',
@@ -111,7 +113,6 @@ class Colors
111
  'lime' => '0,255,0',
112
  'limegreen' => '50,205,50',
113
  'linen' => '250,240,230',
114
- 'magenta' => '255,0,255',
115
  'maroon' => '128,0,0',
116
  'mediumaquamarine' => '102,205,170',
117
  'mediumblue' => '0,0,205',
@@ -145,7 +146,6 @@ class Colors
145
  'plum' => '221,160,221',
146
  'powderblue' => '176,224,230',
147
  'purple' => '128,0,128',
148
- 'rebeccapurple' => '102,51,153',
149
  'red' => '255,0,0',
150
  'rosybrown' => '188,143,143',
151
  'royalblue' => '65,105,225',
@@ -167,7 +167,6 @@ class Colors
167
  'teal' => '0,128,128',
168
  'thistle' => '216,191,216',
169
  'tomato' => '255,99,71',
170
- 'transparent' => '0,0,0,0',
171
  'turquoise' => '64,224,208',
172
  'violet' => '238,130,238',
173
  'wheat' => '245,222,179',
@@ -175,5 +174,72 @@ class Colors
175
  'whitesmoke' => '245,245,245',
176
  'yellow' => '255,255,0',
177
  'yellowgreen' => '154,205,50',
 
 
178
  ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  }
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
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',
48
  'cornflowerblue' => '100,149,237',
49
  'cornsilk' => '255,248,220',
50
  'crimson' => '220,20,60',
 
51
  'darkblue' => '0,0,139',
52
  'darkcyan' => '0,139,139',
53
  'darkgoldenrod' => '184,134,11',
54
  'darkgray' => '169,169,169',
 
55
  'darkgrey' => '169,169,169',
56
+ 'darkgreen' => '0,100,0',
57
  'darkkhaki' => '189,183,107',
58
  'darkmagenta' => '139,0,139',
59
  'darkolivegreen' => '85,107,47',
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',
83
  'goldenrod' => '218,165,32',
84
  'gray' => '128,128,128',
85
+ 'grey' => '128,128,128',
86
  'green' => '0,128,0',
87
  'greenyellow' => '173,255,47',
 
88
  'honeydew' => '240,255,240',
89
  'hotpink' => '255,105,180',
90
  'indianred' => '205,92,92',
100
  'lightcyan' => '224,255,255',
101
  'lightgoldenrodyellow' => '250,250,210',
102
  'lightgray' => '211,211,211',
 
103
  'lightgrey' => '211,211,211',
104
+ 'lightgreen' => '144,238,144',
105
  'lightpink' => '255,182,193',
106
  'lightsalmon' => '255,160,122',
107
  'lightseagreen' => '32,178,170',
113
  'lime' => '0,255,0',
114
  'limegreen' => '50,205,50',
115
  'linen' => '250,240,230',
 
116
  'maroon' => '128,0,0',
117
  'mediumaquamarine' => '102,205,170',
118
  'mediumblue' => '0,0,205',
146
  'plum' => '221,160,221',
147
  'powderblue' => '176,224,230',
148
  'purple' => '128,0,128',
 
149
  'red' => '255,0,0',
150
  'rosybrown' => '188,143,143',
151
  'royalblue' => '65,105,225',
167
  'teal' => '0,128,128',
168
  'thistle' => '216,191,216',
169
  'tomato' => '255,99,71',
 
170
  'turquoise' => '64,224,208',
171
  'violet' => '238,130,238',
172
  'wheat' => '245,222,179',
174
  'whitesmoke' => '245,245,245',
175
  'yellow' => '255,255,0',
176
  'yellowgreen' => '154,205,50',
177
+ 'rebeccapurple' => '102,51,153',
178
+ 'transparent' => '0,0,0,0',
179
  ];
180
+
181
+ /**
182
+ * Convert named color in a [r,g,b[,a]] array
183
+ *
184
+ * @param string $colorName
185
+ *
186
+ * @return array|null
187
+ */
188
+ public static function colorNameToRGBa($colorName)
189
+ {
190
+ if (\is_string($colorName) && isset(static::$cssColors[$colorName])) {
191
+ $rgba = explode(',', static::$cssColors[$colorName]);
192
+
193
+ // only case with opacity is transparent, with opacity=0, so we can intval on opacity also
194
+ $rgba = array_map('intval', $rgba);
195
+
196
+ return $rgba;
197
+ }
198
+
199
+ return null;
200
+ }
201
+
202
+ /**
203
+ * Reverse conversion : from RGBA to a color name if possible
204
+ *
205
+ * @param integer $r
206
+ * @param integer $g
207
+ * @param integer $b
208
+ * @param integer $a
209
+ *
210
+ * @return string|null
211
+ */
212
+ public static function RGBaToColorName($r, $g, $b, $a = 1)
213
+ {
214
+ static $reverseColorTable = null;
215
+
216
+ if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b) || ! is_numeric($a)) {
217
+ return null;
218
+ }
219
+
220
+ if ($a < 1) {
221
+ return null;
222
+ }
223
+
224
+ if (\is_null($reverseColorTable)) {
225
+ $reverseColorTable = [];
226
+
227
+ foreach (static::$cssColors as $name => $rgb_str) {
228
+ $rgb_str = explode(',', $rgb_str);
229
+
230
+ if (
231
+ \count($rgb_str) == 3 &&
232
+ ! isset($reverseColorTable[\intval($rgb_str[0])][\intval($rgb_str[1])][\intval($rgb_str[2])])
233
+ ) {
234
+ $reverseColorTable[\intval($rgb_str[0])][\intval($rgb_str[1])][\intval($rgb_str[2])] = $name;
235
+ }
236
+ }
237
+ }
238
+
239
+ if (isset($reverseColorTable[\intval($r)][\intval($g)][\intval($b)])) {
240
+ return $reverseColorTable[\intval($r)][\intval($g)][\intval($b)];
241
+ }
242
+
243
+ return null;
244
+ }
245
  }
scssphp/src/Compiler.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -73,7 +74,7 @@ class Compiler
73
  /**
74
  * @var array
75
  */
76
- static protected $operatorNames = [
77
  '+' => 'add',
78
  '-' => 'sub',
79
  '*' => 'mul',
@@ -93,23 +94,25 @@ class Compiler
93
  /**
94
  * @var array
95
  */
96
- static protected $namespaces = [
97
  'special' => '%',
98
  'mixin' => '@',
99
  'function' => '^',
100
  ];
101
 
102
- static public $true = [Type::T_KEYWORD, 'true'];
103
- static public $false = [Type::T_KEYWORD, 'false'];
104
- static public $null = [Type::T_NULL];
105
- static public $nullString = [Type::T_STRING, '', []];
106
- static public $defaultValue = [Type::T_KEYWORD, ''];
107
- static public $selfSelector = [Type::T_SELF];
108
- static public $emptyList = [Type::T_LIST, '', []];
109
- static public $emptyMap = [Type::T_MAP, [], []];
110
- static public $emptyString = [Type::T_STRING, '"', []];
111
- static public $with = [Type::T_KEYWORD, 'with'];
112
- static public $without = [Type::T_KEYWORD, 'without'];
 
 
113
 
114
  protected $importPaths = [''];
115
  protected $importCache = [];
@@ -159,11 +162,14 @@ class Compiler
159
  protected $stderr;
160
  protected $shouldEvaluate;
161
  protected $ignoreErrors;
 
162
 
163
  protected $callStack = [];
164
 
165
  /**
166
  * Constructor
 
 
167
  */
168
  public function __construct($cacheOptions = null)
169
  {
@@ -173,8 +179,15 @@ class Compiler
173
  if ($cacheOptions) {
174
  $this->cache = new Cache($cacheOptions);
175
  }
 
 
176
  }
177
 
 
 
 
 
 
178
  public function getCompileOptions()
179
  {
180
  $options = [
@@ -190,6 +203,16 @@ class Compiler
190
  return $options;
191
  }
192
 
 
 
 
 
 
 
 
 
 
 
193
  /**
194
  * Compile scss
195
  *
@@ -203,14 +226,14 @@ class Compiler
203
  public function compile($code, $path = null)
204
  {
205
  if ($this->cache) {
206
- $cacheKey = ($path ? $path : "(stdin)") . ":" . md5($code);
207
  $compileOptions = $this->getCompileOptions();
208
- $cache = $this->cache->getCache("compile", $cacheKey, $compileOptions);
209
 
210
- if (is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) {
211
  // check if any dependency file changed before accepting the cache
212
  foreach ($cache['dependencies'] as $file => $mtime) {
213
- if (! file_exists($file) || filemtime($file) !== $mtime) {
214
  unset($cache);
215
  break;
216
  }
@@ -234,7 +257,7 @@ class Compiler
234
  $this->storeEnv = null;
235
  $this->charsetSeen = null;
236
  $this->shouldEvaluate = null;
237
- $this->stderr = fopen('php://stderr', 'w');
238
 
239
  $this->parser = $this->parserFactory($path);
240
  $tree = $this->parser->parse($code);
@@ -251,7 +274,7 @@ class Compiler
251
  $sourceMapGenerator = null;
252
 
253
  if ($this->sourceMap) {
254
- if (is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) {
255
  $sourceMapGenerator = $this->sourceMap;
256
  $this->sourceMap = self::SOURCE_MAP_FILE;
257
  } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
@@ -284,7 +307,7 @@ class Compiler
284
  'out' => &$out,
285
  ];
286
 
287
- $this->cache->setCache("compile", $cacheKey, $v, $compileOptions);
288
  }
289
 
290
  return $out;
@@ -299,7 +322,18 @@ class Compiler
299
  */
300
  protected function parserFactory($path)
301
  {
302
- $parser = new Parser($path, count($this->sourceNames), $this->encoding, $this->cache);
 
 
 
 
 
 
 
 
 
 
 
303
 
304
  $this->sourceNames[] = $path;
305
  $this->addParsedFile($path);
@@ -318,7 +352,7 @@ class Compiler
318
  protected function isSelfExtend($target, $origin)
319
  {
320
  foreach ($origin as $sel) {
321
- if (in_array($target, $sel)) {
322
  return true;
323
  }
324
  }
@@ -329,17 +363,13 @@ class Compiler
329
  /**
330
  * Push extends
331
  *
332
- * @param array $target
333
- * @param array $origin
334
- * @param \stdClass $block
335
  */
336
  protected function pushExtends($target, $origin, $block)
337
  {
338
- if ($this->isSelfExtend($target, $origin)) {
339
- return;
340
- }
341
-
342
- $i = count($this->extends);
343
  $this->extends[] = [$target, $origin, $block];
344
 
345
  foreach ($target as $part) {
@@ -361,13 +391,13 @@ class Compiler
361
  */
362
  protected function makeOutputBlock($type, $selectors = null)
363
  {
364
- $out = new OutputBlock;
365
- $out->type = $type;
366
- $out->lines = [];
367
- $out->children = [];
368
- $out->parent = $this->scope;
369
- $out->selectors = $selectors;
370
- $out->depth = $this->env->depth;
371
 
372
  if ($this->env->block instanceof Block) {
373
  $out->sourceName = $this->env->block->sourceName;
@@ -417,7 +447,7 @@ class Compiler
417
  $origin = $this->collapseSelectors($origin);
418
 
419
  $this->sourceLine = $block[Parser::SOURCE_LINE];
420
- $this->throwError("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
421
  }
422
  }
423
 
@@ -435,7 +465,7 @@ class Compiler
435
  foreach ($block->selectors as $s) {
436
  $selectors[] = $s;
437
 
438
- if (! is_array($s)) {
439
  continue;
440
  }
441
 
@@ -468,7 +498,7 @@ class Compiler
468
  $block->selectors[] = $this->compileSelector($selector);
469
  }
470
 
471
- if ($placeholderSelector && 0 === count($block->selectors) && null !== $parentKey) {
472
  unset($block->parent->children[$parentKey]);
473
 
474
  return;
@@ -492,16 +522,20 @@ class Compiler
492
  $new = [];
493
 
494
  foreach ($parts as $part) {
495
- if (is_array($part)) {
496
  $part = $this->glueFunctionSelectors($part);
497
  $new[] = $part;
498
  } else {
499
  // a selector part finishing with a ) is the last part of a :not( or :nth-child(
500
  // and need to be joined to this
501
- if (count($new) && is_string($new[count($new) - 1]) &&
502
- strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
 
503
  ) {
504
- $new[count($new) - 1] .= $part;
 
 
 
505
  } else {
506
  $new[] = $part;
507
  }
@@ -522,13 +556,14 @@ class Compiler
522
  protected function matchExtends($selector, &$out, $from = 0, $initial = true)
523
  {
524
  static $partsPile = [];
525
-
526
  $selector = $this->glueFunctionSelectors($selector);
527
 
528
- if (count($selector) == 1 && in_array(reset($selector), $partsPile)) {
529
  return;
530
  }
531
 
 
 
532
  foreach ($selector as $i => $part) {
533
  if ($i < $from) {
534
  continue;
@@ -536,39 +571,43 @@ class Compiler
536
 
537
  // check that we are not building an infinite loop of extensions
538
  // if the new part is just including a previous part don't try to extend anymore
539
- if (count($part) > 1) {
540
  foreach ($partsPile as $previousPart) {
541
- if (! count(array_diff($previousPart, $part))) {
542
  continue 2;
543
  }
544
  }
545
  }
546
 
547
- if ($this->matchExtendsSingle($part, $origin)) {
548
- $partsPile[] = $part;
549
- $after = array_slice($selector, $i + 1);
550
- $before = array_slice($selector, 0, $i);
551
 
 
 
 
552
  list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
553
 
554
  foreach ($origin as $new) {
555
  $k = 0;
556
 
557
  // remove shared parts
558
- if (count($new) > 1) {
559
  while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
560
  $k++;
561
  }
562
  }
563
 
 
 
 
 
564
  $replacement = [];
565
- $tempReplacement = $k > 0 ? array_slice($new, $k) : $new;
566
 
567
- for ($l = count($tempReplacement) - 1; $l >= 0; $l--) {
568
  $slice = [];
569
 
570
  foreach ($tempReplacement[$l] as $chunk) {
571
- if (! in_array($chunk, $slice)) {
572
  $slice[] = $chunk;
573
  }
574
  }
@@ -580,7 +619,7 @@ class Compiler
580
  }
581
  }
582
 
583
- $afterBefore = $l != 0 ? array_slice($tempReplacement, 0, $l) : [];
584
 
585
  // Merge shared direct relationships.
586
  $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore);
@@ -596,64 +635,137 @@ class Compiler
596
  continue;
597
  }
598
 
599
- $out[] = $result;
600
 
601
  // recursively check for more matches
602
- $startRecurseFrom = count($before) + min(count($nonBreakableBefore), count($mergedBefore));
603
- $this->matchExtends($result, $out, $startRecurseFrom, false);
 
 
 
 
 
604
 
605
  // selector sequence merging
606
- if (! empty($before) && count($new) > 1) {
607
- $preSharedParts = $k > 0 ? array_slice($before, 0, $k) : [];
608
- $postSharedParts = $k > 0 ? array_slice($before, $k) : $before;
609
 
610
- list($betweenSharedParts, $nonBreakable2) = $this->extractRelationshipFromFragment($afterBefore);
611
 
612
  $result2 = array_merge(
613
  $preSharedParts,
614
  $betweenSharedParts,
615
  $postSharedParts,
616
- $nonBreakable2,
617
  $nonBreakableBefore,
618
  $replacement,
619
  $after
620
  );
621
 
622
- $out[] = $result2;
623
  }
624
  }
 
 
 
 
 
 
 
 
 
625
 
626
- array_pop($partsPile);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
  }
628
  }
 
629
  }
630
 
631
  /**
632
  * Match extends single
633
  *
634
- * @param array $rawSingle
635
- * @param array $outOrigin
 
636
  *
637
  * @return boolean
638
  */
639
- protected function matchExtendsSingle($rawSingle, &$outOrigin)
640
  {
641
  $counts = [];
642
  $single = [];
643
 
644
  // simple usual cases, no need to do the whole trick
645
- if (in_array($rawSingle, [['>'],['+'],['~']])) {
646
  return false;
647
  }
648
 
649
  foreach ($rawSingle as $part) {
650
  // matches Number
651
- if (! is_string($part)) {
652
  return false;
653
  }
654
 
655
- if (! preg_match('/^[\[.:#%]/', $part) && count($single)) {
656
- $single[count($single) - 1] .= $part;
657
  } else {
658
  $single[] = $part;
659
  }
@@ -661,21 +773,52 @@ class Compiler
661
 
662
  $extendingDecoratedTag = false;
663
 
664
- if (count($single) > 1) {
665
  $matches = null;
666
  $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
667
  }
668
 
669
- foreach ($single as $part) {
 
 
 
670
  if (isset($this->extendsMap[$part])) {
671
  foreach ($this->extendsMap[$part] as $idx) {
672
  $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
673
  }
674
  }
675
- }
676
 
677
- $outOrigin = [];
678
- $found = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
679
 
680
  foreach ($counts as $idx => $count) {
681
  list($target, $origin, /* $block */) = $this->extends[$idx];
@@ -683,7 +826,7 @@ class Compiler
683
  $origin = $this->glueFunctionSelectors($origin);
684
 
685
  // check count
686
- if ($count !== count($target)) {
687
  continue;
688
  }
689
 
@@ -693,14 +836,15 @@ class Compiler
693
 
694
  foreach ($origin as $j => $new) {
695
  // prevent infinite loop when target extends itself
696
- if ($this->isSelfExtend($single, $origin)) {
697
  return false;
698
  }
699
 
700
  $replacement = end($new);
701
 
702
  // Extending a decorated tag with another tag is not possible.
703
- if ($extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
 
704
  preg_match('/^[a-z0-9]+$/i', $replacement[0])
705
  ) {
706
  unset($origin[$j]);
@@ -709,8 +853,8 @@ class Compiler
709
 
710
  $combined = $this->combineSelectorSingle($replacement, $rem);
711
 
712
- if (count(array_diff($combined, $origin[$j][count($origin[$j]) - 1]))) {
713
- $origin[$j][count($origin[$j]) - 1] = $combined;
714
  }
715
  }
716
 
@@ -738,12 +882,13 @@ class Compiler
738
  {
739
  $parents = [];
740
  $children = [];
741
- $j = $i = count($fragment);
 
742
 
743
  for (;;) {
744
- $children = $j != $i ? array_slice($fragment, $j, $i - $j) : [];
745
- $parents = array_slice($fragment, 0, $j);
746
- $slice = end($parents);
747
 
748
  if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
749
  break;
@@ -765,30 +910,45 @@ class Compiler
765
  */
766
  protected function combineSelectorSingle($base, $other)
767
  {
768
- $tag = [];
769
- $out = [];
770
- $wasTag = true;
 
 
 
 
 
 
 
 
771
 
772
- foreach ([$base, $other] as $single) {
773
  foreach ($single as $part) {
774
- if (preg_match('/^[\[.:#]/', $part)) {
775
  $out[] = $part;
776
  $wasTag = false;
777
- } elseif (preg_match('/^[^_-]/', $part)) {
 
 
 
778
  $tag[] = $part;
779
  $wasTag = true;
780
  } elseif ($wasTag) {
781
- $tag[count($tag) - 1] .= $part;
782
  } else {
783
- $out[count($out) - 1] .= $part;
784
  }
 
785
  }
786
  }
787
 
788
- if (count($tag)) {
789
  array_unshift($out, $tag[0]);
790
  }
791
 
 
 
 
 
792
  return $out;
793
  }
794
 
@@ -820,7 +980,8 @@ class Compiler
820
  foreach ($media->children as $child) {
821
  $type = $child[0];
822
 
823
- if ($type !== Type::T_BLOCK &&
 
824
  $type !== Type::T_MEDIA &&
825
  $type !== Type::T_DIRECTIVE &&
826
  $type !== Type::T_IMPORT
@@ -831,17 +992,18 @@ class Compiler
831
  }
832
 
833
  if ($needsWrap) {
834
- $wrapped = new Block;
835
- $wrapped->sourceName = $media->sourceName;
836
- $wrapped->sourceIndex = $media->sourceIndex;
837
- $wrapped->sourceLine = $media->sourceLine;
838
  $wrapped->sourceColumn = $media->sourceColumn;
839
- $wrapped->selectors = [];
840
- $wrapped->comments = [];
841
- $wrapped->parent = $media;
842
- $wrapped->children = $media->children;
843
 
844
  $media->children = [[Type::T_BLOCK, $wrapped]];
 
845
  if (isset($this->lineNumberStyle)) {
846
  $annotation = $this->makeOutputBlock(Type::T_COMMENT);
847
  $annotation->depth = 0;
@@ -898,23 +1060,61 @@ class Compiler
898
  /**
899
  * Compile directive
900
  *
901
- * @param \ScssPhp\ScssPhp\Block $block
 
902
  */
903
- protected function compileDirective(Block $block)
904
  {
905
- $s = '@' . $block->name;
 
 
906
 
907
- if (! empty($block->value)) {
908
- $s .= ' ' . $this->compileValue($block->value);
909
- }
 
 
 
 
 
910
 
911
- if ($block->name === 'keyframes' || substr($block->name, -10) === '-keyframes') {
912
- $this->compileKeyframeBlock($block, [$s]);
 
 
 
913
  } else {
914
- $this->compileNestedBlock($block, [$s]);
 
 
 
 
 
 
 
 
 
 
 
915
  }
916
  }
917
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
918
  /**
919
  * Compile at-root
920
  *
@@ -928,7 +1128,7 @@ class Compiler
928
 
929
  // wrap inline selector
930
  if ($block->selector) {
931
- $wrapped = new Block;
932
  $wrapped->sourceName = $block->sourceName;
933
  $wrapped->sourceIndex = $block->sourceIndex;
934
  $wrapped->sourceLine = $block->sourceLine;
@@ -945,7 +1145,9 @@ class Compiler
945
 
946
  $selfParent = $block->selfParent;
947
 
948
- if (! $block->selfParent->selectors && isset($block->parent) && $block->parent &&
 
 
949
  isset($block->parent->selectors) && $block->parent->selectors
950
  ) {
951
  $selfParent = $block->parent;
@@ -978,6 +1180,7 @@ class Compiler
978
  protected function filterScopeWithWithout($scope, $with, $without)
979
  {
980
  $filteredScopes = [];
 
981
 
982
  if ($scope->type === TYPE::T_ROOT) {
983
  return $scope;
@@ -985,6 +1188,7 @@ class Compiler
985
 
986
  // start from the root
987
  while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) {
 
988
  $scope = $scope->parent;
989
  }
990
 
@@ -996,8 +1200,8 @@ class Compiler
996
  if ($this->isWith($scope, $with, $without)) {
997
  $s = clone $scope;
998
  $s->children = [];
999
- $s->lines = [];
1000
- $s->parent = null;
1001
 
1002
  if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) {
1003
  $s->selectors = [];
@@ -1006,14 +1210,16 @@ class Compiler
1006
  $filteredScopes[] = $s;
1007
  }
1008
 
1009
- if ($scope->children) {
 
 
1010
  $scope = end($scope->children);
1011
  } else {
1012
  $scope = null;
1013
  }
1014
  }
1015
 
1016
- if (! count($filteredScopes)) {
1017
  return $this->rootBlock;
1018
  }
1019
 
@@ -1024,11 +1230,12 @@ class Compiler
1024
 
1025
  $p = &$newScope;
1026
 
1027
- while (count($filteredScopes)) {
1028
  $s = array_shift($filteredScopes);
1029
  $s->parent = $p;
1030
- $p->children[] = &$s;
1031
- $p = $s;
 
1032
  }
1033
 
1034
  return $newScope;
@@ -1045,7 +1252,7 @@ class Compiler
1045
  */
1046
  protected function completeScope($scope, $previousScope)
1047
  {
1048
- if (! $scope->type && (! $scope->selectors || ! count($scope->selectors)) && count($scope->lines)) {
1049
  $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth);
1050
  }
1051
 
@@ -1097,6 +1304,17 @@ class Compiler
1097
  $without = ['rule' => true];
1098
 
1099
  if ($withCondition) {
 
 
 
 
 
 
 
 
 
 
 
1100
  if ($this->libMapHasKey([$withCondition, static::$with])) {
1101
  $without = []; // cancel the default
1102
  $list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
@@ -1126,7 +1344,7 @@ class Compiler
1126
  /**
1127
  * Filter env stack
1128
  *
1129
- * @param array $envs
1130
  * @param array $with
1131
  * @param array $without
1132
  *
@@ -1139,8 +1357,9 @@ class Compiler
1139
  foreach ($envs as $e) {
1140
  if ($e->block && ! $this->isWith($e->block, $with, $without)) {
1141
  $ec = clone $e;
1142
- $ec->block = null;
1143
  $ec->selectors = [];
 
1144
  $filtered[] = $ec;
1145
  } else {
1146
  $filtered[] = $e;
@@ -1168,17 +1387,27 @@ class Compiler
1168
 
1169
  if ($block->type === Type::T_DIRECTIVE) {
1170
  if (isset($block->name)) {
1171
- return $this->testWithWithout($block->name, $with, $without);
1172
- }
1173
- elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
1174
  return $this->testWithWithout($m[1], $with, $without);
1175
- }
1176
- else {
1177
  return $this->testWithWithout('???', $with, $without);
1178
  }
1179
  }
1180
- }
1181
- elseif (isset($block->selectors)) {
 
 
 
 
 
 
 
 
 
 
 
 
1182
  return $this->testWithWithout('rule', $with, $without);
1183
  }
1184
 
@@ -1191,13 +1420,14 @@ class Compiler
1191
  * @param string $what
1192
  * @param array $with
1193
  * @param array $without
1194
- * @return bool
 
1195
  * true if the block should be kept, false to reject
1196
  */
1197
- protected function testWithWithout($what, $with, $without) {
1198
-
1199
  // if without, reject only if in the list (or 'all' is in the list)
1200
- if (count($without)) {
1201
  return (isset($without[$what]) || isset($without['all'])) ? false : true;
1202
  }
1203
 
@@ -1237,8 +1467,8 @@ class Compiler
1237
  /**
1238
  * Compile nested properties lines
1239
  *
1240
- * @param \ScssPhp\ScssPhp\Block $block
1241
- * @param OutputBlock $out
1242
  */
1243
  protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out)
1244
  {
@@ -1263,6 +1493,7 @@ class Compiler
1263
  array_unshift($child[1]->prefix[2], $prefix);
1264
  break;
1265
  }
 
1266
  $this->compileChild($child, $nested);
1267
  }
1268
  }
@@ -1282,7 +1513,7 @@ class Compiler
1282
 
1283
  // wrap assign children in a block
1284
  // except for @font-face
1285
- if ($block->type !== Type::T_DIRECTIVE || $block->name !== "font-face") {
1286
  // need wrapping?
1287
  $needWrapping = false;
1288
 
@@ -1294,16 +1525,16 @@ class Compiler
1294
  }
1295
 
1296
  if ($needWrapping) {
1297
- $wrapped = new Block;
1298
- $wrapped->sourceName = $block->sourceName;
1299
- $wrapped->sourceIndex = $block->sourceIndex;
1300
- $wrapped->sourceLine = $block->sourceLine;
1301
  $wrapped->sourceColumn = $block->sourceColumn;
1302
- $wrapped->selectors = [];
1303
- $wrapped->comments = [];
1304
- $wrapped->parent = $block;
1305
- $wrapped->children = $block->children;
1306
- $wrapped->selfParent = $block->selfParent;
1307
 
1308
  $block->children = [[Type::T_BLOCK, $wrapped]];
1309
  }
@@ -1341,7 +1572,7 @@ class Compiler
1341
 
1342
  $out = $this->makeOutputBlock(null);
1343
 
1344
- if (isset($this->lineNumberStyle) && count($env->selectors) && count($block->children)) {
1345
  $annotation = $this->makeOutputBlock(Type::T_COMMENT);
1346
  $annotation->depth = 0;
1347
 
@@ -1367,7 +1598,7 @@ class Compiler
1367
 
1368
  $this->scope->children[] = $out;
1369
 
1370
- if (count($block->children)) {
1371
  $out->selectors = $this->multiplySelectors($env, $block->selfParent);
1372
 
1373
  // propagate selfParent to the children where they still can be useful
@@ -1380,17 +1611,52 @@ class Compiler
1380
 
1381
  $this->compileChildrenNoReturn($block->children, $out, $block->selfParent);
1382
 
1383
- // and revert for the following childs of the same block
1384
  if ($selfParentSelectors) {
1385
  $block->selfParent->selectors = $selfParentSelectors;
1386
  }
1387
  }
1388
 
1389
- $this->formatter->stripSemicolon($out->lines);
1390
-
1391
  $this->popEnv();
1392
  }
1393
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1394
  /**
1395
  * Compile root level comment
1396
  *
@@ -1399,7 +1665,7 @@ class Compiler
1399
  protected function compileComment($block)
1400
  {
1401
  $out = $this->makeOutputBlock(Type::T_COMMENT);
1402
- $out->lines[] = is_string($block[1]) ? $block[1] : $this->compileValue($block[1]);
1403
 
1404
  $this->scope->children[] = $out;
1405
  }
@@ -1419,9 +1685,9 @@ class Compiler
1419
 
1420
  // after evaluating interpolates, we might need a second pass
1421
  if ($this->shouldEvaluate) {
1422
- $selectors = $this->revertSelfSelector($selectors);
1423
- $buffer = $this->collapseSelectors($selectors);
1424
- $parser = $this->parserFactory(__METHOD__);
1425
 
1426
  if ($parser->parseSelector($buffer, $newSelectors)) {
1427
  $selectors = array_map([$this, 'evalSelector'], $newSelectors);
@@ -1453,14 +1719,15 @@ class Compiler
1453
  protected function evalSelectorPart($part)
1454
  {
1455
  foreach ($part as &$p) {
1456
- if (is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
1457
  $p = $this->compileValue($p);
1458
 
1459
  // force re-evaluation
1460
  if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
1461
  $this->shouldEvaluate = true;
1462
  }
1463
- } elseif (is_string($p) && strlen($p) >= 2 &&
 
1464
  ($first = $p[0]) && ($first === '"' || $first === "'") &&
1465
  substr($p, -1) === $first
1466
  ) {
@@ -1500,14 +1767,15 @@ class Compiler
1500
  );
1501
 
1502
  if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) {
1503
- if (count($output)) {
1504
- $output[count($output) - 1] .= ' ' . $compound;
1505
  } else {
1506
  $output[] = $compound;
1507
  }
 
1508
  $glueNext = true;
1509
  } elseif ($glueNext) {
1510
- $output[count($output) - 1] .= ' ' . $compound;
1511
  $glueNext = false;
1512
  } else {
1513
  $output[] = $compound;
@@ -1518,6 +1786,7 @@ class Compiler
1518
  foreach ($output as &$o) {
1519
  $o = [Type::T_STRING, '', [$o]];
1520
  }
 
1521
  $output = [Type::T_LIST, ' ', $output];
1522
  } else {
1523
  $output = implode(' ', $output);
@@ -1542,14 +1811,18 @@ class Compiler
1542
  *
1543
  * @return array
1544
  */
1545
- protected function revertSelfSelector($selectors)
1546
  {
1547
  foreach ($selectors as &$part) {
1548
- if (is_array($part)) {
1549
  if ($part === [Type::T_SELF]) {
1550
- $part = '&';
 
 
 
 
1551
  } else {
1552
- $part = $this->revertSelfSelector($part);
1553
  }
1554
  }
1555
  }
@@ -1569,18 +1842,19 @@ class Compiler
1569
  $joined = [];
1570
 
1571
  foreach ($single as $part) {
1572
- if (empty($joined) ||
1573
- ! is_string($part) ||
 
1574
  preg_match('/[\[.:#%]/', $part)
1575
  ) {
1576
  $joined[] = $part;
1577
  continue;
1578
  }
1579
 
1580
- if (is_array(end($joined))) {
1581
  $joined[] = $part;
1582
  } else {
1583
- $joined[count($joined) - 1] .= $part;
1584
  }
1585
  }
1586
 
@@ -1596,7 +1870,7 @@ class Compiler
1596
  */
1597
  protected function compileSelector($selector)
1598
  {
1599
- if (! is_array($selector)) {
1600
  return $selector; // media and the like
1601
  }
1602
 
@@ -1619,7 +1893,7 @@ class Compiler
1619
  protected function compileSelectorPart($piece)
1620
  {
1621
  foreach ($piece as &$p) {
1622
- if (! is_array($p)) {
1623
  continue;
1624
  }
1625
 
@@ -1646,13 +1920,13 @@ class Compiler
1646
  */
1647
  protected function hasSelectorPlaceholder($selector)
1648
  {
1649
- if (! is_array($selector)) {
1650
  return false;
1651
  }
1652
 
1653
  foreach ($selector as $parts) {
1654
  foreach ($parts as $part) {
1655
- if (strlen($part) && '%' === $part[0]) {
1656
  return true;
1657
  }
1658
  }
@@ -1671,11 +1945,12 @@ class Compiler
1671
  ];
1672
 
1673
  // infinite calling loop
1674
- if (count($this->callStack) > 25000) {
1675
  // not displayed but you can var_dump it to deep debug
1676
  $msg = $this->callStackMessage(true, 100);
1677
- $msg = "Infinite calling loop";
1678
- $this->throwError($msg);
 
1679
  }
1680
  }
1681
 
@@ -1701,6 +1976,8 @@ class Compiler
1701
  $ret = $this->compileChild($stm, $out);
1702
 
1703
  if (isset($ret)) {
 
 
1704
  return $ret;
1705
  }
1706
  }
@@ -1725,11 +2002,11 @@ class Compiler
1725
  $this->pushCallStack($traceName);
1726
 
1727
  foreach ($stms as $stm) {
1728
- if ($selfParent && isset($stm[1]) && is_object($stm[1]) && $stm[1] instanceof Block) {
1729
  $stm[1]->selfParent = $selfParent;
1730
  $ret = $this->compileChild($stm, $out);
1731
  $stm[1]->selfParent = null;
1732
- } elseif ($selfParent && $stm[0] === TYPE::T_INCLUDE) {
1733
  $stm['selfParent'] = $selfParent;
1734
  $ret = $this->compileChild($stm, $out);
1735
  unset($stm['selfParent']);
@@ -1738,9 +2015,7 @@ class Compiler
1738
  }
1739
 
1740
  if (isset($ret)) {
1741
- $this->throwError('@return may only be used within a function');
1742
-
1743
- return;
1744
  }
1745
  }
1746
 
@@ -1758,16 +2033,20 @@ class Compiler
1758
  protected function evaluateMediaQuery($queryList)
1759
  {
1760
  static $parser = null;
 
1761
  $outQueryList = [];
 
1762
  foreach ($queryList as $kql => $query) {
1763
  $shouldReparse = false;
 
1764
  foreach ($query as $kq => $q) {
1765
- for ($i = 1; $i < count($q); $i++) {
1766
  $value = $this->compileValue($q[$i]);
1767
 
1768
  // the parser had no mean to know if media type or expression if it was an interpolation
1769
  // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type
1770
- if ($q[0] == Type::T_MEDIA_TYPE &&
 
1771
  (strpos($value, '(') !== false ||
1772
  strpos($value, ')') !== false ||
1773
  strpos($value, ':') !== false ||
@@ -1779,24 +2058,31 @@ class Compiler
1779
  $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value];
1780
  }
1781
  }
 
1782
  if ($shouldReparse) {
1783
- if (is_null($parser)) {
1784
  $parser = $this->parserFactory(__METHOD__);
1785
  }
 
1786
  $queryString = $this->compileMediaQuery([$queryList[$kql]]);
1787
  $queryString = reset($queryString);
 
1788
  if (strpos($queryString, '@media ') === 0) {
1789
  $queryString = substr($queryString, 7);
1790
  $queries = [];
 
1791
  if ($parser->parseMediaQueryList($queryString, $queries)) {
1792
  $queries = $this->evaluateMediaQuery($queries[2]);
1793
- while (count($queries)) {
 
1794
  $outQueryList[] = array_shift($queries);
1795
  }
 
1796
  continue;
1797
  }
1798
  }
1799
  }
 
1800
  $outQueryList[] = $queryList[$kql];
1801
  }
1802
 
@@ -1812,10 +2098,10 @@ class Compiler
1812
  */
1813
  protected function compileMediaQuery($queryList)
1814
  {
1815
- $start = '@media ';
1816
  $default = trim($start);
1817
- $out = [];
1818
- $current = "";
1819
 
1820
  foreach ($queryList as $query) {
1821
  $type = null;
@@ -1833,16 +2119,17 @@ class Compiler
1833
  foreach ($query as $q) {
1834
  switch ($q[0]) {
1835
  case Type::T_MEDIA_TYPE:
1836
- $newType = array_map([$this, 'compileValue'], array_slice($q, 1));
 
1837
  // combining not and anything else than media type is too risky and should be avoided
1838
  if (! $mediaTypeOnly) {
1839
- if (in_array(Type::T_NOT, $newType) || ($type && in_array(Type::T_NOT, $type) )) {
1840
  if ($type) {
1841
  array_unshift($parts, implode(' ', array_filter($type)));
1842
  }
1843
 
1844
  if (! empty($parts)) {
1845
- if (strlen($current)) {
1846
  $current .= $this->formatter->tagSeparator;
1847
  }
1848
 
@@ -1853,9 +2140,9 @@ class Compiler
1853
  $out[] = $start . $current;
1854
  }
1855
 
1856
- $current = "";
1857
- $type = null;
1858
- $parts = [];
1859
  }
1860
  }
1861
 
@@ -1905,7 +2192,7 @@ class Compiler
1905
  }
1906
 
1907
  if (! empty($parts)) {
1908
- if (strlen($current)) {
1909
  $current .= $this->formatter->tagSeparator;
1910
  }
1911
 
@@ -1987,23 +2274,19 @@ class Compiler
1987
  return $type1;
1988
  }
1989
 
1990
- $m1 = '';
1991
- $t1 = '';
1992
-
1993
- if (count($type1) > 1) {
1994
- $m1= strtolower($type1[0]);
1995
- $t1= strtolower($type1[1]);
1996
  } else {
 
1997
  $t1 = strtolower($type1[0]);
1998
  }
1999
 
2000
- $m2 = '';
2001
- $t2 = '';
2002
-
2003
- if (count($type2) > 1) {
2004
  $m2 = strtolower($type2[0]);
2005
  $t2 = strtolower($type2[1]);
2006
  } else {
 
2007
  $t2 = strtolower($type2[0]);
2008
  }
2009
 
@@ -2032,7 +2315,7 @@ class Compiler
2032
  }
2033
 
2034
  // t1 == t2, neither m1 nor m2 are "not"
2035
- return [empty($m1)? $m2 : $m1, $t1];
2036
  }
2037
 
2038
  /**
@@ -2049,8 +2332,8 @@ class Compiler
2049
  if ($rawPath[0] === Type::T_STRING) {
2050
  $path = $this->compileStringContent($rawPath);
2051
 
2052
- if ($path = $this->findImport($path)) {
2053
- if (! $once || ! in_array($path, $this->importedFiles)) {
2054
  $this->importFile($path, $out);
2055
  $this->importedFiles[] = $path;
2056
  }
@@ -2058,31 +2341,84 @@ class Compiler
2058
  return true;
2059
  }
2060
 
 
 
2061
  return false;
2062
  }
2063
 
2064
  if ($rawPath[0] === Type::T_LIST) {
2065
  // handle a list of strings
2066
- if (count($rawPath[2]) === 0) {
2067
  return false;
2068
  }
2069
 
2070
  foreach ($rawPath[2] as $path) {
2071
  if ($path[0] !== Type::T_STRING) {
 
 
2072
  return false;
2073
  }
2074
  }
2075
 
2076
  foreach ($rawPath[2] as $path) {
2077
- $this->compileImport($path, $out);
2078
  }
2079
 
2080
  return true;
2081
  }
2082
 
 
 
2083
  return false;
2084
  }
2085
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2086
 
2087
  /**
2088
  * Append a root directive like @import or @charset as near as the possible from the source code
@@ -2102,8 +2438,8 @@ class Compiler
2102
 
2103
  $i = 0;
2104
 
2105
- while ($i < count($root->children)) {
2106
- if (! isset($root->children[$i]->type) || ! in_array($root->children[$i]->type, $allowed)) {
2107
  break;
2108
  }
2109
 
@@ -2113,55 +2449,51 @@ class Compiler
2113
  // remove incompatible children from the bottom of the list
2114
  $saveChildren = [];
2115
 
2116
- while ($i < count($root->children)) {
2117
  $saveChildren[] = array_pop($root->children);
2118
  }
2119
 
2120
  // insert the directive as a comment
2121
  $child = $this->makeOutputBlock(Type::T_COMMENT);
2122
- $child->lines[] = $line;
2123
- $child->sourceName = $this->sourceNames[$this->sourceIndex];
2124
- $child->sourceLine = $this->sourceLine;
2125
  $child->sourceColumn = $this->sourceColumn;
2126
 
2127
  $root->children[] = $child;
2128
 
2129
  // repush children
2130
- while (count($saveChildren)) {
2131
  $root->children[] = array_pop($saveChildren);
2132
  }
2133
  }
2134
 
2135
  /**
2136
- * Append lines to the courrent output block:
2137
  * directly to the block or through a child if necessary
2138
  *
2139
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2140
  * @param string $type
2141
- * @param string $line
2142
  */
2143
  protected function appendOutputLine(OutputBlock $out, $type, $line)
2144
  {
2145
  $outWrite = &$out;
2146
 
2147
- if ($type === Type::T_COMMENT) {
2148
- $parent = $out->parent;
2149
-
2150
- if (end($parent->children) !== $out) {
2151
- $outWrite = &$parent->children[count($parent->children)-1];
2152
- }
2153
- }
2154
-
2155
  // check if it's a flat output or not
2156
- if (count($out->children)) {
2157
- $lastChild = &$out->children[count($out->children) -1];
2158
 
2159
- if ($lastChild->depth === $out->depth && is_null($lastChild->selectors) && ! count($lastChild->children)) {
 
 
 
 
2160
  $outWrite = $lastChild;
2161
  } else {
2162
  $nextLines = $this->makeOutputBlock($type);
2163
  $nextLines->parent = $out;
2164
- $nextLines->depth = $out->depth;
2165
 
2166
  $out->children[] = $nextLines;
2167
  $outWrite = &$nextLines;
@@ -2182,16 +2514,17 @@ class Compiler
2182
  protected function compileChild($child, OutputBlock $out)
2183
  {
2184
  if (isset($child[Parser::SOURCE_LINE])) {
2185
- $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
2186
- $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
2187
  $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
2188
- } elseif (is_array($child) && isset($child[1]->sourceLine)) {
2189
- $this->sourceIndex = $child[1]->sourceIndex;
2190
- $this->sourceLine = $child[1]->sourceLine;
2191
  $this->sourceColumn = $child[1]->sourceColumn;
2192
  } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
2193
- $this->sourceLine = $out->sourceLine;
2194
- $this->sourceIndex = array_search($out->sourceName, $this->sourceNames);
 
2195
 
2196
  if ($this->sourceIndex === false) {
2197
  $this->sourceIndex = null;
@@ -2202,21 +2535,17 @@ class Compiler
2202
  case Type::T_SCSSPHP_IMPORT_ONCE:
2203
  $rawPath = $this->reduce($child[1]);
2204
 
2205
- if (! $this->compileImport($rawPath, $out, true)) {
2206
- $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
2207
- }
2208
  break;
2209
 
2210
  case Type::T_IMPORT:
2211
  $rawPath = $this->reduce($child[1]);
2212
 
2213
- if (! $this->compileImport($rawPath, $out)) {
2214
- $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
2215
- }
2216
  break;
2217
 
2218
  case Type::T_DIRECTIVE:
2219
- $this->compileDirective($child[1]);
2220
  break;
2221
 
2222
  case Type::T_AT_ROOT:
@@ -2238,13 +2567,37 @@ class Compiler
2238
  }
2239
  break;
2240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2241
  case Type::T_ASSIGN:
2242
  list(, $name, $value) = $child;
2243
 
2244
  if ($name[0] === Type::T_VARIABLE) {
2245
- $flags = isset($child[3]) ? $child[3] : [];
2246
- $isDefault = in_array('!default', $flags);
2247
- $isGlobal = in_array('!global', $flags);
2248
 
2249
  if ($isGlobal) {
2250
  $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
@@ -2252,7 +2605,7 @@ class Compiler
2252
  }
2253
 
2254
  $shouldSet = $isDefault &&
2255
- (($result = $this->get($name[1], false)) === null ||
2256
  $result === static::$null);
2257
 
2258
  if (! $isDefault || $shouldSet) {
@@ -2263,28 +2616,78 @@ class Compiler
2263
 
2264
  $compiledName = $this->compileValue($name);
2265
 
2266
- // handle shorthand syntax: size / line-height
2267
- if ($compiledName === 'font' || $compiledName === 'grid-row' || $compiledName === 'grid-column') {
2268
  if ($value[0] === Type::T_VARIABLE) {
2269
  // if the font value comes from variable, the content is already reduced
2270
  // (i.e., formulas were already calculated), so we need the original unreduced value
2271
  $value = $this->get($value[1], true, null, true);
2272
  }
2273
 
2274
- $fontValue=&$value;
 
 
 
 
 
 
 
 
 
 
 
2275
 
2276
- if ($value[0] === Type::T_LIST && $value[1]==',') {
2277
  // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica"
2278
  // we need to handle the first list element
2279
- $fontValue=&$value[2][0];
2280
  }
2281
 
2282
- if ($fontValue[0] === Type::T_EXPRESSION && $fontValue[1] === '/') {
2283
- $fontValue = $this->expToString($fontValue);
2284
- } elseif ($fontValue[0] === Type::T_LIST) {
2285
- foreach ($fontValue[2] as &$item) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2286
  if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
2287
- $item = $this->expToString($item);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2288
  }
2289
  }
2290
  }
@@ -2302,11 +2705,14 @@ class Compiler
2302
 
2303
  $compiledValue = $this->compileValue($value);
2304
 
2305
- $line = $this->formatter->property(
2306
- $compiledName,
2307
- $compiledValue
2308
- );
2309
- $this->appendOutputLine($out, Type::T_ASSIGN, $line);
 
 
 
2310
  break;
2311
 
2312
  case Type::T_COMMENT:
@@ -2315,25 +2721,33 @@ class Compiler
2315
  break;
2316
  }
2317
 
2318
- $this->appendOutputLine($out, Type::T_COMMENT, $child[1]);
 
2319
  break;
2320
 
2321
  case Type::T_MIXIN:
2322
  case Type::T_FUNCTION:
2323
  list(, $block) = $child;
2324
-
 
2325
  $this->set(static::$namespaces[$block->type] . $block->name, $block, true);
2326
  break;
2327
 
2328
  case Type::T_EXTEND:
2329
  foreach ($child[1] as $sel) {
 
2330
  $results = $this->evalSelectors([$sel]);
2331
 
2332
  foreach ($results as $result) {
2333
  // only use the first one
2334
  $result = current($result);
 
 
 
 
 
2335
 
2336
- $this->pushExtends($result, $out->selectors, $child);
2337
  }
2338
  }
2339
  break;
@@ -2346,7 +2760,8 @@ class Compiler
2346
  }
2347
 
2348
  foreach ($if->cases as $case) {
2349
- if ($case->type === Type::T_ELSE ||
 
2350
  $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))
2351
  ) {
2352
  return $this->compileChildren($case->children, $out);
@@ -2357,12 +2772,12 @@ class Compiler
2357
  case Type::T_EACH:
2358
  list(, $each) = $child;
2359
 
2360
- $list = $this->coerceList($this->reduce($each->list));
2361
 
2362
  $this->pushEnv();
2363
 
2364
  foreach ($list[2] as $item) {
2365
- if (count($each->vars) === 1) {
2366
  $this->set($each->vars[0], $item, true);
2367
  } else {
2368
  list(,, $values) = $this->coerceList($item);
@@ -2376,7 +2791,9 @@ class Compiler
2376
 
2377
  if ($ret) {
2378
  if ($ret[0] !== Type::T_CONTROL) {
 
2379
  $this->popEnv();
 
2380
 
2381
  return $ret;
2382
  }
@@ -2386,8 +2803,10 @@ class Compiler
2386
  }
2387
  }
2388
  }
2389
-
2390
  $this->popEnv();
 
 
2391
  break;
2392
 
2393
  case Type::T_WHILE:
@@ -2414,10 +2833,16 @@ class Compiler
2414
  $start = $this->reduce($for->start, true);
2415
  $end = $this->reduce($for->end, true);
2416
 
2417
- if (! ($start[2] == $end[2] || $end->unitless())) {
2418
- $this->throwError('Incompatible units: "%s" and "%s".', $start->unitStr(), $end->unitStr());
 
2419
 
2420
- break;
 
 
 
 
 
2421
  }
2422
 
2423
  $unit = $start[2];
@@ -2426,8 +2851,11 @@ class Compiler
2426
 
2427
  $d = $start < $end ? 1 : -1;
2428
 
 
 
2429
  for (;;) {
2430
- if ((! $for->until && $start - $d == $end) ||
 
2431
  ($for->until && $start == $end)
2432
  ) {
2433
  break;
@@ -2440,6 +2868,10 @@ class Compiler
2440
 
2441
  if ($ret) {
2442
  if ($ret[0] !== Type::T_CONTROL) {
 
 
 
 
2443
  return $ret;
2444
  }
2445
 
@@ -2448,6 +2880,11 @@ class Compiler
2448
  }
2449
  }
2450
  }
 
 
 
 
 
2451
  break;
2452
 
2453
  case Type::T_BREAK:
@@ -2465,13 +2902,12 @@ class Compiler
2465
 
2466
  case Type::T_INCLUDE:
2467
  // including a mixin
2468
- list(, $name, $argValues, $content) = $child;
2469
 
2470
  $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
2471
 
2472
  if (! $mixin) {
2473
- $this->throwError("Undefined mixin $name");
2474
- break;
2475
  }
2476
 
2477
  $callingScope = $this->getStoreEnv();
@@ -2480,9 +2916,6 @@ class Compiler
2480
  $this->pushEnv();
2481
  $this->env->depth--;
2482
 
2483
- $storeEnv = $this->storeEnv;
2484
- $this->storeEnv = $this->env;
2485
-
2486
  // Find the parent selectors in the env to be able to know what '&' refers to in the mixin
2487
  // and assign this fake parent to childs
2488
  $selfParent = null;
@@ -2497,7 +2930,7 @@ class Compiler
2497
  $parent->selectors = $parentSelectors;
2498
 
2499
  foreach ($mixin->children as $k => $child) {
2500
- if (isset($child[1]) && is_object($child[1]) && $child[1] instanceof Block) {
2501
  $mixin->children[$k][1]->parent = $parent;
2502
  }
2503
  }
@@ -2508,39 +2941,64 @@ class Compiler
2508
  // i.e., recursive @include of the same mixin
2509
  if (isset($content)) {
2510
  $copyContent = clone $content;
2511
- $copyContent->scope = $callingScope;
2512
 
2513
  $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
2514
  } else {
2515
  $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env);
2516
  }
2517
 
 
 
 
 
 
 
 
2518
  if (isset($mixin->args)) {
2519
  $this->applyArguments($mixin->args, $argValues);
2520
  }
2521
 
2522
  $this->env->marker = 'mixin';
2523
 
2524
- $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . " " . $name);
 
 
 
 
2525
 
2526
- $this->storeEnv = $storeEnv;
2527
 
2528
  $this->popEnv();
2529
  break;
2530
 
2531
  case Type::T_MIXIN_CONTENT:
2532
- $env = isset($this->storeEnv) ? $this->storeEnv : $this->env;
2533
- $content = $this->get(static::$namespaces['special'] . 'content', false, $env);
 
 
2534
 
2535
  if (! $content) {
2536
- $content = new \stdClass();
2537
- $content->scope = new \stdClass();
2538
- $content->children = $env->parent->block->children;
2539
  break;
2540
  }
2541
 
2542
  $storeEnv = $this->storeEnv;
 
 
 
 
 
 
 
 
 
2543
  $this->storeEnv = $content->scope;
 
 
 
 
 
 
2544
  $this->compileChildrenNoReturn($content->children, $out);
2545
 
2546
  $this->storeEnv = $storeEnv;
@@ -2550,35 +3008,36 @@ class Compiler
2550
  list(, $value) = $child;
2551
 
2552
  $fname = $this->sourceNames[$this->sourceIndex];
2553
- $line = $this->sourceLine;
2554
- $value = $this->compileValue($this->reduce($value, true));
2555
- fwrite($this->stderr, "File $fname on line $line DEBUG: $value\n");
 
2556
  break;
2557
 
2558
  case Type::T_WARN:
2559
  list(, $value) = $child;
2560
 
2561
  $fname = $this->sourceNames[$this->sourceIndex];
2562
- $line = $this->sourceLine;
2563
- $value = $this->compileValue($this->reduce($value, true));
2564
- fwrite($this->stderr, "File $fname on line $line WARN: $value\n");
 
2565
  break;
2566
 
2567
  case Type::T_ERROR:
2568
  list(, $value) = $child;
2569
 
2570
  $fname = $this->sourceNames[$this->sourceIndex];
2571
- $line = $this->sourceLine;
2572
  $value = $this->compileValue($this->reduce($value, true));
2573
- $this->throwError("File $fname on line $line ERROR: $value\n");
2574
- break;
2575
 
2576
  case Type::T_CONTROL:
2577
- $this->throwError('@break/@continue not permitted in this scope');
2578
- break;
2579
 
2580
  default:
2581
- $this->throwError("unknown child type: $child[0]");
2582
  }
2583
  }
2584
 
@@ -2586,14 +3045,21 @@ class Compiler
2586
  * Reduce expression to string
2587
  *
2588
  * @param array $exp
 
2589
  *
2590
  * @return array
2591
  */
2592
- protected function expToString($exp)
2593
  {
2594
- list(, $op, $left, $right, /* $inParens */, $whiteLeft, $whiteRight) = $exp;
2595
 
2596
- $content = [$this->reduce($left)];
 
 
 
 
 
 
2597
 
2598
  if ($whiteLeft) {
2599
  $content[] = ' ';
@@ -2607,6 +3073,10 @@ class Compiler
2607
 
2608
  $content[] = $this->reduce($right);
2609
 
 
 
 
 
2610
  return [Type::T_STRING, '', $content];
2611
  }
2612
 
@@ -2664,10 +3134,13 @@ class Compiler
2664
  * @param array $value
2665
  * @param boolean $inExp
2666
  *
2667
- * @return array|\ScssPhp\ScssPhp\Node\Number
2668
  */
2669
  protected function reduce($value, $inExp = false)
2670
  {
 
 
 
2671
 
2672
  switch ($value[0]) {
2673
  case Type::T_EXPRESSION:
@@ -2683,16 +3156,16 @@ class Compiler
2683
  }
2684
 
2685
  // special case: looks like css shorthand
2686
- if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2]) &&
 
2687
  (($right[0] !== Type::T_NUMBER && $right[2] != '') ||
2688
  ($right[0] === Type::T_NUMBER && ! $right->unitless()))
2689
  ) {
2690
  return $this->expToString($value);
2691
  }
2692
 
2693
- $left = $this->coerceForExpression($left);
2694
  $right = $this->coerceForExpression($right);
2695
-
2696
  $ltype = $left[0];
2697
  $rtype = $right[0];
2698
 
@@ -2706,17 +3179,19 @@ class Compiler
2706
  // 3. op[op name]
2707
  $fn = "op${ucOpName}${ucLType}${ucRType}";
2708
 
2709
- if (is_callable([$this, $fn]) ||
 
2710
  (($fn = "op${ucLType}${ucRType}") &&
2711
- is_callable([$this, $fn]) &&
2712
  $passOp = true) ||
2713
  (($fn = "op${ucOpName}") &&
2714
- is_callable([$this, $fn]) &&
2715
  $genOp = true)
2716
  ) {
2717
  $coerceUnit = false;
2718
 
2719
- if (! isset($genOp) &&
 
2720
  $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER
2721
  ) {
2722
  $coerceUnit = true;
@@ -2746,9 +3221,14 @@ class Compiler
2746
  $targetUnit = $left->unitless() ? $right[2] : $left[2];
2747
  }
2748
 
2749
- if (! $left->unitless() && ! $right->unitless()) {
 
 
 
2750
  $left = $left->normalize();
2751
  $right = $right->normalize();
 
 
2752
  }
2753
  }
2754
 
@@ -2824,7 +3304,7 @@ class Compiler
2824
 
2825
  case Type::T_STRING:
2826
  foreach ($value[2] as &$item) {
2827
- if (is_array($item) || $item instanceof \ArrayAccess) {
2828
  $item = $this->reduce($item);
2829
  }
2830
  }
@@ -2833,6 +3313,7 @@ class Compiler
2833
 
2834
  case Type::T_INTERPOLATE:
2835
  $value[1] = $this->reduce($value[1]);
 
2836
  if ($inExp) {
2837
  return $value[1];
2838
  }
@@ -2843,8 +3324,10 @@ class Compiler
2843
  return $this->fncall($value[1], $value[2]);
2844
 
2845
  case Type::T_SELF:
2846
- $selfSelector = $this->multiplySelectors($this->env);
 
2847
  $selfSelector = $this->collapseSelectors($selfSelector, true);
 
2848
  return $selfSelector;
2849
 
2850
  default:
@@ -2860,30 +3343,233 @@ class Compiler
2860
  *
2861
  * @return array|null
2862
  */
2863
- protected function fncall($name, $argValues)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2864
  {
2865
  // SCSS @function
2866
- if ($this->callScssFunction($name, $argValues, $returnValue)) {
2867
- return $returnValue;
 
 
 
 
2868
  }
2869
 
2870
  // native PHP functions
2871
- if ($this->callNativeFunction($name, $argValues, $returnValue)) {
2872
- return $returnValue;
 
 
 
 
 
 
 
 
2873
  }
2874
 
2875
- // for CSS functions, simply flatten the arguments into a list
2876
- $listArgs = [];
 
2877
 
2878
- foreach ((array) $argValues as $arg) {
2879
- if (empty($arg[0])) {
2880
- $listArgs[] = $this->reduce($arg[1]);
2881
- }
2882
  }
2883
 
2884
- return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', $listArgs]];
2885
  }
2886
 
 
2887
  /**
2888
  * Normalize name
2889
  *
@@ -2919,6 +3605,10 @@ class Compiler
2919
  $value[2][$key] = $this->normalizeValue($item);
2920
  }
2921
 
 
 
 
 
2922
  return $value;
2923
 
2924
  case Type::T_STRING:
@@ -2985,7 +3675,7 @@ class Compiler
2985
  protected function opDivNumberNumber($left, $right)
2986
  {
2987
  if ($right[1] == 0) {
2988
- return [Type::T_STRING, '', [$left[1] . $left[2] . '/' . $right[1] . $right[2]]];
2989
  }
2990
 
2991
  return new Node\Number($left[1] / $right[1], $left[2]);
@@ -3001,6 +3691,10 @@ class Compiler
3001
  */
3002
  protected function opModNumberNumber($left, $right)
3003
  {
 
 
 
 
3004
  return new Node\Number($left[1] % $right[1], $left[2]);
3005
  }
3006
 
@@ -3124,13 +3818,16 @@ class Compiler
3124
  break;
3125
 
3126
  case '%':
 
 
 
 
3127
  $out[] = $lval % $rval;
3128
  break;
3129
 
3130
  case '/':
3131
  if ($rval == 0) {
3132
- $this->throwError("color: Can't divide by zero");
3133
- break 2;
3134
  }
3135
 
3136
  $out[] = (int) ($lval / $rval);
@@ -3143,8 +3840,7 @@ class Compiler
3143
  return $this->opNeq($left, $right);
3144
 
3145
  default:
3146
- $this->throwError("color: unknown op $op");
3147
- break 2;
3148
  }
3149
  }
3150
 
@@ -3335,7 +4031,7 @@ class Compiler
3335
  *
3336
  * @param array $value
3337
  *
3338
- * @return string
3339
  */
3340
  public function compileValue($value)
3341
  {
@@ -3352,14 +4048,38 @@ class Compiler
3352
  // [4] - optional alpha component
3353
  list(, $r, $g, $b) = $value;
3354
 
3355
- $r = round($r);
3356
- $g = round($g);
3357
- $b = round($b);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3358
 
3359
- if (count($value) === 5 && $value[4] !== 1) { // rgba
3360
- $a = new Node\Number($value[4], '');
3361
 
3362
- return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
 
3363
  }
3364
 
3365
  $h = sprintf('#%02x%02x%02x', $r, $g, $b);
@@ -3375,13 +4095,36 @@ class Compiler
3375
  return $value->output($this);
3376
 
3377
  case Type::T_STRING:
3378
- return $value[1] . $this->compileStringContent($value) . $value[1];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3379
 
3380
  case Type::T_FUNCTION:
3381
  $args = ! empty($value[2]) ? $this->compileValue($value[2]) : '';
3382
 
3383
  return "$value[1]($args)";
3384
 
 
 
 
 
 
3385
  case Type::T_LIST:
3386
  $value = $this->extractInterpolation($value);
3387
 
@@ -3390,9 +4133,30 @@ class Compiler
3390
  }
3391
 
3392
  list(, $delim, $items) = $value;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3393
 
3394
  if ($delim !== ' ') {
3395
- $delim .= ' ';
3396
  }
3397
 
3398
  $filtered = [];
@@ -3402,17 +4166,23 @@ class Compiler
3402
  continue;
3403
  }
3404
 
3405
- $filtered[] = $this->compileValue($item);
 
 
 
 
 
 
3406
  }
3407
 
3408
- return implode("$delim", $filtered);
3409
 
3410
  case Type::T_MAP:
3411
- $keys = $value[1];
3412
- $values = $value[2];
3413
  $filtered = [];
3414
 
3415
- for ($i = 0, $s = count($keys); $i < $s; $i++) {
3416
  $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
3417
  }
3418
 
@@ -3427,11 +4197,24 @@ class Compiler
3427
  list(, $interpolate, $left, $right) = $value;
3428
  list(,, $whiteLeft, $whiteRight) = $interpolate;
3429
 
3430
- $left = count($left[2]) > 0 ?
3431
- $this->compileValue($left) . $whiteLeft : '';
 
 
 
 
 
 
 
 
 
 
 
 
 
3432
 
3433
- $right = count($right[2]) > 0 ?
3434
- $whiteRight . $this->compileValue($right) : '';
3435
 
3436
  return $left . $this->compileValue($interpolate) . $right;
3437
 
@@ -3461,6 +4244,7 @@ class Compiler
3461
  }
3462
 
3463
  $temp = $this->compileValue([Type::T_KEYWORD, $item]);
 
3464
  if ($temp[0] === Type::T_STRING) {
3465
  $filtered[] = $this->compileStringContent($temp);
3466
  } elseif ($temp[0] === Type::T_KEYWORD) {
@@ -3486,8 +4270,29 @@ class Compiler
3486
  case Type::T_NULL:
3487
  return 'null';
3488
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3489
  default:
3490
- $this->throwError("unknown value type: ".json_encode($value));
3491
  }
3492
  }
3493
 
@@ -3515,7 +4320,7 @@ class Compiler
3515
  $parts = [];
3516
 
3517
  foreach ($string[2] as $part) {
3518
- if (is_array($part) || $part instanceof \ArrayAccess) {
3519
  $parts[] = $this->compileValue($part);
3520
  } else {
3521
  $parts[] = $part;
@@ -3538,8 +4343,8 @@ class Compiler
3538
 
3539
  foreach ($items as $i => $item) {
3540
  if ($item[0] === Type::T_INTERPOLATE) {
3541
- $before = [Type::T_LIST, $list[1], array_slice($items, 0, $i)];
3542
- $after = [Type::T_LIST, $list[1], array_slice($items, $i + 1)];
3543
 
3544
  return [Type::T_INTERPOLATED, $item, $before, $after];
3545
  }
@@ -3564,7 +4369,7 @@ class Compiler
3564
 
3565
  $selfParentSelectors = null;
3566
 
3567
- if (! is_null($selfParent) && $selfParent->selectors) {
3568
  $selfParentSelectors = $this->evalSelectors($selfParent->selectors);
3569
  }
3570
 
@@ -3576,12 +4381,12 @@ class Compiler
3576
  $selectors = $env->selectors;
3577
 
3578
  do {
3579
- $stillHasSelf = false;
3580
  $prevSelectors = $selectors;
3581
- $selectors = [];
3582
 
3583
- foreach ($prevSelectors as $selector) {
3584
- foreach ($parentSelectors as $parent) {
3585
  if ($selfParentSelectors) {
3586
  foreach ($selfParentSelectors as $selfParent) {
3587
  // if no '&' in the selector, each call will give same result, only add once
@@ -3601,6 +4406,11 @@ class Compiler
3601
 
3602
  $selectors = array_values($selectors);
3603
 
 
 
 
 
 
3604
  return $selectors;
3605
  }
3606
 
@@ -3609,7 +4419,7 @@ class Compiler
3609
  *
3610
  * @param array $parent
3611
  * @param array $child
3612
- * @param boolean &$stillHasSelf
3613
  * @param array $selfParentSelectors
3614
 
3615
  * @return array
@@ -3631,7 +4441,7 @@ class Compiler
3631
  if ($p === static::$selfSelector && ! $setSelf) {
3632
  $setSelf = true;
3633
 
3634
- if (is_null($selfParentSelectors)) {
3635
  $selfParentSelectors = $parent;
3636
  }
3637
 
@@ -3642,11 +4452,13 @@ class Compiler
3642
  }
3643
 
3644
  foreach ($parentPart as $pp) {
3645
- if (is_array($pp)) {
3646
  $flatten = [];
 
3647
  array_walk_recursive($pp, function ($a) use (&$flatten) {
3648
  $flatten[] = $a;
3649
  });
 
3650
  $pp = implode($flatten);
3651
  }
3652
 
@@ -3674,7 +4486,8 @@ class Compiler
3674
  */
3675
  protected function multiplyMedia(Environment $env = null, $childQueries = null)
3676
  {
3677
- if (! isset($env) ||
 
3678
  ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA
3679
  ) {
3680
  return $childQueries;
@@ -3690,12 +4503,14 @@ class Compiler
3690
  : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
3691
 
3692
  $store = [$this->env, $this->storeEnv];
3693
- $this->env = $env;
 
3694
  $this->storeEnv = null;
3695
- $parentQueries = $this->evaluateMediaQuery($parentQueries);
 
3696
  list($this->env, $this->storeEnv) = $store;
3697
 
3698
- if ($childQueries === null) {
3699
  $childQueries = $parentQueries;
3700
  } else {
3701
  $originalQueries = $childQueries;
@@ -3757,13 +4572,15 @@ class Compiler
3757
  */
3758
  protected function pushEnv(Block $block = null)
3759
  {
3760
- $env = new Environment;
3761
  $env->parent = $this->env;
 
3762
  $env->store = [];
3763
  $env->block = $block;
3764
  $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
3765
 
3766
  $this->env = $env;
 
3767
 
3768
  return $env;
3769
  }
@@ -3773,9 +4590,25 @@ class Compiler
3773
  */
3774
  protected function popEnv()
3775
  {
 
3776
  $this->env = $this->env->parent;
3777
  }
3778
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3779
  /**
3780
  * Get store environment
3781
  *
@@ -3821,26 +4654,45 @@ class Compiler
3821
  protected function setExisting($name, $value, Environment $env, $valueUnreduced = null)
3822
  {
3823
  $storeEnv = $env;
 
3824
 
3825
  $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
3826
 
 
 
3827
  for (;;) {
3828
- if (array_key_exists($name, $env->store)) {
3829
  break;
3830
  }
3831
 
3832
- if (! $hasNamespace && isset($env->marker)) {
3833
- $env = $storeEnv;
3834
  break;
3835
  }
3836
 
3837
- if (! isset($env->parent)) {
3838
- $env = $storeEnv;
3839
- break;
3840
- }
 
3841
 
3842
- $env = $env->parent;
3843
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3844
 
3845
  $env->store[$name] = $value;
3846
 
@@ -3887,7 +4739,6 @@ class Compiler
3887
  $env = $this->getStoreEnv();
3888
  }
3889
 
3890
- $nextIsRoot = false;
3891
  $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
3892
 
3893
  $maxDepth = 10000;
@@ -3897,7 +4748,7 @@ class Compiler
3897
  break;
3898
  }
3899
 
3900
- if (array_key_exists($normalizedName, $env->store)) {
3901
  if ($unreduced && isset($env->storeUnreduced[$normalizedName])) {
3902
  return $env->storeUnreduced[$normalizedName];
3903
  }
@@ -3906,24 +4757,30 @@ class Compiler
3906
  }
3907
 
3908
  if (! $hasNamespace && isset($env->marker)) {
3909
- if (! $nextIsRoot && ! empty($env->store[$specialContentKey])) {
3910
  $env = $env->store[$specialContentKey]->scope;
3911
  continue;
3912
  }
3913
 
3914
- $env = $this->rootEnv;
 
 
 
 
3915
  continue;
3916
  }
3917
 
3918
- if (! isset($env->parent)) {
 
 
 
 
3919
  break;
3920
  }
3921
-
3922
- $env = $env->parent;
3923
  }
3924
 
3925
  if ($shouldThrow) {
3926
- $this->throwError("Undefined variable \$$name" . ($maxDepth<=0 ? " (infinite recursion)" : ""));
3927
  }
3928
 
3929
  // found nothing
@@ -3940,7 +4797,7 @@ class Compiler
3940
  */
3941
  protected function has($name, Environment $env = null)
3942
  {
3943
- return $this->get($name, false, $env) !== null;
3944
  }
3945
 
3946
  /**
@@ -4014,7 +4871,7 @@ class Compiler
4014
  */
4015
  public function addParsedFile($path)
4016
  {
4017
- if (isset($path) && file_exists($path)) {
4018
  $this->parsedFiles[realpath($path)] = filemtime($path);
4019
  }
4020
  }
@@ -4040,7 +4897,7 @@ class Compiler
4040
  */
4041
  public function addImportPath($path)
4042
  {
4043
- if (! in_array($path, $this->importPaths)) {
4044
  $this->importPaths[] = $path;
4045
  }
4046
  }
@@ -4063,10 +4920,13 @@ class Compiler
4063
  * @api
4064
  *
4065
  * @param integer $numberPrecision
 
 
4066
  */
4067
  public function setNumberPrecision($numberPrecision)
4068
  {
4069
- Node\Number::$precision = $numberPrecision;
 
4070
  }
4071
 
4072
  /**
@@ -4163,6 +5023,7 @@ class Compiler
4163
  */
4164
  protected function importFile($path, OutputBlock $out)
4165
  {
 
4166
  // see if tree is cached
4167
  $realPath = realpath($path);
4168
 
@@ -4179,9 +5040,11 @@ class Compiler
4179
  }
4180
 
4181
  $pi = pathinfo($path);
 
4182
  array_unshift($this->importPaths, $pi['dirname']);
4183
  $this->compileChildrenNoReturn($tree->children, $out);
4184
  array_shift($this->importPaths);
 
4185
  }
4186
 
4187
  /**
@@ -4197,18 +5060,31 @@ class Compiler
4197
  {
4198
  $urls = [];
4199
 
 
 
4200
  // for "normal" scss imports (ignore vanilla css and external requests)
4201
- if (! preg_match('/\.css$|^https?:\/\//', $url)) {
 
 
4202
  // try both normal and the _partial filename
4203
- $urls = [$url, preg_replace('/[^\/]+$/', '_\0', $url)];
4204
- }
4205
 
4206
- $hasExtension = preg_match('/[.]s?css$/', $url);
 
 
 
 
 
 
 
 
 
4207
 
4208
  foreach ($this->importPaths as $dir) {
4209
- if (is_string($dir)) {
4210
  // check urls for normal import paths
4211
  foreach ($urls as $full) {
 
4212
  $separator = (
4213
  ! empty($dir) &&
4214
  substr($dir, -1) !== '/' &&
@@ -4216,22 +5092,42 @@ class Compiler
4216
  ) ? '/' : '';
4217
  $full = $dir . $separator . $full;
4218
 
4219
- if ($this->fileExists($file = $full . '.scss') ||
4220
- ($hasExtension && $this->fileExists($file = $full))
4221
- ) {
4222
- return $file;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4223
  }
4224
  }
4225
- } elseif (is_callable($dir)) {
4226
  // check custom callback for import path
4227
- $file = call_user_func($dir, $url);
4228
 
4229
- if ($file !== null) {
4230
  return $file;
4231
  }
4232
  }
4233
  }
4234
 
 
 
 
 
 
 
4235
  return null;
4236
  }
4237
 
@@ -4255,14 +5151,30 @@ class Compiler
4255
  * @param boolean $ignoreErrors
4256
  *
4257
  * @return \ScssPhp\ScssPhp\Compiler
 
 
4258
  */
4259
  public function setIgnoreErrors($ignoreErrors)
4260
  {
4261
- $this->ignoreErrors = $ignoreErrors;
4262
 
4263
  return $this;
4264
  }
4265
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4266
  /**
4267
  * Throw error (exception)
4268
  *
@@ -4271,33 +5183,85 @@ class Compiler
4271
  * @param string $msg Message with optional sprintf()-style vararg parameters
4272
  *
4273
  * @throws \ScssPhp\ScssPhp\Exception\CompilerException
 
 
4274
  */
4275
  public function throwError($msg)
4276
  {
4277
- if ($this->ignoreErrors) {
4278
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4279
  }
4280
 
4281
- $line = $this->sourceLine;
4282
- $column = $this->sourceColumn;
 
 
 
 
 
4283
 
4284
- $loc = isset($this->sourceNames[$this->sourceIndex])
4285
- ? $this->sourceNames[$this->sourceIndex] . " on line $line, at column $column"
4286
- : "line: $line, column: $column";
4287
 
4288
- if (func_num_args() > 1) {
4289
- $msg = call_user_func_array('sprintf', func_get_args());
 
 
 
4290
  }
4291
 
4292
- $msg = "$msg: $loc";
 
 
 
 
 
 
 
 
 
 
 
4293
 
4294
- $callStackMsg = $this->callStackMessage();
 
 
 
 
 
 
 
 
4295
 
4296
- if ($callStackMsg) {
4297
- $msg .= "\nCall Stack:\n" . $callStackMsg;
4298
- }
4299
 
4300
- throw new CompilerException($msg);
 
 
 
 
 
 
4301
  }
4302
 
4303
  /**
@@ -4316,14 +5280,15 @@ class Compiler
4316
  if ($this->callStack) {
4317
  foreach (array_reverse($this->callStack) as $call) {
4318
  if ($all || (isset($call['n']) && $call['n'])) {
4319
- $msg = "#" . $ncall++ . " " . $call['n'] . " ";
4320
  $msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]])
4321
  ? $this->sourceNames[$call[Parser::SOURCE_INDEX]]
4322
  : '(unknown file)');
4323
- $msg .= " on line " . $call[Parser::SOURCE_LINE];
 
4324
  $callStackMsg[] = $msg;
4325
 
4326
- if (! is_null($limit) && $ncall>$limit) {
4327
  break;
4328
  }
4329
  }
@@ -4343,113 +5308,99 @@ class Compiler
4343
  protected function handleImportLoop($name)
4344
  {
4345
  for ($env = $this->env; $env; $env = $env->parent) {
 
 
 
 
4346
  $file = $this->sourceNames[$env->block->sourceIndex];
4347
 
4348
  if (realpath($file) === $name) {
4349
- $this->throwError('An @import loop has been found: %s imports %s', $file, basename($file));
4350
- break;
4351
  }
4352
  }
4353
  }
4354
 
4355
- /**
4356
- * Does file exist?
4357
- *
4358
- * @param string $name
4359
- *
4360
- * @return boolean
4361
- */
4362
- protected function fileExists($name)
4363
- {
4364
- return file_exists($name) && is_file($name);
4365
- }
4366
-
4367
  /**
4368
  * Call SCSS @function
4369
  *
4370
- * @param string $name
4371
  * @param array $argValues
4372
- * @param array $returnValue
4373
  *
4374
- * @return boolean Returns true if returnValue is set; otherwise, false
4375
  */
4376
- protected function callScssFunction($name, $argValues, &$returnValue)
4377
  {
4378
- $func = $this->get(static::$namespaces['function'] . $name, false);
4379
-
4380
  if (! $func) {
4381
- return false;
4382
  }
 
4383
 
4384
  $this->pushEnv();
4385
 
4386
- $storeEnv = $this->storeEnv;
4387
- $this->storeEnv = $this->env;
4388
-
4389
  // set the args
4390
  if (isset($func->args)) {
4391
  $this->applyArguments($func->args, $argValues);
4392
  }
4393
 
4394
  // throw away lines and children
4395
- $tmp = new OutputBlock;
4396
  $tmp->lines = [];
4397
  $tmp->children = [];
4398
 
4399
  $this->env->marker = 'function';
4400
 
4401
- $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . " " . $name);
 
 
 
 
4402
 
4403
- $this->storeEnv = $storeEnv;
4404
 
4405
  $this->popEnv();
4406
 
4407
- $returnValue = ! isset($ret) ? static::$defaultValue : $ret;
4408
-
4409
- return true;
4410
  }
4411
 
4412
  /**
4413
  * Call built-in and registered (PHP) functions
4414
  *
4415
  * @param string $name
 
 
4416
  * @param array $args
4417
- * @param array $returnValue
4418
  *
4419
- * @return boolean Returns true if returnValue is set; otherwise, false
4420
  */
4421
- protected function callNativeFunction($name, $args, &$returnValue)
4422
  {
4423
- // try a lib function
4424
- $name = $this->normalizeName($name);
4425
 
4426
- if (isset($this->userFunctions[$name])) {
4427
- // see if we can find a user function
4428
- list($f, $prototype) = $this->userFunctions[$name];
4429
- } elseif (($f = $this->getBuiltinFunction($name)) && is_callable($f)) {
4430
- $libName = $f[1];
4431
- $prototype = isset(static::$$libName) ? static::$$libName : null;
4432
- } else {
4433
- return false;
4434
  }
4435
-
4436
- @list($sorted, $kwargs) = $this->sortArgs($prototype, $args);
4437
 
4438
  if ($name !== 'if' && $name !== 'call') {
 
 
 
 
 
 
4439
  foreach ($sorted as &$val) {
4440
- $val = $this->reduce($val, true);
4441
  }
4442
  }
4443
 
4444
- $returnValue = call_user_func($f, $sorted, $kwargs);
4445
 
4446
  if (! isset($returnValue)) {
4447
- return false;
4448
  }
4449
 
4450
- $returnValue = $this->coerceValue($returnValue);
4451
-
4452
- return true;
4453
  }
4454
 
4455
  /**
@@ -4461,6 +5412,18 @@ class Compiler
4461
  */
4462
  protected function getBuiltinFunction($name)
4463
  {
 
 
 
 
 
 
 
 
 
 
 
 
4464
  $libName = 'lib' . preg_replace_callback(
4465
  '/_(.)/',
4466
  function ($m) {
@@ -4468,19 +5431,29 @@ class Compiler
4468
  },
4469
  ucfirst($name)
4470
  );
 
 
4471
 
4472
- return [$this, $libName];
 
 
 
 
 
 
 
4473
  }
4474
 
4475
  /**
4476
  * Sorts keyword arguments
4477
  *
4478
- * @param array $prototype
4479
- * @param array $args
 
4480
  *
4481
- * @return array
4482
  */
4483
- protected function sortArgs($prototypes, $args)
4484
  {
4485
  static $parser = null;
4486
 
@@ -4488,25 +5461,44 @@ class Compiler
4488
  $keyArgs = [];
4489
  $posArgs = [];
4490
 
 
 
 
 
4491
  // separate positional and keyword arguments
4492
  foreach ($args as $arg) {
4493
  list($key, $value) = $arg;
4494
 
4495
- $key = $key[1];
4496
-
4497
- if (empty($key)) {
4498
  $posArgs[] = empty($arg[2]) ? $value : $arg;
4499
  } else {
4500
- $keyArgs[$key] = $value;
4501
  }
4502
  }
4503
 
4504
  return [$posArgs, $keyArgs];
4505
  }
4506
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4507
  $finalArgs = [];
4508
 
4509
- if (! is_array(reset($prototypes))) {
4510
  $prototypes = [$prototypes];
4511
  }
4512
 
@@ -4524,14 +5516,14 @@ class Compiler
4524
  $p = explode(':', $p, 2);
4525
  $name = array_shift($p);
4526
 
4527
- if (count($p)) {
4528
  $p = trim(reset($p));
4529
 
4530
  if ($p === 'null') {
4531
  // differentiate this null from the static::$null
4532
  $default = [Type::T_KEYWORD, 'null'];
4533
  } else {
4534
- if (is_null($parser)) {
4535
  $parser = $this->parserFactory(__METHOD__);
4536
  }
4537
 
@@ -4549,8 +5541,19 @@ class Compiler
4549
  $argDef[] = [$name, $default, $isVariable];
4550
  }
4551
 
 
 
 
4552
  try {
4553
- $vars = $this->applyArguments($argDef, $args, false);
 
 
 
 
 
 
 
 
4554
 
4555
  // ensure all args are populated
4556
  foreach ($prototype as $i => $p) {
@@ -4586,10 +5589,20 @@ class Compiler
4586
  } catch (CompilerException $e) {
4587
  $exceptionMessage = $e->getMessage();
4588
  }
 
4589
  }
4590
 
4591
  if ($exceptionMessage && ! $prototypeHasMatch) {
4592
- $this->throwError($exceptionMessage);
 
 
 
 
 
 
 
 
 
4593
  }
4594
 
4595
  return [$finalArgs, $keyArgs];
@@ -4598,19 +5611,28 @@ class Compiler
4598
  /**
4599
  * Apply argument values per definition
4600
  *
4601
- * @param array $argDef
4602
- * @param array $argValues
 
 
 
 
 
4603
  *
4604
  * @throws \Exception
4605
  */
4606
- protected function applyArguments($argDef, $argValues, $storeInEnv = true)
4607
  {
4608
  $output = [];
4609
 
 
 
 
 
4610
  if ($storeInEnv) {
4611
  $storeEnv = $this->getStoreEnv();
4612
 
4613
- $env = new Environment;
4614
  $env->store = $storeEnv->store;
4615
  }
4616
 
@@ -4627,6 +5649,7 @@ class Compiler
4627
  $splatSeparator = null;
4628
  $keywordArgs = [];
4629
  $deferredKeywordArgs = [];
 
4630
  $remaining = [];
4631
  $hasKeywordArgument = false;
4632
 
@@ -4635,28 +5658,38 @@ class Compiler
4635
  if (! empty($arg[0])) {
4636
  $hasKeywordArgument = true;
4637
 
4638
- if (! isset($args[$arg[0][1]]) || $args[$arg[0][1]][3]) {
 
 
 
 
 
 
 
 
 
 
 
4639
  if ($hasVariable) {
4640
- $deferredKeywordArgs[$arg[0][1]] = $arg[1];
4641
  } else {
4642
- $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
4643
- break;
4644
  }
4645
- } elseif ($args[$arg[0][1]][0] < count($remaining)) {
4646
- $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
4647
- break;
4648
  } else {
4649
- $keywordArgs[$arg[0][1]] = $arg[1];
4650
  }
4651
- } elseif ($arg[2] === true) {
 
4652
  $val = $this->reduce($arg[1], true);
4653
 
4654
  if ($val[0] === Type::T_LIST) {
4655
  foreach ($val[2] as $name => $item) {
4656
  if (! is_numeric($name)) {
4657
- if (!isset($args[$name])) {
4658
  foreach (array_keys($args) as $an) {
4659
- if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
4660
  $name = $an;
4661
  break;
4662
  }
@@ -4669,9 +5702,10 @@ class Compiler
4669
  $keywordArgs[$name] = $item;
4670
  }
4671
  } else {
4672
- if (is_null($splatSeparator)) {
4673
  $splatSeparator = $val[1];
4674
  }
 
4675
  $remaining[] = $item;
4676
  }
4677
  }
@@ -4681,23 +5715,25 @@ class Compiler
4681
  $item = $val[2][$i];
4682
 
4683
  if (! is_numeric($name)) {
4684
- if (!isset($args[$name])) {
4685
  foreach (array_keys($args) as $an) {
4686
- if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
4687
  $name = $an;
4688
  break;
4689
  }
4690
  }
4691
  }
 
4692
  if ($hasVariable) {
4693
  $deferredKeywordArgs[$name] = $item;
4694
  } else {
4695
  $keywordArgs[$name] = $item;
4696
  }
4697
  } else {
4698
- if (is_null($splatSeparator)) {
4699
  $splatSeparator = $val[1];
4700
  }
 
4701
  $remaining[] = $item;
4702
  }
4703
  }
@@ -4705,8 +5741,7 @@ class Compiler
4705
  $remaining[] = $val;
4706
  }
4707
  } elseif ($hasKeywordArgument) {
4708
- $this->throwError('Positional arguments must come before keyword arguments.');
4709
- break;
4710
  } else {
4711
  $remaining[] = $arg[1];
4712
  }
@@ -4716,15 +5751,27 @@ class Compiler
4716
  list($i, $name, $default, $isVariable) = $arg;
4717
 
4718
  if ($isVariable) {
4719
- $val = [Type::T_LIST, is_null($splatSeparator) ? ',' : $splatSeparator , [], $isVariable];
 
 
 
 
 
 
4720
 
4721
- for ($count = count($remaining); $i < $count; $i++) {
 
 
4722
  $val[2][] = $remaining[$i];
4723
  }
4724
 
4725
  foreach ($deferredKeywordArgs as $itemName => $item) {
4726
  $val[2][$itemName] = $item;
4727
  }
 
 
 
 
4728
  } elseif (isset($remaining[$i])) {
4729
  $val = $remaining[$i];
4730
  } elseif (isset($keywordArgs[$name])) {
@@ -4732,14 +5779,13 @@ class Compiler
4732
  } elseif (! empty($default)) {
4733
  continue;
4734
  } else {
4735
- $this->throwError("Missing argument $name");
4736
- break;
4737
  }
4738
 
4739
  if ($storeInEnv) {
4740
  $this->set($name, $this->reduce($val, true), true, $env);
4741
  } else {
4742
- $output[$name] = $val;
4743
  }
4744
  }
4745
 
@@ -4757,7 +5803,7 @@ class Compiler
4757
  if ($storeInEnv) {
4758
  $this->set($name, $this->reduce($default, true), true);
4759
  } else {
4760
- $output[$name] = $default;
4761
  }
4762
  }
4763
 
@@ -4773,15 +5819,15 @@ class Compiler
4773
  */
4774
  protected function coerceValue($value)
4775
  {
4776
- if (is_array($value) || $value instanceof \ArrayAccess) {
4777
  return $value;
4778
  }
4779
 
4780
- if (is_bool($value)) {
4781
  return $this->toBool($value);
4782
  }
4783
 
4784
- if ($value === null) {
4785
  return static::$null;
4786
  }
4787
 
@@ -4793,30 +5839,14 @@ class Compiler
4793
  return static::$emptyString;
4794
  }
4795
 
4796
- if (preg_match('/^(#([0-9a-f]{6})|#([0-9a-f]{3}))$/i', $value, $m)) {
4797
- $color = [Type::T_COLOR];
4798
-
4799
- if (isset($m[3])) {
4800
- $num = hexdec($m[3]);
4801
-
4802
- foreach ([3, 2, 1] as $i) {
4803
- $t = $num & 0xf;
4804
- $color[$i] = $t << 4 | $t;
4805
- $num >>= 4;
4806
- }
4807
- } else {
4808
- $num = hexdec($m[2]);
4809
-
4810
- foreach ([3, 2, 1] as $i) {
4811
- $color[$i] = $num & 0xff;
4812
- $num >>= 8;
4813
- }
4814
- }
4815
 
 
4816
  return $color;
4817
  }
4818
 
4819
- return [Type::T_KEYWORD, $value];
4820
  }
4821
 
4822
  /**
@@ -4832,24 +5862,34 @@ class Compiler
4832
  return $item;
4833
  }
4834
 
4835
- if ($item === static::$emptyList) {
 
 
 
 
4836
  return static::$emptyMap;
4837
  }
4838
 
4839
- return [Type::T_MAP, [$item], [static::$null]];
4840
  }
4841
 
4842
  /**
4843
  * Coerce something to list
4844
  *
4845
- * @param array $item
4846
- * @param string $delim
 
4847
  *
4848
  * @return array
4849
  */
4850
- protected function coerceList($item, $delim = ',')
4851
  {
4852
  if (isset($item) && $item[0] === Type::T_LIST) {
 
 
 
 
 
4853
  return $item;
4854
  }
4855
 
@@ -4858,13 +5898,15 @@ class Compiler
4858
  $values = $item[2];
4859
  $list = [];
4860
 
4861
- for ($i = 0, $s = count($keys); $i < $s; $i++) {
4862
  $key = $keys[$i];
4863
  $value = $values[$i];
4864
 
4865
  switch ($key[0]) {
4866
  case Type::T_LIST:
4867
  case Type::T_MAP:
 
 
4868
  break;
4869
 
4870
  default:
@@ -4882,7 +5924,7 @@ class Compiler
4882
  return [Type::T_LIST, ',', $list];
4883
  }
4884
 
4885
- return [Type::T_LIST, $delim, ! isset($item) ? []: [$item]];
4886
  }
4887
 
4888
  /**
@@ -4908,21 +5950,107 @@ class Compiler
4908
  *
4909
  * @return array|null
4910
  */
4911
- protected function coerceColor($value)
4912
  {
4913
  switch ($value[0]) {
4914
  case Type::T_COLOR:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4915
  return $value;
4916
 
 
 
 
 
 
 
 
 
 
 
 
 
4917
  case Type::T_KEYWORD:
 
 
 
 
4918
  $name = strtolower($value[1]);
4919
 
4920
- if (isset(Colors::$cssColors[$name])) {
4921
- $rgba = explode(',', Colors::$cssColors[$name]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4922
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4923
  return isset($rgba[3])
4924
- ? [Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3]]
4925
- : [Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]];
4926
  }
4927
 
4928
  return null;
@@ -4931,6 +6059,88 @@ class Compiler
4931
  return null;
4932
  }
4933
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4934
  /**
4935
  * Coerce value to string
4936
  *
@@ -4947,6 +6157,36 @@ class Compiler
4947
  return [Type::T_STRING, '', [$this->compileValue($value)]];
4948
  }
4949
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4950
  /**
4951
  * Coerce value to a percentage
4952
  *
@@ -4983,7 +6223,7 @@ class Compiler
4983
  $value = $this->coerceMap($value);
4984
 
4985
  if ($value[0] !== Type::T_MAP) {
4986
- $this->throwError('expecting map, %s received', $value[0]);
4987
  }
4988
 
4989
  return $value;
@@ -5003,7 +6243,7 @@ class Compiler
5003
  public function assertList($value)
5004
  {
5005
  if ($value[0] !== Type::T_LIST) {
5006
- $this->throwError('expecting list, %s received', $value[0]);
5007
  }
5008
 
5009
  return $value;
@@ -5026,29 +6266,57 @@ class Compiler
5026
  return $color;
5027
  }
5028
 
5029
- $this->throwError('expecting color, %s received', $value[0]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5030
  }
5031
 
5032
  /**
5033
- * Assert value is a number
5034
  *
5035
  * @api
5036
  *
5037
  * @param array $value
 
5038
  *
5039
  * @return integer|float
5040
  *
5041
  * @throws \Exception
5042
  */
5043
- public function assertNumber($value)
5044
  {
5045
- if ($value[0] !== Type::T_NUMBER) {
5046
- $this->throwError('expecting number, %s received', $value[0]);
 
 
 
5047
  }
5048
 
5049
- return $value[1];
5050
  }
5051
 
 
5052
  /**
5053
  * Make sure a color's components don't go out of bounds
5054
  *
@@ -5137,7 +6405,7 @@ class Compiler
5137
  }
5138
 
5139
  if ($h * 3 < 2) {
5140
- return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
5141
  }
5142
 
5143
  return $m1;
@@ -5167,9 +6435,9 @@ class Compiler
5167
  $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
5168
  $m1 = $l * 2 - $m2;
5169
 
5170
- $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
5171
  $g = $this->hueToRGB($m1, $m2, $h) * 255;
5172
- $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
5173
 
5174
  $out = [Type::T_COLOR, $r, $g, $b];
5175
 
@@ -5178,10 +6446,27 @@ class Compiler
5178
 
5179
  // Built in functions
5180
 
5181
- protected static $libCall = ['name', 'args...'];
5182
  protected function libCall($args, $kwargs)
5183
  {
5184
- $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5185
  $callArgs = [];
5186
 
5187
  // $kwargs['args'] is [Type::T_LIST, ',', [..]]
@@ -5195,7 +6480,29 @@ class Compiler
5195
  $callArgs[] = [$varname, $arg, false];
5196
  }
5197
 
5198
- return $this->reduce([Type::T_FUNCTION_CALL, $name, $callArgs]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5199
  }
5200
 
5201
  protected static $libIf = ['condition', 'if-true', 'if-false:'];
@@ -5215,11 +6522,8 @@ class Compiler
5215
  {
5216
  list($list, $value) = $args;
5217
 
5218
- if ($value[0] === Type::T_MAP) {
5219
- return static::$null;
5220
- }
5221
-
5222
- if ($list[0] === Type::T_MAP ||
5223
  $list[0] === Type::T_STRING ||
5224
  $list[0] === Type::T_KEYWORD ||
5225
  $list[0] === Type::T_INTERPOLATE
@@ -5242,30 +6546,68 @@ class Compiler
5242
  return false === $key ? static::$null : $key + 1;
5243
  }
5244
 
5245
- protected static $libRgb = ['red', 'green', 'blue'];
5246
- protected function libRgb($args)
 
 
 
 
 
5247
  {
5248
- list($r, $g, $b) = $args;
 
 
 
 
 
5249
 
5250
- return [Type::T_COLOR, $r[1], $g[1], $b[1]];
5251
- }
5252
 
5253
- protected static $libRgba = [
5254
- ['color', 'alpha:1'],
5255
- ['red', 'green', 'blue', 'alpha:1'] ];
5256
- protected function libRgba($args)
5257
- {
5258
- if ($color = $this->coerceColor($args[0])) {
5259
- $num = isset($args[3]) ? $args[3] : $args[1];
5260
- $alpha = $this->assertNumber($num);
5261
- $color[4] = $alpha;
5262
 
5263
- return $color;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5264
  }
5265
 
5266
- list($r, $g, $b, $a) = $args;
 
5267
 
5268
- return [Type::T_COLOR, $r[1], $g[1], $b[1], $a[1]];
 
 
 
 
 
 
 
 
5269
  }
5270
 
5271
  // helper function for adjust_color, change_color, and scale_color
@@ -5273,21 +6615,25 @@ class Compiler
5273
  {
5274
  $color = $this->assertColor($args[0]);
5275
 
5276
- foreach ([1, 2, 3, 7] as $i) {
5277
- if (isset($args[$i])) {
5278
- $val = $this->assertNumber($args[$i]);
5279
- $ii = $i === 7 ? 4 : $i; // alpha
5280
- $color[$ii] = call_user_func($fn, isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
 
 
 
 
5281
  }
5282
  }
5283
 
5284
  if (! empty($args[4]) || ! empty($args[5]) || ! empty($args[6])) {
5285
  $hsl = $this->toHSL($color[1], $color[2], $color[3]);
5286
 
5287
- foreach ([4, 5, 6] as $i) {
5288
- if (! empty($args[$i])) {
5289
- $val = $this->assertNumber($args[$i]);
5290
- $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
5291
  }
5292
  }
5293
 
@@ -5368,9 +6714,14 @@ class Compiler
5368
  protected function libIeHexStr($args)
5369
  {
5370
  $color = $this->coerceColor($args[0]);
 
 
 
 
 
5371
  $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
5372
 
5373
- return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
5374
  }
5375
 
5376
  protected static $libRed = ['color'];
@@ -5378,6 +6729,10 @@ class Compiler
5378
  {
5379
  $color = $this->coerceColor($args[0]);
5380
 
 
 
 
 
5381
  return $color[1];
5382
  }
5383
 
@@ -5386,6 +6741,10 @@ class Compiler
5386
  {
5387
  $color = $this->coerceColor($args[0]);
5388
 
 
 
 
 
5389
  return $color[2];
5390
  }
5391
 
@@ -5394,6 +6753,10 @@ class Compiler
5394
  {
5395
  $color = $this->coerceColor($args[0]);
5396
 
 
 
 
 
5397
  return $color[3];
5398
  }
5399
 
@@ -5421,7 +6784,10 @@ class Compiler
5421
  }
5422
 
5423
  // mix two colors
5424
- protected static $libMix = ['color-1', 'color-2', 'weight:0.5'];
 
 
 
5425
  protected function libMix($args)
5426
  {
5427
  list($first, $second, $weight) = $args;
@@ -5457,25 +6823,85 @@ class Compiler
5457
  return $this->fixColor($new);
5458
  }
5459
 
5460
- protected static $libHsl = ['hue', 'saturation', 'lightness'];
5461
- protected function libHsl($args)
 
 
 
5462
  {
5463
- list($h, $s, $l) = $args;
5464
 
5465
- return $this->toRGB($h[1], $s[1], $l[1]);
5466
- }
 
 
5467
 
5468
- protected static $libHsla = ['hue', 'saturation', 'lightness', 'alpha'];
5469
- protected function libHsla($args)
5470
- {
5471
- list($h, $s, $l, $a) = $args;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5472
 
5473
- $color = $this->toRGB($h[1], $s[1], $l[1]);
5474
- $color[4] = $a[1];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5475
 
5476
  return $color;
5477
  }
5478
 
 
 
 
 
 
 
 
 
 
5479
  protected static $libHue = ['color'];
5480
  protected function libHue($args)
5481
  {
@@ -5543,7 +6969,7 @@ class Compiler
5543
  return $this->adjustHsl($color, 3, -$amount);
5544
  }
5545
 
5546
- protected static $libSaturate = [['color', 'amount'], ['number']];
5547
  protected function libSaturate($args)
5548
  {
5549
  $value = $args[0];
@@ -5552,6 +6978,11 @@ class Compiler
5552
  return null;
5553
  }
5554
 
 
 
 
 
 
5555
  $color = $this->assertColor($value);
5556
  $amount = 100 * $this->coercePercent($args[1]);
5557
 
@@ -5585,21 +7016,32 @@ class Compiler
5585
  return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
5586
  }
5587
 
5588
- protected static $libInvert = ['color'];
5589
  protected function libInvert($args)
5590
  {
5591
- $value = $args[0];
 
 
 
 
 
 
5592
 
5593
  if ($value[0] === Type::T_NUMBER) {
5594
  return null;
5595
  }
5596
 
5597
  $color = $this->assertColor($value);
5598
- $color[1] = 255 - $color[1];
5599
- $color[2] = 255 - $color[2];
5600
- $color[3] = 255 - $color[3];
 
5601
 
5602
- return $color;
 
 
 
 
5603
  }
5604
 
5605
  // increases opacity by amount
@@ -5664,13 +7106,13 @@ class Compiler
5664
  return [Type::T_STRING, '"', [$value]];
5665
  }
5666
 
5667
- protected static $libPercentage = ['value'];
5668
  protected function libPercentage($args)
5669
  {
5670
  return new Node\Number($this->coercePercent($args[0]) * 100, '%');
5671
  }
5672
 
5673
- protected static $libRound = ['value'];
5674
  protected function libRound($args)
5675
  {
5676
  $num = $args[0];
@@ -5678,7 +7120,7 @@ class Compiler
5678
  return new Node\Number(round($num[1]), $num[2]);
5679
  }
5680
 
5681
- protected static $libFloor = ['value'];
5682
  protected function libFloor($args)
5683
  {
5684
  $num = $args[0];
@@ -5686,7 +7128,7 @@ class Compiler
5686
  return new Node\Number(floor($num[1]), $num[2]);
5687
  }
5688
 
5689
- protected static $libCeil = ['value'];
5690
  protected function libCeil($args)
5691
  {
5692
  $num = $args[0];
@@ -5694,7 +7136,7 @@ class Compiler
5694
  return new Node\Number(ceil($num[1]), $num[2]);
5695
  }
5696
 
5697
- protected static $libAbs = ['value'];
5698
  protected function libAbs($args)
5699
  {
5700
  $num = $args[0];
@@ -5705,29 +7147,47 @@ class Compiler
5705
  protected function libMin($args)
5706
  {
5707
  $numbers = $this->getNormalizedNumbers($args);
5708
- $min = null;
 
 
 
 
5709
 
5710
- foreach ($numbers as $key => $number) {
5711
- if (null === $min || $number[1] <= $min[1]) {
5712
- $min = [$key, $number[1]];
 
 
 
 
 
5713
  }
5714
  }
5715
 
5716
- return $args[$min[0]];
5717
  }
5718
 
5719
  protected function libMax($args)
5720
  {
5721
  $numbers = $this->getNormalizedNumbers($args);
5722
- $max = null;
 
 
 
 
5723
 
5724
- foreach ($numbers as $key => $number) {
5725
- if (null === $max || $number[1] >= $max[1]) {
5726
- $max = [$key, $number[1]];
 
 
 
 
 
5727
  }
5728
  }
5729
 
5730
- return $args[$max[0]];
5731
  }
5732
 
5733
  /**
@@ -5739,27 +7199,23 @@ class Compiler
5739
  */
5740
  protected function getNormalizedNumbers($args)
5741
  {
5742
- $unit = null;
5743
  $originalUnit = null;
5744
- $numbers = [];
5745
 
5746
  foreach ($args as $key => $item) {
5747
- if ($item[0] !== Type::T_NUMBER) {
5748
- $this->throwError('%s is not a number', $item[0]);
5749
- break;
5750
- }
5751
 
5752
  $number = $item->normalize();
5753
 
5754
- if (null === $unit) {
5755
  $unit = $number[2];
5756
  $originalUnit = $item->unitStr();
5757
- } elseif ($number[1] && $unit !== $number[2]) {
5758
- $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
5759
- break;
5760
  }
5761
 
5762
- $numbers[$key] = $number;
5763
  }
5764
 
5765
  return $numbers;
@@ -5768,21 +7224,25 @@ class Compiler
5768
  protected static $libLength = ['list'];
5769
  protected function libLength($args)
5770
  {
5771
- $list = $this->coerceList($args[0]);
5772
 
5773
- return count($list[2]);
5774
  }
5775
 
5776
  //protected static $libListSeparator = ['list...'];
5777
  protected function libListSeparator($args)
5778
  {
5779
- if (count($args) > 1) {
5780
  return 'comma';
5781
  }
5782
 
 
 
 
 
5783
  $list = $this->coerceList($args[0]);
5784
 
5785
- if (count($list[2]) <= 1) {
5786
  return 'space';
5787
  }
5788
 
@@ -5796,13 +7256,13 @@ class Compiler
5796
  protected static $libNth = ['list', 'n'];
5797
  protected function libNth($args)
5798
  {
5799
- $list = $this->coerceList($args[0]);
5800
  $n = $this->assertNumber($args[1]);
5801
 
5802
  if ($n > 0) {
5803
  $n--;
5804
  } elseif ($n < 0) {
5805
- $n += count($list[2]);
5806
  }
5807
 
5808
  return isset($list[2][$n]) ? $list[2][$n] : static::$defaultValue;
@@ -5817,13 +7277,11 @@ class Compiler
5817
  if ($n > 0) {
5818
  $n--;
5819
  } elseif ($n < 0) {
5820
- $n += count($list[2]);
5821
  }
5822
 
5823
  if (! isset($list[2][$n])) {
5824
- $this->throwError('Invalid argument for "n"');
5825
-
5826
- return null;
5827
  }
5828
 
5829
  $list[2][$n] = $args[2];
@@ -5837,9 +7295,10 @@ class Compiler
5837
  $map = $this->assertMap($args[0]);
5838
  $key = $args[1];
5839
 
5840
- if (! is_null($key)) {
5841
  $key = $this->compileStringContent($this->coerceString($key));
5842
- for ($i = count($map[1]) - 1; $i >= 0; $i--) {
 
5843
  if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
5844
  return $map[2][$i];
5845
  }
@@ -5867,14 +7326,20 @@ class Compiler
5867
  return [Type::T_LIST, ',', $values];
5868
  }
5869
 
5870
- protected static $libMapRemove = ['map', 'key'];
5871
  protected function libMapRemove($args)
5872
  {
5873
  $map = $this->assertMap($args[0]);
5874
- $key = $this->compileStringContent($this->coerceString($args[1]));
5875
 
5876
- for ($i = count($map[1]) - 1; $i >= 0; $i--) {
5877
- if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
 
 
 
 
 
 
5878
  array_splice($map[1], $i, 1);
5879
  array_splice($map[2], $i, 1);
5880
  }
@@ -5889,7 +7354,7 @@ class Compiler
5889
  $map = $this->assertMap($args[0]);
5890
  $key = $this->compileStringContent($this->coerceString($args[1]));
5891
 
5892
- for ($i = count($map[1]) - 1; $i >= 0; $i--) {
5893
  if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
5894
  return true;
5895
  }
@@ -5898,7 +7363,10 @@ class Compiler
5898
  return false;
5899
  }
5900
 
5901
- protected static $libMapMerge = ['map-1', 'map-2'];
 
 
 
5902
  protected function libMapMerge($args)
5903
  {
5904
  $map1 = $this->assertMap($args[0]);
@@ -5937,6 +7405,19 @@ class Compiler
5937
  return [Type::T_MAP, $keys, $values];
5938
  }
5939
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5940
  protected function listSeparatorForJoin($list1, $sep)
5941
  {
5942
  if (! isset($sep)) {
@@ -5948,23 +7429,58 @@ class Compiler
5948
  return ',';
5949
 
5950
  case 'space':
5951
- return '';
5952
 
5953
  default:
5954
  return $list1[1];
5955
  }
5956
  }
5957
 
5958
- protected static $libJoin = ['list1', 'list2', 'separator:null'];
5959
  protected function libJoin($args)
5960
  {
5961
- list($list1, $list2, $sep) = $args;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5962
 
5963
- $list1 = $this->coerceList($list1, ' ');
5964
- $list2 = $this->coerceList($list2, ' ');
5965
- $sep = $this->listSeparatorForJoin($list1, $sep);
5966
 
5967
- return [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
5968
  }
5969
 
5970
  protected static $libAppend = ['list', 'val', 'separator:null'];
@@ -5972,36 +7488,48 @@ class Compiler
5972
  {
5973
  list($list1, $value, $sep) = $args;
5974
 
5975
- $list1 = $this->coerceList($list1, ' ');
5976
- $sep = $this->listSeparatorForJoin($list1, $sep);
 
 
 
 
 
5977
 
5978
- return [Type::T_LIST, $sep, array_merge($list1[2], [$value])];
5979
  }
5980
 
5981
  protected function libZip($args)
5982
  {
5983
- foreach ($args as $arg) {
5984
- $this->assertList($arg);
5985
  }
5986
 
5987
  $lists = [];
5988
  $firstList = array_shift($args);
5989
 
5990
- foreach ($firstList[2] as $key => $item) {
5991
- $list = [Type::T_LIST, '', [$item]];
 
 
5992
 
5993
- foreach ($args as $arg) {
5994
- if (isset($arg[2][$key])) {
5995
- $list[2][] = $arg[2][$key];
5996
- } else {
5997
- break 2;
 
5998
  }
 
 
5999
  }
6000
 
6001
- $lists[] = $list;
 
 
6002
  }
6003
 
6004
- return [Type::T_LIST, ',', $lists];
6005
  }
6006
 
6007
  protected static $libTypeOf = ['value'];
@@ -6023,6 +7551,9 @@ class Compiler
6023
  case Type::T_FUNCTION:
6024
  return 'string';
6025
 
 
 
 
6026
  case Type::T_LIST:
6027
  if (isset($value[3]) && $value[3]) {
6028
  return 'arglist';
@@ -6054,17 +7585,19 @@ class Compiler
6054
  return $value[0] === Type::T_NUMBER && $value->unitless();
6055
  }
6056
 
6057
- protected static $libComparable = ['number-1', 'number-2'];
 
 
 
6058
  protected function libComparable($args)
6059
  {
6060
  list($number1, $number2) = $args;
6061
 
6062
- if (! isset($number1[0]) || $number1[0] !== Type::T_NUMBER ||
 
6063
  ! isset($number2[0]) || $number2[0] !== Type::T_NUMBER
6064
  ) {
6065
- $this->throwError('Invalid argument(s) for "comparable"');
6066
-
6067
- return null;
6068
  }
6069
 
6070
  $number1 = $number1->normalize();
@@ -6076,13 +7609,17 @@ class Compiler
6076
  protected static $libStrIndex = ['string', 'substring'];
6077
  protected function libStrIndex($args)
6078
  {
6079
- $string = $this->coerceString($args[0]);
6080
  $stringContent = $this->compileStringContent($string);
6081
 
6082
- $substring = $this->coerceString($args[1]);
6083
  $substringContent = $this->compileStringContent($substring);
6084
 
6085
- $result = strpos($stringContent, $substringContent);
 
 
 
 
6086
 
6087
  return $result === false ? static::$null : new Node\Number($result + 1, '');
6088
  }
@@ -6090,15 +7627,25 @@ class Compiler
6090
  protected static $libStrInsert = ['string', 'insert', 'index'];
6091
  protected function libStrInsert($args)
6092
  {
6093
- $string = $this->coerceString($args[0]);
6094
  $stringContent = $this->compileStringContent($string);
6095
 
6096
- $insert = $this->coerceString($args[1]);
6097
  $insertContent = $this->compileStringContent($insert);
6098
 
6099
- list(, $index) = $args[2];
 
 
 
 
 
 
6100
 
6101
- $string[2] = [substr_replace($stringContent, $insertContent, $index - 1, 0)];
 
 
 
 
6102
 
6103
  return $string;
6104
  }
@@ -6106,16 +7653,16 @@ class Compiler
6106
  protected static $libStrLength = ['string'];
6107
  protected function libStrLength($args)
6108
  {
6109
- $string = $this->coerceString($args[0]);
6110
  $stringContent = $this->compileStringContent($string);
6111
 
6112
- return new Node\Number(strlen($stringContent), '');
6113
  }
6114
 
6115
- protected static $libStrSlice = ['string', 'start-at', 'end-at:null'];
6116
  protected function libStrSlice($args)
6117
  {
6118
- if (isset($args[2]) && $args[2][1] == 0) {
6119
  return static::$nullString;
6120
  }
6121
 
@@ -6128,7 +7675,7 @@ class Compiler
6128
  $start--;
6129
  }
6130
 
6131
- $end = (int) $args[2][1];
6132
  $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end);
6133
 
6134
  $string[2] = $length
@@ -6144,7 +7691,7 @@ class Compiler
6144
  $string = $this->coerceString($args[0]);
6145
  $stringContent = $this->compileStringContent($string);
6146
 
6147
- $string[2] = [function_exists('mb_strtolower') ? mb_strtolower($stringContent) : strtolower($stringContent)];
6148
 
6149
  return $string;
6150
  }
@@ -6155,7 +7702,7 @@ class Compiler
6155
  $string = $this->coerceString($args[0]);
6156
  $stringContent = $this->compileStringContent($string);
6157
 
6158
- $string[2] = [function_exists('mb_strtoupper') ? mb_strtoupper($stringContent) : strtoupper($stringContent)];
6159
 
6160
  return $string;
6161
  }
@@ -6167,7 +7714,7 @@ class Compiler
6167
  $name = $this->compileStringContent($string);
6168
 
6169
  return $this->toBool(
6170
- array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false
6171
  );
6172
  }
6173
 
@@ -6191,7 +7738,7 @@ class Compiler
6191
  // built-in functions
6192
  $f = $this->getBuiltinFunction($name);
6193
 
6194
- return $this->toBool(is_callable($f));
6195
  }
6196
 
6197
  protected static $libGlobalVariableExists = ['name'];
@@ -6235,22 +7782,25 @@ class Compiler
6235
  return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']];
6236
  }
6237
 
6238
- protected static $libRandom = ['limit'];
6239
  protected function libRandom($args)
6240
  {
6241
- if (isset($args[0])) {
6242
  $n = $this->assertNumber($args[0]);
6243
 
6244
  if ($n < 1) {
6245
- $this->throwError("limit must be greater than or equal to 1");
 
6246
 
6247
- return null;
 
6248
  }
6249
 
6250
- return new Node\Number(mt_rand(1, $n), '');
6251
  }
6252
 
6253
- return new Node\Number(mt_rand(1, mt_getrandmax()), '');
 
6254
  }
6255
 
6256
  protected function libUniqueId()
@@ -6258,7 +7808,9 @@ class Compiler
6258
  static $id;
6259
 
6260
  if (! isset($id)) {
6261
- $id = mt_rand(0, pow(36, 8));
 
 
6262
  }
6263
 
6264
  $id += mt_rand(0, 10) + 1;
@@ -6266,14 +7818,47 @@ class Compiler
6266
  return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
6267
  }
6268
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6269
  protected static $libInspect = ['value'];
6270
  protected function libInspect($args)
6271
  {
6272
- if ($args[0] === static::$null) {
6273
- return [Type::T_KEYWORD, 'null'];
6274
- }
6275
 
6276
- return $args[0];
6277
  }
6278
 
6279
  /**
@@ -6283,14 +7868,21 @@ class Compiler
6283
  *
6284
  * @return array|boolean
6285
  */
6286
- protected function getSelectorArg($arg)
6287
  {
6288
  static $parser = null;
6289
 
6290
- if (is_null($parser)) {
6291
  $parser = $this->parserFactory(__METHOD__);
6292
  }
6293
 
 
 
 
 
 
 
 
6294
  $arg = $this->libUnquote([$arg]);
6295
  $arg = $this->compileValue($arg);
6296
 
@@ -6300,10 +7892,44 @@ class Compiler
6300
  $selector = $this->evalSelectors($parsedSelector);
6301
  $gluedSelector = $this->glueFunctionSelectors($selector);
6302
 
 
 
 
 
 
 
 
 
 
 
 
6303
  return $gluedSelector;
6304
  }
6305
 
6306
- return false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6307
  }
6308
 
6309
  /**
@@ -6325,8 +7951,8 @@ class Compiler
6325
  {
6326
  list($super, $sub) = $args;
6327
 
6328
- $super = $this->getSelectorArg($super);
6329
- $sub = $this->getSelectorArg($sub);
6330
 
6331
  return $this->isSuperSelector($super, $sub);
6332
  }
@@ -6342,12 +7968,30 @@ class Compiler
6342
  protected function isSuperSelector($super, $sub)
6343
  {
6344
  // one and only one selector for each arg
6345
- if (! $super || count($super) !== 1) {
6346
- $this->throwError("Invalid super selector for isSuperSelector()");
 
 
 
 
 
 
 
 
 
 
 
 
 
6347
  }
6348
 
6349
- if (! $sub || count($sub) !== 1) {
6350
- $this->throwError("Invalid sub selector for isSuperSelector()");
 
 
 
 
 
6351
  }
6352
 
6353
  $super = reset($super);
@@ -6374,7 +8018,7 @@ class Compiler
6374
  $nextMustMatch = true;
6375
  $i++;
6376
  } else {
6377
- while ($i < count($sub) && ! $this->isSuperPart($node, $sub[$i])) {
6378
  if ($nextMustMatch) {
6379
  return false;
6380
  }
@@ -6382,7 +8026,7 @@ class Compiler
6382
  $i++;
6383
  }
6384
 
6385
- if ($i >= count($sub)) {
6386
  return false;
6387
  }
6388
 
@@ -6407,11 +8051,11 @@ class Compiler
6407
  $i = 0;
6408
 
6409
  foreach ($superParts as $superPart) {
6410
- while ($i < count($subParts) && $subParts[$i] !== $superPart) {
6411
  $i++;
6412
  }
6413
 
6414
- if ($i >= count($subParts)) {
6415
  return false;
6416
  }
6417
 
@@ -6427,11 +8071,15 @@ class Compiler
6427
  // get the selector... list
6428
  $args = reset($args);
6429
  $args = $args[2];
6430
- if (count($args) < 1) {
6431
- $this->throwError("selector-append() needs at least 1 argument");
 
6432
  }
6433
 
6434
- $selectors = array_map([$this, 'getSelectorArg'], $args);
 
 
 
6435
 
6436
  return $this->formatOutputSelector($this->selectorAppend($selectors));
6437
  }
@@ -6450,14 +8098,14 @@ class Compiler
6450
  $lastSelectors = array_pop($selectors);
6451
 
6452
  if (! $lastSelectors) {
6453
- $this->throwError("Invalid selector list in selector-append()");
6454
  }
6455
 
6456
- while (count($selectors)) {
6457
  $previousSelectors = array_pop($selectors);
6458
 
6459
  if (! $previousSelectors) {
6460
- $this->throwError("Invalid selector list in selector-append()");
6461
  }
6462
 
6463
  // do the trick, happening $lastSelector to $previousSelector
@@ -6487,17 +8135,20 @@ class Compiler
6487
  return $lastSelectors;
6488
  }
6489
 
6490
- protected static $libSelectorExtend = ['selectors', 'extendee', 'extender'];
 
 
 
6491
  protected function libSelectorExtend($args)
6492
  {
6493
  list($selectors, $extendee, $extender) = $args;
6494
 
6495
- $selectors = $this->getSelectorArg($selectors);
6496
- $extendee = $this->getSelectorArg($extendee);
6497
- $extender = $this->getSelectorArg($extender);
6498
 
6499
  if (! $selectors || ! $extendee || ! $extender) {
6500
- $this->throwError("selector-extend() invalid arguments");
6501
  }
6502
 
6503
  $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender);
@@ -6505,17 +8156,20 @@ class Compiler
6505
  return $this->formatOutputSelector($extended);
6506
  }
6507
 
6508
- protected static $libSelectorReplace = ['selectors', 'original', 'replacement'];
 
 
 
6509
  protected function libSelectorReplace($args)
6510
  {
6511
  list($selectors, $original, $replacement) = $args;
6512
 
6513
- $selectors = $this->getSelectorArg($selectors);
6514
- $original = $this->getSelectorArg($original);
6515
- $replacement = $this->getSelectorArg($replacement);
6516
 
6517
  if (! $selectors || ! $original || ! $replacement) {
6518
- $this->throwError("selector-replace() invalid arguments");
6519
  }
6520
 
6521
  $replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true);
@@ -6554,12 +8208,12 @@ class Compiler
6554
  $extended[] = $selector;
6555
  }
6556
 
6557
- $n = count($extended);
6558
 
6559
  $this->matchExtends($selector, $extended);
6560
 
6561
  // if didnt match, keep the original selector if we are in a replace operation
6562
- if ($replace and count($extended) === $n) {
6563
  $extended[] = $selector;
6564
  }
6565
  }
@@ -6576,13 +8230,18 @@ class Compiler
6576
  // get the selector... list
6577
  $args = reset($args);
6578
  $args = $args[2];
6579
- if (count($args) < 1) {
6580
- $this->throwError("selector-nest() needs at least 1 argument");
 
6581
  }
6582
 
6583
- $selectorsMap = array_map([$this, 'getSelectorArg'], $args);
 
 
 
6584
 
6585
  $envs = [];
 
6586
  foreach ($selectorsMap as $selectors) {
6587
  $env = new Environment();
6588
  $env->selectors = $selectors;
@@ -6590,18 +8249,21 @@ class Compiler
6590
  $envs[] = $env;
6591
  }
6592
 
6593
- $envs = array_reverse($envs);
6594
- $env = $this->extractEnv($envs);
6595
  $outputSelectors = $this->multiplySelectors($env);
6596
 
6597
  return $this->formatOutputSelector($outputSelectors);
6598
  }
6599
 
6600
- protected static $libSelectorParse = ['selectors'];
 
 
 
6601
  protected function libSelectorParse($args)
6602
  {
6603
  $selectors = reset($args);
6604
- $selectors = $this->getSelectorArg($selectors);
6605
 
6606
  return $this->formatOutputSelector($selectors);
6607
  }
@@ -6611,11 +8273,11 @@ class Compiler
6611
  {
6612
  list($selectors1, $selectors2) = $args;
6613
 
6614
- $selectors1 = $this->getSelectorArg($selectors1);
6615
- $selectors2 = $this->getSelectorArg($selectors2);
6616
 
6617
  if (! $selectors1 || ! $selectors2) {
6618
- $this->throwError("selector-unify() invalid arguments");
6619
  }
6620
 
6621
  // only consider the first compound of each
@@ -6634,22 +8296,23 @@ class Compiler
6634
  *
6635
  * @param array $compound1
6636
  * @param array $compound2
 
6637
  * @return array|mixed
6638
  */
6639
  protected function unifyCompoundSelectors($compound1, $compound2)
6640
  {
6641
- if (! count($compound1)) {
6642
  return $compound2;
6643
  }
6644
 
6645
- if (! count($compound2)) {
6646
  return $compound1;
6647
  }
6648
 
6649
  // check that last part are compatible
6650
  $lastPart1 = array_pop($compound1);
6651
  $lastPart2 = array_pop($compound2);
6652
- $last = $this->mergeParts($lastPart1, $lastPart2);
6653
 
6654
  if (! $last) {
6655
  return [[]];
@@ -6659,7 +8322,7 @@ class Compiler
6659
  $unifiedSelectors = [$unifiedCompound];
6660
 
6661
  // do the rest
6662
- while (count($compound1) || count($compound2)) {
6663
  $part1 = end($compound1);
6664
  $part2 = end($compound2);
6665
 
@@ -6672,6 +8335,7 @@ class Compiler
6672
 
6673
  $c = $this->mergeParts($part1, $part2);
6674
  $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
 
6675
  $part1 = $part2 = null;
6676
 
6677
  array_pop($compound1);
@@ -6686,6 +8350,7 @@ class Compiler
6686
 
6687
  $c = $this->mergeParts($part2, $part1);
6688
  $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
 
6689
  $part1 = $part2 = null;
6690
 
6691
  array_pop($compound2);
@@ -6697,9 +8362,9 @@ class Compiler
6697
  array_pop($compound1);
6698
  array_pop($compound2);
6699
 
6700
- $s = $this->prependSelectors($unifiedSelectors, [$part2]);
6701
  $new = array_merge($new, $this->prependSelectors($s, [$part1]));
6702
- $s = $this->prependSelectors($unifiedSelectors, [$part1]);
6703
  $new = array_merge($new, $this->prependSelectors($s, [$part2]));
6704
  } elseif ($part1) {
6705
  array_pop($compound1);
@@ -6753,11 +8418,11 @@ class Compiler
6753
  protected function matchPartInCompound($part, $compound)
6754
  {
6755
  $partTag = $this->findTagName($part);
6756
- $before = $compound;
6757
- $after = [];
6758
 
6759
  // try to find a match by tag name first
6760
- while (count($before)) {
6761
  $p = array_pop($before);
6762
 
6763
  if ($partTag && $partTag !== '*' && $partTag == $this->findTagName($p)) {
@@ -6771,11 +8436,11 @@ class Compiler
6771
  $before = $compound;
6772
  $after = [];
6773
 
6774
- while (count($before)) {
6775
  $p = array_pop($before);
6776
 
6777
  if ($this->checkCompatibleTags($partTag, $this->findTagName($p))) {
6778
- if (count(array_intersect($part, $p))) {
6779
  return [$before, $p, $after];
6780
  }
6781
  }
@@ -6800,7 +8465,7 @@ class Compiler
6800
  {
6801
  $tag1 = $this->findTagName($parts1);
6802
  $tag2 = $this->findTagName($parts2);
6803
- $tag = $this->checkCompatibleTags($tag1, $tag2);
6804
 
6805
  // not compatible tags
6806
  if ($tag === false) {
@@ -6851,12 +8516,12 @@ class Compiler
6851
  $tags = array_unique($tags);
6852
  $tags = array_filter($tags);
6853
 
6854
- if (count($tags)>1) {
6855
  $tags = array_diff($tags, ['*']);
6856
  }
6857
 
6858
  // not compatible nodes
6859
- if (count($tags)>1) {
6860
  return false;
6861
  }
6862
 
@@ -6885,7 +8550,7 @@ class Compiler
6885
  protected function libSimpleSelectors($args)
6886
  {
6887
  $selector = reset($args);
6888
- $selector = $this->getSelectorArg($selector);
6889
 
6890
  // remove selectors list layer, keeping the first one
6891
  $selector = reset($selector);
@@ -6901,4 +8566,23 @@ class Compiler
6901
 
6902
  return [Type::T_LIST, ',', $listParts];
6903
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6904
  }
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
74
  /**
75
  * @var array
76
  */
77
+ protected static $operatorNames = [
78
  '+' => 'add',
79
  '-' => 'sub',
80
  '*' => 'mul',
94
  /**
95
  * @var array
96
  */
97
+ protected static $namespaces = [
98
  'special' => '%',
99
  'mixin' => '@',
100
  'function' => '^',
101
  ];
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, '', []];
109
+ public static $defaultValue = [Type::T_KEYWORD, ''];
110
+ public static $selfSelector = [Type::T_SELF];
111
+ public static $emptyList = [Type::T_LIST, '', []];
112
+ public static $emptyMap = [Type::T_MAP, [], []];
113
+ public static $emptyString = [Type::T_STRING, '"', []];
114
+ public static $with = [Type::T_KEYWORD, 'with'];
115
+ public static $without = [Type::T_KEYWORD, 'without'];
116
 
117
  protected $importPaths = [''];
118
  protected $importCache = [];
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
  {
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
  {
193
  $options = [
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
  *
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
  }
257
  $this->storeEnv = null;
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);
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) {
307
  'out' => &$out,
308
  ];
309
 
310
+ $this->cache->setCache('compile', $cacheKey, $v, $compileOptions);
311
  }
312
 
313
  return $out;
322
  */
323
  protected function parserFactory($path)
324
  {
325
+ // https://sass-lang.com/documentation/at-rules/import
326
+ // CSS files imported by Sass don’t allow any special Sass features.
327
+ // In order to make sure authors don’t accidentally write Sass in their CSS,
328
+ // all Sass features that aren’t also valid CSS will produce errors.
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);
352
  protected function isSelfExtend($target, $origin)
353
  {
354
  foreach ($origin as $sel) {
355
+ if (\in_array($target, $sel)) {
356
  return true;
357
  }
358
  }
363
  /**
364
  * Push extends
365
  *
366
+ * @param array $target
367
+ * @param array $origin
368
+ * @param array|null $block
369
  */
370
  protected function pushExtends($target, $origin, $block)
371
  {
372
+ $i = \count($this->extends);
 
 
 
 
373
  $this->extends[] = [$target, $origin, $block];
374
 
375
  foreach ($target as $part) {
391
  */
392
  protected function makeOutputBlock($type, $selectors = null)
393
  {
394
+ $out = new OutputBlock();
395
+ $out->type = $type;
396
+ $out->lines = [];
397
+ $out->children = [];
398
+ $out->parent = $this->scope;
399
+ $out->selectors = $selectors;
400
+ $out->depth = $this->env->depth;
401
 
402
  if ($this->env->block instanceof Block) {
403
  $out->sourceName = $this->env->block->sourceName;
447
  $origin = $this->collapseSelectors($origin);
448
 
449
  $this->sourceLine = $block[Parser::SOURCE_LINE];
450
+ throw $this->error("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
451
  }
452
  }
453
 
465
  foreach ($block->selectors as $s) {
466
  $selectors[] = $s;
467
 
468
+ if (! \is_array($s)) {
469
  continue;
470
  }
471
 
498
  $block->selectors[] = $this->compileSelector($selector);
499
  }
500
 
501
+ if ($placeholderSelector && 0 === \count($block->selectors) && null !== $parentKey) {
502
  unset($block->parent->children[$parentKey]);
503
 
504
  return;
522
  $new = [];
523
 
524
  foreach ($parts as $part) {
525
+ if (\is_array($part)) {
526
  $part = $this->glueFunctionSelectors($part);
527
  $new[] = $part;
528
  } else {
529
  // a selector part finishing with a ) is the last part of a :not( or :nth-child(
530
  // and need to be joined to this
531
+ if (
532
+ \count($new) && \is_string($new[\count($new) - 1]) &&
533
+ \strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
534
  ) {
535
+ while (\count($new) > 1 && substr($new[\count($new) - 1], -1) !== '(') {
536
+ $part = array_pop($new) . $part;
537
+ }
538
+ $new[\count($new) - 1] .= $part;
539
  } else {
540
  $new[] = $part;
541
  }
556
  protected function matchExtends($selector, &$out, $from = 0, $initial = true)
557
  {
558
  static $partsPile = [];
 
559
  $selector = $this->glueFunctionSelectors($selector);
560
 
561
+ if (\count($selector) == 1 && \in_array(reset($selector), $partsPile)) {
562
  return;
563
  }
564
 
565
+ $outRecurs = [];
566
+
567
  foreach ($selector as $i => $part) {
568
  if ($i < $from) {
569
  continue;
571
 
572
  // check that we are not building an infinite loop of extensions
573
  // if the new part is just including a previous part don't try to extend anymore
574
+ if (\count($part) > 1) {
575
  foreach ($partsPile as $previousPart) {
576
+ if (! \count(array_diff($previousPart, $part))) {
577
  continue 2;
578
  }
579
  }
580
  }
581
 
582
+ $partsPile[] = $part;
 
 
 
583
 
584
+ if ($this->matchExtendsSingle($part, $origin, $initial)) {
585
+ $after = \array_slice($selector, $i + 1);
586
+ $before = \array_slice($selector, 0, $i);
587
  list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
588
 
589
  foreach ($origin as $new) {
590
  $k = 0;
591
 
592
  // remove shared parts
593
+ if (\count($new) > 1) {
594
  while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
595
  $k++;
596
  }
597
  }
598
 
599
+ if (\count($nonBreakableBefore) && $k === \count($new)) {
600
+ $k--;
601
+ }
602
+
603
  $replacement = [];
604
+ $tempReplacement = $k > 0 ? \array_slice($new, $k) : $new;
605
 
606
+ for ($l = \count($tempReplacement) - 1; $l >= 0; $l--) {
607
  $slice = [];
608
 
609
  foreach ($tempReplacement[$l] as $chunk) {
610
+ if (! \in_array($chunk, $slice)) {
611
  $slice[] = $chunk;
612
  }
613
  }
619
  }
620
  }
621
 
622
+ $afterBefore = $l != 0 ? \array_slice($tempReplacement, 0, $l) : [];
623
 
624
  // Merge shared direct relationships.
625
  $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore);
635
  continue;
636
  }
637
 
638
+ $this->pushOrMergeExtentedSelector($out, $result);
639
 
640
  // recursively check for more matches
641
+ $startRecurseFrom = \count($before) + min(\count($nonBreakableBefore), \count($mergedBefore));
642
+
643
+ if (\count($origin) > 1) {
644
+ $this->matchExtends($result, $out, $startRecurseFrom, false);
645
+ } else {
646
+ $this->matchExtends($result, $outRecurs, $startRecurseFrom, false);
647
+ }
648
 
649
  // selector sequence merging
650
+ if (! empty($before) && \count($new) > 1) {
651
+ $preSharedParts = $k > 0 ? \array_slice($before, 0, $k) : [];
652
+ $postSharedParts = $k > 0 ? \array_slice($before, $k) : $before;
653
 
654
+ list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore);
655
 
656
  $result2 = array_merge(
657
  $preSharedParts,
658
  $betweenSharedParts,
659
  $postSharedParts,
660
+ $nonBreakabl2,
661
  $nonBreakableBefore,
662
  $replacement,
663
  $after
664
  );
665
 
666
+ $this->pushOrMergeExtentedSelector($out, $result2);
667
  }
668
  }
669
+ }
670
+ array_pop($partsPile);
671
+ }
672
+
673
+ while (\count($outRecurs)) {
674
+ $result = array_shift($outRecurs);
675
+ $this->pushOrMergeExtentedSelector($out, $result);
676
+ }
677
+ }
678
 
679
+ /**
680
+ * Test a part for being a pseudo selector
681
+ *
682
+ * @param string $part
683
+ * @param array $matches
684
+ *
685
+ * @return boolean
686
+ */
687
+ protected function isPseudoSelector($part, &$matches)
688
+ {
689
+ if (
690
+ strpos($part, ':') === 0 &&
691
+ preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)
692
+ ) {
693
+ return true;
694
+ }
695
+
696
+ return false;
697
+ }
698
+
699
+ /**
700
+ * Push extended selector except if
701
+ * - this is a pseudo selector
702
+ * - same as previous
703
+ * - in a white list
704
+ * in this case we merge the pseudo selector content
705
+ *
706
+ * @param array $out
707
+ * @param array $extended
708
+ */
709
+ protected function pushOrMergeExtentedSelector(&$out, $extended)
710
+ {
711
+ if (\count($out) && \count($extended) === 1 && \count(reset($extended)) === 1) {
712
+ $single = reset($extended);
713
+ $part = reset($single);
714
+
715
+ if (
716
+ $this->isPseudoSelector($part, $matchesExtended) &&
717
+ \in_array($matchesExtended[1], [ 'slotted' ])
718
+ ) {
719
+ $prev = end($out);
720
+ $prev = $this->glueFunctionSelectors($prev);
721
+
722
+ if (\count($prev) === 1 && \count(reset($prev)) === 1) {
723
+ $single = reset($prev);
724
+ $part = reset($single);
725
+
726
+ if (
727
+ $this->isPseudoSelector($part, $matchesPrev) &&
728
+ $matchesPrev[1] === $matchesExtended[1]
729
+ ) {
730
+ $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2);
731
+ $extended[1] = $matchesPrev[2] . ', ' . $extended[1];
732
+ $extended = implode($matchesExtended[1] . '(', $extended);
733
+ $extended = [ [ $extended ]];
734
+ array_pop($out);
735
+ }
736
+ }
737
  }
738
  }
739
+ $out[] = $extended;
740
  }
741
 
742
  /**
743
  * Match extends single
744
  *
745
+ * @param array $rawSingle
746
+ * @param array $outOrigin
747
+ * @param boolean $initial
748
  *
749
  * @return boolean
750
  */
751
+ protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true)
752
  {
753
  $counts = [];
754
  $single = [];
755
 
756
  // simple usual cases, no need to do the whole trick
757
+ if (\in_array($rawSingle, [['>'],['+'],['~']])) {
758
  return false;
759
  }
760
 
761
  foreach ($rawSingle as $part) {
762
  // matches Number
763
+ if (! \is_string($part)) {
764
  return false;
765
  }
766
 
767
+ if (! preg_match('/^[\[.:#%]/', $part) && \count($single)) {
768
+ $single[\count($single) - 1] .= $part;
769
  } else {
770
  $single[] = $part;
771
  }
773
 
774
  $extendingDecoratedTag = false;
775
 
776
+ if (\count($single) > 1) {
777
  $matches = null;
778
  $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
779
  }
780
 
781
+ $outOrigin = [];
782
+ $found = false;
783
+
784
+ foreach ($single as $k => $part) {
785
  if (isset($this->extendsMap[$part])) {
786
  foreach ($this->extendsMap[$part] as $idx) {
787
  $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
788
  }
789
  }
 
790
 
791
+ if (
792
+ $initial &&
793
+ $this->isPseudoSelector($part, $matches) &&
794
+ ! \in_array($matches[1], [ 'not' ])
795
+ ) {
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);
803
+
804
+ if ($subExtended) {
805
+ $subSelectorsExtended = $subSelectors;
806
+ $subSelectorsExtended[$ksub] = $subExtended;
807
+
808
+ foreach ($subSelectorsExtended as $ksse => $sse) {
809
+ $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse);
810
+ }
811
+
812
+ $subSelectorsExtended = implode(', ', $subSelectorsExtended);
813
+ $singleExtended = $single;
814
+ $singleExtended[$k] = str_replace('(' . $buffer . ')', "($subSelectorsExtended)", $part);
815
+ $outOrigin[] = [ $singleExtended ];
816
+ $found = true;
817
+ }
818
+ }
819
+ }
820
+ }
821
+ }
822
 
823
  foreach ($counts as $idx => $count) {
824
  list($target, $origin, /* $block */) = $this->extends[$idx];
826
  $origin = $this->glueFunctionSelectors($origin);
827
 
828
  // check count
829
+ if ($count !== \count($target)) {
830
  continue;
831
  }
832
 
836
 
837
  foreach ($origin as $j => $new) {
838
  // prevent infinite loop when target extends itself
839
+ if ($this->isSelfExtend($single, $origin) && ! $initial) {
840
  return false;
841
  }
842
 
843
  $replacement = end($new);
844
 
845
  // Extending a decorated tag with another tag is not possible.
846
+ if (
847
+ $extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
848
  preg_match('/^[a-z0-9]+$/i', $replacement[0])
849
  ) {
850
  unset($origin[$j]);
853
 
854
  $combined = $this->combineSelectorSingle($replacement, $rem);
855
 
856
+ if (\count(array_diff($combined, $origin[$j][\count($origin[$j]) - 1]))) {
857
+ $origin[$j][\count($origin[$j]) - 1] = $combined;
858
  }
859
  }
860
 
882
  {
883
  $parents = [];
884
  $children = [];
885
+
886
+ $j = $i = \count($fragment);
887
 
888
  for (;;) {
889
+ $children = $j != $i ? \array_slice($fragment, $j, $i - $j) : [];
890
+ $parents = \array_slice($fragment, 0, $j);
891
+ $slice = end($parents);
892
 
893
  if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
894
  break;
910
  */
911
  protected function combineSelectorSingle($base, $other)
912
  {
913
+ $tag = [];
914
+ $out = [];
915
+ $wasTag = false;
916
+ $pseudo = [];
917
+
918
+ while (\count($other) && strpos(end($other), ':') === 0) {
919
+ array_unshift($pseudo, array_pop($other));
920
+ }
921
+
922
+ foreach ([array_reverse($base), array_reverse($other)] as $single) {
923
+ $rang = count($single);
924
 
 
925
  foreach ($single as $part) {
926
+ if (preg_match('/^[\[:]/', $part)) {
927
  $out[] = $part;
928
  $wasTag = false;
929
+ } elseif (preg_match('/^[\.#]/', $part)) {
930
+ array_unshift($out, $part);
931
+ $wasTag = false;
932
+ } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) {
933
  $tag[] = $part;
934
  $wasTag = true;
935
  } elseif ($wasTag) {
936
+ $tag[\count($tag) - 1] .= $part;
937
  } else {
938
+ array_unshift($out, $part);
939
  }
940
+ $rang--;
941
  }
942
  }
943
 
944
+ if (\count($tag)) {
945
  array_unshift($out, $tag[0]);
946
  }
947
 
948
+ while (\count($pseudo)) {
949
+ $out[] = array_shift($pseudo);
950
+ }
951
+
952
  return $out;
953
  }
954
 
980
  foreach ($media->children as $child) {
981
  $type = $child[0];
982
 
983
+ if (
984
+ $type !== Type::T_BLOCK &&
985
  $type !== Type::T_MEDIA &&
986
  $type !== Type::T_DIRECTIVE &&
987
  $type !== Type::T_IMPORT
992
  }
993
 
994
  if ($needsWrap) {
995
+ $wrapped = new Block();
996
+ $wrapped->sourceName = $media->sourceName;
997
+ $wrapped->sourceIndex = $media->sourceIndex;
998
+ $wrapped->sourceLine = $media->sourceLine;
999
  $wrapped->sourceColumn = $media->sourceColumn;
1000
+ $wrapped->selectors = [];
1001
+ $wrapped->comments = [];
1002
+ $wrapped->parent = $media;
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;
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
  {
1068
+ if (\is_array($directive)) {
1069
+ $directiveName = $this->compileDirectiveName($directive[0]);
1070
+ $s = '@' . $directiveName;
1071
 
1072
+ if (! empty($directive[1])) {
1073
+ $s .= ' ' . $this->compileValue($directive[1]);
1074
+ }
1075
+ // sass-spec compliance on newline after directives, a bit tricky :/
1076
+ $appendNewLine = (! empty($directive[2]) || strpos($s, "\n")) ? "\n" : "";
1077
+ if (\is_array($directive[0]) && empty($directive[1])) {
1078
+ $appendNewLine = "\n";
1079
+ }
1080
 
1081
+ if (empty($directive[3])) {
1082
+ $this->appendRootDirective($s . ';' . $appendNewLine, $out, [Type::T_COMMENT, Type::T_DIRECTIVE]);
1083
+ } else {
1084
+ $this->appendOutputLine($out, Type::T_DIRECTIVE, $s . ';');
1085
+ }
1086
  } else {
1087
+ $directive->name = $this->compileDirectiveName($directive->name);
1088
+ $s = '@' . $directive->name;
1089
+
1090
+ if (! empty($directive->value)) {
1091
+ $s .= ' ' . $this->compileValue($directive->value);
1092
+ }
1093
+
1094
+ if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') {
1095
+ $this->compileKeyframeBlock($directive, [$s]);
1096
+ } else {
1097
+ $this->compileNestedBlock($directive, [$s]);
1098
+ }
1099
  }
1100
  }
1101
 
1102
+ /**
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)
1110
+ {
1111
+ if (is_string($directiveName)) {
1112
+ return $directiveName;
1113
+ }
1114
+
1115
+ return $this->compileValue($directiveName);
1116
+ }
1117
+
1118
  /**
1119
  * Compile at-root
1120
  *
1128
 
1129
  // wrap inline selector
1130
  if ($block->selector) {
1131
+ $wrapped = new Block();
1132
  $wrapped->sourceName = $block->sourceName;
1133
  $wrapped->sourceIndex = $block->sourceIndex;
1134
  $wrapped->sourceLine = $block->sourceLine;
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
  ) {
1153
  $selfParent = $block->parent;
1180
  protected function filterScopeWithWithout($scope, $with, $without)
1181
  {
1182
  $filteredScopes = [];
1183
+ $childStash = [];
1184
 
1185
  if ($scope->type === TYPE::T_ROOT) {
1186
  return $scope;
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
  }
1194
 
1200
  if ($this->isWith($scope, $with, $without)) {
1201
  $s = clone $scope;
1202
  $s->children = [];
1203
+ $s->lines = [];
1204
+ $s->parent = null;
1205
 
1206
  if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) {
1207
  $s->selectors = [];
1210
  $filteredScopes[] = $s;
1211
  }
1212
 
1213
+ if (\count($childStash)) {
1214
+ $scope = array_shift($childStash);
1215
+ } elseif ($scope->children) {
1216
  $scope = end($scope->children);
1217
  } else {
1218
  $scope = null;
1219
  }
1220
  }
1221
 
1222
+ if (! \count($filteredScopes)) {
1223
  return $this->rootBlock;
1224
  }
1225
 
1230
 
1231
  $p = &$newScope;
1232
 
1233
+ while (\count($filteredScopes)) {
1234
  $s = array_shift($filteredScopes);
1235
  $s->parent = $p;
1236
+ $p->children[] = $s;
1237
+ $newScope = &$p->children[0];
1238
+ $p = &$p->children[0];
1239
  }
1240
 
1241
  return $newScope;
1252
  */
1253
  protected function completeScope($scope, $previousScope)
1254
  {
1255
+ if (! $scope->type && (! $scope->selectors || ! \count($scope->selectors)) && \count($scope->lines)) {
1256
  $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth);
1257
  }
1258
 
1304
  $without = ['rule' => true];
1305
 
1306
  if ($withCondition) {
1307
+ if ($withCondition[0] === Type::T_INTERPOLATE) {
1308
+ $w = $this->compileValue($withCondition);
1309
+
1310
+ $buffer = "($w)";
1311
+ $parser = $this->parserFactory(__METHOD__);
1312
+
1313
+ if ($parser->parseValue($buffer, $reParsedWith)) {
1314
+ $withCondition = $reParsedWith;
1315
+ }
1316
+ }
1317
+
1318
  if ($this->libMapHasKey([$withCondition, static::$with])) {
1319
  $without = []; // cancel the default
1320
  $list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
1344
  /**
1345
  * Filter env stack
1346
  *
1347
+ * @param array $envs
1348
  * @param array $with
1349
  * @param array $without
1350
  *
1357
  foreach ($envs as $e) {
1358
  if ($e->block && ! $this->isWith($e->block, $with, $without)) {
1359
  $ec = clone $e;
1360
+ $ec->block = null;
1361
  $ec->selectors = [];
1362
+
1363
  $filtered[] = $ec;
1364
  } else {
1365
  $filtered[] = $e;
1387
 
1388
  if ($block->type === Type::T_DIRECTIVE) {
1389
  if (isset($block->name)) {
1390
+ return $this->testWithWithout($this->compileDirectiveName($block->name), $with, $without);
1391
+ } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
 
1392
  return $this->testWithWithout($m[1], $with, $without);
1393
+ } else {
 
1394
  return $this->testWithWithout('???', $with, $without);
1395
  }
1396
  }
1397
+ } elseif (isset($block->selectors)) {
1398
+ // a selector starting with number is a keyframe rule
1399
+ if (\count($block->selectors)) {
1400
+ $s = reset($block->selectors);
1401
+
1402
+ while (\is_array($s)) {
1403
+ $s = reset($s);
1404
+ }
1405
+
1406
+ if (\is_object($s) && $s instanceof Node\Number) {
1407
+ return $this->testWithWithout('keyframes', $with, $without);
1408
+ }
1409
+ }
1410
+
1411
  return $this->testWithWithout('rule', $with, $without);
1412
  }
1413
 
1420
  * @param string $what
1421
  * @param array $with
1422
  * @param array $without
1423
+ *
1424
+ * @return boolean
1425
  * true if the block should be kept, false to reject
1426
  */
1427
+ protected function testWithWithout($what, $with, $without)
1428
+ {
1429
  // if without, reject only if in the list (or 'all' is in the list)
1430
+ if (\count($without)) {
1431
  return (isset($without[$what]) || isset($without['all'])) ? false : true;
1432
  }
1433
 
1467
  /**
1468
  * Compile nested properties lines
1469
  *
1470
+ * @param \ScssPhp\ScssPhp\Block $block
1471
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1472
  */
1473
  protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out)
1474
  {
1493
  array_unshift($child[1]->prefix[2], $prefix);
1494
  break;
1495
  }
1496
+
1497
  $this->compileChild($child, $nested);
1498
  }
1499
  }
1513
 
1514
  // wrap assign children in a block
1515
  // except for @font-face
1516
+ if ($block->type !== Type::T_DIRECTIVE || $this->compileDirectiveName($block->name) !== 'font-face') {
1517
  // need wrapping?
1518
  $needWrapping = false;
1519
 
1525
  }
1526
 
1527
  if ($needWrapping) {
1528
+ $wrapped = new Block();
1529
+ $wrapped->sourceName = $block->sourceName;
1530
+ $wrapped->sourceIndex = $block->sourceIndex;
1531
+ $wrapped->sourceLine = $block->sourceLine;
1532
  $wrapped->sourceColumn = $block->sourceColumn;
1533
+ $wrapped->selectors = [];
1534
+ $wrapped->comments = [];
1535
+ $wrapped->parent = $block;
1536
+ $wrapped->children = $block->children;
1537
+ $wrapped->selfParent = $block->selfParent;
1538
 
1539
  $block->children = [[Type::T_BLOCK, $wrapped]];
1540
  }
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
 
1598
 
1599
  $this->scope->children[] = $out;
1600
 
1601
+ if (\count($block->children)) {
1602
  $out->selectors = $this->multiplySelectors($env, $block->selfParent);
1603
 
1604
  // propagate selfParent to the children where they still can be useful
1611
 
1612
  $this->compileChildrenNoReturn($block->children, $out, $block->selfParent);
1613
 
1614
+ // and revert for the following children of the same block
1615
  if ($selfParentSelectors) {
1616
  $block->selfParent->selectors = $selfParentSelectors;
1617
  }
1618
  }
1619
 
 
 
1620
  $this->popEnv();
1621
  }
1622
 
1623
+
1624
+ /**
1625
+ * Compile the value of a comment that can have interpolation
1626
+ *
1627
+ * @param array $value
1628
+ * @param boolean $pushEnv
1629
+ *
1630
+ * @return array|mixed|string
1631
+ */
1632
+ protected function compileCommentValue($value, $pushEnv = false)
1633
+ {
1634
+ $c = $value[1];
1635
+
1636
+ if (isset($value[2])) {
1637
+ if ($pushEnv) {
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
+ }
1655
+ }
1656
+
1657
+ return $c;
1658
+ }
1659
+
1660
  /**
1661
  * Compile root level comment
1662
  *
1665
  protected function compileComment($block)
1666
  {
1667
  $out = $this->makeOutputBlock(Type::T_COMMENT);
1668
+ $out->lines[] = $this->compileCommentValue($block, true);
1669
 
1670
  $this->scope->children[] = $out;
1671
  }
1685
 
1686
  // after evaluating interpolates, we might need a second pass
1687
  if ($this->shouldEvaluate) {
1688
+ $selectors = $this->replaceSelfSelector($selectors, '&');
1689
+ $buffer = $this->collapseSelectors($selectors);
1690
+ $parser = $this->parserFactory(__METHOD__);
1691
 
1692
  if ($parser->parseSelector($buffer, $newSelectors)) {
1693
  $selectors = array_map([$this, 'evalSelector'], $newSelectors);
1719
  protected function evalSelectorPart($part)
1720
  {
1721
  foreach ($part as &$p) {
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 (
1730
+ \is_string($p) && \strlen($p) >= 2 &&
1731
  ($first = $p[0]) && ($first === '"' || $first === "'") &&
1732
  substr($p, -1) === $first
1733
  ) {
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;
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);
1811
  *
1812
  * @return array
1813
  */
1814
+ protected function replaceSelfSelector($selectors, $replace = null)
1815
  {
1816
  foreach ($selectors as &$part) {
1817
+ if (\is_array($part)) {
1818
  if ($part === [Type::T_SELF]) {
1819
+ if (\is_null($replace)) {
1820
+ $replace = $this->reduce([Type::T_SELF]);
1821
+ $replace = $this->compileValue($replace);
1822
+ }
1823
+ $part = $replace;
1824
  } else {
1825
+ $part = $this->replaceSelfSelector($part, $replace);
1826
  }
1827
  }
1828
  }
1842
  $joined = [];
1843
 
1844
  foreach ($single as $part) {
1845
+ if (
1846
+ empty($joined) ||
1847
+ ! \is_string($part) ||
1848
  preg_match('/[\[.:#%]/', $part)
1849
  ) {
1850
  $joined[] = $part;
1851
  continue;
1852
  }
1853
 
1854
+ if (\is_array(end($joined))) {
1855
  $joined[] = $part;
1856
  } else {
1857
+ $joined[\count($joined) - 1] .= $part;
1858
  }
1859
  }
1860
 
1870
  */
1871
  protected function compileSelector($selector)
1872
  {
1873
+ if (! \is_array($selector)) {
1874
  return $selector; // media and the like
1875
  }
1876
 
1893
  protected function compileSelectorPart($piece)
1894
  {
1895
  foreach ($piece as &$p) {
1896
+ if (! \is_array($p)) {
1897
  continue;
1898
  }
1899
 
1920
  */
1921
  protected function hasSelectorPlaceholder($selector)
1922
  {
1923
+ if (! \is_array($selector)) {
1924
  return false;
1925
  }
1926
 
1927
  foreach ($selector as $parts) {
1928
  foreach ($parts as $part) {
1929
+ if (\strlen($part) && '%' === $part[0]) {
1930
  return true;
1931
  }
1932
  }
1945
  ];
1946
 
1947
  // infinite calling loop
1948
+ if (\count($this->callStack) > 25000) {
1949
  // not displayed but you can var_dump it to deep debug
1950
  $msg = $this->callStackMessage(true, 100);
1951
+ $msg = 'Infinite calling loop';
1952
+
1953
+ throw $this->error($msg);
1954
  }
1955
  }
1956
 
1976
  $ret = $this->compileChild($stm, $out);
1977
 
1978
  if (isset($ret)) {
1979
+ $this->popCallStack();
1980
+
1981
  return $ret;
1982
  }
1983
  }
2002
  $this->pushCallStack($traceName);
2003
 
2004
  foreach ($stms as $stm) {
2005
+ if ($selfParent && isset($stm[1]) && \is_object($stm[1]) && $stm[1] instanceof Block) {
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']);
2015
  }
2016
 
2017
  if (isset($ret)) {
2018
+ throw $this->error('@return may only be used within a function');
 
 
2019
  }
2020
  }
2021
 
2033
  protected function evaluateMediaQuery($queryList)
2034
  {
2035
  static $parser = null;
2036
+
2037
  $outQueryList = [];
2038
+
2039
  foreach ($queryList as $kql => $query) {
2040
  $shouldReparse = false;
2041
+
2042
  foreach ($query as $kq => $q) {
2043
+ for ($i = 1; $i < \count($q); $i++) {
2044
  $value = $this->compileValue($q[$i]);
2045
 
2046
  // the parser had no mean to know if media type or expression if it was an interpolation
2047
  // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type
2048
+ if (
2049
+ $q[0] == Type::T_MEDIA_TYPE &&
2050
  (strpos($value, '(') !== false ||
2051
  strpos($value, ')') !== false ||
2052
  strpos($value, ':') !== false ||
2058
  $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value];
2059
  }
2060
  }
2061
+
2062
  if ($shouldReparse) {
2063
+ if (\is_null($parser)) {
2064
  $parser = $this->parserFactory(__METHOD__);
2065
  }
2066
+
2067
  $queryString = $this->compileMediaQuery([$queryList[$kql]]);
2068
  $queryString = reset($queryString);
2069
+
2070
  if (strpos($queryString, '@media ') === 0) {
2071
  $queryString = substr($queryString, 7);
2072
  $queries = [];
2073
+
2074
  if ($parser->parseMediaQueryList($queryString, $queries)) {
2075
  $queries = $this->evaluateMediaQuery($queries[2]);
2076
+
2077
+ while (\count($queries)) {
2078
  $outQueryList[] = array_shift($queries);
2079
  }
2080
+
2081
  continue;
2082
  }
2083
  }
2084
  }
2085
+
2086
  $outQueryList[] = $queryList[$kql];
2087
  }
2088
 
2098
  */
2099
  protected function compileMediaQuery($queryList)
2100
  {
2101
+ $start = '@media ';
2102
  $default = trim($start);
2103
+ $out = [];
2104
+ $current = '';
2105
 
2106
  foreach ($queryList as $query) {
2107
  $type = null;
2119
  foreach ($query as $q) {
2120
  switch ($q[0]) {
2121
  case Type::T_MEDIA_TYPE:
2122
+ $newType = array_map([$this, 'compileValue'], \array_slice($q, 1));
2123
+
2124
  // combining not and anything else than media type is too risky and should be avoided
2125
  if (! $mediaTypeOnly) {
2126
+ if (\in_array(Type::T_NOT, $newType) || ($type && \in_array(Type::T_NOT, $type) )) {
2127
  if ($type) {
2128
  array_unshift($parts, implode(' ', array_filter($type)));
2129
  }
2130
 
2131
  if (! empty($parts)) {
2132
+ if (\strlen($current)) {
2133
  $current .= $this->formatter->tagSeparator;
2134
  }
2135
 
2140
  $out[] = $start . $current;
2141
  }
2142
 
2143
+ $current = '';
2144
+ $type = null;
2145
+ $parts = [];
2146
  }
2147
  }
2148
 
2192
  }
2193
 
2194
  if (! empty($parts)) {
2195
+ if (\strlen($current)) {
2196
  $current .= $this->formatter->tagSeparator;
2197
  }
2198
 
2274
  return $type1;
2275
  }
2276
 
2277
+ if (\count($type1) > 1) {
2278
+ $m1 = strtolower($type1[0]);
2279
+ $t1 = strtolower($type1[1]);
 
 
 
2280
  } else {
2281
+ $m1 = '';
2282
  $t1 = strtolower($type1[0]);
2283
  }
2284
 
2285
+ if (\count($type2) > 1) {
 
 
 
2286
  $m2 = strtolower($type2[0]);
2287
  $t2 = strtolower($type2[1]);
2288
  } else {
2289
+ $m2 = '';
2290
  $t2 = strtolower($type2[0]);
2291
  }
2292
 
2315
  }
2316
 
2317
  // t1 == t2, neither m1 nor m2 are "not"
2318
+ return [empty($m1) ? $m2 : $m1, $t1];
2319
  }
2320
 
2321
  /**
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
  }
2341
  return true;
2342
  }
2343
 
2344
+ $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2345
+
2346
  return false;
2347
  }
2348
 
2349
  if ($rawPath[0] === Type::T_LIST) {
2350
  // handle a list of strings
2351
+ if (\count($rawPath[2]) === 0) {
2352
  return false;
2353
  }
2354
 
2355
  foreach ($rawPath[2] as $path) {
2356
  if ($path[0] !== Type::T_STRING) {
2357
+ $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2358
+
2359
  return false;
2360
  }
2361
  }
2362
 
2363
  foreach ($rawPath[2] as $path) {
2364
+ $this->compileImport($path, $out, $once);
2365
  }
2366
 
2367
  return true;
2368
  }
2369
 
2370
+ $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2371
+
2372
  return false;
2373
  }
2374
 
2375
+ /**
2376
+ * @param $rawPath
2377
+ * @return string
2378
+ * @throws CompilerException
2379
+ */
2380
+ protected function compileImportPath($rawPath)
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);
2393
+ }
2394
+
2395
+ return $path;
2396
+ }
2397
+
2398
+ /**
2399
+ * @param array $path
2400
+ * @return array
2401
+ * @throws CompilerException
2402
+ */
2403
+ protected function escapeImportPathString($path)
2404
+ {
2405
+ switch ($path[0]) {
2406
+ case Type::T_LIST:
2407
+ foreach ($path[2] as $k => $v) {
2408
+ $path[2][$k] = $this->escapeImportPathString($v);
2409
+ }
2410
+ break;
2411
+ case Type::T_STRING:
2412
+ if ($path[1]) {
2413
+ $path = $this->compileValue($path);
2414
+ $path = str_replace(' ', '\\ ', $path);
2415
+ $path = [Type::T_KEYWORD, $path];
2416
+ }
2417
+ break;
2418
+ }
2419
+
2420
+ return $path;
2421
+ }
2422
 
2423
  /**
2424
  * Append a root directive like @import or @charset as near as the possible from the source code
2438
 
2439
  $i = 0;
2440
 
2441
+ while ($i < \count($root->children)) {
2442
+ if (! isset($root->children[$i]->type) || ! \in_array($root->children[$i]->type, $allowed)) {
2443
  break;
2444
  }
2445
 
2449
  // remove incompatible children from the bottom of the list
2450
  $saveChildren = [];
2451
 
2452
+ while ($i < \count($root->children)) {
2453
  $saveChildren[] = array_pop($root->children);
2454
  }
2455
 
2456
  // insert the directive as a comment
2457
  $child = $this->makeOutputBlock(Type::T_COMMENT);
2458
+ $child->lines[] = $line;
2459
+ $child->sourceName = $this->sourceNames[$this->sourceIndex];
2460
+ $child->sourceLine = $this->sourceLine;
2461
  $child->sourceColumn = $this->sourceColumn;
2462
 
2463
  $root->children[] = $child;
2464
 
2465
  // repush children
2466
+ while (\count($saveChildren)) {
2467
  $root->children[] = array_pop($saveChildren);
2468
  }
2469
  }
2470
 
2471
  /**
2472
+ * Append lines to the current output block:
2473
  * directly to the block or through a child if necessary
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
  {
2481
  $outWrite = &$out;
2482
 
 
 
 
 
 
 
 
 
2483
  // check if it's a flat output or not
2484
+ if (\count($out->children)) {
2485
+ $lastChild = &$out->children[\count($out->children) - 1];
2486
 
2487
+ if (
2488
+ $lastChild->depth === $out->depth &&
2489
+ \is_null($lastChild->selectors) &&
2490
+ ! \count($lastChild->children)
2491
+ ) {
2492
  $outWrite = $lastChild;
2493
  } else {
2494
  $nextLines = $this->makeOutputBlock($type);
2495
  $nextLines->parent = $out;
2496
+ $nextLines->depth = $out->depth;
2497
 
2498
  $out->children[] = $nextLines;
2499
  $outWrite = &$nextLines;
2514
  protected function compileChild($child, OutputBlock $out)
2515
  {
2516
  if (isset($child[Parser::SOURCE_LINE])) {
2517
+ $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
2518
+ $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
2519
  $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
2520
+ } elseif (\is_array($child) && isset($child[1]->sourceLine)) {
2521
+ $this->sourceIndex = $child[1]->sourceIndex;
2522
+ $this->sourceLine = $child[1]->sourceLine;
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;
2535
  case Type::T_SCSSPHP_IMPORT_ONCE:
2536
  $rawPath = $this->reduce($child[1]);
2537
 
2538
+ $this->compileImport($rawPath, $out, true);
 
 
2539
  break;
2540
 
2541
  case Type::T_IMPORT:
2542
  $rawPath = $this->reduce($child[1]);
2543
 
2544
+ $this->compileImport($rawPath, $out);
 
 
2545
  break;
2546
 
2547
  case Type::T_DIRECTIVE:
2548
+ $this->compileDirective($child[1], $out);
2549
  break;
2550
 
2551
  case Type::T_AT_ROOT:
2567
  }
2568
  break;
2569
 
2570
+ case Type::T_CUSTOM_PROPERTY:
2571
+ list(, $name, $value) = $child;
2572
+ $compiledName = $this->compileValue($name);
2573
+
2574
+ // if the value reduces to null from something else then
2575
+ // the property should be discarded
2576
+ if ($value[0] !== Type::T_NULL) {
2577
+ $value = $this->reduce($value);
2578
+
2579
+ if ($value[0] === Type::T_NULL || $value === static::$nullString) {
2580
+ break;
2581
+ }
2582
+ }
2583
+
2584
+ $compiledValue = $this->compileValue($value);
2585
+
2586
+ $line = $this->formatter->customProperty(
2587
+ $compiledName,
2588
+ $compiledValue
2589
+ );
2590
+
2591
+ $this->appendOutputLine($out, Type::T_ASSIGN, $line);
2592
+ break;
2593
+
2594
  case Type::T_ASSIGN:
2595
  list(, $name, $value) = $child;
2596
 
2597
  if ($name[0] === Type::T_VARIABLE) {
2598
+ $flags = isset($child[3]) ? $child[3] : [];
2599
+ $isDefault = \in_array('!default', $flags);
2600
+ $isGlobal = \in_array('!global', $flags);
2601
 
2602
  if ($isGlobal) {
2603
  $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
2605
  }
2606
 
2607
  $shouldSet = $isDefault &&
2608
+ (\is_null($result = $this->get($name[1], false)) ||
2609
  $result === static::$null);
2610
 
2611
  if (! $isDefault || $shouldSet) {
2616
 
2617
  $compiledName = $this->compileValue($name);
2618
 
2619
+ // handle shorthand syntaxes : size / line-height...
2620
+ if (\in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) {
2621
  if ($value[0] === Type::T_VARIABLE) {
2622
  // if the font value comes from variable, the content is already reduced
2623
  // (i.e., formulas were already calculated), so we need the original unreduced value
2624
  $value = $this->get($value[1], true, null, true);
2625
  }
2626
 
2627
+ $shorthandValue=&$value;
2628
+
2629
+ $shorthandDividerNeedsUnit = false;
2630
+ $maxListElements = null;
2631
+ $maxShorthandDividers = 1;
2632
+
2633
+ switch ($compiledName) {
2634
+ case 'border-radius':
2635
+ $maxListElements = 4;
2636
+ $shorthandDividerNeedsUnit = true;
2637
+ break;
2638
+ }
2639
 
2640
+ if ($compiledName === 'font' && $value[0] === Type::T_LIST && $value[1] === ',') {
2641
  // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica"
2642
  // we need to handle the first list element
2643
+ $shorthandValue=&$value[2][0];
2644
  }
2645
 
2646
+ if ($shorthandValue[0] === Type::T_EXPRESSION && $shorthandValue[1] === '/') {
2647
+ $revert = true;
2648
+
2649
+ if ($shorthandDividerNeedsUnit) {
2650
+ $divider = $shorthandValue[3];
2651
+
2652
+ if (\is_array($divider)) {
2653
+ $divider = $this->reduce($divider, true);
2654
+ }
2655
+
2656
+ if (\intval($divider->dimension) && ! \count($divider->units)) {
2657
+ $revert = false;
2658
+ }
2659
+ }
2660
+
2661
+ if ($revert) {
2662
+ $shorthandValue = $this->expToString($shorthandValue);
2663
+ }
2664
+ } elseif ($shorthandValue[0] === Type::T_LIST) {
2665
+ foreach ($shorthandValue[2] as &$item) {
2666
  if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
2667
+ if ($maxShorthandDividers > 0) {
2668
+ $revert = true;
2669
+
2670
+ // if the list of values is too long, this has to be a shorthand,
2671
+ // otherwise it could be a real division
2672
+ if (\is_null($maxListElements) || \count($shorthandValue[2]) <= $maxListElements) {
2673
+ if ($shorthandDividerNeedsUnit) {
2674
+ $divider = $item[3];
2675
+
2676
+ if (\is_array($divider)) {
2677
+ $divider = $this->reduce($divider, true);
2678
+ }
2679
+
2680
+ if (\intval($divider->dimension) && ! \count($divider->units)) {
2681
+ $revert = false;
2682
+ }
2683
+ }
2684
+ }
2685
+
2686
+ if ($revert) {
2687
+ $item = $this->expToString($item);
2688
+ $maxShorthandDividers--;
2689
+ }
2690
+ }
2691
  }
2692
  }
2693
  }
2705
 
2706
  $compiledValue = $this->compileValue($value);
2707
 
2708
+ // ignore empty value
2709
+ if (\strlen($compiledValue)) {
2710
+ $line = $this->formatter->property(
2711
+ $compiledName,
2712
+ $compiledValue
2713
+ );
2714
+ $this->appendOutputLine($out, Type::T_ASSIGN, $line);
2715
+ }
2716
  break;
2717
 
2718
  case Type::T_COMMENT:
2721
  break;
2722
  }
2723
 
2724
+ $line = $this->compileCommentValue($child, true);
2725
+ $this->appendOutputLine($out, Type::T_COMMENT, $line);
2726
  break;
2727
 
2728
  case Type::T_MIXIN:
2729
  case Type::T_FUNCTION:
2730
  list(, $block) = $child;
2731
+ // the block need to be able to go up to it's parent env to resolve vars
2732
+ $block->parentEnv = $this->getStoreEnv();
2733
  $this->set(static::$namespaces[$block->type] . $block->name, $block, true);
2734
  break;
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'])) {
2747
+ $selectors = $this->multiplySelectors($this->env, $child['selfParent']);
2748
+ }
2749
 
2750
+ $this->pushExtends($result, $selectors, $child);
2751
  }
2752
  }
2753
  break;
2760
  }
2761
 
2762
  foreach ($if->cases as $case) {
2763
+ if (
2764
+ $case->type === Type::T_ELSE ||
2765
  $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))
2766
  ) {
2767
  return $this->compileChildren($case->children, $out);
2772
  case Type::T_EACH:
2773
  list(, $each) = $child;
2774
 
2775
+ $list = $this->coerceList($this->reduce($each->list), ',', true);
2776
 
2777
  $this->pushEnv();
2778
 
2779
  foreach ($list[2] as $item) {
2780
+ if (\count($each->vars) === 1) {
2781
  $this->set($each->vars[0], $item, true);
2782
  } else {
2783
  list(,, $values) = $this->coerceList($item);
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
  }
2803
  }
2804
  }
2805
  }
2806
+ $store = $this->env->store;
2807
  $this->popEnv();
2808
+ $this->backPropagateEnv($store, $each->vars);
2809
+
2810
  break;
2811
 
2812
  case Type::T_WHILE:
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];
2851
 
2852
  $d = $start < $end ? 1 : -1;
2853
 
2854
+ $this->pushEnv();
2855
+
2856
  for (;;) {
2857
+ if (
2858
+ (! $for->until && $start - $d == $end) ||
2859
  ($for->until && $start == $end)
2860
  ) {
2861
  break;
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
 
2880
  }
2881
  }
2882
  }
2883
+
2884
+ $store = $this->env->store;
2885
+ $this->popEnv();
2886
+ $this->backPropagateEnv($store, [$for->var]);
2887
+
2888
  break;
2889
 
2890
  case Type::T_BREAK:
2902
 
2903
  case Type::T_INCLUDE:
2904
  // including a mixin
2905
+ list(, $name, $argValues, $content, $argUsing) = $child;
2906
 
2907
  $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
2908
 
2909
  if (! $mixin) {
2910
+ throw $this->error("Undefined mixin $name");
 
2911
  }
2912
 
2913
  $callingScope = $this->getStoreEnv();
2916
  $this->pushEnv();
2917
  $this->env->depth--;
2918
 
 
 
 
2919
  // Find the parent selectors in the env to be able to know what '&' refers to in the mixin
2920
  // and assign this fake parent to childs
2921
  $selfParent = null;
2930
  $parent->selectors = $parentSelectors;
2931
 
2932
  foreach ($mixin->children as $k => $child) {
2933
+ if (isset($child[1]) && \is_object($child[1]) && $child[1] instanceof Block) {
2934
  $mixin->children[$k][1]->parent = $parent;
2935
  }
2936
  }
2941
  // i.e., recursive @include of the same mixin
2942
  if (isset($content)) {
2943
  $copyContent = clone $content;
2944
+ $copyContent->scope = clone $callingScope;
2945
 
2946
  $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
2947
  } else {
2948
  $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env);
2949
  }
2950
 
2951
+ // save the "using" argument list for applying it to when "@content" is invoked
2952
+ if (isset($argUsing)) {
2953
+ $this->setRaw(static::$namespaces['special'] . 'using', $argUsing, $this->env);
2954
+ } else {
2955
+ $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env);
2956
+ }
2957
+
2958
  if (isset($mixin->args)) {
2959
  $this->applyArguments($mixin->args, $argValues);
2960
  }
2961
 
2962
  $this->env->marker = 'mixin';
2963
 
2964
+ if (! empty($mixin->parentEnv)) {
2965
+ $this->env->declarationScopeParent = $mixin->parentEnv;
2966
+ } else {
2967
+ throw $this->error("@mixin $name() without parentEnv");
2968
+ }
2969
 
2970
+ $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . ' ' . $name);
2971
 
2972
  $this->popEnv();
2973
  break;
2974
 
2975
  case Type::T_MIXIN_CONTENT:
2976
+ $env = isset($this->storeEnv) ? $this->storeEnv : $this->env;
2977
+ $content = $this->get(static::$namespaces['special'] . 'content', false, $env);
2978
+ $argUsing = $this->get(static::$namespaces['special'] . 'using', false, $env);
2979
+ $argContent = $child[1];
2980
 
2981
  if (! $content) {
 
 
 
2982
  break;
2983
  }
2984
 
2985
  $storeEnv = $this->storeEnv;
2986
+ $varsUsing = [];
2987
+
2988
+ if (isset($argUsing) && isset($argContent)) {
2989
+ // Get the arguments provided for the content with the names provided in the "using" argument list
2990
+ $this->storeEnv = null;
2991
+ $varsUsing = $this->applyArguments($argUsing, $argContent, false);
2992
+ }
2993
+
2994
+ // restore the scope from the @content
2995
  $this->storeEnv = $content->scope;
2996
+
2997
+ // append the vars from using if any
2998
+ foreach ($varsUsing as $name => $val) {
2999
+ $this->set($name, $val, true, $this->storeEnv);
3000
+ }
3001
+
3002
  $this->compileChildrenNoReturn($content->children, $out);
3003
 
3004
  $this->storeEnv = $storeEnv;
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
  }
3042
  }
3043
 
3045
  * Reduce expression to string
3046
  *
3047
  * @param array $exp
3048
+ * @param true $keepParens
3049
  *
3050
  * @return array
3051
  */
3052
+ protected function expToString($exp, $keepParens = false)
3053
  {
3054
+ list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
3055
 
3056
+ $content = [];
3057
+
3058
+ if ($keepParens && $inParens) {
3059
+ $content[] = '(';
3060
+ }
3061
+
3062
+ $content[] = $this->reduce($left);
3063
 
3064
  if ($whiteLeft) {
3065
  $content[] = ' ';
3073
 
3074
  $content[] = $this->reduce($right);
3075
 
3076
+ if ($keepParens && $inParens) {
3077
+ $content[] = ')';
3078
+ }
3079
+
3080
  return [Type::T_STRING, '', $content];
3081
  }
3082
 
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]) {
3146
  case Type::T_EXPRESSION:
3156
  }
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);
3165
  }
3166
 
3167
+ $left = $this->coerceForExpression($left);
3168
  $right = $this->coerceForExpression($right);
 
3169
  $ltype = $left[0];
3170
  $rtype = $right[0];
3171
 
3179
  // 3. op[op name]
3180
  $fn = "op${ucOpName}${ucLType}${ucRType}";
3181
 
3182
+ if (
3183
+ \is_callable([$this, $fn]) ||
3184
  (($fn = "op${ucLType}${ucRType}") &&
3185
+ \is_callable([$this, $fn]) &&
3186
  $passOp = true) ||
3187
  (($fn = "op${ucOpName}") &&
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;
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
 
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
  }
3313
 
3314
  case Type::T_INTERPOLATE:
3315
  $value[1] = $this->reduce($value[1]);
3316
+
3317
  if ($inExp) {
3318
  return $value[1];
3319
  }
3324
  return $this->fncall($value[1], $value[2]);
3325
 
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
 
3333
  default:
3343
  *
3344
  * @return array|null
3345
  */
3346
+ protected function fncall($functionReference, $argValues)
3347
+ {
3348
+ // a string means this is a static hard reference coming from the parsing
3349
+ if (is_string($functionReference)) {
3350
+ $name = $functionReference;
3351
+
3352
+ $functionReference = $this->getFunctionReference($name);
3353
+ if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) {
3354
+ $functionReference = [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]];
3355
+ }
3356
+ }
3357
+
3358
+ // a function type means we just want a plain css function call
3359
+ if ($functionReference[0] === Type::T_FUNCTION) {
3360
+ // for CSS functions, simply flatten the arguments into a list
3361
+ $listArgs = [];
3362
+
3363
+ foreach ((array) $argValues as $arg) {
3364
+ if (empty($arg[0]) || count($argValues) === 1) {
3365
+ $listArgs[] = $this->reduce($this->stringifyFncallArgs($arg[1]));
3366
+ }
3367
+ }
3368
+
3369
+ return [Type::T_FUNCTION, $functionReference[1], [Type::T_LIST, ',', $listArgs]];
3370
+ }
3371
+
3372
+ if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) {
3373
+ return static::$defaultValue;
3374
+ }
3375
+
3376
+
3377
+ switch ($functionReference[1]) {
3378
+ // SCSS @function
3379
+ case 'scss':
3380
+ return $this->callScssFunction($functionReference[3], $argValues);
3381
+
3382
+ // native PHP functions
3383
+ case 'user':
3384
+ case 'native':
3385
+ list(,,$name, $fn, $prototype) = $functionReference;
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']
3393
+ );
3394
+ if ($cssFunction !== false) {
3395
+ return $cssFunction;
3396
+ }
3397
+ }
3398
+ $returnValue = $this->callNativeFunction($name, $fn, $prototype, $argValues);
3399
+
3400
+ if (! isset($returnValue)) {
3401
+ return $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $argValues);
3402
+ }
3403
+
3404
+ return $returnValue;
3405
+
3406
+ default:
3407
+ return static::$defaultValue;
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)];
3416
+
3417
+ case Type::T_FUNCTION:
3418
+ if (! \in_array($arg[1], $allowed_function)) {
3419
+ return false;
3420
+ }
3421
+ if ($arg[2][0] === Type::T_LIST) {
3422
+ foreach ($arg[2][2] as $k => $subarg) {
3423
+ $arg[2][2][$k] = $this->cssValidArg($subarg, $allowed_function, $arg[1]);
3424
+ if ($arg[2][2][$k] === false) {
3425
+ return false;
3426
+ }
3427
+ }
3428
+ }
3429
+ return $arg;
3430
+
3431
+ case Type::T_FUNCTION_CALL:
3432
+ if (! \in_array($arg[1], $allowed_function)) {
3433
+ return false;
3434
+ }
3435
+ $cssArgs = [];
3436
+ foreach ($arg[2] as $argValue) {
3437
+ if ($argValue === static::$null) {
3438
+ return false;
3439
+ }
3440
+ $cssArg = $this->cssValidArg($argValue[1], $allowed_function, $arg[1]);
3441
+ if (empty($argValue[0]) && $cssArg !== false) {
3442
+ $cssArgs[] = [$argValue[0], $cssArg];
3443
+ } else {
3444
+ return false;
3445
+ }
3446
+ }
3447
+
3448
+ return $this->fncall([Type::T_FUNCTION, $arg[1], [Type::T_LIST, ',', []]], $cssArgs);
3449
+
3450
+ case Type::T_STRING:
3451
+ case Type::T_KEYWORD:
3452
+ if (!$inFunction or !\in_array($inFunction, ['calc', 'env', 'var'])) {
3453
+ return false;
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;
3463
+ }
3464
+ if (empty($arg['enclosing']) and $arg[1] === '') {
3465
+ foreach ($arg[2] as $k => $subarg) {
3466
+ $arg[2][$k] = $this->cssValidArg($subarg, $allowed_function, $inFunction);
3467
+ if ($arg[2][$k] === false) {
3468
+ return false;
3469
+ }
3470
+ }
3471
+ $arg[0] = Type::T_STRING;
3472
+ return $arg;
3473
+ }
3474
+ return false;
3475
+
3476
+ case Type::T_EXPRESSION:
3477
+ if (! \in_array($arg[1], ['+', '-', '/', '*'])) {
3478
+ return false;
3479
+ }
3480
+ $arg[2] = $this->cssValidArg($arg[2], $allowed_function, $inFunction);
3481
+ $arg[3] = $this->cssValidArg($arg[3], $allowed_function, $inFunction);
3482
+ if ($arg[2] === false || $arg[3] === false) {
3483
+ return false;
3484
+ }
3485
+ return $this->expToString($arg, true);
3486
+
3487
+ case Type::T_VARIABLE:
3488
+ case Type::T_SELF:
3489
+ default:
3490
+ return false;
3491
+ }
3492
+ }
3493
+
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:
3505
+ foreach ($arg[2] as $k => $v) {
3506
+ $arg[2][$k] = $this->stringifyFncallArgs($v);
3507
+ }
3508
+ break;
3509
+
3510
+ case Type::T_EXPRESSION:
3511
+ if ($arg[1] === '/') {
3512
+ $arg[2] = $this->stringifyFncallArgs($arg[2]);
3513
+ $arg[3] = $this->stringifyFncallArgs($arg[3]);
3514
+ $arg[5] = $arg[6] = false; // no space around /
3515
+ $arg = $this->expToString($arg);
3516
+ }
3517
+ break;
3518
+
3519
+ case Type::T_FUNCTION_CALL:
3520
+ $name = strtolower($arg[1]);
3521
+
3522
+ if (in_array($name, ['max', 'min', 'calc'])) {
3523
+ $args = $arg[2];
3524
+ $arg = $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $args);
3525
+ }
3526
+ break;
3527
+ }
3528
+
3529
+ return $arg;
3530
+ }
3531
+
3532
+ /**
3533
+ * Find a function reference
3534
+ * @param string $name
3535
+ * @param bool $safeCopy
3536
+ * @return array
3537
+ */
3538
+ protected function getFunctionReference($name, $safeCopy = false)
3539
  {
3540
  // SCSS @function
3541
+ if ($func = $this->get(static::$namespaces['function'] . $name, false)) {
3542
+ if ($safeCopy) {
3543
+ $func = clone $func;
3544
+ }
3545
+
3546
+ return [Type::T_FUNCTION_REFERENCE, 'scss', $name, $func];
3547
  }
3548
 
3549
  // native PHP functions
3550
+
3551
+ // try to find a native lib function
3552
+ $normalizedName = $this->normalizeName($name);
3553
+ $libName = null;
3554
+
3555
+ if (isset($this->userFunctions[$normalizedName])) {
3556
+ // see if we can find a user function
3557
+ list($f, $prototype) = $this->userFunctions[$normalizedName];
3558
+
3559
+ return [Type::T_FUNCTION_REFERENCE, 'user', $name, $f, $prototype];
3560
  }
3561
 
3562
+ if (($f = $this->getBuiltinFunction($normalizedName)) && \is_callable($f)) {
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
 
3569
+ return static::$null;
3570
  }
3571
 
3572
+
3573
  /**
3574
  * Normalize name
3575
  *
3605
  $value[2][$key] = $this->normalizeValue($item);
3606
  }
3607
 
3608
+ if (! empty($value['enclosing'])) {
3609
+ unset($value['enclosing']);
3610
+ }
3611
+
3612
  return $value;
3613
 
3614
  case Type::T_STRING:
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]);
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
 
3818
  break;
3819
 
3820
  case '%':
3821
+ if ($rval == 0) {
3822
+ throw $this->error("color: Can't take modulo by zero");
3823
+ }
3824
+
3825
  $out[] = $lval % $rval;
3826
  break;
3827
 
3828
  case '/':
3829
  if ($rval == 0) {
3830
+ throw $this->error("color: Can't divide by zero");
 
3831
  }
3832
 
3833
  $out[] = (int) ($lval / $rval);
3840
  return $this->opNeq($left, $right);
3841
 
3842
  default:
3843
+ throw $this->error("color: unknown op $op");
 
3844
  }
3845
  }
3846
 
4031
  *
4032
  * @param array $value
4033
  *
4034
+ * @return string|array
4035
  */
4036
  public function compileValue($value)
4037
  {
4048
  // [4] - optional alpha component
4049
  list(, $r, $g, $b) = $value;
4050
 
4051
+ $r = $this->compileRGBAValue($r);
4052
+ $g = $this->compileRGBAValue($g);
4053
+ $b = $this->compileRGBAValue($b);
4054
+
4055
+ if (\count($value) === 5) {
4056
+ $alpha = $this->compileRGBAValue($value[4], true);
4057
+
4058
+ if (! is_numeric($alpha) || $alpha < 1) {
4059
+ $colorName = Colors::RGBaToColorName($r, $g, $b, $alpha);
4060
+
4061
+ if (! \is_null($colorName)) {
4062
+ return $colorName;
4063
+ }
4064
+
4065
+ if (is_numeric($alpha)) {
4066
+ $a = new Node\Number($alpha, '');
4067
+ } else {
4068
+ $a = $alpha;
4069
+ }
4070
+
4071
+ return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
4072
+ }
4073
+ }
4074
+
4075
+ if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b)) {
4076
+ return 'rgb(' . $r . ', ' . $g . ', ' . $b . ')';
4077
+ }
4078
 
4079
+ $colorName = Colors::RGBaToColorName($r, $g, $b);
 
4080
 
4081
+ if (! \is_null($colorName)) {
4082
+ return $colorName;
4083
  }
4084
 
4085
  $h = sprintf('#%02x%02x%02x', $r, $g, $b);
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
 
4123
+ case Type::T_FUNCTION_REFERENCE:
4124
+ $name = ! empty($value[2]) ? $value[2] : '';
4125
+
4126
+ return "get-function(\"$name\")";
4127
+
4128
  case Type::T_LIST:
4129
  $value = $this->extractInterpolation($value);
4130
 
4133
  }
4134
 
4135
  list(, $delim, $items) = $value;
4136
+ $pre = $post = '';
4137
+
4138
+ if (! empty($value['enclosing'])) {
4139
+ switch ($value['enclosing']) {
4140
+ case 'parent':
4141
+ //$pre = '(';
4142
+ //$post = ')';
4143
+ break;
4144
+ case 'forced_parent':
4145
+ $pre = '(';
4146
+ $post = ')';
4147
+ break;
4148
+ case 'bracket':
4149
+ case 'forced_bracket':
4150
+ $pre = '[';
4151
+ $post = ']';
4152
+ break;
4153
+ }
4154
+ }
4155
+
4156
+ $prefix_value = '';
4157
 
4158
  if ($delim !== ' ') {
4159
+ $prefix_value = ' ';
4160
  }
4161
 
4162
  $filtered = [];
4166
  continue;
4167
  }
4168
 
4169
+ $compiled = $this->compileValue($item);
4170
+
4171
+ if ($prefix_value && \strlen($compiled)) {
4172
+ $compiled = $prefix_value . $compiled;
4173
+ }
4174
+
4175
+ $filtered[] = $compiled;
4176
  }
4177
 
4178
+ return $pre . substr(implode("$delim", $filtered), \strlen($prefix_value)) . $post;
4179
 
4180
  case Type::T_MAP:
4181
+ $keys = $value[1];
4182
+ $values = $value[2];
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
 
4197
  list(, $interpolate, $left, $right) = $value;
4198
  list(,, $whiteLeft, $whiteRight) = $interpolate;
4199
 
4200
+ $delim = $left[1];
4201
+
4202
+ if ($delim && $delim !== ' ' && ! $whiteLeft) {
4203
+ $delim .= ' ';
4204
+ }
4205
+
4206
+ $left = \count($left[2]) > 0
4207
+ ? $this->compileValue($left) . $delim . $whiteLeft
4208
+ : '';
4209
+
4210
+ $delim = $right[1];
4211
+
4212
+ if ($delim && $delim !== ' ') {
4213
+ $delim .= ' ';
4214
+ }
4215
 
4216
+ $right = \count($right[2]) > 0 ?
4217
+ $whiteRight . $delim . $this->compileValue($right) : '';
4218
 
4219
  return $left . $this->compileValue($interpolate) . $right;
4220
 
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) {
4270
  case Type::T_NULL:
4271
  return 'null';
4272
 
4273
+ case Type::T_COMMENT:
4274
+ return $this->compileCommentValue($value);
4275
+
4276
+ default:
4277
+ throw $this->error('unknown value type: ' . json_encode($value));
4278
+ }
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);
4293
+
4294
  default:
4295
+ return $this->compileValue($value);
4296
  }
4297
  }
4298
 
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;
4343
 
4344
  foreach ($items as $i => $item) {
4345
  if ($item[0] === Type::T_INTERPOLATE) {
4346
+ $before = [Type::T_LIST, $list[1], \array_slice($items, 0, $i)];
4347
+ $after = [Type::T_LIST, $list[1], \array_slice($items, $i + 1)];
4348
 
4349
  return [Type::T_INTERPOLATED, $item, $before, $after];
4350
  }
4369
 
4370
  $selfParentSelectors = null;
4371
 
4372
+ if (! \is_null($selfParent) && $selfParent->selectors) {
4373
  $selfParentSelectors = $this->evalSelectors($selfParent->selectors);
4374
  }
4375
 
4381
  $selectors = $env->selectors;
4382
 
4383
  do {
4384
+ $stillHasSelf = false;
4385
  $prevSelectors = $selectors;
4386
+ $selectors = [];
4387
 
4388
+ foreach ($parentSelectors as $parent) {
4389
+ foreach ($prevSelectors as $selector) {
4390
  if ($selfParentSelectors) {
4391
  foreach ($selfParentSelectors as $selfParent) {
4392
  // if no '&' in the selector, each call will give same result, only add once
4406
 
4407
  $selectors = array_values($selectors);
4408
 
4409
+ // case we are just starting a at-root : nothing to multiply but parentSelectors
4410
+ if (! $selectors && $selfParentSelectors) {
4411
+ $selectors = $selfParentSelectors;
4412
+ }
4413
+
4414
  return $selectors;
4415
  }
4416
 
4419
  *
4420
  * @param array $parent
4421
  * @param array $child
4422
+ * @param boolean $stillHasSelf
4423
  * @param array $selfParentSelectors
4424
 
4425
  * @return array
4441
  if ($p === static::$selfSelector && ! $setSelf) {
4442
  $setSelf = true;
4443
 
4444
+ if (\is_null($selfParentSelectors)) {
4445
  $selfParentSelectors = $parent;
4446
  }
4447
 
4452
  }
4453
 
4454
  foreach ($parentPart as $pp) {
4455
+ if (\is_array($pp)) {
4456
  $flatten = [];
4457
+
4458
  array_walk_recursive($pp, function ($a) use (&$flatten) {
4459
  $flatten[] = $a;
4460
  });
4461
+
4462
  $pp = implode($flatten);
4463
  }
4464
 
4486
  */
4487
  protected function multiplyMedia(Environment $env = null, $childQueries = null)
4488
  {
4489
+ if (
4490
+ ! isset($env) ||
4491
  ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA
4492
  ) {
4493
  return $childQueries;
4503
  : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
4504
 
4505
  $store = [$this->env, $this->storeEnv];
4506
+
4507
+ $this->env = $env;
4508
  $this->storeEnv = null;
4509
+ $parentQueries = $this->evaluateMediaQuery($parentQueries);
4510
+
4511
  list($this->env, $this->storeEnv) = $store;
4512
 
4513
+ if (\is_null($childQueries)) {
4514
  $childQueries = $parentQueries;
4515
  } else {
4516
  $originalQueries = $childQueries;
4572
  */
4573
  protected function pushEnv(Block $block = null)
4574
  {
4575
+ $env = new Environment();
4576
  $env->parent = $this->env;
4577
+ $env->parentStore = $this->storeEnv;
4578
  $env->store = [];
4579
  $env->block = $block;
4580
  $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
4581
 
4582
  $this->env = $env;
4583
+ $this->storeEnv = null;
4584
 
4585
  return $env;
4586
  }
4590
  */
4591
  protected function popEnv()
4592
  {
4593
+ $this->storeEnv = $this->env->parentStore;
4594
  $this->env = $this->env->parent;
4595
  }
4596
 
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
+ {
4605
+ foreach ($store as $key => $value) {
4606
+ if (empty($excludedVars) || ! \in_array($key, $excludedVars)) {
4607
+ $this->set($key, $value, true);
4608
+ }
4609
+ }
4610
+ }
4611
+
4612
  /**
4613
  * Get store environment
4614
  *
4654
  protected function setExisting($name, $value, Environment $env, $valueUnreduced = null)
4655
  {
4656
  $storeEnv = $env;
4657
+ $specialContentKey = static::$namespaces['special'] . 'content';
4658
 
4659
  $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
4660
 
4661
+ $maxDepth = 10000;
4662
+
4663
  for (;;) {
4664
+ if ($maxDepth-- <= 0) {
4665
  break;
4666
  }
4667
 
4668
+ if (\array_key_exists($name, $env->store)) {
 
4669
  break;
4670
  }
4671
 
4672
+ if (! $hasNamespace && isset($env->marker)) {
4673
+ if (! empty($env->store[$specialContentKey])) {
4674
+ $env = $env->store[$specialContentKey]->scope;
4675
+ continue;
4676
+ }
4677
 
4678
+ if (! empty($env->declarationScopeParent)) {
4679
+ $env = $env->declarationScopeParent;
4680
+ continue;
4681
+ } else {
4682
+ $env = $storeEnv;
4683
+ break;
4684
+ }
4685
+ }
4686
+
4687
+ if (isset($env->parentStore)) {
4688
+ $env = $env->parentStore;
4689
+ } elseif (isset($env->parent)) {
4690
+ $env = $env->parent;
4691
+ } else {
4692
+ $env = $storeEnv;
4693
+ break;
4694
+ }
4695
+ }
4696
 
4697
  $env->store[$name] = $value;
4698
 
4739
  $env = $this->getStoreEnv();
4740
  }
4741
 
 
4742
  $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
4743
 
4744
  $maxDepth = 10000;
4748
  break;
4749
  }
4750
 
4751
+ if (\array_key_exists($normalizedName, $env->store)) {
4752
  if ($unreduced && isset($env->storeUnreduced[$normalizedName])) {
4753
  return $env->storeUnreduced[$normalizedName];
4754
  }
4757
  }
4758
 
4759
  if (! $hasNamespace && isset($env->marker)) {
4760
+ if (! empty($env->store[$specialContentKey])) {
4761
  $env = $env->store[$specialContentKey]->scope;
4762
  continue;
4763
  }
4764
 
4765
+ if (! empty($env->declarationScopeParent)) {
4766
+ $env = $env->declarationScopeParent;
4767
+ } else {
4768
+ $env = $this->rootEnv;
4769
+ }
4770
  continue;
4771
  }
4772
 
4773
+ if (isset($env->parentStore)) {
4774
+ $env = $env->parentStore;
4775
+ } elseif (isset($env->parent)) {
4776
+ $env = $env->parent;
4777
+ } else {
4778
  break;
4779
  }
 
 
4780
  }
4781
 
4782
  if ($shouldThrow) {
4783
+ throw $this->error("Undefined variable \$$name" . ($maxDepth <= 0 ? ' (infinite recursion)' : ''));
4784
  }
4785
 
4786
  // found nothing
4797
  */
4798
  protected function has($name, Environment $env = null)
4799
  {
4800
+ return ! \is_null($this->get($name, false, $env));
4801
  }
4802
 
4803
  /**
4871
  */
4872
  public function addParsedFile($path)
4873
  {
4874
+ if (isset($path) && is_file($path)) {
4875
  $this->parsedFiles[realpath($path)] = filemtime($path);
4876
  }
4877
  }
4897
  */
4898
  public function addImportPath($path)
4899
  {
4900
+ if (! \in_array($path, $this->importPaths)) {
4901
  $this->importPaths[] = $path;
4902
  }
4903
  }
4920
  * @api
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)
4927
  {
4928
+ @trigger_error('The number precision is not configurable anymore. '
4929
+ . 'The default is enough for all browsers.', E_USER_DEPRECATED);
4930
  }
4931
 
4932
  /**
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
 
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
  /**
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) !== '/' &&
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
5117
+ $file = \call_user_func($dir, $url);
5118
 
5119
+ if (! \is_null($file)) {
5120
  return $file;
5121
  }
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
 
5151
  * @param boolean $ignoreErrors
5152
  *
5153
  * @return \ScssPhp\ScssPhp\Compiler
5154
+ *
5155
+ * @deprecated Ignoring Sass errors is not longer supported.
5156
  */
5157
  public function setIgnoreErrors($ignoreErrors)
5158
  {
5159
+ @trigger_error('Ignoring Sass errors is not longer supported.', E_USER_DEPRECATED);
5160
 
5161
  return $this;
5162
  }
5163
 
5164
+ /**
5165
+ * Get source position
5166
+ *
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];
5176
+ }
5177
+
5178
  /**
5179
  * Throw error (exception)
5180
  *
5183
  * @param string $msg Message with optional sprintf()-style vararg parameters
5184
  *
5185
  * @throws \ScssPhp\ScssPhp\Exception\CompilerException
5186
+ *
5187
+ * @deprecated use "error" and throw the exception in the caller instead.
5188
  */
5189
  public function throwError($msg)
5190
  {
5191
+ @trigger_error(
5192
+ 'The method "throwError" is deprecated. Use "error" and throw the exception in the caller instead',
5193
+ E_USER_DEPRECATED
5194
+ );
5195
+
5196
+ throw $this->error(...func_get_args());
5197
+ }
5198
+
5199
+ /**
5200
+ * Build an error (exception)
5201
+ *
5202
+ * @api
5203
+ *
5204
+ * @param string $msg Message with optional sprintf()-style vararg parameters
5205
+ *
5206
+ * @return CompilerException
5207
+ */
5208
+ public function error($msg, ...$args)
5209
+ {
5210
+ if ($args) {
5211
+ $msg = sprintf($msg, ...$args);
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
+ /**
5235
+ * @param string $functionName
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) {
5245
+ return $this->error(
5246
+ 'Error: Only %d arguments allowed in %s(), but %d were passed.',
5247
+ $nbExpected,
5248
+ $functionName,
5249
+ $nbActual
5250
+ );
5251
+ } else {
5252
+ $missing = [];
5253
 
5254
+ while (count($ExpectedArgs) && count($ExpectedArgs) > $nbActual) {
5255
+ array_unshift($missing, array_pop($ExpectedArgs));
5256
+ }
5257
 
5258
+ return $this->error(
5259
+ 'Error: %s() argument%s %s missing.',
5260
+ $functionName,
5261
+ count($missing) > 1 ? 's' : '',
5262
+ implode(', ', $missing)
5263
+ );
5264
+ }
5265
  }
5266
 
5267
  /**
5280
  if ($this->callStack) {
5281
  foreach (array_reverse($this->callStack) as $call) {
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
+
5289
  $callStackMsg[] = $msg;
5290
 
5291
+ if (! \is_null($limit) && $ncall > $limit) {
5292
  break;
5293
  }
5294
  }
5308
  protected function handleImportLoop($name)
5309
  {
5310
  for ($env = $this->env; $env; $env = $env->parent) {
5311
+ if (! $env->block) {
5312
+ continue;
5313
+ }
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
  }
5320
  }
5321
  }
5322
 
 
 
 
 
 
 
 
 
 
 
 
 
5323
  /**
5324
  * Call SCSS @function
5325
  *
5326
+ * @param Object $func
5327
  * @param array $argValues
 
5328
  *
5329
+ * @return array $returnValue
5330
  */
5331
+ protected function callScssFunction($func, $argValues)
5332
  {
 
 
5333
  if (! $func) {
5334
+ return static::$defaultValue;
5335
  }
5336
+ $name = $func->name;
5337
 
5338
  $this->pushEnv();
5339
 
 
 
 
5340
  // set the args
5341
  if (isset($func->args)) {
5342
  $this->applyArguments($func->args, $argValues);
5343
  }
5344
 
5345
  // throw away lines and children
5346
+ $tmp = new OutputBlock();
5347
  $tmp->lines = [];
5348
  $tmp->children = [];
5349
 
5350
  $this->env->marker = 'function';
5351
 
5352
+ if (! empty($func->parentEnv)) {
5353
+ $this->env->declarationScopeParent = $func->parentEnv;
5354
+ } else {
5355
+ throw $this->error("@function $name() without parentEnv");
5356
+ }
5357
 
5358
+ $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . ' ' . $name);
5359
 
5360
  $this->popEnv();
5361
 
5362
+ return ! isset($ret) ? static::$defaultValue : $ret;
 
 
5363
  }
5364
 
5365
  /**
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
  {
5377
+ $libName = (is_array($function) ? end($function) : null);
5378
+ $sorted_kwargs = $this->sortNativeFunctionArgs($libName, $prototype, $args);
5379
 
5380
+ if (\is_null($sorted_kwargs)) {
5381
+ return null;
 
 
 
 
 
 
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
 
5397
+ $returnValue = \call_user_func($function, $sorted, $kwargs);
5398
 
5399
  if (! isset($returnValue)) {
5400
+ return null;
5401
  }
5402
 
5403
+ return $this->coerceValue($returnValue);
 
 
5404
  }
5405
 
5406
  /**
5412
  */
5413
  protected function getBuiltinFunction($name)
5414
  {
5415
+ $libName = self::normalizeNativeFunctionName($name);
5416
+ return [$this, $libName];
5417
+ }
5418
+
5419
+ /**
5420
+ * Normalize native function name
5421
+ * @param $name
5422
+ * @return string
5423
+ */
5424
+ public static function normalizeNativeFunctionName($name)
5425
+ {
5426
+ $name = str_replace("-", "_", $name);
5427
  $libName = 'lib' . preg_replace_callback(
5428
  '/_(.)/',
5429
  function ($m) {
5431
  },
5432
  ucfirst($name)
5433
  );
5434
+ return $libName;
5435
+ }
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)
5443
+ {
5444
+ return method_exists(Compiler::class, self::normalizeNativeFunctionName($name));
5445
  }
5446
 
5447
  /**
5448
  * Sorts keyword arguments
5449
  *
5450
+ * @param string $functionName
5451
+ * @param array $prototypes
5452
+ * @param array $args
5453
  *
5454
+ * @return array|null
5455
  */
5456
+ protected function sortNativeFunctionArgs($functionName, $prototypes, $args)
5457
  {
5458
  static $parser = null;
5459
 
5461
  $keyArgs = [];
5462
  $posArgs = [];
5463
 
5464
+ if (\is_array($args) && \count($args) && \end($args) === static::$null) {
5465
+ array_pop($args);
5466
+ }
5467
+
5468
  // separate positional and keyword arguments
5469
  foreach ($args as $arg) {
5470
  list($key, $value) = $arg;
5471
 
5472
+ if (empty($key) or empty($key[1])) {
 
 
5473
  $posArgs[] = empty($arg[2]) ? $value : $arg;
5474
  } else {
5475
+ $keyArgs[$key[1]] = $value;
5476
  }
5477
  }
5478
 
5479
  return [$posArgs, $keyArgs];
5480
  }
5481
 
5482
+ // specific cases ?
5483
+ if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) {
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
 
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
 
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) {
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];
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
 
5649
  $splatSeparator = null;
5650
  $keywordArgs = [];
5651
  $deferredKeywordArgs = [];
5652
+ $deferredNamedKeywordArgs = [];
5653
  $remaining = [];
5654
  $hasKeywordArgument = false;
5655
 
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
  }
5702
  $keywordArgs[$name] = $item;
5703
  }
5704
  } else {
5705
+ if (\is_null($splatSeparator)) {
5706
  $splatSeparator = $val[1];
5707
  }
5708
+
5709
  $remaining[] = $item;
5710
  }
5711
  }
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
  }
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
  }
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])) {
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);
5787
  } else {
5788
+ $output[$name] = ($reduce ? $this->reduce($val, true) : $val);
5789
  }
5790
  }
5791
 
5803
  if ($storeInEnv) {
5804
  $this->set($name, $this->reduce($default, true), true);
5805
  } else {
5806
+ $output[$name] = ($reduce ? $this->reduce($default, true) : $default);
5807
  }
5808
  }
5809
 
5819
  */
5820
  protected function coerceValue($value)
5821
  {
5822
+ if (\is_array($value) || $value instanceof \ArrayAccess) {
5823
  return $value;
5824
  }
5825
 
5826
+ if (\is_bool($value)) {
5827
  return $this->toBool($value);
5828
  }
5829
 
5830
+ if (\is_null($value)) {
5831
  return static::$null;
5832
  }
5833
 
5839
  return static::$emptyString;
5840
  }
5841
 
5842
+ $value = [Type::T_KEYWORD, $value];
5843
+ $color = $this->coerceColor($value);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5844
 
5845
+ if ($color) {
5846
  return $color;
5847
  }
5848
 
5849
+ return $value;
5850
  }
5851
 
5852
  /**
5862
  return $item;
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
  }
5872
 
5873
+ return $item;
5874
  }
5875
 
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]);
5891
+ }
5892
+
5893
  return $item;
5894
  }
5895
 
5898
  $values = $item[2];
5899
  $list = [];
5900
 
5901
+ for ($i = 0, $s = \count($keys); $i < $s; $i++) {
5902
  $key = $keys[$i];
5903
  $value = $values[$i];
5904
 
5905
  switch ($key[0]) {
5906
  case Type::T_LIST:
5907
  case Type::T_MAP:
5908
+ case Type::T_STRING:
5909
+ case Type::T_NULL:
5910
  break;
5911
 
5912
  default:
5924
  return [Type::T_LIST, ',', $list];
5925
  }
5926
 
5927
+ return [Type::T_LIST, $delim, ! isset($item) ? [] : [$item]];
5928
  }
5929
 
5930
  /**
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++) {
5958
+ if (! is_numeric($value[$i])) {
5959
+ $cv = $this->compileRGBAValue($value[$i]);
5960
+
5961
+ if (! is_numeric($cv)) {
5962
+ return null;
5963
+ }
5964
+
5965
+ $value[$i] = $cv;
5966
+ }
5967
+
5968
+ if (isset($value[4])) {
5969
+ if (! is_numeric($value[4])) {
5970
+ $cv = $this->compileRGBAValue($value[4], true);
5971
+
5972
+ if (! is_numeric($cv)) {
5973
+ return null;
5974
+ }
5975
+
5976
+ $value[4] = $cv;
5977
+ }
5978
+ }
5979
+ }
5980
+
5981
  return $value;
5982
 
5983
+ case Type::T_LIST:
5984
+ if ($inRGBFunction) {
5985
+ if (\count($value[2]) == 3 || \count($value[2]) == 4) {
5986
+ $color = $value[2];
5987
+ array_unshift($color, Type::T_COLOR);
5988
+
5989
+ return $this->coerceColor($color);
5990
+ }
5991
+ }
5992
+
5993
+ return null;
5994
+
5995
  case Type::T_KEYWORD:
5996
+ if (! \is_string($value[1])) {
5997
+ return null;
5998
+ }
5999
+
6000
  $name = strtolower($value[1]);
6001
 
6002
+ // hexa color?
6003
+ if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) {
6004
+ $nofValues = \strlen($m[1]);
6005
+
6006
+ if (\in_array($nofValues, [3, 4, 6, 8])) {
6007
+ $nbChannels = 3;
6008
+ $color = [];
6009
+ $num = hexdec($m[1]);
6010
+
6011
+ switch ($nofValues) {
6012
+ case 4:
6013
+ $nbChannels = 4;
6014
+ // then continuing with the case 3:
6015
+ case 3:
6016
+ for ($i = 0; $i < $nbChannels; $i++) {
6017
+ $t = $num & 0xf;
6018
+ array_unshift($color, $t << 4 | $t);
6019
+ $num >>= 4;
6020
+ }
6021
 
6022
+ break;
6023
+
6024
+ case 8:
6025
+ $nbChannels = 4;
6026
+ // then continuing with the case 6:
6027
+ case 6:
6028
+ for ($i = 0; $i < $nbChannels; $i++) {
6029
+ array_unshift($color, $num & 0xff);
6030
+ $num >>= 8;
6031
+ }
6032
+
6033
+ break;
6034
+ }
6035
+
6036
+ if ($nbChannels === 4) {
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
+
6044
+ array_unshift($color, Type::T_COLOR);
6045
+
6046
+ return $color;
6047
+ }
6048
+ }
6049
+
6050
+ if ($rgba = Colors::colorNameToRGBa($name)) {
6051
  return isset($rgba[3])
6052
+ ? [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2], $rgba[3]]
6053
+ : [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2]];
6054
  }
6055
 
6056
  return null;
6059
  return null;
6060
  }
6061
 
6062
+ /**
6063
+ * @param integer|\ScssPhp\ScssPhp\Node\Number $value
6064
+ * @param boolean $isAlpha
6065
+ *
6066
+ * @return integer|mixed
6067
+ */
6068
+ protected function compileRGBAValue($value, $isAlpha = false)
6069
+ {
6070
+ if ($isAlpha) {
6071
+ return $this->compileColorPartValue($value, 0, 1, false);
6072
+ }
6073
+
6074
+ return $this->compileColorPartValue($value, 0, 255, true);
6075
+ }
6076
+
6077
+ /**
6078
+ * @param mixed $value
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;
6115
+ } elseif (\is_array($value)) {
6116
+ $value = $this->compileValue($value);
6117
+ }
6118
+ }
6119
+
6120
+ if (is_numeric($value)) {
6121
+ if ($isInt) {
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
+ }
6140
+
6141
+ return $value;
6142
+ }
6143
+
6144
  /**
6145
  * Coerce value to string
6146
  *
6157
  return [Type::T_STRING, '', [$this->compileValue($value)]];
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
+ {
6174
+ // case of url(...) parsed a a function
6175
+ if ($value[0] === Type::T_FUNCTION) {
6176
+ $value = $this->coerceString($value);
6177
+ }
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
  *
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;
6243
  public function assertList($value)
6244
  {
6245
  if ($value[0] !== Type::T_LIST) {
6246
+ throw $this->error('expecting list, %s received', $value[0]);
6247
  }
6248
 
6249
  return $value;
6266
  return $color;
6267
  }
6268
 
6269
+ throw $this->error('expecting color, %s received', $value[0]);
6270
+ }
6271
+
6272
+ /**
6273
+ * Assert value is a number
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
  /**
6296
+ * Assert value is a integer
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
6322
  *
6405
  }
6406
 
6407
  if ($h * 3 < 2) {
6408
+ return $m1 + ($m2 - $m1) * (2 / 3 - $h) * 6;
6409
  }
6410
 
6411
  return $m1;
6435
  $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
6436
  $m1 = $l * 2 - $m2;
6437
 
6438
+ $r = $this->hueToRGB($m1, $m2, $h + 1 / 3) * 255;
6439
  $g = $this->hueToRGB($m1, $m2, $h) * 255;
6440
+ $b = $this->hueToRGB($m1, $m2, $h - 1 / 3) * 255;
6441
 
6442
  $out = [Type::T_COLOR, $r, $g, $b];
6443
 
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) {
6463
+ return static::$null;
6464
+ }
6465
+
6466
+ if (! in_array($functionReference[0], [Type::T_FUNCTION_REFERENCE, Type::T_FUNCTION])) {
6467
+ throw $this->error('Function reference expected, got ' . $functionReference[0]);
6468
+ }
6469
+
6470
  $callArgs = [];
6471
 
6472
  // $kwargs['args'] is [Type::T_LIST, ',', [..]]
6480
  $callArgs[] = [$varname, $arg, false];
6481
  }
6482
 
6483
+ return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]);
6484
+ }
6485
+
6486
+
6487
+ protected static $libGetFunction = [
6488
+ ['name'],
6489
+ ['name', 'css']
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
+
6501
+ if ($isCss) {
6502
+ return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]];
6503
+ }
6504
+
6505
+ return $this->getFunctionReference($name, true);
6506
  }
6507
 
6508
  protected static $libIf = ['condition', 'if-true', 'if-false:'];
6522
  {
6523
  list($list, $value) = $args;
6524
 
6525
+ if (
6526
+ $list[0] === Type::T_MAP ||
 
 
 
6527
  $list[0] === Type::T_STRING ||
6528
  $list[0] === Type::T_KEYWORD ||
6529
  $list[0] === Type::T_INTERPOLATE
6546
  return false === $key ? static::$null : $key + 1;
6547
  }
6548
 
6549
+ protected static $libRgb = [
6550
+ ['color'],
6551
+ ['color', 'alpha'],
6552
+ ['channels'],
6553
+ ['red', 'green', 'blue'],
6554
+ ['red', 'green', 'blue', 'alpha'] ];
6555
+ protected function libRgb($args, $kwargs, $funcName = 'rgb')
6556
  {
6557
+ switch (\count($args)) {
6558
+ case 1:
6559
+ if (! $color = $this->coerceColor($args[0], true)) {
6560
+ $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
6561
+ }
6562
+ break;
6563
 
6564
+ case 3:
6565
+ $color = [Type::T_COLOR, $args[0], $args[1], $args[2]];
6566
 
6567
+ if (! $color = $this->coerceColor($color)) {
6568
+ $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
6569
+ }
 
 
 
 
 
 
6570
 
6571
+ return $color;
6572
+
6573
+ case 2:
6574
+ if ($color = $this->coerceColor($args[0], true)) {
6575
+ $alpha = $this->compileRGBAValue($args[1], true);
6576
+
6577
+ if (is_numeric($alpha)) {
6578
+ $color[4] = $alpha;
6579
+ } else {
6580
+ $color = [Type::T_STRING, '',
6581
+ [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']];
6582
+ }
6583
+ } else {
6584
+ $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
6585
+ }
6586
+ break;
6587
+
6588
+ case 4:
6589
+ default:
6590
+ $color = [Type::T_COLOR, $args[0], $args[1], $args[2], $args[3]];
6591
+
6592
+ if (! $color = $this->coerceColor($color)) {
6593
+ $color = [Type::T_STRING, '',
6594
+ [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
6595
+ }
6596
+ break;
6597
  }
6598
 
6599
+ return $color;
6600
+ }
6601
 
6602
+ protected static $libRgba = [
6603
+ ['color'],
6604
+ ['color', 'alpha'],
6605
+ ['channels'],
6606
+ ['red', 'green', 'blue'],
6607
+ ['red', 'green', 'blue', 'alpha'] ];
6608
+ protected function libRgba($args, $kwargs)
6609
+ {
6610
+ return $this->libRgb($args, $kwargs, 'rgba');
6611
  }
6612
 
6613
  // helper function for adjust_color, change_color, and scale_color
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
 
6714
  protected function libIeHexStr($args)
6715
  {
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;
6723
 
6724
+ return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]];
6725
  }
6726
 
6727
  protected static $libRed = ['color'];
6729
  {
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
 
6741
  {
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
 
6753
  {
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
 
6784
  }
6785
 
6786
  // mix two colors
6787
+ protected static $libMix = [
6788
+ ['color1', 'color2', 'weight:0.5'],
6789
+ ['color-1', 'color-2', 'weight:0.5']
6790
+ ];
6791
  protected function libMix($args)
6792
  {
6793
  list($first, $second, $weight) = $args;
6823
  return $this->fixColor($new);
6824
  }
6825
 
6826
+ protected static $libHsl = [
6827
+ ['channels'],
6828
+ ['hue', 'saturation', 'lightness'],
6829
+ ['hue', 'saturation', 'lightness', 'alpha'] ];
6830
+ protected function libHsl($args, $kwargs, $funcName = 'hsl')
6831
  {
6832
+ $args_to_check = $args;
6833
 
6834
+ if (\count($args) == 1) {
6835
+ if ($args[0][0] !== Type::T_LIST || \count($args[0][2]) < 3 || \count($args[0][2]) > 4) {
6836
+ return [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
6837
+ }
6838
 
6839
+ $args = $args[0][2];
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 (
6864
+ $k >= 2 && count($args) === 4 &&
6865
+ in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) &&
6866
+ in_array($arg[1], ['calc','env'])
6867
+ ) {
6868
+ return null;
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;
6891
+ }
6892
 
6893
  return $color;
6894
  }
6895
 
6896
+ protected static $libHsla = [
6897
+ ['channels'],
6898
+ ['hue', 'saturation', 'lightness'],
6899
+ ['hue', 'saturation', 'lightness', 'alpha']];
6900
+ protected function libHsla($args, $kwargs)
6901
+ {
6902
+ return $this->libHsl($args, $kwargs, 'hsla');
6903
+ }
6904
+
6905
  protected static $libHue = ['color'];
6906
  protected function libHue($args)
6907
  {
6969
  return $this->adjustHsl($color, 3, -$amount);
6970
  }
6971
 
6972
+ protected static $libSaturate = [['color', 'amount'], ['amount']];
6973
  protected function libSaturate($args)
6974
  {
6975
  $value = $args[0];
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
 
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;
7045
  }
7046
 
7047
  // increases opacity by amount
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];
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];
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];
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];
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
  /**
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;
7224
  protected static $libLength = ['list'];
7225
  protected function libLength($args)
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
 
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--;
7264
  } elseif ($n < 0) {
7265
+ $n += \count($list[2]);
7266
  }
7267
 
7268
  return isset($list[2][$n]) ? $list[2][$n] : static::$defaultValue;
7277
  if ($n > 0) {
7278
  $n--;
7279
  } elseif ($n < 0) {
7280
+ $n += \count($list[2]);
7281
  }
7282
 
7283
  if (! isset($list[2][$n])) {
7284
+ throw $this->error('Invalid argument for "n"');
 
 
7285
  }
7286
 
7287
  $list[2][$n] = $args[2];
7295
  $map = $this->assertMap($args[0]);
7296
  $key = $args[1];
7297
 
7298
+ if (! \is_null($key)) {
7299
  $key = $this->compileStringContent($this->coerceString($key));
7300
+
7301
+ for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
7302
  if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
7303
  return $map[2][$i];
7304
  }
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
+
7341
+ for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
7342
+ if (in_array($this->compileStringContent($this->coerceString($map[1][$i])), $keys)) {
7343
  array_splice($map[1], $i, 1);
7344
  array_splice($map[2], $i, 1);
7345
  }
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]))) {
7359
  return true;
7360
  }
7363
  return false;
7364
  }
7365
 
7366
+ protected static $libMapMerge = [
7367
+ ['map1', 'map2'],
7368
+ ['map-1', 'map-2']
7369
+ ];
7370
  protected function libMapMerge($args)
7371
  {
7372
  $map1 = $this->assertMap($args[0]);
7405
  return [Type::T_MAP, $keys, $values];
7406
  }
7407
 
7408
+ protected static $libIsBracketed = ['list'];
7409
+ protected function libIsBracketed($args)
7410
+ {
7411
+ $list = $args[0];
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)) {
7429
  return ',';
7430
 
7431
  case 'space':
7432
+ return ' ';
7433
 
7434
  default:
7435
  return $list1[1];
7436
  }
7437
  }
7438
 
7439
+ protected static $libJoin = ['list1', 'list2', 'separator:null', 'bracketed:auto'];
7440
  protected function libJoin($args)
7441
  {
7442
+ list($list1, $list2, $sep, $bracketed) = $args;
7443
+
7444
+ $list1 = $this->coerceList($list1, ' ', true);
7445
+ $list2 = $this->coerceList($list2, ' ', true);
7446
+ $sep = $this->listSeparatorForJoin($list1, $sep);
7447
+
7448
+ if ($bracketed === static::$true) {
7449
+ $bracketed = true;
7450
+ } elseif ($bracketed === static::$false) {
7451
+ $bracketed = false;
7452
+ } elseif ($bracketed === [Type::T_KEYWORD, 'auto']) {
7453
+ $bracketed = 'auto';
7454
+ } elseif ($bracketed === static::$null) {
7455
+ $bracketed = false;
7456
+ } else {
7457
+ $bracketed = $this->compileValue($bracketed);
7458
+ $bracketed = ! ! $bracketed;
7459
+
7460
+ if ($bracketed === true) {
7461
+ $bracketed = true;
7462
+ }
7463
+ }
7464
+
7465
+ if ($bracketed === 'auto') {
7466
+ $bracketed = false;
7467
+
7468
+ if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') {
7469
+ $bracketed = true;
7470
+ }
7471
+ }
7472
+
7473
+ $res = [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
7474
+
7475
+ if (isset($list1['enclosing'])) {
7476
+ $res['enlcosing'] = $list1['enclosing'];
7477
+ }
7478
 
7479
+ if ($bracketed) {
7480
+ $res['enclosing'] = 'bracket';
7481
+ }
7482
 
7483
+ return $res;
7484
  }
7485
 
7486
  protected static $libAppend = ['list', 'val', 'separator:null'];
7488
  {
7489
  list($list1, $value, $sep) = $args;
7490
 
7491
+ $list1 = $this->coerceList($list1, ' ', true);
7492
+ $sep = $this->listSeparatorForJoin($list1, $sep);
7493
+ $res = [Type::T_LIST, $sep, array_merge($list1[2], [$value])];
7494
+
7495
+ if (isset($list1['enclosing'])) {
7496
+ $res['enclosing'] = $list1['enclosing'];
7497
+ }
7498
 
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 {
7520
+ break 2;
7521
+ }
7522
  }
7523
+
7524
+ $lists[] = $list;
7525
  }
7526
 
7527
+ $result[2] = $lists;
7528
+ } else {
7529
+ $result['enclosing'] = 'parent';
7530
  }
7531
 
7532
+ return $result;
7533
  }
7534
 
7535
  protected static $libTypeOf = ['value'];
7551
  case Type::T_FUNCTION:
7552
  return 'string';
7553
 
7554
+ case Type::T_FUNCTION_REFERENCE:
7555
+ return 'function';
7556
+
7557
  case Type::T_LIST:
7558
  if (isset($value[3]) && $value[3]) {
7559
  return 'arglist';
7585
  return $value[0] === Type::T_NUMBER && $value->unitless();
7586
  }
7587
 
7588
+ protected static $libComparable = [
7589
+ ['number1', 'number2'],
7590
+ ['number-1', 'number-2']
7591
+ ];
7592
  protected function libComparable($args)
7593
  {
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();
7609
  protected static $libStrIndex = ['string', 'substring'];
7610
  protected function libStrIndex($args)
7611
  {
7612
+ $string = $this->assertString($args[0], 'string');
7613
  $stringContent = $this->compileStringContent($string);
7614
 
7615
+ $substring = $this->assertString($args[1], 'substring');
7616
  $substringContent = $this->compileStringContent($substring);
7617
 
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
  }
7627
  protected static $libStrInsert = ['string', 'insert', 'index'];
7628
  protected function libStrInsert($args)
7629
  {
7630
+ $string = $this->assertString($args[0], 'string');
7631
  $stringContent = $this->compileStringContent($string);
7632
 
7633
+ $insert = $this->assertString($args[1], 'insert');
7634
  $insertContent = $this->compileStringContent($insert);
7635
 
7636
+ $index = $this->assertInteger($args[2], 'index');
7637
+ if ($index > 0) {
7638
+ $index = $index - 1;
7639
+ }
7640
+ if ($index < 0) {
7641
+ $index = Util::mbStrlen($stringContent) + 1 + $index;
7642
+ }
7643
 
7644
+ $string[2] = [
7645
+ Util::mbSubstr($stringContent, 0, $index),
7646
+ $insertContent,
7647
+ Util::mbSubstr($stringContent, $index)
7648
+ ];
7649
 
7650
  return $string;
7651
  }
7653
  protected static $libStrLength = ['string'];
7654
  protected function libStrLength($args)
7655
  {
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
 
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
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
  }
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
  }
7714
  $name = $this->compileStringContent($string);
7715
 
7716
  return $this->toBool(
7717
+ \array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false
7718
  );
7719
  }
7720
 
7738
  // built-in functions
7739
  $f = $this->getBuiltinFunction($name);
7740
 
7741
+ return $this->toBool(\is_callable($f));
7742
  }
7743
 
7744
  protected static $libGlobalVariableExists = ['name'];
7782
  return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']];
7783
  }
7784
 
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()
7808
  static $id;
7809
 
7810
  if (! isset($id)) {
7811
+ $id = PHP_INT_SIZE === 4
7812
+ ? mt_rand(0, pow(36, 5)) . str_pad(mt_rand(0, pow(36, 5)) % 10000000, 7, '0', STR_PAD_LEFT)
7813
+ : mt_rand(0, pow(36, 8));
7814
  }
7815
 
7816
  $id += mt_rand(0, 10) + 1;
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) {
7824
+ $value = [Type::T_KEYWORD, 'null'];
7825
+ }
7826
+
7827
+ $stringValue = [$value];
7828
+
7829
+ if ($value[0] === Type::T_LIST) {
7830
+ if (end($value[2]) === static::$null) {
7831
+ array_pop($value[2]);
7832
+ $value[2][] = [Type::T_STRING, '', ['']];
7833
+ $force_enclosing_display = true;
7834
+ }
7835
+
7836
+ if (
7837
+ ! empty($value['enclosing']) &&
7838
+ ($force_enclosing_display ||
7839
+ ($value['enclosing'] === 'bracket') ||
7840
+ ! \count($value[2]))
7841
+ ) {
7842
+ $value['enclosing'] = 'forced_' . $value['enclosing'];
7843
+ $force_enclosing_display = true;
7844
+ }
7845
+
7846
+ foreach ($value[2] as $k => $listelement) {
7847
+ $value[2][$k] = $this->inspectFormatValue($listelement, $force_enclosing_display);
7848
+ }
7849
+
7850
+ $stringValue = [$value];
7851
+ }
7852
+
7853
+ return [Type::T_STRING, '', $stringValue];
7854
+ }
7855
+
7856
  protected static $libInspect = ['value'];
7857
  protected function libInspect($args)
7858
  {
7859
+ $value = $args[0];
 
 
7860
 
7861
+ return $this->inspectFormatValue($value);
7862
  }
7863
 
7864
  /**
7868
  *
7869
  * @return array|boolean
7870
  */
7871
+ protected function getSelectorArg($arg, $varname = null, $allowParent = false)
7872
  {
7873
  static $parser = null;
7874
 
7875
+ if (\is_null($parser)) {
7876
  $parser = $this->parserFactory(__METHOD__);
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
 
7892
  $selector = $this->evalSelectors($parsedSelector);
7893
  $gluedSelector = $this->glueFunctionSelectors($selector);
7894
 
7895
+ if (! $allowParent) {
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
+ }
7904
+ }
7905
+
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
+ /**
7914
+ * Check variable type for getSelectorArg() function
7915
+ * @param array $arg
7916
+ * @param int $maxDepth
7917
+ * @return bool
7918
+ */
7919
+ protected function checkSelectorArgType($arg, $maxDepth = 2)
7920
+ {
7921
+ if ($arg[0] === Type::T_LIST && $maxDepth > 0) {
7922
+ foreach ($arg[2] as $elt) {
7923
+ if (! $this->checkSelectorArgType($elt, $maxDepth - 1)) {
7924
+ return false;
7925
+ }
7926
+ }
7927
+ return true;
7928
+ }
7929
+ if (!in_array($arg[0], [Type::T_STRING, Type::T_KEYWORD])) {
7930
+ return false;
7931
+ }
7932
+ return true;
7933
  }
7934
 
7935
  /**
7951
  {
7952
  list($super, $sub) = $args;
7953
 
7954
+ $super = $this->getSelectorArg($super, 'super');
7955
+ $sub = $this->getSelectorArg($sub, 'sub');
7956
 
7957
  return $this->isSuperSelector($super, $sub);
7958
  }
7968
  protected function isSuperSelector($super, $sub)
7969
  {
7970
  // one and only one selector for each arg
7971
+ if (! $super) {
7972
+ throw $this->error('Invalid super selector for isSuperSelector()');
7973
+ }
7974
+
7975
+ if (! $sub) {
7976
+ throw $this->error('Invalid sub selector for isSuperSelector()');
7977
+ }
7978
+
7979
+ if (count($sub) > 1) {
7980
+ foreach ($sub as $s) {
7981
+ if (! $this->isSuperSelector($super, [$s])) {
7982
+ return false;
7983
+ }
7984
+ }
7985
+ return true;
7986
  }
7987
 
7988
+ if (count($super) > 1) {
7989
+ foreach ($super as $s) {
7990
+ if ($this->isSuperSelector([$s], $sub)) {
7991
+ return true;
7992
+ }
7993
+ }
7994
+ return false;
7995
  }
7996
 
7997
  $super = reset($super);
8018
  $nextMustMatch = true;
8019
  $i++;
8020
  } else {
8021
+ while ($i < \count($sub) && ! $this->isSuperPart($node, $sub[$i])) {
8022
  if ($nextMustMatch) {
8023
  return false;
8024
  }
8026
  $i++;
8027
  }
8028
 
8029
+ if ($i >= \count($sub)) {
8030
  return false;
8031
  }
8032
 
8051
  $i = 0;
8052
 
8053
  foreach ($superParts as $superPart) {
8054
+ while ($i < \count($subParts) && $subParts[$i] !== $superPart) {
8055
  $i++;
8056
  }
8057
 
8058
+ if ($i >= \count($subParts)) {
8059
  return false;
8060
  }
8061
 
8071
  // get the selector... list
8072
  $args = reset($args);
8073
  $args = $args[2];
8074
+
8075
+ if (\count($args) < 1) {
8076
+ throw $this->error('selector-append() needs at least 1 argument');
8077
  }
8078
 
8079
+ $selectors = [];
8080
+ foreach ($args as $arg) {
8081
+ $selectors[] = $this->getSelectorArg($arg, 'selector');
8082
+ }
8083
 
8084
  return $this->formatOutputSelector($this->selectorAppend($selectors));
8085
  }
8098
  $lastSelectors = array_pop($selectors);
8099
 
8100
  if (! $lastSelectors) {
8101
+ throw $this->error('Invalid selector list in selector-append()');
8102
  }
8103
 
8104
+ while (\count($selectors)) {
8105
  $previousSelectors = array_pop($selectors);
8106
 
8107
  if (! $previousSelectors) {
8108
+ throw $this->error('Invalid selector list in selector-append()');
8109
  }
8110
 
8111
  // do the trick, happening $lastSelector to $previousSelector
8135
  return $lastSelectors;
8136
  }
8137
 
8138
+ protected static $libSelectorExtend = [
8139
+ ['selector', 'extendee', 'extender'],
8140
+ ['selectors', 'extendee', 'extender']
8141
+ ];
8142
  protected function libSelectorExtend($args)
8143
  {
8144
  list($selectors, $extendee, $extender) = $args;
8145
 
8146
+ $selectors = $this->getSelectorArg($selectors, 'selector');
8147
+ $extendee = $this->getSelectorArg($extendee, 'extendee');
8148
+ $extender = $this->getSelectorArg($extender, 'extender');
8149
 
8150
  if (! $selectors || ! $extendee || ! $extender) {
8151
+ throw $this->error('selector-extend() invalid arguments');
8152
  }
8153
 
8154
  $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender);
8156
  return $this->formatOutputSelector($extended);
8157
  }
8158
 
8159
+ protected static $libSelectorReplace = [
8160
+ ['selector', 'original', 'replacement'],
8161
+ ['selectors', 'original', 'replacement']
8162
+ ];
8163
  protected function libSelectorReplace($args)
8164
  {
8165
  list($selectors, $original, $replacement) = $args;
8166
 
8167
+ $selectors = $this->getSelectorArg($selectors, 'selector');
8168
+ $original = $this->getSelectorArg($original, 'original');
8169
+ $replacement = $this->getSelectorArg($replacement, 'replacement');
8170
 
8171
  if (! $selectors || ! $original || ! $replacement) {
8172
+ throw $this->error('selector-replace() invalid arguments');
8173
  }
8174
 
8175
  $replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true);
8208
  $extended[] = $selector;
8209
  }
8210
 
8211
+ $n = \count($extended);
8212
 
8213
  $this->matchExtends($selector, $extended);
8214
 
8215
  // if didnt match, keep the original selector if we are in a replace operation
8216
+ if ($replace && \count($extended) === $n) {
8217
  $extended[] = $selector;
8218
  }
8219
  }
8230
  // get the selector... list
8231
  $args = reset($args);
8232
  $args = $args[2];
8233
+
8234
+ if (\count($args) < 1) {
8235
+ throw $this->error('selector-nest() needs at least 1 argument');
8236
  }
8237
 
8238
+ $selectorsMap = [];
8239
+ foreach ($args as $arg) {
8240
+ $selectorsMap[] = $this->getSelectorArg($arg, 'selector', true);
8241
+ }
8242
 
8243
  $envs = [];
8244
+
8245
  foreach ($selectorsMap as $selectors) {
8246
  $env = new Environment();
8247
  $env->selectors = $selectors;
8249
  $envs[] = $env;
8250
  }
8251
 
8252
+ $envs = array_reverse($envs);
8253
+ $env = $this->extractEnv($envs);
8254
  $outputSelectors = $this->multiplySelectors($env);
8255
 
8256
  return $this->formatOutputSelector($outputSelectors);
8257
  }
8258
 
8259
+ protected static $libSelectorParse = [
8260
+ ['selector'],
8261
+ ['selectors']
8262
+ ];
8263
  protected function libSelectorParse($args)
8264
  {
8265
  $selectors = reset($args);
8266
+ $selectors = $this->getSelectorArg($selectors, 'selector');
8267
 
8268
  return $this->formatOutputSelector($selectors);
8269
  }
8273
  {
8274
  list($selectors1, $selectors2) = $args;
8275
 
8276
+ $selectors1 = $this->getSelectorArg($selectors1, 'selectors1');
8277
+ $selectors2 = $this->getSelectorArg($selectors2, 'selectors2');
8278
 
8279
  if (! $selectors1 || ! $selectors2) {
8280
+ throw $this->error('selector-unify() invalid arguments');
8281
  }
8282
 
8283
  // only consider the first compound of each
8296
  *
8297
  * @param array $compound1
8298
  * @param array $compound2
8299
+ *
8300
  * @return array|mixed
8301
  */
8302
  protected function unifyCompoundSelectors($compound1, $compound2)
8303
  {
8304
+ if (! \count($compound1)) {
8305
  return $compound2;
8306
  }
8307
 
8308
+ if (! \count($compound2)) {
8309
  return $compound1;
8310
  }
8311
 
8312
  // check that last part are compatible
8313
  $lastPart1 = array_pop($compound1);
8314
  $lastPart2 = array_pop($compound2);
8315
+ $last = $this->mergeParts($lastPart1, $lastPart2);
8316
 
8317
  if (! $last) {
8318
  return [[]];
8322
  $unifiedSelectors = [$unifiedCompound];
8323
 
8324
  // do the rest
8325
+ while (\count($compound1) || \count($compound2)) {
8326
  $part1 = end($compound1);
8327
  $part2 = end($compound2);
8328
 
8335
 
8336
  $c = $this->mergeParts($part1, $part2);
8337
  $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
8338
+
8339
  $part1 = $part2 = null;
8340
 
8341
  array_pop($compound1);
8350
 
8351
  $c = $this->mergeParts($part2, $part1);
8352
  $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
8353
+
8354
  $part1 = $part2 = null;
8355
 
8356
  array_pop($compound2);
8362
  array_pop($compound1);
8363
  array_pop($compound2);
8364
 
8365
+ $s = $this->prependSelectors($unifiedSelectors, [$part2]);
8366
  $new = array_merge($new, $this->prependSelectors($s, [$part1]));
8367
+ $s = $this->prependSelectors($unifiedSelectors, [$part1]);
8368
  $new = array_merge($new, $this->prependSelectors($s, [$part2]));
8369
  } elseif ($part1) {
8370
  array_pop($compound1);
8418
  protected function matchPartInCompound($part, $compound)
8419
  {
8420
  $partTag = $this->findTagName($part);
8421
+ $before = $compound;
8422
+ $after = [];
8423
 
8424
  // try to find a match by tag name first
8425
+ while (\count($before)) {
8426
  $p = array_pop($before);
8427
 
8428
  if ($partTag && $partTag !== '*' && $partTag == $this->findTagName($p)) {
8436
  $before = $compound;
8437
  $after = [];
8438
 
8439
+ while (\count($before)) {
8440
  $p = array_pop($before);
8441
 
8442
  if ($this->checkCompatibleTags($partTag, $this->findTagName($p))) {
8443
+ if (\count(array_intersect($part, $p))) {
8444
  return [$before, $p, $after];
8445
  }
8446
  }
8465
  {
8466
  $tag1 = $this->findTagName($parts1);
8467
  $tag2 = $this->findTagName($parts2);
8468
+ $tag = $this->checkCompatibleTags($tag1, $tag2);
8469
 
8470
  // not compatible tags
8471
  if ($tag === false) {
8516
  $tags = array_unique($tags);
8517
  $tags = array_filter($tags);
8518
 
8519
+ if (\count($tags) > 1) {
8520
  $tags = array_diff($tags, ['*']);
8521
  }
8522
 
8523
  // not compatible nodes
8524
+ if (\count($tags) > 1) {
8525
  return false;
8526
  }
8527
 
8550
  protected function libSimpleSelectors($args)
8551
  {
8552
  $selector = reset($args);
8553
+ $selector = $this->getSelectorArg($selector, 'selector');
8554
 
8555
  // remove selectors list layer, keeping the first one
8556
  $selector = reset($selector);
8566
 
8567
  return [Type::T_LIST, ',', $listParts];
8568
  }
8569
+
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 = [];
8577
+
8578
+ foreach ($matches as $match) {
8579
+ if (! is_file($match)) {
8580
+ continue;
8581
+ }
8582
+
8583
+ $listParts[] = [Type::T_STRING, '"', [$match]];
8584
+ }
8585
+
8586
+ return [Type::T_LIST, ',', $listParts];
8587
+ }
8588
  }
scssphp/src/Compiler/Environment.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
scssphp/src/Exception/CompilerException.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -16,6 +17,6 @@ namespace ScssPhp\ScssPhp\Exception;
16
  *
17
  * @author Oleksandr Savchenko <traveltino@gmail.com>
18
  */
19
- class CompilerException extends \Exception
20
  {
21
  }
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
17
  *
18
  * @author Oleksandr Savchenko <traveltino@gmail.com>
19
  */
20
+ class CompilerException extends \Exception implements SassException
21
  {
22
  }
scssphp/src/Exception/ParserException.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -16,6 +17,32 @@ namespace ScssPhp\ScssPhp\Exception;
16
  *
17
  * @author Oleksandr Savchenko <traveltino@gmail.com>
18
  */
19
- class ParserException extends \Exception
20
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
17
  *
18
  * @author Oleksandr Savchenko <traveltino@gmail.com>
19
  */
20
+ class ParserException extends \Exception implements SassException
21
  {
22
+ /**
23
+ * @var array
24
+ */
25
+ private $sourcePosition;
26
+
27
+ /**
28
+ * Get source position
29
+ *
30
+ * @api
31
+ */
32
+ public function getSourcePosition()
33
+ {
34
+ return $this->sourcePosition;
35
+ }
36
+
37
+ /**
38
+ * Set source position
39
+ *
40
+ * @api
41
+ *
42
+ * @param array $sourcePosition
43
+ */
44
+ public function setSourcePosition($sourcePosition)
45
+ {
46
+ $this->sourcePosition = $sourcePosition;
47
+ }
48
  }
scssphp/src/Exception/RangeException.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -16,6 +17,6 @@ namespace ScssPhp\ScssPhp\Exception;
16
  *
17
  * @author Anthon Pang <anthon.pang@gmail.com>
18
  */
19
- class RangeException extends \Exception
20
  {
21
  }
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
19
  */
20
+ class RangeException extends \Exception implements SassException
21
  {
22
  }
scssphp/src/Exception/SassException.php ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace ScssPhp\ScssPhp\Exception;
4
+
5
+ interface SassException
6
+ {
7
+ }
scssphp/src/Exception/ServerException.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -16,6 +17,6 @@ namespace ScssPhp\ScssPhp\Exception;
16
  *
17
  * @author Anthon Pang <anthon.pang@gmail.com>
18
  */
19
- class ServerException extends \Exception
20
  {
21
  }
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
17
  *
18
  * @author Anthon Pang <anthon.pang@gmail.com>
19
  */
20
+ class ServerException extends \Exception implements SassException
21
  {
22
  }
scssphp/src/Formatter.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -81,6 +82,11 @@ abstract class Formatter
81
  */
82
  protected $sourceMapGenerator;
83
 
 
 
 
 
 
84
  /**
85
  * Initialize formatter
86
  *
@@ -114,21 +120,19 @@ abstract class Formatter
114
  }
115
 
116
  /**
117
- * Strip semi-colon appended by property(); it's a separator, not a terminator
 
118
  *
119
  * @api
120
  *
121
- * @param array $lines
 
 
 
122
  */
123
- public function stripSemicolon(&$lines)
124
  {
125
- if ($this->keepSemicolons) {
126
- return;
127
- }
128
-
129
- if (($count = count($lines)) && substr($lines[$count - 1], -1) === ';') {
130
- $lines[$count - 1] = substr($lines[$count - 1], 0, -1);
131
- }
132
  }
133
 
134
  /**
@@ -139,8 +143,7 @@ abstract class Formatter
139
  protected function blockLines(OutputBlock $block)
140
  {
141
  $inner = $this->indentStr();
142
-
143
- $glue = $this->break . $inner;
144
 
145
  $this->write($inner . implode($glue, $block->lines));
146
 
@@ -207,6 +210,10 @@ abstract class Formatter
207
  if (! empty($block->selectors)) {
208
  $this->indentLevel--;
209
 
 
 
 
 
210
  if (empty($block->children)) {
211
  $this->write($this->break);
212
  }
@@ -217,8 +224,10 @@ abstract class Formatter
217
 
218
  /**
219
  * Test and clean safely empty children
 
220
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
221
- * @return bool
 
222
  */
223
  protected function testEmptyChildren($block)
224
  {
@@ -228,14 +237,16 @@ abstract class Formatter
228
  foreach ($block->children as $k => &$child) {
229
  if (! $this->testEmptyChildren($child)) {
230
  $isEmpty = false;
231
- } else {
232
- if ($child->type === Type::T_MEDIA || $child->type === Type::T_DIRECTIVE) {
233
- $child->children = [];
234
- $child->selectors = null;
235
- }
 
236
  }
237
  }
238
  }
 
239
  return $isEmpty;
240
  }
241
 
@@ -254,8 +265,8 @@ abstract class Formatter
254
  $this->sourceMapGenerator = null;
255
 
256
  if ($sourceMapGenerator) {
257
- $this->currentLine = 1;
258
- $this->currentColumn = 0;
259
  $this->sourceMapGenerator = $sourceMapGenerator;
260
  }
261
 
@@ -271,10 +282,33 @@ abstract class Formatter
271
  }
272
 
273
  /**
 
 
274
  * @param string $str
275
  */
276
  protected function write($str)
277
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  if ($this->sourceMapGenerator) {
279
  $this->sourceMapGenerator->addMapping(
280
  $this->currentLine,
@@ -286,12 +320,12 @@ abstract class Formatter
286
  );
287
 
288
  $lines = explode("\n", $str);
289
- $lineCount = count($lines);
290
- $this->currentLine += $lineCount-1;
291
 
292
  $lastLine = array_pop($lines);
293
 
294
- $this->currentColumn = ($lineCount === 1 ? $this->currentColumn : 0) + strlen($lastLine);
295
  }
296
 
297
  echo $str;
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
82
  */
83
  protected $sourceMapGenerator;
84
 
85
+ /**
86
+ * @var string
87
+ */
88
+ protected $strippedSemicolon;
89
+
90
  /**
91
  * Initialize formatter
92
  *
120
  }
121
 
122
  /**
123
+ * Return custom property assignment
124
+ * differs in that you have to keep spaces in the value as is
125
  *
126
  * @api
127
  *
128
+ * @param string $name
129
+ * @param mixed $value
130
+ *
131
+ * @return string
132
  */
133
+ public function customProperty($name, $value)
134
  {
135
+ return rtrim($name) . trim($this->assignSeparator) . $value . ';';
 
 
 
 
 
 
136
  }
137
 
138
  /**
143
  protected function blockLines(OutputBlock $block)
144
  {
145
  $inner = $this->indentStr();
146
+ $glue = $this->break . $inner;
 
147
 
148
  $this->write($inner . implode($glue, $block->lines));
149
 
210
  if (! empty($block->selectors)) {
211
  $this->indentLevel--;
212
 
213
+ if (! $this->keepSemicolons) {
214
+ $this->strippedSemicolon = '';
215
+ }
216
+
217
  if (empty($block->children)) {
218
  $this->write($this->break);
219
  }
224
 
225
  /**
226
  * Test and clean safely empty children
227
+ *
228
  * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
229
+ *
230
+ * @return boolean
231
  */
232
  protected function testEmptyChildren($block)
233
  {
237
  foreach ($block->children as $k => &$child) {
238
  if (! $this->testEmptyChildren($child)) {
239
  $isEmpty = false;
240
+ continue;
241
+ }
242
+
243
+ if ($child->type === Type::T_MEDIA || $child->type === Type::T_DIRECTIVE) {
244
+ $child->children = [];
245
+ $child->selectors = null;
246
  }
247
  }
248
  }
249
+
250
  return $isEmpty;
251
  }
252
 
265
  $this->sourceMapGenerator = null;
266
 
267
  if ($sourceMapGenerator) {
268
+ $this->currentLine = 1;
269
+ $this->currentColumn = 0;
270
  $this->sourceMapGenerator = $sourceMapGenerator;
271
  }
272
 
282
  }
283
 
284
  /**
285
+ * Output content
286
+ *
287
  * @param string $str
288
  */
289
  protected function write($str)
290
  {
291
+ if (! empty($this->strippedSemicolon)) {
292
+ echo $this->strippedSemicolon;
293
+
294
+ $this->strippedSemicolon = '';
295
+ }
296
+
297
+ /*
298
+ * Maybe Strip semi-colon appended by property(); it's a separator, not a terminator
299
+ * will be striped for real before a closing, otherwise displayed unchanged starting the next write
300
+ */
301
+ if (
302
+ ! $this->keepSemicolons &&
303
+ $str &&
304
+ (strpos($str, ';') !== false) &&
305
+ (substr($str, -1) === ';')
306
+ ) {
307
+ $str = substr($str, 0, -1);
308
+
309
+ $this->strippedSemicolon = ';';
310
+ }
311
+
312
  if ($this->sourceMapGenerator) {
313
  $this->sourceMapGenerator->addMapping(
314
  $this->currentLine,
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;
scssphp/src/Formatter/Compact.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
scssphp/src/Formatter/Compressed.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
scssphp/src/Formatter/Crunched.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
scssphp/src/Formatter/Debug.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
scssphp/src/Formatter/Expanded.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -55,7 +56,7 @@ class Expanded extends Formatter
55
 
56
  foreach ($block->lines as $index => $line) {
57
  if (substr($line, 0, 2) === '/*') {
58
- $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line);
59
  }
60
  }
61
 
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
56
 
57
  foreach ($block->lines as $index => $line) {
58
  if (substr($line, 0, 2) === '/*') {
59
+ $block->lines[$index] = preg_replace('/\r\n?|\n|\f/', $this->break, $line);
60
  }
61
  }
62
 
scssphp/src/Formatter/Nested.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -58,29 +59,17 @@ class Nested extends Formatter
58
  protected function blockLines(OutputBlock $block)
59
  {
60
  $inner = $this->indentStr();
61
-
62
- $glue = $this->break . $inner;
63
 
64
  foreach ($block->lines as $index => $line) {
65
  if (substr($line, 0, 2) === '/*') {
66
- $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line);
67
  }
68
  }
69
 
70
  $this->write($inner . implode($glue, $block->lines));
71
  }
72
 
73
- protected function hasFlatChild($block)
74
- {
75
- foreach ($block->children as $child) {
76
- if (empty($child->selectors)) {
77
- return true;
78
- }
79
- }
80
-
81
- return false;
82
- }
83
-
84
  /**
85
  * {@inheritdoc}
86
  */
@@ -101,7 +90,7 @@ class Nested extends Formatter
101
  $previousHasSelector = false;
102
  }
103
 
104
- $isMediaOrDirective = in_array($block->type, [Type::T_DIRECTIVE, Type::T_MEDIA]);
105
  $isSupport = ($block->type === Type::T_DIRECTIVE
106
  && $block->selectors && strpos(implode('', $block->selectors), '@supports') !== false);
107
 
@@ -109,7 +98,8 @@ class Nested extends Formatter
109
  array_pop($depths);
110
  $this->depth--;
111
 
112
- if (!$this->depth && ($block->depth <= 1 || (!$this->indentLevel && $block->type === Type::T_COMMENT)) &&
 
113
  (($block->selectors && ! $isMediaOrDirective) || $previousHasSelector)
114
  ) {
115
  $downLevel = $this->break;
@@ -130,10 +120,12 @@ class Nested extends Formatter
130
  if ($block->depth > end($depths)) {
131
  if (! $previousEmpty || $this->depth < 1) {
132
  $this->depth++;
 
133
  $depths[] = $block->depth;
134
  } else {
135
  // keep the current depth unchanged but take the block depth as a new reference for following blocks
136
  array_pop($depths);
 
137
  $depths[] = $block->depth;
138
  }
139
  }
@@ -170,15 +162,18 @@ class Nested extends Formatter
170
  }
171
 
172
  $this->blockLines($block);
 
173
  $closeBlock = $this->break;
174
  }
175
 
176
  if (! empty($block->children)) {
177
- if ($this->depth>0 && ($isMediaOrDirective || ! $this->hasFlatChild($block))) {
178
  array_pop($depths);
 
179
  $this->depth--;
180
  $this->blockChildren($block);
181
  $this->depth++;
 
182
  $depths[] = $block->depth;
183
  } else {
184
  $this->blockChildren($block);
@@ -193,13 +188,19 @@ class Nested extends Formatter
193
  if (! empty($block->selectors)) {
194
  $this->indentLevel--;
195
 
 
 
 
 
196
  $this->write($this->close);
 
197
  $closeBlock = $this->break;
198
 
199
  if ($this->depth > 1 && ! empty($block->children)) {
200
  array_pop($depths);
201
  $this->depth--;
202
  }
 
203
  if (! $isMediaOrDirective) {
204
  $previousHasSelector = true;
205
  }
@@ -209,4 +210,22 @@ class Nested extends Formatter
209
  $this->write($this->break);
210
  }
211
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  }
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
59
  protected function blockLines(OutputBlock $block)
60
  {
61
  $inner = $this->indentStr();
62
+ $glue = $this->break . $inner;
 
63
 
64
  foreach ($block->lines as $index => $line) {
65
  if (substr($line, 0, 2) === '/*') {
66
+ $block->lines[$index] = preg_replace('/\r\n?|\n|\f/', $this->break, $line);
67
  }
68
  }
69
 
70
  $this->write($inner . implode($glue, $block->lines));
71
  }
72
 
 
 
 
 
 
 
 
 
 
 
 
73
  /**
74
  * {@inheritdoc}
75
  */
90
  $previousHasSelector = false;
91
  }
92
 
93
+ $isMediaOrDirective = \in_array($block->type, [Type::T_DIRECTIVE, Type::T_MEDIA]);
94
  $isSupport = ($block->type === Type::T_DIRECTIVE
95
  && $block->selectors && strpos(implode('', $block->selectors), '@supports') !== false);
96
 
98
  array_pop($depths);
99
  $this->depth--;
100
 
101
+ if (
102
+ ! $this->depth && ($block->depth <= 1 || (! $this->indentLevel && $block->type === Type::T_COMMENT)) &&
103
  (($block->selectors && ! $isMediaOrDirective) || $previousHasSelector)
104
  ) {
105
  $downLevel = $this->break;
120
  if ($block->depth > end($depths)) {
121
  if (! $previousEmpty || $this->depth < 1) {
122
  $this->depth++;
123
+
124
  $depths[] = $block->depth;
125
  } else {
126
  // keep the current depth unchanged but take the block depth as a new reference for following blocks
127
  array_pop($depths);
128
+
129
  $depths[] = $block->depth;
130
  }
131
  }
162
  }
163
 
164
  $this->blockLines($block);
165
+
166
  $closeBlock = $this->break;
167
  }
168
 
169
  if (! empty($block->children)) {
170
+ if ($this->depth > 0 && ($isMediaOrDirective || ! $this->hasFlatChild($block))) {
171
  array_pop($depths);
172
+
173
  $this->depth--;
174
  $this->blockChildren($block);
175
  $this->depth++;
176
+
177
  $depths[] = $block->depth;
178
  } else {
179
  $this->blockChildren($block);
188
  if (! empty($block->selectors)) {
189
  $this->indentLevel--;
190
 
191
+ if (! $this->keepSemicolons) {
192
+ $this->strippedSemicolon = '';
193
+ }
194
+
195
  $this->write($this->close);
196
+
197
  $closeBlock = $this->break;
198
 
199
  if ($this->depth > 1 && ! empty($block->children)) {
200
  array_pop($depths);
201
  $this->depth--;
202
  }
203
+
204
  if (! $isMediaOrDirective) {
205
  $previousHasSelector = true;
206
  }
210
  $this->write($this->break);
211
  }
212
  }
213
+
214
+ /**
215
+ * Block has flat child
216
+ *
217
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
218
+ *
219
+ * @return boolean
220
+ */
221
+ private function hasFlatChild($block)
222
+ {
223
+ foreach ($block->children as $child) {
224
+ if (empty($child->selectors)) {
225
+ return true;
226
+ }
227
+ }
228
+
229
+ return false;
230
+ }
231
  }
scssphp/src/Formatter/OutputBlock.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
scssphp/src/Node.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
scssphp/src/Node/Number.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -28,17 +29,20 @@ use ScssPhp\ScssPhp\Type;
28
  */
29
  class Number extends Node implements \ArrayAccess
30
  {
 
 
31
  /**
32
  * @var integer
 
33
  */
34
- static public $precision = 10;
35
 
36
  /**
37
  * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/
38
  *
39
  * @var array
40
  */
41
- static protected $unitTable = [
42
  'in' => [
43
  'in' => 1,
44
  'pc' => 6,
@@ -64,8 +68,8 @@ class Number extends Node implements \ArrayAccess
64
  ],
65
  'dpi' => [
66
  'dpi' => 1,
67
- 'dpcm' => 2.54,
68
- 'dppx' => 96,
69
  ],
70
  ];
71
 
@@ -89,7 +93,7 @@ class Number extends Node implements \ArrayAccess
89
  {
90
  $this->type = Type::T_NUMBER;
91
  $this->dimension = $dimension;
92
- $this->units = is_array($initialUnit)
93
  ? $initialUnit
94
  : ($initialUnit ? [$initialUnit => 1]
95
  : []);
@@ -110,11 +114,18 @@ class Number extends Node implements \ArrayAccess
110
 
111
  $dimension = $this->dimension;
112
 
113
- foreach (static::$unitTable['in'] as $unit => $conv) {
114
- $from = isset($this->units[$unit]) ? $this->units[$unit] : 0;
115
- $to = isset($units[$unit]) ? $units[$unit] : 0;
116
- $factor = pow($conv, $from - $to);
117
- $dimension /= $factor;
 
 
 
 
 
 
 
118
  }
119
 
120
  return new Number($dimension, $units);
@@ -130,7 +141,7 @@ class Number extends Node implements \ArrayAccess
130
  $dimension = $this->dimension;
131
  $units = [];
132
 
133
- $this->normalizeUnits($dimension, $units, 'in');
134
 
135
  return new Number($dimension, $units);
136
  }
@@ -141,14 +152,15 @@ class Number extends Node implements \ArrayAccess
141
  public function offsetExists($offset)
142
  {
143
  if ($offset === -3) {
144
- return $this->sourceColumn !== null;
145
  }
146
 
147
  if ($offset === -2) {
148
- return $this->sourceLine !== null;
149
  }
150
 
151
- if ($offset === -1 ||
 
152
  $offset === 0 ||
153
  $offset === 1 ||
154
  $offset === 2
@@ -231,6 +243,35 @@ class Number extends Node implements \ArrayAccess
231
  return ! array_sum($this->units);
232
  }
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  /**
235
  * Returns unit(s) as the product of numerator units divided by the product of denominator units
236
  *
@@ -243,17 +284,17 @@ class Number extends Node implements \ArrayAccess
243
 
244
  foreach ($this->units as $unit => $unitSize) {
245
  if ($unitSize > 0) {
246
- $numerators = array_pad($numerators, count($numerators) + $unitSize, $unit);
247
  continue;
248
  }
249
 
250
  if ($unitSize < 0) {
251
- $denominators = array_pad($denominators, count($denominators) + $unitSize, $unit);
252
  continue;
253
  }
254
  }
255
 
256
- return implode('*', $numerators) . (count($denominators) ? '/' . implode('*', $denominators) : '');
257
  }
258
 
259
  /**
@@ -265,19 +306,19 @@ class Number extends Node implements \ArrayAccess
265
  */
266
  public function output(Compiler $compiler = null)
267
  {
268
- $dimension = round($this->dimension, static::$precision);
269
 
270
  $units = array_filter($this->units, function ($unitSize) {
271
  return $unitSize;
272
  });
273
 
274
- if (count($units) > 1 && array_sum($units) === 0) {
275
  $dimension = $this->dimension;
276
  $units = [];
277
 
278
- $this->normalizeUnits($dimension, $units, 'in');
279
 
280
- $dimension = round($dimension, static::$precision);
281
  $units = array_filter($units, function ($unitSize) {
282
  return $unitSize;
283
  });
@@ -285,15 +326,17 @@ class Number extends Node implements \ArrayAccess
285
 
286
  $unitSize = array_sum($units);
287
 
288
- if ($compiler && ($unitSize > 1 || $unitSize < 0 || count($units) > 1)) {
289
- $compiler->throwError((string) $dimension . $this->unitStr() . " isn't a valid CSS value.");
 
 
 
 
290
  }
291
 
292
- reset($units);
293
- $unit = key($units);
294
- $dimension = number_format($dimension, static::$precision, '.', '');
295
 
296
- return (static::$precision ? rtrim(rtrim($dimension, '0'), '.') : $dimension) . $unit;
297
  }
298
 
299
  /**
@@ -311,13 +354,17 @@ class Number extends Node implements \ArrayAccess
311
  * @param array $units
312
  * @param string $baseUnit
313
  */
314
- private function normalizeUnits(&$dimension, &$units, $baseUnit = 'in')
315
  {
316
  $dimension = $this->dimension;
317
  $units = [];
318
 
319
  foreach ($this->units as $unit => $exp) {
320
- if (isset(static::$unitTable[$baseUnit][$unit])) {
 
 
 
 
321
  $factor = pow(static::$unitTable[$baseUnit][$unit], $exp);
322
 
323
  $unit = $baseUnit;
@@ -327,4 +374,22 @@ class Number extends Node implements \ArrayAccess
327
  $units[$unit] = $exp + (isset($units[$unit]) ? $units[$unit] : 0);
328
  }
329
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  }
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
29
  */
30
  class Number extends Node implements \ArrayAccess
31
  {
32
+ const PRECISION = 10;
33
+
34
  /**
35
  * @var integer
36
+ * @deprecated use {Number::PRECISION} instead to read the precision. Configuring it is not supported anymore.
37
  */
38
+ public static $precision = self::PRECISION;
39
 
40
  /**
41
  * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/
42
  *
43
  * @var array
44
  */
45
+ protected static $unitTable = [
46
  'in' => [
47
  'in' => 1,
48
  'pc' => 6,
68
  ],
69
  'dpi' => [
70
  'dpi' => 1,
71
+ 'dpcm' => 1 / 2.54,
72
+ 'dppx' => 1 / 96,
73
  ],
74
  ];
75
 
93
  {
94
  $this->type = Type::T_NUMBER;
95
  $this->dimension = $dimension;
96
+ $this->units = \is_array($initialUnit)
97
  ? $initialUnit
98
  : ($initialUnit ? [$initialUnit => 1]
99
  : []);
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);
141
  $dimension = $this->dimension;
142
  $units = [];
143
 
144
+ $this->normalizeUnits($dimension, $units);
145
 
146
  return new Number($dimension, $units);
147
  }
152
  public function offsetExists($offset)
153
  {
154
  if ($offset === -3) {
155
+ return ! \is_null($this->sourceColumn);
156
  }
157
 
158
  if ($offset === -2) {
159
+ return ! \is_null($this->sourceLine);
160
  }
161
 
162
+ if (
163
+ $offset === -1 ||
164
  $offset === 0 ||
165
  $offset === 1 ||
166
  $offset === 2
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
  *
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
  /**
306
  */
307
  public function output(Compiler $compiler = null)
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
  });
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
  /**
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;
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
  }
scssphp/src/Parser.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -65,12 +66,15 @@ class Parser
65
  private $inParens;
66
  private $eatWhiteDefault;
67
  private $discardComments;
 
68
  private $buffer;
69
  private $utf8;
70
  private $encoding;
71
  private $patternModifiers;
72
  private $commentsSeen;
73
 
 
 
74
  /**
75
  * Constructor
76
  *
@@ -81,7 +85,7 @@ class Parser
81
  * @param string $encoding
82
  * @param \ScssPhp\ScssPhp\Cache $cache
83
  */
84
- public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', $cache = null)
85
  {
86
  $this->sourceName = $sourceName ?: '(stdin)';
87
  $this->sourceIndex = $sourceIndex;
@@ -89,7 +93,9 @@ class Parser
89
  $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8';
90
  $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';
91
  $this->commentsSeen = [];
92
- $this->discardComments = false;
 
 
93
 
94
  if (empty(static::$operatorPattern)) {
95
  static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
@@ -138,11 +144,21 @@ class Parser
138
  ? "line: $line, column: $column"
139
  : "$this->sourceName on line $line, at column $column";
140
 
141
- if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
142
- throw new ParserException("$msg: failed at `$m[1]` $loc");
 
 
 
 
 
143
  }
144
 
145
- throw new ParserException("$msg: $loc");
 
 
 
 
 
146
  }
147
 
148
  /**
@@ -157,14 +173,14 @@ class Parser
157
  public function parse($buffer)
158
  {
159
  if ($this->cache) {
160
- $cacheKey = $this->sourceName . ":" . md5($buffer);
161
  $parseOptions = [
162
  'charset' => $this->charset,
163
  'utf8' => $this->utf8,
164
  ];
165
- $v = $this->cache->getCache("parse", $cacheKey, $parseOptions);
166
 
167
- if (! is_null($v)) {
168
  return $v;
169
  }
170
  }
@@ -192,7 +208,7 @@ class Parser
192
  ;
193
  }
194
 
195
- if ($this->count !== strlen($this->buffer)) {
196
  $this->throwParseError();
197
  }
198
 
@@ -207,7 +223,7 @@ class Parser
207
  $this->restoreEncoding();
208
 
209
  if ($this->cache) {
210
- $this->cache->setCache("parse", $cacheKey, $this->env, $parseOptions);
211
  }
212
 
213
  return $this->env;
@@ -218,8 +234,8 @@ class Parser
218
  *
219
  * @api
220
  *
221
- * @param string $buffer
222
- * @param string $out
223
  *
224
  * @return boolean
225
  */
@@ -245,8 +261,8 @@ class Parser
245
  *
246
  * @api
247
  *
248
- * @param string $buffer
249
- * @param string $out
250
  *
251
  * @return boolean
252
  */
@@ -272,10 +288,10 @@ class Parser
272
  *
273
  * @api
274
  *
275
- * @param string $buffer
276
- * @param string $out
277
  *
278
- * @return array
279
  */
280
  public function parseMediaQueryList($buffer, &$out)
281
  {
@@ -287,7 +303,6 @@ class Parser
287
 
288
  $this->saveEncoding();
289
 
290
-
291
  $isMediaQuery = $this->mediaQueryList($out);
292
 
293
  $this->restoreEncoding();
@@ -340,21 +355,31 @@ class Parser
340
 
341
  // the directives
342
  if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
343
- if ($this->literal('@at-root', 8) &&
 
344
  ($this->selectors($selector) || true) &&
345
  ($this->map($with) || true) &&
 
 
 
346
  $this->matchChar('{', false)
347
  ) {
 
 
348
  $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
349
  $atRoot->selector = $selector;
350
- $atRoot->with = $with;
351
 
352
  return true;
353
  }
354
 
355
  $this->seek($s);
356
 
357
- if ($this->literal('@media', 6) && $this->mediaQueryList($mediaQueryList) && $this->matchChar('{', false)) {
 
 
 
 
358
  $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
359
  $media->queryList = $mediaQueryList[2];
360
 
@@ -363,11 +388,14 @@ class Parser
363
 
364
  $this->seek($s);
365
 
366
- if ($this->literal('@mixin', 6) &&
 
367
  $this->keyword($mixinName) &&
368
  ($this->argumentDef($args) || true) &&
369
  $this->matchChar('{', false)
370
  ) {
 
 
371
  $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
372
  $mixin->name = $mixinName;
373
  $mixin->args = $args;
@@ -377,15 +405,27 @@ class Parser
377
 
378
  $this->seek($s);
379
 
380
- if ($this->literal('@include', 8) &&
381
- $this->keyword($mixinName) &&
382
- ($this->matchChar('(') &&
 
383
  ($this->argValues($argValues) || true) &&
384
  $this->matchChar(')') || true) &&
385
- ($this->end() ||
386
- $this->matchChar('{') && $hasBlock = true)
 
 
 
387
  ) {
388
- $child = [Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null];
 
 
 
 
 
 
 
 
389
 
390
  if (! empty($hasBlock)) {
391
  $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
@@ -399,10 +439,13 @@ class Parser
399
 
400
  $this->seek($s);
401
 
402
- if ($this->literal('@scssphp-import-once', 20) &&
 
403
  $this->valueList($importPath) &&
404
  $this->end()
405
  ) {
 
 
406
  $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s);
407
 
408
  return true;
@@ -410,10 +453,18 @@ class Parser
410
 
411
  $this->seek($s);
412
 
413
- if ($this->literal('@import', 7) &&
 
414
  $this->valueList($importPath) &&
 
415
  $this->end()
416
  ) {
 
 
 
 
 
 
417
  $this->append([Type::T_IMPORT, $importPath], $s);
418
 
419
  return true;
@@ -421,10 +472,17 @@ class Parser
421
 
422
  $this->seek($s);
423
 
424
- if ($this->literal('@import', 7) &&
 
425
  $this->url($importPath) &&
426
  $this->end()
427
  ) {
 
 
 
 
 
 
428
  $this->append([Type::T_IMPORT, $importPath], $s);
429
 
430
  return true;
@@ -432,10 +490,13 @@ class Parser
432
 
433
  $this->seek($s);
434
 
435
- if ($this->literal('@extend', 7) &&
 
436
  $this->selectors($selectors) &&
437
  $this->end()
438
  ) {
 
 
439
  // check for '!flag'
440
  $optional = $this->stripOptionalFlag($selectors);
441
  $this->append([Type::T_EXTEND, $selectors, $optional], $s);
@@ -445,11 +506,14 @@ class Parser
445
 
446
  $this->seek($s);
447
 
448
- if ($this->literal('@function', 9) &&
 
449
  $this->keyword($fnName) &&
450
  $this->argumentDef($args) &&
451
  $this->matchChar('{', false)
452
  ) {
 
 
453
  $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
454
  $func->name = $fnName;
455
  $func->args = $args;
@@ -459,7 +523,12 @@ class Parser
459
 
460
  $this->seek($s);
461
 
462
- if ($this->literal('@break', 6) && $this->end()) {
 
 
 
 
 
463
  $this->append([Type::T_BREAK], $s);
464
 
465
  return true;
@@ -467,7 +536,12 @@ class Parser
467
 
468
  $this->seek($s);
469
 
470
- if ($this->literal('@continue', 9) && $this->end()) {
 
 
 
 
 
471
  $this->append([Type::T_CONTINUE], $s);
472
 
473
  return true;
@@ -475,7 +549,13 @@ class Parser
475
 
476
  $this->seek($s);
477
 
478
- if ($this->literal('@return', 7) && ($this->valueList($retVal) || true) && $this->end()) {
 
 
 
 
 
 
479
  $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
480
 
481
  return true;
@@ -483,12 +563,15 @@ class Parser
483
 
484
  $this->seek($s);
485
 
486
- if ($this->literal('@each', 5) &&
 
487
  $this->genericList($varNames, 'variable', ',', false) &&
488
  $this->literal('in', 2) &&
489
  $this->valueList($list) &&
490
  $this->matchChar('{', false)
491
  ) {
 
 
492
  $each = $this->pushSpecialBlock(Type::T_EACH, $s);
493
 
494
  foreach ($varNames[2] as $varName) {
@@ -502,10 +585,22 @@ class Parser
502
 
503
  $this->seek($s);
504
 
505
- if ($this->literal('@while', 6) &&
 
506
  $this->expression($cond) &&
507
  $this->matchChar('{', false)
508
  ) {
 
 
 
 
 
 
 
 
 
 
 
509
  $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
510
  $while->cond = $cond;
511
 
@@ -514,7 +609,8 @@ class Parser
514
 
515
  $this->seek($s);
516
 
517
- if ($this->literal('@for', 4) &&
 
518
  $this->variable($varName) &&
519
  $this->literal('from', 4) &&
520
  $this->expression($start) &&
@@ -523,10 +619,12 @@ class Parser
523
  $this->expression($end) &&
524
  $this->matchChar('{', false)
525
  ) {
 
 
526
  $for = $this->pushSpecialBlock(Type::T_FOR, $s);
527
- $for->var = $varName[1];
528
  $for->start = $start;
529
- $for->end = $end;
530
  $for->until = isset($forUntil);
531
 
532
  return true;
@@ -534,9 +632,24 @@ class Parser
534
 
535
  $this->seek($s);
536
 
537
- if ($this->literal('@if', 3) && $this->valueList($cond) && $this->matchChar('{', false)) {
 
 
 
 
 
538
  $if = $this->pushSpecialBlock(Type::T_IF, $s);
539
- $if->cond = $cond;
 
 
 
 
 
 
 
 
 
 
540
  $if->cases = [];
541
 
542
  return true;
@@ -544,10 +657,12 @@ class Parser
544
 
545
  $this->seek($s);
546
 
547
- if ($this->literal('@debug', 6) &&
548
- $this->valueList($value) &&
549
- $this->end()
550
  ) {
 
 
551
  $this->append([Type::T_DEBUG, $value], $s);
552
 
553
  return true;
@@ -555,10 +670,12 @@ class Parser
555
 
556
  $this->seek($s);
557
 
558
- if ($this->literal('@warn', 5) &&
559
- $this->valueList($value) &&
560
- $this->end()
561
  ) {
 
 
562
  $this->append([Type::T_WARN, $value], $s);
563
 
564
  return true;
@@ -566,10 +683,12 @@ class Parser
566
 
567
  $this->seek($s);
568
 
569
- if ($this->literal('@error', 6) &&
570
- $this->valueList($value) &&
571
- $this->end()
572
  ) {
 
 
573
  $this->append([Type::T_ERROR, $value], $s);
574
 
575
  return true;
@@ -577,8 +696,17 @@ class Parser
577
 
578
  $this->seek($s);
579
 
580
- if ($this->literal('@content', 8) && $this->end()) {
581
- $this->append([Type::T_MIXIN_CONTENT], $s);
 
 
 
 
 
 
 
 
 
582
 
583
  return true;
584
  }
@@ -593,7 +721,11 @@ class Parser
593
  if ($this->literal('@else', 5)) {
594
  if ($this->matchChar('{', false)) {
595
  $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
596
- } elseif ($this->literal('if', 2) && $this->valueList($cond) && $this->matchChar('{', false)) {
 
 
 
 
597
  $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
598
  $else->cond = $cond;
599
  }
@@ -610,7 +742,8 @@ class Parser
610
  }
611
 
612
  // only retain the first @charset directive encountered
613
- if ($this->literal('@charset', 8) &&
 
614
  $this->valueList($charset) &&
615
  $this->end()
616
  ) {
@@ -631,11 +764,13 @@ class Parser
631
 
632
  $this->seek($s);
633
 
634
- if ($this->literal('@supports', 9) &&
635
- ($t1=$this->supportsQuery($supportQuery)) &&
636
- ($t2=$this->matchChar('{', false)) ) {
 
 
637
  $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
638
- $directive->name = 'supports';
639
  $directive->value = $supportQuery;
640
 
641
  return true;
@@ -644,11 +779,16 @@ class Parser
644
  $this->seek($s);
645
 
646
  // doesn't match built in directive, do generic one
647
- if ($this->matchChar('@', false) &&
648
- $this->keyword($dirName) &&
649
- ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
650
- $this->matchChar('{', false)
651
  ) {
 
 
 
 
 
652
  if ($dirName === 'media') {
653
  $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
654
  } else {
@@ -657,6 +797,7 @@ class Parser
657
  }
658
 
659
  if (isset($dirValue)) {
 
660
  $directive->value = $dirValue;
661
  }
662
 
@@ -665,12 +806,102 @@ class Parser
665
 
666
  $this->seek($s);
667
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
668
  return false;
669
  }
670
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
671
  // property shortcut
672
  // captures most properties before having to parse a selector
673
- if ($this->keyword($name, false) &&
 
674
  $this->literal(': ', 2) &&
675
  $this->valueList($value) &&
676
  $this->end()
@@ -684,11 +915,14 @@ class Parser
684
  $this->seek($s);
685
 
686
  // variable assigns
687
- if ($this->variable($name) &&
 
688
  $this->matchChar(':') &&
689
  $this->valueList($value) &&
690
  $this->end()
691
  ) {
 
 
692
  // check for '!flag'
693
  $assignmentFlags = $this->stripAssignmentFlags($value);
694
  $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s);
@@ -704,12 +938,17 @@ class Parser
704
  }
705
 
706
  // opening css block
707
- if ($this->selectors($selectors) && $this->matchChar('{', false)) {
 
 
 
 
 
708
  $this->pushBlock($selectors, $s);
709
 
710
  if ($this->eatWhiteDefault) {
711
  $this->whitespace();
712
- $this->append(null); // collect comments at the begining if needed
713
  }
714
 
715
  return true;
@@ -718,7 +957,10 @@ class Parser
718
  $this->seek($s);
719
 
720
  // property assign, or nested assign
721
- if ($this->propertyName($name) && $this->matchChar(':')) {
 
 
 
722
  $foundSomething = false;
723
 
724
  if ($this->valueList($value)) {
@@ -731,6 +973,8 @@ class Parser
731
  }
732
 
733
  if ($this->matchChar('{', false)) {
 
 
734
  $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
735
  $propBlock->prefix = $name;
736
  $propBlock->hasValue = $foundSomething;
@@ -751,7 +995,7 @@ class Parser
751
  if ($this->matchChar('}', false)) {
752
  $block = $this->popBlock();
753
 
754
- if (!isset($block->type) || $block->type !== Type::T_IF) {
755
  if ($this->env->parent) {
756
  $this->append(null); // collect comments before next statement if needed
757
  }
@@ -780,7 +1024,8 @@ class Parser
780
  }
781
 
782
  // extra stuff
783
- if ($this->matchChar(';') ||
 
784
  $this->literal('<!--', 4)
785
  ) {
786
  return true;
@@ -801,7 +1046,7 @@ class Parser
801
  {
802
  list($line, $column) = $this->getSourcePosition($pos);
803
 
804
- $b = new Block;
805
  $b->sourceName = $this->sourceName;
806
  $b->sourceLine = $line;
807
  $b->sourceColumn = $column;
@@ -823,7 +1068,7 @@ class Parser
823
 
824
  $this->env = $b;
825
 
826
- // collect comments at the begining of a block if needed
827
  if ($this->eatWhiteDefault) {
828
  $this->whitespace();
829
 
@@ -916,13 +1161,187 @@ class Parser
916
  $this->count = $where;
917
  }
918
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
919
  /**
920
  * Match string looking for either ending delim, escape, or string interpolation
921
  *
922
  * {@internal This is a workaround for preg_match's 250K string match limit. }}
923
  *
924
  * @param array $m Matches (passed by reference)
925
- * @param string $delim Delimeter
926
  *
927
  * @return boolean True if match; false otherwise
928
  */
@@ -930,10 +1349,10 @@ class Parser
930
  {
931
  $token = null;
932
 
933
- $end = strlen($this->buffer);
934
 
935
  // look for either ending delim, escape, or string interpolation
936
- foreach (['#{', '\\', $delim] as $lookahead) {
937
  $pos = strpos($this->buffer, $lookahead, $this->count);
938
 
939
  if ($pos !== false && $pos < $end) {
@@ -952,7 +1371,7 @@ class Parser
952
  $match,
953
  $token
954
  ];
955
- $this->count = $end + strlen($token);
956
 
957
  return true;
958
  }
@@ -974,7 +1393,7 @@ class Parser
974
  return false;
975
  }
976
 
977
- $this->count += strlen($out[0]);
978
 
979
  if (! isset($eatWhitespace)) {
980
  $eatWhitespace = $this->eatWhiteDefault;
@@ -1055,28 +1474,29 @@ class Parser
1055
  if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
1056
  // comment that are kept in the output CSS
1057
  $comment = [];
1058
- $endCommentCount = $this->count + strlen($m[1]);
 
1059
 
1060
  // find interpolations in comment
1061
  $p = strpos($this->buffer, '#{', $this->count);
1062
 
1063
  while ($p !== false && $p < $endCommentCount) {
1064
- $c = substr($this->buffer, $this->count, $p - $this->count);
1065
- $comment[] = $c;
1066
  $this->count = $p;
1067
- $out = null;
1068
 
1069
  if ($this->interpolation($out)) {
1070
  // keep right spaces in the following string part
1071
  if ($out[3]) {
1072
- while ($this->buffer[$this->count-1] !== '}') {
1073
  $this->count--;
1074
  }
1075
 
1076
  $out[3] = '';
1077
  }
1078
 
1079
- $comment[] = $out;
1080
  } else {
1081
  $comment[] = substr($this->buffer, $this->count, 2);
1082
 
@@ -1094,14 +1514,19 @@ class Parser
1094
  $this->appendComment([Type::T_COMMENT, $c]);
1095
  } else {
1096
  $comment[] = $c;
1097
- $this->appendComment([Type::T_COMMENT, [Type::T_STRING, '', $comment]]);
 
1098
  }
1099
 
1100
- $this->commentsSeen[$this->count] = true;
1101
  $this->count = $endCommentCount;
1102
  } else {
1103
  // comment that are ignored and not kept in the output css
1104
- $this->count += strlen($m[0]);
 
 
 
 
1105
  }
1106
 
1107
  $gotWhite = true;
@@ -1118,10 +1543,6 @@ class Parser
1118
  protected function appendComment($comment)
1119
  {
1120
  if (! $this->discardComments) {
1121
- if ($comment[0] === Type::T_COMMENT && is_string($comment[1])) {
1122
- $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
1123
- }
1124
-
1125
  $this->env->comments[] = $comment;
1126
  }
1127
  }
@@ -1134,8 +1555,10 @@ class Parser
1134
  */
1135
  protected function append($statement, $pos = null)
1136
  {
1137
- if (! is_null($statement)) {
1138
- if ($pos !== null) {
 
 
1139
  list($line, $column) = $this->getSourcePosition($pos);
1140
 
1141
  $statement[static::SOURCE_LINE] = $line;
@@ -1161,7 +1584,7 @@ class Parser
1161
  */
1162
  protected function last()
1163
  {
1164
- $i = count($this->env->children) - 1;
1165
 
1166
  if (isset($this->env->children[$i])) {
1167
  return $this->env->children[$i];
@@ -1192,7 +1615,9 @@ class Parser
1192
  $expressions = null;
1193
  $parts = [];
1194
 
1195
- if (($this->literal('only', 4) && ($only = true) || $this->literal('not', 3) && ($not = true) || true) &&
 
 
1196
  $this->mixedKeyword($mediaType)
1197
  ) {
1198
  $prop = [Type::T_MEDIA_TYPE];
@@ -1208,7 +1633,7 @@ class Parser
1208
  $media = [Type::T_LIST, '', []];
1209
 
1210
  foreach ((array) $mediaType as $type) {
1211
- if (is_array($type)) {
1212
  $media[2][] = $type;
1213
  } else {
1214
  $media[2][] = [Type::T_KEYWORD, $type];
@@ -1222,7 +1647,7 @@ class Parser
1222
  if (empty($parts) || $this->literal('and', 3)) {
1223
  $this->genericList($expressions, 'mediaExpression', 'and', false);
1224
 
1225
- if (is_array($expressions)) {
1226
  $parts = array_merge($parts, $expressions[2]);
1227
  }
1228
  }
@@ -1247,12 +1672,15 @@ class Parser
1247
  $s = $this->count;
1248
 
1249
  $not = false;
1250
- if (($this->literal('not', 3) && ($not = true) || true) &&
 
 
1251
  $this->matchChar('(') &&
1252
  ($this->expression($property)) &&
1253
  $this->literal(': ', 2) &&
1254
  $this->valueList($value) &&
1255
- $this->matchChar(')')) {
 
1256
  $support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]];
1257
  $support[2][] = $property;
1258
  $support[2][] = [Type::T_KEYWORD, ': '];
@@ -1265,40 +1693,50 @@ class Parser
1265
  $this->seek($s);
1266
  }
1267
 
1268
- if ($this->matchChar('(') &&
 
1269
  $this->supportsQuery($subQuery) &&
1270
- $this->matchChar(')')) {
 
1271
  $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, '('], $subQuery, [Type::T_KEYWORD, ')']]];
1272
  $s = $this->count;
1273
  } else {
1274
  $this->seek($s);
1275
  }
1276
 
1277
- if ($this->literal('not', 3) &&
1278
- $this->supportsQuery($subQuery)) {
 
 
1279
  $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]];
1280
  $s = $this->count;
1281
  } else {
1282
  $this->seek($s);
1283
  }
1284
 
1285
- if ($this->literal('selector(', 9) &&
 
1286
  $this->selector($selector) &&
1287
- $this->matchChar(')')) {
 
1288
  $support = [Type::T_STRING, '', [[Type::T_KEYWORD, 'selector(']]];
1289
 
1290
  $selectorList = [Type::T_LIST, '', []];
 
1291
  foreach ($selector as $sc) {
1292
  $compound = [Type::T_STRING, '', []];
 
1293
  foreach ($sc as $scp) {
1294
- if (is_array($scp)) {
1295
  $compound[2][] = $scp;
1296
  } else {
1297
  $compound[2][] = [Type::T_KEYWORD, $scp];
1298
  }
1299
  }
 
1300
  $selectorList[2][] = $compound;
1301
  }
 
1302
  $support[2][] = $selectorList;
1303
  $support[2][] = [Type::T_KEYWORD, ')'];
1304
  $parts[] = $support;
@@ -1314,29 +1752,37 @@ class Parser
1314
  $this->seek($s);
1315
  }
1316
 
1317
- if ($this->literal('and', 3) &&
1318
- $this->genericList($expressions, 'supportsQuery', ' and', false)) {
 
 
1319
  array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
 
1320
  $parts = [$expressions];
1321
  $s = $this->count;
1322
  } else {
1323
  $this->seek($s);
1324
  }
1325
 
1326
- if ($this->literal('or', 2) &&
1327
- $this->genericList($expressions, 'supportsQuery', ' or', false)) {
 
 
1328
  array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
 
1329
  $parts = [$expressions];
1330
  $s = $this->count;
1331
  } else {
1332
  $this->seek($s);
1333
  }
1334
 
1335
- if (count($parts)) {
1336
  if ($this->eatWhiteDefault) {
1337
  $this->whitespace();
1338
  }
 
1339
  $out = [Type::T_STRING, '', $parts];
 
1340
  return true;
1341
  }
1342
 
@@ -1356,9 +1802,11 @@ class Parser
1356
  $s = $this->count;
1357
  $value = null;
1358
 
1359
- if ($this->matchChar('(') &&
 
1360
  $this->expression($feature) &&
1361
- ($this->matchChar(':') && $this->expression($value) || true) &&
 
1362
  $this->matchChar(')')
1363
  ) {
1364
  $out = [Type::T_MEDIA_EXPRESSION, $feature];
@@ -1384,12 +1832,19 @@ class Parser
1384
  */
1385
  protected function argValues(&$out)
1386
  {
 
 
 
1387
  if ($this->genericList($list, 'argValue', ',', false)) {
1388
  $out = $list[2];
1389
 
 
 
1390
  return true;
1391
  }
1392
 
 
 
1393
  return false;
1394
  }
1395
 
@@ -1408,10 +1863,11 @@ class Parser
1408
 
1409
  if (! $this->variable($keyword) || ! $this->matchChar(':')) {
1410
  $this->seek($s);
 
1411
  $keyword = null;
1412
  }
1413
 
1414
- if ($this->genericList($value, 'expression')) {
1415
  $out = [$keyword, $value, false];
1416
  $s = $this->count;
1417
 
@@ -1428,113 +1884,345 @@ class Parser
1428
  }
1429
 
1430
  /**
1431
- * Parse comma separated value list
1432
- *
1433
- * @param array $out
1434
- *
1435
- * @return boolean
1436
- */
1437
- protected function valueList(&$out)
1438
- {
1439
- return $this->genericList($out, 'spaceList', ',');
1440
- }
1441
-
1442
- /**
1443
- * Parse space separated value list
1444
- *
1445
- * @param array $out
1446
- *
1447
- * @return boolean
1448
  */
1449
- protected function spaceList(&$out)
1450
  {
1451
- return $this->genericList($out, 'expression');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1452
  }
1453
 
1454
  /**
1455
- * Parse generic list
1456
  *
1457
- * @param array $out
1458
- * @param callable $parseItem
1459
- * @param string $delim
1460
- * @param boolean $flatten
1461
  *
1462
  * @return boolean
1463
  */
1464
- protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
1465
  {
1466
  $s = $this->count;
1467
- $items = [];
1468
- $value = null;
1469
 
1470
- while ($this->$parseItem($value)) {
1471
- $items[] = $value;
 
 
1472
 
1473
- if ($delim) {
1474
- if (! $this->literal($delim, strlen($delim))) {
1475
- break;
1476
- }
1477
  }
1478
  }
1479
 
1480
- if (! $items) {
1481
- $this->seek($s);
1482
 
1483
- return false;
 
 
 
 
 
 
 
 
1484
  }
1485
 
1486
- if ($flatten && count($items) === 1) {
1487
- $out = $items[0];
1488
- } else {
1489
- $out = [Type::T_LIST, $delim, $items];
 
 
 
 
 
 
 
 
 
 
 
 
1490
  }
1491
 
1492
- return true;
 
 
 
 
 
 
1493
  }
1494
 
1495
  /**
1496
- * Parse expression
1497
  *
1498
  * @param array $out
1499
  *
1500
  * @return boolean
1501
  */
1502
- protected function expression(&$out)
1503
  {
1504
- $s = $this->count;
1505
- $discard = $this->discardComments;
1506
  $this->discardComments = true;
 
 
1507
 
1508
- if ($this->matchChar('(')) {
1509
- if ($this->parenExpression($out, $s, ")")) {
1510
- $this->discardComments = $discard;
1511
- return true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1512
  }
 
1513
 
 
1514
  $this->seek($s);
 
 
1515
  }
1516
 
1517
- if ($this->matchChar('[')) {
1518
- if ($this->parenExpression($out, $s, "]", [Type::T_LIST, Type::T_KEYWORD])) {
1519
- if ($out[0] !== Type::T_LIST && $out[0] !== Type::T_MAP) {
1520
- $out = [Type::T_STRING, '', [ '[', $out, ']' ]];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1521
  }
1522
 
1523
  $this->discardComments = $discard;
 
1524
  return true;
1525
  }
1526
 
1527
  $this->seek($s);
1528
  }
1529
 
1530
- if ($this->value($lhs)) {
1531
- $out = $this->expHelper($lhs, 0);
 
 
 
 
1532
 
1533
  $this->discardComments = $discard;
 
1534
  return true;
1535
  }
1536
 
1537
  $this->discardComments = $discard;
 
1538
  return false;
1539
  }
1540
 
@@ -1548,21 +2236,50 @@ class Parser
1548
  *
1549
  * @return boolean
1550
  */
1551
- protected function parenExpression(&$out, $s, $closingParen = ")", $allowedTypes = [Type::T_LIST, Type::T_MAP])
1552
  {
1553
- if ($this->matchChar($closingParen)) {
1554
  $out = [Type::T_LIST, '', []];
1555
 
 
 
 
 
 
 
 
 
 
 
1556
  return true;
1557
  }
1558
 
1559
- if ($this->valueList($out) && $this->matchChar($closingParen) && in_array($out[0], $allowedTypes)) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1560
  return true;
1561
  }
1562
 
1563
  $this->seek($s);
1564
 
1565
- if (in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) {
1566
  return true;
1567
  }
1568
 
@@ -1600,7 +2317,11 @@ class Parser
1600
  break;
1601
  }
1602
 
1603
- if (! $this->value($rhs)) {
 
 
 
 
1604
  break;
1605
  }
1606
 
@@ -1610,6 +2331,7 @@ class Parser
1610
  }
1611
 
1612
  $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
 
1613
  $ss = $this->count;
1614
  $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1615
  ctype_space($this->buffer[$this->count - 1]);
@@ -1636,7 +2358,10 @@ class Parser
1636
  $s = $this->count;
1637
  $char = $this->buffer[$this->count];
1638
 
1639
- if ($this->literal('url(', 4) && $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)) {
 
 
 
1640
  $len = strspn(
1641
  $this->buffer,
1642
  'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=',
@@ -1655,7 +2380,10 @@ class Parser
1655
 
1656
  $this->seek($s);
1657
 
1658
- if ($this->literal('url(', 4, false) && $this->match('\s*(\/\/\S+)\s*', $m)) {
 
 
 
1659
  $content = 'url(' . $m[1];
1660
 
1661
  if ($this->matchChar(')')) {
@@ -1670,7 +2398,10 @@ class Parser
1670
 
1671
  // not
1672
  if ($char === 'n' && $this->literal('not', 3, false)) {
1673
- if ($this->whitespace() && $this->value($inner)) {
 
 
 
1674
  $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1675
 
1676
  return true;
@@ -1691,28 +2422,56 @@ class Parser
1691
  if ($char === '+') {
1692
  $this->count++;
1693
 
 
 
1694
  if ($this->value($inner)) {
1695
  $out = [Type::T_UNARY, '+', $inner, $this->inParens];
1696
 
1697
  return true;
1698
  }
1699
 
1700
- $this->count--;
 
 
 
 
 
1701
 
1702
  return false;
1703
  }
1704
 
1705
  // negation
1706
  if ($char === '-') {
 
 
 
 
1707
  $this->count++;
1708
 
 
 
1709
  if ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner)) {
1710
  $out = [Type::T_UNARY, '-', $inner, $this->inParens];
1711
 
1712
  return true;
1713
  }
1714
 
1715
- $this->count--;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1716
  }
1717
 
1718
  // paren
@@ -1724,6 +2483,16 @@ class Parser
1724
  if ($this->interpolation($out) || $this->color($out)) {
1725
  return true;
1726
  }
 
 
 
 
 
 
 
 
 
 
1727
  }
1728
 
1729
  if ($this->matchChar('&', true)) {
@@ -1749,9 +2518,17 @@ class Parser
1749
  }
1750
 
1751
  // unicode range with wildcards
1752
- if ($this->literal('U+', 2) && $this->match('([0-9A-F]+\?*)(-([0-9A-F]+))?', $m, false)) {
1753
- $out = [Type::T_KEYWORD, 'U+' . $m[0]];
1754
- return true;
 
 
 
 
 
 
 
 
1755
  }
1756
 
1757
  if ($this->keyword($keyword, false)) {
@@ -1795,7 +2572,10 @@ class Parser
1795
 
1796
  $this->inParens = true;
1797
 
1798
- if ($this->expression($exp) && $this->matchChar(')')) {
 
 
 
1799
  $out = $exp;
1800
  $this->inParens = $inParens;
1801
 
@@ -1820,7 +2600,8 @@ class Parser
1820
  {
1821
  $s = $this->count;
1822
 
1823
- if ($this->literal('progid:', 7, false) &&
 
1824
  $this->openString('(', $fn) &&
1825
  $this->matchChar('(')
1826
  ) {
@@ -1862,7 +2643,10 @@ class Parser
1862
  if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
1863
  $ss = $this->count;
1864
 
1865
- if ($this->argValues($args) && $this->matchChar(')')) {
 
 
 
1866
  $func = [Type::T_FUNCTION_CALL, $name, $args];
1867
 
1868
  return true;
@@ -1871,7 +2655,8 @@ class Parser
1871
  $this->seek($ss);
1872
  }
1873
 
1874
- if (($this->openString(')', $str, '(') || true) &&
 
1875
  $this->matchChar(')')
1876
  ) {
1877
  $args = [];
@@ -1906,7 +2691,10 @@ class Parser
1906
  $args = [];
1907
 
1908
  while ($this->keyword($var)) {
1909
- if ($this->matchChar('=') && $this->expression($exp)) {
 
 
 
1910
  $args[] = [Type::T_STRING, '', [$var . '=']];
1911
  $arg = $exp;
1912
  } else {
@@ -1952,7 +2740,10 @@ class Parser
1952
 
1953
  $ss = $this->count;
1954
 
1955
- if ($this->matchChar(':') && $this->genericList($defaultVal, 'expression')) {
 
 
 
1956
  $arg[1] = $defaultVal;
1957
  } else {
1958
  $this->seek($ss);
@@ -1968,6 +2759,7 @@ class Parser
1968
  }
1969
 
1970
  $arg[2] = true;
 
1971
  $this->seek($sss);
1972
  } else {
1973
  $this->seek($ss);
@@ -2009,8 +2801,10 @@ class Parser
2009
  $keys = [];
2010
  $values = [];
2011
 
2012
- while ($this->genericList($key, 'expression') && $this->matchChar(':') &&
2013
- $this->genericList($value, 'expression')
 
 
2014
  ) {
2015
  $keys[] = $key;
2016
  $values[] = $value;
@@ -2040,55 +2834,18 @@ class Parser
2040
  */
2041
  protected function color(&$out)
2042
  {
2043
- $color = [Type::T_COLOR];
2044
- $s = $this->count;
2045
-
2046
- if ($this->match('(#([0-9a-f]+))', $m)) {
2047
- $nofValues = strlen($m[2]);
2048
- $hasAlpha = $nofValues === 4 || $nofValues === 8;
2049
- $channels = $hasAlpha ? [4, 3, 2, 1] : [3, 2, 1];
2050
-
2051
- switch ($nofValues) {
2052
- case 3:
2053
- case 4:
2054
- $num = hexdec($m[2]);
2055
-
2056
- foreach ($channels as $i) {
2057
- $t = $num & 0xf;
2058
- $color[$i] = $t << 4 | $t;
2059
- $num >>= 4;
2060
- }
2061
-
2062
- break;
2063
-
2064
- case 6:
2065
- case 8:
2066
- $num = hexdec($m[2]);
2067
-
2068
- foreach ($channels as $i) {
2069
- $color[$i] = $num & 0xff;
2070
- $num >>= 8;
2071
- }
2072
-
2073
- break;
2074
-
2075
- default:
2076
- $this->seek($s);
2077
 
2078
- return false;
2079
- }
 
2080
 
2081
- if ($hasAlpha) {
2082
- if ($color[4] === 255) {
2083
- $color[4] = 1; // fully opaque
2084
- } else {
2085
- $color[4] = round($color[4] / 255, 3);
2086
- }
2087
  }
2088
 
2089
- $out = $color;
2090
 
2091
- return true;
2092
  }
2093
 
2094
  return false;
@@ -2106,7 +2863,7 @@ class Parser
2106
  $s = $this->count;
2107
 
2108
  if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m, false)) {
2109
- if (strlen($this->buffer) === $this->count || ! ctype_digit($this->buffer[$this->count])) {
2110
  $this->whitespace();
2111
 
2112
  $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
@@ -2127,7 +2884,7 @@ class Parser
2127
  *
2128
  * @return boolean
2129
  */
2130
- protected function string(&$out)
2131
  {
2132
  $s = $this->count;
2133
 
@@ -2150,52 +2907,48 @@ class Parser
2150
  }
2151
 
2152
  if ($m[2] === '#{') {
2153
- $this->count -= strlen($m[2]);
2154
 
2155
  if ($this->interpolation($inter, false)) {
2156
  $content[] = $inter;
2157
  $hasInterpolation = true;
2158
  } else {
2159
- $this->count += strlen($m[2]);
2160
  $content[] = '#{'; // ignore it
2161
  }
 
 
 
 
 
 
 
 
 
2162
  } elseif ($m[2] === '\\') {
2163
- if ($this->matchChar('"', false)) {
2164
- $content[] = $m[2] . '"';
2165
- } elseif ($this->matchChar("'", false)) {
2166
- $content[] = $m[2] . "'";
2167
- } elseif ($this->literal("\\", 1, false)) {
2168
- $content[] = $m[2] . "\\";
2169
- } elseif ($this->literal("\r\n", 2, false) ||
2170
- $this->matchChar("\r", false) ||
2171
- $this->matchChar("\n", false) ||
2172
- $this->matchChar("\f", false)
2173
  ) {
2174
  // this is a continuation escaping, to be ignored
 
 
2175
  } else {
2176
- $content[] = $m[2];
2177
  }
2178
  } else {
2179
- $this->count -= strlen($delim);
2180
  break; // delim
2181
  }
2182
  }
2183
 
2184
  $this->eatWhiteDefault = $oldWhite;
2185
 
2186
- if ($this->literal($delim, strlen($delim))) {
2187
- if ($hasInterpolation) {
2188
  $delim = '"';
2189
-
2190
- foreach ($content as &$string) {
2191
- if ($string === "\\\\") {
2192
- $string = "\\";
2193
- } elseif ($string === "\\'") {
2194
- $string = "'";
2195
- } elseif ($string === '\\"') {
2196
- $string = '"';
2197
- }
2198
- }
2199
  }
2200
 
2201
  $out = [Type::T_STRING, $delim, $content];
@@ -2208,6 +2961,39 @@ class Parser
2208
  return false;
2209
  }
2210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2211
  /**
2212
  * Parse keyword or interpolation
2213
  *
@@ -2255,18 +3041,29 @@ class Parser
2255
  /**
2256
  * Parse an unbounded string stopped by $end
2257
  *
2258
- * @param string $end
2259
- * @param array $out
2260
- * @param string $nestingOpen
 
 
 
2261
  *
2262
  * @return boolean
2263
  */
2264
- protected function openString($end, &$out, $nestingOpen = null)
2265
  {
2266
  $oldWhite = $this->eatWhiteDefault;
2267
  $this->eatWhiteDefault = false;
2268
 
2269
- $patt = '(.*?)([\'"]|#\{|' . $this->pregQuote($end) . '|' . static::$commentPattern . ')';
 
 
 
 
 
 
 
 
2270
 
2271
  $nestingLevel = 0;
2272
 
@@ -2276,20 +3073,24 @@ class Parser
2276
  if (isset($m[1]) && $m[1] !== '') {
2277
  $content[] = $m[1];
2278
 
2279
- if ($nestingOpen) {
2280
- $nestingLevel += substr_count($m[1], $nestingOpen);
2281
  }
2282
  }
2283
 
2284
  $tok = $m[2];
2285
 
2286
- $this->count-= strlen($tok);
2287
 
2288
- if ($tok === $end && ! $nestingLevel--) {
2289
  break;
2290
  }
2291
 
2292
- if (($tok === "'" || $tok === '"') && $this->string($str)) {
 
 
 
 
2293
  $content[] = $str;
2294
  continue;
2295
  }
@@ -2300,18 +3101,18 @@ class Parser
2300
  }
2301
 
2302
  $content[] = $tok;
2303
- $this->count+= strlen($tok);
2304
  }
2305
 
2306
  $this->eatWhiteDefault = $oldWhite;
2307
 
2308
- if (! $content) {
2309
  return false;
2310
  }
2311
 
2312
  // trim the end
2313
- if (is_string(end($content))) {
2314
- $content[count($content) - 1] = rtrim(end($content));
2315
  }
2316
 
2317
  $out = [Type::T_STRING, '', $content];
@@ -2322,25 +3123,34 @@ class Parser
2322
  /**
2323
  * Parser interpolation
2324
  *
2325
- * @param array $out
2326
- * @param boolean $lookWhite save information about whitespace before and after
2327
  *
2328
  * @return boolean
2329
  */
2330
  protected function interpolation(&$out, $lookWhite = true)
2331
  {
2332
  $oldWhite = $this->eatWhiteDefault;
 
 
2333
  $this->eatWhiteDefault = true;
2334
 
2335
  $s = $this->count;
2336
 
2337
- if ($this->literal('#{', 2) && $this->valueList($value) && $this->matchChar('}', false)) {
 
 
 
 
2338
  if ($value === [Type::T_SELF]) {
2339
  $out = $value;
2340
  } else {
2341
  if ($lookWhite) {
2342
  $left = ($s > 0 && preg_match('/\s/', $this->buffer[$s - 1])) ? ' ' : '';
2343
- $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
 
 
 
2344
  } else {
2345
  $left = $right = false;
2346
  }
@@ -2349,6 +3159,7 @@ class Parser
2349
  }
2350
 
2351
  $this->eatWhiteDefault = $oldWhite;
 
2352
 
2353
  if ($this->eatWhiteDefault) {
2354
  $this->whitespace();
@@ -2360,6 +3171,7 @@ class Parser
2360
  $this->seek($s);
2361
 
2362
  $this->eatWhiteDefault = $oldWhite;
 
2363
 
2364
  return false;
2365
  }
@@ -2405,16 +3217,10 @@ class Parser
2405
  }
2406
 
2407
  // match comment hack
2408
- if (preg_match(
2409
- static::$whitePattern,
2410
- $this->buffer,
2411
- $m,
2412
- null,
2413
- $this->count
2414
- )) {
2415
  if (! empty($m[0])) {
2416
  $parts[] = $m[0];
2417
- $this->count += strlen($m[0]);
2418
  }
2419
  }
2420
 
@@ -2426,12 +3232,72 @@ class Parser
2426
  }
2427
 
2428
  /**
2429
- * Parse comma separated selector list
2430
  *
2431
  * @param array $out
2432
  *
2433
  * @return boolean
2434
  */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2435
  protected function selectors(&$out, $subSelector = false)
2436
  {
2437
  $s = $this->count;
@@ -2463,7 +3329,8 @@ class Parser
2463
  /**
2464
  * Parse whitespace separated selector list
2465
  *
2466
- * @param array $out
 
2467
  *
2468
  * @return boolean
2469
  */
@@ -2472,9 +3339,18 @@ class Parser
2472
  $selector = [];
2473
 
2474
  for (;;) {
 
 
2475
  if ($this->match('[>+~]+', $m, true)) {
2476
- $selector[] = [$m[0]];
2477
- continue;
 
 
 
 
 
 
 
2478
  }
2479
 
2480
  if ($this->selectorSingle($part, $subSelector)) {
@@ -2507,7 +3383,8 @@ class Parser
2507
  * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
2508
  * }}
2509
  *
2510
- * @param array $out
 
2511
  *
2512
  * @return boolean
2513
  */
@@ -2545,6 +3422,7 @@ class Parser
2545
  case '&':
2546
  $parts[] = Compiler::$selfSelector;
2547
  $this->count++;
 
2548
  continue 2;
2549
 
2550
  case '.':
@@ -2569,6 +3447,7 @@ class Parser
2569
  if ($this->placeholder($placeholder)) {
2570
  $parts[] = '%';
2571
  $parts[] = $placeholder;
 
2572
  continue;
2573
  }
2574
 
@@ -2578,6 +3457,7 @@ class Parser
2578
  if ($char === '#') {
2579
  if ($this->interpolation($inter)) {
2580
  $parts[] = $inter;
 
2581
  continue;
2582
  }
2583
 
@@ -2605,12 +3485,21 @@ class Parser
2605
 
2606
  $ss = $this->count;
2607
 
2608
- if ($nameParts === ['not'] || $nameParts === ['is'] ||
2609
- $nameParts === ['has'] || $nameParts === ['where']
 
 
 
 
 
 
 
 
2610
  ) {
2611
- if ($this->matchChar('(') &&
2612
- ($this->selectors($subs, true) || true) &&
2613
- $this->matchChar(')')
 
2614
  ) {
2615
  $parts[] = '(';
2616
 
@@ -2619,11 +3508,13 @@ class Parser
2619
  foreach ($ps as &$p) {
2620
  $parts[] = $p;
2621
  }
2622
- if (count($sub) && reset($sub)) {
 
2623
  $parts[] = ' ';
2624
  }
2625
  }
2626
- if (count($subs) && reset($subs)) {
 
2627
  $parts[] = ', ';
2628
  }
2629
  }
@@ -2632,21 +3523,20 @@ class Parser
2632
  } else {
2633
  $this->seek($ss);
2634
  }
2635
- } else {
2636
- if ($this->matchChar('(') &&
2637
- ($this->openString(')', $str, '(') || true) &&
2638
- $this->matchChar(')')
2639
- ) {
2640
- $parts[] = '(';
2641
-
2642
- if (! empty($str)) {
2643
- $parts[] = $str;
2644
- }
2645
 
2646
- $parts[] = ')';
2647
- } else {
2648
- $this->seek($ss);
2649
  }
 
 
 
 
2650
  }
2651
 
2652
  continue;
@@ -2655,8 +3545,20 @@ class Parser
2655
 
2656
  $this->seek($s);
2657
 
 
 
 
 
 
 
 
 
 
 
 
2658
  // attribute selector
2659
- if ($char === '[' &&
 
2660
  $this->matchChar('[') &&
2661
  ($this->openString(']', $str, '[') || true) &&
2662
  $this->matchChar(']')
@@ -2709,8 +3611,15 @@ class Parser
2709
  {
2710
  $s = $this->count;
2711
 
2712
- if ($this->matchChar('$', false) && $this->keyword($name)) {
2713
- $out = [Type::T_VARIABLE, $name];
 
 
 
 
 
 
 
2714
 
2715
  return true;
2716
  }
@@ -2730,13 +3639,15 @@ class Parser
2730
  */
2731
  protected function keyword(&$word, $eatWhitespace = null)
2732
  {
2733
- if ($this->match(
2734
  $this->utf8
2735
  ? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|[\\\\].)*)'
2736
  : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
2737
  $m,
2738
  $eatWhitespace
2739
- )) {
 
 
2740
  $word = $m[1];
2741
 
2742
  return true;
@@ -2757,7 +3668,7 @@ class Parser
2757
  {
2758
  $s = $this->count;
2759
 
2760
- if ($this->keyword($word, $eatWhitespace) && (ord($word[0]) > 57 || ord($word[0]) < 48)) {
2761
  return true;
2762
  }
2763
 
@@ -2769,22 +3680,25 @@ class Parser
2769
  /**
2770
  * Parse a placeholder
2771
  *
2772
- * @param string $placeholder
2773
  *
2774
  * @return boolean
2775
  */
2776
  protected function placeholder(&$placeholder)
2777
  {
2778
- if ($this->match(
2779
  $this->utf8
2780
  ? '([\pL\w\-_]+)'
2781
  : '([\w\-_]+)',
2782
  $m
2783
- )) {
 
 
2784
  $placeholder = $m[1];
2785
 
2786
  return true;
2787
  }
 
2788
  if ($this->interpolation($placeholder)) {
2789
  return true;
2790
  }
@@ -2801,10 +3715,28 @@ class Parser
2801
  */
2802
  protected function url(&$out)
2803
  {
2804
- if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) {
2805
- $out = [Type::T_STRING, '', ['url(' . $m[2] . $m[3] . $m[2] . ')']];
2806
 
2807
- return true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2808
  }
2809
 
2810
  return false;
@@ -2812,16 +3744,17 @@ class Parser
2812
 
2813
  /**
2814
  * Consume an end of statement delimiter
 
2815
  *
2816
  * @return boolean
2817
  */
2818
- protected function end()
2819
  {
2820
- if ($this->matchChar(';')) {
2821
  return true;
2822
  }
2823
 
2824
- if ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
2825
  // if there is end of file or a closing block next then we don't need a ;
2826
  return true;
2827
  }
@@ -2840,18 +3773,15 @@ class Parser
2840
  {
2841
  $flags = [];
2842
 
2843
- for ($token = &$value; $token[0] === Type::T_LIST && ($s = count($token[2])); $token = &$lastNode) {
2844
  $lastNode = &$token[2][$s - 1];
2845
 
2846
- while ($lastNode[0] === Type::T_KEYWORD && in_array($lastNode[1], ['!default', '!global'])) {
2847
  array_pop($token[2]);
2848
 
2849
- $node = end($token[2]);
2850
-
2851
- $token = $this->flattenList($token);
2852
-
2853
- $flags[] = $lastNode[1];
2854
-
2855
  $lastNode = $node;
2856
  }
2857
  }
@@ -2869,12 +3799,11 @@ class Parser
2869
  protected function stripOptionalFlag(&$selectors)
2870
  {
2871
  $optional = false;
2872
-
2873
  $selector = end($selectors);
2874
- $part = end($selector);
2875
 
2876
  if ($part === ['!optional']) {
2877
- array_pop($selectors[count($selectors) - 1]);
2878
 
2879
  $optional = true;
2880
  }
@@ -2891,55 +3820,13 @@ class Parser
2891
  */
2892
  protected function flattenList($value)
2893
  {
2894
- if ($value[0] === Type::T_LIST && count($value[2]) === 1) {
2895
  return $this->flattenList($value[2][0]);
2896
  }
2897
 
2898
  return $value;
2899
  }
2900
 
2901
- /**
2902
- * @deprecated
2903
- *
2904
- * {@internal
2905
- * advance counter to next occurrence of $what
2906
- * $until - don't include $what in advance
2907
- * $allowNewline, if string, will be used as valid char set
2908
- * }}
2909
- */
2910
- protected function to($what, &$out, $until = false, $allowNewline = false)
2911
- {
2912
- if (is_string($allowNewline)) {
2913
- $validChars = $allowNewline;
2914
- } else {
2915
- $validChars = $allowNewline ? '.' : "[^\n]";
2916
- }
2917
-
2918
- if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
2919
- return false;
2920
- }
2921
-
2922
- if ($until) {
2923
- $this->count -= strlen($what); // give back $what
2924
- }
2925
-
2926
- $out = $m[1];
2927
-
2928
- return true;
2929
- }
2930
-
2931
- /**
2932
- * @deprecated
2933
- */
2934
- protected function show()
2935
- {
2936
- if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
2937
- return $m[1];
2938
- }
2939
-
2940
- return '';
2941
- }
2942
-
2943
  /**
2944
  * Quote regular expression
2945
  *
@@ -2967,10 +3854,10 @@ class Parser
2967
  $prev = $pos + 1;
2968
  }
2969
 
2970
- $this->sourcePositions[] = strlen($buffer);
2971
 
2972
  if (substr($buffer, -1) !== "\n") {
2973
- $this->sourcePositions[] = strlen($buffer) + 1;
2974
  }
2975
  }
2976
 
@@ -2984,7 +3871,7 @@ class Parser
2984
  private function getSourcePosition($pos)
2985
  {
2986
  $low = 0;
2987
- $high = count($this->sourcePositions);
2988
 
2989
  while ($low < $high) {
2990
  $mid = (int) (($high + $low) / 2);
@@ -3010,13 +3897,7 @@ class Parser
3010
  */
3011
  private function saveEncoding()
3012
  {
3013
- if (version_compare(PHP_VERSION, '7.2.0') >= 0) {
3014
- return;
3015
- }
3016
-
3017
- $iniDirective = 'mbstring' . '.func_overload'; // deprecated in PHP 7.2
3018
-
3019
- if (ini_get($iniDirective) & 2) {
3020
  $this->encoding = mb_internal_encoding();
3021
 
3022
  mb_internal_encoding('iso-8859-1');
@@ -3028,7 +3909,7 @@ class Parser
3028
  */
3029
  private function restoreEncoding()
3030
  {
3031
- if ($this->encoding) {
3032
  mb_internal_encoding($this->encoding);
3033
  }
3034
  }
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
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
  *
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;
93
  $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8';
94
  $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';
95
  $this->commentsSeen = [];
96
+ $this->commentsSeen = [];
97
+ $this->allowVars = true;
98
+ $this->cssOnly = $cssOnly;
99
 
100
  if (empty(static::$operatorPattern)) {
101
  static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
144
  ? "line: $line, column: $column"
145
  : "$this->sourceName on line $line, at column $column";
146
 
147
+ if ($this->peek('(.*?)(\n|$)', $m, $this->count)) {
148
+ $this->restoreEncoding();
149
+
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();
157
+
158
+ $e = new ParserException("$msg: $loc");
159
+ $e->setSourcePosition([$this->sourceName, $line, $column]);
160
+
161
+ throw $e;
162
  }
163
 
164
  /**
173
  public function parse($buffer)
174
  {
175
  if ($this->cache) {
176
+ $cacheKey = $this->sourceName . ':' . md5($buffer);
177
  $parseOptions = [
178
  'charset' => $this->charset,
179
  'utf8' => $this->utf8,
180
  ];
181
+ $v = $this->cache->getCache('parse', $cacheKey, $parseOptions);
182
 
183
+ if (! \is_null($v)) {
184
  return $v;
185
  }
186
  }
208
  ;
209
  }
210
 
211
+ if ($this->count !== \strlen($this->buffer)) {
212
  $this->throwParseError();
213
  }
214
 
223
  $this->restoreEncoding();
224
 
225
  if ($this->cache) {
226
+ $this->cache->setCache('parse', $cacheKey, $this->env, $parseOptions);
227
  }
228
 
229
  return $this->env;
234
  *
235
  * @api
236
  *
237
+ * @param string $buffer
238
+ * @param string|array $out
239
  *
240
  * @return boolean
241
  */
261
  *
262
  * @api
263
  *
264
+ * @param string $buffer
265
+ * @param string|array $out
266
  *
267
  * @return boolean
268
  */
288
  *
289
  * @api
290
  *
291
+ * @param string $buffer
292
+ * @param string|array $out
293
  *
294
+ * @return boolean
295
  */
296
  public function parseMediaQueryList($buffer, &$out)
297
  {
303
 
304
  $this->saveEncoding();
305
 
 
306
  $isMediaQuery = $this->mediaQueryList($out);
307
 
308
  $this->restoreEncoding();
355
 
356
  // the directives
357
  if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
358
+ if (
359
+ $this->literal('@at-root', 8) &&
360
  ($this->selectors($selector) || true) &&
361
  ($this->map($with) || true) &&
362
+ (($this->matchChar('(') &&
363
+ $this->interpolation($with) &&
364
+ $this->matchChar(')')) || true) &&
365
  $this->matchChar('{', false)
366
  ) {
367
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
368
+
369
  $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
370
  $atRoot->selector = $selector;
371
+ $atRoot->with = $with;
372
 
373
  return true;
374
  }
375
 
376
  $this->seek($s);
377
 
378
+ if (
379
+ $this->literal('@media', 6) &&
380
+ $this->mediaQueryList($mediaQueryList) &&
381
+ $this->matchChar('{', false)
382
+ ) {
383
  $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
384
  $media->queryList = $mediaQueryList[2];
385
 
388
 
389
  $this->seek($s);
390
 
391
+ if (
392
+ $this->literal('@mixin', 6) &&
393
  $this->keyword($mixinName) &&
394
  ($this->argumentDef($args) || true) &&
395
  $this->matchChar('{', false)
396
  ) {
397
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
398
+
399
  $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
400
  $mixin->name = $mixinName;
401
  $mixin->args = $args;
405
 
406
  $this->seek($s);
407
 
408
+ if (
409
+ ($this->literal('@include', 8) &&
410
+ $this->keyword($mixinName) &&
411
+ ($this->matchChar('(') &&
412
  ($this->argValues($argValues) || true) &&
413
  $this->matchChar(')') || true) &&
414
+ ($this->end()) ||
415
+ ($this->literal('using', 5) &&
416
+ $this->argumentDef($argUsing) &&
417
+ ($this->end() || $this->matchChar('{') && $hasBlock = true)) ||
418
+ $this->matchChar('{') && $hasBlock = true)
419
  ) {
420
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
421
+
422
+ $child = [
423
+ Type::T_INCLUDE,
424
+ $mixinName,
425
+ isset($argValues) ? $argValues : null,
426
+ null,
427
+ isset($argUsing) ? $argUsing : null
428
+ ];
429
 
430
  if (! empty($hasBlock)) {
431
  $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
439
 
440
  $this->seek($s);
441
 
442
+ if (
443
+ $this->literal('@scssphp-import-once', 20) &&
444
  $this->valueList($importPath) &&
445
  $this->end()
446
  ) {
447
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
448
+
449
  $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s);
450
 
451
  return true;
453
 
454
  $this->seek($s);
455
 
456
+ if (
457
+ $this->literal('@import', 7) &&
458
  $this->valueList($importPath) &&
459
+ $importPath[0] !== Type::T_FUNCTION_CALL &&
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
+ }
467
+
468
  $this->append([Type::T_IMPORT, $importPath], $s);
469
 
470
  return true;
472
 
473
  $this->seek($s);
474
 
475
+ if (
476
+ $this->literal('@import', 7) &&
477
  $this->url($importPath) &&
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
+ }
485
+
486
  $this->append([Type::T_IMPORT, $importPath], $s);
487
 
488
  return true;
490
 
491
  $this->seek($s);
492
 
493
+ if (
494
+ $this->literal('@extend', 7) &&
495
  $this->selectors($selectors) &&
496
  $this->end()
497
  ) {
498
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
499
+
500
  // check for '!flag'
501
  $optional = $this->stripOptionalFlag($selectors);
502
  $this->append([Type::T_EXTEND, $selectors, $optional], $s);
506
 
507
  $this->seek($s);
508
 
509
+ if (
510
+ $this->literal('@function', 9) &&
511
  $this->keyword($fnName) &&
512
  $this->argumentDef($args) &&
513
  $this->matchChar('{', false)
514
  ) {
515
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
516
+
517
  $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
518
  $func->name = $fnName;
519
  $func->args = $args;
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;
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;
549
 
550
  $this->seek($s);
551
 
552
+ if (
553
+ $this->literal('@return', 7) &&
554
+ ($this->valueList($retVal) || true) &&
555
+ $this->end()
556
+ ) {
557
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
558
+
559
  $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
560
 
561
  return true;
563
 
564
  $this->seek($s);
565
 
566
+ if (
567
+ $this->literal('@each', 5) &&
568
  $this->genericList($varNames, 'variable', ',', false) &&
569
  $this->literal('in', 2) &&
570
  $this->valueList($list) &&
571
  $this->matchChar('{', false)
572
  ) {
573
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
574
+
575
  $each = $this->pushSpecialBlock(Type::T_EACH, $s);
576
 
577
  foreach ($varNames[2] as $varName) {
585
 
586
  $this->seek($s);
587
 
588
+ if (
589
+ $this->literal('@while', 6) &&
590
  $this->expression($cond) &&
591
  $this->matchChar('{', false)
592
  ) {
593
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
594
+
595
+ while (
596
+ $cond[0] === Type::T_LIST &&
597
+ ! empty($cond['enclosing']) &&
598
+ $cond['enclosing'] === 'parent' &&
599
+ \count($cond[2]) == 1
600
+ ) {
601
+ $cond = reset($cond[2]);
602
+ }
603
+
604
  $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
605
  $while->cond = $cond;
606
 
609
 
610
  $this->seek($s);
611
 
612
+ if (
613
+ $this->literal('@for', 4) &&
614
  $this->variable($varName) &&
615
  $this->literal('from', 4) &&
616
  $this->expression($start) &&
619
  $this->expression($end) &&
620
  $this->matchChar('{', false)
621
  ) {
622
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
623
+
624
  $for = $this->pushSpecialBlock(Type::T_FOR, $s);
625
+ $for->var = $varName[1];
626
  $for->start = $start;
627
+ $for->end = $end;
628
  $for->until = isset($forUntil);
629
 
630
  return true;
632
 
633
  $this->seek($s);
634
 
635
+ if (
636
+ $this->literal('@if', 3) &&
637
+ $this->functionCallArgumentsList($cond, false, '{', false)
638
+ ) {
639
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
640
+
641
  $if = $this->pushSpecialBlock(Type::T_IF, $s);
642
+
643
+ while (
644
+ $cond[0] === Type::T_LIST &&
645
+ ! empty($cond['enclosing']) &&
646
+ $cond['enclosing'] === 'parent' &&
647
+ \count($cond[2]) == 1
648
+ ) {
649
+ $cond = reset($cond[2]);
650
+ }
651
+
652
+ $if->cond = $cond;
653
  $if->cases = [];
654
 
655
  return true;
657
 
658
  $this->seek($s);
659
 
660
+ if (
661
+ $this->literal('@debug', 6) &&
662
+ $this->functionCallArgumentsList($value, false)
663
  ) {
664
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
665
+
666
  $this->append([Type::T_DEBUG, $value], $s);
667
 
668
  return true;
670
 
671
  $this->seek($s);
672
 
673
+ if (
674
+ $this->literal('@warn', 5) &&
675
+ $this->functionCallArgumentsList($value, false)
676
  ) {
677
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
678
+
679
  $this->append([Type::T_WARN, $value], $s);
680
 
681
  return true;
683
 
684
  $this->seek($s);
685
 
686
+ if (
687
+ $this->literal('@error', 6) &&
688
+ $this->functionCallArgumentsList($value, false)
689
  ) {
690
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
691
+
692
  $this->append([Type::T_ERROR, $value], $s);
693
 
694
  return true;
696
 
697
  $this->seek($s);
698
 
699
+ if (
700
+ $this->literal('@content', 8) &&
701
+ ($this->end() ||
702
+ $this->matchChar('(') &&
703
+ $this->argValues($argContent) &&
704
+ $this->matchChar(')') &&
705
+ $this->end())
706
+ ) {
707
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
708
+
709
+ $this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s);
710
 
711
  return true;
712
  }
721
  if ($this->literal('@else', 5)) {
722
  if ($this->matchChar('{', false)) {
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;
731
  }
742
  }
743
 
744
  // only retain the first @charset directive encountered
745
+ if (
746
+ $this->literal('@charset', 8) &&
747
  $this->valueList($charset) &&
748
  $this->end()
749
  ) {
764
 
765
  $this->seek($s);
766
 
767
+ if (
768
+ $this->literal('@supports', 9) &&
769
+ ($t1 = $this->supportsQuery($supportQuery)) &&
770
+ ($t2 = $this->matchChar('{', false))
771
+ ) {
772
  $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
773
+ $directive->name = 'supports';
774
  $directive->value = $supportQuery;
775
 
776
  return true;
779
  $this->seek($s);
780
 
781
  // doesn't match built in directive, do generic one
782
+ if (
783
+ $this->matchChar('@', false) &&
784
+ $this->mixedKeyword($dirName) &&
785
+ $this->directiveValue($dirValue, '{')
786
  ) {
787
+ if (count($dirName) === 1 && is_string(reset($dirName))) {
788
+ $dirName = reset($dirName);
789
+ } else {
790
+ $dirName = [Type::T_STRING, '', $dirName];
791
+ }
792
  if ($dirName === 'media') {
793
  $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
794
  } else {
797
  }
798
 
799
  if (isset($dirValue)) {
800
+ ! $this->cssOnly || ($dirValue = $this->assertPlainCssValid($dirValue));
801
  $directive->value = $dirValue;
802
  }
803
 
806
 
807
  $this->seek($s);
808
 
809
+ // maybe it's a generic blockless directive
810
+ if (
811
+ $this->matchChar('@', false) &&
812
+ $this->mixedKeyword($dirName) &&
813
+ ! $this->isKnownGenericDirective($dirName) &&
814
+ ($this->end(false) || ($this->directiveValue($dirValue, '') && $this->end(false)))
815
+ ) {
816
+ if (\count($dirName) === 1 && \is_string(\reset($dirName))) {
817
+ $dirName = \reset($dirName);
818
+ } else {
819
+ $dirName = [Type::T_STRING, '', $dirName];
820
+ }
821
+ if (
822
+ ! empty($this->env->parent) &&
823
+ $this->env->type &&
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
+ }
831
+ // blockless directives with a blank line after keeps their blank lines after
832
+ // sass-spec compliance purpose
833
+ $s = $this->count;
834
+ $hasBlankLine = false;
835
+ if ($this->match('\s*?\n\s*\n', $out, false)) {
836
+ $hasBlankLine = true;
837
+ $this->seek($s);
838
+ }
839
+ $isNotRoot = ! empty($this->env->parent);
840
+ $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue, $hasBlankLine, $isNotRoot]], $s);
841
+ $this->whitespace();
842
+
843
+ return true;
844
+ }
845
+
846
+ $this->seek($s);
847
+
848
  return false;
849
  }
850
 
851
+ $inCssSelector = null;
852
+ if ($this->cssOnly) {
853
+ $inCssSelector = (! empty($this->env->parent) &&
854
+ ! in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA]));
855
+ }
856
+ // custom properties : right part is static
857
+ if (($this->customProperty($name) ) && $this->matchChar(':', false)) {
858
+ $start = $this->count;
859
+
860
+ // but can be complex and finish with ; or }
861
+ foreach ([';','}'] as $ending) {
862
+ if (
863
+ $this->openString($ending, $stringValue, '(', ')', false) &&
864
+ $this->end()
865
+ ) {
866
+ $end = $this->count;
867
+ $value = $stringValue;
868
+
869
+ // check if we have only a partial value due to nested [] or { } to take in account
870
+ $nestingPairs = [['[', ']'], ['{', '}']];
871
+
872
+ foreach ($nestingPairs as $nestingPair) {
873
+ $p = strpos($this->buffer, $nestingPair[0], $start);
874
+
875
+ if ($p && $p < $end) {
876
+ $this->seek($start);
877
+
878
+ if (
879
+ $this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) &&
880
+ $this->end() &&
881
+ $this->count > $end
882
+ ) {
883
+ $end = $this->count;
884
+ $value = $stringValue;
885
+ }
886
+ }
887
+ }
888
+
889
+ $this->seek($end);
890
+ $this->append([Type::T_CUSTOM_PROPERTY, $name, $value], $s);
891
+
892
+ return true;
893
+ }
894
+ }
895
+
896
+ // TODO: output an error here if nothing found according to sass spec
897
+ }
898
+
899
+ $this->seek($s);
900
+
901
  // property shortcut
902
  // captures most properties before having to parse a selector
903
+ if (
904
+ $this->keyword($name, false) &&
905
  $this->literal(': ', 2) &&
906
  $this->valueList($value) &&
907
  $this->end()
915
  $this->seek($s);
916
 
917
  // variable assigns
918
+ if (
919
+ $this->variable($name) &&
920
  $this->matchChar(':') &&
921
  $this->valueList($value) &&
922
  $this->end()
923
  ) {
924
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
925
+
926
  // check for '!flag'
927
  $assignmentFlags = $this->stripAssignmentFlags($value);
928
  $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s);
938
  }
939
 
940
  // opening css block
941
+ if (
942
+ $this->selectors($selectors) &&
943
+ $this->matchChar('{', false)
944
+ ) {
945
+ ! $this->cssOnly || ! $inCssSelector || $this->assertPlainCssValid(false);
946
+
947
  $this->pushBlock($selectors, $s);
948
 
949
  if ($this->eatWhiteDefault) {
950
  $this->whitespace();
951
+ $this->append(null); // collect comments at the beginning if needed
952
  }
953
 
954
  return true;
957
  $this->seek($s);
958
 
959
  // property assign, or nested assign
960
+ if (
961
+ $this->propertyName($name) &&
962
+ $this->matchChar(':')
963
+ ) {
964
  $foundSomething = false;
965
 
966
  if ($this->valueList($value)) {
973
  }
974
 
975
  if ($this->matchChar('{', false)) {
976
+ ! $this->cssOnly || $this->assertPlainCssValid(false);
977
+
978
  $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
979
  $propBlock->prefix = $name;
980
  $propBlock->hasValue = $foundSomething;
995
  if ($this->matchChar('}', false)) {
996
  $block = $this->popBlock();
997
 
998
+ if (! isset($block->type) || $block->type !== Type::T_IF) {
999
  if ($this->env->parent) {
1000
  $this->append(null); // collect comments before next statement if needed
1001
  }
1024
  }
1025
 
1026
  // extra stuff
1027
+ if (
1028
+ $this->matchChar(';') ||
1029
  $this->literal('<!--', 4)
1030
  ) {
1031
  return true;
1046
  {
1047
  list($line, $column) = $this->getSourcePosition($pos);
1048
 
1049
+ $b = new Block();
1050
  $b->sourceName = $this->sourceName;
1051
  $b->sourceLine = $line;
1052
  $b->sourceColumn = $column;
1068
 
1069
  $this->env = $b;
1070
 
1071
+ // collect comments at the beginning of a block if needed
1072
  if ($this->eatWhiteDefault) {
1073
  $this->whitespace();
1074
 
1161
  $this->count = $where;
1162
  }
1163
 
1164
+ /**
1165
+ * Assert a parsed part is plain CSS Valid
1166
+ *
1167
+ * @param array $parsed
1168
+ * @param int $startPos
1169
+ * @throws ParserException
1170
+ */
1171
+ protected function assertPlainCssValid($parsed, $startPos = null)
1172
+ {
1173
+ $type = '';
1174
+ if ($parsed) {
1175
+ $type = $parsed[0];
1176
+ $parsed = $this->isPlainCssValidElement($parsed);
1177
+ }
1178
+ if (! $parsed) {
1179
+ if (! \is_null($startPos)) {
1180
+ $plain = rtrim(substr($this->buffer, $startPos, $this->count - $startPos));
1181
+ $message = "Error : `{$plain}` isn't allowed in plain CSS";
1182
+ } else {
1183
+ $message = 'Error: SCSS syntax not allowed in CSS file';
1184
+ }
1185
+ if ($type) {
1186
+ $message .= " ($type)";
1187
+ }
1188
+ $this->throwParseError($message);
1189
+ }
1190
+
1191
+ return $parsed;
1192
+ }
1193
+
1194
+ /**
1195
+ * Check a parsed element is plain CSS Valid
1196
+ * @param array $parsed
1197
+ * @return bool|array
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], [
1204
+ 'alpha',
1205
+ 'attr',
1206
+ 'calc',
1207
+ 'cubic-bezier',
1208
+ 'env',
1209
+ 'grayscale',
1210
+ 'hsl',
1211
+ 'hsla',
1212
+ 'invert',
1213
+ 'linear-gradient',
1214
+ 'min',
1215
+ 'max',
1216
+ 'radial-gradient',
1217
+ 'repeating-linear-gradient',
1218
+ 'repeating-radial-gradient',
1219
+ 'rgb',
1220
+ 'rgba',
1221
+ 'rotate',
1222
+ 'saturate',
1223
+ 'var',
1224
+ ]) &&
1225
+ Compiler::isNativeFunction($parsed[1])
1226
+ ) {
1227
+ return false;
1228
+ }
1229
+
1230
+ switch ($parsed[0]) {
1231
+ case Type::T_BLOCK:
1232
+ case Type::T_KEYWORD:
1233
+ case Type::T_NULL:
1234
+ case Type::T_NUMBER:
1235
+ return $parsed;
1236
+
1237
+ case Type::T_COMMENT:
1238
+ if (isset($parsed[2])) {
1239
+ return false;
1240
+ }
1241
+ return $parsed;
1242
+
1243
+ case Type::T_DIRECTIVE:
1244
+ if (\is_array($parsed[1])) {
1245
+ $parsed[1][1] = $this->isPlainCssValidElement($parsed[1][1]);
1246
+ if (! $parsed[1][1]) {
1247
+ return false;
1248
+ }
1249
+ }
1250
+
1251
+ return $parsed;
1252
+
1253
+ case Type::T_STRING:
1254
+ foreach ($parsed[2] as $k => $substr) {
1255
+ if (\is_array($substr)) {
1256
+ $parsed[2][$k] = $this->isPlainCssValidElement($substr);
1257
+ if (! $parsed[2][$k]) {
1258
+ return false;
1259
+ }
1260
+ }
1261
+ }
1262
+ return $parsed;
1263
+
1264
+ case Type::T_ASSIGN:
1265
+ foreach ([1, 2, 3] as $k) {
1266
+ if (! empty($parsed[$k])) {
1267
+ $parsed[$k] = $this->isPlainCssValidElement($parsed[$k]);
1268
+ if (! $parsed[$k]) {
1269
+ return false;
1270
+ }
1271
+ }
1272
+ }
1273
+ return $parsed;
1274
+
1275
+ case Type::T_EXPRESSION:
1276
+ list( ,$op, $lhs, $rhs, $inParens, $whiteBefore, $whiteAfter) = $parsed;
1277
+ if (! $allowExpression && ! \in_array($op, ['and', 'or', '/'])) {
1278
+ return false;
1279
+ }
1280
+ $lhs = $this->isPlainCssValidElement($lhs, true);
1281
+ if (! $lhs) {
1282
+ return false;
1283
+ }
1284
+ $rhs = $this->isPlainCssValidElement($rhs, true);
1285
+ if (! $rhs) {
1286
+ return false;
1287
+ }
1288
+
1289
+ return [
1290
+ Type::T_STRING,
1291
+ '', [
1292
+ $this->inParens ? '(' : '',
1293
+ $lhs,
1294
+ ($whiteBefore ? ' ' : '') . $op . ($whiteAfter ? ' ' : ''),
1295
+ $rhs,
1296
+ $this->inParens ? ')' : ''
1297
+ ]
1298
+ ];
1299
+
1300
+ case Type::T_UNARY:
1301
+ $parsed[2] = $this->isPlainCssValidElement($parsed[2]);
1302
+ if (! $parsed[2]) {
1303
+ return false;
1304
+ }
1305
+ return $parsed;
1306
+
1307
+ case Type::T_FUNCTION:
1308
+ $argsList = $parsed[2];
1309
+ foreach ($argsList[2] as $argElement) {
1310
+ if (! $this->isPlainCssValidElement($argElement)) {
1311
+ return false;
1312
+ }
1313
+ }
1314
+ return $parsed;
1315
+
1316
+ case Type::T_FUNCTION_CALL:
1317
+ $parsed[0] = Type::T_FUNCTION;
1318
+ $argsList = [Type::T_LIST, ',', []];
1319
+ foreach ($parsed[2] as $arg) {
1320
+ if ($arg[0] || ! empty($arg[2])) {
1321
+ // no named arguments possible in a css function call
1322
+ // nor ... argument
1323
+ return false;
1324
+ }
1325
+ $arg = $this->isPlainCssValidElement($arg[1], $parsed[1] === 'calc');
1326
+ if (! $arg) {
1327
+ return false;
1328
+ }
1329
+ $argsList[2][] = $arg;
1330
+ }
1331
+ $parsed[2] = $argsList;
1332
+ return $parsed;
1333
+ }
1334
+
1335
+ return false;
1336
+ }
1337
+
1338
  /**
1339
  * Match string looking for either ending delim, escape, or string interpolation
1340
  *
1341
  * {@internal This is a workaround for preg_match's 250K string match limit. }}
1342
  *
1343
  * @param array $m Matches (passed by reference)
1344
+ * @param string $delim Delimiter
1345
  *
1346
  * @return boolean True if match; false otherwise
1347
  */
1349
  {
1350
  $token = null;
1351
 
1352
+ $end = \strlen($this->buffer);
1353
 
1354
  // look for either ending delim, escape, or string interpolation
1355
+ foreach (['#{', '\\', "\r", $delim] as $lookahead) {
1356
  $pos = strpos($this->buffer, $lookahead, $this->count);
1357
 
1358
  if ($pos !== false && $pos < $end) {
1371
  $match,
1372
  $token
1373
  ];
1374
+ $this->count = $end + \strlen($token);
1375
 
1376
  return true;
1377
  }
1393
  return false;
1394
  }
1395
 
1396
+ $this->count += \strlen($out[0]);
1397
 
1398
  if (! isset($eatWhitespace)) {
1399
  $eatWhitespace = $this->eatWhiteDefault;
1474
  if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
1475
  // comment that are kept in the output CSS
1476
  $comment = [];
1477
+ $startCommentCount = $this->count;
1478
+ $endCommentCount = $this->count + \strlen($m[1]);
1479
 
1480
  // find interpolations in comment
1481
  $p = strpos($this->buffer, '#{', $this->count);
1482
 
1483
  while ($p !== false && $p < $endCommentCount) {
1484
+ $c = substr($this->buffer, $this->count, $p - $this->count);
1485
+ $comment[] = $c;
1486
  $this->count = $p;
1487
+ $out = null;
1488
 
1489
  if ($this->interpolation($out)) {
1490
  // keep right spaces in the following string part
1491
  if ($out[3]) {
1492
+ while ($this->buffer[$this->count - 1] !== '}') {
1493
  $this->count--;
1494
  }
1495
 
1496
  $out[3] = '';
1497
  }
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
 
1514
  $this->appendComment([Type::T_COMMENT, $c]);
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;
1522
  $this->count = $endCommentCount;
1523
  } else {
1524
  // comment that are ignored and not kept in the output css
1525
+ $this->count += \strlen($m[0]);
1526
+ // silent comments are not allowed in plain CSS files
1527
+ ! $this->cssOnly
1528
+ || ! \strlen(trim($m[0]))
1529
+ || $this->assertPlainCssValid(false, $this->count - \strlen($m[0]));
1530
  }
1531
 
1532
  $gotWhite = true;
1543
  protected function appendComment($comment)
1544
  {
1545
  if (! $this->discardComments) {
 
 
 
 
1546
  $this->env->comments[] = $comment;
1547
  }
1548
  }
1555
  */
1556
  protected function append($statement, $pos = null)
1557
  {
1558
+ if (! \is_null($statement)) {
1559
+ ! $this->cssOnly || ($statement = $this->assertPlainCssValid($statement, $pos));
1560
+
1561
+ if (! \is_null($pos)) {
1562
  list($line, $column) = $this->getSourcePosition($pos);
1563
 
1564
  $statement[static::SOURCE_LINE] = $line;
1584
  */
1585
  protected function last()
1586
  {
1587
+ $i = \count($this->env->children) - 1;
1588
 
1589
  if (isset($this->env->children[$i])) {
1590
  return $this->env->children[$i];
1615
  $expressions = null;
1616
  $parts = [];
1617
 
1618
+ if (
1619
+ ($this->literal('only', 4) && ($only = true) ||
1620
+ $this->literal('not', 3) && ($not = true) || true) &&
1621
  $this->mixedKeyword($mediaType)
1622
  ) {
1623
  $prop = [Type::T_MEDIA_TYPE];
1633
  $media = [Type::T_LIST, '', []];
1634
 
1635
  foreach ((array) $mediaType as $type) {
1636
+ if (\is_array($type)) {
1637
  $media[2][] = $type;
1638
  } else {
1639
  $media[2][] = [Type::T_KEYWORD, $type];
1647
  if (empty($parts) || $this->literal('and', 3)) {
1648
  $this->genericList($expressions, 'mediaExpression', 'and', false);
1649
 
1650
+ if (\is_array($expressions)) {
1651
  $parts = array_merge($parts, $expressions[2]);
1652
  }
1653
  }
1672
  $s = $this->count;
1673
 
1674
  $not = false;
1675
+
1676
+ if (
1677
+ ($this->literal('not', 3) && ($not = true) || true) &&
1678
  $this->matchChar('(') &&
1679
  ($this->expression($property)) &&
1680
  $this->literal(': ', 2) &&
1681
  $this->valueList($value) &&
1682
+ $this->matchChar(')')
1683
+ ) {
1684
  $support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]];
1685
  $support[2][] = $property;
1686
  $support[2][] = [Type::T_KEYWORD, ': '];
1693
  $this->seek($s);
1694
  }
1695
 
1696
+ if (
1697
+ $this->matchChar('(') &&
1698
  $this->supportsQuery($subQuery) &&
1699
+ $this->matchChar(')')
1700
+ ) {
1701
  $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, '('], $subQuery, [Type::T_KEYWORD, ')']]];
1702
  $s = $this->count;
1703
  } else {
1704
  $this->seek($s);
1705
  }
1706
 
1707
+ if (
1708
+ $this->literal('not', 3) &&
1709
+ $this->supportsQuery($subQuery)
1710
+ ) {
1711
  $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]];
1712
  $s = $this->count;
1713
  } else {
1714
  $this->seek($s);
1715
  }
1716
 
1717
+ if (
1718
+ $this->literal('selector(', 9) &&
1719
  $this->selector($selector) &&
1720
+ $this->matchChar(')')
1721
+ ) {
1722
  $support = [Type::T_STRING, '', [[Type::T_KEYWORD, 'selector(']]];
1723
 
1724
  $selectorList = [Type::T_LIST, '', []];
1725
+
1726
  foreach ($selector as $sc) {
1727
  $compound = [Type::T_STRING, '', []];
1728
+
1729
  foreach ($sc as $scp) {
1730
+ if (\is_array($scp)) {
1731
  $compound[2][] = $scp;
1732
  } else {
1733
  $compound[2][] = [Type::T_KEYWORD, $scp];
1734
  }
1735
  }
1736
+
1737
  $selectorList[2][] = $compound;
1738
  }
1739
+
1740
  $support[2][] = $selectorList;
1741
  $support[2][] = [Type::T_KEYWORD, ')'];
1742
  $parts[] = $support;
1752
  $this->seek($s);
1753
  }
1754
 
1755
+ if (
1756
+ $this->literal('and', 3) &&
1757
+ $this->genericList($expressions, 'supportsQuery', ' and', false)
1758
+ ) {
1759
  array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
1760
+
1761
  $parts = [$expressions];
1762
  $s = $this->count;
1763
  } else {
1764
  $this->seek($s);
1765
  }
1766
 
1767
+ if (
1768
+ $this->literal('or', 2) &&
1769
+ $this->genericList($expressions, 'supportsQuery', ' or', false)
1770
+ ) {
1771
  array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
1772
+
1773
  $parts = [$expressions];
1774
  $s = $this->count;
1775
  } else {
1776
  $this->seek($s);
1777
  }
1778
 
1779
+ if (\count($parts)) {
1780
  if ($this->eatWhiteDefault) {
1781
  $this->whitespace();
1782
  }
1783
+
1784
  $out = [Type::T_STRING, '', $parts];
1785
+
1786
  return true;
1787
  }
1788
 
1802
  $s = $this->count;
1803
  $value = null;
1804
 
1805
+ if (
1806
+ $this->matchChar('(') &&
1807
  $this->expression($feature) &&
1808
+ ($this->matchChar(':') &&
1809
+ $this->expression($value) || true) &&
1810
  $this->matchChar(')')
1811
  ) {
1812
  $out = [Type::T_MEDIA_EXPRESSION, $feature];
1832
  */
1833
  protected function argValues(&$out)
1834
  {
1835
+ $discardComments = $this->discardComments;
1836
+ $this->discardComments = true;
1837
+
1838
  if ($this->genericList($list, 'argValue', ',', false)) {
1839
  $out = $list[2];
1840
 
1841
+ $this->discardComments = $discardComments;
1842
+
1843
  return true;
1844
  }
1845
 
1846
+ $this->discardComments = $discardComments;
1847
+
1848
  return false;
1849
  }
1850
 
1863
 
1864
  if (! $this->variable($keyword) || ! $this->matchChar(':')) {
1865
  $this->seek($s);
1866
+
1867
  $keyword = null;
1868
  }
1869
 
1870
+ if ($this->genericList($value, 'expression', '', true)) {
1871
  $out = [$keyword, $value, false];
1872
  $s = $this->count;
1873
 
1884
  }
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)
1892
  {
1893
+ if (\is_array($directiveName) && \is_string(reset($directiveName))) {
1894
+ $directiveName = reset($directiveName);
1895
+ }
1896
+ if (! \is_string($directiveName)) {
1897
+ return false;
1898
+ }
1899
+ if (
1900
+ \in_array($directiveName, [
1901
+ 'at-root',
1902
+ 'media',
1903
+ 'mixin',
1904
+ 'include',
1905
+ 'scssphp-import-once',
1906
+ 'import',
1907
+ 'extend',
1908
+ 'function',
1909
+ 'break',
1910
+ 'continue',
1911
+ 'return',
1912
+ 'each',
1913
+ 'while',
1914
+ 'for',
1915
+ 'if',
1916
+ 'debug',
1917
+ 'warn',
1918
+ 'error',
1919
+ 'content',
1920
+ 'else',
1921
+ 'charset',
1922
+ 'supports',
1923
+ // Todo
1924
+ 'use',
1925
+ 'forward',
1926
+ ])
1927
+ ) {
1928
+ return true;
1929
+ }
1930
+ return false;
1931
  }
1932
 
1933
  /**
1934
+ * Parse directive value list that considers $vars as keyword
1935
  *
1936
+ * @param array $out
1937
+ * @param boolean|string $endChar
 
 
1938
  *
1939
  * @return boolean
1940
  */
1941
+ protected function directiveValue(&$out, $endChar = false)
1942
  {
1943
  $s = $this->count;
 
 
1944
 
1945
+ if ($this->variable($out)) {
1946
+ if ($endChar && $this->matchChar($endChar, false)) {
1947
+ return true;
1948
+ }
1949
 
1950
+ if (! $endChar && $this->end()) {
1951
+ return true;
 
 
1952
  }
1953
  }
1954
 
1955
+ $this->seek($s);
 
1956
 
1957
+ if (\is_string($endChar) && $this->openString($endChar ? $endChar : ';', $out, null, null, true, ";}{")) {
1958
+ if ($endChar && $this->matchChar($endChar, false)) {
1959
+ return true;
1960
+ }
1961
+ $ss = $this->count;
1962
+ if (!$endChar && $this->end()) {
1963
+ $this->seek($ss);
1964
+ return true;
1965
+ }
1966
  }
1967
 
1968
+ $this->seek($s);
1969
+
1970
+ $allowVars = $this->allowVars;
1971
+ $this->allowVars = false;
1972
+
1973
+ $res = $this->genericList($out, 'spaceList', ',');
1974
+ $this->allowVars = $allowVars;
1975
+
1976
+ if ($res) {
1977
+ if ($endChar && $this->matchChar($endChar, false)) {
1978
+ return true;
1979
+ }
1980
+
1981
+ if (! $endChar && $this->end()) {
1982
+ return true;
1983
+ }
1984
  }
1985
 
1986
+ $this->seek($s);
1987
+
1988
+ if ($endChar && $this->matchChar($endChar, false)) {
1989
+ return true;
1990
+ }
1991
+
1992
+ return false;
1993
  }
1994
 
1995
  /**
1996
+ * Parse comma separated value list
1997
  *
1998
  * @param array $out
1999
  *
2000
  * @return boolean
2001
  */
2002
+ protected function valueList(&$out)
2003
  {
2004
+ $discardComments = $this->discardComments;
 
2005
  $this->discardComments = true;
2006
+ $res = $this->genericList($out, 'spaceList', ',');
2007
+ $this->discardComments = $discardComments;
2008
 
2009
+ return $res;
2010
+ }
2011
+
2012
+ /**
2013
+ * Parse a function call, where externals () are part of the call
2014
+ * and not of the value list
2015
+ *
2016
+ * @param $out
2017
+ * @param bool $mandatoryEnclos
2018
+ * @param null|string $charAfter
2019
+ * @param null|bool $eatWhiteSp
2020
+ * @return bool
2021
+ */
2022
+ protected function functionCallArgumentsList(&$out, $mandatoryEnclos = true, $charAfter = null, $eatWhiteSp = null)
2023
+ {
2024
+ $s = $this->count;
2025
+
2026
+ if (
2027
+ $this->matchChar('(') &&
2028
+ $this->valueList($out) &&
2029
+ $this->matchChar(')') &&
2030
+ ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end())
2031
+ ) {
2032
+ return true;
2033
+ }
2034
+
2035
+ if (! $mandatoryEnclos) {
2036
+ $this->seek($s);
2037
+
2038
+ if (
2039
+ $this->valueList($out) &&
2040
+ ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end())
2041
+ ) {
2042
+ return true;
2043
+ }
2044
+ }
2045
+
2046
+ $this->seek($s);
2047
+
2048
+ return false;
2049
+ }
2050
+
2051
+ /**
2052
+ * Parse space separated value list
2053
+ *
2054
+ * @param array $out
2055
+ *
2056
+ * @return boolean
2057
+ */
2058
+ protected function spaceList(&$out)
2059
+ {
2060
+ return $this->genericList($out, 'expression');
2061
+ }
2062
+
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
+ */
2073
+ protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
2074
+ {
2075
+ $s = $this->count;
2076
+ $items = [];
2077
+ $value = null;
2078
+
2079
+ while ($this->$parseItem($value)) {
2080
+ $trailing_delim = false;
2081
+ $items[] = $value;
2082
+
2083
+ if ($delim) {
2084
+ if (! $this->literal($delim, \strlen($delim))) {
2085
+ break;
2086
+ }
2087
+
2088
+ $trailing_delim = true;
2089
+ } else {
2090
+ // if no delim watch that a keyword didn't eat the single/double quote
2091
+ // from the following starting string
2092
+ if ($value[0] === Type::T_KEYWORD) {
2093
+ $word = $value[1];
2094
+
2095
+ $last_char = substr($word, -1);
2096
+
2097
+ if (
2098
+ strlen($word) > 1 &&
2099
+ in_array($last_char, [ "'", '"']) &&
2100
+ substr($word, -2, 1) !== '\\'
2101
+ ) {
2102
+ // if there is a non escaped opening quote in the keyword, this seems unlikely a mistake
2103
+ $word = str_replace('\\' . $last_char, '\\\\', $word);
2104
+ if (strpos($word, $last_char) < strlen($word) - 1) {
2105
+ continue;
2106
+ }
2107
+
2108
+ $currentCount = $this->count;
2109
+
2110
+ // let's try to rewind to previous char and try a parse
2111
+ $this->count--;
2112
+ // in case the keyword also eat spaces
2113
+ while (substr($this->buffer, $this->count, 1) !== $last_char) {
2114
+ $this->count--;
2115
+ }
2116
+
2117
+ $nextValue = null;
2118
+ if ($this->$parseItem($nextValue)) {
2119
+ if ($nextValue[0] === Type::T_KEYWORD && $nextValue[1] === $last_char) {
2120
+ // bad try, forget it
2121
+ $this->seek($currentCount);
2122
+ continue;
2123
+ }
2124
+ if ($nextValue[0] !== Type::T_STRING) {
2125
+ // bad try, forget it
2126
+ $this->seek($currentCount);
2127
+ continue;
2128
+ }
2129
+
2130
+ // OK it was a good idea
2131
+ $value[1] = substr($value[1], 0, -1);
2132
+ array_pop($items);
2133
+ $items[] = $value;
2134
+ $items[] = $nextValue;
2135
+ } else {
2136
+ // bad try, forget it
2137
+ $this->seek($currentCount);
2138
+ continue;
2139
+ }
2140
+ }
2141
+ }
2142
  }
2143
+ }
2144
 
2145
+ if (! $items) {
2146
  $this->seek($s);
2147
+
2148
+ return false;
2149
  }
2150
 
2151
+ if ($trailing_delim) {
2152
+ $items[] = [Type::T_NULL];
2153
+ }
2154
+
2155
+ if ($flatten && \count($items) === 1) {
2156
+ $out = $items[0];
2157
+ } else {
2158
+ $out = [Type::T_LIST, $delim, $items];
2159
+ }
2160
+
2161
+ return true;
2162
+ }
2163
+
2164
+ /**
2165
+ * Parse expression
2166
+ *
2167
+ * @param array $out
2168
+ * @param boolean $listOnly
2169
+ * @param boolean $lookForExp
2170
+ *
2171
+ * @return boolean
2172
+ */
2173
+ protected function expression(&$out, $listOnly = false, $lookForExp = true)
2174
+ {
2175
+ $s = $this->count;
2176
+ $discard = $this->discardComments;
2177
+ $this->discardComments = true;
2178
+ $allowedTypes = ($listOnly ? [Type::T_LIST] : [Type::T_LIST, Type::T_MAP]);
2179
+
2180
+ if ($this->matchChar('(')) {
2181
+ if ($this->enclosedExpression($lhs, $s, ')', $allowedTypes)) {
2182
+ if ($lookForExp) {
2183
+ $out = $this->expHelper($lhs, 0);
2184
+ } else {
2185
+ $out = $lhs;
2186
+ }
2187
+
2188
+ $this->discardComments = $discard;
2189
+
2190
+ return true;
2191
+ }
2192
+
2193
+ $this->seek($s);
2194
+ }
2195
+
2196
+ if (\in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) {
2197
+ if ($this->enclosedExpression($lhs, $s, ']', [Type::T_LIST])) {
2198
+ if ($lookForExp) {
2199
+ $out = $this->expHelper($lhs, 0);
2200
+ } else {
2201
+ $out = $lhs;
2202
  }
2203
 
2204
  $this->discardComments = $discard;
2205
+
2206
  return true;
2207
  }
2208
 
2209
  $this->seek($s);
2210
  }
2211
 
2212
+ if (! $listOnly && $this->value($lhs)) {
2213
+ if ($lookForExp) {
2214
+ $out = $this->expHelper($lhs, 0);
2215
+ } else {
2216
+ $out = $lhs;
2217
+ }
2218
 
2219
  $this->discardComments = $discard;
2220
+
2221
  return true;
2222
  }
2223
 
2224
  $this->discardComments = $discard;
2225
+
2226
  return false;
2227
  }
2228
 
2236
  *
2237
  * @return boolean
2238
  */
2239
+ protected function enclosedExpression(&$out, $s, $closingParen = ')', $allowedTypes = [Type::T_LIST, Type::T_MAP])
2240
  {
2241
+ if ($this->matchChar($closingParen) && \in_array(Type::T_LIST, $allowedTypes)) {
2242
  $out = [Type::T_LIST, '', []];
2243
 
2244
+ switch ($closingParen) {
2245
+ case ')':
2246
+ $out['enclosing'] = 'parent'; // parenthesis list
2247
+ break;
2248
+
2249
+ case ']':
2250
+ $out['enclosing'] = 'bracket'; // bracketed list
2251
+ break;
2252
+ }
2253
+
2254
  return true;
2255
  }
2256
 
2257
+ if (
2258
+ $this->valueList($out) &&
2259
+ $this->matchChar($closingParen) && ! ($closingParen === ')' &&
2260
+ \in_array($out[0], [Type::T_EXPRESSION, Type::T_UNARY])) &&
2261
+ \in_array(Type::T_LIST, $allowedTypes)
2262
+ ) {
2263
+ if ($out[0] !== Type::T_LIST || ! empty($out['enclosing'])) {
2264
+ $out = [Type::T_LIST, '', [$out]];
2265
+ }
2266
+
2267
+ switch ($closingParen) {
2268
+ case ')':
2269
+ $out['enclosing'] = 'parent'; // parenthesis list
2270
+ break;
2271
+
2272
+ case ']':
2273
+ $out['enclosing'] = 'bracket'; // bracketed list
2274
+ break;
2275
+ }
2276
+
2277
  return true;
2278
  }
2279
 
2280
  $this->seek($s);
2281
 
2282
+ if (\in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) {
2283
  return true;
2284
  }
2285
 
2317
  break;
2318
  }
2319
 
2320
+ if (! $this->value($rhs) && ! $this->expression($rhs, true, false)) {
2321
+ break;
2322
+ }
2323
+
2324
+ if ($op === '-' && ! $whiteAfter && $rhs[0] === Type::T_KEYWORD) {
2325
  break;
2326
  }
2327
 
2331
  }
2332
 
2333
  $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
2334
+
2335
  $ss = $this->count;
2336
  $whiteBefore = isset($this->buffer[$this->count - 1]) &&
2337
  ctype_space($this->buffer[$this->count - 1]);
2358
  $s = $this->count;
2359
  $char = $this->buffer[$this->count];
2360
 
2361
+ if (
2362
+ $this->literal('url(', 4) &&
2363
+ $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)
2364
+ ) {
2365
  $len = strspn(
2366
  $this->buffer,
2367
  'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=',
2380
 
2381
  $this->seek($s);
2382
 
2383
+ if (
2384
+ $this->literal('url(', 4, false) &&
2385
+ $this->match('\s*(\/\/[^\s\)]+)\s*', $m)
2386
+ ) {
2387
  $content = 'url(' . $m[1];
2388
 
2389
  if ($this->matchChar(')')) {
2398
 
2399
  // not
2400
  if ($char === 'n' && $this->literal('not', 3, false)) {
2401
+ if (
2402
+ $this->whitespace() &&
2403
+ $this->value($inner)
2404
+ ) {
2405
  $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
2406
 
2407
  return true;
2422
  if ($char === '+') {
2423
  $this->count++;
2424
 
2425
+ $follow_white = $this->whitespace();
2426
+
2427
  if ($this->value($inner)) {
2428
  $out = [Type::T_UNARY, '+', $inner, $this->inParens];
2429
 
2430
  return true;
2431
  }
2432
 
2433
+ if ($follow_white) {
2434
+ $out = [Type::T_KEYWORD, $char];
2435
+ return true;
2436
+ }
2437
+
2438
+ $this->seek($s);
2439
 
2440
  return false;
2441
  }
2442
 
2443
  // negation
2444
  if ($char === '-') {
2445
+ if ($this->customProperty($out)) {
2446
+ return true;
2447
+ }
2448
+
2449
  $this->count++;
2450
 
2451
+ $follow_white = $this->whitespace();
2452
+
2453
  if ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner)) {
2454
  $out = [Type::T_UNARY, '-', $inner, $this->inParens];
2455
 
2456
  return true;
2457
  }
2458
 
2459
+ if (
2460
+ $this->keyword($inner) &&
2461
+ ! $this->func($inner, $out)
2462
+ ) {
2463
+ $out = [Type::T_UNARY, '-', $inner, $this->inParens];
2464
+
2465
+ return true;
2466
+ }
2467
+
2468
+ if ($follow_white) {
2469
+ $out = [Type::T_KEYWORD, $char];
2470
+
2471
+ return true;
2472
+ }
2473
+
2474
+ $this->seek($s);
2475
  }
2476
 
2477
  // paren
2483
  if ($this->interpolation($out) || $this->color($out)) {
2484
  return true;
2485
  }
2486
+
2487
+ $this->count++;
2488
+
2489
+ if ($this->keyword($keyword)) {
2490
+ $out = [Type::T_KEYWORD, '#' . $keyword];
2491
+
2492
+ return true;
2493
+ }
2494
+
2495
+ $this->count--;
2496
  }
2497
 
2498
  if ($this->matchChar('&', true)) {
2518
  }
2519
 
2520
  // unicode range with wildcards
2521
+ if (
2522
+ $this->literal('U+', 2) &&
2523
+ $this->match('\?+|([0-9A-F]+(\?+|(-[0-9A-F]+))?)', $m, false)
2524
+ ) {
2525
+ $unicode = explode('-', $m[0]);
2526
+ if (strlen(reset($unicode)) <= 6 && strlen(end($unicode)) <= 6) {
2527
+ $out = [Type::T_KEYWORD, 'U+' . $m[0]];
2528
+
2529
+ return true;
2530
+ }
2531
+ $this->count -= strlen($m[0]) + 2;
2532
  }
2533
 
2534
  if ($this->keyword($keyword, false)) {
2572
 
2573
  $this->inParens = true;
2574
 
2575
+ if (
2576
+ $this->expression($exp) &&
2577
+ $this->matchChar(')')
2578
+ ) {
2579
  $out = $exp;
2580
  $this->inParens = $inParens;
2581
 
2600
  {
2601
  $s = $this->count;
2602
 
2603
+ if (
2604
+ $this->literal('progid:', 7, false) &&
2605
  $this->openString('(', $fn) &&
2606
  $this->matchChar('(')
2607
  ) {
2643
  if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
2644
  $ss = $this->count;
2645
 
2646
+ if (
2647
+ $this->argValues($args) &&
2648
+ $this->matchChar(')')
2649
+ ) {
2650
  $func = [Type::T_FUNCTION_CALL, $name, $args];
2651
 
2652
  return true;
2655
  $this->seek($ss);
2656
  }
2657
 
2658
+ if (
2659
+ ($this->openString(')', $str, '(') || true) &&
2660
  $this->matchChar(')')
2661
  ) {
2662
  $args = [];
2691
  $args = [];
2692
 
2693
  while ($this->keyword($var)) {
2694
+ if (
2695
+ $this->matchChar('=') &&
2696
+ $this->expression($exp)
2697
+ ) {
2698
  $args[] = [Type::T_STRING, '', [$var . '=']];
2699
  $arg = $exp;
2700
  } else {
2740
 
2741
  $ss = $this->count;
2742
 
2743
+ if (
2744
+ $this->matchChar(':') &&
2745
+ $this->genericList($defaultVal, 'expression', '', true)
2746
+ ) {
2747
  $arg[1] = $defaultVal;
2748
  } else {
2749
  $this->seek($ss);
2759
  }
2760
 
2761
  $arg[2] = true;
2762
+
2763
  $this->seek($sss);
2764
  } else {
2765
  $this->seek($ss);
2801
  $keys = [];
2802
  $values = [];
2803
 
2804
+ while (
2805
+ $this->genericList($key, 'expression', '', true) &&
2806
+ $this->matchChar(':') &&
2807
+ $this->genericList($value, 'expression', '', true)
2808
  ) {
2809
  $keys[] = $key;
2810
  $values[] = $value;
2834
  */
2835
  protected function color(&$out)
2836
  {
2837
+ $s = $this->count;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2838
 
2839
+ if ($this->match('(#([0-9a-f]+)\b)', $m)) {
2840
+ if (\in_array(\strlen($m[2]), [3,4,6,8])) {
2841
+ $out = [Type::T_KEYWORD, $m[0]];
2842
 
2843
+ return true;
 
 
 
 
 
2844
  }
2845
 
2846
+ $this->seek($s);
2847
 
2848
+ return false;
2849
  }
2850
 
2851
  return false;
2863
  $s = $this->count;
2864
 
2865
  if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m, false)) {
2866
+ if (\strlen($this->buffer) === $this->count || ! ctype_digit($this->buffer[$this->count])) {
2867
  $this->whitespace();
2868
 
2869
  $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
2884
  *
2885
  * @return boolean
2886
  */
2887
+ protected function string(&$out, $keepDelimWithInterpolation = false)
2888
  {
2889
  $s = $this->count;
2890
 
2907
  }
2908
 
2909
  if ($m[2] === '#{') {
2910
+ $this->count -= \strlen($m[2]);
2911
 
2912
  if ($this->interpolation($inter, false)) {
2913
  $content[] = $inter;
2914
  $hasInterpolation = true;
2915
  } else {
2916
+ $this->count += \strlen($m[2]);
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.
2924
+ # To include a newline in a string, use "\a" or "\a " as in CSS.
2925
+ if ($this->matchChar("\n", false)) {
2926
+ $content[] = ' ';
2927
+ }
2928
  } elseif ($m[2] === '\\') {
2929
+ if (
2930
+ $this->literal("\r\n", 2, false) ||
2931
+ $this->matchChar("\r", false) ||
2932
+ $this->matchChar("\n", false) ||
2933
+ $this->matchChar("\f", false)
 
 
 
 
 
2934
  ) {
2935
  // this is a continuation escaping, to be ignored
2936
+ } elseif ($this->matchEscapeCharacter($c)) {
2937
+ $content[] = $c;
2938
  } else {
2939
+ $this->throwParseError('Unterminated escape sequence');
2940
  }
2941
  } else {
2942
+ $this->count -= \strlen($delim);
2943
  break; // delim
2944
  }
2945
  }
2946
 
2947
  $this->eatWhiteDefault = $oldWhite;
2948
 
2949
+ if ($this->literal($delim, \strlen($delim))) {
2950
+ if ($hasInterpolation && ! $keepDelimWithInterpolation) {
2951
  $delim = '"';
 
 
 
 
 
 
 
 
 
 
2952
  }
2953
 
2954
  $out = [Type::T_STRING, $delim, $content];
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
+
2969
+ for ($i = 5; $i--;) {
2970
+ if ($this->match('[a-f0-9]', $m, false)) {
2971
+ $hex .= $m[0];
2972
+ } else {
2973
+ break;
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
+ }
2984
+
2985
+ return true;
2986
+ }
2987
+
2988
+ if ($this->match('.', $m, false)) {
2989
+ $out = $m[0];
2990
+
2991
+ return true;
2992
+ }
2993
+
2994
+ return false;
2995
+ }
2996
+
2997
  /**
2998
  * Parse keyword or interpolation
2999
  *
3041
  /**
3042
  * Parse an unbounded string stopped by $end
3043
  *
3044
+ * @param string $end
3045
+ * @param array $out
3046
+ * @param string $nestOpen
3047
+ * @param string $nestClose
3048
+ * @param boolean $rtrim
3049
+ * @param string $disallow
3050
  *
3051
  * @return boolean
3052
  */
3053
+ protected function openString($end, &$out, $nestOpen = null, $nestClose = null, $rtrim = true, $disallow = null)
3054
  {
3055
  $oldWhite = $this->eatWhiteDefault;
3056
  $this->eatWhiteDefault = false;
3057
 
3058
+ if ($nestOpen && ! $nestClose) {
3059
+ $nestClose = $end;
3060
+ }
3061
+
3062
+ $patt = ($disallow ? '[^' . $this->pregQuote($disallow) . ']' : '.');
3063
+ $patt = '(' . $patt . '*?)([\'"]|#\{|'
3064
+ . $this->pregQuote($end) . '|'
3065
+ . (($nestClose && $nestClose !== $end) ? $this->pregQuote($nestClose) . '|' : '')
3066
+ . static::$commentPattern . ')';
3067
 
3068
  $nestingLevel = 0;
3069
 
3073
  if (isset($m[1]) && $m[1] !== '') {
3074
  $content[] = $m[1];
3075
 
3076
+ if ($nestOpen) {
3077
+ $nestingLevel += substr_count($m[1], $nestOpen);
3078
  }
3079
  }
3080
 
3081
  $tok = $m[2];
3082
 
3083
+ $this->count -= \strlen($tok);
3084
 
3085
+ if ($tok === $end && ! $nestingLevel) {
3086
  break;
3087
  }
3088
 
3089
+ if ($tok === $nestClose) {
3090
+ $nestingLevel--;
3091
+ }
3092
+
3093
+ if (($tok === "'" || $tok === '"') && $this->string($str, true)) {
3094
  $content[] = $str;
3095
  continue;
3096
  }
3101
  }
3102
 
3103
  $content[] = $tok;
3104
+ $this->count += \strlen($tok);
3105
  }
3106
 
3107
  $this->eatWhiteDefault = $oldWhite;
3108
 
3109
+ if (! $content || $tok !== $end) {
3110
  return false;
3111
  }
3112
 
3113
  // trim the end
3114
+ if ($rtrim && \is_string(end($content))) {
3115
+ $content[\count($content) - 1] = rtrim(end($content));
3116
  }
3117
 
3118
  $out = [Type::T_STRING, '', $content];
3123
  /**
3124
  * Parser interpolation
3125
  *
3126
+ * @param string|array $out
3127
+ * @param boolean $lookWhite save information about whitespace before and after
3128
  *
3129
  * @return boolean
3130
  */
3131
  protected function interpolation(&$out, $lookWhite = true)
3132
  {
3133
  $oldWhite = $this->eatWhiteDefault;
3134
+ $allowVars = $this->allowVars;
3135
+ $this->allowVars = true;
3136
  $this->eatWhiteDefault = true;
3137
 
3138
  $s = $this->count;
3139
 
3140
+ if (
3141
+ $this->literal('#{', 2) &&
3142
+ $this->valueList($value) &&
3143
+ $this->matchChar('}', false)
3144
+ ) {
3145
  if ($value === [Type::T_SELF]) {
3146
  $out = $value;
3147
  } else {
3148
  if ($lookWhite) {
3149
  $left = ($s > 0 && preg_match('/\s/', $this->buffer[$s - 1])) ? ' ' : '';
3150
+ $right = (
3151
+ ! empty($this->buffer[$this->count]) &&
3152
+ preg_match('/\s/', $this->buffer[$this->count])
3153
+ ) ? ' ' : '';
3154
  } else {
3155
  $left = $right = false;
3156
  }
3159
  }
3160
 
3161
  $this->eatWhiteDefault = $oldWhite;
3162
+ $this->allowVars = $allowVars;
3163
 
3164
  if ($this->eatWhiteDefault) {
3165
  $this->whitespace();
3171
  $this->seek($s);
3172
 
3173
  $this->eatWhiteDefault = $oldWhite;
3174
+ $this->allowVars = $allowVars;
3175
 
3176
  return false;
3177
  }
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]);
3224
  }
3225
  }
3226
 
3232
  }
3233
 
3234
  /**
3235
+ * Parse custom property name (as an array of parts or a string)
3236
  *
3237
  * @param array $out
3238
  *
3239
  * @return boolean
3240
  */
3241
+ protected function customProperty(&$out)
3242
+ {
3243
+ $s = $this->count;
3244
+
3245
+ if (! $this->literal('--', 2, false)) {
3246
+ return false;
3247
+ }
3248
+
3249
+ $parts = ['--'];
3250
+
3251
+ $oldWhite = $this->eatWhiteDefault;
3252
+ $this->eatWhiteDefault = false;
3253
+
3254
+ for (;;) {
3255
+ if ($this->interpolation($inter)) {
3256
+ $parts[] = $inter;
3257
+ continue;
3258
+ }
3259
+
3260
+ if ($this->matchChar('&', false)) {
3261
+ $parts[] = [Type::T_SELF];
3262
+ continue;
3263
+ }
3264
+
3265
+ if ($this->variable($var)) {
3266
+ $parts[] = $var;
3267
+ continue;
3268
+ }
3269
+
3270
+ if ($this->keyword($text)) {
3271
+ $parts[] = $text;
3272
+ continue;
3273
+ }
3274
+
3275
+ break;
3276
+ }
3277
+
3278
+ $this->eatWhiteDefault = $oldWhite;
3279
+
3280
+ if (\count($parts) == 1) {
3281
+ $this->seek($s);
3282
+
3283
+ return false;
3284
+ }
3285
+
3286
+ $this->whitespace(); // get any extra whitespace
3287
+
3288
+ $out = [Type::T_STRING, '', $parts];
3289
+
3290
+ return true;
3291
+ }
3292
+
3293
+ /**
3294
+ * Parse comma separated selector list
3295
+ *
3296
+ * @param array $out
3297
+ * @param boolean $subSelector
3298
+ *
3299
+ * @return boolean
3300
+ */
3301
  protected function selectors(&$out, $subSelector = false)
3302
  {
3303
  $s = $this->count;
3329
  /**
3330
  * Parse whitespace separated selector list
3331
  *
3332
+ * @param array $out
3333
+ * @param boolean $subSelector
3334
  *
3335
  * @return boolean
3336
  */
3339
  $selector = [];
3340
 
3341
  for (;;) {
3342
+ $s = $this->count;
3343
+
3344
  if ($this->match('[>+~]+', $m, true)) {
3345
+ if (
3346
+ $subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0 &&
3347
+ $m[0] === '+' && $this->match("(\d+|n\b)", $counter)
3348
+ ) {
3349
+ $this->seek($s);
3350
+ } else {
3351
+ $selector[] = [$m[0]];
3352
+ continue;
3353
+ }
3354
  }
3355
 
3356
  if ($this->selectorSingle($part, $subSelector)) {
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
  */
3422
  case '&':
3423
  $parts[] = Compiler::$selfSelector;
3424
  $this->count++;
3425
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
3426
  continue 2;
3427
 
3428
  case '.':
3447
  if ($this->placeholder($placeholder)) {
3448
  $parts[] = '%';
3449
  $parts[] = $placeholder;
3450
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
3451
  continue;
3452
  }
3453
 
3457
  if ($char === '#') {
3458
  if ($this->interpolation($inter)) {
3459
  $parts[] = $inter;
3460
+ ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
3461
  continue;
3462
  }
3463
 
3485
 
3486
  $ss = $this->count;
3487
 
3488
+ if (
3489
+ $nameParts === ['not'] ||
3490
+ $nameParts === ['is'] ||
3491
+ $nameParts === ['has'] ||
3492
+ $nameParts === ['where'] ||
3493
+ $nameParts === ['slotted'] ||
3494
+ $nameParts === ['nth-child'] ||
3495
+ $nameParts === ['nth-last-child'] ||
3496
+ $nameParts === ['nth-of-type'] ||
3497
+ $nameParts === ['nth-last-of-type']
3498
  ) {
3499
+ if (
3500
+ $this->matchChar('(', true) &&
3501
+ ($this->selectors($subs, reset($nameParts)) || true) &&
3502
+ $this->matchChar(')')
3503
  ) {
3504
  $parts[] = '(';
3505
 
3508
  foreach ($ps as &$p) {
3509
  $parts[] = $p;
3510
  }
3511
+
3512
+ if (\count($sub) && reset($sub)) {
3513
  $parts[] = ' ';
3514
  }
3515
  }
3516
+
3517
+ if (\count($subs) && reset($subs)) {
3518
  $parts[] = ', ';
3519
  }
3520
  }
3523
  } else {
3524
  $this->seek($ss);
3525
  }
3526
+ } elseif (
3527
+ $this->matchChar('(') &&
3528
+ ($this->openString(')', $str, '(') || true) &&
3529
+ $this->matchChar(')')
3530
+ ) {
3531
+ $parts[] = '(';
 
 
 
 
3532
 
3533
+ if (! empty($str)) {
3534
+ $parts[] = $str;
 
3535
  }
3536
+
3537
+ $parts[] = ')';
3538
+ } else {
3539
+ $this->seek($ss);
3540
  }
3541
 
3542
  continue;
3545
 
3546
  $this->seek($s);
3547
 
3548
+ // 2n+1
3549
+ if ($subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0) {
3550
+ if ($this->match("(\s*(\+\s*|\-\s*)?(\d+|n|\d+n))+", $counter)) {
3551
+ $parts[] = $counter[0];
3552
+ //$parts[] = str_replace(' ', '', $counter[0]);
3553
+ continue;
3554
+ }
3555
+ }
3556
+
3557
+ $this->seek($s);
3558
+
3559
  // attribute selector
3560
+ if (
3561
+ $char === '[' &&
3562
  $this->matchChar('[') &&
3563
  ($this->openString(']', $str, '[') || true) &&
3564
  $this->matchChar(']')
3611
  {
3612
  $s = $this->count;
3613
 
3614
+ if (
3615
+ $this->matchChar('$', false) &&
3616
+ $this->keyword($name)
3617
+ ) {
3618
+ if ($this->allowVars) {
3619
+ $out = [Type::T_VARIABLE, $name];
3620
+ } else {
3621
+ $out = [Type::T_KEYWORD, '$' . $name];
3622
+ }
3623
 
3624
  return true;
3625
  }
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;
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
 
3680
  /**
3681
  * Parse a placeholder
3682
  *
3683
+ * @param string|array $placeholder
3684
  *
3685
  * @return boolean
3686
  */
3687
  protected function placeholder(&$placeholder)
3688
  {
3689
+ $match = $this->match(
3690
  $this->utf8
3691
  ? '([\pL\w\-_]+)'
3692
  : '([\w\-_]+)',
3693
  $m
3694
+ );
3695
+
3696
+ if ($match) {
3697
  $placeholder = $m[1];
3698
 
3699
  return true;
3700
  }
3701
+
3702
  if ($this->interpolation($placeholder)) {
3703
  return true;
3704
  }
3715
  */
3716
  protected function url(&$out)
3717
  {
3718
+ if ($this->literal('url(', 4)) {
3719
+ $s = $this->count;
3720
 
3721
+ if (
3722
+ ($this->string($out) || $this->spaceList($out)) &&
3723
+ $this->matchChar(')')
3724
+ ) {
3725
+ $out = [Type::T_STRING, '', ['url(', $out, ')']];
3726
+
3727
+ return true;
3728
+ }
3729
+
3730
+ $this->seek($s);
3731
+
3732
+ if (
3733
+ $this->openString(')', $out) &&
3734
+ $this->matchChar(')')
3735
+ ) {
3736
+ $out = [Type::T_STRING, '', ['url(', $out, ')']];
3737
+
3738
+ return true;
3739
+ }
3740
  }
3741
 
3742
  return false;
3744
 
3745
  /**
3746
  * Consume an end of statement delimiter
3747
+ * @param bool $eatWhitespace
3748
  *
3749
  * @return boolean
3750
  */
3751
+ protected function end($eatWhitespace = null)
3752
  {
3753
+ if ($this->matchChar(';', $eatWhitespace)) {
3754
  return true;
3755
  }
3756
 
3757
+ if ($this->count === \strlen($this->buffer) || $this->buffer[$this->count] === '}') {
3758
  // if there is end of file or a closing block next then we don't need a ;
3759
  return true;
3760
  }
3773
  {
3774
  $flags = [];
3775
 
3776
+ for ($token = &$value; $token[0] === Type::T_LIST && ($s = \count($token[2])); $token = &$lastNode) {
3777
  $lastNode = &$token[2][$s - 1];
3778
 
3779
+ while ($lastNode[0] === Type::T_KEYWORD && \in_array($lastNode[1], ['!default', '!global'])) {
3780
  array_pop($token[2]);
3781
 
3782
+ $node = end($token[2]);
3783
+ $token = $this->flattenList($token);
3784
+ $flags[] = $lastNode[1];
 
 
 
3785
  $lastNode = $node;
3786
  }
3787
  }
3799
  protected function stripOptionalFlag(&$selectors)
3800
  {
3801
  $optional = false;
 
3802
  $selector = end($selectors);
3803
+ $part = end($selector);
3804
 
3805
  if ($part === ['!optional']) {
3806
+ array_pop($selectors[\count($selectors) - 1]);
3807
 
3808
  $optional = true;
3809
  }
3820
  */
3821
  protected function flattenList($value)
3822
  {
3823
+ if ($value[0] === Type::T_LIST && \count($value[2]) === 1) {
3824
  return $this->flattenList($value[2][0]);
3825
  }
3826
 
3827
  return $value;
3828
  }
3829
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3830
  /**
3831
  * Quote regular expression
3832
  *
3854
  $prev = $pos + 1;
3855
  }
3856
 
3857
+ $this->sourcePositions[] = \strlen($buffer);
3858
 
3859
  if (substr($buffer, -1) !== "\n") {
3860
+ $this->sourcePositions[] = \strlen($buffer) + 1;
3861
  }
3862
  }
3863
 
3871
  private function getSourcePosition($pos)
3872
  {
3873
  $low = 0;
3874
+ $high = \count($this->sourcePositions);
3875
 
3876
  while ($low < $high) {
3877
  $mid = (int) (($high + $low) / 2);
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');
3909
  */
3910
  private function restoreEncoding()
3911
  {
3912
+ if (\extension_loaded('mbstring') && $this->encoding) {
3913
  mb_internal_encoding($this->encoding);
3914
  }
3915
  }
scssphp/src/SourceMap/Base64.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
scssphp/src/SourceMap/Base64VLQ.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -61,7 +62,9 @@ class Base64VLQ
61
 
62
  do {
63
  $digit = $vlq & self::VLQ_BASE_MASK;
64
- $vlq >>= self::VLQ_BASE_SHIFT;
 
 
65
 
66
  if ($vlq > 0) {
67
  $digit |= self::VLQ_CONTINUATION_BIT;
@@ -130,8 +133,19 @@ class Base64VLQ
130
  private static function fromVLQSigned($value)
131
  {
132
  $negate = ($value & 1) === 1;
133
- $value = $value >> 1;
134
 
135
- return $negate ? -$value : $value;
 
 
 
 
 
 
 
 
 
 
 
 
136
  }
137
  }
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
62
 
63
  do {
64
  $digit = $vlq & self::VLQ_BASE_MASK;
65
+
66
+ //$vlq >>>= self::VLQ_BASE_SHIFT; // unsigned right shift
67
+ $vlq = (($vlq >> 1) & PHP_INT_MAX) >> (self::VLQ_BASE_SHIFT - 1);
68
 
69
  if ($vlq > 0) {
70
  $digit |= self::VLQ_CONTINUATION_BIT;
133
  private static function fromVLQSigned($value)
134
  {
135
  $negate = ($value & 1) === 1;
 
136
 
137
+ //$value >>>= 1; // unsigned right shift
138
+ $value = ($value >> 1) & PHP_INT_MAX;
139
+
140
+ if (! $negate) {
141
+ return $value;
142
+ }
143
+
144
+ // We need to OR 0x80000000 here to ensure the 32nd bit (the sign bit) is
145
+ // always set for negative numbers. If `value` were 1, (meaning `negate` is
146
+ // true and all other bits were zeros), `value` would now be 0. -0 is just
147
+ // 0, and doesn't flip the 32nd bit as intended. All positive numbers will
148
+ // successfully flip the 32nd bit without issue, so it's a noop for them.
149
+ return -$value | 0x80000000;
150
  }
151
  }
scssphp/src/SourceMap/SourceMapGenerator.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -132,7 +133,7 @@ class SourceMapGenerator
132
  public function saveMap($content)
133
  {
134
  $file = $this->options['sourceMapWriteTo'];
135
- $dir = dirname($file);
136
 
137
  // directory does not exist
138
  if (! is_dir($dir)) {
@@ -201,7 +202,7 @@ class SourceMapGenerator
201
  }
202
 
203
  // less.js compat fixes
204
- if (count($sourceMap['sources']) && empty($sourceMap['sourceRoot'])) {
205
  unset($sourceMap['sourceRoot']);
206
  }
207
 
@@ -235,7 +236,7 @@ class SourceMapGenerator
235
  */
236
  public function generateMappings()
237
  {
238
- if (! count($this->mappings)) {
239
  return '';
240
  }
241
 
@@ -249,6 +250,7 @@ class SourceMapGenerator
249
  }
250
 
251
  ksort($groupedMap);
 
252
  $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
253
 
254
  foreach ($groupedMap as $lineNumber => $lineMap) {
@@ -313,8 +315,8 @@ class SourceMapGenerator
313
  $basePath = $this->options['sourceMapBasepath'];
314
 
315
  // "Trim" the 'sourceMapBasepath' from the output filename.
316
- if (strlen($basePath) && strpos($filename, $basePath) === 0) {
317
- $filename = substr($filename, strlen($basePath));
318
  }
319
 
320
  // Remove extra leading path separators.
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
133
  public function saveMap($content)
134
  {
135
  $file = $this->options['sourceMapWriteTo'];
136
+ $dir = \dirname($file);
137
 
138
  // directory does not exist
139
  if (! is_dir($dir)) {
202
  }
203
 
204
  // less.js compat fixes
205
+ if (\count($sourceMap['sources']) && empty($sourceMap['sourceRoot'])) {
206
  unset($sourceMap['sourceRoot']);
207
  }
208
 
236
  */
237
  public function generateMappings()
238
  {
239
+ if (! \count($this->mappings)) {
240
  return '';
241
  }
242
 
250
  }
251
 
252
  ksort($groupedMap);
253
+
254
  $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
255
 
256
  foreach ($groupedMap as $lineNumber => $lineMap) {
315
  $basePath = $this->options['sourceMapBasepath'];
316
 
317
  // "Trim" the 'sourceMapBasepath' from the output filename.
318
+ if (\strlen($basePath) && strpos($filename, $basePath) === 0) {
319
+ $filename = substr($filename, \strlen($basePath));
320
  }
321
 
322
  // Remove extra leading path separators.
scssphp/src/Type.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -27,6 +28,7 @@ class Type
27
  const T_COMMENT = 'comment';
28
  const T_CONTINUE = 'continue';
29
  const T_CONTROL = 'control';
 
30
  const T_DEBUG = 'debug';
31
  const T_DIRECTIVE = 'directive';
32
  const T_EACH = 'each';
@@ -37,6 +39,7 @@ class Type
37
  const T_EXTEND = 'extend';
38
  const T_FOR = 'for';
39
  const T_FUNCTION = 'function';
 
40
  const T_FUNCTION_CALL = 'fncall';
41
  const T_HSL = 'hsl';
42
  const T_IF = 'if';
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
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';
33
  const T_DIRECTIVE = 'directive';
34
  const T_EACH = 'each';
39
  const T_EXTEND = 'extend';
40
  const T_FOR = 'for';
41
  const T_FUNCTION = 'function';
42
+ const T_FUNCTION_REFERENCE = 'function-reference';
43
  const T_FUNCTION_CALL = 'fncall';
44
  const T_HSL = 'hsl';
45
  const T_IF = 'if';
scssphp/src/Util.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -39,6 +40,10 @@ class Util
39
  $val = $value[1];
40
  $grace = new Range(-0.00001, 0.00001);
41
 
 
 
 
 
42
  if ($range->includes($val)) {
43
  return $val;
44
  }
@@ -67,4 +72,89 @@ class Util
67
 
68
  return strtr(rawurlencode($string), $revert);
69
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  }
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
40
  $val = $value[1];
41
  $grace = new Range(-0.00001, 0.00001);
42
 
43
+ if (! \is_numeric($val)) {
44
+ throw new RangeException("$name {$val} is not a number.");
45
+ }
46
+
47
  if ($range->includes($val)) {
48
  return $val;
49
  }
72
 
73
  return strtr(rawurlencode($string), $revert);
74
  }
75
+
76
+ /**
77
+ * mb_chr() wrapper
78
+ *
79
+ * @param integer $code
80
+ *
81
+ * @return string
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
+
90
+ if (0x80 > $code %= 0x200000) {
91
+ $s = \chr($code);
92
+ } elseif (0x800 > $code) {
93
+ $s = \chr(0xC0 | $code >> 6) . \chr(0x80 | $code & 0x3F);
94
+ } elseif (0x10000 > $code) {
95
+ $s = \chr(0xE0 | $code >> 12) . \chr(0x80 | $code >> 6 & 0x3F) . \chr(0x80 | $code & 0x3F);
96
+ } else {
97
+ $s = \chr(0xF0 | $code >> 18) . \chr(0x80 | $code >> 12 & 0x3F)
98
+ . \chr(0x80 | $code >> 6 & 0x3F) . \chr(0x80 | $code & 0x3F);
99
+ }
100
+
101
+ return $s;
102
+ }
103
+
104
+ /**
105
+ * mb_strlen() wrapper
106
+ *
107
+ * @param string $string
108
+ * @return false|int
109
+ */
110
+ public static function mbStrlen($string)
111
+ {
112
+ // Use the native implementation if available.
113
+ if (\function_exists('mb_strlen')) {
114
+ return mb_strlen($string, 'UTF-8');
115
+ }
116
+
117
+ if (\function_exists('iconv_strlen')) {
118
+ return @iconv_strlen($string, 'UTF-8');
119
+ }
120
+
121
+ return strlen($string);
122
+ }
123
+
124
+ /**
125
+ * mb_substr() wrapper
126
+ * @param string $string
127
+ * @param int $start
128
+ * @param null|int $length
129
+ * @return string
130
+ */
131
+ public static function mbSubstr($string, $start, $length = null)
132
+ {
133
+ // Use the native implementation if available.
134
+ if (\function_exists('mb_substr')) {
135
+ return mb_substr($string, $start, $length, 'UTF-8');
136
+ }
137
+
138
+ if (\function_exists('iconv_substr')) {
139
+ if ($start < 0) {
140
+ $start = static::mbStrlen($string) + $start;
141
+ if ($start < 0) {
142
+ $start = 0;
143
+ }
144
+ }
145
+
146
+ if (null === $length) {
147
+ $length = 2147483647;
148
+ } elseif ($length < 0) {
149
+ $length = static::mbStrlen($string) + $length - $start;
150
+ if ($length < 0) {
151
+ return '';
152
+ }
153
+ }
154
+
155
+ return (string)iconv_substr($string, $start, $length, 'UTF-8');
156
+ }
157
+
158
+ return substr($string, $start, $length);
159
+ }
160
  }
scssphp/src/Version.php CHANGED
@@ -1,8 +1,9 @@
1
  <?php
 
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
@@ -18,5 +19,5 @@ namespace ScssPhp\ScssPhp;
18
  */
19
  class Version
20
  {
21
- const VERSION = 'v1.0.2';
22
  }
1
  <?php
2
+
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2020 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
19
  */
20
  class Version
21
  {
22
+ const VERSION = '1.2.1';
23
  }
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.1.6
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.1.6');
48
 
49
  // Add version to options table
50
  if ( get_option( WPSCSS_VERSION_KEY ) !== false ) {
@@ -220,7 +220,7 @@ function wp_scss_compile() {
220
  * half of entries in the file.
221
  */
222
 
223
- $log_file = $wpscss_compiler->scss_dir.'error_log.log';
224
 
225
  function wpscss_error_styles() {
226
  echo
@@ -266,15 +266,17 @@ function wpscss_settings_show_errors($errors) {
266
  function wpscss_handle_errors() {
267
  global $wpscss_settings, $log_file, $wpscss_compiler;
268
  // Show to logged in users: All the methods for checking user login are set up later in the WP flow, so this only checks that there is a cookie
269
- if ( !is_admin() && $wpscss_settings['errors'] === 'show-logged-in' && !empty($_COOKIE[LOGGED_IN_COOKIE]) && count($wpscss_compiler->compile_errors) > 0) {
270
- wpscss_settings_show_errors($wpscss_compiler->compile_errors);
 
 
271
  // Show in the header to anyone
272
- } else if ( !is_admin() && $wpscss_settings['errors'] === 'show' && count($wpscss_compiler->compile_errors) > 0) {
273
- wpscss_settings_show_errors($wpscss_compiler->compile_errors);
274
  } else { // Hide errors and print them to a log file.
275
- foreach ($wpscss_compiler->compile_errors as $error) {
276
  $error_string = date('m/d/y g:i:s', time()) .': ';
277
- $error_string .= $error['file'] .' - '. $error['message'] . PHP_EOL;
278
  file_put_contents($log_file, $error_string, FILE_APPEND);
279
  $error_string = "";
280
  }
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
  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 ) {
220
  * half of entries in the file.
221
  */
222
 
223
+ $log_file = $wpscss_compiler->get_scss_dir() . 'error_log.log';
224
 
225
  function wpscss_error_styles() {
226
  echo
266
  function wpscss_handle_errors() {
267
  global $wpscss_settings, $log_file, $wpscss_compiler;
268
  // Show to logged in users: All the methods for checking user login are set up later in the WP flow, so this only checks that there is a cookie
269
+
270
+ $compile_errors = $wpscss_compiler->get_compile_errors();
271
+ if ( !is_admin() && $wpscss_settings['errors'] === 'show-logged-in' && !empty($_COOKIE[LOGGED_IN_COOKIE]) && count($compile_errors) > 0) {
272
+ wpscss_settings_show_errors($compile_errors);
273
  // Show in the header to anyone
274
+ } else if ( !is_admin() && $wpscss_settings['errors'] === 'show' && count($compile_errors) > 0) {
275
+ wpscss_settings_show_errors($compile_errors);
276
  } else { // Hide errors and print them to a log file.
277
+ foreach ($compile_errors as $error) {
278
  $error_string = date('m/d/y g:i:s', time()) .': ';
279
+ $error_string .= $error['file'] . ' - ' . $error['message'] . PHP_EOL;
280
  file_put_contents($log_file, $error_string, FILE_APPEND);
281
  $error_string = "";
282
  }