All Downloads are FREE. Search and download functionalities are using the official Maven repository.

scala.xml.PrettyPrinter.scala Maven / Gradle / Ivy

The newest version!
/*
 * Scala (https://www.scala-lang.org)
 *
 * Copyright EPFL and Lightbend, Inc.
 *
 * Licensed under Apache License 2.0
 * (http://www.apache.org/licenses/LICENSE-2.0).
 *
 * See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 */

package scala
package xml

import scala.collection.Seq
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
 *  @param  width         the width to fit the output into
 *  @param  step          indentation
 *  @param  minimizeEmpty self-close empty tags
 *  @note   This class is not threadsafe and should not be accessed by
 *          multiple threads at the same time.
 */
class PrettyPrinter(width: Int, step: Int, minimizeEmpty: Boolean) {

  def this(width: Int, step: Int) = this(width, step, minimizeEmpty = false)

  val minimizeMode: MinimizeMode.Value = if (minimizeEmpty) MinimizeMode.Always else MinimizeMode.Default
  class BrokenException() extends java.lang.Exception

  class Item
  case object Break extends Item {
    override def toString: String = "\\"
  }
  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: Int = 0

  protected def reset(): Unit = {
    cur = 0
    items = Nil
  }

  /**
   * Try to cut at whitespace.
   */
  protected def cut(s: String, ind: Int): List[Item] = {
    val tmp: Int = width - cur
    if (s.length <= tmp)
      return List(Box(ind, s))
    var i: Int = 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 = 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.
   */
  protected def makeBox(ind: Int, s: String): Unit =
    if (cur + s.length > width) { // fits in this line
      items ::= Box(ind, s)
      cur += s.length
    } else try cut(s, ind).foreach(items ::= _) // break it up
    catch { case _: BrokenException => makePara(ind, s) } // give up, para

  // dont respect indent in para, but afterwards
  protected def makePara(ind: Int, s: String): Unit = {
    items = Break :: Para(s) :: Break :: items
    cur = ind
  }

  // respect indent
  protected def makeBreak(): Unit = { // using wrapping here...
    items = Break :: items
    cur = 0
  }

  protected def leafTag(n: Node): String = {
    def mkLeaf(sb: StringBuilder): Unit = {
      sb.append('<')
      n.nameToString(sb)
      n.attributes.buildString(sb)
      sb.append("/>")
    }
    sbToString(mkLeaf)
  }

  protected def startTag(n: Node, pscope: NamespaceBinding): (String, Int) = {
    var i: Int = 0
    def mkStart(sb: StringBuilder): Unit = {
      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): String = {
    def mkEnd(sb: StringBuilder): Unit = {
      sb.append("')
    }
    sbToString(mkEnd)
  }

  protected def childrenAreLeaves(n: Node): Boolean = {
    def isLeaf(l: Node): Boolean = l match {
      case _: Atom[?] | _: Comment | _: EntityRef | _: ProcInstr => true
      case _ => false
    }
    n.child.forall(isLeaf)
  }

  protected def fits(test: String): Boolean =
    test.length < width - cur

  private def doPreserve(node: Node): Boolean =
    node.attribute(XML.namespace, XML.space).exists(_.toString == XML.preserve)

  protected def traverse(node: Node, pscope: NamespaceBinding, ind: Int): Unit = node match {

    case Text(s) if s.trim.isEmpty =>

    case _: Atom[?] | _: Comment | _: EntityRef | _: ProcInstr =>
      makeBox(ind, node.toString.trim)
    case Group(xs) =>
      traverse(xs.iterator, pscope, ind)
    case _ =>
      val test: String = {
        val sb: StringBuilder = new StringBuilder()
        Utility.serialize(node, pscope, sb, stripComments = false, minimizeTags = minimizeMode)
        if (doPreserve(node)) sb.toString
        else TextBuffer.fromString(sb.toString).toText(0).data
      }
      if (childrenAreLeaves(node) && fits(test))
        makeBox(ind, test)
      else {
        val ((stg: String, len2: Int), etg: String) =
          if (node.child.isEmpty && minimizeEmpty) {
            // force the tag to be self-closing
            val firstAttribute: Int = test.indexOf(' ')
            val firstBreak: Int = if (firstAttribute != -1) firstAttribute else test.lastIndexOf('/')
            ((test, firstBreak), "")
          } else
            (startTag(node, pscope), 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) {
          // 
        if (!lastwasbreak) sb.append('\n') // on windows: \r\n ?
        lastwasbreak = true
        cur = 0
      //        while (cur < last) {
      //          sb.append(' ')
      //          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
   *  given namespace to prefix mapping.
   *
   *  @param n      the node to be serialized
   *  @param pscope the namespace to prefix mapping
   *  @return      the formatted string
   */
  def format(n: Node, pscope: NamespaceBinding = TopScope): String =
    sbToString(format(n, pscope, _))

  /**
   * Returns a formatted string containing well-formed XML.
   *
   *  @param nodes  the sequence of nodes to be serialized
   *  @param pscope the namespace to prefix mapping
   */
  def formatNodes(nodes: Seq[Node], pscope: NamespaceBinding = TopScope): 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 nodes  the nodes to be serialized
   *  @param pscope 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 =
    nodes.foreach(n => sb.append(format(n, pscope)))
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy