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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body