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("<h4>"+TextUtil.replaceEntities(title)+"</h4>\n");
174            sb.append(TextUtil.replaceEntities(title));
175        }
176        else
177        {
178            //sb.append("<h4>"+rb.getString("tableofcontents.title")+"</h4>\n");
179            sb.append(rb.getString("tableofcontents.title"));
180        }
181        sb.append("</h4>\n");
182
183        // should we use an ordered list?
184        m_usingNumberedList = false;
185        if (params.containsKey(PARAM_NUMBERED))
186        {
187            String numbered = params.get(PARAM_NUMBERED);
188            if (numbered.equalsIgnoreCase("true"))
189            {
190                m_usingNumberedList = true;
191            }
192            else if (numbered.equalsIgnoreCase("yes"))
193            {
194                m_usingNumberedList = true;
195            }
196        }
197
198        // if we are using a numbered list, get the rest of the parameters (if any) ...
199        if (m_usingNumberedList)
200        {
201            int start = 0;
202            String startStr = params.get(PARAM_START);
203            if ((startStr != null) && (startStr.matches("^\\d+$")))
204            {
205                start = Integer.parseInt(startStr);
206            }
207            if (start < 0) start = 0;
208
209            m_starting = start;
210            m_level1Index = start - 1;
211            if (m_level1Index < 0) m_level1Index = 0;
212            m_level2Index = 0;
213            m_level3Index = 0;
214            m_prefix = params.get(PARAM_PREFIX);
215            if (m_prefix == null) m_prefix = "";
216            m_lastLevel = Heading.HEADING_LARGE;
217        }
218
219        try
220        {
221            String wikiText = engine.getPureText( page );
222            boolean runFilters = "true".equals( engine.getVariableManager().getValue( context, WikiEngine.PROP_RUNFILTERS, "true" ) );
223            
224            if( runFilters ) {
225                try {
226                    FilterManager fm = engine.getFilterManager();
227                    wikiText = fm.doPreTranslateFiltering(context, wikiText);
228
229                } catch (Exception e) {
230                    log.error("Could not construct table of contents: Filter Error", e);
231                    throw new PluginException("Unable to construct table of contents (see logs)");
232                }
233            }
234            
235            context.setVariable( VAR_ALREADY_PROCESSING, "x" );
236            
237            MarkupParser parser = engine.getRenderingManager().getParser( context, wikiText );
238            parser.addHeadingListener( this );
239            parser.parse();
240
241            sb.append( "<ul>\n"+m_buf.toString()+"</ul>\n" );
242        }
243        catch( IOException e )
244        {
245            log.error("Could not construct table of contents", e);
246            throw new PluginException("Unable to construct table of contents (see logs)");
247        }
248
249        sb.append("</div>\n</div>\n");
250
251        return sb.toString();
252    }
253
254}