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 <areaspec> 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 }