package com.github.sommeri.less4j.core.compiler.stages;
import java.util.ArrayList;
import java.util.List;
import com.github.sommeri.less4j.LessCompiler.Configuration;
import com.github.sommeri.less4j.core.ast.ASTCssNode;
import com.github.sommeri.less4j.core.ast.Body;
import com.github.sommeri.less4j.core.ast.BodyOwner;
import com.github.sommeri.less4j.core.ast.Declaration;
import com.github.sommeri.less4j.core.ast.DetachedRuleset;
import com.github.sommeri.less4j.core.ast.DetachedRulesetReference;
import com.github.sommeri.less4j.core.ast.Expression;
import com.github.sommeri.less4j.core.ast.GeneralBody;
import com.github.sommeri.less4j.core.ast.KeywordExpression;
import com.github.sommeri.less4j.core.ast.ListExpression;
import com.github.sommeri.less4j.core.ast.ListExpressionOperator;
import com.github.sommeri.less4j.core.ast.MixinReference;
import com.github.sommeri.less4j.core.ast.ReusableStructure;
import com.github.sommeri.less4j.core.compiler.expressions.ExpressionEvaluator;
import com.github.sommeri.less4j.core.compiler.expressions.ExpressionFilter;
import com.github.sommeri.less4j.core.compiler.expressions.ExpressionManipulator;
import com.github.sommeri.less4j.core.compiler.expressions.GuardValue;
import com.github.sommeri.less4j.core.compiler.expressions.MixinsGuardsValidator;
import com.github.sommeri.less4j.core.compiler.scopes.FoundMixin;
import com.github.sommeri.less4j.core.compiler.scopes.FullMixinDefinition;
import com.github.sommeri.less4j.core.compiler.scopes.IScope;
import com.github.sommeri.less4j.core.compiler.scopes.InScopeSnapshotRunner;
import com.github.sommeri.less4j.core.compiler.scopes.InScopeSnapshotRunner.IFunction;
import com.github.sommeri.less4j.core.compiler.scopes.InScopeSnapshotRunner.ITask;
import com.github.sommeri.less4j.core.compiler.scopes.ScopeFactory;
import com.github.sommeri.less4j.core.compiler.scopes.view.ScopeView;
import com.github.sommeri.less4j.core.parser.HiddenTokenAwareTree;
import com.github.sommeri.less4j.core.problems.ProblemsHandler;
import com.github.sommeri.less4j.core.problems.UnableToFinish;
import com.github.sommeri.less4j.utils.ArraysUtils;
import com.github.sommeri.less4j.utils.Couple;
class MixinsRulesetsSolver {
private final ProblemsHandler problemsHandler;
private final ReferencesSolver parentSolver;
private final AstNodesStack semiCompiledNodes;
private final Configuration configuration;
private final DefaultGuardHelper defaultGuardHelper;
private final CallerCalleeScopeJoiner scopeManipulation = new CallerCalleeScopeJoiner();
private final ExpressionManipulator expressionManipulator = new ExpressionManipulator();
public MixinsRulesetsSolver(ReferencesSolver parentSolver, AstNodesStack semiCompiledNodes, ProblemsHandler problemsHandler, Configuration configuration) {
this.parentSolver = parentSolver;
this.semiCompiledNodes = semiCompiledNodes;
this.problemsHandler = problemsHandler;
this.configuration = configuration;
this.defaultGuardHelper = new DefaultGuardHelper(problemsHandler);
}
private Couple<List<ASTCssNode>, IScope> resolveCalledBody(final IScope callerScope, final BodyOwner<?> bodyOwner, final IScope referencedMixinScope, final ReturnMode returnMode, final int visibilityBlocks) {
// ... and I'm starting to see the point of closures ...
return InScopeSnapshotRunner.runInLocalDataSnapshot(referencedMixinScope, new IFunction<Couple<List<ASTCssNode>, IScope>>() {
@Override
public Couple<List<ASTCssNode>, IScope> run() {
// compile referenced mixin - keep the original copy unchanged
List<ASTCssNode> replacement = compileBody(bodyOwner.getBody(), referencedMixinScope);
for (ASTCssNode node : replacement) {
node.addVisibilityBlocks(visibilityBlocks);
}
// collect variables and mixins to be imported
IScope returnValues = ScopeFactory.createDummyScope();
if (returnMode == ReturnMode.MIXINS_AND_VARIABLES) {
ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(referencedMixinScope, problemsHandler, configuration);
returnValues.addFilteredVariables(new ImportedScopeFilter(expressionEvaluator, callerScope), referencedMixinScope);
}
List<FullMixinDefinition> unmodifiedMixinsToImport = referencedMixinScope.getAllMixins();
List<FullMixinDefinition> allMixinsToImport = scopeManipulation.mixinsToImport(callerScope, referencedMixinScope, unmodifiedMixinsToImport);
returnValues.addAllMixins(allMixinsToImport);
return new Couple<List<ASTCssNode>, IScope>(replacement, returnValues);
}
});
}
private List<ASTCssNode> compileBody(Body body, IScope scopeSnapshot) {
semiCompiledNodes.push(body.getParent());
try {
Body bodyClone = body.clone();
parentSolver.unsafeDoSolveReferences(bodyClone, scopeSnapshot);
return bodyClone.getMembers();
} finally {
semiCompiledNodes.pop();
}
}
private void shiftComments(ASTCssNode reference, GeneralBody result) {
List<ASTCssNode> childs = result.getMembers();
if (!childs.isEmpty()) {
childs.get(0).addOpeningComments(reference.getOpeningComments());
childs.get(childs.size() - 1).addTrailingComments(reference.getTrailingComments());
}
}
private IScope buildMixinsArguments(EvaluatedMixinReferenceCall evaluatedReference, IScope referenceScope, FullMixinDefinition mixin) {
ArgumentsBuilder builder = new ArgumentsBuilder(evaluatedReference, mixin.getMixin(), problemsHandler);
return builder.build();
}
public GeneralBody buildMixinReferenceReplacement(final EvaluatedMixinReferenceCall evaluatedReference, final IScope callerScope, List<FoundMixin> mixins) {
final MixinReference reference = evaluatedReference.getReference();
if (Thread.currentThread().isInterrupted())
throw new UnableToFinish("Thread Interrupted", (ASTCssNode) null);
final GeneralBody result = new GeneralBody(reference.getUnderlyingStructure());
if (mixins.isEmpty())
return result;
// candidate mixins with information about their default() function use are
// stored here
final List<BodyCompilationData> readyMixins = new ArrayList<BodyCompilationData>();
for (final FoundMixin fullMixin : mixins) {
final ReusableStructure mixin = fullMixin.getMixin();
final IScope mixinScope = fullMixin.getScope();
final BodyCompilationData data = new BodyCompilationData(mixin);
// the following needs to run in snapshot because
// calculateMixinsWorkingScope modifies that scope
InScopeSnapshotRunner.runInLocalDataSnapshot(mixinScope.getParent(), new ITask() {
@Override
public void run() {
// add arguments
IScope mixinArguments = buildMixinsArguments(evaluatedReference, callerScope, fullMixin);
data.setArguments(mixinArguments);
mixinScope.getParent().add(mixinArguments);
ScopeView mixinWorkingScope = scopeManipulation.joinIfIndependent(callerScope, mixinScope);
// it the mixin calls itself recursively, each copy should work on
// independent copy of local data
// that is matters mostly for scope placeholders - if both close
// placeholders in same copy error happen
mixinWorkingScope.toIndependentWorkingCopy();
data.setMixinWorkingScope(mixinWorkingScope);
MixinsGuardsValidator guardsValidator = new MixinsGuardsValidator(mixinWorkingScope, problemsHandler, configuration);
GuardValue guardValue = guardsValidator.evaluateGuards(fullMixin.getGuardsOnPath(), mixin);
data.setGuardValue(guardValue);
readyMixins.add(data);
}
}); // end of InScopeSnapshotRunner.runInLocalDataSnapshot
}
// filter out mixins we do not want to use
List<BodyCompilationData> mixinsToBeUsed = defaultGuardHelper.chooseMixinsToBeUsed(readyMixins, reference);
for (final BodyCompilationData data : mixinsToBeUsed) {
final ScopeView mixinWorkingScope = data.getMixinWorkingScope();
// compilation must run in another localDataSnapshot, because imported
// detached ruleset stored in
// variables point to original scope - making snapshot above is not enough
// since they point to scope as defined during definition, they would not
// know parameters
// of mixins that define them
InScopeSnapshotRunner.runInLocalDataSnapshot(mixinWorkingScope.getParent(), new ITask() {
@Override
public void run() {
BodyOwner<?> mixin = data.getCompiledBodyOwner();
// add arguments again - detached rulesets imported into this one
// via returned variables from sub-calls need would not see arguments
// otherwise
// bc they keep link to original copy and there is no other way how to
// access them
IScope arguments = data.getArguments();
mixinWorkingScope.getParent().add(arguments);
Couple<List<ASTCssNode>, IScope> compiled = resolveCalledBody(callerScope, mixin, mixinWorkingScope, ReturnMode.MIXINS_AND_VARIABLES, reference.getVisibilityBlocks());
// update mixin replacements and update scope with imported variables
// and mixins
result.addMembers(compiled.getT());
callerScope.addToDataPlaceholder(compiled.getM());
}
}); // end of InScopeSnapshotRunner.runInLocalData........Snapshot
}
callerScope.closeDataPlaceholder();
resolveImportance(reference, result);
shiftComments(reference, result);
return result;
}
public GeneralBody buildDetachedRulesetReplacement(DetachedRulesetReference reference, IScope callerScope, DetachedRuleset detachedRuleset, IScope detachedRulesetScope) {
IScope mixinWorkingScope = scopeManipulation.joinIfIndependent(callerScope, detachedRulesetScope);
Couple<List<ASTCssNode>, IScope> compiled = resolveCalledBody(callerScope, detachedRuleset, mixinWorkingScope, ReturnMode.MIXINS, reference.getVisibilityBlocks());
GeneralBody result = new GeneralBody(reference.getUnderlyingStructure());
result.addMembers(compiled.getT());
callerScope.addToDataPlaceholder(compiled.getM());
callerScope.closeDataPlaceholder();
// resolveImportance(reference, result);
shiftComments(reference, result);
return result;
}
private void resolveImportance(MixinReference reference, GeneralBody result) {
if (reference.isImportant()) {
declarationsAreImportant(result);
}
}
@SuppressWarnings("rawtypes")
private void declarationsAreImportant(Body result) {
for (ASTCssNode kid : result.getMembers()) {
if (kid instanceof Declaration) {
Declaration declaration = (Declaration) kid;
addImportantKeyword(declaration);
} else if (kid instanceof BodyOwner<?>) {
BodyOwner owner = (BodyOwner) kid;
declarationsAreImportant(owner.getBody());
}
}
}
private void addImportantKeyword(Declaration declaration) {
Expression expression = declaration.getExpression();
if (expressionManipulator.isImportant(expression))
return;
HiddenTokenAwareTree underlying = expression != null ? expression.getUnderlyingStructure() : declaration.getUnderlyingStructure();
KeywordExpression important = createImportantKeyword(underlying);
ListExpression list = expressionManipulator.findRightmostSpaceSeparatedList(expression);
if (list == null) {
list = new ListExpression(underlying, ArraysUtils.asNonNullList(expression), new ListExpressionOperator(underlying, ListExpressionOperator.Operator.EMPTY_OPERATOR));
}
list.addExpression(important);
list.configureParentToAllChilds();
declaration.setExpression(list);
list.setParent(declaration);
}
private KeywordExpression createImportantKeyword(HiddenTokenAwareTree underlyingStructure) {
return new KeywordExpression(underlyingStructure, "!important", true);
}
class ImportedScopeFilter implements ExpressionFilter {
private final ExpressionEvaluator expressionEvaluator;
private final IScope importTargetScope;
private final CallerCalleeScopeJoiner scopeManipulation = new CallerCalleeScopeJoiner();
public ImportedScopeFilter(ExpressionEvaluator expressionEvaluator, IScope importTargetScope) {
super();
this.expressionEvaluator = expressionEvaluator;
this.importTargetScope = importTargetScope;
}
public Expression apply(Expression input) {
Expression result = expressionEvaluator.evaluate(input);
IScope newScope = apply(result.getScope());
result.setScope(newScope);
return result;
}
private IScope apply(IScope input) {
if (input == null)
return importTargetScope;
return scopeManipulation.joinIfIndependentAndPreserveContent(importTargetScope, input);
}
@Override
public boolean accepts(String name, Expression value) {
return true;
}
}
}