Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

Source code: com/nwalsh/saxon/CalloutEmitter.java


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