6 * @copyright 2012-2020 Leaf Corcoran
8 * @license http://opensource.org/licenses/MIT MIT
10 * @link http://scssphp.github.io/scssphp
13 namespace ScssPhp\ScssPhp
;
15 use ScssPhp\ScssPhp\Formatter\OutputBlock
;
16 use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator
;
21 * @author Leaf Corcoran <leafot@gmail.com>
25 abstract class Formatter
60 public $assignSeparator;
65 public $keepSemicolons;
68 * @var \ScssPhp\ScssPhp\Formatter\OutputBlock
70 protected $currentBlock;
75 protected $currentLine;
80 protected $currentColumn;
83 * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null
85 protected $sourceMapGenerator;
90 protected $strippedSemicolon;
93 * Initialize formatter
97 abstract public function __construct();
100 * Return indentation (whitespace)
104 protected function indentStr()
110 * Return property assignment
114 * @param string $name
115 * @param mixed $value
119 public function property($name, $value)
121 return rtrim($name) . $this->assignSeparator
. $value . ';';
125 * Return custom property assignment
126 * differs in that you have to keep spaces in the value as is
130 * @param string $name
131 * @param mixed $value
135 public function customProperty($name, $value)
137 return rtrim($name) . trim($this->assignSeparator
) . $value . ';';
141 * Output lines inside a block
143 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
147 protected function blockLines(OutputBlock
$block)
149 $inner = $this->indentStr();
150 $glue = $this->break . $inner;
152 $this->write($inner . implode($glue, $block->lines
));
154 if (! empty($block->children
)) {
155 $this->write($this->break);
160 * Output block selectors
162 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
166 protected function blockSelectors(OutputBlock
$block)
168 assert(! empty($block->selectors
));
170 $inner = $this->indentStr();
173 . implode($this->tagSeparator
, $block->selectors
)
174 . $this->open
. $this->break);
178 * Output block children
180 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
184 protected function blockChildren(OutputBlock
$block)
186 foreach ($block->children
as $child) {
187 $this->block($child);
192 * Output non-empty block
194 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
198 protected function block(OutputBlock
$block)
200 if (empty($block->lines
) && empty($block->children
)) {
204 $this->currentBlock
= $block;
206 $pre = $this->indentStr();
208 if (! empty($block->selectors
)) {
209 $this->blockSelectors($block);
211 $this->indentLevel++
;
214 if (! empty($block->lines
)) {
215 $this->blockLines($block);
218 if (! empty($block->children
)) {
219 $this->blockChildren($block);
222 if (! empty($block->selectors
)) {
223 $this->indentLevel
--;
225 if (! $this->keepSemicolons
) {
226 $this->strippedSemicolon
= '';
229 if (empty($block->children
)) {
230 $this->write($this->break);
233 $this->write($pre . $this->close
. $this->break);
238 * Test and clean safely empty children
240 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
244 protected function testEmptyChildren($block)
246 $isEmpty = empty($block->lines
);
248 if ($block->children
) {
249 foreach ($block->children
as $k => &$child) {
250 if (! $this->testEmptyChildren($child)) {
255 if ($child->type
=== Type
::T_MEDIA ||
$child->type
=== Type
::T_DIRECTIVE
) {
256 $child->children
= [];
257 $child->selectors
= null;
266 * Entry point to formatting a block
270 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block An abstract syntax tree
271 * @param \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null $sourceMapGenerator Optional source map generator
275 public function format(OutputBlock
$block, SourceMapGenerator
$sourceMapGenerator = null)
277 $this->sourceMapGenerator
= null;
279 if ($sourceMapGenerator) {
280 $this->currentLine
= 1;
281 $this->currentColumn
= 0;
282 $this->sourceMapGenerator
= $sourceMapGenerator;
285 $this->testEmptyChildren($block);
290 $this->block($block);
291 } catch (\Exception
$e) {
294 } catch (\Throwable
$e) {
299 $out = ob_get_clean();
300 assert($out !== false);
312 protected function write($str)
314 if (! empty($this->strippedSemicolon
)) {
315 echo $this->strippedSemicolon
;
317 $this->strippedSemicolon
= '';
321 * Maybe Strip semi-colon appended by property(); it's a separator, not a terminator
322 * will be striped for real before a closing, otherwise displayed unchanged starting the next write
325 ! $this->keepSemicolons
&&
327 (strpos($str, ';') !== false) &&
328 (substr($str, -1) === ';')
330 $str = substr($str, 0, -1);
332 $this->strippedSemicolon
= ';';
335 if ($this->sourceMapGenerator
) {
336 $lines = explode("\n", $str);
337 $lastLine = array_pop($lines);
339 foreach ($lines as $line) {
340 // If the written line starts is empty, adding a mapping would add it for
341 // a non-existent column as we are at the end of the line
343 assert($this->currentBlock
->sourceLine
!== null);
344 assert($this->currentBlock
->sourceName
!== null);
345 $this->sourceMapGenerator
->addMapping(
347 $this->currentColumn
,
348 $this->currentBlock
->sourceLine
,
349 //columns from parser are off by one
350 $this->currentBlock
->sourceColumn
> 0 ?
$this->currentBlock
->sourceColumn
- 1 : 0,
351 $this->currentBlock
->sourceName
355 $this->currentLine++
;
356 $this->currentColumn
= 0;
359 if ($lastLine !== '') {
360 assert($this->currentBlock
->sourceLine
!== null);
361 assert($this->currentBlock
->sourceName
!== null);
362 $this->sourceMapGenerator
->addMapping(
364 $this->currentColumn
,
365 $this->currentBlock
->sourceLine
,
366 //columns from parser are off by one
367 $this->currentBlock
->sourceColumn
> 0 ?
$this->currentBlock
->sourceColumn
- 1 : 0,
368 $this->currentBlock
->sourceName
372 $this->currentColumn +
= \
strlen($lastLine);