1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.maven.doxia.macro.toc;
20
21 import javax.inject.Named;
22 import javax.inject.Singleton;
23
24 import java.io.StringReader;
25
26 import org.apache.maven.doxia.index.IndexEntry;
27 import org.apache.maven.doxia.index.IndexingSink;
28 import org.apache.maven.doxia.macro.AbstractMacro;
29 import org.apache.maven.doxia.macro.MacroExecutionException;
30 import org.apache.maven.doxia.macro.MacroRequest;
31 import org.apache.maven.doxia.parser.ParseException;
32 import org.apache.maven.doxia.parser.Parser;
33 import org.apache.maven.doxia.sink.Sink;
34 import org.apache.maven.doxia.sink.SinkEventAttributes;
35 import org.apache.maven.doxia.sink.impl.SinkAdapter;
36 import org.apache.maven.doxia.util.DoxiaUtils;
37
38 /**
39 * Macro to display a <code>Table Of Content</code> in a given <code>Sink</code>.
40 * The input parameters for this macro are:
41 * <dl>
42 * <dt>section</dt>
43 * <dd>Display a TOC for the specified section only, or all sections if 0.<br>
44 * Positive int, not mandatory, 0 by default.</dd>
45 * <dt>fromDepth</dt>
46 * <dd>Minimal depth of entries to display in the TOC.
47 * Sections are depth 1, sub-sections depth 2, etc.<br>
48 * Positive int, not mandatory, 0 by default.</dd>
49 * <dt>toDepth</dt>
50 * <dd>Maximum depth of entries to display in the TOC.<br>
51 * Positive int, not mandatory, 5 by default.</dd>
52 * </dl>
53 * For instance, in an APT file, you could write:
54 * <dl>
55 * <dt>%{toc|section=2|fromDepth=2|toDepth=3}</dt>
56 * <dd>Display a TOC for the second section in the document, including all
57 * subsections (depth 2) and sub-subsections (depth 3).</dd>
58 * <dt>%{toc}</dt>
59 * <dd>display a TOC with all section and subsections
60 * (similar to %{toc|section=0})</dd>
61 * </dl>
62 * Moreover, you need to write APT link for section to allow anchor,
63 * for instance:
64 * <pre>
65 * * {SubSection 1}
66 * </pre>
67 *
68 * Similarly, in an XDOC file, you could write:
69 * <pre>
70 * <macro name="toc">
71 * <param name="section" value="1" />
72 * <param name="fromDepth" value="1" />
73 * <param name="toDepth" value="2" />
74 * </macro>
75 * </pre>
76 *
77 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
78 */
79 @Singleton
80 @Named("toc")
81 public class TocMacro extends AbstractMacro {
82 /** The section to display. */
83 private int section;
84
85 /** Start depth. */
86 private int fromDepth;
87
88 /** End depth. */
89 private int toDepth = DEFAULT_DEPTH;
90
91 /** The default end depth. */
92 private static final int DEFAULT_DEPTH = 5;
93
94 /** {@inheritDoc} */
95 public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
96 String source = request.getSourceContent();
97 Parser parser = request.getParser();
98
99 section = getInt(request, "section", 0);
100 fromDepth = getInt(request, "fromDepth", 0);
101 toDepth = getInt(request, "toDepth", DEFAULT_DEPTH);
102
103 if (fromDepth > toDepth) {
104 return;
105 }
106
107 IndexingSink tocSink = new IndexingSink(new SinkAdapter());
108 try {
109 parser.parse(new StringReader(source), tocSink);
110 } catch (ParseException e) {
111 throw new MacroExecutionException(e);
112 } finally {
113 tocSink.close();
114 }
115
116 writeTocForIndexEntry(sink, getAttributesFromMap(request.getParameters()), tocSink.getRootEntry());
117 }
118
119 void writeTocForIndexEntry(Sink sink, SinkEventAttributes listAttributes, IndexEntry rootEntry) {
120 IndexEntry index = rootEntry;
121 if (index.getChildEntries().size() > 0) {
122 sink.list(listAttributes);
123
124 int i = 1;
125
126 for (IndexEntry sectionIndex : index.getChildEntries()) {
127 if ((i == section) || (section == 0)) {
128 writeSubSectionN(sink, sectionIndex, 1);
129 }
130
131 i++;
132 }
133
134 sink.list_();
135 }
136 }
137
138 /**
139 * This recursive method just skips index entries that are not sections (but still evaluates their children).
140 * @param sink The sink to write to.
141 * @param sectionIndex The section index.
142 * @param n The toc depth.
143 */
144 private void writeSubSectionN(Sink sink, IndexEntry sectionIndex, int n) {
145 boolean isRelevantIndex = isRelevantIndexEntry(sectionIndex);
146 if (fromDepth <= n && isRelevantIndex) {
147 sink.listItem();
148 sink.link("#" + DoxiaUtils.encodeId(sectionIndex.getId()));
149 sink.text(sectionIndex.getTitle());
150 sink.link_();
151 }
152
153 if (toDepth > n) {
154 if (sectionIndex.getChildEntries().size() > 0) {
155 if (fromDepth <= n && isRelevantIndex) {
156 sink.list();
157 }
158
159 for (IndexEntry subsectionIndex : sectionIndex.getChildEntries()) {
160 if (n == toDepth - 1 && isRelevantIndex) {
161 sink.listItem();
162 sink.link("#" + DoxiaUtils.encodeId(subsectionIndex.getId()));
163 sink.text(subsectionIndex.getTitle());
164 sink.link_();
165 sink.listItem_();
166 } else {
167 writeSubSectionN(sink, subsectionIndex, n + 1);
168 }
169 }
170
171 if (fromDepth <= n && isRelevantIndex) {
172 sink.list_();
173 }
174 }
175 }
176
177 if (fromDepth <= n && isRelevantIndex) {
178 sink.listItem_();
179 }
180 }
181
182 static boolean isRelevantIndexEntry(IndexEntry indexEntry) {
183 return indexEntry.hasId() && indexEntry.getType().isSection();
184 }
185
186 /**
187 * @param request The MacroRequest.
188 * @param parameter The parameter.
189 * @param defaultValue the default value.
190 * @return the int value of a parameter in the request.
191 * @throws MacroExecutionException if something goes wrong.
192 */
193 private static int getInt(MacroRequest request, String parameter, int defaultValue) throws MacroExecutionException {
194 String value = (String) request.getParameter(parameter);
195
196 if (value == null || value.isEmpty()) {
197 return defaultValue;
198 }
199
200 int i;
201
202 try {
203 i = Integer.parseInt(value);
204 } catch (NumberFormatException e) {
205 return defaultValue;
206 }
207
208 if (i < 0) {
209 throw new MacroExecutionException("The " + parameter + "=" + i + " should be positive.");
210 }
211
212 return i;
213 }
214 }