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