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.wiki.plugin;
020
021import java.io.IOException;
022import java.util.Map;
023import java.util.ResourceBundle;
024
025import org.apache.log4j.Logger;
026import org.apache.wiki.InternalWikiException;
027import org.apache.wiki.WikiContext;
028import org.apache.wiki.WikiEngine;
029import org.apache.wiki.WikiPage;
030import org.apache.wiki.api.engine.FilterManager;
031import org.apache.wiki.api.exceptions.PluginException;
032import org.apache.wiki.api.plugin.WikiPlugin;
033import org.apache.wiki.parser.Heading;
034import org.apache.wiki.parser.HeadingListener;
035import org.apache.wiki.parser.MarkupParser;
036import org.apache.wiki.preferences.Preferences;
037import org.apache.wiki.util.TextUtil;
038
039/**
040 *  Provides a table of contents.
041 *  <p>Parameters : </p>
042 *  <ul>
043 *  <li><b>title</b> - The title of the table of contents.</li>
044 *  <li><b>numbered</b> - if true, generates automatically numbers for the headings.</li>
045 *  <li><b>start</b> - If using a numbered list, sets the start number.</li>
046 *  <li><b>prefix</b> - If using a numbered list, sets the prefix used for the list.</li>
047 *  </ul>
048 *
049 *  @since 2.2
050 */
051public class TableOfContents
052    implements WikiPlugin, HeadingListener
053{
054    private static Logger log = Logger.getLogger( TableOfContents.class );
055
056    /** Parameter name for setting the title. */
057    public static final String PARAM_TITLE = "title";
058
059    /** Parameter name for setting whether the headings should be numbered. */
060    public static final String PARAM_NUMBERED = "numbered";
061
062    /** Parameter name for setting where the numbering should start. */
063    public static final String PARAM_START = "start";
064
065    /** Parameter name for setting what the prefix for the heading is. */
066    public static final String PARAM_PREFIX = "prefix";
067
068    private static final String VAR_ALREADY_PROCESSING = "__TableOfContents.processing";
069
070    StringBuffer m_buf = new StringBuffer();
071    private boolean m_usingNumberedList = false;
072    private String m_prefix = "";
073    private int m_starting = 0;
074    private int m_level1Index = 0;
075    private int m_level2Index = 0;
076    private int m_level3Index = 0;
077    private int m_lastLevel = 0;
078
079    /**
080     *  {@inheritDoc}
081     */
082    public void headingAdded( WikiContext context, Heading hd )
083    {
084        log.debug("HD: "+hd.m_level+", "+hd.m_titleText+", "+hd.m_titleAnchor);
085
086        switch( hd.m_level )
087        {
088          case Heading.HEADING_SMALL:
089            m_buf.append("<li class=\"toclevel-3\">");
090            m_level3Index++;
091            break;
092          case Heading.HEADING_MEDIUM:
093            m_buf.append("<li class=\"toclevel-2\">");
094            m_level2Index++;
095            break;
096          case Heading.HEADING_LARGE:
097            m_buf.append("<li class=\"toclevel-1\">");
098            m_level1Index++;
099            break;
100          default:
101            throw new InternalWikiException("Unknown depth in toc! (Please submit a bug report.)");
102        }
103
104        if (m_level1Index < m_starting)
105        {
106            // in case we never had a large heading ...
107            m_level1Index++;
108        }
109        if ((m_lastLevel == Heading.HEADING_SMALL) && (hd.m_level != Heading.HEADING_SMALL))
110        {
111            m_level3Index = 0;
112        }
113        if ( ((m_lastLevel == Heading.HEADING_SMALL) || (m_lastLevel == Heading.HEADING_MEDIUM)) &&
114                  (hd.m_level == Heading.HEADING_LARGE) )
115        {
116            m_level3Index = 0;
117            m_level2Index = 0;
118        }
119
120        String titleSection = hd.m_titleSection.replace( '%', '_' );
121        String pageName = context.getEngine().encodeName(context.getPage().getName()).replace( '%', '_' );
122
123        String sectref = "#section-"+pageName+"-"+titleSection;
124
125        m_buf.append( "<a class=\"wikipage\" href=\"" + sectref + "\">" );
126        if (m_usingNumberedList)
127        {
128            switch( hd.m_level )
129            {
130            case Heading.HEADING_SMALL:
131                m_buf.append(m_prefix + m_level1Index + "." + m_level2Index + "."+ m_level3Index +" ");
132                break;
133            case Heading.HEADING_MEDIUM:
134                m_buf.append(m_prefix + m_level1Index + "." + m_level2Index + " ");
135                break;
136            case Heading.HEADING_LARGE:
137                m_buf.append(m_prefix + m_level1Index +" ");
138                break;
139            default:
140                throw new InternalWikiException("Unknown depth in toc! (Please submit a bug report.)");
141            }
142        }
143        m_buf.append( TextUtil.replaceEntities(hd.m_titleText)+"</a></li>\n" );
144
145        m_lastLevel = hd.m_level;
146    }
147
148    /**
149     *  {@inheritDoc}
150     */
151    public String execute( WikiContext context, Map<String, String> params )
152        throws PluginException
153    {
154        WikiEngine engine = context.getEngine();
155        WikiPage   page   = context.getPage();
156        ResourceBundle rb = Preferences.getBundle( context, WikiPlugin.CORE_PLUGINS_RESOURCEBUNDLE );
157
158        if( context.getVariable( VAR_ALREADY_PROCESSING ) != null )
159        {
160            //return rb.getString("tableofcontents.title");
161            return "<a href=\"#section-TOC\" class=\"toc\">"+rb.getString("tableofcontents.title")+"</a>";
162        }
163
164        StringBuilder sb = new StringBuilder();
165
166        sb.append("<div class=\"toc\">\n");
167        sb.append("<div class=\"collapsebox\">\n");
168
169        String title = params.get(PARAM_TITLE);
170        sb.append("<h4 id=\"section-TOC\">");
171        if( title != null )
172        {
173            sb.append(TextUtil.replaceEntities(title));
174        }
175        else
176        {
177            sb.append(rb.getString("tableofcontents.title"));
178        }
179        sb.append("</h4>\n");
180
181        // should we use an ordered list?
182        m_usingNumberedList = false;
183        if (params.containsKey(PARAM_NUMBERED))
184        {
185            String numbered = params.get(PARAM_NUMBERED);
186            if (numbered.equalsIgnoreCase("true"))
187            {
188                m_usingNumberedList = true;
189            }
190            else if (numbered.equalsIgnoreCase("yes"))
191            {
192                m_usingNumberedList = true;
193            }
194        }
195
196        // if we are using a numbered list, get the rest of the parameters (if any) ...
197        if (m_usingNumberedList)
198        {
199            int start = 0;
200            String startStr = params.get(PARAM_START);
201            if ((startStr != null) && (startStr.matches("^\\d+$")))
202            {
203                start = Integer.parseInt(startStr);
204            }
205            if (start < 0) start = 0;
206
207            m_starting = start;
208            m_level1Index = start - 1;
209            if (m_level1Index < 0) m_level1Index = 0;
210            m_level2Index = 0;
211            m_level3Index = 0;
212            m_prefix = TextUtil.replaceEntities( params.get(PARAM_PREFIX) );
213            if (m_prefix == null) m_prefix = "";
214            m_lastLevel = Heading.HEADING_LARGE;
215        }
216
217        try
218        {
219            String wikiText = engine.getPureText( page );
220            boolean runFilters = "true".equals( engine.getVariableManager().getValue( context, WikiEngine.PROP_RUNFILTERS, "true" ) );
221
222            if( runFilters ) {
223                try {
224                    FilterManager fm = engine.getFilterManager();
225                    wikiText = fm.doPreTranslateFiltering(context, wikiText);
226
227                } catch (Exception e) {
228                    log.error("Could not construct table of contents: Filter Error", e);
229                    throw new PluginException("Unable to construct table of contents (see logs)");
230                }
231            }
232
233            context.setVariable( VAR_ALREADY_PROCESSING, "x" );
234
235            MarkupParser parser = engine.getRenderingManager().getParser( context, wikiText );
236            parser.addHeadingListener( this );
237            parser.parse();
238
239            sb.append( "<ul>\n"+m_buf.toString()+"</ul>\n" );
240        }
241        catch( IOException e )
242        {
243            log.error("Could not construct table of contents", e);
244            throw new PluginException("Unable to construct table of contents (see logs)");
245        }
246
247        sb.append("</div>\n</div>\n");
248
249        return sb.toString();
250    }
251
252}