See Release Notes
Long Term Support Release
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\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_replace($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|null 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 assert($file !== null); 152 $dir = \dirname($file); 153 154 // directory does not exist 155 if (! is_dir($dir)) { 156 // FIXME: create the dir automatically? 157 throw new CompilerException( 158 sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir) 159 ); 160 } 161 162 // FIXME: proper saving, with dir write check! 163 if (file_put_contents($file, $content) === false) { 164 throw new CompilerException(sprintf('Cannot save the source map to "%s"', $file)); 165 } 166 167 return $this->options['sourceMapURL']; 168 } 169 170 /** 171 * Generates the JSON source map 172 * 173 * @param string $prefix A prefix added in the output file, which needs to shift mappings 174 * 175 * @return string 176 * 177 * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit# 178 */ 179 public function generateJson($prefix = '') 180 { 181 $sourceMap = []; 182 $mappings = $this->generateMappings($prefix); 183 184 // File version (always the first entry in the object) and must be a positive integer. 185 $sourceMap['version'] = self::VERSION; 186 187 // An optional name of the generated code that this source map is associated with. 188 $file = $this->options['sourceMapFilename']; 189 190 if ($file) { 191 $sourceMap['file'] = $file; 192 } 193 194 // An optional source root, useful for relocating source files on a server or removing repeated values in the 195 // 'sources' entry. This value is prepended to the individual entries in the 'source' field. 196 $root = $this->options['sourceRoot']; 197 198 if ($root) { 199 $sourceMap['sourceRoot'] = $root; 200 } 201 202 // A list of original sources used by the 'mappings' entry. 203 $sourceMap['sources'] = []; 204 205 foreach ($this->sources as $sourceFilename) { 206 $sourceMap['sources'][] = $this->normalizeFilename($sourceFilename); 207 } 208 209 // A list of symbol names used by the 'mappings' entry. 210 $sourceMap['names'] = []; 211 212 // A string with the encoded mapping data. 213 $sourceMap['mappings'] = $mappings; 214 215 if ($this->options['outputSourceFiles']) { 216 // An optional list of source content, useful when the 'source' can't be hosted. 217 // The contents are listed in the same order as the sources above. 218 // 'null' may be used if some original sources should be retrieved by name. 219 $sourceMap['sourcesContent'] = $this->getSourcesContent(); 220 } 221 222 // less.js compat fixes 223 if (\count($sourceMap['sources']) && empty($sourceMap['sourceRoot'])) { 224 unset($sourceMap['sourceRoot']); 225 } 226 227 $jsonSourceMap = json_encode($sourceMap, JSON_UNESCAPED_SLASHES); 228 229 if (json_last_error() !== JSON_ERROR_NONE) { 230 throw new \RuntimeException(json_last_error_msg()); 231 } 232 233 assert($jsonSourceMap !== false); 234 235 return $jsonSourceMap; 236 } 237 238 /** 239 * Returns the sources contents 240 * 241 * @return string[]|null 242 */ 243 protected function getSourcesContent() 244 { 245 if (empty($this->sources)) { 246 return null; 247 } 248 249 $content = []; 250 251 foreach ($this->sources as $sourceFile) { 252 $content[] = file_get_contents($sourceFile); 253 } 254 255 return $content; 256 } 257 258 /** 259 * Generates the mappings string 260 * 261 * @param string $prefix A prefix added in the output file, which needs to shift mappings 262 * 263 * @return string 264 */ 265 public function generateMappings($prefix = '') 266 { 267 if (! \count($this->mappings)) { 268 return ''; 269 } 270 271 $prefixLines = substr_count($prefix, "\n"); 272 $lastPrefixNewLine = strrpos($prefix, "\n"); 273 $lastPrefixLineStart = false === $lastPrefixNewLine ? 0 : $lastPrefixNewLine + 1; 274 $prefixColumn = strlen($prefix) - $lastPrefixLineStart; 275 276 $this->sourceKeys = array_flip(array_keys($this->sources)); 277 278 // group mappings by generated line number. 279 $groupedMap = $groupedMapEncoded = []; 280 281 foreach ($this->mappings as $m) { 282 $groupedMap[$m['generated_line']][] = $m; 283 } 284 285 ksort($groupedMap); 286 287 $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0; 288 289 foreach ($groupedMap as $lineNumber => $lineMap) { 290 if ($lineNumber > 1) { 291 // The prefix only impacts the column for the first line of the original output 292 $prefixColumn = 0; 293 } 294 $lineNumber += $prefixLines; 295 296 while (++$lastGeneratedLine < $lineNumber) { 297 $groupedMapEncoded[] = ';'; 298 } 299 300 $lineMapEncoded = []; 301 $lastGeneratedColumn = 0; 302 303 foreach ($lineMap as $m) { 304 $generatedColumn = $m['generated_column'] + $prefixColumn; 305 306 $mapEncoded = $this->encoder->encode($generatedColumn - $lastGeneratedColumn); 307 $lastGeneratedColumn = $generatedColumn; 308 309 // find the index 310 if ($m['source_file']) { 311 $index = $this->findFileIndex($m['source_file']); 312 313 if ($index !== false) { 314 $mapEncoded .= $this->encoder->encode($index - $lastOriginalIndex); 315 $lastOriginalIndex = $index; 316 // lines are stored 0-based in SourceMap spec version 3 317 $mapEncoded .= $this->encoder->encode($m['original_line'] - 1 - $lastOriginalLine); 318 $lastOriginalLine = $m['original_line'] - 1; 319 $mapEncoded .= $this->encoder->encode($m['original_column'] - $lastOriginalColumn); 320 $lastOriginalColumn = $m['original_column']; 321 } 322 } 323 324 $lineMapEncoded[] = $mapEncoded; 325 } 326 327 $groupedMapEncoded[] = implode(',', $lineMapEncoded) . ';'; 328 } 329 330 return rtrim(implode($groupedMapEncoded), ';'); 331 } 332 333 /** 334 * Finds the index for the filename 335 * 336 * @param string $filename 337 * 338 * @return int|false 339 */ 340 protected function findFileIndex($filename) 341 { 342 return $this->sourceKeys[$filename]; 343 } 344 345 /** 346 * Normalize filename 347 * 348 * @param string $filename 349 * 350 * @return string 351 */ 352 protected function normalizeFilename($filename) 353 { 354 $filename = $this->fixWindowsPath($filename); 355 $rootpath = $this->options['sourceMapRootpath']; 356 $basePath = $this->options['sourceMapBasepath']; 357 358 // "Trim" the 'sourceMapBasepath' from the output filename. 359 if (\strlen($basePath) && strpos($filename, $basePath) === 0) { 360 $filename = substr($filename, \strlen($basePath)); 361 } 362 363 // Remove extra leading path separators. 364 if (strpos($filename, '\\') === 0 || strpos($filename, '/') === 0) { 365 $filename = substr($filename, 1); 366 } 367 368 return $rootpath . $filename; 369 } 370 371 /** 372 * Fix windows paths 373 * 374 * @param string $path 375 * @param bool $addEndSlash 376 * 377 * @return string 378 */ 379 public function fixWindowsPath($path, $addEndSlash = false) 380 { 381 $slash = ($addEndSlash) ? '/' : ''; 382 383 if (! empty($path)) { 384 $path = str_replace('\\', '/', $path); 385 $path = rtrim($path, '/') . $slash; 386 } 387 388 return $path; 389 } 390 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body