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