1package com.nwalsh.saxon;
2
3import java.util.Stack;
4import java.util.StringTokenizer;
5import org.xml.sax.*;
6import org.w3c.dom.*;
7import javax.xml.transform.TransformerException;
8import com.icl.saxon.Controller;
9import com.icl.saxon.om.NamePool;
10import com.icl.saxon.output.Emitter;
11import com.icl.saxon.tree.AttributeCollection;
12
13/**
14 * <p>Saxon extension to decorate a result tree fragment with callouts.</p>
15 *
16 * <p>$Id: CalloutEmitter.java,v 1.3 2006/04/27 08:26:47 xmldoc Exp $</p>
17 *
18 * <p>Copyright (C) 2000 Norman Walsh.</p>
19 *
20 * <p>This class provides the guts of a
21 * <a href="http://saxon.sourceforge.net/">Saxon 6.*</a>
22 * implementation of callouts for verbatim environments. (It is used
23 * by the Verbatim class.)</p>
24 *
25 * <p>The general design is this: the stylesheets construct a result tree
26 * fragment for some verbatim environment. The Verbatim class initializes
27 * a CalloutEmitter with information about the callouts that should be applied
28 * to the verbatim environment in question. Then the result tree fragment
29 * is "replayed" through the CalloutEmitter; the CalloutEmitter builds a
30 * new result tree fragment from this event stream, decorated with callouts,
31 * and that is returned.</p>
32 *
33 * <p><b>Change Log:</b></p>
34 * <dl>
35 * <dt>1.0</dt>
36 * <dd><p>Initial release.</p></dd>
37 * </dl>
38 *
39 * @see Verbatim
40 *
41 * @author Norman Walsh
42 * <a href="mailto:ndw@nwalsh.com">ndw@nwalsh.com</a>
43 *
44 * @version $Id: CalloutEmitter.java,v 1.3 2006/04/27 08:26:47 xmldoc Exp $
45 *
46 */
47public class CalloutEmitter extends CopyEmitter {
48  /** A stack for the preserving information about open elements. */
49  protected Stack elementStack = null;
50
51  /** A stack for holding information about temporarily closed elements. */
52  protected Stack tempStack = null;
53
54  /** Is the next element absolutely the first element in the fragment? */
55  protected boolean firstElement = false;
56
57  /** The FO namespace name. */
58  protected static String foURI = "http://www.w3.org/1999/XSL/Format";
59
60  /** The XHTML namespace name. */
61  protected static String xhURI = "http://www.w3.org/1999/xhtml";
62
63  /** The default column for callouts that specify only a line. */
64  protected int defaultColumn = 60;
65
66  /** Is the stylesheet currently running an FO stylesheet? */
67  protected boolean foStylesheet = false;
68
69  /** The current line number. */
70  private static int lineNumber = 0;
71
72  /** The current column number. */
73  private static int colNumber = 0;
74
75  /** The (sorted) array of callouts obtained from the areaspec. */
76  private static Callout callout[] = null;
77
78  /** The number of callouts in the callout array. */
79  private static int calloutCount = 0;
80
81  /** A pointer used to keep track of our position in the callout array. */
82  private static int calloutPos = 0;
83
84  /** The FormatCallout object to use for formatting callouts. */
85  private static FormatCallout fCallout = null;
86
87  /** <p>Constructor for the CalloutEmitter.</p>
88   *
89   * @param controller
90   * @param namePool The name pool to use for constructing elements and attributes.
91   * @param defaultColumn The default column for callouts.
92   * @param foStylesheet Is this an FO stylesheet?
93   * @param fCallout
94   */
95  public CalloutEmitter(Controller controller,
96			NamePool namePool,
97			int defaultColumn,
98			boolean foStylesheet,
99			FormatCallout fCallout) {
100    super(controller, namePool);
101    elementStack = new Stack();
102    firstElement = true;
103
104    this.defaultColumn = defaultColumn;
105    this.foStylesheet = foStylesheet;
106    this.fCallout = fCallout;
107  }
108
109  /**
110   * <p>Examine the areaspec and determine the number and position of
111   * callouts.</p>
112   *
113   * <p>The <code><a href="http://docbook.org/tdg/html/areaspec.html">areaspecNodeSet</a></code>
114   * is examined and a sorted list of the callouts is constructed.</p>
115   *
116   * <p>This data structure is used to augment the result tree fragment
117   * with callout bullets.</p>
118   *
119   * @param areaspecNodeList The source document &lt;areaspec&gt; element.
120   */
121  public void setupCallouts (NodeList areaspecNodeList) {
122    callout = new Callout[10];
123    calloutCount = 0;
124    calloutPos = 0;
125    lineNumber = 1;
126    colNumber = 1;
127
128    // First we walk through the areaspec to calculate the position
129    // of the callouts
130    //  <areaspec>
131    //  <areaset id="ex.plco.const" coords="">
132    //    <area id="ex.plco.c1" coords="4"/>
133    //    <area id="ex.plco.c2" coords="8"/>
134    //  </areaset>
135    //  <area id="ex.plco.ret" coords="12"/>
136    //  <area id="ex.plco.dest" coords="12"/>
137    //  </areaspec>
138    int pos = 0;
139    int coNum = 0;
140    boolean inAreaSet = false;
141    Node areaspec = areaspecNodeList.item(0);
142    NodeList children = areaspec.getChildNodes();
143
144    for (int count = 0; count < children.getLength(); count++) {
145      Node node = children.item(count);
146      if (node.getNodeType() == Node.ELEMENT_NODE) {
147	if (node.getNodeName().equalsIgnoreCase("areaset")) {
148	  coNum++;
149	  NodeList areas = node.getChildNodes();
150	  for (int acount = 0; acount < areas.getLength(); acount++) {
151	    Node area = areas.item(acount);
152	    if (area.getNodeType() == Node.ELEMENT_NODE) {
153	      if (area.getNodeName().equalsIgnoreCase("area")) {
154		addCallout(coNum, area, defaultColumn);
155	      } else {
156		System.out.println("Unexpected element in areaset: "
157				   + area.getNodeName());
158	      }
159	    }
160	  }
161	} else if (node.getNodeName().equalsIgnoreCase("area")) {
162	  coNum++;
163	  addCallout(coNum, node, defaultColumn);
164	} else {
165	  System.out.println("Unexpected element in areaspec: "
166			     + node.getNodeName());
167	}
168      }
169    }
170
171    // Now sort them
172    java.util.Arrays.sort(callout, 0, calloutCount);
173  }
174
175  /** Process characters. */
176  public void characters(char[] chars, int start, int len)
177    throws TransformerException {
178
179    // If we hit characters, then there's no first element...
180    firstElement = false;
181
182    if (lineNumber == 0) {
183      // if there are any text nodes, there's at least one line
184      lineNumber++;
185      colNumber = 1;
186    }
187
188    // Walk through the text node looking for callout positions
189    char[] newChars = new char[len];
190    int pos = 0;
191    for (int count = start; count < start+len; count++) {
192      if (calloutPos < calloutCount
193	  && callout[calloutPos].getLine() == lineNumber
194	  && callout[calloutPos].getColumn() == colNumber) {
195	if (pos > 0) {
196	  rtfEmitter.characters(newChars, 0, pos);
197	  pos = 0;
198	}
199
200	closeOpenElements(rtfEmitter);
201
202	while (calloutPos < calloutCount
203	       && callout[calloutPos].getLine() == lineNumber
204	       && callout[calloutPos].getColumn() == colNumber) {
205	  fCallout.formatCallout(rtfEmitter, callout[calloutPos]);
206	  calloutPos++;
207	}
208
209	openClosedElements(rtfEmitter);
210      }
211
212      if (chars[count] == '\n') {
213	// What if we need to pad this line?
214	if (calloutPos < calloutCount
215	    && callout[calloutPos].getLine() == lineNumber
216	    && callout[calloutPos].getColumn() > colNumber) {
217
218	  if (pos > 0) {
219	    rtfEmitter.characters(newChars, 0, pos);
220	    pos = 0;
221	  }
222
223	  closeOpenElements(rtfEmitter);
224
225	  while (calloutPos < calloutCount
226		 && callout[calloutPos].getLine() == lineNumber
227		 && callout[calloutPos].getColumn() > colNumber) {
228	    formatPad(callout[calloutPos].getColumn() - colNumber);
229	    colNumber = callout[calloutPos].getColumn();
230	    while (calloutPos < calloutCount
231		   && callout[calloutPos].getLine() == lineNumber
232		   && callout[calloutPos].getColumn() == colNumber) {
233	      fCallout.formatCallout(rtfEmitter, callout[calloutPos]);
234	      calloutPos++;
235	    }
236	  }
237
238	  openClosedElements(rtfEmitter);
239	}
240
241	lineNumber++;
242	colNumber = 1;
243      } else {
244	colNumber++;
245      }
246      newChars[pos++] = chars[count];
247    }
248
249    if (pos > 0) {
250      rtfEmitter.characters(newChars, 0, pos);
251    }
252  }
253
254  /**
255   * <p>Add blanks to the result tree fragment.</p>
256   *
257   * <p>This method adds <tt>numBlanks</tt> to the result tree fragment.
258   * It's used to pad lines when callouts occur after the last existing
259   * characater in a line.</p>
260   *
261   * @param numBlanks The number of blanks to add.
262   */
263  protected void formatPad(int numBlanks) {
264    char chars[] = new char[numBlanks];
265    for (int count = 0; count < numBlanks; count++) {
266      chars[count] = ' ';
267    }
268
269    try {
270      rtfEmitter.characters(chars, 0, numBlanks);
271    } catch (TransformerException e) {
272      System.out.println("Transformer Exception in formatPad");
273    }
274  }
275
276  /**
277   * <p>Add a callout to the global callout array</p>
278   *
279   * <p>This method examines a callout <tt>area</tt> and adds it to
280   * the global callout array if it can be interpreted.</p>
281   *
282   * <p>Only the <tt>linecolumn</tt> and <tt>linerange</tt> units are
283   * supported. If no unit is specifed, <tt>linecolumn</tt> is assumed.
284   * If only a line is specified, the callout decoration appears in
285   * the <tt>defaultColumn</tt>.</p>
286   *
287   * @param coNum The callout number.
288   * @param node The <tt>area</tt>.
289   * @param defaultColumn The default column for callouts.
290   */
291  protected void addCallout (int coNum,
292			     Node node,
293			     int defaultColumn) {
294
295    Element area  = (Element) node;
296    String units  = null;
297    String coords = null;
298
299    if (area.hasAttribute("units")) {
300      units = area.getAttribute("units");
301    }
302
303    if (area.hasAttribute("coords")) {
304      coords = area.getAttribute("coords");
305    }
306
307    if (units != null
308	&& !units.equalsIgnoreCase("linecolumn")
309	&& !units.equalsIgnoreCase("linerange")) {
310      System.out.println("Only linecolumn and linerange units are supported");
311      return;
312    }
313
314    if (coords == null) {
315      System.out.println("Coords must be specified");
316      return;
317    }
318
319    // Now let's see if we can interpret the coordinates...
320    StringTokenizer st = new StringTokenizer(coords);
321    int tokenCount = 0;
322    int c1 = 0;
323    int c2 = 0;
324    while (st.hasMoreTokens()) {
325      tokenCount++;
326      if (tokenCount > 2) {
327	System.out.println("Unparseable coordinates");
328	return;
329      }
330      try {
331	String token = st.nextToken();
332	int coord = Integer.parseInt(token);
333	c2 = coord;
334	if (tokenCount == 1) {
335	  c1 = coord;
336	}
337      } catch (NumberFormatException e) {
338	System.out.println("Unparseable coordinate");
339	return;
340      }
341    }
342
343    // Make sure we aren't going to blow past the end of our array
344    if (calloutCount == callout.length) {
345      Callout bigger[] = new Callout[calloutCount+10];
346      for (int count = 0; count < callout.length; count++) {
347	bigger[count] = callout[count];
348      }
349      callout = bigger;
350    }
351
352    // Ok, add the callout
353    if (tokenCount == 2) {
354      if (units != null && units.equalsIgnoreCase("linerange")) {
355	for (int count = c1; count <= c2; count++) {
356	  callout[calloutCount++] = new Callout(coNum, area,
357						count, defaultColumn);
358	}
359      } else {
360	// assume linecolumn
361	callout[calloutCount++] = new Callout(coNum, area, c1, c2);
362      }
363    } else {
364      // if there's only one number, assume it's the line
365      callout[calloutCount++] = new Callout(coNum, area, c1, defaultColumn);
366    }
367  }
368
369  /** Process end element events. */
370  public void endElement(int nameCode)
371    throws TransformerException {
372
373    if (!elementStack.empty()) {
374      // if we didn't push the very first element (an fo:block or
375      // pre or div surrounding the whole block), then the stack will
376      // be empty when we get to the end of the first element...
377      elementStack.pop();
378    }
379    rtfEmitter.endElement(nameCode);
380  }
381
382  /** Process start element events. */
383  public void startElement(int nameCode,
384			   org.xml.sax.Attributes attributes,
385			   int[] namespaces,
386			   int nscount)
387    throws TransformerException {
388
389    if (!skipThisElement(nameCode)) {
390      StartElementInfo sei = new StartElementInfo(nameCode, attributes,
391						  namespaces, nscount);
392      elementStack.push(sei);
393    }
394
395    firstElement = false;
396
397    rtfEmitter.startElement(nameCode, attributes, namespaces, nscount);
398  }
399
400  /**
401   * <p>Protect the outer-most block wrapper.</p>
402   *
403   * <p>Open elements in the result tree fragment are closed and reopened
404   * around callouts (so that callouts don't appear inside links or other
405   * environments). But if the result tree fragment is a single block
406   * (a div or pre in HTML, an fo:block in FO), that outer-most block is
407   * treated specially.</p>
408   *
409   * <p>This method returns true if the element in question is that
410   * outermost block.</p>
411   *
412   * @param nameCode The name code for the element
413   *
414   * @return True if the element is the outer-most block, false otherwise.
415   */
416  protected boolean skipThisElement(int nameCode) {
417    // FIXME: This is such a gross hack...
418    if (firstElement) {
419      int thisFingerprint    = namePool.getFingerprint(nameCode);
420      int foBlockFingerprint = namePool.getFingerprint(foURI, "block");
421      int htmlPreFingerprint = namePool.getFingerprint("", "pre");
422      int htmlDivFingerprint = namePool.getFingerprint("", "div");
423      int xhtmlPreFingerprint = namePool.getFingerprint(xhURI, "pre");
424      int xhtmlDivFingerprint = namePool.getFingerprint(xhURI, "div");
425
426      if ((foStylesheet && thisFingerprint == foBlockFingerprint)
427	  || (!foStylesheet && (thisFingerprint == htmlPreFingerprint
428				|| thisFingerprint == htmlDivFingerprint
429				|| thisFingerprint == xhtmlPreFingerprint
430				|| thisFingerprint == xhtmlDivFingerprint))) {
431	// Don't push the outer-most wrapping div, pre, or fo:block
432	return true;
433      }
434    }
435
436    return false;
437  }
438
439  private void closeOpenElements(Emitter rtfEmitter)
440    throws TransformerException {
441    // Close all the open elements...
442    tempStack = new Stack();
443    while (!elementStack.empty()) {
444      StartElementInfo elem = (StartElementInfo) elementStack.pop();
445      rtfEmitter.endElement(elem.getNameCode());
446      tempStack.push(elem);
447    }
448  }
449
450  private void openClosedElements(Emitter rtfEmitter)
451    throws TransformerException {
452    // Now "reopen" the elements that we closed...
453    while (!tempStack.empty()) {
454      StartElementInfo elem = (StartElementInfo) tempStack.pop();
455      AttributeCollection attr = (AttributeCollection) elem.getAttributes();
456      AttributeCollection newAttr = new AttributeCollection(namePool);
457
458      for (int acount = 0; acount < attr.getLength(); acount++) {
459	String localName = attr.getLocalName(acount);
460	int nameCode = attr.getNameCode(acount);
461	String type = attr.getType(acount);
462	String value = attr.getValue(acount);
463	String uri = attr.getURI(acount);
464	String prefix = "";
465
466	if (localName.indexOf(':') > 0) {
467	  prefix = localName.substring(0, localName.indexOf(':'));
468	  localName = localName.substring(localName.indexOf(':')+1);
469	}
470
471	if (uri.equals("")
472	    && ((foStylesheet
473		 && localName.equals("id"))
474		|| (!foStylesheet
475		    && (localName.equals("id")
476			|| localName.equals("name"))))) {
477	  // skip this attribute
478	} else {
479	  newAttr.addAttribute(prefix, uri, localName, type, value);
480	}
481      }
482
483      rtfEmitter.startElement(elem.getNameCode(),
484			      newAttr,
485			      elem.getNamespaces(),
486			      elem.getNSCount());
487
488      elementStack.push(elem);
489    }
490  }
491
492  /**
493   * <p>A private class for maintaining the information required to call
494   * the startElement method.</p>
495   *
496   * <p>In order to close and reopen elements, information about those
497   * elements has to be maintained. This class is just the little record
498   * that we push on the stack to keep track of that info.</p>
499   */
500  private class StartElementInfo {
501    private int _nameCode;
502    org.xml.sax.Attributes _attributes;
503    int[] _namespaces;
504    int _nscount;
505
506    public StartElementInfo(int nameCode,
507			    org.xml.sax.Attributes attributes,
508			    int[] namespaces,
509			    int nscount) {
510      _nameCode = nameCode;
511      _attributes = attributes;
512      _namespaces = namespaces;
513      _nscount = nscount;
514    }
515
516    public int getNameCode() {
517      return _nameCode;
518    }
519
520    public org.xml.sax.Attributes getAttributes() {
521      return _attributes;
522    }
523
524    public int[] getNamespaces() {
525      return _namespaces;
526    }
527
528    public int getNSCount() {
529      return _nscount;
530    }
531  }
532}
533