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