1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* SCSSPHP |
5
|
|
|
* |
6
|
|
|
* @copyright 2012-2020 Leaf Corcoran |
7
|
|
|
* |
8
|
|
|
* @license http://opensource.org/licenses/MIT MIT |
9
|
|
|
* |
10
|
|
|
* @link http://scssphp.github.io/scssphp |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
namespace ScssPhp\ScssPhp; |
14
|
|
|
|
15
|
|
|
use ScssPhp\ScssPhp\Formatter\OutputBlock; |
16
|
|
|
use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* Base formatter |
20
|
|
|
* |
21
|
|
|
* @author Leaf Corcoran <[email protected]> |
22
|
|
|
*/ |
23
|
|
|
abstract class Formatter |
24
|
|
|
{ |
25
|
|
|
/** |
26
|
|
|
* @var integer |
27
|
|
|
*/ |
28
|
|
|
public $indentLevel; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var string |
32
|
|
|
*/ |
33
|
|
|
public $indentChar; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var string |
37
|
|
|
*/ |
38
|
|
|
public $break; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @var string |
42
|
|
|
*/ |
43
|
|
|
public $open; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @var string |
47
|
|
|
*/ |
48
|
|
|
public $close; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @var string |
52
|
|
|
*/ |
53
|
|
|
public $tagSeparator; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* @var string |
57
|
|
|
*/ |
58
|
|
|
public $assignSeparator; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* @var boolean |
62
|
|
|
*/ |
63
|
|
|
public $keepSemicolons; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @var \ScssPhp\ScssPhp\Formatter\OutputBlock |
67
|
|
|
*/ |
68
|
|
|
protected $currentBlock; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* @var integer |
72
|
|
|
*/ |
73
|
|
|
protected $currentLine; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* @var integer |
77
|
|
|
*/ |
78
|
|
|
protected $currentColumn; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator |
82
|
|
|
*/ |
83
|
|
|
protected $sourceMapGenerator; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* @var string |
87
|
|
|
*/ |
88
|
|
|
protected $strippedSemicolon; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Initialize formatter |
92
|
|
|
* |
93
|
|
|
* @api |
94
|
|
|
*/ |
95
|
|
|
abstract public function __construct(); |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Return indentation (whitespace) |
99
|
|
|
* |
100
|
|
|
* @return string |
101
|
|
|
*/ |
102
|
|
|
protected function indentStr() |
103
|
|
|
{ |
104
|
|
|
return ''; |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* Return property assignment |
109
|
|
|
* |
110
|
|
|
* @api |
111
|
|
|
* |
112
|
|
|
* @param string $name |
113
|
|
|
* @param mixed $value |
114
|
|
|
* |
115
|
|
|
* @return string |
116
|
|
|
*/ |
117
|
|
|
public function property($name, $value) |
118
|
|
|
{ |
119
|
|
|
return rtrim($name) . $this->assignSeparator . $value . ';'; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Return custom property assignment |
124
|
|
|
* differs in that you have to keep spaces in the value as is |
125
|
|
|
* |
126
|
|
|
* @api |
127
|
|
|
* |
128
|
|
|
* @param string $name |
129
|
|
|
* @param mixed $value |
130
|
|
|
* |
131
|
|
|
* @return string |
132
|
|
|
*/ |
133
|
|
|
public function customProperty($name, $value) |
134
|
|
|
{ |
135
|
|
|
return rtrim($name) . trim($this->assignSeparator) . $value . ';'; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* Output lines inside a block |
140
|
|
|
* |
141
|
|
|
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block |
142
|
|
|
*/ |
143
|
|
|
protected function blockLines(OutputBlock $block) |
144
|
|
|
{ |
145
|
|
|
$inner = $this->indentStr(); |
146
|
|
|
$glue = $this->break . $inner; |
147
|
|
|
|
148
|
|
|
$this->write($inner . implode($glue, $block->lines)); |
149
|
|
|
|
150
|
|
|
if (! empty($block->children)) { |
151
|
|
|
$this->write($this->break); |
152
|
|
|
} |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Output block selectors |
157
|
|
|
* |
158
|
|
|
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block |
159
|
|
|
*/ |
160
|
|
|
protected function blockSelectors(OutputBlock $block) |
161
|
|
|
{ |
162
|
|
|
$inner = $this->indentStr(); |
163
|
|
|
|
164
|
|
|
$this->write($inner |
165
|
|
|
. implode($this->tagSeparator, $block->selectors) |
166
|
|
|
. $this->open . $this->break); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* Output block children |
171
|
|
|
* |
172
|
|
|
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block |
173
|
|
|
*/ |
174
|
|
|
protected function blockChildren(OutputBlock $block) |
175
|
|
|
{ |
176
|
|
|
foreach ($block->children as $child) { |
177
|
|
|
$this->block($child); |
178
|
|
|
} |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* Output non-empty block |
183
|
|
|
* |
184
|
|
|
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block |
185
|
|
|
*/ |
186
|
|
|
protected function block(OutputBlock $block) |
187
|
|
|
{ |
188
|
|
|
if (empty($block->lines) && empty($block->children)) { |
189
|
|
|
return; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
$this->currentBlock = $block; |
193
|
|
|
|
194
|
|
|
$pre = $this->indentStr(); |
195
|
|
|
|
196
|
|
|
if (! empty($block->selectors)) { |
197
|
|
|
$this->blockSelectors($block); |
198
|
|
|
|
199
|
|
|
$this->indentLevel++; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
if (! empty($block->lines)) { |
203
|
|
|
$this->blockLines($block); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
if (! empty($block->children)) { |
207
|
|
|
$this->blockChildren($block); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
if (! empty($block->selectors)) { |
211
|
|
|
$this->indentLevel--; |
212
|
|
|
|
213
|
|
|
if (! $this->keepSemicolons) { |
214
|
|
|
$this->strippedSemicolon = ''; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
if (empty($block->children)) { |
218
|
|
|
$this->write($this->break); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
$this->write($pre . $this->close . $this->break); |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* Test and clean safely empty children |
227
|
|
|
* |
228
|
|
|
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block |
229
|
|
|
* |
230
|
|
|
* @return boolean |
231
|
|
|
*/ |
232
|
|
|
protected function testEmptyChildren($block) |
233
|
|
|
{ |
234
|
|
|
$isEmpty = empty($block->lines); |
235
|
|
|
|
236
|
|
|
if ($block->children) { |
|
|
|
|
237
|
|
|
foreach ($block->children as $k => &$child) { |
238
|
|
|
if (! $this->testEmptyChildren($child)) { |
239
|
|
|
$isEmpty = false; |
240
|
|
|
continue; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
if ($child->type === Type::T_MEDIA || $child->type === Type::T_DIRECTIVE) { |
244
|
|
|
$child->children = []; |
245
|
|
|
$child->selectors = null; |
246
|
|
|
} |
247
|
|
|
} |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
return $isEmpty; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* Entry point to formatting a block |
255
|
|
|
* |
256
|
|
|
* @api |
257
|
|
|
* |
258
|
|
|
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block An abstract syntax tree |
259
|
|
|
* @param \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null $sourceMapGenerator Optional source map generator |
260
|
|
|
* |
261
|
|
|
* @return string |
262
|
|
|
*/ |
263
|
|
|
public function format(OutputBlock $block, SourceMapGenerator $sourceMapGenerator = null) |
264
|
|
|
{ |
265
|
|
|
$this->sourceMapGenerator = null; |
266
|
|
|
|
267
|
|
|
if ($sourceMapGenerator) { |
268
|
|
|
$this->currentLine = 1; |
269
|
|
|
$this->currentColumn = 0; |
270
|
|
|
$this->sourceMapGenerator = $sourceMapGenerator; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
$this->testEmptyChildren($block); |
274
|
|
|
|
275
|
|
|
ob_start(); |
276
|
|
|
|
277
|
|
|
$this->block($block); |
278
|
|
|
|
279
|
|
|
$out = ob_get_clean(); |
280
|
|
|
|
281
|
|
|
return $out; |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
/** |
285
|
|
|
* Output content |
286
|
|
|
* |
287
|
|
|
* @param string $str |
288
|
|
|
*/ |
289
|
|
|
protected function write($str) |
290
|
|
|
{ |
291
|
|
|
if (! empty($this->strippedSemicolon)) { |
292
|
|
|
echo $this->strippedSemicolon; |
293
|
|
|
|
294
|
|
|
$this->strippedSemicolon = ''; |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/* |
298
|
|
|
* Maybe Strip semi-colon appended by property(); it's a separator, not a terminator |
299
|
|
|
* will be striped for real before a closing, otherwise displayed unchanged starting the next write |
300
|
|
|
*/ |
301
|
|
|
if ( |
302
|
|
|
! $this->keepSemicolons && |
303
|
|
|
$str && |
304
|
|
|
(strpos($str, ';') !== false) && |
305
|
|
|
(substr($str, -1) === ';') |
306
|
|
|
) { |
307
|
|
|
$str = substr($str, 0, -1); |
308
|
|
|
|
309
|
|
|
$this->strippedSemicolon = ';'; |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
if ($this->sourceMapGenerator) { |
313
|
|
|
$this->sourceMapGenerator->addMapping( |
314
|
|
|
$this->currentLine, |
315
|
|
|
$this->currentColumn, |
316
|
|
|
$this->currentBlock->sourceLine, |
317
|
|
|
//columns from parser are off by one |
318
|
|
|
$this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0, |
319
|
|
|
$this->currentBlock->sourceName |
320
|
|
|
); |
321
|
|
|
|
322
|
|
|
$lines = explode("\n", $str); |
323
|
|
|
$lineCount = \count($lines); |
324
|
|
|
$this->currentLine += $lineCount - 1; |
325
|
|
|
|
326
|
|
|
$lastLine = array_pop($lines); |
327
|
|
|
|
328
|
|
|
$this->currentColumn = ($lineCount === 1 ? $this->currentColumn : 0) + \strlen($lastLine); |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
echo $str; |
332
|
|
|
} |
333
|
|
|
} |
334
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.