WordPress Button Plugin MaxButtons - Version 7.13.4

Version Description

  • Updated ScssPhp to 1.0.7
  • Updated SimpleHTMLDom to 1.9.1
  • Fixed Buttons menu tab
Download this release

Release Info

Developer basszje
Plugin Icon 128x128 WordPress Button Plugin MaxButtons
Version 7.13.4
Comparing to
See all releases

Code changes from version 7.13.3 to 7.13.4

Files changed (62) hide show
  1. assets/libraries/{scssphp_legacy → scssphp}/LICENSE.md +1 -1
  2. assets/libraries/{scssphp_legacy → scssphp}/README.md +4 -5
  3. assets/libraries/scssphp/scss.inc.php +6 -4
  4. assets/libraries/scssphp/src/Base/Range.php +3 -3
  5. assets/libraries/scssphp/src/Block.php +9 -4
  6. assets/libraries/scssphp/src/Cache.php +239 -0
  7. assets/libraries/scssphp/src/Colors.php +71 -4
  8. assets/libraries/scssphp/src/Compiler.php +2979 -677
  9. assets/libraries/scssphp/src/Compiler/Environment.php +10 -5
  10. assets/libraries/scssphp/src/Exception/CompilerException.php +3 -3
  11. assets/libraries/scssphp/src/Exception/ParserException.php +3 -3
  12. assets/libraries/scssphp/src/Exception/RangeException.php +3 -3
  13. assets/libraries/scssphp/src/Exception/ServerException.php +3 -3
  14. assets/libraries/scssphp/src/Formatter.php +78 -36
  15. assets/libraries/scssphp/src/Formatter/Compact.php +4 -4
  16. assets/libraries/scssphp/src/Formatter/Compressed.php +24 -5
  17. assets/libraries/scssphp/src/Formatter/Crunched.php +24 -5
  18. assets/libraries/scssphp/src/Formatter/Debug.php +5 -5
  19. assets/libraries/scssphp/src/Formatter/Expanded.php +6 -6
  20. assets/libraries/scssphp/src/Formatter/Nested.php +110 -83
  21. assets/libraries/scssphp/src/Formatter/OutputBlock.php +4 -4
  22. assets/libraries/scssphp/src/Node.php +3 -3
  23. assets/libraries/scssphp/src/Node/Number.php +15 -15
  24. assets/libraries/scssphp/src/Parser.php +994 -366
  25. assets/libraries/scssphp/src/SourceMap/Base64.php +184 -0
  26. assets/libraries/scssphp/src/SourceMap/Base64VLQ.php +146 -0
  27. assets/libraries/scssphp/src/SourceMap/Base64VLQEncoder.php +0 -218
  28. assets/libraries/scssphp/src/SourceMap/SourceMapGenerator.php +45 -35
  29. assets/libraries/scssphp/src/Type.php +3 -3
  30. assets/libraries/scssphp/src/Util.php +8 -8
  31. assets/libraries/scssphp/src/Version.php +4 -4
  32. assets/libraries/scssphp_legacy/scss.inc.php +0 -29
  33. assets/libraries/scssphp_legacy/src/Base/Range.php +0 -47
  34. assets/libraries/scssphp_legacy/src/Block.php +0 -55
  35. assets/libraries/scssphp_legacy/src/Colors.php +0 -178
  36. assets/libraries/scssphp_legacy/src/Compiler.php +0 -4924
  37. assets/libraries/scssphp_legacy/src/Compiler/Environment.php +0 -42
  38. assets/libraries/scssphp_legacy/src/Formatter.php +0 -202
  39. assets/libraries/scssphp_legacy/src/Formatter/Compact.php +0 -44
  40. assets/libraries/scssphp_legacy/src/Formatter/Compressed.php +0 -84
  41. assets/libraries/scssphp_legacy/src/Formatter/Crunched.php +0 -71
  42. assets/libraries/scssphp_legacy/src/Formatter/Debug.php +0 -118
  43. assets/libraries/scssphp_legacy/src/Formatter/Expanded.php +0 -67
  44. assets/libraries/scssphp_legacy/src/Formatter/Nested.php +0 -197
  45. assets/libraries/scssphp_legacy/src/Formatter/OutputBlock.php +0 -50
  46. assets/libraries/scssphp_legacy/src/Node.php +0 -35
  47. assets/libraries/scssphp_legacy/src/Node/Number.php +0 -236
  48. assets/libraries/scssphp_legacy/src/Parser.php +0 -2377
  49. assets/libraries/scssphp_legacy/src/Server.php +0 -459
  50. assets/libraries/scssphp_legacy/src/Type.php +0 -68
  51. assets/libraries/scssphp_legacy/src/Util.php +0 -55
  52. assets/libraries/scssphp_legacy/src/Version.php +0 -22
  53. assets/libraries/simplehtmldom/CHANGELOG.md +265 -0
  54. assets/libraries/simplehtmldom/LICENSE +21 -0
  55. assets/libraries/simplehtmldom/phpcompatibility.xml +11 -0
  56. assets/libraries/simplehtmldom/phpcs.xml +48 -0
  57. assets/libraries/simplehtmldom/simple_html_dom.php +2354 -1749
  58. classes/maxCSSParser.php +32 -13
  59. classes/maxbuttons-admin-helper.php +2 -2
  60. classes/maxbuttons-class.php +0 -8
  61. maxbuttons.php +3 -20
  62. readme.txt +7 -1
assets/libraries/{scssphp_legacy → scssphp}/LICENSE.md RENAMED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2015 Leaf Corcoran, http://leafo.github.io/scssphp
2
 
3
  Permission is hereby granted, free of charge, to any person obtaining
4
  a copy of this software and associated documentation files (the
1
+ Copyright (c) 2015 Leaf Corcoran, http://scssphp.github.io/scssphp
2
 
3
  Permission is hereby granted, free of charge, to any person obtaining
4
  a copy of this software and associated documentation files (the
assets/libraries/{scssphp_legacy → scssphp}/README.md RENAMED
@@ -1,12 +1,12 @@
1
  # scssphp
2
- ### <http://leafo.github.io/scssphp>
3
 
4
- [![Build](https://travis-ci.org/leafo/scssphp.svg?branch=master)](http://travis-ci.org/leafo/scssphp)
5
- [![License](https://poser.pugx.org/leafo/scssphp/license.svg)](https://packagist.org/packages/leafo/scssphp)
6
 
7
  `scssphp` is a compiler for SCSS written in PHP.
8
 
9
- Checkout the homepage, <http://leafo.github.io/scssphp>, for directions on how to use.
10
 
11
  ## Running Tests
12
 
@@ -24,7 +24,6 @@ There are several tests in the `tests/` directory:
24
  * `InputTest.php` compiles every `.scss` file in the `tests/inputs` directory
25
  then compares to the respective `.css` file in the `tests/outputs` directory.
26
  * `ScssTest.php` extracts (ruby) `scss` tests from the `tests/scss_test.rb` file.
27
- * `ServerTest.php` contains functional tests for the `Server` class.
28
 
29
  When changing any of the tests in `tests/inputs`, the tests will most likely
30
  fail because the output has changed. Once you verify that the output is correct
1
  # scssphp
2
+ ### <http://scssphp.github.io/scssphp>
3
 
4
+ [![Build](https://travis-ci.org/scssphp/scssphp.svg?branch=master)](http://travis-ci.org/scssphp/scssphp)
5
+ [![License](https://poser.pugx.org/scssphp/scssphp/license)](https://packagist.org/packages/scssphp/scssphp)
6
 
7
  `scssphp` is a compiler for SCSS written in PHP.
8
 
9
+ Checkout the homepage, <http://scssphp.github.io/scssphp>, for directions on how to use.
10
 
11
  ## Running Tests
12
 
24
  * `InputTest.php` compiles every `.scss` file in the `tests/inputs` directory
25
  then compares to the respective `.css` file in the `tests/outputs` directory.
26
  * `ScssTest.php` extracts (ruby) `scss` tests from the `tests/scss_test.rb` file.
 
27
 
28
  When changing any of the tests in `tests/inputs`, the tests will most likely
29
  fail because the output has changed. Once you verify that the output is correct
assets/libraries/scssphp/scss.inc.php CHANGED
@@ -1,11 +1,12 @@
1
  <?php
2
- if (version_compare(PHP_VERSION, '5.4') < 0) {
3
- throw new \Exception('scssphp requires PHP 5.4 or above');
4
  }
5
 
6
- if (! class_exists('Leafo\ScssPhp\Version', false)) {
7
  include_once __DIR__ . '/src/Base/Range.php';
8
  include_once __DIR__ . '/src/Block.php';
 
9
  include_once __DIR__ . '/src/Colors.php';
10
  include_once __DIR__ . '/src/Compiler.php';
11
  include_once __DIR__ . '/src/Compiler/Environment.php';
@@ -24,7 +25,8 @@ if (! class_exists('Leafo\ScssPhp\Version', false)) {
24
  include_once __DIR__ . '/src/Node.php';
25
  include_once __DIR__ . '/src/Node/Number.php';
26
  include_once __DIR__ . '/src/Parser.php';
27
- include_once __DIR__ . '/src/SourceMap/Base64VLQEncoder.php';
 
28
  include_once __DIR__ . '/src/SourceMap/SourceMapGenerator.php';
29
  include_once __DIR__ . '/src/Type.php';
30
  include_once __DIR__ . '/src/Util.php';
1
  <?php
2
+ if (version_compare(PHP_VERSION, '5.6') < 0) {
3
+ throw new \Exception('scssphp requires PHP 5.6 or above');
4
  }
5
 
6
+ if (! class_exists('ScssPhp\ScssPhp\Version', false)) {
7
  include_once __DIR__ . '/src/Base/Range.php';
8
  include_once __DIR__ . '/src/Block.php';
9
+ include_once __DIR__ . '/src/Cache.php';
10
  include_once __DIR__ . '/src/Colors.php';
11
  include_once __DIR__ . '/src/Compiler.php';
12
  include_once __DIR__ . '/src/Compiler/Environment.php';
25
  include_once __DIR__ . '/src/Node.php';
26
  include_once __DIR__ . '/src/Node/Number.php';
27
  include_once __DIR__ . '/src/Parser.php';
28
+ include_once __DIR__ . '/src/SourceMap/Base64.php';
29
+ include_once __DIR__ . '/src/SourceMap/Base64VLQ.php';
30
  include_once __DIR__ . '/src/SourceMap/SourceMapGenerator.php';
31
  include_once __DIR__ . '/src/Type.php';
32
  include_once __DIR__ . '/src/Util.php';
assets/libraries/scssphp/src/Base/Range.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2015-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Base;
13
 
14
  /**
15
  * Range
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2015-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Base;
13
 
14
  /**
15
  * Range
assets/libraries/scssphp/src/Block.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
 
14
  /**
15
  * Block
@@ -24,7 +24,7 @@ class Block
24
  public $type;
25
 
26
  /**
27
- * @var \Leafo\ScssPhp\Block
28
  */
29
  public $parent;
30
 
@@ -62,4 +62,9 @@ class Block
62
  * @var array
63
  */
64
  public $children;
 
 
 
 
 
65
  }
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
 
14
  /**
15
  * Block
24
  public $type;
25
 
26
  /**
27
+ * @var \ScssPhp\ScssPhp\Block
28
  */
29
  public $parent;
30
 
62
  * @var array
63
  */
64
  public $children;
65
+
66
+ /**
67
+ * @var \ScssPhp\ScssPhp\Block
68
+ */
69
+ public $selfParent;
70
  }
assets/libraries/scssphp/src/Cache.php ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
+ *
7
+ * @license http://opensource.org/licenses/MIT MIT
8
+ *
9
+ * @link http://scssphp.github.io/scssphp
10
+ */
11
+
12
+ namespace ScssPhp\ScssPhp;
13
+
14
+ use Exception;
15
+
16
+ /**
17
+ * The scss cache manager.
18
+ *
19
+ * In short:
20
+ *
21
+ * allow to put in cache/get from cache a generic result from a known operation on a generic dataset,
22
+ * taking in account options that affects the result
23
+ *
24
+ * The cache manager is agnostic about data format and only the operation is expected to be described by string
25
+ *
26
+ */
27
+
28
+ /**
29
+ * SCSS cache
30
+ *
31
+ * @author Cedric Morin
32
+ */
33
+ class Cache
34
+ {
35
+ const CACHE_VERSION = 1;
36
+
37
+ // directory used for storing data
38
+ public static $cacheDir = false;
39
+
40
+ // prefix for the storing data
41
+ public static $prefix = 'scssphp_';
42
+
43
+ // force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit
44
+ public static $forceRefresh = false;
45
+
46
+ // specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up
47
+ public static $gcLifetime = 604800;
48
+
49
+ // array of already refreshed cache if $forceRefresh==='once'
50
+ protected static $refreshed = [];
51
+
52
+ /**
53
+ * Constructor
54
+ *
55
+ * @param array $options
56
+ */
57
+ public function __construct($options)
58
+ {
59
+ // check $cacheDir
60
+ if (isset($options['cacheDir'])) {
61
+ self::$cacheDir = $options['cacheDir'];
62
+ }
63
+
64
+ if (empty(self::$cacheDir)) {
65
+ throw new Exception('cacheDir not set');
66
+ }
67
+
68
+ if (isset($options['prefix'])) {
69
+ self::$prefix = $options['prefix'];
70
+ }
71
+
72
+ if (empty(self::$prefix)) {
73
+ throw new Exception('prefix not set');
74
+ }
75
+
76
+ if (isset($options['forceRefresh'])) {
77
+ self::$forceRefresh = $options['forceRefresh'];
78
+ }
79
+
80
+ self::checkCacheDir();
81
+ }
82
+
83
+ /**
84
+ * Get the cached result of $operation on $what,
85
+ * which is known as dependant from the content of $options
86
+ *
87
+ * @param string $operation parse, compile...
88
+ * @param mixed $what content key (e.g., filename to be treated)
89
+ * @param array $options any option that affect the operation result on the content
90
+ * @param integer $lastModified last modified timestamp
91
+ *
92
+ * @return mixed
93
+ *
94
+ * @throws \Exception
95
+ */
96
+ public function getCache($operation, $what, $options = [], $lastModified = null)
97
+ {
98
+ $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
99
+
100
+ if (((self::$forceRefresh === false) || (self::$forceRefresh === 'once' &&
101
+ isset(self::$refreshed[$fileCache]))) && file_exists($fileCache)
102
+ ) {
103
+ $cacheTime = filemtime($fileCache);
104
+
105
+ if ((is_null($lastModified) || $cacheTime > $lastModified) &&
106
+ $cacheTime + self::$gcLifetime > time()
107
+ ) {
108
+ $c = file_get_contents($fileCache);
109
+ $c = unserialize($c);
110
+
111
+ if (is_array($c) && isset($c['value'])) {
112
+ return $c['value'];
113
+ }
114
+ }
115
+ }
116
+
117
+ return null;
118
+ }
119
+
120
+ /**
121
+ * Put in cache the result of $operation on $what,
122
+ * which is known as dependant from the content of $options
123
+ *
124
+ * @param string $operation
125
+ * @param mixed $what
126
+ * @param mixed $value
127
+ * @param array $options
128
+ */
129
+ public function setCache($operation, $what, $value, $options = [])
130
+ {
131
+ $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
132
+
133
+ $c = ['value' => $value];
134
+ $c = serialize($c);
135
+ file_put_contents($fileCache, $c);
136
+
137
+ if (self::$forceRefresh === 'once') {
138
+ self::$refreshed[$fileCache] = true;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Get the cache name for the caching of $operation on $what,
144
+ * which is known as dependant from the content of $options
145
+ *
146
+ * @param string $operation
147
+ * @param mixed $what
148
+ * @param array $options
149
+ *
150
+ * @return string
151
+ */
152
+ private static function cacheName($operation, $what, $options = [])
153
+ {
154
+ $t = [
155
+ 'version' => self::CACHE_VERSION,
156
+ 'operation' => $operation,
157
+ 'what' => $what,
158
+ 'options' => $options
159
+ ];
160
+
161
+ $t = self::$prefix
162
+ . sha1(json_encode($t))
163
+ . ".$operation"
164
+ . ".scsscache";
165
+
166
+ return $t;
167
+ }
168
+
169
+ /**
170
+ * Check that the cache dir exists and is writeable
171
+ *
172
+ * @throws \Exception
173
+ */
174
+ public static function checkCacheDir()
175
+ {
176
+ self::$cacheDir = str_replace('\\', '/', self::$cacheDir);
177
+ self::$cacheDir = rtrim(self::$cacheDir, '/') . '/';
178
+
179
+ if (! is_dir(self::$cacheDir)) {
180
+ if (! mkdir(self::$cacheDir)) {
181
+ throw new Exception('Cache directory couldn\'t be created: ' . self::$cacheDir);
182
+ }
183
+ }
184
+
185
+ if (! is_writable(self::$cacheDir)) {
186
+ throw new Exception('Cache directory isn\'t writable: ' . self::$cacheDir);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Delete unused cached files
192
+ */
193
+ public static function cleanCache()
194
+ {
195
+ static $clean = false;
196
+
197
+ if ($clean || empty(self::$cacheDir)) {
198
+ return;
199
+ }
200
+
201
+ $clean = true;
202
+
203
+ // only remove files with extensions created by SCSSPHP Cache
204
+ // css files removed based on the list files
205
+ $removeTypes = ['scsscache' => 1];
206
+
207
+ $files = scandir(self::$cacheDir);
208
+
209
+ if (! $files) {
210
+ return;
211
+ }
212
+
213
+ $checkTime = time() - self::$gcLifetime;
214
+
215
+ foreach ($files as $file) {
216
+ // don't delete if the file wasn't created with SCSSPHP Cache
217
+ if (strpos($file, self::$prefix) !== 0) {
218
+ continue;
219
+ }
220
+
221
+ $parts = explode('.', $file);
222
+ $type = array_pop($parts);
223
+
224
+ if (! isset($removeTypes[$type])) {
225
+ continue;
226
+ }
227
+
228
+ $fullPath = self::$cacheDir . $file;
229
+ $mtime = filemtime($fullPath);
230
+
231
+ // don't delete if it's a relatively new file
232
+ if ($mtime > $checkTime) {
233
+ continue;
234
+ }
235
+
236
+ unlink($fullPath);
237
+ }
238
+ }
239
+ }
assets/libraries/scssphp/src/Colors.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
 
14
  /**
15
  * CSS Colors
@@ -25,7 +25,7 @@ class Colors
25
  *
26
  * @var array
27
  */
28
- public static $cssColors = [
29
  'aliceblue' => '240,248,255',
30
  'antiquewhite' => '250,235,215',
31
  'aqua' => '0,255,255',
@@ -176,4 +176,71 @@ class Colors
176
  'yellow' => '255,255,0',
177
  'yellowgreen' => '154,205,50',
178
  ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  }
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
 
14
  /**
15
  * CSS Colors
25
  *
26
  * @var array
27
  */
28
+ protected static $cssColors = [
29
  'aliceblue' => '240,248,255',
30
  'antiquewhite' => '250,235,215',
31
  'aqua' => '0,255,255',
176
  'yellow' => '255,255,0',
177
  'yellowgreen' => '154,205,50',
178
  ];
179
+
180
+ /**
181
+ * Convert named color in a [r,g,b[,a]] array
182
+ *
183
+ * @param string $colorName
184
+ *
185
+ * @return array|null
186
+ */
187
+ public static function colorNameToRGBa($colorName)
188
+ {
189
+ if (is_string($colorName) && isset(static::$cssColors[$colorName])) {
190
+ $rgba = explode(',', static::$cssColors[$colorName]);
191
+
192
+ // only case with opacity is transparent, with opacity=0, so we can intval on opacity also
193
+ $rgba = array_map('intval', $rgba);
194
+
195
+ return $rgba;
196
+ }
197
+
198
+ return null;
199
+ }
200
+
201
+ /**
202
+ * Reverse conversion : from RGBA to a color name if possible
203
+ *
204
+ * @param integer $r
205
+ * @param integer $g
206
+ * @param integer $b
207
+ * @param integer $a
208
+ *
209
+ * @return string|null
210
+ */
211
+ public static function RGBaToColorName($r, $g, $b, $a = 1)
212
+ {
213
+ static $reverseColorTable = null;
214
+
215
+ if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b) || ! is_numeric($a)) {
216
+ return null;
217
+ }
218
+
219
+ if ($a < 1) {
220
+ # specific case we dont' revert according to spec
221
+ #if (! $a && ! $r && ! $g && ! $b) {
222
+ # return 'transparent';
223
+ #}
224
+
225
+ return null;
226
+ }
227
+
228
+ if (is_null($reverseColorTable)) {
229
+ $reverseColorTable = [];
230
+
231
+ foreach (static::$cssColors as $name => $rgb_str) {
232
+ $rgb_str = explode(',', $rgb_str);
233
+
234
+ if (count($rgb_str) == 3) {
235
+ $reverseColorTable[intval($rgb_str[0])][intval($rgb_str[1])][intval($rgb_str[2])] = $name;
236
+ }
237
+ }
238
+ }
239
+
240
+ if (isset($reverseColorTable[intval($r)][intval($g)][intval($b)])) {
241
+ return $reverseColorTable[intval($r)][intval($g)][intval($b)];
242
+ }
243
+
244
+ return null;
245
+ }
246
  }
assets/libraries/scssphp/src/Compiler.php CHANGED
@@ -2,26 +2,27 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
-
14
- use Leafo\ScssPhp\Base\Range;
15
- use Leafo\ScssPhp\Block;
16
- use Leafo\ScssPhp\Colors;
17
- use Leafo\ScssPhp\Compiler\Environment;
18
- use Leafo\ScssPhp\Exception\CompilerException;
19
- use Leafo\ScssPhp\Formatter\OutputBlock;
20
- use Leafo\ScssPhp\Node;
21
- use Leafo\ScssPhp\SourceMap\SourceMapGenerator;
22
- use Leafo\ScssPhp\Type;
23
- use Leafo\ScssPhp\Parser;
24
- use Leafo\ScssPhp\Util;
 
25
 
26
  /**
27
  * The scss compiler and parser.
@@ -98,17 +99,17 @@ class Compiler
98
  'function' => '^',
99
  ];
100
 
101
- static public $true = [Type::T_KEYWORD, 'true'];
102
- static public $false = [Type::T_KEYWORD, 'false'];
103
- static public $null = [Type::T_NULL];
104
- static public $nullString = [Type::T_STRING, '', []];
105
  static public $defaultValue = [Type::T_KEYWORD, ''];
106
  static public $selfSelector = [Type::T_SELF];
107
- static public $emptyList = [Type::T_LIST, '', []];
108
- static public $emptyMap = [Type::T_MAP, [], []];
109
- static public $emptyString = [Type::T_STRING, '"', []];
110
- static public $with = [Type::T_KEYWORD, 'with'];
111
- static public $without = [Type::T_KEYWORD, 'without'];
112
 
113
  protected $importPaths = [''];
114
  protected $importCache = [];
@@ -129,15 +130,15 @@ class Compiler
129
  protected $sourceMapOptions = [];
130
 
131
  /**
132
- * @var string|\Leafo\ScssPhp\Formatter
133
  */
134
- protected $formatter = 'Leafo\ScssPhp\Formatter\Nested';
135
 
136
  protected $rootEnv;
137
  protected $rootBlock;
138
 
139
  /**
140
- * @var \Leafo\ScssPhp\Compiler\Environment
141
  */
142
  protected $env;
143
  protected $scope;
@@ -145,26 +146,67 @@ class Compiler
145
  protected $charsetSeen;
146
  protected $sourceNames;
147
 
148
- private $indentLevel;
149
- private $commentsSeen;
150
- private $extends;
151
- private $extendsMap;
152
- private $parsedFiles;
153
- private $parser;
154
- private $sourceIndex;
155
- private $sourceLine;
156
- private $sourceColumn;
157
- private $stderr;
158
- private $shouldEvaluate;
159
- private $ignoreErrors;
 
 
 
160
 
161
  /**
162
  * Constructor
 
 
163
  */
164
- public function __construct()
165
  {
166
  $this->parsedFiles = [];
167
  $this->sourceNames = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  }
169
 
170
  /**
@@ -179,8 +221,28 @@ class Compiler
179
  */
180
  public function compile($code, $path = null)
181
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  $this->indentLevel = -1;
183
- $this->commentsSeen = [];
184
  $this->extends = [];
185
  $this->extendsMap = [];
186
  $this->sourceIndex = null;
@@ -191,11 +253,9 @@ class Compiler
191
  $this->storeEnv = null;
192
  $this->charsetSeen = null;
193
  $this->shouldEvaluate = null;
194
- $this->stderr = fopen('php://stderr', 'w');
195
 
196
  $this->parser = $this->parserFactory($path);
197
- $tree = $this->parser->parse($code);
198
-
199
  $this->parser = null;
200
 
201
  $this->formatter = new $this->formatter();
@@ -236,6 +296,15 @@ class Compiler
236
  $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
237
  }
238
 
 
 
 
 
 
 
 
 
 
239
  return $out;
240
  }
241
 
@@ -244,14 +313,13 @@ class Compiler
244
  *
245
  * @param string $path
246
  *
247
- * @return \Leafo\ScssPhp\Parser
248
  */
249
  protected function parserFactory($path)
250
  {
251
- $parser = new Parser($path, count($this->sourceNames), $this->encoding);
252
 
253
  $this->sourceNames[] = $path;
254
-
255
  $this->addParsedFile($path);
256
 
257
  return $parser;
@@ -279,9 +347,9 @@ class Compiler
279
  /**
280
  * Push extends
281
  *
282
- * @param array $target
283
- * @param array $origin
284
- * @param \stdClass $block
285
  */
286
  protected function pushExtends($target, $origin, $block)
287
  {
@@ -307,17 +375,17 @@ class Compiler
307
  * @param string $type
308
  * @param array $selectors
309
  *
310
- * @return \Leafo\ScssPhp\Formatter\OutputBlock
311
  */
312
  protected function makeOutputBlock($type, $selectors = null)
313
  {
314
  $out = new OutputBlock;
315
- $out->type = $type;
316
- $out->lines = [];
317
- $out->children = [];
318
- $out->parent = $this->scope;
319
- $out->selectors = $selectors;
320
- $out->depth = $this->env->depth;
321
 
322
  if ($this->env->block instanceof Block) {
323
  $out->sourceName = $this->env->block->sourceName;
@@ -335,7 +403,7 @@ class Compiler
335
  /**
336
  * Compile root
337
  *
338
- * @param \Leafo\ScssPhp\Block $rootBlock
339
  */
340
  protected function compileRoot(Block $rootBlock)
341
  {
@@ -374,8 +442,8 @@ class Compiler
374
  /**
375
  * Flatten selectors
376
  *
377
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
378
- * @param string $parentKey
379
  */
380
  protected function flattenSelectors(OutputBlock $block, $parentKey = null)
381
  {
@@ -430,6 +498,40 @@ class Compiler
430
  }
431
  }
432
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  /**
434
  * Match extends
435
  *
@@ -440,32 +542,60 @@ class Compiler
440
  */
441
  protected function matchExtends($selector, &$out, $from = 0, $initial = true)
442
  {
 
 
 
 
 
 
 
 
443
  foreach ($selector as $i => $part) {
444
  if ($i < $from) {
445
  continue;
446
  }
447
 
448
- if ($this->matchExtendsSingle($part, $origin)) {
449
- $after = array_slice($selector, $i + 1);
450
- $before = array_slice($selector, 0, $i);
 
 
 
 
 
 
451
 
 
 
 
 
452
  list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
453
 
454
  foreach ($origin as $new) {
455
  $k = 0;
456
 
457
  // remove shared parts
458
- if ($initial) {
459
  while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
460
  $k++;
461
  }
462
  }
 
 
 
463
 
464
  $replacement = [];
465
  $tempReplacement = $k > 0 ? array_slice($new, $k) : $new;
466
 
467
  for ($l = count($tempReplacement) - 1; $l >= 0; $l--) {
468
- $slice = $tempReplacement[$l];
 
 
 
 
 
 
 
469
  array_unshift($replacement, $slice);
470
 
471
  if (! $this->isImmediateRelationshipCombinator(end($slice))) {
@@ -489,33 +619,93 @@ class Compiler
489
  continue;
490
  }
491
 
492
- $out[] = $result;
493
 
494
  // recursively check for more matches
495
- $this->matchExtends($result, $out, count($before) + count($mergedBefore), false);
 
 
 
 
 
496
 
497
  // selector sequence merging
498
  if (! empty($before) && count($new) > 1) {
499
- $sharedParts = $k > 0 ? array_slice($before, 0, $k) : [];
500
  $postSharedParts = $k > 0 ? array_slice($before, $k) : $before;
501
 
502
- list($injectBetweenSharedParts, $nonBreakable2) = $this->extractRelationshipFromFragment($afterBefore);
503
 
504
  $result2 = array_merge(
505
- $sharedParts,
506
- $injectBetweenSharedParts,
507
  $postSharedParts,
508
- $nonBreakable2,
509
  $nonBreakableBefore,
510
  $replacement,
511
  $after
512
  );
513
 
514
- $out[] = $result2;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
  }
516
  }
517
  }
518
  }
 
519
  }
520
 
521
  /**
@@ -523,14 +713,20 @@ class Compiler
523
  *
524
  * @param array $rawSingle
525
  * @param array $outOrigin
 
526
  *
527
  * @return boolean
528
  */
529
- protected function matchExtendsSingle($rawSingle, &$outOrigin)
530
  {
531
  $counts = [];
532
  $single = [];
533
 
 
 
 
 
 
534
  foreach ($rawSingle as $part) {
535
  // matches Number
536
  if (! is_string($part)) {
@@ -551,20 +747,46 @@ class Compiler
551
  $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
552
  }
553
 
554
- foreach ($single as $part) {
 
 
 
555
  if (isset($this->extendsMap[$part])) {
556
  foreach ($this->extendsMap[$part] as $idx) {
557
  $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
558
  }
559
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
  }
561
 
562
- $outOrigin = [];
563
- $found = false;
564
-
565
  foreach ($counts as $idx => $count) {
566
  list($target, $origin, /* $block */) = $this->extends[$idx];
567
 
 
 
568
  // check count
569
  if ($count !== count($target)) {
570
  continue;
@@ -605,7 +827,6 @@ class Compiler
605
  return $found;
606
  }
607
 
608
-
609
  /**
610
  * Extract a relationship from the fragment.
611
  *
@@ -615,18 +836,20 @@ class Compiler
615
  * the rest.
616
  *
617
  * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
 
618
  * @return array The selector without the relationship fragment if any, the relationship fragment.
619
  */
620
  protected function extractRelationshipFromFragment(array $fragment)
621
  {
622
  $parents = [];
623
  $children = [];
 
624
  $j = $i = count($fragment);
625
 
626
  for (;;) {
627
  $children = $j != $i ? array_slice($fragment, $j, $i - $j) : [];
628
- $parents = array_slice($fragment, 0, $j);
629
- $slice = end($parents);
630
 
631
  if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
632
  break;
@@ -648,22 +871,30 @@ class Compiler
648
  */
649
  protected function combineSelectorSingle($base, $other)
650
  {
651
- $tag = [];
652
- $out = [];
653
- $wasTag = true;
 
 
 
 
 
654
 
655
- foreach ([$base, $other] as $single) {
656
  foreach ($single as $part) {
657
- if (preg_match('/^[\[.:#]/', $part)) {
658
  $out[] = $part;
659
  $wasTag = false;
 
 
 
660
  } elseif (preg_match('/^[^_-]/', $part)) {
661
  $tag[] = $part;
662
  $wasTag = true;
663
  } elseif ($wasTag) {
664
  $tag[count($tag) - 1] .= $part;
665
  } else {
666
- $out[count($out) - 1] .= $part;
667
  }
668
  }
669
  }
@@ -671,6 +902,9 @@ class Compiler
671
  if (count($tag)) {
672
  array_unshift($out, $tag[0]);
673
  }
 
 
 
674
 
675
  return $out;
676
  }
@@ -678,19 +912,24 @@ class Compiler
678
  /**
679
  * Compile media
680
  *
681
- * @param \Leafo\ScssPhp\Block $media
682
  */
683
  protected function compileMedia(Block $media)
684
  {
685
  $this->pushEnv($media);
686
 
687
- $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
688
-
689
- if (! empty($mediaQuery)) {
690
- $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
691
 
 
 
692
  $parentScope = $this->mediaParent($this->scope);
693
- $parentScope->children[] = $this->scope;
 
 
 
 
 
 
694
 
695
  // top level properties in a media cause it to be wrapped
696
  $needsWrap = false;
@@ -720,11 +959,35 @@ class Compiler
720
  $wrapped->children = $media->children;
721
 
722
  $media->children = [[Type::T_BLOCK, $wrapped]];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
723
  }
724
 
725
  $this->compileChildrenNoReturn($media->children, $this->scope);
726
 
727
- $this->scope = $this->scope->parent;
728
  }
729
 
730
  $this->popEnv();
@@ -733,9 +996,9 @@ class Compiler
733
  /**
734
  * Media parent
735
  *
736
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope
737
  *
738
- * @return \Leafo\ScssPhp\Formatter\OutputBlock
739
  */
740
  protected function mediaParent(OutputBlock $scope)
741
  {
@@ -753,33 +1016,42 @@ class Compiler
753
  /**
754
  * Compile directive
755
  *
756
- * @param \Leafo\ScssPhp\Block $block
 
757
  */
758
- protected function compileDirective(Block $block)
759
  {
760
- $s = '@' . $block->name;
 
 
 
 
 
 
 
761
 
762
- if (! empty($block->value)) {
763
- $s .= ' ' . $this->compileValue($block->value);
764
- }
765
 
766
- if ($block->name === 'keyframes' || substr($block->name, -10) === '-keyframes') {
767
- $this->compileKeyframeBlock($block, [$s]);
768
- } else {
769
- $this->compileNestedBlock($block, [$s]);
 
770
  }
771
  }
772
 
773
  /**
774
  * Compile at-root
775
  *
776
- * @param \Leafo\ScssPhp\Block $block
777
  */
778
  protected function compileAtRoot(Block $block)
779
  {
780
  $env = $this->pushEnv($block);
781
  $envs = $this->compactEnv($env);
782
- $without = isset($block->with) ? $this->compileWith($block->with) : static::WITH_RULE;
783
 
784
  // wrap inline selector
785
  if ($block->selector) {
@@ -792,18 +1064,29 @@ class Compiler
792
  $wrapped->comments = [];
793
  $wrapped->parent = $block;
794
  $wrapped->children = $block->children;
 
795
 
796
  $block->children = [[Type::T_BLOCK, $wrapped]];
 
 
 
 
 
 
 
 
 
797
  }
798
 
799
- $this->env = $this->filterWithout($envs, $without);
800
- $newBlock = $this->spliceTree($envs, $block, $without);
801
 
802
  $saveScope = $this->scope;
803
- $this->scope = $this->rootBlock;
804
 
805
- $this->compileChild($newBlock, $this->scope);
 
806
 
 
807
  $this->scope = $saveScope;
808
  $this->env = $this->extractEnv($envs);
809
 
@@ -811,152 +1094,190 @@ class Compiler
811
  }
812
 
813
  /**
814
- * Splice parse tree
815
  *
816
- * @param array $envs
817
- * @param \Leafo\ScssPhp\Block $block
818
- * @param integer $without
819
  *
820
- * @return array
821
  */
822
- private function spliceTree($envs, Block $block, $without)
823
  {
824
- $newBlock = null;
 
825
 
826
- foreach ($envs as $e) {
827
- if (! isset($e->block)) {
828
- continue;
829
- }
830
 
831
- if ($e->block === $block) {
832
- continue;
 
 
 
 
 
 
 
833
  }
834
 
835
- if (isset($e->block->type) && $e->block->type === Type::T_AT_ROOT) {
836
- continue;
 
 
 
 
 
 
 
 
 
837
  }
838
 
839
- if ($e->block && $this->isWithout($without, $e->block)) {
840
- continue;
 
 
 
 
841
  }
 
842
 
843
- $b = new Block;
844
- $b->sourceName = $e->block->sourceName;
845
- $b->sourceIndex = $e->block->sourceIndex;
846
- $b->sourceLine = $e->block->sourceLine;
847
- $b->sourceColumn = $e->block->sourceColumn;
848
- $b->selectors = [];
849
- $b->comments = $e->block->comments;
850
- $b->parent = null;
851
 
852
- if ($newBlock) {
853
- $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
854
 
855
- $b->children = [[$type, $newBlock]];
856
 
857
- $newBlock->parent = $b;
858
- } elseif (count($block->children)) {
859
- foreach ($block->children as $child) {
860
- if ($child[0] === Type::T_BLOCK) {
861
- $child[1]->parent = $b;
862
- }
863
- }
864
 
865
- $b->children = $block->children;
866
- }
 
 
 
 
 
867
 
868
- if (isset($e->block->type)) {
869
- $b->type = $e->block->type;
870
- }
871
 
872
- if (isset($e->block->name)) {
873
- $b->name = $e->block->name;
874
- }
 
 
 
 
 
 
 
 
 
 
 
875
 
876
- if (isset($e->block->queryList)) {
877
- $b->queryList = $e->block->queryList;
 
878
  }
 
879
 
880
- if (isset($e->block->value)) {
881
- $b->value = $e->block->value;
882
- }
883
 
884
- $newBlock = $b;
 
 
 
 
 
 
 
 
 
 
 
885
  }
886
 
887
- $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
 
 
 
 
 
 
888
 
889
- return [$type, $newBlock];
890
  }
891
 
892
  /**
893
- * Compile @at-root's with: inclusion / without: exclusion into filter flags
894
  *
895
- * @param array $with
896
  *
897
- * @return integer
898
  */
899
- private function compileWith($with)
900
  {
901
- static $mapping = [
902
- 'rule' => self::WITH_RULE,
903
- 'media' => self::WITH_MEDIA,
904
- 'supports' => self::WITH_SUPPORTS,
905
- 'all' => self::WITH_ALL,
906
- ];
907
 
908
- // exclude selectors by default
909
- $without = static::WITH_RULE;
 
 
910
 
911
- if ($this->libMapHasKey([$with, static::$with])) {
912
- $without = static::WITH_ALL;
913
-
914
- $list = $this->coerceList($this->libMapGet([$with, static::$with]));
915
-
916
- foreach ($list[2] as $item) {
917
- $keyword = $this->compileStringContent($this->coerceString($item));
918
 
919
- if (array_key_exists($keyword, $mapping)) {
920
- $without &= ~($mapping[$keyword]);
921
  }
922
  }
923
- }
924
 
925
- if ($this->libMapHasKey([$with, static::$without])) {
926
- $without = 0;
 
927
 
928
- $list = $this->coerceList($this->libMapGet([$with, static::$without]));
929
-
930
- foreach ($list[2] as $item) {
931
- $keyword = $this->compileStringContent($this->coerceString($item));
932
 
933
- if (array_key_exists($keyword, $mapping)) {
934
- $without |= $mapping[$keyword];
935
  }
936
  }
937
  }
938
 
939
- return $without;
940
  }
941
 
942
  /**
943
  * Filter env stack
944
  *
945
  * @param array $envs
946
- * @param integer $without
 
947
  *
948
- * @return \Leafo\ScssPhp\Compiler\Environment
949
  */
950
- private function filterWithout($envs, $without)
951
  {
952
  $filtered = [];
953
 
954
  foreach ($envs as $e) {
955
- if ($e->block && $this->isWithout($without, $e->block)) {
956
- continue;
957
- }
 
958
 
959
- $filtered[] = $e;
 
 
 
960
  }
961
 
962
  return $this->extractEnv($filtered);
@@ -965,31 +1286,76 @@ class Compiler
965
  /**
966
  * Filter WITH rules
967
  *
968
- * @param integer $without
969
- * @param \Leafo\ScssPhp\Block $block
 
970
  *
971
  * @return boolean
972
  */
973
- private function isWithout($without, Block $block)
974
  {
975
- if ((($without & static::WITH_RULE) && isset($block->selectors)) ||
976
- (($without & static::WITH_MEDIA) &&
977
- isset($block->type) && $block->type === Type::T_MEDIA) ||
978
- (($without & static::WITH_SUPPORTS) &&
979
- isset($block->type) && $block->type === Type::T_DIRECTIVE &&
980
- isset($block->name) && $block->name === 'supports')
981
- ) {
982
- return true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
983
  }
984
 
985
- return false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
986
  }
987
 
 
988
  /**
989
  * Compile keyframe block
990
  *
991
- * @param \Leafo\ScssPhp\Block $block
992
- * @param array $selectors
993
  */
994
  protected function compileKeyframeBlock(Block $block, $selectors)
995
  {
@@ -1013,11 +1379,45 @@ class Compiler
1013
  $this->popEnv();
1014
  }
1015
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1016
  /**
1017
  * Compile nested block
1018
  *
1019
- * @param \Leafo\ScssPhp\Block $block
1020
- * @param array $selectors
1021
  */
1022
  protected function compileNestedBlock(Block $block, $selectors)
1023
  {
@@ -1026,6 +1426,35 @@ class Compiler
1026
  $this->scope = $this->makeOutputBlock($block->type, $selectors);
1027
  $this->scope->parent->children[] = $this->scope;
1028
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1029
  $this->compileChildrenNoReturn($block->children, $this->scope);
1030
 
1031
  $this->scope = $this->scope->parent;
@@ -1049,7 +1478,7 @@ class Compiler
1049
  *
1050
  * @see Compiler::compileChild()
1051
  *
1052
- * @param \Leafo\ScssPhp\Block $block
1053
  */
1054
  protected function compileBlock(Block $block)
1055
  {
@@ -1085,17 +1514,63 @@ class Compiler
1085
  $this->scope->children[] = $out;
1086
 
1087
  if (count($block->children)) {
1088
- $out->selectors = $this->multiplySelectors($env);
1089
 
1090
- $this->compileChildrenNoReturn($block->children, $out);
1091
- }
1092
 
1093
- $this->formatter->stripSemicolon($out->lines);
 
 
 
1094
 
1095
- $this->popEnv();
1096
- }
1097
 
1098
- /**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1099
  * Compile root level comment
1100
  *
1101
  * @param array $block
@@ -1103,7 +1578,8 @@ class Compiler
1103
  protected function compileComment($block)
1104
  {
1105
  $out = $this->makeOutputBlock(Type::T_COMMENT);
1106
- $out->lines[] = $block[1];
 
1107
  $this->scope->children[] = $out;
1108
  }
1109
 
@@ -1122,8 +1598,9 @@ class Compiler
1122
 
1123
  // after evaluating interpolates, we might need a second pass
1124
  if ($this->shouldEvaluate) {
1125
- $buffer = $this->collapseSelectors($selectors);
1126
- $parser = $this->parserFactory(__METHOD__);
 
1127
 
1128
  if ($parser->parseSelector($buffer, $newSelectors)) {
1129
  $selectors = array_map([$this, 'evalSelector'], $newSelectors);
@@ -1176,28 +1653,89 @@ class Compiler
1176
  /**
1177
  * Collapse selectors
1178
  *
1179
- * @param array $selectors
 
 
 
1180
  *
1181
  * @return string
1182
  */
1183
- protected function collapseSelectors($selectors)
1184
  {
1185
  $parts = [];
1186
 
1187
  foreach ($selectors as $selector) {
1188
- $output = '';
 
1189
 
1190
- array_walk_recursive(
1191
- $selector,
1192
- function ($value, $key) use (&$output) {
1193
- $output .= $value;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1194
  }
1195
- );
 
 
 
 
 
 
 
 
 
 
1196
 
1197
  $parts[] = $output;
1198
  }
1199
 
1200
- return implode(', ', $parts);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1201
  }
1202
 
1203
  /**
@@ -1304,16 +1842,43 @@ class Compiler
1304
  return false;
1305
  }
1306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1307
  /**
1308
  * Compile children and return result
1309
  *
1310
- * @param array $stms
1311
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
 
1312
  *
1313
- * @return array
1314
  */
1315
- protected function compileChildren($stms, OutputBlock $out)
1316
  {
 
 
1317
  foreach ($stms as $stm) {
1318
  $ret = $this->compileChild($stm, $out);
1319
 
@@ -1321,20 +1886,38 @@ class Compiler
1321
  return $ret;
1322
  }
1323
  }
 
 
 
 
1324
  }
1325
 
1326
  /**
1327
  * Compile children and throw exception if unexpected @return
1328
  *
1329
- * @param array $stms
1330
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
 
 
1331
  *
1332
  * @throws \Exception
1333
  */
1334
- protected function compileChildrenNoReturn($stms, OutputBlock $out)
1335
  {
 
 
1336
  foreach ($stms as $stm) {
1337
- $ret = $this->compileChild($stm, $out);
 
 
 
 
 
 
 
 
 
 
1338
 
1339
  if (isset($ret)) {
1340
  $this->throwError('@return may only be used within a function');
@@ -1342,6 +1925,74 @@ class Compiler
1342
  return;
1343
  }
1344
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1345
  }
1346
 
1347
  /**
@@ -1349,31 +2000,76 @@ class Compiler
1349
  *
1350
  * @param array $queryList
1351
  *
1352
- * @return string
1353
  */
1354
  protected function compileMediaQuery($queryList)
1355
  {
1356
- $out = '@media';
1357
- $first = true;
 
 
1358
 
1359
  foreach ($queryList as $query) {
1360
  $type = null;
1361
  $parts = [];
1362
 
 
 
 
 
 
 
 
 
 
1363
  foreach ($query as $q) {
1364
  switch ($q[0]) {
1365
  case Type::T_MEDIA_TYPE:
1366
- if ($type) {
1367
- $type = $this->mergeMediaTypes(
1368
- $type,
1369
- array_map([$this, 'compileValue'], array_slice($q, 1))
1370
- );
1371
 
1372
- if (empty($type)) { // merge failed
1373
- return null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1374
  }
1375
- } else {
1376
- $type = array_map([$this, 'compileValue'], array_slice($q, 1));
1377
  }
1378
  break;
1379
 
@@ -1402,20 +2098,34 @@ class Compiler
1402
  }
1403
 
1404
  if (! empty($parts)) {
1405
- if ($first) {
1406
- $first = false;
1407
- $out .= ' ';
1408
- } else {
1409
- $out .= $this->formatter->tagSeparator;
1410
  }
1411
 
1412
- $out .= implode(' and ', $parts);
1413
  }
1414
  }
1415
 
 
 
 
 
 
 
 
 
 
1416
  return $out;
1417
  }
1418
 
 
 
 
 
 
 
 
 
1419
  protected function mergeDirectRelationships($selectors1, $selectors2)
1420
  {
1421
  if (empty($selectors1) || empty($selectors2)) {
@@ -1425,7 +2135,7 @@ class Compiler
1425
  $part1 = end($selectors1);
1426
  $part2 = end($selectors2);
1427
 
1428
- if (! $this->isImmediateRelationshipCombinator($part1[0]) || $part1 !== $part2) {
1429
  return array_merge($selectors1, $selectors2);
1430
  }
1431
 
@@ -1435,13 +2145,18 @@ class Compiler
1435
  $part1 = array_pop($selectors1);
1436
  $part2 = array_pop($selectors2);
1437
 
1438
- if ($this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
1439
- $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
 
 
 
 
 
 
1440
  break;
1441
  }
1442
 
1443
  array_unshift($merged, $part1);
1444
- array_unshift($merged, [array_pop($selectors1)[0] . array_pop($selectors2)[0]]);
1445
  } while (! empty($selectors1) && ! empty($selectors2));
1446
 
1447
  return $merged;
@@ -1465,23 +2180,19 @@ class Compiler
1465
  return $type1;
1466
  }
1467
 
1468
- $m1 = '';
1469
- $t1 = '';
1470
-
1471
  if (count($type1) > 1) {
1472
- $m1= strtolower($type1[0]);
1473
- $t1= strtolower($type1[1]);
1474
  } else {
 
1475
  $t1 = strtolower($type1[0]);
1476
  }
1477
 
1478
- $m2 = '';
1479
- $t2 = '';
1480
-
1481
  if (count($type2) > 1) {
1482
  $m2 = strtolower($type2[0]);
1483
  $t2 = strtolower($type2[1]);
1484
  } else {
 
1485
  $t2 = strtolower($type2[0]);
1486
  }
1487
 
@@ -1516,13 +2227,13 @@ class Compiler
1516
  /**
1517
  * Compile import; returns true if the value was something that could be imported
1518
  *
1519
- * @param array $rawPath
1520
- * @param array $out
1521
- * @param boolean $once
1522
  *
1523
  * @return boolean
1524
  */
1525
- protected function compileImport($rawPath, $out, $once = false)
1526
  {
1527
  if ($rawPath[0] === Type::T_STRING) {
1528
  $path = $this->compileStringContent($rawPath);
@@ -1536,6 +2247,8 @@ class Compiler
1536
  return true;
1537
  }
1538
 
 
 
1539
  return false;
1540
  }
1541
 
@@ -1547,57 +2260,155 @@ class Compiler
1547
 
1548
  foreach ($rawPath[2] as $path) {
1549
  if ($path[0] !== Type::T_STRING) {
 
 
1550
  return false;
1551
  }
1552
  }
1553
 
1554
  foreach ($rawPath[2] as $path) {
1555
- $this->compileImport($path, $out);
1556
  }
1557
 
1558
  return true;
1559
  }
1560
 
 
 
1561
  return false;
1562
  }
1563
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1564
  /**
1565
  * Compile child; returns a value to halt execution
1566
  *
1567
- * @param array $child
1568
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
1569
  *
1570
  * @return array
1571
  */
1572
  protected function compileChild($child, OutputBlock $out)
1573
  {
1574
- $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
1575
- $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
1576
- $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1577
 
1578
  switch ($child[0]) {
1579
  case Type::T_SCSSPHP_IMPORT_ONCE:
1580
- list(, $rawPath) = $child;
1581
-
1582
- $rawPath = $this->reduce($rawPath);
1583
 
1584
- if (! $this->compileImport($rawPath, $out, true)) {
1585
- $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
1586
- }
1587
  break;
1588
 
1589
  case Type::T_IMPORT:
1590
- list(, $rawPath) = $child;
1591
-
1592
- $rawPath = $this->reduce($rawPath);
1593
 
1594
- if (! $this->compileImport($rawPath, $out)) {
1595
- $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
1596
- }
1597
  break;
1598
 
1599
  case Type::T_DIRECTIVE:
1600
- $this->compileDirective($child[1]);
1601
  break;
1602
 
1603
  case Type::T_AT_ROOT:
@@ -1615,8 +2426,7 @@ class Compiler
1615
  case Type::T_CHARSET:
1616
  if (! $this->charsetSeen) {
1617
  $this->charsetSeen = true;
1618
-
1619
- $out->lines[] = '@charset ' . $this->compileValue($child[1]) . ';';
1620
  }
1621
  break;
1622
 
@@ -1624,35 +2434,98 @@ class Compiler
1624
  list(, $name, $value) = $child;
1625
 
1626
  if ($name[0] === Type::T_VARIABLE) {
1627
- $flags = isset($child[3]) ? $child[3] : [];
1628
  $isDefault = in_array('!default', $flags);
1629
- $isGlobal = in_array('!global', $flags);
1630
 
1631
  if ($isGlobal) {
1632
- $this->set($name[1], $this->reduce($value), false, $this->rootEnv);
1633
  break;
1634
  }
1635
 
1636
  $shouldSet = $isDefault &&
1637
- (($result = $this->get($name[1], false)) === null
1638
- || $result === static::$null);
1639
 
1640
  if (! $isDefault || $shouldSet) {
1641
- $this->set($name[1], $this->reduce($value));
1642
  }
1643
  break;
1644
  }
1645
 
1646
  $compiledName = $this->compileValue($name);
1647
 
1648
- // handle shorthand syntax: size / line-height
1649
- if ($compiledName === 'font') {
1650
- if ($value[0] === Type::T_EXPRESSION && $value[1] === '/') {
1651
- $value = $this->expToString($value);
1652
- } elseif ($value[0] === Type::T_LIST) {
1653
- foreach ($value[2] as &$item) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1654
  if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
1655
- $item = $this->expToString($item);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1656
  }
1657
  }
1658
  }
@@ -1670,10 +2543,11 @@ class Compiler
1670
 
1671
  $compiledValue = $this->compileValue($value);
1672
 
1673
- $out->lines[] = $this->formatter->property(
1674
  $compiledName,
1675
  $compiledValue
1676
  );
 
1677
  break;
1678
 
1679
  case Type::T_COMMENT:
@@ -1682,27 +2556,32 @@ class Compiler
1682
  break;
1683
  }
1684
 
1685
- $out->lines[] = $child[1];
 
1686
  break;
1687
 
1688
  case Type::T_MIXIN:
1689
  case Type::T_FUNCTION:
1690
  list(, $block) = $child;
1691
-
1692
- $this->set(static::$namespaces[$block->type] . $block->name, $block);
 
1693
  break;
1694
 
1695
  case Type::T_EXTEND:
1696
- list(, $selectors) = $child;
1697
-
1698
- foreach ($selectors as $sel) {
1699
  $results = $this->evalSelectors([$sel]);
1700
 
1701
  foreach ($results as $result) {
1702
  // only use the first one
1703
  $result = current($result);
 
1704
 
1705
- $this->pushExtends($result, $out->selectors, $child);
 
 
 
 
1706
  }
1707
  }
1708
  break;
@@ -1729,6 +2608,8 @@ class Compiler
1729
  $list = $this->coerceList($this->reduce($each->list));
1730
 
1731
  $this->pushEnv();
 
 
1732
 
1733
  foreach ($list[2] as $item) {
1734
  if (count($each->vars) === 1) {
@@ -1745,7 +2626,14 @@ class Compiler
1745
 
1746
  if ($ret) {
1747
  if ($ret[0] !== Type::T_CONTROL) {
 
 
1748
  $this->popEnv();
 
 
 
 
 
1749
 
1750
  return $ret;
1751
  }
@@ -1755,8 +2643,15 @@ class Compiler
1755
  }
1756
  }
1757
  }
1758
-
 
1759
  $this->popEnv();
 
 
 
 
 
 
1760
  break;
1761
 
1762
  case Type::T_WHILE:
@@ -1829,31 +2724,12 @@ class Compiler
1829
  return $this->reduce($child[1], true);
1830
 
1831
  case Type::T_NESTED_PROPERTY:
1832
- list(, $prop) = $child;
1833
-
1834
- $prefixed = [];
1835
- $prefix = $this->compileValue($prop->prefix) . '-';
1836
-
1837
- foreach ($prop->children as $child) {
1838
- switch ($child[0]) {
1839
- case Type::T_ASSIGN:
1840
- array_unshift($child[1][2], $prefix);
1841
- break;
1842
-
1843
- case Type::T_NESTED_PROPERTY:
1844
- array_unshift($child[1]->prefix[2], $prefix);
1845
- break;
1846
- }
1847
-
1848
- $prefixed[] = $child;
1849
- }
1850
-
1851
- $this->compileChildrenNoReturn($prefixed, $out);
1852
  break;
1853
 
1854
  case Type::T_INCLUDE:
1855
  // including a mixin
1856
- list(, $name, $argValues, $content) = $child;
1857
 
1858
  $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
1859
 
@@ -1871,19 +2747,58 @@ class Compiler
1871
  $storeEnv = $this->storeEnv;
1872
  $this->storeEnv = $this->env;
1873
 
1874
- if (isset($content)) {
1875
- $content->scope = $callingScope;
 
1876
 
1877
- $this->setRaw(static::$namespaces['special'] . 'content', $content, $this->env);
1878
- }
 
 
1879
 
1880
- if (isset($mixin->args)) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1881
  $this->applyArguments($mixin->args, $argValues);
1882
  }
1883
 
1884
  $this->env->marker = 'mixin';
1885
 
1886
- $this->compileChildrenNoReturn($mixin->children, $out);
 
 
 
 
 
 
1887
 
1888
  $this->storeEnv = $storeEnv;
1889
 
@@ -1891,19 +2806,35 @@ class Compiler
1891
  break;
1892
 
1893
  case Type::T_MIXIN_CONTENT:
1894
- $content = $this->get(static::$namespaces['special'] . 'content', false, $this->getStoreEnv())
1895
- ?: $this->get(static::$namespaces['special'] . 'content', false, $this->env);
 
 
1896
 
1897
  if (! $content) {
1898
  $content = new \stdClass();
1899
- $content->scope = new \stdClass();
1900
- $content->children = $this->storeEnv->parent->block->children;
1901
  break;
1902
  }
1903
 
1904
  $storeEnv = $this->storeEnv;
 
 
 
 
 
 
 
 
 
1905
  $this->storeEnv = $content->scope;
1906
 
 
 
 
 
 
1907
  $this->compileChildrenNoReturn($content->children, $out);
1908
 
1909
  $this->storeEnv = $storeEnv;
@@ -1912,25 +2843,31 @@ class Compiler
1912
  case Type::T_DEBUG:
1913
  list(, $value) = $child;
1914
 
1915
- $line = $this->sourceLine;
 
1916
  $value = $this->compileValue($this->reduce($value, true));
1917
- fwrite($this->stderr, "Line $line DEBUG: $value\n");
 
1918
  break;
1919
 
1920
  case Type::T_WARN:
1921
  list(, $value) = $child;
1922
 
1923
- $line = $this->sourceLine;
 
1924
  $value = $this->compileValue($this->reduce($value, true));
1925
- fwrite($this->stderr, "Line $line WARN: $value\n");
 
1926
  break;
1927
 
1928
  case Type::T_ERROR:
1929
  list(, $value) = $child;
1930
 
1931
- $line = $this->sourceLine;
 
1932
  $value = $this->compileValue($this->reduce($value, true));
1933
- $this->throwError("Line $line ERROR: $value\n");
 
1934
  break;
1935
 
1936
  case Type::T_CONTROL:
@@ -1975,7 +2912,7 @@ class Compiler
1975
  *
1976
  * @param array $value
1977
  *
1978
- * @return array
1979
  */
1980
  protected function isTruthy($value)
1981
  {
@@ -2006,7 +2943,7 @@ class Compiler
2006
  switch ($value[0]) {
2007
  case Type::T_EXPRESSION:
2008
  if ($value[1] === '/') {
2009
- return $this->shouldEval($value[2], $value[3]);
2010
  }
2011
 
2012
  // fall-thru
@@ -2024,13 +2961,15 @@ class Compiler
2024
  * @param array $value
2025
  * @param boolean $inExp
2026
  *
2027
- * @return array|\Leafo\ScssPhp\Node\Number
2028
  */
2029
  protected function reduce($value, $inExp = false)
2030
  {
2031
- list($type) = $value;
 
 
2032
 
2033
- switch ($type) {
2034
  case Type::T_EXPRESSION:
2035
  list(, $op, $left, $right, $inParens) = $value;
2036
 
@@ -2044,16 +2983,15 @@ class Compiler
2044
  }
2045
 
2046
  // special case: looks like css shorthand
2047
- if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2])
2048
- && (($right[0] !== Type::T_NUMBER && $right[2] != '')
2049
- || ($right[0] === Type::T_NUMBER && ! $right->unitless()))
2050
  ) {
2051
  return $this->expToString($value);
2052
  }
2053
 
2054
- $left = $this->coerceForExpression($left);
2055
  $right = $this->coerceForExpression($right);
2056
-
2057
  $ltype = $left[0];
2058
  $rtype = $right[0];
2059
 
@@ -2163,9 +3101,7 @@ class Compiler
2163
  return [Type::T_STRING, '', [$op, $exp]];
2164
 
2165
  case Type::T_VARIABLE:
2166
- list(, $name) = $value;
2167
-
2168
- return $this->reduce($this->get($name));
2169
 
2170
  case Type::T_LIST:
2171
  foreach ($value[2] as &$item) {
@@ -2197,12 +3133,20 @@ class Compiler
2197
  case Type::T_INTERPOLATE:
2198
  $value[1] = $this->reduce($value[1]);
2199
 
 
 
 
 
2200
  return $value;
2201
 
2202
  case Type::T_FUNCTION_CALL:
2203
- list(, $name, $argValues) = $value;
 
 
 
 
2204
 
2205
- return $this->fncall($name, $argValues);
2206
 
2207
  default:
2208
  return $value;
@@ -2217,7 +3161,7 @@ class Compiler
2217
  *
2218
  * @return array|null
2219
  */
2220
- private function fncall($name, $argValues)
2221
  {
2222
  // SCSS @function
2223
  if ($this->callScssFunction($name, $argValues, $returnValue)) {
@@ -2263,9 +3207,8 @@ class Compiler
2263
  public function normalizeValue($value)
2264
  {
2265
  $value = $this->coerceForExpression($this->reduce($value));
2266
- list($type) = $value;
2267
 
2268
- switch ($type) {
2269
  case Type::T_LIST:
2270
  $value = $this->extractInterpolation($value);
2271
 
@@ -2277,10 +3220,14 @@ class Compiler
2277
  $value[2][$key] = $this->normalizeValue($item);
2278
  }
2279
 
 
 
 
 
2280
  return $value;
2281
 
2282
  case Type::T_STRING:
2283
- return [$type, '"', [$this->compileStringContent($value)]];
2284
 
2285
  case Type::T_NUMBER:
2286
  return $value->normalize();
@@ -2299,7 +3246,7 @@ class Compiler
2299
  * @param array $left
2300
  * @param array $right
2301
  *
2302
- * @return \Leafo\ScssPhp\Node\Number
2303
  */
2304
  protected function opAddNumberNumber($left, $right)
2305
  {
@@ -2312,7 +3259,7 @@ class Compiler
2312
  * @param array $left
2313
  * @param array $right
2314
  *
2315
- * @return \Leafo\ScssPhp\Node\Number
2316
  */
2317
  protected function opMulNumberNumber($left, $right)
2318
  {
@@ -2325,7 +3272,7 @@ class Compiler
2325
  * @param array $left
2326
  * @param array $right
2327
  *
2328
- * @return \Leafo\ScssPhp\Node\Number
2329
  */
2330
  protected function opSubNumberNumber($left, $right)
2331
  {
@@ -2338,7 +3285,7 @@ class Compiler
2338
  * @param array $left
2339
  * @param array $right
2340
  *
2341
- * @return array|\Leafo\ScssPhp\Node\Number
2342
  */
2343
  protected function opDivNumberNumber($left, $right)
2344
  {
@@ -2355,7 +3302,7 @@ class Compiler
2355
  * @param array $left
2356
  * @param array $right
2357
  *
2358
- * @return \Leafo\ScssPhp\Node\Number
2359
  */
2360
  protected function opModNumberNumber($left, $right)
2361
  {
@@ -2368,7 +3315,7 @@ class Compiler
2368
  * @param array $left
2369
  * @param array $right
2370
  *
2371
- * @return array
2372
  */
2373
  protected function opAdd($left, $right)
2374
  {
@@ -2391,6 +3338,8 @@ class Compiler
2391
 
2392
  return $strRight;
2393
  }
 
 
2394
  }
2395
 
2396
  /**
@@ -2400,15 +3349,21 @@ class Compiler
2400
  * @param array $right
2401
  * @param boolean $shouldEval
2402
  *
2403
- * @return array
2404
  */
2405
  protected function opAnd($left, $right, $shouldEval)
2406
  {
 
 
 
 
2407
  if (! $shouldEval) {
2408
- return;
 
 
2409
  }
2410
 
2411
- if ($left !== static::$false and $left !== static::$null) {
2412
  return $this->reduce($right, true);
2413
  }
2414
 
@@ -2422,15 +3377,21 @@ class Compiler
2422
  * @param array $right
2423
  * @param boolean $shouldEval
2424
  *
2425
- * @return array
2426
  */
2427
  protected function opOr($left, $right, $shouldEval)
2428
  {
 
 
 
 
2429
  if (! $shouldEval) {
2430
- return;
 
 
2431
  }
2432
 
2433
- if ($left !== static::$false and $left !== static::$null) {
2434
  return $left;
2435
  }
2436
 
@@ -2641,7 +3602,7 @@ class Compiler
2641
  * @param array $left
2642
  * @param array $right
2643
  *
2644
- * @return \Leafo\ScssPhp\Node\Number
2645
  */
2646
  protected function opCmpNumberNumber($left, $right)
2647
  {
@@ -2679,15 +3640,13 @@ class Compiler
2679
  *
2680
  * @param array $value
2681
  *
2682
- * @return string
2683
  */
2684
  public function compileValue($value)
2685
  {
2686
  $value = $this->reduce($value);
2687
 
2688
- list($type) = $value;
2689
-
2690
- switch ($type) {
2691
  case Type::T_KEYWORD:
2692
  return $value[1];
2693
 
@@ -2698,14 +3657,38 @@ class Compiler
2698
  // [4] - optional alpha component
2699
  list(, $r, $g, $b) = $value;
2700
 
2701
- $r = round($r);
2702
- $g = round($g);
2703
- $b = round($b);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2704
 
2705
- if (count($value) === 5 && $value[4] !== 1) { // rgba
2706
- $a = new Node\Number($value[4], '');
2707
 
2708
- return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
 
2709
  }
2710
 
2711
  $h = sprintf('#%02x%02x%02x', $r, $g, $b);
@@ -2736,9 +3719,28 @@ class Compiler
2736
  }
2737
 
2738
  list(, $delim, $items) = $value;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2739
 
 
2740
  if ($delim !== ' ') {
2741
- $delim .= ' ';
2742
  }
2743
 
2744
  $filtered = [];
@@ -2748,14 +3750,18 @@ class Compiler
2748
  continue;
2749
  }
2750
 
2751
- $filtered[] = $this->compileValue($item);
 
 
 
 
2752
  }
2753
 
2754
- return implode("$delim", $filtered);
2755
 
2756
  case Type::T_MAP:
2757
- $keys = $value[1];
2758
- $values = $value[2];
2759
  $filtered = [];
2760
 
2761
  for ($i = 0, $s = count($keys); $i < $s; $i++) {
@@ -2773,20 +3779,29 @@ class Compiler
2773
  list(, $interpolate, $left, $right) = $value;
2774
  list(,, $whiteLeft, $whiteRight) = $interpolate;
2775
 
 
 
 
 
 
 
2776
  $left = count($left[2]) > 0 ?
2777
- $this->compileValue($left) . $whiteLeft : '';
 
 
 
 
 
 
2778
 
2779
  $right = count($right[2]) > 0 ?
2780
- $whiteRight . $this->compileValue($right) : '';
2781
 
2782
  return $left . $this->compileValue($interpolate) . $right;
2783
 
2784
  case Type::T_INTERPOLATE:
2785
- // raw parse node
2786
- list(, $exp) = $value;
2787
-
2788
  // strip quotes if it's a string
2789
- $reduced = $this->reduce($exp);
2790
 
2791
  switch ($reduced[0]) {
2792
  case Type::T_LIST:
@@ -2810,6 +3825,7 @@ class Compiler
2810
  }
2811
 
2812
  $temp = $this->compileValue([Type::T_KEYWORD, $item]);
 
2813
  if ($temp[0] === Type::T_STRING) {
2814
  $filtered[] = $this->compileStringContent($temp);
2815
  } elseif ($temp[0] === Type::T_KEYWORD) {
@@ -2835,8 +3851,11 @@ class Compiler
2835
  case Type::T_NULL:
2836
  return 'null';
2837
 
 
 
 
2838
  default:
2839
- $this->throwError("unknown value type: $type");
2840
  }
2841
  }
2842
 
@@ -2900,44 +3919,70 @@ class Compiler
2900
  /**
2901
  * Find the final set of selectors
2902
  *
2903
- * @param \Leafo\ScssPhp\Compiler\Environment $env
 
2904
  *
2905
  * @return array
2906
  */
2907
- protected function multiplySelectors(Environment $env)
2908
  {
2909
  $envs = $this->compactEnv($env);
2910
  $selectors = [];
2911
  $parentSelectors = [[]];
2912
 
 
 
 
 
 
 
2913
  while ($env = array_pop($envs)) {
2914
  if (empty($env->selectors)) {
2915
  continue;
2916
  }
2917
 
2918
- $selectors = [];
 
 
 
 
 
2919
 
2920
- foreach ($env->selectors as $selector) {
2921
- foreach ($parentSelectors as $parent) {
2922
- $selectors[] = $this->joinSelectors($parent, $selector);
 
 
 
 
 
 
 
 
 
 
2923
  }
2924
- }
2925
 
2926
  $parentSelectors = $selectors;
2927
  }
2928
 
 
 
2929
  return $selectors;
2930
  }
2931
 
2932
  /**
2933
  * Join selectors; looks for & to replace, or append parent before child
2934
  *
2935
- * @param array $parent
2936
- * @param array $child
2937
- *
 
 
2938
  * @return array
2939
  */
2940
- protected function joinSelectors($parent, $child)
2941
  {
2942
  $setSelf = false;
2943
  $out = [];
@@ -2946,16 +3991,35 @@ class Compiler
2946
  $newPart = [];
2947
 
2948
  foreach ($part as $p) {
2949
- if ($p === static::$selfSelector) {
 
 
 
 
 
2950
  $setSelf = true;
2951
 
2952
- foreach ($parent as $i => $parentPart) {
 
 
 
 
2953
  if ($i > 0) {
2954
  $out[] = $newPart;
2955
  $newPart = [];
2956
  }
2957
 
2958
  foreach ($parentPart as $pp) {
 
 
 
 
 
 
 
 
 
 
2959
  $newPart[] = $pp;
2960
  }
2961
  }
@@ -2973,8 +4037,8 @@ class Compiler
2973
  /**
2974
  * Multiply media
2975
  *
2976
- * @param \Leafo\ScssPhp\Compiler\Environment $env
2977
- * @param array $childQueries
2978
  *
2979
  * @return array
2980
  */
@@ -2995,7 +4059,15 @@ class Compiler
2995
  ? $env->block->queryList
2996
  : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
2997
 
2998
- if ($childQueries === null) {
 
 
 
 
 
 
 
 
2999
  $childQueries = $parentQueries;
3000
  } else {
3001
  $originalQueries = $childQueries;
@@ -3003,7 +4075,11 @@ class Compiler
3003
 
3004
  foreach ($parentQueries as $parentQuery) {
3005
  foreach ($originalQueries as $childQuery) {
3006
- $childQueries []= array_merge($parentQuery, $childQuery);
 
 
 
 
3007
  }
3008
  }
3009
  }
@@ -3014,11 +4090,11 @@ class Compiler
3014
  /**
3015
  * Convert env linked list to stack
3016
  *
3017
- * @param \Leafo\ScssPhp\Compiler\Environment $env
3018
  *
3019
  * @return array
3020
  */
3021
- private function compactEnv(Environment $env)
3022
  {
3023
  for ($envs = []; $env; $env = $env->parent) {
3024
  $envs[] = $env;
@@ -3032,9 +4108,9 @@ class Compiler
3032
  *
3033
  * @param array $envs
3034
  *
3035
- * @return \Leafo\ScssPhp\Compiler\Environment
3036
  */
3037
- private function extractEnv($envs)
3038
  {
3039
  for ($env = null; $e = array_pop($envs);) {
3040
  $e->parent = $env;
@@ -3047,9 +4123,9 @@ class Compiler
3047
  /**
3048
  * Push environment
3049
  *
3050
- * @param \Leafo\ScssPhp\Block $block
3051
  *
3052
- * @return \Leafo\ScssPhp\Compiler\Environment
3053
  */
3054
  protected function pushEnv(Block $block = null)
3055
  {
@@ -3075,7 +4151,7 @@ class Compiler
3075
  /**
3076
  * Get store environment
3077
  *
3078
- * @return \Leafo\ScssPhp\Compiler\Environment
3079
  */
3080
  protected function getStoreEnv()
3081
  {
@@ -3085,12 +4161,13 @@ class Compiler
3085
  /**
3086
  * Set variable
3087
  *
3088
- * @param string $name
3089
- * @param mixed $value
3090
- * @param boolean $shadow
3091
- * @param \Leafo\ScssPhp\Compiler\Environment $env
 
3092
  */
3093
- protected function set($name, $value, $shadow = false, Environment $env = null)
3094
  {
3095
  $name = $this->normalizeName($name);
3096
 
@@ -3099,22 +4176,24 @@ class Compiler
3099
  }
3100
 
3101
  if ($shadow) {
3102
- $this->setRaw($name, $value, $env);
3103
  } else {
3104
- $this->setExisting($name, $value, $env);
3105
  }
3106
  }
3107
 
3108
  /**
3109
  * Set existing variable
3110
  *
3111
- * @param string $name
3112
- * @param mixed $value
3113
- * @param \Leafo\ScssPhp\Compiler\Environment $env
 
3114
  */
3115
- protected function setExisting($name, $value, Environment $env)
3116
  {
3117
  $storeEnv = $env;
 
3118
 
3119
  $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
3120
 
@@ -3124,8 +4203,18 @@ class Compiler
3124
  }
3125
 
3126
  if (! $hasNamespace && isset($env->marker)) {
3127
- $env = $storeEnv;
3128
- break;
 
 
 
 
 
 
 
 
 
 
3129
  }
3130
 
3131
  if (! isset($env->parent)) {
@@ -3137,18 +4226,27 @@ class Compiler
3137
  }
3138
 
3139
  $env->store[$name] = $value;
 
 
 
 
3140
  }
3141
 
3142
  /**
3143
  * Set raw variable
3144
  *
3145
- * @param string $name
3146
- * @param mixed $value
3147
- * @param \Leafo\ScssPhp\Compiler\Environment $env
 
3148
  */
3149
- protected function setRaw($name, $value, Environment $env)
3150
  {
3151
  $env->store[$name] = $value;
 
 
 
 
3152
  }
3153
 
3154
  /**
@@ -3156,13 +4254,14 @@ class Compiler
3156
  *
3157
  * @api
3158
  *
3159
- * @param string $name
3160
- * @param boolean $shouldThrow
3161
- * @param \Leafo\ScssPhp\Compiler\Environment $env
 
3162
  *
3163
- * @return mixed
3164
  */
3165
- public function get($name, $shouldThrow = true, Environment $env = null)
3166
  {
3167
  $normalizedName = $this->normalizeName($name);
3168
  $specialContentKey = static::$namespaces['special'] . 'content';
@@ -3171,22 +4270,34 @@ class Compiler
3171
  $env = $this->getStoreEnv();
3172
  }
3173
 
3174
- $nextIsRoot = false;
3175
  $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
3176
 
 
 
3177
  for (;;) {
 
 
 
 
3178
  if (array_key_exists($normalizedName, $env->store)) {
 
 
 
 
3179
  return $env->store[$normalizedName];
3180
  }
3181
 
3182
  if (! $hasNamespace && isset($env->marker)) {
3183
- if (! $nextIsRoot && ! empty($env->store[$specialContentKey])) {
3184
  $env = $env->store[$specialContentKey]->scope;
3185
- $nextIsRoot = true;
3186
  continue;
3187
  }
3188
 
3189
- $env = $this->rootEnv;
 
 
 
 
3190
  continue;
3191
  }
3192
 
@@ -3198,23 +4309,24 @@ class Compiler
3198
  }
3199
 
3200
  if ($shouldThrow) {
3201
- $this->throwError("Undefined variable \$$name");
3202
  }
3203
 
3204
  // found nothing
 
3205
  }
3206
 
3207
  /**
3208
  * Has variable?
3209
  *
3210
- * @param string $name
3211
- * @param \Leafo\ScssPhp\Compiler\Environment $env
3212
  *
3213
  * @return boolean
3214
  */
3215
  protected function has($name, Environment $env = null)
3216
  {
3217
- return $this->get($name, false, $env) !== null;
3218
  }
3219
 
3220
  /**
@@ -3288,7 +4400,7 @@ class Compiler
3288
  */
3289
  public function addParsedFile($path)
3290
  {
3291
- if (isset($path) && file_exists($path)) {
3292
  $this->parsedFiles[realpath($path)] = filemtime($path);
3293
  }
3294
  }
@@ -3310,7 +4422,7 @@ class Compiler
3310
  *
3311
  * @api
3312
  *
3313
- * @param string $path
3314
  */
3315
  public function addImportPath($path)
3316
  {
@@ -3432,10 +4544,10 @@ class Compiler
3432
  /**
3433
  * Import file
3434
  *
3435
- * @param string $path
3436
- * @param array $out
3437
  */
3438
- protected function importFile($path, $out)
3439
  {
3440
  // see if tree is cached
3441
  $realPath = realpath($path);
@@ -3453,6 +4565,7 @@ class Compiler
3453
  }
3454
 
3455
  $pi = pathinfo($path);
 
3456
  array_unshift($this->importPaths, $pi['dirname']);
3457
  $this->compileChildrenNoReturn($tree->children, $out);
3458
  array_shift($this->importPaths);
@@ -3472,9 +4585,9 @@ class Compiler
3472
  $urls = [];
3473
 
3474
  // for "normal" scss imports (ignore vanilla css and external requests)
3475
- if (! preg_match('/\.css$|^https?:\/\//', $url)) {
3476
  // try both normal and the _partial filename
3477
- $urls = [$url, preg_replace('/[^\/]+$/', '_\0', $url)];
3478
  }
3479
 
3480
  $hasExtension = preg_match('/[.]s?css$/', $url);
@@ -3483,12 +4596,15 @@ class Compiler
3483
  if (is_string($dir)) {
3484
  // check urls for normal import paths
3485
  foreach ($urls as $full) {
3486
- $full = $dir
3487
- . (! empty($dir) && substr($dir, -1) !== '/' ? '/' : '')
3488
- . $full;
3489
-
3490
- if ($this->fileExists($file = $full . '.scss') ||
3491
- ($hasExtension && $this->fileExists($file = $full))
 
 
 
3492
  ) {
3493
  return $file;
3494
  }
@@ -3497,7 +4613,7 @@ class Compiler
3497
  // check custom callback for import path
3498
  $file = call_user_func($dir, $url);
3499
 
3500
- if ($file !== null) {
3501
  return $file;
3502
  }
3503
  }
@@ -3525,11 +4641,13 @@ class Compiler
3525
  *
3526
  * @param boolean $ignoreErrors
3527
  *
3528
- * @return \Leafo\ScssPhp\Compiler
3529
  */
3530
  public function setIgnoreErrors($ignoreErrors)
3531
  {
3532
  $this->ignoreErrors = $ignoreErrors;
 
 
3533
  }
3534
 
3535
  /**
@@ -3539,7 +4657,7 @@ class Compiler
3539
  *
3540
  * @param string $msg Message with optional sprintf()-style vararg parameters
3541
  *
3542
- * @throws \Leafo\ScssPhp\Exception\CompilerException
3543
  */
3544
  public function throwError($msg)
3545
  {
@@ -3547,16 +4665,62 @@ class Compiler
3547
  return;
3548
  }
3549
 
 
 
 
 
 
 
 
3550
  if (func_num_args() > 1) {
3551
  $msg = call_user_func_array('sprintf', func_get_args());
3552
  }
3553
 
3554
- $line = $this->sourceLine;
3555
- $msg = "$msg: line: $line";
 
 
 
 
 
3556
 
3557
  throw new CompilerException($msg);
3558
  }
3559
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3560
  /**
3561
  * Handle import loop
3562
  *
@@ -3567,6 +4731,10 @@ class Compiler
3567
  protected function handleImportLoop($name)
3568
  {
3569
  for ($env = $this->env; $env; $env = $env->parent) {
 
 
 
 
3570
  $file = $this->sourceNames[$env->block->sourceIndex];
3571
 
3572
  if (realpath($file) === $name) {
@@ -3576,18 +4744,6 @@ class Compiler
3576
  }
3577
  }
3578
 
3579
- /**
3580
- * Does file exist?
3581
- *
3582
- * @param string $name
3583
- *
3584
- * @return boolean
3585
- */
3586
- protected function fileExists($name)
3587
- {
3588
- return file_exists($name) && is_file($name);
3589
- }
3590
-
3591
  /**
3592
  * Call SCSS @function
3593
  *
@@ -3621,8 +4777,13 @@ class Compiler
3621
  $tmp->children = [];
3622
 
3623
  $this->env->marker = 'function';
 
 
 
 
 
3624
 
3625
- $ret = $this->compileChildren($func->children, $tmp);
3626
 
3627
  $this->storeEnv = $storeEnv;
3628
 
@@ -3657,11 +4818,17 @@ class Compiler
3657
  return false;
3658
  }
3659
 
3660
- list($sorted, $kwargs) = $this->sortArgs($prototype, $args);
3661
 
3662
  if ($name !== 'if' && $name !== 'call') {
 
 
 
 
 
 
3663
  foreach ($sorted as &$val) {
3664
- $val = $this->reduce($val, true);
3665
  }
3666
  }
3667
 
@@ -3699,62 +4866,170 @@ class Compiler
3699
  /**
3700
  * Sorts keyword arguments
3701
  *
3702
- * @param array $prototype
3703
- * @param array $args
 
3704
  *
3705
  * @return array
3706
  */
3707
- protected function sortArgs($prototype, $args)
3708
  {
3709
- $keyArgs = [];
3710
- $posArgs = [];
3711
 
3712
- // separate positional and keyword arguments
3713
- foreach ($args as $arg) {
3714
- list($key, $value) = $arg;
 
 
 
 
3715
 
3716
- $key = $key[1];
3717
 
3718
- if (empty($key)) {
3719
- $posArgs[] = $value;
3720
- } else {
3721
- $keyArgs[$key] = $value;
 
3722
  }
3723
- }
3724
 
3725
- if (! isset($prototype)) {
3726
  return [$posArgs, $keyArgs];
3727
  }
3728
 
3729
- // copy positional args
3730
- $finalArgs = array_pad($posArgs, count($prototype), null);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3731
 
3732
- // overwrite positional args with keyword args
3733
- foreach ($prototype as $i => $names) {
3734
- foreach ((array) $names as $name) {
3735
- if (isset($keyArgs[$name])) {
3736
- $finalArgs[$i] = $keyArgs[$name];
3737
  }
 
 
 
3738
  }
3739
  }
3740
 
 
 
 
 
3741
  return [$finalArgs, $keyArgs];
3742
  }
3743
 
3744
  /**
3745
  * Apply argument values per definition
3746
  *
3747
- * @param array $argDef
3748
- * @param array $argValues
 
 
 
 
 
3749
  *
3750
  * @throws \Exception
3751
  */
3752
- protected function applyArguments($argDef, $argValues)
3753
  {
3754
- $storeEnv = $this->getStoreEnv();
 
 
 
3755
 
3756
- $env = new Environment;
3757
- $env->store = $storeEnv->store;
 
 
 
 
3758
 
3759
  $hasVariable = false;
3760
  $args = [];
@@ -3766,37 +5041,64 @@ class Compiler
3766
  $hasVariable |= $isVariable;
3767
  }
3768
 
3769
- $keywordArgs = [];
 
3770
  $deferredKeywordArgs = [];
3771
- $remaining = [];
 
3772
 
3773
  // assign the keyword args
3774
  foreach ((array) $argValues as $arg) {
3775
  if (! empty($arg[0])) {
3776
- if (! isset($args[$arg[0][1]])) {
 
 
 
 
 
 
 
 
 
 
 
3777
  if ($hasVariable) {
3778
- $deferredKeywordArgs[$arg[0][1]] = $arg[1];
3779
  } else {
3780
  $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
3781
  break;
3782
  }
3783
- } elseif ($args[$arg[0][1]][0] < count($remaining)) {
3784
  $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
3785
  break;
3786
  } else {
3787
- $keywordArgs[$arg[0][1]] = $arg[1];
3788
  }
3789
- } elseif (count($keywordArgs)) {
3790
- $this->throwError('Positional arguments must come before keyword arguments.');
3791
- break;
3792
  } elseif ($arg[2] === true) {
3793
  $val = $this->reduce($arg[1], true);
3794
 
3795
  if ($val[0] === Type::T_LIST) {
3796
  foreach ($val[2] as $name => $item) {
3797
  if (! is_numeric($name)) {
3798
- $keywordArgs[$name] = $item;
 
 
 
 
 
 
 
 
 
 
 
 
 
3799
  } else {
 
 
 
 
3800
  $remaining[] = $item;
3801
  }
3802
  }
@@ -3806,14 +5108,34 @@ class Compiler
3806
  $item = $val[2][$i];
3807
 
3808
  if (! is_numeric($name)) {
3809
- $keywordArgs[$name] = $item;
3810
- } else {
3811
- $remaining[] = $item;
3812
- }
3813
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3814
  } else {
3815
  $remaining[] = $val;
3816
  }
 
 
 
3817
  } else {
3818
  $remaining[] = $arg[1];
3819
  }
@@ -3823,7 +5145,7 @@ class Compiler
3823
  list($i, $name, $default, $isVariable) = $arg;
3824
 
3825
  if ($isVariable) {
3826
- $val = [Type::T_LIST, ',', [], $isVariable];
3827
 
3828
  for ($count = count($remaining); $i < $count; $i++) {
3829
  $val[2][] = $remaining[$i];
@@ -3843,10 +5165,16 @@ class Compiler
3843
  break;
3844
  }
3845
 
3846
- $this->set($name, $this->reduce($val, true), true, $env);
 
 
 
 
3847
  }
3848
 
3849
- $storeEnv->store = $env->store;
 
 
3850
 
3851
  foreach ($args as $arg) {
3852
  list($i, $name, $default, $isVariable) = $arg;
@@ -3855,8 +5183,14 @@ class Compiler
3855
  continue;
3856
  }
3857
 
3858
- $this->set($name, $this->reduce($default, true), true);
 
 
 
 
3859
  }
 
 
3860
  }
3861
 
3862
  /**
@@ -3864,9 +5198,9 @@ class Compiler
3864
  *
3865
  * @param mixed $value
3866
  *
3867
- * @return array|\Leafo\ScssPhp\Node\Number
3868
  */
3869
- private function coerceValue($value)
3870
  {
3871
  if (is_array($value) || $value instanceof \ArrayAccess) {
3872
  return $value;
@@ -3876,7 +5210,7 @@ class Compiler
3876
  return $this->toBool($value);
3877
  }
3878
 
3879
- if ($value === null) {
3880
  return static::$null;
3881
  }
3882
 
@@ -3888,30 +5222,14 @@ class Compiler
3888
  return static::$emptyString;
3889
  }
3890
 
3891
- if (preg_match('/^(#([0-9a-f]{6})|#([0-9a-f]{3}))$/i', $value, $m)) {
3892
- $color = [Type::T_COLOR];
3893
-
3894
- if (isset($m[3])) {
3895
- $num = hexdec($m[3]);
3896
-
3897
- foreach ([3, 2, 1] as $i) {
3898
- $t = $num & 0xf;
3899
- $color[$i] = $t << 4 | $t;
3900
- $num >>= 4;
3901
- }
3902
- } else {
3903
- $num = hexdec($m[2]);
3904
-
3905
- foreach ([3, 2, 1] as $i) {
3906
- $color[$i] = $num & 0xff;
3907
- $num >>= 8;
3908
- }
3909
- }
3910
 
 
3911
  return $color;
3912
  }
3913
 
3914
- return [Type::T_KEYWORD, $value];
3915
  }
3916
 
3917
  /**
@@ -3927,7 +5245,9 @@ class Compiler
3927
  return $item;
3928
  }
3929
 
3930
- if ($item === static::$emptyList) {
 
 
3931
  return static::$emptyMap;
3932
  }
3933
 
@@ -3957,10 +5277,21 @@ class Compiler
3957
  $key = $keys[$i];
3958
  $value = $values[$i];
3959
 
 
 
 
 
 
 
 
 
 
 
 
3960
  $list[] = [
3961
  Type::T_LIST,
3962
  '',
3963
- [[Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))], $value]
3964
  ];
3965
  }
3966
 
@@ -3993,21 +5324,106 @@ class Compiler
3993
  *
3994
  * @return array|null
3995
  */
3996
- protected function coerceColor($value)
3997
  {
3998
  switch ($value[0]) {
3999
  case Type::T_COLOR:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4000
  return $value;
4001
 
 
 
 
 
 
 
 
 
 
 
 
 
4002
  case Type::T_KEYWORD:
 
 
 
 
4003
  $name = strtolower($value[1]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4004
 
4005
- if (isset(Colors::$cssColors[$name])) {
4006
- $rgba = explode(',', Colors::$cssColors[$name]);
 
 
 
 
 
 
 
4007
 
 
 
 
 
 
4008
  return isset($rgba[3])
4009
- ? [Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3]]
4010
- : [Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]];
4011
  }
4012
 
4013
  return null;
@@ -4016,6 +5432,88 @@ class Compiler
4016
  return null;
4017
  }
4018
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4019
  /**
4020
  * Coerce value to string
4021
  *
@@ -4068,7 +5566,7 @@ class Compiler
4068
  $value = $this->coerceMap($value);
4069
 
4070
  if ($value[0] !== Type::T_MAP) {
4071
- $this->throwError('expecting map');
4072
  }
4073
 
4074
  return $value;
@@ -4088,7 +5586,7 @@ class Compiler
4088
  public function assertList($value)
4089
  {
4090
  if ($value[0] !== Type::T_LIST) {
4091
- $this->throwError('expecting list');
4092
  }
4093
 
4094
  return $value;
@@ -4111,7 +5609,7 @@ class Compiler
4111
  return $color;
4112
  }
4113
 
4114
- $this->throwError('expecting color');
4115
  }
4116
 
4117
  /**
@@ -4128,7 +5626,7 @@ class Compiler
4128
  public function assertNumber($value)
4129
  {
4130
  if ($value[0] !== Type::T_NUMBER) {
4131
- $this->throwError('expecting number');
4132
  }
4133
 
4134
  return $value[1];
@@ -4205,7 +5703,7 @@ class Compiler
4205
  *
4206
  * @return float
4207
  */
4208
- private function hueToRGB($m1, $m2, $h)
4209
  {
4210
  if ($h < 0) {
4211
  $h += 1;
@@ -4263,28 +5761,27 @@ class Compiler
4263
 
4264
  // Built in functions
4265
 
4266
- //protected static $libCall = ['name', 'args...'];
4267
  protected function libCall($args, $kwargs)
4268
  {
4269
  $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
 
4270
 
4271
- $args = array_map(
4272
- function ($a) {
4273
- return [null, $a, false];
4274
- },
4275
- $args
4276
- );
4277
-
4278
- if (count($kwargs)) {
4279
- foreach ($kwargs as $key => $value) {
4280
- $args[] = [[Type::T_VARIABLE, $key], $value, false];
4281
  }
 
 
4282
  }
4283
 
4284
- return $this->reduce([Type::T_FUNCTION_CALL, $name, $args]);
4285
  }
4286
 
4287
- protected static $libIf = ['condition', 'if-true', 'if-false'];
4288
  protected function libIf($args)
4289
  {
4290
  list($cond, $t, $f) = $args;
@@ -4328,30 +5825,68 @@ class Compiler
4328
  return false === $key ? static::$null : $key + 1;
4329
  }
4330
 
4331
- protected static $libRgb = ['red', 'green', 'blue'];
4332
- protected function libRgb($args)
 
 
 
 
 
4333
  {
4334
- list($r, $g, $b) = $args;
 
 
 
 
 
4335
 
4336
- return [Type::T_COLOR, $r[1], $g[1], $b[1]];
4337
- }
4338
 
4339
- protected static $libRgba = [
4340
- ['red', 'color'],
4341
- 'green', 'blue', 'alpha'];
4342
- protected function libRgba($args)
4343
- {
4344
- if ($color = $this->coerceColor($args[0])) {
4345
- $num = isset($args[3]) ? $args[3] : $args[1];
4346
- $alpha = $this->assertNumber($num);
4347
- $color[4] = $alpha;
4348
 
4349
- return $color;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4350
  }
4351
 
4352
- list($r, $g, $b, $a) = $args;
 
4353
 
4354
- return [Type::T_COLOR, $r[1], $g[1], $b[1], $a[1]];
 
 
 
 
 
 
 
 
4355
  }
4356
 
4357
  // helper function for adjust_color, change_color, and scale_color
@@ -4359,21 +5894,25 @@ class Compiler
4359
  {
4360
  $color = $this->assertColor($args[0]);
4361
 
4362
- foreach ([1, 2, 3, 7] as $i) {
4363
- if (isset($args[$i])) {
4364
- $val = $this->assertNumber($args[$i]);
4365
- $ii = $i === 7 ? 4 : $i; // alpha
4366
- $color[$ii] = call_user_func($fn, isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
 
 
 
 
4367
  }
4368
  }
4369
 
4370
- if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
4371
  $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4372
 
4373
- foreach ([4, 5, 6] as $i) {
4374
- if (isset($args[$i])) {
4375
- $val = $this->assertNumber($args[$i]);
4376
- $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
4377
  }
4378
  }
4379
 
@@ -4390,8 +5929,8 @@ class Compiler
4390
  }
4391
 
4392
  protected static $libAdjustColor = [
4393
- 'color', 'red', 'green', 'blue',
4394
- 'hue', 'saturation', 'lightness', 'alpha'
4395
  ];
4396
  protected function libAdjustColor($args)
4397
  {
@@ -4401,8 +5940,8 @@ class Compiler
4401
  }
4402
 
4403
  protected static $libChangeColor = [
4404
- 'color', 'red', 'green', 'blue',
4405
- 'hue', 'saturation', 'lightness', 'alpha'
4406
  ];
4407
  protected function libChangeColor($args)
4408
  {
@@ -4412,8 +5951,8 @@ class Compiler
4412
  }
4413
 
4414
  protected static $libScaleColor = [
4415
- 'color', 'red', 'green', 'blue',
4416
- 'hue', 'saturation', 'lightness', 'alpha'
4417
  ];
4418
  protected function libScaleColor($args)
4419
  {
@@ -4456,7 +5995,7 @@ class Compiler
4456
  $color = $this->coerceColor($args[0]);
4457
  $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
4458
 
4459
- return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
4460
  }
4461
 
4462
  protected static $libRed = ['color'];
@@ -4507,7 +6046,7 @@ class Compiler
4507
  }
4508
 
4509
  // mix two colors
4510
- protected static $libMix = ['color-1', 'color-2', 'weight'];
4511
  protected function libMix($args)
4512
  {
4513
  list($first, $second, $weight) = $args;
@@ -4543,25 +6082,56 @@ class Compiler
4543
  return $this->fixColor($new);
4544
  }
4545
 
4546
- protected static $libHsl = ['hue', 'saturation', 'lightness'];
4547
- protected function libHsl($args)
 
 
 
4548
  {
4549
- list($h, $s, $l) = $args;
 
 
 
4550
 
4551
- return $this->toRGB($h[1], $s[1], $l[1]);
4552
- }
4553
 
4554
- protected static $libHsla = ['hue', 'saturation', 'lightness', 'alpha'];
4555
- protected function libHsla($args)
4556
- {
4557
- list($h, $s, $l, $a) = $args;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4558
 
4559
- $color = $this->toRGB($h[1], $s[1], $l[1]);
4560
- $color[4] = $a[1];
 
 
 
4561
 
4562
  return $color;
4563
  }
4564
 
 
 
 
 
 
 
 
 
4565
  protected static $libHue = ['color'];
4566
  protected function libHue($args)
4567
  {
@@ -4629,7 +6199,7 @@ class Compiler
4629
  return $this->adjustHsl($color, 3, -$amount);
4630
  }
4631
 
4632
- protected static $libSaturate = ['color', 'amount'];
4633
  protected function libSaturate($args)
4634
  {
4635
  $value = $args[0];
@@ -4671,21 +6241,32 @@ class Compiler
4671
  return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
4672
  }
4673
 
4674
- protected static $libInvert = ['color'];
4675
  protected function libInvert($args)
4676
  {
4677
- $value = $args[0];
 
 
 
 
 
 
4678
 
4679
  if ($value[0] === Type::T_NUMBER) {
4680
  return null;
4681
  }
4682
 
4683
  $color = $this->assertColor($value);
4684
- $color[1] = 255 - $color[1];
4685
- $color[2] = 255 - $color[2];
4686
- $color[3] = 255 - $color[3];
 
4687
 
4688
- return $color;
 
 
 
 
4689
  }
4690
 
4691
  // increases opacity by amount
@@ -4794,7 +6375,7 @@ class Compiler
4794
  $min = null;
4795
 
4796
  foreach ($numbers as $key => $number) {
4797
- if (null === $min || $number[1] <= $min[1]) {
4798
  $min = [$key, $number[1]];
4799
  }
4800
  }
@@ -4808,7 +6389,7 @@ class Compiler
4808
  $max = null;
4809
 
4810
  foreach ($numbers as $key => $number) {
4811
- if (null === $max || $number[1] >= $max[1]) {
4812
  $max = [$key, $number[1]];
4813
  }
4814
  }
@@ -4825,9 +6406,9 @@ class Compiler
4825
  */
4826
  protected function getNormalizedNumbers($args)
4827
  {
4828
- $unit = null;
4829
  $originalUnit = null;
4830
- $numbers = [];
4831
 
4832
  foreach ($args as $key => $item) {
4833
  if ($item[0] !== Type::T_NUMBER) {
@@ -4837,10 +6418,10 @@ class Compiler
4837
 
4838
  $number = $item->normalize();
4839
 
4840
- if (null === $unit) {
4841
  $unit = $number[2];
4842
  $originalUnit = $item->unitStr();
4843
- } elseif ($unit !== $number[2]) {
4844
  $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
4845
  break;
4846
  }
@@ -4909,7 +6490,7 @@ class Compiler
4909
  if (! isset($list[2][$n])) {
4910
  $this->throwError('Invalid argument for "n"');
4911
 
4912
- return;
4913
  }
4914
 
4915
  $list[2][$n] = $args[2];
@@ -4921,11 +6502,15 @@ class Compiler
4921
  protected function libMapGet($args)
4922
  {
4923
  $map = $this->assertMap($args[0]);
4924
- $key = $this->compileStringContent($this->coerceString($args[1]));
4925
 
4926
- for ($i = count($map[1]) - 1; $i >= 0; $i--) {
4927
- if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
4928
- return $map[2][$i];
 
 
 
 
4929
  }
4930
  }
4931
 
@@ -4987,7 +6572,21 @@ class Compiler
4987
  $map1 = $this->assertMap($args[0]);
4988
  $map2 = $this->assertMap($args[1]);
4989
 
4990
- return [Type::T_MAP, array_merge($map1[1], $map2[1]), array_merge($map1[2], $map2[2])];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4991
  }
4992
 
4993
  protected static $libKeywords = ['args'];
@@ -5006,6 +6605,18 @@ class Compiler
5006
  return [Type::T_MAP, $keys, $values];
5007
  }
5008
 
 
 
 
 
 
 
 
 
 
 
 
 
5009
  protected function listSeparatorForJoin($list1, $sep)
5010
  {
5011
  if (! isset($sep)) {
@@ -5017,26 +6628,56 @@ class Compiler
5017
  return ',';
5018
 
5019
  case 'space':
5020
- return '';
5021
 
5022
  default:
5023
  return $list1[1];
5024
  }
5025
  }
5026
 
5027
- protected static $libJoin = ['list1', 'list2', 'separator'];
5028
  protected function libJoin($args)
5029
  {
5030
- list($list1, $list2, $sep) = $args;
5031
 
5032
  $list1 = $this->coerceList($list1, ' ');
5033
  $list2 = $this->coerceList($list2, ' ');
5034
- $sep = $this->listSeparatorForJoin($list1, $sep);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5035
 
5036
- return [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
 
 
 
 
 
 
 
5037
  }
5038
 
5039
- protected static $libAppend = ['list', 'val', 'separator'];
5040
  protected function libAppend($args)
5041
  {
5042
  list($list1, $value, $sep) = $args;
@@ -5044,13 +6685,17 @@ class Compiler
5044
  $list1 = $this->coerceList($list1, ' ');
5045
  $sep = $this->listSeparatorForJoin($list1, $sep);
5046
 
5047
- return [Type::T_LIST, $sep, array_merge($list1[2], [$value])];
 
 
 
 
5048
  }
5049
 
5050
  protected function libZip($args)
5051
  {
5052
- foreach ($args as $arg) {
5053
- $this->assertList($arg);
5054
  }
5055
 
5056
  $lists = [];
@@ -5133,7 +6778,7 @@ class Compiler
5133
  ) {
5134
  $this->throwError('Invalid argument(s) for "comparable"');
5135
 
5136
- return;
5137
  }
5138
 
5139
  $number1 = $number1->normalize();
@@ -5181,10 +6826,10 @@ class Compiler
5181
  return new Node\Number(strlen($stringContent), '');
5182
  }
5183
 
5184
- protected static $libStrSlice = ['string', 'start-at', 'end-at'];
5185
  protected function libStrSlice($args)
5186
  {
5187
- if (isset($args[2]) && $args[2][1] == 0) {
5188
  return static::$nullString;
5189
  }
5190
 
@@ -5197,7 +6842,7 @@ class Compiler
5197
  $start--;
5198
  }
5199
 
5200
- $end = (int) $args[2][1];
5201
  $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end);
5202
 
5203
  $string[2] = $length
@@ -5313,7 +6958,7 @@ class Compiler
5313
  if ($n < 1) {
5314
  $this->throwError("limit must be greater than or equal to 1");
5315
 
5316
- return;
5317
  }
5318
 
5319
  return new Node\Number(mt_rand(1, $n), '');
@@ -5335,13 +6980,670 @@ class Compiler
5335
  return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
5336
  }
5337
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5338
  protected static $libInspect = ['value'];
5339
  protected function libInspect($args)
5340
  {
5341
- if ($args[0] === static::$null) {
5342
- return [Type::T_KEYWORD, 'null'];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5343
  }
5344
 
5345
- return $args[0];
5346
  }
5347
  }
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
+
14
+ use ScssPhp\ScssPhp\Base\Range;
15
+ use ScssPhp\ScssPhp\Block;
16
+ use ScssPhp\ScssPhp\Cache;
17
+ use ScssPhp\ScssPhp\Colors;
18
+ use ScssPhp\ScssPhp\Compiler\Environment;
19
+ use ScssPhp\ScssPhp\Exception\CompilerException;
20
+ use ScssPhp\ScssPhp\Formatter\OutputBlock;
21
+ use ScssPhp\ScssPhp\Node;
22
+ use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
23
+ use ScssPhp\ScssPhp\Type;
24
+ use ScssPhp\ScssPhp\Parser;
25
+ use ScssPhp\ScssPhp\Util;
26
 
27
  /**
28
  * The scss compiler and parser.
99
  'function' => '^',
100
  ];
101
 
102
+ static public $true = [Type::T_KEYWORD, 'true'];
103
+ static public $false = [Type::T_KEYWORD, 'false'];
104
+ static public $null = [Type::T_NULL];
105
+ static public $nullString = [Type::T_STRING, '', []];
106
  static public $defaultValue = [Type::T_KEYWORD, ''];
107
  static public $selfSelector = [Type::T_SELF];
108
+ static public $emptyList = [Type::T_LIST, '', []];
109
+ static public $emptyMap = [Type::T_MAP, [], []];
110
+ static public $emptyString = [Type::T_STRING, '"', []];
111
+ static public $with = [Type::T_KEYWORD, 'with'];
112
+ static public $without = [Type::T_KEYWORD, 'without'];
113
 
114
  protected $importPaths = [''];
115
  protected $importCache = [];
130
  protected $sourceMapOptions = [];
131
 
132
  /**
133
+ * @var string|\ScssPhp\ScssPhp\Formatter
134
  */
135
+ protected $formatter = 'ScssPhp\ScssPhp\Formatter\Nested';
136
 
137
  protected $rootEnv;
138
  protected $rootBlock;
139
 
140
  /**
141
+ * @var \ScssPhp\ScssPhp\Compiler\Environment
142
  */
143
  protected $env;
144
  protected $scope;
146
  protected $charsetSeen;
147
  protected $sourceNames;
148
 
149
+ protected $cache;
150
+
151
+ protected $indentLevel;
152
+ protected $extends;
153
+ protected $extendsMap;
154
+ protected $parsedFiles;
155
+ protected $parser;
156
+ protected $sourceIndex;
157
+ protected $sourceLine;
158
+ protected $sourceColumn;
159
+ protected $stderr;
160
+ protected $shouldEvaluate;
161
+ protected $ignoreErrors;
162
+
163
+ protected $callStack = [];
164
 
165
  /**
166
  * Constructor
167
+ *
168
+ * @param array|null $cacheOptions
169
  */
170
+ public function __construct($cacheOptions = null)
171
  {
172
  $this->parsedFiles = [];
173
  $this->sourceNames = [];
174
+
175
+ if ($cacheOptions) {
176
+ $this->cache = new Cache($cacheOptions);
177
+ }
178
+
179
+ $this->stderr = fopen('php://stderr', 'w');
180
+ }
181
+
182
+ /**
183
+ * Get compiler options
184
+ *
185
+ * @return array
186
+ */
187
+ public function getCompileOptions()
188
+ {
189
+ $options = [
190
+ 'importPaths' => $this->importPaths,
191
+ 'registeredVars' => $this->registeredVars,
192
+ 'registeredFeatures' => $this->registeredFeatures,
193
+ 'encoding' => $this->encoding,
194
+ 'sourceMap' => serialize($this->sourceMap),
195
+ 'sourceMapOptions' => $this->sourceMapOptions,
196
+ 'formatter' => $this->formatter,
197
+ ];
198
+
199
+ return $options;
200
+ }
201
+
202
+ /**
203
+ * Set an alternative error output stream, for testing purpose only
204
+ *
205
+ * @param resource $handle
206
+ */
207
+ public function setErrorOuput($handle)
208
+ {
209
+ $this->stderr = $handle;
210
  }
211
 
212
  /**
221
  */
222
  public function compile($code, $path = null)
223
  {
224
+ if ($this->cache) {
225
+ $cacheKey = ($path ? $path : "(stdin)") . ":" . md5($code);
226
+ $compileOptions = $this->getCompileOptions();
227
+ $cache = $this->cache->getCache("compile", $cacheKey, $compileOptions);
228
+
229
+ if (is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) {
230
+ // check if any dependency file changed before accepting the cache
231
+ foreach ($cache['dependencies'] as $file => $mtime) {
232
+ if (! is_file($file) || filemtime($file) !== $mtime) {
233
+ unset($cache);
234
+ break;
235
+ }
236
+ }
237
+
238
+ if (isset($cache)) {
239
+ return $cache['out'];
240
+ }
241
+ }
242
+ }
243
+
244
+
245
  $this->indentLevel = -1;
 
246
  $this->extends = [];
247
  $this->extendsMap = [];
248
  $this->sourceIndex = null;
253
  $this->storeEnv = null;
254
  $this->charsetSeen = null;
255
  $this->shouldEvaluate = null;
 
256
 
257
  $this->parser = $this->parserFactory($path);
258
+ $tree = $this->parser->parse($code);
 
259
  $this->parser = null;
260
 
261
  $this->formatter = new $this->formatter();
296
  $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
297
  }
298
 
299
+ if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
300
+ $v = [
301
+ 'dependencies' => $this->getParsedFiles(),
302
+ 'out' => &$out,
303
+ ];
304
+
305
+ $this->cache->setCache("compile", $cacheKey, $v, $compileOptions);
306
+ }
307
+
308
  return $out;
309
  }
310
 
313
  *
314
  * @param string $path
315
  *
316
+ * @return \ScssPhp\ScssPhp\Parser
317
  */
318
  protected function parserFactory($path)
319
  {
320
+ $parser = new Parser($path, count($this->sourceNames), $this->encoding, $this->cache);
321
 
322
  $this->sourceNames[] = $path;
 
323
  $this->addParsedFile($path);
324
 
325
  return $parser;
347
  /**
348
  * Push extends
349
  *
350
+ * @param array $target
351
+ * @param array $origin
352
+ * @param array|null $block
353
  */
354
  protected function pushExtends($target, $origin, $block)
355
  {
375
  * @param string $type
376
  * @param array $selectors
377
  *
378
+ * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
379
  */
380
  protected function makeOutputBlock($type, $selectors = null)
381
  {
382
  $out = new OutputBlock;
383
+ $out->type = $type;
384
+ $out->lines = [];
385
+ $out->children = [];
386
+ $out->parent = $this->scope;
387
+ $out->selectors = $selectors;
388
+ $out->depth = $this->env->depth;
389
 
390
  if ($this->env->block instanceof Block) {
391
  $out->sourceName = $this->env->block->sourceName;
403
  /**
404
  * Compile root
405
  *
406
+ * @param \ScssPhp\ScssPhp\Block $rootBlock
407
  */
408
  protected function compileRoot(Block $rootBlock)
409
  {
442
  /**
443
  * Flatten selectors
444
  *
445
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
446
+ * @param string $parentKey
447
  */
448
  protected function flattenSelectors(OutputBlock $block, $parentKey = null)
449
  {
498
  }
499
  }
500
 
501
+ /**
502
+ * Glue parts of :not( or :nth-child( ... that are in general splitted in selectors parts
503
+ *
504
+ * @param array $parts
505
+ *
506
+ * @return array
507
+ */
508
+ protected function glueFunctionSelectors($parts)
509
+ {
510
+ $new = [];
511
+
512
+ foreach ($parts as $part) {
513
+ if (is_array($part)) {
514
+ $part = $this->glueFunctionSelectors($part);
515
+ $new[] = $part;
516
+ } else {
517
+ // a selector part finishing with a ) is the last part of a :not( or :nth-child(
518
+ // and need to be joined to this
519
+ if (count($new) && is_string($new[count($new) - 1]) &&
520
+ strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
521
+ ) {
522
+ while (count($new)>1 && substr($new[count($new) - 1], -1) !== '(') {
523
+ $part = array_pop($new) . $part;
524
+ }
525
+ $new[count($new) - 1] .= $part;
526
+ } else {
527
+ $new[] = $part;
528
+ }
529
+ }
530
+ }
531
+
532
+ return $new;
533
+ }
534
+
535
  /**
536
  * Match extends
537
  *
542
  */
543
  protected function matchExtends($selector, &$out, $from = 0, $initial = true)
544
  {
545
+ static $partsPile = [];
546
+ $selector = $this->glueFunctionSelectors($selector);
547
+
548
+ if (count($selector) == 1 && in_array(reset($selector), $partsPile)) {
549
+ return;
550
+ }
551
+
552
+ $outRecurs = [];
553
  foreach ($selector as $i => $part) {
554
  if ($i < $from) {
555
  continue;
556
  }
557
 
558
+ // check that we are not building an infinite loop of extensions
559
+ // if the new part is just including a previous part don't try to extend anymore
560
+ if (count($part) > 1) {
561
+ foreach ($partsPile as $previousPart) {
562
+ if (! count(array_diff($previousPart, $part))) {
563
+ continue 2;
564
+ }
565
+ }
566
+ }
567
 
568
+ $partsPile[] = $part;
569
+ if ($this->matchExtendsSingle($part, $origin, $initial)) {
570
+ $after = array_slice($selector, $i + 1);
571
+ $before = array_slice($selector, 0, $i);
572
  list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
573
 
574
  foreach ($origin as $new) {
575
  $k = 0;
576
 
577
  // remove shared parts
578
+ if (count($new) > 1) {
579
  while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
580
  $k++;
581
  }
582
  }
583
+ if (count($nonBreakableBefore) and $k == count($new)) {
584
+ $k--;
585
+ }
586
 
587
  $replacement = [];
588
  $tempReplacement = $k > 0 ? array_slice($new, $k) : $new;
589
 
590
  for ($l = count($tempReplacement) - 1; $l >= 0; $l--) {
591
+ $slice = [];
592
+
593
+ foreach ($tempReplacement[$l] as $chunk) {
594
+ if (! in_array($chunk, $slice)) {
595
+ $slice[] = $chunk;
596
+ }
597
+ }
598
+
599
  array_unshift($replacement, $slice);
600
 
601
  if (! $this->isImmediateRelationshipCombinator(end($slice))) {
619
  continue;
620
  }
621
 
622
+ $this->pushOrMergeExtentedSelector($out, $result);
623
 
624
  // recursively check for more matches
625
+ $startRecurseFrom = count($before) + min(count($nonBreakableBefore), count($mergedBefore));
626
+ if (count($origin) > 1) {
627
+ $this->matchExtends($result, $out, $startRecurseFrom, false);
628
+ } else {
629
+ $this->matchExtends($result, $outRecurs, $startRecurseFrom, false);
630
+ }
631
 
632
  // selector sequence merging
633
  if (! empty($before) && count($new) > 1) {
634
+ $preSharedParts = $k > 0 ? array_slice($before, 0, $k) : [];
635
  $postSharedParts = $k > 0 ? array_slice($before, $k) : $before;
636
 
637
+ list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore);
638
 
639
  $result2 = array_merge(
640
+ $preSharedParts,
641
+ $betweenSharedParts,
642
  $postSharedParts,
643
+ $nonBreakabl2,
644
  $nonBreakableBefore,
645
  $replacement,
646
  $after
647
  );
648
 
649
+ $this->pushOrMergeExtentedSelector($out, $result2);
650
+ }
651
+ }
652
+ }
653
+ array_pop($partsPile);
654
+ }
655
+ while (count($outRecurs)) {
656
+ $result = array_shift($outRecurs);
657
+ $this->pushOrMergeExtentedSelector($out, $result);
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Test a part for being a pseudo selector
663
+ * @param string $part
664
+ * @param array $matches
665
+ * @return bool
666
+ */
667
+ protected function isPseudoSelector($part, &$matches)
668
+ {
669
+ if (strpos($part, ":") === 0
670
+ && preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)) {
671
+ return true;
672
+ }
673
+ return false;
674
+ }
675
+
676
+ /**
677
+ * Push extended selector except if
678
+ * - this is a pseudo selector
679
+ * - same as previous
680
+ * - in a white list
681
+ * in this case we merge the pseudo selector content
682
+ * @param array $out
683
+ * @param array $extended
684
+ */
685
+ protected function pushOrMergeExtentedSelector(&$out, $extended)
686
+ {
687
+ if (count($out) && count($extended) === 1 && count(reset($extended)) === 1) {
688
+ $single = reset($extended);
689
+ $part = reset($single);
690
+ if ($this->isPseudoSelector($part, $matchesExtended)
691
+ && in_array($matchesExtended[1], [ 'slotted' ])) {
692
+ $prev = end($out);
693
+ $prev = $this->glueFunctionSelectors($prev);
694
+ if (count($prev) === 1 && count(reset($prev)) === 1) {
695
+ $single = reset($prev);
696
+ $part = reset($single);
697
+ if ($this->isPseudoSelector($part, $matchesPrev)
698
+ && $matchesPrev[1] === $matchesExtended[1]) {
699
+ $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2);
700
+ $extended[1] = $matchesPrev[2] . ", " . $extended[1];
701
+ $extended = implode($matchesExtended[1] . '(', $extended);
702
+ $extended = [ [ $extended ]];
703
+ array_pop($out);
704
  }
705
  }
706
  }
707
  }
708
+ $out[] = $extended;
709
  }
710
 
711
  /**
713
  *
714
  * @param array $rawSingle
715
  * @param array $outOrigin
716
+ * @param bool $initial
717
  *
718
  * @return boolean
719
  */
720
+ protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true)
721
  {
722
  $counts = [];
723
  $single = [];
724
 
725
+ // simple usual cases, no need to do the whole trick
726
+ if (in_array($rawSingle, [['>'],['+'],['~']])) {
727
+ return false;
728
+ }
729
+
730
  foreach ($rawSingle as $part) {
731
  // matches Number
732
  if (! is_string($part)) {
747
  $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
748
  }
749
 
750
+ $outOrigin = [];
751
+ $found = false;
752
+
753
+ foreach ($single as $k => $part) {
754
  if (isset($this->extendsMap[$part])) {
755
  foreach ($this->extendsMap[$part] as $idx) {
756
  $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
757
  }
758
  }
759
+ if ($initial
760
+ && $this->isPseudoSelector($part, $matches)
761
+ && ! in_array($matches[1], [ 'not' ])) {
762
+ $buffer = $matches[2];
763
+ $parser = $this->parserFactory(__METHOD__);
764
+ if ($parser->parseSelector($buffer, $subSelectors)) {
765
+ foreach ($subSelectors as $ksub => $subSelector) {
766
+ $subExtended = [];
767
+ $this->matchExtends($subSelector, $subExtended, 0, false);
768
+ if ($subExtended) {
769
+ $subSelectorsExtended = $subSelectors;
770
+ $subSelectorsExtended[$ksub] = $subExtended;
771
+ foreach ($subSelectorsExtended as $ksse => $sse) {
772
+ $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse);
773
+ }
774
+ $subSelectorsExtended = implode(', ', $subSelectorsExtended);
775
+ $singleExtended = $single;
776
+ $singleExtended[$k] = str_replace("(".$buffer.")", "($subSelectorsExtended)", $part);
777
+ $outOrigin[] = [ $singleExtended ];
778
+ $found = true;
779
+ }
780
+ }
781
+ }
782
+ }
783
  }
784
 
 
 
 
785
  foreach ($counts as $idx => $count) {
786
  list($target, $origin, /* $block */) = $this->extends[$idx];
787
 
788
+ $origin = $this->glueFunctionSelectors($origin);
789
+
790
  // check count
791
  if ($count !== count($target)) {
792
  continue;
827
  return $found;
828
  }
829
 
 
830
  /**
831
  * Extract a relationship from the fragment.
832
  *
836
  * the rest.
837
  *
838
  * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
839
+ *
840
  * @return array The selector without the relationship fragment if any, the relationship fragment.
841
  */
842
  protected function extractRelationshipFromFragment(array $fragment)
843
  {
844
  $parents = [];
845
  $children = [];
846
+
847
  $j = $i = count($fragment);
848
 
849
  for (;;) {
850
  $children = $j != $i ? array_slice($fragment, $j, $i - $j) : [];
851
+ $parents = array_slice($fragment, 0, $j);
852
+ $slice = end($parents);
853
 
854
  if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
855
  break;
871
  */
872
  protected function combineSelectorSingle($base, $other)
873
  {
874
+ $tag = [];
875
+ $out = [];
876
+ $wasTag = false;
877
+ $pseudo = [];
878
+
879
+ while (count($other) && strpos(end($other), ':')===0) {
880
+ array_unshift($pseudo, array_pop($other));
881
+ }
882
 
883
+ foreach ([array_reverse($base), array_reverse($other)] as $single) {
884
  foreach ($single as $part) {
885
+ if (preg_match('/^[\[:]/', $part)) {
886
  $out[] = $part;
887
  $wasTag = false;
888
+ } elseif (preg_match('/^[\.#]/', $part)) {
889
+ array_unshift($out, $part);
890
+ $wasTag = false;
891
  } elseif (preg_match('/^[^_-]/', $part)) {
892
  $tag[] = $part;
893
  $wasTag = true;
894
  } elseif ($wasTag) {
895
  $tag[count($tag) - 1] .= $part;
896
  } else {
897
+ $out[] = $part;
898
  }
899
  }
900
  }
902
  if (count($tag)) {
903
  array_unshift($out, $tag[0]);
904
  }
905
+ while (count($pseudo)) {
906
+ $out[] = array_shift($pseudo);
907
+ }
908
 
909
  return $out;
910
  }
912
  /**
913
  * Compile media
914
  *
915
+ * @param \ScssPhp\ScssPhp\Block $media
916
  */
917
  protected function compileMedia(Block $media)
918
  {
919
  $this->pushEnv($media);
920
 
921
+ $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env));
 
 
 
922
 
923
+ if (! empty($mediaQueries) && $mediaQueries) {
924
+ $previousScope = $this->scope;
925
  $parentScope = $this->mediaParent($this->scope);
926
+
927
+ foreach ($mediaQueries as $mediaQuery) {
928
+ $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
929
+
930
+ $parentScope->children[] = $this->scope;
931
+ $parentScope = $this->scope;
932
+ }
933
 
934
  // top level properties in a media cause it to be wrapped
935
  $needsWrap = false;
959
  $wrapped->children = $media->children;
960
 
961
  $media->children = [[Type::T_BLOCK, $wrapped]];
962
+
963
+ if (isset($this->lineNumberStyle)) {
964
+ $annotation = $this->makeOutputBlock(Type::T_COMMENT);
965
+ $annotation->depth = 0;
966
+
967
+ $file = $this->sourceNames[$media->sourceIndex];
968
+ $line = $media->sourceLine;
969
+
970
+ switch ($this->lineNumberStyle) {
971
+ case static::LINE_COMMENTS:
972
+ $annotation->lines[] = '/* line ' . $line
973
+ . ($file ? ', ' . $file : '')
974
+ . ' */';
975
+ break;
976
+
977
+ case static::DEBUG_INFO:
978
+ $annotation->lines[] = '@media -sass-debug-info{'
979
+ . ($file ? 'filename{font-family:"' . $file . '"}' : '')
980
+ . 'line{font-family:' . $line . '}}';
981
+ break;
982
+ }
983
+
984
+ $this->scope->children[] = $annotation;
985
+ }
986
  }
987
 
988
  $this->compileChildrenNoReturn($media->children, $this->scope);
989
 
990
+ $this->scope = $previousScope;
991
  }
992
 
993
  $this->popEnv();
996
  /**
997
  * Media parent
998
  *
999
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1000
  *
1001
+ * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
1002
  */
1003
  protected function mediaParent(OutputBlock $scope)
1004
  {
1016
  /**
1017
  * Compile directive
1018
  *
1019
+ * @param \ScssPhp\ScssPhp\Block|array $block
1020
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1021
  */
1022
+ protected function compileDirective($directive, OutputBlock $out)
1023
  {
1024
+ if (is_array($directive)) {
1025
+ $s = '@' . $directive[0];
1026
+ if (! empty($directive[1])) {
1027
+ $s .= ' ' . $this->compileValue($directive[1]);
1028
+ }
1029
+ $this->appendRootDirective($s . ';', $out);
1030
+ } else {
1031
+ $s = '@' . $directive->name;
1032
 
1033
+ if (! empty($directive->value)) {
1034
+ $s .= ' ' . $this->compileValue($directive->value);
1035
+ }
1036
 
1037
+ if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') {
1038
+ $this->compileKeyframeBlock($directive, [$s]);
1039
+ } else {
1040
+ $this->compileNestedBlock($directive, [$s]);
1041
+ }
1042
  }
1043
  }
1044
 
1045
  /**
1046
  * Compile at-root
1047
  *
1048
+ * @param \ScssPhp\ScssPhp\Block $block
1049
  */
1050
  protected function compileAtRoot(Block $block)
1051
  {
1052
  $env = $this->pushEnv($block);
1053
  $envs = $this->compactEnv($env);
1054
+ list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null);
1055
 
1056
  // wrap inline selector
1057
  if ($block->selector) {
1064
  $wrapped->comments = [];
1065
  $wrapped->parent = $block;
1066
  $wrapped->children = $block->children;
1067
+ $wrapped->selfParent = $block->selfParent;
1068
 
1069
  $block->children = [[Type::T_BLOCK, $wrapped]];
1070
+ $block->selector = null;
1071
+ }
1072
+
1073
+ $selfParent = $block->selfParent;
1074
+
1075
+ if (! $block->selfParent->selectors && isset($block->parent) && $block->parent &&
1076
+ isset($block->parent->selectors) && $block->parent->selectors
1077
+ ) {
1078
+ $selfParent = $block->parent;
1079
  }
1080
 
1081
+ $this->env = $this->filterWithWithout($envs, $with, $without);
 
1082
 
1083
  $saveScope = $this->scope;
1084
+ $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without);
1085
 
1086
+ // propagate selfParent to the children where they still can be useful
1087
+ $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent);
1088
 
1089
+ $this->scope = $this->completeScope($this->scope, $saveScope);
1090
  $this->scope = $saveScope;
1091
  $this->env = $this->extractEnv($envs);
1092
 
1094
  }
1095
 
1096
  /**
1097
+ * Filter at-root scope depending of with/without option
1098
  *
1099
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1100
+ * @param array $with
1101
+ * @param array $without
1102
  *
1103
+ * @return mixed
1104
  */
1105
+ protected function filterScopeWithWithout($scope, $with, $without)
1106
  {
1107
+ $filteredScopes = [];
1108
+ $childStash = [];
1109
 
1110
+ if ($scope->type === TYPE::T_ROOT) {
1111
+ return $scope;
1112
+ }
 
1113
 
1114
+ // start from the root
1115
+ while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) {
1116
+ array_unshift($childStash, $scope);
1117
+ $scope = $scope->parent;
1118
+ }
1119
+
1120
+ for (;;) {
1121
+ if (! $scope) {
1122
+ break;
1123
  }
1124
 
1125
+ if ($this->isWith($scope, $with, $without)) {
1126
+ $s = clone $scope;
1127
+ $s->children = [];
1128
+ $s->lines = [];
1129
+ $s->parent = null;
1130
+
1131
+ if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) {
1132
+ $s->selectors = [];
1133
+ }
1134
+
1135
+ $filteredScopes[] = $s;
1136
  }
1137
 
1138
+ if (count($childStash)) {
1139
+ $scope = array_shift($childStash);
1140
+ } elseif ($scope->children) {
1141
+ $scope = end($scope->children);
1142
+ } else {
1143
+ $scope = null;
1144
  }
1145
+ }
1146
 
1147
+ if (! count($filteredScopes)) {
1148
+ return $this->rootBlock;
1149
+ }
 
 
 
 
 
1150
 
1151
+ $newScope = array_shift($filteredScopes);
1152
+ $newScope->parent = $this->rootBlock;
1153
 
1154
+ $this->rootBlock->children[] = $newScope;
1155
 
1156
+ $p = &$newScope;
 
 
 
 
 
 
1157
 
1158
+ while (count($filteredScopes)) {
1159
+ $s = array_shift($filteredScopes);
1160
+ $s->parent = $p;
1161
+ $p->children[] = $s;
1162
+ $newScope = &$p->children[0];
1163
+ $p = &$p->children[0];
1164
+ }
1165
 
1166
+ return $newScope;
1167
+ }
 
1168
 
1169
+ /**
1170
+ * found missing selector from a at-root compilation in the previous scope
1171
+ * (if at-root is just enclosing a property, the selector is in the parent tree)
1172
+ *
1173
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1174
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope
1175
+ *
1176
+ * @return mixed
1177
+ */
1178
+ protected function completeScope($scope, $previousScope)
1179
+ {
1180
+ if (! $scope->type && (! $scope->selectors || ! count($scope->selectors)) && count($scope->lines)) {
1181
+ $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth);
1182
+ }
1183
 
1184
+ if ($scope->children) {
1185
+ foreach ($scope->children as $k => $c) {
1186
+ $scope->children[$k] = $this->completeScope($c, $previousScope);
1187
  }
1188
+ }
1189
 
1190
+ return $scope;
1191
+ }
 
1192
 
1193
+ /**
1194
+ * Find a selector by the depth node in the scope
1195
+ *
1196
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1197
+ * @param integer $depth
1198
+ *
1199
+ * @return array
1200
+ */
1201
+ protected function findScopeSelectors($scope, $depth)
1202
+ {
1203
+ if ($scope->depth === $depth && $scope->selectors) {
1204
+ return $scope->selectors;
1205
  }
1206
 
1207
+ if ($scope->children) {
1208
+ foreach (array_reverse($scope->children) as $c) {
1209
+ if ($s = $this->findScopeSelectors($c, $depth)) {
1210
+ return $s;
1211
+ }
1212
+ }
1213
+ }
1214
 
1215
+ return [];
1216
  }
1217
 
1218
  /**
1219
+ * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later
1220
  *
1221
+ * @param array $withCondition
1222
  *
1223
+ * @return array
1224
  */
1225
+ protected function compileWith($withCondition)
1226
  {
1227
+ // just compile what we have in 2 lists
1228
+ $with = [];
1229
+ $without = ['rule' => true];
 
 
 
1230
 
1231
+ if ($withCondition) {
1232
+ if ($this->libMapHasKey([$withCondition, static::$with])) {
1233
+ $without = []; // cancel the default
1234
+ $list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
1235
 
1236
+ foreach ($list[2] as $item) {
1237
+ $keyword = $this->compileStringContent($this->coerceString($item));
 
 
 
 
 
1238
 
1239
+ $with[$keyword] = true;
 
1240
  }
1241
  }
 
1242
 
1243
+ if ($this->libMapHasKey([$withCondition, static::$without])) {
1244
+ $without = []; // cancel the default
1245
+ $list = $this->coerceList($this->libMapGet([$withCondition, static::$without]));
1246
 
1247
+ foreach ($list[2] as $item) {
1248
+ $keyword = $this->compileStringContent($this->coerceString($item));
 
 
1249
 
1250
+ $without[$keyword] = true;
 
1251
  }
1252
  }
1253
  }
1254
 
1255
+ return [$with, $without];
1256
  }
1257
 
1258
  /**
1259
  * Filter env stack
1260
  *
1261
  * @param array $envs
1262
+ * @param array $with
1263
+ * @param array $without
1264
  *
1265
+ * @return \ScssPhp\ScssPhp\Compiler\Environment
1266
  */
1267
+ protected function filterWithWithout($envs, $with, $without)
1268
  {
1269
  $filtered = [];
1270
 
1271
  foreach ($envs as $e) {
1272
+ if ($e->block && ! $this->isWith($e->block, $with, $without)) {
1273
+ $ec = clone $e;
1274
+ $ec->block = null;
1275
+ $ec->selectors = [];
1276
 
1277
+ $filtered[] = $ec;
1278
+ } else {
1279
+ $filtered[] = $e;
1280
+ }
1281
  }
1282
 
1283
  return $this->extractEnv($filtered);
1286
  /**
1287
  * Filter WITH rules
1288
  *
1289
+ * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block
1290
+ * @param array $with
1291
+ * @param array $without
1292
  *
1293
  * @return boolean
1294
  */
1295
+ protected function isWith($block, $with, $without)
1296
  {
1297
+ if (isset($block->type)) {
1298
+ if ($block->type === Type::T_MEDIA) {
1299
+ return $this->testWithWithout('media', $with, $without);
1300
+ }
1301
+
1302
+ if ($block->type === Type::T_DIRECTIVE) {
1303
+ if (isset($block->name)) {
1304
+ return $this->testWithWithout($block->name, $with, $without);
1305
+ } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
1306
+ return $this->testWithWithout($m[1], $with, $without);
1307
+ } else {
1308
+ return $this->testWithWithout('???', $with, $without);
1309
+ }
1310
+ }
1311
+ } elseif (isset($block->selectors)) {
1312
+ // a selector starting with number is a keyframe rule
1313
+ if (count($block->selectors)) {
1314
+ $s = reset($block->selectors);
1315
+
1316
+ while (is_array($s)) {
1317
+ $s = reset($s);
1318
+ }
1319
+
1320
+ if (is_object($s) && $s instanceof Node\Number) {
1321
+ return $this->testWithWithout('keyframes', $with, $without);
1322
+ }
1323
+ }
1324
+
1325
+ return $this->testWithWithout('rule', $with, $without);
1326
  }
1327
 
1328
+ return true;
1329
+ }
1330
+
1331
+ /**
1332
+ * Test a single type of block against with/without lists
1333
+ *
1334
+ * @param string $what
1335
+ * @param array $with
1336
+ * @param array $without
1337
+ *
1338
+ * @return boolean
1339
+ * true if the block should be kept, false to reject
1340
+ */
1341
+ protected function testWithWithout($what, $with, $without)
1342
+ {
1343
+
1344
+ // if without, reject only if in the list (or 'all' is in the list)
1345
+ if (count($without)) {
1346
+ return (isset($without[$what]) || isset($without['all'])) ? false : true;
1347
+ }
1348
+
1349
+ // otherwise reject all what is not in the with list
1350
+ return (isset($with[$what]) || isset($with['all'])) ? true : false;
1351
  }
1352
 
1353
+
1354
  /**
1355
  * Compile keyframe block
1356
  *
1357
+ * @param \ScssPhp\ScssPhp\Block $block
1358
+ * @param array $selectors
1359
  */
1360
  protected function compileKeyframeBlock(Block $block, $selectors)
1361
  {
1379
  $this->popEnv();
1380
  }
1381
 
1382
+ /**
1383
+ * Compile nested properties lines
1384
+ *
1385
+ * @param \ScssPhp\ScssPhp\Block $block
1386
+ * @param OutputBlock $out
1387
+ */
1388
+ protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out)
1389
+ {
1390
+ $prefix = $this->compileValue($block->prefix) . '-';
1391
+
1392
+ $nested = $this->makeOutputBlock($block->type);
1393
+ $nested->parent = $out;
1394
+
1395
+ if ($block->hasValue) {
1396
+ $nested->depth = $out->depth + 1;
1397
+ }
1398
+
1399
+ $out->children[] = $nested;
1400
+
1401
+ foreach ($block->children as $child) {
1402
+ switch ($child[0]) {
1403
+ case Type::T_ASSIGN:
1404
+ array_unshift($child[1][2], $prefix);
1405
+ break;
1406
+
1407
+ case Type::T_NESTED_PROPERTY:
1408
+ array_unshift($child[1]->prefix[2], $prefix);
1409
+ break;
1410
+ }
1411
+
1412
+ $this->compileChild($child, $nested);
1413
+ }
1414
+ }
1415
+
1416
  /**
1417
  * Compile nested block
1418
  *
1419
+ * @param \ScssPhp\ScssPhp\Block $block
1420
+ * @param array $selectors
1421
  */
1422
  protected function compileNestedBlock(Block $block, $selectors)
1423
  {
1426
  $this->scope = $this->makeOutputBlock($block->type, $selectors);
1427
  $this->scope->parent->children[] = $this->scope;
1428
 
1429
+ // wrap assign children in a block
1430
+ // except for @font-face
1431
+ if ($block->type !== Type::T_DIRECTIVE || $block->name !== "font-face") {
1432
+ // need wrapping?
1433
+ $needWrapping = false;
1434
+
1435
+ foreach ($block->children as $child) {
1436
+ if ($child[0] === Type::T_ASSIGN) {
1437
+ $needWrapping = true;
1438
+ break;
1439
+ }
1440
+ }
1441
+
1442
+ if ($needWrapping) {
1443
+ $wrapped = new Block;
1444
+ $wrapped->sourceName = $block->sourceName;
1445
+ $wrapped->sourceIndex = $block->sourceIndex;
1446
+ $wrapped->sourceLine = $block->sourceLine;
1447
+ $wrapped->sourceColumn = $block->sourceColumn;
1448
+ $wrapped->selectors = [];
1449
+ $wrapped->comments = [];
1450
+ $wrapped->parent = $block;
1451
+ $wrapped->children = $block->children;
1452
+ $wrapped->selfParent = $block->selfParent;
1453
+
1454
+ $block->children = [[Type::T_BLOCK, $wrapped]];
1455
+ }
1456
+ }
1457
+
1458
  $this->compileChildrenNoReturn($block->children, $this->scope);
1459
 
1460
  $this->scope = $this->scope->parent;
1478
  *
1479
  * @see Compiler::compileChild()
1480
  *
1481
+ * @param \ScssPhp\ScssPhp\Block $block
1482
  */
1483
  protected function compileBlock(Block $block)
1484
  {
1514
  $this->scope->children[] = $out;
1515
 
1516
  if (count($block->children)) {
1517
+ $out->selectors = $this->multiplySelectors($env, $block->selfParent);
1518
 
1519
+ // propagate selfParent to the children where they still can be useful
1520
+ $selfParentSelectors = null;
1521
 
1522
+ if (isset($block->selfParent->selectors)) {
1523
+ $selfParentSelectors = $block->selfParent->selectors;
1524
+ $block->selfParent->selectors = $out->selectors;
1525
+ }
1526
 
1527
+ $this->compileChildrenNoReturn($block->children, $out, $block->selfParent);
 
1528
 
1529
+ // and revert for the following children of the same block
1530
+ if ($selfParentSelectors) {
1531
+ $block->selfParent->selectors = $selfParentSelectors;
1532
+ }
1533
+ }
1534
+
1535
+ $this->popEnv();
1536
+ }
1537
+
1538
+
1539
+ /**
1540
+ * Compile the value of a comment that can have interpolation
1541
+ *
1542
+ * @param array $value
1543
+ * @param boolean $pushEnv
1544
+ *
1545
+ * @return array|mixed|string
1546
+ */
1547
+ protected function compileCommentValue($value, $pushEnv = false)
1548
+ {
1549
+ $c = $value[1];
1550
+
1551
+ if (isset($value[2])) {
1552
+ if ($pushEnv) {
1553
+ $this->pushEnv();
1554
+ $storeEnv = $this->storeEnv;
1555
+ $this->storeEnv = $this->env;
1556
+ }
1557
+
1558
+ try {
1559
+ $c = $this->compileValue($value[2]);
1560
+ } catch (\Exception $e) {
1561
+ // ignore error in comment compilation which are only interpolation
1562
+ }
1563
+
1564
+ if ($pushEnv) {
1565
+ $this->storeEnv = $storeEnv;
1566
+ $this->popEnv();
1567
+ }
1568
+ }
1569
+
1570
+ return $c;
1571
+ }
1572
+
1573
+ /**
1574
  * Compile root level comment
1575
  *
1576
  * @param array $block
1578
  protected function compileComment($block)
1579
  {
1580
  $out = $this->makeOutputBlock(Type::T_COMMENT);
1581
+ $out->lines[] = $this->compileCommentValue($block, true);
1582
+
1583
  $this->scope->children[] = $out;
1584
  }
1585
 
1598
 
1599
  // after evaluating interpolates, we might need a second pass
1600
  if ($this->shouldEvaluate) {
1601
+ $selectors = $this->revertSelfSelector($selectors);
1602
+ $buffer = $this->collapseSelectors($selectors);
1603
+ $parser = $this->parserFactory(__METHOD__);
1604
 
1605
  if ($parser->parseSelector($buffer, $newSelectors)) {
1606
  $selectors = array_map([$this, 'evalSelector'], $newSelectors);
1653
  /**
1654
  * Collapse selectors
1655
  *
1656
+ * @param array $selectors
1657
+ * @param boolean $selectorFormat
1658
+ * if false return a collapsed string
1659
+ * if true return an array description of a structured selector
1660
  *
1661
  * @return string
1662
  */
1663
+ protected function collapseSelectors($selectors, $selectorFormat = false)
1664
  {
1665
  $parts = [];
1666
 
1667
  foreach ($selectors as $selector) {
1668
+ $output = [];
1669
+ $glueNext = false;
1670
 
1671
+ foreach ($selector as $node) {
1672
+ $compound = '';
1673
+
1674
+ array_walk_recursive(
1675
+ $node,
1676
+ function ($value, $key) use (&$compound) {
1677
+ $compound .= $value;
1678
+ }
1679
+ );
1680
+
1681
+ if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) {
1682
+ if (count($output)) {
1683
+ $output[count($output) - 1] .= ' ' . $compound;
1684
+ } else {
1685
+ $output[] = $compound;
1686
+ }
1687
+
1688
+ $glueNext = true;
1689
+ } elseif ($glueNext) {
1690
+ $output[count($output) - 1] .= ' ' . $compound;
1691
+ $glueNext = false;
1692
+ } else {
1693
+ $output[] = $compound;
1694
  }
1695
+ }
1696
+
1697
+ if ($selectorFormat) {
1698
+ foreach ($output as &$o) {
1699
+ $o = [Type::T_STRING, '', [$o]];
1700
+ }
1701
+
1702
+ $output = [Type::T_LIST, ' ', $output];
1703
+ } else {
1704
+ $output = implode(' ', $output);
1705
+ }
1706
 
1707
  $parts[] = $output;
1708
  }
1709
 
1710
+ if ($selectorFormat) {
1711
+ $parts = [Type::T_LIST, ',', $parts];
1712
+ } else {
1713
+ $parts = implode(', ', $parts);
1714
+ }
1715
+
1716
+ return $parts;
1717
+ }
1718
+
1719
+ /**
1720
+ * Parse down the selector and revert [self] to "&" before a reparsing
1721
+ *
1722
+ * @param array $selectors
1723
+ *
1724
+ * @return array
1725
+ */
1726
+ protected function revertSelfSelector($selectors)
1727
+ {
1728
+ foreach ($selectors as &$part) {
1729
+ if (is_array($part)) {
1730
+ if ($part === [Type::T_SELF]) {
1731
+ $part = '&';
1732
+ } else {
1733
+ $part = $this->revertSelfSelector($part);
1734
+ }
1735
+ }
1736
+ }
1737
+
1738
+ return $selectors;
1739
  }
1740
 
1741
  /**
1842
  return false;
1843
  }
1844
 
1845
+ protected function pushCallStack($name = '')
1846
+ {
1847
+ $this->callStack[] = [
1848
+ 'n' => $name,
1849
+ Parser::SOURCE_INDEX => $this->sourceIndex,
1850
+ Parser::SOURCE_LINE => $this->sourceLine,
1851
+ Parser::SOURCE_COLUMN => $this->sourceColumn
1852
+ ];
1853
+
1854
+ // infinite calling loop
1855
+ if (count($this->callStack) > 25000) {
1856
+ // not displayed but you can var_dump it to deep debug
1857
+ $msg = $this->callStackMessage(true, 100);
1858
+ $msg = "Infinite calling loop";
1859
+
1860
+ $this->throwError($msg);
1861
+ }
1862
+ }
1863
+
1864
+ protected function popCallStack()
1865
+ {
1866
+ array_pop($this->callStack);
1867
+ }
1868
+
1869
  /**
1870
  * Compile children and return result
1871
  *
1872
+ * @param array $stms
1873
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1874
+ * @param string $traceName
1875
  *
1876
+ * @return array|null
1877
  */
1878
+ protected function compileChildren($stms, OutputBlock $out, $traceName = '')
1879
  {
1880
+ $this->pushCallStack($traceName);
1881
+
1882
  foreach ($stms as $stm) {
1883
  $ret = $this->compileChild($stm, $out);
1884
 
1886
  return $ret;
1887
  }
1888
  }
1889
+
1890
+ $this->popCallStack();
1891
+
1892
+ return null;
1893
  }
1894
 
1895
  /**
1896
  * Compile children and throw exception if unexpected @return
1897
  *
1898
+ * @param array $stms
1899
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1900
+ * @param \ScssPhp\ScssPhp\Block $selfParent
1901
+ * @param string $traceName
1902
  *
1903
  * @throws \Exception
1904
  */
1905
+ protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '')
1906
  {
1907
+ $this->pushCallStack($traceName);
1908
+
1909
  foreach ($stms as $stm) {
1910
+ if ($selfParent && isset($stm[1]) && is_object($stm[1]) && $stm[1] instanceof Block) {
1911
+ $stm[1]->selfParent = $selfParent;
1912
+ $ret = $this->compileChild($stm, $out);
1913
+ $stm[1]->selfParent = null;
1914
+ } elseif ($selfParent && in_array($stm[0], [TYPE::T_INCLUDE, TYPE::T_EXTEND])) {
1915
+ $stm['selfParent'] = $selfParent;
1916
+ $ret = $this->compileChild($stm, $out);
1917
+ unset($stm['selfParent']);
1918
+ } else {
1919
+ $ret = $this->compileChild($stm, $out);
1920
+ }
1921
 
1922
  if (isset($ret)) {
1923
  $this->throwError('@return may only be used within a function');
1925
  return;
1926
  }
1927
  }
1928
+
1929
+ $this->popCallStack();
1930
+ }
1931
+
1932
+
1933
+ /**
1934
+ * evaluate media query : compile internal value keeping the structure inchanged
1935
+ *
1936
+ * @param array $queryList
1937
+ *
1938
+ * @return array
1939
+ */
1940
+ protected function evaluateMediaQuery($queryList)
1941
+ {
1942
+ static $parser = null;
1943
+
1944
+ $outQueryList = [];
1945
+
1946
+ foreach ($queryList as $kql => $query) {
1947
+ $shouldReparse = false;
1948
+
1949
+ foreach ($query as $kq => $q) {
1950
+ for ($i = 1; $i < count($q); $i++) {
1951
+ $value = $this->compileValue($q[$i]);
1952
+
1953
+ // the parser had no mean to know if media type or expression if it was an interpolation
1954
+ // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type
1955
+ if ($q[0] == Type::T_MEDIA_TYPE &&
1956
+ (strpos($value, '(') !== false ||
1957
+ strpos($value, ')') !== false ||
1958
+ strpos($value, ':') !== false ||
1959
+ strpos($value, ',') !== false)
1960
+ ) {
1961
+ $shouldReparse = true;
1962
+ }
1963
+
1964
+ $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value];
1965
+ }
1966
+ }
1967
+
1968
+ if ($shouldReparse) {
1969
+ if (is_null($parser)) {
1970
+ $parser = $this->parserFactory(__METHOD__);
1971
+ }
1972
+
1973
+ $queryString = $this->compileMediaQuery([$queryList[$kql]]);
1974
+ $queryString = reset($queryString);
1975
+
1976
+ if (strpos($queryString, '@media ') === 0) {
1977
+ $queryString = substr($queryString, 7);
1978
+ $queries = [];
1979
+
1980
+ if ($parser->parseMediaQueryList($queryString, $queries)) {
1981
+ $queries = $this->evaluateMediaQuery($queries[2]);
1982
+
1983
+ while (count($queries)) {
1984
+ $outQueryList[] = array_shift($queries);
1985
+ }
1986
+
1987
+ continue;
1988
+ }
1989
+ }
1990
+ }
1991
+
1992
+ $outQueryList[] = $queryList[$kql];
1993
+ }
1994
+
1995
+ return $outQueryList;
1996
  }
1997
 
1998
  /**
2000
  *
2001
  * @param array $queryList
2002
  *
2003
+ * @return array
2004
  */
2005
  protected function compileMediaQuery($queryList)
2006
  {
2007
+ $start = '@media ';
2008
+ $default = trim($start);
2009
+ $out = [];
2010
+ $current = "";
2011
 
2012
  foreach ($queryList as $query) {
2013
  $type = null;
2014
  $parts = [];
2015
 
2016
+ $mediaTypeOnly = true;
2017
+
2018
+ foreach ($query as $q) {
2019
+ if ($q[0] !== Type::T_MEDIA_TYPE) {
2020
+ $mediaTypeOnly = false;
2021
+ break;
2022
+ }
2023
+ }
2024
+
2025
  foreach ($query as $q) {
2026
  switch ($q[0]) {
2027
  case Type::T_MEDIA_TYPE:
2028
+ $newType = array_map([$this, 'compileValue'], array_slice($q, 1));
 
 
 
 
2029
 
2030
+ // combining not and anything else than media type is too risky and should be avoided
2031
+ if (! $mediaTypeOnly) {
2032
+ if (in_array(Type::T_NOT, $newType) || ($type && in_array(Type::T_NOT, $type) )) {
2033
+ if ($type) {
2034
+ array_unshift($parts, implode(' ', array_filter($type)));
2035
+ }
2036
+
2037
+ if (! empty($parts)) {
2038
+ if (strlen($current)) {
2039
+ $current .= $this->formatter->tagSeparator;
2040
+ }
2041
+
2042
+ $current .= implode(' and ', $parts);
2043
+ }
2044
+
2045
+ if ($current) {
2046
+ $out[] = $start . $current;
2047
+ }
2048
+
2049
+ $current = "";
2050
+ $type = null;
2051
+ $parts = [];
2052
+ }
2053
+ }
2054
+
2055
+ if ($newType === ['all'] && $default) {
2056
+ $default = $start . 'all';
2057
+ }
2058
+
2059
+ // all can be safely ignored and mixed with whatever else
2060
+ if ($newType !== ['all']) {
2061
+ if ($type) {
2062
+ $type = $this->mergeMediaTypes($type, $newType);
2063
+
2064
+ if (empty($type)) {
2065
+ // merge failed : ignore this query that is not valid, skip to the next one
2066
+ $parts = [];
2067
+ $default = ''; // if everything fail, no @media at all
2068
+ continue 3;
2069
+ }
2070
+ } else {
2071
+ $type = $newType;
2072
  }
 
 
2073
  }
2074
  break;
2075
 
2098
  }
2099
 
2100
  if (! empty($parts)) {
2101
+ if (strlen($current)) {
2102
+ $current .= $this->formatter->tagSeparator;
 
 
 
2103
  }
2104
 
2105
+ $current .= implode(' and ', $parts);
2106
  }
2107
  }
2108
 
2109
+ if ($current) {
2110
+ $out[] = $start . $current;
2111
+ }
2112
+
2113
+ // no @media type except all, and no conflict?
2114
+ if (! $out && $default) {
2115
+ $out[] = $default;
2116
+ }
2117
+
2118
  return $out;
2119
  }
2120
 
2121
+ /**
2122
+ * Merge direct relationships between selectors
2123
+ *
2124
+ * @param array $selectors1
2125
+ * @param array $selectors2
2126
+ *
2127
+ * @return array
2128
+ */
2129
  protected function mergeDirectRelationships($selectors1, $selectors2)
2130
  {
2131
  if (empty($selectors1) || empty($selectors2)) {
2135
  $part1 = end($selectors1);
2136
  $part2 = end($selectors2);
2137
 
2138
+ if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
2139
  return array_merge($selectors1, $selectors2);
2140
  }
2141
 
2145
  $part1 = array_pop($selectors1);
2146
  $part2 = array_pop($selectors2);
2147
 
2148
+ if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
2149
+ if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) {
2150
+ array_unshift($merged, [$part1[0] . $part2[0]]);
2151
+ $merged = array_merge($selectors1, $selectors2, $merged);
2152
+ } else {
2153
+ $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
2154
+ }
2155
+
2156
  break;
2157
  }
2158
 
2159
  array_unshift($merged, $part1);
 
2160
  } while (! empty($selectors1) && ! empty($selectors2));
2161
 
2162
  return $merged;
2180
  return $type1;
2181
  }
2182
 
 
 
 
2183
  if (count($type1) > 1) {
2184
+ $m1 = strtolower($type1[0]);
2185
+ $t1 = strtolower($type1[1]);
2186
  } else {
2187
+ $m1 = '';
2188
  $t1 = strtolower($type1[0]);
2189
  }
2190
 
 
 
 
2191
  if (count($type2) > 1) {
2192
  $m2 = strtolower($type2[0]);
2193
  $t2 = strtolower($type2[1]);
2194
  } else {
2195
+ $m2 = '';
2196
  $t2 = strtolower($type2[0]);
2197
  }
2198
 
2227
  /**
2228
  * Compile import; returns true if the value was something that could be imported
2229
  *
2230
+ * @param array $rawPath
2231
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2232
+ * @param boolean $once
2233
  *
2234
  * @return boolean
2235
  */
2236
+ protected function compileImport($rawPath, OutputBlock $out, $once = false)
2237
  {
2238
  if ($rawPath[0] === Type::T_STRING) {
2239
  $path = $this->compileStringContent($rawPath);
2247
  return true;
2248
  }
2249
 
2250
+ $this->appendRootDirective('@import ' . $this->compileValue($rawPath). ';', $out);
2251
+
2252
  return false;
2253
  }
2254
 
2260
 
2261
  foreach ($rawPath[2] as $path) {
2262
  if ($path[0] !== Type::T_STRING) {
2263
+ $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
2264
+
2265
  return false;
2266
  }
2267
  }
2268
 
2269
  foreach ($rawPath[2] as $path) {
2270
+ $this->compileImport($path, $out, $once);
2271
  }
2272
 
2273
  return true;
2274
  }
2275
 
2276
+ $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
2277
+
2278
  return false;
2279
  }
2280
 
2281
+
2282
+ /**
2283
+ * Append a root directive like @import or @charset as near as the possible from the source code
2284
+ * (keeping before comments, @import and @charset coming before in the source code)
2285
+ *
2286
+ * @param string $line
2287
+ * @param @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2288
+ * @param array $allowed
2289
+ */
2290
+ protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT])
2291
+ {
2292
+ $root = $out;
2293
+
2294
+ while ($root->parent) {
2295
+ $root = $root->parent;
2296
+ }
2297
+
2298
+ $i = 0;
2299
+
2300
+ while ($i < count($root->children)) {
2301
+ if (! isset($root->children[$i]->type) || ! in_array($root->children[$i]->type, $allowed)) {
2302
+ break;
2303
+ }
2304
+
2305
+ $i++;
2306
+ }
2307
+
2308
+ // remove incompatible children from the bottom of the list
2309
+ $saveChildren = [];
2310
+
2311
+ while ($i < count($root->children)) {
2312
+ $saveChildren[] = array_pop($root->children);
2313
+ }
2314
+
2315
+ // insert the directive as a comment
2316
+ $child = $this->makeOutputBlock(Type::T_COMMENT);
2317
+ $child->lines[] = $line;
2318
+ $child->sourceName = $this->sourceNames[$this->sourceIndex];
2319
+ $child->sourceLine = $this->sourceLine;
2320
+ $child->sourceColumn = $this->sourceColumn;
2321
+
2322
+ $root->children[] = $child;
2323
+
2324
+ // repush children
2325
+ while (count($saveChildren)) {
2326
+ $root->children[] = array_pop($saveChildren);
2327
+ }
2328
+ }
2329
+
2330
+ /**
2331
+ * Append lines to the current output block:
2332
+ * directly to the block or through a child if necessary
2333
+ *
2334
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2335
+ * @param string $type
2336
+ * @param string|mixed $line
2337
+ */
2338
+ protected function appendOutputLine(OutputBlock $out, $type, $line)
2339
+ {
2340
+ $outWrite = &$out;
2341
+
2342
+ if ($type === Type::T_COMMENT) {
2343
+ $parent = $out->parent;
2344
+
2345
+ if (end($parent->children) !== $out) {
2346
+ $outWrite = &$parent->children[count($parent->children) - 1];
2347
+ }
2348
+ }
2349
+
2350
+ // check if it's a flat output or not
2351
+ if (count($out->children)) {
2352
+ $lastChild = &$out->children[count($out->children) - 1];
2353
+
2354
+ if ($lastChild->depth === $out->depth && is_null($lastChild->selectors) && ! count($lastChild->children)) {
2355
+ $outWrite = $lastChild;
2356
+ } else {
2357
+ $nextLines = $this->makeOutputBlock($type);
2358
+ $nextLines->parent = $out;
2359
+ $nextLines->depth = $out->depth;
2360
+
2361
+ $out->children[] = $nextLines;
2362
+ $outWrite = &$nextLines;
2363
+ }
2364
+ }
2365
+
2366
+ $outWrite->lines[] = $line;
2367
+ }
2368
+
2369
  /**
2370
  * Compile child; returns a value to halt execution
2371
  *
2372
+ * @param array $child
2373
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2374
  *
2375
  * @return array
2376
  */
2377
  protected function compileChild($child, OutputBlock $out)
2378
  {
2379
+ if (isset($child[Parser::SOURCE_LINE])) {
2380
+ $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
2381
+ $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
2382
+ $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
2383
+ } elseif (is_array($child) && isset($child[1]->sourceLine)) {
2384
+ $this->sourceIndex = $child[1]->sourceIndex;
2385
+ $this->sourceLine = $child[1]->sourceLine;
2386
+ $this->sourceColumn = $child[1]->sourceColumn;
2387
+ } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
2388
+ $this->sourceLine = $out->sourceLine;
2389
+ $this->sourceIndex = array_search($out->sourceName, $this->sourceNames);
2390
+ $this->sourceColumn = $out->sourceColumn;
2391
+
2392
+ if ($this->sourceIndex === false) {
2393
+ $this->sourceIndex = null;
2394
+ }
2395
+ }
2396
 
2397
  switch ($child[0]) {
2398
  case Type::T_SCSSPHP_IMPORT_ONCE:
2399
+ $rawPath = $this->reduce($child[1]);
 
 
2400
 
2401
+ $this->compileImport($rawPath, $out, true);
 
 
2402
  break;
2403
 
2404
  case Type::T_IMPORT:
2405
+ $rawPath = $this->reduce($child[1]);
 
 
2406
 
2407
+ $this->compileImport($rawPath, $out);
 
 
2408
  break;
2409
 
2410
  case Type::T_DIRECTIVE:
2411
+ $this->compileDirective($child[1], $out);
2412
  break;
2413
 
2414
  case Type::T_AT_ROOT:
2426
  case Type::T_CHARSET:
2427
  if (! $this->charsetSeen) {
2428
  $this->charsetSeen = true;
2429
+ $this->appendRootDirective('@charset ' . $this->compileValue($child[1]) . ';', $out);
 
2430
  }
2431
  break;
2432
 
2434
  list(, $name, $value) = $child;
2435
 
2436
  if ($name[0] === Type::T_VARIABLE) {
2437
+ $flags = isset($child[3]) ? $child[3] : [];
2438
  $isDefault = in_array('!default', $flags);
2439
+ $isGlobal = in_array('!global', $flags);
2440
 
2441
  if ($isGlobal) {
2442
+ $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
2443
  break;
2444
  }
2445
 
2446
  $shouldSet = $isDefault &&
2447
+ (is_null($result = $this->get($name[1], false)) ||
2448
+ $result === static::$null);
2449
 
2450
  if (! $isDefault || $shouldSet) {
2451
+ $this->set($name[1], $this->reduce($value), true, null, $value);
2452
  }
2453
  break;
2454
  }
2455
 
2456
  $compiledName = $this->compileValue($name);
2457
 
2458
+ // handle shorthand syntaxes : size / line-height...
2459
+ if (in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) {
2460
+ if ($value[0] === Type::T_VARIABLE) {
2461
+ // if the font value comes from variable, the content is already reduced
2462
+ // (i.e., formulas were already calculated), so we need the original unreduced value
2463
+ $value = $this->get($value[1], true, null, true);
2464
+ }
2465
+
2466
+ $shorthandValue=&$value;
2467
+
2468
+ $shorthandDividerNeedsUnit = false;
2469
+ $maxListElements = null;
2470
+ $maxShorthandDividers = 1;
2471
+
2472
+ switch ($compiledName) {
2473
+ case 'border-radius':
2474
+ $maxListElements = 4;
2475
+ $shorthandDividerNeedsUnit = true;
2476
+ break;
2477
+ }
2478
+
2479
+ if ($compiledName === 'font' and $value[0] === Type::T_LIST && $value[1]==',') {
2480
+ // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica"
2481
+ // we need to handle the first list element
2482
+ $shorthandValue=&$value[2][0];
2483
+ }
2484
+
2485
+ if ($shorthandValue[0] === Type::T_EXPRESSION && $shorthandValue[1] === '/') {
2486
+ $revert = true;
2487
+
2488
+ if ($shorthandDividerNeedsUnit) {
2489
+ $divider = $shorthandValue[3];
2490
+
2491
+ if (is_array($divider)) {
2492
+ $divider = $this->reduce($divider, true);
2493
+ }
2494
+
2495
+ if (intval($divider->dimension) and !count($divider->units)) {
2496
+ $revert = false;
2497
+ }
2498
+ }
2499
+
2500
+ if ($revert) {
2501
+ $shorthandValue = $this->expToString($shorthandValue);
2502
+ }
2503
+ } elseif ($shorthandValue[0] === Type::T_LIST) {
2504
+ foreach ($shorthandValue[2] as &$item) {
2505
  if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
2506
+ if ($maxShorthandDividers > 0) {
2507
+ $revert = true;
2508
+ // if the list of values is too long, this has to be a shorthand,
2509
+ // otherwise it could be a real division
2510
+ if (is_null($maxListElements) or count($shorthandValue[2]) <= $maxListElements) {
2511
+ if ($shorthandDividerNeedsUnit) {
2512
+ $divider = $item[3];
2513
+
2514
+ if (is_array($divider)) {
2515
+ $divider = $this->reduce($divider, true);
2516
+ }
2517
+
2518
+ if (intval($divider->dimension) and !count($divider->units)) {
2519
+ $revert = false;
2520
+ }
2521
+ }
2522
+ }
2523
+
2524
+ if ($revert) {
2525
+ $item = $this->expToString($item);
2526
+ $maxShorthandDividers--;
2527
+ }
2528
+ }
2529
  }
2530
  }
2531
  }
2543
 
2544
  $compiledValue = $this->compileValue($value);
2545
 
2546
+ $line = $this->formatter->property(
2547
  $compiledName,
2548
  $compiledValue
2549
  );
2550
+ $this->appendOutputLine($out, Type::T_ASSIGN, $line);
2551
  break;
2552
 
2553
  case Type::T_COMMENT:
2556
  break;
2557
  }
2558
 
2559
+ $line = $this->compileCommentValue($child, true);
2560
+ $this->appendOutputLine($out, Type::T_COMMENT, $line);
2561
  break;
2562
 
2563
  case Type::T_MIXIN:
2564
  case Type::T_FUNCTION:
2565
  list(, $block) = $child;
2566
+ // the block need to be able to go up to it's parent env to resolve vars
2567
+ $block->parentEnv = $this->getStoreEnv();
2568
+ $this->set(static::$namespaces[$block->type] . $block->name, $block, true);
2569
  break;
2570
 
2571
  case Type::T_EXTEND:
2572
+ foreach ($child[1] as $sel) {
 
 
2573
  $results = $this->evalSelectors([$sel]);
2574
 
2575
  foreach ($results as $result) {
2576
  // only use the first one
2577
  $result = current($result);
2578
+ $selectors = $out->selectors;
2579
 
2580
+ if (! $selectors && isset($child['selfParent'])) {
2581
+ $selectors = $this->multiplySelectors($this->env, $child['selfParent']);
2582
+ }
2583
+
2584
+ $this->pushExtends($result, $selectors, $child);
2585
  }
2586
  }
2587
  break;
2608
  $list = $this->coerceList($this->reduce($each->list));
2609
 
2610
  $this->pushEnv();
2611
+ $storeEnv = $this->storeEnv;
2612
+ $this->storeEnv = $this->env;
2613
 
2614
  foreach ($list[2] as $item) {
2615
  if (count($each->vars) === 1) {
2626
 
2627
  if ($ret) {
2628
  if ($ret[0] !== Type::T_CONTROL) {
2629
+ $store = $this->env->store;
2630
+ $this->storeEnv = $storeEnv;
2631
  $this->popEnv();
2632
+ foreach ($store as $key => $value) {
2633
+ if (!in_array($key, $each->vars)) {
2634
+ $this->set($key, $value, true);
2635
+ }
2636
+ }
2637
 
2638
  return $ret;
2639
  }
2643
  }
2644
  }
2645
  }
2646
+ $store = $this->env->store;
2647
+ $this->storeEnv = $storeEnv;
2648
  $this->popEnv();
2649
+ foreach ($store as $key => $value) {
2650
+ if (!in_array($key, $each->vars)) {
2651
+ $this->set($key, $value, true);
2652
+ }
2653
+ }
2654
+
2655
  break;
2656
 
2657
  case Type::T_WHILE:
2724
  return $this->reduce($child[1], true);
2725
 
2726
  case Type::T_NESTED_PROPERTY:
2727
+ $this->compileNestedPropertiesBlock($child[1], $out);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2728
  break;
2729
 
2730
  case Type::T_INCLUDE:
2731
  // including a mixin
2732
+ list(, $name, $argValues, $content, $argUsing) = $child;
2733
 
2734
  $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
2735
 
2747
  $storeEnv = $this->storeEnv;
2748
  $this->storeEnv = $this->env;
2749
 
2750
+ // Find the parent selectors in the env to be able to know what '&' refers to in the mixin
2751
+ // and assign this fake parent to childs
2752
+ $selfParent = null;
2753
 
2754
+ if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) {
2755
+ $selfParent = $child['selfParent'];
2756
+ } else {
2757
+ $parentSelectors = $this->multiplySelectors($this->env);
2758
 
2759
+ if ($parentSelectors) {
2760
+ $parent = new Block();
2761
+ $parent->selectors = $parentSelectors;
2762
+
2763
+ foreach ($mixin->children as $k => $child) {
2764
+ if (isset($child[1]) && is_object($child[1]) && $child[1] instanceof Block) {
2765
+ $mixin->children[$k][1]->parent = $parent;
2766
+ }
2767
+ }
2768
+ }
2769
+ }
2770
+
2771
+ // clone the stored content to not have its scope spoiled by a further call to the same mixin
2772
+ // i.e., recursive @include of the same mixin
2773
+ if (isset($content)) {
2774
+ $copyContent = clone $content;
2775
+ $copyContent->scope = clone $callingScope;
2776
+
2777
+ $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
2778
+ } else {
2779
+ $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env);
2780
+ }
2781
+
2782
+ // save the "using" argument list for applying it to when "@content" is invoked
2783
+ if (isset($argUsing)) {
2784
+ $this->setRaw(static::$namespaces['special'] . 'using', $argUsing, $this->env);
2785
+ } else {
2786
+ $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env);
2787
+ }
2788
+
2789
+ if (isset($mixin->args)) {
2790
  $this->applyArguments($mixin->args, $argValues);
2791
  }
2792
 
2793
  $this->env->marker = 'mixin';
2794
 
2795
+ if (! empty($mixin->parentEnv)) {
2796
+ $this->env->declarationScopeParent = $mixin->parentEnv;
2797
+ } else {
2798
+ $this->throwError("@mixin $name() without parentEnv");
2799
+ }
2800
+
2801
+ $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . " " . $name);
2802
 
2803
  $this->storeEnv = $storeEnv;
2804
 
2806
  break;
2807
 
2808
  case Type::T_MIXIN_CONTENT:
2809
+ $env = isset($this->storeEnv) ? $this->storeEnv : $this->env;
2810
+ $content = $this->get(static::$namespaces['special'] . 'content', false, $env);
2811
+ $argUsing = $this->get(static::$namespaces['special'] . 'using', false, $env);
2812
+ $argContent = $child[1];
2813
 
2814
  if (! $content) {
2815
  $content = new \stdClass();
2816
+ $content->scope = new \stdClass();
2817
+ $content->children = $env->parent->block->children;
2818
  break;
2819
  }
2820
 
2821
  $storeEnv = $this->storeEnv;
2822
+ $varsUsing = [];
2823
+
2824
+ if (isset($argUsing) && isset($argContent)) {
2825
+ // Get the arguments provided for the content with the names provided in the "using" argument list
2826
+ $this->storeEnv = $this->env;
2827
+ $varsUsing = $this->applyArguments($argUsing, $argContent, false);
2828
+ }
2829
+
2830
+ // restore the scope from the @content
2831
  $this->storeEnv = $content->scope;
2832
 
2833
+ // append the vars from using if any
2834
+ foreach ($varsUsing as $name => $val) {
2835
+ $this->set($name, $val, true, $this->storeEnv);
2836
+ }
2837
+
2838
  $this->compileChildrenNoReturn($content->children, $out);
2839
 
2840
  $this->storeEnv = $storeEnv;
2843
  case Type::T_DEBUG:
2844
  list(, $value) = $child;
2845
 
2846
+ $fname = $this->sourceNames[$this->sourceIndex];
2847
+ $line = $this->sourceLine;
2848
  $value = $this->compileValue($this->reduce($value, true));
2849
+
2850
+ fwrite($this->stderr, "File $fname on line $line DEBUG: $value\n");
2851
  break;
2852
 
2853
  case Type::T_WARN:
2854
  list(, $value) = $child;
2855
 
2856
+ $fname = $this->sourceNames[$this->sourceIndex];
2857
+ $line = $this->sourceLine;
2858
  $value = $this->compileValue($this->reduce($value, true));
2859
+
2860
+ fwrite($this->stderr, "File $fname on line $line WARN: $value\n");
2861
  break;
2862
 
2863
  case Type::T_ERROR:
2864
  list(, $value) = $child;
2865
 
2866
+ $fname = $this->sourceNames[$this->sourceIndex];
2867
+ $line = $this->sourceLine;
2868
  $value = $this->compileValue($this->reduce($value, true));
2869
+
2870
+ $this->throwError("File $fname on line $line ERROR: $value\n");
2871
  break;
2872
 
2873
  case Type::T_CONTROL:
2912
  *
2913
  * @param array $value
2914
  *
2915
+ * @return boolean
2916
  */
2917
  protected function isTruthy($value)
2918
  {
2943
  switch ($value[0]) {
2944
  case Type::T_EXPRESSION:
2945
  if ($value[1] === '/') {
2946
+ return $this->shouldEval($value[2]) || $this->shouldEval($value[3]);
2947
  }
2948
 
2949
  // fall-thru
2961
  * @param array $value
2962
  * @param boolean $inExp
2963
  *
2964
+ * @return null|string|array|\ScssPhp\ScssPhp\Node\Number
2965
  */
2966
  protected function reduce($value, $inExp = false)
2967
  {
2968
+ if (is_null($value)) {
2969
+ return null;
2970
+ }
2971
 
2972
+ switch ($value[0]) {
2973
  case Type::T_EXPRESSION:
2974
  list(, $op, $left, $right, $inParens) = $value;
2975
 
2983
  }
2984
 
2985
  // special case: looks like css shorthand
2986
+ if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2]) &&
2987
+ (($right[0] !== Type::T_NUMBER && $right[2] != '') ||
2988
+ ($right[0] === Type::T_NUMBER && ! $right->unitless()))
2989
  ) {
2990
  return $this->expToString($value);
2991
  }
2992
 
2993
+ $left = $this->coerceForExpression($left);
2994
  $right = $this->coerceForExpression($right);
 
2995
  $ltype = $left[0];
2996
  $rtype = $right[0];
2997
 
3101
  return [Type::T_STRING, '', [$op, $exp]];
3102
 
3103
  case Type::T_VARIABLE:
3104
+ return $this->reduce($this->get($value[1]));
 
 
3105
 
3106
  case Type::T_LIST:
3107
  foreach ($value[2] as &$item) {
3133
  case Type::T_INTERPOLATE:
3134
  $value[1] = $this->reduce($value[1]);
3135
 
3136
+ if ($inExp) {
3137
+ return $value[1];
3138
+ }
3139
+
3140
  return $value;
3141
 
3142
  case Type::T_FUNCTION_CALL:
3143
+ return $this->fncall($value[1], $value[2]);
3144
+
3145
+ case Type::T_SELF:
3146
+ $selfSelector = $this->multiplySelectors($this->env);
3147
+ $selfSelector = $this->collapseSelectors($selfSelector, true);
3148
 
3149
+ return $selfSelector;
3150
 
3151
  default:
3152
  return $value;
3161
  *
3162
  * @return array|null
3163
  */
3164
+ protected function fncall($name, $argValues)
3165
  {
3166
  // SCSS @function
3167
  if ($this->callScssFunction($name, $argValues, $returnValue)) {
3207
  public function normalizeValue($value)
3208
  {
3209
  $value = $this->coerceForExpression($this->reduce($value));
 
3210
 
3211
+ switch ($value[0]) {
3212
  case Type::T_LIST:
3213
  $value = $this->extractInterpolation($value);
3214
 
3220
  $value[2][$key] = $this->normalizeValue($item);
3221
  }
3222
 
3223
+ if (! empty($value['enclosing'])) {
3224
+ unset($value['enclosing']);
3225
+ }
3226
+
3227
  return $value;
3228
 
3229
  case Type::T_STRING:
3230
+ return [$value[0], '"', [$this->compileStringContent($value)]];
3231
 
3232
  case Type::T_NUMBER:
3233
  return $value->normalize();
3246
  * @param array $left
3247
  * @param array $right
3248
  *
3249
+ * @return \ScssPhp\ScssPhp\Node\Number
3250
  */
3251
  protected function opAddNumberNumber($left, $right)
3252
  {
3259
  * @param array $left
3260
  * @param array $right
3261
  *
3262
+ * @return \ScssPhp\ScssPhp\Node\Number
3263
  */
3264
  protected function opMulNumberNumber($left, $right)
3265
  {
3272
  * @param array $left
3273
  * @param array $right
3274
  *
3275
+ * @return \ScssPhp\ScssPhp\Node\Number
3276
  */
3277
  protected function opSubNumberNumber($left, $right)
3278
  {
3285
  * @param array $left
3286
  * @param array $right
3287
  *
3288
+ * @return array|\ScssPhp\ScssPhp\Node\Number
3289
  */
3290
  protected function opDivNumberNumber($left, $right)
3291
  {
3302
  * @param array $left
3303
  * @param array $right
3304
  *
3305
+ * @return \ScssPhp\ScssPhp\Node\Number
3306
  */
3307
  protected function opModNumberNumber($left, $right)
3308
  {
3315
  * @param array $left
3316
  * @param array $right
3317
  *
3318
+ * @return array|null
3319
  */
3320
  protected function opAdd($left, $right)
3321
  {
3338
 
3339
  return $strRight;
3340
  }
3341
+
3342
+ return null;
3343
  }
3344
 
3345
  /**
3349
  * @param array $right
3350
  * @param boolean $shouldEval
3351
  *
3352
+ * @return array|null
3353
  */
3354
  protected function opAnd($left, $right, $shouldEval)
3355
  {
3356
+ $truthy = ($left === static::$null || $right === static::$null) ||
3357
+ ($left === static::$false || $left === static::$true) &&
3358
+ ($right === static::$false || $right === static::$true);
3359
+
3360
  if (! $shouldEval) {
3361
+ if (! $truthy) {
3362
+ return null;
3363
+ }
3364
  }
3365
 
3366
+ if ($left !== static::$false && $left !== static::$null) {
3367
  return $this->reduce($right, true);
3368
  }
3369
 
3377
  * @param array $right
3378
  * @param boolean $shouldEval
3379
  *
3380
+ * @return array|null
3381
  */
3382
  protected function opOr($left, $right, $shouldEval)
3383
  {
3384
+ $truthy = ($left === static::$null || $right === static::$null) ||
3385
+ ($left === static::$false || $left === static::$true) &&
3386
+ ($right === static::$false || $right === static::$true);
3387
+
3388
  if (! $shouldEval) {
3389
+ if (! $truthy) {
3390
+ return null;
3391
+ }
3392
  }
3393
 
3394
+ if ($left !== static::$false && $left !== static::$null) {
3395
  return $left;
3396
  }
3397
 
3602
  * @param array $left
3603
  * @param array $right
3604
  *
3605
+ * @return \ScssPhp\ScssPhp\Node\Number
3606
  */
3607
  protected function opCmpNumberNumber($left, $right)
3608
  {
3640
  *
3641
  * @param array $value
3642
  *
3643
+ * @return string|array
3644
  */
3645
  public function compileValue($value)
3646
  {
3647
  $value = $this->reduce($value);
3648
 
3649
+ switch ($value[0]) {
 
 
3650
  case Type::T_KEYWORD:
3651
  return $value[1];
3652
 
3657
  // [4] - optional alpha component
3658
  list(, $r, $g, $b) = $value;
3659
 
3660
+ $r = $this->compileRGBAValue($r);
3661
+ $g = $this->compileRGBAValue($g);
3662
+ $b = $this->compileRGBAValue($b);
3663
+
3664
+ if (count($value) === 5) {
3665
+ $alpha = $this->compileRGBAValue($value[4], true);
3666
+
3667
+ if (! is_numeric($alpha) || $alpha < 1) {
3668
+ $colorName = Colors::RGBaToColorName($r, $g, $b, $alpha);
3669
+
3670
+ if (! is_null($colorName)) {
3671
+ return $colorName;
3672
+ }
3673
+
3674
+ if (is_numeric($alpha)) {
3675
+ $a = new Node\Number($alpha, '');
3676
+ } else {
3677
+ $a = $alpha;
3678
+ }
3679
+
3680
+ return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
3681
+ }
3682
+ }
3683
+
3684
+ if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b)) {
3685
+ return 'rgb(' . $r . ', ' . $g . ', ' . $b . ')';
3686
+ }
3687
 
3688
+ $colorName = Colors::RGBaToColorName($r, $g, $b);
 
3689
 
3690
+ if (! is_null($colorName)) {
3691
+ return $colorName;
3692
  }
3693
 
3694
  $h = sprintf('#%02x%02x%02x', $r, $g, $b);
3719
  }
3720
 
3721
  list(, $delim, $items) = $value;
3722
+ $pre = $post = "";
3723
+ if (! empty($value['enclosing'])) {
3724
+ switch ($value['enclosing']) {
3725
+ case 'parent':
3726
+ //$pre = "(";
3727
+ //$post = ")";
3728
+ break;
3729
+ case 'forced_parent':
3730
+ $pre = "(";
3731
+ $post = ")";
3732
+ break;
3733
+ case 'bracket':
3734
+ case 'forced_bracket':
3735
+ $pre = "[";
3736
+ $post = "]";
3737
+ break;
3738
+ }
3739
+ }
3740
 
3741
+ $prefix_value = '';
3742
  if ($delim !== ' ') {
3743
+ $prefix_value = ' ';
3744
  }
3745
 
3746
  $filtered = [];
3750
  continue;
3751
  }
3752
 
3753
+ $compiled = $this->compileValue($item);
3754
+ if ($prefix_value && strlen($compiled)) {
3755
+ $compiled = $prefix_value . $compiled;
3756
+ }
3757
+ $filtered[] = $compiled;
3758
  }
3759
 
3760
+ return $pre . substr(implode("$delim", $filtered), strlen($prefix_value)) . $post;
3761
 
3762
  case Type::T_MAP:
3763
+ $keys = $value[1];
3764
+ $values = $value[2];
3765
  $filtered = [];
3766
 
3767
  for ($i = 0, $s = count($keys); $i < $s; $i++) {
3779
  list(, $interpolate, $left, $right) = $value;
3780
  list(,, $whiteLeft, $whiteRight) = $interpolate;
3781
 
3782
+ $delim = $left[1];
3783
+
3784
+ if ($delim && $delim !== ' ' && ! $whiteLeft) {
3785
+ $delim .= ' ';
3786
+ }
3787
+
3788
  $left = count($left[2]) > 0 ?
3789
+ $this->compileValue($left) . $delim . $whiteLeft: '';
3790
+
3791
+ $delim = $right[1];
3792
+
3793
+ if ($delim && $delim !== ' ') {
3794
+ $delim .= ' ';
3795
+ }
3796
 
3797
  $right = count($right[2]) > 0 ?
3798
+ $whiteRight . $delim . $this->compileValue($right) : '';
3799
 
3800
  return $left . $this->compileValue($interpolate) . $right;
3801
 
3802
  case Type::T_INTERPOLATE:
 
 
 
3803
  // strip quotes if it's a string
3804
+ $reduced = $this->reduce($value[1]);
3805
 
3806
  switch ($reduced[0]) {
3807
  case Type::T_LIST:
3825
  }
3826
 
3827
  $temp = $this->compileValue([Type::T_KEYWORD, $item]);
3828
+
3829
  if ($temp[0] === Type::T_STRING) {
3830
  $filtered[] = $this->compileStringContent($temp);
3831
  } elseif ($temp[0] === Type::T_KEYWORD) {
3851
  case Type::T_NULL:
3852
  return 'null';
3853
 
3854
+ case Type::T_COMMENT:
3855
+ return $this->compileCommentValue($value);
3856
+
3857
  default:
3858
+ $this->throwError("unknown value type: ".json_encode($value));
3859
  }
3860
  }
3861
 
3919
  /**
3920
  * Find the final set of selectors
3921
  *
3922
+ * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3923
+ * @param \ScssPhp\ScssPhp\Block $selfParent
3924
  *
3925
  * @return array
3926
  */
3927
+ protected function multiplySelectors(Environment $env, $selfParent = null)
3928
  {
3929
  $envs = $this->compactEnv($env);
3930
  $selectors = [];
3931
  $parentSelectors = [[]];
3932
 
3933
+ $selfParentSelectors = null;
3934
+
3935
+ if (! is_null($selfParent) && $selfParent->selectors) {
3936
+ $selfParentSelectors = $this->evalSelectors($selfParent->selectors);
3937
+ }
3938
+
3939
  while ($env = array_pop($envs)) {
3940
  if (empty($env->selectors)) {
3941
  continue;
3942
  }
3943
 
3944
+ $selectors = $env->selectors;
3945
+
3946
+ do {
3947
+ $stillHasSelf = false;
3948
+ $prevSelectors = $selectors;
3949
+ $selectors = [];
3950
 
3951
+ foreach ($prevSelectors as $selector) {
3952
+ foreach ($parentSelectors as $parent) {
3953
+ if ($selfParentSelectors) {
3954
+ foreach ($selfParentSelectors as $selfParent) {
3955
+ // if no '&' in the selector, each call will give same result, only add once
3956
+ $s = $this->joinSelectors($parent, $selector, $stillHasSelf, $selfParent);
3957
+ $selectors[serialize($s)] = $s;
3958
+ }
3959
+ } else {
3960
+ $s = $this->joinSelectors($parent, $selector, $stillHasSelf);
3961
+ $selectors[serialize($s)] = $s;
3962
+ }
3963
+ }
3964
  }
3965
+ } while ($stillHasSelf);
3966
 
3967
  $parentSelectors = $selectors;
3968
  }
3969
 
3970
+ $selectors = array_values($selectors);
3971
+
3972
  return $selectors;
3973
  }
3974
 
3975
  /**
3976
  * Join selectors; looks for & to replace, or append parent before child
3977
  *
3978
+ * @param array $parent
3979
+ * @param array $child
3980
+ * @param boolean &$stillHasSelf
3981
+ * @param array $selfParentSelectors
3982
+
3983
  * @return array
3984
  */
3985
+ protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null)
3986
  {
3987
  $setSelf = false;
3988
  $out = [];
3991
  $newPart = [];
3992
 
3993
  foreach ($part as $p) {
3994
+ // only replace & once and should be recalled to be able to make combinations
3995
+ if ($p === static::$selfSelector && $setSelf) {
3996
+ $stillHasSelf = true;
3997
+ }
3998
+
3999
+ if ($p === static::$selfSelector && ! $setSelf) {
4000
  $setSelf = true;
4001
 
4002
+ if (is_null($selfParentSelectors)) {
4003
+ $selfParentSelectors = $parent;
4004
+ }
4005
+
4006
+ foreach ($selfParentSelectors as $i => $parentPart) {
4007
  if ($i > 0) {
4008
  $out[] = $newPart;
4009
  $newPart = [];
4010
  }
4011
 
4012
  foreach ($parentPart as $pp) {
4013
+ if (is_array($pp)) {
4014
+ $flatten = [];
4015
+
4016
+ array_walk_recursive($pp, function ($a) use (&$flatten) {
4017
+ $flatten[] = $a;
4018
+ });
4019
+
4020
+ $pp = implode($flatten);
4021
+ }
4022
+
4023
  $newPart[] = $pp;
4024
  }
4025
  }
4037
  /**
4038
  * Multiply media
4039
  *
4040
+ * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4041
+ * @param array $childQueries
4042
  *
4043
  * @return array
4044
  */
4059
  ? $env->block->queryList
4060
  : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
4061
 
4062
+ $store = [$this->env, $this->storeEnv];
4063
+
4064
+ $this->env = $env;
4065
+ $this->storeEnv = null;
4066
+ $parentQueries = $this->evaluateMediaQuery($parentQueries);
4067
+
4068
+ list($this->env, $this->storeEnv) = $store;
4069
+
4070
+ if (is_null($childQueries)) {
4071
  $childQueries = $parentQueries;
4072
  } else {
4073
  $originalQueries = $childQueries;
4075
 
4076
  foreach ($parentQueries as $parentQuery) {
4077
  foreach ($originalQueries as $childQuery) {
4078
+ $childQueries[] = array_merge(
4079
+ $parentQuery,
4080
+ [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]],
4081
+ $childQuery
4082
+ );
4083
  }
4084
  }
4085
  }
4090
  /**
4091
  * Convert env linked list to stack
4092
  *
4093
+ * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4094
  *
4095
  * @return array
4096
  */
4097
+ protected function compactEnv(Environment $env)
4098
  {
4099
  for ($envs = []; $env; $env = $env->parent) {
4100
  $envs[] = $env;
4108
  *
4109
  * @param array $envs
4110
  *
4111
+ * @return \ScssPhp\ScssPhp\Compiler\Environment
4112
  */
4113
+ protected function extractEnv($envs)
4114
  {
4115
  for ($env = null; $e = array_pop($envs);) {
4116
  $e->parent = $env;
4123
  /**
4124
  * Push environment
4125
  *
4126
+ * @param \ScssPhp\ScssPhp\Block $block
4127
  *
4128
+ * @return \ScssPhp\ScssPhp\Compiler\Environment
4129
  */
4130
  protected function pushEnv(Block $block = null)
4131
  {
4151
  /**
4152
  * Get store environment
4153
  *
4154
+ * @return \ScssPhp\ScssPhp\Compiler\Environment
4155
  */
4156
  protected function getStoreEnv()
4157
  {
4161
  /**
4162
  * Set variable
4163
  *
4164
+ * @param string $name
4165
+ * @param mixed $value
4166
+ * @param boolean $shadow
4167
+ * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4168
+ * @param mixed $valueUnreduced
4169
  */
4170
+ protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null)
4171
  {
4172
  $name = $this->normalizeName($name);
4173
 
4176
  }
4177
 
4178
  if ($shadow) {
4179
+ $this->setRaw($name, $value, $env, $valueUnreduced);
4180
  } else {
4181
+ $this->setExisting($name, $value, $env, $valueUnreduced);
4182
  }
4183
  }
4184
 
4185
  /**
4186
  * Set existing variable
4187
  *
4188
+ * @param string $name
4189
+ * @param mixed $value
4190
+ * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4191
+ * @param mixed $valueUnreduced
4192
  */
4193
+ protected function setExisting($name, $value, Environment $env, $valueUnreduced = null)
4194
  {
4195
  $storeEnv = $env;
4196
+ $specialContentKey = static::$namespaces['special'] . 'content';
4197
 
4198
  $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
4199
 
4203
  }
4204
 
4205
  if (! $hasNamespace && isset($env->marker)) {
4206
+ if (! empty($env->store[$specialContentKey])) {
4207
+ $env = $env->store[$specialContentKey]->scope;
4208
+ continue;
4209
+ }
4210
+
4211
+ if (! empty($env->declarationScopeParent)) {
4212
+ $env = $env->declarationScopeParent;
4213
+ continue;
4214
+ } else {
4215
+ $env = $storeEnv;
4216
+ break;
4217
+ }
4218
  }
4219
 
4220
  if (! isset($env->parent)) {
4226
  }
4227
 
4228
  $env->store[$name] = $value;
4229
+
4230
+ if ($valueUnreduced) {
4231
+ $env->storeUnreduced[$name] = $valueUnreduced;
4232
+ }
4233
  }
4234
 
4235
  /**
4236
  * Set raw variable
4237
  *
4238
+ * @param string $name
4239
+ * @param mixed $value
4240
+ * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4241
+ * @param mixed $valueUnreduced
4242
  */
4243
+ protected function setRaw($name, $value, Environment $env, $valueUnreduced = null)
4244
  {
4245
  $env->store[$name] = $value;
4246
+
4247
+ if ($valueUnreduced) {
4248
+ $env->storeUnreduced[$name] = $valueUnreduced;
4249
+ }
4250
  }
4251
 
4252
  /**
4254
  *
4255
  * @api
4256
  *
4257
+ * @param string $name
4258
+ * @param boolean $shouldThrow
4259
+ * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4260
+ * @param boolean $unreduced
4261
  *
4262
+ * @return mixed|null
4263
  */
4264
+ public function get($name, $shouldThrow = true, Environment $env = null, $unreduced = false)
4265
  {
4266
  $normalizedName = $this->normalizeName($name);
4267
  $specialContentKey = static::$namespaces['special'] . 'content';
4270
  $env = $this->getStoreEnv();
4271
  }
4272
 
 
4273
  $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
4274
 
4275
+ $maxDepth = 10000;
4276
+
4277
  for (;;) {
4278
+ if ($maxDepth-- <= 0) {
4279
+ break;
4280
+ }
4281
+
4282
  if (array_key_exists($normalizedName, $env->store)) {
4283
+ if ($unreduced && isset($env->storeUnreduced[$normalizedName])) {
4284
+ return $env->storeUnreduced[$normalizedName];
4285
+ }
4286
+
4287
  return $env->store[$normalizedName];
4288
  }
4289
 
4290
  if (! $hasNamespace && isset($env->marker)) {
4291
+ if (! empty($env->store[$specialContentKey])) {
4292
  $env = $env->store[$specialContentKey]->scope;
 
4293
  continue;
4294
  }
4295
 
4296
+ if (! empty($env->declarationScopeParent)) {
4297
+ $env = $env->declarationScopeParent;
4298
+ } else {
4299
+ $env = $this->rootEnv;
4300
+ }
4301
  continue;
4302
  }
4303
 
4309
  }
4310
 
4311
  if ($shouldThrow) {
4312
+ $this->throwError("Undefined variable \$$name" . ($maxDepth <= 0 ? " (infinite recursion)" : ""));
4313
  }
4314
 
4315
  // found nothing
4316
+ return null;
4317
  }
4318
 
4319
  /**
4320
  * Has variable?
4321
  *
4322
+ * @param string $name
4323
+ * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4324
  *
4325
  * @return boolean
4326
  */
4327
  protected function has($name, Environment $env = null)
4328
  {
4329
+ return ! is_null($this->get($name, false, $env));
4330
  }
4331
 
4332
  /**
4400
  */
4401
  public function addParsedFile($path)
4402
  {
4403
+ if (isset($path) && is_file($path)) {
4404
  $this->parsedFiles[realpath($path)] = filemtime($path);
4405
  }
4406
  }
4422
  *
4423
  * @api
4424
  *
4425
+ * @param string|callable $path
4426
  */
4427
  public function addImportPath($path)
4428
  {
4544
  /**
4545
  * Import file
4546
  *
4547
+ * @param string $path
4548
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
4549
  */
4550
+ protected function importFile($path, OutputBlock $out)
4551
  {
4552
  // see if tree is cached
4553
  $realPath = realpath($path);
4565
  }
4566
 
4567
  $pi = pathinfo($path);
4568
+
4569
  array_unshift($this->importPaths, $pi['dirname']);
4570
  $this->compileChildrenNoReturn($tree->children, $out);
4571
  array_shift($this->importPaths);
4585
  $urls = [];
4586
 
4587
  // for "normal" scss imports (ignore vanilla css and external requests)
4588
+ if (! preg_match('~\.css$|^https?://~', $url)) {
4589
  // try both normal and the _partial filename
4590
+ $urls = [$url, preg_replace('~[^/]+$~', '_\0', $url)];
4591
  }
4592
 
4593
  $hasExtension = preg_match('/[.]s?css$/', $url);
4596
  if (is_string($dir)) {
4597
  // check urls for normal import paths
4598
  foreach ($urls as $full) {
4599
+ $separator = (
4600
+ ! empty($dir) &&
4601
+ substr($dir, -1) !== '/' &&
4602
+ substr($full, 0, 1) !== '/'
4603
+ ) ? '/' : '';
4604
+ $full = $dir . $separator . $full;
4605
+
4606
+ if (is_file($file = $full . '.scss') ||
4607
+ ($hasExtension && is_file($file = $full))
4608
  ) {
4609
  return $file;
4610
  }
4613
  // check custom callback for import path
4614
  $file = call_user_func($dir, $url);
4615
 
4616
+ if (! is_null($file)) {
4617
  return $file;
4618
  }
4619
  }
4641
  *
4642
  * @param boolean $ignoreErrors
4643
  *
4644
+ * @return \ScssPhp\ScssPhp\Compiler
4645
  */
4646
  public function setIgnoreErrors($ignoreErrors)
4647
  {
4648
  $this->ignoreErrors = $ignoreErrors;
4649
+
4650
+ return $this;
4651
  }
4652
 
4653
  /**
4657
  *
4658
  * @param string $msg Message with optional sprintf()-style vararg parameters
4659
  *
4660
+ * @throws \ScssPhp\ScssPhp\Exception\CompilerException
4661
  */
4662
  public function throwError($msg)
4663
  {
4665
  return;
4666
  }
4667
 
4668
+ $line = $this->sourceLine;
4669
+ $column = $this->sourceColumn;
4670
+
4671
+ $loc = isset($this->sourceNames[$this->sourceIndex])
4672
+ ? $this->sourceNames[$this->sourceIndex] . " on line $line, at column $column"
4673
+ : "line: $line, column: $column";
4674
+
4675
  if (func_num_args() > 1) {
4676
  $msg = call_user_func_array('sprintf', func_get_args());
4677
  }
4678
 
4679
+ $msg = "$msg: $loc";
4680
+
4681
+ $callStackMsg = $this->callStackMessage();
4682
+
4683
+ if ($callStackMsg) {
4684
+ $msg .= "\nCall Stack:\n" . $callStackMsg;
4685
+ }
4686
 
4687
  throw new CompilerException($msg);
4688
  }
4689
 
4690
+ /**
4691
+ * Beautify call stack for output
4692
+ *
4693
+ * @param boolean $all
4694
+ * @param null $limit
4695
+ *
4696
+ * @return string
4697
+ */
4698
+ protected function callStackMessage($all = false, $limit = null)
4699
+ {
4700
+ $callStackMsg = [];
4701
+ $ncall = 0;
4702
+
4703
+ if ($this->callStack) {
4704
+ foreach (array_reverse($this->callStack) as $call) {
4705
+ if ($all || (isset($call['n']) && $call['n'])) {
4706
+ $msg = "#" . $ncall++ . " " . $call['n'] . " ";
4707
+ $msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]])
4708
+ ? $this->sourceNames[$call[Parser::SOURCE_INDEX]]
4709
+ : '(unknown file)');
4710
+ $msg .= " on line " . $call[Parser::SOURCE_LINE];
4711
+
4712
+ $callStackMsg[] = $msg;
4713
+
4714
+ if (! is_null($limit) && $ncall > $limit) {
4715
+ break;
4716
+ }
4717
+ }
4718
+ }
4719
+ }
4720
+
4721
+ return implode("\n", $callStackMsg);
4722
+ }
4723
+
4724
  /**
4725
  * Handle import loop
4726
  *
4731
  protected function handleImportLoop($name)
4732
  {
4733
  for ($env = $this->env; $env; $env = $env->parent) {
4734
+ if (! $env->block) {
4735
+ continue;
4736
+ }
4737
+
4738
  $file = $this->sourceNames[$env->block->sourceIndex];
4739
 
4740
  if (realpath($file) === $name) {
4744
  }
4745
  }
4746
 
 
 
 
 
 
 
 
 
 
 
 
 
4747
  /**
4748
  * Call SCSS @function
4749
  *
4777
  $tmp->children = [];
4778
 
4779
  $this->env->marker = 'function';
4780
+ if (! empty($func->parentEnv)) {
4781
+ $this->env->declarationScopeParent = $func->parentEnv;
4782
+ } else {
4783
+ $this->throwError("@function $name() without parentEnv");
4784
+ }
4785
 
4786
+ $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . " " . $name);
4787
 
4788
  $this->storeEnv = $storeEnv;
4789
 
4818
  return false;
4819
  }
4820
 
4821
+ @list($sorted, $kwargs) = $this->sortNativeFunctionArgs($libName, $prototype, $args);
4822
 
4823
  if ($name !== 'if' && $name !== 'call') {
4824
+ $inExp = true;
4825
+
4826
+ if ($name === 'join') {
4827
+ $inExp = false;
4828
+ }
4829
+
4830
  foreach ($sorted as &$val) {
4831
+ $val = $this->reduce($val, $inExp);
4832
  }
4833
  }
4834
 
4866
  /**
4867
  * Sorts keyword arguments
4868
  *
4869
+ * @param string $functionName
4870
+ * @param array $prototypes
4871
+ * @param array $args
4872
  *
4873
  * @return array
4874
  */
4875
+ protected function sortNativeFunctionArgs($functionName, $prototypes, $args)
4876
  {
4877
+ static $parser = null;
 
4878
 
4879
+ if (! isset($prototypes)) {
4880
+ $keyArgs = [];
4881
+ $posArgs = [];
4882
+
4883
+ // separate positional and keyword arguments
4884
+ foreach ($args as $arg) {
4885
+ list($key, $value) = $arg;
4886
 
4887
+ $key = $key[1];
4888
 
4889
+ if (empty($key)) {
4890
+ $posArgs[] = empty($arg[2]) ? $value : $arg;
4891
+ } else {
4892
+ $keyArgs[$key] = $value;
4893
+ }
4894
  }
 
4895
 
 
4896
  return [$posArgs, $keyArgs];
4897
  }
4898
 
4899
+ // specific cases ?
4900
+ if (in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) {
4901
+ // notation 100 127 255 / 0 is in fact a simple list of 4 values
4902
+ foreach ($args as $k => $arg) {
4903
+ if ($arg[1][0] === Type::T_LIST && count($arg[1][2]) === 3) {
4904
+ $last = end($arg[1][2]);
4905
+
4906
+ if ($last[0] === Type::T_EXPRESSION && $last[1] === '/') {
4907
+ array_pop($arg[1][2]);
4908
+ $arg[1][2][] = $last[2];
4909
+ $arg[1][2][] = $last[3];
4910
+ $args[$k] = $arg;
4911
+ }
4912
+ }
4913
+ }
4914
+ }
4915
+
4916
+ $finalArgs = [];
4917
+
4918
+ if (! is_array(reset($prototypes))) {
4919
+ $prototypes = [$prototypes];
4920
+ }
4921
+
4922
+ $keyArgs = [];
4923
+
4924
+ // trying each prototypes
4925
+ $prototypeHasMatch = false;
4926
+ $exceptionMessage = '';
4927
+
4928
+ foreach ($prototypes as $prototype) {
4929
+ $argDef = [];
4930
+
4931
+ foreach ($prototype as $i => $p) {
4932
+ $default = null;
4933
+ $p = explode(':', $p, 2);
4934
+ $name = array_shift($p);
4935
+
4936
+ if (count($p)) {
4937
+ $p = trim(reset($p));
4938
+
4939
+ if ($p === 'null') {
4940
+ // differentiate this null from the static::$null
4941
+ $default = [Type::T_KEYWORD, 'null'];
4942
+ } else {
4943
+ if (is_null($parser)) {
4944
+ $parser = $this->parserFactory(__METHOD__);
4945
+ }
4946
+
4947
+ $parser->parseValue($p, $default);
4948
+ }
4949
+ }
4950
+
4951
+ $isVariable = false;
4952
+
4953
+ if (substr($name, -3) === '...') {
4954
+ $isVariable = true;
4955
+ $name = substr($name, 0, -3);
4956
+ }
4957
+
4958
+ $argDef[] = [$name, $default, $isVariable];
4959
+ }
4960
+
4961
+ try {
4962
+ $vars = $this->applyArguments($argDef, $args, false, false);
4963
+
4964
+ // ensure all args are populated
4965
+ foreach ($prototype as $i => $p) {
4966
+ $name = explode(':', $p)[0];
4967
+
4968
+ if (! isset($finalArgs[$i])) {
4969
+ $finalArgs[$i] = null;
4970
+ }
4971
+ }
4972
+
4973
+ // apply positional args
4974
+ foreach (array_values($vars) as $i => $val) {
4975
+ $finalArgs[$i] = $val;
4976
+ }
4977
+
4978
+ $keyArgs = array_merge($keyArgs, $vars);
4979
+ $prototypeHasMatch = true;
4980
+
4981
+ // overwrite positional args with keyword args
4982
+ foreach ($prototype as $i => $p) {
4983
+ $name = explode(':', $p)[0];
4984
+
4985
+ if (isset($keyArgs[$name])) {
4986
+ $finalArgs[$i] = $keyArgs[$name];
4987
+ }
4988
 
4989
+ // special null value as default: translate to real null here
4990
+ if ($finalArgs[$i] === [Type::T_KEYWORD, 'null']) {
4991
+ $finalArgs[$i] = null;
4992
+ }
 
4993
  }
4994
+ // should we break if this prototype seems fulfilled?
4995
+ } catch (CompilerException $e) {
4996
+ $exceptionMessage = $e->getMessage();
4997
  }
4998
  }
4999
 
5000
+ if ($exceptionMessage && ! $prototypeHasMatch) {
5001
+ $this->throwError($exceptionMessage);
5002
+ }
5003
+
5004
  return [$finalArgs, $keyArgs];
5005
  }
5006
 
5007
  /**
5008
  * Apply argument values per definition
5009
  *
5010
+ * @param array $argDef
5011
+ * @param array $argValues
5012
+ * @param boolean $storeInEnv
5013
+ * @param boolean $reduce
5014
+ * only used if $storeInEnv = false
5015
+ *
5016
+ * @return array
5017
  *
5018
  * @throws \Exception
5019
  */
5020
+ protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true)
5021
  {
5022
+ $output = [];
5023
+ if (is_array($argValues) && count($argValues) && end($argValues) === static::$null) {
5024
+ array_pop($argValues);
5025
+ }
5026
 
5027
+ if ($storeInEnv) {
5028
+ $storeEnv = $this->getStoreEnv();
5029
+
5030
+ $env = new Environment;
5031
+ $env->store = $storeEnv->store;
5032
+ }
5033
 
5034
  $hasVariable = false;
5035
  $args = [];
5041
  $hasVariable |= $isVariable;
5042
  }
5043
 
5044
+ $splatSeparator = null;
5045
+ $keywordArgs = [];
5046
  $deferredKeywordArgs = [];
5047
+ $remaining = [];
5048
+ $hasKeywordArgument = false;
5049
 
5050
  // assign the keyword args
5051
  foreach ((array) $argValues as $arg) {
5052
  if (! empty($arg[0])) {
5053
+ $hasKeywordArgument = true;
5054
+
5055
+ $name = $arg[0][1];
5056
+ if (! isset($args[$name])) {
5057
+ foreach (array_keys($args) as $an) {
5058
+ if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
5059
+ $name = $an;
5060
+ break;
5061
+ }
5062
+ }
5063
+ }
5064
+ if (! isset($args[$name]) || $args[$name][3]) {
5065
  if ($hasVariable) {
5066
+ $deferredKeywordArgs[$name] = $arg[1];
5067
  } else {
5068
  $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
5069
  break;
5070
  }
5071
+ } elseif ($args[$name][0] < count($remaining)) {
5072
  $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
5073
  break;
5074
  } else {
5075
+ $keywordArgs[$name] = $arg[1];
5076
  }
 
 
 
5077
  } elseif ($arg[2] === true) {
5078
  $val = $this->reduce($arg[1], true);
5079
 
5080
  if ($val[0] === Type::T_LIST) {
5081
  foreach ($val[2] as $name => $item) {
5082
  if (! is_numeric($name)) {
5083
+ if (! isset($args[$name])) {
5084
+ foreach (array_keys($args) as $an) {
5085
+ if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
5086
+ $name = $an;
5087
+ break;
5088
+ }
5089
+ }
5090
+ }
5091
+
5092
+ if ($hasVariable) {
5093
+ $deferredKeywordArgs[$name] = $item;
5094
+ } else {
5095
+ $keywordArgs[$name] = $item;
5096
+ }
5097
  } else {
5098
+ if (is_null($splatSeparator)) {
5099
+ $splatSeparator = $val[1];
5100
+ }
5101
+
5102
  $remaining[] = $item;
5103
  }
5104
  }
5108
  $item = $val[2][$i];
5109
 
5110
  if (! is_numeric($name)) {
5111
+ if (! isset($args[$name])) {
5112
+ foreach (array_keys($args) as $an) {
5113
+ if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
5114
+ $name = $an;
5115
+ break;
5116
+ }
5117
+ }
5118
+ }
5119
+
5120
+ if ($hasVariable) {
5121
+ $deferredKeywordArgs[$name] = $item;
5122
+ } else {
5123
+ $keywordArgs[$name] = $item;
5124
+ }
5125
+ } else {
5126
+ if (is_null($splatSeparator)) {
5127
+ $splatSeparator = $val[1];
5128
+ }
5129
+
5130
+ $remaining[] = $item;
5131
+ }
5132
+ }
5133
  } else {
5134
  $remaining[] = $val;
5135
  }
5136
+ } elseif ($hasKeywordArgument) {
5137
+ $this->throwError('Positional arguments must come before keyword arguments.');
5138
+ break;
5139
  } else {
5140
  $remaining[] = $arg[1];
5141
  }
5145
  list($i, $name, $default, $isVariable) = $arg;
5146
 
5147
  if ($isVariable) {
5148
+ $val = [Type::T_LIST, is_null($splatSeparator) ? ',' : $splatSeparator , [], $isVariable];
5149
 
5150
  for ($count = count($remaining); $i < $count; $i++) {
5151
  $val[2][] = $remaining[$i];
5165
  break;
5166
  }
5167
 
5168
+ if ($storeInEnv) {
5169
+ $this->set($name, $this->reduce($val, true), true, $env);
5170
+ } else {
5171
+ $output[$name] = ($reduce ? $this->reduce($val, true) : $val);
5172
+ }
5173
  }
5174
 
5175
+ if ($storeInEnv) {
5176
+ $storeEnv->store = $env->store;
5177
+ }
5178
 
5179
  foreach ($args as $arg) {
5180
  list($i, $name, $default, $isVariable) = $arg;
5183
  continue;
5184
  }
5185
 
5186
+ if ($storeInEnv) {
5187
+ $this->set($name, $this->reduce($default, true), true);
5188
+ } else {
5189
+ $output[$name] = ($reduce ? $this->reduce($default, true) : $default);
5190
+ }
5191
  }
5192
+
5193
+ return $output;
5194
  }
5195
 
5196
  /**
5198
  *
5199
  * @param mixed $value
5200
  *
5201
+ * @return array|\ScssPhp\ScssPhp\Node\Number
5202
  */
5203
+ protected function coerceValue($value)
5204
  {
5205
  if (is_array($value) || $value instanceof \ArrayAccess) {
5206
  return $value;
5210
  return $this->toBool($value);
5211
  }
5212
 
5213
+ if (is_null($value)) {
5214
  return static::$null;
5215
  }
5216
 
5222
  return static::$emptyString;
5223
  }
5224
 
5225
+ $value = [Type::T_KEYWORD, $value];
5226
+ $color = $this->coerceColor($value);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5227
 
5228
+ if ($color) {
5229
  return $color;
5230
  }
5231
 
5232
+ return $value;
5233
  }
5234
 
5235
  /**
5245
  return $item;
5246
  }
5247
 
5248
+ if ($item[0] === static::$emptyList[0]
5249
+ && $item[1] === static::$emptyList[1]
5250
+ && $item[2] === static::$emptyList[2]) {
5251
  return static::$emptyMap;
5252
  }
5253
 
5277
  $key = $keys[$i];
5278
  $value = $values[$i];
5279
 
5280
+ switch ($key[0]) {
5281
+ case Type::T_LIST:
5282
+ case Type::T_MAP:
5283
+ case Type::T_STRING:
5284
+ break;
5285
+
5286
+ default:
5287
+ $key = [Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))];
5288
+ break;
5289
+ }
5290
+
5291
  $list[] = [
5292
  Type::T_LIST,
5293
  '',
5294
+ [$key, $value]
5295
  ];
5296
  }
5297
 
5324
  *
5325
  * @return array|null
5326
  */
5327
+ protected function coerceColor($value, $inRGBFunction = false)
5328
  {
5329
  switch ($value[0]) {
5330
  case Type::T_COLOR:
5331
+ for ($i = 1; $i <= 3; $i++) {
5332
+ if (! is_numeric($value[$i])) {
5333
+ $cv = $this->compileRGBAValue($value[$i]);
5334
+
5335
+ if (! is_numeric($cv)) {
5336
+ return null;
5337
+ }
5338
+
5339
+ $value[$i] = $cv;
5340
+ }
5341
+
5342
+ if (isset($value[4])) {
5343
+ if (! is_numeric($value[4])) {
5344
+ $cv = $this->compileRGBAValue($value[4], true);
5345
+
5346
+ if (! is_numeric($cv)) {
5347
+ return null;
5348
+ }
5349
+
5350
+ $value[4] = $cv;
5351
+ }
5352
+ }
5353
+ }
5354
+
5355
  return $value;
5356
 
5357
+ case Type::T_LIST:
5358
+ if ($inRGBFunction) {
5359
+ if (count($value[2]) == 3 || count($value[2]) == 4) {
5360
+ $color = $value[2];
5361
+ array_unshift($color, Type::T_COLOR);
5362
+
5363
+ return $this->coerceColor($color);
5364
+ }
5365
+ }
5366
+
5367
+ return null;
5368
+
5369
  case Type::T_KEYWORD:
5370
+ if (! is_string($value[1])) {
5371
+ return null;
5372
+ }
5373
+
5374
  $name = strtolower($value[1]);
5375
+ // hexa color?
5376
+ if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) {
5377
+ $nofValues = strlen($m[1]);
5378
+
5379
+ if (in_array($nofValues, [3, 4, 6, 8])) {
5380
+ $nbChannels = 3;
5381
+ $color = [];
5382
+ $num = hexdec($m[1]);
5383
+
5384
+ switch ($nofValues) {
5385
+ case 4:
5386
+ $nbChannels = 4;
5387
+ // then continuing with the case 3:
5388
+ case 3:
5389
+ for ($i = 0; $i < $nbChannels; $i++) {
5390
+ $t = $num & 0xf;
5391
+ array_unshift($color, $t << 4 | $t);
5392
+ $num >>= 4;
5393
+ }
5394
+
5395
+ break;
5396
+
5397
+ case 8:
5398
+ $nbChannels = 4;
5399
+ // then continuing with the case 6:
5400
+ case 6:
5401
+ for ($i = 0; $i < $nbChannels; $i++) {
5402
+ array_unshift($color, $num & 0xff);
5403
+ $num >>= 8;
5404
+ }
5405
+
5406
+ break;
5407
+ }
5408
 
5409
+ if ($nbChannels === 4) {
5410
+ if ($color[3] === 255) {
5411
+ $color[3] = 1; // fully opaque
5412
+ } else {
5413
+ $color[3] = round($color[3] / 255, 3);
5414
+ }
5415
+ }
5416
+
5417
+ array_unshift($color, Type::T_COLOR);
5418
 
5419
+ return $color;
5420
+ }
5421
+ }
5422
+
5423
+ if ($rgba = Colors::colorNameToRGBa($name)) {
5424
  return isset($rgba[3])
5425
+ ? [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2], $rgba[3]]
5426
+ : [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2]];
5427
  }
5428
 
5429
  return null;
5432
  return null;
5433
  }
5434
 
5435
+ /**
5436
+ * @param integer|\ScssPhp\ScssPhp\Node\Number $value
5437
+ * @param boolean $isAlpha
5438
+ *
5439
+ * @return integer|mixed
5440
+ */
5441
+ protected function compileRGBAValue($value, $isAlpha = false)
5442
+ {
5443
+ if ($isAlpha) {
5444
+ return $this->compileColorPartValue($value, 0, 1, false);
5445
+ }
5446
+
5447
+ return $this->compileColorPartValue($value, 0, 255, true);
5448
+ }
5449
+
5450
+ /**
5451
+ * @param mixed $value
5452
+ * @param integer|float $min
5453
+ * @param integer|float $max
5454
+ * @param boolean $isInt
5455
+ * @param boolean $clamp
5456
+ * @param boolean $modulo
5457
+ *
5458
+ * @return integer|mixed
5459
+ */
5460
+ protected function compileColorPartValue($value, $min, $max, $isInt = true, $clamp = true, $modulo = false)
5461
+ {
5462
+ if (! is_numeric($value)) {
5463
+ if (is_array($value)) {
5464
+ $reduced = $this->reduce($value);
5465
+
5466
+ if (is_object($reduced) && $value->type === Type::T_NUMBER) {
5467
+ $value = $reduced;
5468
+ }
5469
+ }
5470
+
5471
+ if (is_object($value) && $value->type === Type::T_NUMBER) {
5472
+ $num = $value->dimension;
5473
+
5474
+ if (count($value->units)) {
5475
+ $unit = array_keys($value->units);
5476
+ $unit = reset($unit);
5477
+
5478
+ switch ($unit) {
5479
+ case '%':
5480
+ $num *= $max / 100;
5481
+ break;
5482
+ default:
5483
+ break;
5484
+ }
5485
+ }
5486
+
5487
+ $value = $num;
5488
+ } elseif (is_array($value)) {
5489
+ $value = $this->compileValue($value);
5490
+ }
5491
+ }
5492
+
5493
+ if (is_numeric($value)) {
5494
+ if ($isInt) {
5495
+ $value = round($value);
5496
+ }
5497
+
5498
+ if ($clamp) {
5499
+ $value = min($max, max($min, $value));
5500
+ }
5501
+
5502
+ if ($modulo) {
5503
+ $value = $value % $max;
5504
+
5505
+ // still negative?
5506
+ while ($value < $min) {
5507
+ $value += $max;
5508
+ }
5509
+ }
5510
+
5511
+ return $value;
5512
+ }
5513
+
5514
+ return $value;
5515
+ }
5516
+
5517
  /**
5518
  * Coerce value to string
5519
  *
5566
  $value = $this->coerceMap($value);
5567
 
5568
  if ($value[0] !== Type::T_MAP) {
5569
+ $this->throwError('expecting map, %s received', $value[0]);
5570
  }
5571
 
5572
  return $value;
5586
  public function assertList($value)
5587
  {
5588
  if ($value[0] !== Type::T_LIST) {
5589
+ $this->throwError('expecting list, %s received', $value[0]);
5590
  }
5591
 
5592
  return $value;
5609
  return $color;
5610
  }
5611
 
5612
+ $this->throwError('expecting color, %s received', $value[0]);
5613
  }
5614
 
5615
  /**
5626
  public function assertNumber($value)
5627
  {
5628
  if ($value[0] !== Type::T_NUMBER) {
5629
+ $this->throwError('expecting number, %s received', $value[0]);
5630
  }
5631
 
5632
  return $value[1];
5703
  *
5704
  * @return float
5705
  */
5706
+ protected function hueToRGB($m1, $m2, $h)
5707
  {
5708
  if ($h < 0) {
5709
  $h += 1;
5761
 
5762
  // Built in functions
5763
 
5764
+ protected static $libCall = ['name', 'args...'];
5765
  protected function libCall($args, $kwargs)
5766
  {
5767
  $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
5768
+ $callArgs = [];
5769
 
5770
+ // $kwargs['args'] is [Type::T_LIST, ',', [..]]
5771
+ foreach ($kwargs['args'][2] as $varname => $arg) {
5772
+ if (is_numeric($varname)) {
5773
+ $varname = null;
5774
+ } else {
5775
+ $varname = [ 'var', $varname];
 
 
 
 
5776
  }
5777
+
5778
+ $callArgs[] = [$varname, $arg, false];
5779
  }
5780
 
5781
+ return $this->reduce([Type::T_FUNCTION_CALL, $name, $callArgs]);
5782
  }
5783
 
5784
+ protected static $libIf = ['condition', 'if-true', 'if-false:'];
5785
  protected function libIf($args)
5786
  {
5787
  list($cond, $t, $f) = $args;
5825
  return false === $key ? static::$null : $key + 1;
5826
  }
5827
 
5828
+ protected static $libRgb = [
5829
+ ['color'],
5830
+ ['color', 'alpha'],
5831
+ ['channels'],
5832
+ ['red', 'green', 'blue'],
5833
+ ['red', 'green', 'blue', 'alpha'] ];
5834
+ protected function libRgb($args, $kwargs, $funcName = 'rgb')
5835
  {
5836
+ switch (count($args)) {
5837
+ case 1:
5838
+ if (! $color = $this->coerceColor($args[0], true)) {
5839
+ $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
5840
+ }
5841
+ break;
5842
 
5843
+ case 3:
5844
+ $color = [Type::T_COLOR, $args[0], $args[1], $args[2]];
5845
 
5846
+ if (! $color = $this->coerceColor($color)) {
5847
+ $color = [Type::T_STRING, '', [$funcName .'(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
5848
+ }
 
 
 
 
 
 
5849
 
5850
+ return $color;
5851
+
5852
+ case 2:
5853
+ if ($color = $this->coerceColor($args[0], true)) {
5854
+ $alpha = $this->compileRGBAValue($args[1], true);
5855
+
5856
+ if (is_numeric($alpha)) {
5857
+ $color[4] = $alpha;
5858
+ } else {
5859
+ $color = [Type::T_STRING, '',
5860
+ [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']];
5861
+ }
5862
+ } else {
5863
+ $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
5864
+ }
5865
+ break;
5866
+
5867
+ case 4:
5868
+ default:
5869
+ $color = [Type::T_COLOR, $args[0], $args[1], $args[2], $args[3]];
5870
+
5871
+ if (! $color = $this->coerceColor($color)) {
5872
+ $color = [Type::T_STRING, '',
5873
+ [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
5874
+ }
5875
+ break;
5876
  }
5877
 
5878
+ return $color;
5879
+ }
5880
 
5881
+ protected static $libRgba = [
5882
+ ['color'],
5883
+ ['color', 'alpha'],
5884
+ ['channels'],
5885
+ ['red', 'green', 'blue'],
5886
+ ['red', 'green', 'blue', 'alpha'] ];
5887
+ protected function libRgba($args, $kwargs)
5888
+ {
5889
+ return $this->libRgb($args, $kwargs, 'rgba');
5890
  }
5891
 
5892
  // helper function for adjust_color, change_color, and scale_color
5894
  {
5895
  $color = $this->assertColor($args[0]);
5896
 
5897
+ foreach ([1 => 1, 2 => 2, 3 => 3, 7 => 4] as $iarg => $irgba) {
5898
+ if (isset($args[$iarg])) {
5899
+ $val = $this->assertNumber($args[$iarg]);
5900
+
5901
+ if (! isset($color[$irgba])) {
5902
+ $color[$irgba] = (($irgba < 4) ? 0 : 1);
5903
+ }
5904
+
5905
+ $color[$irgba] = call_user_func($fn, $color[$irgba], $val, $iarg);
5906
  }
5907
  }
5908
 
5909
+ if (! empty($args[4]) || ! empty($args[5]) || ! empty($args[6])) {
5910
  $hsl = $this->toHSL($color[1], $color[2], $color[3]);
5911
 
5912
+ foreach ([4 => 1, 5 => 2, 6 => 3] as $iarg => $ihsl) {
5913
+ if (! empty($args[$iarg])) {
5914
+ $val = $this->assertNumber($args[$iarg]);
5915
+ $hsl[$ihsl] = call_user_func($fn, $hsl[$ihsl], $val, $iarg);
5916
  }
5917
  }
5918
 
5929
  }
5930
 
5931
  protected static $libAdjustColor = [
5932
+ 'color', 'red:null', 'green:null', 'blue:null',
5933
+ 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
5934
  ];
5935
  protected function libAdjustColor($args)
5936
  {
5940
  }
5941
 
5942
  protected static $libChangeColor = [
5943
+ 'color', 'red:null', 'green:null', 'blue:null',
5944
+ 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
5945
  ];
5946
  protected function libChangeColor($args)
5947
  {
5951
  }
5952
 
5953
  protected static $libScaleColor = [
5954
+ 'color', 'red:null', 'green:null', 'blue:null',
5955
+ 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
5956
  ];
5957
  protected function libScaleColor($args)
5958
  {
5995
  $color = $this->coerceColor($args[0]);
5996
  $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
5997
 
5998
+ return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]];
5999
  }
6000
 
6001
  protected static $libRed = ['color'];
6046
  }
6047
 
6048
  // mix two colors
6049
+ protected static $libMix = ['color-1', 'color-2', 'weight:0.5'];
6050
  protected function libMix($args)
6051
  {
6052
  list($first, $second, $weight) = $args;
6082
  return $this->fixColor($new);
6083
  }
6084
 
6085
+ protected static $libHsl =[
6086
+ ['channels'],
6087
+ ['hue', 'saturation', 'lightness'],
6088
+ ['hue', 'saturation', 'lightness', 'alpha'] ];
6089
+ protected function libHsl($args, $kwargs, $funcName = 'hsl')
6090
  {
6091
+ if (count($args) == 1) {
6092
+ if ($args[0][0] !== Type::T_LIST || count($args[0][2]) < 3 || count($args[0][2]) > 4) {
6093
+ return [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
6094
+ }
6095
 
6096
+ $args = $args[0][2];
6097
+ }
6098
 
6099
+ $hue = $this->compileColorPartValue($args[0], 0, 360, false, false, true);
6100
+ $saturation = $this->compileColorPartValue($args[1], 0, 100, false);
6101
+ $lightness = $this->compileColorPartValue($args[2], 0, 100, false);
6102
+
6103
+ $alpha = null;
6104
+
6105
+ if (count($args) === 4) {
6106
+ $alpha = $this->compileColorPartValue($args[3], 0, 100, false);
6107
+
6108
+ if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness) || ! is_numeric($alpha)) {
6109
+ return [Type::T_STRING, '',
6110
+ [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
6111
+ }
6112
+ } else {
6113
+ if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness)) {
6114
+ return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
6115
+ }
6116
+ }
6117
 
6118
+ $color = $this->toRGB($hue, $saturation, $lightness);
6119
+
6120
+ if (! is_null($alpha)) {
6121
+ $color[4] = $alpha;
6122
+ }
6123
 
6124
  return $color;
6125
  }
6126
 
6127
+ protected static $libHsla = [
6128
+ ['channels'],
6129
+ ['hue', 'saturation', 'lightness', 'alpha:1'] ];
6130
+ protected function libHsla($args, $kwargs)
6131
+ {
6132
+ return $this->libHsl($args, $kwargs, 'hsla');
6133
+ }
6134
+
6135
  protected static $libHue = ['color'];
6136
  protected function libHue($args)
6137
  {
6199
  return $this->adjustHsl($color, 3, -$amount);
6200
  }
6201
 
6202
+ protected static $libSaturate = [['color', 'amount'], ['number']];
6203
  protected function libSaturate($args)
6204
  {
6205
  $value = $args[0];
6241
  return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
6242
  }
6243
 
6244
+ protected static $libInvert = ['color', 'weight:1'];
6245
  protected function libInvert($args)
6246
  {
6247
+ list($value, $weight) = $args;
6248
+
6249
+ if (! isset($weight)) {
6250
+ $weight = 1;
6251
+ } else {
6252
+ $weight = $this->coercePercent($weight);
6253
+ }
6254
 
6255
  if ($value[0] === Type::T_NUMBER) {
6256
  return null;
6257
  }
6258
 
6259
  $color = $this->assertColor($value);
6260
+ $inverted = $color;
6261
+ $inverted[1] = 255 - $inverted[1];
6262
+ $inverted[2] = 255 - $inverted[2];
6263
+ $inverted[3] = 255 - $inverted[3];
6264
 
6265
+ if ($weight < 1) {
6266
+ return $this->libMix([$inverted, $color, [Type::T_NUMBER, $weight]]);
6267
+ }
6268
+
6269
+ return $inverted;
6270
  }
6271
 
6272
  // increases opacity by amount
6375
  $min = null;
6376
 
6377
  foreach ($numbers as $key => $number) {
6378
+ if (is_null($min) || $number[1] <= $min[1]) {
6379
  $min = [$key, $number[1]];
6380
  }
6381
  }
6389
  $max = null;
6390
 
6391
  foreach ($numbers as $key => $number) {
6392
+ if (is_null($max) || $number[1] >= $max[1]) {
6393
  $max = [$key, $number[1]];
6394
  }
6395
  }
6406
  */
6407
  protected function getNormalizedNumbers($args)
6408
  {
6409
+ $unit = null;
6410
  $originalUnit = null;
6411
+ $numbers = [];
6412
 
6413
  foreach ($args as $key => $item) {
6414
  if ($item[0] !== Type::T_NUMBER) {
6418
 
6419
  $number = $item->normalize();
6420
 
6421
+ if (is_null($unit)) {
6422
  $unit = $number[2];
6423
  $originalUnit = $item->unitStr();
6424
+ } elseif ($number[1] && $unit !== $number[2]) {
6425
  $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
6426
  break;
6427
  }
6490
  if (! isset($list[2][$n])) {
6491
  $this->throwError('Invalid argument for "n"');
6492
 
6493
+ return null;
6494
  }
6495
 
6496
  $list[2][$n] = $args[2];
6502
  protected function libMapGet($args)
6503
  {
6504
  $map = $this->assertMap($args[0]);
6505
+ $key = $args[1];
6506
 
6507
+ if (! is_null($key)) {
6508
+ $key = $this->compileStringContent($this->coerceString($key));
6509
+
6510
+ for ($i = count($map[1]) - 1; $i >= 0; $i--) {
6511
+ if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
6512
+ return $map[2][$i];
6513
+ }
6514
  }
6515
  }
6516
 
6572
  $map1 = $this->assertMap($args[0]);
6573
  $map2 = $this->assertMap($args[1]);
6574
 
6575
+ foreach ($map2[1] as $i2 => $key2) {
6576
+ $key = $this->compileStringContent($this->coerceString($key2));
6577
+
6578
+ foreach ($map1[1] as $i1 => $key1) {
6579
+ if ($key === $this->compileStringContent($this->coerceString($key1))) {
6580
+ $map1[2][$i1] = $map2[2][$i2];
6581
+ continue 2;
6582
+ }
6583
+ }
6584
+
6585
+ $map1[1][] = $map2[1][$i2];
6586
+ $map1[2][] = $map2[2][$i2];
6587
+ }
6588
+
6589
+ return $map1;
6590
  }
6591
 
6592
  protected static $libKeywords = ['args'];
6605
  return [Type::T_MAP, $keys, $values];
6606
  }
6607
 
6608
+ protected static $libIsBracketed = ['list'];
6609
+ protected function libIsBracketed($args)
6610
+ {
6611
+ $list = $args[0];
6612
+ $this->coerceList($list, ' ');
6613
+ if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') {
6614
+ return true;
6615
+ }
6616
+ return false;
6617
+ }
6618
+
6619
+
6620
  protected function listSeparatorForJoin($list1, $sep)
6621
  {
6622
  if (! isset($sep)) {
6628
  return ',';
6629
 
6630
  case 'space':
6631
+ return ' ';
6632
 
6633
  default:
6634
  return $list1[1];
6635
  }
6636
  }
6637
 
6638
+ protected static $libJoin = ['list1', 'list2', 'separator:null', 'bracketed:auto'];
6639
  protected function libJoin($args)
6640
  {
6641
+ list($list1, $list2, $sep, $bracketed) = $args;
6642
 
6643
  $list1 = $this->coerceList($list1, ' ');
6644
  $list2 = $this->coerceList($list2, ' ');
6645
+ $sep = $this->listSeparatorForJoin($list1, $sep);
6646
+
6647
+ if ($bracketed === static::$true) {
6648
+ $bracketed = true;
6649
+ } elseif ($bracketed === static::$false) {
6650
+ $bracketed = false;
6651
+ } elseif ($bracketed === [Type::T_KEYWORD, 'auto']) {
6652
+ $bracketed = 'auto';
6653
+ } elseif ($bracketed === static::$null) {
6654
+ $bracketed = false;
6655
+ } else {
6656
+ $bracketed = $this->compileValue($bracketed);
6657
+ $bracketed = ! ! $bracketed;
6658
+ if ($bracketed === true) {
6659
+ $bracketed = true;
6660
+ }
6661
+ }
6662
+
6663
+ if ($bracketed === 'auto') {
6664
+ $bracketed = false;
6665
+ if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') {
6666
+ $bracketed = true;
6667
+ }
6668
+ }
6669
 
6670
+ $res = [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
6671
+ if (isset($list1['enclosing'])) {
6672
+ $res['enlcosing'] = $list1['enclosing'];
6673
+ }
6674
+ if ($bracketed) {
6675
+ $res['enclosing'] = 'bracket';
6676
+ }
6677
+ return $res;
6678
  }
6679
 
6680
+ protected static $libAppend = ['list', 'val', 'separator:null'];
6681
  protected function libAppend($args)
6682
  {
6683
  list($list1, $value, $sep) = $args;
6685
  $list1 = $this->coerceList($list1, ' ');
6686
  $sep = $this->listSeparatorForJoin($list1, $sep);
6687
 
6688
+ $res = [Type::T_LIST, $sep, array_merge($list1[2], [$value])];
6689
+ if (isset($list1['enclosing'])) {
6690
+ $res['enclosing'] = $list1['enclosing'];
6691
+ }
6692
+ return $res;
6693
  }
6694
 
6695
  protected function libZip($args)
6696
  {
6697
+ foreach ($args as $key => $arg) {
6698
+ $args[$key] = $this->coerceList($arg);
6699
  }
6700
 
6701
  $lists = [];
6778
  ) {
6779
  $this->throwError('Invalid argument(s) for "comparable"');
6780
 
6781
+ return null;
6782
  }
6783
 
6784
  $number1 = $number1->normalize();
6826
  return new Node\Number(strlen($stringContent), '');
6827
  }
6828
 
6829
+ protected static $libStrSlice = ['string', 'start-at', 'end-at:-1'];
6830
  protected function libStrSlice($args)
6831
  {
6832
+ if (isset($args[2]) && ! $args[2][1]) {
6833
  return static::$nullString;
6834
  }
6835
 
6842
  $start--;
6843
  }
6844
 
6845
+ $end = isset($args[2]) ? (int) $args[2][1] : -1;
6846
  $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end);
6847
 
6848
  $string[2] = $length
6958
  if ($n < 1) {
6959
  $this->throwError("limit must be greater than or equal to 1");
6960
 
6961
+ return null;
6962
  }
6963
 
6964
  return new Node\Number(mt_rand(1, $n), '');
6980
  return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
6981
  }
6982
 
6983
+ protected function inspectFormatValue($value, $force_enclosing_display = false)
6984
+ {
6985
+ if ($value === static::$null) {
6986
+ $value = [Type::T_KEYWORD, 'null'];
6987
+ }
6988
+ $stringValue = [$value];
6989
+ if ($value[0] === Type::T_LIST) {
6990
+ if (end($value[2]) === static::$null) {
6991
+ array_pop($value[2]);
6992
+ $value[2][] = [Type::T_STRING, '', ['']];
6993
+ $force_enclosing_display = true;
6994
+ }
6995
+ if (! empty($value['enclosing'])) {
6996
+ if ($force_enclosing_display
6997
+ || ($value['enclosing'] === 'bracket' )
6998
+ || !count($value[2])) {
6999
+ $value['enclosing'] = 'forced_'.$value['enclosing'];
7000
+ $force_enclosing_display = true;
7001
+ }
7002
+ }
7003
+ foreach ($value[2] as $k => $listelement) {
7004
+ $value[2][$k] = $this->inspectFormatValue($listelement, $force_enclosing_display);
7005
+ }
7006
+ $stringValue = [$value];
7007
+ }
7008
+
7009
+ return [Type::T_STRING, '', $stringValue];
7010
+ }
7011
+
7012
  protected static $libInspect = ['value'];
7013
  protected function libInspect($args)
7014
  {
7015
+ $value = $args[0];
7016
+ return $this->inspectFormatValue($value);
7017
+ }
7018
+
7019
+ /**
7020
+ * Preprocess selector args
7021
+ *
7022
+ * @param array $arg
7023
+ *
7024
+ * @return array|boolean
7025
+ */
7026
+ protected function getSelectorArg($arg)
7027
+ {
7028
+ static $parser = null;
7029
+
7030
+ if (is_null($parser)) {
7031
+ $parser = $this->parserFactory(__METHOD__);
7032
+ }
7033
+
7034
+ $arg = $this->libUnquote([$arg]);
7035
+ $arg = $this->compileValue($arg);
7036
+
7037
+ $parsedSelector = [];
7038
+
7039
+ if ($parser->parseSelector($arg, $parsedSelector)) {
7040
+ $selector = $this->evalSelectors($parsedSelector);
7041
+ $gluedSelector = $this->glueFunctionSelectors($selector);
7042
+
7043
+ return $gluedSelector;
7044
+ }
7045
+
7046
+ return false;
7047
+ }
7048
+
7049
+ /**
7050
+ * Postprocess selector to output in right format
7051
+ *
7052
+ * @param array $selectors
7053
+ *
7054
+ * @return string
7055
+ */
7056
+ protected function formatOutputSelector($selectors)
7057
+ {
7058
+ $selectors = $this->collapseSelectors($selectors, true);
7059
+
7060
+ return $selectors;
7061
+ }
7062
+
7063
+ protected static $libIsSuperselector = ['super', 'sub'];
7064
+ protected function libIsSuperselector($args)
7065
+ {
7066
+ list($super, $sub) = $args;
7067
+
7068
+ $super = $this->getSelectorArg($super);
7069
+ $sub = $this->getSelectorArg($sub);
7070
+
7071
+ return $this->isSuperSelector($super, $sub);
7072
+ }
7073
+
7074
+ /**
7075
+ * Test a $super selector again $sub
7076
+ *
7077
+ * @param array $super
7078
+ * @param array $sub
7079
+ *
7080
+ * @return boolean
7081
+ */
7082
+ protected function isSuperSelector($super, $sub)
7083
+ {
7084
+ // one and only one selector for each arg
7085
+ if (! $super || count($super) !== 1) {
7086
+ $this->throwError("Invalid super selector for isSuperSelector()");
7087
+ }
7088
+
7089
+ if (! $sub || count($sub) !== 1) {
7090
+ $this->throwError("Invalid sub selector for isSuperSelector()");
7091
+ }
7092
+
7093
+ $super = reset($super);
7094
+ $sub = reset($sub);
7095
+
7096
+ $i = 0;
7097
+ $nextMustMatch = false;
7098
+
7099
+ foreach ($super as $node) {
7100
+ $compound = '';
7101
+
7102
+ array_walk_recursive(
7103
+ $node,
7104
+ function ($value, $key) use (&$compound) {
7105
+ $compound .= $value;
7106
+ }
7107
+ );
7108
+
7109
+ if ($this->isImmediateRelationshipCombinator($compound)) {
7110
+ if ($node !== $sub[$i]) {
7111
+ return false;
7112
+ }
7113
+
7114
+ $nextMustMatch = true;
7115
+ $i++;
7116
+ } else {
7117
+ while ($i < count($sub) && ! $this->isSuperPart($node, $sub[$i])) {
7118
+ if ($nextMustMatch) {
7119
+ return false;
7120
+ }
7121
+
7122
+ $i++;
7123
+ }
7124
+
7125
+ if ($i >= count($sub)) {
7126
+ return false;
7127
+ }
7128
+
7129
+ $nextMustMatch = false;
7130
+ $i++;
7131
+ }
7132
+ }
7133
+
7134
+ return true;
7135
+ }
7136
+
7137
+ /**
7138
+ * Test a part of super selector again a part of sub selector
7139
+ *
7140
+ * @param array $superParts
7141
+ * @param array $subParts
7142
+ *
7143
+ * @return boolean
7144
+ */
7145
+ protected function isSuperPart($superParts, $subParts)
7146
+ {
7147
+ $i = 0;
7148
+
7149
+ foreach ($superParts as $superPart) {
7150
+ while ($i < count($subParts) && $subParts[$i] !== $superPart) {
7151
+ $i++;
7152
+ }
7153
+
7154
+ if ($i >= count($subParts)) {
7155
+ return false;
7156
+ }
7157
+
7158
+ $i++;
7159
+ }
7160
+
7161
+ return true;
7162
+ }
7163
+
7164
+ protected static $libSelectorAppend = ['selector...'];
7165
+ protected function libSelectorAppend($args)
7166
+ {
7167
+ // get the selector... list
7168
+ $args = reset($args);
7169
+ $args = $args[2];
7170
+
7171
+ if (count($args) < 1) {
7172
+ $this->throwError("selector-append() needs at least 1 argument");
7173
+ }
7174
+
7175
+ $selectors = array_map([$this, 'getSelectorArg'], $args);
7176
+
7177
+ return $this->formatOutputSelector($this->selectorAppend($selectors));
7178
+ }
7179
+
7180
+ /**
7181
+ * Append parts of the last selector in the list to the previous, recursively
7182
+ *
7183
+ * @param array $selectors
7184
+ *
7185
+ * @return array
7186
+ *
7187
+ * @throws \ScssPhp\ScssPhp\Exception\CompilerException
7188
+ */
7189
+ protected function selectorAppend($selectors)
7190
+ {
7191
+ $lastSelectors = array_pop($selectors);
7192
+
7193
+ if (! $lastSelectors) {
7194
+ $this->throwError("Invalid selector list in selector-append()");
7195
+ }
7196
+
7197
+ while (count($selectors)) {
7198
+ $previousSelectors = array_pop($selectors);
7199
+
7200
+ if (! $previousSelectors) {
7201
+ $this->throwError("Invalid selector list in selector-append()");
7202
+ }
7203
+
7204
+ // do the trick, happening $lastSelector to $previousSelector
7205
+ $appended = [];
7206
+
7207
+ foreach ($lastSelectors as $lastSelector) {
7208
+ $previous = $previousSelectors;
7209
+
7210
+ foreach ($lastSelector as $lastSelectorParts) {
7211
+ foreach ($lastSelectorParts as $lastSelectorPart) {
7212
+ foreach ($previous as $i => $previousSelector) {
7213
+ foreach ($previousSelector as $j => $previousSelectorParts) {
7214
+ $previous[$i][$j][] = $lastSelectorPart;
7215
+ }
7216
+ }
7217
+ }
7218
+ }
7219
+
7220
+ foreach ($previous as $ps) {
7221
+ $appended[] = $ps;
7222
+ }
7223
+ }
7224
+
7225
+ $lastSelectors = $appended;
7226
+ }
7227
+
7228
+ return $lastSelectors;
7229
+ }
7230
+
7231
+ protected static $libSelectorExtend = ['selectors', 'extendee', 'extender'];
7232
+ protected function libSelectorExtend($args)
7233
+ {
7234
+ list($selectors, $extendee, $extender) = $args;
7235
+
7236
+ $selectors = $this->getSelectorArg($selectors);
7237
+ $extendee = $this->getSelectorArg($extendee);
7238
+ $extender = $this->getSelectorArg($extender);
7239
+
7240
+ if (! $selectors || ! $extendee || ! $extender) {
7241
+ $this->throwError("selector-extend() invalid arguments");
7242
+ }
7243
+
7244
+ $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender);
7245
+
7246
+ return $this->formatOutputSelector($extended);
7247
+ }
7248
+
7249
+ protected static $libSelectorReplace = ['selectors', 'original', 'replacement'];
7250
+ protected function libSelectorReplace($args)
7251
+ {
7252
+ list($selectors, $original, $replacement) = $args;
7253
+
7254
+ $selectors = $this->getSelectorArg($selectors);
7255
+ $original = $this->getSelectorArg($original);
7256
+ $replacement = $this->getSelectorArg($replacement);
7257
+
7258
+ if (! $selectors || ! $original || ! $replacement) {
7259
+ $this->throwError("selector-replace() invalid arguments");
7260
+ }
7261
+
7262
+ $replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true);
7263
+
7264
+ return $this->formatOutputSelector($replaced);
7265
+ }
7266
+
7267
+ /**
7268
+ * Extend/replace in selectors
7269
+ * used by selector-extend and selector-replace that use the same logic
7270
+ *
7271
+ * @param array $selectors
7272
+ * @param array $extendee
7273
+ * @param array $extender
7274
+ * @param boolean $replace
7275
+ *
7276
+ * @return array
7277
+ */
7278
+ protected function extendOrReplaceSelectors($selectors, $extendee, $extender, $replace = false)
7279
+ {
7280
+ $saveExtends = $this->extends;
7281
+ $saveExtendsMap = $this->extendsMap;
7282
+
7283
+ $this->extends = [];
7284
+ $this->extendsMap = [];
7285
+
7286
+ foreach ($extendee as $es) {
7287
+ // only use the first one
7288
+ $this->pushExtends(reset($es), $extender, null);
7289
+ }
7290
+
7291
+ $extended = [];
7292
+
7293
+ foreach ($selectors as $selector) {
7294
+ if (! $replace) {
7295
+ $extended[] = $selector;
7296
+ }
7297
+
7298
+ $n = count($extended);
7299
+
7300
+ $this->matchExtends($selector, $extended);
7301
+
7302
+ // if didnt match, keep the original selector if we are in a replace operation
7303
+ if ($replace and count($extended) === $n) {
7304
+ $extended[] = $selector;
7305
+ }
7306
+ }
7307
+
7308
+ $this->extends = $saveExtends;
7309
+ $this->extendsMap = $saveExtendsMap;
7310
+
7311
+ return $extended;
7312
+ }
7313
+
7314
+ protected static $libSelectorNest = ['selector...'];
7315
+ protected function libSelectorNest($args)
7316
+ {
7317
+ // get the selector... list
7318
+ $args = reset($args);
7319
+ $args = $args[2];
7320
+
7321
+ if (count($args) < 1) {
7322
+ $this->throwError("selector-nest() needs at least 1 argument");
7323
+ }
7324
+
7325
+ $selectorsMap = array_map([$this, 'getSelectorArg'], $args);
7326
+ $envs = [];
7327
+
7328
+ foreach ($selectorsMap as $selectors) {
7329
+ $env = new Environment();
7330
+ $env->selectors = $selectors;
7331
+
7332
+ $envs[] = $env;
7333
+ }
7334
+
7335
+ $envs = array_reverse($envs);
7336
+ $env = $this->extractEnv($envs);
7337
+ $outputSelectors = $this->multiplySelectors($env);
7338
+
7339
+ return $this->formatOutputSelector($outputSelectors);
7340
+ }
7341
+
7342
+ protected static $libSelectorParse = ['selectors'];
7343
+ protected function libSelectorParse($args)
7344
+ {
7345
+ $selectors = reset($args);
7346
+ $selectors = $this->getSelectorArg($selectors);
7347
+
7348
+ return $this->formatOutputSelector($selectors);
7349
+ }
7350
+
7351
+ protected static $libSelectorUnify = ['selectors1', 'selectors2'];
7352
+ protected function libSelectorUnify($args)
7353
+ {
7354
+ list($selectors1, $selectors2) = $args;
7355
+
7356
+ $selectors1 = $this->getSelectorArg($selectors1);
7357
+ $selectors2 = $this->getSelectorArg($selectors2);
7358
+
7359
+ if (! $selectors1 || ! $selectors2) {
7360
+ $this->throwError("selector-unify() invalid arguments");
7361
+ }
7362
+
7363
+ // only consider the first compound of each
7364
+ $compound1 = reset($selectors1);
7365
+ $compound2 = reset($selectors2);
7366
+
7367
+ // unify them and that's it
7368
+ $unified = $this->unifyCompoundSelectors($compound1, $compound2);
7369
+
7370
+ return $this->formatOutputSelector($unified);
7371
+ }
7372
+
7373
+ /**
7374
+ * The selector-unify magic as its best
7375
+ * (at least works as expected on test cases)
7376
+ *
7377
+ * @param array $compound1
7378
+ * @param array $compound2
7379
+ *
7380
+ * @return array|mixed
7381
+ */
7382
+ protected function unifyCompoundSelectors($compound1, $compound2)
7383
+ {
7384
+ if (! count($compound1)) {
7385
+ return $compound2;
7386
+ }
7387
+
7388
+ if (! count($compound2)) {
7389
+ return $compound1;
7390
+ }
7391
+
7392
+ // check that last part are compatible
7393
+ $lastPart1 = array_pop($compound1);
7394
+ $lastPart2 = array_pop($compound2);
7395
+ $last = $this->mergeParts($lastPart1, $lastPart2);
7396
+
7397
+ if (! $last) {
7398
+ return [[]];
7399
+ }
7400
+
7401
+ $unifiedCompound = [$last];
7402
+ $unifiedSelectors = [$unifiedCompound];
7403
+
7404
+ // do the rest
7405
+ while (count($compound1) || count($compound2)) {
7406
+ $part1 = end($compound1);
7407
+ $part2 = end($compound2);
7408
+
7409
+ if ($part1 && ($match2 = $this->matchPartInCompound($part1, $compound2))) {
7410
+ list($compound2, $part2, $after2) = $match2;
7411
+
7412
+ if ($after2) {
7413
+ $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after2);
7414
+ }
7415
+
7416
+ $c = $this->mergeParts($part1, $part2);
7417
+ $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
7418
+
7419
+ $part1 = $part2 = null;
7420
+
7421
+ array_pop($compound1);
7422
+ }
7423
+
7424
+ if ($part2 && ($match1 = $this->matchPartInCompound($part2, $compound1))) {
7425
+ list($compound1, $part1, $after1) = $match1;
7426
+
7427
+ if ($after1) {
7428
+ $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after1);
7429
+ }
7430
+
7431
+ $c = $this->mergeParts($part2, $part1);
7432
+ $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
7433
+
7434
+ $part1 = $part2 = null;
7435
+
7436
+ array_pop($compound2);
7437
+ }
7438
+
7439
+ $new = [];
7440
+
7441
+ if ($part1 && $part2) {
7442
+ array_pop($compound1);
7443
+ array_pop($compound2);
7444
+
7445
+ $s = $this->prependSelectors($unifiedSelectors, [$part2]);
7446
+ $new = array_merge($new, $this->prependSelectors($s, [$part1]));
7447
+ $s = $this->prependSelectors($unifiedSelectors, [$part1]);
7448
+ $new = array_merge($new, $this->prependSelectors($s, [$part2]));
7449
+ } elseif ($part1) {
7450
+ array_pop($compound1);
7451
+
7452
+ $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part1]));
7453
+ } elseif ($part2) {
7454
+ array_pop($compound2);
7455
+
7456
+ $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part2]));
7457
+ }
7458
+
7459
+ if ($new) {
7460
+ $unifiedSelectors = $new;
7461
+ }
7462
+ }
7463
+
7464
+ return $unifiedSelectors;
7465
+ }
7466
+
7467
+ /**
7468
+ * Prepend each selector from $selectors with $parts
7469
+ *
7470
+ * @param array $selectors
7471
+ * @param array $parts
7472
+ *
7473
+ * @return array
7474
+ */
7475
+ protected function prependSelectors($selectors, $parts)
7476
+ {
7477
+ $new = [];
7478
+
7479
+ foreach ($selectors as $compoundSelector) {
7480
+ array_unshift($compoundSelector, $parts);
7481
+
7482
+ $new[] = $compoundSelector;
7483
+ }
7484
+
7485
+ return $new;
7486
+ }
7487
+
7488
+ /**
7489
+ * Try to find a matching part in a compound:
7490
+ * - with same html tag name
7491
+ * - with some class or id or something in common
7492
+ *
7493
+ * @param array $part
7494
+ * @param array $compound
7495
+ *
7496
+ * @return array|boolean
7497
+ */
7498
+ protected function matchPartInCompound($part, $compound)
7499
+ {
7500
+ $partTag = $this->findTagName($part);
7501
+ $before = $compound;
7502
+ $after = [];
7503
+
7504
+ // try to find a match by tag name first
7505
+ while (count($before)) {
7506
+ $p = array_pop($before);
7507
+
7508
+ if ($partTag && $partTag !== '*' && $partTag == $this->findTagName($p)) {
7509
+ return [$before, $p, $after];
7510
+ }
7511
+
7512
+ $after[] = $p;
7513
+ }
7514
+
7515
+ // try again matching a non empty intersection and a compatible tagname
7516
+ $before = $compound;
7517
+ $after = [];
7518
+
7519
+ while (count($before)) {
7520
+ $p = array_pop($before);
7521
+
7522
+ if ($this->checkCompatibleTags($partTag, $this->findTagName($p))) {
7523
+ if (count(array_intersect($part, $p))) {
7524
+ return [$before, $p, $after];
7525
+ }
7526
+ }
7527
+
7528
+ $after[] = $p;
7529
+ }
7530
+
7531
+ return false;
7532
+ }
7533
+
7534
+ /**
7535
+ * Merge two part list taking care that
7536
+ * - the html tag is coming first - if any
7537
+ * - the :something are coming last
7538
+ *
7539
+ * @param array $parts1
7540
+ * @param array $parts2
7541
+ *
7542
+ * @return array
7543
+ */
7544
+ protected function mergeParts($parts1, $parts2)
7545
+ {
7546
+ $tag1 = $this->findTagName($parts1);
7547
+ $tag2 = $this->findTagName($parts2);
7548
+ $tag = $this->checkCompatibleTags($tag1, $tag2);
7549
+
7550
+ // not compatible tags
7551
+ if ($tag === false) {
7552
+ return [];
7553
+ }
7554
+
7555
+ if ($tag) {
7556
+ if ($tag1) {
7557
+ $parts1 = array_diff($parts1, [$tag1]);
7558
+ }
7559
+
7560
+ if ($tag2) {
7561
+ $parts2 = array_diff($parts2, [$tag2]);
7562
+ }
7563
+ }
7564
+
7565
+ $mergedParts = array_merge($parts1, $parts2);
7566
+ $mergedOrderedParts = [];
7567
+
7568
+ foreach ($mergedParts as $part) {
7569
+ if (strpos($part, ':') === 0) {
7570
+ $mergedOrderedParts[] = $part;
7571
+ }
7572
+ }
7573
+
7574
+ $mergedParts = array_diff($mergedParts, $mergedOrderedParts);
7575
+ $mergedParts = array_merge($mergedParts, $mergedOrderedParts);
7576
+
7577
+ if ($tag) {
7578
+ array_unshift($mergedParts, $tag);
7579
+ }
7580
+
7581
+ return $mergedParts;
7582
+ }
7583
+
7584
+ /**
7585
+ * Check the compatibility between two tag names:
7586
+ * if both are defined they should be identical or one has to be '*'
7587
+ *
7588
+ * @param string $tag1
7589
+ * @param string $tag2
7590
+ *
7591
+ * @return array|boolean
7592
+ */
7593
+ protected function checkCompatibleTags($tag1, $tag2)
7594
+ {
7595
+ $tags = [$tag1, $tag2];
7596
+ $tags = array_unique($tags);
7597
+ $tags = array_filter($tags);
7598
+
7599
+ if (count($tags) > 1) {
7600
+ $tags = array_diff($tags, ['*']);
7601
+ }
7602
+
7603
+ // not compatible nodes
7604
+ if (count($tags) > 1) {
7605
+ return false;
7606
+ }
7607
+
7608
+ return $tags;
7609
+ }
7610
+
7611
+ /**
7612
+ * Find the html tag name in a selector parts list
7613
+ *
7614
+ * @param array $parts
7615
+ *
7616
+ * @return mixed|string
7617
+ */
7618
+ protected function findTagName($parts)
7619
+ {
7620
+ foreach ($parts as $part) {
7621
+ if (! preg_match('/^[\[.:#%_-]/', $part)) {
7622
+ return $part;
7623
+ }
7624
+ }
7625
+
7626
+ return '';
7627
+ }
7628
+
7629
+ protected static $libSimpleSelectors = ['selector'];
7630
+ protected function libSimpleSelectors($args)
7631
+ {
7632
+ $selector = reset($args);
7633
+ $selector = $this->getSelectorArg($selector);
7634
+
7635
+ // remove selectors list layer, keeping the first one
7636
+ $selector = reset($selector);
7637
+
7638
+ // remove parts list layer, keeping the first part
7639
+ $part = reset($selector);
7640
+
7641
+ $listParts = [];
7642
+
7643
+ foreach ($part as $p) {
7644
+ $listParts[] = [Type::T_STRING, '', [$p]];
7645
  }
7646
 
7647
+ return [Type::T_LIST, ',', $listParts];
7648
  }
7649
  }
assets/libraries/scssphp/src/Compiler/Environment.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Compiler;
13
 
14
  /**
15
  * Compiler environment
@@ -19,12 +19,12 @@ namespace Leafo\ScssPhp\Compiler;
19
  class Environment
20
  {
21
  /**
22
- * @var \Leafo\ScssPhp\Block
23
  */
24
  public $block;
25
 
26
  /**
27
- * @var \Leafo\ScssPhp\Compiler\Environment
28
  */
29
  public $parent;
30
 
@@ -33,6 +33,11 @@ class Environment
33
  */
34
  public $store;
35
 
 
 
 
 
 
36
  /**
37
  * @var integer
38
  */
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Compiler;
13
 
14
  /**
15
  * Compiler environment
19
  class Environment
20
  {
21
  /**
22
+ * @var \ScssPhp\ScssPhp\Block
23
  */
24
  public $block;
25
 
26
  /**
27
+ * @var \ScssPhp\ScssPhp\Compiler\Environment
28
  */
29
  public $parent;
30
 
33
  */
34
  public $store;
35
 
36
+ /**
37
+ * @var array
38
+ */
39
+ public $storeUnreduced;
40
+
41
  /**
42
  * @var integer
43
  */
assets/libraries/scssphp/src/Exception/CompilerException.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Exception;
13
 
14
  /**
15
  * Compiler exception
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Exception;
13
 
14
  /**
15
  * Compiler exception
assets/libraries/scssphp/src/Exception/ParserException.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Exception;
13
 
14
  /**
15
  * Parser Exception
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Exception;
13
 
14
  /**
15
  * Parser Exception
assets/libraries/scssphp/src/Exception/RangeException.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Exception;
13
 
14
  /**
15
  * Range exception
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Exception;
13
 
14
  /**
15
  * Range exception
assets/libraries/scssphp/src/Exception/ServerException.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Exception;
13
 
14
  /**
15
  * Server Exception
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Exception;
13
 
14
  /**
15
  * Server Exception
assets/libraries/scssphp/src/Formatter.php CHANGED
@@ -2,17 +2,17 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
 
14
- use Leafo\ScssPhp\Formatter\OutputBlock;
15
- use Leafo\ScssPhp\SourceMap\SourceMapGenerator;
16
 
17
  /**
18
  * Base formatter
@@ -62,7 +62,7 @@ abstract class Formatter
62
  public $keepSemicolons;
63
 
64
  /**
65
- * @var \Leafo\ScssPhp\Formatter\OutputBlock
66
  */
67
  protected $currentBlock;
68
 
@@ -77,10 +77,15 @@ abstract class Formatter
77
  protected $currentColumn;
78
 
79
  /**
80
- * @var \Leafo\ScssPhp\SourceMap\SourceMapGenerator
81
  */
82
  protected $sourceMapGenerator;
83
 
 
 
 
 
 
84
  /**
85
  * Initialize formatter
86
  *
@@ -113,30 +118,10 @@ abstract class Formatter
113
  return rtrim($name) . $this->assignSeparator . $value . ';';
114
  }
115
 
116
- /**
117
- * Strip semi-colon appended by property(); it's a separator, not a terminator
118
- *
119
- * @api
120
- *
121
- * @param array $lines
122
- */
123
- public function stripSemicolon(&$lines)
124
- {
125
- if ($this->keepSemicolons) {
126
- return;
127
- }
128
-
129
- if (($count = count($lines))
130
- && substr($lines[$count - 1], -1) === ';'
131
- ) {
132
- $lines[$count - 1] = substr($lines[$count - 1], 0, -1);
133
- }
134
- }
135
-
136
  /**
137
  * Output lines inside a block
138
  *
139
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
140
  */
141
  protected function blockLines(OutputBlock $block)
142
  {
@@ -154,7 +139,7 @@ abstract class Formatter
154
  /**
155
  * Output block selectors
156
  *
157
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
158
  */
159
  protected function blockSelectors(OutputBlock $block)
160
  {
@@ -168,7 +153,7 @@ abstract class Formatter
168
  /**
169
  * Output block children
170
  *
171
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
172
  */
173
  protected function blockChildren(OutputBlock $block)
174
  {
@@ -180,7 +165,7 @@ abstract class Formatter
180
  /**
181
  * Output non-empty block
182
  *
183
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
184
  */
185
  protected function block(OutputBlock $block)
186
  {
@@ -209,6 +194,10 @@ abstract class Formatter
209
  if (! empty($block->selectors)) {
210
  $this->indentLevel--;
211
 
 
 
 
 
212
  if (empty($block->children)) {
213
  $this->write($this->break);
214
  }
@@ -217,13 +206,41 @@ abstract class Formatter
217
  }
218
  }
219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  /**
221
  * Entry point to formatting a block
222
  *
223
  * @api
224
  *
225
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block An abstract syntax tree
226
- * @param \Leafo\ScssPhp\SourceMap\SourceMapGenerator|null $sourceMapGenerator Optional source map generator
227
  *
228
  * @return string
229
  */
@@ -232,11 +249,13 @@ abstract class Formatter
232
  $this->sourceMapGenerator = null;
233
 
234
  if ($sourceMapGenerator) {
235
- $this->currentLine = 1;
236
- $this->currentColumn = 0;
237
  $this->sourceMapGenerator = $sourceMapGenerator;
238
  }
239
 
 
 
240
  ob_start();
241
 
242
  $this->block($block);
@@ -247,16 +266,39 @@ abstract class Formatter
247
  }
248
 
249
  /**
 
 
250
  * @param string $str
251
  */
252
  protected function write($str)
253
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  if ($this->sourceMapGenerator) {
255
  $this->sourceMapGenerator->addMapping(
256
  $this->currentLine,
257
  $this->currentColumn,
258
  $this->currentBlock->sourceLine,
259
- $this->currentBlock->sourceColumn - 1, //columns from parser are off by one
 
260
  $this->currentBlock->sourceName
261
  );
262
 
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
 
14
+ use ScssPhp\ScssPhp\Formatter\OutputBlock;
15
+ use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
16
 
17
  /**
18
  * Base formatter
62
  public $keepSemicolons;
63
 
64
  /**
65
+ * @var \ScssPhp\ScssPhp\Formatter\OutputBlock
66
  */
67
  protected $currentBlock;
68
 
77
  protected $currentColumn;
78
 
79
  /**
80
+ * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator
81
  */
82
  protected $sourceMapGenerator;
83
 
84
+ /**
85
+ * @var string
86
+ */
87
+ protected $strippedSemicolon;
88
+
89
  /**
90
  * Initialize formatter
91
  *
118
  return rtrim($name) . $this->assignSeparator . $value . ';';
119
  }
120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  /**
122
  * Output lines inside a block
123
  *
124
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
125
  */
126
  protected function blockLines(OutputBlock $block)
127
  {
139
  /**
140
  * Output block selectors
141
  *
142
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
143
  */
144
  protected function blockSelectors(OutputBlock $block)
145
  {
153
  /**
154
  * Output block children
155
  *
156
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
157
  */
158
  protected function blockChildren(OutputBlock $block)
159
  {
165
  /**
166
  * Output non-empty block
167
  *
168
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
169
  */
170
  protected function block(OutputBlock $block)
171
  {
194
  if (! empty($block->selectors)) {
195
  $this->indentLevel--;
196
 
197
+ if (! $this->keepSemicolons) {
198
+ $this->strippedSemicolon = '';
199
+ }
200
+
201
  if (empty($block->children)) {
202
  $this->write($this->break);
203
  }
206
  }
207
  }
208
 
209
+ /**
210
+ * Test and clean safely empty children
211
+ *
212
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
213
+ *
214
+ * @return boolean
215
+ */
216
+ protected function testEmptyChildren($block)
217
+ {
218
+ $isEmpty = empty($block->lines);
219
+
220
+ if ($block->children) {
221
+ foreach ($block->children as $k => &$child) {
222
+ if (! $this->testEmptyChildren($child)) {
223
+ $isEmpty = false;
224
+ continue;
225
+ }
226
+
227
+ if ($child->type === Type::T_MEDIA || $child->type === Type::T_DIRECTIVE) {
228
+ $child->children = [];
229
+ $child->selectors = null;
230
+ }
231
+ }
232
+ }
233
+
234
+ return $isEmpty;
235
+ }
236
+
237
  /**
238
  * Entry point to formatting a block
239
  *
240
  * @api
241
  *
242
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block An abstract syntax tree
243
+ * @param \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null $sourceMapGenerator Optional source map generator
244
  *
245
  * @return string
246
  */
249
  $this->sourceMapGenerator = null;
250
 
251
  if ($sourceMapGenerator) {
252
+ $this->currentLine = 1;
253
+ $this->currentColumn = 0;
254
  $this->sourceMapGenerator = $sourceMapGenerator;
255
  }
256
 
257
+ $this->testEmptyChildren($block);
258
+
259
  ob_start();
260
 
261
  $this->block($block);
266
  }
267
 
268
  /**
269
+ * Output content
270
+ *
271
  * @param string $str
272
  */
273
  protected function write($str)
274
  {
275
+ if (! empty($this->strippedSemicolon)) {
276
+ echo $this->strippedSemicolon;
277
+
278
+ $this->strippedSemicolon = '';
279
+ }
280
+
281
+ /*
282
+ * Maybe Strip semi-colon appended by property(); it's a separator, not a terminator
283
+ * will be striped for real before a closing, otherwise displayed unchanged starting the next write
284
+ */
285
+ if (! $this->keepSemicolons &&
286
+ $str &&
287
+ (strpos($str, ';') !== false) &&
288
+ (substr($str, -1) === ';')
289
+ ) {
290
+ $str = substr($str, 0, -1);
291
+
292
+ $this->strippedSemicolon = ';';
293
+ }
294
+
295
  if ($this->sourceMapGenerator) {
296
  $this->sourceMapGenerator->addMapping(
297
  $this->currentLine,
298
  $this->currentColumn,
299
  $this->currentBlock->sourceLine,
300
+ //columns from parser are off by one
301
+ $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0,
302
  $this->currentBlock->sourceName
303
  );
304
 
assets/libraries/scssphp/src/Formatter/Compact.php CHANGED
@@ -2,16 +2,16 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Formatter;
13
 
14
- use Leafo\ScssPhp\Formatter;
15
 
16
  /**
17
  * Compact formatter
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Formatter;
13
 
14
+ use ScssPhp\ScssPhp\Formatter;
15
 
16
  /**
17
  * Compact formatter
assets/libraries/scssphp/src/Formatter/Compressed.php CHANGED
@@ -2,17 +2,17 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Formatter;
13
 
14
- use Leafo\ScssPhp\Formatter;
15
- use Leafo\ScssPhp\Formatter\OutputBlock;
16
 
17
  /**
18
  * Compressed formatter
@@ -59,4 +59,23 @@ class Compressed extends Formatter
59
  $this->write($this->break);
60
  }
61
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  }
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Formatter;
13
 
14
+ use ScssPhp\ScssPhp\Formatter;
15
+ use ScssPhp\ScssPhp\Formatter\OutputBlock;
16
 
17
  /**
18
  * Compressed formatter
59
  $this->write($this->break);
60
  }
61
  }
62
+
63
+ /**
64
+ * Output block selectors
65
+ *
66
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
67
+ */
68
+ protected function blockSelectors(OutputBlock $block)
69
+ {
70
+ $inner = $this->indentStr();
71
+
72
+ $this->write(
73
+ $inner
74
+ . implode(
75
+ $this->tagSeparator,
76
+ str_replace([' > ', ' + ', ' ~ '], ['>', '+', '~'], $block->selectors)
77
+ )
78
+ . $this->open . $this->break
79
+ );
80
+ }
81
  }
assets/libraries/scssphp/src/Formatter/Crunched.php CHANGED
@@ -2,17 +2,17 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Formatter;
13
 
14
- use Leafo\ScssPhp\Formatter;
15
- use Leafo\ScssPhp\Formatter\OutputBlock;
16
 
17
  /**
18
  * Crunched formatter
@@ -57,4 +57,23 @@ class Crunched extends Formatter
57
  $this->write($this->break);
58
  }
59
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Formatter;
13
 
14
+ use ScssPhp\ScssPhp\Formatter;
15
+ use ScssPhp\ScssPhp\Formatter\OutputBlock;
16
 
17
  /**
18
  * Crunched formatter
57
  $this->write($this->break);
58
  }
59
  }
60
+
61
+ /**
62
+ * Output block selectors
63
+ *
64
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
65
+ */
66
+ protected function blockSelectors(OutputBlock $block)
67
+ {
68
+ $inner = $this->indentStr();
69
+
70
+ $this->write(
71
+ $inner
72
+ . implode(
73
+ $this->tagSeparator,
74
+ str_replace([' > ', ' + ', ' ~ '], ['>', '+', '~'], $block->selectors)
75
+ )
76
+ . $this->open . $this->break
77
+ );
78
+ }
79
  }
assets/libraries/scssphp/src/Formatter/Debug.php CHANGED
@@ -2,17 +2,17 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Formatter;
13
 
14
- use Leafo\ScssPhp\Formatter;
15
- use Leafo\ScssPhp\Formatter\OutputBlock;
16
 
17
  /**
18
  * Debug formatter
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Formatter;
13
 
14
+ use ScssPhp\ScssPhp\Formatter;
15
+ use ScssPhp\ScssPhp\Formatter\OutputBlock;
16
 
17
  /**
18
  * Debug formatter
assets/libraries/scssphp/src/Formatter/Expanded.php CHANGED
@@ -2,17 +2,17 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Formatter;
13
 
14
- use Leafo\ScssPhp\Formatter;
15
- use Leafo\ScssPhp\Formatter\OutputBlock;
16
 
17
  /**
18
  * Expanded formatter
@@ -55,7 +55,7 @@ class Expanded extends Formatter
55
 
56
  foreach ($block->lines as $index => $line) {
57
  if (substr($line, 0, 2) === '/*') {
58
- $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line);
59
  }
60
  }
61
 
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Formatter;
13
 
14
+ use ScssPhp\ScssPhp\Formatter;
15
+ use ScssPhp\ScssPhp\Formatter\OutputBlock;
16
 
17
  /**
18
  * Expanded formatter
55
 
56
  foreach ($block->lines as $index => $line) {
57
  if (substr($line, 0, 2) === '/*') {
58
+ $block->lines[$index] = preg_replace('/[\r\n]+/', $glue, $line);
59
  }
60
  }
61
 
assets/libraries/scssphp/src/Formatter/Nested.php CHANGED
@@ -2,17 +2,18 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Formatter;
13
 
14
- use Leafo\ScssPhp\Formatter;
15
- use Leafo\ScssPhp\Formatter\OutputBlock;
 
16
 
17
  /**
18
  * Nested formatter
@@ -62,58 +63,50 @@ class Nested extends Formatter
62
 
63
  foreach ($block->lines as $index => $line) {
64
  if (substr($line, 0, 2) === '/*') {
65
- $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line);
66
  }
67
  }
68
 
69
  $this->write($inner . implode($glue, $block->lines));
70
-
71
- if (! empty($block->children)) {
72
- $this->write($this->break);
73
- }
74
  }
75
 
76
  /**
77
  * {@inheritdoc}
78
  */
79
- protected function blockSelectors(OutputBlock $block)
80
  {
81
- $inner = $this->indentStr();
82
-
83
- $this->write($inner
84
- . implode($this->tagSeparator, $block->selectors)
85
- . $this->open . $this->break);
86
- }
87
 
88
- /**
89
- * {@inheritdoc}
90
- */
91
- protected function blockChildren(OutputBlock $block)
92
- {
93
- foreach ($block->children as $i => $child) {
94
- $this->block($child);
 
95
 
96
- if ($i < count($block->children) - 1) {
97
- $this->write($this->break);
 
98
 
99
- if (isset($block->children[$i + 1])) {
100
- $next = $block->children[$i + 1];
 
101
 
102
- if ($next->depth === max($block->depth, 1) && $child->depth >= $next->depth) {
103
- $this->write($this->break);
104
- }
105
- }
106
  }
107
- }
108
- }
109
 
110
- /**
111
- * {@inheritdoc}
112
- */
113
- protected function block(OutputBlock $block)
114
- {
115
- if ($block->type === 'root') {
116
- $this->adjustAllChildren($block);
117
  }
118
 
119
  if (empty($block->lines) && empty($block->children)) {
@@ -122,27 +115,92 @@ class Nested extends Formatter
122
 
123
  $this->currentBlock = $block;
124
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- $this->depth = $block->depth;
 
127
 
128
  if (! empty($block->selectors)) {
 
 
 
 
 
 
 
 
 
 
129
  $this->blockSelectors($block);
130
 
131
  $this->indentLevel++;
132
  }
133
 
134
  if (! empty($block->lines)) {
 
 
 
 
 
 
 
 
 
 
135
  $this->blockLines($block);
 
 
136
  }
137
 
138
  if (! empty($block->children)) {
139
- $this->blockChildren($block);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  }
141
 
142
  if (! empty($block->selectors)) {
143
  $this->indentLevel--;
144
 
 
 
 
 
145
  $this->write($this->close);
 
 
 
 
 
 
 
 
 
 
 
146
  }
147
 
148
  if ($block->type === 'root') {
@@ -151,51 +209,20 @@ class Nested extends Formatter
151
  }
152
 
153
  /**
154
- * Adjust the depths of all children, depth first
 
 
155
  *
156
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
157
  */
158
- private function adjustAllChildren(OutputBlock $block)
159
  {
160
- // flatten empty nested blocks
161
- $children = [];
162
-
163
- foreach ($block->children as $i => $child) {
164
- if (empty($child->lines) && empty($child->children)) {
165
- if (isset($block->children[$i + 1])) {
166
- $block->children[$i + 1]->depth = $child->depth;
167
- }
168
-
169
- continue;
170
- }
171
-
172
- $children[] = $child;
173
- }
174
-
175
- $count = count($children);
176
-
177
- for ($i = 0; $i < $count; $i++) {
178
- $depth = $children[$i]->depth;
179
- $j = $i + 1;
180
-
181
- if (isset($children[$j]) && $depth < $children[$j]->depth) {
182
- $childDepth = $children[$j]->depth;
183
-
184
- for (; $j < $count; $j++) {
185
- if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) {
186
- $children[$j]->depth = $depth + 1;
187
- }
188
- }
189
  }
190
  }
191
 
192
- $block->children = $children;
193
-
194
- // make relative to parent
195
- foreach ($block->children as $child) {
196
- $this->adjustAllChildren($child);
197
-
198
- $child->depth = $child->depth - $block->depth;
199
- }
200
  }
201
  }
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Formatter;
13
 
14
+ use ScssPhp\ScssPhp\Formatter;
15
+ use ScssPhp\ScssPhp\Formatter\OutputBlock;
16
+ use ScssPhp\ScssPhp\Type;
17
 
18
  /**
19
  * Nested formatter
63
 
64
  foreach ($block->lines as $index => $line) {
65
  if (substr($line, 0, 2) === '/*') {
66
+ $block->lines[$index] = preg_replace('/[\r\n]+/', $glue, $line);
67
  }
68
  }
69
 
70
  $this->write($inner . implode($glue, $block->lines));
 
 
 
 
71
  }
72
 
73
  /**
74
  * {@inheritdoc}
75
  */
76
+ protected function block(OutputBlock $block)
77
  {
78
+ static $depths;
79
+ static $downLevel;
80
+ static $closeBlock;
81
+ static $previousEmpty;
82
+ static $previousHasSelector;
 
83
 
84
+ if ($block->type === 'root') {
85
+ $depths = [ 0 ];
86
+ $downLevel = '';
87
+ $closeBlock = '';
88
+ $this->depth = 0;
89
+ $previousEmpty = false;
90
+ $previousHasSelector = false;
91
+ }
92
 
93
+ $isMediaOrDirective = in_array($block->type, [Type::T_DIRECTIVE, Type::T_MEDIA]);
94
+ $isSupport = ($block->type === Type::T_DIRECTIVE
95
+ && $block->selectors && strpos(implode('', $block->selectors), '@supports') !== false);
96
 
97
+ while ($block->depth < end($depths) || ($block->depth == 1 && end($depths) == 1)) {
98
+ array_pop($depths);
99
+ $this->depth--;
100
 
101
+ if (! $this->depth && ($block->depth <= 1 || (! $this->indentLevel && $block->type === Type::T_COMMENT)) &&
102
+ (($block->selectors && ! $isMediaOrDirective) || $previousHasSelector)
103
+ ) {
104
+ $downLevel = $this->break;
105
  }
 
 
106
 
107
+ if (empty($block->lines) && empty($block->children)) {
108
+ $previousEmpty = true;
109
+ }
 
 
 
 
110
  }
111
 
112
  if (empty($block->lines) && empty($block->children)) {
115
 
116
  $this->currentBlock = $block;
117
 
118
+ if (! empty($block->lines) || (! empty($block->children) && ($this->depth < 1 || $isSupport))) {
119
+ if ($block->depth > end($depths)) {
120
+ if (! $previousEmpty || $this->depth < 1) {
121
+ $this->depth++;
122
+ $depths[] = $block->depth;
123
+ } else {
124
+ // keep the current depth unchanged but take the block depth as a new reference for following blocks
125
+ array_pop($depths);
126
+ $depths[] = $block->depth;
127
+ }
128
+ }
129
+ }
130
 
131
+ $previousEmpty = ($block->type === Type::T_COMMENT);
132
+ $previousHasSelector = false;
133
 
134
  if (! empty($block->selectors)) {
135
+ if ($closeBlock) {
136
+ $this->write($closeBlock);
137
+ $closeBlock = '';
138
+ }
139
+
140
+ if ($downLevel) {
141
+ $this->write($downLevel);
142
+ $downLevel = '';
143
+ }
144
+
145
  $this->blockSelectors($block);
146
 
147
  $this->indentLevel++;
148
  }
149
 
150
  if (! empty($block->lines)) {
151
+ if ($closeBlock) {
152
+ $this->write($closeBlock);
153
+ $closeBlock = '';
154
+ }
155
+
156
+ if ($downLevel) {
157
+ $this->write($downLevel);
158
+ $downLevel = '';
159
+ }
160
+
161
  $this->blockLines($block);
162
+
163
+ $closeBlock = $this->break;
164
  }
165
 
166
  if (! empty($block->children)) {
167
+ if ($this->depth > 0 && ($isMediaOrDirective || ! $this->hasFlatChild($block))) {
168
+ array_pop($depths);
169
+
170
+ $this->depth--;
171
+ $this->blockChildren($block);
172
+ $this->depth++;
173
+
174
+ $depths[] = $block->depth;
175
+ } else {
176
+ $this->blockChildren($block);
177
+ }
178
+ }
179
+
180
+ // reclear to not be spoiled by children if T_DIRECTIVE
181
+ if ($block->type === Type::T_DIRECTIVE) {
182
+ $previousHasSelector = false;
183
  }
184
 
185
  if (! empty($block->selectors)) {
186
  $this->indentLevel--;
187
 
188
+ if (! $this->keepSemicolons) {
189
+ $this->strippedSemicolon = '';
190
+ }
191
+
192
  $this->write($this->close);
193
+
194
+ $closeBlock = $this->break;
195
+
196
+ if ($this->depth > 1 && ! empty($block->children)) {
197
+ array_pop($depths);
198
+ $this->depth--;
199
+ }
200
+
201
+ if (! $isMediaOrDirective) {
202
+ $previousHasSelector = true;
203
+ }
204
  }
205
 
206
  if ($block->type === 'root') {
209
  }
210
 
211
  /**
212
+ * Block has flat child
213
+ *
214
+ * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
215
  *
216
+ * @return boolean
217
  */
218
+ private function hasFlatChild($block)
219
  {
220
+ foreach ($block->children as $child) {
221
+ if (empty($child->selectors)) {
222
+ return true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  }
224
  }
225
 
226
+ return false;
 
 
 
 
 
 
 
227
  }
228
  }
assets/libraries/scssphp/src/Formatter/OutputBlock.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Formatter;
13
 
14
  /**
15
  * Output block
@@ -44,7 +44,7 @@ class OutputBlock
44
  public $children;
45
 
46
  /**
47
- * @var \Leafo\ScssPhp\Formatter\OutputBlock
48
  */
49
  public $parent;
50
 
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Formatter;
13
 
14
  /**
15
  * Output block
44
  public $children;
45
 
46
  /**
47
+ * @var \ScssPhp\ScssPhp\Formatter\OutputBlock
48
  */
49
  public $parent;
50
 
assets/libraries/scssphp/src/Node.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
 
14
  /**
15
  * Base node
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
 
14
  /**
15
  * Base node
assets/libraries/scssphp/src/Node/Number.php CHANGED
@@ -2,18 +2,18 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\Node;
13
 
14
- use Leafo\ScssPhp\Compiler;
15
- use Leafo\ScssPhp\Node;
16
- use Leafo\ScssPhp\Type;
17
 
18
  /**
19
  * Dimension + optional units
@@ -100,7 +100,7 @@ class Number extends Node implements \ArrayAccess
100
  *
101
  * @param array $units
102
  *
103
- * @return \Leafo\ScssPhp\Node\Number
104
  */
105
  public function coerce($units)
106
  {
@@ -123,7 +123,7 @@ class Number extends Node implements \ArrayAccess
123
  /**
124
  * Normalize number
125
  *
126
- * @return \Leafo\ScssPhp\Node\Number
127
  */
128
  public function normalize()
129
  {
@@ -141,17 +141,17 @@ class Number extends Node implements \ArrayAccess
141
  public function offsetExists($offset)
142
  {
143
  if ($offset === -3) {
144
- return $this->sourceColumn !== null;
145
  }
146
 
147
  if ($offset === -2) {
148
- return $this->sourceLine !== null;
149
  }
150
 
151
- if ($offset === -1
152
- || $offset === 0
153
- || $offset === 1
154
- || $offset === 2
155
  ) {
156
  return true;
157
  }
@@ -259,7 +259,7 @@ class Number extends Node implements \ArrayAccess
259
  /**
260
  * Output number
261
  *
262
- * @param \Leafo\ScssPhp\Compiler $compiler
263
  *
264
  * @return string
265
  */
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\Node;
13
 
14
+ use ScssPhp\ScssPhp\Compiler;
15
+ use ScssPhp\ScssPhp\Node;
16
+ use ScssPhp\ScssPhp\Type;
17
 
18
  /**
19
  * Dimension + optional units
100
  *
101
  * @param array $units
102
  *
103
+ * @return \ScssPhp\ScssPhp\Node\Number
104
  */
105
  public function coerce($units)
106
  {
123
  /**
124
  * Normalize number
125
  *
126
+ * @return \ScssPhp\ScssPhp\Node\Number
127
  */
128
  public function normalize()
129
  {
141
  public function offsetExists($offset)
142
  {
143
  if ($offset === -3) {
144
+ return ! is_null($this->sourceColumn);
145
  }
146
 
147
  if ($offset === -2) {
148
+ return ! is_null($this->sourceLine);
149
  }
150
 
151
+ if ($offset === -1 ||
152
+ $offset === 0 ||
153
+ $offset === 1 ||
154
+ $offset === 2
155
  ) {
156
  return true;
157
  }
259
  /**
260
  * Output number
261
  *
262
+ * @param \ScssPhp\ScssPhp\Compiler $compiler
263
  *
264
  * @return string
265
  */
assets/libraries/scssphp/src/Parser.php CHANGED
@@ -2,20 +2,21 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
 
14
- use Leafo\ScssPhp\Block;
15
- use Leafo\ScssPhp\Compiler;
16
- use Leafo\ScssPhp\Exception\ParserException;
17
- use Leafo\ScssPhp\Node;
18
- use Leafo\ScssPhp\Type;
 
19
 
20
  /**
21
  * Parser
@@ -53,6 +54,8 @@ class Parser
53
  protected static $operatorPattern;
54
  protected static $whitePattern;
55
 
 
 
56
  private $sourceName;
57
  private $sourceIndex;
58
  private $sourcePositions;
@@ -61,27 +64,32 @@ class Parser
61
  private $env;
62
  private $inParens;
63
  private $eatWhiteDefault;
 
64
  private $buffer;
65
  private $utf8;
66
  private $encoding;
67
  private $patternModifiers;
 
68
 
69
  /**
70
  * Constructor
71
  *
72
  * @api
73
  *
74
- * @param string $sourceName
75
- * @param integer $sourceIndex
76
- * @param string $encoding
 
77
  */
78
- public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8')
79
  {
80
  $this->sourceName = $sourceName ?: '(stdin)';
81
  $this->sourceIndex = $sourceIndex;
82
  $this->charset = null;
83
  $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8';
84
  $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';
 
 
85
 
86
  if (empty(static::$operatorPattern)) {
87
  static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
@@ -95,6 +103,10 @@ class Parser
95
  ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS'
96
  : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS';
97
  }
 
 
 
 
98
  }
99
 
100
  /**
@@ -116,13 +128,15 @@ class Parser
116
  *
117
  * @param string $msg
118
  *
119
- * @throws \Leafo\ScssPhp\Exception\ParserException
120
  */
121
  public function throwParseError($msg = 'parse error')
122
  {
123
- list($line, /* $column */) = $this->getSourcePosition($this->count);
124
 
125
- $loc = empty($this->sourceName) ? "line: $line" : "$this->sourceName on line $line";
 
 
126
 
127
  if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
128
  throw new ParserException("$msg: failed at `$m[1]` $loc");
@@ -138,10 +152,23 @@ class Parser
138
  *
139
  * @param string $buffer
140
  *
141
- * @return \Leafo\ScssPhp\Block
142
  */
143
  public function parse($buffer)
144
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  // strip BOM (byte order marker)
146
  if (substr($buffer, 0, 3) === "\xef\xbb\xbf") {
147
  $buffer = substr($buffer, 3);
@@ -177,10 +204,12 @@ class Parser
177
  array_unshift($this->env->children, $this->charset);
178
  }
179
 
180
- $this->env->isRoot = true;
181
-
182
  $this->restoreEncoding();
183
 
 
 
 
 
184
  return $this->env;
185
  }
186
 
@@ -189,8 +218,8 @@ class Parser
189
  *
190
  * @api
191
  *
192
- * @param string $buffer
193
- * @param string $out
194
  *
195
  * @return boolean
196
  */
@@ -216,8 +245,8 @@ class Parser
216
  *
217
  * @api
218
  *
219
- * @param string $buffer
220
- * @param string $out
221
  *
222
  * @return boolean
223
  */
@@ -238,6 +267,33 @@ class Parser
238
  return $selector;
239
  }
240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  /**
242
  * Parse a single chunk off the head of the buffer and append it to the
243
  * current parse environment.
@@ -271,7 +327,7 @@ class Parser
271
  * the buffer position will be left at an invalid state. In order to
272
  * avoid this, Compiler::seek() is used to remember and set buffer positions.
273
  *
274
- * Before parsing a chain, use $s = $this->seek() to remember the current
275
  * position into $s. Then if a chain fails, use $this->seek($s) to
276
  * go back where we started.
277
  *
@@ -279,25 +335,28 @@ class Parser
279
  */
280
  protected function parseChunk()
281
  {
282
- $s = $this->seek();
283
 
284
  // the directives
285
  if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
286
- if ($this->literal('@at-root') &&
287
  ($this->selectors($selector) || true) &&
288
  ($this->map($with) || true) &&
289
- $this->literal('{')
 
 
 
290
  ) {
291
  $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
292
  $atRoot->selector = $selector;
293
- $atRoot->with = $with;
294
 
295
  return true;
296
  }
297
 
298
  $this->seek($s);
299
 
300
- if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) {
301
  $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
302
  $media->queryList = $mediaQueryList[2];
303
 
@@ -306,10 +365,10 @@ class Parser
306
 
307
  $this->seek($s);
308
 
309
- if ($this->literal('@mixin') &&
310
  $this->keyword($mixinName) &&
311
  ($this->argumentDef($args) || true) &&
312
- $this->literal('{')
313
  ) {
314
  $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
315
  $mixin->name = $mixinName;
@@ -320,15 +379,24 @@ class Parser
320
 
321
  $this->seek($s);
322
 
323
- if ($this->literal('@include') &&
324
  $this->keyword($mixinName) &&
325
- ($this->literal('(') &&
326
  ($this->argValues($argValues) || true) &&
327
- $this->literal(')') || true) &&
328
  ($this->end() ||
329
- $this->literal('{') && $hasBlock = true)
 
 
 
330
  ) {
331
- $child = [Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null];
 
 
 
 
 
 
332
 
333
  if (! empty($hasBlock)) {
334
  $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
@@ -342,7 +410,7 @@ class Parser
342
 
343
  $this->seek($s);
344
 
345
- if ($this->literal('@scssphp-import-once') &&
346
  $this->valueList($importPath) &&
347
  $this->end()
348
  ) {
@@ -353,7 +421,7 @@ class Parser
353
 
354
  $this->seek($s);
355
 
356
- if ($this->literal('@import') &&
357
  $this->valueList($importPath) &&
358
  $this->end()
359
  ) {
@@ -364,7 +432,7 @@ class Parser
364
 
365
  $this->seek($s);
366
 
367
- if ($this->literal('@import') &&
368
  $this->url($importPath) &&
369
  $this->end()
370
  ) {
@@ -375,7 +443,7 @@ class Parser
375
 
376
  $this->seek($s);
377
 
378
- if ($this->literal('@extend') &&
379
  $this->selectors($selectors) &&
380
  $this->end()
381
  ) {
@@ -388,10 +456,10 @@ class Parser
388
 
389
  $this->seek($s);
390
 
391
- if ($this->literal('@function') &&
392
  $this->keyword($fnName) &&
393
  $this->argumentDef($args) &&
394
- $this->literal('{')
395
  ) {
396
  $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
397
  $func->name = $fnName;
@@ -402,7 +470,7 @@ class Parser
402
 
403
  $this->seek($s);
404
 
405
- if ($this->literal('@break') && $this->end()) {
406
  $this->append([Type::T_BREAK], $s);
407
 
408
  return true;
@@ -410,7 +478,7 @@ class Parser
410
 
411
  $this->seek($s);
412
 
413
- if ($this->literal('@continue') && $this->end()) {
414
  $this->append([Type::T_CONTINUE], $s);
415
 
416
  return true;
@@ -418,8 +486,7 @@ class Parser
418
 
419
  $this->seek($s);
420
 
421
-
422
- if ($this->literal('@return') && ($this->valueList($retVal) || true) && $this->end()) {
423
  $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
424
 
425
  return true;
@@ -427,11 +494,11 @@ class Parser
427
 
428
  $this->seek($s);
429
 
430
- if ($this->literal('@each') &&
431
  $this->genericList($varNames, 'variable', ',', false) &&
432
- $this->literal('in') &&
433
  $this->valueList($list) &&
434
- $this->literal('{')
435
  ) {
436
  $each = $this->pushSpecialBlock(Type::T_EACH, $s);
437
 
@@ -446,9 +513,9 @@ class Parser
446
 
447
  $this->seek($s);
448
 
449
- if ($this->literal('@while') &&
450
  $this->expression($cond) &&
451
- $this->literal('{')
452
  ) {
453
  $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
454
  $while->cond = $cond;
@@ -458,19 +525,19 @@ class Parser
458
 
459
  $this->seek($s);
460
 
461
- if ($this->literal('@for') &&
462
  $this->variable($varName) &&
463
- $this->literal('from') &&
464
  $this->expression($start) &&
465
- ($this->literal('through') ||
466
- ($forUntil = true && $this->literal('to'))) &&
467
  $this->expression($end) &&
468
- $this->literal('{')
469
  ) {
470
  $for = $this->pushSpecialBlock(Type::T_FOR, $s);
471
- $for->var = $varName[1];
472
  $for->start = $start;
473
- $for->end = $end;
474
  $for->until = isset($forUntil);
475
 
476
  return true;
@@ -478,9 +545,15 @@ class Parser
478
 
479
  $this->seek($s);
480
 
481
- if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) {
482
  $if = $this->pushSpecialBlock(Type::T_IF, $s);
483
- $if->cond = $cond;
 
 
 
 
 
 
484
  $if->cases = [];
485
 
486
  return true;
@@ -488,7 +561,7 @@ class Parser
488
 
489
  $this->seek($s);
490
 
491
- if ($this->literal('@debug') &&
492
  $this->valueList($value) &&
493
  $this->end()
494
  ) {
@@ -499,7 +572,7 @@ class Parser
499
 
500
  $this->seek($s);
501
 
502
- if ($this->literal('@warn') &&
503
  $this->valueList($value) &&
504
  $this->end()
505
  ) {
@@ -510,7 +583,7 @@ class Parser
510
 
511
  $this->seek($s);
512
 
513
- if ($this->literal('@error') &&
514
  $this->valueList($value) &&
515
  $this->end()
516
  ) {
@@ -521,8 +594,15 @@ class Parser
521
 
522
  $this->seek($s);
523
 
524
- if ($this->literal('@content') && $this->end()) {
525
- $this->append([Type::T_MIXIN_CONTENT], $s);
 
 
 
 
 
 
 
526
 
527
  return true;
528
  }
@@ -534,10 +614,10 @@ class Parser
534
  if (isset($last) && $last[0] === Type::T_IF) {
535
  list(, $if) = $last;
536
 
537
- if ($this->literal('@else')) {
538
- if ($this->literal('{')) {
539
  $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
540
- } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) {
541
  $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
542
  $else->cond = $cond;
543
  }
@@ -554,7 +634,7 @@ class Parser
554
  }
555
 
556
  // only retain the first @charset directive encountered
557
- if ($this->literal('@charset') &&
558
  $this->valueList($charset) &&
559
  $this->end()
560
  ) {
@@ -575,11 +655,24 @@ class Parser
575
 
576
  $this->seek($s);
577
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
  // doesn't match built in directive, do generic one
579
- if ($this->literal('@', false) &&
580
  $this->keyword($dirName) &&
581
  ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
582
- $this->literal('{')
583
  ) {
584
  if ($dirName === 'media') {
585
  $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
@@ -597,13 +690,26 @@ class Parser
597
 
598
  $this->seek($s);
599
 
 
 
 
 
 
 
 
 
 
 
 
 
 
600
  return false;
601
  }
602
 
603
  // property shortcut
604
  // captures most properties before having to parse a selector
605
  if ($this->keyword($name, false) &&
606
- $this->literal(': ') &&
607
  $this->valueList($value) &&
608
  $this->end()
609
  ) {
@@ -617,7 +723,7 @@ class Parser
617
 
618
  // variable assigns
619
  if ($this->variable($name) &&
620
- $this->literal(':') &&
621
  $this->valueList($value) &&
622
  $this->end()
623
  ) {
@@ -631,31 +737,42 @@ class Parser
631
  $this->seek($s);
632
 
633
  // misc
634
- if ($this->literal('-->')) {
635
  return true;
636
  }
637
 
638
  // opening css block
639
- if ($this->selectors($selectors) && $this->literal('{')) {
640
  $this->pushBlock($selectors, $s);
641
 
 
 
 
 
 
642
  return true;
643
  }
644
 
645
  $this->seek($s);
646
 
647
  // property assign, or nested assign
648
- if ($this->propertyName($name) && $this->literal(':')) {
649
  $foundSomething = false;
650
 
651
  if ($this->valueList($value)) {
 
 
 
 
652
  $this->append([Type::T_ASSIGN, $name, $value], $s);
653
  $foundSomething = true;
654
  }
655
 
656
- if ($this->literal('{')) {
657
  $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
658
  $propBlock->prefix = $name;
 
 
659
  $foundSomething = true;
660
  } elseif ($foundSomething) {
661
  $foundSomething = $this->end();
@@ -669,9 +786,15 @@ class Parser
669
  $this->seek($s);
670
 
671
  // closing a block
672
- if ($this->literal('}')) {
673
  $block = $this->popBlock();
674
 
 
 
 
 
 
 
675
  if (isset($block->type) && $block->type === Type::T_INCLUDE) {
676
  $include = $block->child;
677
  unset($block->child);
@@ -682,12 +805,21 @@ class Parser
682
  $this->append([$type, $block], $s);
683
  }
684
 
 
 
 
 
 
 
 
 
 
685
  return true;
686
  }
687
 
688
  // extra stuff
689
- if ($this->literal(';') ||
690
- $this->literal('<!--')
691
  ) {
692
  return true;
693
  }
@@ -701,7 +833,7 @@ class Parser
701
  * @param array $selectors
702
  * @param integer $pos
703
  *
704
- * @return \Leafo\ScssPhp\Block
705
  */
706
  protected function pushBlock($selectors, $pos = 0)
707
  {
@@ -729,6 +861,15 @@ class Parser
729
 
730
  $this->env = $b;
731
 
 
 
 
 
 
 
 
 
 
732
  return $b;
733
  }
734
 
@@ -738,7 +879,7 @@ class Parser
738
  * @param string $type
739
  * @param integer $pos
740
  *
741
- * @return \Leafo\ScssPhp\Block
742
  */
743
  protected function pushSpecialBlock($type, $pos)
744
  {
@@ -751,26 +892,33 @@ class Parser
751
  /**
752
  * Pop scope and return last block
753
  *
754
- * @return \Leafo\ScssPhp\Block
755
  *
756
  * @throws \Exception
757
  */
758
  protected function popBlock()
759
  {
 
 
 
 
 
 
 
760
  $block = $this->env;
761
 
762
  if (empty($block->parent)) {
763
  $this->throwParseError('unexpected }');
764
  }
765
 
 
 
 
 
 
766
  $this->env = $block->parent;
767
- unset($block->parent);
768
 
769
- $comments = $block->comments;
770
- if (count($comments)) {
771
- $this->env->comments = $comments;
772
- unset($block->comments);
773
- }
774
 
775
  return $block;
776
  }
@@ -800,18 +948,10 @@ class Parser
800
  * Seek to position in input stream (or return current position in input stream)
801
  *
802
  * @param integer $where
803
- *
804
- * @return integer
805
  */
806
- protected function seek($where = null)
807
  {
808
- if ($where === null) {
809
- return $this->count;
810
- }
811
-
812
  $this->count = $where;
813
-
814
- return true;
815
  }
816
 
817
  /**
@@ -866,52 +1006,78 @@ class Parser
866
  */
867
  protected function match($regex, &$out, $eatWhitespace = null)
868
  {
 
 
 
 
 
 
 
 
869
  if (! isset($eatWhitespace)) {
870
  $eatWhitespace = $this->eatWhiteDefault;
871
  }
872
 
873
- $r = '/' . $regex . '/' . $this->patternModifiers;
 
 
874
 
875
- if (preg_match($r, $this->buffer, $out, null, $this->count)) {
876
- $this->count += strlen($out[0]);
877
 
878
- if ($eatWhitespace) {
879
- $this->whitespace();
880
- }
 
 
 
 
 
 
 
 
 
 
881
 
882
- return true;
 
 
 
883
  }
884
 
885
- return false;
 
 
 
 
886
  }
887
 
888
  /**
889
  * Match literal string
890
  *
891
  * @param string $what
 
892
  * @param boolean $eatWhitespace
893
  *
894
  * @return boolean
895
  */
896
- protected function literal($what, $eatWhitespace = null)
897
  {
898
- if (! isset($eatWhitespace)) {
899
- $eatWhitespace = $this->eatWhiteDefault;
900
  }
901
 
902
- $len = strlen($what);
903
 
904
- if (strcasecmp(substr($this->buffer, $this->count, $len), $what) === 0) {
905
- $this->count += $len;
906
-
907
- if ($eatWhitespace) {
908
- $this->whitespace();
909
- }
910
 
911
- return true;
 
912
  }
913
 
914
- return false;
915
  }
916
 
917
  /**
@@ -925,12 +1091,59 @@ class Parser
925
 
926
  while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) {
927
  if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
928
- $this->appendComment([Type::T_COMMENT, $m[1]]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
929
 
930
- $this->commentsSeen[$this->count] = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
931
  }
932
 
933
- $this->count += strlen($m[0]);
934
  $gotWhite = true;
935
  }
936
 
@@ -944,9 +1157,26 @@ class Parser
944
  */
945
  protected function appendComment($comment)
946
  {
947
- $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
948
 
949
- $this->env->comments[] = $comment;
 
950
  }
951
 
952
  /**
@@ -957,19 +1187,21 @@ class Parser
957
  */
958
  protected function append($statement, $pos = null)
959
  {
960
- if ($pos !== null) {
961
- list($line, $column) = $this->getSourcePosition($pos);
 
962
 
963
- $statement[static::SOURCE_LINE] = $line;
964
- $statement[static::SOURCE_COLUMN] = $column;
965
- $statement[static::SOURCE_INDEX] = $this->sourceIndex;
966
- }
967
 
968
- $this->env->children[] = $statement;
 
969
 
970
  $comments = $this->env->comments;
971
 
972
- if (count($comments)) {
973
  $this->env->children = array_merge($this->env->children, $comments);
974
  $this->env->comments = [];
975
  }
@@ -1013,7 +1245,7 @@ class Parser
1013
  $expressions = null;
1014
  $parts = [];
1015
 
1016
- if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) &&
1017
  $this->mixedKeyword($mediaType)
1018
  ) {
1019
  $prop = [Type::T_MEDIA_TYPE];
@@ -1040,7 +1272,7 @@ class Parser
1040
  $parts[] = $prop;
1041
  }
1042
 
1043
- if (empty($parts) || $this->literal('and')) {
1044
  $this->genericList($expressions, 'mediaExpression', 'and', false);
1045
 
1046
  if (is_array($expressions)) {
@@ -1053,6 +1285,130 @@ class Parser
1053
  return true;
1054
  }
1055
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1056
  /**
1057
  * Parse media expression
1058
  *
@@ -1062,13 +1418,13 @@ class Parser
1062
  */
1063
  protected function mediaExpression(&$out)
1064
  {
1065
- $s = $this->seek();
1066
  $value = null;
1067
 
1068
- if ($this->literal('(') &&
1069
  $this->expression($feature) &&
1070
- ($this->literal(':') && $this->expression($value) || true) &&
1071
- $this->literal(')')
1072
  ) {
1073
  $out = [Type::T_MEDIA_EXPRESSION, $feature];
1074
 
@@ -1111,20 +1467,21 @@ class Parser
1111
  */
1112
  protected function argValue(&$out)
1113
  {
1114
- $s = $this->seek();
1115
 
1116
  $keyword = null;
1117
 
1118
- if (! $this->variable($keyword) || ! $this->literal(':')) {
1119
  $this->seek($s);
 
1120
  $keyword = null;
1121
  }
1122
 
1123
  if ($this->genericList($value, 'expression')) {
1124
  $out = [$keyword, $value, false];
1125
- $s = $this->seek();
1126
 
1127
- if ($this->literal('...')) {
1128
  $out[2] = true;
1129
  } else {
1130
  $this->seek($s);
@@ -1139,13 +1496,18 @@ class Parser
1139
  /**
1140
  * Parse comma separated value list
1141
  *
1142
- * @param string $out
1143
  *
1144
  * @return boolean
1145
  */
1146
  protected function valueList(&$out)
1147
  {
1148
- return $this->genericList($out, 'spaceList', ',');
 
 
 
 
 
1149
  }
1150
 
1151
  /**
@@ -1172,25 +1534,31 @@ class Parser
1172
  */
1173
  protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
1174
  {
1175
- $s = $this->seek();
1176
  $items = [];
 
1177
 
1178
  while ($this->$parseItem($value)) {
 
1179
  $items[] = $value;
1180
 
1181
  if ($delim) {
1182
- if (! $this->literal($delim)) {
1183
  break;
1184
  }
 
1185
  }
1186
  }
1187
 
1188
- if (count($items) === 0) {
1189
  $this->seek($s);
1190
 
1191
  return false;
1192
  }
1193
 
 
 
 
1194
  if ($flatten && count($items) === 1) {
1195
  $out = $items[0];
1196
  } else {
@@ -1204,36 +1572,110 @@ class Parser
1204
  * Parse expression
1205
  *
1206
  * @param array $out
 
 
1207
  *
1208
  * @return boolean
1209
  */
1210
- protected function expression(&$out)
1211
  {
1212
- $s = $this->seek();
 
 
 
 
 
 
 
 
 
 
 
1213
 
1214
- if ($this->literal('(')) {
1215
- if ($this->literal(')')) {
1216
- $out = [Type::T_LIST, '', []];
1217
 
1218
  return true;
1219
  }
1220
 
1221
- if ($this->valueList($out) && $this->literal(')') && $out[0] === Type::T_LIST) {
 
 
 
 
 
 
 
 
 
 
 
1222
  return true;
1223
  }
1224
 
1225
  $this->seek($s);
 
1226
 
1227
- if ($this->map($out)) {
1228
- return true;
 
 
 
1229
  }
1230
 
1231
- $this->seek($s);
 
 
1232
  }
1233
 
1234
- if ($this->value($lhs)) {
1235
- $out = $this->expHelper($lhs, 0);
 
1236
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1237
  return true;
1238
  }
1239
 
@@ -1252,7 +1694,7 @@ class Parser
1252
  {
1253
  $operators = static::$operatorPattern;
1254
 
1255
- $ss = $this->seek();
1256
  $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1257
  ctype_space($this->buffer[$this->count - 1]);
1258
 
@@ -1271,7 +1713,7 @@ class Parser
1271
  break;
1272
  }
1273
 
1274
- if (! $this->value($rhs)) {
1275
  break;
1276
  }
1277
 
@@ -1281,7 +1723,7 @@ class Parser
1281
  }
1282
 
1283
  $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
1284
- $ss = $this->seek();
1285
  $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1286
  ctype_space($this->buffer[$this->count - 1]);
1287
  }
@@ -1300,14 +1742,23 @@ class Parser
1300
  */
1301
  protected function value(&$out)
1302
  {
1303
- $s = $this->seek();
 
 
1304
 
1305
- if ($this->literal('url(') && $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)) {
1306
- $len = strspn($this->buffer, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=', $this->count);
 
 
 
 
 
 
 
1307
 
1308
  $this->count += $len;
1309
 
1310
- if ($this->literal(')')) {
1311
  $content = substr($this->buffer, $s, $this->count - $s);
1312
  $out = [Type::T_KEYWORD, $content];
1313
 
@@ -1317,56 +1768,113 @@ class Parser
1317
 
1318
  $this->seek($s);
1319
 
1320
- if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) {
1321
- $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1322
 
1323
- return true;
 
 
 
 
 
1324
  }
1325
 
1326
  $this->seek($s);
1327
 
1328
- if ($this->literal('not', false) && $this->parenValue($inner)) {
1329
- $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
 
 
 
 
 
 
 
1330
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1331
  return true;
1332
  }
1333
 
1334
- $this->seek($s);
 
 
 
 
1335
 
1336
- if ($this->literal('+') && $this->value($inner)) {
1337
- $out = [Type::T_UNARY, '+', $inner, $this->inParens];
1338
 
1339
  return true;
1340
  }
1341
 
1342
- $this->seek($s);
 
 
1343
 
1344
- // negation
1345
- if ($this->literal('-', false) &&
1346
- ($this->variable($inner) ||
1347
- $this->unit($inner) ||
1348
- $this->parenValue($inner))
1349
- ) {
1350
- $out = [Type::T_UNARY, '-', $inner, $this->inParens];
1351
 
 
1352
  return true;
1353
  }
1354
 
1355
- $this->seek($s);
 
 
 
 
 
 
1356
 
1357
- if ($this->parenValue($out) ||
1358
- $this->interpolation($out) ||
1359
- $this->variable($out) ||
1360
- $this->color($out) ||
1361
- $this->unit($out) ||
1362
- $this->string($out) ||
1363
- $this->func($out) ||
1364
- $this->progid($out)
1365
- ) {
1366
  return true;
1367
  }
1368
 
1369
- if ($this->keyword($keyword)) {
 
 
 
 
 
 
1370
  if ($keyword === 'null') {
1371
  $out = [Type::T_NULL];
1372
  } else {
@@ -1388,12 +1896,12 @@ class Parser
1388
  */
1389
  protected function parenValue(&$out)
1390
  {
1391
- $s = $this->seek();
1392
 
1393
  $inParens = $this->inParens;
1394
 
1395
- if ($this->literal('(')) {
1396
- if ($this->literal(')')) {
1397
  $out = [Type::T_LIST, '', []];
1398
 
1399
  return true;
@@ -1401,7 +1909,7 @@ class Parser
1401
 
1402
  $this->inParens = true;
1403
 
1404
- if ($this->expression($exp) && $this->literal(')')) {
1405
  $out = $exp;
1406
  $this->inParens = $inParens;
1407
 
@@ -1424,15 +1932,15 @@ class Parser
1424
  */
1425
  protected function progid(&$out)
1426
  {
1427
- $s = $this->seek();
1428
 
1429
- if ($this->literal('progid:', false) &&
1430
  $this->openString('(', $fn) &&
1431
- $this->literal('(')
1432
  ) {
1433
  $this->openString(')', $args, '(');
1434
 
1435
- if ($this->literal(')')) {
1436
  $out = [Type::T_STRING, '', [
1437
  'progid:', $fn, '(', $args, ')'
1438
  ]];
@@ -1449,17 +1957,16 @@ class Parser
1449
  /**
1450
  * Parse function call
1451
  *
1452
- * @param array $out
 
1453
  *
1454
  * @return boolean
1455
  */
1456
- protected function func(&$func)
1457
  {
1458
- $s = $this->seek();
1459
 
1460
- if ($this->keyword($name, false) &&
1461
- $this->literal('(')
1462
- ) {
1463
  if ($name === 'alpha' && $this->argumentList($args)) {
1464
  $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]];
1465
 
@@ -1467,9 +1974,9 @@ class Parser
1467
  }
1468
 
1469
  if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
1470
- $ss = $this->seek();
1471
 
1472
- if ($this->argValues($args) && $this->literal(')')) {
1473
  $func = [Type::T_FUNCTION_CALL, $name, $args];
1474
 
1475
  return true;
@@ -1479,7 +1986,7 @@ class Parser
1479
  }
1480
 
1481
  if (($this->openString(')', $str, '(') || true) &&
1482
- $this->literal(')')
1483
  ) {
1484
  $args = [];
1485
 
@@ -1507,13 +2014,13 @@ class Parser
1507
  */
1508
  protected function argumentList(&$out)
1509
  {
1510
- $s = $this->seek();
1511
- $this->literal('(');
1512
 
1513
  $args = [];
1514
 
1515
  while ($this->keyword($var)) {
1516
- if ($this->literal('=') && $this->expression($exp)) {
1517
  $args[] = [Type::T_STRING, '', [$var . '=']];
1518
  $arg = $exp;
1519
  } else {
@@ -1522,14 +2029,14 @@ class Parser
1522
 
1523
  $args[] = $arg;
1524
 
1525
- if (! $this->literal(',')) {
1526
  break;
1527
  }
1528
 
1529
  $args[] = [Type::T_STRING, '', [', ']];
1530
  }
1531
 
1532
- if (! $this->literal(')') || ! count($args)) {
1533
  $this->seek($s);
1534
 
1535
  return false;
@@ -1549,28 +2056,28 @@ class Parser
1549
  */
1550
  protected function argumentDef(&$out)
1551
  {
1552
- $s = $this->seek();
1553
- $this->literal('(');
1554
 
1555
  $args = [];
1556
 
1557
  while ($this->variable($var)) {
1558
  $arg = [$var[1], null, false];
1559
 
1560
- $ss = $this->seek();
1561
 
1562
- if ($this->literal(':') && $this->genericList($defaultVal, 'expression')) {
1563
  $arg[1] = $defaultVal;
1564
  } else {
1565
  $this->seek($ss);
1566
  }
1567
 
1568
- $ss = $this->seek();
1569
 
1570
- if ($this->literal('...')) {
1571
- $sss = $this->seek();
1572
 
1573
- if (! $this->literal(')')) {
1574
  $this->throwParseError('... has to be after the final argument');
1575
  }
1576
 
@@ -1582,12 +2089,12 @@ class Parser
1582
 
1583
  $args[] = $arg;
1584
 
1585
- if (! $this->literal(',')) {
1586
  break;
1587
  }
1588
  }
1589
 
1590
- if (! $this->literal(')')) {
1591
  $this->seek($s);
1592
 
1593
  return false;
@@ -1607,27 +2114,27 @@ class Parser
1607
  */
1608
  protected function map(&$out)
1609
  {
1610
- $s = $this->seek();
1611
 
1612
- if (! $this->literal('(')) {
1613
  return false;
1614
  }
1615
 
1616
  $keys = [];
1617
  $values = [];
1618
 
1619
- while ($this->genericList($key, 'expression') && $this->literal(':') &&
1620
  $this->genericList($value, 'expression')
1621
  ) {
1622
  $keys[] = $key;
1623
  $values[] = $value;
1624
 
1625
- if (! $this->literal(',')) {
1626
  break;
1627
  }
1628
  }
1629
 
1630
- if (! count($keys) || ! $this->literal(')')) {
1631
  $this->seek($s);
1632
 
1633
  return false;
@@ -1647,29 +2154,16 @@ class Parser
1647
  */
1648
  protected function color(&$out)
1649
  {
1650
- $color = [Type::T_COLOR];
1651
 
1652
- if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
1653
- if (isset($m[3])) {
1654
- $num = hexdec($m[3]);
1655
-
1656
- foreach ([3, 2, 1] as $i) {
1657
- $t = $num & 0xf;
1658
- $color[$i] = $t << 4 | $t;
1659
- $num >>= 4;
1660
- }
1661
- } else {
1662
- $num = hexdec($m[2]);
1663
-
1664
- foreach ([3, 2, 1] as $i) {
1665
- $color[$i] = $num & 0xff;
1666
- $num >>= 8;
1667
- }
1668
  }
1669
 
1670
- $out = $color;
1671
-
1672
- return true;
1673
  }
1674
 
1675
  return false;
@@ -1678,16 +2172,24 @@ class Parser
1678
  /**
1679
  * Parse number with unit
1680
  *
1681
- * @param array $out
1682
  *
1683
  * @return boolean
1684
  */
1685
  protected function unit(&$unit)
1686
  {
1687
- if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
1688
- $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
1689
 
1690
- return true;
 
 
 
 
 
 
 
 
 
1691
  }
1692
 
1693
  return false;
@@ -1702,11 +2204,11 @@ class Parser
1702
  */
1703
  protected function string(&$out)
1704
  {
1705
- $s = $this->seek();
1706
 
1707
- if ($this->literal('"', false)) {
1708
  $delim = '"';
1709
- } elseif ($this->literal("'", false)) {
1710
  $delim = "'";
1711
  } else {
1712
  return false;
@@ -1733,10 +2235,18 @@ class Parser
1733
  $content[] = '#{'; // ignore it
1734
  }
1735
  } elseif ($m[2] === '\\') {
1736
- if ($this->literal('"', false)) {
1737
  $content[] = $m[2] . '"';
1738
- } elseif ($this->literal("'", false)) {
1739
  $content[] = $m[2] . "'";
 
 
 
 
 
 
 
 
1740
  } else {
1741
  $content[] = $m[2];
1742
  }
@@ -1748,12 +2258,14 @@ class Parser
1748
 
1749
  $this->eatWhiteDefault = $oldWhite;
1750
 
1751
- if ($this->literal($delim)) {
1752
  if ($hasInterpolation) {
1753
  $delim = '"';
1754
 
1755
  foreach ($content as &$string) {
1756
- if ($string === "\\'") {
 
 
1757
  $string = "'";
1758
  } elseif ($string === '\\"') {
1759
  $string = '"';
@@ -1774,11 +2286,12 @@ class Parser
1774
  /**
1775
  * Parse keyword or interpolation
1776
  *
1777
- * @param array $out
 
1778
  *
1779
  * @return boolean
1780
  */
1781
- protected function mixedKeyword(&$out)
1782
  {
1783
  $parts = [];
1784
 
@@ -1786,7 +2299,7 @@ class Parser
1786
  $this->eatWhiteDefault = false;
1787
 
1788
  for (;;) {
1789
- if ($this->keyword($key)) {
1790
  $parts[] = $key;
1791
  continue;
1792
  }
@@ -1801,7 +2314,7 @@ class Parser
1801
 
1802
  $this->eatWhiteDefault = $oldWhite;
1803
 
1804
- if (count($parts) === 0) {
1805
  return false;
1806
  }
1807
 
@@ -1867,7 +2380,7 @@ class Parser
1867
 
1868
  $this->eatWhiteDefault = $oldWhite;
1869
 
1870
- if (count($content) === 0) {
1871
  return false;
1872
  }
1873
 
@@ -1884,8 +2397,8 @@ class Parser
1884
  /**
1885
  * Parser interpolation
1886
  *
1887
- * @param array $out
1888
- * @param boolean $lookWhite save information about whitespace before and after
1889
  *
1890
  * @return boolean
1891
  */
@@ -1894,17 +2407,22 @@ class Parser
1894
  $oldWhite = $this->eatWhiteDefault;
1895
  $this->eatWhiteDefault = true;
1896
 
1897
- $s = $this->seek();
1898
 
1899
- if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) {
1900
- if ($lookWhite) {
1901
- $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
1902
- $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
1903
  } else {
1904
- $left = $right = false;
 
 
 
 
 
 
 
1905
  }
1906
 
1907
- $out = [Type::T_INTERPOLATE, $value, $left, $right];
1908
  $this->eatWhiteDefault = $oldWhite;
1909
 
1910
  if ($this->eatWhiteDefault) {
@@ -1915,6 +2433,7 @@ class Parser
1915
  }
1916
 
1917
  $this->seek($s);
 
1918
  $this->eatWhiteDefault = $oldWhite;
1919
 
1920
  return false;
@@ -1945,7 +2464,7 @@ class Parser
1945
  continue;
1946
  }
1947
 
1948
- if (count($parts) === 0 && $this->match('[:.#]', $m, false)) {
1949
  // css hacks
1950
  $parts[] = $m[0];
1951
  continue;
@@ -1956,7 +2475,7 @@ class Parser
1956
 
1957
  $this->eatWhiteDefault = $oldWhite;
1958
 
1959
- if (count($parts) === 0) {
1960
  return false;
1961
  }
1962
 
@@ -1984,28 +2503,29 @@ class Parser
1984
  /**
1985
  * Parse comma separated selector list
1986
  *
1987
- * @param array $out
 
1988
  *
1989
  * @return boolean
1990
  */
1991
- protected function selectors(&$out)
1992
  {
1993
- $s = $this->seek();
1994
  $selectors = [];
1995
 
1996
- while ($this->selector($sel)) {
1997
  $selectors[] = $sel;
1998
 
1999
- if (! $this->literal(',')) {
2000
  break;
2001
  }
2002
 
2003
- while ($this->literal(',')) {
2004
  ; // ignore extra
2005
  }
2006
  }
2007
 
2008
- if (count($selectors) === 0) {
2009
  $this->seek($s);
2010
 
2011
  return false;
@@ -2019,27 +2539,36 @@ class Parser
2019
  /**
2020
  * Parse whitespace separated selector list
2021
  *
2022
- * @param array $out
 
2023
  *
2024
  * @return boolean
2025
  */
2026
- protected function selector(&$out)
2027
  {
2028
  $selector = [];
2029
 
2030
  for (;;) {
2031
- if ($this->match('[>+~]+', $m)) {
2032
- $selector[] = [$m[0]];
2033
- continue;
 
 
 
 
 
 
 
 
2034
  }
2035
 
2036
- if ($this->selectorSingle($part)) {
2037
  $selector[] = $part;
2038
  $this->match('\s+', $m);
2039
  continue;
2040
  }
2041
 
2042
- if ($this->match('\/[^\/]+\/', $m)) {
2043
  $selector[] = [$m[0]];
2044
  continue;
2045
  }
@@ -2047,11 +2576,12 @@ class Parser
2047
  break;
2048
  }
2049
 
2050
- if (count($selector) === 0) {
2051
  return false;
2052
  }
2053
 
2054
  $out = $selector;
 
2055
  return true;
2056
  }
2057
 
@@ -2062,112 +2592,176 @@ class Parser
2062
  * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
2063
  * }}
2064
  *
2065
- * @param array $out
 
2066
  *
2067
  * @return boolean
2068
  */
2069
- protected function selectorSingle(&$out)
2070
  {
2071
  $oldWhite = $this->eatWhiteDefault;
2072
  $this->eatWhiteDefault = false;
2073
 
2074
  $parts = [];
2075
 
2076
- if ($this->literal('*', false)) {
2077
  $parts[] = '*';
2078
  }
2079
 
2080
  for (;;) {
2081
- // see if we can stop early
2082
- if ($this->match('\s*[{,]', $m)) {
2083
- $this->count--;
2084
  break;
2085
  }
2086
 
2087
- $s = $this->seek();
 
2088
 
2089
- // self
2090
- if ($this->literal('&', false)) {
2091
- $parts[] = Compiler::$selfSelector;
2092
- continue;
2093
  }
2094
 
2095
- if ($this->literal('.', false)) {
2096
- $parts[] = '.';
2097
- continue;
2098
  }
2099
 
2100
- if ($this->literal('|', false)) {
2101
- $parts[] = '|';
2102
- continue;
 
 
 
 
 
 
 
 
 
 
 
 
 
2103
  }
2104
 
2105
- if ($this->match('\\\\\S', $m)) {
2106
  $parts[] = $m[0];
2107
  continue;
2108
  }
2109
 
2110
- // for keyframes
2111
- if ($this->unit($unit)) {
2112
- $parts[] = $unit;
2113
- continue;
2114
- }
2115
 
2116
- if ($this->keyword($name)) {
2117
- $parts[] = $name;
2118
- continue;
2119
- }
 
2120
 
2121
- if ($this->interpolation($inter)) {
2122
- $parts[] = $inter;
2123
- continue;
2124
  }
2125
 
2126
- if ($this->literal('%', false) && $this->placeholder($placeholder)) {
2127
- $parts[] = '%';
2128
- $parts[] = $placeholder;
2129
- continue;
2130
- }
2131
 
2132
- if ($this->literal('#', false)) {
2133
  $parts[] = '#';
 
2134
  continue;
2135
  }
2136
 
2137
  // a pseudo selector
2138
- if ($this->match('::?', $m) && $this->mixedKeyword($nameParts)) {
2139
- $parts[] = $m[0];
2140
-
2141
- foreach ($nameParts as $sub) {
2142
- $parts[] = $sub;
 
 
2143
  }
2144
 
2145
- $ss = $this->seek();
 
2146
 
2147
- if ($this->literal('(') &&
2148
- ($this->openString(')', $str, '(') || true) &&
2149
- $this->literal(')')
2150
- ) {
2151
- $parts[] = '(';
2152
 
2153
- if (! empty($str)) {
2154
- $parts[] = $str;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2155
  }
2156
 
2157
- $parts[] = ')';
2158
- } else {
2159
- $this->seek($ss);
2160
  }
 
2161
 
2162
- continue;
 
 
 
 
 
 
 
 
2163
  }
2164
 
2165
  $this->seek($s);
2166
 
2167
  // attribute selector
2168
- if ($this->literal('[') &&
2169
- ($this->openString(']', $str, '[') || true) &&
2170
- $this->literal(']')
 
2171
  ) {
2172
  $parts[] = '[';
2173
 
@@ -2176,18 +2770,28 @@ class Parser
2176
  }
2177
 
2178
  $parts[] = ']';
2179
-
2180
  continue;
2181
  }
2182
 
2183
  $this->seek($s);
2184
 
 
 
 
 
 
 
 
 
 
 
 
2185
  break;
2186
  }
2187
 
2188
  $this->eatWhiteDefault = $oldWhite;
2189
 
2190
- if (count($parts) === 0) {
2191
  return false;
2192
  }
2193
 
@@ -2205,9 +2809,9 @@ class Parser
2205
  */
2206
  protected function variable(&$out)
2207
  {
2208
- $s = $this->seek();
2209
 
2210
- if ($this->literal('$', false) && $this->keyword($name)) {
2211
  $out = [Type::T_VARIABLE, $name];
2212
 
2213
  return true;
@@ -2230,7 +2834,7 @@ class Parser
2230
  {
2231
  if ($this->match(
2232
  $this->utf8
2233
- ? '(([\pL\w_\-\*!"\']|[\\\\].)([\pL\w\-_"\']|[\\\\].)*)'
2234
  : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
2235
  $m,
2236
  $eatWhitespace
@@ -2243,10 +2847,31 @@ class Parser
2243
  return false;
2244
  }
2245
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2246
  /**
2247
  * Parse a placeholder
2248
  *
2249
- * @param string $placeholder
2250
  *
2251
  * @return boolean
2252
  */
@@ -2254,8 +2879,8 @@ class Parser
2254
  {
2255
  if ($this->match(
2256
  $this->utf8
2257
- ? '([\pL\w\-_]+|#[{][$][\pL\w\-_]+[}])'
2258
- : '([\w\-_]+|#[{][$][\w\-_]+[}])',
2259
  $m
2260
  )) {
2261
  $placeholder = $m[1];
@@ -2263,6 +2888,10 @@ class Parser
2263
  return true;
2264
  }
2265
 
 
 
 
 
2266
  return false;
2267
  }
2268
 
@@ -2291,7 +2920,7 @@ class Parser
2291
  */
2292
  protected function end()
2293
  {
2294
- if ($this->literal(';')) {
2295
  return true;
2296
  }
2297
 
@@ -2320,12 +2949,9 @@ class Parser
2320
  while ($lastNode[0] === Type::T_KEYWORD && in_array($lastNode[1], ['!default', '!global'])) {
2321
  array_pop($token[2]);
2322
 
2323
- $node = end($token[2]);
2324
-
2325
- $token = $this->flattenList($token);
2326
-
2327
- $flags[] = $lastNode[1];
2328
-
2329
  $lastNode = $node;
2330
  }
2331
  }
@@ -2343,9 +2969,8 @@ class Parser
2343
  protected function stripOptionalFlag(&$selectors)
2344
  {
2345
  $optional = false;
2346
-
2347
  $selector = end($selectors);
2348
- $part = end($selector);
2349
 
2350
  if ($part === ['!optional']) {
2351
  array_pop($selectors[count($selectors) - 1]);
@@ -2389,6 +3014,8 @@ class Parser
2389
  $validChars = $allowNewline ? '.' : "[^\n]";
2390
  }
2391
 
 
 
2392
  if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
2393
  return false;
2394
  }
@@ -2453,7 +3080,7 @@ class Parser
2453
  *
2454
  * @param integer $pos
2455
  *
2456
- * @return integer
2457
  */
2458
  private function getSourcePosition($pos)
2459
  {
@@ -2488,9 +3115,10 @@ class Parser
2488
  return;
2489
  }
2490
 
2491
- $iniDirective = 'mbstring' . '.func_overload'; // deprecated in PHP 7.2
 
2492
 
2493
- if (ini_get($iniDirective) & 2) {
2494
  $this->encoding = mb_internal_encoding();
2495
 
2496
  mb_internal_encoding('iso-8859-1');
@@ -2502,7 +3130,7 @@ class Parser
2502
  */
2503
  private function restoreEncoding()
2504
  {
2505
- if ($this->encoding) {
2506
  mb_internal_encoding($this->encoding);
2507
  }
2508
  }
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
 
14
+ use ScssPhp\ScssPhp\Block;
15
+ use ScssPhp\ScssPhp\Cache;
16
+ use ScssPhp\ScssPhp\Compiler;
17
+ use ScssPhp\ScssPhp\Exception\ParserException;
18
+ use ScssPhp\ScssPhp\Node;
19
+ use ScssPhp\ScssPhp\Type;
20
 
21
  /**
22
  * Parser
54
  protected static $operatorPattern;
55
  protected static $whitePattern;
56
 
57
+ protected $cache;
58
+
59
  private $sourceName;
60
  private $sourceIndex;
61
  private $sourcePositions;
64
  private $env;
65
  private $inParens;
66
  private $eatWhiteDefault;
67
+ private $discardComments;
68
  private $buffer;
69
  private $utf8;
70
  private $encoding;
71
  private $patternModifiers;
72
+ private $commentsSeen;
73
 
74
  /**
75
  * Constructor
76
  *
77
  * @api
78
  *
79
+ * @param string $sourceName
80
+ * @param integer $sourceIndex
81
+ * @param string $encoding
82
+ * @param \ScssPhp\ScssPhp\Cache $cache
83
  */
84
+ public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', $cache = null)
85
  {
86
  $this->sourceName = $sourceName ?: '(stdin)';
87
  $this->sourceIndex = $sourceIndex;
88
  $this->charset = null;
89
  $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8';
90
  $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';
91
+ $this->commentsSeen = [];
92
+ $this->discardComments = false;
93
 
94
  if (empty(static::$operatorPattern)) {
95
  static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
103
  ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS'
104
  : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS';
105
  }
106
+
107
+ if ($cache) {
108
+ $this->cache = $cache;
109
+ }
110
  }
111
 
112
  /**
128
  *
129
  * @param string $msg
130
  *
131
+ * @throws \ScssPhp\ScssPhp\Exception\ParserException
132
  */
133
  public function throwParseError($msg = 'parse error')
134
  {
135
+ list($line, $column) = $this->getSourcePosition($this->count);
136
 
137
+ $loc = empty($this->sourceName)
138
+ ? "line: $line, column: $column"
139
+ : "$this->sourceName on line $line, at column $column";
140
 
141
  if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
142
  throw new ParserException("$msg: failed at `$m[1]` $loc");
152
  *
153
  * @param string $buffer
154
  *
155
+ * @return \ScssPhp\ScssPhp\Block
156
  */
157
  public function parse($buffer)
158
  {
159
+ if ($this->cache) {
160
+ $cacheKey = $this->sourceName . ":" . md5($buffer);
161
+ $parseOptions = [
162
+ 'charset' => $this->charset,
163
+ 'utf8' => $this->utf8,
164
+ ];
165
+ $v = $this->cache->getCache("parse", $cacheKey, $parseOptions);
166
+
167
+ if (! is_null($v)) {
168
+ return $v;
169
+ }
170
+ }
171
+
172
  // strip BOM (byte order marker)
173
  if (substr($buffer, 0, 3) === "\xef\xbb\xbf") {
174
  $buffer = substr($buffer, 3);
204
  array_unshift($this->env->children, $this->charset);
205
  }
206
 
 
 
207
  $this->restoreEncoding();
208
 
209
+ if ($this->cache) {
210
+ $this->cache->setCache("parse", $cacheKey, $this->env, $parseOptions);
211
+ }
212
+
213
  return $this->env;
214
  }
215
 
218
  *
219
  * @api
220
  *
221
+ * @param string $buffer
222
+ * @param string|array $out
223
  *
224
  * @return boolean
225
  */
245
  *
246
  * @api
247
  *
248
+ * @param string $buffer
249
+ * @param string|array $out
250
  *
251
  * @return boolean
252
  */
267
  return $selector;
268
  }
269
 
270
+ /**
271
+ * Parse a media Query
272
+ *
273
+ * @api
274
+ *
275
+ * @param string $buffer
276
+ * @param string|array $out
277
+ *
278
+ * @return boolean
279
+ */
280
+ public function parseMediaQueryList($buffer, &$out)
281
+ {
282
+ $this->count = 0;
283
+ $this->env = null;
284
+ $this->inParens = false;
285
+ $this->eatWhiteDefault = true;
286
+ $this->buffer = (string) $buffer;
287
+
288
+ $this->saveEncoding();
289
+
290
+ $isMediaQuery = $this->mediaQueryList($out);
291
+
292
+ $this->restoreEncoding();
293
+
294
+ return $isMediaQuery;
295
+ }
296
+
297
  /**
298
  * Parse a single chunk off the head of the buffer and append it to the
299
  * current parse environment.
327
  * the buffer position will be left at an invalid state. In order to
328
  * avoid this, Compiler::seek() is used to remember and set buffer positions.
329
  *
330
+ * Before parsing a chain, use $s = $this->count to remember the current
331
  * position into $s. Then if a chain fails, use $this->seek($s) to
332
  * go back where we started.
333
  *
335
  */
336
  protected function parseChunk()
337
  {
338
+ $s = $this->count;
339
 
340
  // the directives
341
  if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
342
+ if ($this->literal('@at-root', 8) &&
343
  ($this->selectors($selector) || true) &&
344
  ($this->map($with) || true) &&
345
+ (($this->matchChar('(')
346
+ && $this->interpolation($with)
347
+ && $this->matchChar(')')) || true) &&
348
+ $this->matchChar('{', false)
349
  ) {
350
  $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
351
  $atRoot->selector = $selector;
352
+ $atRoot->with = $with;
353
 
354
  return true;
355
  }
356
 
357
  $this->seek($s);
358
 
359
+ if ($this->literal('@media', 6) && $this->mediaQueryList($mediaQueryList) && $this->matchChar('{', false)) {
360
  $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
361
  $media->queryList = $mediaQueryList[2];
362
 
365
 
366
  $this->seek($s);
367
 
368
+ if ($this->literal('@mixin', 6) &&
369
  $this->keyword($mixinName) &&
370
  ($this->argumentDef($args) || true) &&
371
+ $this->matchChar('{', false)
372
  ) {
373
  $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
374
  $mixin->name = $mixinName;
379
 
380
  $this->seek($s);
381
 
382
+ if ($this->literal('@include', 8) &&
383
  $this->keyword($mixinName) &&
384
+ ($this->matchChar('(') &&
385
  ($this->argValues($argValues) || true) &&
386
+ $this->matchChar(')') || true) &&
387
  ($this->end() ||
388
+ ($this->literal('using', 5) &&
389
+ $this->argumentDef($argUsing) &&
390
+ ($this->end() || $this->matchChar('{') && $hasBlock = true)) ||
391
+ $this->matchChar('{') && $hasBlock = true)
392
  ) {
393
+ $child = [
394
+ Type::T_INCLUDE,
395
+ $mixinName,
396
+ isset($argValues) ? $argValues : null,
397
+ null,
398
+ isset($argUsing) ? $argUsing : null
399
+ ];
400
 
401
  if (! empty($hasBlock)) {
402
  $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
410
 
411
  $this->seek($s);
412
 
413
+ if ($this->literal('@scssphp-import-once', 20) &&
414
  $this->valueList($importPath) &&
415
  $this->end()
416
  ) {
421
 
422
  $this->seek($s);
423
 
424
+ if ($this->literal('@import', 7) &&
425
  $this->valueList($importPath) &&
426
  $this->end()
427
  ) {
432
 
433
  $this->seek($s);
434
 
435
+ if ($this->literal('@import', 7) &&
436
  $this->url($importPath) &&
437
  $this->end()
438
  ) {
443
 
444
  $this->seek($s);
445
 
446
+ if ($this->literal('@extend', 7) &&
447
  $this->selectors($selectors) &&
448
  $this->end()
449
  ) {
456
 
457
  $this->seek($s);
458
 
459
+ if ($this->literal('@function', 9) &&
460
  $this->keyword($fnName) &&
461
  $this->argumentDef($args) &&
462
+ $this->matchChar('{', false)
463
  ) {
464
  $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
465
  $func->name = $fnName;
470
 
471
  $this->seek($s);
472
 
473
+ if ($this->literal('@break', 6) && $this->end()) {
474
  $this->append([Type::T_BREAK], $s);
475
 
476
  return true;
478
 
479
  $this->seek($s);
480
 
481
+ if ($this->literal('@continue', 9) && $this->end()) {
482
  $this->append([Type::T_CONTINUE], $s);
483
 
484
  return true;
486
 
487
  $this->seek($s);
488
 
489
+ if ($this->literal('@return', 7) && ($this->valueList($retVal) || true) && $this->end()) {
 
490
  $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
491
 
492
  return true;
494
 
495
  $this->seek($s);
496
 
497
+ if ($this->literal('@each', 5) &&
498
  $this->genericList($varNames, 'variable', ',', false) &&
499
+ $this->literal('in', 2) &&
500
  $this->valueList($list) &&
501
+ $this->matchChar('{', false)
502
  ) {
503
  $each = $this->pushSpecialBlock(Type::T_EACH, $s);
504
 
513
 
514
  $this->seek($s);
515
 
516
+ if ($this->literal('@while', 6) &&
517
  $this->expression($cond) &&
518
+ $this->matchChar('{', false)
519
  ) {
520
  $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
521
  $while->cond = $cond;
525
 
526
  $this->seek($s);
527
 
528
+ if ($this->literal('@for', 4) &&
529
  $this->variable($varName) &&
530
+ $this->literal('from', 4) &&
531
  $this->expression($start) &&
532
+ ($this->literal('through', 7) ||
533
+ ($forUntil = true && $this->literal('to', 2))) &&
534
  $this->expression($end) &&
535
+ $this->matchChar('{', false)
536
  ) {
537
  $for = $this->pushSpecialBlock(Type::T_FOR, $s);
538
+ $for->var = $varName[1];
539
  $for->start = $start;
540
+ $for->end = $end;
541
  $for->until = isset($forUntil);
542
 
543
  return true;
545
 
546
  $this->seek($s);
547
 
548
+ if ($this->literal('@if', 3) && $this->valueList($cond) && $this->matchChar('{', false)) {
549
  $if = $this->pushSpecialBlock(Type::T_IF, $s);
550
+ while ($cond[0] === Type::T_LIST
551
+ && !empty($cond['enclosing'])
552
+ && $cond['enclosing'] === 'parent'
553
+ && count($cond[2]) == 1) {
554
+ $cond = reset($cond[2]);
555
+ }
556
+ $if->cond = $cond;
557
  $if->cases = [];
558
 
559
  return true;
561
 
562
  $this->seek($s);
563
 
564
+ if ($this->literal('@debug', 6) &&
565
  $this->valueList($value) &&
566
  $this->end()
567
  ) {
572
 
573
  $this->seek($s);
574
 
575
+ if ($this->literal('@warn', 5) &&
576
  $this->valueList($value) &&
577
  $this->end()
578
  ) {
583
 
584
  $this->seek($s);
585
 
586
+ if ($this->literal('@error', 6) &&
587
  $this->valueList($value) &&
588
  $this->end()
589
  ) {
594
 
595
  $this->seek($s);
596
 
597
+ #if ($this->literal('@content', 8))
598
+
599
+ if ($this->literal('@content', 8) &&
600
+ ($this->end() ||
601
+ $this->matchChar('(') &&
602
+ $this->argValues($argContent) &&
603
+ $this->matchChar(')') &&
604
+ $this->end())) {
605
+ $this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s);
606
 
607
  return true;
608
  }
614
  if (isset($last) && $last[0] === Type::T_IF) {
615
  list(, $if) = $last;
616
 
617
+ if ($this->literal('@else', 5)) {
618
+ if ($this->matchChar('{', false)) {
619
  $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
620
+ } elseif ($this->literal('if', 2) && $this->valueList($cond) && $this->matchChar('{', false)) {
621
  $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
622
  $else->cond = $cond;
623
  }
634
  }
635
 
636
  // only retain the first @charset directive encountered
637
+ if ($this->literal('@charset', 8) &&
638
  $this->valueList($charset) &&
639
  $this->end()
640
  ) {
655
 
656
  $this->seek($s);
657
 
658
+ if ($this->literal('@supports', 9) &&
659
+ ($t1=$this->supportsQuery($supportQuery)) &&
660
+ ($t2=$this->matchChar('{', false))
661
+ ) {
662
+ $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
663
+ $directive->name = 'supports';
664
+ $directive->value = $supportQuery;
665
+
666
+ return true;
667
+ }
668
+
669
+ $this->seek($s);
670
+
671
  // doesn't match built in directive, do generic one
672
+ if ($this->matchChar('@', false) &&
673
  $this->keyword($dirName) &&
674
  ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
675
+ $this->matchChar('{', false)
676
  ) {
677
  if ($dirName === 'media') {
678
  $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
690
 
691
  $this->seek($s);
692
 
693
+ // maybe it's a generic blockless directive
694
+ if ($this->matchChar('@', false) &&
695
+ $this->keyword($dirName) &&
696
+ $this->valueList($dirValue) &&
697
+ $this->end()
698
+ ) {
699
+ $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue]], $s);
700
+
701
+ return true;
702
+ }
703
+
704
+ $this->seek($s);
705
+
706
  return false;
707
  }
708
 
709
  // property shortcut
710
  // captures most properties before having to parse a selector
711
  if ($this->keyword($name, false) &&
712
+ $this->literal(': ', 2) &&
713
  $this->valueList($value) &&
714
  $this->end()
715
  ) {
723
 
724
  // variable assigns
725
  if ($this->variable($name) &&
726
+ $this->matchChar(':') &&
727
  $this->valueList($value) &&
728
  $this->end()
729
  ) {
737
  $this->seek($s);
738
 
739
  // misc
740
+ if ($this->literal('-->', 3)) {
741
  return true;
742
  }
743
 
744
  // opening css block
745
+ if ($this->selectors($selectors) && $this->matchChar('{', false)) {
746
  $this->pushBlock($selectors, $s);
747
 
748
+ if ($this->eatWhiteDefault) {
749
+ $this->whitespace();
750
+ $this->append(null); // collect comments at the beginning if needed
751
+ }
752
+
753
  return true;
754
  }
755
 
756
  $this->seek($s);
757
 
758
  // property assign, or nested assign
759
+ if ($this->propertyName($name) && $this->matchChar(':')) {
760
  $foundSomething = false;
761
 
762
  if ($this->valueList($value)) {
763
+ if (empty($this->env->parent)) {
764
+ $this->throwParseError('expected "{"');
765
+ }
766
+
767
  $this->append([Type::T_ASSIGN, $name, $value], $s);
768
  $foundSomething = true;
769
  }
770
 
771
+ if ($this->matchChar('{', false)) {
772
  $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
773
  $propBlock->prefix = $name;
774
+ $propBlock->hasValue = $foundSomething;
775
+
776
  $foundSomething = true;
777
  } elseif ($foundSomething) {
778
  $foundSomething = $this->end();
786
  $this->seek($s);
787
 
788
  // closing a block
789
+ if ($this->matchChar('}', false)) {
790
  $block = $this->popBlock();
791
 
792
+ if (! isset($block->type) || $block->type !== Type::T_IF) {
793
+ if ($this->env->parent) {
794
+ $this->append(null); // collect comments before next statement if needed
795
+ }
796
+ }
797
+
798
  if (isset($block->type) && $block->type === Type::T_INCLUDE) {
799
  $include = $block->child;
800
  unset($block->child);
805
  $this->append([$type, $block], $s);
806
  }
807
 
808
+ // collect comments just after the block closing if needed
809
+ if ($this->eatWhiteDefault) {
810
+ $this->whitespace();
811
+
812
+ if ($this->env->comments) {
813
+ $this->append(null);
814
+ }
815
+ }
816
+
817
  return true;
818
  }
819
 
820
  // extra stuff
821
+ if ($this->matchChar(';') ||
822
+ $this->literal('<!--', 4)
823
  ) {
824
  return true;
825
  }
833
  * @param array $selectors
834
  * @param integer $pos
835
  *
836
+ * @return \ScssPhp\ScssPhp\Block
837
  */
838
  protected function pushBlock($selectors, $pos = 0)
839
  {
861
 
862
  $this->env = $b;
863
 
864
+ // collect comments at the beginning of a block if needed
865
+ if ($this->eatWhiteDefault) {
866
+ $this->whitespace();
867
+
868
+ if ($this->env->comments) {
869
+ $this->append(null);
870
+ }
871
+ }
872
+
873
  return $b;
874
  }
875
 
879
  * @param string $type
880
  * @param integer $pos
881
  *
882
+ * @return \ScssPhp\ScssPhp\Block
883
  */
884
  protected function pushSpecialBlock($type, $pos)
885
  {
892
  /**
893
  * Pop scope and return last block
894
  *
895
+ * @return \ScssPhp\ScssPhp\Block
896
  *
897
  * @throws \Exception
898
  */
899
  protected function popBlock()
900
  {
901
+
902
+ // collect comments ending just before of a block closing
903
+ if ($this->env->comments) {
904
+ $this->append(null);
905
+ }
906
+
907
+ // pop the block
908
  $block = $this->env;
909
 
910
  if (empty($block->parent)) {
911
  $this->throwParseError('unexpected }');
912
  }
913
 
914
+ if ($block->type == Type::T_AT_ROOT) {
915
+ // keeps the parent in case of self selector &
916
+ $block->selfParent = $block->parent;
917
+ }
918
+
919
  $this->env = $block->parent;
 
920
 
921
+ unset($block->parent);
 
 
 
 
922
 
923
  return $block;
924
  }
948
  * Seek to position in input stream (or return current position in input stream)
949
  *
950
  * @param integer $where
 
 
951
  */
952
+ protected function seek($where)
953
  {
 
 
 
 
954
  $this->count = $where;
 
 
955
  }
956
 
957
  /**
1006
  */
1007
  protected function match($regex, &$out, $eatWhitespace = null)
1008
  {
1009
+ $r = '/' . $regex . '/' . $this->patternModifiers;
1010
+
1011
+ if (! preg_match($r, $this->buffer, $out, null, $this->count)) {
1012
+ return false;
1013
+ }
1014
+
1015
+ $this->count += strlen($out[0]);
1016
+
1017
  if (! isset($eatWhitespace)) {
1018
  $eatWhitespace = $this->eatWhiteDefault;
1019
  }
1020
 
1021
+ if ($eatWhitespace) {
1022
+ $this->whitespace();
1023
+ }
1024
 
1025
+ return true;
1026
+ }
1027
 
1028
+ /**
1029
+ * Match a single string
1030
+ *
1031
+ * @param string $char
1032
+ * @param boolean $eatWhitespace
1033
+ *
1034
+ * @return boolean
1035
+ */
1036
+ protected function matchChar($char, $eatWhitespace = null)
1037
+ {
1038
+ if (! isset($this->buffer[$this->count]) || $this->buffer[$this->count] !== $char) {
1039
+ return false;
1040
+ }
1041
 
1042
+ $this->count++;
1043
+
1044
+ if (! isset($eatWhitespace)) {
1045
+ $eatWhitespace = $this->eatWhiteDefault;
1046
  }
1047
 
1048
+ if ($eatWhitespace) {
1049
+ $this->whitespace();
1050
+ }
1051
+
1052
+ return true;
1053
  }
1054
 
1055
  /**
1056
  * Match literal string
1057
  *
1058
  * @param string $what
1059
+ * @param integer $len
1060
  * @param boolean $eatWhitespace
1061
  *
1062
  * @return boolean
1063
  */
1064
+ protected function literal($what, $len, $eatWhitespace = null)
1065
  {
1066
+ if (strcasecmp(substr($this->buffer, $this->count, $len), $what) !== 0) {
1067
+ return false;
1068
  }
1069
 
1070
+ $this->count += $len;
1071
 
1072
+ if (! isset($eatWhitespace)) {
1073
+ $eatWhitespace = $this->eatWhiteDefault;
1074
+ }
 
 
 
1075
 
1076
+ if ($eatWhitespace) {
1077
+ $this->whitespace();
1078
  }
1079
 
1080
+ return true;
1081
  }
1082
 
1083
  /**
1091
 
1092
  while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) {
1093
  if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
1094
+ // comment that are kept in the output CSS
1095
+ $comment = [];
1096
+ $startCommentCount = $this->count;
1097
+ $endCommentCount = $this->count + strlen($m[1]);
1098
+
1099
+ // find interpolations in comment
1100
+ $p = strpos($this->buffer, '#{', $this->count);
1101
+
1102
+ while ($p !== false && $p < $endCommentCount) {
1103
+ $c = substr($this->buffer, $this->count, $p - $this->count);
1104
+ $comment[] = $c;
1105
+ $this->count = $p;
1106
+ $out = null;
1107
+
1108
+ if ($this->interpolation($out)) {
1109
+ // keep right spaces in the following string part
1110
+ if ($out[3]) {
1111
+ while ($this->buffer[$this->count-1] !== '}') {
1112
+ $this->count--;
1113
+ }
1114
+
1115
+ $out[3] = '';
1116
+ }
1117
+
1118
+ $comment[] = [Type::T_COMMENT, substr($this->buffer, $p, $this->count - $p), $out];
1119
+ } else {
1120
+ $comment[] = substr($this->buffer, $this->count, 2);
1121
+
1122
+ $this->count += 2;
1123
+ }
1124
+
1125
+ $p = strpos($this->buffer, '#{', $this->count);
1126
+ }
1127
 
1128
+ // remaining part
1129
+ $c = substr($this->buffer, $this->count, $endCommentCount - $this->count);
1130
+
1131
+ if (! $comment) {
1132
+ // single part static comment
1133
+ $this->appendComment([Type::T_COMMENT, $c]);
1134
+ } else {
1135
+ $comment[] = $c;
1136
+ $staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount);
1137
+ $this->appendComment([Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]]);
1138
+ }
1139
+
1140
+ $this->commentsSeen[$startCommentCount] = true;
1141
+ $this->count = $endCommentCount;
1142
+ } else {
1143
+ // comment that are ignored and not kept in the output css
1144
+ $this->count += strlen($m[0]);
1145
  }
1146
 
 
1147
  $gotWhite = true;
1148
  }
1149
 
1157
  */
1158
  protected function appendComment($comment)
1159
  {
1160
+ if (! $this->discardComments) {
1161
+ if ($comment[0] === Type::T_COMMENT) {
1162
+ if (is_string($comment[1])) {
1163
+ $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
1164
+ }
1165
+ if (isset($comment[2]) and is_array($comment[2]) and $comment[2][0] === Type::T_STRING) {
1166
+ foreach ($comment[2][2] as $k => $v) {
1167
+ if (is_string($v)) {
1168
+ $p = strpos($v, "\n");
1169
+ if ($p !== false) {
1170
+ $comment[2][2][$k] = substr($v, 0, $p + 1)
1171
+ . preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], substr($v, $p+1));
1172
+ }
1173
+ }
1174
+ }
1175
+ }
1176
+ }
1177
 
1178
+ $this->env->comments[] = $comment;
1179
+ }
1180
  }
1181
 
1182
  /**
1187
  */
1188
  protected function append($statement, $pos = null)
1189
  {
1190
+ if (! is_null($statement)) {
1191
+ if (! is_null($pos)) {
1192
+ list($line, $column) = $this->getSourcePosition($pos);
1193
 
1194
+ $statement[static::SOURCE_LINE] = $line;
1195
+ $statement[static::SOURCE_COLUMN] = $column;
1196
+ $statement[static::SOURCE_INDEX] = $this->sourceIndex;
1197
+ }
1198
 
1199
+ $this->env->children[] = $statement;
1200
+ }
1201
 
1202
  $comments = $this->env->comments;
1203
 
1204
+ if ($comments) {
1205
  $this->env->children = array_merge($this->env->children, $comments);
1206
  $this->env->comments = [];
1207
  }
1245
  $expressions = null;
1246
  $parts = [];
1247
 
1248
+ if (($this->literal('only', 4) && ($only = true) || $this->literal('not', 3) && ($not = true) || true) &&
1249
  $this->mixedKeyword($mediaType)
1250
  ) {
1251
  $prop = [Type::T_MEDIA_TYPE];
1272
  $parts[] = $prop;
1273
  }
1274
 
1275
+ if (empty($parts) || $this->literal('and', 3)) {
1276
  $this->genericList($expressions, 'mediaExpression', 'and', false);
1277
 
1278
  if (is_array($expressions)) {
1285
  return true;
1286
  }
1287
 
1288
+ /**
1289
+ * Parse supports query
1290
+ *
1291
+ * @param array $out
1292
+ *
1293
+ * @return boolean
1294
+ */
1295
+ protected function supportsQuery(&$out)
1296
+ {
1297
+ $expressions = null;
1298
+ $parts = [];
1299
+
1300
+ $s = $this->count;
1301
+
1302
+ $not = false;
1303
+
1304
+ if (($this->literal('not', 3) && ($not = true) || true) &&
1305
+ $this->matchChar('(') &&
1306
+ ($this->expression($property)) &&
1307
+ $this->literal(': ', 2) &&
1308
+ $this->valueList($value) &&
1309
+ $this->matchChar(')')
1310
+ ) {
1311
+ $support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]];
1312
+ $support[2][] = $property;
1313
+ $support[2][] = [Type::T_KEYWORD, ': '];
1314
+ $support[2][] = $value;
1315
+ $support[2][] = [Type::T_KEYWORD, ')'];
1316
+
1317
+ $parts[] = $support;
1318
+ $s = $this->count;
1319
+ } else {
1320
+ $this->seek($s);
1321
+ }
1322
+
1323
+ if ($this->matchChar('(') &&
1324
+ $this->supportsQuery($subQuery) &&
1325
+ $this->matchChar(')')
1326
+ ) {
1327
+ $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, '('], $subQuery, [Type::T_KEYWORD, ')']]];
1328
+ $s = $this->count;
1329
+ } else {
1330
+ $this->seek($s);
1331
+ }
1332
+
1333
+ if ($this->literal('not', 3) &&
1334
+ $this->supportsQuery($subQuery)
1335
+ ) {
1336
+ $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]];
1337
+ $s = $this->count;
1338
+ } else {
1339
+ $this->seek($s);
1340
+ }
1341
+
1342
+ if ($this->literal('selector(', 9) &&
1343
+ $this->selector($selector) &&
1344
+ $this->matchChar(')')
1345
+ ) {
1346
+ $support = [Type::T_STRING, '', [[Type::T_KEYWORD, 'selector(']]];
1347
+
1348
+ $selectorList = [Type::T_LIST, '', []];
1349
+
1350
+ foreach ($selector as $sc) {
1351
+ $compound = [Type::T_STRING, '', []];
1352
+
1353
+ foreach ($sc as $scp) {
1354
+ if (is_array($scp)) {
1355
+ $compound[2][] = $scp;
1356
+ } else {
1357
+ $compound[2][] = [Type::T_KEYWORD, $scp];
1358
+ }
1359
+ }
1360
+
1361
+ $selectorList[2][] = $compound;
1362
+ }
1363
+ $support[2][] = $selectorList;
1364
+ $support[2][] = [Type::T_KEYWORD, ')'];
1365
+ $parts[] = $support;
1366
+ $s = $this->count;
1367
+ } else {
1368
+ $this->seek($s);
1369
+ }
1370
+
1371
+ if ($this->variable($var) or $this->interpolation($var)) {
1372
+ $parts[] = $var;
1373
+ $s = $this->count;
1374
+ } else {
1375
+ $this->seek($s);
1376
+ }
1377
+
1378
+ if ($this->literal('and', 3) &&
1379
+ $this->genericList($expressions, 'supportsQuery', ' and', false)) {
1380
+ array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
1381
+
1382
+ $parts = [$expressions];
1383
+ $s = $this->count;
1384
+ } else {
1385
+ $this->seek($s);
1386
+ }
1387
+
1388
+ if ($this->literal('or', 2) &&
1389
+ $this->genericList($expressions, 'supportsQuery', ' or', false)) {
1390
+ array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
1391
+
1392
+ $parts = [$expressions];
1393
+ $s = $this->count;
1394
+ } else {
1395
+ $this->seek($s);
1396
+ }
1397
+
1398
+ if (count($parts)) {
1399
+ if ($this->eatWhiteDefault) {
1400
+ $this->whitespace();
1401
+ }
1402
+
1403
+ $out = [Type::T_STRING, '', $parts];
1404
+
1405
+ return true;
1406
+ }
1407
+
1408
+ return false;
1409
+ }
1410
+
1411
+
1412
  /**
1413
  * Parse media expression
1414
  *
1418
  */
1419
  protected function mediaExpression(&$out)
1420
  {
1421
+ $s = $this->count;
1422
  $value = null;
1423
 
1424
+ if ($this->matchChar('(') &&
1425
  $this->expression($feature) &&
1426
+ ($this->matchChar(':') && $this->expression($value) || true) &&
1427
+ $this->matchChar(')')
1428
  ) {
1429
  $out = [Type::T_MEDIA_EXPRESSION, $feature];
1430
 
1467
  */
1468
  protected function argValue(&$out)
1469
  {
1470
+ $s = $this->count;
1471
 
1472
  $keyword = null;
1473
 
1474
+ if (! $this->variable($keyword) || ! $this->matchChar(':')) {
1475
  $this->seek($s);
1476
+
1477
  $keyword = null;
1478
  }
1479
 
1480
  if ($this->genericList($value, 'expression')) {
1481
  $out = [$keyword, $value, false];
1482
+ $s = $this->count;
1483
 
1484
+ if ($this->literal('...', 3)) {
1485
  $out[2] = true;
1486
  } else {
1487
  $this->seek($s);
1496
  /**
1497
  * Parse comma separated value list
1498
  *
1499
+ * @param array $out
1500
  *
1501
  * @return boolean
1502
  */
1503
  protected function valueList(&$out)
1504
  {
1505
+ $discardComments = $this->discardComments;
1506
+ $this->discardComments = true;
1507
+ $res = $this->genericList($out, 'spaceList', ',');
1508
+ $this->discardComments = $discardComments;
1509
+
1510
+ return $res;
1511
  }
1512
 
1513
  /**
1534
  */
1535
  protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
1536
  {
1537
+ $s = $this->count;
1538
  $items = [];
1539
+ $value = null;
1540
 
1541
  while ($this->$parseItem($value)) {
1542
+ $trailing_delim = false;
1543
  $items[] = $value;
1544
 
1545
  if ($delim) {
1546
+ if (! $this->literal($delim, strlen($delim))) {
1547
  break;
1548
  }
1549
+ $trailing_delim = true;
1550
  }
1551
  }
1552
 
1553
+ if (! $items) {
1554
  $this->seek($s);
1555
 
1556
  return false;
1557
  }
1558
 
1559
+ if ($trailing_delim) {
1560
+ $items[] = [Type::T_NULL];
1561
+ }
1562
  if ($flatten && count($items) === 1) {
1563
  $out = $items[0];
1564
  } else {
1572
  * Parse expression
1573
  *
1574
  * @param array $out
1575
+ * @param bool $listOnly
1576
+ * @param bool $lookForExp
1577
  *
1578
  * @return boolean
1579
  */
1580
+ protected function expression(&$out, $listOnly = false, $lookForExp = true)
1581
  {
1582
+ $s = $this->count;
1583
+ $discard = $this->discardComments;
1584
+ $this->discardComments = true;
1585
+ $allowedTypes = ($listOnly ? [Type::T_LIST] : [Type::T_LIST, Type::T_MAP]);
1586
+
1587
+ if ($this->matchChar('(')) {
1588
+ if ($this->enclosedExpression($lhs, $s, ")", $allowedTypes)) {
1589
+ if ($lookForExp) {
1590
+ $out = $this->expHelper($lhs, 0);
1591
+ } else {
1592
+ $out = $lhs;
1593
+ }
1594
 
1595
+ $this->discardComments = $discard;
 
 
1596
 
1597
  return true;
1598
  }
1599
 
1600
+ $this->seek($s);
1601
+ }
1602
+
1603
+ if (in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) {
1604
+ if ($this->enclosedExpression($lhs, $s, "]", [Type::T_LIST])) {
1605
+ if ($lookForExp) {
1606
+ $out = $this->expHelper($lhs, 0);
1607
+ } else {
1608
+ $out = $lhs;
1609
+ }
1610
+ $this->discardComments = $discard;
1611
+
1612
  return true;
1613
  }
1614
 
1615
  $this->seek($s);
1616
+ }
1617
 
1618
+ if (!$listOnly && $this->value($lhs)) {
1619
+ if ($lookForExp) {
1620
+ $out = $this->expHelper($lhs, 0);
1621
+ } else {
1622
+ $out = $lhs;
1623
  }
1624
 
1625
+ $this->discardComments = $discard;
1626
+
1627
+ return true;
1628
  }
1629
 
1630
+ $this->discardComments = $discard;
1631
+ return false;
1632
+ }
1633
 
1634
+ /**
1635
+ * Parse expression specifically checking for lists in parenthesis or brackets
1636
+ *
1637
+ * @param array $out
1638
+ * @param integer $s
1639
+ * @param string $closingParen
1640
+ * @param array $allowedTypes
1641
+ *
1642
+ * @return boolean
1643
+ */
1644
+ protected function enclosedExpression(&$out, $s, $closingParen = ")", $allowedTypes = [Type::T_LIST, Type::T_MAP])
1645
+ {
1646
+ if ($this->matchChar($closingParen) && in_array(Type::T_LIST, $allowedTypes)) {
1647
+ $out = [Type::T_LIST, '', []];
1648
+ switch ($closingParen) {
1649
+ case ")":
1650
+ $out['enclosing'] = 'parent'; // parenthesis list
1651
+ break;
1652
+ case "]":
1653
+ $out['enclosing'] = 'bracket'; // bracketed list
1654
+ break;
1655
+ }
1656
+ return true;
1657
+ }
1658
+
1659
+ if ($this->valueList($out) && $this->matchChar($closingParen)
1660
+ && in_array($out[0], [Type::T_LIST, Type::T_KEYWORD])
1661
+ && in_array(Type::T_LIST, $allowedTypes)) {
1662
+ if ($out[0] !== Type::T_LIST || ! empty($out['enclosing'])) {
1663
+ $out = [Type::T_LIST, '', [$out]];
1664
+ }
1665
+ switch ($closingParen) {
1666
+ case ")":
1667
+ $out['enclosing'] = 'parent'; // parenthesis list
1668
+ break;
1669
+ case "]":
1670
+ $out['enclosing'] = 'bracket'; // bracketed list
1671
+ break;
1672
+ }
1673
+ return true;
1674
+ }
1675
+
1676
+ $this->seek($s);
1677
+
1678
+ if (in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) {
1679
  return true;
1680
  }
1681
 
1694
  {
1695
  $operators = static::$operatorPattern;
1696
 
1697
+ $ss = $this->count;
1698
  $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1699
  ctype_space($this->buffer[$this->count - 1]);
1700
 
1713
  break;
1714
  }
1715
 
1716
+ if (! $this->value($rhs) && ! $this->expression($rhs, true, false)) {
1717
  break;
1718
  }
1719
 
1723
  }
1724
 
1725
  $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
1726
+ $ss = $this->count;
1727
  $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1728
  ctype_space($this->buffer[$this->count - 1]);
1729
  }
1742
  */
1743
  protected function value(&$out)
1744
  {
1745
+ if (! isset($this->buffer[$this->count])) {
1746
+ return false;
1747
+ }
1748
 
1749
+ $s = $this->count;
1750
+ $char = $this->buffer[$this->count];
1751
+
1752
+ if ($this->literal('url(', 4) && $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)) {
1753
+ $len = strspn(
1754
+ $this->buffer,
1755
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=',
1756
+ $this->count
1757
+ );
1758
 
1759
  $this->count += $len;
1760
 
1761
+ if ($this->matchChar(')')) {
1762
  $content = substr($this->buffer, $s, $this->count - $s);
1763
  $out = [Type::T_KEYWORD, $content];
1764
 
1768
 
1769
  $this->seek($s);
1770
 
1771
+ if ($this->literal('url(', 4, false) && $this->match('\s*(\/\/[^\s\)]+)\s*', $m)) {
1772
+ $content = 'url(' . $m[1];
1773
 
1774
+ if ($this->matchChar(')')) {
1775
+ $content .= ')';
1776
+ $out = [Type::T_KEYWORD, $content];
1777
+
1778
+ return true;
1779
+ }
1780
  }
1781
 
1782
  $this->seek($s);
1783
 
1784
+ // not
1785
+ if ($char === 'n' && $this->literal('not', 3, false)) {
1786
+ if ($this->whitespace() && $this->value($inner)) {
1787
+ $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1788
+
1789
+ return true;
1790
+ }
1791
+
1792
+ $this->seek($s);
1793
 
1794
+ if ($this->parenValue($inner)) {
1795
+ $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1796
+
1797
+ return true;
1798
+ }
1799
+
1800
+ $this->seek($s);
1801
+ }
1802
+
1803
+ // addition
1804
+ if ($char === '+') {
1805
+ $this->count++;
1806
+
1807
+ if ($this->value($inner)) {
1808
+ $out = [Type::T_UNARY, '+', $inner, $this->inParens];
1809
+
1810
+ return true;
1811
+ }
1812
+
1813
+ $this->count--;
1814
+
1815
+ return false;
1816
+ }
1817
+
1818
+ // negation
1819
+ if ($char === '-') {
1820
+ $this->count++;
1821
+
1822
+ if ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner)) {
1823
+ $out = [Type::T_UNARY, '-', $inner, $this->inParens];
1824
+
1825
+ return true;
1826
+ }
1827
+
1828
+ $this->count--;
1829
+ }
1830
+
1831
+ // paren
1832
+ if ($char === '(' && $this->parenValue($out)) {
1833
  return true;
1834
  }
1835
 
1836
+ if ($char === '#') {
1837
+ if ($this->interpolation($out) || $this->color($out)) {
1838
+ return true;
1839
+ }
1840
+ }
1841
 
1842
+ if ($this->matchChar('&', true)) {
1843
+ $out = [Type::T_SELF];
1844
 
1845
  return true;
1846
  }
1847
 
1848
+ if ($char === '$' && $this->variable($out)) {
1849
+ return true;
1850
+ }
1851
 
1852
+ if ($char === 'p' && $this->progid($out)) {
1853
+ return true;
1854
+ }
 
 
 
 
1855
 
1856
+ if (($char === '"' || $char === "'") && $this->string($out)) {
1857
  return true;
1858
  }
1859
 
1860
+ if ($this->unit($out)) {
1861
+ return true;
1862
+ }
1863
+
1864
+ // unicode range with wildcards
1865
+ if ($this->literal('U+', 2) && $this->match('([0-9A-F]+\?*)(-([0-9A-F]+))?', $m, false)) {
1866
+ $out = [Type::T_KEYWORD, 'U+' . $m[0]];
1867
 
 
 
 
 
 
 
 
 
 
1868
  return true;
1869
  }
1870
 
1871
+ if ($this->keyword($keyword, false)) {
1872
+ if ($this->func($keyword, $out)) {
1873
+ return true;
1874
+ }
1875
+
1876
+ $this->whitespace();
1877
+
1878
  if ($keyword === 'null') {
1879
  $out = [Type::T_NULL];
1880
  } else {
1896
  */
1897
  protected function parenValue(&$out)
1898
  {
1899
+ $s = $this->count;
1900
 
1901
  $inParens = $this->inParens;
1902
 
1903
+ if ($this->matchChar('(')) {
1904
+ if ($this->matchChar(')')) {
1905
  $out = [Type::T_LIST, '', []];
1906
 
1907
  return true;
1909
 
1910
  $this->inParens = true;
1911
 
1912
+ if ($this->expression($exp) && $this->matchChar(')')) {
1913
  $out = $exp;
1914
  $this->inParens = $inParens;
1915
 
1932
  */
1933
  protected function progid(&$out)
1934
  {
1935
+ $s = $this->count;
1936
 
1937
+ if ($this->literal('progid:', 7, false) &&
1938
  $this->openString('(', $fn) &&
1939
+ $this->matchChar('(')
1940
  ) {
1941
  $this->openString(')', $args, '(');
1942
 
1943
+ if ($this->matchChar(')')) {
1944
  $out = [Type::T_STRING, '', [
1945
  'progid:', $fn, '(', $args, ')'
1946
  ]];
1957
  /**
1958
  * Parse function call
1959
  *
1960
+ * @param string $name
1961
+ * @param array $func
1962
  *
1963
  * @return boolean
1964
  */
1965
+ protected function func($name, &$func)
1966
  {
1967
+ $s = $this->count;
1968
 
1969
+ if ($this->matchChar('(')) {
 
 
1970
  if ($name === 'alpha' && $this->argumentList($args)) {
1971
  $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]];
1972
 
1974
  }
1975
 
1976
  if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
1977
+ $ss = $this->count;
1978
 
1979
+ if ($this->argValues($args) && $this->matchChar(')')) {
1980
  $func = [Type::T_FUNCTION_CALL, $name, $args];
1981
 
1982
  return true;
1986
  }
1987
 
1988
  if (($this->openString(')', $str, '(') || true) &&
1989
+ $this->matchChar(')')
1990
  ) {
1991
  $args = [];
1992
 
2014
  */
2015
  protected function argumentList(&$out)
2016
  {
2017
+ $s = $this->count;
2018
+ $this->matchChar('(');
2019
 
2020
  $args = [];
2021
 
2022
  while ($this->keyword($var)) {
2023
+ if ($this->matchChar('=') && $this->expression($exp)) {
2024
  $args[] = [Type::T_STRING, '', [$var . '=']];
2025
  $arg = $exp;
2026
  } else {
2029
 
2030
  $args[] = $arg;
2031
 
2032
+ if (! $this->matchChar(',')) {
2033
  break;
2034
  }
2035
 
2036
  $args[] = [Type::T_STRING, '', [', ']];
2037
  }
2038
 
2039
+ if (! $this->matchChar(')') || ! $args) {
2040
  $this->seek($s);
2041
 
2042
  return false;
2056
  */
2057
  protected function argumentDef(&$out)
2058
  {
2059
+ $s = $this->count;
2060
+ $this->matchChar('(');
2061
 
2062
  $args = [];
2063
 
2064
  while ($this->variable($var)) {
2065
  $arg = [$var[1], null, false];
2066
 
2067
+ $ss = $this->count;
2068
 
2069
+ if ($this->matchChar(':') && $this->genericList($defaultVal, 'expression')) {
2070
  $arg[1] = $defaultVal;
2071
  } else {
2072
  $this->seek($ss);
2073
  }
2074
 
2075
+ $ss = $this->count;
2076
 
2077
+ if ($this->literal('...', 3)) {
2078
+ $sss = $this->count;
2079
 
2080
+ if (! $this->matchChar(')')) {
2081
  $this->throwParseError('... has to be after the final argument');
2082
  }
2083
 
2089
 
2090
  $args[] = $arg;
2091
 
2092
+ if (! $this->matchChar(',')) {
2093
  break;
2094
  }
2095
  }
2096
 
2097
+ if (! $this->matchChar(')')) {
2098
  $this->seek($s);
2099
 
2100
  return false;
2114
  */
2115
  protected function map(&$out)
2116
  {
2117
+ $s = $this->count;
2118
 
2119
+ if (! $this->matchChar('(')) {
2120
  return false;
2121
  }
2122
 
2123
  $keys = [];
2124
  $values = [];
2125
 
2126
+ while ($this->genericList($key, 'expression') && $this->matchChar(':') &&
2127
  $this->genericList($value, 'expression')
2128
  ) {
2129
  $keys[] = $key;
2130
  $values[] = $value;
2131
 
2132
+ if (! $this->matchChar(',')) {
2133
  break;
2134
  }
2135
  }
2136
 
2137
+ if (! $keys || ! $this->matchChar(')')) {
2138
  $this->seek($s);
2139
 
2140
  return false;
2154
  */
2155
  protected function color(&$out)
2156
  {
2157
+ $s = $this->count;
2158
 
2159
+ if ($this->match('(#([0-9a-f]+))', $m)) {
2160
+ if (in_array(strlen($m[2]), [3,4,6,8])) {
2161
+ $out = [Type::T_KEYWORD, $m[0]];
2162
+ return true;
 
 
 
 
 
 
 
 
 
 
 
 
2163
  }
2164
 
2165
+ $this->seek($s);
2166
+ return false;
 
2167
  }
2168
 
2169
  return false;
2172
  /**
2173
  * Parse number with unit
2174
  *
2175
+ * @param array $unit
2176
  *
2177
  * @return boolean
2178
  */
2179
  protected function unit(&$unit)
2180
  {
2181
+ $s = $this->count;
 
2182
 
2183
+ if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m, false)) {
2184
+ if (strlen($this->buffer) === $this->count || ! ctype_digit($this->buffer[$this->count])) {
2185
+ $this->whitespace();
2186
+
2187
+ $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
2188
+
2189
+ return true;
2190
+ }
2191
+
2192
+ $this->seek($s);
2193
  }
2194
 
2195
  return false;
2204
  */
2205
  protected function string(&$out)
2206
  {
2207
+ $s = $this->count;
2208
 
2209
+ if ($this->matchChar('"', false)) {
2210
  $delim = '"';
2211
+ } elseif ($this->matchChar("'", false)) {
2212
  $delim = "'";
2213
  } else {
2214
  return false;
2235
  $content[] = '#{'; // ignore it
2236
  }
2237
  } elseif ($m[2] === '\\') {
2238
+ if ($this->matchChar('"', false)) {
2239
  $content[] = $m[2] . '"';
2240
+ } elseif ($this->matchChar("'", false)) {
2241
  $content[] = $m[2] . "'";
2242
+ } elseif ($this->literal("\\", 1, false)) {
2243
+ $content[] = $m[2] . "\\";
2244
+ } elseif ($this->literal("\r\n", 2, false) ||
2245
+ $this->matchChar("\r", false) ||
2246
+ $this->matchChar("\n", false) ||
2247
+ $this->matchChar("\f", false)
2248
+ ) {
2249
+ // this is a continuation escaping, to be ignored
2250
  } else {
2251
  $content[] = $m[2];
2252
  }
2258
 
2259
  $this->eatWhiteDefault = $oldWhite;
2260
 
2261
+ if ($this->literal($delim, strlen($delim))) {
2262
  if ($hasInterpolation) {
2263
  $delim = '"';
2264
 
2265
  foreach ($content as &$string) {
2266
+ if ($string === "\\\\") {
2267
+ $string = "\\";
2268
+ } elseif ($string === "\\'") {
2269
  $string = "'";
2270
  } elseif ($string === '\\"') {
2271
  $string = '"';
2286
  /**
2287
  * Parse keyword or interpolation
2288
  *
2289
+ * @param array $out
2290
+ * @param boolean $restricted
2291
  *
2292
  * @return boolean
2293
  */
2294
+ protected function mixedKeyword(&$out, $restricted = false)
2295
  {
2296
  $parts = [];
2297
 
2299
  $this->eatWhiteDefault = false;
2300
 
2301
  for (;;) {
2302
+ if ($restricted ? $this->restrictedKeyword($key) : $this->keyword($key)) {
2303
  $parts[] = $key;
2304
  continue;
2305
  }
2314
 
2315
  $this->eatWhiteDefault = $oldWhite;
2316
 
2317
+ if (! $parts) {
2318
  return false;
2319
  }
2320
 
2380
 
2381
  $this->eatWhiteDefault = $oldWhite;
2382
 
2383
+ if (! $content) {
2384
  return false;
2385
  }
2386
 
2397
  /**
2398
  * Parser interpolation
2399
  *
2400
+ * @param string|array $out
2401
+ * @param boolean $lookWhite save information about whitespace before and after
2402
  *
2403
  * @return boolean
2404
  */
2407
  $oldWhite = $this->eatWhiteDefault;
2408
  $this->eatWhiteDefault = true;
2409
 
2410
+ $s = $this->count;
2411
 
2412
+ if ($this->literal('#{', 2) && $this->valueList($value) && $this->matchChar('}', false)) {
2413
+ if ($value === [Type::T_SELF]) {
2414
+ $out = $value;
 
2415
  } else {
2416
+ if ($lookWhite) {
2417
+ $left = ($s > 0 && preg_match('/\s/', $this->buffer[$s - 1])) ? ' ' : '';
2418
+ $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
2419
+ } else {
2420
+ $left = $right = false;
2421
+ }
2422
+
2423
+ $out = [Type::T_INTERPOLATE, $value, $left, $right];
2424
  }
2425
 
 
2426
  $this->eatWhiteDefault = $oldWhite;
2427
 
2428
  if ($this->eatWhiteDefault) {
2433
  }
2434
 
2435
  $this->seek($s);
2436
+
2437
  $this->eatWhiteDefault = $oldWhite;
2438
 
2439
  return false;
2464
  continue;
2465
  }
2466
 
2467
+ if (! $parts && $this->match('[:.#]', $m, false)) {
2468
  // css hacks
2469
  $parts[] = $m[0];
2470
  continue;
2475
 
2476
  $this->eatWhiteDefault = $oldWhite;
2477
 
2478
+ if (! $parts) {
2479
  return false;
2480
  }
2481
 
2503
  /**
2504
  * Parse comma separated selector list
2505
  *
2506
+ * @param array $out
2507
+ * @param boolean $subSelector
2508
  *
2509
  * @return boolean
2510
  */
2511
+ protected function selectors(&$out, $subSelector = false)
2512
  {
2513
+ $s = $this->count;
2514
  $selectors = [];
2515
 
2516
+ while ($this->selector($sel, $subSelector)) {
2517
  $selectors[] = $sel;
2518
 
2519
+ if (! $this->matchChar(',', true)) {
2520
  break;
2521
  }
2522
 
2523
+ while ($this->matchChar(',', true)) {
2524
  ; // ignore extra
2525
  }
2526
  }
2527
 
2528
+ if (! $selectors) {
2529
  $this->seek($s);
2530
 
2531
  return false;
2539
  /**
2540
  * Parse whitespace separated selector list
2541
  *
2542
+ * @param array $out
2543
+ * @param boolean $subSelector
2544
  *
2545
  * @return boolean
2546
  */
2547
+ protected function selector(&$out, $subSelector = false)
2548
  {
2549
  $selector = [];
2550
 
2551
  for (;;) {
2552
+ $s = $this->count;
2553
+
2554
+ if ($this->match('[>+~]+', $m, true)) {
2555
+ if ($subSelector && is_string($subSelector) && strpos($subSelector, 'nth-') === 0 &&
2556
+ $m[0] === '+' && $this->match("(\d+|n\b)", $counter)
2557
+ ) {
2558
+ $this->seek($s);
2559
+ } else {
2560
+ $selector[] = [$m[0]];
2561
+ continue;
2562
+ }
2563
  }
2564
 
2565
+ if ($this->selectorSingle($part, $subSelector)) {
2566
  $selector[] = $part;
2567
  $this->match('\s+', $m);
2568
  continue;
2569
  }
2570
 
2571
+ if ($this->match('\/[^\/]+\/', $m, true)) {
2572
  $selector[] = [$m[0]];
2573
  continue;
2574
  }
2576
  break;
2577
  }
2578
 
2579
+ if (! $selector) {
2580
  return false;
2581
  }
2582
 
2583
  $out = $selector;
2584
+
2585
  return true;
2586
  }
2587
 
2592
  * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
2593
  * }}
2594
  *
2595
+ * @param array $out
2596
+ * @param boolean $subSelector
2597
  *
2598
  * @return boolean
2599
  */
2600
+ protected function selectorSingle(&$out, $subSelector = false)
2601
  {
2602
  $oldWhite = $this->eatWhiteDefault;
2603
  $this->eatWhiteDefault = false;
2604
 
2605
  $parts = [];
2606
 
2607
+ if ($this->matchChar('*', false)) {
2608
  $parts[] = '*';
2609
  }
2610
 
2611
  for (;;) {
2612
+ if (! isset($this->buffer[$this->count])) {
 
 
2613
  break;
2614
  }
2615
 
2616
+ $s = $this->count;
2617
+ $char = $this->buffer[$this->count];
2618
 
2619
+ // see if we can stop early
2620
+ if ($char === '{' || $char === ',' || $char === ';' || $char === '}' || $char === '@') {
2621
+ break;
 
2622
  }
2623
 
2624
+ // parsing a sub selector in () stop with the closing )
2625
+ if ($subSelector && $char === ')') {
2626
+ break;
2627
  }
2628
 
2629
+ //self
2630
+ switch ($char) {
2631
+ case '&':
2632
+ $parts[] = Compiler::$selfSelector;
2633
+ $this->count++;
2634
+ continue 2;
2635
+
2636
+ case '.':
2637
+ $parts[] = '.';
2638
+ $this->count++;
2639
+ continue 2;
2640
+
2641
+ case '|':
2642
+ $parts[] = '|';
2643
+ $this->count++;
2644
+ continue 2;
2645
  }
2646
 
2647
+ if ($char === '\\' && $this->match('\\\\\S', $m)) {
2648
  $parts[] = $m[0];
2649
  continue;
2650
  }
2651
 
2652
+ if ($char === '%') {
2653
+ $this->count++;
 
 
 
2654
 
2655
+ if ($this->placeholder($placeholder)) {
2656
+ $parts[] = '%';
2657
+ $parts[] = $placeholder;
2658
+ continue;
2659
+ }
2660
 
2661
+ break;
 
 
2662
  }
2663
 
2664
+ if ($char === '#') {
2665
+ if ($this->interpolation($inter)) {
2666
+ $parts[] = $inter;
2667
+ continue;
2668
+ }
2669
 
 
2670
  $parts[] = '#';
2671
+ $this->count++;
2672
  continue;
2673
  }
2674
 
2675
  // a pseudo selector
2676
+ if ($char === ':') {
2677
+ if ($this->buffer[$this->count + 1] === ':') {
2678
+ $this->count += 2;
2679
+ $part = '::';
2680
+ } else {
2681
+ $this->count++;
2682
+ $part = ':';
2683
  }
2684
 
2685
+ if ($this->mixedKeyword($nameParts, true)) {
2686
+ $parts[] = $part;
2687
 
2688
+ foreach ($nameParts as $sub) {
2689
+ $parts[] = $sub;
2690
+ }
 
 
2691
 
2692
+ $ss = $this->count;
2693
+
2694
+ if ($nameParts === ['not'] || $nameParts === ['is'] ||
2695
+ $nameParts === ['has'] || $nameParts === ['where'] ||
2696
+ $nameParts === ['slotted'] ||
2697
+ $nameParts === ['nth-child'] || $nameParts == ['nth-last-child'] ||
2698
+ $nameParts === ['nth-of-type'] || $nameParts == ['nth-last-of-type']
2699
+ ) {
2700
+ if ($this->matchChar('(', true) &&
2701
+ ($this->selectors($subs, reset($nameParts)) || true) &&
2702
+ $this->matchChar(')')
2703
+ ) {
2704
+ $parts[] = '(';
2705
+
2706
+ while ($sub = array_shift($subs)) {
2707
+ while ($ps = array_shift($sub)) {
2708
+ foreach ($ps as &$p) {
2709
+ $parts[] = $p;
2710
+ }
2711
+
2712
+ if (count($sub) && reset($sub)) {
2713
+ $parts[] = ' ';
2714
+ }
2715
+ }
2716
+
2717
+ if (count($subs) && reset($subs)) {
2718
+ $parts[] = ', ';
2719
+ }
2720
+ }
2721
+
2722
+ $parts[] = ')';
2723
+ } else {
2724
+ $this->seek($ss);
2725
+ }
2726
+ } else {
2727
+ if ($this->matchChar('(') &&
2728
+ ($this->openString(')', $str, '(') || true) &&
2729
+ $this->matchChar(')')
2730
+ ) {
2731
+ $parts[] = '(';
2732
+
2733
+ if (! empty($str)) {
2734
+ $parts[] = $str;
2735
+ }
2736
+
2737
+ $parts[] = ')';
2738
+ } else {
2739
+ $this->seek($ss);
2740
+ }
2741
  }
2742
 
2743
+ continue;
 
 
2744
  }
2745
+ }
2746
 
2747
+ $this->seek($s);
2748
+
2749
+ // 2n+1
2750
+ if ($subSelector && is_string($subSelector) && strpos($subSelector, 'nth-') === 0) {
2751
+ if ($this->match("(\s*(\+\s*|\-\s*)?(\d+|n|\d+n))+", $counter)) {
2752
+ $parts[] = $counter[0];
2753
+ //$parts[] = str_replace(' ', '', $counter[0]);
2754
+ continue;
2755
+ }
2756
  }
2757
 
2758
  $this->seek($s);
2759
 
2760
  // attribute selector
2761
+ if ($char === '[' &&
2762
+ $this->matchChar('[') &&
2763
+ ($this->openString(']', $str, '[') || true) &&
2764
+ $this->matchChar(']')
2765
  ) {
2766
  $parts[] = '[';
2767
 
2770
  }
2771
 
2772
  $parts[] = ']';
 
2773
  continue;
2774
  }
2775
 
2776
  $this->seek($s);
2777
 
2778
+ // for keyframes
2779
+ if ($this->unit($unit)) {
2780
+ $parts[] = $unit;
2781
+ continue;
2782
+ }
2783
+
2784
+ if ($this->restrictedKeyword($name)) {
2785
+ $parts[] = $name;
2786
+ continue;
2787
+ }
2788
+
2789
  break;
2790
  }
2791
 
2792
  $this->eatWhiteDefault = $oldWhite;
2793
 
2794
+ if (! $parts) {
2795
  return false;
2796
  }
2797
 
2809
  */
2810
  protected function variable(&$out)
2811
  {
2812
+ $s = $this->count;
2813
 
2814
+ if ($this->matchChar('$', false) && $this->keyword($name)) {
2815
  $out = [Type::T_VARIABLE, $name];
2816
 
2817
  return true;
2834
  {
2835
  if ($this->match(
2836
  $this->utf8
2837
+ ? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|[\\\\].)*)'
2838
  : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
2839
  $m,
2840
  $eatWhitespace
2847
  return false;
2848
  }
2849
 
2850
+ /**
2851
+ * Parse a keyword that should not start with a number
2852
+ *
2853
+ * @param string $word
2854
+ * @param boolean $eatWhitespace
2855
+ *
2856
+ * @return boolean
2857
+ */
2858
+ protected function restrictedKeyword(&$word, $eatWhitespace = null)
2859
+ {
2860
+ $s = $this->count;
2861
+
2862
+ if ($this->keyword($word, $eatWhitespace) && (ord($word[0]) > 57 || ord($word[0]) < 48)) {
2863
+ return true;
2864
+ }
2865
+
2866
+ $this->seek($s);
2867
+
2868
+ return false;
2869
+ }
2870
+
2871
  /**
2872
  * Parse a placeholder
2873
  *
2874
+ * @param string|array $placeholder
2875
  *
2876
  * @return boolean
2877
  */
2879
  {
2880
  if ($this->match(
2881
  $this->utf8
2882
+ ? '([\pL\w\-_]+)'
2883
+ : '([\w\-_]+)',
2884
  $m
2885
  )) {
2886
  $placeholder = $m[1];
2888
  return true;
2889
  }
2890
 
2891
+ if ($this->interpolation($placeholder)) {
2892
+ return true;
2893
+ }
2894
+
2895
  return false;
2896
  }
2897
 
2920
  */
2921
  protected function end()
2922
  {
2923
+ if ($this->matchChar(';')) {
2924
  return true;
2925
  }
2926
 
2949
  while ($lastNode[0] === Type::T_KEYWORD && in_array($lastNode[1], ['!default', '!global'])) {
2950
  array_pop($token[2]);
2951
 
2952
+ $node = end($token[2]);
2953
+ $token = $this->flattenList($token);
2954
+ $flags[] = $lastNode[1];
 
 
 
2955
  $lastNode = $node;
2956
  }
2957
  }
2969
  protected function stripOptionalFlag(&$selectors)
2970
  {
2971
  $optional = false;
 
2972
  $selector = end($selectors);
2973
+ $part = end($selector);
2974
 
2975
  if ($part === ['!optional']) {
2976
  array_pop($selectors[count($selectors) - 1]);
3014
  $validChars = $allowNewline ? '.' : "[^\n]";
3015
  }
3016
 
3017
+ $m = null;
3018
+
3019
  if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
3020
  return false;
3021
  }
3080
  *
3081
  * @param integer $pos
3082
  *
3083
+ * @return array
3084
  */
3085
  private function getSourcePosition($pos)
3086
  {
3115
  return;
3116
  }
3117
 
3118
+ // deprecated in PHP 7.2
3119
+ $iniDirective = 'mbstring.func_overload';
3120
 
3121
+ if (extension_loaded('mbstring') && ini_get($iniDirective) & 2) {
3122
  $this->encoding = mb_internal_encoding();
3123
 
3124
  mb_internal_encoding('iso-8859-1');
3130
  */
3131
  private function restoreEncoding()
3132
  {
3133
+ if (extension_loaded('mbstring') && $this->encoding) {
3134
  mb_internal_encoding($this->encoding);
3135
  }
3136
  }
assets/libraries/scssphp/src/SourceMap/Base64.php ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
+ *
7
+ * @license http://opensource.org/licenses/MIT MIT
8
+ *
9
+ * @link http://scssphp.github.io/scssphp
10
+ */
11
+
12
+ namespace ScssPhp\ScssPhp\SourceMap;
13
+
14
+ /**
15
+ * Base 64 Encode/Decode
16
+ *
17
+ * @author Anthon Pang <anthon.pang@gmail.com>
18
+ */
19
+ class Base64
20
+ {
21
+ /**
22
+ * @var array
23
+ */
24
+ private static $encodingMap = [
25
+ 0 => 'A',
26
+ 1 => 'B',
27
+ 2 => 'C',
28
+ 3 => 'D',
29
+ 4 => 'E',
30
+ 5 => 'F',
31
+ 6 => 'G',
32
+ 7 => 'H',
33
+ 8 => 'I',
34
+ 9 => 'J',
35
+ 10 => 'K',
36
+ 11 => 'L',
37
+ 12 => 'M',
38
+ 13 => 'N',
39
+ 14 => 'O',
40
+ 15 => 'P',
41
+ 16 => 'Q',
42
+ 17 => 'R',
43
+ 18 => 'S',
44
+ 19 => 'T',
45
+ 20 => 'U',
46
+ 21 => 'V',
47
+ 22 => 'W',
48
+ 23 => 'X',
49
+ 24 => 'Y',
50
+ 25 => 'Z',
51
+ 26 => 'a',
52
+ 27 => 'b',
53
+ 28 => 'c',
54
+ 29 => 'd',
55
+ 30 => 'e',
56
+ 31 => 'f',
57
+ 32 => 'g',
58
+ 33 => 'h',
59
+ 34 => 'i',
60
+ 35 => 'j',
61
+ 36 => 'k',
62
+ 37 => 'l',
63
+ 38 => 'm',
64
+ 39 => 'n',
65
+ 40 => 'o',
66
+ 41 => 'p',
67
+ 42 => 'q',
68
+ 43 => 'r',
69
+ 44 => 's',
70
+ 45 => 't',
71
+ 46 => 'u',
72
+ 47 => 'v',
73
+ 48 => 'w',
74
+ 49 => 'x',
75
+ 50 => 'y',
76
+ 51 => 'z',
77
+ 52 => '0',
78
+ 53 => '1',
79
+ 54 => '2',
80
+ 55 => '3',
81
+ 56 => '4',
82
+ 57 => '5',
83
+ 58 => '6',
84
+ 59 => '7',
85
+ 60 => '8',
86
+ 61 => '9',
87
+ 62 => '+',
88
+ 63 => '/',
89
+ ];
90
+
91
+ /**
92
+ * @var array
93
+ */
94
+ private static $decodingMap = [
95
+ 'A' => 0,
96
+ 'B' => 1,
97
+ 'C' => 2,
98
+ 'D' => 3,
99
+ 'E' => 4,
100
+ 'F' => 5,
101
+ 'G' => 6,
102
+ 'H' => 7,
103
+ 'I' => 8,
104
+ 'J' => 9,
105
+ 'K' => 10,
106
+ 'L' => 11,
107
+ 'M' => 12,
108
+ 'N' => 13,
109
+ 'O' => 14,
110
+ 'P' => 15,
111
+ 'Q' => 16,
112
+ 'R' => 17,
113
+ 'S' => 18,
114
+ 'T' => 19,
115
+ 'U' => 20,
116
+ 'V' => 21,
117
+ 'W' => 22,
118
+ 'X' => 23,
119
+ 'Y' => 24,
120
+ 'Z' => 25,
121
+ 'a' => 26,
122
+ 'b' => 27,
123
+ 'c' => 28,
124
+ 'd' => 29,
125
+ 'e' => 30,
126
+ 'f' => 31,
127
+ 'g' => 32,
128
+ 'h' => 33,
129
+ 'i' => 34,
130
+ 'j' => 35,
131
+ 'k' => 36,
132
+ 'l' => 37,
133
+ 'm' => 38,
134
+ 'n' => 39,
135
+ 'o' => 40,
136
+ 'p' => 41,
137
+ 'q' => 42,
138
+ 'r' => 43,
139
+ 's' => 44,
140
+ 't' => 45,
141
+ 'u' => 46,
142
+ 'v' => 47,
143
+ 'w' => 48,
144
+ 'x' => 49,
145
+ 'y' => 50,
146
+ 'z' => 51,
147
+ 0 => 52,
148
+ 1 => 53,
149
+ 2 => 54,
150
+ 3 => 55,
151
+ 4 => 56,
152
+ 5 => 57,
153
+ 6 => 58,
154
+ 7 => 59,
155
+ 8 => 60,
156
+ 9 => 61,
157
+ '+' => 62,
158
+ '/' => 63,
159
+ ];
160
+
161
+ /**
162
+ * Convert to base64
163
+ *
164
+ * @param integer $value
165
+ *
166
+ * @return string
167
+ */
168
+ public static function encode($value)
169
+ {
170
+ return self::$encodingMap[$value];
171
+ }
172
+
173
+ /**
174
+ * Convert from base64
175
+ *
176
+ * @param string $value
177
+ *
178
+ * @return integer
179
+ */
180
+ public static function decode($value)
181
+ {
182
+ return self::$decodingMap[$value];
183
+ }
184
+ }
assets/libraries/scssphp/src/SourceMap/Base64VLQ.php ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * SCSSPHP
4
+ *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
+ *
7
+ * @license http://opensource.org/licenses/MIT MIT
8
+ *
9
+ * @link http://scssphp.github.io/scssphp
10
+ */
11
+
12
+ namespace ScssPhp\ScssPhp\SourceMap;
13
+
14
+ use ScssPhp\ScssPhp\SourceMap\Base64;
15
+
16
+ /**
17
+ * Base 64 VLQ
18
+ *
19
+ * Based on the Base 64 VLQ implementation in Closure Compiler:
20
+ * https://github.com/google/closure-compiler/blob/master/src/com/google/debugging/sourcemap/Base64VLQ.java
21
+ *
22
+ * Copyright 2011 The Closure Compiler Authors.
23
+ *
24
+ * Licensed under the Apache License, Version 2.0 (the "License");
25
+ * you may not use this file except in compliance with the License.
26
+ * You may obtain a copy of the License at
27
+ *
28
+ * http://www.apache.org/licenses/LICENSE-2.0
29
+ *
30
+ * Unless required by applicable law or agreed to in writing, software
31
+ * distributed under the License is distributed on an "AS IS" BASIS,
32
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
33
+ * See the License for the specific language governing permissions and
34
+ * limitations under the License.
35
+ *
36
+ * @author John Lenz <johnlenz@google.com>
37
+ * @author Anthon Pang <anthon.pang@gmail.com>
38
+ */
39
+ class Base64VLQ
40
+ {
41
+ // A Base64 VLQ digit can represent 5 bits, so it is base-32.
42
+ const VLQ_BASE_SHIFT = 5;
43
+
44
+ // A mask of bits for a VLQ digit (11111), 31 decimal.
45
+ const VLQ_BASE_MASK = 31;
46
+
47
+ // The continuation bit is the 6th bit.
48
+ const VLQ_CONTINUATION_BIT = 32;
49
+
50
+ /**
51
+ * Returns the VLQ encoded value.
52
+ *
53
+ * @param integer $value
54
+ *
55
+ * @return string
56
+ */
57
+ public static function encode($value)
58
+ {
59
+ $encoded = '';
60
+ $vlq = self::toVLQSigned($value);
61
+
62
+ do {
63
+ $digit = $vlq & self::VLQ_BASE_MASK;
64
+ $vlq >>= self::VLQ_BASE_SHIFT;
65
+
66
+ if ($vlq > 0) {
67
+ $digit |= self::VLQ_CONTINUATION_BIT;
68
+ }
69
+
70
+ $encoded .= Base64::encode($digit);
71
+ } while ($vlq > 0);
72
+
73
+ return $encoded;
74
+ }
75
+
76
+ /**
77
+ * Decodes VLQValue.
78
+ *
79
+ * @param string $str
80
+ * @param integer $index
81
+ *
82
+ * @return integer
83
+ */
84
+ public static function decode($str, &$index)
85
+ {
86
+ $result = 0;
87
+ $shift = 0;
88
+
89
+ do {
90
+ $c = $str[$index++];
91
+ $digit = Base64::decode($c);
92
+ $continuation = ($digit & self::VLQ_CONTINUATION_BIT) != 0;
93
+ $digit &= self::VLQ_BASE_MASK;
94
+ $result = $result + ($digit << $shift);
95
+ $shift = $shift + self::VLQ_BASE_SHIFT;
96
+ } while ($continuation);
97
+
98
+ return self::fromVLQSigned($result);
99
+ }
100
+
101
+ /**
102
+ * Converts from a two-complement value to a value where the sign bit is
103
+ * is placed in the least significant bit. For example, as decimals:
104
+ * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
105
+ * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
106
+ *
107
+ * @param integer $value
108
+ *
109
+ * @return integer
110
+ */
111
+ private static function toVLQSigned($value)
112
+ {
113
+ if ($value < 0) {
114
+ return ((-$value) << 1) + 1;
115
+ }
116
+
117
+ return ($value << 1) + 0;
118
+ }
119
+
120
+ /**
121
+ * Converts to a two-complement value from a value where the sign bit is
122
+ * is placed in the least significant bit. For example, as decimals:
123
+ * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1
124
+ * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2
125
+ *
126
+ * @param integer $value
127
+ *
128
+ * @return integer
129
+ */
130
+ private static function fromVLQSigned($value)
131
+ {
132
+ $negate = ($value & 1) === 1;
133
+ $value = ($value >> 1) & ~(1<<(8 * PHP_INT_SIZE - 1)); // unsigned right shift
134
+
135
+ if (! $negate) {
136
+ return $value;
137
+ }
138
+
139
+ // We need to OR 0x80000000 here to ensure the 32nd bit (the sign bit) is
140
+ // always set for negative numbers. If `value` were 1, (meaning `negate` is
141
+ // true and all other bits were zeros), `value` would now be 0. -0 is just
142
+ // 0, and doesn't flip the 32nd bit as intended. All positive numbers will
143
+ // successfully flip the 32nd bit without issue, so it's a noop for them.
144
+ return -$value | 0x80000000;
145
+ }
146
+ }
assets/libraries/scssphp/src/SourceMap/Base64VLQEncoder.php DELETED
@@ -1,218 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2018 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp\SourceMap;
13
-
14
- /**
15
- * Base64 VLQ Encoder
16
- *
17
- * {@internal Derivative of oyejorge/less.php's lib/SourceMap/Base64VLQ.php, relicensed with permission. }}
18
- *
19
- * @author Josh Schmidt <oyejorge@gmail.com>
20
- * @author Nicolas FRANÇOIS <nicolas.francois@frog-labs.com>
21
- */
22
- class Base64VLQEncoder
23
- {
24
- /**
25
- * Shift
26
- *
27
- * @var integer
28
- */
29
- private $shift = 5;
30
-
31
- /**
32
- * Mask
33
- *
34
- * @var integer
35
- */
36
- private $mask = 0x1F; // == (1 << shift) == 0b00011111
37
-
38
- /**
39
- * Continuation bit
40
- *
41
- * @var integer
42
- */
43
- private $continuationBit = 0x20; // == (mask - 1 ) == 0b00100000
44
-
45
- /**
46
- * Char to integer map
47
- *
48
- * @var array
49
- */
50
- private $charToIntMap = array(
51
- 'A' => 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6, 'H' => 7,
52
- 'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13, 'O' => 14, 'P' => 15,
53
- 'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20, 'V' => 21, 'W' => 22, 'X' => 23,
54
- 'Y' => 24, 'Z' => 25, 'a' => 26, 'b' => 27, 'c' => 28, 'd' => 29, 'e' => 30, 'f' => 31,
55
- 'g' => 32, 'h' => 33, 'i' => 34, 'j' => 35, 'k' => 36, 'l' => 37, 'm' => 38, 'n' => 39,
56
- 'o' => 40, 'p' => 41, 'q' => 42, 'r' => 43, 's' => 44, 't' => 45, 'u' => 46, 'v' => 47,
57
- 'w' => 48, 'x' => 49, 'y' => 50, 'z' => 51, 0 => 52, 1 => 53, 2 => 54, 3 => 55,
58
- 4 => 56, 5 => 57, 6 => 58, 7 => 59, 8 => 60, 9 => 61, '+' => 62, '/' => 63,
59
- );
60
-
61
- /**
62
- * Integer to char map
63
- *
64
- * @var array
65
- */
66
- private $intToCharMap = array(
67
- 0 => 'A', 1 => 'B', 2 => 'C', 3 => 'D', 4 => 'E', 5 => 'F', 6 => 'G', 7 => 'H',
68
- 8 => 'I', 9 => 'J', 10 => 'K', 11 => 'L', 12 => 'M', 13 => 'N', 14 => 'O', 15 => 'P',
69
- 16 => 'Q', 17 => 'R', 18 => 'S', 19 => 'T', 20 => 'U', 21 => 'V', 22 => 'W', 23 => 'X',
70
- 24 => 'Y', 25 => 'Z', 26 => 'a', 27 => 'b', 28 => 'c', 29 => 'd', 30 => 'e', 31 => 'f',
71
- 32 => 'g', 33 => 'h', 34 => 'i', 35 => 'j', 36 => 'k', 37 => 'l', 38 => 'm', 39 => 'n',
72
- 40 => 'o', 41 => 'p', 42 => 'q', 43 => 'r', 44 => 's', 45 => 't', 46 => 'u', 47 => 'v',
73
- 48 => 'w', 49 => 'x', 50 => 'y', 51 => 'z', 52 => '0', 53 => '1', 54 => '2', 55 => '3',
74
- 56 => '4', 57 => '5', 58 => '6', 59 => '7', 60 => '8', 61 => '9', 62 => '+', 63 => '/',
75
- );
76
-
77
- /**
78
- * Constructor
79
- */
80
- public function __construct()
81
- {
82
- // I leave it here for future reference
83
- // foreach (str_split('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/') as $i => $char)
84
- // {
85
- // $this->charToIntMap[$char] = $i;
86
- // $this->intToCharMap[$i] = $char;
87
- // }
88
- }
89
-
90
- /**
91
- * Convert from a two-complement value to a value where the sign bit is
92
- * is placed in the least significant bit. For example, as decimals:
93
- * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
94
- * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
95
- * We generate the value for 32 bit machines, hence -2147483648 becomes 1, not 4294967297,
96
- * even on a 64 bit machine.
97
- *
98
- * @param string $aValue
99
- */
100
- public function toVLQSigned($aValue)
101
- {
102
- return 0xffffffff & ($aValue < 0 ? ((-$aValue) << 1) + 1 : ($aValue << 1) + 0);
103
- }
104
-
105
- /**
106
- * Convert to a two-complement value from a value where the sign bit is
107
- * is placed in the least significant bit. For example, as decimals:
108
- * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1
109
- * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2
110
- * We assume that the value was generated with a 32 bit machine in mind.
111
- * Hence
112
- * 1 becomes -2147483648
113
- * even on a 64 bit machine.
114
- *
115
- * @param integer $aValue
116
- */
117
- public function fromVLQSigned($aValue)
118
- {
119
- return $aValue & 1 ? $this->zeroFill(~$aValue + 2, 1) | (-1 - 0x7fffffff) : $this->zeroFill($aValue, 1);
120
- }
121
-
122
- /**
123
- * Return the base 64 VLQ encoded value.
124
- *
125
- * @param string $aValue The value to encode
126
- *
127
- * @return string The encoded value
128
- */
129
- public function encode($aValue)
130
- {
131
- $encoded = '';
132
- $vlq = $this->toVLQSigned($aValue);
133
-
134
- do {
135
- $digit = $vlq & $this->mask;
136
- $vlq = $this->zeroFill($vlq, $this->shift);
137
-
138
- if ($vlq > 0) {
139
- $digit |= $this->continuationBit;
140
- }
141
-
142
- $encoded .= $this->base64Encode($digit);
143
- } while ($vlq > 0);
144
-
145
- return $encoded;
146
- }
147
-
148
- /**
149
- * Return the value decoded from base 64 VLQ.
150
- *
151
- * @param string $encoded The encoded value to decode
152
- *
153
- * @return integer The decoded value
154
- */
155
- public function decode($encoded)
156
- {
157
- $vlq = 0;
158
- $i = 0;
159
-
160
- do {
161
- $digit = $this->base64Decode($encoded[$i]);
162
- $vlq |= ($digit & $this->mask) << ($i * $this->shift);
163
- $i++;
164
- } while ($digit & $this->continuationBit);
165
-
166
- return $this->fromVLQSigned($vlq);
167
- }
168
-
169
- /**
170
- * Right shift with zero fill.
171
- *
172
- * @param integer $a number to shift
173
- * @param integer $b number of bits to shift
174
- *
175
- * @return integer
176
- */
177
- public function zeroFill($a, $b)
178
- {
179
- $bmin = $b - 1;
180
- return ($a >= 0) ? ($a >> $b) : ($a >> $b) & (PHP_INT_MAX >> ($bmin));
181
- }
182
-
183
- /**
184
- * Encode single 6-bit digit as base64.
185
- *
186
- * @param integer $number
187
- *
188
- * @return string
189
- *
190
- * @throws \Exception If the number is invalid
191
- */
192
- public function base64Encode($number)
193
- {
194
- if ($number < 0 || $number > 63) {
195
- throw new \Exception(sprintf('Invalid number "%s" given. Must be between 0 and 63.', $number));
196
- }
197
-
198
- return $this->intToCharMap[$number];
199
- }
200
-
201
- /**
202
- * Decode single 6-bit digit from base64
203
- *
204
- * @param string $char
205
- *
206
- * @return integer
207
- *
208
- * @throws \Exception If the number is invalid
209
- */
210
- public function base64Decode($char)
211
- {
212
- if (! array_key_exists($char, $this->charToIntMap)) {
213
- throw new \Exception(sprintf('Invalid base 64 digit "%s" given.', $char));
214
- }
215
-
216
- return $this->charToIntMap[$char];
217
- }
218
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp/src/SourceMap/SourceMapGenerator.php CHANGED
@@ -2,16 +2,16 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp\SourceMap;
13
 
14
- use Leafo\ScssPhp\Exception\CompilerException;
15
 
16
  /**
17
  * Source Map Generator
@@ -33,7 +33,7 @@ class SourceMapGenerator
33
  *
34
  * @var array
35
  */
36
- protected $defaultOptions = array(
37
  // an optional source root, useful for relocating source files
38
  // on a server or removing repeated values in the 'sources' entry.
39
  // This value is prepended to the individual entries in the 'source' field.
@@ -56,12 +56,12 @@ class SourceMapGenerator
56
 
57
  // base path for filename normalization
58
  'sourceMapBasepath' => ''
59
- );
60
 
61
  /**
62
  * The base64 VLQ encoder
63
  *
64
- * @var \Leafo\ScssPhp\SourceMap\Base64VLQEncoder
65
  */
66
  protected $encoder;
67
 
@@ -70,22 +70,22 @@ class SourceMapGenerator
70
  *
71
  * @var array
72
  */
73
- protected $mappings = array();
74
 
75
  /**
76
  * Array of contents map
77
  *
78
  * @var array
79
  */
80
- protected $contentsMap = array();
81
 
82
  /**
83
  * File to content map
84
  *
85
  * @var array
86
  */
87
- protected $sources = array();
88
- protected $source_keys = array();
89
 
90
  /**
91
  * @var array
@@ -95,7 +95,7 @@ class SourceMapGenerator
95
  public function __construct(array $options = [])
96
  {
97
  $this->options = array_merge($this->defaultOptions, $options);
98
- $this->encoder = new Base64VLQEncoder();
99
  }
100
 
101
  /**
@@ -109,13 +109,13 @@ class SourceMapGenerator
109
  */
110
  public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $sourceFile)
111
  {
112
- $this->mappings[] = array(
113
- 'generated_line' => $generatedLine,
114
  'generated_column' => $generatedColumn,
115
- 'original_line' => $originalLine,
116
- 'original_column' => $originalColumn,
117
- 'source_file' => $sourceFile
118
- );
119
 
120
  $this->sources[$sourceFile] = $sourceFile;
121
  }
@@ -123,10 +123,11 @@ class SourceMapGenerator
123
  /**
124
  * Saves the source map to a file
125
  *
126
- * @param string $file The absolute path to a file
127
  * @param string $content The content to write
128
  *
129
- * @throws \Leafo\ScssPhp\Exception\CompilerException If the file could not be saved
 
 
130
  */
131
  public function saveMap($content)
132
  {
@@ -136,7 +137,9 @@ class SourceMapGenerator
136
  // directory does not exist
137
  if (! is_dir($dir)) {
138
  // FIXME: create the dir automatically?
139
- throw new CompilerException(sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir));
 
 
140
  }
141
 
142
  // FIXME: proper saving, with dir write check!
@@ -156,7 +159,7 @@ class SourceMapGenerator
156
  */
157
  public function generateJson()
158
  {
159
- $sourceMap = array();
160
  $mappings = $this->generateMappings();
161
 
162
  // File version (always the first entry in the object) and must be a positive integer.
@@ -178,14 +181,14 @@ class SourceMapGenerator
178
  }
179
 
180
  // A list of original sources used by the 'mappings' entry.
181
- $sourceMap['sources'] = array();
182
 
183
- foreach ($this->sources as $source_uri => $source_filename) {
184
- $sourceMap['sources'][] = $this->normalizeFilename($source_filename);
185
  }
186
 
187
  // A list of symbol names used by the 'mappings' entry.
188
- $sourceMap['names'] = array();
189
 
190
  // A string with the encoded mapping data.
191
  $sourceMap['mappings'] = $mappings;
@@ -202,7 +205,7 @@ class SourceMapGenerator
202
  unset($sourceMap['sourceRoot']);
203
  }
204
 
205
- return json_encode($sourceMap);
206
  }
207
 
208
  /**
@@ -216,7 +219,7 @@ class SourceMapGenerator
216
  return null;
217
  }
218
 
219
- $content = array();
220
 
221
  foreach ($this->sources as $sourceFile) {
222
  $content[] = file_get_contents($sourceFile);
@@ -236,10 +239,10 @@ class SourceMapGenerator
236
  return '';
237
  }
238
 
239
- $this->source_keys = array_flip(array_keys($this->sources));
240
 
241
  // group mappings by generated line number.
242
- $groupedMap = $groupedMapEncoded = array();
243
 
244
  foreach ($this->mappings as $m) {
245
  $groupedMap[$m['generated_line']][] = $m;
@@ -248,15 +251,15 @@ class SourceMapGenerator
248
  ksort($groupedMap);
249
  $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
250
 
251
- foreach ($groupedMap as $lineNumber => $line_map) {
252
  while (++$lastGeneratedLine < $lineNumber) {
253
  $groupedMapEncoded[] = ';';
254
  }
255
 
256
- $lineMapEncoded = array();
257
  $lastGeneratedColumn = 0;
258
 
259
- foreach ($line_map as $m) {
260
  $mapEncoded = $this->encoder->encode($m['generated_column'] - $lastGeneratedColumn);
261
  $lastGeneratedColumn = $m['generated_column'];
262
 
@@ -293,9 +296,16 @@ class SourceMapGenerator
293
  */
294
  protected function findFileIndex($filename)
295
  {
296
- return $this->source_keys[$filename];
297
  }
298
 
 
 
 
 
 
 
 
299
  protected function normalizeFilename($filename)
300
  {
301
  $filename = $this->fixWindowsPath($filename);
@@ -303,7 +313,7 @@ class SourceMapGenerator
303
  $basePath = $this->options['sourceMapBasepath'];
304
 
305
  // "Trim" the 'sourceMapBasepath' from the output filename.
306
- if (strpos($filename, $basePath) === 0) {
307
  $filename = substr($filename, strlen($basePath));
308
  }
309
 
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp\SourceMap;
13
 
14
+ use ScssPhp\ScssPhp\Exception\CompilerException;
15
 
16
  /**
17
  * Source Map Generator
33
  *
34
  * @var array
35
  */
36
+ protected $defaultOptions = [
37
  // an optional source root, useful for relocating source files
38
  // on a server or removing repeated values in the 'sources' entry.
39
  // This value is prepended to the individual entries in the 'source' field.
56
 
57
  // base path for filename normalization
58
  'sourceMapBasepath' => ''
59
+ ];
60
 
61
  /**
62
  * The base64 VLQ encoder
63
  *
64
+ * @var \ScssPhp\ScssPhp\SourceMap\Base64VLQ
65
  */
66
  protected $encoder;
67
 
70
  *
71
  * @var array
72
  */
73
+ protected $mappings = [];
74
 
75
  /**
76
  * Array of contents map
77
  *
78
  * @var array
79
  */
80
+ protected $contentsMap = [];
81
 
82
  /**
83
  * File to content map
84
  *
85
  * @var array
86
  */
87
+ protected $sources = [];
88
+ protected $sourceKeys = [];
89
 
90
  /**
91
  * @var array
95
  public function __construct(array $options = [])
96
  {
97
  $this->options = array_merge($this->defaultOptions, $options);
98
+ $this->encoder = new Base64VLQ();
99
  }
100
 
101
  /**
109
  */
110
  public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $sourceFile)
111
  {
112
+ $this->mappings[] = [
113
+ 'generated_line' => $generatedLine,
114
  'generated_column' => $generatedColumn,
115
+ 'original_line' => $originalLine,
116
+ 'original_column' => $originalColumn,
117
+ 'source_file' => $sourceFile
118
+ ];
119
 
120
  $this->sources[$sourceFile] = $sourceFile;
121
  }
123
  /**
124
  * Saves the source map to a file
125
  *
 
126
  * @param string $content The content to write
127
  *
128
+ * @return string
129
+ *
130
+ * @throws \ScssPhp\ScssPhp\Exception\CompilerException If the file could not be saved
131
  */
132
  public function saveMap($content)
133
  {
137
  // directory does not exist
138
  if (! is_dir($dir)) {
139
  // FIXME: create the dir automatically?
140
+ throw new CompilerException(
141
+ sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir)
142
+ );
143
  }
144
 
145
  // FIXME: proper saving, with dir write check!
159
  */
160
  public function generateJson()
161
  {
162
+ $sourceMap = [];
163
  $mappings = $this->generateMappings();
164
 
165
  // File version (always the first entry in the object) and must be a positive integer.
181
  }
182
 
183
  // A list of original sources used by the 'mappings' entry.
184
+ $sourceMap['sources'] = [];
185
 
186
+ foreach ($this->sources as $sourceUri => $sourceFilename) {
187
+ $sourceMap['sources'][] = $this->normalizeFilename($sourceFilename);
188
  }
189
 
190
  // A list of symbol names used by the 'mappings' entry.
191
+ $sourceMap['names'] = [];
192
 
193
  // A string with the encoded mapping data.
194
  $sourceMap['mappings'] = $mappings;
205
  unset($sourceMap['sourceRoot']);
206
  }
207
 
208
+ return json_encode($sourceMap, JSON_UNESCAPED_SLASHES);
209
  }
210
 
211
  /**
219
  return null;
220
  }
221
 
222
+ $content = [];
223
 
224
  foreach ($this->sources as $sourceFile) {
225
  $content[] = file_get_contents($sourceFile);
239
  return '';
240
  }
241
 
242
+ $this->sourceKeys = array_flip(array_keys($this->sources));
243
 
244
  // group mappings by generated line number.
245
+ $groupedMap = $groupedMapEncoded = [];
246
 
247
  foreach ($this->mappings as $m) {
248
  $groupedMap[$m['generated_line']][] = $m;
251
  ksort($groupedMap);
252
  $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
253
 
254
+ foreach ($groupedMap as $lineNumber => $lineMap) {
255
  while (++$lastGeneratedLine < $lineNumber) {
256
  $groupedMapEncoded[] = ';';
257
  }
258
 
259
+ $lineMapEncoded = [];
260
  $lastGeneratedColumn = 0;
261
 
262
+ foreach ($lineMap as $m) {
263
  $mapEncoded = $this->encoder->encode($m['generated_column'] - $lastGeneratedColumn);
264
  $lastGeneratedColumn = $m['generated_column'];
265
 
296
  */
297
  protected function findFileIndex($filename)
298
  {
299
+ return $this->sourceKeys[$filename];
300
  }
301
 
302
+ /**
303
+ * Normalize filename
304
+ *
305
+ * @param string $filename
306
+ *
307
+ * @return string
308
+ */
309
  protected function normalizeFilename($filename)
310
  {
311
  $filename = $this->fixWindowsPath($filename);
313
  $basePath = $this->options['sourceMapBasepath'];
314
 
315
  // "Trim" the 'sourceMapBasepath' from the output filename.
316
+ if (strlen($basePath) && strpos($filename, $basePath) === 0) {
317
  $filename = substr($filename, strlen($basePath));
318
  }
319
 
assets/libraries/scssphp/src/Type.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
 
14
  /**
15
  * Block/node types
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
 
14
  /**
15
  * Block/node types
assets/libraries/scssphp/src/Util.php CHANGED
@@ -2,17 +2,17 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
 
14
- use Leafo\ScssPhp\Base\Range;
15
- use Leafo\ScssPhp\Exception\RangeException;
16
 
17
  /**
18
  * Utilty functions
@@ -26,13 +26,13 @@ class Util
26
  * room for slight floating-point errors.
27
  *
28
  * @param string $name The name of the value. Used in the error message.
29
- * @param \Leafo\ScssPhp\Base\Range $range Range of values.
30
  * @param array $value The value to check.
31
  * @param string $unit The unit of the value. Used in error reporting.
32
  *
33
  * @return mixed `value` adjusted to fall within range, if it was outside by a floating-point margin.
34
  *
35
- * @throws \Leafo\ScssPhp\Exception\RangeException
36
  */
37
  public static function checkRange($name, Range $range, $value, $unit = '')
38
  {
@@ -63,7 +63,7 @@ class Util
63
  */
64
  public static function encodeURIComponent($string)
65
  {
66
- $revert = array('%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')');
67
 
68
  return strtr(rawurlencode($string), $revert);
69
  }
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
 
14
+ use ScssPhp\ScssPhp\Base\Range;
15
+ use ScssPhp\ScssPhp\Exception\RangeException;
16
 
17
  /**
18
  * Utilty functions
26
  * room for slight floating-point errors.
27
  *
28
  * @param string $name The name of the value. Used in the error message.
29
+ * @param \ScssPhp\ScssPhp\Base\Range $range Range of values.
30
  * @param array $value The value to check.
31
  * @param string $unit The unit of the value. Used in error reporting.
32
  *
33
  * @return mixed `value` adjusted to fall within range, if it was outside by a floating-point margin.
34
  *
35
+ * @throws \ScssPhp\ScssPhp\Exception\RangeException
36
  */
37
  public static function checkRange($name, Range $range, $value, $unit = '')
38
  {
63
  */
64
  public static function encodeURIComponent($string)
65
  {
66
+ $revert = ['%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')'];
67
 
68
  return strtr(rawurlencode($string), $revert);
69
  }
assets/libraries/scssphp/src/Version.php CHANGED
@@ -2,14 +2,14 @@
2
  /**
3
  * SCSSPHP
4
  *
5
- * @copyright 2012-2018 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
- * @link http://leafo.github.io/scssphp
10
  */
11
 
12
- namespace Leafo\ScssPhp;
13
 
14
  /**
15
  * SCSSPHP version
@@ -18,5 +18,5 @@ namespace Leafo\ScssPhp;
18
  */
19
  class Version
20
  {
21
- const VERSION = 'v0.7.6';
22
  }
2
  /**
3
  * SCSSPHP
4
  *
5
+ * @copyright 2012-2020 Leaf Corcoran
6
  *
7
  * @license http://opensource.org/licenses/MIT MIT
8
  *
9
+ * @link http://scssphp.github.io/scssphp
10
  */
11
 
12
+ namespace ScssPhp\ScssPhp;
13
 
14
  /**
15
  * SCSSPHP version
18
  */
19
  class Version
20
  {
21
+ const VERSION = 'v1.0.7';
22
  }
assets/libraries/scssphp_legacy/scss.inc.php DELETED
@@ -1,29 +0,0 @@
1
- <?php
2
-
3
- if (version_compare(PHP_VERSION, '5.3') < 0) {
4
- die('Requires PHP 5.3 or above');
5
- }
6
-
7
- if (! class_exists('scssc', false)) {
8
- include_once __DIR__ . '/src/Base/Range.php';
9
- include_once __DIR__ . '/src/Block.php';
10
- include_once __DIR__ . '/src/Colors.php';
11
- include_once __DIR__ . '/src/Compiler.php';
12
- include_once __DIR__ . '/src/Compiler/Environment.php';
13
- include_once __DIR__ . '/src/Formatter.php';
14
- include_once __DIR__ . '/src/Formatter/Compact.php';
15
- include_once __DIR__ . '/src/Formatter/Compressed.php';
16
- include_once __DIR__ . '/src/Formatter/Crunched.php';
17
- include_once __DIR__ . '/src/Formatter/Debug.php';
18
- include_once __DIR__ . '/src/Formatter/Expanded.php';
19
- include_once __DIR__ . '/src/Formatter/Nested.php';
20
- include_once __DIR__ . '/src/Formatter/OutputBlock.php';
21
- include_once __DIR__ . '/src/Node.php';
22
- include_once __DIR__ . '/src/Node/Number.php';
23
- include_once __DIR__ . '/src/Parser.php';
24
- include_once __DIR__ . '/src/Type.php';
25
- include_once __DIR__ . '/src/Util.php';
26
- include_once __DIR__ . '/src/Version.php';
27
- include_once __DIR__ . '/src/Server.php';
28
- // include_once __DIR__ . '/classmap.php';
29
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Base/Range.php DELETED
@@ -1,47 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp\Base;
13
-
14
- /**
15
- * Range class
16
- *
17
- * @author Anthon Pang <anthon.pang@gmail.com>
18
- */
19
- class Range
20
- {
21
- public $first;
22
- public $last;
23
-
24
- /**
25
- * Initialize range
26
- *
27
- * @param integer|float $first
28
- * @param integer|float $last
29
- */
30
- public function __construct($first, $last)
31
- {
32
- $this->first = $first;
33
- $this->last = $last;
34
- }
35
-
36
- /**
37
- * Test for inclusion in range
38
- *
39
- * @param integer|float $value
40
- *
41
- * @return boolean
42
- */
43
- public function includes($value)
44
- {
45
- return $value >= $this->first && $value <= $this->last;
46
- }
47
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Block.php DELETED
@@ -1,55 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp;
13
-
14
- /**
15
- * SCSS block
16
- *
17
- * @author Anthon Pang <anthon.pang@gmail.com>
18
- */
19
- class Block
20
- {
21
- /**
22
- * @var string
23
- */
24
- public $type;
25
-
26
- /**
27
- * @var \Leafo\ScssPhp\Block
28
- */
29
- public $parent;
30
-
31
- /**
32
- * @var integer
33
- */
34
- public $sourcePosition;
35
-
36
- /**
37
- * @var integer
38
- */
39
- public $sourceIndex;
40
-
41
- /**
42
- * @var array
43
- */
44
- public $selectors;
45
-
46
- /**
47
- * @var array
48
- */
49
- public $comments;
50
-
51
- /**
52
- * @var array
53
- */
54
- public $children;
55
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Colors.php DELETED
@@ -1,178 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp;
13
-
14
- /**
15
- * CSS Colors
16
- *
17
- * @author Leaf Corcoran <leafot@gmail.com>
18
- */
19
- class Colors
20
- {
21
- /**
22
- * CSS Colors
23
- *
24
- * @see http://www.w3.org/TR/css3-color
25
- *
26
- * @var array
27
- */
28
- public static $cssColors = array(
29
- 'aliceblue' => '240,248,255',
30
- 'antiquewhite' => '250,235,215',
31
- 'aqua' => '0,255,255',
32
- 'aquamarine' => '127,255,212',
33
- 'azure' => '240,255,255',
34
- 'beige' => '245,245,220',
35
- 'bisque' => '255,228,196',
36
- 'black' => '0,0,0',
37
- 'blanchedalmond' => '255,235,205',
38
- 'blue' => '0,0,255',
39
- 'blueviolet' => '138,43,226',
40
- 'brown' => '165,42,42',
41
- 'burlywood' => '222,184,135',
42
- 'cadetblue' => '95,158,160',
43
- 'chartreuse' => '127,255,0',
44
- 'chocolate' => '210,105,30',
45
- 'coral' => '255,127,80',
46
- 'cornflowerblue' => '100,149,237',
47
- 'cornsilk' => '255,248,220',
48
- 'crimson' => '220,20,60',
49
- 'cyan' => '0,255,255',
50
- 'darkblue' => '0,0,139',
51
- 'darkcyan' => '0,139,139',
52
- 'darkgoldenrod' => '184,134,11',
53
- 'darkgray' => '169,169,169',
54
- 'darkgreen' => '0,100,0',
55
- 'darkgrey' => '169,169,169',
56
- 'darkkhaki' => '189,183,107',
57
- 'darkmagenta' => '139,0,139',
58
- 'darkolivegreen' => '85,107,47',
59
- 'darkorange' => '255,140,0',
60
- 'darkorchid' => '153,50,204',
61
- 'darkred' => '139,0,0',
62
- 'darksalmon' => '233,150,122',
63
- 'darkseagreen' => '143,188,143',
64
- 'darkslateblue' => '72,61,139',
65
- 'darkslategray' => '47,79,79',
66
- 'darkslategrey' => '47,79,79',
67
- 'darkturquoise' => '0,206,209',
68
- 'darkviolet' => '148,0,211',
69
- 'deeppink' => '255,20,147',
70
- 'deepskyblue' => '0,191,255',
71
- 'dimgray' => '105,105,105',
72
- 'dimgrey' => '105,105,105',
73
- 'dodgerblue' => '30,144,255',
74
- 'firebrick' => '178,34,34',
75
- 'floralwhite' => '255,250,240',
76
- 'forestgreen' => '34,139,34',
77
- 'fuchsia' => '255,0,255',
78
- 'gainsboro' => '220,220,220',
79
- 'ghostwhite' => '248,248,255',
80
- 'gold' => '255,215,0',
81
- 'goldenrod' => '218,165,32',
82
- 'gray' => '128,128,128',
83
- 'green' => '0,128,0',
84
- 'greenyellow' => '173,255,47',
85
- 'grey' => '128,128,128',
86
- 'honeydew' => '240,255,240',
87
- 'hotpink' => '255,105,180',
88
- 'indianred' => '205,92,92',
89
- 'indigo' => '75,0,130',
90
- 'ivory' => '255,255,240',
91
- 'khaki' => '240,230,140',
92
- 'lavender' => '230,230,250',
93
- 'lavenderblush' => '255,240,245',
94
- 'lawngreen' => '124,252,0',
95
- 'lemonchiffon' => '255,250,205',
96
- 'lightblue' => '173,216,230',
97
- 'lightcoral' => '240,128,128',
98
- 'lightcyan' => '224,255,255',
99
- 'lightgoldenrodyellow' => '250,250,210',
100
- 'lightgray' => '211,211,211',
101
- 'lightgreen' => '144,238,144',
102
- 'lightgrey' => '211,211,211',
103
- 'lightpink' => '255,182,193',
104
- 'lightsalmon' => '255,160,122',
105
- 'lightseagreen' => '32,178,170',
106
- 'lightskyblue' => '135,206,250',
107
- 'lightslategray' => '119,136,153',
108
- 'lightslategrey' => '119,136,153',
109
- 'lightsteelblue' => '176,196,222',
110
- 'lightyellow' => '255,255,224',
111
- 'lime' => '0,255,0',
112
- 'limegreen' => '50,205,50',
113
- 'linen' => '250,240,230',
114
- 'magenta' => '255,0,255',
115
- 'maroon' => '128,0,0',
116
- 'mediumaquamarine' => '102,205,170',
117
- 'mediumblue' => '0,0,205',
118
- 'mediumorchid' => '186,85,211',
119
- 'mediumpurple' => '147,112,219',
120
- 'mediumseagreen' => '60,179,113',
121
- 'mediumslateblue' => '123,104,238',
122
- 'mediumspringgreen' => '0,250,154',
123
- 'mediumturquoise' => '72,209,204',
124
- 'mediumvioletred' => '199,21,133',
125
- 'midnightblue' => '25,25,112',
126
- 'mintcream' => '245,255,250',
127
- 'mistyrose' => '255,228,225',
128
- 'moccasin' => '255,228,181',
129
- 'navajowhite' => '255,222,173',
130
- 'navy' => '0,0,128',
131
- 'oldlace' => '253,245,230',
132
- 'olive' => '128,128,0',
133
- 'olivedrab' => '107,142,35',
134
- 'orange' => '255,165,0',
135
- 'orangered' => '255,69,0',
136
- 'orchid' => '218,112,214',
137
- 'palegoldenrod' => '238,232,170',
138
- 'palegreen' => '152,251,152',
139
- 'paleturquoise' => '175,238,238',
140
- 'palevioletred' => '219,112,147',
141
- 'papayawhip' => '255,239,213',
142
- 'peachpuff' => '255,218,185',
143
- 'peru' => '205,133,63',
144
- 'pink' => '255,192,203',
145
- 'plum' => '221,160,221',
146
- 'powderblue' => '176,224,230',
147
- 'purple' => '128,0,128',
148
- 'red' => '255,0,0',
149
- 'rosybrown' => '188,143,143',
150
- 'royalblue' => '65,105,225',
151
- 'saddlebrown' => '139,69,19',
152
- 'salmon' => '250,128,114',
153
- 'sandybrown' => '244,164,96',
154
- 'seagreen' => '46,139,87',
155
- 'seashell' => '255,245,238',
156
- 'sienna' => '160,82,45',
157
- 'silver' => '192,192,192',
158
- 'skyblue' => '135,206,235',
159
- 'slateblue' => '106,90,205',
160
- 'slategray' => '112,128,144',
161
- 'slategrey' => '112,128,144',
162
- 'snow' => '255,250,250',
163
- 'springgreen' => '0,255,127',
164
- 'steelblue' => '70,130,180',
165
- 'tan' => '210,180,140',
166
- 'teal' => '0,128,128',
167
- 'thistle' => '216,191,216',
168
- 'tomato' => '255,99,71',
169
- 'transparent' => '0,0,0,0',
170
- 'turquoise' => '64,224,208',
171
- 'violet' => '238,130,238',
172
- 'wheat' => '245,222,179',
173
- 'white' => '255,255,255',
174
- 'whitesmoke' => '245,245,245',
175
- 'yellow' => '255,255,0',
176
- 'yellowgreen' => '154,205,50'
177
- );
178
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Compiler.php DELETED
@@ -1,4924 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp;
13
-
14
- use Leafo\ScssPhp\Base\Range;
15
- use Leafo\ScssPhp\Block;
16
- use Leafo\ScssPhp\Colors;
17
- use Leafo\ScssPhp\Compiler\Environment;
18
- use Leafo\ScssPhp\Formatter\OutputBlock;
19
- use Leafo\ScssPhp\Node;
20
- use Leafo\ScssPhp\Type;
21
- use Leafo\ScssPhp\Parser;
22
- use Leafo\ScssPhp\Util;
23
-
24
- /**
25
- * The scss compiler and parser.
26
- *
27
- * Converting SCSS to CSS is a three stage process. The incoming file is parsed
28
- * by `Parser` into a syntax tree, then it is compiled into another tree
29
- * representing the CSS structure by `Compiler`. The CSS tree is fed into a
30
- * formatter, like `Formatter` which then outputs CSS as a string.
31
- *
32
- * During the first compile, all values are *reduced*, which means that their
33
- * types are brought to the lowest form before being dump as strings. This
34
- * handles math equations, variable dereferences, and the like.
35
- *
36
- * The `compile` function of `Compiler` is the entry point.
37
- *
38
- * In summary:
39
- *
40
- * The `Compiler` class creates an instance of the parser, feeds it SCSS code,
41
- * then transforms the resulting tree to a CSS tree. This class also holds the
42
- * evaluation context, such as all available mixins and variables at any given
43
- * time.
44
- *
45
- * The `Parser` class is only concerned with parsing its input.
46
- *
47
- * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
48
- * handling things like indentation.
49
- */
50
-
51
- /**
52
- * SCSS compiler
53
- *
54
- * @author Leaf Corcoran <leafot@gmail.com>
55
- */
56
- class Compiler
57
- {
58
- const LINE_COMMENTS = 1;
59
- const DEBUG_INFO = 2;
60
-
61
- const WITH_RULE = 1;
62
- const WITH_MEDIA = 2;
63
- const WITH_SUPPORTS = 4;
64
- const WITH_ALL = 7;
65
-
66
- /**
67
- * @var array
68
- */
69
- static protected $operatorNames = array(
70
- '+' => 'add',
71
- '-' => 'sub',
72
- '*' => 'mul',
73
- '/' => 'div',
74
- '%' => 'mod',
75
-
76
- '==' => 'eq',
77
- '!=' => 'neq',
78
- '<' => 'lt',
79
- '>' => 'gt',
80
-
81
- '<=' => 'lte',
82
- '>=' => 'gte',
83
- '<=>' => 'cmp',
84
- );
85
-
86
- /**
87
- * @var array
88
- */
89
- static protected $namespaces = array(
90
- 'special' => '%',
91
- 'mixin' => '@',
92
- 'function' => '^',
93
- );
94
-
95
- static public $true = array(Type::T_KEYWORD, 'true');
96
- static public $false = array(Type::T_KEYWORD, 'false');
97
- static public $null = array(Type::T_NULL);
98
- static public $defaultValue = array(Type::T_KEYWORD, '');
99
- static public $selfSelector = array(Type::T_SELF);
100
- static public $emptyList = array(Type::T_LIST, '', array());
101
- static public $emptyMap = array(Type::T_MAP, array(), array());
102
- static public $emptyString = array(Type::T_STRING, '"', array());
103
- static public $with = array(Type::T_KEYWORD, 'with');
104
- static public $without = array(Type::T_KEYWORD, 'without');
105
-
106
- protected $importPaths = array('');
107
- protected $importCache = array();
108
- protected $userFunctions = array();
109
- protected $registeredVars = array();
110
- protected $registeredFeatures = array(
111
- 'extend-selector-pseudoclass' => false,
112
- 'at-error' => true,
113
- 'units-level-3' => false,
114
- 'global-variable-shadowing' => false,
115
- );
116
-
117
- protected $lineNumberStyle = null;
118
-
119
- protected $formatter = 'Leafo\ScssPhp\Formatter\Nested';
120
-
121
- protected $rootEnv;
122
- protected $rootBlock;
123
-
124
- private $indentLevel;
125
- private $commentsSeen;
126
- private $extends;
127
- private $extendsMap;
128
- private $parsedFiles;
129
- private $env;
130
- private $scope;
131
- private $parser;
132
- private $sourcePos;
133
- private $sourceParsers;
134
- private $sourceIndex;
135
- private $storeEnv;
136
- private $charsetSeen;
137
- private $stderr;
138
- private $shouldEvaluate;
139
-
140
- /**
141
- * Compile scss
142
- *
143
- * @api
144
- *
145
- * @param string $code
146
- * @param string $path
147
- *
148
- * @return string
149
- */
150
- public function compile($code, $path = null)
151
- {
152
- $locale = setlocale(LC_NUMERIC, 0);
153
- setlocale(LC_NUMERIC, 'C');
154
-
155
- $this->indentLevel = -1;
156
- $this->commentsSeen = array();
157
- $this->extends = array();
158
- $this->extendsMap = array();
159
- $this->parsedFiles = array();
160
- $this->sourceParsers = array();
161
- $this->sourceIndex = null;
162
- $this->env = null;
163
- $this->scope = null;
164
- $this->storeEnv = null;
165
- $this->stderr = fopen('php://stderr', 'w');
166
-
167
- $this->parser = $this->parserFactory($path);
168
- $tree = $this->parser->parse($code);
169
-
170
- $this->formatter = new $this->formatter();
171
-
172
- $this->rootEnv = $this->pushEnv($tree);
173
- $this->injectVariables($this->registeredVars);
174
- $this->compileRoot($tree);
175
- $this->popEnv();
176
-
177
- $out = $this->formatter->format($this->scope);
178
-
179
- setlocale(LC_NUMERIC, $locale);
180
-
181
- return $out;
182
- }
183
-
184
- /**
185
- * Instantiate parser
186
- *
187
- * @param string $path
188
- *
189
- * @return \Leafo\ScssPhp\Parser
190
- */
191
- private function parserFactory($path)
192
- {
193
- $parser = new Parser($path, count($this->sourceParsers));
194
-
195
- $this->sourceParsers[] = $parser;
196
- $this->addParsedFile($path);
197
-
198
- return $parser;
199
- }
200
-
201
- /**
202
- * Is self extend?
203
- *
204
- * @param array $target
205
- * @param array $origin
206
- *
207
- * @return boolean
208
- */
209
- protected function isSelfExtend($target, $origin)
210
- {
211
- foreach ($origin as $sel) {
212
- if (in_array($target, $sel)) {
213
- return true;
214
- }
215
- }
216
-
217
- return false;
218
- }
219
-
220
- /**
221
- * Push extends
222
- *
223
- * @param array $target
224
- * @param array $origin
225
- */
226
- protected function pushExtends($target, $origin)
227
- {
228
- if ($this->isSelfExtend($target, $origin)) {
229
- return;
230
- }
231
-
232
- $i = count($this->extends);
233
- $this->extends[] = array($target, $origin);
234
-
235
- foreach ($target as $part) {
236
- if (isset($this->extendsMap[$part])) {
237
- $this->extendsMap[$part][] = $i;
238
- } else {
239
- $this->extendsMap[$part] = array($i);
240
- }
241
- }
242
- }
243
-
244
- /**
245
- * Make output block
246
- *
247
- * @param string $type
248
- * @param array $selectors
249
- *
250
- * @return \Leafo\ScssPhp\Formatter\OutputBlock
251
- */
252
- protected function makeOutputBlock($type, $selectors = null)
253
- {
254
- $out = new OutputBlock;
255
- $out->type = $type;
256
- $out->lines = array();
257
- $out->children = array();
258
- $out->parent = $this->scope;
259
- $out->selectors = $selectors;
260
- $out->depth = $this->env->depth;
261
-
262
- return $out;
263
- }
264
-
265
- /**
266
- * Compile root
267
- *
268
- * @param \Leafo\ScssPhp\Block $rootBlock
269
- */
270
- protected function compileRoot(Block $rootBlock)
271
- {
272
- $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT);
273
-
274
- $this->compileChildrenNoReturn($rootBlock->children, $this->scope);
275
- $this->flattenSelectors($this->scope);
276
- }
277
-
278
- /**
279
- * Flatten selectors
280
- *
281
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
282
- * @param string $parentKey
283
- */
284
- protected function flattenSelectors(OutputBlock $block, $parentKey = null)
285
- {
286
- if ($block->selectors) {
287
- $selectors = array();
288
-
289
- foreach ($block->selectors as $s) {
290
- $selectors[] = $s;
291
-
292
- if (! is_array($s)) {
293
- continue;
294
- }
295
-
296
- // check extends
297
- if (! empty($this->extendsMap)) {
298
- $this->matchExtends($s, $selectors);
299
-
300
- // remove duplicates
301
- array_walk($selectors, function (&$value) {
302
- $value = serialize($value);
303
- });
304
-
305
- $selectors = array_unique($selectors);
306
-
307
- array_walk($selectors, function (&$value) {
308
- $value = unserialize($value);
309
- });
310
- }
311
- }
312
-
313
- $block->selectors = array();
314
- $placeholderSelector = false;
315
-
316
- foreach ($selectors as $selector) {
317
- if ($this->hasSelectorPlaceholder($selector)) {
318
- $placeholderSelector = true;
319
- continue;
320
- }
321
-
322
- $block->selectors[] = $this->compileSelector($selector);
323
- }
324
-
325
- if ($placeholderSelector && 0 === count($block->selectors) && null !== $parentKey) {
326
- unset($block->parent->children[$parentKey]);
327
-
328
- return;
329
- }
330
- }
331
-
332
- foreach ($block->children as $key => $child) {
333
- $this->flattenSelectors($child, $key);
334
- }
335
- }
336
-
337
- /**
338
- * Match extends
339
- *
340
- * @param array $selector
341
- * @param array $out
342
- * @param integer $from
343
- * @param boolean $initial
344
- */
345
- protected function matchExtends($selector, &$out, $from = 0, $initial = true)
346
- {
347
- foreach ($selector as $i => $part) {
348
- if ($i < $from) {
349
- continue;
350
- }
351
-
352
- if ($this->matchExtendsSingle($part, $origin)) {
353
- $before = array_slice($selector, 0, $i);
354
- $after = array_slice($selector, $i + 1);
355
- $s = count($before);
356
-
357
- foreach ($origin as $new) {
358
- $k = 0;
359
-
360
- // remove shared parts
361
- if ($initial) {
362
- while ($k < $s && isset($new[$k]) && $before[$k] === $new[$k]) {
363
- $k++;
364
- }
365
- }
366
-
367
- $result = array_merge(
368
- $before,
369
- $k > 0 ? array_slice($new, $k) : $new,
370
- $after
371
- );
372
-
373
- if ($result === $selector) {
374
- continue;
375
- }
376
-
377
- $out[] = $result;
378
-
379
- // recursively check for more matches
380
- $this->matchExtends($result, $out, $i, false);
381
-
382
- // selector sequence merging
383
- if (! empty($before) && count($new) > 1) {
384
- $result2 = array_merge(
385
- array_slice($new, 0, -1),
386
- $k > 0 ? array_slice($before, $k) : $before,
387
- array_slice($new, -1),
388
- $after
389
- );
390
-
391
- $out[] = $result2;
392
- }
393
- }
394
- }
395
- }
396
- }
397
-
398
- /**
399
- * Match extends single
400
- *
401
- * @param array $rawSingle
402
- * @param array $outOrigin
403
- *
404
- * @return boolean
405
- */
406
- protected function matchExtendsSingle($rawSingle, &$outOrigin)
407
- {
408
- $counts = array();
409
- $single = array();
410
-
411
- foreach ($rawSingle as $part) {
412
- // matches Number
413
- if (! is_string($part)) {
414
- return false;
415
- }
416
-
417
- if (! preg_match('/^[\[.:#%]/', $part) && count($single)) {
418
- $single[count($single) - 1] .= $part;
419
- } else {
420
- $single[] = $part;
421
- }
422
- }
423
-
424
- foreach ($single as $part) {
425
- if (isset($this->extendsMap[$part])) {
426
- foreach ($this->extendsMap[$part] as $idx) {
427
- $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
428
- }
429
- }
430
- }
431
-
432
- $outOrigin = array();
433
- $found = false;
434
-
435
- foreach ($counts as $idx => $count) {
436
- list($target, $origin) = $this->extends[$idx];
437
-
438
- // check count
439
- if ($count !== count($target)) {
440
- continue;
441
- }
442
-
443
- $rem = array_diff($single, $target);
444
-
445
- foreach ($origin as $j => $new) {
446
- // prevent infinite loop when target extends itself
447
- if ($this->isSelfExtend($single, $origin)) {
448
- return false;
449
- }
450
-
451
- $origin[$j][count($origin[$j]) - 1] = $this->combineSelectorSingle(end($new), $rem);
452
- }
453
-
454
- $outOrigin = array_merge($outOrigin, $origin);
455
-
456
- $found = true;
457
- }
458
-
459
- return $found;
460
- }
461
-
462
- /**
463
- * Combine selector single
464
- *
465
- * @param array $base
466
- * @param array $other
467
- *
468
- * @return array
469
- */
470
- protected function combineSelectorSingle($base, $other)
471
- {
472
- $tag = null;
473
- $out = array();
474
-
475
- foreach (array($base, $other) as $single) {
476
- foreach ($single as $part) {
477
- if (preg_match('/^[^\[.#:]/', $part)) {
478
- $tag = $part;
479
- } else {
480
- $out[] = $part;
481
- }
482
- }
483
- }
484
-
485
- if ($tag) {
486
- array_unshift($out, $tag);
487
- }
488
-
489
- return $out;
490
- }
491
-
492
- /**
493
- * Compile media
494
- *
495
- * @param \Leafo\ScssPhp\Block $media
496
- */
497
- protected function compileMedia(Block $media)
498
- {
499
- $this->pushEnv($media);
500
-
501
- $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
502
-
503
- if (! empty($mediaQuery)) {
504
- $this->scope = $this->makeOutputBlock(Type::T_MEDIA, array($mediaQuery));
505
-
506
- $parentScope = $this->mediaParent($this->scope);
507
- $parentScope->children[] = $this->scope;
508
-
509
- // top level properties in a media cause it to be wrapped
510
- $needsWrap = false;
511
-
512
- foreach ($media->children as $child) {
513
- $type = $child[0];
514
-
515
- if ($type !== Type::T_BLOCK &&
516
- $type !== Type::T_MEDIA &&
517
- $type !== Type::T_DIRECTIVE &&
518
- $type !== Type::T_IMPORT
519
- ) {
520
- $needsWrap = true;
521
- break;
522
- }
523
- }
524
-
525
- if ($needsWrap) {
526
- $wrapped = new Block;
527
- $wrapped->selectors = array();
528
- $wrapped->children = $media->children;
529
-
530
- $media->children = array(array(Type::T_BLOCK, $wrapped));
531
- }
532
-
533
- $this->compileChildrenNoReturn($media->children, $this->scope);
534
-
535
- $this->scope = $this->scope->parent;
536
- }
537
-
538
- $this->popEnv();
539
- }
540
-
541
- /**
542
- * Media parent
543
- *
544
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope
545
- *
546
- * @return \Leafo\ScssPhp\Formatter\OutputBlock
547
- */
548
- protected function mediaParent(OutputBlock $scope)
549
- {
550
- while (! empty($scope->parent)) {
551
- if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) {
552
- break;
553
- }
554
-
555
- $scope = $scope->parent;
556
- }
557
-
558
- return $scope;
559
- }
560
-
561
- /**
562
- * Compile directive
563
- *
564
- * @param \Leafo\ScssPhp\Block $block
565
- */
566
- protected function compileDirective(Block $block)
567
- {
568
- $s = '@' . $block->name;
569
-
570
- if (! empty($block->value)) {
571
- $s .= ' ' . $this->compileValue($block->value);
572
- }
573
-
574
- if ($block->name === 'keyframes' || substr($block->name, -10) === '-keyframes') {
575
- $this->compileKeyframeBlock($block, array($s));
576
- } else {
577
- $this->compileNestedBlock($block, array($s));
578
- }
579
- }
580
-
581
- /**
582
- * Compile at-root
583
- *
584
- * @param \Leafo\ScssPhp\Block $block
585
- */
586
- protected function compileAtRoot(Block $block)
587
- {
588
- $env = $this->pushEnv($block);
589
- $envs = $this->compactEnv($env);
590
- $without = isset($block->with) ? $this->compileWith($block->with) : self::WITH_RULE;
591
-
592
- // wrap inline selector
593
- if ($block->selector) {
594
- $wrapped = new Block;
595
- $wrapped->parent = $block;
596
- $wrapped->sourcePosition = $block->sourcePosition;
597
- $wrapped->sourceIndex = $block->sourceIndex;
598
- $wrapped->selectors = $block->selector;
599
- $wrapped->comments = array();
600
- $wrapped->children = $block->children;
601
-
602
- $block->children = array(array(Type::T_BLOCK, $wrapped));
603
- }
604
-
605
- $this->env = $this->filterWithout($envs, $without);
606
- $newBlock = $this->spliceTree($envs, $block, $without);
607
-
608
- $saveScope = $this->scope;
609
- $this->scope = $this->rootBlock;
610
-
611
- $this->compileChild($newBlock, $this->scope);
612
-
613
- $this->scope = $saveScope;
614
- $this->env = $this->extractEnv($envs);
615
-
616
- $this->popEnv();
617
- }
618
-
619
- /**
620
- * Splice parse tree
621
- *
622
- * @param array $envs
623
- * @param \Leafo\ScssPhp\Block $block
624
- * @param integer $without
625
- *
626
- * @return array
627
- */
628
- private function spliceTree($envs, Block $block, $without)
629
- {
630
- $newBlock = null;
631
-
632
- foreach ($envs as $e) {
633
- if (! isset($e->block)) {
634
- continue;
635
- }
636
-
637
- if (isset($e->block) && $e->block === $block) {
638
- continue;
639
- }
640
-
641
- if (isset($e->block->type) && $e->block->type === Type::T_AT_ROOT) {
642
- continue;
643
- }
644
-
645
- if (($without & self::WITH_RULE) && isset($e->block->selectors)) {
646
- continue;
647
- }
648
-
649
- if (($without & self::WITH_MEDIA) &&
650
- isset($e->block->type) && $e->block->type === Type::T_MEDIA
651
- ) {
652
- continue;
653
- }
654
-
655
- if (($without & self::WITH_SUPPORTS) &&
656
- isset($e->block->type) && $e->block->type === Type::T_DIRECTIVE &&
657
- isset($e->block->name) && $e->block->name === 'supports'
658
- ) {
659
- continue;
660
- }
661
-
662
- $b = new Block;
663
-
664
- if (isset($e->block->sourcePosition)) {
665
- $b->sourcePosition = $e->block->sourcePosition;
666
- }
667
-
668
- if (isset($e->block->sourceIndex)) {
669
- $b->sourceIndex = $e->block->sourceIndex;
670
- }
671
-
672
- $b->selectors = array();
673
-
674
- if (isset($e->block->comments)) {
675
- $b->comments = $e->block->comments;
676
- }
677
-
678
- if (isset($e->block->type)) {
679
- $b->type = $e->block->type;
680
- }
681
-
682
- if (isset($e->block->name)) {
683
- $b->name = $e->block->name;
684
- }
685
-
686
- if (isset($e->block->queryList)) {
687
- $b->queryList = $e->block->queryList;
688
- }
689
-
690
- if (isset($e->block->value)) {
691
- $b->value = $e->block->value;
692
- }
693
-
694
- if ($newBlock) {
695
- $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
696
-
697
- $b->children = array(array($type, $newBlock));
698
-
699
- $newBlock->parent = $b;
700
- } elseif (count($block->children)) {
701
- foreach ($block->children as $child) {
702
- if ($child[0] === Type::T_BLOCK) {
703
- $child[1]->parent = $b;
704
- }
705
- }
706
-
707
- $b->children = $block->children;
708
- }
709
-
710
- $b->parent = null;
711
-
712
- $newBlock = $b;
713
- }
714
-
715
- $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
716
-
717
- return array($type, $newBlock);
718
- }
719
-
720
- /**
721
- * Compile @at-root's with: inclusion / without: exclusion into filter flags
722
- *
723
- * @param array $with
724
- *
725
- * @return integer
726
- */
727
- private function compileWith($with)
728
- {
729
- static $mapping = array(
730
- 'rule' => self::WITH_RULE,
731
- 'media' => self::WITH_MEDIA,
732
- 'supports' => self::WITH_SUPPORTS,
733
- 'all' => self::WITH_ALL,
734
- );
735
-
736
- // exclude selectors by default
737
- $without = self::WITH_RULE;
738
-
739
- if ($this->libMapHasKey(array($with, self::$with))) {
740
- $without = self::WITH_ALL;
741
-
742
- $list = $this->coerceList($this->libMapGet(array($with, self::$with)));
743
-
744
- foreach ($list[2] as $item) {
745
- $keyword = $this->compileStringContent($this->coerceString($item));
746
-
747
- if (array_key_exists($keyword, $mapping)) {
748
- $without &= ~($mapping[$keyword]);
749
- }
750
- }
751
- }
752
-
753
- if ($this->libMapHasKey(array($with, self::$without))) {
754
- $without = 0;
755
-
756
- $list = $this->coerceList($this->libMapGet(array($with, self::$without)));
757
-
758
- foreach ($list[2] as $item) {
759
- $keyword = $this->compileStringContent($this->coerceString($item));
760
-
761
- if (array_key_exists($keyword, $mapping)) {
762
- $without |= $mapping[$keyword];
763
- }
764
- }
765
- }
766
-
767
- return $without;
768
- }
769
-
770
- /**
771
- * Filter env stack
772
- *
773
- * @param array $envs
774
- * @param integer $without
775
- *
776
- * @return \Leafo\ScssPhp\Compiler\Environment
777
- */
778
- private function filterWithout($envs, $without)
779
- {
780
- $filtered = array();
781
-
782
- foreach ($envs as $e) {
783
- if (($without & self::WITH_RULE) && isset($e->block->selectors)) {
784
- continue;
785
- }
786
-
787
- if (($without & self::WITH_MEDIA) &&
788
- isset($e->block->type) && $e->block->type === Type::T_MEDIA
789
- ) {
790
- continue;
791
- }
792
-
793
- if (($without & self::WITH_SUPPORTS) &&
794
- isset($e->block->type) && $e->block->type === Type::T_DIRECTIVE &&
795
- isset($e->block->name) && $e->block->name === 'supports'
796
- ) {
797
- continue;
798
- }
799
-
800
- $filtered[] = $e;
801
- }
802
-
803
- return $this->extractEnv($filtered);
804
- }
805
-
806
- /**
807
- * Compile keyframe block
808
- *
809
- * @param \Leafo\ScssPhp\Block $block
810
- * @param array $selectors
811
- */
812
- protected function compileKeyframeBlock(Block $block, $selectors)
813
- {
814
- $env = $this->pushEnv($block);
815
-
816
- $envs = $this->compactEnv($env);
817
-
818
- $this->env = $this->extractEnv(array_filter($envs, function ($e) {
819
- return ! isset($e->block->selectors);
820
- }));
821
-
822
- $this->scope = $this->makeOutputBlock($block->type, $selectors);
823
- $this->scope->depth = 1;
824
- $this->scope->parent->children[] = $this->scope;
825
-
826
- $this->compileChildrenNoReturn($block->children, $this->scope);
827
-
828
- $this->scope = $this->scope->parent;
829
- $this->env = $this->extractEnv($envs);
830
-
831
- $this->popEnv();
832
- }
833
-
834
- /**
835
- * Compile nested block
836
- *
837
- * @param \Leafo\ScssPhp\Block $block
838
- * @param array $selectors
839
- */
840
- protected function compileNestedBlock(Block $block, $selectors)
841
- {
842
- $this->pushEnv($block);
843
-
844
- $this->scope = $this->makeOutputBlock($block->type, $selectors);
845
- $this->scope->parent->children[] = $this->scope;
846
-
847
- $this->compileChildrenNoReturn($block->children, $this->scope);
848
-
849
- $this->scope = $this->scope->parent;
850
-
851
- $this->popEnv();
852
- }
853
-
854
- /**
855
- * Recursively compiles a block.
856
- *
857
- * A block is analogous to a CSS block in most cases. A single SCSS document
858
- * is encapsulated in a block when parsed, but it does not have parent tags
859
- * so all of its children appear on the root level when compiled.
860
- *
861
- * Blocks are made up of selectors and children.
862
- *
863
- * The children of a block are just all the blocks that are defined within.
864
- *
865
- * Compiling the block involves pushing a fresh environment on the stack,
866
- * and iterating through the props, compiling each one.
867
- *
868
- * @see Compiler::compileChild()
869
- *
870
- * @param \Leafo\ScssPhp\Block $block
871
- */
872
- protected function compileBlock(Block $block)
873
- {
874
- $env = $this->pushEnv($block);
875
- $env->selectors = $this->evalSelectors($block->selectors);
876
-
877
- $out = $this->makeOutputBlock(null);
878
-
879
- if (isset($this->lineNumberStyle) && count($env->selectors) && count($block->children)) {
880
- $annotation = $this->makeOutputBlock(Type::T_COMMENT);
881
- $annotation->depth = 0;
882
-
883
- $parser = $this->sourceParsers[$block->sourceIndex];
884
- $file = $parser->getSourceName();
885
- $line = $parser->getLineNo($block->sourcePosition);
886
-
887
- switch ($this->lineNumberStyle) {
888
- case self::LINE_COMMENTS:
889
- $annotation->lines[] = '/* line ' . $line . ', ' . $file . ' */';
890
- break;
891
-
892
- case self::DEBUG_INFO:
893
- $annotation->lines[] = '@media -sass-debug-info{filename{font-family:"' . $file
894
- . '"}line{font-family:' . $line . '}}';
895
- break;
896
- }
897
-
898
- $this->scope->children[] = $annotation;
899
- }
900
-
901
- $this->scope->children[] = $out;
902
-
903
- if (count($block->children)) {
904
- $out->selectors = $this->multiplySelectors($env);
905
-
906
- $this->compileChildrenNoReturn($block->children, $out);
907
- }
908
-
909
- $this->formatter->stripSemicolon($out->lines);
910
-
911
- $this->popEnv();
912
- }
913
-
914
- /**
915
- * Compile root level comment
916
- *
917
- * @param array $block
918
- */
919
- protected function compileComment($block)
920
- {
921
- $out = $this->makeOutputBlock(Type::T_COMMENT);
922
- $out->lines[] = $block[1];
923
- $this->scope->children[] = $out;
924
- }
925
-
926
- /**
927
- * Evaluate selectors
928
- *
929
- * @param array $selectors
930
- *
931
- * @return array
932
- */
933
- protected function evalSelectors($selectors)
934
- {
935
- $this->shouldEvaluate = false;
936
-
937
- $selectors = array_map(array($this, 'evalSelector'), $selectors);
938
-
939
- // after evaluating interpolates, we might need a second pass
940
- if ($this->shouldEvaluate) {
941
- $buffer = $this->collapseSelectors($selectors);
942
- $parser = $this->parserFactory(__METHOD__);
943
-
944
- if ($parser->parseSelector($buffer, $newSelectors)) {
945
- $selectors = array_map(array($this, 'evalSelector'), $newSelectors);
946
- }
947
- }
948
-
949
- return $selectors;
950
- }
951
-
952
- /**
953
- * Evaluate selector
954
- *
955
- * @param array $selector
956
- *
957
- * @return array
958
- */
959
- protected function evalSelector($selector)
960
- {
961
- return array_map(array($this, 'evalSelectorPart'), $selector);
962
- }
963
-
964
- /**
965
- * Evaluate selector part; replaces all the interpolates, stripping quotes
966
- *
967
- * @param array $part
968
- *
969
- * @return array
970
- */
971
- protected function evalSelectorPart($part)
972
- {
973
- foreach ($part as &$p) {
974
- if (is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
975
- $p = $this->compileValue($p);
976
-
977
- // force re-evaluation
978
- if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
979
- $this->shouldEvaluate = true;
980
- }
981
- } elseif (is_string($p) && strlen($p) >= 2 &&
982
- ($first = $p[0]) && ($first === '"' || $first === "'") &&
983
- substr($p, -1) === $first
984
- ) {
985
- $p = substr($p, 1, -1);
986
- }
987
- }
988
-
989
- return $this->flattenSelectorSingle($part);
990
- }
991
-
992
- /**
993
- * Collapse selectors
994
- *
995
- * @param array $selectors
996
- *
997
- * @return string
998
- */
999
- protected function collapseSelectors($selectors)
1000
- {
1001
- $parts = array();
1002
-
1003
- foreach ($selectors as $selector) {
1004
- $output = '';
1005
-
1006
- array_walk_recursive(
1007
- $selector,
1008
- function ($value, $key) use (&$output) {
1009
- $output .= $value;
1010
- }
1011
- );
1012
-
1013
- $parts[] = $output;
1014
- }
1015
-
1016
- return implode(', ', $parts);
1017
- }
1018
-
1019
- /**
1020
- * Flatten selector single; joins together .classes and #ids
1021
- *
1022
- * @param array $single
1023
- *
1024
- * @return array
1025
- */
1026
- protected function flattenSelectorSingle($single)
1027
- {
1028
- $joined = array();
1029
-
1030
- foreach ($single as $part) {
1031
- if (empty($joined) ||
1032
- ! is_string($part) ||
1033
- preg_match('/[\[.:#%]/', $part)
1034
- ) {
1035
- $joined[] = $part;
1036
- continue;
1037
- }
1038
-
1039
- if (is_array(end($joined))) {
1040
- $joined[] = $part;
1041
- } else {
1042
- $joined[count($joined) - 1] .= $part;
1043
- }
1044
- }
1045
-
1046
- return $joined;
1047
- }
1048
-
1049
- /**
1050
- * Compile selector to string; self(&) should have been replaced by now
1051
- *
1052
- * @param array $selector
1053
- *
1054
- * @return string
1055
- */
1056
- protected function compileSelector($selector)
1057
- {
1058
- if (! is_array($selector)) {
1059
- return $selector; // media and the like
1060
- }
1061
-
1062
- return implode(
1063
- ' ',
1064
- array_map(
1065
- array($this, 'compileSelectorPart'),
1066
- $selector
1067
- )
1068
- );
1069
- }
1070
-
1071
- /**
1072
- * Compile selector part
1073
- *
1074
- * @param arary $piece
1075
- *
1076
- * @return string
1077
- */
1078
- protected function compileSelectorPart($piece)
1079
- {
1080
- foreach ($piece as &$p) {
1081
- if (! is_array($p)) {
1082
- continue;
1083
- }
1084
-
1085
- switch ($p[0]) {
1086
- case Type::T_SELF:
1087
- $p = '&';
1088
- break;
1089
-
1090
- default:
1091
- $p = $this->compileValue($p);
1092
- break;
1093
- }
1094
- }
1095
-
1096
- return implode($piece);
1097
- }
1098
-
1099
- /**
1100
- * Has selector placeholder?
1101
- *
1102
- * @param array $selector
1103
- *
1104
- * @return boolean
1105
- */
1106
- protected function hasSelectorPlaceholder($selector)
1107
- {
1108
- if (! is_array($selector)) {
1109
- return false;
1110
- }
1111
-
1112
- foreach ($selector as $parts) {
1113
- foreach ($parts as $part) {
1114
- if ('%' === $part[0]) {
1115
- return true;
1116
- }
1117
- }
1118
- }
1119
-
1120
- return false;
1121
- }
1122
-
1123
- /**
1124
- * Compile children and return result
1125
- *
1126
- * @param array $stms
1127
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
1128
- *
1129
- * @return array
1130
- */
1131
- protected function compileChildren($stms, OutputBlock $out)
1132
- {
1133
- foreach ($stms as $stm) {
1134
- $ret = $this->compileChild($stm, $out);
1135
-
1136
- if (isset($ret)) {
1137
- return $ret;
1138
- }
1139
- }
1140
- }
1141
-
1142
- /**
1143
- * Compile children and throw exception if unexpected @return
1144
- *
1145
- * @param array $stms
1146
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
1147
- *
1148
- * @throws \Exception
1149
- */
1150
- protected function compileChildrenNoReturn($stms, OutputBlock $out)
1151
- {
1152
- foreach ($stms as $stm) {
1153
- $ret = $this->compileChild($stm, $out);
1154
-
1155
- if (isset($ret)) {
1156
- $this->throwError('@return may only be used within a function');
1157
- }
1158
- }
1159
- }
1160
-
1161
- /**
1162
- * Compile media query
1163
- *
1164
- * @param array $queryList
1165
- *
1166
- * @return string
1167
- */
1168
- protected function compileMediaQuery($queryList)
1169
- {
1170
- $out = '@media';
1171
- $first = true;
1172
-
1173
- foreach ($queryList as $query) {
1174
- $type = null;
1175
- $parts = array();
1176
-
1177
- foreach ($query as $q) {
1178
- switch ($q[0]) {
1179
- case Type::T_MEDIA_TYPE:
1180
- if ($type) {
1181
- $type = $this->mergeMediaTypes(
1182
- $type,
1183
- array_map(array($this, 'compileValue'), array_slice($q, 1))
1184
- );
1185
-
1186
- if (empty($type)) { // merge failed
1187
- return null;
1188
- }
1189
- } else {
1190
- $type = array_map(array($this, 'compileValue'), array_slice($q, 1));
1191
- }
1192
- break;
1193
-
1194
- case Type::T_MEDIA_EXPRESSION:
1195
- if (isset($q[2])) {
1196
- $parts[] = '('
1197
- . $this->compileValue($q[1])
1198
- . $this->formatter->assignSeparator
1199
- . $this->compileValue($q[2])
1200
- . ')';
1201
- } else {
1202
- $parts[] = '('
1203
- . $this->compileValue($q[1])
1204
- . ')';
1205
- }
1206
- break;
1207
-
1208
- case Type::T_MEDIA_VALUE:
1209
- $parts[] = $this->compileValue($q[1]);
1210
- break;
1211
- }
1212
- }
1213
-
1214
- if ($type) {
1215
- array_unshift($parts, implode(' ', array_filter($type)));
1216
- }
1217
-
1218
- if (! empty($parts)) {
1219
- if ($first) {
1220
- $first = false;
1221
- $out .= ' ';
1222
- } else {
1223
- $out .= $this->formatter->tagSeparator;
1224
- }
1225
-
1226
- $out .= implode(' and ', $parts);
1227
- }
1228
- }
1229
-
1230
- return $out;
1231
- }
1232
-
1233
- /**
1234
- * Merge media types
1235
- *
1236
- * @param array $type1
1237
- * @param array $type2
1238
- *
1239
- * @return array|null
1240
- */
1241
- protected function mergeMediaTypes($type1, $type2)
1242
- {
1243
- if (empty($type1)) {
1244
- return $type2;
1245
- }
1246
-
1247
- if (empty($type2)) {
1248
- return $type1;
1249
- }
1250
-
1251
- $m1 = '';
1252
- $t1 = '';
1253
-
1254
- if (count($type1) > 1) {
1255
- $m1= strtolower($type1[0]);
1256
- $t1= strtolower($type1[1]);
1257
- } else {
1258
- $t1 = strtolower($type1[0]);
1259
- }
1260
-
1261
- $m2 = '';
1262
- $t2 = '';
1263
-
1264
- if (count($type2) > 1) {
1265
- $m2 = strtolower($type2[0]);
1266
- $t2 = strtolower($type2[1]);
1267
- } else {
1268
- $t2 = strtolower($type2[0]);
1269
- }
1270
-
1271
- if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) {
1272
- if ($t1 === $t2) {
1273
- return null;
1274
- }
1275
-
1276
- return array(
1277
- $m1 === Type::T_NOT ? $m2 : $m1,
1278
- $m1 === Type::T_NOT ? $t2 : $t1,
1279
- );
1280
- }
1281
-
1282
- if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) {
1283
- // CSS has no way of representing "neither screen nor print"
1284
- if ($t1 !== $t2) {
1285
- return null;
1286
- }
1287
-
1288
- return array(Type::T_NOT, $t1);
1289
- }
1290
-
1291
- if ($t1 !== $t2) {
1292
- return null;
1293
- }
1294
-
1295
- // t1 == t2, neither m1 nor m2 are "not"
1296
- return array(empty($m1)? $m2 : $m1, $t1);
1297
- }
1298
-
1299
- /**
1300
- * Compile import; returns true if the value was something that could be imported
1301
- *
1302
- * @param array $rawPath
1303
- * @param array $out
1304
- *
1305
- * @return boolean
1306
- */
1307
- protected function compileImport($rawPath, $out)
1308
- {
1309
- if ($rawPath[0] === Type::T_STRING) {
1310
- $path = $this->compileStringContent($rawPath);
1311
-
1312
- if ($path = $this->findImport($path)) {
1313
- $this->importFile($path, $out);
1314
-
1315
- return true;
1316
- }
1317
-
1318
- return false;
1319
- }
1320
-
1321
- if ($rawPath[0] === Type::T_LIST) {
1322
- // handle a list of strings
1323
- if (count($rawPath[2]) === 0) {
1324
- return false;
1325
- }
1326
-
1327
- foreach ($rawPath[2] as $path) {
1328
- if ($path[0] !== Type::T_STRING) {
1329
- return false;
1330
- }
1331
- }
1332
-
1333
- foreach ($rawPath[2] as $path) {
1334
- $this->compileImport($path, $out);
1335
- }
1336
-
1337
- return true;
1338
- }
1339
-
1340
- return false;
1341
- }
1342
-
1343
- /**
1344
- * Compile child; returns a value to halt execution
1345
- *
1346
- * @param array $child
1347
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
1348
- *
1349
- * @return array
1350
- */
1351
- protected function compileChild($child, OutputBlock $out)
1352
- {
1353
- $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
1354
- $this->sourcePos = isset($child[Parser::SOURCE_POSITION]) ? $child[Parser::SOURCE_POSITION] : -1;
1355
-
1356
- switch ($child[0]) {
1357
- case Type::T_IMPORT:
1358
- list(, $rawPath) = $child;
1359
-
1360
- $rawPath = $this->reduce($rawPath);
1361
-
1362
- if (! $this->compileImport($rawPath, $out)) {
1363
- $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
1364
- }
1365
- break;
1366
-
1367
- case Type::T_DIRECTIVE:
1368
- $this->compileDirective($child[1]);
1369
- break;
1370
-
1371
- case Type::T_AT_ROOT:
1372
- $this->compileAtRoot($child[1]);
1373
- break;
1374
-
1375
- case Type::T_MEDIA:
1376
- $this->compileMedia($child[1]);
1377
- break;
1378
-
1379
- case Type::T_BLOCK:
1380
- $this->compileBlock($child[1]);
1381
- break;
1382
-
1383
- case Type::T_CHARSET:
1384
- if (! $this->charsetSeen) {
1385
- $this->charsetSeen = true;
1386
-
1387
- $out->lines[] = '@charset ' . $this->compileValue($child[1]) . ';';
1388
- }
1389
- break;
1390
-
1391
- case Type::T_ASSIGN:
1392
- list(, $name, $value) = $child;
1393
-
1394
- if ($name[0] === Type::T_VARIABLE) {
1395
- $flag = isset($child[3]) ? $child[3] : null;
1396
- $isDefault = $flag === '!default';
1397
- $isGlobal = $flag === '!global';
1398
-
1399
- if ($isGlobal) {
1400
- $this->set($name[1], $this->reduce($value), false, $this->rootEnv);
1401
- break;
1402
- }
1403
-
1404
- $shouldSet = $isDefault &&
1405
- (($result = $this->get($name[1], false)) === null
1406
- || $result === self::$null);
1407
-
1408
- if (! $isDefault || $shouldSet) {
1409
- $this->set($name[1], $this->reduce($value));
1410
- }
1411
- break;
1412
- }
1413
-
1414
- $compiledName = $this->compileValue($name);
1415
-
1416
- // handle shorthand syntax: size / line-height
1417
- if ($compiledName === 'font') {
1418
- if ($value[0] === Type::T_EXPRESSION && $value[1] === '/') {
1419
- $value = $this->expToString($value);
1420
- } elseif ($value[0] === Type::T_LIST) {
1421
- foreach ($value[2] as &$item) {
1422
- if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
1423
- $item = $this->expToString($item);
1424
- }
1425
- }
1426
- }
1427
- }
1428
-
1429
- // if the value reduces to null from something else then
1430
- // the property should be discarded
1431
- if ($value[0] !== Type::T_NULL) {
1432
- $value = $this->reduce($value);
1433
-
1434
- if ($value[0] === Type::T_NULL) {
1435
- break;
1436
- }
1437
- }
1438
-
1439
- $compiledValue = $this->compileValue($value);
1440
-
1441
- $out->lines[] = $this->formatter->property(
1442
- $compiledName,
1443
- $compiledValue
1444
- );
1445
- break;
1446
-
1447
- case Type::T_COMMENT:
1448
- if ($out->type === Type::T_ROOT) {
1449
- $this->compileComment($child);
1450
- break;
1451
- }
1452
-
1453
- $out->lines[] = $child[1];
1454
- break;
1455
-
1456
- case Type::T_MIXIN:
1457
- case Type::T_FUNCTION:
1458
- list(, $block) = $child;
1459
-
1460
- $this->set(self::$namespaces[$block->type] . $block->name, $block);
1461
- break;
1462
-
1463
- case Type::T_EXTEND:
1464
- list(, $selectors) = $child;
1465
-
1466
- foreach ($selectors as $sel) {
1467
- $results = $this->evalSelectors(array($sel));
1468
-
1469
- foreach ($results as $result) {
1470
- // only use the first one
1471
- $result = current($result);
1472
-
1473
- $this->pushExtends($result, $out->selectors);
1474
- }
1475
- }
1476
- break;
1477
-
1478
- case Type::T_IF:
1479
- list(, $if) = $child;
1480
-
1481
- if ($this->isTruthy($this->reduce($if->cond, true))) {
1482
- return $this->compileChildren($if->children, $out);
1483
- }
1484
-
1485
- foreach ($if->cases as $case) {
1486
- if ($case->type === Type::T_ELSE ||
1487
- $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))
1488
- ) {
1489
- return $this->compileChildren($case->children, $out);
1490
- }
1491
- }
1492
- break;
1493
-
1494
- case Type::T_EACH:
1495
- list(, $each) = $child;
1496
-
1497
- $list = $this->coerceList($this->reduce($each->list));
1498
-
1499
- $this->pushEnv();
1500
-
1501
- foreach ($list[2] as $item) {
1502
- if (count($each->vars) === 1) {
1503
- $this->set($each->vars[0], $item, true);
1504
- } else {
1505
- list(,, $values) = $this->coerceList($item);
1506
-
1507
- foreach ($each->vars as $i => $var) {
1508
- $this->set($var, isset($values[$i]) ? $values[$i] : self::$null, true);
1509
- }
1510
- }
1511
-
1512
- $ret = $this->compileChildren($each->children, $out);
1513
-
1514
- if ($ret) {
1515
- if ($ret[0] !== Type::T_CONTROL) {
1516
- $this->popEnv();
1517
-
1518
- return $ret;
1519
- }
1520
-
1521
- if ($ret[1]) {
1522
- break;
1523
- }
1524
- }
1525
- }
1526
-
1527
- $this->popEnv();
1528
- break;
1529
-
1530
- case Type::T_WHILE:
1531
- list(, $while) = $child;
1532
-
1533
- while ($this->isTruthy($this->reduce($while->cond, true))) {
1534
- $ret = $this->compileChildren($while->children, $out);
1535
-
1536
- if ($ret) {
1537
- if ($ret[0] !== Type::T_CONTROL) {
1538
- return $ret;
1539
- }
1540
-
1541
- if ($ret[1]) {
1542
- break;
1543
- }
1544
- }
1545
- }
1546
- break;
1547
-
1548
- case Type::T_FOR:
1549
- list(, $for) = $child;
1550
-
1551
- $start = $this->reduce($for->start, true);
1552
- $start = $start[1];
1553
- $end = $this->reduce($for->end, true);
1554
- $end = $end[1];
1555
- $d = $start < $end ? 1 : -1;
1556
-
1557
- while (true) {
1558
- if ((! $for->until && $start - $d == $end) ||
1559
- ($for->until && $start == $end)
1560
- ) {
1561
- break;
1562
- }
1563
-
1564
- $this->set($for->var, new Node\Number($start, ''));
1565
- $start += $d;
1566
-
1567
- $ret = $this->compileChildren($for->children, $out);
1568
-
1569
- if ($ret) {
1570
- if ($ret[0] !== Type::T_CONTROL) {
1571
- return $ret;
1572
- }
1573
-
1574
- if ($ret[1]) {
1575
- break;
1576
- }
1577
- }
1578
- }
1579
- break;
1580
-
1581
- case Type::T_BREAK:
1582
- return array(Type::T_CONTROL, true);
1583
-
1584
- case Type::T_CONTINUE:
1585
- return array(Type::T_CONTROL, false);
1586
-
1587
- case Type::T_RETURN:
1588
- return $this->reduce($child[1], true);
1589
-
1590
- case Type::T_NESTED_PROPERTY:
1591
- list(, $prop) = $child;
1592
-
1593
- $prefixed = array();
1594
- $prefix = $this->compileValue($prop->prefix) . '-';
1595
-
1596
- foreach ($prop->children as $child) {
1597
- if ($child[0] === Type::T_ASSIGN) {
1598
- array_unshift($child[1][2], $prefix);
1599
- }
1600
-
1601
- if ($child[0] === Type::T_NESTED_PROPERTY) {
1602
- array_unshift($child[1]->prefix[2], $prefix);
1603
- }
1604
-
1605
- $prefixed[] = $child;
1606
- }
1607
-
1608
- $this->compileChildrenNoReturn($prefixed, $out);
1609
- break;
1610
-
1611
- case Type::T_INCLUDE:
1612
- // including a mixin
1613
- list(, $name, $argValues, $content) = $child;
1614
-
1615
- $mixin = $this->get(self::$namespaces['mixin'] . $name, false);
1616
-
1617
- if (! $mixin) {
1618
- $this->throwError("Undefined mixin $name");
1619
- }
1620
-
1621
- $callingScope = $this->getStoreEnv();
1622
-
1623
- // push scope, apply args
1624
- $this->pushEnv();
1625
- $this->env->depth--;
1626
-
1627
- if (isset($content)) {
1628
- $content->scope = $callingScope;
1629
-
1630
- $this->setRaw(self::$namespaces['special'] . 'content', $content, $this->getStoreEnv());
1631
- }
1632
-
1633
- if (isset($mixin->args)) {
1634
- $this->applyArguments($mixin->args, $argValues);
1635
- }
1636
-
1637
- $this->env->marker = 'mixin';
1638
-
1639
- $this->compileChildrenNoReturn($mixin->children, $out);
1640
-
1641
- $this->popEnv();
1642
- break;
1643
-
1644
- case Type::T_MIXIN_CONTENT:
1645
- $content = $this->get(self::$namespaces['special'] . 'content', false, $this->getStoreEnv());
1646
-
1647
- if (! $content) {
1648
- $this->throwError('Expected @content inside of mixin');
1649
- }
1650
-
1651
- if (! isset($content->children)) {
1652
- break;
1653
- }
1654
-
1655
- $storeEnv = $this->storeEnv;
1656
- $this->storeEnv = $content->scope;
1657
-
1658
- $this->compileChildrenNoReturn($content->children, $out);
1659
-
1660
- $this->storeEnv = $storeEnv;
1661
- break;
1662
-
1663
- case Type::T_DEBUG:
1664
- list(, $value) = $child;
1665
-
1666
- $line = $this->parser->getLineNo($this->sourcePos);
1667
- $value = $this->compileValue($this->reduce($value, true));
1668
- fwrite($this->stderr, "Line $line DEBUG: $value\n");
1669
- break;
1670
-
1671
- case Type::T_WARN:
1672
- list(, $value) = $child;
1673
-
1674
- $line = $this->parser->getLineNo($this->sourcePos);
1675
- $value = $this->compileValue($this->reduce($value, true));
1676
- echo "Line $line WARN: $value\n";
1677
- break;
1678
-
1679
- case Type::T_ERROR:
1680
- list(, $value) = $child;
1681
-
1682
- $line = $this->parser->getLineNo($this->sourcePos);
1683
- $value = $this->compileValue($this->reduce($value, true));
1684
- $this->throwError("Line $line ERROR: $value\n");
1685
- break;
1686
-
1687
- case Type::T_CONTROL:
1688
- $this->throwError('@break/@continue not permitted in this scope');
1689
- break;
1690
-
1691
- default:
1692
- $this->throwError("unknown child type: $child[0]");
1693
- }
1694
- }
1695
-
1696
- /**
1697
- * Reduce expression to string
1698
- *
1699
- * @param array $exp
1700
- *
1701
- * @return array
1702
- */
1703
- protected function expToString($exp)
1704
- {
1705
- list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
1706
-
1707
- $content = array($this->reduce($left));
1708
-
1709
- if ($whiteLeft) {
1710
- $content[] = ' ';
1711
- }
1712
-
1713
- $content[] = $op;
1714
-
1715
- if ($whiteRight) {
1716
- $content[] = ' ';
1717
- }
1718
-
1719
- $content[] = $this->reduce($right);
1720
-
1721
- return array(Type::T_STRING, '', $content);
1722
- }
1723
-
1724
- /**
1725
- * Is truthy?
1726
- *
1727
- * @param array $value
1728
- *
1729
- * @return array
1730
- */
1731
- protected function isTruthy($value)
1732
- {
1733
- return $value !== self::$false && $value !== self::$null;
1734
- }
1735
-
1736
- /**
1737
- * Should $value cause its operand to eval
1738
- *
1739
- * @param array $value
1740
- *
1741
- * @return boolean
1742
- */
1743
- protected function shouldEval($value)
1744
- {
1745
- switch ($value[0]) {
1746
- case Type::T_EXPRESSION:
1747
- if ($value[1] === '/') {
1748
- return $this->shouldEval($value[2], $value[3]);
1749
- }
1750
-
1751
- // fall-thru
1752
- case Type::T_VARIABLE:
1753
- case Type::T_FUNCTION_CALL:
1754
- return true;
1755
- }
1756
-
1757
- return false;
1758
- }
1759
-
1760
- /**
1761
- * Reduce value
1762
- *
1763
- * @param array $value
1764
- * @param boolean $inExp
1765
- *
1766
- * @return array
1767
- */
1768
- protected function reduce($value, $inExp = false)
1769
- {
1770
- list($type) = $value;
1771
-
1772
- switch ($type) {
1773
- case Type::T_EXPRESSION:
1774
- list(, $op, $left, $right, $inParens) = $value;
1775
-
1776
- $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
1777
- $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
1778
-
1779
- $left = $this->reduce($left, true);
1780
-
1781
- if ($op !== 'and' && $op !== 'or') {
1782
- $right = $this->reduce($right, true);
1783
- }
1784
-
1785
- // special case: looks like css shorthand
1786
- if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2])
1787
- && (($right[0] !== Type::T_NUMBER && $right[2] != '')
1788
- || ($right[0] === Type::T_NUMBER && ! $right->unitless()))
1789
- ) {
1790
- return $this->expToString($value);
1791
- }
1792
-
1793
- $left = $this->coerceForExpression($left);
1794
- $right = $this->coerceForExpression($right);
1795
-
1796
- $ltype = $left[0];
1797
- $rtype = $right[0];
1798
-
1799
- $ucOpName = ucfirst($opName);
1800
- $ucLType = ucfirst($ltype);
1801
- $ucRType = ucfirst($rtype);
1802
-
1803
- // this tries:
1804
- // 1. op[op name][left type][right type]
1805
- // 2. op[left type][right type] (passing the op as first arg
1806
- // 3. op[op name]
1807
- $fn = "op${ucOpName}${ucLType}${ucRType}";
1808
-
1809
- if (is_callable(array($this, $fn)) ||
1810
- (($fn = "op${ucLType}${ucRType}") &&
1811
- is_callable(array($this, $fn)) &&
1812
- $passOp = true) ||
1813
- (($fn = "op${ucOpName}") &&
1814
- is_callable(array($this, $fn)) &&
1815
- $genOp = true)
1816
- ) {
1817
- $unitChange = false;
1818
-
1819
- if (! isset($genOp) &&
1820
- $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER
1821
- ) {
1822
- if ($opName === 'mod' && ! $right->unitless()) {
1823
- $this->throwError(
1824
- 'Cannot modulo by a number with units: %s%s',
1825
- $right[1],
1826
- $right->unitStr()
1827
- );
1828
- }
1829
-
1830
- $unitChange = true;
1831
- $emptyUnit = $left->unitless() || $right->unitless();
1832
- $targetUnit = $left->unitless() ? $right[2] : $left[2];
1833
-
1834
- if ($opName !== 'mul') {
1835
- $left[2] = $left->unitless() ? $targetUnit : $left[2];
1836
- $right[2] = $right->unitless() ? $targetUnit : $right[2];
1837
- }
1838
-
1839
- if ($opName !== 'mod') {
1840
- $left = $left->normalize();
1841
- $right = $right->normalize();
1842
- }
1843
-
1844
- if ($opName === 'div' && ! $emptyUnit && $left[2] === $right[2]) {
1845
- $targetUnit = '';
1846
- }
1847
-
1848
- if ($opName === 'mul') {
1849
- $left[2] = $left->unitless() ? $right[2] : $left[2];
1850
- $right[2] = $right->unitless() ? $left[2] : $right[2];
1851
- } elseif ($opName === 'div' && $left[2] === $right[2]) {
1852
- $left[2] = '';
1853
- $right[2] = '';
1854
- }
1855
- }
1856
-
1857
- $shouldEval = $inParens || $inExp;
1858
-
1859
- if (isset($passOp)) {
1860
- $out = $this->$fn($op, $left, $right, $shouldEval);
1861
- } else {
1862
- $out = $this->$fn($left, $right, $shouldEval);
1863
- }
1864
-
1865
- if (isset($out)) {
1866
- if ($unitChange && $out[0] === Type::T_NUMBER) {
1867
- $out = $out->coerce($targetUnit);
1868
- }
1869
-
1870
- return $out;
1871
- }
1872
- }
1873
-
1874
- return $this->expToString($value);
1875
-
1876
- case Type::T_UNARY:
1877
- list(, $op, $exp, $inParens) = $value;
1878
-
1879
- $inExp = $inExp || $this->shouldEval($exp);
1880
- $exp = $this->reduce($exp);
1881
-
1882
- if ($exp[0] === Type::T_NUMBER) {
1883
- switch ($op) {
1884
- case '+':
1885
- return new Node\Number($exp[1], $exp[2]);
1886
-
1887
- case '-':
1888
- return new Node\Number(-$exp[1], $exp[2]);
1889
- }
1890
- }
1891
-
1892
- if ($op === 'not') {
1893
- if ($inExp || $inParens) {
1894
- if ($exp === self::$false) {
1895
- return self::$true;
1896
- }
1897
-
1898
- return self::$false;
1899
- }
1900
-
1901
- $op = $op . ' ';
1902
- }
1903
-
1904
- return array(Type::T_STRING, '', array($op, $exp));
1905
-
1906
- case Type::T_VARIABLE:
1907
- list(, $name) = $value;
1908
-
1909
- return $this->reduce($this->get($name));
1910
-
1911
- case Type::T_LIST:
1912
- foreach ($value[2] as &$item) {
1913
- $item = $this->reduce($item);
1914
- }
1915
-
1916
- return $value;
1917
-
1918
- case Type::T_MAP:
1919
- foreach ($value[1] as &$item) {
1920
- $item = $this->reduce($item);
1921
- }
1922
-
1923
- foreach ($value[2] as &$item) {
1924
- $item = $this->reduce($item);
1925
- }
1926
-
1927
- return $value;
1928
-
1929
- case Type::T_STRING:
1930
- foreach ($value[2] as &$item) {
1931
- if (is_array($item) || $item instanceof \ArrayAccess) {
1932
- $item = $this->reduce($item);
1933
- }
1934
- }
1935
-
1936
- return $value;
1937
-
1938
- case Type::T_INTERPOLATE:
1939
- $value[1] = $this->reduce($value[1]);
1940
-
1941
- return $value;
1942
-
1943
- case Type::T_FUNCTION_CALL:
1944
- list(, $name, $argValues) = $value;
1945
-
1946
- return $this->fncall($name, $argValues);
1947
-
1948
- default:
1949
- return $value;
1950
- }
1951
- }
1952
-
1953
- /**
1954
- * Function caller
1955
- *
1956
- * @param string $name
1957
- * @param array $argValues
1958
- *
1959
- * @return array|null
1960
- */
1961
- private function fncall($name, $argValues)
1962
- {
1963
- // SCSS @function
1964
- if ($this->callScssFunction($name, $argValues, $returnValue)) {
1965
- return $returnValue;
1966
- }
1967
-
1968
- // native PHP functions
1969
- if ($this->callNativeFunction($name, $argValues, $returnValue)) {
1970
- return $returnValue;
1971
- }
1972
-
1973
- // for CSS functions, simply flatten the arguments into a list
1974
- $listArgs = array();
1975
-
1976
- foreach ((array) $argValues as $arg) {
1977
- if (empty($arg[0])) {
1978
- $listArgs[] = $this->reduce($arg[1]);
1979
- }
1980
- }
1981
-
1982
- return array(Type::T_FUNCTION, $name, array(Type::T_LIST, ',', $listArgs));
1983
- }
1984
-
1985
- /**
1986
- * Normalize name
1987
- *
1988
- * @param string $name
1989
- *
1990
- * @return string
1991
- */
1992
- protected function normalizeName($name)
1993
- {
1994
- return str_replace('-', '_', $name);
1995
- }
1996
-
1997
- /**
1998
- * Normalize value
1999
- *
2000
- * @param array $value
2001
- *
2002
- * @return array
2003
- */
2004
- public function normalizeValue($value)
2005
- {
2006
- $value = $this->coerceForExpression($this->reduce($value));
2007
- list($type) = $value;
2008
-
2009
- switch ($type) {
2010
- case Type::T_LIST:
2011
- $value = $this->extractInterpolation($value);
2012
-
2013
- if ($value[0] !== Type::T_LIST) {
2014
- return array(Type::T_KEYWORD, $this->compileValue($value));
2015
- }
2016
-
2017
- foreach ($value[2] as $key => $item) {
2018
- $value[2][$key] = $this->normalizeValue($item);
2019
- }
2020
-
2021
- return $value;
2022
-
2023
- case Type::T_STRING:
2024
- return array($type, '"', array($this->compileStringContent($value)));
2025
-
2026
- case Type::T_NUMBER:
2027
- return $value->normalize();
2028
-
2029
- case Type::T_INTERPOLATE:
2030
- return array(Type::T_KEYWORD, $this->compileValue($value));
2031
-
2032
- default:
2033
- return $value;
2034
- }
2035
- }
2036
-
2037
- /**
2038
- * Add numbers
2039
- *
2040
- * @param array $left
2041
- * @param array $right
2042
- *
2043
- * @return array
2044
- */
2045
- protected function opAddNumberNumber($left, $right)
2046
- {
2047
- return new Node\Number($left[1] + $right[1], $left[2]);
2048
- }
2049
-
2050
- /**
2051
- * Multiply numbers
2052
- *
2053
- * @param array $left
2054
- * @param array $right
2055
- *
2056
- * @return array
2057
- */
2058
- protected function opMulNumberNumber($left, $right)
2059
- {
2060
- return new Node\Number($left[1] * $right[1], $left[2]);
2061
- }
2062
-
2063
- /**
2064
- * Subtract numbers
2065
- *
2066
- * @param array $left
2067
- * @param array $right
2068
- *
2069
- * @return array
2070
- */
2071
- protected function opSubNumberNumber($left, $right)
2072
- {
2073
- return new Node\Number($left[1] - $right[1], $left[2]);
2074
- }
2075
-
2076
- /**
2077
- * Divide numbers
2078
- *
2079
- * @param array $left
2080
- * @param array $right
2081
- *
2082
- * @return array
2083
- */
2084
- protected function opDivNumberNumber($left, $right)
2085
- {
2086
- if ($right[1] == 0) {
2087
- return array(Type::T_STRING, '', array($left[1] . $left[2] . '/' . $right[1] . $right[2]));
2088
- }
2089
-
2090
- return new Node\Number($left[1] / $right[1], $left[2]);
2091
- }
2092
-
2093
- /**
2094
- * Mod numbers
2095
- *
2096
- * @param array $left
2097
- * @param array $right
2098
- *
2099
- * @return array
2100
- */
2101
- protected function opModNumberNumber($left, $right)
2102
- {
2103
- return new Node\Number($left[1] % $right[1], $left[2]);
2104
- }
2105
-
2106
- /**
2107
- * Add strings
2108
- *
2109
- * @param array $left
2110
- * @param array $right
2111
- *
2112
- * @return array
2113
- */
2114
- protected function opAdd($left, $right)
2115
- {
2116
- if ($strLeft = $this->coerceString($left)) {
2117
- if ($right[0] === Type::T_STRING) {
2118
- $right[1] = '';
2119
- }
2120
-
2121
- $strLeft[2][] = $right;
2122
-
2123
- return $strLeft;
2124
- }
2125
-
2126
- if ($strRight = $this->coerceString($right)) {
2127
- if ($left[0] === Type::T_STRING) {
2128
- $left[1] = '';
2129
- }
2130
-
2131
- array_unshift($strRight[2], $left);
2132
-
2133
- return $strRight;
2134
- }
2135
- }
2136
-
2137
- /**
2138
- * Boolean and
2139
- *
2140
- * @param array $left
2141
- * @param array $right
2142
- * @param boolean $shouldEval
2143
- *
2144
- * @return array
2145
- */
2146
- protected function opAnd($left, $right, $shouldEval)
2147
- {
2148
- if (! $shouldEval) {
2149
- return;
2150
- }
2151
-
2152
- if ($left !== self::$false) {
2153
- return $this->reduce($right, true);
2154
- }
2155
-
2156
- return $left;
2157
- }
2158
-
2159
- /**
2160
- * Boolean or
2161
- *
2162
- * @param array $left
2163
- * @param array $right
2164
- * @param boolean $shouldEval
2165
- *
2166
- * @return array
2167
- */
2168
- protected function opOr($left, $right, $shouldEval)
2169
- {
2170
- if (! $shouldEval) {
2171
- return;
2172
- }
2173
-
2174
- if ($left !== self::$false) {
2175
- return $left;
2176
- }
2177
-
2178
- return $this->reduce($right, true);
2179
- }
2180
-
2181
- /**
2182
- * Compare colors
2183
- *
2184
- * @param string $op
2185
- * @param array $left
2186
- * @param array $right
2187
- *
2188
- * @return array
2189
- */
2190
- protected function opColorColor($op, $left, $right)
2191
- {
2192
- $out = array(Type::T_COLOR);
2193
-
2194
- foreach (array(1, 2, 3) as $i) {
2195
- $lval = isset($left[$i]) ? $left[$i] : 0;
2196
- $rval = isset($right[$i]) ? $right[$i] : 0;
2197
-
2198
- switch ($op) {
2199
- case '+':
2200
- $out[] = $lval + $rval;
2201
- break;
2202
-
2203
- case '-':
2204
- $out[] = $lval - $rval;
2205
- break;
2206
-
2207
- case '*':
2208
- $out[] = $lval * $rval;
2209
- break;
2210
-
2211
- case '%':
2212
- $out[] = $lval % $rval;
2213
- break;
2214
-
2215
- case '/':
2216
- if ($rval == 0) {
2217
- $this->throwError("color: Can't divide by zero");
2218
- }
2219
-
2220
- $out[] = (int) ($lval / $rval);
2221
- break;
2222
-
2223
- case '==':
2224
- return $this->opEq($left, $right);
2225
-
2226
- case '!=':
2227
- return $this->opNeq($left, $right);
2228
-
2229
- default:
2230
- $this->throwError("color: unknown op $op");
2231
- }
2232
- }
2233
-
2234
- if (isset($left[4])) {
2235
- $out[4] = $left[4];
2236
- } elseif (isset($right[4])) {
2237
- $out[4] = $right[4];
2238
- }
2239
-
2240
- return $this->fixColor($out);
2241
- }
2242
-
2243
- /**
2244
- * Compare color and number
2245
- *
2246
- * @param string $op
2247
- * @param array $left
2248
- * @param array $right
2249
- *
2250
- * @return array
2251
- */
2252
- protected function opColorNumber($op, $left, $right)
2253
- {
2254
- $value = $right[1];
2255
-
2256
- return $this->opColorColor(
2257
- $op,
2258
- $left,
2259
- array(Type::T_COLOR, $value, $value, $value)
2260
- );
2261
- }
2262
-
2263
- /**
2264
- * Compare number and color
2265
- *
2266
- * @param string $op
2267
- * @param array $left
2268
- * @param array $right
2269
- *
2270
- * @return array
2271
- */
2272
- protected function opNumberColor($op, $left, $right)
2273
- {
2274
- $value = $left[1];
2275
-
2276
- return $this->opColorColor(
2277
- $op,
2278
- array(Type::T_COLOR, $value, $value, $value),
2279
- $right
2280
- );
2281
- }
2282
-
2283
- /**
2284
- * Compare number1 == number2
2285
- *
2286
- * @param array $left
2287
- * @param array $right
2288
- *
2289
- * @return array
2290
- */
2291
- protected function opEq($left, $right)
2292
- {
2293
- if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
2294
- $lStr[1] = '';
2295
- $rStr[1] = '';
2296
-
2297
- $left = $this->compileValue($lStr);
2298
- $right = $this->compileValue($rStr);
2299
- }
2300
-
2301
- return $this->toBool($left === $right);
2302
- }
2303
-
2304
- /**
2305
- * Compare number1 != number2
2306
- *
2307
- * @param array $left
2308
- * @param array $right
2309
- *
2310
- * @return array
2311
- */
2312
- protected function opNeq($left, $right)
2313
- {
2314
- if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
2315
- $lStr[1] = '';
2316
- $rStr[1] = '';
2317
-
2318
- $left = $this->compileValue($lStr);
2319
- $right = $this->compileValue($rStr);
2320
- }
2321
-
2322
- return $this->toBool($left !== $right);
2323
- }
2324
-
2325
- /**
2326
- * Compare number1 >= number2
2327
- *
2328
- * @param array $left
2329
- * @param array $right
2330
- *
2331
- * @return array
2332
- */
2333
- protected function opGteNumberNumber($left, $right)
2334
- {
2335
- return $this->toBool($left[1] >= $right[1]);
2336
- }
2337
-
2338
- /**
2339
- * Compare number1 > number2
2340
- *
2341
- * @param array $left
2342
- * @param array $right
2343
- *
2344
- * @return array
2345
- */
2346
- protected function opGtNumberNumber($left, $right)
2347
- {
2348
- return $this->toBool($left[1] > $right[1]);
2349
- }
2350
-
2351
- /**
2352
- * Compare number1 <= number2
2353
- *
2354
- * @param array $left
2355
- * @param array $right
2356
- *
2357
- * @return array
2358
- */
2359
- protected function opLteNumberNumber($left, $right)
2360
- {
2361
- return $this->toBool($left[1] <= $right[1]);
2362
- }
2363
-
2364
- /**
2365
- * Compare number1 < number2
2366
- *
2367
- * @param array $left
2368
- * @param array $right
2369
- *
2370
- * @return array
2371
- */
2372
- protected function opLtNumberNumber($left, $right)
2373
- {
2374
- return $this->toBool($left[1] < $right[1]);
2375
- }
2376
-
2377
- /**
2378
- * Three-way comparison, aka spaceship operator
2379
- *
2380
- * @param array $left
2381
- * @param array $right
2382
- *
2383
- * @return array
2384
- */
2385
- protected function opCmpNumberNumber($left, $right)
2386
- {
2387
- $n = $left[1] - $right[1];
2388
-
2389
- return new Node\Number($n ? $n / abs($n) : 0, '');
2390
- }
2391
-
2392
- /**
2393
- * Cast to boolean
2394
- *
2395
- * @api
2396
- *
2397
- * @param mixed $thing
2398
- *
2399
- * @return array
2400
- */
2401
- public function toBool($thing)
2402
- {
2403
- return $thing ? self::$true : self::$false;
2404
- }
2405
-
2406
- /**
2407
- * Compiles a primitive value into a CSS property value.
2408
- *
2409
- * Values in scssphp are typed by being wrapped in arrays, their format is
2410
- * typically:
2411
- *
2412
- * array(type, contents [, additional_contents]*)
2413
- *
2414
- * The input is expected to be reduced. This function will not work on
2415
- * things like expressions and variables.
2416
- *
2417
- * @api
2418
- *
2419
- * @param array $value
2420
- *
2421
- * @return string
2422
- */
2423
- public function compileValue($value)
2424
- {
2425
- $value = $this->reduce($value);
2426
-
2427
- list($type) = $value;
2428
-
2429
- switch ($type) {
2430
- case Type::T_KEYWORD:
2431
- return $value[1];
2432
-
2433
- case Type::T_COLOR:
2434
- // [1] - red component (either number for a %)
2435
- // [2] - green component
2436
- // [3] - blue component
2437
- // [4] - optional alpha component
2438
- list(, $r, $g, $b) = $value;
2439
-
2440
- $r = round($r);
2441
- $g = round($g);
2442
- $b = round($b);
2443
-
2444
- if (count($value) === 5 && $value[4] !== 1) { // rgba
2445
- return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $value[4] . ')';
2446
- }
2447
-
2448
- $h = sprintf('#%02x%02x%02x', $r, $g, $b);
2449
-
2450
- // Converting hex color to short notation (e.g. #003399 to #039)
2451
- if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
2452
- $h = '#' . $h[1] . $h[3] . $h[5];
2453
- }
2454
-
2455
- return $h;
2456
-
2457
- case Type::T_NUMBER:
2458
- return (string) $value;
2459
-
2460
- case Type::T_STRING:
2461
- return $value[1] . $this->compileStringContent($value) . $value[1];
2462
-
2463
- case Type::T_FUNCTION:
2464
- $args = ! empty($value[2]) ? $this->compileValue($value[2]) : '';
2465
-
2466
- return "$value[1]($args)";
2467
-
2468
- case Type::T_LIST:
2469
- $value = $this->extractInterpolation($value);
2470
-
2471
- if ($value[0] !== Type::T_LIST) {
2472
- return $this->compileValue($value);
2473
- }
2474
-
2475
- list(, $delim, $items) = $value;
2476
-
2477
- if ($delim !== ' ') {
2478
- $delim .= ' ';
2479
- }
2480
-
2481
- $filtered = array();
2482
-
2483
- foreach ($items as $item) {
2484
- if ($item[0] === Type::T_NULL) {
2485
- continue;
2486
- }
2487
-
2488
- $filtered[] = $this->compileValue($item);
2489
- }
2490
-
2491
- return implode("$delim", $filtered);
2492
-
2493
- case Type::T_MAP:
2494
- $keys = $value[1];
2495
- $values = $value[2];
2496
- $filtered = array();
2497
-
2498
- for ($i = 0, $s = count($keys); $i < $s; $i++) {
2499
- $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
2500
- }
2501
-
2502
- array_walk($filtered, function (&$value, $key) {
2503
- $value = $key . ': ' . $value;
2504
- });
2505
-
2506
- return '(' . implode(', ', $filtered) . ')';
2507
-
2508
- case Type::T_INTERPOLATED:
2509
- // node created by extractInterpolation
2510
- list(, $interpolate, $left, $right) = $value;
2511
- list(,, $whiteLeft, $whiteRight) = $interpolate;
2512
-
2513
- $left = count($left[2]) > 0 ?
2514
- $this->compileValue($left) . $whiteLeft : '';
2515
-
2516
- $right = count($right[2]) > 0 ?
2517
- $whiteRight . $this->compileValue($right) : '';
2518
-
2519
- return $left . $this->compileValue($interpolate) . $right;
2520
-
2521
- case Type::T_INTERPOLATE:
2522
- // raw parse node
2523
- list(, $exp) = $value;
2524
-
2525
- // strip quotes if it's a string
2526
- $reduced = $this->reduce($exp);
2527
-
2528
- switch ($reduced[0]) {
2529
- case Type::T_STRING:
2530
- $reduced = array(Type::T_KEYWORD, $this->compileStringContent($reduced));
2531
- break;
2532
-
2533
- case Type::T_NULL:
2534
- $reduced = array(Type::T_KEYWORD, '');
2535
- }
2536
-
2537
- return $this->compileValue($reduced);
2538
-
2539
- case Type::T_NULL:
2540
- return 'null';
2541
-
2542
- default:
2543
- $this->throwError("unknown value type: $type");
2544
- }
2545
- }
2546
-
2547
- /**
2548
- * Flatten list
2549
- *
2550
- * @param array $list
2551
- *
2552
- * @return string
2553
- */
2554
- protected function flattenList($list)
2555
- {
2556
- return $this->compileValue($list);
2557
- }
2558
-
2559
- /**
2560
- * Compile string content
2561
- *
2562
- * @param array $string
2563
- *
2564
- * @return string
2565
- */
2566
- protected function compileStringContent($string)
2567
- {
2568
- $parts = array();
2569
-
2570
- foreach ($string[2] as $part) {
2571
- if (is_array($part) || $part instanceof \ArrayAccess) {
2572
- $parts[] = $this->compileValue($part);
2573
- } else {
2574
- $parts[] = $part;
2575
- }
2576
- }
2577
-
2578
- return implode($parts);
2579
- }
2580
-
2581
- /**
2582
- * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
2583
- *
2584
- * @param array $list
2585
- *
2586
- * @return array
2587
- */
2588
- protected function extractInterpolation($list)
2589
- {
2590
- $items = $list[2];
2591
-
2592
- foreach ($items as $i => $item) {
2593
- if ($item[0] === Type::T_INTERPOLATE) {
2594
- $before = array(Type::T_LIST, $list[1], array_slice($items, 0, $i));
2595
- $after = array(Type::T_LIST, $list[1], array_slice($items, $i + 1));
2596
-
2597
- return array(Type::T_INTERPOLATED, $item, $before, $after);
2598
- }
2599
- }
2600
-
2601
- return $list;
2602
- }
2603
-
2604
- /**
2605
- * Find the final set of selectors
2606
- *
2607
- * @param \Leafo\ScssPhp\Compiler\Environment $env
2608
- *
2609
- * @return array
2610
- */
2611
- protected function multiplySelectors(Environment $env)
2612
- {
2613
- $envs = $this->compactEnv($env);
2614
- $selectors = array();
2615
- $parentSelectors = array(array());
2616
-
2617
- while ($env = array_pop($envs)) {
2618
- if (empty($env->selectors)) {
2619
- continue;
2620
- }
2621
-
2622
- $selectors = array();
2623
-
2624
- foreach ($env->selectors as $selector) {
2625
- foreach ($parentSelectors as $parent) {
2626
- $selectors[] = $this->joinSelectors($parent, $selector);
2627
- }
2628
- }
2629
-
2630
- $parentSelectors = $selectors;
2631
- }
2632
-
2633
- return $selectors;
2634
- }
2635
-
2636
- /**
2637
- * Join selectors; looks for & to replace, or append parent before child
2638
- *
2639
- * @param array $parent
2640
- * @param array $child
2641
- *
2642
- * @return array
2643
- */
2644
- protected function joinSelectors($parent, $child)
2645
- {
2646
- $setSelf = false;
2647
- $out = array();
2648
-
2649
- foreach ($child as $part) {
2650
- $newPart = array();
2651
-
2652
- foreach ($part as $p) {
2653
- if ($p === self::$selfSelector) {
2654
- $setSelf = true;
2655
-
2656
- foreach ($parent as $i => $parentPart) {
2657
- if ($i > 0) {
2658
- $out[] = $newPart;
2659
- $newPart = array();
2660
- }
2661
-
2662
- foreach ($parentPart as $pp) {
2663
- $newPart[] = $pp;
2664
- }
2665
- }
2666
- } else {
2667
- $newPart[] = $p;
2668
- }
2669
- }
2670
-
2671
- $out[] = $newPart;
2672
- }
2673
-
2674
- return $setSelf ? $out : array_merge($parent, $child);
2675
- }
2676
-
2677
- /**
2678
- * Multiply media
2679
- *
2680
- * @param \Leafo\ScssPhp\Compiler\Environment $env
2681
- * @param array $childQueries
2682
- *
2683
- * @return array
2684
- */
2685
- protected function multiplyMedia(Environment $env = null, $childQueries = null)
2686
- {
2687
- if (! isset($env) ||
2688
- ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA
2689
- ) {
2690
- return $childQueries;
2691
- }
2692
-
2693
- // plain old block, skip
2694
- if (empty($env->block->type)) {
2695
- return $this->multiplyMedia($env->parent, $childQueries);
2696
- }
2697
-
2698
- $parentQueries = isset($env->block->queryList)
2699
- ? $env->block->queryList
2700
- : array(array(array(Type::T_MEDIA_VALUE, $env->block->value)));
2701
-
2702
- if ($childQueries === null) {
2703
- $childQueries = $parentQueries;
2704
- } else {
2705
- $originalQueries = $childQueries;
2706
- $childQueries = array();
2707
-
2708
- foreach ($parentQueries as $parentQuery) {
2709
- foreach ($originalQueries as $childQuery) {
2710
- $childQueries []= array_merge($parentQuery, $childQuery);
2711
- }
2712
- }
2713
- }
2714
-
2715
- return $this->multiplyMedia($env->parent, $childQueries);
2716
- }
2717
-
2718
- /**
2719
- * Convert env linked list to stack
2720
- *
2721
- * @param \Leafo\ScssPhp\Compiler\Environment $env
2722
- *
2723
- * @return array
2724
- */
2725
- private function compactEnv(Environment $env)
2726
- {
2727
- for ($envs = array(); $env; $env = $env->parent) {
2728
- $envs[] = $env;
2729
- }
2730
-
2731
- return $envs;
2732
- }
2733
-
2734
- /**
2735
- * Convert env stack to singly linked list
2736
- *
2737
- * @param array $envs
2738
- *
2739
- * @return \Leafo\ScssPhp\Compiler\Environment
2740
- */
2741
- private function extractEnv($envs)
2742
- {
2743
- for ($env = null; $e = array_pop($envs);) {
2744
- $e->parent = $env;
2745
- $env = $e;
2746
- }
2747
-
2748
- return $env;
2749
- }
2750
-
2751
- /**
2752
- * Push environment
2753
- *
2754
- * @param \Leafo\ScssPhp\Block $block
2755
- *
2756
- * @return \Leafo\ScssPhp\Compiler\Environment
2757
- */
2758
- protected function pushEnv(Block $block = null)
2759
- {
2760
- $env = new Environment;
2761
- $env->parent = $this->env;
2762
- $env->store = array();
2763
- $env->block = $block;
2764
- $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
2765
-
2766
- $this->env = $env;
2767
-
2768
- return $env;
2769
- }
2770
-
2771
- /**
2772
- * Pop environment
2773
- */
2774
- protected function popEnv()
2775
- {
2776
- $this->env = $this->env->parent;
2777
- }
2778
-
2779
- /**
2780
- * Get store environment
2781
- *
2782
- * @return \Leafo\ScssPhp\Compiler\Environment
2783
- */
2784
- protected function getStoreEnv()
2785
- {
2786
- return isset($this->storeEnv) ? $this->storeEnv : $this->env;
2787
- }
2788
-
2789
- /**
2790
- * Set variable
2791
- *
2792
- * @param string $name
2793
- * @param mixed $value
2794
- * @param boolean $shadow
2795
- * @param \Leafo\ScssPhp\Compiler\Environment $env
2796
- */
2797
- protected function set($name, $value, $shadow = false, Environment $env = null)
2798
- {
2799
- $name = $this->normalizeName($name);
2800
-
2801
- if (! isset($env)) {
2802
- $env = $this->getStoreEnv();
2803
- }
2804
-
2805
- if ($shadow) {
2806
- $this->setRaw($name, $value, $env);
2807
- } else {
2808
- $this->setExisting($name, $value, $env);
2809
- }
2810
- }
2811
-
2812
- /**
2813
- * Set existing variable
2814
- *
2815
- * @param string $name
2816
- * @param mixed $value
2817
- * @param \Leafo\ScssPhp\Compiler\Environment $env
2818
- */
2819
- protected function setExisting($name, $value, Environment $env)
2820
- {
2821
- $storeEnv = $env;
2822
-
2823
- $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
2824
-
2825
- for (;;) {
2826
- if (array_key_exists($name, $env->store)) {
2827
- break;
2828
- }
2829
-
2830
- if (! $hasNamespace && isset($env->marker)) {
2831
- $env = $storeEnv;
2832
- break;
2833
- }
2834
-
2835
- if (! isset($env->parent)) {
2836
- $env = $storeEnv;
2837
- break;
2838
- }
2839
-
2840
- $env = $env->parent;
2841
- }
2842
-
2843
- $env->store[$name] = $value;
2844
- }
2845
-
2846
- /**
2847
- * Set raw variable
2848
- *
2849
- * @param string $name
2850
- * @param mixed $value
2851
- * @param \Leafo\ScssPhp\Compiler\Environment $env
2852
- */
2853
- protected function setRaw($name, $value, Environment $env)
2854
- {
2855
- $env->store[$name] = $value;
2856
- }
2857
-
2858
- /**
2859
- * Get variable
2860
- *
2861
- * @api
2862
- *
2863
- * @param string $name
2864
- * @param boolean $shouldThrow
2865
- * @param \Leafo\ScssPhp\Compiler\Environment $env
2866
- *
2867
- * @return mixed
2868
- */
2869
- public function get($name, $shouldThrow = true, Environment $env = null)
2870
- {
2871
- $name = $this->normalizeName($name);
2872
-
2873
- if (! isset($env)) {
2874
- $env = $this->getStoreEnv();
2875
- }
2876
-
2877
- $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
2878
-
2879
- for (;;) {
2880
- if (array_key_exists($name, $env->store)) {
2881
- return $env->store[$name];
2882
- }
2883
-
2884
- if (! $hasNamespace && isset($env->marker)) {
2885
- $env = $this->rootEnv;
2886
- continue;
2887
- }
2888
-
2889
- if (! isset($env->parent)) {
2890
- break;
2891
- }
2892
-
2893
- $env = $env->parent;
2894
- }
2895
-
2896
- if ($shouldThrow) {
2897
- $this->throwError("Undefined variable \$$name");
2898
- }
2899
-
2900
- // found nothing
2901
- }
2902
-
2903
- /**
2904
- * Has variable?
2905
- *
2906
- * @param string $name
2907
- * @param \Leafo\ScssPhp\Compiler\Environment $env
2908
- *
2909
- * @return boolean
2910
- */
2911
- protected function has($name, Environment $env = null)
2912
- {
2913
- return $this->get($name, false, $env) !== null;
2914
- }
2915
-
2916
- /**
2917
- * Inject variables
2918
- *
2919
- * @param array $args
2920
- */
2921
- protected function injectVariables(array $args)
2922
- {
2923
- if (empty($args)) {
2924
- return;
2925
- }
2926
-
2927
- $parser = $this->parserFactory(__METHOD__);
2928
-
2929
- foreach ($args as $name => $strValue) {
2930
- if ($name[0] === '$') {
2931
- $name = substr($name, 1);
2932
- }
2933
-
2934
- if (! $parser->parseValue($strValue, $value)) {
2935
- $value = $this->coerceValue($strValue);
2936
- }
2937
-
2938
- $this->set($name, $value);
2939
- }
2940
- }
2941
-
2942
- /**
2943
- * Set variables
2944
- *
2945
- * @api
2946
- *
2947
- * @param array $variables
2948
- */
2949
- public function setVariables(array $variables)
2950
- {
2951
- $this->registeredVars = array_merge($this->registeredVars, $variables);
2952
- }
2953
-
2954
- /**
2955
- * Unset variable
2956
- *
2957
- * @api
2958
- *
2959
- * @param string $name
2960
- */
2961
- public function unsetVariable($name)
2962
- {
2963
- unset($this->registeredVars[$name]);
2964
- }
2965
-
2966
- /**
2967
- * Returns list of variables
2968
- *
2969
- * @api
2970
- *
2971
- * @return array
2972
- */
2973
- public function getVariables()
2974
- {
2975
- return $this->registeredVars;
2976
- }
2977
-
2978
- /**
2979
- * Adds to list of parsed files
2980
- *
2981
- * @api
2982
- *
2983
- * @param string $path
2984
- */
2985
- public function addParsedFile($path)
2986
- {
2987
- if (isset($path) && file_exists($path)) {
2988
- $this->parsedFiles[realpath($path)] = filemtime($path);
2989
- }
2990
- }
2991
-
2992
- /**
2993
- * Returns list of parsed files
2994
- *
2995
- * @api
2996
- *
2997
- * @return array
2998
- */
2999
- public function getParsedFiles()
3000
- {
3001
- return $this->parsedFiles;
3002
- }
3003
-
3004
- /**
3005
- * Add import path
3006
- *
3007
- * @api
3008
- *
3009
- * @param string $path
3010
- */
3011
- public function addImportPath($path)
3012
- {
3013
- if (! in_array($path, $this->importPaths)) {
3014
- $this->importPaths[] = $path;
3015
- }
3016
- }
3017
-
3018
- /**
3019
- * Set import paths
3020
- *
3021
- * @api
3022
- *
3023
- * @param string|array $path
3024
- */
3025
- public function setImportPaths($path)
3026
- {
3027
- $this->importPaths = (array) $path;
3028
- }
3029
-
3030
- /**
3031
- * Set number precision
3032
- *
3033
- * @api
3034
- *
3035
- * @param integer $numberPrecision
3036
- */
3037
- public function setNumberPrecision($numberPrecision)
3038
- {
3039
- Node\Number::$precision = $numberPrecision;
3040
- }
3041
-
3042
- /**
3043
- * Set formatter
3044
- *
3045
- * @api
3046
- *
3047
- * @param string $formatterName
3048
- */
3049
- public function setFormatter($formatterName)
3050
- {
3051
- $this->formatter = $formatterName;
3052
- }
3053
-
3054
- /**
3055
- * Set line number style
3056
- *
3057
- * @api
3058
- *
3059
- * @param string $lineNumberStyle
3060
- */
3061
- public function setLineNumberStyle($lineNumberStyle)
3062
- {
3063
- $this->lineNumberStyle = $lineNumberStyle;
3064
- }
3065
-
3066
- /**
3067
- * Register function
3068
- *
3069
- * @api
3070
- *
3071
- * @param string $name
3072
- * @param callable $func
3073
- * @param array $prototype
3074
- */
3075
- public function registerFunction($name, $func, $prototype = null)
3076
- {
3077
- $this->userFunctions[$this->normalizeName($name)] = array($func, $prototype);
3078
- }
3079
-
3080
- /**
3081
- * Unregister function
3082
- *
3083
- * @api
3084
- *
3085
- * @param string $name
3086
- */
3087
- public function unregisterFunction($name)
3088
- {
3089
- unset($this->userFunctions[$this->normalizeName($name)]);
3090
- }
3091
-
3092
- /**
3093
- * Add feature
3094
- *
3095
- * @api
3096
- *
3097
- * @param string $name
3098
- */
3099
- public function addFeature($name)
3100
- {
3101
- $this->registeredFeatures[$name] = true;
3102
- }
3103
-
3104
- /**
3105
- * Import file
3106
- *
3107
- * @param string $path
3108
- * @param array $out
3109
- */
3110
- protected function importFile($path, $out)
3111
- {
3112
- // see if tree is cached
3113
- $realPath = realpath($path);
3114
-
3115
- if (isset($this->importCache[$realPath])) {
3116
- $this->handleImportLoop($realPath);
3117
-
3118
- $tree = $this->importCache[$realPath];
3119
- } else {
3120
- $code = file_get_contents($path);
3121
- $parser = $this->parserFactory($path);
3122
- $tree = $parser->parse($code);
3123
-
3124
- $this->importCache[$realPath] = $tree;
3125
- }
3126
-
3127
- $pi = pathinfo($path);
3128
- array_unshift($this->importPaths, $pi['dirname']);
3129
- $this->compileChildrenNoReturn($tree->children, $out);
3130
- array_shift($this->importPaths);
3131
- }
3132
-
3133
- /**
3134
- * Return the file path for an import url if it exists
3135
- *
3136
- * @api
3137
- *
3138
- * @param string $url
3139
- *
3140
- * @return string|null
3141
- */
3142
- public function findImport($url)
3143
- {
3144
- $urls = array();
3145
-
3146
- // for "normal" scss imports (ignore vanilla css and external requests)
3147
- if (! preg_match('/\.css$|^https?:\/\//', $url)) {
3148
- // try both normal and the _partial filename
3149
- $urls = array($url, preg_replace('/[^\/]+$/', '_\0', $url));
3150
- }
3151
-
3152
- foreach ($this->importPaths as $dir) {
3153
- if (is_string($dir)) {
3154
- // check urls for normal import paths
3155
- foreach ($urls as $full) {
3156
- $full = $dir
3157
- . (! empty($dir) && substr($dir, -1) !== '/' ? '/' : '')
3158
- . $full;
3159
-
3160
- if ($this->fileExists($file = $full . '.scss') ||
3161
- $this->fileExists($file = $full)
3162
- ) {
3163
- return $file;
3164
- }
3165
- }
3166
- } elseif (is_callable($dir)) {
3167
- // check custom callback for import path
3168
- $file = call_user_func($dir, $url);
3169
-
3170
- if ($file !== null) {
3171
- return $file;
3172
- }
3173
- }
3174
- }
3175
-
3176
- return null;
3177
- }
3178
-
3179
- /**
3180
- * Throw error (exception)
3181
- *
3182
- * @api
3183
- *
3184
- * @param string $msg Message with optional sprintf()-style vararg parameters
3185
- *
3186
- * @throws \Exception
3187
- */
3188
- public function throwError($msg)
3189
- {
3190
- if (func_num_args() > 1) {
3191
- $msg = call_user_func_array('sprintf', func_get_args());
3192
- }
3193
-
3194
- if ($this->sourcePos >= 0 && isset($this->sourceIndex)) {
3195
- $parser = $this->sourceParsers[$this->sourceIndex];
3196
- $parser->throwParseError($msg, $this->sourcePos);
3197
- }
3198
-
3199
- throw new \Exception($msg);
3200
- }
3201
-
3202
- /**
3203
- * Handle import loop
3204
- *
3205
- * @param string $name
3206
- *
3207
- * @throws \Exception
3208
- */
3209
- private function handleImportLoop($name)
3210
- {
3211
- for ($env = $this->env; $env; $env = $env->parent) {
3212
- $parser = $this->sourceParsers[$env->block->sourceIndex];
3213
- $file = $parser->getSourceName();
3214
-
3215
- if (realpath($file) === $name) {
3216
- $this->throwError('An @import loop has been found: %s imports %s', $file, basename($file));
3217
- }
3218
- }
3219
- }
3220
-
3221
- /**
3222
- * Does file exist?
3223
- *
3224
- * @param string $name
3225
- *
3226
- * @return boolean
3227
- */
3228
- protected function fileExists($name)
3229
- {
3230
- return is_file($name);
3231
- }
3232
-
3233
- /**
3234
- * Call SCSS @function
3235
- *
3236
- * @param string $name
3237
- * @param array $args
3238
- * @param array $returnValue
3239
- *
3240
- * @return boolean Returns true if returnValue is set; otherwise, false
3241
- */
3242
- protected function callScssFunction($name, $argValues, &$returnValue)
3243
- {
3244
- $func = $this->get(self::$namespaces['function'] . $name, false);
3245
-
3246
- if (! $func) {
3247
- return false;
3248
- }
3249
-
3250
- $this->pushEnv();
3251
-
3252
- // set the args
3253
- if (isset($func->args)) {
3254
- $this->applyArguments($func->args, $argValues);
3255
- }
3256
-
3257
- // throw away lines and children
3258
- $tmp = new OutputBlock;
3259
- $tmp->lines = array();
3260
- $tmp->children = array();
3261
-
3262
- $this->env->marker = 'function';
3263
-
3264
- $ret = $this->compileChildren($func->children, $tmp);
3265
-
3266
- $this->popEnv();
3267
-
3268
- $returnValue = ! isset($ret) ? self::$defaultValue : $ret;
3269
-
3270
- return true;
3271
- }
3272
-
3273
- /**
3274
- * Call built-in and registered (PHP) functions
3275
- *
3276
- * @param string $name
3277
- * @param array $args
3278
- * @param array $returnValue
3279
- *
3280
- * @return boolean Returns true if returnValue is set; otherwise, false
3281
- */
3282
- protected function callNativeFunction($name, $args, &$returnValue)
3283
- {
3284
- // try a lib function
3285
- $name = $this->normalizeName($name);
3286
-
3287
- if (isset($this->userFunctions[$name])) {
3288
- // see if we can find a user function
3289
- list($f, $prototype) = $this->userFunctions[$name];
3290
- } elseif (($f = $this->getBuiltinFunction($name)) && is_callable($f)) {
3291
- $libName = $f[1];
3292
- $prototype = isset(self::$$libName) ? self::$$libName : null;
3293
- } else {
3294
- return false;
3295
- }
3296
-
3297
- list($sorted, $kwargs) = $this->sortArgs($prototype, $args);
3298
-
3299
- if ($name !== 'if' && $name !== 'call') {
3300
- foreach ($sorted as &$val) {
3301
- $val = $this->reduce($val, true);
3302
- }
3303
- }
3304
-
3305
- $returnValue = call_user_func($f, $sorted, $kwargs);
3306
-
3307
- if (! isset($returnValue)) {
3308
- return false;
3309
- }
3310
-
3311
- $returnValue = $this->coerceValue($returnValue);
3312
-
3313
- return true;
3314
- }
3315
-
3316
- /**
3317
- * Get built-in function
3318
- *
3319
- * @param string $name Normalized name
3320
- *
3321
- * @return array
3322
- */
3323
- protected function getBuiltinFunction($name)
3324
- {
3325
- $libName = 'lib' . preg_replace_callback(
3326
- '/_(.)/',
3327
- function ($m) {
3328
- return ucfirst($m[1]);
3329
- },
3330
- ucfirst($name)
3331
- );
3332
-
3333
- return array($this, $libName);
3334
- }
3335
-
3336
- /**
3337
- * Sorts keyword arguments
3338
- *
3339
- * @todo Merge with applyArguments()?
3340
- *
3341
- * @param array $prototype
3342
- * @param array $args
3343
- *
3344
- * @return array
3345
- */
3346
- protected function sortArgs($prototype, $args)
3347
- {
3348
- $keyArgs = array();
3349
- $posArgs = array();
3350
-
3351
- // separate positional and keyword arguments
3352
- foreach ($args as $arg) {
3353
- list($key, $value) = $arg;
3354
-
3355
- $key = $key[1];
3356
-
3357
- if (empty($key)) {
3358
- $posArgs[] = $value;
3359
- } else {
3360
- $keyArgs[$key] = $value;
3361
- }
3362
- }
3363
-
3364
- if (! isset($prototype)) {
3365
- return array($posArgs, $keyArgs);
3366
- }
3367
-
3368
- // copy positional args
3369
- $finalArgs = array_pad($posArgs, count($prototype), null);
3370
-
3371
- // overwrite positional args with keyword args
3372
- foreach ($prototype as $i => $names) {
3373
- foreach ((array) $names as $name) {
3374
- if (isset($keyArgs[$name])) {
3375
- $finalArgs[$i] = $keyArgs[$name];
3376
- }
3377
- }
3378
- }
3379
-
3380
- return array($finalArgs, $keyArgs);
3381
- }
3382
-
3383
- /**
3384
- * Apply argument values per definition
3385
- *
3386
- * @param array $argDef
3387
- * @param array $argValues
3388
- *
3389
- * @throws \Exception
3390
- */
3391
- protected function applyArguments($argDef, $argValues)
3392
- {
3393
- $storeEnv = $this->getStoreEnv();
3394
-
3395
- $env = new Environment;
3396
- $env->store = $storeEnv->store;
3397
-
3398
- $hasVariable = false;
3399
- $args = array();
3400
-
3401
- foreach ($argDef as $i => $arg) {
3402
- list($name, $default, $isVariable) = $argDef[$i];
3403
-
3404
- $args[$name] = array($i, $name, $default, $isVariable);
3405
- $hasVariable |= $isVariable;
3406
- }
3407
-
3408
- $keywordArgs = array();
3409
- $deferredKeywordArgs = array();
3410
- $remaining = array();
3411
-
3412
- // assign the keyword args
3413
- foreach ((array) $argValues as $arg) {
3414
- if (! empty($arg[0])) {
3415
- if (! isset($args[$arg[0][1]])) {
3416
- if ($hasVariable) {
3417
- $deferredKeywordArgs[$arg[0][1]] = $arg[1];
3418
- } else {
3419
- $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
3420
- }
3421
- } elseif ($args[$arg[0][1]][0] < count($remaining)) {
3422
- $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
3423
- } else {
3424
- $keywordArgs[$arg[0][1]] = $arg[1];
3425
- }
3426
- } elseif (count($keywordArgs)) {
3427
- $this->throwError('Positional arguments must come before keyword arguments.');
3428
- } elseif ($arg[2] === true) {
3429
- $val = $this->reduce($arg[1], true);
3430
-
3431
- if ($val[0] === Type::T_LIST) {
3432
- foreach ($val[2] as $name => $item) {
3433
- if (! is_numeric($name)) {
3434
- $keywordArgs[$name] = $item;
3435
- } else {
3436
- $remaining[] = $item;
3437
- }
3438
- }
3439
- } elseif ($val[0] === Type::T_MAP) {
3440
- foreach ($val[1] as $i => $name) {
3441
- $name = $this->compileStringContent($this->coerceString($name));
3442
- $item = $val[2][$i];
3443
-
3444
- if (! is_numeric($name)) {
3445
- $keywordArgs[$name] = $item;
3446
- } else {
3447
- $remaining[] = $item;
3448
- }
3449
- }
3450
- } else {
3451
- $remaining[] = $val;
3452
- }
3453
- } else {
3454
- $remaining[] = $arg[1];
3455
- }
3456
- }
3457
-
3458
- foreach ($args as $arg) {
3459
- list($i, $name, $default, $isVariable) = $arg;
3460
-
3461
- if ($isVariable) {
3462
- $val = array(Type::T_LIST, ',', array(), $isVariable);
3463
-
3464
- for ($count = count($remaining); $i < $count; $i++) {
3465
- $val[2][] = $remaining[$i];
3466
- }
3467
-
3468
- foreach ($deferredKeywordArgs as $itemName => $item) {
3469
- $val[2][$itemName] = $item;
3470
- }
3471
- } elseif (isset($remaining[$i])) {
3472
- $val = $remaining[$i];
3473
- } elseif (isset($keywordArgs[$name])) {
3474
- $val = $keywordArgs[$name];
3475
- } elseif (! empty($default)) {
3476
- continue;
3477
- } else {
3478
- $this->throwError("Missing argument $name");
3479
- }
3480
-
3481
- $this->set($name, $this->reduce($val, true), true, $env);
3482
- }
3483
-
3484
- $storeEnv->store = $env->store;
3485
-
3486
- foreach ($args as $arg) {
3487
- list($i, $name, $default, $isVariable) = $arg;
3488
-
3489
- if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) {
3490
- continue;
3491
- }
3492
-
3493
- $this->set($name, $this->reduce($default, true), true);
3494
- }
3495
- }
3496
-
3497
- /**
3498
- * Coerce a php value into a scss one
3499
- *
3500
- * @param mixed $value
3501
- *
3502
- * @return array
3503
- */
3504
- private function coerceValue($value)
3505
- {
3506
- if (is_array($value) || $value instanceof \ArrayAccess) {
3507
- return $value;
3508
- }
3509
-
3510
- if (is_bool($value)) {
3511
- return $this->toBool($value);
3512
- }
3513
-
3514
- if ($value === null) {
3515
- $value = self::$null;
3516
- }
3517
-
3518
- if (is_numeric($value)) {
3519
- return new Node\Number($value, '');
3520
- }
3521
-
3522
- if ($value === '') {
3523
- return self::$emptyString;
3524
- }
3525
-
3526
- return array(Type::T_KEYWORD, $value);
3527
- }
3528
-
3529
- /**
3530
- * Coerce something to map
3531
- *
3532
- * @param array $item
3533
- *
3534
- * @return array
3535
- */
3536
- protected function coerceMap($item)
3537
- {
3538
- if ($item[0] === Type::T_MAP) {
3539
- return $item;
3540
- }
3541
-
3542
- if ($item === self::$emptyList) {
3543
- return self::$emptyMap;
3544
- }
3545
-
3546
- return array(Type::T_MAP, array($item), array(self::$null));
3547
- }
3548
-
3549
- /**
3550
- * Coerce something to list
3551
- *
3552
- * @param array $item
3553
- *
3554
- * @return array
3555
- */
3556
- protected function coerceList($item, $delim = ',')
3557
- {
3558
- if (isset($item) && $item[0] === Type::T_LIST) {
3559
- return $item;
3560
- }
3561
-
3562
- if (isset($item) && $item[0] === Type::T_MAP) {
3563
- $keys = $item[1];
3564
- $values = $item[2];
3565
- $list = array();
3566
-
3567
- for ($i = 0, $s = count($keys); $i < $s; $i++) {
3568
- $key = $keys[$i];
3569
- $value = $values[$i];
3570
-
3571
- $list[] = array(
3572
- Type::T_LIST,
3573
- '',
3574
- array(array(Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))), $value)
3575
- );
3576
- }
3577
-
3578
- return array(Type::T_LIST, ',', $list);
3579
- }
3580
-
3581
- return array(Type::T_LIST, $delim, ! isset($item) ? array(): array($item));
3582
- }
3583
-
3584
- /**
3585
- * Coerce color for expression
3586
- *
3587
- * @param array $value
3588
- *
3589
- * @return array|null
3590
- */
3591
- protected function coerceForExpression($value)
3592
- {
3593
- if ($color = $this->coerceColor($value)) {
3594
- return $color;
3595
- }
3596
-
3597
- return $value;
3598
- }
3599
-
3600
- /**
3601
- * Coerce value to color
3602
- *
3603
- * @param array $value
3604
- *
3605
- * @return array|null
3606
- */
3607
- protected function coerceColor($value)
3608
- {
3609
- switch ($value[0]) {
3610
- case Type::T_COLOR:
3611
- return $value;
3612
-
3613
- case Type::T_KEYWORD:
3614
- $name = strtolower($value[1]);
3615
-
3616
- if (isset(Colors::$cssColors[$name])) {
3617
- $rgba = explode(',', Colors::$cssColors[$name]);
3618
-
3619
- return isset($rgba[3])
3620
- ? array(Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3])
3621
- : array(Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]);
3622
- }
3623
-
3624
- return null;
3625
- }
3626
-
3627
- return null;
3628
- }
3629
-
3630
- /**
3631
- * Coerce value to string
3632
- *
3633
- * @param array $value
3634
- *
3635
- * @return array|null
3636
- */
3637
- protected function coerceString($value)
3638
- {
3639
- if ($value[0] === Type::T_STRING) {
3640
- return $value;
3641
- }
3642
-
3643
- return array(Type::T_STRING, '', array($this->compileValue($value)));
3644
- }
3645
-
3646
- /**
3647
- * Coerce value to a percentage
3648
- *
3649
- * @param array $value
3650
- *
3651
- * @return integer|float
3652
- */
3653
- protected function coercePercent($value)
3654
- {
3655
- if ($value[0] === Type::T_NUMBER) {
3656
- if ($value[2] === '%') {
3657
- return $value[1] / 100;
3658
- }
3659
-
3660
- return $value[1];
3661
- }
3662
-
3663
- return 0;
3664
- }
3665
-
3666
- /**
3667
- * Assert value is a map
3668
- *
3669
- * @api
3670
- *
3671
- * @param array $value
3672
- *
3673
- * @return array
3674
- *
3675
- * @throws \Exception
3676
- */
3677
- public function assertMap($value)
3678
- {
3679
- $value = $this->coerceMap($value);
3680
-
3681
- if ($value[0] !== Type::T_MAP) {
3682
- $this->throwError('expecting map');
3683
- }
3684
-
3685
- return $value;
3686
- }
3687
-
3688
- /**
3689
- * Assert value is a list
3690
- *
3691
- * @api
3692
- *
3693
- * @param array $value
3694
- *
3695
- * @return array
3696
- *
3697
- * @throws \Exception
3698
- */
3699
- public function assertList($value)
3700
- {
3701
- if ($value[0] !== Type::T_LIST) {
3702
- $this->throwError('expecting list');
3703
- }
3704
-
3705
- return $value;
3706
- }
3707
-
3708
- /**
3709
- * Assert value is a color
3710
- *
3711
- * @api
3712
- *
3713
- * @param array $value
3714
- *
3715
- * @return array
3716
- *
3717
- * @throws \Exception
3718
- */
3719
- public function assertColor($value)
3720
- {
3721
- if ($color = $this->coerceColor($value)) {
3722
- return $color;
3723
- }
3724
-
3725
- $this->throwError('expecting color');
3726
- }
3727
-
3728
- /**
3729
- * Assert value is a number
3730
- *
3731
- * @api
3732
- *
3733
- * @param array $value
3734
- *
3735
- * @return integer|float
3736
- *
3737
- * @throws \Exception
3738
- */
3739
- public function assertNumber($value)
3740
- {
3741
- if ($value[0] !== Type::T_NUMBER) {
3742
- $this->throwError('expecting number');
3743
- }
3744
-
3745
- return $value[1];
3746
- }
3747
-
3748
- /**
3749
- * Make sure a color's components don't go out of bounds
3750
- *
3751
- * @param array $c
3752
- *
3753
- * @return array
3754
- */
3755
- protected function fixColor($c)
3756
- {
3757
- foreach (array(1, 2, 3) as $i) {
3758
- if ($c[$i] < 0) {
3759
- $c[$i] = 0;
3760
- }
3761
-
3762
- if ($c[$i] > 255) {
3763
- $c[$i] = 255;
3764
- }
3765
- }
3766
-
3767
- return $c;
3768
- }
3769
-
3770
- /**
3771
- * Convert RGB to HSL
3772
- *
3773
- * @api
3774
- *
3775
- * @param integer $red
3776
- * @param integer $green
3777
- * @param integer $blue
3778
- *
3779
- * @return array
3780
- */
3781
- public function toHSL($red, $green, $blue)
3782
- {
3783
- $min = min($red, $green, $blue);
3784
- $max = max($red, $green, $blue);
3785
-
3786
- $l = $min + $max;
3787
- $d = $max - $min;
3788
-
3789
- if ((int) $d === 0) {
3790
- $h = $s = 0;
3791
- } else {
3792
- if ($l < 255) {
3793
- $s = $d / $l;
3794
- } else {
3795
- $s = $d / (510 - $l);
3796
- }
3797
-
3798
- if ($red == $max) {
3799
- $h = 60 * ($green - $blue) / $d;
3800
- } elseif ($green == $max) {
3801
- $h = 60 * ($blue - $red) / $d + 120;
3802
- } elseif ($blue == $max) {
3803
- $h = 60 * ($red - $green) / $d + 240;
3804
- }
3805
- }
3806
-
3807
- return array(Type::T_HSL, fmod($h, 360), $s * 100, $l / 5.1);
3808
- }
3809
-
3810
- /**
3811
- * Hue to RGB helper
3812
- *
3813
- * @param float $m1
3814
- * @param float $m2
3815
- * @param float $h
3816
- *
3817
- * @return float
3818
- */
3819
- private function hueToRGB($m1, $m2, $h)
3820
- {
3821
- if ($h < 0) {
3822
- $h += 1;
3823
- } elseif ($h > 1) {
3824
- $h -= 1;
3825
- }
3826
-
3827
- if ($h * 6 < 1) {
3828
- return $m1 + ($m2 - $m1) * $h * 6;
3829
- }
3830
-
3831
- if ($h * 2 < 1) {
3832
- return $m2;
3833
- }
3834
-
3835
- if ($h * 3 < 2) {
3836
- return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
3837
- }
3838
-
3839
- return $m1;
3840
- }
3841
-
3842
- /**
3843
- * Convert HSL to RGB
3844
- *
3845
- * @api
3846
- *
3847
- * @param integer $hue H from 0 to 360
3848
- * @param integer $saturation S from 0 to 100
3849
- * @param integer $lightness L from 0 to 100
3850
- *
3851
- * @return array
3852
- */
3853
- public function toRGB($hue, $saturation, $lightness)
3854
- {
3855
- if ($hue < 0) {
3856
- $hue += 360;
3857
- }
3858
-
3859
- $h = $hue / 360;
3860
- $s = min(100, max(0, $saturation)) / 100;
3861
- $l = min(100, max(0, $lightness)) / 100;
3862
-
3863
- $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
3864
- $m1 = $l * 2 - $m2;
3865
-
3866
- $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
3867
- $g = $this->hueToRGB($m1, $m2, $h) * 255;
3868
- $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
3869
-
3870
- $out = array(Type::T_COLOR, $r, $g, $b);
3871
-
3872
- return $out;
3873
- }
3874
-
3875
- // Built in functions
3876
-
3877
- //protected static $libCall = array('name', 'args...');
3878
- protected function libCall($args, $kwargs)
3879
- {
3880
- $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
3881
-
3882
- $args = array_map(
3883
- function ($a) {
3884
- return array(null, $a, false);
3885
- },
3886
- $args
3887
- );
3888
-
3889
- if (count($kwargs)) {
3890
- foreach ($kwargs as $key => $value) {
3891
- $args[] = array(array(Type::T_VARIABLE, $key), $value, false);
3892
- }
3893
- }
3894
-
3895
- return $this->reduce(array(Type::T_FUNCTION_CALL, $name, $args));
3896
- }
3897
-
3898
- protected static $libIf = array('condition', 'if-true', 'if-false');
3899
- protected function libIf($args)
3900
- {
3901
- list($cond, $t, $f) = $args;
3902
-
3903
- if (! $this->isTruthy($this->reduce($cond, true))) {
3904
- return $this->reduce($f, true);
3905
- }
3906
-
3907
- return $this->reduce($t, true);
3908
- }
3909
-
3910
- protected static $libIndex = array('list', 'value');
3911
- protected function libIndex($args)
3912
- {
3913
- list($list, $value) = $args;
3914
-
3915
- if ($value[0] === Type::T_MAP) {
3916
- return self::$null;
3917
- }
3918
-
3919
- if ($list[0] === Type::T_MAP ||
3920
- $list[0] === Type::T_STRING ||
3921
- $list[0] === Type::T_KEYWORD ||
3922
- $list[0] === Type::T_INTERPOLATE
3923
- ) {
3924
- $list = $this->coerceList($list, ' ');
3925
- }
3926
-
3927
- if ($list[0] !== Type::T_LIST) {
3928
- return self::$null;
3929
- }
3930
-
3931
- $values = array();
3932
-
3933
- foreach ($list[2] as $item) {
3934
- $values[] = $this->normalizeValue($item);
3935
- }
3936
-
3937
- $key = array_search($this->normalizeValue($value), $values);
3938
-
3939
- return false === $key ? self::$null : $key + 1;
3940
- }
3941
-
3942
- protected static $libRgb = array('red', 'green', 'blue');
3943
- protected function libRgb($args)
3944
- {
3945
- list($r, $g, $b) = $args;
3946
-
3947
- return array(Type::T_COLOR, $r[1], $g[1], $b[1]);
3948
- }
3949
-
3950
- protected static $libRgba = array(
3951
- array('red', 'color'),
3952
- 'green', 'blue', 'alpha');
3953
- protected function libRgba($args)
3954
- {
3955
- if ($color = $this->coerceColor($args[0])) {
3956
- // workaround https://github.com/facebook/hhvm/issues/5457
3957
- reset($args);
3958
-
3959
- $num = ! isset($args[1]) ? $args[3] : $args[1];
3960
- $alpha = $this->assertNumber($num);
3961
- $color[4] = $alpha;
3962
-
3963
- return $color;
3964
- }
3965
-
3966
- list($r, $g, $b, $a) = $args;
3967
-
3968
- return array(Type::T_COLOR, $r[1], $g[1], $b[1], $a[1]);
3969
- }
3970
-
3971
- // helper function for adjust_color, change_color, and scale_color
3972
- protected function alterColor($args, $fn)
3973
- {
3974
- $color = $this->assertColor($args[0]);
3975
-
3976
- // workaround https://github.com/facebook/hhvm/issues/5457
3977
- reset($args);
3978
-
3979
- foreach (array(1, 2, 3, 7) as $i) {
3980
- if (isset($args[$i])) {
3981
- $val = $this->assertNumber($args[$i]);
3982
- $ii = $i === 7 ? 4 : $i; // alpha
3983
- $color[$ii] = call_user_func($fn, isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
3984
- }
3985
- }
3986
-
3987
- if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
3988
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
3989
-
3990
- foreach (array(4, 5, 6) as $i) {
3991
- if (isset($args[$i])) {
3992
- $val = $this->assertNumber($args[$i]);
3993
- $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
3994
- }
3995
- }
3996
-
3997
- $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
3998
-
3999
- if (isset($color[4])) {
4000
- $rgb[4] = $color[4];
4001
- }
4002
-
4003
- $color = $rgb;
4004
- }
4005
-
4006
- return $color;
4007
- }
4008
-
4009
- protected static $libAdjustColor = array(
4010
- 'color', 'red', 'green', 'blue',
4011
- 'hue', 'saturation', 'lightness', 'alpha'
4012
- );
4013
- protected function libAdjustColor($args)
4014
- {
4015
- return $this->alterColor($args, function ($base, $alter, $i) {
4016
- return $base + $alter;
4017
- });
4018
- }
4019
-
4020
- protected static $libChangeColor = array(
4021
- 'color', 'red', 'green', 'blue',
4022
- 'hue', 'saturation', 'lightness', 'alpha'
4023
- );
4024
- protected function libChangeColor($args)
4025
- {
4026
- return $this->alterColor($args, function ($base, $alter, $i) {
4027
- return $alter;
4028
- });
4029
- }
4030
-
4031
- protected static $libScaleColor = array(
4032
- 'color', 'red', 'green', 'blue',
4033
- 'hue', 'saturation', 'lightness', 'alpha'
4034
- );
4035
- protected function libScaleColor($args)
4036
- {
4037
- return $this->alterColor($args, function ($base, $scale, $i) {
4038
- // 1, 2, 3 - rgb
4039
- // 4, 5, 6 - hsl
4040
- // 7 - a
4041
- switch ($i) {
4042
- case 1:
4043
- case 2:
4044
- case 3:
4045
- $max = 255;
4046
- break;
4047
-
4048
- case 4:
4049
- $max = 360;
4050
- break;
4051
-
4052
- case 7:
4053
- $max = 1;
4054
- break;
4055
-
4056
- default:
4057
- $max = 100;
4058
- }
4059
-
4060
- $scale = $scale / 100;
4061
-
4062
- if ($scale < 0) {
4063
- return $base * $scale + $base;
4064
- }
4065
-
4066
- return ($max - $base) * $scale + $base;
4067
- });
4068
- }
4069
-
4070
- protected static $libIeHexStr = array('color');
4071
- protected function libIeHexStr($args)
4072
- {
4073
- $color = $this->coerceColor($args[0]);
4074
- $color[4] = isset($color[4]) ? round(255*$color[4]) : 255;
4075
-
4076
- return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
4077
- }
4078
-
4079
- protected static $libRed = array('color');
4080
- protected function libRed($args)
4081
- {
4082
- $color = $this->coerceColor($args[0]);
4083
-
4084
- return $color[1];
4085
- }
4086
-
4087
- protected static $libGreen = array('color');
4088
- protected function libGreen($args)
4089
- {
4090
- $color = $this->coerceColor($args[0]);
4091
-
4092
- return $color[2];
4093
- }
4094
-
4095
- protected static $libBlue = array('color');
4096
- protected function libBlue($args)
4097
- {
4098
- $color = $this->coerceColor($args[0]);
4099
-
4100
- return $color[3];
4101
- }
4102
-
4103
- protected static $libAlpha = array('color');
4104
- protected function libAlpha($args)
4105
- {
4106
- if ($color = $this->coerceColor($args[0])) {
4107
- return isset($color[4]) ? $color[4] : 1;
4108
- }
4109
-
4110
- // this might be the IE function, so return value unchanged
4111
- return null;
4112
- }
4113
-
4114
- protected static $libOpacity = array('color');
4115
- protected function libOpacity($args)
4116
- {
4117
- $value = $args[0];
4118
-
4119
- if ($value[0] === Type::T_NUMBER) {
4120
- return null;
4121
- }
4122
-
4123
- return $this->libAlpha($args);
4124
- }
4125
-
4126
- // mix two colors
4127
- protected static $libMix = array('color-1', 'color-2', 'weight');
4128
- protected function libMix($args)
4129
- {
4130
- list($first, $second, $weight) = $args;
4131
-
4132
- $first = $this->assertColor($first);
4133
- $second = $this->assertColor($second);
4134
-
4135
- if (! isset($weight)) {
4136
- $weight = 0.5;
4137
- } else {
4138
- $weight = $this->coercePercent($weight);
4139
- }
4140
-
4141
- $firstAlpha = isset($first[4]) ? $first[4] : 1;
4142
- $secondAlpha = isset($second[4]) ? $second[4] : 1;
4143
-
4144
- $w = $weight * 2 - 1;
4145
- $a = $firstAlpha - $secondAlpha;
4146
-
4147
- $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
4148
- $w2 = 1.0 - $w1;
4149
-
4150
- $new = array(Type::T_COLOR,
4151
- $w1 * $first[1] + $w2 * $second[1],
4152
- $w1 * $first[2] + $w2 * $second[2],
4153
- $w1 * $first[3] + $w2 * $second[3],
4154
- );
4155
-
4156
- if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
4157
- $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1);
4158
- }
4159
-
4160
- return $this->fixColor($new);
4161
- }
4162
-
4163
- protected static $libHsl = array('hue', 'saturation', 'lightness');
4164
- protected function libHsl($args)
4165
- {
4166
- list($h, $s, $l) = $args;
4167
-
4168
- return $this->toRGB($h[1], $s[1], $l[1]);
4169
- }
4170
-
4171
- protected static $libHsla = array('hue', 'saturation', 'lightness', 'alpha');
4172
- protected function libHsla($args)
4173
- {
4174
- list($h, $s, $l, $a) = $args;
4175
-
4176
- $color = $this->toRGB($h[1], $s[1], $l[1]);
4177
- $color[4] = $a[1];
4178
-
4179
- return $color;
4180
- }
4181
-
4182
- protected static $libHue = array('color');
4183
- protected function libHue($args)
4184
- {
4185
- $color = $this->assertColor($args[0]);
4186
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4187
-
4188
- return new Node\Number($hsl[1], 'deg');
4189
- }
4190
-
4191
- protected static $libSaturation = array('color');
4192
- protected function libSaturation($args)
4193
- {
4194
- $color = $this->assertColor($args[0]);
4195
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4196
-
4197
- return new Node\Number($hsl[2], '%');
4198
- }
4199
-
4200
- protected static $libLightness = array('color');
4201
- protected function libLightness($args)
4202
- {
4203
- $color = $this->assertColor($args[0]);
4204
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4205
-
4206
- return new Node\Number($hsl[3], '%');
4207
- }
4208
-
4209
- protected function adjustHsl($color, $idx, $amount)
4210
- {
4211
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4212
- $hsl[$idx] += $amount;
4213
- $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
4214
-
4215
- if (isset($color[4])) {
4216
- $out[4] = $color[4];
4217
- }
4218
-
4219
- return $out;
4220
- }
4221
-
4222
- protected static $libAdjustHue = array('color', 'degrees');
4223
- protected function libAdjustHue($args)
4224
- {
4225
- $color = $this->assertColor($args[0]);
4226
- $degrees = $this->assertNumber($args[1]);
4227
-
4228
- return $this->adjustHsl($color, 1, $degrees);
4229
- }
4230
-
4231
- protected static $libLighten = array('color', 'amount');
4232
- protected function libLighten($args)
4233
- {
4234
- $color = $this->assertColor($args[0]);
4235
- $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
4236
-
4237
- return $this->adjustHsl($color, 3, $amount);
4238
- }
4239
-
4240
- protected static $libDarken = array('color', 'amount');
4241
- protected function libDarken($args)
4242
- {
4243
- $color = $this->assertColor($args[0]);
4244
- $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
4245
-
4246
- return $this->adjustHsl($color, 3, -$amount);
4247
- }
4248
-
4249
- protected static $libSaturate = array('color', 'amount');
4250
- protected function libSaturate($args)
4251
- {
4252
- $value = $args[0];
4253
-
4254
- if ($value[0] === Type::T_NUMBER) {
4255
- return null;
4256
- }
4257
-
4258
- $color = $this->assertColor($value);
4259
- $amount = 100 * $this->coercePercent($args[1]);
4260
-
4261
- return $this->adjustHsl($color, 2, $amount);
4262
- }
4263
-
4264
- protected static $libDesaturate = array('color', 'amount');
4265
- protected function libDesaturate($args)
4266
- {
4267
- $color = $this->assertColor($args[0]);
4268
- $amount = 100 * $this->coercePercent($args[1]);
4269
-
4270
- return $this->adjustHsl($color, 2, -$amount);
4271
- }
4272
-
4273
- protected static $libGrayscale = array('color');
4274
- protected function libGrayscale($args)
4275
- {
4276
- $value = $args[0];
4277
-
4278
- if ($value[0] === Type::T_NUMBER) {
4279
- return null;
4280
- }
4281
-
4282
- return $this->adjustHsl($this->assertColor($value), 2, -100);
4283
- }
4284
-
4285
- protected static $libComplement = array('color');
4286
- protected function libComplement($args)
4287
- {
4288
- return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
4289
- }
4290
-
4291
- protected static $libInvert = array('color');
4292
- protected function libInvert($args)
4293
- {
4294
- $value = $args[0];
4295
-
4296
- if ($value[0] === Type::T_NUMBER) {
4297
- return null;
4298
- }
4299
-
4300
- $color = $this->assertColor($value);
4301
- $color[1] = 255 - $color[1];
4302
- $color[2] = 255 - $color[2];
4303
- $color[3] = 255 - $color[3];
4304
-
4305
- return $color;
4306
- }
4307
-
4308
- // increases opacity by amount
4309
- protected static $libOpacify = array('color', 'amount');
4310
- protected function libOpacify($args)
4311
- {
4312
- $color = $this->assertColor($args[0]);
4313
- $amount = $this->coercePercent($args[1]);
4314
-
4315
- $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
4316
- $color[4] = min(1, max(0, $color[4]));
4317
-
4318
- return $color;
4319
- }
4320
-
4321
- protected static $libFadeIn = array('color', 'amount');
4322
- protected function libFadeIn($args)
4323
- {
4324
- return $this->libOpacify($args);
4325
- }
4326
-
4327
- // decreases opacity by amount
4328
- protected static $libTransparentize = array('color', 'amount');
4329
- protected function libTransparentize($args)
4330
- {
4331
- $color = $this->assertColor($args[0]);
4332
- $amount = $this->coercePercent($args[1]);
4333
-
4334
- $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
4335
- $color[4] = min(1, max(0, $color[4]));
4336
-
4337
- return $color;
4338
- }
4339
-
4340
- protected static $libFadeOut = array('color', 'amount');
4341
- protected function libFadeOut($args)
4342
- {
4343
- return $this->libTransparentize($args);
4344
- }
4345
-
4346
- protected static $libUnquote = array('string');
4347
- protected function libUnquote($args)
4348
- {
4349
- $str = $args[0];
4350
-
4351
- if ($str[0] === Type::T_STRING) {
4352
- $str[1] = '';
4353
- }
4354
-
4355
- return $str;
4356
- }
4357
-
4358
- protected static $libQuote = array('string');
4359
- protected function libQuote($args)
4360
- {
4361
- $value = $args[0];
4362
-
4363
- if ($value[0] === Type::T_STRING && ! empty($value[1])) {
4364
- return $value;
4365
- }
4366
-
4367
- return array(Type::T_STRING, '"', array($value));
4368
- }
4369
-
4370
- protected static $libPercentage = array('value');
4371
- protected function libPercentage($args)
4372
- {
4373
- return new Node\Number($this->coercePercent($args[0]) * 100, '%');
4374
- }
4375
-
4376
- protected static $libRound = array('value');
4377
- protected function libRound($args)
4378
- {
4379
- $num = $args[0];
4380
- $num[1] = round($num[1]);
4381
-
4382
- return $num;
4383
- }
4384
-
4385
- protected static $libFloor = array('value');
4386
- protected function libFloor($args)
4387
- {
4388
- $num = $args[0];
4389
- $num[1] = floor($num[1]);
4390
-
4391
- return $num;
4392
- }
4393
-
4394
- protected static $libCeil = array('value');
4395
- protected function libCeil($args)
4396
- {
4397
- $num = $args[0];
4398
- $num[1] = ceil($num[1]);
4399
-
4400
- return $num;
4401
- }
4402
-
4403
- protected static $libAbs = array('value');
4404
- protected function libAbs($args)
4405
- {
4406
- $num = $args[0];
4407
- $num[1] = abs($num[1]);
4408
-
4409
- return $num;
4410
- }
4411
-
4412
- protected function libMin($args)
4413
- {
4414
- $numbers = $this->getNormalizedNumbers($args);
4415
- $min = null;
4416
-
4417
- foreach ($numbers as $key => $number) {
4418
- if (null === $min || $number[1] <= $min[1]) {
4419
- $min = array($key, $number[1]);
4420
- }
4421
- }
4422
-
4423
- return $args[$min[0]];
4424
- }
4425
-
4426
- protected function libMax($args)
4427
- {
4428
- $numbers = $this->getNormalizedNumbers($args);
4429
- $max = null;
4430
-
4431
- foreach ($numbers as $key => $number) {
4432
- if (null === $max || $number[1] >= $max[1]) {
4433
- $max = array($key, $number[1]);
4434
- }
4435
- }
4436
-
4437
- return $args[$max[0]];
4438
- }
4439
-
4440
- /**
4441
- * Helper to normalize args containing numbers
4442
- *
4443
- * @param array $args
4444
- *
4445
- * @return array
4446
- */
4447
- protected function getNormalizedNumbers($args)
4448
- {
4449
- $unit = null;
4450
- $originalUnit = null;
4451
- $numbers = array();
4452
-
4453
- foreach ($args as $key => $item) {
4454
- if ($item[0] !== Type::T_NUMBER) {
4455
- $this->throwError('%s is not a number', $item[0]);
4456
- }
4457
-
4458
- $number = $item->normalize();
4459
-
4460
- if (null === $unit) {
4461
- $unit = $number[2];
4462
- $originalUnit = $item->unitStr();
4463
- } elseif ($unit !== $number[2]) {
4464
- $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
4465
- }
4466
-
4467
- $numbers[$key] = $number;
4468
- }
4469
-
4470
- return $numbers;
4471
- }
4472
-
4473
- protected static $libLength = array('list');
4474
- protected function libLength($args)
4475
- {
4476
- $list = $this->coerceList($args[0]);
4477
-
4478
- return count($list[2]);
4479
- }
4480
-
4481
- // TODO: need a way to declare this built-in as varargs
4482
- //protected static $libListSeparator = array('list...');
4483
- protected function libListSeparator($args)
4484
- {
4485
- if (count($args) > 1) {
4486
- return 'comma';
4487
- }
4488
-
4489
- $list = $this->coerceList($args[0]);
4490
-
4491
- if (count($list[2]) <= 1) {
4492
- return 'space';
4493
- }
4494
-
4495
- if ($list[1] === ',') {
4496
- return 'comma';
4497
- }
4498
-
4499
- return 'space';
4500
- }
4501
-
4502
- protected static $libNth = array('list', 'n');
4503
- protected function libNth($args)
4504
- {
4505
- $list = $this->coerceList($args[0]);
4506
- $n = $this->assertNumber($args[1]);
4507
-
4508
- if ($n > 0) {
4509
- $n--;
4510
- } elseif ($n < 0) {
4511
- $n += count($list[2]);
4512
- }
4513
-
4514
- return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue;
4515
- }
4516
-
4517
- protected static $libSetNth = array('list', 'n', 'value');
4518
- protected function libSetNth($args)
4519
- {
4520
- $list = $this->coerceList($args[0]);
4521
- $n = $this->assertNumber($args[1]);
4522
-
4523
- if ($n > 0) {
4524
- $n--;
4525
- } elseif ($n < 0) {
4526
- $n += count($list[2]);
4527
- }
4528
-
4529
- if (! isset($list[2][$n])) {
4530
- $this->throwError('Invalid argument for "n"');
4531
- }
4532
-
4533
- $list[2][$n] = $args[2];
4534
-
4535
- return $list;
4536
- }
4537
-
4538
- protected static $libMapGet = array('map', 'key');
4539
- protected function libMapGet($args)
4540
- {
4541
- $map = $this->assertMap($args[0]);
4542
- $key = $this->compileStringContent($this->coerceString($args[1]));
4543
-
4544
- for ($i = count($map[1]) - 1; $i >= 0; $i--) {
4545
- if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
4546
- return $map[2][$i];
4547
- }
4548
- }
4549
-
4550
- return self::$null;
4551
- }
4552
-
4553
- protected static $libMapKeys = array('map');
4554
- protected function libMapKeys($args)
4555
- {
4556
- $map = $this->assertMap($args[0]);
4557
- $keys = $map[1];
4558
-
4559
- return array(Type::T_LIST, ',', $keys);
4560
- }
4561
-
4562
- protected static $libMapValues = array('map');
4563
- protected function libMapValues($args)
4564
- {
4565
- $map = $this->assertMap($args[0]);
4566
- $values = $map[2];
4567
-
4568
- return array(Type::T_LIST, ',', $values);
4569
- }
4570
-
4571
- protected static $libMapRemove = array('map', 'key');
4572
- protected function libMapRemove($args)
4573
- {
4574
- $map = $this->assertMap($args[0]);
4575
- $key = $this->compileStringContent($this->coerceString($args[1]));
4576
-
4577
- for ($i = count($map[1]) - 1; $i >= 0; $i--) {
4578
- if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
4579
- array_splice($map[1], $i, 1);
4580
- array_splice($map[2], $i, 1);
4581
- }
4582
- }
4583
-
4584
- return $map;
4585
- }
4586
-
4587
- protected static $libMapHasKey = array('map', 'key');
4588
- protected function libMapHasKey($args)
4589
- {
4590
- $map = $this->assertMap($args[0]);
4591
- $key = $this->compileStringContent($this->coerceString($args[1]));
4592
-
4593
- for ($i = count($map[1]) - 1; $i >= 0; $i--) {
4594
- if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
4595
- return true;
4596
- }
4597
- }
4598
-
4599
- return false;
4600
- }
4601
-
4602
- protected static $libMapMerge = array('map-1', 'map-2');
4603
- protected function libMapMerge($args)
4604
- {
4605
- $map1 = $this->assertMap($args[0]);
4606
- $map2 = $this->assertMap($args[1]);
4607
-
4608
- return array(Type::T_MAP, array_merge($map1[1], $map2[1]), array_merge($map1[2], $map2[2]));
4609
- }
4610
-
4611
- protected function listSeparatorForJoin($list1, $sep)
4612
- {
4613
- if (! isset($sep)) {
4614
- return $list1[1];
4615
- }
4616
-
4617
- switch ($this->compileValue($sep)) {
4618
- case 'comma':
4619
- return ',';
4620
-
4621
- case 'space':
4622
- return '';
4623
-
4624
- default:
4625
- return $list1[1];
4626
- }
4627
- }
4628
-
4629
- protected static $libJoin = array('list1', 'list2', 'separator');
4630
- protected function libJoin($args)
4631
- {
4632
- list($list1, $list2, $sep) = $args;
4633
-
4634
- $list1 = $this->coerceList($list1, ' ');
4635
- $list2 = $this->coerceList($list2, ' ');
4636
- $sep = $this->listSeparatorForJoin($list1, $sep);
4637
-
4638
- return array(Type::T_LIST, $sep, array_merge($list1[2], $list2[2]));
4639
- }
4640
-
4641
- protected static $libAppend = array('list', 'val', 'separator');
4642
- protected function libAppend($args)
4643
- {
4644
- list($list1, $value, $sep) = $args;
4645
-
4646
- $list1 = $this->coerceList($list1, ' ');
4647
- $sep = $this->listSeparatorForJoin($list1, $sep);
4648
-
4649
- return array(Type::T_LIST, $sep, array_merge($list1[2], array($value)));
4650
- }
4651
-
4652
- protected function libZip($args)
4653
- {
4654
- foreach ($args as $arg) {
4655
- $this->assertList($arg);
4656
- }
4657
-
4658
- $lists = array();
4659
- $firstList = array_shift($args);
4660
-
4661
- foreach ($firstList[2] as $key => $item) {
4662
- $list = array(Type::T_LIST, '', array($item));
4663
-
4664
- foreach ($args as $arg) {
4665
- if (isset($arg[2][$key])) {
4666
- $list[2][] = $arg[2][$key];
4667
- } else {
4668
- break 2;
4669
- }
4670
- }
4671
-
4672
- $lists[] = $list;
4673
- }
4674
-
4675
- return array(Type::T_LIST, ',', $lists);
4676
- }
4677
-
4678
- protected static $libTypeOf = array('value');
4679
- protected function libTypeOf($args)
4680
- {
4681
- $value = $args[0];
4682
-
4683
- switch ($value[0]) {
4684
- case Type::T_KEYWORD:
4685
- if ($value === self::$true || $value === self::$false) {
4686
- return 'bool';
4687
- }
4688
-
4689
- if ($this->coerceColor($value)) {
4690
- return 'color';
4691
- }
4692
-
4693
- // fall-thru
4694
- case Type::T_FUNCTION:
4695
- return 'string';
4696
-
4697
- case Type::T_LIST:
4698
- if (isset($value[3]) && $value[3]) {
4699
- return 'arglist';
4700
- }
4701
-
4702
- // fall-thru
4703
- default:
4704
- return $value[0];
4705
- }
4706
- }
4707
-
4708
- protected static $libUnit = array('number');
4709
- protected function libUnit($args)
4710
- {
4711
- $num = $args[0];
4712
-
4713
- if ($num[0] === Type::T_NUMBER) {
4714
- return array(Type::T_STRING, '"', array($num->unitStr()));
4715
- }
4716
-
4717
- return '';
4718
- }
4719
-
4720
- protected static $libUnitless = array('number');
4721
- protected function libUnitless($args)
4722
- {
4723
- $value = $args[0];
4724
-
4725
- return $value[0] === Type::T_NUMBER && $value->unitless();
4726
- }
4727
-
4728
- protected static $libComparable = array('number-1', 'number-2');
4729
- protected function libComparable($args)
4730
- {
4731
- list($number1, $number2) = $args;
4732
-
4733
- if (! isset($number1[0]) || $number1[0] !== Type::T_NUMBER ||
4734
- ! isset($number2[0]) || $number2[0] !== Type::T_NUMBER
4735
- ) {
4736
- $this->throwError('Invalid argument(s) for "comparable"');
4737
- }
4738
-
4739
- $number1 = $number1->normalize();
4740
- $number2 = $number2->normalize();
4741
-
4742
- return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless();
4743
- }
4744
-
4745
- protected static $libStrIndex = array('string', 'substring');
4746
- protected function libStrIndex($args)
4747
- {
4748
- $string = $this->coerceString($args[0]);
4749
- $stringContent = $this->compileStringContent($string);
4750
-
4751
- $substring = $this->coerceString($args[1]);
4752
- $substringContent = $this->compileStringContent($substring);
4753
-
4754
- $result = strpos($stringContent, $substringContent);
4755
-
4756
- return $result === false ? self::$null : new Node\Number($result + 1, '');
4757
- }
4758
-
4759
- protected static $libStrInsert = array('string', 'insert', 'index');
4760
- protected function libStrInsert($args)
4761
- {
4762
- $string = $this->coerceString($args[0]);
4763
- $stringContent = $this->compileStringContent($string);
4764
-
4765
- $insert = $this->coerceString($args[1]);
4766
- $insertContent = $this->compileStringContent($insert);
4767
-
4768
- list(, $index) = $args[2];
4769
-
4770
- $string[2] = array(substr_replace($stringContent, $insertContent, $index - 1, 0));
4771
-
4772
- return $string;
4773
- }
4774
-
4775
- protected static $libStrLength = array('string');
4776
- protected function libStrLength($args)
4777
- {
4778
- $string = $this->coerceString($args[0]);
4779
- $stringContent = $this->compileStringContent($string);
4780
-
4781
- return new Node\Number(strlen($stringContent), '');
4782
- }
4783
-
4784
- protected static $libStrSlice = array('string', 'start-at', 'end-at');
4785
- protected function libStrSlice($args)
4786
- {
4787
- if ($args[2][1] == 0) {
4788
- return self::$null;
4789
- }
4790
-
4791
- $string = $this->coerceString($args[0]);
4792
- $stringContent = $this->compileStringContent($string);
4793
-
4794
- $start = (int) $args[1][1] ?: 1;
4795
- $end = (int) $args[2][1];
4796
-
4797
- $string[2] = array(substr($stringContent, $start - 1, ($end < 0 ? $end : $end - $start) + 1));
4798
-
4799
- return $string;
4800
- }
4801
-
4802
- protected static $libToLowerCase = array('string');
4803
- protected function libToLowerCase($args)
4804
- {
4805
- $string = $this->coerceString($args[0]);
4806
- $stringContent = $this->compileStringContent($string);
4807
-
4808
- $string[2] = array(mb_strtolower($stringContent));
4809
-
4810
- return $string;
4811
- }
4812
-
4813
- protected static $libToUpperCase = array('string');
4814
- protected function libToUpperCase($args)
4815
- {
4816
- $string = $this->coerceString($args[0]);
4817
- $stringContent = $this->compileStringContent($string);
4818
-
4819
- $string[2] = array(mb_strtoupper($stringContent));
4820
-
4821
- return $string;
4822
- }
4823
-
4824
- protected static $libFeatureExists = array('feature');
4825
- protected function libFeatureExists($args)
4826
- {
4827
- $string = $this->coerceString($args[0]);
4828
- $name = $this->compileStringContent($string);
4829
-
4830
- return $this->toBool(
4831
- array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false
4832
- );
4833
- }
4834
-
4835
- protected static $libFunctionExists = array('name');
4836
- protected function libFunctionExists($args)
4837
- {
4838
- $string = $this->coerceString($args[0]);
4839
- $name = $this->compileStringContent($string);
4840
-
4841
- // user defined functions
4842
- if ($this->has(self::$namespaces['function'] . $name)) {
4843
- return true;
4844
- }
4845
-
4846
- $name = $this->normalizeName($name);
4847
-
4848
- if (isset($this->userFunctions[$name])) {
4849
- return true;
4850
- }
4851
-
4852
- // built-in functions
4853
- $f = $this->getBuiltinFunction($name);
4854
-
4855
- return $this->toBool(is_callable($f));
4856
- }
4857
-
4858
- protected static $libGlobalVariableExists = array('name');
4859
- protected function libGlobalVariableExists($args)
4860
- {
4861
- $string = $this->coerceString($args[0]);
4862
- $name = $this->compileStringContent($string);
4863
-
4864
- return $this->has($name, $this->rootEnv);
4865
- }
4866
-
4867
- protected static $libMixinExists = array('name');
4868
- protected function libMixinExists($args)
4869
- {
4870
- $string = $this->coerceString($args[0]);
4871
- $name = $this->compileStringContent($string);
4872
-
4873
- return $this->has(self::$namespaces['mixin'] . $name);
4874
- }
4875
-
4876
- protected static $libVariableExists = array('name');
4877
- protected function libVariableExists($args)
4878
- {
4879
- $string = $this->coerceString($args[0]);
4880
- $name = $this->compileStringContent($string);
4881
-
4882
- return $this->has($name);
4883
- }
4884
-
4885
- /**
4886
- * Workaround IE7's content counter bug.
4887
- *
4888
- * @param array $args
4889
- */
4890
- protected function libCounter($args)
4891
- {
4892
- $list = array_map(array($this, 'compileValue'), $args);
4893
-
4894
- return array(Type::T_STRING, '', array('counter(' . implode(',', $list) . ')'));
4895
- }
4896
-
4897
- protected function libRandom($args)
4898
- {
4899
- if (isset($args[0])) {
4900
- $n = $this->assertNumber($args[0]);
4901
-
4902
- if ($n < 1) {
4903
- $this->throwError("limit must be greater than or equal to 1");
4904
- }
4905
-
4906
- return new Node\Number(mt_rand(1, $n), '');
4907
- }
4908
-
4909
- return new Node\Number(mt_rand(1, mt_getrandmax()), '');
4910
- }
4911
-
4912
- protected function libUniqueId()
4913
- {
4914
- static $id;
4915
-
4916
- if (! isset($id)) {
4917
- $id = mt_rand(0, pow(36, 8));
4918
- }
4919
-
4920
- $id += mt_rand(0, 10) + 1;
4921
-
4922
- return array(Type::T_STRING, '', array('u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)));
4923
- }
4924
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Compiler/Environment.php DELETED
@@ -1,42 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp\Compiler;
13
-
14
- use Leafo\ScssPhp\Block;
15
-
16
- /**
17
- * SCSS compiler environment
18
- *
19
- * @author Anthon Pang <anthon.pang@gmail.com>
20
- */
21
- class Environment
22
- {
23
- /**
24
- * @var \Leafo\ScssPhp\Block
25
- */
26
- public $block;
27
-
28
- /**
29
- * @var \Leafo\ScssPhp\Compiler\Environment
30
- */
31
- public $parent;
32
-
33
- /**
34
- * @var array
35
- */
36
- public $store;
37
-
38
- /**
39
- * @var integer
40
- */
41
- public $depth;
42
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Formatter.php DELETED
@@ -1,202 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp;
13
-
14
- use Leafo\ScssPhp\Formatter\OutputBlock;
15
-
16
- /**
17
- * SCSS base formatter
18
- *
19
- * @author Leaf Corcoran <leafot@gmail.com>
20
- */
21
- abstract class Formatter
22
- {
23
- /**
24
- * @var integer
25
- */
26
- public $indentLevel;
27
-
28
- /**
29
- * @var string
30
- */
31
- public $indentChar;
32
-
33
- /**
34
- * @var string
35
- */
36
- public $break;
37
-
38
- /**
39
- * @var string
40
- */
41
- public $open;
42
-
43
- /**
44
- * @var string
45
- */
46
- public $close;
47
-
48
- /**
49
- * @var string
50
- */
51
- public $tagSeparator;
52
-
53
- /**
54
- * @var string
55
- */
56
- public $assignSeparator;
57
-
58
- /**
59
- * Initialize formatter
60
- *
61
- * @api
62
- */
63
- abstract public function __construct();
64
-
65
- /**
66
- * Return indentation (whitespace)
67
- *
68
- * @return string
69
- */
70
- protected function indentStr()
71
- {
72
- return '';
73
- }
74
-
75
- /**
76
- * Return property assignment
77
- *
78
- * @api
79
- *
80
- * @param string $name
81
- * @param mixed $value
82
- *
83
- * @return string
84
- */
85
- public function property($name, $value)
86
- {
87
- return rtrim($name) . $this->assignSeparator . $value . ';';
88
- }
89
-
90
- /**
91
- * Strip semi-colon appended by property(); it's a separator, not a terminator
92
- *
93
- * @api
94
- *
95
- * @param array $lines
96
- */
97
- public function stripSemicolon(&$lines)
98
- {
99
- }
100
-
101
- /**
102
- * Output lines inside a block
103
- *
104
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
105
- */
106
- protected function blockLines(OutputBlock $block)
107
- {
108
- $inner = $this->indentStr();
109
-
110
- $glue = $this->break . $inner;
111
-
112
- echo $inner . implode($glue, $block->lines);
113
-
114
- if (! empty($block->children)) {
115
- echo $this->break;
116
- }
117
- }
118
-
119
- /**
120
- * Output block selectors
121
- *
122
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
123
- */
124
- protected function blockSelectors(OutputBlock $block)
125
- {
126
- $inner = $this->indentStr();
127
-
128
- echo $inner
129
- . implode($this->tagSeparator, $block->selectors)
130
- . $this->open . $this->break;
131
- }
132
-
133
- /**
134
- * Output block children
135
- *
136
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
137
- */
138
- protected function blockChildren(OutputBlock $block)
139
- {
140
- foreach ($block->children as $child) {
141
- $this->block($child);
142
- }
143
- }
144
-
145
- /**
146
- * Output non-empty block
147
- *
148
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
149
- */
150
- protected function block(OutputBlock $block)
151
- {
152
- if (empty($block->lines) && empty($block->children)) {
153
- return;
154
- }
155
-
156
- $pre = $this->indentStr();
157
-
158
- if (! empty($block->selectors)) {
159
- $this->blockSelectors($block);
160
-
161
- $this->indentLevel++;
162
- }
163
-
164
- if (! empty($block->lines)) {
165
- $this->blockLines($block);
166
- }
167
-
168
- if (! empty($block->children)) {
169
- $this->blockChildren($block);
170
- }
171
-
172
- if (! empty($block->selectors)) {
173
- $this->indentLevel--;
174
-
175
- if (empty($block->children)) {
176
- echo $this->break;
177
- }
178
-
179
- echo $pre . $this->close . $this->break;
180
- }
181
- }
182
-
183
- /**
184
- * Entry point to formatting a block
185
- *
186
- * @api
187
- *
188
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block An abstract syntax tree
189
- *
190
- * @return string
191
- */
192
- public function format(OutputBlock $block)
193
- {
194
- ob_start();
195
-
196
- $this->block($block);
197
-
198
- $out = ob_get_clean();
199
-
200
- return $out;
201
- }
202
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Formatter/Compact.php DELETED
@@ -1,44 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp\Formatter;
13
-
14
- use Leafo\ScssPhp\Formatter;
15
-
16
- /**
17
- * SCSS compact formatter
18
- *
19
- * @author Leaf Corcoran <leafot@gmail.com>
20
- */
21
- class Compact extends Formatter
22
- {
23
- /**
24
- * {@inheritdoc}
25
- */
26
- public function __construct()
27
- {
28
- $this->indentLevel = 0;
29
- $this->indentChar = '';
30
- $this->break = '';
31
- $this->open = ' {';
32
- $this->close = "}\n\n";
33
- $this->tagSeparator = ',';
34
- $this->assignSeparator = ':';
35
- }
36
-
37
- /**
38
- * {@inheritdoc}
39
- */
40
- public function indentStr()
41
- {
42
- return ' ';
43
- }
44
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Formatter/Compressed.php DELETED
@@ -1,84 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp\Formatter;
13
-
14
- use Leafo\ScssPhp\Formatter;
15
- use Leafo\ScssPhp\Formatter\OutputBlock;
16
-
17
- /**
18
- * SCSS compressed formatter
19
- *
20
- * @author Leaf Corcoran <leafot@gmail.com>
21
- */
22
- class Compressed extends Formatter
23
- {
24
- /**
25
- * {@inheritdoc}
26
- */
27
- public function __construct()
28
- {
29
- $this->indentLevel = 0;
30
- $this->indentChar = ' ';
31
- $this->break = '';
32
- $this->open = '{';
33
- $this->close = '}';
34
- $this->tagSeparator = ',';
35
- $this->assignSeparator = ':';
36
- }
37
-
38
- /**
39
- * {@inheritdoc}
40
- */
41
- public function stripSemicolon(&$lines)
42
- {
43
- if (($count = count($lines))
44
- && substr($lines[$count - 1], -1) === ';'
45
- ) {
46
- $lines[$count - 1] = substr($lines[$count - 1], 0, -1);
47
- }
48
- }
49
-
50
- /**
51
- * {@inheritdoc}
52
- */
53
- public function blockLines(OutputBlock $block)
54
- {
55
- $inner = $this->indentStr();
56
-
57
- $glue = $this->break . $inner;
58
-
59
- foreach ($block->lines as $index => $line) {
60
- if (substr($line, 0, 2) === '/*' && substr($line, 2, 1) !== '!') {
61
- unset($block->lines[$index]);
62
- } elseif (substr($line, 0, 3) === '/*!') {
63
- $block->lines[$index] = '/*' . substr($line, 3);
64
- }
65
- }
66
-
67
- echo $inner . implode($glue, $block->lines);
68
-
69
- if (! empty($block->children)) {
70
- echo $this->break;
71
- }
72
- }
73
-
74
- /**
75
- * {@inherit}
76
- */
77
- public function format(OutputBlock $block)
78
- {
79
- return parent::format($block);
80
-
81
- // TODO: we need to fix the 2 "compressed" tests where the "close" is applied
82
- return trim(str_replace(';}', '}', parent::format($block)));
83
- }
84
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Formatter/Crunched.php DELETED
@@ -1,71 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp\Formatter;
13
-
14
- use Leafo\ScssPhp\Formatter;
15
- use Leafo\ScssPhp\Formatter\OutputBlock;
16
-
17
- /**
18
- * SCSS crunched formatter
19
- *
20
- * @author Anthon Pang <anthon.pang@gmail.com>
21
- */
22
- class Crunched extends Formatter
23
- {
24
- /**
25
- * {@inheritdoc}
26
- */
27
- public function __construct()
28
- {
29
- $this->indentLevel = 0;
30
- $this->indentChar = ' ';
31
- $this->break = '';
32
- $this->open = '{';
33
- $this->close = '}';
34
- $this->tagSeparator = ',';
35
- $this->assignSeparator = ':';
36
- }
37
-
38
- /**
39
- * {@inheritdoc}
40
- */
41
- public function stripSemicolon(&$lines)
42
- {
43
- if (($count = count($lines))
44
- && substr($lines[$count - 1], -1) === ';'
45
- ) {
46
- $lines[$count - 1] = substr($lines[$count - 1], 0, -1);
47
- }
48
- }
49
-
50
- /**
51
- * {@inheritdoc}
52
- */
53
- public function blockLines(OutputBlock $block)
54
- {
55
- $inner = $this->indentStr();
56
-
57
- $glue = $this->break . $inner;
58
-
59
- foreach ($block->lines as $index => $line) {
60
- if (substr($line, 0, 2) === '/*') {
61
- unset($block->lines[$index]);
62
- }
63
- }
64
-
65
- echo $inner . implode($glue, $block->lines);
66
-
67
- if (! empty($block->children)) {
68
- echo $this->break;
69
- }
70
- }
71
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Formatter/Debug.php DELETED
@@ -1,118 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp\Formatter;
13
-
14
- use Leafo\ScssPhp\Formatter;
15
- use Leafo\ScssPhp\Formatter\OutputBlock;
16
-
17
- /**
18
- * SCSS debug formatter
19
- *
20
- * @author Anthon Pang <anthon.pang@gmail.com>
21
- */
22
- class Debug extends Formatter
23
- {
24
- /**
25
- * {@inheritdoc}
26
- */
27
- public function __construct()
28
- {
29
- $this->indentLevel = 0;
30
- $this->indentChar = '';
31
- $this->break = "\n";
32
- $this->open = ' {';
33
- $this->close = ' }';
34
- $this->tagSeparator = ', ';
35
- $this->assignSeparator = ': ';
36
- }
37
-
38
- /**
39
- * {@inheritdoc}
40
- */
41
- protected function indentStr()
42
- {
43
- return str_repeat(' ', $this->indentLevel);
44
- }
45
-
46
- /**
47
- * {@inheritdoc}
48
- */
49
- protected function blockLines(OutputBlock $block)
50
- {
51
- $indent = $this->indentStr();
52
-
53
- if (empty($block->lines)) {
54
- echo "{$indent}block->lines: []\n";
55
-
56
- return;
57
- }
58
-
59
- foreach ($block->lines as $index => $line) {
60
- echo "{$indent}block->lines[{$index}]: $line\n";
61
- }
62
- }
63
-
64
- /**
65
- * {@inheritdoc}
66
- */
67
- protected function blockSelectors(OutputBlock $block)
68
- {
69
- $indent = $this->indentStr();
70
-
71
- if (empty($block->selectors)) {
72
- echo "{$indent}block->selectors: []\n";
73
-
74
- return;
75
- }
76
-
77
- foreach ($block->selectors as $index => $selector) {
78
- echo "{$indent}block->selectors[{$index}]: $selector\n";
79
- }
80
- }
81
-
82
- /**
83
- * {@inheritdoc}
84
- */
85
- protected function blockChildren(OutputBlock $block)
86
- {
87
- $indent = $this->indentStr();
88
-
89
- if (empty($block->children)) {
90
- echo "{$indent}block->children: []\n";
91
-
92
- return;
93
- }
94
-
95
- $this->indentLevel++;
96
-
97
- foreach ($block->children as $i => $child) {
98
- $this->block($child);
99
- }
100
-
101
- $this->indentLevel--;
102
- }
103
-
104
- /**
105
- * {@inheritdoc}
106
- */
107
- protected function block(OutputBlock $block)
108
- {
109
- $indent = $this->indentStr();
110
-
111
- echo "{$indent}block->type: {$block->type}\n" .
112
- "{$indent}block->depth: {$block->depth}\n";
113
-
114
- $this->blockSelectors($block);
115
- $this->blockLines($block);
116
- $this->blockChildren($block);
117
- }
118
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Formatter/Expanded.php DELETED
@@ -1,67 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp\Formatter;
13
-
14
- use Leafo\ScssPhp\Formatter;
15
- use Leafo\ScssPhp\Formatter\OutputBlock;
16
-
17
- /**
18
- * SCSS expanded formatter
19
- *
20
- * @author Leaf Corcoran <leafot@gmail.com>
21
- */
22
- class Expanded extends Formatter
23
- {
24
- /**
25
- * {@inheritdoc}
26
- */
27
- public function __construct()
28
- {
29
- $this->indentLevel = 0;
30
- $this->indentChar = ' ';
31
- $this->break = "\n";
32
- $this->open = ' {';
33
- $this->close = '}';
34
- $this->tagSeparator = ', ';
35
- $this->assignSeparator = ': ';
36
- }
37
-
38
- /**
39
- * {@inheritdoc}
40
- */
41
- protected function indentStr()
42
- {
43
- return str_repeat($this->indentChar, $this->indentLevel);
44
- }
45
-
46
- /**
47
- * {@inheritdoc}
48
- */
49
- protected function blockLines(OutputBlock $block)
50
- {
51
- $inner = $this->indentStr();
52
-
53
- $glue = $this->break . $inner;
54
-
55
- foreach ($block->lines as $index => $line) {
56
- if (substr($line, 0, 2) === '/*') {
57
- $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line);
58
- }
59
- }
60
-
61
- echo $inner . implode($glue, $block->lines);
62
-
63
- if (empty($block->selectors) || ! empty($block->children)) {
64
- echo $this->break;
65
- }
66
- }
67
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Formatter/Nested.php DELETED
@@ -1,197 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp\Formatter;
13
-
14
- use Leafo\ScssPhp\Formatter;
15
- use Leafo\ScssPhp\Formatter\OutputBlock;
16
-
17
- /**
18
- * SCSS nested formatter
19
- *
20
- * @author Leaf Corcoran <leafot@gmail.com>
21
- */
22
- class Nested extends Formatter
23
- {
24
- /**
25
- * @var integer
26
- */
27
- private $depth;
28
-
29
- /**
30
- * {@inheritdoc}
31
- */
32
- public function __construct()
33
- {
34
- $this->indentLevel = 0;
35
- $this->indentChar = ' ';
36
- $this->break = "\n";
37
- $this->open = ' {';
38
- $this->close = ' }';
39
- $this->tagSeparator = ', ';
40
- $this->assignSeparator = ': ';
41
- }
42
-
43
- /**
44
- * {@inheritdoc}
45
- */
46
- protected function indentStr()
47
- {
48
- $n = $this->depth - 1;
49
-
50
- return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
51
- }
52
-
53
- /**
54
- * {@inheritdoc}
55
- */
56
- protected function blockLines(OutputBlock $block)
57
- {
58
- $inner = $this->indentStr();
59
-
60
- $glue = $this->break . $inner;
61
-
62
- foreach ($block->lines as $index => $line) {
63
- if (substr($line, 0, 2) === '/*') {
64
- $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line);
65
- }
66
- }
67
-
68
- echo $inner . implode($glue, $block->lines);
69
-
70
- if (! empty($block->children)) {
71
- echo $this->break;
72
- }
73
- }
74
-
75
- /**
76
- * {@inheritdoc}
77
- */
78
- protected function blockSelectors(OutputBlock $block)
79
- {
80
- $inner = $this->indentStr();
81
-
82
- echo $inner
83
- . implode($this->tagSeparator, $block->selectors)
84
- . $this->open . $this->break;
85
- }
86
-
87
- /**
88
- * {@inheritdoc}
89
- */
90
- protected function blockChildren(OutputBlock $block)
91
- {
92
- foreach ($block->children as $i => $child) {
93
- $this->block($child);
94
-
95
- if ($i < count($block->children) - 1) {
96
- echo $this->break;
97
-
98
- if (isset($block->children[$i + 1])) {
99
- $next = $block->children[$i + 1];
100
-
101
- if ($next->depth === max($block->depth, 1) && $child->depth >= $next->depth) {
102
- echo $this->break;
103
- }
104
- }
105
- }
106
- }
107
- }
108
-
109
- /**
110
- * {@inheritdoc}
111
- */
112
- protected function block(OutputBlock $block)
113
- {
114
- if ($block->type === 'root') {
115
- $this->adjustAllChildren($block);
116
- }
117
-
118
- if (empty($block->lines) && empty($block->children)) {
119
- return;
120
- }
121
-
122
- $this->depth = $block->depth;
123
-
124
- if (! empty($block->selectors)) {
125
- $this->blockSelectors($block);
126
-
127
- $this->indentLevel++;
128
- }
129
-
130
- if (! empty($block->lines)) {
131
- $this->blockLines($block);
132
- }
133
-
134
- if (! empty($block->children)) {
135
- $this->blockChildren($block);
136
- }
137
-
138
- if (! empty($block->selectors)) {
139
- $this->indentLevel--;
140
-
141
- echo $this->close;
142
- }
143
-
144
- if ($block->type === 'root') {
145
- echo $this->break;
146
- }
147
- }
148
-
149
- /**
150
- * Adjust the depths of all children, depth first
151
- *
152
- * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
153
- */
154
- private function adjustAllChildren(OutputBlock $block)
155
- {
156
- // flatten empty nested blocks
157
- $children = array();
158
-
159
- foreach ($block->children as $i => $child) {
160
- if (empty($child->lines) && empty($child->children)) {
161
- if (isset($block->children[$i + 1])) {
162
- $block->children[$i + 1]->depth = $child->depth;
163
- }
164
-
165
- continue;
166
- }
167
-
168
- $children[] = $child;
169
- }
170
-
171
- $count = count($children);
172
-
173
- for ($i = 0; $i < $count; $i++) {
174
- $depth = $children[$i]->depth;
175
- $j = $i + 1;
176
-
177
- if (isset($children[$j]) && $depth < $children[$j]->depth) {
178
- $childDepth = $children[$j]->depth;
179
-
180
- for (; $j < $count; $j++) {
181
- if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) {
182
- $children[$j]->depth = $depth + 1;
183
- }
184
- }
185
- }
186
- }
187
-
188
- $block->children = $children;
189
-
190
- // make relative to parent
191
- foreach ($block->children as $child) {
192
- $this->adjustAllChildren($child);
193
-
194
- $child->depth = $child->depth - $block->depth;
195
- }
196
- }
197
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Formatter/OutputBlock.php DELETED
@@ -1,50 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp\Formatter;
13
-
14
- /**
15
- * SCSS output block
16
- *
17
- * @author Anthon Pang <anthon.pang@gmail.com>
18
- */
19
- class OutputBlock
20
- {
21
- /**
22
- * @var string
23
- */
24
- public $type;
25
-
26
- /**
27
- * @var integer
28
- */
29
- public $depth;
30
-
31
- /**
32
- * @var array
33
- */
34
- public $selectors;
35
-
36
- /**
37
- * @var array
38
- */
39
- public $lines;
40
-
41
- /**
42
- * @var array
43
- */
44
- public $children;
45
-
46
- /**
47
- * @var \Leafo\ScssPhp\Formatter\OutputBlock
48
- */
49
- public $parent;
50
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Node.php DELETED
@@ -1,35 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp;
13
-
14
- /**
15
- * SCSS node
16
- *
17
- * @author Anthon Pang <anthon.pang@gmail.com>
18
- */
19
- abstract class Node
20
- {
21
- /**
22
- * @var string
23
- */
24
- public $type;
25
-
26
- /**
27
- * @var integer
28
- */
29
- public $sourcePosition;
30
-
31
- /**
32
- * @var integer
33
- */
34
- public $sourceIndex;
35
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Node/Number.php DELETED
@@ -1,236 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp\Node;
13
-
14
- use Leafo\ScssPhp\Node;
15
- use Leafo\ScssPhp\Type;
16
-
17
- /**
18
- * SCSS dimension + optional units
19
- *
20
- * {@internal
21
- * This is a work-in-progress.
22
- *
23
- * The \ArrayAccess interface is temporary until the migration is complete.
24
- * }}
25
- *
26
- * @author Anthon Pang <anthon.pang@gmail.com>
27
- */
28
- class Number extends Node implements \ArrayAccess
29
- {
30
- /**
31
- * @var integer
32
- */
33
- static public $precision = 5;
34
-
35
- /**
36
- * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/
37
- *
38
- * @var array
39
- */
40
- static protected $unitTable = array(
41
- 'in' => array(
42
- 'in' => 1,
43
- 'pc' => 6,
44
- 'pt' => 72,
45
- 'px' => 96,
46
- 'cm' => 2.54,
47
- 'mm' => 25.4,
48
- 'q' => 101.6,
49
- ),
50
- 'turn' => array(
51
- 'deg' => 180,
52
- 'grad' => 200,
53
- 'rad' => M_PI,
54
- 'turn' => 0.5,
55
- ),
56
- 's' => array(
57
- 's' => 1,
58
- 'ms' => 1000,
59
- ),
60
- 'Hz' => array(
61
- 'Hz' => 1,
62
- 'kHz' => 0.001,
63
- ),
64
- 'dpi' => array(
65
- 'dpi' => 1,
66
- 'dpcm' => 2.54,
67
- 'dppx' => 96,
68
- ),
69
- );
70
-
71
- /**
72
- * @var integer|float
73
- */
74
- public $dimension;
75
-
76
- /**
77
- * @var string
78
- */
79
- public $units;
80
-
81
- /**
82
- * Initialize number
83
- *
84
- * @param mixed $dimension
85
- * @param string $initialUnit
86
- */
87
- public function __construct($dimension, $initialUnit)
88
- {
89
- $this->type = Type::T_NUMBER;
90
- $this->dimension = $dimension;
91
- $this->units = $initialUnit;
92
- }
93
-
94
- /**
95
- * Coerce number to target units
96
- *
97
- * @param array $units
98
- *
99
- * @return \Leafo\ScssPhp\Node\Number
100
- */
101
- public function coerce($units)
102
- {
103
- $value = $this->dimension;
104
-
105
- if (isset(self::$unitTable[$this->units][$units])) {
106
- $value *= self::$unitTable[$this->units][$units];
107
- }
108
-
109
- return new Number($value, $units);
110
- }
111
-
112
- /**
113
- * Normalize number
114
- *
115
- * @return \Leafo\ScssPhp\Node\Number
116
- */
117
- public function normalize()
118
- {
119
- if (isset(self::$unitTable['in'][$this->units])) {
120
- $conv = self::$unitTable['in'][$this->units];
121
-
122
- return new Number($this->dimension / $conv, 'in');
123
- }
124
-
125
- return new Number($this->dimension, $this->units);
126
- }
127
-
128
- /**
129
- * {@inheritdoc}
130
- */
131
- public function offsetExists($offset)
132
- {
133
- if ($offset === -2) {
134
- return $sourceIndex !== null;
135
- }
136
-
137
- if ($offset === -1
138
- || $offset === 0
139
- || $offset === 1
140
- || $offset === 2
141
- ) {
142
- return true;
143
- }
144
-
145
- return false;
146
- }
147
-
148
- /**
149
- * {@inheritdoc}
150
- */
151
- public function offsetGet($offset)
152
- {
153
- switch ($offset) {
154
- case -2:
155
- return $this->sourceIndex;
156
-
157
- case -1:
158
- return $this->sourcePosition;
159
-
160
- case 0:
161
- return $this->type;
162
-
163
- case 1:
164
- return $this->dimension;
165
-
166
- case 2:
167
- return $this->units;
168
- }
169
- }
170
-
171
- /**
172
- * {@inheritdoc}
173
- */
174
- public function offsetSet($offset, $value)
175
- {
176
- if ($offset === 1) {
177
- $this->dimension = $value;
178
- } elseif ($offset === 2) {
179
- $this->units = $value;
180
- } elseif ($offset == -1) {
181
- $this->sourcePosition = $value;
182
- } elseif ($offset == -2) {
183
- $this->sourceIndex = $value;
184
- }
185
- }
186
-
187
- /**
188
- * {@inheritdoc}
189
- */
190
- public function offsetUnset($offset)
191
- {
192
- if ($offset === 1) {
193
- $this->dimension = null;
194
- } elseif ($offset === 2) {
195
- $this->units = null;
196
- } elseif ($offset === -1) {
197
- $this->sourcePosition = null;
198
- } elseif ($offset === -2) {
199
- $this->sourceIndex = null;
200
- }
201
- }
202
-
203
- /**
204
- * Returns true if the number is unitless
205
- *
206
- * @return boolean
207
- */
208
- public function unitless()
209
- {
210
- return empty($this->units);
211
- }
212
-
213
- /**
214
- * Returns unit(s) as the product of numerator units divided by the product of denominator units
215
- *
216
- * @return string
217
- */
218
- public function unitStr()
219
- {
220
- return $this->units;
221
- }
222
-
223
- /**
224
- * {@inheritdoc}
225
- */
226
- public function __toString()
227
- {
228
- $value = round($this->dimension, self::$precision);
229
-
230
- if (empty($this->units)) {
231
- return (string) $value;
232
- }
233
-
234
- return (string) $value . $this->units;
235
- }
236
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Parser.php DELETED
@@ -1,2377 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp;
13
-
14
- use Leafo\ScssPhp\Block;
15
- use Leafo\ScssPhp\Compiler;
16
- use Leafo\ScssPhp\Node;
17
- use Leafo\ScssPhp\Type;
18
-
19
- /**
20
- * SCSS parser
21
- *
22
- * @author Leaf Corcoran <leafot@gmail.com>
23
- */
24
- class Parser
25
- {
26
- const SOURCE_INDEX = -1;
27
- const SOURCE_POSITION = -2;
28
-
29
- /**
30
- * @var array
31
- */
32
- protected static $precedence = array(
33
- '=' => 0,
34
- 'or' => 1,
35
- 'and' => 2,
36
- '==' => 3,
37
- '!=' => 3,
38
- '<=>' => 3,
39
- '<=' => 4,
40
- '>=' => 4,
41
- '<' => 4,
42
- '>' => 4,
43
- '+' => 5,
44
- '-' => 5,
45
- '*' => 6,
46
- '/' => 6,
47
- '%' => 6,
48
- );
49
-
50
- protected static $commentPattern;
51
- protected static $operatorPattern;
52
- protected static $whitePattern;
53
-
54
- private $sourceName;
55
- private $sourceIndex;
56
- private $charset;
57
- private $count;
58
- private $env;
59
- private $inParens;
60
- private $eatWhiteDefault;
61
- private $buffer;
62
-
63
- /**
64
- * Constructor
65
- *
66
- * @api
67
- *
68
- * @param string $sourceName
69
- * @param integer $sourceIndex
70
- */
71
- public function __construct($sourceName, $sourceIndex = 0)
72
- {
73
- $this->sourceName = $sourceName ?: '(stdin)';
74
- $this->sourceIndex = $sourceIndex;
75
- $this->charset = null;
76
-
77
- if (empty(self::$operatorPattern)) {
78
- self::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
79
-
80
- $commentSingle = '\/\/';
81
- $commentMultiLeft = '\/\*';
82
- $commentMultiRight = '\*\/';
83
-
84
- self::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight;
85
- self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentPattern . ')\s*|\s+/Ais';
86
- }
87
- }
88
-
89
- /**
90
- * Get source file name
91
- *
92
- * @api
93
- *
94
- * @return string
95
- */
96
- public function getSourceName()
97
- {
98
- return $this->sourceName;
99
- }
100
-
101
- /**
102
- * Get source line number (given character position in the buffer)
103
- *
104
- * @api
105
- *
106
- * @param integer $pos
107
- *
108
- * @return integer
109
- */
110
- public function getLineNo($pos)
111
- {
112
- return 1 + substr_count(substr($this->buffer, 0, $pos), "\n");
113
- }
114
-
115
- /**
116
- * Throw parser error
117
- *
118
- * @api
119
- *
120
- * @param string $msg
121
- * @param integer $count
122
- *
123
- * @throws \Exception
124
- */
125
- public function throwParseError($msg = 'parse error', $count = null)
126
- {
127
- $count = ! isset($count) ? $this->count : $count;
128
-
129
- $line = $this->getLineNo($count);
130
-
131
- if (! empty($this->sourceName)) {
132
- $loc = "$this->sourceName on line $line";
133
- } else {
134
- $loc = "line: $line";
135
- }
136
-
137
- if ($this->peek("(.*?)(\n|$)", $m, $count)) {
138
- throw new \Exception("$msg: failed at `$m[1]` $loc");
139
- }
140
-
141
- throw new \Exception("$msg: $loc");
142
- }
143
-
144
- /**
145
- * Parser buffer
146
- *
147
- * @api
148
- *
149
- * @param string $buffer
150
- *
151
- * @return \Leafo\ScssPhp\Block
152
- */
153
- public function parse($buffer)
154
- {
155
- $this->count = 0;
156
- $this->env = null;
157
- $this->inParens = false;
158
- $this->eatWhiteDefault = true;
159
- $this->buffer = rtrim($buffer, "\x00..\x1f");
160
-
161
- $this->pushBlock(null); // root block
162
-
163
- $this->whitespace();
164
- $this->pushBlock(null);
165
- $this->popBlock();
166
-
167
- while ($this->parseChunk()) {
168
- ;
169
- }
170
-
171
- if ($this->count !== strlen($this->buffer)) {
172
- $this->throwParseError();
173
- }
174
-
175
- if (! empty($this->env->parent)) {
176
- $this->throwParseError('unclosed block');
177
- }
178
-
179
- if ($this->charset) {
180
- array_unshift($this->env->children, $this->charset);
181
- }
182
-
183
- $this->env->isRoot = true;
184
-
185
- return $this->env;
186
- }
187
-
188
- /**
189
- * Parse a value or value list
190
- *
191
- * @api
192
- *
193
- * @param string $buffer
194
- * @param string $out
195
- *
196
- * @return boolean
197
- */
198
- public function parseValue($buffer, &$out)
199
- {
200
- $this->count = 0;
201
- $this->env = null;
202
- $this->inParens = false;
203
- $this->eatWhiteDefault = true;
204
- $this->buffer = (string) $buffer;
205
-
206
- return $this->valueList($out);
207
- }
208
-
209
- /**
210
- * Parse a selector or selector list
211
- *
212
- * @api
213
- *
214
- * @param string $buffer
215
- * @param string $out
216
- *
217
- * @return boolean
218
- */
219
- public function parseSelector($buffer, &$out)
220
- {
221
- $this->count = 0;
222
- $this->env = null;
223
- $this->inParens = false;
224
- $this->eatWhiteDefault = true;
225
- $this->buffer = (string) $buffer;
226
-
227
- return $this->selectors($out);
228
- }
229
-
230
- /**
231
- * Parse a single chunk off the head of the buffer and append it to the
232
- * current parse environment.
233
- *
234
- * Returns false when the buffer is empty, or when there is an error.
235
- *
236
- * This function is called repeatedly until the entire document is
237
- * parsed.
238
- *
239
- * This parser is most similar to a recursive descent parser. Single
240
- * functions represent discrete grammatical rules for the language, and
241
- * they are able to capture the text that represents those rules.
242
- *
243
- * Consider the function Compiler::keyword(). (All parse functions are
244
- * structured the same.)
245
- *
246
- * The function takes a single reference argument. When calling the
247
- * function it will attempt to match a keyword on the head of the buffer.
248
- * If it is successful, it will place the keyword in the referenced
249
- * argument, advance the position in the buffer, and return true. If it
250
- * fails then it won't advance the buffer and it will return false.
251
- *
252
- * All of these parse functions are powered by Compiler::match(), which behaves
253
- * the same way, but takes a literal regular expression. Sometimes it is
254
- * more convenient to use match instead of creating a new function.
255
- *
256
- * Because of the format of the functions, to parse an entire string of
257
- * grammatical rules, you can chain them together using &&.
258
- *
259
- * But, if some of the rules in the chain succeed before one fails, then
260
- * the buffer position will be left at an invalid state. In order to
261
- * avoid this, Compiler::seek() is used to remember and set buffer positions.
262
- *
263
- * Before parsing a chain, use $s = $this->seek() to remember the current
264
- * position into $s. Then if a chain fails, use $this->seek($s) to
265
- * go back where we started.
266
- *
267
- * @return boolean
268
- */
269
- protected function parseChunk()
270
- {
271
- $s = $this->seek();
272
-
273
- // the directives
274
- if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
275
- if ($this->literal('@at-root') &&
276
- ($this->selectors($selector) || true) &&
277
- ($this->map($with) || true) &&
278
- $this->literal('{')
279
- ) {
280
- $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
281
- $atRoot->selector = $selector;
282
- $atRoot->with = $with;
283
-
284
- return true;
285
- }
286
-
287
- $this->seek($s);
288
-
289
- if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) {
290
- $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
291
- $media->queryList = $mediaQueryList[2];
292
-
293
- return true;
294
- }
295
-
296
- $this->seek($s);
297
-
298
- if ($this->literal('@mixin') &&
299
- $this->keyword($mixinName) &&
300
- ($this->argumentDef($args) || true) &&
301
- $this->literal('{')
302
- ) {
303
- $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
304
- $mixin->name = $mixinName;
305
- $mixin->args = $args;
306
-
307
- return true;
308
- }
309
-
310
- $this->seek($s);
311
-
312
- if ($this->literal('@include') &&
313
- $this->keyword($mixinName) &&
314
- ($this->literal('(') &&
315
- ($this->argValues($argValues) || true) &&
316
- $this->literal(')') || true) &&
317
- ($this->end() ||
318
- $this->literal('{') && $hasBlock = true)
319
- ) {
320
- $child = array(Type::T_INCLUDE,
321
- $mixinName, isset($argValues) ? $argValues : null, null);
322
-
323
- if (! empty($hasBlock)) {
324
- $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
325
- $include->child = $child;
326
- } else {
327
- $this->append($child, $s);
328
- }
329
-
330
- return true;
331
- }
332
-
333
- $this->seek($s);
334
-
335
- if ($this->literal('@import') &&
336
- $this->valueList($importPath) &&
337
- $this->end()
338
- ) {
339
- $this->append(array(Type::T_IMPORT, $importPath), $s);
340
-
341
- return true;
342
- }
343
-
344
- $this->seek($s);
345
-
346
- if ($this->literal('@import') &&
347
- $this->url($importPath) &&
348
- $this->end()
349
- ) {
350
- $this->append(array(Type::T_IMPORT, $importPath), $s);
351
-
352
- return true;
353
- }
354
-
355
- $this->seek($s);
356
-
357
- if ($this->literal('@extend') &&
358
- $this->selectors($selector) &&
359
- $this->end()
360
- ) {
361
- $this->append(array(Type::T_EXTEND, $selector), $s);
362
-
363
- return true;
364
- }
365
-
366
- $this->seek($s);
367
-
368
- if ($this->literal('@function') &&
369
- $this->keyword($fnName) &&
370
- $this->argumentDef($args) &&
371
- $this->literal('{')
372
- ) {
373
- $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
374
- $func->name = $fnName;
375
- $func->args = $args;
376
-
377
- return true;
378
- }
379
-
380
- $this->seek($s);
381
-
382
- if ($this->literal('@break') && $this->end()) {
383
- $this->append(array(Type::T_BREAK), $s);
384
-
385
- return true;
386
- }
387
-
388
- $this->seek($s);
389
-
390
- if ($this->literal('@continue') && $this->end()) {
391
- $this->append(array(Type::T_CONTINUE), $s);
392
-
393
- return true;
394
- }
395
-
396
- $this->seek($s);
397
-
398
-
399
- if ($this->literal('@return') && ($this->valueList($retVal) || true) && $this->end()) {
400
- $this->append(array(Type::T_RETURN, isset($retVal) ? $retVal : array(Type::T_NULL)), $s);
401
-
402
- return true;
403
- }
404
-
405
- $this->seek($s);
406
-
407
- if ($this->literal('@each') &&
408
- $this->genericList($varNames, 'variable', ',', false) &&
409
- $this->literal('in') &&
410
- $this->valueList($list) &&
411
- $this->literal('{')
412
- ) {
413
- $each = $this->pushSpecialBlock(Type::T_EACH, $s);
414
-
415
- foreach ($varNames[2] as $varName) {
416
- $each->vars[] = $varName[1];
417
- }
418
-
419
- $each->list = $list;
420
-
421
- return true;
422
- }
423
-
424
- $this->seek($s);
425
-
426
- if ($this->literal('@while') &&
427
- $this->expression($cond) &&
428
- $this->literal('{')
429
- ) {
430
- $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
431
- $while->cond = $cond;
432
-
433
- return true;
434
- }
435
-
436
- $this->seek($s);
437
-
438
- if ($this->literal('@for') &&
439
- $this->variable($varName) &&
440
- $this->literal('from') &&
441
- $this->expression($start) &&
442
- ($this->literal('through') ||
443
- ($forUntil = true && $this->literal('to'))) &&
444
- $this->expression($end) &&
445
- $this->literal('{')
446
- ) {
447
- $for = $this->pushSpecialBlock(Type::T_FOR, $s);
448
- $for->var = $varName[1];
449
- $for->start = $start;
450
- $for->end = $end;
451
- $for->until = isset($forUntil);
452
-
453
- return true;
454
- }
455
-
456
- $this->seek($s);
457
-
458
- if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) {
459
- $if = $this->pushSpecialBlock(Type::T_IF, $s);
460
- $if->cond = $cond;
461
- $if->cases = array();
462
-
463
- return true;
464
- }
465
-
466
- $this->seek($s);
467
-
468
- if ($this->literal('@debug') &&
469
- $this->valueList($value) &&
470
- $this->end()
471
- ) {
472
- $this->append(array(Type::T_DEBUG, $value), $s);
473
-
474
- return true;
475
- }
476
-
477
- $this->seek($s);
478
-
479
- if ($this->literal('@warn') &&
480
- $this->valueList($value) &&
481
- $this->end()
482
- ) {
483
- $this->append(array(Type::T_WARN, $value), $s);
484
-
485
- return true;
486
- }
487
-
488
- $this->seek($s);
489
-
490
- if ($this->literal('@error') &&
491
- $this->valueList($value) &&
492
- $this->end()
493
- ) {
494
- $this->append(array(Type::T_ERROR, $value), $s);
495
-
496
- return true;
497
- }
498
-
499
- $this->seek($s);
500
-
501
- if ($this->literal('@content') && $this->end()) {
502
- $this->append(array(Type::T_MIXIN_CONTENT), $s);
503
-
504
- return true;
505
- }
506
-
507
- $this->seek($s);
508
-
509
- $last = $this->last();
510
-
511
- if (isset($last) && $last[0] === Type::T_IF) {
512
- list(, $if) = $last;
513
-
514
- if ($this->literal('@else')) {
515
- if ($this->literal('{')) {
516
- $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
517
- } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) {
518
- $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
519
- $else->cond = $cond;
520
- }
521
-
522
- if (isset($else)) {
523
- $else->dontAppend = true;
524
- $if->cases[] = $else;
525
-
526
- return true;
527
- }
528
- }
529
-
530
- $this->seek($s);
531
- }
532
-
533
- // only retain the first @charset directive encountered
534
- if ($this->literal('@charset') &&
535
- $this->valueList($charset) &&
536
- $this->end()
537
- ) {
538
- if (! isset($this->charset)) {
539
- $statement = array(Type::T_CHARSET, $charset);
540
-
541
- $statement[self::SOURCE_POSITION] = $s;
542
- $statement[self::SOURCE_INDEX] = $this->sourceIndex;
543
-
544
- $this->charset = $statement;
545
- }
546
-
547
- return true;
548
- }
549
-
550
- $this->seek($s);
551
-
552
- // doesn't match built in directive, do generic one
553
- if ($this->literal('@', false) &&
554
- $this->keyword($dirName) &&
555
- ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
556
- $this->literal('{')
557
- ) {
558
- if ($dirName === 'media') {
559
- $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
560
- } else {
561
- $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
562
- $directive->name = $dirName;
563
- }
564
-
565
- if (isset($dirValue)) {
566
- $directive->value = $dirValue;
567
- }
568
-
569
- return true;
570
- }
571
-
572
- $this->seek($s);
573
-
574
- return false;
575
- }
576
-
577
- // property shortcut
578
- // captures most properties before having to parse a selector
579
- if ($this->keyword($name, false) &&
580
- $this->literal(': ') &&
581
- $this->valueList($value) &&
582
- $this->end()
583
- ) {
584
- $name = array(Type::T_STRING, '', array($name));
585
- $this->append(array(Type::T_ASSIGN, $name, $value), $s);
586
-
587
- return true;
588
- }
589
-
590
- $this->seek($s);
591
-
592
- // variable assigns
593
- if ($this->variable($name) &&
594
- $this->literal(':') &&
595
- $this->valueList($value) &&
596
- $this->end()
597
- ) {
598
- // check for '!flag'
599
- $assignmentFlag = $this->stripAssignmentFlag($value);
600
- $this->append(array(Type::T_ASSIGN, $name, $value, $assignmentFlag), $s);
601
-
602
- return true;
603
- }
604
-
605
- $this->seek($s);
606
-
607
- // misc
608
- if ($this->literal('-->')) {
609
- return true;
610
- }
611
-
612
- // opening css block
613
- if ($this->selectors($selectors) && $this->literal('{')) {
614
- $b = $this->pushBlock($selectors, $s);
615
-
616
- return true;
617
- }
618
-
619
- $this->seek($s);
620
-
621
- // property assign, or nested assign
622
- if ($this->propertyName($name) && $this->literal(':')) {
623
- $foundSomething = false;
624
-
625
- if ($this->valueList($value)) {
626
- $this->append(array(Type::T_ASSIGN, $name, $value), $s);
627
- $foundSomething = true;
628
- }
629
-
630
- if ($this->literal('{')) {
631
- $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
632
- $propBlock->prefix = $name;
633
- $foundSomething = true;
634
- } elseif ($foundSomething) {
635
- $foundSomething = $this->end();
636
- }
637
-
638
- if ($foundSomething) {
639
- return true;
640
- }
641
- }
642
-
643
- $this->seek($s);
644
-
645
- // closing a block
646
- if ($this->literal('}')) {
647
- $block = $this->popBlock();
648
-
649
- if (isset($block->type) && $block->type === Type::T_INCLUDE) {
650
- $include = $block->child;
651
- unset($block->child);
652
- $include[3] = $block;
653
- $this->append($include, $s);
654
- } elseif (empty($block->dontAppend)) {
655
- $type = isset($block->type) ? $block->type : Type::T_BLOCK;
656
- $this->append(array($type, $block), $s);
657
- }
658
-
659
- return true;
660
- }
661
-
662
- // extra stuff
663
- if ($this->literal(';') ||
664
- $this->literal('<!--')
665
- ) {
666
- return true;
667
- }
668
-
669
- return false;
670
- }
671
-
672
- /**
673
- * Push block onto parse tree
674
- *
675
- * @param array $selectors
676
- * @param integer $pos
677
- *
678
- * @return \Leafo\ScssPhp\Block
679
- */
680
- protected function pushBlock($selectors, $pos = 0)
681
- {
682
- $b = new Block;
683
- $b->parent = $this->env;
684
- $b->sourcePosition = $pos;
685
- $b->sourceIndex = $this->sourceIndex;
686
- $b->selectors = $selectors;
687
- $b->comments = array();
688
-
689
- if (! $this->env) {
690
- $b->children = array();
691
- } elseif (empty($this->env->children)) {
692
- $this->env->children = $this->env->comments;
693
- $b->children = array();
694
- $this->env->comments = array();
695
- } else {
696
- $b->children = $this->env->comments;
697
- $this->env->comments = array();
698
- }
699
-
700
- $this->env = $b;
701
-
702
- return $b;
703
- }
704
-
705
- /**
706
- * Push special (named) block onto parse tree
707
- *
708
- * @param string $type
709
- * @param integer $pos
710
- *
711
- * @return \Leafo\ScssPhp\Block
712
- */
713
- protected function pushSpecialBlock($type, $pos)
714
- {
715
- $block = $this->pushBlock(null, $pos);
716
- $block->type = $type;
717
-
718
- return $block;
719
- }
720
-
721
- /**
722
- * Pop scope and return last block
723
- *
724
- * @return \Leafo\ScssPhp\Block
725
- *
726
- * @throws \Exception
727
- */
728
- protected function popBlock()
729
- {
730
- $block = $this->env;
731
-
732
- if (empty($block->parent)) {
733
- $this->throwParseError('unexpected }');
734
- }
735
-
736
- $this->env = $block->parent;
737
- unset($block->parent);
738
-
739
- $comments = $block->comments;
740
- if (count($comments)) {
741
- $this->env->comments = $comments;
742
- unset($block->comments);
743
- }
744
-
745
- return $block;
746
- }
747
-
748
- /**
749
- * Peek input stream
750
- *
751
- * @param string $regex
752
- * @param array $out
753
- * @param integer $from
754
- *
755
- * @return integer
756
- */
757
- protected function peek($regex, &$out, $from = null)
758
- {
759
- if (! isset($from)) {
760
- $from = $this->count;
761
- }
762
-
763
- $r = '/' . $regex . '/Ais';
764
- $result = preg_match($r, $this->buffer, $out, null, $from);
765
-
766
- return $result;
767
- }
768
-
769
- /**
770
- * Seek to position in input stream (or return current position in input stream)
771
- *
772
- * @param integer $where
773
- *
774
- * @return integer
775
- */
776
- protected function seek($where = null)
777
- {
778
- if ($where === null) {
779
- return $this->count;
780
- }
781
-
782
- $this->count = $where;
783
-
784
- return true;
785
- }
786
-
787
- /**
788
- * Match string looking for either ending delim, escape, or string interpolation
789
- *
790
- * {@internal This is a workaround for preg_match's 250K string match limit. }}
791
- *
792
- * @param array $m Matches (passed by reference)
793
- * @param string $delim Delimeter
794
- *
795
- * @return boolean True if match; false otherwise
796
- */
797
- protected function matchString(&$m, $delim)
798
- {
799
- $token = null;
800
-
801
- $end = strlen($this->buffer);
802
-
803
- // look for either ending delim, escape, or string interpolation
804
- foreach (array('#{', '\\', $delim) as $lookahead) {
805
- $pos = strpos($this->buffer, $lookahead, $this->count);
806
-
807
- if ($pos !== false && $pos < $end) {
808
- $end = $pos;
809
- $token = $lookahead;
810
- }
811
- }
812
-
813
- if (! isset($token)) {
814
- return false;
815
- }
816
-
817
- $match = substr($this->buffer, $this->count, $end - $this->count);
818
- $m = array(
819
- $match . $token,
820
- $match,
821
- $token
822
- );
823
- $this->count = $end + strlen($token);
824
-
825
- return true;
826
- }
827
-
828
- /**
829
- * Try to match something on head of buffer
830
- *
831
- * @param string $regex
832
- * @param array $out
833
- * @param boolean $eatWhitespace
834
- *
835
- * @return boolean
836
- */
837
- protected function match($regex, &$out, $eatWhitespace = null)
838
- {
839
- if (! isset($eatWhitespace)) {
840
- $eatWhitespace = $this->eatWhiteDefault;
841
- }
842
-
843
- $r = '/' . $regex . '/Ais';
844
-
845
- if (preg_match($r, $this->buffer, $out, null, $this->count)) {
846
- $this->count += strlen($out[0]);
847
-
848
- if ($eatWhitespace) {
849
- $this->whitespace();
850
- }
851
-
852
- return true;
853
- }
854
-
855
- return false;
856
- }
857
-
858
- /**
859
- * Match literal string
860
- *
861
- * @param string $what
862
- * @param boolean $eatWhitespace
863
- *
864
- * @return boolean
865
- */
866
- protected function literal($what, $eatWhitespace = null)
867
- {
868
- if (! isset($eatWhitespace)) {
869
- $eatWhitespace = $this->eatWhiteDefault;
870
- }
871
-
872
- // shortcut on single letter
873
- if (! isset($what[1]) && isset($this->buffer[$this->count])) {
874
- if ($this->buffer[$this->count] === $what) {
875
- if (! $eatWhitespace) {
876
- $this->count++;
877
-
878
- return true;
879
- }
880
-
881
- // goes below...
882
- } else {
883
- return false;
884
- }
885
- }
886
-
887
- return $this->match($this->pregQuote($what), $m, $eatWhitespace);
888
- }
889
-
890
- /**
891
- * Match some whitespace
892
- *
893
- * @return boolean
894
- */
895
- protected function whitespace()
896
- {
897
- $gotWhite = false;
898
-
899
- while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
900
- if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
901
- $this->appendComment(array(Type::T_COMMENT, $m[1]));
902
-
903
- $this->commentsSeen[$this->count] = true;
904
- }
905
-
906
- $this->count += strlen($m[0]);
907
- $gotWhite = true;
908
- }
909
-
910
- return $gotWhite;
911
- }
912
-
913
- /**
914
- * Append comment to current block
915
- *
916
- * @param array $comment
917
- */
918
- protected function appendComment($comment)
919
- {
920
- $comment[1] = substr(preg_replace(array('/^\s+/m', '/^(.)/m'), array('', ' \1'), $comment[1]), 1);
921
-
922
- $this->env->comments[] = $comment;
923
- }
924
-
925
- /**
926
- * Append statement to current block
927
- *
928
- * @param array $statement
929
- * @param integer $pos
930
- */
931
- protected function append($statement, $pos = null)
932
- {
933
- if ($pos !== null) {
934
- $statement[self::SOURCE_POSITION] = $pos;
935
- $statement[self::SOURCE_INDEX] = $this->sourceIndex;
936
- }
937
-
938
- $this->env->children[] = $statement;
939
-
940
- $comments = $this->env->comments;
941
-
942
- if (count($comments)) {
943
- $this->env->children = array_merge($this->env->children, $comments);
944
- $this->env->comments = array();
945
- }
946
- }
947
-
948
- /**
949
- * Returns last child was appended
950
- *
951
- * @return array|null
952
- */
953
- protected function last()
954
- {
955
- $i = count($this->env->children) - 1;
956
-
957
- if (isset($this->env->children[$i])) {
958
- return $this->env->children[$i];
959
- }
960
- }
961
-
962
- /**
963
- * Parse media query list
964
- *
965
- * @param array $out
966
- *
967
- * @return boolean
968
- */
969
- protected function mediaQueryList(&$out)
970
- {
971
- return $this->genericList($out, 'mediaQuery', ',', false);
972
- }
973
-
974
- /**
975
- * Parse media query
976
- *
977
- * @param array $out
978
- *
979
- * @return boolean
980
- */
981
- protected function mediaQuery(&$out)
982
- {
983
- $s = $this->seek();
984
-
985
- $expressions = null;
986
- $parts = array();
987
-
988
- if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) &&
989
- $this->mixedKeyword($mediaType)
990
- ) {
991
- $prop = array(Type::T_MEDIA_TYPE);
992
-
993
- if (isset($only)) {
994
- $prop[] = array(Type::T_KEYWORD, 'only');
995
- }
996
-
997
- if (isset($not)) {
998
- $prop[] = array(Type::T_KEYWORD, 'not');
999
- }
1000
-
1001
- $media = array(Type::T_LIST, '', array());
1002
-
1003
- foreach ((array)$mediaType as $type) {
1004
- if (is_array($type)) {
1005
- $media[2][] = $type;
1006
- } else {
1007
- $media[2][] = array(Type::T_KEYWORD, $type);
1008
- }
1009
- }
1010
-
1011
- $prop[] = $media;
1012
- $parts[] = $prop;
1013
- }
1014
-
1015
- if (empty($parts) || $this->literal('and')) {
1016
- $this->genericList($expressions, 'mediaExpression', 'and', false);
1017
-
1018
- if (is_array($expressions)) {
1019
- $parts = array_merge($parts, $expressions[2]);
1020
- }
1021
- }
1022
-
1023
- $out = $parts;
1024
-
1025
- return true;
1026
- }
1027
-
1028
- /**
1029
- * Parse media expression
1030
- *
1031
- * @param array $out
1032
- *
1033
- * @return boolean
1034
- */
1035
- protected function mediaExpression(&$out)
1036
- {
1037
- $s = $this->seek();
1038
- $value = null;
1039
-
1040
- if ($this->literal('(') &&
1041
- $this->expression($feature) &&
1042
- ($this->literal(':') && $this->expression($value) || true) &&
1043
- $this->literal(')')
1044
- ) {
1045
- $out = array(Type::T_MEDIA_EXPRESSION, $feature);
1046
-
1047
- if ($value) {
1048
- $out[] = $value;
1049
- }
1050
-
1051
- return true;
1052
- }
1053
-
1054
- $this->seek($s);
1055
-
1056
- return false;
1057
- }
1058
-
1059
- /**
1060
- * Parse argument values
1061
- *
1062
- * @param array $out
1063
- *
1064
- * @return boolean
1065
- */
1066
- protected function argValues(&$out)
1067
- {
1068
- if ($this->genericList($list, 'argValue', ',', false)) {
1069
- $out = $list[2];
1070
-
1071
- return true;
1072
- }
1073
-
1074
- return false;
1075
- }
1076
-
1077
- /**
1078
- * Parse argument value
1079
- *
1080
- * @param array $out
1081
- *
1082
- * @return boolean
1083
- */
1084
- protected function argValue(&$out)
1085
- {
1086
- $s = $this->seek();
1087
-
1088
- $keyword = null;
1089
-
1090
- if (! $this->variable($keyword) || ! $this->literal(':')) {
1091
- $this->seek($s);
1092
- $keyword = null;
1093
- }
1094
-
1095
- if ($this->genericList($value, 'expression')) {
1096
- $out = array($keyword, $value, false);
1097
- $s = $this->seek();
1098
-
1099
- if ($this->literal('...')) {
1100
- $out[2] = true;
1101
- } else {
1102
- $this->seek($s);
1103
- }
1104
-
1105
- return true;
1106
- }
1107
-
1108
- return false;
1109
- }
1110
-
1111
- /**
1112
- * Parse comma separated value list
1113
- *
1114
- * @param string $out
1115
- *
1116
- * @return boolean
1117
- */
1118
- protected function valueList(&$out)
1119
- {
1120
- return $this->genericList($out, 'spaceList', ',');
1121
- }
1122
-
1123
- /**
1124
- * Parse space separated value list
1125
- *
1126
- * @param array $out
1127
- *
1128
- * @return boolean
1129
- */
1130
- protected function spaceList(&$out)
1131
- {
1132
- return $this->genericList($out, 'expression');
1133
- }
1134
-
1135
- /**
1136
- * Parse generic list
1137
- *
1138
- * @param array $out
1139
- * @param callable $parseItem
1140
- * @param string $delim
1141
- * @param boolean $flatten
1142
- *
1143
- * @return boolean
1144
- */
1145
- protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
1146
- {
1147
- $s = $this->seek();
1148
- $items = array();
1149
-
1150
- while ($this->$parseItem($value)) {
1151
- $items[] = $value;
1152
-
1153
- if ($delim) {
1154
- if (! $this->literal($delim)) {
1155
- break;
1156
- }
1157
- }
1158
- }
1159
-
1160
- if (count($items) === 0) {
1161
- $this->seek($s);
1162
-
1163
- return false;
1164
- }
1165
-
1166
- if ($flatten && count($items) === 1) {
1167
- $out = $items[0];
1168
- } else {
1169
- $out = array(Type::T_LIST, $delim, $items);
1170
- }
1171
-
1172
- return true;
1173
- }
1174
-
1175
- /**
1176
- * Parse expression
1177
- *
1178
- * @param array $out
1179
- *
1180
- * @return boolean
1181
- */
1182
- protected function expression(&$out)
1183
- {
1184
- $s = $this->seek();
1185
-
1186
- if ($this->literal('(')) {
1187
- if ($this->literal(')')) {
1188
- $out = array(Type::T_LIST, '', array());
1189
-
1190
- return true;
1191
- }
1192
-
1193
- if ($this->valueList($out) && $this->literal(')') && $out[0] === Type::T_LIST) {
1194
- return true;
1195
- }
1196
-
1197
- $this->seek($s);
1198
-
1199
- if ($this->map($out)) {
1200
- return true;
1201
- }
1202
-
1203
- $this->seek($s);
1204
- }
1205
-
1206
- if ($this->value($lhs)) {
1207
- $out = $this->expHelper($lhs, 0);
1208
-
1209
- return true;
1210
- }
1211
-
1212
- return false;
1213
- }
1214
-
1215
- /**
1216
- * Parse left-hand side of subexpression
1217
- *
1218
- * @param array $lhs
1219
- * @param integer $minP
1220
- *
1221
- * @return array
1222
- */
1223
- protected function expHelper($lhs, $minP)
1224
- {
1225
- $operators = self::$operatorPattern;
1226
-
1227
- $ss = $this->seek();
1228
- $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1229
- ctype_space($this->buffer[$this->count - 1]);
1230
-
1231
- while ($this->match($operators, $m, false) && self::$precedence[$m[1]] >= $minP) {
1232
- $whiteAfter = isset($this->buffer[$this->count]) &&
1233
- ctype_space($this->buffer[$this->count]);
1234
- $varAfter = isset($this->buffer[$this->count]) &&
1235
- $this->buffer[$this->count] === '$';
1236
-
1237
- $this->whitespace();
1238
-
1239
- $op = $m[1];
1240
-
1241
- // don't turn negative numbers into expressions
1242
- if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) {
1243
- break;
1244
- }
1245
-
1246
- if (! $this->value($rhs)) {
1247
- break;
1248
- }
1249
-
1250
- // peek and see if rhs belongs to next operator
1251
- if ($this->peek($operators, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
1252
- $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
1253
- }
1254
-
1255
- $lhs = array(Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter);
1256
- $ss = $this->seek();
1257
- $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1258
- ctype_space($this->buffer[$this->count - 1]);
1259
- }
1260
-
1261
- $this->seek($ss);
1262
-
1263
- return $lhs;
1264
- }
1265
-
1266
- /**
1267
- * Parse value
1268
- *
1269
- * @param array $out
1270
- *
1271
- * @return boolean
1272
- */
1273
- protected function value(&$out)
1274
- {
1275
- $s = $this->seek();
1276
-
1277
- if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) {
1278
- $out = array(Type::T_UNARY, 'not', $inner, $this->inParens);
1279
-
1280
- return true;
1281
- }
1282
-
1283
- $this->seek($s);
1284
-
1285
- if ($this->literal('not', false) && $this->parenValue($inner)) {
1286
- $out = array(Type::T_UNARY, 'not', $inner, $this->inParens);
1287
-
1288
- return true;
1289
- }
1290
-
1291
- $this->seek($s);
1292
-
1293
- if ($this->literal('+') && $this->value($inner)) {
1294
- $out = array(Type::T_UNARY, '+', $inner, $this->inParens);
1295
-
1296
- return true;
1297
- }
1298
-
1299
- $this->seek($s);
1300
-
1301
- // negation
1302
- if ($this->literal('-', false) &&
1303
- ($this->variable($inner) ||
1304
- $this->unit($inner) ||
1305
- $this->parenValue($inner))
1306
- ) {
1307
- $out = array(Type::T_UNARY, '-', $inner, $this->inParens);
1308
-
1309
- return true;
1310
- }
1311
-
1312
- $this->seek($s);
1313
-
1314
- if ($this->parenValue($out) ||
1315
- $this->interpolation($out) ||
1316
- $this->variable($out) ||
1317
- $this->color($out) ||
1318
- $this->unit($out) ||
1319
- $this->string($out) ||
1320
- $this->func($out) ||
1321
- $this->progid($out)
1322
- ) {
1323
- return true;
1324
- }
1325
-
1326
- if ($this->keyword($keyword)) {
1327
- if ($keyword === 'null') {
1328
- $out = array(Type::T_NULL);
1329
- } else {
1330
- $out = array(Type::T_KEYWORD, $keyword);
1331
- }
1332
-
1333
- return true;
1334
- }
1335
-
1336
- return false;
1337
- }
1338
-
1339
- /**
1340
- * Parse parenthesized value
1341
- *
1342
- * @param array $out
1343
- *
1344
- * @return boolean
1345
- */
1346
- protected function parenValue(&$out)
1347
- {
1348
- $s = $this->seek();
1349
-
1350
- $inParens = $this->inParens;
1351
-
1352
- if ($this->literal('(')) {
1353
- if ($this->literal(')')) {
1354
- $out = array(Type::T_LIST, '', array());
1355
-
1356
- return true;
1357
- }
1358
-
1359
- $this->inParens = true;
1360
-
1361
- if ($this->expression($exp) && $this->literal(')')) {
1362
- $out = $exp;
1363
- $this->inParens = $inParens;
1364
-
1365
- return true;
1366
- }
1367
- }
1368
-
1369
- $this->inParens = $inParens;
1370
- $this->seek($s);
1371
-
1372
- return false;
1373
- }
1374
-
1375
- /**
1376
- * Parse "progid:"
1377
- *
1378
- * @param array $out
1379
- *
1380
- * @return boolean
1381
- */
1382
- protected function progid(&$out)
1383
- {
1384
- $s = $this->seek();
1385
-
1386
- if ($this->literal('progid:', false) &&
1387
- $this->openString('(', $fn) &&
1388
- $this->literal('(')
1389
- ) {
1390
- $this->openString(')', $args, '(');
1391
-
1392
- if ($this->literal(')')) {
1393
- $out = array(Type::T_STRING, '', array(
1394
- 'progid:', $fn, '(', $args, ')'
1395
- ));
1396
-
1397
- return true;
1398
- }
1399
- }
1400
-
1401
- $this->seek($s);
1402
-
1403
- return false;
1404
- }
1405
-
1406
- /**
1407
- * Parse function call
1408
- *
1409
- * @param array $out
1410
- *
1411
- * @return boolean
1412
- */
1413
- protected function func(&$func)
1414
- {
1415
- $s = $this->seek();
1416
-
1417
- if ($this->keyword($name, false) &&
1418
- $this->literal('(')
1419
- ) {
1420
- if ($name === 'alpha' && $this->argumentList($args)) {
1421
- $func = array(Type::T_FUNCTION, $name, array(Type::T_STRING, '', $args));
1422
-
1423
- return true;
1424
- }
1425
-
1426
- if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
1427
- $ss = $this->seek();
1428
-
1429
- if ($this->argValues($args) && $this->literal(')')) {
1430
- $func = array(Type::T_FUNCTION_CALL, $name, $args);
1431
-
1432
- return true;
1433
- }
1434
-
1435
- $this->seek($ss);
1436
- }
1437
-
1438
- if (($this->openString(')', $str, '(') || true ) &&
1439
- $this->literal(')')
1440
- ) {
1441
- $args = array();
1442
-
1443
- if (! empty($str)) {
1444
- $args[] = array(null, array(Type::T_STRING, '', array($str)));
1445
- }
1446
-
1447
- $func = array(Type::T_FUNCTION_CALL, $name, $args);
1448
-
1449
- return true;
1450
- }
1451
- }
1452
-
1453
- $this->seek($s);
1454
-
1455
- return false;
1456
- }
1457
-
1458
- /**
1459
- * Parse function call argument list
1460
- *
1461
- * @param array $out
1462
- *
1463
- * @return boolean
1464
- */
1465
- protected function argumentList(&$out)
1466
- {
1467
- $s = $this->seek();
1468
- $this->literal('(');
1469
-
1470
- $args = array();
1471
-
1472
- while ($this->keyword($var)) {
1473
- $ss = $this->seek();
1474
-
1475
- if ($this->literal('=') && $this->expression($exp)) {
1476
- $args[] = array(Type::T_STRING, '', array($var . '='));
1477
- $arg = $exp;
1478
- } else {
1479
- break;
1480
- }
1481
-
1482
- $args[] = $arg;
1483
-
1484
- if (! $this->literal(',')) {
1485
- break;
1486
- }
1487
-
1488
- $args[] = array(Type::T_STRING, '', array(', '));
1489
- }
1490
-
1491
- if (! $this->literal(')') || ! count($args)) {
1492
- $this->seek($s);
1493
-
1494
- return false;
1495
- }
1496
-
1497
- $out = $args;
1498
-
1499
- return true;
1500
- }
1501
-
1502
- /**
1503
- * Parse mixin/function definition argument list
1504
- *
1505
- * @param array $out
1506
- *
1507
- * @return boolean
1508
- */
1509
- protected function argumentDef(&$out)
1510
- {
1511
- $s = $this->seek();
1512
- $this->literal('(');
1513
-
1514
- $args = array();
1515
-
1516
- while ($this->variable($var)) {
1517
- $arg = array($var[1], null, false);
1518
-
1519
- $ss = $this->seek();
1520
-
1521
- if ($this->literal(':') && $this->genericList($defaultVal, 'expression')) {
1522
- $arg[1] = $defaultVal;
1523
- } else {
1524
- $this->seek($ss);
1525
- }
1526
-
1527
- $ss = $this->seek();
1528
-
1529
- if ($this->literal('...')) {
1530
- $sss = $this->seek();
1531
-
1532
- if (! $this->literal(')')) {
1533
- $this->throwParseError('... has to be after the final argument');
1534
- }
1535
-
1536
- $arg[2] = true;
1537
- $this->seek($sss);
1538
- } else {
1539
- $this->seek($ss);
1540
- }
1541
-
1542
- $args[] = $arg;
1543
-
1544
- if (! $this->literal(',')) {
1545
- break;
1546
- }
1547
- }
1548
-
1549
- if (! $this->literal(')')) {
1550
- $this->seek($s);
1551
-
1552
- return false;
1553
- }
1554
-
1555
- $out = $args;
1556
-
1557
- return true;
1558
- }
1559
-
1560
- /**
1561
- * Parse map
1562
- *
1563
- * @param array $out
1564
- *
1565
- * @return boolean
1566
- */
1567
- protected function map(&$out)
1568
- {
1569
- $s = $this->seek();
1570
-
1571
- if (! $this->literal('(')) {
1572
- return false;
1573
- }
1574
-
1575
- $keys = array();
1576
- $values = array();
1577
-
1578
- while ($this->genericList($key, 'expression') && $this->literal(':') &&
1579
- $this->genericList($value, 'expression')
1580
- ) {
1581
- $keys[] = $key;
1582
- $values[] = $value;
1583
-
1584
- if (! $this->literal(',')) {
1585
- break;
1586
- }
1587
- }
1588
-
1589
- if (! count($keys) || ! $this->literal(')')) {
1590
- $this->seek($s);
1591
-
1592
- return false;
1593
- }
1594
-
1595
- $out = array(Type::T_MAP, $keys, $values);
1596
-
1597
- return true;
1598
- }
1599
-
1600
- /**
1601
- * Parse color
1602
- *
1603
- * @param array $out
1604
- *
1605
- * @return boolean
1606
- */
1607
- protected function color(&$out)
1608
- {
1609
- $color = array(Type::T_COLOR);
1610
-
1611
- if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
1612
- if (isset($m[3])) {
1613
- $num = hexdec($m[3]);
1614
-
1615
- foreach (array(3, 2, 1) as $i) {
1616
- $t = $num & 0xf;
1617
- $color[$i] = $t << 4 | $t;
1618
- $num >>= 4;
1619
- }
1620
- } else {
1621
- $num = hexdec($m[2]);
1622
-
1623
- foreach (array(3, 2, 1) as $i) {
1624
- $color[$i] = $num & 0xff;
1625
- $num >>= 8;
1626
- }
1627
- }
1628
-
1629
- $out = $color;
1630
-
1631
- return true;
1632
- }
1633
-
1634
- return false;
1635
- }
1636
-
1637
- /**
1638
- * Parse number with unit
1639
- *
1640
- * @param array $out
1641
- *
1642
- * @return boolean
1643
- */
1644
- protected function unit(&$unit)
1645
- {
1646
- if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
1647
- $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
1648
-
1649
- return true;
1650
- }
1651
-
1652
- return false;
1653
- }
1654
-
1655
- /**
1656
- * Parse string
1657
- *
1658
- * @param array $out
1659
- *
1660
- * @return boolean
1661
- */
1662
- protected function string(&$out)
1663
- {
1664
- $s = $this->seek();
1665
-
1666
- if ($this->literal('"', false)) {
1667
- $delim = '"';
1668
- } elseif ($this->literal('\'', false)) {
1669
- $delim = '\'';
1670
- } else {
1671
- return false;
1672
- }
1673
-
1674
- $content = array();
1675
- $oldWhite = $this->eatWhiteDefault;
1676
- $this->eatWhiteDefault = false;
1677
-
1678
- while ($this->matchString($m, $delim)) {
1679
- $content[] = $m[1];
1680
-
1681
- if ($m[2] === '#{') {
1682
- $this->count -= strlen($m[2]);
1683
-
1684
- if ($this->interpolation($inter, false)) {
1685
- $content[] = $inter;
1686
- } else {
1687
- $this->count += strlen($m[2]);
1688
- $content[] = '#{'; // ignore it
1689
- }
1690
- } elseif ($m[2] === '\\') {
1691
- $content[] = $m[2];
1692
-
1693
- if ($this->literal($delim, false)) {
1694
- $content[] = $delim;
1695
- }
1696
- } else {
1697
- $this->count -= strlen($delim);
1698
- break; // delim
1699
- }
1700
- }
1701
-
1702
- $this->eatWhiteDefault = $oldWhite;
1703
-
1704
- if ($this->literal($delim)) {
1705
- $out = array(Type::T_STRING, $delim, $content);
1706
-
1707
- return true;
1708
- }
1709
-
1710
- $this->seek($s);
1711
-
1712
- return false;
1713
- }
1714
-
1715
- /**
1716
- * Parse keyword or interpolation
1717
- *
1718
- * @param array $out
1719
- *
1720
- * @return boolean
1721
- */
1722
- protected function mixedKeyword(&$out)
1723
- {
1724
- $s = $this->seek();
1725
-
1726
- $parts = array();
1727
-
1728
- $oldWhite = $this->eatWhiteDefault;
1729
- $this->eatWhiteDefault = false;
1730
-
1731
- for (;;) {
1732
- if ($this->keyword($key)) {
1733
- $parts[] = $key;
1734
- continue;
1735
- }
1736
-
1737
- if ($this->interpolation($inter)) {
1738
- $parts[] = $inter;
1739
- continue;
1740
- }
1741
-
1742
- break;
1743
- }
1744
-
1745
- $this->eatWhiteDefault = $oldWhite;
1746
-
1747
- if (count($parts) === 0) {
1748
- return false;
1749
- }
1750
-
1751
- if ($this->eatWhiteDefault) {
1752
- $this->whitespace();
1753
- }
1754
-
1755
- $out = $parts;
1756
-
1757
- return true;
1758
- }
1759
-
1760
- /**
1761
- * Parse an unbounded string stopped by $end
1762
- *
1763
- * @param string $end
1764
- * @param array $out
1765
- * @param string $nestingOpen
1766
- *
1767
- * @return boolean
1768
- */
1769
- protected function openString($end, &$out, $nestingOpen = null)
1770
- {
1771
- $oldWhite = $this->eatWhiteDefault;
1772
- $this->eatWhiteDefault = false;
1773
-
1774
- $patt = '(.*?)([\'"]|#\{|' . $this->pregQuote($end) . '|' . self::$commentPattern . ')';
1775
-
1776
- $nestingLevel = 0;
1777
-
1778
- $content = array();
1779
-
1780
- while ($this->match($patt, $m, false)) {
1781
- if (isset($m[1]) && $m[1] !== '') {
1782
- $content[] = $m[1];
1783
-
1784
- if ($nestingOpen) {
1785
- $nestingLevel += substr_count($m[1], $nestingOpen);
1786
- }
1787
- }
1788
-
1789
- $tok = $m[2];
1790
-
1791
- $this->count-= strlen($tok);
1792
-
1793
- if ($tok === $end && ! $nestingLevel--) {
1794
- break;
1795
- }
1796
-
1797
- if (($tok === '\'' || $tok === '"') && $this->string($str)) {
1798
- $content[] = $str;
1799
- continue;
1800
- }
1801
-
1802
- if ($tok === '#{' && $this->interpolation($inter)) {
1803
- $content[] = $inter;
1804
- continue;
1805
- }
1806
-
1807
- $content[] = $tok;
1808
- $this->count+= strlen($tok);
1809
- }
1810
-
1811
- $this->eatWhiteDefault = $oldWhite;
1812
-
1813
- if (count($content) === 0) {
1814
- return false;
1815
- }
1816
-
1817
- // trim the end
1818
- if (is_string(end($content))) {
1819
- $content[count($content) - 1] = rtrim(end($content));
1820
- }
1821
-
1822
- $out = array(Type::T_STRING, '', $content);
1823
-
1824
- return true;
1825
- }
1826
-
1827
- /**
1828
- * Parser interpolation
1829
- *
1830
- * @param array $out
1831
- * @param boolean $lookWhite save information about whitespace before and after
1832
- *
1833
- * @return boolean
1834
- */
1835
- protected function interpolation(&$out, $lookWhite = true)
1836
- {
1837
- $oldWhite = $this->eatWhiteDefault;
1838
- $this->eatWhiteDefault = true;
1839
-
1840
- $s = $this->seek();
1841
-
1842
- if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) {
1843
- // TODO: don't error if out of bounds
1844
-
1845
- if ($lookWhite) {
1846
- $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
1847
- $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
1848
- } else {
1849
- $left = $right = false;
1850
- }
1851
-
1852
- $out = array(Type::T_INTERPOLATE, $value, $left, $right);
1853
- $this->eatWhiteDefault = $oldWhite;
1854
-
1855
- if ($this->eatWhiteDefault) {
1856
- $this->whitespace();
1857
- }
1858
-
1859
- return true;
1860
- }
1861
-
1862
- $this->seek($s);
1863
- $this->eatWhiteDefault = $oldWhite;
1864
-
1865
- return false;
1866
- }
1867
-
1868
- /**
1869
- * Parse property name (as an array of parts or a string)
1870
- *
1871
- * @param array $out
1872
- *
1873
- * @return boolean
1874
- */
1875
- protected function propertyName(&$out)
1876
- {
1877
- $s = $this->seek();
1878
- $parts = array();
1879
-
1880
- $oldWhite = $this->eatWhiteDefault;
1881
- $this->eatWhiteDefault = false;
1882
-
1883
- for (;;) {
1884
- if ($this->interpolation($inter)) {
1885
- $parts[] = $inter;
1886
- continue;
1887
- }
1888
-
1889
- if ($this->keyword($text)) {
1890
- $parts[] = $text;
1891
- continue;
1892
- }
1893
-
1894
- if (count($parts) === 0 && $this->match('[:.#]', $m, false)) {
1895
- // css hacks
1896
- $parts[] = $m[0];
1897
- continue;
1898
- }
1899
-
1900
- break;
1901
- }
1902
-
1903
- $this->eatWhiteDefault = $oldWhite;
1904
-
1905
- if (count($parts) === 0) {
1906
- return false;
1907
- }
1908
-
1909
- // match comment hack
1910
- if (preg_match(
1911
- self::$whitePattern,
1912
- $this->buffer,
1913
- $m,
1914
- null,
1915
- $this->count
1916
- )) {
1917
- if (! empty($m[0])) {
1918
- $parts[] = $m[0];
1919
- $this->count += strlen($m[0]);
1920
- }
1921
- }
1922
-
1923
- $this->whitespace(); // get any extra whitespace
1924
-
1925
- $out = array(Type::T_STRING, '', $parts);
1926
-
1927
- return true;
1928
- }
1929
-
1930
- /**
1931
- * Parse comma separated selector list
1932
- *
1933
- * @param array $out
1934
- *
1935
- * @return boolean
1936
- */
1937
- protected function selectors(&$out)
1938
- {
1939
- $s = $this->seek();
1940
- $selectors = array();
1941
-
1942
- while ($this->selector($sel)) {
1943
- $selectors[] = $sel;
1944
-
1945
- if (! $this->literal(',')) {
1946
- break;
1947
- }
1948
-
1949
- while ($this->literal(',')) {
1950
- ; // ignore extra
1951
- }
1952
- }
1953
-
1954
- if (count($selectors) === 0) {
1955
- $this->seek($s);
1956
-
1957
- return false;
1958
- }
1959
-
1960
- $out = $selectors;
1961
-
1962
- return true;
1963
- }
1964
-
1965
- /**
1966
- * Parse whitespace separated selector list
1967
- *
1968
- * @param array $out
1969
- *
1970
- * @return boolean
1971
- */
1972
- protected function selector(&$out)
1973
- {
1974
- $selector = array();
1975
-
1976
- for (;;) {
1977
- if ($this->match('[>+~]+', $m)) {
1978
- $selector[] = array($m[0]);
1979
- continue;
1980
- }
1981
-
1982
- if ($this->selectorSingle($part)) {
1983
- $selector[] = $part;
1984
- $this->match('\s+', $m);
1985
- continue;
1986
- }
1987
-
1988
- if ($this->match('\/[^\/]+\/', $m)) {
1989
- $selector[] = array($m[0]);
1990
- continue;
1991
- }
1992
-
1993
- break;
1994
- }
1995
-
1996
- if (count($selector) === 0) {
1997
- return false;
1998
- }
1999
-
2000
- $out = $selector;
2001
- return true;
2002
- }
2003
-
2004
- /**
2005
- * Parse the parts that make up a selector
2006
- *
2007
- * {@internal
2008
- * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
2009
- * }}
2010
- *
2011
- * @param array $out
2012
- *
2013
- * @return boolean
2014
- */
2015
- protected function selectorSingle(&$out)
2016
- {
2017
- $oldWhite = $this->eatWhiteDefault;
2018
- $this->eatWhiteDefault = false;
2019
-
2020
- $parts = array();
2021
-
2022
- if ($this->literal('*', false)) {
2023
- $parts[] = '*';
2024
- }
2025
-
2026
- for (;;) {
2027
- // see if we can stop early
2028
- if ($this->match('\s*[{,]', $m)) {
2029
- $this->count--;
2030
- break;
2031
- }
2032
-
2033
- $s = $this->seek();
2034
-
2035
- // self
2036
- if ($this->literal('&', false)) {
2037
- $parts[] = Compiler::$selfSelector;
2038
- continue;
2039
- }
2040
-
2041
- if ($this->literal('.', false)) {
2042
- $parts[] = '.';
2043
- continue;
2044
- }
2045
-
2046
- if ($this->literal('|', false)) {
2047
- $parts[] = '|';
2048
- continue;
2049
- }
2050
-
2051
- if ($this->match('\\\\\S', $m)) {
2052
- $parts[] = $m[0];
2053
- continue;
2054
- }
2055
-
2056
- // for keyframes
2057
- if ($this->unit($unit)) {
2058
- $parts[] = $unit;
2059
- continue;
2060
- }
2061
-
2062
- if ($this->keyword($name)) {
2063
- $parts[] = $name;
2064
- continue;
2065
- }
2066
-
2067
- if ($this->interpolation($inter)) {
2068
- $parts[] = $inter;
2069
- continue;
2070
- }
2071
-
2072
- if ($this->literal('%', false) && $this->placeholder($placeholder)) {
2073
- $parts[] = '%';
2074
- $parts[] = $placeholder;
2075
- continue;
2076
- }
2077
-
2078
- if ($this->literal('#', false)) {
2079
- $parts[] = '#';
2080
- continue;
2081
- }
2082
-
2083
- // a pseudo selector
2084
- if ($this->match('::?', $m) && $this->mixedKeyword($nameParts)) {
2085
- $parts[] = $m[0];
2086
-
2087
- foreach ($nameParts as $sub) {
2088
- $parts[] = $sub;
2089
- }
2090
-
2091
- $ss = $this->seek();
2092
-
2093
- if ($this->literal('(') &&
2094
- ($this->openString(')', $str, '(') || true ) &&
2095
- $this->literal(')')
2096
- ) {
2097
- $parts[] = '(';
2098
-
2099
- if (! empty($str)) {
2100
- $parts[] = $str;
2101
- }
2102
-
2103
- $parts[] = ')';
2104
- } else {
2105
- $this->seek($ss);
2106
- }
2107
-
2108
- continue;
2109
- }
2110
-
2111
- $this->seek($s);
2112
-
2113
- // attribute selector
2114
- // TODO: replace with open string?
2115
- if ($this->literal('[', false)) {
2116
- $attrParts = array('[');
2117
-
2118
- // keyword, string, operator
2119
- for (;;) {
2120
- if ($this->literal(']', false)) {
2121
- $this->count--;
2122
- break; // get out early
2123
- }
2124
-
2125
- if ($this->match('\s+', $m)) {
2126
- $attrParts[] = ' ';
2127
- continue;
2128
- }
2129
-
2130
- if ($this->string($str)) {
2131
- $attrParts[] = $str;
2132
- continue;
2133
- }
2134
-
2135
- if ($this->keyword($word)) {
2136
- $attrParts[] = $word;
2137
- continue;
2138
- }
2139
-
2140
- if ($this->interpolation($inter, false)) {
2141
- $attrParts[] = $inter;
2142
- continue;
2143
- }
2144
-
2145
- // operator, handles attr namespace too
2146
- if ($this->match('[|-~\$\*\^=]+', $m)) {
2147
- $attrParts[] = $m[0];
2148
- continue;
2149
- }
2150
-
2151
- break;
2152
- }
2153
-
2154
- if ($this->literal(']', false)) {
2155
- $attrParts[] = ']';
2156
-
2157
- foreach ($attrParts as $part) {
2158
- $parts[] = $part;
2159
- }
2160
-
2161
- continue;
2162
- }
2163
-
2164
- $this->seek($s);
2165
- // TODO: should just break here?
2166
- }
2167
-
2168
- break;
2169
- }
2170
-
2171
- $this->eatWhiteDefault = $oldWhite;
2172
-
2173
- if (count($parts) === 0) {
2174
- return false;
2175
- }
2176
-
2177
- $out = $parts;
2178
-
2179
- return true;
2180
- }
2181
-
2182
- /**
2183
- * Parse a variable
2184
- *
2185
- * @param array $out
2186
- *
2187
- * @return boolean
2188
- */
2189
- protected function variable(&$out)
2190
- {
2191
- $s = $this->seek();
2192
-
2193
- if ($this->literal('$', false) && $this->keyword($name)) {
2194
- $out = array(Type::T_VARIABLE, $name);
2195
-
2196
- return true;
2197
- }
2198
-
2199
- $this->seek($s);
2200
-
2201
- return false;
2202
- }
2203
-
2204
- /**
2205
- * Parse a keyword
2206
- *
2207
- * @param string $word
2208
- * @param boolean $eatWhitespace
2209
- *
2210
- * @return boolean
2211
- */
2212
- protected function keyword(&$word, $eatWhitespace = null)
2213
- {
2214
- if ($this->match(
2215
- '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
2216
- $m,
2217
- $eatWhitespace
2218
- )) {
2219
- $word = $m[1];
2220
-
2221
- return true;
2222
- }
2223
-
2224
- return false;
2225
- }
2226
-
2227
- /**
2228
- * Parse a placeholder
2229
- *
2230
- * @param string $placeholder
2231
- *
2232
- * @return boolean
2233
- */
2234
- protected function placeholder(&$placeholder)
2235
- {
2236
- if ($this->match('([\w\-_]+|#[{][$][\w\-_]+[}])', $m)) {
2237
- $placeholder = $m[1];
2238
-
2239
- return true;
2240
- }
2241
-
2242
- return false;
2243
- }
2244
-
2245
- /**
2246
- * Parse a url
2247
- *
2248
- * @param array $out
2249
- *
2250
- * @return boolean
2251
- */
2252
- protected function url(&$out)
2253
- {
2254
- if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) {
2255
- $out = array(Type::T_STRING, '', array('url(' . $m[2] . $m[3] . $m[2] . ')'));
2256
-
2257
- return true;
2258
- }
2259
-
2260
- return false;
2261
- }
2262
-
2263
- /**
2264
- * Consume an end of statement delimiter
2265
- *
2266
- * @return boolean
2267
- */
2268
- protected function end()
2269
- {
2270
- if ($this->literal(';')) {
2271
- return true;
2272
- }
2273
-
2274
- if ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
2275
- // if there is end of file or a closing block next then we don't need a ;
2276
- return true;
2277
- }
2278
-
2279
- return false;
2280
- }
2281
-
2282
- /**
2283
- * Strip assignment flag from the list
2284
- *
2285
- * @param array $value
2286
- *
2287
- * @return string
2288
- */
2289
- protected function stripAssignmentFlag(&$value)
2290
- {
2291
- $token = &$value;
2292
-
2293
- for ($token = &$value; $token[0] === Type::T_LIST && ($s = count($token[2])); $token = &$lastNode) {
2294
- $lastNode = &$token[2][$s - 1];
2295
-
2296
- if ($lastNode[0] === Type::T_KEYWORD && in_array($lastNode[1], array('!default', '!global'))) {
2297
- array_pop($token[2]);
2298
-
2299
- $token = $this->flattenList($token);
2300
-
2301
- return $lastNode[1];
2302
- }
2303
- }
2304
-
2305
- return false;
2306
- }
2307
-
2308
- /**
2309
- * Turn list of length 1 into value type
2310
- *
2311
- * @param array $value
2312
- *
2313
- * @return array
2314
- */
2315
- protected function flattenList($value)
2316
- {
2317
- if ($value[0] === Type::T_LIST && count($value[2]) === 1) {
2318
- return $this->flattenList($value[2][0]);
2319
- }
2320
-
2321
- return $value;
2322
- }
2323
-
2324
- /**
2325
- * @deprecated
2326
- *
2327
- * {@internal
2328
- * advance counter to next occurrence of $what
2329
- * $until - don't include $what in advance
2330
- * $allowNewline, if string, will be used as valid char set
2331
- * }}
2332
- */
2333
- protected function to($what, &$out, $until = false, $allowNewline = false)
2334
- {
2335
- if (is_string($allowNewline)) {
2336
- $validChars = $allowNewline;
2337
- } else {
2338
- $validChars = $allowNewline ? '.' : "[^\n]";
2339
- }
2340
-
2341
- if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
2342
- return false;
2343
- }
2344
-
2345
- if ($until) {
2346
- $this->count -= strlen($what); // give back $what
2347
- }
2348
-
2349
- $out = $m[1];
2350
-
2351
- return true;
2352
- }
2353
-
2354
- /**
2355
- * @deprecated
2356
- */
2357
- protected function show()
2358
- {
2359
- if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
2360
- return $m[1];
2361
- }
2362
-
2363
- return '';
2364
- }
2365
-
2366
- /**
2367
- * Quote regular expression
2368
- *
2369
- * @param string $what
2370
- *
2371
- * @return string
2372
- */
2373
- private function pregQuote($what)
2374
- {
2375
- return preg_quote($what, '/');
2376
- }
2377
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Server.php DELETED
@@ -1,459 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp;
13
-
14
- use Leafo\ScssPhp\Compiler;
15
- use Leafo\ScssPhp\Version;
16
-
17
- /**
18
- * SCSS server
19
- *
20
- * @author Leaf Corcoran <leafot@gmail.com>
21
- */
22
- class Server
23
- {
24
- /**
25
- * @var boolean
26
- */
27
- private $showErrorsAsCSS;
28
-
29
- /**
30
- * @var string
31
- */
32
- private $dir;
33
-
34
- /**
35
- * @var string
36
- */
37
- private $cacheDir;
38
-
39
- /**
40
- * @var \Leafo\ScssPhp\Compiler
41
- */
42
- private $scss;
43
-
44
- /**
45
- * Join path components
46
- *
47
- * @param string $left Path component, left of the directory separator
48
- * @param string $right Path component, right of the directory separator
49
- *
50
- * @return string
51
- */
52
- protected function join($left, $right)
53
- {
54
- return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\');
55
- }
56
-
57
- /**
58
- * Get name of requested .scss file
59
- *
60
- * @return string|null
61
- */
62
- protected function inputName()
63
- {
64
- switch (true) {
65
- case isset($_GET['p']):
66
- return $_GET['p'];
67
- case isset($_SERVER['PATH_INFO']):
68
- return $_SERVER['PATH_INFO'];
69
- case isset($_SERVER['DOCUMENT_URI']):
70
- return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME']));
71
- }
72
- }
73
-
74
- /**
75
- * Get path to requested .scss file
76
- *
77
- * @return string
78
- */
79
- protected function findInput()
80
- {
81
- if (($input = $this->inputName())
82
- && strpos($input, '..') === false
83
- && substr($input, -5) === '.scss'
84
- ) {
85
- $name = $this->join($this->dir, $input);
86
-
87
- if (is_file($name) && is_readable($name)) {
88
- return $name;
89
- }
90
- }
91
-
92
- return false;
93
- }
94
-
95
- /**
96
- * Get path to cached .css file
97
- *
98
- * @return string
99
- */
100
- protected function cacheName($fname)
101
- {
102
- return $this->join($this->cacheDir, md5($fname) . '.css');
103
- }
104
-
105
- /**
106
- * Get path to meta data
107
- *
108
- * @return string
109
- */
110
- protected function metadataName($out)
111
- {
112
- return $out . '.meta';
113
- }
114
-
115
- /**
116
- * Determine whether .scss file needs to be re-compiled.
117
- *
118
- * @param string $in Input path
119
- * @param string $out Output path
120
- * @param string $etag ETag
121
- *
122
- * @return boolean True if compile required.
123
- */
124
- protected function needsCompile($in, $out, &$etag)
125
- {
126
- if (! is_file($out)) {
127
- return true;
128
- }
129
-
130
- $mtime = filemtime($out);
131
-
132
- $metadataName = $this->metadataName($out);
133
-
134
- if (is_readable($metadataName)) {
135
- $metadata = unserialize(file_get_contents($metadataName));
136
-
137
- foreach ($metadata['imports'] as $import => $originalMtime) {
138
- $currentMtime = filemtime($import);
139
-
140
- if ($currentMtime !== $originalMtime || $currentMtime > $mtime) {
141
- return true;
142
- }
143
- }
144
-
145
- $metaVars = crc32(serialize($this->scss->getVariables()));
146
-
147
- if ($metaVars !== $metadata['vars']) {
148
- return true;
149
- }
150
-
151
- $etag = $metadata['etag'];
152
-
153
- return false;
154
- }
155
-
156
- return true;
157
- }
158
-
159
- /**
160
- * Get If-Modified-Since header from client request
161
- *
162
- * @return string|null
163
- */
164
- protected function getIfModifiedSinceHeader()
165
- {
166
- $modifiedSince = null;
167
-
168
- if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
169
- $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
170
-
171
- if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) {
172
- $modifiedSince = substr($modifiedSince, 0, $semicolonPos);
173
- }
174
- }
175
-
176
- return $modifiedSince;
177
- }
178
-
179
- /**
180
- * Get If-None-Match header from client request
181
- *
182
- * @return string|null
183
- */
184
- protected function getIfNoneMatchHeader()
185
- {
186
- $noneMatch = null;
187
-
188
- if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
189
- $noneMatch = $_SERVER['HTTP_IF_NONE_MATCH'];
190
- }
191
-
192
- return $noneMatch;
193
- }
194
-
195
- /**
196
- * Compile .scss file
197
- *
198
- * @param string $in Input path (.scss)
199
- * @param string $out Output path (.css)
200
- *
201
- * @return array
202
- */
203
- protected function compile($in, $out)
204
- {
205
- $start = microtime(true);
206
- $css = $this->scss->compile(file_get_contents($in), $in);
207
- $elapsed = round((microtime(true) - $start), 4);
208
-
209
- $v = Version::VERSION;
210
- $t = @date('r');
211
- $css = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css;
212
- $etag = md5($css);
213
-
214
- file_put_contents($out, $css);
215
- file_put_contents(
216
- $this->metadataName($out),
217
- serialize(array(
218
- 'etag' => $etag,
219
- 'imports' => $this->scss->getParsedFiles(),
220
- 'vars' => crc32(serialize($this->scss->getVariables())),
221
- ))
222
- );
223
-
224
- return array($css, $etag);
225
- }
226
-
227
- /**
228
- * Format error as a pseudo-element in CSS
229
- *
230
- * @param \Exception $error
231
- *
232
- * @return string
233
- */
234
- protected function createErrorCSS($error)
235
- {
236
- $message = str_replace(
237
- array("'", "\n"),
238
- array("\\'", "\\A"),
239
- $error->getfile() . ":\n\n" . $error->getMessage()
240
- );
241
-
242
- return "body { display: none !important; }
243
- html:after {
244
- background: white;
245
- color: black;
246
- content: '$message';
247
- display: block !important;
248
- font-family: mono;
249
- padding: 1em;
250
- white-space: pre;
251
- }";
252
- }
253
-
254
- /**
255
- * Render errors as a pseudo-element within valid CSS, displaying the errors on any
256
- * page that includes this CSS.
257
- *
258
- * @param boolean $show
259
- */
260
- public function showErrorsAsCSS($show = true)
261
- {
262
- $this->showErrorsAsCSS = $show;
263
- }
264
-
265
- /**
266
- * Compile .scss file
267
- *
268
- * @param string $in Input file (.scss)
269
- * @param string $out Output file (.css) optional
270
- *
271
- * @return string|bool
272
- */
273
- public function compileFile($in, $out = null)
274
- {
275
- if (! is_readable($in)) {
276
- throw new \Exception('load error: failed to find ' . $in);
277
- }
278
-
279
- $pi = pathinfo($in);
280
-
281
- $this->scss->addImportPath($pi['dirname'] . '/');
282
-
283
- $compiled = $this->scss->compile(file_get_contents($in), $in);
284
-
285
- if ($out !== null) {
286
- return file_put_contents($out, $compiled);
287
- }
288
-
289
- return $compiled;
290
- }
291
-
292
- /**
293
- * Check if file need compiling
294
- *
295
- * @param string $in Input file (.scss)
296
- * @param string $out Output file (.css)
297
- *
298
- * @return bool
299
- */
300
- public function checkedCompile($in, $out)
301
- {
302
- if (! is_file($out) || filemtime($in) > filemtime($out)) {
303
- $this->compileFile($in, $out);
304
-
305
- return true;
306
- }
307
-
308
- return false;
309
- }
310
-
311
- /**
312
- * Compile requested scss and serve css. Outputs HTTP response.
313
- *
314
- * @param string $salt Prefix a string to the filename for creating the cache name hash
315
- */
316
- public function serve($salt = '')
317
- {
318
- $protocol = isset($_SERVER['SERVER_PROTOCOL'])
319
- ? $_SERVER['SERVER_PROTOCOL']
320
- : 'HTTP/1.0';
321
-
322
- if ($input = $this->findInput()) {
323
- $output = $this->cacheName($salt . $input);
324
- $etag = $noneMatch = trim($this->getIfNoneMatchHeader(), '"');
325
-
326
- if ($this->needsCompile($input, $output, $etag)) {
327
- try {
328
- list($css, $etag) = $this->compile($input, $output);
329
-
330
- $lastModified = gmdate('D, d M Y H:i:s', filemtime($output)) . ' GMT';
331
-
332
- header('Last-Modified: ' . $lastModified);
333
- header('Content-type: text/css');
334
- header('ETag: "' . $etag . '"');
335
-
336
- echo $css;
337
-
338
- } catch (\Exception $e) {
339
- if ($this->showErrorsAsCSS) {
340
- header('Content-type: text/css');
341
-
342
- echo $this->createErrorCSS($e);
343
- } else {
344
- header($protocol . ' 500 Internal Server Error');
345
- header('Content-type: text/plain');
346
-
347
- echo 'Parse error: ' . $e->getMessage() . "\n";
348
- }
349
-
350
- }
351
-
352
- return;
353
- }
354
-
355
- header('X-SCSS-Cache: true');
356
- header('Content-type: text/css');
357
- header('ETag: "' . $etag . '"');
358
-
359
- if ($etag === $noneMatch) {
360
- header($protocol . ' 304 Not Modified');
361
-
362
- return;
363
- }
364
-
365
- $modifiedSince = $this->getIfModifiedSinceHeader();
366
- $mtime = filemtime($output);
367
-
368
- if (@strtotime($modifiedSince) === $mtime) {
369
- header($protocol . ' 304 Not Modified');
370
-
371
- return;
372
- }
373
-
374
- $lastModified = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
375
- header('Last-Modified: ' . $lastModified);
376
-
377
- echo file_get_contents($output);
378
-
379
- return;
380
- }
381
-
382
- header($protocol . ' 404 Not Found');
383
- header('Content-type: text/plain');
384
-
385
- $v = Version::VERSION;
386
- echo "/* INPUT NOT FOUND scss $v */\n";
387
- }
388
-
389
- /**
390
- * Based on explicit input/output files does a full change check on cache before compiling.
391
- *
392
- * @param string $in
393
- * @param string $out
394
- * @param boolean $force
395
- *
396
- * @return string Compiled CSS results
397
- *
398
- * @throws \Exception
399
- */
400
- public function checkedCachedCompile($in, $out, $force = false)
401
- {
402
- if (! is_file($in) || ! is_readable($in)) {
403
- throw new \Exception('Invalid or unreadable input file specified.');
404
- }
405
-
406
- if (is_dir($out) || ! is_writable(file_exists($out) ? $out : dirname($out))) {
407
- throw new \Exception('Invalid or unwritable output file specified.');
408
- }
409
-
410
- if ($force || $this->needsCompile($in, $out, $etag)) {
411
- list($css, $etag) = $this->compile($in, $out);
412
- } else {
413
- $css = file_get_contents($out);
414
- }
415
-
416
- return $css;
417
- }
418
-
419
- /**
420
- * Constructor
421
- *
422
- * @param string $dir Root directory to .scss files
423
- * @param string $cacheDir Cache directory
424
- * @param \Leafo\ScssPhp\Compiler|null $scss SCSS compiler instance
425
- */
426
- public function __construct($dir, $cacheDir = null, $scss = null)
427
- {
428
- $this->dir = $dir;
429
-
430
- if (! isset($cacheDir)) {
431
- $cacheDir = $this->join($dir, 'scss_cache');
432
- }
433
-
434
- $this->cacheDir = $cacheDir;
435
-
436
- if (! is_dir($this->cacheDir)) {
437
- mkdir($this->cacheDir, 0755, true);
438
- }
439
-
440
- if (! isset($scss)) {
441
- $scss = new Compiler();
442
- $scss->setImportPaths($this->dir);
443
- }
444
-
445
- $this->scss = $scss;
446
- $this->showErrorsAsCSS = false;
447
- }
448
-
449
- /**
450
- * Helper method to serve compiled scss
451
- *
452
- * @param string $path Root path
453
- */
454
- public static function serveFrom($path)
455
- {
456
- $server = new self($path);
457
- $server->serve();
458
- }
459
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Type.php DELETED
@@ -1,68 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp;
13
-
14
- /**
15
- * SCSS block/node types
16
- *
17
- * @author Anthon Pang <anthon.pang@gmail.com>
18
- */
19
- class Type
20
- {
21
- const T_ASSIGN = 'assign';
22
- const T_AT_ROOT = 'at-root';
23
- const T_BLOCK = 'block';
24
- const T_BREAK = 'break';
25
- const T_CHARSET = 'charset';
26
- const T_COLOR = 'color';
27
- const T_COMMENT = 'comment';
28
- const T_CONTINUE = 'continue';
29
- const T_CONTROL = 'control';
30
- const T_DEBUG = 'debug';
31
- const T_DIRECTIVE = 'directive';
32
- const T_EACH = 'each';
33
- const T_ELSE = 'else';
34
- const T_ELSEIF = 'elseif';
35
- const T_ERROR = 'error';
36
- const T_EXPRESSION = 'exp';
37
- const T_EXTEND = 'extend';
38
- const T_FOR = 'for';
39
- const T_FUNCTION = 'function';
40
- const T_FUNCTION_CALL = 'fncall';
41
- const T_HSL = 'hsl';
42
- const T_IF = 'if';
43
- const T_IMPORT = 'import';
44
- const T_INCLUDE = 'include';
45
- const T_INTERPOLATE = 'interpolate';
46
- const T_INTERPOLATED = 'interpolated';
47
- const T_KEYWORD = 'keyword';
48
- const T_LIST = 'list';
49
- const T_MAP = 'map';
50
- const T_MEDIA = 'media';
51
- const T_MEDIA_EXPRESSION = 'mediaExp';
52
- const T_MEDIA_TYPE = 'mediaType';
53
- const T_MEDIA_VALUE = 'mediaValue';
54
- const T_MIXIN = 'mixin';
55
- const T_MIXIN_CONTENT = 'mixin_content';
56
- const T_NESTED_PROPERTY = 'nestedprop';
57
- const T_NOT = 'not';
58
- const T_NULL = 'null';
59
- const T_NUMBER = 'number';
60
- const T_RETURN = 'return';
61
- const T_ROOT = 'root';
62
- const T_SELF = 'self';
63
- const T_STRING = 'string';
64
- const T_UNARY = 'unary';
65
- const T_VARIABLE = 'var';
66
- const T_WARN = 'warn';
67
- const T_WHILE = 'while';
68
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Util.php DELETED
@@ -1,55 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp;
13
-
14
- use Leafo\ScssPhp\Base\Range;
15
-
16
- /**
17
- * SCSS utilties
18
- *
19
- * @author Anthon Pang <anthon.pang@gmail.com>
20
- */
21
- class Util
22
- {
23
- /**
24
- * Asserts that `value` falls within `range` (inclusive), leaving
25
- * room for slight floating-point errors.
26
- *
27
- * @param string $name The name of the value. Used in the error message.
28
- * @param Range $range Range of values.
29
- * @param array $value The value to check.
30
- * @param string $unit The unit of the value. Used in error reporting.
31
- *
32
- * @return mixed `value` adjusted to fall within range, if it was outside by a floating-point margin.
33
- *
34
- * @throws \Exception
35
- */
36
- public static function checkRange($name, Range $range, $value, $unit = '')
37
- {
38
- $val = $value[1];
39
- $grace = new Range(-0.00001, 0.00001);
40
-
41
- if ($range->includes($val)) {
42
- return $val;
43
- }
44
-
45
- if ($grace->includes($val - $range->first)) {
46
- return $range->first;
47
- }
48
-
49
- if ($grace->includes($val - $range->last)) {
50
- return $range->last;
51
- }
52
-
53
- throw new \Exception("$name {$val} must be between {$range->first} and {$range->last}$unit");
54
- }
55
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/scssphp_legacy/src/Version.php DELETED
@@ -1,22 +0,0 @@
1
- <?php
2
- /**
3
- * SCSSPHP
4
- *
5
- * @copyright 2012-2015 Leaf Corcoran
6
- *
7
- * @license http://opensource.org/licenses/MIT MIT
8
- *
9
- * @link http://leafo.github.io/scssphp
10
- */
11
-
12
- namespace Leafo\ScssPhp;
13
-
14
- /**
15
- * SCSSPHP version
16
- *
17
- * @author Leaf Corcoran <leafot@gmail.com>
18
- */
19
- class Version
20
- {
21
- const VERSION = 'v0.4.0';
22
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/libraries/simplehtmldom/CHANGELOG.md ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
+
6
+ ## [1.9.1] - 2019-10-20
7
+ ### Fixed
8
+ - Fixed broken "text" selectors [#175](https://sourceforge.net/p/simplehtmldom/bugs/175/)
9
+
10
+ ## [1.9] - 2019-05-30
11
+ ### Added
12
+ - Added unit test for bug reports
13
+ - Added test for bug [#153](https://sourceforge.net/p/simplehtmldom/bugs/153/)
14
+ - Added test for bug [#163](https://sourceforge.net/p/simplehtmldom/bugs/163/)
15
+ - Added test for bug [#166](https://sourceforge.net/p/simplehtmldom/bugs/166/)
16
+ - Added test for bug [#169](https://sourceforge.net/p/simplehtmldom/bugs/169/)
17
+ - Added unit test for character sets UTF-8, CP1251 and CP1252 (#142)
18
+ - Added support for meta charset to parse_charset
19
+ - Added detection for CP1251 to parse_charset, using iconv
20
+ - Added LICENSE file (MIT) to the project root
21
+ - Added functions to `simple_html_dom_node`
22
+ - `remove`: Removes the current node recursively from the DOM tree
23
+ - `removeChild`: Removes a child node recursively from the DOM tree
24
+ - `hasClass`: Checks if the current node has the specified class name
25
+ - `addClass`: Adds one or more classes to the current node
26
+ - `removeClass`: Removes one or more classes from the current node
27
+ - `save`: Saves the current node to disk
28
+ ### Changed
29
+ - Changed manual from custom implementation to MkDocs (https://www.mkdocs.org/)
30
+ ### Fixed
31
+ - Fixed warning when trying to clear() the DOM on a null nodes list (#153)
32
+ - Fixed missing whitespace when returning plaintext (#163)
33
+ - Fixed broken detection of duplicate attributes (#166)
34
+ - Fixed broken detection of CP1252 (ISO-8859-1) documents (#142)
35
+ - Fixed error using next-sibling combinator ('E + F') on last child
36
+ - Fixed selector parsing for attribute selectors ending on "s" or "i" (#169)
37
+
38
+ ## [1.8.1] - 2019-01-13
39
+ ### Fixed
40
+ - Fixed various bugs related to parsing classes and ids
41
+
42
+ ## [1.8] - 2019-01-13
43
+ ### Added
44
+ - Added documentation for `simple_html_dom_node::find`
45
+ - Added documentation for `simple_html_dom_node::parse_selector`
46
+ - Added documentation for `simple_html_dom_node::seek`
47
+ - Added documentation for `simple_html_dom_node::match`
48
+ - Added unit tests for bug reports
49
+ - Added test for bug [#62](https://sourceforge.net/p/simplehtmldom/bugs/62/)
50
+ - Added test for bug [#79](https://sourceforge.net/p/simplehtmldom/bugs/79/)
51
+ - Added test for bug [#144](https://sourceforge.net/p/simplehtmldom/bugs/144/)
52
+ - Added unit tests for CSS selectors
53
+ - Added ability to define constants before simple_html_dom does
54
+ - 'DEFAULT_TARGET_CHARSET'
55
+ - 'DEFAULT_BR_TEXT'
56
+ - 'DEFAULT_SPAN_TEXT'
57
+ - 'MAX_FILE_SIZE'
58
+ - Added support for CSS combinators
59
+ - Added support for Child Combinator (`>`)
60
+ - Added support for Next Sibling Combinator (`+`)
61
+ - Added support for Subsequent Sibling Combinator (`~`)
62
+ - Added support for multiclass selectors (`.class.class.class`)
63
+ - Added support for multiattribute selectors (`[attr1][attr2][attribute3]`)
64
+ - Added support for attribute selectors
65
+ - Added support for pipe selectors (`|=`)
66
+ - Added support for tilde selectors (`~=`)
67
+ - Added support for case sensitivity selectors (`i` and `s`)
68
+ - Added unit tests for PHP compatibility to PHP 5.6+
69
+ - Added coding standard using PHP_CodeSniffer
70
+ ### Changed
71
+ - Removed automatic filtering of 'tbody' selectors (#79)
72
+ > Remove 'tbody' from all selectors to maintain the previous state!
73
+ - Coding standard using PHP_CodeSniffer
74
+ ### Fixed
75
+ - Fixed broken CSS selector attributes with value "0" (#62)
76
+ - Fixed broken simple_html_dom::load_file
77
+ - Fixed forward slashes in CSS selector breaks value matching using '*=' (#144)
78
+ - Fixed Universal Selectors
79
+
80
+ ## [1.7] - 2018-12-10
81
+ ### Added
82
+ - Added code documentation to improve readability
83
+ - Added unit tests for `simple_html_dom::$self_closing_tags`
84
+ - Added unit tests for `simple_html_dom::$optional_closing_tags`
85
+ - Added unit tests for bug reports
86
+ - Added test for bug [#56](https://sourceforge.net/p/simplehtmldom/bugs/56/)
87
+ - Added test for bug [#97](https://sourceforge.net/p/simplehtmldom/bugs/97/)
88
+ - Added test for bug [#116](https://sourceforge.net/p/simplehtmldom/bugs/116/)
89
+ - Added test for bug [#121](https://sourceforge.net/p/simplehtmldom/bugs/127/)
90
+ - Added test for bug [#127](https://sourceforge.net/p/simplehtmldom/bugs/127/)
91
+ - Added test for bug [#154](https://sourceforge.net/p/simplehtmldom/bugs/154/)
92
+ - Added test for bug [#160](https://sourceforge.net/p/simplehtmldom/bugs/160/)
93
+ - Added unit tests for memory management of the parser
94
+ - Added bit flags to `simple_html_dom::load()`
95
+ - Added bit flag `HDOM_SMARTY_AS_TEXT` to optionally filter Smarty scripts (#154)\
96
+ **Note**: Smarty scripts are no longer filtered by default!\
97
+ - Added build script to automate releases
98
+ - Added support for attributes without whitespace to separate them
99
+ ### Changed
100
+ - Improved documentation and readability for `$self_closing_tags`
101
+ - Improved documentation and readability for `$block_tags`
102
+ - Improved documentation and readability for `$optional_closing_tags`
103
+ - Updated list of `simple_html_dom::$self_closing_tags`
104
+ - Removed 'spacer' (obsolete)
105
+ - Added 'area'
106
+ - Added 'col'
107
+ - Added 'meta'
108
+ - Added 'param'
109
+ - Added 'source'
110
+ - Added 'track'
111
+ - Added 'wbr'
112
+ - Updated list of `simple_html_dom::$optional_closing_tags`
113
+ - Removed "nobr" (obsolete)
114
+ - Added 'th' as closable element to 'td'
115
+ - Added 'td' as closable element to 'th'
116
+ - Added 'optgroup' with 'optgroup' and 'option' as closable elements
117
+ - Added 'optgroup' as closable element to 'option'
118
+ - Added 'rp' with 'rp' and 'rt' as closable elements
119
+ - Added 'rt' with 'rt' and 'rp' as closable elements
120
+ - Clarified meaning of `simple_html_dom->parent`
121
+ - Changed default `$offset` for `file_get_html()` from -1 to 0 (#161)
122
+ - Changed `simple_html_dom::load()` to remove script tags before replacing newline characters
123
+ - `simple_html_dom_node::text()` no longer adds whitespace to top level span elements (only to sub-elements)
124
+ - `simple_html_dom_node::text()` adds blank lines between paragraphs
125
+ - Normalized line endings in the repository to LF via `.gitattributes`
126
+ - Improved performance of `simple_html_dom::parse_charset()` by approximately 25%
127
+ - Improved performance of `simple_html_dom::parse()` by approximately 10%
128
+ ### Deprecated
129
+ - `str_get_html()` is deprecated and should be replaced by `new simple_html_dom()`
130
+ ### Removed
131
+ - Removed protected function `simple_html_dom::copy_until_char_escaped()`
132
+ ### Fixed
133
+ - Fixed compatibility issues with PHP 7.3
134
+ - Fixed typo (#147)
135
+ - Fixed handling of incorrectly escaped text (#160)
136
+ - Restore functionality of `$maxLen` in `file_get_html()`
137
+ - Fixed load_file breaks if an error ocurred in another script
138
+
139
+ ## [1.6] - 2014-05-28
140
+ ### Added
141
+ - Added some ability to insert and create nodes
142
+ - Add ability to search the "noise" array
143
+
144
+ ## [1.5] - 2012-09-10
145
+ ### Added
146
+ - Added flag: LOCK_EX while calling "file_put_contents()"
147
+ - Added support for detecting the source html character set. This is used to convert characters when plaintext is requested.
148
+ - Other little fixes and features, too numerous to categorize
149
+ ### Changed
150
+ - Error of "file_get_contents()" will be thrown as an exception
151
+ ### Fixed
152
+ - Fixed the typo of "token_blank_t"
153
+ - Memory leak fixed
154
+
155
+ ## [1.11] - 2008-12-14
156
+ ### Added
157
+ - Supports xpath generated from Firebug
158
+ - New method "dump" of "simple_html_dom_node"
159
+ - New attribute "xmltext" of "simple_html_dom_node"
160
+ ### Changed
161
+ - Remove preg_quote on selector match function: `[attribute*=value]`
162
+ - Element "Comment" will treat as children
163
+ ### Fixed
164
+ - Fixed the problem with `<pre>`
165
+ - Fixed bug #2207477 (does not load some pages properly)
166
+ - Fixed bug #2315853 (Error with character after < sign)
167
+
168
+ ## [1.10] - 2008-10-25
169
+ ### Changed
170
+ - Negative indexes supports of "find" method, thanks for Vadim Voituk
171
+ - Constructor with automatically load contents either text or file/url, thanks for Antcs
172
+ - Fully supports wildcard in selectors
173
+ ### Fixed
174
+ - Fixed bug of confusing by the < symbol inside the text
175
+ - Fixed bug of dash in selectors
176
+ - Fixed bug of `<nobr>`
177
+ - Fixed bug #2155883 (Nested List Parses Incorrectly)
178
+ - Fixed bug #2155113 (error with unclosed html tags)
179
+
180
+ ## [1.00] - 2008-09-05
181
+ ### Added
182
+ - New method "getAllAttributes" of "simple_html_dom_node"
183
+ - Supports full javascript string in selector: `$e->find("a[onclick=alert('hello')]")`
184
+ ### Changed
185
+ - Changed selector "*=" to case-insentive
186
+ ### Fixed
187
+ - Fixed the bug of selector in some critical conditions
188
+ - Fixed the bug of striping php tags
189
+ - Fixed the bug of remove_noise()
190
+ - Fixed the bug of noise in attributes
191
+
192
+ ## [0.99] - 2008-08-03
193
+ ### Changed
194
+ - Performance tuning (boost 10%)
195
+ - Memory requirement reduced by 25%
196
+ - Changed function name from "file_get_dom()" to "file_get_html()"
197
+ - Changed function name from "str_get_dom()" to "str_get_html()"
198
+ ### Fixed
199
+ - Fixed bug #2011286 (Error with unclosed html tags)
200
+ - Fixed bug #2012551 (Error parsing divs)
201
+ - Fixed bug #2020924 (Error for missed tag)
202
+ - Fixed bug (problem with `<body>` tag's innertext)
203
+
204
+ ## [0.98] - 2008-06-24
205
+ ### Added
206
+ - Supports "multiple class" selector feature: `<div class="a b c"></div>`
207
+ - New "callback function" feature
208
+ - New "multiple selectors" feature: $dom->find('p,a,b')
209
+ - New examples
210
+ - Supports extract contents from HTML features: $dom->plaintext
211
+ ### Changed
212
+ - Performance tuning (boost 20%)
213
+ - Changed simple_html_dom_node method name from "text()" to "makeup()"
214
+ ### Fixed
215
+ - Fixed the bug of $dom->clear()
216
+ - Fixed the bug of text nodes' innertext
217
+ - Fixed the bug of comment nodes' innertext
218
+ - Fixed the bug of decendent selector with optional tags
219
+
220
+ ## [0.97] - 2008-05-09
221
+ ### Added
222
+ - New node type "comment" (eg. $dom->find('comment'))
223
+ - Add self-closing tags: 'base', 'spacer'
224
+ - New example "simple_html_dom_utility.php"
225
+ ### Changed
226
+ - File and class name changed (html_dom_parser->simple_html_dom)
227
+ ### Removed
228
+ - ($dom->save_file) will not support anymore
229
+ - Remove example "example_customize_parser.php"
230
+ ### Fixed
231
+ - Fixed the bug of outertext (th)
232
+ - Fixed the bug of regular expression escaping chars ($dom->find)
233
+ - Fixed the bug while line-breaker and "\t" in tags
234
+
235
+ ## [0.96] - 2008-04-27
236
+ ### Added
237
+ - Reference section in manual
238
+ - Added traverse section in manual
239
+ - Added the solution while server behind proxy in FAQ (Thanks to Yousuke Shaggy)
240
+ - New method to remove attribute.
241
+ - New DOM operations(first_child, last_child, next_sibling, previous_sibling) (Request #1936000)
242
+ ### Changed
243
+ - Now file_get_dom supports full file_get_contents parameters
244
+ ### Fixed
245
+ - Fixed the bug of self-closing tags in the end of file
246
+ - Fixed the bug of blanks in the end of tag
247
+ - Fixed some typo of testcase
248
+
249
+ ## [0.95] - 2008-04-13
250
+ ### Added
251
+ - Supports tag name with namespace
252
+ ### Changed
253
+ - New attribute filters (Thanks to Yousuke Kumakura)
254
+ - Refine structure of testcase
255
+ ### Fixed
256
+ - Fix the bug of optional-closing tags
257
+ - Fix the bug of parsing the line break next to the tag's name
258
+
259
+ ## [0.94] - 2008-04-06
260
+ ### Added
261
+ - Add FAQ section in manual
262
+ ### Fixed
263
+ - Fixed infinity loop while the source content is BAD HTML
264
+ - Fixed the bug of adding new attributes to self closing tags
265
+ - Fixed the bug of customize parser without $dom->remove_noise()
assets/libraries/simplehtmldom/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2019 S.C. Chen, John Schlick, logmanoriginal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
assets/libraries/simplehtmldom/phpcompatibility.xml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ruleset name="PHPCompatibility">
3
+ <description>Defines rules for PHPCompatibility</description>
4
+ <exclude-pattern>./app</exclude-pattern>
5
+ <exclude-pattern>./example</exclude-pattern>
6
+ <exclude-pattern>./manual</exclude-pattern>
7
+ <exclude-pattern>./testcase</exclude-pattern>
8
+ <exclude-pattern>./tests</exclude-pattern>
9
+ <config name="testVersion" value="5.6"/>
10
+ <rule ref="PHPCompatibility" />
11
+ </ruleset>
assets/libraries/simplehtmldom/phpcs.xml ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ruleset name="Ruleset">
3
+ <description>Created with the PHP Coding Standard Generator. http://edorian.github.com/php-coding-standard-generator/</description>
4
+ <exclude-pattern>./app</exclude-pattern>
5
+ <exclude-pattern>./example</exclude-pattern>
6
+ <exclude-pattern>./manual</exclude-pattern>
7
+ <exclude-pattern>./testcase</exclude-pattern>
8
+ <rule ref="Generic.WhiteSpace.DisallowSpaceIndent"/>
9
+ <rule ref="Generic.Classes.DuplicateClassName"/>
10
+ <rule ref="PSR2.Methods.MethodDeclaration" />
11
+ <rule ref="Generic.CodeAnalysis.EmptyStatement"/>
12
+ <rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
13
+ <rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
14
+ <rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
15
+ <rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
16
+ <rule ref="Generic.Functions.OpeningFunctionBraceBsdAllman"/>
17
+ <rule ref="PEAR.Functions.ValidDefaultValue"/>
18
+ <rule ref="PSR2.ControlStructures.ElseIfDeclaration"/>
19
+ <rule ref="PSR2.ControlStructures.ControlStructureSpacing"/>
20
+ <rule ref="Squiz.WhiteSpace.CastSpacing"/>
21
+ <rule ref="Squiz.WhiteSpace.OperatorSpacing"/>
22
+ <rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
23
+ <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"/>
24
+ <rule ref="Squiz.Strings.ConcatenationSpacing">
25
+ <properties>
26
+ <property name="spacing" value="1"/>
27
+ <property name="ignoreNewlines" value="true"/>
28
+ </properties>
29
+ </rule>
30
+ <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing">
31
+ <properties>
32
+ <property name="equalsSpacing" value="1"/>
33
+ </properties>
34
+ </rule>
35
+ <rule ref="Generic.Files.LineLength">
36
+ <properties>
37
+ <property name="lineLimit" value="80"/>
38
+ <property name="absoluteLineLimit" value="120"/>
39
+ </properties>
40
+ </rule>
41
+ <rule ref="Generic.NamingConventions.UpperCaseConstantName"/>
42
+ <rule ref="Generic.PHP.LowerCaseConstant"/>
43
+ <rule ref="Squiz.Strings.DoubleQuoteUsage">
44
+ <exclude name="Squiz.Strings.DoubleQuoteUsage.ContainsVar" />
45
+ </rule>
46
+ <rule ref="Generic.Strings.UnnecessaryStringConcat"/>
47
+ <rule ref="PSR2.Files.EndFileNewline"/>
48
+ </ruleset>
assets/libraries/simplehtmldom/simple_html_dom.php CHANGED
@@ -1,1749 +1,2354 @@
1
- <?php
2
- /**
3
- * Website: http://sourceforge.net/projects/simplehtmldom/
4
- * Additional projects that may be used: http://sourceforge.net/projects/debugobject/
5
- * Acknowledge: Jose Solorzano (https://sourceforge.net/projects/php-html/)
6
- * Contributions by:
7
- * Yousuke Kumakura (Attribute filters)
8
- * Vadim Voituk (Negative indexes supports of "find" method)
9
- * Antcs (Constructor with automatically load contents either text or file/url)
10
- *
11
- * all affected sections have comments starting with "PaperG"
12
- *
13
- * Paperg - Added case insensitive testing of the value of the selector.
14
- * Paperg - Added tag_start for the starting index of tags - NOTE: This works but not accurately.
15
- * This tag_start gets counted AFTER \r\n have been crushed out, and after the remove_noice calls so it will not reflect the REAL position of the tag in the source,
16
- * it will almost always be smaller by some amount.
17
- * We use this to determine how far into the file the tag in question is. This "percentage will never be accurate as the $dom->size is the "real" number of bytes the dom was created from.
18
- * but for most purposes, it's a really good estimation.
19
- * Paperg - Added the forceTagsClosed to the dom constructor. Forcing tags closed is great for malformed html, but it CAN lead to parsing errors.
20
- * Allow the user to tell us how much they trust the html.
21
- * Paperg add the text and plaintext to the selectors for the find syntax. plaintext implies text in the innertext of a node. text implies that the tag is a text node.
22
- * This allows for us to find tags based on the text they contain.
23
- * Create find_ancestor_tag to see if a tag is - at any level - inside of another specific tag.
24
- * Paperg: added parse_charset so that we know about the character set of the source document.
25
- * NOTE: If the user's system has a routine called get_last_retrieve_url_contents_content_type availalbe, we will assume it's returning the content-type header from the
26
- * last transfer or curl_exec, and we will parse that and use it in preference to any other method of charset detection.
27
- *
28
- * Found infinite loop in the case of broken html in restore_noise. Rewrote to protect from that.
29
- * PaperG (John Schlick) Added get_display_size for "IMG" tags.
30
- *
31
- * Licensed under The MIT License
32
- * Redistributions of files must retain the above copyright notice.
33
- *
34
- * @author S.C. Chen <me578022@gmail.com>
35
- * @author John Schlick
36
- * @author Rus Carroll
37
- * @version 1.5 ($Rev: 210 $)
38
- * @package PlaceLocalInclude
39
- * @subpackage simple_html_dom
40
- */
41
-
42
- /**
43
- * All of the Defines for the classes below.
44
- * @author S.C. Chen <me578022@gmail.com>
45
- */
46
-
47
-
48
- define('HDOM_TYPE_ELEMENT', 1);
49
- define('HDOM_TYPE_COMMENT', 2);
50
- define('HDOM_TYPE_TEXT', 3);
51
- define('HDOM_TYPE_ENDTAG', 4);
52
- define('HDOM_TYPE_ROOT', 5);
53
- define('HDOM_TYPE_UNKNOWN', 6);
54
- define('HDOM_QUOTE_DOUBLE', 0);
55
- define('HDOM_QUOTE_SINGLE', 1);
56
- define('HDOM_QUOTE_NO', 3);
57
- define('HDOM_INFO_BEGIN', 0);
58
- define('HDOM_INFO_END', 1);
59
- define('HDOM_INFO_QUOTE', 2);
60
- define('HDOM_INFO_SPACE', 3);
61
- define('HDOM_INFO_TEXT', 4);
62
- define('HDOM_INFO_INNER', 5);
63
- define('HDOM_INFO_OUTER', 6);
64
- define('HDOM_INFO_ENDSPACE',7);
65
- define('DEFAULT_TARGET_CHARSET', 'UTF-8');
66
- define('DEFAULT_BR_TEXT', "\r\n");
67
- define('DEFAULT_SPAN_TEXT', " ");
68
- define('MAX_FILE_SIZE', 600000);
69
-
70
-
71
- // helper functions
72
- // -----------------------------------------------------------------------------
73
- // get html dom from file
74
- // $maxlen is defined in the code as PHP_STREAM_COPY_ALL which is defined as -1.
75
- function file_get_html($url, $use_include_path = false, $context=null, $offset = -1, $maxLen=-1, $lowercase = true, $forceTagsClosed=true, $target_charset = DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
76
- {
77
- // We DO force the tags to be terminated.
78
- $dom = new simple_html_dom(null, $lowercase, $forceTagsClosed, $target_charset, $stripRN, $defaultBRText, $defaultSpanText);
79
- // For sourceforge users: uncomment the next line and comment the retreive_url_contents line 2 lines down if it is not already done.
80
- $contents = file_get_contents($url, $use_include_path, $context, $offset);
81
- // Paperg - use our own mechanism for getting the contents as we want to control the timeout.
82
- //$contents = retrieve_url_contents($url);
83
- if (empty($contents) || strlen($contents) > MAX_FILE_SIZE)
84
- {
85
- return false;
86
- }
87
- // The second parameter can force the selectors to all be lowercase.
88
- $dom->load($contents, $lowercase, $stripRN);
89
- return $dom;
90
- }
91
-
92
- // get html dom from string
93
- function str_get_html($str, $lowercase=true, $forceTagsClosed=true, $target_charset = DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
94
- {
95
- $dom = new simple_html_dom(null, $lowercase, $forceTagsClosed, $target_charset, $stripRN, $defaultBRText, $defaultSpanText);
96
- if (empty($str) || strlen($str) > MAX_FILE_SIZE)
97
- {
98
- $dom->clear();
99
- return false;
100
- }
101
- $dom->load($str, $lowercase, $stripRN);
102
- return $dom;
103
- }
104
-
105
- // dump html dom tree
106
- function dump_html_tree($node, $show_attr=true, $deep=0)
107
- {
108
- $node->dump($node);
109
- }
110
-
111
-
112
- /**
113
- * simple html dom node
114
- * PaperG - added ability for "find" routine to lowercase the value of the selector.
115
- * PaperG - added $tag_start to track the start position of the tag in the total byte index
116
- *
117
- * @package PlaceLocalInclude
118
- */
119
- class simple_html_dom_node
120
- {
121
- public $nodetype = HDOM_TYPE_TEXT;
122
- public $tag = 'text';
123
- public $attr = array();
124
- public $children = array();
125
- public $nodes = array();
126
- public $parent = null;
127
- // The "info" array - see HDOM_INFO_... for what each element contains.
128
- public $_ = array();
129
- public $tag_start = 0;
130
- private $dom = null;
131
-
132
- function __construct($dom)
133
- {
134
- $this->dom = $dom;
135
- $dom->nodes[] = $this;
136
- }
137
-
138
- function __destruct()
139
- {
140
- $this->clear();
141
- }
142
-
143
- function __toString()
144
- {
145
- return $this->outertext();
146
- }
147
-
148
- // clean up memory due to php5 circular references memory leak...
149
- function clear()
150
- {
151
- $this->dom = null;
152
- $this->nodes = null;
153
- $this->parent = null;
154
- $this->children = null;
155
- }
156
-
157
- // dump node's tree
158
- function dump($show_attr=true, $deep=0)
159
- {
160
- $lead = str_repeat(' ', $deep);
161
-
162
- echo $lead.$this->tag;
163
- if ($show_attr && count($this->attr)>0)
164
- {
165
- echo '(';
166
- foreach ($this->attr as $k=>$v)
167
- echo "[$k]=>\"".$this->$k.'", ';
168
- echo ')';
169
- }
170
- echo "\n";
171
-
172
- if ($this->nodes)
173
- {
174
- foreach ($this->nodes as $c)
175
- {
176
- $c->dump($show_attr, $deep+1);
177
- }
178
- }
179
- }
180
-
181
-
182
- // Debugging function to dump a single dom node with a bunch of information about it.
183
- function dump_node($echo=true)
184
- {
185
-
186
- $string = $this->tag;
187
- if (count($this->attr)>0)
188
- {
189
- $string .= '(';
190
- foreach ($this->attr as $k=>$v)
191
- {
192
- $string .= "[$k]=>\"".$this->$k.'", ';
193
- }
194
- $string .= ')';
195
- }
196
- if (count($this->_)>0)
197
- {
198
- $string .= ' $_ (';
199
- foreach ($this->_ as $k=>$v)
200
- {
201
- if (is_array($v))
202
- {
203
- $string .= "[$k]=>(";
204
- foreach ($v as $k2=>$v2)
205
- {
206
- $string .= "[$k2]=>\"".$v2.'", ';
207
- }
208
- $string .= ")";
209
- } else {
210
- $string .= "[$k]=>\"".$v.'", ';
211
- }
212
- }
213
- $string .= ")";
214
- }
215
-
216
- if (isset($this->text))
217
- {
218
- $string .= " text: (" . $this->text . ")";
219
- }
220
-
221
- $string .= " HDOM_INNER_INFO: '";
222
- if (isset($node->_[HDOM_INFO_INNER]))
223
- {
224
- $string .= $node->_[HDOM_INFO_INNER] . "'";
225
- }
226
- else
227
- {
228
- $string .= ' NULL ';
229
- }
230
-
231
- $string .= " children: " . count($this->children);
232
- $string .= " nodes: " . count($this->nodes);
233
- $string .= " tag_start: " . $this->tag_start;
234
- $string .= "\n";
235
-
236
- if ($echo)
237
- {
238
- echo $string;
239
- return;
240
- }
241
- else
242
- {
243
- return $string;
244
- }
245
- }
246
-
247
- // returns the parent of node
248
- // If a node is passed in, it will reset the parent of the current node to that one.
249
- function parent($parent=null)
250
- {
251
- // I am SURE that this doesn't work properly.
252
- // It fails to unset the current node from it's current parents nodes or children list first.
253
- if ($parent !== null)
254
- {
255
- $this->parent = $parent;
256
- $this->parent->nodes[] = $this;
257
- $this->parent->children[] = $this;
258
- }
259
-
260
- return $this->parent;
261
- }
262
-
263
- // verify that node has children
264
- function has_child()
265
- {
266
- return !empty($this->children);
267
- }
268
-
269
- // returns children of node
270
- function children($idx=-1)
271
- {
272
- if ($idx===-1)
273
- {
274
- return $this->children;
275
- }
276
- if (isset($this->children[$idx]))
277
- {
278
- return $this->children[$idx];
279
- }
280
- return null;
281
- }
282
-
283
- // returns the first child of node
284
- function first_child()
285
- {
286
- if (count($this->children)>0)
287
- {
288
- return $this->children[0];
289
- }
290
- return null;
291
- }
292
-
293
- // returns the last child of node
294
- function last_child()
295
- {
296
- if (($count=count($this->children))>0)
297
- {
298
- return $this->children[$count-1];
299
- }
300
- return null;
301
- }
302
-
303
- // returns the next sibling of node
304
- function next_sibling()
305
- {
306
- if ($this->parent===null)
307
- {
308
- return null;
309
- }
310
-
311
- $idx = 0;
312
- $count = count($this->parent->children);
313
- while ($idx<$count && $this!==$this->parent->children[$idx])
314
- {
315
- ++$idx;
316
- }
317
- if (++$idx>=$count)
318
- {
319
- return null;
320
- }
321
- return $this->parent->children[$idx];
322
- }
323
-
324
- // returns the previous sibling of node
325
- function prev_sibling()
326
- {
327
- if ($this->parent===null) return null;
328
- $idx = 0;
329
- $count = count($this->parent->children);
330
- while ($idx<$count && $this!==$this->parent->children[$idx])
331
- ++$idx;
332
- if (--$idx<0) return null;
333
- return $this->parent->children[$idx];
334
- }
335
-
336
- // function to locate a specific ancestor tag in the path to the root.
337
- function find_ancestor_tag($tag)
338
- {
339
- global $debug_object;
340
- if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
341
-
342
- // Start by including ourselves in the comparison.
343
- $returnDom = $this;
344
-
345
- while (!is_null($returnDom))
346
- {
347
- if (is_object($debug_object)) { $debug_object->debug_log(2, "Current tag is: " . $returnDom->tag); }
348
-
349
- if ($returnDom->tag == $tag)
350
- {
351
- break;
352
- }
353
- $returnDom = $returnDom->parent;
354
- }
355
- return $returnDom;
356
- }
357
-
358
- // get dom node's inner html
359
- function innertext()
360
- {
361
- if (isset($this->_[HDOM_INFO_INNER])) return $this->_[HDOM_INFO_INNER];
362
- if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
363
-
364
- $ret = '';
365
- foreach ($this->nodes as $n)
366
- $ret .= $n->outertext();
367
- return $ret;
368
- }
369
-
370
- // get dom node's outer text (with tag)
371
- function outertext()
372
- {
373
- global $debug_object;
374
- if (is_object($debug_object))
375
- {
376
- $text = '';
377
- if ($this->tag == 'text')
378
- {
379
- if (!empty($this->text))
380
- {
381
- $text = " with text: " . $this->text;
382
- }
383
- }
384
- $debug_object->debug_log(1, 'Innertext of tag: ' . $this->tag . $text);
385
- }
386
-
387
- if ($this->tag==='root') return $this->innertext();
388
-
389
- // trigger callback
390
- if ($this->dom && $this->dom->callback!==null)
391
- {
392
- call_user_func_array($this->dom->callback, array($this));
393
- }
394
-
395
- if (isset($this->_[HDOM_INFO_OUTER])) return $this->_[HDOM_INFO_OUTER];
396
- if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
397
-
398
- // render begin tag
399
- if ($this->dom && $this->dom->nodes[$this->_[HDOM_INFO_BEGIN]])
400
- {
401
- $ret = $this->dom->nodes[$this->_[HDOM_INFO_BEGIN]]->makeup();
402
- } else {
403
- $ret = "";
404
- }
405
-
406
- // render inner text
407
- if (isset($this->_[HDOM_INFO_INNER]))
408
- {
409
- // If it's a br tag... don't return the HDOM_INNER_INFO that we may or may not have added.
410
- if ($this->tag != "br")
411
- {
412
- $ret .= $this->_[HDOM_INFO_INNER];
413
- }
414
- } else {
415
- if ($this->nodes)
416
- {
417
- foreach ($this->nodes as $n)
418
- {
419
- $ret .= $this->convert_text($n->outertext());
420
- }
421
- }
422
- }
423
-
424
- // render end tag
425
- if (isset($this->_[HDOM_INFO_END]) && $this->_[HDOM_INFO_END]!=0)
426
- $ret .= '</'.$this->tag.'>';
427
- return $ret;
428
- }
429
-
430
- // get dom node's plain text
431
- function text()
432
- {
433
- if (isset($this->_[HDOM_INFO_INNER])) return $this->_[HDOM_INFO_INNER];
434
- switch ($this->nodetype)
435
- {
436
- case HDOM_TYPE_TEXT: return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
437
- case HDOM_TYPE_COMMENT: return '';
438
- case HDOM_TYPE_UNKNOWN: return '';
439
- }
440
- if (strcasecmp($this->tag, 'script')===0) return '';
441
- if (strcasecmp($this->tag, 'style')===0) return '';
442
-
443
- $ret = '';
444
- // In rare cases, (always node type 1 or HDOM_TYPE_ELEMENT - observed for some span tags, and some p tags) $this->nodes is set to NULL.
445
- // NOTE: This indicates that there is a problem where it's set to NULL without a clear happening.
446
- // WHY is this happening?
447
- if (!is_null($this->nodes))
448
- {
449
- foreach ($this->nodes as $n)
450
- {
451
- $ret .= $this->convert_text($n->text());
452
- }
453
-
454
- // If this node is a span... add a space at the end of it so multiple spans don't run into each other. This is plaintext after all.
455
- if ($this->tag == "span")
456
- {
457
- $ret .= $this->dom->default_span_text;
458
- }
459
-
460
-
461
- }
462
- return $ret;
463
- }
464
-
465
- function xmltext()
466
- {
467
- $ret = $this->innertext();
468
- $ret = str_ireplace('<![CDATA[', '', $ret);
469
- $ret = str_replace(']]>', '', $ret);
470
- return $ret;
471
- }
472
-
473
- // build node's text with tag
474
- function makeup()
475
- {
476
- // text, comment, unknown
477
- if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
478
-
479
- $ret = '<'.$this->tag;
480
- $i = -1;
481
-
482
- foreach ($this->attr as $key=>$val)
483
- {
484
- ++$i;
485
-
486
- // skip removed attribute
487
- if ($val===null || $val===false)
488
- continue;
489
-
490
- $ret .= $this->_[HDOM_INFO_SPACE][$i][0];
491
- //no value attr: nowrap, checked selected...
492
- if ($val===true)
493
- $ret .= $key;
494
- else {
495
- switch ($this->_[HDOM_INFO_QUOTE][$i])
496
- {
497
- case HDOM_QUOTE_DOUBLE: $quote = '"'; break;
498
- case HDOM_QUOTE_SINGLE: $quote = '\''; break;
499
- default: $quote = '';
500
- }
501
- $ret .= $key.$this->_[HDOM_INFO_SPACE][$i][1].'='.$this->_[HDOM_INFO_SPACE][$i][2].$quote.$val.$quote;
502
- }
503
- }
504
- $ret = $this->dom->restore_noise($ret);
505
- return $ret . $this->_[HDOM_INFO_ENDSPACE] . '>';
506
- }
507
-
508
- // find elements by css selector
509
- //PaperG - added ability for find to lowercase the value of the selector.
510
- function find($selector, $idx=null, $lowercase=false)
511
- {
512
- $selectors = $this->parse_selector($selector);
513
- if (($count=count($selectors))===0) return array();
514
- $found_keys = array();
515
-
516
- // find each selector
517
- for ($c=0; $c<$count; ++$c)
518
- {
519
- // The change on the below line was documented on the sourceforge code tracker id 2788009
520
- // used to be: if (($levle=count($selectors[0]))===0) return array();
521
- if (($levle=count($selectors[$c]))===0) return array();
522
- if (!isset($this->_[HDOM_INFO_BEGIN])) return array();
523
-
524
- $head = array($this->_[HDOM_INFO_BEGIN]=>1);
525
-
526
- // handle descendant selectors, no recursive!
527
- for ($l=0; $l<$levle; ++$l)
528
- {
529
- $ret = array();
530
- foreach ($head as $k=>$v)
531
- {
532
- $n = ($k===-1) ? $this->dom->root : $this->dom->nodes[$k];
533
- //PaperG - Pass this optional parameter on to the seek function.
534
- $n->seek($selectors[$c][$l], $ret, $lowercase);
535
- }
536
- $head = $ret;
537
- }
538
-
539
- foreach ($head as $k=>$v)
540
- {
541
- if (!isset($found_keys[$k]))
542
- {
543
- $found_keys[$k] = 1;
544
- }
545
- }
546
- }
547
-
548
- // sort keys
549
- ksort($found_keys);
550
-
551
- $found = array();
552
- foreach ($found_keys as $k=>$v)
553
- $found[] = $this->dom->nodes[$k];
554
-
555
- // return nth-element or array
556
- if (is_null($idx)) return $found;
557
- else if ($idx<0) $idx = count($found) + $idx;
558
- return (isset($found[$idx])) ? $found[$idx] : null;
559
- }
560
-
561
- // seek for given conditions
562
- // PaperG - added parameter to allow for case insensitive testing of the value of a selector.
563
- protected function seek($selector, &$ret, $lowercase=false)
564
- {
565
- global $debug_object;
566
- if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
567
-
568
- list($tag, $key, $val, $exp, $no_key) = $selector;
569
-
570
- // xpath index
571
- if ($tag && $key && is_numeric($key))
572
- {
573
- $count = 0;
574
- foreach ($this->children as $c)
575
- {
576
- if ($tag==='*' || $tag===$c->tag) {
577
- if (++$count==$key) {
578
- $ret[$c->_[HDOM_INFO_BEGIN]] = 1;
579
- return;
580
- }
581
- }
582
- }
583
- return;
584
- }
585
-
586
- $end = (!empty($this->_[HDOM_INFO_END])) ? $this->_[HDOM_INFO_END] : 0;
587
- if ($end==0) {
588
- $parent = $this->parent;
589
- while (!isset($parent->_[HDOM_INFO_END]) && $parent!==null) {
590
- $end -= 1;
591
- $parent = $parent->parent;
592
- }
593
- $end += $parent->_[HDOM_INFO_END];
594
- }
595
-
596
- for ($i=$this->_[HDOM_INFO_BEGIN]+1; $i<$end; ++$i) {
597
- $node = $this->dom->nodes[$i];
598
-
599
- $pass = true;
600
-
601
- if ($tag==='*' && !$key) {
602
- if (in_array($node, $this->children, true))
603
- $ret[$i] = 1;
604
- continue;
605
- }
606
-
607
- // compare tag
608
- if ($tag && $tag!=$node->tag && $tag!=='*') {$pass=false;}
609
- // compare key
610
- if ($pass && $key) {
611
- if ($no_key) {
612
- if (isset($node->attr[$key])) $pass=false;
613
- } else {
614
- if (($key != "plaintext") && !isset($node->attr[$key])) $pass=false;
615
- }
616
- }
617
- // compare value
618
- if ($pass && $key && $val && $val!=='*') {
619
- // If they have told us that this is a "plaintext" search then we want the plaintext of the node - right?
620
- if ($key == "plaintext") {
621
- // $node->plaintext actually returns $node->text();
622
- $nodeKeyValue = $node->text();
623
- } else {
624
- // this is a normal search, we want the value of that attribute of the tag.
625
- $nodeKeyValue = $node->attr[$key];
626
- }
627
- if (is_object($debug_object)) {$debug_object->debug_log(2, "testing node: " . $node->tag . " for attribute: " . $key . $exp . $val . " where nodes value is: " . $nodeKeyValue);}
628
-
629
- //PaperG - If lowercase is set, do a case insensitive test of the value of the selector.
630
- if ($lowercase) {
631
- $check = $this->match($exp, strtolower($val), strtolower($nodeKeyValue));
632
- } else {
633
- $check = $this->match($exp, $val, $nodeKeyValue);
634
- }
635
- if (is_object($debug_object)) {$debug_object->debug_log(2, "after match: " . ($check ? "true" : "false"));}
636
-
637
- // handle multiple class
638
- if (!$check && strcasecmp($key, 'class')===0) {
639
- foreach (explode(' ',$node->attr[$key]) as $k) {
640
- // Without this, there were cases where leading, trailing, or double spaces lead to our comparing blanks - bad form.
641
- if (!empty($k)) {
642
- if ($lowercase) {
643
- $check = $this->match($exp, strtolower($val), strtolower($k));
644
- } else {
645
- $check = $this->match($exp, $val, $k);
646
- }
647
- if ($check) break;
648
- }
649
- }
650
- }
651
- if (!$check) $pass = false;
652
- }
653
- if ($pass) $ret[$i] = 1;
654
- unset($node);
655
- }
656
- // It's passed by reference so this is actually what this function returns.
657
- if (is_object($debug_object)) {$debug_object->debug_log(1, "EXIT - ret: ", $ret);}
658
- }
659
-
660
- protected function match($exp, $pattern, $value) {
661
- global $debug_object;
662
- if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
663
-
664
- switch ($exp) {
665
- case '=':
666
- return ($value===$pattern);
667
- case '!=':
668
- return ($value!==$pattern);
669
- case '^=':
670
- return preg_match("/^".preg_quote($pattern,'/')."/", $value);
671
- case '$=':
672
- return preg_match("/".preg_quote($pattern,'/')."$/", $value);
673
- case '*=':
674
- if ($pattern[0]=='/') {
675
- return preg_match($pattern, $value);
676
- }
677
- return preg_match("/".$pattern."/i", $value);
678
- }
679
- return false;
680
- }
681
-
682
- protected function parse_selector($selector_string) {
683
- global $debug_object;
684
- if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
685
-
686
- // pattern of CSS selectors, modified from mootools
687
- // Paperg: Add the colon to the attrbute, so that it properly finds <tag attr:ibute="something" > like google does.
688
- // Note: if you try to look at this attribute, yo MUST use getAttribute since $dom->x:y will fail the php syntax check.
689
- // Notice the \[ starting the attbute? and the @? following? This implies that an attribute can begin with an @ sign that is not captured.
690
- // This implies that an html attribute specifier may start with an @ sign that is NOT captured by the expression.
691
- // farther study is required to determine of this should be documented or removed.
692
- // $pattern = "/([\w-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is";
693
-
694
- // original $pattern = "/([\w-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-:]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is";
695
- $pattern = "/([\w\-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w\-:]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is";
696
- preg_match_all($pattern, trim($selector_string).' ', $matches, PREG_SET_ORDER);
697
- if (is_object($debug_object)) {$debug_object->debug_log(2, "Matches Array: ", $matches);}
698
-
699
- $selectors = array();
700
- $result = array();
701
-
702
-
703
- foreach ($matches as $m) {
704
- $m[0] = trim($m[0]);
705
- if ($m[0]==='' || $m[0]==='/' || $m[0]==='//') continue;
706
- // for browser generated xpath
707
- if ($m[1]==='tbody') continue;
708
-
709
- list($tag, $key, $val, $exp, $no_key) = array($m[1], null, null, '=', false);
710
- if (!empty($m[2])) {$key='id'; $val=$m[2];}
711
- if (!empty($m[3])) {$key='class'; $val=$m[3];}
712
- if (!empty($m[4])) {$key=$m[4];}
713
- if (!empty($m[5])) {$exp=$m[5];}
714
- if (!empty($m[6])) {$val=$m[6];}
715
-
716
- // convert to lowercase
717
- if ($this->dom->lowercase) {$tag=strtolower($tag); $key=strtolower($key);}
718
- //elements that do NOT have the specified attribute
719
- if (isset($key[0]) && $key[0]==='!') {$key=substr($key, 1); $no_key=true;}
720
-
721
- $result[] = array($tag, $key, $val, $exp, $no_key);
722
- if (trim($m[7])===',') {
723
- $selectors[] = $result;
724
- $result = array();
725
- }
726
- }
727
- if (count($result)>0)
728
- $selectors[] = $result;
729
- return $selectors;
730
- }
731
-
732
- function __get($name)
733
- {
734
- if (isset($this->attr[$name]))
735
- {
736
- return $this->convert_text($this->attr[$name]);
737
- }
738
- switch ($name)
739
- {
740
- case 'outertext': return $this->outertext();
741
- case 'innertext': return $this->innertext();
742
- case 'plaintext': return $this->text();
743
- case 'xmltext': return $this->xmltext();
744
- default: return array_key_exists($name, $this->attr);
745
- }
746
- }
747
-
748
- function __set($name, $value)
749
- {
750
- global $debug_object;
751
- if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
752
-
753
- switch ($name)
754
- {
755
- case 'outertext': return $this->_[HDOM_INFO_OUTER] = $value;
756
- case 'innertext':
757
- if (isset($this->_[HDOM_INFO_TEXT])) return $this->_[HDOM_INFO_TEXT] = $value;
758
- return $this->_[HDOM_INFO_INNER] = $value;
759
- }
760
- if (!isset($this->attr[$name]))
761
- {
762
- $this->_[HDOM_INFO_SPACE][] = array(' ', '', '');
763
- $this->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_DOUBLE;
764
- }
765
- $this->attr[$name] = $value;
766
- }
767
-
768
- function __isset($name)
769
- {
770
- switch ($name)
771
- {
772
- case 'outertext': return true;
773
- case 'innertext': return true;
774
- case 'plaintext': return true;
775
- }
776
- //no value attr: nowrap, checked selected...
777
- return (array_key_exists($name, $this->attr)) ? true : isset($this->attr[$name]);
778
- }
779
-
780
- function __unset($name) {
781
- if (isset($this->attr[$name]))
782
- unset($this->attr[$name]);
783
- }
784
-
785
- // PaperG - Function to convert the text from one character set to another if the two sets are not the same.
786
- function convert_text($text)
787
- {
788
- global $debug_object;
789
- if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
790
-
791
- $converted_text = $text;
792
-
793
- $sourceCharset = "";
794
- $targetCharset = "";
795
-
796
- if ($this->dom)
797
- {
798
- $sourceCharset = strtoupper($this->dom->_charset);
799
- $targetCharset = strtoupper($this->dom->_target_charset);
800
- }
801
- if (is_object($debug_object)) {$debug_object->debug_log(3, "source charset: " . $sourceCharset . " target charaset: " . $targetCharset);}
802
-
803
- if (!empty($sourceCharset) && !empty($targetCharset) && (strcasecmp($sourceCharset, $targetCharset) != 0))
804
- {
805
- // Check if the reported encoding could have been incorrect and the text is actually already UTF-8
806
- if ((strcasecmp($targetCharset, 'UTF-8') == 0) && ($this->is_utf8($text)))
807
- {
808
- $converted_text = $text;
809
- }
810
- else
811
- {
812
- $converted_text = iconv($sourceCharset, $targetCharset, $text);
813
- }
814
- }
815
-
816
- // Lets make sure that we don't have that silly BOM issue with any of the utf-8 text we output.
817
- if ($targetCharset == 'UTF-8')
818
- {
819
- if (substr($converted_text, 0, 3) == "\xef\xbb\xbf")
820
- {
821
- $converted_text = substr($converted_text, 3);
822
- }
823
- if (substr($converted_text, -3) == "\xef\xbb\xbf")
824
- {
825
- $converted_text = substr($converted_text, 0, -3);
826
- }
827
- }
828
-
829
- return $converted_text;
830
- }
831
-
832
- /**
833
- * Returns true if $string is valid UTF-8 and false otherwise.
834
- *
835
- * @param mixed $str String to be tested
836
- * @return boolean
837
- */
838
- static function is_utf8($str)
839
- {
840
- $c=0; $b=0;
841
- $bits=0;
842
- $len=strlen($str);
843
- for($i=0; $i<$len; $i++)
844
- {
845
- $c=ord($str[$i]);
846
- if($c > 128)
847
- {
848
- if(($c >= 254)) return false;
849
- elseif($c >= 252) $bits=6;
850
- elseif($c >= 248) $bits=5;
851
- elseif($c >= 240) $bits=4;
852
- elseif($c >= 224) $bits=3;
853
- elseif($c >= 192) $bits=2;
854
- else return false;
855
- if(($i+$bits) > $len) return false;
856
- while($bits > 1)
857
- {
858
- $i++;
859
- $b=ord($str[$i]);
860
- if($b < 128 || $b > 191) return false;
861
- $bits--;
862
- }
863
- }
864
- }
865
- return true;
866
- }
867
- /*
868
- function is_utf8($string)
869
- {
870
- //this is buggy
871
- return (utf8_encode(utf8_decode($string)) == $string);
872
- }
873
- */
874
-
875
- /**
876
- * Function to try a few tricks to determine the displayed size of an img on the page.
877
- * NOTE: This will ONLY work on an IMG tag. Returns FALSE on all other tag types.
878
- *
879
- * @author John Schlick
880
- * @version April 19 2012
881
- * @return array an array containing the 'height' and 'width' of the image on the page or -1 if we can't figure it out.
882
- */
883
- function get_display_size()
884
- {
885
- global $debug_object;
886
-
887
- $width = -1;
888
- $height = -1;
889
-
890
- if ($this->tag !== 'img')
891
- {
892
- return false;
893
- }
894
-
895
- // See if there is aheight or width attribute in the tag itself.
896
- if (isset($this->attr['width']))
897
- {
898
- $width = $this->attr['width'];
899
- }
900
-
901
- if (isset($this->attr['height']))
902
- {
903
- $height = $this->attr['height'];
904
- }
905
-
906
- // Now look for an inline style.
907
- if (isset($this->attr['style']))
908
- {
909
- // Thanks to user gnarf from stackoverflow for this regular expression.
910
- $attributes = array();
911
- preg_match_all("/([\w-]+)\s*:\s*([^;]+)\s*;?/", $this->attr['style'], $matches, PREG_SET_ORDER);
912
- foreach ($matches as $match) {
913
- $attributes[$match[1]] = $match[2];
914
- }
915
-
916
- // If there is a width in the style attributes:
917
- if (isset($attributes['width']) && $width == -1)
918
- {
919
- // check that the last two characters are px (pixels)
920
- if (strtolower(substr($attributes['width'], -2)) == 'px')
921
- {
922
- $proposed_width = substr($attributes['width'], 0, -2);
923
- // Now make sure that it's an integer and not something stupid.
924
- if (filter_var($proposed_width, FILTER_VALIDATE_INT))
925
- {
926
- $width = $proposed_width;
927
- }
928
- }
929
- }
930
-
931
- // If there is a width in the style attributes:
932
- if (isset($attributes['height']) && $height == -1)
933
- {
934
- // check that the last two characters are px (pixels)
935
- if (strtolower(substr($attributes['height'], -2)) == 'px')
936
- {
937
- $proposed_height = substr($attributes['height'], 0, -2);
938
- // Now make sure that it's an integer and not something stupid.
939
- if (filter_var($proposed_height, FILTER_VALIDATE_INT))
940
- {
941
- $height = $proposed_height;
942
- }
943
- }
944
- }
945
-
946
- }
947
-
948
- // Future enhancement:
949
- // Look in the tag to see if there is a class or id specified that has a height or width attribute to it.
950
-
951
- // Far future enhancement
952
- // Look at all the parent tags of this image to see if they specify a class or id that has an img selector that specifies a height or width
953
- // Note that in this case, the class or id will have the img subselector for it to apply to the image.
954
-
955
- // ridiculously far future development
956
- // If the class or id is specified in a SEPARATE css file thats not on the page, go get it and do what we were just doing for the ones on the page.
957
-
958
- $result = array('height' => $height,
959
- 'width' => $width);
960
- return $result;
961
- }
962
-
963
- // camel naming conventions
964
- function getAllAttributes() {return $this->attr;}
965
- function getAttribute($name) {return $this->__get($name);}
966
- function setAttribute($name, $value) {$this->__set($name, $value);}
967
- function hasAttribute($name) {return $this->__isset($name);}
968
- function removeAttribute($name) {$this->__set($name, null);}
969
- function getElementById($id) {return $this->find("#$id", 0);}
970
- function getElementsById($id, $idx=null) {return $this->find("#$id", $idx);}
971
- function getElementByTagName($name) {return $this->find($name, 0);}
972
- function getElementsByTagName($name, $idx=null) {return $this->find($name, $idx);}
973
- function parentNode() {return $this->parent();}
974
- function childNodes($idx=-1) {return $this->children($idx);}
975
- function firstChild() {return $this->first_child();}
976
- function lastChild() {return $this->last_child();}
977
- function nextSibling() {return $this->next_sibling();}
978
- function previousSibling() {return $this->prev_sibling();}
979
- function hasChildNodes() {return $this->has_child();}
980
- function nodeName() {return $this->tag;}
981
- function appendChild($node) {$node->parent($this); return $node;}
982
-
983
- }
984
-
985
- /**
986
- * simple html dom parser
987
- * Paperg - in the find routine: allow us to specify that we want case insensitive testing of the value of the selector.
988
- * Paperg - change $size from protected to public so we can easily access it
989
- * Paperg - added ForceTagsClosed in the constructor which tells us whether we trust the html or not. Default is to NOT trust it.
990
- *
991
- * @package PlaceLocalInclude
992
- */
993
- class simple_html_dom
994
- {
995
- public $root = null;
996
- public $nodes = array();
997
- public $callback = null;
998
- public $lowercase = false;
999
- // Used to keep track of how large the text was when we started.
1000
- public $original_size;
1001
- public $size;
1002
- protected $pos;
1003
- protected $doc;
1004
- protected $char;
1005
- protected $cursor;
1006
- protected $parent;
1007
- protected $noise = array();
1008
- protected $token_blank = " \t\r\n";
1009
- protected $token_equal = ' =/>';
1010
- protected $token_slash = " />\r\n\t";
1011
- protected $token_attr = ' >';
1012
- // Note that this is referenced by a child node, and so it needs to be public for that node to see this information.
1013
- public $_charset = '';
1014
- public $_target_charset = '';
1015
- protected $default_br_text = "";
1016
- public $default_span_text = "";
1017
-
1018
- // use isset instead of in_array, performance boost about 30%...
1019
- protected $self_closing_tags = array('img'=>1, 'br'=>1, 'input'=>1, 'meta'=>1, 'link'=>1, 'hr'=>1, 'base'=>1, 'embed'=>1, 'spacer'=>1);
1020
- protected $block_tags = array('root'=>1, 'body'=>1, 'form'=>1, 'div'=>1, 'span'=>1, 'table'=>1);
1021
- // Known sourceforge issue #2977341
1022
- // B tags that are not closed cause us to return everything to the end of the document.
1023
- protected $optional_closing_tags = array(
1024
- 'tr'=>array('tr'=>1, 'td'=>1, 'th'=>1),
1025
- 'th'=>array('th'=>1),
1026
- 'td'=>array('td'=>1),
1027
- 'li'=>array('li'=>1),
1028
- 'dt'=>array('dt'=>1, 'dd'=>1),
1029
- 'dd'=>array('dd'=>1, 'dt'=>1),
1030
- 'dl'=>array('dd'=>1, 'dt'=>1),
1031
- 'p'=>array('p'=>1),
1032
- 'nobr'=>array('nobr'=>1),
1033
- 'b'=>array('b'=>1),
1034
- 'option'=>array('option'=>1),
1035
- );
1036
-
1037
- function __construct($str=null, $lowercase=true, $forceTagsClosed=true, $target_charset=DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
1038
- {
1039
- if ($str)
1040
- {
1041
- if (preg_match("/^http:\/\//i",$str) || is_file($str))
1042
- {
1043
- $this->load_file($str);
1044
- }
1045
- else
1046
- {
1047
- $this->load($str, $lowercase, $stripRN, $defaultBRText, $defaultSpanText);
1048
- }
1049
- }
1050
- // Forcing tags to be closed implies that we don't trust the html, but it can lead to parsing errors if we SHOULD trust the html.
1051
- if (!$forceTagsClosed) {
1052
- $this->optional_closing_array=array();
1053
- }
1054
- $this->_target_charset = $target_charset;
1055
- }
1056
-
1057
- function __destruct()
1058
- {
1059
- $this->clear();
1060
- }
1061
-
1062
- // load html from string
1063
- function load($str, $lowercase=true, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
1064
- {
1065
- global $debug_object;
1066
-
1067
- // prepare
1068
- $this->prepare($str, $lowercase, $stripRN, $defaultBRText, $defaultSpanText);
1069
- // strip out cdata
1070
- $this->remove_noise("'<!\[CDATA\[(.*?)\]\]>'is", true);
1071
- // strip out comments
1072
- $this->remove_noise("'<!--(.*?)-->'is");
1073
- // Per sourceforge http://sourceforge.net/tracker/?func=detail&aid=2949097&group_id=218559&atid=1044037
1074
- // Script tags removal now preceeds style tag removal.
1075
- // strip out <script> tags
1076
- $this->remove_noise("'<\s*script[^>]*[^/]>(.*?)<\s*/\s*script\s*>'is");
1077
- $this->remove_noise("'<\s*script\s*>(.*?)<\s*/\s*script\s*>'is");
1078
- // strip out <style> tags
1079
- $this->remove_noise("'<\s*style[^>]*[^/]>(.*?)<\s*/\s*style\s*>'is");
1080
- $this->remove_noise("'<\s*style\s*>(.*?)<\s*/\s*style\s*>'is");
1081
- // strip out preformatted tags
1082
- $this->remove_noise("'<\s*(?:code)[^>]*>(.*?)<\s*/\s*(?:code)\s*>'is");
1083
- // strip out server side scripts
1084
- $this->remove_noise("'(<\?)(.*?)(\?>)'s", true);
1085
- // strip smarty scripts
1086
- $this->remove_noise("'(\{\w)(.*?)(\})'s", true);
1087
-
1088
- // parsing
1089
- while ($this->parse());
1090
- // end
1091
- $this->root->_[HDOM_INFO_END] = $this->cursor;
1092
- $this->parse_charset();
1093
-
1094
- // make load function chainable
1095
- return $this;
1096
-
1097
- }
1098
-
1099
- // load html from file
1100
- function load_file()
1101
- {
1102
- $args = func_get_args();
1103
- $this->load(call_user_func_array('file_get_contents', $args), true);
1104
- // Throw an error if we can't properly load the dom.
1105
- if (($error=error_get_last())!==null) {
1106
- $this->clear();
1107
- return false;
1108
- }
1109
- }
1110
-
1111
- // set callback function
1112
- function set_callback($function_name)
1113
- {
1114
- $this->callback = $function_name;
1115
- }
1116
-
1117
- // remove callback function
1118
- function remove_callback()
1119
- {
1120
- $this->callback = null;
1121
- }
1122
-
1123
- // save dom as string
1124
- function save($filepath='')
1125
- {
1126
- $ret = $this->root->innertext();
1127
- if ($filepath!=='') file_put_contents($filepath, $ret, LOCK_EX);
1128
- return $ret;
1129
- }
1130
-
1131
- // find dom node by css selector
1132
- // Paperg - allow us to specify that we want case insensitive testing of the value of the selector.
1133
- function find($selector, $idx=null, $lowercase=false)
1134
- {
1135
- return $this->root->find($selector, $idx, $lowercase);
1136
- }
1137
-
1138
- // clean up memory due to php5 circular references memory leak...
1139
- function clear()
1140
- {
1141
- foreach ($this->nodes as $n) {$n->clear(); $n = null;}
1142
- // This add next line is documented in the sourceforge repository. 2977248 as a fix for ongoing memory leaks that occur even with the use of clear.
1143
- if (isset($this->children)) foreach ($this->children as $n) {$n->clear(); $n = null;}
1144
- if (isset($this->parent)) {$this->parent->clear(); unset($this->parent);}
1145
- if (isset($this->root)) {$this->root->clear(); unset($this->root);}
1146
- unset($this->doc);
1147
- unset($this->noise);
1148
- }
1149
-
1150
- function dump($show_attr=true)
1151
- {
1152
- $this->root->dump($show_attr);
1153
- }
1154
-
1155
- // prepare HTML data and init everything
1156
- protected function prepare($str, $lowercase=true, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
1157
- {
1158
- $this->clear();
1159
-
1160
- // set the length of content before we do anything to it.
1161
- $this->size = strlen($str);
1162
- // Save the original size of the html that we got in. It might be useful to someone.
1163
- $this->original_size = $this->size;
1164
-
1165
- //before we save the string as the doc... strip out the \r \n's if we are told to.
1166
- if ($stripRN) {
1167
- $str = str_replace("\r", " ", $str);
1168
- $str = str_replace("\n", " ", $str);
1169
-
1170
- // set the length of content since we have changed it.
1171
- $this->size = strlen($str);
1172
- }
1173
-
1174
- $this->doc = $str;
1175
- $this->pos = 0;
1176
- $this->cursor = 1;
1177
- $this->noise = array();
1178
- $this->nodes = array();
1179
- $this->lowercase = $lowercase;
1180
- $this->default_br_text = $defaultBRText;
1181
- $this->default_span_text = $defaultSpanText;
1182
- $this->root = new simple_html_dom_node($this);
1183
- $this->root->tag = 'root';
1184
- $this->root->_[HDOM_INFO_BEGIN] = -1;
1185
- $this->root->nodetype = HDOM_TYPE_ROOT;
1186
- $this->parent = $this->root;
1187
- if ($this->size>0) $this->char = $this->doc[0];
1188
- }
1189
-
1190
- // parse html content
1191
- protected function parse()
1192
- {
1193
- if (($s = $this->copy_until_char('<'))==='')
1194
- {
1195
- return $this->read_tag();
1196
- }
1197
-
1198
- // text
1199
- $node = new simple_html_dom_node($this);
1200
- ++$this->cursor;
1201
- $node->_[HDOM_INFO_TEXT] = $s;
1202
- $this->link_nodes($node, false);
1203
- return true;
1204
- }
1205
-
1206
- // PAPERG - dkchou - added this to try to identify the character set of the page we have just parsed so we know better how to spit it out later.
1207
- // NOTE: IF you provide a routine called get_last_retrieve_url_contents_content_type which returns the CURLINFO_CONTENT_TYPE from the last curl_exec
1208
- // (or the content_type header from the last transfer), we will parse THAT, and if a charset is specified, we will use it over any other mechanism.
1209
- protected function parse_charset()
1210
- {
1211
- global $debug_object;
1212
-
1213
- $charset = null;
1214
-
1215
- if (function_exists('get_last_retrieve_url_contents_content_type'))
1216
- {
1217
- $contentTypeHeader = get_last_retrieve_url_contents_content_type();
1218
- $success = preg_match('/charset=(.+)/', $contentTypeHeader, $matches);
1219
- if ($success)
1220
- {
1221
- $charset = $matches[1];
1222
- if (is_object($debug_object)) {$debug_object->debug_log(2, 'header content-type found charset of: ' . $charset);}
1223
- }
1224
-
1225
- }
1226
-
1227
- if (empty($charset))
1228
- {
1229
- $el = $this->root->find('meta[http-equiv=Content-Type]',0, true);
1230
- if (!empty($el))
1231
- {
1232
- $fullvalue = $el->content;
1233
- if (is_object($debug_object)) {$debug_object->debug_log(2, 'meta content-type tag found' . $fullvalue);}
1234
-
1235
- if (!empty($fullvalue))
1236
- {
1237
- $success = preg_match('/charset=(.+)/i', $fullvalue, $matches);
1238
- if ($success)
1239
- {
1240
- $charset = $matches[1];
1241
- }
1242
- else
1243
- {
1244
- // If there is a meta tag, and they don't specify the character set, research says that it's typically ISO-8859-1
1245
- if (is_object($debug_object)) {$debug_object->debug_log(2, 'meta content-type tag couldn\'t be parsed. using iso-8859 default.');}
1246
- $charset = 'ISO-8859-1';
1247
- }
1248
- }
1249
- }
1250
- }
1251
-
1252
- // If we couldn't find a charset above, then lets try to detect one based on the text we got...
1253
- if (empty($charset))
1254
- {
1255
- // Use this in case mb_detect_charset isn't installed/loaded on this machine.
1256
- $charset = false;
1257
- if (function_exists('mb_detect_encoding'))
1258
- {
1259
- // Have php try to detect the encoding from the text given to us.
1260
- $charset = mb_detect_encoding($this->root->plaintext . "ascii", $encoding_list = array( "UTF-8", "CP1252" ) );
1261
- if (is_object($debug_object)) {$debug_object->debug_log(2, 'mb_detect found: ' . $charset);}
1262
- }
1263
-
1264
- // and if this doesn't work... then we need to just wrongheadedly assume it's UTF-8 so that we can move on - cause this will usually give us most of what we need...
1265
- if ($charset === false)
1266
- {
1267
- if (is_object($debug_object)) {$debug_object->debug_log(2, 'since mb_detect failed - using default of utf-8');}
1268
- $charset = 'UTF-8';
1269
- }
1270
- }
1271
-
1272
- // Since CP1252 is a superset, if we get one of it's subsets, we want it instead.
1273
- if ((strtolower($charset) == strtolower('ISO-8859-1')) || (strtolower($charset) == strtolower('Latin1')) || (strtolower($charset) == strtolower('Latin-1')))
1274
- {
1275
- if (is_object($debug_object)) {$debug_object->debug_log(2, 'replacing ' . $charset . ' with CP1252 as its a superset');}
1276
- $charset = 'CP1252';
1277
- }
1278
-
1279
- if (is_object($debug_object)) {$debug_object->debug_log(1, 'EXIT - ' . $charset);}
1280
-
1281
- return $this->_charset = $charset;
1282
- }
1283
-
1284
- // read tag info
1285
- protected function read_tag()
1286
- {
1287
- if ($this->char!=='<')
1288
- {
1289
- $this->root->_[HDOM_INFO_END] = $this->cursor;
1290
- return false;
1291
- }
1292
- $begin_tag_pos = $this->pos;
1293
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1294
-
1295
- // end tag
1296
- if ($this->char==='/')
1297
- {
1298
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1299
- // This represents the change in the simple_html_dom trunk from revision 180 to 181.
1300
- // $this->skip($this->token_blank_t);
1301
- $this->skip($this->token_blank);
1302
- $tag = $this->copy_until_char('>');
1303
-
1304
- // skip attributes in end tag
1305
- if (($pos = strpos($tag, ' '))!==false)
1306
- $tag = substr($tag, 0, $pos);
1307
-
1308
- $parent_lower = strtolower($this->parent->tag);
1309
- $tag_lower = strtolower($tag);
1310
-
1311
- if ($parent_lower!==$tag_lower)
1312
- {
1313
- if (isset($this->optional_closing_tags[$parent_lower]) && isset($this->block_tags[$tag_lower]))
1314
- {
1315
- $this->parent->_[HDOM_INFO_END] = 0;
1316
- $org_parent = $this->parent;
1317
-
1318
- while (($this->parent->parent) && strtolower($this->parent->tag)!==$tag_lower)
1319
- $this->parent = $this->parent->parent;
1320
-
1321
- if (strtolower($this->parent->tag)!==$tag_lower) {
1322
- $this->parent = $org_parent; // restore origonal parent
1323
- if ($this->parent->parent) $this->parent = $this->parent->parent;
1324
- $this->parent->_[HDOM_INFO_END] = $this->cursor;
1325
- return $this->as_text_node($tag);
1326
- }
1327
- }
1328
- else if (($this->parent->parent) && isset($this->block_tags[$tag_lower]))
1329
- {
1330
- $this->parent->_[HDOM_INFO_END] = 0;
1331
- $org_parent = $this->parent;
1332
-
1333
- while (($this->parent->parent) && strtolower($this->parent->tag)!==$tag_lower)
1334
- $this->parent = $this->parent->parent;
1335
-
1336
- if (strtolower($this->parent->tag)!==$tag_lower)
1337
- {
1338
- $this->parent = $org_parent; // restore origonal parent
1339
- $this->parent->_[HDOM_INFO_END] = $this->cursor;
1340
- return $this->as_text_node($tag);
1341
- }
1342
- }
1343
- else if (($this->parent->parent) && strtolower($this->parent->parent->tag)===$tag_lower)
1344
- {
1345
- $this->parent->_[HDOM_INFO_END] = 0;
1346
- $this->parent = $this->parent->parent;
1347
- }
1348
- else
1349
- return $this->as_text_node($tag);
1350
- }
1351
-
1352
- $this->parent->_[HDOM_INFO_END] = $this->cursor;
1353
- if ($this->parent->parent) $this->parent = $this->parent->parent;
1354
-
1355
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1356
- return true;
1357
- }
1358
-
1359
- $node = new simple_html_dom_node($this);
1360
- $node->_[HDOM_INFO_BEGIN] = $this->cursor;
1361
- ++$this->cursor;
1362
- $tag = $this->copy_until($this->token_slash);
1363
- $node->tag_start = $begin_tag_pos;
1364
-
1365
- // doctype, cdata & comments...
1366
- if (isset($tag[0]) && $tag[0]==='!') {
1367
- $node->_[HDOM_INFO_TEXT] = '<' . $tag . $this->copy_until_char('>');
1368
-
1369
- if (isset($tag[2]) && $tag[1]==='-' && $tag[2]==='-') {
1370
- $node->nodetype = HDOM_TYPE_COMMENT;
1371
- $node->tag = 'comment';
1372
- } else {
1373
- $node->nodetype = HDOM_TYPE_UNKNOWN;
1374
- $node->tag = 'unknown';
1375
- }
1376
- if ($this->char==='>') $node->_[HDOM_INFO_TEXT].='>';
1377
- $this->link_nodes($node, true);
1378
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1379
- return true;
1380
- }
1381
-
1382
- // text
1383
- if ($pos=strpos($tag, '<')!==false) {
1384
- $tag = '<' . substr($tag, 0, -1);
1385
- $node->_[HDOM_INFO_TEXT] = $tag;
1386
- $this->link_nodes($node, false);
1387
- $this->char = $this->doc[--$this->pos]; // prev
1388
- return true;
1389
- }
1390
-
1391
- // escaped hyphen here
1392
- if (!preg_match("/^[\w\-:]+$/", $tag)) {
1393
- $node->_[HDOM_INFO_TEXT] = '<' . $tag . $this->copy_until('<>');
1394
- if ($this->char==='<') {
1395
- $this->link_nodes($node, false);
1396
- return true;
1397
- }
1398
-
1399
- if ($this->char==='>') $node->_[HDOM_INFO_TEXT].='>';
1400
- $this->link_nodes($node, false);
1401
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1402
- return true;
1403
- }
1404
-
1405
- // begin tag
1406
- $node->nodetype = HDOM_TYPE_ELEMENT;
1407
- $tag_lower = strtolower($tag);
1408
- $node->tag = ($this->lowercase) ? $tag_lower : $tag;
1409
-
1410
- // handle optional closing tags
1411
- if (isset($this->optional_closing_tags[$tag_lower]) )
1412
- {
1413
- while (isset($this->optional_closing_tags[$tag_lower][strtolower($this->parent->tag)]))
1414
- {
1415
- $this->parent->_[HDOM_INFO_END] = 0;
1416
- $this->parent = $this->parent->parent;
1417
- }
1418
- $node->parent = $this->parent;
1419
- }
1420
-
1421
- $guard = 0; // prevent infinity loop
1422
- $space = array($this->copy_skip($this->token_blank), '', '');
1423
-
1424
- // attributes
1425
- do
1426
- {
1427
- if ($this->char!==null && $space[0]==='')
1428
- {
1429
- break;
1430
- }
1431
- $name = $this->copy_until($this->token_equal);
1432
- if ($guard===$this->pos)
1433
- {
1434
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1435
- continue;
1436
- }
1437
- $guard = $this->pos;
1438
-
1439
- // handle endless '<'
1440
- if ($this->pos>=$this->size-1 && $this->char!=='>') {
1441
- $node->nodetype = HDOM_TYPE_TEXT;
1442
- $node->_[HDOM_INFO_END] = 0;
1443
- $node->_[HDOM_INFO_TEXT] = '<'.$tag . $space[0] . $name;
1444
- $node->tag = 'text';
1445
- $this->link_nodes($node, false);
1446
- return true;
1447
- }
1448
-
1449
- // handle mismatch '<'
1450
- if ($this->doc[$this->pos-1]=='<') {
1451
- $node->nodetype = HDOM_TYPE_TEXT;
1452
- $node->tag = 'text';
1453
- $node->attr = array();
1454
- $node->_[HDOM_INFO_END] = 0;
1455
- $node->_[HDOM_INFO_TEXT] = substr($this->doc, $begin_tag_pos, $this->pos-$begin_tag_pos-1);
1456
- $this->pos -= 2;
1457
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1458
- $this->link_nodes($node, false);
1459
- return true;
1460
- }
1461
-
1462
- if ($name!=='/' && $name!=='') {
1463
- $space[1] = $this->copy_skip($this->token_blank);
1464
- $name = $this->restore_noise($name);
1465
- if ($this->lowercase) $name = strtolower($name);
1466
- if ($this->char==='=') {
1467
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1468
- $this->parse_attr($node, $name, $space);
1469
- }
1470
- else {
1471
- //no value attr: nowrap, checked selected...
1472
- $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_NO;
1473
- $node->attr[$name] = true;
1474
- if ($this->char!='>') $this->char = $this->doc[--$this->pos]; // prev
1475
- }
1476
- $node->_[HDOM_INFO_SPACE][] = $space;
1477
- $space = array($this->copy_skip($this->token_blank), '', '');
1478
- }
1479
- else
1480
- break;
1481
- } while ($this->char!=='>' && $this->char!=='/');
1482
-
1483
- $this->link_nodes($node, true);
1484
- $node->_[HDOM_INFO_ENDSPACE] = $space[0];
1485
-
1486
- // check self closing
1487
- if ($this->copy_until_char_escape('>')==='/')
1488
- {
1489
- $node->_[HDOM_INFO_ENDSPACE] .= '/';
1490
- $node->_[HDOM_INFO_END] = 0;
1491
- }
1492
- else
1493
- {
1494
- // reset parent
1495
- if (!isset($this->self_closing_tags[strtolower($node->tag)])) $this->parent = $node;
1496
- }
1497
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1498
-
1499
- // If it's a BR tag, we need to set it's text to the default text.
1500
- // This way when we see it in plaintext, we can generate formatting that the user wants.
1501
- // since a br tag never has sub nodes, this works well.
1502
- if ($node->tag == "br")
1503
- {
1504
- $node->_[HDOM_INFO_INNER] = $this->default_br_text;
1505
- }
1506
-
1507
- return true;
1508
- }
1509
-
1510
- // parse attributes
1511
- protected function parse_attr($node, $name, &$space)
1512
- {
1513
- // Per sourceforge: http://sourceforge.net/tracker/?func=detail&aid=3061408&group_id=218559&atid=1044037
1514
- // If the attribute is already defined inside a tag, only pay atetntion to the first one as opposed to the last one.
1515
- if (isset($node->attr[$name]))
1516
- {
1517
- return;
1518
- }
1519
-
1520
- $space[2] = $this->copy_skip($this->token_blank);
1521
- switch ($this->char) {
1522
- case '"':
1523
- $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_DOUBLE;
1524
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1525
- $node->attr[$name] = $this->restore_noise($this->copy_until_char_escape('"'));
1526
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1527
- break;
1528
- case '\'':
1529
- $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_SINGLE;
1530
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1531
- $node->attr[$name] = $this->restore_noise($this->copy_until_char_escape('\''));
1532
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1533
- break;
1534
- default:
1535
- $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_NO;
1536
- $node->attr[$name] = $this->restore_noise($this->copy_until($this->token_attr));
1537
- }
1538
- // PaperG: Attributes should not have \r or \n in them, that counts as html whitespace.
1539
- $node->attr[$name] = str_replace("\r", "", $node->attr[$name]);
1540
- $node->attr[$name] = str_replace("\n", "", $node->attr[$name]);
1541
- // PaperG: If this is a "class" selector, lets get rid of the preceeding and trailing space since some people leave it in the multi class case.
1542
- if ($name == "class") {
1543
- $node->attr[$name] = trim($node->attr[$name]);
1544
- }
1545
- }
1546
-
1547
- // link node's parent
1548
- protected function link_nodes(&$node, $is_child)
1549
- {
1550
- $node->parent = $this->parent;
1551
- $this->parent->nodes[] = $node;
1552
- if ($is_child)
1553
- {
1554
- $this->parent->children[] = $node;
1555
- }
1556
- }
1557
-
1558
- // as a text node
1559
- protected function as_text_node($tag)
1560
- {
1561
- $node = new simple_html_dom_node($this);
1562
- ++$this->cursor;
1563
- $node->_[HDOM_INFO_TEXT] = '</' . $tag . '>';
1564
- $this->link_nodes($node, false);
1565
- $this->char = (++$this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1566
- return true;
1567
- }
1568
-
1569
- protected function skip($chars)
1570
- {
1571
- $this->pos += strspn($this->doc, $chars, $this->pos);
1572
- $this->char = ($this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1573
- }
1574
-
1575
- protected function copy_skip($chars)
1576
- {
1577
- $pos = $this->pos;
1578
- $len = strspn($this->doc, $chars, $pos);
1579
- $this->pos += $len;
1580
- $this->char = ($this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1581
- if ($len===0) return '';
1582
- return substr($this->doc, $pos, $len);
1583
- }
1584
-
1585
- protected function copy_until($chars)
1586
- {
1587
- $pos = $this->pos;
1588
- $len = strcspn($this->doc, $chars, $pos);
1589
- $this->pos += $len;
1590
- $this->char = ($this->pos<$this->size) ? $this->doc[$this->pos] : null; // next
1591
- return substr($this->doc, $pos, $len);
1592
- }
1593
-
1594
- protected function copy_until_char($char)
1595
- {
1596
- if ($this->char===null) return '';
1597
-
1598
- if (($pos = strpos($this->doc, $char, $this->pos))===false) {
1599
- $ret = substr($this->doc, $this->pos, $this->size-$this->pos);
1600
- $this->char = null;
1601
- $this->pos = $this->size;
1602
- return $ret;
1603
- }
1604
-
1605
- if ($pos===$this->pos) return '';
1606
- $pos_old = $this->pos;
1607
- $this->char = $this->doc[$pos];
1608
- $this->pos = $pos;
1609
- return substr($this->doc, $pos_old, $pos-$pos_old);
1610
- }
1611
-
1612
- protected function copy_until_char_escape($char)
1613
- {
1614
- if ($this->char===null) return '';
1615
-
1616
- $start = $this->pos;
1617
- while (1)
1618
- {
1619
- if (($pos = strpos($this->doc, $char, $start))===false)
1620
- {
1621
- $ret = substr($this->doc, $this->pos, $this->size-$this->pos);
1622
- $this->char = null;
1623
- $this->pos = $this->size;
1624
- return $ret;
1625
- }
1626
-
1627
- if ($pos===$this->pos) return '';
1628
-
1629
- if ($this->doc[$pos-1]==='\\') {
1630
- $start = $pos+1;
1631
- continue;
1632
- }
1633
-
1634
- $pos_old = $this->pos;
1635
- $this->char = $this->doc[$pos];
1636
- $this->pos = $pos;
1637
- return substr($this->doc, $pos_old, $pos-$pos_old);
1638
- }
1639
- }
1640
-
1641
- // remove noise from html content
1642
- // save the noise in the $this->noise array.
1643
- protected function remove_noise($pattern, $remove_tag=false)
1644
- {
1645
- global $debug_object;
1646
- if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
1647
-
1648
- $count = preg_match_all($pattern, $this->doc, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE);
1649
-
1650
- for ($i=$count-1; $i>-1; --$i)
1651
- {
1652
- $key = '___noise___'.sprintf('% 5d', count($this->noise)+1000);
1653
- if (is_object($debug_object)) { $debug_object->debug_log(2, 'key is: ' . $key); }
1654
- $idx = ($remove_tag) ? 0 : 1;
1655
- $this->noise[$key] = $matches[$i][$idx][0];
1656
- $this->doc = substr_replace($this->doc, $key, $matches[$i][$idx][1], strlen($matches[$i][$idx][0]));
1657
- }
1658
-
1659
- // reset the length of content
1660
- $this->size = strlen($this->doc);
1661
- if ($this->size>0)
1662
- {
1663
- $this->char = $this->doc[0];
1664
- }
1665
- }
1666
-
1667
- // restore noise to html content
1668
- function restore_noise($text)
1669
- {
1670
- global $debug_object;
1671
- if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
1672
-
1673
- while (($pos=strpos($text, '___noise___'))!==false)
1674
- {
1675
- // Sometimes there is a broken piece of markup, and we don't GET the pos+11 etc... token which indicates a problem outside of us...
1676
- if (strlen($text) > $pos+15)
1677
- {
1678
- $key = '___noise___'.$text[$pos+11].$text[$pos+12].$text[$pos+13].$text[$pos+14].$text[$pos+15];
1679
- if (is_object($debug_object)) { $debug_object->debug_log(2, 'located key of: ' . $key); }
1680
-
1681
- if (isset($this->noise[$key]))
1682
- {
1683
- $text = substr($text, 0, $pos).$this->noise[$key].substr($text, $pos+16);
1684
- }
1685
- else
1686
- {
1687
- // do this to prevent an infinite loop.
1688
- $text = substr($text, 0, $pos).'UNDEFINED NOISE FOR KEY: '.$key . substr($text, $pos+16);
1689
- }
1690
- }
1691
- else
1692
- {
1693
- // There is no valid key being given back to us... We must get rid of the ___noise___ or we will have a problem.
1694
- $text = substr($text, 0, $pos).'NO NUMERIC NOISE KEY' . substr($text, $pos+11);
1695
- }
1696
- }
1697
- return $text;
1698
- }
1699
-
1700
- // Sometimes we NEED one of the noise elements.
1701
- function search_noise($text)
1702
- {
1703
- global $debug_object;
1704
- if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
1705
-
1706
- foreach($this->noise as $noiseElement)
1707
- {
1708
- if (strpos($noiseElement, $text)!==false)
1709
- {
1710
- return $noiseElement;
1711
- }
1712
- }
1713
- }
1714
- function __toString()
1715
- {
1716
- return $this->root->innertext();
1717
- }
1718
-
1719
- function __get($name)
1720
- {
1721
- switch ($name)
1722
- {
1723
- case 'outertext':
1724
- return $this->root->innertext();
1725
- case 'innertext':
1726
- return $this->root->innertext();
1727
- case 'plaintext':
1728
- return $this->root->text();
1729
- case 'charset':
1730
- return $this->_charset;
1731
- case 'target_charset':
1732
- return $this->_target_charset;
1733
- }
1734
- }
1735
-
1736
- // camel naming conventions
1737
- function childNodes($idx=-1) {return $this->root->childNodes($idx);}
1738
- function firstChild() {return $this->root->first_child();}
1739
- function lastChild() {return $this->root->last_child();}
1740
- function createElement($name, $value=null) {return @str_get_html("<$name>$value</$name>")->first_child();}
1741
- function createTextNode($value) {return @end(str_get_html($value)->nodes);}
1742
- function getElementById($id) {return $this->find("#$id", 0);}
1743
- function getElementsById($id, $idx=null) {return $this->find("#$id", $idx);}
1744
- function getElementByTagName($name) {return $this->find($name, 0);}
1745
- function getElementsByTagName($name, $idx=-1) {return $this->find($name, $idx);}
1746
- function loadFile() {$args = func_get_args();$this->load_file($args);}
1747
- }
1748
-
1749
- ?>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /** https://simplehtmldom.sourceforge.io/docs/1.9/index.html
3
+
4
+ * Website: http://sourceforge.net/projects/simplehtmldom/
5
+ * Additional projects: http://sourceforge.net/projects/debugobject/
6
+ * Acknowledge: Jose Solorzano (https://sourceforge.net/projects/php-html/)
7
+ *
8
+ * Licensed under The MIT License
9
+ * See the LICENSE file in the project root for more information.
10
+ *
11
+ * Authors:
12
+ * S.C. Chen
13
+ * John Schlick
14
+ * Rus Carroll
15
+ * logmanoriginal
16
+ *
17
+ * Contributors:
18
+ * Yousuke Kumakura
19
+ * Vadim Voituk
20
+ * Antcs
21
+ *
22
+ * Version Rev. 1.9.1 (291)
23
+ */
24
+
25
+ define('HDOM_TYPE_ELEMENT', 1);
26
+ define('HDOM_TYPE_COMMENT', 2);
27
+ define('HDOM_TYPE_TEXT', 3);
28
+ define('HDOM_TYPE_ENDTAG', 4);
29
+ define('HDOM_TYPE_ROOT', 5);
30
+ define('HDOM_TYPE_UNKNOWN', 6);
31
+ define('HDOM_QUOTE_DOUBLE', 0);
32
+ define('HDOM_QUOTE_SINGLE', 1);
33
+ define('HDOM_QUOTE_NO', 3);
34
+ define('HDOM_INFO_BEGIN', 0);
35
+ define('HDOM_INFO_END', 1);
36
+ define('HDOM_INFO_QUOTE', 2);
37
+ define('HDOM_INFO_SPACE', 3);
38
+ define('HDOM_INFO_TEXT', 4);
39
+ define('HDOM_INFO_INNER', 5);
40
+ define('HDOM_INFO_OUTER', 6);
41
+ define('HDOM_INFO_ENDSPACE', 7);
42
+
43
+ defined('DEFAULT_TARGET_CHARSET') || define('DEFAULT_TARGET_CHARSET', 'UTF-8');
44
+ defined('DEFAULT_BR_TEXT') || define('DEFAULT_BR_TEXT', "\r\n");
45
+ defined('DEFAULT_SPAN_TEXT') || define('DEFAULT_SPAN_TEXT', ' ');
46
+ defined('MAX_FILE_SIZE') || define('MAX_FILE_SIZE', 600000);
47
+ define('HDOM_SMARTY_AS_TEXT', 1);
48
+
49
+ function file_get_html(
50
+ $url,
51
+ $use_include_path = false,
52
+ $context = null,
53
+ $offset = 0,
54
+ $maxLen = -1,
55
+ $lowercase = true,
56
+ $forceTagsClosed = true,
57
+ $target_charset = DEFAULT_TARGET_CHARSET,
58
+ $stripRN = true,
59
+ $defaultBRText = DEFAULT_BR_TEXT,
60
+ $defaultSpanText = DEFAULT_SPAN_TEXT)
61
+ {
62
+ if($maxLen <= 0) { $maxLen = MAX_FILE_SIZE; }
63
+
64
+ $dom = new simple_html_dom(
65
+ null,
66
+ $lowercase,
67
+ $forceTagsClosed,
68
+ $target_charset,
69
+ $stripRN,
70
+ $defaultBRText,
71
+ $defaultSpanText
72
+ );
73
+
74
+ /**
75
+ * For sourceforge users: uncomment the next line and comment the
76
+ * retrieve_url_contents line 2 lines down if it is not already done.
77
+ */
78
+ $contents = file_get_contents(
79
+ $url,
80
+ $use_include_path,
81
+ $context,
82
+ $offset,
83
+ $maxLen
84
+ );
85
+ // $contents = retrieve_url_contents($url);
86
+
87
+ if (empty($contents) || strlen($contents) > $maxLen) {
88
+ $dom->clear();
89
+ return false;
90
+ }
91
+
92
+ return $dom->load($contents, $lowercase, $stripRN);
93
+ }
94
+
95
+ function str_get_html(
96
+ $str,
97
+ $lowercase = true,
98
+ $forceTagsClosed = true,
99
+ $target_charset = DEFAULT_TARGET_CHARSET,
100
+ $stripRN = true,
101
+ $defaultBRText = DEFAULT_BR_TEXT,
102
+ $defaultSpanText = DEFAULT_SPAN_TEXT)
103
+ {
104
+ $dom = new simple_html_dom(
105
+ null,
106
+ $lowercase,
107
+ $forceTagsClosed,
108
+ $target_charset,
109
+ $stripRN,
110
+ $defaultBRText,
111
+ $defaultSpanText
112
+ );
113
+
114
+ if (empty($str) || strlen($str) > MAX_FILE_SIZE) {
115
+ $dom->clear();
116
+ return false;
117
+ }
118
+
119
+ return $dom->load($str, $lowercase, $stripRN);
120
+ }
121
+
122
+ function dump_html_tree($node, $show_attr = true, $deep = 0)
123
+ {
124
+ $node->dump($node);
125
+ }
126
+
127
+ class simple_html_dom_node
128
+ {
129
+ public $nodetype = HDOM_TYPE_TEXT;
130
+ public $tag = 'text';
131
+ public $attr = array();
132
+ public $children = array();
133
+ public $nodes = array();
134
+ public $parent = null;
135
+ public $_ = array();
136
+ public $tag_start = 0;
137
+ private $dom = null;
138
+
139
+ function __construct($dom)
140
+ {
141
+ $this->dom = $dom;
142
+ $dom->nodes[] = $this;
143
+ }
144
+
145
+ function __destruct()
146
+ {
147
+ $this->clear();
148
+ }
149
+
150
+ function __toString()
151
+ {
152
+ return $this->outertext();
153
+ }
154
+
155
+ function clear()
156
+ {
157
+ $this->dom = null;
158
+ $this->nodes = null;
159
+ $this->parent = null;
160
+ $this->children = null;
161
+ }
162
+
163
+ function dump($show_attr = true, $depth = 0)
164
+ {
165
+ echo str_repeat("\t", $depth) . $this->tag;
166
+
167
+ if ($show_attr && count($this->attr) > 0) {
168
+ echo '(';
169
+ foreach ($this->attr as $k => $v) {
170
+ echo "[$k]=>\"$v\", ";
171
+ }
172
+ echo ')';
173
+ }
174
+
175
+ echo "\n";
176
+
177
+ if ($this->nodes) {
178
+ foreach ($this->nodes as $node) {
179
+ $node->dump($show_attr, $depth + 1);
180
+ }
181
+ }
182
+ }
183
+
184
+ function dump_node($echo = true)
185
+ {
186
+ $string = $this->tag;
187
+
188
+ if (count($this->attr) > 0) {
189
+ $string .= '(';
190
+ foreach ($this->attr as $k => $v) {
191
+ $string .= "[$k]=>\"$v\", ";
192
+ }
193
+ $string .= ')';
194
+ }
195
+
196
+ if (count($this->_) > 0) {
197
+ $string .= ' $_ (';
198
+ foreach ($this->_ as $k => $v) {
199
+ if (is_array($v)) {
200
+ $string .= "[$k]=>(";
201
+ foreach ($v as $k2 => $v2) {
202
+ $string .= "[$k2]=>\"$v2\", ";
203
+ }
204
+ $string .= ')';
205
+ } else {
206
+ $string .= "[$k]=>\"$v\", ";
207
+ }
208
+ }
209
+ $string .= ')';
210
+ }
211
+
212
+ if (isset($this->text)) {
213
+ $string .= " text: ({$this->text})";
214
+ }
215
+
216
+ $string .= ' HDOM_INNER_INFO: ';
217
+
218
+ if (isset($node->_[HDOM_INFO_INNER])) {
219
+ $string .= "'" . $node->_[HDOM_INFO_INNER] . "'";
220
+ } else {
221
+ $string .= ' NULL ';
222
+ }
223
+
224
+ $string .= ' children: ' . count($this->children);
225
+ $string .= ' nodes: ' . count($this->nodes);
226
+ $string .= ' tag_start: ' . $this->tag_start;
227
+ $string .= "\n";
228
+
229
+ if ($echo) {
230
+ echo $string;
231
+ return;
232
+ } else {
233
+ return $string;
234
+ }
235
+ }
236
+
237
+ function parent($parent = null)
238
+ {
239
+ // I am SURE that this doesn't work properly.
240
+ // It fails to unset the current node from it's current parents nodes or
241
+ // children list first.
242
+ if ($parent !== null) {
243
+ $this->parent = $parent;
244
+ $this->parent->nodes[] = $this;
245
+ $this->parent->children[] = $this;
246
+ }
247
+
248
+ return $this->parent;
249
+ }
250
+
251
+ function has_child()
252
+ {
253
+ return !empty($this->children);
254
+ }
255
+
256
+ function children($idx = -1)
257
+ {
258
+ if ($idx === -1) {
259
+ return $this->children;
260
+ }
261
+
262
+ if (isset($this->children[$idx])) {
263
+ return $this->children[$idx];
264
+ }
265
+
266
+ return null;
267
+ }
268
+
269
+ function first_child()
270
+ {
271
+ if (count($this->children) > 0) {
272
+ return $this->children[0];
273
+ }
274
+ return null;
275
+ }
276
+
277
+ function last_child()
278
+ {
279
+ if (count($this->children) > 0) {
280
+ return end($this->children);
281
+ }
282
+ return null;
283
+ }
284
+
285
+ function next_sibling()
286
+ {
287
+ if ($this->parent === null) {
288
+ return null;
289
+ }
290
+
291
+ $idx = array_search($this, $this->parent->children, true);
292
+
293
+ if ($idx !== false && isset($this->parent->children[$idx + 1])) {
294
+ return $this->parent->children[$idx + 1];
295
+ }
296
+
297
+ return null;
298
+ }
299
+
300
+ function prev_sibling()
301
+ {
302
+ if ($this->parent === null) {
303
+ return null;
304
+ }
305
+
306
+ $idx = array_search($this, $this->parent->children, true);
307
+
308
+ if ($idx !== false && $idx > 0) {
309
+ return $this->parent->children[$idx - 1];
310
+ }
311
+
312
+ return null;
313
+ }
314
+
315
+ function find_ancestor_tag($tag)
316
+ {
317
+ global $debug_object;
318
+ if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
319
+
320
+ if ($this->parent === null) {
321
+ return null;
322
+ }
323
+
324
+ $ancestor = $this->parent;
325
+
326
+ while (!is_null($ancestor)) {
327
+ if (is_object($debug_object)) {
328
+ $debug_object->debug_log(2, 'Current tag is: ' . $ancestor->tag);
329
+ }
330
+
331
+ if ($ancestor->tag === $tag) {
332
+ break;
333
+ }
334
+
335
+ $ancestor = $ancestor->parent;
336
+ }
337
+
338
+ return $ancestor;
339
+ }
340
+
341
+ function innertext()
342
+ {
343
+ if (isset($this->_[HDOM_INFO_INNER])) {
344
+ return $this->_[HDOM_INFO_INNER];
345
+ }
346
+
347
+ if (isset($this->_[HDOM_INFO_TEXT])) {
348
+ return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
349
+ }
350
+
351
+ $ret = '';
352
+
353
+ foreach ($this->nodes as $n) {
354
+ $ret .= $n->outertext();
355
+ }
356
+
357
+ return $ret;
358
+ }
359
+
360
+ function outertext()
361
+ {
362
+ global $debug_object;
363
+
364
+ if (is_object($debug_object)) {
365
+ $text = '';
366
+
367
+ if ($this->tag === 'text') {
368
+ if (!empty($this->text)) {
369
+ $text = ' with text: ' . $this->text;
370
+ }
371
+ }
372
+
373
+ $debug_object->debug_log(1, 'Innertext of tag: ' . $this->tag . $text);
374
+ }
375
+
376
+ if ($this->tag === 'root') {
377
+ return $this->innertext();
378
+ }
379
+
380
+ // todo: What is the use of this callback? Remove?
381
+ if ($this->dom && $this->dom->callback !== null) {
382
+ call_user_func_array($this->dom->callback, array($this));
383
+ }
384
+
385
+ if (isset($this->_[HDOM_INFO_OUTER])) {
386
+ return $this->_[HDOM_INFO_OUTER];
387
+ }
388
+
389
+ if (isset($this->_[HDOM_INFO_TEXT])) {
390
+ return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
391
+ }
392
+
393
+ $ret = '';
394
+
395
+ if ($this->dom && $this->dom->nodes[$this->_[HDOM_INFO_BEGIN]]) {
396
+ $ret = $this->dom->nodes[$this->_[HDOM_INFO_BEGIN]]->makeup();
397
+ }
398
+
399
+ if (isset($this->_[HDOM_INFO_INNER])) {
400
+ // todo: <br> should either never have HDOM_INFO_INNER or always
401
+ if ($this->tag !== 'br') {
402
+ $ret .= $this->_[HDOM_INFO_INNER];
403
+ }
404
+ } elseif ($this->nodes) {
405
+ foreach ($this->nodes as $n) {
406
+ $ret .= $this->convert_text($n->outertext());
407
+ }
408
+ }
409
+
410
+ if (isset($this->_[HDOM_INFO_END]) && $this->_[HDOM_INFO_END] != 0) {
411
+ $ret .= '</' . $this->tag . '>';
412
+ }
413
+
414
+ return $ret;
415
+ }
416
+
417
+ function text()
418
+ {
419
+ if (isset($this->_[HDOM_INFO_INNER])) {
420
+ return $this->_[HDOM_INFO_INNER];
421
+ }
422
+
423
+ switch ($this->nodetype) {
424
+ case HDOM_TYPE_TEXT: return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
425
+ case HDOM_TYPE_COMMENT: return '';
426
+ case HDOM_TYPE_UNKNOWN: return '';
427
+ }
428
+
429
+ if (strcasecmp($this->tag, 'script') === 0) { return ''; }
430
+ if (strcasecmp($this->tag, 'style') === 0) { return ''; }
431
+
432
+ $ret = '';
433
+
434
+ // In rare cases, (always node type 1 or HDOM_TYPE_ELEMENT - observed
435
+ // for some span tags, and some p tags) $this->nodes is set to NULL.
436
+ // NOTE: This indicates that there is a problem where it's set to NULL
437
+ // without a clear happening.
438
+ // WHY is this happening?
439
+ if (!is_null($this->nodes)) {
440
+ foreach ($this->nodes as $n) {
441
+ // Start paragraph after a blank line
442
+ if ($n->tag === 'p') {
443
+ $ret = trim($ret) . "\n\n";
444
+ }
445
+
446
+ $ret .= $this->convert_text($n->text());
447
+
448
+ // If this node is a span... add a space at the end of it so
449
+ // multiple spans don't run into each other. This is plaintext
450
+ // after all.
451
+ if ($n->tag === 'span') {
452
+ $ret .= $this->dom->default_span_text;
453
+ }
454
+ }
455
+ }
456
+ return $ret;
457
+ }
458
+
459
+ function xmltext()
460
+ {
461
+ $ret = $this->innertext();
462
+ $ret = str_ireplace('<![CDATA[', '', $ret);
463
+ $ret = str_replace(']]>', '', $ret);
464
+ return $ret;
465
+ }
466
+
467
+ function makeup()
468
+ {
469
+ // text, comment, unknown
470
+ if (isset($this->_[HDOM_INFO_TEXT])) {
471
+ return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]);
472
+ }
473
+
474
+ $ret = '<' . $this->tag;
475
+ $i = -1;
476
+
477
+ foreach ($this->attr as $key => $val) {
478
+ ++$i;
479
+
480
+ // skip removed attribute
481
+ if ($val === null || $val === false) { continue; }
482
+
483
+ $ret .= $this->_[HDOM_INFO_SPACE][$i][0];
484
+
485
+ //no value attr: nowrap, checked selected...
486
+ if ($val === true) {
487
+ $ret .= $key;
488
+ } else {
489
+ switch ($this->_[HDOM_INFO_QUOTE][$i])
490
+ {
491
+ case HDOM_QUOTE_DOUBLE: $quote = '"'; break;
492
+ case HDOM_QUOTE_SINGLE: $quote = '\''; break;
493
+ default: $quote = '';
494
+ }
495
+
496
+ $ret .= $key
497
+ . $this->_[HDOM_INFO_SPACE][$i][1]
498
+ . '='
499
+ . $this->_[HDOM_INFO_SPACE][$i][2]
500
+ . $quote
501
+ . $val
502
+ . $quote;
503
+ }
504
+ }
505
+
506
+ $ret = $this->dom->restore_noise($ret);
507
+ return $ret . $this->_[HDOM_INFO_ENDSPACE] . '>';
508
+ }
509
+
510
+ function find($selector, $idx = null, $lowercase = false)
511
+ {
512
+ $selectors = $this->parse_selector($selector);
513
+ if (($count = count($selectors)) === 0) { return array(); }
514
+ $found_keys = array();
515
+
516
+ // find each selector
517
+ for ($c = 0; $c < $count; ++$c) {
518
+ // The change on the below line was documented on the sourceforge
519
+ // code tracker id 2788009
520
+ // used to be: if (($levle=count($selectors[0]))===0) return array();
521
+ if (($levle = count($selectors[$c])) === 0) { return array(); }
522
+ if (!isset($this->_[HDOM_INFO_BEGIN])) { return array(); }
523
+
524
+ $head = array($this->_[HDOM_INFO_BEGIN] => 1);
525
+ $cmd = ' '; // Combinator
526
+
527
+ // handle descendant selectors, no recursive!
528
+ for ($l = 0; $l < $levle; ++$l) {
529
+ $ret = array();
530
+
531
+ foreach ($head as $k => $v) {
532
+ $n = ($k === -1) ? $this->dom->root : $this->dom->nodes[$k];
533
+ //PaperG - Pass this optional parameter on to the seek function.
534
+ $n->seek($selectors[$c][$l], $ret, $cmd, $lowercase);
535
+ }
536
+
537
+ $head = $ret;
538
+ $cmd = $selectors[$c][$l][4]; // Next Combinator
539
+ }
540
+
541
+ foreach ($head as $k => $v) {
542
+ if (!isset($found_keys[$k])) {
543
+ $found_keys[$k] = 1;
544
+ }
545
+ }
546
+ }
547
+
548
+ // sort keys
549
+ ksort($found_keys);
550
+
551
+ $found = array();
552
+ foreach ($found_keys as $k => $v) {
553
+ $found[] = $this->dom->nodes[$k];
554
+ }
555
+
556
+ // return nth-element or array
557
+ if (is_null($idx)) { return $found; }
558
+ elseif ($idx < 0) { $idx = count($found) + $idx; }
559
+ return (isset($found[$idx])) ? $found[$idx] : null;
560
+ }
561
+
562
+ protected function seek($selector, &$ret, $parent_cmd, $lowercase = false)
563
+ {
564
+ global $debug_object;
565
+ if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
566
+
567
+ list($tag, $id, $class, $attributes, $cmb) = $selector;
568
+ $nodes = array();
569
+
570
+ if ($parent_cmd === ' ') { // Descendant Combinator
571
+ // Find parent closing tag if the current element doesn't have a closing
572
+ // tag (i.e. void element)
573
+ $end = (!empty($this->_[HDOM_INFO_END])) ? $this->_[HDOM_INFO_END] : 0;
574
+ if ($end == 0) {
575
+ $parent = $this->parent;
576
+ while (!isset($parent->_[HDOM_INFO_END]) && $parent !== null) {
577
+ $end -= 1;
578
+ $parent = $parent->parent;
579
+ }
580
+ $end += $parent->_[HDOM_INFO_END];
581
+ }
582
+
583
+ // Get list of target nodes
584
+ $nodes_start = $this->_[HDOM_INFO_BEGIN] + 1;
585
+ $nodes_count = $end - $nodes_start;
586
+ $nodes = array_slice($this->dom->nodes, $nodes_start, $nodes_count, true);
587
+ } elseif ($parent_cmd === '>') { // Child Combinator
588
+ $nodes = $this->children;
589
+ } elseif ($parent_cmd === '+'
590
+ && $this->parent
591
+ && in_array($this, $this->parent->children)) { // Next-Sibling Combinator
592
+ $index = array_search($this, $this->parent->children, true) + 1;
593
+ if ($index < count($this->parent->children))
594
+ $nodes[] = $this->parent->children[$index];
595
+ } elseif ($parent_cmd === '~'
596
+ && $this->parent
597
+ && in_array($this, $this->parent->children)) { // Subsequent Sibling Combinator
598
+ $index = array_search($this, $this->parent->children, true);
599
+ $nodes = array_slice($this->parent->children, $index);
600
+ }
601
+
602
+ // Go throgh each element starting at this element until the end tag
603
+ // Note: If this element is a void tag, any previous void element is
604
+ // skipped.
605
+ foreach($nodes as $node) {
606
+ $pass = true;
607
+
608
+ // Skip root nodes
609
+ if(!$node->parent) {
610
+ $pass = false;
611
+ }
612
+
613
+ // Handle 'text' selector
614
+ if($pass && $tag === 'text' && $node->tag === 'text') {
615
+ $ret[array_search($node, $this->dom->nodes, true)] = 1;
616
+ unset($node);
617
+ continue;
618
+ }
619
+
620
+ // Skip if node isn't a child node (i.e. text nodes)
621
+ if($pass && !in_array($node, $node->parent->children, true)) {
622
+ $pass = false;
623
+ }
624
+
625
+ // Skip if tag doesn't match
626
+ if ($pass && $tag !== '' && $tag !== $node->tag && $tag !== '*') {
627
+ $pass = false;
628
+ }
629
+
630
+ // Skip if ID doesn't exist
631
+ if ($pass && $id !== '' && !isset($node->attr['id'])) {
632
+ $pass = false;
633
+ }
634
+
635
+ // Check if ID matches
636
+ if ($pass && $id !== '' && isset($node->attr['id'])) {
637
+ // Note: Only consider the first ID (as browsers do)
638
+ $node_id = explode(' ', trim($node->attr['id']))[0];
639
+
640
+ if($id !== $node_id) { $pass = false; }
641
+ }
642
+
643
+ // Check if all class(es) exist
644
+ if ($pass && $class !== '' && is_array($class) && !empty($class)) {
645
+ if (isset($node->attr['class'])) {
646
+ $node_classes = explode(' ', $node->attr['class']);
647
+
648
+ if ($lowercase) {
649
+ $node_classes = array_map('strtolower', $node_classes);
650
+ }
651
+
652
+ foreach($class as $c) {
653
+ if(!in_array($c, $node_classes)) {
654
+ $pass = false;
655
+ break;
656
+ }
657
+ }
658
+ } else {
659
+ $pass = false;
660
+ }
661
+ }
662
+
663
+ // Check attributes
664
+ if ($pass
665
+ && $attributes !== ''
666
+ && is_array($attributes)
667
+ && !empty($attributes)) {
668
+ foreach($attributes as $a) {
669
+ list (
670
+ $att_name,
671
+ $att_expr,
672
+ $att_val,
673
+ $att_inv,
674
+ $att_case_sensitivity
675
+ ) = $a;
676
+
677
+ // Handle indexing attributes (i.e. "[2]")
678
+ /**
679
+ * Note: This is not supported by the CSS Standard but adds
680
+ * the ability to select items compatible to XPath (i.e.
681
+ * the 3rd element within it's parent).
682
+ *
683
+ * Note: This doesn't conflict with the CSS Standard which
684
+ * doesn't work on numeric attributes anyway.
685
+ */
686
+ if (is_numeric($att_name)
687
+ && $att_expr === ''
688
+ && $att_val === '') {
689
+ $count = 0;
690
+
691
+ // Find index of current element in parent
692
+ foreach ($node->parent->children as $c) {
693
+ if ($c->tag === $node->tag) ++$count;
694
+ if ($c === $node) break;
695
+ }
696
+
697
+ // If this is the correct node, continue with next
698
+ // attribute
699
+ if ($count === (int)$att_name) continue;
700
+ }
701
+
702
+ // Check attribute availability
703
+ if ($att_inv) { // Attribute should NOT be set
704
+ if (isset($node->attr[$att_name])) {
705
+ $pass = false;
706
+ break;
707
+ }
708
+ } else { // Attribute should be set
709
+ // todo: "plaintext" is not a valid CSS selector!
710
+ if ($att_name !== 'plaintext'
711
+ && !isset($node->attr[$att_name])) {
712
+ $pass = false;
713
+ break;
714
+ }
715
+ }
716
+
717
+ // Continue with next attribute if expression isn't defined
718
+ if ($att_expr === '') continue;
719
+
720
+ // If they have told us that this is a "plaintext"
721
+ // search then we want the plaintext of the node - right?
722
+ // todo "plaintext" is not a valid CSS selector!
723
+ if ($att_name === 'plaintext') {
724
+ $nodeKeyValue = $node->text();
725
+ } else {
726
+ $nodeKeyValue = $node->attr[$att_name];
727
+ }
728
+
729
+ if (is_object($debug_object)) {
730
+ $debug_object->debug_log(2,
731
+ 'testing node: '
732
+ . $node->tag
733
+ . ' for attribute: '
734
+ . $att_name
735
+ . $att_expr
736
+ . $att_val
737
+ . ' where nodes value is: '
738
+ . $nodeKeyValue
739
+ );
740
+ }
741
+
742
+ // If lowercase is set, do a case insensitive test of
743
+ // the value of the selector.
744
+ if ($lowercase) {
745
+ $check = $this->match(
746
+ $att_expr,
747
+ strtolower($att_val),
748
+ strtolower($nodeKeyValue),
749
+ $att_case_sensitivity
750
+ );
751
+ } else {
752
+ $check = $this->match(
753
+ $att_expr,
754
+ $att_val,
755
+ $nodeKeyValue,
756
+ $att_case_sensitivity
757
+ );
758
+ }
759
+
760
+ if (is_object($debug_object)) {
761
+ $debug_object->debug_log(2,
762
+ 'after match: '
763
+ . ($check ? 'true' : 'false')
764
+ );
765
+ }
766
+
767
+ if (!$check) {
768
+ $pass = false;
769
+ break;
770
+ }
771
+ }
772
+ }
773
+
774
+ // Found a match. Add to list and clear node
775
+ if ($pass) $ret[$node->_[HDOM_INFO_BEGIN]] = 1;
776
+ unset($node);
777
+ }
778
+ // It's passed by reference so this is actually what this function returns.
779
+ if (is_object($debug_object)) {
780
+ $debug_object->debug_log(1, 'EXIT - ret: ', $ret);
781
+ }
782
+ }
783
+
784
+ protected function match($exp, $pattern, $value, $case_sensitivity)
785
+ {
786
+ global $debug_object;
787
+ if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
788
+
789
+ if ($case_sensitivity === 'i') {
790
+ $pattern = strtolower($pattern);
791
+ $value = strtolower($value);
792
+ }
793
+
794
+ switch ($exp) {
795
+ case '=':
796
+ return ($value === $pattern);
797
+ case '!=':
798
+ return ($value !== $pattern);
799
+ case '^=':
800
+ return preg_match('/^' . preg_quote($pattern, '/') . '/', $value);
801
+ case '$=':
802
+ return preg_match('/' . preg_quote($pattern, '/') . '$/', $value);
803
+ case '*=':
804
+ return preg_match('/' . preg_quote($pattern, '/') . '/', $value);
805
+ case '|=':
806
+ /**
807
+ * [att|=val]
808
+ *
809
+ * Represents an element with the att attribute, its value
810
+ * either being exactly "val" or beginning with "val"
811
+ * immediately followed by "-" (U+002D).
812
+ */
813
+ return strpos($value, $pattern) === 0;
814
+ case '~=':
815
+ /**
816
+ * [att~=val]
817
+ *
818
+ * Represents an element with the att attribute whose value is a
819
+ * whitespace-separated list of words, one of which is exactly
820
+ * "val". If "val" contains whitespace, it will never represent
821
+ * anything (since the words are separated by spaces). Also if
822
+ * "val" is the empty string, it will never represent anything.
823
+ */
824
+ return in_array($pattern, explode(' ', trim($value)), true);
825
+ }
826
+ return false;
827
+ }
828
+
829
+ protected function parse_selector($selector_string)
830
+ {
831
+ global $debug_object;
832
+ if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
833
+
834
+ /**
835
+ * Pattern of CSS selectors, modified from mootools (https://mootools.net/)
836
+ *
837
+ * Paperg: Add the colon to the attribute, so that it properly finds
838
+ * <tag attr:ibute="something" > like google does.
839
+ *
840
+ * Note: if you try to look at this attribute, you MUST use getAttribute
841
+ * since $dom->x:y will fail the php syntax check.
842
+ *
843
+ * Notice the \[ starting the attribute? and the @? following? This
844
+ * implies that an attribute can begin with an @ sign that is not
845
+ * captured. This implies that an html attribute specifier may start
846
+ * with an @ sign that is NOT captured by the expression. Farther study
847
+ * is required to determine of this should be documented or removed.
848
+ *
849
+ * Matches selectors in this order:
850
+ *
851
+ * [0] - full match
852
+ *
853
+ * [1] - tag name
854
+ * ([\w:\*-]*)
855
+ * Matches the tag name consisting of zero or more words, colons,
856
+ * asterisks and hyphens.
857
+ *
858
+ * [2] - id name
859
+ * (?:\#([\w-]+))
860
+ * Optionally matches a id name, consisting of an "#" followed by
861
+ * the id name (one or more words and hyphens).
862
+ *
863
+ * [3] - class names (including dots)
864
+ * (?:\.([\w\.-]+))?
865
+ * Optionally matches a list of classs, consisting of an "."
866
+ * followed by the class name (one or more words and hyphens)
867
+ * where multiple classes can be chained (i.e. ".foo.bar.baz")
868
+ *
869
+ * [4] - attributes
870
+ * ((?:\[@?(?:!?[\w:-]+)(?:(?:[!*^$|~]?=)[\"']?(?:.*?)[\"']?)?(?:\s*?(?:[iIsS])?)?\])+)?
871
+ * Optionally matches the attributes list
872
+ *
873
+ * [5] - separator
874
+ * ([\/, >+~]+)
875
+ * Matches the selector list separator
876
+ */
877
+ // phpcs:ignore Generic.Files.LineLength
878
+ $pattern = "/([\w:\*-]*)(?:\#([\w-]+))?(?:|\.([\w\.-]+))?((?:\[@?(?:!?[\w:-]+)(?:(?:[!*^$|~]?=)[\"']?(?:.*?)[\"']?)?(?:\s*?(?:[iIsS])?)?\])+)?([\/, >+~]+)/is";
879
+
880
+ preg_match_all(
881
+ $pattern,
882
+ trim($selector_string) . ' ', // Add final ' ' as pseudo separator
883
+ $matches,
884
+ PREG_SET_ORDER
885
+ );
886
+
887
+ if (is_object($debug_object)) {
888
+ $debug_object->debug_log(2, 'Matches Array: ', $matches);
889
+ }
890
+
891
+ $selectors = array();
892
+ $result = array();
893
+
894
+ foreach ($matches as $m) {
895
+ $m[0] = trim($m[0]);
896
+
897
+ // Skip NoOps
898
+ if ($m[0] === '' || $m[0] === '/' || $m[0] === '//') { continue; }
899
+
900
+ // Convert to lowercase
901
+ if ($this->dom->lowercase) {
902
+ $m[1] = strtolower($m[1]);
903
+ }
904
+
905
+ // Extract classes
906
+ if ($m[3] !== '') { $m[3] = explode('.', $m[3]); }
907
+
908
+ /* Extract attributes (pattern based on the pattern above!)
909
+
910
+ * [0] - full match
911
+ * [1] - attribute name
912
+ * [2] - attribute expression
913
+ * [3] - attribute value
914
+ * [4] - case sensitivity
915
+ *
916
+ * Note: Attributes can be negated with a "!" prefix to their name
917
+ */
918
+ if($m[4] !== '') {
919
+ preg_match_all(
920
+ "/\[@?(!?[\w:-]+)(?:([!*^$|~]?=)[\"']?(.*?)[\"']?)?(?:\s+?([iIsS])?)?\]/is",
921
+ trim($m[4]),
922
+ $attributes,
923
+ PREG_SET_ORDER
924
+ );
925
+
926
+ // Replace element by array
927
+ $m[4] = array();
928
+
929
+ foreach($attributes as $att) {
930
+ // Skip empty matches
931
+ if(trim($att[0]) === '') { continue; }
932
+
933
+ $inverted = (isset($att[1][0]) && $att[1][0] === '!');
934
+ $m[4][] = array(
935
+ $inverted ? substr($att[1], 1) : $att[1], // Name
936
+ (isset($att[2])) ? $att[2] : '', // Expression
937
+ (isset($att[3])) ? $att[3] : '', // Value
938
+ $inverted, // Inverted Flag
939
+ (isset($att[4])) ? strtolower($att[4]) : '', // Case-Sensitivity
940
+ );
941
+ }
942
+ }
943
+
944
+ // Sanitize Separator
945
+ if ($m[5] !== '' && trim($m[5]) === '') { // Descendant Separator
946
+ $m[5] = ' ';
947
+ } else { // Other Separator
948
+ $m[5] = trim($m[5]);
949
+ }
950
+
951
+ // Clear Separator if it's a Selector List
952
+ if ($is_list = ($m[5] === ',')) { $m[5] = ''; }
953
+
954
+ // Remove full match before adding to results
955
+ array_shift($m);
956
+ $result[] = $m;
957
+
958
+ if ($is_list) { // Selector List
959
+ $selectors[] = $result;
960
+ $result = array();
961
+ }
962
+ }
963
+
964
+ if (count($result) > 0) { $selectors[] = $result; }
965
+ return $selectors;
966
+ }
967
+
968
+ function __get($name)
969
+ {
970
+ if (isset($this->attr[$name])) {
971
+ return $this->convert_text($this->attr[$name]);
972
+ }
973
+ switch ($name) {
974
+ case 'outertext': return $this->outertext();
975
+ case 'innertext': return $this->innertext();
976
+ case 'plaintext': return $this->text();
977
+ case 'xmltext': return $this->xmltext();
978
+ default: return array_key_exists($name, $this->attr);
979
+ }
980
+ }
981
+
982
+ function __set($name, $value)
983
+ {
984
+ global $debug_object;
985
+ if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
986
+
987
+ switch ($name) {
988
+ case 'outertext': return $this->_[HDOM_INFO_OUTER] = $value;
989
+ case 'innertext':
990
+ if (isset($this->_[HDOM_INFO_TEXT])) {
991
+ return $this->_[HDOM_INFO_TEXT] = $value;
992
+ }
993
+ return $this->_[HDOM_INFO_INNER] = $value;
994
+ }
995
+
996
+ if (!isset($this->attr[$name])) {
997
+ $this->_[HDOM_INFO_SPACE][] = array(' ', '', '');
998
+ $this->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_DOUBLE;
999
+ }
1000
+
1001
+ $this->attr[$name] = $value;
1002
+ }
1003
+
1004
+ function __isset($name)
1005
+ {
1006
+ switch ($name) {
1007
+ case 'outertext': return true;
1008
+ case 'innertext': return true;
1009
+ case 'plaintext': return true;
1010
+ }
1011
+ //no value attr: nowrap, checked selected...
1012
+ return (array_key_exists($name, $this->attr)) ? true : isset($this->attr[$name]);
1013
+ }
1014
+
1015
+ function __unset($name)
1016
+ {
1017
+ if (isset($this->attr[$name])) { unset($this->attr[$name]); }
1018
+ }
1019
+
1020
+ function convert_text($text)
1021
+ {
1022
+ global $debug_object;
1023
+ if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
1024
+
1025
+ $converted_text = $text;
1026
+
1027
+ $sourceCharset = '';
1028
+ $targetCharset = '';
1029
+
1030
+ if ($this->dom) {
1031
+ $sourceCharset = strtoupper($this->dom->_charset);
1032
+ $targetCharset = strtoupper($this->dom->_target_charset);
1033
+ }
1034
+
1035
+ if (is_object($debug_object)) {
1036
+ $debug_object->debug_log(3,
1037
+ 'source charset: '
1038
+ . $sourceCharset
1039
+ . ' target charaset: '
1040
+ . $targetCharset
1041
+ );
1042
+ }
1043
+
1044
+ if (!empty($sourceCharset)
1045
+ && !empty($targetCharset)
1046
+ && (strcasecmp($sourceCharset, $targetCharset) != 0)) {
1047
+ // Check if the reported encoding could have been incorrect and the text is actually already UTF-8
1048
+ if ((strcasecmp($targetCharset, 'UTF-8') == 0)
1049
+ && ($this->is_utf8($text))) {
1050
+ $converted_text = $text;
1051
+ } else {
1052
+ $converted_text = iconv($sourceCharset, $targetCharset, $text);
1053
+ }
1054
+ }
1055
+
1056
+ // Lets make sure that we don't have that silly BOM issue with any of the utf-8 text we output.
1057
+ if ($targetCharset === 'UTF-8') {
1058
+ if (substr($converted_text, 0, 3) === "\xef\xbb\xbf") {
1059
+ $converted_text = substr($converted_text, 3);
1060
+ }
1061
+
1062
+ if (substr($converted_text, -3) === "\xef\xbb\xbf") {
1063
+ $converted_text = substr($converted_text, 0, -3);
1064
+ }
1065
+ }
1066
+
1067
+ return $converted_text;
1068
+ }
1069
+
1070
+ static function is_utf8($str)
1071
+ {
1072
+ $c = 0; $b = 0;
1073
+ $bits = 0;
1074
+ $len = strlen($str);
1075
+ for($i = 0; $i < $len; $i++) {
1076
+ $c = ord($str[$i]);
1077
+ if($c > 128) {
1078
+ if(($c >= 254)) { return false; }
1079
+ elseif($c >= 252) { $bits = 6; }
1080
+ elseif($c >= 248) { $bits = 5; }
1081
+ elseif($c >= 240) { $bits = 4; }
1082
+ elseif($c >= 224) { $bits = 3; }
1083
+ elseif($c >= 192) { $bits = 2; }
1084
+ else { return false; }
1085
+ if(($i + $bits) > $len) { return false; }
1086
+ while($bits > 1) {
1087
+ $i++;
1088
+ $b = ord($str[$i]);
1089
+ if($b < 128 || $b > 191) { return false; }
1090
+ $bits--;
1091
+ }
1092
+ }
1093
+ }
1094
+ return true;
1095
+ }
1096
+
1097
+ function get_display_size()
1098
+ {
1099
+ global $debug_object;
1100
+
1101
+ $width = -1;
1102
+ $height = -1;
1103
+
1104
+ if ($this->tag !== 'img') {
1105
+ return false;
1106
+ }
1107
+
1108
+ // See if there is aheight or width attribute in the tag itself.
1109
+ if (isset($this->attr['width'])) {
1110
+ $width = $this->attr['width'];
1111
+ }
1112
+
1113
+ if (isset($this->attr['height'])) {
1114
+ $height = $this->attr['height'];
1115
+ }
1116
+
1117
+ // Now look for an inline style.
1118
+ if (isset($this->attr['style'])) {
1119
+ // Thanks to user gnarf from stackoverflow for this regular expression.
1120
+ $attributes = array();
1121
+
1122
+ preg_match_all(
1123
+ '/([\w-]+)\s*:\s*([^;]+)\s*;?/',
1124
+ $this->attr['style'],
1125
+ $matches,
1126
+ PREG_SET_ORDER
1127
+ );
1128
+
1129
+ foreach ($matches as $match) {
1130
+ $attributes[$match[1]] = $match[2];
1131
+ }
1132
+
1133
+ // If there is a width in the style attributes:
1134
+ if (isset($attributes['width']) && $width == -1) {
1135
+ // check that the last two characters are px (pixels)
1136
+ if (strtolower(substr($attributes['width'], -2)) === 'px') {
1137
+ $proposed_width = substr($attributes['width'], 0, -2);
1138
+ // Now make sure that it's an integer and not something stupid.
1139
+ if (filter_var($proposed_width, FILTER_VALIDATE_INT)) {
1140
+ $width = $proposed_width;
1141
+ }
1142
+ }
1143
+ }
1144
+
1145
+ // If there is a width in the style attributes:
1146
+ if (isset($attributes['height']) && $height == -1) {
1147
+ // check that the last two characters are px (pixels)
1148
+ if (strtolower(substr($attributes['height'], -2)) == 'px') {
1149
+ $proposed_height = substr($attributes['height'], 0, -2);
1150
+ // Now make sure that it's an integer and not something stupid.
1151
+ if (filter_var($proposed_height, FILTER_VALIDATE_INT)) {
1152
+ $height = $proposed_height;
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ }
1158
+
1159
+ // Future enhancement:
1160
+ // Look in the tag to see if there is a class or id specified that has
1161
+ // a height or width attribute to it.
1162
+
1163
+ // Far future enhancement
1164
+ // Look at all the parent tags of this image to see if they specify a
1165
+ // class or id that has an img selector that specifies a height or width
1166
+ // Note that in this case, the class or id will have the img subselector
1167
+ // for it to apply to the image.
1168
+
1169
+ // ridiculously far future development
1170
+ // If the class or id is specified in a SEPARATE css file thats not on
1171
+ // the page, go get it and do what we were just doing for the ones on
1172
+ // the page.
1173
+
1174
+ $result = array(
1175
+ 'height' => $height,
1176
+ 'width' => $width
1177
+ );
1178
+
1179
+ return $result;
1180
+ }
1181
+
1182
+ function save($filepath = '')
1183
+ {
1184
+ $ret = $this->outertext();
1185
+
1186
+ if ($filepath !== '') {
1187
+ file_put_contents($filepath, $ret, LOCK_EX);
1188
+ }
1189
+
1190
+ return $ret;
1191
+ }
1192
+
1193
+ function addClass($class)
1194
+ {
1195
+ if (is_string($class)) {
1196
+ $class = explode(' ', $class);
1197
+ }
1198
+
1199
+ if (is_array($class)) {
1200
+ foreach($class as $c) {
1201
+ if (isset($this->class)) {
1202
+ if ($this->hasClass($c)) {
1203
+ continue;
1204
+ } else {
1205
+ $this->class .= ' ' . $c;
1206
+ }
1207
+ } else {
1208
+ $this->class = $c;
1209
+ }
1210
+ }
1211
+ } else {
1212
+ if (is_object($debug_object)) {
1213
+ $debug_object->debug_log(2, 'Invalid type: ', gettype($class));
1214
+ }
1215
+ }
1216
+ }
1217
+
1218
+ function hasClass($class)
1219
+ {
1220
+ if (is_string($class)) {
1221
+ if (isset($this->class)) {
1222
+ return in_array($class, explode(' ', $this->class), true);
1223
+ }
1224
+ } else {
1225
+ if (is_object($debug_object)) {
1226
+ $debug_object->debug_log(2, 'Invalid type: ', gettype($class));
1227
+ }
1228
+ }
1229
+
1230
+ return false;
1231
+ }
1232
+
1233
+ function removeClass($class = null)
1234
+ {
1235
+ if (!isset($this->class)) {
1236
+ return;
1237
+ }
1238
+
1239
+ if (is_null($class)) {
1240
+ $this->removeAttribute('class');
1241
+ return;
1242
+ }
1243
+
1244
+ if (is_string($class)) {
1245
+ $class = explode(' ', $class);
1246
+ }
1247
+
1248
+ if (is_array($class)) {
1249
+ $class = array_diff(explode(' ', $this->class), $class);
1250
+ if (empty($class)) {
1251
+ $this->removeAttribute('class');
1252
+ } else {
1253
+ $this->class = implode(' ', $class);
1254
+ }
1255
+ }
1256
+ }
1257
+
1258
+ function getAllAttributes()
1259
+ {
1260
+ return $this->attr;
1261
+ }
1262
+
1263
+ function getAttribute($name)
1264
+ {
1265
+ return $this->__get($name);
1266
+ }
1267
+
1268
+ function setAttribute($name, $value)
1269
+ {
1270
+ $this->__set($name, $value);
1271
+ }
1272
+
1273
+ function hasAttribute($name)
1274
+ {
1275
+ return $this->__isset($name);
1276
+ }
1277
+
1278
+ function removeAttribute($name)
1279
+ {
1280
+ $this->__set($name, null);
1281
+ }
1282
+
1283
+ function remove()
1284
+ {
1285
+ if ($this->parent) {
1286
+ $this->parent->removeChild($this);
1287
+ }
1288
+ }
1289
+
1290
+ function removeChild($node)
1291
+ {
1292
+ $nidx = array_search($node, $this->nodes, true);
1293
+ $cidx = array_search($node, $this->children, true);
1294
+ $didx = array_search($node, $this->dom->nodes, true);
1295
+
1296
+ if ($nidx !== false && $cidx !== false && $didx !== false) {
1297
+
1298
+ foreach($node->children as $child) {
1299
+ $node->removeChild($child);
1300
+ }
1301
+
1302
+ foreach($node->nodes as $entity) {
1303
+ $enidx = array_search($entity, $node->nodes, true);
1304
+ $edidx = array_search($entity, $node->dom->nodes, true);
1305
+
1306
+ if ($enidx !== false && $edidx !== false) {
1307
+ unset($node->nodes[$enidx]);
1308
+ unset($node->dom->nodes[$edidx]);
1309
+ }
1310
+ }
1311
+
1312
+ unset($this->nodes[$nidx]);
1313
+ unset($this->children[$cidx]);
1314
+ unset($this->dom->nodes[$didx]);
1315
+
1316
+ $node->clear();
1317
+
1318
+ }
1319
+ }
1320
+
1321
+ function getElementById($id)
1322
+ {
1323
+ return $this->find("#$id", 0);
1324
+ }
1325
+
1326
+ function getElementsById($id, $idx = null)
1327
+ {
1328
+ return $this->find("#$id", $idx);
1329
+ }
1330
+
1331
+ function getElementByTagName($name)
1332
+ {
1333
+ return $this->find($name, 0);
1334
+ }
1335
+
1336
+ function getElementsByTagName($name, $idx = null)
1337
+ {
1338
+ return $this->find($name, $idx);
1339
+ }
1340
+
1341
+ function parentNode()
1342
+ {
1343
+ return $this->parent();
1344
+ }
1345
+
1346
+ function childNodes($idx = -1)
1347
+ {
1348
+ return $this->children($idx);
1349
+ }
1350
+
1351
+ function firstChild()
1352
+ {
1353
+ return $this->first_child();
1354
+ }
1355
+
1356
+ function lastChild()
1357
+ {
1358
+ return $this->last_child();
1359
+ }
1360
+
1361
+ function nextSibling()
1362
+ {
1363
+ return $this->next_sibling();
1364
+ }
1365
+
1366
+ function previousSibling()
1367
+ {
1368
+ return $this->prev_sibling();
1369
+ }
1370
+
1371
+ function hasChildNodes()
1372
+ {
1373
+ return $this->has_child();
1374
+ }
1375
+
1376
+ function nodeName()
1377
+ {
1378
+ return $this->tag;
1379
+ }
1380
+
1381
+ function appendChild($node)
1382
+ {
1383
+ $node->parent($this);
1384
+ return $node;
1385
+ }
1386
+
1387
+ }
1388
+
1389
+ class simple_html_dom
1390
+ {
1391
+ public $root = null;
1392
+ public $nodes = array();
1393
+ public $callback = null;
1394
+ public $lowercase = false;
1395
+ public $original_size;
1396
+ public $size;
1397
+
1398
+ protected $pos;
1399
+ protected $doc;
1400
+ protected $char;
1401
+
1402
+ protected $cursor;
1403
+ protected $parent;
1404
+ protected $noise = array();
1405
+ protected $token_blank = " \t\r\n";
1406
+ protected $token_equal = ' =/>';
1407
+ protected $token_slash = " />\r\n\t";
1408
+ protected $token_attr = ' >';
1409
+
1410
+ public $_charset = '';
1411
+ public $_target_charset = '';
1412
+
1413
+ protected $default_br_text = '';
1414
+
1415
+ public $default_span_text = '';
1416
+
1417
+ protected $self_closing_tags = array(
1418
+ 'area' => 1,
1419
+ 'base' => 1,
1420
+ 'br' => 1,
1421
+ 'col' => 1,
1422
+ 'embed' => 1,
1423
+ 'hr' => 1,
1424
+ 'img' => 1,
1425
+ 'input' => 1,
1426
+ 'link' => 1,
1427
+ 'meta' => 1,
1428
+ 'param' => 1,
1429
+ 'source' => 1,
1430
+ 'track' => 1,
1431
+ 'wbr' => 1
1432
+ );
1433
+ protected $block_tags = array(
1434
+ 'body' => 1,
1435
+ 'div' => 1,
1436
+ 'form' => 1,
1437
+ 'root' => 1,
1438
+ 'span' => 1,
1439
+ 'table' => 1
1440
+ );
1441
+ protected $optional_closing_tags = array(
1442
+ // Not optional, see
1443
+ // https://www.w3.org/TR/html/textlevel-semantics.html#the-b-element
1444
+ 'b' => array('b' => 1),
1445
+ 'dd' => array('dd' => 1, 'dt' => 1),
1446
+ // Not optional, see
1447
+ // https://www.w3.org/TR/html/grouping-content.html#the-dl-element
1448
+ 'dl' => array('dd' => 1, 'dt' => 1),
1449
+ 'dt' => array('dd' => 1, 'dt' => 1),
1450
+ 'li' => array('li' => 1),
1451
+ 'optgroup' => array('optgroup' => 1, 'option' => 1),
1452
+ 'option' => array('optgroup' => 1, 'option' => 1),
1453
+ 'p' => array('p' => 1),
1454
+ 'rp' => array('rp' => 1, 'rt' => 1),
1455
+ 'rt' => array('rp' => 1, 'rt' => 1),
1456
+ 'td' => array('td' => 1, 'th' => 1),
1457
+ 'th' => array('td' => 1, 'th' => 1),
1458
+ 'tr' => array('td' => 1, 'th' => 1, 'tr' => 1),
1459
+ );
1460
+
1461
+ function __construct(
1462
+ $str = null,
1463
+ $lowercase = true,
1464
+ $forceTagsClosed = true,
1465
+ $target_charset = DEFAULT_TARGET_CHARSET,
1466
+ $stripRN = true,
1467
+ $defaultBRText = DEFAULT_BR_TEXT,
1468
+ $defaultSpanText = DEFAULT_SPAN_TEXT,
1469
+ $options = 0)
1470
+ {
1471
+ if ($str) {
1472
+ if (preg_match('/^http:\/\//i', $str) || is_file($str)) {
1473
+ $this->load_file($str);
1474
+ } else {
1475
+ $this->load(
1476
+ $str,
1477
+ $lowercase,
1478
+ $stripRN,
1479
+ $defaultBRText,
1480
+ $defaultSpanText,
1481
+ $options
1482
+ );
1483
+ }
1484
+ }
1485
+ // Forcing tags to be closed implies that we don't trust the html, but
1486
+ // it can lead to parsing errors if we SHOULD trust the html.
1487
+ if (!$forceTagsClosed) {
1488
+ $this->optional_closing_array = array();
1489
+ }
1490
+
1491
+ $this->_target_charset = $target_charset;
1492
+ }
1493
+
1494
+ function __destruct()
1495
+ {
1496
+ $this->clear();
1497
+ }
1498
+
1499
+ function load(
1500
+ $str,
1501
+ $lowercase = true,
1502
+ $stripRN = true,
1503
+ $defaultBRText = DEFAULT_BR_TEXT,
1504
+ $defaultSpanText = DEFAULT_SPAN_TEXT,
1505
+ $options = 0)
1506
+ {
1507
+ global $debug_object;
1508
+
1509
+ // prepare
1510
+ $this->prepare($str, $lowercase, $defaultBRText, $defaultSpanText);
1511
+
1512
+ // Per sourceforge http://sourceforge.net/tracker/?func=detail&aid=2949097&group_id=218559&atid=1044037
1513
+ // Script tags removal now preceeds style tag removal.
1514
+ // strip out <script> tags
1515
+ $this->remove_noise("'<\s*script[^>]*[^/]>(.*?)<\s*/\s*script\s*>'is");
1516
+ $this->remove_noise("'<\s*script\s*>(.*?)<\s*/\s*script\s*>'is");
1517
+
1518
+ // strip out the \r \n's if we are told to.
1519
+ if ($stripRN) {
1520
+ $this->doc = str_replace("\r", ' ', $this->doc);
1521
+ $this->doc = str_replace("\n", ' ', $this->doc);
1522
+
1523
+ // set the length of content since we have changed it.
1524
+ $this->size = strlen($this->doc);
1525
+ }
1526
+
1527
+ // strip out cdata
1528
+ $this->remove_noise("'<!\[CDATA\[(.*?)\]\]>'is", true);
1529
+ // strip out comments
1530
+ $this->remove_noise("'<!--(.*?)-->'is");
1531
+ // strip out <style> tags
1532
+ $this->remove_noise("'<\s*style[^>]*[^/]>(.*?)<\s*/\s*style\s*>'is");
1533
+ $this->remove_noise("'<\s*style\s*>(.*?)<\s*/\s*style\s*>'is");
1534
+ // strip out preformatted tags
1535
+ $this->remove_noise("'<\s*(?:code)[^>]*>(.*?)<\s*/\s*(?:code)\s*>'is");
1536
+ // strip out server side scripts
1537
+ $this->remove_noise("'(<\?)(.*?)(\?>)'s", true);
1538
+
1539
+ if($options & HDOM_SMARTY_AS_TEXT) { // Strip Smarty scripts
1540
+ $this->remove_noise("'(\{\w)(.*?)(\})'s", true);
1541
+ }
1542
+
1543
+ // parsing
1544
+ $this->parse();
1545
+ // end
1546
+ $this->root->_[HDOM_INFO_END] = $this->cursor;
1547
+ $this->parse_charset();
1548
+
1549
+ // make load function chainable
1550
+ return $this;
1551
+ }
1552
+
1553
+ function load_file()
1554
+ {
1555
+ $args = func_get_args();
1556
+
1557
+ if(($doc = call_user_func_array('file_get_contents', $args)) !== false) {
1558
+ $this->load($doc, true);
1559
+ } else {
1560
+ return false;
1561
+ }
1562
+ }
1563
+
1564
+ function set_callback($function_name)
1565
+ {
1566
+ $this->callback = $function_name;
1567
+ }
1568
+
1569
+ function remove_callback()
1570
+ {
1571
+ $this->callback = null;
1572
+ }
1573
+
1574
+ function save($filepath = '')
1575
+ {
1576
+ $ret = $this->root->innertext();
1577
+ if ($filepath !== '') { file_put_contents($filepath, $ret, LOCK_EX); }
1578
+ return $ret;
1579
+ }
1580
+
1581
+ function find($selector, $idx = null, $lowercase = false)
1582
+ {
1583
+ return $this->root->find($selector, $idx, $lowercase);
1584
+ }
1585
+
1586
+ function clear()
1587
+ {
1588
+ if (isset($this->nodes)) {
1589
+ foreach ($this->nodes as $n) {
1590
+ $n->clear();
1591
+ $n = null;
1592
+ }
1593
+ }
1594
+
1595
+ // This add next line is documented in the sourceforge repository.
1596
+ // 2977248 as a fix for ongoing memory leaks that occur even with the
1597
+ // use of clear.
1598
+ if (isset($this->children)) {
1599
+ foreach ($this->children as $n) {
1600
+ $n->clear();
1601
+ $n = null;
1602
+ }
1603
+ }
1604
+
1605
+ if (isset($this->parent)) {
1606
+ $this->parent->clear();
1607
+ unset($this->parent);
1608
+ }
1609
+
1610
+ if (isset($this->root)) {
1611
+ $this->root->clear();
1612
+ unset($this->root);
1613
+ }
1614
+
1615
+ unset($this->doc);
1616
+ unset($this->noise);
1617
+ }
1618
+
1619
+ function dump($show_attr = true)
1620
+ {
1621
+ $this->root->dump($show_attr);
1622
+ }
1623
+
1624
+ protected function prepare(
1625
+ $str, $lowercase = true,
1626
+ $defaultBRText = DEFAULT_BR_TEXT,
1627
+ $defaultSpanText = DEFAULT_SPAN_TEXT)
1628
+ {
1629
+ $this->clear();
1630
+
1631
+ $this->doc = trim($str);
1632
+ $this->size = strlen($this->doc);
1633
+ $this->original_size = $this->size; // original size of the html
1634
+ $this->pos = 0;
1635
+ $this->cursor = 1;
1636
+ $this->noise = array();
1637
+ $this->nodes = array();
1638
+ $this->lowercase = $lowercase;
1639
+ $this->default_br_text = $defaultBRText;
1640
+ $this->default_span_text = $defaultSpanText;
1641
+ $this->root = new simple_html_dom_node($this);
1642
+ $this->root->tag = 'root';
1643
+ $this->root->_[HDOM_INFO_BEGIN] = -1;
1644
+ $this->root->nodetype = HDOM_TYPE_ROOT;
1645
+ $this->parent = $this->root;
1646
+ if ($this->size > 0) { $this->char = $this->doc[0]; }
1647
+ }
1648
+
1649
+ protected function parse()
1650
+ {
1651
+ while (true) {
1652
+ // Read next tag if there is no text between current position and the
1653
+ // next opening tag.
1654
+ if (($s = $this->copy_until_char('<')) === '') {
1655
+ if($this->read_tag()) {
1656
+ continue;
1657
+ } else {
1658
+ return true;
1659
+ }
1660
+ }
1661
+
1662
+ // Add a text node for text between tags
1663
+ $node = new simple_html_dom_node($this);
1664
+ ++$this->cursor;
1665
+ $node->_[HDOM_INFO_TEXT] = $s;
1666
+ $this->link_nodes($node, false);
1667
+ }
1668
+ }
1669
+
1670
+ protected function parse_charset()
1671
+ {
1672
+ global $debug_object;
1673
+
1674
+ $charset = null;
1675
+
1676
+ if (function_exists('get_last_retrieve_url_contents_content_type')) {
1677
+ $contentTypeHeader = get_last_retrieve_url_contents_content_type();
1678
+ $success = preg_match('/charset=(.+)/', $contentTypeHeader, $matches);
1679
+ if ($success) {
1680
+ $charset = $matches[1];
1681
+ if (is_object($debug_object)) {
1682
+ $debug_object->debug_log(2,
1683
+ 'header content-type found charset of: '
1684
+ . $charset
1685
+ );
1686
+ }
1687
+ }
1688
+ }
1689
+
1690
+ if (empty($charset)) {
1691
+ // https://www.w3.org/TR/html/document-metadata.html#statedef-http-equiv-content-type
1692
+ $el = $this->root->find('meta[http-equiv=Content-Type]', 0, true);
1693
+
1694
+ if (!empty($el)) {
1695
+ $fullvalue = $el->content;
1696
+ if (is_object($debug_object)) {
1697
+ $debug_object->debug_log(2,
1698
+ 'meta content-type tag found'
1699
+ . $fullvalue
1700
+ );
1701
+ }
1702
+
1703
+ if (!empty($fullvalue)) {
1704
+ $success = preg_match(
1705
+ '/charset=(.+)/i',
1706
+ $fullvalue,
1707
+ $matches
1708
+ );
1709
+
1710
+ if ($success) {
1711
+ $charset = $matches[1];
1712
+ } else {
1713
+ // If there is a meta tag, and they don't specify the
1714
+ // character set, research says that it's typically
1715
+ // ISO-8859-1
1716
+ if (is_object($debug_object)) {
1717
+ $debug_object->debug_log(2,
1718
+ 'meta content-type tag couldn\'t be parsed. using iso-8859 default.'
1719
+ );
1720
+ }
1721
+
1722
+ $charset = 'ISO-8859-1';
1723
+ }
1724
+ }
1725
+ }
1726
+ }
1727
+
1728
+ if (empty($charset)) {
1729
+ // https://www.w3.org/TR/html/document-metadata.html#character-encoding-declaration
1730
+ if ($meta = $this->root->find('meta[charset]', 0)) {
1731
+ $charset = $meta->charset;
1732
+ if (is_object($debug_object)) {
1733
+ $debug_object->debug_log(2, 'meta charset: ' . $charset);
1734
+ }
1735
+ }
1736
+ }
1737
+
1738
+ if (empty($charset)) {
1739
+ // Try to guess the charset based on the content
1740
+ // Requires Multibyte String (mbstring) support (optional)
1741
+ if (function_exists('mb_detect_encoding')) {
1742
+ /**
1743
+ * mb_detect_encoding() is not intended to distinguish between
1744
+ * charsets, especially single-byte charsets. Its primary
1745
+ * purpose is to detect which multibyte encoding is in use,
1746
+ * i.e. UTF-8, UTF-16, shift-JIS, etc.
1747
+ *
1748
+ * -- https://bugs.php.net/bug.php?id=38138
1749
+ *
1750
+ * Adding both CP1251/ISO-8859-5 and CP1252/ISO-8859-1 will
1751
+ * always result in CP1251/ISO-8859-5 and vice versa.
1752
+ *
1753
+ * Thus, only detect if it's either UTF-8 or CP1252/ISO-8859-1
1754
+ * to stay compatible.
1755
+ */
1756
+ $encoding = mb_detect_encoding(
1757
+ $this->doc,
1758
+ array( 'UTF-8', 'CP1252', 'ISO-8859-1' )
1759
+ );
1760
+
1761
+ if ($encoding === 'CP1252' || $encoding === 'ISO-8859-1') {
1762
+ // Due to a limitation of mb_detect_encoding
1763
+ // 'CP1251'/'ISO-8859-5' will be detected as
1764
+ // 'CP1252'/'ISO-8859-1'. This will cause iconv to fail, in
1765
+ // which case we can simply assume it is the other charset.
1766
+ if (!@iconv('CP1252', 'UTF-8', $this->doc)) {
1767
+ $encoding = 'CP1251';
1768
+ }
1769
+ }
1770
+
1771
+ if ($encoding !== false) {
1772
+ $charset = $encoding;
1773
+ if (is_object($debug_object)) {
1774
+ $debug_object->debug_log(2, 'mb_detect: ' . $charset);
1775
+ }
1776
+ }
1777
+ }
1778
+ }
1779
+
1780
+ if (empty($charset)) {
1781
+ // Assume it's UTF-8 as it is the most likely charset to be used
1782
+ $charset = 'UTF-8';
1783
+ if (is_object($debug_object)) {
1784
+ $debug_object->debug_log(2, 'No match found, assume ' . $charset);
1785
+ }
1786
+ }
1787
+
1788
+ // Since CP1252 is a superset, if we get one of it's subsets, we want
1789
+ // it instead.
1790
+ if ((strtolower($charset) == 'iso-8859-1')
1791
+ || (strtolower($charset) == 'latin1')
1792
+ || (strtolower($charset) == 'latin-1')) {
1793
+ $charset = 'CP1252';
1794
+ if (is_object($debug_object)) {
1795
+ $debug_object->debug_log(2,
1796
+ 'replacing ' . $charset . ' with CP1252 as its a superset'
1797
+ );
1798
+ }
1799
+ }
1800
+
1801
+ if (is_object($debug_object)) {
1802
+ $debug_object->debug_log(1, 'EXIT - ' . $charset);
1803
+ }
1804
+
1805
+ return $this->_charset = $charset;
1806
+ }
1807
+
1808
+ protected function read_tag()
1809
+ {
1810
+ // Set end position if no further tags found
1811
+ if ($this->char !== '<') {
1812
+ $this->root->_[HDOM_INFO_END] = $this->cursor;
1813
+ return false;
1814
+ }
1815
+
1816
+ $begin_tag_pos = $this->pos;
1817
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
1818
+
1819
+ // end tag
1820
+ if ($this->char === '/') {
1821
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
1822
+
1823
+ // Skip whitespace in end tags (i.e. in "</ html>")
1824
+ $this->skip($this->token_blank);
1825
+ $tag = $this->copy_until_char('>');
1826
+
1827
+ // Skip attributes in end tags
1828
+ if (($pos = strpos($tag, ' ')) !== false) {
1829
+ $tag = substr($tag, 0, $pos);
1830
+ }
1831
+
1832
+ $parent_lower = strtolower($this->parent->tag);
1833
+ $tag_lower = strtolower($tag);
1834
+
1835
+ // The end tag is supposed to close the parent tag. Handle situations
1836
+ // when it doesn't
1837
+ if ($parent_lower !== $tag_lower) {
1838
+ // Parent tag does not have to be closed necessarily (optional closing tag)
1839
+ // Current tag is a block tag, so it may close an ancestor
1840
+ if (isset($this->optional_closing_tags[$parent_lower])
1841
+ && isset($this->block_tags[$tag_lower])) {
1842
+
1843
+ $this->parent->_[HDOM_INFO_END] = 0;
1844
+ $org_parent = $this->parent;
1845
+
1846
+ // Traverse ancestors to find a matching opening tag
1847
+ // Stop at root node
1848
+ while (($this->parent->parent)
1849
+ && strtolower($this->parent->tag) !== $tag_lower
1850
+ ){
1851
+ $this->parent = $this->parent->parent;
1852
+ }
1853
+
1854
+ // If we don't have a match add current tag as text node
1855
+ if (strtolower($this->parent->tag) !== $tag_lower) {
1856
+ $this->parent = $org_parent; // restore origonal parent
1857
+
1858
+ if ($this->parent->parent) {
1859
+ $this->parent = $this->parent->parent;
1860
+ }
1861
+
1862
+ $this->parent->_[HDOM_INFO_END] = $this->cursor;
1863
+ return $this->as_text_node($tag);
1864
+ }
1865
+ } elseif (($this->parent->parent)
1866
+ && isset($this->block_tags[$tag_lower])
1867
+ ) {
1868
+ // Grandparent exists and current tag is a block tag, so our
1869
+ // parent doesn't have an end tag
1870
+ $this->parent->_[HDOM_INFO_END] = 0; // No end tag
1871
+ $org_parent = $this->parent;
1872
+
1873
+ // Traverse ancestors to find a matching opening tag
1874
+ // Stop at root node
1875
+ while (($this->parent->parent)
1876
+ && strtolower($this->parent->tag) !== $tag_lower
1877
+ ) {
1878
+ $this->parent = $this->parent->parent;
1879
+ }
1880
+
1881
+ // If we don't have a match add current tag as text node
1882
+ if (strtolower($this->parent->tag) !== $tag_lower) {
1883
+ $this->parent = $org_parent; // restore origonal parent
1884
+ $this->parent->_[HDOM_INFO_END] = $this->cursor;
1885
+ return $this->as_text_node($tag);
1886
+ }
1887
+ } elseif (($this->parent->parent)
1888
+ && strtolower($this->parent->parent->tag) === $tag_lower
1889
+ ) { // Grandparent exists and current tag closes it
1890
+ $this->parent->_[HDOM_INFO_END] = 0;
1891
+ $this->parent = $this->parent->parent;
1892
+ } else { // Random tag, add as text node
1893
+ return $this->as_text_node($tag);
1894
+ }
1895
+ }
1896
+
1897
+ // Set end position of parent tag to current cursor position
1898
+ $this->parent->_[HDOM_INFO_END] = $this->cursor;
1899
+
1900
+ if ($this->parent->parent) {
1901
+ $this->parent = $this->parent->parent;
1902
+ }
1903
+
1904
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
1905
+ return true;
1906
+ }
1907
+
1908
+ // start tag
1909
+ $node = new simple_html_dom_node($this);
1910
+ $node->_[HDOM_INFO_BEGIN] = $this->cursor;
1911
+ ++$this->cursor;
1912
+ $tag = $this->copy_until($this->token_slash); // Get tag name
1913
+ $node->tag_start = $begin_tag_pos;
1914
+
1915
+ // doctype, cdata & comments...
1916
+ // <!DOCTYPE html>
1917
+ // <![CDATA[ ... ]]>
1918
+ // <!-- Comment -->
1919
+ if (isset($tag[0]) && $tag[0] === '!') {
1920
+ $node->_[HDOM_INFO_TEXT] = '<' . $tag . $this->copy_until_char('>');
1921
+
1922
+ if (isset($tag[2]) && $tag[1] === '-' && $tag[2] === '-') { // Comment ("<!--")
1923
+ $node->nodetype = HDOM_TYPE_COMMENT;
1924
+ $node->tag = 'comment';
1925
+ } else { // Could be doctype or CDATA but we don't care
1926
+ $node->nodetype = HDOM_TYPE_UNKNOWN;
1927
+ $node->tag = 'unknown';
1928
+ }
1929
+
1930
+ if ($this->char === '>') { $node->_[HDOM_INFO_TEXT] .= '>'; }
1931
+
1932
+ $this->link_nodes($node, true);
1933
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
1934
+ return true;
1935
+ }
1936
+
1937
+ // The start tag cannot contain another start tag, if so add as text
1938
+ // i.e. "<<html>"
1939
+ if ($pos = strpos($tag, '<') !== false) {
1940
+ $tag = '<' . substr($tag, 0, -1);
1941
+ $node->_[HDOM_INFO_TEXT] = $tag;
1942
+ $this->link_nodes($node, false);
1943
+ $this->char = $this->doc[--$this->pos]; // prev
1944
+ return true;
1945
+ }
1946
+
1947
+ // Handle invalid tag names (i.e. "<html#doc>")
1948
+ if (!preg_match('/^\w[\w:-]*$/', $tag)) {
1949
+ $node->_[HDOM_INFO_TEXT] = '<' . $tag . $this->copy_until('<>');
1950
+
1951
+ // Next char is the beginning of a new tag, don't touch it.
1952
+ if ($this->char === '<') {
1953
+ $this->link_nodes($node, false);
1954
+ return true;
1955
+ }
1956
+
1957
+ // Next char closes current tag, add and be done with it.
1958
+ if ($this->char === '>') { $node->_[HDOM_INFO_TEXT] .= '>'; }
1959
+ $this->link_nodes($node, false);
1960
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
1961
+ return true;
1962
+ }
1963
+
1964
+ // begin tag, add new node
1965
+ $node->nodetype = HDOM_TYPE_ELEMENT;
1966
+ $tag_lower = strtolower($tag);
1967
+ $node->tag = ($this->lowercase) ? $tag_lower : $tag;
1968
+
1969
+ // handle optional closing tags
1970
+ if (isset($this->optional_closing_tags[$tag_lower])) {
1971
+ // Traverse ancestors to close all optional closing tags
1972
+ while (isset($this->optional_closing_tags[$tag_lower][strtolower($this->parent->tag)])) {
1973
+ $this->parent->_[HDOM_INFO_END] = 0;
1974
+ $this->parent = $this->parent->parent;
1975
+ }
1976
+ $node->parent = $this->parent;
1977
+ }
1978
+
1979
+ $guard = 0; // prevent infinity loop
1980
+
1981
+ // [0] Space between tag and first attribute
1982
+ $space = array($this->copy_skip($this->token_blank), '', '');
1983
+
1984
+ // attributes
1985
+ do {
1986
+ // Everything until the first equal sign should be the attribute name
1987
+ $name = $this->copy_until($this->token_equal);
1988
+
1989
+ if ($name === '' && $this->char !== null && $space[0] === '') {
1990
+ break;
1991
+ }
1992
+
1993
+ if ($guard === $this->pos) { // Escape infinite loop
1994
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
1995
+ continue;
1996
+ }
1997
+
1998
+ $guard = $this->pos;
1999
+
2000
+ // handle endless '<'
2001
+ // Out of bounds before the tag ended
2002
+ if ($this->pos >= $this->size - 1 && $this->char !== '>') {
2003
+ $node->nodetype = HDOM_TYPE_TEXT;
2004
+ $node->_[HDOM_INFO_END] = 0;
2005
+ $node->_[HDOM_INFO_TEXT] = '<' . $tag . $space[0] . $name;
2006
+ $node->tag = 'text';
2007
+ $this->link_nodes($node, false);
2008
+ return true;
2009
+ }
2010
+
2011
+ // handle mismatch '<'
2012
+ // Attributes cannot start after opening tag
2013
+ if ($this->doc[$this->pos - 1] == '<') {
2014
+ $node->nodetype = HDOM_TYPE_TEXT;
2015
+ $node->tag = 'text';
2016
+ $node->attr = array();
2017
+ $node->_[HDOM_INFO_END] = 0;
2018
+ $node->_[HDOM_INFO_TEXT] = substr(
2019
+ $this->doc,
2020
+ $begin_tag_pos,
2021
+ $this->pos - $begin_tag_pos - 1
2022
+ );
2023
+ $this->pos -= 2;
2024
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
2025
+ $this->link_nodes($node, false);
2026
+ return true;
2027
+ }
2028
+
2029
+ if ($name !== '/' && $name !== '') { // this is a attribute name
2030
+ // [1] Whitespace after attribute name
2031
+ $space[1] = $this->copy_skip($this->token_blank);
2032
+
2033
+ $name = $this->restore_noise($name); // might be a noisy name
2034
+
2035
+ if ($this->lowercase) { $name = strtolower($name); }
2036
+
2037
+ if ($this->char === '=') { // attribute with value
2038
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
2039
+ $this->parse_attr($node, $name, $space); // get attribute value
2040
+ } else {
2041
+ //no value attr: nowrap, checked selected...
2042
+ $node->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_NO;
2043
+ $node->attr[$name] = true;
2044
+ if ($this->char != '>') { $this->char = $this->doc[--$this->pos]; } // prev
2045
+ }
2046
+
2047
+ $node->_[HDOM_INFO_SPACE][] = $space;
2048
+
2049
+ // prepare for next attribute
2050
+ $space = array(
2051
+ $this->copy_skip($this->token_blank),
2052
+ '',
2053
+ ''
2054
+ );
2055
+ } else { // no more attributes
2056
+ break;
2057
+ }
2058
+ } while ($this->char !== '>' && $this->char !== '/'); // go until the tag ended
2059
+
2060
+ $this->link_nodes($node, true);
2061
+ $node->_[HDOM_INFO_ENDSPACE] = $space[0];
2062
+
2063
+ // handle empty tags (i.e. "<div/>")
2064
+ if ($this->copy_until_char('>') === '/') {
2065
+ $node->_[HDOM_INFO_ENDSPACE] .= '/';
2066
+ $node->_[HDOM_INFO_END] = 0;
2067
+ } else {
2068
+ // reset parent
2069
+ if (!isset($this->self_closing_tags[strtolower($node->tag)])) {
2070
+ $this->parent = $node;
2071
+ }
2072
+ }
2073
+
2074
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
2075
+
2076
+ // If it's a BR tag, we need to set it's text to the default text.
2077
+ // This way when we see it in plaintext, we can generate formatting that the user wants.
2078
+ // since a br tag never has sub nodes, this works well.
2079
+ if ($node->tag === 'br') {
2080
+ $node->_[HDOM_INFO_INNER] = $this->default_br_text;
2081
+ }
2082
+
2083
+ return true;
2084
+ }
2085
+
2086
+ protected function parse_attr($node, $name, &$space)
2087
+ {
2088
+ $is_duplicate = isset($node->attr[$name]);
2089
+
2090
+ if (!$is_duplicate) // Copy whitespace between "=" and value
2091
+ $space[2] = $this->copy_skip($this->token_blank);
2092
+
2093
+ switch ($this->char) {
2094
+ case '"':
2095
+ $quote_type = HDOM_QUOTE_DOUBLE;
2096
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
2097
+ $value = $this->copy_until_char('"');
2098
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
2099
+ break;
2100
+ case '\'':
2101
+ $quote_type = HDOM_QUOTE_SINGLE;
2102
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
2103
+ $value = $this->copy_until_char('\'');
2104
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
2105
+ break;
2106
+ default:
2107
+ $quote_type = HDOM_QUOTE_NO;
2108
+ $value = $this->copy_until($this->token_attr);
2109
+ }
2110
+
2111
+ $value = $this->restore_noise($value);
2112
+
2113
+ // PaperG: Attributes should not have \r or \n in them, that counts as
2114
+ // html whitespace.
2115
+ $value = str_replace("\r", '', $value);
2116
+ $value = str_replace("\n", '', $value);
2117
+
2118
+ // PaperG: If this is a "class" selector, lets get rid of the preceeding
2119
+ // and trailing space since some people leave it in the multi class case.
2120
+ if ($name === 'class') {
2121
+ $value = trim($value);
2122
+ }
2123
+
2124
+ if (!$is_duplicate) {
2125
+ $node->_[HDOM_INFO_QUOTE][] = $quote_type;
2126
+ $node->attr[$name] = $value;
2127
+ }
2128
+ }
2129
+
2130
+ protected function link_nodes(&$node, $is_child)
2131
+ {
2132
+ $node->parent = $this->parent;
2133
+ $this->parent->nodes[] = $node;
2134
+ if ($is_child) {
2135
+ $this->parent->children[] = $node;
2136
+ }
2137
+ }
2138
+
2139
+ protected function as_text_node($tag)
2140
+ {
2141
+ $node = new simple_html_dom_node($this);
2142
+ ++$this->cursor;
2143
+ $node->_[HDOM_INFO_TEXT] = '</' . $tag . '>';
2144
+ $this->link_nodes($node, false);
2145
+ $this->char = (++$this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
2146
+ return true;
2147
+ }
2148
+
2149
+ protected function skip($chars)
2150
+ {
2151
+ $this->pos += strspn($this->doc, $chars, $this->pos);
2152
+ $this->char = ($this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
2153
+ }
2154
+
2155
+ protected function copy_skip($chars)
2156
+ {
2157
+ $pos = $this->pos;
2158
+ $len = strspn($this->doc, $chars, $pos);
2159
+ $this->pos += $len;
2160
+ $this->char = ($this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
2161
+ if ($len === 0) { return ''; }
2162
+ return substr($this->doc, $pos, $len);
2163
+ }
2164
+
2165
+ protected function copy_until($chars)
2166
+ {
2167
+ $pos = $this->pos;
2168
+ $len = strcspn($this->doc, $chars, $pos);
2169
+ $this->pos += $len;
2170
+ $this->char = ($this->pos < $this->size) ? $this->doc[$this->pos] : null; // next
2171
+ return substr($this->doc, $pos, $len);
2172
+ }
2173
+
2174
+ protected function copy_until_char($char)
2175
+ {
2176
+ if ($this->char === null) { return ''; }
2177
+
2178
+ if (($pos = strpos($this->doc, $char, $this->pos)) === false) {
2179
+ $ret = substr($this->doc, $this->pos, $this->size - $this->pos);
2180
+ $this->char = null;
2181
+ $this->pos = $this->size;
2182
+ return $ret;
2183
+ }
2184
+
2185
+ if ($pos === $this->pos) { return ''; }
2186
+
2187
+ $pos_old = $this->pos;
2188
+ $this->char = $this->doc[$pos];
2189
+ $this->pos = $pos;
2190
+ return substr($this->doc, $pos_old, $pos - $pos_old);
2191
+ }
2192
+
2193
+ protected function remove_noise($pattern, $remove_tag = false)
2194
+ {
2195
+ global $debug_object;
2196
+ if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
2197
+
2198
+ $count = preg_match_all(
2199
+ $pattern,
2200
+ $this->doc,
2201
+ $matches,
2202
+ PREG_SET_ORDER | PREG_OFFSET_CAPTURE
2203
+ );
2204
+
2205
+ for ($i = $count - 1; $i > -1; --$i) {
2206
+ $key = '___noise___' . sprintf('% 5d', count($this->noise) + 1000);
2207
+
2208
+ if (is_object($debug_object)) {
2209
+ $debug_object->debug_log(2, 'key is: ' . $key);
2210
+ }
2211
+
2212
+ $idx = ($remove_tag) ? 0 : 1; // 0 = entire match, 1 = submatch
2213
+ $this->noise[$key] = $matches[$i][$idx][0];
2214
+ $this->doc = substr_replace($this->doc, $key, $matches[$i][$idx][1], strlen($matches[$i][$idx][0]));
2215
+ }
2216
+
2217
+ // reset the length of content
2218
+ $this->size = strlen($this->doc);
2219
+
2220
+ if ($this->size > 0) {
2221
+ $this->char = $this->doc[0];
2222
+ }
2223
+ }
2224
+
2225
+ function restore_noise($text)
2226
+ {
2227
+ global $debug_object;
2228
+ if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
2229
+
2230
+ while (($pos = strpos($text, '___noise___')) !== false) {
2231
+ // Sometimes there is a broken piece of markup, and we don't GET the
2232
+ // pos+11 etc... token which indicates a problem outside of us...
2233
+
2234
+ // todo: "___noise___1000" (or any number with four or more digits)
2235
+ // in the DOM causes an infinite loop which could be utilized by
2236
+ // malicious software
2237
+ if (strlen($text) > $pos + 15) {
2238
+ $key = '___noise___'
2239
+ . $text[$pos + 11]
2240
+ . $text[$pos + 12]
2241
+ . $text[$pos + 13]
2242
+ . $text[$pos + 14]
2243
+ . $text[$pos + 15];
2244
+
2245
+ if (is_object($debug_object)) {
2246
+ $debug_object->debug_log(2, 'located key of: ' . $key);
2247
+ }
2248
+
2249
+ if (isset($this->noise[$key])) {
2250
+ $text = substr($text, 0, $pos)
2251
+ . $this->noise[$key]
2252
+ . substr($text, $pos + 16);
2253
+ } else {
2254
+ // do this to prevent an infinite loop.
2255
+ $text = substr($text, 0, $pos)
2256
+ . 'UNDEFINED NOISE FOR KEY: '
2257
+ . $key
2258
+ . substr($text, $pos + 16);
2259
+ }
2260
+ } else {
2261
+ // There is no valid key being given back to us... We must get
2262
+ // rid of the ___noise___ or we will have a problem.
2263
+ $text = substr($text, 0, $pos)
2264
+ . 'NO NUMERIC NOISE KEY'
2265
+ . substr($text, $pos + 11);
2266
+ }
2267
+ }
2268
+ return $text;
2269
+ }
2270
+
2271
+ function search_noise($text)
2272
+ {
2273
+ global $debug_object;
2274
+ if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
2275
+
2276
+ foreach($this->noise as $noiseElement) {
2277
+ if (strpos($noiseElement, $text) !== false) {
2278
+ return $noiseElement;
2279
+ }
2280
+ }
2281
+ }
2282
+
2283
+ function __toString()
2284
+ {
2285
+ return $this->root->innertext();
2286
+ }
2287
+
2288
+ function __get($name)
2289
+ {
2290
+ switch ($name) {
2291
+ case 'outertext':
2292
+ return $this->root->innertext();
2293
+ case 'innertext':
2294
+ return $this->root->innertext();
2295
+ case 'plaintext':
2296
+ return $this->root->text();
2297
+ case 'charset':
2298
+ return $this->_charset;
2299
+ case 'target_charset':
2300
+ return $this->_target_charset;
2301
+ }
2302
+ }
2303
+
2304
+ function childNodes($idx = -1)
2305
+ {
2306
+ return $this->root->childNodes($idx);
2307
+ }
2308
+
2309
+ function firstChild()
2310
+ {
2311
+ return $this->root->first_child();
2312
+ }
2313
+
2314
+ function lastChild()
2315
+ {
2316
+ return $this->root->last_child();
2317
+ }
2318
+
2319
+ function createElement($name, $value = null)
2320
+ {
2321
+ return @str_get_html("<$name>$value</$name>")->firstChild();
2322
+ }
2323
+
2324
+ function createTextNode($value)
2325
+ {
2326
+ return @end(str_get_html($value)->nodes);
2327
+ }
2328
+
2329
+ function getElementById($id)
2330
+ {
2331
+ return $this->find("#$id", 0);
2332
+ }
2333
+
2334
+ function getElementsById($id, $idx = null)
2335
+ {
2336
+ return $this->find("#$id", $idx);
2337
+ }
2338
+
2339
+ function getElementByTagName($name)
2340
+ {
2341
+ return $this->find($name, 0);
2342
+ }
2343
+
2344
+ function getElementsByTagName($name, $idx = -1)
2345
+ {
2346
+ return $this->find($name, $idx);
2347
+ }
2348
+
2349
+ function loadFile()
2350
+ {
2351
+ $args = func_get_args();
2352
+ $this->load_file($args);
2353
+ }
2354
+ }
classes/maxCSSParser.php CHANGED
@@ -55,7 +55,8 @@ class maxCSSParser
55
  {
56
  $this->domObj = $domObj;
57
 
58
- $root = $domObj->find(0,0);
 
59
  $struct[$root->tag] = array();
60
 
61
  $children = $root->children();
@@ -152,14 +153,14 @@ class maxCSSParser
152
 
153
  public function compile($css)
154
  {
155
- $scss = new \Leafo\ScssPhp\Compiler();
156
  $scss->setImportPaths(MB()->get_plugin_path() . "assets/scss");
157
 
158
  $minify = get_option("maxbuttons_minify", 1);
159
 
160
  if ($minify == 1)
161
  {
162
- $scss->setFormatter('\Leafo\ScssPhp\Formatter\Crunched');
163
  }
164
 
165
  $compile = " @import '_mixins.scss'; " . $css;
@@ -595,13 +596,12 @@ class maxCSSParser
595
  return $values;
596
  }
597
 
598
- function outputInline($domObj, $pseudo = 'normal')
599
  {
600
  $domObj = $domObj->load($domObj->save());
601
 
602
  $inline = $this->inline;
603
 
604
-
605
  // ISSUE #43 Sometimes this breaks
606
  if (! isset($inline[$pseudo]))
607
  return $domObj;
@@ -610,20 +610,24 @@ class maxCSSParser
610
  if ($pseudo != 'normal') // gather all elements
611
  $elements = array_merge($elements, array_keys($inline["normal"]));
612
 
613
- foreach($elements as $element )
614
  {
615
  $styles = isset($inline[$pseudo][$element]) ? $inline[$pseudo][$element] : '';
616
-
617
- if ($pseudo != 'normal') // parse all possible missing styles from pseudo el.
618
- $normstyle = $this->compile($inline['normal'][$element]);
619
-
620
  $normstyle = '';
621
- if ($pseudo != 'normal') // parse all possible missing styles from pseudo el.
622
- $normstyle = $this->compile($inline['normal'][$element]);
623
 
 
 
 
 
 
 
624
  maxUtils::addTime("CSSParser: Parse inline done");
625
 
626
- $styles = $normstyle . $this->compile($styles);
 
 
 
 
627
 
628
  $element = trim(str_replace("."," ", $element)); // molten css class, seperator.
629
 
@@ -637,6 +641,21 @@ class maxCSSParser
637
 
638
  }
639
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
640
 
641
  }
642
 
55
  {
56
  $this->domObj = $domObj;
57
 
58
+ $root = $domObj->root;
59
+
60
  $struct[$root->tag] = array();
61
 
62
  $children = $root->children();
153
 
154
  public function compile($css)
155
  {
156
+ $scss = new \ScssPhp\ScssPhp\Compiler();
157
  $scss->setImportPaths(MB()->get_plugin_path() . "assets/scss");
158
 
159
  $minify = get_option("maxbuttons_minify", 1);
160
 
161
  if ($minify == 1)
162
  {
163
+ $scss->setFormatter('\ScssPhp\ScssPhp\Formatter\Crunched');
164
  }
165
 
166
  $compile = " @import '_mixins.scss'; " . $css;
596
  return $values;
597
  }
598
 
599
+ public function outputInline($domObj, $pseudo = 'normal')
600
  {
601
  $domObj = $domObj->load($domObj->save());
602
 
603
  $inline = $this->inline;
604
 
 
605
  // ISSUE #43 Sometimes this breaks
606
  if (! isset($inline[$pseudo]))
607
  return $domObj;
610
  if ($pseudo != 'normal') // gather all elements
611
  $elements = array_merge($elements, array_keys($inline["normal"]));
612
 
613
+ foreach($elements as $element)
614
  {
615
  $styles = isset($inline[$pseudo][$element]) ? $inline[$pseudo][$element] : '';
 
 
 
 
616
  $normstyle = '';
 
 
617
 
618
+ if ($pseudo != 'normal') // parse all possible missing styles from pseudo el
619
+ {
620
+ $tocompile = $this->dummify($inline['normal'][$element]);
621
+ $normstyle = $this->compile($tocompile);
622
+ $normstyle = $this->undummify($normstyle);
623
+ }
624
  maxUtils::addTime("CSSParser: Parse inline done");
625
 
626
+ // add dummy {} here because new scssphp parse doesn't like styles without it. Remove it after compile, since this is inline.
627
+ $styles = $this->compile($this->dummify($normstyle . $styles));
628
+ $styles = $this->undummify($styles);
629
+
630
+ //$styles = $normstyle . $styles;
631
 
632
  $element = trim(str_replace("."," ", $element)); // molten css class, seperator.
633
 
641
 
642
  }
643
 
644
+ /** Dummyfi, because Scssphp compiler doesn't like CSS without {} */
645
+ private function dummify($string)
646
+ {
647
+ return 'dummy{' . $string . '}';
648
+ }
649
+
650
+ private function undummify($string)
651
+ {
652
+ $string = trim(str_replace(array('dummy','{','}'),'', $string)); // remove the dummy
653
+ if (substr($string,-1) !== ';') // the last ; might not be there, put it so we can glue more CSS there.
654
+ $string .= ';';
655
+
656
+ return $string;
657
+ }
658
+
659
 
660
  }
661
 
classes/maxbuttons-admin-helper.php CHANGED
@@ -34,8 +34,8 @@ class maxAdmin
34
  {
35
  self::$tabs = array(
36
  "list" => array("name" => __('Buttons', 'maxbuttons'),
37
- "link" => "page=maxbuttons-list",
38
- "active" => "maxbuttons-list", ),
39
  "pro" => array( "name" => __('Upgrade to Pro', 'maxbuttons'),
40
  "link" => "page=maxbuttons-pro",
41
  "active" => "maxbuttons-pro",
34
  {
35
  self::$tabs = array(
36
  "list" => array("name" => __('Buttons', 'maxbuttons'),
37
+ "link" => "page=maxbuttons-controller",
38
+ "active" => "maxbuttons-controller", ),
39
  "pro" => array( "name" => __('Upgrade to Pro', 'maxbuttons'),
40
  "link" => "page=maxbuttons-pro",
41
  "active" => "maxbuttons-pro",
classes/maxbuttons-class.php CHANGED
@@ -402,15 +402,7 @@ class maxButtonsPlugin
402
 
403
  if ($libname == 'scss')
404
  {
405
- // external libraries
406
- if ( version_compare(PHP_VERSION, '5.4', '<' ) )
407
- {
408
- require_once($this->get_plugin_path() . "assets/libraries/scssphp_legacy/scss.inc.php");
409
- }
410
- else
411
- {
412
  require_once($this->get_plugin_path() . "assets/libraries/scssphp/scss.inc.php");
413
- }
414
  }
415
 
416
  if ($libname == 'simple_template')
402
 
403
  if ($libname == 'scss')
404
  {
 
 
 
 
 
 
 
405
  require_once($this->get_plugin_path() . "assets/libraries/scssphp/scss.inc.php");
 
406
  }
407
 
408
  if ($libname == 'simple_template')
maxbuttons.php CHANGED
@@ -3,7 +3,7 @@
3
  Plugin Name: MaxButtons
4
  Plugin URI: http://maxbuttons.com
5
  Description: The best WordPress button generator. This is the free version; the Pro version <a href="http://maxbuttons.com/?ref=mbfree">can be found here</a>.
6
- Version: 7.13.3
7
  Author: Max Foundry
8
  Author URI: http://maxfoundry.com
9
  Text Domain: maxbuttons
@@ -16,9 +16,9 @@ namespace MaxButtons;
16
  if (! defined('MAXBUTTONS_ROOT_FILE'))
17
  define("MAXBUTTONS_ROOT_FILE", __FILE__);
18
  if (! defined('MAXBUTTONS_VERSION_NUM'))
19
- define('MAXBUTTONS_VERSION_NUM', '7.13.3');
20
 
21
- define('MAXBUTTONS_RELEASE',"11 November 2019");
22
 
23
  if (! function_exists('MaxButtons\maxbutton_double_load'))
24
  {
@@ -42,23 +42,6 @@ if (function_exists("MaxButtons\MB"))
42
 
43
  require_once( trailingslashit(dirname(MAXBUTTONS_ROOT_FILE)) . "classes/maxbuttons-class.php");
44
 
45
- /*
46
-
47
- // external libraries
48
- if ( version_compare(PHP_VERSION, '5.4', '<' ) )
49
- {
50
- require_once("assets/libraries/scssphp_legacy/scss.inc.php");
51
- }
52
- else
53
- {
54
- require_once("assets/libraries/scssphp/scss.inc.php");
55
- }
56
- require_once("assets/libraries/simple-template/simple_template.php");
57
-
58
- if (! class_exists('simple_html_dom_node'))
59
- require_once("assets/libraries/simplehtmldom/simple_html_dom.php");
60
- */
61
-
62
  // runtime.
63
  if (! function_exists("MaxButtons\MB")) {
64
  function MB()
3
  Plugin Name: MaxButtons
4
  Plugin URI: http://maxbuttons.com
5
  Description: The best WordPress button generator. This is the free version; the Pro version <a href="http://maxbuttons.com/?ref=mbfree">can be found here</a>.
6
+ Version: 7.13.4
7
  Author: Max Foundry
8
  Author URI: http://maxfoundry.com
9
  Text Domain: maxbuttons
16
  if (! defined('MAXBUTTONS_ROOT_FILE'))
17
  define("MAXBUTTONS_ROOT_FILE", __FILE__);
18
  if (! defined('MAXBUTTONS_VERSION_NUM'))
19
+ define('MAXBUTTONS_VERSION_NUM', '7.13.4');
20
 
21
+ define('MAXBUTTONS_RELEASE',"8 February 2020");
22
 
23
  if (! function_exists('MaxButtons\maxbutton_double_load'))
24
  {
42
 
43
  require_once( trailingslashit(dirname(MAXBUTTONS_ROOT_FILE)) . "classes/maxbuttons-class.php");
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  // runtime.
46
  if (! function_exists("MaxButtons\MB")) {
47
  function MB()
readme.txt CHANGED
@@ -4,7 +4,7 @@ Tags: wordpress button plugin, share button, wordpress buttons, css3 button gene
4
  Requires at least: 4.8
5
  Tested up to: 5.3
6
  Requires PHP: 5.6
7
- Stable tag: 7.13.3
8
 
9
  WordPress button plugin so powerful and easy to use anyone can create beautiful buttons, share buttons and social icons.
10
 
@@ -255,6 +255,12 @@ Secondly, please use latin only characters for button name ( Basic settings) and
255
 
256
  == Changelog ==
257
 
 
 
 
 
 
 
258
  = 7.13.3 =
259
 
260
  * Fixed - relative require_once include, sometimes this crashes.
4
  Requires at least: 4.8
5
  Tested up to: 5.3
6
  Requires PHP: 5.6
7
+ Stable tag: 7.13.4
8
 
9
  WordPress button plugin so powerful and easy to use anyone can create beautiful buttons, share buttons and social icons.
10
 
255
 
256
  == Changelog ==
257
 
258
+ = 7.13.4 =
259
+
260
+ * Updated ScssPhp to 1.0.7
261
+ * Updated SimpleHTMLDom to 1.9.1
262
+ * Fixed Buttons menu tab
263
+
264
  = 7.13.3 =
265
 
266
  * Fixed - relative require_once include, sometimes this crashes.