Your IP : 216.73.217.112


Current Path : /home/annegardxb/www/so-assurances/plugins/content/readlesstext/
Upload File :
Current File : /home/annegardxb/www/so-assurances/plugins/content/readlesstext/readlesstextmain.php

<?php
/**
 * @package readlesstext
 * @copyright 2008-2014 Parvus
 * @license http://www.gnu.org/licenses/gpl-3.0.html
 * @link http://joomlacode.org/gf/project/cutoff/
 * @author Parvus
 *
 * readless 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.
 *
 * readless 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 readless. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * @version $Id$
 */

defined( '_JEXEC' ) or die;
require_once 'readlesstextcache.php';
require_once 'readlesstextexpand.php';
require_once 'readlesstexthelper.php';
require_once 'readlesstextthumb.php';

if ( !/*NOT*/ class_exists( 'ContentHelperRoute' ) )
{
  /* There have been some reports of people that somehow got errors by having
   * this class not being included by default (not only with this plugin; also
   * with other completely unrelated plugins.
   * Although this block should never be entered, it serves as a workaround
   * for those people having this issue.
   */
  require_once( JPATH_SITE . '/components/com_content/helpers/route.php' );
}

class ReadLessTextMain extends JPlugin
{
  /**
   * Constructor
   * @param object &$subject The object to observe
   * @param array $config An optional associative array of configuration settings.
   */
  public function __construct( &$subject, $config = array() )
  {
    parent::__construct( $subject, $config );
  }

  /**
   * Main entry function. The configuration parameters set in the constructor are used to fully exert read less text's
   * functionality.
   * @param JTableContent $article The item/article being prepared for display.
   * @param number $callCount The number of times this function has been
   *   called by the same plugin. This value is used to determine whether
   *   the given article may be shortened.
   *   Will have been incremented by one when this function returns.
   * @param string $pluginName
   */
  public function ReadLessText( &$article, &$callCount, $pluginName = 'plg_content_readlesstext', $cacheTable = '#__readlesstext' )
  {
//    jimport( 'joomla.error.profiler' );
//    $this->profiler = new JProfiler();
//    $this->profiles = array();
//    $this->profiles[] = $this->profiler->mark( ' IN ' . $article->id );

    JPlugin::loadLanguage( $pluginName );

    /* The two blocks below are a workaround for issues in the Joomla core.
     * - In a 'featured' view, the fulltext is not set or empty, even when it
     *   is present in the database
     * - In a single article view, the readmore property is not set.
     * Seen in 1.6.6, 1.7.2 & 2.5.4
     *
     * It also ensures that when this plugin is invoked on other components (e.g. com_media),
     * There are no php notices.
     */
    foreach ( array( 'fulltext', 'readmore' ) as $var )
    {
      if ( !/*NOT*/ isset( $article->$var ) )
      {
        $article->$var = '';
      }
    }
    if ( isset( $article->id ) and $article->readmore and ( !/*NOT*/ $article->fulltext ) )
    {
      $db = JFactory::getDBO();
      $query = "SELECT c.fulltext
          FROM #__content c
          WHERE c.id = " . $article->id;
      $db->setQuery( $query );
      $article->fulltext = $db->loadResult();
    }

    $this->_isShortened = false;
    $this->_prefix = '';
    $this->_suffix = '';

    $alwaysActiveForGuests = $this->params->get( 'alwaysActiveForGuests', '0' );
    if ( $alwaysActiveForGuests )
    {
      $extraDiscoverInfo = 'Guests can only see the shortened article, unless a context in the <em>Disallowed</em> field matches this item.';
      $activeByDefaultOnAllContentItems = ( JFactory::getUser()->guest == 1 );
    }
    else
    {
      $extraDiscoverInfo = '';
      $activeByDefaultOnAllContentItems = false;
    }

//    $this->profiles[] = $this->profiler->mark( ' before filter ' );
    $options = array();
    if ( ReadLessTextHelper::Filter( $callCount, $article, $this->params, $options,
        $activeByDefaultOnAllContentItems, $pluginName, $extraDiscoverInfo ) )
    {
      /* Entering this block means: read less text must be active. */
//      $this->profiles[] = $this->profiler->mark( ' after filter ' );

      if ( $options[ 'discover' ] )
      {
        /* Discover mode is active: the article's text has been replaced with
         * discover information and may not be altered.
         * No need to update the private variables - they will not be used.
         */
      }
      else
      {
//        $this->profiles[] = $this->profiler->mark( ' before prepare ' );
        $length = $this->_PrepareArticleText( $article, $this->params, $cacheTable );
//        $this->profiles[] = $this->profiler->mark( ' after prepare ' );

        $imageHtml = '';
        $prefix = '';
        $shortened = $article->text;
        $inlineSuffix = '';
        $closingTags = '';
        $suffix = '';

        $expand = new ReadLessTextExpand();
        $expand->SetExpandables( $article, $this->_plaintext, Null, Null );
        $this->_GetParams( $article, $length, $expand );

        if ( ( $this->_applyFormatting == 'when_active' )
            or ( ( $this->_applyFormatting == 'when_long_enough' ) and $length ) )
        {
//          $this->profiles[] = $this->profiler->mark( ' before strip ' );
          $article->text = $this->_StripTokens( $article->text );
//          $this->profiles[] = $this->profiler->mark( ' after strip ' );

//          $this->profiles[] = $this->profiler->mark( ' before cutoff ' );
           $a = $this->_CutOff( $article->text, $length );
           $shortened = $a[0];
           $closingTags = $a[1];
//          $this->profiles[] = $this->profiler->mark( ' after cutoff ' );
        }

        if ( ( $this->_createThumbnail == 'when_active' )
            or ( ( $this->_createThumbnail == 'when_shortened' ) and $this->_isShortened ) )
        {
//          $this->profiles[] = $this->profiler->mark( ' before thumbnail ' );
          $imageHtml = $this->_GetThumbnailHtml( $article );
//          $this->profiles[] = $this->profiler->mark( ' after thumbnail ' );
        }

        if ( ( $this->_addPrefix == 'when_active' )
            or ( ( $this->_addPrefix == 'when_shortened' ) and $this->_isShortened ) )
        {
          $prefix = $this->_prefix;
        }
        if ( ( $this->_addInlineSuffix == 'when_active' )
            or ( ( $this->_addInlineSuffix == 'when_shortened' ) and $this->_isShortened ) )
        {
          $inlineSuffix = $this->_inlineSuffix;
        }
        if ( ( $this->_addSuffix == 'when_active' )
            or ( ( $this->_addSuffix == 'when_shortened' ) and $this->_isShortened ) )
        {
          $suffix = $this->_suffix;
        }

        if ( $this->_cache )
        {
          $this->_cache->Store();
        }

        $article->text = $this->_wrapperTag[ 'open' ] . $imageHtml . $prefix
            . $shortened
            . $inlineSuffix . $closingTags . $suffix . $this->_wrapperTag[ 'close' ];
      }

      $article->introtext = $article->text;
      $article->fulltext = '';

//       $this->profiles[] = $this->profiler->mark( ' OUT ' . $article->id );
//       $article->text .= '<div>';
//       foreach ( $this->profiles as $profile )
//       {
//         $article->text .= "<br/>" . $profile;
//       }
//       $article->text .= '<br/></div>';
//       $article->introtext = $article->text;
//       $article->fulltext = '';
    }
  }

  /**
   * Determines the html version of the intro and the full field,
   * irrespective the component.
   * @param JTableContent $article The item/article being prepared for display.
   * @return An array, with the first element containing the intro text,
   *   if any, and the second element the remainder of the text.
   */
  private static function _DetermineArticleText( $article )
  {
    /* Be extra careful, so that other components do not give PHP notices
     * and warnings.
     * - Some components do not have the introtext and/or the fulltext field
     *   but only the text field,
     *   e.g. ??
     * - Others have none of the three, but use a special name (why o why),
     *   e.g. in com_eventlist:
     *     datdescription for an event.
     *     catdescription for a category.
     *     locdescription for a venue.
     *     description for a group.
     * - whereas others have all three of them, and full info is to be
     *   fetched from introtext and fulltext.
     */
    $introtext = '';
    $fulltext = '';
    if ( isset( $article->introtext ) )
    {
      $introtext = $article->introtext;
    }
    if ( isset( $article->fulltext ) )
    {
      $fulltext = $article->fulltext;
    }
    if ( !/*NOT*/ $introtext and !/*NOT*/ $fulltext)
    {
      if ( isset( $article->text ) and $article->text )
      {
        $fulltext = $article->text;
      }
    }
    if ( !/*NOT*/ $introtext and !/*NOT*/ $fulltext)
    {
      /* No text has been found yet. Check if it is stored in a variable named
       * xxxdescription, like eventlist does.
       */
      $values = array();
      foreach ( get_object_vars( $article ) as $key => $value )
      {
        if ( strstr( $key, 'description') !== false )
        {
          $values[] = $value;
        }
      }
      /* Only accept the text from a xxxdescription when it is the only one
       * found: e.g. eventlist uses one object to store the text of both
       * the event and the venue, and it is impossible to choose the correct
       * one.
       */
      if ( count( $values ) == 1 )
      {
        $fulltext = $values[0];
      }
    }

    $readmoreStartPos = JString::strpos( $introtext, '<p class="readmore">' );
    if ( $readmoreStartPos !== false )
    {
      /* Another plugin has already tampered with this article, and somehow a readmore token got included.
       * e.g. NoNumber's ArticleAnywhere can cause this.
       * Consider only the part before that token as the intro text.
       */
      $introtext = JString::substr_replace( $introtext, '', $readmoreStartPos );
    }

    return array( $introtext, $fulltext );
  }

  /**
   * Determines the length of @c $this->_htmltext in the given unit.
   * The length is fetched from the cache. If it is not available in the cache, it is calculated and then added to the
   * cache - but not yet committed in the cache, use @c Store() for that.
   * @param string $lengthUnit One of 'char', 'word', 'sentence', 'paragraph'
   * @return int the requested length, in number of @c lengthUnit's
   */
  private function _DetermineLength( $string, $lengthUnit )
  {
    $length = $this->_cache->Get( $lengthUnit );
    if ( !/*NOT*/ $length )
    {
      $end = true; /* Just to define it; it is used as output parameter in the next call, but ignored by us. */
      $length = ReadLessTextHelper::DetermineLength( $this->_htmltext, $lengthUnit, $end );
      if ( $length )
      {
        $this->_cache->Set( $lengthUnit, $length );
      }
    }
    return $length;
  }

  /**
   * Determines the correct text to operate on. Sets the private variables plaintext and htmltext.
   * @param JTableContent $article IN OUT. The item/article being prepared for display. The properties introtext, text,
   *   fulltext and readmore will have been set or adapted.
   * @param params $params the parameters to use
   * @return mixed boolean false to indicate article may not be shortened, or a number indicating the nr of units to
   *   retain. It is still possible this number is greater than the entire article text length.
   * @post @c $article->text contains the text to be shortened or to display.
   * @note Even when @c false is returned, @c $article->text may have been changed - a manually inserted read more
   *   request is then overridden.
   */
  private function _PrepareArticleText( &$article, $params, $cacheTable )
  {
    $a = self::_DetermineArticleText( $article );
    $introtext = $a[0];
    $fulltext = $a[1];
    $plainintrotext = strip_tags( $introtext );
    $plainfulltext = strip_tags( $fulltext );
    $this->_plaintext = $plainintrotext . ' ' . $plainfulltext;
    $this->_htmltext = $introtext . ' ' . $fulltext;

    /* It is tempting to execute the line below, but this yields wrong results.
     * There are Russian (e.g. Р 0xd0 0xa0) and Greek (e.g. Π 0xce 0xa0) multi-byte characters
     * that are stripped from the 0xa0 byte, giving a rubbish character.
     * $this->_htmltext will - after further adaptation - eventually be output,
     * so all characters must at all times remain intact.
     * - btw, at the very least, the u modifier should be used. See e.g. ReadLessTextHelper::Rtrim()
     */
    //$this->_htmltext = preg_replace( '/\s+/muis', ' ', $this->_htmltext )

    $this->_cache = new ReadLessTextCache( $cacheTable, JRequest::getWord( 'option' ), $article->id,
        'v5.2 (r274)' . md5( $this->_htmltext ) );

    $this->_wordCount = $this->_DetermineLength( $this->_htmltext, 'word' );
    $this->_lengthUnit = $params->get( 'lengthUnit', 'char' );

    /* True when a 'read more' link has been explicitly inserted, and fulltext is not empty.
     * - the full text is made by combining introtext, (closing the paragraph), a
     *   hr tag (id 'system-readmore') (opening the paragraph) and fulltext.
     */
    $readmoreMarkerPresent = ( $fulltext and isset( $article->readmore ) and $article->readmore );
    if ( $readmoreMarkerPresent and ( $this->_respectExistingReadmoreLink == 'respectShowIntro' ) )
    {
      /* Shorten the article's intro text. */
      $article->text = $introtext;
      $cutOffLength = $params->get( 'cutOffTextLength', 1 );
    }
    else if ( $readmoreMarkerPresent and $this->_respectExistingReadmoreLink ) /* 1 / true */
    {
      /* Use the article's full intro text; do not try to shorten it. This is
       * accomplished by requesting to cut off at a ridiculously high value.
       */
      $article->text = $introtext;
      $this->_isShortened = true;
      $cutOffLength = PHP_INT_MAX;
    }
    else /* No read more present, or 0 / false */
    {
      $minimumLength = $params->get( 'minimumTextLength', 1 );
      $cutOffLength = $params->get( 'cutOffTextLength', 1 );
      $length = $this->_DetermineLength( $this->_htmltext, $this->_lengthUnit );
      if ( $length < $minimumLength )
      {
        $cutOffLength = false;
      }
      else if ( $length <= $cutOffLength )
      {
        $cutOffLength = false;
      }
      else
      {
        /* Shorten the article's full text. */
        $article->text = $this->_htmltext;
      }
    }

    $article->readmore = 0; /* We do not want Joomla to insert his own 'read more' token */
    if ( !/*NOT*/ $cutOffLength )
    {
      /* The article may not be shortened. Ensure the full article is present,
       * even when a manual to-be-ignored read more is present.
       */
      $article->text = $this->_htmltext;
    }

    $this->_wrapperTag = array( 'open' => '', 'close' => '' );
    $wrapperTag = $params->get( 'wrapperTag', '' );
    if ( $wrapperTag )
    {
      $wrapperClass = $params->get( 'wrapperClass', '' );
      if ( $wrapperClass )
      {
        $this->_wrapperTag[ 'open' ] = '<' . $wrapperTag . ' class="' . $wrapperClass . '">';
      }
      else
      {
        $this->_wrapperTag[ 'open' ] = '<' . $wrapperTag . '>';
      }

      $this->_wrapperTag[ 'close' ] = '</' . $wrapperTag . '>';
    }

    return $cutOffLength;
  }

  /**
   * Determines various options, which are stored in the private variables.
   * @note Not all private variables will be set. Those are covered in the @c _PrepareArticleText function
   * @param JTableContent $article RO. The item/article being prepared for display.
   * @params mixed $length Boolean false or a positive number. Indicates whether it is already determined if the article
   *   is to be shortened and how long the shortened article must become.
   * @param class $expand Instance of ReadLessTextExpand.
   * @pre _PrepareArticleText() must have been called beforehand: e.g. @c _plaintext is used (while expanding).
   */
  private function _GetParams( &$article, $length, $expand )
  {
    $this->_applyFormatting = $this->params->get( 'applyFormatting', 'when_active' );
    $this->_addPrefix = $this->params->get( 'addPrefix', 'when_active' );
    $this->_addInlineSuffix = $this->params->get( 'addInlineSuffix', 'when_active' );
    if ( $this->_addInlineSuffix == 'when_active_use_article_manager_option' )
    {
      if ( JComponentHelper::getParams( 'com_content' )->get( 'show_readmore' ) )
      {
        $this->_addInlineSuffix = 'when_active';
      }
      else
      {
        $this->_addInlineSuffix = 'no';
      }
    }
    $this->_addSuffix = $this->params->get( 'addSuffix', 'when_active' );
    if ( $this->_addSuffix == 'when_active_use_article_manager_option' )
    {
      if ( JComponentHelper::getParams( 'com_content' )->get( 'show_readmore' ) )
      {
        $this->_addSuffix = 'when_active';
      }
      else
     {
        $this->_addSuffix = 'no';
      }
    }
    $this->_respectExistingReadmoreLink = $this->params->get( 'respectExistingReadmoreLink', true );
    if ( $this->_respectExistingReadmoreLink == 'respectShowIntro')
    {
      if ( JComponentHelper::getParams( 'com_content' )->get( 'show_intro' ) )
      {
        $this->_respectExistingReadmoreLink = false;
      }
      else
      {
        /* The user does _not_ want to resepect an existing 'read more' token,
         * and likes to have the intro text separated from the full text after it.
         * Only shorten the intro text
         */
        $this->_respectExistingReadmoreLink = 'shortenIntroOnly';
      }
    }

    /* An array of tags or tokens which are used later on. */
    if ( ( $this->_applyFormatting == 'when_active' )
        or ( ( $this->_applyFormatting == 'when_long_enough' ) and $length ) )
    {
      $list = array(
          'extraSelfClosingTags',
          'tagsToRemove',
          'tagsToRemoveWithContents',
          'squareTokensToRemove',
          'curlyTokensToRemove',
          'squareTokensToRemoveWithContents',
          'curlyTokensToRemoveWithContents' );
    }
    else
    {
      $list = array(
          'extraSelfClosingTags');
    }
    foreach ( $list as $parameter )
    {
      $string = JString::strtolower( $this->params->get( $parameter, '' ) );
      $string = JString::str_ireplace( ' ', '', $string );
      if ( $string )
      {
        $parameter = '_' . $parameter;
        $this->$parameter = explode( ',', $string );
      }
    }

    $this->_articleUrl = JRoute::_( ContentHelperRoute::getArticleRoute(
        ReadLessTextHelper::GetArticleSlug( $article ), ReadLessTextHelper::GetCategorySlug( $article ) ) );
    $expand->SetExpandables( Null, Null, Null, array( '{url}' => $this->_articleUrl, '{words}' => $this->_wordCount ) );

    /* prefix */
    $translateAdditions = $this->params->get( 'translateAdditions', false );
    $prefixLinksToFullArticle = false;
    if ( JFactory::getUser()->guest == 0 )
    {
      $this->_prefix = $this->params->get( 'userPrefix', '' );
      $prefixLinksToFullArticle = $this->params->get( 'userPrefixLinksToFullArticle', true );
    }
    else
    {
      $this->_prefix = $this->params->get( 'guestPrefix', '' );
      $prefixLinksToFullArticle = $this->params->get( 'guestPrefixLinksToFullArticle', true);
    }
    if ( $translateAdditions )
    {
      $this->_prefix = JText::_( $this->_prefix );
    }
    $expand->SetExpandables( Null, Null, $this->params->get( 'prefixDateFormat', '%m/%d' ), null );
    $this->_prefix = $expand->expand( $this->_prefix );
    if ( $prefixLinksToFullArticle and $this->_prefix )
    {
      $this->_prefix = '<a href="' . $this->_articleUrl . '">' . $this->_prefix . '</a>';
    }

    /* inline suffix */
    $inlineSuffixLinksToFullArticle = false;
    if ( JFactory::getUser()->guest == 0 )
    {
      $this->_inlineSuffix = $this->params->get( 'userInlineSuffix', '' );
      $inlineSuffixLinksToFullArticle = $this->params->get( 'userInlineSuffixLinksToFullArticle', true );
    }
    else
    {
      $this->_inlineSuffix = $this->params->get( 'guestInlineSuffix', '' );
      $inlineSuffixLinksToFullArticle = $this->params->get( 'guestInlineSuffixLinksToFullArticle', true );
    }
    if ( $translateAdditions )
    {
      $this->_inlineSuffix = JText::_( $this->_inlineSuffix );
    }
    $expand->SetExpandables( Null, Null, $this->params->get( 'suffixDateFormat', '%m/%d' ), null );
    $this->_inlineSuffix = $expand->expand( $this->_inlineSuffix );
    if ( $inlineSuffixLinksToFullArticle and $this->_inlineSuffix )
    {
      $this->_inlineSuffix = '<a href="' . $this->_articleUrl . '">' . $this->_inlineSuffix . '</a>';
    }

    /* suffix */
    $suffixLinksToFullArticle = false;
    if ( JFactory::getUser()->guest == 0 )
    {
      $this->_suffix = $this->params->get( 'userSuffix', '' );
      $suffixLinksToFullArticle = $this->params->get( 'userSuffixLinksToFullArticle', true );
    }
    else
    {
      $this->_suffix = $this->params->get( 'guestSuffix', '' );
      $suffixLinksToFullArticle = $this->params->get( 'guestSuffixLinksToFullArticle', true );
    }
    if ( $translateAdditions )
    {
      $this->_suffix = JText::_( $this->_suffix );
    }
    $expand->SetExpandables( Null, Null, $this->params->get( 'suffixDateFormat', '%m/%d' ), null );
    $this->_suffix = $expand->expand( $this->_suffix );
    if ( $suffixLinksToFullArticle and $this->_suffix )
    {
      $this->_suffix = '<a href="' . $this->_articleUrl . '">' . $this->_suffix . '</a>';
    }

    /* Use the title or one of the -fixes as thumbnail tooltip. */
    switch ( $this->params->get( 'thumbnailTitle', 'articleTitle' ) )
    {
      case '0':
        $this->_thumbnailTitle = '';

      case 'prefix':
        $this->_thumbnailTitle = $this->_prefix;
        break;

      case 'suffix':
        $this->_thumbnailTitle = $this->_suffix;
        break;

      case 'inlineSuffix':
        $this->_thumbnailTitle = $this->_inlineSuffix;
        break;

      case 'articleTitle':
      default:
        $this->_thumbnailTitle = $article->title;
        break;
    }
    $this->_thumbnailTitle = strip_tags( $this->_thumbnailTitle );

    $this->_createThumbnail = $this->params->get( 'createThumbnail', 'when_active' );
    $this->_linkThumbnail = $this->params->get( 'linkThumbnail', true );
    $this->_defaultThumbnail = $this->params->get( 'defaultThumbnailTemplate', '' );
    $this->_defaultThumbnail = $expand->expand( $this->_defaultThumbnail );

    $this->_retainWholeWords = $this->params->get( 'retainWholeWords', false );

    $this->_crop[ 'horizontal_position' ] = $this->params->get( 'cropHorizontalPosition', 'no' );
    $this->_crop[ 'vertical_position' ] = $this->params->get( 'cropVerticalPosition', 'no' );

    $this->_cacheTime = $this->params->get( 'thumbCacheTime', 2419200 /* 4 weeks */ );
    $this->_maxImageLoadTime = $this->params->get( 'maxImageLoadTime', 0 );
    if ( $this->_maxImageLoadTime <= 0 )
    {
      $this->_maxImageLoadTime = 9999; /* Ridicously high: clamped below. */
    }
    $this->_maxImageLoadTime = max( 1, min( 11 /* seconds */, $this->_maxImageLoadTime ) );
    $this->_thumbWidth = $this->params->get( 'thumbWidth', 0 );
    $this->_thumbHeight = $this->params->get( 'thumbHeight', 0 );
    $this->_minimum[ 'width' ] = $this->params->get( 'minimumImageWidth', 0 );
    $this->_minimum[ 'height' ] = $this->params->get( 'minimumImageHeight', 0 );
    $this->_minimum[ 'ratio' ] = max( 0.05, min( 0.95, $this->params->get( 'minimumImageRatio', 0 ) ) );
  }

  /**
   * Searches for an image that passes all constraints set in the configuration settings; removes that image from the
   * article's text - if present - and returns HTML code containing a thumbnail to that image, readily suitable for
   * display.
   * @param JTableContent $article IN OUT. The item/article being prepared for display.
   * @param string $articleText IN, OUT. $article->text may have been adapted when this function returns.
   * @return String. The HTML code for displaying the styled, resized and linked first image according to the
   *   configuration settings, the empty string otherwise.
   */
  private function _GetThumbnailHtml( $article )
  {
    /* - First try to get all data from the cached information. If this succeeds, we can avoid using regular expressions.
     * - Else, try to find one in the full unshortened article.
     * - Else, try to find one in a possibly referenced gallery.
     * - Else, try to find a default thumbnail.
     */
    $thumbnailUrl = $this->_GetCachedThumbnail( $article->text );
    if ( !/*NOT*/ $thumbnailUrl )
    {
      $thumbnailUrl = $this->_PopImage( $this->_htmltext );
      if ( !/*NOT*/ $thumbnailUrl )
      {
        $thumbnailUrl = $this->_PopGalleryImage( $this->_htmltext );
        if ( !/*NOT*/ $thumbnailUrl )
        {
          $thumbnailUrl = $this->_GetDefaultThumbnail();
        }
      }
    }

    if ( $thumbnailUrl )
    {
      $attributes = $this->_GetImageAttributes( $this->_thumbnailTitle, $this->_thumbWidth, $this->_thumbHeight );
      if ( $this->_linkThumbnail )
      {
        $thumbnailHtml = '<a href="' . $this->_articleUrl . '"><img src="' . $thumbnailUrl. '"' . $attributes . '/></a>';
      }
      else
      {
        $thumbnailHtml = '<img src="' . $thumbnailUrl. '"' . $attributes . '/>';
      }
    }
    else
    {
      $thumbnailHtml = false;
    }

    return $thumbnailHtml;
  }

  /**
   * Fetches all cached image information. If the cached information is valid, it is used to construct the HTML code
   * containing a thumbnail, readily suitable for display. The image used to create the thumbnail, if present in the
   * article, will be stripped from the article's text.
   * @param string $articleText OUT. the string to adapt.
   * @return String. The HTML code for displaying the styled, resized and
   *   linked image according to the configuration settings, the empty
   *   string otherwise.
   */
  private function _GetCachedThumbnail( &$articleText )
  {
    $isValid = false;
    if ( $this->_cache )
    {
      $imageTagStartPos = $this->_cache->Get( 'image_tag_start_pos' );
      $imageTagLength = $this->_cache->Get( 'image_tag_length' );
      $imageUrl = $this->_cache->Get( 'image_url' );
      $thumbnailUrl = $this->_cache->Get( 'thumbnail_url' );

      if ( $thumbnailUrl )
      {
        $isValid = ReadLessTextThumb::ValidateThumbnail( $imageUrl, $thumbnailUrl, $this->_minimum, $this->_crop,
            $this->_thumbWidth, $this->_thumbHeight );
      }
    }

    if ( $isValid )
    {
      /* Check if the thumbnail was derived from an image referenced in the article text
       * i.e., check if it is not a default thumbnail.
       */
      if ( ( $imageTagStartPos >= 0 ) and ( $imageTagLength > 0 ) )
      {
        /* Remove the code for the image on the old location.
         * Don't do this blindly: it can be (it is likely, as I think it is a often used option)
         * that all images already have been removed from the given text.
         */
        if ( ( JString::substr( $articleText, $imageTagStartPos, 1 ) == '<' )
            and ( JString::substr( $articleText, $imageTagStartPos + $imageTagLength, 1 ) == '>' ) )
        {
          $imageUrlStartPos = strpos( $articleText, $imageUrl );
          if ( ( $imageTagStartPos < $imageUrlStartPos )
              and ( $imageUrlStartPos < $imageTagStartPos + $imageTagLength ) )
          {
            /* Ok, the portion we want to strip down does contain the url, and
             * does start and end with an opening resp. closing tag.
             * That's all we do to ascertain we will remove the correct portion.
             */
            $articleText = JString::substr_replace( $articleText, '', $imageTagStartPos, $imageTagLength );
          }
        }
      }
    }
    else
    {
      $thumbnailUrl = false;
    }

    return $thumbnailUrl;
  }

  /**
   * Searches for an image according to the default thumbnail template, and
   * returns HTML code containing a thumbnail to that image, readily
   * suitable for display.
   * @return String. The HTML code for displaying the styled, resized and
   *   linked image according to the configuration settings, the empty
   *   string otherwise.
   */
  private function _GetDefaultThumbnail()
  {
    if ( $this->_defaultThumbnail )
    {
      /* Be more lenient with respect to the default thumbnail: ensure it does not get rejected due to size and
       * dimension constraints.
       * The minimum contraints are not used any more after this (so no need to restore them afterwards).
       */
      $minimum[ 'width' ] = 0;
      $minimum[ 'height' ] = 0;
      $minimum[ 'ratio' ] = 0;
      $thumbnailUrl = ReadLessTextThumb::GetThumbnail( $this->_defaultThumbnail, $minimum, $this->_crop,
          $this->_thumbWidth, $this->_thumbHeight, $this->_cacheTime );

      if ( $thumbnailUrl )
      {
        $this->_cache->Set( 'image_tag_start_pos', -1 );
        $this->_cache->Set( 'image_tag_length', 0 );
        $this->_cache->Set( 'image_url', $this->_defaultThumbnail );
        $this->_cache->Set( 'thumbnail_url', $thumbnailUrl );
      }
    }
    else
    {
      $thumbnailUrl = false;
    }
    return $thumbnailUrl;
  }

  /**
   * Searches for an image in the article's text which is big enough according
   * to the configuration settings; removes that image from the article's
   * text, and returns HTML code containing a thumbnail to that image, readily
   * suitable for display.
   * @param string $articleText IN, OUT. the string to search in and to adapt.
   * @return String. The HTML code for displaying the styled, resized and
   *   linked first image according to the configuration settings, the empty
   *   string otherwise.
   * @note When an image is found, the results will have been cached when this function returns.
   */
  private function _PopImage( &$articleText )
  {
    $thumbnailUrl = false;
    $matchCount = preg_match( self::_imgPattern, $articleText, $matches, PREG_OFFSET_CAPTURE, 0 );
    while ( $matchCount )
    {
      $imageCode = $matches[0][0];
      $imageUrl = $matches[1][0];

      /* No acceptable image has been found yet, try this one. */
      $thumbnailUrl = ReadLessTextThumb::GetThumbnail( $imageUrl, $this->_minimum, $this->_crop, $this->_thumbWidth,
          $this->_thumbHeight, $this->_cacheTime, $this->_maxImageLoadTime );
      if ( $thumbnailUrl )
      {
        $imageTagStartPos = JString::strpos( $articleText, $imageCode );
        $imageTagLength = JString::strlen( $imageCode );

        /* Remove the code for the image on the old location. */
        $articleText = JString::substr_replace( $articleText, '', $imageTagStartPos, $imageTagLength );

        $this->_cache->Set( 'image_tag_start_pos', $imageTagStartPos );
        $this->_cache->Set( 'image_tag_length', $imageTagLength );
        $this->_cache->Set( 'image_url', $imageUrl );
        $this->_cache->Set( 'thumbnail_url', $thumbnailUrl );

        $matchCount = false; /* end the loop */
      }
      else
      {
        /* Prepare the next iteration
         * - The value of $offset determines where the next search starts.
         */
        $offset = $matches[ count( $matches ) - 1 ][1] + 1;
        $matchCount = preg_match( self::_imgPattern, $articleText, $matches, PREG_OFFSET_CAPTURE, $offset );
      }
    }

    return $thumbnailUrl;
  }

  /**
   * Searches for a tag which indicates a content gallery plugin is running. If one is found, it will search through the
   * content gallery for a suitable image (i.e. an image that satisfies the configuration constraints), and returns HTML
   * code containing a thumbnail to that image, readily suitable for display.
   * @param string $articleText IN, OUT. the string to search in and to adapt.
   * @return String. The HTML code for displaying the styled, resized and linked first image according to the
   *   configuration settings, the empty string otherwise.
   * @note When an image is found, the results will have been cached when this function returns.
   */
  private function _PopGalleryImage( &$articleText )
  {
    $thumbnailUrl = false;
    $matchCount = preg_match( self::_galleryPattern, $articleText, $matches, PREG_OFFSET_CAPTURE, 0 );
    while ( $matchCount )
    {
      $galleryCode = $matches[0][0];
      $galleryPath = $matches[2][0];

      /* No acceptable image has been found yet, try this one. */
      if ( $directoryContents = @dir( JPATH_SITE . '/images/' . $galleryPath ) )
      {
        while ( !/*NOT*/ $thumbnailUrl and $imagePath = $directoryContents->read() )
        {
          $imagePath = JPATH_SITE . '/images/' . $imagePath;
          $thumbnailUrl = ReadLessTextThumb::GetThumbnail( $imagePath, $this->_minimum, $this->_crop,
              $this->_thumbWidth, $this->_thumbHeight, $this->_cacheTime, $this->_maxImageLoadTime );
        }
        $directoryContents->close();
      }

      if ( $thumbnailUrl )
      {
        $imageTagStartPos = JString::strpos( $articleText, $galleryCode );
        $imageTagLength = JString::strlen( $galleryCode );

        /* Remove the code for the image on the old location. */
        $articleText = JString::substr_replace( $articleText, '', $imageTagStartPos, $imageTagLength );

        $this->_cache->Set( 'image_tag_start_pos', $imageTagStartPos );
        $this->_cache->Set( 'image_tag_length', $imageTagLength );
        $this->_cache->Set( 'image_url', $imagePath );
        $this->_cache->Set( 'thumbnail_url', $thumbnailUrl );

        $matchCount = false; /* end the loop */
      }
      else
      {
        /* Prepare the next iteration
         * - The value of $offset determines where the next search starts.
        */
        $offset = $matches[ count( $matches ) - 1 ][1] + 1;
        $matchCount = preg_match( self::_imgPattern, $articleText, $matches, PREG_OFFSET_CAPTURE, $offset );
      }
    }

    return $thumbnailUrl;
  }

  /**
   * Composes the attributes for the moved image, inclusive the inline CSS
   * attribute @c style.
   * This function does not make any modification.
   * @param alt: A string. The alternative text to be used for the image. Is also used as image title
   * @param $width: A Number. The size of the image in pixels. If 0 or negative, this value is not set.
   * @param $height: A Number. The size of the image in pixels. If 0 or negative, this value is not set.
   * @return string: Either the empty string when no attributes are needed;
   *   either a string ready for insertion as an attribute in a HTML tag.
   * @note Double quotes are used to quote the value.
   */
  private function _GetImageAttributes( $alt, $width = 0, $height = 0 )
  {
    $attributes = '';
    $styleValue = '';

    $attributes .= ' alt="' . $alt . '"';
    $attributes .= ' title="' . $alt . '"';
    if ( $width > 0 )
    {
      $attributes .= ' width="' . $width . '"';
    }
    if ( $height > 0 )
    {
      $attributes .= ' height="' . $height . '"';
    }

    $class = $this->params->get( 'thumbClass', '' );
    if ( $class )
    {
      $attributes .= ' class="' . $class . '"';
    }

    $imagePosition = $this->params->get( 'thumbPosition', 'left' );
    if ( $imagePosition )
    {
      $styleValue .= ' float:' . $imagePosition . ';';
    }
    $padding = $this->params->get( 'thumbPadding', -1 );
    if ( $padding >= 0 )
    {
      $styleValue .= ' padding:' . $padding . 'px;';
    }
    $margin = $this->params->get( 'thumbMargin', '' );
    if ( $margin and ( $margin[0] !== '-' /* a negative number starts with - */ ) )
    {
      $margin = JString::strtolower( $margin );
      $search = array( ' ', ';' );
      $replace = array( ',', ',' );
      $margin = JString::str_ireplace( $search, $replace, $margin );
      $marginList = explode( ',', $margin );
      $n = 0;
      while ( $n < count( $marginList ) )
      {
        if ( $marginList[ $n ] or ( $marginList[ $n ] === '0' ) )
        {
          if ( JString::strpos( '0123456789', $marginList[ $n ][ JString::strlen( $marginList[ $n ] ) - 1 ] ) === FALSE )
          {
            /* Ok, a unit is already appended */
          }
          else
          {
            $marginList[ $n ] .= "px";
          }
          $marginList[ $n ] = ' ' . $marginList[ $n ];
          $n++;
        }
        else
        {
          unset( $marginList[ $n ] );
          $marginList = array_values( $marginList ); /* re-index */
        }
      }
      $margin = implode( '', $marginList );
      $styleValue .= ' margin:' . $margin . ';';
    }
    $borderWidth = $this->params->get ( 'thumbBorderWidth', - 1 );
    if ($borderWidth >= 0) {
      $borderColor = $this->params->get ( 'thumbBorderColor', '#cccccc' );
      $borderStyle = $this->params->get ( 'thumbBorderStyle', '' );
      $styleValue .= ' border: ' . $borderWidth . 'px ' . $borderStyle . ' ' . $borderColor . ';';
    }
    if ( $styleValue )
    {
      $attributes .= ' style="' . $styleValue . '"';
    }

    return $attributes;
  }

  /**
   * Searches the given text for [token]...[/token] or {token}...{/token} constructs that must be removed.
   * @param string $text The text too operate on.
   * @note Possibly writes isShortened.
   * @return The stripped text.
   */
  private function _StripTokens( $text )
  {
    $settings = array (
        array ( 'pattern' => self::_tokenPattern,
            'opening' => '\[',
            'closing' => '\]',
            'tokens' => $this->_squareTokensToRemove
        ),
        array ( 'pattern' => self::_tokenPatternWithContents,
            'opening' => '\[',
            'closing' => '\]',
            'tokens' => $this->_squareTokensToRemoveWithContents
        ),
        array ( 'pattern' => self::_tokenPattern,
            'opening' => '{',
            'closing' => '}',
            'tokens' => $this->_curlyTokensToRemove
        ),
        array ( 'pattern' => self::_tokenPatternWithContents,
            'opening' => '{',
            'closing' => '}',
            'tokens' => $this->_curlyTokensToRemoveWithContents
        ) );
    /* Remove first without contents, then remove with contents. */
    foreach ( $settings as $setting )
    {
      foreach ( $setting[ 'tokens' ] as $token )
      {
        $search = array( self::_tokenOpeningReplacer, self::_tokenClosingReplacer, self::_tokenReplacer );
        $replace = array( $setting[ 'opening' ], $setting[ 'closing' ], $token );
        $pattern = JString::str_ireplace( $search, $replace, $setting[ 'pattern' ] );
        $count = 0;
        $text = preg_replace( $pattern, '', $text, -1, $count );
        if ( $count )
        {
          /* We have stripped away a - likely fancy - part of the article. The user must
           * get the possibility to access the full article - and thus that fancy part.
           */
          $this->_isShortened = true;
        }
      }
    }
    return $text;
  }

  /**
   * Cuts off the article text, retaining the full formatting. It uses regular expressions to find tags, and a simple
   * push/pop system to retain the opened but still-unclosed tags. When the text has been cut off after a
   * admin-configurable unit count, all the opened and still-unclosed tags are then closed.
   * @note No prefix nor suffix will be added in this function.
   * @post If the shortened string differs from the given @c text, @c _isShortened is set to @c true.
   * @param string $text RO. The HTML text to shorten.
   * @param integer $cutOffLength The size in number of @c _lengthUnit units of the plain text to retain, or @c false
   *   to indicate all text must be retained (safe the tags that must be removed according to the configuraion
   *   settings).
   * @return An array of strings: first 'the shortened text', second all 'the closing tags'. The second string may be
   *   empty - this indicates all text was to be retained, safe for the @c _tagsToRemove and
   *   @c _tagsToRemoveWithContents.
   */
  private function _CutOff( $text, $cutOffLength )
  {
    /* Treated as a fifo stack. A list of tags that have been opened but not yet closed. */
    $openTags = array();

    /* Portions of $text is continuously appended until the $cutOffLength is reached.
     * Will be returned as 'the shortened text'.
     */
    $shortened = array();

    /* Also the tag that follows whitespace is not immediately added to $shortened.
     * It is first stored in here.
     * When a new portion is found, this string is fully appended to $shortened.
     * When the loops ends, this string is expanded with all closing tags that still
     * need to be added (one for each opened tag that is still not yet closed) are
     * appended here at the end of this fucmtion, before returning.
     * Will be returned as 'the closing tags'
     */
    $closingTags = '';

    /* The total length of the plain text strings may not be bigger than the
     * maximum article length setting.
     */
    if ( $cutOffLength )
    {
      $nrOfUnitsYetToRetain = $cutOffLength;
    }
    else
    {
      $nrOfUnitsYetToRetain = PHP_INT_MAX;
    }

    /* Use an integer here, not a boolean.
     * If a tag is found which has to be removed with its content - i.e. it is
     * listed in @c _tagsToRemoveWithContents - we search for that specific
     * tagin the next loop. If can be that that same tag is nested, and that
     * an opening tag is found again. By using a counter we can be sure when
     * the outer closing tag has been found.
     */
    $removeAllUntilEndOfTag = 0;

    /* The position in bytes from where the search for the next match of the regular expression must start. */
    $offset = 0;

    /* A tag does not necessarily indicate the end of the 'word', 'sentence', or 'paragraph'.
     * When this boolean is false, the (first portion of the) next plaintext indicates the start of a new unit.
     * When this boolean is true, the (first portion of the) next plaintext belongs to the same 'word', etc.
     */
    $currentUnitIsOngoing = false;

    /* This is not a constant: normally we search for any tag, but when we know we have to skip portions
     * we can as well directly search for the corresponding end tag. This can be achieved by adapting the
     * tag pattern.
     */
    $tagPattern = JString::str_ireplace( self::_tagReplacer, self::_anyTag, self::_tagPattern );

    $continue = true;
    while ( $continue )
    {
      /* Find HTML tags. Each match is an array of (mostly) interesting data
       * about the tag - see @c _tagPattern. Using that, we can find the plain
       * text in front of it.
       */
      /* By using PREG_OFFSET_CAPTURE, each entry in the array returned will
       * be an array itself, containing the matched (sub)string and the starting
       * offset.
       */
      $matchCount = preg_match( $tagPattern, $text, $matches, PREG_OFFSET_CAPTURE, $offset );
      if ( !/*NOT*/ $matchCount or ( count( $matches ) < 4 ) )
      {
        /* No (more) HTML tags were found. We can not just assume that the last part
         * of the string is always a HTML tag, especially not when no wysiwig editor
         * has been used to create this article.
         * Fetch the remainder of text, and set the needed variables to some dummy value,
         * so that the loop can finish correctly.
         */
        $matchCount = preg_match ( "/.*/is", $text, $matches, PREG_OFFSET_CAPTURE, $offset );
        $plainText = $matches[0][0];
        $fullTag = "";
        $tag = "";
        $isSelfClosingTag = false;
        $isClosingTag = false;
        //$offset = /* Don't care */
        $continue = false;
      }
      else
      {
        $plainText = $matches[1][0];
        $fullTag = $matches[2][0];
        $tag = JString::strtolower( $matches[4][0] );

        /* Determine whether the tag we found is an opening tag (e.g. <abc>), a closing tag (e.g. </abc>)
         * or a self-closing tag (e.g. <abc />).
         * If a closing slash is present, it is captured by the regular expression, either in index 3 (<abc />),
         * either in the last-but-one index (<abc />) - and the last-but-one index is always greater than 3.
         */
        $isSelfClosingTag = $matches[ count( $matches) - 2 ][0] == '/';
        $isClosingTag = $matches[3][0] == '/';

        /* Those pesky html4 tags are still lingering around. Be sure to
         * correctly determine a tag as self-closing, even when no closing
         * slash was present. e.g. <br> vs. <br/>
         */
        if ( !/*NOT*/ $isClosingTag )
        {
          if ( in_array( $tag, $this->_extraSelfClosingTags ) )
          {
            $isSelfClosingTag = true;
          }
        }

        /* Prepare the next loop: the value of $offset determines where the next search starts. */
        $offset = $matches[ count( $matches ) - 1 ][1] + 1;
        /* $continue remains true for now. May become false below. */
      }

      if ( $removeAllUntilEndOfTag )
      {
        /* We're inside a block of code of which nothing may be retained in the cut-off text.
         * Do not add the plaintext and tag strings.
         */
        if ( $isClosingTag )
        {
          $removeAllUntilEndOfTag--;
          if ( $removeAllUntilEndOfTag <= 0 )
          {
            $tagPattern = JString::str_ireplace( self::_tagReplacer, self::_anyTag, self::_tagPattern );
          }
        }
        else
        {
          $removeAllUntilEndOfTag++;
        }
      }
      else
      {
        /* Determine the length of the just extracted portion of $text.
         * Depending on the outcome, and the current value of $currentUnitIsOngoing
         * that may need to be tweaked a bit.
         * e.g. we need to count in paragraphs, the match contains no plaintext and a paragraph ending:
         *   the current ongoing paragraph is then closed and must be counted.
         * e.g. we need to count in words, the match equals 'def ghi?<b>':
         *   the returned value will be two, but we may only count one if '?' equals 'j' (two if it equals ' ').
         * Also update $currentUnitIsOngoing to reflect the state after the current portion has been added.
         */
        $end = true;
        $length = ReadLessTextHelper::DetermineLength( $plainText . $fullTag, $this->_lengthUnit, $end );
        if ( $currentUnitIsOngoing and $end )
        {
          if ( !/*NOT*/ $length )
          {
            $length = 1;
          }
        }
        if ( $length and !/*NOT*/ $end )
        {
          $length--;
        }
        if ( $currentUnitIsOngoing )
        {
          if ( $end )
          {
            $currentUnitIsOngoing = false;
          }
        }
        else
       {
         if ( $length and !/*NOT*/ $end )
         {
           $currentUnitIsOngoing = true;
         }
        }

        /* Add the new plaintext. */
        if ( $length >= $nrOfUnitsYetToRetain )
        {
          $plainText = ReadLessTextHelper::Substr( $plainText, $nrOfUnitsYetToRetain, $this->_lengthUnit, $this->_retainWholeWords );
          $shortened[] = $plainText;
          $this->_isShortened = true;
          //$nrOfUnitsYetToRetain = /* Don't care */
          $continue = false;
        }
        else
        {
          $shortened[] = $plainText;
          $nrOfUnitsYetToRetain -= $length;
          /* $continue remains true. */
        }

        /* Determine what to append to the just-added plain text.
         * Also:
         * - update the list of opened-and-not-yet-closed tags and
         * - prepare the next loop (if there is another one).
         */
        if ( in_array( $tag, $this->_tagsToRemoveWithContents ) )
        {
          /* We don't know what will be stripped away in the shortened text:
           * it could be markup, a title or a table.
           * For sure is that the author wants this tag with its contents in the
           * full article, and that it may not taken along in the shortened
           * version. Even when all the remainder of the article fits in the
           * shortened text, it is best to ensure a pre- and/or suffix is
           * appended when done shortening.
           */
          $this->_isShortened = true;

          /* Do not add the tag. */
          if ( $isClosingTag or $isSelfClosingTag )
          {
            /* Nothing more to do. */
          }
          else
          {
            $removeAllUntilEndOfTag++;
            $tagPattern = JString::str_ireplace( self::_tagReplacer, $tag, self::_tagPattern );
          }
        }
        else if ( in_array( $tag, $this->_tagsToRemove )
            or in_array( 'all', $this->_tagsToRemove ) )
        {
          /* Do not add the tag. */
        }
        else if ( $isSelfClosingTag )
        {
          $shortened[] = $fullTag;
        }
        else if ( $isClosingTag )
        {
          $shortened[] = $fullTag;
          /* For simplicity, just assume at this point the text only contains
           * valid HTML, i.e. that all opening tags are properly closed in the
           * correct order. That means we do not need to check whether some
           * pushed opening tag matches with this closing tag: it just has to
           * be.
           */
          unset( $openTags[ count( $openTags ) - 1 ] );
          $openTags = array_values( $openTags ); /* re-index */
        }
        else if ( $tag )
        {
          if ( $continue )
          {
            /* The tag found is a valid opening tag that must remain in the cut-off text. */
            $shortened[] = $fullTag;
            $openTags[] = $tag;
          }
          else
          {
            /* No need to open a tag just at the point where we will stop. */
          }
        }
      }
    } /* while ( $continue ) */

    /* Not all parts of $shortened will become part of 'the shortened text'.
     * All last portions that are either
     * - a tag,
     * - an empty string,
     * - a whitespace string
     * will be added to 'the closing tags'.
     * Plus - after the removal of these elements - the then last plaintext string is right trimmed.
     * All this to allow to 'glue' the inline suffix right after the last retained plaintext
     * character.
     */
    $continue = true;
    $i = count( $shortened ) - 1;
    while ( $continue and ( $i > 0 ) )
    {
      if ( ( $shortened[ $i ] === '' ) /* an empty string */
          or ( substr_compare( $shortened[ $i ], '<', 0, 1 ) == 0 ) /* a tag */
          or ( ctype_space( $shortened[ $i ] ) ) ) /* a whitespace string */
      {
        $closingTags = $shortened[ $i ] . $closingTags;
        $shortened[ $i ] = '';
        $i--;
      }
      else
      {
        $continue = false;
      }
    }
    if ( $i > 0 )
    {
      $shortened[ $i ] = ReadLessTextHelper::Rtrim( $shortened[ $i ] );
    }
    $shortened = implode( '', $shortened );

    /* All tags that were opened and not yet closed are now closed here
     * in the correct order: i.e. in reverse order.
     */
    if ( count( $openTags ) > 0 )
    {
      $openTags = array_reverse( $openTags );
      $closingTags .= '</' . implode( '></', $openTags ) . '>';
    }

    return array ( $shortened, $closingTags );
  }

  /* *********************************************************************** */

  /**
   * Various options needed while operating on the text.
   * May only be set in _PrepareArticleText or _GetParams.
   * Once set, it is to be considered RO.
   * @{
   */
  private $_cache = null; /* Set correctly in _PrepareArticleText(). */

  private $_addPrefix = 'no'; /**< Must be checked after shortening, (if applicable) in combination with _isShortened. */
  private $_addInlineSuffix = 'no'; /**< Must be checked after shortening, (if applicable) in combination with _isShortened. */
  private $_addSuffix = 'no'; /**< Must be checked after shortening, (if applicable) in combination with _isShortened. */
  private $_respectExistingReadmoreLink = true; /* Set correctly in _GetParams() */
  private $_applyFormatting = 'no'; /**< Must be checked after checking the article's length, (if applicable) in combination with _isShortened. */
  private $_createThumbnail = 'no'; /**< Must be checked after shortening, (if applicable) in combination with _isShortened. */
  private $_linkThumbnail = true; /* Set correctly in _GetParams() */
  private $_thumbnailTitle = ''; /* Set correctly in _GetParams() */
  private $_prefix = ''; /* Set correctly in _GetParams() */
  private $_inlineSuffix = ''; /* Set correctly in _GetParams() */
  private $_suffix = ''; /* Set correctly in _GetParams() */
  private $_articleUrl = '.';
  private $_retainWholeWords = false;
  private $_crop = array( 'horizontal_position' => 'no', 'vertical_position' => 'no' );

  private $_cacheTime = 2419200 /* 4 weeks */;
  private $_thumbWidth = 0;
  private $_thumbHeight = 0;
  private $_minimum = array( 'width' => 0, 'height' => 0, 'ratio' => 0.05 );

  private $_extraSelfClosingTags = array();
  private $_tagsToRemove = array();
  private $_tagsToRemoveWithContents = array();
  private $_squareTokensToRemove = array();
  private $_curlyTokensToRemove = array();
  private $_squareTokensToRemoveWithContents = array();
  private $_curlyTokensToRemoveWithContents = array();

  private $_htmltext = ''; /* Set correctly in _PrepareArticleText() */
  private $_plaintext = ''; /* Set correctly in _PrepareArticleText() */
  private $_lengthUnit = 'char'; /* Set correctly in _PrepareArticleText() */
  private $_wordCount = 0; /* Set correctly in _PrepareArticleText() */
  private $_hash = 0; /* Set correctly in _PrepareArticleText(). */

  private $_wrapperTag = array( 'open' => '', 'close' => '' ); /* Set correctly in _PrepareArticleText(). */
  /** @} */

  /**
   * Can be set to true, when an existing manually inserted read more token
   * is found; or later on, when the formatting options cause the removal of
   * parts of the article; or when the article is automatically shortened and
   * some remainder of the article is excluded.
   * Once set to true, the variable may not be set to false again.
   */
  private $_isShortened = false; /* Reset in _GetParams() */
  /** @} /*

  /* *********************************************************************** */

  /**
   * Used to find an image tag in a text.
   *
   * A match found using this regular expression will return at each index:
   * [0] The complete @c img tag, inclusive the brackets and the attributes
   * [1] The value of @src attribute, i.e. the URL where the image can be fetched.
   * [last] The closing bracket. Used to know the precise end byte
   *     offset of the matched tag. Especially needed for multi byte strings
   *     (some of the attributes inside the tag might very well be that),
   *     since JString::strlen returns the number characters, not the number
   *     of bytes. The offset given to preg_match needs to be a byte offset.
   *
   * @note: It is not possible (or: I could not get it to work) to include correct
   *   positions together with the found matches when using non-ASCII UTF8 strings
   *   when using this pattern with preg_match.
   *   The matches returned seem correct though, and strpos can be used to fetch
   *   the starting UTF8 character index.
   * @note This is a simplified version of _tagPattern. Potentially this can
   *   match a great portion of the text, crossing several html tags, _if_ an img
   *   tag is given _without_ a src attribute. If this happens, give them what they
   *   are asking for. Or, in other words: don't worry about that.
   */
  const _imgPattern = '/<img.+?src\s*=\s*["\']([^"\']+)["\'][^>]*(>)/muis';
  /*                                           1111111            -1    */
  /*                    00000000000000000000000000000000000000000000    */

  /**
   * Used to find an content gallery token in a text.
   *
   * A match found using this regular expression will return at each index:
   * [0] The complete @c content gallery token, inclusive the gallery data and the closing token.
   * [1] The gallery data, i.e. the relative URL where the gallery images can be fetched.
   * [last] The closing bracket. Used to know the precise end byte offset of the matched tag. Especially needed for
   *   multi byte strings since JString::strlen returns the number characters, not the number of bytes. The offset given
   *   to preg_match needs to be a byte offset.
   */
  const _galleryPattern = '/{(gallery|vsig|ppgallery|becssg)}\s*([^<]+)\s*{\/\1}/muis';
  /*                                                             11111        -1     */
  /*                        0000000000000000000000000000000000000000000000000000     */

  /**
   * Used to parse an html text by finding all the HTML tags.
   * Thanks to http://kev.coolcavemen.com/2007/03/ultimate-regular-expression-for-html-tag-parsing-with-php/
   * It is a bit modified, to catch the tag without attributes and brackets,
   * the closing tag character '/', and the last char in the match as well.
   * @note Before usage, the string @c _tagReplacer in @c _tagPattern must be replaced with the tag to search for,
   *   or with @c _anyTag
   *
   * A match found using this regular expression will return at each index:
   * [0] The full match; i.e. the concatenation of everything below
   * [1] The plaintext preceding the tag
   * [2] The complete tag, inclusive the brackets and the attributes
   * [3] Either empty, either '/' when the tag is a closing tag.
   * [4] The tag name
   * [5] The full attributes - possibly not present
   * [6] ...
   * [last but one] If the tag is self closing: '/'. Else, and if attributes
   *     are present: the last attribute. Else: the empty string.
   * [last] The closing bracket. Used to know the precise end byte
   *     offset of the matched tag. Especially needed for multi byte strings
   *     (some of the attributes inside the tag might very well be that),
   *     since JString::strlen returns the number characters, not the number
   *     of bytes. The offset given to preg_match needs to be a byte offset.
   *
   * @note the @c xxx part is to be replaced
   * - replace with @c _anyTag to catch any tag
   * - replace with 'abc' to catch tag abc
   *
   * @{
   */
  const _tagPattern = "/(.*?)(<(\/?)(xxx)((\s+(\w|\w[\w-]*\w)(\s*=\s*(?:\".*?\"|'.*?'|[^'\">\s]+))?)+\s*|\s*)(\/?)(>))/muis";
  /*                     111    333  444   --- one attribute --------------------------------------           -2   -1      */
  /*                                      --- all attributes from index 5 until index -3 ------------                      */
  /*                          222222222222222222222222222222222222222222222222222222222222222222222222222222222222222      */
  /*                    0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000     */
  const _tagReplacer = 'xxx';
  const _anyTag = '\w+';
  /** @} */

  /**
   * @li _tokenPattern Used to find third party token indications like
   *   {slider=MySlider}
   *   {gallery path/to/images}
   * @li _tokenPatternWithContents Used to find third party token indications like
   *   [pgn]...[/pgn]
   *   {slide}...{slide}...{slides}
   *
   * What can not be detected:
   * @li nested token codes
   * @li self closing token codes
   * @li escaped starts of token codes
   *
   * A match found using this regular expression will return at each index:
   * @li _tokenPattern
   *   [0] The complete token code, inclusive the opening and closing character.
   * @li _tokenPatternWithContents
   *   [0] The complete opening token, the complete closign token, and all text in between.
   *
   * @note the @c xxx, @c yyy and @c zzz parts are to be replaced
   * - replace @c xxx with [ or { or ... to catch the start of a token
   * - replace @c yyy with ] or } or ... to catch the end of a token
   *
   * @{
   */
  const _tokenPattern = "/xxx\s*.?\s*zzz.*?yyy/muis";
  const _tokenPatternWithContents = "/xxx\s*zzz.*?yyy.*?xxx\s*\/\s*zzzs?\s*yyy/muis";
  const _tokenOpeningReplacer = 'xxx';
  const _tokenClosingReplacer = 'yyy';
  const _tokenReplacer = 'zzz';
  /** @} */

//     private $profiler = null;
//     private $profiles = null;
}

?>