<?php

/** @file

 * QueryPath templates. See QPTPL.

 */

/**

 * QPTPL is a template library for QueryPath.

 *

 * The QPTPL extension provides template tools that can be used in 

 * conjunction with QueryPath.

 *

 * There are two basic modes in which this tool operates. Both merge data into

 * a pure HTML template. Both base their insertions on classes and IDs in the

 * HTML data. Where they differ is in the kind of data merged into the template.

 *

 * One mode takes array data and does a deep (recursive) merge into the template.

 * It can be used for simple substitutions, but it can also be used to loop through

 * "rows" of data and create tables.

 *

 * The second mode takes a classed object and introspects that object to find out

 * what CSS classes it is capable of filling. This is one way of bridging an object

 * model and QueryPath data.

 *

 * The unit tests are a good place for documentation, as is the QueryPath webste.

 *

 * @author M Butcher <matt@aleph-null.tv>

 * @license http://opensource.org/licenses/lgpl-2.1.php LGPL or MIT-like license.

 * @see QueryPathExtension

 * @see QueryPathExtensionRegistry::extend()

 * @see https://fedorahosted.org/querypath/wiki/QueryPathTemplate

 * @ingroup querypath_extensions

 */

class QPTPL implements QueryPathExtension {

  protected $qp;

  public function __construct(QueryPath $qp) {

    $this->qp = $qp;

  }

  

  /**

   * Apply a template to an object and then insert the results.

   *

   * This takes a template (an arbitrary fragment of XML/HTML) and an object

   * or array and inserts the contents of the object into the template. The 

   * template is then appended to all of the nodes in the current list.

   *

   * Note that the data in the object is *not* escaped before it is merged 

   * into the template. For that reason, an object can return markup (as 

   * long as it is well-formed).

   * 

   * @param mixed $template

   *  The template. It can be of any of the types that {@link qp()} supports

   *  natively. Typically it is a string of XML/HTML.

   * @param mixed $object

   *  Either an object or an associative array. 

   *  - In the case where the parameter

   *  is an object, this will introspect the object, looking for getters (a la

   *  Java bean behavior). It will then search the document for CSS classes

   *  that match the method name. The function is then executed and its contents

   *  inserted into the document. (If the function returns NULL, nothing is 

   *  inserted.)

   *  - In the case where the paramter is an associative array, the function will

   *  look through the template for CSS classes that match the keys of the 

   *  array. When an array key is found, the array value is inserted into the 

   *  DOM as a child of the currently matched element(s).

   * @param array $options

   *  The options for this function. Valid options are:

   *  - <None defined yet>

   * @return QueryPath

   *  Returns a QueryPath object with all of the changes from the template

   *  applied into the QueryPath elements.

   * @see QueryPath::append()

   */

  public function tpl($template, $object, $options = array()) {

    // Handle default options here.



    //$tqp = ($template instanceof QueryPath) ? clone $template: qp($template);

    $tqp = qp($template);

    

    if (is_array($object) || $object instanceof Traversable) {

      $this->tplArrayR($tqp, $object, $options);

      return $this->qp->append($tqp->top());

    }

    elseif (is_object($object)) {

      $this->tplObject($tqp, $object, $options);

    }

    

    return $this->qp->append($tqp->top());

  }

  

  /**

   * Given one template, do substitutions for all objects.

   *

   * Using this method, one template can be populated from a variety of 

   * sources. That one template is then appended to the QueryPath object.

   * @see tpl()

   * @param mixed $template

   *  The template. It can be of any of the types that {@link qp()} supports

   *  natively. Typically it is a string of XML/HTML.

   * @param array $objects

   *  An indexed array containing a list of objects or arrays (See {@link tpl()})

   *  that will be merged into the template.

   * @param array $options

   *  An array of options. See {@link tpl()} for a list.

   * @return QueryPath

   *  Returns the QueryPath object.

   */

  public function tplAll($template, $objects, $options = array()) {

    $tqp = qp($template, ':root');

    foreach ($objects as $object) {

      if (is_array($object)) 

        $tqp = $this->tplArrayR($tqp, $object, $options);

      elseif (is_object($object)) 

        $tqp = $this->tplObject($tqp, $object, $options);

    }

    return $this->qp->append($tqp->top());

  }

  

  /*

  protected function tplArray($tqp, $array, $options = array()) {

    

    // If we find something that's not an array, we try to handle it.

    if (!is_array($array)) {

     is_object($array) ? $this->tplObject($tqp, $array, $options) : $tqp->append($array);

    }

    // An assoc array means we have mappings of classes to content.

    elseif ($this->isAssoc($array)) {

      print 'Assoc array found.' . PHP_EOL;

      foreach ($array as $key => $value) {

        $first = substr($key,0,1);



        // We allow classes and IDs if explicit. Otherwise we assume

        // a class.

        if ($first != '.' && $first != '#') $key = '.' . $key;

        

        if ($tqp->top()->find($key)->size() > 0) {

          print "Value: " . $value . PHP_EOL;

          if (is_array($value)) {

            //$newqp = qp($tqp)->cloneAll();

            print $tqp->xml();

            $this->tplArray($tqp, $value, $options);

            print "Finished recursion\n";

          }

          else {

            print 'QP is ' . $tqp->size() . " inserting value: " . $value . PHP_EOL;

            

            $tqp->append($value);

          }

        }

      }

    }

    // An indexed array means we have multiple instances of class->content matches.

    // We copy the portion of the template and then call repeatedly.

    else {

      print "Array of arrays found..\n";

      foreach ($array as $array2) {

        $clone = qp($tqp->xml());

        $this->tplArray($clone, $array2, $options);

        print "Now appending clone.\n" . $clone->xml();

        $tqp->append($clone->parent());

      }

    }

    

    

    //return $tqp->top();

    return $tqp;

  }

  */

  

  /**

   * Introspect objects to map their functions to CSS classes in a template.

   */

  protected function tplObject($tqp, $object, $options = array()) {

    $ref = new ReflectionObject($object);

    $methods = $ref->getMethods();

    foreach ($methods as $method) {

      if (strpos($method->getName(), 'get') === 0) {

        $cssClass = $this->method2class($method->getName());

        if ($tqp->top()->find($cssClass)->size() > 0) {

          $tqp->append($method->invoke($object));

        }

        else {

          // Revert to the find() that found something.

          $tqp->end();

        }

      }

    }

    //return $tqp->top();

    return $tqp;

  }

  

  /**

   * Recursively merge array data into a template.

   */

  public function tplArrayR($qp, $array, $options = NULL) {

    // If the value looks primitive, append it.

    if (!is_array($array) && !($array instanceof Traversable)) {

      $qp->append($array);

    }

    // If we are dealing with an associative array, traverse it

    // and merge as we go.

    elseif ($this->isAssoc($array)) {

      // Do key/value substitutions

      foreach ($array as $k => $v) {

        

        // If no dot or hash, assume class.

        $first = substr($k,0,1);

        if ($first != '.' && $first != '#') $k = '.' . $k;

        

        // If value is an array, recurse.

        if (is_array($v)) {

          // XXX: Not totally sure that starting at the 

          // top is right. Perhaps it should start

          // at some other context?

          $this->tplArrayR($qp->top($k), $v, $options);

        }

        // Otherwise, try to append value.

        else {

          $qp->branch()->children($k)->append($v);

        }

      }

    }

    // Otherwise we have an indexed array, and we iterate through

    // it.

    else {

      // Get a copy of the current template and then recurse.

      foreach ($array as $entry) {

        $eles = $qp->get();

        $template = array();

        

        // We manually deep clone the template.

        foreach ($eles as $ele) {

          $template = $ele->cloneNode(TRUE);

        }

        $tpl = qp($template);

        $tpl = $this->tplArrayR($tpl, $entry, $options);

        $qp->before($tpl);

      }

      // Remove the original template without loosing a handle to the

      // newly injected one.

      $dead = $qp->branch();

      $qp->parent();

      $dead->remove();

      unset($dead);

    }

    return $qp;

  }

  

  /**

   * Check whether an array is associative.

   * If the keys of the array are not consecutive integers starting with 0,

   * this will return false.

   *

   * @param array $array

   *  The array to test.

   * @return Boolean

   *  TRUE if this is an associative array, FALSE otherwise.

   */

  public function isAssoc($array) {

    $i = 0;

    foreach ($array as $k => $v) if ($k !== $i++) return TRUE;

    // If we get here, all keys passed.

    return FALSE;

  }



  /**

   * Convert a function name to a CSS class selector (e.g. myFunc becomes '.myFunc').

   * @param string $mname

   *  Method name.

   * @return string

   *  CSS 3 Class Selector.

   */

  protected function method2class($mname) {

    return '.' . substr($mname, 3);

  }

}

QueryPathExtensionRegistry::extend('QPTPL');