All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.squarespace.less.exec.MixinMatcher Maven / Gradle / Ivy

The newest version!
/**
 * Copyright (c) 2014 SQUARESPACE, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.squarespace.less.exec;

import static com.squarespace.less.core.ExecuteErrorMaker.argNamedNotFound;
import static com.squarespace.less.core.ExecuteErrorMaker.argTooMany;

import java.util.ArrayDeque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;

import com.squarespace.less.LessContext;
import com.squarespace.less.LessException;
import com.squarespace.less.core.LessInternalException;
import com.squarespace.less.model.Argument;
import com.squarespace.less.model.Block;
import com.squarespace.less.model.Expression;
import com.squarespace.less.model.GenericBlock;
import com.squarespace.less.model.MixinCall;
import com.squarespace.less.model.MixinCallArgs;
import com.squarespace.less.model.MixinParams;
import com.squarespace.less.model.Node;
import com.squarespace.less.model.Parameter;


/**
 * Carries out argument-to-parameter pattern matching and binding for MIXIN
 * resolution and execution.
 */
public class MixinMatcher {

  private final ExecEnv callEnv;

  private final MixinCall mixinCall;

  private final MixinCallArgs mixinArgs;

  public MixinMatcher(ExecEnv callEnv, MixinCall call) throws LessException {
    this.callEnv = callEnv;
    this.mixinCall = call;
    MixinCallArgs args = call.args();
    this.mixinArgs = (MixinCallArgs) (args == null ? args : args.eval(callEnv));
  }

  public MixinCall mixinCall() {
    return mixinCall;
  }

  public MixinCallArgs mixinArgs() {
    return mixinArgs;
  }

  public ExecEnv callEnv() {
    return callEnv;
  }

  /**
   * Attempts to bind the mixin arguments to the mixin parameters. This happens in the following
   * discrete steps:
   *
   *  1. Bind default values for named parameters and create the expression to capture variadic
   *     arguments, if any.
   *  2. Bind named arguments and track which named parameters have been bound.
   *  3. Bind positional argument values to remaining named parameters, or collect the argument's
   *     value in the variadic expression, or skip them if they were just used for pattern matching.
   *  4. Build the final list of bindings and add the special "@arguments" variable.
   */
  public GenericBlock bind(MixinParams mixinParams) throws LessException {
    if (mixinParams.needsEval()) {
      throw new LessInternalException("Serious error: params must already be evaluated!");
    }

    LessContext ctx = callEnv.context();

    List params = mixinParams.params();
    List args = mixinArgs == null ? null : mixinArgs.args();
    int paramSize = params.size();
    int argSize = args == null ? 0 : args.size();

    Map boundValues = new LinkedHashMap<>();
    Queue names = new ArrayDeque<>();

    String variadicName = null;
    Expression variadic = null;

    // Bind parameter default values, collect names, and prepare variadic expression, if any.
    for (int i = 0; i < paramSize; i++) {
      Parameter param = params.get(i);
      String paramName = param.name();
      if (param.variadic()) {
        variadicName = paramName;
        variadic = new Expression();

      } else if (paramName != null) {
        names.add(paramName);
        Node value = param.value();
        if (value != null) {
          boundValues.put(paramName, value);
        }
      }
    }

    // Bind all named arguments.
    for (int i = 0; i < argSize; i++) {
      Argument arg = args.get(i);
      String argName = arg.name();
      if (argName == null) {
        continue;
      }
      if (!names.contains(argName)) {
        LessException exc = new LessException(argNamedNotFound(argName));
        exc.push(mixinCall);
        throw exc;
      }
      boundValues.put(argName, arg.value());
      names.remove(argName);
    }

    // Bind all remaining positional arguments.
    for (int i = 0; i < argSize; i++) {
      Argument arg = args.get(i);
      String argName = arg.name();
      if (argName != null) {
        continue;
      }

      // Have a positional parameter?  Check for variadic or value pattern match
      boolean haveParams = i < paramSize;
      if (haveParams) {
        Parameter param = params.get(i);
        if (param.variadic()) {
          variadic.add(arg.value());
          continue;

        } else if (param.name() == null) {
          // Pattern match.
          continue;
        }
      }

      // Positional, assign to one of the remaining named arguments if any.
      if (!names.isEmpty()) {
        argName = names.poll();
        boundValues.put(argName, arg.value());

      } else if (variadic != null) {
        variadic.add(arg.value());
      } else {
        // No names left and no variadic exists to collect overflow
        // We should never reach this point since patternMatch() would have also failed.
        throw new LessException(argTooMany());
      }
    }

    // Build the final bindings block.
    Expression arguments = new Expression();
    Block bindings = new Block(boundValues.size());
    for (Map.Entry entry : boundValues.entrySet()) {
      Node value = entry.getValue();
      bindings.appendNode(ctx.nodeBuilder().buildDefinition(entry.getKey(), value));
      arguments.add(value);
    }
    if (variadicName != null) {
      bindings.appendNode(ctx.nodeBuilder().buildDefinition(variadicName, variadic));
    }
    if (variadic != null) {
      for (Node value : variadic.values()) {
        arguments.add(value);
      }
    }
    bindings.appendNode(ctx.nodeBuilder().buildDefinition("@arguments", arguments));
    return new GenericBlock(bindings);
  }

  /**
   * Determine if the arguments match the parameter's pattern.
   */
  public boolean patternMatch(MixinParams mixinParams) throws LessException {
    List params = mixinParams.params();
    List args = mixinArgs == null ? null : mixinArgs.args();
    int paramSize = params.size();
    int argSize = args == null ? 0 : args.size();

    if (argSize < mixinParams.required()) {
      return false;
    }
    if (!mixinParams.variadic() && argSize > params.size()) {
      return false;
    }

    // Check if args are compatible and match the parameter pattern fully.
    int size = Math.min(argSize, paramSize);
    for (int i = 0; i < size; i++) {
      Argument arg = args.get(i);
      Parameter param = params.get(i);
      String paramName = param.name();
      if (paramName == null && !param.variadic() && !valueEquals(arg, param)) {
        return false;
      }
    }
    return true;
  }

  /**
   * Check if the argument's value is equal to the parameter's value. It first
   * tries the Node.equals() method, and if that fails it falls back to comparing
   * the rendered output of each node.
   */
  private boolean valueEquals(Argument arg, Parameter param) throws LessException {
    Node val1 = arg.value();
    Node val2 = param.value();
    if (!val1.equals(val2)) {
      // If Node.equals() fails, try compare the rendered output.
      LessContext ctx = callEnv.context();
      String v0 = ctx.render(val1);
      String v1 = ctx.render(val2);
      if (!v0.equals(v1)) {
        return false;
      }
    }
    return true;
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy