Passed
Push — master ( d4a329...711fef )
by Jeroen De
03:36
created

SourceMapGenerator::getSourcesContent()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 6
c 1
b 0
f 0
nc 3
nop 0
dl 0
loc 13
rs 10
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 <[email protected]>
23
 * @author Nicolas FRANÇOIS <[email protected]>
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
     * @return string
158
     *
159
     * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#
160
     */
161
    public function generateJson()
162
    {
163
        $sourceMap = [];
164
        $mappings  = $this->generateMappings();
165
166
        // File version (always the first entry in the object) and must be a positive integer.
167
        $sourceMap['version'] = self::VERSION;
168
169
        // An optional name of the generated code that this source map is associated with.
170
        $file = $this->options['sourceMapFilename'];
171
172
        if ($file) {
173
            $sourceMap['file'] = $file;
174
        }
175
176
        // An optional source root, useful for relocating source files on a server or removing repeated values in the
177
        // 'sources' entry. This value is prepended to the individual entries in the 'source' field.
178
        $root = $this->options['sourceRoot'];
179
180
        if ($root) {
181
            $sourceMap['sourceRoot'] = $root;
182
        }
183
184
        // A list of original sources used by the 'mappings' entry.
185
        $sourceMap['sources'] = [];
186
187
        foreach ($this->sources as $sourceUri => $sourceFilename) {
188
            $sourceMap['sources'][] = $this->normalizeFilename($sourceFilename);
189
        }
190
191
        // A list of symbol names used by the 'mappings' entry.
192
        $sourceMap['names'] = [];
193
194
        // A string with the encoded mapping data.
195
        $sourceMap['mappings'] = $mappings;
196
197
        if ($this->options['outputSourceFiles']) {
198
            // An optional list of source content, useful when the 'source' can't be hosted.
199
            // The contents are listed in the same order as the sources above.
200
            // 'null' may be used if some original sources should be retrieved by name.
201
            $sourceMap['sourcesContent'] = $this->getSourcesContent();
202
        }
203
204
        // less.js compat fixes
205
        if (\count($sourceMap['sources']) && empty($sourceMap['sourceRoot'])) {
206
            unset($sourceMap['sourceRoot']);
207
        }
208
209
        return json_encode($sourceMap, JSON_UNESCAPED_SLASHES);
210
    }
211
212
    /**
213
     * Returns the sources contents
214
     *
215
     * @return array|null
216
     */
217
    protected function getSourcesContent()
218
    {
219
        if (empty($this->sources)) {
220
            return null;
221
        }
222
223
        $content = [];
224
225
        foreach ($this->sources as $sourceFile) {
226
            $content[] = file_get_contents($sourceFile);
227
        }
228
229
        return $content;
230
    }
231
232
    /**
233
     * Generates the mappings string
234
     *
235
     * @return string
236
     */
237
    public function generateMappings()
238
    {
239
        if (! \count($this->mappings)) {
240
            return '';
241
        }
242
243
        $this->sourceKeys = array_flip(array_keys($this->sources));
244
245
        // group mappings by generated line number.
246
        $groupedMap = $groupedMapEncoded = [];
247
248
        foreach ($this->mappings as $m) {
249
            $groupedMap[$m['generated_line']][] = $m;
250
        }
251
252
        ksort($groupedMap);
253
254
        $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
255
256
        foreach ($groupedMap as $lineNumber => $lineMap) {
257
            while (++$lastGeneratedLine < $lineNumber) {
258
                $groupedMapEncoded[] = ';';
259
            }
260
261
            $lineMapEncoded = [];
262
            $lastGeneratedColumn = 0;
263
264
            foreach ($lineMap as $m) {
265
                $mapEncoded = $this->encoder->encode($m['generated_column'] - $lastGeneratedColumn);
266
                $lastGeneratedColumn = $m['generated_column'];
267
268
                // find the index
269
                if ($m['source_file']) {
270
                    $index = $this->findFileIndex($m['source_file']);
271
272
                    if ($index !== false) {
273
                        $mapEncoded .= $this->encoder->encode($index - $lastOriginalIndex);
274
                        $lastOriginalIndex = $index;
275
                        // lines are stored 0-based in SourceMap spec version 3
276
                        $mapEncoded .= $this->encoder->encode($m['original_line'] - 1 - $lastOriginalLine);
277
                        $lastOriginalLine = $m['original_line'] - 1;
278
                        $mapEncoded .= $this->encoder->encode($m['original_column'] - $lastOriginalColumn);
279
                        $lastOriginalColumn = $m['original_column'];
280
                    }
281
                }
282
283
                $lineMapEncoded[] = $mapEncoded;
284
            }
285
286
            $groupedMapEncoded[] = implode(',', $lineMapEncoded) . ';';
287
        }
288
289
        return rtrim(implode($groupedMapEncoded), ';');
290
    }
291
292
    /**
293
     * Finds the index for the filename
294
     *
295
     * @param string $filename
296
     *
297
     * @return integer|false
298
     */
299
    protected function findFileIndex($filename)
300
    {
301
        return $this->sourceKeys[$filename];
302
    }
303
304
    /**
305
     * Normalize filename
306
     *
307
     * @param string $filename
308
     *
309
     * @return string
310
     */
311
    protected function normalizeFilename($filename)
312
    {
313
        $filename = $this->fixWindowsPath($filename);
314
        $rootpath = $this->options['sourceMapRootpath'];
315
        $basePath = $this->options['sourceMapBasepath'];
316
317
        // "Trim" the 'sourceMapBasepath' from the output filename.
318
        if (\strlen($basePath) && strpos($filename, $basePath) === 0) {
319
            $filename = substr($filename, \strlen($basePath));
320
        }
321
322
        // Remove extra leading path separators.
323
        if (strpos($filename, '\\') === 0 || strpos($filename, '/') === 0) {
324
            $filename = substr($filename, 1);
325
        }
326
327
        return $rootpath . $filename;
328
    }
329
330
    /**
331
     * Fix windows paths
332
     *
333
     * @param string  $path
334
     * @param boolean $addEndSlash
335
     *
336
     * @return string
337
     */
338
    public function fixWindowsPath($path, $addEndSlash = false)
339
    {
340
        $slash = ($addEndSlash) ? '/' : '';
341
342
        if (! empty($path)) {
343
            $path = str_replace('\\', '/', $path);
344
            $path = rtrim($path, '/') . $slash;
345
        }
346
347
        return $path;
348
    }
349
}
350