1 /* xml++.cc
2  * libxml++ and this file are copyright (C) 2000 by Ari Johnson, and
3  * are covered by the GNU Lesser General Public License, which should be
4  * included with libxml++ as the file COPYING.
5  * Modified for Ardour and released under the same terms.
6  */
7 
8 #include <string.h>
9 #include <iostream>
10 
11 #include "pbd/xml++.h"
12 
13 #include <libxml/debugXML.h>
14 #include <libxml/xpath.h>
15 #include <libxml/xpathInternals.h>
16 
17 xmlChar* xml_version = xmlCharStrdup("1.0");
18 
19 using namespace std;
20 
21 static XMLNode*           readnode(xmlNodePtr);
22 static void               writenode(xmlDocPtr, XMLNode*, xmlNodePtr, int);
23 static XMLSharedNodeList* find_impl(xmlXPathContext* ctxt, const string& xpath);
24 
XMLTree()25 XMLTree::XMLTree()
26 	: _filename()
27 	, _root(0)
28 	, _doc (0)
29 	, _compression(0)
30 {
31 }
32 
XMLTree(const string & fn,bool validate)33 XMLTree::XMLTree(const string& fn, bool validate)
34 	: _filename(fn)
35 	, _root(0)
36 	, _doc (0)
37 	, _compression(0)
38 {
39 	read_internal(validate);
40 }
41 
XMLTree(const XMLTree * from)42 XMLTree::XMLTree(const XMLTree* from)
43 	: _filename(from->filename())
44 	, _root(new XMLNode(*from->root()))
45 	, _doc (xmlCopyDoc (from->_doc, 1))
46 	, _compression(from->compression())
47 {
48 
49 }
50 
~XMLTree()51 XMLTree::~XMLTree()
52 {
53 	delete _root;
54 
55 	if (_doc) {
56 		xmlFreeDoc (_doc);
57 	}
58 }
59 
60 int
set_compression(int c)61 XMLTree::set_compression(int c)
62 {
63 	if (c > 9) {
64 		c = 9;
65 	} else if (c < 0) {
66 		c = 0;
67 	}
68 
69 	_compression = c;
70 
71 	return _compression;
72 }
73 
74 bool
read_internal(bool validate)75 XMLTree::read_internal(bool validate)
76 {
77 	//shouldnt be used anywhere ATM, remove if so!
78 	assert(!validate);
79 
80 	delete _root;
81 	_root = 0;
82 
83 	if (_doc) {
84 		xmlFreeDoc (_doc);
85 		_doc = 0;
86 	}
87 
88 	/* Calling this prevents libxml2 from treating whitespace as active
89 	   nodes. It needs to be called before we create a parser context.
90 	*/
91 	xmlKeepBlanksDefault(0);
92 
93 	/* create a parser context */
94 	xmlParserCtxtPtr ctxt = xmlNewParserCtxt();
95 	if (ctxt == NULL) {
96 		return false;
97 	}
98 
99 	/* parse the file, activating the DTD validation option */
100 	if (validate) {
101 		_doc = xmlCtxtReadFile(ctxt, _filename.c_str(), NULL, XML_PARSE_DTDVALID);
102 	} else {
103 		_doc = xmlCtxtReadFile(ctxt, _filename.c_str(), NULL, XML_PARSE_HUGE);
104 	}
105 
106 	/* check if parsing suceeded */
107 	if (_doc == NULL) {
108 		xmlFreeParserCtxt(ctxt);
109 		return false;
110 	} else {
111 		/* check if validation suceeded */
112 		if (validate && ctxt->valid == 0) {
113 			xmlFreeParserCtxt(ctxt);
114 			throw XMLException("Failed to validate document " + _filename);
115 		}
116 	}
117 
118 	_root = readnode(xmlDocGetRootElement(_doc));
119 
120 	/* free up the parser context */
121 	xmlFreeParserCtxt(ctxt);
122 
123 	return true;
124 }
125 
126 bool
read_buffer(char const * buffer,bool to_tree_doc)127 XMLTree::read_buffer (char const* buffer, bool to_tree_doc)
128 {
129 	xmlDocPtr doc;
130 
131 	_filename = "";
132 
133 	delete _root;
134 	_root = 0;
135 
136 	xmlKeepBlanksDefault(0);
137 
138 	doc = xmlParseMemory (buffer, ::strlen(buffer));
139 	if (!doc) {
140 		return false;
141 	}
142 
143 	_root = readnode(xmlDocGetRootElement(doc));
144 	if (to_tree_doc) {
145 		if (_doc) {
146 			xmlFreeDoc (_doc);
147 		}
148 		_doc = doc;
149 	} else {
150 		xmlFreeDoc (doc);
151 	}
152 
153 	return true;
154 }
155 
156 
157 bool
write() const158 XMLTree::write() const
159 {
160 	xmlDocPtr doc;
161 	XMLNodeList children;
162 	int result;
163 
164 	xmlKeepBlanksDefault(0);
165 	doc = xmlNewDoc(xml_version);
166 	xmlSetDocCompressMode(doc, _compression);
167 	writenode(doc, _root, doc->children, 1);
168 	result = xmlSaveFormatFileEnc(_filename.c_str(), doc, "UTF-8", 1);
169 #ifndef NDEBUG
170 	if (result == -1) {
171 		xmlErrorPtr xerr = xmlGetLastError ();
172 		if (!xerr) {
173 			std::cerr << "unknown XML error during xmlSaveFormatFileEnc()." << std::endl;
174 		} else {
175 			std::cerr << "xmlSaveFormatFileEnc: error"
176 				<< " domain: " << xerr->domain
177 				<< " code: " << xerr->code
178 				<< " msg: " << xerr->message
179 				<< std::endl;
180 		}
181 	}
182 #endif
183 	xmlFreeDoc(doc);
184 
185 	if (result == -1) {
186 		return false;
187 	}
188 
189 	return true;
190 }
191 
192 void
debug(FILE * out) const193 XMLTree::debug(FILE* out) const
194 {
195 #ifdef LIBXML_DEBUG_ENABLED
196 	xmlDocPtr doc;
197 	XMLNodeList children;
198 
199 	xmlKeepBlanksDefault(0);
200 	doc = xmlNewDoc(xml_version);
201 	xmlSetDocCompressMode(doc, _compression);
202 	writenode(doc, _root, doc->children, 1);
203 	xmlDebugDumpDocument (out, doc);
204 	xmlFreeDoc(doc);
205 #endif
206 }
207 
208 const string&
write_buffer() const209 XMLTree::write_buffer() const
210 {
211 	static string retval;
212 	char* ptr;
213 	int len;
214 	xmlDocPtr doc;
215 	XMLNodeList children;
216 
217 	xmlKeepBlanksDefault(0);
218 	doc = xmlNewDoc(xml_version);
219 	xmlSetDocCompressMode(doc, _compression);
220 	writenode(doc, _root, doc->children, 1);
221 	xmlDocDumpMemory(doc, (xmlChar **) & ptr, &len);
222 	xmlFreeDoc(doc);
223 
224 	retval = ptr;
225 
226 	free(ptr);
227 
228 	return retval;
229 }
230 
231 static const int PROPERTY_RESERVE_COUNT = 16;
232 
XMLNode(const string & n)233 XMLNode::XMLNode(const string& n)
234 	: _name(n)
235 	, _is_content(false)
236 {
237 	_proplist.reserve (PROPERTY_RESERVE_COUNT);
238 }
239 
XMLNode(const string & n,const string & c)240 XMLNode::XMLNode(const string& n, const string& c)
241 	: _name(n)
242 	, _is_content(true)
243 	, _content(c)
244 {
245 	_proplist.reserve (PROPERTY_RESERVE_COUNT);
246 }
247 
XMLNode(const XMLNode & from)248 XMLNode::XMLNode(const XMLNode& from)
249 {
250 	_proplist.reserve (PROPERTY_RESERVE_COUNT);
251 	*this = from;
252 }
253 
~XMLNode()254 XMLNode::~XMLNode()
255 {
256 	clear_lists ();
257 }
258 
259 void
clear_lists()260 XMLNode::clear_lists ()
261 {
262 	XMLNodeIterator curchild;
263 	XMLPropertyIterator curprop;
264 
265 	_selected_children.clear ();
266 
267 	for (curchild = _children.begin(); curchild != _children.end(); ++curchild) {
268 		delete *curchild;
269 	}
270 
271 	_children.clear ();
272 
273 	for (curprop = _proplist.begin(); curprop != _proplist.end(); ++curprop) {
274 		delete *curprop;
275 	}
276 
277 	_proplist.clear ();
278 }
279 
280 XMLNode&
operator =(const XMLNode & from)281 XMLNode::operator= (const XMLNode& from)
282 {
283 	if (&from == this) {
284 		return *this;
285 	}
286 
287 	clear_lists ();
288 
289 	_name = from.name ();
290 	set_content (from.content ());
291 
292 	const XMLPropertyList& props = from.properties ();
293 
294 	for (XMLPropertyConstIterator prop_iter = props.begin (); prop_iter != props.end (); ++prop_iter) {
295 		set_property ((*prop_iter)->name ().c_str (), (*prop_iter)->value ());
296 	}
297 
298 	const XMLNodeList& nodes = from.children ();
299 	for (XMLNodeConstIterator child_iter = nodes.begin (); child_iter != nodes.end (); ++child_iter) {
300 		add_child_copy (**child_iter);
301 	}
302 
303 	return *this;
304 }
305 
306 bool
operator ==(const XMLNode & other) const307 XMLNode::operator== (const XMLNode& other) const
308 {
309 	if (is_content () != other.is_content ()) {
310 		return false;
311 	}
312 
313 	if (is_content ()) {
314 		if (content () != other.content ()) {
315 			return false;
316 		}
317 	} else {
318 		if (name () != other.name ()) {
319 			return false;
320 		}
321 	}
322 
323 	XMLPropertyList const& other_properties = other.properties ();
324 
325 	if (_proplist.size () != other_properties.size ()) {
326 		return false;
327 	}
328 
329 	XMLPropertyConstIterator our_prop_iter = _proplist.begin();
330 	XMLPropertyConstIterator other_prop_iter = other_properties.begin();
331 
332 	while (our_prop_iter != _proplist.end ()) {
333 		XMLProperty const* our_prop = *our_prop_iter;
334 		XMLProperty const* other_prop = *other_prop_iter;
335 		if (our_prop->name () != other_prop->name () || our_prop->value () != other_prop->value ()) {
336 			return false;
337 		}
338 		++our_prop_iter;
339 		++other_prop_iter;
340 	}
341 
342 	XMLNodeList const& other_children = other.children();
343 
344 	if (_children.size() != other_children.size()) {
345 		return false;
346 	}
347 
348 	XMLNodeConstIterator our_child_iter = _children.begin ();
349 	XMLNodeConstIterator other_child_iter = other_children.begin ();
350 
351 	while (our_child_iter != _children.end()) {
352 		XMLNode const* our_child = *our_child_iter;
353 		XMLNode const* other_child = *other_child_iter;
354 
355 		if (*our_child != *other_child) {
356 			return false;
357 		}
358 		++our_child_iter;
359 		++other_child_iter;
360 	}
361 	return true;
362 }
363 
364 bool
operator !=(const XMLNode & other) const365 XMLNode::operator!= (const XMLNode& other) const
366 {
367 	return !(*this == other);
368 }
369 
370 const string&
set_content(const string & c)371 XMLNode::set_content(const string& c)
372 {
373 	if (c.empty()) {
374 		_is_content = false;
375 	} else {
376 		_is_content = true;
377 	}
378 
379 	_content = c;
380 
381 	return _content;
382 }
383 
384 XMLNode*
child(const char * name) const385 XMLNode::child (const char* name) const
386 {
387 	/* returns first child matching name */
388 
389 	XMLNodeConstIterator cur;
390 
391 	if (name == 0) {
392 		return 0;
393 	}
394 
395 	for (cur = _children.begin(); cur != _children.end(); ++cur) {
396 		if ((*cur)->name() == name) {
397 			return *cur;
398 		}
399 	}
400 
401 	return 0;
402 }
403 
404 const XMLNodeList&
children(const string & n) const405 XMLNode::children(const string& n) const
406 {
407 	/* returns all children matching name */
408 
409 	XMLNodeConstIterator cur;
410 
411 	if (n.empty()) {
412 		return _children;
413 	}
414 
415 	_selected_children.clear();
416 
417 	for (cur = _children.begin(); cur != _children.end(); ++cur) {
418 		if ((*cur)->name() == n) {
419 			_selected_children.insert(_selected_children.end(), *cur);
420 		}
421 	}
422 
423 	return _selected_children;
424 }
425 
426 XMLNode*
add_child(const char * n)427 XMLNode::add_child(const char* n)
428 {
429 	return add_child_copy(XMLNode (n));
430 }
431 
432 void
add_child_nocopy(XMLNode & n)433 XMLNode::add_child_nocopy(XMLNode& n)
434 {
435 	_children.insert(_children.end(), &n);
436 }
437 
438 XMLNode*
add_child_copy(const XMLNode & n)439 XMLNode::add_child_copy(const XMLNode& n)
440 {
441 	XMLNode *copy = new XMLNode(n);
442 	_children.insert(_children.end(), copy);
443 	return copy;
444 }
445 
446 boost::shared_ptr<XMLSharedNodeList>
find(const string xpath,XMLNode * node) const447 XMLTree::find(const string xpath, XMLNode* node) const
448 {
449 	xmlXPathContext* ctxt;
450 	xmlDocPtr doc = 0;
451 
452 	if (node) {
453 		doc = xmlNewDoc(xml_version);
454 		writenode(doc, node, doc->children, 1);
455 		ctxt = xmlXPathNewContext(doc);
456 	} else {
457 		ctxt = xmlXPathNewContext(_doc);
458 	}
459 
460 	boost::shared_ptr<XMLSharedNodeList> result =
461 		boost::shared_ptr<XMLSharedNodeList>(find_impl(ctxt, xpath));
462 
463 	xmlXPathFreeContext(ctxt);
464 	if (doc) {
465 		xmlFreeDoc (doc);
466 	}
467 
468 	return result;
469 }
470 
471 std::string
attribute_value()472 XMLNode::attribute_value()
473 {
474 	XMLNodeList children = this->children();
475 	if (_is_content)
476 		throw XMLException("XMLNode: attribute_value failed (is_content) for requested node: " + name());
477 
478 	if (children.size() != 1)
479 		throw XMLException("XMLNode: attribute_value failed (children.size != 1) for requested node: " + name());
480 
481 	XMLNode* child = *(children.begin());
482 	if (!child->is_content())
483 		throw XMLException("XMLNode: attribute_value failed (!child->is_content()) for requested node: " + name());
484 
485 	return child->content();
486 }
487 
488 XMLNode*
add_content(const string & c)489 XMLNode::add_content(const string& c)
490 {
491 	if (c.empty ()) {
492 		/* this would add a "</>" child, leading to invalid XML.
493 		 * Also in XML, empty string content is equivalent to no content.
494 		 */
495 		return NULL;
496 	}
497 	return add_child_copy(XMLNode (string(), c));
498 }
499 
500 XMLProperty const *
property(const char * name) const501 XMLNode::property(const char* name) const
502 {
503 	XMLPropertyConstIterator iter = _proplist.begin();
504 
505 	while (iter != _proplist.end()) {
506 		if ((*iter)->name() == name) {
507 			return *iter;
508 		}
509 		++iter;
510 	}
511 
512 	return 0;
513 }
514 
515 XMLProperty const *
property(const string & name) const516 XMLNode::property(const string& name) const
517 {
518 	XMLPropertyConstIterator iter = _proplist.begin();
519 
520 	while (iter != _proplist.end()) {
521 		if ((*iter)->name() == name) {
522 			return *iter;
523 		}
524 		++iter;
525 	}
526 	return 0;
527 }
528 
529 XMLProperty *
property(const char * name)530 XMLNode::property(const char* name)
531 {
532 	XMLPropertyIterator iter = _proplist.begin();
533 
534 	while (iter != _proplist.end()) {
535 		if ((*iter)->name() == name) {
536 			return *iter;
537 		}
538 		++iter;
539 	}
540 	return 0;
541 }
542 
543 XMLProperty *
property(const string & name)544 XMLNode::property(const string& name)
545 {
546 	XMLPropertyIterator iter = _proplist.begin();
547 
548 	while (iter != _proplist.end()) {
549 		if ((*iter)->name() == name) {
550 			return *iter;
551 		}
552 		++iter;
553 	}
554 
555 	return 0;
556 }
557 
558 bool
has_property_with_value(const string & name,const string & value) const559 XMLNode::has_property_with_value (const string& name, const string& value) const
560 {
561 	XMLPropertyConstIterator iter = _proplist.begin();
562 
563 	while (iter != _proplist.end()) {
564 		if ((*iter)->name() == name && (*iter)->value() == value) {
565 			return true;
566 		}
567 		++iter;
568 	}
569 	return false;
570 }
571 
572 bool
set_property(const char * name,const string & value)573 XMLNode::set_property(const char* name, const string& value)
574 {
575 	XMLPropertyIterator iter = _proplist.begin();
576 
577 	while (iter != _proplist.end()) {
578 		if ((*iter)->name() == name) {
579 			(*iter)->set_value (value);
580 			return *iter;
581 		}
582 		++iter;
583 	}
584 
585 	XMLProperty* new_property = new XMLProperty(name, value);
586 
587 	if (!new_property) {
588 		return 0;
589 	}
590 
591 	_proplist.insert(_proplist.end(), new_property);
592 
593 	return new_property;
594 }
595 
596 bool
get_property(const char * name,std::string & value) const597 XMLNode::get_property(const char* name, std::string& value) const
598 {
599 	XMLProperty const* const prop = property (name);
600 	if (!prop)
601 		return false;
602 
603 	value = prop->value ();
604 
605 	return true;
606 }
607 
608 void
remove_property(const string & name)609 XMLNode::remove_property(const string& name)
610 {
611 	XMLPropertyIterator iter = _proplist.begin();
612 
613 	while (iter != _proplist.end()) {
614 		if ((*iter)->name() == name) {
615 			XMLProperty* property = *iter;
616 			_proplist.erase (iter);
617 			delete property;
618 			break;
619 		}
620 		++iter;
621 	}
622 }
623 
624 /** Remove any property with the given name from this node and its children */
625 void
remove_property_recursively(const string & n)626 XMLNode::remove_property_recursively(const string& n)
627 {
628 	remove_property (n);
629 	for (XMLNodeIterator i = _children.begin(); i != _children.end(); ++i) {
630 		(*i)->remove_property_recursively (n);
631 	}
632 }
633 
634 void
remove_nodes(const string & n)635 XMLNode::remove_nodes(const string& n)
636 {
637 	XMLNodeIterator i = _children.begin();
638 	while (i != _children.end()) {
639 		if ((*i)->name() == n) {
640 			i = _children.erase (i);
641 		} else {
642 			++i;
643 		}
644 	}
645 }
646 
647 void
remove_nodes_and_delete(const string & n)648 XMLNode::remove_nodes_and_delete(const string& n)
649 {
650 	XMLNodeIterator i = _children.begin();
651 
652 	while (i != _children.end()) {
653 		if ((*i)->name() == n) {
654 			delete *i;
655 			i = _children.erase (i);
656 		} else {
657 			++i;
658 		}
659 	}
660 }
661 
662 void
remove_nodes_and_delete(const string & propname,const string & val)663 XMLNode::remove_nodes_and_delete(const string& propname, const string& val)
664 {
665 	XMLNodeIterator i = _children.begin();
666 	XMLProperty const * prop;
667 
668 	while (i != _children.end()) {
669 		prop = (*i)->property(propname);
670 		if (prop && prop->value() == val) {
671 			delete *i;
672 			i = _children.erase(i);
673 		} else {
674 			++i;
675 		}
676 	}
677 }
678 
679 void
remove_node_and_delete(const string & n,const string & propname,const string & val)680 XMLNode::remove_node_and_delete(const string& n, const string& propname, const string& val)
681 {
682 	for (XMLNodeIterator i = _children.begin(); i != _children.end(); ++i) {
683 		if ((*i)->name() == n) {
684 			XMLProperty const * prop = (*i)->property (propname);
685 			if (prop && prop->value() == val) {
686 				delete *i;
687 				_children.erase (i);
688 				break;
689 			}
690 		}
691 	}
692 }
693 
XMLProperty(const string & n,const string & v)694 XMLProperty::XMLProperty(const string& n, const string& v)
695 	: _name(n)
696 	, _value(v)
697 {
698 }
699 
~XMLProperty()700 XMLProperty::~XMLProperty()
701 {
702 }
703 
704 static XMLNode*
readnode(xmlNodePtr node)705 readnode(xmlNodePtr node)
706 {
707 	string name, content;
708 	xmlNodePtr child;
709 	XMLNode* tmp;
710 	xmlAttrPtr attr;
711 
712 	if (node->name) {
713 		name = (const char*)node->name;
714 	}
715 
716 	tmp = new XMLNode(name);
717 
718 	for (attr = node->properties; attr; attr = attr->next) {
719 		content = "";
720 		if (attr->children) {
721 			content = (char*)attr->children->content;
722 		}
723 		tmp->set_property((const char*)attr->name, content);
724 	}
725 
726 	if (node->content) {
727 		tmp->set_content((char*)node->content);
728 	} else {
729 		tmp->set_content(string());
730 	}
731 
732 	for (child = node->children; child; child = child->next) {
733 		tmp->add_child_nocopy (*readnode(child));
734 	}
735 
736 	return tmp;
737 }
738 
739 static void
writenode(xmlDocPtr doc,XMLNode * n,xmlNodePtr p,int root=0)740 writenode(xmlDocPtr doc, XMLNode* n, xmlNodePtr p, int root = 0)
741 {
742 	xmlNodePtr node;
743 
744 	if (root) {
745 		node = doc->children = xmlNewDocNode(doc, 0, (const xmlChar*) n->name().c_str(), 0);
746 	} else {
747 		node = xmlNewChild(p, 0, (const xmlChar*) n->name().c_str(), 0);
748 	}
749 
750 	if (n->is_content()) {
751 		node->type = XML_TEXT_NODE;
752 		xmlNodeSetContentLen(node, (const xmlChar*)n->content().c_str(), n->content().length());
753 	}
754 
755 	const XMLPropertyList& props = n->properties();
756 
757 	for (XMLPropertyConstIterator prop_iter = props.begin (); prop_iter != props.end ();
758 	     ++prop_iter) {
759 		xmlSetProp (node, (const xmlChar*)(*prop_iter)->name ().c_str (),
760 		            (const xmlChar*)(*prop_iter)->value ().c_str ());
761 	}
762 
763 	const XMLNodeList& children = n->children ();
764 	for (XMLNodeConstIterator child_iter = children.begin (); child_iter != children.end ();
765 	     ++child_iter) {
766 		writenode (doc, *child_iter, node);
767 	}
768 }
769 
find_impl(xmlXPathContext * ctxt,const string & xpath)770 static XMLSharedNodeList* find_impl(xmlXPathContext* ctxt, const string& xpath)
771 {
772 	xmlXPathObject* result = xmlXPathEval((const xmlChar*)xpath.c_str(), ctxt);
773 
774 	if (!result) {
775 		xmlFreeDoc(ctxt->doc);
776 		xmlXPathFreeContext(ctxt);
777 
778 		throw XMLException("Invalid XPath: " + xpath);
779 	}
780 
781 	if (result->type != XPATH_NODESET) {
782 		xmlXPathFreeObject(result);
783 		xmlFreeDoc(ctxt->doc);
784 		xmlXPathFreeContext(ctxt);
785 
786 		throw XMLException("Only nodeset result types are supported.");
787 	}
788 
789 	xmlNodeSet* nodeset = result->nodesetval;
790 	XMLSharedNodeList* nodes = new XMLSharedNodeList();
791 	if (nodeset) {
792 		for (int i = 0; i < nodeset->nodeNr; ++i) {
793 			XMLNode* node = readnode(nodeset->nodeTab[i]);
794 			nodes->push_back(boost::shared_ptr<XMLNode>(node));
795 		}
796 	} else {
797 		// return empty set
798 	}
799 
800 	xmlXPathFreeObject(result);
801 
802 	return nodes;
803 }
804 
805 /** Dump a node, its properties and children to a stream */
806 void
dump(ostream & s,string p) const807 XMLNode::dump (ostream& s, string p) const
808 {
809 	if (_is_content) {
810 		s << p << "  " << content() << "\n";
811 	} else {
812 		s << p << "<" << _name;
813 		for (XMLPropertyList::const_iterator i = _proplist.begin(); i != _proplist.end(); ++i) {
814 			s << " " << (*i)->name() << "=\"" << (*i)->value() << "\"";
815 		}
816 		s << ">\n";
817 
818 		for (XMLNodeList::const_iterator i = _children.begin(); i != _children.end(); ++i) {
819 			(*i)->dump (s, p + "  ");
820 		}
821 
822 		s << p << "</" << _name << ">\n";
823 	}
824 }
825