com.squarespace.less.parse.LessParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of less-core Show documentation
Show all versions of less-core Show documentation
Less compiler in Java, based on less.js
/**
* Copyright, 2015, 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.parse;
import static com.squarespace.less.core.SyntaxErrorMaker.recursiveImport;
import static com.squarespace.less.parse.PrimaryParselet.evaluateImport;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.squarespace.less.LessContext;
import com.squarespace.less.LessException;
import com.squarespace.less.core.FlexList;
import com.squarespace.less.exec.ExecEnv;
import com.squarespace.less.model.Block;
import com.squarespace.less.model.Import;
import com.squarespace.less.model.Node;
import com.squarespace.less.model.Stylesheet;
/**
* Entry point for all LESS stylesheet parsing.
*/
public class LessParser {
/**
* Context for this parse.
*/
private final LessContext context;
/**
* Maintains a stack of streams during the parse.
*/
private final FlexList streams = new FlexList<>();
/**
* List of stream paths currently being parsed.
*/
private final Set streamPaths = new HashSet<>();
/**
* Root block that this parser is populating.
*/
private final Block rootBlock;
/**
* List of deferred blocks / closures for later evaluation.
*/
private final List deferreds = new ArrayList<>();
/**
* Execution environment captured during the parse. It tracks
* the current stack of all entered blocks at each point in
* the parse. This is used to save a closure around any
* block which requires deferred evaluation.
*/
private final ExecEnv parseEnv;
/**
* Construct a parser with the given context.
*/
public LessParser(LessContext context) {
this.context = context;
this.rootBlock = new Block();
this.parseEnv = new ExecEnv(context);
}
/**
* Returns a stylesheet wrapping the parser's root block.
*/
public Stylesheet stylesheet() {
return context.nodeBuilder().buildStylesheet(rootBlock);
}
/**
* Returns the context associated with this parser instance.
*/
public LessContext context() {
return this.context;
}
/**
* Top-level parse entry point. Parses the given string and file path
* and appends it to the current global block.
*/
public void parse(String raw, Path filePath) throws LessException {
LessStream stream = push(raw, filePath, parseEnv);
Block block = (Block)stream.parse(Parselets.PRIMARY);
// Ensure the stream was completely consumed by the parser.
stream.checkComplete();
pop();
// Evaluate all deferred blocks.
evaluateDeferred();
// Append the parsed block to the global block. This allows
// more than one independent parse populate the same global
// stylesheet.
rootBlock.appendBlock(block);
}
/**
* Push a stream onto the stack, typically to process an {@link Import} statement.
*/
public LessStream push(String raw, Path filePath, ExecEnv env) throws LessException {
// Make sure we're not recursing through the same import file.
if (this.streamPaths.contains(filePath)) {
LessStream current = this.streams.last();
throw current.parseError(new LessException(recursiveImport(filePath)));
}
this.streamPaths.add(filePath);
LessStream stream = null;
stream = new LessStream(this, raw, filePath, env);
this.streams.push(stream);
// TODO: in the future we may want enforce some limit on maximum import depth.
return stream;
}
/**
* Pops the current stream.
*/
public LessStream pop() {
LessStream stream = this.streams.pop();
this.streamPaths.remove(stream.path());
List envs = stream.deferreds();
if (!envs.isEmpty()) {
deferreds.add(new Deferred(envs, stream.raw()));
}
return stream;
}
/**
* Carry out evaluation of parsed blocks with one or more children
* which require evaluation. These evaluations have been deferred.
*
* There are 2 types of imports encountered while parsing a stream:
* 1. static path
* 2. interpolated path
*
* Imports with static paths can be processed immediately during the
* parse.
*
* Imports whose paths require variable interpolation need
* to be deferred until the parse completes, since the variables they
* reference may not have been parsed yet.
*/
private void evaluateDeferred() throws LessException {
while (!deferreds.isEmpty()) {
List processing = new ArrayList<>(deferreds);
deferreds.clear();
for (Deferred deferred : processing) {
for (ExecEnv env : deferred.envs) {
evaluateDeferredClosure(deferred, env);
}
}
}
}
/**
* Evaluate a deferred block against its closure. The closure is
* the stack captured at the time the block was parsed.
*/
private void evaluateDeferredClosure(Deferred deferred, ExecEnv env) throws LessException {
// Get the block at the top of the closure stack.
Block block = env.frames().last();
// Clear the deferred evaluation flag. This is because we will be
// adding new imports to this block which may require variable interpolation.
// Clearing the flag ensures that, if any interpolated imports are present,
// this block will be deferred for evaluation again.
block.clearDeferred();
// Iterate over all rules in the block looking for nodes which require evaluation.
FlexList rules = block.rules();
for (int i = 0; i < rules.size(); i++) {
Node node = rules.get(i);
// Handle imports which require variable interpolation.
if (node instanceof Import) {
Import oldImport = (Import)node;
// Evaluate the import's path against the closure and perform the import.
Import newImport = new Import(oldImport.path().eval(env), oldImport.features(), oldImport.once());
newImport.rootPath(oldImport.rootPath());
newImport.fileName(oldImport.fileName());
// Store all imported rules on a temporary block.
Block tempBlock = new Block();
try {
// Perform the import. This will append all imported rules to the
// temporary block, returning true if the import was processed.
if (evaluateImport(context.importer(), this, env, tempBlock, newImport)) {
// Splice imported rules into block, replacing the import node.
i += block.splice(i, 1, tempBlock) - 1;
// Ensure the variable cache gets rebuilt, so any variable lookups
// see definitions added by the import.
block.resetVariableCache();
block.orFlags(tempBlock);
}
} catch (LessException e) {
throw ParseUtils.parseError(e, newImport.fileName(), deferred.raw, newImport.parseOffset());
}
}
}
}
/**
* Captures a list of closures for later evaluation
*/
private static class Deferred {
/**
* The closures captured and deferred during the parse.
*/
private final List envs;
/**
* Input source for the active stream when this closure was deferred.
*/
private final String raw;
public Deferred(List envs, String raw) {
this.envs = envs;
this.raw = raw;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy