package com.nwalsh.saxon; import java.util.Stack; import java.util.StringTokenizer; import org.xml.sax.*; import org.w3c.dom.*; import javax.xml.transform.TransformerException; import com.icl.saxon.output.*; import com.icl.saxon.om.*; import com.icl.saxon.Controller; import com.icl.saxon.tree.AttributeCollection; import com.icl.saxon.expr.FragmentValue; /** *
Saxon extension to decorate a result tree fragment with line numbers.
* *$Id: NumberLinesEmitter.java 5907 2006-04-27 08:26:47Z xmldoc $
* *Copyright (C) 2000 Norman Walsh.
* *This class provides the guts of a * Saxon 6.* * implementation of line numbering for verbatim environments. (It is used * by the Verbatim class.)
* *The general design is this: the stylesheets construct a result tree * fragment for some verbatim environment. The Verbatim class initializes * a NumberLinesEmitter with information about what lines should be * numbered and how. Then the result tree fragment * is "replayed" through the NumberLinesEmitter; the NumberLinesEmitter * builds a * new result tree fragment from this event stream, decorated with line * numbers, * and that is returned.
* *Change Log:
*Initial release.
startinglinenumber
. */
protected int startinglinenumber = 1;
/** Every modulus
line will be numbered. */
protected int modulus = 5;
/** Line numbers are width
characters wide. */
protected int width = 3;
/** Line numbers are separated from the listing by separator
. */
protected String separator = " ";
/** Is the stylesheet currently running an FO stylesheet? */
protected boolean foStylesheet = false;
/** Constructor for the NumberLinesEmitter.
* * @param namePool The name pool to use for constructing elements and attributes. * @param modulus The modulus to use for this listing. * @param width The width to use for line numbers in this listing. * @param separator The separator to use for this listing. * @param foStylesheet Is this an FO stylesheet? */ public NumberLinesEmitter(Controller controller, NamePool namePool, int startingLineNumber, int modulus, int width, String separator, boolean foStylesheet) { super(controller,namePool); elementStack = new Stack(); firstElement = true; this.modulus = modulus; this.startinglinenumber = startingLineNumber; this.width = width; this.separator = separator; this.foStylesheet = foStylesheet; } /** Process characters. */ public void characters(char[] chars, int start, int len) throws TransformerException { // If we hit characters, then there's no first element... firstElement = false; if (lineNumber == 0) { // The first line is always numbered lineNumber = startinglinenumber; formatLineNumber(lineNumber); } // Walk through the text node looking for newlines char[] newChars = new char[len]; int pos = 0; for (int count = start; count < start+len; count++) { if (chars[count] == '\n') { // This is the tricky bit; if we find a newline, make sure // it doesn't occur inside any markup. if (pos > 0) { // Output any characters that preceded this newline rtfEmitter.characters(newChars, 0, pos); pos = 0; } // Close all the open elements... Stack tempStack = new Stack(); while (!elementStack.empty()) { StartElementInfo elem = (StartElementInfo) elementStack.pop(); rtfEmitter.endElement(elem.getNameCode()); tempStack.push(elem); } // Copy the newline to the output newChars[pos++] = chars[count]; rtfEmitter.characters(newChars, 0, pos); pos = 0; // Add the line number formatLineNumber(++lineNumber); // Now "reopen" the elements that we closed... while (!tempStack.empty()) { StartElementInfo elem = (StartElementInfo) tempStack.pop(); AttributeCollection attr = (AttributeCollection)elem.getAttributes(); AttributeCollection newAttr = new AttributeCollection(namePool); for (int acount = 0; acount < attr.getLength(); acount++) { String localName = attr.getLocalName(acount); int nameCode = attr.getNameCode(acount); String type = attr.getType(acount); String value = attr.getValue(acount); String uri = attr.getURI(acount); String prefix = ""; if (localName.indexOf(':') > 0) { prefix = localName.substring(0, localName.indexOf(':')); localName = localName.substring(localName.indexOf(':')+1); } if (uri.equals("") && ((foStylesheet && localName.equals("id")) || (!foStylesheet && (localName.equals("id") || localName.equals("name"))))) { // skip this attribute } else { newAttr.addAttribute(prefix, uri, localName, type, value); } } rtfEmitter.startElement(elem.getNameCode(), newAttr, elem.getNamespaces(), elem.getNSCount()); elementStack.push(elem); } } else { newChars[pos++] = chars[count]; } } if (pos > 0) { rtfEmitter.characters(newChars, 0, pos); pos = 0; } } /** *Add a formatted line number to the result tree fragment.
* * @param lineNumber The number of the current line. */ protected void formatLineNumber(int lineNumber) throws TransformerException { char ch = 160; // String lno = ""; if (lineNumber == 1 || (modulus >= 1 && (lineNumber % modulus == 0))) { lno = "" + lineNumber; } while (lno.length() < width) { lno = ch + lno; } lno += separator; char chars[] = new char[lno.length()]; for (int count = 0; count < lno.length(); count++) { chars[count] = lno.charAt(count); } characters(chars, 0, lno.length()); } /** Process end element events. */ public void endElement(int nameCode) throws TransformerException { if (!elementStack.empty()) { // if we didn't push the very first element (an fo:block or // pre or div surrounding the whole block), then the stack will // be empty when we get to the end of the first element... elementStack.pop(); } rtfEmitter.endElement(nameCode); } /** Process start element events. */ public void startElement(int nameCode, org.xml.sax.Attributes attributes, int[] namespaces, int nscount) throws TransformerException { if (!skipThisElement(nameCode)) { StartElementInfo sei = new StartElementInfo(nameCode, attributes, namespaces, nscount); elementStack.push(sei); } firstElement = false; rtfEmitter.startElement(nameCode, attributes, namespaces, nscount); } /** *Protect the outer-most block wrapper.
* *Open elements in the result tree fragment are closed and reopened * around callouts (so that callouts don't appear inside links or other * environments). But if the result tree fragment is a single block * (a div or pre in HTML, an fo:block in FO), that outer-most block is * treated specially.
* *This method returns true if the element in question is that * outermost block.
* * @param nameCode The name code for the element * * @return True if the element is the outer-most block, false otherwise. */ protected boolean skipThisElement(int nameCode) { // FIXME: This is such a gross hack... if (firstElement) { int thisFingerprint = namePool.getFingerprint(nameCode); int foBlockFingerprint = namePool.getFingerprint(foURI, "block"); int htmlPreFingerprint = namePool.getFingerprint("", "pre"); int htmlDivFingerprint = namePool.getFingerprint("", "div"); int xhtmlPreFingerprint = namePool.getFingerprint(xhURI, "pre"); int xhtmlDivFingerprint = namePool.getFingerprint(xhURI, "div"); if ((foStylesheet && thisFingerprint == foBlockFingerprint) || (!foStylesheet && (thisFingerprint == htmlPreFingerprint || thisFingerprint == htmlDivFingerprint || thisFingerprint == xhtmlPreFingerprint || thisFingerprint == xhtmlDivFingerprint))) { // Don't push the outer-most wrapping div, pre, or fo:block return true; } } return false; } /** *A private class for maintaining the information required to call * the startElement method.
* *In order to close and reopen elements, information about those * elements has to be maintained. This class is just the little record * that we push on the stack to keep track of that info.
*/ private class StartElementInfo { private int _nameCode; org.xml.sax.Attributes _attributes; int[] _namespaces; int _nscount; public StartElementInfo(int nameCode, org.xml.sax.Attributes attributes, int[] namespaces, int nscount) { _nameCode = nameCode; _attributes = attributes; _namespaces = namespaces; _nscount = nscount; } public int getNameCode() { return _nameCode; } public org.xml.sax.Attributes getAttributes() { return _attributes; } public int[] getNamespaces() { return _namespaces; } public int getNSCount() { return _nscount; } } }