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}