/* __ *\ ** ________ ___ / / ___ Scala API ** ** / __/ __// _ | / / / _ | (c) 2003-2009, LAMP/EPFL ** ** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** ** /____/\___/_/ |_/____/_/ | | ** ** |/ ** \* */ // $Id: PrettyPrinter.scala 17856 2009-05-27 19:35:02Z dubochet $ package scala.xml import scala.collection.Map import Utility.sbToString /** Class for pretty printing. After instantiating, you can use the * format() and formatNode() methods to convert XML to a formatted * string. The class can be reused to pretty print any number of * XML nodes. * * @author Burak Emir * @version 1.0 * * @param width the width to fit the output into * @step indentation */ class PrettyPrinter( width:Int, step:Int ) { class BrokenException() extends java.lang.Exception class Item case object Break extends Item { override def toString() = "\\" } case class Box(col: Int, s: String) extends Item case class Para(s: String) extends Item protected var items: List[Item] = Nil protected var cur = 0 //protected var pmap:Map[String,String] = _ protected def reset() = { cur = 0 items = Nil } /** Try to cut at whitespace. * * @param s ... * @param ind ... * @return ... */ protected def cut(s: String, ind: Int): List[Item] = { val tmp = width - cur if (s.length <= tmp) return List(Box(ind, s)) val sb = new StringBuilder() var i = s.indexOf(' ') if (i > tmp || i == -1) throw new BrokenException() // cannot break var last: List[Int] = Nil while (i != -1 && i < tmp) { last = i::last i = s.indexOf(' ', i+1) } var res: List[Item] = Nil while (Nil != last) try { val b = Box(ind, s.substring(0, last.head)) cur = ind res = b :: Break :: cut(s.substring(last.head, s.length), ind) // backtrack last = last.tail } catch { case _:BrokenException => last = last.tail } throw new BrokenException() } /** Try to make indented box, if possible, else para. * * @param ind ... * @param s ... * @return ... */ protected def makeBox(ind: Int, s: String) = { if (cur < ind) cur == ind if (cur + s.length > width) { // fits in this line items = Box(ind, s) :: items cur += s.length } else try { for (b <- cut(s, ind).iterator) // break it up items = b :: items } catch { case _:BrokenException => makePara(ind, s) // give up, para } } // dont respect indent in para, but afterwards protected def makePara(ind: Int, s: String) = { items = Break::Para(s)::Break::items cur = ind } // respect indent protected def makeBreak() = { // using wrapping here... items = Break :: items cur = 0 } /** * @param n ... * @return ... */ protected def leafTag(n: Node) = { def mkLeaf(sb: StringBuilder) { sb append '<' n nameToString sb n.attributes buildString sb sb append "/>" } sbToString(mkLeaf) } protected def startTag(n: Node, pscope: NamespaceBinding): (String, Int) = { var i = 0 def mkStart(sb: StringBuilder) { sb append '<' n nameToString sb i = sb.length + 1 n.attributes buildString sb n.scope.buildString(sb, pscope) sb append '>' } (sbToString(mkStart), i) } protected def endTag(n: Node) = { def mkEnd(sb: StringBuilder) { sb append "</" n nameToString sb sb append '>' } sbToString(mkEnd) } protected def childrenAreLeaves(n: Node): Boolean = { def isLeaf(l: Node) = l match { case _:Atom[_] | _:Comment | _:EntityRef | _:ProcInstr => true case _ => false } n.child forall isLeaf } protected def fits(test: String) = test.length < width - cur private def doPreserve(node: Node) = node.attribute(XML.namespace, XML.space).map(_.toString == XML.preserve) getOrElse false /** @param tail: what we'd like to sqeeze in */ protected def traverse(node: Node, pscope: NamespaceBinding, ind: Int): Unit = node match { case Text(s) if s.trim() == "" => ; case _:Atom[_] | _:Comment | _:EntityRef | _:ProcInstr => makeBox( ind, node.toString().trim() ) case g @ Group(xs) => traverse(xs.iterator, pscope, ind) case _ => val test = { val sb = new StringBuilder() Utility.toXML(node, pscope, sb, false) if (doPreserve(node)) sb.toString else TextBuffer.fromString(sb.toString()).toText(0)._data } if (childrenAreLeaves(node) && fits(test)) { makeBox(ind, test) } else { val (stg, len2) = startTag(node, pscope) val etg = endTag(node) if (stg.length < width - cur) { // start tag fits makeBox(ind, stg) makeBreak() traverse(node.child.iterator, node.scope, ind + step) makeBox(ind, etg) } else if (len2 < width - cur) { // <start label + attrs + tag + content + end tag makeBox(ind, stg.substring(0, len2)) makeBreak() // todo: break the rest in pieces /*{ //@todo val sq:Seq[String] = stg.split(" "); val it = sq.iterator; it.next; for (c <- it) { makeBox(ind+len2-2, c) makeBreak() } }*/ makeBox(ind, stg.substring(len2, stg.length)) makeBreak() traverse(node.child.iterator, node.scope, ind + step) makeBox(cur, etg) makeBreak() } else { // give up makeBox(ind, test) makeBreak() } } } protected def traverse(it: Iterator[Node], scope: NamespaceBinding, ind: Int ): Unit = for (c <- it) { traverse(c, scope, ind) makeBreak() } /** Appends a formatted string containing well-formed XML with * given namespace to prefix mapping to the given string buffer. * * @param n the node to be serialized * @param pmap the namespace to prefix mapping * @param sb the stringbuffer to append to */ def format(n: Node, sb: StringBuilder ): Unit = // entry point format(n, null, sb) def format(n: Node, pscope: NamespaceBinding, sb: StringBuilder): Unit = { // entry point var lastwasbreak = false reset() traverse(n, pscope, 0) var cur = 0 for (b <- items.reverse) b match { case Break => if (!lastwasbreak) sb.append('\n') // on windows: \r\n ? lastwasbreak = true cur = 0 // while( cur < last ) { // sb.append(' '); // cur = cur + 1; // } case Box(i, s) => lastwasbreak = false while (cur < i) { sb.append(' ') cur += 1 } sb.append(s) case Para( s ) => lastwasbreak = false sb.append(s) } } // public convenience methods /** returns a formatted string containing well-formed XML with * default namespace prefix mapping * * @param n the node to be serialized * @return ... */ def format(n: Node): String = format(n, null) //Utility.defaultPrefixes(n)) /** Returns a formatted string containing well-formed XML with * given namespace to prefix mapping. * * @param n the node to be serialized * @param pmap the namespace to prefix mapping * @return ... */ def format(n: Node, pscope: NamespaceBinding): String = sbToString(format(n, pscope, _)) /** Returns a formatted string containing well-formed XML nodes with * default namespace prefix mapping. * * @param nodes ... * @return ... */ def formatNodes(nodes: Seq[Node]): String = formatNodes(nodes, null) /** Returns a formatted string containing well-formed XML. * * @param nodes the sequence of nodes to be serialized * @param pmap the namespace to prefix mapping */ def formatNodes(nodes: Seq[Node], pscope: NamespaceBinding): String = sbToString(formatNodes(nodes, pscope, _)) /** Appends a formatted string containing well-formed XML with * the given namespace to prefix mapping to the given stringbuffer. * * @param n the node to be serialized * @param pmap the namespace to prefix mapping * @param sb the string buffer to which to append to */ def formatNodes(nodes: Seq[Node], pscope: NamespaceBinding, sb: StringBuilder): Unit = for (n <- nodes.iterator) { sb.append(format(n, pscope)) } }