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