/* * Copyright [2014] [Christian Loehnert, krampenschiesser@gmail.com] * 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 de.ks.text; import com.google.common.base.Charsets; import com.google.common.io.Files; import org.apache.commons.lang3.StringUtils; import org.asciidoctor.Asciidoctor; import org.asciidoctor.AttributesBuilder; import org.asciidoctor.Options; import org.asciidoctor.OptionsBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.enterprise.inject.Vetoed; import java.io.File; import java.io.IOException; import java.net.URI; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @Vetoed public class AsciiDocParser { private static final Logger log = LoggerFactory.getLogger(AsciiDocParser.class); protected static final String mathJaxStart = " <script type=\"text/x-mathjax-config\">\n" + " MathJax.Hub.Config({\n" + " asciimath2jax: {\n" + " delimiters: [['`','`'], ['$$','$$'], ['||','||']]\n" + " }\n" + " });\n" + " </script>\n<script type=\"text/javascript\"\n" + " src=\""; protected static final String mathJaxEnd = "MathJax.js?config=AM_HTMLorMML-full\">\n" + "</script>"; private static final Pattern footerPattern = Pattern.compile("<div id=\"footer\">\n<div id=\"footer-text\">\n" + ".*\n" + "</div>\n</div>"); protected static final String fontlink = "<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400\">"; public static final String DATADIR_NAME = "_data"; protected final ThreadLocal<Asciidoctor> asciidoctor = ThreadLocal.withInitial(() -> { synchronized (AsciiDocParser.class) { return Asciidoctor.Factory.create(); } }); private final OptionsBuilder defaultOptions; private final Map<String, String> cssCache = new ConcurrentHashMap<>(); private final File dataDir; private final AsciiDocMetaData metaData = new AsciiDocMetaData(); public AsciiDocParser() { dataDir = new AsciiDocMetaData().disocverDataDir(); defaultOptions = getDefaultOptions(getDefaultAttributes()); asciidoctor.get(); } public AttributesBuilder getDefaultAttributes() { AttributesBuilder attributes = AttributesBuilder.attributes()// .experimental(true).sourceHighlighter("coderay").copyCss(true).stylesDir(dataDir.toURI().toString()); return attributes; } public OptionsBuilder getDefaultOptions(AttributesBuilder attributes) { return OptionsBuilder.options().headerFooter(true).backend(AsciiDocBackend.HTML5.name().toLowerCase(Locale.ROOT)).attributes(attributes.get()); } public String parse(String input) { try { String mathjaxDir = new File(dataDir, "mathjax").toURI().toString() + File.separator; return parse(input, true, true, mathjaxDir, defaultOptions); } catch (Exception e) { log.error("Got error", e); throw e; } } public String parse(String input, boolean removeFooter, boolean addMathJax, String mathjaxDir, OptionsBuilder options) { String render = asciidoctor.get().render(input, options); String backend = (String) options.asMap().get(Options.BACKEND); if (backend.equals(AsciiDocBackend.HTML5.name().toLowerCase(Locale.ROOT))) { if (removeFooter) { render = removeFooter(render); } if (addMathJax) { render = addMathJax(render, mathjaxDir); } render = removeFontLink(render); } return render; } public void renderToFile(String input, AsciiDocBackend backend, File file) { if (file.exists()) { log.info("Removing existing render target {}", file); file.delete(); } boolean isPdf = backend == AsciiDocBackend.PDF; if (isPdf) { AttributesBuilder attributes = getDefaultAttributes(); attributes.stylesDir(dataDir.getName()); attributes.tableOfContents(true); OptionsBuilder options = getDefaultOptions(attributes); options.backend(backend.name().toLowerCase(Locale.ROOT)); String adocFileName = StringUtils.replace(file.getName(), ".pdf", ".adoc"); File src = new File(file.getParent(), adocFileName); try { Files.write(input, src, Charsets.UTF_8); asciidoctor.get().convertFile(src, options); src.delete(); } catch (IOException e) { log.error("Could not write to file {}", src, e); throw new RuntimeException(e); } } else { File dataDir = createDataDir(file); String mathjaxDir = "./" + dataDir.getName() + "/" + AsciiDocMetaData.MATHJAX + "/"; boolean needsMathJax = needsMathJax(input); metaData.copyToDir(dataDir, needsMathJax); AttributesBuilder attributes = getDefaultAttributes(); attributes.stylesDir(dataDir.getName()); attributes.tableOfContents(true); OptionsBuilder options = getDefaultOptions(attributes); options.backend(backend.name().toLowerCase(Locale.ROOT)); String parse = parse(input, false, needsMathJax, mathjaxDir, options); try { parse = copyFiles(parse, dataDir); Files.write(parse, file, Charsets.UTF_8); } catch (IOException e) { log.error("Could not write to file {}", file, e); throw new RuntimeException(e); } } } private String copyFiles(String parse, File dataDir) throws IOException { Pattern pattern = Pattern.compile("\"file:.*\""); Matcher matcher = pattern.matcher(parse); int bodyTag = parse.indexOf("<body"); Map<String, String> replacements = new HashMap<>(); while (matcher.find()) { int start = matcher.start(); if (start < bodyTag) { continue; } int end = matcher.end(); String fileReference = parse.substring(start + 1, end - 1); end = fileReference.indexOf("\""); fileReference = fileReference.substring(0, end); log.debug("Found file reference {}", fileReference); URI uri = URI.create(fileReference.replace('\\', '/')); File sourceFile = new File(uri); File targetFile = new File(dataDir, sourceFile.getName()); java.nio.file.Files.copy(sourceFile.toPath(), targetFile.toPath()); replacements.put(fileReference, dataDir.getName() + "/" + targetFile.getName()); } for (Map.Entry<String, String> entry : replacements.entrySet()) { String original = entry.getKey(); String replacement = entry.getValue(); parse = StringUtils.replace(parse, original, replacement); } return parse; } protected boolean needsMathJax(String input) { if (input.contains("+++\n$$")) { return true; } else { return false; } } protected File createDataDir(File file) { String child = file.getName().contains(".") ? file.getName().substring(0, file.getName().lastIndexOf('.')) : file.getName(); File dataDir = new File(file.getParent(), child + DATADIR_NAME); log.debug("using target data dir {}", dataDir); if (!dataDir.exists()) { try { java.nio.file.Files.createDirectories(dataDir.toPath()); } catch (IOException e) { log.error("Could not create datadir {}", dataDir, e); throw new RuntimeException(e); } } return dataDir; } private String addMathJax(String render, String mathjaxDir) { int index = render.lastIndexOf("</head>"); if (index > 0) { String first = render.substring(0, index); String last = render.substring(index); return first + mathJaxStart + mathjaxDir + mathJaxEnd + last; } return render; } private String removeFooter(String render) { return footerPattern.matcher(render).replaceFirst(""); } private String removeFontLink(String render) { return StringUtils.remove(render, fontlink); } }