001: /**
002: * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE, version 2.1, dated February 1999.
003: *
004: * This program is free software; you can redistribute it and/or modify
005: * it under the terms of the latest version of the GNU Lesser General
006: * Public License as published by the Free Software Foundation;
007: *
008: * This program is distributed in the hope that it will be useful,
009: * but WITHOUT ANY WARRANTY; without even the implied warranty of
010: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
011: * GNU Lesser General Public License for more details.
012: *
013: * You should have received a copy of the GNU Lesser General Public License
014: * along with this program (LICENSE.txt); if not, write to the Free Software
015: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
016: */package org.jamwiki.parser;
017:
018: import java.util.Iterator;
019: import java.util.LinkedHashMap;
020: import java.util.Map;
021: import org.apache.commons.lang.StringUtils;
022: import org.jamwiki.Environment;
023: import org.jamwiki.utils.WikiLogger;
024: import org.jamwiki.utils.Utilities;
025:
026: /**
027: * This class is used to generate a table of contents based on values passed in
028: * through the parser.
029: */
030: public class TableOfContents {
031:
032: private static final WikiLogger logger = WikiLogger
033: .getLogger(TableOfContents.class.getName());
034: /** Status indicating that this TOC object has not yet been initialized. For the JFlex parser this will mean no __TOC__ tag has been added to the document being parsed. */
035: public static final int STATUS_TOC_UNINITIALIZED = 0;
036: /** Status indicating that this TOC object has been initialized. For the JFlex parser this will mean a __TOC__ tag has been added to the document being parsed. */
037: public static final int STATUS_TOC_INITIALIZED = 1;
038: /** Status indicating that the document being parsed does not allow a table of contents. */
039: public static final int STATUS_NO_TOC = 2;
040: private int currentLevel = 0;
041: /** Force a TOC to appear */
042: private boolean forceTOC = false;
043: /** It is possible for a user to include more than one "TOC" tag in a document, so keep count. */
044: private int insertTagCount = 0;
045: /** Keep track of how many times the parser attempts to insert the TOC (one per "TOC" tag) */
046: private int insertionAttempt = 0;
047: private int minLevel = 4;
048: private final Map entries = new LinkedHashMap();
049: private int status = STATUS_TOC_UNINITIALIZED;
050: /** The minimum number of headings that must be present for a TOC to appear, unless forceTOC is set to true. */
051: private static final int MINIMUM_HEADINGS = 4;
052:
053: /**
054: * Add a new table of contents entry to the table of contents object.
055: * The entry should contain the name to use in the HTML anchor tag,
056: * the text to display in the table of contents, and the indentation
057: * level for the entry within the table of contents.
058: *
059: * @param name The name of the entry, to be used in the anchor tag name.
060: * @param text The text to display for the table of contents entry.
061: * @param level The level of the entry. If an entry is a sub-heading of
062: * another entry the value should be 2. If there is a sub-heading of that
063: * entry then its value would be 3, and so forth.
064: */
065: public void addEntry(String name, String text, int level) {
066: if (this .status != STATUS_NO_TOC
067: && this .status != STATUS_TOC_INITIALIZED) {
068: this .setStatus(STATUS_TOC_INITIALIZED);
069: }
070: name = this .checkForUniqueName(name);
071: TableOfContentsEntry entry = new TableOfContentsEntry(name,
072: text, level);
073: this .entries.put(name, entry);
074: if (level < minLevel) {
075: minLevel = level;
076: }
077: }
078:
079: /**
080: * This method checks to see if a TOC is allowed to be inserted, and if so
081: * returns an HTML representation of the TOC.
082: *
083: * @return An HTML representation of the current table of contents object,
084: * or an empty string if the table of contents can not be inserted due
085: * to an inadequate number of entries or some other reason.
086: */
087: public String attemptTOCInsertion() {
088: this .insertionAttempt++;
089: if (this .size() == 0
090: || (this .size() < MINIMUM_HEADINGS && !this .forceTOC)) {
091: // too few headings
092: return "";
093: }
094: if (this .getStatus() == TableOfContents.STATUS_NO_TOC) {
095: // TOC disallowed
096: return "";
097: }
098: if (!Environment.getBooleanValue(Environment.PROP_PARSER_TOC)) {
099: // TOC turned off for the wiki
100: return "";
101: }
102: if (this .insertionAttempt < this .insertTagCount) {
103: // user specified a TOC location, only insert there
104: return "";
105: }
106: return this .toHTML();
107: }
108:
109: /**
110: * Verify the the TOC name is unique. If it is already in use append
111: * a numerical suffix onto it.
112: *
113: * @param name The name to use in the TOC, unless it is already in use.
114: * @return A unique name for use in the TOC, of the form "name" or "name_1"
115: * if "name" is already in use.
116: */
117: public String checkForUniqueName(String name) {
118: if (StringUtils.isBlank(name)) {
119: name = "empty";
120: }
121: int count = 0;
122: String candidate = name;
123: while (count < 1000) {
124: if (this .entries.get(candidate) == null) {
125: return candidate;
126: }
127: count++;
128: candidate = name + "_" + count;
129: }
130: logger.warning("Unable to find appropriate TOC name after "
131: + count + " iterations for value " + name);
132: return candidate;
133: }
134:
135: /**
136: * Internal method to close any list tags prior to adding the next entry.
137: */
138: private void closeList(int level, StringBuffer text) {
139: while (level < currentLevel) {
140: // close lists to current level
141: text.append("</li></ol>");
142: currentLevel--;
143: }
144: }
145:
146: /**
147: * Return the current table of contents status, such as "no table of contents
148: * allowed" or "uninitialized".
149: *
150: * @return The current status of this table of contents object.
151: */
152: public int getStatus() {
153: return this .status;
154: }
155:
156: /**
157: * Internal method to open any list tags prior to adding the next entry.
158: */
159: private void openList(int level, StringBuffer text) {
160: if (level == currentLevel) {
161: // same level as previous item, close previous and open new
162: text.append("</li><li>");
163: return;
164: }
165: while (level > currentLevel) {
166: // open lists to current level
167: text.append("<ol><li>");
168: currentLevel++;
169: }
170: }
171:
172: /**
173: * Force a TOC to appear, even if there are fewer than four headings.
174: *
175: * @param forceTOC Set to <code>true</code> if a TOC is being forced
176: * to appear, false otherwise.
177: */
178: public void setForceTOC(boolean forceTOC) {
179: this .forceTOC = forceTOC;
180: }
181:
182: /**
183: * Set the current table of contents status, such as "no table of contents
184: * allowed" or "uninitialized".
185: *
186: * @param status The current status of this table of contents object.
187: */
188: public void setStatus(int status) {
189: if (status == STATUS_TOC_INITIALIZED) {
190: // keep track of how many TOC insertion tags are present
191: this .insertTagCount++;
192: }
193: this .status = status;
194: }
195:
196: /**
197: * Return the number of entries in this TOC object.
198: *
199: * @return The number of entries in this table of contents object.
200: */
201: public int size() {
202: return this .entries.size();
203: }
204:
205: /**
206: * Return an HTML representation of this table of contents object.
207: *
208: * @return An HTML representation of this table of contents object.
209: */
210: public String toHTML() {
211: Iterator tocIterator = this .entries.keySet().iterator();
212: StringBuffer text = new StringBuffer();
213: text
214: .append("<div class=\"toc-container\"><div class=\"toc-content\">");
215: TableOfContentsEntry entry = null;
216: int adjustedLevel = 0;
217: int previousLevel = 0;
218: while (tocIterator.hasNext()) {
219: String key = (String) tocIterator.next();
220: entry = (TableOfContentsEntry) this .entries.get(key);
221: // adjusted level determines how far to indent the list
222: adjustedLevel = ((entry.level - minLevel) + 1);
223: // cannot increase TOC indent level more than one level at a time
224: if (adjustedLevel > (previousLevel + 1)) {
225: adjustedLevel = previousLevel + 1;
226: }
227: previousLevel = adjustedLevel;
228: if (adjustedLevel > Environment
229: .getIntValue(Environment.PROP_PARSER_TOC_DEPTH)) {
230: // do not display if nested deeper than max
231: continue;
232: }
233: closeList(adjustedLevel, text);
234: openList(adjustedLevel, text);
235: text.append("<a href=\"#").append(
236: Utilities.encodeForURL(entry.name)).append("\">")
237: .append(entry.text).append("</a>");
238: }
239: closeList(0, text);
240: text.append("</div><div class=\"clear\"></div></div>");
241: return text.toString();
242: }
243:
244: /**
245: * Inner class holds TOC entries until they can be processed for display.
246: */
247: class TableOfContentsEntry {
248:
249: int level;
250: String name;
251: String text;
252:
253: /**
254: *
255: */
256: TableOfContentsEntry(String name, String text, int level) {
257: this.name = name;
258: this.text = text;
259: this.level = level;
260: }
261: }
262: }
|