Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.92% covered (success)
94.92%
112 / 118
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Less_Tree_Import
94.92% covered (success)
94.92%
112 / 118
70.00% covered (warning)
70.00%
7 / 10
57.43
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
8
 accept
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 genCSS
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getPath
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 isVariableImport
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 compileForImport
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 compilePath
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 compile
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
9
 PathAndUri
87.88% covered (warning)
87.88%
29 / 33
0.00% covered (danger)
0.00%
0 / 1
15.40
 skip
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
1<?php
2/**
3 * CSS `@import` node
4 *
5 * The general strategy here is that we don't want to wait
6 * for the parsing to be completed, before we start importing
7 * the file. That's because in the context of a browser,
8 * most of the time will be spent waiting for the server to respond.
9 *
10 * On creation, we push the import path to our import queue, though
11 * `import,push`, we also pass it a callback, which it'll call once
12 * the file has been fetched, and parsed.
13 *
14 * @private
15 * @see less-2.5.3.js#Import.prototype
16 */
17class Less_Tree_Import extends Less_Tree {
18
19    public $options;
20    public $index;
21    public $path;
22    public $features;
23    public $currentFileInfo;
24    public $css;
25    /** @var bool|null This is populated by Less_ImportVisitor */
26    public $doSkip = false;
27    /** @var string|null This is populated by Less_ImportVisitor */
28    public $importedFilename;
29    /**
30     * This is populated by Less_ImportVisitor.
31     *
32     * For imports that use "inline", this holds a raw string.
33     *
34     * @var string|Less_Tree_Ruleset|null
35     */
36    public $root;
37
38    public function __construct( $path, $features, array $options, $index, $currentFileInfo = null ) {
39        $this->options = $options + [ 'inline' => false, 'optional' => false, 'multiple' => false ];
40        $this->index = $index;
41        $this->path = $path;
42        $this->features = $features;
43        $this->currentFileInfo = $currentFileInfo;
44
45        if ( isset( $this->options['less'] ) || $this->options['inline'] ) {
46            $this->css = !isset( $this->options['less'] ) || !$this->options['less'] || $this->options['inline'];
47        } else {
48            $pathValue = $this->getPath();
49            // Leave any ".css" file imports as literals for the browser.
50            // Also leave any remote HTTP resources as literals regardless of whether
51            // they contain ".css" in their filename.
52            if ( $pathValue && (
53                preg_match( '/[#\.\&\?\/]css([\?;].*)?$/', $pathValue )
54                || preg_match( '/^(https?:)?\/\//i', $pathValue )
55            ) ) {
56                $this->css = true;
57            }
58        }
59    }
60
61//
62// The actual import node doesn't return anything, when converted to CSS.
63// The reason is that it's used at the evaluation stage, so that the rules
64// it imports can be treated like any other rules.
65//
66// In `eval`, we make sure all Import nodes get evaluated, recursively, so
67// we end up with a flat structure, which can easily be imported in the parent
68// ruleset.
69//
70
71    public function accept( $visitor ) {
72        if ( $this->features ) {
73            $this->features = $visitor->visitObj( $this->features );
74        }
75        $this->path = $visitor->visitObj( $this->path );
76
77        if ( !$this->options['inline'] && $this->root ) {
78            $this->root = $visitor->visit( $this->root );
79        }
80    }
81
82    public function genCSS( $output ) {
83        if ( $this->css && !isset( $this->path->currentFileInfo["reference"] ) ) {
84            $output->add( '@import ', $this->currentFileInfo, $this->index );
85            $this->path->genCSS( $output );
86            if ( $this->features ) {
87                $output->add( ' ' );
88                $this->features->genCSS( $output );
89            }
90            $output->add( ';' );
91        }
92    }
93
94    /**
95     * @return string|null
96     */
97    public function getPath() {
98        // During the first pass, Less_Tree_Url may contain a Less_Tree_Variable (not yet expanded),
99        // and thus has no value property defined yet. Return null until we reach the next phase.
100        // https://github.com/wikimedia/less.php/issues/29
101        // TODO: Upstream doesn't need a check against Less_Tree_Variable. Why do we?
102        $path = ( $this->path instanceof Less_Tree_Url && !( $this->path->value instanceof Less_Tree_Variable ) )
103            ? $this->path->value->value
104            // e.g. Less_Tree_Quoted
105            : $this->path->value;
106
107        if ( is_string( $path ) ) {
108            // remove query string and fragment
109            return preg_replace( '/[\?#][^\?]*$/', '', $path );
110        }
111    }
112
113    public function isVariableImport() {
114        $path = $this->path;
115        if ( $path instanceof Less_Tree_Url ) {
116            $path = $path->value;
117        }
118        if ( $path instanceof Less_Tree_Quoted ) {
119            return $path->containsVariables();
120        }
121        return true;
122    }
123
124    public function compileForImport( $env ) {
125        $path = $this->path;
126        if ( $path instanceof Less_Tree_Url ) {
127             $path = $path->value;
128        }
129        return new self( $path->compile( $env ), $this->features, $this->options, $this->index, $this->currentFileInfo );
130    }
131
132    public function compilePath( $env ) {
133        $path = $this->path->compile( $env );
134        $rootpath = $this->currentFileInfo['rootpath'] ?? null;
135
136        if ( !( $path instanceof Less_Tree_Url ) ) {
137            if ( $rootpath ) {
138                $pathValue = $path->value;
139                // Add the base path if the import is relative
140                if ( $pathValue && Less_Environment::isPathRelative( $pathValue ) ) {
141                    $path->value = $this->currentFileInfo['uri_root'] . $pathValue;
142                }
143            }
144            $path->value = Less_Environment::normalizePath( $path->value );
145        }
146
147        return $path;
148    }
149
150    /**
151     * @param Less_Environment $env
152     * @see less-2.5.3.js#Import.prototype.eval
153     */
154    public function compile( $env ) {
155        $features = ( $this->features ? $this->features->compile( $env ) : null );
156
157        // TODO: Upstream doesn't do path resolution here. The reason we need it here is
158        // because skip() takes a $path_and_uri argument. Once the TODO in ImportVisitor
159        // about Less_Tree_Import::PathAndUri() is fixed, this can be removed by letting
160        // skip() call $this->PathAndUri() on its own.
161        // get path & uri
162        $path_and_uri = $env->callImportCallback( $this );
163        if ( !$path_and_uri ) {
164            $path_and_uri = $this->PathAndUri();
165        }
166        if ( $path_and_uri ) {
167            [ $full_path, $uri ] = $path_and_uri;
168        } else {
169            $full_path = $uri = $this->getPath();
170        }
171        '@phan-var string $full_path';
172
173        // import once
174        if ( $this->skip( $full_path, $env ) ) {
175            return [];
176        }
177
178        if ( $this->options['inline'] ) {
179            $contents = new Less_Tree_Anonymous( $this->root, 0,
180                [
181                    'filename' => $this->importedFilename,
182                    'reference' => $this->currentFileInfo['reference'] ?? null,
183                ],
184                true,
185                true,
186                false
187            );
188            return $this->features
189                ? new Less_Tree_Media( [ $contents ], $this->features->value )
190                : [ $contents ];
191        } elseif ( $this->css ) {
192            $newImport = new self( $this->compilePath( $env ), $features, $this->options, $this->index );
193            // TODO: We might need upstream's `if (!newImport.css && this.error) { throw this.error;`
194            return $newImport;
195        } else {
196            $ruleset = new Less_Tree_Ruleset( null, $this->root->rules );
197
198            $ruleset->evalImports( $env );
199
200            return $this->features
201                ? new Less_Tree_Media( $ruleset->rules, $this->features->value )
202                : $ruleset->rules;
203
204        }
205    }
206
207    /**
208     * Using the import directories, get the full absolute path and uri of the import
209     *
210     * @see less-node/FileManager.getPath https://github.com/less/less.js/blob/v2.5.3/lib/less-node/file-manager.js#L70
211     */
212    public function PathAndUri() {
213        $evald_path = $this->getPath();
214
215        $tryAppendLessExtension = $this->css === null;
216
217        if ( $tryAppendLessExtension ) {
218            $evald_path = ( isset( $this->css ) || preg_match( '/(\.[a-z]*$)|([\?;].*)$/', $evald_path ) ) ? $evald_path : $evald_path . '.less';
219        }
220
221        // TODO: Move callImportCallback() and getPath() fallback logic from callers
222        //       to here so that PathAndUri() is equivalent to upstream fileManager.getPath()
223
224        if ( $evald_path ) {
225
226            $import_dirs = [];
227
228            if ( Less_Environment::isPathRelative( $evald_path ) ) {
229                // if the path is relative, the file should be in the current directory
230                if ( $this->currentFileInfo ) {
231                    $import_dirs[ $this->currentFileInfo['currentDirectory'] ] = $this->currentFileInfo['uri_root'];
232                }
233
234            } else {
235                // otherwise, the file should be relative to the server root
236                if ( $this->currentFileInfo ) {
237                    $import_dirs[ $this->currentFileInfo['entryPath'] ] = $this->currentFileInfo['entryUri'];
238                }
239                // if the user supplied entryPath isn't the actual root
240                $import_dirs[ $_SERVER['DOCUMENT_ROOT'] ] = '';
241
242            }
243
244            // always look in user supplied import directories
245            $import_dirs = array_merge( $import_dirs, Less_Parser::$options['import_dirs'] );
246
247            foreach ( $import_dirs as $rootpath => $rooturi ) {
248                if ( is_callable( $rooturi ) ) {
249                    $res = $rooturi( $evald_path );
250                    if ( $res && is_string( $res[0] ) ) {
251                        return [
252                            Less_Environment::normalizePath( $res[0] ),
253                            Less_Environment::normalizePath( $res[1] ?? dirname( $evald_path ) )
254                        ];
255                    }
256                } elseif ( !empty( $rootpath ) ) {
257                    $path = rtrim( $rootpath, '/\\' ) . '/' . ltrim( $evald_path, '/\\' );
258                    if ( file_exists( $path ) ) {
259                        return [
260                            Less_Environment::normalizePath( $path ),
261                            Less_Environment::normalizePath( dirname( $rooturi . $evald_path ) )
262                        ];
263                    }
264                    if ( file_exists( $path . '.less' ) ) {
265                        return [
266                            Less_Environment::normalizePath( $path . '.less' ),
267                            Less_Environment::normalizePath( dirname( $rooturi . $evald_path . '.less' ) )
268                        ];
269                    }
270                }
271            }
272        }
273    }
274
275    /**
276     * Should the import be skipped?
277     *
278     * @param string|null $path
279     * @param Less_Environment $env
280     * @return bool|null
281     */
282    public function skip( $path, $env ) {
283        if ( $this->doSkip !== null ) {
284            return $this->doSkip;
285        }
286
287        // @see less-2.5.3.js#ImportVisitor.prototype.onImported
288        if ( isset( $env->importVisitorOnceMap[$path] ) ) {
289            return true;
290        }
291
292        $env->importVisitorOnceMap[$path] = true;
293        return false;
294    }
295}