Markdown: configure optional banner in navbar.md The navbar.md file can now define a documentation set header and optional project logo: # Gerrit Code Review [logo]: http://storage.googleapis.com/gerrit-static/diffy-w200.png [home]: /index.md * [Home][home] * [APIs](/api/index.md) * [Source](/src/main/java/index.md) The optional site header must be an H1 header and must be specified to use the logo or home references. The optional project logo is specified by a reference named "logo". As images (or any other file type) are not served directly from Gitiles this must be a link to an external service. Recommended size is no taller than 44px. The optional "home" reference will create an <a href> to wrap the logo and H1 inside an anchor to jump back to the logical top of the documentation set. It can also be used inside the outline part of the navbar to create a "Home" link. These nodes are extracted from navbar.md and hoisted into the top of the Soy template above the navbar's usual rendering. +-------+ | Diffy | | | | Gerrit Code Review | APIs | Logo | | +-------+ Home APIs Source The current page's H1 title is extracted and displayed to the right, e.g. "APIs" above. Change-Id: I70b4fd5dd51d4a92d328692740dd6cfa9655e84c
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/DocServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/DocServlet.java index 2718e6e..0ef13bd 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/DocServlet.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/DocServlet.java
@@ -166,6 +166,7 @@ private void showDoc(HttpServletRequest req, HttpServletResponse res, GitilesView view, RootNode nav, RootNode doc) throws IOException { Map<String, Object> data = new HashMap<>(); + data.putAll(Navbar.bannerSoyData(view, nav)); data.put("pageTitle", MoreObjects.firstNonNull( MarkdownHelper.getTitle(doc), view.getPathPart()));
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/Navbar.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/Navbar.java new file mode 100644 index 0000000..784f990 --- /dev/null +++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/Navbar.java
@@ -0,0 +1,76 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// 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.google.gitiles.doc; + +import com.google.gitiles.GitilesView; +import com.google.gitiles.doc.html.HtmlBuilder; +import com.google.template.soy.shared.restricted.Sanitizers; + +import org.pegdown.ast.HeaderNode; +import org.pegdown.ast.Node; +import org.pegdown.ast.ReferenceNode; +import org.pegdown.ast.RootNode; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +class Navbar { + static Map<String, Object> bannerSoyData(GitilesView view, RootNode nav) { + Map<String, Object> data = new HashMap<>(); + data.put("siteTitle", null); + data.put("logoUrl", null); + data.put("homeUrl", null); + + if (nav == null) { + return data; + } + + for (Iterator<Node> i = nav.getChildren().iterator(); i.hasNext();) { + Node n = i.next(); + if (n instanceof HeaderNode) { + HeaderNode h = (HeaderNode) n; + if (h.getLevel() == 1) { + data.put("siteTitle", MarkdownHelper.getInnerText(h)); + i.remove(); + break; + } + } + } + + for (ReferenceNode r : nav.getReferences()) { + String key = MarkdownHelper.getInnerText(r); + String url = r.getUrl(); + if ("logo".equalsIgnoreCase(key)) { + Object src; + if (HtmlBuilder.isImageDataUri(url)) { + src = Sanitizers.filterImageDataUri(url); + } else { + src = url; + } + data.put("logoUrl", src); + } else if ("home".equalsIgnoreCase(key)) { + if (MarkdownHelper.isAbsolutePathToMarkdown(url)) { + url = GitilesView.doc().copyFrom(view).setPathPart(url).toUrl(); + } + data.put("homeUrl", url); + } + } + return data; + } + + private Navbar() { + } +}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java index c80c28c..6676ed3 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java
@@ -58,6 +58,11 @@ private static final FilterNormalizeUri URI = FilterNormalizeUri.INSTANCE; private static final FilterImageDataUri IMAGE_DATA = FilterImageDataUri.INSTANCE; + /** Check if URL is valid for {@code <img src="data:image/*;base64,...">}. */ + public static boolean isImageDataUri(String url) { + return IMAGE_DATA.getValueFilter().matcher(url).find(); + } + private final StringBuilder htmlBuf; private final Appendable textBuf; private String tag; @@ -118,7 +123,7 @@ && URI.getValueFilter().matcher(val).find()) { return URI.escape(val); } - if (IMAGE_DATA.getValueFilter().matcher(val).find()) { + if (isImageDataUri(val)) { return val; // pass through data:image/*;base64,... } return IMAGE_DATA.getInnocuousOutput();
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/doc.css b/gitiles-servlet/src/main/resources/com/google/gitiles/static/doc.css index 198f8f6..6ca3c6c 100644 --- a/gitiles-servlet/src/main/resources/com/google/gitiles/static/doc.css +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/doc.css
@@ -21,6 +21,36 @@ margin: 0; } +.banner { + min-height: 44px; + margin: 0; + padding: 14px 15px 13px; + border-bottom: 1px solid #eee; +} +.banner h1, .banner h2 { + float: left; + font-size: 32px; + font-weight: 300; + line-height: 1.375; + margin: 0; +} +.banner img { + margin: -1px 10px -4px 0px; + vertical-align: middle; +} +.banner a, .banner a:hover { + text-decoration: none; +} +.banner, .banner a:link, .banner a:visited { + color: #777; +} +.banner h2:before { + border-right: 1px solid #eee; + content: ""; + float: left; + height: 44px; + margin: 0 12px 0 14px; +} .nav, .footer-line { color: #333; @@ -45,7 +75,7 @@ .nav li a:hover { color: #0000f9; } -.nav ul:after, .cols:after { +.banner:after, .nav ul:after, .cols:after { clear: both; content: ""; display: block; @@ -138,6 +168,10 @@ padding: 0; } +img { + border: 0; +} + pre { border: 1px solid silver; background: #fafafa;
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Doc.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Doc.soy index 530573d..ae91645 100644 --- a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Doc.soy +++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Doc.soy
@@ -16,7 +16,10 @@ /** * Documentation page rendered from markdown. * - * @param pageTitle h1 title from the documentation. + * @param? siteTitle h1 title from navbar.md. + * @param pageTitle h1 title from specific page. + * @param? logoUrl url of image logo. + * @param? homeUrl url to jump to top of site. * @param sourceUrl url for source view of the page. * @param logUrl url for log history of page. * @param blameUrl url for blame of page source. @@ -27,10 +30,26 @@ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> - <title>{$pageTitle}</title> + <title> + {if $siteTitle}{$siteTitle} -{sp}{/if} + {$pageTitle} + </title> <link rel="stylesheet" type="text/css" href="{gitiles.DOC_CSS_URL}" /> </head> <body> + {if $siteTitle} + <div class="banner" role="banner"> + <div class="nav-aux"> + <h1> + {if $homeUrl}<a href="{$homeUrl}">{/if} + {if $logoUrl}<img src="{$logoUrl}" alt="project logo" />{/if} + {$siteTitle} + {if $homeUrl}</a>{/if} + </h1> + <h2>{$pageTitle}</h2> + </div> + </div> + {/if} {if $navbarHtml} <div class="nav" role="navigation"> <div class="nav-aux">