1// Verbatim.java - Xalan extensions supporting DocBook verbatim environments
2
3package com.nwalsh.xalan;
4
5import java.util.Hashtable;
6
7import org.xml.sax.Attributes;
8import org.xml.sax.SAXException;
9import org.xml.sax.helpers.AttributesImpl;
10
11import org.w3c.dom.Attr;
12import org.w3c.dom.Document;
13import org.w3c.dom.DocumentFragment;
14import org.w3c.dom.Element;
15import org.w3c.dom.NamedNodeMap;
16import org.w3c.dom.Node;
17import org.w3c.dom.traversal.NodeIterator;
18
19import javax.xml.transform.TransformerException;
20
21import org.apache.xpath.objects.XObject;
22import org.apache.xpath.XPathContext;
23import org.apache.xalan.extensions.ExpressionContext;
24import org.apache.xml.utils.DOMBuilder;
25import javax.xml.parsers.DocumentBuilder;
26import javax.xml.parsers.DocumentBuilderFactory;
27import javax.xml.parsers.ParserConfigurationException;
28import org.apache.xml.utils.QName;
29import org.apache.xml.utils.AttList;
30
31/**
32 * <p>Xalan extensions supporting Tables</p>
33 *
34 * <p>$Id: Table.java,v 1.2 2006/05/15 11:14:03 nwalsh Exp $</p>
35 *
36 * <p>Copyright (C) 2000 Norman Walsh.</p>
37 *
38 * <p>This class provides a
39 * <a href="http://xml.apache.org/xalan-j/">Xalan</a>
40 * implementation of some code to adjust CALS Tables to HTML
41 * Tables.</p>
42 *
43 * <p><b>Column Widths</b></p>
44 * <p>The <tt>adjustColumnWidths</tt> method takes a result tree
45 * fragment (assumed to contain the colgroup of an HTML Table)
46 * and returns the result tree fragment with the column widths
47 * adjusted to HTML terms.</p>
48 *
49 * <p><b>Convert Lengths</b></p>
50 * <p>The <tt>convertLength</tt> method takes a length specification
51 * of the form 9999.99xx (where "xx" is a unit) and returns that length
52 * as an integral number of pixels. For convenience, percentage lengths
53 * are returned unchanged.</p>
54 * <p>The recognized units are: inches (in), centimeters (cm),
55 * millimeters (mm), picas (pc, 1pc=12pt), points (pt), and pixels (px).
56 * A number with no units is assumed to be pixels.</p>
57 *
58 * <p><b>Change Log:</b></p>
59 * <dl>
60 * <dt>1.0</dt>
61 * <dd><p>Initial release.</p></dd>
62 * </dl>
63 *
64 * @author Norman Walsh
65 * <a href="mailto:ndw@nwalsh.com">ndw@nwalsh.com</a>
66 *
67 * @version $Id: Table.java,v 1.2 2006/05/15 11:14:03 nwalsh Exp $
68 *
69 */
70public class Table {
71  /** The number of pixels per inch */
72  private static int pixelsPerInch = 96;
73
74  /** The hash used to associate units with a length in pixels. */
75  protected static Hashtable unitHash = null;
76
77  /** The FO namespace name. */
78  protected static String foURI = "http://www.w3.org/1999/XSL/Format";
79
80  /**
81   * <p>Constructor for Verbatim</p>
82   *
83   * <p>All of the methods are static, so the constructor does nothing.</p>
84   */
85  public Table() {
86  }
87
88  /** Initialize the internal hash table with proper values. */
89  protected static void initializeHash() {
90    unitHash = new Hashtable();
91    unitHash.put("in", new Float(pixelsPerInch));
92    unitHash.put("cm", new Float(pixelsPerInch / 2.54));
93    unitHash.put("mm", new Float(pixelsPerInch / 25.4));
94    unitHash.put("pc", new Float((pixelsPerInch / 72) * 12));
95    unitHash.put("pt", new Float(pixelsPerInch / 72));
96    unitHash.put("px", new Float(1));
97  }
98
99  /** Set the pixels-per-inch value. Only positive values are legal. */
100  public static void setPixelsPerInch(int value) {
101    if (value > 0) {
102      pixelsPerInch = value;
103      initializeHash();
104    }
105  }
106
107  /** Return the current pixels-per-inch value. */
108  public int getPixelsPerInch() {
109    return pixelsPerInch;
110  }
111
112  /**
113   * <p>Convert a length specification to a number of pixels.</p>
114   *
115   * <p>The specified length should be of the form [+/-]999.99xx,
116   * where xx is a valid unit.</p>
117   */
118  public static int convertLength(String length) {
119    // The format of length should be 999.999xx
120    int sign = 1;
121    String digits = "";
122    String units = "";
123    char lench[] = length.toCharArray();
124    float flength = 0;
125    boolean done = false;
126    int pos = 0;
127    float factor = 1;
128    int pixels = 0;
129
130    if (unitHash == null) {
131      initializeHash();
132    }
133
134    if (lench[pos] == '+' || lench[pos] == '-') {
135      if (lench[pos] == '-') {
136	sign = -1;
137      }
138      pos++;
139    }
140
141    while (!done) {
142      if (pos >= lench.length) {
143	done = true;
144      } else {
145	if ((lench[pos] > '9' || lench[pos] < '0') && lench[pos] != '.') {
146	  done = true;
147	  units = length.substring(pos);
148	} else {
149	  digits += lench[pos++];
150	}
151      }
152    }
153
154    try {
155      flength = Float.parseFloat(digits);
156    } catch (NumberFormatException e) {
157      System.out.println(digits + " is not a number; 1 used instead.");
158      flength = 1;
159    }
160
161    Float f = null;
162
163    if (!units.equals("")) {
164      f = (Float) unitHash.get(units);
165      if (f == null) {
166	System.out.println(units + " is not a known unit; 1 used instead.");
167	factor = 1;
168      } else {
169	factor = f.floatValue();
170      }
171    } else {
172      factor = 1;
173    }
174
175    f = new Float(flength * factor);
176
177    pixels = f.intValue() * sign;
178
179    return pixels;
180  }
181
182  /**
183   * <p>Adjust column widths in an HTML table.</p>
184   *
185   * <p>The specification of column widths in CALS (a relative width
186   * plus an optional absolute width) are incompatible with HTML column
187   * widths. This method adjusts CALS column width specifiers in an
188   * attempt to produce equivalent HTML specifiers.</p>
189   *
190   * <p>In order for this method to work, the CALS width specifications
191   * should be placed in the "width" attribute of the &lt;col>s within
192   * a &lt;colgroup>. Then the colgroup result tree fragment is passed
193   * to this method.</p>
194   *
195   * <p>This method makes use of two parameters from the XSL stylesheet
196   * that calls it: <code>nominal.table.width</code> and
197   * <code>table.width</code>. The value of <code>nominal.table.width</code>
198   * must be an absolute distance. The value of <code>table.width</code>
199   * can be either absolute or relative.</p>
200   *
201   * <p>Presented with a mixture of relative and
202   * absolute lengths, the table width is used to calculate
203   * appropriate values. If the <code>table.width</code> is relative,
204   * the nominal width is used for this calculation.</p>
205   *
206   * <p>There are three possible combinations of values:</p>
207   *
208   * <ol>
209   * <li>There are no relative widths; in this case the absolute widths
210   * are used in the HTML table.</li>
211   * <li>There are no absolute widths; in this case the relative widths
212   * are used in the HTML table.</li>
213   * <li>There are a mixture of absolute and relative widths:
214   *   <ol>
215   *     <li>If the table width is absolute, all widths become absolute.</li>
216   *     <li>If the table width is relative, make all the widths absolute
217   *         relative to the nominal table width then turn them all
218   *         back into relative widths.</li>
219   *   </ol>
220   * </li>
221   * </ol>
222   *
223   * @param context The stylesheet context; supplied automatically by Xalan
224   * @param xalanNI
225   *
226   * @return The result tree fragment containing the adjusted colgroup.
227   *
228   */
229
230  public DocumentFragment adjustColumnWidths (ExpressionContext context,
231					      NodeIterator xalanNI) {
232
233    int nominalWidth = convertLength(Params.getString(context,
234						      "nominal.table.width"));
235    String tableWidth = Params.getString(context, "table.width");
236    String styleType = Params.getString(context, "stylesheet.result.type");
237    boolean foStylesheet = styleType.equals("fo");
238
239    DocumentFragment xalanRTF = (DocumentFragment) xalanNI.nextNode();
240    Element colgroup = (Element) xalanRTF.getFirstChild();
241
242    // N.B. ...stree.ElementImpl doesn't implement getElementsByTagName()
243
244    Node firstCol = null;
245    // If this is an FO tree, there might be no colgroup...
246    if (colgroup.getLocalName().equals("colgroup")) {
247      firstCol = colgroup.getFirstChild();
248    } else {
249      firstCol = colgroup;
250    }
251
252    // Count the number of columns...
253    Node child = firstCol;
254    int numColumns = 0;
255    while (child != null) {
256      if (child.getNodeType() == Node.ELEMENT_NODE
257	  && (child.getNodeName().equals("col")
258	      || (child.getNamespaceURI().equals(foURI)
259		  && child.getLocalName().equals("table-column")))) {
260	numColumns++;
261      }
262
263      child = child.getNextSibling();
264    }
265
266    String widths[] = new String[numColumns];
267    Element columns[] = new Element[numColumns];
268    int colnum = 0;
269
270    child = firstCol;
271    while (child != null) {
272      if (child.getNodeType() == Node.ELEMENT_NODE
273	  && (child.getNodeName().equals("col")
274	      || (child.getNamespaceURI().equals(foURI)
275		  && child.getLocalName().equals("table-column")))) {
276	Element col = (Element) child;
277
278	columns[colnum] = col;
279
280	if (foStylesheet) {
281	  if ("".equals(col.getAttribute("column-width"))) {
282	    widths[colnum] = "1*";
283	  } else {
284	    widths[colnum] = col.getAttribute("column-width");
285	  }
286	} else {
287	  if ("".equals(col.getAttribute("width"))) {
288	    widths[colnum] = "1*";
289	  } else {
290	    widths[colnum] = col.getAttribute("width");
291	  }
292	}
293
294	colnum++;
295      }
296      child = child.getNextSibling();
297    }
298
299    float relTotal = 0;
300    float relParts[] = new float[numColumns];
301
302    float absTotal = 0;
303    float absParts[] = new float[numColumns];
304
305    for (int count = 0; count < numColumns; count++) {
306      String width = widths[count];
307      int pos = width.indexOf("*");
308      if (pos >= 0) {
309	String relPart = width.substring(0, pos);
310	String absPart = width.substring(pos+1);
311
312	try {
313	  float rel = Float.parseFloat(relPart);
314	  relTotal += rel;
315	  relParts[count] = rel;
316	} catch (NumberFormatException e) {
317	  System.out.println(relPart + " is not a valid relative unit.");
318	}
319
320	int pixels = 0;
321	if (absPart != null && !absPart.equals("")) {
322	  pixels = convertLength(absPart);
323	}
324
325	absTotal += pixels;
326	absParts[count] = pixels;
327      } else {
328	relParts[count] = 0;
329
330	int pixels = 0;
331	if (width != null && !width.equals("")) {
332	  pixels = convertLength(width);
333	}
334
335	absTotal += pixels;
336	absParts[count] = pixels;
337      }
338    }
339
340    // Ok, now we have the relative widths and absolute widths in
341    // two parallel arrays.
342    //
343    // - If there are no relative widths, output the absolute widths
344    // - If there are no absolute widths, output the relative widths
345    // - If there are a mixture of relative and absolute widths,
346    //   - If the table width is absolute, turn these all into absolute
347    //     widths.
348    //   - If the table width is relative, turn these all into absolute
349    //     widths in the nominalWidth and then turn them back into
350    //     percentages.
351
352    if (relTotal == 0) {
353      for (int count = 0; count < numColumns; count++) {
354	Float f = new Float(absParts[count]);
355	if (foStylesheet) {
356	  int pixels = f.intValue();
357	  float inches = (float) pixels / pixelsPerInch;
358	  widths[count] = inches + "in";
359	} else {
360	  widths[count] = Integer.toString(f.intValue());
361	}
362      }
363    } else if (absTotal == 0) {
364      for (int count = 0; count < numColumns; count++) {
365	float rel = relParts[count] / relTotal * 100;
366	Float f = new Float(rel);
367	widths[count] = Integer.toString(f.intValue());
368      }
369      widths = correctRoundingError(widths);
370    } else {
371      int pixelWidth = nominalWidth;
372
373      if (tableWidth.indexOf("%") <= 0) {
374	pixelWidth = convertLength(tableWidth);
375      }
376
377      if (pixelWidth <= absTotal) {
378	System.out.println("Table is wider than table width.");
379      } else {
380	pixelWidth -= absTotal;
381      }
382
383      absTotal = 0;
384      for (int count = 0; count < numColumns; count++) {
385	float rel = relParts[count] / relTotal * pixelWidth;
386	relParts[count] = rel + absParts[count];
387	absTotal += rel + absParts[count];
388      }
389
390      if (tableWidth.indexOf("%") <= 0) {
391	for (int count = 0; count < numColumns; count++) {
392	  Float f = new Float(relParts[count]);
393	  if (foStylesheet) {
394	    int pixels = f.intValue();
395	    float inches = (float) pixels / pixelsPerInch;
396	    widths[count] = inches + "in";
397	  } else {
398	    widths[count] = Integer.toString(f.intValue());
399	  }
400	}
401      } else {
402	for (int count = 0; count < numColumns; count++) {
403	  float rel = relParts[count] / absTotal * 100;
404	  Float f = new Float(rel);
405	  widths[count] = Integer.toString(f.intValue());
406	}
407	widths = correctRoundingError(widths);
408      }
409    }
410
411    // Now rebuild the colgroup with the right widths
412
413    DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
414    DocumentBuilder docBuilder = null;
415
416    try {
417      docBuilder = docFactory.newDocumentBuilder();
418    } catch (ParserConfigurationException e) {
419      System.out.println("PCE!");
420      return xalanRTF;
421    }
422    Document doc = docBuilder.newDocument();
423    DocumentFragment df = doc.createDocumentFragment();
424    DOMBuilder rtf = new DOMBuilder(doc, df);
425
426    try {
427      String ns = colgroup.getNamespaceURI();
428      String localName = colgroup.getLocalName();
429      String name = colgroup.getTagName();
430
431      if (colgroup.getLocalName().equals("colgroup")) {
432	rtf.startElement(ns, localName, name,
433			 copyAttributes(colgroup));
434      }
435
436      for (colnum = 0; colnum < numColumns; colnum++) {
437	Element col = columns[colnum];
438
439	NamedNodeMap domAttr = col.getAttributes();
440
441	AttributesImpl attr = new AttributesImpl();
442	for (int acount = 0; acount < domAttr.getLength(); acount++) {
443	  Node a = domAttr.item(acount);
444	  String a_ns = a.getNamespaceURI();
445	  String a_localName = a.getLocalName();
446
447	  if ((foStylesheet && !a_localName.equals("column-width"))
448	      || !a_localName.equalsIgnoreCase("width")) {
449	    attr.addAttribute(a.getNamespaceURI(),
450			      a.getLocalName(),
451			      a.getNodeName(),
452			      "CDATA",
453			      a.getNodeValue());
454	  }
455	}
456
457	if (foStylesheet) {
458	  attr.addAttribute("", "column-width", "column-width", "CDATA", widths[colnum]);
459	} else {
460	  attr.addAttribute("", "width", "width", "CDATA", widths[colnum]);
461	}
462
463	rtf.startElement(col.getNamespaceURI(),
464			 col.getLocalName(),
465			 col.getTagName(),
466			 attr);
467	rtf.endElement(col.getNamespaceURI(),
468		       col.getLocalName(),
469		       col.getTagName());
470      }
471
472      if (colgroup.getLocalName().equals("colgroup")) {
473	rtf.endElement(ns, localName, name);
474      }
475    } catch (SAXException se) {
476      System.out.println("SE!");
477      return xalanRTF;
478    }
479
480    return df;
481  }
482
483  private Attributes copyAttributes(Element node) {
484    AttributesImpl attrs = new AttributesImpl();
485    NamedNodeMap nnm = node.getAttributes();
486    for (int count = 0; count < nnm.getLength(); count++) {
487      Attr attr = (Attr) nnm.item(count);
488      String name = attr.getName();
489      if (name.startsWith("xmlns:") || name.equals("xmlns")) {
490	// Skip it; (don't ya just love it!!)
491      } else {
492	attrs.addAttribute(attr.getNamespaceURI(), attr.getName(),
493			   attr.getName(), "CDATA", attr.getValue());
494      }
495    }
496    return attrs;
497  }
498
499  /**
500   * Correct rounding errors introduced in calculating the width of each
501   * column. Make sure they sum to 100% in the end.
502   */
503  protected String[] correctRoundingError(String widths[]) {
504    int totalWidth = 0;
505
506    for (int count = 0; count < widths.length; count++) {
507      try {
508	int width = Integer.parseInt(widths[count]);
509	totalWidth += width;
510      } catch (NumberFormatException nfe) {
511	// nop; "can't happen"
512      }
513    }
514
515    float totalError = 100 - totalWidth;
516    float columnError = totalError / widths.length;
517    float error = 0;
518
519    for (int count = 0; count < widths.length; count++) {
520      try {
521	int width = Integer.parseInt(widths[count]);
522	error = error + columnError;
523	if (error >= 1.0) {
524	  int adj = (int) Math.round(Math.floor(error));
525	  error = error - (float) Math.floor(error);
526	  width = width + adj;
527	  widths[count] = Integer.toString(width) + "%";
528	} else {
529	  widths[count] = Integer.toString(width) + "%";
530	}
531      } catch (NumberFormatException nfe) {
532	// nop; "can't happen"
533      }
534    }
535
536    return widths;
537  }
538}
539