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