<?php
#################################################################################
#																				#
#	XML functions																#
#		written by benjamin ramey												#
#		bsr@rameydesign.net														#
#																				#
#	best viewed with:															#
#		resolution = 1280x960													#
#		tab = 4 spaces															#
#		indent = 8 spaces														#
#																				#
#################################################################################

##################################################### FUNCTION: xml_parse_file	#
# parameters: path to file to parse
# returns: associative array of xml file data
# DOES NOT: handle CDATA, doing anything with WSDL,XSL,namespaces, or numerous
# other cool XML stuff
function xml_parse_file($xml_file)
{
	$xml = '';
	$data = array();
	
	# first we don't want to waste time, so make sure the file to open exists
	if(file_exists($xml_file))
	{
		# put entire file contents in $xml
		$xml = file_get_contents($xml_file);
		# if the file is empty, return and error
		if(empty($xml)) {
			return('XML document is empty.');
		}
	}
	# if the file doesn't exist return an error
	else { return("Could not find xml file."); }
	
	# this pattern matches the first line of every xml doc, the <?xml line
	$patt = "/^<\?xml([^\?]*)\?>/";
	
	# try to find this mandatory opening <?xml line
	if(preg_match($patt,$xml,$matches))
	{
		# get the xml specs inside the <?xml line
		$xml_specs = explode(' ',$matches[1]);
		foreach($xml_specs as $spec)
		{
			# if a spec is empty for some reason, don't attempt to work with it,
			# just continue to next one
			if(empty($spec)) { continue; }
			# specs are always in the name='value' format
			list($name,$value) = explode('=',$spec);
			# strip away any quotes from the value side of the '='
			$value = str_replace("'",'',$value);
			$value = str_replace('"','',$value);
			
			# assign these xml specs to the data array
			$data[$name] = $value;
		}
		$xml = substr($xml,strlen($matches[0]));
	}
	# if we don't find that mandatory opening <?xml line, return an error
	else { return('XML document missing opening xml tag.'); }
	
	# xml_left is a heap, initialize with all xml at first
	$xml_left = array($xml);
	# tmp_refs is also a heap, holding references to points in the xml document
	$tmp_refs[] =& $data;
	# we will use this pattern to recognize an opening xml tag
	$patt = "/^<(\w+)([^>]*)>/";
	
	# we continue through the xml until nothing is left in $xml_left
	while(!empty($xml_left))
	{
		# the last element of $xml_left is always the high priority, 
		# so process it next
		$to_process = array_pop($xml_left);
		# trim fore and aft spaces from the xml to process
		$to_process = trim($to_process);
		# the last index of our tmp_refs array, important later
		$last_index = count($tmp_refs) - 1;

		# attempt the match the pattern that finds an opening xml tag
		if(preg_match($patt,$to_process,$matches)) {
			$element = $matches[1];
			$parameters = isset($matches[2]) ? $matches[2] : '';
			
			if(isset($tmp_refs[$last_index][$element]))
			{
				if(is_array($tmp_refs[$last_index][$element]))
				{
					# case example: [biography]=>[0],[1]...[x]; $element = biography
					# endsup: [biography]=>[0],[1]...[x+1]
					if(isset($tmp_refs[$last_index][$element][0]))
					{
						array_push($tmp_refs[$last_index][$element],'');
						$tmp_index = count($tmp_refs[$last_index][$element]) - 1;
						$tmp_refs[] =& $tmp_refs[$last_index][$element][$tmp_index];
					}
					# case example: [biography]=>[paragraph]; $element = biography
					# endsup: [biography]=>[0]=>[paragraph]
					#						[1]=>''
					else
					{
						$tmp_ary = $tmp_refs[$last_index][$element];
						$tmp_refs[$last_index][$element] = array($tmp_ary,'');				
						$tmp_refs[] =& $tmp_refs[$last_index][$element][1];
					}
				}
				# case example: [biography]=>"string"; $element = biography
				#  endsup: [biography]=>[0]=>"string"
				#						[1]=>''
				else
				{
					$tmp_refs[$last_index][$element] = array($tmp_refs[$last_index][$element],'');
					$tmp_refs[] =& $tmp_refs[$last_index][$element][1];	
				}
			}
			# case example: [biography]=> NULL; $element = biography
			# endsup: [biography]=>""
			else
			{
				$tmp_refs[$last_index][$element] = '';
				$tmp_refs[] =& $tmp_refs[$last_index][$element];
			}
			
			# if there are parameters, add them as if they were imbedded elements
			if(!empty($parameters))
			{
				$pairs = explode(' ',$parameters);
				$last_index = count($tmp_refs) - 1;
				
				# we can do this, because we know the last element added 
				# is always fresh and new
				$tmp_refs[$last_index] = array();
				
				foreach($pairs as $pair)
				{
					# if a parameter is empty, just continue to next one
					if(empty($pair)) { continue; }
					
					list($name,$value) = explode('=',$pair);
					$value = str_replace("'",'',$value);
					$value = str_replace('"','',$value);
					
					$tmp_refs[$last_index][$name] = $value;
				}
			}
			
			# we've already found an opening tag, now find the closing tag
			$end_tag = strpos($to_process,"</$element>");
			
			# if there is not closing tag--oops!  badly formatted xml doc
			if($end_tag === FALSE)
			{
				return("XML document improperly formed.
						No closing tag found for element: $element");
			}
			
			# get the string length of the opening tag and all the paramters
			# inside, plus the opening and closing brackets
			$element_len = strlen($matches[0]);
			# then, the contents of this xml tag start after the above
			# measured length to the end of the $to_process string, minus
			# the length of the closing tag
			$contents = substr($to_process,$element_len,$end_tag - $element_len);
			
			# if there is text in the $to_process string past this closing
			# tag that we just found, put it on top of the $xml_left tag
			# it's a sister element of the current one
			if($extra = substr($to_process,$end_tag + strlen("</$element>")))
			{
				array_push($xml_left,$extra);
			}
			# if not sister elements are found, we are at the end of this 
			# generation of elements, so place an END indicator in the 
			# $xml_left array
			else
			{
				array_push($xml_left,'!~~END~~!'); 
			}
			array_push($xml_left,$contents);
		}
		# we place the identifier END to indicate the end of an xml tag that
		# has nested tags and no content itself, if this is NOT the case
		# we have content to assign the current xml tag
		else if($to_process != '!~~END~~!')
		{
			$tmp_refs[$last_index] = $to_process;
			array_pop($tmp_refs);
		}
		# if we miss the above two ifs, we are at the end of an element
		# but don't have content to assign, so step back one generation
		# in the xml document
		else
		{
			$tmp = array_pop($tmp_refs);
		}
	}
	
	# if all went well, we return an associative array with all the xml
	# data in it, properly nested
	return($data);
}

##################################################### FUNCTION: xml_force_array	#
# parameters: path to file to parse
# returns: associative array of xml file data
function xml_force_array($element)
{
	$array = array();
	if(is_array($element))
	{
		if(isset($element[0]))	{ $array = $element; }
		else					{ array_push($array,$element); }
	}
	else 						{ array_push($array,$element); }	
	
	return($array);
}
?>