Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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_replace($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|null
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
        assert($file !== null);
152
        $dir  = \dirname($file);
153
 
154
        // directory does not exist
155
        if (! is_dir($dir)) {
156
            // FIXME: create the dir automatically?
157
            throw new CompilerException(
158
                sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir)
159
            );
160
        }
161
 
162
        // FIXME: proper saving, with dir write check!
163
        if (file_put_contents($file, $content) === false) {
164
            throw new CompilerException(sprintf('Cannot save the source map to "%s"', $file));
165
        }
166
 
167
        return $this->options['sourceMapURL'];
168
    }
169
 
170
    /**
171
     * Generates the JSON source map
172
     *
173
     * @param string $prefix A prefix added in the output file, which needs to shift mappings
174
     *
175
     * @return string
176
     *
177
     * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#
178
     */
179
    public function generateJson($prefix = '')
180
    {
181
        $sourceMap = [];
182
        $mappings  = $this->generateMappings($prefix);
183
 
184
        // File version (always the first entry in the object) and must be a positive integer.
185
        $sourceMap['version'] = self::VERSION;
186
 
187
        // An optional name of the generated code that this source map is associated with.
188
        $file = $this->options['sourceMapFilename'];
189
 
190
        if ($file) {
191
            $sourceMap['file'] = $file;
192
        }
193
 
194
        // An optional source root, useful for relocating source files on a server or removing repeated values in the
195
        // 'sources' entry. This value is prepended to the individual entries in the 'source' field.
196
        $root = $this->options['sourceRoot'];
197
 
198
        if ($root) {
199
            $sourceMap['sourceRoot'] = $root;
200
        }
201
 
202
        // A list of original sources used by the 'mappings' entry.
203
        $sourceMap['sources'] = [];
204
 
205
        foreach ($this->sources as $sourceFilename) {
206
            $sourceMap['sources'][] = $this->normalizeFilename($sourceFilename);
207
        }
208
 
209
        // A list of symbol names used by the 'mappings' entry.
210
        $sourceMap['names'] = [];
211
 
212
        // A string with the encoded mapping data.
213
        $sourceMap['mappings'] = $mappings;
214
 
215
        if ($this->options['outputSourceFiles']) {
216
            // An optional list of source content, useful when the 'source' can't be hosted.
217
            // The contents are listed in the same order as the sources above.
218
            // 'null' may be used if some original sources should be retrieved by name.
219
            $sourceMap['sourcesContent'] = $this->getSourcesContent();
220
        }
221
 
222
        // less.js compat fixes
223
        if (\count($sourceMap['sources']) && empty($sourceMap['sourceRoot'])) {
224
            unset($sourceMap['sourceRoot']);
225
        }
226
 
227
        $jsonSourceMap = json_encode($sourceMap, JSON_UNESCAPED_SLASHES);
228
 
229
        if (json_last_error() !== JSON_ERROR_NONE) {
230
            throw new \RuntimeException(json_last_error_msg());
231
        }
232
 
233
        assert($jsonSourceMap !== false);
234
 
235
        return $jsonSourceMap;
236
    }
237
 
238
    /**
239
     * Returns the sources contents
240
     *
241
     * @return string[]|null
242
     */
243
    protected function getSourcesContent()
244
    {
245
        if (empty($this->sources)) {
246
            return null;
247
        }
248
 
249
        $content = [];
250
 
251
        foreach ($this->sources as $sourceFile) {
252
            $content[] = file_get_contents($sourceFile);
253
        }
254
 
255
        return $content;
256
    }
257
 
258
    /**
259
     * Generates the mappings string
260
     *
261
     * @param string $prefix A prefix added in the output file, which needs to shift mappings
262
     *
263
     * @return string
264
     */
265
    public function generateMappings($prefix = '')
266
    {
267
        if (! \count($this->mappings)) {
268
            return '';
269
        }
270
 
271
        $prefixLines = substr_count($prefix, "\n");
272
        $lastPrefixNewLine = strrpos($prefix, "\n");
273
        $lastPrefixLineStart = false === $lastPrefixNewLine ? 0 : $lastPrefixNewLine + 1;
274
        $prefixColumn = strlen($prefix) - $lastPrefixLineStart;
275
 
276
        $this->sourceKeys = array_flip(array_keys($this->sources));
277
 
278
        // group mappings by generated line number.
279
        $groupedMap = $groupedMapEncoded = [];
280
 
281
        foreach ($this->mappings as $m) {
282
            $groupedMap[$m['generated_line']][] = $m;
283
        }
284
 
285
        ksort($groupedMap);
286
 
287
        $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
288
 
289
        foreach ($groupedMap as $lineNumber => $lineMap) {
290
            if ($lineNumber > 1) {
291
                // The prefix only impacts the column for the first line of the original output
292
                $prefixColumn = 0;
293
            }
294
            $lineNumber += $prefixLines;
295
 
296
            while (++$lastGeneratedLine < $lineNumber) {
297
                $groupedMapEncoded[] = ';';
298
            }
299
 
300
            $lineMapEncoded = [];
301
            $lastGeneratedColumn = 0;
302
 
303
            foreach ($lineMap as $m) {
304
                $generatedColumn = $m['generated_column'] + $prefixColumn;
305
 
306
                $mapEncoded = $this->encoder->encode($generatedColumn - $lastGeneratedColumn);
307
                $lastGeneratedColumn = $generatedColumn;
308
 
309
                // find the index
310
                if ($m['source_file']) {
311
                    $index = $this->findFileIndex($m['source_file']);
312
 
313
                    if ($index !== false) {
314
                        $mapEncoded .= $this->encoder->encode($index - $lastOriginalIndex);
315
                        $lastOriginalIndex = $index;
316
                        // lines are stored 0-based in SourceMap spec version 3
317
                        $mapEncoded .= $this->encoder->encode($m['original_line'] - 1 - $lastOriginalLine);
318
                        $lastOriginalLine = $m['original_line'] - 1;
319
                        $mapEncoded .= $this->encoder->encode($m['original_column'] - $lastOriginalColumn);
320
                        $lastOriginalColumn = $m['original_column'];
321
                    }
322
                }
323
 
324
                $lineMapEncoded[] = $mapEncoded;
325
            }
326
 
327
            $groupedMapEncoded[] = implode(',', $lineMapEncoded) . ';';
328
        }
329
 
330
        return rtrim(implode($groupedMapEncoded), ';');
331
    }
332
 
333
    /**
334
     * Finds the index for the filename
335
     *
336
     * @param string $filename
337
     *
338
     * @return int|false
339
     */
340
    protected function findFileIndex($filename)
341
    {
342
        return $this->sourceKeys[$filename];
343
    }
344
 
345
    /**
346
     * Normalize filename
347
     *
348
     * @param string $filename
349
     *
350
     * @return string
351
     */
352
    protected function normalizeFilename($filename)
353
    {
354
        $filename = $this->fixWindowsPath($filename);
355
        $rootpath = $this->options['sourceMapRootpath'];
356
        $basePath = $this->options['sourceMapBasepath'];
357
 
358
        // "Trim" the 'sourceMapBasepath' from the output filename.
359
        if (\strlen($basePath) && strpos($filename, $basePath) === 0) {
360
            $filename = substr($filename, \strlen($basePath));
361
        }
362
 
363
        // Remove extra leading path separators.
364
        if (strpos($filename, '\\') === 0 || strpos($filename, '/') === 0) {
365
            $filename = substr($filename, 1);
366
        }
367
 
368
        return $rootpath . $filename;
369
    }
370
 
371
    /**
372
     * Fix windows paths
373
     *
374
     * @param string $path
375
     * @param bool   $addEndSlash
376
     *
377
     * @return string
378
     */
379
    public function fixWindowsPath($path, $addEndSlash = false)
380
    {
381
        $slash = ($addEndSlash) ? '/' : '';
382
 
383
        if (! empty($path)) {
384
            $path = str_replace('\\', '/', $path);
385
            $path = rtrim($path, '/') . $slash;
386
        }
387
 
388
        return $path;
389
    }
390
}