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