/* * xtc - The eXTensible Compiler * Copyright (C) 2004-2007 Robert Grimm * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * version 2 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA. */ package xtc.tree; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; import java.text.BreakIterator; import java.util.Iterator; import java.util.List; import java.util.Locale; import xtc.Constants; import xtc.util.Pair; import xtc.util.Utilities; /** * A node pretty printing utility. This class helps with the pretty * printing of syntax trees, including with the generation of source * code. It provides facilities for indenting, escaping, aligning, * and line-wrapping text. Note that, for the facilities of this * class to work, newlines should never be printed through a character * or string constant (e.g., by using '<code>\n</code>' or * '<code>\r</code>') but always by calling the appropriate method. * * @author Robert Grimm * @version $Revision: 1.62 $ */ public class Printer extends Utility { /** The break iterator, if any. */ protected BreakIterator breaks; /** The current print writer to print to. */ protected PrintWriter out; /** The original print writer. */ protected PrintWriter directOut; /** The string writer, if output is currently being buffered. */ protected StringWriter bufferedOut = null; /** The number of outstanding invocations to {@link #buffer()}. */ protected int buffering = 0; /** The current indentation level. */ protected int indent = 0; /** The current column. */ protected int column = Constants.FIRST_COLUMN; /** The current line. */ protected long line = Constants.FIRST_LINE; // ======================================================================== /** * Create a new printer with the specified output stream. The * printer does <i>not</i> flush the specified output stream on * newlines. * * @param out The output stream. */ public Printer(OutputStream out) { this(new PrintWriter(out, false)); } /** * Create a new printer with the specified writer. The printer does * <i>not</i> flush the specified writer on newlines. * * @param out The writer. */ public Printer(Writer out) { this(new PrintWriter(out, false)); } /** * Create a new printer with the specified print writer. * * @param out The print writer to output to. */ public Printer(PrintWriter out) { this.out = out; directOut = out; } // ======================================================================== /** * Reset this printer. This method stops buffering (if this printer * was buffering) and clears the current indentation level, column * number, and line number. * * @return This printer. */ public Printer reset() { stopBuffering(); indent = 0; column = Constants.FIRST_COLUMN; line = Constants.FIRST_LINE; return this; } // ======================================================================== /** * Get the current column number. * * @return The current column number. */ public int column() { return column; } /** * Set the current column to the specified number. * * @param column The new column number. * @return This printer. */ public Printer column(int column) { this.column = column; return this; } /** * Get the current line number. * * @return The current line number. */ public long line() { return line; } /** * Set the current line to the specified number. * * @param line The new line number. * @return This printer. */ public Printer line(long line) { this.line = line; return this; } // ======================================================================== /** * Start buffering the output. This method starts redirecting all * output into a buffer, so that later invocations to {@link * #fit()}, {@link #fit(String)}, or {@link #fitMore()} can ensure * that the output fits onto the current line. * * <p />Note that invocations to this method are matched with * invocations to the <code>fit()</code> and <code>fitMore()</code> * methods. In other words, the <code>fit()</code> and * <code>fitMore()</code> methods only have an effect, if they * correspond to the first invocation of this method after (1) this * printer has been created, (2) the last invocation of {@link * #reset()}, (3) the last invocation of {@link #unbuffer()}, or (4) * the last invocation of any methods printing a newline. * * @return This printer. */ public Printer buffer() { if (0 == buffering) { // Create a new string writer and make it the current print // writer. bufferedOut = new StringWriter(); out = new PrintWriter(bufferedOut, false); } buffering++; return this; } /** * Reset any buffering. If this printer is currently buffering the * output, this method stops buffering and returns the buffer * contents. Otherwise, it returns the empty string. * * @return The buffer contents. */ protected String stopBuffering() { if (null != bufferedOut) { // Flush the current print writer. out.flush(); // Get the buffer contents. final String s = bufferedOut.toString(); // Restore the writers and buffer count. out = directOut; bufferedOut = null; buffering = 0; return s; } else { return ""; } } /** * Ensure that the buffer contents fit onto the current line. This * method writes the buffer contents out. If the contents do not * fit onto the current line, it first writes a newline and then * indents the output. * * @see #buffer() * * @return This printer. */ public Printer fit() { if (1 == buffering) { final String s = stopBuffering(); if (Constants.LINE_LENGTH + Constants.FIRST_COLUMN < column) { // We write through this printer's methods to count the buffer // contents again. out.println(); column = Constants.FIRST_COLUMN; line++; indent().p(s); } else { // We write directly, as the buffer contents have already been // counted. out.print(s); } } else if (1 < buffering) { buffering--; } return this; } /** * Ensure that the buffer contents fit onto the current line. This * method writes the buffer contents out. If the contents do not * fit onto the current line, it first writes a newline and then * aligns the output with specified alignment. * * @see #buffer() * * @param align The alignment. * @return This printer. */ public Printer fit(int align) { if (1 == buffering) { final String s = stopBuffering(); if (Constants.LINE_LENGTH + Constants.FIRST_COLUMN < column) { // We write through this printer's methods to count the buffer // contents again. out.println(); column = Constants.FIRST_COLUMN; line++; align(align).p(s); } else { // We write directly, as the buffer contents have already been // counted. out.print(s); } } else if (1 < buffering) { buffering--; } return this; } /** * Ensure that the buffer contents fit onto the current line. This * method writes the buffer contents out. If the contents do not * fit onto the current line, it first writes a newline, then * indents the output, and then writes the specified prefix. * * @see #buffer() * * @param prefix The prefix. * @return This printer. */ public Printer fit(String prefix) { if (1 == buffering) { final String s = stopBuffering(); if (Constants.LINE_LENGTH + Constants.FIRST_COLUMN < column) { // We write through this printer's methods to count the buffer // contents again. out.println(); column = Constants.FIRST_COLUMN; line++; indent().p(prefix).p(s); } else { // We write directly, as the buffer contents have already been // counted. out.print(s); } } else if (1 < buffering) { buffering--; } return this; } /** * Ensure that the buffer contents fit onto the current line. This * method writes the buffer contents out. If the contents do not * fit onto the current line, it first writes a newline and then * indents the output one tab stop more than the current indentation * level. * * @see #buffer() * * @return This printer. */ public Printer fitMore() { if (1 == buffering) { final String s = stopBuffering(); if (Constants.LINE_LENGTH + Constants.FIRST_COLUMN < column) { // We write through this printer's methods to count the buffer // contents again. out.println(); column = Constants.FIRST_COLUMN; line++; indentMore().p(s); } else { // We write directly, as the buffer contents have already been // counted. out.print(s); } } else if (1 < buffering) { buffering--; } return this; } /** * Stop buffering the output. If the output is currently being * buffered, this method writes the buffer contents out and stops * buffering. Otherwise, it has no effect. * * @return This printer. */ public Printer unbuffer() { if (0 < buffering) { final String s = stopBuffering(); out.write(s); } return this; } // ======================================================================== /** * Print whitespace to align the output. This method prints * whitespace to cover the difference between the current column * number and the specified, absolute alignment. If the column * number is greater or equal the specified alignment, a single * space character is printed. * * @param alignment The number of characters to align at. * @return This printer. */ public Printer align(int alignment) { int toPrint = alignment - column; if (0 >= toPrint) toPrint = 1; for (int i=0; i<toPrint; i++) { out.write(' '); } column += toPrint; return this; } // ======================================================================== /** * Get the current indentation level. * * @return The current indentation level. */ public int level() { return indent / Constants.INDENTATION; } /** * Set the current indentation level. * * @param level The new indentation level. * @return This printer. * @throws IllegalArgumentException * Signals that the specified level is negative. */ public Printer setLevel(int level) { if (0 > level) { throw new IllegalArgumentException("Negative indentation level"); } indent = level * Constants.INDENTATION; return this; } /** * Increase the current indentation level. * * @return This printer. */ public Printer incr() { indent += Constants.INDENTATION; return this; } /** * Decrease the current indentation level. * * @return This printer. */ public Printer decr() { indent -= Constants.INDENTATION; return this; } /** * Indent. * * @return This printer. */ public Printer indent() { for (int i=0; i<indent; i++) { out.print(' '); } column += indent; return this; } /** * Indent one tab stop less than the current indentation level. * * @return This printer. */ public Printer indentLess() { int w = indent - Constants.INDENTATION; if (0 > w) { w = 0; } for (int i=0; i<w; i++) { out.print(' '); } column += w; return this; } /** * Indent one tab stop more than the current indentation level. * * @return This printer. */ public Printer indentMore() { final int w = indent + Constants.INDENTATION; for (int i=0; i<w; i++) { out.print(' '); } column += w; return this; } // ======================================================================== /** * Print the specified character. * * @param c The character to print. * @return This printer. */ public Printer p(char c) { out.print(c); column += 1; return this; } /** * Print the specified integer. * * @param i The integer to print. * @return This printer. */ public Printer p(int i) { return p(Integer.toString(i)); } /** * Print the specified long. * * @param l The long to print. * @return This printer. */ public Printer p(long l) { return p(Long.toString(l)); } /** * Print the specified double. * * @param d The double to print. * @return This printer. */ public Printer p(double d) { return p(Double.toString(d)); } /** * Print the specified string. * * @param s The string to print. * @return This printer. */ public Printer p(String s) { out.print(s); column += s.length(); return this; } /** * Print the specified character followed by a newline. * * @param c The character to print. * @return This printer. */ public Printer pln(char c) { unbuffer(); out.println(c); column = Constants.FIRST_COLUMN; line++; return this; } /** * Print the specified integer followed by a newline. * * @param i The integer to print. * @return This printer. */ public Printer pln(int i) { return pln(Integer.toString(i)); } /** * Print the specified long followed by a newline. * * @param l The long to print. * @return This printer. */ public Printer pln(long l) { return pln(Long.toString(l)); } /** * Print the specified double followed by a newline. * * @param d The double to print. * @return This printer. */ public Printer pln(double d) { return pln(Double.toString(d)); } /** * Print the specified string followed by a newline. * * @param s The string to print. * @return This printer. */ public Printer pln(String s) { unbuffer(); out.println(s); column = Constants.FIRST_COLUMN; line++; return this; } /** * Print a newline. * * @return This printer. */ public Printer pln() { unbuffer(); out.println(); column = Constants.FIRST_COLUMN; line++; return this; } // ======================================================================== /** * Print the specified character using C escapes. * * @param c The character to print. * @return This printer. */ public Printer escape(char c) { return p(Utilities.escape(c, Utilities.C_ESCAPES)); } /** * Print the specified character with the specified escape sequences. * * @see Utilities * * @param c The character to print. * @param flags The escape flags. * @return This printer. */ public Printer escape(char c, int flags) { return p(Utilities.escape(c, flags)); } /** * Print the specified string using C escapes. * * @param s The string to print. * @return This printer. */ public Printer escape(String s) { return p(Utilities.escape(s, Utilities.C_ESCAPES)); } /** * Print the specified string with the specified escape sequences. * * @see Utilities * * @param s The string to print. * @param flags The escape flags. * @return This printer. */ public Printer escape(String s, int flags) { return p(Utilities.escape(s, flags)); } // ======================================================================== /** * Print the specified long while also padding it to the specified * width with leading spaces. * * @param l The long to print. * @param width The width. * @return This printer. */ public Printer pad(long l, int width) { final String text = Long.toString(l); final int padding = width - text.length(); for (int i=0; i<padding; i++) p(' '); p(text); return this; } // ======================================================================== /** * Print an indented separation comment. * * @return This printer. */ public Printer sep() { unbuffer(); indent().p("// "); final int n = Constants.LINE_LENGTH - indent - 3; for (int i=0; i<n; i++) { out.print('='); } out.println(); column = Constants.FIRST_COLUMN; line++; return this; } // ======================================================================== /** * Print the specified text. This method line-wraps the specified * text with the specified per-line alignment. It does, however, * not print the initial alignment or the final end-of-line. * * @param alignment The per-line alignment. * @param text The text. */ public Printer wrap(int alignment, String text) { if (null == breaks) { breaks = BreakIterator.getLineInstance(Locale.ENGLISH); } breaks.setText(text); int start = breaks.first(); int end = breaks.next(); boolean first = true; while (BreakIterator.DONE != end) { String word = text.substring(start, end); if (! first && (Constants.LINE_LENGTH + Constants.FIRST_COLUMN < column + word.length())) { pln(); if (Constants.FIRST_COLUMN != alignment) { align(alignment); } } p(word); start = end; end = breaks.next(); first = false; } return this; } // ======================================================================== /** * Print the specified node. If the specified node is * <code>null</code>, nothing is printed. * * @param node The node to print. * @return This printer. */ public Printer p(Node node) { visitor.dispatch(node); return this; } /** * Print the specified attribute. * * @param attribute The attribute. * @return This printer. */ public Printer p(Attribute attribute) { p(attribute.name); if (null != attribute.value) { p('('); if ((attribute.value instanceof List) || (attribute.value instanceof Pair)) { boolean first = true; for (Object o : (Iterable<?>)attribute.value) { if (first) { first = false; } else { p(", "); } p(o.toString()); } } else { p(attribute.value.toString()); } p(')'); } return this; } /** * Print the specified comment. Note that this method does * <em>not</em> indent the first line, but it does indent all * following lines for comments spanning multiple lines. Further * note that this method does <em>not</em> dispatch this printer's * visitor on the node contained in the comment. * * @param comment The comment to print. * @return This printer. */ public Printer p(Comment comment) { if (0 == comment.text.size()) return this; if (Comment.Kind.SINGLE_LINE == comment.kind) { p("// ").pln(comment.text.get(0)); } else { if (Comment.Kind.MULTIPLE_LINES == comment.kind) { p("/*"); } else { p("/**"); } if (1 == comment.text.size()) { p(' ').p(comment.text.get(0)).pln(" */"); } else { pln(); for (String line : comment.text) { indent().p(" * ").pln(line); } indent().pln(" */"); } } return this; } // ======================================================================== /** * Format the specified node. Instead of using this printer's * visitor to print the specified node, this method emits a general * representation of the node, using the node's generic traversal * methods if available. * * @param n The node. * @return This printer. */ public Printer format(Node n) { return format1(n, false); } /** * Format the specified node. Instead of using this printer's * visitor to print the specified node, this method emits a general * representation of the node, using the node's generic traversal * methods if available. * * @param n The node. * @param locate The flag for printing a node's location. * @return This printer. */ public Printer format(Node n, boolean locate) { formatFile = null; return format1(n, locate); } /** The last file name encountered when printing locations. */ private String formatFile; /** * Format the specified object. * * @param o The object. * @param locate The flag for printing a node's location. * @return This printer. */ private Printer format1(Object o, boolean locate) { indent(); if (null == o) { p("null"); } else if (o instanceof Node) { final Node n = (Node)o; p(n.getName()); if (locate && n.hasLocation()) { final Location loc = n.getLocation(); p('@'); if (! loc.file.equals(formatFile)) { p(loc.file).p(':'); formatFile = loc.file; } p(loc.line).p(':').p(loc.column); } p('('); if (n.isEmpty()) { p(')'); } else { pln().incr().formatElements(n, locate).decr().indent().p(')'); } } else if (o instanceof Pair) { final Pair<?> p = (Pair<?>)o; if (p.isEmpty()) { p("[]"); } else { pln('[').incr().formatElements(p, locate).decr().indent().p(']'); } } else if (o instanceof String) { p('"').escape(o.toString(), Utilities.C_ESCAPES).p('"'); } else { p(o.toString()); } return this; } /** * Format the specified composite's elements. * * @param composite The composite. * @param locate The flag for printing a node's location. * @return This printer. */ private Printer formatElements(Iterable<?> composite, boolean locate) { for (Iterator<?> iter = composite.iterator(); iter.hasNext(); ) { format1(iter.next(), locate); if (iter.hasNext()) p(','); pln(); } return this; } // ======================================================================== /** * Print the location for the specified locatable object. First, if * the locatable is a node and has a {@link Constants#ORIGINAL} * property, that property's locatable value replaces the specified * locatable object. Second, if the actual locatable has a * location, this method prints the file name, a colon, the line * number, another colon, and the column number. If the actual * locatable does not have a location, nothing is printed. * * @param locatable The locatable object. * @return This printer. */ public Printer loc(Locatable locatable) { if (locatable instanceof Node) { final Node node = (Node)locatable; if (node.hasProperty(Constants.ORIGINAL)) { locatable = (Locatable)node.getProperty(Constants.ORIGINAL); } } if (locatable.hasLocation()) { final Location loc = locatable.getLocation(); p(loc.file).p(':').p(loc.line).p(':').p(loc.column); } return this; } /** * Line this printer up at the specified locatable object's * location. * * @param locatable The locatable object. * @return This printer. */ public Printer lineUp(Locatable locatable) { return lineUp(locatable, 0); } /** * Line this printer up at the specified number of characters before * the specified locatable object's location, * * @param locatable The locatable object. * @param before The number of characters before the object. * @return This printer. */ public Printer lineUp(Locatable locatable, int before) { if (! locatable.hasLocation()) { throw new IllegalArgumentException("Locatable without location " + locatable); } final Location loc = locatable.getLocation(); if (0 > loc.column - before) { throw new IllegalArgumentException("Invalid character distance " + before); } if (loc.line > line) { for (int i=0; i<loc.line-line; i++) pln(); for (int i=0; i<loc.column-before; i++) p(' '); } else if ((loc.line == line) && (loc.column-before >= column)) { for (int i=0; i<loc.column-before-column; i++) p(' '); } else { p(' '); } return this; } // ======================================================================== /** * Flush the underlying print writer. * * @return This printer. */ public Printer flush() { out.flush(); return this; } /** * Close this printer. This method stops buffering (if this printer * was buffering) and then closes the underlying print writer. */ public void close() { stopBuffering(); out.close(); } }