001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.doxia.macro.toc;
020
021import javax.inject.Named;
022import javax.inject.Singleton;
023
024import java.io.StringReader;
025
026import org.apache.maven.doxia.index.IndexEntry;
027import org.apache.maven.doxia.index.IndexingSink;
028import org.apache.maven.doxia.macro.AbstractMacro;
029import org.apache.maven.doxia.macro.MacroExecutionException;
030import org.apache.maven.doxia.macro.MacroRequest;
031import org.apache.maven.doxia.parser.ParseException;
032import org.apache.maven.doxia.parser.Parser;
033import org.apache.maven.doxia.sink.Sink;
034import org.apache.maven.doxia.sink.SinkEventAttributes;
035import org.apache.maven.doxia.sink.impl.SinkAdapter;
036import org.apache.maven.doxia.util.DoxiaUtils;
037
038/**
039 * Macro to display a <code>Table Of Content</code> in a given <code>Sink</code>.
040 * The input parameters for this macro are:
041 * <dl>
042 * <dt>section</dt>
043 * <dd>Display a TOC for the specified section only, or all sections if 0.<br>
044 * Positive int, not mandatory, 0 by default.</dd>
045 * <dt>fromDepth</dt>
046 * <dd>Minimal depth of entries to display in the TOC.
047 * Sections are depth 1, sub-sections depth 2, etc.<br>
048 * Positive int, not mandatory, 0 by default.</dd>
049 * <dt>toDepth</dt>
050 * <dd>Maximum depth of entries to display in the TOC.<br>
051 * Positive int, not mandatory, 5 by default.</dd>
052 * </dl>
053 * For instance, in an APT file, you could write:
054 * <dl>
055 * <dt>%{toc|section=2|fromDepth=2|toDepth=3}</dt>
056 * <dd>Display a TOC for the second section in the document, including all
057 * subsections (depth 2) and  sub-subsections (depth 3).</dd>
058 * <dt>%{toc}</dt>
059 * <dd>display a TOC with all section and subsections
060 * (similar to %{toc|section=0})</dd>
061 * </dl>
062 * Moreover, you need to write APT link for section to allow anchor,
063 * for instance:
064 * <pre>
065 * * {SubSection 1}
066 * </pre>
067 *
068 * Similarly, in an XDOC file, you could write:
069 * <pre>
070 * &lt;macro name="toc"&gt;
071 *   &lt;param name="section" value="1" /&gt;
072 *   &lt;param name="fromDepth" value="1" /&gt;
073 *   &lt;param name="toDepth" value="2" /&gt;
074 * &lt;/macro&gt;
075 * </pre>
076 *
077 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
078 */
079@Singleton
080@Named("toc")
081public class TocMacro extends AbstractMacro {
082    /** The section to display. */
083    private int section;
084
085    /** Start depth. */
086    private int fromDepth;
087
088    /** End depth. */
089    private int toDepth = DEFAULT_DEPTH;
090
091    /** The default end depth. */
092    private static final int DEFAULT_DEPTH = 5;
093
094    /** {@inheritDoc} */
095    public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
096        String source = request.getSourceContent();
097        Parser parser = request.getParser();
098
099        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}