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 | |||
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 | for ( $i = 0; $i < count( $importParent->rules ); $i++ ) { |
63 | if ( $importParent->rules[$i] === $importNode ) { |
64 | $importParent->rules[$i] = $evaldImportNode; |
65 | break; |
66 | } |
67 | } |
68 | |
69 | // Rename $evaldImportNode to $importNode here so that we avoid avoid mistaken use |
70 | // of not-yet-compiled $importNode after this point, which upstream's code doesn't |
71 | // have access to after this point, either. |
72 | $importNode = $evaldImportNode; |
73 | unset( $evaldImportNode ); |
74 | |
75 | // NOTE: Upstream Less.js's ImportVisitor puts the rest of the processImportNode logic |
76 | // into a separate ImportVisitor.prototype.onImported function, because file loading |
77 | // is async there. They implement and call: |
78 | // |
79 | // - ImportSequencer.prototype.addImport: |
80 | // remembers what processImportNode() was doing, and will call onImported |
81 | // once the async file load is finished. |
82 | // - ImportManager.prototype.push: |
83 | // resolves the import path to full path and uri, |
84 | // then parses the file content into a root Ruleset for that file. |
85 | // - ImportVisitor.prototype.onImported: |
86 | // marks the file as parsed (for skipping duplicates, to avoid recursion), |
87 | // and calls tryRun() if this is the last remaining import. |
88 | // |
89 | // In PHP we load files synchronously, so we can put a simpler version of this |
90 | // logic directly here. |
91 | |
92 | // @see less-2.5.3.js#ImportManager.prototype.push |
93 | |
94 | // NOTE: This is the equivalent to upstream `newFileInfo` and `fileManager.getPath()` |
95 | // TODO: Move this into Less_Tree_Import->PathAndUri along with the rest of the |
96 | // import dirs resolution logic. |
97 | // TODO: We might need upstream's `tryAppendLessExtension` logic here. |
98 | // We currenlty do do this in getPath instead. |
99 | |
100 | $path_and_uri = $env->callImportCallback( $importNode ); |
101 | if ( !$path_and_uri ) { |
102 | $path_and_uri = $importNode->PathAndUri(); |
103 | } |
104 | if ( $path_and_uri ) { |
105 | [ $full_path, $uri ] = $path_and_uri; |
106 | } else { |
107 | $full_path = $uri = $importNode->getPath(); |
108 | } |
109 | '@phan-var string $full_path'; |
110 | |
111 | // @see less-2.5.3.js#ImportManager.prototype.push/loadFileCallback |
112 | |
113 | // NOTE: Upstream creates the next `currentFileInfo` here as `newFileInfo` |
114 | // We instead let Less_Parser::SetFileInfo() do that later via Less_Parser::parseFile(). |
115 | // This means that instead of setting `newFileInfo.reference` we modify the $env, |
116 | // and Less_Parser::SetFileInfo will inherit that. |
117 | if ( $importNode->options['reference'] ?? false ) { |
118 | $env->currentFileInfo['reference'] = true; |
119 | } |
120 | |
121 | $e = null; |
122 | try { |
123 | if ( $importNode->options['inline'] ) { |
124 | if ( !file_exists( $full_path ) ) { |
125 | throw new Less_Exception_Parser( |
126 | sprintf( 'File `%s` not found.', $full_path ), |
127 | null, |
128 | $importNode->index, |
129 | $importNode->currentFileInfo |
130 | ); |
131 | } |
132 | $root = file_get_contents( $full_path ); |
133 | } else { |
134 | $parser = new Less_Parser( $env ); |
135 | // NOTE: Upstream sets `env->processImports = false` here to avoid |
136 | // running ImportVisitor again (infinite loop). We instead separate |
137 | // Less_Parser->parseFile() from Less_Parser->getCss(), |
138 | // and only getCss() runs ImportVisitor. |
139 | $root = $parser->parseFile( $full_path, $uri, true ); |
140 | } |
141 | } catch ( Less_Exception_Parser $err ) { |
142 | $e = $err; |
143 | } |
144 | |
145 | // @see less-2.5.3.js#ImportManager.prototype.push/fileParsedFunc |
146 | |
147 | if ( $importNode->options['optional'] && $e ) { |
148 | $e = null; |
149 | $root = new Less_Tree_Ruleset( null, [] ); |
150 | $full_path = null; |
151 | } |
152 | |
153 | // @see less-2.5.3.js#ImportVisitor.prototype.onImported |
154 | |
155 | if ( $e instanceof Less_Exception_Parser ) { |
156 | if ( !is_numeric( $e->index ) ) { |
157 | $e->index = $importNode->index; |
158 | $e->currentFile = $importNode->currentFileInfo; |
159 | $e->genMessage(); |
160 | } |
161 | throw $e; |
162 | } |
163 | |
164 | $duplicateImport = isset( $this->recursionDetector[$full_path] ); |
165 | |
166 | if ( !$env->importMultiple ) { |
167 | if ( $duplicateImport ) { |
168 | $importNode->doSkip = true; |
169 | } else { |
170 | // NOTE: Upstream implements skip() as dynamic function. |
171 | // We instead have a regular Less_Tree_Import::skip() method, |
172 | // and in cases where skip() would be re-defined here we set doSkip=null. |
173 | $importNode->doSkip = null; |
174 | } |
175 | } |
176 | |
177 | if ( !$full_path && $importNode->options['optional'] ) { |
178 | $importNode->doSkip = true; |
179 | } |
180 | |
181 | if ( $root ) { |
182 | $importNode->root = $root; |
183 | $importNode->importedFilename = $full_path; |
184 | |
185 | if ( !$inlineCSS && ( $env->importMultiple || !$duplicateImport ) && $full_path ) { |
186 | $this->recursionDetector[$full_path] = true; |
187 | $oldContext = $this->env; |
188 | $this->env = $env; |
189 | $this->visitObj( $root ); |
190 | $this->env = $oldContext; |
191 | } |
192 | } |
193 | } else { |
194 | $this->tryRun(); |
195 | } |
196 | } |
197 | |
198 | public function addVariableImport( $callback ) { |
199 | $this->variableImports[] = $callback; |
200 | } |
201 | |
202 | public function tryRun() { |
203 | while ( true ) { |
204 | // NOTE: Upstream keeps a `this.imports` queue here that resumes |
205 | // processImportNode() logic by calling onImported() after a file |
206 | // is finished loading. We don't need that since we load and parse |
207 | // synchronously within processImportNode() instead. |
208 | |
209 | if ( count( $this->variableImports ) === 0 ) { |
210 | break; |
211 | } |
212 | $variableImport = $this->variableImports[0]; |
213 | |
214 | $this->variableImports = array_slice( $this->variableImports, 1 ); |
215 | $function = $variableImport['function']; |
216 | |
217 | $this->$function( ...$variableImport["args"] ); |
218 | } |
219 | } |
220 | |
221 | public function visitRule( $ruleNode, $visitDeeper ) { |
222 | // TODO: We might need upstream's `if (… DetachedRuleset) { this.context.frames.unshift(ruleNode); }` |
223 | $visitDeeper = false; |
224 | } |
225 | |
226 | // TODO: We might need upstream's visitRuleOut() |
227 | // if (… DetachedRuleset) { this.context.frames.shift(); } |
228 | |
229 | public function visitDirective( $directiveNode, $visitArgs ) { |
230 | array_unshift( $this->env->frames, $directiveNode ); |
231 | } |
232 | |
233 | public function visitDirectiveOut( $directiveNode ) { |
234 | array_shift( $this->env->frames ); |
235 | } |
236 | |
237 | public function visitMixinDefinition( $mixinDefinitionNode, $visitArgs ) { |
238 | array_unshift( $this->env->frames, $mixinDefinitionNode ); |
239 | } |
240 | |
241 | public function visitMixinDefinitionOut( $mixinDefinitionNode ) { |
242 | array_shift( $this->env->frames ); |
243 | } |
244 | |
245 | public function visitRuleset( $rulesetNode, $visitArgs ) { |
246 | array_unshift( $this->env->frames, $rulesetNode ); |
247 | } |
248 | |
249 | public function visitRulesetOut( $rulesetNode ) { |
250 | array_shift( $this->env->frames ); |
251 | } |
252 | |
253 | public function visitMedia( $mediaNode, $visitArgs ) { |
254 | // TODO: Upsteam does not modify $mediaNode here. Why do we? |
255 | $mediaNode->allExtends = []; |
256 | array_unshift( $this->env->frames, $mediaNode->allExtends ); |
257 | } |
258 | |
259 | public function visitMediaOut( $mediaNode ) { |
260 | array_shift( $this->env->frames ); |
261 | } |
262 | |
263 | } |