WP-SCSS - Version 2.0.0

Version Description

  • Requires PHP 5.6
  • Update src to use ScssPHP github repo at 1.0.2
  • Added check to make sure 'compiler' function was not already defined. Shadoath
Download this release

Release Info

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

Code changes from version 1.2.6 to 2.0.0

cache/.gitkeep CHANGED
@@ -0,0 +1 @@
 
1
+
class/class-wp-scss.php CHANGED
@@ -1,7 +1,7 @@
1
  <?php
2
 
3
  include_once( WPSCSS_PLUGIN_DIR . '/scssphp/scss.inc.php' );
4
- use Leafo\ScssPhp\Compiler;
5
 
6
  class Wp_Scss {
7
  /**
@@ -58,75 +58,82 @@ class Wp_Scss {
58
 
59
  //Compiler - Takes scss $in and writes compiled css to $out file
60
  // catches errors and puts them the object's compiled_errors property
61
- function compiler($in, $out, $instance) {
62
- global $scssc, $cache;
 
63
 
64
- if (!file_exists($cache)) {
65
- mkdir($cache, 0644);
66
- }
67
-
68
- if (is_writable($cache)) {
69
- try {
70
- $map = basename($out) . '.map';
71
- $scssc->setSourceMap(constant('Leafo\ScssPhp\Compiler::' . $instance->sourcemaps));
72
- $scssc->setSourceMapOptions(array(
73
- 'sourceMapWriteTo' => $instance->css_dir . $map, // absolute path to a file to write the map to
74
- 'sourceMapURL' => $map, // url of the map
75
- 'sourceMapBasepath' => rtrim(ABSPATH, '/'), // base path for filename normalization
76
- 'sourceRoot' => '/', // This value is prepended to the individual entries in the 'source' field.
77
- ));
78
-
79
- $css = $scssc->compile(file_get_contents($in), $in);
80
-
81
- file_put_contents($cache.basename($out), $css);
82
- } catch (Exception $e) {
 
 
 
 
 
 
83
  $errors = array (
84
- 'file' => basename($in),
85
- 'message' => $e->getMessage(),
86
  );
87
  array_push($instance->compile_errors, $errors);
88
  }
89
- } else {
90
- $errors = array (
91
- 'file' => $cache,
92
- 'message' => "File Permission Error, permission denied. Please make the cache directory writable."
93
- );
94
- array_push($instance->compile_errors, $errors);
95
  }
96
- }
97
 
98
- $input_files = array();
99
- // Loop through directory and get .scss file that do not start with '_'
100
- foreach(new DirectoryIterator($this->scss_dir) as $file) {
101
- if (substr($file, 0, 1) != "_" && pathinfo($file->getFilename(), PATHINFO_EXTENSION) == 'scss') {
102
- array_push($input_files, $file->getFilename());
 
103
  }
104
- }
105
 
106
- // For each input file, find matching css file and compile
107
- foreach ($input_files as $scss_file) {
108
- $input = $this->scss_dir.$scss_file;
109
- $outputName = preg_replace("/\.[^$]*/",".css", $scss_file);
110
- $output = $this->css_dir.$outputName;
111
 
112
- compiler($input, $output, $this);
113
- }
114
 
115
- if (count($this->compile_errors) < 1) {
116
- if ( is_writable($this->css_dir) ) {
117
- foreach (new DirectoryIterator($cache) as $cache_file) {
118
- if ( pathinfo($cache_file->getFilename(), PATHINFO_EXTENSION) == 'css') {
119
- file_put_contents($this->css_dir.$cache_file, file_get_contents($cache.$cache_file));
120
- unlink($cache.$cache_file->getFilename()); // Delete file on successful write
 
121
  }
 
 
 
 
 
 
122
  }
123
- } else {
124
- $errors = array(
125
- 'file' => 'CSS Directory',
126
- 'message' => "File Permissions Error, permission denied. Please make your CSS directory writable."
127
- );
128
- array_push($this->compile_errors, $errors);
129
  }
 
 
 
 
 
 
130
  }
131
  }
132
 
@@ -228,5 +235,4 @@ class Wp_Scss {
228
  global $scssc;
229
  $scssc->setVariables($variables);
230
  }
231
-
232
  } // End Wp_Scss Class
1
  <?php
2
 
3
  include_once( WPSCSS_PLUGIN_DIR . '/scssphp/scss.inc.php' );
4
+ use ScssPhp\ScssPhp\Compiler;
5
 
6
  class Wp_Scss {
7
  /**
58
 
59
  //Compiler - Takes scss $in and writes compiled css to $out file
60
  // catches errors and puts them the object's compiled_errors property
61
+ if (!function_exists( 'compiler' )) {
62
+ function compiler($in, $out, $instance) {
63
+ global $scssc, $cache;
64
 
65
+ if (!file_exists($cache)) {
66
+ mkdir($cache, 0644);
67
+ }
68
+ if (is_writable($cache)) {
69
+ try {
70
+ $map = basename($out) . '.map';
71
+ $scssc->setSourceMap(constant('ScssPhp\ScssPhp\Compiler::' . $instance->sourcemaps));
72
+ $scssc->setSourceMapOptions(array(
73
+ 'sourceMapWriteTo' => $instance->css_dir . $map, // absolute path to a file to write the map to
74
+ 'sourceMapURL' => $map, // url of the map
75
+ 'sourceMapBasepath' => rtrim(ABSPATH, '/'), // base path for filename normalization
76
+ 'sourceRoot' => '/', // This value is prepended to the individual entries in the 'source' field.
77
+ ));
78
+
79
+ $css = $scssc->compile(file_get_contents($in), $in);
80
+
81
+ file_put_contents($cache.basename($out), $css);
82
+ } catch (Exception $e) {
83
+ $errors = array (
84
+ 'file' => basename($in),
85
+ 'message' => $e->getMessage(),
86
+ );
87
+ array_push($instance->compile_errors, $errors);
88
+ }
89
+ } else {
90
  $errors = array (
91
+ 'file' => $cache,
92
+ 'message' => "File Permission Error, permission denied. Please make the cache directory writable."
93
  );
94
  array_push($instance->compile_errors, $errors);
95
  }
 
 
 
 
 
 
96
  }
 
97
 
98
+ $input_files = array();
99
+ // Loop through directory and get .scss file that do not start with '_'
100
+ foreach(new DirectoryIterator($this->scss_dir) as $file) {
101
+ if (substr($file, 0, 1) != "_" && pathinfo($file->getFilename(), PATHINFO_EXTENSION) == 'scss') {
102
+ array_push($input_files, $file->getFilename());
103
+ }
104
  }
 
105
 
106
+ // For each input file, find matching css file and compile
107
+ foreach ($input_files as $scss_file) {
108
+ $input = $this->scss_dir.$scss_file;
109
+ $outputName = preg_replace("/\.[^$]*/",".css", $scss_file);
110
+ $output = $this->css_dir.$outputName;
111
 
112
+ compiler($input, $output, $this);
113
+ }
114
 
115
+ if (count($this->compile_errors) < 1) {
116
+ if ( is_writable($this->css_dir) ) {
117
+ foreach (new DirectoryIterator($cache) as $cache_file) {
118
+ if ( pathinfo($cache_file->getFilename(), PATHINFO_EXTENSION) == 'css') {
119
+ file_put_contents($this->css_dir.$cache_file, file_get_contents($cache.$cache_file));
120
+ unlink($cache.$cache_file->getFilename()); // Delete file on successful write
121
+ }
122
  }
123
+ } else {
124
+ $errors = array(
125
+ 'file' => 'CSS Directory',
126
+ 'message' => "File Permissions Error, permission denied. Please make your CSS directory writable."
127
+ );
128
+ array_push($this->compile_errors, $errors);
129
  }
 
 
 
 
 
 
130
  }
131
+ }else{
132
+ $errors = array (
133
+ 'file' => 'SCSS compiler',
134
+ 'message' => "Compiling Error, function 'compiler' already exists."
135
+ );
136
+ array_push($this->compile_errors, $errors);
137
  }
138
  }
139
 
235
  global $scssc;
236
  $scssc->setVariables($variables);
237
  }
 
238
  } // End Wp_Scss Class
options.php CHANGED
@@ -1,260 +1,258 @@
1
  <?php
2
  class Wp_Scss_Settings
3
  {
4
- /**
5
- * Holds the values to be used in the fields callbacks
6
- */
7
- private $options;
8
 
9
- /**
10
- * Start up
11
- */
12
- public function __construct()
13
- {
14
- add_action( 'admin_menu', array( $this, 'add_plugin_page' ) );
15
- add_action( 'admin_init', array( $this, 'page_init' ) );
16
- }
17
 
18
- /**
19
- * Add options page
20
- */
21
- public function add_plugin_page()
22
- {
23
- // This page will be under "Settings"
24
- add_options_page(
25
- 'Settings Admin',
26
- 'WP-SCSS',
27
- 'manage_options',
28
- 'wpscss_options',
29
- array( $this, 'create_admin_page' )
30
- );
31
- }
32
 
33
- /**
34
- * Options page callback
35
- */
36
- public function create_admin_page()
37
- {
38
- // Set class property
39
- $this->options = get_option( 'wpscss_options' );
40
- ?>
41
- <div class="wrap">
42
- <h2>WP-SCSS Settings</h2>
43
- <p>
44
- <span class="version">Version <em><?php echo get_option('wpscss_version'); ?></em>
45
- <br/>
46
- <span class="author">By: <a href="http://connectthink.com" target="_blank">Connect Think</a></span>
47
- <br/>
48
- <span class="repo">Help & Issues: <a href="https://github.com/ConnectThink/WP-SCSS" target="_blank">Github</a></span>
49
- </p>
50
- <form method="post" action="options.php">
51
- <?php
52
- // This prints out all hidden setting fields
53
- settings_fields( 'wpscss_options_group' );
54
- do_settings_sections( 'wpscss_options' );
55
- submit_button();
56
- ?>
57
- </form>
58
- </div>
59
  <?php
60
- }
 
 
 
 
 
 
 
 
61
 
62
- /**
63
- * Register and add settings
64
- */
65
- public function page_init()
66
- {
67
- register_setting(
68
- 'wpscss_options_group', // Option group
69
- 'wpscss_options', // Option name
70
- array( $this, 'sanitize' ) // Sanitize
71
- );
72
 
73
- // Paths to Directories
74
- add_settings_section(
75
- 'wpscss_paths_section', // ID
76
- 'Configure Paths', // Title
77
- array( $this, 'print_paths_info' ), // Callback
78
- 'wpscss_options' // Page
79
- );
80
 
81
- add_settings_field(
82
- 'wpscss_scss_dir', // ID
83
- 'Scss Location', // Title
84
- array( $this, 'input_text_callback' ), // Callback
85
- 'wpscss_options', // Page
86
- 'wpscss_paths_section', // Section
87
- array( // args
88
- 'name' => 'scss_dir',
89
- )
90
- );
91
 
92
- add_settings_field(
93
- 'wpscss_css_dir', // ID
94
- 'CSS Location', // Title
95
- array( $this, 'input_text_callback' ), // Callback
96
- 'wpscss_options', // Page
97
- 'wpscss_paths_section', // Section
98
- array( // args
99
- 'name' => 'css_dir',
100
- )
101
- );
102
 
103
- // Compiling Options
104
- add_settings_section(
105
- 'wpscss_compile_section', // ID
106
- 'Compiling Options', // Title
107
- array( $this, 'print_compile_info' ), // Callback
108
- 'wpscss_options' // Page
109
- );
110
 
111
- add_settings_field(
112
- 'Compiling Mode', // ID
113
- 'Compiling Mode', // Title
114
- array( $this, 'input_select_callback' ), // Callback
115
- 'wpscss_options', // Page
116
- 'wpscss_compile_section', // Section
117
- array( // args
118
- 'name' => 'compiling_options',
119
- 'type' => apply_filters( 'wp_scss_compiling_modes',
120
- array(
121
- 'Leafo\ScssPhp\Formatter\Expanded' => 'Expanded',
122
- 'Leafo\ScssPhp\Formatter\Nested' => 'Nested',
123
- 'Leafo\ScssPhp\Formatter\Compressed' => 'Compressed',
124
- 'Leafo\ScssPhp\Formatter\Compact' => 'Compact',
125
- 'Leafo\ScssPhp\Formatter\Crunched' => 'Crunched',
126
- 'Leafo\ScssPhp\Formatter\Debug' => 'Debug'
127
- )
128
- )
129
- )
130
- );
131
-
132
- add_settings_field(
133
- 'Source Map Mode', // ID
134
- 'Source Map Mode', // Title
135
- array( $this, 'input_select_callback' ), // Callback
136
- 'wpscss_options', // Page
137
- 'wpscss_compile_section', // Section
138
- array( // args
139
- 'name' => 'sourcemap_options',
140
- 'type' => apply_filters( 'wp_scss_sourcemap_modes' ,
141
- array(
142
- 'SOURCE_MAP_NONE' => 'None',
143
- 'SOURCE_MAP_INLINE' => 'Inline',
144
- 'SOURCE_MAP_FILE' => 'File'
145
- )
146
- )
147
- )
148
- );
149
 
150
- add_settings_field(
151
- 'Error Display', // ID
152
- 'Error Display', // Title
153
- array( $this, 'input_select_callback' ), // Callback
154
- 'wpscss_options', // Page
155
- 'wpscss_compile_section', // Section
156
- array( // args
157
- 'name' => 'errors',
158
- 'type' => apply_filters( 'wp_scss_error_diplay',
159
- array(
160
- 'show' => 'Show in Header',
161
- 'show-logged-in' => 'Show to Logged In Users',
162
- 'hide' => 'Print to Log',
163
- )
164
- )
165
- )
166
- );
167
 
168
- // Enqueuing Options
169
- add_settings_section(
170
- 'wpscss_enqueue_section', // ID
171
- 'Enqueuing Options', // Title
172
- array( $this, 'print_enqueue_info' ), // Callback
173
- 'wpscss_options' // Page
174
- );
 
 
 
 
 
 
 
 
 
 
175
 
176
- add_settings_field(
177
- 'Enqueue Stylesheets', // ID
178
- 'Enqueue Stylesheets', // Title
179
- array( $this, 'input_checkbox_callback' ), // Callback
180
- 'wpscss_options', // Page
181
- 'wpscss_enqueue_section', // Section
182
- array( // args
183
- 'name' => 'enqueue'
184
- )
185
- );
186
-
187
- }
 
 
 
 
 
 
188
 
189
- /**
190
- * Sanitize each setting field as needed
191
- *
192
- * @param array $input Contains all settings fields as array keys
193
- */
194
- public function sanitize( $input ) {
195
- foreach( ['scss_dir', 'css_dir'] as $dir ){
196
- if( !empty( $input[$dir] ) ) {
197
- $input[$dir] = sanitize_text_field( $input[$dir] );
198
 
199
- // Add a trailing slash if not already present
200
- if(substr($input[$dir], -1) != '/'){
201
- $input[$dir] .= '/';
202
- }
203
- }
 
 
 
 
 
 
 
 
204
  }
205
-
206
- return $input;
207
  }
208
 
209
- /**
210
- * Print the Section text
211
- */
212
- public function print_paths_info() {
213
- print 'Add the paths to your directories below. Paths should start with the root of your theme. example: "/library/scss/"';
214
- }
215
- public function print_compile_info() {
216
- print 'Choose how you would like SCSS and source maps to be compiled and how you would like the plugin to handle errors';
217
- }
218
- public function print_enqueue_info() {
219
- print 'WP-SCSS can enqueue your css stylesheets in the header automatically.';
220
- }
221
 
222
- /**
223
- * Text Fields' Callback
224
- */
225
- public function input_text_callback( $args ) {
226
- printf(
227
- '<input type="text" id="%s" name="wpscss_options[%s]" value="%s" />',
228
- esc_attr( $args['name'] ), esc_attr( $args['name'] ), esc_attr( isset($this->options[$args['name']]) ? $this->options[$args['name']] : '' )
229
- );
230
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
- /**
233
- * Select Boxes' Callbacks
234
- */
235
- public function input_select_callback( $args ) {
236
- $this->options = get_option( 'wpscss_options' );
237
-
238
- $html = sprintf( '<select id="%s" name="wpscss_options[%s]">', esc_attr( $args['name'] ), esc_attr( $args['name'] ) );
239
- foreach( $args['type'] as $value => $title ) {
240
- $html .= '<option value="' . esc_attr( $value ) . '"' . selected( isset($this->options[esc_attr( $args['name'] )]) ? $this->options[esc_attr( $args['name'] )] : '', esc_attr( $value ), false) . '>' . esc_attr( $title ) . '</option>';
241
- }
242
- $html .= '</select>';
243
-
244
- echo $html;
245
  }
 
246
 
247
- /**
248
- * Checkboxes' Callbacks
249
- */
250
- public function input_checkbox_callback( $args ) {
251
- $this->options = get_option( 'wpscss_options' );
252
-
253
- $html = '<input type="checkbox" id="' . esc_attr( $args['name'] ) . '" name="wpscss_options[' . esc_attr( $args['name'] ) . ']" value="1"' . checked( 1, isset( $this->options[esc_attr( $args['name'] )] ) ? $this->options[esc_attr( $args['name'] )] : 0, false ) . '/>';
254
- $html .= '<label for="' . esc_attr( $args['name'] ) . '"></label>';
255
-
256
- echo $html;
257
- }
258
 
259
- }
 
 
 
 
260
 
 
 
 
 
 
 
1
  <?php
2
  class Wp_Scss_Settings
3
  {
4
+ /**
5
+ * Holds the values to be used in the fields callbacks
6
+ */
7
+ private $options;
8
 
9
+ /**
10
+ * Start up
11
+ */
12
+ public function __construct()
13
+ {
14
+ add_action( 'admin_menu', array( $this, 'add_plugin_page' ) );
15
+ add_action( 'admin_init', array( $this, 'page_init' ) );
16
+ }
17
 
18
+ /**
19
+ * Add options page
20
+ */
21
+ public function add_plugin_page()
22
+ {
23
+ // This page will be under "Settings"
24
+ add_options_page(
25
+ 'Settings Admin',
26
+ 'WP-SCSS',
27
+ 'manage_options',
28
+ 'wpscss_options',
29
+ array( $this, 'create_admin_page' )
30
+ );
31
+ }
32
 
33
+ /**
34
+ * Options page callback
35
+ */
36
+ public function create_admin_page()
37
+ {
38
+ // Set class property
39
+ $this->options = get_option( 'wpscss_options' );
40
+ ?>
41
+ <div class="wrap">
42
+ <h2>WP-SCSS Settings</h2>
43
+ <p>
44
+ <span class="version">Version <em><?php echo get_option('wpscss_version'); ?></em>
45
+ <br/>
46
+ <span class="author">By: <a href="http://connectthink.com" target="_blank">Connect Think</a></span>
47
+ <br/>
48
+ <span class="repo">Help & Issues: <a href="https://github.com/ConnectThink/WP-SCSS" target="_blank">Github</a></span>
49
+ </p>
50
+ <form method="post" action="options.php">
 
 
 
 
 
 
 
 
51
  <?php
52
+ // This prints out all hidden setting fields
53
+ settings_fields( 'wpscss_options_group' );
54
+ do_settings_sections( 'wpscss_options' );
55
+ submit_button();
56
+ ?>
57
+ </form>
58
+ </div>
59
+ <?php
60
+ }
61
 
62
+ /**
63
+ * Register and add settings
64
+ */
65
+ public function page_init()
66
+ {
67
+ register_setting(
68
+ 'wpscss_options_group', // Option group
69
+ 'wpscss_options', // Option name
70
+ array( $this, 'sanitize' ) // Sanitize
71
+ );
72
 
73
+ // Paths to Directories
74
+ add_settings_section(
75
+ 'wpscss_paths_section', // ID
76
+ 'Configure Paths', // Title
77
+ array( $this, 'print_paths_info' ), // Callback
78
+ 'wpscss_options' // Page
79
+ );
80
 
81
+ add_settings_field(
82
+ 'wpscss_scss_dir', // ID
83
+ 'Scss Location', // Title
84
+ array( $this, 'input_text_callback' ), // Callback
85
+ 'wpscss_options', // Page
86
+ 'wpscss_paths_section', // Section
87
+ array( // args
88
+ 'name' => 'scss_dir',
89
+ )
90
+ );
91
 
92
+ add_settings_field(
93
+ 'wpscss_css_dir', // ID
94
+ 'CSS Location', // Title
95
+ array( $this, 'input_text_callback' ), // Callback
96
+ 'wpscss_options', // Page
97
+ 'wpscss_paths_section', // Section
98
+ array( // args
99
+ 'name' => 'css_dir',
100
+ )
101
+ );
102
 
103
+ // Compiling Options
104
+ add_settings_section(
105
+ 'wpscss_compile_section', // ID
106
+ 'Compiling Options', // Title
107
+ array( $this, 'print_compile_info' ), // Callback
108
+ 'wpscss_options' // Page
109
+ );
110
 
111
+ add_settings_field(
112
+ 'Compiling Mode', // ID
113
+ 'Compiling Mode', // Title
114
+ array( $this, 'input_select_callback' ), // Callback
115
+ 'wpscss_options', // Page
116
+ 'wpscss_compile_section', // Section
117
+ array( // args
118
+ 'name' => 'compiling_options',
119
+ 'type' => apply_filters( 'wp_scss_compiling_modes',
120
+ array(
121
+ 'ScssPhp\ScssPhp\Formatter\Expanded' => 'Expanded',
122
+ 'ScssPhp\ScssPhp\Formatter\Nested' => 'Nested',
123
+ 'ScssPhp\ScssPhp\Formatter\Compressed' => 'Compressed',
124
+ 'ScssPhp\ScssPhp\Formatter\Compact' => 'Compact',
125
+ 'ScssPhp\ScssPhp\Formatter\Crunched' => 'Crunched',
126
+ 'ScssPhp\ScssPhp\Formatter\Debug' => 'Debug'
127
+ )
128
+ )
129
+ )
130
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
+ add_settings_field(
133
+ 'Source Map Mode', // ID
134
+ 'Source Map Mode', // Title
135
+ array( $this, 'input_select_callback' ), // Callback
136
+ 'wpscss_options', // Page
137
+ 'wpscss_compile_section', // Section
138
+ array( // args
139
+ 'name' => 'sourcemap_options',
140
+ 'type' => apply_filters( 'wp_scss_sourcemap_modes',
141
+ array(
142
+ 'SOURCE_MAP_NONE' => 'None',
143
+ 'SOURCE_MAP_INLINE' => 'Inline',
144
+ 'SOURCE_MAP_FILE' => 'File'
145
+ )
146
+ )
147
+ )
148
+ );
149
 
150
+ add_settings_field(
151
+ 'Error Display', // ID
152
+ 'Error Display', // Title
153
+ array( $this, 'input_select_callback' ), // Callback
154
+ 'wpscss_options', // Page
155
+ 'wpscss_compile_section', // Section
156
+ array( // args
157
+ 'name' => 'errors',
158
+ 'type' => apply_filters( 'wp_scss_error_diplay',
159
+ array(
160
+ 'show' => 'Show in Header',
161
+ 'show-logged-in' => 'Show to Logged In Users',
162
+ 'hide' => 'Print to Log',
163
+ )
164
+ )
165
+ )
166
+ );
167
 
168
+ // Enqueuing Options
169
+ add_settings_section(
170
+ 'wpscss_enqueue_section', // ID
171
+ 'Enqueuing Options', // Title
172
+ array( $this, 'print_enqueue_info' ), // Callback
173
+ 'wpscss_options' // Page
174
+ );
175
+
176
+ add_settings_field(
177
+ 'Enqueue Stylesheets', // ID
178
+ 'Enqueue Stylesheets', // Title
179
+ array( $this, 'input_checkbox_callback' ), // Callback
180
+ 'wpscss_options', // Page
181
+ 'wpscss_enqueue_section', // Section
182
+ array( // args
183
+ 'name' => 'enqueue'
184
+ )
185
+ );
186
 
187
+ }
 
 
 
 
 
 
 
 
188
 
189
+ /**
190
+ * Sanitize each setting field as needed
191
+ *
192
+ * @param array $input Contains all settings fields as array keys
193
+ */
194
+ public function sanitize( $input ) {
195
+ foreach( ['scss_dir', 'css_dir'] as $dir ){
196
+ if( !empty( $input[$dir] ) ) {
197
+ $input[$dir] = sanitize_text_field( $input[$dir] );
198
+
199
+ // Add a trailing slash if not already present
200
+ if(substr($input[$dir], -1) != '/'){
201
+ $input[$dir] .= '/';
202
  }
203
+ }
 
204
  }
205
 
206
+ return $input;
207
+ }
 
 
 
 
 
 
 
 
 
 
208
 
209
+ /**
210
+ * Print the Section text
211
+ */
212
+ public function print_paths_info() {
213
+ print 'Add the paths to your directories below. Paths should start with the root of your theme. example: "/library/scss/"';
214
+ }
215
+ public function print_compile_info() {
216
+ print 'Choose how you would like SCSS and source maps to be compiled and how you would like the plugin to handle errors';
217
+ }
218
+ public function print_enqueue_info() {
219
+ print 'WP-SCSS can enqueue your css stylesheets in the header automatically.';
220
+ }
221
+
222
+ /**
223
+ * Text Fields' Callback
224
+ */
225
+ public function input_text_callback( $args ) {
226
+ printf(
227
+ '<input type="text" id="%s" name="wpscss_options[%s]" value="%s" />',
228
+ esc_attr( $args['name'] ), esc_attr( $args['name'] ), esc_attr( isset($this->options[$args['name']]) ? $this->options[$args['name']] : '' )
229
+ );
230
+ }
231
 
232
+ /**
233
+ * Select Boxes' Callbacks
234
+ */
235
+ public function input_select_callback( $args ) {
236
+ $this->options = get_option( 'wpscss_options' );
237
+
238
+ $html = sprintf( '<select id="%s" name="wpscss_options[%s]">', esc_attr( $args['name'] ), esc_attr( $args['name'] ) );
239
+ foreach( $args['type'] as $value => $title ) {
240
+ $html .= '<option value="' . esc_attr( $value ) . '"' . selected( isset($this->options[esc_attr( $args['name'] )]) ? $this->options[esc_attr( $args['name'] )] : '', esc_attr( $value ), false) . '>' . esc_attr( $title ) . '</option>';
 
 
 
 
241
  }
242
+ $html .= '</select>';
243
 
244
+ echo $html;
245
+ }
 
 
 
 
 
 
 
 
 
246
 
247
+ /**
248
+ * Checkboxes' Callbacks
249
+ */
250
+ public function input_checkbox_callback( $args ) {
251
+ $this->options = get_option( 'wpscss_options' );
252
 
253
+ $html = '<input type="checkbox" id="' . esc_attr( $args['name'] ) . '" name="wpscss_options[' . esc_attr( $args['name'] ) . ']" value="1"' . checked( 1, isset( $this->options[esc_attr( $args['name'] )] ) ? $this->options[esc_attr( $args['name'] )] : 0, false ) . '/>';
254
+ $html .= '<label for="' . esc_attr( $args['name'] ) . '"></label>';
255
+
256
+ echo $html;
257
+ }
258
+ }
readme.txt CHANGED
@@ -3,8 +3,8 @@ Contributors: connectthink
3
  Tags: sass, scss, css
4
  Plugin URI: https://github.com/ConnectThink/WP-SCSS
5
  Requires at least: 3.0.1
6
- Tested up to: 5.3
7
- Stable tag: 1.2.6
8
  License: GPLv3 or later
9
  License URI: http://www.gnu.org/copyleft/gpl.html
10
 
@@ -30,6 +30,12 @@ The plugin only compiles when changes have been made to the scss files. Compiles
30
 
31
  Yes, absolutely. Make sure you define your directories relative to your child theme and that your child theme is active. Otherwise you'll see an error regarding missing directories.
32
 
 
 
 
 
 
 
33
  = How do I @import subfiles =
34
 
35
  You can import other scss files into parent files and compile them into a single css file. To do this, use @import as normal in your scss file. All imported file names *must* start with an underscore. Otherwise they will be compiled into their own css file.
@@ -62,6 +68,11 @@ Make sure your directories are properly defined in the settings. Paths are defin
62
  If you are having issues with the plugin, create an issue on [github](https://github.com/ConnectThink/WP-SCSS), and we'll do our best to help.
63
 
64
  == Changelog ==
 
 
 
 
 
65
  = 1.2.6 =
66
  * Create cache dir if it doesn't exist [@XNBlank](https://github.com/ConnectThink/WP-SCSS/pull/135
67
  * Add cache dir as default [@mhbapcc](https://github.com/ConnectThink/WP-SCSS/pull/144)
3
  Tags: sass, scss, css
4
  Plugin URI: https://github.com/ConnectThink/WP-SCSS
5
  Requires at least: 3.0.1
6
+ sested up to: 5.6.1
7
+ Stable tag: 2.0.0
8
  License: GPLv3 or later
9
  License URI: http://www.gnu.org/copyleft/gpl.html
10
 
30
 
31
  Yes, absolutely. Make sure you define your directories relative to your child theme and that your child theme is active. Otherwise you'll see an error regarding missing directories.
32
 
33
+
34
+ = What version of PHP is required? =
35
+
36
+ PHP 5.6j is required to run WP-SCSS
37
+
38
+
39
  = How do I @import subfiles =
40
 
41
  You can import other scss files into parent files and compile them into a single css file. To do this, use @import as normal in your scss file. All imported file names *must* start with an underscore. Otherwise they will be compiled into their own css file.
68
  If you are having issues with the plugin, create an issue on [github](https://github.com/ConnectThink/WP-SCSS), and we'll do our best to help.
69
 
70
  == Changelog ==
71
+ = 2.0.0 =
72
+ * Requires PHP 5.6
73
+ * Update src to use [ScssPHP github repo at 1.0.2](https://github.com/scssphp/scssphp/tree/1.0.2)
74
+ * Added check to make sure 'compiler' function was not already defined. [Shadoath](https://github.com/ConnectThink/WP-SCSS/pull/155)
75
+
76
  = 1.2.6 =
77
  * Create cache dir if it doesn't exist [@XNBlank](https://github.com/ConnectThink/WP-SCSS/pull/135
78
  * Add cache dir as default [@mhbapcc](https://github.com/ConnectThink/WP-SCSS/pull/144)
scssphp/bin/pscss CHANGED
@@ -3,24 +3,24 @@
3
  /**
4
  * SCSSPHP
5
  *
6
- * @copyright 2012-2015 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
10
- * @link http://leafo.github.io/scssphp
11
  */
12
 
13
  error_reporting(E_ALL);
14
 
15
- if (version_compare(PHP_VERSION, '5.4') < 0) {
16
- die('Requires PHP 5.4 or above');
17
  }
18
 
19
  include __DIR__ . '/../scss.inc.php';
20
 
21
- use Leafo\ScssPhp\Compiler;
22
- use Leafo\ScssPhp\Parser;
23
- use Leafo\ScssPhp\Version;
24
 
25
  $style = null;
26
  $loadPaths = null;
@@ -197,11 +197,11 @@ if ($precision) {
197
  }
198
 
199
  if ($style) {
200
- $scss->setFormatter('Leafo\\ScssPhp\\Formatter\\' . ucfirst($style));
201
  }
202
 
203
  if ($sourceMap) {
204
- $scss->setSourceMap(Compiler::SOURCE_MAP_FILE);
205
  }
206
 
207
  if ($encoding) {
3
  /**
4
  * SCSSPHP
5
  *
6
+ * @copyright 2012-2019 Leaf Corcoran
7
  *
8
  * @license http://opensource.org/licenses/MIT MIT
9
  *
10
+ * @link http://scssphp.github.io/scssphp
11
  */
12
 
13
  error_reporting(E_ALL);
14
 
15
+ if (version_compare(PHP_VERSION, '5.6') < 0) {
16
+ die('Requires PHP 5.6 or above');
17
  }
18
 
19
  include __DIR__ . '/../scss.inc.php';
20
 
21
+ use ScssPhp\ScssPhp\Compiler;
22
+ use ScssPhp\ScssPhp\Parser;
23
+ use ScssPhp\ScssPhp\Version;
24
 
25
  $style = null;
26
  $loadPaths = null;
197
  }
198
 
199
  if ($style) {
200
+ $scss->setFormatter('ScssPhp\\ScssPhp\\Formatter\\' . ucfirst($style));
201
  }
202
 
203
  if ($sourceMap) {
204
+ $scss->setSourceMap(Compiler::SOURCE_MAP_INLINE);
205
  }
206
 
207
  if ($encoding) {
scssphp/scss.inc.php CHANGED
@@ -1,32 +1,34 @@
1
  <?php
2
- if (version_compare(PHP_VERSION, '5.4') < 0) {
3
- throw new \Exception('scssphp requires PHP 5.4 or above');
4
  }
5
 
6
- if (! class_exists('Leafo\ScssPhp\Version', false)) {
7
- include_once __DIR__ . '/src/Base/Range.php';
8
- include_once __DIR__ . '/src/Block.php';
9
- include_once __DIR__ . '/src/Colors.php';
10
- include_once __DIR__ . '/src/Compiler.php';
11
- include_once __DIR__ . '/src/Compiler/Environment.php';
12
- include_once __DIR__ . '/src/Exception/CompilerException.php';
13
- include_once __DIR__ . '/src/Exception/ParserException.php';
14
- include_once __DIR__ . '/src/Exception/RangeException.php';
15
- include_once __DIR__ . '/src/Exception/ServerException.php';
16
- include_once __DIR__ . '/src/Formatter.php';
17
- include_once __DIR__ . '/src/Formatter/Compact.php';
18
- include_once __DIR__ . '/src/Formatter/Compressed.php';
19
- include_once __DIR__ . '/src/Formatter/Crunched.php';
20
- include_once __DIR__ . '/src/Formatter/Debug.php';
21
- include_once __DIR__ . '/src/Formatter/Expanded.php';
22
- include_once __DIR__ . '/src/Formatter/Nested.php';
23
- include_once __DIR__ . '/src/Formatter/OutputBlock.php';
24
- include_once __DIR__ . '/src/Node.php';
25
- include_once __DIR__ . '/src/Node/Number.php';
26
- include_once __DIR__ . '/src/Parser.php';
27
- include_once __DIR__ . '/src/SourceMap/Base64VLQEncoder.php';
28
- include_once __DIR__ . '/src/SourceMap/SourceMapGenerator.php';
29
- include_once __DIR__ . '/src/Type.php';
30
- include_once __DIR__ . '/src/Util.php';
31
- include_once __DIR__ . '/src/Version.php';
 
 
32
  }
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
  }
scssphp/src/Base/Range.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2015-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Base;
13
 
14
  /**
15
  * Range
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2015-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Base;
13
 
14
  /**
15
  * Range
scssphp/src/Block.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
 
14
  /**
15
  * Block
@@ -24,7 +24,7 @@ class Block
24
  public $type;
25
 
26
  /**
27
- * @var \Leafo\ScssPhp\Block
28
  */
29
  public $parent;
30
 
@@ -62,4 +62,9 @@ class Block
62
  * @var array
63
  */
64
  public $children;
 
 
 
 
 
65
  }
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
 
14
  /**
15
  * Block
24
  public $type;
25
 
26
  /**
27
+ * @var \ScssPhp\ScssPhp\Block
28
  */
29
  public $parent;
30
 
62
  * @var array
63
  */
64
  public $children;
65
+
66
+ /**
67
+ * @var \ScssPhp\ScssPhp\Block
68
+ */
69
+ public $selfParent;
70
  }
scssphp/src/Cache.php ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
+ *
7
+ * @license http://opensource.org/licenses/MIT MIT
8
+ *
9
+ * @link http://scssphp.github.io/scssphp
10
+ */
11
+
12
+ namespace ScssPhp\ScssPhp;
13
+
14
+ use Exception;
15
+
16
+ /**
17
+ * The scss cache manager.
18
+ *
19
+ * In short:
20
+ *
21
+ * allow to put in cache/get from cache a generic result from a known operation on a generic dataset,
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
+ {
35
+ const CACHE_VERSION = 1;
36
+
37
+ // directory used for storing data
38
+ public static $cacheDir = false;
39
+
40
+ // prefix for the storing data
41
+ public static $prefix = 'scssphp_';
42
+
43
+ // force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit
44
+ public static $forceRefresh = false;
45
+
46
+ // specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up
47
+ public static $gcLifetime = 604800;
48
+
49
+ // array of already refreshed cache if $forceRefresh==='once'
50
+ protected static $refreshed = [];
51
+
52
+ /**
53
+ * Constructor
54
+ *
55
+ * @param array $options
56
+ */
57
+ public function __construct($options)
58
+ {
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'])) {
69
+ self::$prefix = $options['prefix'];
70
+ }
71
+
72
+ if (empty(self::$prefix)) {
73
+ throw new Exception('prefix not set');
74
+ }
75
+
76
+ if (isset($options['forceRefresh'])) {
77
+ self::$forceRefresh = $options['force_refresh'];
78
+ }
79
+
80
+ self::checkCacheDir();
81
+ }
82
+
83
+ /**
84
+ * Get the cached result of $operation on $what,
85
+ * which is known as dependant from the content of $options
86
+ *
87
+ * @param string $operation parse, compile...
88
+ * @param mixed $what content key (e.g., filename to be treated)
89
+ * @param array $options any option that affect the operation result on the content
90
+ * @param integer $lastModified last modified timestamp
91
+ *
92
+ * @return mixed
93
+ *
94
+ * @throws \Exception
95
+ */
96
+ public function getCache($operation, $what, $options = [], $lastModified = null)
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
+ }
115
+ }
116
+
117
+ return null;
118
+ }
119
+
120
+ /**
121
+ * Put in cache the result of $operation on $what,
122
+ * which is known as dependant from the content of $options
123
+ *
124
+ * @param string $operation
125
+ * @param mixed $what
126
+ * @param mixed $value
127
+ * @param array $options
128
+ */
129
+ public function setCache($operation, $what, $value, $options = [])
130
+ {
131
+ $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
132
+
133
+ $c = ['value' => $value];
134
+ $c = serialize($c);
135
+ file_put_contents($fileCache, $c);
136
+
137
+ if (self::$forceRefresh === 'once') {
138
+ self::$refreshed[$fileCache] = true;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Get the cache name for the caching of $operation on $what,
144
+ * which is known as dependant from the content of $options
145
+ *
146
+ * @param string $operation
147
+ * @param mixed $what
148
+ * @param array $options
149
+ *
150
+ * @return string
151
+ */
152
+ private static function cacheName($operation, $what, $options = [])
153
+ {
154
+ $t = [
155
+ 'version' => self::CACHE_VERSION,
156
+ 'operation' => $operation,
157
+ 'what' => $what,
158
+ 'options' => $options
159
+ ];
160
+
161
+ $t = self::$prefix
162
+ . sha1(json_encode($t))
163
+ . ".$operation"
164
+ . ".scsscache";
165
+
166
+ return $t;
167
+ }
168
+
169
+ /**
170
+ * Check that the cache dir exists and is writeable
171
+ *
172
+ * @throws \Exception
173
+ */
174
+ public static function checkCacheDir()
175
+ {
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
+ }
189
+
190
+ /**
191
+ * Delete unused cached files
192
+ */
193
+ public static function cleanCache()
194
+ {
195
+ static $clean = false;
196
+
197
+ if ($clean || empty(self::$cacheDir)) {
198
+ return;
199
+ }
200
+
201
+ $clean = true;
202
+
203
+ // only remove files with extensions created by SCSSPHP Cache
204
+ // css files removed based on the list files
205
+ $removeTypes = ['scsscache' => 1];
206
+
207
+ $files = scandir(self::$cacheDir);
208
+
209
+ if (! $files) {
210
+ return;
211
+ }
212
+
213
+ $checkTime = time() - self::$gcLifetime;
214
+
215
+ foreach ($files as $file) {
216
+ // don't delete if the file wasn't created with SCSSPHP Cache
217
+ if (strpos($file, self::$prefix) !== 0) {
218
+ continue;
219
+ }
220
+
221
+ $parts = explode('.', $file);
222
+ $type = array_pop($parts);
223
+
224
+ if (! isset($removeTypes[$type])) {
225
+ continue;
226
+ }
227
+
228
+ $fullPath = self::$cacheDir . $file;
229
+ $mtime = filemtime($fullPath);
230
+
231
+ // don't delete if it's a relatively new file
232
+ if ($mtime > $checkTime) {
233
+ continue;
234
+ }
235
+
236
+ unlink($fullPath);
237
+ }
238
+ }
239
+ }
scssphp/src/Colors.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
 
14
  /**
15
  * CSS Colors
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
 
14
  /**
15
  * CSS Colors
scssphp/src/Compiler.php CHANGED
@@ -2,26 +2,27 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
-
14
- use Leafo\ScssPhp\Base\Range;
15
- use Leafo\ScssPhp\Block;
16
- use Leafo\ScssPhp\Colors;
17
- use Leafo\ScssPhp\Compiler\Environment;
18
- use Leafo\ScssPhp\Exception\CompilerException;
19
- use Leafo\ScssPhp\Formatter\OutputBlock;
20
- use Leafo\ScssPhp\Node;
21
- use Leafo\ScssPhp\SourceMap\SourceMapGenerator;
22
- use Leafo\ScssPhp\Type;
23
- use Leafo\ScssPhp\Parser;
24
- use Leafo\ScssPhp\Util;
 
25
 
26
  /**
27
  * The scss compiler and parser.
@@ -98,17 +99,17 @@ class Compiler
98
  'function' => '^',
99
  ];
100
 
101
- static public $true = [Type::T_KEYWORD, 'true'];
102
- static public $false = [Type::T_KEYWORD, 'false'];
103
- static public $null = [Type::T_NULL];
104
- static public $nullString = [Type::T_STRING, '', []];
105
  static public $defaultValue = [Type::T_KEYWORD, ''];
106
  static public $selfSelector = [Type::T_SELF];
107
- static public $emptyList = [Type::T_LIST, '', []];
108
- static public $emptyMap = [Type::T_MAP, [], []];
109
- static public $emptyString = [Type::T_STRING, '"', []];
110
- static public $with = [Type::T_KEYWORD, 'with'];
111
- static public $without = [Type::T_KEYWORD, 'without'];
112
 
113
  protected $importPaths = [''];
114
  protected $importCache = [];
@@ -129,15 +130,15 @@ class Compiler
129
  protected $sourceMapOptions = [];
130
 
131
  /**
132
- * @var string|\Leafo\ScssPhp\Formatter
133
  */
134
- protected $formatter = 'Leafo\ScssPhp\Formatter\Nested';
135
 
136
  protected $rootEnv;
137
  protected $rootBlock;
138
 
139
  /**
140
- * @var \Leafo\ScssPhp\Compiler\Environment
141
  */
142
  protected $env;
143
  protected $scope;
@@ -145,26 +146,48 @@ class Compiler
145
  protected $charsetSeen;
146
  protected $sourceNames;
147
 
148
- private $indentLevel;
149
- private $commentsSeen;
150
- private $extends;
151
- private $extendsMap;
152
- private $parsedFiles;
153
- private $parser;
154
- private $sourceIndex;
155
- private $sourceLine;
156
- private $sourceColumn;
157
- private $stderr;
158
- private $shouldEvaluate;
159
- private $ignoreErrors;
 
 
 
160
 
161
  /**
162
  * Constructor
163
  */
164
- public function __construct()
165
  {
166
  $this->parsedFiles = [];
167
  $this->sourceNames = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  }
169
 
170
  /**
@@ -179,8 +202,28 @@ class Compiler
179
  */
180
  public function compile($code, $path = null)
181
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  $this->indentLevel = -1;
183
- $this->commentsSeen = [];
184
  $this->extends = [];
185
  $this->extendsMap = [];
186
  $this->sourceIndex = null;
@@ -194,7 +237,7 @@ class Compiler
194
  $this->stderr = fopen('php://stderr', 'w');
195
 
196
  $this->parser = $this->parserFactory($path);
197
- $tree = $this->parser->parse($code);
198
  $this->parser = null;
199
 
200
  $this->formatter = new $this->formatter();
@@ -235,6 +278,15 @@ class Compiler
235
  $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
236
  }
237
 
 
 
 
 
 
 
 
 
 
238
  return $out;
239
  }
240
 
@@ -243,11 +295,11 @@ class Compiler
243
  *
244
  * @param string $path
245
  *
246
- * @return \Leafo\ScssPhp\Parser
247
  */
248
  protected function parserFactory($path)
249
  {
250
- $parser = new Parser($path, count($this->sourceNames), $this->encoding);
251
 
252
  $this->sourceNames[] = $path;
253
  $this->addParsedFile($path);
@@ -305,7 +357,7 @@ class Compiler
305
  * @param string $type
306
  * @param array $selectors
307
  *
308
- * @return \Leafo\ScssPhp\Formatter\OutputBlock
309
  */
310
  protected function makeOutputBlock($type, $selectors = null)
311
  {
@@ -316,9 +368,16 @@ class Compiler
316
  $out->parent = $this->scope;
317
  $out->selectors = $selectors;
318
  $out->depth = $this->env->depth;
319
- $out->sourceName = $this->env->block->sourceName;
320
- $out->sourceLine = $this->env->block->sourceLine;
321
- $out->sourceColumn = $this->env->block->sourceColumn;
 
 
 
 
 
 
 
322
 
323
  return $out;
324
  }
@@ -326,7 +385,7 @@ class Compiler
326
  /**
327
  * Compile root
328
  *
329
- * @param \Leafo\ScssPhp\Block $rootBlock
330
  */
331
  protected function compileRoot(Block $rootBlock)
332
  {
@@ -365,8 +424,8 @@ class Compiler
365
  /**
366
  * Flatten selectors
367
  *
368
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
369
- * @param string $parentKey
370
  */
371
  protected function flattenSelectors(OutputBlock $block, $parentKey = null)
372
  {
@@ -421,6 +480,37 @@ class Compiler
421
  }
422
  }
423
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  /**
425
  * Match extends
426
  *
@@ -431,14 +521,33 @@ class Compiler
431
  */
432
  protected function matchExtends($selector, &$out, $from = 0, $initial = true)
433
  {
 
 
 
 
 
 
 
 
434
  foreach ($selector as $i => $part) {
435
  if ($i < $from) {
436
  continue;
437
  }
438
 
 
 
 
 
 
 
 
 
 
 
439
  if ($this->matchExtendsSingle($part, $origin)) {
440
- $after = array_slice($selector, $i + 1);
441
- $before = array_slice($selector, 0, $i);
 
442
 
443
  list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
444
 
@@ -446,7 +555,7 @@ class Compiler
446
  $k = 0;
447
 
448
  // remove shared parts
449
- if ($initial) {
450
  while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
451
  $k++;
452
  }
@@ -456,7 +565,14 @@ class Compiler
456
  $tempReplacement = $k > 0 ? array_slice($new, $k) : $new;
457
 
458
  for ($l = count($tempReplacement) - 1; $l >= 0; $l--) {
459
- $slice = $tempReplacement[$l];
 
 
 
 
 
 
 
460
  array_unshift($replacement, $slice);
461
 
462
  if (! $this->isImmediateRelationshipCombinator(end($slice))) {
@@ -483,18 +599,19 @@ class Compiler
483
  $out[] = $result;
484
 
485
  // recursively check for more matches
486
- $this->matchExtends($result, $out, count($before) + count($mergedBefore), false);
 
487
 
488
  // selector sequence merging
489
  if (! empty($before) && count($new) > 1) {
490
- $sharedParts = $k > 0 ? array_slice($before, 0, $k) : [];
491
  $postSharedParts = $k > 0 ? array_slice($before, $k) : $before;
492
 
493
- list($injectBetweenSharedParts, $nonBreakable2) = $this->extractRelationshipFromFragment($afterBefore);
494
 
495
  $result2 = array_merge(
496
- $sharedParts,
497
- $injectBetweenSharedParts,
498
  $postSharedParts,
499
  $nonBreakable2,
500
  $nonBreakableBefore,
@@ -505,6 +622,8 @@ class Compiler
505
  $out[] = $result2;
506
  }
507
  }
 
 
508
  }
509
  }
510
  }
@@ -522,6 +641,11 @@ class Compiler
522
  $counts = [];
523
  $single = [];
524
 
 
 
 
 
 
525
  foreach ($rawSingle as $part) {
526
  // matches Number
527
  if (! is_string($part)) {
@@ -556,6 +680,8 @@ class Compiler
556
  foreach ($counts as $idx => $count) {
557
  list($target, $origin, /* $block */) = $this->extends[$idx];
558
 
 
 
559
  // check count
560
  if ($count !== count($target)) {
561
  continue;
@@ -596,7 +722,6 @@ class Compiler
596
  return $found;
597
  }
598
 
599
-
600
  /**
601
  * Extract a relationship from the fragment.
602
  *
@@ -606,6 +731,7 @@ class Compiler
606
  * the rest.
607
  *
608
  * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
 
609
  * @return array The selector without the relationship fragment if any, the relationship fragment.
610
  */
611
  protected function extractRelationshipFromFragment(array $fragment)
@@ -669,19 +795,24 @@ class Compiler
669
  /**
670
  * Compile media
671
  *
672
- * @param \Leafo\ScssPhp\Block $media
673
  */
674
  protected function compileMedia(Block $media)
675
  {
676
  $this->pushEnv($media);
677
 
678
- $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
679
-
680
- if (! empty($mediaQuery)) {
681
- $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
682
 
 
 
683
  $parentScope = $this->mediaParent($this->scope);
684
- $parentScope->children[] = $this->scope;
 
 
 
 
 
 
685
 
686
  // top level properties in a media cause it to be wrapped
687
  $needsWrap = false;
@@ -701,21 +832,44 @@ class Compiler
701
 
702
  if ($needsWrap) {
703
  $wrapped = new Block;
704
- $wrapped->sourceName = $media->sourceName;
705
- $wrapped->sourceIndex = $media->sourceIndex;
706
- $wrapped->sourceLine = $media->sourceLine;
707
  $wrapped->sourceColumn = $media->sourceColumn;
708
- $wrapped->selectors = [];
709
- $wrapped->comments = [];
710
- $wrapped->parent = $media;
711
- $wrapped->children = $media->children;
712
 
713
  $media->children = [[Type::T_BLOCK, $wrapped]];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
714
  }
715
 
716
  $this->compileChildrenNoReturn($media->children, $this->scope);
717
 
718
- $this->scope = $this->scope->parent;
719
  }
720
 
721
  $this->popEnv();
@@ -724,9 +878,9 @@ class Compiler
724
  /**
725
  * Media parent
726
  *
727
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope
728
  *
729
- * @return \Leafo\ScssPhp\Formatter\OutputBlock
730
  */
731
  protected function mediaParent(OutputBlock $scope)
732
  {
@@ -744,7 +898,7 @@ class Compiler
744
  /**
745
  * Compile directive
746
  *
747
- * @param \Leafo\ScssPhp\Block $block
748
  */
749
  protected function compileDirective(Block $block)
750
  {
@@ -764,13 +918,13 @@ class Compiler
764
  /**
765
  * Compile at-root
766
  *
767
- * @param \Leafo\ScssPhp\Block $block
768
  */
769
  protected function compileAtRoot(Block $block)
770
  {
771
  $env = $this->pushEnv($block);
772
  $envs = $this->compactEnv($env);
773
- $without = isset($block->with) ? $this->compileWith($block->with) : static::WITH_RULE;
774
 
775
  // wrap inline selector
776
  if ($block->selector) {
@@ -783,18 +937,29 @@ class Compiler
783
  $wrapped->comments = [];
784
  $wrapped->parent = $block;
785
  $wrapped->children = $block->children;
 
786
 
787
  $block->children = [[Type::T_BLOCK, $wrapped]];
 
 
 
 
 
 
 
 
 
788
  }
789
 
790
- $this->env = $this->filterWithout($envs, $without);
791
- $newBlock = $this->spliceTree($envs, $block, $without);
792
 
793
  $saveScope = $this->scope;
794
- $this->scope = $this->rootBlock;
795
 
796
- $this->compileChild($newBlock, $this->scope);
 
797
 
 
798
  $this->scope = $saveScope;
799
  $this->env = $this->extractEnv($envs);
800
 
@@ -802,152 +967,184 @@ class Compiler
802
  }
803
 
804
  /**
805
- * Splice parse tree
806
  *
807
- * @param array $envs
808
- * @param \Leafo\ScssPhp\Block $block
809
- * @param integer $without
810
  *
811
- * @return array
812
  */
813
- private function spliceTree($envs, Block $block, $without)
814
  {
815
- $newBlock = null;
816
 
817
- foreach ($envs as $e) {
818
- if (! isset($e->block)) {
819
- continue;
820
- }
821
 
822
- if ($e->block === $block) {
823
- continue;
 
 
 
 
 
 
824
  }
825
 
826
- if (isset($e->block->type) && $e->block->type === Type::T_AT_ROOT) {
827
- continue;
 
 
 
 
 
 
 
 
 
828
  }
829
 
830
- if ($e->block && $this->isWithout($without, $e->block)) {
831
- continue;
 
 
832
  }
 
833
 
834
- $b = new Block;
835
- $b->sourceName = $e->block->sourceName;
836
- $b->sourceIndex = $e->block->sourceIndex;
837
- $b->sourceLine = $e->block->sourceLine;
838
- $b->sourceColumn = $e->block->sourceColumn;
839
- $b->selectors = [];
840
- $b->comments = $e->block->comments;
841
- $b->parent = null;
842
 
843
- if ($newBlock) {
844
- $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
845
 
846
- $b->children = [[$type, $newBlock]];
847
 
848
- $newBlock->parent = $b;
849
- } elseif (count($block->children)) {
850
- foreach ($block->children as $child) {
851
- if ($child[0] === Type::T_BLOCK) {
852
- $child[1]->parent = $b;
853
- }
854
- }
855
 
856
- $b->children = $block->children;
857
- }
 
 
 
 
858
 
859
- if (isset($e->block->type)) {
860
- $b->type = $e->block->type;
861
- }
862
 
863
- if (isset($e->block->name)) {
864
- $b->name = $e->block->name;
865
- }
 
 
 
 
 
 
 
 
 
 
 
866
 
867
- if (isset($e->block->queryList)) {
868
- $b->queryList = $e->block->queryList;
 
869
  }
 
870
 
871
- if (isset($e->block->value)) {
872
- $b->value = $e->block->value;
873
- }
874
 
875
- $newBlock = $b;
 
 
 
 
 
 
 
 
 
 
 
876
  }
877
 
878
- $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
 
 
 
 
 
 
879
 
880
- return [$type, $newBlock];
881
  }
882
 
883
  /**
884
- * Compile @at-root's with: inclusion / without: exclusion into filter flags
885
  *
886
- * @param array $with
887
  *
888
- * @return integer
889
  */
890
- private function compileWith($with)
891
  {
892
- static $mapping = [
893
- 'rule' => self::WITH_RULE,
894
- 'media' => self::WITH_MEDIA,
895
- 'supports' => self::WITH_SUPPORTS,
896
- 'all' => self::WITH_ALL,
897
- ];
898
-
899
- // exclude selectors by default
900
- $without = static::WITH_RULE;
901
 
902
- if ($this->libMapHasKey([$with, static::$with])) {
903
- $without = static::WITH_ALL;
 
 
904
 
905
- $list = $this->coerceList($this->libMapGet([$with, static::$with]));
906
-
907
- foreach ($list[2] as $item) {
908
- $keyword = $this->compileStringContent($this->coerceString($item));
909
 
910
- if (array_key_exists($keyword, $mapping)) {
911
- $without &= ~($mapping[$keyword]);
912
  }
913
  }
914
- }
915
-
916
- if ($this->libMapHasKey([$with, static::$without])) {
917
- $without = 0;
918
 
919
- $list = $this->coerceList($this->libMapGet([$with, static::$without]));
 
 
920
 
921
- foreach ($list[2] as $item) {
922
- $keyword = $this->compileStringContent($this->coerceString($item));
923
 
924
- if (array_key_exists($keyword, $mapping)) {
925
- $without |= $mapping[$keyword];
926
  }
927
  }
928
  }
929
 
930
- return $without;
931
  }
932
 
933
  /**
934
  * Filter env stack
935
  *
936
  * @param array $envs
937
- * @param integer $without
 
938
  *
939
- * @return \Leafo\ScssPhp\Compiler\Environment
940
  */
941
- private function filterWithout($envs, $without)
942
  {
943
  $filtered = [];
944
 
945
  foreach ($envs as $e) {
946
- if ($e->block && $this->isWithout($without, $e->block)) {
947
- continue;
 
 
 
 
 
948
  }
949
-
950
- $filtered[] = $e;
951
  }
952
 
953
  return $this->extractEnv($filtered);
@@ -956,31 +1153,64 @@ class Compiler
956
  /**
957
  * Filter WITH rules
958
  *
959
- * @param integer $without
960
- * @param \Leafo\ScssPhp\Block $block
 
961
  *
962
  * @return boolean
963
  */
964
- private function isWithout($without, Block $block)
965
  {
966
- if ((($without & static::WITH_RULE) && isset($block->selectors)) ||
967
- (($without & static::WITH_MEDIA) &&
968
- isset($block->type) && $block->type === Type::T_MEDIA) ||
969
- (($without & static::WITH_SUPPORTS) &&
970
- isset($block->type) && $block->type === Type::T_DIRECTIVE &&
971
- isset($block->name) && $block->name === 'supports')
972
- ) {
973
- return true;
 
 
 
 
 
 
 
 
 
 
 
974
  }
975
 
976
- return false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
977
  }
978
 
 
979
  /**
980
  * Compile keyframe block
981
  *
982
- * @param \Leafo\ScssPhp\Block $block
983
- * @param array $selectors
984
  */
985
  protected function compileKeyframeBlock(Block $block, $selectors)
986
  {
@@ -1004,11 +1234,44 @@ class Compiler
1004
  $this->popEnv();
1005
  }
1006
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1007
  /**
1008
  * Compile nested block
1009
  *
1010
- * @param \Leafo\ScssPhp\Block $block
1011
- * @param array $selectors
1012
  */
1013
  protected function compileNestedBlock(Block $block, $selectors)
1014
  {
@@ -1017,6 +1280,35 @@ class Compiler
1017
  $this->scope = $this->makeOutputBlock($block->type, $selectors);
1018
  $this->scope->parent->children[] = $this->scope;
1019
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1020
  $this->compileChildrenNoReturn($block->children, $this->scope);
1021
 
1022
  $this->scope = $this->scope->parent;
@@ -1040,7 +1332,7 @@ class Compiler
1040
  *
1041
  * @see Compiler::compileChild()
1042
  *
1043
- * @param \Leafo\ScssPhp\Block $block
1044
  */
1045
  protected function compileBlock(Block $block)
1046
  {
@@ -1076,9 +1368,22 @@ class Compiler
1076
  $this->scope->children[] = $out;
1077
 
1078
  if (count($block->children)) {
1079
- $out->selectors = $this->multiplySelectors($env);
 
 
 
 
 
 
 
 
 
 
1080
 
1081
- $this->compileChildrenNoReturn($block->children, $out);
 
 
 
1082
  }
1083
 
1084
  $this->formatter->stripSemicolon($out->lines);
@@ -1094,7 +1399,8 @@ class Compiler
1094
  protected function compileComment($block)
1095
  {
1096
  $out = $this->makeOutputBlock(Type::T_COMMENT);
1097
- $out->lines[] = $block[1];
 
1098
  $this->scope->children[] = $out;
1099
  }
1100
 
@@ -1113,6 +1419,7 @@ class Compiler
1113
 
1114
  // after evaluating interpolates, we might need a second pass
1115
  if ($this->shouldEvaluate) {
 
1116
  $buffer = $this->collapseSelectors($selectors);
1117
  $parser = $this->parserFactory(__METHOD__);
1118
 
@@ -1167,28 +1474,87 @@ class Compiler
1167
  /**
1168
  * Collapse selectors
1169
  *
1170
- * @param array $selectors
 
 
 
1171
  *
1172
  * @return string
1173
  */
1174
- protected function collapseSelectors($selectors)
1175
  {
1176
  $parts = [];
1177
 
1178
  foreach ($selectors as $selector) {
1179
- $output = '';
 
1180
 
1181
- array_walk_recursive(
1182
- $selector,
1183
- function ($value, $key) use (&$output) {
1184
- $output .= $value;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1185
  }
1186
- );
 
 
 
 
 
 
 
 
 
1187
 
1188
  $parts[] = $output;
1189
  }
1190
 
1191
- return implode(', ', $parts);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1192
  }
1193
 
1194
  /**
@@ -1295,16 +1661,42 @@ class Compiler
1295
  return false;
1296
  }
1297
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1298
  /**
1299
  * Compile children and return result
1300
  *
1301
- * @param array $stms
1302
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
 
1303
  *
1304
- * @return array
1305
  */
1306
- protected function compileChildren($stms, OutputBlock $out)
1307
  {
 
 
1308
  foreach ($stms as $stm) {
1309
  $ret = $this->compileChild($stm, $out);
1310
 
@@ -1312,20 +1704,38 @@ class Compiler
1312
  return $ret;
1313
  }
1314
  }
 
 
 
 
1315
  }
1316
 
1317
  /**
1318
  * Compile children and throw exception if unexpected @return
1319
  *
1320
- * @param array $stms
1321
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
 
 
1322
  *
1323
  * @throws \Exception
1324
  */
1325
- protected function compileChildrenNoReturn($stms, OutputBlock $out)
1326
  {
 
 
1327
  foreach ($stms as $stm) {
1328
- $ret = $this->compileChild($stm, $out);
 
 
 
 
 
 
 
 
 
 
1329
 
1330
  if (isset($ret)) {
1331
  $this->throwError('@return may only be used within a function');
@@ -1333,38 +1743,140 @@ class Compiler
1333
  return;
1334
  }
1335
  }
 
 
1336
  }
1337
 
 
1338
  /**
1339
- * Compile media query
1340
  *
1341
  * @param array $queryList
1342
  *
1343
- * @return string
1344
  */
1345
- protected function compileMediaQuery($queryList)
1346
- {
1347
- $out = '@media';
1348
- $first = true;
1349
-
1350
- foreach ($queryList as $query) {
1351
- $type = null;
1352
- $parts = [];
 
 
 
 
 
 
 
 
 
 
 
 
1353
 
1354
- foreach ($query as $q) {
1355
- switch ($q[0]) {
1356
- case Type::T_MEDIA_TYPE:
1357
- if ($type) {
1358
- $type = $this->mergeMediaTypes(
1359
- $type,
1360
- array_map([$this, 'compileValue'], array_slice($q, 1))
1361
- );
1362
-
1363
- if (empty($type)) { // merge failed
1364
- return null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1365
  }
1366
- } else {
1367
- $type = array_map([$this, 'compileValue'], array_slice($q, 1));
1368
  }
1369
  break;
1370
 
@@ -1393,20 +1905,34 @@ class Compiler
1393
  }
1394
 
1395
  if (! empty($parts)) {
1396
- if ($first) {
1397
- $first = false;
1398
- $out .= ' ';
1399
- } else {
1400
- $out .= $this->formatter->tagSeparator;
1401
  }
1402
 
1403
- $out .= implode(' and ', $parts);
1404
  }
1405
  }
1406
 
 
 
 
 
 
 
 
 
 
1407
  return $out;
1408
  }
1409
 
 
 
 
 
 
 
 
 
1410
  protected function mergeDirectRelationships($selectors1, $selectors2)
1411
  {
1412
  if (empty($selectors1) || empty($selectors2)) {
@@ -1416,7 +1942,7 @@ class Compiler
1416
  $part1 = end($selectors1);
1417
  $part2 = end($selectors2);
1418
 
1419
- if (! $this->isImmediateRelationshipCombinator($part1[0]) || $part1 !== $part2) {
1420
  return array_merge($selectors1, $selectors2);
1421
  }
1422
 
@@ -1426,13 +1952,18 @@ class Compiler
1426
  $part1 = array_pop($selectors1);
1427
  $part2 = array_pop($selectors2);
1428
 
1429
- if ($this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
1430
- $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
 
 
 
 
 
 
1431
  break;
1432
  }
1433
 
1434
  array_unshift($merged, $part1);
1435
- array_unshift($merged, [array_pop($selectors1)[0] . array_pop($selectors2)[0]]);
1436
  } while (! empty($selectors1) && ! empty($selectors2));
1437
 
1438
  return $merged;
@@ -1507,13 +2038,13 @@ class Compiler
1507
  /**
1508
  * Compile import; returns true if the value was something that could be imported
1509
  *
1510
- * @param array $rawPath
1511
- * @param array $out
1512
- * @param boolean $once
1513
  *
1514
  * @return boolean
1515
  */
1516
- protected function compileImport($rawPath, $out, $once = false)
1517
  {
1518
  if ($rawPath[0] === Type::T_STRING) {
1519
  $path = $this->compileStringContent($rawPath);
@@ -1552,38 +2083,135 @@ class Compiler
1552
  return false;
1553
  }
1554
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1555
  /**
1556
  * Compile child; returns a value to halt execution
1557
  *
1558
- * @param array $child
1559
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
1560
  *
1561
  * @return array
1562
  */
1563
  protected function compileChild($child, OutputBlock $out)
1564
  {
1565
- $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
1566
- $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
1567
- $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
 
 
 
 
 
 
 
 
 
 
 
 
 
1568
 
1569
  switch ($child[0]) {
1570
  case Type::T_SCSSPHP_IMPORT_ONCE:
1571
- list(, $rawPath) = $child;
1572
-
1573
- $rawPath = $this->reduce($rawPath);
1574
 
1575
  if (! $this->compileImport($rawPath, $out, true)) {
1576
- $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
1577
  }
1578
  break;
1579
 
1580
  case Type::T_IMPORT:
1581
- list(, $rawPath) = $child;
1582
-
1583
- $rawPath = $this->reduce($rawPath);
1584
 
1585
  if (! $this->compileImport($rawPath, $out)) {
1586
- $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
1587
  }
1588
  break;
1589
 
@@ -1606,8 +2234,7 @@ class Compiler
1606
  case Type::T_CHARSET:
1607
  if (! $this->charsetSeen) {
1608
  $this->charsetSeen = true;
1609
-
1610
- $out->lines[] = '@charset ' . $this->compileValue($child[1]) . ';';
1611
  }
1612
  break;
1613
 
@@ -1620,16 +2247,16 @@ class Compiler
1620
  $isGlobal = in_array('!global', $flags);
1621
 
1622
  if ($isGlobal) {
1623
- $this->set($name[1], $this->reduce($value), false, $this->rootEnv);
1624
  break;
1625
  }
1626
 
1627
  $shouldSet = $isDefault &&
1628
- (($result = $this->get($name[1], false)) === null
1629
- || $result === static::$null);
1630
 
1631
  if (! $isDefault || $shouldSet) {
1632
- $this->set($name[1], $this->reduce($value));
1633
  }
1634
  break;
1635
  }
@@ -1637,11 +2264,25 @@ class Compiler
1637
  $compiledName = $this->compileValue($name);
1638
 
1639
  // handle shorthand syntax: size / line-height
1640
- if ($compiledName === 'font') {
1641
- if ($value[0] === Type::T_EXPRESSION && $value[1] === '/') {
1642
- $value = $this->expToString($value);
1643
- } elseif ($value[0] === Type::T_LIST) {
1644
- foreach ($value[2] as &$item) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1645
  if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
1646
  $item = $this->expToString($item);
1647
  }
@@ -1661,10 +2302,11 @@ class Compiler
1661
 
1662
  $compiledValue = $this->compileValue($value);
1663
 
1664
- $out->lines[] = $this->formatter->property(
1665
  $compiledName,
1666
  $compiledValue
1667
  );
 
1668
  break;
1669
 
1670
  case Type::T_COMMENT:
@@ -1673,20 +2315,18 @@ class Compiler
1673
  break;
1674
  }
1675
 
1676
- $out->lines[] = $child[1];
1677
  break;
1678
 
1679
  case Type::T_MIXIN:
1680
  case Type::T_FUNCTION:
1681
  list(, $block) = $child;
1682
 
1683
- $this->set(static::$namespaces[$block->type] . $block->name, $block);
1684
  break;
1685
 
1686
  case Type::T_EXTEND:
1687
- list(, $selectors) = $child;
1688
-
1689
- foreach ($selectors as $sel) {
1690
  $results = $this->evalSelectors([$sel]);
1691
 
1692
  foreach ($results as $result) {
@@ -1820,26 +2460,7 @@ class Compiler
1820
  return $this->reduce($child[1], true);
1821
 
1822
  case Type::T_NESTED_PROPERTY:
1823
- list(, $prop) = $child;
1824
-
1825
- $prefixed = [];
1826
- $prefix = $this->compileValue($prop->prefix) . '-';
1827
-
1828
- foreach ($prop->children as $child) {
1829
- switch ($child[0]) {
1830
- case Type::T_ASSIGN:
1831
- array_unshift($child[1][2], $prefix);
1832
- break;
1833
-
1834
- case Type::T_NESTED_PROPERTY:
1835
- array_unshift($child[1]->prefix[2], $prefix);
1836
- break;
1837
- }
1838
-
1839
- $prefixed[] = $child;
1840
- }
1841
-
1842
- $this->compileChildrenNoReturn($prefixed, $out);
1843
  break;
1844
 
1845
  case Type::T_INCLUDE:
@@ -1862,10 +2483,36 @@ class Compiler
1862
  $storeEnv = $this->storeEnv;
1863
  $this->storeEnv = $this->env;
1864
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1865
  if (isset($content)) {
1866
- $content->scope = $callingScope;
 
1867
 
1868
- $this->setRaw(static::$namespaces['special'] . 'content', $content, $this->env);
 
 
1869
  }
1870
 
1871
  if (isset($mixin->args)) {
@@ -1874,7 +2521,7 @@ class Compiler
1874
 
1875
  $this->env->marker = 'mixin';
1876
 
1877
- $this->compileChildrenNoReturn($mixin->children, $out);
1878
 
1879
  $this->storeEnv = $storeEnv;
1880
 
@@ -1882,19 +2529,18 @@ class Compiler
1882
  break;
1883
 
1884
  case Type::T_MIXIN_CONTENT:
1885
- $content = $this->get(static::$namespaces['special'] . 'content', false, $this->getStoreEnv())
1886
- ?: $this->get(static::$namespaces['special'] . 'content', false, $this->env);
1887
 
1888
  if (! $content) {
1889
  $content = new \stdClass();
1890
  $content->scope = new \stdClass();
1891
- $content->children = $this->storeEnv->parent->block->children;
1892
  break;
1893
  }
1894
 
1895
  $storeEnv = $this->storeEnv;
1896
  $this->storeEnv = $content->scope;
1897
-
1898
  $this->compileChildrenNoReturn($content->children, $out);
1899
 
1900
  $this->storeEnv = $storeEnv;
@@ -1903,25 +2549,28 @@ class Compiler
1903
  case Type::T_DEBUG:
1904
  list(, $value) = $child;
1905
 
 
1906
  $line = $this->sourceLine;
1907
  $value = $this->compileValue($this->reduce($value, true));
1908
- fwrite($this->stderr, "Line $line DEBUG: $value\n");
1909
  break;
1910
 
1911
  case Type::T_WARN:
1912
  list(, $value) = $child;
1913
 
 
1914
  $line = $this->sourceLine;
1915
  $value = $this->compileValue($this->reduce($value, true));
1916
- fwrite($this->stderr, "Line $line WARN: $value\n");
1917
  break;
1918
 
1919
  case Type::T_ERROR:
1920
  list(, $value) = $child;
1921
 
 
1922
  $line = $this->sourceLine;
1923
  $value = $this->compileValue($this->reduce($value, true));
1924
- $this->throwError("Line $line ERROR: $value\n");
1925
  break;
1926
 
1927
  case Type::T_CONTROL:
@@ -1966,7 +2615,7 @@ class Compiler
1966
  *
1967
  * @param array $value
1968
  *
1969
- * @return array
1970
  */
1971
  protected function isTruthy($value)
1972
  {
@@ -1997,7 +2646,7 @@ class Compiler
1997
  switch ($value[0]) {
1998
  case Type::T_EXPRESSION:
1999
  if ($value[1] === '/') {
2000
- return $this->shouldEval($value[2], $value[3]);
2001
  }
2002
 
2003
  // fall-thru
@@ -2015,13 +2664,12 @@ class Compiler
2015
  * @param array $value
2016
  * @param boolean $inExp
2017
  *
2018
- * @return array|\Leafo\ScssPhp\Node\Number
2019
  */
2020
  protected function reduce($value, $inExp = false)
2021
  {
2022
- list($type) = $value;
2023
 
2024
- switch ($type) {
2025
  case Type::T_EXPRESSION:
2026
  list(, $op, $left, $right, $inParens) = $value;
2027
 
@@ -2035,9 +2683,9 @@ class Compiler
2035
  }
2036
 
2037
  // special case: looks like css shorthand
2038
- if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2])
2039
- && (($right[0] !== Type::T_NUMBER && $right[2] != '')
2040
- || ($right[0] === Type::T_NUMBER && ! $right->unitless()))
2041
  ) {
2042
  return $this->expToString($value);
2043
  }
@@ -2154,9 +2802,7 @@ class Compiler
2154
  return [Type::T_STRING, '', [$op, $exp]];
2155
 
2156
  case Type::T_VARIABLE:
2157
- list(, $name) = $value;
2158
-
2159
- return $this->reduce($this->get($name));
2160
 
2161
  case Type::T_LIST:
2162
  foreach ($value[2] as &$item) {
@@ -2187,13 +2833,19 @@ class Compiler
2187
 
2188
  case Type::T_INTERPOLATE:
2189
  $value[1] = $this->reduce($value[1]);
 
 
 
2190
 
2191
  return $value;
2192
 
2193
  case Type::T_FUNCTION_CALL:
2194
- list(, $name, $argValues) = $value;
2195
 
2196
- return $this->fncall($name, $argValues);
 
 
 
2197
 
2198
  default:
2199
  return $value;
@@ -2208,7 +2860,7 @@ class Compiler
2208
  *
2209
  * @return array|null
2210
  */
2211
- private function fncall($name, $argValues)
2212
  {
2213
  // SCSS @function
2214
  if ($this->callScssFunction($name, $argValues, $returnValue)) {
@@ -2254,9 +2906,8 @@ class Compiler
2254
  public function normalizeValue($value)
2255
  {
2256
  $value = $this->coerceForExpression($this->reduce($value));
2257
- list($type) = $value;
2258
 
2259
- switch ($type) {
2260
  case Type::T_LIST:
2261
  $value = $this->extractInterpolation($value);
2262
 
@@ -2271,7 +2922,7 @@ class Compiler
2271
  return $value;
2272
 
2273
  case Type::T_STRING:
2274
- return [$type, '"', [$this->compileStringContent($value)]];
2275
 
2276
  case Type::T_NUMBER:
2277
  return $value->normalize();
@@ -2290,7 +2941,7 @@ class Compiler
2290
  * @param array $left
2291
  * @param array $right
2292
  *
2293
- * @return \Leafo\ScssPhp\Node\Number
2294
  */
2295
  protected function opAddNumberNumber($left, $right)
2296
  {
@@ -2303,7 +2954,7 @@ class Compiler
2303
  * @param array $left
2304
  * @param array $right
2305
  *
2306
- * @return \Leafo\ScssPhp\Node\Number
2307
  */
2308
  protected function opMulNumberNumber($left, $right)
2309
  {
@@ -2316,7 +2967,7 @@ class Compiler
2316
  * @param array $left
2317
  * @param array $right
2318
  *
2319
- * @return \Leafo\ScssPhp\Node\Number
2320
  */
2321
  protected function opSubNumberNumber($left, $right)
2322
  {
@@ -2329,7 +2980,7 @@ class Compiler
2329
  * @param array $left
2330
  * @param array $right
2331
  *
2332
- * @return array|\Leafo\ScssPhp\Node\Number
2333
  */
2334
  protected function opDivNumberNumber($left, $right)
2335
  {
@@ -2346,7 +2997,7 @@ class Compiler
2346
  * @param array $left
2347
  * @param array $right
2348
  *
2349
- * @return \Leafo\ScssPhp\Node\Number
2350
  */
2351
  protected function opModNumberNumber($left, $right)
2352
  {
@@ -2359,7 +3010,7 @@ class Compiler
2359
  * @param array $left
2360
  * @param array $right
2361
  *
2362
- * @return array
2363
  */
2364
  protected function opAdd($left, $right)
2365
  {
@@ -2382,6 +3033,8 @@ class Compiler
2382
 
2383
  return $strRight;
2384
  }
 
 
2385
  }
2386
 
2387
  /**
@@ -2391,15 +3044,21 @@ class Compiler
2391
  * @param array $right
2392
  * @param boolean $shouldEval
2393
  *
2394
- * @return array
2395
  */
2396
  protected function opAnd($left, $right, $shouldEval)
2397
  {
 
 
 
 
2398
  if (! $shouldEval) {
2399
- return;
 
 
2400
  }
2401
 
2402
- if ($left !== static::$false and $left !== static::$null) {
2403
  return $this->reduce($right, true);
2404
  }
2405
 
@@ -2413,15 +3072,21 @@ class Compiler
2413
  * @param array $right
2414
  * @param boolean $shouldEval
2415
  *
2416
- * @return array
2417
  */
2418
  protected function opOr($left, $right, $shouldEval)
2419
  {
 
 
 
 
2420
  if (! $shouldEval) {
2421
- return;
 
 
2422
  }
2423
 
2424
- if ($left !== static::$false and $left !== static::$null) {
2425
  return $left;
2426
  }
2427
 
@@ -2632,7 +3297,7 @@ class Compiler
2632
  * @param array $left
2633
  * @param array $right
2634
  *
2635
- * @return \Leafo\ScssPhp\Node\Number
2636
  */
2637
  protected function opCmpNumberNumber($left, $right)
2638
  {
@@ -2676,9 +3341,7 @@ class Compiler
2676
  {
2677
  $value = $this->reduce($value);
2678
 
2679
- list($type) = $value;
2680
-
2681
- switch ($type) {
2682
  case Type::T_KEYWORD:
2683
  return $value[1];
2684
 
@@ -2694,7 +3357,9 @@ class Compiler
2694
  $b = round($b);
2695
 
2696
  if (count($value) === 5 && $value[4] !== 1) { // rgba
2697
- return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $value[4] . ')';
 
 
2698
  }
2699
 
2700
  $h = sprintf('#%02x%02x%02x', $r, $g, $b);
@@ -2771,11 +3436,8 @@ class Compiler
2771
  return $left . $this->compileValue($interpolate) . $right;
2772
 
2773
  case Type::T_INTERPOLATE:
2774
- // raw parse node
2775
- list(, $exp) = $value;
2776
-
2777
  // strip quotes if it's a string
2778
- $reduced = $this->reduce($exp);
2779
 
2780
  switch ($reduced[0]) {
2781
  case Type::T_LIST:
@@ -2825,7 +3487,7 @@ class Compiler
2825
  return 'null';
2826
 
2827
  default:
2828
- $this->throwError("unknown value type: $type");
2829
  }
2830
  }
2831
 
@@ -2889,44 +3551,70 @@ class Compiler
2889
  /**
2890
  * Find the final set of selectors
2891
  *
2892
- * @param \Leafo\ScssPhp\Compiler\Environment $env
 
2893
  *
2894
  * @return array
2895
  */
2896
- protected function multiplySelectors(Environment $env)
2897
  {
2898
  $envs = $this->compactEnv($env);
2899
  $selectors = [];
2900
  $parentSelectors = [[]];
2901
 
 
 
 
 
 
 
2902
  while ($env = array_pop($envs)) {
2903
  if (empty($env->selectors)) {
2904
  continue;
2905
  }
2906
 
2907
- $selectors = [];
2908
 
2909
- foreach ($env->selectors as $selector) {
2910
- foreach ($parentSelectors as $parent) {
2911
- $selectors[] = $this->joinSelectors($parent, $selector);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2912
  }
2913
- }
2914
 
2915
  $parentSelectors = $selectors;
2916
  }
2917
 
 
 
2918
  return $selectors;
2919
  }
2920
 
2921
  /**
2922
  * Join selectors; looks for & to replace, or append parent before child
2923
  *
2924
- * @param array $parent
2925
- * @param array $child
2926
- *
 
 
2927
  * @return array
2928
  */
2929
- protected function joinSelectors($parent, $child)
2930
  {
2931
  $setSelf = false;
2932
  $out = [];
@@ -2935,16 +3623,33 @@ class Compiler
2935
  $newPart = [];
2936
 
2937
  foreach ($part as $p) {
2938
- if ($p === static::$selfSelector) {
 
 
 
 
 
2939
  $setSelf = true;
2940
 
2941
- foreach ($parent as $i => $parentPart) {
 
 
 
 
2942
  if ($i > 0) {
2943
  $out[] = $newPart;
2944
  $newPart = [];
2945
  }
2946
 
2947
  foreach ($parentPart as $pp) {
 
 
 
 
 
 
 
 
2948
  $newPart[] = $pp;
2949
  }
2950
  }
@@ -2962,8 +3667,8 @@ class Compiler
2962
  /**
2963
  * Multiply media
2964
  *
2965
- * @param \Leafo\ScssPhp\Compiler\Environment $env
2966
- * @param array $childQueries
2967
  *
2968
  * @return array
2969
  */
@@ -2984,6 +3689,12 @@ class Compiler
2984
  ? $env->block->queryList
2985
  : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
2986
 
 
 
 
 
 
 
2987
  if ($childQueries === null) {
2988
  $childQueries = $parentQueries;
2989
  } else {
@@ -2992,7 +3703,11 @@ class Compiler
2992
 
2993
  foreach ($parentQueries as $parentQuery) {
2994
  foreach ($originalQueries as $childQuery) {
2995
- $childQueries []= array_merge($parentQuery, $childQuery);
 
 
 
 
2996
  }
2997
  }
2998
  }
@@ -3003,11 +3718,11 @@ class Compiler
3003
  /**
3004
  * Convert env linked list to stack
3005
  *
3006
- * @param \Leafo\ScssPhp\Compiler\Environment $env
3007
  *
3008
  * @return array
3009
  */
3010
- private function compactEnv(Environment $env)
3011
  {
3012
  for ($envs = []; $env; $env = $env->parent) {
3013
  $envs[] = $env;
@@ -3021,9 +3736,9 @@ class Compiler
3021
  *
3022
  * @param array $envs
3023
  *
3024
- * @return \Leafo\ScssPhp\Compiler\Environment
3025
  */
3026
- private function extractEnv($envs)
3027
  {
3028
  for ($env = null; $e = array_pop($envs);) {
3029
  $e->parent = $env;
@@ -3036,9 +3751,9 @@ class Compiler
3036
  /**
3037
  * Push environment
3038
  *
3039
- * @param \Leafo\ScssPhp\Block $block
3040
  *
3041
- * @return \Leafo\ScssPhp\Compiler\Environment
3042
  */
3043
  protected function pushEnv(Block $block = null)
3044
  {
@@ -3064,7 +3779,7 @@ class Compiler
3064
  /**
3065
  * Get store environment
3066
  *
3067
- * @return \Leafo\ScssPhp\Compiler\Environment
3068
  */
3069
  protected function getStoreEnv()
3070
  {
@@ -3074,12 +3789,13 @@ class Compiler
3074
  /**
3075
  * Set variable
3076
  *
3077
- * @param string $name
3078
- * @param mixed $value
3079
- * @param boolean $shadow
3080
- * @param \Leafo\ScssPhp\Compiler\Environment $env
 
3081
  */
3082
- protected function set($name, $value, $shadow = false, Environment $env = null)
3083
  {
3084
  $name = $this->normalizeName($name);
3085
 
@@ -3088,20 +3804,21 @@ class Compiler
3088
  }
3089
 
3090
  if ($shadow) {
3091
- $this->setRaw($name, $value, $env);
3092
  } else {
3093
- $this->setExisting($name, $value, $env);
3094
  }
3095
  }
3096
 
3097
  /**
3098
  * Set existing variable
3099
  *
3100
- * @param string $name
3101
- * @param mixed $value
3102
- * @param \Leafo\ScssPhp\Compiler\Environment $env
 
3103
  */
3104
- protected function setExisting($name, $value, Environment $env)
3105
  {
3106
  $storeEnv = $env;
3107
 
@@ -3126,18 +3843,27 @@ class Compiler
3126
  }
3127
 
3128
  $env->store[$name] = $value;
 
 
 
 
3129
  }
3130
 
3131
  /**
3132
  * Set raw variable
3133
  *
3134
- * @param string $name
3135
- * @param mixed $value
3136
- * @param \Leafo\ScssPhp\Compiler\Environment $env
 
3137
  */
3138
- protected function setRaw($name, $value, Environment $env)
3139
  {
3140
  $env->store[$name] = $value;
 
 
 
 
3141
  }
3142
 
3143
  /**
@@ -3145,13 +3871,14 @@ class Compiler
3145
  *
3146
  * @api
3147
  *
3148
- * @param string $name
3149
- * @param boolean $shouldThrow
3150
- * @param \Leafo\ScssPhp\Compiler\Environment $env
 
3151
  *
3152
- * @return mixed
3153
  */
3154
- public function get($name, $shouldThrow = true, Environment $env = null)
3155
  {
3156
  $normalizedName = $this->normalizeName($name);
3157
  $specialContentKey = static::$namespaces['special'] . 'content';
@@ -3163,15 +3890,24 @@ class Compiler
3163
  $nextIsRoot = false;
3164
  $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
3165
 
 
 
3166
  for (;;) {
 
 
 
 
3167
  if (array_key_exists($normalizedName, $env->store)) {
 
 
 
 
3168
  return $env->store[$normalizedName];
3169
  }
3170
 
3171
  if (! $hasNamespace && isset($env->marker)) {
3172
  if (! $nextIsRoot && ! empty($env->store[$specialContentKey])) {
3173
  $env = $env->store[$specialContentKey]->scope;
3174
- $nextIsRoot = true;
3175
  continue;
3176
  }
3177
 
@@ -3187,17 +3923,18 @@ class Compiler
3187
  }
3188
 
3189
  if ($shouldThrow) {
3190
- $this->throwError("Undefined variable \$$name");
3191
  }
3192
 
3193
  // found nothing
 
3194
  }
3195
 
3196
  /**
3197
  * Has variable?
3198
  *
3199
- * @param string $name
3200
- * @param \Leafo\ScssPhp\Compiler\Environment $env
3201
  *
3202
  * @return boolean
3203
  */
@@ -3299,7 +4036,7 @@ class Compiler
3299
  *
3300
  * @api
3301
  *
3302
- * @param string $path
3303
  */
3304
  public function addImportPath($path)
3305
  {
@@ -3421,10 +4158,10 @@ class Compiler
3421
  /**
3422
  * Import file
3423
  *
3424
- * @param string $path
3425
- * @param array $out
3426
  */
3427
- protected function importFile($path, $out)
3428
  {
3429
  // see if tree is cached
3430
  $realPath = realpath($path);
@@ -3472,9 +4209,12 @@ class Compiler
3472
  if (is_string($dir)) {
3473
  // check urls for normal import paths
3474
  foreach ($urls as $full) {
3475
- $full = $dir
3476
- . (! empty($dir) && substr($dir, -1) !== '/' ? '/' : '')
3477
- . $full;
 
 
 
3478
 
3479
  if ($this->fileExists($file = $full . '.scss') ||
3480
  ($hasExtension && $this->fileExists($file = $full))
@@ -3514,11 +4254,13 @@ class Compiler
3514
  *
3515
  * @param boolean $ignoreErrors
3516
  *
3517
- * @return \Leafo\ScssPhp\Compiler
3518
  */
3519
  public function setIgnoreErrors($ignoreErrors)
3520
  {
3521
  $this->ignoreErrors = $ignoreErrors;
 
 
3522
  }
3523
 
3524
  /**
@@ -3528,7 +4270,7 @@ class Compiler
3528
  *
3529
  * @param string $msg Message with optional sprintf()-style vararg parameters
3530
  *
3531
- * @throws \Leafo\ScssPhp\Exception\CompilerException
3532
  */
3533
  public function throwError($msg)
3534
  {
@@ -3536,16 +4278,61 @@ class Compiler
3536
  return;
3537
  }
3538
 
 
 
 
 
 
 
 
3539
  if (func_num_args() > 1) {
3540
  $msg = call_user_func_array('sprintf', func_get_args());
3541
  }
3542
 
3543
- $line = $this->sourceLine;
3544
- $msg = "$msg: line: $line";
 
 
 
 
 
3545
 
3546
  throw new CompilerException($msg);
3547
  }
3548
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3549
  /**
3550
  * Handle import loop
3551
  *
@@ -3611,7 +4398,7 @@ class Compiler
3611
 
3612
  $this->env->marker = 'function';
3613
 
3614
- $ret = $this->compileChildren($func->children, $tmp);
3615
 
3616
  $this->storeEnv = $storeEnv;
3617
 
@@ -3646,7 +4433,7 @@ class Compiler
3646
  return false;
3647
  }
3648
 
3649
- list($sorted, $kwargs) = $this->sortArgs($prototype, $args);
3650
 
3651
  if ($name !== 'if' && $name !== 'call') {
3652
  foreach ($sorted as &$val) {
@@ -3693,76 +4480,162 @@ class Compiler
3693
  *
3694
  * @return array
3695
  */
3696
- protected function sortArgs($prototype, $args)
3697
  {
3698
- $keyArgs = [];
3699
- $posArgs = [];
3700
 
3701
- // separate positional and keyword arguments
3702
- foreach ($args as $arg) {
3703
- list($key, $value) = $arg;
3704
 
3705
- $key = $key[1];
 
 
3706
 
3707
- if (empty($key)) {
3708
- $posArgs[] = $value;
3709
- } else {
3710
- $keyArgs[$key] = $value;
 
 
 
3711
  }
3712
- }
3713
 
3714
- if (! isset($prototype)) {
3715
  return [$posArgs, $keyArgs];
3716
  }
3717
 
3718
- // copy positional args
3719
- $finalArgs = array_pad($posArgs, count($prototype), null);
3720
 
3721
- // overwrite positional args with keyword args
3722
- foreach ($prototype as $i => $names) {
3723
- foreach ((array) $names as $name) {
3724
- if (isset($keyArgs[$name])) {
3725
- $finalArgs[$i] = $keyArgs[$name];
3726
- }
3727
- }
3728
  }
3729
 
3730
- return [$finalArgs, $keyArgs];
3731
- }
3732
 
3733
- /**
3734
- * Apply argument values per definition
3735
- *
3736
- * @param array $argDef
3737
- * @param array $argValues
3738
- *
3739
- * @throws \Exception
3740
- */
3741
- protected function applyArguments($argDef, $argValues)
3742
- {
3743
- $storeEnv = $this->getStoreEnv();
3744
 
3745
- $env = new Environment;
3746
- $env->store = $storeEnv->store;
3747
 
3748
- $hasVariable = false;
3749
- $args = [];
 
 
3750
 
3751
- foreach ($argDef as $i => $arg) {
3752
- list($name, $default, $isVariable) = $argDef[$i];
3753
 
3754
- $args[$name] = [$i, $name, $default, $isVariable];
3755
- $hasVariable |= $isVariable;
3756
- }
 
 
 
 
3757
 
3758
- $keywordArgs = [];
3759
- $deferredKeywordArgs = [];
3760
- $remaining = [];
3761
 
3762
- // assign the keyword args
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3763
  foreach ((array) $argValues as $arg) {
3764
  if (! empty($arg[0])) {
3765
- if (! isset($args[$arg[0][1]])) {
 
 
3766
  if ($hasVariable) {
3767
  $deferredKeywordArgs[$arg[0][1]] = $arg[1];
3768
  } else {
@@ -3775,17 +4648,30 @@ class Compiler
3775
  } else {
3776
  $keywordArgs[$arg[0][1]] = $arg[1];
3777
  }
3778
- } elseif (count($keywordArgs)) {
3779
- $this->throwError('Positional arguments must come before keyword arguments.');
3780
- break;
3781
  } elseif ($arg[2] === true) {
3782
  $val = $this->reduce($arg[1], true);
3783
 
3784
  if ($val[0] === Type::T_LIST) {
3785
  foreach ($val[2] as $name => $item) {
3786
  if (! is_numeric($name)) {
3787
- $keywordArgs[$name] = $item;
 
 
 
 
 
 
 
 
 
 
 
 
 
3788
  } else {
 
 
 
3789
  $remaining[] = $item;
3790
  }
3791
  }
@@ -3795,14 +4681,32 @@ class Compiler
3795
  $item = $val[2][$i];
3796
 
3797
  if (! is_numeric($name)) {
3798
- $keywordArgs[$name] = $item;
 
 
 
 
 
 
 
 
 
 
 
 
3799
  } else {
 
 
 
3800
  $remaining[] = $item;
3801
  }
3802
  }
3803
  } else {
3804
  $remaining[] = $val;
3805
  }
 
 
 
3806
  } else {
3807
  $remaining[] = $arg[1];
3808
  }
@@ -3812,7 +4716,7 @@ class Compiler
3812
  list($i, $name, $default, $isVariable) = $arg;
3813
 
3814
  if ($isVariable) {
3815
- $val = [Type::T_LIST, ',', [], $isVariable];
3816
 
3817
  for ($count = count($remaining); $i < $count; $i++) {
3818
  $val[2][] = $remaining[$i];
@@ -3832,10 +4736,16 @@ class Compiler
3832
  break;
3833
  }
3834
 
3835
- $this->set($name, $this->reduce($val, true), true, $env);
 
 
 
 
3836
  }
3837
 
3838
- $storeEnv->store = $env->store;
 
 
3839
 
3840
  foreach ($args as $arg) {
3841
  list($i, $name, $default, $isVariable) = $arg;
@@ -3844,8 +4754,14 @@ class Compiler
3844
  continue;
3845
  }
3846
 
3847
- $this->set($name, $this->reduce($default, true), true);
 
 
 
 
3848
  }
 
 
3849
  }
3850
 
3851
  /**
@@ -3853,9 +4769,9 @@ class Compiler
3853
  *
3854
  * @param mixed $value
3855
  *
3856
- * @return array|\Leafo\ScssPhp\Node\Number
3857
  */
3858
- private function coerceValue($value)
3859
  {
3860
  if (is_array($value) || $value instanceof \ArrayAccess) {
3861
  return $value;
@@ -3946,10 +4862,20 @@ class Compiler
3946
  $key = $keys[$i];
3947
  $value = $values[$i];
3948
 
 
 
 
 
 
 
 
 
 
 
3949
  $list[] = [
3950
  Type::T_LIST,
3951
  '',
3952
- [[Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))], $value]
3953
  ];
3954
  }
3955
 
@@ -4057,7 +4983,7 @@ class Compiler
4057
  $value = $this->coerceMap($value);
4058
 
4059
  if ($value[0] !== Type::T_MAP) {
4060
- $this->throwError('expecting map');
4061
  }
4062
 
4063
  return $value;
@@ -4077,7 +5003,7 @@ class Compiler
4077
  public function assertList($value)
4078
  {
4079
  if ($value[0] !== Type::T_LIST) {
4080
- $this->throwError('expecting list');
4081
  }
4082
 
4083
  return $value;
@@ -4100,7 +5026,7 @@ class Compiler
4100
  return $color;
4101
  }
4102
 
4103
- $this->throwError('expecting color');
4104
  }
4105
 
4106
  /**
@@ -4117,7 +5043,7 @@ class Compiler
4117
  public function assertNumber($value)
4118
  {
4119
  if ($value[0] !== Type::T_NUMBER) {
4120
- $this->throwError('expecting number');
4121
  }
4122
 
4123
  return $value[1];
@@ -4194,7 +5120,7 @@ class Compiler
4194
  *
4195
  * @return float
4196
  */
4197
- private function hueToRGB($m1, $m2, $h)
4198
  {
4199
  if ($h < 0) {
4200
  $h += 1;
@@ -4252,28 +5178,27 @@ class Compiler
4252
 
4253
  // Built in functions
4254
 
4255
- //protected static $libCall = ['name', 'args...'];
4256
  protected function libCall($args, $kwargs)
4257
  {
4258
  $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
 
4259
 
4260
- $args = array_map(
4261
- function ($a) {
4262
- return [null, $a, false];
4263
- },
4264
- $args
4265
- );
4266
-
4267
- if (count($kwargs)) {
4268
- foreach ($kwargs as $key => $value) {
4269
- $args[] = [[Type::T_VARIABLE, $key], $value, false];
4270
  }
 
 
4271
  }
4272
 
4273
- return $this->reduce([Type::T_FUNCTION_CALL, $name, $args]);
4274
  }
4275
 
4276
- protected static $libIf = ['condition', 'if-true', 'if-false'];
4277
  protected function libIf($args)
4278
  {
4279
  list($cond, $t, $f) = $args;
@@ -4326,12 +5251,12 @@ class Compiler
4326
  }
4327
 
4328
  protected static $libRgba = [
4329
- ['red', 'color'],
4330
- 'green', 'blue', 'alpha'];
4331
  protected function libRgba($args)
4332
  {
4333
  if ($color = $this->coerceColor($args[0])) {
4334
- $num = ! isset($args[1]) ? $args[3] : $args[1];
4335
  $alpha = $this->assertNumber($num);
4336
  $color[4] = $alpha;
4337
 
@@ -4356,11 +5281,11 @@ class Compiler
4356
  }
4357
  }
4358
 
4359
- if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
4360
  $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4361
 
4362
  foreach ([4, 5, 6] as $i) {
4363
- if (isset($args[$i])) {
4364
  $val = $this->assertNumber($args[$i]);
4365
  $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
4366
  }
@@ -4379,8 +5304,8 @@ class Compiler
4379
  }
4380
 
4381
  protected static $libAdjustColor = [
4382
- 'color', 'red', 'green', 'blue',
4383
- 'hue', 'saturation', 'lightness', 'alpha'
4384
  ];
4385
  protected function libAdjustColor($args)
4386
  {
@@ -4390,8 +5315,8 @@ class Compiler
4390
  }
4391
 
4392
  protected static $libChangeColor = [
4393
- 'color', 'red', 'green', 'blue',
4394
- 'hue', 'saturation', 'lightness', 'alpha'
4395
  ];
4396
  protected function libChangeColor($args)
4397
  {
@@ -4401,8 +5326,8 @@ class Compiler
4401
  }
4402
 
4403
  protected static $libScaleColor = [
4404
- 'color', 'red', 'green', 'blue',
4405
- 'hue', 'saturation', 'lightness', 'alpha'
4406
  ];
4407
  protected function libScaleColor($args)
4408
  {
@@ -4496,7 +5421,7 @@ class Compiler
4496
  }
4497
 
4498
  // mix two colors
4499
- protected static $libMix = ['color-1', 'color-2', 'weight'];
4500
  protected function libMix($args)
4501
  {
4502
  list($first, $second, $weight) = $args;
@@ -4618,7 +5543,7 @@ class Compiler
4618
  return $this->adjustHsl($color, 3, -$amount);
4619
  }
4620
 
4621
- protected static $libSaturate = ['color', 'amount'];
4622
  protected function libSaturate($args)
4623
  {
4624
  $value = $args[0];
@@ -4829,7 +5754,7 @@ class Compiler
4829
  if (null === $unit) {
4830
  $unit = $number[2];
4831
  $originalUnit = $item->unitStr();
4832
- } elseif ($unit !== $number[2]) {
4833
  $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
4834
  break;
4835
  }
@@ -4898,7 +5823,7 @@ class Compiler
4898
  if (! isset($list[2][$n])) {
4899
  $this->throwError('Invalid argument for "n"');
4900
 
4901
- return;
4902
  }
4903
 
4904
  $list[2][$n] = $args[2];
@@ -4910,11 +5835,14 @@ class Compiler
4910
  protected function libMapGet($args)
4911
  {
4912
  $map = $this->assertMap($args[0]);
4913
- $key = $this->compileStringContent($this->coerceString($args[1]));
4914
 
4915
- for ($i = count($map[1]) - 1; $i >= 0; $i--) {
4916
- if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
4917
- return $map[2][$i];
 
 
 
4918
  }
4919
  }
4920
 
@@ -4976,7 +5904,21 @@ class Compiler
4976
  $map1 = $this->assertMap($args[0]);
4977
  $map2 = $this->assertMap($args[1]);
4978
 
4979
- return [Type::T_MAP, array_merge($map1[1], $map2[1]), array_merge($map1[2], $map2[2])];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4980
  }
4981
 
4982
  protected static $libKeywords = ['args'];
@@ -5013,7 +5955,7 @@ class Compiler
5013
  }
5014
  }
5015
 
5016
- protected static $libJoin = ['list1', 'list2', 'separator'];
5017
  protected function libJoin($args)
5018
  {
5019
  list($list1, $list2, $sep) = $args;
@@ -5025,7 +5967,7 @@ class Compiler
5025
  return [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
5026
  }
5027
 
5028
- protected static $libAppend = ['list', 'val', 'separator'];
5029
  protected function libAppend($args)
5030
  {
5031
  list($list1, $value, $sep) = $args;
@@ -5122,7 +6064,7 @@ class Compiler
5122
  ) {
5123
  $this->throwError('Invalid argument(s) for "comparable"');
5124
 
5125
- return;
5126
  }
5127
 
5128
  $number1 = $number1->normalize();
@@ -5170,7 +6112,7 @@ class Compiler
5170
  return new Node\Number(strlen($stringContent), '');
5171
  }
5172
 
5173
- protected static $libStrSlice = ['string', 'start-at', 'end-at'];
5174
  protected function libStrSlice($args)
5175
  {
5176
  if (isset($args[2]) && $args[2][1] == 0) {
@@ -5302,7 +6244,7 @@ class Compiler
5302
  if ($n < 1) {
5303
  $this->throwError("limit must be greater than or equal to 1");
5304
 
5305
- return;
5306
  }
5307
 
5308
  return new Node\Number(mt_rand(1, $n), '');
@@ -5333,4 +6275,630 @@ class Compiler
5333
 
5334
  return $args[0];
5335
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5336
  }
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
+
14
+ use ScssPhp\ScssPhp\Base\Range;
15
+ use ScssPhp\ScssPhp\Block;
16
+ use ScssPhp\ScssPhp\Cache;
17
+ use ScssPhp\ScssPhp\Colors;
18
+ use ScssPhp\ScssPhp\Compiler\Environment;
19
+ use ScssPhp\ScssPhp\Exception\CompilerException;
20
+ use ScssPhp\ScssPhp\Formatter\OutputBlock;
21
+ use ScssPhp\ScssPhp\Node;
22
+ use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
23
+ use ScssPhp\ScssPhp\Type;
24
+ use ScssPhp\ScssPhp\Parser;
25
+ use ScssPhp\ScssPhp\Util;
26
 
27
  /**
28
  * The scss compiler and parser.
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 = [];
130
  protected $sourceMapOptions = [];
131
 
132
  /**
133
+ * @var string|\ScssPhp\ScssPhp\Formatter
134
  */
135
+ protected $formatter = 'ScssPhp\ScssPhp\Formatter\Nested';
136
 
137
  protected $rootEnv;
138
  protected $rootBlock;
139
 
140
  /**
141
+ * @var \ScssPhp\ScssPhp\Compiler\Environment
142
  */
143
  protected $env;
144
  protected $scope;
146
  protected $charsetSeen;
147
  protected $sourceNames;
148
 
149
+ protected $cache;
150
+
151
+ protected $indentLevel;
152
+ protected $extends;
153
+ protected $extendsMap;
154
+ protected $parsedFiles;
155
+ protected $parser;
156
+ protected $sourceIndex;
157
+ protected $sourceLine;
158
+ protected $sourceColumn;
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
  {
170
  $this->parsedFiles = [];
171
  $this->sourceNames = [];
172
+
173
+ if ($cacheOptions) {
174
+ $this->cache = new Cache($cacheOptions);
175
+ }
176
+ }
177
+
178
+ public function getCompileOptions()
179
+ {
180
+ $options = [
181
+ 'importPaths' => $this->importPaths,
182
+ 'registeredVars' => $this->registeredVars,
183
+ 'registeredFeatures' => $this->registeredFeatures,
184
+ 'encoding' => $this->encoding,
185
+ 'sourceMap' => serialize($this->sourceMap),
186
+ 'sourceMapOptions' => $this->sourceMapOptions,
187
+ 'formatter' => $this->formatter,
188
+ ];
189
+
190
+ return $options;
191
  }
192
 
193
  /**
202
  */
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
+ }
217
+ }
218
+
219
+ if (isset($cache)) {
220
+ return $cache['out'];
221
+ }
222
+ }
223
+ }
224
+
225
+
226
  $this->indentLevel = -1;
 
227
  $this->extends = [];
228
  $this->extendsMap = [];
229
  $this->sourceIndex = null;
237
  $this->stderr = fopen('php://stderr', 'w');
238
 
239
  $this->parser = $this->parserFactory($path);
240
+ $tree = $this->parser->parse($code);
241
  $this->parser = null;
242
 
243
  $this->formatter = new $this->formatter();
278
  $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
279
  }
280
 
281
+ if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
282
+ $v = [
283
+ 'dependencies' => $this->getParsedFiles(),
284
+ 'out' => &$out,
285
+ ];
286
+
287
+ $this->cache->setCache("compile", $cacheKey, $v, $compileOptions);
288
+ }
289
+
290
  return $out;
291
  }
292
 
295
  *
296
  * @param string $path
297
  *
298
+ * @return \ScssPhp\ScssPhp\Parser
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);
357
  * @param string $type
358
  * @param array $selectors
359
  *
360
+ * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
361
  */
362
  protected function makeOutputBlock($type, $selectors = null)
363
  {
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;
374
+ $out->sourceLine = $this->env->block->sourceLine;
375
+ $out->sourceColumn = $this->env->block->sourceColumn;
376
+ } else {
377
+ $out->sourceName = null;
378
+ $out->sourceLine = null;
379
+ $out->sourceColumn = null;
380
+ }
381
 
382
  return $out;
383
  }
385
  /**
386
  * Compile root
387
  *
388
+ * @param \ScssPhp\ScssPhp\Block $rootBlock
389
  */
390
  protected function compileRoot(Block $rootBlock)
391
  {
424
  /**
425
  * Flatten selectors
426
  *
427
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
428
+ * @param string $parentKey
429
  */
430
  protected function flattenSelectors(OutputBlock $block, $parentKey = null)
431
  {
480
  }
481
  }
482
 
483
+ /**
484
+ * Glue parts of :not( or :nth-child( ... that are in general splitted in selectors parts
485
+ *
486
+ * @param array $parts
487
+ *
488
+ * @return array
489
+ */
490
+ protected function glueFunctionSelectors($parts)
491
+ {
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
+ }
508
+ }
509
+ }
510
+
511
+ return $new;
512
+ }
513
+
514
  /**
515
  * Match extends
516
  *
521
  */
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;
535
  }
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
 
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
  }
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
+ }
575
+
576
  array_unshift($replacement, $slice);
577
 
578
  if (! $this->isImmediateRelationshipCombinator(end($slice))) {
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,
622
  $out[] = $result2;
623
  }
624
  }
625
+
626
+ array_pop($partsPile);
627
  }
628
  }
629
  }
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)) {
680
  foreach ($counts as $idx => $count) {
681
  list($target, $origin, /* $block */) = $this->extends[$idx];
682
 
683
+ $origin = $this->glueFunctionSelectors($origin);
684
+
685
  // check count
686
  if ($count !== count($target)) {
687
  continue;
722
  return $found;
723
  }
724
 
 
725
  /**
726
  * Extract a relationship from the fragment.
727
  *
731
  * the rest.
732
  *
733
  * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
734
+ *
735
  * @return array The selector without the relationship fragment if any, the relationship fragment.
736
  */
737
  protected function extractRelationshipFromFragment(array $fragment)
795
  /**
796
  * Compile media
797
  *
798
+ * @param \ScssPhp\ScssPhp\Block $media
799
  */
800
  protected function compileMedia(Block $media)
801
  {
802
  $this->pushEnv($media);
803
 
804
+ $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env));
 
 
 
805
 
806
+ if (! empty($mediaQueries) && $mediaQueries) {
807
+ $previousScope = $this->scope;
808
  $parentScope = $this->mediaParent($this->scope);
809
+
810
+ foreach ($mediaQueries as $mediaQuery) {
811
+ $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
812
+
813
+ $parentScope->children[] = $this->scope;
814
+ $parentScope = $this->scope;
815
+ }
816
 
817
  // top level properties in a media cause it to be wrapped
818
  $needsWrap = false;
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;
848
+
849
+ $file = $this->sourceNames[$media->sourceIndex];
850
+ $line = $media->sourceLine;
851
+
852
+ switch ($this->lineNumberStyle) {
853
+ case static::LINE_COMMENTS:
854
+ $annotation->lines[] = '/* line ' . $line
855
+ . ($file ? ', ' . $file : '')
856
+ . ' */';
857
+ break;
858
+
859
+ case static::DEBUG_INFO:
860
+ $annotation->lines[] = '@media -sass-debug-info{'
861
+ . ($file ? 'filename{font-family:"' . $file . '"}' : '')
862
+ . 'line{font-family:' . $line . '}}';
863
+ break;
864
+ }
865
+
866
+ $this->scope->children[] = $annotation;
867
+ }
868
  }
869
 
870
  $this->compileChildrenNoReturn($media->children, $this->scope);
871
 
872
+ $this->scope = $previousScope;
873
  }
874
 
875
  $this->popEnv();
878
  /**
879
  * Media parent
880
  *
881
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
882
  *
883
+ * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
884
  */
885
  protected function mediaParent(OutputBlock $scope)
886
  {
898
  /**
899
  * Compile directive
900
  *
901
+ * @param \ScssPhp\ScssPhp\Block $block
902
  */
903
  protected function compileDirective(Block $block)
904
  {
918
  /**
919
  * Compile at-root
920
  *
921
+ * @param \ScssPhp\ScssPhp\Block $block
922
  */
923
  protected function compileAtRoot(Block $block)
924
  {
925
  $env = $this->pushEnv($block);
926
  $envs = $this->compactEnv($env);
927
+ list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null);
928
 
929
  // wrap inline selector
930
  if ($block->selector) {
937
  $wrapped->comments = [];
938
  $wrapped->parent = $block;
939
  $wrapped->children = $block->children;
940
+ $wrapped->selfParent = $block->selfParent;
941
 
942
  $block->children = [[Type::T_BLOCK, $wrapped]];
943
+ $block->selector = null;
944
+ }
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;
952
  }
953
 
954
+ $this->env = $this->filterWithWithout($envs, $with, $without);
 
955
 
956
  $saveScope = $this->scope;
957
+ $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without);
958
 
959
+ // propagate selfParent to the children where they still can be useful
960
+ $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent);
961
 
962
+ $this->scope = $this->completeScope($this->scope, $saveScope);
963
  $this->scope = $saveScope;
964
  $this->env = $this->extractEnv($envs);
965
 
967
  }
968
 
969
  /**
970
+ * Filter at-root scope depending of with/without option
971
  *
972
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
973
+ * @param array $with
974
+ * @param array $without
975
  *
976
+ * @return mixed
977
  */
978
+ protected function filterScopeWithWithout($scope, $with, $without)
979
  {
980
+ $filteredScopes = [];
981
 
982
+ if ($scope->type === TYPE::T_ROOT) {
983
+ return $scope;
984
+ }
 
985
 
986
+ // start from the root
987
+ while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) {
988
+ $scope = $scope->parent;
989
+ }
990
+
991
+ for (;;) {
992
+ if (! $scope) {
993
+ break;
994
  }
995
 
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 = [];
1004
+ }
1005
+
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
 
1020
+ $newScope = array_shift($filteredScopes);
1021
+ $newScope->parent = $this->rootBlock;
1022
 
1023
+ $this->rootBlock->children[] = $newScope;
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;
1035
+ }
 
1036
 
1037
+ /**
1038
+ * found missing selector from a at-root compilation in the previous scope
1039
+ * (if at-root is just enclosing a property, the selector is in the parent tree)
1040
+ *
1041
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1042
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope
1043
+ *
1044
+ * @return mixed
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
 
1052
+ if ($scope->children) {
1053
+ foreach ($scope->children as $k => $c) {
1054
+ $scope->children[$k] = $this->completeScope($c, $previousScope);
1055
  }
1056
+ }
1057
 
1058
+ return $scope;
1059
+ }
 
1060
 
1061
+ /**
1062
+ * Find a selector by the depth node in the scope
1063
+ *
1064
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1065
+ * @param integer $depth
1066
+ *
1067
+ * @return array
1068
+ */
1069
+ protected function findScopeSelectors($scope, $depth)
1070
+ {
1071
+ if ($scope->depth === $depth && $scope->selectors) {
1072
+ return $scope->selectors;
1073
  }
1074
 
1075
+ if ($scope->children) {
1076
+ foreach (array_reverse($scope->children) as $c) {
1077
+ if ($s = $this->findScopeSelectors($c, $depth)) {
1078
+ return $s;
1079
+ }
1080
+ }
1081
+ }
1082
 
1083
+ return [];
1084
  }
1085
 
1086
  /**
1087
+ * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later
1088
  *
1089
+ * @param array $withCondition
1090
  *
1091
+ * @return array
1092
  */
1093
+ protected function compileWith($withCondition)
1094
  {
1095
+ // just compile what we have in 2 lists
1096
+ $with = [];
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]));
1103
 
1104
+ foreach ($list[2] as $item) {
1105
+ $keyword = $this->compileStringContent($this->coerceString($item));
 
 
1106
 
1107
+ $with[$keyword] = true;
 
1108
  }
1109
  }
 
 
 
 
1110
 
1111
+ if ($this->libMapHasKey([$withCondition, static::$without])) {
1112
+ $without = []; // cancel the default
1113
+ $list = $this->coerceList($this->libMapGet([$withCondition, static::$without]));
1114
 
1115
+ foreach ($list[2] as $item) {
1116
+ $keyword = $this->compileStringContent($this->coerceString($item));
1117
 
1118
+ $without[$keyword] = true;
 
1119
  }
1120
  }
1121
  }
1122
 
1123
+ return [$with, $without];
1124
  }
1125
 
1126
  /**
1127
  * Filter env stack
1128
  *
1129
  * @param array $envs
1130
+ * @param array $with
1131
+ * @param array $without
1132
  *
1133
+ * @return \ScssPhp\ScssPhp\Compiler\Environment
1134
  */
1135
+ protected function filterWithWithout($envs, $with, $without)
1136
  {
1137
  $filtered = [];
1138
 
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;
1147
  }
 
 
1148
  }
1149
 
1150
  return $this->extractEnv($filtered);
1153
  /**
1154
  * Filter WITH rules
1155
  *
1156
+ * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block
1157
+ * @param array $with
1158
+ * @param array $without
1159
  *
1160
  * @return boolean
1161
  */
1162
+ protected function isWith($block, $with, $without)
1163
  {
1164
+ if (isset($block->type)) {
1165
+ if ($block->type === Type::T_MEDIA) {
1166
+ return $this->testWithWithout('media', $with, $without);
1167
+ }
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
 
1185
+ return true;
1186
+ }
1187
+
1188
+ /**
1189
+ * Test a single type of block against with/without lists
1190
+ *
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
+
1204
+ // otherwise reject all what is not in the with list
1205
+ return (isset($with[$what]) || isset($with['all'])) ? true : false;
1206
  }
1207
 
1208
+
1209
  /**
1210
  * Compile keyframe block
1211
  *
1212
+ * @param \ScssPhp\ScssPhp\Block $block
1213
+ * @param array $selectors
1214
  */
1215
  protected function compileKeyframeBlock(Block $block, $selectors)
1216
  {
1234
  $this->popEnv();
1235
  }
1236
 
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
+ {
1245
+ $prefix = $this->compileValue($block->prefix) . '-';
1246
+
1247
+ $nested = $this->makeOutputBlock($block->type);
1248
+ $nested->parent = $out;
1249
+
1250
+ if ($block->hasValue) {
1251
+ $nested->depth = $out->depth + 1;
1252
+ }
1253
+
1254
+ $out->children[] = $nested;
1255
+
1256
+ foreach ($block->children as $child) {
1257
+ switch ($child[0]) {
1258
+ case Type::T_ASSIGN:
1259
+ array_unshift($child[1][2], $prefix);
1260
+ break;
1261
+
1262
+ case Type::T_NESTED_PROPERTY:
1263
+ array_unshift($child[1]->prefix[2], $prefix);
1264
+ break;
1265
+ }
1266
+ $this->compileChild($child, $nested);
1267
+ }
1268
+ }
1269
+
1270
  /**
1271
  * Compile nested block
1272
  *
1273
+ * @param \ScssPhp\ScssPhp\Block $block
1274
+ * @param array $selectors
1275
  */
1276
  protected function compileNestedBlock(Block $block, $selectors)
1277
  {
1280
  $this->scope = $this->makeOutputBlock($block->type, $selectors);
1281
  $this->scope->parent->children[] = $this->scope;
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
+
1289
+ foreach ($block->children as $child) {
1290
+ if ($child[0] === Type::T_ASSIGN) {
1291
+ $needWrapping = true;
1292
+ break;
1293
+ }
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
+ }
1310
+ }
1311
+
1312
  $this->compileChildrenNoReturn($block->children, $this->scope);
1313
 
1314
  $this->scope = $this->scope->parent;
1332
  *
1333
  * @see Compiler::compileChild()
1334
  *
1335
+ * @param \ScssPhp\ScssPhp\Block $block
1336
  */
1337
  protected function compileBlock(Block $block)
1338
  {
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
1374
+ $selfParentSelectors = null;
1375
+
1376
+ if (isset($block->selfParent->selectors)) {
1377
+ $selfParentSelectors = $block->selfParent->selectors;
1378
+ $block->selfParent->selectors = $out->selectors;
1379
+ }
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);
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
  }
1406
 
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
 
1474
  /**
1475
  * Collapse selectors
1476
  *
1477
+ * @param array $selectors
1478
+ * @param boolean $selectorFormat
1479
+ * if false return a collapsed string
1480
+ * if true return an array description of a structured selector
1481
  *
1482
  * @return string
1483
  */
1484
+ protected function collapseSelectors($selectors, $selectorFormat = false)
1485
  {
1486
  $parts = [];
1487
 
1488
  foreach ($selectors as $selector) {
1489
+ $output = [];
1490
+ $glueNext = false;
1491
 
1492
+ foreach ($selector as $node) {
1493
+ $compound = '';
1494
+
1495
+ array_walk_recursive(
1496
+ $node,
1497
+ function ($value, $key) use (&$compound) {
1498
+ $compound .= $value;
1499
+ }
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;
1514
  }
1515
+ }
1516
+
1517
+ if ($selectorFormat) {
1518
+ foreach ($output as &$o) {
1519
+ $o = [Type::T_STRING, '', [$o]];
1520
+ }
1521
+ $output = [Type::T_LIST, ' ', $output];
1522
+ } else {
1523
+ $output = implode(' ', $output);
1524
+ }
1525
 
1526
  $parts[] = $output;
1527
  }
1528
 
1529
+ if ($selectorFormat) {
1530
+ $parts = [Type::T_LIST, ',', $parts];
1531
+ } else {
1532
+ $parts = implode(', ', $parts);
1533
+ }
1534
+
1535
+ return $parts;
1536
+ }
1537
+
1538
+ /**
1539
+ * Parse down the selector and revert [self] to "&" before a reparsing
1540
+ *
1541
+ * @param array $selectors
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
+ }
1556
+
1557
+ return $selectors;
1558
  }
1559
 
1560
  /**
1661
  return false;
1662
  }
1663
 
1664
+ protected function pushCallStack($name = '')
1665
+ {
1666
+ $this->callStack[] = [
1667
+ 'n' => $name,
1668
+ Parser::SOURCE_INDEX => $this->sourceIndex,
1669
+ Parser::SOURCE_LINE => $this->sourceLine,
1670
+ Parser::SOURCE_COLUMN => $this->sourceColumn
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
+
1682
+ protected function popCallStack()
1683
+ {
1684
+ array_pop($this->callStack);
1685
+ }
1686
+
1687
  /**
1688
  * Compile children and return result
1689
  *
1690
+ * @param array $stms
1691
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1692
+ * @param string $traceName
1693
  *
1694
+ * @return array|null
1695
  */
1696
+ protected function compileChildren($stms, OutputBlock $out, $traceName = '')
1697
  {
1698
+ $this->pushCallStack($traceName);
1699
+
1700
  foreach ($stms as $stm) {
1701
  $ret = $this->compileChild($stm, $out);
1702
 
1704
  return $ret;
1705
  }
1706
  }
1707
+
1708
+ $this->popCallStack();
1709
+
1710
+ return null;
1711
  }
1712
 
1713
  /**
1714
  * Compile children and throw exception if unexpected @return
1715
  *
1716
+ * @param array $stms
1717
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1718
+ * @param \ScssPhp\ScssPhp\Block $selfParent
1719
+ * @param string $traceName
1720
  *
1721
  * @throws \Exception
1722
  */
1723
+ protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '')
1724
  {
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']);
1736
+ } else {
1737
+ $ret = $this->compileChild($stm, $out);
1738
+ }
1739
 
1740
  if (isset($ret)) {
1741
  $this->throwError('@return may only be used within a function');
1743
  return;
1744
  }
1745
  }
1746
+
1747
+ $this->popCallStack();
1748
  }
1749
 
1750
+
1751
  /**
1752
+ * evaluate media query : compile internal value keeping the structure inchanged
1753
  *
1754
  * @param array $queryList
1755
  *
1756
+ * @return array
1757
  */
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 ||
1774
+ strpos($value, ',') !== false)
1775
+ ) {
1776
+ $shouldReparse = true;
1777
+ }
1778
 
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
+
1803
+ return $outQueryList;
1804
+ }
1805
+
1806
+ /**
1807
+ * Compile media query
1808
+ *
1809
+ * @param array $queryList
1810
+ *
1811
+ * @return array
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;
1822
+ $parts = [];
1823
+
1824
+ $mediaTypeOnly = true;
1825
+
1826
+ foreach ($query as $q) {
1827
+ if ($q[0] !== Type::T_MEDIA_TYPE) {
1828
+ $mediaTypeOnly = false;
1829
+ break;
1830
+ }
1831
+ }
1832
+
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
+
1849
+ $current .= implode(' and ', $parts);
1850
+ }
1851
+
1852
+ if ($current) {
1853
+ $out[] = $start . $current;
1854
+ }
1855
+
1856
+ $current = "";
1857
+ $type = null;
1858
+ $parts = [];
1859
+ }
1860
+ }
1861
+
1862
+ if ($newType === ['all'] && $default) {
1863
+ $default = $start . 'all';
1864
+ }
1865
+
1866
+ // all can be safely ignored and mixed with whatever else
1867
+ if ($newType !== ['all']) {
1868
+ if ($type) {
1869
+ $type = $this->mergeMediaTypes($type, $newType);
1870
+
1871
+ if (empty($type)) {
1872
+ // merge failed : ignore this query that is not valid, skip to the next one
1873
+ $parts = [];
1874
+ $default = ''; // if everything fail, no @media at all
1875
+ continue 3;
1876
+ }
1877
+ } else {
1878
+ $type = $newType;
1879
  }
 
 
1880
  }
1881
  break;
1882
 
1905
  }
1906
 
1907
  if (! empty($parts)) {
1908
+ if (strlen($current)) {
1909
+ $current .= $this->formatter->tagSeparator;
 
 
 
1910
  }
1911
 
1912
+ $current .= implode(' and ', $parts);
1913
  }
1914
  }
1915
 
1916
+ if ($current) {
1917
+ $out[] = $start . $current;
1918
+ }
1919
+
1920
+ // no @media type except all, and no conflict?
1921
+ if (! $out && $default) {
1922
+ $out[] = $default;
1923
+ }
1924
+
1925
  return $out;
1926
  }
1927
 
1928
+ /**
1929
+ * Merge direct relationships between selectors
1930
+ *
1931
+ * @param array $selectors1
1932
+ * @param array $selectors2
1933
+ *
1934
+ * @return array
1935
+ */
1936
  protected function mergeDirectRelationships($selectors1, $selectors2)
1937
  {
1938
  if (empty($selectors1) || empty($selectors2)) {
1942
  $part1 = end($selectors1);
1943
  $part2 = end($selectors2);
1944
 
1945
+ if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
1946
  return array_merge($selectors1, $selectors2);
1947
  }
1948
 
1952
  $part1 = array_pop($selectors1);
1953
  $part2 = array_pop($selectors2);
1954
 
1955
+ if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
1956
+ if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) {
1957
+ array_unshift($merged, [$part1[0] . $part2[0]]);
1958
+ $merged = array_merge($selectors1, $selectors2, $merged);
1959
+ } else {
1960
+ $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
1961
+ }
1962
+
1963
  break;
1964
  }
1965
 
1966
  array_unshift($merged, $part1);
 
1967
  } while (! empty($selectors1) && ! empty($selectors2));
1968
 
1969
  return $merged;
2038
  /**
2039
  * Compile import; returns true if the value was something that could be imported
2040
  *
2041
+ * @param array $rawPath
2042
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2043
+ * @param boolean $once
2044
  *
2045
  * @return boolean
2046
  */
2047
+ protected function compileImport($rawPath, OutputBlock $out, $once = false)
2048
  {
2049
  if ($rawPath[0] === Type::T_STRING) {
2050
  $path = $this->compileStringContent($rawPath);
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
2089
+ * (keeping before comments, @import and @charset coming before in the source code)
2090
+ *
2091
+ * @param string $line
2092
+ * @param @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2093
+ * @param array $allowed
2094
+ */
2095
+ protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT])
2096
+ {
2097
+ $root = $out;
2098
+
2099
+ while ($root->parent) {
2100
+ $root = $root->parent;
2101
+ }
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
+
2110
+ $i++;
2111
+ }
2112
+
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;
2168
+ }
2169
+ }
2170
+
2171
+ $outWrite->lines[] = $line;
2172
+ }
2173
+
2174
  /**
2175
  * Compile child; returns a value to halt execution
2176
  *
2177
+ * @param array $child
2178
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2179
  *
2180
  * @return array
2181
  */
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;
2198
+ }
2199
+ }
2200
 
2201
  switch ($child[0]) {
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
 
2234
  case Type::T_CHARSET:
2235
  if (! $this->charsetSeen) {
2236
  $this->charsetSeen = true;
2237
+ $this->appendRootDirective('@charset ' . $this->compileValue($child[1]) . ';', $out);
 
2238
  }
2239
  break;
2240
 
2247
  $isGlobal = in_array('!global', $flags);
2248
 
2249
  if ($isGlobal) {
2250
+ $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
2251
  break;
2252
  }
2253
 
2254
  $shouldSet = $isDefault &&
2255
+ (($result = $this->get($name[1], false)) === null ||
2256
+ $result === static::$null);
2257
 
2258
  if (! $isDefault || $shouldSet) {
2259
+ $this->set($name[1], $this->reduce($value), true, null, $value);
2260
  }
2261
  break;
2262
  }
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
  }
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
  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) {
2460
  return $this->reduce($child[1], true);
2461
 
2462
  case Type::T_NESTED_PROPERTY:
2463
+ $this->compileNestedPropertiesBlock($child[1], $out);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2464
  break;
2465
 
2466
  case Type::T_INCLUDE:
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;
2489
+
2490
+ if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) {
2491
+ $selfParent = $child['selfParent'];
2492
+ } else {
2493
+ $parentSelectors = $this->multiplySelectors($this->env);
2494
+
2495
+ if ($parentSelectors) {
2496
+ $parent = new Block();
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
+ }
2504
+ }
2505
+ }
2506
+
2507
+ // clone the stored content to not have its scope spoiled by a further call to the same mixin
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)) {
2521
 
2522
  $this->env->marker = 'mixin';
2523
 
2524
+ $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . " " . $name);
2525
 
2526
  $this->storeEnv = $storeEnv;
2527
 
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;
2549
  case Type::T_DEBUG:
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:
2615
  *
2616
  * @param array $value
2617
  *
2618
+ * @return boolean
2619
  */
2620
  protected function isTruthy($value)
2621
  {
2646
  switch ($value[0]) {
2647
  case Type::T_EXPRESSION:
2648
  if ($value[1] === '/') {
2649
+ return $this->shouldEval($value[2]) || $this->shouldEval($value[3]);
2650
  }
2651
 
2652
  // fall-thru
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:
2674
  list(, $op, $left, $right, $inParens) = $value;
2675
 
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
  }
2802
  return [Type::T_STRING, '', [$op, $exp]];
2803
 
2804
  case Type::T_VARIABLE:
2805
+ return $this->reduce($this->get($value[1]));
 
 
2806
 
2807
  case Type::T_LIST:
2808
  foreach ($value[2] as &$item) {
2833
 
2834
  case Type::T_INTERPOLATE:
2835
  $value[1] = $this->reduce($value[1]);
2836
+ if ($inExp) {
2837
+ return $value[1];
2838
+ }
2839
 
2840
  return $value;
2841
 
2842
  case Type::T_FUNCTION_CALL:
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:
2851
  return $value;
2860
  *
2861
  * @return array|null
2862
  */
2863
+ protected function fncall($name, $argValues)
2864
  {
2865
  // SCSS @function
2866
  if ($this->callScssFunction($name, $argValues, $returnValue)) {
2906
  public function normalizeValue($value)
2907
  {
2908
  $value = $this->coerceForExpression($this->reduce($value));
 
2909
 
2910
+ switch ($value[0]) {
2911
  case Type::T_LIST:
2912
  $value = $this->extractInterpolation($value);
2913
 
2922
  return $value;
2923
 
2924
  case Type::T_STRING:
2925
+ return [$value[0], '"', [$this->compileStringContent($value)]];
2926
 
2927
  case Type::T_NUMBER:
2928
  return $value->normalize();
2941
  * @param array $left
2942
  * @param array $right
2943
  *
2944
+ * @return \ScssPhp\ScssPhp\Node\Number
2945
  */
2946
  protected function opAddNumberNumber($left, $right)
2947
  {
2954
  * @param array $left
2955
  * @param array $right
2956
  *
2957
+ * @return \ScssPhp\ScssPhp\Node\Number
2958
  */
2959
  protected function opMulNumberNumber($left, $right)
2960
  {
2967
  * @param array $left
2968
  * @param array $right
2969
  *
2970
+ * @return \ScssPhp\ScssPhp\Node\Number
2971
  */
2972
  protected function opSubNumberNumber($left, $right)
2973
  {
2980
  * @param array $left
2981
  * @param array $right
2982
  *
2983
+ * @return array|\ScssPhp\ScssPhp\Node\Number
2984
  */
2985
  protected function opDivNumberNumber($left, $right)
2986
  {
2997
  * @param array $left
2998
  * @param array $right
2999
  *
3000
+ * @return \ScssPhp\ScssPhp\Node\Number
3001
  */
3002
  protected function opModNumberNumber($left, $right)
3003
  {
3010
  * @param array $left
3011
  * @param array $right
3012
  *
3013
+ * @return array|null
3014
  */
3015
  protected function opAdd($left, $right)
3016
  {
3033
 
3034
  return $strRight;
3035
  }
3036
+
3037
+ return null;
3038
  }
3039
 
3040
  /**
3044
  * @param array $right
3045
  * @param boolean $shouldEval
3046
  *
3047
+ * @return array|null
3048
  */
3049
  protected function opAnd($left, $right, $shouldEval)
3050
  {
3051
+ $truthy = ($left === static::$null || $right === static::$null) ||
3052
+ ($left === static::$false || $left === static::$true) &&
3053
+ ($right === static::$false || $right === static::$true);
3054
+
3055
  if (! $shouldEval) {
3056
+ if (! $truthy) {
3057
+ return null;
3058
+ }
3059
  }
3060
 
3061
+ if ($left !== static::$false && $left !== static::$null) {
3062
  return $this->reduce($right, true);
3063
  }
3064
 
3072
  * @param array $right
3073
  * @param boolean $shouldEval
3074
  *
3075
+ * @return array|null
3076
  */
3077
  protected function opOr($left, $right, $shouldEval)
3078
  {
3079
+ $truthy = ($left === static::$null || $right === static::$null) ||
3080
+ ($left === static::$false || $left === static::$true) &&
3081
+ ($right === static::$false || $right === static::$true);
3082
+
3083
  if (! $shouldEval) {
3084
+ if (! $truthy) {
3085
+ return null;
3086
+ }
3087
  }
3088
 
3089
+ if ($left !== static::$false && $left !== static::$null) {
3090
  return $left;
3091
  }
3092
 
3297
  * @param array $left
3298
  * @param array $right
3299
  *
3300
+ * @return \ScssPhp\ScssPhp\Node\Number
3301
  */
3302
  protected function opCmpNumberNumber($left, $right)
3303
  {
3341
  {
3342
  $value = $this->reduce($value);
3343
 
3344
+ switch ($value[0]) {
 
 
3345
  case Type::T_KEYWORD:
3346
  return $value[1];
3347
 
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);
3436
  return $left . $this->compileValue($interpolate) . $right;
3437
 
3438
  case Type::T_INTERPOLATE:
 
 
 
3439
  // strip quotes if it's a string
3440
+ $reduced = $this->reduce($value[1]);
3441
 
3442
  switch ($reduced[0]) {
3443
  case Type::T_LIST:
3487
  return 'null';
3488
 
3489
  default:
3490
+ $this->throwError("unknown value type: ".json_encode($value));
3491
  }
3492
  }
3493
 
3551
  /**
3552
  * Find the final set of selectors
3553
  *
3554
+ * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3555
+ * @param \ScssPhp\ScssPhp\Block $selfParent
3556
  *
3557
  * @return array
3558
  */
3559
+ protected function multiplySelectors(Environment $env, $selfParent = null)
3560
  {
3561
  $envs = $this->compactEnv($env);
3562
  $selectors = [];
3563
  $parentSelectors = [[]];
3564
 
3565
+ $selfParentSelectors = null;
3566
+
3567
+ if (! is_null($selfParent) && $selfParent->selectors) {
3568
+ $selfParentSelectors = $this->evalSelectors($selfParent->selectors);
3569
+ }
3570
+
3571
  while ($env = array_pop($envs)) {
3572
  if (empty($env->selectors)) {
3573
  continue;
3574
  }
3575
 
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
3588
+ $s = $this->joinSelectors($parent, $selector, $stillHasSelf, $selfParent);
3589
+ $selectors[serialize($s)] = $s;
3590
+ }
3591
+ } else {
3592
+ $s = $this->joinSelectors($parent, $selector, $stillHasSelf);
3593
+ $selectors[serialize($s)] = $s;
3594
+ }
3595
+ }
3596
  }
3597
+ } while ($stillHasSelf);
3598
 
3599
  $parentSelectors = $selectors;
3600
  }
3601
 
3602
+ $selectors = array_values($selectors);
3603
+
3604
  return $selectors;
3605
  }
3606
 
3607
  /**
3608
  * Join selectors; looks for & to replace, or append parent before child
3609
  *
3610
+ * @param array $parent
3611
+ * @param array $child
3612
+ * @param boolean &$stillHasSelf
3613
+ * @param array $selfParentSelectors
3614
+
3615
  * @return array
3616
  */
3617
+ protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null)
3618
  {
3619
  $setSelf = false;
3620
  $out = [];
3623
  $newPart = [];
3624
 
3625
  foreach ($part as $p) {
3626
+ // only replace & once and should be recalled to be able to make combinations
3627
+ if ($p === static::$selfSelector && $setSelf) {
3628
+ $stillHasSelf = true;
3629
+ }
3630
+
3631
+ if ($p === static::$selfSelector && ! $setSelf) {
3632
  $setSelf = true;
3633
 
3634
+ if (is_null($selfParentSelectors)) {
3635
+ $selfParentSelectors = $parent;
3636
+ }
3637
+
3638
+ foreach ($selfParentSelectors as $i => $parentPart) {
3639
  if ($i > 0) {
3640
  $out[] = $newPart;
3641
  $newPart = [];
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
+
3653
  $newPart[] = $pp;
3654
  }
3655
  }
3667
  /**
3668
  * Multiply media
3669
  *
3670
+ * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3671
+ * @param array $childQueries
3672
  *
3673
  * @return array
3674
  */
3689
  ? $env->block->queryList
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 {
3703
 
3704
  foreach ($parentQueries as $parentQuery) {
3705
  foreach ($originalQueries as $childQuery) {
3706
+ $childQueries[] = array_merge(
3707
+ $parentQuery,
3708
+ [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]],
3709
+ $childQuery
3710
+ );
3711
  }
3712
  }
3713
  }
3718
  /**
3719
  * Convert env linked list to stack
3720
  *
3721
+ * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3722
  *
3723
  * @return array
3724
  */
3725
+ protected function compactEnv(Environment $env)
3726
  {
3727
  for ($envs = []; $env; $env = $env->parent) {
3728
  $envs[] = $env;
3736
  *
3737
  * @param array $envs
3738
  *
3739
+ * @return \ScssPhp\ScssPhp\Compiler\Environment
3740
  */
3741
+ protected function extractEnv($envs)
3742
  {
3743
  for ($env = null; $e = array_pop($envs);) {
3744
  $e->parent = $env;
3751
  /**
3752
  * Push environment
3753
  *
3754
+ * @param \ScssPhp\ScssPhp\Block $block
3755
  *
3756
+ * @return \ScssPhp\ScssPhp\Compiler\Environment
3757
  */
3758
  protected function pushEnv(Block $block = null)
3759
  {
3779
  /**
3780
  * Get store environment
3781
  *
3782
+ * @return \ScssPhp\ScssPhp\Compiler\Environment
3783
  */
3784
  protected function getStoreEnv()
3785
  {
3789
  /**
3790
  * Set variable
3791
  *
3792
+ * @param string $name
3793
+ * @param mixed $value
3794
+ * @param boolean $shadow
3795
+ * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3796
+ * @param mixed $valueUnreduced
3797
  */
3798
+ protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null)
3799
  {
3800
  $name = $this->normalizeName($name);
3801
 
3804
  }
3805
 
3806
  if ($shadow) {
3807
+ $this->setRaw($name, $value, $env, $valueUnreduced);
3808
  } else {
3809
+ $this->setExisting($name, $value, $env, $valueUnreduced);
3810
  }
3811
  }
3812
 
3813
  /**
3814
  * Set existing variable
3815
  *
3816
+ * @param string $name
3817
+ * @param mixed $value
3818
+ * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3819
+ * @param mixed $valueUnreduced
3820
  */
3821
+ protected function setExisting($name, $value, Environment $env, $valueUnreduced = null)
3822
  {
3823
  $storeEnv = $env;
3824
 
3843
  }
3844
 
3845
  $env->store[$name] = $value;
3846
+
3847
+ if ($valueUnreduced) {
3848
+ $env->storeUnreduced[$name] = $valueUnreduced;
3849
+ }
3850
  }
3851
 
3852
  /**
3853
  * Set raw variable
3854
  *
3855
+ * @param string $name
3856
+ * @param mixed $value
3857
+ * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3858
+ * @param mixed $valueUnreduced
3859
  */
3860
+ protected function setRaw($name, $value, Environment $env, $valueUnreduced = null)
3861
  {
3862
  $env->store[$name] = $value;
3863
+
3864
+ if ($valueUnreduced) {
3865
+ $env->storeUnreduced[$name] = $valueUnreduced;
3866
+ }
3867
  }
3868
 
3869
  /**
3871
  *
3872
  * @api
3873
  *
3874
+ * @param string $name
3875
+ * @param boolean $shouldThrow
3876
+ * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3877
+ * @param boolean $unreduced
3878
  *
3879
+ * @return mixed|null
3880
  */
3881
+ public function get($name, $shouldThrow = true, Environment $env = null, $unreduced = false)
3882
  {
3883
  $normalizedName = $this->normalizeName($name);
3884
  $specialContentKey = static::$namespaces['special'] . 'content';
3890
  $nextIsRoot = false;
3891
  $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
3892
 
3893
+ $maxDepth = 10000;
3894
+
3895
  for (;;) {
3896
+ if ($maxDepth-- <= 0) {
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
+ }
3904
+
3905
  return $env->store[$normalizedName];
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
 
3923
  }
3924
 
3925
  if ($shouldThrow) {
3926
+ $this->throwError("Undefined variable \$$name" . ($maxDepth<=0 ? " (infinite recursion)" : ""));
3927
  }
3928
 
3929
  // found nothing
3930
+ return null;
3931
  }
3932
 
3933
  /**
3934
  * Has variable?
3935
  *
3936
+ * @param string $name
3937
+ * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3938
  *
3939
  * @return boolean
3940
  */
4036
  *
4037
  * @api
4038
  *
4039
+ * @param string|callable $path
4040
  */
4041
  public function addImportPath($path)
4042
  {
4158
  /**
4159
  * Import file
4160
  *
4161
+ * @param string $path
4162
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
4163
  */
4164
+ protected function importFile($path, OutputBlock $out)
4165
  {
4166
  // see if tree is cached
4167
  $realPath = realpath($path);
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) !== '/' &&
4215
+ substr($full, 0, 1) !== '/'
4216
+ ) ? '/' : '';
4217
+ $full = $dir . $separator . $full;
4218
 
4219
  if ($this->fileExists($file = $full . '.scss') ||
4220
  ($hasExtension && $this->fileExists($file = $full))
4254
  *
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
  /**
4270
  *
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
  {
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
+ /**
4304
+ * Beautify call stack for output
4305
+ *
4306
+ * @param boolean $all
4307
+ * @param null $limit
4308
+ *
4309
+ * @return string
4310
+ */
4311
+ protected function callStackMessage($all = false, $limit = null)
4312
+ {
4313
+ $callStackMsg = [];
4314
+ $ncall = 0;
4315
+
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
+ }
4330
+ }
4331
+ }
4332
+
4333
+ return implode("\n", $callStackMsg);
4334
+ }
4335
+
4336
  /**
4337
  * Handle import loop
4338
  *
4398
 
4399
  $this->env->marker = 'function';
4400
 
4401
+ $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . " " . $name);
4402
 
4403
  $this->storeEnv = $storeEnv;
4404
 
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) {
4480
  *
4481
  * @return array
4482
  */
4483
+ protected function sortArgs($prototypes, $args)
4484
  {
4485
+ static $parser = null;
 
4486
 
4487
+ if (! isset($prototypes)) {
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
 
4513
+ $keyArgs = [];
 
4514
 
4515
+ // trying each prototypes
4516
+ $prototypeHasMatch = false;
4517
+ $exceptionMessage = '';
 
 
 
 
 
 
 
 
4518
 
4519
+ foreach ($prototypes as $prototype) {
4520
+ $argDef = [];
4521
 
4522
+ foreach ($prototype as $i => $p) {
4523
+ $default = null;
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
 
4538
+ $parser->parseValue($p, $default);
4539
+ }
4540
+ }
4541
 
4542
+ $isVariable = false;
4543
+
4544
+ if (substr($name, -3) === '...') {
4545
+ $isVariable = true;
4546
+ $name = substr($name, 0, -3);
4547
+ }
4548
+
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) {
4557
+ $name = explode(':', $p)[0];
4558
+
4559
+ if (! isset($finalArgs[$i])) {
4560
+ $finalArgs[$i] = null;
4561
+ }
4562
+ }
4563
+
4564
+ // apply positional args
4565
+ foreach (array_values($vars) as $i => $val) {
4566
+ $finalArgs[$i] = $val;
4567
+ }
4568
+
4569
+ $keyArgs = array_merge($keyArgs, $vars);
4570
+ $prototypeHasMatch = true;
4571
+
4572
+ // overwrite positional args with keyword args
4573
+ foreach ($prototype as $i => $p) {
4574
+ $name = explode(':', $p)[0];
4575
+
4576
+ if (isset($keyArgs[$name])) {
4577
+ $finalArgs[$i] = $keyArgs[$name];
4578
+ }
4579
+
4580
+ // special null value as default: translate to real null here
4581
+ if ($finalArgs[$i] === [Type::T_KEYWORD, 'null']) {
4582
+ $finalArgs[$i] = null;
4583
+ }
4584
+ }
4585
+ // should we break if this prototype seems fulfilled?
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];
4596
+ }
4597
+
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
+
4617
+ $hasVariable = false;
4618
+ $args = [];
4619
+
4620
+ foreach ($argDef as $i => $arg) {
4621
+ list($name, $default, $isVariable) = $argDef[$i];
4622
+
4623
+ $args[$name] = [$i, $name, $default, $isVariable];
4624
+ $hasVariable |= $isVariable;
4625
+ }
4626
+
4627
+ $splatSeparator = null;
4628
+ $keywordArgs = [];
4629
+ $deferredKeywordArgs = [];
4630
+ $remaining = [];
4631
+ $hasKeywordArgument = false;
4632
+
4633
+ // assign the keyword args
4634
  foreach ((array) $argValues as $arg) {
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 {
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
+ }
4663
+ }
4664
+ }
4665
+
4666
+ if ($hasVariable) {
4667
+ $deferredKeywordArgs[$name] = $item;
4668
+ } else {
4669
+ $keywordArgs[$name] = $item;
4670
+ }
4671
  } else {
4672
+ if (is_null($splatSeparator)) {
4673
+ $splatSeparator = $val[1];
4674
+ }
4675
  $remaining[] = $item;
4676
  }
4677
  }
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
  }
4704
  } else {
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
  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];
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
 
4746
+ if ($storeInEnv) {
4747
+ $storeEnv->store = $env->store;
4748
+ }
4749
 
4750
  foreach ($args as $arg) {
4751
  list($i, $name, $default, $isVariable) = $arg;
4754
  continue;
4755
  }
4756
 
4757
+ if ($storeInEnv) {
4758
+ $this->set($name, $this->reduce($default, true), true);
4759
+ } else {
4760
+ $output[$name] = $default;
4761
+ }
4762
  }
4763
+
4764
+ return $output;
4765
  }
4766
 
4767
  /**
4769
  *
4770
  * @param mixed $value
4771
  *
4772
+ * @return array|\ScssPhp\ScssPhp\Node\Number
4773
  */
4774
+ protected function coerceValue($value)
4775
  {
4776
  if (is_array($value) || $value instanceof \ArrayAccess) {
4777
  return $value;
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:
4871
+ $key = [Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))];
4872
+ break;
4873
+ }
4874
+
4875
  $list[] = [
4876
  Type::T_LIST,
4877
  '',
4878
+ [$key, $value]
4879
  ];
4880
  }
4881
 
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
  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
  return $color;
5027
  }
5028
 
5029
+ $this->throwError('expecting color, %s received', $value[0]);
5030
  }
5031
 
5032
  /**
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];
5120
  *
5121
  * @return float
5122
  */
5123
+ protected function hueToRGB($m1, $m2, $h)
5124
  {
5125
  if ($h < 0) {
5126
  $h += 1;
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, ',', [..]]
5188
+ foreach ($kwargs['args'][2] as $varname => $arg) {
5189
+ if (is_numeric($varname)) {
5190
+ $varname = null;
5191
+ } else {
5192
+ $varname = [ 'var', $varname];
 
 
 
 
5193
  }
5194
+
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:'];
5202
  protected function libIf($args)
5203
  {
5204
  list($cond, $t, $f) = $args;
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
 
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
  }
5304
  }
5305
 
5306
  protected static $libAdjustColor = [
5307
+ 'color', 'red:null', 'green:null', 'blue:null',
5308
+ 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
5309
  ];
5310
  protected function libAdjustColor($args)
5311
  {
5315
  }
5316
 
5317
  protected static $libChangeColor = [
5318
+ 'color', 'red:null', 'green:null', 'blue:null',
5319
+ 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
5320
  ];
5321
  protected function libChangeColor($args)
5322
  {
5326
  }
5327
 
5328
  protected static $libScaleColor = [
5329
+ 'color', 'red:null', 'green:null', 'blue:null',
5330
+ 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
5331
  ];
5332
  protected function libScaleColor($args)
5333
  {
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;
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];
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
  }
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];
5835
  protected function libMapGet($args)
5836
  {
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
+ }
5846
  }
5847
  }
5848
 
5904
  $map1 = $this->assertMap($args[0]);
5905
  $map2 = $this->assertMap($args[1]);
5906
 
5907
+ foreach ($map2[1] as $i2 => $key2) {
5908
+ $key = $this->compileStringContent($this->coerceString($key2));
5909
+
5910
+ foreach ($map1[1] as $i1 => $key1) {
5911
+ if ($key === $this->compileStringContent($this->coerceString($key1))) {
5912
+ $map1[2][$i1] = $map2[2][$i2];
5913
+ continue 2;
5914
+ }
5915
+ }
5916
+
5917
+ $map1[1][] = $map2[1][$i2];
5918
+ $map1[2][] = $map2[2][$i2];
5919
+ }
5920
+
5921
+ return $map1;
5922
  }
5923
 
5924
  protected static $libKeywords = ['args'];
5955
  }
5956
  }
5957
 
5958
+ protected static $libJoin = ['list1', 'list2', 'separator:null'];
5959
  protected function libJoin($args)
5960
  {
5961
  list($list1, $list2, $sep) = $args;
5967
  return [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
5968
  }
5969
 
5970
+ protected static $libAppend = ['list', 'val', 'separator:null'];
5971
  protected function libAppend($args)
5972
  {
5973
  list($list1, $value, $sep) = $args;
6064
  ) {
6065
  $this->throwError('Invalid argument(s) for "comparable"');
6066
 
6067
+ return null;
6068
  }
6069
 
6070
  $number1 = $number1->normalize();
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) {
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), '');
6275
 
6276
  return $args[0];
6277
  }
6278
+
6279
+ /**
6280
+ * Preprocess selector args
6281
+ *
6282
+ * @param array $arg
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
+
6297
+ $parsedSelector = [];
6298
+
6299
+ if ($parser->parseSelector($arg, $parsedSelector)) {
6300
+ $selector = $this->evalSelectors($parsedSelector);
6301
+ $gluedSelector = $this->glueFunctionSelectors($selector);
6302
+
6303
+ return $gluedSelector;
6304
+ }
6305
+
6306
+ return false;
6307
+ }
6308
+
6309
+ /**
6310
+ * Postprocess selector to output in right format
6311
+ *
6312
+ * @param array $selectors
6313
+ *
6314
+ * @return string
6315
+ */
6316
+ protected function formatOutputSelector($selectors)
6317
+ {
6318
+ $selectors = $this->collapseSelectors($selectors, true);
6319
+
6320
+ return $selectors;
6321
+ }
6322
+
6323
+ protected static $libIsSuperselector = ['super', 'sub'];
6324
+ protected function libIsSuperselector($args)
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
+ }
6333
+
6334
+ /**
6335
+ * Test a $super selector again $sub
6336
+ *
6337
+ * @param array $super
6338
+ * @param array $sub
6339
+ *
6340
+ * @return boolean
6341
+ */
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);
6354
+ $sub = reset($sub);
6355
+
6356
+ $i = 0;
6357
+ $nextMustMatch = false;
6358
+
6359
+ foreach ($super as $node) {
6360
+ $compound = '';
6361
+
6362
+ array_walk_recursive(
6363
+ $node,
6364
+ function ($value, $key) use (&$compound) {
6365
+ $compound .= $value;
6366
+ }
6367
+ );
6368
+
6369
+ if ($this->isImmediateRelationshipCombinator($compound)) {
6370
+ if ($node !== $sub[$i]) {
6371
+ return false;
6372
+ }
6373
+
6374
+ $nextMustMatch = true;
6375
+ $i++;
6376
+ } else {
6377
+ while ($i < count($sub) && ! $this->isSuperPart($node, $sub[$i])) {
6378
+ if ($nextMustMatch) {
6379
+ return false;
6380
+ }
6381
+
6382
+ $i++;
6383
+ }
6384
+
6385
+ if ($i >= count($sub)) {
6386
+ return false;
6387
+ }
6388
+
6389
+ $nextMustMatch = false;
6390
+ $i++;
6391
+ }
6392
+ }
6393
+
6394
+ return true;
6395
+ }
6396
+
6397
+ /**
6398
+ * Test a part of super selector again a part of sub selector
6399
+ *
6400
+ * @param array $superParts
6401
+ * @param array $subParts
6402
+ *
6403
+ * @return boolean
6404
+ */
6405
+ protected function isSuperPart($superParts, $subParts)
6406
+ {
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
+
6418
+ $i++;
6419
+ }
6420
+
6421
+ return true;
6422
+ }
6423
+
6424
+ protected static $libSelectorAppend = ['selector...'];
6425
+ protected function libSelectorAppend($args)
6426
+ {
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
+ }
6438
+
6439
+ /**
6440
+ * Append parts of the last selector in the list to the previous, recursively
6441
+ *
6442
+ * @param array $selectors
6443
+ *
6444
+ * @return array
6445
+ *
6446
+ * @throws \ScssPhp\ScssPhp\Exception\CompilerException
6447
+ */
6448
+ protected function selectorAppend($selectors)
6449
+ {
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
6464
+ $appended = [];
6465
+
6466
+ foreach ($lastSelectors as $lastSelector) {
6467
+ $previous = $previousSelectors;
6468
+
6469
+ foreach ($lastSelector as $lastSelectorParts) {
6470
+ foreach ($lastSelectorParts as $lastSelectorPart) {
6471
+ foreach ($previous as $i => $previousSelector) {
6472
+ foreach ($previousSelector as $j => $previousSelectorParts) {
6473
+ $previous[$i][$j][] = $lastSelectorPart;
6474
+ }
6475
+ }
6476
+ }
6477
+ }
6478
+
6479
+ foreach ($previous as $ps) {
6480
+ $appended[] = $ps;
6481
+ }
6482
+ }
6483
+
6484
+ $lastSelectors = $appended;
6485
+ }
6486
+
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);
6504
+
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);
6522
+
6523
+ return $this->formatOutputSelector($replaced);
6524
+ }
6525
+
6526
+ /**
6527
+ * Extend/replace in selectors
6528
+ * used by selector-extend and selector-replace that use the same logic
6529
+ *
6530
+ * @param array $selectors
6531
+ * @param array $extendee
6532
+ * @param array $extender
6533
+ * @param boolean $replace
6534
+ *
6535
+ * @return array
6536
+ */
6537
+ protected function extendOrReplaceSelectors($selectors, $extendee, $extender, $replace = false)
6538
+ {
6539
+ $saveExtends = $this->extends;
6540
+ $saveExtendsMap = $this->extendsMap;
6541
+
6542
+ $this->extends = [];
6543
+ $this->extendsMap = [];
6544
+
6545
+ foreach ($extendee as $es) {
6546
+ // only use the first one
6547
+ $this->pushExtends(reset($es), $extender, null);
6548
+ }
6549
+
6550
+ $extended = [];
6551
+
6552
+ foreach ($selectors as $selector) {
6553
+ if (! $replace) {
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
+ }
6566
+
6567
+ $this->extends = $saveExtends;
6568
+ $this->extendsMap = $saveExtendsMap;
6569
+
6570
+ return $extended;
6571
+ }
6572
+
6573
+ protected static $libSelectorNest = ['selector...'];
6574
+ protected function libSelectorNest($args)
6575
+ {
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;
6589
+
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
+ }
6608
+
6609
+ protected static $libSelectorUnify = ['selectors1', 'selectors2'];
6610
+ protected function libSelectorUnify($args)
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
6622
+ $compound1 = reset($selectors1);
6623
+ $compound2 = reset($selectors2);
6624
+
6625
+ // unify them and that's it
6626
+ $unified = $this->unifyCompoundSelectors($compound1, $compound2);
6627
+
6628
+ return $this->formatOutputSelector($unified);
6629
+ }
6630
+
6631
+ /**
6632
+ * The selector-unify magic as its best
6633
+ * (at least works as expected on test cases)
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 [[]];
6656
+ }
6657
+
6658
+ $unifiedCompound = [$last];
6659
+ $unifiedSelectors = [$unifiedCompound];
6660
+
6661
+ // do the rest
6662
+ while (count($compound1) || count($compound2)) {
6663
+ $part1 = end($compound1);
6664
+ $part2 = end($compound2);
6665
+
6666
+ if ($part1 && ($match2 = $this->matchPartInCompound($part1, $compound2))) {
6667
+ list($compound2, $part2, $after2) = $match2;
6668
+
6669
+ if ($after2) {
6670
+ $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after2);
6671
+ }
6672
+
6673
+ $c = $this->mergeParts($part1, $part2);
6674
+ $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
6675
+ $part1 = $part2 = null;
6676
+
6677
+ array_pop($compound1);
6678
+ }
6679
+
6680
+ if ($part2 && ($match1 = $this->matchPartInCompound($part2, $compound1))) {
6681
+ list($compound1, $part1, $after1) = $match1;
6682
+
6683
+ if ($after1) {
6684
+ $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after1);
6685
+ }
6686
+
6687
+ $c = $this->mergeParts($part2, $part1);
6688
+ $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
6689
+ $part1 = $part2 = null;
6690
+
6691
+ array_pop($compound2);
6692
+ }
6693
+
6694
+ $new = [];
6695
+
6696
+ if ($part1 && $part2) {
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);
6706
+
6707
+ $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part1]));
6708
+ } elseif ($part2) {
6709
+ array_pop($compound2);
6710
+
6711
+ $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part2]));
6712
+ }
6713
+
6714
+ if ($new) {
6715
+ $unifiedSelectors = $new;
6716
+ }
6717
+ }
6718
+
6719
+ return $unifiedSelectors;
6720
+ }
6721
+
6722
+ /**
6723
+ * Prepend each selector from $selectors with $parts
6724
+ *
6725
+ * @param array $selectors
6726
+ * @param array $parts
6727
+ *
6728
+ * @return array
6729
+ */
6730
+ protected function prependSelectors($selectors, $parts)
6731
+ {
6732
+ $new = [];
6733
+
6734
+ foreach ($selectors as $compoundSelector) {
6735
+ array_unshift($compoundSelector, $parts);
6736
+
6737
+ $new[] = $compoundSelector;
6738
+ }
6739
+
6740
+ return $new;
6741
+ }
6742
+
6743
+ /**
6744
+ * Try to find a matching part in a compound:
6745
+ * - with same html tag name
6746
+ * - with some class or id or something in common
6747
+ *
6748
+ * @param array $part
6749
+ * @param array $compound
6750
+ *
6751
+ * @return array|boolean
6752
+ */
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)) {
6764
+ return [$before, $p, $after];
6765
+ }
6766
+
6767
+ $after[] = $p;
6768
+ }
6769
+
6770
+ // try again matching a non empty intersection and a compatible tagname
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
+ }
6782
+
6783
+ $after[] = $p;
6784
+ }
6785
+
6786
+ return false;
6787
+ }
6788
+
6789
+ /**
6790
+ * Merge two part list taking care that
6791
+ * - the html tag is coming first - if any
6792
+ * - the :something are coming last
6793
+ *
6794
+ * @param array $parts1
6795
+ * @param array $parts2
6796
+ *
6797
+ * @return array
6798
+ */
6799
+ protected function mergeParts($parts1, $parts2)
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) {
6807
+ return [];
6808
+ }
6809
+
6810
+ if ($tag) {
6811
+ if ($tag1) {
6812
+ $parts1 = array_diff($parts1, [$tag1]);
6813
+ }
6814
+
6815
+ if ($tag2) {
6816
+ $parts2 = array_diff($parts2, [$tag2]);
6817
+ }
6818
+ }
6819
+
6820
+ $mergedParts = array_merge($parts1, $parts2);
6821
+ $mergedOrderedParts = [];
6822
+
6823
+ foreach ($mergedParts as $part) {
6824
+ if (strpos($part, ':') === 0) {
6825
+ $mergedOrderedParts[] = $part;
6826
+ }
6827
+ }
6828
+
6829
+ $mergedParts = array_diff($mergedParts, $mergedOrderedParts);
6830
+ $mergedParts = array_merge($mergedParts, $mergedOrderedParts);
6831
+
6832
+ if ($tag) {
6833
+ array_unshift($mergedParts, $tag);
6834
+ }
6835
+
6836
+ return $mergedParts;
6837
+ }
6838
+
6839
+ /**
6840
+ * Check the compatibility between two tag names:
6841
+ * if both are defined they should be identical or one has to be '*'
6842
+ *
6843
+ * @param string $tag1
6844
+ * @param string $tag2
6845
+ *
6846
+ * @return array|boolean
6847
+ */
6848
+ protected function checkCompatibleTags($tag1, $tag2)
6849
+ {
6850
+ $tags = [$tag1, $tag2];
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
+
6863
+ return $tags;
6864
+ }
6865
+
6866
+ /**
6867
+ * Find the html tag name in a selector parts list
6868
+ *
6869
+ * @param array $parts
6870
+ *
6871
+ * @return mixed|string
6872
+ */
6873
+ protected function findTagName($parts)
6874
+ {
6875
+ foreach ($parts as $part) {
6876
+ if (! preg_match('/^[\[.:#%_-]/', $part)) {
6877
+ return $part;
6878
+ }
6879
+ }
6880
+
6881
+ return '';
6882
+ }
6883
+
6884
+ protected static $libSimpleSelectors = ['selector'];
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);
6892
+
6893
+ // remove parts list layer, keeping the first part
6894
+ $part = reset($selector);
6895
+
6896
+ $listParts = [];
6897
+
6898
+ foreach ($part as $p) {
6899
+ $listParts[] = [Type::T_STRING, '', [$p]];
6900
+ }
6901
+
6902
+ return [Type::T_LIST, ',', $listParts];
6903
+ }
6904
  }
scssphp/src/Compiler/Environment.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Compiler;
13
 
14
  /**
15
  * Compiler environment
@@ -19,12 +19,12 @@ namespace Leafo\ScssPhp\Compiler;
19
  class Environment
20
  {
21
  /**
22
- * @var \Leafo\ScssPhp\Block
23
  */
24
  public $block;
25
 
26
  /**
27
- * @var \Leafo\ScssPhp\Compiler\Environment
28
  */
29
  public $parent;
30
 
@@ -33,6 +33,11 @@ class Environment
33
  */
34
  public $store;
35
 
 
 
 
 
 
36
  /**
37
  * @var integer
38
  */
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Compiler;
13
 
14
  /**
15
  * Compiler environment
19
  class Environment
20
  {
21
  /**
22
+ * @var \ScssPhp\ScssPhp\Block
23
  */
24
  public $block;
25
 
26
  /**
27
+ * @var \ScssPhp\ScssPhp\Compiler\Environment
28
  */
29
  public $parent;
30
 
33
  */
34
  public $store;
35
 
36
+ /**
37
+ * @var array
38
+ */
39
+ public $storeUnreduced;
40
+
41
  /**
42
  * @var integer
43
  */
scssphp/src/Exception/CompilerException.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Exception;
13
 
14
  /**
15
  * Compiler exception
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Exception;
13
 
14
  /**
15
  * Compiler exception
scssphp/src/Exception/ParserException.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Exception;
13
 
14
  /**
15
  * Parser Exception
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Exception;
13
 
14
  /**
15
  * Parser Exception
scssphp/src/Exception/RangeException.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Exception;
13
 
14
  /**
15
  * Range exception
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Exception;
13
 
14
  /**
15
  * Range exception
scssphp/src/Exception/ServerException.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Exception;
13
 
14
  /**
15
  * Server Exception
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Exception;
13
 
14
  /**
15
  * Server Exception
scssphp/src/Formatter.php CHANGED
@@ -2,17 +2,17 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
 
14
- use Leafo\ScssPhp\Formatter\OutputBlock;
15
- use Leafo\ScssPhp\SourceMap\SourceMapGenerator;
16
 
17
  /**
18
  * Base formatter
@@ -62,7 +62,7 @@ abstract class Formatter
62
  public $keepSemicolons;
63
 
64
  /**
65
- * @var \Leafo\ScssPhp\Formatter\OutputBlock
66
  */
67
  protected $currentBlock;
68
 
@@ -77,7 +77,7 @@ abstract class Formatter
77
  protected $currentColumn;
78
 
79
  /**
80
- * @var \Leafo\ScssPhp\SourceMap\SourceMapGenerator
81
  */
82
  protected $sourceMapGenerator;
83
 
@@ -126,9 +126,7 @@ abstract class Formatter
126
  return;
127
  }
128
 
129
- if (($count = count($lines))
130
- && substr($lines[$count - 1], -1) === ';'
131
- ) {
132
  $lines[$count - 1] = substr($lines[$count - 1], 0, -1);
133
  }
134
  }
@@ -136,7 +134,7 @@ abstract class Formatter
136
  /**
137
  * Output lines inside a block
138
  *
139
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
140
  */
141
  protected function blockLines(OutputBlock $block)
142
  {
@@ -154,7 +152,7 @@ abstract class Formatter
154
  /**
155
  * Output block selectors
156
  *
157
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
158
  */
159
  protected function blockSelectors(OutputBlock $block)
160
  {
@@ -168,7 +166,7 @@ abstract class Formatter
168
  /**
169
  * Output block children
170
  *
171
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
172
  */
173
  protected function blockChildren(OutputBlock $block)
174
  {
@@ -180,7 +178,7 @@ abstract class Formatter
180
  /**
181
  * Output non-empty block
182
  *
183
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
184
  */
185
  protected function block(OutputBlock $block)
186
  {
@@ -217,13 +215,37 @@ abstract class Formatter
217
  }
218
  }
219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  /**
221
  * Entry point to formatting a block
222
  *
223
  * @api
224
  *
225
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block An abstract syntax tree
226
- * @param \Leafo\ScssPhp\SourceMap\SourceMapGenerator|null $sourceMapGenerator Optional source map generator
227
  *
228
  * @return string
229
  */
@@ -237,6 +259,8 @@ abstract class Formatter
237
  $this->sourceMapGenerator = $sourceMapGenerator;
238
  }
239
 
 
 
240
  ob_start();
241
 
242
  $this->block($block);
@@ -256,7 +280,8 @@ abstract class Formatter
256
  $this->currentLine,
257
  $this->currentColumn,
258
  $this->currentBlock->sourceLine,
259
- $this->currentBlock->sourceColumn - 1, //columns from parser are off by one
 
260
  $this->currentBlock->sourceName
261
  );
262
 
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
 
14
+ use ScssPhp\ScssPhp\Formatter\OutputBlock;
15
+ use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
16
 
17
  /**
18
  * Base formatter
62
  public $keepSemicolons;
63
 
64
  /**
65
+ * @var \ScssPhp\ScssPhp\Formatter\OutputBlock
66
  */
67
  protected $currentBlock;
68
 
77
  protected $currentColumn;
78
 
79
  /**
80
+ * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator
81
  */
82
  protected $sourceMapGenerator;
83
 
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
  }
134
  /**
135
  * Output lines inside a block
136
  *
137
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
138
  */
139
  protected function blockLines(OutputBlock $block)
140
  {
152
  /**
153
  * Output block selectors
154
  *
155
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
156
  */
157
  protected function blockSelectors(OutputBlock $block)
158
  {
166
  /**
167
  * Output block children
168
  *
169
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
170
  */
171
  protected function blockChildren(OutputBlock $block)
172
  {
178
  /**
179
  * Output non-empty block
180
  *
181
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
182
  */
183
  protected function block(OutputBlock $block)
184
  {
215
  }
216
  }
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
+ {
225
+ $isEmpty = empty($block->lines);
226
+
227
+ if ($block->children) {
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
+
242
  /**
243
  * Entry point to formatting a block
244
  *
245
  * @api
246
  *
247
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block An abstract syntax tree
248
+ * @param \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null $sourceMapGenerator Optional source map generator
249
  *
250
  * @return string
251
  */
259
  $this->sourceMapGenerator = $sourceMapGenerator;
260
  }
261
 
262
+ $this->testEmptyChildren($block);
263
+
264
  ob_start();
265
 
266
  $this->block($block);
280
  $this->currentLine,
281
  $this->currentColumn,
282
  $this->currentBlock->sourceLine,
283
+ //columns from parser are off by one
284
+ $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0,
285
  $this->currentBlock->sourceName
286
  );
287
 
scssphp/src/Formatter/Compact.php CHANGED
@@ -2,16 +2,16 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Formatter;
13
 
14
- use Leafo\ScssPhp\Formatter;
15
 
16
  /**
17
  * Compact formatter
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Formatter;
13
 
14
+ use ScssPhp\ScssPhp\Formatter;
15
 
16
  /**
17
  * Compact formatter
scssphp/src/Formatter/Compressed.php CHANGED
@@ -2,17 +2,17 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Formatter;
13
 
14
- use Leafo\ScssPhp\Formatter;
15
- use Leafo\ScssPhp\Formatter\OutputBlock;
16
 
17
  /**
18
  * Compressed formatter
@@ -59,4 +59,23 @@ class Compressed extends Formatter
59
  $this->write($this->break);
60
  }
61
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  }
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Formatter;
13
 
14
+ use ScssPhp\ScssPhp\Formatter;
15
+ use ScssPhp\ScssPhp\Formatter\OutputBlock;
16
 
17
  /**
18
  * Compressed formatter
59
  $this->write($this->break);
60
  }
61
  }
62
+
63
+ /**
64
+ * Output block selectors
65
+ *
66
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
67
+ */
68
+ protected function blockSelectors(OutputBlock $block)
69
+ {
70
+ $inner = $this->indentStr();
71
+
72
+ $this->write(
73
+ $inner
74
+ . implode(
75
+ $this->tagSeparator,
76
+ str_replace([' > ', ' + ', ' ~ '], ['>', '+', '~'], $block->selectors)
77
+ )
78
+ . $this->open . $this->break
79
+ );
80
+ }
81
  }
scssphp/src/Formatter/Crunched.php CHANGED
@@ -2,17 +2,17 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Formatter;
13
 
14
- use Leafo\ScssPhp\Formatter;
15
- use Leafo\ScssPhp\Formatter\OutputBlock;
16
 
17
  /**
18
  * Crunched formatter
@@ -57,4 +57,23 @@ class Crunched extends Formatter
57
  $this->write($this->break);
58
  }
59
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Formatter;
13
 
14
+ use ScssPhp\ScssPhp\Formatter;
15
+ use ScssPhp\ScssPhp\Formatter\OutputBlock;
16
 
17
  /**
18
  * Crunched formatter
57
  $this->write($this->break);
58
  }
59
  }
60
+
61
+ /**
62
+ * Output block selectors
63
+ *
64
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
65
+ */
66
+ protected function blockSelectors(OutputBlock $block)
67
+ {
68
+ $inner = $this->indentStr();
69
+
70
+ $this->write(
71
+ $inner
72
+ . implode(
73
+ $this->tagSeparator,
74
+ str_replace([' > ', ' + ', ' ~ '], ['>', '+', '~'], $block->selectors)
75
+ )
76
+ . $this->open . $this->break
77
+ );
78
+ }
79
  }
scssphp/src/Formatter/Debug.php CHANGED
@@ -2,17 +2,17 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Formatter;
13
 
14
- use Leafo\ScssPhp\Formatter;
15
- use Leafo\ScssPhp\Formatter\OutputBlock;
16
 
17
  /**
18
  * Debug formatter
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Formatter;
13
 
14
+ use ScssPhp\ScssPhp\Formatter;
15
+ use ScssPhp\ScssPhp\Formatter\OutputBlock;
16
 
17
  /**
18
  * Debug formatter
scssphp/src/Formatter/Expanded.php CHANGED
@@ -2,17 +2,17 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Formatter;
13
 
14
- use Leafo\ScssPhp\Formatter;
15
- use Leafo\ScssPhp\Formatter\OutputBlock;
16
 
17
  /**
18
  * Expanded formatter
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Formatter;
13
 
14
+ use ScssPhp\ScssPhp\Formatter;
15
+ use ScssPhp\ScssPhp\Formatter\OutputBlock;
16
 
17
  /**
18
  * Expanded formatter
scssphp/src/Formatter/Nested.php CHANGED
@@ -2,17 +2,18 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Formatter;
13
 
14
- use Leafo\ScssPhp\Formatter;
15
- use Leafo\ScssPhp\Formatter\OutputBlock;
 
16
 
17
  /**
18
  * Nested formatter
@@ -67,53 +68,56 @@ class Nested extends Formatter
67
  }
68
 
69
  $this->write($inner . implode($glue, $block->lines));
70
-
71
- if (! empty($block->children)) {
72
- $this->write($this->break);
73
- }
74
  }
75
 
76
- /**
77
- * {@inheritdoc}
78
- */
79
- protected function blockSelectors(OutputBlock $block)
80
  {
81
- $inner = $this->indentStr();
 
 
 
 
82
 
83
- $this->write($inner
84
- . implode($this->tagSeparator, $block->selectors)
85
- . $this->open . $this->break);
86
  }
87
 
88
  /**
89
  * {@inheritdoc}
90
  */
91
- protected function blockChildren(OutputBlock $block)
92
  {
93
- foreach ($block->children as $i => $child) {
94
- $this->block($child);
 
 
 
95
 
96
- if ($i < count($block->children) - 1) {
97
- $this->write($this->break);
 
 
 
 
 
 
98
 
99
- if (isset($block->children[$i + 1])) {
100
- $next = $block->children[$i + 1];
 
101
 
102
- if ($next->depth === max($block->depth, 1) && $child->depth >= $next->depth) {
103
- $this->write($this->break);
104
- }
105
- }
 
 
 
 
106
  }
107
- }
108
- }
109
 
110
- /**
111
- * {@inheritdoc}
112
- */
113
- protected function block(OutputBlock $block)
114
- {
115
- if ($block->type === 'root') {
116
- $this->adjustAllChildren($block);
117
  }
118
 
119
  if (empty($block->lines) && empty($block->children)) {
@@ -122,80 +126,87 @@ class Nested extends Formatter
122
 
123
  $this->currentBlock = $block;
124
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- $this->depth = $block->depth;
 
127
 
128
  if (! empty($block->selectors)) {
 
 
 
 
 
 
 
 
 
 
129
  $this->blockSelectors($block);
130
 
131
  $this->indentLevel++;
132
  }
133
 
134
  if (! empty($block->lines)) {
 
 
 
 
 
 
 
 
 
 
135
  $this->blockLines($block);
 
136
  }
137
 
138
  if (! empty($block->children)) {
139
- $this->blockChildren($block);
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  }
141
 
142
  if (! empty($block->selectors)) {
143
  $this->indentLevel--;
144
 
145
  $this->write($this->close);
146
- }
147
 
148
- if ($block->type === 'root') {
149
- $this->write($this->break);
150
- }
151
- }
152
-
153
- /**
154
- * Adjust the depths of all children, depth first
155
- *
156
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
157
- */
158
- private function adjustAllChildren(OutputBlock $block)
159
- {
160
- // flatten empty nested blocks
161
- $children = [];
162
-
163
- foreach ($block->children as $i => $child) {
164
- if (empty($child->lines) && empty($child->children)) {
165
- if (isset($block->children[$i + 1])) {
166
- $block->children[$i + 1]->depth = $child->depth;
167
- }
168
-
169
- continue;
170
  }
171
-
172
- $children[] = $child;
173
- }
174
-
175
- $count = count($children);
176
-
177
- for ($i = 0; $i < $count; $i++) {
178
- $depth = $children[$i]->depth;
179
- $j = $i + 1;
180
-
181
- if (isset($children[$j]) && $depth < $children[$j]->depth) {
182
- $childDepth = $children[$j]->depth;
183
-
184
- for (; $j < $count; $j++) {
185
- if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) {
186
- $children[$j]->depth = $depth + 1;
187
- }
188
- }
189
  }
190
  }
191
 
192
- $block->children = $children;
193
-
194
- // make relative to parent
195
- foreach ($block->children as $child) {
196
- $this->adjustAllChildren($child);
197
-
198
- $child->depth = $child->depth - $block->depth;
199
  }
200
  }
201
  }
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Formatter;
13
 
14
+ use ScssPhp\ScssPhp\Formatter;
15
+ use ScssPhp\ScssPhp\Formatter\OutputBlock;
16
+ use ScssPhp\ScssPhp\Type;
17
 
18
  /**
19
  * Nested formatter
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
  */
87
+ protected function block(OutputBlock $block)
88
  {
89
+ static $depths;
90
+ static $downLevel;
91
+ static $closeBlock;
92
+ static $previousEmpty;
93
+ static $previousHasSelector;
94
 
95
+ if ($block->type === 'root') {
96
+ $depths = [ 0 ];
97
+ $downLevel = '';
98
+ $closeBlock = '';
99
+ $this->depth = 0;
100
+ $previousEmpty = false;
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
 
108
+ while ($block->depth < end($depths) || ($block->depth == 1 && end($depths) == 1)) {
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;
116
  }
 
 
117
 
118
+ if (empty($block->lines) && empty($block->children)) {
119
+ $previousEmpty = true;
120
+ }
 
 
 
 
121
  }
122
 
123
  if (empty($block->lines) && empty($block->children)) {
126
 
127
  $this->currentBlock = $block;
128
 
129
+ if (! empty($block->lines) || (! empty($block->children) && ($this->depth < 1 || $isSupport))) {
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
+ }
140
+ }
141
 
142
+ $previousEmpty = ($block->type === Type::T_COMMENT);
143
+ $previousHasSelector = false;
144
 
145
  if (! empty($block->selectors)) {
146
+ if ($closeBlock) {
147
+ $this->write($closeBlock);
148
+ $closeBlock = '';
149
+ }
150
+
151
+ if ($downLevel) {
152
+ $this->write($downLevel);
153
+ $downLevel = '';
154
+ }
155
+
156
  $this->blockSelectors($block);
157
 
158
  $this->indentLevel++;
159
  }
160
 
161
  if (! empty($block->lines)) {
162
+ if ($closeBlock) {
163
+ $this->write($closeBlock);
164
+ $closeBlock = '';
165
+ }
166
+
167
+ if ($downLevel) {
168
+ $this->write($downLevel);
169
+ $downLevel = '';
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);
185
+ }
186
+ }
187
+
188
+ // reclear to not be spoiled by children if T_DIRECTIVE
189
+ if ($block->type === Type::T_DIRECTIVE) {
190
+ $previousHasSelector = false;
191
  }
192
 
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
  }
206
  }
207
 
208
+ if ($block->type === 'root') {
209
+ $this->write($this->break);
 
 
 
 
 
210
  }
211
  }
212
  }
scssphp/src/Formatter/OutputBlock.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Formatter;
13
 
14
  /**
15
  * Output block
@@ -44,7 +44,7 @@ class OutputBlock
44
  public $children;
45
 
46
  /**
47
- * @var \Leafo\ScssPhp\Formatter\OutputBlock
48
  */
49
  public $parent;
50
 
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Formatter;
13
 
14
  /**
15
  * Output block
44
  public $children;
45
 
46
  /**
47
+ * @var \ScssPhp\ScssPhp\Formatter\OutputBlock
48
  */
49
  public $parent;
50
 
scssphp/src/Node.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
 
14
  /**
15
  * Base node
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
 
14
  /**
15
  * Base node
scssphp/src/Node/Number.php CHANGED
@@ -2,18 +2,18 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Node;
13
 
14
- use Leafo\ScssPhp\Compiler;
15
- use Leafo\ScssPhp\Node;
16
- use Leafo\ScssPhp\Type;
17
 
18
  /**
19
  * Dimension + optional units
@@ -100,7 +100,7 @@ class Number extends Node implements \ArrayAccess
100
  *
101
  * @param array $units
102
  *
103
- * @return \Leafo\ScssPhp\Node\Number
104
  */
105
  public function coerce($units)
106
  {
@@ -123,7 +123,7 @@ class Number extends Node implements \ArrayAccess
123
  /**
124
  * Normalize number
125
  *
126
- * @return \Leafo\ScssPhp\Node\Number
127
  */
128
  public function normalize()
129
  {
@@ -148,10 +148,10 @@ class Number extends Node implements \ArrayAccess
148
  return $this->sourceLine !== null;
149
  }
150
 
151
- if ($offset === -1
152
- || $offset === 0
153
- || $offset === 1
154
- || $offset === 2
155
  ) {
156
  return true;
157
  }
@@ -259,7 +259,7 @@ class Number extends Node implements \ArrayAccess
259
  /**
260
  * Output number
261
  *
262
- * @param \Leafo\ScssPhp\Compiler $compiler
263
  *
264
  * @return string
265
  */
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Node;
13
 
14
+ use ScssPhp\ScssPhp\Compiler;
15
+ use ScssPhp\ScssPhp\Node;
16
+ use ScssPhp\ScssPhp\Type;
17
 
18
  /**
19
  * Dimension + optional units
100
  *
101
  * @param array $units
102
  *
103
+ * @return \ScssPhp\ScssPhp\Node\Number
104
  */
105
  public function coerce($units)
106
  {
123
  /**
124
  * Normalize number
125
  *
126
+ * @return \ScssPhp\ScssPhp\Node\Number
127
  */
128
  public function normalize()
129
  {
148
  return $this->sourceLine !== null;
149
  }
150
 
151
+ if ($offset === -1 ||
152
+ $offset === 0 ||
153
+ $offset === 1 ||
154
+ $offset === 2
155
  ) {
156
  return true;
157
  }
259
  /**
260
  * Output number
261
  *
262
+ * @param \ScssPhp\ScssPhp\Compiler $compiler
263
  *
264
  * @return string
265
  */
scssphp/src/Parser.php CHANGED
@@ -2,20 +2,21 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
 
14
- use Leafo\ScssPhp\Block;
15
- use Leafo\ScssPhp\Compiler;
16
- use Leafo\ScssPhp\Exception\ParserException;
17
- use Leafo\ScssPhp\Node;
18
- use Leafo\ScssPhp\Type;
 
19
 
20
  /**
21
  * Parser
@@ -53,6 +54,8 @@ class Parser
53
  protected static $operatorPattern;
54
  protected static $whitePattern;
55
 
 
 
56
  private $sourceName;
57
  private $sourceIndex;
58
  private $sourcePositions;
@@ -61,27 +64,32 @@ class Parser
61
  private $env;
62
  private $inParens;
63
  private $eatWhiteDefault;
 
64
  private $buffer;
65
  private $utf8;
66
  private $encoding;
67
  private $patternModifiers;
 
68
 
69
  /**
70
  * Constructor
71
  *
72
  * @api
73
  *
74
- * @param string $sourceName
75
- * @param integer $sourceIndex
76
- * @param string $encoding
 
77
  */
78
- public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8')
79
  {
80
  $this->sourceName = $sourceName ?: '(stdin)';
81
  $this->sourceIndex = $sourceIndex;
82
  $this->charset = null;
83
  $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8';
84
  $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';
 
 
85
 
86
  if (empty(static::$operatorPattern)) {
87
  static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
@@ -95,6 +103,10 @@ class Parser
95
  ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS'
96
  : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS';
97
  }
 
 
 
 
98
  }
99
 
100
  /**
@@ -116,13 +128,15 @@ class Parser
116
  *
117
  * @param string $msg
118
  *
119
- * @throws \Leafo\ScssPhp\Exception\ParserException
120
  */
121
  public function throwParseError($msg = 'parse error')
122
  {
123
- list($line, /* $column */) = $this->getSourcePosition($this->count);
124
 
125
- $loc = empty($this->sourceName) ? "line: $line" : "$this->sourceName on line $line";
 
 
126
 
127
  if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
128
  throw new ParserException("$msg: failed at `$m[1]` $loc");
@@ -138,10 +152,23 @@ class Parser
138
  *
139
  * @param string $buffer
140
  *
141
- * @return \Leafo\ScssPhp\Block
142
  */
143
  public function parse($buffer)
144
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  // strip BOM (byte order marker)
146
  if (substr($buffer, 0, 3) === "\xef\xbb\xbf") {
147
  $buffer = substr($buffer, 3);
@@ -177,10 +204,12 @@ class Parser
177
  array_unshift($this->env->children, $this->charset);
178
  }
179
 
180
- $this->env->isRoot = true;
181
-
182
  $this->restoreEncoding();
183
 
 
 
 
 
184
  return $this->env;
185
  }
186
 
@@ -238,6 +267,34 @@ class Parser
238
  return $selector;
239
  }
240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  /**
242
  * Parse a single chunk off the head of the buffer and append it to the
243
  * current parse environment.
@@ -271,7 +328,7 @@ class Parser
271
  * the buffer position will be left at an invalid state. In order to
272
  * avoid this, Compiler::seek() is used to remember and set buffer positions.
273
  *
274
- * Before parsing a chain, use $s = $this->seek() to remember the current
275
  * position into $s. Then if a chain fails, use $this->seek($s) to
276
  * go back where we started.
277
  *
@@ -279,14 +336,14 @@ class Parser
279
  */
280
  protected function parseChunk()
281
  {
282
- $s = $this->seek();
283
 
284
  // the directives
285
  if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
286
- if ($this->literal('@at-root') &&
287
  ($this->selectors($selector) || true) &&
288
  ($this->map($with) || true) &&
289
- $this->literal('{')
290
  ) {
291
  $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
292
  $atRoot->selector = $selector;
@@ -297,7 +354,7 @@ class Parser
297
 
298
  $this->seek($s);
299
 
300
- if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) {
301
  $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
302
  $media->queryList = $mediaQueryList[2];
303
 
@@ -306,10 +363,10 @@ class Parser
306
 
307
  $this->seek($s);
308
 
309
- if ($this->literal('@mixin') &&
310
  $this->keyword($mixinName) &&
311
  ($this->argumentDef($args) || true) &&
312
- $this->literal('{')
313
  ) {
314
  $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
315
  $mixin->name = $mixinName;
@@ -320,13 +377,13 @@ class Parser
320
 
321
  $this->seek($s);
322
 
323
- if ($this->literal('@include') &&
324
  $this->keyword($mixinName) &&
325
- ($this->literal('(') &&
326
  ($this->argValues($argValues) || true) &&
327
- $this->literal(')') || true) &&
328
  ($this->end() ||
329
- $this->literal('{') && $hasBlock = true)
330
  ) {
331
  $child = [Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null];
332
 
@@ -342,7 +399,7 @@ class Parser
342
 
343
  $this->seek($s);
344
 
345
- if ($this->literal('@scssphp-import-once') &&
346
  $this->valueList($importPath) &&
347
  $this->end()
348
  ) {
@@ -353,7 +410,7 @@ class Parser
353
 
354
  $this->seek($s);
355
 
356
- if ($this->literal('@import') &&
357
  $this->valueList($importPath) &&
358
  $this->end()
359
  ) {
@@ -364,7 +421,7 @@ class Parser
364
 
365
  $this->seek($s);
366
 
367
- if ($this->literal('@import') &&
368
  $this->url($importPath) &&
369
  $this->end()
370
  ) {
@@ -375,7 +432,7 @@ class Parser
375
 
376
  $this->seek($s);
377
 
378
- if ($this->literal('@extend') &&
379
  $this->selectors($selectors) &&
380
  $this->end()
381
  ) {
@@ -388,10 +445,10 @@ class Parser
388
 
389
  $this->seek($s);
390
 
391
- if ($this->literal('@function') &&
392
  $this->keyword($fnName) &&
393
  $this->argumentDef($args) &&
394
- $this->literal('{')
395
  ) {
396
  $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
397
  $func->name = $fnName;
@@ -402,7 +459,7 @@ class Parser
402
 
403
  $this->seek($s);
404
 
405
- if ($this->literal('@break') && $this->end()) {
406
  $this->append([Type::T_BREAK], $s);
407
 
408
  return true;
@@ -410,7 +467,7 @@ class Parser
410
 
411
  $this->seek($s);
412
 
413
- if ($this->literal('@continue') && $this->end()) {
414
  $this->append([Type::T_CONTINUE], $s);
415
 
416
  return true;
@@ -418,8 +475,7 @@ class Parser
418
 
419
  $this->seek($s);
420
 
421
-
422
- if ($this->literal('@return') && ($this->valueList($retVal) || true) && $this->end()) {
423
  $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
424
 
425
  return true;
@@ -427,11 +483,11 @@ class Parser
427
 
428
  $this->seek($s);
429
 
430
- if ($this->literal('@each') &&
431
  $this->genericList($varNames, 'variable', ',', false) &&
432
- $this->literal('in') &&
433
  $this->valueList($list) &&
434
- $this->literal('{')
435
  ) {
436
  $each = $this->pushSpecialBlock(Type::T_EACH, $s);
437
 
@@ -446,9 +502,9 @@ class Parser
446
 
447
  $this->seek($s);
448
 
449
- if ($this->literal('@while') &&
450
  $this->expression($cond) &&
451
- $this->literal('{')
452
  ) {
453
  $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
454
  $while->cond = $cond;
@@ -458,14 +514,14 @@ class Parser
458
 
459
  $this->seek($s);
460
 
461
- if ($this->literal('@for') &&
462
  $this->variable($varName) &&
463
- $this->literal('from') &&
464
  $this->expression($start) &&
465
- ($this->literal('through') ||
466
- ($forUntil = true && $this->literal('to'))) &&
467
  $this->expression($end) &&
468
- $this->literal('{')
469
  ) {
470
  $for = $this->pushSpecialBlock(Type::T_FOR, $s);
471
  $for->var = $varName[1];
@@ -478,7 +534,7 @@ class Parser
478
 
479
  $this->seek($s);
480
 
481
- if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) {
482
  $if = $this->pushSpecialBlock(Type::T_IF, $s);
483
  $if->cond = $cond;
484
  $if->cases = [];
@@ -488,7 +544,7 @@ class Parser
488
 
489
  $this->seek($s);
490
 
491
- if ($this->literal('@debug') &&
492
  $this->valueList($value) &&
493
  $this->end()
494
  ) {
@@ -499,7 +555,7 @@ class Parser
499
 
500
  $this->seek($s);
501
 
502
- if ($this->literal('@warn') &&
503
  $this->valueList($value) &&
504
  $this->end()
505
  ) {
@@ -510,7 +566,7 @@ class Parser
510
 
511
  $this->seek($s);
512
 
513
- if ($this->literal('@error') &&
514
  $this->valueList($value) &&
515
  $this->end()
516
  ) {
@@ -521,7 +577,7 @@ class Parser
521
 
522
  $this->seek($s);
523
 
524
- if ($this->literal('@content') && $this->end()) {
525
  $this->append([Type::T_MIXIN_CONTENT], $s);
526
 
527
  return true;
@@ -534,10 +590,10 @@ class Parser
534
  if (isset($last) && $last[0] === Type::T_IF) {
535
  list(, $if) = $last;
536
 
537
- if ($this->literal('@else')) {
538
- if ($this->literal('{')) {
539
  $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
540
- } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) {
541
  $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
542
  $else->cond = $cond;
543
  }
@@ -554,7 +610,7 @@ class Parser
554
  }
555
 
556
  // only retain the first @charset directive encountered
557
- if ($this->literal('@charset') &&
558
  $this->valueList($charset) &&
559
  $this->end()
560
  ) {
@@ -575,11 +631,23 @@ class Parser
575
 
576
  $this->seek($s);
577
 
 
 
 
 
 
 
 
 
 
 
 
 
578
  // doesn't match built in directive, do generic one
579
- if ($this->literal('@', false) &&
580
  $this->keyword($dirName) &&
581
  ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
582
- $this->literal('{')
583
  ) {
584
  if ($dirName === 'media') {
585
  $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
@@ -603,7 +671,7 @@ class Parser
603
  // property shortcut
604
  // captures most properties before having to parse a selector
605
  if ($this->keyword($name, false) &&
606
- $this->literal(': ') &&
607
  $this->valueList($value) &&
608
  $this->end()
609
  ) {
@@ -617,7 +685,7 @@ class Parser
617
 
618
  // variable assigns
619
  if ($this->variable($name) &&
620
- $this->literal(':') &&
621
  $this->valueList($value) &&
622
  $this->end()
623
  ) {
@@ -631,31 +699,42 @@ class Parser
631
  $this->seek($s);
632
 
633
  // misc
634
- if ($this->literal('-->')) {
635
  return true;
636
  }
637
 
638
  // opening css block
639
- if ($this->selectors($selectors) && $this->literal('{')) {
640
  $this->pushBlock($selectors, $s);
641
 
 
 
 
 
 
642
  return true;
643
  }
644
 
645
  $this->seek($s);
646
 
647
  // property assign, or nested assign
648
- if ($this->propertyName($name) && $this->literal(':')) {
649
  $foundSomething = false;
650
 
651
  if ($this->valueList($value)) {
 
 
 
 
652
  $this->append([Type::T_ASSIGN, $name, $value], $s);
653
  $foundSomething = true;
654
  }
655
 
656
- if ($this->literal('{')) {
657
  $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
658
  $propBlock->prefix = $name;
 
 
659
  $foundSomething = true;
660
  } elseif ($foundSomething) {
661
  $foundSomething = $this->end();
@@ -669,9 +748,15 @@ class Parser
669
  $this->seek($s);
670
 
671
  // closing a block
672
- if ($this->literal('}')) {
673
  $block = $this->popBlock();
674
 
 
 
 
 
 
 
675
  if (isset($block->type) && $block->type === Type::T_INCLUDE) {
676
  $include = $block->child;
677
  unset($block->child);
@@ -682,12 +767,21 @@ class Parser
682
  $this->append([$type, $block], $s);
683
  }
684
 
 
 
 
 
 
 
 
 
 
685
  return true;
686
  }
687
 
688
  // extra stuff
689
- if ($this->literal(';') ||
690
- $this->literal('<!--')
691
  ) {
692
  return true;
693
  }
@@ -701,7 +795,7 @@ class Parser
701
  * @param array $selectors
702
  * @param integer $pos
703
  *
704
- * @return \Leafo\ScssPhp\Block
705
  */
706
  protected function pushBlock($selectors, $pos = 0)
707
  {
@@ -729,6 +823,15 @@ class Parser
729
 
730
  $this->env = $b;
731
 
 
 
 
 
 
 
 
 
 
732
  return $b;
733
  }
734
 
@@ -738,7 +841,7 @@ class Parser
738
  * @param string $type
739
  * @param integer $pos
740
  *
741
- * @return \Leafo\ScssPhp\Block
742
  */
743
  protected function pushSpecialBlock($type, $pos)
744
  {
@@ -751,26 +854,33 @@ class Parser
751
  /**
752
  * Pop scope and return last block
753
  *
754
- * @return \Leafo\ScssPhp\Block
755
  *
756
  * @throws \Exception
757
  */
758
  protected function popBlock()
759
  {
 
 
 
 
 
 
 
760
  $block = $this->env;
761
 
762
  if (empty($block->parent)) {
763
  $this->throwParseError('unexpected }');
764
  }
765
 
 
 
 
 
 
766
  $this->env = $block->parent;
767
- unset($block->parent);
768
 
769
- $comments = $block->comments;
770
- if (count($comments)) {
771
- $this->env->comments = $comments;
772
- unset($block->comments);
773
- }
774
 
775
  return $block;
776
  }
@@ -800,18 +910,10 @@ class Parser
800
  * Seek to position in input stream (or return current position in input stream)
801
  *
802
  * @param integer $where
803
- *
804
- * @return integer
805
  */
806
- protected function seek($where = null)
807
  {
808
- if ($where === null) {
809
- return $this->count;
810
- }
811
-
812
  $this->count = $where;
813
-
814
- return true;
815
  }
816
 
817
  /**
@@ -866,52 +968,78 @@ class Parser
866
  */
867
  protected function match($regex, &$out, $eatWhitespace = null)
868
  {
 
 
 
 
 
 
 
 
869
  if (! isset($eatWhitespace)) {
870
  $eatWhitespace = $this->eatWhiteDefault;
871
  }
872
 
873
- $r = '/' . $regex . '/' . $this->patternModifiers;
 
 
874
 
875
- if (preg_match($r, $this->buffer, $out, null, $this->count)) {
876
- $this->count += strlen($out[0]);
877
 
878
- if ($eatWhitespace) {
879
- $this->whitespace();
880
- }
 
 
 
 
 
 
 
 
 
 
881
 
882
- return true;
 
 
 
883
  }
884
 
885
- return false;
 
 
 
 
886
  }
887
 
888
  /**
889
  * Match literal string
890
  *
891
  * @param string $what
 
892
  * @param boolean $eatWhitespace
893
  *
894
  * @return boolean
895
  */
896
- protected function literal($what, $eatWhitespace = null)
897
  {
898
- if (! isset($eatWhitespace)) {
899
- $eatWhitespace = $this->eatWhiteDefault;
900
  }
901
 
902
- $len = strlen($what);
903
-
904
- if (strcasecmp(substr($this->buffer, $this->count, $len), $what) === 0) {
905
- $this->count += $len;
906
 
907
- if ($eatWhitespace) {
908
- $this->whitespace();
909
- }
910
 
911
- return true;
 
912
  }
913
 
914
- return false;
915
  }
916
 
917
  /**
@@ -925,12 +1053,57 @@ class Parser
925
 
926
  while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) {
927
  if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
928
- $this->appendComment([Type::T_COMMENT, $m[1]]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
929
 
930
  $this->commentsSeen[$this->count] = true;
 
 
 
 
931
  }
932
 
933
- $this->count += strlen($m[0]);
934
  $gotWhite = true;
935
  }
936
 
@@ -944,9 +1117,13 @@ class Parser
944
  */
945
  protected function appendComment($comment)
946
  {
947
- $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
 
 
 
948
 
949
- $this->env->comments[] = $comment;
 
950
  }
951
 
952
  /**
@@ -957,19 +1134,21 @@ class Parser
957
  */
958
  protected function append($statement, $pos = null)
959
  {
960
- if ($pos !== null) {
961
- list($line, $column) = $this->getSourcePosition($pos);
 
962
 
963
- $statement[static::SOURCE_LINE] = $line;
964
- $statement[static::SOURCE_COLUMN] = $column;
965
- $statement[static::SOURCE_INDEX] = $this->sourceIndex;
966
- }
967
 
968
- $this->env->children[] = $statement;
 
969
 
970
  $comments = $this->env->comments;
971
 
972
- if (count($comments)) {
973
  $this->env->children = array_merge($this->env->children, $comments);
974
  $this->env->comments = [];
975
  }
@@ -1013,7 +1192,7 @@ class Parser
1013
  $expressions = null;
1014
  $parts = [];
1015
 
1016
- if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) &&
1017
  $this->mixedKeyword($mediaType)
1018
  ) {
1019
  $prop = [Type::T_MEDIA_TYPE];
@@ -1040,7 +1219,7 @@ class Parser
1040
  $parts[] = $prop;
1041
  }
1042
 
1043
- if (empty($parts) || $this->literal('and')) {
1044
  $this->genericList($expressions, 'mediaExpression', 'and', false);
1045
 
1046
  if (is_array($expressions)) {
@@ -1053,6 +1232,118 @@ class Parser
1053
  return true;
1054
  }
1055
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1056
  /**
1057
  * Parse media expression
1058
  *
@@ -1062,13 +1353,13 @@ class Parser
1062
  */
1063
  protected function mediaExpression(&$out)
1064
  {
1065
- $s = $this->seek();
1066
  $value = null;
1067
 
1068
- if ($this->literal('(') &&
1069
  $this->expression($feature) &&
1070
- ($this->literal(':') && $this->expression($value) || true) &&
1071
- $this->literal(')')
1072
  ) {
1073
  $out = [Type::T_MEDIA_EXPRESSION, $feature];
1074
 
@@ -1111,20 +1402,20 @@ class Parser
1111
  */
1112
  protected function argValue(&$out)
1113
  {
1114
- $s = $this->seek();
1115
 
1116
  $keyword = null;
1117
 
1118
- if (! $this->variable($keyword) || ! $this->literal(':')) {
1119
  $this->seek($s);
1120
  $keyword = null;
1121
  }
1122
 
1123
  if ($this->genericList($value, 'expression')) {
1124
  $out = [$keyword, $value, false];
1125
- $s = $this->seek();
1126
 
1127
- if ($this->literal('...')) {
1128
  $out[2] = true;
1129
  } else {
1130
  $this->seek($s);
@@ -1139,7 +1430,7 @@ class Parser
1139
  /**
1140
  * Parse comma separated value list
1141
  *
1142
- * @param string $out
1143
  *
1144
  * @return boolean
1145
  */
@@ -1172,20 +1463,21 @@ class Parser
1172
  */
1173
  protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
1174
  {
1175
- $s = $this->seek();
1176
  $items = [];
 
1177
 
1178
  while ($this->$parseItem($value)) {
1179
  $items[] = $value;
1180
 
1181
  if ($delim) {
1182
- if (! $this->literal($delim)) {
1183
  break;
1184
  }
1185
  }
1186
  }
1187
 
1188
- if (count($items) === 0) {
1189
  $this->seek($s);
1190
 
1191
  return false;
@@ -1209,22 +1501,26 @@ class Parser
1209
  */
1210
  protected function expression(&$out)
1211
  {
1212
- $s = $this->seek();
1213
-
1214
- if ($this->literal('(')) {
1215
- if ($this->literal(')')) {
1216
- $out = [Type::T_LIST, '', []];
1217
 
1218
- return true;
1219
- }
1220
-
1221
- if ($this->valueList($out) && $this->literal(')') && $out[0] === Type::T_LIST) {
1222
  return true;
1223
  }
1224
 
1225
  $this->seek($s);
 
1226
 
1227
- if ($this->map($out)) {
 
 
 
 
 
 
1228
  return true;
1229
  }
1230
 
@@ -1234,6 +1530,39 @@ class Parser
1234
  if ($this->value($lhs)) {
1235
  $out = $this->expHelper($lhs, 0);
1236
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1237
  return true;
1238
  }
1239
 
@@ -1252,7 +1581,7 @@ class Parser
1252
  {
1253
  $operators = static::$operatorPattern;
1254
 
1255
- $ss = $this->seek();
1256
  $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1257
  ctype_space($this->buffer[$this->count - 1]);
1258
 
@@ -1281,7 +1610,7 @@ class Parser
1281
  }
1282
 
1283
  $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
1284
- $ss = $this->seek();
1285
  $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1286
  ctype_space($this->buffer[$this->count - 1]);
1287
  }
@@ -1300,58 +1629,138 @@ class Parser
1300
  */
1301
  protected function value(&$out)
1302
  {
1303
- $s = $this->seek();
 
 
1304
 
1305
- if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) {
1306
- $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1307
 
1308
- return true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1309
  }
1310
 
1311
  $this->seek($s);
1312
 
1313
- if ($this->literal('not', false) && $this->parenValue($inner)) {
1314
- $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1315
 
1316
- return true;
 
 
 
 
 
1317
  }
1318
 
1319
  $this->seek($s);
1320
 
1321
- if ($this->literal('+') && $this->value($inner)) {
1322
- $out = [Type::T_UNARY, '+', $inner, $this->inParens];
 
 
1323
 
1324
- return true;
 
 
 
 
 
 
 
 
 
 
 
1325
  }
1326
 
1327
- $this->seek($s);
 
 
 
 
 
 
 
 
 
 
 
 
 
1328
 
1329
  // negation
1330
- if ($this->literal('-', false) &&
1331
- ($this->variable($inner) ||
1332
- $this->unit($inner) ||
1333
- $this->parenValue($inner))
1334
- ) {
1335
- $out = [Type::T_UNARY, '-', $inner, $this->inParens];
 
 
 
 
 
1336
 
 
 
1337
  return true;
1338
  }
1339
 
1340
- $this->seek($s);
 
 
 
 
1341
 
1342
- if ($this->parenValue($out) ||
1343
- $this->interpolation($out) ||
1344
- $this->variable($out) ||
1345
- $this->color($out) ||
1346
- $this->unit($out) ||
1347
- $this->string($out) ||
1348
- $this->func($out) ||
1349
- $this->progid($out)
1350
- ) {
 
 
 
 
 
 
1351
  return true;
1352
  }
1353
 
1354
- if ($this->keyword($keyword)) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1355
  if ($keyword === 'null') {
1356
  $out = [Type::T_NULL];
1357
  } else {
@@ -1373,12 +1782,12 @@ class Parser
1373
  */
1374
  protected function parenValue(&$out)
1375
  {
1376
- $s = $this->seek();
1377
 
1378
  $inParens = $this->inParens;
1379
 
1380
- if ($this->literal('(')) {
1381
- if ($this->literal(')')) {
1382
  $out = [Type::T_LIST, '', []];
1383
 
1384
  return true;
@@ -1386,7 +1795,7 @@ class Parser
1386
 
1387
  $this->inParens = true;
1388
 
1389
- if ($this->expression($exp) && $this->literal(')')) {
1390
  $out = $exp;
1391
  $this->inParens = $inParens;
1392
 
@@ -1409,15 +1818,15 @@ class Parser
1409
  */
1410
  protected function progid(&$out)
1411
  {
1412
- $s = $this->seek();
1413
 
1414
- if ($this->literal('progid:', false) &&
1415
  $this->openString('(', $fn) &&
1416
- $this->literal('(')
1417
  ) {
1418
  $this->openString(')', $args, '(');
1419
 
1420
- if ($this->literal(')')) {
1421
  $out = [Type::T_STRING, '', [
1422
  'progid:', $fn, '(', $args, ')'
1423
  ]];
@@ -1434,17 +1843,16 @@ class Parser
1434
  /**
1435
  * Parse function call
1436
  *
1437
- * @param array $out
 
1438
  *
1439
  * @return boolean
1440
  */
1441
- protected function func(&$func)
1442
  {
1443
- $s = $this->seek();
1444
 
1445
- if ($this->keyword($name, false) &&
1446
- $this->literal('(')
1447
- ) {
1448
  if ($name === 'alpha' && $this->argumentList($args)) {
1449
  $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]];
1450
 
@@ -1452,9 +1860,9 @@ class Parser
1452
  }
1453
 
1454
  if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
1455
- $ss = $this->seek();
1456
 
1457
- if ($this->argValues($args) && $this->literal(')')) {
1458
  $func = [Type::T_FUNCTION_CALL, $name, $args];
1459
 
1460
  return true;
@@ -1464,7 +1872,7 @@ class Parser
1464
  }
1465
 
1466
  if (($this->openString(')', $str, '(') || true) &&
1467
- $this->literal(')')
1468
  ) {
1469
  $args = [];
1470
 
@@ -1492,13 +1900,13 @@ class Parser
1492
  */
1493
  protected function argumentList(&$out)
1494
  {
1495
- $s = $this->seek();
1496
- $this->literal('(');
1497
 
1498
  $args = [];
1499
 
1500
  while ($this->keyword($var)) {
1501
- if ($this->literal('=') && $this->expression($exp)) {
1502
  $args[] = [Type::T_STRING, '', [$var . '=']];
1503
  $arg = $exp;
1504
  } else {
@@ -1507,14 +1915,14 @@ class Parser
1507
 
1508
  $args[] = $arg;
1509
 
1510
- if (! $this->literal(',')) {
1511
  break;
1512
  }
1513
 
1514
  $args[] = [Type::T_STRING, '', [', ']];
1515
  }
1516
 
1517
- if (! $this->literal(')') || ! count($args)) {
1518
  $this->seek($s);
1519
 
1520
  return false;
@@ -1534,28 +1942,28 @@ class Parser
1534
  */
1535
  protected function argumentDef(&$out)
1536
  {
1537
- $s = $this->seek();
1538
- $this->literal('(');
1539
 
1540
  $args = [];
1541
 
1542
  while ($this->variable($var)) {
1543
  $arg = [$var[1], null, false];
1544
 
1545
- $ss = $this->seek();
1546
 
1547
- if ($this->literal(':') && $this->genericList($defaultVal, 'expression')) {
1548
  $arg[1] = $defaultVal;
1549
  } else {
1550
  $this->seek($ss);
1551
  }
1552
 
1553
- $ss = $this->seek();
1554
 
1555
- if ($this->literal('...')) {
1556
- $sss = $this->seek();
1557
 
1558
- if (! $this->literal(')')) {
1559
  $this->throwParseError('... has to be after the final argument');
1560
  }
1561
 
@@ -1567,12 +1975,12 @@ class Parser
1567
 
1568
  $args[] = $arg;
1569
 
1570
- if (! $this->literal(',')) {
1571
  break;
1572
  }
1573
  }
1574
 
1575
- if (! $this->literal(')')) {
1576
  $this->seek($s);
1577
 
1578
  return false;
@@ -1592,27 +2000,27 @@ class Parser
1592
  */
1593
  protected function map(&$out)
1594
  {
1595
- $s = $this->seek();
1596
 
1597
- if (! $this->literal('(')) {
1598
  return false;
1599
  }
1600
 
1601
  $keys = [];
1602
  $values = [];
1603
 
1604
- while ($this->genericList($key, 'expression') && $this->literal(':') &&
1605
  $this->genericList($value, 'expression')
1606
  ) {
1607
  $keys[] = $key;
1608
  $values[] = $value;
1609
 
1610
- if (! $this->literal(',')) {
1611
  break;
1612
  }
1613
  }
1614
 
1615
- if (! count($keys) || ! $this->literal(')')) {
1616
  $this->seek($s);
1617
 
1618
  return false;
@@ -1633,22 +2041,48 @@ class Parser
1633
  protected function color(&$out)
1634
  {
1635
  $color = [Type::T_COLOR];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1636
 
1637
- if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
1638
- if (isset($m[3])) {
1639
- $num = hexdec($m[3]);
1640
 
1641
- foreach ([3, 2, 1] as $i) {
1642
- $t = $num & 0xf;
1643
- $color[$i] = $t << 4 | $t;
1644
- $num >>= 4;
1645
- }
1646
- } else {
1647
- $num = hexdec($m[2]);
 
 
 
 
 
 
 
 
 
1648
 
1649
- foreach ([3, 2, 1] as $i) {
1650
- $color[$i] = $num & 0xff;
1651
- $num >>= 8;
 
 
1652
  }
1653
  }
1654
 
@@ -1663,16 +2097,24 @@ class Parser
1663
  /**
1664
  * Parse number with unit
1665
  *
1666
- * @param array $out
1667
  *
1668
  * @return boolean
1669
  */
1670
  protected function unit(&$unit)
1671
  {
1672
- if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
1673
- $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
1674
 
1675
- return true;
 
 
 
 
 
 
 
 
 
1676
  }
1677
 
1678
  return false;
@@ -1687,11 +2129,11 @@ class Parser
1687
  */
1688
  protected function string(&$out)
1689
  {
1690
- $s = $this->seek();
1691
 
1692
- if ($this->literal('"', false)) {
1693
  $delim = '"';
1694
- } elseif ($this->literal("'", false)) {
1695
  $delim = "'";
1696
  } else {
1697
  return false;
@@ -1718,10 +2160,18 @@ class Parser
1718
  $content[] = '#{'; // ignore it
1719
  }
1720
  } elseif ($m[2] === '\\') {
1721
- if ($this->literal('"', false)) {
1722
  $content[] = $m[2] . '"';
1723
- } elseif ($this->literal("'", false)) {
1724
  $content[] = $m[2] . "'";
 
 
 
 
 
 
 
 
1725
  } else {
1726
  $content[] = $m[2];
1727
  }
@@ -1733,12 +2183,14 @@ class Parser
1733
 
1734
  $this->eatWhiteDefault = $oldWhite;
1735
 
1736
- if ($this->literal($delim)) {
1737
  if ($hasInterpolation) {
1738
  $delim = '"';
1739
 
1740
  foreach ($content as &$string) {
1741
- if ($string === "\\'") {
 
 
1742
  $string = "'";
1743
  } elseif ($string === '\\"') {
1744
  $string = '"';
@@ -1759,11 +2211,12 @@ class Parser
1759
  /**
1760
  * Parse keyword or interpolation
1761
  *
1762
- * @param array $out
 
1763
  *
1764
  * @return boolean
1765
  */
1766
- protected function mixedKeyword(&$out)
1767
  {
1768
  $parts = [];
1769
 
@@ -1771,7 +2224,7 @@ class Parser
1771
  $this->eatWhiteDefault = false;
1772
 
1773
  for (;;) {
1774
- if ($this->keyword($key)) {
1775
  $parts[] = $key;
1776
  continue;
1777
  }
@@ -1786,7 +2239,7 @@ class Parser
1786
 
1787
  $this->eatWhiteDefault = $oldWhite;
1788
 
1789
- if (count($parts) === 0) {
1790
  return false;
1791
  }
1792
 
@@ -1852,7 +2305,7 @@ class Parser
1852
 
1853
  $this->eatWhiteDefault = $oldWhite;
1854
 
1855
- if (count($content) === 0) {
1856
  return false;
1857
  }
1858
 
@@ -1879,17 +2332,22 @@ class Parser
1879
  $oldWhite = $this->eatWhiteDefault;
1880
  $this->eatWhiteDefault = true;
1881
 
1882
- $s = $this->seek();
1883
 
1884
- if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) {
1885
- if ($lookWhite) {
1886
- $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
1887
- $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
1888
  } else {
1889
- $left = $right = false;
 
 
 
 
 
 
 
1890
  }
1891
 
1892
- $out = [Type::T_INTERPOLATE, $value, $left, $right];
1893
  $this->eatWhiteDefault = $oldWhite;
1894
 
1895
  if ($this->eatWhiteDefault) {
@@ -1900,6 +2358,7 @@ class Parser
1900
  }
1901
 
1902
  $this->seek($s);
 
1903
  $this->eatWhiteDefault = $oldWhite;
1904
 
1905
  return false;
@@ -1930,7 +2389,7 @@ class Parser
1930
  continue;
1931
  }
1932
 
1933
- if (count($parts) === 0 && $this->match('[:.#]', $m, false)) {
1934
  // css hacks
1935
  $parts[] = $m[0];
1936
  continue;
@@ -1941,7 +2400,7 @@ class Parser
1941
 
1942
  $this->eatWhiteDefault = $oldWhite;
1943
 
1944
- if (count($parts) === 0) {
1945
  return false;
1946
  }
1947
 
@@ -1973,24 +2432,24 @@ class Parser
1973
  *
1974
  * @return boolean
1975
  */
1976
- protected function selectors(&$out)
1977
  {
1978
- $s = $this->seek();
1979
  $selectors = [];
1980
 
1981
- while ($this->selector($sel)) {
1982
  $selectors[] = $sel;
1983
 
1984
- if (! $this->literal(',')) {
1985
  break;
1986
  }
1987
 
1988
- while ($this->literal(',')) {
1989
  ; // ignore extra
1990
  }
1991
  }
1992
 
1993
- if (count($selectors) === 0) {
1994
  $this->seek($s);
1995
 
1996
  return false;
@@ -2008,23 +2467,23 @@ class Parser
2008
  *
2009
  * @return boolean
2010
  */
2011
- protected function selector(&$out)
2012
  {
2013
  $selector = [];
2014
 
2015
  for (;;) {
2016
- if ($this->match('[>+~]+', $m)) {
2017
  $selector[] = [$m[0]];
2018
  continue;
2019
  }
2020
 
2021
- if ($this->selectorSingle($part)) {
2022
  $selector[] = $part;
2023
  $this->match('\s+', $m);
2024
  continue;
2025
  }
2026
 
2027
- if ($this->match('\/[^\/]+\/', $m)) {
2028
  $selector[] = [$m[0]];
2029
  continue;
2030
  }
@@ -2032,11 +2491,12 @@ class Parser
2032
  break;
2033
  }
2034
 
2035
- if (count($selector) === 0) {
2036
  return false;
2037
  }
2038
 
2039
  $out = $selector;
 
2040
  return true;
2041
  }
2042
 
@@ -2051,108 +2511,155 @@ class Parser
2051
  *
2052
  * @return boolean
2053
  */
2054
- protected function selectorSingle(&$out)
2055
  {
2056
  $oldWhite = $this->eatWhiteDefault;
2057
  $this->eatWhiteDefault = false;
2058
 
2059
  $parts = [];
2060
 
2061
- if ($this->literal('*', false)) {
2062
  $parts[] = '*';
2063
  }
2064
 
2065
  for (;;) {
2066
- // see if we can stop early
2067
- if ($this->match('\s*[{,]', $m)) {
2068
- $this->count--;
2069
  break;
2070
  }
2071
 
2072
- $s = $this->seek();
 
2073
 
2074
- // self
2075
- if ($this->literal('&', false)) {
2076
- $parts[] = Compiler::$selfSelector;
2077
- continue;
2078
  }
2079
 
2080
- if ($this->literal('.', false)) {
2081
- $parts[] = '.';
2082
- continue;
2083
  }
2084
 
2085
- if ($this->literal('|', false)) {
2086
- $parts[] = '|';
2087
- continue;
 
 
 
 
 
 
 
 
 
 
 
 
 
2088
  }
2089
 
2090
- if ($this->match('\\\\\S', $m)) {
2091
  $parts[] = $m[0];
2092
  continue;
2093
  }
2094
 
2095
- // for keyframes
2096
- if ($this->unit($unit)) {
2097
- $parts[] = $unit;
2098
- continue;
2099
- }
2100
 
2101
- if ($this->keyword($name)) {
2102
- $parts[] = $name;
2103
- continue;
2104
- }
 
2105
 
2106
- if ($this->interpolation($inter)) {
2107
- $parts[] = $inter;
2108
- continue;
2109
  }
2110
 
2111
- if ($this->literal('%', false) && $this->placeholder($placeholder)) {
2112
- $parts[] = '%';
2113
- $parts[] = $placeholder;
2114
- continue;
2115
- }
2116
 
2117
- if ($this->literal('#', false)) {
2118
  $parts[] = '#';
 
2119
  continue;
2120
  }
2121
 
2122
  // a pseudo selector
2123
- if ($this->match('::?', $m) && $this->mixedKeyword($nameParts)) {
2124
- $parts[] = $m[0];
2125
-
2126
- foreach ($nameParts as $sub) {
2127
- $parts[] = $sub;
 
 
2128
  }
2129
 
2130
- $ss = $this->seek();
 
2131
 
2132
- if ($this->literal('(') &&
2133
- ($this->openString(')', $str, '(') || true) &&
2134
- $this->literal(')')
2135
- ) {
2136
- $parts[] = '(';
2137
 
2138
- if (! empty($str)) {
2139
- $parts[] = $str;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2140
  }
2141
 
2142
- $parts[] = ')';
2143
- } else {
2144
- $this->seek($ss);
2145
  }
2146
-
2147
- continue;
2148
  }
2149
 
2150
  $this->seek($s);
2151
 
2152
  // attribute selector
2153
- if ($this->literal('[') &&
2154
- ($this->openString(']', $str, '[') || true) &&
2155
- $this->literal(']')
 
2156
  ) {
2157
  $parts[] = '[';
2158
 
@@ -2161,18 +2668,28 @@ class Parser
2161
  }
2162
 
2163
  $parts[] = ']';
2164
-
2165
  continue;
2166
  }
2167
 
2168
  $this->seek($s);
2169
 
 
 
 
 
 
 
 
 
 
 
 
2170
  break;
2171
  }
2172
 
2173
  $this->eatWhiteDefault = $oldWhite;
2174
 
2175
- if (count($parts) === 0) {
2176
  return false;
2177
  }
2178
 
@@ -2190,9 +2707,9 @@ class Parser
2190
  */
2191
  protected function variable(&$out)
2192
  {
2193
- $s = $this->seek();
2194
 
2195
- if ($this->literal('$', false) && $this->keyword($name)) {
2196
  $out = [Type::T_VARIABLE, $name];
2197
 
2198
  return true;
@@ -2215,7 +2732,7 @@ class Parser
2215
  {
2216
  if ($this->match(
2217
  $this->utf8
2218
- ? '(([\pL\w_\-\*!"\']|[\\\\].)([\pL\w\-_"\']|[\\\\].)*)'
2219
  : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
2220
  $m,
2221
  $eatWhitespace
@@ -2228,6 +2745,27 @@ class Parser
2228
  return false;
2229
  }
2230
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2231
  /**
2232
  * Parse a placeholder
2233
  *
@@ -2239,14 +2777,17 @@ class Parser
2239
  {
2240
  if ($this->match(
2241
  $this->utf8
2242
- ? '([\pL\w\-_]+|#[{][$][\pL\w\-_]+[}])'
2243
- : '([\w\-_]+|#[{][$][\w\-_]+[}])',
2244
  $m
2245
  )) {
2246
  $placeholder = $m[1];
2247
 
2248
  return true;
2249
  }
 
 
 
2250
 
2251
  return false;
2252
  }
@@ -2276,7 +2817,7 @@ class Parser
2276
  */
2277
  protected function end()
2278
  {
2279
- if ($this->literal(';')) {
2280
  return true;
2281
  }
2282
 
@@ -2438,7 +2979,7 @@ class Parser
2438
  *
2439
  * @param integer $pos
2440
  *
2441
- * @return integer
2442
  */
2443
  private function getSourcePosition($pos)
2444
  {
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
 
14
+ use ScssPhp\ScssPhp\Block;
15
+ use ScssPhp\ScssPhp\Cache;
16
+ use ScssPhp\ScssPhp\Compiler;
17
+ use ScssPhp\ScssPhp\Exception\ParserException;
18
+ use ScssPhp\ScssPhp\Node;
19
+ use ScssPhp\ScssPhp\Type;
20
 
21
  /**
22
  * Parser
54
  protected static $operatorPattern;
55
  protected static $whitePattern;
56
 
57
+ protected $cache;
58
+
59
  private $sourceName;
60
  private $sourceIndex;
61
  private $sourcePositions;
64
  private $env;
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
  *
77
  * @api
78
  *
79
+ * @param string $sourceName
80
+ * @param integer $sourceIndex
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;
88
  $this->charset = null;
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)';
103
  ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS'
104
  : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS';
105
  }
106
+
107
+ if ($cache) {
108
+ $this->cache = $cache;
109
+ }
110
  }
111
 
112
  /**
128
  *
129
  * @param string $msg
130
  *
131
+ * @throws \ScssPhp\ScssPhp\Exception\ParserException
132
  */
133
  public function throwParseError($msg = 'parse error')
134
  {
135
+ list($line, $column) = $this->getSourcePosition($this->count);
136
 
137
+ $loc = empty($this->sourceName)
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");
152
  *
153
  * @param string $buffer
154
  *
155
+ * @return \ScssPhp\ScssPhp\Block
156
  */
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
+ }
171
+
172
  // strip BOM (byte order marker)
173
  if (substr($buffer, 0, 3) === "\xef\xbb\xbf") {
174
  $buffer = substr($buffer, 3);
204
  array_unshift($this->env->children, $this->charset);
205
  }
206
 
 
 
207
  $this->restoreEncoding();
208
 
209
+ if ($this->cache) {
210
+ $this->cache->setCache("parse", $cacheKey, $this->env, $parseOptions);
211
+ }
212
+
213
  return $this->env;
214
  }
215
 
267
  return $selector;
268
  }
269
 
270
+ /**
271
+ * Parse a media Query
272
+ *
273
+ * @api
274
+ *
275
+ * @param string $buffer
276
+ * @param string $out
277
+ *
278
+ * @return array
279
+ */
280
+ public function parseMediaQueryList($buffer, &$out)
281
+ {
282
+ $this->count = 0;
283
+ $this->env = null;
284
+ $this->inParens = false;
285
+ $this->eatWhiteDefault = true;
286
+ $this->buffer = (string) $buffer;
287
+
288
+ $this->saveEncoding();
289
+
290
+
291
+ $isMediaQuery = $this->mediaQueryList($out);
292
+
293
+ $this->restoreEncoding();
294
+
295
+ return $isMediaQuery;
296
+ }
297
+
298
  /**
299
  * Parse a single chunk off the head of the buffer and append it to the
300
  * current parse environment.
328
  * the buffer position will be left at an invalid state. In order to
329
  * avoid this, Compiler::seek() is used to remember and set buffer positions.
330
  *
331
+ * Before parsing a chain, use $s = $this->count to remember the current
332
  * position into $s. Then if a chain fails, use $this->seek($s) to
333
  * go back where we started.
334
  *
336
  */
337
  protected function parseChunk()
338
  {
339
+ $s = $this->count;
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;
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
 
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;
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
 
399
 
400
  $this->seek($s);
401
 
402
+ if ($this->literal('@scssphp-import-once', 20) &&
403
  $this->valueList($importPath) &&
404
  $this->end()
405
  ) {
410
 
411
  $this->seek($s);
412
 
413
+ if ($this->literal('@import', 7) &&
414
  $this->valueList($importPath) &&
415
  $this->end()
416
  ) {
421
 
422
  $this->seek($s);
423
 
424
+ if ($this->literal('@import', 7) &&
425
  $this->url($importPath) &&
426
  $this->end()
427
  ) {
432
 
433
  $this->seek($s);
434
 
435
+ if ($this->literal('@extend', 7) &&
436
  $this->selectors($selectors) &&
437
  $this->end()
438
  ) {
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;
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
 
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
 
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
 
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
 
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;
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) &&
521
+ ($this->literal('through', 7) ||
522
+ ($forUntil = true && $this->literal('to', 2))) &&
523
  $this->expression($end) &&
524
+ $this->matchChar('{', false)
525
  ) {
526
  $for = $this->pushSpecialBlock(Type::T_FOR, $s);
527
  $for->var = $varName[1];
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 = [];
544
 
545
  $this->seek($s);
546
 
547
+ if ($this->literal('@debug', 6) &&
548
  $this->valueList($value) &&
549
  $this->end()
550
  ) {
555
 
556
  $this->seek($s);
557
 
558
+ if ($this->literal('@warn', 5) &&
559
  $this->valueList($value) &&
560
  $this->end()
561
  ) {
566
 
567
  $this->seek($s);
568
 
569
+ if ($this->literal('@error', 6) &&
570
  $this->valueList($value) &&
571
  $this->end()
572
  ) {
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;
590
  if (isset($last) && $last[0] === Type::T_IF) {
591
  list(, $if) = $last;
592
 
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
  }
611
 
612
  // only retain the first @charset directive encountered
613
+ if ($this->literal('@charset', 8) &&
614
  $this->valueList($charset) &&
615
  $this->end()
616
  ) {
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;
642
+ }
643
+
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);
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()
677
  ) {
685
 
686
  // variable assigns
687
  if ($this->variable($name) &&
688
+ $this->matchChar(':') &&
689
  $this->valueList($value) &&
690
  $this->end()
691
  ) {
699
  $this->seek($s);
700
 
701
  // misc
702
+ if ($this->literal('-->', 3)) {
703
  return true;
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;
716
  }
717
 
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)) {
725
+ if (empty($this->env->parent)) {
726
+ $this->throwParseError('expected "{"');
727
+ }
728
+
729
  $this->append([Type::T_ASSIGN, $name, $value], $s);
730
  $foundSomething = true;
731
  }
732
 
733
+ if ($this->matchChar('{', false)) {
734
  $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
735
  $propBlock->prefix = $name;
736
+ $propBlock->hasValue = $foundSomething;
737
+
738
  $foundSomething = true;
739
  } elseif ($foundSomething) {
740
  $foundSomething = $this->end();
748
  $this->seek($s);
749
 
750
  // closing a block
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
+ }
758
+ }
759
+
760
  if (isset($block->type) && $block->type === Type::T_INCLUDE) {
761
  $include = $block->child;
762
  unset($block->child);
767
  $this->append([$type, $block], $s);
768
  }
769
 
770
+ // collect comments just after the block closing if needed
771
+ if ($this->eatWhiteDefault) {
772
+ $this->whitespace();
773
+
774
+ if ($this->env->comments) {
775
+ $this->append(null);
776
+ }
777
+ }
778
+
779
  return true;
780
  }
781
 
782
  // extra stuff
783
+ if ($this->matchChar(';') ||
784
+ $this->literal('<!--', 4)
785
  ) {
786
  return true;
787
  }
795
  * @param array $selectors
796
  * @param integer $pos
797
  *
798
+ * @return \ScssPhp\ScssPhp\Block
799
  */
800
  protected function pushBlock($selectors, $pos = 0)
801
  {
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
+
830
+ if ($this->env->comments) {
831
+ $this->append(null);
832
+ }
833
+ }
834
+
835
  return $b;
836
  }
837
 
841
  * @param string $type
842
  * @param integer $pos
843
  *
844
+ * @return \ScssPhp\ScssPhp\Block
845
  */
846
  protected function pushSpecialBlock($type, $pos)
847
  {
854
  /**
855
  * Pop scope and return last block
856
  *
857
+ * @return \ScssPhp\ScssPhp\Block
858
  *
859
  * @throws \Exception
860
  */
861
  protected function popBlock()
862
  {
863
+
864
+ // collect comments ending just before of a block closing
865
+ if ($this->env->comments) {
866
+ $this->append(null);
867
+ }
868
+
869
+ // pop the block
870
  $block = $this->env;
871
 
872
  if (empty($block->parent)) {
873
  $this->throwParseError('unexpected }');
874
  }
875
 
876
+ if ($block->type == Type::T_AT_ROOT) {
877
+ // keeps the parent in case of self selector &
878
+ $block->selfParent = $block->parent;
879
+ }
880
+
881
  $this->env = $block->parent;
 
882
 
883
+ unset($block->parent);
 
 
 
 
884
 
885
  return $block;
886
  }
910
  * Seek to position in input stream (or return current position in input stream)
911
  *
912
  * @param integer $where
 
 
913
  */
914
+ protected function seek($where)
915
  {
 
 
 
 
916
  $this->count = $where;
 
 
917
  }
918
 
919
  /**
968
  */
969
  protected function match($regex, &$out, $eatWhitespace = null)
970
  {
971
+ $r = '/' . $regex . '/' . $this->patternModifiers;
972
+
973
+ if (! preg_match($r, $this->buffer, $out, null, $this->count)) {
974
+ return false;
975
+ }
976
+
977
+ $this->count += strlen($out[0]);
978
+
979
  if (! isset($eatWhitespace)) {
980
  $eatWhitespace = $this->eatWhiteDefault;
981
  }
982
 
983
+ if ($eatWhitespace) {
984
+ $this->whitespace();
985
+ }
986
 
987
+ return true;
988
+ }
989
 
990
+ /**
991
+ * Match a single string
992
+ *
993
+ * @param string $char
994
+ * @param boolean $eatWhitespace
995
+ *
996
+ * @return boolean
997
+ */
998
+ protected function matchChar($char, $eatWhitespace = null)
999
+ {
1000
+ if (! isset($this->buffer[$this->count]) || $this->buffer[$this->count] !== $char) {
1001
+ return false;
1002
+ }
1003
 
1004
+ $this->count++;
1005
+
1006
+ if (! isset($eatWhitespace)) {
1007
+ $eatWhitespace = $this->eatWhiteDefault;
1008
  }
1009
 
1010
+ if ($eatWhitespace) {
1011
+ $this->whitespace();
1012
+ }
1013
+
1014
+ return true;
1015
  }
1016
 
1017
  /**
1018
  * Match literal string
1019
  *
1020
  * @param string $what
1021
+ * @param integer $len
1022
  * @param boolean $eatWhitespace
1023
  *
1024
  * @return boolean
1025
  */
1026
+ protected function literal($what, $len, $eatWhitespace = null)
1027
  {
1028
+ if (strcasecmp(substr($this->buffer, $this->count, $len), $what) !== 0) {
1029
+ return false;
1030
  }
1031
 
1032
+ $this->count += $len;
 
 
 
1033
 
1034
+ if (! isset($eatWhitespace)) {
1035
+ $eatWhitespace = $this->eatWhiteDefault;
1036
+ }
1037
 
1038
+ if ($eatWhitespace) {
1039
+ $this->whitespace();
1040
  }
1041
 
1042
+ return true;
1043
  }
1044
 
1045
  /**
1053
 
1054
  while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) {
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
+
1083
+ $this->count += 2;
1084
+ }
1085
+
1086
+ $p = strpos($this->buffer, '#{', $this->count);
1087
+ }
1088
+
1089
+ // remaining part
1090
+ $c = substr($this->buffer, $this->count, $endCommentCount - $this->count);
1091
+
1092
+ if (! $comment) {
1093
+ // single part static comment
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;
1108
  }
1109
 
1117
  */
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
  }
1128
 
1129
  /**
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;
1142
+ $statement[static::SOURCE_COLUMN] = $column;
1143
+ $statement[static::SOURCE_INDEX] = $this->sourceIndex;
1144
+ }
1145
 
1146
+ $this->env->children[] = $statement;
1147
+ }
1148
 
1149
  $comments = $this->env->comments;
1150
 
1151
+ if ($comments) {
1152
  $this->env->children = array_merge($this->env->children, $comments);
1153
  $this->env->comments = [];
1154
  }
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];
1219
  $parts[] = $prop;
1220
  }
1221
 
1222
+ if (empty($parts) || $this->literal('and', 3)) {
1223
  $this->genericList($expressions, 'mediaExpression', 'and', false);
1224
 
1225
  if (is_array($expressions)) {
1232
  return true;
1233
  }
1234
 
1235
+ /**
1236
+ * Parse supports query
1237
+ *
1238
+ * @param array $out
1239
+ *
1240
+ * @return boolean
1241
+ */
1242
+ protected function supportsQuery(&$out)
1243
+ {
1244
+ $expressions = null;
1245
+ $parts = [];
1246
+
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, ': '];
1259
+ $support[2][] = $value;
1260
+ $support[2][] = [Type::T_KEYWORD, ')'];
1261
+
1262
+ $parts[] = $support;
1263
+ $s = $this->count;
1264
+ } else {
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;
1305
+ $s = $this->count;
1306
+ } else {
1307
+ $this->seek($s);
1308
+ }
1309
+
1310
+ if ($this->variable($var) or $this->interpolation($var)) {
1311
+ $parts[] = $var;
1312
+ $s = $this->count;
1313
+ } else {
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
+
1343
+ return false;
1344
+ }
1345
+
1346
+
1347
  /**
1348
  * Parse media expression
1349
  *
1353
  */
1354
  protected function mediaExpression(&$out)
1355
  {
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];
1365
 
1402
  */
1403
  protected function argValue(&$out)
1404
  {
1405
+ $s = $this->count;
1406
 
1407
  $keyword = null;
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
 
1418
+ if ($this->literal('...', 3)) {
1419
  $out[2] = true;
1420
  } else {
1421
  $this->seek($s);
1430
  /**
1431
  * Parse comma separated value list
1432
  *
1433
+ * @param array $out
1434
  *
1435
  * @return boolean
1436
  */
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;
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
 
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
+
1541
+ /**
1542
+ * Parse expression specifically checking for lists in parenthesis or brackets
1543
+ *
1544
+ * @param array $out
1545
+ * @param integer $s
1546
+ * @param string $closingParen
1547
+ * @param array $allowedTypes
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
 
1581
  {
1582
  $operators = static::$operatorPattern;
1583
 
1584
+ $ss = $this->count;
1585
  $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1586
  ctype_space($this->buffer[$this->count - 1]);
1587
 
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]);
1616
  }
1629
  */
1630
  protected function value(&$out)
1631
  {
1632
+ if (! isset($this->buffer[$this->count])) {
1633
+ return false;
1634
+ }
1635
 
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+/=',
1643
+ $this->count
1644
+ );
1645
+
1646
+ $this->count += $len;
1647
+
1648
+ if ($this->matchChar(')')) {
1649
+ $content = substr($this->buffer, $s, $this->count - $s);
1650
+ $out = [Type::T_KEYWORD, $content];
1651
+
1652
+ return true;
1653
+ }
1654
  }
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(')')) {
1662
+ $content .= ')';
1663
+ $out = [Type::T_KEYWORD, $content];
1664
+
1665
+ return true;
1666
+ }
1667
  }
1668
 
1669
  $this->seek($s);
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;
1677
+ }
1678
+
1679
+ $this->seek($s);
1680
+
1681
+ if ($this->parenValue($inner)) {
1682
+ $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1683
+
1684
+ return true;
1685
+ }
1686
+
1687
+ $this->seek($s);
1688
  }
1689
 
1690
+ // addition
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
1719
+ if ($char === '(' && $this->parenValue($out)) {
1720
  return true;
1721
  }
1722
 
1723
+ if ($char === '#') {
1724
+ if ($this->interpolation($out) || $this->color($out)) {
1725
+ return true;
1726
+ }
1727
+ }
1728
 
1729
+ if ($this->matchChar('&', true)) {
1730
+ $out = [Type::T_SELF];
1731
+
1732
+ return true;
1733
+ }
1734
+
1735
+ if ($char === '$' && $this->variable($out)) {
1736
+ return true;
1737
+ }
1738
+
1739
+ if ($char === 'p' && $this->progid($out)) {
1740
+ return true;
1741
+ }
1742
+
1743
+ if (($char === '"' || $char === "'") && $this->string($out)) {
1744
  return true;
1745
  }
1746
 
1747
+ if ($this->unit($out)) {
1748
+ return true;
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)) {
1758
+ if ($this->func($keyword, $out)) {
1759
+ return true;
1760
+ }
1761
+
1762
+ $this->whitespace();
1763
+
1764
  if ($keyword === 'null') {
1765
  $out = [Type::T_NULL];
1766
  } else {
1782
  */
1783
  protected function parenValue(&$out)
1784
  {
1785
+ $s = $this->count;
1786
 
1787
  $inParens = $this->inParens;
1788
 
1789
+ if ($this->matchChar('(')) {
1790
+ if ($this->matchChar(')')) {
1791
  $out = [Type::T_LIST, '', []];
1792
 
1793
  return true;
1795
 
1796
  $this->inParens = true;
1797
 
1798
+ if ($this->expression($exp) && $this->matchChar(')')) {
1799
  $out = $exp;
1800
  $this->inParens = $inParens;
1801
 
1818
  */
1819
  protected function progid(&$out)
1820
  {
1821
+ $s = $this->count;
1822
 
1823
+ if ($this->literal('progid:', 7, false) &&
1824
  $this->openString('(', $fn) &&
1825
+ $this->matchChar('(')
1826
  ) {
1827
  $this->openString(')', $args, '(');
1828
 
1829
+ if ($this->matchChar(')')) {
1830
  $out = [Type::T_STRING, '', [
1831
  'progid:', $fn, '(', $args, ')'
1832
  ]];
1843
  /**
1844
  * Parse function call
1845
  *
1846
+ * @param string $name
1847
+ * @param array $func
1848
  *
1849
  * @return boolean
1850
  */
1851
+ protected function func($name, &$func)
1852
  {
1853
+ $s = $this->count;
1854
 
1855
+ if ($this->matchChar('(')) {
 
 
1856
  if ($name === 'alpha' && $this->argumentList($args)) {
1857
  $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]];
1858
 
1860
  }
1861
 
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;
1872
  }
1873
 
1874
  if (($this->openString(')', $str, '(') || true) &&
1875
+ $this->matchChar(')')
1876
  ) {
1877
  $args = [];
1878
 
1900
  */
1901
  protected function argumentList(&$out)
1902
  {
1903
+ $s = $this->count;
1904
+ $this->matchChar('(');
1905
 
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 {
1915
 
1916
  $args[] = $arg;
1917
 
1918
+ if (! $this->matchChar(',')) {
1919
  break;
1920
  }
1921
 
1922
  $args[] = [Type::T_STRING, '', [', ']];
1923
  }
1924
 
1925
+ if (! $this->matchChar(')') || ! $args) {
1926
  $this->seek($s);
1927
 
1928
  return false;
1942
  */
1943
  protected function argumentDef(&$out)
1944
  {
1945
+ $s = $this->count;
1946
+ $this->matchChar('(');
1947
 
1948
  $args = [];
1949
 
1950
  while ($this->variable($var)) {
1951
  $arg = [$var[1], null, false];
1952
 
1953
+ $ss = $this->count;
1954
 
1955
+ if ($this->matchChar(':') && $this->genericList($defaultVal, 'expression')) {
1956
  $arg[1] = $defaultVal;
1957
  } else {
1958
  $this->seek($ss);
1959
  }
1960
 
1961
+ $ss = $this->count;
1962
 
1963
+ if ($this->literal('...', 3)) {
1964
+ $sss = $this->count;
1965
 
1966
+ if (! $this->matchChar(')')) {
1967
  $this->throwParseError('... has to be after the final argument');
1968
  }
1969
 
1975
 
1976
  $args[] = $arg;
1977
 
1978
+ if (! $this->matchChar(',')) {
1979
  break;
1980
  }
1981
  }
1982
 
1983
+ if (! $this->matchChar(')')) {
1984
  $this->seek($s);
1985
 
1986
  return false;
2000
  */
2001
  protected function map(&$out)
2002
  {
2003
+ $s = $this->count;
2004
 
2005
+ if (! $this->matchChar('(')) {
2006
  return false;
2007
  }
2008
 
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;
2017
 
2018
+ if (! $this->matchChar(',')) {
2019
  break;
2020
  }
2021
  }
2022
 
2023
+ if (! $keys || ! $this->matchChar(')')) {
2024
  $this->seek($s);
2025
 
2026
  return false;
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
 
2097
  /**
2098
  * Parse number with unit
2099
  *
2100
+ * @param array $unit
2101
  *
2102
  * @return boolean
2103
  */
2104
  protected function unit(&$unit)
2105
  {
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]);
2113
+
2114
+ return true;
2115
+ }
2116
+
2117
+ $this->seek($s);
2118
  }
2119
 
2120
  return false;
2129
  */
2130
  protected function string(&$out)
2131
  {
2132
+ $s = $this->count;
2133
 
2134
+ if ($this->matchChar('"', false)) {
2135
  $delim = '"';
2136
+ } elseif ($this->matchChar("'", false)) {
2137
  $delim = "'";
2138
  } else {
2139
  return false;
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
  }
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 = '"';
2211
  /**
2212
  * Parse keyword or interpolation
2213
  *
2214
+ * @param array $out
2215
+ * @param boolean $restricted
2216
  *
2217
  * @return boolean
2218
  */
2219
+ protected function mixedKeyword(&$out, $restricted = false)
2220
  {
2221
  $parts = [];
2222
 
2224
  $this->eatWhiteDefault = false;
2225
 
2226
  for (;;) {
2227
+ if ($restricted ? $this->restrictedKeyword($key) : $this->keyword($key)) {
2228
  $parts[] = $key;
2229
  continue;
2230
  }
2239
 
2240
  $this->eatWhiteDefault = $oldWhite;
2241
 
2242
+ if (! $parts) {
2243
  return false;
2244
  }
2245
 
2305
 
2306
  $this->eatWhiteDefault = $oldWhite;
2307
 
2308
+ if (! $content) {
2309
  return false;
2310
  }
2311
 
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
+ }
2347
+
2348
+ $out = [Type::T_INTERPOLATE, $value, $left, $right];
2349
  }
2350
 
 
2351
  $this->eatWhiteDefault = $oldWhite;
2352
 
2353
  if ($this->eatWhiteDefault) {
2358
  }
2359
 
2360
  $this->seek($s);
2361
+
2362
  $this->eatWhiteDefault = $oldWhite;
2363
 
2364
  return false;
2389
  continue;
2390
  }
2391
 
2392
+ if (! $parts && $this->match('[:.#]', $m, false)) {
2393
  // css hacks
2394
  $parts[] = $m[0];
2395
  continue;
2400
 
2401
  $this->eatWhiteDefault = $oldWhite;
2402
 
2403
+ if (! $parts) {
2404
  return false;
2405
  }
2406
 
2432
  *
2433
  * @return boolean
2434
  */
2435
+ protected function selectors(&$out, $subSelector = false)
2436
  {
2437
+ $s = $this->count;
2438
  $selectors = [];
2439
 
2440
+ while ($this->selector($sel, $subSelector)) {
2441
  $selectors[] = $sel;
2442
 
2443
+ if (! $this->matchChar(',', true)) {
2444
  break;
2445
  }
2446
 
2447
+ while ($this->matchChar(',', true)) {
2448
  ; // ignore extra
2449
  }
2450
  }
2451
 
2452
+ if (! $selectors) {
2453
  $this->seek($s);
2454
 
2455
  return false;
2467
  *
2468
  * @return boolean
2469
  */
2470
+ protected function selector(&$out, $subSelector = false)
2471
  {
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)) {
2481
  $selector[] = $part;
2482
  $this->match('\s+', $m);
2483
  continue;
2484
  }
2485
 
2486
+ if ($this->match('\/[^\/]+\/', $m, true)) {
2487
  $selector[] = [$m[0]];
2488
  continue;
2489
  }
2491
  break;
2492
  }
2493
 
2494
+ if (! $selector) {
2495
  return false;
2496
  }
2497
 
2498
  $out = $selector;
2499
+
2500
  return true;
2501
  }
2502
 
2511
  *
2512
  * @return boolean
2513
  */
2514
+ protected function selectorSingle(&$out, $subSelector = false)
2515
  {
2516
  $oldWhite = $this->eatWhiteDefault;
2517
  $this->eatWhiteDefault = false;
2518
 
2519
  $parts = [];
2520
 
2521
+ if ($this->matchChar('*', false)) {
2522
  $parts[] = '*';
2523
  }
2524
 
2525
  for (;;) {
2526
+ if (! isset($this->buffer[$this->count])) {
 
 
2527
  break;
2528
  }
2529
 
2530
+ $s = $this->count;
2531
+ $char = $this->buffer[$this->count];
2532
 
2533
+ // see if we can stop early
2534
+ if ($char === '{' || $char === ',' || $char === ';' || $char === '}' || $char === '@') {
2535
+ break;
 
2536
  }
2537
 
2538
+ // parsing a sub selector in () stop with the closing )
2539
+ if ($subSelector && $char === ')') {
2540
+ break;
2541
  }
2542
 
2543
+ //self
2544
+ switch ($char) {
2545
+ case '&':
2546
+ $parts[] = Compiler::$selfSelector;
2547
+ $this->count++;
2548
+ continue 2;
2549
+
2550
+ case '.':
2551
+ $parts[] = '.';
2552
+ $this->count++;
2553
+ continue 2;
2554
+
2555
+ case '|':
2556
+ $parts[] = '|';
2557
+ $this->count++;
2558
+ continue 2;
2559
  }
2560
 
2561
+ if ($char === '\\' && $this->match('\\\\\S', $m)) {
2562
  $parts[] = $m[0];
2563
  continue;
2564
  }
2565
 
2566
+ if ($char === '%') {
2567
+ $this->count++;
 
 
 
2568
 
2569
+ if ($this->placeholder($placeholder)) {
2570
+ $parts[] = '%';
2571
+ $parts[] = $placeholder;
2572
+ continue;
2573
+ }
2574
 
2575
+ break;
 
 
2576
  }
2577
 
2578
+ if ($char === '#') {
2579
+ if ($this->interpolation($inter)) {
2580
+ $parts[] = $inter;
2581
+ continue;
2582
+ }
2583
 
 
2584
  $parts[] = '#';
2585
+ $this->count++;
2586
  continue;
2587
  }
2588
 
2589
  // a pseudo selector
2590
+ if ($char === ':') {
2591
+ if ($this->buffer[$this->count + 1] === ':') {
2592
+ $this->count += 2;
2593
+ $part = '::';
2594
+ } else {
2595
+ $this->count++;
2596
+ $part = ':';
2597
  }
2598
 
2599
+ if ($this->mixedKeyword($nameParts, true)) {
2600
+ $parts[] = $part;
2601
 
2602
+ foreach ($nameParts as $sub) {
2603
+ $parts[] = $sub;
2604
+ }
 
 
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
+
2617
+ while ($sub = array_shift($subs)) {
2618
+ while ($ps = array_shift($sub)) {
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
+ }
2630
+
2631
+ $parts[] = ')';
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;
 
 
2653
  }
 
 
2654
  }
2655
 
2656
  $this->seek($s);
2657
 
2658
  // attribute selector
2659
+ if ($char === '[' &&
2660
+ $this->matchChar('[') &&
2661
+ ($this->openString(']', $str, '[') || true) &&
2662
+ $this->matchChar(']')
2663
  ) {
2664
  $parts[] = '[';
2665
 
2668
  }
2669
 
2670
  $parts[] = ']';
 
2671
  continue;
2672
  }
2673
 
2674
  $this->seek($s);
2675
 
2676
+ // for keyframes
2677
+ if ($this->unit($unit)) {
2678
+ $parts[] = $unit;
2679
+ continue;
2680
+ }
2681
+
2682
+ if ($this->restrictedKeyword($name)) {
2683
+ $parts[] = $name;
2684
+ continue;
2685
+ }
2686
+
2687
  break;
2688
  }
2689
 
2690
  $this->eatWhiteDefault = $oldWhite;
2691
 
2692
+ if (! $parts) {
2693
  return false;
2694
  }
2695
 
2707
  */
2708
  protected function variable(&$out)
2709
  {
2710
+ $s = $this->count;
2711
 
2712
+ if ($this->matchChar('$', false) && $this->keyword($name)) {
2713
  $out = [Type::T_VARIABLE, $name];
2714
 
2715
  return true;
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
2745
  return false;
2746
  }
2747
 
2748
+ /**
2749
+ * Parse a keyword that should not start with a number
2750
+ *
2751
+ * @param string $word
2752
+ * @param boolean $eatWhitespace
2753
+ *
2754
+ * @return boolean
2755
+ */
2756
+ protected function restrictedKeyword(&$word, $eatWhitespace = null)
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
+
2764
+ $this->seek($s);
2765
+
2766
+ return false;
2767
+ }
2768
+
2769
  /**
2770
  * Parse a placeholder
2771
  *
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
+ }
2791
 
2792
  return false;
2793
  }
2817
  */
2818
  protected function end()
2819
  {
2820
+ if ($this->matchChar(';')) {
2821
  return true;
2822
  }
2823
 
2979
  *
2980
  * @param integer $pos
2981
  *
2982
+ * @return array
2983
  */
2984
  private function getSourcePosition($pos)
2985
  {
scssphp/src/SourceMap/Base64.php ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
+ *
7
+ * @license http://opensource.org/licenses/MIT MIT
8
+ *
9
+ * @link http://scssphp.github.io/scssphp
10
+ */
11
+
12
+ namespace ScssPhp\ScssPhp\SourceMap;
13
+
14
+ /**
15
+ * Base 64 Encode/Decode
16
+ *
17
+ * @author Anthon Pang <anthon.pang@gmail.com>
18
+ */
19
+ class Base64
20
+ {
21
+ /**
22
+ * @var array
23
+ */
24
+ private static $encodingMap = [
25
+ 0 => 'A',
26
+ 1 => 'B',
27
+ 2 => 'C',
28
+ 3 => 'D',
29
+ 4 => 'E',
30
+ 5 => 'F',
31
+ 6 => 'G',
32
+ 7 => 'H',
33
+ 8 => 'I',
34
+ 9 => 'J',
35
+ 10 => 'K',
36
+ 11 => 'L',
37
+ 12 => 'M',
38
+ 13 => 'N',
39
+ 14 => 'O',
40
+ 15 => 'P',
41
+ 16 => 'Q',
42
+ 17 => 'R',
43
+ 18 => 'S',
44
+ 19 => 'T',
45
+ 20 => 'U',
46
+ 21 => 'V',
47
+ 22 => 'W',
48
+ 23 => 'X',
49
+ 24 => 'Y',
50
+ 25 => 'Z',
51
+ 26 => 'a',
52
+ 27 => 'b',
53
+ 28 => 'c',
54
+ 29 => 'd',
55
+ 30 => 'e',
56
+ 31 => 'f',
57
+ 32 => 'g',
58
+ 33 => 'h',
59
+ 34 => 'i',
60
+ 35 => 'j',
61
+ 36 => 'k',
62
+ 37 => 'l',
63
+ 38 => 'm',
64
+ 39 => 'n',
65
+ 40 => 'o',
66
+ 41 => 'p',
67
+ 42 => 'q',
68
+ 43 => 'r',
69
+ 44 => 's',
70
+ 45 => 't',
71
+ 46 => 'u',
72
+ 47 => 'v',
73
+ 48 => 'w',
74
+ 49 => 'x',
75
+ 50 => 'y',
76
+ 51 => 'z',
77
+ 52 => '0',
78
+ 53 => '1',
79
+ 54 => '2',
80
+ 55 => '3',
81
+ 56 => '4',
82
+ 57 => '5',
83
+ 58 => '6',
84
+ 59 => '7',
85
+ 60 => '8',
86
+ 61 => '9',
87
+ 62 => '+',
88
+ 63 => '/',
89
+ ];
90
+
91
+ /**
92
+ * @var array
93
+ */
94
+ private static $decodingMap = [
95
+ 'A' => 0,
96
+ 'B' => 1,
97
+ 'C' => 2,
98
+ 'D' => 3,
99
+ 'E' => 4,
100
+ 'F' => 5,
101
+ 'G' => 6,
102
+ 'H' => 7,
103
+ 'I' => 8,
104
+ 'J' => 9,
105
+ 'K' => 10,
106
+ 'L' => 11,
107
+ 'M' => 12,
108
+ 'N' => 13,
109
+ 'O' => 14,
110
+ 'P' => 15,
111
+ 'Q' => 16,
112
+ 'R' => 17,
113
+ 'S' => 18,
114
+ 'T' => 19,
115
+ 'U' => 20,
116
+ 'V' => 21,
117
+ 'W' => 22,
118
+ 'X' => 23,
119
+ 'Y' => 24,
120
+ 'Z' => 25,
121
+ 'a' => 26,
122
+ 'b' => 27,
123
+ 'c' => 28,
124
+ 'd' => 29,
125
+ 'e' => 30,
126
+ 'f' => 31,
127
+ 'g' => 32,
128
+ 'h' => 33,
129
+ 'i' => 34,
130
+ 'j' => 35,
131
+ 'k' => 36,
132
+ 'l' => 37,
133
+ 'm' => 38,
134
+ 'n' => 39,
135
+ 'o' => 40,
136
+ 'p' => 41,
137
+ 'q' => 42,
138
+ 'r' => 43,
139
+ 's' => 44,
140
+ 't' => 45,
141
+ 'u' => 46,
142
+ 'v' => 47,
143
+ 'w' => 48,
144
+ 'x' => 49,
145
+ 'y' => 50,
146
+ 'z' => 51,
147
+ 0 => 52,
148
+ 1 => 53,
149
+ 2 => 54,
150
+ 3 => 55,
151
+ 4 => 56,
152
+ 5 => 57,
153
+ 6 => 58,
154
+ 7 => 59,
155
+ 8 => 60,
156
+ 9 => 61,
157
+ '+' => 62,
158
+ '/' => 63,
159
+ ];
160
+
161
+ /**
162
+ * Convert to base64
163
+ *
164
+ * @param integer $value
165
+ *
166
+ * @return string
167
+ */
168
+ public static function encode($value)
169
+ {
170
+ return self::$encodingMap[$value];
171
+ }
172
+
173
+ /**
174
+ * Convert from base64
175
+ *
176
+ * @param string $value
177
+ *
178
+ * @return integer
179
+ */
180
+ public static function decode($value)
181
+ {
182
+ return self::$decodingMap[$value];
183
+ }
184
+ }
scssphp/src/SourceMap/Base64VLQ.php ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
+ *
7
+ * @license http://opensource.org/licenses/MIT MIT
8
+ *
9
+ * @link http://scssphp.github.io/scssphp
10
+ */
11
+
12
+ namespace ScssPhp\ScssPhp\SourceMap;
13
+
14
+ use ScssPhp\ScssPhp\SourceMap\Base64;
15
+
16
+ /**
17
+ * Base 64 VLQ
18
+ *
19
+ * Based on the Base 64 VLQ implementation in Closure Compiler:
20
+ * https://github.com/google/closure-compiler/blob/master/src/com/google/debugging/sourcemap/Base64VLQ.java
21
+ *
22
+ * Copyright 2011 The Closure Compiler Authors.
23
+ *
24
+ * Licensed under the Apache License, Version 2.0 (the "License");
25
+ * you may not use this file except in compliance with the License.
26
+ * You may obtain a copy of the License at
27
+ *
28
+ * http://www.apache.org/licenses/LICENSE-2.0
29
+ *
30
+ * Unless required by applicable law or agreed to in writing, software
31
+ * distributed under the License is distributed on an "AS IS" BASIS,
32
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
33
+ * See the License for the specific language governing permissions and
34
+ * limitations under the License.
35
+ *
36
+ * @author John Lenz <johnlenz@google.com>
37
+ * @author Anthon Pang <anthon.pang@gmail.com>
38
+ */
39
+ class Base64VLQ
40
+ {
41
+ // A Base64 VLQ digit can represent 5 bits, so it is base-32.
42
+ const VLQ_BASE_SHIFT = 5;
43
+
44
+ // A mask of bits for a VLQ digit (11111), 31 decimal.
45
+ const VLQ_BASE_MASK = 31;
46
+
47
+ // The continuation bit is the 6th bit.
48
+ const VLQ_CONTINUATION_BIT = 32;
49
+
50
+ /**
51
+ * Returns the VLQ encoded value.
52
+ *
53
+ * @param integer $value
54
+ *
55
+ * @return string
56
+ */
57
+ public static function encode($value)
58
+ {
59
+ $encoded = '';
60
+ $vlq = self::toVLQSigned($value);
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;
68
+ }
69
+
70
+ $encoded .= Base64::encode($digit);
71
+ } while ($vlq > 0);
72
+
73
+ return $encoded;
74
+ }
75
+
76
+ /**
77
+ * Decodes VLQValue.
78
+ *
79
+ * @param string $str
80
+ * @param integer $index
81
+ *
82
+ * @return integer
83
+ */
84
+ public static function decode($str, &$index)
85
+ {
86
+ $result = 0;
87
+ $shift = 0;
88
+
89
+ do {
90
+ $c = $str[$index++];
91
+ $digit = Base64::decode($c);
92
+ $continuation = ($digit & self::VLQ_CONTINUATION_BIT) != 0;
93
+ $digit &= self::VLQ_BASE_MASK;
94
+ $result = $result + ($digit << $shift);
95
+ $shift = $shift + self::VLQ_BASE_SHIFT;
96
+ } while ($continuation);
97
+
98
+ return self::fromVLQSigned($result);
99
+ }
100
+
101
+ /**
102
+ * Converts from a two-complement value to a value where the sign bit is
103
+ * is placed in the least significant bit. For example, as decimals:
104
+ * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
105
+ * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
106
+ *
107
+ * @param integer $value
108
+ *
109
+ * @return integer
110
+ */
111
+ private static function toVLQSigned($value)
112
+ {
113
+ if ($value < 0) {
114
+ return ((-$value) << 1) + 1;
115
+ }
116
+
117
+ return ($value << 1) + 0;
118
+ }
119
+
120
+ /**
121
+ * Converts to a two-complement value from a value where the sign bit is
122
+ * is placed in the least significant bit. For example, as decimals:
123
+ * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1
124
+ * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2
125
+ *
126
+ * @param integer $value
127
+ *
128
+ * @return integer
129
+ */
130
+ private static function fromVLQSigned($value)
131
+ {
132
+ $negate = ($value & 1) === 1;
133
+ $value = $value >> 1;
134
+
135
+ return $negate ? -$value : $value;
136
+ }
137
+ }
scssphp/src/SourceMap/Base64VLQEncoder.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\SourceMap;
13
 
14
  /**
15
  * Base64 VLQ Encoder
@@ -47,7 +47,7 @@ class Base64VLQEncoder
47
  *
48
  * @var array
49
  */
50
- private $charToIntMap = array(
51
  'A' => 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6, 'H' => 7,
52
  'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13, 'O' => 14, 'P' => 15,
53
  'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20, 'V' => 21, 'W' => 22, 'X' => 23,
@@ -56,14 +56,14 @@ class Base64VLQEncoder
56
  'o' => 40, 'p' => 41, 'q' => 42, 'r' => 43, 's' => 44, 't' => 45, 'u' => 46, 'v' => 47,
57
  'w' => 48, 'x' => 49, 'y' => 50, 'z' => 51, 0 => 52, 1 => 53, 2 => 54, 3 => 55,
58
  4 => 56, 5 => 57, 6 => 58, 7 => 59, 8 => 60, 9 => 61, '+' => 62, '/' => 63,
59
- );
60
 
61
  /**
62
  * Integer to char map
63
  *
64
  * @var array
65
  */
66
- private $intToCharMap = array(
67
  0 => 'A', 1 => 'B', 2 => 'C', 3 => 'D', 4 => 'E', 5 => 'F', 6 => 'G', 7 => 'H',
68
  8 => 'I', 9 => 'J', 10 => 'K', 11 => 'L', 12 => 'M', 13 => 'N', 14 => 'O', 15 => 'P',
69
  16 => 'Q', 17 => 'R', 18 => 'S', 19 => 'T', 20 => 'U', 21 => 'V', 22 => 'W', 23 => 'X',
@@ -72,7 +72,7 @@ class Base64VLQEncoder
72
  40 => 'o', 41 => 'p', 42 => 'q', 43 => 'r', 44 => 's', 45 => 't', 46 => 'u', 47 => 'v',
73
  48 => 'w', 49 => 'x', 50 => 'y', 51 => 'z', 52 => '0', 53 => '1', 54 => '2', 55 => '3',
74
  56 => '4', 57 => '5', 58 => '6', 59 => '7', 60 => '8', 61 => '9', 62 => '+', 63 => '/',
75
- );
76
 
77
  /**
78
  * Constructor
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\SourceMap;
13
 
14
  /**
15
  * Base64 VLQ Encoder
47
  *
48
  * @var array
49
  */
50
+ private $charToIntMap = [
51
  'A' => 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6, 'H' => 7,
52
  'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13, 'O' => 14, 'P' => 15,
53
  'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20, 'V' => 21, 'W' => 22, 'X' => 23,
56
  'o' => 40, 'p' => 41, 'q' => 42, 'r' => 43, 's' => 44, 't' => 45, 'u' => 46, 'v' => 47,
57
  'w' => 48, 'x' => 49, 'y' => 50, 'z' => 51, 0 => 52, 1 => 53, 2 => 54, 3 => 55,
58
  4 => 56, 5 => 57, 6 => 58, 7 => 59, 8 => 60, 9 => 61, '+' => 62, '/' => 63,
59
+ ];
60
 
61
  /**
62
  * Integer to char map
63
  *
64
  * @var array
65
  */
66
+ private $intToCharMap = [
67
  0 => 'A', 1 => 'B', 2 => 'C', 3 => 'D', 4 => 'E', 5 => 'F', 6 => 'G', 7 => 'H',
68
  8 => 'I', 9 => 'J', 10 => 'K', 11 => 'L', 12 => 'M', 13 => 'N', 14 => 'O', 15 => 'P',
69
  16 => 'Q', 17 => 'R', 18 => 'S', 19 => 'T', 20 => 'U', 21 => 'V', 22 => 'W', 23 => 'X',
72
  40 => 'o', 41 => 'p', 42 => 'q', 43 => 'r', 44 => 's', 45 => 't', 46 => 'u', 47 => 'v',
73
  48 => 'w', 49 => 'x', 50 => 'y', 51 => 'z', 52 => '0', 53 => '1', 54 => '2', 55 => '3',
74
  56 => '4', 57 => '5', 58 => '6', 59 => '7', 60 => '8', 61 => '9', 62 => '+', 63 => '/',
75
+ ];
76
 
77
  /**
78
  * Constructor
scssphp/src/SourceMap/SourceMapGenerator.php CHANGED
@@ -2,16 +2,16 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\SourceMap;
13
 
14
- use Leafo\ScssPhp\Exception\CompilerException;
15
 
16
  /**
17
  * Source Map Generator
@@ -33,7 +33,7 @@ class SourceMapGenerator
33
  *
34
  * @var array
35
  */
36
- protected $defaultOptions = array(
37
  // an optional source root, useful for relocating source files
38
  // on a server or removing repeated values in the 'sources' entry.
39
  // This value is prepended to the individual entries in the 'source' field.
@@ -56,12 +56,12 @@ class SourceMapGenerator
56
 
57
  // base path for filename normalization
58
  'sourceMapBasepath' => ''
59
- );
60
 
61
  /**
62
  * The base64 VLQ encoder
63
  *
64
- * @var \Leafo\ScssPhp\SourceMap\Base64VLQEncoder
65
  */
66
  protected $encoder;
67
 
@@ -70,22 +70,22 @@ class SourceMapGenerator
70
  *
71
  * @var array
72
  */
73
- protected $mappings = array();
74
 
75
  /**
76
  * Array of contents map
77
  *
78
  * @var array
79
  */
80
- protected $contentsMap = array();
81
 
82
  /**
83
  * File to content map
84
  *
85
  * @var array
86
  */
87
- protected $sources = array();
88
- protected $source_keys = array();
89
 
90
  /**
91
  * @var array
@@ -95,7 +95,7 @@ class SourceMapGenerator
95
  public function __construct(array $options = [])
96
  {
97
  $this->options = array_merge($this->defaultOptions, $options);
98
- $this->encoder = new Base64VLQEncoder();
99
  }
100
 
101
  /**
@@ -109,13 +109,13 @@ class SourceMapGenerator
109
  */
110
  public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $sourceFile)
111
  {
112
- $this->mappings[] = array(
113
- 'generated_line' => $generatedLine,
114
  'generated_column' => $generatedColumn,
115
- 'original_line' => $originalLine,
116
- 'original_column' => $originalColumn,
117
- 'source_file' => $sourceFile
118
- );
119
 
120
  $this->sources[$sourceFile] = $sourceFile;
121
  }
@@ -123,10 +123,11 @@ class SourceMapGenerator
123
  /**
124
  * Saves the source map to a file
125
  *
126
- * @param string $file The absolute path to a file
127
  * @param string $content The content to write
128
  *
129
- * @throws \Leafo\ScssPhp\Exception\CompilerException If the file could not be saved
 
 
130
  */
131
  public function saveMap($content)
132
  {
@@ -136,7 +137,9 @@ class SourceMapGenerator
136
  // directory does not exist
137
  if (! is_dir($dir)) {
138
  // FIXME: create the dir automatically?
139
- throw new CompilerException(sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir));
 
 
140
  }
141
 
142
  // FIXME: proper saving, with dir write check!
@@ -156,7 +159,7 @@ class SourceMapGenerator
156
  */
157
  public function generateJson()
158
  {
159
- $sourceMap = array();
160
  $mappings = $this->generateMappings();
161
 
162
  // File version (always the first entry in the object) and must be a positive integer.
@@ -178,14 +181,14 @@ class SourceMapGenerator
178
  }
179
 
180
  // A list of original sources used by the 'mappings' entry.
181
- $sourceMap['sources'] = array();
182
 
183
- foreach ($this->sources as $source_uri => $source_filename) {
184
- $sourceMap['sources'][] = $this->normalizeFilename($source_filename);
185
  }
186
 
187
  // A list of symbol names used by the 'mappings' entry.
188
- $sourceMap['names'] = array();
189
 
190
  // A string with the encoded mapping data.
191
  $sourceMap['mappings'] = $mappings;
@@ -202,7 +205,7 @@ class SourceMapGenerator
202
  unset($sourceMap['sourceRoot']);
203
  }
204
 
205
- return json_encode($sourceMap);
206
  }
207
 
208
  /**
@@ -216,7 +219,7 @@ class SourceMapGenerator
216
  return null;
217
  }
218
 
219
- $content = array();
220
 
221
  foreach ($this->sources as $sourceFile) {
222
  $content[] = file_get_contents($sourceFile);
@@ -236,10 +239,10 @@ class SourceMapGenerator
236
  return '';
237
  }
238
 
239
- $this->source_keys = array_flip(array_keys($this->sources));
240
 
241
  // group mappings by generated line number.
242
- $groupedMap = $groupedMapEncoded = array();
243
 
244
  foreach ($this->mappings as $m) {
245
  $groupedMap[$m['generated_line']][] = $m;
@@ -248,15 +251,15 @@ class SourceMapGenerator
248
  ksort($groupedMap);
249
  $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
250
 
251
- foreach ($groupedMap as $lineNumber => $line_map) {
252
  while (++$lastGeneratedLine < $lineNumber) {
253
  $groupedMapEncoded[] = ';';
254
  }
255
 
256
- $lineMapEncoded = array();
257
  $lastGeneratedColumn = 0;
258
 
259
- foreach ($line_map as $m) {
260
  $mapEncoded = $this->encoder->encode($m['generated_column'] - $lastGeneratedColumn);
261
  $lastGeneratedColumn = $m['generated_column'];
262
 
@@ -293,9 +296,16 @@ class SourceMapGenerator
293
  */
294
  protected function findFileIndex($filename)
295
  {
296
- return $this->source_keys[$filename];
297
  }
298
 
 
 
 
 
 
 
 
299
  protected function normalizeFilename($filename)
300
  {
301
  $filename = $this->fixWindowsPath($filename);
@@ -303,7 +313,7 @@ class SourceMapGenerator
303
  $basePath = $this->options['sourceMapBasepath'];
304
 
305
  // "Trim" the 'sourceMapBasepath' from the output filename.
306
- if (strpos($filename, $basePath) === 0) {
307
  $filename = substr($filename, strlen($basePath));
308
  }
309
 
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\SourceMap;
13
 
14
+ use ScssPhp\ScssPhp\Exception\CompilerException;
15
 
16
  /**
17
  * Source Map Generator
33
  *
34
  * @var array
35
  */
36
+ protected $defaultOptions = [
37
  // an optional source root, useful for relocating source files
38
  // on a server or removing repeated values in the 'sources' entry.
39
  // This value is prepended to the individual entries in the 'source' field.
56
 
57
  // base path for filename normalization
58
  'sourceMapBasepath' => ''
59
+ ];
60
 
61
  /**
62
  * The base64 VLQ encoder
63
  *
64
+ * @var \ScssPhp\ScssPhp\SourceMap\Base64VLQ
65
  */
66
  protected $encoder;
67
 
70
  *
71
  * @var array
72
  */
73
+ protected $mappings = [];
74
 
75
  /**
76
  * Array of contents map
77
  *
78
  * @var array
79
  */
80
+ protected $contentsMap = [];
81
 
82
  /**
83
  * File to content map
84
  *
85
  * @var array
86
  */
87
+ protected $sources = [];
88
+ protected $sourceKeys = [];
89
 
90
  /**
91
  * @var array
95
  public function __construct(array $options = [])
96
  {
97
  $this->options = array_merge($this->defaultOptions, $options);
98
+ $this->encoder = new Base64VLQ();
99
  }
100
 
101
  /**
109
  */
110
  public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $sourceFile)
111
  {
112
+ $this->mappings[] = [
113
+ 'generated_line' => $generatedLine,
114
  'generated_column' => $generatedColumn,
115
+ 'original_line' => $originalLine,
116
+ 'original_column' => $originalColumn,
117
+ 'source_file' => $sourceFile
118
+ ];
119
 
120
  $this->sources[$sourceFile] = $sourceFile;
121
  }
123
  /**
124
  * Saves the source map to a file
125
  *
 
126
  * @param string $content The content to write
127
  *
128
+ * @return string
129
+ *
130
+ * @throws \ScssPhp\ScssPhp\Exception\CompilerException If the file could not be saved
131
  */
132
  public function saveMap($content)
133
  {
137
  // directory does not exist
138
  if (! is_dir($dir)) {
139
  // FIXME: create the dir automatically?
140
+ throw new CompilerException(
141
+ sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir)
142
+ );
143
  }
144
 
145
  // FIXME: proper saving, with dir write check!
159
  */
160
  public function generateJson()
161
  {
162
+ $sourceMap = [];
163
  $mappings = $this->generateMappings();
164
 
165
  // File version (always the first entry in the object) and must be a positive integer.
181
  }
182
 
183
  // A list of original sources used by the 'mappings' entry.
184
+ $sourceMap['sources'] = [];
185
 
186
+ foreach ($this->sources as $sourceUri => $sourceFilename) {
187
+ $sourceMap['sources'][] = $this->normalizeFilename($sourceFilename);
188
  }
189
 
190
  // A list of symbol names used by the 'mappings' entry.
191
+ $sourceMap['names'] = [];
192
 
193
  // A string with the encoded mapping data.
194
  $sourceMap['mappings'] = $mappings;
205
  unset($sourceMap['sourceRoot']);
206
  }
207
 
208
+ return json_encode($sourceMap, JSON_UNESCAPED_SLASHES);
209
  }
210
 
211
  /**
219
  return null;
220
  }
221
 
222
+ $content = [];
223
 
224
  foreach ($this->sources as $sourceFile) {
225
  $content[] = file_get_contents($sourceFile);
239
  return '';
240
  }
241
 
242
+ $this->sourceKeys = array_flip(array_keys($this->sources));
243
 
244
  // group mappings by generated line number.
245
+ $groupedMap = $groupedMapEncoded = [];
246
 
247
  foreach ($this->mappings as $m) {
248
  $groupedMap[$m['generated_line']][] = $m;
251
  ksort($groupedMap);
252
  $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
253
 
254
+ foreach ($groupedMap as $lineNumber => $lineMap) {
255
  while (++$lastGeneratedLine < $lineNumber) {
256
  $groupedMapEncoded[] = ';';
257
  }
258
 
259
+ $lineMapEncoded = [];
260
  $lastGeneratedColumn = 0;
261
 
262
+ foreach ($lineMap as $m) {
263
  $mapEncoded = $this->encoder->encode($m['generated_column'] - $lastGeneratedColumn);
264
  $lastGeneratedColumn = $m['generated_column'];
265
 
296
  */
297
  protected function findFileIndex($filename)
298
  {
299
+ return $this->sourceKeys[$filename];
300
  }
301
 
302
+ /**
303
+ * Normalize filename
304
+ *
305
+ * @param string $filename
306
+ *
307
+ * @return string
308
+ */
309
  protected function normalizeFilename($filename)
310
  {
311
  $filename = $this->fixWindowsPath($filename);
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
 
scssphp/src/Type.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
 
14
  /**
15
  * Block/node types
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
 
14
  /**
15
  * Block/node types
scssphp/src/Util.php CHANGED
@@ -2,17 +2,17 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
 
14
- use Leafo\ScssPhp\Base\Range;
15
- use Leafo\ScssPhp\Exception\RangeException;
16
 
17
  /**
18
  * Utilty functions
@@ -26,13 +26,13 @@ class Util
26
  * room for slight floating-point errors.
27
  *
28
  * @param string $name The name of the value. Used in the error message.
29
- * @param \Leafo\ScssPhp\Base\Range $range Range of values.
30
  * @param array $value The value to check.
31
  * @param string $unit The unit of the value. Used in error reporting.
32
  *
33
  * @return mixed `value` adjusted to fall within range, if it was outside by a floating-point margin.
34
  *
35
- * @throws \Leafo\ScssPhp\Exception\RangeException
36
  */
37
  public static function checkRange($name, Range $range, $value, $unit = '')
38
  {
@@ -63,7 +63,7 @@ class Util
63
  */
64
  public static function encodeURIComponent($string)
65
  {
66
- $revert = array('%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')');
67
 
68
  return strtr(rawurlencode($string), $revert);
69
  }
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
 
14
+ use ScssPhp\ScssPhp\Base\Range;
15
+ use ScssPhp\ScssPhp\Exception\RangeException;
16
 
17
  /**
18
  * Utilty functions
26
  * room for slight floating-point errors.
27
  *
28
  * @param string $name The name of the value. Used in the error message.
29
+ * @param \ScssPhp\ScssPhp\Base\Range $range Range of values.
30
  * @param array $value The value to check.
31
  * @param string $unit The unit of the value. Used in error reporting.
32
  *
33
  * @return mixed `value` adjusted to fall within range, if it was outside by a floating-point margin.
34
  *
35
+ * @throws \ScssPhp\ScssPhp\Exception\RangeException
36
  */
37
  public static function checkRange($name, Range $range, $value, $unit = '')
38
  {
63
  */
64
  public static function encodeURIComponent($string)
65
  {
66
+ $revert = ['%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')'];
67
 
68
  return strtr(rawurlencode($string), $revert);
69
  }
scssphp/src/Version.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
 
14
  /**
15
  * SCSSPHP version
@@ -18,5 +18,5 @@ namespace Leafo\ScssPhp;
18
  */
19
  class Version
20
  {
21
- const VERSION = 'v0.7.5';
22
  }
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2019 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
 
14
  /**
15
  * SCSSPHP version
18
  */
19
  class Version
20
  {
21
+ const VERSION = 'v1.0.2';
22
  }
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: 1.2.6
7
  * Author: Connect Think
8
  * Author URI: http://connectthink.com
9
  * License: GPLv3
@@ -30,36 +30,36 @@
30
 
31
  // Plugin Paths
32
  if (!defined('WPSCSS_THEME_DIR'))
33
- define('WPSCSS_THEME_DIR', get_stylesheet_directory());
34
 
35
  if (!defined('WPSCSS_PLUGIN_NAME'))
36
- define('WPSCSS_PLUGIN_NAME', trim(dirname(plugin_basename(__FILE__)), '/'));
37
 
38
  if (!defined('WPSCSS_PLUGIN_DIR'))
39
- define('WPSCSS_PLUGIN_DIR', WP_PLUGIN_DIR . '/' . WPSCSS_PLUGIN_NAME);
40
 
41
  if (!defined('WPSCSS_PLUGIN_URL'))
42
- define('WPSCSS_PLUGIN_URL', WP_PLUGIN_URL . '/' . WPSCSS_PLUGIN_NAME);
43
 
44
  // Plugin Version
45
  if (!defined('WPSCSS_VERSION_KEY'))
46
- define('WPSCSS_VERSION_KEY', 'wpscss_version');
47
 
48
  if (!defined('WPSCSS_VERSION_NUM'))
49
- define('WPSCSS_VERSION_NUM', '1.2.6');
50
 
51
  // Add version to options table
52
  if ( get_option( WPSCSS_VERSION_KEY ) !== false ) {
53
 
54
- // The option already exists, so we just update it.
55
- update_option( WPSCSS_VERSION_KEY, WPSCSS_VERSION_NUM );
56
 
57
  } else {
58
 
59
- // The option hasn't been added yet. We'll add it with $autoload set to 'no'.
60
- $deprecated = null;
61
- $autoload = 'no';
62
- add_option( WPSCSS_VERSION_KEY, WPSCSS_VERSION_NUM, $deprecated, $autoload );
63
  }
64
 
65
 
@@ -84,7 +84,7 @@ include_once WPSCSS_PLUGIN_DIR . '/options.php'; // Options page class
84
  */
85
 
86
  if( is_admin() ) {
87
- $wpscss_settings = new Wp_Scss_Settings();
88
  }
89
 
90
  add_filter('plugin_action_links', 'wpscss_plugin_action_links', 10, 2);
@@ -96,11 +96,11 @@ function wpscss_plugin_action_links($links, $file) {
96
  }
97
 
98
  if ($file == $this_plugin) {
99
- $settings_link = '<a href="' . get_bloginfo('wpurl') . '/wp-admin/admin.php?page=wpscss_options">Settings</a>';
100
- array_unshift($links, $settings_link);
101
- }
102
 
103
- return $links;
104
  }
105
 
106
 
@@ -119,26 +119,26 @@ $css_dir_setting = isset($wpscss_options['css_dir']) ? $wpscss_options['css_dir'
119
  // Checks if directories are empty
120
  if( $scss_dir_setting == false || $css_dir_setting == false ) {
121
  function wpscss_settings_error() {
122
- echo '<div class="error">
123
- <p><strong>Wp-Scss</strong> requires both directories be specified. <a href="' . get_bloginfo('wpurl') . '/wp-admin/admin.php?page=wpscss_options">Please update your settings.</a></p>
124
  </div>';
125
  }
126
  add_action('admin_notices', 'wpscss_settings_error');
127
  return 0; //exits
128
 
129
- // Checks if directory exists
130
  } elseif ( !is_dir(WPSCSS_THEME_DIR . $scss_dir_setting) ) {
131
  function wpscss_settings_error() {
132
- echo '<div class="error">
133
- <p><strong>Wp-Scss:</strong> SCSS directory does not exist (' . WPSCSS_THEME_DIR . $scss_dir_setting . '). Please create the directory or <a href="' . get_bloginfo('wpurl') . '/wp-admin/admin.php?page=wpscss_options">update your settings.</a></p>
134
  </div>';
135
  }
136
  add_action('admin_notices', 'wpscss_settings_error');
137
  return 0; //exits
138
  } elseif ( !is_dir(WPSCSS_THEME_DIR . $css_dir_setting) ) {
139
  function wpscss_settings_error() {
140
- echo '<div class="error">
141
- <p><strong>Wp-Scss:</strong> CSS directory does not exist (' . WPSCSS_THEME_DIR . $css_dir_setting . '). Please create the directory or <a href="' . get_bloginfo('wpurl') . '/wp-admin/admin.php?page=wpscss_options">update your settings.</a></p>
142
  </div>';
143
  }
144
  add_action('admin_notices', 'wpscss_settings_error');
@@ -149,7 +149,7 @@ if( $scss_dir_setting == false || $css_dir_setting == false ) {
149
  $wpscss_settings = array(
150
  'scss_dir' => WPSCSS_THEME_DIR . $scss_dir_setting,
151
  'css_dir' => WPSCSS_THEME_DIR . $css_dir_setting,
152
- 'compiling' => isset($wpscss_options['compiling_options']) ? $wpscss_options['compiling_options'] : 'Leafo\ScssPhp\Formatter\Expanded',
153
  'errors' => isset($wpscss_options['errors']) ? $wpscss_options['errors'] : 'show',
154
  'sourcemaps' => isset($wpscss_options['sourcemap_options']) ? $wpscss_options['sourcemap_options'] : 'SOURCE_MAP_NONE',
155
  'enqueue' => isset($wpscss_options['enqueue']) ? $wpscss_options['enqueue'] : 0
@@ -206,32 +206,33 @@ function wp_scss_compile() {
206
  * After the file gets over 1MB it does a purge and deletes the first
207
  * half of entries in the file.
208
  */
 
209
  $log_file = $wpscss_compiler->scss_dir.'error_log.log';
210
 
211
  function wpscss_error_styles() {
212
  echo
213
- '<style>
214
- .scss_errors {
215
- position: fixed;
216
- top: 0px;
217
- z-index: 99999;
218
- width: 100%;
219
- }
220
- .scss_errors pre {
221
- background: #f5f5f5;
222
- border-left: 5px solid #DD3D36;
223
- box-shadow: 0 2px 3px rgba(51,51,51, .4);
224
- color: #666;
225
- font-family: monospace;
226
- font-size: 14px;
227
- margin: 20px 0;
228
- overflow: auto;
229
- padding: 20px;
230
- white-space: pre;
231
- white-space: pre-wrap;
232
- word-wrap: break-word;
233
- }
234
- </style>';
235
  }
236
 
237
  function wpscss_settings_show_errors($errors) {
@@ -250,33 +251,32 @@ function wpscss_settings_show_errors($errors) {
250
  }
251
 
252
  function wpscss_handle_errors() {
253
- global $wpscss_settings, $log_file, $wpscss_compiler;
254
- // 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
255
- if ( !is_admin() && $wpscss_settings['errors'] === 'show-logged-in' && !empty($_COOKIE[LOGGED_IN_COOKIE]) && count($wpscss_compiler->compile_errors) > 0) {
256
- wpscss_settings_show_errors($wpscss_compiler->compile_errors);
257
- // Show in the header to anyone
258
- } else if ( !is_admin() && $wpscss_settings['errors'] === 'show' && count($wpscss_compiler->compile_errors) > 0) {
259
- wpscss_settings_show_errors($wpscss_compiler->compile_errors);
260
- } else { // Hide errors and print them to a log file.
261
- foreach ($wpscss_compiler->compile_errors as $error) {
262
- $error_string = date('m/d/y g:i:s', time()) .': ';
263
- $error_string .= $error['file'] .' - '. $error['message'] . PHP_EOL;
264
- file_put_contents($log_file, $error_string, FILE_APPEND);
265
- $error_string = "";
266
- }
267
  }
 
268
 
269
- // Clean out log file if it get's too large
270
- if ( file_exists($log_file) ) {
271
- if ( filesize($log_file) > 1000000) {
272
- $log_contents = file_get_contents($log_file);
273
- $log_arr = explode("\n", $log_contents);
274
- $new_contents_arr = array_slice($log_arr, count($log_arr)/2);
275
- $new_contents = implode(PHP_EOL, $new_contents_arr) . 'LOG FILE CLEANED ' . date('n/j/y g:i:s', time());
276
- file_put_contents($log_file, $new_contents);
277
- }
278
  }
279
-
280
  }
281
 
282
 
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.0.0
7
  * Author: Connect Think
8
  * Author URI: http://connectthink.com
9
  * License: GPLv3
30
 
31
  // Plugin Paths
32
  if (!defined('WPSCSS_THEME_DIR'))
33
+ define('WPSCSS_THEME_DIR', get_stylesheet_directory());
34
 
35
  if (!defined('WPSCSS_PLUGIN_NAME'))
36
+ define('WPSCSS_PLUGIN_NAME', trim(dirname(plugin_basename(__FILE__)), '/'));
37
 
38
  if (!defined('WPSCSS_PLUGIN_DIR'))
39
+ define('WPSCSS_PLUGIN_DIR', WP_PLUGIN_DIR . '/' . WPSCSS_PLUGIN_NAME);
40
 
41
  if (!defined('WPSCSS_PLUGIN_URL'))
42
+ define('WPSCSS_PLUGIN_URL', WP_PLUGIN_URL . '/' . WPSCSS_PLUGIN_NAME);
43
 
44
  // Plugin Version
45
  if (!defined('WPSCSS_VERSION_KEY'))
46
+ define('WPSCSS_VERSION_KEY', 'wpscss_version');
47
 
48
  if (!defined('WPSCSS_VERSION_NUM'))
49
+ define('WPSCSS_VERSION_NUM', '2.0.0');
50
 
51
  // Add version to options table
52
  if ( get_option( WPSCSS_VERSION_KEY ) !== false ) {
53
 
54
+ // The option already exists, so we just update it.
55
+ update_option( WPSCSS_VERSION_KEY, WPSCSS_VERSION_NUM );
56
 
57
  } else {
58
 
59
+ // The option hasn't been added yet. We'll add it with $autoload set to 'no'.
60
+ $deprecated = null;
61
+ $autoload = 'no';
62
+ add_option( WPSCSS_VERSION_KEY, WPSCSS_VERSION_NUM, $deprecated, $autoload );
63
  }
64
 
65
 
84
  */
85
 
86
  if( is_admin() ) {
87
+ $wpscss_settings = new Wp_Scss_Settings();
88
  }
89
 
90
  add_filter('plugin_action_links', 'wpscss_plugin_action_links', 10, 2);
96
  }
97
 
98
  if ($file == $this_plugin) {
99
+ $settings_link = '<a href="' . get_bloginfo('wpurl') . '/wp-admin/admin.php?page=wpscss_options">Settings</a>';
100
+ array_unshift($links, $settings_link);
101
+ }
102
 
103
+ return $links;
104
  }
105
 
106
 
119
  // Checks if directories are empty
120
  if( $scss_dir_setting == false || $css_dir_setting == false ) {
121
  function wpscss_settings_error() {
122
+ echo '<div class="error">
123
+ <p><strong>Wp-Scss</strong> requires both directories be specified. <a href="' . get_bloginfo('wpurl') . '/wp-admin/admin.php?page=wpscss_options">Please update your settings.</a></p>
124
  </div>';
125
  }
126
  add_action('admin_notices', 'wpscss_settings_error');
127
  return 0; //exits
128
 
129
+ // Checks if directory exists
130
  } elseif ( !is_dir(WPSCSS_THEME_DIR . $scss_dir_setting) ) {
131
  function wpscss_settings_error() {
132
+ echo '<div class="error">
133
+ <p><strong>Wp-Scss:</strong> SCSS directory does not exist (' . WPSCSS_THEME_DIR . $scss_dir_setting . '). Please create the directory or <a href="' . get_bloginfo('wpurl') . '/wp-admin/admin.php?page=wpscss_options">update your settings.</a></p>
134
  </div>';
135
  }
136
  add_action('admin_notices', 'wpscss_settings_error');
137
  return 0; //exits
138
  } elseif ( !is_dir(WPSCSS_THEME_DIR . $css_dir_setting) ) {
139
  function wpscss_settings_error() {
140
+ echo '<div class="error">
141
+ <p><strong>Wp-Scss:</strong> CSS directory does not exist (' . WPSCSS_THEME_DIR . $css_dir_setting . '). Please create the directory or <a href="' . get_bloginfo('wpurl') . '/wp-admin/admin.php?page=wpscss_options">update your settings.</a></p>
142
  </div>';
143
  }
144
  add_action('admin_notices', 'wpscss_settings_error');
149
  $wpscss_settings = array(
150
  'scss_dir' => WPSCSS_THEME_DIR . $scss_dir_setting,
151
  'css_dir' => WPSCSS_THEME_DIR . $css_dir_setting,
152
+ 'compiling' => isset($wpscss_options['compiling_options']) ? $wpscss_options['compiling_options'] : 'ScssPhp\ScssPhp\Formatter\Expanded',
153
  'errors' => isset($wpscss_options['errors']) ? $wpscss_options['errors'] : 'show',
154
  'sourcemaps' => isset($wpscss_options['sourcemap_options']) ? $wpscss_options['sourcemap_options'] : 'SOURCE_MAP_NONE',
155
  'enqueue' => isset($wpscss_options['enqueue']) ? $wpscss_options['enqueue'] : 0
206
  * After the file gets over 1MB it does a purge and deletes the first
207
  * half of entries in the file.
208
  */
209
+
210
  $log_file = $wpscss_compiler->scss_dir.'error_log.log';
211
 
212
  function wpscss_error_styles() {
213
  echo
214
+ '<style>
215
+ .scss_errors {
216
+ position: fixed;
217
+ top: 0px;
218
+ z-index: 99999;
219
+ width: 100%;
220
+ }
221
+ .scss_errors pre {
222
+ background: #f5f5f5;
223
+ border-left: 5px solid #DD3D36;
224
+ box-shadow: 0 2px 3px rgba(51,51,51, .4);
225
+ color: #666;
226
+ font-family: monospace;
227
+ font-size: 14px;
228
+ margin: 20px 0;
229
+ overflow: auto;
230
+ padding: 20px;
231
+ white-space: pre;
232
+ white-space: pre-wrap;
233
+ word-wrap: break-word;
234
+ }
235
+ </style>';
236
  }
237
 
238
  function wpscss_settings_show_errors($errors) {
251
  }
252
 
253
  function wpscss_handle_errors() {
254
+ global $wpscss_settings, $log_file, $wpscss_compiler;
255
+ // 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
256
+ if ( !is_admin() && $wpscss_settings['errors'] === 'show-logged-in' && !empty($_COOKIE[LOGGED_IN_COOKIE]) && count($wpscss_compiler->compile_errors) > 0) {
257
+ wpscss_settings_show_errors($wpscss_compiler->compile_errors);
258
+ // Show in the header to anyone
259
+ } else if ( !is_admin() && $wpscss_settings['errors'] === 'show' && count($wpscss_compiler->compile_errors) > 0) {
260
+ wpscss_settings_show_errors($wpscss_compiler->compile_errors);
261
+ } else { // Hide errors and print them to a log file.
262
+ foreach ($wpscss_compiler->compile_errors as $error) {
263
+ $error_string = date('m/d/y g:i:s', time()) .': ';
264
+ $error_string .= $error['file'] .' - '. $error['message'] . PHP_EOL;
265
+ file_put_contents($log_file, $error_string, FILE_APPEND);
266
+ $error_string = "";
 
267
  }
268
+ }
269
 
270
+ // Clean out log file if it get's too large
271
+ if ( file_exists($log_file) ) {
272
+ if ( filesize($log_file) > 1000000) {
273
+ $log_contents = file_get_contents($log_file);
274
+ $log_arr = explode("\n", $log_contents);
275
+ $new_contents_arr = array_slice($log_arr, count($log_arr)/2);
276
+ $new_contents = implode(PHP_EOL, $new_contents_arr) . 'LOG FILE CLEANED ' . date('n/j/y g:i:s', time());
277
+ file_put_contents($log_file, $new_contents);
 
278
  }
279
+ }
280
  }
281
 
282