/* * 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"); } } }