<?php
/*
**	Copyright (C) 1995-2007 by the ITools Authors.
**	This file is part of ITools - the Internet Tools Library
**
**	ITools is free software; you can redistribute it and/or modify
**	it under the terms of the GNU General Public License as published by
**	the Free Software Foundation; either version 3 of the License, or
**	(at your option) any later version.
**
**	ITools is distributed in the hope that it will be useful,
**	but WITHOUT ANY WARRANTY; without even the implied warranty of
**	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
**	GNU General Public License for more details.
**
**	You should have received a copy of the GNU General Public License
**	along with this program.  If not, see <http://www.gnu.org/licenses/>.
**
**	UltraHTML 3000 tool layer. Create functions for html tags.
**
**	new it_html;
**	echo html(head('title' => 'hello'), body(h1('hello'), p('Hello world!')));
**/

/**
 * Parse an arg array (mixed key=>value pairs and strings) and return it
 * as array(0 => all numerical args concatenated, 1 => array(key=>value pairs)
 * php version if c extension is not loaded
 */
if (!function_exists("it_parse_args")) {
function it_parse_args($args)
{
	$p = array();
	foreach ($args as $arg)
	{
		if (is_array($arg))
		{
			foreach ($arg as $key => $value)
			{
				if (is_int($key))
					$data .= it_taintcheck($value);
				else
					$p[$key] = $value;
			}
		}
		else
			$data .= it_taintcheck($arg);
	}

	return array($data, $p);
}
}


class it_html
{
	static $voidtags = array('area' => 1, 'base' => 1, 'br' => 1, 'col' => 1, 'command' => 1, 'embed' => 1, 'hr' => 1, 'img' => 1, 'input' => 1, 'keygen' => 1, 'link' => 1, 'meta' => 1, 'param' => 1, 'source' => 1, 'track' => 1, 'wbr' => 1); # need no closing tag

/**
 * Create a HTML object and global functions for all methods (exlcluding
 * methods starting with '_') in this class plus the default tags (see below).
 *
 * @param $p Configuration settings. Can be set/overridden in constructor, configure(), html() or head().
 *           See source code for a list of supported values
 */
function it_html($p = array())
{
	# Default configuration of html class
	$this->p = $p + array(
		'charset' => ini_get('default_charset') ?: 'iso-8859-1',
		'doctype' => null,                    # Custom doctype (will usually be calculated from htmltype)
		'head' => '',                         # Code to put into head() section
		'htmltype' => 'html5',                # 'html5', 'html' (=old-style), 'xhtml' or 'xhtml-mobile'
		'lang' => 'de',                       # Language code to use in <html lang="..."> tag
		'ie_png_fix' => false,                # To enable, supply URL of a transparent gif (like /images/0.gif)
		'moretags' => '',                     # Comma-separated list of tag-functions to generate additionally to 'tags'
		'name' => 'it_html',                  # Name of global variable $this is assigned to (string), XXX Copy and paste in configure() to keep PHP4 compatibility
		'nonewlinetags' => 'a,b,em,img,input,label,span,noscript', # tags that do not like newlines after them
		'alwaysclosetags' => 'a,button,div,iframe,pre,script,span,tbody,td,tfoot,thead,textarea,ul,ol', # tags which always get a close tag
		'prettyprint' => it::is_devel(),      # Should output be prettily indented?
		'show_boot_dom' => false,             # If true, append invisible <div id="it_boot_dom"> at the end of body
		'show_content_type' => true,          # If true, add <meta http-equiv="Content-Type" ...> header
		'show_favicon' => true,               # If true, add <link> tag to /favicon.ico if it exists
		'favicon' => '',	              # If set, add favicon <link> tag to this url
		'staticallycallable' => 'Q,U,select', # Those methods are statically callable (have same arguments as global stubs) but are a bit slower
		'tags' => "a,b,br,button,div,em,fieldset,form,h1,h2,h3,h4,h5,h6,hr,input,label,legend,li,meta,noscript,p,pre,span,style,table,tbody,td,textarea,tfoot,th,thead,tr,ul,ol,article,section",
		'title' => '',                        # HTML title (default: no title added)
		'use_it_state' => false,              # If true, generate code needed by state.js (aka 'history iframe')
		'srclines' => $GLOBALS['debug_srclines'], # append stackdump to each tag
	);
	$this->p['notexported'] = trim($p['notexported'] . ',configure,sanitize,comment', ',');

	# We know these doctypes. If you need something else, supply 'doctype' in p
	$this->doctypes = array(
		'html5'		=> '<!DOCTYPE html>',
		'html'		=> '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',
		'xhtml'		=> '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
		'xhtml-mobile'	=> '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.0//EN" "http://www.wapforum.org/DTD/xhtml-mobile10.dtd">'
	);

	# Since name is given as param, it is our duty to store it, not our caller's.
	$GLOBALS[$this->p['name']] =& $this;

	it_html::configure(array('name' => $this->p['name']));
	$notexported = array_flip(explode(',', "dummy," . $this->p['notexported']));		# dummy keeps values > 0

	# Create global functions for _tags
	foreach (array_merge(explode(',', $this->p['tags']), explode(',', $this->p['moretags'])) as $func)
	{
		if (!function_exists($func) && $func)
			$code[$func] = "function $func() { \$args = func_get_args(); return \$GLOBALS['{$this->p['name']}']->fasttag ? it_tag('$func', \$args) : \$GLOBALS['{$this->p['name']}']->_tag('$func', \$args); }";
	}

	# Create global functions for it_html methods
	foreach (get_class_methods(get_class($this)) as $func)
	{
		if (!preg_match('/^_/', $func) && !is_a($this, $func) && $func && !function_exists($func) && !$notexported[$func])
			$code[$func] = "function $func() { \$args = func_get_args(); return \$GLOBALS['{$this->p['name']}']->$func(\$args); }";
	}

	# Create global functions for methods that are statically callable (have same arguments as global stubs)
	foreach (explode(',', $this->p['staticallycallable']) as $func)
	{
		if ($func && !function_exists($func))
			$code[$func] = "function $func() { \$args = func_get_args(); return call_user_func_array(array(&\$GLOBALS['{$this->p['name']}'], '$func'), \$args); }";
	}

	eval(join('', (array)$code));
}


/**
 * Modify configuration of it_html, e.g. htmltype after it was instantiated.
 * @param $p Configuration settings. Can be set/overridden in constructor, configure(), html() or head().
 *           See constructor for a list of supported values
 */
function configure($p)
{
	$ithtml = $GLOBALS[$p['name'] ? $p['name'] : "it_html"];
	$ithtml->p = $p + (array)$ithtml->p;
	$ithtml->hasnonewline = array_flip(explode(',', $ithtml->p['nonewlinetags']));
	$ithtml->alwaysclose = array_flip(explode(',', $ithtml->p['alwaysclosetags']));
	$ithtml->fasttag = function_exists("it_tag") && ($GLOBALS['debug_fasttag'] || ($ithtml->p['name'] == "it_html" && !$ithtml->p['srclines'] && !$GLOBALS['debug_utf8check'] && $ithtml->p['charset'] == "utf-8" && !$ithtml->p['prettyprint']));
}

/**
 * Return doctype and entire HTML page.
 * Example application code to render page:
 *     echo html(head(...), body(...));
 *
 * @param any number of text args or array of key => value pairs
 * Defaults for key => value parameters are inherited from the it_html constructor and should be set there.
 * The parameters are $p['lang'], $p['htmltype'], $p['doctype'], $p['class'] and $p['manifest']
 */
function html($args)
{
	list($data, $p) = it_parse_args($args);
	$p += $this->p;

	foreach (array('class', 'manifest') as $attr)
		$attrs .= $p[$attr] ? " $attr=" . '"' . Q($p[$attr]) . '"' : "";

	$html = ($p['doctype'] ? $p['doctype'] : $this->doctypes[$p['htmltype']]) . "\n" .
		'<html ' . ($p['htmltype'] == "xhtml" ? 'xmlns="http://www.w3.org/1999/xhtml" ' : '') . ($p['htmltype'] == "xhtml-mobile" ? 'xml:lang' : 'lang') . "=\"{$p['lang']}\"$attrs>\n" . $data . ($p['omit_endhtml'] ? '' : "</html>\n");

	return EDC('upd') ? it::replace(array('<!DOCTYPE.*<head>' => ''), $html, array('singleline' => true)) : $html;
}


/**
 * Return HTML header on first call or empty string on subsequent calls
 *
 * @param args... any number of assoc arrays and strings. strings will be content of <head>
 * @param $p['content-type']  content type (default: "text/html; charset=iso-8859-1")
 * @param $p['headers']       Array of HTTP headers (e.g. [ 'Vary' => "User-Agent,Accept-Language" ])
 * @param $p['cssinline']     stylesheet to be put in header
 * @param $p['description']   data for <meta name="description"> tag
 * @param $p['keywords']      data for <meta name="keywords"> tag
 * @param $p['stylesheets']   array mediatype => url of styleshests
 * @param $p['title']         content of <title>, will be html encoded
 */
function head($args = array())
{
	if (!$this->head_sent++)
	{
		list($data, $p) = it_parse_args($args);

		$p += $this->p;
		$this->p = ($p += array('content-type' => "text/html; charset={$p['charset']}"));

		$header = $p['show_content_type'] ? meta(array('http-equiv' => "Content-Type", 'content' => $p['content-type'])) : "";

		# Enable latest IE mode
		if(strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE'))
			$header .= meta(array('http-equiv' => "X-UA-Compatible", 'content' => "IE=edge"));

		foreach(array('description', 'keywords') as $name)
			if (!empty($p[$name]))
				$header .= meta(array('name' => $name, 'content' => $p[$name]));

		# Add favicon
		if ($p['favicon'])
			$header .= tag('link', array('rel' => "shortcut icon", 'href' => $p['favicon']));
		elseif ($p['show_favicon'] && @file_exists($_SERVER['DOCUMENT_ROOT'] . '/favicon.ico'))
			$header .= tag('link', array('rel' => "shortcut icon", 'href' => "/favicon.ico"));

		foreach((array)$p['stylesheets'] as $type => $url)
			$header .= tag('link', array('rel' => "stylesheet", 'type' => "text/css", 'href' => $url) + (is_int($type) ? array() : array('media' => $type)));

		if (!empty($p['cssinline']))
			$header .= tag('style', array('type' => "text/css", "\n" . preg_replace(array('/\s*\/\*[^\*]+\*\//Um', '/\s*\{\s*/', '/;\s+/'), array('', '{', ';'), $p['cssinline'])));

		$header .= $p['head'] . ($p['title'] ? tag('title', strip_tags(Q(strip_tags($p['title'])))) : ""); # remove coloring by .texts and .q debug param

		if($this->p['htmltype'] == "xhtml-mobile" && strpos($_SERVER['HTTP_USER_AGENT'], 'W3C_Validator'))
			header("Content-Type: application/xhtml+xml; charset={$this->p['charset']}"); # for validation
		else if (!headers_sent()) # prevent warnings when ED() in use
		{
			header("Content-Type: " . $p['content-type']);
			foreach ((array)$p['headers'] as $key => $value)
				header("$key: $value");
		}

		$js = isset($p['jsenv']) ? "var env = " . itjs::serialize($p['jsenv']) . ";\n" : '';
		$js .= $this->_itjs($p['jsinline'], 'inline');

		if ($p['js'])
		{
			$js .= $this->_itjs("boot.js", "inline");
			$js .= "function it_boot_start(){ " . trim($p['jsboot']) . " }\n";
			$js .= "it_boot('/itjs/" . U($p['js'], array('s' => itjs::checksum($p['js']))) . "');\n";
		}

		if ($js)
			$data .= $this->js(array(self::_cleanup($js, $p['charset'])));

		return tag('head', $header, $data);
	}
}


/**
 * Return HTML head (if not already sent) and body with it_state and it_boot magic code
 */
function body($args)
{
	if ($this->p['use_it_state'])
		array_unshift($args, tag('iframe', array('id' => "it_state", 'src' => "/itjs/state.html", 'width' => 1, 'height' => 1, 'frameborder' => 0)));

	if (EDC('what'))
	{
		$debugvars = array_merge($GLOBALS['ULTRADEBUGVARS'], array('srclines' => 1, 'texts' => 1));
		ksort($debugvars);
		foreach ($debugvars as $var => $dummy)
		{
			if (EDC($var))
				$toggled_host = preg_replace("/$var\./", "", $_SERVER['HTTP_HOST']);
			else
				$toggled_host = preg_replace("/\.([^.]*)\.([^.]*)\.([^.]*)$/", ".$var.\$1.\$2.\$3", $_SERVER['HTTP_HOST']); 
			$debug_links[] = a(array('href' => U('http://' . $toggled_host . $_SERVER['REQUEST_URI']), 'style' => 'font-weight:' . (EDC($var) ? 'bold' : 'normal') . ';'), $var);
		}
		$args[] = div(array('style' => 'font-family:monospace;'), "Debugvars: " . join(" ", $debug_links));
	}

	if ($this->p['show_boot_dom'])
		$args[] = div(array('id' => "it_boot_dom", 'style' => "visibility:hidden"));

	return $this->head() . $this->_tag('body', $args);
}


/**
 * function div($args...)
 * Return a <div>...</div> element
 * Any strings in the argument list will be content of the <div>
 * Any associative arrays among the arguments will create attributes for <div>.
 * Attributes with values false or null will be omitted completely.
 * Attributes with value true can be used for boolean attributes like 'checked'
 * Attribute values will be html encoded, content values won't so you may need to use Q(). 
 * @see _tag()
 */ #:}

/**
 * INTERNAL: Create html tag from name and args array (the arguments of the parent function)
 */
function _tag($name, $args)
{
	if ($this->fasttag)
		return it_tag($name, $args);

	list($data, $attr) = it_parse_args($args);

	$newline = isset($this->hasnonewline[$name]) ? "" : "\n";

	# Ultra XML PrettyPrinter 3000 [\] by SCA
	if ($this->p['prettyprint'] && $newline && (substr($data, -1, 1) == "\n") && (strpos($data, '<textarea') === false && strpos($data, '<pre') === false) && ($data != strip_tags($data)))
		$data = str_replace("\n", "\n  ", "\n" . trim($data)) . "\n";

	# debugging aid: add backtrace
	if (($levels = intval($this->p['srclines'])) && !it::match('^(head|meta|title|script|style|link)', $name))
		$attr = array('title' => it_debug::backtrace(array('levels' => max(3, $levels), 'skipfiles' => "_html\\.class"))) + $attr;

	$charset = $GLOBALS['it_html']->p['charset'];
	$result .= "<$name";

	# add attributes. If $value === true, use key only (<td nowrap> instead of <td nowrap=""> for old html, <td nowrap="nowrap"> for xhtml style)
	foreach($attr as $key => $value)
	{
		if (($value === null) || ($value === false))	# null or false: omit whole tag
			;
		else if (isset($value) && $value !== true)	# normal case: value
		{
			if (preg_match('/[<>&"\x00-\x08\x0a-\x0c\x0e-\x1f\x80-\x9f]/', $value)) # WARNING: copy/pasted from Q()
				$result .= " $key=\"" . str_replace("\n", "&#10;", htmlspecialchars(self::_cleanup($value, $charset), ENT_COMPAT, $charset)) . '"';
			else
				$result .= " $key=\"$value\"";
		}
		else						# true: tag without value
			$result .= ($this->p['htmltype'][0] != 'x') ? " $key" : " $key=\"$key\"";
	}

	# Apply a kind of magic... this needs further investigation
	if (isset($data) || isset($this->alwaysclose[$name]))
		$result .= ">$data</$name>$newline";
	elseif ($this->p['htmltype'][0] != 'x')
		$result .= ">$newline";
	else
		$result .= " />$newline";

	if ($GLOBALS['debug_utf8check'] && $charset == "utf-8")
		$result = it::any2utf8($result, "error in $name()");

	return $result;
}

/**
 * Return a <tag> containing optional data.
 * @param $name tag name ('style', etc.)
 * @param ... any number optional data or array of key => value arguments
 * @return string containing XML/HTML tag
 */
function tag($args)
{
	$name = array_shift($args);
	return $this->_tag($name, $args);
}

/**
 * Special img() function patches png transparency for IE 5.5-6 if ie_png_fix is set
 * @param ... any number optional data or array of key => value arguments
 * @return <img ... />
 */
function img($args)
{
	if ($this->p['ie_png_fix'] && preg_match('/MSIE [56]/', $_SERVER['HTTP_USER_AGENT']))
	{
		foreach($args as $id => $arg)
			if (preg_match('/\.png(\?.*)?$/', $arg['src']))
			{
				$args[$id]['style'] = "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='{$arg['src']}',sizingMethod='crop');" . $arg['style'];
				$args[$id]['src'] = $this->p['ie_png_fix'];
			}
	}

	foreach ($args as $arg)
		$havealt += isset($arg['alt']);

	if (!$havealt)
		it::error('img() needs alt');

	return $this->_tag("img", $args);
}


/**
 * Create a dropdown menu object. Warning: encodes html code within options!
 * @param $tags key => value pairs of <select> tag
 * @param $options array (value => text) of available options or
 *        string key:val{,key:val} where key will be rawurldecoded so it may contain %2C as comma
 *        supports optgroups as array (value => optgroup => array(value => text))
 * @param $selected optional currently selected value, or comma-separated list or array for multi-select
 * Note: use tag('select') and tag('option') if you want do roll your own
 */
function select($tags, $options, $selected = null)
{
	# Transmogrify key:val{,key:val} to array(key => val)
	if (!is_array($options))
	{
		$opts = explode(',', $options);
		$options = array();

		foreach($opts as $opt)
		{
			list($key, $value) = explode(':', $opt);
			$options[rawurldecode($key)] = $value;
		}
	}

	$selected = ($tags['multiple'] && is_string($selected)) ? explode(',', $selected) : (array)$selected;

	$html = "";
	foreach($options as $value => $option)
	{
		if (is_array($option))
		{
			$grouphtml = "";
			foreach($option as $optval => $opt)
				$grouphtml .= $this->_tag("option", array(array('value' => $optval, 'selected' => in_array((string)$optval, $selected)), self::_strip_tags(Q(self::_strip_tags($opt)))));
			$html .= $this->_tag("optgroup", array(array('label' => $value, $grouphtml)));
		}
		else
			$html .= $this->_tag("option", array(array('value' => $value, 'selected' => in_array((string)$value, $selected), 'disabled' => $option === ""), (trim($option) === "") ? "&nbsp;" : self::_strip_tags(Q(self::_strip_tags($option))))); # self::_strip_tags removes .q debug param coloring
	}

	return $this->_tag("select", array($tags, $html));
}

/**
 * Outputs string as correctly quoted HTML comment
 */
function comment($string)
{
	return "<!-- " . it::replace(array('--' => "&#45;&#45;"), $string) . " -->";
}

# internal: strip spans added by debug params
function _strip_tags($html)
{
	return preg_replace('!</?(span|a)\b[^>]*>!', '', $html);
}

/**
 * Return HTML with all evil things stripped. Allowed are a coupld of simple
 * tags like div, p, i, b, br without attributes, a with absolute href,
 * img with absolute src url. Also ensures that tags are balanced.
 * @param $html HTML string to be sanitized
 * @return Sanitized HTML
 */
function sanitize($html)
{
	$result = "";
	$charset = $GLOBALS['it_html']->p['charset'] ? $GLOBALS['it_html']->p['charset'] : 'iso-8859-1';
	if ($charset == "utf-8")
		$html = it::any2utf8($html);
	$html = it::replace(array('[\0\s]+' => " "), $html);	# \s also matches \r and \n
	$urlpattern = 'https?://[^">]+';

	if ($tag = it::match("(.*)<(div|p|i|b)\b[^>]*>(.*?)</\\2>(.*)", $html))
	{
		# Simple tags with content, no attributes kept
		list($head, $tagname, $content, $tail) = $tag;
		$tagname = strtolower($tagname);
		$result .= it_html::sanitize($head) . "<$tagname>" . it_html::sanitize($content) . "</$tagname>" . it_html::sanitize($tail);
	}
	else if ($tag = it::match('(.*)<a\b[^>]+?href="(' . $urlpattern . ')"[^>]*?>(.*?)</a>(.*)', $html))
	{
		# Link tags, keeps only href attribute
		list($head, $href, $content, $tail) = $tag;
		$result .= it_html::sanitize($head) . '<a href="' . it_html::Q(it_html::U(html_entity_decode($href, ENT_COMPAT, $charset))) . '">' . it_html::sanitize($content) . "</a>" . it_html::sanitize($tail);
	}
	else if ($tag = it::match('(.*)<img\b[^>]+?src="(' . $urlpattern . ')"[^>]*?>(.*)', $html))
	{
		# Image tags, keeps only src attribute
		list($head, $src, $tail) = $tag;
		$result .= it_html::sanitize($head) . '<img src="' . it_html::Q(it_html::U(html_entity_decode($src, ENT_COMPAT, $charset))) . '" alt="" />' . it_html::sanitize($tail);
	}
	else if ($tag = it::match("(.*)<(br|/tr)\b[^>]*>(.*)", $html))
	{
		# brs and table rows are converted so simple line breaks
		list($head, $tagname, $tail) = $tag;
		$result .= it_html::sanitize($head) . "<br />" . it_html::sanitize($tail);
	}
	else
		$result = it::replace(array('&amp;(#\d+;)' => '&$1'), it_html::Q(html_entity_decode(strip_tags($html), ENT_COMPAT, $charset)));

	return $GLOBALS['debug_q'] ? "<span style='background:#8FF'>$result</span>" : it::replace(array('<(div|p|i|b|a)></\1>' => ""), $result); # remove empty tags
}

/**
 * Decode all entities to encoding set for it_html
 */
function entity_decode($string)
{
	$charset = $GLOBALS['it_html']->p['charset'];
	$string = preg_replace('/&#(8217|65533);/', "'", html_entity_decode($string, ENT_COMPAT, $charset));
	$string = preg_replace('/&#[^;]*;/i', " ", $string); # remove remaining illegal numeric entities, e.g. 0x80-0x9f

	return self::_cleanup($string, $charset);
}

/**
 * Replace or remove all illegal characters from a HTML string (knows utf-8 and latin1)
 */
function _cleanup($string, $charset)
{
	$result = $charset == "utf-8"
		? preg_replace('/\xc2[\x80-\x9f]/', ' ', $string)
		: preg_replace('/[\x80-\x9f]/', ' ', strtr($string, array("\x80" => "EUR", "\x82" => "'", "\x84" => "\"", "\x85" => "...", "\x8a" => "S", "\x8c" => "OE", "\x8e" => "Z", "\x91" => "'", "\x92" => "'", "\x93" => "\"", "\x94" => "\"", "\x96" => "-", "\x97" => "-", "\x9a" => "s", "\x9e" => "z")));

	return preg_replace('/[\x00-\x08\x0b-\x0c\x0e-\x1f]/', ' ', $result);
}

/**
 * Shortcut: return htmlspecialchars($string) and encode forbidden characters 80-9f if latin1 is output
 * @param $string String to encode with htmlspecialchars()
 * @return htmlspecialchars($string)
 */
function Q($string)
{
	if (preg_match('/[<>&"\x00-\x08\x0a-\x0c\x0e-\x1f\x80-\xff]/', $string)) # WARNING: copy/pasted to _tag()
	{
		$charset = $GLOBALS['it_html']->p['charset'];
		if ($GLOBALS['debug_utf8check'] && $charset == "utf-8")
			$string = it::any2utf8($string, "error in Q()");

		$origstring = $string;
		$string = @htmlspecialchars(self::_cleanup($string, $charset), ENT_COMPAT, $charset);
		if ($string === "" && $origstring)
			it::error("invalid utf-8 '$origstring'");
	}

	return $GLOBALS['debug_q'] && $string ? "<span style='background:#8FF'>$string</span>" : $string;
}


/**
 * Build a complete url from base-url and params
 * @param ... scalar args and numeric indices build base-url, rest as params
 */
static function U(/* ... */)
{
	$args = func_get_args();
	list($base, $params) = it_parse_args($args);

	if (!isset($base))
		$base = preg_replace('/\?.*/', '', $_SERVER['REQUEST_URI']);

	$base = preg_replace(array('|\0|', '/\\\\/'), array('', '/'), $base); # kill null chars, turn \ to /

	if (!($u = @parse_url($base)))
		list($u['path'], $u['query']) = explode("?", $base, 2);

	$u['host'] = preg_match('/[^-_.0-9a-z]/i', $u['host']) && function_exists('idn_to_ascii') && ($idnahost = idn_to_ascii($GLOBALS['it_html']->p['charset'] == "iso-8859-1" ? utf8_encode($u['host']) : $u['host'])) ? $idnahost : $u['host'];	# Punycode hostname to include into webpage
	$u['host'] = preg_replace_callback('/[^-_.0-9a-z\x80-\xff]/i', function($m) { return rawurlencode($m[0]); }, $u['host']); # Encode garbage chars in host

	# handle scheme, user (urlencoded), password, host
	$hostpart = 
		($u['user'] ? preg_replace_callback('|[^-a-z0-9_.+!*(),:?@&=/~$%#]|i', function($m) { return rawurlencode($m[0]); }, $u['user'] . ($u['pass'] ? ":" . $u['pass'] : "") . "@") : "") .
		($u['host'] ? $u['host'] : "") .
		($u['port'] ? ":" . intval($u['port']) : "");

	$schemepart = $hostpart ? ($u['scheme'] ? $u['scheme'] . ":" : "") . "//$hostpart" : ($u['scheme'] == "mailto" ? $u['scheme'] . ":" : "");

	$path = $u['path'] . ($u['query'] ? "?" . $u['query'] : "") . ($u['fragment'] ? "#" . $u['fragment'] : "");

	# hack: encode % if not followed by two hex digits
	$parts = preg_split('/%([^%]{0,2})/', $path, -1, PREG_SPLIT_DELIM_CAPTURE);
	for ($i = 1; $i < count($parts); $i+=2)
		$parts[$i] = (preg_match('/[0-9a-f][0-9a-f]/i', $parts[$i]) ? "%" : "%25") . $parts[$i];
	$path = join("", $parts);

	$path = preg_replace_callback('|[^-a-z0-9_.+!*(),:?@&=/~$%#]|i', function($m) { return rawurlencode($m[0]); }, $path);
	$path = preg_replace('|^([a-z0-9_]+:)?//[^/]*$|', '$0/', $path);	# Add slash if absolute url without a path, e.g. http://gna.ch
	$queryparams = it_url::params($params);
	$separator = strpos($path, "?") === false ? "?" : "&";

	return $schemepart . $path . ($queryparams ? "$separator$queryparams" : "");
}


/**
 * Insert a javascript script
 * @param ... any number optional data or array of key => value arguments
 * @return <script type="text/javascript"...>...</script>
 */
function js($args)
{
	if (($this->p['htmltype'][0] == 'x') && $args[0] && ((array)$args[0] === array_values((array)$args[0])))
	{
		array_unshift($args, "<!--//--><![CDATA[//><!--\n");
		$args[] = "\n//--><!]]>";
	}

	if ($this->p['htmltype'] != "html5")
		array_unshift($args, array('type' => 'text/javascript'));

	return $this->_tag('script', $args);
}


/**
 * Include javascript code or generate HTML to include it
 */
function _itjs($files, $mode)
{
	$result = "";

	if ($files)
	{
		$filenames = itjs::filenames($files);

		if ($mode == "files")
		{
			foreach ($filenames as $file)
				$result .= tag('script', array('type' => "text/javascript", 'src' => "/itjs/" . basename($file) . "?v=" . itjs::checksum($file)));
		}
		else if ($mode == "inline")
		{
			$jsfile = "";

			foreach ($filenames as $file)
				$jsfile .= @file_get_contents($file);

			$result .= itjs::strip($jsfile);
		}
		else
			$result .= tag('script', array('type' => "text/javascript", 'src' => "/itjs/$files?v=" . itjs::checksum($filenames)));
	}

	return $result;
}

}
?>