    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}