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