7ff9c04d5140dfe524ed7624870bd441039f915c
[pear] / XML / Tree / Node.php
1 <?php
2 // +----------------------------------------------------------------------+
3 // | PEAR :: XML_Tree                                                     |
4 // +----------------------------------------------------------------------+
5 // | Copyright (c) 1997-2003 The PHP Group                                |
6 // +----------------------------------------------------------------------+
7 // | This source file is subject to version 2.02 of the PHP license,      |
8 // | that is bundled with this package in the file LICENSE, and is        |
9 // | available at through the world-wide-web at                           |
10 // | http://www.php.net/license/2_02.txt.                                 |
11 // | If you did not receive a copy of the PHP license and are unable to   |
12 // | obtain it through the world-wide-web, please send a note to          |
13 // | license@php.net so we can mail you a copy immediately.               |
14 // +----------------------------------------------------------------------+
15 // | Authors: Bernd Römer <berndr@bonn.edu>                               |
16 // |          Sebastian Bergmann <sb@sebastian-bergmann.de>               |
17 // |          Christian Kühn <ck@chkuehn.de> (escape xml entities)        |
18 // |          Michele Manzato <michele.manzato@verona.miz.it>             |
19 // +----------------------------------------------------------------------+
20 //
21 // $Id: Node.php,v 1.18 2003/09/11 19:03:27 cox Exp $
22 //
23
24 /**
25 * PEAR::XML_Tree_Node
26 *
27 * @author  Bernd Römer <berndr@bonn.edu>
28 * @package XML_Tree
29 * @version 1.0  16-Aug-2001
30 */
31 class XML_Tree_Node {
32     /**
33     * Attributes of this node
34     *
35     * @var  array
36     */
37     var $attributes;
38
39     /**
40     * Children of this node
41     *
42     * @var  array
43     */
44     var $children;
45
46     /**
47     * Content (text) of this node
48     *
49     * @var  string
50     */
51     var $content;
52
53     /**
54     * Name of the node
55     *
56     * @var  string
57     */
58     var $name;
59
60     /**
61     * Constructor
62     *
63     * @param  string    name            Node name
64     * @param  string    content         Node content (text)
65     * @param  array     attributes      Attribute-hash for the node
66     */
67     function XML_Tree_Node($name, $content = '', $attributes = array(), $lineno = null)
68     {
69         $this->name = $name;
70         $this->setContent($content);
71         $this->attributes = $attributes;
72         $this->children   = array();
73         $this->lineno     = $lineno;
74     }
75
76     /**
77     * Append a child node to this node, after all other nodes
78     *
79     * @param mixed      child           Child to insert (XML_Tree or XML_Tree_Node),
80     *                                   or name of child node
81     * @param string     content         Content (text) for the new node (only if
82     *                                   $child is the node name)
83     * @param array      attributes      Attribute-hash for new node
84     *
85     * @return object  reference to new child node
86     * @access public
87     */
88     function &addChild($child, $content = '', $attributes = array(), $lineno = null)
89     {
90         $index = sizeof($this->children);
91
92         if (is_object($child)) {
93             if (strtolower(get_class($child)) == 'xml_tree_node') {
94                 $this->children[$index] = $child;
95             }
96
97             if (strtolower(get_class($child)) == 'xml_tree' && isset($child->root)) {
98                 $this->children[$index] = $child->root->getElement();
99             }
100         } else {
101             $this->children[$index] = new XML_Tree_Node($child, $content, $attributes, $lineno);
102         }
103
104         return $this->children[$index];
105     }
106
107     /**
108     * Get a copy of this node by clone this node and all of its children,
109     * recursively.
110     *
111     * @return object    Reference to the cloned copy.
112     * @access public
113     */
114     function &cloneNode()
115     {
116         $clone = new XML_Tree_Node($this->name,$this->content,$this->attributes);
117
118         $max_child=count($this->children);
119         for($i=0;$i<$max_child;$i++) {
120             $clone->children[]=$this->children[$i]->cloneNode();
121         }
122
123         /* for future use....
124             // clone all other vars
125             $temp=get_object_vars($this);
126             foreach($temp as $varname => $value)
127                 if (!in_array($varname,array('name','content','attributes','children')))
128                     $clone->$varname=$value;
129         */
130
131         return $clone;
132     }
133
134     /**
135     * Inserts child ($child) to a specified child-position ($pos)
136     *
137     * @param mixed      path            Path to parent node to add child (see getNodeAt()
138     *                                   for format). If null child is inserted in the
139     *                                   current node.
140     * @param integer    pos             Position where to insert the new child.
141     *                                   0 < means |$pos| elements before the end,
142     *                                   e.g. -1 appends as last child.
143     * @param mixed      child           Child to insert (XML_Tree or XML_Tree_Node),
144     *                                   or name of child node
145     * @param string     content         Content (text) for the new node (only if
146     *                                   $child is the node name)
147     * @param array      attributes      Attribute-hash for new node
148     *
149     * @return Reference to the newly inserted node, or PEAR_Error upon insertion error.
150     * @access public
151     */
152     function &insertChild($path,$pos,&$child, $content = '', $attributes = array())
153     {
154         $parent =& $this->getNodeAt($path);
155         if (PEAR::isError($parent)) {
156             // $path was not found
157             return $parent;
158         } elseif ($parent != $this) {
159             // Insert at the node found
160             return $parent->insertChild(null, $pos, $child, $content, $attributes);
161         }
162
163         if (($pos < -count($this->children)) || ($pos > count($this->children))) {
164             return new PEAR_Error("Invalid insert position.");
165         }
166
167         if (is_object($child)) { // child is an object
168             // insert a single node
169             if (strtolower(get_class($child)) == 'xml_tree_node') {
170                 array_splice($this->children, $pos, 0, 'dummy');
171                 if ($pos < 0) {
172                     $pos = count($this->children) + $pos - 1;
173                 }
174                 $this->children[$pos] = &$child;
175             // insert a tree i.e insert root-element of tree
176             } elseif (strtolower(get_class($child)) == 'xml_tree' && isset($child->root)) {
177                 array_splice($this->children, $pos, 0, 'dummy');
178                 if ($pos < 0) {
179                     $pos = count($this->children) + $pos - 1;
180                 }
181                 $this->children[$pos] = $child->root;
182             } else {
183                 return new PEAR_Error("Bad node (must be a XML_Tree or an XML_Tree_Node)");
184             }
185         } else { // child offered is a string
186             array_splice($this->children, $pos, 0, 'dummy');
187             if ($pos < 0) {
188                 $pos = count($this->children) + $pos - 1;
189             }
190             $this->children[$pos] = new XML_Tree_Node($child, $content, $attributes);
191         }
192         return $this;
193     }
194
195     /**
196     * Removes child at a given position
197     *
198     * @param    integer     pos     position of child to remove in children-list.
199     *                               0 < means |$pos| elements before the end,
200     *                               e.g. -1 removes the last child.
201     *
202     * @return mixed     The removed node, or PEAR_Error upon removal error.
203     * @access public
204     */
205     function &removeChild($pos)
206     {
207         if (($pos < -count($this->children)) || ($pos >= count($this->children))) {
208             return new PEAR_Error("Invalid remove position.");
209         }
210
211         // Using array_splice() instead of a simple unset() to maintain index-integrity
212         return array_splice($this->children, $pos, 1);
213     }
214
215     /**
216     * Returns text representation of this node.
217     *
218     * @return  string   text (xml) representation of this node. Each tag is
219     *                   indented according to its level.
220     * @access public
221     */
222     function &get()
223     {
224         static $deep = -1;
225         static $do_ident = true;
226         $deep++;
227         if ($this->name !== null) {
228             $ident = str_repeat('  ', $deep);
229             if ($do_ident) {
230                 $out = $ident . '<' . $this->name;
231             } else {
232                 $out = '<' . $this->name;
233             }
234             foreach ($this->attributes as $name => $value) {
235                 $out .= ' ' . $name . '="' . $value . '"';
236             }
237
238             $out .= '>' . $this->content;
239
240             if (sizeof($this->children) > 0) {
241                 $out .= "\n";
242                 foreach ($this->children as $child) {
243                     $out .= $child->get();
244                 }
245             } else {
246                 $ident = '';
247             }
248             if ($do_ident) {
249                 $out .= $ident . '</' . $this->name . ">\n";
250             } else {
251                 $out .= '</' . $this->name . '>';
252             }
253             $do_ident = true;
254         } else {
255             $out = $this->content;
256             $do_ident = false;
257         }
258         $deep--;
259         return $out;
260     }
261
262     /**
263     * Get an attribute by its name.
264     *
265     * @param  string  $name     Name of attribute to retrieve
266     *
267     * @return string  attribute, or null if attribute is unset.
268     * @access public
269     */
270     function getAttribute($name)
271     {
272         if (isset($this->attributes[strtolower($name)])) {
273             return $this->attributes[strtolower($name)];
274         }
275         return null;
276     }
277
278     /**
279     * Sets an attribute for this node.
280     *
281     * @param  string    name        Name of attribute to set
282     * @param  string    value       Value of attribute
283     *
284     * @access public
285     */
286     function setAttribute($name, $value = '')
287     {
288         $this->attributes[strtolower($name)] = $value;
289     }
290
291     /**
292     * Unsets an attribute of this node.
293     *
294     * @param  string  $name     Name of attribute to unset
295     *
296     * @access public
297     */
298     function unsetAttribute($name)
299     {
300         if (isset($this->attributes[strtolower($name)])) {
301             unset($this->attributes[strtolower($name)]);
302         }
303     }
304
305     /**
306     * Sets the content for this node.
307     *
308     * @param  string    content     Node content to assign
309     *
310     * @access public
311     */
312     function setContent(&$content)
313     {
314         $this->content = $this->encodeXmlEntities($content);
315     }
316
317     /**
318     * Gets an element by its 'path'.
319     *
320     * @param  array     path    path to element: sequence of indexes to the
321     *                           children. E.g. array(1, 2, 3) means "third
322     *                           child of second child of first child" of the node.
323     *
324     * @return object    reference to element found, or PEAR_Error if node can't
325     *                   be found.
326     * @access public
327     */
328     function &getElement($path)
329     {
330         if (!is_array($path)) {
331             $path = array($path);
332         }
333         if (sizeof($path) == 0) {
334             return $this;
335         }
336
337         $path1 = $path;
338         $next = array_shift($path1);
339         if (isset($this->children[$next])) {
340             $x =& $this->children[$next]->getElement($path1);
341             if (!PEAR::isError($x)) {
342                 return $x;
343             }
344         }
345
346         return new PEAR_Error("Bad path to node: [".implode('-', $path)."]");
347     }
348
349     /**
350     * Get a reference to a node by its 'path'.
351     *
352     * @param  mixed     path    Path to node. Can be either a string (slash-separated
353     *   children names) or an array (sequence of children names) both
354     *                           starting from this node. The first name in sequence
355     *   is a child name, not the name of this node.
356     *
357     * @return object    Reference to the XML_Tree_Node found, or PEAR_Error if
358     *                   the path does not match any node. Note that if more than
359     *                   one element matches then only the first matching node is
360     *                   returned.
361     * @access public
362     */
363     function &getNodeAt($path)
364     {
365         if (is_string($path))
366             $path = explode("/", $path);
367
368         if (sizeof($path) == 0) {
369             return $this;
370         }
371
372         $path1 = $path;
373         $next = array_shift($path1);
374
375         // Get the first children of this node whose name is '$next'
376         $child = null;
377         for ($i = 0; $i < count($this->children); $i++) {
378             if ($this->children[$i]->name == $next) {
379                 $child =& $this->children[$i];
380                 break;
381             }
382         }
383         if (!is_null($child)) {
384             $x =& $child->getNodeAt($path1);
385             if (!PEAR::isError($x)) {
386                 return $x;
387             }
388         }
389
390         // No node with that name found
391         return new PEAR_Error("Bad path to node: [".implode('/', $path)."]");
392     }
393
394     /**
395     * Escape XML entities.
396     *
397     * @param   string  xml      Text string to escape.
398     *
399     * @return  string  xml
400     * @access  public
401     */
402     function encodeXmlEntities($xml)
403     {
404         $xml = str_replace(array('ü', 'Ü', 'ö',
405                                  'Ö', 'ä', 'Ä',
406                                  'ß', '<', '>',
407                                  '"', '\''
408                                 ),
409                            array('&#252;', '&#220;', '&#246;',
410                                  '&#214;', '&#228;', '&#196;',
411                                   '&#223;', '&lt;', '&gt;',
412                                   '&quot;', '&apos;'
413                                 ),
414                            $xml
415                           );
416         // fixme - remove the /e!!!
417         $xml = @preg_replace(array("/\&([a-z\d\#]+)\;/i",
418                                   "/\&/",
419                                   "/\#\|\|([a-z\d\#]+)\|\|\#/i",
420                                   "/([^a-zA-Z\d\s\<\>\&\;\.\:\=\"\-\/\%\?\!\'\(\)\[\]\{\}\$\#\+\,\@_])/e"
421                                  ),
422                             array("#||\\1||#",
423                                   "&amp;",
424                                   "&\\1;",
425                                   "'&#'.ord('\\1').';'"
426                                  ),
427                             $xml
428                            );
429
430         return $xml;
431     }
432
433     /**
434     * Decode XML entities in a text string.
435     *
436     * @param   string  xml  Text to decode
437     *
438     * @return  string  Decoded text
439     * @access  public
440     */
441     function decodeXmlEntities($xml)
442     {
443         static $trans_tbl = null;
444         if (!$trans_tbl) {
445             $trans_tbl = get_html_translation_table(HTML_ENTITIES);
446             $trans_tbl = array_flip($trans_tbl);
447         }
448         for ($i = 1; $i <= 255; $i++) {
449             $ent = sprintf("&#%03d;", $i);
450             $ch = chr($i);
451             $xml = str_replace($ent, $ch, $xml);
452         }
453
454         return strtr($xml, $trans_tbl);
455     }
456
457
458     /**
459     * Print text representation of XML_Tree_Node.
460     *
461     * @access  public
462     */
463     function dump() {
464         echo $this->get();
465     }
466 }