Merge branch '3.1' into 5.2
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / api / leafo / scssphp / src / SourceMap / SourceMapGenerator.php
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 <oyejorge@gmail.com>
22 * @author Nicolas FRANÇOIS <nicolas.francois@frog-labs.com>
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 = [
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\Base64VLQ
65 */
66 protected $encoder;
67
68 /**
69 * Array of mappings
70 *
71 * @var array
72 */
73 protected $mappings = [];
74
75 /**
76 * Array of contents map
77 *
78 * @var array
79 */
80 protected $contentsMap = [];
81
82 /**
83 * File to content map
84 *
85 * @var array
86 */
87 protected $sources = [];
88 protected $source_keys = [];
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 Base64VLQ();
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[] = [
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 = [];
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'] = [];
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'] = [];
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, JSON_UNESCAPED_SLASHES);
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 = [];
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 = [];
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 = [];
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 (strlen($basePath) && 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 }