/*
documentr - Edit, maintain, and present software documentation on the web.
Copyright (C) 2012-2013 Maik Schreiber
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.blizzy.documentr.markdown;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import org.apache.commons.lang3.StringUtils;
import org.parboiled.Parboiled;
import org.pegdown.DocumentrParser;
import org.pegdown.Parser;
import org.pegdown.PegDownProcessor;
import org.pegdown.ast.Node;
import org.pegdown.ast.ParaNode;
import org.pegdown.ast.RootNode;
import org.pegdown.ast.SuperNode;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import com.google.common.collect.Lists;
import de.blizzy.documentr.markdown.macro.IMacro;
import de.blizzy.documentr.markdown.macro.IMacroDescriptor;
import de.blizzy.documentr.markdown.macro.IMacroRunnable;
import de.blizzy.documentr.markdown.macro.MacroFactory;
import de.blizzy.documentr.markdown.macro.impl.UnknownMacroMacro;
import de.blizzy.documentr.page.IPageStore;
import de.blizzy.documentr.system.SystemSettingsStore;
import de.blizzy.documentr.util.Replacement;
@Component
public class MarkdownProcessor {
static final String NON_CACHEABLE_MACRO_MARKER = MarkdownProcessor.class.getName() + "_NON_CACHEABLE_MACRO"; //$NON-NLS-1$
static final String NON_CACHEABLE_MACRO_BODY_MARKER = MarkdownProcessor.class.getName() + "_NON_CACHEABLE_MACRO_BODY"; //$NON-NLS-1$
private static final String TEXT_RANGE_RE = "data-text-range=\"[0-9]+,[0-9]+\""; //$NON-NLS-1$
@SuppressWarnings("nls")
private static final List<Replacement> CLEANUP = Lists.newArrayList(
Replacement.dotAllNoCase("<p( " + TEXT_RANGE_RE + ")?><div(.*?</div>.*?)</p>", "<div$1$2"),
Replacement.dotAllNoCase("<p( " + TEXT_RANGE_RE + ")?><ul(.*?</ul>.*?)</p>", "<ul$1$2"),
Replacement.dotAllNoCase("<p( " + TEXT_RANGE_RE + ")?><ol(.*?</ol>.*?)</p>", "<ol$1$2"),
Replacement.dotAllNoCase("<p( " + TEXT_RANGE_RE + ")?><pre(.*?</pre>.*?)</p>", "<pre$1$2"),
Replacement.dotAllNoCase("<p( " + TEXT_RANGE_RE + ")?><span(.*?)><div(.*?</div>.*?</span>)</p>", "<span$2><div$1$3"),
Replacement.dotAllNoCase("<p( " + TEXT_RANGE_RE + ")?><span(.*?)><ul(.*?</ul>.*?</span>)</p>", "<span$2><ul$1$3"),
Replacement.dotAllNoCase("<p( " + TEXT_RANGE_RE + ")?><span(.*?)><ol(.*?</ol>.*?</span>)</p>", "<span$2><ol$1$3"),
Replacement.dotAllNoCase("<p( " + TEXT_RANGE_RE + ")?><span(.*?)><pre(.*?</pre>.*?</span>)</p>", "<span$2><pre$1$3"),
Replacement.dotAllNoCase("<p[^>]*>[ \\t\\r\\n]*</p>", StringUtils.EMPTY),
Replacement.dotAllNoCase("(<p[^>]*>)(?:<br/>)+", "$1"),
Replacement.dotAllNoCase("(?:<br/>)+</p>", "</p>"),
Replacement.dotAllNoCase(
"(<li class=\"span3\"><a class=\"thumbnail\" (?:[^>]+)>" +
"<img (?:[^>]+)/></a></li>)</ul>(?:[ \t]|<br/>)*" +
"<ul class=\"thumbnails\">(<li class=\"span3\">" +
"<a class=\"thumbnail\" (?:[^>]+)>)",
"$1$2")
);
@Autowired
private MacroFactory macroFactory;
@Autowired
private BeanFactory beanFactory;
@Autowired
private IPageStore pageStore;
@Autowired
private SystemSettingsStore systemSettingsStore;
public String markdownToHtml(String markdown, String projectName, String branchName, String path,
Authentication authentication, Locale locale, String contextPath) {
return markdownToHtml(markdown, projectName, branchName, path, authentication, locale, true, contextPath);
}
public String markdownToHtml(String markdown, String projectName, String branchName, String path,
Authentication authentication, Locale locale, boolean nonCacheableMacros, String contextPath) {
RootNode rootNode = parse(markdown);
removeHeader(rootNode);
return markdownToHtml(rootNode, projectName, branchName, path, authentication, locale, nonCacheableMacros, contextPath);
}
public String headerMarkdownToHtml(String markdown, String projectName, String branchName, String path,
Authentication authentication, Locale locale, String contextPath) {
RootNode rootNode = parse(markdown);
extractHeader(rootNode);
return markdownToHtml(rootNode, projectName, branchName, path, authentication, locale, true, contextPath);
}
private RootNode parse(String markdown) {
Parser parser = Parboiled.createParser(DocumentrParser.class);
PegDownProcessor proc = new PegDownProcessor(parser);
RootNode rootNode = proc.parseMarkdown(markdown.toCharArray());
fixParaNodes(rootNode);
return rootNode;
}
private String markdownToHtml(RootNode rootNode, String projectName, String branchName, String path,
Authentication authentication, Locale locale, boolean nonCacheableMacros, String contextPath) {
HtmlSerializerContext context = new HtmlSerializerContext(projectName, branchName, path, this, authentication, locale,
pageStore, systemSettingsStore, contextPath);
HtmlSerializer serializer = new HtmlSerializer(context);
String html = serializer.toHtml(rootNode);
List<MacroInvocation> macroInvocations = Lists.newArrayList(context.getMacroInvocations());
// reverse order so that inner invocations will be processed before outer
Collections.reverse(macroInvocations);
int nonCacheableMacroIdx = 1;
for (MacroInvocation invocation : macroInvocations) {
IMacro macro = macroFactory.get(invocation.getMacroName());
if (macro == null) {
macro = new UnknownMacroMacro();
}
IMacroDescriptor macroDescriptor = macro.getDescriptor();
String startMarker = invocation.getStartMarker();
String endMarker = invocation.getEndMarker();
String body = StringUtils.substringBetween(html, startMarker, endMarker);
if (macroDescriptor.isCacheable()) {
MacroContext macroContext = MacroContext.create(invocation.getMacroName(), invocation.getParameters(),
body, context, locale, beanFactory);
IMacroRunnable macroRunnable = macro.createRunnable();
String macroHtml = StringUtils.defaultString(macroRunnable.getHtml(macroContext));
html = StringUtils.replace(html, startMarker + body + endMarker, macroHtml);
} else if (nonCacheableMacros) {
String macroName = invocation.getMacroName();
String params = invocation.getParameters();
String idx = String.valueOf(nonCacheableMacroIdx++);
html = StringUtils.replace(html, startMarker + body + endMarker,
"__" + NON_CACHEABLE_MACRO_MARKER + "_" + idx + "__" + //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
macroName + " " + StringUtils.defaultString(params) + //$NON-NLS-1$
"__" + NON_CACHEABLE_MACRO_BODY_MARKER + "__" + //$NON-NLS-1$ //$NON-NLS-2$
body +
"__/" + NON_CACHEABLE_MACRO_MARKER + "_" + idx + "__"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
} else {
html = StringUtils.replace(html, startMarker + body + endMarker, StringUtils.EMPTY);
}
}
html = cleanupHtml(html, macroInvocations, true);
return html;
}
private void fixParaNodes(Node node) {
if ((node instanceof MacroNode) || (node instanceof PageHeaderNode)) {
List<Node> children = ((SuperNode) node).getChildren();
if ((children.size() == 1) && (children.get(0) instanceof ParaNode)) {
List<Node> newChildren = ((ParaNode) children.get(0)).getChildren();
children.clear();
children.addAll(newChildren);
}
}
if (node instanceof SuperNode) {
for (Node child : ((SuperNode) node).getChildren()) {
fixParaNodes(child);
}
}
}
private void extractHeader(RootNode rootNode) {
List<Node> children = rootNode.getChildren();
PageHeaderNode headerNode = findHeaderNode(rootNode);
children.clear();
if (headerNode != null) {
children.addAll(headerNode.getChildren());
}
}
private PageHeaderNode findHeaderNode(Node node) {
if (node instanceof PageHeaderNode) {
return (PageHeaderNode) node;
}
if (node instanceof SuperNode) {
for (Node child : ((SuperNode) node).getChildren()) {
PageHeaderNode headerNode = findHeaderNode(child);
if (headerNode != null) {
return headerNode;
}
}
}
return null;
}
private void removeHeader(Node node) {
if (node instanceof SuperNode) {
List<Node> children = ((SuperNode) node).getChildren();
for (Iterator<Node> iter = children.iterator(); iter.hasNext();) {
Node child = iter.next();
if (child instanceof PageHeaderNode) {
iter.remove();
}
}
for (Node child : children) {
removeHeader(child);
}
}
}
public String processNonCacheableMacros(String html, String projectName, String branchName, String path,
Authentication authentication, Locale locale, String contextPath) {
HtmlSerializerContext context = new HtmlSerializerContext(projectName, branchName, path, this, authentication, locale,
pageStore, systemSettingsStore, contextPath);
String startMarkerPrefix = "__" + NON_CACHEABLE_MACRO_MARKER + "_"; //$NON-NLS-1$ //$NON-NLS-2$
String endMarkerPrefix = "__/" + NON_CACHEABLE_MACRO_MARKER + "_"; //$NON-NLS-1$ //$NON-NLS-2$
String bodyMarker = "__" + NON_CACHEABLE_MACRO_BODY_MARKER + "__"; //$NON-NLS-1$ //$NON-NLS-2$
for (;;) {
int start = html.indexOf(startMarkerPrefix);
if (start < 0) {
break;
}
start += startMarkerPrefix.length();
int end = html.indexOf('_', start);
if (end < 0) {
break;
}
String idx = html.substring(start, end);
start = html.indexOf("__", start); //$NON-NLS-1$
if (start < 0) {
break;
}
start += 2;
end = html.indexOf(endMarkerPrefix + idx + "__", start); //$NON-NLS-1$
if (end < 0) {
break;
}
String macroCallWithBody = html.substring(start, end);
String macroCall = StringUtils.substringBefore(macroCallWithBody, bodyMarker);
String body = StringUtils.substringAfter(macroCallWithBody, bodyMarker);
String macroName = StringUtils.substringBefore(macroCall, " "); //$NON-NLS-1$
String params = StringUtils.substringAfter(macroCall, " "); //$NON-NLS-1$
IMacro macro = macroFactory.get(macroName);
MacroContext macroContext = MacroContext.create(macroName, params, body, context, locale, beanFactory);
IMacroRunnable macroRunnable = macro.createRunnable();
html = StringUtils.replace(html,
startMarkerPrefix + idx + "__" + macroCallWithBody + endMarkerPrefix + idx + "__", //$NON-NLS-1$ //$NON-NLS-2$
StringUtils.defaultString(macroRunnable.getHtml(macroContext)));
MacroInvocation invocation = new MacroInvocation(macroName, params);
html = cleanupHtml(html, Collections.singletonList(invocation), false);
}
return html;
}
private String cleanupHtml(String html, List<MacroInvocation> macroInvocations, boolean cacheable) {
for (;;) {
String newHtml = html;
for (Replacement replacement : CLEANUP) {
newHtml = replacement.replaceAll(newHtml);
}
for (MacroInvocation macroInvocation : macroInvocations) {
IMacro macro = macroFactory.get(macroInvocation.getMacroName());
if (macro != null) {
IMacroDescriptor macroDescriptor = macro.getDescriptor();
if (macroDescriptor.isCacheable() == cacheable) {
IMacroRunnable macroRunnable = macro.createRunnable();
newHtml = StringUtils.defaultString(macroRunnable.cleanupHtml(newHtml), newHtml);
}
}
}
if (newHtml.equals(html)) {
break;
}
html = newHtml;
}
return html;
}
}