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