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