Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]

   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 <leafot@gmail.com>
  22   *
  23   * @internal
  24   */
  25  abstract class Formatter
  26  {
  27      /**
  28       * @var int
  29       */
  30      public $indentLevel;
  31  
  32      /**
  33       * @var string
  34       */
  35      public $indentChar;
  36  
  37      /**
  38       * @var string
  39       */
  40      public $break;
  41  
  42      /**
  43       * @var string
  44       */
  45      public $open;
  46  
  47      /**
  48       * @var string
  49       */
  50      public $close;
  51  
  52      /**
  53       * @var string
  54       */
  55      public $tagSeparator;
  56  
  57      /**
  58       * @var string
  59       */
  60      public $assignSeparator;
  61  
  62      /**
  63       * @var bool
  64       */
  65      public $keepSemicolons;
  66  
  67      /**
  68       * @var \ScssPhp\ScssPhp\Formatter\OutputBlock
  69       */
  70      protected $currentBlock;
  71  
  72      /**
  73       * @var int
  74       */
  75      protected $currentLine;
  76  
  77      /**
  78       * @var int
  79       */
  80      protected $currentColumn;
  81  
  82      /**
  83       * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null
  84       */
  85      protected $sourceMapGenerator;
  86  
  87      /**
  88       * @var string
  89       */
  90      protected $strippedSemicolon;
  91  
  92      /**
  93       * Initialize formatter
  94       *
  95       * @api
  96       */
  97      abstract public function __construct();
  98  
  99      /**
 100       * Return indentation (whitespace)
 101       *
 102       * @return string
 103       */
 104      protected function indentStr()
 105      {
 106          return '';
 107      }
 108  
 109      /**
 110       * Return property assignment
 111       *
 112       * @api
 113       *
 114       * @param string $name
 115       * @param mixed  $value
 116       *
 117       * @return string
 118       */
 119      public function property($name, $value)
 120      {
 121          return rtrim($name) . $this->assignSeparator . $value . ';';
 122      }
 123  
 124      /**
 125       * Return custom property assignment
 126       * differs in that you have to keep spaces in the value as is
 127       *
 128       * @api
 129       *
 130       * @param string $name
 131       * @param mixed  $value
 132       *
 133       * @return string
 134       */
 135      public function customProperty($name, $value)
 136      {
 137          return rtrim($name) . trim($this->assignSeparator) . $value . ';';
 138      }
 139  
 140      /**
 141       * Output lines inside a block
 142       *
 143       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
 144       *
 145       * @return void
 146       */
 147      protected function blockLines(OutputBlock $block)
 148      {
 149          $inner = $this->indentStr();
 150          $glue  = $this->break . $inner;
 151  
 152          $this->write($inner . implode($glue, $block->lines));
 153  
 154          if (! empty($block->children)) {
 155              $this->write($this->break);
 156          }
 157      }
 158  
 159      /**
 160       * Output block selectors
 161       *
 162       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
 163       *
 164       * @return void
 165       */
 166      protected function blockSelectors(OutputBlock $block)
 167      {
 168          assert(! empty($block->selectors));
 169  
 170          $inner = $this->indentStr();
 171  
 172          $this->write($inner
 173              . implode($this->tagSeparator, $block->selectors)
 174              . $this->open . $this->break);
 175      }
 176  
 177      /**
 178       * Output block children
 179       *
 180       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
 181       *
 182       * @return void
 183       */
 184      protected function blockChildren(OutputBlock $block)
 185      {
 186          foreach ($block->children as $child) {
 187              $this->block($child);
 188          }
 189      }
 190  
 191      /**
 192       * Output non-empty block
 193       *
 194       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
 195       *
 196       * @return void
 197       */
 198      protected function block(OutputBlock $block)
 199      {
 200          if (empty($block->lines) && empty($block->children)) {
 201              return;
 202          }
 203  
 204          $this->currentBlock = $block;
 205  
 206          $pre = $this->indentStr();
 207  
 208          if (! empty($block->selectors)) {
 209              $this->blockSelectors($block);
 210  
 211              $this->indentLevel++;
 212          }
 213  
 214          if (! empty($block->lines)) {
 215              $this->blockLines($block);
 216          }
 217  
 218          if (! empty($block->children)) {
 219              $this->blockChildren($block);
 220          }
 221  
 222          if (! empty($block->selectors)) {
 223              $this->indentLevel--;
 224  
 225              if (! $this->keepSemicolons) {
 226                  $this->strippedSemicolon = '';
 227              }
 228  
 229              if (empty($block->children)) {
 230                  $this->write($this->break);
 231              }
 232  
 233              $this->write($pre . $this->close . $this->break);
 234          }
 235      }
 236  
 237      /**
 238       * Test and clean safely empty children
 239       *
 240       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
 241       *
 242       * @return bool
 243       */
 244      protected function testEmptyChildren($block)
 245      {
 246          $isEmpty = empty($block->lines);
 247  
 248          if ($block->children) {
 249              foreach ($block->children as $k => &$child) {
 250                  if (! $this->testEmptyChildren($child)) {
 251                      $isEmpty = false;
 252                      continue;
 253                  }
 254  
 255                  if ($child->type === Type::T_MEDIA || $child->type === Type::T_DIRECTIVE) {
 256                      $child->children = [];
 257                      $child->selectors = null;
 258                  }
 259              }
 260          }
 261  
 262          return $isEmpty;
 263      }
 264  
 265      /**
 266       * Entry point to formatting a block
 267       *
 268       * @api
 269       *
 270       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock             $block              An abstract syntax tree
 271       * @param \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null $sourceMapGenerator Optional source map generator
 272       *
 273       * @return string
 274       */
 275      public function format(OutputBlock $block, SourceMapGenerator $sourceMapGenerator = null)
 276      {
 277          $this->sourceMapGenerator = null;
 278  
 279          if ($sourceMapGenerator) {
 280              $this->currentLine        = 1;
 281              $this->currentColumn      = 0;
 282              $this->sourceMapGenerator = $sourceMapGenerator;
 283          }
 284  
 285          $this->testEmptyChildren($block);
 286  
 287          ob_start();
 288  
 289          $this->block($block);
 290  
 291          $out = ob_get_clean();
 292  
 293          return $out;
 294      }
 295  
 296      /**
 297       * Output content
 298       *
 299       * @param string $str
 300       *
 301       * @return void
 302       */
 303      protected function write($str)
 304      {
 305          if (! empty($this->strippedSemicolon)) {
 306              echo $this->strippedSemicolon;
 307  
 308              $this->strippedSemicolon = '';
 309          }
 310  
 311          /*
 312           * Maybe Strip semi-colon appended by property(); it's a separator, not a terminator
 313           * will be striped for real before a closing, otherwise displayed unchanged starting the next write
 314           */
 315          if (
 316              ! $this->keepSemicolons &&
 317              $str &&
 318              (strpos($str, ';') !== false) &&
 319              (substr($str, -1) === ';')
 320          ) {
 321              $str = substr($str, 0, -1);
 322  
 323              $this->strippedSemicolon = ';';
 324          }
 325  
 326          if ($this->sourceMapGenerator) {
 327              $lines = explode("\n", $str);
 328              $lastLine = array_pop($lines);
 329  
 330              foreach ($lines as $line) {
 331                  // If the written line starts is empty, adding a mapping would add it for
 332                  // a non-existent column as we are at the end of the line
 333                  if ($line !== '') {
 334                      $this->sourceMapGenerator->addMapping(
 335                          $this->currentLine,
 336                          $this->currentColumn,
 337                          $this->currentBlock->sourceLine,
 338                          //columns from parser are off by one
 339                          $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0,
 340                          $this->currentBlock->sourceName
 341                      );
 342                  }
 343  
 344                  $this->currentLine++;
 345                  $this->currentColumn = 0;
 346              }
 347  
 348              if ($lastLine !== '') {
 349                  $this->sourceMapGenerator->addMapping(
 350                      $this->currentLine,
 351                      $this->currentColumn,
 352                      $this->currentBlock->sourceLine,
 353                      //columns from parser are off by one
 354                      $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0,
 355                      $this->currentBlock->sourceName
 356                  );
 357              }
 358  
 359              $this->currentColumn += \strlen($lastLine);
 360          }
 361  
 362          echo $str;
 363      }
 364  }