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