<?php

/************************************************************
 Library to convert HTML into an approximate text equivalent
v2.0 update 04/03/2009 with major new functionality
*************************************************************

Please see http://www.howtocreate.co.uk/php/ for details
Please see http://www.howtocreate.co.uk/php/html2texthowto.html for detailed instructions
Please see http://www.howtocreate.co.uk/jslibs/termsOfUse.html for terms and conditions of use

Note that although version 1 of this script was included with permission in a GPL bundle,
the script still retains its own license terms, separate from the terms of other software
included in the bundle. This version is not available under GPL.

*/

$html2text_elements = Array();
$html2text_elements['the document'] = Array(true,true); //drop first and last margins in the document - recommended: true,true
$html2text_elements['unknown element'] = Array(false,false,false,0,0,false,'','',false,false,false); //used for all unknown or default elements

//list all elements that need special (non-default) handling:
$html2text_elements['html'] = Array(true,false,false,10,10,false,'','',false,false,false);
$html2text_elements['title'] = Array(true,false,false,1,1,false,'',"\r\n          --------------------",false,false,false);
$html2text_elements['script'] =
$html2text_elements['style'] =
$html2text_elements['datalist'] = Array(false,false,true,0,0,false,'','',false,false,false);
$html2text_elements['h1'] = Array(true,false,false,2,2,false,'** ',' **',false,false,false);
$html2text_elements['h2'] = Array(true,false,false,2,2,false,'*','*',false,false,false);
$html2text_elements['h3'] = Array(true,false,false,2,2,false,'-','-',false,false,false);
$html2text_elements['h4'] =
$html2text_elements['h5'] =
$html2text_elements['h6'] =
$html2text_elements['p'] =
$html2text_elements['ul'] =
$html2text_elements['dl'] =
$html2text_elements['table'] =
$html2text_elements['blockquote'] =
$html2text_elements['legend'] =
$html2text_elements['dir'] =
$html2text_elements['menu'] =
$html2text_elements['article'] =
$html2text_elements['aside'] =
$html2text_elements['datagrid'] =
$html2text_elements['details'] =
$html2text_elements['dialog'] =
$html2text_elements['figure'] =
$html2text_elements['footer'] =
$html2text_elements['nav'] =
$html2text_elements['section'] = Array(true,false,false,2,2,false,'','',false,false,false);
$html2text_elements['blockquote'] = Array(true,false,false,2,2,false,'html2text_before_quote','}}',true,false,false);
$html2text_elements['form'] = Array(true,false,false,2,2,false,'html2text_before_form','',true,false,false);
$html2text_elements['pre'] =
$html2text_elements['listing'] =
$html2text_elements['plaintext'] =
$html2text_elements['xmp'] = Array(true,true,false,2,2,false,'','',false,false,false);
$html2text_elements['head'] =
$html2text_elements['body'] =
$html2text_elements['noframes'] =
$html2text_elements['div'] =
$html2text_elements['fieldset'] =
$html2text_elements['dt'] =
$html2text_elements['caption'] =
$html2text_elements['thead'] =
$html2text_elements['body'] =
$html2text_elements['tfoot'] =
$html2text_elements['tr'] =
$html2text_elements['address'] =
$html2text_elements['center'] =
$html2text_elements['marquee'] =
$html2text_elements['header'] = Array(true,false,false,1,1,false,'','',false,false,false);
$html2text_elements['dt'] = Array(true,false,false,1,1,false,'* ','',false,false,false);
$html2text_elements['th'] =
$html2text_elements['td'] = Array(true,false,false,0,0,true,"\t",'',false,false,true);
$html2text_elements['dd'] = Array(true,false,false,1,1,true,"        ",'',false,false,false);
$html2text_elements['ol'] = Array(true,false,false,2,2,false,'','',false,false,false);
$html2text_elements['li'] = Array(true,false,false,1,1,true,'html2text_before_li','',true,false,false);
$html2text_elements['br'] = Array(true,false,false,0,0,false,"\r\n",'',false,false,false); //use \r\n instead of margin - must not collapse
$html2text_elements['hr'] = Array(true,false,false,1,1,false,'          --------------------','',false,false,false);
$html2text_elements['sup'] = Array(false,false,false,0,0,false,'^','',false,false,false);
$html2text_elements['sub'] = Array(false,false,false,0,0,false,'[',']',false,false,false);
$html2text_elements['s'] =
$html2text_elements['strike'] =
$html2text_elements['del'] = Array(false,false,false,0,0,false,'[DEL: ',' :DEL]',false,false,false);
$html2text_elements['ins'] = Array(false,false,false,0,0,false,'[INS: ',' :INS]',false,false,false);
$html2text_elements['strong'] =
$html2text_elements['b'] =
$html2text_elements['mark'] = Array(false,false,false,0,0,false,'*','*',false,false,false);
$html2text_elements['em'] =
$html2text_elements['i'] = Array(false,false,false,0,0,false,'/','/',false,false,false);
$html2text_elements['u'] = Array(false,false,false,0,0,false,'_','_',false,false,false);
$html2text_elements['q'] = Array(false,false,false,0,0,false,'"','"',false,false,false);
$html2text_elements['a'] = Array(false,false,false,0,0,false,'html2text_before_link','',true,false,false);
$html2text_elements['area'] = Array(false,false,false,0,0,false,'html2text_before_area','',true,false,false);
$html2text_elements['base'] = Array(false,false,false,0,0,false,'html2text_before_base','',true,false,false);
$html2text_elements['input'] = Array(false,false,false,0,0,false,'html2text_before_input','',true,false,false);
$html2text_elements['bb'] = Array(false,false,false,0,0,false,'[INPUT]','',false,false,false);
$html2text_elements['isindex'] = Array(true,false,false,2,2,false,'html2text_before_isindex','',true,false,false);
$html2text_elements['textarea'] =
$html2text_elements['button'] =
$html2text_elements['select'] = Array(false,false,true,0,0,false,'[INPUT]','',false,false,false);
$html2text_elements['img'] = Array(false,false,false,0,0,false,'html2text_before_img','',true,false,false);

//functions for special case elements, where an attribute or some other feature of the element determines how it should behave

function html2text_before_li($element,$index) {
	//get parent tag name do work out if it is a bullet or numbered list
	$parent_node = $element->parentNode;
	$parent_tag = ( $parent_node == $element->ownerDocument ) ? '' : $element->parentNode->tagName;
	if( !$element->ownerDocument->h2t_isxml ) {
		$parent_tag = strToLower($parent_tag);
	}
	$prefix = '';
	//for each LI ancestor, add indents for nested lists
	while( $parent_node != $element->ownerDocument ) {
		if( ( $element->ownerDocument->h2t_isxml ? $parent_node->tagName : strtolower($parent_node->tagName) ) == 'li' ) {
			$prefix .= '  ';
		}
		$parent_node = $parent_node->parentNode;
	}
	return $prefix.(($parent_tag=='ol')?($index.'.'):'·').' ';
}

function html2text_before_img($element,$index) {
	if( $element->hasAttribute('alt') && ( $alt = html2text_cleanspace($element->getAttribute('alt')) ) ) {
		return "[IMG: $alt]";
	}
	return '';
}

function html2text_before_input($element,$index) {
	if( $element->hasAttribute('type') && strtolower(html2text_cleanspace($element->getAttribute('type'))) != 'hidden' ) {
		return '[INPUT]';
	}
	return '';
}

function html2text_before_isindex($element,$index) {
	if( $element->hasAttribute('prompt') && ( $prompt = html2text_cleanspace($element->getAttribute('prompt')) ) ) {
		return "$prompt [INPUT]";
	}
	return '[INPUT]';
}

function html2text_before_link($element,$index) {
	if( $element->hasAttribute('href') && ( $href = html2text_resolve($element->getAttribute('href'),$element) ) ) {
		if( $element->childNodes->length == 1 && $element->firstChild->nodeType == XML_TEXT_NODE && $element->firstChild->nodeValue == preg_replace( "/^mailto:/iu", '', $href ) ) {
			//link text is exactly the same as the link itself - leave only the textNode
			//textContent exists, but has to do more work, is slower, ignores before/after and picks up void content, so only accept single text nodes
			return '';
		}
		return "[LINK: $href] ";
	}
	return '';
}

function html2text_before_area($element,$index) {
	if( $element->hasAttribute('href') && ( $href = html2text_resolve($element->getAttribute('href'),$element) ) ) {
		return "[LINK: $href] ".($element->hasAttribute('alt')?html2text_cleanspace($element->getAttribute('alt')):'');
	}
	return '';
}

function html2text_before_form($element,$index) {
	if( $element->hasAttribute('action') && ( $action = html2text_resolve($element->getAttribute('action'),$element) ) ) {
		return "[FORM: $action]";
	}
	return '';
}

function html2text_before_quote($element,$index) {
	if( $element->hasAttribute('cite') && ( $cite = html2text_resolve($element->getAttribute('cite'),$element) ) ) {
		return "{{ [CITE: $cite]";
	}
	return '{{';
}

function html2text_before_base($element,$index) {
	if( $element->hasAttribute('href') ) {
		$element->ownerDocument->h2t_base = @parse_url($element->getAttribute('href'));
		$element->ownerDocument->h2t_base['pathdir'] = preg_replace("/[^\/]+$/u",'',$element->ownerDocument->h2t_base['path']);
		$element->ownerDocument->h2t_base['pathfile'] = preg_replace("/^[\w\W]*\//u",'',$element->ownerDocument->h2t_base['path']);
		$element->ownerDocument->h2t_base['basefound'] = true;
		//parse_url does not populate properties if it does not find those parts of the URL - this prevents it using unititialised values
		foreach( Array('scheme','host','port','user','pass','path','query','fragment') as $key ) {
			if( !isset($element->ownerDocument->h2t_base[$key]) ) {
				$element->ownerDocument->h2t_base[$key] = '';
			}
		}
	}
}

function html2text_cleanspace($str) {
	return preg_replace("/\s+/u",' ',preg_replace("/^\s+|\s+$/u",'',$str));
}

function html2text_resolve($href,$element) {
	$base = $element->ownerDocument->h2t_base;
	//resolve $href according to the $base href
	if( preg_match("/^javascript:/iu",$href) ) { return ''; } //JavaScript URLs are useless
	if( preg_match("/^[^\/\#?]*:/u",$href) ) { return $href; } //assume absolute URL
	if( ( !$href || preg_match("/^\#/u",$href) ) && !$base['basefound'] ) { return ''; } //relative or fragment url with no visible path - useless
	if( !$base['basefound'] ) { return $href; } //relative with visible path but no base - can't help that
	//if it begins with // then just add protocol
	$prefix = $base['scheme'].':';
	if( preg_match("/^\/\//u",$href) ) { return $prefix.$href; }
	//if it begins with / then add protocol://user?:pass?@?host:port?
	@$prefix .= '//'.($base['user']?$base['user']:'').($base['pass']?(':'.$base['pass']):'').(($base['user']||$base['pass'])?'@':'').
	           $base['host'].($base['port']?(':'.$base['port']):'');
	if( preg_match("/^\//u",$href) ) { return $prefix.$href; }
	//if it begins with ./ or [^.?\#] then add protocol://user?:pass?@?host:port?/folder_path/
	$pathprefix = $base['pathdir'];
	if( preg_match("/^(\.\/|[^.?\#])/u",$href) ) { return $prefix.$pathprefix.preg_replace("/^\.\//u",'',$href); }
	//if it begins with ../ then remove one folder for each initial occurence of ../
	if( preg_match("/^\.\.\//u",$href) ) {
		do {
			$href = preg_replace("/^\.\.\//u",'',$href);
			$pathprefix = preg_replace("/[^\/]*\/[^\/]*$/u",'',$pathprefix);
		} while( preg_match("/^\.\.\//u",$href) );
		return $prefix.($pathprefix?$pathprefix:'/').$href; //put back a / if it stepped back past the end of the folder path (too many ../)
	}
	//if it begins with ? then add protocol://user?:pass?@?host:port?/folder_path/filename
	$pathprefix .= $base['pathfile'];
	if( preg_match("/^\?/u",$href) ) { return $prefix.$pathprefix.$href; }
	//if it begins with # then add protocol://user?:pass?@?host:port?/folder_path/filename?querystring
	$pathprefix .= $base['query']?('?'.$base['query']):'';
	if( preg_match("/^\#/u",$href) ) { return $prefix.$pathprefix.$href; }
	return $prefix.$pathprefix.$href.($base['fragment']?('#'.$base['fragment']):'');
}

function html2text_formattext($flags,$node_value) {
	//return the text node, with pending margins, and pending spaces
	//reset all pending states
	$output = '';
	if( $flags->h2t_blockstart ) {
		for( $i = 0; !$flags->h2t_ignoremargin and ( $i < $flags->h2t_currentmargin ); $i++ ) {
			$output .= "\r\n";
		}
		$flags->h2t_currentmargin = 0;
		$flags->h2t_blockstart = false;
	} else if( $flags->h2t_pendingspace ) {
		$flags->h2t_pendingspace = false;
		$output .= ' ';
	}
	$flags->h2t_ignoremargin = false;
	$output .= preg_replace("/\x0160/u",' ',$node_value); //nbsp becomes a regular space
	return $output;
}

function html2text_render($element,$elementindex) {
	//try to create a textual rendering of the element and its children
	global $html2text_elements;
	$flags = $element->ownerDocument;
	//store last element formats, to restore them as needed when leaving this element
	$previous_format = $flags->h2t_ispre;
	$previous_margindrop = $flags->h2t_ignoremargin;
	//get formatting information for this tag
	$tag_name = $flags->h2t_isxml ? $element->tagName : strtolower($element->tagName);
	$elem_det = &$html2text_elements[$tag_name];
	if( !$elem_det ) { $elem_det = &$html2text_elements['unknown element']; }
	//determine computed formatting
	$flags->h2t_currentmargin = max( $flags->h2t_currentmargin, $elem_det[3] ); //basic margin collapse ;)
	if( $elem_det[0] ) {
		//block start - drop any pending spaces
		$flags->h2t_pendingspace = false;
		$flags->h2t_blockstart = true;
	}
	$flags->h2t_ispre = $elem_det[1] || $previous_format;
	//start working out element rendering
	$element_render = '';

	//deal with ::before
	$temprender = '';
	if( $elem_det[6] ) {
		//it can be dropped using the :first-child rule, but still need to output something, or dropFirstChildMargin can remove all linebreaks (TD/TH)
		$drop = $elementindex == 1 && $elem_det[10];
		if( !$elem_det[8] ) {
			$element_render .= html2text_formattext($flags,$drop?'':$elem_det[6]);
		} elseif( $temprender = call_user_func($elem_det[6],$element,$elementindex) ) {
			$element_render .= html2text_formattext($flags,$drop?'':$temprender);
		}
		if( $elem_det[0] && preg_match( "/\s$/u", $elem_det[8] ? $temprender : $elem_det[6] ) ) {
			//there was some output ending in whitespace that will have used up any pending margin
			//it must not be allowed to affect pending whitespace status or it creates weird indents
			$flags->h2t_pendingspace = false;
			$flags->h2t_blockstart = true;
		}
	}

	//deal with contents
	$flags->h2t_ignoremargin = $elem_det[5] || $previous_margindrop;
	$num_child = $element->childNodes->length;
	for( $i = 0, $elemindex = 0; !$elem_det[2] && ( $i < $num_child ); $i++ ) {
		$node = $element->childNodes->item($i);
		if( $node->nodeType == XML_TEXT_NODE || $node->nodeType == XML_CDATA_SECTION_NODE ) {
			if( $flags->h2t_ispre ) {
				//render entire text node, but only use windows linebreaks, as required by 2822
				$element_render .= html2text_formattext($flags,preg_replace("/\r\n?|\n/u","\r\n",$node->nodeValue));
			} else {
				//enforce: <p>  <span> </span> <span> Single <span>  space </span> </span></p>
				//use [ \t\f\r\n] because \s also matches nbsp when 'u' flag is used
				if( !$flags->h2t_blockstart ) {
					$flags->h2t_pendingspace = $flags->h2t_pendingspace || preg_match("/^[ \t\f\r\n]/u",$node->nodeValue);
				}
				//'u' flag incorrectly deletes utf-8 nodes here if * is used instead of +
				$content = preg_replace("/[ \t\f\r\n]+/u",' ',preg_replace("/^[ \t\f\r\n]+|[ \t\f\r\n]+$/u",'',$node->nodeValue));
				if( $content ) {
					$element_render .= html2text_formattext($flags,$content);
					$flags->h2t_pendingspace = $flags->h2t_pendingspace || preg_match("/[ \t\f\r\n]$/u",$node->nodeValue);
				}
			}
		} elseif ( $node->nodeType == XML_ELEMENT_NODE ) {
			$elemindex++;
			$element_render .= html2text_render($node,$elemindex);
		}
	}

	//deal with ::after
	if( $elem_det[7] ) {
		if( !$elem_det[9] ) {
			if( $elem_det[0] && preg_match( "/^\s/u", $elem_det[7] ) ) {
				//don't output pending spaces on blocks if the 'after' content has its own
				$flags->h2t_pendingspace = false;
			}
			$element_render .= html2text_formattext($flags,$elem_det[7]);
		} elseif( $temprender = call_user_func($elem_det[7],$element,$elementindex) ) {
			if( $elem_det[0] && preg_match( "/^\s/u", $temprender ) ) {
				$flags->h2t_pendingspace = false;
			}
			$element_render .= html2text_formattext($flags,$temprender);
		}
	}

	//restore preformatting state, and margin drop if it has not been used
	$flags->h2t_ispre = $previous_format;
	$flags->h2t_ignoremargin = $previous_margindrop && $flags->h2t_ignoremargin;
	if( $elem_det[0] ) {
		//block end (next output creates a new block, even if it is inline) - drop any pending spaces
		$flags->h2t_pendingspace = false;
		$flags->h2t_blockstart = true;
	}
	$flags->h2t_currentmargin = max( $flags->h2t_currentmargin, $elem_det[4] );
	return $element_render;
}

function html2text( $sourceStr, $isXML = false, $isfile = false ) {
	global $html2text_elements;
	if( is_object($sourceStr) ) {
		$DOM = $sourceStr;
	} else {
		$DOM = new DOMDocument();
		//parse the markup
		if( $isXML ) {
			if( $isfile ) {
				$DOM->load($sourceStr);
			} else {
				$DOM->loadXML($sourceStr);
			}
		} else if( $isfile ) {
			$DOM->loadHTMLFile($sourceStr);
		} else {
			//remove any PHP (and XML prologs) if it exists
			$strippedStr = preg_replace( "/<\?[\w\W]*\?>/u", '', $sourceStr );
			if( $strippedStr === null ) {
				//ouch - encoding problem detected
				trigger_error('String passed to html2text has encoding issues (contains characters not permitted in the encoding PHP is using) - attempting recovery by not stripping PHP from the source string',E_USER_WARNING);
				$DOM->loadHTML( $sourceStr );
			} else {
				$DOM->loadHTML( $strippedStr );
			}
			unset($strippedStr);
		}
	}
	unset($sourceStr); //free up memory before layout happens
	//flags - it would be possible to pass flags between recursive function calls, or use references, but this is easier
	//flag: current dropFirstChildMargin value
	$DOM->h2t_ignoremargin = $html2text_elements['the document'][0];
	//flag: current margin
	$DOM->h2t_currentmargin = 0;
	//flag: a block/flow has been opened, but no text content has been rendered in it yet
	$DOM->h2t_blockstart = true;
	//flag: is there a pending whitespace character to output before next text content
	$DOM->h2t_pendingspace = false;
	//flag: says if any parent node has the isPreformatted state set
	$DOM->h2t_ispre = false;
	//flag: says if tagName is case sensitive
	$DOM->h2t_isxml = $isXML;
	//flag: the document's base href
	$DOM->h2t_base = Array('scheme'=>'','host'=>'','port'=>'','user'=>'','pass'=>'','path'=>'','query'=>'','fragment'=>'','pathdir'=>'','pathfile'=>'','basefound'=>'');
	//wordrapping is quite brutal and will also affect indents and preformatting, but as this is intended for email use, I don't care
	if( $DOM->documentElement ) {
		$sourceStr = html2text_render($DOM->documentElement,1);
		if( !$html2text_elements['the document'][1] ) {
			$DOM->h2t_blockstart = true;
			//get any remaining linebreaks
			$sourceStr .= html2text_formattext($DOM,'');
		}
		//wordwrap is not multibyte-safe ... can't help that, and it works in most cases
		return wordwrap($sourceStr,75,"\r\n");
	} else {
		return '';
	}
}
