Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

   1  <?php
   2  /**
   3   * SCSSPHP
   4   *
   5   * @copyright 2012-2019 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
  18   *
  19   * {@internal Derivative of oyejorge/less.php's lib/SourceMap/Generator.php, relicensed with permission. }}
  20   *
  21   * @author Josh Schmidt <oyejorge@gmail.com>
  22   * @author Nicolas FRANÇOIS <nicolas.francois@frog-labs.com>
  23   */
  24  class SourceMapGenerator
  25  {
  26      /**
  27       * What version of source map does the generator generate?
  28       */
  29      const VERSION = 3;
  30  
  31      /**
  32       * Array of default options
  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.
  40          'sourceRoot' => '',
  41  
  42          // an optional name of the generated code that this source map is associated with.
  43          'sourceMapFilename' => null,
  44  
  45          // url of the map
  46          'sourceMapURL' => null,
  47  
  48          // absolute path to a file to write the map to
  49          'sourceMapWriteTo' => null,
  50  
  51          // output source contents?
  52          'outputSourceFiles' => false,
  53  
  54          // base path for filename normalization
  55          'sourceMapRootpath' => '',
  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  
  68      /**
  69       * Array of mappings
  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
  92       */
  93      private $options;
  94  
  95      public function __construct(array $options = [])
  96      {
  97          $this->options = array_merge($this->defaultOptions, $options);
  98          $this->encoder = new Base64VLQ();
  99      }
 100  
 101      /**
 102       * Adds a mapping
 103       *
 104       * @param integer $generatedLine   The line number in generated file
 105       * @param integer $generatedColumn The column number in generated file
 106       * @param integer $originalLine    The line number in original file
 107       * @param integer $originalColumn  The column number in original file
 108       * @param string  $sourceFile      The original source file
 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      }
 122  
 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      {
 134          $file = $this->options['sourceMapWriteTo'];
 135          $dir  = dirname($file);
 136  
 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!
 146          if (file_put_contents($file, $content) === false) {
 147              throw new CompilerException(sprintf('Cannot save the source map to "%s"', $file));
 148          }
 149  
 150          return $this->options['sourceMapURL'];
 151      }
 152  
 153      /**
 154       * Generates the JSON source map
 155       *
 156       * @return string
 157       *
 158       * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#
 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.
 166          $sourceMap['version'] = self::VERSION;
 167  
 168          // An optional name of the generated code that this source map is associated with.
 169          $file = $this->options['sourceMapFilename'];
 170  
 171          if ($file) {
 172              $sourceMap['file'] = $file;
 173          }
 174  
 175          // An optional source root, useful for relocating source files on a server or removing repeated values in the
 176          // 'sources' entry. This value is prepended to the individual entries in the 'source' field.
 177          $root = $this->options['sourceRoot'];
 178  
 179          if ($root) {
 180              $sourceMap['sourceRoot'] = $root;
 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;
 195  
 196          if ($this->options['outputSourceFiles']) {
 197              // An optional list of source content, useful when the 'source' can't be hosted.
 198              // The contents are listed in the same order as the sources above.
 199              // 'null' may be used if some original sources should be retrieved by name.
 200              $sourceMap['sourcesContent'] = $this->getSourcesContent();
 201          }
 202  
 203          // less.js compat fixes
 204          if (count($sourceMap['sources']) && empty($sourceMap['sourceRoot'])) {
 205              unset($sourceMap['sourceRoot']);
 206          }
 207  
 208          return json_encode($sourceMap, JSON_UNESCAPED_SLASHES);
 209      }
 210  
 211      /**
 212       * Returns the sources contents
 213       *
 214       * @return array|null
 215       */
 216      protected function getSourcesContent()
 217      {
 218          if (empty($this->sources)) {
 219              return null;
 220          }
 221  
 222          $content = [];
 223  
 224          foreach ($this->sources as $sourceFile) {
 225              $content[] = file_get_contents($sourceFile);
 226          }
 227  
 228          return $content;
 229      }
 230  
 231      /**
 232       * Generates the mappings string
 233       *
 234       * @return string
 235       */
 236      public function generateMappings()
 237      {
 238          if (! count($this->mappings)) {
 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;
 249          }
 250  
 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  
 266                  // find the index
 267                  if ($m['source_file']) {
 268                      $index = $this->findFileIndex($m['source_file']);
 269  
 270                      if ($index !== false) {
 271                          $mapEncoded .= $this->encoder->encode($index - $lastOriginalIndex);
 272                          $lastOriginalIndex = $index;
 273                          // lines are stored 0-based in SourceMap spec version 3
 274                          $mapEncoded .= $this->encoder->encode($m['original_line'] - 1 - $lastOriginalLine);
 275                          $lastOriginalLine = $m['original_line'] - 1;
 276                          $mapEncoded .= $this->encoder->encode($m['original_column'] - $lastOriginalColumn);
 277                          $lastOriginalColumn = $m['original_column'];
 278                      }
 279                  }
 280  
 281                  $lineMapEncoded[] = $mapEncoded;
 282              }
 283  
 284              $groupedMapEncoded[] = implode(',', $lineMapEncoded) . ';';
 285          }
 286  
 287          return rtrim(implode($groupedMapEncoded), ';');
 288      }
 289  
 290      /**
 291       * Finds the index for the filename
 292       *
 293       * @param string $filename
 294       *
 295       * @return integer|false
 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);
 312          $rootpath = $this->options['sourceMapRootpath'];
 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  
 320          // Remove extra leading path separators.
 321          if (strpos($filename, '\\') === 0 || strpos($filename, '/') === 0) {
 322              $filename = substr($filename, 1);
 323          }
 324  
 325          return $rootpath . $filename;
 326      }
 327  
 328      /**
 329       * Fix windows paths
 330       *
 331       * @param string  $path
 332       * @param boolean $addEndSlash
 333       *
 334       * @return string
 335       */
 336      public function fixWindowsPath($path, $addEndSlash = false)
 337      {
 338          $slash = ($addEndSlash) ? '/' : '';
 339  
 340          if (! empty($path)) {
 341              $path = str_replace('\\', '/', $path);
 342              $path = rtrim($path, '/') . $slash;
 343          }
 344  
 345          return $path;
 346      }
 347  }