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

com.squarespace.less.exec.CssModel 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.model.NodeType.BLOCK_DIRECTIVE;
import static com.squarespace.less.model.NodeType.MEDIA;
import static com.squarespace.less.model.NodeType.RULESET;
import static com.squarespace.less.model.NodeType.STYLESHEET;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;

import com.squarespace.less.LessContext;
import com.squarespace.less.core.Buffer;
import com.squarespace.less.core.LessInternalException;
import com.squarespace.less.core.LessUtils;
import com.squarespace.less.model.BlockDirective;
import com.squarespace.less.model.Media;
import com.squarespace.less.model.NodeType;
import com.squarespace.less.model.Ruleset;
import com.squarespace.less.model.Stylesheet;

/**
 * Extremely simplistic model for CSS structure. Allows the LESS renderer
 * to defer final output in order to suppress empty blocks, and eliminate
 * duplicate rules.  It also ensures that each nested block is emitted
 * in the output model at the correct scope.
 */
public class CssModel {

  /**
   * Set of node types that can be children of a {@link Stylesheet}.
   */
  private static final EnumSet STYLESHEET_ACCEPT = EnumSet.of(
      STYLESHEET, RULESET, MEDIA, BLOCK_DIRECTIVE
      );

  /**
   * Set of node types that can be children of a {@link Media} node.
   */
  private static final EnumSet MEDIA_ACCEPT = EnumSet.of(
      BLOCK_DIRECTIVE, RULESET
      );

  /**
   * Set of node types that can be children of a {@link BlockDirective} node.
   */
  private static final EnumSet BLOCK_DIRECTIVE_ACCEPT = EnumSet.of(
      BLOCK_DIRECTIVE, RULESET
      );

  /**
   * Set of node types that can be children of a {@link Ruleset} node.
   */
  private static final EnumSet RULESET_ACCEPT = EnumSet.noneOf(NodeType.class);

  /**
   * Stack of CSS blocks.
   */
  private final Deque stack = new ArrayDeque<>();

  /**
   * Internal buffer for rendering the CSS output.
   */
  private final Buffer buffer;

  /**
   * Current block being operated on.
   */
  private CssBlock current;

  /**
   * Sequential block identifier.
   */
  private int blockId;

  /**
   * Complexity of the model.
   */
  private int complexity;

  /**
   * Tracks the size of the model's output as it is constructed.
   */
  private int size;

  /**
   * Constructs a CSS model with the given context.
   */
  public CssModel(LessContext ctx) {
    buffer = ctx.newBuffer();
    current = new CssBlock(this.blockId++, STYLESHEET);
  }

  public int complexity() {
    return complexity;
  }

  public int size() {
    return size;
  }

  /**
   * Renders the CSS model into text form.
   */
  public String render() {
    if (current.type() != STYLESHEET) {
      throw new LessInternalException("Serious error: stack was not fully popped.");
    }
    buffer.reset();
    current.render(buffer);
    return buffer.toString();
  }

  /**
   * Appends a value to the current block.
   */
  public CssModel value(String value) {
    this.complexity++;
    this.size += value.length();
    current.add(new CssValue(value));
    return this;
  }

  /**
   * Appends a comment to the current block.
   */
  public CssModel comment(String value) {
    this.complexity++;
    this.size += value.length();
    if (!value.isEmpty()) {
      current.add(new CssComment(value));
    }
    return this;
  }

  /**
   * Add raw strings to the header of the current block.
   */
  public CssModel header(String value) {
    this.complexity++;
    this.size += value.length();
    current.add(value);
    return this;
  }

  /**
   * Pushes an empty block onto the stack and associates it with the given node type.
   */
  public CssModel push(NodeType type) {
    stack.push(current);
    CssBlock child = new CssBlock(this.blockId++, type);
    defer(child);
    current = child;
    return this;
  }

  /**
   * Pops a block from the top of the stack, setting flags indicating whether
   * anything was appended to the block.  This is used to prune empty blocks.
   */
  public CssModel pop() {
    CssBlock parent = current.parent();
    parent.populated |= current.populated;
    current = stack.pop();
    return this;
  }

  /**
   * Push this block up the stack until it finds its proper parent.
   */
  private void defer(CssBlock block) {
    if (current.accept(block)) {
      return;
    }
    Iterator iter = stack.iterator();
    while (iter.hasNext()) {
      CssBlock candidate = iter.next();
      if (candidate.accept(block)) {
        return;
      }
    }
    throw new LessInternalException("Serious error: no block accepted " + block.type());
  }

  /**
   * Represents a CSS block that can contain other nodes and blocks.
   */
  static class CssBlock extends CssNode {

    private final List headers = new ArrayList<>();

    private final CssSet nodes;

    private final int id;

    private final NodeType type;

    private final EnumSet acceptFilter;

    private CssBlock parent;

    private boolean populated = false;

    CssBlock(int id, NodeType type) {
      this.id = id;
      this.type = type;
      int capacity = type == STYLESHEET ? 512 : 32;
      this.nodes = new CssSet<>(capacity);
      switch (type) {

        case BLOCK_DIRECTIVE:
          acceptFilter = BLOCK_DIRECTIVE_ACCEPT;
          break;

        case MEDIA:
          acceptFilter = MEDIA_ACCEPT;
          break;

        case RULESET:
          acceptFilter = RULESET_ACCEPT;
          break;

        case STYLESHEET:
          acceptFilter = STYLESHEET_ACCEPT;
          break;

        default:
          throw new LessInternalException("Serious error: css model block cannot be " + type);
      }
    }

    /**
     * Determines if this block can accept a block of the given type as a
     * child.
     */
    public boolean accept(CssBlock block) {
      if (acceptFilter.contains(block.type())) {
        add(block);
        block.setParent(this);
        return true;
      }
      return false;
    }

    public CssBlock parent() {
      return parent;
    }

    public void setParent(CssBlock parent) {
      this.parent = parent;
    }

    public NodeType type() {
      return type;
    }

    @Override
    public boolean populated() {
      return populated;
    }

    public void add(String header) {
      headers.add(header);
    }

    public void add(CssNode node) {
      nodes.add(node);
      populated |= node.populated();
    }

    @Override
    public int hashCode() {
      return this.id;
    }

    @Override
    public boolean equals(Object obj) {
      return false;
    }

    @Override
    public boolean isValue() {
      return false;
    }

    @Override
    public void render(Buffer buf) {
      if (!populated) {
        return;
      }

      if (!headers.isEmpty()) {
        int size = headers.size();
        for (int i = 0; i < size; i++) {
          if (i > 0) {
            buf.selectorSep();
          }
          buf.indent();
          buf.append(headers.get(i));
        }
        buf.blockOpen();
      }

      Iterator iter = nodes.iterator();
      while (iter.hasNext()) {
        CssNode node = iter.next();
        node.render(buf);
        // If we're adding a rule, and we're not compressed or this is not the last
        // rule in the ruleset, append the semicolon.
        if (node instanceof CssValue && (!buf.compress() || iter.hasNext())) {
          buf.ruleEnd();
        }
      }
      if (!headers.isEmpty()) {
        buf.blockClose();
      }
    }
  }

  /**
   * Represents a simple value in a CSS model.
   */
  static class CssValue extends CssNode {

    private final String value;

    CssValue(String value) {
      this.value = value;
    }

    @Override
    public boolean equals(Object obj) {
      return (obj instanceof CssValue) ? LessUtils.safeEquals(value, ((CssValue)obj).value) : false;
    }

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

    @Override
    public void render(Buffer buf) {
      buf.indent();
      buf.append(value);
    }

  }

  /**
   * Represents a comment in a CSS model.
   */
  static class CssComment extends CssNode {

    private final String value;

    CssComment(String value) {
      this.value = value;
    }

    @Override
    public boolean equals(Object obj) {
      return (obj instanceof CssComment) ? LessUtils.safeEquals(value, ((CssComment)obj).value) : false;
    }

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

    @Override
    public void render(Buffer buf) {
      buf.indent().append(value);
    }

  }

  /**
   * Abstract node in a CSS model.
   */
  static abstract class CssNode {

    public boolean isValue() {
      return true;
    }

    public boolean populated() {
      return true;
    }

    public abstract void render(Buffer buf);

  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy