com.squarespace.less.exec.LessRenderer Maven / Gradle / Ivy
/**
* 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.core.LessInternalException;
import com.squarespace.less.model.Block;
import com.squarespace.less.model.BlockDirective;
import com.squarespace.less.model.BlockNode;
import com.squarespace.less.model.Comment;
import com.squarespace.less.model.Definition;
import com.squarespace.less.model.DetachedRuleset;
import com.squarespace.less.model.Directive;
import com.squarespace.less.model.ExtendList;
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 CSS output.
*/
public class LessRenderer {
/**
* 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;
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);
}
/**
* Shortcut to render a stylesheet against the given context.
*/
public static String render(LessContext context, Stylesheet sheet) throws LessException {
LessRenderer renderer = new LessRenderer(context, sheet);
return renderer.render();
}
/**
* Render the {@link Stylesheet} to the {@link CssModel} and return the
* rendered output.
*/
private String render() throws LessException {
indexExtends(stylesheet);
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();
return model.render();
}
/**
* Scan the stylesheet for extends and index them.
*/
private void indexExtends(BlockNode blockNode) throws LessException {
env.push(blockNode);
// If one of the selectors has an extend list, index it.
if (blockNode instanceof Ruleset) {
Selectors selectors = env.frame().selectors();
if (selectors.hasExtend()) {
for (Selector selector : selectors.selectors()) {
if (selector.hasExtend()) {
env.indexSelector(selector);
}
}
}
}
// Iterate looking for rule-level extends and other block nodes.
// We use the flags to try to avoid scanning blocks unnecessarily.
if (canIndex(blockNode)) {
Block block = blockNode.block();
FlexList rules = block.rules();
int size = rules.size();
for (int i = 0; i < size; i++) {
Node node = rules.get(i);
if (node instanceof BlockNode) {
// Recurse into the block.
indexExtends((BlockNode)node);
} else if (node instanceof ExtendList) {
// Index the rule-level extend.
env.indexSelector(env.frame().selectors(), (ExtendList)node);
}
}
}
env.pop();
}
/**
* The switch below defines a type white list and criteria for entry of a
* block node for purposes of indexing EXTEND lists.
*
* When indexing extends we must ignore block nodes within the tree which
* are not intended to be rendered. A MIXIN definition exists to be
* resolved and invoked by a MIXIN_CALL, but the definition itself is
* never rendered into CSS.
*/
private boolean canIndex(BlockNode blockNode) {
switch (blockNode.type()) {
case BLOCK_DIRECTIVE:
case DETACHED_RULESET:
case MEDIA:
case RULESET:
case STYLESHEET:
Block block = blockNode.block();
return block.hasNestedBlock() || block.hasNestedExtend();
default:
break;
}
return false;
}
/**
* Render a {@link Ruleset}
*/
private void renderRuleset(Ruleset ruleset) throws LessException {
Block block = ruleset.block();
// Skip rulesets that exist solely for extension. No sense doing
// more work than we need to.
if (block.rules().isEmpty()) {
return;
}
env.push(ruleset);
model.push(NodeType.RULESET);
Selectors selectors = env.frame().selectors();
if (!selectors.isEmpty()) {
// Selectors are indented and delimited by the model. We render
// them to this temporary buffer and add them to the model.
Buffer buf = ctx.acquireBuffer();
for (Selector selector : selectors.selectors()) {
NodeRenderer.render(buf, selector);
model.header(buf.toString());
buf.reset();
}
// Try matching each selector against the extend indices. This
// will return a list of generated selectors.
List extended = env.extend(selectors);
if (extended != null) {
for (Selector selector : extended) {
NodeRenderer.render(buf, selector);
model.header(buf.toString());
buf.reset();
}
}
ctx.returnBuffer();
}
renderBlock(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);
if (node instanceof Import) {
renderImport((Import)node);
}
}
}
/**
* Render all children for the given {@link Block}, optionally including
* imports.
*/
private void renderBlock(Block block, boolean includeImports) throws LessException {
LessBlockRuleMerger ruleMerger = block.hasPropertyMergeModes() ? new LessBlockRuleMerger(ctx) : null;
FlexList rules = block.rules();
int size = rules.size();
for (int i = 0; i < size; i++) {
Node node = rules.get(i);
switch (node.type()) {
case BLOCK:
renderBlock((Block)node, includeImports);
break;
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 DETACHED_RULESET:
{
DetachedRuleset ruleset = (DetachedRuleset)node;
renderBlock(ruleset.block(), includeImports);
break;
}
case DIRECTIVE:
{
Directive directive = (Directive)node;
if (!directive.name().equals("@charset")) {
model.value(ctx.render(directive));
}
break;
}
case DUMMY:
case EXTEND_LIST:
{
// No visible representation. Ignore.
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:
if (ruleMerger == null) {
renderRule((Rule)node);
} else {
ruleMerger.add((Rule)node);
}
break;
case RULESET:
renderRuleset((Ruleset)node);
break;
default:
throw new LessInternalException("Unhandled node: " + node.type());
}
}
// If rule merging was in effect, we need to render all rules here.
if (ruleMerger != null) {
for (Rule rule : ruleMerger.rules()) {
renderRule(rule);
}
}
}
/**
* 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, 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 ");
NodeRenderer.render(buf, imp.path());
Features features = imp.features();
if (features != null && !features.isEmpty()) {
buf.append(' ');
NodeRenderer.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();
NodeRenderer.render(buf, rule.property());
buf.ruleSep();
NodeRenderer.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 - 2025 Weber Informatics LLC | Privacy Policy