001: package com.nwalsh.saxon;
002:
003: import java.util.Stack;
004: import java.util.StringTokenizer;
005: import org.xml.sax.*;
006: import org.w3c.dom.*;
007: import javax.xml.transform.TransformerException;
008: import com.icl.saxon.Controller;
009: import com.icl.saxon.om.NamePool;
010: import com.icl.saxon.output.Emitter;
011: import com.icl.saxon.tree.AttributeCollection;
012:
013: /**
014: * <p>Saxon extension to decorate a result tree fragment with callouts.</p>
015: *
016: * <p>$Id: CalloutEmitter.java,v 1.1 2006/05/31 17:21:21 mbatchelor Exp $</p>
017: *
018: * <p>Copyright (C) 2000 Norman Walsh.</p>
019: *
020: * <p>This class provides the guts of a
021: * <a href="http://users.iclway.co.uk/mhkay/saxon/">Saxon 6.*</a>
022: * implementation of callouts for verbatim environments. (It is used
023: * by the Verbatim class.)</p>
024: *
025: * <p>The general design is this: the stylesheets construct a result tree
026: * fragment for some verbatim environment. The Verbatim class initializes
027: * a CalloutEmitter with information about the callouts that should be applied
028: * to the verbatim environment in question. Then the result tree fragment
029: * is "replayed" through the CalloutEmitter; the CalloutEmitter builds a
030: * new result tree fragment from this event stream, decorated with callouts,
031: * and that is returned.</p>
032: *
033: * <p><b>Change Log:</b></p>
034: * <dl>
035: * <dt>1.0</dt>
036: * <dd><p>Initial release.</p></dd>
037: * </dl>
038: *
039: * @see Verbatim
040: *
041: * @author Norman Walsh
042: * <a href="mailto:ndw@nwalsh.com">ndw@nwalsh.com</a>
043: *
044: * @version $Id: CalloutEmitter.java,v 1.1 2006/05/31 17:21:21 mbatchelor Exp $
045: *
046: */
047: public class CalloutEmitter extends CopyEmitter {
048: /** A stack for the preserving information about open elements. */
049: protected Stack elementStack = null;
050:
051: /** A stack for holding information about temporarily closed elements. */
052: protected Stack tempStack = null;
053:
054: /** Is the next element absolutely the first element in the fragment? */
055: protected boolean firstElement = false;
056:
057: /** The FO namespace name. */
058: protected static String foURI = "http://www.w3.org/1999/XSL/Format";
059:
060: /** The XHTML namespace name. */
061: protected static String xhURI = "http://www.w3.org/1999/xhtml";
062:
063: /** The default column for callouts that specify only a line. */
064: protected int defaultColumn = 60;
065:
066: /** Is the stylesheet currently running an FO stylesheet? */
067: protected boolean foStylesheet = false;
068:
069: /** The current line number. */
070: private static int lineNumber = 0;
071:
072: /** The current column number. */
073: private static int colNumber = 0;
074:
075: /** The (sorted) array of callouts obtained from the areaspec. */
076: private static Callout callout[] = null;
077:
078: /** The number of callouts in the callout array. */
079: private static int calloutCount = 0;
080:
081: /** A pointer used to keep track of our position in the callout array. */
082: private static int calloutPos = 0;
083:
084: /** The FormatCallout object to use for formatting callouts. */
085: private static FormatCallout fCallout = null;
086:
087: /** <p>Constructor for the CalloutEmitter.</p>
088: *
089: * @param namePool The name pool to use for constructing elements and attributes.
090: * @param graphicsPath The path to callout number graphics.
091: * @param graphicsExt The extension for callout number graphics.
092: * @param graphicsMax The largest callout number that can be represented as a graphic.
093: * @param defaultColumn The default column for callouts.
094: * @param foStylesheet Is this an FO stylesheet?
095: */
096: public CalloutEmitter(Controller controller, NamePool namePool,
097: int defaultColumn, boolean foStylesheet,
098: FormatCallout fCallout) {
099: super (controller, namePool);
100: elementStack = new Stack();
101: firstElement = true;
102:
103: this .defaultColumn = defaultColumn;
104: this .foStylesheet = foStylesheet;
105: this .fCallout = fCallout;
106: }
107:
108: /**
109: * <p>Examine the areaspec and determine the number and position of
110: * callouts.</p>
111: *
112: * <p>The <code><a href="http://docbook.org/tdg/html/areaspec.html">areaspecNodeSet</a></code>
113: * is examined and a sorted list of the callouts is constructed.</p>
114: *
115: * <p>This data structure is used to augment the result tree fragment
116: * with callout bullets.</p>
117: *
118: * @param areaspecNodeSet The source document <areaspec> element.
119: *
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(
154: "area")) {
155: addCallout(coNum, area, defaultColumn);
156: } else {
157: System.out
158: .println("Unexpected element in areaset: "
159: + area.getNodeName());
160: }
161: }
162: }
163: } else if (node.getNodeName().equalsIgnoreCase("area")) {
164: coNum++;
165: addCallout(coNum, node, defaultColumn);
166: } else {
167: System.out
168: .println("Unexpected element in areaspec: "
169: + node.getNodeName());
170: }
171: }
172: }
173:
174: // Now sort them
175: java.util.Arrays.sort(callout, 0, calloutCount);
176: }
177:
178: /** Process characters. */
179: public void characters(char[] chars, int start, int len)
180: throws TransformerException {
181:
182: // If we hit characters, then there's no first element...
183: firstElement = false;
184:
185: if (lineNumber == 0) {
186: // if there are any text nodes, there's at least one line
187: lineNumber++;
188: colNumber = 1;
189: }
190:
191: // Walk through the text node looking for callout positions
192: char[] newChars = new char[len];
193: int pos = 0;
194: for (int count = start; count < start + len; count++) {
195: if (calloutPos < calloutCount
196: && callout[calloutPos].getLine() == lineNumber
197: && callout[calloutPos].getColumn() == colNumber) {
198: if (pos > 0) {
199: rtfEmitter.characters(newChars, 0, pos);
200: pos = 0;
201: }
202:
203: closeOpenElements(rtfEmitter);
204:
205: while (calloutPos < calloutCount
206: && callout[calloutPos].getLine() == lineNumber
207: && callout[calloutPos].getColumn() == colNumber) {
208: fCallout.formatCallout(rtfEmitter,
209: callout[calloutPos]);
210: calloutPos++;
211: }
212:
213: openClosedElements(rtfEmitter);
214: }
215:
216: if (chars[count] == '\n') {
217: // What if we need to pad this line?
218: if (calloutPos < calloutCount
219: && callout[calloutPos].getLine() == lineNumber
220: && callout[calloutPos].getColumn() > colNumber) {
221:
222: if (pos > 0) {
223: rtfEmitter.characters(newChars, 0, pos);
224: pos = 0;
225: }
226:
227: closeOpenElements(rtfEmitter);
228:
229: while (calloutPos < calloutCount
230: && callout[calloutPos].getLine() == lineNumber
231: && callout[calloutPos].getColumn() > colNumber) {
232: formatPad(callout[calloutPos].getColumn()
233: - colNumber);
234: colNumber = callout[calloutPos].getColumn();
235: while (calloutPos < calloutCount
236: && callout[calloutPos].getLine() == lineNumber
237: && callout[calloutPos].getColumn() == colNumber) {
238: fCallout.formatCallout(rtfEmitter,
239: callout[calloutPos]);
240: calloutPos++;
241: }
242: }
243:
244: openClosedElements(rtfEmitter);
245: }
246:
247: lineNumber++;
248: colNumber = 1;
249: } else {
250: colNumber++;
251: }
252: newChars[pos++] = chars[count];
253: }
254:
255: if (pos > 0) {
256: rtfEmitter.characters(newChars, 0, pos);
257: }
258: }
259:
260: /**
261: * <p>Add blanks to the result tree fragment.</p>
262: *
263: * <p>This method adds <tt>numBlanks</tt> to the result tree fragment.
264: * It's used to pad lines when callouts occur after the last existing
265: * characater in a line.</p>
266: *
267: * @param numBlanks The number of blanks to add.
268: */
269: protected void formatPad(int numBlanks) {
270: char chars[] = new char[numBlanks];
271: for (int count = 0; count < numBlanks; count++) {
272: chars[count] = ' ';
273: }
274:
275: try {
276: rtfEmitter.characters(chars, 0, numBlanks);
277: } catch (TransformerException e) {
278: System.out.println("Transformer Exception in formatPad");
279: }
280: }
281:
282: /**
283: * <p>Add a callout to the global callout array</p>
284: *
285: * <p>This method examines a callout <tt>area</tt> and adds it to
286: * the global callout array if it can be interpreted.</p>
287: *
288: * <p>Only the <tt>linecolumn</tt> and <tt>linerange</tt> units are
289: * supported. If no unit is specifed, <tt>linecolumn</tt> is assumed.
290: * If only a line is specified, the callout decoration appears in
291: * the <tt>defaultColumn</tt>.</p>
292: *
293: * @param coNum The callout number.
294: * @param node The <tt>area</tt>.
295: * @param defaultColumn The default column for callouts.
296: */
297: protected void addCallout(int coNum, Node node, int defaultColumn) {
298:
299: Element area = (Element) node;
300: String units = null;
301: String coords = null;
302:
303: if (area.hasAttribute("units")) {
304: units = area.getAttribute("units");
305: }
306:
307: if (area.hasAttribute("coords")) {
308: coords = area.getAttribute("coords");
309: }
310:
311: if (units != null && !units.equalsIgnoreCase("linecolumn")
312: && !units.equalsIgnoreCase("linerange")) {
313: System.out
314: .println("Only linecolumn and linerange units are supported");
315: return;
316: }
317:
318: if (coords == null) {
319: System.out.println("Coords must be specified");
320: return;
321: }
322:
323: // Now let's see if we can interpret the coordinates...
324: StringTokenizer st = new StringTokenizer(coords);
325: int tokenCount = 0;
326: int c1 = 0;
327: int c2 = 0;
328: while (st.hasMoreTokens()) {
329: tokenCount++;
330: if (tokenCount > 2) {
331: System.out.println("Unparseable coordinates");
332: return;
333: }
334: try {
335: String token = st.nextToken();
336: int coord = Integer.parseInt(token);
337: c2 = coord;
338: if (tokenCount == 1) {
339: c1 = coord;
340: }
341: } catch (NumberFormatException e) {
342: System.out.println("Unparseable coordinate");
343: return;
344: }
345: }
346:
347: // Make sure we aren't going to blow past the end of our array
348: if (calloutCount == callout.length) {
349: Callout bigger[] = new Callout[calloutCount + 10];
350: for (int count = 0; count < callout.length; count++) {
351: bigger[count] = callout[count];
352: }
353: callout = bigger;
354: }
355:
356: // Ok, add the callout
357: if (tokenCount == 2) {
358: if (units != null && units.equalsIgnoreCase("linerange")) {
359: for (int count = c1; count <= c2; count++) {
360: callout[calloutCount++] = new Callout(coNum, area,
361: count, defaultColumn);
362: }
363: } else {
364: // assume linecolumn
365: callout[calloutCount++] = new Callout(coNum, area, c1,
366: c2);
367: }
368: } else {
369: // if there's only one number, assume it's the line
370: callout[calloutCount++] = new Callout(coNum, area, c1,
371: defaultColumn);
372: }
373: }
374:
375: /** Process end element events. */
376: public void endElement(int nameCode) throws TransformerException {
377:
378: if (!elementStack.empty()) {
379: // if we didn't push the very first element (an fo:block or
380: // pre or div surrounding the whole block), then the stack will
381: // be empty when we get to the end of the first element...
382: elementStack.pop();
383: }
384: rtfEmitter.endElement(nameCode);
385: }
386:
387: /** Process start element events. */
388: public void startElement(int nameCode,
389: org.xml.sax.Attributes attributes, int[] namespaces,
390: int nscount) throws TransformerException {
391:
392: if (!skipThisElement(nameCode)) {
393: StartElementInfo sei = new StartElementInfo(nameCode,
394: attributes, namespaces, nscount);
395: elementStack.push(sei);
396: }
397:
398: firstElement = false;
399:
400: rtfEmitter.startElement(nameCode, attributes, namespaces,
401: nscount);
402: }
403:
404: /**
405: * <p>Protect the outer-most block wrapper.</p>
406: *
407: * <p>Open elements in the result tree fragment are closed and reopened
408: * around callouts (so that callouts don't appear inside links or other
409: * environments). But if the result tree fragment is a single block
410: * (a div or pre in HTML, an fo:block in FO), that outer-most block is
411: * treated specially.</p>
412: *
413: * <p>This method returns true if the element in question is that
414: * outermost block.</p>
415: *
416: * @param nameCode The name code for the element
417: *
418: * @return True if the element is the outer-most block, false otherwise.
419: */
420: protected boolean skipThisElement(int nameCode) {
421: // FIXME: This is such a gross hack...
422: if (firstElement) {
423: int this Fingerprint = namePool.getFingerprint(nameCode);
424: int foBlockFingerprint = namePool.getFingerprint(foURI,
425: "block");
426: int htmlPreFingerprint = namePool.getFingerprint("", "pre");
427: int htmlDivFingerprint = namePool.getFingerprint("", "div");
428: int xhtmlPreFingerprint = namePool.getFingerprint(xhURI,
429: "pre");
430: int xhtmlDivFingerprint = namePool.getFingerprint(xhURI,
431: "div");
432:
433: if ((foStylesheet && this Fingerprint == foBlockFingerprint)
434: || (!foStylesheet && (this Fingerprint == htmlPreFingerprint
435: || this Fingerprint == htmlDivFingerprint
436: || this Fingerprint == xhtmlPreFingerprint || this Fingerprint == xhtmlDivFingerprint))) {
437: // Don't push the outer-most wrapping div, pre, or fo:block
438: return true;
439: }
440: }
441:
442: return false;
443: }
444:
445: private void closeOpenElements(Emitter rtfEmitter)
446: throws TransformerException {
447: // Close all the open elements...
448: tempStack = new Stack();
449: while (!elementStack.empty()) {
450: StartElementInfo elem = (StartElementInfo) elementStack
451: .pop();
452: rtfEmitter.endElement(elem.getNameCode());
453: tempStack.push(elem);
454: }
455: }
456:
457: private void openClosedElements(Emitter rtfEmitter)
458: throws TransformerException {
459: // Now "reopen" the elements that we closed...
460: while (!tempStack.empty()) {
461: StartElementInfo elem = (StartElementInfo) tempStack.pop();
462: AttributeCollection attr = (AttributeCollection) elem
463: .getAttributes();
464: AttributeCollection newAttr = new AttributeCollection(
465: namePool);
466:
467: for (int acount = 0; acount < attr.getLength(); acount++) {
468: String localName = attr.getLocalName(acount);
469: int nameCode = attr.getNameCode(acount);
470: String type = attr.getType(acount);
471: String value = attr.getValue(acount);
472: String uri = attr.getURI(acount);
473: String prefix = "";
474:
475: if (localName.indexOf(':') > 0) {
476: prefix = localName.substring(0, localName
477: .indexOf(':'));
478: localName = localName.substring(localName
479: .indexOf(':') + 1);
480: }
481:
482: if (uri.equals("")
483: && ((foStylesheet && localName.equals("id")) || (!foStylesheet && (localName
484: .equals("id") || localName
485: .equals("name"))))) {
486: // skip this attribute
487: } else {
488: newAttr.addAttribute(prefix, uri, localName, type,
489: value);
490: }
491: }
492:
493: rtfEmitter.startElement(elem.getNameCode(), newAttr, elem
494: .getNamespaces(), elem.getNSCount());
495:
496: elementStack.push(elem);
497: }
498: }
499:
500: /**
501: * <p>A private class for maintaining the information required to call
502: * the startElement method.</p>
503: *
504: * <p>In order to close and reopen elements, information about those
505: * elements has to be maintained. This class is just the little record
506: * that we push on the stack to keep track of that info.</p>
507: */
508: private class StartElementInfo {
509: private int _nameCode;
510: org.xml.sax.Attributes _attributes;
511: int[] _namespaces;
512: int _nscount;
513:
514: public StartElementInfo(int nameCode,
515: org.xml.sax.Attributes attributes, int[] namespaces,
516: int nscount) {
517: _nameCode = nameCode;
518: _attributes = attributes;
519: _namespaces = namespaces;
520: _nscount = nscount;
521: }
522:
523: public int getNameCode() {
524: return _nameCode;
525: }
526:
527: public org.xml.sax.Attributes getAttributes() {
528: return _attributes;
529: }
530:
531: public int[] getNamespaces() {
532: return _namespaces;
533: }
534:
535: public int getNSCount() {
536: return _nscount;
537: }
538: }
539: }
|