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 |
50.43 | |
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 |
32.38 | |||
| 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 = $fullPath && 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 | } |