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