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