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}