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

com.squarespace.less.model.Block 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.model;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.squarespace.less.core.Buffer;
import com.squarespace.less.core.FlexList;
import com.squarespace.less.core.LessUtils;


/**
 * Represents an array of rules.
 *
 * During evaluation some instructions such as {@link Import} or {@link MixinCall},
 * when produce one or more new rules.  These rules can be spliced into the
 * block, replacing the node which produced them.
 *
 * For this reason, blocks manage their arrays of objects directly.
 *
 * Each block contains a set of flags which are used to avoid unnecessary
 * scans of its rules during evaluation.
 */
public class Block implements Node {

  /**
   * Variable cache needs to be rebuilt.
   */
  private static final byte FLAG_REBUILD_VARS = 0x01;

  /**
   * Block contains import statements which need to be evaluated.
   */
  private static final byte FLAG_HAS_IMPORTS = 0x02;

  /**
   * Block contains mixin calls which need to be evaluated.
   */
  private static final byte FLAG_HAS_MIXIN_CALLS = 0x04;

  /**
   * Initial capacity of the blocks array.
   */
  private static final int INITIAL_CAPACITY = 8;

  /**
   * List of nodes contained within this block.
   */
  protected final FlexList rules;

  /**
   * Charset directive, if any, associated with this block.
   */
  protected Directive charset;

  /**
   * Variable cache. Since variables can be dynamically added to a block
   * during execution, periodically this cache needs to be rebuilt so that
   * these new variables are found during evaluation.
   */
  protected Map variables;

  /**
   * Ordered list of mixin definitions contained in this block that share a
   * common prefix.
   */
  protected Map> mixins;

  /**
   * Initial flags controlling this block. On creation we need to build the
   * variable cache.
   */
  protected byte flags = FLAG_REBUILD_VARS;

  /**
   * Constructs a block with the default initial capcity.
   */
  public Block() {
    this(INITIAL_CAPACITY);
  }

  /**
   * Constructs a block with the given initial capacity.
   */
  public Block(int initialCapacity) {
    rules = new FlexList<>(initialCapacity);
  }

  /**
   * Private constructor, used by the {@link Block#copy()} method.
   */
  private Block(FlexList rules, byte flags, Map> mixins) {
    this.rules = rules;
    this.flags = flags;
    this.mixins = copyMixins(mixins);
  }

  /**
   * Deep copy the mixin index.
   */
  private Map> copyMixins(Map> mixins) {
    if (mixins == null) {
      return null;
    }
    Map> result = new HashMap<>();
    for (Map.Entry> entry : mixins.entrySet()) {
      result.put(entry.getKey(), new ArrayList<>(entry.getValue()));
    }
    return result;
  }

  /**
   * Sets the charset {@link Directive} for this block.
   */
  public void charset(Directive charset) {
    this.charset = charset;
  }

  /**
   * Returns the charset {@link Directive} for this block, if any.
   */
  public Directive charset() {
    return charset;
  }

  /**
   * Inserts a {@link Node} at the head of the block.
   */
  public void prependNode(Node node) {
    setFlags(node);
    rules.splice(0, 0, new Node[] { node });
  }

  /**
   * Appends a {@link Node} to the tail of the block.
   */
  public void appendNode(Node node) {
    setFlags(node);
    rules.append(node);
  }

  /**
   * Appends all {@link Node}s from the argument {@code Block} to
   * the tail of this instance.
   */
  public void appendBlock(Block block) {
    FlexList rules = block.rules();
    int size = rules.size();
    for (int i = 0; i < size; i++) {
      this.appendNode(rules.get(i));
    }
  }

  /**
   * Returns the internal {@link FlexList} holding the block's rules.
   */
  public FlexList rules() {
    return rules;
  }

  public void splice(int start, int num, FlexList other) {
    this.rules.splice(start, num, other);
    int size = other.size();
    for (int i = 0; i < size; i++) {
      setFlags(other.get(i));
    }
  }

  public Map> mixins() {
    return this.mixins;
  }

  /**
   * Indicate whether this block contains {@link Import} node.
   */
  public boolean hasImports() {
    return (flags & FLAG_HAS_IMPORTS) != 0;
  }

  /**
   * Indicate whether this block contains a {@link MixinCall} node.
   */
  public boolean hasMixinCalls() {
    return (flags & FLAG_HAS_MIXIN_CALLS) != 0;
  }

  /**
   * Mark that the variable cache needs to be rebuilt.
   */
  public void resetVariableCache() {
    flags |= FLAG_REBUILD_VARS;
  }

  /**
   * Resolve a {@link Definition} with the given {@code name} against
   * the rules in this block.
   */
  public Definition resolveDefinition(String name) {
    if ((flags & FLAG_REBUILD_VARS) != 0) {
      buildVariables();
    }
    return variables.get(name);
  }

  /**
   * Builds the variable cache by locating all {@link Definition} nodes
   * within the block, and mapping them by name.
   */
  private void buildVariables() {
    if (variables == null) {
      variables = new HashMap<>();
    } else {
      variables.clear();
    }
    int size = rules.size();
    for (int i = 0; i < size; i++) {
      Node node = rules.get(i);
      if (!node.type().equals(NodeType.DEFINITION)) {
        continue;
      }
      Definition def = (Definition)node;
      variables.put(def.name(), def);
    }
    flags &= ~FLAG_REBUILD_VARS;
  }

  /**
   * Create a shallow copy of this block.
   */
  public Block copy() {
    return new Block(rules.copy(), flags, mixins);
  }

  /**
   * Sets this instance's flags by OR-ing with the arguments flags.
   */
  public void orFlags(Block block) {
    flags |= block.flags;
  }

  /**
   * Debug method - collects the definitions inside this block.
   */
//  public String dumpDefs() {
//    Buffer buf = new Buffer(4);
//    dumpDefs(buf);
//    return buf.toString();
//  }

  /**
   * Debug method - collects the definitions inside this block.
   */
//  public boolean dumpDefs(Buffer buf) {
//    boolean output = false;
//    int size = rules.size();
//    for (int i = 0; i < size; i++) {
//      Node node = rules.get(i);
//      if (node.type().equals(NodeType.DEFINITION)) {
//        String repr = node.toString().replaceAll("\\s+", " ");
//        buf.indent().append(repr).append('\n');
//        output = true;
//      }
//    }
//    return output;
//  }

  /**
   * See {@link Node#type()}
   */
  @Override
  public NodeType type() {
    return NodeType.BLOCK;
  }

  /**
   * See {@link Node#repr()}
   */
  @Override
  public void repr(Buffer buf) {
    int size = rules.size();
    for (int i = 0; i < size; i++) {
      Node rule = rules.get(i);
      if (rule == null) {
        continue;
      }
      if (!buf.compress()) {
        buf.indent();
      }
      rule.repr(buf);
      if (!(rule instanceof BlockNode) && !(rule instanceof Comment)) {
        if (!buf.compress() || i + 1 < size) {
          buf.append(';');
        }
        if (!buf.compress()) {
          buf.append('\n');
        }
      }
    }
  }

  /**
   * See {@link Node#modelRepr(Buffer)}
   */
  @Override
  public void modelRepr(Buffer buf) {
    if (rules != null) {
      ReprUtils.modelRepr(buf, "\n", true, rules);
    }
  }

  @Override
  public boolean equals(Object obj) {
    if (obj instanceof Block) {
      return LessUtils.safeEquals(rules, ((Block)obj).rules);
    }
    return false;
  }

  @Override
  public int hashCode() {
    return ModelUtils.notHashable();
  }

  @Override
  public String toString() {
    return rules.toString();
  }

  /**
   * Updates this block's flags and mixin index.
   */
  private void setFlags(Node node) {
    if (node instanceof Import) {
      flags |= FLAG_HAS_IMPORTS;
    } else if (node instanceof MixinCall) {
      flags |= FLAG_HAS_MIXIN_CALLS;
    } else if (node instanceof Mixin) {
      this.indexMixin(((Mixin)node).name, node);
    } else if (node instanceof Ruleset) {
      Ruleset ruleset = (Ruleset)node;
      Selectors selectors = ruleset.selectors();
      if (selectors.hasMixinPath()) {
        List _selectors = selectors.selectors();
        int size = _selectors.size();
        for (int i = 0; i < size; i++) {
          Selector selector = _selectors.get(i);
          List path = selector.mixinPath();
          if (path != null) {
            this.indexMixin(path.get(0), node);
          }
        }
      }
    }
  }

  private void indexMixin(String prefix, Node node) {
    // Optimization for fast prefix matching of mixin paths.
    // Index the ruleset and mixin paths by the first segment
    // of their path. This lets is prune the search tree drastically
    // by (a) ignoring branches with no valid prefix and (b) iterating
    // over just the mixins when a prefix is valid.

    if (this.mixins == null) {
      this.mixins = new HashMap<>();
    }

    List nodes = this.mixins.get(prefix);
    if (nodes == null) {
      nodes = new ArrayList<>();
      this.mixins.put(prefix, nodes);
    }
    nodes.add(node);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy