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