Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   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\SourceMap;
  14  
  15  use ScssPhp\ScssPhp\Exception\CompilerException;
  16  
  17  /**
  18   * Source Map Generator
  19   *
  20   * {@internal Derivative of oyejorge/less.php's lib/SourceMap/Generator.php, relicensed with permission. }}
  21   *
  22   * @author Josh Schmidt <oyejorge@gmail.com>
  23   * @author Nicolas FRANÇOIS <nicolas.francois@frog-labs.com>
  24   */
  25  class SourceMapGenerator
  26  {
  27      /**
  28       * What version of source map does the generator generate?
  29       */
  30      const VERSION = 3;
  31  
  32      /**
  33       * Array of default options
  34       *
  35       * @var array
  36       */
  37      protected $defaultOptions = [
  38          // an optional source root, useful for relocating source files
  39          // on a server or removing repeated values in the 'sources' entry.
  40          // This value is prepended to the individual entries in the 'source' field.
  41          'sourceRoot' => '',
  42  
  43          // an optional name of the generated code that this source map is associated with.
  44          'sourceMapFilename' => null,
  45  
  46          // url of the map
  47          'sourceMapURL' => null,
  48  
  49          // absolute path to a file to write the map to
  50          'sourceMapWriteTo' => null,
  51  
  52          // output source contents?
  53          'outputSourceFiles' => false,
  54  
  55          // base path for filename normalization
  56          'sourceMapRootpath' => '',
  57  
  58          // base path for filename normalization
  59          'sourceMapBasepath' => ''
  60      ];
  61  
  62      /**
  63       * The base64 VLQ encoder
  64       *
  65       * @var \ScssPhp\ScssPhp\SourceMap\Base64VLQ
  66       */
  67      protected $encoder;
  68  
  69      /**
  70       * Array of mappings
  71       *
  72       * @var array
  73       */
  74      protected $mappings = [];
  75  
  76      /**
  77       * Array of contents map
  78       *
  79       * @var array
  80       */
  81      protected $contentsMap = [];
  82  
  83      /**
  84       * File to content map
  85       *
  86       * @var array
  87       */
  88      protected $sources = [];
  89      protected $sourceKeys = [];
  90  
  91      /**
  92       * @var array
  93       */
  94      private $options;
  95  
  96      public function __construct(array $options = [])
  97      {
  98          $this->options = array_merge($this->defaultOptions, $options);
  99          $this->encoder = new Base64VLQ();
 100      }
 101  
 102      /**
 103       * Adds a mapping
 104       *
 105       * @param integer $generatedLine   The line number in generated file
 106       * @param integer $generatedColumn The column number in generated file
 107       * @param integer $originalLine    The line number in original file
 108       * @param integer $originalColumn  The column number in original file
 109       * @param string  $sourceFile      The original source file
 110       */
 111      public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $sourceFile)
 112      {
 113          $this->mappings[] = [
 114              'generated_line'   => $generatedLine,
 115              'generated_column' => $generatedColumn,
 116              'original_line'    => $originalLine,
 117              'original_column'  => $originalColumn,
 118              'source_file'      => $sourceFile
 119          ];
 120  
 121          $this->sources[$sourceFile] = $sourceFile;
 122      }
 123  
 124      /**
 125       * Saves the source map to a file
 126       *
 127       * @param string $content The content to write
 128       *
 129       * @return string
 130       *
 131       * @throws \ScssPhp\ScssPhp\Exception\CompilerException If the file could not be saved
 132       */
 133      public function saveMap($content)
 134      {
 135          $file = $this->options['sourceMapWriteTo'];
 136          $dir  = \dirname($file);
 137  
 138          // directory does not exist
 139          if (! is_dir($dir)) {
 140              // FIXME: create the dir automatically?
 141              throw new CompilerException(
 142                  sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir)
 143              );
 144          }
 145  
 146          // FIXME: proper saving, with dir write check!
 147          if (file_put_contents($file, $content) === false) {
 148              throw new CompilerException(sprintf('Cannot save the source map to "%s"', $file));
 149          }
 150  
 151          return $this->options['sourceMapURL'];
 152      }
 153  
 154      /**
 155       * Generates the JSON source map
 156       *
 157       * @param string $prefix A prefix added in the output file, which needs to shift mappings
 158       *
 159       * @return string
 160       *
 161       * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#
 162       */
 163      public function generateJson($prefix = '')
 164      {
 165          $sourceMap = [];
 166          $mappings  = $this->generateMappings($prefix);
 167  
 168          // File version (always the first entry in the object) and must be a positive integer.
 169          $sourceMap['version'] = self::VERSION;
 170  
 171          // An optional name of the generated code that this source map is associated with.
 172          $file = $this->options['sourceMapFilename'];
 173  
 174          if ($file) {
 175              $sourceMap['file'] = $file;
 176          }
 177  
 178          // An optional source root, useful for relocating source files on a server or removing repeated values in the
 179          // 'sources' entry. This value is prepended to the individual entries in the 'source' field.
 180          $root = $this->options['sourceRoot'];
 181  
 182          if ($root) {
 183              $sourceMap['sourceRoot'] = $root;
 184          }
 185  
 186          // A list of original sources used by the 'mappings' entry.
 187          $sourceMap['sources'] = [];
 188  
 189          foreach ($this->sources as $sourceUri => $sourceFilename) {
 190              $sourceMap['sources'][] = $this->normalizeFilename($sourceFilename);
 191          }
 192  
 193          // A list of symbol names used by the 'mappings' entry.
 194          $sourceMap['names'] = [];
 195  
 196          // A string with the encoded mapping data.
 197          $sourceMap['mappings'] = $mappings;
 198  
 199          if ($this->options['outputSourceFiles']) {
 200              // An optional list of source content, useful when the 'source' can't be hosted.
 201              // The contents are listed in the same order as the sources above.
 202              // 'null' may be used if some original sources should be retrieved by name.
 203              $sourceMap['sourcesContent'] = $this->getSourcesContent();
 204          }
 205  
 206          // less.js compat fixes
 207          if (\count($sourceMap['sources']) && empty($sourceMap['sourceRoot'])) {
 208              unset($sourceMap['sourceRoot']);
 209          }
 210  
 211          return json_encode($sourceMap, JSON_UNESCAPED_SLASHES);
 212      }
 213  
 214      /**
 215       * Returns the sources contents
 216       *
 217       * @return array|null
 218       */
 219      protected function getSourcesContent()
 220      {
 221          if (empty($this->sources)) {
 222              return null;
 223          }
 224  
 225          $content = [];
 226  
 227          foreach ($this->sources as $sourceFile) {
 228              $content[] = file_get_contents($sourceFile);
 229          }
 230  
 231          return $content;
 232      }
 233  
 234      /**
 235       * Generates the mappings string
 236       *
 237       * @param string $prefix A prefix added in the output file, which needs to shift mappings
 238       *
 239       * @return string
 240       */
 241      public function generateMappings($prefix = '')
 242      {
 243          if (! \count($this->mappings)) {
 244              return '';
 245          }
 246  
 247          $prefixLines = substr_count($prefix, "\n");
 248          $lastPrefixNewLine = strrpos($prefix, "\n");
 249          $lastPrefixLineStart = false === $lastPrefixNewLine ? 0 : $lastPrefixNewLine + 1;
 250          $prefixColumn = strlen($prefix) - $lastPrefixLineStart;
 251  
 252          $this->sourceKeys = array_flip(array_keys($this->sources));
 253  
 254          // group mappings by generated line number.
 255          $groupedMap = $groupedMapEncoded = [];
 256  
 257          foreach ($this->mappings as $m) {
 258              $groupedMap[$m['generated_line']][] = $m;
 259          }
 260  
 261          ksort($groupedMap);
 262  
 263          $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
 264  
 265          foreach ($groupedMap as $lineNumber => $lineMap) {
 266              if ($lineNumber > 1) {
 267                  // The prefix only impacts the column for the first line of the original output
 268                  $prefixColumn = 0;
 269              }
 270              $lineNumber += $prefixLines;
 271  
 272              while (++$lastGeneratedLine < $lineNumber) {
 273                  $groupedMapEncoded[] = ';';
 274              }
 275  
 276              $lineMapEncoded = [];
 277              $lastGeneratedColumn = 0;
 278  
 279              foreach ($lineMap as $m) {
 280                  $generatedColumn = $m['generated_column'] + $prefixColumn;
 281  
 282                  $mapEncoded = $this->encoder->encode($generatedColumn - $lastGeneratedColumn);
 283                  $lastGeneratedColumn = $generatedColumn;
 284  
 285                  // find the index
 286                  if ($m['source_file']) {
 287                      $index = $this->findFileIndex($m['source_file']);
 288  
 289                      if ($index !== false) {
 290                          $mapEncoded .= $this->encoder->encode($index - $lastOriginalIndex);
 291                          $lastOriginalIndex = $index;
 292                          // lines are stored 0-based in SourceMap spec version 3
 293                          $mapEncoded .= $this->encoder->encode($m['original_line'] - 1 - $lastOriginalLine);
 294                          $lastOriginalLine = $m['original_line'] - 1;
 295                          $mapEncoded .= $this->encoder->encode($m['original_column'] - $lastOriginalColumn);
 296                          $lastOriginalColumn = $m['original_column'];
 297                      }
 298                  }
 299  
 300                  $lineMapEncoded[] = $mapEncoded;
 301              }
 302  
 303              $groupedMapEncoded[] = implode(',', $lineMapEncoded) . ';';
 304          }
 305  
 306          return rtrim(implode($groupedMapEncoded), ';');
 307      }
 308  
 309      /**
 310       * Finds the index for the filename
 311       *
 312       * @param string $filename
 313       *
 314       * @return integer|false
 315       */
 316      protected function findFileIndex($filename)
 317      {
 318          return $this->sourceKeys[$filename];
 319      }
 320  
 321      /**
 322       * Normalize filename
 323       *
 324       * @param string $filename
 325       *
 326       * @return string
 327       */
 328      protected function normalizeFilename($filename)
 329      {
 330          $filename = $this->fixWindowsPath($filename);
 331          $rootpath = $this->options['sourceMapRootpath'];
 332          $basePath = $this->options['sourceMapBasepath'];
 333  
 334          // "Trim" the 'sourceMapBasepath' from the output filename.
 335          if (\strlen($basePath) && strpos($filename, $basePath) === 0) {
 336              $filename = substr($filename, \strlen($basePath));
 337          }
 338  
 339          // Remove extra leading path separators.
 340          if (strpos($filename, '\\') === 0 || strpos($filename, '/') === 0) {
 341              $filename = substr($filename, 1);
 342          }
 343  
 344          return $rootpath . $filename;
 345      }
 346  
 347      /**
 348       * Fix windows paths
 349       *
 350       * @param string  $path
 351       * @param boolean $addEndSlash
 352       *
 353       * @return string
 354       */
 355      public function fixWindowsPath($path, $addEndSlash = false)
 356      {
 357          $slash = ($addEndSlash) ? '/' : '';
 358  
 359          if (! empty($path)) {
 360              $path = str_replace('\\', '/', $path);
 361              $path = rtrim($path, '/') . $slash;
 362          }
 363  
 364          return $path;
 365      }
 366  }