Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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

   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          try {
 290              $this->block($block);
 291          } catch (\Exception $e) {
 292              ob_end_clean();
 293              throw $e;
 294          } catch (\Throwable $e) {
 295              ob_end_clean();
 296              throw $e;
 297          }
 298  
 299          $out = ob_get_clean();
 300          assert($out !== false);
 301  
 302          return $out;
 303      }
 304  
 305      /**
 306       * Output content
 307       *
 308       * @param string $str
 309       *
 310       * @return void
 311       */
 312      protected function write($str)
 313      {
 314          if (! empty($this->strippedSemicolon)) {
 315              echo $this->strippedSemicolon;
 316  
 317              $this->strippedSemicolon = '';
 318          }
 319  
 320          /*
 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
 323           */
 324          if (
 325              ! $this->keepSemicolons &&
 326              $str &&
 327              (strpos($str, ';') !== false) &&
 328              (substr($str, -1) === ';')
 329          ) {
 330              $str = substr($str, 0, -1);
 331  
 332              $this->strippedSemicolon = ';';
 333          }
 334  
 335          if ($this->sourceMapGenerator) {
 336              $lines = explode("\n", $str);
 337              $lastLine = array_pop($lines);
 338  
 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
 342                  if ($line !== '') {
 343                      assert($this->currentBlock->sourceLine !== null);
 344                      assert($this->currentBlock->sourceName !== null);
 345                      $this->sourceMapGenerator->addMapping(
 346                          $this->currentLine,
 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
 352                      );
 353                  }
 354  
 355                  $this->currentLine++;
 356                  $this->currentColumn = 0;
 357              }
 358  
 359              if ($lastLine !== '') {
 360                  assert($this->currentBlock->sourceLine !== null);
 361                  assert($this->currentBlock->sourceName !== null);
 362                  $this->sourceMapGenerator->addMapping(
 363                      $this->currentLine,
 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
 369                  );
 370              }
 371  
 372              $this->currentColumn += \strlen($lastLine);
 373          }
 374  
 375          echo $str;
 376      }
 377  }