Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
88.42% |
84 / 95 |
|
93.33% |
14 / 15 |
CRAP | |
0.00% |
0 / 1 |
Less_ImportVisitor | |
88.42% |
84 / 95 |
|
93.33% |
14 / 15 |
49.28 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
run | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
visitImport | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
processImportNode | |
82.26% |
51 / 62 |
|
0.00% |
0 / 1 |
31.07 | |||
addVariableImport | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
tryRun | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
visitDeclaration | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitAtRule | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitAtRuleOut | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitMixinDefinition | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitMixinDefinitionOut | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitRuleset | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitRulesetOut | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
visitMedia | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
visitMediaOut | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * @private |
4 | */ |
5 | class Less_ImportVisitor extends Less_Visitor { |
6 | |
7 | /** @var Less_Environment */ |
8 | public $env; |
9 | /** @var array<array{function:string,args:array}> */ |
10 | public $variableImports = []; |
11 | /** @var array<string,true> */ |
12 | public $recursionDetector = []; |
13 | |
14 | /** @var int */ |
15 | public $_currentDepth = 0; |
16 | /** @var mixed */ |
17 | public $importItem; |
18 | |
19 | public function __construct( $env ) { |
20 | parent::__construct(); |
21 | // NOTE: Upstream creates a new environment/context here. We re-use the main one instead. |
22 | // This makes Less_Environment->addParsedFile() easier to support (which is custom to Less.php) |
23 | $this->env = $env; |
24 | // NOTE: Upstream `importCount` is not here, appears unused. |
25 | // NOTE: Upstream `isFinished` is not here, we simply call tryRun() once at the end. |
26 | // NOTE: Upstream `onceFileDetectionMap` is instead Less_Environment->isFileParsed. |
27 | // NOTE: Upstream `ImportSequencer` logic is directly inside ImportVisitor for simplicity. |
28 | } |
29 | |
30 | public function run( $root ) { |
31 | $this->visitObj( $root ); |
32 | $this->tryRun(); |
33 | } |
34 | |
35 | public function visitImport( $importNode, &$visitDeeper ) { |
36 | $inlineCSS = $importNode->options['inline']; |
37 | |
38 | if ( !$importNode->css || $inlineCSS ) { |
39 | |
40 | $env = $this->env->clone(); |
41 | $importParent = $env->frames[0]; |
42 | if ( $importNode->isVariableImport() ) { |
43 | $this->addVariableImport( [ |
44 | 'function' => 'processImportNode', |
45 | 'args' => [ $importNode, $env, $importParent ] |
46 | ] ); |
47 | } else { |
48 | $this->processImportNode( $importNode, $env, $importParent ); |
49 | } |
50 | } |
51 | $visitDeeper = false; |
52 | } |
53 | |
54 | public function processImportNode( $importNode, $env, &$importParent ) { |
55 | $evaldImportNode = $inlineCSS = $importNode->options['inline']; |
56 | |
57 | try { |
58 | $evaldImportNode = $importNode->compileForImport( $env ); |
59 | } catch ( Exception $e ) { |
60 | $importNode->css = true; |
61 | } |
62 | |
63 | if ( $evaldImportNode && ( !$evaldImportNode->css || $inlineCSS ) ) { |
64 | |
65 | if ( $importNode->options['multiple'] ) { |
66 | $env->importMultiple = true; |
67 | } |
68 | |
69 | $tryAppendLessExtension = $evaldImportNode->css === null; |
70 | |
71 | for ( $i = 0; $i < count( $importParent->rules ); $i++ ) { |
72 | if ( $importParent->rules[$i] === $importNode ) { |
73 | $importParent->rules[$i] = $evaldImportNode; |
74 | break; |
75 | } |
76 | } |
77 | |
78 | // Rename $evaldImportNode to $importNode here so that we avoid avoid mistaken use |
79 | // of not-yet-compiled $importNode after this point, which upstream's code doesn't |
80 | // have access to after this point, either. |
81 | $importNode = $evaldImportNode; |
82 | unset( $evaldImportNode ); |
83 | |
84 | // NOTE: Upstream Less.js's ImportVisitor puts the rest of the processImportNode logic |
85 | // into a separate ImportVisitor.prototype.onImported function, because file loading |
86 | // is async there. They implement and call: |
87 | // |
88 | // - ImportSequencer.prototype.addImport: |
89 | // remembers what processImportNode() was doing, and will call onImported |
90 | // once the async file load is finished. |
91 | // - ImportManager.prototype.push: |
92 | // resolves the import path to full path and uri, |
93 | // then parses the file content into a root Ruleset for that file. |
94 | // - ImportVisitor.prototype.onImported: |
95 | // marks the file as parsed (for skipping duplicates, to avoid recursion), |
96 | // and calls tryRun() if this is the last remaining import. |
97 | // |
98 | // In PHP we load files synchronously, so we can put a simpler version of this |
99 | // logic directly here. |
100 | |
101 | // @see less-2.5.3.js#ImportManager.prototype.push |
102 | |
103 | // NOTE: This is the equivalent to upstream `newFileInfo` and `fileManager.getPath()` |
104 | |
105 | $path = $importNode->getPath(); |
106 | |
107 | if ( $tryAppendLessExtension ) { |
108 | $path = preg_match( '/(\.[a-z]*$)|([\?;].*)$/', $path ) ? $path : $path . '.less'; |
109 | } |
110 | |
111 | [ $fullPath, $uri ] = |
112 | Less_FileManager::getFilePath( $path, $importNode->currentFileInfo ) ?? [ $path, $path ]; |
113 | |
114 | // @see less-2.5.3.js#ImportManager.prototype.push/loadFileCallback |
115 | |
116 | // NOTE: Upstream creates the next `currentFileInfo` here as `newFileInfo` |
117 | // We instead let Less_Parser::SetFileInfo() do that later via Less_Parser::parseFile(). |
118 | // This means that instead of setting `newFileInfo.reference` we modify the $env, |
119 | // and Less_Parser::SetFileInfo will inherit that. |
120 | if ( $importNode->options['reference'] ?? false ) { |
121 | $env->currentFileInfo['reference'] = true; |
122 | } |
123 | |
124 | $e = null; |
125 | try { |
126 | if ( $importNode->options['inline'] ) { |
127 | if ( !file_exists( $fullPath ) ) { |
128 | throw new Less_Exception_Parser( |
129 | sprintf( 'File `%s` not found.', $fullPath ), |
130 | null, |
131 | $importNode->index, |
132 | $importNode->currentFileInfo |
133 | ); |
134 | } |
135 | $root = file_get_contents( $fullPath ); |
136 | } else { |
137 | $parser = new Less_Parser( $env ); |
138 | // NOTE: Upstream sets `env->processImports = false` here to avoid |
139 | // running ImportVisitor again (infinite loop). We instead separate |
140 | // Less_Parser->parseFile() from Less_Parser->getCss(), |
141 | // and only getCss() runs ImportVisitor. |
142 | $root = $parser->parseFile( $fullPath, $uri, true ); |
143 | } |
144 | } catch ( Less_Exception_Parser $err ) { |
145 | $e = $err; |
146 | } |
147 | |
148 | // @see less-2.5.3.js#ImportManager.prototype.push/fileParsedFunc |
149 | |
150 | if ( $importNode->options['optional'] && $e ) { |
151 | $e = null; |
152 | $root = new Less_Tree_Ruleset( null, [] ); |
153 | $fullPath = null; |
154 | } |
155 | |
156 | // @see less-2.5.3.js#ImportVisitor.prototype.onImported |
157 | |
158 | if ( $e instanceof Less_Exception_Parser ) { |
159 | if ( !is_numeric( $e->index ) ) { |
160 | $e->index = $importNode->index; |
161 | $e->currentFile = $importNode->currentFileInfo; |
162 | $e->genMessage(); |
163 | } |
164 | throw $e; |
165 | } |
166 | |
167 | $duplicateImport = isset( $this->recursionDetector[$fullPath] ); |
168 | |
169 | if ( !$env->importMultiple ) { |
170 | if ( $duplicateImport ) { |
171 | $importNode->doSkip = true; |
172 | } else { |
173 | // NOTE: Upstream implements skip() as dynamic function. |
174 | // We instead have a regular Less_Tree_Import::skip() method, |
175 | // and in cases where skip() would be re-defined here we set doSkip=null. |
176 | $importNode->doSkip = null; |
177 | } |
178 | } |
179 | |
180 | if ( !$fullPath && $importNode->options['optional'] ) { |
181 | $importNode->doSkip = true; |
182 | } |
183 | |
184 | if ( $root ) { |
185 | $importNode->root = $root; |
186 | $importNode->importedFilename = $fullPath; |
187 | |
188 | if ( !$inlineCSS && ( $env->importMultiple || !$duplicateImport ) && $fullPath ) { |
189 | $this->recursionDetector[$fullPath] = true; |
190 | $oldContext = $this->env; |
191 | $this->env = $env; |
192 | $this->visitObj( $root ); |
193 | $this->env = $oldContext; |
194 | } |
195 | } |
196 | } else { |
197 | $this->tryRun(); |
198 | } |
199 | } |
200 | |
201 | public function addVariableImport( $callback ) { |
202 | $this->variableImports[] = $callback; |
203 | } |
204 | |
205 | public function tryRun() { |
206 | while ( true ) { |
207 | // NOTE: Upstream keeps a `this.imports` queue here that resumes |
208 | // processImportNode() logic by calling onImported() after a file |
209 | // is finished loading. We don't need that since we load and parse |
210 | // synchronously within processImportNode() instead. |
211 | |
212 | if ( count( $this->variableImports ) === 0 ) { |
213 | break; |
214 | } |
215 | $variableImport = $this->variableImports[0]; |
216 | |
217 | $this->variableImports = array_slice( $this->variableImports, 1 ); |
218 | $function = $variableImport['function']; |
219 | |
220 | $this->$function( ...$variableImport["args"] ); |
221 | } |
222 | } |
223 | |
224 | public function visitDeclaration( $declNode, $visitDeeper ) { |
225 | // TODO: We might need upstream's `if (… DetachedRuleset) { this.context.frames.unshift(ruleNode); }` |
226 | $visitDeeper = false; |
227 | } |
228 | |
229 | // TODO: Implement less-3.13.1.js#ImportVisitor.prototype.visitDeclarationOut |
230 | // if (… DetachedRuleset) { this.context.frames.shift(); } |
231 | |
232 | public function visitAtRule( $atRuleNode, $visitArgs ) { |
233 | array_unshift( $this->env->frames, $atRuleNode ); |
234 | } |
235 | |
236 | public function visitAtRuleOut( $atRuleNode ) { |
237 | array_shift( $this->env->frames ); |
238 | } |
239 | |
240 | public function visitMixinDefinition( $mixinDefinitionNode, $visitArgs ) { |
241 | array_unshift( $this->env->frames, $mixinDefinitionNode ); |
242 | } |
243 | |
244 | public function visitMixinDefinitionOut( $mixinDefinitionNode ) { |
245 | array_shift( $this->env->frames ); |
246 | } |
247 | |
248 | public function visitRuleset( $rulesetNode, $visitArgs ) { |
249 | array_unshift( $this->env->frames, $rulesetNode ); |
250 | } |
251 | |
252 | public function visitRulesetOut( $rulesetNode ) { |
253 | array_shift( $this->env->frames ); |
254 | } |
255 | |
256 | public function visitMedia( $mediaNode, $visitArgs ) { |
257 | // TODO: Upsteam does not modify $mediaNode here. Why do we? |
258 | $mediaNode->allExtends = []; |
259 | array_unshift( $this->env->frames, $mediaNode->allExtends ); |
260 | } |
261 | |
262 | public function visitMediaOut( $mediaNode ) { |
263 | array_shift( $this->env->frames ); |
264 | } |
265 | |
266 | } |