Allow projects to append or override the default CSS

At the moment project-specific CSS always completely overrides the
default CSS. This is bad if projects just want to make a single
modification because it would require to copy the complete default CSS
and then change it. Add a new formatter config option to control
whether the project-specific CSS should be appended to the default CSS
(default case) or whether the project-specific CSS should completely
override the default CSS.

Change-Id: I585569129bfd356a2f8029cdf82bed93fd84c7b1
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocGlobalConfig.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocGlobalConfig.java
index fb791d6..a8ba4df 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocGlobalConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocGlobalConfig.java
@@ -23,6 +23,7 @@
 public class XDocGlobalConfig {
   public static final String SECTION_FORMATTER = "formatter";
   public static final String KEY_ALLOW_HTML = "allowHtml";
+  public static final String KEY_APPEND_CSS = "appendCss";
   public static final String KEY_ENABLED = "enabled";
   public static final String KEY_EXT = "ext";
   public static final String KEY_INCLUDE_TOC = "includeToc";
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/AsciidoctorFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/AsciidoctorFormatter.java
index 233e302..b0611c7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/AsciidoctorFormatter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/AsciidoctorFormatter.java
@@ -14,6 +14,7 @@
 
 package com.googlesource.gerrit.plugins.xdocs.formatter;
 
+import static com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.KEY_APPEND_CSS;
 import static com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.KEY_INCLUDE_TOC;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
@@ -70,22 +71,26 @@
   @Override
   public String format(String projectName, String revision,
       ConfigSection globalCfg, String raw) throws IOException {
+    ConfigSection projectCfg =
+        formatters.getFormatterConfig(globalCfg.getSubsection(), projectName);
     // asciidoctor ignores all attributes if no output file is specified,
     // this is why we must specify an output file and then read its content
     File tmpFile =
         new File(baseDir, "tmp/asciidoctor-" + TimeUtil.nowTs().getNanos() + ".tmp");
     try {
       Asciidoctor.Factory.create(AsciidoctorFormatter.class.getClassLoader())
-          .render(raw, createOptions(
-              formatters.getFormatterConfig(globalCfg.getSubsection(), projectName),
-              revision, tmpFile));
+          .render(raw, createOptions(projectCfg, revision, tmpFile));
       try (FileInputStream input = new FileInputStream(tmpFile)) {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         ByteStreams.copy(input, out);
-        return util.insertCss(
-            out.toString(UTF_8.name()),
-            MoreObjects.firstNonNull(
-                util.getCss(projectName, "asciidoctor"), defaultCss));
+        String html = out.toString(UTF_8.name());
+        String projectCss = util.getCss(projectName, "asciidoctor");
+        if (projectCfg.getBoolean(KEY_APPEND_CSS, true)) {
+          return util.insertCss(html, defaultCss, projectCss);
+        } else {
+          return util.insertCss(html,
+              MoreObjects.firstNonNull(projectCss, defaultCss));
+        }
       }
     } finally {
       if (!tmpFile.delete()) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/FormatterUtil.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/FormatterUtil.java
index 3fb1d18..349b81a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/FormatterUtil.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/FormatterUtil.java
@@ -64,7 +64,7 @@
    * Inserts the given CSS into the given HTML.
    *
    * @param html the HTML
-   * @param css the CSS
+   * @param css the CSS, may be <code>null</code>
    * @return the HTML that includes the CSS
    */
   public String insertCss(String html, String css) {
@@ -72,13 +72,36 @@
       return html;
     }
 
+    return insertCss(html, css, null);
+  }
+
+  /**
+   * Inserts the given CSS's into the given HTML.
+   *
+   * @param html the HTML
+   * @param css1 first CSS, may be <code>null</code>
+   * @param css2 second CSS, may be <code>null</code>
+   * @return the HTML that includes the CSS
+   */
+  public String insertCss(String html, String css1, String css2) {
+    if (html == null || (css1 == null && css2 == null)) {
+      return html;
+    }
+
     int p = html.lastIndexOf("</head>");
     if (p > 0) {
       StringBuilder b = new StringBuilder();
       b.append(html.substring(0, p));
-      b.append("<style type=\"text/css\">\n");
-      b.append(css);
-      b.append("</style>\n");
+      if (css1 != null) {
+        b.append("<style type=\"text/css\">\n");
+        b.append(css1);
+        b.append("</style>\n");
+      }
+      if (css2 != null) {
+        b.append("<style type=\"text/css\">\n");
+        b.append(css2);
+        b.append("</style>\n");
+      }
       b.append(html.substring(p));
       return b.toString();
     } else {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/MarkdownFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/MarkdownFormatter.java
index e646027..1863d54 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/MarkdownFormatter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/MarkdownFormatter.java
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.xdocs.formatter;
 
 import static com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.KEY_ALLOW_HTML;
+import static com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.KEY_APPEND_CSS;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.inject.Inject;
@@ -27,25 +28,40 @@
   public final static String NAME = "MARKDOWN";
 
   private final FormatterUtil util;
+  private final Formatters formatters;
 
   @Inject
-  MarkdownFormatter(FormatterUtil formatterUtil) {
+  MarkdownFormatter(
+      FormatterUtil formatterUtil,
+      Formatters formatters) {
     this.util = formatterUtil;
+    this.formatters = formatters;
   }
 
   @Override
-  public String format(String projectName, String revision, ConfigSection cfg,
-      String raw) throws UnsupportedEncodingException {
+  public String format(String projectName, String revision,
+      ConfigSection globalCfg, String raw) throws UnsupportedEncodingException {
+    ConfigSection projectCfg =
+        formatters.getFormatterConfig(globalCfg.getSubsection(), projectName);
     com.google.gerrit.server.documentation.MarkdownFormatter f =
         new com.google.gerrit.server.documentation.MarkdownFormatter();
-    if (!cfg.getBoolean(KEY_ALLOW_HTML, false)) {
+    if (!globalCfg.getBoolean(KEY_ALLOW_HTML, false)) {
       f.suppressHtml();
     }
-    // if there is no project-specific CSS and f.setCss(null) is invoked
-    // com.google.gerrit.server.documentation.MarkdownFormatter applies the
-    // default CSS
-    f.setCss(util.getCss(projectName, "markdown"));
-    byte[] b = f.markdownToDocHtml(raw, UTF_8.name());
-    return new String(b, UTF_8);
+    String projectCss = util.getCss(projectName, "markdown");
+    if (projectCfg.getBoolean(KEY_APPEND_CSS, true)) {
+      // if f.setCss(css) is not invoked
+      // com.google.gerrit.server.documentation.MarkdownFormatter applies the
+      // default CSS
+      byte[] b = f.markdownToDocHtml(raw, UTF_8.name());
+      return util.insertCss(new String(b, UTF_8), projectCss);
+    } else {
+      // if there is no project-specific CSS and f.setCss(null) is invoked
+      // com.google.gerrit.server.documentation.MarkdownFormatter applies the
+      // default CSS
+      f.setCss(projectCss);
+      byte[] b = f.markdownToDocHtml(raw, UTF_8.name());
+      return new String(b, UTF_8);
+    }
   }
 }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 75f29b6..02e7d75 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -68,6 +68,20 @@
 
 	Default: `true`
 
+<a id="appendCss">
+formatter.<formatter>.appendCss
+:	Whether the project-specific CSS should be appended to the default CSS.
+
+	If `false` the default CSS is completely replaced by the
+	project-specific CSS.
+
+	Overrides the [global configuration of `appendCss`](#formatterAppendCss)
+	for this formatter.
+
+	Supported for the following formatters: `ASCIIDOCTOR`, `MARKDOWN`
+
+	Default: `true` (project-specific CSS is appended to the default CSS)
+
 <a id="projectCss">
 Project-Specific CSS
 --------------------
@@ -170,3 +184,16 @@
 	Supported for the following formatters: `ASCIIDOCTOR`
 
 	Default: `true`
+
+<a id="formatterAppendCss">
+formatter.<formatter>.appendCss
+:	Whether project-specific CSS should be appended to the default CSS.
+
+	If `false` the default CSS is completely replaced by the
+	project-specific CSS.
+
+	Can be overridden on [project-level](#appendCss).
+
+	Supported for the following formatters: `ASCIIDOCTOR`, `MARKDOWN`
+
+	Default: `true` (project-specific CSS is appended to the default CSS)