Passed
Push — add/6 ( 1ceb7c...3ff08f )
by
unknown
09:22 queued 04:37
created

SourceMapGenerator   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 322
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 100
dl 0
loc 322
rs 9.84
c 0
b 0
f 0
wmc 32

9 Methods

Rating   Name   Duplication   Size   Complexity  
A normalizeFilename() 0 17 5
B generateMappings() 0 52 8
A addMapping() 0 11 1
A findFileIndex() 0 3 1
A saveMap() 0 19 3
A getSourcesContent() 0 13 3
A fixWindowsPath() 0 10 3
B generateJson() 0 49 7
A __construct() 0 4 1
1
<?php
2
/**
3
 * SCSSPHP
4
 *
5
 * @copyright 2012-2018 Leaf Corcoran
6
 *
7
 * @license http://opensource.org/licenses/MIT MIT
8
 *
9
 * @link http://leafo.github.io/scssphp
10
 */
11
12
namespace Leafo\ScssPhp\SourceMap;
13
14
use Leafo\ScssPhp\Exception\CompilerException;
15
16
/**
17
 * Source Map Generator
18
 *
19
 * {@internal Derivative of oyejorge/less.php's lib/SourceMap/Generator.php, relicensed with permission. }}
20
 *
21
 * @author Josh Schmidt <[email protected]>
22
 * @author Nicolas FRANÇOIS <[email protected]>
23
 */
24
class SourceMapGenerator
25
{
26
    /**
27
     * What version of source map does the generator generate?
28
     */
29
    const VERSION = 3;
30
31
    /**
32
     * Array of default options
33
     *
34
     * @var array
35
     */
36
    protected $defaultOptions = array(
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.
40
        'sourceRoot' => '',
41
42
        // an optional name of the generated code that this source map is associated with.
43
        'sourceMapFilename' => null,
44
45
        // url of the map
46
        'sourceMapURL' => null,
47
48
        // absolute path to a file to write the map to
49
        'sourceMapWriteTo' => null,
50
51
        // output source contents?
52
        'outputSourceFiles' => false,
53
54
        // base path for filename normalization
55
        'sourceMapRootpath' => '',
56
57
        // base path for filename normalization
58
        'sourceMapBasepath' => ''
59
    );
60
61
    /**
62
     * The base64 VLQ encoder
63
     *
64
     * @var \Leafo\ScssPhp\SourceMap\Base64VLQEncoder
65
     */
66
    protected $encoder;
67
68
    /**
69
     * Array of mappings
70
     *
71
     * @var array
72
     */
73
    protected $mappings = array();
74
75
    /**
76
     * Array of contents map
77
     *
78
     * @var array
79
     */
80
    protected $contentsMap = array();
81
82
    /**
83
     * File to content map
84
     *
85
     * @var array
86
     */
87
    protected $sources = array();
88
    protected $source_keys = array();
89
90
    /**
91
     * @var array
92
     */
93
    private $options;
94
95
    public function __construct(array $options = [])
96
    {
97
        $this->options = array_merge($this->defaultOptions, $options);
98
        $this->encoder = new Base64VLQEncoder();
99
    }
100
101
    /**
102
     * Adds a mapping
103
     *
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
109
     */
110
    public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $sourceFile)
111
    {
112
        $this->mappings[] = array(
113
            'generated_line' => $generatedLine,
114
            'generated_column' => $generatedColumn,
115
            'original_line' => $originalLine,
116
            'original_column' => $originalColumn,
117
            'source_file' => $sourceFile
118
        );
119
120
        $this->sources[$sourceFile] = $sourceFile;
121
    }
122
123
    /**
124
     * Saves the source map to a file
125
     *
126
     * @param string $file    The absolute path to a file
127
     * @param string $content The content to write
128
     *
129
     * @throws \Leafo\ScssPhp\Exception\CompilerException If the file could not be saved
130
     */
131
    public function saveMap($content)
132
    {
133
        $file = $this->options['sourceMapWriteTo'];
134
        $dir  = dirname($file);
135
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));
140
        }
141
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));
145
        }
146
147
        return $this->options['sourceMapURL'];
148
    }
149
150
    /**
151
     * Generates the JSON source map
152
     *
153
     * @return string
154
     *
155
     * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#
156
     */
157
    public function generateJson()
158
    {
159
        $sourceMap = array();
160
        $mappings  = $this->generateMappings();
161
162
        // File version (always the first entry in the object) and must be a positive integer.
163
        $sourceMap['version'] = self::VERSION;
164
165
        // An optional name of the generated code that this source map is associated with.
166
        $file = $this->options['sourceMapFilename'];
167
168
        if ($file) {
169
            $sourceMap['file'] = $file;
170
        }
171
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'];
175
176
        if ($root) {
177
            $sourceMap['sourceRoot'] = $root;
178
        }
179
180
        // A list of original sources used by the 'mappings' entry.
181
        $sourceMap['sources'] = array();
182
183
        foreach ($this->sources as $source_uri => $source_filename) {
184
            $sourceMap['sources'][] = $this->normalizeFilename($source_filename);
185
        }
186
187
        // A list of symbol names used by the 'mappings' entry.
188
        $sourceMap['names'] = array();
189
190
        // A string with the encoded mapping data.
191
        $sourceMap['mappings'] = $mappings;
192
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();
198
        }
199
200
        // less.js compat fixes
201
        if (count($sourceMap['sources']) && empty($sourceMap['sourceRoot'])) {
202
            unset($sourceMap['sourceRoot']);
203
        }
204
205
        return json_encode($sourceMap);
206
    }
207
208
    /**
209
     * Returns the sources contents
210
     *
211
     * @return array|null
212
     */
213
    protected function getSourcesContent()
214
    {
215
        if (empty($this->sources)) {
216
            return null;
217
        }
218
219
        $content = array();
220
221
        foreach ($this->sources as $sourceFile) {
222
            $content[] = file_get_contents($sourceFile);
223
        }
224
225
        return $content;
226
    }
227
228
    /**
229
     * Generates the mappings string
230
     *
231
     * @return string
232
     */
233
    public function generateMappings()
234
    {
235
        if (! count($this->mappings)) {
236
            return '';
237
        }
238
239
        $this->source_keys = array_flip(array_keys($this->sources));
240
241
        // group mappings by generated line number.
242
        $groupedMap = $groupedMapEncoded = array();
243
244
        foreach ($this->mappings as $m) {
245
            $groupedMap[$m['generated_line']][] = $m;
246
        }
247
248
        ksort($groupedMap);
249
        $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
250
251
        foreach ($groupedMap as $lineNumber => $line_map) {
252
            while (++$lastGeneratedLine < $lineNumber) {
253
                $groupedMapEncoded[] = ';';
254
            }
255
256
            $lineMapEncoded = array();
257
            $lastGeneratedColumn = 0;
258
259
            foreach ($line_map as $m) {
260
                $mapEncoded = $this->encoder->encode($m['generated_column'] - $lastGeneratedColumn);
261
                $lastGeneratedColumn = $m['generated_column'];
262
263
                // find the index
264
                if ($m['source_file']) {
265
                    $index = $this->findFileIndex($m['source_file']);
266
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'];
275
                    }
276
                }
277
278
                $lineMapEncoded[] = $mapEncoded;
279
            }
280
281
            $groupedMapEncoded[] = implode(',', $lineMapEncoded) . ';';
282
        }
283
284
        return rtrim(implode($groupedMapEncoded), ';');
285
    }
286
287
    /**
288
     * Finds the index for the filename
289
     *
290
     * @param string $filename
291
     *
292
     * @return integer|false
293
     */
294
    protected function findFileIndex($filename)
295
    {
296
        return $this->source_keys[$filename];
297
    }
298
299
    protected function normalizeFilename($filename)
300
    {
301
        $filename = $this->fixWindowsPath($filename);
302
        $rootpath = $this->options['sourceMapRootpath'];
303
        $basePath = $this->options['sourceMapBasepath'];
304
305
        // "Trim" the 'sourceMapBasepath' from the output filename.
306
        if (strpos($filename, $basePath) === 0) {
307
            $filename = substr($filename, strlen($basePath));
308
        }
309
310
        // Remove extra leading path separators.
311
        if (strpos($filename, '\\') === 0 || strpos($filename, '/') === 0) {
312
            $filename = substr($filename, 1);
313
        }
314
315
        return $rootpath . $filename;
316
    }
317
318
    /**
319
     * Fix windows paths
320
     *
321
     * @param string  $path
322
     * @param boolean $addEndSlash
323
     *
324
     * @return string
325
     */
326
    public function fixWindowsPath($path, $addEndSlash = false)
327
    {
328
        $slash = ($addEndSlash) ? '/' : '';
329
330
        if (! empty($path)) {
331
            $path = str_replace('\\', '/', $path);
332
            $path = rtrim($path, '/') . $slash;
333
        }
334
335
        return $path;
336
    }
337
}
338