// Verbatim.java - Xalan extensions supporting DocBook verbatim environments

package com.nwalsh.xalan;

import java.util.Stack;
import java.util.StringTokenizer;

import org.xml.sax.*;
import org.xml.sax.helpers.AttributesImpl;
import org.w3c.dom.*;
import org.w3c.dom.traversal.NodeIterator;
import org.apache.xerces.dom.*;

import org.apache.xpath.objects.XObject;
import org.apache.xpath.XPath;
import org.apache.xpath.XPathContext;
import org.apache.xpath.NodeSet;
import org.apache.xpath.DOMHelper;
import org.apache.xalan.extensions.XSLProcessorContext;
import org.apache.xalan.extensions.ExpressionContext;
import org.apache.xalan.transformer.TransformerImpl;
import org.apache.xalan.templates.StylesheetRoot;
import org.apache.xalan.templates.ElemExtensionCall;
import org.apache.xalan.templates.OutputProperties;
import org.apache.xalan.res.XSLTErrorResources;
import org.apache.xml.utils.DOMBuilder;
import org.apache.xml.utils.AttList;
import org.apache.xml.utils.QName;

import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.TransformerException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import com.nwalsh.xalan.Callout;
import com.nwalsh.xalan.Params;

/**
 * <p>Xalan extensions supporting DocBook verbatim environments</p>
 *
 * <p>$Id: Verbatim.java,v 1.5 2003/12/17 01:01:34 nwalsh Exp $</p>
 *
 * <p>Copyright (C) 2001 Norman Walsh.</p>
 *
 * <p>This class provides a
 * <a href="http://xml.apache.org/xalan">Xalan</a>
 * implementation of two features that would be impractical to
 * implement directly in XSLT: line numbering and callouts.</p>
 *
 * <p><b>Line Numbering</b></p>
 * <p>The <tt>numberLines</tt> family of functions takes a result tree
 * fragment (assumed to contain the contents of a formatted verbatim
 * element in DocBook: programlisting, screen, address, literallayout,
 * or synopsis) and returns a result tree fragment decorated with
 * line numbers.</p>
 *
 * <p><b>Callouts</b></p>
 * <p>The <tt>insertCallouts</tt> family of functions takes an
 * <tt>areaspec</tt> and a result tree fragment
 * (assumed to contain the contents of a formatted verbatim
 * element in DocBook: programlisting, screen, address, literallayout,
 * or synopsis) and returns a result tree fragment decorated with
 * callouts.</p>
 *
 * <p><b>Change Log:</b></p>
 * <dl>
 * <dt>1.0</dt>
 * <dd><p>Initial release.</p></dd>
 * </dl>
 *
 * @author Norman Walsh
 * <a href="mailto:ndw@nwalsh.com">ndw@nwalsh.com</a>
 *
 * @version $Id: Verbatim.java,v 1.5 2003/12/17 01:01:34 nwalsh Exp $
 *
 */
public class Verbatim {
  /** A stack to hold the open elements while walking through a RTF. */
  private Stack elementStack = null;
  /** A stack to hold the temporarily closed elements. */
  private Stack tempStack = null;
  /** The current line number. */
  private int lineNumber = 0;
  /** The current column number. */
  private int colNumber = 0;
  /** The modulus for line numbering (every 'modulus' line is numbered). */
  private int modulus = 0;
  /** The width (in characters) of line numbers (for padding). */
  private int width = 0;
  /** The separator between the line number and the verbatim text. */
  private String separator = "";
  /** The (sorted) array of callouts obtained from the areaspec. */
  private Callout callout[] = null;
  /** The number of callouts in the callout array. */
  private int calloutCount = 0;
  /** A pointer used to keep track of our position in the callout array. */
  private int calloutPos = 0;
  /** The path to use for graphical callout decorations. */
  private String graphicsPath = null;
  /** The extension to use for graphical callout decorations. */
  private String graphicsExt = null;
  /** The largest callout number that can be represented graphically. */
  private int graphicsMax = 10;
  /** Should graphic callouts use fo:external-graphics or imgs. */
  private boolean graphicsFO = false;

  private static final String foURI = "http://www.w3.org/1999/XSL/Format";
  private static final String xhURI = "http://www.w3.org/1999/xhtml";

  /**
   * <p>Constructor for Verbatim</p>
   *
   * <p>All of the methods are static, so the constructor does nothing.</p>
   */
  public Verbatim() {
  }

  /**
   * <p>Number lines in a verbatim environment.</p>
   *
   * <p>This method adds line numbers to a result tree fragment. Each
   * newline that occurs in a text node is assumed to start a new line.
   * The first line is always numbered, every subsequent xalanMod line
   * is numbered (so if xalanMod=5, lines 1, 5, 10, 15, etc. will be
   * numbered. If there are fewer than xalanMod lines in the environment,
   * every line is numbered.</p>
   *
   * <p>xalanMod is taken from the $linenumbering.everyNth parameter.</p>
   *
   * <p>Every line number will be right justified in a string xalanWidth
   * characters long. If the line number of the last line in the
   * environment is too long to fit in the specified width, the width
   * is automatically increased to the smallest value that can hold the
   * number of the last line. (In other words, if you specify the value 2
   * and attempt to enumerate the lines of an environment that is 100 lines
   * long, the value 3 will automatically be used for every line in the
   * environment.)</p>
   *
   * <p>xalanWidth is taken from the $linenumbering.width parameter.</p>
   *
   * <p>The xalanSep string is inserted between the line
   * number and the original program listing. Lines that aren't numbered
   * are preceded by a xalanWidth blank string and the separator.</p>
   *
   * <p>xalanSep is taken from the $linenumbering.separator parameter.</p>
   *
   * <p>If inline markup extends across line breaks, markup changes are
   * required. All the open elements are closed before the line break and
   * "reopened" afterwards. The reopened elements will have the same
   * attributes as the originals, except that 'name' and 'id' attributes
   * are not duplicated.</p>
   *
   * @param xalanRTF The result tree fragment of the verbatim environment.
   *
   * @return The modified result tree fragment.
   */
  public DocumentFragment numberLines (ExpressionContext context,
				       NodeIterator xalanNI) {

    int xalanMod = Params.getInt(context, "linenumbering.everyNth");
    int xalanWidth = Params.getInt(context, "linenumbering.width");
    String xalanSep = Params.getString(context, "linenumbering.separator");

    DocumentFragment xalanRTF = (DocumentFragment) xalanNI.nextNode();
    int numLines = countLineBreaks(xalanRTF) + 1;

    DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
    DocumentBuilder docBuilder = null;

    try {
      docBuilder = docFactory.newDocumentBuilder();
    } catch (ParserConfigurationException e) {
      System.out.println("PCE!");
      return xalanRTF;
    }
    Document doc = docBuilder.newDocument();
    DocumentFragment df = doc.createDocumentFragment();
    DOMBuilder db = new DOMBuilder(doc, df);

    elementStack = new Stack();
    lineNumber = 0;
    modulus = numLines < xalanMod ? 1 : xalanMod;
    width = xalanWidth;
    separator = xalanSep;

    double log10numLines = Math.log(numLines) / Math.log(10);

    if (width < log10numLines + 1) {
      width = (int) Math.floor(log10numLines + 1);
    }

    lineNumberFragment(db, xalanRTF);
    return df;
  }

  /**
   * <p>Count the number of lines in a verbatim environment.</p>
   *
   * <p>This method walks over the nodes of a DocumentFragment and
   * returns the number of lines breaks that it contains.</p>
   *
   * @param node The root of the tree walk over.
   */
  private int countLineBreaks(Node node) {
    int numLines = 0;

    if (node.getNodeType() == Node.DOCUMENT_FRAGMENT_NODE
	|| node.getNodeType() == Node.DOCUMENT_NODE
	|| node.getNodeType() == Node.ELEMENT_NODE) {
      Node child = node.getFirstChild();
      while (child != null) {
	numLines += countLineBreaks(child);
	child = child.getNextSibling();
      }
    } else if (node.getNodeType() == Node.TEXT_NODE) {
      String text = node.getNodeValue();

      // Walk through the text node looking for newlines
      int pos = 0;
      for (int count = 0; count < text.length(); count++) {
	if (text.charAt(count) == '\n') {
	  numLines++;
	}
      }
    } else {
      // nop
    }

    return numLines;
  }

  /**
   * <p>Build a DocumentFragment with numbered lines.</p>
   *
   * <p>This is the method that actually does the work of numbering
   * lines in a verbatim environment. It recursively walks through a
   * tree of nodes, copying the structure into the rtf. Text nodes
   * are examined for new lines and modified as requested by the
   * global line numbering parameters.</p>
   *
   * <p>When called, rtf should be an empty DocumentFragment and node
   * should be the first child of the result tree fragment that contains
   * the existing, formatted verbatim text.</p>
   *
   * @param rtf The resulting verbatim environment with numbered lines.
   * @param node The root of the tree to copy.
   */
  private void lineNumberFragment(DOMBuilder rtf,
				  Node node) {
    try {
      if (node.getNodeType() == Node.DOCUMENT_FRAGMENT_NODE
	  || node.getNodeType() == Node.DOCUMENT_NODE) {
	Node child = node.getFirstChild();
	while (child != null) {
	  lineNumberFragment(rtf, child);
	  child = child.getNextSibling();
	}
      } else if (node.getNodeType() == Node.ELEMENT_NODE) {
	String ns = node.getNamespaceURI();
	String localName = node.getLocalName();
	String name = ((Element) node).getTagName();

	rtf.startElement(ns, localName, name,
			 copyAttributes((Element) node));

	elementStack.push(node);

	Node child = node.getFirstChild();
	while (child != null) {
	  lineNumberFragment(rtf, child);
	  child = child.getNextSibling();
	}
      } else if (node.getNodeType() == Node.TEXT_NODE) {
	String text = node.getNodeValue();

	if (lineNumber == 0) {
	  // The first line is always numbered
	  formatLineNumber(rtf, ++lineNumber);
	}

	// Walk through the text node looking for newlines
	char chars[] = text.toCharArray();
	int pos = 0;
	for (int count = 0; count < text.length(); count++) {
	  if (text.charAt(count) == '\n') {
	    // This is the tricky bit; if we find a newline, make sure
	    // it doesn't occur inside any markup.

	    if (pos > 0) {
	      rtf.characters(chars, 0, pos);
	      pos = 0;
	    }

	    closeOpenElements(rtf);

	    // Copy the newline to the output
	    chars[pos++] = text.charAt(count);
	    rtf.characters(chars, 0, pos);
	    pos = 0;

	    // Add the line number
	    formatLineNumber(rtf, ++lineNumber);

	    openClosedElements(rtf);
	  } else {
	    chars[pos++] = text.charAt(count);
	  }
	}

	if (pos > 0) {
	  rtf.characters(chars, 0, pos);
	}
      } else if (node.getNodeType() == Node.COMMENT_NODE) {
	String text = node.getNodeValue();
	char chars[] = text.toCharArray();
	rtf.comment(chars, 0, text.length());
      } else if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) {
	rtf.processingInstruction(node.getNodeName(), node.getNodeValue());
      } else {
	System.out.println("Warning: unexpected node type in lineNumberFragment");
      }

      if (node.getNodeType() == Node.ELEMENT_NODE) {
	String ns = node.getNamespaceURI();
	String localName = node.getLocalName();
	String name = ((Element) node).getTagName();
	rtf.endElement(ns, localName, name);
	elementStack.pop();
      }
    } catch (SAXException e) {
      System.out.println("SAX Exception in lineNumberFragment");
    }
  }

  /**
   * <p>Add a formatted line number to the result tree fragment.</p>
   *
   * <p>This method examines the global parameters that control line
   * number presentation (modulus, width, and separator) and adds
   * the appropriate text to the result tree fragment.</p>
   *
   * @param rtf The resulting verbatim environment with numbered lines.
   * @param lineNumber The number of the current line.
   */
  private void formatLineNumber(DOMBuilder rtf,
				int lineNumber) {
    char ch = 160;
    String lno = "";
    if (lineNumber == 1
	|| (modulus >= 1 && (lineNumber % modulus == 0))) {
      lno = "" + lineNumber;
    }

    while (lno.length() < width) {
      lno = ch + lno;
    }

    lno += separator;

    char chars[] = lno.toCharArray();
    try {
      rtf.characters(chars, 0, lno.length());
    } catch (SAXException e) {
      System.out.println("SAX Exception in formatLineNumber");
    }
  }

  /**
   * <p>Insert text callouts into a verbatim environment.</p>
   *
   * <p>This method examines the <tt>areaset</tt> and <tt>area</tt> elements
   * in the supplied <tt>areaspec</tt> and decorates the supplied
   * result tree fragment with appropriate callout markers.</p>
   *
   * <p>If a <tt>label</tt> attribute is supplied on an <tt>area</tt>,
   * its content will be used for the label, otherwise the callout
   * number will be used, surrounded by parenthesis. Callouts are
   * numbered in document order. All of the <tt>area</tt>s in an
   * <tt>areaset</tt> get the same number.</p>
   *
   * <p>Only the <tt>linecolumn</tt> and <tt>linerange</tt> units are
   * supported. If no unit is specifed, <tt>linecolumn</tt> is assumed.
   * If only a line is specified, the callout decoration appears in
   * the defaultColumn. Lines will be padded with blanks to reach the
   * necessary column, but callouts that are located beyond the last
   * line of the verbatim environment will be ignored.</p>
   *
   * <p>Callouts are inserted before the character at the line/column
   * where they are to occur.</p>
   *
   * @param areaspecNodeSet The source node set that contains the areaspec.
   * @param xalanRTF The result tree fragment of the verbatim environment.
   * @param defaultColumn The column for callouts that specify only a line.
   *
   * @return The modified result tree fragment.  */

  /**
   * <p>Insert graphical callouts into a verbatim environment.</p>
   *
   * <p>This method examines the <tt>areaset</tt> and <tt>area</tt> elements
   * in the supplied <tt>areaspec</tt> and decorates the supplied
   * result tree fragment with appropriate callout markers.</p>
   *
   * <p>If a <tt>label</tt> attribute is supplied on an <tt>area</tt>,
   * its content will be used for the label, otherwise the callout
   * number will be used. Callouts are
   * numbered in document order. All of the <tt>area</tt>s in an
   * <tt>areaset</tt> get the same number.</p>
   *
   * <p>If the callout number is not greater than <tt>gMax</tt>, the
   * callout generated will be:</p>
   *
   * <pre>
   * &lt;img src="$gPath/conumber$gExt" alt="conumber">
   * </pre>
   *
   * <p>Otherwise, it will be the callout number surrounded by
   * parenthesis.</p>
   *
   * <p>Only the <tt>linecolumn</tt> and <tt>linerange</tt> units are
   * supported. If no unit is specifed, <tt>linecolumn</tt> is assumed.
   * If only a line is specified, the callout decoration appears in
   * the defaultColumn. Lines will be padded with blanks to reach the
   * necessary column, but callouts that are located beyond the last
   * line of the verbatim environment will be ignored.</p>
   *
   * <p>Callouts are inserted before the character at the line/column
   * where they are to occur.</p>
   *
   * @param areaspecNodeSet The source node set that contains the areaspec.
   * @param xalanRTF The result tree fragment of the verbatim environment.
   * @param defaultColumn The column for callouts that specify only a line.
   * @param gPath The path to use for callout graphics.
   * @param gExt The extension to use for callout graphics.
   * @param gMax The largest number that can be represented as a graphic.
   * @param useFO Should fo:external-graphics be produced, as opposed to
   * HTML imgs. This is bogus, the extension should figure it out, but I
   * haven't figured out how to do that yet.
   *
   * @return The modified result tree fragment.
   */

  public DocumentFragment insertCallouts (ExpressionContext context,
					  NodeIterator areaspecNodeSet,
					  NodeIterator xalanNI) {
    String type = Params.getString(context, "stylesheet.result.type");
    boolean useFO = type.equals("fo");
    int defaultColumn = Params.getInt(context, "callout.defaultcolumn");

    if (Params.getBoolean(context, "callout.graphics")) {
      String gPath = Params.getString(context, "callout.graphics.path");
      String gExt = Params.getString(context, "callout.graphics.extension");
      int gMax = Params.getInt(context, "callout.graphics.number.limit");
      return insertGraphicCallouts(areaspecNodeSet, xalanNI, defaultColumn,
				   gPath, gExt, gMax, useFO);
    } else if (Params.getBoolean(context, "callout.unicode")) {
      int uStart = Params.getInt(context, "callout.unicode.start.character");
      int uMax = Params.getInt(context, "callout.unicode.number.limit");
      String uFont = Params.getString(context, "callout.unicode.font");
      return insertUnicodeCallouts(areaspecNodeSet, xalanNI, defaultColumn,
				   uFont, uStart, uMax, useFO);
    } else if (Params.getBoolean(context, "callout.dingbats")) {
      int dMax = 10;
      return insertDingbatCallouts(areaspecNodeSet, xalanNI, defaultColumn,
				   dMax, useFO);
    } else {
      return insertTextCallouts(areaspecNodeSet, xalanNI, defaultColumn, useFO);
    }
  }

  public DocumentFragment insertGraphicCallouts (NodeIterator areaspecNodeSet,
						 NodeIterator xalanNI,
						 int defaultColumn,
						 String gPath,
						 String gExt,
						 int gMax,
						 boolean useFO) {
    FormatGraphicCallout fgc = new FormatGraphicCallout(gPath,gExt,gMax,useFO);
    return insertCallouts(areaspecNodeSet, xalanNI, defaultColumn, fgc);
  }

  public DocumentFragment insertUnicodeCallouts (NodeIterator areaspecNodeSet,
						 NodeIterator xalanNI,
						 int defaultColumn,
						 String uFont,
						 int uStart,
						 int uMax,
						 boolean useFO) {
    FormatUnicodeCallout fuc = new FormatUnicodeCallout(uFont, uStart, uMax, useFO);
    return insertCallouts(areaspecNodeSet, xalanNI, defaultColumn, fuc);
  }

  public DocumentFragment insertDingbatCallouts (NodeIterator areaspecNodeSet,
						 NodeIterator xalanNI,
						 int defaultColumn,
						 int gMax,
						 boolean useFO) {
    FormatDingbatCallout fdc = new FormatDingbatCallout(gMax,useFO);
    return insertCallouts(areaspecNodeSet, xalanNI, defaultColumn, fdc);
  }

  public DocumentFragment insertTextCallouts (NodeIterator areaspecNodeSet,
					      NodeIterator xalanNI,
					      int defaultColumn,
					      boolean useFO) {
    FormatTextCallout ftc = new FormatTextCallout(useFO);
    return insertCallouts(areaspecNodeSet, xalanNI, defaultColumn, ftc);
  }

  public DocumentFragment insertCallouts (NodeIterator areaspecNodeSet,
					  NodeIterator xalanNI,
					  int defaultColumn,
					  FormatCallout fCallout) {

    DocumentFragment xalanRTF = (DocumentFragment) xalanNI.nextNode();

    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
    //  <areaspec>
    //  <areaset id="ex.plco.const" coords="">
    //    <area id="ex.plco.c1" coords="4"/>
    //    <area id="ex.plco.c2" coords="8"/>
    //  </areaset>
    //  <area id="ex.plco.ret" coords="12"/>
    //  <area id="ex.plco.dest" coords="12"/>
    //  </areaspec>
    int pos = 0;
    int coNum = 0;
    boolean inAreaSet = false;
    Node node = areaspecNodeSet.nextNode();
    node = node.getFirstChild();
    while (node != null) {
      if (node.getNodeType() == Node.ELEMENT_NODE) {
	if (node.getNodeName().equals("areaset")) {
	  coNum++;
	  Node area = node.getFirstChild();
	  while (area != null) {
	    if (area.getNodeType() == Node.ELEMENT_NODE) {
	      if (area.getNodeName().equals("area")) {
		addCallout(coNum, area, defaultColumn);
	      } else {
		System.out.println("Unexpected element in areaset: "
				   + area.getNodeName());
	      }
	    }
	    area = area.getNextSibling();
	  }
	} else if (node.getNodeName().equalsIgnoreCase("area")) {
	  coNum++;
	  addCallout(coNum, node, defaultColumn);
	} else {
	  System.out.println("Unexpected element in areaspec: "
			     + node.getNodeName());
	}
      }

      node = node.getNextSibling();
    }

    // Now sort them
    java.util.Arrays.sort(callout, 0, calloutCount);

    DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
    DocumentBuilder docBuilder = null;

    try {
      docBuilder = docFactory.newDocumentBuilder();
    } catch (ParserConfigurationException e) {
      System.out.println("PCE 2!");
      return xalanRTF;
    }
    Document doc = docBuilder.newDocument();
    DocumentFragment df = doc.createDocumentFragment();
    DOMBuilder db = new DOMBuilder(doc, df);

    elementStack = new Stack();
    calloutFragment(db, xalanRTF, fCallout);
    return df;
  }

  /**
   * <p>Build a FragmentValue with callout decorations.</p>
   *
   * <p>This is the method that actually does the work of adding
   * callouts to a verbatim environment. It recursively walks through a
   * tree of nodes, copying the structure into the rtf. Text nodes
   * are examined for the position of callouts as described by the
   * global callout parameters.</p>
   *
   * <p>When called, rtf should be an empty FragmentValue and node
   * should be the first child of the result tree fragment that contains
   * the existing, formatted verbatim text.</p>
   *
   * @param rtf The resulting verbatim environment with numbered lines.
   * @param node The root of the tree to copy.
   */
  private void calloutFragment(DOMBuilder rtf,
			       Node node,
			       FormatCallout fCallout) {
    try {
      if (node.getNodeType() == Node.DOCUMENT_FRAGMENT_NODE
	|| node.getNodeType() == Node.DOCUMENT_NODE) {
	Node child = node.getFirstChild();
	while (child != null) {
	  calloutFragment(rtf, child, fCallout);
	  child = child.getNextSibling();
	}
      } else if (node.getNodeType() == Node.ELEMENT_NODE) {
	String ns = node.getNamespaceURI();
	String localName = node.getLocalName();
	String name = ((Element) node).getTagName();

	rtf.startElement(ns, localName, name,
			 copyAttributes((Element) node));

	elementStack.push(node);

	Node child = node.getFirstChild();
	while (child != null) {
	  calloutFragment(rtf, child, fCallout);
	  child = child.getNextSibling();
	}
      } else if (node.getNodeType() == Node.TEXT_NODE) {
	String text = node.getNodeValue();

	char chars[] = text.toCharArray();
	int pos = 0;
	for (int count = 0; count < text.length(); count++) {
	  if (calloutPos < calloutCount
	      && callout[calloutPos].getLine() == lineNumber
	      && callout[calloutPos].getColumn() == colNumber) {
	    if (pos > 0) {
	      rtf.characters(chars, 0, pos);
	      pos = 0;
	    }

	    closeOpenElements(rtf);

	    while (calloutPos < calloutCount
		   && callout[calloutPos].getLine() == lineNumber
		   && callout[calloutPos].getColumn() == colNumber) {
	      fCallout.formatCallout(rtf, callout[calloutPos]);
	      calloutPos++;
	    }

	    openClosedElements(rtf);
	  }

	  if (text.charAt(count) == '\n') {
	    // What if we need to pad this line?
	    if (calloutPos < calloutCount
		&& callout[calloutPos].getLine() == lineNumber
		&& callout[calloutPos].getColumn() > colNumber) {

	      if (pos > 0) {
		rtf.characters(chars, 0, pos);
		pos = 0;
	      }

	      closeOpenElements(rtf);

	      while (calloutPos < calloutCount
		     && callout[calloutPos].getLine() == lineNumber
		     && callout[calloutPos].getColumn() > colNumber) {
		formatPad(rtf, callout[calloutPos].getColumn() - colNumber);
		colNumber = callout[calloutPos].getColumn();
		while (calloutPos < calloutCount
		       && callout[calloutPos].getLine() == lineNumber
		       && callout[calloutPos].getColumn() == colNumber) {
		  fCallout.formatCallout(rtf, callout[calloutPos]);
		  calloutPos++;
		}
	      }

	      openClosedElements(rtf);
	    }

	    lineNumber++;
	    colNumber = 1;
	  } else {
	    colNumber++;
	  }
	  chars[pos++] = text.charAt(count);
	}

	if (pos > 0) {
	  rtf.characters(chars, 0, pos);
	}
      } else if (node.getNodeType() == Node.COMMENT_NODE) {
	String text = node.getNodeValue();
	char chars[] = text.toCharArray();
	rtf.comment(chars, 0, text.length());
      } else if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) {
	rtf.processingInstruction(node.getNodeName(), node.getNodeValue());
      } else {
	System.out.println("Warning: unexpected node type in calloutFragment: " + node.getNodeType() + ": " + node.getNodeName());
      }

      if (node.getNodeType() == Node.ELEMENT_NODE) {
	String ns = node.getNamespaceURI();
	String localName = node.getLocalName();
	String name = ((Element) node).getTagName();
	rtf.endElement(ns, localName, name);
	elementStack.pop();
      } else {
	// nop
      }
    } catch (SAXException e) {
      System.out.println("SAX Exception in calloutFragment");
    }
  }

  /**
   * <p>Add a callout to the global callout array</p>
   *
   * <p>This method examines a callout <tt>area</tt> and adds it to
   * the global callout array if it can be interpreted.</p>
   *
   * <p>Only the <tt>linecolumn</tt> and <tt>linerange</tt> units are
   * supported. If no unit is specifed, <tt>linecolumn</tt> is assumed.
   * If only a line is specified, the callout decoration appears in
   * the <tt>defaultColumn</tt>.</p>
   *
   * @param coNum The callout number.
   * @param node The <tt>area</tt>.
   * @param defaultColumn The default column for callouts.
   */
  private void addCallout (int coNum,
			   Node node,
			   int defaultColumn) {
    Element area = (Element) node;

    String units = area.getAttribute("units");
    String otherUnits = area.getAttribute("otherunits");
    String coords = area.getAttribute("coords");
    int type = 0;
    String otherType = null;

    if (units == null || units.equals("linecolumn")) {
      type = Callout.LINE_COLUMN; // the default
    } else if (units.equals("linerange")) {
      type = Callout.LINE_RANGE;
    } else if (units.equals("linecolumnpair")) {
      type = Callout.LINE_COLUMN_PAIR;
    } else if (units.equals("calspair")) {
      type = Callout.CALS_PAIR;
    } else {
      type = Callout.OTHER;
      otherType = otherUnits;
    }

    if (type != Callout.LINE_COLUMN
	&& type != Callout.LINE_RANGE) {
      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 (type == Callout.LINE_RANGE) {
	for (int count = c1; count <= c2; count++) {
	  callout[calloutCount++] = new Callout(coNum, area,
						count, defaultColumn,
						type);
	}
      } else {
	// assume linecolumn
	callout[calloutCount++] = new Callout(coNum, area, c1, c2, type);
      }
    } else {
      // if there's only one number, assume it's the line
      callout[calloutCount++] = new Callout(coNum, area, c1, defaultColumn, type);
    }
  }

  /**
   * <p>Add blanks to the result tree fragment.</p>
   *
   * <p>This method adds <tt>numBlanks</tt> to the result tree fragment.
   * It's used to pad lines when callouts occur after the last existing
   * characater in a line.</p>
   *
   * @param rtf The resulting verbatim environment with numbered lines.
   * @param numBlanks The number of blanks to add.
   */
  private void formatPad(DOMBuilder rtf,
			 int numBlanks) {
    char chars[] = new char[numBlanks];
    for (int count = 0; count < numBlanks; count++) {
      chars[count] = ' ';
    }

    try {
      rtf.characters(chars, 0, numBlanks);
    } catch (SAXException e) {
      System.out.println("SAX Exception in formatCallout");
    }
  }

  private void closeOpenElements(DOMBuilder rtf)
    throws SAXException {
    // Close all the open elements...
    tempStack = new Stack();
    while (!elementStack.empty()) {
      Node elem = (Node) elementStack.pop();

      String ns = elem.getNamespaceURI();
      String localName = elem.getLocalName();
      String name = ((Element) elem).getTagName();

      // If this is the bottom of the stack and it's an fo:block
      // or an HTML pre or div, don't duplicate it...
      if (elementStack.empty()
	  && (((ns != null)
	       && ns.equals(foURI)
	       && localName.equals("block"))
	      || (((ns == null)
		   && localName.equalsIgnoreCase("pre"))
		  || ((ns != null)
		      && ns.equals(xhURI)
		      && localName.equals("pre")))
	      || (((ns == null)
		   && localName.equalsIgnoreCase("div"))
		  || ((ns != null)
		      && ns.equals(xhURI)
		      && localName.equals("div"))))) {
	elementStack.push(elem);
	break;
      } else {
	rtf.endElement(ns, localName, name);
	tempStack.push(elem);
      }
    }
  }

  private void openClosedElements(DOMBuilder rtf)
    throws SAXException {
    // Now "reopen" the elements that we closed...
    while (!tempStack.empty()) {
      Node elem = (Node) tempStack.pop();

      String ns = elem.getNamespaceURI();
      String localName = elem.getLocalName();
      String name = ((Element) elem).getTagName();
      NamedNodeMap domAttr = elem.getAttributes();

      AttributesImpl attr = new AttributesImpl();
      for (int acount = 0; acount < domAttr.getLength(); acount++) {
	Node a = domAttr.item(acount);

	if (((ns == null || ns == "http://www.w3.org/1999/xhtml")
	     && localName.equalsIgnoreCase("a"))
	    || (a.getLocalName().equalsIgnoreCase("id"))) {
	  // skip this attribute
	} else {
	  attr.addAttribute(a.getNamespaceURI(),
			    a.getLocalName(),
			    a.getNodeName(),
			    "CDATA",
			    a.getNodeValue());
	}
      }

      rtf.startElement(ns, localName, name, attr);
      elementStack.push(elem);
    }

    tempStack = null;
  }

  private Attributes copyAttributes(Element node) {
    AttributesImpl attrs = new AttributesImpl();
    NamedNodeMap nnm = node.getAttributes();
    for (int count = 0; count < nnm.getLength(); count++) {
      Attr attr = (Attr) nnm.item(count);
      String name = attr.getName();
      if (name.startsWith("xmlns:") || name.equals("xmlns")) {
	// Skip it; (don't ya just love it!!)
      } else {
	attrs.addAttribute(attr.getNamespaceURI(), attr.getName(),
			   attr.getName(), "CDATA", attr.getValue());
      }
    }
    return attrs;
  }
}