View Javadoc
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   * &lt;macro name="toc"&gt;
71   *   &lt;param name="section" value="1" /&gt;
72   *   &lt;param name="fromDepth" value="1" /&gt;
73   *   &lt;param name="toDepth" value="2" /&gt;
74   * &lt;/macro&gt;
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 }