/*
* Copyright (C) 2011 4th Line GmbH, Switzerland
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.fourthline.lemma.processor.xhtml;
import com.sun.tools.javac.util.Pair;
import org.seamless.xhtml.Body;
import org.seamless.xhtml.XHTML;
import org.seamless.xhtml.XHTMLElement;
import org.fourthline.lemma.Constants;
import org.fourthline.lemma.pipeline.Context;
import org.fourthline.lemma.anchor.CitationAnchor;
import org.fourthline.lemma.processor.AbstractProcessor;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
/**
* Replaces <code>toc</code> anchors with a generated table-of-contents.
* <p>
* The table-of-contents is generated by discovering all containers in an XHTML
* document which are classed as a {@link org.fourthline.lemma.processor.xhtml.TocProcessor.SectionType}.
* Depending on their nesting level in the document (and not their name - all are equal), they
* are added to the table-of-contents.
* </p>
*
* @author Christian Bauer
*/
public class TocProcessor extends AbstractProcessor<XHTML, XHTML> {
private Logger log = Logger.getLogger(TocProcessor.class.getName());
public static final String TYPE_TOC = "toc";
public static final String TYPE_TOC_ITEM = "tocitem";
public static final String TYPE_TOC_ITEM_PREFIX = "prefix";
public static final String TYPE_TOC_ITEM_LEVEL = "level_";
public static final String SECTION_ELEMENT = "div";
public static enum SectionType {
part, chapter, section, sect1, sect2, sect3
}
public XHTML process(XHTML input, Context context) {
log.fine("Processing input...");
return addTableOfContents(input, context);
}
protected XHTML addTableOfContents(XHTML input, Context context) {
Body body = input.getRoot(getXPath()).getBody();
if (body == null) {
log.info("Body element not found, skipping TOC generation");
return input;
}
// We might have more than one TOC to generate
CitationAnchor[] tocAnchors = CitationAnchor.findCitationAnchors(
getXPath(),
input.getRoot(getXPath()).getBody(),
TYPE_TOC
);
if (tocAnchors.length == 0) {
log.info("No class='toc' anchors found, returning input unchanged");
return input;
}
// Build the TOC tree by appending TocItems to the root item, starting from the XHTML body element
TocItem rootItem = new TocItem();
// Walk the body recursively and append TocItems
addChildTocItems(rootItem, body);
// Now produce the TOC from the TocItem graph
XHTML toc = generateToc(rootItem);
// For every anchor, replace with the (same) TOC
for (CitationAnchor tocAnchor : tocAnchors) {
tocAnchor.getParent().replaceChild(tocAnchor, toc.getRoot(getXPath()), true);
}
return input;
}
protected void addChildTocItems(TocItem currentTocItem, XHTMLElement currentElement) {
// A TOC with more than 3 levels makes no sense (well, this should be configurable...)
if (currentTocItem.level == 3)
return;
// Find all children that are section elements (e.g. all chapter/section div's)
List<Pair<SectionType, XHTMLElement>> sectionElements = new ArrayList();
for (XHTMLElement element : currentElement.getChildren(SECTION_ELEMENT)) {
boolean isSection = false;
String[] types = element.getClasses();
for (SectionType sectionType : SectionType.values()) {
for (String t : types) {
if (t.trim().equals(sectionType.name())) {
isSection = true;
break;
}
}
if (isSection) {
sectionElements.add(new Pair<SectionType, XHTMLElement>(sectionType, element));
break;
}
}
}
log.fine("Found section elements as children of '" + currentElement.getElementName() + "': " + sectionElements.size());
// Create TOC items as needed for each section
if (sectionElements.size() > 0) {
for (Pair<SectionType, XHTMLElement> pair : sectionElements) {
SectionType sectionType = pair.fst;
XHTMLElement sectionElement = pair.snd;
log.finest("Analyzing section element of type: " + sectionType);
// First, we need an identifier so we can link to the TOC item
log.finest("Trying to find nearest identifier of: " + sectionElement.getElementName());
String id = findNearestIdentifier(sectionElement);
// A title would be nice, so we can modify it later with a TOC number prefix
log.finest("Trying to find nearest title element of: " + sectionElement.getElementName());
XHTMLElement titleElement = findNearestTitleElement(sectionElement);
// If we don't have them...
if (id == null || titleElement == null) {
log.info("Skipping section element, no id or title found: " + sectionType);
// Don't create a TOC item but continue down the tree within this section
addChildTocItems(currentTocItem, sectionElement);
continue;
}
log.finest("Found identifier: " + id);
log.finest("Found title element content: " + titleElement.getContent());
// If we have them, create the TOC item and link it into the TOC tree
TocItem sectionTocItem = new TocItem(currentTocItem.level + 1, sectionType.name(), id, titleElement);
sectionTocItem.parent = currentTocItem;
currentTocItem.children.add(sectionTocItem);
// Recursion
addChildTocItems(sectionTocItem, sectionElement);
}
} else {
// Recursion (only for child elements)
for (XHTMLElement child : currentElement.getChildren()) {
addChildTocItems(currentTocItem, child);
}
}
}
protected String findNearestIdentifier(XHTMLElement sectionElement) {
String id = sectionElement.getId();
// If no identifier was specified, we look for a child that was a citation, and take its identifier
if (id == null) {
log.finest("Section element has no identifier, looking for child citation elements with identifier");
XHTMLElement[] childCitations =
sectionElement.findChildrenWithClass(Constants.WRAPPER_ELEMENT, Constants.TYPE_CITATION);
if (childCitations.length > 0) {
id = childCitations[0].getId();
log.finest("Child citation elements found, using the identifier of the first: " + id);
} else {
log.finest("No child citation elements found");
}
}
return id == null || id.length() == 0 ? null : id;
}
protected XHTMLElement findNearestTitleElement(XHTMLElement sectionElement) {
// Take the first <div class="title"> child we can find
XHTMLElement[] titleElements =
sectionElement.findChildrenWithClass(Constants.WRAPPER_ELEMENT, Constants.TYPE_TITLE);
return titleElements.length > 0 ? titleElements[0] : null;
}
protected XHTML generateToc(TocItem root) {
XHTML tocDom = getParser().createDocument();
XHTMLElement container = tocDom
.createRoot(getXPath(), Constants.WRAPPER_ELEMENT)
.setAttribute(XHTML.ATTR.CLASS, TYPE_TOC);
for (int i = 0; i < root.children.size(); i++) {
TocItem child = root.children.get(i);
child.generate(container, "", i);
}
return tocDom;
}
protected class TocItem {
int level;
String levelName;
String id;
XHTMLElement titleElement;
List<TocItem> children = new ArrayList();
TocItem parent;
public TocItem() {
}
public TocItem(int level, String levelName, String id, XHTMLElement titleElement) {
this.level = level;
this.levelName = levelName;
this.id = id;
this.titleElement = titleElement;
}
public void generate(XHTMLElement parentElement, String numberPrefix, int number) {
log.finest("Generating TOC item output, item in parent is " + number + " and prefix is: " + numberPrefix);
String itemClass = TYPE_TOC_ITEM + " " + TYPE_TOC_ITEM_LEVEL + level + " " + TYPE_TOC_ITEM_LEVEL + levelName;
String prefixedNumber = numberPrefix + (number + 1) + ".";
XHTMLElement itemElement = parentElement
.createChild(Constants.WRAPPER_ELEMENT)
.setAttribute(XHTML.ATTR.CLASS, itemClass);
itemElement.createChild(XHTML.ELEMENT.span)
.setAttribute(XHTML.ATTR.CLASS, TYPE_TOC_ITEM_PREFIX)
.setContent(prefixedNumber);
itemElement.createChild(XHTML.ELEMENT.a)
.setAttribute(XHTML.ATTR.href, "#" + id)
.setContent(titleElement.getContent());
log.finest("Prefixing title '" + titleElement.getContent() + "' with numbers: " + prefixedNumber);
titleElement.setContent(prefixedNumber + " " + titleElement.getContent());
for (int i = 0; i < children.size(); i++) {
TocItem child = children.get(i);
child.generate(parentElement, prefixedNumber, i);
}
}
/*
// TODO: This is debug code
public void print() {
for (int i = 0; i < level; i++) {
System.out.print("-");
}
System.out.println(" " + toString());
for (TocItem child : children) {
child.print();
}
}
*/
@Override
public String toString() {
return "LEVEL " + level + " TITLE: " + (titleElement != null ? titleElement.getContent() : "NULL");
}
}
}