5 * @copyright 2012-2018 Leaf Corcoran
7 * @license http://opensource.org/licenses/MIT MIT
9 * @link http://leafo.github.io/scssphp
12 namespace Leafo\ScssPhp\SourceMap
;
14 use Leafo\ScssPhp\Exception\CompilerException
;
17 * Source Map Generator
19 * {@internal Derivative of oyejorge/less.php's lib/SourceMap/Generator.php, relicensed with permission. }}
21 * @author Josh Schmidt <oyejorge@gmail.com>
22 * @author Nicolas FRANÇOIS <nicolas.francois@frog-labs.com>
24 class SourceMapGenerator
27 * What version of source map does the generator generate?
32 * Array of default options
36 protected $defaultOptions = [
37 // an optional source root, useful for relocating source files
38 // on a server or removing repeated values in the 'sources' entry.
39 // This value is prepended to the individual entries in the 'source' field.
42 // an optional name of the generated code that this source map is associated with.
43 'sourceMapFilename' => null,
46 'sourceMapURL' => null,
48 // absolute path to a file to write the map to
49 'sourceMapWriteTo' => null,
51 // output source contents?
52 'outputSourceFiles' => false,
54 // base path for filename normalization
55 'sourceMapRootpath' => '',
57 // base path for filename normalization
58 'sourceMapBasepath' => ''
62 * The base64 VLQ encoder
64 * @var \Leafo\ScssPhp\SourceMap\Base64VLQ
73 protected $mappings = [];
76 * Array of contents map
80 protected $contentsMap = [];
87 protected $sources = [];
88 protected $source_keys = [];
95 public function __construct(array $options = [])
97 $this->options
= array_merge($this->defaultOptions
, $options);
98 $this->encoder
= new Base64VLQ();
104 * @param integer $generatedLine The line number in generated file
105 * @param integer $generatedColumn The column number in generated file
106 * @param integer $originalLine The line number in original file
107 * @param integer $originalColumn The column number in original file
108 * @param string $sourceFile The original source file
110 public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $sourceFile)
112 $this->mappings
[] = [
113 'generated_line' => $generatedLine,
114 'generated_column' => $generatedColumn,
115 'original_line' => $originalLine,
116 'original_column' => $originalColumn,
117 'source_file' => $sourceFile
120 $this->sources
[$sourceFile] = $sourceFile;
124 * Saves the source map to a file
126 * @param string $file The absolute path to a file
127 * @param string $content The content to write
129 * @throws \Leafo\ScssPhp\Exception\CompilerException If the file could not be saved
131 public function saveMap($content)
133 $file = $this->options
['sourceMapWriteTo'];
134 $dir = dirname($file);
136 // directory does not exist
137 if (! is_dir($dir)) {
138 // FIXME: create the dir automatically?
139 throw new CompilerException(sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir));
142 // FIXME: proper saving, with dir write check!
143 if (file_put_contents($file, $content) === false) {
144 throw new CompilerException(sprintf('Cannot save the source map to "%s"', $file));
147 return $this->options
['sourceMapURL'];
151 * Generates the JSON source map
155 * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#
157 public function generateJson()
160 $mappings = $this->generateMappings();
162 // File version (always the first entry in the object) and must be a positive integer.
163 $sourceMap['version'] = self
::VERSION
;
165 // An optional name of the generated code that this source map is associated with.
166 $file = $this->options
['sourceMapFilename'];
169 $sourceMap['file'] = $file;
172 // An optional source root, useful for relocating source files on a server or removing repeated values in the
173 // 'sources' entry. This value is prepended to the individual entries in the 'source' field.
174 $root = $this->options
['sourceRoot'];
177 $sourceMap['sourceRoot'] = $root;
180 // A list of original sources used by the 'mappings' entry.
181 $sourceMap['sources'] = [];
183 foreach ($this->sources
as $source_uri => $source_filename) {
184 $sourceMap['sources'][] = $this->normalizeFilename($source_filename);
187 // A list of symbol names used by the 'mappings' entry.
188 $sourceMap['names'] = [];
190 // A string with the encoded mapping data.
191 $sourceMap['mappings'] = $mappings;
193 if ($this->options
['outputSourceFiles']) {
194 // An optional list of source content, useful when the 'source' can't be hosted.
195 // The contents are listed in the same order as the sources above.
196 // 'null' may be used if some original sources should be retrieved by name.
197 $sourceMap['sourcesContent'] = $this->getSourcesContent();
200 // less.js compat fixes
201 if (count($sourceMap['sources']) && empty($sourceMap['sourceRoot'])) {
202 unset($sourceMap['sourceRoot']);
205 return json_encode($sourceMap, JSON_UNESCAPED_SLASHES
);
209 * Returns the sources contents
213 protected function getSourcesContent()
215 if (empty($this->sources
)) {
221 foreach ($this->sources
as $sourceFile) {
222 $content[] = file_get_contents($sourceFile);
229 * Generates the mappings string
233 public function generateMappings()
235 if (! count($this->mappings
)) {
239 $this->source_keys
= array_flip(array_keys($this->sources
));
241 // group mappings by generated line number.
242 $groupedMap = $groupedMapEncoded = [];
244 foreach ($this->mappings
as $m) {
245 $groupedMap[$m['generated_line']][] = $m;
249 $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
251 foreach ($groupedMap as $lineNumber => $line_map) {
252 while (++
$lastGeneratedLine < $lineNumber) {
253 $groupedMapEncoded[] = ';';
256 $lineMapEncoded = [];
257 $lastGeneratedColumn = 0;
259 foreach ($line_map as $m) {
260 $mapEncoded = $this->encoder
->encode($m['generated_column'] - $lastGeneratedColumn);
261 $lastGeneratedColumn = $m['generated_column'];
264 if ($m['source_file']) {
265 $index = $this->findFileIndex($m['source_file']);
267 if ($index !== false) {
268 $mapEncoded .= $this->encoder
->encode($index - $lastOriginalIndex);
269 $lastOriginalIndex = $index;
270 // lines are stored 0-based in SourceMap spec version 3
271 $mapEncoded .= $this->encoder
->encode($m['original_line'] - 1 - $lastOriginalLine);
272 $lastOriginalLine = $m['original_line'] - 1;
273 $mapEncoded .= $this->encoder
->encode($m['original_column'] - $lastOriginalColumn);
274 $lastOriginalColumn = $m['original_column'];
278 $lineMapEncoded[] = $mapEncoded;
281 $groupedMapEncoded[] = implode(',', $lineMapEncoded) . ';';
284 return rtrim(implode($groupedMapEncoded), ';');
288 * Finds the index for the filename
290 * @param string $filename
292 * @return integer|false
294 protected function findFileIndex($filename)
296 return $this->source_keys
[$filename];
299 protected function normalizeFilename($filename)
301 $filename = $this->fixWindowsPath($filename);
302 $rootpath = $this->options
['sourceMapRootpath'];
303 $basePath = $this->options
['sourceMapBasepath'];
305 // "Trim" the 'sourceMapBasepath' from the output filename.
306 if (strlen($basePath) && strpos($filename, $basePath) === 0) {
307 $filename = substr($filename, strlen($basePath));
310 // Remove extra leading path separators.
311 if (strpos($filename, '\\') === 0 ||
strpos($filename, '/') === 0) {
312 $filename = substr($filename, 1);
315 return $rootpath . $filename;
321 * @param string $path
322 * @param boolean $addEndSlash
326 public function fixWindowsPath($path, $addEndSlash = false)
328 $slash = ($addEndSlash) ?
'/' : '';
330 if (! empty($path)) {
331 $path = str_replace('\\', '/', $path);
332 $path = rtrim($path, '/') . $slash;