<?php
/**
 * XMLFormatter2
 *
 * This class allows you to format/indent an XML string.
 *
 * Original version was taken from Zilvinas Saltys <zilvinas.saltys@gmail.com>
 * on http://www.thedeveloperday.com/ (version 0.3.0). Thanks!
 *
 * Main difference: this version takes a string instead of streams.
 *
 * @author Guenther Mair <info@crowdedplace.com>
 * @url http://www.crowdedplace.com/
 * @version 2.0
 * @package XMLFormatter2
 */
class XMLFormatter2
{
  protected $_parser = null;
  protected $_input = "";
  protected $_output = "";
  protected $_offset = 0;
  protected $_depth = 0;
  protected $_empty = false;
  protected $_options = array(
    "bufferSize"        => 4096, // in KB
    "paddingString"     => " ",  // for indentation
    "paddingMultiplier" => 2,    // indentation multiplier
    "formatCData"       => true, // whether to format CDATA
    "multipleLineCData" => true, // whether to format multiline CDATA
    "wordwrapCData"     => 75,   // CDATA wordwrap length (or FALSE)
    "inputEOL"          => "\n", // end of line (input)
    "outputEOL"         => "\n", // end of line (output)
  );

  /**
   * Constructor
   *
   * @access public
   * @param  array  options
   * @return void
   */
  public function __construct(Array $options = array()) {
    $this->_options = array_merge($this->_options, $options);

    $this->_parser = xml_parser_create();

    xml_set_object($this->_parser, $this);

    xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false);
    xml_parser_set_option($this->_parser, XML_OPTION_SKIP_WHITE, 0);

    xml_set_element_handler($this->_parser, "_cbElementStart", "_cbElementEnd");
    xml_set_character_data_handler($this->_parser, "_cbCharacterData");
  }

  /**
   * Get padding string relative to XML depth index
   *
   * @access protected
   * @param  void
   * @return string    padding string
   */
  protected function _getPaddingStr() {
    return str_repeat($this->_options["paddingString"], $this->_depth * $this->_options["paddingMultiplier"]);
  }

  /**
   * Element start callback
   *
   * @access protected
   * @param  resource  $parser xml parser
   * @param  string    $name element name
   * @param  array     $attributes element attributes
   * @return void
   */
  protected function _cbElementStart($parser, $name, Array $attributes) {
    $idx = xml_get_current_byte_index($this->_parser);
    $this->_empty = $this->_input[$idx - $this->_offset] == '/';

    $attrs = "";
    foreach ($attributes as $key => $val)
      $attrs .= " " . $key . "=\"" . $val . "\"";

    $this->_output .= $this->_getPaddingStr() . "<" . $name . $attrs . ($this->_empty ? ' />' : '>') . $this->_options["outputEOL"];

    if ( ! $this->_empty)
      $this->_depth++;
  }

  /**
   * Element end callback
   *
   * @access protected
   * @param  resource  $parser xml parser
   * @param  string   $name element name
   * @return void
   */
  protected function _cbElementEnd($parser, $name) {
    if ( ! $this->_empty) {
      $this->_depth--;
      $this->_output .= $this->_getPaddingStr() . "</" . $name . ">" . $this->_options["outputEOL"];
    } else {
      $this->_empty = false;
    }
  }

  /**
   * Character data callback
   *
   * @access protected
   * @param  resource  $parser xml parser
   * @param  string    $data character data
   * @return void
   */
  protected function _cbCharacterData($parser, $data) {
    if ( ! $this->_options["formatCData"])
      return;

    $data = trim($data);
      
    if (strlen($data)) {
      $pad = $this->_getPaddingStr();
      
      if ($this->_options["multipleLineCData"]) {
        // remove tabs
        $data = str_replace("\t", "", $data);
        
        // append each line with a padding string
        $data = implode($this->_options["inputEOL"] . $pad, explode($this->_options["inputEOL"], $data));
      }
      
      if ($this->_options["wordwrapCData"])
        $data = wordwrap($data, $this->_options["wordwrapCData"], $this->_options["outputEOL"] . $pad, false);
      
      $this->_output .= $pad . $data . $this->_options["outputEOL"];
    }
  }

  /**
   * Main format method
   *
   * @access public
   * @param  string    input XML string
   * @throws Exception
   * @return string    output XML string
   */
  public function format($input) {
    $this->_input = $input;
    if ( ! xml_parse($this->_parser, $this->_input, TRUE))
      throw new Exception(sprintf("XML error: %s at line %d",
        xml_error_string(xml_get_error_code($this->_parser)),
        xml_get_current_line_number($this->_parser)));

    xml_parser_free($this->_parser);

    return $this->_output;
  }
}
