WP-SCSS - Version 1.1.1

Version Description

Download this release

Release Info

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

Version 1.1.1

Files changed (5) hide show
  1. class/class-wp-scss.php +194 -0
  2. options.php +240 -0
  3. readme.txt +67 -0
  4. scssphp/scss.inc.php +4371 -0
  5. wp-scss.php +241 -0
class/class-wp-scss.php ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class Wp_Scss {
4
+ /**
5
+ * Compiling preferences properites
6
+ *
7
+ * @var string
8
+ * @access public
9
+ */
10
+ public $scss_dir, $css_dir, $compile_method, $scssc, $compile_errors;
11
+
12
+
13
+ /**
14
+ * Set values for Wp_Scss::properties
15
+ *
16
+ * @param string scss_dir - path to source directory for scss files
17
+ * @param string css_dir - path to output directory for css files
18
+ * @param string method - type of compile (compressed, expanded, etc)
19
+ *
20
+ * @var object scssc - instantiate the compiling object.
21
+ *
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
+ /**
38
+ * METHOD COMPILE
39
+ * Loops through scss directory and compilers files that end
40
+ * with .scss and do not have '_' in front.
41
+ *
42
+ * @function compiler - passes input content through scssphp,
43
+ * puts compiled css into cache file
44
+ *
45
+ * @var array input_files - array of .scss files with no '_' in front
46
+ * @var array sdir_arr - an array of all the files in the scss directory
47
+ *
48
+ * @return nothing - Puts successfully compiled css into apporpriate location
49
+ * Puts error in 'compile_errors' property
50
+ * @access public
51
+ */
52
+ public function compile() {
53
+ global $scssc, $cache;
54
+ $cache = WPSCSS_PLUGIN_DIR . '/cache/';
55
+
56
+ //Compiler - Takes scss $in and writes compiled css to $out file
57
+ // catches errors and puts them the object's compiled_errors property
58
+ function compiler($in, $out, $instance) {
59
+ global $scssc, $cache;
60
+
61
+ if (is_writable($cache)) {
62
+ try {
63
+ $css = $scssc->compile(file_get_contents($in));
64
+ file_put_contents($cache.basename($out), $css);
65
+ } catch (Exception $e) {
66
+ $errors = array (
67
+ 'file' => basename($in),
68
+ 'message' => $e->getMessage(),
69
+ );
70
+ array_push($instance->compile_errors, $errors);
71
+ }
72
+ } else {
73
+ $errors = array (
74
+ 'file' => "/wp-plugins/wp-scss/cache/",
75
+ 'message' => "File Permission Error, permission denied. Please make the cache directory writable."
76
+ );
77
+ array_push($instance->compile_errors, $errors);
78
+ }
79
+ }
80
+
81
+ $input_files = array();
82
+ // Loop through directory and get .scss file that do not start with '_'
83
+ foreach(new DirectoryIterator($this->scss_dir) as $file) {
84
+ if (substr($file, 0, 1) != "_" && pathinfo($file->getFilename(), PATHINFO_EXTENSION) == 'scss') {
85
+ array_push($input_files, $file->getFilename());
86
+ }
87
+ }
88
+
89
+ // For each input file, find matching css file and compile
90
+ foreach ($input_files as $scss_file) {
91
+ $input = $this->scss_dir.$scss_file;
92
+ $outputName = preg_replace("/\.[^$]*/",".css", $scss_file);
93
+ $output = $this->css_dir.$outputName;
94
+
95
+ compiler($input, $output, $this);
96
+ }
97
+
98
+ if (count($this->compile_errors) < 1) {
99
+ if ( is_writable($this->css_dir) ) {
100
+ foreach (new DirectoryIterator($cache) as $cache_file) {
101
+ if ( pathinfo($cache_file->getFilename(), PATHINFO_EXTENSION) == 'css') {
102
+ file_put_contents($this->css_dir.$cache_file, file_get_contents($cache.$cache_file));
103
+ unlink($cache.$cache_file->getFilename()); // Delete file on successful write
104
+ }
105
+ }
106
+ } else {
107
+ $errors = array(
108
+ 'file' => 'CSS Directory',
109
+ 'message' => "File Permissions Error, permission denied. Please make your CSS directory writable."
110
+ );
111
+ array_push($this->compile_errors, $errors);
112
+ }
113
+ }
114
+ }
115
+
116
+
117
+ /**
118
+ * METHOD NEEDS_COMPILING
119
+ * Gets the most recently modified file in the scss directory
120
+ * and compares that do the most recently modified css file.
121
+ * If scss is greater, we assume that changes have been made
122
+ * and compiling needs to occur to update css.
123
+ *
124
+ * @param string scss_dir - path to scss folder
125
+ * @param string css_dir - path to css folder
126
+ *
127
+ * @var array sdir_arr - scss directory files
128
+ * @var array cdir_arr - css directory files
129
+ *
130
+ * @var string latest_scss - file mod time of the most recent file change
131
+ * @var string latest_css - file mod time of the most recent file change
132
+ *
133
+ * @return bool - true if compiling is needed
134
+ */
135
+ public function needs_compiling() {
136
+ $latest_scss = 0;
137
+ $latest_css = 0;
138
+
139
+ foreach ( new DirectoryIterator($this->scss_dir) as $sfile ) {
140
+ if (pathinfo($sfile->getFilename(), PATHINFO_EXTENSION) == 'scss') {
141
+ $file_time = $sfile->getMTime();
142
+
143
+ if ( (int) $file_time > $latest_scss) {
144
+ $latest_scss = $file_time;
145
+ }
146
+ }
147
+ }
148
+
149
+ foreach ( new DirectoryIterator($this->css_dir) as $cfile ) {
150
+ if (pathinfo($cfile->getFilename(), PATHINFO_EXTENSION) == 'css') {
151
+ $file_time = $cfile->getMTime();
152
+
153
+ if ( (int) $file_time > $latest_css) {
154
+ $latest_css = $file_time;
155
+ }
156
+ }
157
+ }
158
+
159
+ if ($latest_scss > $latest_css) {
160
+ return true;
161
+ } else {
162
+ return false;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * METHOD ENQUEUE STYLES
168
+ * Enqueues all styles in the css directory.
169
+ *
170
+ * @param $css_folder - directory from theme root. We need this passed in separately
171
+ * so it can be used in a url, not path
172
+ */
173
+ public function enqueue_files($css_folder) {
174
+
175
+ foreach( new DirectoryIterator($this->css_dir) as $stylesheet ) {
176
+ if ( pathinfo($stylesheet->getFilename(), PATHINFO_EXTENSION) == 'css' ) {
177
+ $name = $stylesheet->getBasename('.css') . '-style';
178
+ $uri = get_stylesheet_directory_uri().$css_folder.$stylesheet->getFilename();
179
+ $ver = $stylesheet->getMTime();
180
+
181
+
182
+ wp_register_style(
183
+ $name,
184
+ $uri,
185
+ array(),
186
+ $ver,
187
+ $media = 'all' );
188
+
189
+ wp_enqueue_style( $name );
190
+ }
191
+ }
192
+ }
193
+
194
+ } // End Wp_Scss Class
options.php ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ class Wp_Scss_Settings
3
+ {
4
+ /**
5
+ * Holds the values to be used in the fields callbacks
6
+ */
7
+ private $options;
8
+
9
+ /**
10
+ * Start up
11
+ */
12
+ public function __construct()
13
+ {
14
+ add_action( 'admin_menu', array( $this, 'add_plugin_page' ) );
15
+ add_action( 'admin_init', array( $this, 'page_init' ) );
16
+ }
17
+
18
+ /**
19
+ * Add options page
20
+ */
21
+ public function add_plugin_page()
22
+ {
23
+ // This page will be under "Settings"
24
+ add_options_page(
25
+ 'Settings Admin',
26
+ 'WP-SCSS',
27
+ 'manage_options',
28
+ 'wpscss_options',
29
+ array( $this, 'create_admin_page' )
30
+ );
31
+ }
32
+
33
+ /**
34
+ * Options page callback
35
+ */
36
+ public function create_admin_page()
37
+ {
38
+ // Set class property
39
+ $this->options = get_option( 'wpscss_options' );
40
+ ?>
41
+ <div class="wrap">
42
+ <?php screen_icon(); ?>
43
+ <h2>WP-SCSS Settings</h2>
44
+ <p>
45
+ <span class="version">Version <em><?php echo get_option('wpscss_version'); ?></em>
46
+ <br/>
47
+ <span class="author">By: <a href="http://connectthink.com" target="_blank">Connect Think</a></span>
48
+ <br/>
49
+ <span class="repo">Help & Issues: <a href="https://github.com/ConnectThink/WP-SCSS" target="_blank">Github</a></span>
50
+ </p>
51
+ <form method="post" action="options.php">
52
+ <?php
53
+ // This prints out all hidden setting fields
54
+ settings_fields( 'wpscss_options_group' );
55
+ do_settings_sections( 'wpscss_options' );
56
+ submit_button();
57
+ ?>
58
+ </form>
59
+ </div>
60
+ <?php
61
+ }
62
+
63
+ /**
64
+ * Register and add settings
65
+ */
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
+ }
156
+
157
+ /**
158
+ * Sanitize each setting field as needed
159
+ *
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
+
173
+ /**
174
+ * Print the Section text
175
+ */
176
+ public function print_paths_info() {
177
+ print 'Add the paths to your directories below. Paths should start with the root of your theme. example: "/library/scss/"';
178
+ }
179
+ public function print_compile_info() {
180
+ print 'Choose how you would like SCSS to be compiled and how you would like the plugin to handle errors';
181
+ }
182
+ public function print_enqueue_info() {
183
+ print 'WP-SCSS can enqueue your css stylesheets in the header automatically.';
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 .= '</select>';
213
+
214
+ echo $html;
215
+ }
216
+ public function errors_callback() {
217
+ $this->options = get_option( 'wpscss_options' );
218
+
219
+ $html = '<select id="errors" name="wpscss_options[errors]">';
220
+ $html .= '<option value="show"' . selected( $this->options['errors'], 'show', false) . '>Show in Header</option>';
221
+ $html .= '<option value="log"' . selected( $this->options['errors'], 'hide', false) . '>Print to Log</option>';
222
+ $html .= '</select>';
223
+
224
+ echo $html;
225
+ }
226
+
227
+ /**
228
+ * Checkboxes' Callbacks
229
+ */
230
+ function enqueue_callback() {
231
+ $this->options = get_option( 'wpscss_options' );
232
+
233
+ $html = '<input type="checkbox" id="enqueue" name="wpscss_options[enqueue]" value="1"' . checked( 1, $this->options['enqueue'], false ) . '/>';
234
+ $html .= '<label for="enqueue"></label>';
235
+
236
+ echo $html;
237
+ }
238
+
239
+ }
240
+
readme.txt ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ === WP-SCSS ===
2
+ 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: 3.8
7
+ Stable tag: 1.1.1
8
+ License: GPLv3 or later
9
+ License URI: http://www.gnu.org/copyleft/gpl.html
10
+
11
+ Compiles .scss files to .css and enqueues them.
12
+
13
+ == Description ==
14
+
15
+ Compiles .scss files on your wordpress install using [lefo's scssphp](https://github.com/leafo/scssphp). Includes settings page for configuring directories, error reporting, compiling options, and auto enqueuing.
16
+
17
+ The plugin only compiles when changes have been made to the scss files. Compiles are made to the matching css file, so disabling this plugin will not take down your stylesheets. In the instance where a matching css file does not exist yet, the plugin will create the appropriate css file in the css directory.
18
+
19
+ [Get detailed instructions on github](https://github.com/ConnectThink/WP-SCSS)
20
+
21
+ == Installation ==
22
+
23
+ 1. Upload plugin to plugins directory
24
+ 2. Active plugin through the 'Plugins' menu in Wordpress
25
+ 3. Configure plugin options through settings page `settings -> WP-SCSS`.
26
+
27
+ == Frequently Asked Questions ==
28
+
29
+ = How do I @import subfiles =
30
+
31
+ You can import other scss files into parent files and compile them into a single css file. To do this, use @import as normal in your scss file. All imported file names *must* start with an underscore. Otherwise they will be compiled into their own css file.
32
+
33
+ When importing in your scss file, you can leave off the underscore.
34
+
35
+ > `@import 'subfile';`
36
+
37
+
38
+ = Does this plugin support Compass? =
39
+ Currently there isn't a way to fully support [compass](https://github.com/chriseppstein/compass) with a php compiler. If you want limited support, you can manually import the compass framework. You'll need both the _compass.scss and compass directory.
40
+
41
+
42
+ > `compass / frameworks / compass / stylesheets /
43
+
44
+ > `@import 'compass';`
45
+
46
+
47
+ Alternatively, you can include [Bourbon](https://github.com/thoughtbot/bourbon) in a similar fashion.
48
+
49
+ = Can I use .sass syntax with this Plugin? =
50
+ This plugin will only work with .scss format.
51
+
52
+ = It's not updating my css, what's happening? =
53
+ Do you have errors printing to the front end? If not, check your log file in your scss directory. The css will not be updated if there are errors in your sass file(s).
54
+
55
+ Make sure your directories are properly defined in the settings. Paths are defined from the root of the theme.
56
+
57
+ = I'm having other issues and need help =
58
+ 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.
59
+
60
+ == Changelog ==
61
+
62
+ = 1.1 =
63
+ * Added error handling for file permissions issues
64
+ * Changed error log to .log for auto updating errors
65
+
66
+ = 1.0 =
67
+ * Initial Build
scssphp/scss.inc.php ADDED
@@ -0,0 +1,4371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 `scssc_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 `scssc_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 `scssc_parser` class is only concerned with parsing its input.
35
+ *
36
+ * The `scssc_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.8";
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 $numberPrecision = 5;
71
+ static protected $unitTable = array(
72
+ "in" => array(
73
+ "in" => 1,
74
+ "pt" => 72,
75
+ "pc" => 6,
76
+ "cm" => 2.54,
77
+ "mm" => 25.4,
78
+ "px" => 96,
79
+ )
80
+ );
81
+
82
+ static public $true = array("keyword", "true");
83
+ static public $false = array("keyword", "false");
84
+ static public $null = array("null");
85
+
86
+ static public $defaultValue = array("keyword", "");
87
+ static public $selfSelector = array("self");
88
+
89
+ protected $importPaths = array("");
90
+ protected $importCache = array();
91
+
92
+ protected $userFunctions = array();
93
+
94
+ protected $formatter = "scss_formatter_nested";
95
+
96
+ function compile($code, $name=null) {
97
+ $this->indentLevel = -1;
98
+ $this->commentsSeen = array();
99
+ $this->extends = array();
100
+ $this->extendsMap = array();
101
+
102
+ $locale = setlocale(LC_NUMERIC, 0);
103
+ setlocale(LC_NUMERIC, "C");
104
+
105
+ $this->parsedFiles = array();
106
+ $this->parser = new scss_parser($name);
107
+ $tree = $this->parser->parse($code);
108
+
109
+ $this->formatter = new $this->formatter();
110
+
111
+ $this->env = null;
112
+ $this->scope = null;
113
+
114
+ $this->compileRoot($tree);
115
+
116
+ $out = $this->formatter->format($this->scope);
117
+
118
+ setlocale(LC_NUMERIC, $locale);
119
+ return $out;
120
+ }
121
+
122
+ protected function isSelfExtend($target, $origin) {
123
+ foreach ($origin as $sel) {
124
+ if (in_array($target, $sel)) {
125
+ return true;
126
+ }
127
+ }
128
+
129
+ return false;
130
+ }
131
+
132
+ protected function pushExtends($target, $origin) {
133
+ if ($this->isSelfExtend($target, $origin)) {
134
+ return;
135
+ }
136
+
137
+ $i = count($this->extends);
138
+ $this->extends[] = array($target, $origin);
139
+
140
+ foreach ($target as $part) {
141
+ if (isset($this->extendsMap[$part])) {
142
+ $this->extendsMap[$part][] = $i;
143
+ } else {
144
+ $this->extendsMap[$part] = array($i);
145
+ }
146
+ }
147
+ }
148
+
149
+ protected function makeOutputBlock($type, $selectors = null) {
150
+ $out = new stdClass;
151
+ $out->type = $type;
152
+ $out->lines = array();
153
+ $out->children = array();
154
+ $out->parent = $this->scope;
155
+ $out->selectors = $selectors;
156
+ $out->depth = $this->env->depth;
157
+
158
+ return $out;
159
+ }
160
+
161
+ protected function matchExtendsSingle($single, &$outOrigin) {
162
+ $counts = array();
163
+ foreach ($single as $part) {
164
+ if (!is_string($part)) return false; // hmm
165
+
166
+ if (isset($this->extendsMap[$part])) {
167
+ foreach ($this->extendsMap[$part] as $idx) {
168
+ $counts[$idx] =
169
+ isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
170
+ }
171
+ }
172
+ }
173
+
174
+ $outOrigin = array();
175
+ $found = false;
176
+
177
+ foreach ($counts as $idx => $count) {
178
+ list($target, $origin) = $this->extends[$idx];
179
+
180
+ // check count
181
+ if ($count != count($target)) continue;
182
+
183
+ // check if target is subset of single
184
+ if (array_diff(array_intersect($single, $target), $target)) continue;
185
+
186
+ $rem = array_diff($single, $target);
187
+
188
+ foreach ($origin as $j => $new) {
189
+ $origin[$j][count($origin[$j]) - 1] = $this->combineSelectorSingle(end($new), $rem);
190
+ }
191
+
192
+ $outOrigin = array_merge($outOrigin, $origin);
193
+
194
+ $found = true;
195
+ }
196
+
197
+ return $found;
198
+ }
199
+
200
+ protected function combineSelectorSingle($base, $other) {
201
+ $tag = null;
202
+ $out = array();
203
+
204
+ foreach (array($base, $other) as $single) {
205
+ foreach ($single as $part) {
206
+ if (preg_match('/^[^\[.#:]/', $part)) {
207
+ $tag = $part;
208
+ } else {
209
+ $out[] = $part;
210
+ }
211
+ }
212
+ }
213
+
214
+ if ($tag) {
215
+ array_unshift($out, $tag);
216
+ }
217
+
218
+ return $out;
219
+ }
220
+
221
+ protected function matchExtends($selector, &$out, $from = 0, $initial=true) {
222
+ foreach ($selector as $i => $part) {
223
+ if ($i < $from) continue;
224
+
225
+ if ($this->matchExtendsSingle($part, $origin)) {
226
+ $before = array_slice($selector, 0, $i);
227
+ $after = array_slice($selector, $i + 1);
228
+
229
+ foreach ($origin as $new) {
230
+ $k = 0;
231
+
232
+ // remove shared parts
233
+ if ($initial) {
234
+ foreach ($before as $k => $val) {
235
+ if (!isset($new[$k]) || $val != $new[$k]) {
236
+ break;
237
+ }
238
+ }
239
+ }
240
+
241
+ $result = array_merge(
242
+ $before,
243
+ $k > 0 ? array_slice($new, $k) : $new,
244
+ $after);
245
+
246
+
247
+ if ($result == $selector) continue;
248
+ $out[] = $result;
249
+
250
+ // recursively check for more matches
251
+ $this->matchExtends($result, $out, $i, false);
252
+
253
+ // selector sequence merging
254
+ if (!empty($before) && count($new) > 1) {
255
+ $result2 = array_merge(
256
+ array_slice($new, 0, -1),
257
+ $k > 0 ? array_slice($before, $k) : $before,
258
+ array_slice($new, -1),
259
+ $after);
260
+
261
+ $out[] = $result2;
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ protected function flattenSelectors($block, $parentKey = null) {
269
+ if ($block->selectors) {
270
+ $selectors = array();
271
+ foreach ($block->selectors as $s) {
272
+ $selectors[] = $s;
273
+ if (!is_array($s)) continue;
274
+ // check extends
275
+ if (!empty($this->extendsMap)) {
276
+ $this->matchExtends($s, $selectors);
277
+ }
278
+ }
279
+
280
+ $block->selectors = array();
281
+ $placeholderSelector = false;
282
+ foreach ($selectors as $selector) {
283
+ if ($this->hasSelectorPlaceholder($selector)) {
284
+ $placeholderSelector = true;
285
+ continue;
286
+ }
287
+ $block->selectors[] = $this->compileSelector($selector);
288
+ }
289
+
290
+ if ($placeholderSelector && 0 == count($block->selectors) && null !== $parentKey) {
291
+ unset($block->parent->children[$parentKey]);
292
+ return;
293
+ }
294
+ }
295
+
296
+ foreach ($block->children as $key => $child) {
297
+ $this->flattenSelectors($child, $key);
298
+ }
299
+ }
300
+
301
+ protected function compileRoot($rootBlock) {
302
+ $this->pushEnv($rootBlock);
303
+ $this->scope = $this->makeOutputBlock("root");
304
+
305
+ $this->compileChildren($rootBlock->children, $this->scope);
306
+ $this->flattenSelectors($this->scope);
307
+
308
+ $this->popEnv();
309
+ }
310
+
311
+ protected function compileMedia($media) {
312
+ $this->pushEnv($media);
313
+ $parentScope = $this->mediaParent($this->scope);
314
+
315
+ $this->scope = $this->makeOutputBlock("media", array(
316
+ $this->compileMediaQuery($this->multiplyMedia($this->env)))
317
+ );
318
+
319
+ $parentScope->children[] = $this->scope;
320
+
321
+ // top level properties in a media cause it to be wrapped
322
+ $needsWrap = false;
323
+ foreach ($media->children as $child) {
324
+ $type = $child[0];
325
+ if ($type !== 'block' && $type !== 'media' && $type !== 'directive') {
326
+ $needsWrap = true;
327
+ break;
328
+ }
329
+ }
330
+
331
+ if ($needsWrap) {
332
+ $wrapped = (object)array(
333
+ "selectors" => array(),
334
+ "children" => $media->children
335
+ );
336
+ $media->children = array(array("block", $wrapped));
337
+ }
338
+
339
+ $this->compileChildren($media->children, $this->scope);
340
+
341
+ $this->scope = $this->scope->parent;
342
+ $this->popEnv();
343
+ }
344
+
345
+ protected function mediaParent($scope) {
346
+ while (!empty($scope->parent)) {
347
+ if (!empty($scope->type) && $scope->type != "media") {
348
+ break;
349
+ }
350
+ $scope = $scope->parent;
351
+ }
352
+
353
+ return $scope;
354
+ }
355
+
356
+ // TODO refactor compileNestedBlock and compileMedia into same thing
357
+ protected function compileNestedBlock($block, $selectors) {
358
+ $this->pushEnv($block);
359
+
360
+ $this->scope = $this->makeOutputBlock($block->type, $selectors);
361
+ $this->scope->parent->children[] = $this->scope;
362
+ $this->compileChildren($block->children, $this->scope);
363
+
364
+ $this->scope = $this->scope->parent;
365
+ $this->popEnv();
366
+ }
367
+
368
+ /**
369
+ * Recursively compiles a block.
370
+ *
371
+ * A block is analogous to a CSS block in most cases. A single SCSS document
372
+ * is encapsulated in a block when parsed, but it does not have parent tags
373
+ * so all of its children appear on the root level when compiled.
374
+ *
375
+ * Blocks are made up of selectors and children.
376
+ *
377
+ * The children of a block are just all the blocks that are defined within.
378
+ *
379
+ * Compiling the block involves pushing a fresh environment on the stack,
380
+ * and iterating through the props, compiling each one.
381
+ *
382
+ * @see scss::compileChild()
383
+ *
384
+ * @param \StdClass $block
385
+ */
386
+ protected function compileBlock($block) {
387
+ $env = $this->pushEnv($block);
388
+
389
+ $env->selectors =
390
+ array_map(array($this, "evalSelector"), $block->selectors);
391
+
392
+ $out = $this->makeOutputBlock(null, $this->multiplySelectors($env));
393
+ $this->scope->children[] = $out;
394
+ $this->compileChildren($block->children, $out);
395
+
396
+ $this->popEnv();
397
+ }
398
+
399
+ // joins together .classes and #ids
400
+ protected function flattenSelectorSingle($single) {
401
+ $joined = array();
402
+ foreach ($single as $part) {
403
+ if (empty($joined) ||
404
+ !is_string($part) ||
405
+ preg_match('/[\[.:#%]/', $part))
406
+ {
407
+ $joined[] = $part;
408
+ continue;
409
+ }
410
+
411
+ if (is_array(end($joined))) {
412
+ $joined[] = $part;
413
+ } else {
414
+ $joined[count($joined) - 1] .= $part;
415
+ }
416
+ }
417
+
418
+ return $joined;
419
+ }
420
+
421
+ // replaces all the interpolates
422
+ protected function evalSelector($selector) {
423
+ return array_map(array($this, "evalSelectorPart"), $selector);
424
+ }
425
+
426
+ protected function evalSelectorPart($piece) {
427
+ foreach ($piece as &$p) {
428
+ if (!is_array($p)) continue;
429
+
430
+ switch ($p[0]) {
431
+ case "interpolate":
432
+ $p = $this->compileValue($p);
433
+ break;
434
+ case "string":
435
+ $p = $this->compileValue($p);
436
+ break;
437
+ }
438
+ }
439
+
440
+ return $this->flattenSelectorSingle($piece);
441
+ }
442
+
443
+ // compiles to string
444
+ // self(&) should have been replaced by now
445
+ protected function compileSelector($selector) {
446
+ if (!is_array($selector)) return $selector; // media and the like
447
+
448
+ return implode(" ", array_map(
449
+ array($this, "compileSelectorPart"), $selector));
450
+ }
451
+
452
+ protected function compileSelectorPart($piece) {
453
+ foreach ($piece as &$p) {
454
+ if (!is_array($p)) continue;
455
+
456
+ switch ($p[0]) {
457
+ case "self":
458
+ $p = "&";
459
+ break;
460
+ default:
461
+ $p = $this->compileValue($p);
462
+ break;
463
+ }
464
+ }
465
+
466
+ return implode($piece);
467
+ }
468
+
469
+ protected function hasSelectorPlaceholder($selector)
470
+ {
471
+ if (!is_array($selector)) return false;
472
+
473
+ foreach ($selector as $parts) {
474
+ foreach ($parts as $part) {
475
+ if ('%' == $part[0]) {
476
+ return true;
477
+ }
478
+ }
479
+ }
480
+
481
+ return false;
482
+ }
483
+
484
+ protected function compileChildren($stms, $out) {
485
+ foreach ($stms as $stm) {
486
+ $ret = $this->compileChild($stm, $out);
487
+ if (!is_null($ret)) return $ret;
488
+ }
489
+ }
490
+
491
+ protected function compileMediaQuery($queryList) {
492
+ $out = "@media";
493
+ $first = true;
494
+ foreach ($queryList as $query){
495
+ $parts = array();
496
+ foreach ($query as $q) {
497
+ switch ($q[0]) {
498
+ case "mediaType":
499
+ $parts[] = implode(" ", array_map(array($this, "compileValue"), array_slice($q, 1)));
500
+ break;
501
+ case "mediaExp":
502
+ if (isset($q[2])) {
503
+ $parts[] = "(". $this->compileValue($q[1]) . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ")";
504
+ } else {
505
+ $parts[] = "(" . $this->compileValue($q[1]) . ")";
506
+ }
507
+ break;
508
+ }
509
+ }
510
+ if (!empty($parts)) {
511
+ if ($first) {
512
+ $first = false;
513
+ $out .= " ";
514
+ } else {
515
+ $out .= $this->formatter->tagSeparator;
516
+ }
517
+ $out .= implode(" and ", $parts);
518
+ }
519
+ }
520
+ return $out;
521
+ }
522
+
523
+ // returns true if the value was something that could be imported
524
+ protected function compileImport($rawPath, $out) {
525
+ if ($rawPath[0] == "string") {
526
+ $path = $this->compileStringContent($rawPath);
527
+ if ($path = $this->findImport($path)) {
528
+ $this->importFile($path, $out);
529
+ return true;
530
+ }
531
+ return false;
532
+ }
533
+ if ($rawPath[0] == "list") {
534
+ // handle a list of strings
535
+ if (count($rawPath[2]) == 0) return false;
536
+ foreach ($rawPath[2] as $path) {
537
+ if ($path[0] != "string") return false;
538
+ }
539
+
540
+ foreach ($rawPath[2] as $path) {
541
+ $this->compileImport($path, $out);
542
+ }
543
+
544
+ return true;
545
+ }
546
+
547
+ return false;
548
+ }
549
+
550
+ // return a value to halt execution
551
+ protected function compileChild($child, $out) {
552
+ $this->sourcePos = isset($child[-1]) ? $child[-1] : -1;
553
+ $this->sourceParser = isset($child[-2]) ? $child[-2] : $this->parser;
554
+
555
+ switch ($child[0]) {
556
+ case "import":
557
+ list(,$rawPath) = $child;
558
+ $rawPath = $this->reduce($rawPath);
559
+ if (!$this->compileImport($rawPath, $out)) {
560
+ $out->lines[] = "@import " . $this->compileValue($rawPath) . ";";
561
+ }
562
+ break;
563
+ case "directive":
564
+ list(, $directive) = $child;
565
+ $s = "@" . $directive->name;
566
+ if (!empty($directive->value)) {
567
+ $s .= " " . $this->compileValue($directive->value);
568
+ }
569
+ $this->compileNestedBlock($directive, array($s));
570
+ break;
571
+ case "media":
572
+ $this->compileMedia($child[1]);
573
+ break;
574
+ case "block":
575
+ $this->compileBlock($child[1]);
576
+ break;
577
+ case "charset":
578
+ $out->lines[] = "@charset ".$this->compileValue($child[1]).";";
579
+ break;
580
+ case "assign":
581
+ list(,$name, $value) = $child;
582
+ if ($name[0] == "var") {
583
+ $isDefault = !empty($child[3]);
584
+
585
+ if ($isDefault) {
586
+ $existingValue = $this->get($name[1], true);
587
+ $shouldSet = $existingValue === true || $existingValue == self::$null;
588
+ }
589
+
590
+ if (!$isDefault || $shouldSet) {
591
+ $this->set($name[1], $this->reduce($value));
592
+ }
593
+ break;
594
+ }
595
+
596
+ // if the value reduces to null from something else then
597
+ // the property should be discarded
598
+ if ($value[0] != "null") {
599
+ $value = $this->reduce($value);
600
+ if ($value[0] == "null") {
601
+ break;
602
+ }
603
+ }
604
+
605
+ $compiledValue = $this->compileValue($value);
606
+ $out->lines[] = $this->formatter->property(
607
+ $this->compileValue($name),
608
+ $compiledValue);
609
+ break;
610
+ case "comment":
611
+ $out->lines[] = $child[1];
612
+ break;
613
+ case "mixin":
614
+ case "function":
615
+ list(,$block) = $child;
616
+ $this->set(self::$namespaces[$block->type] . $block->name, $block);
617
+ break;
618
+ case "extend":
619
+ list(, $selectors) = $child;
620
+ foreach ($selectors as $sel) {
621
+ // only use the first one
622
+ $sel = current($this->evalSelector($sel));
623
+ $this->pushExtends($sel, $out->selectors);
624
+ }
625
+ break;
626
+ case "if":
627
+ list(, $if) = $child;
628
+ if ($this->isTruthy($this->reduce($if->cond, true))) {
629
+ return $this->compileChildren($if->children, $out);
630
+ } else {
631
+ foreach ($if->cases as $case) {
632
+ if ($case->type == "else" ||
633
+ $case->type == "elseif" && $this->isTruthy($this->reduce($case->cond)))
634
+ {
635
+ return $this->compileChildren($case->children, $out);
636
+ }
637
+ }
638
+ }
639
+ break;
640
+ case "return":
641
+ return $this->reduce($child[1], true);
642
+ case "each":
643
+ list(,$each) = $child;
644
+ $list = $this->coerceList($this->reduce($each->list));
645
+ foreach ($list[2] as $item) {
646
+ $this->pushEnv();
647
+ $this->set($each->var, $item);
648
+ // TODO: allow return from here
649
+ $this->compileChildren($each->children, $out);
650
+ $this->popEnv();
651
+ }
652
+ break;
653
+ case "while":
654
+ list(,$while) = $child;
655
+ while ($this->isTruthy($this->reduce($while->cond, true))) {
656
+ $ret = $this->compileChildren($while->children, $out);
657
+ if ($ret) return $ret;
658
+ }
659
+ break;
660
+ case "for":
661
+ list(,$for) = $child;
662
+ $start = $this->reduce($for->start, true);
663
+ $start = $start[1];
664
+ $end = $this->reduce($for->end, true);
665
+ $end = $end[1];
666
+ $d = $start < $end ? 1 : -1;
667
+
668
+ while (true) {
669
+ if ((!$for->until && $start - $d == $end) ||
670
+ ($for->until && $start == $end))
671
+ {
672
+ break;
673
+ }
674
+
675
+ $this->set($for->var, array("number", $start, ""));
676
+ $start += $d;
677
+
678
+ $ret = $this->compileChildren($for->children, $out);
679
+ if ($ret) return $ret;
680
+ }
681
+
682
+ break;
683
+ case "nestedprop":
684
+ list(,$prop) = $child;
685
+ $prefixed = array();
686
+ $prefix = $this->compileValue($prop->prefix) . "-";
687
+ foreach ($prop->children as $child) {
688
+ if ($child[0] == "assign") {
689
+ array_unshift($child[1][2], $prefix);
690
+ }
691
+ if ($child[0] == "nestedprop") {
692
+ array_unshift($child[1]->prefix[2], $prefix);
693
+ }
694
+ $prefixed[] = $child;
695
+ }
696
+ $this->compileChildren($prefixed, $out);
697
+ break;
698
+ case "include": // including a mixin
699
+ list(,$name, $argValues, $content) = $child;
700
+ $mixin = $this->get(self::$namespaces["mixin"] . $name, false);
701
+ if (!$mixin) {
702
+ $this->throwError("Undefined mixin $name");
703
+ }
704
+
705
+ $callingScope = $this->env;
706
+
707
+ // push scope, apply args
708
+ $this->pushEnv();
709
+ if ($this->env->depth > 0) {
710
+ $this->env->depth--;
711
+ }
712
+
713
+ if (!is_null($content)) {
714
+ $content->scope = $callingScope;
715
+ $this->setRaw(self::$namespaces["special"] . "content", $content);
716
+ }
717
+
718
+ if (!is_null($mixin->args)) {
719
+ $this->applyArguments($mixin->args, $argValues);
720
+ }
721
+
722
+ foreach ($mixin->children as $child) {
723
+ $this->compileChild($child, $out);
724
+ }
725
+
726
+ $this->popEnv();
727
+
728
+ break;
729
+ case "mixin_content":
730
+ $content = $this->get(self::$namespaces["special"] . "content");
731
+ if (is_null($content)) {
732
+ $this->throwError("Expected @content inside of mixin");
733
+ }
734
+
735
+ $strongTypes = array('include', 'block', 'for', 'while');
736
+ foreach ($content->children as $child) {
737
+ $this->storeEnv = (in_array($child[0], $strongTypes))
738
+ ? null
739
+ : $content->scope;
740
+
741
+ $this->compileChild($child, $out);
742
+ }
743
+
744
+ unset($this->storeEnv);
745
+ break;
746
+ case "debug":
747
+ list(,$value, $pos) = $child;
748
+ $line = $this->parser->getLineNo($pos);
749
+ $value = $this->compileValue($this->reduce($value, true));
750
+ fwrite(STDERR, "Line $line DEBUG: $value\n");
751
+ break;
752
+ default:
753
+ $this->throwError("unknown child type: $child[0]");
754
+ }
755
+ }
756
+
757
+ protected function expToString($exp) {
758
+ list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
759
+ $content = array($this->reduce($left));
760
+ if ($whiteLeft) $content[] = " ";
761
+ $content[] = $op;
762
+ if ($whiteRight) $content[] = " ";
763
+ $content[] = $this->reduce($right);
764
+ return array("string", "", $content);
765
+ }
766
+
767
+ protected function isTruthy($value) {
768
+ return $value != self::$false && $value != self::$null;
769
+ }
770
+
771
+ // should $value cause its operand to eval
772
+ protected function shouldEval($value) {
773
+ switch ($value[0]) {
774
+ case "exp":
775
+ if ($value[1] == "/") {
776
+ return $this->shouldEval($value[2], $value[3]);
777
+ }
778
+ case "var":
779
+ case "fncall":
780
+ return true;
781
+ }
782
+ return false;
783
+ }
784
+
785
+ protected function reduce($value, $inExp = false) {
786
+ list($type) = $value;
787
+ switch ($type) {
788
+ case "exp":
789
+ list(, $op, $left, $right, $inParens) = $value;
790
+ $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
791
+
792
+ $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
793
+
794
+ $left = $this->reduce($left, true);
795
+ $right = $this->reduce($right, true);
796
+
797
+ // only do division in special cases
798
+ if ($opName == "div" && !$inParens && !$inExp) {
799
+ if ($left[0] != "color" && $right[0] != "color") {
800
+ return $this->expToString($value);
801
+ }
802
+ }
803
+
804
+ $left = $this->coerceForExpression($left);
805
+ $right = $this->coerceForExpression($right);
806
+
807
+ $ltype = $left[0];
808
+ $rtype = $right[0];
809
+
810
+ // this tries:
811
+ // 1. op_[op name]_[left type]_[right type]
812
+ // 2. op_[left type]_[right type] (passing the op as first arg
813
+ // 3. op_[op name]
814
+ $fn = "op_${opName}_${ltype}_${rtype}";
815
+ if (is_callable(array($this, $fn)) ||
816
+ (($fn = "op_${ltype}_${rtype}") &&
817
+ is_callable(array($this, $fn)) &&
818
+ $passOp = true) ||
819
+ (($fn = "op_${opName}") &&
820
+ is_callable(array($this, $fn)) &&
821
+ $genOp = true))
822
+ {
823
+ $unitChange = false;
824
+ if (!isset($genOp) &&
825
+ $left[0] == "number" && $right[0] == "number")
826
+ {
827
+ if ($opName == "mod" && $right[2] != "") {
828
+ $this->throwError("Cannot modulo by a number with units: $right[1]$right[2].");
829
+ }
830
+
831
+ $unitChange = true;
832
+ $emptyUnit = $left[2] == "" || $right[2] == "";
833
+ $targetUnit = "" != $left[2] ? $left[2] : $right[2];
834
+
835
+ if ($opName != "mul") {
836
+ $left[2] = "" != $left[2] ? $left[2] : $targetUnit;
837
+ $right[2] = "" != $right[2] ? $right[2] : $targetUnit;
838
+ }
839
+
840
+ if ($opName != "mod") {
841
+ $left = $this->normalizeNumber($left);
842
+ $right = $this->normalizeNumber($right);
843
+ }
844
+
845
+ if ($opName == "div" && !$emptyUnit && $left[2] == $right[2]) {
846
+ $targetUnit = "";
847
+ }
848
+
849
+ if ($opName == "mul") {
850
+ $left[2] = "" != $left[2] ? $left[2] : $right[2];
851
+ $right[2] = "" != $right[2] ? $right[2] : $left[2];
852
+ } elseif ($opName == "div" && $left[2] == $right[2]) {
853
+ $left[2] = "";
854
+ $right[2] = "";
855
+ }
856
+ }
857
+
858
+ $shouldEval = $inParens || $inExp;
859
+ if (isset($passOp)) {
860
+ $out = $this->$fn($op, $left, $right, $shouldEval);
861
+ } else {
862
+ $out = $this->$fn($left, $right, $shouldEval);
863
+ }
864
+
865
+ if (!is_null($out)) {
866
+ if ($unitChange && $out[0] == "number") {
867
+ $out = $this->coerceUnit($out, $targetUnit);
868
+ }
869
+ return $out;
870
+ }
871
+ }
872
+
873
+ return $this->expToString($value);
874
+ case "unary":
875
+ list(, $op, $exp, $inParens) = $value;
876
+ $inExp = $inExp || $this->shouldEval($exp);
877
+
878
+ $exp = $this->reduce($exp);
879
+ if ($exp[0] == "number") {
880
+ switch ($op) {
881
+ case "+":
882
+ return $exp;
883
+ case "-":
884
+ $exp[1] *= -1;
885
+ return $exp;
886
+ }
887
+ }
888
+
889
+ if ($op == "not") {
890
+ if ($inExp || $inParens) {
891
+ if ($exp == self::$false) {
892
+ return self::$true;
893
+ } else {
894
+ return self::$false;
895
+ }
896
+ } else {
897
+ $op = $op . " ";
898
+ }
899
+ }
900
+
901
+ return array("string", "", array($op, $exp));
902
+ case "var":
903
+ list(, $name) = $value;
904
+ return $this->reduce($this->get($name));
905
+ case "list":
906
+ foreach ($value[2] as &$item) {
907
+ $item = $this->reduce($item);
908
+ }
909
+ return $value;
910
+ case "string":
911
+ foreach ($value[2] as &$item) {
912
+ if (is_array($item)) {
913
+ $item = $this->reduce($item);
914
+ }
915
+ }
916
+ return $value;
917
+ case "interpolate":
918
+ $value[1] = $this->reduce($value[1]);
919
+ return $value;
920
+ case "fncall":
921
+ list(,$name, $argValues) = $value;
922
+
923
+ // user defined function?
924
+ $func = $this->get(self::$namespaces["function"] . $name, false);
925
+ if ($func) {
926
+ $this->pushEnv();
927
+
928
+ // set the args
929
+ if (isset($func->args)) {
930
+ $this->applyArguments($func->args, $argValues);
931
+ }
932
+
933
+ // throw away lines and children
934
+ $tmp = (object)array(
935
+ "lines" => array(),
936
+ "children" => array()
937
+ );
938
+ $ret = $this->compileChildren($func->children, $tmp);
939
+ $this->popEnv();
940
+
941
+ return is_null($ret) ? self::$defaultValue : $ret;
942
+ }
943
+
944
+ // built in function
945
+ if ($this->callBuiltin($name, $argValues, $returnValue)) {
946
+ return $returnValue;
947
+ }
948
+
949
+ // need to flatten the arguments into a list
950
+ $listArgs = array();
951
+ foreach ((array)$argValues as $arg) {
952
+ if (empty($arg[0])) {
953
+ $listArgs[] = $this->reduce($arg[1]);
954
+ }
955
+ }
956
+ return array("function", $name, array("list", ",", $listArgs));
957
+ default:
958
+ return $value;
959
+ }
960
+ }
961
+
962
+ public function normalizeValue($value) {
963
+ $value = $this->coerceForExpression($this->reduce($value));
964
+ list($type) = $value;
965
+
966
+ switch ($type) {
967
+ case "list":
968
+ $value = $this->extractInterpolation($value);
969
+ if ($value[0] != "list") {
970
+ return array("keyword", $this->compileValue($value));
971
+ }
972
+ foreach ($value[2] as $key => $item) {
973
+ $value[2][$key] = $this->normalizeValue($item);
974
+ }
975
+ return $value;
976
+ case "number":
977
+ return $this->normalizeNumber($value);
978
+ default:
979
+ return $value;
980
+ }
981
+ }
982
+
983
+ // just does physical lengths for now
984
+ protected function normalizeNumber($number) {
985
+ list(, $value, $unit) = $number;
986
+ if (isset(self::$unitTable["in"][$unit])) {
987
+ $conv = self::$unitTable["in"][$unit];
988
+ return array("number", $value / $conv, "in");
989
+ }
990
+ return $number;
991
+ }
992
+
993
+ // $number should be normalized
994
+ protected function coerceUnit($number, $unit) {
995
+ list(, $value, $baseUnit) = $number;
996
+ if (isset(self::$unitTable[$baseUnit][$unit])) {
997
+ $value = $value * self::$unitTable[$baseUnit][$unit];
998
+ }
999
+
1000
+ return array("number", $value, $unit);
1001
+ }
1002
+
1003
+ protected function op_add_number_number($left, $right) {
1004
+ return array("number", $left[1] + $right[1], $left[2]);
1005
+ }
1006
+
1007
+ protected function op_mul_number_number($left, $right) {
1008
+ return array("number", $left[1] * $right[1], $left[2]);
1009
+ }
1010
+
1011
+ protected function op_sub_number_number($left, $right) {
1012
+ return array("number", $left[1] - $right[1], $left[2]);
1013
+ }
1014
+
1015
+ protected function op_div_number_number($left, $right) {
1016
+ return array("number", $left[1] / $right[1], $left[2]);
1017
+ }
1018
+
1019
+ protected function op_mod_number_number($left, $right) {
1020
+ return array("number", $left[1] % $right[1], $left[2]);
1021
+ }
1022
+
1023
+ // adding strings
1024
+ protected function op_add($left, $right) {
1025
+ if ($strLeft = $this->coerceString($left)) {
1026
+ if ($right[0] == "string") {
1027
+ $right[1] = "";
1028
+ }
1029
+ $strLeft[2][] = $right;
1030
+ return $strLeft;
1031
+ }
1032
+
1033
+ if ($strRight = $this->coerceString($right)) {
1034
+ if ($left[0] == "string") {
1035
+ $left[1] = "";
1036
+ }
1037
+ array_unshift($strRight[2], $left);
1038
+ return $strRight;
1039
+ }
1040
+ }
1041
+
1042
+ protected function op_and($left, $right, $shouldEval) {
1043
+ if (!$shouldEval) return;
1044
+ if ($left != self::$false) return $right;
1045
+ return $left;
1046
+ }
1047
+
1048
+ protected function op_or($left, $right, $shouldEval) {
1049
+ if (!$shouldEval) return;
1050
+ if ($left != self::$false) return $left;
1051
+ return $right;
1052
+ }
1053
+
1054
+ protected function op_color_color($op, $left, $right) {
1055
+ $out = array('color');
1056
+ foreach (range(1, 3) as $i) {
1057
+ $lval = isset($left[$i]) ? $left[$i] : 0;
1058
+ $rval = isset($right[$i]) ? $right[$i] : 0;
1059
+ switch ($op) {
1060
+ case '+':
1061
+ $out[] = $lval + $rval;
1062
+ break;
1063
+ case '-':
1064
+ $out[] = $lval - $rval;
1065
+ break;
1066
+ case '*':
1067
+ $out[] = $lval * $rval;
1068
+ break;
1069
+ case '%':
1070
+ $out[] = $lval % $rval;
1071
+ break;
1072
+ case '/':
1073
+ if ($rval == 0) {
1074
+ $this->throwError("color: Can't divide by zero");
1075
+ }
1076
+ $out[] = $lval / $rval;
1077
+ break;
1078
+ case "==":
1079
+ return $this->op_eq($left, $right);
1080
+ case "!=":
1081
+ return $this->op_neq($left, $right);
1082
+ default:
1083
+ $this->throwError("color: unknown op $op");
1084
+ }
1085
+ }
1086
+
1087
+ if (isset($left[4])) $out[4] = $left[4];
1088
+ elseif (isset($right[4])) $out[4] = $right[4];
1089
+
1090
+ return $this->fixColor($out);
1091
+ }
1092
+
1093
+ protected function op_color_number($op, $left, $right) {
1094
+ $value = $right[1];
1095
+ return $this->op_color_color($op, $left,
1096
+ array("color", $value, $value, $value));
1097
+ }
1098
+
1099
+ protected function op_number_color($op, $left, $right) {
1100
+ $value = $left[1];
1101
+ return $this->op_color_color($op,
1102
+ array("color", $value, $value, $value), $right);
1103
+ }
1104
+
1105
+ protected function op_eq($left, $right) {
1106
+ if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
1107
+ $lStr[1] = "";
1108
+ $rStr[1] = "";
1109
+ return $this->toBool($this->compileValue($lStr) == $this->compileValue($rStr));
1110
+ }
1111
+
1112
+ return $this->toBool($left == $right);
1113
+ }
1114
+
1115
+ protected function op_neq($left, $right) {
1116
+ return $this->toBool($left != $right);
1117
+ }
1118
+
1119
+ protected function op_gte_number_number($left, $right) {
1120
+ return $this->toBool($left[1] >= $right[1]);
1121
+ }
1122
+
1123
+ protected function op_gt_number_number($left, $right) {
1124
+ return $this->toBool($left[1] > $right[1]);
1125
+ }
1126
+
1127
+ protected function op_lte_number_number($left, $right) {
1128
+ return $this->toBool($left[1] <= $right[1]);
1129
+ }
1130
+
1131
+ protected function op_lt_number_number($left, $right) {
1132
+ return $this->toBool($left[1] < $right[1]);
1133
+ }
1134
+
1135
+ protected function toBool($thing) {
1136
+ return $thing ? self::$true : self::$false;
1137
+ }
1138
+
1139
+ /**
1140
+ * Compiles a primitive value into a CSS property value.
1141
+ *
1142
+ * Values in scssphp are typed by being wrapped in arrays, their format is
1143
+ * typically:
1144
+ *
1145
+ * array(type, contents [, additional_contents]*)
1146
+ *
1147
+ * The input is expected to be reduced. This function will not work on
1148
+ * things like expressions and variables.
1149
+ *
1150
+ * @param array $value
1151
+ */
1152
+ protected function compileValue($value) {
1153
+ $value = $this->reduce($value);
1154
+
1155
+ list($type) = $value;
1156
+ switch ($type) {
1157
+ case "keyword":
1158
+ return $value[1];
1159
+ case "color":
1160
+ // [1] - red component (either number for a %)
1161
+ // [2] - green component
1162
+ // [3] - blue component
1163
+ // [4] - optional alpha component
1164
+ list(, $r, $g, $b) = $value;
1165
+
1166
+ $r = round($r);
1167
+ $g = round($g);
1168
+ $b = round($b);
1169
+
1170
+ if (count($value) == 5 && $value[4] != 1) { // rgba
1171
+ return 'rgba('.$r.', '.$g.', '.$b.', '.$value[4].')';
1172
+ }
1173
+
1174
+ $h = sprintf("#%02x%02x%02x", $r, $g, $b);
1175
+
1176
+ // Converting hex color to short notation (e.g. #003399 to #039)
1177
+ if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
1178
+ $h = '#' . $h[1] . $h[3] . $h[5];
1179
+ }
1180
+
1181
+ return $h;
1182
+ case "number":
1183
+ return round($value[1], self::$numberPrecision) . $value[2];
1184
+ case "string":
1185
+ return $value[1] . $this->compileStringContent($value) . $value[1];
1186
+ case "function":
1187
+ $args = !empty($value[2]) ? $this->compileValue($value[2]) : "";
1188
+ return "$value[1]($args)";
1189
+ case "list":
1190
+ $value = $this->extractInterpolation($value);
1191
+ if ($value[0] != "list") return $this->compileValue($value);
1192
+
1193
+ list(, $delim, $items) = $value;
1194
+
1195
+ $filtered = array();
1196
+ foreach ($items as $item) {
1197
+ if ($item[0] == "null") continue;
1198
+ $filtered[] = $this->compileValue($item);
1199
+ }
1200
+
1201
+ return implode("$delim ", $filtered);
1202
+ case "interpolated": # node created by extractInterpolation
1203
+ list(, $interpolate, $left, $right) = $value;
1204
+ list(,, $whiteLeft, $whiteRight) = $interpolate;
1205
+
1206
+ $left = count($left[2]) > 0 ?
1207
+ $this->compileValue($left).$whiteLeft : "";
1208
+
1209
+ $right = count($right[2]) > 0 ?
1210
+ $whiteRight.$this->compileValue($right) : "";
1211
+
1212
+ return $left.$this->compileValue($interpolate).$right;
1213
+
1214
+ case "interpolate": # raw parse node
1215
+ list(, $exp) = $value;
1216
+
1217
+ // strip quotes if it's a string
1218
+ $reduced = $this->reduce($exp);
1219
+ switch ($reduced[0]) {
1220
+ case "string":
1221
+ $reduced = array("keyword",
1222
+ $this->compileStringContent($reduced));
1223
+ break;
1224
+ case "null":
1225
+ $reduced = array("keyword", "");
1226
+ }
1227
+
1228
+ return $this->compileValue($reduced);
1229
+ case "null":
1230
+ return "null";
1231
+ default:
1232
+ $this->throwError("unknown value type: $type");
1233
+ }
1234
+ }
1235
+
1236
+ protected function compileStringContent($string) {
1237
+ $parts = array();
1238
+ foreach ($string[2] as $part) {
1239
+ if (is_array($part)) {
1240
+ $parts[] = $this->compileValue($part);
1241
+ } else {
1242
+ $parts[] = $part;
1243
+ }
1244
+ }
1245
+
1246
+ return implode($parts);
1247
+ }
1248
+
1249
+ // doesn't need to be recursive, compileValue will handle that
1250
+ protected function extractInterpolation($list) {
1251
+ $items = $list[2];
1252
+ foreach ($items as $i => $item) {
1253
+ if ($item[0] == "interpolate") {
1254
+ $before = array("list", $list[1], array_slice($items, 0, $i));
1255
+ $after = array("list", $list[1], array_slice($items, $i + 1));
1256
+ return array("interpolated", $item, $before, $after);
1257
+ }
1258
+ }
1259
+ return $list;
1260
+ }
1261
+
1262
+ // find the final set of selectors
1263
+ protected function multiplySelectors($env) {
1264
+ $envs = array();
1265
+ while (null !== $env) {
1266
+ if (!empty($env->selectors)) {
1267
+ $envs[] = $env;
1268
+ }
1269
+ $env = $env->parent;
1270
+ };
1271
+
1272
+ $selectors = array();
1273
+ $parentSelectors = array(array());
1274
+ while ($env = array_pop($envs)) {
1275
+ $selectors = array();
1276
+ foreach ($env->selectors as $selector) {
1277
+ foreach ($parentSelectors as $parent) {
1278
+ $selectors[] = $this->joinSelectors($parent, $selector);
1279
+ }
1280
+ }
1281
+ $parentSelectors = $selectors;
1282
+ }
1283
+
1284
+ return $selectors;
1285
+ }
1286
+
1287
+ // looks for & to replace, or append parent before child
1288
+ protected function joinSelectors($parent, $child) {
1289
+ $setSelf = false;
1290
+ $out = array();
1291
+ foreach ($child as $part) {
1292
+ $newPart = array();
1293
+ foreach ($part as $p) {
1294
+ if ($p == self::$selfSelector) {
1295
+ $setSelf = true;
1296
+ foreach ($parent as $i => $parentPart) {
1297
+ if ($i > 0) {
1298
+ $out[] = $newPart;
1299
+ $newPart = array();
1300
+ }
1301
+
1302
+ foreach ($parentPart as $pp) {
1303
+ $newPart[] = $pp;
1304
+ }
1305
+ }
1306
+ } else {
1307
+ $newPart[] = $p;
1308
+ }
1309
+ }
1310
+
1311
+ $out[] = $newPart;
1312
+ }
1313
+
1314
+ return $setSelf ? $out : array_merge($parent, $child);
1315
+ }
1316
+
1317
+ protected function multiplyMedia($env, $childQueries = null) {
1318
+ if (is_null($env) ||
1319
+ !empty($env->block->type) && $env->block->type != "media")
1320
+ {
1321
+ return $childQueries;
1322
+ }
1323
+
1324
+ // plain old block, skip
1325
+ if (empty($env->block->type)) {
1326
+ return $this->multiplyMedia($env->parent, $childQueries);
1327
+ }
1328
+
1329
+ $parentQueries = $env->block->queryList;
1330
+ if ($childQueries == null) {
1331
+ $childQueries = $parentQueries;
1332
+ } else {
1333
+ $originalQueries = $childQueries;
1334
+ $childQueries = array();
1335
+
1336
+ foreach ($parentQueries as $parentQuery){
1337
+ foreach ($originalQueries as $childQuery) {
1338
+ $childQueries []= array_merge($parentQuery, $childQuery);
1339
+ }
1340
+ }
1341
+ }
1342
+
1343
+ return $this->multiplyMedia($env->parent, $childQueries);
1344
+ }
1345
+
1346
+ // convert something to list
1347
+ protected function coerceList($item, $delim = ",") {
1348
+ if (!is_null($item) && $item[0] == "list") {
1349
+ return $item;
1350
+ }
1351
+
1352
+ return array("list", $delim, is_null($item) ? array(): array($item));
1353
+ }
1354
+
1355
+ protected function applyArguments($argDef, $argValues) {
1356
+ $hasVariable = false;
1357
+ $args = array();
1358
+ foreach ($argDef as $i => $arg) {
1359
+ list($name, $default, $isVariable) = $argDef[$i];
1360
+ $args[$name] = array($i, $name, $default, $isVariable);
1361
+ $hasVariable |= $isVariable;
1362
+ }
1363
+
1364
+ $keywordArgs = array();
1365
+ $deferredKeywordArgs = array();
1366
+ $remaining = array();
1367
+ // assign the keyword args
1368
+ foreach ((array) $argValues as $arg) {
1369
+ if (!empty($arg[0])) {
1370
+ if (!isset($args[$arg[0][1]])) {
1371
+ if ($hasVariable) {
1372
+ $deferredKeywordArgs[$arg[0][1]] = $arg[1];
1373
+ } else {
1374
+ $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
1375
+ }
1376
+ } elseif ($args[$arg[0][1]][0] < count($remaining)) {
1377
+ $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
1378
+ } else {
1379
+ $keywordArgs[$arg[0][1]] = $arg[1];
1380
+ }
1381
+ } elseif (count($keywordArgs)) {
1382
+ $this->throwError('Positional arguments must come before keyword arguments.');
1383
+ } elseif ($arg[2] == true) {
1384
+ $val = $this->reduce($arg[1], true);
1385
+ if ($val[0] == "list") {
1386
+ foreach ($val[2] as $name => $item) {
1387
+ if (!is_numeric($name)) {
1388
+ $keywordArgs[$name] = $item;
1389
+ } else {
1390
+ $remaining[] = $item;
1391
+ }
1392
+ }
1393
+ } else {
1394
+ $remaining[] = $val;
1395
+ }
1396
+ } else {
1397
+ $remaining[] = $arg[1];
1398
+ }
1399
+ }
1400
+
1401
+ foreach ($args as $arg) {
1402
+ list($i, $name, $default, $isVariable) = $arg;
1403
+ if ($isVariable) {
1404
+ $val = array("list", ",", array());
1405
+ for ($count = count($remaining); $i < $count; $i++) {
1406
+ $val[2][] = $remaining[$i];
1407
+ }
1408
+ foreach ($deferredKeywordArgs as $itemName => $item) {
1409
+ $val[2][$itemName] = $item;
1410
+ }
1411
+ } elseif (isset($remaining[$i])) {
1412
+ $val = $remaining[$i];
1413
+ } elseif (isset($keywordArgs[$name])) {
1414
+ $val = $keywordArgs[$name];
1415
+ } elseif (!empty($default)) {
1416
+ $val = $default;
1417
+ } else {
1418
+ $this->throwError("Missing argument $name");
1419
+ }
1420
+
1421
+ $this->set($name, $this->reduce($val, true), true);
1422
+ }
1423
+ }
1424
+
1425
+ protected function pushEnv($block=null) {
1426
+ $env = new stdClass;
1427
+ $env->parent = $this->env;
1428
+ $env->store = array();
1429
+ $env->block = $block;
1430
+ $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
1431
+
1432
+ $this->env = $env;
1433
+ return $env;
1434
+ }
1435
+
1436
+ protected function normalizeName($name) {
1437
+ return str_replace("-", "_", $name);
1438
+ }
1439
+
1440
+ protected function getStoreEnv() {
1441
+ return isset($this->storeEnv) ? $this->storeEnv : $this->env;
1442
+ }
1443
+
1444
+ protected function set($name, $value, $shadow=false) {
1445
+ $name = $this->normalizeName($name);
1446
+
1447
+ if ($shadow) {
1448
+ $this->setRaw($name, $value);
1449
+ } else {
1450
+ $this->setExisting($name, $value);
1451
+ }
1452
+ }
1453
+
1454
+ // todo: this is bugged?
1455
+ protected function setExisting($name, $value, $env = null) {
1456
+ if (is_null($env)) $env = $this->getStoreEnv();
1457
+
1458
+ if (isset($env->store[$name])) {
1459
+ $env->store[$name] = $value;
1460
+ } elseif (!is_null($env->parent)) {
1461
+ $this->setExisting($name, $value, $env->parent);
1462
+ } else {
1463
+ $this->env->store[$name] = $value;
1464
+ }
1465
+ }
1466
+
1467
+ protected function setRaw($name, $value) {
1468
+ $this->env->store[$name] = $value;
1469
+ }
1470
+
1471
+ protected function get($name, $defaultValue = null, $env = null) {
1472
+ $name = $this->normalizeName($name);
1473
+
1474
+ if (is_null($env)) $env = $this->getStoreEnv();
1475
+ if (is_null($defaultValue)) $defaultValue = self::$defaultValue;
1476
+
1477
+ if (isset($env->store[$name])) {
1478
+ return $env->store[$name];
1479
+ } elseif (isset($env->parent)) {
1480
+ return $this->get($name, $defaultValue, $env->parent);
1481
+ }
1482
+
1483
+ return $defaultValue; // found nothing
1484
+ }
1485
+
1486
+ protected function popEnv() {
1487
+ $env = $this->env;
1488
+ $this->env = $this->env->parent;
1489
+ return $env;
1490
+ }
1491
+
1492
+ public function getParsedFiles() {
1493
+ return $this->parsedFiles;
1494
+ }
1495
+
1496
+ public function addImportPath($path) {
1497
+ $this->importPaths[] = $path;
1498
+ }
1499
+
1500
+ public function setImportPaths($path) {
1501
+ $this->importPaths = (array)$path;
1502
+ }
1503
+
1504
+ public function setFormatter($formatterName) {
1505
+ $this->formatter = $formatterName;
1506
+ }
1507
+
1508
+ public function registerFunction($name, $func) {
1509
+ $this->userFunctions[$this->normalizeName($name)] = $func;
1510
+ }
1511
+
1512
+ public function unregisterFunction($name) {
1513
+ unset($this->userFunctions[$this->normalizeName($name)]);
1514
+ }
1515
+
1516
+ protected function importFile($path, $out) {
1517
+ // see if tree is cached
1518
+ $realPath = realpath($path);
1519
+ if (isset($this->importCache[$realPath])) {
1520
+ $tree = $this->importCache[$realPath];
1521
+ } else {
1522
+ $code = file_get_contents($path);
1523
+ $parser = new scss_parser($path, false);
1524
+ $tree = $parser->parse($code);
1525
+ $this->parsedFiles[] = $path;
1526
+
1527
+ $this->importCache[$realPath] = $tree;
1528
+ }
1529
+
1530
+ $pi = pathinfo($path);
1531
+ array_unshift($this->importPaths, $pi['dirname']);
1532
+ $this->compileChildren($tree->children, $out);
1533
+ array_shift($this->importPaths);
1534
+ }
1535
+
1536
+ // results the file path for an import url if it exists
1537
+ protected function findImport($url) {
1538
+ $urls = array();
1539
+
1540
+ // for "normal" scss imports (ignore vanilla css and external requests)
1541
+ if (!preg_match('/\.css|^http:\/\/$/', $url)) {
1542
+ // try both normal and the _partial filename
1543
+ $urls = array($url, preg_replace('/[^\/]+$/', '_\0', $url));
1544
+ }
1545
+
1546
+ foreach ($this->importPaths as $dir) {
1547
+ if (is_string($dir)) {
1548
+ // check urls for normal import paths
1549
+ foreach ($urls as $full) {
1550
+ $full = $dir .
1551
+ (!empty($dir) && substr($dir, -1) != '/' ? '/' : '') .
1552
+ $full;
1553
+
1554
+ if ($this->fileExists($file = $full.'.scss') ||
1555
+ $this->fileExists($file = $full))
1556
+ {
1557
+ return $file;
1558
+ }
1559
+ }
1560
+ } else {
1561
+ // check custom callback for import path
1562
+ $file = call_user_func($dir,$url,$this);
1563
+ if ($file !== null) {
1564
+ return $file;
1565
+ }
1566
+ }
1567
+ }
1568
+
1569
+ return null;
1570
+ }
1571
+
1572
+ protected function fileExists($name) {
1573
+ return is_file($name);
1574
+ }
1575
+
1576
+ protected function callBuiltin($name, $args, &$returnValue) {
1577
+ // try a lib function
1578
+ $name = $this->normalizeName($name);
1579
+ $libName = "lib_".$name;
1580
+ $f = array($this, $libName);
1581
+ $prototype = isset(self::$$libName) ? self::$$libName : null;
1582
+
1583
+ if (is_callable($f)) {
1584
+ $sorted = $this->sortArgs($prototype, $args);
1585
+ foreach ($sorted as &$val) {
1586
+ $val = $this->reduce($val, true);
1587
+ }
1588
+ $returnValue = call_user_func($f, $sorted, $this);
1589
+ } elseif (isset($this->userFunctions[$name])) {
1590
+ // see if we can find a user function
1591
+ $fn = $this->userFunctions[$name];
1592
+
1593
+ foreach ($args as &$val) {
1594
+ $val = $this->reduce($val[1], true);
1595
+ }
1596
+
1597
+ $returnValue = call_user_func($fn, $args, $this);
1598
+ }
1599
+
1600
+ if (isset($returnValue)) {
1601
+ // coerce a php value into a scss one
1602
+ if (is_numeric($returnValue)) {
1603
+ $returnValue = array('number', $returnValue, "");
1604
+ } elseif (is_bool($returnValue)) {
1605
+ $returnValue = $returnValue ? self::$true : self::$false;
1606
+ } elseif (!is_array($returnValue)) {
1607
+ $returnValue = array('keyword', $returnValue);
1608
+ }
1609
+
1610
+ return true;
1611
+ }
1612
+
1613
+ return false;
1614
+ }
1615
+
1616
+ // sorts any keyword arguments
1617
+ // TODO: merge with apply arguments
1618
+ protected function sortArgs($prototype, $args) {
1619
+ $keyArgs = array();
1620
+ $posArgs = array();
1621
+
1622
+ foreach ($args as $arg) {
1623
+ list($key, $value) = $arg;
1624
+ $key = $key[1];
1625
+ if (empty($key)) {
1626
+ $posArgs[] = $value;
1627
+ } else {
1628
+ $keyArgs[$key] = $value;
1629
+ }
1630
+ }
1631
+
1632
+ if (is_null($prototype)) return $posArgs;
1633
+
1634
+ $finalArgs = array();
1635
+ foreach ($prototype as $i => $names) {
1636
+ if (isset($posArgs[$i])) {
1637
+ $finalArgs[] = $posArgs[$i];
1638
+ continue;
1639
+ }
1640
+
1641
+ $set = false;
1642
+ foreach ((array)$names as $name) {
1643
+ if (isset($keyArgs[$name])) {
1644
+ $finalArgs[] = $keyArgs[$name];
1645
+ $set = true;
1646
+ break;
1647
+ }
1648
+ }
1649
+
1650
+ if (!$set) {
1651
+ $finalArgs[] = null;
1652
+ }
1653
+ }
1654
+
1655
+ return $finalArgs;
1656
+ }
1657
+
1658
+ protected function coerceForExpression($value) {
1659
+ if ($color = $this->coerceColor($value)) {
1660
+ return $color;
1661
+ }
1662
+
1663
+ return $value;
1664
+ }
1665
+
1666
+ protected function coerceColor($value) {
1667
+ switch ($value[0]) {
1668
+ case "color": return $value;
1669
+ case "keyword":
1670
+ $name = $value[1];
1671
+ if (isset(self::$cssColors[$name])) {
1672
+ @list($r, $g, $b, $a) = explode(',', self::$cssColors[$name]);
1673
+ return isset($a)
1674
+ ? array('color', (int) $r, (int) $g, (int) $b, (int) $a)
1675
+ : array('color', (int) $r, (int) $g, (int) $b);
1676
+ }
1677
+ return null;
1678
+ }
1679
+
1680
+ return null;
1681
+ }
1682
+
1683
+ protected function coerceString($value) {
1684
+ switch ($value[0]) {
1685
+ case "string":
1686
+ return $value;
1687
+ case "keyword":
1688
+ return array("string", "", array($value[1]));
1689
+ }
1690
+ return null;
1691
+ }
1692
+
1693
+ protected function assertList($value) {
1694
+ if ($value[0] != "list")
1695
+ $this->throwError("expecting list");
1696
+ return $value;
1697
+ }
1698
+
1699
+ protected function assertColor($value) {
1700
+ if ($color = $this->coerceColor($value)) return $color;
1701
+ $this->throwError("expecting color");
1702
+ }
1703
+
1704
+ protected function assertNumber($value) {
1705
+ if ($value[0] != "number")
1706
+ $this->throwError("expecting number");
1707
+ return $value[1];
1708
+ }
1709
+
1710
+ protected function coercePercent($value) {
1711
+ if ($value[0] == "number") {
1712
+ if ($value[2] == "%") {
1713
+ return $value[1] / 100;
1714
+ }
1715
+ return $value[1];
1716
+ }
1717
+ return 0;
1718
+ }
1719
+
1720
+ // make sure a color's components don't go out of bounds
1721
+ protected function fixColor($c) {
1722
+ foreach (range(1, 3) as $i) {
1723
+ if ($c[$i] < 0) $c[$i] = 0;
1724
+ if ($c[$i] > 255) $c[$i] = 255;
1725
+ }
1726
+
1727
+ return $c;
1728
+ }
1729
+
1730
+ function toHSL($red, $green, $blue) {
1731
+ $r = $red / 255;
1732
+ $g = $green / 255;
1733
+ $b = $blue / 255;
1734
+
1735
+ $min = min($r, $g, $b);
1736
+ $max = max($r, $g, $b);
1737
+ $d = $max - $min;
1738
+ $l = ($min + $max) / 2;
1739
+
1740
+ if ($min == $max) {
1741
+ $s = $h = 0;
1742
+ } else {
1743
+ if ($l < 0.5)
1744
+ $s = $d / (2 * $l);
1745
+ else
1746
+ $s = $d / (2 - 2 * $l);
1747
+
1748
+ if ($r == $max)
1749
+ $h = 60 * ($g - $b) / $d;
1750
+ elseif ($g == $max)
1751
+ $h = 60 * ($b - $r) / $d + 120;
1752
+ elseif ($b == $max)
1753
+ $h = 60 * ($r - $g) / $d + 240;
1754
+ }
1755
+
1756
+ return array('hsl', fmod($h, 360), $s * 100, $l * 100);
1757
+ }
1758
+
1759
+ function hueToRGB($m1, $m2, $h) {
1760
+ if ($h < 0)
1761
+ $h += 1;
1762
+ elseif ($h > 1)
1763
+ $h -= 1;
1764
+
1765
+ if ($h * 6 < 1)
1766
+ return $m1 + ($m2 - $m1) * $h * 6;
1767
+
1768
+ if ($h * 2 < 1)
1769
+ return $m2;
1770
+
1771
+ if ($h * 3 < 2)
1772
+ return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
1773
+
1774
+ return $m1;
1775
+ }
1776
+
1777
+ // H from 0 to 360, S and L from 0 to 100
1778
+ function toRGB($hue, $saturation, $lightness) {
1779
+ if ($hue < 0) {
1780
+ $hue += 360;
1781
+ }
1782
+
1783
+ $h = $hue / 360;
1784
+ $s = min(100, max(0, $saturation)) / 100;
1785
+ $l = min(100, max(0, $lightness)) / 100;
1786
+
1787
+ $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
1788
+ $m1 = $l * 2 - $m2;
1789
+
1790
+ $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
1791
+ $g = $this->hueToRGB($m1, $m2, $h) * 255;
1792
+ $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
1793
+
1794
+ $out = array('color', $r, $g, $b);
1795
+ return $out;
1796
+ }
1797
+
1798
+ // Built in functions
1799
+
1800
+ protected static $lib_if = array("condition", "if-true", "if-false");
1801
+ protected function lib_if($args) {
1802
+ list($cond,$t, $f) = $args;
1803
+ if ($cond == self::$false) return $f;
1804
+ return $t;
1805
+ }
1806
+
1807
+ protected static $lib_index = array("list", "value");
1808
+ protected function lib_index($args) {
1809
+ list($list, $value) = $args;
1810
+ $list = $this->assertList($list);
1811
+
1812
+ $values = array();
1813
+ foreach ($list[2] as $item) {
1814
+ $values[] = $this->normalizeValue($item);
1815
+ }
1816
+ $key = array_search($this->normalizeValue($value), $values);
1817
+
1818
+ return false === $key ? false : $key + 1;
1819
+ }
1820
+
1821
+ protected static $lib_rgb = array("red", "green", "blue");
1822
+ protected function lib_rgb($args) {
1823
+ list($r,$g,$b) = $args;
1824
+ return array("color", $r[1], $g[1], $b[1]);
1825
+ }
1826
+
1827
+ protected static $lib_rgba = array(
1828
+ array("red", "color"),
1829
+ "green", "blue", "alpha");
1830
+ protected function lib_rgba($args) {
1831
+ if ($color = $this->coerceColor($args[0])) {
1832
+ $num = is_null($args[1]) ? $args[3] : $args[1];
1833
+ $alpha = $this->assertNumber($num);
1834
+ $color[4] = $alpha;
1835
+ return $color;
1836
+ }
1837
+
1838
+ list($r,$g,$b, $a) = $args;
1839
+ return array("color", $r[1], $g[1], $b[1], $a[1]);
1840
+ }
1841
+
1842
+ // helper function for adjust_color, change_color, and scale_color
1843
+ protected function alter_color($args, $fn) {
1844
+ $color = $this->assertColor($args[0]);
1845
+
1846
+ foreach (array(1,2,3,7) as $i) {
1847
+ if (!is_null($args[$i])) {
1848
+ $val = $this->assertNumber($args[$i]);
1849
+ $ii = $i == 7 ? 4 : $i; // alpha
1850
+ $color[$ii] =
1851
+ $this->$fn(isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
1852
+ }
1853
+ }
1854
+
1855
+ if (!is_null($args[4]) || !is_null($args[5]) || !is_null($args[6])) {
1856
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
1857
+ foreach (array(4,5,6) as $i) {
1858
+ if (!is_null($args[$i])) {
1859
+ $val = $this->assertNumber($args[$i]);
1860
+ $hsl[$i - 3] = $this->$fn($hsl[$i - 3], $val, $i);
1861
+ }
1862
+ }
1863
+
1864
+ $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
1865
+ if (isset($color[4])) $rgb[4] = $color[4];
1866
+ $color = $rgb;
1867
+ }
1868
+
1869
+ return $color;
1870
+ }
1871
+
1872
+ protected static $lib_adjust_color = array(
1873
+ "color", "red", "green", "blue",
1874
+ "hue", "saturation", "lightness", "alpha"
1875
+ );
1876
+ protected function adjust_color_helper($base, $alter, $i) {
1877
+ return $base += $alter;
1878
+ }
1879
+ protected function lib_adjust_color($args) {
1880
+ return $this->alter_color($args, "adjust_color_helper");
1881
+ }
1882
+
1883
+ protected static $lib_change_color = array(
1884
+ "color", "red", "green", "blue",
1885
+ "hue", "saturation", "lightness", "alpha"
1886
+ );
1887
+ protected function change_color_helper($base, $alter, $i) {
1888
+ return $alter;
1889
+ }
1890
+ protected function lib_change_color($args) {
1891
+ return $this->alter_color($args, "change_color_helper");
1892
+ }
1893
+
1894
+ protected static $lib_scale_color = array(
1895
+ "color", "red", "green", "blue",
1896
+ "hue", "saturation", "lightness", "alpha"
1897
+ );
1898
+ protected function scale_color_helper($base, $scale, $i) {
1899
+ // 1,2,3 - rgb
1900
+ // 4, 5, 6 - hsl
1901
+ // 7 - a
1902
+ switch ($i) {
1903
+ case 1:
1904
+ case 2:
1905
+ case 3:
1906
+ $max = 255; break;
1907
+ case 4:
1908
+ $max = 360; break;
1909
+ case 7:
1910
+ $max = 1; break;
1911
+ default:
1912
+ $max = 100;
1913
+ }
1914
+
1915
+ $scale = $scale / 100;
1916
+ if ($scale < 0) {
1917
+ return $base * $scale + $base;
1918
+ } else {
1919
+ return ($max - $base) * $scale + $base;
1920
+ }
1921
+ }
1922
+ protected function lib_scale_color($args) {
1923
+ return $this->alter_color($args, "scale_color_helper");
1924
+ }
1925
+
1926
+ protected static $lib_ie_hex_str = array("color");
1927
+ protected function lib_ie_hex_str($args) {
1928
+ $color = $this->coerceColor($args[0]);
1929
+ $color[4] = isset($color[4]) ? round(255*$color[4]) : 255;
1930
+
1931
+ return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
1932
+ }
1933
+
1934
+ protected static $lib_red = array("color");
1935
+ protected function lib_red($args) {
1936
+ $color = $this->coerceColor($args[0]);
1937
+ return $color[1];
1938
+ }
1939
+
1940
+ protected static $lib_green = array("color");
1941
+ protected function lib_green($args) {
1942
+ $color = $this->coerceColor($args[0]);
1943
+ return $color[2];
1944
+ }
1945
+
1946
+ protected static $lib_blue = array("color");
1947
+ protected function lib_blue($args) {
1948
+ $color = $this->coerceColor($args[0]);
1949
+ return $color[3];
1950
+ }
1951
+
1952
+ protected static $lib_alpha = array("color");
1953
+ protected function lib_alpha($args) {
1954
+ if ($color = $this->coerceColor($args[0])) {
1955
+ return isset($color[4]) ? $color[4] : 1;
1956
+ }
1957
+
1958
+ // this might be the IE function, so return value unchanged
1959
+ return null;
1960
+ }
1961
+
1962
+ protected static $lib_opacity = array("color");
1963
+ protected function lib_opacity($args) {
1964
+ $value = $args[0];
1965
+ if ($value[0] === 'number') return null;
1966
+ return $this->lib_alpha($args);
1967
+ }
1968
+
1969
+ // mix two colors
1970
+ protected static $lib_mix = array("color-1", "color-2", "weight");
1971
+ protected function lib_mix($args) {
1972
+ list($first, $second, $weight) = $args;
1973
+ $first = $this->assertColor($first);
1974
+ $second = $this->assertColor($second);
1975
+
1976
+ if (is_null($weight)) {
1977
+ $weight = 0.5;
1978
+ } else {
1979
+ $weight = $this->coercePercent($weight);
1980
+ }
1981
+
1982
+ $firstAlpha = isset($first[4]) ? $first[4] : 1;
1983
+ $secondAlpha = isset($second[4]) ? $second[4] : 1;
1984
+
1985
+ $w = $weight * 2 - 1;
1986
+ $a = $firstAlpha - $secondAlpha;
1987
+
1988
+ $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
1989
+ $w2 = 1.0 - $w1;
1990
+
1991
+ $new = array('color',
1992
+ $w1 * $first[1] + $w2 * $second[1],
1993
+ $w1 * $first[2] + $w2 * $second[2],
1994
+ $w1 * $first[3] + $w2 * $second[3],
1995
+ );
1996
+
1997
+ if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
1998
+ $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1);
1999
+ }
2000
+
2001
+ return $this->fixColor($new);
2002
+ }
2003
+
2004
+ protected static $lib_hsl = array("hue", "saturation", "lightness");
2005
+ protected function lib_hsl($args) {
2006
+ list($h, $s, $l) = $args;
2007
+ return $this->toRGB($h[1], $s[1], $l[1]);
2008
+ }
2009
+
2010
+ protected static $lib_hsla = array("hue", "saturation",
2011
+ "lightness", "alpha");
2012
+ protected function lib_hsla($args) {
2013
+ list($h, $s, $l, $a) = $args;
2014
+ $color = $this->toRGB($h[1], $s[1], $l[1]);
2015
+ $color[4] = $a[1];
2016
+ return $color;
2017
+ }
2018
+
2019
+ protected static $lib_hue = array("color");
2020
+ protected function lib_hue($args) {
2021
+ $color = $this->assertColor($args[0]);
2022
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2023
+ return array("number", $hsl[1], "deg");
2024
+ }
2025
+
2026
+ protected static $lib_saturation = array("color");
2027
+ protected function lib_saturation($args) {
2028
+ $color = $this->assertColor($args[0]);
2029
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2030
+ return array("number", $hsl[2], "%");
2031
+ }
2032
+
2033
+ protected static $lib_lightness = array("color");
2034
+ protected function lib_lightness($args) {
2035
+ $color = $this->assertColor($args[0]);
2036
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2037
+ return array("number", $hsl[3], "%");
2038
+ }
2039
+
2040
+ protected function adjustHsl($color, $idx, $amount) {
2041
+ $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2042
+ $hsl[$idx] += $amount;
2043
+ $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
2044
+ if (isset($color[4])) $out[4] = $color[4];
2045
+ return $out;
2046
+ }
2047
+
2048
+ protected static $lib_adjust_hue = array("color", "degrees");
2049
+ protected function lib_adjust_hue($args) {
2050
+ $color = $this->assertColor($args[0]);
2051
+ $degrees = $this->assertNumber($args[1]);
2052
+ return $this->adjustHsl($color, 1, $degrees);
2053
+ }
2054
+
2055
+ protected static $lib_lighten = array("color", "amount");
2056
+ protected function lib_lighten($args) {
2057
+ $color = $this->assertColor($args[0]);
2058
+ $amount = 100*$this->coercePercent($args[1]);
2059
+ return $this->adjustHsl($color, 3, $amount);
2060
+ }
2061
+
2062
+ protected static $lib_darken = array("color", "amount");
2063
+ protected function lib_darken($args) {
2064
+ $color = $this->assertColor($args[0]);
2065
+ $amount = 100*$this->coercePercent($args[1]);
2066
+ return $this->adjustHsl($color, 3, -$amount);
2067
+ }
2068
+
2069
+ protected static $lib_saturate = array("color", "amount");
2070
+ protected function lib_saturate($args) {
2071
+ $value = $args[0];
2072
+ if ($value[0] === 'number') return null;
2073
+ $color = $this->assertColor($value);
2074
+ $amount = 100*$this->coercePercent($args[1]);
2075
+ return $this->adjustHsl($color, 2, $amount);
2076
+ }
2077
+
2078
+ protected static $lib_desaturate = array("color", "amount");
2079
+ protected function lib_desaturate($args) {
2080
+ $color = $this->assertColor($args[0]);
2081
+ $amount = 100*$this->coercePercent($args[1]);
2082
+ return $this->adjustHsl($color, 2, -$amount);
2083
+ }
2084
+
2085
+ protected static $lib_grayscale = array("color");
2086
+ protected function lib_grayscale($args) {
2087
+ $value = $args[0];
2088
+ if ($value[0] === 'number') return null;
2089
+ return $this->adjustHsl($this->assertColor($value), 2, -100);
2090
+ }
2091
+
2092
+ protected static $lib_complement = array("color");
2093
+ protected function lib_complement($args) {
2094
+ return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
2095
+ }
2096
+
2097
+ protected static $lib_invert = array("color");
2098
+ protected function lib_invert($args) {
2099
+ $value = $args[0];
2100
+ if ($value[0] === 'number') return null;
2101
+ $color = $this->assertColor($value);
2102
+ $color[1] = 255 - $color[1];
2103
+ $color[2] = 255 - $color[2];
2104
+ $color[3] = 255 - $color[3];
2105
+ return $color;
2106
+ }
2107
+
2108
+ // increases opacity by amount
2109
+ protected static $lib_opacify = array("color", "amount");
2110
+ protected function lib_opacify($args) {
2111
+ $color = $this->assertColor($args[0]);
2112
+ $amount = $this->coercePercent($args[1]);
2113
+
2114
+ $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
2115
+ $color[4] = min(1, max(0, $color[4]));
2116
+ return $color;
2117
+ }
2118
+
2119
+ protected static $lib_fade_in = array("color", "amount");
2120
+ protected function lib_fade_in($args) {
2121
+ return $this->lib_opacify($args);
2122
+ }
2123
+
2124
+ // decreases opacity by amount
2125
+ protected static $lib_transparentize = array("color", "amount");
2126
+ protected function lib_transparentize($args) {
2127
+ $color = $this->assertColor($args[0]);
2128
+ $amount = $this->coercePercent($args[1]);
2129
+
2130
+ $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
2131
+ $color[4] = min(1, max(0, $color[4]));
2132
+ return $color;
2133
+ }
2134
+
2135
+ protected static $lib_fade_out = array("color", "amount");
2136
+ protected function lib_fade_out($args) {
2137
+ return $this->lib_transparentize($args);
2138
+ }
2139
+
2140
+ protected static $lib_unquote = array("string");
2141
+ protected function lib_unquote($args) {
2142
+ $str = $args[0];
2143
+ if ($str[0] == "string") $str[1] = "";
2144
+ return $str;
2145
+ }
2146
+
2147
+ protected static $lib_quote = array("string");
2148
+ protected function lib_quote($args) {
2149
+ $value = $args[0];
2150
+ if ($value[0] == "string" && !empty($value[1]))
2151
+ return $value;
2152
+ return array("string", '"', array($value));
2153
+ }
2154
+
2155
+ protected static $lib_percentage = array("value");
2156
+ protected function lib_percentage($args) {
2157
+ return array("number",
2158
+ $this->coercePercent($args[0]) * 100,
2159
+ "%");
2160
+ }
2161
+
2162
+ protected static $lib_round = array("value");
2163
+ protected function lib_round($args) {
2164
+ $num = $args[0];
2165
+ $num[1] = round($num[1]);
2166
+ return $num;
2167
+ }
2168
+
2169
+ protected static $lib_floor = array("value");
2170
+ protected function lib_floor($args) {
2171
+ $num = $args[0];
2172
+ $num[1] = floor($num[1]);
2173
+ return $num;
2174
+ }
2175
+
2176
+ protected static $lib_ceil = array("value");
2177
+ protected function lib_ceil($args) {
2178
+ $num = $args[0];
2179
+ $num[1] = ceil($num[1]);
2180
+ return $num;
2181
+ }
2182
+
2183
+ protected static $lib_abs = array("value");
2184
+ protected function lib_abs($args) {
2185
+ $num = $args[0];
2186
+ $num[1] = abs($num[1]);
2187
+ return $num;
2188
+ }
2189
+
2190
+ protected function lib_min($args) {
2191
+ $numbers = $this->getNormalizedNumbers($args);
2192
+ $min = null;
2193
+ foreach ($numbers as $key => $number) {
2194
+ if (null === $min || $number[1] <= $min[1]) {
2195
+ $min = array($key, $number[1]);
2196
+ }
2197
+ }
2198
+
2199
+ return $args[$min[0]];
2200
+ }
2201
+
2202
+ protected function lib_max($args) {
2203
+ $numbers = $this->getNormalizedNumbers($args);
2204
+ $max = null;
2205
+ foreach ($numbers as $key => $number) {
2206
+ if (null === $max || $number[1] >= $max[1]) {
2207
+ $max = array($key, $number[1]);
2208
+ }
2209
+ }
2210
+
2211
+ return $args[$max[0]];
2212
+ }
2213
+
2214
+ protected function getNormalizedNumbers($args) {
2215
+ $unit = null;
2216
+ $originalUnit = null;
2217
+ $numbers = array();
2218
+ foreach ($args as $key => $item) {
2219
+ if ('number' != $item[0]) {
2220
+ $this->throwError("%s is not a number", $item[0]);
2221
+ }
2222
+ $number = $this->normalizeNumber($item);
2223
+
2224
+ if (null === $unit) {
2225
+ $unit = $number[2];
2226
+ $originalUnit = $item[2];
2227
+ } elseif ($unit !== $number[2]) {
2228
+ $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item[2]);
2229
+ }
2230
+
2231
+ $numbers[$key] = $number;
2232
+ }
2233
+
2234
+ return $numbers;
2235
+ }
2236
+
2237
+ protected static $lib_length = array("list");
2238
+ protected function lib_length($args) {
2239
+ $list = $this->coerceList($args[0]);
2240
+ return count($list[2]);
2241
+ }
2242
+
2243
+ protected static $lib_nth = array("list", "n");
2244
+ protected function lib_nth($args) {
2245
+ $list = $this->coerceList($args[0]);
2246
+ $n = $this->assertNumber($args[1]) - 1;
2247
+ return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue;
2248
+ }
2249
+
2250
+ protected function listSeparatorForJoin($list1, $sep) {
2251
+ if (is_null($sep)) return $list1[1];
2252
+ switch ($this->compileValue($sep)) {
2253
+ case "comma":
2254
+ return ",";
2255
+ case "space":
2256
+ return "";
2257
+ default:
2258
+ return $list1[1];
2259
+ }
2260
+ }
2261
+
2262
+ protected static $lib_join = array("list1", "list2", "separator");
2263
+ protected function lib_join($args) {
2264
+ list($list1, $list2, $sep) = $args;
2265
+ $list1 = $this->coerceList($list1, " ");
2266
+ $list2 = $this->coerceList($list2, " ");
2267
+ $sep = $this->listSeparatorForJoin($list1, $sep);
2268
+ return array("list", $sep, array_merge($list1[2], $list2[2]));
2269
+ }
2270
+
2271
+ protected static $lib_append = array("list", "val", "separator");
2272
+ protected function lib_append($args) {
2273
+ list($list1, $value, $sep) = $args;
2274
+ $list1 = $this->coerceList($list1, " ");
2275
+ $sep = $this->listSeparatorForJoin($list1, $sep);
2276
+ return array("list", $sep, array_merge($list1[2], array($value)));
2277
+ }
2278
+
2279
+ protected function lib_zip($args) {
2280
+ foreach ($args as $arg) {
2281
+ $this->assertList($arg);
2282
+ }
2283
+
2284
+ $lists = array();
2285
+ $firstList = array_shift($args);
2286
+ foreach ($firstList[2] as $key => $item) {
2287
+ $list = array("list", "", array($item));
2288
+ foreach ($args as $arg) {
2289
+ if (isset($arg[2][$key])) {
2290
+ $list[2][] = $arg[2][$key];
2291
+ } else {
2292
+ break 2;
2293
+ }
2294
+ }
2295
+ $lists[] = $list;
2296
+ }
2297
+
2298
+ return array("list", ",", $lists);
2299
+ }
2300
+
2301
+ protected static $lib_type_of = array("value");
2302
+ protected function lib_type_of($args) {
2303
+ $value = $args[0];
2304
+ switch ($value[0]) {
2305
+ case "keyword":
2306
+ if ($value == self::$true || $value == self::$false) {
2307
+ return "bool";
2308
+ }
2309
+
2310
+ if ($this->coerceColor($value)) {
2311
+ return "color";
2312
+ }
2313
+
2314
+ return "string";
2315
+ default:
2316
+ return $value[0];
2317
+ }
2318
+ }
2319
+
2320
+ protected static $lib_unit = array("number");
2321
+ protected function lib_unit($args) {
2322
+ $num = $args[0];
2323
+ if ($num[0] == "number") {
2324
+ return array("string", '"', array($num[2]));
2325
+ }
2326
+ return "";
2327
+ }
2328
+
2329
+ protected static $lib_unitless = array("number");
2330
+ protected function lib_unitless($args) {
2331
+ $value = $args[0];
2332
+ return $value[0] == "number" && empty($value[2]);
2333
+ }
2334
+
2335
+ protected static $lib_comparable = array("number-1", "number-2");
2336
+ protected function lib_comparable($args) {
2337
+ list($number1, $number2) = $args;
2338
+ if (!isset($number1[0]) || $number1[0] != "number" || !isset($number2[0]) || $number2[0] != "number") {
2339
+ $this->throwError('Invalid argument(s) for "comparable"');
2340
+ }
2341
+
2342
+ $number1 = $this->normalizeNumber($number1);
2343
+ $number2 = $this->normalizeNumber($number2);
2344
+
2345
+ return $number1[2] == $number2[2] || $number1[2] == "" || $number2[2] == "";
2346
+ }
2347
+
2348
+ /**
2349
+ * Workaround IE7's content counter bug.
2350
+ *
2351
+ * @param array $args
2352
+ */
2353
+ protected function lib_counter($args) {
2354
+ $list = array_map(array($this, 'compileValue'), $args);
2355
+ return array('string', '', array('counter(' . implode(',', $list) . ')'));
2356
+ }
2357
+
2358
+ protected function throwError($msg = null) {
2359
+ if (func_num_args() > 1) {
2360
+ $msg = call_user_func_array("sprintf", func_get_args());
2361
+ }
2362
+
2363
+ if ($this->sourcePos >= 0 && isset($this->sourceParser)) {
2364
+ $this->sourceParser->throwParseError($msg, $this->sourcePos);
2365
+ }
2366
+
2367
+ throw new Exception($msg);
2368
+ }
2369
+
2370
+ /**
2371
+ * CSS Colors
2372
+ *
2373
+ * @see http://www.w3.org/TR/css3-color
2374
+ */
2375
+ static protected $cssColors = array(
2376
+ 'aliceblue' => '240,248,255',
2377
+ 'antiquewhite' => '250,235,215',
2378
+ 'aqua' => '0,255,255',
2379
+ 'aquamarine' => '127,255,212',
2380
+ 'azure' => '240,255,255',
2381
+ 'beige' => '245,245,220',
2382
+ 'bisque' => '255,228,196',
2383
+ 'black' => '0,0,0',
2384
+ 'blanchedalmond' => '255,235,205',
2385
+ 'blue' => '0,0,255',
2386
+ 'blueviolet' => '138,43,226',
2387
+ 'brown' => '165,42,42',
2388
+ 'burlywood' => '222,184,135',
2389
+ 'cadetblue' => '95,158,160',
2390
+ 'chartreuse' => '127,255,0',
2391
+ 'chocolate' => '210,105,30',
2392
+ 'coral' => '255,127,80',
2393
+ 'cornflowerblue' => '100,149,237',
2394
+ 'cornsilk' => '255,248,220',
2395
+ 'crimson' => '220,20,60',
2396
+ 'cyan' => '0,255,255',
2397
+ 'darkblue' => '0,0,139',
2398
+ 'darkcyan' => '0,139,139',
2399
+ 'darkgoldenrod' => '184,134,11',
2400
+ 'darkgray' => '169,169,169',
2401
+ 'darkgreen' => '0,100,0',
2402
+ 'darkgrey' => '169,169,169',
2403
+ 'darkkhaki' => '189,183,107',
2404
+ 'darkmagenta' => '139,0,139',
2405
+ 'darkolivegreen' => '85,107,47',
2406
+ 'darkorange' => '255,140,0',
2407
+ 'darkorchid' => '153,50,204',
2408
+ 'darkred' => '139,0,0',
2409
+ 'darksalmon' => '233,150,122',
2410
+ 'darkseagreen' => '143,188,143',
2411
+ 'darkslateblue' => '72,61,139',
2412
+ 'darkslategray' => '47,79,79',
2413
+ 'darkslategrey' => '47,79,79',
2414
+ 'darkturquoise' => '0,206,209',
2415
+ 'darkviolet' => '148,0,211',
2416
+ 'deeppink' => '255,20,147',
2417
+ 'deepskyblue' => '0,191,255',
2418
+ 'dimgray' => '105,105,105',
2419
+ 'dimgrey' => '105,105,105',
2420
+ 'dodgerblue' => '30,144,255',
2421
+ 'firebrick' => '178,34,34',
2422
+ 'floralwhite' => '255,250,240',
2423
+ 'forestgreen' => '34,139,34',
2424
+ 'fuchsia' => '255,0,255',
2425
+ 'gainsboro' => '220,220,220',
2426
+ 'ghostwhite' => '248,248,255',
2427
+ 'gold' => '255,215,0',
2428
+ 'goldenrod' => '218,165,32',
2429
+ 'gray' => '128,128,128',
2430
+ 'green' => '0,128,0',
2431
+ 'greenyellow' => '173,255,47',
2432
+ 'grey' => '128,128,128',
2433
+ 'honeydew' => '240,255,240',
2434
+ 'hotpink' => '255,105,180',
2435
+ 'indianred' => '205,92,92',
2436
+ 'indigo' => '75,0,130',
2437
+ 'ivory' => '255,255,240',
2438
+ 'khaki' => '240,230,140',
2439
+ 'lavender' => '230,230,250',
2440
+ 'lavenderblush' => '255,240,245',
2441
+ 'lawngreen' => '124,252,0',
2442
+ 'lemonchiffon' => '255,250,205',
2443
+ 'lightblue' => '173,216,230',
2444
+ 'lightcoral' => '240,128,128',
2445
+ 'lightcyan' => '224,255,255',
2446
+ 'lightgoldenrodyellow' => '250,250,210',
2447
+ 'lightgray' => '211,211,211',
2448
+ 'lightgreen' => '144,238,144',
2449
+ 'lightgrey' => '211,211,211',
2450
+ 'lightpink' => '255,182,193',
2451
+ 'lightsalmon' => '255,160,122',
2452
+ 'lightseagreen' => '32,178,170',
2453
+ 'lightskyblue' => '135,206,250',
2454
+ 'lightslategray' => '119,136,153',
2455
+ 'lightslategrey' => '119,136,153',
2456
+ 'lightsteelblue' => '176,196,222',
2457
+ 'lightyellow' => '255,255,224',
2458
+ 'lime' => '0,255,0',
2459
+ 'limegreen' => '50,205,50',
2460
+ 'linen' => '250,240,230',
2461
+ 'magenta' => '255,0,255',
2462
+ 'maroon' => '128,0,0',
2463
+ 'mediumaquamarine' => '102,205,170',
2464
+ 'mediumblue' => '0,0,205',
2465
+ 'mediumorchid' => '186,85,211',
2466
+ 'mediumpurple' => '147,112,219',
2467
+ 'mediumseagreen' => '60,179,113',
2468
+ 'mediumslateblue' => '123,104,238',
2469
+ 'mediumspringgreen' => '0,250,154',
2470
+ 'mediumturquoise' => '72,209,204',
2471
+ 'mediumvioletred' => '199,21,133',
2472
+ 'midnightblue' => '25,25,112',
2473
+ 'mintcream' => '245,255,250',
2474
+ 'mistyrose' => '255,228,225',
2475
+ 'moccasin' => '255,228,181',
2476
+ 'navajowhite' => '255,222,173',
2477
+ 'navy' => '0,0,128',
2478
+ 'oldlace' => '253,245,230',
2479
+ 'olive' => '128,128,0',
2480
+ 'olivedrab' => '107,142,35',
2481
+ 'orange' => '255,165,0',
2482
+ 'orangered' => '255,69,0',
2483
+ 'orchid' => '218,112,214',
2484
+ 'palegoldenrod' => '238,232,170',
2485
+ 'palegreen' => '152,251,152',
2486
+ 'paleturquoise' => '175,238,238',
2487
+ 'palevioletred' => '219,112,147',
2488
+ 'papayawhip' => '255,239,213',
2489
+ 'peachpuff' => '255,218,185',
2490
+ 'peru' => '205,133,63',
2491
+ 'pink' => '255,192,203',
2492
+ 'plum' => '221,160,221',
2493
+ 'powderblue' => '176,224,230',
2494
+ 'purple' => '128,0,128',
2495
+ 'red' => '255,0,0',
2496
+ 'rosybrown' => '188,143,143',
2497
+ 'royalblue' => '65,105,225',
2498
+ 'saddlebrown' => '139,69,19',
2499
+ 'salmon' => '250,128,114',
2500
+ 'sandybrown' => '244,164,96',
2501
+ 'seagreen' => '46,139,87',
2502
+ 'seashell' => '255,245,238',
2503
+ 'sienna' => '160,82,45',
2504
+ 'silver' => '192,192,192',
2505
+ 'skyblue' => '135,206,235',
2506
+ 'slateblue' => '106,90,205',
2507
+ 'slategray' => '112,128,144',
2508
+ 'slategrey' => '112,128,144',
2509
+ 'snow' => '255,250,250',
2510
+ 'springgreen' => '0,255,127',
2511
+ 'steelblue' => '70,130,180',
2512
+ 'tan' => '210,180,140',
2513
+ 'teal' => '0,128,128',
2514
+ 'thistle' => '216,191,216',
2515
+ 'tomato' => '255,99,71',
2516
+ 'transparent' => '0,0,0,0',
2517
+ 'turquoise' => '64,224,208',
2518
+ 'violet' => '238,130,238',
2519
+ 'wheat' => '245,222,179',
2520
+ 'white' => '255,255,255',
2521
+ 'whitesmoke' => '245,245,245',
2522
+ 'yellow' => '255,255,0',
2523
+ 'yellowgreen' => '154,205,50'
2524
+ );
2525
+ }
2526
+
2527
+ /**
2528
+ * SCSS parser
2529
+ *
2530
+ * @author Leaf Corcoran <leafot@gmail.com>
2531
+ */
2532
+ class scss_parser {
2533
+ static protected $precedence = array(
2534
+ "or" => 0,
2535
+ "and" => 1,
2536
+
2537
+ '==' => 2,
2538
+ '!=' => 2,
2539
+ '<=' => 2,
2540
+ '>=' => 2,
2541
+ '=' => 2,
2542
+ '<' => 3,
2543
+ '>' => 2,
2544
+
2545
+ '+' => 3,
2546
+ '-' => 3,
2547
+ '*' => 4,
2548
+ '/' => 4,
2549
+ '%' => 4,
2550
+ );
2551
+
2552
+ static protected $operators = array("+", "-", "*", "/", "%",
2553
+ "==", "!=", "<=", ">=", "<", ">", "and", "or");
2554
+
2555
+ static protected $operatorStr;
2556
+ static protected $whitePattern;
2557
+ static protected $commentMulti;
2558
+
2559
+ static protected $commentSingle = "//";
2560
+ static protected $commentMultiLeft = "/*";
2561
+ static protected $commentMultiRight = "*/";
2562
+
2563
+ function __construct($sourceName = null, $rootParser = true) {
2564
+ $this->sourceName = $sourceName;
2565
+ $this->rootParser = $rootParser;
2566
+
2567
+ if (empty(self::$operatorStr)) {
2568
+ self::$operatorStr = $this->makeOperatorStr(self::$operators);
2569
+
2570
+ $commentSingle = $this->preg_quote(self::$commentSingle);
2571
+ $commentMultiLeft = $this->preg_quote(self::$commentMultiLeft);
2572
+ $commentMultiRight = $this->preg_quote(self::$commentMultiRight);
2573
+ self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
2574
+ self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
2575
+ }
2576
+ }
2577
+
2578
+ static protected function makeOperatorStr($operators) {
2579
+ return '('.implode('|', array_map(array('scss_parser','preg_quote'),
2580
+ $operators)).')';
2581
+ }
2582
+
2583
+ function parse($buffer) {
2584
+ $this->count = 0;
2585
+ $this->env = null;
2586
+ $this->inParens = false;
2587
+ $this->pushBlock(null); // root block
2588
+ $this->eatWhiteDefault = true;
2589
+ $this->insertComments = true;
2590
+
2591
+ $this->buffer = $buffer;
2592
+
2593
+ $this->whitespace();
2594
+ while (false !== $this->parseChunk());
2595
+
2596
+ if ($this->count != strlen($this->buffer))
2597
+ $this->throwParseError();
2598
+
2599
+ if (!empty($this->env->parent)) {
2600
+ $this->throwParseError("unclosed block");
2601
+ }
2602
+
2603
+ $this->env->isRoot = true;
2604
+ return $this->env;
2605
+ }
2606
+
2607
+ /**
2608
+ * Parse a single chunk off the head of the buffer and append it to the
2609
+ * current parse environment.
2610
+ *
2611
+ * Returns false when the buffer is empty, or when there is an error.
2612
+ *
2613
+ * This function is called repeatedly until the entire document is
2614
+ * parsed.
2615
+ *
2616
+ * This parser is most similar to a recursive descent parser. Single
2617
+ * functions represent discrete grammatical rules for the language, and
2618
+ * they are able to capture the text that represents those rules.
2619
+ *
2620
+ * Consider the function scssc::keyword(). (All parse functions are
2621
+ * structured the same.)
2622
+ *
2623
+ * The function takes a single reference argument. When calling the
2624
+ * function it will attempt to match a keyword on the head of the buffer.
2625
+ * If it is successful, it will place the keyword in the referenced
2626
+ * argument, advance the position in the buffer, and return true. If it
2627
+ * fails then it won't advance the buffer and it will return false.
2628
+ *
2629
+ * All of these parse functions are powered by scssc::match(), which behaves
2630
+ * the same way, but takes a literal regular expression. Sometimes it is
2631
+ * more convenient to use match instead of creating a new function.
2632
+ *
2633
+ * Because of the format of the functions, to parse an entire string of
2634
+ * grammatical rules, you can chain them together using &&.
2635
+ *
2636
+ * But, if some of the rules in the chain succeed before one fails, then
2637
+ * the buffer position will be left at an invalid state. In order to
2638
+ * avoid this, scssc::seek() is used to remember and set buffer positions.
2639
+ *
2640
+ * Before parsing a chain, use $s = $this->seek() to remember the current
2641
+ * position into $s. Then if a chain fails, use $this->seek($s) to
2642
+ * go back where we started.
2643
+ *
2644
+ * @return boolean
2645
+ */
2646
+ protected function parseChunk() {
2647
+ $s = $this->seek();
2648
+
2649
+ // the directives
2650
+ if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") {
2651
+ if ($this->literal("@media") && $this->mediaQueryList($mediaQueryList) && $this->literal("{")) {
2652
+ $media = $this->pushSpecialBlock("media");
2653
+ $media->queryList = $mediaQueryList[2];
2654
+ return true;
2655
+ } else {
2656
+ $this->seek($s);
2657
+ }
2658
+
2659
+ if ($this->literal("@mixin") &&
2660
+ $this->keyword($mixinName) &&
2661
+ ($this->argumentDef($args) || true) &&
2662
+ $this->literal("{"))
2663
+ {
2664
+ $mixin = $this->pushSpecialBlock("mixin");
2665
+ $mixin->name = $mixinName;
2666
+ $mixin->args = $args;
2667
+ return true;
2668
+ } else {
2669
+ $this->seek($s);
2670
+ }
2671
+
2672
+ if ($this->literal("@include") &&
2673
+ $this->keyword($mixinName) &&
2674
+ ($this->literal("(") &&
2675
+ ($this->argValues($argValues) || true) &&
2676
+ $this->literal(")") || true) &&
2677
+ ($this->end() ||
2678
+ $this->literal("{") && $hasBlock = true))
2679
+ {
2680
+ $child = array("include",
2681
+ $mixinName, isset($argValues) ? $argValues : null, null);
2682
+
2683
+ if (!empty($hasBlock)) {
2684
+ $include = $this->pushSpecialBlock("include");
2685
+ $include->child = $child;
2686
+ } else {
2687
+ $this->append($child, $s);
2688
+ }
2689
+
2690
+ return true;
2691
+ } else {
2692
+ $this->seek($s);
2693
+ }
2694
+
2695
+ if ($this->literal("@import") &&
2696
+ $this->valueList($importPath) &&
2697
+ $this->end())
2698
+ {
2699
+ $this->append(array("import", $importPath), $s);
2700
+ return true;
2701
+ } else {
2702
+ $this->seek($s);
2703
+ }
2704
+
2705
+ if ($this->literal("@extend") &&
2706
+ $this->selectors($selector) &&
2707
+ $this->end())
2708
+ {
2709
+ $this->append(array("extend", $selector), $s);
2710
+ return true;
2711
+ } else {
2712
+ $this->seek($s);
2713
+ }
2714
+
2715
+ if ($this->literal("@function") &&
2716
+ $this->keyword($fnName) &&
2717
+ $this->argumentDef($args) &&
2718
+ $this->literal("{"))
2719
+ {
2720
+ $func = $this->pushSpecialBlock("function");
2721
+ $func->name = $fnName;
2722
+ $func->args = $args;
2723
+ return true;
2724
+ } else {
2725
+ $this->seek($s);
2726
+ }
2727
+
2728
+ if ($this->literal("@return") && $this->valueList($retVal) && $this->end()) {
2729
+ $this->append(array("return", $retVal), $s);
2730
+ return true;
2731
+ } else {
2732
+ $this->seek($s);
2733
+ }
2734
+
2735
+ if ($this->literal("@each") &&
2736
+ $this->variable($varName) &&
2737
+ $this->literal("in") &&
2738
+ $this->valueList($list) &&
2739
+ $this->literal("{"))
2740
+ {
2741
+ $each = $this->pushSpecialBlock("each");
2742
+ $each->var = $varName[1];
2743
+ $each->list = $list;
2744
+ return true;
2745
+ } else {
2746
+ $this->seek($s);
2747
+ }
2748
+
2749
+ if ($this->literal("@while") &&
2750
+ $this->expression($cond) &&
2751
+ $this->literal("{"))
2752
+ {
2753
+ $while = $this->pushSpecialBlock("while");
2754
+ $while->cond = $cond;
2755
+ return true;
2756
+ } else {
2757
+ $this->seek($s);
2758
+ }
2759
+
2760
+ if ($this->literal("@for") &&
2761
+ $this->variable($varName) &&
2762
+ $this->literal("from") &&
2763
+ $this->expression($start) &&
2764
+ ($this->literal("through") ||
2765
+ ($forUntil = true && $this->literal("to"))) &&
2766
+ $this->expression($end) &&
2767
+ $this->literal("{"))
2768
+ {
2769
+ $for = $this->pushSpecialBlock("for");
2770
+ $for->var = $varName[1];
2771
+ $for->start = $start;
2772
+ $for->end = $end;
2773
+ $for->until = isset($forUntil);
2774
+ return true;
2775
+ } else {
2776
+ $this->seek($s);
2777
+ }
2778
+
2779
+ if ($this->literal("@if") && $this->valueList($cond) && $this->literal("{")) {
2780
+ $if = $this->pushSpecialBlock("if");
2781
+ $if->cond = $cond;
2782
+ $if->cases = array();
2783
+ return true;
2784
+ } else {
2785
+ $this->seek($s);
2786
+ }
2787
+
2788
+ if (($this->literal("@debug") || $this->literal("@warn")) &&
2789
+ $this->valueList($value) &&
2790
+ $this->end()) {
2791
+ $this->append(array("debug", $value, $s), $s);
2792
+ return true;
2793
+ } else {
2794
+ $this->seek($s);
2795
+ }
2796
+
2797
+ if ($this->literal("@content") && $this->end()) {
2798
+ $this->append(array("mixin_content"), $s);
2799
+ return true;
2800
+ } else {
2801
+ $this->seek($s);
2802
+ }
2803
+
2804
+ $last = $this->last();
2805
+ if (!is_null($last) && $last[0] == "if") {
2806
+ list(, $if) = $last;
2807
+ if ($this->literal("@else")) {
2808
+ if ($this->literal("{")) {
2809
+ $else = $this->pushSpecialBlock("else");
2810
+ } elseif ($this->literal("if") && $this->valueList($cond) && $this->literal("{")) {
2811
+ $else = $this->pushSpecialBlock("elseif");
2812
+ $else->cond = $cond;
2813
+ }
2814
+
2815
+ if (isset($else)) {
2816
+ $else->dontAppend = true;
2817
+ $if->cases[] = $else;
2818
+ return true;
2819
+ }
2820
+ }
2821
+
2822
+ $this->seek($s);
2823
+ }
2824
+
2825
+ if ($this->literal("@charset") &&
2826
+ $this->valueList($charset) && $this->end())
2827
+ {
2828
+ $this->append(array("charset", $charset), $s);
2829
+ return true;
2830
+ } else {
2831
+ $this->seek($s);
2832
+ }
2833
+
2834
+ // doesn't match built in directive, do generic one
2835
+ if ($this->literal("@", false) && $this->keyword($dirName) &&
2836
+ ($this->openString("{", $dirValue) || true) &&
2837
+ $this->literal("{"))
2838
+ {
2839
+ $directive = $this->pushSpecialBlock("directive");
2840
+ $directive->name = $dirName;
2841
+ if (isset($dirValue)) $directive->value = $dirValue;
2842
+ return true;
2843
+ }
2844
+
2845
+ $this->seek($s);
2846
+ return false;
2847
+ }
2848
+
2849
+ // property shortcut
2850
+ // captures most properties before having to parse a selector
2851
+ if ($this->keyword($name, false) &&
2852
+ $this->literal(": ") &&
2853
+ $this->valueList($value) &&
2854
+ $this->end())
2855
+ {
2856
+ $name = array("string", "", array($name));
2857
+ $this->append(array("assign", $name, $value), $s);
2858
+ return true;
2859
+ } else {
2860
+ $this->seek($s);
2861
+ }
2862
+
2863
+ // variable assigns
2864
+ if ($this->variable($name) &&
2865
+ $this->literal(":") &&
2866
+ $this->valueList($value) && $this->end())
2867
+ {
2868
+ // check for !default
2869
+ $defaultVar = $value[0] == "list" && $this->stripDefault($value);
2870
+ $this->append(array("assign", $name, $value, $defaultVar), $s);
2871
+ return true;
2872
+ } else {
2873
+ $this->seek($s);
2874
+ }
2875
+
2876
+ // misc
2877
+ if ($this->literal("-->")) {
2878
+ return true;
2879
+ }
2880
+
2881
+ // opening css block
2882
+ $oldComments = $this->insertComments;
2883
+ $this->insertComments = false;
2884
+ if ($this->selectors($selectors) && $this->literal("{")) {
2885
+ $this->pushBlock($selectors);
2886
+ $this->insertComments = $oldComments;
2887
+ return true;
2888
+ } else {
2889
+ $this->seek($s);
2890
+ }
2891
+ $this->insertComments = $oldComments;
2892
+
2893
+ // property assign, or nested assign
2894
+ if ($this->propertyName($name) && $this->literal(":")) {
2895
+ $foundSomething = false;
2896
+ if ($this->valueList($value)) {
2897
+ $this->append(array("assign", $name, $value), $s);
2898
+ $foundSomething = true;
2899
+ }
2900
+
2901
+ if ($this->literal("{")) {
2902
+ $propBlock = $this->pushSpecialBlock("nestedprop");
2903
+ $propBlock->prefix = $name;
2904
+ $foundSomething = true;
2905
+ } elseif ($foundSomething) {
2906
+ $foundSomething = $this->end();
2907
+ }
2908
+
2909
+ if ($foundSomething) {
2910
+ return true;
2911
+ }
2912
+
2913
+ $this->seek($s);
2914
+ } else {
2915
+ $this->seek($s);
2916
+ }
2917
+
2918
+ // closing a block
2919
+ if ($this->literal("}")) {
2920
+ $block = $this->popBlock();
2921
+ if (isset($block->type) && $block->type == "include") {
2922
+ $include = $block->child;
2923
+ unset($block->child);
2924
+ $include[3] = $block;
2925
+ $this->append($include, $s);
2926
+ } elseif (empty($block->dontAppend)) {
2927
+ $type = isset($block->type) ? $block->type : "block";
2928
+ $this->append(array($type, $block), $s);
2929
+ }
2930
+ return true;
2931
+ }
2932
+
2933
+ // extra stuff
2934
+ if ($this->literal(";") ||
2935
+ $this->literal("<!--"))
2936
+ {
2937
+ return true;
2938
+ }
2939
+
2940
+ return false;
2941
+ }
2942
+
2943
+ protected function stripDefault(&$value) {
2944
+ $def = end($value[2]);
2945
+ if ($def[0] == "keyword" && $def[1] == "!default") {
2946
+ array_pop($value[2]);
2947
+ $value = $this->flattenList($value);
2948
+ return true;
2949
+ }
2950
+
2951
+ if ($def[0] == "list") {
2952
+ return $this->stripDefault($value[2][count($value[2]) - 1]);
2953
+ }
2954
+
2955
+ return false;
2956
+ }
2957
+
2958
+ protected function literal($what, $eatWhitespace = null) {
2959
+ if (is_null($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault;
2960
+
2961
+ // shortcut on single letter
2962
+ if (!isset($what[1]) && isset($this->buffer[$this->count])) {
2963
+ if ($this->buffer[$this->count] == $what) {
2964
+ if (!$eatWhitespace) {
2965
+ $this->count++;
2966
+ return true;
2967
+ }
2968
+ // goes below...
2969
+ } else {
2970
+ return false;
2971
+ }
2972
+ }
2973
+
2974
+ return $this->match($this->preg_quote($what), $m, $eatWhitespace);
2975
+ }
2976
+
2977
+ // tree builders
2978
+
2979
+ protected function pushBlock($selectors) {
2980
+ $b = new stdClass;
2981
+ $b->parent = $this->env; // not sure if we need this yet
2982
+
2983
+ $b->selectors = $selectors;
2984
+ $b->children = array();
2985
+
2986
+ $this->env = $b;
2987
+ return $b;
2988
+ }
2989
+
2990
+ protected function pushSpecialBlock($type) {
2991
+ $block = $this->pushBlock(null);
2992
+ $block->type = $type;
2993
+ return $block;
2994
+ }
2995
+
2996
+ protected function popBlock() {
2997
+ if (empty($this->env->parent)) {
2998
+ $this->throwParseError("unexpected }");
2999
+ }
3000
+
3001
+ $old = $this->env;
3002
+ $this->env = $this->env->parent;
3003
+ unset($old->parent);
3004
+ return $old;
3005
+ }
3006
+
3007
+ protected function append($statement, $pos=null) {
3008
+ if ($pos !== null) {
3009
+ $statement[-1] = $pos;
3010
+ if (!$this->rootParser) $statement[-2] = $this;
3011
+ }
3012
+ $this->env->children[] = $statement;
3013
+ }
3014
+
3015
+ // last child that was appended
3016
+ protected function last() {
3017
+ $i = count($this->env->children) - 1;
3018
+ if (isset($this->env->children[$i]))
3019
+ return $this->env->children[$i];
3020
+ }
3021
+
3022
+ // high level parsers (they return parts of ast)
3023
+
3024
+ protected function mediaQueryList(&$out) {
3025
+ return $this->genericList($out, "mediaQuery", ",", false);
3026
+ }
3027
+
3028
+ protected function mediaQuery(&$out) {
3029
+ $s = $this->seek();
3030
+
3031
+ $expressions = null;
3032
+ $parts = array();
3033
+
3034
+ if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->mixedKeyword($mediaType)) {
3035
+ $prop = array("mediaType");
3036
+ if (isset($only)) $prop[] = array("keyword", "only");
3037
+ if (isset($not)) $prop[] = array("keyword", "not");
3038
+ $media = array("list", "", array());
3039
+ foreach ((array)$mediaType as $type) {
3040
+ if (is_array($type)) {
3041
+ $media[2][] = $type;
3042
+ } else {
3043
+ $media[2][] = array("keyword", $type);
3044
+ }
3045
+ }
3046
+ $prop[] = $media;
3047
+ $parts[] = $prop;
3048
+ }
3049
+
3050
+ if (empty($parts) || $this->literal("and")) {
3051
+ $this->genericList($expressions, "mediaExpression", "and", false);
3052
+ if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
3053
+ }
3054
+
3055
+ $out = $parts;
3056
+ return true;
3057
+ }
3058
+
3059
+ protected function mediaExpression(&$out) {
3060
+ $s = $this->seek();
3061
+ $value = null;
3062
+ if ($this->literal("(") &&
3063
+ $this->expression($feature) &&
3064
+ ($this->literal(":") && $this->expression($value) || true) &&
3065
+ $this->literal(")"))
3066
+ {
3067
+ $out = array("mediaExp", $feature);
3068
+ if ($value) $out[] = $value;
3069
+ return true;
3070
+ }
3071
+
3072
+ $this->seek($s);
3073
+ return false;
3074
+ }
3075
+
3076
+ protected function argValues(&$out) {
3077
+ if ($this->genericList($list, "argValue", ",", false)) {
3078
+ $out = $list[2];
3079
+ return true;
3080
+ }
3081
+ return false;
3082
+ }
3083
+
3084
+ protected function argValue(&$out) {
3085
+ $s = $this->seek();
3086
+
3087
+ $keyword = null;
3088
+ if (!$this->variable($keyword) || !$this->literal(":")) {
3089
+ $this->seek($s);
3090
+ $keyword = null;
3091
+ }
3092
+
3093
+ if ($this->genericList($value, "expression")) {
3094
+ $out = array($keyword, $value, false);
3095
+ $s = $this->seek();
3096
+ if ($this->literal("...")) {
3097
+ $out[2] = true;
3098
+ } else {
3099
+ $this->seek($s);
3100
+ }
3101
+ return true;
3102
+ }
3103
+
3104
+ return false;
3105
+ }
3106
+
3107
+
3108
+ protected function valueList(&$out) {
3109
+ return $this->genericList($out, "spaceList", ",");
3110
+ }
3111
+
3112
+ protected function spaceList(&$out) {
3113
+ return $this->genericList($out, "expression");
3114
+ }
3115
+
3116
+ protected function genericList(&$out, $parseItem, $delim="", $flatten=true) {
3117
+ $s = $this->seek();
3118
+ $items = array();
3119
+ while ($this->$parseItem($value)) {
3120
+ $items[] = $value;
3121
+ if ($delim) {
3122
+ if (!$this->literal($delim)) break;
3123
+ }
3124
+ }
3125
+
3126
+ if (count($items) == 0) {
3127
+ $this->seek($s);
3128
+ return false;
3129
+ }
3130
+
3131
+ if ($flatten && count($items) == 1) {
3132
+ $out = $items[0];
3133
+ } else {
3134
+ $out = array("list", $delim, $items);
3135
+ }
3136
+
3137
+ return true;
3138
+ }
3139
+
3140
+ protected function expression(&$out) {
3141
+ $s = $this->seek();
3142
+
3143
+ if ($this->literal("(")) {
3144
+ if ($this->literal(")")) {
3145
+ $out = array("list", "", array());
3146
+ return true;
3147
+ }
3148
+
3149
+ if ($this->valueList($out) && $this->literal(')') && $out[0] == "list") {
3150
+ return true;
3151
+ }
3152
+
3153
+ $this->seek($s);
3154
+ }
3155
+
3156
+ if ($this->value($lhs)) {
3157
+ $out = $this->expHelper($lhs, 0);
3158
+ return true;
3159
+ }
3160
+
3161
+ return false;
3162
+ }
3163
+
3164
+ protected function expHelper($lhs, $minP) {
3165
+ $opstr = self::$operatorStr;
3166
+
3167
+ $ss = $this->seek();
3168
+ $whiteBefore = isset($this->buffer[$this->count - 1]) &&
3169
+ ctype_space($this->buffer[$this->count - 1]);
3170
+ while ($this->match($opstr, $m) && self::$precedence[$m[1]] >= $minP) {
3171
+ $whiteAfter = isset($this->buffer[$this->count - 1]) &&
3172
+ ctype_space($this->buffer[$this->count - 1]);
3173
+
3174
+ $op = $m[1];
3175
+
3176
+ // don't turn negative numbers into expressions
3177
+ if ($op == "-" && $whiteBefore) {
3178
+ if (!$whiteAfter) break;
3179
+ }
3180
+
3181
+ if (!$this->value($rhs)) break;
3182
+
3183
+ // peek and see if rhs belongs to next operator
3184
+ if ($this->peek($opstr, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
3185
+ $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
3186
+ }
3187
+
3188
+ $lhs = array("exp", $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter);
3189
+ $ss = $this->seek();
3190
+ $whiteBefore = isset($this->buffer[$this->count - 1]) &&
3191
+ ctype_space($this->buffer[$this->count - 1]);
3192
+ }
3193
+
3194
+ $this->seek($ss);
3195
+ return $lhs;
3196
+ }
3197
+
3198
+ protected function value(&$out) {
3199
+ $s = $this->seek();
3200
+
3201
+ if ($this->literal("not", false) && $this->whitespace() && $this->value($inner)) {
3202
+ $out = array("unary", "not", $inner, $this->inParens);
3203
+ return true;
3204
+ } else {
3205
+ $this->seek($s);
3206
+ }
3207
+
3208
+ if ($this->literal("+") && $this->value($inner)) {
3209
+ $out = array("unary", "+", $inner, $this->inParens);
3210
+ return true;
3211
+ } else {
3212
+ $this->seek($s);
3213
+ }
3214
+
3215
+ // negation
3216
+ if ($this->literal("-", false) &&
3217
+ ($this->variable($inner) ||
3218
+ $this->unit($inner) ||
3219
+ $this->parenValue($inner)))
3220
+ {
3221
+ $out = array("unary", "-", $inner, $this->inParens);
3222
+ return true;
3223
+ } else {
3224
+ $this->seek($s);
3225
+ }
3226
+
3227
+ if ($this->parenValue($out)) return true;
3228
+ if ($this->interpolation($out)) return true;
3229
+ if ($this->variable($out)) return true;
3230
+ if ($this->color($out)) return true;
3231
+ if ($this->unit($out)) return true;
3232
+ if ($this->string($out)) return true;
3233
+ if ($this->func($out)) return true;
3234
+ if ($this->progid($out)) return true;
3235
+
3236
+ if ($this->keyword($keyword)) {
3237
+ if ($keyword == "null") {
3238
+ $out = array("null");
3239
+ } else {
3240
+ $out = array("keyword", $keyword);
3241
+ }
3242
+ return true;
3243
+ }
3244
+
3245
+ return false;
3246
+ }
3247
+
3248
+ // value wrappen in parentheses
3249
+ protected function parenValue(&$out) {
3250
+ $s = $this->seek();
3251
+
3252
+ $inParens = $this->inParens;
3253
+ if ($this->literal("(") &&
3254
+ ($this->inParens = true) && $this->expression($exp) &&
3255
+ $this->literal(")"))
3256
+ {
3257
+ $out = $exp;
3258
+ $this->inParens = $inParens;
3259
+ return true;
3260
+ } else {
3261
+ $this->inParens = $inParens;
3262
+ $this->seek($s);
3263
+ }
3264
+
3265
+ return false;
3266
+ }
3267
+
3268
+ protected function progid(&$out) {
3269
+ $s = $this->seek();
3270
+ if ($this->literal("progid:", false) &&
3271
+ $this->openString("(", $fn) &&
3272
+ $this->literal("("))
3273
+ {
3274
+ $this->openString(")", $args, "(");
3275
+ if ($this->literal(")")) {
3276
+ $out = array("string", "", array(
3277
+ "progid:", $fn, "(", $args, ")"
3278
+ ));
3279
+ return true;
3280
+ }
3281
+ }
3282
+
3283
+ $this->seek($s);
3284
+ return false;
3285
+ }
3286
+
3287
+ protected function func(&$func) {
3288
+ $s = $this->seek();
3289
+
3290
+ if ($this->keyword($name, false) &&
3291
+ $this->literal("("))
3292
+ {
3293
+ if ($name == "alpha" && $this->argumentList($args)) {
3294
+ $func = array("function", $name, array("string", "", $args));
3295
+ return true;
3296
+ }
3297
+
3298
+ if ($name != "expression" && !preg_match("/^(-[a-z]+-)?calc$/", $name)) {
3299
+ $ss = $this->seek();
3300
+ if ($this->argValues($args) && $this->literal(")")) {
3301
+ $func = array("fncall", $name, $args);
3302
+ return true;
3303
+ }
3304
+ $this->seek($ss);
3305
+ }
3306
+
3307
+ if (($this->openString(")", $str, "(") || true ) &&
3308
+ $this->literal(")"))
3309
+ {
3310
+ $args = array();
3311
+ if (!empty($str)) {
3312
+ $args[] = array(null, array("string", "", array($str)));
3313
+ }
3314
+
3315
+ $func = array("fncall", $name, $args);
3316
+ return true;
3317
+ }
3318
+ }
3319
+
3320
+ $this->seek($s);
3321
+ return false;
3322
+ }
3323
+
3324
+ protected function argumentList(&$out) {
3325
+ $s = $this->seek();
3326
+ $this->literal("(");
3327
+
3328
+ $args = array();
3329
+ while ($this->keyword($var)) {
3330
+ $ss = $this->seek();
3331
+
3332
+ if ($this->literal("=") && $this->expression($exp)) {
3333
+ $args[] = array("string", "", array($var."="));
3334
+ $arg = $exp;
3335
+ } else {
3336
+ break;
3337
+ }
3338
+
3339
+ $args[] = $arg;
3340
+
3341
+ if (!$this->literal(",")) break;
3342
+
3343
+ $args[] = array("string", "", array(", "));
3344
+ }
3345
+
3346
+ if (!$this->literal(")") || !count($args)) {
3347
+ $this->seek($s);
3348
+ return false;
3349
+ }
3350
+
3351
+ $out = $args;
3352
+ return true;
3353
+ }
3354
+
3355
+ protected function argumentDef(&$out) {
3356
+ $s = $this->seek();
3357
+ $this->literal("(");
3358
+
3359
+ $args = array();
3360
+ while ($this->variable($var)) {
3361
+ $arg = array($var[1], null, false);
3362
+
3363
+ $ss = $this->seek();
3364
+ if ($this->literal(":") && $this->genericList($defaultVal, "expression")) {
3365
+ $arg[1] = $defaultVal;
3366
+ } else {
3367
+ $this->seek($ss);
3368
+ }
3369
+
3370
+ $ss = $this->seek();
3371
+ if ($this->literal("...")) {
3372
+ $sss = $this->seek();
3373
+ if (!$this->literal(")")) {
3374
+ $this->throwParseError("... has to be after the final argument");
3375
+ }
3376
+ $arg[2] = true;
3377
+ $this->seek($sss);
3378
+ } else {
3379
+ $this->seek($ss);
3380
+ }
3381
+
3382
+ $args[] = $arg;
3383
+ if (!$this->literal(",")) break;
3384
+ }
3385
+
3386
+ if (!$this->literal(")")) {
3387
+ $this->seek($s);
3388
+ return false;
3389
+ }
3390
+
3391
+ $out = $args;
3392
+ return true;
3393
+ }
3394
+
3395
+ protected function color(&$out) {
3396
+ $color = array('color');
3397
+
3398
+ if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
3399
+ if (isset($m[3])) {
3400
+ $num = $m[3];
3401
+ $width = 16;
3402
+ } else {
3403
+ $num = $m[2];
3404
+ $width = 256;
3405
+ }
3406
+
3407
+ $num = hexdec($num);
3408
+ foreach (array(3,2,1) as $i) {
3409
+ $t = $num % $width;
3410
+ $num /= $width;
3411
+
3412
+ $color[$i] = $t * (256/$width) + $t * floor(16/$width);
3413
+ }
3414
+
3415
+ $out = $color;
3416
+ return true;
3417
+ }
3418
+
3419
+ return false;
3420
+ }
3421
+
3422
+ protected function unit(&$unit) {
3423
+ if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
3424
+ $unit = array("number", $m[1], empty($m[3]) ? "" : $m[3]);
3425
+ return true;
3426
+ }
3427
+ return false;
3428
+ }
3429
+
3430
+ protected function string(&$out) {
3431
+ $s = $this->seek();
3432
+ if ($this->literal('"', false)) {
3433
+ $delim = '"';
3434
+ } elseif ($this->literal("'", false)) {
3435
+ $delim = "'";
3436
+ } else {
3437
+ return false;
3438
+ }
3439
+
3440
+ $content = array();
3441
+ $oldWhite = $this->eatWhiteDefault;
3442
+ $this->eatWhiteDefault = false;
3443
+
3444
+ while ($this->matchString($m, $delim)) {
3445
+ $content[] = $m[1];
3446
+ if ($m[2] == "#{") {
3447
+ $this->count -= strlen($m[2]);
3448
+ if ($this->interpolation($inter, false)) {
3449
+ $content[] = $inter;
3450
+ } else {
3451
+ $this->count += strlen($m[2]);
3452
+ $content[] = "#{"; // ignore it
3453
+ }
3454
+ } elseif ($m[2] == '\\') {
3455
+ $content[] = $m[2];
3456
+ if ($this->literal($delim, false)) {
3457
+ $content[] = $delim;
3458
+ }
3459
+ } else {
3460
+ $this->count -= strlen($delim);
3461
+ break; // delim
3462
+ }
3463
+ }
3464
+
3465
+ $this->eatWhiteDefault = $oldWhite;
3466
+
3467
+ if ($this->literal($delim)) {
3468
+ $out = array("string", $delim, $content);
3469
+ return true;
3470
+ }
3471
+
3472
+ $this->seek($s);
3473
+ return false;
3474
+ }
3475
+
3476
+ protected function mixedKeyword(&$out) {
3477
+ $s = $this->seek();
3478
+
3479
+ $parts = array();
3480
+
3481
+ $oldWhite = $this->eatWhiteDefault;
3482
+ $this->eatWhiteDefault = false;
3483
+
3484
+ while (true) {
3485
+ if ($this->keyword($key)) {
3486
+ $parts[] = $key;
3487
+ continue;
3488
+ }
3489
+
3490
+ if ($this->interpolation($inter)) {
3491
+ $parts[] = $inter;
3492
+ continue;
3493
+ }
3494
+
3495
+ break;
3496
+ }
3497
+
3498
+ $this->eatWhiteDefault = $oldWhite;
3499
+
3500
+ if (count($parts) == 0) return false;
3501
+
3502
+ if ($this->eatWhiteDefault) {
3503
+ $this->whitespace();
3504
+ }
3505
+
3506
+ $out = $parts;
3507
+ return true;
3508
+ }
3509
+
3510
+ // an unbounded string stopped by $end
3511
+ protected function openString($end, &$out, $nestingOpen=null) {
3512
+ $oldWhite = $this->eatWhiteDefault;
3513
+ $this->eatWhiteDefault = false;
3514
+
3515
+ $stop = array("'", '"', "#{", $end);
3516
+ $stop = array_map(array($this, "preg_quote"), $stop);
3517
+ $stop[] = self::$commentMulti;
3518
+
3519
+ $patt = '(.*?)('.implode("|", $stop).')';
3520
+
3521
+ $nestingLevel = 0;
3522
+
3523
+ $content = array();
3524
+ while ($this->match($patt, $m, false)) {
3525
+ if (isset($m[1]) && $m[1] !== '') {
3526
+ $content[] = $m[1];
3527
+ if ($nestingOpen) {
3528
+ $nestingLevel += substr_count($m[1], $nestingOpen);
3529
+ }
3530
+ }
3531
+
3532
+ $tok = $m[2];
3533
+
3534
+ $this->count-= strlen($tok);
3535
+ if ($tok == $end) {
3536
+ if ($nestingLevel == 0) {
3537
+ break;
3538
+ } else {
3539
+ $nestingLevel--;
3540
+ }
3541
+ }
3542
+
3543
+ if (($tok == "'" || $tok == '"') && $this->string($str)) {
3544
+ $content[] = $str;
3545
+ continue;
3546
+ }
3547
+
3548
+ if ($tok == "#{" && $this->interpolation($inter)) {
3549
+ $content[] = $inter;
3550
+ continue;
3551
+ }
3552
+
3553
+ $content[] = $tok;
3554
+ $this->count+= strlen($tok);
3555
+ }
3556
+
3557
+ $this->eatWhiteDefault = $oldWhite;
3558
+
3559
+ if (count($content) == 0) return false;
3560
+
3561
+ // trim the end
3562
+ if (is_string(end($content))) {
3563
+ $content[count($content) - 1] = rtrim(end($content));
3564
+ }
3565
+
3566
+ $out = array("string", "", $content);
3567
+ return true;
3568
+ }
3569
+
3570
+ // $lookWhite: save information about whitespace before and after
3571
+ protected function interpolation(&$out, $lookWhite=true) {
3572
+ $oldWhite = $this->eatWhiteDefault;
3573
+ $this->eatWhiteDefault = true;
3574
+
3575
+ $s = $this->seek();
3576
+ if ($this->literal("#{") && $this->valueList($value) && $this->literal("}", false)) {
3577
+
3578
+ // TODO: don't error if out of bounds
3579
+
3580
+ if ($lookWhite) {
3581
+ $left = preg_match('/\s/', $this->buffer[$s - 1]) ? " " : "";
3582
+ $right = preg_match('/\s/', $this->buffer[$this->count]) ? " ": "";
3583
+ } else {
3584
+ $left = $right = false;
3585
+ }
3586
+
3587
+ $out = array("interpolate", $value, $left, $right);
3588
+ $this->eatWhiteDefault = $oldWhite;
3589
+ if ($this->eatWhiteDefault) $this->whitespace();
3590
+ return true;
3591
+ }
3592
+
3593
+ $this->seek($s);
3594
+ $this->eatWhiteDefault = $oldWhite;
3595
+ return false;
3596
+ }
3597
+
3598
+ // low level parsers
3599
+
3600
+ // returns an array of parts or a string
3601
+ protected function propertyName(&$out) {
3602
+ $s = $this->seek();
3603
+ $parts = array();
3604
+
3605
+ $oldWhite = $this->eatWhiteDefault;
3606
+ $this->eatWhiteDefault = false;
3607
+
3608
+ while (true) {
3609
+ if ($this->interpolation($inter)) {
3610
+ $parts[] = $inter;
3611
+ } elseif ($this->keyword($text)) {
3612
+ $parts[] = $text;
3613
+ } elseif (count($parts) == 0 && $this->match('[:.#]', $m, false)) {
3614
+ // css hacks
3615
+ $parts[] = $m[0];
3616
+ } else {
3617
+ break;
3618
+ }
3619
+ }
3620
+
3621
+ $this->eatWhiteDefault = $oldWhite;
3622
+ if (count($parts) == 0) return false;
3623
+
3624
+ // match comment hack
3625
+ if (preg_match(self::$whitePattern,
3626
+ $this->buffer, $m, null, $this->count))
3627
+ {
3628
+ if (!empty($m[0])) {
3629
+ $parts[] = $m[0];
3630
+ $this->count += strlen($m[0]);
3631
+ }
3632
+ }
3633
+
3634
+ $this->whitespace(); // get any extra whitespace
3635
+
3636
+ $out = array("string", "", $parts);
3637
+ return true;
3638
+ }
3639
+
3640
+ // comma separated list of selectors
3641
+ protected function selectors(&$out) {
3642
+ $s = $this->seek();
3643
+ $selectors = array();
3644
+ while ($this->selector($sel)) {
3645
+ $selectors[] = $sel;
3646
+ if (!$this->literal(",")) break;
3647
+ while ($this->literal(",")); // ignore extra
3648
+ }
3649
+
3650
+ if (count($selectors) == 0) {
3651
+ $this->seek($s);
3652
+ return false;
3653
+ }
3654
+
3655
+ $out = $selectors;
3656
+ return true;
3657
+ }
3658
+
3659
+ // whitespace separated list of selectorSingle
3660
+ protected function selector(&$out) {
3661
+ $selector = array();
3662
+
3663
+ while (true) {
3664
+ if ($this->match('[>+~]+', $m)) {
3665
+ $selector[] = array($m[0]);
3666
+ } elseif ($this->selectorSingle($part)) {
3667
+ $selector[] = $part;
3668
+ $this->whitespace();
3669
+ } elseif ($this->match('\/[^\/]+\/', $m)) {
3670
+ $selector[] = array($m[0]);
3671
+ } else {
3672
+ break;
3673
+ }
3674
+
3675
+ }
3676
+
3677
+ if (count($selector) == 0) {
3678
+ return false;
3679
+ }
3680
+
3681
+ $out = $selector;
3682
+ return true;
3683
+ }
3684
+
3685
+ // the parts that make up
3686
+ // div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
3687
+ protected function selectorSingle(&$out) {
3688
+ $oldWhite = $this->eatWhiteDefault;
3689
+ $this->eatWhiteDefault = false;
3690
+
3691
+ $parts = array();
3692
+
3693
+ if ($this->literal("*", false)) {
3694
+ $parts[] = "*";
3695
+ }
3696
+
3697
+ while (true) {
3698
+ // see if we can stop early
3699
+ if ($this->match("\s*[{,]", $m)) {
3700
+ $this->count--;
3701
+ break;
3702
+ }
3703
+
3704
+ $s = $this->seek();
3705
+ // self
3706
+ if ($this->literal("&", false)) {
3707
+ $parts[] = scssc::$selfSelector;
3708
+ continue;
3709
+ }
3710
+
3711
+ if ($this->literal(".", false)) {
3712
+ $parts[] = ".";
3713
+ continue;
3714
+ }
3715
+
3716
+ if ($this->literal("|", false)) {
3717
+ $parts[] = "|";
3718
+ continue;
3719
+ }
3720
+
3721
+ // for keyframes
3722
+ if ($this->unit($unit)) {
3723
+ $parts[] = $unit;
3724
+ continue;
3725
+ }
3726
+
3727
+ if ($this->keyword($name)) {
3728
+ $parts[] = $name;
3729
+ continue;
3730
+ }
3731
+
3732
+ if ($this->interpolation($inter)) {
3733
+ $parts[] = $inter;
3734
+ continue;
3735
+ }
3736
+
3737
+ if ($this->literal('%', false) && $this->placeholder($placeholder)) {
3738
+ $parts[] = '%';
3739
+ $parts[] = $placeholder;
3740
+ continue;
3741
+ }
3742
+
3743
+ if ($this->literal("#", false)) {
3744
+ $parts[] = "#";
3745
+ continue;
3746
+ }
3747
+
3748
+ // a pseudo selector
3749
+ if ($this->match("::?", $m) && $this->mixedKeyword($nameParts)) {
3750
+ $parts[] = $m[0];
3751
+ foreach ($nameParts as $sub) {
3752
+ $parts[] = $sub;
3753
+ }
3754
+
3755
+ $ss = $this->seek();
3756
+ if ($this->literal("(") &&
3757
+ ($this->openString(")", $str, "(") || true ) &&
3758
+ $this->literal(")"))
3759
+ {
3760
+ $parts[] = "(";
3761
+ if (!empty($str)) $parts[] = $str;
3762
+ $parts[] = ")";
3763
+ } else {
3764
+ $this->seek($ss);
3765
+ }
3766
+
3767
+ continue;
3768
+ } else {
3769
+ $this->seek($s);
3770
+ }
3771
+
3772
+ // attribute selector
3773
+ // TODO: replace with open string?
3774
+ if ($this->literal("[", false)) {
3775
+ $attrParts = array("[");
3776
+ // keyword, string, operator
3777
+ while (true) {
3778
+ if ($this->literal("]", false)) {
3779
+ $this->count--;
3780
+ break; // get out early
3781
+ }
3782
+
3783
+ if ($this->match('\s+', $m)) {
3784
+ $attrParts[] = " ";
3785
+ continue;
3786
+ }
3787
+ if ($this->string($str)) {
3788
+ $attrParts[] = $str;
3789
+ continue;
3790
+ }
3791
+
3792
+ if ($this->keyword($word)) {
3793
+ $attrParts[] = $word;
3794
+ continue;
3795
+ }
3796
+
3797
+ if ($this->interpolation($inter, false)) {
3798
+ $attrParts[] = $inter;
3799
+ continue;
3800
+ }
3801
+
3802
+ // operator, handles attr namespace too
3803
+ if ($this->match('[|-~\$\*\^=]+', $m)) {
3804
+ $attrParts[] = $m[0];
3805
+ continue;
3806
+ }
3807
+
3808
+ break;
3809
+ }
3810
+
3811
+ if ($this->literal("]", false)) {
3812
+ $attrParts[] = "]";
3813
+ foreach ($attrParts as $part) {
3814
+ $parts[] = $part;
3815
+ }
3816
+ continue;
3817
+ }
3818
+ $this->seek($s);
3819
+ // should just break here?
3820
+ }
3821
+
3822
+ break;
3823
+ }
3824
+
3825
+ $this->eatWhiteDefault = $oldWhite;
3826
+
3827
+ if (count($parts) == 0) return false;
3828
+
3829
+ $out = $parts;
3830
+ return true;
3831
+ }
3832
+
3833
+ protected function variable(&$out) {
3834
+ $s = $this->seek();
3835
+ if ($this->literal("$", false) && $this->keyword($name)) {
3836
+ $out = array("var", $name);
3837
+ return true;
3838
+ }
3839
+ $this->seek($s);
3840
+ return false;
3841
+ }
3842
+
3843
+ protected function keyword(&$word, $eatWhitespace = null) {
3844
+ if ($this->match('([\w_\-\*!"\'\\\\][\w\-_"\'\\\\]*)',
3845
+ $m, $eatWhitespace))
3846
+ {
3847
+ $word = $m[1];
3848
+ return true;
3849
+ }
3850
+ return false;
3851
+ }
3852
+
3853
+ protected function placeholder(&$placeholder) {
3854
+ if ($this->match('([\w\-_]+)', $m)) {
3855
+ $placeholder = $m[1];
3856
+ return true;
3857
+ }
3858
+ return false;
3859
+ }
3860
+
3861
+ // consume an end of statement delimiter
3862
+ protected function end() {
3863
+ if ($this->literal(';')) {
3864
+ return true;
3865
+ } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
3866
+ // if there is end of file or a closing block next then we don't need a ;
3867
+ return true;
3868
+ }
3869
+ return false;
3870
+ }
3871
+
3872
+ // advance counter to next occurrence of $what
3873
+ // $until - don't include $what in advance
3874
+ // $allowNewline, if string, will be used as valid char set
3875
+ protected function to($what, &$out, $until = false, $allowNewline = false) {
3876
+ if (is_string($allowNewline)) {
3877
+ $validChars = $allowNewline;
3878
+ } else {
3879
+ $validChars = $allowNewline ? "." : "[^\n]";
3880
+ }
3881
+ if (!$this->match('('.$validChars.'*?)'.$this->preg_quote($what), $m, !$until)) return false;
3882
+ if ($until) $this->count -= strlen($what); // give back $what
3883
+ $out = $m[1];
3884
+ return true;
3885
+ }
3886
+
3887
+ public function throwParseError($msg = "parse error", $count = null) {
3888
+ $count = is_null($count) ? $this->count : $count;
3889
+
3890
+ $line = $this->getLineNo($count);
3891
+
3892
+ if (!empty($this->sourceName)) {
3893
+ $loc = "$this->sourceName on line $line";
3894
+ } else {
3895
+ $loc = "line: $line";
3896
+ }
3897
+
3898
+ if ($this->peek("(.*?)(\n|$)", $m, $count)) {
3899
+ throw new Exception("$msg: failed at `$m[1]` $loc");
3900
+ } else {
3901
+ throw new Exception("$msg: $loc");
3902
+ }
3903
+ }
3904
+
3905
+ public function getLineNo($pos) {
3906
+ return 1 + substr_count(substr($this->buffer, 0, $pos), "\n");
3907
+ }
3908
+
3909
+ /**
3910
+ * Match string looking for either ending delim, escape, or string interpolation
3911
+ *
3912
+ * {@internal This is a workaround for preg_match's 250K string match limit. }}
3913
+ *
3914
+ * @param array $m Matches (passed by reference)
3915
+ * @param string $delim Delimeter
3916
+ *
3917
+ * @return boolean True if match; false otherwise
3918
+ */
3919
+ protected function matchString(&$m, $delim) {
3920
+ $token = null;
3921
+
3922
+ $end = strpos($this->buffer, "\n", $this->count);
3923
+ if ($end === false) {
3924
+ $end = strlen($this->buffer);
3925
+ }
3926
+
3927
+ // look for either ending delim, escape, or string interpolation
3928
+ foreach (array('#{', '\\', $delim) as $lookahead) {
3929
+ $pos = strpos($this->buffer, $lookahead, $this->count);
3930
+ if ($pos !== false && $pos < $end) {
3931
+ $end = $pos;
3932
+ $token = $lookahead;
3933
+ }
3934
+ }
3935
+
3936
+ if (!isset($token)) {
3937
+ return false;
3938
+ }
3939
+
3940
+ $match = substr($this->buffer, $this->count, $end - $this->count);
3941
+ $m = array(
3942
+ $match . $token,
3943
+ $match,
3944
+ $token
3945
+ );
3946
+ $this->count = $end + strlen($token);
3947
+
3948
+ return true;
3949
+ }
3950
+
3951
+ // try to match something on head of buffer
3952
+ protected function match($regex, &$out, $eatWhitespace = null) {
3953
+ if (is_null($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault;
3954
+
3955
+ $r = '/'.$regex.'/Ais';
3956
+ if (preg_match($r, $this->buffer, $out, null, $this->count)) {
3957
+ $this->count += strlen($out[0]);
3958
+ if ($eatWhitespace) $this->whitespace();
3959
+ return true;
3960
+ }
3961
+ return false;
3962
+ }
3963
+
3964
+ // match some whitespace
3965
+ protected function whitespace() {
3966
+ $gotWhite = false;
3967
+ while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
3968
+ if ($this->insertComments) {
3969
+ if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
3970
+ $this->append(array("comment", $m[1]));
3971
+ $this->commentsSeen[$this->count] = true;
3972
+ }
3973
+ }
3974
+ $this->count += strlen($m[0]);
3975
+ $gotWhite = true;
3976
+ }
3977
+ return $gotWhite;
3978
+ }
3979
+
3980
+ protected function peek($regex, &$out, $from=null) {
3981
+ if (is_null($from)) $from = $this->count;
3982
+
3983
+ $r = '/'.$regex.'/Ais';
3984
+ $result = preg_match($r, $this->buffer, $out, null, $from);
3985
+
3986
+ return $result;
3987
+ }
3988
+
3989
+ protected function seek($where = null) {
3990
+ if ($where === null) return $this->count;
3991
+ else $this->count = $where;
3992
+ return true;
3993
+ }
3994
+
3995
+ static function preg_quote($what) {
3996
+ return preg_quote($what, '/');
3997
+ }
3998
+
3999
+ protected function show() {
4000
+ if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
4001
+ return $m[1];
4002
+ }
4003
+ return "";
4004
+ }
4005
+
4006
+ // turn list of length 1 into value type
4007
+ protected function flattenList($value) {
4008
+ if ($value[0] == "list" && count($value[2]) == 1) {
4009
+ return $this->flattenList($value[2][0]);
4010
+ }
4011
+ return $value;
4012
+ }
4013
+ }
4014
+
4015
+ /**
4016
+ * SCSS base formatter
4017
+ *
4018
+ * @author Leaf Corcoran <leafot@gmail.com>
4019
+ */
4020
+ class scss_formatter {
4021
+ public $indentChar = " ";
4022
+
4023
+ public $break = "\n";
4024
+ public $open = " {";
4025
+ public $close = "}";
4026
+ public $tagSeparator = ", ";
4027
+ public $assignSeparator = ": ";
4028
+
4029
+ public function __construct() {
4030
+ $this->indentLevel = 0;
4031
+ }
4032
+
4033
+ public function indentStr($n = 0) {
4034
+ return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
4035
+ }
4036
+
4037
+ public function property($name, $value) {
4038
+ return $name . $this->assignSeparator . $value . ";";
4039
+ }
4040
+
4041
+ protected function block($block) {
4042
+ if (empty($block->lines) && empty($block->children)) return;
4043
+
4044
+ $inner = $pre = $this->indentStr();
4045
+
4046
+ if (!empty($block->selectors)) {
4047
+ echo $pre .
4048
+ implode($this->tagSeparator, $block->selectors) .
4049
+ $this->open . $this->break;
4050
+ $this->indentLevel++;
4051
+ $inner = $this->indentStr();
4052
+ }
4053
+
4054
+ if (!empty($block->lines)) {
4055
+ $glue = $this->break.$inner;
4056
+ echo $inner . implode($glue, $block->lines);
4057
+ if (!empty($block->children)) {
4058
+ echo $this->break;
4059
+ }
4060
+ }
4061
+
4062
+ foreach ($block->children as $child) {
4063
+ $this->block($child);
4064
+ }
4065
+
4066
+ if (!empty($block->selectors)) {
4067
+ $this->indentLevel--;
4068
+ if (empty($block->children)) echo $this->break;
4069
+ echo $pre . $this->close . $this->break;
4070
+ }
4071
+ }
4072
+
4073
+ public function format($block) {
4074
+ ob_start();
4075
+ $this->block($block);
4076
+ $out = ob_get_clean();
4077
+
4078
+ return $out;
4079
+ }
4080
+ }
4081
+
4082
+ /**
4083
+ * SCSS nested formatter
4084
+ *
4085
+ * @author Leaf Corcoran <leafot@gmail.com>
4086
+ */
4087
+ class scss_formatter_nested extends scss_formatter {
4088
+ public $close = " }";
4089
+
4090
+ // adjust the depths of all children, depth first
4091
+ public function adjustAllChildren($block) {
4092
+ // flatten empty nested blocks
4093
+ $children = array();
4094
+ foreach ($block->children as $i => $child) {
4095
+ if (empty($child->lines) && empty($child->children)) {
4096
+ if (isset($block->children[$i + 1])) {
4097
+ $block->children[$i + 1]->depth = $child->depth;
4098
+ }
4099
+ continue;
4100
+ }
4101
+ $children[] = $child;
4102
+ }
4103
+
4104
+ $count = count($children);
4105
+ for ($i = 0; $i < $count; $i++) {
4106
+ $depth = $children[$i]->depth;
4107
+ $j = $i + 1;
4108
+ if (isset($children[$j]) && $depth < $children[$j]->depth) {
4109
+ $childDepth = $children[$j]->depth;
4110
+ for (; $j < $count; $j++) {
4111
+ if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) {
4112
+ $children[$j]->depth = $depth + 1;
4113
+ }
4114
+ }
4115
+ }
4116
+ }
4117
+
4118
+ $block->children = $children;
4119
+
4120
+ // make relative to parent
4121
+ foreach ($block->children as $child) {
4122
+ $this->adjustAllChildren($child);
4123
+ $child->depth = $child->depth - $block->depth;
4124
+ }
4125
+ }
4126
+
4127
+ protected function block($block) {
4128
+ if ($block->type == "root") {
4129
+ $this->adjustAllChildren($block);
4130
+ }
4131
+
4132
+ $inner = $pre = $this->indentStr($block->depth - 1);
4133
+ if (!empty($block->selectors)) {
4134
+ echo $pre .
4135
+ implode($this->tagSeparator, $block->selectors) .
4136
+ $this->open . $this->break;
4137
+ $this->indentLevel++;
4138
+ $inner = $this->indentStr($block->depth - 1);
4139
+ }
4140
+
4141
+ if (!empty($block->lines)) {
4142
+ $glue = $this->break.$inner;
4143
+ echo $inner . implode($glue, $block->lines);
4144
+ if (!empty($block->children)) echo $this->break;
4145
+ }
4146
+
4147
+ foreach ($block->children as $i => $child) {
4148
+ // echo "*** block: ".$block->depth." child: ".$child->depth."\n";
4149
+ $this->block($child);
4150
+ if ($i < count($block->children) - 1) {
4151
+ echo $this->break;
4152
+
4153
+ if (isset($block->children[$i + 1])) {
4154
+ $next = $block->children[$i + 1];
4155
+ if ($next->depth == max($block->depth, 1) && $child->depth >= $next->depth) {
4156
+ echo $this->break;
4157
+ }
4158
+ }
4159
+ }
4160
+ }
4161
+
4162
+ if (!empty($block->selectors)) {
4163
+ $this->indentLevel--;
4164
+ echo $this->close;
4165
+ }
4166
+
4167
+ if ($block->type == "root") {
4168
+ echo $this->break;
4169
+ }
4170
+ }
4171
+ }
4172
+
4173
+ /**
4174
+ * SCSS compressed formatter
4175
+ *
4176
+ * @author Leaf Corcoran <leafot@gmail.com>
4177
+ */
4178
+ class scss_formatter_compressed extends scss_formatter {
4179
+ public $open = "{";
4180
+ public $tagSeparator = ",";
4181
+ public $assignSeparator = ":";
4182
+ public $break = "";
4183
+
4184
+ public function indentStr($n = 0) {
4185
+ return "";
4186
+ }
4187
+ }
4188
+
4189
+ /**
4190
+ * SCSS server
4191
+ *
4192
+ * @author Leaf Corcoran <leafot@gmail.com>
4193
+ */
4194
+ class scss_server {
4195
+ /**
4196
+ * Join path components
4197
+ *
4198
+ * @param string $left Path component, left of the directory separator
4199
+ * @param string $right Path component, right of the directory separator
4200
+ *
4201
+ * @return string
4202
+ */
4203
+ protected function join($left, $right) {
4204
+ return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\');
4205
+ }
4206
+
4207
+ /**
4208
+ * Get name of requested .scss file
4209
+ *
4210
+ * @return string|null
4211
+ */
4212
+ protected function inputName() {
4213
+ switch (true) {
4214
+ case isset($_GET['p']):
4215
+ return $_GET['p'];
4216
+ case isset($_SERVER['PATH_INFO']):
4217
+ return $_SERVER['PATH_INFO'];
4218
+ case isset($_SERVER['DOCUMENT_URI']):
4219
+ return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME']));
4220
+ }
4221
+ }
4222
+
4223
+ /**
4224
+ * Get path to requested .scss file
4225
+ *
4226
+ * @return string
4227
+ */
4228
+ protected function findInput() {
4229
+ if (($input = $this->inputName())
4230
+ && strpos($input, '..') === false
4231
+ && substr($input, -5) === '.scss'
4232
+ ) {
4233
+ $name = $this->join($this->dir, $input);
4234
+
4235
+ if (is_file($name) && is_readable($name)) {
4236
+ return $name;
4237
+ }
4238
+ }
4239
+
4240
+ return false;
4241
+ }
4242
+
4243
+ /**
4244
+ * Get path to cached .css file
4245
+ *
4246
+ * @return string
4247
+ */
4248
+ protected function cacheName($fname) {
4249
+ return $this->join($this->cacheDir, md5($fname) . '.css');
4250
+ }
4251
+
4252
+ /**
4253
+ * Get path to cached imports
4254
+ *
4255
+ * @return string
4256
+ */
4257
+ protected function importsCacheName($out) {
4258
+ return $out . '.imports';
4259
+ }
4260
+
4261
+ /**
4262
+ * Determine whether .scss file needs to be re-compiled.
4263
+ *
4264
+ * @param string $in Input path
4265
+ * @param string $out Output path
4266
+ *
4267
+ * @return boolean True if compile required.
4268
+ */
4269
+ protected function needsCompile($in, $out) {
4270
+ if (!is_file($out)) return true;
4271
+
4272
+ $mtime = filemtime($out);
4273
+ if (filemtime($in) > $mtime) return true;
4274
+
4275
+ // look for modified imports
4276
+ $icache = $this->importsCacheName($out);
4277
+ if (is_readable($icache)) {
4278
+ $imports = unserialize(file_get_contents($icache));
4279
+ foreach ($imports as $import) {
4280
+ if (filemtime($import) > $mtime) return true;
4281
+ }
4282
+ }
4283
+ return false;
4284
+ }
4285
+
4286
+ /**
4287
+ * Compile .scss file
4288
+ *
4289
+ * @param string $in Input path (.scss)
4290
+ * @param string $out Output path (.css)
4291
+ *
4292
+ * @return string
4293
+ */
4294
+ protected function compile($in, $out) {
4295
+ $start = microtime(true);
4296
+ $css = $this->scss->compile(file_get_contents($in), $in);
4297
+ $elapsed = round((microtime(true) - $start), 4);
4298
+
4299
+ $v = scssc::$VERSION;
4300
+ $t = date('r');
4301
+ $css = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css;
4302
+
4303
+ file_put_contents($out, $css);
4304
+ file_put_contents($this->importsCacheName($out),
4305
+ serialize($this->scss->getParsedFiles()));
4306
+ return $css;
4307
+ }
4308
+
4309
+ /**
4310
+ * Compile requested scss and serve css. Outputs HTTP response.
4311
+ */
4312
+ public function serve() {
4313
+ if ($input = $this->findInput()) {
4314
+ $output = $this->cacheName($input);
4315
+ header('Content-type: text/css');
4316
+
4317
+ if ($this->needsCompile($input, $output)) {
4318
+ try {
4319
+ echo $this->compile($input, $output);
4320
+ } catch (Exception $e) {
4321
+ header('HTTP/1.1 500 Internal Server Error');
4322
+ echo 'Parse error: ' . $e->getMessage() . "\n";
4323
+ }
4324
+ } else {
4325
+ header('X-SCSS-Cache: true');
4326
+ echo file_get_contents($output);
4327
+ }
4328
+
4329
+ return;
4330
+ }
4331
+
4332
+ header('HTTP/1.0 404 Not Found');
4333
+ header('Content-type: text');
4334
+ $v = scssc::$VERSION;
4335
+ echo "/* INPUT NOT FOUND scss $v */\n";
4336
+ }
4337
+
4338
+ /**
4339
+ * Constructor
4340
+ *
4341
+ * @param string $dir Root directory to .scss files
4342
+ * @param string $cacheDir Cache directory
4343
+ * @param \scssc|null $scss SCSS compiler instance
4344
+ */
4345
+ public function __construct($dir, $cacheDir=null, $scss=null) {
4346
+ $this->dir = $dir;
4347
+
4348
+ if (is_null($cacheDir)) {
4349
+ $cacheDir = $this->join($dir, 'scss_cache');
4350
+ }
4351
+
4352
+ $this->cacheDir = $cacheDir;
4353
+ if (!is_dir($this->cacheDir)) mkdir($this->cacheDir, 0755, true);
4354
+
4355
+ if (is_null($scss)) {
4356
+ $scss = new scssc();
4357
+ $scss->setImportPaths($this->dir);
4358
+ }
4359
+ $this->scss = $scss;
4360
+ }
4361
+
4362
+ /**
4363
+ * Helper method to serve compiled scss
4364
+ *
4365
+ * @param string $path Root path
4366
+ */
4367
+ static public function serveFrom($path) {
4368
+ $server = new self($path);
4369
+ $server->serve();
4370
+ }
4371
+ }
wp-scss.php ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
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.1
7
+ * Author: Connect Think
8
+ * Author URI: http://connectthink.com
9
+ * License: GPLv3
10
+ */
11
+
12
+ /**
13
+ * Plugin Workflow
14
+ * 1. Create plugin global variables
15
+ * 2. Require dependancies
16
+ * a. scssphp - does scss compiling using php (vendor)
17
+ * b. wp-scss class - manages compiling
18
+ * c. options class - builds settings page
19
+ * 3. Registering Settings Page and Options
20
+ * 4. Assign plugin settings
21
+ * 5. Instantiate wp_scss object and run compiler
22
+ * 6. Handle Errors
23
+ * 7. Enqueue Styles
24
+ */
25
+
26
+
27
+ /*
28
+ * 1. PLUGIN GLOBAL VARIABLES
29
+ */
30
+
31
+ // Plugin Paths
32
+ if (!defined('WPSCSS_THEME_DIR'))
33
+ define('WPSCSS_THEME_DIR', ABSPATH . 'wp-content/themes/' . get_stylesheet());
34
+
35
+ if (!defined('WPSCSS_PLUGIN_NAME'))
36
+ define('WPSCSS_PLUGIN_NAME', trim(dirname(plugin_basename(__FILE__)), '/'));
37
+
38
+ if (!defined('WPSCSS_PLUGIN_DIR'))
39
+ define('WPSCSS_PLUGIN_DIR', WP_PLUGIN_DIR . '/' . WPSCSS_PLUGIN_NAME);
40
+
41
+ if (!defined('WPSCSS_PLUGIN_URL'))
42
+ define('WPSCSS_PLUGIN_URL', WP_PLUGIN_URL . '/' . WPSCSS_PLUGIN_NAME);
43
+
44
+ // Plugin Version
45
+ if (!defined('WPSCSS_VERSION_KEY'))
46
+ define('WPSCSS_VERSION_KEY', 'wpscss_version');
47
+
48
+ if (!defined('WPSCSS_VERSION_NUM'))
49
+ define('WPSCSS_VERSION_NUM', '1.1.1');
50
+
51
+ // Add version to options table
52
+ add_option(WPSCSS_VERSION_KEY, WPSCSS_VERSION_NUM);
53
+
54
+
55
+ /*
56
+ * 2. REQUIRE DEPENDANCIES
57
+ *
58
+ * scssphp - scss compiler
59
+ * class-wp-scss
60
+ * options.php - settings for plugin page
61
+ */
62
+
63
+ include_once WPSCSS_PLUGIN_DIR . '/scssphp/scss.inc.php'; // Sass Compiler (vendor)
64
+ include_once WPSCSS_PLUGIN_DIR . '/class/class-wp-scss.php'; // Compiling Manager
65
+ include_once WPSCSS_PLUGIN_DIR . '/options.php'; // Options page class
66
+
67
+
68
+ /**
69
+ * 3. REGISTER SETTINGS
70
+ *
71
+ * Instantiate Options Page
72
+ * Create link on plugin page to settings page
73
+ */
74
+
75
+ if( is_admin() ) {
76
+ $wpscss_settings = new Wp_Scss_Settings();
77
+ }
78
+
79
+ add_filter('plugin_action_links', 'wpscss_plugin_action_links', 10, 2);
80
+ function wpscss_plugin_action_links($links, $file) {
81
+ static $this_plugin;
82
+
83
+ if( !$this_plugin ) {
84
+ $this_plugin = plugin_basename(__FILE__);
85
+ }
86
+
87
+ if ($file == $this_plugin) {
88
+ $settings_link = '<a href="' . get_bloginfo('wpurl') . '/wp-admin/admin.php?page=wpscss_options">Settings</a>';
89
+ array_unshift($links, $settings_link);
90
+ }
91
+
92
+ return $links;
93
+ }
94
+
95
+
96
+ /**
97
+ * 4. PLUGIN SETTINGS
98
+ *
99
+ * Pull settings from options table
100
+ * Scrub empty fields or directories that don't exists
101
+ * Assign settings via settings array to pass to object
102
+ */
103
+
104
+ $wpscss_options = get_option( 'wpscss_options' );
105
+ $scss_dir_setting = $wpscss_options['scss_dir'];
106
+ $css_dir_setting = $wpscss_options['css_dir'];
107
+
108
+ // Checks if directories are empty
109
+ if( $scss_dir_setting == false || $css_dir_setting == false ) {
110
+ function wpscss_settings_error() {
111
+ echo '<div class="error">
112
+ <p><strong>Wp-Scss</strong> requires both directories be specified. <a href="' . get_bloginfo('wpurl') . '/wp-admin/admin.php?page=wpscss_options">Please update your settings.</a></p>
113
+ </div>';
114
+ }
115
+ add_action('admin_notices', 'wpscss_settings_error');
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>
123
+ </div>';
124
+ }
125
+ add_action('admin_notices', 'wpscss_settings_error');
126
+ return 0; //exits
127
+ }
128
+
129
+ // Plugin Settings
130
+ $wpscss_settings = array(
131
+ 'scss_dir' => WPSCSS_THEME_DIR . $scss_dir_setting,
132
+ 'css_dir' => WPSCSS_THEME_DIR . $css_dir_setting,
133
+ 'compiling' => $wpscss_options['compiling_options'],
134
+ 'errors' => $wpscss_options['errors'],
135
+ 'enqueue' => $wpscss_options['enqueue']
136
+ );
137
+
138
+
139
+ /**
140
+ * 5. INSTANTIATE & EXECUTE COMPILER
141
+ *
142
+ * Passes settings to the object
143
+ * If needs_compiling passes, runs compile method
144
+ */
145
+
146
+ $wpscss_compiler = new Wp_Scss(
147
+ $wpscss_settings['scss_dir'],
148
+ $wpscss_settings['css_dir'],
149
+ $wpscss_settings['compiling']
150
+ );
151
+
152
+
153
+ if ( $wpscss_compiler->needs_compiling() ) {
154
+ $wpscss_compiler->compile();
155
+ }
156
+
157
+
158
+ /**
159
+ * 6. HANDLE COMPILING ERRORS
160
+ *
161
+ * First block handles print errors to front end.
162
+ * This adds a small style block the header to help errors get noticed
163
+ *
164
+ * Second block handles print errors to log file.
165
+ * After the file gets over 1MB it does a purge and deletes the first
166
+ * half of entries in the file.
167
+ */
168
+ $log_file = $wpscss_compiler->scss_dir.'error_log.log';
169
+
170
+ if ( !is_admin() && $wpscss_settings['errors'] === 'show' && count($wpscss_compiler->compile_errors) > 0) {
171
+
172
+ echo '<div class="scss_errors"><pre>';
173
+ echo '<h6 style="margin: 15px 0;">Sass Compiling Error</h6>';
174
+
175
+ foreach( $wpscss_compiler->compile_errors as $error) {
176
+ echo '<p class="sass_error">';
177
+ echo '<strong>'. $error['file'] .'</strong> <br/><em>"'. $error['message'] .'"</em>';
178
+ echo '<p class="sass_error">';
179
+ }
180
+
181
+ echo '</pre></div>';
182
+
183
+ function wpscss_error_styles() {
184
+ echo
185
+ '<style>
186
+ .scss_errors {
187
+ position: fixed;
188
+ top: 0px;
189
+ z-index: 99999;
190
+ width: 100%;
191
+ }
192
+ .scss_errors pre {
193
+ background: #f5f5f5;
194
+ border-left: 5px solid #DD3D36;
195
+ box-shadow: 0 2px 3px rgba(51,51,51, .4);
196
+ color: #666;
197
+ font-family: monospace;
198
+ font-size: 14px;
199
+ margin: 20px 0;
200
+ overflow: auto;
201
+ padding: 20px;
202
+ white-space: pre;
203
+ white-space: pre-wrap;
204
+ word-wrap: break-word;
205
+ }
206
+ </style>';
207
+ }
208
+ add_action('wp_print_styles', 'wpscss_error_styles');
209
+
210
+ } else { // Hide errors and print them to a log file.
211
+ foreach ($wpscss_compiler->compile_errors as $error) {
212
+ $error_string = date('m/d/y g:i:s', time()) .': ';
213
+ $error_string .= $error['file'] .' - '. $error['message'] . PHP_EOL;
214
+ file_put_contents($log_file, $error_string, FILE_APPEND);
215
+ $error_string = "";
216
+ }
217
+ }
218
+
219
+ // Clean out log file if it get's too large
220
+ if ( file_exists($log_file) ) {
221
+ if ( filesize($log_file) > 1000000) {
222
+ $log_contents = file_get_contents($log_file);
223
+ $log_arr = explode("\n", $log_contents);
224
+ $new_contents_arr = array_slice($log_arr, count($log_arr)/2);
225
+ $new_contents = implode(PHP_EOL, $new_contents_arr) . 'LOG FILE CLEANED ' . date('n/j/y g:i:s', time());
226
+ file_put_contents($log_file, $new_contents);
227
+ }
228
+ }
229
+
230
+
231
+ /**
232
+ * 7. ENQUEUE STYLES
233
+ */
234
+
235
+ if ( $wpscss_settings['enqueue'] == '1' ) {
236
+ function wpscss_enqueue_styles() {
237
+ global $wpscss_compiler, $wpscss_options;
238
+ $wpscss_compiler->enqueue_files($wpscss_options['css_dir']);
239
+ }
240
+ add_action('wp_enqueue_scripts', 'wpscss_enqueue_styles', 50);
241
+ }