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

com.squarespace.less.exec.LessRenderer 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 java.nio.file.Path;
import java.util.List;

import com.squarespace.less.LessContext;
import com.squarespace.less.LessException;
import com.squarespace.less.LessOptions;
import com.squarespace.less.core.Buffer;
import com.squarespace.less.core.FlexList;
import com.squarespace.less.model.Block;
import com.squarespace.less.model.BlockDirective;
import com.squarespace.less.model.Comment;
import com.squarespace.less.model.Definition;
import com.squarespace.less.model.Directive;
import com.squarespace.less.model.Features;
import com.squarespace.less.model.Import;
import com.squarespace.less.model.ImportMarker;
import com.squarespace.less.model.Media;
import com.squarespace.less.model.MixinCall;
import com.squarespace.less.model.MixinMarker;
import com.squarespace.less.model.Node;
import com.squarespace.less.model.NodeType;
import com.squarespace.less.model.Rule;
import com.squarespace.less.model.Ruleset;
import com.squarespace.less.model.Selector;
import com.squarespace.less.model.Selectors;
import com.squarespace.less.model.Stylesheet;


/**
 * Given an executed tree, renders the final output.
 */
public class LessRenderer {

  /**
   * Rendering complexity threshold.
   */
  private static final int COMPLEXITY_THRESHOLD = 2000000;

  /**
   * Output size limit of 10 MB.
   */
  private static final int SIZE_THRESHOLD = 10 * 1024 * 1024;

  /**
   * Context for the current compile.
   */
  private final LessContext ctx;

  /**
   * {@link Stylesheet} instance to render.
   */
  private final Stylesheet stylesheet;

  /**
   * Rendering enviromment.
   */
  private final RenderEnv env;

  /**
   * Options for the current compile.
   */
  private final LessOptions opts;

  /**
   * CSS model to build.
   */
  private final CssModel model;

  /**
   * Sequence for generating trace identifiers.
   */
  private int traceId;

  /**
   * Sequence for generating warning identifiers.
   */
  private int warningId;

  /**
   * Indicates if a complexity warning was emitted.
   */
  private boolean complexity_warning = false;

  /**
   * Indicates if a size warning was emitted.
   */
  private boolean size_warning = false;

  protected LessRenderer(LessContext context, Stylesheet stylesheet) {
    this.ctx = context;
    this.stylesheet = stylesheet;
    this.env = context.newRenderEnv();
    this.opts = context.options();
    this.model = new CssModel(context);
  }

  public static String render(LessContext context, Stylesheet sheet) throws LessException {
    return new LessRenderer(context, sheet).render();
  }

  /**
   * Render the {@link Stylesheet} to the {@link CssModel} and return the
   * rendered output.
   */
  public String render() throws LessException {
    env.push(stylesheet);
    Block block = stylesheet.block();
    Directive charset = block.charset();
    if (charset != null) {
      model.value(ctx.render(charset));
    }
    renderImports(block);
    renderBlock(block, false);
    env.pop();

    if (model.complexity() > COMPLEXITY_THRESHOLD) {
      model.comment("/* RENDER: exceeded render complexity limit: " + model.complexity() + " */");
    }
    if (model.size() > SIZE_THRESHOLD) {
      model.comment("/* RENDER: exceeded render complexity limit: " + model.size() + " chars */");
    }
    return model.render();
  }

  /**
   * Render a {@link Ruleset}
   */
  private void renderRuleset(Ruleset ruleset) throws LessException {
    env.push(ruleset);
    model.push(NodeType.RULESET);

    // Check if selector complexity threshold was exceeded and emit a comment

    Selectors selectors = env.frame().selectors();
    if (!selectors.isEmpty()) {
      // Selectors are indented and delimited by the model.
      Buffer buf = ctx.acquireBuffer();
      List _selectors = selectors.selectors();
      int size = _selectors.size();
      for (int i = 0; i < size; i++) {
        Selector selector = _selectors.get(i);
        ctx.render(buf, selector);
        model.header(buf.toString());
        buf.reset();
      }
      ctx.returnBuffer();
    }

    renderBlock(ruleset.block(), true);
    model.pop();
    env.pop();
  }

  /**
   * Render a {@link Media}
   */
  private void renderMedia(Media media) throws LessException {
    env.push(media);
    model.push(NodeType.MEDIA);
    model.header("@media " + ctx.render(env.frame().features()));

    // Force any parent selectors to be emitted, to wrap our rules.
    Ruleset inner = new Ruleset();
    inner.setBlock(media.block());
    renderRuleset(inner);

    model.pop();
    env.pop();
  }

  /**
   * Render a {@link BlockDirective}
   */
  private void renderBlockDirective(BlockDirective directive) throws LessException {
    env.push(directive);
    model.push(NodeType.BLOCK_DIRECTIVE);
    model.header(directive.name());

    renderBlock(directive.block(), true);

    model.pop();
    env.pop();
  }

  /**
   * Render all {@Import} rules found in the given block.
   */
  private void renderImports(Block block) throws LessException {
    if (!block.hasImports()) {
      return;
    }
    FlexList rules = block.rules();
    int size = rules.size();
    for (int i = 0; i < size; i++) {
      Node node = rules.get(i);
      switch (node.type()) {

        case IMPORT:
          renderImport((Import)node);
          break;

        default:
          break;
      }
    }
  }

  /**
   * Render all children for the given {@link Block}, optionally including
   * imports.
   */
  private void renderBlock(Block block, boolean includeImports) throws LessException {
    if (model.complexity() > COMPLEXITY_THRESHOLD) {
      if (!complexity_warning) {
        model.comment("/* ERROR: Render incomplete: stylesheet exceeeded complexity threshold */");
        complexity_warning = true;
      }
      return;
    }

    if (model.size() > SIZE_THRESHOLD) {
      if (!size_warning) {
        model.comment("/* ERROR: Render incomplete: stylesheet exceeded size threshold */");
        size_warning = true;
      }
      return;
    }

    FlexList rules = block.rules();
    int size = rules.size();
    for (int i = 0; i < size; i++) {
      Node node = rules.get(i);
      if (node == null) {
        continue;
      }
      switch (node.type()) {

        case BLOCK_DIRECTIVE:
          renderBlockDirective((BlockDirective)node);
          break;

        case COMMENT:
          Comment comment = (Comment)node;
          if (comment.block() && (!opts.compress() || comment.hasBang())) {
            model.comment(ctx.render(comment));
          }
          break;

        case DEFINITION:
          renderDefinition((Definition)node);
          break;

        case DIRECTIVE:
          Directive directive = (Directive)node;
          if (!directive.name().equals("@charset")) {
            model.value(ctx.render(directive));
          }
          break;

        case IMPORT:
          if (includeImports) {
            renderImport((Import)node);
          }
          break;

        case IMPORT_MARKER:
          renderImportMarker((ImportMarker)node);
          break;

        case MEDIA:
          renderMedia((Media)node);
          break;

        case MIXIN:
          // Ignore in render phase.
          break;

        case MIXIN_MARKER:
          renderMixinMarker((MixinMarker)node);
          break;

        case RULE:
          renderRule((Rule)node);
          break;

        case RULESET:
          renderRuleset((Ruleset)node);
          break;

        default:
          // Ignore unhandled nodes. We may have exceeded a complexity threshold during
          // evaluation, which could leave some nodes in the stylesheet unevaluated.
          // Some of these nodes have no rendering representation (e.g. MIXIN_CALL) so must
          // be ignored.
          break;
      }
    }
  }

  /**
   * Render a {@link Definition}.
   */
  private void renderDefinition(Definition def) throws LessException {
    String warnings = def.warnings();
    if (warnings != null) {
      String repr = "definition '" + def.name() + "'";
      emitWarnings(repr, def.warnings());
    }
    if (opts.tracing()) {
      Path fileName = def.fileName();
      Buffer buf = ctx.acquireBuffer();

      buf.append("  define   ");
      buf.append(def.repr().trim());
      if (fileName != null) {
        buf.append("    ").append(def.fileName().toString());
      }
      buf.append(':').append(def.lineOffset() + 1).append(' ');
      emitTrace(buf.toString());
      ctx.returnBuffer();
    }
  }

  /**
   * Render an {@link Import}
   */
  private void renderImport(Import imp) throws LessException {
    Buffer buf = new Buffer(0);
    buf.append("@import ");
    ctx.render(buf, imp.path());
    Features features = imp.features();
    if (features != null && !features.isEmpty()) {
      buf.append(' ');
      ctx.render(buf, features);
    }
    model.value(buf.toString());
  }

  /**
   * Render an {@link ImportMarker}
   */
  private void renderImportMarker(ImportMarker marker) throws LessException {
    Import imp = marker.importStatement();
    String repr = imp.repr().trim();
    Path fileName = imp.fileName();
    String line = (fileName != null ? fileName.toString() : "") + ":" + (imp.lineOffset() + 1);
    if (marker.beginning()) {
      emitTrace("    start   " + repr + "    " + line + " ");
    } else {
      emitTrace("      end   " + repr + "    " + line + " ");
    }
  }

  /**
   * Render a {@link MixinMarker}
   */
  private void renderMixinMarker(MixinMarker marker) throws LessException {
    MixinCall call = marker.mixinCall();
    String repr = call.repr().trim();
    Path fileName = call.fileName();
    String line = (fileName != null ? fileName.toString() : "") + ":" + (call.lineOffset() + 1);
    if (marker.beginning()) {
      emitTrace("    start   " + repr + "    " + line + " ");
    } else {
      emitTrace("      end   " + repr + "    " + line + " ");
    }
  }

  /**
   * Render a rule, consisting of a property, value and optional "!important" modifier.
   */
  private void renderRule(Rule rule) throws LessException {
    emitWarnings("next rule", rule.warnings());
    if (opts.tracing()) {
      Path fileName = rule.fileName();
      String line = (fileName != null ? fileName.toString() : "") + ":" + (rule.lineOffset() + 1);
      emitTrace("next rule defined at '" + line + "'");
    }
    Buffer buf = ctx.acquireBuffer();
    ctx.render(buf, rule.property());
    buf.ruleSep();
    ctx.render(buf, rule.value());
    if (rule.important()) {
      buf.append(" !important");
    }
    model.value(buf.toString());
    ctx.returnBuffer();
  }

  /**
   * Emit a tracing comment.
   */
  private void emitTrace(String what) {
    model.comment("/* TRACE[" + (++traceId) + "]: " + what + " */\n");
  }

  /**
   * Emit a warning comment.
   */
  private void emitWarnings(String what, String warnings) {
    if (warnings != null) {
      // Build a comment containing all of the warnings.
      model.comment("/* WARNING[" + (++warningId) + "] raised evaluating " + what + ": " + warnings + " */\n");
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy