WP-SCSS - Version 1.2.0

Version Description

  • Fixed a bug where directory inputs were not getting sanitized @mmcev106
  • Made the missing directory warning also display if a specified path is a file @mmcev106
  • Added /vendor to .gitignore @mmcev106
  • Dont enqueue already enqueued stylesheets @bobbysmith007
Download this release

Release Info

Developer connectthink
Plugin Icon wp plugin WP-SCSS
Version 1.2.0
Comparing to
See all releases

Code changes from version 1.1.9 to 1.2.0

class/class-wp-scss.php CHANGED
@@ -1,5 +1,7 @@
1
  <?php
2
 
 
 
3
  class Wp_Scss {
4
  /**
5
  * Compiling preferences properites
@@ -9,7 +11,6 @@ class Wp_Scss {
9
  */
10
  public $scss_dir, $css_dir, $compile_method, $scssc, $compile_errors;
11
 
12
-
13
  /**
14
  * Set values for Wp_Scss::properties
15
  *
@@ -22,16 +23,15 @@ class Wp_Scss {
22
  * @var array compile_errors - catches errors from compile
23
  */
24
  public function __construct ($scss_dir, $css_dir, $compile_method) {
25
- $this->scss_dir = $scss_dir;
26
- $this->css_dir = $css_dir;
27
- $this->compile_method = $compile_method;
28
-
29
  global $scssc;
30
- $scssc = new scssc();
31
- $scssc->setFormatter($compile_method);
32
- $scssc->setImportPaths($scss_dir);
33
-
34
  $this->compile_errors = array();
 
 
 
 
35
  }
36
 
37
  /**
@@ -167,6 +167,16 @@ class Wp_Scss {
167
  }
168
  }
169
 
 
 
 
 
 
 
 
 
 
 
170
  /**
171
  * METHOD ENQUEUE STYLES
172
  * Enqueues all styles in the css directory.
@@ -190,14 +200,16 @@ class Wp_Scss {
190
  $ver,
191
  $media = 'all' );
192
 
193
- wp_enqueue_style( $name );
 
 
194
  }
195
  }
196
  }
197
 
198
  public function set_variables(array $variables) {
199
- global $scssc;
200
- $scssc->setVariables($variables);
201
  }
202
 
203
  } // End Wp_Scss Class
1
  <?php
2
 
3
+ use Leafo\ScssPhp\Compiler;
4
+
5
  class Wp_Scss {
6
  /**
7
  * Compiling preferences properites
11
  */
12
  public $scss_dir, $css_dir, $compile_method, $scssc, $compile_errors;
13
 
 
14
  /**
15
  * Set values for Wp_Scss::properties
16
  *
23
  * @var array compile_errors - catches errors from compile
24
  */
25
  public function __construct ($scss_dir, $css_dir, $compile_method) {
 
 
 
 
26
  global $scssc;
27
+ $this->scss_dir = $scss_dir;
28
+ $this->css_dir = $css_dir;
29
+ $this->compile_method = $compile_method;
 
30
  $this->compile_errors = array();
31
+ $scssc = new Compiler();
32
+
33
+ $scssc->setFormatter( $compile_method );
34
+ $scssc->setImportPaths( $scss_dir );
35
  }
36
 
37
  /**
167
  }
168
  }
169
 
170
+ public function style_url_enqueued($url){
171
+ global $wp_styles;
172
+ foreach($wp_styles->queue as $wps_name){
173
+ $wps = $wp_styles->registered[$wps_name];
174
+ if($wps->src == $url){
175
+ return $wps;
176
+ }
177
+ }
178
+ return false;
179
+ }
180
  /**
181
  * METHOD ENQUEUE STYLES
182
  * Enqueues all styles in the css directory.
200
  $ver,
201
  $media = 'all' );
202
 
203
+ if(!$this->style_url_enqueued($uri)){
204
+ wp_enqueue_style($name);
205
+ }
206
  }
207
  }
208
  }
209
 
210
  public function set_variables(array $variables) {
211
+ global $scssc;
212
+ $scssc->setVariables($variables);
213
  }
214
 
215
  } // End Wp_Scss Class
options.php CHANGED
@@ -66,90 +66,105 @@ class Wp_Scss_Settings
66
  public function page_init()
67
  {
68
  register_setting(
69
- 'wpscss_options_group', // Option group
70
- 'wpscss_options', // Option name
71
  array( $this, 'sanitize' ) // Sanitize
72
  );
73
 
74
  // Paths to Directories
75
  add_settings_section(
76
- 'wpscss_paths_section', // ID
77
- 'Configure Paths', // Title
78
  array( $this, 'print_paths_info' ), // Callback
79
- 'wpscss_options' // Page
80
  );
 
81
  add_settings_field(
82
- 'wpscss_scss_dir', // ID
83
- 'Scss Location', // Title
84
- array( $this, 'scss_dir_callback' ), // Callback
85
- 'wpscss_options', // Page
86
- 'wpscss_paths_section' // Section
 
 
 
87
  );
 
88
  add_settings_field(
89
- 'wpscss_css_dir',
90
- 'CSS Location',
91
- array( $this, 'css_dir_callback' ),
92
- 'wpscss_options',
93
- 'wpscss_paths_section'
 
 
 
94
  );
95
 
96
  // Compiling Options
97
  add_settings_section(
98
- 'wpscss_compile_section', // ID
99
- 'Compiling Options', // Title
100
  array( $this, 'print_compile_info' ), // Callback
101
- 'wpscss_options' // Page
102
  );
 
103
  add_settings_field(
104
- 'Compiling Mode',
105
- 'Compiling Mode',
106
- array( $this, 'compiling_mode_callback' ), // Callback
107
- 'wpscss_options', // Page
108
- 'wpscss_compile_section' // Section
109
- );
110
- add_settings_field(
111
- 'Error Display',
112
- 'Error Display',
113
- array( $this, 'errors_callback' ), // Callback
114
- 'wpscss_options', // Page
115
- 'wpscss_compile_section' // Section
 
 
 
 
 
 
116
  );
117
 
118
- // Compiling Options
119
- add_settings_section(
120
- 'wpscss_compile_section', // ID
121
- 'Compiling Options', // Title
122
- array( $this, 'print_compile_info' ), // Callback
123
- 'wpscss_options' // Page
124
- );
125
  add_settings_field(
126
- 'Compiling Mode',
127
- 'Compiling Mode',
128
- array( $this, 'compiling_mode_callback' ), // Callback
129
- 'wpscss_options', // Page
130
- 'wpscss_compile_section' // Section
131
- );
132
- add_settings_field(
133
- 'Error Display',
134
- 'Error Display',
135
- array( $this, 'errors_callback' ), // Callback
136
- 'wpscss_options', // Page
137
- 'wpscss_compile_section' // Section
 
 
 
138
  );
139
 
140
  // Enqueuing Options
141
  add_settings_section(
142
- 'wpscss_enqueue_section', // ID
143
- 'Enqueuing Options', // Title
144
  array( $this, 'print_enqueue_info' ), // Callback
145
- 'wpscss_options' // Page
146
  );
 
147
  add_settings_field(
148
- 'Enqueue Stylesheets',
149
- 'Enqueue Stylesheets',
150
- array( $this, 'enqueue_callback' ), // Callback
151
- 'wpscss_options', // Page
152
- 'wpscss_enqueue_section' // Section
 
 
 
153
  );
154
 
155
  }
@@ -160,13 +175,17 @@ class Wp_Scss_Settings
160
  * @param array $input Contains all settings fields as array keys
161
  */
162
  public function sanitize( $input ) {
163
-
164
- if( !empty( $input['wpscss_scss_dir'] ) )
165
- $input['wpscss_scss_dir'] = sanitize_text_field( $input['wpscss_scss_dir'] );
166
-
167
- if( !empty( $input['wpscss_css_dir'] ) )
168
- $input['wpscss_css_dir'] = sanitize_text_field( $input['wpscss_css_dir'] );
169
-
 
 
 
 
170
  return $input;
171
  }
172
 
@@ -184,58 +203,40 @@ class Wp_Scss_Settings
184
  }
185
 
186
  /**
187
- * Text Fields' Callbacks
188
  */
189
- public function scss_dir_callback() {
190
- printf(
191
- '<input type="text" id="scss_dir" name="wpscss_options[scss_dir]" value="%s" />',
192
- esc_attr( $this->options['scss_dir'])
193
- );
194
- }
195
- public function css_dir_callback() {
196
  printf(
197
- '<input type="text" id="css_dir" name="wpscss_options[css_dir]" value="%s" />',
198
- esc_attr( $this->options['css_dir'])
199
  );
200
  }
201
 
202
  /**
203
  * Select Boxes' Callbacks
204
  */
205
- public function compiling_mode_callback() {
206
  $this->options = get_option( 'wpscss_options' );
207
 
208
- $html = '<select id="compiling_options" name="wpscss_options[compiling_options]">';
209
- $html .= '<option value="scss_formatter"' . selected( $this->options['compiling_options'], 'scss_formatter', false) . '>Expanded</option>';
210
- $html .= '<option value="scss_formatter_nested"' . selected( $this->options['compiling_options'], 'scss_formatter_nested', false) . '>Nested</option>';
211
- $html .= '<option value="scss_formatter_compressed"' . selected( $this->options['compiling_options'], 'scss_formatter_compressed', false) . '>Compressed</option>';
212
- $html .= '<option value="scss_formatter_minified"' . selected( $this->options['compiling_options'], 'scss_formatter_minified', false) . '>Minified</option>';
213
  $html .= '</select>';
214
 
215
- echo $html;
216
- }
217
- public function errors_callback() {
218
- $this->options = get_option( 'wpscss_options' );
219
-
220
- $html = '<select id="errors" name="wpscss_options[errors]">';
221
- $html .= '<option value="show"' . selected( $this->options['errors'], 'show', false) . '>Show in Header</option>';
222
- $html .= '<option value="show-logged-in"' . selected( $this->options['errors'], 'show-logged-in', false) . '>Show to Logged In Users</option>';
223
- $html .= '<option value="log"' . selected( $this->options['errors'], 'hide', false) . '>Print to Log</option>';
224
- $html .= '</select>';
225
-
226
- echo $html;
227
  }
228
 
229
  /**
230
  * Checkboxes' Callbacks
231
  */
232
- function enqueue_callback() {
233
- $this->options = get_option( 'wpscss_options' );
234
 
235
- $html = '<input type="checkbox" id="enqueue" name="wpscss_options[enqueue]" value="1"' . checked( 1, isset($this->options['enqueue']) ? $this->options['enqueue'] : 0, false ) . '/>';
236
- $html .= '<label for="enqueue"></label>';
237
 
238
- echo $html;
239
  }
240
 
241
  }
66
  public function page_init()
67
  {
68
  register_setting(
69
+ 'wpscss_options_group', // Option group
70
+ 'wpscss_options', // Option name
71
  array( $this, 'sanitize' ) // Sanitize
72
  );
73
 
74
  // Paths to Directories
75
  add_settings_section(
76
+ 'wpscss_paths_section', // ID
77
+ 'Configure Paths', // Title
78
  array( $this, 'print_paths_info' ), // Callback
79
+ 'wpscss_options' // Page
80
  );
81
+
82
  add_settings_field(
83
+ 'wpscss_scss_dir', // ID
84
+ 'Scss Location', // Title
85
+ array( $this, 'input_text_callback' ), // Callback
86
+ 'wpscss_options', // Page
87
+ 'wpscss_paths_section', // Section
88
+ array( // args
89
+ 'name' => 'scss_dir',
90
+ )
91
  );
92
+
93
  add_settings_field(
94
+ 'wpscss_css_dir', // ID
95
+ 'CSS Location', // Title
96
+ array( $this, 'input_text_callback' ), // Callback
97
+ 'wpscss_options', // Page
98
+ 'wpscss_paths_section', // Section
99
+ array( // args
100
+ 'name' => 'css_dir',
101
+ )
102
  );
103
 
104
  // Compiling Options
105
  add_settings_section(
106
+ 'wpscss_compile_section', // ID
107
+ 'Compiling Options', // Title
108
  array( $this, 'print_compile_info' ), // Callback
109
+ 'wpscss_options' // Page
110
  );
111
+
112
  add_settings_field(
113
+ 'Compiling Mode', // ID
114
+ 'Compiling Mode', // Title
115
+ array( $this, 'input_select_callback' ), // Callback
116
+ 'wpscss_options', // Page
117
+ 'wpscss_compile_section', // Section
118
+ array( // args
119
+ 'name' => 'compiling_options',
120
+ 'type' => apply_filters( 'wp_scss_compiling_modes',
121
+ array(
122
+ 'Leafo\ScssPhp\Formatter\Expanded' => 'Expanded',
123
+ 'Leafo\ScssPhp\Formatter\Nested' => 'Nested',
124
+ 'Leafo\ScssPhp\Formatter\Compressed' => 'Compressed',
125
+ 'Leafo\ScssPhp\Formatter\Compact' => 'Compact',
126
+ 'Leafo\ScssPhp\Formatter\Crunched' => 'Crunched',
127
+ 'Leafo\ScssPhp\Formatter\Debug' => 'Debug'
128
+ )
129
+ )
130
+ )
131
  );
132
 
 
 
 
 
 
 
 
133
  add_settings_field(
134
+ 'Error Display', // ID
135
+ 'Error Display', // Title
136
+ array( $this, 'input_select_callback' ), // Callback
137
+ 'wpscss_options', // Page
138
+ 'wpscss_compile_section', // Section
139
+ array( // args
140
+ 'name' => 'errors',
141
+ 'type' => apply_filters( 'wp_scss_error_diplay',
142
+ array(
143
+ 'show' => 'Show in Header',
144
+ 'show-logged-in' => 'Show to Logged In Users',
145
+ 'hide' => 'Print to Log',
146
+ )
147
+ )
148
+ )
149
  );
150
 
151
  // Enqueuing Options
152
  add_settings_section(
153
+ 'wpscss_enqueue_section', // ID
154
+ 'Enqueuing Options', // Title
155
  array( $this, 'print_enqueue_info' ), // Callback
156
+ 'wpscss_options' // Page
157
  );
158
+
159
  add_settings_field(
160
+ 'Enqueue Stylesheets', // ID
161
+ 'Enqueue Stylesheets', // Title
162
+ array( $this, 'input_checkbox_callback' ), // Callback
163
+ 'wpscss_options', // Page
164
+ 'wpscss_enqueue_section', // Section
165
+ array( // args
166
+ 'name' => 'enqueue'
167
+ )
168
  );
169
 
170
  }
175
  * @param array $input Contains all settings fields as array keys
176
  */
177
  public function sanitize( $input ) {
178
+ foreach( ['scss_dir', 'css_dir'] as $dir ){
179
+ if( !empty( $input[$dir] ) ) {
180
+ $input[$dir] = sanitize_text_field( $input[$dir] );
181
+
182
+ // Add a trailing slash if not already present
183
+ if(substr($input[$dir], -1) != '/'){
184
+ $input[$dir] .= '/';
185
+ }
186
+ }
187
+ }
188
+
189
  return $input;
190
  }
191
 
203
  }
204
 
205
  /**
206
+ * Text Fields' Callback
207
  */
208
+ public function input_text_callback( $args ) {
 
 
 
 
 
 
209
  printf(
210
+ '<input type="text" id="%s" name="wpscss_options[%s]" value="%s" />',
211
+ esc_attr( $args['name'] ), esc_attr( $args['name'] ), esc_attr( $this->options[$args['name']])
212
  );
213
  }
214
 
215
  /**
216
  * Select Boxes' Callbacks
217
  */
218
+ public function input_select_callback( $args ) {
219
  $this->options = get_option( 'wpscss_options' );
220
 
221
+ $html = sprintf( '<select id="%s" name="wpscss_options[%s]">', esc_attr( $args['name'] ), esc_attr( $args['name'] ) );
222
+ foreach( $args['type'] as $value => $title ) {
223
+ $html .= '<option value="' . esc_attr( $value ) . '"' . selected( $this->options[esc_attr( $args['name'] )], esc_attr( $value ), false) . '>' . esc_attr( $title ) . '</option>';
224
+ }
 
225
  $html .= '</select>';
226
 
227
+ echo $html;
 
 
 
 
 
 
 
 
 
 
 
228
  }
229
 
230
  /**
231
  * Checkboxes' Callbacks
232
  */
233
+ public function input_checkbox_callback( $args ) {
234
+ $this->options = get_option( 'wpscss_options' );
235
 
236
+ $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 ) . '/>';
237
+ $html .= '<label for="' . esc_attr( $args['name'] ) . '"></label>';
238
 
239
+ echo $html;
240
  }
241
 
242
  }
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: 4.3
7
- Stable tag: 1.1.9
8
  License: GPLv3 or later
9
  License URI: http://www.gnu.org/copyleft/gpl.html
10
 
@@ -62,6 +62,12 @@ 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.1.9 =
66
  * Added filter to set variables via PHP [@ohlle](https://github.com/ohlle)
67
  * Added option to minify CSS output [@mndewitt](https://github.com/mndewitt)
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: 4.7
7
+ Stable tag: 1.2.0
8
  License: GPLv3 or later
9
  License URI: http://www.gnu.org/copyleft/gpl.html
10
 
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.0 =
66
+ * Fixed a bug where directory inputs were not getting sanitized [@mmcev106](https://github.com/ConnectThink/WP-SCSS/pull/66)
67
+ * Made the missing directory warning also display if a specified path is a file [@mmcev106](https://github.com/ConnectThink/WP-SCSS/pull/65)
68
+ * Added /vendor to .gitignore [@mmcev106](https://github.com/ConnectThink/WP-SCSS/pull/64)
69
+ * Dont enqueue already enqueued stylesheets [@bobbysmith007](https://github.com/ConnectThink/WP-SCSS/pull/61)
70
+
71
  = 1.1.9 =
72
  * Added filter to set variables via PHP [@ohlle](https://github.com/ohlle)
73
  * Added option to minify CSS output [@mndewitt](https://github.com/mndewitt)
scssphp/bin/pscss ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env php
2
+ <?php
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;
27
+ $precision = null;
28
+ $dumpTree = false;
29
+ $inputFile = null;
30
+ $changeDir = false;
31
+ $debugInfo = false;
32
+ $lineNumbers = false;
33
+ $ignoreErrors = false;
34
+ $encoding = false;
35
+
36
+ /**
37
+ * Parse argument
38
+ *
39
+ * @param integer $i
40
+ * @param array $options
41
+ *
42
+ * @return string|null
43
+ */
44
+ function parseArgument(&$i, $options) {
45
+ global $argc;
46
+ global $argv;
47
+
48
+ if (! preg_match('/^(?:' . implode('|', (array) $options) . ')=?(.*)/', $argv[$i], $matches)) {
49
+ return;
50
+ }
51
+
52
+ if (strlen($matches[1])) {
53
+ return $matches[1];
54
+ }
55
+
56
+ if ($i + 1 < $argc) {
57
+ $i++;
58
+
59
+ return $argv[$i];
60
+ }
61
+ }
62
+
63
+ for ($i = 1; $i < $argc; $i++) {
64
+ if ($argv[$i] === '-h' || $argv[$i] === '--help') {
65
+ $exe = $argv[0];
66
+
67
+ $HELP = <<<EOT
68
+ Usage: $exe [options] [input-file]
69
+
70
+ Options include:
71
+
72
+ -h, --help Show this message
73
+ --continue-on-error Continue compilation (as best as possible) when error encountered
74
+ --debug-info Annotate selectors with CSS referring to the source file and line number
75
+ -f=format Set the output format (compact, compressed, crunched, expanded, or nested)
76
+ -i=path Set import path
77
+ --iso8859-1 Use iso8859-1 encoding instead of utf-8 (default utf-8)
78
+ --line-numbers Annotate selectors with comments referring to the source file and line number
79
+ -p=precision Set decimal number precision (default 5)
80
+ -T Dump formatted parse tree
81
+ -v, --version Print the version
82
+
83
+ EOT;
84
+ exit($HELP);
85
+ }
86
+
87
+ if ($argv[$i] === '-v' || $argv[$i] === '--version') {
88
+ exit(Version::VERSION . "\n");
89
+ }
90
+
91
+ if ($argv[$i] === '--continue-on-error') {
92
+ $ignoreErrors = true;
93
+ continue;
94
+ }
95
+
96
+ if ($argv[$i] === '--debug-info') {
97
+ $debugInfo = true;
98
+ continue;
99
+ }
100
+
101
+ if ($argv[$i] === '--iso8859-1') {
102
+ $encoding = 'iso8859-1';
103
+ continue;
104
+ }
105
+
106
+ if ($argv[$i] === '--line-numbers' || $argv[$i] === '--line-comments') {
107
+ $lineNumbers = true;
108
+ continue;
109
+ }
110
+
111
+ if ($argv[$i] === '-T') {
112
+ $dumpTree = true;
113
+ continue;
114
+ }
115
+
116
+ $value = parseArgument($i, array('-f', '--style'));
117
+
118
+ if (isset($value)) {
119
+ $style = $value;
120
+ continue;
121
+ }
122
+
123
+ $value = parseArgument($i, array('-i', '--load_paths'));
124
+
125
+ if (isset($value)) {
126
+ $loadPaths = $value;
127
+ continue;
128
+ }
129
+
130
+ $value = parseArgument($i, array('-p', '--precision'));
131
+
132
+ if (isset($value)) {
133
+ $precision = $value;
134
+ continue;
135
+ }
136
+
137
+ if (file_exists($argv[$i])) {
138
+ $inputFile = $argv[$i];
139
+ continue;
140
+ }
141
+ }
142
+
143
+
144
+ if ($inputFile) {
145
+ $data = file_get_contents($inputFile);
146
+
147
+ $newWorkingDir = dirname(realpath($inputFile));
148
+ $oldWorkingDir = getcwd();
149
+
150
+ if ($oldWorkingDir !== $newWorkingDir) {
151
+ $changeDir = chdir($newWorkingDir);
152
+ $inputFile = basename($inputFile);
153
+ }
154
+ } else {
155
+ $data = '';
156
+
157
+ while (! feof(STDIN)) {
158
+ $data .= fread(STDIN, 8192);
159
+ }
160
+ }
161
+
162
+ if ($dumpTree) {
163
+ $parser = new Parser($inputFile);
164
+
165
+ print_r(json_decode(json_encode($parser->parse($data)), true));
166
+
167
+ exit();
168
+ }
169
+
170
+ $scss = new Compiler();
171
+
172
+ if ($debugInfo && $inputFile) {
173
+ $scss->setLineNumberStyle(Compiler::DEBUG_INFO);
174
+ }
175
+
176
+ if ($lineNumbers && $inputFile) {
177
+ $scss->setLineNumberStyle(Compiler::LINE_COMMENTS);
178
+ }
179
+
180
+ if ($ignoreErrors) {
181
+ $scss->setIgnoreErrors($ignoreErrors);
182
+ }
183
+
184
+ if ($loadPaths) {
185
+ $scss->setImportPaths(explode(PATH_SEPARATOR, $loadPaths));
186
+ }
187
+
188
+ if ($precision) {
189
+ $scss->setNumberPrecision($precision);
190
+ }
191
+
192
+ if ($style) {
193
+ $scss->setFormatter('Leafo\\ScssPhp\\Formatter\\' . ucfirst($style));
194
+ }
195
+
196
+ if ($encoding) {
197
+ $scss->setEncoding($encoding);
198
+ }
199
+
200
+ echo $scss->compile($data, $inputFile);
201
+
202
+ if ($changeDir) {
203
+ chdir($oldWorkingDir);
204
+ }
scssphp/scss.inc.php CHANGED
@@ -1,4600 +1,30 @@
1
  <?php
2
- /**
3
- * SCSS compiler written in PHP
4
- *
5
- * @copyright 2012-2013 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/gpl-license GPL-3.0
8
- * @license http://opensource.org/licenses/MIT MIT
9
- *
10
- * @link http://leafo.net/scssphp
11
- */
12
-
13
- /**
14
- * The scss compiler and parser.
15
- *
16
- * Converting SCSS to CSS is a three stage process. The incoming file is parsed
17
- * by `scss_parser` into a syntax tree, then it is compiled into another tree
18
- * representing the CSS structure by `scssc`. The CSS tree is fed into a
19
- * formatter, like `scss_formatter` which then outputs CSS as a string.
20
- *
21
- * During the first compile, all values are *reduced*, which means that their
22
- * types are brought to the lowest form before being dump as strings. This
23
- * handles math equations, variable dereferences, and the like.
24
- *
25
- * The `parse` function of `scssc` is the entry point.
26
- *
27
- * In summary:
28
- *
29
- * The `scssc` class creates an instance of the parser, feeds it SCSS code,
30
- * then transforms the resulting tree to a CSS tree. This class also holds the
31
- * evaluation context, such as all available mixins and variables at any given
32
- * time.
33
- *
34
- * The `scss_parser` class is only concerned with parsing its input.
35
- *
36
- * The `scss_formatter` takes a CSS tree, and dumps it to a formatted string,
37
- * handling things like indentation.
38
- */
39
-
40
- /**
41
- * SCSS compiler
42
- *
43
- * @author Leaf Corcoran <leafot@gmail.com>
44
- */
45
- class scssc {
46
- static public $VERSION = 'v0.0.12';
47
-
48
- static protected $operatorNames = array(
49
- '+' => "add",
50
- '-' => "sub",
51
- '*' => "mul",
52
- '/' => "div",
53
- '%' => "mod",
54
-
55
- '==' => "eq",
56
- '!=' => "neq",
57
- '<' => "lt",
58
- '>' => "gt",
59
-
60
- '<=' => "lte",
61
- '>=' => "gte",
62
- );
63
-
64
- static protected $namespaces = array(
65
- "special" => "%",
66
- "mixin" => "@",
67
- "function" => "^",
68
- );
69
-
70
- static protected $unitTable = array(
71
- "in" => array(
72
- "in" => 1,
73
- "pt" => 72,
74
- "pc" => 6,
75
- "cm" => 2.54,
76
- "mm" => 25.4,
77
- "px" => 96,
78
- )
79
- );
80
-
81
- static public $true = array("keyword", "true");
82
- static public $false = array("keyword", "false");
83
- static public $null = array("null");
84
-
85
- static public $defaultValue = array("keyword", "");
86
- static public $selfSelector = array("self");
87
-
88
- protected $importPaths = array("");
89
- protected $importCache = array();
90
-
91
- protected $userFunctions = array();
92
- protected $registeredVars = array();
93
-
94
- protected $numberPrecision = 5;
95
-
96
- protected $formatter = "scss_formatter_nested";
97
-
98
- /**
99
- * Compile scss
100
- *
101
- * @param string $code
102
- * @param string $name
103
- *
104
- * @return string
105
- */
106
- public function compile($code, $name = null)
107
- {
108
- $this->indentLevel = -1;
109
- $this->commentsSeen = array();
110
- $this->extends = array();
111
- $this->extendsMap = array();
112
- $this->parsedFiles = array();
113
- $this->env = null;
114
- $this->scope = null;
115
-
116
- $locale = setlocale(LC_NUMERIC, 0);
117
- setlocale(LC_NUMERIC, "C");
118
-
119
- $this->parser = new scss_parser($name);
120
-
121
- $tree = $this->parser->parse($code);
122
-
123
- $this->formatter = new $this->formatter();
124
-
125
- $this->pushEnv($tree);
126
- $this->injectVariables($this->registeredVars);
127
- $this->compileRoot($tree);
128
- $this->popEnv();
129
-
130
- $out = $this->formatter->format($this->scope);
131
-
132
- setlocale(LC_NUMERIC, $locale);
133
-
134
- return $out;
135
- }
136
-
137
- protected function isSelfExtend($target, $origin) {
138
- foreach ($origin as $sel) {
139
- if (in_array($target, $sel)) {
140
- return true;
141
- }
142
- }
143
-
144
- return false;
145
- }
146
-
147
- protected function pushExtends($target, $origin) {
148
- if ($this->isSelfExtend($target, $origin)) {
149
- return;
150
- }
151
-
152
- $i = count($this->extends);
153
- $this->extends[] = array($target, $origin);
154
-
155
- foreach ($target as $part) {
156
- if (isset($this->extendsMap[$part])) {
157
- $this->extendsMap[$part][] = $i;
158
- } else {
159
- $this->extendsMap[$part] = array($i);
160
- }
161
- }
162
- }
163
-
164
- protected function makeOutputBlock($type, $selectors = null) {
165
- $out = new stdClass;
166
- $out->type = $type;
167
- $out->lines = array();
168
- $out->children = array();
169
- $out->parent = $this->scope;
170
- $out->selectors = $selectors;
171
- $out->depth = $this->env->depth;
172
-
173
- return $out;
174
- }
175
-
176
- protected function matchExtendsSingle($single, &$outOrigin) {
177
- $counts = array();
178
- foreach ($single as $part) {
179
- if (!is_string($part)) return false; // hmm
180
-
181
- if (isset($this->extendsMap[$part])) {
182
- foreach ($this->extendsMap[$part] as $idx) {
183
- $counts[$idx] =
184
- isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
185
- }
186
- }
187
- }
188
-
189
- $outOrigin = array();
190
- $found = false;
191
-
192
- foreach ($counts as $idx => $count) {
193
- list($target, $origin) = $this->extends[$idx];
194
-
195
- // check count
196
- if ($count != count($target)) continue;
197
-
198
- // check if target is subset of single
199
- if (array_diff(array_intersect($single, $target), $target)) continue;
200
-
201
- $rem = array_diff($single, $target);
202
-
203
- foreach ($origin as $j => $new) {
204
- // prevent infinite loop when target extends itself
205
- foreach ($new as $new_selector) {
206
- if (!array_diff($single, $new_selector)) {
207
- continue 2;
208
- }
209
- }
210
-
211
- $origin[$j][count($origin[$j]) - 1] = $this->combineSelectorSingle(end($new), $rem);
212
- }
213
-
214
- $outOrigin = array_merge($outOrigin, $origin);
215
-
216
- $found = true;
217
- }
218
-
219
- return $found;
220
- }
221
-
222
- protected function combineSelectorSingle($base, $other) {
223
- $tag = null;
224
- $out = array();
225
-
226
- foreach (array($base, $other) as $single) {
227
- foreach ($single as $part) {
228
- if (preg_match('/^[^\[.#:]/', $part)) {
229
- $tag = $part;
230
- } else {
231
- $out[] = $part;
232
- }
233
- }
234
- }
235
-
236
- if ($tag) {
237
- array_unshift($out, $tag);
238
- }
239
-
240
- return $out;
241
- }
242
-
243
- protected function matchExtends($selector, &$out, $from = 0, $initial=true) {
244
- foreach ($selector as $i => $part) {
245
- if ($i < $from) continue;
246
-
247
- if ($this->matchExtendsSingle($part, $origin)) {
248
- $before = array_slice($selector, 0, $i);
249
- $after = array_slice($selector, $i + 1);
250
-
251
- foreach ($origin as $new) {
252
- $k = 0;
253
-
254
- // remove shared parts
255
- if ($initial) {
256
- foreach ($before as $k => $val) {
257
- if (!isset($new[$k]) || $val != $new[$k]) {
258
- break;
259
- }
260
- }
261
- }
262
-
263
- $result = array_merge(
264
- $before,
265
- $k > 0 ? array_slice($new, $k) : $new,
266
- $after);
267
-
268
-
269
- if ($result == $selector) continue;
270
- $out[] = $result;
271
-
272
- // recursively check for more matches
273
- $this->matchExtends($result, $out, $i, false);
274
-
275
- // selector sequence merging
276
- if (!empty($before) && count($new) > 1) {
277
- $result2 = array_merge(
278
- array_slice($new, 0, -1),
279
- $k > 0 ? array_slice($before, $k) : $before,
280
- array_slice($new, -1),
281
- $after);
282
-
283
- $out[] = $result2;
284
- }
285
- }
286
- }
287
- }
288
- }
289
-
290
- protected function flattenSelectors($block, $parentKey = null) {
291
- if ($block->selectors) {
292
- $selectors = array();
293
- foreach ($block->selectors as $s) {
294
- $selectors[] = $s;
295
- if (!is_array($s)) continue;
296
- // check extends
297
- if (!empty($this->extendsMap)) {
298
- $this->matchExtends($s, $selectors);
299
- }
300
- }
301
-
302
- $block->selectors = array();
303
- $placeholderSelector = false;
304
- foreach ($selectors as $selector) {
305
- if ($this->hasSelectorPlaceholder($selector)) {
306
- $placeholderSelector = true;
307
- continue;
308
- }
309
- $block->selectors[] = $this->compileSelector($selector);
310
- }
311
-
312
- if ($placeholderSelector && 0 == count($block->selectors) && null !== $parentKey) {
313
- unset($block->parent->children[$parentKey]);
314
- return;
315
- }
316
- }
317
-
318
- foreach ($block->children as $key => $child) {
319
- $this->flattenSelectors($child, $key);
320
- }
321
- }
322
-
323
- protected function compileRoot($rootBlock)
324
- {
325
- $this->scope = $this->makeOutputBlock('root');
326
-
327
- $this->compileChildren($rootBlock->children, $this->scope);
328
- $this->flattenSelectors($this->scope);
329
- }
330
-
331
- protected function compileMedia($media) {
332
- $this->pushEnv($media);
333
-
334
- $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
335
-
336
- if (!empty($mediaQuery)) {
337
-
338
- $this->scope = $this->makeOutputBlock("media", array($mediaQuery));
339
-
340
- $parentScope = $this->mediaParent($this->scope);
341
-
342
- $parentScope->children[] = $this->scope;
343
-
344
- // top level properties in a media cause it to be wrapped
345
- $needsWrap = false;
346
- foreach ($media->children as $child) {
347
- $type = $child[0];
348
- if ($type !== 'block' && $type !== 'media' && $type !== 'directive') {
349
- $needsWrap = true;
350
- break;
351
- }
352
- }
353
-
354
- if ($needsWrap) {
355
- $wrapped = (object)array(
356
- "selectors" => array(),
357
- "children" => $media->children
358
- );
359
- $media->children = array(array("block", $wrapped));
360
- }
361
-
362
- $this->compileChildren($media->children, $this->scope);
363
-
364
- $this->scope = $this->scope->parent;
365
- }
366
-
367
- $this->popEnv();
368
- }
369
-
370
- protected function mediaParent($scope) {
371
- while (!empty($scope->parent)) {
372
- if (!empty($scope->type) && $scope->type != "media") {
373
- break;
374
- }
375
- $scope = $scope->parent;
376
- }
377
-
378
- return $scope;
379
- }
380
-
381
- // TODO refactor compileNestedBlock and compileMedia into same thing
382
- protected function compileNestedBlock($block, $selectors) {
383
- $this->pushEnv($block);
384
-
385
- $this->scope = $this->makeOutputBlock($block->type, $selectors);
386
- $this->scope->parent->children[] = $this->scope;
387
- $this->compileChildren($block->children, $this->scope);
388
-
389
- $this->scope = $this->scope->parent;
390
- $this->popEnv();
391
- }
392
-
393
- /**
394
- * Recursively compiles a block.
395
- *
396
- * A block is analogous to a CSS block in most cases. A single SCSS document
397
- * is encapsulated in a block when parsed, but it does not have parent tags
398
- * so all of its children appear on the root level when compiled.
399
- *
400
- * Blocks are made up of selectors and children.
401
- *
402
- * The children of a block are just all the blocks that are defined within.
403
- *
404
- * Compiling the block involves pushing a fresh environment on the stack,
405
- * and iterating through the props, compiling each one.
406
- *
407
- * @see scss::compileChild()
408
- *
409
- * @param \StdClass $block
410
- */
411
- protected function compileBlock($block) {
412
- $env = $this->pushEnv($block);
413
-
414
- $env->selectors =
415
- array_map(array($this, "evalSelector"), $block->selectors);
416
-
417
- $out = $this->makeOutputBlock(null, $this->multiplySelectors($env));
418
- $this->scope->children[] = $out;
419
- $this->compileChildren($block->children, $out);
420
-
421
- $this->popEnv();
422
- }
423
-
424
- // joins together .classes and #ids
425
- protected function flattenSelectorSingle($single) {
426
- $joined = array();
427
- foreach ($single as $part) {
428
- if (empty($joined) ||
429
- !is_string($part) ||
430
- preg_match('/[\[.:#%]/', $part))
431
- {
432
- $joined[] = $part;
433
- continue;
434
- }
435
-
436
- if (is_array(end($joined))) {
437
- $joined[] = $part;
438
- } else {
439
- $joined[count($joined) - 1] .= $part;
440
- }
441
- }
442
-
443
- return $joined;
444
- }
445
-
446
- // replaces all the interpolates
447
- protected function evalSelector($selector) {
448
- return array_map(array($this, "evalSelectorPart"), $selector);
449
- }
450
-
451
- protected function evalSelectorPart($piece) {
452
- foreach ($piece as &$p) {
453
- if (!is_array($p)) continue;
454
-
455
- switch ($p[0]) {
456
- case "interpolate":
457
- $p = $this->compileValue($p);
458
- break;
459
- case "string":
460
- $p = $this->compileValue($p);
461
- break;
462
- }
463
- }
464
-
465
- return $this->flattenSelectorSingle($piece);
466
- }
467
-
468
- // compiles to string
469
- // self(&) should have been replaced by now
470
- protected function compileSelector($selector) {
471
- if (!is_array($selector)) return $selector; // media and the like
472
-
473
- return implode(" ", array_map(
474
- array($this, "compileSelectorPart"), $selector));
475
- }
476
-
477
- protected function compileSelectorPart($piece) {
478
- foreach ($piece as &$p) {
479
- if (!is_array($p)) continue;
480
-
481
- switch ($p[0]) {
482
- case "self":
483
- $p = "&";
484
- break;
485
- default:
486
- $p = $this->compileValue($p);
487
- break;
488
- }
489
- }
490
-
491
- return implode($piece);
492
- }
493
-
494
- protected function hasSelectorPlaceholder($selector)
495
- {
496
- if (!is_array($selector)) return false;
497
-
498
- foreach ($selector as $parts) {
499
- foreach ($parts as $part) {
500
- if ('%' == $part[0]) {
501
- return true;
502
- }
503
- }
504
- }
505
-
506
- return false;
507
- }
508
-
509
- protected function compileChildren($stms, $out) {
510
- foreach ($stms as $stm) {
511
- $ret = $this->compileChild($stm, $out);
512
- if (isset($ret)) return $ret;
513
- }
514
- }
515
-
516
- protected function compileMediaQuery($queryList) {
517
- $out = "@media";
518
- $first = true;
519
- foreach ($queryList as $query){
520
- $type = null;
521
- $parts = array();
522
- foreach ($query as $q) {
523
- switch ($q[0]) {
524
- case "mediaType":
525
- if ($type) {
526
- $type = $this->mergeMediaTypes($type, array_map(array($this, "compileValue"), array_slice($q, 1)));
527
- if (empty($type)) { // merge failed
528
- return null;
529
- }
530
- } else {
531
- $type = array_map(array($this, "compileValue"), array_slice($q, 1));
532
- }
533
- break;
534
- case "mediaExp":
535
- if (isset($q[2])) {
536
- $parts[] = "(". $this->compileValue($q[1]) . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ")";
537
- } else {
538
- $parts[] = "(" . $this->compileValue($q[1]) . ")";
539
- }
540
- break;
541
- }
542
- }
543
- if ($type) {
544
- array_unshift($parts, implode(' ', array_filter($type)));
545
- }
546
- if (!empty($parts)) {
547
- if ($first) {
548
- $first = false;
549
- $out .= " ";
550
- } else {
551
- $out .= $this->formatter->tagSeparator;
552
- }
553
- $out .= implode(" and ", $parts);
554
- }
555
- }
556
- return $out;
557
- }
558
-
559
- protected function mergeMediaTypes($type1, $type2) {
560
- if (empty($type1)) {
561
- return $type2;
562
- }
563
- if (empty($type2)) {
564
- return $type1;
565
- }
566
- $m1 = '';
567
- $t1 = '';
568
- if (count($type1) > 1) {
569
- $m1= strtolower($type1[0]);
570
- $t1= strtolower($type1[1]);
571
- } else {
572
- $t1 = strtolower($type1[0]);
573
- }
574
- $m2 = '';
575
- $t2 = '';
576
- if (count($type2) > 1) {
577
- $m2 = strtolower($type2[0]);
578
- $t2 = strtolower($type2[1]);
579
- } else {
580
- $t2 = strtolower($type2[0]);
581
- }
582
- if (($m1 == 'not') ^ ($m2 == 'not')) {
583
- if ($t1 == $t2) {
584
- return null;
585
- }
586
- return array(
587
- $m1 == 'not' ? $m2 : $m1,
588
- $m1 == 'not' ? $t2 : $t1
589
- );
590
- } elseif ($m1 == 'not' && $m2 == 'not') {
591
- # CSS has no way of representing "neither screen nor print"
592
- if ($t1 != $t2) {
593
- return null;
594
- }
595
- return array('not', $t1);
596
- } elseif ($t1 != $t2) {
597
- return null;
598
- } else { // t1 == t2, neither m1 nor m2 are "not"
599
- return array(empty($m1)? $m2 : $m1, $t1);
600
- }
601
- }
602
-
603
- // returns true if the value was something that could be imported
604
- protected function compileImport($rawPath, $out) {
605
- if ($rawPath[0] == "string") {
606
- $path = $this->compileStringContent($rawPath);
607
- if ($path = $this->findImport($path)) {
608
- $this->importFile($path, $out);
609
- return true;
610
- }
611
- return false;
612
- }
613
- if ($rawPath[0] == "list") {
614
- // handle a list of strings
615
- if (count($rawPath[2]) == 0) return false;
616
- foreach ($rawPath[2] as $path) {
617
- if ($path[0] != "string") return false;
618
- }
619
-
620
- foreach ($rawPath[2] as $path) {
621
- $this->compileImport($path, $out);
622
- }
623
-
624
- return true;
625
- }
626
-
627
- return false;
628
- }
629
-
630
- // return a value to halt execution
631
- protected function compileChild($child, $out) {
632
- $this->sourcePos = isset($child[-1]) ? $child[-1] : -1;
633
- $this->sourceParser = isset($child[-2]) ? $child[-2] : $this->parser;
634
-
635
- switch ($child[0]) {
636
- case "import":
637
- list(,$rawPath) = $child;
638
- $rawPath = $this->reduce($rawPath);
639
- if (!$this->compileImport($rawPath, $out)) {
640
- $out->lines[] = "@import " . $this->compileValue($rawPath) . ";";
641
- }
642
- break;
643
- case "directive":
644
- list(, $directive) = $child;
645
- $s = "@" . $directive->name;
646
- if (!empty($directive->value)) {
647
- $s .= " " . $this->compileValue($directive->value);
648
- }
649
- $this->compileNestedBlock($directive, array($s));
650
- break;
651
- case "media":
652
- $this->compileMedia($child[1]);
653
- break;
654
- case "block":
655
- $this->compileBlock($child[1]);
656
- break;
657
- case "charset":
658
- $out->lines[] = "@charset ".$this->compileValue($child[1]).";";
659
- break;
660
- case "assign":
661
- list(,$name, $value) = $child;
662
- if ($name[0] == "var") {
663
- $isDefault = !empty($child[3]);
664
-
665
- if ($isDefault) {
666
- $existingValue = $this->get($name[1], true);
667
- $shouldSet = $existingValue === true || $existingValue == self::$null;
668
- }
669
-
670
- if (!$isDefault || $shouldSet) {
671
- $this->set($name[1], $this->reduce($value));
672
- }
673
- break;
674
- }
675
-
676
- // if the value reduces to null from something else then
677
- // the property should be discarded
678
- if ($value[0] != "null") {
679
- $value = $this->reduce($value);
680
- if ($value[0] == "null") {
681
- break;
682
- }
683
- }
684
-
685
- $compiledValue = $this->compileValue($value);
686
- $out->lines[] = $this->formatter->property(
687
- $this->compileValue($name),
688
- $compiledValue);
689
- break;
690
- case "comment":
691
- $out->lines[] = $child[1];
692
- break;
693
- case "mixin":
694
- case "function":
695
- list(,$block) = $child;
696
- $this->set(self::$namespaces[$block->type] . $block->name, $block);
697
- break;
698
- case "extend":
699
- list(, $selectors) = $child;
700
- foreach ($selectors as $sel) {
701
- // only use the first one
702
- $sel = current($this->evalSelector($sel));
703
- $this->pushExtends($sel, $out->selectors);
704
- }
705
- break;
706
- case "if":
707
- list(, $if) = $child;
708
- if ($this->isTruthy($this->reduce($if->cond, true))) {
709
- return $this->compileChildren($if->children, $out);
710
- } else {
711
- foreach ($if->cases as $case) {
712
- if ($case->type == "else" ||
713
- $case->type == "elseif" && $this->isTruthy($this->reduce($case->cond)))
714
- {
715
- return $this->compileChildren($case->children, $out);
716
- }
717
- }
718
- }
719
- break;
720
- case "return":
721
- return $this->reduce($child[1], true);
722
- case "each":
723
- list(,$each) = $child;
724
- $list = $this->coerceList($this->reduce($each->list));
725
- foreach ($list[2] as $item) {
726
- $this->pushEnv();
727
- $this->set($each->var, $item);
728
- // TODO: allow return from here
729
- $this->compileChildren($each->children, $out);
730
- $this->popEnv();
731
- }
732
- break;
733
- case "while":
734
- list(,$while) = $child;
735
- while ($this->isTruthy($this->reduce($while->cond, true))) {
736
- $ret = $this->compileChildren($while->children, $out);
737
- if ($ret) return $ret;
738
- }
739
- break;
740
- case "for":
741
- list(,$for) = $child;
742
- $start = $this->reduce($for->start, true);
743
- $start = $start[1];
744
- $end = $this->reduce($for->end, true);
745
- $end = $end[1];
746
- $d = $start < $end ? 1 : -1;
747
-
748
- while (true) {
749
- if ((!$for->until && $start - $d == $end) ||
750
- ($for->until && $start == $end))
751
- {
752
- break;
753
- }
754
-
755
- $this->set($for->var, array("number", $start, ""));
756
- $start += $d;
757
-
758
- $ret = $this->compileChildren($for->children, $out);
759
- if ($ret) return $ret;
760
- }
761
-
762
- break;
763
- case "nestedprop":
764
- list(,$prop) = $child;
765
- $prefixed = array();
766
- $prefix = $this->compileValue($prop->prefix) . "-";
767
- foreach ($prop->children as $child) {
768
- if ($child[0] == "assign") {
769
- array_unshift($child[1][2], $prefix);
770
- }
771
- if ($child[0] == "nestedprop") {
772
- array_unshift($child[1]->prefix[2], $prefix);
773
- }
774
- $prefixed[] = $child;
775
- }
776
- $this->compileChildren($prefixed, $out);
777
- break;
778
- case "include": // including a mixin
779
- list(,$name, $argValues, $content) = $child;
780
- $mixin = $this->get(self::$namespaces["mixin"] . $name, false);
781
- if (!$mixin) {
782
- $this->throwError("Undefined mixin $name");
783
- }
784
-
785
- $callingScope = $this->env;
786
-
787
- // push scope, apply args
788
- $this->pushEnv();
789
- if ($this->env->depth > 0) {
790
- $this->env->depth--;
791
- }
792
-
793
- if (isset($content)) {
794
- $content->scope = $callingScope;
795
- $this->setRaw(self::$namespaces["special"] . "content", $content);
796
- }
797
-
798
- if (isset($mixin->args)) {
799
- $this->applyArguments($mixin->args, $argValues);
800
- }
801
-
802
- foreach ($mixin->children as $child) {
803
- $this->compileChild($child, $out);
804
- }
805
-
806
- $this->popEnv();
807
-
808
- break;
809
- case "mixin_content":
810
- $content = $this->get(self::$namespaces["special"] . "content");
811
- if (!isset($content)) {
812
- $this->throwError("Expected @content inside of mixin");
813
- }
814
-
815
- $strongTypes = array('include', 'block', 'for', 'while');
816
- foreach ($content->children as $child) {
817
- $this->storeEnv = (in_array($child[0], $strongTypes))
818
- ? null
819
- : $content->scope;
820
-
821
- $this->compileChild($child, $out);
822
- }
823
-
824
- unset($this->storeEnv);
825
- break;
826
- case "debug":
827
- list(,$value, $pos) = $child;
828
- $line = $this->parser->getLineNo($pos);
829
- $value = $this->compileValue($this->reduce($value, true));
830
- fwrite(STDERR, "Line $line DEBUG: $value\n");
831
- break;
832
- default:
833
- $this->throwError("unknown child type: $child[0]");
834
- }
835
- }
836
-
837
- protected function expToString($exp) {
838
- list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
839
- $content = array($this->reduce($left));
840
- if ($whiteLeft) $content[] = " ";
841
- $content[] = $op;
842
- if ($whiteRight) $content[] = " ";
843
- $content[] = $this->reduce($right);
844
- return array("string", "", $content);
845
- }
846
-
847
- protected function isTruthy($value) {
848
- return $value != self::$false && $value != self::$null;
849
- }
850
-
851
- // should $value cause its operand to eval
852
- protected function shouldEval($value) {
853
- switch ($value[0]) {
854
- case "exp":
855
- if ($value[1] == "/") {
856
- return $this->shouldEval($value[2], $value[3]);
857
- }
858
- case "var":
859
- case "fncall":
860
- return true;
861
- }
862
- return false;
863
- }
864
-
865
- protected function reduce($value, $inExp = false) {
866
- list($type) = $value;
867
- switch ($type) {
868
- case "exp":
869
- list(, $op, $left, $right, $inParens) = $value;
870
- $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
871
-
872
- $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
873
-
874
- $left = $this->reduce($left, true);
875
- $right = $this->reduce($right, true);
876
-
877
- // only do division in special cases
878
- if ($opName == "div" && !$inParens && !$inExp) {
879
- if ($left[0] != "color" && $right[0] != "color") {
880
- return $this->expToString($value);
881
- }
882
- }
883
-
884
- $left = $this->coerceForExpression($left);
885
- $right = $this->coerceForExpression($right);
886
-
887
- $ltype = $left[0];
888
- $rtype = $right[0];
889
-
890
- // this tries:
891
- // 1. op_[op name]_[left type]_[right type]
892
- // 2. op_[left type]_[right type] (passing the op as first arg
893
- // 3. op_[op name]
894
- $fn = "op_${opName}_${ltype}_${rtype}";
895
- if (is_callable(array($this, $fn)) ||
896
- (($fn = "op_${ltype}_${rtype}") &&
897
- is_callable(array($this, $fn)) &&
898
- $passOp = true) ||
899
- (($fn = "op_${opName}") &&
900
- is_callable(array($this, $fn)) &&
901
- $genOp = true))
902
- {
903
- $unitChange = false;
904
- if (!isset($genOp) &&
905
- $left[0] == "number" && $right[0] == "number")
906
- {
907
- if ($opName == "mod" && $right[2] != "") {
908
- $this->throwError("Cannot modulo by a number with units: $right[1]$right[2].");
909
- }
910
-
911
- $unitChange = true;
912
- $emptyUnit = $left[2] == "" || $right[2] == "";
913
- $targetUnit = "" != $left[2] ? $left[2] : $right[2];
914
-
915
- if ($opName != "mul") {
916
- $left[2] = "" != $left[2] ? $left[2] : $targetUnit;
917
- $right[2] = "" != $right[2] ? $right[2] : $targetUnit;
918
- }
919
-
920
- if ($opName != "mod") {
921
- $left = $this->normalizeNumber($left);
922
- $right = $this->normalizeNumber($right);
923
- }
924
-
925
- if ($opName == "div" && !$emptyUnit && $left[2] == $right[2]) {
926
- $targetUnit = "";
927
- }
928
-
929
- if ($opName == "mul") {
930
- $left[2] = "" != $left[2] ? $left[2] : $right[2];
931
- $right[2] = "" != $right[2] ? $right[2] : $left[2];
932
- } elseif ($opName == "div" && $left[2] == $right[2]) {
933
- $left[2] = "";
934
- $right[2] = "";
935
- }
936
- }
937
-
938
- $shouldEval = $inParens || $inExp;
939
- if (isset($passOp)) {
940
- $out = $this->$fn($op, $left, $right, $shouldEval);
941
- } else {
942
- $out = $this->$fn($left, $right, $shouldEval);
943
- }
944
-
945
- if (isset($out)) {
946
- if ($unitChange && $out[0] == "number") {
947
- $out = $this->coerceUnit($out, $targetUnit);
948
- }
949
- return $out;
950
- }
951
- }
952
-
953
- return $this->expToString($value);
954
- case "unary":
955
- list(, $op, $exp, $inParens) = $value;
956
- $inExp = $inExp || $this->shouldEval($exp);
957
-
958
- $exp = $this->reduce($exp);
959
- if ($exp[0] == "number") {
960
- switch ($op) {
961
- case "+":
962
- return $exp;
963
- case "-":
964
- $exp[1] *= -1;
965
- return $exp;
966
- }
967
- }
968
-
969
- if ($op == "not") {
970
- if ($inExp || $inParens) {
971
- if ($exp == self::$false) {
972
- return self::$true;
973
- } else {
974
- return self::$false;
975
- }
976
- } else {
977
- $op = $op . " ";
978
- }
979
- }
980
-
981
- return array("string", "", array($op, $exp));
982
- case "var":
983
- list(, $name) = $value;
984
- return $this->reduce($this->get($name));
985
- case "list":
986
- foreach ($value[2] as &$item) {
987
- $item = $this->reduce($item);
988
- }
989
- return $value;
990
- case "string":
991
- foreach ($value[2] as &$item) {
992
- if (is_array($item)) {
993
- $item = $this->reduce($item);
994
- }
995
- }
996
- return $value;
997
- case "interpolate":
998
- $value[1] = $this->reduce($value[1]);
999
- return $value;
1000
- case "fncall":
1001
- list(,$name, $argValues) = $value;
1002
-
1003
- // user defined function?
1004
- $func = $this->get(self::$namespaces["function"] . $name, false);
1005
- if ($func) {
1006
- $this->pushEnv();
1007
-
1008
- // set the args
1009
- if (isset($func->args)) {
1010
- $this->applyArguments($func->args, $argValues);
1011
- }
1012
-
1013
- // throw away lines and children
1014
- $tmp = (object)array(
1015
- "lines" => array(),
1016
- "children" => array()
1017
- );
1018
- $ret = $this->compileChildren($func->children, $tmp);
1019
- $this->popEnv();
1020
-
1021
- return !isset($ret) ? self::$defaultValue : $ret;
1022
- }
1023
-
1024
- // built in function
1025
- if ($this->callBuiltin($name, $argValues, $returnValue)) {
1026
- return $returnValue;
1027
- }
1028
-
1029
- // need to flatten the arguments into a list
1030
- $listArgs = array();
1031
- foreach ((array)$argValues as $arg) {
1032
- if (empty($arg[0])) {
1033
- $listArgs[] = $this->reduce($arg[1]);
1034
- }
1035
- }
1036
- return array("function", $name, array("list", ",", $listArgs));
1037
- default:
1038
- return $value;
1039
- }
1040
- }
1041
-
1042
- public function normalizeValue($value) {
1043
- $value = $this->coerceForExpression($this->reduce($value));
1044
- list($type) = $value;
1045
-
1046
- switch ($type) {
1047
- case "list":
1048
- $value = $this->extractInterpolation($value);
1049
- if ($value[0] != "list") {
1050
- return array("keyword", $this->compileValue($value));
1051
- }
1052
- foreach ($value[2] as $key => $item) {
1053
- $value[2][$key] = $this->normalizeValue($item);
1054
- }
1055
- return $value;
1056
- case "number":
1057
- return $this->normalizeNumber($value);
1058
- default:
1059
- return $value;
1060
- }
1061
- }
1062
-
1063
- // just does physical lengths for now
1064
- protected function normalizeNumber($number) {
1065
- list(, $value, $unit) = $number;
1066
- if (isset(self::$unitTable["in"][$unit])) {
1067
- $conv = self::$unitTable["in"][$unit];
1068
- return array("number", $value / $conv, "in");
1069
- }
1070
- return $number;
1071
- }
1072
-
1073
- // $number should be normalized
1074
- protected function coerceUnit($number, $unit) {
1075
- list(, $value, $baseUnit) = $number;
1076
- if (isset(self::$unitTable[$baseUnit][$unit])) {
1077
- $value = $value * self::$unitTable[$baseUnit][$unit];
1078
- }
1079
-
1080
- return array("number", $value, $unit);
1081
- }
1082
-
1083
- protected function op_add_number_number($left, $right) {
1084
- return array("number", $left[1] + $right[1], $left[2]);
1085
- }
1086
-
1087
- protected function op_mul_number_number($left, $right) {
1088
- return array("number", $left[1] * $right[1], $left[2]);
1089
- }
1090
-
1091
- protected function op_sub_number_number($left, $right) {
1092
- return array("number", $left[1] - $right[1], $left[2]);
1093
- }
1094
-
1095
- protected function op_div_number_number($left, $right) {
1096
- return array("number", $left[1] / $right[1], $left[2]);
1097
- }
1098
-
1099
- protected function op_mod_number_number($left, $right) {
1100
- return array("number", $left[1] % $right[1], $left[2]);
1101
- }
1102
-
1103
- // adding strings
1104
- protected function op_add($left, $right) {
1105
- if ($strLeft = $this->coerceString($left)) {
1106
- if ($right[0] == "string") {
1107
- $right[1] = "";
1108
- }
1109
- $strLeft[2][] = $right;
1110
- return $strLeft;
1111
- }
1112
-
1113
- if ($strRight = $this->coerceString($right)) {
1114
- if ($left[0] == "string") {
1115
- $left[1] = "";
1116
- }
1117
- array_unshift($strRight[2], $left);
1118
- return $strRight;
1119
- }
1120
- }
1121
-
1122
- protected function op_and($left, $right, $shouldEval) {
1123
- if (!$shouldEval) return;
1124
- if ($left != self::$false) return $right;
1125
- return $left;
1126
- }
1127
-
1128
- protected function op_or($left, $right, $shouldEval) {
1129
- if (!$shouldEval) return;
1130
- if ($left != self::$false) return $left;
1131
- return $right;
1132
- }
1133
-
1134
- protected function op_color_color($op, $left, $right) {
1135
- $out = array('color');
1136
- foreach (range(1, 3) as $i) {
1137
- $lval = isset($left[$i]) ? $left[$i] : 0;
1138
- $rval = isset($right[$i]) ? $right[$i] : 0;
1139
- switch ($op) {
1140
- case '+':
1141
- $out[] = $lval + $rval;
1142
- break;
1143
- case '-':
1144
- $out[] = $lval - $rval;
1145
- break;
1146
- case '*':
1147
- $out[] = $lval * $rval;
1148
- break;
1149
- case '%':
1150
- $out[] = $lval % $rval;
1151
- break;
1152
- case '/':
1153
- if ($rval == 0) {
1154
- $this->throwError("color: Can't divide by zero");
1155
- }
1156
- $out[] = $lval / $rval;
1157
- break;
1158
- case "==":
1159
- return $this->op_eq($left, $right);
1160
- case "!=":
1161
- return $this->op_neq($left, $right);
1162
- default:
1163
- $this->throwError("color: unknown op $op");
1164
- }
1165
- }
1166
-
1167
- if (isset($left[4])) $out[4] = $left[4];
1168
- elseif (isset($right[4])) $out[4] = $right[4];
1169
-
1170
- return $this->fixColor($out);
1171
- }
1172
-
1173
- protected function op_color_number($op, $left, $right) {
1174
- $value = $right[1];
1175
- return $this->op_color_color($op, $left,
1176
- array("color", $value, $value, $value));
1177
- }
1178
-
1179
- protected function op_number_color($op, $left, $right) {
1180
- $value = $left[1];
1181
- return $this->op_color_color($op,
1182
- array("color", $value, $value, $value), $right);
1183
- }
1184
-
1185
- protected function op_eq($left, $right) {
1186
- if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
1187
- $lStr[1] = "";
1188
- $rStr[1] = "";
1189
- return $this->toBool($this->compileValue($lStr) == $this->compileValue($rStr));
1190
- }
1191
-
1192
- return $this->toBool($left == $right);
1193
- }
1194
-
1195
- protected function op_neq($left, $right) {
1196
- return $this->toBool($left != $right);
1197
- }
1198
-
1199
- protected function op_gte_number_number($left, $right) {
1200
- return $this->toBool($left[1] >= $right[1]);
1201
- }
1202
-
1203
- protected function op_gt_number_number($left, $right) {
1204
- return $this->toBool($left[1] > $right[1]);
1205
- }
1206
-
1207
- protected function op_lte_number_number($left, $right) {
1208
- return $this->toBool($left[1] <= $right[1]);
1209
- }
1210
-
1211
- protected function op_lt_number_number($left, $right) {
1212
- return $this->toBool($left[1] < $right[1]);
1213
- }
1214
-
1215
- public function toBool($thing) {
1216
- return $thing ? self::$true : self::$false;
1217
- }
1218
-
1219
- /**
1220
- * Compiles a primitive value into a CSS property value.
1221
- *
1222
- * Values in scssphp are typed by being wrapped in arrays, their format is
1223
- * typically:
1224
- *
1225
- * array(type, contents [, additional_contents]*)
1226
- *
1227
- * The input is expected to be reduced. This function will not work on
1228
- * things like expressions and variables.
1229
- *
1230
- * @param array $value
1231
- */
1232
- protected function compileValue($value) {
1233
- $value = $this->reduce($value);
1234
-
1235
- list($type) = $value;
1236
- switch ($type) {
1237
- case "keyword":
1238
- return $value[1];
1239
- case "color":
1240
- // [1] - red component (either number for a %)
1241
- // [2] - green component
1242
- // [3] - blue component
1243
- // [4] - optional alpha component
1244
- list(, $r, $g, $b) = $value;
1245
-
1246
- $r = round($r);
1247
- $g = round($g);
1248
- $b = round($b);
1249
-
1250
- if (count($value) == 5 && $value[4] != 1) { // rgba
1251
- return 'rgba('.$r.', '.$g.', '.$b.', '.$value[4].')';
1252
- }
1253
-
1254
- $h = sprintf("#%02x%02x%02x", $r, $g, $b);
1255
-
1256
- // Converting hex color to short notation (e.g. #003399 to #039)
1257
- if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
1258
- $h = '#' . $h[1] . $h[3] . $h[5];
1259
- }
1260
-
1261
- return $h;
1262
- case "number":
1263
- return round($value[1], $this->numberPrecision) . $value[2];
1264
- case "string":
1265
- return $value[1] . $this->compileStringContent($value) . $value[1];
1266
- case "function":
1267
- $args = !empty($value[2]) ? $this->compileValue($value[2]) : "";
1268
- return "$value[1]($args)";
1269
- case "list":
1270
- $value = $this->extractInterpolation($value);
1271
- if ($value[0] != "list") return $this->compileValue($value);
1272
-
1273
- list(, $delim, $items) = $value;
1274
-
1275
- $filtered = array();
1276
- foreach ($items as $item) {
1277
- if ($item[0] == "null") continue;
1278
- $filtered[] = $this->compileValue($item);
1279
- }
1280
-
1281
- return implode("$delim ", $filtered);
1282
- case "interpolated": # node created by extractInterpolation
1283
- list(, $interpolate, $left, $right) = $value;
1284
- list(,, $whiteLeft, $whiteRight) = $interpolate;
1285
-
1286
- $left = count($left[2]) > 0 ?
1287
- $this->compileValue($left).$whiteLeft : "";
1288
-
1289
- $right = count($right[2]) > 0 ?
1290
- $whiteRight.$this->compileValue($right) : "";
1291
-
1292
- return $left.$this->compileValue($interpolate).$right;
1293
-
1294
- case "interpolate": # raw parse node
1295
- list(, $exp) = $value;
1296
-
1297
- // strip quotes if it's a string
1298
- $reduced = $this->reduce($exp);
1299
- switch ($reduced[0]) {
1300
- case "string":
1301
- $reduced = array("keyword",
1302
- $this->compileStringContent($reduced));
1303
- break;
1304
- case "null":
1305
- $reduced = array("keyword", "");
1306
- }
1307
-
1308
- return $this->compileValue($reduced);
1309
- case "null":
1310
- return "null";
1311
- default:
1312
- $this->throwError("unknown value type: $type");
1313
- }
1314
- }
1315
-
1316
- protected function compileStringContent($string) {
1317
- $parts = array();
1318
- foreach ($string[2] as $part) {
1319
- if (is_array($part)) {
1320
- $parts[] = $this->compileValue($part);
1321
- } else {
1322
- $parts[] = $part;
1323
- }
1324
- }
1325
-
1326
- return implode($parts);
1327
- }
1328
-
1329
- // doesn't need to be recursive, compileValue will handle that
1330
- protected function extractInterpolation($list) {
1331
- $items = $list[2];
1332
- foreach ($items as $i => $item) {
1333
- if ($item[0] == "interpolate") {
1334
- $before = array("list", $list[1], array_slice($items, 0, $i));
1335
- $after = array("list", $list[1], array_slice($items, $i + 1));
1336
- return array("interpolated", $item, $before, $after);
1337
- }
1338
- }
1339
- return $list;
1340
- }
1341
-
1342
- // find the final set of selectors
1343
- protected function multiplySelectors($env) {
1344
- $envs = array();
1345
- while (null !== $env) {
1346
- if (!empty($env->selectors)) {
1347
- $envs[] = $env;
1348
- }
1349
- $env = $env->parent;
1350
- };
1351
-
1352
- $selectors = array();
1353
- $parentSelectors = array(array());
1354
- while ($env = array_pop($envs)) {
1355
- $selectors = array();
1356
- foreach ($env->selectors as $selector) {
1357
- foreach ($parentSelectors as $parent) {
1358
- $selectors[] = $this->joinSelectors($parent, $selector);
1359
- }
1360
- }
1361
- $parentSelectors = $selectors;
1362
- }
1363
-
1364
- return $selectors;
1365
- }
1366
-
1367
- // looks for & to replace, or append parent before child
1368
- protected function joinSelectors($parent, $child) {
1369
- $setSelf = false;
1370
- $out = array();
1371
- foreach ($child as $part) {
1372
- $newPart = array();
1373
- foreach ($part as $p) {
1374
- if ($p == self::$selfSelector) {
1375
- $setSelf = true;
1376
- foreach ($parent as $i => $parentPart) {
1377
- if ($i > 0) {
1378
- $out[] = $newPart;
1379
- $newPart = array();
1380
- }
1381
-
1382
- foreach ($parentPart as $pp) {
1383
- $newPart[] = $pp;
1384
- }
1385
- }
1386
- } else {
1387
- $newPart[] = $p;
1388
- }
1389
- }
1390
-
1391
- $out[] = $newPart;
1392
- }
1393
-
1394
- return $setSelf ? $out : array_merge($parent, $child);
1395
- }
1396
-
1397
- protected function multiplyMedia($env, $childQueries = null) {
1398
- if (!isset($env) ||
1399
- !empty($env->block->type) && $env->block->type != "media")
1400
- {
1401
- return $childQueries;
1402
- }
1403
-
1404
- // plain old block, skip
1405
- if (empty($env->block->type)) {
1406
- return $this->multiplyMedia($env->parent, $childQueries);
1407
- }
1408
-
1409
- $parentQueries = $env->block->queryList;
1410
- if ($childQueries == null) {
1411
- $childQueries = $parentQueries;
1412
- } else {
1413
- $originalQueries = $childQueries;
1414
- $childQueries = array();
1415
-
1416
- foreach ($parentQueries as $parentQuery){
1417
- foreach ($originalQueries as $childQuery) {
1418
- $childQueries []= array_merge($parentQuery, $childQuery);
1419
- }
1420
- }
1421
- }
1422
-
1423
- return $this->multiplyMedia($env->parent, $childQueries);
1424
- }
1425
-
1426
- // convert something to list
1427
- protected function coerceList($item, $delim = ",") {
1428
- if (isset($item) && $item[0] == "list") {
1429
- return $item;
1430
- }
1431
-
1432
- return array("list", $delim, !isset($item) ? array(): array($item));
1433
- }
1434
-
1435
- protected function applyArguments($argDef, $argValues) {
1436
- $hasVariable = false;
1437
- $args = array();
1438
- foreach ($argDef as $i => $arg) {
1439
- list($name, $default, $isVariable) = $argDef[$i];
1440
- $args[$name] = array($i, $name, $default, $isVariable);
1441
- $hasVariable |= $isVariable;
1442
- }
1443
-
1444
- $keywordArgs = array();
1445
- $deferredKeywordArgs = array();
1446
- $remaining = array();
1447
- // assign the keyword args
1448
- foreach ((array) $argValues as $arg) {
1449
- if (!empty($arg[0])) {
1450
- if (!isset($args[$arg[0][1]])) {
1451
- if ($hasVariable) {
1452
- $deferredKeywordArgs[$arg[0][1]] = $arg[1];
1453
- } else {
1454
- $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
1455
- }
1456
- } elseif ($args[$arg[0][1]][0] < count($remaining)) {
1457
- $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
1458
- } else {
1459
- $keywordArgs[$arg[0][1]] = $arg[1];
1460
- }
1461
- } elseif (count($keywordArgs)) {
1462
- $this->throwError('Positional arguments must come before keyword arguments.');
1463
- } elseif ($arg[2] == true) {
1464
- $val = $this->reduce($arg[1], true);
1465
- if ($val[0] == "list") {
1466
- foreach ($val[2] as $name => $item) {
1467
- if (!is_numeric($name)) {
1468
- $keywordArgs[$name] = $item;
1469
- } else {
1470
- $remaining[] = $item;
1471
- }
1472
- }
1473
- } else {
1474
- $remaining[] = $val;
1475
- }
1476
- } else {
1477
- $remaining[] = $arg[1];
1478
- }
1479
- }
1480
-
1481
- foreach ($args as $arg) {
1482
- list($i, $name, $default, $isVariable) = $arg;
1483
- if ($isVariable) {
1484
- $val = array("list", ",", array());
1485
- for ($count = count($remaining); $i < $count; $i++) {
1486
- $val[2][] = $remaining[$i];
1487
- }
1488
- foreach ($deferredKeywordArgs as $itemName => $item) {
1489
- $val[2][$itemName] = $item;
1490
- }
1491
- } elseif (isset($remaining[$i])) {
1492
- $val = $remaining[$i];
1493
- } elseif (isset($keywordArgs[$name])) {
1494
- $val = $keywordArgs[$name];
1495
- } elseif (!empty($default)) {
1496
- $val = $default;
1497
- } else {
1498
- $this->throwError("Missing argument $name");
1499
- }
1500
-
1501
- $this->set($name, $this->reduce($val, true), true);
1502
- }
1503
- }
1504
-
1505
- protected function pushEnv($block=null) {
1506
- $env = new stdClass;
1507
- $env->parent = $this->env;
1508
- $env->store = array();
1509
- $env->block = $block;
1510
- $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
1511
-
1512
- $this->env = $env;
1513
- return $env;
1514
- }
1515
-
1516
- protected function normalizeName($name) {
1517
- return str_replace("-", "_", $name);
1518
- }
1519
-
1520
- protected function getStoreEnv() {
1521
- return isset($this->storeEnv) ? $this->storeEnv : $this->env;
1522
- }
1523
-
1524
- protected function set($name, $value, $shadow=false) {
1525
- $name = $this->normalizeName($name);
1526
-
1527
- if ($shadow) {
1528
- $this->setRaw($name, $value);
1529
- } else {
1530
- $this->setExisting($name, $value);
1531
- }
1532
- }
1533
-
1534
- protected function setExisting($name, $value, $env = null) {
1535
- if (!isset($env)) $env = $this->getStoreEnv();
1536
-
1537
- if (isset($env->store[$name]) || !isset($env->parent)) {
1538
- $env->store[$name] = $value;
1539
- } else {
1540
- $this->setExisting($name, $value, $env->parent);
1541
- }
1542
- }
1543
-
1544
- protected function setRaw($name, $value) {
1545
- $env = $this->getStoreEnv();
1546
- $env->store[$name] = $value;
1547
- }
1548
-
1549
- public function get($name, $defaultValue = null, $env = null) {
1550
- $name = $this->normalizeName($name);
1551
-
1552
- if (!isset($env)) $env = $this->getStoreEnv();
1553
- if (!isset($defaultValue)) $defaultValue = self::$defaultValue;
1554
-
1555
- if (isset($env->store[$name])) {
1556
- return $env->store[$name];
1557
- } elseif (isset($env->parent)) {
1558
- return $this->get($name, $defaultValue, $env->parent);
1559
- }
1560
-
1561
- return $defaultValue; // found nothing
1562
- }
1563
-
1564
- protected function injectVariables(array $args)
1565
- {
1566
- if (empty($args)) {
1567
- return;
1568
- }
1569
-
1570
- $parser = new scss_parser(__METHOD__, false);
1571
-
1572
- foreach ($args as $name => $strValue) {
1573
- if ($name[0] === '$') {
1574
- $name = substr($name, 1);
1575
- }
1576
-
1577
- $parser->env = null;
1578
- $parser->count = 0;
1579
- $parser->buffer = (string) $strValue;
1580
- $parser->inParens = false;
1581
- $parser->eatWhiteDefault = true;
1582
- $parser->insertComments = true;
1583
-
1584
- if ( ! $parser->valueList($value)) {
1585
- throw new Exception("failed to parse passed in variable $name: $strValue");
1586
- }
1587
-
1588
- $this->set($name, $value);
1589
- }
1590
- }
1591
-
1592
- /**
1593
- * Set variables
1594
- *
1595
- * @param array $variables
1596
- */
1597
- public function setVariables(array $variables)
1598
- {
1599
- $this->registeredVars = array_merge($this->registeredVars, $variables);
1600
- }
1601
-
1602
- /**
1603
- * Unset variable
1604
- *
1605
- * @param string $name
1606
- */
1607
- public function unsetVariable($name)
1608
- {
1609
- unset($this->registeredVars[$name]);
1610
- }
1611
-
1612
- protected function popEnv() {
1613
- $env = $this->env;
1614
- $this->env = $this->env->parent;
1615
- return $env;
1616
- }
1617
-
1618
- public function getParsedFiles() {
1619
- return $this->parsedFiles;
1620
- }
1621
-
1622
- public function addImportPath($path) {
1623
- $this->importPaths[] = $path;
1624
- }
1625
-
1626
- public function setImportPaths($path) {
1627
- $this->importPaths = (array)$path;
1628
- }
1629
-
1630
- public function setNumberPrecision($numberPrecision) {
1631
- $this->numberPrecision = $numberPrecision;
1632
- }
1633
-
1634
- public function setFormatter($formatterName) {
1635
- $this->formatter = $formatterName;
1636
- }
1637
-
1638
- public function registerFunction($name, $func) {
1639
- $this->userFunctions[$this->normalizeName($name)] = $func;
1640
- }
1641
-
1642
- public function unregisterFunction($name) {
1643
- unset($this->userFunctions[$this->normalizeName($name)]);
1644
- }
1645
-
1646
- protected function importFile($path, $out) {
1647
- // see if tree is cached
1648
- $realPath = realpath($path);
1649
- if (isset($this->importCache[$realPath])) {
1650
- $tree = $this->importCache[$realPath];
1651
- } else {
1652
- $code = file_get_contents($path);
1653
- $parser = new scss_parser($path, false);
1654
- $tree = $parser->parse($code);
1655
- $this->parsedFiles[] = $path;
1656
-
1657
- $this->importCache[$realPath] = $tree;
1658
- }
1659
-
1660
- $pi = pathinfo($path);
1661
- array_unshift($this->importPaths, $pi['dirname']);
1662
- $this->compileChildren($tree->children, $out);
1663
- array_shift($this->importPaths);
1664
- }
1665
-
1666
- // results the file path for an import url if it exists
1667
- public function findImport($url) {
1668
- $urls = array();
1669
-
1670
- // for "normal" scss imports (ignore vanilla css and external requests)
1671
- if (!preg_match('/\.css|^http:\/\/$/', $url)) {
1672
- // try both normal and the _partial filename
1673
- $urls = array($url, preg_replace('/[^\/]+$/', '_\0', $url));
1674
- }
1675
-
1676
- foreach ($this->importPaths as $dir) {
1677
- if (is_string($dir)) {
1678
- // check urls for normal import paths
1679
- foreach ($urls as $full) {
1680
- $full = $dir .
1681
- (!empty($dir) && substr($dir, -1) != '/' ? '/' : '') .
1682
- $full;
1683
-
1684
- if ($this->fileExists($file = $full.'.scss') ||
1685
- $this->fileExists($file = $full))
1686
- {
1687
- return $file;
1688
- }
1689
- }
1690
- } else {
1691
- // check custom callback for import path
1692
- $file = call_user_func($dir,$url,$this);
1693
- if ($file !== null) {
1694
- return $file;
1695
- }
1696
- }
1697
- }
1698
-
1699
- return null;
1700
- }
1701
-
1702
- protected function fileExists($name) {
1703
- return is_file($name);
1704
- }
1705
-
1706
- protected function callBuiltin($name, $args, &$returnValue) {
1707
- // try a lib function
1708
- $name = $this->normalizeName($name);
1709
- $libName = "lib_".$name;
1710
- $f = array($this, $libName);
1711
- if (is_callable($f)) {
1712
- $prototype = isset(self::$$libName) ? self::$$libName : null;
1713
- $sorted = $this->sortArgs($prototype, $args);
1714
- foreach ($sorted as &$val) {
1715
- $val = $this->reduce($val, true);
1716
- }
1717
- $returnValue = call_user_func($f, $sorted, $this);
1718
- } elseif (isset($this->userFunctions[$name])) {
1719
- // see if we can find a user function
1720
- $fn = $this->userFunctions[$name];
1721
-
1722
- foreach ($args as &$val) {
1723
- $val = $this->reduce($val[1], true);
1724
- }
1725
-
1726
- $returnValue = call_user_func($fn, $args, $this);
1727
- }
1728
-
1729
- if (isset($returnValue)) {
1730
- // coerce a php value into a scss one
1731
- if (is_numeric($returnValue)) {
1732
- $returnValue = array('number', $returnValue, "");
1733
- } elseif (is_bool($returnValue)) {
1734
- $returnValue = $returnValue ? self::$true : self::$false;
1735
- } elseif (!is_array($returnValue)) {
1736
- $returnValue = array('keyword', $returnValue);
1737
- }
1738
-
1739
- return true;
1740
- }
1741
-
1742
- return false;
1743
- }
1744
-
1745
- // sorts any keyword arguments
1746
- // TODO: merge with apply arguments
1747
- protected function sortArgs($prototype, $args) {
1748
- $keyArgs = array();
1749
- $posArgs = array();
1750
-
1751
- foreach ($args as $arg) {
1752
- list($key, $value) = $arg;
1753
- $key = $key[1];
1754
- if (empty($key)) {
1755
- $posArgs[] = $value;
1756
- } else {
1757
- $keyArgs[$key] = $value;
1758
- }
1759
- }
1760
-
1761
- if (!isset($prototype)) return $posArgs;
1762
-
1763
- $finalArgs = array();
1764
- foreach ($prototype as $i => $names) {
1765
- if (isset($posArgs[$i])) {
1766
- $finalArgs[] = $posArgs[$i];
1767
- continue;
1768
- }
1769
-
1770
- $set = false;
1771
- foreach ((array)$names as $name) {
1772
- if (isset($keyArgs[$name])) {
1773
- $finalArgs[] = $keyArgs[$name];
1774
- $set = true;
1775
- break;
1776
- }
1777
- }
1778
-
1779
- if (!$set) {
1780
- $finalArgs[] = null;
1781
- }
1782
- }
1783
-
1784
- return $finalArgs;
1785
- }
1786
-
1787
- protected function coerceForExpression($value) {
1788
- if ($color = $this->coerceColor($value)) {
1789
- return $color;
1790
- }
1791
-
1792
- return $value;
1793
- }
1794
-
1795
- protected function coerceColor($value) {
1796
- switch ($value[0]) {
1797
- case "color": return $value;
1798
- case "keyword":
1799
- $name = $value[1];
1800
- if (isset(self::$cssColors[$name])) {
1801
- $rgba = explode(',', self::$cssColors[$name]);
1802
- return isset($rgba[3])
1803
- ? array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3])
1804
- : array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]);
1805
- }
1806
- return null;
1807
- }
1808
-
1809
- return null;
1810
- }
1811
-
1812
- protected function coerceString($value) {
1813
- switch ($value[0]) {
1814
- case "string":
1815
- return $value;
1816
- case "keyword":
1817
- return array("string", "", array($value[1]));
1818
- }
1819
- return null;
1820
- }
1821
-
1822
- public function assertList($value) {
1823
- if ($value[0] != "list")
1824
- $this->throwError("expecting list");
1825
- return $value;
1826
- }
1827
-
1828
- public function assertColor($value) {
1829
- if ($color = $this->coerceColor($value)) return $color;
1830
- $this->throwError("expecting color");
1831
- }
1832
-
1833
- public function assertNumber($value) {
1834
- if ($value[0] != "number")
1835
- $this->throwError("expecting number");
1836
- return $value[1];
1837
- }
1838
-
1839
- protected function coercePercent($value) {
1840
- if ($value[0] == "number") {
1841
- if ($value[2] == "%") {
1842
- return $value[1] / 100;
1843
- }
1844
- return $value[1];
1845
- }
1846
- return 0;
1847
- }
1848
-
1849
- // make sure a color's components don't go out of bounds
1850
- protected function fixColor($c) {
1851
- foreach (range(1, 3) as $i) {
1852
- if ($c[$i] < 0) $c[$i] = 0;
1853
- if ($c[$i] > 255) $c[$i] = 255;
1854
- }
1855
-
1856
- return $c;
1857
- }
1858
-
1859
- public function toHSL($red, $green, $blue) {
1860
- $min = min($red, $green, $blue);
1861
- $max = max($red, $green, $blue);
1862
-
1863
- $l = $min + $max;
1864
-
1865
- if ($min == $max) {
1866
- $s = $h = 0;
1867
- } else {
1868
- $d = $max - $min;
1869
-
1870
- if ($l < 255)
1871
- $s = $d / $l;
1872
- else
1873
- $s = $d / (510 - $l);
1874
-
1875
- if ($red == $max)
1876
- $h = 60 * ($green - $blue) / $d;
1877
- elseif ($green == $max)
1878
- $h = 60 * ($blue - $red) / $d + 120;
1879
- elseif ($blue == $max)
1880
- $h = 60 * ($red - $green) / $d + 240;
1881
- }
1882
-
1883
- return array('hsl', fmod($h, 360), $s * 100, $l / 5.1);
1884
- }
1885
-
1886
- public function hueToRGB($m1, $m2, $h) {
1887
- if ($h < 0)
1888
- $h += 1;
1889
- elseif ($h > 1)
1890
- $h -= 1;
1891
-
1892
- if ($h * 6 < 1)
1893
- return $m1 + ($m2 - $m1) * $h * 6;
1894
-
1895
- if ($h * 2 < 1)
1896
- return $m2;
1897
-
1898
- if ($h * 3 < 2)
1899
- return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
1900
-
1901
- return $m1;
1902
- }
1903
-
1904
- // H from 0 to 360, S and L from 0 to 100
1905
- public function toRGB($hue, $saturation, $lightness) {
1906
- if ($hue < 0) {
1907
- $hue += 360;
1908
- }
1909
-
1910
- $h = $hue / 360;
1911
- $s = min(100, max(0, $saturation)) / 100;
1912
- $l = min(100, max(0, $lightness)) / 100;
1913
-
1914
- $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
1915
- $m1 = $l * 2 - $m2;
1916
-
1917
- $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
1918
- $g = $this->hueToRGB($m1, $m2, $h) * 255;
1919
- $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
1920
-
1921
- $out = array('color', $r, $g, $b);
1922
- return $out;
1923
- }
1924
-
1925
- // Built in functions
1926
-
1927
- protected static $lib_if = array("condition", "if-true", "if-false");
1928
- protected function lib_if($args) {
1929
- list($cond,$t, $f) = $args;
1930
- if (!$this->isTruthy($cond)) return $f;
1931
- return $t;
1932
- }
1933
-
1934
- protected static $lib_index = array("list", "value");
1935
- protected function lib_index($args) {
1936
- list($list, $value) = $args;
1937
- $list = $this->assertList($list);
1938
-
1939
- $values = array();
1940
- foreach ($list[2] as $item) {
1941
- $values[] = $this->normalizeValue($item);
1942
- }
1943
- $key = array_search($this->normalizeValue($value), $values);
1944
-
1945
- return false === $key ? false : $key + 1;
1946
- }
1947
-
1948
- protected static $lib_rgb = array("red", "green", "blue");
1949
- protected function lib_rgb($args) {
1950
- list($r,$g,$b) = $args;
1951
- return array("color", $r[1], $g[1], $b[1]);
1952
- }
1953
-
1954
- protected static $lib_rgba = array(
1955
- array("red", "color"),
1956
- "green", "blue", "alpha");
1957
- protected function lib_rgba($args) {
1958
- if ($color = $this->coerceColor($args[0])) {
1959
- $num = !isset($args[1]) ? $args[3] : $args[1];
1960
- $alpha = $this->assertNumber($num);
1961
- $color[4] = $alpha;
1962
- return $color;
1963
- }
1964
-
1965
- list($r,$g,$b, $a) = $args;
1966
- return array("color", $r[1], $g[1], $b[1], $a[1]);
1967
- }
1968
-
1969
- // helper function for adjust_color, change_color, and scale_color
1970
- protected function alter_color($args, $fn) {
1971
- $color = $this->assertColor($args[0]);
1972
-
1973
- foreach (array(1,2,3,7) as $i) {
1974
- if (isset($args[$i])) {
1975
- $val = $this->assertNumber($args[$i]);
1976
- $ii = $i == 7 ? 4 : $i; // alpha
1977
- $color[$ii] =
1978
- $this->$fn(isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
1979
- }
1980
- }
1981
-
1982
- if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
1983
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
1984
- foreach (array(4,5,6) as $i) {
1985
- if (isset($args[$i])) {
1986
- $val = $this->assertNumber($args[$i]);
1987
- $hsl[$i - 3] = $this->$fn($hsl[$i - 3], $val, $i);
1988
- }
1989
- }
1990
-
1991
- $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
1992
- if (isset($color[4])) $rgb[4] = $color[4];
1993
- $color = $rgb;
1994
- }
1995
-
1996
- return $color;
1997
- }
1998
-
1999
- protected static $lib_adjust_color = array(
2000
- "color", "red", "green", "blue",
2001
- "hue", "saturation", "lightness", "alpha"
2002
- );
2003
- protected function adjust_color_helper($base, $alter, $i) {
2004
- return $base += $alter;
2005
- }
2006
- protected function lib_adjust_color($args) {
2007
- return $this->alter_color($args, "adjust_color_helper");
2008
- }
2009
-
2010
- protected static $lib_change_color = array(
2011
- "color", "red", "green", "blue",
2012
- "hue", "saturation", "lightness", "alpha"
2013
- );
2014
- protected function change_color_helper($base, $alter, $i) {
2015
- return $alter;
2016
- }
2017
- protected function lib_change_color($args) {
2018
- return $this->alter_color($args, "change_color_helper");
2019
- }
2020
-
2021
- protected static $lib_scale_color = array(
2022
- "color", "red", "green", "blue",
2023
- "hue", "saturation", "lightness", "alpha"
2024
- );
2025
- protected function scale_color_helper($base, $scale, $i) {
2026
- // 1,2,3 - rgb
2027
- // 4, 5, 6 - hsl
2028
- // 7 - a
2029
- switch ($i) {
2030
- case 1:
2031
- case 2:
2032
- case 3:
2033
- $max = 255; break;
2034
- case 4:
2035
- $max = 360; break;
2036
- case 7:
2037
- $max = 1; break;
2038
- default:
2039
- $max = 100;
2040
- }
2041
-
2042
- $scale = $scale / 100;
2043
- if ($scale < 0) {
2044
- return $base * $scale + $base;
2045
- } else {
2046
- return ($max - $base) * $scale + $base;
2047
- }
2048
- }
2049
- protected function lib_scale_color($args) {
2050
- return $this->alter_color($args, "scale_color_helper");
2051
- }
2052
-
2053
- protected static $lib_ie_hex_str = array("color");
2054
- protected function lib_ie_hex_str($args) {
2055
- $color = $this->coerceColor($args[0]);
2056
- $color[4] = isset($color[4]) ? round(255*$color[4]) : 255;
2057
-
2058
- return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
2059
- }
2060
-
2061
- protected static $lib_red = array("color");
2062
- protected function lib_red($args) {
2063
- $color = $this->coerceColor($args[0]);
2064
- return $color[1];
2065
- }
2066
-
2067
- protected static $lib_green = array("color");
2068
- protected function lib_green($args) {
2069
- $color = $this->coerceColor($args[0]);
2070
- return $color[2];
2071
- }
2072
-
2073
- protected static $lib_blue = array("color");
2074
- protected function lib_blue($args) {
2075
- $color = $this->coerceColor($args[0]);
2076
- return $color[3];
2077
- }
2078
-
2079
- protected static $lib_alpha = array("color");
2080
- protected function lib_alpha($args) {
2081
- if ($color = $this->coerceColor($args[0])) {
2082
- return isset($color[4]) ? $color[4] : 1;
2083
- }
2084
-
2085
- // this might be the IE function, so return value unchanged
2086
- return null;
2087
- }
2088
-
2089
- protected static $lib_opacity = array("color");
2090
- protected function lib_opacity($args) {
2091
- $value = $args[0];
2092
- if ($value[0] === 'number') return null;
2093
- return $this->lib_alpha($args);
2094
- }
2095
-
2096
- // mix two colors
2097
- protected static $lib_mix = array("color-1", "color-2", "weight");
2098
- protected function lib_mix($args) {
2099
- list($first, $second, $weight) = $args;
2100
- $first = $this->assertColor($first);
2101
- $second = $this->assertColor($second);
2102
-
2103
- if (!isset($weight)) {
2104
- $weight = 0.5;
2105
- } else {
2106
- $weight = $this->coercePercent($weight);
2107
- }
2108
-
2109
- $firstAlpha = isset($first[4]) ? $first[4] : 1;
2110
- $secondAlpha = isset($second[4]) ? $second[4] : 1;
2111
-
2112
- $w = $weight * 2 - 1;
2113
- $a = $firstAlpha - $secondAlpha;
2114
-
2115
- $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
2116
- $w2 = 1.0 - $w1;
2117
-
2118
- $new = array('color',
2119
- $w1 * $first[1] + $w2 * $second[1],
2120
- $w1 * $first[2] + $w2 * $second[2],
2121
- $w1 * $first[3] + $w2 * $second[3],
2122
- );
2123
-
2124
- if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
2125
- $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1);
2126
- }
2127
-
2128
- return $this->fixColor($new);
2129
- }
2130
-
2131
- protected static $lib_hsl = array("hue", "saturation", "lightness");
2132
- protected function lib_hsl($args) {
2133
- list($h, $s, $l) = $args;
2134
- return $this->toRGB($h[1], $s[1], $l[1]);
2135
- }
2136
-
2137
- protected static $lib_hsla = array("hue", "saturation",
2138
- "lightness", "alpha");
2139
- protected function lib_hsla($args) {
2140
- list($h, $s, $l, $a) = $args;
2141
- $color = $this->toRGB($h[1], $s[1], $l[1]);
2142
- $color[4] = $a[1];
2143
- return $color;
2144
- }
2145
-
2146
- protected static $lib_hue = array("color");
2147
- protected function lib_hue($args) {
2148
- $color = $this->assertColor($args[0]);
2149
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2150
- return array("number", $hsl[1], "deg");
2151
- }
2152
-
2153
- protected static $lib_saturation = array("color");
2154
- protected function lib_saturation($args) {
2155
- $color = $this->assertColor($args[0]);
2156
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2157
- return array("number", $hsl[2], "%");
2158
- }
2159
-
2160
- protected static $lib_lightness = array("color");
2161
- protected function lib_lightness($args) {
2162
- $color = $this->assertColor($args[0]);
2163
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2164
- return array("number", $hsl[3], "%");
2165
- }
2166
-
2167
- protected function adjustHsl($color, $idx, $amount) {
2168
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2169
- $hsl[$idx] += $amount;
2170
- $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
2171
- if (isset($color[4])) $out[4] = $color[4];
2172
- return $out;
2173
- }
2174
-
2175
- protected static $lib_adjust_hue = array("color", "degrees");
2176
- protected function lib_adjust_hue($args) {
2177
- $color = $this->assertColor($args[0]);
2178
- $degrees = $this->assertNumber($args[1]);
2179
- return $this->adjustHsl($color, 1, $degrees);
2180
- }
2181
-
2182
- protected static $lib_lighten = array("color", "amount");
2183
- protected function lib_lighten($args) {
2184
- $color = $this->assertColor($args[0]);
2185
- $amount = 100*$this->coercePercent($args[1]);
2186
- return $this->adjustHsl($color, 3, $amount);
2187
- }
2188
-
2189
- protected static $lib_darken = array("color", "amount");
2190
- protected function lib_darken($args) {
2191
- $color = $this->assertColor($args[0]);
2192
- $amount = 100*$this->coercePercent($args[1]);
2193
- return $this->adjustHsl($color, 3, -$amount);
2194
- }
2195
-
2196
- protected static $lib_saturate = array("color", "amount");
2197
- protected function lib_saturate($args) {
2198
- $value = $args[0];
2199
- if ($value[0] === 'number') return null;
2200
- $color = $this->assertColor($value);
2201
- $amount = 100*$this->coercePercent($args[1]);
2202
- return $this->adjustHsl($color, 2, $amount);
2203
- }
2204
-
2205
- protected static $lib_desaturate = array("color", "amount");
2206
- protected function lib_desaturate($args) {
2207
- $color = $this->assertColor($args[0]);
2208
- $amount = 100*$this->coercePercent($args[1]);
2209
- return $this->adjustHsl($color, 2, -$amount);
2210
- }
2211
-
2212
- protected static $lib_grayscale = array("color");
2213
- protected function lib_grayscale($args) {
2214
- $value = $args[0];
2215
- if ($value[0] === 'number') return null;
2216
- return $this->adjustHsl($this->assertColor($value), 2, -100);
2217
- }
2218
-
2219
- protected static $lib_complement = array("color");
2220
- protected function lib_complement($args) {
2221
- return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
2222
- }
2223
-
2224
- protected static $lib_invert = array("color");
2225
- protected function lib_invert($args) {
2226
- $value = $args[0];
2227
- if ($value[0] === 'number') return null;
2228
- $color = $this->assertColor($value);
2229
- $color[1] = 255 - $color[1];
2230
- $color[2] = 255 - $color[2];
2231
- $color[3] = 255 - $color[3];
2232
- return $color;
2233
- }
2234
-
2235
- // increases opacity by amount
2236
- protected static $lib_opacify = array("color", "amount");
2237
- protected function lib_opacify($args) {
2238
- $color = $this->assertColor($args[0]);
2239
- $amount = $this->coercePercent($args[1]);
2240
-
2241
- $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
2242
- $color[4] = min(1, max(0, $color[4]));
2243
- return $color;
2244
- }
2245
-
2246
- protected static $lib_fade_in = array("color", "amount");
2247
- protected function lib_fade_in($args) {
2248
- return $this->lib_opacify($args);
2249
- }
2250
-
2251
- // decreases opacity by amount
2252
- protected static $lib_transparentize = array("color", "amount");
2253
- protected function lib_transparentize($args) {
2254
- $color = $this->assertColor($args[0]);
2255
- $amount = $this->coercePercent($args[1]);
2256
-
2257
- $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
2258
- $color[4] = min(1, max(0, $color[4]));
2259
- return $color;
2260
- }
2261
-
2262
- protected static $lib_fade_out = array("color", "amount");
2263
- protected function lib_fade_out($args) {
2264
- return $this->lib_transparentize($args);
2265
- }
2266
-
2267
- protected static $lib_unquote = array("string");
2268
- protected function lib_unquote($args) {
2269
- $str = $args[0];
2270
- if ($str[0] == "string") $str[1] = "";
2271
- return $str;
2272
- }
2273
-
2274
- protected static $lib_quote = array("string");
2275
- protected function lib_quote($args) {
2276
- $value = $args[0];
2277
- if ($value[0] == "string" && !empty($value[1]))
2278
- return $value;
2279
- return array("string", '"', array($value));
2280
- }
2281
-
2282
- protected static $lib_percentage = array("value");
2283
- protected function lib_percentage($args) {
2284
- return array("number",
2285
- $this->coercePercent($args[0]) * 100,
2286
- "%");
2287
- }
2288
-
2289
- protected static $lib_round = array("value");
2290
- protected function lib_round($args) {
2291
- $num = $args[0];
2292
- $num[1] = round($num[1]);
2293
- return $num;
2294
- }
2295
-
2296
- protected static $lib_floor = array("value");
2297
- protected function lib_floor($args) {
2298
- $num = $args[0];
2299
- $num[1] = floor($num[1]);
2300
- return $num;
2301
- }
2302
-
2303
- protected static $lib_ceil = array("value");
2304
- protected function lib_ceil($args) {
2305
- $num = $args[0];
2306
- $num[1] = ceil($num[1]);
2307
- return $num;
2308
- }
2309
-
2310
- protected static $lib_abs = array("value");
2311
- protected function lib_abs($args) {
2312
- $num = $args[0];
2313
- $num[1] = abs($num[1]);
2314
- return $num;
2315
- }
2316
-
2317
- protected function lib_min($args) {
2318
- $numbers = $this->getNormalizedNumbers($args);
2319
- $min = null;
2320
- foreach ($numbers as $key => $number) {
2321
- if (null === $min || $number[1] <= $min[1]) {
2322
- $min = array($key, $number[1]);
2323
- }
2324
- }
2325
-
2326
- return $args[$min[0]];
2327
- }
2328
-
2329
- protected function lib_max($args) {
2330
- $numbers = $this->getNormalizedNumbers($args);
2331
- $max = null;
2332
- foreach ($numbers as $key => $number) {
2333
- if (null === $max || $number[1] >= $max[1]) {
2334
- $max = array($key, $number[1]);
2335
- }
2336
- }
2337
-
2338
- return $args[$max[0]];
2339
- }
2340
-
2341
- protected function getNormalizedNumbers($args) {
2342
- $unit = null;
2343
- $originalUnit = null;
2344
- $numbers = array();
2345
- foreach ($args as $key => $item) {
2346
- if ('number' != $item[0]) {
2347
- $this->throwError("%s is not a number", $item[0]);
2348
- }
2349
- $number = $this->normalizeNumber($item);
2350
-
2351
- if (null === $unit) {
2352
- $unit = $number[2];
2353
- $originalUnit = $item[2];
2354
- } elseif ($unit !== $number[2]) {
2355
- $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item[2]);
2356
- }
2357
-
2358
- $numbers[$key] = $number;
2359
- }
2360
-
2361
- return $numbers;
2362
- }
2363
-
2364
- protected static $lib_length = array("list");
2365
- protected function lib_length($args) {
2366
- $list = $this->coerceList($args[0]);
2367
- return count($list[2]);
2368
- }
2369
-
2370
- protected static $lib_nth = array("list", "n");
2371
- protected function lib_nth($args) {
2372
- $list = $this->coerceList($args[0]);
2373
- $n = $this->assertNumber($args[1]) - 1;
2374
- return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue;
2375
- }
2376
-
2377
- protected function listSeparatorForJoin($list1, $sep) {
2378
- if (!isset($sep)) return $list1[1];
2379
- switch ($this->compileValue($sep)) {
2380
- case "comma":
2381
- return ",";
2382
- case "space":
2383
- return "";
2384
- default:
2385
- return $list1[1];
2386
- }
2387
- }
2388
-
2389
- protected static $lib_join = array("list1", "list2", "separator");
2390
- protected function lib_join($args) {
2391
- list($list1, $list2, $sep) = $args;
2392
- $list1 = $this->coerceList($list1, " ");
2393
- $list2 = $this->coerceList($list2, " ");
2394
- $sep = $this->listSeparatorForJoin($list1, $sep);
2395
- return array("list", $sep, array_merge($list1[2], $list2[2]));
2396
- }
2397
-
2398
- protected static $lib_append = array("list", "val", "separator");
2399
- protected function lib_append($args) {
2400
- list($list1, $value, $sep) = $args;
2401
- $list1 = $this->coerceList($list1, " ");
2402
- $sep = $this->listSeparatorForJoin($list1, $sep);
2403
- return array("list", $sep, array_merge($list1[2], array($value)));
2404
- }
2405
-
2406
- protected function lib_zip($args) {
2407
- foreach ($args as $arg) {
2408
- $this->assertList($arg);
2409
- }
2410
-
2411
- $lists = array();
2412
- $firstList = array_shift($args);
2413
- foreach ($firstList[2] as $key => $item) {
2414
- $list = array("list", "", array($item));
2415
- foreach ($args as $arg) {
2416
- if (isset($arg[2][$key])) {
2417
- $list[2][] = $arg[2][$key];
2418
- } else {
2419
- break 2;
2420
- }
2421
- }
2422
- $lists[] = $list;
2423
- }
2424
-
2425
- return array("list", ",", $lists);
2426
- }
2427
-
2428
- protected static $lib_type_of = array("value");
2429
- protected function lib_type_of($args) {
2430
- $value = $args[0];
2431
- switch ($value[0]) {
2432
- case "keyword":
2433
- if ($value == self::$true || $value == self::$false) {
2434
- return "bool";
2435
- }
2436
-
2437
- if ($this->coerceColor($value)) {
2438
- return "color";
2439
- }
2440
-
2441
- return "string";
2442
- default:
2443
- return $value[0];
2444
- }
2445
- }
2446
-
2447
- protected static $lib_unit = array("number");
2448
- protected function lib_unit($args) {
2449
- $num = $args[0];
2450
- if ($num[0] == "number") {
2451
- return array("string", '"', array($num[2]));
2452
- }
2453
- return "";
2454
- }
2455
-
2456
- protected static $lib_unitless = array("number");
2457
- protected function lib_unitless($args) {
2458
- $value = $args[0];
2459
- return $value[0] == "number" && empty($value[2]);
2460
- }
2461
-
2462
- protected static $lib_comparable = array("number-1", "number-2");
2463
- protected function lib_comparable($args) {
2464
- list($number1, $number2) = $args;
2465
- if (!isset($number1[0]) || $number1[0] != "number" || !isset($number2[0]) || $number2[0] != "number") {
2466
- $this->throwError('Invalid argument(s) for "comparable"');
2467
- }
2468
-
2469
- $number1 = $this->normalizeNumber($number1);
2470
- $number2 = $this->normalizeNumber($number2);
2471
-
2472
- return $number1[2] == $number2[2] || $number1[2] == "" || $number2[2] == "";
2473
- }
2474
-
2475
- /**
2476
- * Workaround IE7's content counter bug.
2477
- *
2478
- * @param array $args
2479
- */
2480
- protected function lib_counter($args) {
2481
- $list = array_map(array($this, 'compileValue'), $args);
2482
- return array('string', '', array('counter(' . implode(',', $list) . ')'));
2483
- }
2484
-
2485
- public function throwError($msg = null) {
2486
- if (func_num_args() > 1) {
2487
- $msg = call_user_func_array("sprintf", func_get_args());
2488
- }
2489
-
2490
- if ($this->sourcePos >= 0 && isset($this->sourceParser)) {
2491
- $this->sourceParser->throwParseError($msg, $this->sourcePos);
2492
- }
2493
-
2494
- throw new Exception($msg);
2495
- }
2496
-
2497
- /**
2498
- * CSS Colors
2499
- *
2500
- * @see http://www.w3.org/TR/css3-color
2501
- */
2502
- static protected $cssColors = array(
2503
- 'aliceblue' => '240,248,255',
2504
- 'antiquewhite' => '250,235,215',
2505
- 'aqua' => '0,255,255',
2506
- 'aquamarine' => '127,255,212',
2507
- 'azure' => '240,255,255',
2508
- 'beige' => '245,245,220',
2509
- 'bisque' => '255,228,196',
2510
- 'black' => '0,0,0',
2511
- 'blanchedalmond' => '255,235,205',
2512
- 'blue' => '0,0,255',
2513
- 'blueviolet' => '138,43,226',
2514
- 'brown' => '165,42,42',
2515
- 'burlywood' => '222,184,135',
2516
- 'cadetblue' => '95,158,160',
2517
- 'chartreuse' => '127,255,0',
2518
- 'chocolate' => '210,105,30',
2519
- 'coral' => '255,127,80',
2520
- 'cornflowerblue' => '100,149,237',
2521
- 'cornsilk' => '255,248,220',
2522
- 'crimson' => '220,20,60',
2523
- 'cyan' => '0,255,255',
2524
- 'darkblue' => '0,0,139',
2525
- 'darkcyan' => '0,139,139',
2526
- 'darkgoldenrod' => '184,134,11',
2527
- 'darkgray' => '169,169,169',
2528
- 'darkgreen' => '0,100,0',
2529
- 'darkgrey' => '169,169,169',
2530
- 'darkkhaki' => '189,183,107',
2531
- 'darkmagenta' => '139,0,139',
2532
- 'darkolivegreen' => '85,107,47',
2533
- 'darkorange' => '255,140,0',
2534
- 'darkorchid' => '153,50,204',
2535
- 'darkred' => '139,0,0',
2536
- 'darksalmon' => '233,150,122',
2537
- 'darkseagreen' => '143,188,143',
2538
- 'darkslateblue' => '72,61,139',
2539
- 'darkslategray' => '47,79,79',
2540
- 'darkslategrey' => '47,79,79',
2541
- 'darkturquoise' => '0,206,209',
2542
- 'darkviolet' => '148,0,211',
2543
- 'deeppink' => '255,20,147',
2544
- 'deepskyblue' => '0,191,255',
2545
- 'dimgray' => '105,105,105',
2546
- 'dimgrey' => '105,105,105',
2547
- 'dodgerblue' => '30,144,255',
2548
- 'firebrick' => '178,34,34',
2549
- 'floralwhite' => '255,250,240',
2550
- 'forestgreen' => '34,139,34',
2551
- 'fuchsia' => '255,0,255',
2552
- 'gainsboro' => '220,220,220',
2553
- 'ghostwhite' => '248,248,255',
2554
- 'gold' => '255,215,0',
2555
- 'goldenrod' => '218,165,32',
2556
- 'gray' => '128,128,128',
2557
- 'green' => '0,128,0',
2558
- 'greenyellow' => '173,255,47',
2559
- 'grey' => '128,128,128',
2560
- 'honeydew' => '240,255,240',
2561
- 'hotpink' => '255,105,180',
2562
- 'indianred' => '205,92,92',
2563
- 'indigo' => '75,0,130',
2564
- 'ivory' => '255,255,240',
2565
- 'khaki' => '240,230,140',
2566
- 'lavender' => '230,230,250',
2567
- 'lavenderblush' => '255,240,245',
2568
- 'lawngreen' => '124,252,0',
2569
- 'lemonchiffon' => '255,250,205',
2570
- 'lightblue' => '173,216,230',
2571
- 'lightcoral' => '240,128,128',
2572
- 'lightcyan' => '224,255,255',
2573
- 'lightgoldenrodyellow' => '250,250,210',
2574
- 'lightgray' => '211,211,211',
2575
- 'lightgreen' => '144,238,144',
2576
- 'lightgrey' => '211,211,211',
2577
- 'lightpink' => '255,182,193',
2578
- 'lightsalmon' => '255,160,122',
2579
- 'lightseagreen' => '32,178,170',
2580
- 'lightskyblue' => '135,206,250',
2581
- 'lightslategray' => '119,136,153',
2582
- 'lightslategrey' => '119,136,153',
2583
- 'lightsteelblue' => '176,196,222',
2584
- 'lightyellow' => '255,255,224',
2585
- 'lime' => '0,255,0',
2586
- 'limegreen' => '50,205,50',
2587
- 'linen' => '250,240,230',
2588
- 'magenta' => '255,0,255',
2589
- 'maroon' => '128,0,0',
2590
- 'mediumaquamarine' => '102,205,170',
2591
- 'mediumblue' => '0,0,205',
2592
- 'mediumorchid' => '186,85,211',
2593
- 'mediumpurple' => '147,112,219',
2594
- 'mediumseagreen' => '60,179,113',
2595
- 'mediumslateblue' => '123,104,238',
2596
- 'mediumspringgreen' => '0,250,154',
2597
- 'mediumturquoise' => '72,209,204',
2598
- 'mediumvioletred' => '199,21,133',
2599
- 'midnightblue' => '25,25,112',
2600
- 'mintcream' => '245,255,250',
2601
- 'mistyrose' => '255,228,225',
2602
- 'moccasin' => '255,228,181',
2603
- 'navajowhite' => '255,222,173',
2604
- 'navy' => '0,0,128',
2605
- 'oldlace' => '253,245,230',
2606
- 'olive' => '128,128,0',
2607
- 'olivedrab' => '107,142,35',
2608
- 'orange' => '255,165,0',
2609
- 'orangered' => '255,69,0',
2610
- 'orchid' => '218,112,214',
2611
- 'palegoldenrod' => '238,232,170',
2612
- 'palegreen' => '152,251,152',
2613
- 'paleturquoise' => '175,238,238',
2614
- 'palevioletred' => '219,112,147',
2615
- 'papayawhip' => '255,239,213',
2616
- 'peachpuff' => '255,218,185',
2617
- 'peru' => '205,133,63',
2618
- 'pink' => '255,192,203',
2619
- 'plum' => '221,160,221',
2620
- 'powderblue' => '176,224,230',
2621
- 'purple' => '128,0,128',
2622
- 'red' => '255,0,0',
2623
- 'rosybrown' => '188,143,143',
2624
- 'royalblue' => '65,105,225',
2625
- 'saddlebrown' => '139,69,19',
2626
- 'salmon' => '250,128,114',
2627
- 'sandybrown' => '244,164,96',
2628
- 'seagreen' => '46,139,87',
2629
- 'seashell' => '255,245,238',
2630
- 'sienna' => '160,82,45',
2631
- 'silver' => '192,192,192',
2632
- 'skyblue' => '135,206,235',
2633
- 'slateblue' => '106,90,205',
2634
- 'slategray' => '112,128,144',
2635
- 'slategrey' => '112,128,144',
2636
- 'snow' => '255,250,250',
2637
- 'springgreen' => '0,255,127',
2638
- 'steelblue' => '70,130,180',
2639
- 'tan' => '210,180,140',
2640
- 'teal' => '0,128,128',
2641
- 'thistle' => '216,191,216',
2642
- 'tomato' => '255,99,71',
2643
- 'transparent' => '0,0,0,0',
2644
- 'turquoise' => '64,224,208',
2645
- 'violet' => '238,130,238',
2646
- 'wheat' => '245,222,179',
2647
- 'white' => '255,255,255',
2648
- 'whitesmoke' => '245,245,245',
2649
- 'yellow' => '255,255,0',
2650
- 'yellowgreen' => '154,205,50'
2651
- );
2652
- }
2653
-
2654
- /**
2655
- * SCSS parser
2656
- *
2657
- * @author Leaf Corcoran <leafot@gmail.com>
2658
- */
2659
- class scss_parser {
2660
- static protected $precedence = array(
2661
- "or" => 0,
2662
- "and" => 1,
2663
-
2664
- '==' => 2,
2665
- '!=' => 2,
2666
- '<=' => 2,
2667
- '>=' => 2,
2668
- '=' => 2,
2669
- '<' => 3,
2670
- '>' => 2,
2671
-
2672
- '+' => 3,
2673
- '-' => 3,
2674
- '*' => 4,
2675
- '/' => 4,
2676
- '%' => 4,
2677
- );
2678
-
2679
- static protected $operators = array("+", "-", "*", "/", "%",
2680
- "==", "!=", "<=", ">=", "<", ">", "and", "or");
2681
-
2682
- static protected $operatorStr;
2683
- static protected $whitePattern;
2684
- static protected $commentMulti;
2685
-
2686
- static protected $commentSingle = "//";
2687
- static protected $commentMultiLeft = "/*";
2688
- static protected $commentMultiRight = "*/";
2689
-
2690
- /**
2691
- * Constructor
2692
- *
2693
- * @param string $sourceName
2694
- * @param boolean $rootParser
2695
- */
2696
- public function __construct($sourceName = null, $rootParser = true) {
2697
- $this->sourceName = $sourceName;
2698
- $this->rootParser = $rootParser;
2699
-
2700
- if (empty(self::$operatorStr)) {
2701
- self::$operatorStr = $this->makeOperatorStr(self::$operators);
2702
-
2703
- $commentSingle = $this->preg_quote(self::$commentSingle);
2704
- $commentMultiLeft = $this->preg_quote(self::$commentMultiLeft);
2705
- $commentMultiRight = $this->preg_quote(self::$commentMultiRight);
2706
- self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
2707
- self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
2708
- }
2709
- }
2710
-
2711
- static protected function makeOperatorStr($operators) {
2712
- return '('.implode('|', array_map(array('scss_parser','preg_quote'),
2713
- $operators)).')';
2714
- }
2715
-
2716
- /**
2717
- * Parser buffer
2718
- *
2719
- * @param string $buffer;
2720
- *
2721
- * @return \StdClass
2722
- */
2723
- public function parse($buffer)
2724
- {
2725
- $this->count = 0;
2726
- $this->env = null;
2727
- $this->inParens = false;
2728
- $this->eatWhiteDefault = true;
2729
- $this->insertComments = true;
2730
- $this->buffer = $buffer;
2731
-
2732
- $this->pushBlock(null); // root block
2733
- $this->whitespace();
2734
-
2735
- while (false !== $this->parseChunk())
2736
- ;
2737
-
2738
- if ($this->count != strlen($this->buffer)) {
2739
- $this->throwParseError();
2740
- }
2741
-
2742
- if (!empty($this->env->parent)) {
2743
- $this->throwParseError("unclosed block");
2744
- }
2745
-
2746
- $this->env->isRoot = true;
2747
-
2748
- return $this->env;
2749
- }
2750
-
2751
- /**
2752
- * Parse a single chunk off the head of the buffer and append it to the
2753
- * current parse environment.
2754
- *
2755
- * Returns false when the buffer is empty, or when there is an error.
2756
- *
2757
- * This function is called repeatedly until the entire document is
2758
- * parsed.
2759
- *
2760
- * This parser is most similar to a recursive descent parser. Single
2761
- * functions represent discrete grammatical rules for the language, and
2762
- * they are able to capture the text that represents those rules.
2763
- *
2764
- * Consider the function scssc::keyword(). (All parse functions are
2765
- * structured the same.)
2766
- *
2767
- * The function takes a single reference argument. When calling the
2768
- * function it will attempt to match a keyword on the head of the buffer.
2769
- * If it is successful, it will place the keyword in the referenced
2770
- * argument, advance the position in the buffer, and return true. If it
2771
- * fails then it won't advance the buffer and it will return false.
2772
- *
2773
- * All of these parse functions are powered by scssc::match(), which behaves
2774
- * the same way, but takes a literal regular expression. Sometimes it is
2775
- * more convenient to use match instead of creating a new function.
2776
- *
2777
- * Because of the format of the functions, to parse an entire string of
2778
- * grammatical rules, you can chain them together using &&.
2779
- *
2780
- * But, if some of the rules in the chain succeed before one fails, then
2781
- * the buffer position will be left at an invalid state. In order to
2782
- * avoid this, scssc::seek() is used to remember and set buffer positions.
2783
- *
2784
- * Before parsing a chain, use $s = $this->seek() to remember the current
2785
- * position into $s. Then if a chain fails, use $this->seek($s) to
2786
- * go back where we started.
2787
- *
2788
- * @return boolean
2789
- */
2790
- protected function parseChunk() {
2791
- $s = $this->seek();
2792
-
2793
- // the directives
2794
- if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") {
2795
- if ($this->literal("@media") && $this->mediaQueryList($mediaQueryList) && $this->literal("{")) {
2796
- $media = $this->pushSpecialBlock("media");
2797
- $media->queryList = $mediaQueryList[2];
2798
- return true;
2799
- } else {
2800
- $this->seek($s);
2801
- }
2802
-
2803
- if ($this->literal("@mixin") &&
2804
- $this->keyword($mixinName) &&
2805
- ($this->argumentDef($args) || true) &&
2806
- $this->literal("{"))
2807
- {
2808
- $mixin = $this->pushSpecialBlock("mixin");
2809
- $mixin->name = $mixinName;
2810
- $mixin->args = $args;
2811
- return true;
2812
- } else {
2813
- $this->seek($s);
2814
- }
2815
-
2816
- if ($this->literal("@include") &&
2817
- $this->keyword($mixinName) &&
2818
- ($this->literal("(") &&
2819
- ($this->argValues($argValues) || true) &&
2820
- $this->literal(")") || true) &&
2821
- ($this->end() ||
2822
- $this->literal("{") && $hasBlock = true))
2823
- {
2824
- $child = array("include",
2825
- $mixinName, isset($argValues) ? $argValues : null, null);
2826
-
2827
- if (!empty($hasBlock)) {
2828
- $include = $this->pushSpecialBlock("include");
2829
- $include->child = $child;
2830
- } else {
2831
- $this->append($child, $s);
2832
- }
2833
-
2834
- return true;
2835
- } else {
2836
- $this->seek($s);
2837
- }
2838
-
2839
- if ($this->literal("@import") &&
2840
- $this->valueList($importPath) &&
2841
- $this->end())
2842
- {
2843
- $this->append(array("import", $importPath), $s);
2844
- return true;
2845
- } else {
2846
- $this->seek($s);
2847
- }
2848
-
2849
- if ($this->literal("@extend") &&
2850
- $this->selectors($selector) &&
2851
- $this->end())
2852
- {
2853
- $this->append(array("extend", $selector), $s);
2854
- return true;
2855
- } else {
2856
- $this->seek($s);
2857
- }
2858
-
2859
- if ($this->literal("@function") &&
2860
- $this->keyword($fnName) &&
2861
- $this->argumentDef($args) &&
2862
- $this->literal("{"))
2863
- {
2864
- $func = $this->pushSpecialBlock("function");
2865
- $func->name = $fnName;
2866
- $func->args = $args;
2867
- return true;
2868
- } else {
2869
- $this->seek($s);
2870
- }
2871
-
2872
- if ($this->literal("@return") && $this->valueList($retVal) && $this->end()) {
2873
- $this->append(array("return", $retVal), $s);
2874
- return true;
2875
- } else {
2876
- $this->seek($s);
2877
- }
2878
-
2879
- if ($this->literal("@each") &&
2880
- $this->variable($varName) &&
2881
- $this->literal("in") &&
2882
- $this->valueList($list) &&
2883
- $this->literal("{"))
2884
- {
2885
- $each = $this->pushSpecialBlock("each");
2886
- $each->var = $varName[1];
2887
- $each->list = $list;
2888
- return true;
2889
- } else {
2890
- $this->seek($s);
2891
- }
2892
-
2893
- if ($this->literal("@while") &&
2894
- $this->expression($cond) &&
2895
- $this->literal("{"))
2896
- {
2897
- $while = $this->pushSpecialBlock("while");
2898
- $while->cond = $cond;
2899
- return true;
2900
- } else {
2901
- $this->seek($s);
2902
- }
2903
-
2904
- if ($this->literal("@for") &&
2905
- $this->variable($varName) &&
2906
- $this->literal("from") &&
2907
- $this->expression($start) &&
2908
- ($this->literal("through") ||
2909
- ($forUntil = true && $this->literal("to"))) &&
2910
- $this->expression($end) &&
2911
- $this->literal("{"))
2912
- {
2913
- $for = $this->pushSpecialBlock("for");
2914
- $for->var = $varName[1];
2915
- $for->start = $start;
2916
- $for->end = $end;
2917
- $for->until = isset($forUntil);
2918
- return true;
2919
- } else {
2920
- $this->seek($s);
2921
- }
2922
-
2923
- if ($this->literal("@if") && $this->valueList($cond) && $this->literal("{")) {
2924
- $if = $this->pushSpecialBlock("if");
2925
- $if->cond = $cond;
2926
- $if->cases = array();
2927
- return true;
2928
- } else {
2929
- $this->seek($s);
2930
- }
2931
-
2932
- if (($this->literal("@debug") || $this->literal("@warn")) &&
2933
- $this->valueList($value) &&
2934
- $this->end()) {
2935
- $this->append(array("debug", $value, $s), $s);
2936
- return true;
2937
- } else {
2938
- $this->seek($s);
2939
- }
2940
-
2941
- if ($this->literal("@content") && $this->end()) {
2942
- $this->append(array("mixin_content"), $s);
2943
- return true;
2944
- } else {
2945
- $this->seek($s);
2946
- }
2947
-
2948
- $last = $this->last();
2949
- if (isset($last) && $last[0] == "if") {
2950
- list(, $if) = $last;
2951
- if ($this->literal("@else")) {
2952
- if ($this->literal("{")) {
2953
- $else = $this->pushSpecialBlock("else");
2954
- } elseif ($this->literal("if") && $this->valueList($cond) && $this->literal("{")) {
2955
- $else = $this->pushSpecialBlock("elseif");
2956
- $else->cond = $cond;
2957
- }
2958
-
2959
- if (isset($else)) {
2960
- $else->dontAppend = true;
2961
- $if->cases[] = $else;
2962
- return true;
2963
- }
2964
- }
2965
-
2966
- $this->seek($s);
2967
- }
2968
-
2969
- if ($this->literal("@charset") &&
2970
- $this->valueList($charset) && $this->end())
2971
- {
2972
- $this->append(array("charset", $charset), $s);
2973
- return true;
2974
- } else {
2975
- $this->seek($s);
2976
- }
2977
-
2978
- // doesn't match built in directive, do generic one
2979
- if ($this->literal("@", false) && $this->keyword($dirName) &&
2980
- ($this->openString("{", $dirValue) || true) &&
2981
- $this->literal("{"))
2982
- {
2983
- $directive = $this->pushSpecialBlock("directive");
2984
- $directive->name = $dirName;
2985
- if (isset($dirValue)) $directive->value = $dirValue;
2986
- return true;
2987
- }
2988
-
2989
- $this->seek($s);
2990
- return false;
2991
- }
2992
-
2993
- // property shortcut
2994
- // captures most properties before having to parse a selector
2995
- if ($this->keyword($name, false) &&
2996
- $this->literal(": ") &&
2997
- $this->valueList($value) &&
2998
- $this->end())
2999
- {
3000
- $name = array("string", "", array($name));
3001
- $this->append(array("assign", $name, $value), $s);
3002
- return true;
3003
- } else {
3004
- $this->seek($s);
3005
- }
3006
-
3007
- // variable assigns
3008
- if ($this->variable($name) &&
3009
- $this->literal(":") &&
3010
- $this->valueList($value) && $this->end())
3011
- {
3012
- // check for !default
3013
- $defaultVar = $value[0] == "list" && $this->stripDefault($value);
3014
- $this->append(array("assign", $name, $value, $defaultVar), $s);
3015
- return true;
3016
- } else {
3017
- $this->seek($s);
3018
- }
3019
-
3020
- // misc
3021
- if ($this->literal("-->")) {
3022
- return true;
3023
- }
3024
-
3025
- // opening css block
3026
- $oldComments = $this->insertComments;
3027
- $this->insertComments = false;
3028
- if ($this->selectors($selectors) && $this->literal("{")) {
3029
- $this->pushBlock($selectors);
3030
- $this->insertComments = $oldComments;
3031
- return true;
3032
- } else {
3033
- $this->seek($s);
3034
- }
3035
- $this->insertComments = $oldComments;
3036
-
3037
- // property assign, or nested assign
3038
- if ($this->propertyName($name) && $this->literal(":")) {
3039
- $foundSomething = false;
3040
- if ($this->valueList($value)) {
3041
- $this->append(array("assign", $name, $value), $s);
3042
- $foundSomething = true;
3043
- }
3044
-
3045
- if ($this->literal("{")) {
3046
- $propBlock = $this->pushSpecialBlock("nestedprop");
3047
- $propBlock->prefix = $name;
3048
- $foundSomething = true;
3049
- } elseif ($foundSomething) {
3050
- $foundSomething = $this->end();
3051
- }
3052
-
3053
- if ($foundSomething) {
3054
- return true;
3055
- }
3056
-
3057
- $this->seek($s);
3058
- } else {
3059
- $this->seek($s);
3060
- }
3061
-
3062
- // closing a block
3063
- if ($this->literal("}")) {
3064
- $block = $this->popBlock();
3065
- if (isset($block->type) && $block->type == "include") {
3066
- $include = $block->child;
3067
- unset($block->child);
3068
- $include[3] = $block;
3069
- $this->append($include, $s);
3070
- } elseif (empty($block->dontAppend)) {
3071
- $type = isset($block->type) ? $block->type : "block";
3072
- $this->append(array($type, $block), $s);
3073
- }
3074
- return true;
3075
- }
3076
-
3077
- // extra stuff
3078
- if ($this->literal(";") ||
3079
- $this->literal("<!--"))
3080
- {
3081
- return true;
3082
- }
3083
-
3084
- return false;
3085
- }
3086
-
3087
- protected function stripDefault(&$value) {
3088
- $def = end($value[2]);
3089
- if ($def[0] == "keyword" && $def[1] == "!default") {
3090
- array_pop($value[2]);
3091
- $value = $this->flattenList($value);
3092
- return true;
3093
- }
3094
-
3095
- if ($def[0] == "list") {
3096
- return $this->stripDefault($value[2][count($value[2]) - 1]);
3097
- }
3098
-
3099
- return false;
3100
- }
3101
-
3102
- protected function literal($what, $eatWhitespace = null) {
3103
- if (!isset($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault;
3104
-
3105
- // shortcut on single letter
3106
- if (!isset($what[1]) && isset($this->buffer[$this->count])) {
3107
- if ($this->buffer[$this->count] == $what) {
3108
- if (!$eatWhitespace) {
3109
- $this->count++;
3110
- return true;
3111
- }
3112
- // goes below...
3113
- } else {
3114
- return false;
3115
- }
3116
- }
3117
-
3118
- return $this->match($this->preg_quote($what), $m, $eatWhitespace);
3119
- }
3120
-
3121
- // tree builders
3122
-
3123
- protected function pushBlock($selectors) {
3124
- $b = new stdClass;
3125
- $b->parent = $this->env; // not sure if we need this yet
3126
-
3127
- $b->selectors = $selectors;
3128
- $b->children = array();
3129
-
3130
- $this->env = $b;
3131
- return $b;
3132
- }
3133
-
3134
- protected function pushSpecialBlock($type) {
3135
- $block = $this->pushBlock(null);
3136
- $block->type = $type;
3137
- return $block;
3138
- }
3139
-
3140
- protected function popBlock() {
3141
- if (empty($this->env->parent)) {
3142
- $this->throwParseError("unexpected }");
3143
- }
3144
-
3145
- $old = $this->env;
3146
- $this->env = $this->env->parent;
3147
- unset($old->parent);
3148
- return $old;
3149
- }
3150
-
3151
- protected function append($statement, $pos=null) {
3152
- if ($pos !== null) {
3153
- $statement[-1] = $pos;
3154
- if (!$this->rootParser) $statement[-2] = $this;
3155
- }
3156
- $this->env->children[] = $statement;
3157
- }
3158
-
3159
- // last child that was appended
3160
- protected function last() {
3161
- $i = count($this->env->children) - 1;
3162
- if (isset($this->env->children[$i]))
3163
- return $this->env->children[$i];
3164
- }
3165
-
3166
- // high level parsers (they return parts of ast)
3167
-
3168
- protected function mediaQueryList(&$out) {
3169
- return $this->genericList($out, "mediaQuery", ",", false);
3170
- }
3171
-
3172
- protected function mediaQuery(&$out) {
3173
- $s = $this->seek();
3174
-
3175
- $expressions = null;
3176
- $parts = array();
3177
-
3178
- if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->mixedKeyword($mediaType)) {
3179
- $prop = array("mediaType");
3180
- if (isset($only)) $prop[] = array("keyword", "only");
3181
- if (isset($not)) $prop[] = array("keyword", "not");
3182
- $media = array("list", "", array());
3183
- foreach ((array)$mediaType as $type) {
3184
- if (is_array($type)) {
3185
- $media[2][] = $type;
3186
- } else {
3187
- $media[2][] = array("keyword", $type);
3188
- }
3189
- }
3190
- $prop[] = $media;
3191
- $parts[] = $prop;
3192
- }
3193
-
3194
- if (empty($parts) || $this->literal("and")) {
3195
- $this->genericList($expressions, "mediaExpression", "and", false);
3196
- if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
3197
- }
3198
-
3199
- $out = $parts;
3200
- return true;
3201
- }
3202
-
3203
- protected function mediaExpression(&$out) {
3204
- $s = $this->seek();
3205
- $value = null;
3206
- if ($this->literal("(") &&
3207
- $this->expression($feature) &&
3208
- ($this->literal(":") && $this->expression($value) || true) &&
3209
- $this->literal(")"))
3210
- {
3211
- $out = array("mediaExp", $feature);
3212
- if ($value) $out[] = $value;
3213
- return true;
3214
- }
3215
-
3216
- $this->seek($s);
3217
- return false;
3218
- }
3219
-
3220
- protected function argValues(&$out) {
3221
- if ($this->genericList($list, "argValue", ",", false)) {
3222
- $out = $list[2];
3223
- return true;
3224
- }
3225
- return false;
3226
- }
3227
-
3228
- protected function argValue(&$out) {
3229
- $s = $this->seek();
3230
-
3231
- $keyword = null;
3232
- if (!$this->variable($keyword) || !$this->literal(":")) {
3233
- $this->seek($s);
3234
- $keyword = null;
3235
- }
3236
-
3237
- if ($this->genericList($value, "expression")) {
3238
- $out = array($keyword, $value, false);
3239
- $s = $this->seek();
3240
- if ($this->literal("...")) {
3241
- $out[2] = true;
3242
- } else {
3243
- $this->seek($s);
3244
- }
3245
- return true;
3246
- }
3247
-
3248
- return false;
3249
- }
3250
-
3251
- /**
3252
- * Parse list
3253
- *
3254
- * @param string $out
3255
- *
3256
- * @return boolean
3257
- */
3258
- public function valueList(&$out)
3259
- {
3260
- return $this->genericList($out, 'spaceList', ',');
3261
- }
3262
-
3263
- protected function spaceList(&$out)
3264
- {
3265
- return $this->genericList($out, 'expression');
3266
- }
3267
-
3268
- protected function genericList(&$out, $parseItem, $delim="", $flatten=true) {
3269
- $s = $this->seek();
3270
- $items = array();
3271
- while ($this->$parseItem($value)) {
3272
- $items[] = $value;
3273
- if ($delim) {
3274
- if (!$this->literal($delim)) break;
3275
- }
3276
- }
3277
-
3278
- if (count($items) == 0) {
3279
- $this->seek($s);
3280
- return false;
3281
- }
3282
-
3283
- if ($flatten && count($items) == 1) {
3284
- $out = $items[0];
3285
- } else {
3286
- $out = array("list", $delim, $items);
3287
- }
3288
-
3289
- return true;
3290
- }
3291
-
3292
- protected function expression(&$out) {
3293
- $s = $this->seek();
3294
-
3295
- if ($this->literal("(")) {
3296
- if ($this->literal(")")) {
3297
- $out = array("list", "", array());
3298
- return true;
3299
- }
3300
-
3301
- if ($this->valueList($out) && $this->literal(')') && $out[0] == "list") {
3302
- return true;
3303
- }
3304
-
3305
- $this->seek($s);
3306
- }
3307
-
3308
- if ($this->value($lhs)) {
3309
- $out = $this->expHelper($lhs, 0);
3310
- return true;
3311
- }
3312
-
3313
- return false;
3314
- }
3315
-
3316
- protected function expHelper($lhs, $minP) {
3317
- $opstr = self::$operatorStr;
3318
-
3319
- $ss = $this->seek();
3320
- $whiteBefore = isset($this->buffer[$this->count - 1]) &&
3321
- ctype_space($this->buffer[$this->count - 1]);
3322
- while ($this->match($opstr, $m) && self::$precedence[$m[1]] >= $minP) {
3323
- $whiteAfter = isset($this->buffer[$this->count - 1]) &&
3324
- ctype_space($this->buffer[$this->count - 1]);
3325
-
3326
- $op = $m[1];
3327
-
3328
- // don't turn negative numbers into expressions
3329
- if ($op == "-" && $whiteBefore) {
3330
- if (!$whiteAfter) break;
3331
- }
3332
-
3333
- if (!$this->value($rhs)) break;
3334
-
3335
- // peek and see if rhs belongs to next operator
3336
- if ($this->peek($opstr, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
3337
- $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
3338
- }
3339
-
3340
- $lhs = array("exp", $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter);
3341
- $ss = $this->seek();
3342
- $whiteBefore = isset($this->buffer[$this->count - 1]) &&
3343
- ctype_space($this->buffer[$this->count - 1]);
3344
- }
3345
-
3346
- $this->seek($ss);
3347
- return $lhs;
3348
- }
3349
-
3350
- protected function value(&$out) {
3351
- $s = $this->seek();
3352
-
3353
- if ($this->literal("not", false) && $this->whitespace() && $this->value($inner)) {
3354
- $out = array("unary", "not", $inner, $this->inParens);
3355
- return true;
3356
- } else {
3357
- $this->seek($s);
3358
- }
3359
-
3360
- if ($this->literal("+") && $this->value($inner)) {
3361
- $out = array("unary", "+", $inner, $this->inParens);
3362
- return true;
3363
- } else {
3364
- $this->seek($s);
3365
- }
3366
-
3367
- // negation
3368
- if ($this->literal("-", false) &&
3369
- ($this->variable($inner) ||
3370
- $this->unit($inner) ||
3371
- $this->parenValue($inner)))
3372
- {
3373
- $out = array("unary", "-", $inner, $this->inParens);
3374
- return true;
3375
- } else {
3376
- $this->seek($s);
3377
- }
3378
-
3379
- if ($this->parenValue($out)) return true;
3380
- if ($this->interpolation($out)) return true;
3381
- if ($this->variable($out)) return true;
3382
- if ($this->color($out)) return true;
3383
- if ($this->unit($out)) return true;
3384
- if ($this->string($out)) return true;
3385
- if ($this->func($out)) return true;
3386
- if ($this->progid($out)) return true;
3387
-
3388
- if ($this->keyword($keyword)) {
3389
- if ($keyword == "null") {
3390
- $out = array("null");
3391
- } else {
3392
- $out = array("keyword", $keyword);
3393
- }
3394
- return true;
3395
- }
3396
-
3397
- return false;
3398
- }
3399
-
3400
- // value wrappen in parentheses
3401
- protected function parenValue(&$out) {
3402
- $s = $this->seek();
3403
-
3404
- $inParens = $this->inParens;
3405
- if ($this->literal("(") &&
3406
- ($this->inParens = true) && $this->expression($exp) &&
3407
- $this->literal(")"))
3408
- {
3409
- $out = $exp;
3410
- $this->inParens = $inParens;
3411
- return true;
3412
- } else {
3413
- $this->inParens = $inParens;
3414
- $this->seek($s);
3415
- }
3416
-
3417
- return false;
3418
- }
3419
-
3420
- protected function progid(&$out) {
3421
- $s = $this->seek();
3422
- if ($this->literal("progid:", false) &&
3423
- $this->openString("(", $fn) &&
3424
- $this->literal("("))
3425
- {
3426
- $this->openString(")", $args, "(");
3427
- if ($this->literal(")")) {
3428
- $out = array("string", "", array(
3429
- "progid:", $fn, "(", $args, ")"
3430
- ));
3431
- return true;
3432
- }
3433
- }
3434
-
3435
- $this->seek($s);
3436
- return false;
3437
- }
3438
-
3439
- protected function func(&$func) {
3440
- $s = $this->seek();
3441
-
3442
- if ($this->keyword($name, false) &&
3443
- $this->literal("("))
3444
- {
3445
- if ($name == "alpha" && $this->argumentList($args)) {
3446
- $func = array("function", $name, array("string", "", $args));
3447
- return true;
3448
- }
3449
-
3450
- if ($name != "expression" && !preg_match("/^(-[a-z]+-)?calc$/", $name)) {
3451
- $ss = $this->seek();
3452
- if ($this->argValues($args) && $this->literal(")")) {
3453
- $func = array("fncall", $name, $args);
3454
- return true;
3455
- }
3456
- $this->seek($ss);
3457
- }
3458
-
3459
- if (($this->openString(")", $str, "(") || true ) &&
3460
- $this->literal(")"))
3461
- {
3462
- $args = array();
3463
- if (!empty($str)) {
3464
- $args[] = array(null, array("string", "", array($str)));
3465
- }
3466
-
3467
- $func = array("fncall", $name, $args);
3468
- return true;
3469
- }
3470
- }
3471
-
3472
- $this->seek($s);
3473
- return false;
3474
- }
3475
-
3476
- protected function argumentList(&$out) {
3477
- $s = $this->seek();
3478
- $this->literal("(");
3479
-
3480
- $args = array();
3481
- while ($this->keyword($var)) {
3482
- $ss = $this->seek();
3483
-
3484
- if ($this->literal("=") && $this->expression($exp)) {
3485
- $args[] = array("string", "", array($var."="));
3486
- $arg = $exp;
3487
- } else {
3488
- break;
3489
- }
3490
-
3491
- $args[] = $arg;
3492
-
3493
- if (!$this->literal(",")) break;
3494
-
3495
- $args[] = array("string", "", array(", "));
3496
- }
3497
-
3498
- if (!$this->literal(")") || !count($args)) {
3499
- $this->seek($s);
3500
- return false;
3501
- }
3502
-
3503
- $out = $args;
3504
- return true;
3505
- }
3506
-
3507
- protected function argumentDef(&$out) {
3508
- $s = $this->seek();
3509
- $this->literal("(");
3510
-
3511
- $args = array();
3512
- while ($this->variable($var)) {
3513
- $arg = array($var[1], null, false);
3514
-
3515
- $ss = $this->seek();
3516
- if ($this->literal(":") && $this->genericList($defaultVal, "expression")) {
3517
- $arg[1] = $defaultVal;
3518
- } else {
3519
- $this->seek($ss);
3520
- }
3521
-
3522
- $ss = $this->seek();
3523
- if ($this->literal("...")) {
3524
- $sss = $this->seek();
3525
- if (!$this->literal(")")) {
3526
- $this->throwParseError("... has to be after the final argument");
3527
- }
3528
- $arg[2] = true;
3529
- $this->seek($sss);
3530
- } else {
3531
- $this->seek($ss);
3532
- }
3533
-
3534
- $args[] = $arg;
3535
- if (!$this->literal(",")) break;
3536
- }
3537
-
3538
- if (!$this->literal(")")) {
3539
- $this->seek($s);
3540
- return false;
3541
- }
3542
-
3543
- $out = $args;
3544
- return true;
3545
- }
3546
-
3547
- protected function color(&$out) {
3548
- $color = array('color');
3549
-
3550
- if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
3551
- if (isset($m[3])) {
3552
- $num = $m[3];
3553
- $width = 16;
3554
- } else {
3555
- $num = $m[2];
3556
- $width = 256;
3557
- }
3558
-
3559
- $num = hexdec($num);
3560
- foreach (array(3,2,1) as $i) {
3561
- $t = $num % $width;
3562
- $num /= $width;
3563
-
3564
- $color[$i] = $t * (256/$width) + $t * floor(16/$width);
3565
- }
3566
-
3567
- $out = $color;
3568
- return true;
3569
- }
3570
-
3571
- return false;
3572
- }
3573
-
3574
- protected function unit(&$unit) {
3575
- if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
3576
- $unit = array("number", $m[1], empty($m[3]) ? "" : $m[3]);
3577
- return true;
3578
- }
3579
- return false;
3580
- }
3581
-
3582
- protected function string(&$out) {
3583
- $s = $this->seek();
3584
- if ($this->literal('"', false)) {
3585
- $delim = '"';
3586
- } elseif ($this->literal("'", false)) {
3587
- $delim = "'";
3588
- } else {
3589
- return false;
3590
- }
3591
-
3592
- $content = array();
3593
- $oldWhite = $this->eatWhiteDefault;
3594
- $this->eatWhiteDefault = false;
3595
-
3596
- while ($this->matchString($m, $delim)) {
3597
- $content[] = $m[1];
3598
- if ($m[2] == "#{") {
3599
- $this->count -= strlen($m[2]);
3600
- if ($this->interpolation($inter, false)) {
3601
- $content[] = $inter;
3602
- } else {
3603
- $this->count += strlen($m[2]);
3604
- $content[] = "#{"; // ignore it
3605
- }
3606
- } elseif ($m[2] == '\\') {
3607
- $content[] = $m[2];
3608
- if ($this->literal($delim, false)) {
3609
- $content[] = $delim;
3610
- }
3611
- } else {
3612
- $this->count -= strlen($delim);
3613
- break; // delim
3614
- }
3615
- }
3616
-
3617
- $this->eatWhiteDefault = $oldWhite;
3618
-
3619
- if ($this->literal($delim)) {
3620
- $out = array("string", $delim, $content);
3621
- return true;
3622
- }
3623
-
3624
- $this->seek($s);
3625
- return false;
3626
- }
3627
-
3628
- protected function mixedKeyword(&$out) {
3629
- $s = $this->seek();
3630
-
3631
- $parts = array();
3632
-
3633
- $oldWhite = $this->eatWhiteDefault;
3634
- $this->eatWhiteDefault = false;
3635
-
3636
- while (true) {
3637
- if ($this->keyword($key)) {
3638
- $parts[] = $key;
3639
- continue;
3640
- }
3641
-
3642
- if ($this->interpolation($inter)) {
3643
- $parts[] = $inter;
3644
- continue;
3645
- }
3646
-
3647
- break;
3648
- }
3649
-
3650
- $this->eatWhiteDefault = $oldWhite;
3651
-
3652
- if (count($parts) == 0) return false;
3653
-
3654
- if ($this->eatWhiteDefault) {
3655
- $this->whitespace();
3656
- }
3657
-
3658
- $out = $parts;
3659
- return true;
3660
- }
3661
-
3662
- // an unbounded string stopped by $end
3663
- protected function openString($end, &$out, $nestingOpen=null) {
3664
- $oldWhite = $this->eatWhiteDefault;
3665
- $this->eatWhiteDefault = false;
3666
-
3667
- $stop = array("'", '"', "#{", $end);
3668
- $stop = array_map(array($this, "preg_quote"), $stop);
3669
- $stop[] = self::$commentMulti;
3670
-
3671
- $patt = '(.*?)('.implode("|", $stop).')';
3672
-
3673
- $nestingLevel = 0;
3674
-
3675
- $content = array();
3676
- while ($this->match($patt, $m, false)) {
3677
- if (isset($m[1]) && $m[1] !== '') {
3678
- $content[] = $m[1];
3679
- if ($nestingOpen) {
3680
- $nestingLevel += substr_count($m[1], $nestingOpen);
3681
- }
3682
- }
3683
-
3684
- $tok = $m[2];
3685
-
3686
- $this->count-= strlen($tok);
3687
- if ($tok == $end) {
3688
- if ($nestingLevel == 0) {
3689
- break;
3690
- } else {
3691
- $nestingLevel--;
3692
- }
3693
- }
3694
-
3695
- if (($tok == "'" || $tok == '"') && $this->string($str)) {
3696
- $content[] = $str;
3697
- continue;
3698
- }
3699
-
3700
- if ($tok == "#{" && $this->interpolation($inter)) {
3701
- $content[] = $inter;
3702
- continue;
3703
- }
3704
-
3705
- $content[] = $tok;
3706
- $this->count+= strlen($tok);
3707
- }
3708
-
3709
- $this->eatWhiteDefault = $oldWhite;
3710
-
3711
- if (count($content) == 0) return false;
3712
-
3713
- // trim the end
3714
- if (is_string(end($content))) {
3715
- $content[count($content) - 1] = rtrim(end($content));
3716
- }
3717
-
3718
- $out = array("string", "", $content);
3719
- return true;
3720
- }
3721
-
3722
- // $lookWhite: save information about whitespace before and after
3723
- protected function interpolation(&$out, $lookWhite=true) {
3724
- $oldWhite = $this->eatWhiteDefault;
3725
- $this->eatWhiteDefault = true;
3726
-
3727
- $s = $this->seek();
3728
- if ($this->literal("#{") && $this->valueList($value) && $this->literal("}", false)) {
3729
-
3730
- // TODO: don't error if out of bounds
3731
-
3732
- if ($lookWhite) {
3733
- $left = preg_match('/\s/', $this->buffer[$s - 1]) ? " " : "";
3734
- $right = preg_match('/\s/', $this->buffer[$this->count]) ? " ": "";
3735
- } else {
3736
- $left = $right = false;
3737
- }
3738
-
3739
- $out = array("interpolate", $value, $left, $right);
3740
- $this->eatWhiteDefault = $oldWhite;
3741
- if ($this->eatWhiteDefault) $this->whitespace();
3742
- return true;
3743
- }
3744
-
3745
- $this->seek($s);
3746
- $this->eatWhiteDefault = $oldWhite;
3747
- return false;
3748
- }
3749
-
3750
- // low level parsers
3751
-
3752
- // returns an array of parts or a string
3753
- protected function propertyName(&$out) {
3754
- $s = $this->seek();
3755
- $parts = array();
3756
-
3757
- $oldWhite = $this->eatWhiteDefault;
3758
- $this->eatWhiteDefault = false;
3759
-
3760
- while (true) {
3761
- if ($this->interpolation($inter)) {
3762
- $parts[] = $inter;
3763
- } elseif ($this->keyword($text)) {
3764
- $parts[] = $text;
3765
- } elseif (count($parts) == 0 && $this->match('[:.#]', $m, false)) {
3766
- // css hacks
3767
- $parts[] = $m[0];
3768
- } else {
3769
- break;
3770
- }
3771
- }
3772
-
3773
- $this->eatWhiteDefault = $oldWhite;
3774
- if (count($parts) == 0) return false;
3775
-
3776
- // match comment hack
3777
- if (preg_match(self::$whitePattern,
3778
- $this->buffer, $m, null, $this->count))
3779
- {
3780
- if (!empty($m[0])) {
3781
- $parts[] = $m[0];
3782
- $this->count += strlen($m[0]);
3783
- }
3784
- }
3785
-
3786
- $this->whitespace(); // get any extra whitespace
3787
-
3788
- $out = array("string", "", $parts);
3789
- return true;
3790
- }
3791
-
3792
- // comma separated list of selectors
3793
- protected function selectors(&$out) {
3794
- $s = $this->seek();
3795
- $selectors = array();
3796
- while ($this->selector($sel)) {
3797
- $selectors[] = $sel;
3798
- if (!$this->literal(",")) break;
3799
- while ($this->literal(",")); // ignore extra
3800
- }
3801
-
3802
- if (count($selectors) == 0) {
3803
- $this->seek($s);
3804
- return false;
3805
- }
3806
-
3807
- $out = $selectors;
3808
- return true;
3809
- }
3810
-
3811
- // whitespace separated list of selectorSingle
3812
- protected function selector(&$out) {
3813
- $selector = array();
3814
-
3815
- while (true) {
3816
- if ($this->match('[>+~]+', $m)) {
3817
- $selector[] = array($m[0]);
3818
- } elseif ($this->selectorSingle($part)) {
3819
- $selector[] = $part;
3820
- $this->whitespace();
3821
- } elseif ($this->match('\/[^\/]+\/', $m)) {
3822
- $selector[] = array($m[0]);
3823
- } else {
3824
- break;
3825
- }
3826
-
3827
- }
3828
-
3829
- if (count($selector) == 0) {
3830
- return false;
3831
- }
3832
-
3833
- $out = $selector;
3834
- return true;
3835
- }
3836
-
3837
- // the parts that make up
3838
- // div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
3839
- protected function selectorSingle(&$out) {
3840
- $oldWhite = $this->eatWhiteDefault;
3841
- $this->eatWhiteDefault = false;
3842
-
3843
- $parts = array();
3844
-
3845
- if ($this->literal("*", false)) {
3846
- $parts[] = "*";
3847
- }
3848
-
3849
- while (true) {
3850
- // see if we can stop early
3851
- if ($this->match("\s*[{,]", $m)) {
3852
- $this->count--;
3853
- break;
3854
- }
3855
-
3856
- $s = $this->seek();
3857
- // self
3858
- if ($this->literal("&", false)) {
3859
- $parts[] = scssc::$selfSelector;
3860
- continue;
3861
- }
3862
-
3863
- if ($this->literal(".", false)) {
3864
- $parts[] = ".";
3865
- continue;
3866
- }
3867
-
3868
- if ($this->literal("|", false)) {
3869
- $parts[] = "|";
3870
- continue;
3871
- }
3872
-
3873
- // for keyframes
3874
- if ($this->unit($unit)) {
3875
- $parts[] = $unit;
3876
- continue;
3877
- }
3878
-
3879
- if ($this->keyword($name)) {
3880
- $parts[] = $name;
3881
- continue;
3882
- }
3883
-
3884
- if ($this->interpolation($inter)) {
3885
- $parts[] = $inter;
3886
- continue;
3887
- }
3888
-
3889
- if ($this->literal('%', false) && $this->placeholder($placeholder)) {
3890
- $parts[] = '%';
3891
- $parts[] = $placeholder;
3892
- continue;
3893
- }
3894
-
3895
- if ($this->literal("#", false)) {
3896
- $parts[] = "#";
3897
- continue;
3898
- }
3899
-
3900
- // a pseudo selector
3901
- if ($this->match("::?", $m) && $this->mixedKeyword($nameParts)) {
3902
- $parts[] = $m[0];
3903
- foreach ($nameParts as $sub) {
3904
- $parts[] = $sub;
3905
- }
3906
-
3907
- $ss = $this->seek();
3908
- if ($this->literal("(") &&
3909
- ($this->openString(")", $str, "(") || true ) &&
3910
- $this->literal(")"))
3911
- {
3912
- $parts[] = "(";
3913
- if (!empty($str)) $parts[] = $str;
3914
- $parts[] = ")";
3915
- } else {
3916
- $this->seek($ss);
3917
- }
3918
-
3919
- continue;
3920
- } else {
3921
- $this->seek($s);
3922
- }
3923
-
3924
- // attribute selector
3925
- // TODO: replace with open string?
3926
- if ($this->literal("[", false)) {
3927
- $attrParts = array("[");
3928
- // keyword, string, operator
3929
- while (true) {
3930
- if ($this->literal("]", false)) {
3931
- $this->count--;
3932
- break; // get out early
3933
- }
3934
-
3935
- if ($this->match('\s+', $m)) {
3936
- $attrParts[] = " ";
3937
- continue;
3938
- }
3939
- if ($this->string($str)) {
3940
- $attrParts[] = $str;
3941
- continue;
3942
- }
3943
-
3944
- if ($this->keyword($word)) {
3945
- $attrParts[] = $word;
3946
- continue;
3947
- }
3948
-
3949
- if ($this->interpolation($inter, false)) {
3950
- $attrParts[] = $inter;
3951
- continue;
3952
- }
3953
-
3954
- // operator, handles attr namespace too
3955
- if ($this->match('[|-~\$\*\^=]+', $m)) {
3956
- $attrParts[] = $m[0];
3957
- continue;
3958
- }
3959
-
3960
- break;
3961
- }
3962
-
3963
- if ($this->literal("]", false)) {
3964
- $attrParts[] = "]";
3965
- foreach ($attrParts as $part) {
3966
- $parts[] = $part;
3967
- }
3968
- continue;
3969
- }
3970
- $this->seek($s);
3971
- // should just break here?
3972
- }
3973
-
3974
- break;
3975
- }
3976
-
3977
- $this->eatWhiteDefault = $oldWhite;
3978
-
3979
- if (count($parts) == 0) return false;
3980
-
3981
- $out = $parts;
3982
- return true;
3983
- }
3984
-
3985
- protected function variable(&$out) {
3986
- $s = $this->seek();
3987
- if ($this->literal("$", false) && $this->keyword($name)) {
3988
- $out = array("var", $name);
3989
- return true;
3990
- }
3991
- $this->seek($s);
3992
- return false;
3993
- }
3994
-
3995
- protected function keyword(&$word, $eatWhitespace = null) {
3996
- if ($this->match('([\w_\-\*!"\'\\\\][\w\-_"\'\\\\]*)',
3997
- $m, $eatWhitespace))
3998
- {
3999
- $word = $m[1];
4000
- return true;
4001
- }
4002
- return false;
4003
- }
4004
-
4005
- protected function placeholder(&$placeholder) {
4006
- if ($this->match('([\w\-_]+)', $m)) {
4007
- $placeholder = $m[1];
4008
- return true;
4009
- }
4010
- return false;
4011
- }
4012
-
4013
- // consume an end of statement delimiter
4014
- protected function end() {
4015
- if ($this->literal(';')) {
4016
- return true;
4017
- } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
4018
- // if there is end of file or a closing block next then we don't need a ;
4019
- return true;
4020
- }
4021
- return false;
4022
- }
4023
-
4024
- // advance counter to next occurrence of $what
4025
- // $until - don't include $what in advance
4026
- // $allowNewline, if string, will be used as valid char set
4027
- protected function to($what, &$out, $until = false, $allowNewline = false) {
4028
- if (is_string($allowNewline)) {
4029
- $validChars = $allowNewline;
4030
- } else {
4031
- $validChars = $allowNewline ? "." : "[^\n]";
4032
- }
4033
- if (!$this->match('('.$validChars.'*?)'.$this->preg_quote($what), $m, !$until)) return false;
4034
- if ($until) $this->count -= strlen($what); // give back $what
4035
- $out = $m[1];
4036
- return true;
4037
- }
4038
-
4039
- public function throwParseError($msg = "parse error", $count = null) {
4040
- $count = !isset($count) ? $this->count : $count;
4041
-
4042
- $line = $this->getLineNo($count);
4043
-
4044
- if (!empty($this->sourceName)) {
4045
- $loc = "$this->sourceName on line $line";
4046
- } else {
4047
- $loc = "line: $line";
4048
- }
4049
-
4050
- if ($this->peek("(.*?)(\n|$)", $m, $count)) {
4051
- throw new Exception("$msg: failed at `$m[1]` $loc");
4052
- } else {
4053
- throw new Exception("$msg: $loc");
4054
- }
4055
- }
4056
-
4057
- public function getLineNo($pos) {
4058
- return 1 + substr_count(substr($this->buffer, 0, $pos), "\n");
4059
- }
4060
-
4061
- /**
4062
- * Match string looking for either ending delim, escape, or string interpolation
4063
- *
4064
- * {@internal This is a workaround for preg_match's 250K string match limit. }}
4065
- *
4066
- * @param array $m Matches (passed by reference)
4067
- * @param string $delim Delimeter
4068
- *
4069
- * @return boolean True if match; false otherwise
4070
- */
4071
- protected function matchString(&$m, $delim) {
4072
- $token = null;
4073
-
4074
- $end = strpos($this->buffer, "\n", $this->count);
4075
- if ($end === false || $this->buffer[$end - 1] == '\\' || $this->buffer[$end - 2] == '\\' && $this->buffer[$end - 1] == "\r") {
4076
- $end = strlen($this->buffer);
4077
- }
4078
-
4079
- // look for either ending delim, escape, or string interpolation
4080
- foreach (array('#{', '\\', $delim) as $lookahead) {
4081
- $pos = strpos($this->buffer, $lookahead, $this->count);
4082
- if ($pos !== false && $pos < $end) {
4083
- $end = $pos;
4084
- $token = $lookahead;
4085
- }
4086
- }
4087
-
4088
- if (!isset($token)) {
4089
- return false;
4090
- }
4091
-
4092
- $match = substr($this->buffer, $this->count, $end - $this->count);
4093
- $m = array(
4094
- $match . $token,
4095
- $match,
4096
- $token
4097
- );
4098
- $this->count = $end + strlen($token);
4099
-
4100
- return true;
4101
- }
4102
-
4103
- // try to match something on head of buffer
4104
- protected function match($regex, &$out, $eatWhitespace = null) {
4105
- if (!isset($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault;
4106
-
4107
- $r = '/'.$regex.'/Ais';
4108
- if (preg_match($r, $this->buffer, $out, null, $this->count)) {
4109
- $this->count += strlen($out[0]);
4110
- if ($eatWhitespace) $this->whitespace();
4111
- return true;
4112
- }
4113
- return false;
4114
- }
4115
-
4116
- // match some whitespace
4117
- protected function whitespace() {
4118
- $gotWhite = false;
4119
- while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
4120
- if ($this->insertComments) {
4121
- if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
4122
- $this->append(array("comment", $m[1]));
4123
- $this->commentsSeen[$this->count] = true;
4124
- }
4125
- }
4126
- $this->count += strlen($m[0]);
4127
- $gotWhite = true;
4128
- }
4129
- return $gotWhite;
4130
- }
4131
-
4132
- protected function peek($regex, &$out, $from=null) {
4133
- if (!isset($from)) $from = $this->count;
4134
-
4135
- $r = '/'.$regex.'/Ais';
4136
- $result = preg_match($r, $this->buffer, $out, null, $from);
4137
-
4138
- return $result;
4139
- }
4140
-
4141
- protected function seek($where = null) {
4142
- if ($where === null) return $this->count;
4143
- else $this->count = $where;
4144
- return true;
4145
- }
4146
-
4147
- static function preg_quote($what) {
4148
- return preg_quote($what, '/');
4149
- }
4150
-
4151
- protected function show() {
4152
- if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
4153
- return $m[1];
4154
- }
4155
- return "";
4156
- }
4157
-
4158
- // turn list of length 1 into value type
4159
- protected function flattenList($value) {
4160
- if ($value[0] == "list" && count($value[2]) == 1) {
4161
- return $this->flattenList($value[2][0]);
4162
- }
4163
- return $value;
4164
- }
4165
- }
4166
-
4167
- /**
4168
- * SCSS base formatter
4169
- *
4170
- * @author Leaf Corcoran <leafot@gmail.com>
4171
- */
4172
- class scss_formatter {
4173
- public $indentChar = " ";
4174
-
4175
- public $break = "\n";
4176
- public $open = " {";
4177
- public $close = "}";
4178
- public $tagSeparator = ", ";
4179
- public $assignSeparator = ": ";
4180
-
4181
- public function __construct() {
4182
- $this->indentLevel = 0;
4183
- }
4184
-
4185
- public function indentStr($n = 0) {
4186
- return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
4187
- }
4188
-
4189
- public function property($name, $value) {
4190
- return $name . $this->assignSeparator . $value . ";";
4191
- }
4192
-
4193
- protected function block($block) {
4194
- if (empty($block->lines) && empty($block->children)) return;
4195
-
4196
- $inner = $pre = $this->indentStr();
4197
-
4198
- if (!empty($block->selectors)) {
4199
- echo $pre .
4200
- implode($this->tagSeparator, $block->selectors) .
4201
- $this->open . $this->break;
4202
- $this->indentLevel++;
4203
- $inner = $this->indentStr();
4204
- }
4205
-
4206
- if (!empty($block->lines)) {
4207
- $glue = $this->break.$inner;
4208
- echo $inner . implode($glue, $block->lines);
4209
- if (!empty($block->children)) {
4210
- echo $this->break;
4211
- }
4212
- }
4213
-
4214
- foreach ($block->children as $child) {
4215
- $this->block($child);
4216
- }
4217
-
4218
- if (!empty($block->selectors)) {
4219
- $this->indentLevel--;
4220
- if (empty($block->children)) echo $this->break;
4221
- echo $pre . $this->close . $this->break;
4222
- }
4223
- }
4224
-
4225
- public function format($block) {
4226
- ob_start();
4227
- $this->block($block);
4228
- $out = ob_get_clean();
4229
-
4230
- return $out;
4231
- }
4232
- }
4233
-
4234
- /**
4235
- * SCSS nested formatter
4236
- *
4237
- * @author Leaf Corcoran <leafot@gmail.com>
4238
- */
4239
- class scss_formatter_nested extends scss_formatter {
4240
- public $close = " }";
4241
-
4242
- // adjust the depths of all children, depth first
4243
- public function adjustAllChildren($block) {
4244
- // flatten empty nested blocks
4245
- $children = array();
4246
- foreach ($block->children as $i => $child) {
4247
- if (empty($child->lines) && empty($child->children)) {
4248
- if (isset($block->children[$i + 1])) {
4249
- $block->children[$i + 1]->depth = $child->depth;
4250
- }
4251
- continue;
4252
- }
4253
- $children[] = $child;
4254
- }
4255
-
4256
- $count = count($children);
4257
- for ($i = 0; $i < $count; $i++) {
4258
- $depth = $children[$i]->depth;
4259
- $j = $i + 1;
4260
- if (isset($children[$j]) && $depth < $children[$j]->depth) {
4261
- $childDepth = $children[$j]->depth;
4262
- for (; $j < $count; $j++) {
4263
- if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) {
4264
- $children[$j]->depth = $depth + 1;
4265
- }
4266
- }
4267
- }
4268
- }
4269
-
4270
- $block->children = $children;
4271
-
4272
- // make relative to parent
4273
- foreach ($block->children as $child) {
4274
- $this->adjustAllChildren($child);
4275
- $child->depth = $child->depth - $block->depth;
4276
- }
4277
- }
4278
-
4279
- protected function block($block) {
4280
- if ($block->type == "root") {
4281
- $this->adjustAllChildren($block);
4282
- }
4283
-
4284
- $inner = $pre = $this->indentStr($block->depth - 1);
4285
- if (!empty($block->selectors)) {
4286
- echo $pre .
4287
- implode($this->tagSeparator, $block->selectors) .
4288
- $this->open . $this->break;
4289
- $this->indentLevel++;
4290
- $inner = $this->indentStr($block->depth - 1);
4291
- }
4292
-
4293
- if (!empty($block->lines)) {
4294
- $glue = $this->break.$inner;
4295
- echo $inner . implode($glue, $block->lines);
4296
- if (!empty($block->children)) echo $this->break;
4297
- }
4298
-
4299
- foreach ($block->children as $i => $child) {
4300
- // echo "*** block: ".$block->depth." child: ".$child->depth."\n";
4301
- $this->block($child);
4302
- if ($i < count($block->children) - 1) {
4303
- echo $this->break;
4304
-
4305
- if (isset($block->children[$i + 1])) {
4306
- $next = $block->children[$i + 1];
4307
- if ($next->depth == max($block->depth, 1) && $child->depth >= $next->depth) {
4308
- echo $this->break;
4309
- }
4310
- }
4311
- }
4312
- }
4313
-
4314
- if (!empty($block->selectors)) {
4315
- $this->indentLevel--;
4316
- echo $this->close;
4317
- }
4318
-
4319
- if ($block->type == "root") {
4320
- echo $this->break;
4321
- }
4322
- }
4323
- }
4324
-
4325
- /**
4326
- * SCSS compressed formatter
4327
- *
4328
- * @author Leaf Corcoran <leafot@gmail.com>
4329
- */
4330
- class scss_formatter_compressed extends scss_formatter {
4331
- public $open = "{";
4332
- public $tagSeparator = ",";
4333
- public $assignSeparator = ":";
4334
- public $break = "";
4335
-
4336
- public function indentStr($n = 0) {
4337
- return "";
4338
- }
4339
- }
4340
-
4341
- /**
4342
- * SCSS minified formatter
4343
- *
4344
- * @author Mike DeWitt
4345
- */
4346
- class scss_formatter_minified extends scss_formatter {
4347
- public $open = "{";
4348
- public $tagSeparator = ",";
4349
- public $assignSeparator = ":";
4350
- public $break = "";
4351
-
4352
- public function indentStr($n = 0) {
4353
- return "";
4354
- }
4355
-
4356
- public function format($block) {
4357
- ob_start();
4358
- $this->block($block);
4359
- $out = ob_get_clean();
4360
-
4361
- $out = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $out);
4362
-
4363
- return $out;
4364
- }
4365
  }
4366
 
4367
- /**
4368
- * SCSS server
4369
- *
4370
- * @author Leaf Corcoran <leafot@gmail.com>
4371
- */
4372
- class scss_server {
4373
- /**
4374
- * Join path components
4375
- *
4376
- * @param string $left Path component, left of the directory separator
4377
- * @param string $right Path component, right of the directory separator
4378
- *
4379
- * @return string
4380
- */
4381
- protected function join($left, $right) {
4382
- return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\');
4383
- }
4384
-
4385
- /**
4386
- * Get name of requested .scss file
4387
- *
4388
- * @return string|null
4389
- */
4390
- protected function inputName() {
4391
- switch (true) {
4392
- case isset($_GET['p']):
4393
- return $_GET['p'];
4394
- case isset($_SERVER['PATH_INFO']):
4395
- return $_SERVER['PATH_INFO'];
4396
- case isset($_SERVER['DOCUMENT_URI']):
4397
- return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME']));
4398
- }
4399
- }
4400
-
4401
- /**
4402
- * Get path to requested .scss file
4403
- *
4404
- * @return string
4405
- */
4406
- protected function findInput() {
4407
- if (($input = $this->inputName())
4408
- && strpos($input, '..') === false
4409
- && substr($input, -5) === '.scss'
4410
- ) {
4411
- $name = $this->join($this->dir, $input);
4412
-
4413
- if (is_file($name) && is_readable($name)) {
4414
- return $name;
4415
- }
4416
- }
4417
-
4418
- return false;
4419
- }
4420
-
4421
- /**
4422
- * Get path to cached .css file
4423
- *
4424
- * @return string
4425
- */
4426
- protected function cacheName($fname) {
4427
- return $this->join($this->cacheDir, md5($fname) . '.css');
4428
- }
4429
-
4430
- /**
4431
- * Get path to cached imports
4432
- *
4433
- * @return string
4434
- */
4435
- protected function importsCacheName($out) {
4436
- return $out . '.imports';
4437
- }
4438
-
4439
- /**
4440
- * Determine whether .scss file needs to be re-compiled.
4441
- *
4442
- * @param string $in Input path
4443
- * @param string $out Output path
4444
- *
4445
- * @return boolean True if compile required.
4446
- */
4447
- protected function needsCompile($in, $out) {
4448
- if (!is_file($out)) return true;
4449
-
4450
- $mtime = filemtime($out);
4451
- if (filemtime($in) > $mtime) return true;
4452
-
4453
- // look for modified imports
4454
- $icache = $this->importsCacheName($out);
4455
- if (is_readable($icache)) {
4456
- $imports = unserialize(file_get_contents($icache));
4457
- foreach ($imports as $import) {
4458
- if (filemtime($import) > $mtime) return true;
4459
- }
4460
- }
4461
- return false;
4462
- }
4463
-
4464
- /**
4465
- * Get If-Modified-Since header from client request
4466
- *
4467
- * @return string
4468
- */
4469
- protected function getModifiedSinceHeader()
4470
- {
4471
- $modifiedSince = '';
4472
-
4473
- if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
4474
- $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
4475
-
4476
- if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) {
4477
- $modifiedSince = substr($modifiedSince, 0, $semicolonPos);
4478
- }
4479
- }
4480
-
4481
- return $modifiedSince;
4482
- }
4483
-
4484
- /**
4485
- * Compile .scss file
4486
- *
4487
- * @param string $in Input path (.scss)
4488
- * @param string $out Output path (.css)
4489
- *
4490
- * @return string
4491
- */
4492
- protected function compile($in, $out) {
4493
- $start = microtime(true);
4494
- $css = $this->scss->compile(file_get_contents($in), $in);
4495
- $elapsed = round((microtime(true) - $start), 4);
4496
-
4497
- $v = scssc::$VERSION;
4498
- $t = @date('r');
4499
- $css = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css;
4500
-
4501
- file_put_contents($out, $css);
4502
- file_put_contents($this->importsCacheName($out),
4503
- serialize($this->scss->getParsedFiles()));
4504
- return $css;
4505
- }
4506
-
4507
- /**
4508
- * Compile requested scss and serve css. Outputs HTTP response.
4509
- *
4510
- * @param string $salt Prefix a string to the filename for creating the cache name hash
4511
- */
4512
- public function serve($salt = '') {
4513
- $protocol = isset($_SERVER['SERVER_PROTOCOL'])
4514
- ? $_SERVER['SERVER_PROTOCOL']
4515
- : 'HTTP/1.0';
4516
-
4517
- if ($input = $this->findInput()) {
4518
- $output = $this->cacheName($salt . $input);
4519
-
4520
- if ($this->needsCompile($input, $output)) {
4521
- try {
4522
- $css = $this->compile($input, $output);
4523
-
4524
- $lastModified = gmdate('D, d M Y H:i:s', filemtime($output)) . ' GMT';
4525
-
4526
- header('Last-Modified: ' . $lastModified);
4527
- header('Content-type: text/css');
4528
-
4529
- echo $css;
4530
-
4531
- return;
4532
- } catch (Exception $e) {
4533
- header($protocol . ' 500 Internal Server Error');
4534
- header('Content-type: text/plain');
4535
-
4536
- echo 'Parse error: ' . $e->getMessage() . "\n";
4537
- }
4538
- }
4539
-
4540
- header('X-SCSS-Cache: true');
4541
- header('Content-type: text/css');
4542
-
4543
- $modifiedSince = $this->getModifiedSinceHeader();
4544
- $mtime = filemtime($output);
4545
-
4546
- if (@strtotime($modifiedSince) === $mtime) {
4547
- header($protocol . ' 304 Not Modified');
4548
-
4549
- return;
4550
- }
4551
-
4552
- $lastModified = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
4553
- header('Last-Modified: ' . $lastModified);
4554
-
4555
- echo file_get_contents($output);
4556
-
4557
- return;
4558
- }
4559
-
4560
- header($protocol . ' 404 Not Found');
4561
- header('Content-type: text/plain');
4562
-
4563
- $v = scssc::$VERSION;
4564
- echo "/* INPUT NOT FOUND scss $v */\n";
4565
- }
4566
-
4567
- /**
4568
- * Constructor
4569
- *
4570
- * @param string $dir Root directory to .scss files
4571
- * @param string $cacheDir Cache directory
4572
- * @param \scssc|null $scss SCSS compiler instance
4573
- */
4574
- public function __construct($dir, $cacheDir=null, $scss=null) {
4575
- $this->dir = $dir;
4576
-
4577
- if (!isset($cacheDir)) {
4578
- $cacheDir = $this->join($dir, 'scss_cache');
4579
- }
4580
-
4581
- $this->cacheDir = $cacheDir;
4582
- if (!is_dir($this->cacheDir)) mkdir($this->cacheDir, 0755, true);
4583
-
4584
- if (!isset($scss)) {
4585
- $scss = new scssc();
4586
- $scss->setImportPaths($this->dir);
4587
- }
4588
- $this->scss = $scss;
4589
- }
4590
-
4591
- /**
4592
- * Helper method to serve compiled scss
4593
- *
4594
- * @param string $path Root path
4595
- */
4596
- static public function serveFrom($path) {
4597
- $server = new self($path);
4598
- $server->serve();
4599
- }
4600
  }
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('scssc', 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/ServerException.php';
15
+ include_once __DIR__ . '/src/Formatter.php';
16
+ include_once __DIR__ . '/src/Formatter/Compact.php';
17
+ include_once __DIR__ . '/src/Formatter/Compressed.php';
18
+ include_once __DIR__ . '/src/Formatter/Crunched.php';
19
+ include_once __DIR__ . '/src/Formatter/Debug.php';
20
+ include_once __DIR__ . '/src/Formatter/Expanded.php';
21
+ include_once __DIR__ . '/src/Formatter/Nested.php';
22
+ include_once __DIR__ . '/src/Formatter/OutputBlock.php';
23
+ include_once __DIR__ . '/src/Node.php';
24
+ include_once __DIR__ . '/src/Node/Number.php';
25
+ include_once __DIR__ . '/src/Parser.php';
26
+ include_once __DIR__ . '/src/Type.php';
27
+ include_once __DIR__ . '/src/Util.php';
28
+ include_once __DIR__ . '/src/Version.php';
29
+ include_once __DIR__ . '/src/Server.php';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  }
scssphp/src/Base/Range.php ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2015 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
16
+ *
17
+ * @author Anthon Pang <anthon.pang@gmail.com>
18
+ */
19
+ class Range
20
+ {
21
+ public $first;
22
+ public $last;
23
+
24
+ /**
25
+ * Initialize range
26
+ *
27
+ * @param integer|float $first
28
+ * @param integer|float $last
29
+ */
30
+ public function __construct($first, $last)
31
+ {
32
+ $this->first = $first;
33
+ $this->last = $last;
34
+ }
35
+
36
+ /**
37
+ * Test for inclusion in range
38
+ *
39
+ * @param integer|float $value
40
+ *
41
+ * @return boolean
42
+ */
43
+ public function includes($value)
44
+ {
45
+ return $value >= $this->first && $value <= $this->last;
46
+ }
47
+ }
scssphp/src/Block.php ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
16
+ *
17
+ * @author Anthon Pang <anthon.pang@gmail.com>
18
+ */
19
+ class Block
20
+ {
21
+ /**
22
+ * @var string
23
+ */
24
+ public $type;
25
+
26
+ /**
27
+ * @var \Leafo\ScssPhp\Block
28
+ */
29
+ public $parent;
30
+
31
+ /**
32
+ * @var integer
33
+ */
34
+ public $sourceIndex;
35
+
36
+ /**
37
+ * @var integer
38
+ */
39
+ public $sourceLine;
40
+
41
+ /**
42
+ * @var integer
43
+ */
44
+ public $sourceColumn;
45
+
46
+ /**
47
+ * @var array
48
+ */
49
+ public $selectors;
50
+
51
+ /**
52
+ * @var array
53
+ */
54
+ public $comments;
55
+
56
+ /**
57
+ * @var array
58
+ */
59
+ public $children;
60
+ }
scssphp/src/Colors.php ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
16
+ *
17
+ * @author Leaf Corcoran <leafot@gmail.com>
18
+ */
19
+ class Colors
20
+ {
21
+ /**
22
+ * CSS Colors
23
+ *
24
+ * @see http://www.w3.org/TR/css3-color
25
+ *
26
+ * @var array
27
+ */
28
+ public static $cssColors = [
29
+ 'aliceblue' => '240,248,255',
30
+ 'antiquewhite' => '250,235,215',
31
+ 'aqua' => '0,255,255',
32
+ 'aquamarine' => '127,255,212',
33
+ 'azure' => '240,255,255',
34
+ 'beige' => '245,245,220',
35
+ 'bisque' => '255,228,196',
36
+ 'black' => '0,0,0',
37
+ 'blanchedalmond' => '255,235,205',
38
+ 'blue' => '0,0,255',
39
+ 'blueviolet' => '138,43,226',
40
+ 'brown' => '165,42,42',
41
+ 'burlywood' => '222,184,135',
42
+ 'cadetblue' => '95,158,160',
43
+ 'chartreuse' => '127,255,0',
44
+ 'chocolate' => '210,105,30',
45
+ 'coral' => '255,127,80',
46
+ 'cornflowerblue' => '100,149,237',
47
+ 'cornsilk' => '255,248,220',
48
+ 'crimson' => '220,20,60',
49
+ 'cyan' => '0,255,255',
50
+ 'darkblue' => '0,0,139',
51
+ 'darkcyan' => '0,139,139',
52
+ 'darkgoldenrod' => '184,134,11',
53
+ 'darkgray' => '169,169,169',
54
+ 'darkgreen' => '0,100,0',
55
+ 'darkgrey' => '169,169,169',
56
+ 'darkkhaki' => '189,183,107',
57
+ 'darkmagenta' => '139,0,139',
58
+ 'darkolivegreen' => '85,107,47',
59
+ 'darkorange' => '255,140,0',
60
+ 'darkorchid' => '153,50,204',
61
+ 'darkred' => '139,0,0',
62
+ 'darksalmon' => '233,150,122',
63
+ 'darkseagreen' => '143,188,143',
64
+ 'darkslateblue' => '72,61,139',
65
+ 'darkslategray' => '47,79,79',
66
+ 'darkslategrey' => '47,79,79',
67
+ 'darkturquoise' => '0,206,209',
68
+ 'darkviolet' => '148,0,211',
69
+ 'deeppink' => '255,20,147',
70
+ 'deepskyblue' => '0,191,255',
71
+ 'dimgray' => '105,105,105',
72
+ 'dimgrey' => '105,105,105',
73
+ 'dodgerblue' => '30,144,255',
74
+ 'firebrick' => '178,34,34',
75
+ 'floralwhite' => '255,250,240',
76
+ 'forestgreen' => '34,139,34',
77
+ 'fuchsia' => '255,0,255',
78
+ 'gainsboro' => '220,220,220',
79
+ 'ghostwhite' => '248,248,255',
80
+ 'gold' => '255,215,0',
81
+ 'goldenrod' => '218,165,32',
82
+ 'gray' => '128,128,128',
83
+ 'green' => '0,128,0',
84
+ 'greenyellow' => '173,255,47',
85
+ 'grey' => '128,128,128',
86
+ 'honeydew' => '240,255,240',
87
+ 'hotpink' => '255,105,180',
88
+ 'indianred' => '205,92,92',
89
+ 'indigo' => '75,0,130',
90
+ 'ivory' => '255,255,240',
91
+ 'khaki' => '240,230,140',
92
+ 'lavender' => '230,230,250',
93
+ 'lavenderblush' => '255,240,245',
94
+ 'lawngreen' => '124,252,0',
95
+ 'lemonchiffon' => '255,250,205',
96
+ 'lightblue' => '173,216,230',
97
+ 'lightcoral' => '240,128,128',
98
+ 'lightcyan' => '224,255,255',
99
+ 'lightgoldenrodyellow' => '250,250,210',
100
+ 'lightgray' => '211,211,211',
101
+ 'lightgreen' => '144,238,144',
102
+ 'lightgrey' => '211,211,211',
103
+ 'lightpink' => '255,182,193',
104
+ 'lightsalmon' => '255,160,122',
105
+ 'lightseagreen' => '32,178,170',
106
+ 'lightskyblue' => '135,206,250',
107
+ 'lightslategray' => '119,136,153',
108
+ 'lightslategrey' => '119,136,153',
109
+ 'lightsteelblue' => '176,196,222',
110
+ 'lightyellow' => '255,255,224',
111
+ 'lime' => '0,255,0',
112
+ 'limegreen' => '50,205,50',
113
+ 'linen' => '250,240,230',
114
+ 'magenta' => '255,0,255',
115
+ 'maroon' => '128,0,0',
116
+ 'mediumaquamarine' => '102,205,170',
117
+ 'mediumblue' => '0,0,205',
118
+ 'mediumorchid' => '186,85,211',
119
+ 'mediumpurple' => '147,112,219',
120
+ 'mediumseagreen' => '60,179,113',
121
+ 'mediumslateblue' => '123,104,238',
122
+ 'mediumspringgreen' => '0,250,154',
123
+ 'mediumturquoise' => '72,209,204',
124
+ 'mediumvioletred' => '199,21,133',
125
+ 'midnightblue' => '25,25,112',
126
+ 'mintcream' => '245,255,250',
127
+ 'mistyrose' => '255,228,225',
128
+ 'moccasin' => '255,228,181',
129
+ 'navajowhite' => '255,222,173',
130
+ 'navy' => '0,0,128',
131
+ 'oldlace' => '253,245,230',
132
+ 'olive' => '128,128,0',
133
+ 'olivedrab' => '107,142,35',
134
+ 'orange' => '255,165,0',
135
+ 'orangered' => '255,69,0',
136
+ 'orchid' => '218,112,214',
137
+ 'palegoldenrod' => '238,232,170',
138
+ 'palegreen' => '152,251,152',
139
+ 'paleturquoise' => '175,238,238',
140
+ 'palevioletred' => '219,112,147',
141
+ 'papayawhip' => '255,239,213',
142
+ 'peachpuff' => '255,218,185',
143
+ 'peru' => '205,133,63',
144
+ 'pink' => '255,192,203',
145
+ 'plum' => '221,160,221',
146
+ 'powderblue' => '176,224,230',
147
+ 'purple' => '128,0,128',
148
+ 'rebeccapurple' => '102,51,153',
149
+ 'red' => '255,0,0',
150
+ 'rosybrown' => '188,143,143',
151
+ 'royalblue' => '65,105,225',
152
+ 'saddlebrown' => '139,69,19',
153
+ 'salmon' => '250,128,114',
154
+ 'sandybrown' => '244,164,96',
155
+ 'seagreen' => '46,139,87',
156
+ 'seashell' => '255,245,238',
157
+ 'sienna' => '160,82,45',
158
+ 'silver' => '192,192,192',
159
+ 'skyblue' => '135,206,235',
160
+ 'slateblue' => '106,90,205',
161
+ 'slategray' => '112,128,144',
162
+ 'slategrey' => '112,128,144',
163
+ 'snow' => '255,250,250',
164
+ 'springgreen' => '0,255,127',
165
+ 'steelblue' => '70,130,180',
166
+ 'tan' => '210,180,140',
167
+ 'teal' => '0,128,128',
168
+ 'thistle' => '216,191,216',
169
+ 'tomato' => '255,99,71',
170
+ 'transparent' => '0,0,0,0',
171
+ 'turquoise' => '64,224,208',
172
+ 'violet' => '238,130,238',
173
+ 'wheat' => '245,222,179',
174
+ 'white' => '255,255,255',
175
+ 'whitesmoke' => '245,245,245',
176
+ 'yellow' => '255,255,0',
177
+ 'yellowgreen' => '154,205,50',
178
+ ];
179
+ }
scssphp/src/Compiler.php ADDED
@@ -0,0 +1,5059 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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\Type;
22
+ use Leafo\ScssPhp\Parser;
23
+ use Leafo\ScssPhp\Util;
24
+
25
+ /**
26
+ * The scss compiler and parser.
27
+ *
28
+ * Converting SCSS to CSS is a three stage process. The incoming file is parsed
29
+ * by `Parser` into a syntax tree, then it is compiled into another tree
30
+ * representing the CSS structure by `Compiler`. The CSS tree is fed into a
31
+ * formatter, like `Formatter` which then outputs CSS as a string.
32
+ *
33
+ * During the first compile, all values are *reduced*, which means that their
34
+ * types are brought to the lowest form before being dump as strings. This
35
+ * handles math equations, variable dereferences, and the like.
36
+ *
37
+ * The `compile` function of `Compiler` is the entry point.
38
+ *
39
+ * In summary:
40
+ *
41
+ * The `Compiler` class creates an instance of the parser, feeds it SCSS code,
42
+ * then transforms the resulting tree to a CSS tree. This class also holds the
43
+ * evaluation context, such as all available mixins and variables at any given
44
+ * time.
45
+ *
46
+ * The `Parser` class is only concerned with parsing its input.
47
+ *
48
+ * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
49
+ * handling things like indentation.
50
+ */
51
+
52
+ /**
53
+ * SCSS compiler
54
+ *
55
+ * @author Leaf Corcoran <leafot@gmail.com>
56
+ */
57
+ class Compiler
58
+ {
59
+ const LINE_COMMENTS = 1;
60
+ const DEBUG_INFO = 2;
61
+
62
+ const WITH_RULE = 1;
63
+ const WITH_MEDIA = 2;
64
+ const WITH_SUPPORTS = 4;
65
+ const WITH_ALL = 7;
66
+
67
+ /**
68
+ * @var array
69
+ */
70
+ static protected $operatorNames = [
71
+ '+' => 'add',
72
+ '-' => 'sub',
73
+ '*' => 'mul',
74
+ '/' => 'div',
75
+ '%' => 'mod',
76
+
77
+ '==' => 'eq',
78
+ '!=' => 'neq',
79
+ '<' => 'lt',
80
+ '>' => 'gt',
81
+
82
+ '<=' => 'lte',
83
+ '>=' => 'gte',
84
+ '<=>' => 'cmp',
85
+ ];
86
+
87
+ /**
88
+ * @var array
89
+ */
90
+ static protected $namespaces = [
91
+ 'special' => '%',
92
+ 'mixin' => '@',
93
+ 'function' => '^',
94
+ ];
95
+
96
+ static public $true = [Type::T_KEYWORD, 'true'];
97
+ static public $false = [Type::T_KEYWORD, 'false'];
98
+ static public $null = [Type::T_NULL];
99
+ static public $nullString = [Type::T_STRING, '', []];
100
+ static public $defaultValue = [Type::T_KEYWORD, ''];
101
+ static public $selfSelector = [Type::T_SELF];
102
+ static public $emptyList = [Type::T_LIST, '', []];
103
+ static public $emptyMap = [Type::T_MAP, [], []];
104
+ static public $emptyString = [Type::T_STRING, '"', []];
105
+ static public $with = [Type::T_KEYWORD, 'with'];
106
+ static public $without = [Type::T_KEYWORD, 'without'];
107
+
108
+ protected $importPaths = [''];
109
+ protected $importCache = [];
110
+ protected $importedFiles = [];
111
+ protected $userFunctions = [];
112
+ protected $registeredVars = [];
113
+ protected $registeredFeatures = [
114
+ 'extend-selector-pseudoclass' => false,
115
+ 'at-error' => true,
116
+ 'units-level-3' => false,
117
+ 'global-variable-shadowing' => false,
118
+ ];
119
+
120
+ protected $encoding = null;
121
+ protected $lineNumberStyle = null;
122
+
123
+ protected $formatter = 'Leafo\ScssPhp\Formatter\Nested';
124
+
125
+ protected $rootEnv;
126
+ protected $rootBlock;
127
+
128
+ private $indentLevel;
129
+ private $commentsSeen;
130
+ private $extends;
131
+ private $extendsMap;
132
+ private $parsedFiles;
133
+ private $env;
134
+ private $scope;
135
+ private $parser;
136
+ private $sourceNames;
137
+ private $sourceIndex;
138
+ private $sourceLine;
139
+ private $sourceColumn;
140
+ private $storeEnv;
141
+ private $charsetSeen;
142
+ private $stderr;
143
+ private $shouldEvaluate;
144
+ private $ignoreErrors;
145
+
146
+ /**
147
+ * Constructor
148
+ */
149
+ public function __construct()
150
+ {
151
+ $this->parsedFiles = [];
152
+ $this->sourceNames = [];
153
+ }
154
+
155
+ /**
156
+ * Compile scss
157
+ *
158
+ * @api
159
+ *
160
+ * @param string $code
161
+ * @param string $path
162
+ *
163
+ * @return string
164
+ */
165
+ public function compile($code, $path = null)
166
+ {
167
+ $locale = setlocale(LC_NUMERIC, 0);
168
+ setlocale(LC_NUMERIC, 'C');
169
+
170
+ $this->indentLevel = -1;
171
+ $this->commentsSeen = [];
172
+ $this->extends = [];
173
+ $this->extendsMap = [];
174
+ $this->sourceIndex = null;
175
+ $this->sourceLine = null;
176
+ $this->sourceColumn = null;
177
+ $this->env = null;
178
+ $this->scope = null;
179
+ $this->storeEnv = null;
180
+ $this->charsetSeen = null;
181
+ $this->shouldEvaluate = null;
182
+ $this->stderr = fopen('php://stderr', 'w');
183
+
184
+ $this->parser = $this->parserFactory($path);
185
+ $tree = $this->parser->parse($code);
186
+ $this->parser = null;
187
+
188
+ $this->formatter = new $this->formatter();
189
+ $this->rootBlock = null;
190
+ $this->rootEnv = $this->pushEnv($tree);
191
+
192
+ $this->injectVariables($this->registeredVars);
193
+ $this->compileRoot($tree);
194
+ $this->popEnv();
195
+
196
+ $out = $this->formatter->format($this->scope);
197
+
198
+ setlocale(LC_NUMERIC, $locale);
199
+
200
+ return $out;
201
+ }
202
+
203
+ /**
204
+ * Instantiate parser
205
+ *
206
+ * @param string $path
207
+ *
208
+ * @return \Leafo\ScssPhp\Parser
209
+ */
210
+ private function parserFactory($path)
211
+ {
212
+ $parser = new Parser($path, count($this->sourceNames), $this->encoding);
213
+
214
+ $this->sourceNames[] = $path;
215
+ $this->addParsedFile($path);
216
+
217
+ return $parser;
218
+ }
219
+
220
+ /**
221
+ * Is self extend?
222
+ *
223
+ * @param array $target
224
+ * @param array $origin
225
+ *
226
+ * @return boolean
227
+ */
228
+ protected function isSelfExtend($target, $origin)
229
+ {
230
+ foreach ($origin as $sel) {
231
+ if (in_array($target, $sel)) {
232
+ return true;
233
+ }
234
+ }
235
+
236
+ return false;
237
+ }
238
+
239
+ /**
240
+ * Push extends
241
+ *
242
+ * @param array $target
243
+ * @param array $origin
244
+ * @param \stdClass $block
245
+ */
246
+ protected function pushExtends($target, $origin, $block)
247
+ {
248
+ if ($this->isSelfExtend($target, $origin)) {
249
+ return;
250
+ }
251
+
252
+ $i = count($this->extends);
253
+ $this->extends[] = [$target, $origin, $block];
254
+
255
+ foreach ($target as $part) {
256
+ if (isset($this->extendsMap[$part])) {
257
+ $this->extendsMap[$part][] = $i;
258
+ } else {
259
+ $this->extendsMap[$part] = [$i];
260
+ }
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Make output block
266
+ *
267
+ * @param string $type
268
+ * @param array $selectors
269
+ *
270
+ * @return \Leafo\ScssPhp\Formatter\OutputBlock
271
+ */
272
+ protected function makeOutputBlock($type, $selectors = null)
273
+ {
274
+ $out = new OutputBlock;
275
+ $out->type = $type;
276
+ $out->lines = [];
277
+ $out->children = [];
278
+ $out->parent = $this->scope;
279
+ $out->selectors = $selectors;
280
+ $out->depth = $this->env->depth;
281
+
282
+ return $out;
283
+ }
284
+
285
+ /**
286
+ * Compile root
287
+ *
288
+ * @param \Leafo\ScssPhp\Block $rootBlock
289
+ */
290
+ protected function compileRoot(Block $rootBlock)
291
+ {
292
+ $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT);
293
+
294
+ $this->compileChildrenNoReturn($rootBlock->children, $this->scope);
295
+ $this->flattenSelectors($this->scope);
296
+ $this->missingSelectors();
297
+ }
298
+
299
+ /**
300
+ * Report missing selectors
301
+ */
302
+ protected function missingSelectors()
303
+ {
304
+ foreach ($this->extends as $extend) {
305
+ if (isset($extend[3])) {
306
+ continue;
307
+ }
308
+
309
+ list($target, $origin, $block) = $extend;
310
+
311
+ // ignore if !optional
312
+ if ($block[2]) {
313
+ continue;
314
+ }
315
+
316
+ $target = implode(' ', $target);
317
+ $origin = $this->collapseSelectors($origin);
318
+
319
+ $this->sourceLine = $block[Parser::SOURCE_LINE];
320
+ $this->throwError("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Flatten selectors
326
+ *
327
+ * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
328
+ * @param string $parentKey
329
+ */
330
+ protected function flattenSelectors(OutputBlock $block, $parentKey = null)
331
+ {
332
+ if ($block->selectors) {
333
+ $selectors = [];
334
+
335
+ foreach ($block->selectors as $s) {
336
+ $selectors[] = $s;
337
+
338
+ if (! is_array($s)) {
339
+ continue;
340
+ }
341
+
342
+ // check extends
343
+ if (! empty($this->extendsMap)) {
344
+ $this->matchExtends($s, $selectors);
345
+
346
+ // remove duplicates
347
+ array_walk($selectors, function (&$value) {
348
+ $value = serialize($value);
349
+ });
350
+
351
+ $selectors = array_unique($selectors);
352
+
353
+ array_walk($selectors, function (&$value) {
354
+ $value = unserialize($value);
355
+ });
356
+ }
357
+ }
358
+
359
+ $block->selectors = [];
360
+ $placeholderSelector = false;
361
+
362
+ foreach ($selectors as $selector) {
363
+ if ($this->hasSelectorPlaceholder($selector)) {
364
+ $placeholderSelector = true;
365
+ continue;
366
+ }
367
+
368
+ $block->selectors[] = $this->compileSelector($selector);
369
+ }
370
+
371
+ if ($placeholderSelector && 0 === count($block->selectors) && null !== $parentKey) {
372
+ unset($block->parent->children[$parentKey]);
373
+
374
+ return;
375
+ }
376
+ }
377
+
378
+ foreach ($block->children as $key => $child) {
379
+ $this->flattenSelectors($child, $key);
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Match extends
385
+ *
386
+ * @param array $selector
387
+ * @param array $out
388
+ * @param integer $from
389
+ * @param boolean $initial
390
+ */
391
+ protected function matchExtends($selector, &$out, $from = 0, $initial = true)
392
+ {
393
+ foreach ($selector as $i => $part) {
394
+ if ($i < $from) {
395
+ continue;
396
+ }
397
+
398
+ if ($this->matchExtendsSingle($part, $origin)) {
399
+ $before = array_slice($selector, 0, $i);
400
+ $after = array_slice($selector, $i + 1);
401
+ $s = count($before);
402
+
403
+ foreach ($origin as $new) {
404
+ $k = 0;
405
+
406
+ // remove shared parts
407
+ if ($initial) {
408
+ while ($k < $s && isset($new[$k]) && $before[$k] === $new[$k]) {
409
+ $k++;
410
+ }
411
+ }
412
+
413
+ $result = array_merge(
414
+ $before,
415
+ $k > 0 ? array_slice($new, $k) : $new,
416
+ $after
417
+ );
418
+
419
+ if ($result === $selector) {
420
+ continue;
421
+ }
422
+
423
+ $out[] = $result;
424
+
425
+ // recursively check for more matches
426
+ $this->matchExtends($result, $out, $i, false);
427
+
428
+ // selector sequence merging
429
+ if (! empty($before) && count($new) > 1) {
430
+ $result2 = array_merge(
431
+ array_slice($new, 0, -1),
432
+ $k > 0 ? array_slice($before, $k) : $before,
433
+ array_slice($new, -1),
434
+ $after
435
+ );
436
+
437
+ $out[] = $result2;
438
+ }
439
+ }
440
+ }
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Match extends single
446
+ *
447
+ * @param array $rawSingle
448
+ * @param array $outOrigin
449
+ *
450
+ * @return boolean
451
+ */
452
+ protected function matchExtendsSingle($rawSingle, &$outOrigin)
453
+ {
454
+ $counts = [];
455
+ $single = [];
456
+
457
+ foreach ($rawSingle as $part) {
458
+ // matches Number
459
+ if (! is_string($part)) {
460
+ return false;
461
+ }
462
+
463
+ if (! preg_match('/^[\[.:#%]/', $part) && count($single)) {
464
+ $single[count($single) - 1] .= $part;
465
+ } else {
466
+ $single[] = $part;
467
+ }
468
+ }
469
+
470
+ foreach ($single as $part) {
471
+ if (isset($this->extendsMap[$part])) {
472
+ foreach ($this->extendsMap[$part] as $idx) {
473
+ $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
474
+ }
475
+ }
476
+ }
477
+
478
+ $outOrigin = [];
479
+ $found = false;
480
+
481
+ foreach ($counts as $idx => $count) {
482
+ list($target, $origin, /* $block */) = $this->extends[$idx];
483
+
484
+ // check count
485
+ if ($count !== count($target)) {
486
+ continue;
487
+ }
488
+
489
+ $this->extends[$idx][3] = true;
490
+
491
+ $rem = array_diff($single, $target);
492
+
493
+ foreach ($origin as $j => $new) {
494
+ // prevent infinite loop when target extends itself
495
+ if ($this->isSelfExtend($single, $origin)) {
496
+ return false;
497
+ }
498
+
499
+ $combined = $this->combineSelectorSingle(end($new), $rem);
500
+
501
+ if (count(array_diff($combined, $origin[$j][count($origin[$j]) - 1]))) {
502
+ $origin[$j][count($origin[$j]) - 1] = $combined;
503
+ }
504
+ }
505
+
506
+ $outOrigin = array_merge($outOrigin, $origin);
507
+
508
+ $found = true;
509
+ }
510
+
511
+ return $found;
512
+ }
513
+
514
+ /**
515
+ * Combine selector single
516
+ *
517
+ * @param array $base
518
+ * @param array $other
519
+ *
520
+ * @return array
521
+ */
522
+ protected function combineSelectorSingle($base, $other)
523
+ {
524
+ $tag = [];
525
+ $out = [];
526
+ $wasTag = true;
527
+
528
+ foreach ([$base, $other] as $single) {
529
+ foreach ($single as $part) {
530
+ if (preg_match('/^[\[.:#]/', $part)) {
531
+ $out[] = $part;
532
+ $wasTag = false;
533
+ } elseif (preg_match('/^[^_-]/', $part)) {
534
+ $tag[] = $part;
535
+ $wasTag = true;
536
+ } elseif ($wasTag) {
537
+ $tag[count($tag) - 1] .= $part;
538
+ } else {
539
+ $out[count($out) - 1] .= $part;
540
+ }
541
+ }
542
+ }
543
+
544
+ if (count($tag)) {
545
+ array_unshift($out, $tag[0]);
546
+ }
547
+
548
+ return $out;
549
+ }
550
+
551
+ /**
552
+ * Compile media
553
+ *
554
+ * @param \Leafo\ScssPhp\Block $media
555
+ */
556
+ protected function compileMedia(Block $media)
557
+ {
558
+ $this->pushEnv($media);
559
+
560
+ $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
561
+
562
+ if (! empty($mediaQuery)) {
563
+ $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
564
+
565
+ $parentScope = $this->mediaParent($this->scope);
566
+ $parentScope->children[] = $this->scope;
567
+
568
+ // top level properties in a media cause it to be wrapped
569
+ $needsWrap = false;
570
+
571
+ foreach ($media->children as $child) {
572
+ $type = $child[0];
573
+
574
+ if ($type !== Type::T_BLOCK &&
575
+ $type !== Type::T_MEDIA &&
576
+ $type !== Type::T_DIRECTIVE &&
577
+ $type !== Type::T_IMPORT
578
+ ) {
579
+ $needsWrap = true;
580
+ break;
581
+ }
582
+ }
583
+
584
+ if ($needsWrap) {
585
+ $wrapped = new Block;
586
+ $wrapped->sourceIndex = $media->sourceIndex;
587
+ $wrapped->sourceLine = $media->sourceLine;
588
+ $wrapped->sourceColumn = $media->sourceColumn;
589
+ $wrapped->selectors = [];
590
+ $wrapped->comments = [];
591
+ $wrapped->parent = $media;
592
+ $wrapped->children = $media->children;
593
+
594
+ $media->children = [[Type::T_BLOCK, $wrapped]];
595
+ }
596
+
597
+ $this->compileChildrenNoReturn($media->children, $this->scope);
598
+
599
+ $this->scope = $this->scope->parent;
600
+ }
601
+
602
+ $this->popEnv();
603
+ }
604
+
605
+ /**
606
+ * Media parent
607
+ *
608
+ * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope
609
+ *
610
+ * @return \Leafo\ScssPhp\Formatter\OutputBlock
611
+ */
612
+ protected function mediaParent(OutputBlock $scope)
613
+ {
614
+ while (! empty($scope->parent)) {
615
+ if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) {
616
+ break;
617
+ }
618
+
619
+ $scope = $scope->parent;
620
+ }
621
+
622
+ return $scope;
623
+ }
624
+
625
+ /**
626
+ * Compile directive
627
+ *
628
+ * @param \Leafo\ScssPhp\Block $block
629
+ */
630
+ protected function compileDirective(Block $block)
631
+ {
632
+ $s = '@' . $block->name;
633
+
634
+ if (! empty($block->value)) {
635
+ $s .= ' ' . $this->compileValue($block->value);
636
+ }
637
+
638
+ if ($block->name === 'keyframes' || substr($block->name, -10) === '-keyframes') {
639
+ $this->compileKeyframeBlock($block, [$s]);
640
+ } else {
641
+ $this->compileNestedBlock($block, [$s]);
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Compile at-root
647
+ *
648
+ * @param \Leafo\ScssPhp\Block $block
649
+ */
650
+ protected function compileAtRoot(Block $block)
651
+ {
652
+ $env = $this->pushEnv($block);
653
+ $envs = $this->compactEnv($env);
654
+ $without = isset($block->with) ? $this->compileWith($block->with) : self::WITH_RULE;
655
+
656
+ // wrap inline selector
657
+ if ($block->selector) {
658
+ $wrapped = new Block;
659
+ $wrapped->sourceIndex = $block->sourceIndex;
660
+ $wrapped->sourceLine = $block->sourceLine;
661
+ $wrapped->sourceColumn = $block->sourceColumn;
662
+ $wrapped->selectors = $block->selector;
663
+ $wrapped->comments = [];
664
+ $wrapped->parent = $block;
665
+ $wrapped->children = $block->children;
666
+
667
+ $block->children = [[Type::T_BLOCK, $wrapped]];
668
+ }
669
+
670
+ $this->env = $this->filterWithout($envs, $without);
671
+ $newBlock = $this->spliceTree($envs, $block, $without);
672
+
673
+ $saveScope = $this->scope;
674
+ $this->scope = $this->rootBlock;
675
+
676
+ $this->compileChild($newBlock, $this->scope);
677
+
678
+ $this->scope = $saveScope;
679
+ $this->env = $this->extractEnv($envs);
680
+
681
+ $this->popEnv();
682
+ }
683
+
684
+ /**
685
+ * Splice parse tree
686
+ *
687
+ * @param array $envs
688
+ * @param \Leafo\ScssPhp\Block $block
689
+ * @param integer $without
690
+ *
691
+ * @return array
692
+ */
693
+ private function spliceTree($envs, Block $block, $without)
694
+ {
695
+ $newBlock = null;
696
+
697
+ foreach ($envs as $e) {
698
+ if (! isset($e->block)) {
699
+ continue;
700
+ }
701
+
702
+ if ($e->block === $block) {
703
+ continue;
704
+ }
705
+
706
+ if (isset($e->block->type) && $e->block->type === Type::T_AT_ROOT) {
707
+ continue;
708
+ }
709
+
710
+ if ($e->block && $this->isWithout($without, $e->block)) {
711
+ continue;
712
+ }
713
+
714
+ $b = new Block;
715
+ $b->sourceIndex = $e->block->sourceIndex;
716
+ $b->sourceLine = $e->block->sourceLine;
717
+ $b->sourceColumn = $e->block->sourceColumn;
718
+ $b->selectors = [];
719
+ $b->comments = $e->block->comments;
720
+ $b->parent = null;
721
+
722
+ if ($newBlock) {
723
+ $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
724
+
725
+ $b->children = [[$type, $newBlock]];
726
+
727
+ $newBlock->parent = $b;
728
+ } elseif (count($block->children)) {
729
+ foreach ($block->children as $child) {
730
+ if ($child[0] === Type::T_BLOCK) {
731
+ $child[1]->parent = $b;
732
+ }
733
+ }
734
+
735
+ $b->children = $block->children;
736
+ }
737
+
738
+ if (isset($e->block->type)) {
739
+ $b->type = $e->block->type;
740
+ }
741
+
742
+ if (isset($e->block->name)) {
743
+ $b->name = $e->block->name;
744
+ }
745
+
746
+ if (isset($e->block->queryList)) {
747
+ $b->queryList = $e->block->queryList;
748
+ }
749
+
750
+ if (isset($e->block->value)) {
751
+ $b->value = $e->block->value;
752
+ }
753
+
754
+ $newBlock = $b;
755
+ }
756
+
757
+ $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
758
+
759
+ return [$type, $newBlock];
760
+ }
761
+
762
+ /**
763
+ * Compile @at-root's with: inclusion / without: exclusion into filter flags
764
+ *
765
+ * @param array $with
766
+ *
767
+ * @return integer
768
+ */
769
+ private function compileWith($with)
770
+ {
771
+ static $mapping = [
772
+ 'rule' => self::WITH_RULE,
773
+ 'media' => self::WITH_MEDIA,
774
+ 'supports' => self::WITH_SUPPORTS,
775
+ 'all' => self::WITH_ALL,
776
+ ];
777
+
778
+ // exclude selectors by default
779
+ $without = self::WITH_RULE;
780
+
781
+ if ($this->libMapHasKey([$with, self::$with])) {
782
+ $without = self::WITH_ALL;
783
+
784
+ $list = $this->coerceList($this->libMapGet([$with, self::$with]));
785
+
786
+ foreach ($list[2] as $item) {
787
+ $keyword = $this->compileStringContent($this->coerceString($item));
788
+
789
+ if (array_key_exists($keyword, $mapping)) {
790
+ $without &= ~($mapping[$keyword]);
791
+ }
792
+ }
793
+ }
794
+
795
+ if ($this->libMapHasKey([$with, self::$without])) {
796
+ $without = 0;
797
+
798
+ $list = $this->coerceList($this->libMapGet([$with, self::$without]));
799
+
800
+ foreach ($list[2] as $item) {
801
+ $keyword = $this->compileStringContent($this->coerceString($item));
802
+
803
+ if (array_key_exists($keyword, $mapping)) {
804
+ $without |= $mapping[$keyword];
805
+ }
806
+ }
807
+ }
808
+
809
+ return $without;
810
+ }
811
+
812
+ /**
813
+ * Filter env stack
814
+ *
815
+ * @param array $envs
816
+ * @param integer $without
817
+ *
818
+ * @return \Leafo\ScssPhp\Compiler\Environment
819
+ */
820
+ private function filterWithout($envs, $without)
821
+ {
822
+ $filtered = [];
823
+
824
+ foreach ($envs as $e) {
825
+ if ($e->block && $this->isWithout($without, $e->block)) {
826
+ continue;
827
+ }
828
+
829
+ $filtered[] = $e;
830
+ }
831
+
832
+ return $this->extractEnv($filtered);
833
+ }
834
+
835
+ /**
836
+ * Filter WITH rules
837
+ *
838
+ * @param integer $without
839
+ * @param \Leafo\ScssPhp\Block $block
840
+ *
841
+ * @return boolean
842
+ */
843
+ private function isWithout($without, Block $block)
844
+ {
845
+ if ((($without & self::WITH_RULE) && isset($block->selectors)) ||
846
+ (($without & self::WITH_MEDIA) &&
847
+ isset($block->type) && $block->type === Type::T_MEDIA) ||
848
+ (($without & self::WITH_SUPPORTS) &&
849
+ isset($block->type) && $block->type === Type::T_DIRECTIVE &&
850
+ isset($block->name) && $block->name === 'supports')
851
+ ) {
852
+ return true;
853
+ }
854
+
855
+ return false;
856
+ }
857
+
858
+ /**
859
+ * Compile keyframe block
860
+ *
861
+ * @param \Leafo\ScssPhp\Block $block
862
+ * @param array $selectors
863
+ */
864
+ protected function compileKeyframeBlock(Block $block, $selectors)
865
+ {
866
+ $env = $this->pushEnv($block);
867
+
868
+ $envs = $this->compactEnv($env);
869
+
870
+ $this->env = $this->extractEnv(array_filter($envs, function (Environment $e) {
871
+ return ! isset($e->block->selectors);
872
+ }));
873
+
874
+ $this->scope = $this->makeOutputBlock($block->type, $selectors);
875
+ $this->scope->depth = 1;
876
+ $this->scope->parent->children[] = $this->scope;
877
+
878
+ $this->compileChildrenNoReturn($block->children, $this->scope);
879
+
880
+ $this->scope = $this->scope->parent;
881
+ $this->env = $this->extractEnv($envs);
882
+
883
+ $this->popEnv();
884
+ }
885
+
886
+ /**
887
+ * Compile nested block
888
+ *
889
+ * @param \Leafo\ScssPhp\Block $block
890
+ * @param array $selectors
891
+ */
892
+ protected function compileNestedBlock(Block $block, $selectors)
893
+ {
894
+ $this->pushEnv($block);
895
+
896
+ $this->scope = $this->makeOutputBlock($block->type, $selectors);
897
+ $this->scope->parent->children[] = $this->scope;
898
+
899
+ $this->compileChildrenNoReturn($block->children, $this->scope);
900
+
901
+ $this->scope = $this->scope->parent;
902
+
903
+ $this->popEnv();
904
+ }
905
+
906
+ /**
907
+ * Recursively compiles a block.
908
+ *
909
+ * A block is analogous to a CSS block in most cases. A single SCSS document
910
+ * is encapsulated in a block when parsed, but it does not have parent tags
911
+ * so all of its children appear on the root level when compiled.
912
+ *
913
+ * Blocks are made up of selectors and children.
914
+ *
915
+ * The children of a block are just all the blocks that are defined within.
916
+ *
917
+ * Compiling the block involves pushing a fresh environment on the stack,
918
+ * and iterating through the props, compiling each one.
919
+ *
920
+ * @see Compiler::compileChild()
921
+ *
922
+ * @param \Leafo\ScssPhp\Block $block
923
+ */
924
+ protected function compileBlock(Block $block)
925
+ {
926
+ $env = $this->pushEnv($block);
927
+ $env->selectors = $this->evalSelectors($block->selectors);
928
+
929
+ $out = $this->makeOutputBlock(null);
930
+
931
+ if (isset($this->lineNumberStyle) && count($env->selectors) && count($block->children)) {
932
+ $annotation = $this->makeOutputBlock(Type::T_COMMENT);
933
+ $annotation->depth = 0;
934
+
935
+ $file = $this->sourceNames[$block->sourceIndex];
936
+ $line = $block->sourceLine;
937
+
938
+ switch ($this->lineNumberStyle) {
939
+ case self::LINE_COMMENTS:
940
+ $annotation->lines[] = '/* line ' . $line . ', ' . $file . ' */';
941
+ break;
942
+
943
+ case self::DEBUG_INFO:
944
+ $annotation->lines[] = '@media -sass-debug-info{filename{font-family:"' . $file
945
+ . '"}line{font-family:' . $line . '}}';
946
+ break;
947
+ }
948
+
949
+ $this->scope->children[] = $annotation;
950
+ }
951
+
952
+ $this->scope->children[] = $out;
953
+
954
+ if (count($block->children)) {
955
+ $out->selectors = $this->multiplySelectors($env);
956
+
957
+ $this->compileChildrenNoReturn($block->children, $out);
958
+ }
959
+
960
+ $this->formatter->stripSemicolon($out->lines);
961
+
962
+ $this->popEnv();
963
+ }
964
+
965
+ /**
966
+ * Compile root level comment
967
+ *
968
+ * @param array $block
969
+ */
970
+ protected function compileComment($block)
971
+ {
972
+ $out = $this->makeOutputBlock(Type::T_COMMENT);
973
+ $out->lines[] = $block[1];
974
+ $this->scope->children[] = $out;
975
+ }
976
+
977
+ /**
978
+ * Evaluate selectors
979
+ *
980
+ * @param array $selectors
981
+ *
982
+ * @return array
983
+ */
984
+ protected function evalSelectors($selectors)
985
+ {
986
+ $this->shouldEvaluate = false;
987
+
988
+ $selectors = array_map([$this, 'evalSelector'], $selectors);
989
+
990
+ // after evaluating interpolates, we might need a second pass
991
+ if ($this->shouldEvaluate) {
992
+ $buffer = $this->collapseSelectors($selectors);
993
+ $parser = $this->parserFactory(__METHOD__);
994
+
995
+ if ($parser->parseSelector($buffer, $newSelectors)) {
996
+ $selectors = array_map([$this, 'evalSelector'], $newSelectors);
997
+ }
998
+ }
999
+
1000
+ return $selectors;
1001
+ }
1002
+
1003
+ /**
1004
+ * Evaluate selector
1005
+ *
1006
+ * @param array $selector
1007
+ *
1008
+ * @return array
1009
+ */
1010
+ protected function evalSelector($selector)
1011
+ {
1012
+ return array_map([$this, 'evalSelectorPart'], $selector);
1013
+ }
1014
+
1015
+ /**
1016
+ * Evaluate selector part; replaces all the interpolates, stripping quotes
1017
+ *
1018
+ * @param array $part
1019
+ *
1020
+ * @return array
1021
+ */
1022
+ protected function evalSelectorPart($part)
1023
+ {
1024
+ foreach ($part as &$p) {
1025
+ if (is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
1026
+ $p = $this->compileValue($p);
1027
+
1028
+ // force re-evaluation
1029
+ if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
1030
+ $this->shouldEvaluate = true;
1031
+ }
1032
+ } elseif (is_string($p) && strlen($p) >= 2 &&
1033
+ ($first = $p[0]) && ($first === '"' || $first === "'") &&
1034
+ substr($p, -1) === $first
1035
+ ) {
1036
+ $p = substr($p, 1, -1);
1037
+ }
1038
+ }
1039
+
1040
+ return $this->flattenSelectorSingle($part);
1041
+ }
1042
+
1043
+ /**
1044
+ * Collapse selectors
1045
+ *
1046
+ * @param array $selectors
1047
+ *
1048
+ * @return string
1049
+ */
1050
+ protected function collapseSelectors($selectors)
1051
+ {
1052
+ $parts = [];
1053
+
1054
+ foreach ($selectors as $selector) {
1055
+ $output = '';
1056
+
1057
+ array_walk_recursive(
1058
+ $selector,
1059
+ function ($value, $key) use (&$output) {
1060
+ $output .= $value;
1061
+ }
1062
+ );
1063
+
1064
+ $parts[] = $output;
1065
+ }
1066
+
1067
+ return implode(', ', $parts);
1068
+ }
1069
+
1070
+ /**
1071
+ * Flatten selector single; joins together .classes and #ids
1072
+ *
1073
+ * @param array $single
1074
+ *
1075
+ * @return array
1076
+ */
1077
+ protected function flattenSelectorSingle($single)
1078
+ {
1079
+ $joined = [];
1080
+
1081
+ foreach ($single as $part) {
1082
+ if (empty($joined) ||
1083
+ ! is_string($part) ||
1084
+ preg_match('/[\[.:#%]/', $part)
1085
+ ) {
1086
+ $joined[] = $part;
1087
+ continue;
1088
+ }
1089
+
1090
+ if (is_array(end($joined))) {
1091
+ $joined[] = $part;
1092
+ } else {
1093
+ $joined[count($joined) - 1] .= $part;
1094
+ }
1095
+ }
1096
+
1097
+ return $joined;
1098
+ }
1099
+
1100
+ /**
1101
+ * Compile selector to string; self(&) should have been replaced by now
1102
+ *
1103
+ * @param array $selector
1104
+ *
1105
+ * @return string
1106
+ */
1107
+ protected function compileSelector($selector)
1108
+ {
1109
+ if (! is_array($selector)) {
1110
+ return $selector; // media and the like
1111
+ }
1112
+
1113
+ return implode(
1114
+ ' ',
1115
+ array_map(
1116
+ [$this, 'compileSelectorPart'],
1117
+ $selector
1118
+ )
1119
+ );
1120
+ }
1121
+
1122
+ /**
1123
+ * Compile selector part
1124
+ *
1125
+ * @param arary $piece
1126
+ *
1127
+ * @return string
1128
+ */
1129
+ protected function compileSelectorPart($piece)
1130
+ {
1131
+ foreach ($piece as &$p) {
1132
+ if (! is_array($p)) {
1133
+ continue;
1134
+ }
1135
+
1136
+ switch ($p[0]) {
1137
+ case Type::T_SELF:
1138
+ $p = '&';
1139
+ break;
1140
+
1141
+ default:
1142
+ $p = $this->compileValue($p);
1143
+ break;
1144
+ }
1145
+ }
1146
+
1147
+ return implode($piece);
1148
+ }
1149
+
1150
+ /**
1151
+ * Has selector placeholder?
1152
+ *
1153
+ * @param array $selector
1154
+ *
1155
+ * @return boolean
1156
+ */
1157
+ protected function hasSelectorPlaceholder($selector)
1158
+ {
1159
+ if (! is_array($selector)) {
1160
+ return false;
1161
+ }
1162
+
1163
+ foreach ($selector as $parts) {
1164
+ foreach ($parts as $part) {
1165
+ if (strlen($part) && '%' === $part[0]) {
1166
+ return true;
1167
+ }
1168
+ }
1169
+ }
1170
+
1171
+ return false;
1172
+ }
1173
+
1174
+ /**
1175
+ * Compile children and return result
1176
+ *
1177
+ * @param array $stms
1178
+ * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
1179
+ *
1180
+ * @return array
1181
+ */
1182
+ protected function compileChildren($stms, OutputBlock $out)
1183
+ {
1184
+ foreach ($stms as $stm) {
1185
+ $ret = $this->compileChild($stm, $out);
1186
+
1187
+ if (isset($ret)) {
1188
+ return $ret;
1189
+ }
1190
+ }
1191
+ }
1192
+
1193
+ /**
1194
+ * Compile children and throw exception if unexpected @return
1195
+ *
1196
+ * @param array $stms
1197
+ * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
1198
+ *
1199
+ * @throws \Exception
1200
+ */
1201
+ protected function compileChildrenNoReturn($stms, OutputBlock $out)
1202
+ {
1203
+ foreach ($stms as $stm) {
1204
+ $ret = $this->compileChild($stm, $out);
1205
+
1206
+ if (isset($ret)) {
1207
+ $this->throwError('@return may only be used within a function');
1208
+
1209
+ return;
1210
+ }
1211
+ }
1212
+ }
1213
+
1214
+ /**
1215
+ * Compile media query
1216
+ *
1217
+ * @param array $queryList
1218
+ *
1219
+ * @return string
1220
+ */
1221
+ protected function compileMediaQuery($queryList)
1222
+ {
1223
+ $out = '@media';
1224
+ $first = true;
1225
+
1226
+ foreach ($queryList as $query) {
1227
+ $type = null;
1228
+ $parts = [];
1229
+
1230
+ foreach ($query as $q) {
1231
+ switch ($q[0]) {
1232
+ case Type::T_MEDIA_TYPE:
1233
+ if ($type) {
1234
+ $type = $this->mergeMediaTypes(
1235
+ $type,
1236
+ array_map([$this, 'compileValue'], array_slice($q, 1))
1237
+ );
1238
+
1239
+ if (empty($type)) { // merge failed
1240
+ return null;
1241
+ }
1242
+ } else {
1243
+ $type = array_map([$this, 'compileValue'], array_slice($q, 1));
1244
+ }
1245
+ break;
1246
+
1247
+ case Type::T_MEDIA_EXPRESSION:
1248
+ if (isset($q[2])) {
1249
+ $parts[] = '('
1250
+ . $this->compileValue($q[1])
1251
+ . $this->formatter->assignSeparator
1252
+ . $this->compileValue($q[2])
1253
+ . ')';
1254
+ } else {
1255
+ $parts[] = '('
1256
+ . $this->compileValue($q[1])
1257
+ . ')';
1258
+ }
1259
+ break;
1260
+
1261
+ case Type::T_MEDIA_VALUE:
1262
+ $parts[] = $this->compileValue($q[1]);
1263
+ break;
1264
+ }
1265
+ }
1266
+
1267
+ if ($type) {
1268
+ array_unshift($parts, implode(' ', array_filter($type)));
1269
+ }
1270
+
1271
+ if (! empty($parts)) {
1272
+ if ($first) {
1273
+ $first = false;
1274
+ $out .= ' ';
1275
+ } else {
1276
+ $out .= $this->formatter->tagSeparator;
1277
+ }
1278
+
1279
+ $out .= implode(' and ', $parts);
1280
+ }
1281
+ }
1282
+
1283
+ return $out;
1284
+ }
1285
+
1286
+ /**
1287
+ * Merge media types
1288
+ *
1289
+ * @param array $type1
1290
+ * @param array $type2
1291
+ *
1292
+ * @return array|null
1293
+ */
1294
+ protected function mergeMediaTypes($type1, $type2)
1295
+ {
1296
+ if (empty($type1)) {
1297
+ return $type2;
1298
+ }
1299
+
1300
+ if (empty($type2)) {
1301
+ return $type1;
1302
+ }
1303
+
1304
+ $m1 = '';
1305
+ $t1 = '';
1306
+
1307
+ if (count($type1) > 1) {
1308
+ $m1= strtolower($type1[0]);
1309
+ $t1= strtolower($type1[1]);
1310
+ } else {
1311
+ $t1 = strtolower($type1[0]);
1312
+ }
1313
+
1314
+ $m2 = '';
1315
+ $t2 = '';
1316
+
1317
+ if (count($type2) > 1) {
1318
+ $m2 = strtolower($type2[0]);
1319
+ $t2 = strtolower($type2[1]);
1320
+ } else {
1321
+ $t2 = strtolower($type2[0]);
1322
+ }
1323
+
1324
+ if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) {
1325
+ if ($t1 === $t2) {
1326
+ return null;
1327
+ }
1328
+
1329
+ return [
1330
+ $m1 === Type::T_NOT ? $m2 : $m1,
1331
+ $m1 === Type::T_NOT ? $t2 : $t1,
1332
+ ];
1333
+ }
1334
+
1335
+ if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) {
1336
+ // CSS has no way of representing "neither screen nor print"
1337
+ if ($t1 !== $t2) {
1338
+ return null;
1339
+ }
1340
+
1341
+ return [Type::T_NOT, $t1];
1342
+ }
1343
+
1344
+ if ($t1 !== $t2) {
1345
+ return null;
1346
+ }
1347
+
1348
+ // t1 == t2, neither m1 nor m2 are "not"
1349
+ return [empty($m1)? $m2 : $m1, $t1];
1350
+ }
1351
+
1352
+ /**
1353
+ * Compile import; returns true if the value was something that could be imported
1354
+ *
1355
+ * @param array $rawPath
1356
+ * @param array $out
1357
+ * @param boolean $once
1358
+ *
1359
+ * @return boolean
1360
+ */
1361
+ protected function compileImport($rawPath, $out, $once = false)
1362
+ {
1363
+ if ($rawPath[0] === Type::T_STRING) {
1364
+ $path = $this->compileStringContent($rawPath);
1365
+
1366
+ if ($path = $this->findImport($path)) {
1367
+ if (! $once || ! in_array($path, $this->importedFiles)) {
1368
+ $this->importFile($path, $out);
1369
+ $this->importedFiles[] = $path;
1370
+ }
1371
+
1372
+ return true;
1373
+ }
1374
+
1375
+ return false;
1376
+ }
1377
+
1378
+ if ($rawPath[0] === Type::T_LIST) {
1379
+ // handle a list of strings
1380
+ if (count($rawPath[2]) === 0) {
1381
+ return false;
1382
+ }
1383
+
1384
+ foreach ($rawPath[2] as $path) {
1385
+ if ($path[0] !== Type::T_STRING) {
1386
+ return false;
1387
+ }
1388
+ }
1389
+
1390
+ foreach ($rawPath[2] as $path) {
1391
+ $this->compileImport($path, $out);
1392
+ }
1393
+
1394
+ return true;
1395
+ }
1396
+
1397
+ return false;
1398
+ }
1399
+
1400
+ /**
1401
+ * Compile child; returns a value to halt execution
1402
+ *
1403
+ * @param array $child
1404
+ * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
1405
+ *
1406
+ * @return array
1407
+ */
1408
+ protected function compileChild($child, OutputBlock $out)
1409
+ {
1410
+ $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
1411
+ $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
1412
+ $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
1413
+
1414
+ switch ($child[0]) {
1415
+ case Type::T_SCSSPHP_IMPORT_ONCE:
1416
+ list(, $rawPath) = $child;
1417
+
1418
+ $rawPath = $this->reduce($rawPath);
1419
+
1420
+ if (! $this->compileImport($rawPath, $out, true)) {
1421
+ $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
1422
+ }
1423
+ break;
1424
+
1425
+ case Type::T_IMPORT:
1426
+ list(, $rawPath) = $child;
1427
+
1428
+ $rawPath = $this->reduce($rawPath);
1429
+
1430
+ if (! $this->compileImport($rawPath, $out)) {
1431
+ $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
1432
+ }
1433
+ break;
1434
+
1435
+ case Type::T_DIRECTIVE:
1436
+ $this->compileDirective($child[1]);
1437
+ break;
1438
+
1439
+ case Type::T_AT_ROOT:
1440
+ $this->compileAtRoot($child[1]);
1441
+ break;
1442
+
1443
+ case Type::T_MEDIA:
1444
+ $this->compileMedia($child[1]);
1445
+ break;
1446
+
1447
+ case Type::T_BLOCK:
1448
+ $this->compileBlock($child[1]);
1449
+ break;
1450
+
1451
+ case Type::T_CHARSET:
1452
+ if (! $this->charsetSeen) {
1453
+ $this->charsetSeen = true;
1454
+
1455
+ $out->lines[] = '@charset ' . $this->compileValue($child[1]) . ';';
1456
+ }
1457
+ break;
1458
+
1459
+ case Type::T_ASSIGN:
1460
+ list(, $name, $value) = $child;
1461
+
1462
+ if ($name[0] === Type::T_VARIABLE) {
1463
+ $flag = isset($child[3]) ? $child[3] : null;
1464
+ $isDefault = $flag === '!default';
1465
+ $isGlobal = $flag === '!global';
1466
+
1467
+ if ($isGlobal) {
1468
+ $this->set($name[1], $this->reduce($value), false, $this->rootEnv);
1469
+ break;
1470
+ }
1471
+
1472
+ $shouldSet = $isDefault &&
1473
+ (($result = $this->get($name[1], false)) === null
1474
+ || $result === self::$null);
1475
+
1476
+ if (! $isDefault || $shouldSet) {
1477
+ $this->set($name[1], $this->reduce($value));
1478
+ }
1479
+ break;
1480
+ }
1481
+
1482
+ $compiledName = $this->compileValue($name);
1483
+
1484
+ // handle shorthand syntax: size / line-height
1485
+ if ($compiledName === 'font') {
1486
+ if ($value[0] === Type::T_EXPRESSION && $value[1] === '/') {
1487
+ $value = $this->expToString($value);
1488
+ } elseif ($value[0] === Type::T_LIST) {
1489
+ foreach ($value[2] as &$item) {
1490
+ if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
1491
+ $item = $this->expToString($item);
1492
+ }
1493
+ }
1494
+ }
1495
+ }
1496
+
1497
+ // if the value reduces to null from something else then
1498
+ // the property should be discarded
1499
+ if ($value[0] !== Type::T_NULL) {
1500
+ $value = $this->reduce($value);
1501
+
1502
+ if ($value[0] === Type::T_NULL || $value === self::$nullString) {
1503
+ break;
1504
+ }
1505
+ }
1506
+
1507
+ $compiledValue = $this->compileValue($value);
1508
+
1509
+ $out->lines[] = $this->formatter->property(
1510
+ $compiledName,
1511
+ $compiledValue
1512
+ );
1513
+ break;
1514
+
1515
+ case Type::T_COMMENT:
1516
+ if ($out->type === Type::T_ROOT) {
1517
+ $this->compileComment($child);
1518
+ break;
1519
+ }
1520
+
1521
+ $out->lines[] = $child[1];
1522
+ break;
1523
+
1524
+ case Type::T_MIXIN:
1525
+ case Type::T_FUNCTION:
1526
+ list(, $block) = $child;
1527
+
1528
+ $this->set(self::$namespaces[$block->type] . $block->name, $block);
1529
+ break;
1530
+
1531
+ case Type::T_EXTEND:
1532
+ list(, $selectors) = $child;
1533
+
1534
+ foreach ($selectors as $sel) {
1535
+ $results = $this->evalSelectors([$sel]);
1536
+
1537
+ foreach ($results as $result) {
1538
+ // only use the first one
1539
+ $result = current($result);
1540
+
1541
+ $this->pushExtends($result, $out->selectors, $child);
1542
+ }
1543
+ }
1544
+ break;
1545
+
1546
+ case Type::T_IF:
1547
+ list(, $if) = $child;
1548
+
1549
+ if ($this->isTruthy($this->reduce($if->cond, true))) {
1550
+ return $this->compileChildren($if->children, $out);
1551
+ }
1552
+
1553
+ foreach ($if->cases as $case) {
1554
+ if ($case->type === Type::T_ELSE ||
1555
+ $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))
1556
+ ) {
1557
+ return $this->compileChildren($case->children, $out);
1558
+ }
1559
+ }
1560
+ break;
1561
+
1562
+ case Type::T_EACH:
1563
+ list(, $each) = $child;
1564
+
1565
+ $list = $this->coerceList($this->reduce($each->list));
1566
+
1567
+ $this->pushEnv();
1568
+
1569
+ foreach ($list[2] as $item) {
1570
+ if (count($each->vars) === 1) {
1571
+ $this->set($each->vars[0], $item, true);
1572
+ } else {
1573
+ list(,, $values) = $this->coerceList($item);
1574
+
1575
+ foreach ($each->vars as $i => $var) {
1576
+ $this->set($var, isset($values[$i]) ? $values[$i] : self::$null, true);
1577
+ }
1578
+ }
1579
+
1580
+ $ret = $this->compileChildren($each->children, $out);
1581
+
1582
+ if ($ret) {
1583
+ if ($ret[0] !== Type::T_CONTROL) {
1584
+ $this->popEnv();
1585
+
1586
+ return $ret;
1587
+ }
1588
+
1589
+ if ($ret[1]) {
1590
+ break;
1591
+ }
1592
+ }
1593
+ }
1594
+
1595
+ $this->popEnv();
1596
+ break;
1597
+
1598
+ case Type::T_WHILE:
1599
+ list(, $while) = $child;
1600
+
1601
+ while ($this->isTruthy($this->reduce($while->cond, true))) {
1602
+ $ret = $this->compileChildren($while->children, $out);
1603
+
1604
+ if ($ret) {
1605
+ if ($ret[0] !== Type::T_CONTROL) {
1606
+ return $ret;
1607
+ }
1608
+
1609
+ if ($ret[1]) {
1610
+ break;
1611
+ }
1612
+ }
1613
+ }
1614
+ break;
1615
+
1616
+ case Type::T_FOR:
1617
+ list(, $for) = $child;
1618
+
1619
+ $start = $this->reduce($for->start, true);
1620
+ $start = $start[1];
1621
+ $end = $this->reduce($for->end, true);
1622
+ $end = $end[1];
1623
+ $d = $start < $end ? 1 : -1;
1624
+
1625
+ while (true) {
1626
+ if ((! $for->until && $start - $d == $end) ||
1627
+ ($for->until && $start == $end)
1628
+ ) {
1629
+ break;
1630
+ }
1631
+
1632
+ $this->set($for->var, new Node\Number($start, ''));
1633
+ $start += $d;
1634
+
1635
+ $ret = $this->compileChildren($for->children, $out);
1636
+
1637
+ if ($ret) {
1638
+ if ($ret[0] !== Type::T_CONTROL) {
1639
+ return $ret;
1640
+ }
1641
+
1642
+ if ($ret[1]) {
1643
+ break;
1644
+ }
1645
+ }
1646
+ }
1647
+ break;
1648
+
1649
+ case Type::T_BREAK:
1650
+ return [Type::T_CONTROL, true];
1651
+
1652
+ case Type::T_CONTINUE:
1653
+ return [Type::T_CONTROL, false];
1654
+
1655
+ case Type::T_RETURN:
1656
+ return $this->reduce($child[1], true);
1657
+
1658
+ case Type::T_NESTED_PROPERTY:
1659
+ list(, $prop) = $child;
1660
+
1661
+ $prefixed = [];
1662
+ $prefix = $this->compileValue($prop->prefix) . '-';
1663
+
1664
+ foreach ($prop->children as $child) {
1665
+ switch ($child[0]) {
1666
+ case Type::T_ASSIGN:
1667
+ array_unshift($child[1][2], $prefix);
1668
+ break;
1669
+
1670
+ case Type::T_NESTED_PROPERTY:
1671
+ array_unshift($child[1]->prefix[2], $prefix);
1672
+ break;
1673
+ }
1674
+
1675
+ $prefixed[] = $child;
1676
+ }
1677
+
1678
+ $this->compileChildrenNoReturn($prefixed, $out);
1679
+ break;
1680
+
1681
+ case Type::T_INCLUDE:
1682
+ // including a mixin
1683
+ list(, $name, $argValues, $content) = $child;
1684
+
1685
+ $mixin = $this->get(self::$namespaces['mixin'] . $name, false);
1686
+
1687
+ if (! $mixin) {
1688
+ $this->throwError("Undefined mixin $name");
1689
+ break;
1690
+ }
1691
+
1692
+ $callingScope = $this->getStoreEnv();
1693
+
1694
+ // push scope, apply args
1695
+ $this->pushEnv();
1696
+ $this->env->depth--;
1697
+
1698
+ if (isset($content)) {
1699
+ $content->scope = $callingScope;
1700
+
1701
+ $this->setRaw(self::$namespaces['special'] . 'content', $content, $this->env);
1702
+ }
1703
+
1704
+ if (isset($mixin->args)) {
1705
+ $this->applyArguments($mixin->args, $argValues);
1706
+ }
1707
+
1708
+ $this->env->marker = 'mixin';
1709
+
1710
+ $this->compileChildrenNoReturn($mixin->children, $out);
1711
+
1712
+ $this->popEnv();
1713
+ break;
1714
+
1715
+ case Type::T_MIXIN_CONTENT:
1716
+ $content = $this->get(self::$namespaces['special'] . 'content', false, $this->getStoreEnv())
1717
+ ?: $this->get(self::$namespaces['special'] . 'content', false, $this->env);
1718
+
1719
+ if (! $content) {
1720
+ $this->throwError('Expected @content inside of mixin');
1721
+ break;
1722
+ }
1723
+
1724
+ $storeEnv = $this->storeEnv;
1725
+ $this->storeEnv = $content->scope;
1726
+
1727
+ $this->compileChildrenNoReturn($content->children, $out);
1728
+
1729
+ $this->storeEnv = $storeEnv;
1730
+ break;
1731
+
1732
+ case Type::T_DEBUG:
1733
+ list(, $value) = $child;
1734
+
1735
+ $line = $this->sourceLine;
1736
+ $value = $this->compileValue($this->reduce($value, true));
1737
+ fwrite($this->stderr, "Line $line DEBUG: $value\n");
1738
+ break;
1739
+
1740
+ case Type::T_WARN:
1741
+ list(, $value) = $child;
1742
+
1743
+ $line = $this->sourceLine;
1744
+ $value = $this->compileValue($this->reduce($value, true));
1745
+ echo "Line $line WARN: $value\n";
1746
+ break;
1747
+
1748
+ case Type::T_ERROR:
1749
+ list(, $value) = $child;
1750
+
1751
+ $line = $this->sourceLine;
1752
+ $value = $this->compileValue($this->reduce($value, true));
1753
+ $this->throwError("Line $line ERROR: $value\n");
1754
+ break;
1755
+
1756
+ case Type::T_CONTROL:
1757
+ $this->throwError('@break/@continue not permitted in this scope');
1758
+ break;
1759
+
1760
+ default:
1761
+ $this->throwError("unknown child type: $child[0]");
1762
+ }
1763
+ }
1764
+
1765
+ /**
1766
+ * Reduce expression to string
1767
+ *
1768
+ * @param array $exp
1769
+ *
1770
+ * @return array
1771
+ */
1772
+ protected function expToString($exp)
1773
+ {
1774
+ list(, $op, $left, $right, /* $inParens */, $whiteLeft, $whiteRight) = $exp;
1775
+
1776
+ $content = [$this->reduce($left)];
1777
+
1778
+ if ($whiteLeft) {
1779
+ $content[] = ' ';
1780
+ }
1781
+
1782
+ $content[] = $op;
1783
+
1784
+ if ($whiteRight) {
1785
+ $content[] = ' ';
1786
+ }
1787
+
1788
+ $content[] = $this->reduce($right);
1789
+
1790
+ return [Type::T_STRING, '', $content];
1791
+ }
1792
+
1793
+ /**
1794
+ * Is truthy?
1795
+ *
1796
+ * @param array $value
1797
+ *
1798
+ * @return array
1799
+ */
1800
+ protected function isTruthy($value)
1801
+ {
1802
+ return $value !== self::$false && $value !== self::$null;
1803
+ }
1804
+
1805
+ /**
1806
+ * Should $value cause its operand to eval
1807
+ *
1808
+ * @param array $value
1809
+ *
1810
+ * @return boolean
1811
+ */
1812
+ protected function shouldEval($value)
1813
+ {
1814
+ switch ($value[0]) {
1815
+ case Type::T_EXPRESSION:
1816
+ if ($value[1] === '/') {
1817
+ return $this->shouldEval($value[2], $value[3]);
1818
+ }
1819
+
1820
+ // fall-thru
1821
+ case Type::T_VARIABLE:
1822
+ case Type::T_FUNCTION_CALL:
1823
+ return true;
1824
+ }
1825
+
1826
+ return false;
1827
+ }
1828
+
1829
+ /**
1830
+ * Reduce value
1831
+ *
1832
+ * @param array $value
1833
+ * @param boolean $inExp
1834
+ *
1835
+ * @return array
1836
+ */
1837
+ protected function reduce($value, $inExp = false)
1838
+ {
1839
+ list($type) = $value;
1840
+
1841
+ switch ($type) {
1842
+ case Type::T_EXPRESSION:
1843
+ list(, $op, $left, $right, $inParens) = $value;
1844
+
1845
+ $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
1846
+ $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
1847
+
1848
+ $left = $this->reduce($left, true);
1849
+
1850
+ if ($op !== 'and' && $op !== 'or') {
1851
+ $right = $this->reduce($right, true);
1852
+ }
1853
+
1854
+ // special case: looks like css shorthand
1855
+ if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2])
1856
+ && (($right[0] !== Type::T_NUMBER && $right[2] != '')
1857
+ || ($right[0] === Type::T_NUMBER && ! $right->unitless()))
1858
+ ) {
1859
+ return $this->expToString($value);
1860
+ }
1861
+
1862
+ $left = $this->coerceForExpression($left);
1863
+ $right = $this->coerceForExpression($right);
1864
+
1865
+ $ltype = $left[0];
1866
+ $rtype = $right[0];
1867
+
1868
+ $ucOpName = ucfirst($opName);
1869
+ $ucLType = ucfirst($ltype);
1870
+ $ucRType = ucfirst($rtype);
1871
+
1872
+ // this tries:
1873
+ // 1. op[op name][left type][right type]
1874
+ // 2. op[left type][right type] (passing the op as first arg
1875
+ // 3. op[op name]
1876
+ $fn = "op${ucOpName}${ucLType}${ucRType}";
1877
+
1878
+ if (is_callable([$this, $fn]) ||
1879
+ (($fn = "op${ucLType}${ucRType}") &&
1880
+ is_callable([$this, $fn]) &&
1881
+ $passOp = true) ||
1882
+ (($fn = "op${ucOpName}") &&
1883
+ is_callable([$this, $fn]) &&
1884
+ $genOp = true)
1885
+ ) {
1886
+ $coerceUnit = false;
1887
+
1888
+ if (! isset($genOp) &&
1889
+ $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER
1890
+ ) {
1891
+ $coerceUnit = true;
1892
+
1893
+ switch ($opName) {
1894
+ case 'mul':
1895
+ $targetUnit = $left[2];
1896
+
1897
+ foreach ($right[2] as $unit => $exp) {
1898
+ $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp;
1899
+ }
1900
+ break;
1901
+
1902
+ case 'div':
1903
+ $targetUnit = $left[2];
1904
+
1905
+ foreach ($right[2] as $unit => $exp) {
1906
+ $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) - $exp;
1907
+ }
1908
+ break;
1909
+
1910
+ case 'mod':
1911
+ $targetUnit = $left[2];
1912
+ break;
1913
+
1914
+ default:
1915
+ $targetUnit = $left->unitless() ? $right[2] : $left[2];
1916
+ }
1917
+
1918
+ if (! $left->unitless() && ! $right->unitless()) {
1919
+ $left = $left->normalize();
1920
+ $right = $right->normalize();
1921
+ }
1922
+ }
1923
+
1924
+ $shouldEval = $inParens || $inExp;
1925
+
1926
+ if (isset($passOp)) {
1927
+ $out = $this->$fn($op, $left, $right, $shouldEval);
1928
+ } else {
1929
+ $out = $this->$fn($left, $right, $shouldEval);
1930
+ }
1931
+
1932
+ if (isset($out)) {
1933
+ if ($coerceUnit && $out[0] === Type::T_NUMBER) {
1934
+ $out = $out->coerce($targetUnit);
1935
+ }
1936
+
1937
+ return $out;
1938
+ }
1939
+ }
1940
+
1941
+ return $this->expToString($value);
1942
+
1943
+ case Type::T_UNARY:
1944
+ list(, $op, $exp, $inParens) = $value;
1945
+
1946
+ $inExp = $inExp || $this->shouldEval($exp);
1947
+ $exp = $this->reduce($exp);
1948
+
1949
+ if ($exp[0] === Type::T_NUMBER) {
1950
+ switch ($op) {
1951
+ case '+':
1952
+ return new Node\Number($exp[1], $exp[2]);
1953
+
1954
+ case '-':
1955
+ return new Node\Number(-$exp[1], $exp[2]);
1956
+ }
1957
+ }
1958
+
1959
+ if ($op === 'not') {
1960
+ if ($inExp || $inParens) {
1961
+ if ($exp === self::$false || $exp === self::$null) {
1962
+ return self::$true;
1963
+ }
1964
+
1965
+ return self::$false;
1966
+ }
1967
+
1968
+ $op = $op . ' ';
1969
+ }
1970
+
1971
+ return [Type::T_STRING, '', [$op, $exp]];
1972
+
1973
+ case Type::T_VARIABLE:
1974
+ list(, $name) = $value;
1975
+
1976
+ return $this->reduce($this->get($name));
1977
+
1978
+ case Type::T_LIST:
1979
+ foreach ($value[2] as &$item) {
1980
+ $item = $this->reduce($item);
1981
+ }
1982
+
1983
+ return $value;
1984
+
1985
+ case Type::T_MAP:
1986
+ foreach ($value[1] as &$item) {
1987
+ $item = $this->reduce($item);
1988
+ }
1989
+
1990
+ foreach ($value[2] as &$item) {
1991
+ $item = $this->reduce($item);
1992
+ }
1993
+
1994
+ return $value;
1995
+
1996
+ case Type::T_STRING:
1997
+ foreach ($value[2] as &$item) {
1998
+ if (is_array($item) || $item instanceof \ArrayAccess) {
1999
+ $item = $this->reduce($item);
2000
+ }
2001
+ }
2002
+
2003
+ return $value;
2004
+
2005
+ case Type::T_INTERPOLATE:
2006
+ $value[1] = $this->reduce($value[1]);
2007
+
2008
+ return $value;
2009
+
2010
+ case Type::T_FUNCTION_CALL:
2011
+ list(, $name, $argValues) = $value;
2012
+
2013
+ return $this->fncall($name, $argValues);
2014
+
2015
+ default:
2016
+ return $value;
2017
+ }
2018
+ }
2019
+
2020
+ /**
2021
+ * Function caller
2022
+ *
2023
+ * @param string $name
2024
+ * @param array $argValues
2025
+ *
2026
+ * @return array|null
2027
+ */
2028
+ private function fncall($name, $argValues)
2029
+ {
2030
+ // SCSS @function
2031
+ if ($this->callScssFunction($name, $argValues, $returnValue)) {
2032
+ return $returnValue;
2033
+ }
2034
+
2035
+ // native PHP functions
2036
+ if ($this->callNativeFunction($name, $argValues, $returnValue)) {
2037
+ return $returnValue;
2038
+ }
2039
+
2040
+ // for CSS functions, simply flatten the arguments into a list
2041
+ $listArgs = [];
2042
+
2043
+ foreach ((array) $argValues as $arg) {
2044
+ if (empty($arg[0])) {
2045
+ $listArgs[] = $this->reduce($arg[1]);
2046
+ }
2047
+ }
2048
+
2049
+ return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', $listArgs]];
2050
+ }
2051
+
2052
+ /**
2053
+ * Normalize name
2054
+ *
2055
+ * @param string $name
2056
+ *
2057
+ * @return string
2058
+ */
2059
+ protected function normalizeName($name)
2060
+ {
2061
+ return str_replace('-', '_', $name);
2062
+ }
2063
+
2064
+ /**
2065
+ * Normalize value
2066
+ *
2067
+ * @param array $value
2068
+ *
2069
+ * @return array
2070
+ */
2071
+ public function normalizeValue($value)
2072
+ {
2073
+ $value = $this->coerceForExpression($this->reduce($value));
2074
+ list($type) = $value;
2075
+
2076
+ switch ($type) {
2077
+ case Type::T_LIST:
2078
+ $value = $this->extractInterpolation($value);
2079
+
2080
+ if ($value[0] !== Type::T_LIST) {
2081
+ return [Type::T_KEYWORD, $this->compileValue($value)];
2082
+ }
2083
+
2084
+ foreach ($value[2] as $key => $item) {
2085
+ $value[2][$key] = $this->normalizeValue($item);
2086
+ }
2087
+
2088
+ return $value;
2089
+
2090
+ case Type::T_STRING:
2091
+ return [$type, '"', [$this->compileStringContent($value)]];
2092
+
2093
+ case Type::T_NUMBER:
2094
+ return $value->normalize();
2095
+
2096
+ case Type::T_INTERPOLATE:
2097
+ return [Type::T_KEYWORD, $this->compileValue($value)];
2098
+
2099
+ default:
2100
+ return $value;
2101
+ }
2102
+ }
2103
+
2104
+ /**
2105
+ * Add numbers
2106
+ *
2107
+ * @param array $left
2108
+ * @param array $right
2109
+ *
2110
+ * @return array
2111
+ */
2112
+ protected function opAddNumberNumber($left, $right)
2113
+ {
2114
+ return new Node\Number($left[1] + $right[1], $left[2]);
2115
+ }
2116
+
2117
+ /**
2118
+ * Multiply numbers
2119
+ *
2120
+ * @param array $left
2121
+ * @param array $right
2122
+ *
2123
+ * @return array
2124
+ */
2125
+ protected function opMulNumberNumber($left, $right)
2126
+ {
2127
+ return new Node\Number($left[1] * $right[1], $left[2]);
2128
+ }
2129
+
2130
+ /**
2131
+ * Subtract numbers
2132
+ *
2133
+ * @param array $left
2134
+ * @param array $right
2135
+ *
2136
+ * @return array
2137
+ */
2138
+ protected function opSubNumberNumber($left, $right)
2139
+ {
2140
+ return new Node\Number($left[1] - $right[1], $left[2]);
2141
+ }
2142
+
2143
+ /**
2144
+ * Divide numbers
2145
+ *
2146
+ * @param array $left
2147
+ * @param array $right
2148
+ *
2149
+ * @return array
2150
+ */
2151
+ protected function opDivNumberNumber($left, $right)
2152
+ {
2153
+ if ($right[1] == 0) {
2154
+ return [Type::T_STRING, '', [$left[1] . $left[2] . '/' . $right[1] . $right[2]]];
2155
+ }
2156
+
2157
+ return new Node\Number($left[1] / $right[1], $left[2]);
2158
+ }
2159
+
2160
+ /**
2161
+ * Mod numbers
2162
+ *
2163
+ * @param array $left
2164
+ * @param array $right
2165
+ *
2166
+ * @return array
2167
+ */
2168
+ protected function opModNumberNumber($left, $right)
2169
+ {
2170
+ return new Node\Number($left[1] % $right[1], $left[2]);
2171
+ }
2172
+
2173
+ /**
2174
+ * Add strings
2175
+ *
2176
+ * @param array $left
2177
+ * @param array $right
2178
+ *
2179
+ * @return array
2180
+ */
2181
+ protected function opAdd($left, $right)
2182
+ {
2183
+ if ($strLeft = $this->coerceString($left)) {
2184
+ if ($right[0] === Type::T_STRING) {
2185
+ $right[1] = '';
2186
+ }
2187
+
2188
+ $strLeft[2][] = $right;
2189
+
2190
+ return $strLeft;
2191
+ }
2192
+
2193
+ if ($strRight = $this->coerceString($right)) {
2194
+ if ($left[0] === Type::T_STRING) {
2195
+ $left[1] = '';
2196
+ }
2197
+
2198
+ array_unshift($strRight[2], $left);
2199
+
2200
+ return $strRight;
2201
+ }
2202
+ }
2203
+
2204
+ /**
2205
+ * Boolean and
2206
+ *
2207
+ * @param array $left
2208
+ * @param array $right
2209
+ * @param boolean $shouldEval
2210
+ *
2211
+ * @return array
2212
+ */
2213
+ protected function opAnd($left, $right, $shouldEval)
2214
+ {
2215
+ if (! $shouldEval) {
2216
+ return;
2217
+ }
2218
+
2219
+ if ($left !== self::$false) {
2220
+ return $this->reduce($right, true);
2221
+ }
2222
+
2223
+ return $left;
2224
+ }
2225
+
2226
+ /**
2227
+ * Boolean or
2228
+ *
2229
+ * @param array $left
2230
+ * @param array $right
2231
+ * @param boolean $shouldEval
2232
+ *
2233
+ * @return array
2234
+ */
2235
+ protected function opOr($left, $right, $shouldEval)
2236
+ {
2237
+ if (! $shouldEval) {
2238
+ return;
2239
+ }
2240
+
2241
+ if ($left !== self::$false) {
2242
+ return $left;
2243
+ }
2244
+
2245
+ return $this->reduce($right, true);
2246
+ }
2247
+
2248
+ /**
2249
+ * Compare colors
2250
+ *
2251
+ * @param string $op
2252
+ * @param array $left
2253
+ * @param array $right
2254
+ *
2255
+ * @return array
2256
+ */
2257
+ protected function opColorColor($op, $left, $right)
2258
+ {
2259
+ $out = [Type::T_COLOR];
2260
+
2261
+ foreach ([1, 2, 3] as $i) {
2262
+ $lval = isset($left[$i]) ? $left[$i] : 0;
2263
+ $rval = isset($right[$i]) ? $right[$i] : 0;
2264
+
2265
+ switch ($op) {
2266
+ case '+':
2267
+ $out[] = $lval + $rval;
2268
+ break;
2269
+
2270
+ case '-':
2271
+ $out[] = $lval - $rval;
2272
+ break;
2273
+
2274
+ case '*':
2275
+ $out[] = $lval * $rval;
2276
+ break;
2277
+
2278
+ case '%':
2279
+ $out[] = $lval % $rval;
2280
+ break;
2281
+
2282
+ case '/':
2283
+ if ($rval == 0) {
2284
+ $this->throwError("color: Can't divide by zero");
2285
+ break 2;
2286
+ }
2287
+
2288
+ $out[] = (int) ($lval / $rval);
2289
+ break;
2290
+
2291
+ case '==':
2292
+ return $this->opEq($left, $right);
2293
+
2294
+ case '!=':
2295
+ return $this->opNeq($left, $right);
2296
+
2297
+ default:
2298
+ $this->throwError("color: unknown op $op");
2299
+ break 2;
2300
+ }
2301
+ }
2302
+
2303
+ if (isset($left[4])) {
2304
+ $out[4] = $left[4];
2305
+ } elseif (isset($right[4])) {
2306
+ $out[4] = $right[4];
2307
+ }
2308
+
2309
+ return $this->fixColor($out);
2310
+ }
2311
+
2312
+ /**
2313
+ * Compare color and number
2314
+ *
2315
+ * @param string $op
2316
+ * @param array $left
2317
+ * @param array $right
2318
+ *
2319
+ * @return array
2320
+ */
2321
+ protected function opColorNumber($op, $left, $right)
2322
+ {
2323
+ $value = $right[1];
2324
+
2325
+ return $this->opColorColor(
2326
+ $op,
2327
+ $left,
2328
+ [Type::T_COLOR, $value, $value, $value]
2329
+ );
2330
+ }
2331
+
2332
+ /**
2333
+ * Compare number and color
2334
+ *
2335
+ * @param string $op
2336
+ * @param array $left
2337
+ * @param array $right
2338
+ *
2339
+ * @return array
2340
+ */
2341
+ protected function opNumberColor($op, $left, $right)
2342
+ {
2343
+ $value = $left[1];
2344
+
2345
+ return $this->opColorColor(
2346
+ $op,
2347
+ [Type::T_COLOR, $value, $value, $value],
2348
+ $right
2349
+ );
2350
+ }
2351
+
2352
+ /**
2353
+ * Compare number1 == number2
2354
+ *
2355
+ * @param array $left
2356
+ * @param array $right
2357
+ *
2358
+ * @return array
2359
+ */
2360
+ protected function opEq($left, $right)
2361
+ {
2362
+ if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
2363
+ $lStr[1] = '';
2364
+ $rStr[1] = '';
2365
+
2366
+ $left = $this->compileValue($lStr);
2367
+ $right = $this->compileValue($rStr);
2368
+ }
2369
+
2370
+ return $this->toBool($left === $right);
2371
+ }
2372
+
2373
+ /**
2374
+ * Compare number1 != number2
2375
+ *
2376
+ * @param array $left
2377
+ * @param array $right
2378
+ *
2379
+ * @return array
2380
+ */
2381
+ protected function opNeq($left, $right)
2382
+ {
2383
+ if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
2384
+ $lStr[1] = '';
2385
+ $rStr[1] = '';
2386
+
2387
+ $left = $this->compileValue($lStr);
2388
+ $right = $this->compileValue($rStr);
2389
+ }
2390
+
2391
+ return $this->toBool($left !== $right);
2392
+ }
2393
+
2394
+ /**
2395
+ * Compare number1 >= number2
2396
+ *
2397
+ * @param array $left
2398
+ * @param array $right
2399
+ *
2400
+ * @return array
2401
+ */
2402
+ protected function opGteNumberNumber($left, $right)
2403
+ {
2404
+ return $this->toBool($left[1] >= $right[1]);
2405
+ }
2406
+
2407
+ /**
2408
+ * Compare number1 > number2
2409
+ *
2410
+ * @param array $left
2411
+ * @param array $right
2412
+ *
2413
+ * @return array
2414
+ */
2415
+ protected function opGtNumberNumber($left, $right)
2416
+ {
2417
+ return $this->toBool($left[1] > $right[1]);
2418
+ }
2419
+
2420
+ /**
2421
+ * Compare number1 <= number2
2422
+ *
2423
+ * @param array $left
2424
+ * @param array $right
2425
+ *
2426
+ * @return array
2427
+ */
2428
+ protected function opLteNumberNumber($left, $right)
2429
+ {
2430
+ return $this->toBool($left[1] <= $right[1]);
2431
+ }
2432
+
2433
+ /**
2434
+ * Compare number1 < number2
2435
+ *
2436
+ * @param array $left
2437
+ * @param array $right
2438
+ *
2439
+ * @return array
2440
+ */
2441
+ protected function opLtNumberNumber($left, $right)
2442
+ {
2443
+ return $this->toBool($left[1] < $right[1]);
2444
+ }
2445
+
2446
+ /**
2447
+ * Three-way comparison, aka spaceship operator
2448
+ *
2449
+ * @param array $left
2450
+ * @param array $right
2451
+ *
2452
+ * @return array
2453
+ */
2454
+ protected function opCmpNumberNumber($left, $right)
2455
+ {
2456
+ $n = $left[1] - $right[1];
2457
+
2458
+ return new Node\Number($n ? $n / abs($n) : 0, '');
2459
+ }
2460
+
2461
+ /**
2462
+ * Cast to boolean
2463
+ *
2464
+ * @api
2465
+ *
2466
+ * @param mixed $thing
2467
+ *
2468
+ * @return array
2469
+ */
2470
+ public function toBool($thing)
2471
+ {
2472
+ return $thing ? self::$true : self::$false;
2473
+ }
2474
+
2475
+ /**
2476
+ * Compiles a primitive value into a CSS property value.
2477
+ *
2478
+ * Values in scssphp are typed by being wrapped in arrays, their format is
2479
+ * typically:
2480
+ *
2481
+ * array(type, contents [, additional_contents]*)
2482
+ *
2483
+ * The input is expected to be reduced. This function will not work on
2484
+ * things like expressions and variables.
2485
+ *
2486
+ * @api
2487
+ *
2488
+ * @param array $value
2489
+ *
2490
+ * @return string
2491
+ */
2492
+ public function compileValue($value)
2493
+ {
2494
+ $value = $this->reduce($value);
2495
+
2496
+ list($type) = $value;
2497
+
2498
+ switch ($type) {
2499
+ case Type::T_KEYWORD:
2500
+ return $value[1];
2501
+
2502
+ case Type::T_COLOR:
2503
+ // [1] - red component (either number for a %)
2504
+ // [2] - green component
2505
+ // [3] - blue component
2506
+ // [4] - optional alpha component
2507
+ list(, $r, $g, $b) = $value;
2508
+
2509
+ $r = round($r);
2510
+ $g = round($g);
2511
+ $b = round($b);
2512
+
2513
+ if (count($value) === 5 && $value[4] !== 1) { // rgba
2514
+ return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $value[4] . ')';
2515
+ }
2516
+
2517
+ $h = sprintf('#%02x%02x%02x', $r, $g, $b);
2518
+
2519
+ // Converting hex color to short notation (e.g. #003399 to #039)
2520
+ if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
2521
+ $h = '#' . $h[1] . $h[3] . $h[5];
2522
+ }
2523
+
2524
+ return $h;
2525
+
2526
+ case Type::T_NUMBER:
2527
+ return $value->output($this);
2528
+
2529
+ case Type::T_STRING:
2530
+ return $value[1] . $this->compileStringContent($value) . $value[1];
2531
+
2532
+ case Type::T_FUNCTION:
2533
+ $args = ! empty($value[2]) ? $this->compileValue($value[2]) : '';
2534
+
2535
+ return "$value[1]($args)";
2536
+
2537
+ case Type::T_LIST:
2538
+ $value = $this->extractInterpolation($value);
2539
+
2540
+ if ($value[0] !== Type::T_LIST) {
2541
+ return $this->compileValue($value);
2542
+ }
2543
+
2544
+ list(, $delim, $items) = $value;
2545
+
2546
+ if ($delim !== ' ') {
2547
+ $delim .= ' ';
2548
+ }
2549
+
2550
+ $filtered = [];
2551
+
2552
+ foreach ($items as $item) {
2553
+ if ($item[0] === Type::T_NULL) {
2554
+ continue;
2555
+ }
2556
+
2557
+ $filtered[] = $this->compileValue($item);
2558
+ }
2559
+
2560
+ return implode("$delim", $filtered);
2561
+
2562
+ case Type::T_MAP:
2563
+ $keys = $value[1];
2564
+ $values = $value[2];
2565
+ $filtered = [];
2566
+
2567
+ for ($i = 0, $s = count($keys); $i < $s; $i++) {
2568
+ $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
2569
+ }
2570
+
2571
+ array_walk($filtered, function (&$value, $key) {
2572
+ $value = $key . ': ' . $value;
2573
+ });
2574
+
2575
+ return '(' . implode(', ', $filtered) . ')';
2576
+
2577
+ case Type::T_INTERPOLATED:
2578
+ // node created by extractInterpolation
2579
+ list(, $interpolate, $left, $right) = $value;
2580
+ list(,, $whiteLeft, $whiteRight) = $interpolate;
2581
+
2582
+ $left = count($left[2]) > 0 ?
2583
+ $this->compileValue($left) . $whiteLeft : '';
2584
+
2585
+ $right = count($right[2]) > 0 ?
2586
+ $whiteRight . $this->compileValue($right) : '';
2587
+
2588
+ return $left . $this->compileValue($interpolate) . $right;
2589
+
2590
+ case Type::T_INTERPOLATE:
2591
+ // raw parse node
2592
+ list(, $exp) = $value;
2593
+
2594
+ // strip quotes if it's a string
2595
+ $reduced = $this->reduce($exp);
2596
+
2597
+ switch ($reduced[0]) {
2598
+ case Type::T_STRING:
2599
+ $reduced = [Type::T_KEYWORD, $this->compileStringContent($reduced)];
2600
+ break;
2601
+
2602
+ case Type::T_NULL:
2603
+ $reduced = [Type::T_KEYWORD, ''];
2604
+ }
2605
+
2606
+ return $this->compileValue($reduced);
2607
+
2608
+ case Type::T_NULL:
2609
+ return 'null';
2610
+
2611
+ default:
2612
+ $this->throwError("unknown value type: $type");
2613
+ }
2614
+ }
2615
+
2616
+ /**
2617
+ * Flatten list
2618
+ *
2619
+ * @param array $list
2620
+ *
2621
+ * @return string
2622
+ */
2623
+ protected function flattenList($list)
2624
+ {
2625
+ return $this->compileValue($list);
2626
+ }
2627
+
2628
+ /**
2629
+ * Compile string content
2630
+ *
2631
+ * @param array $string
2632
+ *
2633
+ * @return string
2634
+ */
2635
+ protected function compileStringContent($string)
2636
+ {
2637
+ $parts = [];
2638
+
2639
+ foreach ($string[2] as $part) {
2640
+ if (is_array($part) || $part instanceof \ArrayAccess) {
2641
+ $parts[] = $this->compileValue($part);
2642
+ } else {
2643
+ $parts[] = $part;
2644
+ }
2645
+ }
2646
+
2647
+ return implode($parts);
2648
+ }
2649
+
2650
+ /**
2651
+ * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
2652
+ *
2653
+ * @param array $list
2654
+ *
2655
+ * @return array
2656
+ */
2657
+ protected function extractInterpolation($list)
2658
+ {
2659
+ $items = $list[2];
2660
+
2661
+ foreach ($items as $i => $item) {
2662
+ if ($item[0] === Type::T_INTERPOLATE) {
2663
+ $before = [Type::T_LIST, $list[1], array_slice($items, 0, $i)];
2664
+ $after = [Type::T_LIST, $list[1], array_slice($items, $i + 1)];
2665
+
2666
+ return [Type::T_INTERPOLATED, $item, $before, $after];
2667
+ }
2668
+ }
2669
+
2670
+ return $list;
2671
+ }
2672
+
2673
+ /**
2674
+ * Find the final set of selectors
2675
+ *
2676
+ * @param \Leafo\ScssPhp\Compiler\Environment $env
2677
+ *
2678
+ * @return array
2679
+ */
2680
+ protected function multiplySelectors(Environment $env)
2681
+ {
2682
+ $envs = $this->compactEnv($env);
2683
+ $selectors = [];
2684
+ $parentSelectors = [[]];
2685
+
2686
+ while ($env = array_pop($envs)) {
2687
+ if (empty($env->selectors)) {
2688
+ continue;
2689
+ }
2690
+
2691
+ $selectors = [];
2692
+
2693
+ foreach ($env->selectors as $selector) {
2694
+ foreach ($parentSelectors as $parent) {
2695
+ $selectors[] = $this->joinSelectors($parent, $selector);
2696
+ }
2697
+ }
2698
+
2699
+ $parentSelectors = $selectors;
2700
+ }
2701
+
2702
+ return $selectors;
2703
+ }
2704
+
2705
+ /**
2706
+ * Join selectors; looks for & to replace, or append parent before child
2707
+ *
2708
+ * @param array $parent
2709
+ * @param array $child
2710
+ *
2711
+ * @return array
2712
+ */
2713
+ protected function joinSelectors($parent, $child)
2714
+ {
2715
+ $setSelf = false;
2716
+ $out = [];
2717
+
2718
+ foreach ($child as $part) {
2719
+ $newPart = [];
2720
+
2721
+ foreach ($part as $p) {
2722
+ if ($p === self::$selfSelector) {
2723
+ $setSelf = true;
2724
+
2725
+ foreach ($parent as $i => $parentPart) {
2726
+ if ($i > 0) {
2727
+ $out[] = $newPart;
2728
+ $newPart = [];
2729
+ }
2730
+
2731
+ foreach ($parentPart as $pp) {
2732
+ $newPart[] = $pp;
2733
+ }
2734
+ }
2735
+ } else {
2736
+ $newPart[] = $p;
2737
+ }
2738
+ }
2739
+
2740
+ $out[] = $newPart;
2741
+ }
2742
+
2743
+ return $setSelf ? $out : array_merge($parent, $child);
2744
+ }
2745
+
2746
+ /**
2747
+ * Multiply media
2748
+ *
2749
+ * @param \Leafo\ScssPhp\Compiler\Environment $env
2750
+ * @param array $childQueries
2751
+ *
2752
+ * @return array
2753
+ */
2754
+ protected function multiplyMedia(Environment $env = null, $childQueries = null)
2755
+ {
2756
+ if (! isset($env) ||
2757
+ ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA
2758
+ ) {
2759
+ return $childQueries;
2760
+ }
2761
+
2762
+ // plain old block, skip
2763
+ if (empty($env->block->type)) {
2764
+ return $this->multiplyMedia($env->parent, $childQueries);
2765
+ }
2766
+
2767
+ $parentQueries = isset($env->block->queryList)
2768
+ ? $env->block->queryList
2769
+ : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
2770
+
2771
+ if ($childQueries === null) {
2772
+ $childQueries = $parentQueries;
2773
+ } else {
2774
+ $originalQueries = $childQueries;
2775
+ $childQueries = [];
2776
+
2777
+ foreach ($parentQueries as $parentQuery) {
2778
+ foreach ($originalQueries as $childQuery) {
2779
+ $childQueries []= array_merge($parentQuery, $childQuery);
2780
+ }
2781
+ }
2782
+ }
2783
+
2784
+ return $this->multiplyMedia($env->parent, $childQueries);
2785
+ }
2786
+
2787
+ /**
2788
+ * Convert env linked list to stack
2789
+ *
2790
+ * @param \Leafo\ScssPhp\Compiler\Environment $env
2791
+ *
2792
+ * @return array
2793
+ */
2794
+ private function compactEnv(Environment $env)
2795
+ {
2796
+ for ($envs = []; $env; $env = $env->parent) {
2797
+ $envs[] = $env;
2798
+ }
2799
+
2800
+ return $envs;
2801
+ }
2802
+
2803
+ /**
2804
+ * Convert env stack to singly linked list
2805
+ *
2806
+ * @param array $envs
2807
+ *
2808
+ * @return \Leafo\ScssPhp\Compiler\Environment
2809
+ */
2810
+ private function extractEnv($envs)
2811
+ {
2812
+ for ($env = null; $e = array_pop($envs);) {
2813
+ $e->parent = $env;
2814
+ $env = $e;
2815
+ }
2816
+
2817
+ return $env;
2818
+ }
2819
+
2820
+ /**
2821
+ * Push environment
2822
+ *
2823
+ * @param \Leafo\ScssPhp\Block $block
2824
+ *
2825
+ * @return \Leafo\ScssPhp\Compiler\Environment
2826
+ */
2827
+ protected function pushEnv(Block $block = null)
2828
+ {
2829
+ $env = new Environment;
2830
+ $env->parent = $this->env;
2831
+ $env->store = [];
2832
+ $env->block = $block;
2833
+ $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
2834
+
2835
+ $this->env = $env;
2836
+
2837
+ return $env;
2838
+ }
2839
+
2840
+ /**
2841
+ * Pop environment
2842
+ */
2843
+ protected function popEnv()
2844
+ {
2845
+ $this->env = $this->env->parent;
2846
+ }
2847
+
2848
+ /**
2849
+ * Get store environment
2850
+ *
2851
+ * @return \Leafo\ScssPhp\Compiler\Environment
2852
+ */
2853
+ protected function getStoreEnv()
2854
+ {
2855
+ return isset($this->storeEnv) ? $this->storeEnv : $this->env;
2856
+ }
2857
+
2858
+ /**
2859
+ * Set variable
2860
+ *
2861
+ * @param string $name
2862
+ * @param mixed $value
2863
+ * @param boolean $shadow
2864
+ * @param \Leafo\ScssPhp\Compiler\Environment $env
2865
+ */
2866
+ protected function set($name, $value, $shadow = false, Environment $env = null)
2867
+ {
2868
+ $name = $this->normalizeName($name);
2869
+
2870
+ if (! isset($env)) {
2871
+ $env = $this->getStoreEnv();
2872
+ }
2873
+
2874
+ if ($shadow) {
2875
+ $this->setRaw($name, $value, $env);
2876
+ } else {
2877
+ $this->setExisting($name, $value, $env);
2878
+ }
2879
+ }
2880
+
2881
+ /**
2882
+ * Set existing variable
2883
+ *
2884
+ * @param string $name
2885
+ * @param mixed $value
2886
+ * @param \Leafo\ScssPhp\Compiler\Environment $env
2887
+ */
2888
+ protected function setExisting($name, $value, Environment $env)
2889
+ {
2890
+ $storeEnv = $env;
2891
+
2892
+ $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
2893
+
2894
+ for (;;) {
2895
+ if (array_key_exists($name, $env->store)) {
2896
+ break;
2897
+ }
2898
+
2899
+ if (! $hasNamespace && isset($env->marker)) {
2900
+ $env = $storeEnv;
2901
+ break;
2902
+ }
2903
+
2904
+ if (! isset($env->parent)) {
2905
+ $env = $storeEnv;
2906
+ break;
2907
+ }
2908
+
2909
+ $env = $env->parent;
2910
+ }
2911
+
2912
+ $env->store[$name] = $value;
2913
+ }
2914
+
2915
+ /**
2916
+ * Set raw variable
2917
+ *
2918
+ * @param string $name
2919
+ * @param mixed $value
2920
+ * @param \Leafo\ScssPhp\Compiler\Environment $env
2921
+ */
2922
+ protected function setRaw($name, $value, Environment $env)
2923
+ {
2924
+ $env->store[$name] = $value;
2925
+ }
2926
+
2927
+ /**
2928
+ * Get variable
2929
+ *
2930
+ * @api
2931
+ *
2932
+ * @param string $name
2933
+ * @param boolean $shouldThrow
2934
+ * @param \Leafo\ScssPhp\Compiler\Environment $env
2935
+ *
2936
+ * @return mixed
2937
+ */
2938
+ public function get($name, $shouldThrow = true, Environment $env = null)
2939
+ {
2940
+ $name = $this->normalizeName($name);
2941
+
2942
+ if (! isset($env)) {
2943
+ $env = $this->getStoreEnv();
2944
+ }
2945
+
2946
+ $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
2947
+
2948
+ for (;;) {
2949
+ if (array_key_exists($name, $env->store)) {
2950
+ return $env->store[$name];
2951
+ }
2952
+
2953
+ if (! $hasNamespace && isset($env->marker)) {
2954
+ $env = $this->rootEnv;
2955
+ continue;
2956
+ }
2957
+
2958
+ if (! isset($env->parent)) {
2959
+ break;
2960
+ }
2961
+
2962
+ $env = $env->parent;
2963
+ }
2964
+
2965
+ if ($shouldThrow) {
2966
+ $this->throwError("Undefined variable \$$name");
2967
+ }
2968
+
2969
+ // found nothing
2970
+ }
2971
+
2972
+ /**
2973
+ * Has variable?
2974
+ *
2975
+ * @param string $name
2976
+ * @param \Leafo\ScssPhp\Compiler\Environment $env
2977
+ *
2978
+ * @return boolean
2979
+ */
2980
+ protected function has($name, Environment $env = null)
2981
+ {
2982
+ return $this->get($name, false, $env) !== null;
2983
+ }
2984
+
2985
+ /**
2986
+ * Inject variables
2987
+ *
2988
+ * @param array $args
2989
+ */
2990
+ protected function injectVariables(array $args)
2991
+ {
2992
+ if (empty($args)) {
2993
+ return;
2994
+ }
2995
+
2996
+ $parser = $this->parserFactory(__METHOD__);
2997
+
2998
+ foreach ($args as $name => $strValue) {
2999
+ if ($name[0] === '$') {
3000
+ $name = substr($name, 1);
3001
+ }
3002
+
3003
+ if (! $parser->parseValue($strValue, $value)) {
3004
+ $value = $this->coerceValue($strValue);
3005
+ }
3006
+
3007
+ $this->set($name, $value);
3008
+ }
3009
+ }
3010
+
3011
+ /**
3012
+ * Set variables
3013
+ *
3014
+ * @api
3015
+ *
3016
+ * @param array $variables
3017
+ */
3018
+ public function setVariables(array $variables)
3019
+ {
3020
+ $this->registeredVars = array_merge($this->registeredVars, $variables);
3021
+ }
3022
+
3023
+ /**
3024
+ * Unset variable
3025
+ *
3026
+ * @api
3027
+ *
3028
+ * @param string $name
3029
+ */
3030
+ public function unsetVariable($name)
3031
+ {
3032
+ unset($this->registeredVars[$name]);
3033
+ }
3034
+
3035
+ /**
3036
+ * Returns list of variables
3037
+ *
3038
+ * @api
3039
+ *
3040
+ * @return array
3041
+ */
3042
+ public function getVariables()
3043
+ {
3044
+ return $this->registeredVars;
3045
+ }
3046
+
3047
+ /**
3048
+ * Adds to list of parsed files
3049
+ *
3050
+ * @api
3051
+ *
3052
+ * @param string $path
3053
+ */
3054
+ public function addParsedFile($path)
3055
+ {
3056
+ if (isset($path) && file_exists($path)) {
3057
+ $this->parsedFiles[realpath($path)] = filemtime($path);
3058
+ }
3059
+ }
3060
+
3061
+ /**
3062
+ * Returns list of parsed files
3063
+ *
3064
+ * @api
3065
+ *
3066
+ * @return array
3067
+ */
3068
+ public function getParsedFiles()
3069
+ {
3070
+ return $this->parsedFiles;
3071
+ }
3072
+
3073
+ /**
3074
+ * Add import path
3075
+ *
3076
+ * @api
3077
+ *
3078
+ * @param string $path
3079
+ */
3080
+ public function addImportPath($path)
3081
+ {
3082
+ if (! in_array($path, $this->importPaths)) {
3083
+ $this->importPaths[] = $path;
3084
+ }
3085
+ }
3086
+
3087
+ /**
3088
+ * Set import paths
3089
+ *
3090
+ * @api
3091
+ *
3092
+ * @param string|array $path
3093
+ */
3094
+ public function setImportPaths($path)
3095
+ {
3096
+ $this->importPaths = (array) $path;
3097
+ }
3098
+
3099
+ /**
3100
+ * Set number precision
3101
+ *
3102
+ * @api
3103
+ *
3104
+ * @param integer $numberPrecision
3105
+ */
3106
+ public function setNumberPrecision($numberPrecision)
3107
+ {
3108
+ Node\Number::$precision = $numberPrecision;
3109
+ }
3110
+
3111
+ /**
3112
+ * Set formatter
3113
+ *
3114
+ * @api
3115
+ *
3116
+ * @param string $formatterName
3117
+ */
3118
+ public function setFormatter($formatterName)
3119
+ {
3120
+ $this->formatter = $formatterName;
3121
+ }
3122
+
3123
+ /**
3124
+ * Set line number style
3125
+ *
3126
+ * @api
3127
+ *
3128
+ * @param string $lineNumberStyle
3129
+ */
3130
+ public function setLineNumberStyle($lineNumberStyle)
3131
+ {
3132
+ $this->lineNumberStyle = $lineNumberStyle;
3133
+ }
3134
+
3135
+ /**
3136
+ * Register function
3137
+ *
3138
+ * @api
3139
+ *
3140
+ * @param string $name
3141
+ * @param callable $func
3142
+ * @param array $prototype
3143
+ */
3144
+ public function registerFunction($name, $func, $prototype = null)
3145
+ {
3146
+ $this->userFunctions[$this->normalizeName($name)] = [$func, $prototype];
3147
+ }
3148
+
3149
+ /**
3150
+ * Unregister function
3151
+ *
3152
+ * @api
3153
+ *
3154
+ * @param string $name
3155
+ */
3156
+ public function unregisterFunction($name)
3157
+ {
3158
+ unset($this->userFunctions[$this->normalizeName($name)]);
3159
+ }
3160
+
3161
+ /**
3162
+ * Add feature
3163
+ *
3164
+ * @api
3165
+ *
3166
+ * @param string $name
3167
+ */
3168
+ public function addFeature($name)
3169
+ {
3170
+ $this->registeredFeatures[$name] = true;
3171
+ }
3172
+
3173
+ /**
3174
+ * Import file
3175
+ *
3176
+ * @param string $path
3177
+ * @param array $out
3178
+ */
3179
+ protected function importFile($path, $out)
3180
+ {
3181
+ // see if tree is cached
3182
+ $realPath = realpath($path);
3183
+
3184
+ if (isset($this->importCache[$realPath])) {
3185
+ $this->handleImportLoop($realPath);
3186
+
3187
+ $tree = $this->importCache[$realPath];
3188
+ } else {
3189
+ $code = file_get_contents($path);
3190
+ $parser = $this->parserFactory($path);
3191
+ $tree = $parser->parse($code);
3192
+
3193
+ $this->importCache[$realPath] = $tree;
3194
+ }
3195
+
3196
+ $pi = pathinfo($path);
3197
+ array_unshift($this->importPaths, $pi['dirname']);
3198
+ $this->compileChildrenNoReturn($tree->children, $out);
3199
+ array_shift($this->importPaths);
3200
+ }
3201
+
3202
+ /**
3203
+ * Return the file path for an import url if it exists
3204
+ *
3205
+ * @api
3206
+ *
3207
+ * @param string $url
3208
+ *
3209
+ * @return string|null
3210
+ */
3211
+ public function findImport($url)
3212
+ {
3213
+ $urls = [];
3214
+
3215
+ // for "normal" scss imports (ignore vanilla css and external requests)
3216
+ if (! preg_match('/\.css$|^https?:\/\//', $url)) {
3217
+ // try both normal and the _partial filename
3218
+ $urls = [$url, preg_replace('/[^\/]+$/', '_\0', $url)];
3219
+ }
3220
+
3221
+ foreach ($this->importPaths as $dir) {
3222
+ if (is_string($dir)) {
3223
+ // check urls for normal import paths
3224
+ foreach ($urls as $full) {
3225
+ $full = $dir
3226
+ . (! empty($dir) && substr($dir, -1) !== '/' ? '/' : '')
3227
+ . $full;
3228
+
3229
+ if ($this->fileExists($file = $full . '.scss') ||
3230
+ $this->fileExists($file = $full)
3231
+ ) {
3232
+ return $file;
3233
+ }
3234
+ }
3235
+ } elseif (is_callable($dir)) {
3236
+ // check custom callback for import path
3237
+ $file = call_user_func($dir, $url);
3238
+
3239
+ if ($file !== null) {
3240
+ return $file;
3241
+ }
3242
+ }
3243
+ }
3244
+
3245
+ return null;
3246
+ }
3247
+
3248
+ /**
3249
+ * Set encoding
3250
+ *
3251
+ * @api
3252
+ *
3253
+ * @param string $encoding
3254
+ */
3255
+ public function setEncoding($encoding)
3256
+ {
3257
+ $this->encoding = $encoding;
3258
+ }
3259
+
3260
+ /**
3261
+ * Ignore errors?
3262
+ *
3263
+ * @api
3264
+ *
3265
+ * @param boolean $ignoreErrors
3266
+ *
3267
+ * @return \Leafo\ScssPhp\Compiler
3268
+ */
3269
+ public function setIgnoreErrors($ignoreErrors)
3270
+ {
3271
+ $this->ignoreErrors = $ignoreErrors;
3272
+ }
3273
+
3274
+ /**
3275
+ * Throw error (exception)
3276
+ *
3277
+ * @api
3278
+ *
3279
+ * @param string $msg Message with optional sprintf()-style vararg parameters
3280
+ *
3281
+ * @throws \Leafo\ScssPhp\Exception\CompilerException
3282
+ */
3283
+ public function throwError($msg)
3284
+ {
3285
+ if ($this->ignoreErrors) {
3286
+ return;
3287
+ }
3288
+
3289
+ if (func_num_args() > 1) {
3290
+ $msg = call_user_func_array('sprintf', func_get_args());
3291
+ }
3292
+
3293
+ $line = $this->sourceLine;
3294
+ $msg = "$msg: line: $line";
3295
+
3296
+ throw new CompilerException($msg);
3297
+ }
3298
+
3299
+ /**
3300
+ * Handle import loop
3301
+ *
3302
+ * @param string $name
3303
+ *
3304
+ * @throws \Exception
3305
+ */
3306
+ private function handleImportLoop($name)
3307
+ {
3308
+ for ($env = $this->env; $env; $env = $env->parent) {
3309
+ $file = $this->sourceNames[$env->block->sourceIndex];
3310
+
3311
+ if (realpath($file) === $name) {
3312
+ $this->throwError('An @import loop has been found: %s imports %s', $file, basename($file));
3313
+ break;
3314
+ }
3315
+ }
3316
+ }
3317
+
3318
+ /**
3319
+ * Does file exist?
3320
+ *
3321
+ * @param string $name
3322
+ *
3323
+ * @return boolean
3324
+ */
3325
+ protected function fileExists($name)
3326
+ {
3327
+ return is_file($name);
3328
+ }
3329
+
3330
+ /**
3331
+ * Call SCSS @function
3332
+ *
3333
+ * @param string $name
3334
+ * @param array $args
3335
+ * @param array $returnValue
3336
+ *
3337
+ * @return boolean Returns true if returnValue is set; otherwise, false
3338
+ */
3339
+ protected function callScssFunction($name, $argValues, &$returnValue)
3340
+ {
3341
+ $func = $this->get(self::$namespaces['function'] . $name, false);
3342
+
3343
+ if (! $func) {
3344
+ return false;
3345
+ }
3346
+
3347
+ $this->pushEnv();
3348
+
3349
+ // set the args
3350
+ if (isset($func->args)) {
3351
+ $this->applyArguments($func->args, $argValues);
3352
+ }
3353
+
3354
+ // throw away lines and children
3355
+ $tmp = new OutputBlock;
3356
+ $tmp->lines = [];
3357
+ $tmp->children = [];
3358
+
3359
+ $this->env->marker = 'function';
3360
+
3361
+ $ret = $this->compileChildren($func->children, $tmp);
3362
+
3363
+ $this->popEnv();
3364
+
3365
+ $returnValue = ! isset($ret) ? self::$defaultValue : $ret;
3366
+
3367
+ return true;
3368
+ }
3369
+
3370
+ /**
3371
+ * Call built-in and registered (PHP) functions
3372
+ *
3373
+ * @param string $name
3374
+ * @param array $args
3375
+ * @param array $returnValue
3376
+ *
3377
+ * @return boolean Returns true if returnValue is set; otherwise, false
3378
+ */
3379
+ protected function callNativeFunction($name, $args, &$returnValue)
3380
+ {
3381
+ // try a lib function
3382
+ $name = $this->normalizeName($name);
3383
+
3384
+ if (isset($this->userFunctions[$name])) {
3385
+ // see if we can find a user function
3386
+ list($f, $prototype) = $this->userFunctions[$name];
3387
+ } elseif (($f = $this->getBuiltinFunction($name)) && is_callable($f)) {
3388
+ $libName = $f[1];
3389
+ $prototype = isset(self::$$libName) ? self::$$libName : null;
3390
+ } else {
3391
+ return false;
3392
+ }
3393
+
3394
+ list($sorted, $kwargs) = $this->sortArgs($prototype, $args);
3395
+
3396
+ if ($name !== 'if' && $name !== 'call') {
3397
+ foreach ($sorted as &$val) {
3398
+ $val = $this->reduce($val, true);
3399
+ }
3400
+ }
3401
+
3402
+ $returnValue = call_user_func($f, $sorted, $kwargs);
3403
+
3404
+ if (! isset($returnValue)) {
3405
+ return false;
3406
+ }
3407
+
3408
+ $returnValue = $this->coerceValue($returnValue);
3409
+
3410
+ return true;
3411
+ }
3412
+
3413
+ /**
3414
+ * Get built-in function
3415
+ *
3416
+ * @param string $name Normalized name
3417
+ *
3418
+ * @return array
3419
+ */
3420
+ protected function getBuiltinFunction($name)
3421
+ {
3422
+ $libName = 'lib' . preg_replace_callback(
3423
+ '/_(.)/',
3424
+ function ($m) {
3425
+ return ucfirst($m[1]);
3426
+ },
3427
+ ucfirst($name)
3428
+ );
3429
+
3430
+ return [$this, $libName];
3431
+ }
3432
+
3433
+ /**
3434
+ * Sorts keyword arguments
3435
+ *
3436
+ * @param array $prototype
3437
+ * @param array $args
3438
+ *
3439
+ * @return array
3440
+ */
3441
+ protected function sortArgs($prototype, $args)
3442
+ {
3443
+ $keyArgs = [];
3444
+ $posArgs = [];
3445
+
3446
+ // separate positional and keyword arguments
3447
+ foreach ($args as $arg) {
3448
+ list($key, $value) = $arg;
3449
+
3450
+ $key = $key[1];
3451
+
3452
+ if (empty($key)) {
3453
+ $posArgs[] = $value;
3454
+ } else {
3455
+ $keyArgs[$key] = $value;
3456
+ }
3457
+ }
3458
+
3459
+ if (! isset($prototype)) {
3460
+ return [$posArgs, $keyArgs];
3461
+ }
3462
+
3463
+ // copy positional args
3464
+ $finalArgs = array_pad($posArgs, count($prototype), null);
3465
+
3466
+ // overwrite positional args with keyword args
3467
+ foreach ($prototype as $i => $names) {
3468
+ foreach ((array) $names as $name) {
3469
+ if (isset($keyArgs[$name])) {
3470
+ $finalArgs[$i] = $keyArgs[$name];
3471
+ }
3472
+ }
3473
+ }
3474
+
3475
+ return [$finalArgs, $keyArgs];
3476
+ }
3477
+
3478
+ /**
3479
+ * Apply argument values per definition
3480
+ *
3481
+ * @param array $argDef
3482
+ * @param array $argValues
3483
+ *
3484
+ * @throws \Exception
3485
+ */
3486
+ protected function applyArguments($argDef, $argValues)
3487
+ {
3488
+ $storeEnv = $this->getStoreEnv();
3489
+
3490
+ $env = new Environment;
3491
+ $env->store = $storeEnv->store;
3492
+
3493
+ $hasVariable = false;
3494
+ $args = [];
3495
+
3496
+ foreach ($argDef as $i => $arg) {
3497
+ list($name, $default, $isVariable) = $argDef[$i];
3498
+
3499
+ $args[$name] = [$i, $name, $default, $isVariable];
3500
+ $hasVariable |= $isVariable;
3501
+ }
3502
+
3503
+ $keywordArgs = [];
3504
+ $deferredKeywordArgs = [];
3505
+ $remaining = [];
3506
+
3507
+ // assign the keyword args
3508
+ foreach ((array) $argValues as $arg) {
3509
+ if (! empty($arg[0])) {
3510
+ if (! isset($args[$arg[0][1]])) {
3511
+ if ($hasVariable) {
3512
+ $deferredKeywordArgs[$arg[0][1]] = $arg[1];
3513
+ } else {
3514
+ $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
3515
+ break;
3516
+ }
3517
+ } elseif ($args[$arg[0][1]][0] < count($remaining)) {
3518
+ $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
3519
+ break;
3520
+ } else {
3521
+ $keywordArgs[$arg[0][1]] = $arg[1];
3522
+ }
3523
+ } elseif (count($keywordArgs)) {
3524
+ $this->throwError('Positional arguments must come before keyword arguments.');
3525
+ break;
3526
+ } elseif ($arg[2] === true) {
3527
+ $val = $this->reduce($arg[1], true);
3528
+
3529
+ if ($val[0] === Type::T_LIST) {
3530
+ foreach ($val[2] as $name => $item) {
3531
+ if (! is_numeric($name)) {
3532
+ $keywordArgs[$name] = $item;
3533
+ } else {
3534
+ $remaining[] = $item;
3535
+ }
3536
+ }
3537
+ } elseif ($val[0] === Type::T_MAP) {
3538
+ foreach ($val[1] as $i => $name) {
3539
+ $name = $this->compileStringContent($this->coerceString($name));
3540
+ $item = $val[2][$i];
3541
+
3542
+ if (! is_numeric($name)) {
3543
+ $keywordArgs[$name] = $item;
3544
+ } else {
3545
+ $remaining[] = $item;
3546
+ }
3547
+ }
3548
+ } else {
3549
+ $remaining[] = $val;
3550
+ }
3551
+ } else {
3552
+ $remaining[] = $arg[1];
3553
+ }
3554
+ }
3555
+
3556
+ foreach ($args as $arg) {
3557
+ list($i, $name, $default, $isVariable) = $arg;
3558
+
3559
+ if ($isVariable) {
3560
+ $val = [Type::T_LIST, ',', [], $isVariable];
3561
+
3562
+ for ($count = count($remaining); $i < $count; $i++) {
3563
+ $val[2][] = $remaining[$i];
3564
+ }
3565
+
3566
+ foreach ($deferredKeywordArgs as $itemName => $item) {
3567
+ $val[2][$itemName] = $item;
3568
+ }
3569
+ } elseif (isset($remaining[$i])) {
3570
+ $val = $remaining[$i];
3571
+ } elseif (isset($keywordArgs[$name])) {
3572
+ $val = $keywordArgs[$name];
3573
+ } elseif (! empty($default)) {
3574
+ continue;
3575
+ } else {
3576
+ $this->throwError("Missing argument $name");
3577
+ break;
3578
+ }
3579
+
3580
+ $this->set($name, $this->reduce($val, true), true, $env);
3581
+ }
3582
+
3583
+ $storeEnv->store = $env->store;
3584
+
3585
+ foreach ($args as $arg) {
3586
+ list($i, $name, $default, $isVariable) = $arg;
3587
+
3588
+ if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) {
3589
+ continue;
3590
+ }
3591
+
3592
+ $this->set($name, $this->reduce($default, true), true);
3593
+ }
3594
+ }
3595
+
3596
+ /**
3597
+ * Coerce a php value into a scss one
3598
+ *
3599
+ * @param mixed $value
3600
+ *
3601
+ * @return array
3602
+ */
3603
+ private function coerceValue($value)
3604
+ {
3605
+ if (is_array($value) || $value instanceof \ArrayAccess) {
3606
+ return $value;
3607
+ }
3608
+
3609
+ if (is_bool($value)) {
3610
+ return $this->toBool($value);
3611
+ }
3612
+
3613
+ if ($value === null) {
3614
+ $value = self::$null;
3615
+ }
3616
+
3617
+ if (is_numeric($value)) {
3618
+ return new Node\Number($value, '');
3619
+ }
3620
+
3621
+ if ($value === '') {
3622
+ return self::$emptyString;
3623
+ }
3624
+
3625
+ return [Type::T_KEYWORD, $value];
3626
+ }
3627
+
3628
+ /**
3629
+ * Coerce something to map
3630
+ *
3631
+ * @param array $item
3632
+ *
3633
+ * @return array
3634
+ */
3635
+ protected function coerceMap($item)
3636
+ {
3637
+ if ($item[0] === Type::T_MAP) {
3638
+ return $item;
3639
+ }
3640
+
3641
+ if ($item === self::$emptyList) {
3642
+ return self::$emptyMap;
3643
+ }
3644
+
3645
+ return [Type::T_MAP, [$item], [self::$null]];
3646
+ }
3647
+
3648
+ /**
3649
+ * Coerce something to list
3650
+ *
3651
+ * @param array $item
3652
+ *
3653
+ * @return array
3654
+ */
3655
+ protected function coerceList($item, $delim = ',')
3656
+ {
3657
+ if (isset($item) && $item[0] === Type::T_LIST) {
3658
+ return $item;
3659
+ }
3660
+
3661
+ if (isset($item) && $item[0] === Type::T_MAP) {
3662
+ $keys = $item[1];
3663
+ $values = $item[2];
3664
+ $list = [];
3665
+
3666
+ for ($i = 0, $s = count($keys); $i < $s; $i++) {
3667
+ $key = $keys[$i];
3668
+ $value = $values[$i];
3669
+
3670
+ $list[] = [
3671
+ Type::T_LIST,
3672
+ '',
3673
+ [[Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))], $value]
3674
+ ];
3675
+ }
3676
+
3677
+ return [Type::T_LIST, ',', $list];
3678
+ }
3679
+
3680
+ return [Type::T_LIST, $delim, ! isset($item) ? []: [$item]];
3681
+ }
3682
+
3683
+ /**
3684
+ * Coerce color for expression
3685
+ *
3686
+ * @param array $value
3687
+ *
3688
+ * @return array|null
3689
+ */
3690
+ protected function coerceForExpression($value)
3691
+ {
3692
+ if ($color = $this->coerceColor($value)) {
3693
+ return $color;
3694
+ }
3695
+
3696
+ return $value;
3697
+ }
3698
+
3699
+ /**
3700
+ * Coerce value to color
3701
+ *
3702
+ * @param array $value
3703
+ *
3704
+ * @return array|null
3705
+ */
3706
+ protected function coerceColor($value)
3707
+ {
3708
+ switch ($value[0]) {
3709
+ case Type::T_COLOR:
3710
+ return $value;
3711
+
3712
+ case Type::T_KEYWORD:
3713
+ $name = strtolower($value[1]);
3714
+
3715
+ if (isset(Colors::$cssColors[$name])) {
3716
+ $rgba = explode(',', Colors::$cssColors[$name]);
3717
+
3718
+ return isset($rgba[3])
3719
+ ? [Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3]]
3720
+ : [Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]];
3721
+ }
3722
+
3723
+ return null;
3724
+ }
3725
+
3726
+ return null;
3727
+ }
3728
+
3729
+ /**
3730
+ * Coerce value to string
3731
+ *
3732
+ * @param array $value
3733
+ *
3734
+ * @return array|null
3735
+ */
3736
+ protected function coerceString($value)
3737
+ {
3738
+ if ($value[0] === Type::T_STRING) {
3739
+ return $value;
3740
+ }
3741
+
3742
+ return [Type::T_STRING, '', [$this->compileValue($value)]];
3743
+ }
3744
+
3745
+ /**
3746
+ * Coerce value to a percentage
3747
+ *
3748
+ * @param array $value
3749
+ *
3750
+ * @return integer|float
3751
+ */
3752
+ protected function coercePercent($value)
3753
+ {
3754
+ if ($value[0] === Type::T_NUMBER) {
3755
+ if (! empty($value[2]['%'])) {
3756
+ return $value[1] / 100;
3757
+ }
3758
+
3759
+ return $value[1];
3760
+ }
3761
+
3762
+ return 0;
3763
+ }
3764
+
3765
+ /**
3766
+ * Assert value is a map
3767
+ *
3768
+ * @api
3769
+ *
3770
+ * @param array $value
3771
+ *
3772
+ * @return array
3773
+ *
3774
+ * @throws \Exception
3775
+ */
3776
+ public function assertMap($value)
3777
+ {
3778
+ $value = $this->coerceMap($value);
3779
+
3780
+ if ($value[0] !== Type::T_MAP) {
3781
+ $this->throwError('expecting map');
3782
+ }
3783
+
3784
+ return $value;
3785
+ }
3786
+
3787
+ /**
3788
+ * Assert value is a list
3789
+ *
3790
+ * @api
3791
+ *
3792
+ * @param array $value
3793
+ *
3794
+ * @return array
3795
+ *
3796
+ * @throws \Exception
3797
+ */
3798
+ public function assertList($value)
3799
+ {
3800
+ if ($value[0] !== Type::T_LIST) {
3801
+ $this->throwError('expecting list');
3802
+ }
3803
+
3804
+ return $value;
3805
+ }
3806
+
3807
+ /**
3808
+ * Assert value is a color
3809
+ *
3810
+ * @api
3811
+ *
3812
+ * @param array $value
3813
+ *
3814
+ * @return array
3815
+ *
3816
+ * @throws \Exception
3817
+ */
3818
+ public function assertColor($value)
3819
+ {
3820
+ if ($color = $this->coerceColor($value)) {
3821
+ return $color;
3822
+ }
3823
+
3824
+ $this->throwError('expecting color');
3825
+ }
3826
+
3827
+ /**
3828
+ * Assert value is a number
3829
+ *
3830
+ * @api
3831
+ *
3832
+ * @param array $value
3833
+ *
3834
+ * @return integer|float
3835
+ *
3836
+ * @throws \Exception
3837
+ */
3838
+ public function assertNumber($value)
3839
+ {
3840
+ if ($value[0] !== Type::T_NUMBER) {
3841
+ $this->throwError('expecting number');
3842
+ }
3843
+
3844
+ return $value[1];
3845
+ }
3846
+
3847
+ /**
3848
+ * Make sure a color's components don't go out of bounds
3849
+ *
3850
+ * @param array $c
3851
+ *
3852
+ * @return array
3853
+ */
3854
+ protected function fixColor($c)
3855
+ {
3856
+ foreach ([1, 2, 3] as $i) {
3857
+ if ($c[$i] < 0) {
3858
+ $c[$i] = 0;
3859
+ }
3860
+
3861
+ if ($c[$i] > 255) {
3862
+ $c[$i] = 255;
3863
+ }
3864
+ }
3865
+
3866
+ return $c;
3867
+ }
3868
+
3869
+ /**
3870
+ * Convert RGB to HSL
3871
+ *
3872
+ * @api
3873
+ *
3874
+ * @param integer $red
3875
+ * @param integer $green
3876
+ * @param integer $blue
3877
+ *
3878
+ * @return array
3879
+ */
3880
+ public function toHSL($red, $green, $blue)
3881
+ {
3882
+ $min = min($red, $green, $blue);
3883
+ $max = max($red, $green, $blue);
3884
+
3885
+ $l = $min + $max;
3886
+ $d = $max - $min;
3887
+
3888
+ if ((int) $d === 0) {
3889
+ $h = $s = 0;
3890
+ } else {
3891
+ if ($l < 255) {
3892
+ $s = $d / $l;
3893
+ } else {
3894
+ $s = $d / (510 - $l);
3895
+ }
3896
+
3897
+ if ($red == $max) {
3898
+ $h = 60 * ($green - $blue) / $d;
3899
+ } elseif ($green == $max) {
3900
+ $h = 60 * ($blue - $red) / $d + 120;
3901
+ } elseif ($blue == $max) {
3902
+ $h = 60 * ($red - $green) / $d + 240;
3903
+ }
3904
+ }
3905
+
3906
+ return [Type::T_HSL, fmod($h, 360), $s * 100, $l / 5.1];
3907
+ }
3908
+
3909
+ /**
3910
+ * Hue to RGB helper
3911
+ *
3912
+ * @param float $m1
3913
+ * @param float $m2
3914
+ * @param float $h
3915
+ *
3916
+ * @return float
3917
+ */
3918
+ private function hueToRGB($m1, $m2, $h)
3919
+ {
3920
+ if ($h < 0) {
3921
+ $h += 1;
3922
+ } elseif ($h > 1) {
3923
+ $h -= 1;
3924
+ }
3925
+
3926
+ if ($h * 6 < 1) {
3927
+ return $m1 + ($m2 - $m1) * $h * 6;
3928
+ }
3929
+
3930
+ if ($h * 2 < 1) {
3931
+ return $m2;
3932
+ }
3933
+
3934
+ if ($h * 3 < 2) {
3935
+ return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
3936
+ }
3937
+
3938
+ return $m1;
3939
+ }
3940
+
3941
+ /**
3942
+ * Convert HSL to RGB
3943
+ *
3944
+ * @api
3945
+ *
3946
+ * @param integer $hue H from 0 to 360
3947
+ * @param integer $saturation S from 0 to 100
3948
+ * @param integer $lightness L from 0 to 100
3949
+ *
3950
+ * @return array
3951
+ */
3952
+ public function toRGB($hue, $saturation, $lightness)
3953
+ {
3954
+ if ($hue < 0) {
3955
+ $hue += 360;
3956
+ }
3957
+
3958
+ $h = $hue / 360;
3959
+ $s = min(100, max(0, $saturation)) / 100;
3960
+ $l = min(100, max(0, $lightness)) / 100;
3961
+
3962
+ $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
3963
+ $m1 = $l * 2 - $m2;
3964
+
3965
+ $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
3966
+ $g = $this->hueToRGB($m1, $m2, $h) * 255;
3967
+ $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
3968
+
3969
+ $out = [Type::T_COLOR, $r, $g, $b];
3970
+
3971
+ return $out;
3972
+ }
3973
+
3974
+ // Built in functions
3975
+
3976
+ //protected static $libCall = ['name', 'args...'];
3977
+ protected function libCall($args, $kwargs)
3978
+ {
3979
+ $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
3980
+
3981
+ $args = array_map(
3982
+ function ($a) {
3983
+ return [null, $a, false];
3984
+ },
3985
+ $args
3986
+ );
3987
+
3988
+ if (count($kwargs)) {
3989
+ foreach ($kwargs as $key => $value) {
3990
+ $args[] = [[Type::T_VARIABLE, $key], $value, false];
3991
+ }
3992
+ }
3993
+
3994
+ return $this->reduce([Type::T_FUNCTION_CALL, $name, $args]);
3995
+ }
3996
+
3997
+ protected static $libIf = ['condition', 'if-true', 'if-false'];
3998
+ protected function libIf($args)
3999
+ {
4000
+ list($cond, $t, $f) = $args;
4001
+
4002
+ if (! $this->isTruthy($this->reduce($cond, true))) {
4003
+ return $this->reduce($f, true);
4004
+ }
4005
+
4006
+ return $this->reduce($t, true);
4007
+ }
4008
+
4009
+ protected static $libIndex = ['list', 'value'];
4010
+ protected function libIndex($args)
4011
+ {
4012
+ list($list, $value) = $args;
4013
+
4014
+ if ($value[0] === Type::T_MAP) {
4015
+ return self::$null;
4016
+ }
4017
+
4018
+ if ($list[0] === Type::T_MAP ||
4019
+ $list[0] === Type::T_STRING ||
4020
+ $list[0] === Type::T_KEYWORD ||
4021
+ $list[0] === Type::T_INTERPOLATE
4022
+ ) {
4023
+ $list = $this->coerceList($list, ' ');
4024
+ }
4025
+
4026
+ if ($list[0] !== Type::T_LIST) {
4027
+ return self::$null;
4028
+ }
4029
+
4030
+ $values = [];
4031
+
4032
+ foreach ($list[2] as $item) {
4033
+ $values[] = $this->normalizeValue($item);
4034
+ }
4035
+
4036
+ $key = array_search($this->normalizeValue($value), $values);
4037
+
4038
+ return false === $key ? self::$null : $key + 1;
4039
+ }
4040
+
4041
+ protected static $libRgb = ['red', 'green', 'blue'];
4042
+ protected function libRgb($args)
4043
+ {
4044
+ list($r, $g, $b) = $args;
4045
+
4046
+ return [Type::T_COLOR, $r[1], $g[1], $b[1]];
4047
+ }
4048
+
4049
+ protected static $libRgba = [
4050
+ ['red', 'color'],
4051
+ 'green', 'blue', 'alpha'];
4052
+ protected function libRgba($args)
4053
+ {
4054
+ if ($color = $this->coerceColor($args[0])) {
4055
+ $num = ! isset($args[1]) ? $args[3] : $args[1];
4056
+ $alpha = $this->assertNumber($num);
4057
+ $color[4] = $alpha;
4058
+
4059
+ return $color;
4060
+ }
4061
+
4062
+ list($r, $g, $b, $a) = $args;
4063
+
4064
+ return [Type::T_COLOR, $r[1], $g[1], $b[1], $a[1]];
4065
+ }
4066
+
4067
+ // helper function for adjust_color, change_color, and scale_color
4068
+ protected function alterColor($args, $fn)
4069
+ {
4070
+ $color = $this->assertColor($args[0]);
4071
+
4072
+ foreach ([1, 2, 3, 7] as $i) {
4073
+ if (isset($args[$i])) {
4074
+ $val = $this->assertNumber($args[$i]);
4075
+ $ii = $i === 7 ? 4 : $i; // alpha
4076
+ $color[$ii] = call_user_func($fn, isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
4077
+ }
4078
+ }
4079
+
4080
+ if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
4081
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4082
+
4083
+ foreach ([4, 5, 6] as $i) {
4084
+ if (isset($args[$i])) {
4085
+ $val = $this->assertNumber($args[$i]);
4086
+ $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
4087
+ }
4088
+ }
4089
+
4090
+ $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
4091
+
4092
+ if (isset($color[4])) {
4093
+ $rgb[4] = $color[4];
4094
+ }
4095
+
4096
+ $color = $rgb;
4097
+ }
4098
+
4099
+ return $color;
4100
+ }
4101
+
4102
+ protected static $libAdjustColor = [
4103
+ 'color', 'red', 'green', 'blue',
4104
+ 'hue', 'saturation', 'lightness', 'alpha'
4105
+ ];
4106
+ protected function libAdjustColor($args)
4107
+ {
4108
+ return $this->alterColor($args, function ($base, $alter, $i) {
4109
+ return $base + $alter;
4110
+ });
4111
+ }
4112
+
4113
+ protected static $libChangeColor = [
4114
+ 'color', 'red', 'green', 'blue',
4115
+ 'hue', 'saturation', 'lightness', 'alpha'
4116
+ ];
4117
+ protected function libChangeColor($args)
4118
+ {
4119
+ return $this->alterColor($args, function ($base, $alter, $i) {
4120
+ return $alter;
4121
+ });
4122
+ }
4123
+
4124
+ protected static $libScaleColor = [
4125
+ 'color', 'red', 'green', 'blue',
4126
+ 'hue', 'saturation', 'lightness', 'alpha'
4127
+ ];
4128
+ protected function libScaleColor($args)
4129
+ {
4130
+ return $this->alterColor($args, function ($base, $scale, $i) {
4131
+ // 1, 2, 3 - rgb
4132
+ // 4, 5, 6 - hsl
4133
+ // 7 - a
4134
+ switch ($i) {
4135
+ case 1:
4136
+ case 2:
4137
+ case 3:
4138
+ $max = 255;
4139
+ break;
4140
+
4141
+ case 4:
4142
+ $max = 360;
4143
+ break;
4144
+
4145
+ case 7:
4146
+ $max = 1;
4147
+ break;
4148
+
4149
+ default:
4150
+ $max = 100;
4151
+ }
4152
+
4153
+ $scale = $scale / 100;
4154
+
4155
+ if ($scale < 0) {
4156
+ return $base * $scale + $base;
4157
+ }
4158
+
4159
+ return ($max - $base) * $scale + $base;
4160
+ });
4161
+ }
4162
+
4163
+ protected static $libIeHexStr = ['color'];
4164
+ protected function libIeHexStr($args)
4165
+ {
4166
+ $color = $this->coerceColor($args[0]);
4167
+ $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
4168
+
4169
+ return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
4170
+ }
4171
+
4172
+ protected static $libRed = ['color'];
4173
+ protected function libRed($args)
4174
+ {
4175
+ $color = $this->coerceColor($args[0]);
4176
+
4177
+ return $color[1];
4178
+ }
4179
+
4180
+ protected static $libGreen = ['color'];
4181
+ protected function libGreen($args)
4182
+ {
4183
+ $color = $this->coerceColor($args[0]);
4184
+
4185
+ return $color[2];
4186
+ }
4187
+
4188
+ protected static $libBlue = ['color'];
4189
+ protected function libBlue($args)
4190
+ {
4191
+ $color = $this->coerceColor($args[0]);
4192
+
4193
+ return $color[3];
4194
+ }
4195
+
4196
+ protected static $libAlpha = ['color'];
4197
+ protected function libAlpha($args)
4198
+ {
4199
+ if ($color = $this->coerceColor($args[0])) {
4200
+ return isset($color[4]) ? $color[4] : 1;
4201
+ }
4202
+
4203
+ // this might be the IE function, so return value unchanged
4204
+ return null;
4205
+ }
4206
+
4207
+ protected static $libOpacity = ['color'];
4208
+ protected function libOpacity($args)
4209
+ {
4210
+ $value = $args[0];
4211
+
4212
+ if ($value[0] === Type::T_NUMBER) {
4213
+ return null;
4214
+ }
4215
+
4216
+ return $this->libAlpha($args);
4217
+ }
4218
+
4219
+ // mix two colors
4220
+ protected static $libMix = ['color-1', 'color-2', 'weight'];
4221
+ protected function libMix($args)
4222
+ {
4223
+ list($first, $second, $weight) = $args;
4224
+
4225
+ $first = $this->assertColor($first);
4226
+ $second = $this->assertColor($second);
4227
+
4228
+ if (! isset($weight)) {
4229
+ $weight = 0.5;
4230
+ } else {
4231
+ $weight = $this->coercePercent($weight);
4232
+ }
4233
+
4234
+ $firstAlpha = isset($first[4]) ? $first[4] : 1;
4235
+ $secondAlpha = isset($second[4]) ? $second[4] : 1;
4236
+
4237
+ $w = $weight * 2 - 1;
4238
+ $a = $firstAlpha - $secondAlpha;
4239
+
4240
+ $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
4241
+ $w2 = 1.0 - $w1;
4242
+
4243
+ $new = [Type::T_COLOR,
4244
+ $w1 * $first[1] + $w2 * $second[1],
4245
+ $w1 * $first[2] + $w2 * $second[2],
4246
+ $w1 * $first[3] + $w2 * $second[3],
4247
+ ];
4248
+
4249
+ if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
4250
+ $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1);
4251
+ }
4252
+
4253
+ return $this->fixColor($new);
4254
+ }
4255
+
4256
+ protected static $libHsl = ['hue', 'saturation', 'lightness'];
4257
+ protected function libHsl($args)
4258
+ {
4259
+ list($h, $s, $l) = $args;
4260
+
4261
+ return $this->toRGB($h[1], $s[1], $l[1]);
4262
+ }
4263
+
4264
+ protected static $libHsla = ['hue', 'saturation', 'lightness', 'alpha'];
4265
+ protected function libHsla($args)
4266
+ {
4267
+ list($h, $s, $l, $a) = $args;
4268
+
4269
+ $color = $this->toRGB($h[1], $s[1], $l[1]);
4270
+ $color[4] = $a[1];
4271
+
4272
+ return $color;
4273
+ }
4274
+
4275
+ protected static $libHue = ['color'];
4276
+ protected function libHue($args)
4277
+ {
4278
+ $color = $this->assertColor($args[0]);
4279
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4280
+
4281
+ return new Node\Number($hsl[1], 'deg');
4282
+ }
4283
+
4284
+ protected static $libSaturation = ['color'];
4285
+ protected function libSaturation($args)
4286
+ {
4287
+ $color = $this->assertColor($args[0]);
4288
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4289
+
4290
+ return new Node\Number($hsl[2], '%');
4291
+ }
4292
+
4293
+ protected static $libLightness = ['color'];
4294
+ protected function libLightness($args)
4295
+ {
4296
+ $color = $this->assertColor($args[0]);
4297
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4298
+
4299
+ return new Node\Number($hsl[3], '%');
4300
+ }
4301
+
4302
+ protected function adjustHsl($color, $idx, $amount)
4303
+ {
4304
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4305
+ $hsl[$idx] += $amount;
4306
+ $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
4307
+
4308
+ if (isset($color[4])) {
4309
+ $out[4] = $color[4];
4310
+ }
4311
+
4312
+ return $out;
4313
+ }
4314
+
4315
+ protected static $libAdjustHue = ['color', 'degrees'];
4316
+ protected function libAdjustHue($args)
4317
+ {
4318
+ $color = $this->assertColor($args[0]);
4319
+ $degrees = $this->assertNumber($args[1]);
4320
+
4321
+ return $this->adjustHsl($color, 1, $degrees);
4322
+ }
4323
+
4324
+ protected static $libLighten = ['color', 'amount'];
4325
+ protected function libLighten($args)
4326
+ {
4327
+ $color = $this->assertColor($args[0]);
4328
+ $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
4329
+
4330
+ return $this->adjustHsl($color, 3, $amount);
4331
+ }
4332
+
4333
+ protected static $libDarken = ['color', 'amount'];
4334
+ protected function libDarken($args)
4335
+ {
4336
+ $color = $this->assertColor($args[0]);
4337
+ $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
4338
+
4339
+ return $this->adjustHsl($color, 3, -$amount);
4340
+ }
4341
+
4342
+ protected static $libSaturate = ['color', 'amount'];
4343
+ protected function libSaturate($args)
4344
+ {
4345
+ $value = $args[0];
4346
+
4347
+ if ($value[0] === Type::T_NUMBER) {
4348
+ return null;
4349
+ }
4350
+
4351
+ $color = $this->assertColor($value);
4352
+ $amount = 100 * $this->coercePercent($args[1]);
4353
+
4354
+ return $this->adjustHsl($color, 2, $amount);
4355
+ }
4356
+
4357
+ protected static $libDesaturate = ['color', 'amount'];
4358
+ protected function libDesaturate($args)
4359
+ {
4360
+ $color = $this->assertColor($args[0]);
4361
+ $amount = 100 * $this->coercePercent($args[1]);
4362
+
4363
+ return $this->adjustHsl($color, 2, -$amount);
4364
+ }
4365
+
4366
+ protected static $libGrayscale = ['color'];
4367
+ protected function libGrayscale($args)
4368
+ {
4369
+ $value = $args[0];
4370
+
4371
+ if ($value[0] === Type::T_NUMBER) {
4372
+ return null;
4373
+ }
4374
+
4375
+ return $this->adjustHsl($this->assertColor($value), 2, -100);
4376
+ }
4377
+
4378
+ protected static $libComplement = ['color'];
4379
+ protected function libComplement($args)
4380
+ {
4381
+ return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
4382
+ }
4383
+
4384
+ protected static $libInvert = ['color'];
4385
+ protected function libInvert($args)
4386
+ {
4387
+ $value = $args[0];
4388
+
4389
+ if ($value[0] === Type::T_NUMBER) {
4390
+ return null;
4391
+ }
4392
+
4393
+ $color = $this->assertColor($value);
4394
+ $color[1] = 255 - $color[1];
4395
+ $color[2] = 255 - $color[2];
4396
+ $color[3] = 255 - $color[3];
4397
+
4398
+ return $color;
4399
+ }
4400
+
4401
+ // increases opacity by amount
4402
+ protected static $libOpacify = ['color', 'amount'];
4403
+ protected function libOpacify($args)
4404
+ {
4405
+ $color = $this->assertColor($args[0]);
4406
+ $amount = $this->coercePercent($args[1]);
4407
+
4408
+ $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
4409
+ $color[4] = min(1, max(0, $color[4]));
4410
+
4411
+ return $color;
4412
+ }
4413
+
4414
+ protected static $libFadeIn = ['color', 'amount'];
4415
+ protected function libFadeIn($args)
4416
+ {
4417
+ return $this->libOpacify($args);
4418
+ }
4419
+
4420
+ // decreases opacity by amount
4421
+ protected static $libTransparentize = ['color', 'amount'];
4422
+ protected function libTransparentize($args)
4423
+ {
4424
+ $color = $this->assertColor($args[0]);
4425
+ $amount = $this->coercePercent($args[1]);
4426
+
4427
+ $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
4428
+ $color[4] = min(1, max(0, $color[4]));
4429
+
4430
+ return $color;
4431
+ }
4432
+
4433
+ protected static $libFadeOut = ['color', 'amount'];
4434
+ protected function libFadeOut($args)
4435
+ {
4436
+ return $this->libTransparentize($args);
4437
+ }
4438
+
4439
+ protected static $libUnquote = ['string'];
4440
+ protected function libUnquote($args)
4441
+ {
4442
+ $str = $args[0];
4443
+
4444
+ if ($str[0] === Type::T_STRING) {
4445
+ $str[1] = '';
4446
+ }
4447
+
4448
+ return $str;
4449
+ }
4450
+
4451
+ protected static $libQuote = ['string'];
4452
+ protected function libQuote($args)
4453
+ {
4454
+ $value = $args[0];
4455
+
4456
+ if ($value[0] === Type::T_STRING && ! empty($value[1])) {
4457
+ return $value;
4458
+ }
4459
+
4460
+ return [Type::T_STRING, '"', [$value]];
4461
+ }
4462
+
4463
+ protected static $libPercentage = ['value'];
4464
+ protected function libPercentage($args)
4465
+ {
4466
+ return new Node\Number($this->coercePercent($args[0]) * 100, '%');
4467
+ }
4468
+
4469
+ protected static $libRound = ['value'];
4470
+ protected function libRound($args)
4471
+ {
4472
+ $num = $args[0];
4473
+ $num[1] = round($num[1]);
4474
+
4475
+ return $num;
4476
+ }
4477
+
4478
+ protected static $libFloor = ['value'];
4479
+ protected function libFloor($args)
4480
+ {
4481
+ $num = $args[0];
4482
+ $num[1] = floor($num[1]);
4483
+
4484
+ return $num;
4485
+ }
4486
+
4487
+ protected static $libCeil = ['value'];
4488
+ protected function libCeil($args)
4489
+ {
4490
+ $num = $args[0];
4491
+ $num[1] = ceil($num[1]);
4492
+
4493
+ return $num;
4494
+ }
4495
+
4496
+ protected static $libAbs = ['value'];
4497
+ protected function libAbs($args)
4498
+ {
4499
+ $num = $args[0];
4500
+ $num[1] = abs($num[1]);
4501
+
4502
+ return $num;
4503
+ }
4504
+
4505
+ protected function libMin($args)
4506
+ {
4507
+ $numbers = $this->getNormalizedNumbers($args);
4508
+ $min = null;
4509
+
4510
+ foreach ($numbers as $key => $number) {
4511
+ if (null === $min || $number[1] <= $min[1]) {
4512
+ $min = [$key, $number[1]];
4513
+ }
4514
+ }
4515
+
4516
+ return $args[$min[0]];
4517
+ }
4518
+
4519
+ protected function libMax($args)
4520
+ {
4521
+ $numbers = $this->getNormalizedNumbers($args);
4522
+ $max = null;
4523
+
4524
+ foreach ($numbers as $key => $number) {
4525
+ if (null === $max || $number[1] >= $max[1]) {
4526
+ $max = [$key, $number[1]];
4527
+ }
4528
+ }
4529
+
4530
+ return $args[$max[0]];
4531
+ }
4532
+
4533
+ /**
4534
+ * Helper to normalize args containing numbers
4535
+ *
4536
+ * @param array $args
4537
+ *
4538
+ * @return array
4539
+ */
4540
+ protected function getNormalizedNumbers($args)
4541
+ {
4542
+ $unit = null;
4543
+ $originalUnit = null;
4544
+ $numbers = [];
4545
+
4546
+ foreach ($args as $key => $item) {
4547
+ if ($item[0] !== Type::T_NUMBER) {
4548
+ $this->throwError('%s is not a number', $item[0]);
4549
+ break;
4550
+ }
4551
+
4552
+ $number = $item->normalize();
4553
+
4554
+ if (null === $unit) {
4555
+ $unit = $number[2];
4556
+ $originalUnit = $item->unitStr();
4557
+ } elseif ($unit !== $number[2]) {
4558
+ $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
4559
+ break;
4560
+ }
4561
+
4562
+ $numbers[$key] = $number;
4563
+ }
4564
+
4565
+ return $numbers;
4566
+ }
4567
+
4568
+ protected static $libLength = ['list'];
4569
+ protected function libLength($args)
4570
+ {
4571
+ $list = $this->coerceList($args[0]);
4572
+
4573
+ return count($list[2]);
4574
+ }
4575
+
4576
+ //protected static $libListSeparator = ['list...'];
4577
+ protected function libListSeparator($args)
4578
+ {
4579
+ if (count($args) > 1) {
4580
+ return 'comma';
4581
+ }
4582
+
4583
+ $list = $this->coerceList($args[0]);
4584
+
4585
+ if (count($list[2]) <= 1) {
4586
+ return 'space';
4587
+ }
4588
+
4589
+ if ($list[1] === ',') {
4590
+ return 'comma';
4591
+ }
4592
+
4593
+ return 'space';
4594
+ }
4595
+
4596
+ protected static $libNth = ['list', 'n'];
4597
+ protected function libNth($args)
4598
+ {
4599
+ $list = $this->coerceList($args[0]);
4600
+ $n = $this->assertNumber($args[1]);
4601
+
4602
+ if ($n > 0) {
4603
+ $n--;
4604
+ } elseif ($n < 0) {
4605
+ $n += count($list[2]);
4606
+ }
4607
+
4608
+ return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue;
4609
+ }
4610
+
4611
+ protected static $libSetNth = ['list', 'n', 'value'];
4612
+ protected function libSetNth($args)
4613
+ {
4614
+ $list = $this->coerceList($args[0]);
4615
+ $n = $this->assertNumber($args[1]);
4616
+
4617
+ if ($n > 0) {
4618
+ $n--;
4619
+ } elseif ($n < 0) {
4620
+ $n += count($list[2]);
4621
+ }
4622
+
4623
+ if (! isset($list[2][$n])) {
4624
+ $this->throwError('Invalid argument for "n"');
4625
+
4626
+ return;
4627
+ }
4628
+
4629
+ $list[2][$n] = $args[2];
4630
+
4631
+ return $list;
4632
+ }
4633
+
4634
+ protected static $libMapGet = ['map', 'key'];
4635
+ protected function libMapGet($args)
4636
+ {
4637
+ $map = $this->assertMap($args[0]);
4638
+ $key = $this->compileStringContent($this->coerceString($args[1]));
4639
+
4640
+ for ($i = count($map[1]) - 1; $i >= 0; $i--) {
4641
+ if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
4642
+ return $map[2][$i];
4643
+ }
4644
+ }
4645
+
4646
+ return self::$null;
4647
+ }
4648
+
4649
+ protected static $libMapKeys = ['map'];
4650
+ protected function libMapKeys($args)
4651
+ {
4652
+ $map = $this->assertMap($args[0]);
4653
+ $keys = $map[1];
4654
+
4655
+ return [Type::T_LIST, ',', $keys];
4656
+ }
4657
+
4658
+ protected static $libMapValues = ['map'];
4659
+ protected function libMapValues($args)
4660
+ {
4661
+ $map = $this->assertMap($args[0]);
4662
+ $values = $map[2];
4663
+
4664
+ return [Type::T_LIST, ',', $values];
4665
+ }
4666
+
4667
+ protected static $libMapRemove = ['map', 'key'];
4668
+ protected function libMapRemove($args)
4669
+ {
4670
+ $map = $this->assertMap($args[0]);
4671
+ $key = $this->compileStringContent($this->coerceString($args[1]));
4672
+
4673
+ for ($i = count($map[1]) - 1; $i >= 0; $i--) {
4674
+ if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
4675
+ array_splice($map[1], $i, 1);
4676
+ array_splice($map[2], $i, 1);
4677
+ }
4678
+ }
4679
+
4680
+ return $map;
4681
+ }
4682
+
4683
+ protected static $libMapHasKey = ['map', 'key'];
4684
+ protected function libMapHasKey($args)
4685
+ {
4686
+ $map = $this->assertMap($args[0]);
4687
+ $key = $this->compileStringContent($this->coerceString($args[1]));
4688
+
4689
+ for ($i = count($map[1]) - 1; $i >= 0; $i--) {
4690
+ if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
4691
+ return true;
4692
+ }
4693
+ }
4694
+
4695
+ return false;
4696
+ }
4697
+
4698
+ protected static $libMapMerge = ['map-1', 'map-2'];
4699
+ protected function libMapMerge($args)
4700
+ {
4701
+ $map1 = $this->assertMap($args[0]);
4702
+ $map2 = $this->assertMap($args[1]);
4703
+
4704
+ return [Type::T_MAP, array_merge($map1[1], $map2[1]), array_merge($map1[2], $map2[2])];
4705
+ }
4706
+
4707
+ protected static $libKeywords = ['args'];
4708
+ protected function libKeywords($args)
4709
+ {
4710
+ $this->assertList($args[0]);
4711
+
4712
+ $keys = [];
4713
+ $values = [];
4714
+
4715
+ foreach ($args[0][2] as $name => $arg) {
4716
+ $keys[] = [Type::T_KEYWORD, $name];
4717
+ $values[] = $arg;
4718
+ }
4719
+
4720
+ return [Type::T_MAP, $keys, $values];
4721
+ }
4722
+
4723
+ protected function listSeparatorForJoin($list1, $sep)
4724
+ {
4725
+ if (! isset($sep)) {
4726
+ return $list1[1];
4727
+ }
4728
+
4729
+ switch ($this->compileValue($sep)) {
4730
+ case 'comma':
4731
+ return ',';
4732
+
4733
+ case 'space':
4734
+ return '';
4735
+
4736
+ default:
4737
+ return $list1[1];
4738
+ }
4739
+ }
4740
+
4741
+ protected static $libJoin = ['list1', 'list2', 'separator'];
4742
+ protected function libJoin($args)
4743
+ {
4744
+ list($list1, $list2, $sep) = $args;
4745
+
4746
+ $list1 = $this->coerceList($list1, ' ');
4747
+ $list2 = $this->coerceList($list2, ' ');
4748
+ $sep = $this->listSeparatorForJoin($list1, $sep);
4749
+
4750
+ return [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
4751
+ }
4752
+
4753
+ protected static $libAppend = ['list', 'val', 'separator'];
4754
+ protected function libAppend($args)
4755
+ {
4756
+ list($list1, $value, $sep) = $args;
4757
+
4758
+ $list1 = $this->coerceList($list1, ' ');
4759
+ $sep = $this->listSeparatorForJoin($list1, $sep);
4760
+
4761
+ return [Type::T_LIST, $sep, array_merge($list1[2], [$value])];
4762
+ }
4763
+
4764
+ protected function libZip($args)
4765
+ {
4766
+ foreach ($args as $arg) {
4767
+ $this->assertList($arg);
4768
+ }
4769
+
4770
+ $lists = [];
4771
+ $firstList = array_shift($args);
4772
+
4773
+ foreach ($firstList[2] as $key => $item) {
4774
+ $list = [Type::T_LIST, '', [$item]];
4775
+
4776
+ foreach ($args as $arg) {
4777
+ if (isset($arg[2][$key])) {
4778
+ $list[2][] = $arg[2][$key];
4779
+ } else {
4780
+ break 2;
4781
+ }
4782
+ }
4783
+
4784
+ $lists[] = $list;
4785
+ }
4786
+
4787
+ return [Type::T_LIST, ',', $lists];
4788
+ }
4789
+
4790
+ protected static $libTypeOf = ['value'];
4791
+ protected function libTypeOf($args)
4792
+ {
4793
+ $value = $args[0];
4794
+
4795
+ switch ($value[0]) {
4796
+ case Type::T_KEYWORD:
4797
+ if ($value === self::$true || $value === self::$false) {
4798
+ return 'bool';
4799
+ }
4800
+
4801
+ if ($this->coerceColor($value)) {
4802
+ return 'color';
4803
+ }
4804
+
4805
+ // fall-thru
4806
+ case Type::T_FUNCTION:
4807
+ return 'string';
4808
+
4809
+ case Type::T_LIST:
4810
+ if (isset($value[3]) && $value[3]) {
4811
+ return 'arglist';
4812
+ }
4813
+
4814
+ // fall-thru
4815
+ default:
4816
+ return $value[0];
4817
+ }
4818
+ }
4819
+
4820
+ protected static $libUnit = ['number'];
4821
+ protected function libUnit($args)
4822
+ {
4823
+ $num = $args[0];
4824
+
4825
+ if ($num[0] === Type::T_NUMBER) {
4826
+ return [Type::T_STRING, '"', [$num->unitStr()]];
4827
+ }
4828
+
4829
+ return '';
4830
+ }
4831
+
4832
+ protected static $libUnitless = ['number'];
4833
+ protected function libUnitless($args)
4834
+ {
4835
+ $value = $args[0];
4836
+
4837
+ return $value[0] === Type::T_NUMBER && $value->unitless();
4838
+ }
4839
+
4840
+ protected static $libComparable = ['number-1', 'number-2'];
4841
+ protected function libComparable($args)
4842
+ {
4843
+ list($number1, $number2) = $args;
4844
+
4845
+ if (! isset($number1[0]) || $number1[0] !== Type::T_NUMBER ||
4846
+ ! isset($number2[0]) || $number2[0] !== Type::T_NUMBER
4847
+ ) {
4848
+ $this->throwError('Invalid argument(s) for "comparable"');
4849
+
4850
+ return;
4851
+ }
4852
+
4853
+ $number1 = $number1->normalize();
4854
+ $number2 = $number2->normalize();
4855
+
4856
+ return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless();
4857
+ }
4858
+
4859
+ protected static $libStrIndex = ['string', 'substring'];
4860
+ protected function libStrIndex($args)
4861
+ {
4862
+ $string = $this->coerceString($args[0]);
4863
+ $stringContent = $this->compileStringContent($string);
4864
+
4865
+ $substring = $this->coerceString($args[1]);
4866
+ $substringContent = $this->compileStringContent($substring);
4867
+
4868
+ $result = strpos($stringContent, $substringContent);
4869
+
4870
+ return $result === false ? self::$null : new Node\Number($result + 1, '');
4871
+ }
4872
+
4873
+ protected static $libStrInsert = ['string', 'insert', 'index'];
4874
+ protected function libStrInsert($args)
4875
+ {
4876
+ $string = $this->coerceString($args[0]);
4877
+ $stringContent = $this->compileStringContent($string);
4878
+
4879
+ $insert = $this->coerceString($args[1]);
4880
+ $insertContent = $this->compileStringContent($insert);
4881
+
4882
+ list(, $index) = $args[2];
4883
+
4884
+ $string[2] = [substr_replace($stringContent, $insertContent, $index - 1, 0)];
4885
+
4886
+ return $string;
4887
+ }
4888
+
4889
+ protected static $libStrLength = ['string'];
4890
+ protected function libStrLength($args)
4891
+ {
4892
+ $string = $this->coerceString($args[0]);
4893
+ $stringContent = $this->compileStringContent($string);
4894
+
4895
+ return new Node\Number(strlen($stringContent), '');
4896
+ }
4897
+
4898
+ protected static $libStrSlice = ['string', 'start-at', 'end-at'];
4899
+ protected function libStrSlice($args)
4900
+ {
4901
+ if (isset($args[2]) && $args[2][1] == 0) {
4902
+ return self::$nullString;
4903
+ }
4904
+
4905
+ $string = $this->coerceString($args[0]);
4906
+ $stringContent = $this->compileStringContent($string);
4907
+
4908
+ $start = (int) $args[1][1];
4909
+
4910
+ if ($start > 0) {
4911
+ $start--;
4912
+ }
4913
+
4914
+ $end = (int) $args[2][1];
4915
+ $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end);
4916
+
4917
+ $string[2] = $length
4918
+ ? [substr($stringContent, $start, $length)]
4919
+ : [substr($stringContent, $start)];
4920
+
4921
+ return $string;
4922
+ }
4923
+
4924
+ protected static $libToLowerCase = ['string'];
4925
+ protected function libToLowerCase($args)
4926
+ {
4927
+ $string = $this->coerceString($args[0]);
4928
+ $stringContent = $this->compileStringContent($string);
4929
+
4930
+ $string[2] = [mb_strtolower($stringContent)];
4931
+
4932
+ return $string;
4933
+ }
4934
+
4935
+ protected static $libToUpperCase = ['string'];
4936
+ protected function libToUpperCase($args)
4937
+ {
4938
+ $string = $this->coerceString($args[0]);
4939
+ $stringContent = $this->compileStringContent($string);
4940
+
4941
+ $string[2] = [mb_strtoupper($stringContent)];
4942
+
4943
+ return $string;
4944
+ }
4945
+
4946
+ protected static $libFeatureExists = ['feature'];
4947
+ protected function libFeatureExists($args)
4948
+ {
4949
+ $string = $this->coerceString($args[0]);
4950
+ $name = $this->compileStringContent($string);
4951
+
4952
+ return $this->toBool(
4953
+ array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false
4954
+ );
4955
+ }
4956
+
4957
+ protected static $libFunctionExists = ['name'];
4958
+ protected function libFunctionExists($args)
4959
+ {
4960
+ $string = $this->coerceString($args[0]);
4961
+ $name = $this->compileStringContent($string);
4962
+
4963
+ // user defined functions
4964
+ if ($this->has(self::$namespaces['function'] . $name)) {
4965
+ return true;
4966
+ }
4967
+
4968
+ $name = $this->normalizeName($name);
4969
+
4970
+ if (isset($this->userFunctions[$name])) {
4971
+ return true;
4972
+ }
4973
+
4974
+ // built-in functions
4975
+ $f = $this->getBuiltinFunction($name);
4976
+
4977
+ return $this->toBool(is_callable($f));
4978
+ }
4979
+
4980
+ protected static $libGlobalVariableExists = ['name'];
4981
+ protected function libGlobalVariableExists($args)
4982
+ {
4983
+ $string = $this->coerceString($args[0]);
4984
+ $name = $this->compileStringContent($string);
4985
+
4986
+ return $this->has($name, $this->rootEnv);
4987
+ }
4988
+
4989
+ protected static $libMixinExists = ['name'];
4990
+ protected function libMixinExists($args)
4991
+ {
4992
+ $string = $this->coerceString($args[0]);
4993
+ $name = $this->compileStringContent($string);
4994
+
4995
+ return $this->has(self::$namespaces['mixin'] . $name);
4996
+ }
4997
+
4998
+ protected static $libVariableExists = ['name'];
4999
+ protected function libVariableExists($args)
5000
+ {
5001
+ $string = $this->coerceString($args[0]);
5002
+ $name = $this->compileStringContent($string);
5003
+
5004
+ return $this->has($name);
5005
+ }
5006
+
5007
+ /**
5008
+ * Workaround IE7's content counter bug.
5009
+ *
5010
+ * @param array $args
5011
+ */
5012
+ protected function libCounter($args)
5013
+ {
5014
+ $list = array_map([$this, 'compileValue'], $args);
5015
+
5016
+ return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']];
5017
+ }
5018
+
5019
+ protected static $libRandom = ['limit'];
5020
+ protected function libRandom($args)
5021
+ {
5022
+ if (isset($args[0])) {
5023
+ $n = $this->assertNumber($args[0]);
5024
+
5025
+ if ($n < 1) {
5026
+ $this->throwError("limit must be greater than or equal to 1");
5027
+
5028
+ return;
5029
+ }
5030
+
5031
+ return new Node\Number(mt_rand(1, $n), '');
5032
+ }
5033
+
5034
+ return new Node\Number(mt_rand(1, mt_getrandmax()), '');
5035
+ }
5036
+
5037
+ protected function libUniqueId()
5038
+ {
5039
+ static $id;
5040
+
5041
+ if (! isset($id)) {
5042
+ $id = mt_rand(0, pow(36, 8));
5043
+ }
5044
+
5045
+ $id += mt_rand(0, 10) + 1;
5046
+
5047
+ return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
5048
+ }
5049
+
5050
+ protected static $libInspect = ['value'];
5051
+ protected function libInspect($args)
5052
+ {
5053
+ if ($args[0] === self::$null) {
5054
+ return [Type::T_KEYWORD, 'null'];
5055
+ }
5056
+
5057
+ return $args[0];
5058
+ }
5059
+ }
scssphp/src/Compiler/Environment.php ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
16
+ *
17
+ * @author Anthon Pang <anthon.pang@gmail.com>
18
+ */
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
+
31
+ /**
32
+ * @var array
33
+ */
34
+ public $store;
35
+
36
+ /**
37
+ * @var integer
38
+ */
39
+ public $depth;
40
+ }
scssphp/src/Exception/CompilerException.php ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
16
+ *
17
+ * @author Oleksandr Savchenko <traveltino@gmail.com>
18
+ */
19
+ class CompilerException extends \Exception
20
+ {
21
+ }
scssphp/src/Exception/ParserException.php ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
16
+ *
17
+ * @author Oleksandr Savchenko <traveltino@gmail.com>
18
+ */
19
+ class ParserException extends \Exception
20
+ {
21
+ }
scssphp/src/Exception/ServerException.php ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
16
+ *
17
+ * @author Anthon Pang <anthon.pang@gmail.com>
18
+ */
19
+ class ServerException extends \Exception
20
+ {
21
+ }
scssphp/src/Formatter.php ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
+
16
+ /**
17
+ * Base formatter
18
+ *
19
+ * @author Leaf Corcoran <leafot@gmail.com>
20
+ */
21
+ abstract class Formatter
22
+ {
23
+ /**
24
+ * @var integer
25
+ */
26
+ public $indentLevel;
27
+
28
+ /**
29
+ * @var string
30
+ */
31
+ public $indentChar;
32
+
33
+ /**
34
+ * @var string
35
+ */
36
+ public $break;
37
+
38
+ /**
39
+ * @var string
40
+ */
41
+ public $open;
42
+
43
+ /**
44
+ * @var string
45
+ */
46
+ public $close;
47
+
48
+ /**
49
+ * @var string
50
+ */
51
+ public $tagSeparator;
52
+
53
+ /**
54
+ * @var string
55
+ */
56
+ public $assignSeparator;
57
+
58
+ /**
59
+ * @var boolea
60
+ */
61
+ public $keepSemicolons;
62
+
63
+ /**
64
+ * Initialize formatter
65
+ *
66
+ * @api
67
+ */
68
+ abstract public function __construct();
69
+
70
+ /**
71
+ * Return indentation (whitespace)
72
+ *
73
+ * @return string
74
+ */
75
+ protected function indentStr()
76
+ {
77
+ return '';
78
+ }
79
+
80
+ /**
81
+ * Return property assignment
82
+ *
83
+ * @api
84
+ *
85
+ * @param string $name
86
+ * @param mixed $value
87
+ *
88
+ * @return string
89
+ */
90
+ public function property($name, $value)
91
+ {
92
+ return rtrim($name) . $this->assignSeparator . $value . ';';
93
+ }
94
+
95
+ /**
96
+ * Strip semi-colon appended by property(); it's a separator, not a terminator
97
+ *
98
+ * @api
99
+ *
100
+ * @param array $lines
101
+ */
102
+ public function stripSemicolon(&$lines)
103
+ {
104
+ if ($this->keepSemicolons) {
105
+ return;
106
+ }
107
+
108
+ if (($count = count($lines))
109
+ && substr($lines[$count - 1], -1) === ';'
110
+ ) {
111
+ $lines[$count - 1] = substr($lines[$count - 1], 0, -1);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Output lines inside a block
117
+ *
118
+ * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
119
+ */
120
+ protected function blockLines(OutputBlock $block)
121
+ {
122
+ $inner = $this->indentStr();
123
+
124
+ $glue = $this->break . $inner;
125
+
126
+ echo $inner . implode($glue, $block->lines);
127
+
128
+ if (! empty($block->children)) {
129
+ echo $this->break;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Output block selectors
135
+ *
136
+ * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
137
+ */
138
+ protected function blockSelectors(OutputBlock $block)
139
+ {
140
+ $inner = $this->indentStr();
141
+
142
+ echo $inner
143
+ . implode($this->tagSeparator, $block->selectors)
144
+ . $this->open . $this->break;
145
+ }
146
+
147
+ /**
148
+ * Output block children
149
+ *
150
+ * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
151
+ */
152
+ protected function blockChildren(OutputBlock $block)
153
+ {
154
+ foreach ($block->children as $child) {
155
+ $this->block($child);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Output non-empty block
161
+ *
162
+ * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
163
+ */
164
+ protected function block(OutputBlock $block)
165
+ {
166
+ if (empty($block->lines) && empty($block->children)) {
167
+ return;
168
+ }
169
+
170
+ $pre = $this->indentStr();
171
+
172
+ if (! empty($block->selectors)) {
173
+ $this->blockSelectors($block);
174
+
175
+ $this->indentLevel++;
176
+ }
177
+
178
+ if (! empty($block->lines)) {
179
+ $this->blockLines($block);
180
+ }
181
+
182
+ if (! empty($block->children)) {
183
+ $this->blockChildren($block);
184
+ }
185
+
186
+ if (! empty($block->selectors)) {
187
+ $this->indentLevel--;
188
+
189
+ if (empty($block->children)) {
190
+ echo $this->break;
191
+ }
192
+
193
+ echo $pre . $this->close . $this->break;
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Entry point to formatting a block
199
+ *
200
+ * @api
201
+ *
202
+ * @param \Leafo\ScssPhp\Formatter\OutputBlock $block An abstract syntax tree
203
+ *
204
+ * @return string
205
+ */
206
+ public function format(OutputBlock $block)
207
+ {
208
+ ob_start();
209
+
210
+ $this->block($block);
211
+
212
+ $out = ob_get_clean();
213
+
214
+ return $out;
215
+ }
216
+ }
scssphp/src/Formatter/Compact.php ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
18
+ *
19
+ * @author Leaf Corcoran <leafot@gmail.com>
20
+ */
21
+ class Compact extends Formatter
22
+ {
23
+ /**
24
+ * {@inheritdoc}
25
+ */
26
+ public function __construct()
27
+ {
28
+ $this->indentLevel = 0;
29
+ $this->indentChar = '';
30
+ $this->break = '';
31
+ $this->open = ' {';
32
+ $this->close = "}\n\n";
33
+ $this->tagSeparator = ',';
34
+ $this->assignSeparator = ':';
35
+ $this->keepSemicolons = true;
36
+ }
37
+
38
+ /**
39
+ * {@inheritdoc}
40
+ */
41
+ public function indentStr()
42
+ {
43
+ return ' ';
44
+ }
45
+ }
scssphp/src/Formatter/Compressed.php ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
19
+ *
20
+ * @author Leaf Corcoran <leafot@gmail.com>
21
+ */
22
+ class Compressed extends Formatter
23
+ {
24
+ /**
25
+ * {@inheritdoc}
26
+ */
27
+ public function __construct()
28
+ {
29
+ $this->indentLevel = 0;
30
+ $this->indentChar = ' ';
31
+ $this->break = '';
32
+ $this->open = '{';
33
+ $this->close = '}';
34
+ $this->tagSeparator = ',';
35
+ $this->assignSeparator = ':';
36
+ $this->keepSemicolons = false;
37
+ }
38
+
39
+ /**
40
+ * {@inheritdoc}
41
+ */
42
+ public function blockLines(OutputBlock $block)
43
+ {
44
+ $inner = $this->indentStr();
45
+
46
+ $glue = $this->break . $inner;
47
+
48
+ foreach ($block->lines as $index => $line) {
49
+ if (substr($line, 0, 2) === '/*' && substr($line, 2, 1) !== '!') {
50
+ unset($block->lines[$index]);
51
+ } elseif (substr($line, 0, 3) === '/*!') {
52
+ $block->lines[$index] = '/*' . substr($line, 3);
53
+ }
54
+ }
55
+
56
+ echo $inner . implode($glue, $block->lines);
57
+
58
+ if (! empty($block->children)) {
59
+ echo $this->break;
60
+ }
61
+ }
62
+ }
scssphp/src/Formatter/Crunched.php ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
19
+ *
20
+ * @author Anthon Pang <anthon.pang@gmail.com>
21
+ */
22
+ class Crunched extends Formatter
23
+ {
24
+ /**
25
+ * {@inheritdoc}
26
+ */
27
+ public function __construct()
28
+ {
29
+ $this->indentLevel = 0;
30
+ $this->indentChar = ' ';
31
+ $this->break = '';
32
+ $this->open = '{';
33
+ $this->close = '}';
34
+ $this->tagSeparator = ',';
35
+ $this->assignSeparator = ':';
36
+ $this->keepSemicolons = false;
37
+ }
38
+
39
+ /**
40
+ * {@inheritdoc}
41
+ */
42
+ public function blockLines(OutputBlock $block)
43
+ {
44
+ $inner = $this->indentStr();
45
+
46
+ $glue = $this->break . $inner;
47
+
48
+ foreach ($block->lines as $index => $line) {
49
+ if (substr($line, 0, 2) === '/*') {
50
+ unset($block->lines[$index]);
51
+ }
52
+ }
53
+
54
+ echo $inner . implode($glue, $block->lines);
55
+
56
+ if (! empty($block->children)) {
57
+ echo $this->break;
58
+ }
59
+ }
60
+ }
scssphp/src/Formatter/Debug.php ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
19
+ *
20
+ * @author Anthon Pang <anthon.pang@gmail.com>
21
+ */
22
+ class Debug extends Formatter
23
+ {
24
+ /**
25
+ * {@inheritdoc}
26
+ */
27
+ public function __construct()
28
+ {
29
+ $this->indentLevel = 0;
30
+ $this->indentChar = '';
31
+ $this->break = "\n";
32
+ $this->open = ' {';
33
+ $this->close = ' }';
34
+ $this->tagSeparator = ', ';
35
+ $this->assignSeparator = ': ';
36
+ $this->keepSemicolons = true;
37
+ }
38
+
39
+ /**
40
+ * {@inheritdoc}
41
+ */
42
+ protected function indentStr()
43
+ {
44
+ return str_repeat(' ', $this->indentLevel);
45
+ }
46
+
47
+ /**
48
+ * {@inheritdoc}
49
+ */
50
+ protected function blockLines(OutputBlock $block)
51
+ {
52
+ $indent = $this->indentStr();
53
+
54
+ if (empty($block->lines)) {
55
+ echo "{$indent}block->lines: []\n";
56
+
57
+ return;
58
+ }
59
+
60
+ foreach ($block->lines as $index => $line) {
61
+ echo "{$indent}block->lines[{$index}]: $line\n";
62
+ }
63
+ }
64
+
65
+ /**
66
+ * {@inheritdoc}
67
+ */
68
+ protected function blockSelectors(OutputBlock $block)
69
+ {
70
+ $indent = $this->indentStr();
71
+
72
+ if (empty($block->selectors)) {
73
+ echo "{$indent}block->selectors: []\n";
74
+
75
+ return;
76
+ }
77
+
78
+ foreach ($block->selectors as $index => $selector) {
79
+ echo "{$indent}block->selectors[{$index}]: $selector\n";
80
+ }
81
+ }
82
+
83
+ /**
84
+ * {@inheritdoc}
85
+ */
86
+ protected function blockChildren(OutputBlock $block)
87
+ {
88
+ $indent = $this->indentStr();
89
+
90
+ if (empty($block->children)) {
91
+ echo "{$indent}block->children: []\n";
92
+
93
+ return;
94
+ }
95
+
96
+ $this->indentLevel++;
97
+
98
+ foreach ($block->children as $i => $child) {
99
+ $this->block($child);
100
+ }
101
+
102
+ $this->indentLevel--;
103
+ }
104
+
105
+ /**
106
+ * {@inheritdoc}
107
+ */
108
+ protected function block(OutputBlock $block)
109
+ {
110
+ $indent = $this->indentStr();
111
+
112
+ echo "{$indent}block->type: {$block->type}\n" .
113
+ "{$indent}block->depth: {$block->depth}\n";
114
+
115
+ $this->blockSelectors($block);
116
+ $this->blockLines($block);
117
+ $this->blockChildren($block);
118
+ }
119
+ }
scssphp/src/Formatter/Expanded.php ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
19
+ *
20
+ * @author Leaf Corcoran <leafot@gmail.com>
21
+ */
22
+ class Expanded extends Formatter
23
+ {
24
+ /**
25
+ * {@inheritdoc}
26
+ */
27
+ public function __construct()
28
+ {
29
+ $this->indentLevel = 0;
30
+ $this->indentChar = ' ';
31
+ $this->break = "\n";
32
+ $this->open = ' {';
33
+ $this->close = '}';
34
+ $this->tagSeparator = ', ';
35
+ $this->assignSeparator = ': ';
36
+ $this->keepSemicolons = true;
37
+ }
38
+
39
+ /**
40
+ * {@inheritdoc}
41
+ */
42
+ protected function indentStr()
43
+ {
44
+ return str_repeat($this->indentChar, $this->indentLevel);
45
+ }
46
+
47
+ /**
48
+ * {@inheritdoc}
49
+ */
50
+ protected function blockLines(OutputBlock $block)
51
+ {
52
+ $inner = $this->indentStr();
53
+
54
+ $glue = $this->break . $inner;
55
+
56
+ foreach ($block->lines as $index => $line) {
57
+ if (substr($line, 0, 2) === '/*') {
58
+ $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line);
59
+ }
60
+ }
61
+
62
+ echo $inner . implode($glue, $block->lines);
63
+
64
+ if (empty($block->selectors) || ! empty($block->children)) {
65
+ echo $this->break;
66
+ }
67
+ }
68
+ }
scssphp/src/Formatter/Nested.php ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
19
+ *
20
+ * @author Leaf Corcoran <leafot@gmail.com>
21
+ */
22
+ class Nested extends Formatter
23
+ {
24
+ /**
25
+ * @var integer
26
+ */
27
+ private $depth;
28
+
29
+ /**
30
+ * {@inheritdoc}
31
+ */
32
+ public function __construct()
33
+ {
34
+ $this->indentLevel = 0;
35
+ $this->indentChar = ' ';
36
+ $this->break = "\n";
37
+ $this->open = ' {';
38
+ $this->close = ' }';
39
+ $this->tagSeparator = ', ';
40
+ $this->assignSeparator = ': ';
41
+ $this->keepSemicolons = true;
42
+ }
43
+
44
+ /**
45
+ * {@inheritdoc}
46
+ */
47
+ protected function indentStr()
48
+ {
49
+ $n = $this->depth - 1;
50
+
51
+ return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
52
+ }
53
+
54
+ /**
55
+ * {@inheritdoc}
56
+ */
57
+ protected function blockLines(OutputBlock $block)
58
+ {
59
+ $inner = $this->indentStr();
60
+
61
+ $glue = $this->break . $inner;
62
+
63
+ foreach ($block->lines as $index => $line) {
64
+ if (substr($line, 0, 2) === '/*') {
65
+ $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line);
66
+ }
67
+ }
68
+
69
+ echo $inner . implode($glue, $block->lines);
70
+
71
+ if (! empty($block->children)) {
72
+ echo $this->break;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * {@inheritdoc}
78
+ */
79
+ protected function blockSelectors(OutputBlock $block)
80
+ {
81
+ $inner = $this->indentStr();
82
+
83
+ echo $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
+ echo $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
+ echo $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)) {
120
+ return;
121
+ }
122
+
123
+ $this->depth = $block->depth;
124
+
125
+ if (! empty($block->selectors)) {
126
+ $this->blockSelectors($block);
127
+
128
+ $this->indentLevel++;
129
+ }
130
+
131
+ if (! empty($block->lines)) {
132
+ $this->blockLines($block);
133
+ }
134
+
135
+ if (! empty($block->children)) {
136
+ $this->blockChildren($block);
137
+ }
138
+
139
+ if (! empty($block->selectors)) {
140
+ $this->indentLevel--;
141
+
142
+ echo $this->close;
143
+ }
144
+
145
+ if ($block->type === 'root') {
146
+ echo $this->break;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Adjust the depths of all children, depth first
152
+ *
153
+ * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
154
+ */
155
+ private function adjustAllChildren(OutputBlock $block)
156
+ {
157
+ // flatten empty nested blocks
158
+ $children = [];
159
+
160
+ foreach ($block->children as $i => $child) {
161
+ if (empty($child->lines) && empty($child->children)) {
162
+ if (isset($block->children[$i + 1])) {
163
+ $block->children[$i + 1]->depth = $child->depth;
164
+ }
165
+
166
+ continue;
167
+ }
168
+
169
+ $children[] = $child;
170
+ }
171
+
172
+ $count = count($children);
173
+
174
+ for ($i = 0; $i < $count; $i++) {
175
+ $depth = $children[$i]->depth;
176
+ $j = $i + 1;
177
+
178
+ if (isset($children[$j]) && $depth < $children[$j]->depth) {
179
+ $childDepth = $children[$j]->depth;
180
+
181
+ for (; $j < $count; $j++) {
182
+ if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) {
183
+ $children[$j]->depth = $depth + 1;
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ $block->children = $children;
190
+
191
+ // make relative to parent
192
+ foreach ($block->children as $child) {
193
+ $this->adjustAllChildren($child);
194
+
195
+ $child->depth = $child->depth - $block->depth;
196
+ }
197
+ }
198
+ }
scssphp/src/Formatter/OutputBlock.php ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
16
+ *
17
+ * @author Anthon Pang <anthon.pang@gmail.com>
18
+ */
19
+ class OutputBlock
20
+ {
21
+ /**
22
+ * @var string
23
+ */
24
+ public $type;
25
+
26
+ /**
27
+ * @var integer
28
+ */
29
+ public $depth;
30
+
31
+ /**
32
+ * @var array
33
+ */
34
+ public $selectors;
35
+
36
+ /**
37
+ * @var array
38
+ */
39
+ public $lines;
40
+
41
+ /**
42
+ * @var array
43
+ */
44
+ public $children;
45
+
46
+ /**
47
+ * @var \Leafo\ScssPhp\Formatter\OutputBlock
48
+ */
49
+ public $parent;
50
+ }
scssphp/src/Node.php ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
16
+ *
17
+ * @author Anthon Pang <anthon.pang@gmail.com>
18
+ */
19
+ abstract class Node
20
+ {
21
+ /**
22
+ * @var string
23
+ */
24
+ public $type;
25
+
26
+ /**
27
+ * @var integer
28
+ */
29
+ public $sourceIndex;
30
+
31
+ /**
32
+ * @var integer
33
+ */
34
+ public $sourceLine;
35
+
36
+ /**
37
+ * @var integer
38
+ */
39
+ public $sourceColumn;
40
+ }
scssphp/src/Node/Number.php ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
20
+ *
21
+ * {@internal
22
+ * This is a work-in-progress.
23
+ *
24
+ * The \ArrayAccess interface is temporary until the migration is complete.
25
+ * }}
26
+ *
27
+ * @author Anthon Pang <anthon.pang@gmail.com>
28
+ */
29
+ class Number extends Node implements \ArrayAccess
30
+ {
31
+ /**
32
+ * @var integer
33
+ */
34
+ static public $precision = 5;
35
+
36
+ /**
37
+ * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/
38
+ *
39
+ * @var array
40
+ */
41
+ static protected $unitTable = [
42
+ 'in' => [
43
+ 'in' => 1,
44
+ 'pc' => 6,
45
+ 'pt' => 72,
46
+ 'px' => 96,
47
+ 'cm' => 2.54,
48
+ 'mm' => 25.4,
49
+ 'q' => 101.6,
50
+ ],
51
+ 'turn' => [
52
+ 'deg' => 360,
53
+ 'grad' => 400,
54
+ 'rad' => 6.28318530717958647692528676, // 2 * M_PI
55
+ 'turn' => 1,
56
+ ],
57
+ 's' => [
58
+ 's' => 1,
59
+ 'ms' => 1000,
60
+ ],
61
+ 'Hz' => [
62
+ 'Hz' => 1,
63
+ 'kHz' => 0.001,
64
+ ],
65
+ 'dpi' => [
66
+ 'dpi' => 1,
67
+ 'dpcm' => 2.54,
68
+ 'dppx' => 96,
69
+ ],
70
+ ];
71
+
72
+ /**
73
+ * @var integer|float
74
+ */
75
+ public $dimension;
76
+
77
+ /**
78
+ * @var array
79
+ */
80
+ public $units;
81
+
82
+ /**
83
+ * Initialize number
84
+ *
85
+ * @param mixed $dimension
86
+ * @param mixed $initialUnit
87
+ */
88
+ public function __construct($dimension, $initialUnit)
89
+ {
90
+ $this->type = Type::T_NUMBER;
91
+ $this->dimension = $dimension;
92
+ $this->units = is_array($initialUnit)
93
+ ? $initialUnit
94
+ : ($initialUnit ? [$initialUnit => 1]
95
+ : []);
96
+ }
97
+
98
+ /**
99
+ * Coerce number to target units
100
+ *
101
+ * @param array $units
102
+ *
103
+ * @return \Leafo\ScssPhp\Node\Number
104
+ */
105
+ public function coerce($units)
106
+ {
107
+ if ($this->unitless()) {
108
+ return new Number($this->dimension, $units);
109
+ }
110
+
111
+ $dimension = $this->dimension;
112
+
113
+ foreach (self::$unitTable['in'] as $unit => $conv) {
114
+ $from = isset($this->units[$unit]) ? $this->units[$unit] : 0;
115
+ $to = isset($units[$unit]) ? $units[$unit] : 0;
116
+ $factor = pow($conv, $from - $to);
117
+ $dimension /= $factor;
118
+ }
119
+
120
+ return new Number($dimension, $units);
121
+ }
122
+
123
+ /**
124
+ * Normalize number
125
+ *
126
+ * @return \Leafo\ScssPhp\Node\Number
127
+ */
128
+ public function normalize()
129
+ {
130
+ $dimension = $this->dimension;
131
+ $units = [];
132
+
133
+ $this->normalizeUnits($dimension, $units, 'in');
134
+
135
+ return new Number($dimension, $units);
136
+ }
137
+
138
+ /**
139
+ * {@inheritdoc}
140
+ */
141
+ public function offsetExists($offset)
142
+ {
143
+ if ($offset === -3) {
144
+ return $this->sourceColumn !== null;
145
+ }
146
+
147
+ if ($offset === -2) {
148
+ return $this->sourceLine !== null;
149
+ }
150
+
151
+ if ($offset === -1
152
+ || $offset === 0
153
+ || $offset === 1
154
+ || $offset === 2
155
+ ) {
156
+ return true;
157
+ }
158
+
159
+ return false;
160
+ }
161
+
162
+ /**
163
+ * {@inheritdoc}
164
+ */
165
+ public function offsetGet($offset)
166
+ {
167
+ switch ($offset) {
168
+ case -3:
169
+ return $this->sourceColumn;
170
+
171
+ case -2:
172
+ return $this->sourceLine;
173
+
174
+ case -1:
175
+ return $this->sourceIndex;
176
+
177
+ case 0:
178
+ return $this->type;
179
+
180
+ case 1:
181
+ return $this->dimension;
182
+
183
+ case 2:
184
+ return $this->units;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * {@inheritdoc}
190
+ */
191
+ public function offsetSet($offset, $value)
192
+ {
193
+ if ($offset === 1) {
194
+ $this->dimension = $value;
195
+ } elseif ($offset === 2) {
196
+ $this->units = $value;
197
+ } elseif ($offset == -1) {
198
+ $this->sourceIndex = $value;
199
+ } elseif ($offset == -2) {
200
+ $this->sourceLine = $value;
201
+ } elseif ($offset == -3) {
202
+ $this->sourceColumn = $value;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * {@inheritdoc}
208
+ */
209
+ public function offsetUnset($offset)
210
+ {
211
+ if ($offset === 1) {
212
+ $this->dimension = null;
213
+ } elseif ($offset === 2) {
214
+ $this->units = null;
215
+ } elseif ($offset === -1) {
216
+ $this->sourceIndex = null;
217
+ } elseif ($offset === -2) {
218
+ $this->sourceLine = null;
219
+ } elseif ($offset === -3) {
220
+ $this->sourceColumn = null;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Returns true if the number is unitless
226
+ *
227
+ * @return boolean
228
+ */
229
+ public function unitless()
230
+ {
231
+ return ! array_sum($this->units);
232
+ }
233
+
234
+ /**
235
+ * Returns unit(s) as the product of numerator units divided by the product of denominator units
236
+ *
237
+ * @return string
238
+ */
239
+ public function unitStr()
240
+ {
241
+ $numerators = [];
242
+ $denominators = [];
243
+
244
+ foreach ($this->units as $unit => $unitSize) {
245
+ if ($unitSize > 0) {
246
+ $numerators = array_pad($numerators, count($numerators) + $unitSize, $unit);
247
+ continue;
248
+ }
249
+
250
+ if ($unitSize < 0) {
251
+ $denominators = array_pad($denominators, count($denominators) + $unitSize, $unit);
252
+ continue;
253
+ }
254
+ }
255
+
256
+ return implode('*', $numerators) . (count($denominators) ? '/' . implode('*', $denominators) : '');
257
+ }
258
+
259
+ /**
260
+ * Output number
261
+ *
262
+ * @param \Leafo\ScssPhp\Compiler $compiler
263
+ *
264
+ * @return string
265
+ */
266
+ public function output(Compiler $compiler = null)
267
+ {
268
+ $dimension = round($this->dimension, self::$precision);
269
+
270
+ $units = array_filter($this->units, function ($unitSize) {
271
+ return $unitSize;
272
+ });
273
+
274
+ if (count($units) > 1 && array_sum($units) === 0) {
275
+ $dimension = $this->dimension;
276
+ $units = [];
277
+
278
+ $this->normalizeUnits($dimension, $units, 'in');
279
+
280
+ $dimension = round($dimension, self::$precision);
281
+ $units = array_filter($units, function ($unitSize) {
282
+ return $unitSize;
283
+ });
284
+ }
285
+
286
+ $unitSize = array_sum($units);
287
+
288
+ if ($compiler && ($unitSize > 1 || $unitSize < 0 || count($units) > 1)) {
289
+ $compiler->throwError((string) $dimension . $this->unitStr() . " isn't a valid CSS value.");
290
+ }
291
+
292
+ reset($units);
293
+ list($unit, ) = each($units);
294
+
295
+ return (string) $dimension . $unit;
296
+ }
297
+
298
+ /**
299
+ * {@inheritdoc}
300
+ */
301
+ public function __toString()
302
+ {
303
+ return $this->output();
304
+ }
305
+
306
+ /**
307
+ * Normalize units
308
+ *
309
+ * @param integer|float $dimension
310
+ * @param array $units
311
+ * @param string $baseUnit
312
+ */
313
+ private function normalizeUnits(&$dimension, &$units, $baseUnit = 'in')
314
+ {
315
+ $dimension = $this->dimension;
316
+ $units = [];
317
+
318
+ foreach ($this->units as $unit => $exp) {
319
+ if (isset(self::$unitTable[$baseUnit][$unit])) {
320
+ $factor = pow(self::$unitTable[$baseUnit][$unit], $exp);
321
+
322
+ $unit = $baseUnit;
323
+ $dimension /= $factor;
324
+ }
325
+
326
+ $units[$unit] = $exp + (isset($units[$unit]) ? $units[$unit] : 0);
327
+ }
328
+ }
329
+ }
scssphp/src/Parser.php ADDED
@@ -0,0 +1,2478 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
22
+ *
23
+ * @author Leaf Corcoran <leafot@gmail.com>
24
+ */
25
+ class Parser
26
+ {
27
+ const SOURCE_INDEX = -1;
28
+ const SOURCE_LINE = -2;
29
+ const SOURCE_COLUMN = -3;
30
+
31
+ /**
32
+ * @var array
33
+ */
34
+ protected static $precedence = [
35
+ '=' => 0,
36
+ 'or' => 1,
37
+ 'and' => 2,
38
+ '==' => 3,
39
+ '!=' => 3,
40
+ '<=>' => 3,
41
+ '<=' => 4,
42
+ '>=' => 4,
43
+ '<' => 4,
44
+ '>' => 4,
45
+ '+' => 5,
46
+ '-' => 5,
47
+ '*' => 6,
48
+ '/' => 6,
49
+ '%' => 6,
50
+ ];
51
+
52
+ protected static $commentPattern;
53
+ protected static $operatorPattern;
54
+ protected static $whitePattern;
55
+
56
+ private $sourceName;
57
+ private $sourceIndex;
58
+ private $sourcePositions;
59
+ private $charset;
60
+ private $count;
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(self::$operatorPattern)) {
87
+ self::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
88
+
89
+ $commentSingle = '\/\/';
90
+ $commentMultiLeft = '\/\*';
91
+ $commentMultiRight = '\*\/';
92
+
93
+ self::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight;
94
+ self::$whitePattern = $this->utf8
95
+ ? '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentPattern . ')\s*|\s+/AisuS'
96
+ : '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentPattern . ')\s*|\s+/AisS';
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Get source file name
102
+ *
103
+ * @api
104
+ *
105
+ * @return string
106
+ */
107
+ public function getSourceName()
108
+ {
109
+ return $this->sourceName;
110
+ }
111
+
112
+ /**
113
+ * Throw parser error
114
+ *
115
+ * @api
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");
129
+ }
130
+
131
+ throw new ParserException("$msg: $loc");
132
+ }
133
+
134
+ /**
135
+ * Parser buffer
136
+ *
137
+ * @api
138
+ *
139
+ * @param string $buffer
140
+ *
141
+ * @return \Leafo\ScssPhp\Block
142
+ */
143
+ public function parse($buffer)
144
+ {
145
+ $this->count = 0;
146
+ $this->env = null;
147
+ $this->inParens = false;
148
+ $this->eatWhiteDefault = true;
149
+ $this->buffer = rtrim($buffer, "\x00..\x1f");
150
+
151
+ $this->saveEncoding();
152
+ $this->extractLineNumbers($buffer);
153
+
154
+ $this->pushBlock(null); // root block
155
+ $this->whitespace();
156
+ $this->pushBlock(null);
157
+ $this->popBlock();
158
+
159
+ while ($this->parseChunk()) {
160
+ ;
161
+ }
162
+
163
+ if ($this->count !== strlen($this->buffer)) {
164
+ $this->throwParseError();
165
+ }
166
+
167
+ if (! empty($this->env->parent)) {
168
+ $this->throwParseError('unclosed block');
169
+ }
170
+
171
+ if ($this->charset) {
172
+ array_unshift($this->env->children, $this->charset);
173
+ }
174
+
175
+ $this->env->isRoot = true;
176
+
177
+ $this->restoreEncoding();
178
+
179
+ return $this->env;
180
+ }
181
+
182
+ /**
183
+ * Parse a value or value list
184
+ *
185
+ * @api
186
+ *
187
+ * @param string $buffer
188
+ * @param string $out
189
+ *
190
+ * @return boolean
191
+ */
192
+ public function parseValue($buffer, &$out)
193
+ {
194
+ $this->count = 0;
195
+ $this->env = null;
196
+ $this->inParens = false;
197
+ $this->eatWhiteDefault = true;
198
+ $this->buffer = (string) $buffer;
199
+
200
+ $this->saveEncoding();
201
+
202
+ $list = $this->valueList($out);
203
+
204
+ $this->restoreEncoding();
205
+
206
+ return $list;
207
+ }
208
+
209
+ /**
210
+ * Parse a selector or selector list
211
+ *
212
+ * @api
213
+ *
214
+ * @param string $buffer
215
+ * @param string $out
216
+ *
217
+ * @return boolean
218
+ */
219
+ public function parseSelector($buffer, &$out)
220
+ {
221
+ $this->count = 0;
222
+ $this->env = null;
223
+ $this->inParens = false;
224
+ $this->eatWhiteDefault = true;
225
+ $this->buffer = (string) $buffer;
226
+
227
+ $this->saveEncoding();
228
+
229
+ $selector = $this->selectors($out);
230
+
231
+ $this->restoreEncoding();
232
+
233
+ return $selector;
234
+ }
235
+
236
+ /**
237
+ * Parse a single chunk off the head of the buffer and append it to the
238
+ * current parse environment.
239
+ *
240
+ * Returns false when the buffer is empty, or when there is an error.
241
+ *
242
+ * This function is called repeatedly until the entire document is
243
+ * parsed.
244
+ *
245
+ * This parser is most similar to a recursive descent parser. Single
246
+ * functions represent discrete grammatical rules for the language, and
247
+ * they are able to capture the text that represents those rules.
248
+ *
249
+ * Consider the function Compiler::keyword(). (All parse functions are
250
+ * structured the same.)
251
+ *
252
+ * The function takes a single reference argument. When calling the
253
+ * function it will attempt to match a keyword on the head of the buffer.
254
+ * If it is successful, it will place the keyword in the referenced
255
+ * argument, advance the position in the buffer, and return true. If it
256
+ * fails then it won't advance the buffer and it will return false.
257
+ *
258
+ * All of these parse functions are powered by Compiler::match(), which behaves
259
+ * the same way, but takes a literal regular expression. Sometimes it is
260
+ * more convenient to use match instead of creating a new function.
261
+ *
262
+ * Because of the format of the functions, to parse an entire string of
263
+ * grammatical rules, you can chain them together using &&.
264
+ *
265
+ * But, if some of the rules in the chain succeed before one fails, then
266
+ * the buffer position will be left at an invalid state. In order to
267
+ * avoid this, Compiler::seek() is used to remember and set buffer positions.
268
+ *
269
+ * Before parsing a chain, use $s = $this->seek() to remember the current
270
+ * position into $s. Then if a chain fails, use $this->seek($s) to
271
+ * go back where we started.
272
+ *
273
+ * @return boolean
274
+ */
275
+ protected function parseChunk()
276
+ {
277
+ $s = $this->seek();
278
+
279
+ // the directives
280
+ if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
281
+ if ($this->literal('@at-root') &&
282
+ ($this->selectors($selector) || true) &&
283
+ ($this->map($with) || true) &&
284
+ $this->literal('{')
285
+ ) {
286
+ $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
287
+ $atRoot->selector = $selector;
288
+ $atRoot->with = $with;
289
+
290
+ return true;
291
+ }
292
+
293
+ $this->seek($s);
294
+
295
+ if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) {
296
+ $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
297
+ $media->queryList = $mediaQueryList[2];
298
+
299
+ return true;
300
+ }
301
+
302
+ $this->seek($s);
303
+
304
+ if ($this->literal('@mixin') &&
305
+ $this->keyword($mixinName) &&
306
+ ($this->argumentDef($args) || true) &&
307
+ $this->literal('{')
308
+ ) {
309
+ $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
310
+ $mixin->name = $mixinName;
311
+ $mixin->args = $args;
312
+
313
+ return true;
314
+ }
315
+
316
+ $this->seek($s);
317
+
318
+ if ($this->literal('@include') &&
319
+ $this->keyword($mixinName) &&
320
+ ($this->literal('(') &&
321
+ ($this->argValues($argValues) || true) &&
322
+ $this->literal(')') || true) &&
323
+ ($this->end() ||
324
+ $this->literal('{') && $hasBlock = true)
325
+ ) {
326
+ $child = [Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null];
327
+
328
+ if (! empty($hasBlock)) {
329
+ $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
330
+ $include->child = $child;
331
+ } else {
332
+ $this->append($child, $s);
333
+ }
334
+
335
+ return true;
336
+ }
337
+
338
+ $this->seek($s);
339
+
340
+ if ($this->literal('@scssphp-import-once') &&
341
+ $this->valueList($importPath) &&
342
+ $this->end()
343
+ ) {
344
+ $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s);
345
+
346
+ return true;
347
+ }
348
+
349
+ $this->seek($s);
350
+
351
+ if ($this->literal('@import') &&
352
+ $this->valueList($importPath) &&
353
+ $this->end()
354
+ ) {
355
+ $this->append([Type::T_IMPORT, $importPath], $s);
356
+
357
+ return true;
358
+ }
359
+
360
+ $this->seek($s);
361
+
362
+ if ($this->literal('@import') &&
363
+ $this->url($importPath) &&
364
+ $this->end()
365
+ ) {
366
+ $this->append([Type::T_IMPORT, $importPath], $s);
367
+
368
+ return true;
369
+ }
370
+
371
+ $this->seek($s);
372
+
373
+ if ($this->literal('@extend') &&
374
+ $this->selectors($selectors) &&
375
+ $this->end()
376
+ ) {
377
+ // check for '!flag'
378
+ $optional = $this->stripOptionalFlag($selectors);
379
+ $this->append([Type::T_EXTEND, $selectors, $optional], $s);
380
+
381
+ return true;
382
+ }
383
+
384
+ $this->seek($s);
385
+
386
+ if ($this->literal('@function') &&
387
+ $this->keyword($fnName) &&
388
+ $this->argumentDef($args) &&
389
+ $this->literal('{')
390
+ ) {
391
+ $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
392
+ $func->name = $fnName;
393
+ $func->args = $args;
394
+
395
+ return true;
396
+ }
397
+
398
+ $this->seek($s);
399
+
400
+ if ($this->literal('@break') && $this->end()) {
401
+ $this->append([Type::T_BREAK], $s);
402
+
403
+ return true;
404
+ }
405
+
406
+ $this->seek($s);
407
+
408
+ if ($this->literal('@continue') && $this->end()) {
409
+ $this->append([Type::T_CONTINUE], $s);
410
+
411
+ return true;
412
+ }
413
+
414
+ $this->seek($s);
415
+
416
+
417
+ if ($this->literal('@return') && ($this->valueList($retVal) || true) && $this->end()) {
418
+ $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
419
+
420
+ return true;
421
+ }
422
+
423
+ $this->seek($s);
424
+
425
+ if ($this->literal('@each') &&
426
+ $this->genericList($varNames, 'variable', ',', false) &&
427
+ $this->literal('in') &&
428
+ $this->valueList($list) &&
429
+ $this->literal('{')
430
+ ) {
431
+ $each = $this->pushSpecialBlock(Type::T_EACH, $s);
432
+
433
+ foreach ($varNames[2] as $varName) {
434
+ $each->vars[] = $varName[1];
435
+ }
436
+
437
+ $each->list = $list;
438
+
439
+ return true;
440
+ }
441
+
442
+ $this->seek($s);
443
+
444
+ if ($this->literal('@while') &&
445
+ $this->expression($cond) &&
446
+ $this->literal('{')
447
+ ) {
448
+ $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
449
+ $while->cond = $cond;
450
+
451
+ return true;
452
+ }
453
+
454
+ $this->seek($s);
455
+
456
+ if ($this->literal('@for') &&
457
+ $this->variable($varName) &&
458
+ $this->literal('from') &&
459
+ $this->expression($start) &&
460
+ ($this->literal('through') ||
461
+ ($forUntil = true && $this->literal('to'))) &&
462
+ $this->expression($end) &&
463
+ $this->literal('{')
464
+ ) {
465
+ $for = $this->pushSpecialBlock(Type::T_FOR, $s);
466
+ $for->var = $varName[1];
467
+ $for->start = $start;
468
+ $for->end = $end;
469
+ $for->until = isset($forUntil);
470
+
471
+ return true;
472
+ }
473
+
474
+ $this->seek($s);
475
+
476
+ if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) {
477
+ $if = $this->pushSpecialBlock(Type::T_IF, $s);
478
+ $if->cond = $cond;
479
+ $if->cases = [];
480
+
481
+ return true;
482
+ }
483
+
484
+ $this->seek($s);
485
+
486
+ if ($this->literal('@debug') &&
487
+ $this->valueList($value) &&
488
+ $this->end()
489
+ ) {
490
+ $this->append([Type::T_DEBUG, $value], $s);
491
+
492
+ return true;
493
+ }
494
+
495
+ $this->seek($s);
496
+
497
+ if ($this->literal('@warn') &&
498
+ $this->valueList($value) &&
499
+ $this->end()
500
+ ) {
501
+ $this->append([Type::T_WARN, $value], $s);
502
+
503
+ return true;
504
+ }
505
+
506
+ $this->seek($s);
507
+
508
+ if ($this->literal('@error') &&
509
+ $this->valueList($value) &&
510
+ $this->end()
511
+ ) {
512
+ $this->append([Type::T_ERROR, $value], $s);
513
+
514
+ return true;
515
+ }
516
+
517
+ $this->seek($s);
518
+
519
+ if ($this->literal('@content') && $this->end()) {
520
+ $this->append([Type::T_MIXIN_CONTENT], $s);
521
+
522
+ return true;
523
+ }
524
+
525
+ $this->seek($s);
526
+
527
+ $last = $this->last();
528
+
529
+ if (isset($last) && $last[0] === Type::T_IF) {
530
+ list(, $if) = $last;
531
+
532
+ if ($this->literal('@else')) {
533
+ if ($this->literal('{')) {
534
+ $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
535
+ } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) {
536
+ $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
537
+ $else->cond = $cond;
538
+ }
539
+
540
+ if (isset($else)) {
541
+ $else->dontAppend = true;
542
+ $if->cases[] = $else;
543
+
544
+ return true;
545
+ }
546
+ }
547
+
548
+ $this->seek($s);
549
+ }
550
+
551
+ // only retain the first @charset directive encountered
552
+ if ($this->literal('@charset') &&
553
+ $this->valueList($charset) &&
554
+ $this->end()
555
+ ) {
556
+ if (! isset($this->charset)) {
557
+ $statement = [Type::T_CHARSET, $charset];
558
+
559
+ list($line, $column) = $this->getSourcePosition($s);
560
+
561
+ $statement[self::SOURCE_LINE] = $line;
562
+ $statement[self::SOURCE_COLUMN] = $column;
563
+ $statement[self::SOURCE_INDEX] = $this->sourceIndex;
564
+
565
+ $this->charset = $statement;
566
+ }
567
+
568
+ return true;
569
+ }
570
+
571
+ $this->seek($s);
572
+
573
+ // doesn't match built in directive, do generic one
574
+ if ($this->literal('@', false) &&
575
+ $this->keyword($dirName) &&
576
+ ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
577
+ $this->literal('{')
578
+ ) {
579
+ if ($dirName === 'media') {
580
+ $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
581
+ } else {
582
+ $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
583
+ $directive->name = $dirName;
584
+ }
585
+
586
+ if (isset($dirValue)) {
587
+ $directive->value = $dirValue;
588
+ }
589
+
590
+ return true;
591
+ }
592
+
593
+ $this->seek($s);
594
+
595
+ return false;
596
+ }
597
+
598
+ // property shortcut
599
+ // captures most properties before having to parse a selector
600
+ if ($this->keyword($name, false) &&
601
+ $this->literal(': ') &&
602
+ $this->valueList($value) &&
603
+ $this->end()
604
+ ) {
605
+ $name = [Type::T_STRING, '', [$name]];
606
+ $this->append([Type::T_ASSIGN, $name, $value], $s);
607
+
608
+ return true;
609
+ }
610
+
611
+ $this->seek($s);
612
+
613
+ // variable assigns
614
+ if ($this->variable($name) &&
615
+ $this->literal(':') &&
616
+ $this->valueList($value) &&
617
+ $this->end()
618
+ ) {
619
+ // check for '!flag'
620
+ $assignmentFlag = $this->stripAssignmentFlag($value);
621
+ $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlag], $s);
622
+
623
+ return true;
624
+ }
625
+
626
+ $this->seek($s);
627
+
628
+ // misc
629
+ if ($this->literal('-->')) {
630
+ return true;
631
+ }
632
+
633
+ // opening css block
634
+ if ($this->selectors($selectors) && $this->literal('{')) {
635
+ $this->pushBlock($selectors, $s);
636
+
637
+ return true;
638
+ }
639
+
640
+ $this->seek($s);
641
+
642
+ // property assign, or nested assign
643
+ if ($this->propertyName($name) && $this->literal(':')) {
644
+ $foundSomething = false;
645
+
646
+ if ($this->valueList($value)) {
647
+ $this->append([Type::T_ASSIGN, $name, $value], $s);
648
+ $foundSomething = true;
649
+ }
650
+
651
+ if ($this->literal('{')) {
652
+ $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
653
+ $propBlock->prefix = $name;
654
+ $foundSomething = true;
655
+ } elseif ($foundSomething) {
656
+ $foundSomething = $this->end();
657
+ }
658
+
659
+ if ($foundSomething) {
660
+ return true;
661
+ }
662
+ }
663
+
664
+ $this->seek($s);
665
+
666
+ // closing a block
667
+ if ($this->literal('}')) {
668
+ $block = $this->popBlock();
669
+
670
+ if (isset($block->type) && $block->type === Type::T_INCLUDE) {
671
+ $include = $block->child;
672
+ unset($block->child);
673
+ $include[3] = $block;
674
+ $this->append($include, $s);
675
+ } elseif (empty($block->dontAppend)) {
676
+ $type = isset($block->type) ? $block->type : Type::T_BLOCK;
677
+ $this->append([$type, $block], $s);
678
+ }
679
+
680
+ return true;
681
+ }
682
+
683
+ // extra stuff
684
+ if ($this->literal(';') ||
685
+ $this->literal('<!--')
686
+ ) {
687
+ return true;
688
+ }
689
+
690
+ return false;
691
+ }
692
+
693
+ /**
694
+ * Push block onto parse tree
695
+ *
696
+ * @param array $selectors
697
+ * @param integer $pos
698
+ *
699
+ * @return \Leafo\ScssPhp\Block
700
+ */
701
+ protected function pushBlock($selectors, $pos = 0)
702
+ {
703
+ list($line, $column) = $this->getSourcePosition($pos);
704
+
705
+ $b = new Block;
706
+ $b->sourceLine = $line;
707
+ $b->sourceColumn = $column;
708
+ $b->sourceIndex = $this->sourceIndex;
709
+ $b->selectors = $selectors;
710
+ $b->comments = [];
711
+ $b->parent = $this->env;
712
+
713
+ if (! $this->env) {
714
+ $b->children = [];
715
+ } elseif (empty($this->env->children)) {
716
+ $this->env->children = $this->env->comments;
717
+ $b->children = [];
718
+ $this->env->comments = [];
719
+ } else {
720
+ $b->children = $this->env->comments;
721
+ $this->env->comments = [];
722
+ }
723
+
724
+ $this->env = $b;
725
+
726
+ return $b;
727
+ }
728
+
729
+ /**
730
+ * Push special (named) block onto parse tree
731
+ *
732
+ * @param string $type
733
+ * @param integer $pos
734
+ *
735
+ * @return \Leafo\ScssPhp\Block
736
+ */
737
+ protected function pushSpecialBlock($type, $pos)
738
+ {
739
+ $block = $this->pushBlock(null, $pos);
740
+ $block->type = $type;
741
+
742
+ return $block;
743
+ }
744
+
745
+ /**
746
+ * Pop scope and return last block
747
+ *
748
+ * @return \Leafo\ScssPhp\Block
749
+ *
750
+ * @throws \Exception
751
+ */
752
+ protected function popBlock()
753
+ {
754
+ $block = $this->env;
755
+
756
+ if (empty($block->parent)) {
757
+ $this->throwParseError('unexpected }');
758
+ }
759
+
760
+ $this->env = $block->parent;
761
+ unset($block->parent);
762
+
763
+ $comments = $block->comments;
764
+ if (count($comments)) {
765
+ $this->env->comments = $comments;
766
+ unset($block->comments);
767
+ }
768
+
769
+ return $block;
770
+ }
771
+
772
+ /**
773
+ * Peek input stream
774
+ *
775
+ * @param string $regex
776
+ * @param array $out
777
+ * @param integer $from
778
+ *
779
+ * @return integer
780
+ */
781
+ protected function peek($regex, &$out, $from = null)
782
+ {
783
+ if (! isset($from)) {
784
+ $from = $this->count;
785
+ }
786
+
787
+ $r = '/' . $regex . '/' . $this->patternModifiers;
788
+ $result = preg_match($r, $this->buffer, $out, null, $from);
789
+
790
+ return $result;
791
+ }
792
+
793
+ /**
794
+ * Seek to position in input stream (or return current position in input stream)
795
+ *
796
+ * @param integer $where
797
+ *
798
+ * @return integer
799
+ */
800
+ protected function seek($where = null)
801
+ {
802
+ if ($where === null) {
803
+ return $this->count;
804
+ }
805
+
806
+ $this->count = $where;
807
+
808
+ return true;
809
+ }
810
+
811
+ /**
812
+ * Match string looking for either ending delim, escape, or string interpolation
813
+ *
814
+ * {@internal This is a workaround for preg_match's 250K string match limit. }}
815
+ *
816
+ * @param array $m Matches (passed by reference)
817
+ * @param string $delim Delimeter
818
+ *
819
+ * @return boolean True if match; false otherwise
820
+ */
821
+ protected function matchString(&$m, $delim)
822
+ {
823
+ $token = null;
824
+
825
+ $end = strlen($this->buffer);
826
+
827
+ // look for either ending delim, escape, or string interpolation
828
+ foreach (['#{', '\\', $delim] as $lookahead) {
829
+ $pos = strpos($this->buffer, $lookahead, $this->count);
830
+
831
+ if ($pos !== false && $pos < $end) {
832
+ $end = $pos;
833
+ $token = $lookahead;
834
+ }
835
+ }
836
+
837
+ if (! isset($token)) {
838
+ return false;
839
+ }
840
+
841
+ $match = substr($this->buffer, $this->count, $end - $this->count);
842
+ $m = [
843
+ $match . $token,
844
+ $match,
845
+ $token
846
+ ];
847
+ $this->count = $end + strlen($token);
848
+
849
+ return true;
850
+ }
851
+
852
+ /**
853
+ * Try to match something on head of buffer
854
+ *
855
+ * @param string $regex
856
+ * @param array $out
857
+ * @param boolean $eatWhitespace
858
+ *
859
+ * @return boolean
860
+ */
861
+ protected function match($regex, &$out, $eatWhitespace = null)
862
+ {
863
+ if (! isset($eatWhitespace)) {
864
+ $eatWhitespace = $this->eatWhiteDefault;
865
+ }
866
+
867
+ $r = '/' . $regex . '/' . $this->patternModifiers;
868
+
869
+ if (preg_match($r, $this->buffer, $out, null, $this->count)) {
870
+ $this->count += strlen($out[0]);
871
+
872
+ if ($eatWhitespace) {
873
+ $this->whitespace();
874
+ }
875
+
876
+ return true;
877
+ }
878
+
879
+ return false;
880
+ }
881
+
882
+ /**
883
+ * Match literal string
884
+ *
885
+ * @param string $what
886
+ * @param boolean $eatWhitespace
887
+ *
888
+ * @return boolean
889
+ */
890
+ protected function literal($what, $eatWhitespace = null)
891
+ {
892
+ if (! isset($eatWhitespace)) {
893
+ $eatWhitespace = $this->eatWhiteDefault;
894
+ }
895
+
896
+ $len = strlen($what);
897
+
898
+ if (substr($this->buffer, $this->count, $len) === $what) {
899
+ $this->count += $len;
900
+
901
+ if ($eatWhitespace) {
902
+ $this->whitespace();
903
+ }
904
+
905
+ return true;
906
+ }
907
+
908
+ return false;
909
+ }
910
+
911
+ /**
912
+ * Match some whitespace
913
+ *
914
+ * @return boolean
915
+ */
916
+ protected function whitespace()
917
+ {
918
+ $gotWhite = false;
919
+
920
+ while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
921
+ if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
922
+ $this->appendComment([Type::T_COMMENT, $m[1]]);
923
+
924
+ $this->commentsSeen[$this->count] = true;
925
+ }
926
+
927
+ $this->count += strlen($m[0]);
928
+ $gotWhite = true;
929
+ }
930
+
931
+ return $gotWhite;
932
+ }
933
+
934
+ /**
935
+ * Append comment to current block
936
+ *
937
+ * @param array $comment
938
+ */
939
+ protected function appendComment($comment)
940
+ {
941
+ $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
942
+
943
+ $this->env->comments[] = $comment;
944
+ }
945
+
946
+ /**
947
+ * Append statement to current block
948
+ *
949
+ * @param array $statement
950
+ * @param integer $pos
951
+ */
952
+ protected function append($statement, $pos = null)
953
+ {
954
+ if ($pos !== null) {
955
+ list($line, $column) = $this->getSourcePosition($pos);
956
+
957
+ $statement[self::SOURCE_LINE] = $line;
958
+ $statement[self::SOURCE_COLUMN] = $column;
959
+ $statement[self::SOURCE_INDEX] = $this->sourceIndex;
960
+ }
961
+
962
+ $this->env->children[] = $statement;
963
+
964
+ $comments = $this->env->comments;
965
+
966
+ if (count($comments)) {
967
+ $this->env->children = array_merge($this->env->children, $comments);
968
+ $this->env->comments = [];
969
+ }
970
+ }
971
+
972
+ /**
973
+ * Returns last child was appended
974
+ *
975
+ * @return array|null
976
+ */
977
+ protected function last()
978
+ {
979
+ $i = count($this->env->children) - 1;
980
+
981
+ if (isset($this->env->children[$i])) {
982
+ return $this->env->children[$i];
983
+ }
984
+ }
985
+
986
+ /**
987
+ * Parse media query list
988
+ *
989
+ * @param array $out
990
+ *
991
+ * @return boolean
992
+ */
993
+ protected function mediaQueryList(&$out)
994
+ {
995
+ return $this->genericList($out, 'mediaQuery', ',', false);
996
+ }
997
+
998
+ /**
999
+ * Parse media query
1000
+ *
1001
+ * @param array $out
1002
+ *
1003
+ * @return boolean
1004
+ */
1005
+ protected function mediaQuery(&$out)
1006
+ {
1007
+ $expressions = null;
1008
+ $parts = [];
1009
+
1010
+ if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) &&
1011
+ $this->mixedKeyword($mediaType)
1012
+ ) {
1013
+ $prop = [Type::T_MEDIA_TYPE];
1014
+
1015
+ if (isset($only)) {
1016
+ $prop[] = [Type::T_KEYWORD, 'only'];
1017
+ }
1018
+
1019
+ if (isset($not)) {
1020
+ $prop[] = [Type::T_KEYWORD, 'not'];
1021
+ }
1022
+
1023
+ $media = [Type::T_LIST, '', []];
1024
+
1025
+ foreach ((array) $mediaType as $type) {
1026
+ if (is_array($type)) {
1027
+ $media[2][] = $type;
1028
+ } else {
1029
+ $media[2][] = [Type::T_KEYWORD, $type];
1030
+ }
1031
+ }
1032
+
1033
+ $prop[] = $media;
1034
+ $parts[] = $prop;
1035
+ }
1036
+
1037
+ if (empty($parts) || $this->literal('and')) {
1038
+ $this->genericList($expressions, 'mediaExpression', 'and', false);
1039
+
1040
+ if (is_array($expressions)) {
1041
+ $parts = array_merge($parts, $expressions[2]);
1042
+ }
1043
+ }
1044
+
1045
+ $out = $parts;
1046
+
1047
+ return true;
1048
+ }
1049
+
1050
+ /**
1051
+ * Parse media expression
1052
+ *
1053
+ * @param array $out
1054
+ *
1055
+ * @return boolean
1056
+ */
1057
+ protected function mediaExpression(&$out)
1058
+ {
1059
+ $s = $this->seek();
1060
+ $value = null;
1061
+
1062
+ if ($this->literal('(') &&
1063
+ $this->expression($feature) &&
1064
+ ($this->literal(':') && $this->expression($value) || true) &&
1065
+ $this->literal(')')
1066
+ ) {
1067
+ $out = [Type::T_MEDIA_EXPRESSION, $feature];
1068
+
1069
+ if ($value) {
1070
+ $out[] = $value;
1071
+ }
1072
+
1073
+ return true;
1074
+ }
1075
+
1076
+ $this->seek($s);
1077
+
1078
+ return false;
1079
+ }
1080
+
1081
+ /**
1082
+ * Parse argument values
1083
+ *
1084
+ * @param array $out
1085
+ *
1086
+ * @return boolean
1087
+ */
1088
+ protected function argValues(&$out)
1089
+ {
1090
+ if ($this->genericList($list, 'argValue', ',', false)) {
1091
+ $out = $list[2];
1092
+
1093
+ return true;
1094
+ }
1095
+
1096
+ return false;
1097
+ }
1098
+
1099
+ /**
1100
+ * Parse argument value
1101
+ *
1102
+ * @param array $out
1103
+ *
1104
+ * @return boolean
1105
+ */
1106
+ protected function argValue(&$out)
1107
+ {
1108
+ $s = $this->seek();
1109
+
1110
+ $keyword = null;
1111
+
1112
+ if (! $this->variable($keyword) || ! $this->literal(':')) {
1113
+ $this->seek($s);
1114
+ $keyword = null;
1115
+ }
1116
+
1117
+ if ($this->genericList($value, 'expression')) {
1118
+ $out = [$keyword, $value, false];
1119
+ $s = $this->seek();
1120
+
1121
+ if ($this->literal('...')) {
1122
+ $out[2] = true;
1123
+ } else {
1124
+ $this->seek($s);
1125
+ }
1126
+
1127
+ return true;
1128
+ }
1129
+
1130
+ return false;
1131
+ }
1132
+
1133
+ /**
1134
+ * Parse comma separated value list
1135
+ *
1136
+ * @param string $out
1137
+ *
1138
+ * @return boolean
1139
+ */
1140
+ protected function valueList(&$out)
1141
+ {
1142
+ return $this->genericList($out, 'spaceList', ',');
1143
+ }
1144
+
1145
+ /**
1146
+ * Parse space separated value list
1147
+ *
1148
+ * @param array $out
1149
+ *
1150
+ * @return boolean
1151
+ */
1152
+ protected function spaceList(&$out)
1153
+ {
1154
+ return $this->genericList($out, 'expression');
1155
+ }
1156
+
1157
+ /**
1158
+ * Parse generic list
1159
+ *
1160
+ * @param array $out
1161
+ * @param callable $parseItem
1162
+ * @param string $delim
1163
+ * @param boolean $flatten
1164
+ *
1165
+ * @return boolean
1166
+ */
1167
+ protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
1168
+ {
1169
+ $s = $this->seek();
1170
+ $items = [];
1171
+
1172
+ while ($this->$parseItem($value)) {
1173
+ $items[] = $value;
1174
+
1175
+ if ($delim) {
1176
+ if (! $this->literal($delim)) {
1177
+ break;
1178
+ }
1179
+ }
1180
+ }
1181
+
1182
+ if (count($items) === 0) {
1183
+ $this->seek($s);
1184
+
1185
+ return false;
1186
+ }
1187
+
1188
+ if ($flatten && count($items) === 1) {
1189
+ $out = $items[0];
1190
+ } else {
1191
+ $out = [Type::T_LIST, $delim, $items];
1192
+ }
1193
+
1194
+ return true;
1195
+ }
1196
+
1197
+ /**
1198
+ * Parse expression
1199
+ *
1200
+ * @param array $out
1201
+ *
1202
+ * @return boolean
1203
+ */
1204
+ protected function expression(&$out)
1205
+ {
1206
+ $s = $this->seek();
1207
+
1208
+ if ($this->literal('(')) {
1209
+ if ($this->literal(')')) {
1210
+ $out = [Type::T_LIST, '', []];
1211
+
1212
+ return true;
1213
+ }
1214
+
1215
+ if ($this->valueList($out) && $this->literal(')') && $out[0] === Type::T_LIST) {
1216
+ return true;
1217
+ }
1218
+
1219
+ $this->seek($s);
1220
+
1221
+ if ($this->map($out)) {
1222
+ return true;
1223
+ }
1224
+
1225
+ $this->seek($s);
1226
+ }
1227
+
1228
+ if ($this->value($lhs)) {
1229
+ $out = $this->expHelper($lhs, 0);
1230
+
1231
+ return true;
1232
+ }
1233
+
1234
+ return false;
1235
+ }
1236
+
1237
+ /**
1238
+ * Parse left-hand side of subexpression
1239
+ *
1240
+ * @param array $lhs
1241
+ * @param integer $minP
1242
+ *
1243
+ * @return array
1244
+ */
1245
+ protected function expHelper($lhs, $minP)
1246
+ {
1247
+ $operators = self::$operatorPattern;
1248
+
1249
+ $ss = $this->seek();
1250
+ $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1251
+ ctype_space($this->buffer[$this->count - 1]);
1252
+
1253
+ while ($this->match($operators, $m, false) && self::$precedence[$m[1]] >= $minP) {
1254
+ $whiteAfter = isset($this->buffer[$this->count]) &&
1255
+ ctype_space($this->buffer[$this->count]);
1256
+ $varAfter = isset($this->buffer[$this->count]) &&
1257
+ $this->buffer[$this->count] === '$';
1258
+
1259
+ $this->whitespace();
1260
+
1261
+ $op = $m[1];
1262
+
1263
+ // don't turn negative numbers into expressions
1264
+ if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) {
1265
+ break;
1266
+ }
1267
+
1268
+ if (! $this->value($rhs)) {
1269
+ break;
1270
+ }
1271
+
1272
+ // peek and see if rhs belongs to next operator
1273
+ if ($this->peek($operators, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
1274
+ $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
1275
+ }
1276
+
1277
+ $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
1278
+ $ss = $this->seek();
1279
+ $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1280
+ ctype_space($this->buffer[$this->count - 1]);
1281
+ }
1282
+
1283
+ $this->seek($ss);
1284
+
1285
+ return $lhs;
1286
+ }
1287
+
1288
+ /**
1289
+ * Parse value
1290
+ *
1291
+ * @param array $out
1292
+ *
1293
+ * @return boolean
1294
+ */
1295
+ protected function value(&$out)
1296
+ {
1297
+ $s = $this->seek();
1298
+
1299
+ if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) {
1300
+ $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1301
+
1302
+ return true;
1303
+ }
1304
+
1305
+ $this->seek($s);
1306
+
1307
+ if ($this->literal('not', false) && $this->parenValue($inner)) {
1308
+ $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1309
+
1310
+ return true;
1311
+ }
1312
+
1313
+ $this->seek($s);
1314
+
1315
+ if ($this->literal('+') && $this->value($inner)) {
1316
+ $out = [Type::T_UNARY, '+', $inner, $this->inParens];
1317
+
1318
+ return true;
1319
+ }
1320
+
1321
+ $this->seek($s);
1322
+
1323
+ // negation
1324
+ if ($this->literal('-', false) &&
1325
+ ($this->variable($inner) ||
1326
+ $this->unit($inner) ||
1327
+ $this->parenValue($inner))
1328
+ ) {
1329
+ $out = [Type::T_UNARY, '-', $inner, $this->inParens];
1330
+
1331
+ return true;
1332
+ }
1333
+
1334
+ $this->seek($s);
1335
+
1336
+ if ($this->parenValue($out) ||
1337
+ $this->interpolation($out) ||
1338
+ $this->variable($out) ||
1339
+ $this->color($out) ||
1340
+ $this->unit($out) ||
1341
+ $this->string($out) ||
1342
+ $this->func($out) ||
1343
+ $this->progid($out)
1344
+ ) {
1345
+ return true;
1346
+ }
1347
+
1348
+ if ($this->keyword($keyword)) {
1349
+ if ($keyword === 'null') {
1350
+ $out = [Type::T_NULL];
1351
+ } else {
1352
+ $out = [Type::T_KEYWORD, $keyword];
1353
+ }
1354
+
1355
+ return true;
1356
+ }
1357
+
1358
+ return false;
1359
+ }
1360
+
1361
+ /**
1362
+ * Parse parenthesized value
1363
+ *
1364
+ * @param array $out
1365
+ *
1366
+ * @return boolean
1367
+ */
1368
+ protected function parenValue(&$out)
1369
+ {
1370
+ $s = $this->seek();
1371
+
1372
+ $inParens = $this->inParens;
1373
+
1374
+ if ($this->literal('(')) {
1375
+ if ($this->literal(')')) {
1376
+ $out = [Type::T_LIST, '', []];
1377
+
1378
+ return true;
1379
+ }
1380
+
1381
+ $this->inParens = true;
1382
+
1383
+ if ($this->expression($exp) && $this->literal(')')) {
1384
+ $out = $exp;
1385
+ $this->inParens = $inParens;
1386
+
1387
+ return true;
1388
+ }
1389
+ }
1390
+
1391
+ $this->inParens = $inParens;
1392
+ $this->seek($s);
1393
+
1394
+ return false;
1395
+ }
1396
+
1397
+ /**
1398
+ * Parse "progid:"
1399
+ *
1400
+ * @param array $out
1401
+ *
1402
+ * @return boolean
1403
+ */
1404
+ protected function progid(&$out)
1405
+ {
1406
+ $s = $this->seek();
1407
+
1408
+ if ($this->literal('progid:', false) &&
1409
+ $this->openString('(', $fn) &&
1410
+ $this->literal('(')
1411
+ ) {
1412
+ $this->openString(')', $args, '(');
1413
+
1414
+ if ($this->literal(')')) {
1415
+ $out = [Type::T_STRING, '', [
1416
+ 'progid:', $fn, '(', $args, ')'
1417
+ ]];
1418
+
1419
+ return true;
1420
+ }
1421
+ }
1422
+
1423
+ $this->seek($s);
1424
+
1425
+ return false;
1426
+ }
1427
+
1428
+ /**
1429
+ * Parse function call
1430
+ *
1431
+ * @param array $out
1432
+ *
1433
+ * @return boolean
1434
+ */
1435
+ protected function func(&$func)
1436
+ {
1437
+ $s = $this->seek();
1438
+
1439
+ if ($this->keyword($name, false) &&
1440
+ $this->literal('(')
1441
+ ) {
1442
+ if ($name === 'alpha' && $this->argumentList($args)) {
1443
+ $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]];
1444
+
1445
+ return true;
1446
+ }
1447
+
1448
+ if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
1449
+ $ss = $this->seek();
1450
+
1451
+ if ($this->argValues($args) && $this->literal(')')) {
1452
+ $func = [Type::T_FUNCTION_CALL, $name, $args];
1453
+
1454
+ return true;
1455
+ }
1456
+
1457
+ $this->seek($ss);
1458
+ }
1459
+
1460
+ if (($this->openString(')', $str, '(') || true) &&
1461
+ $this->literal(')')
1462
+ ) {
1463
+ $args = [];
1464
+
1465
+ if (! empty($str)) {
1466
+ $args[] = [null, [Type::T_STRING, '', [$str]]];
1467
+ }
1468
+
1469
+ $func = [Type::T_FUNCTION_CALL, $name, $args];
1470
+
1471
+ return true;
1472
+ }
1473
+ }
1474
+
1475
+ $this->seek($s);
1476
+
1477
+ return false;
1478
+ }
1479
+
1480
+ /**
1481
+ * Parse function call argument list
1482
+ *
1483
+ * @param array $out
1484
+ *
1485
+ * @return boolean
1486
+ */
1487
+ protected function argumentList(&$out)
1488
+ {
1489
+ $s = $this->seek();
1490
+ $this->literal('(');
1491
+
1492
+ $args = [];
1493
+
1494
+ while ($this->keyword($var)) {
1495
+ if ($this->literal('=') && $this->expression($exp)) {
1496
+ $args[] = [Type::T_STRING, '', [$var . '=']];
1497
+ $arg = $exp;
1498
+ } else {
1499
+ break;
1500
+ }
1501
+
1502
+ $args[] = $arg;
1503
+
1504
+ if (! $this->literal(',')) {
1505
+ break;
1506
+ }
1507
+
1508
+ $args[] = [Type::T_STRING, '', [', ']];
1509
+ }
1510
+
1511
+ if (! $this->literal(')') || ! count($args)) {
1512
+ $this->seek($s);
1513
+
1514
+ return false;
1515
+ }
1516
+
1517
+ $out = $args;
1518
+
1519
+ return true;
1520
+ }
1521
+
1522
+ /**
1523
+ * Parse mixin/function definition argument list
1524
+ *
1525
+ * @param array $out
1526
+ *
1527
+ * @return boolean
1528
+ */
1529
+ protected function argumentDef(&$out)
1530
+ {
1531
+ $s = $this->seek();
1532
+ $this->literal('(');
1533
+
1534
+ $args = [];
1535
+
1536
+ while ($this->variable($var)) {
1537
+ $arg = [$var[1], null, false];
1538
+
1539
+ $ss = $this->seek();
1540
+
1541
+ if ($this->literal(':') && $this->genericList($defaultVal, 'expression')) {
1542
+ $arg[1] = $defaultVal;
1543
+ } else {
1544
+ $this->seek($ss);
1545
+ }
1546
+
1547
+ $ss = $this->seek();
1548
+
1549
+ if ($this->literal('...')) {
1550
+ $sss = $this->seek();
1551
+
1552
+ if (! $this->literal(')')) {
1553
+ $this->throwParseError('... has to be after the final argument');
1554
+ }
1555
+
1556
+ $arg[2] = true;
1557
+ $this->seek($sss);
1558
+ } else {
1559
+ $this->seek($ss);
1560
+ }
1561
+
1562
+ $args[] = $arg;
1563
+
1564
+ if (! $this->literal(',')) {
1565
+ break;
1566
+ }
1567
+ }
1568
+
1569
+ if (! $this->literal(')')) {
1570
+ $this->seek($s);
1571
+
1572
+ return false;
1573
+ }
1574
+
1575
+ $out = $args;
1576
+
1577
+ return true;
1578
+ }
1579
+
1580
+ /**
1581
+ * Parse map
1582
+ *
1583
+ * @param array $out
1584
+ *
1585
+ * @return boolean
1586
+ */
1587
+ protected function map(&$out)
1588
+ {
1589
+ $s = $this->seek();
1590
+
1591
+ if (! $this->literal('(')) {
1592
+ return false;
1593
+ }
1594
+
1595
+ $keys = [];
1596
+ $values = [];
1597
+
1598
+ while ($this->genericList($key, 'expression') && $this->literal(':') &&
1599
+ $this->genericList($value, 'expression')
1600
+ ) {
1601
+ $keys[] = $key;
1602
+ $values[] = $value;
1603
+
1604
+ if (! $this->literal(',')) {
1605
+ break;
1606
+ }
1607
+ }
1608
+
1609
+ if (! count($keys) || ! $this->literal(')')) {
1610
+ $this->seek($s);
1611
+
1612
+ return false;
1613
+ }
1614
+
1615
+ $out = [Type::T_MAP, $keys, $values];
1616
+
1617
+ return true;
1618
+ }
1619
+
1620
+ /**
1621
+ * Parse color
1622
+ *
1623
+ * @param array $out
1624
+ *
1625
+ * @return boolean
1626
+ */
1627
+ protected function color(&$out)
1628
+ {
1629
+ $color = [Type::T_COLOR];
1630
+
1631
+ if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
1632
+ if (isset($m[3])) {
1633
+ $num = hexdec($m[3]);
1634
+
1635
+ foreach ([3, 2, 1] as $i) {
1636
+ $t = $num & 0xf;
1637
+ $color[$i] = $t << 4 | $t;
1638
+ $num >>= 4;
1639
+ }
1640
+ } else {
1641
+ $num = hexdec($m[2]);
1642
+
1643
+ foreach ([3, 2, 1] as $i) {
1644
+ $color[$i] = $num & 0xff;
1645
+ $num >>= 8;
1646
+ }
1647
+ }
1648
+
1649
+ $out = $color;
1650
+
1651
+ return true;
1652
+ }
1653
+
1654
+ return false;
1655
+ }
1656
+
1657
+ /**
1658
+ * Parse number with unit
1659
+ *
1660
+ * @param array $out
1661
+ *
1662
+ * @return boolean
1663
+ */
1664
+ protected function unit(&$unit)
1665
+ {
1666
+ if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
1667
+ $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
1668
+
1669
+ return true;
1670
+ }
1671
+
1672
+ return false;
1673
+ }
1674
+
1675
+ /**
1676
+ * Parse string
1677
+ *
1678
+ * @param array $out
1679
+ *
1680
+ * @return boolean
1681
+ */
1682
+ protected function string(&$out)
1683
+ {
1684
+ $s = $this->seek();
1685
+
1686
+ if ($this->literal('"', false)) {
1687
+ $delim = '"';
1688
+ } elseif ($this->literal("'", false)) {
1689
+ $delim = "'";
1690
+ } else {
1691
+ return false;
1692
+ }
1693
+
1694
+ $content = [];
1695
+ $oldWhite = $this->eatWhiteDefault;
1696
+ $this->eatWhiteDefault = false;
1697
+ $hasInterpolation = false;
1698
+
1699
+ while ($this->matchString($m, $delim)) {
1700
+ if ($m[1] !== '') {
1701
+ $content[] = $m[1];
1702
+ }
1703
+
1704
+ if ($m[2] === '#{') {
1705
+ $this->count -= strlen($m[2]);
1706
+
1707
+ if ($this->interpolation($inter, false)) {
1708
+ $content[] = $inter;
1709
+ $hasInterpolation = true;
1710
+ } else {
1711
+ $this->count += strlen($m[2]);
1712
+ $content[] = '#{'; // ignore it
1713
+ }
1714
+ } elseif ($m[2] === '\\') {
1715
+ if ($this->literal('"', false)) {
1716
+ $content[] = $m[2] . '"';
1717
+ } elseif ($this->literal("'", false)) {
1718
+ $content[] = $m[2] . "'";
1719
+ } else {
1720
+ $content[] = $m[2];
1721
+ }
1722
+ } else {
1723
+ $this->count -= strlen($delim);
1724
+ break; // delim
1725
+ }
1726
+ }
1727
+
1728
+ $this->eatWhiteDefault = $oldWhite;
1729
+
1730
+ if ($this->literal($delim)) {
1731
+ if ($hasInterpolation) {
1732
+ $delim = '"';
1733
+
1734
+ foreach ($content as &$string) {
1735
+ if ($string === "\\'") {
1736
+ $string = "'";
1737
+ } elseif ($string === '\\"') {
1738
+ $string = '"';
1739
+ }
1740
+ }
1741
+ }
1742
+
1743
+ $out = [Type::T_STRING, $delim, $content];
1744
+
1745
+ return true;
1746
+ }
1747
+
1748
+ $this->seek($s);
1749
+
1750
+ return false;
1751
+ }
1752
+
1753
+ /**
1754
+ * Parse keyword or interpolation
1755
+ *
1756
+ * @param array $out
1757
+ *
1758
+ * @return boolean
1759
+ */
1760
+ protected function mixedKeyword(&$out)
1761
+ {
1762
+ $parts = [];
1763
+
1764
+ $oldWhite = $this->eatWhiteDefault;
1765
+ $this->eatWhiteDefault = false;
1766
+
1767
+ for (;;) {
1768
+ if ($this->keyword($key)) {
1769
+ $parts[] = $key;
1770
+ continue;
1771
+ }
1772
+
1773
+ if ($this->interpolation($inter)) {
1774
+ $parts[] = $inter;
1775
+ continue;
1776
+ }
1777
+
1778
+ break;
1779
+ }
1780
+
1781
+ $this->eatWhiteDefault = $oldWhite;
1782
+
1783
+ if (count($parts) === 0) {
1784
+ return false;
1785
+ }
1786
+
1787
+ if ($this->eatWhiteDefault) {
1788
+ $this->whitespace();
1789
+ }
1790
+
1791
+ $out = $parts;
1792
+
1793
+ return true;
1794
+ }
1795
+
1796
+ /**
1797
+ * Parse an unbounded string stopped by $end
1798
+ *
1799
+ * @param string $end
1800
+ * @param array $out
1801
+ * @param string $nestingOpen
1802
+ *
1803
+ * @return boolean
1804
+ */
1805
+ protected function openString($end, &$out, $nestingOpen = null)
1806
+ {
1807
+ $oldWhite = $this->eatWhiteDefault;
1808
+ $this->eatWhiteDefault = false;
1809
+
1810
+ $patt = '(.*?)([\'"]|#\{|' . $this->pregQuote($end) . '|' . self::$commentPattern . ')';
1811
+
1812
+ $nestingLevel = 0;
1813
+
1814
+ $content = [];
1815
+
1816
+ while ($this->match($patt, $m, false)) {
1817
+ if (isset($m[1]) && $m[1] !== '') {
1818
+ $content[] = $m[1];
1819
+
1820
+ if ($nestingOpen) {
1821
+ $nestingLevel += substr_count($m[1], $nestingOpen);
1822
+ }
1823
+ }
1824
+
1825
+ $tok = $m[2];
1826
+
1827
+ $this->count-= strlen($tok);
1828
+
1829
+ if ($tok === $end && ! $nestingLevel--) {
1830
+ break;
1831
+ }
1832
+
1833
+ if (($tok === "'" || $tok === '"') && $this->string($str)) {
1834
+ $content[] = $str;
1835
+ continue;
1836
+ }
1837
+
1838
+ if ($tok === '#{' && $this->interpolation($inter)) {
1839
+ $content[] = $inter;
1840
+ continue;
1841
+ }
1842
+
1843
+ $content[] = $tok;
1844
+ $this->count+= strlen($tok);
1845
+ }
1846
+
1847
+ $this->eatWhiteDefault = $oldWhite;
1848
+
1849
+ if (count($content) === 0) {
1850
+ return false;
1851
+ }
1852
+
1853
+ // trim the end
1854
+ if (is_string(end($content))) {
1855
+ $content[count($content) - 1] = rtrim(end($content));
1856
+ }
1857
+
1858
+ $out = [Type::T_STRING, '', $content];
1859
+
1860
+ return true;
1861
+ }
1862
+
1863
+ /**
1864
+ * Parser interpolation
1865
+ *
1866
+ * @param array $out
1867
+ * @param boolean $lookWhite save information about whitespace before and after
1868
+ *
1869
+ * @return boolean
1870
+ */
1871
+ protected function interpolation(&$out, $lookWhite = true)
1872
+ {
1873
+ $oldWhite = $this->eatWhiteDefault;
1874
+ $this->eatWhiteDefault = true;
1875
+
1876
+ $s = $this->seek();
1877
+
1878
+ if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) {
1879
+ if ($lookWhite) {
1880
+ $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
1881
+ $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
1882
+ } else {
1883
+ $left = $right = false;
1884
+ }
1885
+
1886
+ $out = [Type::T_INTERPOLATE, $value, $left, $right];
1887
+ $this->eatWhiteDefault = $oldWhite;
1888
+
1889
+ if ($this->eatWhiteDefault) {
1890
+ $this->whitespace();
1891
+ }
1892
+
1893
+ return true;
1894
+ }
1895
+
1896
+ $this->seek($s);
1897
+ $this->eatWhiteDefault = $oldWhite;
1898
+
1899
+ return false;
1900
+ }
1901
+
1902
+ /**
1903
+ * Parse property name (as an array of parts or a string)
1904
+ *
1905
+ * @param array $out
1906
+ *
1907
+ * @return boolean
1908
+ */
1909
+ protected function propertyName(&$out)
1910
+ {
1911
+ $parts = [];
1912
+
1913
+ $oldWhite = $this->eatWhiteDefault;
1914
+ $this->eatWhiteDefault = false;
1915
+
1916
+ for (;;) {
1917
+ if ($this->interpolation($inter)) {
1918
+ $parts[] = $inter;
1919
+ continue;
1920
+ }
1921
+
1922
+ if ($this->keyword($text)) {
1923
+ $parts[] = $text;
1924
+ continue;
1925
+ }
1926
+
1927
+ if (count($parts) === 0 && $this->match('[:.#]', $m, false)) {
1928
+ // css hacks
1929
+ $parts[] = $m[0];
1930
+ continue;
1931
+ }
1932
+
1933
+ break;
1934
+ }
1935
+
1936
+ $this->eatWhiteDefault = $oldWhite;
1937
+
1938
+ if (count($parts) === 0) {
1939
+ return false;
1940
+ }
1941
+
1942
+ // match comment hack
1943
+ if (preg_match(
1944
+ self::$whitePattern,
1945
+ $this->buffer,
1946
+ $m,
1947
+ null,
1948
+ $this->count
1949
+ )) {
1950
+ if (! empty($m[0])) {
1951
+ $parts[] = $m[0];
1952
+ $this->count += strlen($m[0]);
1953
+ }
1954
+ }
1955
+
1956
+ $this->whitespace(); // get any extra whitespace
1957
+
1958
+ $out = [Type::T_STRING, '', $parts];
1959
+
1960
+ return true;
1961
+ }
1962
+
1963
+ /**
1964
+ * Parse comma separated selector list
1965
+ *
1966
+ * @param array $out
1967
+ *
1968
+ * @return boolean
1969
+ */
1970
+ protected function selectors(&$out)
1971
+ {
1972
+ $s = $this->seek();
1973
+ $selectors = [];
1974
+
1975
+ while ($this->selector($sel)) {
1976
+ $selectors[] = $sel;
1977
+
1978
+ if (! $this->literal(',')) {
1979
+ break;
1980
+ }
1981
+
1982
+ while ($this->literal(',')) {
1983
+ ; // ignore extra
1984
+ }
1985
+ }
1986
+
1987
+ if (count($selectors) === 0) {
1988
+ $this->seek($s);
1989
+
1990
+ return false;
1991
+ }
1992
+
1993
+ $out = $selectors;
1994
+
1995
+ return true;
1996
+ }
1997
+
1998
+ /**
1999
+ * Parse whitespace separated selector list
2000
+ *
2001
+ * @param array $out
2002
+ *
2003
+ * @return boolean
2004
+ */
2005
+ protected function selector(&$out)
2006
+ {
2007
+ $selector = [];
2008
+
2009
+ for (;;) {
2010
+ if ($this->match('[>+~]+', $m)) {
2011
+ $selector[] = [$m[0]];
2012
+ continue;
2013
+ }
2014
+
2015
+ if ($this->selectorSingle($part)) {
2016
+ $selector[] = $part;
2017
+ $this->match('\s+', $m);
2018
+ continue;
2019
+ }
2020
+
2021
+ if ($this->match('\/[^\/]+\/', $m)) {
2022
+ $selector[] = [$m[0]];
2023
+ continue;
2024
+ }
2025
+
2026
+ break;
2027
+ }
2028
+
2029
+ if (count($selector) === 0) {
2030
+ return false;
2031
+ }
2032
+
2033
+ $out = $selector;
2034
+ return true;
2035
+ }
2036
+
2037
+ /**
2038
+ * Parse the parts that make up a selector
2039
+ *
2040
+ * {@internal
2041
+ * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
2042
+ * }}
2043
+ *
2044
+ * @param array $out
2045
+ *
2046
+ * @return boolean
2047
+ */
2048
+ protected function selectorSingle(&$out)
2049
+ {
2050
+ $oldWhite = $this->eatWhiteDefault;
2051
+ $this->eatWhiteDefault = false;
2052
+
2053
+ $parts = [];
2054
+
2055
+ if ($this->literal('*', false)) {
2056
+ $parts[] = '*';
2057
+ }
2058
+
2059
+ for (;;) {
2060
+ // see if we can stop early
2061
+ if ($this->match('\s*[{,]', $m)) {
2062
+ $this->count--;
2063
+ break;
2064
+ }
2065
+
2066
+ $s = $this->seek();
2067
+
2068
+ // self
2069
+ if ($this->literal('&', false)) {
2070
+ $parts[] = Compiler::$selfSelector;
2071
+ continue;
2072
+ }
2073
+
2074
+ if ($this->literal('.', false)) {
2075
+ $parts[] = '.';
2076
+ continue;
2077
+ }
2078
+
2079
+ if ($this->literal('|', false)) {
2080
+ $parts[] = '|';
2081
+ continue;
2082
+ }
2083
+
2084
+ if ($this->match('\\\\\S', $m)) {
2085
+ $parts[] = $m[0];
2086
+ continue;
2087
+ }
2088
+
2089
+ // for keyframes
2090
+ if ($this->unit($unit)) {
2091
+ $parts[] = $unit;
2092
+ continue;
2093
+ }
2094
+
2095
+ if ($this->keyword($name)) {
2096
+ $parts[] = $name;
2097
+ continue;
2098
+ }
2099
+
2100
+ if ($this->interpolation($inter)) {
2101
+ $parts[] = $inter;
2102
+ continue;
2103
+ }
2104
+
2105
+ if ($this->literal('%', false) && $this->placeholder($placeholder)) {
2106
+ $parts[] = '%';
2107
+ $parts[] = $placeholder;
2108
+ continue;
2109
+ }
2110
+
2111
+ if ($this->literal('#', false)) {
2112
+ $parts[] = '#';
2113
+ continue;
2114
+ }
2115
+
2116
+ // a pseudo selector
2117
+ if ($this->match('::?', $m) && $this->mixedKeyword($nameParts)) {
2118
+ $parts[] = $m[0];
2119
+
2120
+ foreach ($nameParts as $sub) {
2121
+ $parts[] = $sub;
2122
+ }
2123
+
2124
+ $ss = $this->seek();
2125
+
2126
+ if ($this->literal('(') &&
2127
+ ($this->openString(')', $str, '(') || true) &&
2128
+ $this->literal(')')
2129
+ ) {
2130
+ $parts[] = '(';
2131
+
2132
+ if (! empty($str)) {
2133
+ $parts[] = $str;
2134
+ }
2135
+
2136
+ $parts[] = ')';
2137
+ } else {
2138
+ $this->seek($ss);
2139
+ }
2140
+
2141
+ continue;
2142
+ }
2143
+
2144
+ $this->seek($s);
2145
+
2146
+ // attribute selector
2147
+ if ($this->literal('[') &&
2148
+ ($this->openString(']', $str, '[') || true) &&
2149
+ $this->literal(']')
2150
+ ) {
2151
+ $parts[] = '[';
2152
+
2153
+ if (! empty($str)) {
2154
+ $parts[] = $str;
2155
+ }
2156
+
2157
+ $parts[] = ']';
2158
+
2159
+ continue;
2160
+ }
2161
+
2162
+ $this->seek($s);
2163
+
2164
+ break;
2165
+ }
2166
+
2167
+ $this->eatWhiteDefault = $oldWhite;
2168
+
2169
+ if (count($parts) === 0) {
2170
+ return false;
2171
+ }
2172
+
2173
+ $out = $parts;
2174
+
2175
+ return true;
2176
+ }
2177
+
2178
+ /**
2179
+ * Parse a variable
2180
+ *
2181
+ * @param array $out
2182
+ *
2183
+ * @return boolean
2184
+ */
2185
+ protected function variable(&$out)
2186
+ {
2187
+ $s = $this->seek();
2188
+
2189
+ if ($this->literal('$', false) && $this->keyword($name)) {
2190
+ $out = [Type::T_VARIABLE, $name];
2191
+
2192
+ return true;
2193
+ }
2194
+
2195
+ $this->seek($s);
2196
+
2197
+ return false;
2198
+ }
2199
+
2200
+ /**
2201
+ * Parse a keyword
2202
+ *
2203
+ * @param string $word
2204
+ * @param boolean $eatWhitespace
2205
+ *
2206
+ * @return boolean
2207
+ */
2208
+ protected function keyword(&$word, $eatWhitespace = null)
2209
+ {
2210
+ if ($this->match(
2211
+ $this->utf8
2212
+ ? '(([\pL\w_\-\*!"\']|[\\\\].)([\pL\w\-_"\']|[\\\\].)*)'
2213
+ : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
2214
+ $m,
2215
+ $eatWhitespace
2216
+ )) {
2217
+ $word = $m[1];
2218
+
2219
+ return true;
2220
+ }
2221
+
2222
+ return false;
2223
+ }
2224
+
2225
+ /**
2226
+ * Parse a placeholder
2227
+ *
2228
+ * @param string $placeholder
2229
+ *
2230
+ * @return boolean
2231
+ */
2232
+ protected function placeholder(&$placeholder)
2233
+ {
2234
+ if ($this->match(
2235
+ $this->utf8
2236
+ ? '([\pL\w\-_]+|#[{][$][\pL\w\-_]+[}])'
2237
+ : '([\w\-_]+|#[{][$][\w\-_]+[}])',
2238
+ $m
2239
+ )) {
2240
+ $placeholder = $m[1];
2241
+
2242
+ return true;
2243
+ }
2244
+
2245
+ return false;
2246
+ }
2247
+
2248
+ /**
2249
+ * Parse a url
2250
+ *
2251
+ * @param array $out
2252
+ *
2253
+ * @return boolean
2254
+ */
2255
+ protected function url(&$out)
2256
+ {
2257
+ if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) {
2258
+ $out = [Type::T_STRING, '', ['url(' . $m[2] . $m[3] . $m[2] . ')']];
2259
+
2260
+ return true;
2261
+ }
2262
+
2263
+ return false;
2264
+ }
2265
+
2266
+ /**
2267
+ * Consume an end of statement delimiter
2268
+ *
2269
+ * @return boolean
2270
+ */
2271
+ protected function end()
2272
+ {
2273
+ if ($this->literal(';')) {
2274
+ return true;
2275
+ }
2276
+
2277
+ if ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
2278
+ // if there is end of file or a closing block next then we don't need a ;
2279
+ return true;
2280
+ }
2281
+
2282
+ return false;
2283
+ }
2284
+
2285
+ /**
2286
+ * Strip assignment flag from the list
2287
+ *
2288
+ * @param array $value
2289
+ *
2290
+ * @return string
2291
+ */
2292
+ protected function stripAssignmentFlag(&$value)
2293
+ {
2294
+ $token = &$value;
2295
+
2296
+ for ($token = &$value; $token[0] === Type::T_LIST && ($s = count($token[2])); $token = &$lastNode) {
2297
+ $lastNode = &$token[2][$s - 1];
2298
+
2299
+ if ($lastNode[0] === Type::T_KEYWORD && in_array($lastNode[1], ['!default', '!global'])) {
2300
+ array_pop($token[2]);
2301
+
2302
+ $token = $this->flattenList($token);
2303
+
2304
+ return $lastNode[1];
2305
+ }
2306
+ }
2307
+
2308
+ return false;
2309
+ }
2310
+
2311
+ /**
2312
+ * Strip optional flag from selector list
2313
+ *
2314
+ * @param array $selectors
2315
+ *
2316
+ * @return string
2317
+ */
2318
+ protected function stripOptionalFlag(&$selectors)
2319
+ {
2320
+ $optional = false;
2321
+
2322
+ $selector = end($selectors);
2323
+ $part = end($selector);
2324
+
2325
+ if ($part === ['!optional']) {
2326
+ array_pop($selectors[count($selectors) - 1]);
2327
+
2328
+ $optional = true;
2329
+ }
2330
+
2331
+ return $optional;
2332
+ }
2333
+
2334
+ /**
2335
+ * Turn list of length 1 into value type
2336
+ *
2337
+ * @param array $value
2338
+ *
2339
+ * @return array
2340
+ */
2341
+ protected function flattenList($value)
2342
+ {
2343
+ if ($value[0] === Type::T_LIST && count($value[2]) === 1) {
2344
+ return $this->flattenList($value[2][0]);
2345
+ }
2346
+
2347
+ return $value;
2348
+ }
2349
+
2350
+ /**
2351
+ * @deprecated
2352
+ *
2353
+ * {@internal
2354
+ * advance counter to next occurrence of $what
2355
+ * $until - don't include $what in advance
2356
+ * $allowNewline, if string, will be used as valid char set
2357
+ * }}
2358
+ */
2359
+ protected function to($what, &$out, $until = false, $allowNewline = false)
2360
+ {
2361
+ if (is_string($allowNewline)) {
2362
+ $validChars = $allowNewline;
2363
+ } else {
2364
+ $validChars = $allowNewline ? '.' : "[^\n]";
2365
+ }
2366
+
2367
+ if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
2368
+ return false;
2369
+ }
2370
+
2371
+ if ($until) {
2372
+ $this->count -= strlen($what); // give back $what
2373
+ }
2374
+
2375
+ $out = $m[1];
2376
+
2377
+ return true;
2378
+ }
2379
+
2380
+ /**
2381
+ * @deprecated
2382
+ */
2383
+ protected function show()
2384
+ {
2385
+ if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
2386
+ return $m[1];
2387
+ }
2388
+
2389
+ return '';
2390
+ }
2391
+
2392
+ /**
2393
+ * Quote regular expression
2394
+ *
2395
+ * @param string $what
2396
+ *
2397
+ * @return string
2398
+ */
2399
+ private function pregQuote($what)
2400
+ {
2401
+ return preg_quote($what, '/');
2402
+ }
2403
+
2404
+ /**
2405
+ * Extract line numbers from buffer
2406
+ *
2407
+ * @param string $buffer
2408
+ */
2409
+ private function extractLineNumbers($buffer)
2410
+ {
2411
+ $this->sourcePositions = [0 => 0];
2412
+ $prev = 0;
2413
+
2414
+ while (($pos = strpos($buffer, "\n", $prev)) !== false) {
2415
+ $this->sourcePositions[] = $pos;
2416
+ $prev = $pos + 1;
2417
+ }
2418
+
2419
+ $this->sourcePositions[] = strlen($buffer);
2420
+
2421
+ if (substr($buffer, -1) !== "\n") {
2422
+ $this->sourcePositions[] = strlen($buffer) + 1;
2423
+ }
2424
+ }
2425
+
2426
+ /**
2427
+ * Get source line number and column (given character position in the buffer)
2428
+ *
2429
+ * @param integer $pos
2430
+ *
2431
+ * @return integer
2432
+ */
2433
+ private function getSourcePosition($pos)
2434
+ {
2435
+ $low = 0;
2436
+ $high = count($this->sourcePositions);
2437
+
2438
+ while ($low < $high) {
2439
+ $mid = (int) (($high + $low) / 2);
2440
+
2441
+ if ($pos < $this->sourcePositions[$mid]) {
2442
+ $high = $mid - 1;
2443
+ continue;
2444
+ }
2445
+
2446
+ if ($pos >= $this->sourcePositions[$mid + 1]) {
2447
+ $low = $mid + 1;
2448
+ continue;
2449
+ }
2450
+
2451
+ return [$mid + 1, $pos - $this->sourcePositions[$mid]];
2452
+ }
2453
+
2454
+ return [$low + 1, $pos - $this->sourcePositions[$low]];
2455
+ }
2456
+
2457
+ /**
2458
+ * Save internal encoding
2459
+ */
2460
+ private function saveEncoding()
2461
+ {
2462
+ if (ini_get('mbstring.func_overload') & 2) {
2463
+ $this->encoding = mb_internal_encoding();
2464
+
2465
+ mb_internal_encoding('iso-8859-1');
2466
+ }
2467
+ }
2468
+
2469
+ /**
2470
+ * Restore internal encoding
2471
+ */
2472
+ private function restoreEncoding()
2473
+ {
2474
+ if ($this->encoding) {
2475
+ mb_internal_encoding($this->encoding);
2476
+ }
2477
+ }
2478
+ }
scssphp/src/Server.php ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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\Compiler;
15
+ use Leafo\ScssPhp\Exception\ServerException;
16
+ use Leafo\ScssPhp\Version;
17
+
18
+ /**
19
+ * Server
20
+ *
21
+ * @author Leaf Corcoran <leafot@gmail.com>
22
+ */
23
+ class Server
24
+ {
25
+ /**
26
+ * @var boolean
27
+ */
28
+ private $showErrorsAsCSS;
29
+
30
+ /**
31
+ * @var string
32
+ */
33
+ private $dir;
34
+
35
+ /**
36
+ * @var string
37
+ */
38
+ private $cacheDir;
39
+
40
+ /**
41
+ * @var \Leafo\ScssPhp\Compiler
42
+ */
43
+ private $scss;
44
+
45
+ /**
46
+ * Join path components
47
+ *
48
+ * @param string $left Path component, left of the directory separator
49
+ * @param string $right Path component, right of the directory separator
50
+ *
51
+ * @return string
52
+ */
53
+ protected function join($left, $right)
54
+ {
55
+ return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\');
56
+ }
57
+
58
+ /**
59
+ * Get name of requested .scss file
60
+ *
61
+ * @return string|null
62
+ */
63
+ protected function inputName()
64
+ {
65
+ switch (true) {
66
+ case isset($_GET['p']):
67
+ return $_GET['p'];
68
+ case isset($_SERVER['PATH_INFO']):
69
+ return $_SERVER['PATH_INFO'];
70
+ case isset($_SERVER['DOCUMENT_URI']):
71
+ return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME']));
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Get path to requested .scss file
77
+ *
78
+ * @return string
79
+ */
80
+ protected function findInput()
81
+ {
82
+ if (($input = $this->inputName())
83
+ && strpos($input, '..') === false
84
+ && substr($input, -5) === '.scss'
85
+ ) {
86
+ $name = $this->join($this->dir, $input);
87
+
88
+ if (is_file($name) && is_readable($name)) {
89
+ return $name;
90
+ }
91
+ }
92
+
93
+ return false;
94
+ }
95
+
96
+ /**
97
+ * Get path to cached .css file
98
+ *
99
+ * @return string
100
+ */
101
+ protected function cacheName($fname)
102
+ {
103
+ return $this->join($this->cacheDir, md5($fname) . '.css');
104
+ }
105
+
106
+ /**
107
+ * Get path to meta data
108
+ *
109
+ * @return string
110
+ */
111
+ protected function metadataName($out)
112
+ {
113
+ return $out . '.meta';
114
+ }
115
+
116
+ /**
117
+ * Determine whether .scss file needs to be re-compiled.
118
+ *
119
+ * @param string $out Output path
120
+ * @param string $etag ETag
121
+ *
122
+ * @return boolean True if compile required.
123
+ */
124
+ protected function needsCompile($out, &$etag)
125
+ {
126
+ if (! is_file($out)) {
127
+ return true;
128
+ }
129
+
130
+ $mtime = filemtime($out);
131
+
132
+ $metadataName = $this->metadataName($out);
133
+
134
+ if (is_readable($metadataName)) {
135
+ $metadata = unserialize(file_get_contents($metadataName));
136
+
137
+ foreach ($metadata['imports'] as $import => $originalMtime) {
138
+ $currentMtime = filemtime($import);
139
+
140
+ if ($currentMtime !== $originalMtime || $currentMtime > $mtime) {
141
+ return true;
142
+ }
143
+ }
144
+
145
+ $metaVars = crc32(serialize($this->scss->getVariables()));
146
+
147
+ if ($metaVars !== $metadata['vars']) {
148
+ return true;
149
+ }
150
+
151
+ $etag = $metadata['etag'];
152
+
153
+ return false;
154
+ }
155
+
156
+ return true;
157
+ }
158
+
159
+ /**
160
+ * Get If-Modified-Since header from client request
161
+ *
162
+ * @return string|null
163
+ */
164
+ protected function getIfModifiedSinceHeader()
165
+ {
166
+ $modifiedSince = null;
167
+
168
+ if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
169
+ $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
170
+
171
+ if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) {
172
+ $modifiedSince = substr($modifiedSince, 0, $semicolonPos);
173
+ }
174
+ }
175
+
176
+ return $modifiedSince;
177
+ }
178
+
179
+ /**
180
+ * Get If-None-Match header from client request
181
+ *
182
+ * @return string|null
183
+ */
184
+ protected function getIfNoneMatchHeader()
185
+ {
186
+ $noneMatch = null;
187
+
188
+ if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
189
+ $noneMatch = $_SERVER['HTTP_IF_NONE_MATCH'];
190
+ }
191
+
192
+ return $noneMatch;
193
+ }
194
+
195
+ /**
196
+ * Compile .scss file
197
+ *
198
+ * @param string $in Input path (.scss)
199
+ * @param string $out Output path (.css)
200
+ *
201
+ * @return array
202
+ */
203
+ protected function compile($in, $out)
204
+ {
205
+ $start = microtime(true);
206
+ $css = $this->scss->compile(file_get_contents($in), $in);
207
+ $elapsed = round((microtime(true) - $start), 4);
208
+
209
+ $v = Version::VERSION;
210
+ $t = date('r');
211
+ $css = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css;
212
+ $etag = md5($css);
213
+
214
+ file_put_contents($out, $css);
215
+ file_put_contents(
216
+ $this->metadataName($out),
217
+ serialize([
218
+ 'etag' => $etag,
219
+ 'imports' => $this->scss->getParsedFiles(),
220
+ 'vars' => crc32(serialize($this->scss->getVariables())),
221
+ ])
222
+ );
223
+
224
+ return [$css, $etag];
225
+ }
226
+
227
+ /**
228
+ * Format error as a pseudo-element in CSS
229
+ *
230
+ * @param \Exception $error
231
+ *
232
+ * @return string
233
+ */
234
+ protected function createErrorCSS(\Exception $error)
235
+ {
236
+ $message = str_replace(
237
+ ["'", "\n"],
238
+ ["\\'", "\\A"],
239
+ $error->getfile() . ":\n\n" . $error->getMessage()
240
+ );
241
+
242
+ return "body { display: none !important; }
243
+ html:after {
244
+ background: white;
245
+ color: black;
246
+ content: '$message';
247
+ display: block !important;
248
+ font-family: mono;
249
+ padding: 1em;
250
+ white-space: pre;
251
+ }";
252
+ }
253
+
254
+ /**
255
+ * Render errors as a pseudo-element within valid CSS, displaying the errors on any
256
+ * page that includes this CSS.
257
+ *
258
+ * @param boolean $show
259
+ */
260
+ public function showErrorsAsCSS($show = true)
261
+ {
262
+ $this->showErrorsAsCSS = $show;
263
+ }
264
+
265
+ /**
266
+ * Compile .scss file
267
+ *
268
+ * @param string $in Input file (.scss)
269
+ * @param string $out Output file (.css) optional
270
+ *
271
+ * @return string|bool
272
+ *
273
+ * @throws \Leafo\ScssPhp\Exception\ServerException
274
+ */
275
+ public function compileFile($in, $out = null)
276
+ {
277
+ if (! is_readable($in)) {
278
+ throw new ServerException('load error: failed to find ' . $in);
279
+ }
280
+
281
+ $pi = pathinfo($in);
282
+
283
+ $this->scss->addImportPath($pi['dirname'] . '/');
284
+
285
+ $compiled = $this->scss->compile(file_get_contents($in), $in);
286
+
287
+ if ($out !== null) {
288
+ return file_put_contents($out, $compiled);
289
+ }
290
+
291
+ return $compiled;
292
+ }
293
+
294
+ /**
295
+ * Check if file need compiling
296
+ *
297
+ * @param string $in Input file (.scss)
298
+ * @param string $out Output file (.css)
299
+ *
300
+ * @return bool
301
+ */
302
+ public function checkedCompile($in, $out)
303
+ {
304
+ if (! is_file($out) || filemtime($in) > filemtime($out)) {
305
+ $this->compileFile($in, $out);
306
+
307
+ return true;
308
+ }
309
+
310
+ return false;
311
+ }
312
+
313
+ /**
314
+ * Compile requested scss and serve css. Outputs HTTP response.
315
+ *
316
+ * @param string $salt Prefix a string to the filename for creating the cache name hash
317
+ */
318
+ public function serve($salt = '')
319
+ {
320
+ $protocol = isset($_SERVER['SERVER_PROTOCOL'])
321
+ ? $_SERVER['SERVER_PROTOCOL']
322
+ : 'HTTP/1.0';
323
+
324
+ if ($input = $this->findInput()) {
325
+ $output = $this->cacheName($salt . $input);
326
+ $etag = $noneMatch = trim($this->getIfNoneMatchHeader(), '"');
327
+
328
+ if ($this->needsCompile($output, $etag)) {
329
+ try {
330
+ list($css, $etag) = $this->compile($input, $output);
331
+
332
+ $lastModified = gmdate('D, d M Y H:i:s', filemtime($output)) . ' GMT';
333
+
334
+ header('Last-Modified: ' . $lastModified);
335
+ header('Content-type: text/css');
336
+ header('ETag: "' . $etag . '"');
337
+
338
+ echo $css;
339
+ } catch (\Exception $e) {
340
+ if ($this->showErrorsAsCSS) {
341
+ header('Content-type: text/css');
342
+
343
+ echo $this->createErrorCSS($e);
344
+ } else {
345
+ header($protocol . ' 500 Internal Server Error');
346
+ header('Content-type: text/plain');
347
+
348
+ echo 'Parse error: ' . $e->getMessage() . "\n";
349
+ }
350
+ }
351
+
352
+ return;
353
+ }
354
+
355
+ header('X-SCSS-Cache: true');
356
+ header('Content-type: text/css');
357
+ header('ETag: "' . $etag . '"');
358
+
359
+ if ($etag === $noneMatch) {
360
+ header($protocol . ' 304 Not Modified');
361
+
362
+ return;
363
+ }
364
+
365
+ $modifiedSince = $this->getIfModifiedSinceHeader();
366
+ $mtime = filemtime($output);
367
+
368
+ if (strtotime($modifiedSince) === $mtime) {
369
+ header($protocol . ' 304 Not Modified');
370
+
371
+ return;
372
+ }
373
+
374
+ $lastModified = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
375
+ header('Last-Modified: ' . $lastModified);
376
+
377
+ echo file_get_contents($output);
378
+
379
+ return;
380
+ }
381
+
382
+ header($protocol . ' 404 Not Found');
383
+ header('Content-type: text/plain');
384
+
385
+ $v = Version::VERSION;
386
+ echo "/* INPUT NOT FOUND scss $v */\n";
387
+ }
388
+
389
+ /**
390
+ * Based on explicit input/output files does a full change check on cache before compiling.
391
+ *
392
+ * @param string $in
393
+ * @param string $out
394
+ * @param boolean $force
395
+ *
396
+ * @return string Compiled CSS results
397
+ *
398
+ * @throws \Leafo\ScssPhp\Exception\ServerException
399
+ */
400
+ public function checkedCachedCompile($in, $out, $force = false)
401
+ {
402
+ if (! is_file($in) || ! is_readable($in)) {
403
+ throw new ServerException('Invalid or unreadable input file specified.');
404
+ }
405
+
406
+ if (is_dir($out) || ! is_writable(file_exists($out) ? $out : dirname($out))) {
407
+ throw new ServerException('Invalid or unwritable output file specified.');
408
+ }
409
+
410
+ if ($force || $this->needsCompile($out, $etag)) {
411
+ list($css, $etag) = $this->compile($in, $out);
412
+ } else {
413
+ $css = file_get_contents($out);
414
+ }
415
+
416
+ return $css;
417
+ }
418
+
419
+ /**
420
+ * Constructor
421
+ *
422
+ * @param string $dir Root directory to .scss files
423
+ * @param string $cacheDir Cache directory
424
+ * @param \Leafo\ScssPhp\Compiler|null $scss SCSS compiler instance
425
+ */
426
+ public function __construct($dir, $cacheDir = null, $scss = null)
427
+ {
428
+ $this->dir = $dir;
429
+
430
+ if (! isset($cacheDir)) {
431
+ $cacheDir = $this->join($dir, 'scss_cache');
432
+ }
433
+
434
+ $this->cacheDir = $cacheDir;
435
+
436
+ if (! is_dir($this->cacheDir)) {
437
+ mkdir($this->cacheDir, 0755, true);
438
+ }
439
+
440
+ if (! isset($scss)) {
441
+ $scss = new Compiler();
442
+ $scss->setImportPaths($this->dir);
443
+ }
444
+
445
+ $this->scss = $scss;
446
+ $this->showErrorsAsCSS = false;
447
+
448
+ if (! ini_get('date.timezone')) {
449
+ date_default_timezone_set('UTC');
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Helper method to serve compiled scss
455
+ *
456
+ * @param string $path Root path
457
+ */
458
+ public static function serveFrom($path)
459
+ {
460
+ $server = new self($path);
461
+ $server->serve();
462
+ }
463
+ }
scssphp/src/Type.php ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
16
+ *
17
+ * @author Anthon Pang <anthon.pang@gmail.com>
18
+ */
19
+ class Type
20
+ {
21
+ const T_ASSIGN = 'assign';
22
+ const T_AT_ROOT = 'at-root';
23
+ const T_BLOCK = 'block';
24
+ const T_BREAK = 'break';
25
+ const T_CHARSET = 'charset';
26
+ const T_COLOR = 'color';
27
+ const T_COMMENT = 'comment';
28
+ const T_CONTINUE = 'continue';
29
+ const T_CONTROL = 'control';
30
+ const T_DEBUG = 'debug';
31
+ const T_DIRECTIVE = 'directive';
32
+ const T_EACH = 'each';
33
+ const T_ELSE = 'else';
34
+ const T_ELSEIF = 'elseif';
35
+ const T_ERROR = 'error';
36
+ const T_EXPRESSION = 'exp';
37
+ const T_EXTEND = 'extend';
38
+ const T_FOR = 'for';
39
+ const T_FUNCTION = 'function';
40
+ const T_FUNCTION_CALL = 'fncall';
41
+ const T_HSL = 'hsl';
42
+ const T_IF = 'if';
43
+ const T_IMPORT = 'import';
44
+ const T_INCLUDE = 'include';
45
+ const T_INTERPOLATE = 'interpolate';
46
+ const T_INTERPOLATED = 'interpolated';
47
+ const T_KEYWORD = 'keyword';
48
+ const T_LIST = 'list';
49
+ const T_MAP = 'map';
50
+ const T_MEDIA = 'media';
51
+ const T_MEDIA_EXPRESSION = 'mediaExp';
52
+ const T_MEDIA_TYPE = 'mediaType';
53
+ const T_MEDIA_VALUE = 'mediaValue';
54
+ const T_MIXIN = 'mixin';
55
+ const T_MIXIN_CONTENT = 'mixin_content';
56
+ const T_NESTED_PROPERTY = 'nestedprop';
57
+ const T_NOT = 'not';
58
+ const T_NULL = 'null';
59
+ const T_NUMBER = 'number';
60
+ const T_RETURN = 'return';
61
+ const T_ROOT = 'root';
62
+ const T_SCSSPHP_IMPORT_ONCE = 'scssphp-import-once';
63
+ const T_SELF = 'self';
64
+ const T_STRING = 'string';
65
+ const T_UNARY = 'unary';
66
+ const T_VARIABLE = 'var';
67
+ const T_WARN = 'warn';
68
+ const T_WHILE = 'while';
69
+ }
scssphp/src/Util.php ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
+
16
+ /**
17
+ * Utilties
18
+ *
19
+ * @author Anthon Pang <anthon.pang@gmail.com>
20
+ */
21
+ class Util
22
+ {
23
+ /**
24
+ * Asserts that `value` falls within `range` (inclusive), leaving
25
+ * room for slight floating-point errors.
26
+ *
27
+ * @param string $name The name of the value. Used in the error message.
28
+ * @param Range $range Range of values.
29
+ * @param array $value The value to check.
30
+ * @param string $unit The unit of the value. Used in error reporting.
31
+ *
32
+ * @return mixed `value` adjusted to fall within range, if it was outside by a floating-point margin.
33
+ *
34
+ * @throws \Exception
35
+ */
36
+ public static function checkRange($name, Range $range, $value, $unit = '')
37
+ {
38
+ $val = $value[1];
39
+ $grace = new Range(-0.00001, 0.00001);
40
+
41
+ if ($range->includes($val)) {
42
+ return $val;
43
+ }
44
+
45
+ if ($grace->includes($val - $range->first)) {
46
+ return $range->first;
47
+ }
48
+
49
+ if ($grace->includes($val - $range->last)) {
50
+ return $range->last;
51
+ }
52
+
53
+ throw new \Exception("$name {$val} must be between {$range->first} and {$range->last}$unit");
54
+ }
55
+ }
scssphp/src/Version.php ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2015 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
16
+ *
17
+ * @author Leaf Corcoran <leafot@gmail.com>
18
+ */
19
+ class Version
20
+ {
21
+ const VERSION = 'v0.6.3';
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.1.9
7
  * Author: Connect Think
8
  * Author URI: http://connectthink.com
9
  * License: GPLv3
@@ -46,7 +46,7 @@ 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.1.9');
50
 
51
  // Add version to options table
52
  add_option(WPSCSS_VERSION_KEY, WPSCSS_VERSION_NUM);
@@ -116,7 +116,7 @@ if( $scss_dir_setting == false || $css_dir_setting == false ) {
116
  return 0; //exits
117
 
118
  // Checks if directory exists
119
- } elseif ( !file_exists(WPSCSS_THEME_DIR . $scss_dir_setting) || !file_exists(WPSCSS_THEME_DIR . $css_dir_setting) ) {
120
  function wpscss_settings_error() {
121
  echo '<div class="error">
122
  <p><strong>Wp-Scss:</strong> One or more specified directories does not exist. Please create the directories or <a href="' . get_bloginfo('wpurl') . '/wp-admin/admin.php?page=wpscss_options">update your settings.</a></p>
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.0
7
  * Author: Connect Think
8
  * Author URI: http://connectthink.com
9
  * License: GPLv3
46
  define('WPSCSS_VERSION_KEY', 'wpscss_version');
47
 
48
  if (!defined('WPSCSS_VERSION_NUM'))
49
+ define('WPSCSS_VERSION_NUM', '1.2.0');
50
 
51
  // Add version to options table
52
  add_option(WPSCSS_VERSION_KEY, WPSCSS_VERSION_NUM);
116
  return 0; //exits
117
 
118
  // Checks if directory exists
119
+ } elseif ( !is_dir(WPSCSS_THEME_DIR . $scss_dir_setting) || !is_dir(WPSCSS_THEME_DIR . $css_dir_setting) ) {
120
  function wpscss_settings_error() {
121
  echo '<div class="error">
122
  <p><strong>Wp-Scss:</strong> One or more specified directories does not exist. Please create the directories or <a href="' . get_bloginfo('wpurl') . '/wp-admin/admin.php?page=wpscss_options">update your settings.</a></p>