blob: 6f25f583a757c4e488c64652e0b422e3bc6d353b [file] [log] [blame]
Shawn Pearcec9549982015-02-11 13:09:01 -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
Shawn Pearced57ade02015-06-22 12:34:38 -070017import com.google.common.collect.Iterables;
Dave Borowitz9d6f8de2017-01-17 10:12:34 -050018import com.google.common.collect.ListMultimap;
Shawn Pearced57ade02015-06-22 12:34:38 -070019import com.google.common.collect.Maps;
Dave Borowitz9d6f8de2017-01-17 10:12:34 -050020import com.google.common.collect.MultimapBuilder;
Shawn Pearcec9549982015-02-11 13:09:01 -080021import com.google.gitiles.doc.html.HtmlBuilder;
Shawn Pearced57ade02015-06-22 12:34:38 -070022import java.util.ArrayDeque;
23import java.util.ArrayList;
24import java.util.Collection;
25import java.util.Deque;
26import java.util.List;
27import java.util.Map;
Dave Borowitz3b744b12016-08-19 16:11:10 -040028import org.apache.commons.lang3.StringUtils;
29import org.commonmark.node.Heading;
30import org.commonmark.node.Node;
Shawn Pearce8d3e9f22015-02-11 13:50:48 -080031
Shawn Pearcec9549982015-02-11 13:09:01 -080032/** Outputs outline from HeaderNodes in the AST. */
33class TocFormatter {
34 private final HtmlBuilder html;
35 private final int maxLevel;
36
Shawn Pearcec9549982015-02-11 13:09:01 -080037 private int countH1;
Shawn Pearce12c8fab2016-05-15 16:55:21 -070038 private List<Heading> outline;
39 private Map<Heading, String> ids;
Shawn Pearcec9549982015-02-11 13:09:01 -080040
41 private int level;
42
43 TocFormatter(HtmlBuilder html, int maxLevel) {
44 this.html = html;
45 this.maxLevel = maxLevel;
46 }
47
Shawn Pearce12c8fab2016-05-15 16:55:21 -070048 void setRoot(Node doc) {
Shawn Pearced57ade02015-06-22 12:34:38 -070049 outline = new ArrayList<>();
Dave Borowitz9d6f8de2017-01-17 10:12:34 -050050 ListMultimap<String, TocEntry> entries =
51 MultimapBuilder.hashKeys(16).arrayListValues(4).build();
Shawn Pearce12c8fab2016-05-15 16:55:21 -070052 scan(doc, entries, new ArrayDeque<Heading>());
Shawn Pearced57ade02015-06-22 12:34:38 -070053 ids = generateIds(entries);
Shawn Pearcec9549982015-02-11 13:09:01 -080054 }
55
Shawn Pearce12c8fab2016-05-15 16:55:21 -070056 private boolean include(Heading h) {
Shawn Pearced57ade02015-06-22 12:34:38 -070057 if (h.getLevel() == 1) {
Shawn Pearcec9549982015-02-11 13:09:01 -080058 return countH1 > 1;
59 }
60 return h.getLevel() <= maxLevel;
61 }
62
Shawn Pearce12c8fab2016-05-15 16:55:21 -070063 String idFromHeader(Heading header) {
Shawn Pearced57ade02015-06-22 12:34:38 -070064 return ids.get(header);
Shawn Pearcec9549982015-02-11 13:09:01 -080065 }
66
67 void format() {
Shawn Pearcec9549982015-02-11 13:09:01 -080068 int startLevel = countH1 > 1 ? 1 : 2;
Shawn Pearcec9549982015-02-11 13:09:01 -080069 level = startLevel;
70
71 html.open("div")
72 .attribute("class", "toc")
73 .attribute("role", "navigation")
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +020074 .open("h2")
75 .appendAndEscape("Contents")
76 .close("h2")
77 .open("div")
78 .attribute("class", "toc-aux")
79 .open("ul");
Shawn Pearce12c8fab2016-05-15 16:55:21 -070080 for (Heading header : outline) {
Shawn Pearced57ade02015-06-22 12:34:38 -070081 outline(header);
82 }
Shawn Pearcec9549982015-02-11 13:09:01 -080083 while (level >= startLevel) {
84 html.close("ul");
85 level--;
86 }
87 html.close("div").close("div");
88 }
89
Shawn Pearce12c8fab2016-05-15 16:55:21 -070090 private void outline(Heading h) {
Shawn Pearcec9549982015-02-11 13:09:01 -080091 if (!include(h)) {
92 return;
93 }
94
Shawn Pearce8d3e9f22015-02-11 13:50:48 -080095 String id = idFromHeader(h);
96 if (id == null) {
Shawn Pearcec9549982015-02-11 13:09:01 -080097 return;
98 }
99
100 while (level > h.getLevel()) {
101 html.close("ul");
102 level--;
103 }
104 while (level < h.getLevel()) {
105 html.open("ul");
106 level++;
107 }
108
109 html.open("li")
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200110 .open("a")
111 .attribute("href", "#" + id)
112 .appendAndEscape(MarkdownUtil.getInnerText(h))
113 .close("a")
114 .close("li");
Shawn Pearcec9549982015-02-11 13:09:01 -0800115 }
116
Dave Borowitz9d6f8de2017-01-17 10:12:34 -0500117 private void scan(Node node, ListMultimap<String, TocEntry> entries, Deque<Heading> stack) {
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700118 if (node instanceof Heading) {
119 scan((Heading) node, entries, stack);
Shawn Pearced57ade02015-06-22 12:34:38 -0700120 } else {
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700121 for (Node c = node.getFirstChild(); c != null; c = c.getNext()) {
122 scan(c, entries, stack);
Shawn Pearced57ade02015-06-22 12:34:38 -0700123 }
124 }
125 }
126
Dave Borowitz9d6f8de2017-01-17 10:12:34 -0500127 private void scan(Heading header, ListMultimap<String, TocEntry> entries, Deque<Heading> stack) {
Shawn Pearced57ade02015-06-22 12:34:38 -0700128 if (header.getLevel() == 1) {
129 countH1++;
130 }
131 while (!stack.isEmpty() && stack.getLast().getLevel() >= header.getLevel()) {
132 stack.removeLast();
133 }
134
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700135 NamedAnchor node = findAnchor(header);
Shawn Pearce25d91962015-06-22 15:35:36 -0700136 if (node != null) {
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700137 entries.put(node.getName(), new TocEntry(stack, header, false, node.getName()));
Shawn Pearce25d91962015-06-22 15:35:36 -0700138 stack.add(header);
139 outline.add(header);
140 return;
141 }
142
Shawn Pearced57ade02015-06-22 12:34:38 -0700143 String title = MarkdownUtil.getInnerText(header);
144 if (title != null) {
145 String id = idFromTitle(title);
Shawn Pearce25d91962015-06-22 15:35:36 -0700146 entries.put(id, new TocEntry(stack, header, true, id));
Shawn Pearced57ade02015-06-22 12:34:38 -0700147 stack.add(header);
148 outline.add(header);
149 }
150 }
151
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700152 private static NamedAnchor findAnchor(Node node) {
153 for (Node c = node.getFirstChild(); c != null; c = c.getNext()) {
154 if (c instanceof NamedAnchor) {
155 return (NamedAnchor) c;
Shawn Pearce25d91962015-06-22 15:35:36 -0700156 }
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700157 NamedAnchor anchor = findAnchor(c);
Shawn Pearce25d91962015-06-22 15:35:36 -0700158 if (anchor != null) {
159 return anchor;
160 }
161 }
162 return null;
163 }
164
Dave Borowitz9d6f8de2017-01-17 10:12:34 -0500165 private Map<Heading, String> generateIds(ListMultimap<String, TocEntry> entries) {
166 ListMultimap<String, TocEntry> tmp =
167 MultimapBuilder.hashKeys(entries.size()).arrayListValues(2).build();
Shawn Pearced57ade02015-06-22 12:34:38 -0700168 for (Collection<TocEntry> headers : entries.asMap().values()) {
169 if (headers.size() == 1) {
170 TocEntry entry = Iterables.getOnlyElement(headers);
171 tmp.put(entry.id, entry);
172 continue;
173 }
174
175 // Try to deduplicate by prefixing with text derived from parents.
176 for (TocEntry entry : headers) {
Shawn Pearce25d91962015-06-22 15:35:36 -0700177 if (!entry.generated) {
178 tmp.put(entry.id, entry);
179 continue;
180 }
181
Shawn Pearced57ade02015-06-22 12:34:38 -0700182 StringBuilder b = new StringBuilder();
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700183 for (Heading p : entry.path) {
Shawn Pearced57ade02015-06-22 12:34:38 -0700184 if (p.getLevel() > 1 || countH1 > 1) {
185 String text = MarkdownUtil.getInnerText(p);
186 if (text != null) {
187 b.append(idFromTitle(text)).append('-');
188 }
189 }
190 }
191 b.append(idFromTitle(MarkdownUtil.getInnerText(entry.header)));
192 entry.id = b.toString();
193 tmp.put(entry.id, entry);
194 }
195 }
196
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700197 Map<Heading, String> ids = Maps.newHashMapWithExpectedSize(tmp.size());
Shawn Pearced57ade02015-06-22 12:34:38 -0700198 for (Collection<TocEntry> headers : tmp.asMap().values()) {
199 if (headers.size() == 1) {
200 TocEntry entry = Iterables.getOnlyElement(headers);
201 ids.put(entry.header, entry.id);
202 } else {
203 int i = 1;
204 for (TocEntry entry : headers) {
205 ids.put(entry.header, entry.id + "-" + (i++));
206 }
207 }
208 }
209 return ids;
210 }
211
212 private static class TocEntry {
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700213 final Heading[] path;
214 final Heading header;
Shawn Pearce25d91962015-06-22 15:35:36 -0700215 final boolean generated;
Shawn Pearced57ade02015-06-22 12:34:38 -0700216 String id;
217
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700218 TocEntry(Deque<Heading> stack, Heading header, boolean generated, String id) {
219 this.path = stack.toArray(new Heading[stack.size()]);
Shawn Pearced57ade02015-06-22 12:34:38 -0700220 this.header = header;
Shawn Pearce25d91962015-06-22 15:35:36 -0700221 this.generated = generated;
Shawn Pearced57ade02015-06-22 12:34:38 -0700222 this.id = id;
223 }
224 }
225
Shawn Pearcec9549982015-02-11 13:09:01 -0800226 private static String idFromTitle(String title) {
227 StringBuilder b = new StringBuilder(title.length());
228 for (char c : StringUtils.stripAccents(title).toCharArray()) {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200229 if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9')) {
Shawn Pearcec9549982015-02-11 13:09:01 -0800230 b.append(c);
231 } else if (c == ' ') {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200232 if (b.length() > 0 && b.charAt(b.length() - 1) != '-' && b.charAt(b.length() - 1) != '_') {
Shawn Pearcec9549982015-02-11 13:09:01 -0800233 b.append('-');
234 }
235 } else if (b.length() > 0
236 && b.charAt(b.length() - 1) != '-'
237 && b.charAt(b.length() - 1) != '_') {
238 b.append('_');
239 }
240 }
241 while (b.length() > 0) {
242 char c = b.charAt(b.length() - 1);
243 if (c == '-' || c == '_') {
244 b.setLength(b.length() - 1);
245 continue;
246 }
247 break;
248 }
249 return b.toString();
250 }
Shawn Pearcec9549982015-02-11 13:09:01 -0800251}