blob: 0fc275846022b2e4637d4c70e36c6c962ad12ffd [file] [log] [blame]
Shawn Pearce99cdbce2015-02-10 12:05:45 -08001// Copyright 2015 Google Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.gitiles.doc;
16
David Pursehousef39cadc2017-07-07 08:47:51 +090017import static java.util.stream.Collectors.toSet;
18
Shawn Pearcef485a312017-07-01 12:10:27 -070019import com.google.common.base.CharMatcher;
20import com.google.common.base.Splitter;
Shawn Pearce99cdbce2015-02-10 12:05:45 -080021import com.google.gitiles.doc.html.HtmlBuilder;
Soy Authorsbffb5fd2018-02-02 18:38:08 -050022import com.google.template.soy.shared.internal.Sanitizers;
Shawn Pearce99cdbce2015-02-10 12:05:45 -080023import java.util.HashMap;
Shawn Pearce99cdbce2015-02-10 12:05:45 -080024import java.util.Map;
Shawn Pearcef485a312017-07-01 12:10:27 -070025import java.util.Set;
Shawn Pearce12c8fab2016-05-15 16:55:21 -070026import java.util.regex.Matcher;
27import java.util.regex.Pattern;
Dave Borowitz3b744b12016-08-19 16:11:10 -040028import org.commonmark.node.Heading;
29import org.commonmark.node.Node;
30import org.eclipse.jgit.util.RawParseUtils;
Shawn Pearce99cdbce2015-02-10 12:05:45 -080031
32class Navbar {
Shawn Pearcef485a312017-07-01 12:10:27 -070033 private static final Pattern META_LINK =
34 Pattern.compile(
35 "^\\[(logo|home|extensions)\\]:\\s*(.+)$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
David Pursehouse374c9992019-06-15 14:06:40 +090036 private static final Pattern EXTENSIONS_LINK =
37 Pattern.compile("^\\[extensions\\]:\\s*(.+)$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
Shawn Pearce12c8fab2016-05-15 16:55:21 -070038
Shawn Pearcef485a312017-07-01 12:10:27 -070039 private MarkdownConfig cfg;
Shawn Pearce47fd6562016-05-28 14:15:15 -070040 private MarkdownToHtml fmt;
41 private Node node;
42 private String siteTitle;
43 private String logoUrl;
44 private String homeUrl;
Shawn Pearce99cdbce2015-02-10 12:05:45 -080045
Shawn Pearce47fd6562016-05-28 14:15:15 -070046 Navbar() {}
47
Shawn Pearcef485a312017-07-01 12:10:27 -070048 MarkdownConfig getConfig() {
49 return cfg;
50 }
51
52 Navbar setConfig(MarkdownConfig cfg) {
53 this.cfg = cfg;
54 return this;
55 }
56
Shawn Pearce47fd6562016-05-28 14:15:15 -070057 Navbar setFormatter(MarkdownToHtml html) {
58 this.fmt = html;
59 return this;
60 }
61
Shawn Pearcef485a312017-07-01 12:10:27 -070062 Navbar setMarkdown(byte[] md) {
Shawn Pearce47fd6562016-05-28 14:15:15 -070063 if (md != null && md.length > 0) {
Shawn Pearcef485a312017-07-01 12:10:27 -070064 parse(RawParseUtils.decode(md));
Shawn Pearce47fd6562016-05-28 14:15:15 -070065 }
66 return this;
67 }
68
69 Map<String, Object> toSoyData() {
70 Map<String, Object> data = new HashMap<>();
71 data.put("siteTitle", siteTitle);
72 data.put("logoUrl", logo());
73 data.put("homeUrl", homeUrl != null ? fmt.href(homeUrl) : null);
74 data.put("navbarHtml", node != null ? fmt.toSoyHtml(node) : null);
75 return data;
76 }
77
78 private Object logo() {
79 if (logoUrl == null) {
80 return null;
Shawn Pearce99cdbce2015-02-10 12:05:45 -080081 }
82
Shawn Pearce47fd6562016-05-28 14:15:15 -070083 String url = fmt.image(logoUrl);
84 if (HtmlBuilder.isValidHttpUri(url)) {
85 return url;
86 } else if (HtmlBuilder.isImageDataUri(url)) {
87 return Sanitizers.filterImageDataUri(url);
88 } else {
Dave Borowitz12181942018-02-12 09:22:45 -050089 return SoyConstants.IMAGE_URI_INNOCUOUS_OUTPUT;
Shawn Pearce47fd6562016-05-28 14:15:15 -070090 }
91 }
92
Shawn Pearcef485a312017-07-01 12:10:27 -070093 private void parse(String markdown) {
Vlad Bokovfbda0c92019-04-17 13:47:44 +070094 String md = extractMetadata(markdown);
95 node = GitilesMarkdown.parse(cfg, md);
Shawn Pearce47fd6562016-05-28 14:15:15 -070096 extractSiteTitle();
Shawn Pearce47fd6562016-05-28 14:15:15 -070097 }
98
99 private void extractSiteTitle() {
100 for (Node c = node.getFirstChild(); c != null; c = c.getNext()) {
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700101 if (c instanceof Heading) {
102 Heading h = (Heading) c;
Shawn Pearce99cdbce2015-02-10 12:05:45 -0800103 if (h.getLevel() == 1) {
Shawn Pearce47fd6562016-05-28 14:15:15 -0700104 siteTitle = MarkdownUtil.getInnerText(h);
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700105 h.unlink();
Shawn Pearce99cdbce2015-02-10 12:05:45 -0800106 break;
107 }
108 }
109 }
Shawn Pearce47fd6562016-05-28 14:15:15 -0700110 }
Shawn Pearce99cdbce2015-02-10 12:05:45 -0800111
Vlad Bokovfbda0c92019-04-17 13:47:44 +0700112 private String extractMetadata(String markdown) {
David Pursehouse374c9992019-06-15 14:06:40 +0900113 boolean extensionsFound = false;
Shawn Pearcef485a312017-07-01 12:10:27 -0700114 Matcher m = META_LINK.matcher(markdown);
David Pursehouse374c9992019-06-15 14:06:40 +0900115 while (m.find()) {
116 String key = m.group(1).toLowerCase();
117 String url = m.group(2).trim();
118 switch (key) {
119 case "logo":
120 logoUrl = url;
121 break;
122 case "home":
123 homeUrl = url;
124 break;
125 case "extensions":
126 extensionsFound = true;
127 Set<String> names = splitExtensionNames(url);
128 cfg = cfg.copyWithExtensions(enabled(names), disabled(names));
129 break;
130 }
131 }
132 if (extensionsFound) {
133 return EXTENSIONS_LINK.matcher(markdown).replaceAll("");
Shawn Pearce99cdbce2015-02-10 12:05:45 -0800134 }
Vlad Bokovfbda0c92019-04-17 13:47:44 +0700135 return markdown;
Shawn Pearce99cdbce2015-02-10 12:05:45 -0800136 }
Shawn Pearcef485a312017-07-01 12:10:27 -0700137
138 private static Set<String> splitExtensionNames(String url) {
David Pursehouse76346b02019-03-14 10:55:46 +0900139 return Splitter.on(CharMatcher.whitespace().or(CharMatcher.is(','))).trimResults()
140 .omitEmptyStrings().splitToList(url).stream()
Shawn Pearcef485a312017-07-01 12:10:27 -0700141 .map(String::toLowerCase)
142 .collect(toSet());
143 }
144
145 private static Set<String> enabled(Set<String> names) {
146 return names.stream().filter(n -> !n.startsWith("!")).collect(toSet());
147 }
148
149 private static Set<String> disabled(Set<String> names) {
150 return names.stream().filter(n -> n.startsWith("!")).map(n -> n.substring(1)).collect(toSet());
151 }
Shawn Pearce99cdbce2015-02-10 12:05:45 -0800152}