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; }
@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; } }