Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.59% covered (success)
97.59%
81 / 83
77.78% covered (warning)
77.78%
7 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Less_Tree_Import
97.59% covered (success)
97.59%
81 / 83
77.78% covered (warning)
77.78%
7 / 9
40
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%
26 / 26
100.00% covered (success)
100.00%
1 / 1
7
 skip
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
1<?php
2declare( strict_types = 1 );
3
4/**
5 * CSS `@import` node
6 *
7 * The general strategy here is that we don't want to wait
8 * for the parsing to be completed, before we start importing
9 * the file. That's because in the context of a browser,
10 * most of the time will be spent waiting for the server to respond.
11 *
12 * On creation, we push the import path to our import queue, though
13 * `import,push`, we also pass it a callback, which it'll call once
14 * the file has been fetched, and parsed.
15 *
16 * @private
17 * @see less-2.5.3.js#Import.prototype
18 */
19class Less_Tree_Import extends Less_Tree {
20
21    /** @var array<string,bool> */
22    public $options;
23    /** @var int */
24    public $index;
25    /** @var Less_Tree_Quoted|Less_Tree_Url */
26    public $path;
27    /** @var Less_Tree_Value */
28    public $features;
29    /** @var array|null */
30    public $currentFileInfo;
31    /** @var bool|null */
32    public $css;
33    /** @var bool|null This is populated by Less_ImportVisitor */
34    public $doSkip = false;
35    /** @var string|null This is populated by Less_ImportVisitor */
36    public $importedFilename;
37    /**
38     * This is populated by Less_ImportVisitor.
39     *
40     * For imports that use "inline", this holds a raw string.
41     *
42     * @var string|Less_Tree_Ruleset|null
43     */
44    public $root;
45
46    public function __construct( $path, $features, array $options, $index, $currentFileInfo = null ) {
47        $this->options = $options + [ 'inline' => false, 'optional' => false, 'multiple' => false ];
48        $this->index = $index;
49        $this->path = $path;
50        $this->features = $features;
51        $this->currentFileInfo = $currentFileInfo;
52
53        if ( isset( $this->options['less'] ) || $this->options['inline'] ) {
54            $this->css = !isset( $this->options['less'] ) || !$this->options['less'] || $this->options['inline'];
55        } else {
56            $pathValue = $this->getPath();
57            // Leave any ".css" file imports as literals for the browser.
58            // Also leave any remote HTTP resources as literals regardless of whether
59            // they contain ".css" in their filename.
60            if ( $pathValue && (
61                preg_match( '/[#\.\&\?\/]css([\?;].*)?$/', $pathValue )
62                || preg_match( '/^(https?:)?\/\//i', $pathValue )
63            ) ) {
64                $this->css = true;
65            }
66        }
67    }
68
69//
70// The actual import node doesn't return anything, when converted to CSS.
71// The reason is that it's used at the evaluation stage, so that the rules
72// it imports can be treated like any other rules.
73//
74// In `eval`, we make sure all Import nodes get evaluated, recursively, so
75// we end up with a flat structure, which can easily be imported in the parent
76// ruleset.
77//
78
79    public function accept( $visitor ) {
80        if ( $this->features ) {
81            $this->features = $visitor->visitObj( $this->features );
82        }
83        $this->path = $visitor->visitObj( $this->path );
84
85        if ( !$this->options['inline'] && $this->root ) {
86            $this->root = $visitor->visit( $this->root );
87        }
88    }
89
90    public function genCSS( $output ) {
91        if ( $this->css && !isset( $this->path->currentFileInfo["reference"] ) ) {
92            $output->add( '@import ', $this->currentFileInfo, $this->index );
93            $this->path->genCSS( $output );
94            if ( $this->features ) {
95                $output->add( ' ' );
96                $this->features->genCSS( $output );
97            }
98            $output->add( ';' );
99        }
100    }
101
102    /**
103     * @return string|null
104     */
105    public function getPath() {
106        // During the first pass, Less_Tree_Url may contain a Less_Tree_Variable (not yet expanded),
107        // and thus has no value property defined yet. Return null until we reach the next phase.
108        // https://github.com/wikimedia/less.php/issues/29
109        // TODO: Upstream doesn't need a check against Less_Tree_Variable. Why do we?
110        $path = ( $this->path instanceof Less_Tree_Url && !( $this->path->value instanceof Less_Tree_Variable ) )
111            ? $this->path->value->value
112            // e.g. Less_Tree_Quoted
113            : $this->path->value;
114
115        if ( is_string( $path ) ) {
116            // remove query string and fragment
117            return preg_replace( '/[\?#][^\?]*$/', '', $path );
118        }
119    }
120
121    public function isVariableImport() {
122        $path = $this->path;
123        if ( $path instanceof Less_Tree_Url ) {
124            $path = $path->value;
125        }
126        if ( $path instanceof Less_Tree_Quoted ) {
127            return $path->containsVariables();
128        }
129        return true;
130    }
131
132    public function compileForImport( $env ) {
133        $path = $this->path;
134        if ( $path instanceof Less_Tree_Url ) {
135            $path = $path->value;
136        }
137        return new self( $path->compile( $env ), $this->features, $this->options, $this->index, $this->currentFileInfo );
138    }
139
140    public function compilePath( $env ) {
141        $path = $this->path->compile( $env );
142        $rootpath = $this->currentFileInfo['rootpath'] ?? null;
143
144        if ( !( $path instanceof Less_Tree_Url ) ) {
145            if ( $rootpath ) {
146                $pathValue = $path->value;
147                // Add the base path if the import is relative
148                if ( $pathValue && Less_Environment::isPathRelative( $pathValue ) ) {
149                    $path->value = $this->currentFileInfo['uri_root'] . $pathValue;
150                }
151            }
152            $path->value = Less_Environment::normalizePath( $path->value );
153        }
154
155        return $path;
156    }
157
158    /**
159     * @param Less_Environment $env
160     * @see less-2.5.3.js#Import.prototype.eval
161     */
162    public function compile( $env ) {
163        $features = ( $this->features ? $this->features->compile( $env ) : null );
164
165        // import once
166        if ( $this->skip( $env ) ) {
167            return [];
168        }
169
170        if ( $this->options['inline'] ) {
171            $contents = new Less_Tree_Anonymous(
172                $this->root,
173                0,
174                [
175                    'filename' => $this->importedFilename,
176                    'reference' => $this->currentFileInfo['reference'] ?? null,
177                ],
178                true,
179                true,
180                false
181            );
182            return $this->features
183                ? new Less_Tree_Media( [ $contents ], $this->features->value )
184                : [ $contents ];
185        } elseif ( $this->css ) {
186            $newImport = new self( $this->compilePath( $env ), $features, $this->options, $this->index );
187            // TODO: We might need upstream's `if (!newImport.css && this.error) { throw this.error;`
188            return $newImport;
189        } else {
190            $ruleset = new Less_Tree_Ruleset( null, $this->root->rules );
191
192            $ruleset->evalImports( $env );
193
194            return $this->features
195                ? new Less_Tree_Media( $ruleset->rules, $this->features->value )
196                : $ruleset->rules;
197
198        }
199    }
200
201    /**
202     * Should the import be skipped?
203     *
204     * @param Less_Environment $env
205     * @return bool|null
206     */
207    public function skip( $env ) {
208        $path = $this->getPath();
209        // TODO: Since our Import->getPath() varies from upstream Less.js (ours can return null).
210        // we therefore need an empty string fallback here. Remove this fallback once getPath()
211        // is in sync with upstream.
212        $fullPath = Less_FileManager::getFilePath( $path, $this->currentFileInfo )[0] ?? $path ?? '';
213
214        if ( $this->doSkip !== null ) {
215            return $this->doSkip;
216        }
217
218        // @see less-2.5.3.js#ImportVisitor.prototype.onImported
219        if ( isset( $env->importVisitorOnceMap[$fullPath] ) ) {
220            return true;
221        }
222
223        $env->importVisitorOnceMap[$fullPath] = true;
224        return false;
225    }
226}