Generator   B
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 395
Duplicated Lines 0 %

Coupling/Cohesion

Dependencies 6

Importance

Changes 0
Metric Value
wmc 40
cbo 6
dl 0
loc 395
rs 8.2608
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 2
A setup() 0 7 2
C generateCSS() 0 52 9
A saveMap() 0 15 4
A normalizeFilename() 0 15 4
A addMapping() 0 17 1
A clear() 0 6 1
A setEncoder() 0 6 1
A getEncoder() 0 4 1
B generateJson() 0 32 4
A getSourcesContent() 0 9 2
C generateMappings() 0 50 8
A findFileIndex() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Generator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Generator, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the ILess
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
namespace ILess\SourceMap;
11
12
use ILess\Configurable;
13
use ILess\Context;
14
use ILess\Exception\IOException;
15
use ILess\Node\RulesetNode;
16
use InvalidArgumentException;
17
use ILess\Output\MappedOutput;
18
use ILess\Util;
19
20
/**
21
 * Source map generator.
22
 */
23
class Generator extends Configurable
24
{
25
    /**
26
     * What version of source map does the generator generate?
27
     */
28
    const VERSION = 3;
29
30
    /**
31
     * Array of default options.
32
     *
33
     * @var array
34
     */
35
    protected $defaultOptions = [
36
        // an optional source root, useful for relocating source files
37
        // on a server or removing repeated values in the 'sources' entry.
38
        // This value is prepended to the individual entries in the 'source' field.
39
        'sourceRoot' => '',
40
        // an optional name of the generated code that this source map is associated with.
41
        'filename' => null,
42
        // url of the map
43
        'url' => null,
44
        // absolute path to a file to write the map to
45
        'write_to' => null,
46
        // output source contents?
47
        'source_contents' => false,
48
        // base path for filename normalization
49
        'base_path' => '',
50
        // encode inline map using base64?
51
        'inline_encode_base64' => true,
52
    ];
53
54
    /**
55
     * The base64 VLQ encoder.
56
     *
57
     * @var Base64VLQ
58
     */
59
    protected $encoder;
60
61
    /**
62
     * Array of mappings.
63
     *
64
     * @var array
65
     */
66
    protected $mappings = [];
67
68
    /**
69
     * The root node.
70
     *
71
     * @var RulesetNode
72
     */
73
    protected $root;
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
89
    /**
90
     * Constructor.
91
     *
92
     * @param RulesetNode $root The root node
93
     * @param array $contentsMap Array of file contents map
94
     * @param array $options Array of options
95
     * @param Base64VLQ $encoder The encoder
96
     */
97
    public function __construct(
98
        RulesetNode $root,
99
        array $contentsMap,
100
        $options = [],
101
        Base64VLQ $encoder = null
102
    ) {
103
        $this->root = $root;
104
        $this->contentsMap = $contentsMap;
105
        $this->encoder = $encoder ? $encoder : new Base64VLQ();
106
        parent::__construct($options);
107
    }
108
109
    /**
110
     * Setups the generator.
111
     */
112
    protected function setup()
113
    {
114
        // fix windows paths
115
        if ($basePath = $this->getOption('base_path')) {
116
            $this->setOption('base_path', Util::normalizePath($basePath));
117
        }
118
    }
119
120
    /**
121
     * Generates the CSS.
122
     *
123
     * @param Context $context
124
     *
125
     * @return string
126
     */
127
    public function generateCSS(Context $context)
128
    {
129
        $output = new MappedOutput($this->contentsMap, $this);
130
131
        // catch the output
132
        $this->root->generateCSS($context, $output);
133
134
        // prepare sources
135
        foreach ($this->contentsMap as $filename => $contents) {
136
            // match md5 hash in square brackets _[#HASH#]_
137
            // see ILess\Parser\Core::parseString()
138
            if (preg_match('/(\[__[0-9a-f]{32}__\])+$/', $filename)) {
139
                $filename = substr($filename, 0, -38);
140
            }
141
142
            $this->sources[$this->normalizeFilename($filename)] = $contents;
143
        }
144
145
        $sourceMapUrl = null;
146
        if ($url = $this->getOption('url')) {
147
            $sourceMapUrl = $url;
148
        } elseif ($path = $this->getOption('filename')) {
149
            $sourceMapUrl = $this->normalizeFilename($path);
150
            // naming conventions, make it foobar.css.map
151
            if (!preg_match('/\.map$/', $sourceMapUrl)) {
152
                $sourceMapUrl = sprintf('%s.map', $sourceMapUrl);
153
            }
154
        }
155
156
        $sourceMapContent = $this->generateJson();
157
158
        // write map to a file
159
        if ($file = $this->getOption('write_to')) {
160
            $this->saveMap($file, $sourceMapContent);
161
        } // inline the map
162
        else {
163
            $sourceMap = 'data:application/json;';
164
            if ($this->getOption('inline_encode_base64')) {
165
                $sourceMap .= 'base64,';
166
                $sourceMapContent = base64_encode($sourceMapContent);
167
            } else {
168
                $sourceMapContent = Util::encodeURIComponent($sourceMapContent);
169
            }
170
            $sourceMapUrl = $sourceMap . $sourceMapContent;
171
        }
172
173
        if ($sourceMapUrl) {
174
            $output->add(sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl));
175
        }
176
177
        return $output->toString();
178
    }
179
180
    /**
181
     * Saves the source map to a file.
182
     *
183
     * @param string $file The absolute path to a file
184
     * @param string $content The content to write
185
     *
186
     * @throws IOException If the file could not be saved
187
     * @throws InvalidArgumentException If the directory to write the map to does not exist or is not writable
188
     *
189
     * @return true
190
     */
191
    protected function saveMap($file, $content)
192
    {
193
        $dir = dirname($file);
194
195
        if (!is_dir($dir) || !is_writable($dir)) {
196
            throw new InvalidArgumentException(sprintf('The directory "%s" does not exist or is not writable. Cannot save the source map.',
197
                $dir));
198
        }
199
200
        if (@file_put_contents($file, $content, LOCK_EX) === false) {
201
            throw new IOException(sprintf('Cannot save the source map to "%s".', $file));
202
        }
203
204
        return true;
205
    }
206
207
    /**
208
     * Normalizes the filename.
209
     *
210
     * @param string $filename
211
     *
212
     * @return string
213
     */
214
    protected function normalizeFilename($filename)
215
    {
216
        $filename = Util::normalizePath($filename);
217
        if (($basePath = $this->getOption('base_path'))
218
            && ($pos = strpos($filename, $basePath)) !== false
219
        ) {
220
            $filename = substr($filename, $pos + strlen($basePath));
221
222
            if (strpos($filename, '/') === 0) {
223
                $filename = substr($filename, 1);
224
            }
225
        }
226
227
        return $this->getOption('root_path') . $filename;
228
    }
229
230
    /**
231
     * Adds a mapping.
232
     *
233
     * @param int $generatedLine The line number in generated file
234
     * @param int $generatedColumn The column number in generated file
235
     * @param int $originalLine The line number in original file
236
     * @param int $originalColumn The column number in original file
237
     * @param string $sourceFile The original source file
238
     *
239
     * @return Generator
240
     */
241
    public function addMapping(
242
        $generatedLine,
243
        $generatedColumn,
244
        $originalLine,
245
        $originalColumn,
246
        $sourceFile
247
    ) {
248
        $this->mappings[] = [
249
            'generated_line' => $generatedLine,
250
            'generated_column' => $generatedColumn,
251
            'original_line' => $originalLine,
252
            'original_column' => $originalColumn,
253
            'source_file' => $sourceFile,
254
        ];
255
256
        return $this;
257
    }
258
259
    /**
260
     * Clear the mappings.
261
     *
262
     * @return Generator
263
     */
264
    public function clear()
265
    {
266
        $this->mappings = [];
267
268
        return $this;
269
    }
270
271
    /**
272
     * Sets the encoder.
273
     *
274
     * @param Base64VLQ $encoder
275
     *
276
     * @return Generator
277
     */
278
    public function setEncoder(Base64VLQ $encoder)
279
    {
280
        $this->encoder = $encoder;
281
282
        return $this;
283
    }
284
285
    /**
286
     * Returns the encoder.
287
     *
288
     * @return Base64VLQ
289
     */
290
    public function getEncoder()
291
    {
292
        return $this->encoder;
293
    }
294
295
    /**
296
     * Generates the JSON source map.
297
     *
298
     * @return string
299
     *
300
     * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#
301
     */
302
    protected function generateJson()
303
    {
304
        $sourceMap = [
305
            // File version (always the first entry in the object) and must be a positive integer.
306
            'version' => self::VERSION,
307
            // An optional name of the generated code that this source map is associated with.
308
            'file' => $this->getOption('filename'),
309
            // An optional source root, useful for relocating source files on a server or removing repeated values in the 'sources' entry.  This value is prepended to the individual entries in the 'source' field.
310
            'sourceRoot' => $this->getOption('sourceRoot'),
311
            // A list of original sources used by the 'mappings' entry.
312
            'sources' => array_keys($this->sources),
313
        ];
314
315
        // A list of symbol names used by the 'mappings' entry.
316
        $sourceMap['names'] = [];
317
        // A string with the encoded mapping data.
318
        $sourceMap['mappings'] = $this->generateMappings();
319
320
        if ($this->getOption('source_contents')) {
321
            // An optional list of source content, useful when the 'source' can't be hosted.
322
            // The contents are listed in the same order as the sources above.
323
            // 'null' may be used if some original sources should be retrieved by name.
324
            $sourceMap['sourcesContent'] = $this->getSourcesContent();
325
        }
326
327
        // less.js compatibility fixes
328
        if (count($sourceMap['sources']) && !($sourceMap['sourceRoot'])) {
329
            unset($sourceMap['sourceRoot']);
330
        }
331
332
        return json_encode($sourceMap);
333
    }
334
335
    /**
336
     * Returns the sources contents.
337
     *
338
     * @return array|null
339
     */
340
    protected function getSourcesContent()
341
    {
342
        if (empty($this->sources)) {
343
            return;
344
        }
345
346
        // FIXME: we should output only those which were used
347
        return array_values($this->sources);
348
    }
349
350
    /**
351
     * Generates the mappings string.
352
     *
353
     * @return string
354
     */
355
    public function generateMappings()
356
    {
357
        if (!count($this->mappings)) {
358
            return '';
359
        }
360
361
        // group mappings by generated line number.
362
        $groupedMap = $groupedMapEncoded = [];
363
        foreach ($this->mappings as $m) {
364
            $groupedMap[$m['generated_line']][] = $m;
365
        }
366
        ksort($groupedMap);
367
368
        $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
369
370
        foreach ($groupedMap as $lineNumber => $line_map) {
371
            while (++$lastGeneratedLine < $lineNumber) {
372
                $groupedMapEncoded[] = ';';
373
            }
374
375
            $lineMapEncoded = [];
376
            $lastGeneratedColumn = 0;
377
378
            foreach ($line_map as $m) {
379
                $mapEncoded = $this->encoder->encode($m['generated_column'] - $lastGeneratedColumn);
380
                $lastGeneratedColumn = $m['generated_column'];
381
382
                // find the index
383
                if ($m['source_file'] &&
384
                    ($index = $this->findFileIndex($this->normalizeFilename($m['source_file']))) !== false
385
                ) {
386
                    $mapEncoded .= $this->encoder->encode($index - $lastOriginalIndex);
387
                    $lastOriginalIndex = $index;
388
389
                    // lines are stored 0-based in SourceMap spec version 3
390
                    $mapEncoded .= $this->encoder->encode($m['original_line'] - 1 - $lastOriginalLine);
391
                    $lastOriginalLine = $m['original_line'] - 1;
392
393
                    $mapEncoded .= $this->encoder->encode($m['original_column'] - $lastOriginalColumn);
394
                    $lastOriginalColumn = $m['original_column'];
395
                }
396
397
                $lineMapEncoded[] = $mapEncoded;
398
            }
399
400
            $groupedMapEncoded[] = implode(',', $lineMapEncoded) . ';';
401
        }
402
403
        return rtrim(implode($groupedMapEncoded), ';');
404
    }
405
406
    /**
407
     * Finds the index for the filename.
408
     *
409
     * @param string $filename
410
     *
411
     * @return int|false
412
     */
413
    protected function findFileIndex($filename)
414
    {
415
        return array_search($filename, array_keys($this->sources));
416
    }
417
}
418