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:
*Initial release.
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 //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; } } }