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 2001/07/16 21:23:57 nwalsh 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 2001/07/16 21:23:57 nwalsh 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 default column for callouts that specify only a line. */
061: protected int defaultColumn = 60;
062:
063: /** Is the stylesheet currently running an FO stylesheet? */
064: protected boolean foStylesheet = false;
065:
066: /** The current line number. */
067: private static int lineNumber = 0;
068:
069: /** The current column number. */
070: private static int colNumber = 0;
071:
072: /** The (sorted) array of callouts obtained from the areaspec. */
073: private static Callout callout[] = null;
074:
075: /** The number of callouts in the callout array. */
076: private static int calloutCount = 0;
077:
078: /** A pointer used to keep track of our position in the callout array. */
079: private static int calloutPos = 0;
080:
081: /** The FormatCallout object to use for formatting callouts. */
082: private static FormatCallout fCallout = null;
083:
084: /** <p>Constructor for the CalloutEmitter.</p>
085: *
086: * @param namePool The name pool to use for constructing elements and attributes.
087: * @param graphicsPath The path to callout number graphics.
088: * @param graphicsExt The extension for callout number graphics.
089: * @param graphicsMax The largest callout number that can be represented as a graphic.
090: * @param defaultColumn The default column for callouts.
091: * @param foStylesheet Is this an FO stylesheet?
092: */
093: public CalloutEmitter(Controller controller, NamePool namePool,
094: int defaultColumn, boolean foStylesheet,
095: FormatCallout fCallout) {
096: super (controller, namePool);
097: elementStack = new Stack();
098: firstElement = true;
099:
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(
151: "area")) {
152: addCallout(coNum, area, defaultColumn);
153: } else {
154: System.out
155: .println("Unexpected element in areaset: "
156: + area.getNodeName());
157: }
158: }
159: }
160: } else if (node.getNodeName().equalsIgnoreCase("area")) {
161: coNum++;
162: addCallout(coNum, node, defaultColumn);
163: } else {
164: System.out
165: .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,
206: callout[calloutPos]);
207: calloutPos++;
208: }
209:
210: openClosedElements(rtfEmitter);
211: }
212:
213: if (chars[count] == '\n') {
214: // What if we need to pad this line?
215: if (calloutPos < calloutCount
216: && callout[calloutPos].getLine() == lineNumber
217: && callout[calloutPos].getColumn() > colNumber) {
218:
219: if (pos > 0) {
220: rtfEmitter.characters(newChars, 0, pos);
221: pos = 0;
222: }
223:
224: closeOpenElements(rtfEmitter);
225:
226: while (calloutPos < calloutCount
227: && callout[calloutPos].getLine() == lineNumber
228: && callout[calloutPos].getColumn() > colNumber) {
229: formatPad(callout[calloutPos].getColumn()
230: - colNumber);
231: colNumber = callout[calloutPos].getColumn();
232: while (calloutPos < calloutCount
233: && callout[calloutPos].getLine() == lineNumber
234: && callout[calloutPos].getColumn() == colNumber) {
235: fCallout.formatCallout(rtfEmitter,
236: callout[calloutPos]);
237: calloutPos++;
238: }
239: }
240:
241: openClosedElements(rtfEmitter);
242: }
243:
244: lineNumber++;
245: colNumber = 1;
246: } else {
247: colNumber++;
248: }
249: newChars[pos++] = chars[count];
250: }
251:
252: if (pos > 0) {
253: rtfEmitter.characters(newChars, 0, pos);
254: }
255: }
256:
257: /**
258: * <p>Add blanks to the result tree fragment.</p>
259: *
260: * <p>This method adds <tt>numBlanks</tt> to the result tree fragment.
261: * It's used to pad lines when callouts occur after the last existing
262: * characater in a line.</p>
263: *
264: * @param numBlanks The number of blanks to add.
265: */
266: protected void formatPad(int numBlanks) {
267: char chars[] = new char[numBlanks];
268: for (int count = 0; count < numBlanks; count++) {
269: chars[count] = ' ';
270: }
271:
272: try {
273: rtfEmitter.characters(chars, 0, numBlanks);
274: } catch (TransformerException e) {
275: System.out.println("Transformer Exception in formatPad");
276: }
277: }
278:
279: /**
280: * <p>Add a callout to the global callout array</p>
281: *
282: * <p>This method examines a callout <tt>area</tt> and adds it to
283: * the global callout array if it can be interpreted.</p>
284: *
285: * <p>Only the <tt>linecolumn</tt> and <tt>linerange</tt> units are
286: * supported. If no unit is specifed, <tt>linecolumn</tt> is assumed.
287: * If only a line is specified, the callout decoration appears in
288: * the <tt>defaultColumn</tt>.</p>
289: *
290: * @param coNum The callout number.
291: * @param node The <tt>area</tt>.
292: * @param defaultColumn The default column for callouts.
293: */
294: protected void addCallout(int coNum, Node node, int defaultColumn) {
295:
296: Element area = (Element) node;
297: String units = null;
298: String coords = null;
299:
300: if (area.hasAttribute("units")) {
301: units = area.getAttribute("units");
302: }
303:
304: if (area.hasAttribute("coords")) {
305: coords = area.getAttribute("coords");
306: }
307:
308: if (units != null && !units.equalsIgnoreCase("linecolumn")
309: && !units.equalsIgnoreCase("linerange")) {
310: System.out
311: .println("Only linecolumn and linerange units are supported");
312: return;
313: }
314:
315: if (coords == null) {
316: System.out.println("Coords must be specified");
317: return;
318: }
319:
320: // Now let's see if we can interpret the coordinates...
321: StringTokenizer st = new StringTokenizer(coords);
322: int tokenCount = 0;
323: int c1 = 0;
324: int c2 = 0;
325: while (st.hasMoreTokens()) {
326: tokenCount++;
327: if (tokenCount > 2) {
328: System.out.println("Unparseable coordinates");
329: return;
330: }
331: try {
332: String token = st.nextToken();
333: int coord = Integer.parseInt(token);
334: c2 = coord;
335: if (tokenCount == 1) {
336: c1 = coord;
337: }
338: } catch (NumberFormatException e) {
339: System.out.println("Unparseable coordinate");
340: return;
341: }
342: }
343:
344: // Make sure we aren't going to blow past the end of our array
345: if (calloutCount == callout.length) {
346: Callout bigger[] = new Callout[calloutCount + 10];
347: for (int count = 0; count < callout.length; count++) {
348: bigger[count] = callout[count];
349: }
350: callout = bigger;
351: }
352:
353: // Ok, add the callout
354: if (tokenCount == 2) {
355: if (units != null && units.equalsIgnoreCase("linerange")) {
356: for (int count = c1; count <= c2; count++) {
357: callout[calloutCount++] = new Callout(coNum, area,
358: count, defaultColumn);
359: }
360: } else {
361: // assume linecolumn
362: callout[calloutCount++] = new Callout(coNum, area, c1,
363: c2);
364: }
365: } else {
366: // if there's only one number, assume it's the line
367: callout[calloutCount++] = new Callout(coNum, area, c1,
368: defaultColumn);
369: }
370: }
371:
372: /** Process end element events. */
373: public void endElement(int nameCode) throws TransformerException {
374:
375: if (!elementStack.empty()) {
376: // if we didn't push the very first element (an fo:block or
377: // pre or div surrounding the whole block), then the stack will
378: // be empty when we get to the end of the first element...
379: elementStack.pop();
380: }
381: rtfEmitter.endElement(nameCode);
382: }
383:
384: /** Process start element events. */
385: public void startElement(int nameCode,
386: org.xml.sax.Attributes attributes, int[] namespaces,
387: int nscount) throws TransformerException {
388:
389: if (!skipThisElement(nameCode)) {
390: StartElementInfo sei = new StartElementInfo(nameCode,
391: attributes, namespaces, nscount);
392: elementStack.push(sei);
393: }
394:
395: firstElement = false;
396:
397: rtfEmitter.startElement(nameCode, attributes, namespaces,
398: nscount);
399: }
400:
401: /**
402: * <p>Protect the outer-most block wrapper.</p>
403: *
404: * <p>Open elements in the result tree fragment are closed and reopened
405: * around callouts (so that callouts don't appear inside links or other
406: * environments). But if the result tree fragment is a single block
407: * (a div or pre in HTML, an fo:block in FO), that outer-most block is
408: * treated specially.</p>
409: *
410: * <p>This method returns true if the element in question is that
411: * outermost block.</p>
412: *
413: * @param nameCode The name code for the element
414: *
415: * @return True if the element is the outer-most block, false otherwise.
416: */
417: protected boolean skipThisElement(int nameCode) {
418: if (firstElement) {
419: int this Fingerprint = namePool.getFingerprint(nameCode);
420: int foBlockFingerprint = namePool.getFingerprint(foURI,
421: "block");
422: int htmlPreFingerprint = namePool.getFingerprint("", "pre");
423: int htmlDivFingerprint = namePool.getFingerprint("", "div");
424:
425: if ((foStylesheet && this Fingerprint == foBlockFingerprint)
426: || (!foStylesheet && (this Fingerprint == htmlPreFingerprint || this Fingerprint == htmlDivFingerprint))) {
427: // Don't push the outer-most wrapping div, pre, or fo:block
428: return true;
429: }
430: }
431:
432: return false;
433: }
434:
435: private void closeOpenElements(Emitter rtfEmitter)
436: throws TransformerException {
437: // Close all the open elements...
438: tempStack = new Stack();
439: while (!elementStack.empty()) {
440: StartElementInfo elem = (StartElementInfo) elementStack
441: .pop();
442: rtfEmitter.endElement(elem.getNameCode());
443: tempStack.push(elem);
444: }
445: }
446:
447: private void openClosedElements(Emitter rtfEmitter)
448: throws TransformerException {
449: // Now "reopen" the elements that we closed...
450: while (!tempStack.empty()) {
451: StartElementInfo elem = (StartElementInfo) tempStack.pop();
452: AttributeCollection attr = (AttributeCollection) elem
453: .getAttributes();
454: AttributeCollection newAttr = new AttributeCollection(
455: namePool);
456:
457: for (int acount = 0; acount < attr.getLength(); acount++) {
458: String localName = attr.getLocalName(acount);
459: int nameCode = attr.getNameCode(acount);
460: String type = attr.getType(acount);
461: String value = attr.getValue(acount);
462: String uri = attr.getURI(acount);
463: String prefix = "";
464:
465: if (localName.indexOf(':') > 0) {
466: prefix = localName.substring(0, localName
467: .indexOf(':'));
468: localName = localName.substring(localName
469: .indexOf(':') + 1);
470: }
471:
472: if (uri.equals("")
473: && ((foStylesheet && localName.equals("id")) || (!foStylesheet && (localName
474: .equals("id") || localName
475: .equals("name"))))) {
476: // skip this attribute
477: } else {
478: newAttr.addAttribute(prefix, uri, localName, type,
479: value);
480: }
481: }
482:
483: rtfEmitter.startElement(elem.getNameCode(), newAttr, elem
484: .getNamespaces(), elem.getNSCount());
485:
486: elementStack.push(elem);
487: }
488: }
489:
490: /**
491: * <p>A private class for maintaining the information required to call
492: * the startElement method.</p>
493: *
494: * <p>In order to close and reopen elements, information about those
495: * elements has to be maintained. This class is just the little record
496: * that we push on the stack to keep track of that info.</p>
497: */
498: private class StartElementInfo {
499: private int _nameCode;
500: org.xml.sax.Attributes _attributes;
501: int[] _namespaces;
502: int _nscount;
503:
504: public StartElementInfo(int nameCode,
505: org.xml.sax.Attributes attributes, int[] namespaces,
506: int nscount) {
507: _nameCode = nameCode;
508: _attributes = attributes;
509: _namespaces = namespaces;
510: _nscount = nscount;
511: }
512:
513: public int getNameCode() {
514: return _nameCode;
515: }
516:
517: public org.xml.sax.Attributes getAttributes() {
518: return _attributes;
519: }
520:
521: public int[] getNamespaces() {
522: return _namespaces;
523: }
524:
525: public int getNSCount() {
526: return _nscount;
527: }
528: }
529: }
|