package com.nwalsh.saxon; import java.util.Stack; import java.util.StringTokenizer; import org.xml.sax.*; import org.w3c.dom.*; import javax.xml.transform.TransformerException; import com.icl.saxon.Controller; import com.icl.saxon.om.NamePool; import com.icl.saxon.output.Emitter; import com.icl.saxon.tree.AttributeCollection; /** *

Saxon extension to decorate a result tree fragment with callouts.

* *

$Id: CalloutEmitter.java 5907 2006-04-27 08:26:47Z xmldoc $

* *

Copyright (C) 2000 Norman Walsh.

* *

This class provides the guts of a * Saxon 6.* * implementation of callouts for verbatim environments. (It is used * by the Verbatim class.)

* *

The general design is this: the stylesheets construct a result tree * fragment for some verbatim environment. The Verbatim class initializes * a CalloutEmitter with information about the callouts that should be applied * to the verbatim environment in question. Then the result tree fragment * is "replayed" through the CalloutEmitter; the CalloutEmitter builds a * new result tree fragment from this event stream, decorated with callouts, * and that is returned.

* *

Change Log:

*
*
1.0
*

Initial release.

*
* * @see Verbatim * * @author Norman Walsh * ndw@nwalsh.com * * @version $Id: CalloutEmitter.java 5907 2006-04-27 08:26:47Z xmldoc $ * */ public class CalloutEmitter extends CopyEmitter { /** A stack for the preserving information about open elements. */ protected Stack elementStack = null; /** A stack for holding information about temporarily closed elements. */ protected Stack tempStack = null; /** Is the next element absolutely the first element in the fragment? */ protected boolean firstElement = false; /** The FO namespace name. */ protected static String foURI = "http://www.w3.org/1999/XSL/Format"; /** The XHTML namespace name. */ protected static String xhURI = "http://www.w3.org/1999/xhtml"; /** The default column for callouts that specify only a line. */ protected int defaultColumn = 60; /** Is the stylesheet currently running an FO stylesheet? */ protected boolean foStylesheet = false; /** The current line number. */ private static int lineNumber = 0; /** The current column number. */ private static int colNumber = 0; /** The (sorted) array of callouts obtained from the areaspec. */ private static Callout callout[] = null; /** The number of callouts in the callout array. */ private static int calloutCount = 0; /** A pointer used to keep track of our position in the callout array. */ private static int calloutPos = 0; /** The FormatCallout object to use for formatting callouts. */ private static FormatCallout fCallout = null; /**

Constructor for the CalloutEmitter.

* * @param controller * @param namePool The name pool to use for constructing elements and attributes. * @param defaultColumn The default column for callouts. * @param foStylesheet Is this an FO stylesheet? * @param fCallout */ public CalloutEmitter(Controller controller, NamePool namePool, int defaultColumn, boolean foStylesheet, FormatCallout fCallout) { super(controller, namePool); elementStack = new Stack(); firstElement = true; this.defaultColumn = defaultColumn; this.foStylesheet = foStylesheet; this.fCallout = fCallout; } /** *

Examine the areaspec and determine the number and position of * callouts.

* *

The areaspecNodeSet * is examined and a sorted list of the callouts is constructed.

* *

This data structure is used to augment the result tree fragment * with callout bullets.

* * @param areaspecNodeList The source document <areaspec> element. */ public void setupCallouts (NodeList areaspecNodeList) { callout = new Callout[10]; calloutCount = 0; calloutPos = 0; lineNumber = 1; colNumber = 1; // First we walk through the areaspec to calculate the position // of the callouts // // // // // // // // int pos = 0; int coNum = 0; boolean inAreaSet = false; Node areaspec = areaspecNodeList.item(0); NodeList children = areaspec.getChildNodes(); for (int count = 0; count < children.getLength(); count++) { Node node = children.item(count); if (node.getNodeType() == Node.ELEMENT_NODE) { if (node.getNodeName().equalsIgnoreCase("areaset")) { coNum++; NodeList areas = node.getChildNodes(); for (int acount = 0; acount < areas.getLength(); acount++) { Node area = areas.item(acount); if (area.getNodeType() == Node.ELEMENT_NODE) { if (area.getNodeName().equalsIgnoreCase("area")) { addCallout(coNum, area, defaultColumn); } else { System.out.println("Unexpected element in areaset: " + area.getNodeName()); } } } } else if (node.getNodeName().equalsIgnoreCase("area")) { coNum++; addCallout(coNum, node, defaultColumn); } else { System.out.println("Unexpected element in areaspec: " + node.getNodeName()); } } } // Now sort them java.util.Arrays.sort(callout, 0, calloutCount); } /** Process characters. */ public void characters(char[] chars, int start, int len) throws TransformerException { // If we hit characters, then there's no first element... firstElement = false; if (lineNumber == 0) { // if there are any text nodes, there's at least one line lineNumber++; colNumber = 1; } // Walk through the text node looking for callout positions char[] newChars = new char[len]; int pos = 0; for (int count = start; count < start+len; count++) { if (calloutPos < calloutCount && callout[calloutPos].getLine() == lineNumber && callout[calloutPos].getColumn() == colNumber) { if (pos > 0) { rtfEmitter.characters(newChars, 0, pos); pos = 0; } closeOpenElements(rtfEmitter); while (calloutPos < calloutCount && callout[calloutPos].getLine() == lineNumber && callout[calloutPos].getColumn() == colNumber) { fCallout.formatCallout(rtfEmitter, callout[calloutPos]); calloutPos++; } openClosedElements(rtfEmitter); } if (chars[count] == '\n') { // What if we need to pad this line? if (calloutPos < calloutCount && callout[calloutPos].getLine() == lineNumber && callout[calloutPos].getColumn() > colNumber) { if (pos > 0) { rtfEmitter.characters(newChars, 0, pos); pos = 0; } closeOpenElements(rtfEmitter); while (calloutPos < calloutCount && callout[calloutPos].getLine() == lineNumber && callout[calloutPos].getColumn() > colNumber) { formatPad(callout[calloutPos].getColumn() - colNumber); colNumber = callout[calloutPos].getColumn(); while (calloutPos < calloutCount && callout[calloutPos].getLine() == lineNumber && callout[calloutPos].getColumn() == colNumber) { fCallout.formatCallout(rtfEmitter, callout[calloutPos]); calloutPos++; } } openClosedElements(rtfEmitter); } lineNumber++; colNumber = 1; } else { colNumber++; } newChars[pos++] = chars[count]; } if (pos > 0) { rtfEmitter.characters(newChars, 0, pos); } } /** *

Add blanks to the result tree fragment.

* *

This method adds numBlanks to the result tree fragment. * It's used to pad lines when callouts occur after the last existing * characater in a line.

* * @param numBlanks The number of blanks to add. */ protected void formatPad(int numBlanks) { char chars[] = new char[numBlanks]; for (int count = 0; count < numBlanks; count++) { chars[count] = ' '; } try { rtfEmitter.characters(chars, 0, numBlanks); } catch (TransformerException e) { System.out.println("Transformer Exception in formatPad"); } } /** *

Add a callout to the global callout array

* *

This method examines a callout area and adds it to * the global callout array if it can be interpreted.

* *

Only the linecolumn and linerange units are * supported. If no unit is specifed, linecolumn is assumed. * If only a line is specified, the callout decoration appears in * the defaultColumn.

* * @param coNum The callout number. * @param node The area. * @param defaultColumn The default column for callouts. */ protected void addCallout (int coNum, Node node, int defaultColumn) { Element area = (Element) node; String units = null; String coords = null; if (area.hasAttribute("units")) { units = area.getAttribute("units"); } if (area.hasAttribute("coords")) { coords = area.getAttribute("coords"); } if (units != null && !units.equalsIgnoreCase("linecolumn") && !units.equalsIgnoreCase("linerange")) { System.out.println("Only linecolumn and linerange units are supported"); return; } if (coords == null) { System.out.println("Coords must be specified"); return; } // Now let's see if we can interpret the coordinates... StringTokenizer st = new StringTokenizer(coords); int tokenCount = 0; int c1 = 0; int c2 = 0; while (st.hasMoreTokens()) { tokenCount++; if (tokenCount > 2) { System.out.println("Unparseable coordinates"); return; } try { String token = st.nextToken(); int coord = Integer.parseInt(token); c2 = coord; if (tokenCount == 1) { c1 = coord; } } catch (NumberFormatException e) { System.out.println("Unparseable coordinate"); return; } } // Make sure we aren't going to blow past the end of our array if (calloutCount == callout.length) { Callout bigger[] = new Callout[calloutCount+10]; for (int count = 0; count < callout.length; count++) { bigger[count] = callout[count]; } callout = bigger; } // Ok, add the callout if (tokenCount == 2) { if (units != null && units.equalsIgnoreCase("linerange")) { for (int count = c1; count <= c2; count++) { callout[calloutCount++] = new Callout(coNum, area, count, defaultColumn); } } else { // assume linecolumn callout[calloutCount++] = new Callout(coNum, area, c1, c2); } } else { // if there's only one number, assume it's the line callout[calloutCount++] = new Callout(coNum, area, c1, defaultColumn); } } /** Process end element events. */ public void endElement(int nameCode) throws TransformerException { if (!elementStack.empty()) { // if we didn't push the very first element (an fo:block or // pre or div surrounding the whole block), then the stack will // be empty when we get to the end of the first element... elementStack.pop(); } rtfEmitter.endElement(nameCode); } /** Process start element events. */ public void startElement(int nameCode, org.xml.sax.Attributes attributes, int[] namespaces, int nscount) throws TransformerException { if (!skipThisElement(nameCode)) { StartElementInfo sei = new StartElementInfo(nameCode, attributes, namespaces, nscount); elementStack.push(sei); } firstElement = false; rtfEmitter.startElement(nameCode, attributes, namespaces, nscount); } /** *

Protect the outer-most block wrapper.

* *

Open elements in the result tree fragment are closed and reopened * around callouts (so that callouts don't appear inside links or other * environments). But if the result tree fragment is a single block * (a div or pre in HTML, an fo:block in FO), that outer-most block is * treated specially.

* *

This method returns true if the element in question is that * outermost block.

* * @param nameCode The name code for the element * * @return True if the element is the outer-most block, false otherwise. */ protected boolean skipThisElement(int nameCode) { // FIXME: This is such a gross hack... if (firstElement) { int thisFingerprint = namePool.getFingerprint(nameCode); int foBlockFingerprint = namePool.getFingerprint(foURI, "block"); int htmlPreFingerprint = namePool.getFingerprint("", "pre"); int htmlDivFingerprint = namePool.getFingerprint("", "div"); int xhtmlPreFingerprint = namePool.getFingerprint(xhURI, "pre"); int xhtmlDivFingerprint = namePool.getFingerprint(xhURI, "div"); if ((foStylesheet && thisFingerprint == foBlockFingerprint) || (!foStylesheet && (thisFingerprint == htmlPreFingerprint || thisFingerprint == htmlDivFingerprint || thisFingerprint == xhtmlPreFingerprint || thisFingerprint == xhtmlDivFingerprint))) { // Don't push the outer-most wrapping div, pre, or fo:block return true; } } return false; } private void closeOpenElements(Emitter rtfEmitter) throws TransformerException { // Close all the open elements... tempStack = new Stack(); while (!elementStack.empty()) { StartElementInfo elem = (StartElementInfo) elementStack.pop(); rtfEmitter.endElement(elem.getNameCode()); tempStack.push(elem); } } private void openClosedElements(Emitter rtfEmitter) throws TransformerException { // Now "reopen" the elements that we closed... while (!tempStack.empty()) { StartElementInfo elem = (StartElementInfo) tempStack.pop(); AttributeCollection attr = (AttributeCollection) elem.getAttributes(); AttributeCollection newAttr = new AttributeCollection(namePool); for (int acount = 0; acount < attr.getLength(); acount++) { String localName = attr.getLocalName(acount); int nameCode = attr.getNameCode(acount); String type = attr.getType(acount); String value = attr.getValue(acount); String uri = attr.getURI(acount); String prefix = ""; if (localName.indexOf(':') > 0) { prefix = localName.substring(0, localName.indexOf(':')); localName = localName.substring(localName.indexOf(':')+1); } if (uri.equals("") && ((foStylesheet && localName.equals("id")) || (!foStylesheet && (localName.equals("id") || localName.equals("name"))))) { // skip this attribute } else { newAttr.addAttribute(prefix, uri, localName, type, value); } } rtfEmitter.startElement(elem.getNameCode(), newAttr, elem.getNamespaces(), elem.getNSCount()); elementStack.push(elem); } } /** *

A private class for maintaining the information required to call * the startElement method.

* *

In order to close and reopen elements, information about those * elements has to be maintained. This class is just the little record * that we push on the stack to keep track of that info.

*/ private class StartElementInfo { private int _nameCode; org.xml.sax.Attributes _attributes; int[] _namespaces; int _nscount; public StartElementInfo(int nameCode, org.xml.sax.Attributes attributes, int[] namespaces, int nscount) { _nameCode = nameCode; _attributes = attributes; _namespaces = namespaces; _nscount = nscount; } public int getNameCode() { return _nameCode; } public org.xml.sax.Attributes getAttributes() { return _attributes; } public int[] getNamespaces() { return _namespaces; } public int getNSCount() { return _nscount; } } }