Merge "Support inheriting of CSS"
diff --git a/BUCK b/BUCK
index 99a8eca..cccde03 100644
--- a/BUCK
+++ b/BUCK
@@ -2,7 +2,7 @@
 
 MODULE = 'com.googlesource.gerrit.plugins.xdocs.XDocs'
 
-ASCIIDOCTOR = '//lib/asciidoctor:asciidoc_lib' if __standalone_mode__ \
+ASCIIDOCTOR = '//lib/asciidoctor:asciidoc_lib' if STANDALONE_MODE \
   else '//plugins/x-docs/lib/asciidoctor:asciidoc_lib'
 
 gerrit_plugin(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocLoader.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocLoader.java
index cf0971c..7971096 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocLoader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocLoader.java
@@ -22,6 +22,8 @@
 import com.google.common.cache.Weigher;
 import com.google.common.collect.Maps;
 import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.httpd.resources.Resource;
 import com.google.gerrit.httpd.resources.SmallResource;
 import com.google.gerrit.reviewdb.client.Project;
@@ -33,8 +35,11 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import com.googlesource.gerrit.plugins.xdocs.formatter.Formatter;
 import com.googlesource.gerrit.plugins.xdocs.formatter.Formatters;
 import com.googlesource.gerrit.plugins.xdocs.formatter.Formatters.FormatterProvider;
+import com.googlesource.gerrit.plugins.xdocs.formatter.StreamFormatter;
+import com.googlesource.gerrit.plugins.xdocs.formatter.StringFormatter;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
@@ -48,14 +53,19 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.filter.PathFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 @Singleton
 public class XDocLoader extends CacheLoader<String, Resource> {
+  private static final Logger log = LoggerFactory.getLogger(XDocLoader.class);
+
   private static final String DEFAULT_HOST = "review.example.com";
 
   private final GitRepositoryManager repoManager;
@@ -80,60 +90,118 @@
   @Override
   public Resource load(String strKey) throws Exception {
     XDocResourceKey key = XDocResourceKey.fromString(strKey);
-    XDocGlobalConfig cfg =
-        new XDocGlobalConfig(cfgFactory.getGlobalPluginConfig(pluginName));
-    FormatterProvider formatter = formatters.getByName(key.getFormatter());
-    if (formatter == null) {
-      return Resource.NOT_FOUND;
-    }
-    ConfigSection formatterCfg = cfg.getFormatterConfig(formatter.getName());
-    Repository repo = repoManager.openRepository(key.getProject());
     try {
-      RevWalk rw = new RevWalk(repo);
+      FormatterProvider formatter = getFormatter(key.getFormatter());
+      Repository repo = repoManager.openRepository(key.getProject());
       try {
-        ObjectId revId = key.getRevId();
-        if (revId == null) {
-          return Resource.NOT_FOUND;
-        }
-        RevCommit commit = rw.parseCommit(revId);
-        RevTree tree = commit.getTree();
-        TreeWalk tw = new TreeWalk(repo);
+        RevWalk rw = new RevWalk(repo);
         try {
-          tw.addTree(tree);
-          tw.setRecursive(true);
-          tw.setFilter(PathFilter.create(key.getResource()));
-          if (!tw.next()) {
-            return Resource.NOT_FOUND;
-          }
-          ObjectId objectId = tw.getObjectId(0);
-          ObjectLoader loader = repo.open(objectId);
-          byte[] bytes = loader.getBytes(Integer.MAX_VALUE);
-          boolean isBinary = RawText.isBinary(bytes);
-          if (formatter.getName().equals(Formatters.RAW_FORMATTER) && isBinary) {
-            return Resources.METHOD_NOT_ALLOWED;
-          }
-          ObjectReader reader = repo.newObjectReader();
+          ObjectId revId = checkRevId(key.getRevId());
+          RevCommit commit = rw.parseCommit(revId);
+          RevTree tree = commit.getTree();
+          TreeWalk tw = new TreeWalk(repo);
           try {
-            String abbrRevId = reader.abbreviate(revId).name();
-            String raw = new String(bytes, UTF_8);
-            if (!isBinary) {
-              raw = replaceMacros(repo, key.getProject(), revId, abbrRevId, raw);
+            tw.addTree(tree);
+            tw.setRecursive(true);
+            tw.setFilter(PathFilter.create(key.getResource()));
+            if (!tw.next()) {
+              throw new ResourceNotFoundException();
             }
+            ObjectId objectId = tw.getObjectId(0);
+            ObjectLoader loader = repo.open(objectId);
             String html =
-                formatter.get().format(key.getProject().get(),
-                    abbrRevId, formatterCfg, raw);
+                getHtml(formatter, repo, loader, key.getProject(), revId);
             return getAsHtmlResource(html, commit.getCommitTime());
           } finally {
-            reader.release();
+            tw.release();
           }
         } finally {
-          tw.release();
+          rw.release();
         }
       } finally {
-        rw.release();
+        repo.close();
       }
+    } catch (ResourceNotFoundException e) {
+      return Resource.NOT_FOUND;
+    } catch (MethodNotAllowedException e) {
+      return Resources.METHOD_NOT_ALLOWED;
+    }
+  }
+
+  private FormatterProvider getFormatter(String formatterName)
+      throws ResourceNotFoundException {
+    FormatterProvider formatter = formatters.getByName(formatterName);
+    if (formatter == null) {
+      throw new ResourceNotFoundException();
+    }
+    return formatter;
+  }
+
+  private static ObjectId checkRevId(ObjectId revId)
+      throws ResourceNotFoundException {
+    if (revId == null) {
+      throw new ResourceNotFoundException();
+    }
+    return revId;
+  }
+
+  private String getHtml(FormatterProvider formatter, Repository repo,
+      ObjectLoader loader, Project.NameKey project, ObjectId revId)
+      throws MethodNotAllowedException, IOException, GitAPIException,
+      ResourceNotFoundException {
+    Formatter f = formatter.get();
+    if (f instanceof StringFormatter) {
+      return getHtml(formatter.getName(), (StringFormatter) f, repo, loader,
+          project, revId);
+    } else if (f instanceof StreamFormatter) {
+      return getHtml(formatter.getName(), (StreamFormatter) f, repo, loader,
+          project, revId);
+    } else {
+      log.error(String.format("Unsupported formatter: %s", formatter.getName()));
+      throw new ResourceNotFoundException();
+    }
+  }
+
+  private String getHtml(String formatterName, StringFormatter f,
+      Repository repo, ObjectLoader loader, Project.NameKey project,
+      ObjectId revId) throws MethodNotAllowedException, IOException,
+      GitAPIException {
+    byte[] bytes = loader.getBytes(Integer.MAX_VALUE);
+    boolean isBinary = RawText.isBinary(bytes);
+    if (formatterName.equals(Formatters.RAW_FORMATTER) && isBinary) {
+      throw new MethodNotAllowedException();
+    }
+    String raw = new String(bytes, UTF_8);
+    String abbrRevId = getAbbrRevId(repo, revId);
+    if (!isBinary) {
+      raw = replaceMacros(repo, project, revId, abbrRevId, raw);
+    }
+    return f.format(project.get(), abbrRevId,
+        getFormatterConfig(formatterName), raw);
+  }
+
+  private String getHtml(String formatterName, StreamFormatter f,
+      Repository repo, ObjectLoader loader, Project.NameKey project,
+      ObjectId revId) throws IOException {
+    try (InputStream raw = loader.openStream()) {
+      return ((StreamFormatter) f).format(project.get(),
+          getAbbrRevId(repo, revId), getFormatterConfig(formatterName), raw);
+    }
+  }
+
+  private ConfigSection getFormatterConfig(String formatterName) {
+    XDocGlobalConfig cfg =
+        new XDocGlobalConfig(cfgFactory.getGlobalPluginConfig(pluginName));
+    return cfg.getFormatterConfig(formatterName);
+  }
+
+  private static String getAbbrRevId(Repository repo, ObjectId revId)
+      throws IOException {
+    ObjectReader reader = repo.newObjectReader();
+    try {
+      return reader.abbreviate(revId).name();
     } finally {
-      repo.close();
+      reader.release();
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocServlet.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocServlet.java
index f557bee..78520f7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocServlet.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocServlet.java
@@ -23,6 +23,7 @@
 import com.google.common.net.HttpHeaders;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.httpd.resources.Resource;
 import com.google.gerrit.httpd.resources.SmallResource;
@@ -106,104 +107,52 @@
   @Override
   public void service(HttpServletRequest req, HttpServletResponse res)
       throws IOException {
-    if (!"GET".equals(req.getMethod()) && !"HEAD".equals(req.getMethod())) {
-      CacheHeaders.setNotCacheable(res);
-      res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
-      return;
-    }
+    try {
+      validateRequestMethod(req);
 
-    ResourceKey key = ResourceKey.fromPath(req.getPathInfo());
-    ProjectState state = projectCache.get(key.project);
-    if (state == null) {
-      Resource.NOT_FOUND.send(req, res);
-      return;
-    }
-    XDocProjectConfig cfg = cfgFactory.create(state);
-    if (key.file == null) {
-      res.sendRedirect(getRedirectUrl(req, key, cfg));
-      return;
-    }
+      ResourceKey key = ResourceKey.fromPath(req.getPathInfo());
+      ProjectState state = getProject(key);
+      XDocProjectConfig cfg = cfgFactory.create(state);
 
-    MimeType mimeType = fileTypeRegistry.getMimeType(key.file, null);
-    FormatterProvider formatter;
-    if (req.getParameter("raw") != null) {
-      formatter = formatters.getRawFormatter();
-    } else {
-      formatter = formatters.get(state, key.file);
-      if (formatter == null
-          && !("image".equals(mimeType.getMediaType())
-              && fileTypeRegistry.isSafeInline(mimeType))) {
-        Resource.NOT_FOUND.send(req, res);
+      if (key.file == null) {
+        res.sendRedirect(getRedirectUrl(req, key, cfg));
         return;
       }
-    }
 
-    try {
+      MimeType mimeType = fileTypeRegistry.getMimeType(key.file, null);
+      FormatterProvider formatter = getFormatter(req, key);
+      if (formatter == null && !isSafeImage(mimeType)) {
+        throw new ResourceNotFoundException();
+      }
+
       ProjectControl projectControl = projectControlFactory.validateFor(key.project);
-      String rev = key.revision;
-      if (rev == null) {
-        rev = cfg.getIndexRef();
-      }
-      if (Constants.HEAD.equals(rev)) {
-        rev = getHead.get().apply(new ProjectResource(projectControl));
-      } else  {
-        if (!ObjectId.isId(rev)) {
-          if (!rev.startsWith(Constants.R_REFS)) {
-            rev = Constants.R_HEADS + rev;
-          }
-          if (!projectControl.controlForRef(rev).isVisible()) {
-            Resource.NOT_FOUND.send(req, res);
-            return;
-          }
-        }
-      }
+      String rev = getRevision(cfg, key.revision, projectControl);
 
       Repository repo = repoManager.openRepository(key.project);
       try {
-        ObjectId revId =
-            repo.resolve(MoreObjects.firstNonNull(rev, Constants.HEAD));
-        if (revId == null) {
-          Resource.NOT_FOUND.send(req, res);
-          return;
-        }
+        ObjectId revId = resolveRevision(repo, rev);
 
         if (ObjectId.isId(rev)) {
-          RevWalk rw = new RevWalk(repo);
-          try {
-            RevCommit commit = rw.parseCommit(repo.resolve(rev));
-            if (!projectControl.canReadCommit(db.get(), rw, commit)) {
-              Resource.NOT_FOUND.send(req, res);
-              return;
-            }
-          } finally {
-            rw.release();
-          }
+          validateCanReadCommit(repo, projectControl, revId);
         }
 
-        String eTag = null;
-        String receivedETag = req.getHeader(HttpHeaders.IF_NONE_MATCH);
-        if (receivedETag != null) {
-          eTag = computeETag(key.project, revId, key.file);
-          if (eTag.equals(receivedETag)) {
-            res.sendError(SC_NOT_MODIFIED);
-            return;
-          }
+        if (isResourceNotModified(req, key, revId)) {
+          res.sendError(SC_NOT_MODIFIED);
+          return;
         }
 
         Resource rsc;
         if (formatter != null) {
           rsc = docCache.get(formatter, key.project, key.file, revId);
-        } else if ("image".equals(mimeType.getMediaType())) {
+        } else if (isImage(mimeType)) {
           rsc = getImageResource(repo, revId, key.file);
         } else {
           rsc = Resource.NOT_FOUND;
         }
 
         if (rsc != Resource.NOT_FOUND) {
-          res.setHeader(
-              HttpHeaders.ETAG,
-              MoreObjects.firstNonNull(eTag,
-                  computeETag(key.project, revId, key.file)));
+          res.setHeader(HttpHeaders.ETAG,
+              computeETag(key.project, revId, key.file));
         }
         CacheHeaders.setCacheablePrivate(res, 7, TimeUnit.DAYS, false);
         rsc.send(req, res);
@@ -214,7 +163,9 @@
     } catch (RepositoryNotFoundException | NoSuchProjectException
         | ResourceNotFoundException | AuthException | RevisionSyntaxException e) {
       Resource.NOT_FOUND.send(req, res);
-      return;
+    } catch (MethodNotAllowedException e) {
+      CacheHeaders.setNotCacheable(res);
+      res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
     }
   }
 
@@ -236,8 +187,7 @@
         byte[] content = loader.getBytes(Integer.MAX_VALUE);
 
         MimeType mimeType = fileTypeRegistry.getMimeType(file, content);
-        if (!"image".equals(mimeType.getMediaType())
-            || !fileTypeRegistry.isSafeInline(mimeType)) {
+        if (!isSafeImage(mimeType)) {
           return Resource.NOT_FOUND;
         }
         return new SmallResource(content)
@@ -254,6 +204,94 @@
     }
   }
 
+  private static void validateRequestMethod(HttpServletRequest req)
+      throws MethodNotAllowedException {
+    if (!("GET".equals(req.getMethod()) || "HEAD".equals(req.getMethod()))) {
+      throw new MethodNotAllowedException();
+    }
+  }
+
+  private ProjectState getProject(ResourceKey key)
+      throws ResourceNotFoundException {
+    ProjectState state = projectCache.get(key.project);
+    if (state == null) {
+      throw new ResourceNotFoundException();
+    }
+    return state;
+  }
+
+  private FormatterProvider getFormatter(HttpServletRequest req, ResourceKey key)
+      throws ResourceNotFoundException {
+    if (req.getParameter("raw") != null) {
+      return formatters.getRawFormatter();
+    } else {
+      return formatters.get(getProject(key), key.file);
+    }
+  }
+
+  private boolean isSafeImage(MimeType mimeType) {
+    return isImage(mimeType) && fileTypeRegistry.isSafeInline(mimeType);
+  }
+
+  private static boolean isImage(MimeType mimeType) {
+    return "image".equals(mimeType.getMediaType());
+  }
+
+  private String getRevision(XDocProjectConfig cfg, String revision,
+      ProjectControl projectControl) throws ResourceNotFoundException,
+      AuthException, IOException {
+    String rev = revision;
+    if (rev == null) {
+      rev = cfg.getIndexRef();
+    }
+    if (Constants.HEAD.equals(rev)) {
+      rev = getHead.get().apply(new ProjectResource(projectControl));
+    } else {
+      if (!ObjectId.isId(rev)) {
+        if (!rev.startsWith(Constants.R_REFS)) {
+          rev = Constants.R_HEADS + rev;
+        }
+        if (!projectControl.controlForRef(rev).isVisible()) {
+          throw new ResourceNotFoundException();
+        }
+      }
+    }
+    return rev;
+  }
+
+  private static ObjectId resolveRevision(Repository repo, String revision)
+      throws ResourceNotFoundException, IOException {
+    ObjectId revId =
+        repo.resolve(MoreObjects.firstNonNull(revision, Constants.HEAD));
+    if (revId == null) {
+      throw new ResourceNotFoundException();
+    }
+    return revId;
+  }
+
+  private void validateCanReadCommit(Repository repo,
+      ProjectControl projectControl, ObjectId revId)
+      throws ResourceNotFoundException, IOException {
+    RevWalk rw = new RevWalk(repo);
+    try {
+      RevCommit commit = rw.parseCommit(revId);
+      if (!projectControl.canReadCommit(db.get(), rw, commit)) {
+        throw new ResourceNotFoundException();
+      }
+    } finally {
+      rw.release();
+    }
+  }
+
+  private static boolean isResourceNotModified(HttpServletRequest req,
+      ResourceKey key, ObjectId revId) {
+    String receivedETag = req.getHeader(HttpHeaders.IF_NONE_MATCH);
+    if (receivedETag != null) {
+      return receivedETag.equals(computeETag(key.project, revId, key.file));
+    }
+    return false;
+  }
+
   private static String computeETag(Project.NameKey project, ObjectId revId,
       String file) {
     return Hashing.md5().newHasher()
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocScreen.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocScreen.java
index 92fa3ee..0bfa9c0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocScreen.java
@@ -53,8 +53,11 @@
     setStyleName("xdocs-panel");
 
     HorizontalPanel p = new HorizontalPanel();
+    p.setStyleName("xdocs-header");
     p.add(new InlineHyperlink(projectName, "/admin/projects/" + projectName));
-    p.add(new Label(" / " + fileName + " (" + revision + ")"));
+    p.add(new Label("/"));
+    p.add(new Label(fileName));
+    p.add(new Label("(" + revision + ")"));
     add(p);
 
     final String url = getUrl(projectName, revision, fileName);
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 8f86ccf..42c0456 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
@@ -49,7 +49,7 @@
 import java.util.Properties;
 
 @Singleton
-public class AsciidoctorFormatter implements Formatter {
+public class AsciidoctorFormatter implements StringFormatter {
   public static final String NAME = "ASCIIDOCTOR";
 
   private static final String BACKEND = "html5";
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatter.java
index c975147..d7c6549 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatter.java
@@ -16,25 +16,6 @@
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 
-import com.googlesource.gerrit.plugins.xdocs.ConfigSection;
-
-import java.io.IOException;
-
 @ExtensionPoint
 public interface Formatter {
-
-  /**
-   * Formats the given raw text as html.
-   *
-   * @param projectName the name of the project that contains the file to be
-   *        formatted
-   * @param revision the abbreviated revision from which the file is loaded
-   * @param cfg the global configuration for this formatter
-   * @param raw the raw text
-   * @return the given text formatted as html
-   * @throws IOException thrown if the formatting fails
-   */
-  public String format(String projectName, String revision, ConfigSection cfg,
-      String raw)
-      throws IOException;
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatters.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatters.java
index 4e3c359..95e412e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatters.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/Formatters.java
@@ -201,10 +201,9 @@
 
   public static class FormatterProvider {
     private final String name;
-    private final Provider<Formatter> formatter;
+    private final Provider<? extends Formatter> formatter;
 
-    FormatterProvider(String name,
-        Provider<Formatter> formatter) {
+    FormatterProvider(String name, Provider<? extends Formatter> formatter) {
       this.name = name;
       this.formatter = formatter;
     }
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 5ce5a73..58a8966 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
@@ -25,7 +25,7 @@
 
 import java.io.IOException;
 
-public class MarkdownFormatter implements Formatter {
+public class MarkdownFormatter implements StringFormatter {
   public final static String NAME = "MARKDOWN";
 
   private final FormatterUtil util;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/PlainTextFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/PlainTextFormatter.java
index 0c79bd5..6e36bba 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/PlainTextFormatter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/PlainTextFormatter.java
@@ -18,7 +18,7 @@
 
 import com.googlesource.gerrit.plugins.xdocs.ConfigSection;
 
-public class PlainTextFormatter implements Formatter {
+public class PlainTextFormatter implements StringFormatter {
   public final static String NAME = "PLAIN_TEXT";
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/StreamFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/StreamFormatter.java
new file mode 100644
index 0000000..c435b23
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/StreamFormatter.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.xdocs.formatter;
+
+import com.googlesource.gerrit.plugins.xdocs.ConfigSection;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public interface StreamFormatter extends Formatter {
+  /**
+   * Formats the given raw text as html.
+   *
+   * @param projectName the name of the project that contains the file to be
+   *        formatted
+   * @param revision the abbreviated revision from which the file is loaded
+   * @param cfg the global configuration for this formatter
+   * @param raw the raw stream
+   * @return the content from the given stream formatted as html
+   * @throws IOException thrown if the formatting fails
+   */
+  public String format(String projectName, String revision, ConfigSection cfg,
+      InputStream raw) throws IOException;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/StringFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/StringFormatter.java
new file mode 100644
index 0000000..952794c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/formatter/StringFormatter.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.xdocs.formatter;
+
+import com.googlesource.gerrit.plugins.xdocs.ConfigSection;
+
+import java.io.IOException;
+
+public interface StringFormatter extends Formatter {
+
+  /**
+   * Formats the given raw text as html.
+   *
+   * @param projectName the name of the project that contains the file to be
+   *        formatted
+   * @param revision the abbreviated revision from which the file is loaded
+   * @param cfg the global configuration for this formatter
+   * @param raw the raw text
+   * @return the given text formatted as html
+   * @throws IOException thrown if the formatting fails
+   */
+  public String format(String projectName, String revision, ConfigSection cfg,
+      String raw) throws IOException;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/public/xdocs.css b/src/main/java/com/googlesource/gerrit/plugins/xdocs/public/xdocs.css
index cf984d4..42471c7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/public/xdocs.css
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/public/xdocs.css
@@ -3,8 +3,14 @@
   width: 100%;
 }
 
+.xdocs-header td {
+  padding-right: 5px;
+}
+
 .xdocs-panel iframe {
   width: 100%;
+  border-width: 1px;
+  border-style: solid;
 }
 
 .xdocs-error {