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