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