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(TextUtil.replaceEntities(title)); 174 } 175 else 176 { 177 sb.append(rb.getString("tableofcontents.title")); 178 } 179 sb.append("</h4>\n"); 180 181 // should we use an ordered list? 182 m_usingNumberedList = false; 183 if (params.containsKey(PARAM_NUMBERED)) 184 { 185 String numbered = params.get(PARAM_NUMBERED); 186 if (numbered.equalsIgnoreCase("true")) 187 { 188 m_usingNumberedList = true; 189 } 190 else if (numbered.equalsIgnoreCase("yes")) 191 { 192 m_usingNumberedList = true; 193 } 194 } 195 196 // if we are using a numbered list, get the rest of the parameters (if any) ... 197 if (m_usingNumberedList) 198 { 199 int start = 0; 200 String startStr = params.get(PARAM_START); 201 if ((startStr != null) && (startStr.matches("^\\d+$"))) 202 { 203 start = Integer.parseInt(startStr); 204 } 205 if (start < 0) start = 0; 206 207 m_starting = start; 208 m_level1Index = start - 1; 209 if (m_level1Index < 0) m_level1Index = 0; 210 m_level2Index = 0; 211 m_level3Index = 0; 212 m_prefix = TextUtil.replaceEntities( params.get(PARAM_PREFIX) ); 213 if (m_prefix == null) m_prefix = ""; 214 m_lastLevel = Heading.HEADING_LARGE; 215 } 216 217 try 218 { 219 String wikiText = engine.getPureText( page ); 220 boolean runFilters = "true".equals( engine.getVariableManager().getValue( context, WikiEngine.PROP_RUNFILTERS, "true" ) ); 221 222 if( runFilters ) { 223 try { 224 FilterManager fm = engine.getFilterManager(); 225 wikiText = fm.doPreTranslateFiltering(context, wikiText); 226 227 } catch (Exception e) { 228 log.error("Could not construct table of contents: Filter Error", e); 229 throw new PluginException("Unable to construct table of contents (see logs)"); 230 } 231 } 232 233 context.setVariable( VAR_ALREADY_PROCESSING, "x" ); 234 235 MarkupParser parser = engine.getRenderingManager().getParser( context, wikiText ); 236 parser.addHeadingListener( this ); 237 parser.parse(); 238 239 sb.append( "<ul>\n"+m_buf.toString()+"</ul>\n" ); 240 } 241 catch( IOException e ) 242 { 243 log.error("Could not construct table of contents", e); 244 throw new PluginException("Unable to construct table of contents (see logs)"); 245 } 246 247 sb.append("</div>\n</div>\n"); 248 249 return sb.toString(); 250 } 251 252}