code:
<?php
/**
 * @package Habari
 *
 */

/**
 * HabariDateTime class to wrap dates in.
 *
 * @property-read HabariDateTime $clone Returns a clonned object.
 * @property-read string $sql Returns a unix timestamp for inserting into DB.
 * @property-read int $int Returns a unix timestamp as integer.
 */
class HabariDateTime extends DateTime
{
	private static $default_timezone;
	private static $default_datetime_format = 'c';
	private static $default_date_format;
	private static $default_time_format;

	/**
	 * Set default timezone to system default on init.
	 *
	 * @static
	 */
	public static function __static()
	{

		if ( Options::get( 'timezone' ) ) {
			self::set_default_timezone( Options::get( 'timezone' ) );
		}

		self::$default_timezone = date_default_timezone_get();

		self::$default_date_format = Options::get('dateformat');
		self::$default_time_format = Options::get('timeformat');

		if ( self::$default_date_format || self::$default_time_format ) {
			self::set_default_datetime_format( self::$default_date_format . ' ' . self::$default_time_format );
		}
	}

	/**
	 * Set default date/time format. The format is the same as the
	 * internal php {@link http://ca.php.net/date date() function}.
	 *
	 * @static
	 * @param string $format The date format.
	 */
	public static function set_default_datetime_format( $format )
	{
		self::$default_datetime_format = $format;
	}

	/**
	 * Get the default date/time format set.
	 *
	 * @static
	 * @see set_default_datetime_format()
	 * @return string The date format set.
	 */
	public static function get_default_datetime_format()
	{
		return self::$default_datetime_format;
	}

	/**
	 * Sets the timezone for Habari and PHP.
	 *
	 * @static
	 * @param string $timezone A timezone name, not an abbreviation, for example 'America/New York'
	 */
	public static function set_default_timezone( $timezone )
	{
		self::$default_timezone = $timezone;
		date_default_timezone_set( self::$default_timezone );
	}

	/**
	 * Get the timezone for Habari and PHP.
	 * Defaults to system timezone if not set.
	 *
	 * @static
	 * @see set_default_timezone()
	 * @param string The deafult timezone.
	 */
	public static function get_default_timezone()
	{
		return self::$default_timezone;
	}

	/**
	 * Helper function to create a HabariDateTime object for the given
	 * time and timezone. If no time is given, defaults to 'now'. If no
	 * timezone given defaults to timezone set in {@link set_default_timezone()}
	 *
	 * @static
	 * @see DateTime::__construct()
	 * @param string $time String in a format accepted by
	 * {@link http://ca.php.net/strtotime strtotime()}, defaults to "now".
	 * @param string $timezone A timezone name, not an abbreviation.
	 */
	public static function date_create( $time = null, $timezone = null )
	{
		if ( $time instanceOf HabariDateTime ) {
			return $time;
		}
		elseif ( $time instanceOf DateTime ) {
			$time = $time->format('U');
		}
		elseif ( $time == null ) {
			$time = 'now';
		}
		elseif ( is_numeric($time) ) {
			$time = '@' . $time;
		}

		if ( $timezone === null ) {
			$timezone = self::$default_timezone;
		}

		// passing the timezone to construct doesn't seem to do anything.
		$datetime = new HabariDateTime($time);
		$datetime->set_timezone($timezone);
		return $datetime;
	}

	/**
	 * Set the date of this object
	 *
	 * @see DateTime::setDate()
	 * @param int $year Year of the date
	 * @param int $month Month of the date
	 * @param int $day Day of the date
	 */
	public function set_date( $year, $month, $day )
	{
		parent::setDate($year, $month, $day);
		return $this;
	}

	/**
	 * Sets the ISO date
	 *
	 * @see DateTime::setISODate()
	 * @param int $year Year of the date
	 * @param int $month Month of the date
	 * @param int $day Day of the date
	 */
	public function set_isodate( $year, $week, $day = null )
	{
		parent::setISODate($year, $week, $day);
		return $this;
	}

	/**
	 * Set the time of this object
	 *
	 * @see DateTime::setTime()
	 * @param int $hour Hour of the time
	 * @param int $minute Minute of the time
	 * @param int $second Second of the time
	 */
	public function set_time( $hour, $minute, $second = null )
	{
		parent::setTime($hour, $minute, $second);
		return $this;
	}

	/**
	 * Set the timezone for this datetime object. Can be either string
	 * timezone identifier, or DateTimeZone object.
	 *
	 * @see DateTime::setTimezone()
	 * @param mixed The timezone to use.
	 * @return HabariDateTime $this object.
	 */
	public function set_timezone( $timezone )
	{
		if ( ! $timezone instanceof DateTimeZone ) {
			$timezone = new DateTimeZone($timezone);
		}
		parent::setTimezone($timezone);
		return $this;
	}

	/**
	 * Get the timezone identifier that is set for this datetime object.
	 *
	 * @return DateTimeZone The timezone object.
	 */
	public function get_timezone()
	{
		return parent::getTimezone();
	}

	/**
	 * Returns date formatted according to given format.
	 *
	 * @see DateTime::format()
	 * @param string $format Format accepted by {@link http://php.net/date date()}.
	 * @return string The formatted date, false on failure.
	 */
	public function format($format = null)
	{
		if ( $format === null ) {
			$format = self::$default_datetime_format;
		}
		return parent::format($format);
	}

	/**
	 * Returns date components inserted into a string
	 *
	 * Example:
	 * echo HabariDateTime::date_create('2010-01-01')->text_format('The year was {Y}.');
	 * // Expected output:  The year was 2010.
	 *
	 * @param string $format A string with single-character date format codes {@link http://php.net/date date()} surrounded by braces
	 * @return string The string with date components inserted
	 */
	public function text_format($format)
	{
		return preg_replace_callback('%\{(\w)\}%iu', array($this, 'text_format_callback'), $format);
	}

	/**
	 * Callback method for supplying replacements for HabariDatTime::text_format()
	 *
	 * @param array $matches The matches found in the regular expression.
	 * @return string The date component value for the matched character.
	 */
	private function text_format_callback($matches)
	{
		return $this->format($matches[1]);
	}

	/**
	 * Alters the timestamp
	 *
	 * @param string $format A format accepted by {@link http://php.net/strtotime strtotime()}.
	 * @return HabariDateTime $this object.
	 */
	public function modify( $args )
	{
		parent::modify( $args );
		return $this;
	}

	/**
	 * @see format()
	 */
	public function get($format = null)
	{
		return $this->format($format);
	}

	/**
	 * Echos date formatted according to given format.
	 *
	 * @see format()
	 * @param string $format Format accepted by {@link http://php.net/date date()}.
	 */
	public function out($format = null)
	{
		echo $this->format($format);
	}

	/**
	 * Magic method called when this object is cast to string. Returns the
	 * unix timestamp of this object.
	 *
	 * @return string The unix timestamp
	 */
	public function __toString()
	{
		return $this->format('U');
	}

	/**
	 * Magic method to get magic ponies... properties, I mean.
	 */
	public function __get($property)
	{
		switch ($property) {
			case 'clone':
				return clone $this;

			case 'sql':
				return $this->format('U');
				break;

			case 'int':
				return intval( $this->format('U') );
				break;

			case 'time':
				return $this->format( self::get_default_time_format() );
				break;

			case 'date':
				return $this->format( self::get_default_date_format() );
				break;

			default:
				$info = getdate($this->format('U'));
				$info['mon0'] = substr('0' . $info['mon'], -2, 2);
				$info['mday0'] = substr('0' . $info['mday'], -2, 2);
				if ( isset($info[$property]) ) {
					return $info[$property];
				}
				return $this->$property;
		}
	}

	/**
	 * Return the default date format, as set in the Options table
	 *
	 * @return The default date format
	 **/
	public static function get_default_date_format ( ) {

		return self::$default_date_format;

	}

	/**
	 * Return the default time format, as set in the Options table
	 *
	 * @return The default time format
	 **/
	public static function get_default_time_format ( ) {

		return self::$default_time_format;

	}

	/**
	 * Returns an associative array containing the date information for
	 * this HabariDateTime object, as per {@link http://php.net/getdate getdate()}
	 *
	 * @return array Associative array containing the date information
	 */
	public function getdate()
	{
		$info = getdate($this->format('U'));
		$info['mon0'] = substr('0' . $info['mon'], -2, 2);
		$info['mday0'] = substr('0' . $info['mday'], -2, 2);
		return $info;
	}
}

?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * Habari Locale Class
 *
 * Provides translation services.
 *
 */
class HabariLocale
{
	private static $uselocale = FALSE;
	private static $messages = array();
	private static $plural_function;
	private static $locale;

	/**
	 * Sets the locale for Habari.
	 *
	 * @param string $locale A language code like 'en' or 'en-us' or 'x-klingon', will be lowercased
	 **/
	public static function set( $locale = NULL )
	{
		if ( $locale == NULL ) {
			return;
		}

		self::$locale = strtolower( $locale );
		self::$uselocale = self::load_domain( 'habari' );
	}

	/**
	 * Set system locale.
	 *
	 * The problem is that every platform has its own way to designate a locale,
	 * so for German you could have 'de', 'de_DE', 'de_DE.UTF-8', 'de_DE.UTF-8@euro'
	 * (Linux) or 'DEU' (Windows), etc.
	 *
	 * @todo: This setting should probably be stored in the language files.
	 *
	 * @param string... $locale The locale(s) to set. They will be tried in order.
	 * @return string the locale that was picked, or FALSE if an error occurred
	 */
	public static function set_system_locale()
	{
		if ( func_num_args() == 0 ) return;
		$args = func_get_args();
		array_unshift( $args, LC_ALL );

		return call_user_func_array( 'setlocale', $args );
	}

	/**
	 * Load translations for a given domain and base directory for a pluggable object.
	 * Translations are stored in gettext-style .mo files.
	 * The internal workings of the file format are not entirely meant to be understood.
	 *
	 * @link http://www.gnu.org/software/gettext/manual/html_node/gettext_136.html GNU Gettext Manual: Description of the MO file format
	 * @param string $domain the domain to load
	 * @param string $base_dir the base directory in which to find the translation files
	 * @return boolean TRUE if data was successfully loaded, FALSE otherwise
	 **/
	public static function load_pluggable_domain( $domain, $base_dir )
	{
		$file = $base_dir . '/locale/' . self::$locale . '/LC_MESSAGES/' . $domain . '.mo';
		return self::load_file( $domain, $file );
	}

	/**
	 * Load translations for a given domain.
	 * Translations are stored in gettext-style .mo files.
	 * The internal workings of the file format are not entirely meant to be understood.
	 *
	 * @link http://www.gnu.org/software/gettext/manual/html_node/gettext_136.html GNU Gettext Manual: Description of the MO file format
	 * @param string $domain the domain to load
	 * @return boolean TRUE if data was successfully loaded, FALSE otherwise
	 **/
	private static function load_domain( $domain )
	{
		$file_end = self::$locale . '/LC_MESSAGES/' . $domain . '.mo';

		if ( file_exists( Site::get_dir( 'config' ) . '/locale/' . $file_end ) ) {
			$file = Site::get_dir( 'config' ) . '/locale/' . $file_end;
		}
		else if (file_exists( HABARI_PATH . '/user/locale/' . $file_end ) ) {
			$file = HABARI_PATH . '/user/locale/' . $file_end;
		}
		else if (file_exists( HABARI_PATH . '/3rdparty/locale/' . $file_end ) ) {
			$file = HABARI_PATH . '/3rdparty/locale/' . $file_end;
		}
		else {
			$file = HABARI_PATH . '/system/locale/' . $file_end;
		}

		return self::load_file( $domain, $file );
	}

	/**
	* function list_all
	 * Retrieves an array of the Habari locales that are installed
	 *
	 * @return array. An array of Habari locales in the installation
	 **/
	public static function list_all()
	{
		$localedirs = array( HABARI_PATH . '/system/locale/', HABARI_PATH . '/3rdparty/locale/', HABARI_PATH . '/user/locale/' );
		if ( Site::CONFIG_LOCAL != Site::$config_type ) {
			// include site-specific locales
			$localedirs[] = Site::get_dir( 'config' ) . '/locale/';
		}

		$dirs = array();
		foreach ( $localedirs as $localedir ) {
			if ( file_exists( $localedir ) ) {
				$dirs = array_merge( $dirs, Utils::glob( $localedir . '*', GLOB_ONLYDIR | GLOB_MARK ) );
			}
		}
		$dirs = array_filter( $dirs, create_function('$a', 'return file_exists($a . "LC_MESSAGES/habari.mo");') );

		$locales = array_map( 'basename', $dirs );
		ksort( $locales );
		return $locales;
	}

	/**
	 * Load translations from a given file.
	 *
	 * @param string $domain the domain to load the data into
	 * @param string $file the file name
	 * @return boolean TRUE if data was successfully loaded, FALSE otherwise
	 */
	private static function load_file( $domain, $file )
	{
		if ( ! file_exists( $file ) ) {
			Error::raise( sprintf( _t('No translations found for locale %s, domain %s!'), self::$locale, $domain ) );
			return FALSE;
		}
		if ( filesize( $file ) < 24 ) {
			Error::raise( sprintf( _t('Invalid .MO file for locale %s, domain %s!'), self::$locale, $domain ) );
			return FALSE;
		}

		$fp = fopen( $file, 'rb' );
		$data = fread( $fp, filesize( $file ) );
		fclose( $fp );

		// determine endianness
		$little_endian = TRUE;

		list(,$magic) = unpack( 'V1', substr( $data, 0, 4 ) );
		switch ( $magic & 0xFFFFFFFF ) {
			case (int)0x950412de:
				$little_endian = TRUE;
				break;
			case (int)0xde120495:
				$little_endian = FALSE;
				break;
			default:
				Error::raise( sprintf( _t('Invalid magic number 0x%08x in %s!'), $magic, $file ) );
				return FALSE;
		}

		$revision = substr( $data, 4, 4 );
		if ( $revision != 0 ) {
			Error::raise( sprintf( _t('Unknown revision number %d in %s!'), $revision, $file ) );
			return FALSE;
		}

		$l = $little_endian ? 'V' : 'N';

		if ( $data && strlen( $data ) >= 20 ) {
			$header = substr( $data, 8, 12 );
			$header = unpack( "{$l}1msgcount/{$l}1msgblock/{$l}1transblock", $header );

			if ( $header['msgblock'] + ($header['msgcount'] - 1 ) * 8 > filesize( $file ) ) {
				Error::raise( sprintf( _t('Message count (%d) out of bounds in %s!'), $header['msgcount'], $file ) );
				return FALSE;
			}

			$lo = "{$l}1length/{$l}1offset";

			for ( $msgindex = 0; $msgindex < $header['msgcount']; $msgindex++ ) {
				$msginfo = unpack( $lo, substr( $data, $header['msgblock'] + $msgindex * 8, 8 ) );
				$msgids = explode( "\0", substr( $data, $msginfo['offset'], $msginfo['length'] ) );
				$transinfo = unpack( $lo, substr( $data, $header['transblock'] + $msgindex * 8, 8 ) );
				$transids = explode( "\0", substr( $data, $transinfo['offset'], $transinfo['length'] ) );
				self::$messages[$domain][$msgids[0]] = array(
					$msgids,
					$transids,
				);
			}
		}

		// setup plural functionality
		self::$plural_function = self::get_plural_function( self::$messages[$domain][''][1][0] );

		// only use locale if we actually read something
		return ( count( self::$messages ) > 0 );
	}

	private static function get_plural_function( $header )
	{
		if ( preg_match('/plural-forms: (.*?)$/i', $header, $matches) && preg_match('/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $matches[1], $matches) ) {
			// sanitize
			$nplurals = preg_replace( '/[^0-9]/', '', $matches[1] );
			$plural = preg_replace( '#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2] );

			$body = str_replace(
				array('plural',  'n',  '$n$plurals', ),
				array('$plural', '$n', '$nplurals', ),
				'nplurals='. $nplurals . '; plural=' . $plural
			);

			// add parens
			// important since PHP's ternary evaluates from left to right
			$body .= ';';
			$res = '';
			$p = 0;
			for ( $i = 0; $i < strlen($body); $i++ ) {
				$ch = $body[$i];
				switch ($ch) {
					case '?':
						$res .= ' ? (';
						$p++;
						break;
					case ':':
						$res .= ') : (';
						break;
					case ';':
						$res .= str_repeat( ')', $p) . ';';
						$p = 0;
						break;
					default:
						$res .= $ch;
				}
			}

			$body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);';
			$fn = create_function(
				'$n',
				$body
			);
		}
		else {
			// default: one plural form for all cases but n==1 (english)
			$fn = create_function(
				'$n',
				'$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);'
			);
		}

		return $fn;
	}

	/**
	 * DO NOT USE THIS FUNCTION.
	 * This function is only to be used by the test case for the Locale class!
	 */
	public static function __run_plural_test( $header )
	{
		$fn = self::get_plural_function( $header );
		$res = '';
		for ( $n = 0; $n < 200; $n++ ) {
			$res .= $fn($n);
		}

		return $res;
	}

	/**
	 * DO NOT USE THIS FUNCTION.
	 * This function is only to be used by the test case for the Locale class!
	 */
	public static function __run_loadfile_test( $filename )
	{
		return self::load_file( 'test', $filename );
	}

	/**
	 * Echo a version of the string translated into the current locale
	 * @param string $text The text to echo translated
	 * @param string $domain (optional) The domain to search for the message
	 **/
	public static function _e()
	{
		$args = func_get_args();
		echo call_user_func_array(array('HabariLocale', '_t'), $args);
	}

	/**
	 * Return a version of the string translated into the current locale
	 *
	 * @param string $text The text to echo translated
	 * @param string $domain (optional) The domain to search for the message
	 * @return string The translated string
	 **/
	public static function _t($text, $args = array(), $domain = 'habari')
	{
		if ( is_string($args) ) {
			$domain = $args;
		}

		if ( isset( self::$messages[$domain][$text] ) ) {
			$t = self::$messages[$domain][$text][1][0];
		}
		else {
			$t = $text;
		}

		if ( !empty($args) && is_array($args) ) {
			array_unshift($args, $t);
			$t = call_user_func_array('sprintf', $args);
		}

		return $t;
	}

	/**
	 * Given a string translated into the current locale, return the untranslated string.
	 *
	 * @param string $text The translated string
	 * @param string $domain (optional) The domain to search for the message
	 * @return string The untranslated string
	 **/
	public static function _u($text, $domain = 'habari')
	{
		$t = $text;
		foreach ( self::$messages[$domain] as $msg ) {
			if ( $text == $msg[1][0] ) {
				$t = $msg[0][0];
				break;
			}
		}

		return $t;
	}
	/**
	 * Echo singular or plural version of the string, translated into the current locale, based on the count provided
	 *
	 * @param string $singular The singular form
	 * @param string $plural The plural form
	 * @param string $count The count
	 * @param string $domain (optional) The domain to search for the message
	 **/
	public static function _ne( $singular, $plural, $count, $domain = 'habari' )
	{
		echo self::_n( $singular, $plural, $count, $domain );
	}

	/**
	 * Return a singular or plural string translated into the current locale based on the count provided
	 *
	 * @param string $singular The singular form
	 * @param string $plural The plural form
	 * @param string $count The count
	 * @param string $domain (optional) The domain to search for the message
	 * @return string The appropriately translated string
	 **/
	public static function _n($singular, $plural, $count, $domain = 'habari')
	{
		if ( isset( self::$messages[$domain][$singular] ) ) {
			// XXX workaround, but direct calling doesn't work
			$fn = self::$plural_function;
			$n = $fn($count);
			if ( isset( self::$messages[$domain][$singular][1][$n] ) ) {
				return self::$messages[$domain][$singular][1][$n];
			}
		}
		// fall-through else for both cases
		return ( $count == 1 ? $singular : $plural );
	}
}

/**
 * Echo a version of the string translated into the current locale, alias for HabariLocale::_e()
 *
 * @param string $text The text to translate
 **/
function _e( $text, $args = array(), $domain = 'habari' )
{
	return HabariLocale::_e( $text, $args, $domain );
}

/**
 * function _ne
 * Echo singular or plural version of the string, translated into the current locale, based on the count provided,
 * alias for HabariLocale::_ne()
 * @param string $singular The singular form
 * @param string $plural The plural form
 * @param string $count The count
 **/
function _ne( $singular, $plural, $count, $domain = 'habari' )
{
	return HabariLocale::_ne( $singular, $plural, $count, $domain );
}

/**
 * Return a version of the string translated into the current locale, alias for HabariLocale::_t()
 *
 * @param string $text The text to translate
 * @return string The translated string
 **/
function _t( $text, $args = array(), $domain = 'habari' )
{
	return HabariLocale::_t( $text, $args, $domain );
}

/**
 * Return a singular or plural string translated into the current locale based on the count provided
 *
 * @param string $singular The singular form
 * @param string $plural The plural form
 * @param string $count The count
 * @return string The appropriately translated string
 **/
function _n( $singular, $plural, $count, $domain = 'habari' )
{
	return HabariLocale::_n( $singular, $plural, $count, $domain );
}

/**
 * Given a string translated into the current locale, return the untranslated version of the string.
 * Alias for HabariLocale::_u()
 *
 * @param string $text The translated string
 * @param string $domain (optional) The domain to search for the message
 * @return string The untranslated string
 **/
function _u( $text, $domain = 'habari' )
{
	return HabariLocale::_u( $text, $domain );
}

?>

repo_name: DaSourcerer/habari


code:
<?php
/**
 * @package Habari
 *
 */

/**
 *
 * Habari HiEngine class
 *
 * The HiEngine is a subclass of the RawPHPEngine class
 * which is intended for those theme designers who want to use
 * simple {hi:*} tags four output instead of PHP
 *
 * To use this engine, specify "hiengine" in the theme.xml of a theme.
 *
 * This engine behaves exactly like RawPHPEngine after the template tags are
 * processed, so if an existing RawPHPEngine template is switched to HiEngine,
 * it should still run without issue.
 */
class HiEngine extends RawPHPEngine {
	/**
	 *
	 * Constructor for HiEngine
	 *
	 * Sets up the stream protocol handler
	 */
	public function __construct()
	{
		$streams = stream_get_wrappers();
		if ( ! in_array( 'hi' , $streams ) ) {
			stream_wrapper_register("hi", "HiEngineParser")
			or die(_t("Failed to register HiEngine stream protocol"));
		}
	}

	/**
	 *
	 * A function which outputs the result of a transposed
	 * template to the output stream
	 * @param template $ Name of template to display
	 */
	public function display($template)
	{
		extract($this->engine_vars);
		if ( $this->template_exists($template) ) {
			$template_file = isset($this->template_map[$template]) ? $this->template_map[$template] : null;
			$template_file = Plugins::filter('include_template_file', $template_file, $template, __CLASS__);
			$template_file = 'hi://' . $template_file;
			$fc = file_get_contents($template_file);
			eval('?>' . $fc);
			//include $template_file;  // stopped working properly in PHP 5.2.8
		}
	}
}

/**
 * HiEngineParser - A stream filtering class for the HiEngine
 */
class HiEngineParser
{
	protected $file;
	protected $filename;
	protected $position;
	protected $contexts;
	protected $strings = array();

	/**
	 * Open a HiEngineParser stream
	 *
	 * @param string $path Path of the opened resource, including the protocol specifier
	 * @param string $mode Mode used to open the file
	 * @param integer $options Bitmask options for opening this stream
	 * @param string $opened_path The actual path opened if using relative path, by reference
	 * @return boolean true on success
	 */
	function stream_open($path, $mode, $options, &$opened_path)
	{
		$this->filename = substr($path, 5);
		$this->file = file_get_contents($this->filename);

		// This processed value should cache and invalidate if a checksum of the template changes!  :)
		$this->file = $this->process($this->file);

		$this->position = 0;

		return true;
	}

	/**
	 * Read data from a HiEngineParser stream
	 *
	 * @param integer $count Number of characters to read from the current position
	 * @return string Characters read from the stream
	 */
	function stream_read($count)
	{
		if ( $this->stream_eof() ) {
			return false;
		}
		$ret = substr($this->file, $this->position, $count);
		$this->position += strlen($ret);
		return $ret;
	}

	/**
	 * Srite data to a HiEngineParser stream
	 *
	 * @param string $data Data to write
	 * @return boolean false, since this stream type is read-only
	 */
	function stream_write($data)
	{
		// HiEngineParser streams are read-only
		return false;
	}

	/**
	 * Report the position in the stream
	 *
	 * @return integer the position in the stream
	 */
	function stream_tell()
	{
		return $this->position;
	}

	/**
	 * Report whether the stream is at the end of the file
	 *
	 * @return boolean true if the file pointer is at or beyond the end of the file
	 */
	function stream_eof()
	{
		return $this->position >= strlen($this->file);
	}

	/**
	 * Seek to a specific position within the stream
	 *
	 * @param integer $offset The offset from the specified position
	 * @param integer $whence The position to seek from
	 * @return boolean true if seek was successful
	 */
	function stream_seek($offset, $whence)
	{
		switch ( $whence ) {
			case SEEK_SET:
				if ( $offset < strlen($this->file) && $offset >= 0 ) {
					$this->position = $offset;
					return true;
				}
				else {
					return false;
				}
				break;

			case SEEK_CUR:
				if ( $offset >= 0 ) {
					$this->position += $offset;
					return true;
				}
				else {
					return false;
				}
				break;

			case SEEK_END:
				if ( strlen($this->file) + $offset >= 0 ) {
					$this->position = strlen($this->file) + $offset;
					return true;
				}
				else {
					return false;
				}
				break;

			default:
				return false;
		}

	}

	/**
	 * Return fstat() info as required when calling stats on the stream
	 * @return array An array of stat info
	 */
	function stream_stat()
	{
		return array();
	}

	/**
	 * Process the template file for template tags
	 *
	 * @param string $template The template file contents
	 * @return string The processed template
	 */
	function process( $template )
	{
		$template = preg_replace_callback('/\{hi:(".+?")((?:\s*[\w\.]+){0,2})\s*\}/smu', array($this, 'hi_quote'), $template);
		$template = preg_replace_callback('%\{hi:([^\?]+?)\}(.+?)\{/hi:\1\}%ism', array($this, 'hi_loop'), $template);
		$template = preg_replace_callback('%\{hi:\?\s*(.+?)\}(.+?)\{/hi:\?\}%ismu', array($this, 'hi_if'), $template);
		$template = preg_replace_callback('%\{hi:(.+?)\}%i', array($this, 'hi_command'), $template);
		return $template;
	}

	/**
	 * Replace a single template tag with its PHP counterpart
	 *
	 * @param array $matches The match array found in HiEngineParser::process()
	 * @return string The PHP replacement for the template tag
	 */
	function hi_command($matches)
	{
		$cmd = trim($matches[1]);

		// Straight variable or property output, ala {hi:variable_name} or {hi:post.title}
		if ( preg_match('%^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff.]*$%i', $cmd) ) {
			$cmd = str_replace('.', '->', $cmd);
			if ( count($this->contexts) ) {
				// Build a conditional that checks for the most specific, then the least
				// eg.- $a->b->c->d->x, then $a->b->c->x, down to just $x
				$ctx = $this->contexts;
				$prefixes = array();
				foreach ( $ctx as $void ) {
					$prefixes[] = implode('->', $this->contexts);
					array_pop($ctx);
				}
				$output = '<?php echo ';
				foreach ( $prefixes as $prefix ) {
					$output .= '(is_object($' . $prefix . ') && !'.'is_null($' . $prefix . '->' . $cmd . ')) ? $' . $prefix . '->' . $cmd . ' : ';
				}
				$output .= '$' . $cmd . '; ?>';
				return $output;
			}
			else {
				return '<?php echo $'. $cmd . '; ?>';
			}
		}

		// Catch tags in the format {hi:command:parameter}
		if ( preg_match('/^(\w+):(.+)$/u', $cmd, $cmd_matches) ) {
			switch ( strtolower($cmd_matches[1]) ) {
				case 'area':
					return '<?php $theme->area(\'' . $cmd_matches[2] . '\'); ?>';
				case 'display':
					return '<?php $theme->display(\'' . $cmd_matches[2] . '\'); ?>';
				case 'option':
				case 'options':
					return '<?php Options::out(\'' . $cmd_matches[2] . '\'); ?>';
				case 'siteurl':
					return '<?php Site::out_url( \'' . $cmd_matches[2] . '\' ); ?>';
				case 'url':
					return '<?php URL::out( \'' . $cmd_matches[2] . '\' ); ?>';
				case 'session':
					switch($cmd_matches[2]) {
						case 'messages':
							return '<?php if(Session::has_messages()){Session::messages_out();} ?>';
						case 'errors':
							return '<?php if(Session::has_errors()){Session::messages_out();} ?>';
					}
				// this is an internal match
				case 'context':
					return $this->hi_to_var($cmd_matches[2]);
				case 'escape':
					return '<?php echo Utils::htmlspecialchars( ' . $this->hi_to_var( $cmd_matches[2] ) . ' ); ?>';
			}
		}

		// Use tags in the format {hi:@foo} as theme functions, ala $theme->foo();
		if ( $cmd[0] == '@' ) {
			return '<?php $theme->' . substr($cmd, 1) . '(); ?>';
		}

		// Didn't match anything we support so far
		return $matches[0];
	}

	/**
	 * Replace a loop tag section with its PHP counterpart, and add the context to the stack
	 *
	 * @param array $matches The match array found in HiEngineParser::process()
	 * @return string The PHP replacement for the template tag
	 */
	function hi_loop($matches)
	{
		$hivar = $matches[1];
		$phpvar = $this->hi_to_var($hivar);
		$iterator = strpos($hivar, '.') ? substr($hivar, strrpos($hivar, '.') + 1) : $hivar;
		$output = '<?php foreach(' . $phpvar . ' as $' . $iterator . '_index => $' . $iterator . '_1): ?>';
		$this->contexts[] = "{$iterator}_1";
		$output .= $this->process($matches[2]);
		$output .= '<?php endforeach; ?>';
		array_pop($this->contexts);
		return $output;
	}

	/**
	 * Replace variables in the hiengine syntax with PHP varaibles
	 * @param array $matches The match array found in hi_if()
	 * @returns string A PHP variable string to use as the replacement
	 */
	function var_replace($matches)
	{
		$var = $matches[1];

		if ( is_callable($var) ) {
			return $var;
		}
		if ( preg_match('/true|false|null|isset|empty/i', $var) ) {
			return $var;
		}

		$var = $this->hi_to_var($var);

		return $var;
	}

	function hi_to_var($hisyntax)
	{
		$var = str_replace('.', '->', $hisyntax);
		if ( count($this->contexts) ) {
			// Build a conditional that checks for the most specific, then the least
			// eg.- $a->b->c->d->x, then $a->b->c->x, down to just $x
			$ctx = $this->contexts;
			$prefixes = array();
			foreach ( $ctx as $void ) {
				$prefixes[] = implode('->', $this->contexts);
				array_pop($ctx);
			}
			$output = '';
			foreach ( $prefixes as $prefix ) {
				$output .= '(!is_null($' . $prefix . '->' . $var . ') ? $' . $prefix . '->' . $var . ' : ';
			}
			$output .= '$' . $var . ')';
			return $output;
		}
		else {
			return '$'. $var;
		}
	}

	/**
	 * Creates a table of static strings in hiengine expressions to be replaced in later
	 * @param array $matches The match found in hi_if()
	 * @returns string An uncommon string index for the stored static string.
	 */
	function string_stack($matches)
	{
		$key = chr(0) . count($this->strings) . chr(1);
		$this->strings[$key] = $matches[0];
		return $key;
	}

	/**
	 * Replace an if tag section with its PHP counterpart
	 *
	 * @param array $matches The match array found in HiEngineParser::process()
	 * @return string The PHP replacement for the template tag
	 */
	function hi_if($matches)
	{
		list($void, $eval, $context) = $matches;

		$eval = preg_replace_callback('/([\'"]).*?(?<!\\\\)\1/i', array($this, 'string_stack'), $eval);
		$eval = preg_replace_callback('/\b((?<!::)[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff.]*(?!::))\b/i', array($this, 'var_replace'), $eval);
		$eval = preg_replace('/(?<!=)=(?!=)/i', '==', $eval);
		$eval = str_replace(array_keys($this->strings), $this->strings, $eval);

		$context = preg_replace('/\{hi:\?else\?\}/i', '<?php else: ?>', $context);

		$output = '<?php if(' . $eval . '): ?>';
		$output .= $this->process($context);
		$output .= '<?php endif; ?>';
		return $output;
	}

	/**
	* Prepare strings for translation
	* @param array $matches Matches in HiEngineParser::process()
	* @param string The PHP replacement for the template tag
	*/
	function hi_quote($matches)
	{
		$args = preg_split('/\s+/u', trim($matches[2]));

		preg_match_all('/"(.+?)(?<!\\\\)"/', $matches[1], $quotes);
		$count = 0;
		$all_vars = array();
		foreach ( $quotes[1] as $index => $quote ) {
			preg_match_all('/{hi:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff.]*)}/', $quote, $vars, PREG_SET_ORDER);
			foreach ( $vars as $var ) {
				$count++;
				$quote = str_replace($var[0], '%'.$count.'\$s', $quote);
				$all_vars[] = '{hi:context:' . $var[1] . '}';  //$this->hi_to_var($var[1]);
			}
			$quotes[1][$index] = $quote;
		}
		if ( count($quotes[1]) > 1 ) {
			$output = '<?php printf(_n("'.$quotes[1][0].'", "'.$quotes[1][1].'", {hi:context:'.$args[0].'})';
			// Add vars
			if ( count($all_vars) > 0 ) {
				$output .= ', ' . implode(', ', $all_vars);
			}
			array_shift($args);
		}
		else {
			$output = '<?php _e("'.$quotes[1][0].'"';
			// Add vars
			if ( count($all_vars) > 0 ) {
				$output .= ', array(' . implode(', ', $all_vars) . ')';
			}
		}

		// Add the domain, if any
		if ( isset($args[0]) ) {
			$output .= ', "'.$args[0].'"';
		}

		// Close the tag
		$output .= '); ?>';

		return $output;
	}
}

?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * Tokenizer for HTML.
 * For use by HTMLParser.
 */
class HTMLTokenizer
{
	const NODE_TYPE_TEXT = 1;
	const NODE_TYPE_ELEMENT_OPEN = 2;
	const NODE_TYPE_ELEMENT_CLOSE = 3;
	const NODE_TYPE_PI = 4;
	const NODE_TYPE_COMMENT = 5;
	const NODE_TYPE_CDATA_SECTION = 6;
	const NODE_TYPE_STATEMENT = 7;

	/* States of the Machine ;p */
	private static $STATE_FINISHED = -1;
	private static $STATE_START = 0;
	private static $STATE_TAG = 1;
	private static $STATE_ELEMENT_OPEN = 2;
	private static $STATE_ELEMENT_CLOSE = 3;
	private static $STATE_STATEMENT = 4;
	private static $STATE_PI = 5;

	/* Character Ranges */
	private static $CHR_TAG_BEGIN = '<';
	private static $CHR_TAG_END = '>';
	private static $CHR_TAG_END_TRIM = '/';
	private static $CHR_ATTRNAME_END = '=';
	private static $CHR_WHITESPACE = " \t\r\n"; // SP, TAB, CR, LF

	private $html;
	private $pos;
	private $len;

	private $state;

	private $nodes;

	public function __construct( $html, $escape = true )
	{
		$this->html = $html;
		$this->len = strlen( $html );
		$this->pos = 0;
		$this->nodes = new HTMLTokenSet($escape);

		$this->state = self::$STATE_START;
	}

	public function parse()
	{
		while ( $this->has_more() && $this->state != self::$STATE_FINISHED )
		{
			switch ( $this->state ) {
				case self::$STATE_START:
					$this->state = $this->parse_start();
					break;
				case self::$STATE_TAG:
					$this->state = $this->parse_tag();
					break;
				case self::$STATE_ELEMENT_OPEN:
					$this->state = $this->parse_element_open();
					break;
				case self::$STATE_ELEMENT_CLOSE:
					$this->state = $this->parse_element_close();
					break;
				case self::$STATE_STATEMENT:
					$this->state = $this->parse_statement();
					break;
				case self::$STATE_PI:
					$this->state = $this->parse_pi();
					break;
				default:
					Error::raise( sprintf( _t('Invalid state %d in %s->parse()'), $this->state, __CLASS__ ) );
					$this->state = self::$STATE_FINISHED;
					break;
			}
		}

		return $this->nodes;
	}

	public function has_more()
	{
		return ( $this->pos < $this->len );
	}

	private function node( $type, $name, $value, $attrs )
	{
		$this->nodes[] = array(
			'type' => $type,
			'name' => $name,
			'value' => $value,
			'attrs' => $attrs,
		);
	}

	private function dec( $n = 1 )
	{
		$this->pos -= $n;
	}

	private function inc( $n = 1 )
	{
		$this->pos += $n;
	}

	private function get()
	{
		if ( $this->has_more() ) {
			return $this->html{ $this->pos++ };
		}

		return NULL;
	}

	private function peek()
	{
		return $this->html{ $this->pos };
	}

	private function up_to_str( $str )
	{
		$pos = $this->pos;
		$this->pos = strpos( $this->html, $str, $pos );
		if ( $this->pos === FALSE ) {
			// finish
			$this->pos = $this->len;
		}

		return substr( $this->html, $pos, $this->pos - $pos );
	}

	private function up_to_chr( $chr )
	{
		$pos = $this->pos;
		$seg_len = strcspn( $this->html, $chr, $pos );
		$this->pos += $seg_len;

		return substr( $this->html, $pos, $seg_len );
	}

	private function skip_whitespace()
	{
		$this->pos += strspn( $this->html, self::$CHR_WHITESPACE, $this->pos );
	}

	private function parse_start()
	{
		$data = $this->up_to_str( self::$CHR_TAG_BEGIN );
		$this->inc();
		if ( $data != '' ) {
			$this->node( self::NODE_TYPE_TEXT, '#text', $data, NULL );
		}

		return self::$STATE_TAG;
	}

	private function parse_attributes()
	{
		$attr = array();
		$name = '';

		$this->skip_whitespace();

		// read attribute name
		while ( $name = $this->up_to_chr( self::$CHR_ATTRNAME_END . self::$CHR_TAG_END . self::$CHR_WHITESPACE ) ) {
			$name = strtolower( rtrim( $name, self::$CHR_TAG_END_TRIM ) );
			// skip any whitespace
			$this->skip_whitespace();
			// first non-ws char
			$char = $this->get();
			if ( $char == '=' ) {
				// attribute value follows
				$this->skip_whitespace();
				$char = $this->get();
				if ( $char == '"' ) {
					// double-quoted
					$value = $this->up_to_str( '"' );
					$this->inc();
				}
				elseif ( $char == '\'' ) {
					// single-quoted
					$value = $this->up_to_str( '\'' );
					$this->inc();
				}
				else {
					// bad, bad, bad
					$this->dec();
					$value = $this->up_to_chr( self::$CHR_WHITESPACE . '>' );
				}
			}
			elseif ( $char !== NULL ) {
				// TODO HTMLParser should handle #IMPLIED attrs
				$value = NULL;
				$this->dec();
			}
			else {
				// default
				$value = NULL;
			}
			// store that attribute only if it's not empty
			if ( $name ) {
				$attr[$name] = $value;
			}
			$this->skip_whitespace();
		}

		return $attr;
	}

	private function parse_tag()
	{
		switch ( $this->get() ) {
			case '!':
				return self::$STATE_STATEMENT;
				break;
			case '?':
				// mmmh, PI
				return self::$STATE_PI;
				break;
			case '/':
				return self::$STATE_ELEMENT_CLOSE;
				break;
			default:
				// we just ate the first char of the tagName, oops
				$this->dec();
				return self::$STATE_ELEMENT_OPEN;
		}
	}

	private function parse_element_open()
	{
		$tag = rtrim( $this->up_to_chr( self::$CHR_TAG_END . self::$CHR_WHITESPACE ), self::$CHR_TAG_END_TRIM );
		if ( $tag != '' ) {
			$attr = $this->parse_attributes();
			$char = $this->get();
			if ( $char == '/' && $this->peek() == '>' ) {
				$this->inc(); // skip peeked '>'
				// empty tag in collapsed form (<br />)
				// XXX mark this somehow?
				$this->node( self::NODE_TYPE_ELEMENT_OPEN, $tag, NULL, $attr );
				$this->node( self::NODE_TYPE_ELEMENT_CLOSE, $tag, NULL, NULL );
			}
			else {
				$this->node( self::NODE_TYPE_ELEMENT_OPEN, $tag, NULL, $attr );
			}
		}

		return self::$STATE_START;
	}

	private function parse_element_close()
	{
		$tag = $this->up_to_chr( self::$CHR_TAG_END );

		if ( $tag != '' ) {
			$char = $this->get();
			if ( $char == '/' && $this->peek() == '>' ) {
				$this->inc();
			}

			$this->node( self::NODE_TYPE_ELEMENT_CLOSE, $tag, NULL, NULL );
		}

		return self::$STATE_START;
	}

	private function parse_statement()
	{
		// everything starting with <!
		$nodeName = '#statement';
		$nodeType = self::NODE_TYPE_STATEMENT;

		$char = $this->get();
		if ( $char == '[' ) {
			// CDATA
			// <http://www.w3.org/TR/DOM-Level-2-Core/core.html>
			$nodeName = '#cdata-section';
			$nodeType = self::NODE_TYPE_CDATA_SECTION;

			$this->inc( 6 ); // strlen( 'CDATA[' )
			$data = $this->up_to_str( ']]>' );
			$this->inc( 2 ); // strlen( ']]' )
		}
		elseif ( $char == '-' && $this->peek() == '-' ) {
			// comment
			$nodeName = '#comment';
			$nodeType = self::NODE_TYPE_COMMENT;

			// skip peeked -
			$this->inc();
			// consume text
			$data = $this->up_to_str( '-->' );
			$data = $data; // should trim() upstream
			// skip over final --
			$this->inc( 2 );
		}
		else {
			// some other kind of statement
			$this->dec();
		}

		if ( $nodeType == self::NODE_TYPE_STATEMENT ) {
			$data = '';
			$nodeName = $this->up_to_chr( self::$CHR_TAG_END . self::$CHR_TAG_END_TRIM . self::$CHR_WHITESPACE );
			if ( $this->peek() != '>' ) {
				// there be data or something
				$this->skip_whitespace();
				$data .= $this->up_to_chr( '[>' );
				if ( $this->peek() == '[' ) {
					// internal subset
					$data .= $this->get() . $this->up_to_str( ']' ) . $this->get();
				}
			}
			$data .= $this->up_to_str( '>' );
			// not like anyone uses them, eh?
		}

		// skip over final '>'
		$this->inc();

		if ( $data != '' ) {
			$this->node( $nodeType, $nodeName, $data, NULL );
		}

		return self::$STATE_START;
	}

	private function parse_pi()
	{
		$target = $this->up_to_chr( self::$CHR_TAG_END . self::$CHR_WHITESPACE );
		$data = $this->up_to_chr( self::$CHR_TAG_END );
		// skip over closing tag
		$this->inc( 1 );

		$this->node( self::NODE_TYPE_PI, $target, $data, array() );

		return self::$STATE_START;
	}

}

?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * HTML Token Set (created by @see HTMLTokenizer)
 */
class HTMLTokenSet implements Iterator, ArrayAccess
{
	protected $tokens = array();

	protected $sliceOffsetBegin  = null;
	protected $sliceOffsetLength = null;

	public $escape;

	public function __construct($escape = true)
	{
		$this->escape = $escape;
	}

	public function __tostring()
	{
		$out = '';
		foreach ( $this->tokens as $token ) {
			$out .= self::token_to_string($token, $this->escape);
		}
		return $out;
	}

	public static function token_to_string( array $token, $escape = true )
	{
		switch ( $token['type'] ) {
			case HTMLTokenizer::NODE_TYPE_TEXT:
				return $escape ? Utils::htmlspecialchars($token['value']) : $token['value'];
				break;

			case HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN:
				$out  = '<' . $token['name'];
				if ( isset( $token['attrs'] ) && is_array( $token['attrs'] ) ) {
					foreach ( $token['attrs'] as $attr => $attrval ) {
						$out .= " {$attr}=\"";
						if ( $escape ) {
							$out .= Utils::htmlspecialchars( html_entity_decode( $attrval, ENT_QUOTES, 'UTF-8' ) );
						}
						else {
							$out .= html_entity_decode( $attrval, ENT_QUOTES, 'UTF-8' );
						}
						$out .= '"';
					}
				}
				$out .= '>';
				break;

			case HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE:
				$out = "</{$token['name']}>";
				break;

			case HTMLTokenizer::NODE_TYPE_PI:
				$out = "<?{$token['name']}{$token['value']}>";
				break;

			case HTMLTokenizer::NODE_TYPE_COMMENT:
				$out = "<!--{$token['value']}-->";
				break;

			case HTMLTokenizer::NODE_TYPE_CDATA_SECTION:
				$out = "<![CDATA[{$token['value']}]]>";
				break;

			case HTMLTokenizer::NODE_TYPE_STATEMENT:
				$out = "<!{$token['name']}";
				if ( !empty($token['value']) ) {
					$out .= " {$token['value']}";
				}
				$out .= ">";
				break;
		}
		return $out;
	}

	public function get_end_offset()
	{
		return $this->sliceOffsetBegin + $this->sliceOffsetLength;
	}

	/**
	 * Fetch a section of the tokens, based on passed criteria
	 */
	public function slice( $names, array $attr = null )
	{
		$names = (array)$names;
		$ret = array();
		foreach ( $names as $name ) {
			$offset = 0;
			$slices = array();
			while ( $slice = $this->find_slice( $offset, $name, $attr ) ) {
				$slices[] = $slice;
				$offset = $slice->get_end_offset();
			}
			// Meed to reverse this because we need to splice the last chunks first
			// if we splice the earlier chunks first, then the offsets get all
			// messed up. Trust me.
			$ret = array_merge($ret, array_reverse( $slices ));
		}
		return $ret;
	}

	protected function find_slice( $offset, $name, array $attr)
	{
		// find start:
		$foundStart = false;
		for ( ; $offset < count($this->tokens); $offset++) {
			// short circuit if possible
			if ( $this->tokens[$offset]['type'] != HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN ) {
				continue;
			}
			if ( $this->tokens[$offset]['name'] != $name ) {
				continue;
			}

			// check attributes
			if ( !count($attr) ) {
				$foundStart = true;
				break; // To: FOUNDSTARTBREAKPOINT
			}
			foreach ( $attr as $compareName => $compareVal ) {
				if ( isset( $this->tokens[$offset]['attrs'][$compareName] ) &&
						stripos( $this->tokens[$offset]['attrs'][$compareName], $compareVal ) !== false ) {
					$foundStart = true;
					break 2; // To: FOUNDSTARTBREAKPOINT
				}
			}
		}
		// Fake label: FOUNDSTARTBREAKPOINT

		// short circuit if possible:
		if ( !$foundStart ) {
			return false;
		}

		$startOffset = $offset;

		// find the closing tag
		// (keep a stack so we don't mistake a nested node for this closing node)
		$stackDepth = 0;
		$foundEnd = false;
		for ( ; $offset < count( $this->tokens ) ; $offset++ ) {
			switch ( $this->tokens[$offset]['type'] ) {
				case HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN:
					if ( $this->tokens[$offset]['name'] == $name ) {
						++$stackDepth;
					}
					break;
				case HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE:
					if ( $this->tokens[$offset]['name'] == $name ) {
						--$stackDepth;
					}
					break;
				// default: skip
			}
			if ( $stackDepth <= 0 ) {
				$foundEnd = true;
				break;
			}
		}

		// short circuit if possible:
		if ( !$foundEnd ) {
			return false;
		}

		$offsetLength = $offset - $startOffset + 1;

		// now, place the found set into a new HTMLTokenSet:
		$slice = new HTMLTokenSet($this->escape);
		$slice->sliceOffsetBegin  = $startOffset;
		$slice->sliceOffsetLength = $offsetLength;
		$slice->tokens = array_slice($this->tokens, $slice->sliceOffsetBegin, $slice->sliceOffsetLength);
		return $slice;
	}

	public function trim_container()
	{
		$this->tokens = array_slice($this->tokens, 1, -1);
	}

	public function replace_slice( HTMLTokenSet $slice )
	{
		array_splice(
			$this->tokens,
			$slice->sliceOffsetBegin,
			$slice->sliceOffsetLength,
			$slice->tokens
		);
	}

	public function tokenize_replace( $source )
	{
		$ht = new HTMLTokenizer( $source, $this->escape );
		$this->tokens = $ht->parse()->tokens;
		return $this->tokens;
	}

	////////////////////////////////////////////////////

	// Iterator implemetation:

	public function rewind()
	{
		reset( $this->tokens );
	}

	public function current()
	{
		return current( $this->tokens );
	}

	public function key()
	{
		return key( $this->tokens );
	}

	public function next()
	{
		return next( $this->tokens );
	}

	public function valid()
	{
		return $this->current() !== false;
	}

	// ArrayAccess implementation

	public function offsetExists( $offset )
	{
		return isset( $this->tokens[ $offset ] );
	}

	public function offsetGet( $offset )
	{
		return $this->tokens[ $offset ];
	}

	public function offsetSet( $offset, $value )
	{
		if ( $offset === null) {
			$this->tokens[] = $value;
		}
		else {
			$this->tokens[ $offset ] = $value;
		}
	}

	public function offsetUnset( $offset )
	{
		unset( $this->tokens[ $offset ] );
	}
}
?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * This interface should be applied to plugins that implement importing
 * from other blogging tools.
 *
 */
interface importer
{

	/**
	 * Return a list of names of things that this importer imports
	 *
	 * @return array List of importables.
	 */
	public function filter_import_names($import_names);

	/**
	 * Return the page content for a specific stage of the import
	 *
	 * @return string Content of import stage
	 */
	public function filter_import_stage($stageoutput, $import_name, $stage, $step);

}

?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * Object metadata
 *
 */
class InfoObject extends InfoRecords
{

	function __construct ( $params )
	{
		// Don't call the parent constructor if this is read-only
		foreach ( $params as $key => $value ) {
			$this->$key = $value;
		}
	}
}
?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * Habari InfoRecords Class
 *
 * Base class for managing metadata about various Habari objects
 *
 */
abstract class InfoRecords implements URLProperties
{
	// the info array
	protected $__inforecord_array = array();
	// table which contains the info records
	protected $_table_name;
	// name of the primary key in the info record table
	protected $_key_name;
	// value of the primary key - the master record
	protected $_key_value;
	// set to TRUE only when the inforecords have been loaded
	protected $_loaded = FALSE;

	protected $url_args;

	/**
	 * Takes three parameters. The table of the options table, the name of the master key  and the record_id for which options are managed.
	 * posts take a slug as a record_id, comments take a comment_id and users take a user_id (user_name)
	 *
	 * <b>IMPORTANT:</b> if <tt>$primary_key_value</tt> is not set in the constructor, set_key MUST be called before inforecords can be set. Or bad things
	 * (think swarms of locusts o'er the land) will happen
	 *
	 * @param string $table_name name of the table to insert info (use the DB::o()->table_name syntax)
	 * @param string $key_name name of the primary key (for example "post_id")
	 * @param mixed $key_value (optional) the master record key value (for example, info for post_id = 1 managed by setting this param to 1). Use
	 *		set_key method if not set here.
	 **/
	public function __construct( $table_name, $key_name, $key_value = NULL )
	{
		$this->_table_name = $table_name;
		$this->_key_name = $key_name;
		$this->_key_value = $key_value;
		$this->_loaded = FALSE;
	}

	/**
	 * Test if the master record value has been set (and thus, safe to set info records).
	 *
	 * @return boolean TRUE if master record value has been set already, FALSE otherwise
	 **/
	public function is_key_set()
	{
		return isset( $this->_key_value );
	}

	/**
	 * function set_key
	 * For use in cases where the master record key is not known at the time of object instantiation (ie: a new post)
	 *
	 * @param mixed $metadata_key the id of the master record (could be int or string, most likely int)
	 *
	 **/
	public function set_key( $metadata_key )
	{
		$this->_key_value = $metadata_key;
	}

	/**
	 * Populate the internal hashmap with the values from the DB.
	 */
	protected function _load()
	{
		if ( $this->_loaded == TRUE ) {
			return;
		}
		if ( empty($this->_key_value) ) {
			$this->_loaded == TRUE;
			return;
		}

		// This InfoRecord is read-only?
		if(empty($this->_table_name)) {
			$this->_loaded == TRUE;
			return;
		}

		$result = DB::get_results( '
			SELECT name, value, type
			FROM ' . $this->_table_name . '
			WHERE ' . $this->_key_name . ' = ?',
			array( $this->_key_value )
		);

		foreach ( $result as $result_element ) {
			// XXX is this logic right?
			if ( $result_element->type == 1 ) {
				$this->__inforecord_array[$result_element->name] = array('value'=>unserialize($result_element->value));
			}
			else {
				$this->__inforecord_array[$result_element->name] = array('value'=>$result_element->value);
			}
		}

		$this->_loaded = TRUE;
	}

	/**
	 * Fetch info record value.
	 * @param string $name Name of the key to get
	 * @return mixed Stored value for specified key
	 **/
	public function __get ( $name )
	{
		$this->_load();
		if ( ! isset( $this->__inforecord_array[$name] ) ) {
			return false;
		}
		return $this->__inforecord_array[$name]['value'];
	}

	/**
	 * Update the info record.
	 * The value will not be stored in the database until calling $this->commit();
	 *
	 * @param string $name Name of the key to set
	 * @param mixed $value Value to set
	 **/
	public function __set( $name, $value )
	{
		$this->_load();
		$this->__inforecord_array[$name] = array('changed'=>true, 'value'=>$value);
	}

	/**
	 * Test for the existence of specified info value
	 *
	 * @param string $name Name of the option to set
	 * @return boolean TRUE if the info option exists, FALSE in all other cases
	 **/
	public function __isset ( $name )
	{
		$this->_load();
		return isset( $this->__inforecord_array[$name] );
	}

	/**
	 * Remove an info option; immediately unsets from the storage AND removes from database. Use with caution.
	 *
	 * @param string $name Name of the option to unset
	 * @return boolean TRUE if the option is successfully unset, FALSE otherwise
	 **/
		public function __unset( $name )
	{
		$this->_load();
		if ( isset( $this->__inforecord_array[$name] ) ) {
			DB::delete( $this->_table_name, array ( $this->_key_name => $this->_key_value, "name"=> $name ) );
			unset( $this->__inforecord_array[$name] );
			return true;
		}
		return false;
	}

	/**
	 * Returns a set of properties used by URL::get to create URLs
	 * @return array Properties of this post used to build a URL
	 **/
	public function get_url_args()
	{
		if ( !$this->url_args ) {
			$this->_load();
			$this->url_args = array_map( create_function( '$a', 'return $a["value"];' ), $this->__inforecord_array );
		}
		return $this->url_args;
	}

	/**
	 * Remove all info options. Primarily used when deleting the parent object.
	 * I.E. when deleting a user, the delete method would call this.
	 *
	 * @return boolean TRUE if the options were successfully unset, FALSE otherwise
	 **/
	public function delete_all()
	{
		// This InfoRecord is read-only?
		if(empty($this->_table_name)) {
			return;
		}
		$result = DB::query( '
			DELETE FROM ' . $this->_table_name . '
			WHERE ' . $this->_key_name . ' = ?',
			array( $this->_key_value )
		);
		if ( Error::is_error( $result ) ) {
			$result->out();
			return false;
		}
		$this->__inforecord_array = array();
		return true;
	}

	/**
	 * Commit all of the changed info options to the database.
	 * If this function is not called, then the options will not be written.
	 *
	 * @param mixed $metadata_key (optional) Key to use when writing info data.
	 */
	public function commit( $metadata_key = null )
	{
		if ( isset( $metadata_key ) ) {
			$this->_key_value = $metadata_key;
		}
		// If the info is not already loaded, and the key value is empty,
		// then we don't have enough info to do the commit
		if ( !$this->_loaded && empty($this->_key_value) ) {
			return true;
		}

		foreach( (array)$this->__inforecord_array as $name=>$record ) {
			if( isset( $record['changed'] ) && $record['changed'] ) {
				$value = $record['value'];
				if ( is_array( $value ) || is_object( $value ) ) {
					$result = DB::update(
						$this->_table_name,
						array(
							$this->_key_name=>$this->_key_value,
							'name'=>$name,
							'value'=>serialize($value),
							'type'=>1
						),
						array('name'=>$name, $this->_key_name=>$this->_key_value)
					);
				}
				else {
					$result = DB::update(
						$this->_table_name,
						array(
							$this->_key_name=>$this->_key_value,
							'name'=>$name,
							'value'=>$value,
							'type'=>0
						),
						array('name'=>$name, $this->_key_name=> $this->_key_value)
					);
				}

				if ( Error::is_error( $result ) ) {
					$result->out();
				}
				$this->__inforecord_array[$name] = array('value'=>$value);
			}
		}
	}

}

?>


code:
<?php
/**
 * @package Habari
 *
 */

/**
 * Input filtering functions.
 *
 */
class InputFilter
{
	/**
	 * Allowed elements.
	 */
	private static $whitelist_elements = array(
		// http://www.w3.org/TR/html4/struct/global.html#h-7.5.4
		'div', 'span',
		// http://www.w3.org/TR/html4/struct/links.html#h-12.2
		'a',
		// http://www.w3.org/TR/html4/struct/text.html#h-9.2.1
		'strong', 'em', 'code', 'kbd', 'dfn', 'samp', 'var', 'cite', 'abbr', 'acronym',
		// http://www.w3.org/TR/html4/struct/text.html#h-9.2.2
		'blockquote', 'q',
		// http://www.w3.org/TR/html4/struct/text.html#h-9.2.3
		'sub', 'sup',
		// http://www.w3.org/TR/html4/struct/text.html#h-9.3.1
		'p',
		// http://www.w3.org/TR/html4/struct/text.html#h-9.3.2.1
		'br',
		// http://www.w3.org/TR/html4/struct/text.html#h-9.3.4
		'pre',
		// http://www.w3.org/TR/html4/struct/text.html#h-9.4
		'ins', 'del',
		// http://www.w3.org/TR/html4/struct/lists.html#h-10.2
		'ol', 'ul', 'li',
		// http://www.w3.org/TR/html4/struct/lists.html#h-10.3
		'dl', 'dt', 'dd',
		// http://www.w3.org/TR/html4/present/graphics.html#h-15.2.1
		'b', 'i', 'u', 's', 'tt',
		// http://www.w3.org/TR/html4/struct/global.html#h-7.5.5
		'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
		// http://www.w3.org/TR/html4/struct/global.html#h-7.5.6
		'address',
		// http://www.w3.org/TR/html4/struct/dirlang.html#h-8.2.4
		'bdo',
		// http://www.w3.org/TR/html4/struct/tables.html#h-11.2.1
		'table',
		// http://www.w3.org/TR/html4/struct/tables.html#h-11.2.2
		'caption',
		// http://www.w3.org/TR/html4/struct/tables.html#h-11.2.3
		'thead', 'tfoot', 'tbody',
		// http://www.w3.org/TR/html4/struct/tables.html#h-11.2.4
		'colgroup', 'col',
		// http://www.w3.org/TR/html4/struct/tables.html#h-11.2.5
		'tr',
		// http://www.w3.org/TR/html4/struct/tables.html#h-11.2.6
		'th', 'td',
		// http://www.w3.org/TR/html4/struct/objects.html#h-13.2
		'img',
		// http://www.w3.org/TR/html4/struct/objects.html#h-13.6.1
		'map', 'area',
		// http://www.w3.org/TR/html4/present/graphics.html#h-15.2.1 (the non-deprecated ones)
		'tt', 'i', 'b', 'big', 'small',
		// http://www.w3.org/TR/html4/present/graphics.html#h-15.3
		'hr',
		// http://www.w3.org/TR/html4/present/frames.html#h-16.2.1
		'frameset',
		// http://www.w3.org/TR/html4/present/frames.html#h-16.2.2
		'frame',
		// http://www.w3.org/TR/html4/present/frames.html#h-16.4.1
		'noframes',
		// http://www.w3.org/TR/html4/present/frames.html#h-16.5
		'iframe',

	);

	/**
	 * Allowed attributes and values.
	 */
	private static $whitelist_attributes = array(
		// attributes that are valid for ALL elements (a subset of coreattrs)
		// elements that only take coreattrs don't need to be listed separately
		'*' => array(
			'lang' => 'language-code',
			'xml:lang' => 'language-code', // this is our xhtml support... all of it
			'dir' => array( 'ltr', 'rtl', ),
			'title' => 'text',
		),
		// http://www.w3.org/TR/html4/struct/links.html#h-12.2
		'a' => array( 'href' => 'uri', ),
		// http://www.w3.org/TR/html4/struct/text.html#h-9.4
		'ins' => array( 'cite' => 'uri', 'datetime' => 'datetime', ),
		'del' => array( 'cite' => 'uri', 'datetime' => 'datetime', ),
		// http://www.w3.org/TR/html4/struct/text.html#h-9.2.2
		'blockquote' => array( 'cite' => 'uri', ),
		'q' => array( 'cite' => 'uri', ),
	);

	/**
	 * #EMPTY elements.
	 */
	private static $elements_empty = array(
		'img',
	);

	/**
	 * Protocols that are ok for use in URIs.
	 */
	private static $whitelist_protocols = array(
		'http', 'https', 'ftp', 'mailto', 'irc', 'news', 'nntp', 'callto', 'rtsp', 'mms', 'svn',
	);

	/**
	 * List of all defined named character entities in HTML 4.01 and XHTML.
	 */
	private static $character_entities = array(
		'nbsp', 'iexcl', 'cent', 'pound', 'curren', 'yen', 'brvbar', 'sect', 'uml',
		'copy', 'ordf', 'laquo', 'not', 'shy', 'reg', 'macr', 'deg', 'plusmn',
		'sup2', 'sup3', 'acute', 'micro', 'para', 'middot', 'cedil', 'sup1', 'ordm',
		'raquo', 'frac14', 'frac12', 'frac34', 'iquest', 'Agrave', 'Aacute', 'Acirc',
		'Atilde', 'Auml', 'Aring', 'AElig', 'Ccedil', 'Egrave', 'Eacute', 'Ecirc',
		'Euml', 'Igrave', 'Iacute', 'Icirc', 'Iuml', 'ETH', 'Ntilde', 'Ograve',
		'Oacute', 'Ocirc', 'Otilde', 'Ouml', 'times', 'Oslash', 'Ugrave', 'Uacute',
		'Ucirc', 'Uuml', 'Yacute', 'THORN', 'szlig', 'agrave', 'aacute', 'acirc',
		'atilde', 'auml', 'aring', 'aelig', 'ccedil', 'egrave', 'eacute', 'ecirc',
		'euml', 'igrave', 'iacute', 'icirc', 'iuml', 'eth', 'ntilde', 'ograve',
		'oacute', 'ocirc', 'otilde', 'ouml', 'divide', 'oslash', 'ugrave', 'uacute',
		'ucirc', 'uuml', 'yacute', 'thorn', 'yuml', 'fnof', 'Alpha', 'Beta', 'Gamma',
		'Delta', 'Epsilon', 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda', 'Mu',
		'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma', 'Tau', 'Upsilon', 'Phi', 'Chi',
		'Psi', 'Omega', 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta',
		'theta', 'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi', 'omicron', 'pi', 'rho',
		'sigmaf', 'sigma', 'tau', 'upsilon', 'phi', 'chi', 'psi', 'omega',
		'thetasym', 'upsih', 'piv', 'bull', 'hellip', 'prime', 'Prime', 'oline',
		'frasl', 'weierp', 'image', 'real', 'trade', 'alefsym', 'larr', 'uarr',
		'rarr', 'darr', 'harr', 'crarr', 'lArr', 'uArr', 'rArr', 'dArr', 'hArr',
		'forall', 'part', 'exist', 'empty', 'nabla', 'isin', 'notin', 'ni', 'prod',
		'sum', 'minus', 'lowast', 'radic', 'prop', 'infin', 'ang', 'and', 'or',
		'cap', 'cup', 'int', 'there4', 'sim', 'cong', 'asymp', 'ne', 'equiv', 'le',
		'ge', 'sub', 'sup', 'nsub', 'sube', 'supe', 'oplus', 'otimes', 'perp',
		'sdot', 'lceil', 'rceil', 'lfloor', 'rfloor', 'lang', 'rang', 'loz',
		'spades', 'clubs', 'hearts', 'diams', 'quot', 'amp', 'lt', 'gt', 'OElig',
		'oelig', 'Scaron', 'scaron', 'Yuml', 'circ', 'tilde', 'ensp', 'emsp',
		'thinsp', 'zwnj', 'zwj', 'lrm', 'rlm', 'ndash', 'mdash', 'lsquo', 'rsquo',
		'sbquo', 'ldquo', 'rdquo', 'bdquo', 'dagger', 'Dagger', 'permil', 'lsaquo',
		'rsaquo', 'euro',
	);
	private static $character_entities_re = '';

	/**
	 * Perform all filtering, return new string.
	 * @param string $str Input string.
	 * @return string Filtered output string.
	 */
	public static function filter( $str )
	{
		if ( !MultiByte::valid_data( $str ) ) {
			return '';
		}
		else {
			do {
				$_str = $str;
				$str = self::strip_nulls( $str );
				$str = self::strip_illegal_entities( $str );
				$str = self::filter_html_elements( $str );
			} while ( $str != $_str );

			return $str;
		}
	}

	public static function strip_nulls( $str )
	{
		$str = str_replace( '\0', '', $str );

		return $str;
	}

	/**
	 * Callback function for strip_illegal_entities, do not use.
	 * @access private
	 * @param array $m matches
	 */
	public static function _validate_entity( $m )
	{
		$is_valid = FALSE;

		// valid entity references have the form
		//   /&named([;<\n\r])/
		// for named entities, or
		//   /&#(\d{1,5}|[xX][0-9a-fA-F]{1,4})([;<\n\r])/
		// for numeric character references

		$e = trim( $m[1] );
		$r = $m[2];
		if ( $r == ';' ) {
			$r = '';
		}

		if ( $e{0} == '#' ) {
			$e = strtolower( $e );
			if ( $e{1} == 'x' ) {
				$e = hexdec( substr( $e, 2 ) );
			}
			else {
				$e = substr( $e, 1 );
			}

			// numeric character references may only have values in the range 0-65535 (16 bit)
			// we strip null, though, just for kicks
			$is_valid = ( intval( $e ) > 0 && intval( $e ) <= 65535 );

			if ( $is_valid ) {
				// normalize to decimal form
				$e = '#' . intval( $e ) . ';';
			}
		}
		else {
			if ( self::$character_entities_re == '' ) {
				self::$character_entities_re = ';(' . implode( '|', self::$character_entities ) . ');';
			}

			// named entities must be known
			$is_valid = preg_match( self::$character_entities_re, $e, $matches );

			// XXX should we map named entities to their numeric equivalents?

			if ( $is_valid ) {
				// normalize to name and nothing but the name... eh.
				$e = $matches[1] . ';';
			}
		}

		return $is_valid ? '&' . $e . $r : '';
	}

	public static function strip_illegal_entities( $str )
	{
		$str = preg_replace_callback( "/&([^;<\n\r]+)([;<\n\r])/", array( __CLASS__, '_validate_entity' ), $str );

		return $str;
	}

	/**
	 * This really doesn't belong here. It should also be done much better. This is a nasty, NASTY kludge.
	 */
	public static function parse_url( $url )
	{
		// result array
		$r = array(
			'scheme' => '',
			'host' => '',
			'port' => '',
			'user' => '',
			'pass' => '',
			'path' => '',
			'query' => '',
			'fragment' => '',
			//
			'is_relative' => FALSE,
			'is_pseudo' => FALSE,
			'is_error' => FALSE,
			//
			'pseudo_args' => '',
		);

		// Use PHP's parse_url to get the basics
		$r = array_merge( $r, parse_url( $url ) );

		$r['is_pseudo'] = !in_array( $r['scheme'], array( 'http', 'https', '' ) );
		$r['is_relative'] = ( $r['host'] == '' && !$r['is_pseudo'] );
		if( $r['is_pseudo'] ) {
			$r['pseudo_args'] = $r['path'];
			$r['path'] = '';
		}

		return $r;
	}

	/**
	 * Restore a URL separated by a parse_url() call.
	 * @param $parsed_url array An array as returned by parse_url()
	 */
	public static function glue_url( $parsed_url )
	{
		if ( ! is_array( $parsed_url ) ) {
			return FALSE;
		}

		$res = '';
		$res .= $parsed_url['scheme'];
		if ( $parsed_url['is_pseudo'] || in_array( strtolower( $parsed_url['scheme'] ), array( 'mailto', 'callto' ) ) ) {
			$res .= ':';
		}
		else {
			if ( ! $parsed_url['is_relative'] ) {
				$res .= '://';
			}
		}
		if ( $parsed_url['is_pseudo'] ) {
			$res .= $parsed_url['pseudo_args'];
		}
		else {
			// user[:pass]@
			if ( $parsed_url['user'] ) {
				$res .= $parsed_url['user'];
				if ( $parsed_url['pass'] ) {
					$res .= ':' . $parsed_url['pass'];
				}
				$res .= '@';
			}
			$res .= $parsed_url['host'];
			if ( !empty( $parsed_url['port'] ) ) {
				if ( array_key_exists( $parsed_url['scheme'], Utils::scheme_ports() ) && Utils::scheme_ports( $parsed_url['scheme'] ) == $parsed_url['port'] ) {
					// default port for this scheme, do nothing
				}
				else {
					$res .= ':' . $parsed_url['port'];
				}
			}
			if ( !empty( $parsed_url['path'] ) ) {
				$res .= $parsed_url['path'];
			}
			else {
				$res .= '/';
			}
			if ( $parsed_url['query'] ) {
				$res .= '?' . $parsed_url['query'];
			}
			if ( $parsed_url['fragment'] ) {
				$res .= '#' . $parsed_url['fragment'];
			}
		}

		return $res;
	}

	private static function check_attr_value( $k, $v, $type )
	{
		if ( is_array( $type ) ) {
			// array of allowed values, exact matches only
			return in_array( $v, $type, TRUE );
		}
		else {
			// data type
			switch ( $type ) {
				case 'uri':
					// RfC 2396 <http://www.ietf.org/rfc/rfc2396.txt>
					$bits = self::parse_url( $v );
					return $bits['is_relative'] || in_array( $bits['scheme'], self::$whitelist_protocols );
					break;
				case 'language-code':
					// RfC 1766 <http://www.ietf.org/rfc/rfc1766.txt>
					//    Language-Tag = Primary-tag *( "-" Subtag )
					//    Primary-tag = 1*8ALPHA
					//    Subtag = 1*8ALPHA
					return preg_match( '/^[a-zA-Z]{1,8}(?:-[a-zA-Z]{1,8})*$/i', $v );
					break;
				case 'text':
					// XXX is this sufficient?
					return is_string( $v );
					break;
				case 'datetime':
					// <http://www.w3.org/TR/1998/NOTE-datetime-19980827>
					// <http://www.w3.org/TR/html4/types.html#h-6.11>
					//    YYYY-MM-DDThh:mm:ssTZD
					return preg_match( '/^[0-9]{4}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9](?:Z|[\+-][0-2][0-9]:[0-5][0-9])$/', $v );
					break;
				default:
					Error::raise( sprintf( _t('Unkown attribute type "%s" in %s'), $type, __CLASS__ ) );
					return FALSE;
			}
		}
	}

	/**
	 * @todo TODO must build DOM to really properly remove offending elements
	 * @todo TODO properly filter URLs
	 */
	public static function filter_html_elements( $str )
	{
		$tokenizer = new HTMLTokenizer( $str );

		// tokenize, baby
		$tokens = $tokenizer->parse();

		// filter token stream
		$filtered = new HTMLTokenSet;
		$stack = array();
		foreach ( $tokens as $node ) {
			switch ( $node['type'] ) {
				case HTMLTokenizer::NODE_TYPE_TEXT:
					// XXX use blog charset setting
					$node['value'] = html_entity_decode( $node['value'], ENT_QUOTES, 'utf-8' );
					break;
				case HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN:
					// is this element allowed at all?
					if ( ! in_array( strtolower( $node['name'] ), self::$whitelist_elements ) ) {
						if ( ! in_array( strtolower( $node['name'] ), self::$elements_empty ) ) {
							array_push( $stack, $node['name'] );
						}
						//$node = NULL; //remove the node completely
						// convert the node to text
						$node = array(
							'type' => HTMLTokenizer::NODE_TYPE_TEXT,
							'name' => '#text',
							'value' => HTMLTokenSet::token_to_string($node),
							'attrs' => array(),
                                                );
					}
					else {
						// check attributes
						foreach ( $node['attrs'] as $k => $v ) {
							$attr_ok = (
								(
									   in_array( strtolower( $k ), self::$whitelist_attributes['*'] )
									|| ( array_key_exists( strtolower( $node['name'] ), self::$whitelist_attributes ) &&
									     array_key_exists( strtolower( $k ), self::$whitelist_attributes[strtolower( $node['name'] )] ) )
								)
								&& self::check_attr_value( strtolower( $k ), $v, self::$whitelist_attributes[strtolower( $node['name'] )][strtolower( $k )] )
							);
							if ( ! $attr_ok ) {
								unset( $node['attrs'][$k] );
							}
						}
					}
					break;
				case HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE:
					if ( ! in_array( strtolower( $node['name'] ), self::$whitelist_elements ) ) {
						if ( strtolower( $temp = array_pop( $stack ) ) !== strtolower( $node['name'] ) ) {
							// something weird happened (Luke, use the DOM!)
							array_push( $stack, $temp );
						}
						//$node = NULL;
						//convert the node to text
						$node = array(
							'type' => HTMLTokenizer::NODE_TYPE_TEXT,
							'name' => '#text',
							'value' => HTMLTokenSet::token_to_string($node),
							'attrs' => array(),
                                                );
					}
					break;
				case HTMLTokenizer::NODE_TYPE_PI:
				case HTMLTokenizer::NODE_TYPE_COMMENT:
				case HTMLTokenizer::NODE_TYPE_CDATA_SECTION:
				case HTMLTokenizer::NODE_TYPE_STATEMENT:
					$node = NULL;
					break;
				default:
			}

			if ( $node != NULL ) {
				$filtered[] = $node;
			}
		}

		// rebuild our output string
		return preg_replace( '#<([^>\s]+)(?:\s+[^>]+)?></\1>#u', '', (string) $filtered );
	}
}

?>