1// Verbatim.java - Xalan extensions supporting DocBook verbatim environments
2
3package com.nwalsh.xalan;
4
5import java.util.Stack;
6import java.util.StringTokenizer;
7
8import org.xml.sax.Attributes;
9import org.xml.sax.SAXException;
10
11import org.w3c.dom.Attr;
12import org.w3c.dom.Document;
13import org.w3c.dom.DocumentFragment;
14import org.w3c.dom.NamedNodeMap;
15import org.w3c.dom.Node;
16import org.w3c.dom.Element;
17import org.w3c.dom.traversal.NodeIterator;
18
19import org.apache.xpath.objects.XObject;
20import org.apache.xpath.XPath;
21import org.apache.xpath.XPathContext;
22import org.apache.xpath.NodeSet;
23import org.apache.xalan.extensions.XSLProcessorContext;
24import org.apache.xalan.extensions.ExpressionContext;
25import org.apache.xalan.transformer.TransformerImpl;
26import org.apache.xalan.templates.StylesheetRoot;
27import org.apache.xalan.templates.ElemExtensionCall;
28import org.apache.xalan.templates.OutputProperties;
29import org.apache.xalan.res.XSLTErrorResources;
30import org.apache.xml.utils.DOMBuilder;
31import org.apache.xml.utils.AttList;
32import org.apache.xml.utils.QName;
33
34import javax.xml.transform.stream.StreamResult;
35import javax.xml.transform.TransformerException;
36import javax.xml.parsers.DocumentBuilder;
37import javax.xml.parsers.DocumentBuilderFactory;
38import javax.xml.parsers.ParserConfigurationException;
39
40import com.nwalsh.xalan.Callout;
41import com.nwalsh.xalan.Params;
42import org.xml.sax.helpers.AttributesImpl;
43
44/**
45 * <p>Xalan extensions supporting DocBook verbatim environments</p>
46 *
47 * <p>$Id: Verbatim.java,v 1.2 2006/05/15 11:14:03 nwalsh Exp $</p>
48 *
49 * <p>Copyright (C) 2001 Norman Walsh.</p>
50 *
51 * <p>This class provides a
52 * <a href="http://xml.apache.org/xalan-j/">Xalan</a>
53 * implementation of two features that would be impractical to
54 * implement directly in XSLT: line numbering and callouts.</p>
55 *
56 * <p><b>Line Numbering</b></p>
57 * <p>The <tt>numberLines</tt> family of functions takes a result tree
58 * fragment (assumed to contain the contents of a formatted verbatim
59 * element in DocBook: programlisting, screen, address, literallayout,
60 * or synopsis) and returns a result tree fragment decorated with
61 * line numbers.</p>
62 *
63 * <p><b>Callouts</b></p>
64 * <p>The <tt>insertCallouts</tt> family of functions takes an
65 * <tt>areaspec</tt> and a result tree fragment
66 * (assumed to contain the contents of a formatted verbatim
67 * element in DocBook: programlisting, screen, address, literallayout,
68 * or synopsis) and returns a result tree fragment decorated with
69 * callouts.</p>
70 *
71 * <p><b>Change Log:</b></p>
72 * <dl>
73 * <dt>1.0</dt>
74 * <dd><p>Initial release.</p></dd>
75 * </dl>
76 *
77 * @author Norman Walsh
78 * <a href="mailto:ndw@nwalsh.com">ndw@nwalsh.com</a>
79 *
80 * @version $Id: Verbatim.java,v 1.2 2006/05/15 11:14:03 nwalsh Exp $
81 *
82 */
83public class Verbatim {
84  /** A stack to hold the open elements while walking through a RTF. */
85  private Stack elementStack = null;
86  /** A stack to hold the temporarily closed elements. */
87  private Stack tempStack = null;
88  /** The current line number. */
89  private int lineNumber = 0;
90  /** The current column number. */
91  private int colNumber = 0;
92  /** The modulus for line numbering (every 'modulus' line is numbered). */
93  private int modulus = 0;
94  /** The width (in characters) of line numbers (for padding). */
95  private int width = 0;
96  /** The separator between the line number and the verbatim text. */
97  private String separator = "";
98  /** The (sorted) array of callouts obtained from the areaspec. */
99  private Callout callout[] = null;
100  /** The number of callouts in the callout array. */
101  private int calloutCount = 0;
102  /** A pointer used to keep track of our position in the callout array. */
103  private int calloutPos = 0;
104  /** The path to use for graphical callout decorations. */
105  private String graphicsPath = null;
106  /** The extension to use for graphical callout decorations. */
107  private String graphicsExt = null;
108  /** The largest callout number that can be represented graphically. */
109  private int graphicsMax = 10;
110  /** Should graphic callouts use fo:external-graphics or imgs. */
111  private boolean graphicsFO = false;
112
113  private static final String foURI = "http://www.w3.org/1999/XSL/Format";
114  private static final String xhURI = "http://www.w3.org/1999/xhtml";
115
116  /**
117   * <p>Constructor for Verbatim</p>
118   *
119   * <p>All of the methods are static, so the constructor does nothing.</p>
120   */
121  public Verbatim() {
122  }
123
124  /**
125   * <p>Number lines in a verbatim environment.</p>
126   *
127   * <p>This method adds line numbers to a result tree fragment. Each
128   * newline that occurs in a text node is assumed to start a new line.
129   * The first line is always numbered, every subsequent xalanMod line
130   * is numbered (so if xalanMod=5, lines 1, 5, 10, 15, etc. will be
131   * numbered. If there are fewer than xalanMod lines in the environment,
132   * every line is numbered.</p>
133   *
134   * <p>xalanMod is taken from the $linenumbering.everyNth parameter.</p>
135   *
136   * <p>Every line number will be right justified in a string xalanWidth
137   * characters long. If the line number of the last line in the
138   * environment is too long to fit in the specified width, the width
139   * is automatically increased to the smallest value that can hold the
140   * number of the last line. (In other words, if you specify the value 2
141   * and attempt to enumerate the lines of an environment that is 100 lines
142   * long, the value 3 will automatically be used for every line in the
143   * environment.)</p>
144   *
145   * <p>xalanWidth is taken from the $linenumbering.width parameter.</p>
146   *
147   * <p>The xalanSep string is inserted between the line
148   * number and the original program listing. Lines that aren't numbered
149   * are preceded by a xalanWidth blank string and the separator.</p>
150   *
151   * <p>xalanSep is taken from the $linenumbering.separator parameter.</p>
152   *
153   * <p>If inline markup extends across line breaks, markup changes are
154   * required. All the open elements are closed before the line break and
155   * "reopened" afterwards. The reopened elements will have the same
156   * attributes as the originals, except that 'name' and 'id' attributes
157   * are not duplicated.</p>
158   *
159   * @param context
160   * @param xalanNI
161   *
162   * @return The modified result tree fragment.
163   */
164  public DocumentFragment numberLines (ExpressionContext context,
165				       NodeIterator xalanNI) {
166
167    int xalanMod = Params.getInt(context, "linenumbering.everyNth");
168    int xalanWidth = Params.getInt(context, "linenumbering.width");
169    String xalanSep = Params.getString(context, "linenumbering.separator");
170
171    DocumentFragment xalanRTF = (DocumentFragment) xalanNI.nextNode();
172    int numLines = countLineBreaks(xalanRTF) + 1;
173
174    DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
175    DocumentBuilder docBuilder = null;
176
177    try {
178      docBuilder = docFactory.newDocumentBuilder();
179    } catch (ParserConfigurationException e) {
180      System.out.println("PCE!");
181      return xalanRTF;
182    }
183    Document doc = docBuilder.newDocument();
184    DocumentFragment df = doc.createDocumentFragment();
185    DOMBuilder db = new DOMBuilder(doc, df);
186
187    elementStack = new Stack();
188    lineNumber = 0;
189    modulus = numLines < xalanMod ? 1 : xalanMod;
190    width = xalanWidth;
191    separator = xalanSep;
192
193    double log10numLines = Math.log(numLines) / Math.log(10);
194
195    if (width < log10numLines + 1) {
196      width = (int) Math.floor(log10numLines + 1);
197    }
198
199    lineNumberFragment(db, xalanRTF);
200    return df;
201  }
202
203  /**
204   * <p>Count the number of lines in a verbatim environment.</p>
205   *
206   * <p>This method walks over the nodes of a DocumentFragment and
207   * returns the number of lines breaks that it contains.</p>
208   *
209   * @param node The root of the tree walk over.
210   */
211  private int countLineBreaks(Node node) {
212    int numLines = 0;
213
214    if (node.getNodeType() == Node.DOCUMENT_FRAGMENT_NODE
215	|| node.getNodeType() == Node.DOCUMENT_NODE
216	|| node.getNodeType() == Node.ELEMENT_NODE) {
217      Node child = node.getFirstChild();
218      while (child != null) {
219	numLines += countLineBreaks(child);
220	child = child.getNextSibling();
221      }
222    } else if (node.getNodeType() == Node.TEXT_NODE) {
223      String text = node.getNodeValue();
224
225      // Walk through the text node looking for newlines
226      int pos = 0;
227      for (int count = 0; count < text.length(); count++) {
228	if (text.charAt(count) == '\n') {
229	  numLines++;
230	}
231      }
232    } else {
233      // nop
234    }
235
236    return numLines;
237  }
238
239  /**
240   * <p>Build a DocumentFragment with numbered lines.</p>
241   *
242   * <p>This is the method that actually does the work of numbering
243   * lines in a verbatim environment. It recursively walks through a
244   * tree of nodes, copying the structure into the rtf. Text nodes
245   * are examined for new lines and modified as requested by the
246   * global line numbering parameters.</p>
247   *
248   * <p>When called, rtf should be an empty DocumentFragment and node
249   * should be the first child of the result tree fragment that contains
250   * the existing, formatted verbatim text.</p>
251   *
252   * @param rtf The resulting verbatim environment with numbered lines.
253   * @param node The root of the tree to copy.
254   */
255  private void lineNumberFragment(DOMBuilder rtf,
256				  Node node) {
257    try {
258      if (node.getNodeType() == Node.DOCUMENT_FRAGMENT_NODE
259	  || node.getNodeType() == Node.DOCUMENT_NODE) {
260	Node child = node.getFirstChild();
261	while (child != null) {
262	  lineNumberFragment(rtf, child);
263	  child = child.getNextSibling();
264	}
265      } else if (node.getNodeType() == Node.ELEMENT_NODE) {
266	String ns = node.getNamespaceURI();
267	String localName = node.getLocalName();
268	String name = ((Element) node).getTagName();
269
270	rtf.startElement(ns, localName, name,
271			 copyAttributes((Element) node));
272
273	elementStack.push(node);
274
275	Node child = node.getFirstChild();
276	while (child != null) {
277	  lineNumberFragment(rtf, child);
278	  child = child.getNextSibling();
279	}
280      } else if (node.getNodeType() == Node.TEXT_NODE) {
281	String text = node.getNodeValue();
282
283	if (lineNumber == 0) {
284	  // The first line is always numbered
285	  formatLineNumber(rtf, ++lineNumber);
286	}
287
288	// Walk through the text node looking for newlines
289	char chars[] = text.toCharArray();
290	int pos = 0;
291	for (int count = 0; count < text.length(); count++) {
292	  if (text.charAt(count) == '\n') {
293	    // This is the tricky bit; if we find a newline, make sure
294	    // it doesn't occur inside any markup.
295
296	    if (pos > 0) {
297	      rtf.characters(chars, 0, pos);
298	      pos = 0;
299	    }
300
301	    closeOpenElements(rtf);
302
303	    // Copy the newline to the output
304	    chars[pos++] = text.charAt(count);
305	    rtf.characters(chars, 0, pos);
306	    pos = 0;
307
308	    // Add the line number
309	    formatLineNumber(rtf, ++lineNumber);
310
311	    openClosedElements(rtf);
312	  } else {
313	    chars[pos++] = text.charAt(count);
314	  }
315	}
316
317	if (pos > 0) {
318	  rtf.characters(chars, 0, pos);
319	}
320      } else if (node.getNodeType() == Node.COMMENT_NODE) {
321	String text = node.getNodeValue();
322	char chars[] = text.toCharArray();
323	rtf.comment(chars, 0, text.length());
324      } else if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) {
325	rtf.processingInstruction(node.getNodeName(), node.getNodeValue());
326      } else {
327	System.out.println("Warning: unexpected node type in lineNumberFragment");
328      }
329
330      if (node.getNodeType() == Node.ELEMENT_NODE) {
331	String ns = node.getNamespaceURI();
332	String localName = node.getLocalName();
333	String name = ((Element) node).getTagName();
334	rtf.endElement(ns, localName, name);
335	elementStack.pop();
336      }
337    } catch (SAXException e) {
338      System.out.println("SAX Exception in lineNumberFragment");
339    }
340  }
341
342  /**
343   * <p>Add a formatted line number to the result tree fragment.</p>
344   *
345   * <p>This method examines the global parameters that control line
346   * number presentation (modulus, width, and separator) and adds
347   * the appropriate text to the result tree fragment.</p>
348   *
349   * @param rtf The resulting verbatim environment with numbered lines.
350   * @param lineNumber The number of the current line.
351   */
352  private void formatLineNumber(DOMBuilder rtf,
353				int lineNumber) {
354    char ch = 160;
355    String lno = "";
356    if (lineNumber == 1
357	|| (modulus >= 1 && (lineNumber % modulus == 0))) {
358      lno = "" + lineNumber;
359    }
360
361    while (lno.length() < width) {
362      lno = ch + lno;
363    }
364
365    lno += separator;
366
367    char chars[] = lno.toCharArray();
368    try {
369      rtf.characters(chars, 0, lno.length());
370    } catch (SAXException e) {
371      System.out.println("SAX Exception in formatLineNumber");
372    }
373  }
374
375  /**
376   * <p>Insert text callouts into a verbatim environment.</p>
377   *
378   * <p>This method examines the <tt>areaset</tt> and <tt>area</tt> elements
379   * in the supplied <tt>areaspec</tt> and decorates the supplied
380   * result tree fragment with appropriate callout markers.</p>
381   *
382   * <p>If a <tt>label</tt> attribute is supplied on an <tt>area</tt>,
383   * its content will be used for the label, otherwise the callout
384   * number will be used, surrounded by parenthesis. Callouts are
385   * numbered in document order. All of the <tt>area</tt>s in an
386   * <tt>areaset</tt> get the same number.</p>
387   *
388   * <p>Only the <tt>linecolumn</tt> and <tt>linerange</tt> units are
389   * supported. If no unit is specifed, <tt>linecolumn</tt> is assumed.
390   * If only a line is specified, the callout decoration appears in
391   * the defaultColumn. Lines will be padded with blanks to reach the
392   * necessary column, but callouts that are located beyond the last
393   * line of the verbatim environment will be ignored.</p>
394   *
395   * <p>Callouts are inserted before the character at the line/column
396   * where they are to occur.</p>
397   *
398   * @param areaspecNodeSet The source node set that contains the areaspec.
399   * @param xalanRTF The result tree fragment of the verbatim environment.
400   * @param defaultColumn The column for callouts that specify only a line.
401   *
402   * @return The modified result tree fragment.  */
403
404  /**
405   * <p>Insert graphical callouts into a verbatim environment.</p>
406   *
407   * <p>This method examines the <tt>areaset</tt> and <tt>area</tt> elements
408   * in the supplied <tt>areaspec</tt> and decorates the supplied
409   * result tree fragment with appropriate callout markers.</p>
410   *
411   * <p>If a <tt>label</tt> attribute is supplied on an <tt>area</tt>,
412   * its content will be used for the label, otherwise the callout
413   * number will be used. Callouts are
414   * numbered in document order. All of the <tt>area</tt>s in an
415   * <tt>areaset</tt> get the same number.</p>
416   *
417   * <p>If the callout number is not greater than <tt>gMax</tt>, the
418   * callout generated will be:</p>
419   *
420   * <pre>
421   * &lt;img src="$gPath/conumber$gExt" alt="conumber">
422   * </pre>
423   *
424   * <p>Otherwise, it will be the callout number surrounded by
425   * parenthesis.</p>
426   *
427   * <p>Only the <tt>linecolumn</tt> and <tt>linerange</tt> units are
428   * supported. If no unit is specifed, <tt>linecolumn</tt> is assumed.
429   * If only a line is specified, the callout decoration appears in
430   * the defaultColumn. Lines will be padded with blanks to reach the
431   * necessary column, but callouts that are located beyond the last
432   * line of the verbatim environment will be ignored.</p>
433   *
434   * <p>Callouts are inserted before the character at the line/column
435   * where they are to occur.</p>
436   *
437   * @param context
438   * @param areaspecNodeSet The source node set that contains the areaspec.
439   * @param xalanNI
440   *
441   * @return The modified result tree fragment.
442   */
443  public DocumentFragment insertCallouts (ExpressionContext context,
444					  NodeIterator areaspecNodeSet,
445					  NodeIterator xalanNI) {
446    String type = Params.getString(context, "stylesheet.result.type");
447    boolean useFO = type.equals("fo");
448    int defaultColumn = Params.getInt(context, "callout.defaultcolumn");
449
450    if (Params.getBoolean(context, "callout.graphics")) {
451      String gPath = Params.getString(context, "callout.graphics.path");
452      String gExt = Params.getString(context, "callout.graphics.extension");
453      int gMax = Params.getInt(context, "callout.graphics.number.limit");
454      return insertGraphicCallouts(areaspecNodeSet, xalanNI, defaultColumn,
455				   gPath, gExt, gMax, useFO);
456    } else if (Params.getBoolean(context, "callout.unicode")) {
457      int uStart = Params.getInt(context, "callout.unicode.start.character");
458      int uMax = Params.getInt(context, "callout.unicode.number.limit");
459      String uFont = Params.getString(context, "callout.unicode.font");
460      return insertUnicodeCallouts(areaspecNodeSet, xalanNI, defaultColumn,
461				   uFont, uStart, uMax, useFO);
462    } else if (Params.getBoolean(context, "callout.dingbats")) {
463      int dMax = 10;
464      return insertDingbatCallouts(areaspecNodeSet, xalanNI, defaultColumn,
465				   dMax, useFO);
466    } else {
467      return insertTextCallouts(areaspecNodeSet, xalanNI, defaultColumn, useFO);
468    }
469  }
470
471  public DocumentFragment insertGraphicCallouts (NodeIterator areaspecNodeSet,
472						 NodeIterator xalanNI,
473						 int defaultColumn,
474						 String gPath,
475						 String gExt,
476						 int gMax,
477						 boolean useFO) {
478    FormatGraphicCallout fgc = new FormatGraphicCallout(gPath,gExt,gMax,useFO);
479    return insertCallouts(areaspecNodeSet, xalanNI, defaultColumn, fgc);
480  }
481
482  public DocumentFragment insertUnicodeCallouts (NodeIterator areaspecNodeSet,
483						 NodeIterator xalanNI,
484						 int defaultColumn,
485						 String uFont,
486						 int uStart,
487						 int uMax,
488						 boolean useFO) {
489    FormatUnicodeCallout fuc = new FormatUnicodeCallout(uFont, uStart, uMax, useFO);
490    return insertCallouts(areaspecNodeSet, xalanNI, defaultColumn, fuc);
491  }
492
493  public DocumentFragment insertDingbatCallouts (NodeIterator areaspecNodeSet,
494						 NodeIterator xalanNI,
495						 int defaultColumn,
496						 int gMax,
497						 boolean useFO) {
498    FormatDingbatCallout fdc = new FormatDingbatCallout(gMax,useFO);
499    return insertCallouts(areaspecNodeSet, xalanNI, defaultColumn, fdc);
500  }
501
502  public DocumentFragment insertTextCallouts (NodeIterator areaspecNodeSet,
503					      NodeIterator xalanNI,
504					      int defaultColumn,
505					      boolean useFO) {
506    FormatTextCallout ftc = new FormatTextCallout(useFO);
507    return insertCallouts(areaspecNodeSet, xalanNI, defaultColumn, ftc);
508  }
509
510  public DocumentFragment insertCallouts (NodeIterator areaspecNodeSet,
511					  NodeIterator xalanNI,
512					  int defaultColumn,
513					  FormatCallout fCallout) {
514
515    DocumentFragment xalanRTF = (DocumentFragment) xalanNI.nextNode();
516
517    callout = new Callout[10];
518    calloutCount = 0;
519    calloutPos = 0;
520    lineNumber = 1;
521    colNumber = 1;
522
523    // First we walk through the areaspec to calculate the position
524    // of the callouts
525    //  <areaspec>
526    //  <areaset id="ex.plco.const" coords="">
527    //    <area id="ex.plco.c1" coords="4"/>
528    //    <area id="ex.plco.c2" coords="8"/>
529    //  </areaset>
530    //  <area id="ex.plco.ret" coords="12"/>
531    //  <area id="ex.plco.dest" coords="12"/>
532    //  </areaspec>
533    int pos = 0;
534    int coNum = 0;
535    boolean inAreaSet = false;
536    Node node = areaspecNodeSet.nextNode();
537    node = node.getFirstChild();
538    while (node != null) {
539      if (node.getNodeType() == Node.ELEMENT_NODE) {
540	if (node.getNodeName().equals("areaset")) {
541	  coNum++;
542	  Node area = node.getFirstChild();
543	  while (area != null) {
544	    if (area.getNodeType() == Node.ELEMENT_NODE) {
545	      if (area.getNodeName().equals("area")) {
546		addCallout(coNum, area, defaultColumn);
547	      } else {
548		System.out.println("Unexpected element in areaset: "
549				   + area.getNodeName());
550	      }
551	    }
552	    area = area.getNextSibling();
553	  }
554	} else if (node.getNodeName().equalsIgnoreCase("area")) {
555	  coNum++;
556	  addCallout(coNum, node, defaultColumn);
557	} else {
558	  System.out.println("Unexpected element in areaspec: "
559			     + node.getNodeName());
560	}
561      }
562
563      node = node.getNextSibling();
564    }
565
566    // Now sort them
567    java.util.Arrays.sort(callout, 0, calloutCount);
568
569    DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
570    DocumentBuilder docBuilder = null;
571
572    try {
573      docBuilder = docFactory.newDocumentBuilder();
574    } catch (ParserConfigurationException e) {
575      System.out.println("PCE 2!");
576      return xalanRTF;
577    }
578    Document doc = docBuilder.newDocument();
579    DocumentFragment df = doc.createDocumentFragment();
580    DOMBuilder db = new DOMBuilder(doc, df);
581
582    elementStack = new Stack();
583    calloutFragment(db, xalanRTF, fCallout);
584    return df;
585  }
586
587  /**
588   * <p>Build a FragmentValue with callout decorations.</p>
589   *
590   * <p>This is the method that actually does the work of adding
591   * callouts to a verbatim environment. It recursively walks through a
592   * tree of nodes, copying the structure into the rtf. Text nodes
593   * are examined for the position of callouts as described by the
594   * global callout parameters.</p>
595   *
596   * <p>When called, rtf should be an empty FragmentValue and node
597   * should be the first child of the result tree fragment that contains
598   * the existing, formatted verbatim text.</p>
599   *
600   * @param rtf The resulting verbatim environment with numbered lines.
601   * @param node The root of the tree to copy.
602   */
603  private void calloutFragment(DOMBuilder rtf,
604			       Node node,
605			       FormatCallout fCallout) {
606    try {
607      if (node.getNodeType() == Node.DOCUMENT_FRAGMENT_NODE
608	|| node.getNodeType() == Node.DOCUMENT_NODE) {
609	Node child = node.getFirstChild();
610	while (child != null) {
611	  calloutFragment(rtf, child, fCallout);
612	  child = child.getNextSibling();
613	}
614      } else if (node.getNodeType() == Node.ELEMENT_NODE) {
615	String ns = node.getNamespaceURI();
616	String localName = node.getLocalName();
617	String name = ((Element) node).getTagName();
618
619	rtf.startElement(ns, localName, name,
620			 copyAttributes((Element) node));
621
622	elementStack.push(node);
623
624	Node child = node.getFirstChild();
625	while (child != null) {
626	  calloutFragment(rtf, child, fCallout);
627	  child = child.getNextSibling();
628	}
629      } else if (node.getNodeType() == Node.TEXT_NODE) {
630	String text = node.getNodeValue();
631
632	char chars[] = text.toCharArray();
633	int pos = 0;
634	for (int count = 0; count < text.length(); count++) {
635	  if (calloutPos < calloutCount
636	      && callout[calloutPos].getLine() == lineNumber
637	      && callout[calloutPos].getColumn() == colNumber) {
638
639	    if (pos > 0) {
640	      rtf.characters(chars, 0, pos);
641	      pos = 0;
642	    }
643
644	    closeOpenElements(rtf);
645
646	    while (calloutPos < calloutCount
647		   && callout[calloutPos].getLine() == lineNumber
648		   && callout[calloutPos].getColumn() == colNumber) {
649	      fCallout.formatCallout(rtf, callout[calloutPos]);
650	      calloutPos++;
651	    }
652
653	    openClosedElements(rtf);
654	  }
655
656	  if (text.charAt(count) == '\n') {
657	    // What if we need to pad this line?
658	    if (calloutPos < calloutCount
659		&& callout[calloutPos].getLine() == lineNumber
660		&& callout[calloutPos].getColumn() > colNumber) {
661
662	      if (pos > 0) {
663		rtf.characters(chars, 0, pos);
664		pos = 0;
665	      }
666
667	      closeOpenElements(rtf);
668
669	      while (calloutPos < calloutCount
670		     && callout[calloutPos].getLine() == lineNumber
671		     && callout[calloutPos].getColumn() > colNumber) {
672		formatPad(rtf, callout[calloutPos].getColumn() - colNumber);
673		colNumber = callout[calloutPos].getColumn();
674		while (calloutPos < calloutCount
675		       && callout[calloutPos].getLine() == lineNumber
676		       && callout[calloutPos].getColumn() == colNumber) {
677		  fCallout.formatCallout(rtf, callout[calloutPos]);
678		  calloutPos++;
679		}
680	      }
681
682	      openClosedElements(rtf);
683	    }
684
685	    lineNumber++;
686	    colNumber = 1;
687	  } else {
688	    colNumber++;
689	  }
690	  chars[pos++] = text.charAt(count);
691	}
692
693	if (pos > 0) {
694	  rtf.characters(chars, 0, pos);
695	}
696      } else if (node.getNodeType() == Node.COMMENT_NODE) {
697	String text = node.getNodeValue();
698	char chars[] = text.toCharArray();
699	rtf.comment(chars, 0, text.length());
700      } else if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) {
701	rtf.processingInstruction(node.getNodeName(), node.getNodeValue());
702      } else {
703	System.out.println("Warning: unexpected node type in calloutFragment: " + node.getNodeType() + ": " + node.getNodeName());
704      }
705
706      if (node.getNodeType() == Node.ELEMENT_NODE) {
707	String ns = node.getNamespaceURI();
708	String localName = node.getLocalName();
709	String name = ((Element) node).getTagName();
710	rtf.endElement(ns, localName, name);
711	elementStack.pop();
712      } else {
713	// nop
714      }
715    } catch (SAXException e) {
716      System.out.println("SAX Exception in calloutFragment");
717    }
718  }
719
720  /**
721   * <p>Add a callout to the global callout array</p>
722   *
723   * <p>This method examines a callout <tt>area</tt> and adds it to
724   * the global callout array if it can be interpreted.</p>
725   *
726   * <p>Only the <tt>linecolumn</tt> and <tt>linerange</tt> units are
727   * supported. If no unit is specifed, <tt>linecolumn</tt> is assumed.
728   * If only a line is specified, the callout decoration appears in
729   * the <tt>defaultColumn</tt>.</p>
730   *
731   * @param coNum The callout number.
732   * @param node The <tt>area</tt>.
733   * @param defaultColumn The default column for callouts.
734   */
735  private void addCallout (int coNum,
736			   Node node,
737			   int defaultColumn) {
738    Element area = (Element) node;
739
740    String units = area.getAttribute("units");
741    String otherUnits = area.getAttribute("otherunits");
742    String coords = area.getAttribute("coords");
743    int type = 0;
744    String otherType = null;
745
746    if ("".equals(units) || units.equals("linecolumn")) {
747      type = Callout.LINE_COLUMN; // the default
748    } else if (units.equals("linerange")) {
749      type = Callout.LINE_RANGE;
750    } else if (units.equals("linecolumnpair")) {
751      type = Callout.LINE_COLUMN_PAIR;
752    } else if (units.equals("calspair")) {
753      type = Callout.CALS_PAIR;
754    } else {
755      type = Callout.OTHER;
756      otherType = otherUnits;
757    }
758
759    if (type != Callout.LINE_COLUMN
760	&& type != Callout.LINE_RANGE) {
761      System.out.println("Only linecolumn and linerange units are supported");
762      return;
763    }
764
765    if (coords == null) {
766      System.out.println("Coords must be specified");
767      return;
768    }
769
770    // Now let's see if we can interpret the coordinates...
771    StringTokenizer st = new StringTokenizer(coords);
772    int tokenCount = 0;
773    int c1 = 0;
774    int c2 = 0;
775    while (st.hasMoreTokens()) {
776      tokenCount++;
777      if (tokenCount > 2) {
778	System.out.println("Unparseable coordinates");
779	return;
780      }
781      try {
782	String token = st.nextToken();
783	int coord = Integer.parseInt(token);
784	c2 = coord;
785	if (tokenCount == 1) {
786	  c1 = coord;
787	}
788      } catch (NumberFormatException e) {
789	System.out.println("Unparseable coordinate");
790	return;
791      }
792    }
793
794    // Make sure we aren't going to blow past the end of our array
795    if (calloutCount == callout.length) {
796      Callout bigger[] = new Callout[calloutCount+10];
797      for (int count = 0; count < callout.length; count++) {
798	bigger[count] = callout[count];
799      }
800      callout = bigger;
801    }
802
803    // Ok, add the callout
804    if (tokenCount == 2) {
805      if (type == Callout.LINE_RANGE) {
806	for (int count = c1; count <= c2; count++) {
807	  callout[calloutCount++] = new Callout(coNum, area,
808						count, defaultColumn,
809						type);
810	}
811      } else {
812	// assume linecolumn
813	callout[calloutCount++] = new Callout(coNum, area, c1, c2, type);
814      }
815    } else {
816      // if there's only one number, assume it's the line
817      callout[calloutCount++] = new Callout(coNum, area, c1, defaultColumn, type);
818    }
819  }
820
821  /**
822   * <p>Add blanks to the result tree fragment.</p>
823   *
824   * <p>This method adds <tt>numBlanks</tt> to the result tree fragment.
825   * It's used to pad lines when callouts occur after the last existing
826   * characater in a line.</p>
827   *
828   * @param rtf The resulting verbatim environment with numbered lines.
829   * @param numBlanks The number of blanks to add.
830   */
831  private void formatPad(DOMBuilder rtf,
832			 int numBlanks) {
833    char chars[] = new char[numBlanks];
834    for (int count = 0; count < numBlanks; count++) {
835      chars[count] = ' ';
836    }
837
838    try {
839      rtf.characters(chars, 0, numBlanks);
840    } catch (SAXException e) {
841      System.out.println("SAX Exception in formatCallout");
842    }
843  }
844
845  private void closeOpenElements(DOMBuilder rtf)
846    throws SAXException {
847    // Close all the open elements...
848    tempStack = new Stack();
849    while (!elementStack.empty()) {
850      Node elem = (Node) elementStack.pop();
851
852      String ns = elem.getNamespaceURI();
853      String localName = elem.getLocalName();
854      String name = ((Element) elem).getTagName();
855
856      // If this is the bottom of the stack and it's an fo:block
857      // or an HTML pre or div, don't duplicate it...
858      if (elementStack.empty()
859	  && (((ns != null)
860	       && ns.equals(foURI)
861	       && localName.equals("block"))
862	      || (((ns == null)
863		   && localName.equalsIgnoreCase("pre"))
864		  || ((ns != null)
865		      && ns.equals(xhURI)
866		      && localName.equals("pre")))
867	      || (((ns == null)
868		   && localName.equalsIgnoreCase("div"))
869		  || ((ns != null)
870		      && ns.equals(xhURI)
871		      && localName.equals("div"))))) {
872	elementStack.push(elem);
873	break;
874      } else {
875	rtf.endElement(ns, localName, name);
876	tempStack.push(elem);
877      }
878    }
879  }
880
881  private void openClosedElements(DOMBuilder rtf)
882    throws SAXException {
883    // Now "reopen" the elements that we closed...
884    while (!tempStack.empty()) {
885      Node elem = (Node) tempStack.pop();
886
887      String ns = elem.getNamespaceURI();
888      String localName = elem.getLocalName();
889      String name = ((Element) elem).getTagName();
890      NamedNodeMap domAttr = elem.getAttributes();
891
892      AttributesImpl attr = new AttributesImpl();
893      for (int acount = 0; acount < domAttr.getLength(); acount++) {
894	Node a = domAttr.item(acount);
895
896	if (((ns == null || ns == "http://www.w3.org/1999/xhtml")
897	     && localName.equalsIgnoreCase("a"))
898	    || (a.getLocalName().equalsIgnoreCase("id"))) {
899	  // skip this attribute
900	} else {
901	  attr.addAttribute(a.getNamespaceURI(),
902			    a.getLocalName(),
903			    a.getNodeName(),
904			    "CDATA",
905			    a.getNodeValue());
906	}
907      }
908
909      rtf.startElement(ns, localName, name, attr);
910      elementStack.push(elem);
911    }
912
913    tempStack = null;
914  }
915
916  private Attributes copyAttributes(Element node) {
917    AttributesImpl attrs = new AttributesImpl();
918    NamedNodeMap nnm = node.getAttributes();
919    for (int count = 0; count < nnm.getLength(); count++) {
920      Attr attr = (Attr) nnm.item(count);
921      String name = attr.getName();
922      if (name.startsWith("xmlns:") || name.equals("xmlns")) {
923	// Skip it; (don't ya just love it!!)
924      } else {
925	attrs.addAttribute(attr.getNamespaceURI(), attr.getName(),
926			   attr.getName(), "CDATA", attr.getValue());
927      }
928    }
929    return attrs;
930  }
931}
932