summaryrefslogtreecommitdiff
path: root/lib/querypath/Extension/QPTPL.php
blob: 6e479357b5f7be11f3e5c31b9c06c31fe57c4eb5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
<?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');