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

com.nitorcreations.dopeplugin.DopeMojo Maven / Gradle / Ivy

package com.nitorcreations.dopeplugin;

import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.imageio.ImageIO;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.WordUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.pdfbox.exceptions.COSVisitorException;
import org.apache.pdfbox.util.PDFMergerUtility;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.imgscalr.Scalr;
import org.pegdown.Extensions;
import org.pegdown.LinkRenderer;
import org.pegdown.PegDownProcessor;
import org.pegdown.ToHtmlSerializer;
import org.pegdown.ast.RootNode;
import org.pegdown.ast.VerbatimNode;
import org.python.util.PythonInterpreter;

import com.github.jarlakxen.embedphantomjs.ExecutionTimeout;
import com.github.jarlakxen.embedphantomjs.PhantomJSReference;
import com.github.jarlakxen.embedphantomjs.executor.PhantomJSFileExecutor;

@Mojo( name = "render", defaultPhase = LifecyclePhase.COMPILE )
public class DopeMojo extends AbstractMojo {
	@Parameter( defaultValue = "${project.build.directory}/classes/markdown", property = "markdownDir", required = true )
	private File markdownDirectory;

	@Parameter( defaultValue = "${project.build.directory}/classes/html", property = "htmlDir", required = true )
	private File htmlDirectory;

	private File htmlTemplate;
	private File titleTemplate;

	@Parameter( defaultValue = "${project.build.directory}/classes/slides", property = "buildDir", required = true )
	private File slidesDirectory;

	@Parameter( defaultValue = "${project.build.directory}/classes/slides-small", property = "buildDir", required = true )
	private File smallSlidesDirectory;

	@Parameter( defaultValue = "${project.build.directory}", property = "buildDir", required = true )
	private File buildDirectory;

	@Parameter( defaultValue = "${project.groupId}.css", property = "css", required = true )
	private String css;

	@Parameter( defaultValue = "${project.name}", property = "name", required = true )
	private String name;

	@Parameter( defaultValue = "${project}", required = true )
	private MavenProject project;

	@Parameter( defaultValue = "", property = "pngoptimizer" )
	private String pngoptimizer;

	@Parameter( defaultValue = "UTF-8", property = "charset" )
	private String charset;
	
	private static File renderScript;
	private static File videoPositionScript;

	ExecutorService service = Executors.newCachedThreadPool();

	static {
		try {
			renderScript = extractFile("render.js", ".js");
			videoPositionScript = extractFile("videoposition.js", ".js");
		} catch (IOException e) {
			throw new RuntimeException("Failed to create temporary resource", e);
		}
	}

	public final class RenderHtmlTask implements Callable {
		private final File out;
		private final Map htmls;
		private final Map notes;
		private final String markdown;
		private final boolean isSlide;
		private final String slideName;
		private final long lastModified;
		
		public final List> children;

		private RenderHtmlTask(File nextSource, Map htmls, Map notes, List> children) throws IOException {
			this(new String(Files.readAllBytes(Paths.get(nextSource.toURI())), Charset.defaultCharset()), 
					htmls, notes, children, nextSource.getName().endsWith(".md"), nextSource.getName().replaceAll("\\.md(\\.notes)?$", ""), nextSource.lastModified());
		}
		
		private RenderHtmlTask(String markdown, Map htmls, Map notes, 
 		         List> children, boolean isSlide, String slideName, long lastModified) {
			this.out = htmlDirectory;
			this.markdown = markdown;
			this.htmls = htmls;
			this.notes = notes;
			this.children = children;
			this.isSlide = isSlide;
			this.slideName = slideName;
			this.lastModified = lastModified;
		}

		public Throwable call() {
			try {
				PegDownProcessor processor = new PegDownProcessor(Extensions.AUTOLINKS + Extensions.TABLES + Extensions.FENCED_CODE_BLOCKS);

				if (isSlide) {
					File htmlFinal = new File(out, slideName + ".html");
					RootNode astRoot = processor.parseMarkdown(markdown.toCharArray());
					String nextHtml = new JHightlihtToHtmlSerializer().toHtml(astRoot);
					htmls.put(slideName, nextHtml);
					if (htmlFinal.exists()  && (htmlFinal.lastModified() >= lastModified)) {
						return null;
					}
					MergeHtml m = new MergeHtml(nextHtml, slideName, htmlFinal);
					m.merge();
					children.add(service.submit(new RenderPngPdfTask(htmlFinal, "png")));
					children.add(service.submit(new RenderPngPdfTask(htmlFinal, "pdf")));
					children.add(service.submit(new VideoPositionTask(htmlFinal)));
				} else {
					RootNode astRoot = processor.parseMarkdown(markdown.toCharArray());
					String nextHtml = new JHightlihtToHtmlSerializer().toHtml(astRoot);
					notes.put(slideName, nextHtml);
				}

			} catch (IOException e) {
				return e;
			}
			return null;
		}
	}

	public final class MergeHtml {
		private final String html;
		private final String slideName;
		private final String css;
		private final File out;
		private final File template;
		private final File htmlFinal;

		public MergeHtml(String html, String slideName, File htmlFinal) {
			this.html = html;
			this.slideName = slideName;
			this.css = DopeMojo.this.css;
			this.out = htmlDirectory;
			this.template = htmlTemplate;
			this.htmlFinal = htmlFinal;
		}

		public void merge() throws IOException {
			VelocityEngine ve = new VelocityEngine();
			ve.setProperty("resource.loader", "file");
			ve.setProperty("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.FileResourceLoader");
			ve.setProperty("file.resource.loader.path", "");
			ve.init();
			Template t = ve.getTemplate(template.getAbsolutePath());
			VelocityContext context = new VelocityContext();
			context.put("name", name);
			context.put("slideName", slideName);
			context.put("css", css);
			context.put("html", html);
			context.put("project", project);
			File nextOut = new File(out, slideName + ".html.tmp");
			FileWriter w = new FileWriter(nextOut);
			t.merge( context, w);
			w.flush();
			nextOut.renameTo(htmlFinal);
		}
	}

	public final class RenderPngPdfTask implements Callable {
		private final File slides;
		private final File smallSlides;
		private final File nextSource;
		private final String format;

		private RenderPngPdfTask(File nextSource, String format) {
			this.slides = slidesDirectory;
			this.smallSlides = smallSlidesDirectory;
			this.nextSource = nextSource;
			this.format = format;
		}

		@Override
		public Throwable call() {
			String slideName = nextSource.getName().substring(0, nextSource.getName().length() - 5);
			File outFolder;
			if ("png".equals(format)) {
				outFolder = slides;
			} else {
				outFolder = buildDirectory;
			}
			File nextPngPdf = new File(outFolder, slideName + ".tmp." + format);
			File finalPngPdf = new File(outFolder, slideName + "." + format);
			if (finalPngPdf.exists() && (finalPngPdf.lastModified() >= nextSource.lastModified())) {
				return null;
			}
			PhantomJSFileExecutor ex = new PhantomJSFileExecutor(PhantomJSReference.create().build(), new ExecutionTimeout(10, TimeUnit.SECONDS));
			String output;
			try {
				output = ex.execute(renderScript, nextSource.getAbsolutePath(), nextPngPdf.getAbsolutePath()).get();
			} catch (InterruptedException | ExecutionException e) {
				return e;
			}
			if (output.length() == 0) {
				nextPngPdf.renameTo(finalPngPdf);
				if ("png".equals(format)) {
					try {
						Future ob = service.submit(new OptimizePngTask(finalPngPdf));
						BufferedImage image = ImageIO.read(finalPngPdf);
						BufferedImage smallImage =
								Scalr.resize(image, Scalr.Method.QUALITY, Scalr.Mode.FIT_TO_WIDTH,
										960, 0, Scalr.OP_ANTIALIAS);
						File nextSmallPng = new File(smallSlides, finalPngPdf.getName() + ".tmp");
						File finalSmallPng = new File(smallSlides, finalPngPdf.getName());
						ImageIO.write(smallImage, "png", nextSmallPng);
						nextSmallPng.renameTo(finalSmallPng);
						Future os = service.submit(new OptimizePngTask(finalSmallPng));
						Throwable bt = ob.get();
						Throwable st = os.get();
						if (bt != null) {
							return bt;
						} else {
							return st;
						}
					} catch (IOException | InterruptedException | ExecutionException e) {
						return e;
					}
				}
			} else {
				return new Throwable(String.format("Failed to render %s '%s'.%s: %s", format, slideName, format, output));
			}
			return null;
		}
	}
	public final class VideoPositionTask implements Callable {
		private final File slides;
		private final File smallSlides;
		private final File nextSource;

		private VideoPositionTask(File nextSource) {
			this.slides = slidesDirectory;
			this.smallSlides = smallSlidesDirectory;
			this.nextSource = nextSource;
		}

		@Override
		public Throwable call() {
			String slideName = nextSource.getName().substring(0, nextSource.getName().length() - 5);
			File nextVideo = new File(slides, slideName + ".tmp.video");
			File finalVideo = new File(slides, slideName + ".video");
			File nextSmallVideo = new File(smallSlides, slideName + ".tmp.video");
			File finalSmallVideo = new File(smallSlides, slideName + ".video");
			if (finalVideo.exists() && (finalVideo.lastModified() >= nextSource.lastModified())) {
				return null;
			}
			PhantomJSFileExecutor ex = new PhantomJSFileExecutor(PhantomJSReference.create().build(), new ExecutionTimeout(10, TimeUnit.SECONDS));
			String output;
			try {
				output = ex.execute(videoPositionScript, nextSource.getAbsolutePath()).get();
			} catch (InterruptedException | ExecutionException e) {
				return e;
			}
			if (output.length() > 0) {
				try (FileOutputStream out = new FileOutputStream(nextVideo);
						FileOutputStream smallOut = new FileOutputStream(nextSmallVideo);
						) {
					out.write(output.getBytes(Charset.defaultCharset()));
					out.flush();
					smallOut.write(output.getBytes(Charset.defaultCharset()));
					smallOut.flush();
				} catch (IOException e) {
					return e;
				}
				nextVideo.renameTo(finalVideo);
				nextSmallVideo.renameTo(finalSmallVideo);
			}
			return null;
		}
	}

	public final class OptimizePngTask implements Callable {
		
		private final File png;

		public OptimizePngTask(File png) {
			this.png = png;
		}
		
		@Override
		public Throwable call() {
			if (pngoptimizer == null || pngoptimizer.length() == 0) {
				return null;
			}
			VelocityEngine ve = new VelocityEngine();
			ve.init();
			VelocityContext context = new VelocityContext();
			context.put("png", png.getAbsolutePath());
			context.put("project", project);
			
			StringWriter out = new StringWriter();
			if (!ve.evaluate(context, out, "png", pngoptimizer)) {
				return new RuntimeException("Failed to merge optimizer template");
			}
			try {
				List list = new ArrayList();
				Matcher m = Pattern.compile("([^\"]\\S*|\".+?\")\\s*").matcher(out.toString().trim());
				while (m.find()) {
				    list.add(m.group(1).replace("\"", ""));
				}
				Process optimize = new ProcessBuilder(list).redirectErrorStream(true).start();
				final InputStream is = optimize.getInputStream();
				Thread pump = new Thread() {
					public void start() {
						InputStreamReader isr = new InputStreamReader(is);
						BufferedReader br = new BufferedReader(isr);
						String line;
						try {
							while ((line = br.readLine()) != null) {
								getLog().debug(line);
							}
						} catch (IOException e) {
						}
					}
				};
				pump.start();
				if (optimize.waitFor() != 0) {
					return new RuntimeException("Failed to run optimizer - check debug output for why");
				}
				pump.interrupt();
			} catch (IOException | InterruptedException | IllegalThreadStateException e) {
				return e;
			}
			return null;
		}
		
	}
	public class IndexTemplateTask implements Callable {
		private final File nextIndex;
		private final Map htmls;
		private final Map notes;
		private final List slideNames;
		private final MavenProject project;

		private IndexTemplateTask(File nextIndex, Map htmls, Map notes, 
				List slideNames) {
			this.nextIndex = nextIndex;
			this.htmls = htmls;
			this.notes = notes;
			this.slideNames = slideNames;
			this.project = DopeMojo.this.project;
		}

		@Override
		public Throwable call() {
			VelocityEngine ve = new VelocityEngine();
			ve.setProperty("resource.loader", "file");
			ve.setProperty("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.FileResourceLoader");
			ve.setProperty("file.resource.loader.path", "");
			ve.init();
			Template t = ve.getTemplate(nextIndex.getAbsolutePath());
			VelocityContext context = new VelocityContext();
			context.put("name", name);
			context.put("htmls", htmls);
			context.put("notes", notes);
			context.put("slidenames", slideNames);
			context.put("project", project);
			context.put("css", css);
			File nextOut = new File(nextIndex.getParent(), nextIndex.getName() + ".tmp");
			try (FileWriter w = new FileWriter(nextOut)){
				t.merge( context, w);
				w.flush();
				nextOut.renameTo(nextIndex);
			} catch (IOException e) {
				return e;
			}
			return null;
		}
	}
	
	public final class TitleTemplateTask extends IndexTemplateTask {
		private final List> children;
		public TitleTemplateTask(List> children) {
			super(titleTemplate, null, null, null);
			this.children = children;
		}
		@Override
		public Throwable call() {
			Throwable superRes = super.call();
			if (superRes == null) {
				children.add(service.submit(new RenderPngPdfTask(titleTemplate, "png")));
				children.add(service.submit(new RenderPngPdfTask(titleTemplate, "pdf")));
				children.add(service.submit(new VideoPositionTask(titleTemplate)));
				return null;
			} else {
				return superRes;
			}
		}
	}

	private static File extractFile(String name, String suffix) throws IOException {
		File target = File.createTempFile(name.substring(0, name.length() - suffix.length()), suffix);
		target.deleteOnExit();
		try (FileOutputStream outStream = new FileOutputStream(target); 
				InputStream inStream = 
						DopeMojo.class.getClassLoader().getResourceAsStream(name)) {
			IOUtils.copy(inStream, outStream);
			return target;
		}
	}

	public void execute() throws MojoExecutionException {
		File f = markdownDirectory;
		htmlTemplate = new File(htmlDirectory, "slidetemplate.html");
		titleTemplate = new File(htmlDirectory, "title.html");
		
		getLog().debug(String.format("Markdown from %s", f.getAbsolutePath()));
		if ( !f.exists() ) {
			return;
		}
		getLog().debug(String.format("HTML to %s", htmlDirectory.getAbsolutePath()));
		ensureDir(htmlDirectory);
		getLog().debug(String.format("Slides to %s", slidesDirectory.getAbsolutePath()));
		ensureDir(slidesDirectory);
		getLog().debug(String.format("Small slides to %s", smallSlidesDirectory.getAbsolutePath()));
		ensureDir(smallSlidesDirectory);
		final File[] sources = f.listFiles(new FilenameFilter() {
			public boolean accept(File dir, String name) {
				return name.endsWith(".md") || name.endsWith(".md.notes");
			}
		});
		getLog().info(String.format("Processing %d markdown files", sources.length));
		final Map notes = new ConcurrentHashMap<>();
		final Map htmls = new ConcurrentHashMap<>();
		final List> execs = new ArrayList<>();
		final List> children = new CopyOnWriteArrayList<>();
		final ArrayList slideNames = new ArrayList<>();
		for (int i=0; i -1) {
					String slideId = slideName + "$" + index;
					if (nextIsSlide) {
						slideNames.add(slideId);
						index++;
					} else {
						slideId = slideName + "$" + (index-1);
					}
					execs.add(service.submit(new RenderHtmlTask(nextMarkdown.substring(slideStart, nextStart), htmls, notes, children, nextIsSlide, slideId, nextSource.lastModified())));
					nextIsSlide = !nextMarkdown.regionMatches(nextStart, "