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

wrm.AbstractSassMojo Maven / Gradle / Ivy

The newest version!
package wrm;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.project.MavenProject;
import org.sonatype.plexus.build.incremental.BuildContext;

import io.bit3.jsass.CompilationException;
import io.bit3.jsass.Output;
import wrm.libsass.SassCompiler;

public abstract class AbstractSassMojo extends AbstractMojo {

	/**
	 * The directory in which the compiled CSS files will be placed. The default value is
	 * ${project.build.directory}
	 *
	 * @parameter property="project.build.directory"
	 * @required
	 */
	protected File outputPath;
	/**
	 * The directory from which the source .scss files will be read. This directory will be
	 * traversed recursively, and all .scss files found in this directory or subdirectories
	 * will be compiled. The default value is src/main/sass
	 *
	 * @parameter default-value="src/main/sass"
	 */
	protected String inputPath;
	/**
	 * Additional include path, ';'-separated. The default value is null
	 *
	 * @parameter
	 */
	protected String includePath;
	/**
	 * Output style for the generated css code. One of nested, expanded,
	 * compact, compressed. Note that as of libsass 3.1, expanded
	 * and compact are the same as nested. The default value is
	 * nested.
	 *
	 * @parameter default-value="nested"
	 */
	private SassCompiler.OutputStyle outputStyle;
	/**
	 * Emit comments in the compiled CSS indicating the corresponding source line. The default
	 * value is false
	 *
	 * @parameter default-value="false"
	 */
	private boolean generateSourceComments;
	/**
	 * Generate source map files. The generated source map files will be placed in the directory
	 * specified by sourceMapOutputPath. The default value is true.
	 *
	 * @parameter default-value="true"
	 */
	private boolean generateSourceMap;
	/**
	 * The directory in which the source map files that correspond to the compiled CSS will be
	 * placed. The default value is ${project.build.directory}
	 *
	 * @parameter property="project.build.directory"
	 */
	private String sourceMapOutputPath;
	/**
	 * Prevents the generation of the sourceMappingURL special comment as the last
	 * line of the compiled CSS. The default value is false.
	 *
	 * @parameter default-value="false"
	 */
	private boolean omitSourceMapingURL;
	/**
	 * Embeds the whole source map data directly into the compiled CSS file by transforming
	 * sourceMappingURL into a data URI. The default value is false.
	 *
	 * @parameter default-value="false"
	 */
	private boolean embedSourceMapInCSS;
	/**
	 * Embeds the contents of the source .scss files in the source map file instead of the
	 * paths to those files. The default value is false
	 *
	 * @parameter default-value="false"
	 */
	private boolean embedSourceContentsInSourceMap;
	/**
	 * Switches the input syntax used by the files to either sass or scss.
	 * The default value is scss.
	 *
	 * @parameter default-value="scss"
	 */
	private SassCompiler.InputSyntax inputSyntax;
	/**
	 * Precision for fractional numbers. The default value is 5.
	 *
	 * @parameter default-value="5"
	 */
	private int precision;
	/**
	 * Enables classpath aware importer which make possible to @import
	 * files from classpath and WebJars.
	 *
	 * @parameter default-value="false"
	 */
	private boolean enableClasspathAwareImporter;
	/**
	 * should fail the build in case of compilation errors.
	 *
	 * @parameter default-value="true"
	 */
	protected boolean failOnError;
	
    /**
     * Copy source files to output directory.
     *
     * @parameter default-value="false"
     */
    private boolean copySourceToOutput;

    /**
	 * @parameter property="project"
	 * @required
	 * @readonly
	 */
	protected MavenProject project;

	/**
	 * @component
	 */
	protected BuildContext buildContext;

	protected SassCompiler compiler;

	private static final Pattern PATTERN_ERROR_JSON_LINE = Pattern.compile("[\"']line[\"'][:\\s]+([0-9]+)");
	private static final Pattern PATTERN_ERROR_JSON_COLUMN = Pattern.compile("[\"']column[\"'][:\\s]+([0-9]+)");

	public AbstractSassMojo() {
		super();
	}

	protected void compile() throws Exception {
		final Path root = project.getBasedir().toPath().resolve(Paths.get(inputPath));
		String fileExt = getFileExtension();
		String globPattern = "glob:{**/,}*."+fileExt;
		getLog().debug("Glob = " + globPattern);
	
		final PathMatcher matcher = FileSystems.getDefault().getPathMatcher(globPattern);
		final AtomicInteger errorCount = new AtomicInteger(0);
		final AtomicInteger fileCount = new AtomicInteger(0);
		Files.walkFileTree(root, new SimpleFileVisitor() {
			@Override
			public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
				if (matcher.matches(file) && !file.getFileName().toString().startsWith("_")) {
					fileCount.incrementAndGet();
					if(!processFile(root, file)){
						errorCount.incrementAndGet();
					}
				}
	
				return FileVisitResult.CONTINUE;
			}
	
			@Override
			public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
				return FileVisitResult.CONTINUE;
			}
		});
	
		getLog().info("Compiled " + fileCount + " files");
		if (errorCount.get() > 0) {
			if (failOnError) {
				throw new Exception("Failed with " + errorCount.get() + " errors");
			} else {
				getLog().error("Failed with " + errorCount.get() + " errors. Continuing due to failOnError=false.");
			}
		}
	}

	protected String getFileExtension() {
		return inputSyntax.toString();
	}

	protected void validateConfig() {
		if (!generateSourceMap) {
			if (embedSourceMapInCSS) {
				getLog().warn("embedSourceMapInCSS=true is ignored. Cause: generateSourceMap=false");
			}
			if (embedSourceContentsInSourceMap) {
				getLog().warn("embedSourceContentsInSourceMap=true is ignored. Cause: generateSourceMap=false");
			}
		}
		if (outputStyle != SassCompiler.OutputStyle.compressed && outputStyle != SassCompiler.OutputStyle.nested) {
			getLog().warn("outputStyle=" + outputStyle + " is replaced by nested. Cause: libsass 3.1 only supports compressed and nested");
		}
	}

	private void setCompileClasspath() {
		try {
			Set urls = new HashSet<>();
			List elements = project.getCompileClasspathElements();
			for (String element : elements) {
				urls.add(new File(element).toURI().toURL());
			}

			ClassLoader contextClassLoader = URLClassLoader.newInstance(
					urls.toArray(new URL[0]),
					Thread.currentThread().getContextClassLoader());

			Thread.currentThread().setContextClassLoader(contextClassLoader);

		} catch (DependencyResolutionRequiredException e) {
			throw new RuntimeException(e);
		} catch (MalformedURLException e) {
			throw new RuntimeException(e);
		}
	}

	protected SassCompiler initCompiler() {
		setCompileClasspath();
		
		SassCompiler compiler = new SassCompiler();
		compiler.setEmbedSourceMapInCSS(this.embedSourceMapInCSS);
		compiler.setEmbedSourceContentsInSourceMap(this.embedSourceContentsInSourceMap);
		compiler.setGenerateSourceComments(this.generateSourceComments);
		compiler.setGenerateSourceMap(this.generateSourceMap);
		compiler.setIncludePaths(this.includePath);
		compiler.setInputSyntax(this.inputSyntax);
		compiler.setOmitSourceMappingURL(this.omitSourceMapingURL);
		compiler.setOutputStyle(this.outputStyle);
		compiler.setPrecision(this.precision);
		compiler.setEnableClasspathAwareImporter(this.enableClasspathAwareImporter);
		return compiler;
	}

	protected boolean processFile(Path inputRootPath, Path inputFilePath) throws IOException {
		getLog().debug("Processing File " + inputFilePath);
	
		Path relativeInputPath = inputRootPath.relativize(inputFilePath);
	
		Path outputRootPath = this.outputPath.toPath();
		Path outputFilePath = outputRootPath.resolve(relativeInputPath);
		String fileExtension = getFileExtension();
		outputFilePath = Paths.get(outputFilePath.toAbsolutePath().toString().replaceFirst("\\."+fileExtension+"$", ".css"));
	
		Path sourceMapRootPath = Paths.get(this.sourceMapOutputPath);
		Path sourceMapOutputPath = sourceMapRootPath.resolve(relativeInputPath);
		sourceMapOutputPath = Paths.get(sourceMapOutputPath.toAbsolutePath().toString().replaceFirst("\\.scss$", ".css.map"));
	
		if (copySourceToOutput) {
			Path inputOutputPath = outputRootPath.resolve(relativeInputPath);
			inputOutputPath.toFile().mkdirs();
			Files.copy(inputFilePath, inputOutputPath, REPLACE_EXISTING);
			buildContext.refresh(inputOutputPath.toFile());
			inputFilePath = inputOutputPath;
		}
		
		
		Output out;
		try {
			out = compiler.compileFile(
					inputFilePath.toAbsolutePath().toString(),
					outputFilePath.toAbsolutePath().toString(),
					sourceMapOutputPath.toAbsolutePath().toString()
			);
		}
		catch (CompilationException e) {
			getLog().error(e.getMessage());
			getLog().debug(e);

			// we need this info from json:
			// "line": 4,
			// "column": 1,
			// - a full blown parser for this would probably be an overkill, let's just regex
			String errorJson = e.getErrorJson();
			int line = 0, column = 0;
			if (errorJson != null) { // defensive, in case we don't always get it
				Matcher lineMatcher = PATTERN_ERROR_JSON_LINE.matcher(errorJson);
				if (lineMatcher.find())
					try {
						line = Integer.parseInt(lineMatcher.group(1));
					} catch (IndexOutOfBoundsException | NumberFormatException e1) { // in case regex doesn't cut it anymore
						getLog().error("Failed to parse error json line: " + e1.getMessage());
						getLog().debug(e1);
					}
				Matcher columnMatcher = PATTERN_ERROR_JSON_COLUMN.matcher(errorJson);
				if (columnMatcher.find())
					try {
						column = Integer.parseInt(columnMatcher.group(1));
					} catch (IndexOutOfBoundsException | NumberFormatException e1) { // in case regex doesn't cut it anymore
						getLog().error("Failed to parse error json column: " + e1.getMessage());
						getLog().debug(e1);
					}
			}
			buildContext.addMessage(inputFilePath.toFile(), line, column, e.getErrorMessage(), BuildContext.SEVERITY_ERROR, e);

			return false;
		}
	
		getLog().debug("Compilation finished.");
	
		writeContentToFile(outputFilePath, out.getCss());
		if (out.getSourceMap() != null) {
			writeContentToFile(sourceMapOutputPath, out.getSourceMap());
		}
		return true;
	}

	private void writeContentToFile(Path outputFilePath, String content) throws IOException {
		File f = outputFilePath.toFile();
		f.getParentFile().mkdirs();
		f.createNewFile();
		OutputStreamWriter os = null;
		try{
			os = new OutputStreamWriter(new FileOutputStream(f), "UTF-8");
			os.write(content);
			os.flush();
		} finally {
			if (os != null)
				os.close();
		}
		buildContext.refresh(outputFilePath.toFile());
		getLog().debug("Written to: " + f);
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy