#-----------------------------------------------------------------------------
#
#	$Id : XPath.pm 2.215 2006-06-10 JMG$
#
#	Initial developer: Jean-Marie Gouarne
#	Copyright 2006 by Genicorp, S.A. (www.genicorp.com)
#	License:
#		- Licence Publique Generale Genicorp v1.0
#		- GNU Lesser General Public License v2.1
#
#-----------------------------------------------------------------------------

package	OpenOffice::OODoc::XPath;
use	5.008_000;
our	$VERSION	= 2.216;
use	XML::Twig	3.22;
use	Encode;

#------------------------------------------------------------------------------

our %XMLNAMES	=			# OODoc root element names
	(
	'content'	=> 'office:document-content',
	'styles'	=> 'office:document-styles',
	'meta'		=> 'office:document-meta',
	'manifest'	=> 'manifest:manifest',
	'settings'	=> 'office:document-settings'
	);

					# characters to be escaped in XML
our	$CHARS_TO_ESCAPE	= "\"<>'&";
					# standard external character set
our	$LOCAL_CHARSET		= 'iso-8859-1';
					# OpenOffice.org character set
our	$OO_CHARSET		= 'utf8';

#------------------------------------------------------------------------------
# basic conversion between internal & printable encodings

sub	OpenOffice::OODoc::XPath::decode_text
	{
	return Encode::encode($LOCAL_CHARSET, shift);
	}

sub	OpenOffice::OODoc::XPath::encode_text
	{
	return Encode::decode($LOCAL_CHARSET, shift);
	}

#------------------------------------------------------------------------------
# object coordinates, size, description control

sub	setObjectCoordinates
	{
	my $self	= shift;
	my $element	= shift	or return undef;
	my ($x, $y)	= @_;
	if ($x && ($x =~ /,/))	# X and Y are concatenated in a single string
		{
		$x =~ s/\s*//g;			# remove the spaces
		$x =~ s/,(.*)//; $y = $1;	# split on the comma
		}
	$x = '0cm' unless $x; $y = '0cm' unless $y;
	$x .= 'cm' unless $x =~ /[a-zA-Z]$/;
	$y .= 'cm' unless $y =~ /[a-zA-Z]$/;
	$self->setAttributes($element, 'svg:x' => $x, 'svg:y' => $y);
	return wantarray ? ($x, $y) : ($x . ',' . $y);
	}

sub	getObjectCoordinates
	{
	my $self	= shift;
	my $element	= shift	or return undef;
	my $x		= $element->getAttribute('svg:x');
	my $y		= $element->getAttribute('svg:y');
	return undef unless defined $x and defined $y;
	return wantarray ? ($x, $y) : ($x . ',' . $y);
	}
		
sub	setObjectSize
	{
	my $self	= shift;
	my $element	= shift	or return undef;
	my ($w, $h)	= @_;
	if ($w && ($w =~ /,/))	# W and H are concatenated in a single string
		{
		$w =~ s/\s*//g;			# remove the spaces
		$w =~ s/,(.*)//; $h = $1;	# split on the comma
		}
	$w = '0cm' unless $w; $h = '0cm' unless $h;
	$w .= 'cm' unless $w =~ /[a-zA-Z]$/;
	$h .= 'cm' unless $h =~ /[a-zA-Z]$/;
	$self->setAttributes($element, 'svg:width' => $w, 'svg:height' => $h);
	return wantarray ? ($w, $h) : ($w . ',' . $h);
	}
	
sub	getObjectSize
	{
	my $self	= shift;
	my $element	= shift	or return undef;
	my $w		= $element->getAttribute('svg:width');
	my $h		= $element->getAttribute('svg:height');
	return wantarray ? ($w, $h) : ($w . ',' . $h);
	}
	
sub	setObjectDescription
	{
	my $self	= shift;
	my $element	= shift or return undef;
	my $text	= shift;
	my $desc	= $element->first_child('svg:desc');
	unless ($desc)
		{
		$self->appendElement($element, 'svg:desc', text => $text)
			if (defined $text);
		}
	else
		{
		if (defined $text)	{ $self->setText($desc, $text, @_);	}
		else			{ $self->removeElement($desc, @_);	}
		}
	return $desc;
	}
	
sub	getObjectDescription
	{
	my $self	= shift;
	my $element	= shift or return undef;
	return $self->getXPathValue($element, 'svg:desc');
	}

sub	objectName
	{
	my $self	= shift;
	my $element	= shift or return undef;
	my $name	= shift;
	return (defined $name) ?
		$self->setAttribute($element, 'draw:name' => $name)	:
		$self->getAttribute($element, 'draw:name'); 
	}
	
#------------------------------------------------------------------------------
# basic element creation

sub	OpenOffice::OODoc::XPath::new_element
	{
	my $name	= shift		or return undef;
	return undef if ref $name;

	$name		=~ s/^\s+//;
	$name		=~ s/\s+$//;
	
	if ($name =~ /^<.*>$/)	# create element from XML string
		{
		return OpenOffice::OODoc::Element->parse($name, @_);
		}
	else			# create element from name and optional data
		{
		return OpenOffice::OODoc::Element->new($name, @_);
		}
	}

#------------------------------------------------------------------------------
# text node creation

sub	OpenOffice::OODoc::XPath::new_text_node
	{
	return OpenOffice::OODoc::XPath::new_element('#PCDATA');
	}

#------------------------------------------------------------------------------
# basic conversion between internal & printable encodings (object version)

sub	inputTextConversion
	{
	my $self	= shift;
	my $text	= shift;
	my $local_encoding = $self->{'local_encoding'} or return $text;
	return Encode::decode($local_encoding, $text);
	}

sub	outputTextConversion
	{
	my $self	= shift;
	my $text	= shift;
	my $local_encoding = $self->{'local_encoding'} or return $text;
	return Encode::encode($local_encoding, $text);
	}

sub	localEncoding
	{
	my $self	= shift;
	my $encoding	= shift;
	$self->{'local_encoding'} = $encoding if $encoding;
	return $self->{'local_encoding'} || '';
	}

sub	noLocalEncoding
	{
	my $self	= shift;
	delete $self->{'local_encoding'};
	return 1;
	}

#------------------------------------------------------------------------------
# search/replace text processing routine
# if $replace is a user-provided routine, it's called back with
# the current argument stack, plus the substring found

sub	_find_text
	{
	my $self	= shift;
	my $text	= shift;
	my $pattern	= $self->inputTextConversion(shift);
	my $replace	= shift;

	if (defined $pattern)
	    {
	    if (defined $replace)
		{
		if (ref $replace)
		    {
		    if ((ref $replace) eq 'CODE')
		    	{
			return undef
			  unless
			    (
			    $text =~
			    	s/($pattern)/
				    	{
					my $found = $1;
					Encode::_utf8_on($found)
						if Encode::is_utf8($text);
					my $result = &$replace(@_, $found);
					$result = $found
						unless (defined $result);
					$result;
					}
				/eg
			    );
			}
		    else
		    	{
			return undef unless ($text =~ /$pattern/);
			}
		    }
		else
		    {
		    my $r = $self->inputTextConversion($replace);
		    return undef unless ($text =~ s/$pattern/$r/g);
		    }
		}
	    else
		{
		return undef unless ($text =~ /$pattern/);
		}
	    }
	return $text;
	}

#------------------------------------------------------------------------------
# search/replace content in descendant nodes

sub	_search_content
	{
	my $self	= shift;
	my $element	= shift or return undef;
	my $content	= undef;
	
	foreach my $child ($element->children)
		{
		my $text = undef;
		if ($child->isTextNode)
			{
			$text = $self->_find_text($child->text, @_);
			$child->set_text($text) if defined $text;
			}
		else
			{
			my $t = $self->_search_content($child, @_);
			$text .= $t if defined $t;
			}
		$content .= $text if defined $text;
		}
	return $content;
	}

#------------------------------------------------------------------------------
# is this an OASIS Open Document or an OpenOffice 1.x Document ?

sub	isOpenDocument
	{
	my $self	= shift;
	my $root	= $self->getRootElement;
	my $ns		= $root->att('xmlns:office');
	return $ns && ($ns =~ /opendocument/) ? 1 : undef;
	}

#------------------------------------------------------------------------------
# document class check
	
sub	isContent
	{
	my $self	= shift;
	return ($self->contentClass()) ? 1 : undef;
	}

sub	isCalcDocument
	{
	my $self	= shift;
	return ($self->contentClass() eq 'spreadsheet') ? 1 : undef;
	}
sub	isImpressDocument
	{
	my $self	= shift;
	return ($self->contentClass() eq 'impress') ? 1 : undef;
	}
sub	isDrawDocument
	{
	my $self	= shift;
	return ($self->contentClass() eq 'drawing') ? 1 : undef;
	}
sub	isWriterDocument
	{
	my $self	= shift;
	return ($self->contentClass() eq 'text') ? 1 : undef;
	}

#------------------------------------------------------------------------------
# constructor; accepts one from 3 types of parameters to create an instance:
#	file	=> a regular OpenOffice.org filename
#	archive	=> an OODoc::File, previously created object
#	xml	=> an XML string, representing an OO XML member
# if 'file' or 'archive' (not 'xml') is provided, another parameter 'member'
# must be provided in addition
# 	member	=> member of the zip archive (meta.xml, content.xml, ...)

sub	new
	{
	my $caller	= shift;
	my $class	= ref($caller) || $caller;
	my $self	=
		{
		auto_style_path		=> '//office:automatic-styles',
		master_style_path	=> '//office:master-styles',
		named_style_path	=> '//office:styles',
		image_container		=> 'draw:image',
		image_xpath		=> '//draw:image',
		image_fpath		=> '#Pictures/',
		local_encoding		=>
				$OpenOffice::OODoc::XPath::LOCAL_CHARSET,
		@_
		};

	my $twig = undef;

	if ($self->{'member'} && ! $self->{'element'})
		{
		my $m	= lc $self->{'member'};
		$m	=~ /(^.*)\..*/;
		$m	= $1	if $1;
		$self->{'element'} =
		    $OpenOffice::OODoc::XPath::XMLNAMES{$m};
		}
					# create the XML::Twig
	if 	(
		$self->{'readable_XML'} &&
		    (
		    	($self->{'readable_XML'} eq '1')
				||
			($self->{'readable_XML'} eq 'true')
				||
			($self->{'readable_XML'} eq 'on')
		    )
		)
			{
			$self->{'readable_XML'} = 'indented';
			}
	$self->{'element'} = $OpenOffice::OODoc::XPath::XMLNAMES{'content'}
				unless $self->{'element'};
	if ($self->{'element'})
		{
		$twig = XML::Twig->new
			(
			elt_class	=> "OpenOffice::OODoc::Element",
			twig_roots	=>
				{
				$self->{'element'}	=> 1
				},
			pretty_print	=> $self->{'readable_XML'},
			%{$self->{'twig_options'}}
			);
		}
	else
		{
		$twig = XML::Twig->new
			(
			elt_class	=> "OpenOffice::OODoc::Element",
			pretty_print	=> $self->{'readable_XML'},
			%{$self->{'twig_options'}}
			);
		}

	unless ($self->{'archive'})
		{
		if (ref $self->{'file'})
			{
		 	my $obj = $self->{'file'};
			if	($obj->isa("OpenOffice::OODoc::File"))
				{
				$self->{'archive'} = $obj;
				}
			elsif	($obj->isa("OpenOffice::OODoc::XPath"))
				{
				$self->{'archive'} = $obj->{'archive'};
				}
			else
				{
				warn "[" . __PACKAGE__ . "::new] " .
					"Invalid file object\n";
				return undef;
				}
			delete $self->{'file'};
			}
		}
		
	if ($self->{'xml'})		# load from XML string
		{
		$self->{'xpath'} = $twig->safe_parse($self->{'xml'});
		}
	elsif ($self->{'archive'})	# load from existing OOFile
		{
		unless ($self->{'archive'}->isa("OpenOffice::OODoc::File"))
			{
			warn "[" . __PACKAGE__ . "] Invalid archive\n";
			return undef;
			}
		$self->{'member'} = 'content' unless $self->{'member'};
		$self->{'xml'} = $self->{'archive'}->link($self);
		$self->{'xpath'} = $twig->safe_parse($self->{'xml'});
		}
	elsif ($self->{'file'})		# look for a file
		{
		unless	(
			$self->{'flat_xml'}
					||
			(lc $self->{'file'}) =~ /\.xml$/
			)
			{		# create OOFile & extract from it
			require OpenOffice::OODoc::File;

			$self->{'archive'} = OpenOffice::OODoc::File->new
				(
				$self->{'file'},
				create		=> $self->{'create'},
				opendocument	=> $self->{'opendocument'},
				template_path	=> $self->{'template_path'}
				);
			$self->{'member'} = 'content'
					unless $self->{'member'};
			$self->{'xml'} = $self->{'archive'}->link($self);
			$self->{'xpath'} = $twig->safe_parse($self->{'xml'});
			}
		else
			{		# load from XML flat file
			$self->{'xpath'} =
				$twig->safe_parsefile($self->{'file'});		
			}
		}
	else
		{
		warn "[" . __PACKAGE__ . "::new] No XML content\n";
		return undef;
		}
	delete $self->{'xml'};
	unless ($self->{'xpath'})
		{
		warn "[" . __PACKAGE__ . "::new] No well formed content\n";
		return undef;
		}
	$self->{'twig'} = $twig;
	$self->{'context'} = $self->{'xpath'};
	bless $self, $class;
	$self->{'opendocument'} = $self->isOpenDocument;
	if ($self->{'opendocument'})
		{
		$self->{'image_container'}	= 'draw:frame';
		$self->{'image_xpath'}		= '//draw:frame';
		$self->{'image_fpath'}		= 'Pictures/';
		}
	$self->{'body'} = $self->getBody;
	return $self;
	}

#------------------------------------------------------------------------------
# destructor

sub	DESTROY
	{
	my $self	= shift;
	delete $self->{'file'};
	delete $self->{'context'};
	delete $self->{'xpath'};
	delete $self->{'xml'};
	delete $self->{'body'};
	delete $self->{'content_class'};
	$self->{'twig'}->dispose	if $self->{'twig'};
	delete $self->{'twig'};
	delete $self->{'archive'};
	delete $self->{'twig_options'};
	$self = {};
	}

sub	dispose
	{
	my $self	= shift;
	$self->DESTROY(@_);
	}

#------------------------------------------------------------------------------
# get a reference to the embedded XML parser for share

sub	getXMLParser
	{
	warn	"[" . __PACKAGE__ . "::getXMLParser] No longer implemented\n";
	return undef;
	}

#------------------------------------------------------------------------------
# make the changes persistent in an OpenOffice.org file 

sub	save
	{
	my $self	= shift;
	my $target	= shift;

	my $filename	= ($target) ? $target : $self->{'file'};
	my $archive	= $self->{'archive'};
	unless ($archive)
		{
		my $ro = $self->{'read_only'};
		return undef if $ro &&
			(($ro eq '1') || ($ro eq 'on') || ($ro eq 'true'));
		if ($filename)
			{
			open my $fh, ">:utf8", $filename;
			$self->exportXMLContent($fh);
			close $fh;
			return $filename;
			}
		else
			{
			warn "[" . __PACKAGE__ . "::save] No archive object\n";
			return undef;
			}
		}
	$filename	= $archive->{'source_file'}	unless $filename;
	unless ($filename)
		{
		warn "[" . __PACKAGE__ . "::save] No target file\n";
		return undef;
		}
	my $member	= $self->{'member'};
	unless ($member)
		{
		warn "[" . __PACKAGE__ . "::save] No member\n";
		return undef;
		}
	
	my $result = $archive->save($filename);

	return $result;
	}

sub	update
	{
	my $self	= shift;
	return $self->save(@_);
	}

#------------------------------------------------------------------------------
# raw file import

sub	raw_import
	{
	my $self	= shift;
	if ($self->{'archive'})
		{
		my $target	= shift;
		unless ($target)
			{
			warn	"[" . __PACKAGE__ . "::raw_import] "	.
				"No target member for import\n";
			return undef;
			}
		$target =~ s/^#//;
		return $self->{'archive'}->raw_import($target, @_);
		}
	else
		{
		warn	"[" . __PACKAGE__ . "::raw_import] "	.
			"No archive for file import\n";
		return undef;
		}
	}

#------------------------------------------------------------------------------
# raw file export

sub	raw_export
	{
	my $self	= shift;
	if ($self->{'archive'})
		{
		my $source	= shift;
		unless ($source)
			{
			warn	"[" . __PACKAGE__ . "::raw_import] "	.
				"Missing source file name\n";
			return undef;
			}
		$source =~ s/^#//;
		return $self->{'archive'}->raw_export($source, @_);
		}
	else
		{
		warn	"[" . __PACKAGE__ . "::raw_import] "	.
			"No archive for file export\n";
		return undef;
		}
	}

#------------------------------------------------------------------------------
# exports the whole content of the document as an XML string

sub	exportXMLContent
	{
	my $self	= shift;
	my $target	= shift;
	if ($target)
		{
		return $self->{'twig'}->print($target);
		}
	else
		{
		return $self->{'twig'}->sprint;
		}
	}

sub	getXMLContent
	{
	my $self	= shift;
	return $self->exportXMLContent(@_);
	}

sub	getContent
	{
	my $self	= shift;
	return $self->exportXMLContent(@_);
	}

#------------------------------------------------------------------------------
# brute force tree reorganization

sub	reorganize
	{
	warn "[" . __PACKAGE__ . "::reorganize] No longer implemented\n";
	return undef;
	}

#------------------------------------------------------------------------------
# returns the root of the XML document

sub	getRoot
	{
	my $self	= shift;
	return $self->{'xpath'}->root;
	}

#------------------------------------------------------------------------------
# returns the root element of the XML document

sub	getRootElement
	{
	my $self	= shift;

	my $root	= $self->{'xpath'}->root;
	my $rootname	= $root->name() || '';
	return ($rootname eq $self->{'element'})	?
			$root				:
			$root->first_child($self->{'element'});
	}
		
#------------------------------------------------------------------------------
# get/set/reset the current search context

sub	currentContext
	{
	my $self	= shift;
	my $new_context	= shift;
	$self->{'context'} = $new_context if (ref $new_context);
	return $self->{'context'};
	}

sub	resetCurrentContext
	{
	$self		= shift;
	return $self->currentContext($self->{'xpath'});
	}

#------------------------------------------------------------------------------
# returns the content class (text, spreadsheet, presentation, drawing)

sub	contentClass
	{
	my $self	= shift;

	my $content_class	=
		$self->getRootElement->getAttribute('office:class');
	return $content_class if $content_class;

	my $body = $self->getBody	or return undef;
	my $name = $body->name		or return undef;
	$name =~ /(.*):(.*)/;
	return $2;
	}

#------------------------------------------------------------------------------
# element name check

sub	getRootName
	{
	my $self	= shift;
	return $self->getRootElement->name;
	}

#------------------------------------------------------------------------------
# member type checks

sub	isMeta
	{
	my $self	= shift;
	return ($self->getRootName() eq $XMLNAMES{'meta'}) ? 1 : undef;
	}
	
sub	isStyles
	{
	my $self	= shift;
	return ($self->getRootName() eq $XMLNAMES{'styles'}) ? 1 : undef;
	}
	
sub	isSettings
	{
	my $self	= shift;
	return ($self->getRootName() eq $XMLNAMES{'settings'}) ? 1 : undef;
	}

#------------------------------------------------------------------------------
# returns the document body element (if defined)

sub	getBody
	{
	my $self	= shift;

	return $self->{'body'} if ref $self->{'body'};

	if ($self->{'body_path'})
		{
		$self->{'body'} =$self->getElement($self->{'body_path'}, 0);
		return $self->{'body'};
		}
	my $office_body = $self->getElement('//office:body', 0);
	if ($office_body)
		{
		$self->{'body'} = $self->{'opendocument'} ?
		    $office_body->selectChildElement
			('office:(text|spreadsheet|presentation|drawing)')
			:
		    $office_body;
		}
	else
		{
		$self->{'body'} = $self->getRootElement->selectChildElement
				(
				'office:(body|meta|master-styles|settings)'
				);
		}
	return $self->{'body'};
	}

#------------------------------------------------------------------------------
# makes the current OODoc::XPath object share the same content as another one

sub	cloneContent
	{
	$self	= shift;
	$source	= shift;

	unless ($source && $source->{'xpath'})
		{
		warn "[" . __PACKAGE__ . "]::cloneContent - No valid source\n";
		return undef;
		}
		
	$self->{'xpath'}	= $source->{'xpath'};
	$self->{'begin'}	= $source->{'begin'};
	$self->{'xml'}		= $source->{'xml'};
	$self->{'end'}		= $source->{'end'};

	return $self->getRoot;
	}

#------------------------------------------------------------------------------
# exports an individual element as an XML string

sub	exportXMLElement
	{
	my $self	= shift;
	my $path	= shift;
	my $element	=
		(ref $path) ? $path : $self->getElement($path, shift);
	return $element->sprint(@_);
	}

#------------------------------------------------------------------------------
# exports the document body (if defined) as an XML string

sub	exportXMLBody
	{
	my $self	= shift;

	return	$self->exportXMLElement($self->getBody, @_);
	}

#------------------------------------------------------------------------------
# gets the reference of an XML element identified by path & position
# for subsequent processing

sub	getElement
	{
	my $self	= shift;
	my $path	= shift;
	return undef	unless $path;
	if (ref $path)
		{
		return	$path->isElementNode ? $path : undef;
		}
	my $pos		= shift || 0;
	my $context	= shift || $self->{'context'};
	if (defined $pos && (($pos =~ /^\d*$/) || ($pos =~ /^[\d+-]\d+$/)))
		{
		$path =~ s/^\/\//\.\// unless $context eq $self->{'xpath'};
		my $node	= $context->get_xpath($path, $pos);

		return	$node && $node->isElementNode ? $node : undef;
		}
	else
		{
		warn	"[" . __PACKAGE__ . "::getElement] "	.
			"Invalid node position\n";
		return undef;
		}
	}

#------------------------------------------------------------------------------
# get the list of children (or the first child unless wantarray) matching
# a given element name and belonging to a given element

sub	selectChildElementsByName
	{
	my $self	= shift;
	my $path	= shift;
	my $element	= ref $path ? $path : $self->getElement($path, shift);
	return undef	unless $element;

	return $element->selectChildElements(@_);
	}

#------------------------------------------------------------------------------
# get the first child belonging to a given element and matching a given name

sub	selectChildElementByName
	{
	my $self	= shift;
	my $path	= shift;
	my $element	= ref $path ? $path : $self->getElement($path, shift);
	return undef			unless $element;
	return $element->selectChildElement(@_);
	}

sub	getChildElementByName
	{
	my $self	= shift;
	return $self->selectChildElementByName(@_);
	}

#-----------------------------------------------------------------------------
# some usual text field constructors

sub	create_field
	{
	my $self	= shift;
	my $tag		= shift;
	my %opt		= @_;
	my $prefix	= $opt{'-prefix'};
	delete $opt{'-prefix'};
	
	if ($prefix)
		{
		$tag = "$prefix:$tag" unless $tag =~ /:/; 
		my %att = ();
		foreach my $k (keys %opt)
			{
			my $a = ($k =~ /:/) ? $k : "$prefix:$k";
			$att{$a} = $opt{$k};
			}
		%opt = %att;
		}
	my $element = OpenOffice::OODoc::Element->new($tag);
	$self->setAttributes($element, %opt);
	return $element;
	}

sub	spaces
	{
	my $self	= shift;
	my $length	= shift;
	return $self->create_field('text:s', 'text:c' => $length, @_);
	}

sub	blankSpaces
	{
	my $self	= shift;
	return $self->spaces(@_);
	}
	
sub	createSpaces
	{
	my $self	= shift;
	return $self->spaces(@_);
	}

sub	tabStop
	{
	my $self	= shift;
	my $tag = $self->{'opendocument'} ? 'text:tab' : 'text:tab-stop';
	return $self->create_field($tag, @_);
	}
	
sub	lineBreak
	{
	my $self	= shift;
	return $self->create_field('text:line-break', @_);
	}

#------------------------------------------------------------------------------

sub	appendLineBreak
	{
	my $self	= shift;
	my $element	= shift;
	
	return $element->appendChild('text:line-break');
	}

#------------------------------------------------------------------------------

sub	appendSpaces
	{
	my $self	= shift;
	my $element	= shift;
	my $length	= shift;
	
	my $spaces	= $self->createSpaces($length) or return undef;
	$spaces->paste_last_child($element);
	}
	
#------------------------------------------------------------------------------

sub	appendTabStop
	{
	my $self	= shift;
	my $element	= shift;
	
	my $tabtag = $self->{'opendocument'} ? 'text:tab' : 'text:tab-stop';
	
	return $element->appendChild($tabtag);
	}

#------------------------------------------------------------------------------

sub	createFrameElement
	{
	my $self	= shift;
	my %opt		= @_;
	my %attr	= ();
	
	$attr{'draw:name'} = $opt{'name'}; delete $opt{'name'};
		
	my $content_class = $self->contentClass;
		
	$attr{'draw:style-name'} = $opt{'style'}; delete $opt{'style'};
	if ($opt{'page'})
		{
		my $pg = $opt{'page'};
		if (ref $pg)
			{
			$opt{'attachment'} = $pg unless $opt{'attachment'};
			}
		elsif ($content_class eq 'text')
			{
			$opt{'attachment'} = $self->{'body'};
			$attr{'text:anchor-type'} = 'page';
			$attr{'text:anchor-page-number'} = $pg;
			}
		elsif 	(
				($content_class eq 'presentation')
					or
				($content_class eq 'drawing')
			)
			{
			$opt{'attachment'} = $self->getNodeByXPath
					("//draw:page[\@draw:name=\"$pg\"]");
			}
		}
	delete $opt{'page'};

	my $tag = $opt{'tag'} || 'draw:frame'; delete $opt{'tag'};
	
	my $frame = OpenOffice::OODoc::XPath::new_element($tag);

	if ($opt{'position'})
		{
		$self->setObjectCoordinates($frame, $opt{'position'});
		delete $opt{'position'};
		}
	if ($opt{'size'})
		{
		$self->setObjectSize($frame, $opt{'size'});
		delete $opt{'size'};
		}
	if ($opt{'description'})
		{
		$self->setObjectDescription($frame, $opt{'description'});
		delete $opt{'description'};
		}
	if ($opt{'attachment'})
		{
		$frame->paste_first_child($opt{'attachment'});
		delete $opt{'attachment'};
		}
		
	foreach my $k (keys %opt)
		{
		$attr{$k} = $opt{$k} if ($k =~ /:/);
		}
	$self->setAttributes($frame, %attr);
		
	return $frame;
	}
	
sub	createFrame
	{
	my $self	= shift;
	return $self->createFrameElement(@_);
	}
	
#-----------------------------------------------------------------------------
# select an individual frame element by name

sub	selectFrameElementByName
	{
	my $self	= shift;
	my $text	= shift;
	my $tag		= shift || 'draw:frame';
	return $self->selectNodeByXPath
			("//$tag\[\@draw:name=\"$text\"\]", @_);
	}

#-----------------------------------------------------------------------------
# gets frame element (name or ref, with type checking)

sub	getFrameElement
	{
	my $self	= shift;
	my $frame	= shift;
	return undef unless defined $frame;
	my $tag		= shift || 'draw:frame';
	
	my $element	= undef;
	if (ref $frame)
		{
		$element = $frame;
		}
	else
		{
		if ($frame =~ /^[\-0-9]*$/)
			{
			return $self->getElement("//$tag", $frame, @_);
			}
		else
			{
			return $self->selectFrameElementByName
				($frame, $tag, @_);
			}
		}	
	}
	
sub	getFrame
	{
	my $self	= shift;
	return $self->getFrameElement(@_);
	}

#------------------------------------------------------------------------------

sub	getFrameList
	{
	my $self	= shift;
	return $self->getDescendants('draw:frame', shift);
	}
	
#------------------------------------------------------------------------------

sub	frameStyle
	{
	my $self	= shift;
	my $frame	= $self->getFrameElement(shift) or return undef;
	my $style	= shift;
	my $attr	= 'draw:style-name';
	return (defined $style) ?
		$self->setAttribute($frame, $attr => shift)	:
		$self->getAttribute($frame, $attr);
	}
	
#------------------------------------------------------------------------------
# replaces any previous content of an existing element by a given text
# without processing other than encoding

sub	setFlatText
	{
	my $self	= shift;
	my $path	= shift;
	my $pos		= (ref $path) ? undef : shift;
	my $text	= shift;

	return undef	unless defined $text;
	my $element 	= $self->OpenOffice::OODoc::XPath::getElement
					($path, $pos)
				or return undef;
	my $t		= $self->inputTextConversion($text);
	$element->set_text($t)	if defined $t;
	return $text;
	}

#------------------------------------------------------------------------------
# replaces any previous content of an existing element by a given text
# processing tab stops and line breaks

sub	setText
	{
	my $self	= shift;
	my $path	= shift;
	my $pos		= (ref $path) ? undef : shift;
	my $text	= shift;

	return undef	unless defined $text;
	
	my $element 	= $self->OpenOffice::OODoc::XPath::getElement
					($path, $pos)
					or return undef;

	unless ($text)
		{
		$element->set_text($text); return $text;
		}
	my $tabtag = $self->{'opendocument'} ? 'text:tab' : 'text:tab-stop';
	$element->set_text("");
	my @lines	= split "\n", $text;
	while (@lines)
		{
		my $line	= shift @lines;
		my @columns	= split "\t", $line;
		while (@columns)
			{
			my $column	= 
				$self->inputTextConversion(shift @columns);
			$element->appendTextChild($column);
			$element->appendChild($tabtag) if (@columns);
			}
		$element->appendChild('text:line-break') if (@lines);
		}

	return $text;
	}

#------------------------------------------------------------------------------
# extends the text of an existing element

sub	extendText
	{
	my $self	= shift;
	my $path	= shift;
	my $pos		= (ref $path) ? undef : shift;
	my $text	= shift;

	return undef	unless defined $text;
	
	my $element 	= $self->getElement($path, $pos);
	return undef	unless $element;

	my $offset	= shift;
		
	if (ref $text)
		{
		if ($text->isElementNode)
			{
			unless (defined $offset)
				{
				$text->paste_last_child($element);
				}
			else
				{
				$text->paste_within($element, $offset);
				}
			}
		return $text;
		}

	my $tabtag = $self->{'opendocument'} ? 'text:tab' : 'text:tab-stop';
	my @lines	= split "\n", $text;
	my $ref_node	= undef;
	while (@lines)
		{
		my $line	= shift @lines;
		my @columns	= split "\t", $line;
		while (@columns)
			{
			my $column	=
				$self->inputTextConversion(shift @columns);
			unless ($ref_node)
				{
				$ref_node = $element->insertTextChild
						($column, $offset);
				$ref_node = $ref_node->insertNewNode
						($tabtag, 'after')			
					if (@columns);
				}
			else
				{
				my $tn = $self->createTextNode($column);
				$ref_node = $ref_node->insertNewNode
						($tn, 'after');
				$ref_node = $ref_node->insertNewNode
						($tabtag, 'after')			
					if (@columns);
				}
			}
		if (@lines)
			{
			if ($ref_node)
				{
				$ref_node->insertNewNode
						('text:line-break', 'after');
				}
			else
				{
			 	$element->insertNewNode
						(
						'text:line-break',
						'within',
						$offset
						);
				}
			}
		}

	return $text;
	}

#------------------------------------------------------------------------------
# converts the content of an element to flat text

sub	flatten
	{
	my $self	= shift;
	my $element	= shift || $self->{'context'};
	return $element->flatten;
	}

#------------------------------------------------------------------------------
# creates a new encoded text node

sub	createTextNode
	{
	my $self	= shift;
	my $text	= shift		or return undef;
	my $content	= $self->inputTextConversion($text);
	return OpenOffice::OODoc::Element->new('#PCDATA' => $text);
	}

#------------------------------------------------------------------------------
# replaces substring in an element 

sub	replaceText
	{
	my $self	= shift;
	my $path	= shift;
	my $element	= (ref $path) ?
				$path	:
				$self->getElement($path, shift);

	return $self->_search_content($element, @_);
	}

#------------------------------------------------------------------------------
# gets decoded text without other processing

sub	getFlatText
	{
	my $self	= shift;
	my $element	= $self->getElement(@_)	or return undef;
	return $self->outputTextConversion($element->text);
	}

#------------------------------------------------------------------------------
# gets text in element by path (sub-element texts are concatenated)

sub	getText	
	{
	my $self	= shift;
	my $element	= $self->OpenOffice::OODoc::XPath::getElement(@_);
	return undef	unless ($element && $element->isElementNode);
	my $text	= '';
	
	my $name	= $element->getName;

	if	($name =~ /^text:tab(|-stop)$/)	{ return "\t"; }
	if	($name eq 'text:line-break')	{ return "\n"; }
	if	($name eq 'text:s')
		{
		my $spaces = "";
		my $count = $element->att('text:c') || 1;
		while ($count > 0) { $spaces .= ' '; $count--; }
		return $spaces;
		}
	foreach my $node ($element->getChildNodes)
		{
		if ($node->isElementNode)
			{
			$text .=
			    (
			    $self->getText($node)
			    		||
			    ''
			    );
			}
		else
			{
			my $t = ($node->text() || '');
			$text .= $self->outputTextConversion($t);
			}
		}
	return $text;
	}

#------------------------------------------------------------------------------
# returns the children of a given element

sub	getElementList
	{
	my $self	= shift;
	my $path	= shift;		
	my $context	= shift;

	if ($context)
		{
		$path	= "./"	unless $path;
		}
	else
		{
		$path	= "/"	unless $path;
		$context = $self->{'context'};
		}
	$path =~ s/^\/\//\.\// if $context ne $self->{'xpath'};
	return $context->findnodes($path);
	}

#------------------------------------------------------------------------------

sub	getDescendants
	{
	my $self	= shift;
	my $tag		= shift;
	my $context	= shift || $self->{'context'};
	return $context->descendants($tag, @_);
	}

#------------------------------------------------------------------------------
# brute XPath nodelist selection; allows any XML::XPath expression

sub	selectNodesByXPath
	{
	my $self	= shift;
	my ($p1, $p2)	= @_;
	my $path	= undef;
	my $context	= undef;
	if (ref $p1)	{ $context = $p1; $path = $p2; }
	else		{ $path = $p1; $context = $p2; }

	$context = $self->{'context'} unless ref $context;
	return $context->get_xpath($path);
	}

#------------------------------------------------------------------------------
# brute XPath single node selection; allows any XML::XPath expression

sub	selectNodeByXPath
	{
	my $self	= shift;
	my ($p1, $p2)	= @_;
	my $path	= undef;
	my $context	= undef;
	if (ref $p1)	{ $context = $p1; $path = $p2; }
	else		{ $path = $p1; $context = $p2; }
	$context = $self->{'context'} unless ref $context;
	return $context->get_xpath($path, 0);
	}

sub	getNodeByXPath
	{
	my $self	= shift;
	return $self->selectNodeByXPath(@_);
	}

#------------------------------------------------------------------------------
# brute XPath value extraction; allows any XML::XPath expression

sub	getXPathValue
	{
	my $self	= shift;
	my ($p1, $p2)	= @_;
	my $path	= undef;
	my $context	= undef;
	if (ref $p1)	{ $context = $p1; $path = $p2; }
	else		{ $path = $p1; $context = $p2; }
	
	$context = $self->{'context'} unless $context;
	$path =~ s/^\/*// if $context ne $self->{'xpath'};
	return $self->outputTextConversion($context->findvalue($path, @_));
	}

#------------------------------------------------------------------------------
# create or update an xpath

sub	makeXPath
	{
	my $self	= shift;
	my $path	= shift;
	my $root	= undef;
	if (ref $path)
		{
		$root	= $path;
		$path	= shift;
		}
	else
		{
		$root	= $self->getRoot;
		}
	$path =~ s/^[\/ ]*//; $path =~ s/[\/ ]*$//;
	my @list	= split '/', $path;
	my $posnode	= $root;
	while (@list)
		{
		my $item	= shift @list;
		while (($item =~ /\[.*/) && !($item =~ /\[.*\]/))
			{
			my $cont = shift @list or last;
			$item .= ('/' . $cont);
			}
		next unless $item;
		my $node	= undef;
		my $name	= undef;
		my $param	= undef;
		$item =~ s/\[(.*)\] *//;
		$param = $1;
		$name = $item; $name =~ s/^ *//; $name =~ s/ *$//;
		my %attributes = ();
		my $text = undef;
		my $indice = undef;
		if ($param)
			{
			my @attrlist = [];
			$indice = undef;
			$param =~ s/^ *//; $param =~ s/ *$//;
			$param =~ s/^@//;
			@attrlist = split /@/, $param;
			foreach my $a (@attrlist)
				{
				next unless $a;
				$a =~ s/^ *//;
				my $tmp = $a;
				$tmp =~ s/ *$//;
				if ($tmp =~ /^\d*$/)
					{
					$indice = $tmp;
					next;
					}
				if ($a =~ s/^\"(.*)\".*/$1/)
					{
					$text = $1; next;
					}
				if ($a =~ /^=/)
					{
					$a	=~ s/^=//;
					$a	=~ '^"(.*)"$';
					$text	= $1 ? $1 : $a;
					next;
					}
				$a =~ s/^@//;
				my ($attname, $attvalue) = split '=', $a;
				next unless $attname;
				if ($attvalue)
					{
					$attvalue =~ '"(.*)"';
					$attvalue = $1 if $1;
					}
				$attname =~ s/^ *//; $attname =~ s/ *$//;
				$attributes{$attname} = $attvalue;
				}
			}
		if (defined $indice)
			{
			$node = $self->getNodeByXPath
					($posnode, "$name\[$indice\]");
			}
		else
			{
			$node	=
				$self->getChildElementByName($posnode, $name);
			}
		if ($node)
			{
			$self->setAttributes($node, %attributes);
			$self->setText($node, $text)	if (defined $text);
			}
		else
			{
			$node = $self->appendElement
					(
					$posnode, $name,
					text		=> $text,
					attributes	=> {%attributes}
					);
			}
		if ($node)	{ $posnode = $node;	}
		else		{ return undef;		}
		}
	return $posnode;
	}

#------------------------------------------------------------------------------
# selects element by path and attribute

sub	selectElementByAttribute
	{
	my $self	= shift;
	my $path	= shift;
	my $key		= shift;
	my $value	= shift || '';

	my @candidates	=  $self->getElementList($path);
	return @candidates	unless $key;

	for (@candidates)
		{
		if ($_->isElementNode)
			{
			my $v = $self->getAttribute($_, $key);
			return $_ if (defined $v && ($v =~ /$value/));
			}
		}
	return undef;
	}

#------------------------------------------------------------------------------
# selects list of elements by path and attribute

sub	selectElementsByAttribute
	{
	my $self	= shift;
	my $path	= shift;
	my $key		= shift;
	my $value	= shift || '';

	my @candidates	=  $self->getElementList($path);
	return @candidates	unless $key;

	my @selection	= ();
	for (@candidates)
		{
		if ($_->isElementNode)
			{
			my $v	= $self->getAttribute($_, $key);
			push @selection, $_	if ($v && ($v =~ /$value/));
			}
		}

	return wantarray ? @selection : $selection[0];
	}

#------------------------------------------------------------------------------
# get a list of elements matching a given path and an optional content pattern

sub	findElementList
	{
	my $self	= shift;
	my $path	= shift;
	my $pattern	= shift;
	my $replace	= shift;

	return undef unless $path;

	my @result	= ();

	my $context = $self->{'context'};
	$path =~ s/^\/\//\.\// unless $context eq $self->{'xpath'};
	foreach my $n ($context->findnodes($path))
		{
		push @result,
		    [ $self->findDescendants($n, $pattern, $replace, @_) ];
		}

	return @result;
	}

#------------------------------------------------------------------------------
# get a list of elements matching a given path and an optional content pattern
# without replacement operation, and from an optional context node

sub	selectElements
	{
	my $self	= shift;
	my $path	= shift;
	my $context	= $self->{'xpath'};
	if (ref $path)
		{
		$context	= $path;
		$path		= shift;
		}
	my $filter	= shift;

	my @candidates	= $self->selectNodesByXPath($context, $path);
	return @candidates	unless $filter;

	my @result	= ();
	while (@candidates)
		{
		my $node = shift @candidates;
		push @result, $node
			if $self->_search_content($node, $filter, @_, $node);
		}
	return @result;
	}

#------------------------------------------------------------------------------
# get the 1st element matching a given path and on optional content pattern

sub	selectElement
	{
	my $self	= shift;
	my $path	= shift;
	my $context	= $self->{'xpath'};
	if (ref $path)
		{
		$context	= $path;
		$path		= shift;
		}
	return undef	unless $path;
	my $filter	= shift;

	my @candidates	= $self->selectNodesByXPath($context, $path);
	return $candidates[0]	unless $filter;

	while (@candidates)
		{
		my $node = shift @candidates;
		return $node
			if $self->_search_content($node, $filter, @_, $node);
		}
	return undef;
	}

#------------------------------------------------------------------------------
# gets the descendants of a given node, with optional in fly search/replacement

sub	findDescendants
	{
	my $self	= shift;
	my $node	= shift;
	my $pattern	= shift;
	my $replace	= shift;
	
	my @result		= ();

	my $n	= $self->selectNodeByContent($node, $pattern, $replace, @_);
	push @result, $n	if $n;
	foreach my $m ($node->getChildNodes)
		{
		push @result,
		    [ $self->findDescendants($m, $pattern, $replace, @_) ];
		}

	return @result;
	}

#------------------------------------------------------------------------------
# search & replace text in an individual node

sub	selectNodeByContent
	{
	my $self	= shift;
	my $node	= shift;
	my $pattern	= shift;
	my $replace	= shift;

	return $node	unless $pattern;
	my $l	= $node->text;

	return undef	unless $l;

	unless (defined $replace)
		{
		return ($l =~ /$pattern/) ? $node : undef;
		}
	else
		{
		if (ref $replace)
			{
			unless
			  ($l =~ s/($pattern)/&$replace(@_, $node, $1)/eg)
				{
				return undef;
				}
			}
		else
			{
			unless ($l =~ s/$pattern/$replace/g)
				{
				return undef;
				}
			}
		$node->set_text($l);
		return $node;
		}
	}

#------------------------------------------------------------------------------
# gets the text content of a nodelist

sub	getTextList
	{
	my $self	= shift;
	my $path	= shift;
	my $pattern	= shift;
	my $context	= shift;
	
	return undef unless $path;
	
	$context = $self->{'context'} unless $context;
	$path =~ s/^\/\//\.\// unless $context eq $self->{'xpath'};
	my @nodelist = $context->findnodes($path);
	my @text = ();

	foreach my $n (@nodelist)
		{
		my $l	= $self->outputTextConversion($n->string_value);
		push @text, $l if ((! defined $pattern) || ($l =~ /$pattern/));
		}

	return wantarray ? @text : join "\n", @text;
	}

#------------------------------------------------------------------------------
# gets the attributes of an element in the key => value form

sub	getAttributes
	{
	my $self	= shift;
	my $path	= shift;
	my $pos		= (ref $path) ? undef : shift;

	return undef	unless $path;
	$pos	= 0	unless $pos;

	my $node	= $self->getElement($path, $pos, @_);
	return undef	unless $path;

	my %attributes	= ();
	my $aa		= $node->atts(@_);
	my %atts	= %{$aa} if $aa;
	foreach my $a (keys %atts)
		{
		$attributes{$a}	= $self->outputTextConversion($atts{$a});
		}

	return %attributes;
	}

#------------------------------------------------------------------------------
# gets the value of an attribute by path + name

sub	getAttribute
	{
	my $self	= shift;
	my $path	= shift;
	my $pos		= (ref $path) ? undef : shift;
	my $name	= shift;

	return undef	unless $path;
	$pos	= 0	unless $pos;

	my $node	= $self->getElement($path, $pos, @_);
	return	$self->outputTextConversion($node->att($name));
	}

#------------------------------------------------------------------------------
# set/replace a list of attributes in an element

sub	setAttributes
	{
	my $self	= shift;
	my $path	= shift;
	my $pos		= (ref $path) ? undef : shift;
	my %attr	= @_;

	my $node	= $self->getElement($path, $pos, $attr{'context'});
	return undef	unless $node;

	foreach my $k (keys %attr)
		{
		if (defined $attr{$k})
		    {
		    $node->set_att
		    		(
				$k,
				$self->inputTextConversion($attr{$k})
				);
		    }
		else
		    {
		    $node->del_att($k) if $node->att($k);
		    }
		}

	return %attr;
	}

#------------------------------------------------------------------------------
# set/replace a single attribute in an element

sub	setAttribute
	{
	my $self	= shift;
	my $path	= shift;
	my $pos		= (ref $path) ? undef : shift;

	my $attribute	= shift or return undef;
	my $value	= shift;
	my $node	= $self->getElement($path, $pos, @_)
		or return undef;

	if (defined $value)
		{
		$node->set_att
			(
			$attribute,
			$self->inputTextConversion($value)
			);
		}
	else
		{
		$node->del_att($attribute) if $node->att($attribute);
		}
	
	return $value; 
	}

#------------------------------------------------------------------------------
# removes an attribute in element

sub	removeAttribute
	{
	my $self	= shift;
	my $path	= shift;
	my $pos		= (ref $path) ? undef : shift;
	my $name	= shift;

	my $node	= $self->getElement($path, $pos, @_);

	return undef	unless $node;
	return $node->del_att($name) if $node->att($name);
	}

#------------------------------------------------------------------------------
# replicates an existing element, provided as an XPath ref or an XML string

sub	replicateElement
	{
	my $self	= shift;
	my $proto	= shift;
	my $position	= shift;
	my %options	= @_;

	unless ($proto && ref $proto && $proto->isElementNode)
		{
		warn "[" . __PACKAGE__ . "::replicateElement] No prototype\n";
		return undef;
		}

	$position	= 'end'	unless $position;

	my $element		= $proto->copy;
	$self->setAttributes($element, %{$options{'attribute'}});

	if	(ref $position)
		{
		if (! $options{'position'})
			{
			$element->paste_last_child($position);
			}
		elsif ($options{'position'} eq 'before')
			{
			$element->paste_before($position);
			}
		elsif ($options{'position'} eq 'after')
			{
			$element->paste_after($position);
			}
		elsif ($options{'position'} ne 'free')
			{
			warn	"[" . __PACKAGE__ . "::replicateElement] " .
				"No valid attachment option\n";
			}
		}
	elsif	($position eq 'end')
		{
		$element->paste_last_child($self->{'xpath'}->root);
		}
	elsif	($position eq 'body')
		{
		$element->paste_last_child($self->getBody);
		}

	return $element;
	}

#------------------------------------------------------------------------------
# create an element, just with a mandatory name and an optional text
# the name can have the namespace:name form
# if the $name argument is a '<.*>' string, it's processed as XML and
# the new element is completely generated from it

sub	createElement
	{
	my $self	= shift;
	my $name	= shift;
	my $text	= shift;

	my $element = OpenOffice::OODoc::XPath::new_element($name, @_);
	unless ($element)
		{
		warn	"[" . __PACKAGE__ . "::createElement] "	.
			"Element creation failure\n";
		return undef;
		}

	$self->setText($element, $text)		if defined $text;

	return $element;
	}
	
#------------------------------------------------------------------------------
# replaces an element by another one
# the new element is inserted before the old one,
# then the old element is removed.
# the new element can be inserted by copy (default) or by reference
# return = new element if success, undef if failure

sub	replaceElement
	{
	my $self	= shift;
	my $path	= shift;
	my $pos		= (ref $path) ? undef : shift;
	my $new_element	= shift;
	my %options	=
			(
			mode		=> 'copy',
			@_
			);
	unless ($new_element)
		{
		warn	"[" . __PACKAGE__ . "::replaceElement] " .
			"Missing new element\n";
		return undef;
		}
	unless (ref $new_element)
		{
		$new_element = $self->createElement($new_element);
		$options{'mode'} = 'reference';
		}
	unless ($new_element && $new_element->isElementNode)
		{
		warn	"[" . __PACKAGE__ . "::replaceElement] " .
			"No valid replacement\n";
		return undef;
		}

	my $result	= undef;

	my $old_element	= $self->getElement
			($path, $pos, $options{'context'});
	unless ($old_element)
		{
		warn	"[" . __PACKAGE__ . "::replaceElement] " .
			"Non existing element to be replaced\n";
		return undef;
		}
	if	(! $options{'mode'} || $options{'mode'} eq 'copy')
		{
		$result = $new_element->copy;
		$result->replace($old_element);
		return $result;
		}
	elsif	($options{'mode'} && $options{'mode'} eq 'reference')
		{
		$result = $self->insertElement
					(
					$old_element,
					$new_element,
					position	=> 'before'
					);
		$self->removeElement($old_element);
		return $result;
		}
	else
		{
		warn	"[" . __PACKAGE__ . "::replaceElement] " .
			"Unknown option\n";
		}
	return undef;
	}

#------------------------------------------------------------------------------
# appends a new or existing child element to any existing element

sub	appendElement
	{
	my $self	= shift;
	my $path	= shift;
	my $pos		= (ref $path) ? undef : shift;
	my $name	= shift;
	my %opt		= @_;
	$opt{'attribute'} = $opt{'attributes'} unless ($opt{'attribute'});

	return undef	unless $name;
	my $element	= undef;
	
	unless (ref $name)
		{
		$element	= $self->createElement($name, $opt{'text'});
		}
	else
		{
		$element	= $name;
		$self->setText($element, $opt{'text'})	if $opt{'text'};
		}
	return undef	unless $element;
	my $parent	= $self->getElement
			($path, $pos, $opt{'context'});
	unless ($parent)
		{
		warn	"[" . __PACKAGE__ .
			"::appendElement] Position not found\n";
		return undef;
		}
	$element->paste_last_child($parent);
	$self->setAttributes($element, %{$opt{'attribute'}});

	return $element;
	}

#------------------------------------------------------------------------------
# appends a list of children to an existing element

sub	appendElements
	{
	my $self	= shift;
	my $path	= shift;
	my $pos		= (ref $path) ? undef : shift;
	my $parent	= $self->getElement($path, $pos) or return undef;
	my @children	= @_;
	foreach my $child (@children)
		{
		$parent->appendChild($child);
		}
	return $parent;
	}

#------------------------------------------------------------------------------
# cuts a set of existing elements and pastes them as children of a given one

sub	moveElements
	{
	my $self	= shift;
	my $path	= shift;
	my $pos		= (ref $path) ? undef : shift;
	my $parent	= $self->getElement($path, $pos) or return undef;
	$parent->pickUpChildren(@_);
	return $parent;
	}

#------------------------------------------------------------------------------
# inserts a new element before or after a given node

sub	insertElement
	{
	my $self	= shift;
	my $path	= shift;
	my $pos		= (ref $path) ? undef : shift;
	my $name	= shift;
	my %opt		= @_;

	return undef	unless $name;
	my $element	= undef;
	unless (ref $name)
		{
		$element	= $self->createElement($name, $opt{'text'});
		}
	else
		{
		$element	= $name;
		$self->setText($element, $opt{'text'})	if $opt{'text'};
		}
	return undef	unless $element;
	
	my $posnode	= $self->getElement($path, $pos, $opt{'context'});
	unless ($posnode)
		{
		warn "[" . __PACKAGE__ . "::insertElement] Unknown position\n";
		return undef;
		}
 
	if ($opt{'position'})
		{
		if ($opt{'position'} eq 'after')
			{
			$element->paste_after($posnode);
			}
		elsif ($opt{'position'} eq 'before')
			{
			$element->paste_before($posnode);
			}
		elsif ($opt{'position'} eq 'within')
			{
			my $offset = $opt{'offset'} || 0;
			$element->paste_within($posnode, $offset);
			}
		else
			{
			warn	"[" . __PACKAGE__ . "::insertElement] "	.
				"Invalid $opt{'position'} option\n";
			return undef;
			}
		}
	else
		{
		$element->paste_before($posnode);
		}

	$self->setAttributes($element, %{$opt{'attribute'}});

	return $element;
	}

#------------------------------------------------------------------------------
# removes the given element & children

sub	removeElement
	{
	my $self	= shift;

	my $e	= $self->getElement(@_);
	return undef	unless $e;
	return $e->delete;
	}

#------------------------------------------------------------------------------
# cuts the given element & children (to be pasted elsewhere)

sub	cutElement
	{
	my $self	= shift;

	my $e	= $self->getElement(@_);
	return undef	unless $e;
	$e->cut;

	return $e;
	}

#------------------------------------------------------------------------------
# splits a text element at a given offset

sub	splitElement
	{
	my $self	= shift;
	my $path	= shift;
	my $old_element	=
		(ref $path) ? $path : $self->getElement($path, shift);
	my $offset	= shift;
	
	my $new_element = $old_element->split_at($offset);
	$new_element->set_atts($old_element->atts);
	return wantarray ? ($old_element, $new_element) : $new_element;
	}

#------------------------------------------------------------------------------
# some extensions for XML Twig elements
package	OpenOffice::OODoc::Element;
our @ISA	= qw ( XML::Twig::Elt );
#------------------------------------------------------------------------------

sub	hasTag
	{
	my $node	= shift;
	my $name	= $node->getName;
	my $value	= shift;
	return ($name && ($name eq $value)) ? 1 : undef;
	}

sub	isFrame
	{
	my $node	= shift;
	return $node->hasTag('draw:frame');
	}
	
sub	getLocalPosition
	{
	my $node	= shift;
	my $tag		= $node->getName or return undef;
	my $xpos	= $node->pos($tag);
	return defined $xpos ? $xpos - 1 : undef;
	}

sub	setName
	{
	my $node	= shift;
	return $node->set_tag(shift);
	}

sub	getPrefix
	{
	my $node	= shift;
	return $node->ns_prefix;
	}

sub	selectChildElements
	{
	my $node	= shift;
	my $filter	= shift;
	return $node->getChildNodes() unless $filter;
	my @list	= ();
	my $fc = $node->first_child;
	my $name = $fc->name if $fc;
	while ($fc)
		{
		if ($name && ($name =~ /$filter/))
			{
			return $fc unless wantarray;
			push @list, $fc;
			}
		$fc = $fc->next_sibling;
		$name = $fc->name if $fc;;
		}
	return @list;
	}

sub	selectChildElement
	{
	my $node	= shift;
	my $filter	= shift;
	my $pos		= shift || 0;

	my $count	= 0;
	my $fc = $node->first_child;
	my $name = $fc->name if $fc;
	while ($fc)
		{
		if ($name && ($name =~ /$filter/))
			{
			return $fc if ($count >= $pos);
			$count++;
			}
		$fc = $fc->next_sibling;
		$name = $fc->name if $fc;
		}
	return undef;
	}

sub	getParentNode
	{
	my $node	= shift;
	return $node->parent;
	}
	
sub	getFirstChild
	{
	my $node	= shift;
	my $fc = $node->first_child(@_);
	my $name = $fc->name if $fc;
	while ($name && ($name =~ /^#/))
		{
		$fc = $fc->next_sibling(@_);
		$name = $fc->name if $fc;
		}
	return $fc;
	}

sub	getLastChild
	{
	my $node	= shift;
	my $lc = $node->last_child(@_);
	my $name = $lc->name;
	while ($name && ($name =~ /^#/))
		{
		$lc = $lc->prev_sibling(@_);
		$name = $lc->name;
		}
	return $lc;
	}

sub	getDescendantNodes
	{
	my $node	= shift;
	my @children	= $node->getChildNodes(@_);
	my @descendants = ();
	foreach my $child (@children)
		{
		push @descendants, $child, $child->getDescendantNodes(@_);
		}
	return @descendants;
	}

sub	getDescendantTextNodes
	{
	my $node	= shift;
	return $node->descendants('#PCDATA');
	}

sub	appendChild
	{
	my $node	= shift;
	my $child	= shift;
	unless (ref $child)
		{
		$child = OpenOffice::OODoc::XPath::new_element($child, @_);
		}
	return $child->paste_last_child($node);
	}
	
sub	pickUpChildren
	{
	my $parent	= shift;
	my @children	= @_;
	foreach my $child (@children)
		{
		$child->move(last_child => $parent);
		}
	return $parent;
	}

sub	insertNewNode
	{
	my $node	= shift;
	my $newnode	= shift or return undef;
	my $position	= shift;	# 'before', 'after', 'within', ...
	my $offset	= shift;
	unless (ref $newnode)
		{
		$newnode = OpenOffice::OODoc::XPath::new_element($newnode, @_);
		}
	if (defined $offset)
		{
		return $newnode->paste($position => $node, $offset);
		}
	else
		{
		return $newnode->paste($position => $node);
		}
	}

sub	removeChildNodes
	{
	my $node	= shift;
	return $node->cut_children(@_);
	}

sub	replicateNode
	{
	my $node	= shift;
	my $number	= shift || 1;
	my $position	= shift || 'after';
	my $lastnode	= $node;
	while ($number > 0)
		{
		my $newnode	= $node->copy;
		$newnode->paste($position => $lastnode);
		$last_node	= $newnode;
		$number--;
		}
	return $last_node;
	}

sub	getNodeValue
	{
	my $node	= shift;
	return $node->text;
	}

sub	getValue
	{
	my $node	= shift;
	return $node->text;
	}

sub	setNodeValue
	{
	my $node	= shift;
	return $node->set_text(@_);
	}

sub	flatten
	{
	my $node	= shift;
	return $node->set_text($node->text);
	}
	
sub	appendTextChild
	{
	my $node	= shift;
	my $text	= shift;
	return undef unless defined $text;
	my $text_node	= OpenOffice::OODoc::Element->new('#PCDATA' => $text);
	return $text_node->paste_last_child($node);
	}

sub	insertTextChild
	{
	my $node	= shift;
	my $text	= shift;
	return undef unless defined $text;
	my $offset	= shift;
	return $node->appendTextChild($text) unless defined $offset;
	my $text_node	= OpenOffice::OODoc::Element->new('#PCDATA' => $text);
	return $text_node->paste_within($node, $offset);
	}

sub	getAttribute
	{
	my $node	= shift;
	return $node->att(@_);
	}

sub	getAttributes
	{
	my $node	= shift;
	return %{$node->atts(@_)};
	}

sub	setAttribute
	{
	my $node	= shift or return undef;
	my $attribute	= shift;
	my $value	= shift;
	if (defined $value)
		{
		return $node->set_att($attribute, $value, @_);
		}
	else
		{
		if ($node->{$attribute})
			{
			return $node->del_att($attribute);
			}
		else
			{
			return undef;
			}
		}
	}

sub	removeAttribute
	{
	my $node	= shift or return undef;
	return $node->del_att(shift);
	}

#------------------------------------------------------------------------------
1;