| Current Path : /home/annegardxb/www/so-assurances/plugins/content/readlesstext/ |
| Current File : /home/annegardxb/www/so-assurances/plugins/content/readlesstext/readlesstexthelper.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;
jimport( 'joomla.plugin.plugin' );
jimport( 'joomla.utilities.date' );
class ReadLessTextHelper
{
/**
* Checks whether it is allowed to run.
* This function does not make any modification, except when in discover
* mode: @c $article->text will then be set to the discover information.
* @param JTableContent $article the item fetched from the database
* @param params $params the parameters to use
* @param dict $options OUT the key 'discover' with a boolean value will be
* filled in.
* @param bool $activeByDefaultOnAllContentItems True if all content items
* must be shortened by 'read less', unless explicitly disallowed.
* @param string $pluginName When Discover mode is active, this string
* will be added to the discover information.
* @param string $extraDiscoverInfo When Discover mode is active, this string
* will be added to the discover information.
* @return Boolean value
*/
public static function Filter( &$callCount, $article, $params, &$options,
$activeByDefaultOnAllContentItems = false, $pluginName, $extraDiscoverInfo = '' )
{
$app = JFactory::getApplication();
$current = array();
$current[ 'component' ] = JRequest::getWord( 'option' );
$current[ 'scope' ] = $app->scope;
$current[ 'view' ] = JRequest::getWord( 'view' );
$current[ 'viewId' ] = JRequest::getInt( 'id' );
$current[ 'layout' ] = JRequest::getWord( 'layout' );
$current[ 'articleId' ] = self::GetArticleId( $article );
$current[ 'articleSlug' ] = self::GetArticleSlug( $article );
$current[ 'articleCategoryId' ] = self::GetCategoryId( $article );
$articleNumberSkipCount = max( 0, $params->get( 'articleNumberSkipCount' ) );
$articleNumberShortenCount = max( 0, $params->get( 'articleNumberShortenCount' ) );
$discoverTextAboutScopeType = '';
$discoverTextAboutCallCount = '';
$lastMatchingContext = false;
$contexts = array(); /* Used to contain all contexts that match the item for this code is executed. */
$contextDescriptions = array(); /* Only used to facilitate Discover mode. */
$possiblyInterchangeable = array(); /* Only used to facilitate Discover mode. */
$scope = array( 'com_' => $params->get( 'componentScope' ), 'mod_' => $params->get( 'moduleScope' ) );
$key = JString::substr( $current[ 'scope' ], 0, 4 /* com_ or mod_ */ );
if ( !/*NOT*/key_exists( $key, $scope ) )
{
$scope[ $key ] = 'never';
}
switch ( $scope[ $key ] )
{
case 'always':
$discoverTextAboutScopeType .= "<br/>This plugin must always be active on <tt>" . $current[ 'scope' ] . "</tt>.";
$allowed = true;
break;
case 'never':
$discoverTextAboutScopeType .= "<br/>This plugin may never be active on <tt>" . $current[ 'scope' ] . "</tt>.";
$allowed = false;
break;
case 'accordingToContexts':
default:
$allowed = self::_CheckCallCount( $callCount, $articleNumberSkipCount, $articleNumberShortenCount, $discoverTextAboutCallCount );
$callCount++;
if ( $allowed )
{
if ( $params->get( 'when', '0' ) == '0' )
{
/* common usage */
if ( $activeByDefaultOnAllContentItems )
{
$allowedFilters = 'com_content';
$disallowedFilters = '';
}
else
{
$allowedFilters = 'com_content:blog, com_content:categories, com_content:category, com_content:featured, com_content:frontpage, com_content:section';
$disallowedFilters = '';
}
}
else
{
/* specific usage */
$allowedFilters = $params->get( 'allowed', '' );
$disallowedFilters = $params->get( 'disallowed', '' );
}
if ( $activeByDefaultOnAllContentItems and ( $current[ 'component' ] == 'com_content' ) )
{
$allowedFilters = 'com_content, ' . $allowedFilters;
}
$contexts = self::_GetContexts( $current, $contextDescriptions, $possiblyInterchangeable );
$allowed = self::_CheckContexts( $contexts, $allowedFilters, $disallowedFilters, $lastMatchingContext );
}
break;
}
/* An extra performance penalty is present in Joomla 2.5.5+
* Since that version, all articles on a page are processed by content plugins,
* even when it is known their content is not going to be visible.
* See http://joomlacode.org/gf/project/joomla/tracker/?action=TrackerItemEdit&tracker_item_id=28591
* Examples are the list of article titles at the bottom of an article category blog,
* or an article list page.
* For read less text (again), these articles can better be skipped immediately.
* Both view and layout are checked to see if one of these two situations occur.
* It is also limited for component articles of com_content, as I'm uncertain whether other components
* behave the same. Better the (for most people small) performance penalty than a faulty behavior.
* * Thank you for this, gabs087. *
*/
if ( ( $key == 'com_' ) and ( $current[ 'component' ] == 'com_content' ) )
{
if ( ( $current[ 'view' ] == 'category' ) and ( $current[ 'layout' ] != 'blog' ) )
{
/* Don't process articles in category lists. */
$allowed = false;
}
if ( ( $current[ 'view' ] == 'featured' ) or ( $current[ 'layout' ] == 'blog' ) )
{
$maxVisibleArticleCount = $article->params->get( 'num_leading_articles', 0 )
+ $article->params->get( 'num_intro_articles', 0 );
if ( ( $maxVisibleArticleCount > 0 ) and ( $callCount > $maxVisibleArticleCount ) )
{
/* Don't process articles on blog pages whose content will not be part of the page. */
$allowed = false;
}
}
}
$discover = $params->get( 'discover', false );
if ( $discover )
{
$version = new JVersion();
$discover = JFactory::getUser()->authorise( 'core.login.admin' );
}
if ( $options !== NULL )
{
/* Store this value so that the same logic in this function doen't need to be performed later on. */
$options[ 'discover' ] = $discover;
}
if ( $discover )
{
$article->text = self::_GetDiscoverText( $pluginName, $allowed, $current, $contexts, $contextDescriptions,
$possiblyInterchangeable, $lastMatchingContext, $discoverTextAboutScopeType, $discoverTextAboutCallCount,
$extraDiscoverInfo );
$article->introtext = $article->text;
$article->fulltext = "";
}
return $allowed;
}
private static function _CheckContexts( $contexts, $allowedFilters, $disallowedFilters, &$lastMatchingContext )
{
$allowed = true;
/* Loop over key (may be active if filter does not match) value (parameter name) pairs */
$loop = array( false => $allowedFilters, true => $disallowedFilters);
foreach ( $loop as $defaultAllowed => $filters )
{
if ( $filters )
{
/* A bit more manipulation is required here: $filters can be given in
* different formats
* @li {component}:{view}:id targets a specific article displayed in all the views of the given type.
* @li {component}:{view}=nr:id targets a specific article displayed in the given view only.
* @li {component}:id targets a specific article displayed in any view.
* @li {component}:{view} targets all articles displayed in all the views of the given type.
* @li {component}:{view}=nr targets all articles displayed in the given view .
* @li {component} targets all articles of that component.
* @note If {component}: is not given, com_content: is assumed.
* @note If {view} is not given, it is not checked for.
* Plus contexts may be given on different lines.
* The string manipulations below ensure that all filters start with a component name
* followed by a view (with or without nr) and/or an id.
*/
$filters = ',' . JString::strtolower( $filters );
$filters = preg_replace( '/[\r\n]+/', ',', $filters );
$search = array( ' ', ',', '+com_', '+all', '+' );
$replace = array( '', ',+', 'com_', 'all', 'com_content:' );
/* A BB CCCCC DDDD EEEEEEEEEEE
* A: remove all whitespaces
* B: append a + (a character that can not occur in a correct context) after each ,
* C: remove all + if it was already followed by a component name
* D: remove all + if it was already followed by the special keyword all
* E: all remaining + chars indicate the absence of a component name. Fill in the default
*/
$filters = JString::str_ireplace( $search, $replace, $filters );
$filterList = explode( ',', $filters );
$filterAllows = $defaultAllowed;
foreach ( $contexts as $c )
{
if ( in_array( $c, $filterList ) !== FALSE )
{
$filterAllows = !/*NOT*/$defaultAllowed;
$lastMatchingContext = $c;
break;
}
}
$allowed &= $filterAllows;
}
else
{
/* There is no restriction set. Retain the default or already determined value for $allowed. */
}
}
return $allowed;
}
private static function _GetContexts( $current, &$contextDescriptions, &$possiblyInterchangeable )
{
$contexts = array();
/* Category/section/other descriptions have to be explicitly enabled.
* Do not include the more general compact indications of the current
* page/article in that case.
*/
if ( $current[ 'articleId' ] != 0 )
{
$contexts[] = $current[ 'component' ];
$contextDescriptions[] = 'all pages of this component';
$contexts[] = $current[ 'component' ] . ':' . $current[ 'view' ];
$contextDescriptions[] = 'all similar pages';
if ( $current[ 'viewId' ] )
{
$contexts[] = $current[ 'component' ] . ':' . $current[ 'view' ] . '=' . $current[ 'viewId' ];
$contextDescriptions[] = 'all items on this page only';
$possiblyInterchangeable[] = $contexts[ count( $contexts ) - 1 ];
}
}
$contexts[] = $current[ 'component' ] . ':' . $current[ 'articleId' ];
$contextDescriptions[] = 'this item only on all pages';
$contexts[] = $current[ 'component' ] . ':' . 'all-in-' . $current[ 'articleCategoryId' ];
$contextDescriptions[] = 'all items from category ' . $current[ 'articleCategoryId' ] . ' on all pages';
$contexts[] = $current[ 'component' ] . ':' . $current[ 'view' ] . ':' . $current[ 'articleId' ];
$contextDescriptions[] = 'this item only on all similar pages';
$contexts[] = $current[ 'component' ] . ':' . $current[ 'view' ] . ':' . 'all-in-' . $current[ 'articleCategoryId' ];
$contextDescriptions[] = 'all items from category ' . $current[ 'articleCategoryId' ] . ' on all similar pages';
$possiblyInterchangeable[] = $contexts[ count( $contexts ) - 1 ];
if ( $current[ 'viewId' ] )
{
$contexts[] = $current[ 'component' ] . ':' . $current[ 'view' ] . '=' . $current[ 'viewId' ] . ':' . $current[ 'articleId' ];
$contextDescriptions[] = 'this item only on this page only';
$contexts[] = $current[ 'component' ] . ':' . $current[ 'view' ] . '=' . $current[ 'viewId' ] . ':' . 'all-in-' . $current[ 'articleCategoryId' ];
$contextDescriptions[] = 'all items from category ' . $current[ 'articleCategoryId' ] . ' on this page only';
$possiblyInterchangeable[] = $contexts[ count( $contexts ) - 1 ];
}
return $contexts;
}
private static function _CheckCallCount( $callCount, $articleNumberSkipCount, $articleNumberShortenCount, &$discoverTextAboutCallCount )
{
$allowed = false;
if ( $callCount < $articleNumberSkipCount )
{
$discoverTextAboutCallCount .= "<br/>This plugin may only become active on this page after skipping "
. $articleNumberSkipCount . " article(s) or item(s) on this page (still "
. ( $articleNumberSkipCount - $callCount ) . " to skip).";
}
else if ( ( $articleNumberShortenCount == 0 )
or ( $callCount < $articleNumberSkipCount + $articleNumberShortenCount ) )
{
$allowed = true;
}
else
{
$discoverTextAboutCallCount .= "<br/>This plugin may only become active on this page for "
. $articleNumberShortenCount . " articles or items on this page.";
}
return $allowed;
}
private static function _GetDiscoverText( $pluginName, $allowed, $current, $contexts, $contextDescriptions,
$possiblyInterchangeable, $lastMatchingContext, $discoverTextAboutScopeType, $discoverTextAboutCallCount,
$extraDiscoverInfo )
{
$enableOrDisable = array( true => "disable", false => "enable" );
$activeOrNot = array( true => "<strong>active</strong>", false => "<strong>not active</strong>" );
$text = "<p>";
$text .= "<tt>" . $pluginName . "</tt> is " . $activeOrNot[ $allowed ] . " on this item ";
if ( $allowed )
{
$text .= "(provided the contents' length is large enough).";
}
$text .= "</dt><dd>";
$text .= " " . $discoverTextAboutScopeType . "<br>";
$text .= " " . $discoverTextAboutCallCount . "<br>";
$text .= "</p>";
$text .= "<dl><dt><strong>Information</strong> you can use to create your own contexts:</dt><dd>";
$text .= " component: <tt>" . $current[ 'component' ] . "</tt></br>";
$text .= " scope: <tt>" . $current[ 'scope' ] . "</tt></br>";
$text .= " layout: <tt>" . $current[ 'layout' ] . "</tt></br>";
$text .= " view: <tt>" . $current[ 'view' ] . "</tt></br>";
$text .= " view id: <tt>" . $current[ 'viewId' ] . "</tt></br>";
$text .= " item id: <tt>" . $current[ 'articleId' ] . "</tt></br>";
$text .= " category id of item: <tt>" . $current[ 'articleCategoryId' ] . "</tt>";
$text .= "</dd></dl>";
if ( $lastMatchingContext )
{
/* Maybe active, maybe not, but at least one context matched. */
$text .= "<strong>The last context you configured that matched the current item is <tt>" . $lastMatchingContext . "</tt></strong>";
if ( !/*NOT*/$allowed )
{
$text .= "<br/>If you want to enable the plugin on this item on this page, you minimally need to remove or change this context.";
}
}
else if ( $allowed )
{
/* Active, but no context ever matched. */
$text .= "<br/>There are no contexts listed where <tt>" . $pluginName . "</tt> is allowed to be active, so it is <strong>active by default</strong>.";
}
else
{
if ( count( $contexts ) == 0 )
{
/* Not active, and no contexts have been reseaRched. Do not print anything about contexts. */
}
else
{
/* Not active, but no context ever matched. */
$text .= "<br/>No context matches the current item, so it is <strong>not active by default</strong>.";
}
}
$text .= "</p>";
if ( count( $contexts ) == 0 )
{
/* Not active, and no contexts have been reserached. Do not print anything about contexts. */
}
else
{
$text .= "<dl><dt><strong>Contexts matching this article</strong>: to " . $enableOrDisable[ $allowed ] . " <tt>" . $pluginName . "</tt> on</dt><dd>";
for ( $i = 0; $i < count( $contexts ); $i++)
{
$text .= " " . $contextDescriptions[$i] . ", use <tt>" . $contexts[$i] . "</tt><br/>";
}
$text .= "</dd></dl>";
}
$text .= "<p>";
if ( $current[ 'articleId' ] == $current[ 'viewId' ] )
{
$text .= "<strong>Note</strong>: if the view name <tt>"
. $current[ 'view' ]
. "</tt> serves to display a single item/article, the contexts <tt>"
. implode( '</tt>, <tt>n', $possiblyInterchangeable )
. "</tt> may yield the same result and are interchangeable.<br/>";
}
$text .= "<strong>Note</strong>: this discover information is only displayed to users with back-end permissions and can be disabled in the back-end.<br/>";
if ( count( $contexts ) == 0 )
{
/* Not active, and no contexts have been researched. Do not print anything about contexts. */
}
else
{
$text .= "<dl><dt><strong>General</strong>: all contexts follow this syntax: <tt>component:view:item</tt></dt><dd>";
$text .= " if the <tt>ncomponent</tt> is left out, <tt>com_content:</tt> is assumed;<br/>";
$text .= " if the <tt>nview</tt> is left out, <tt>all</tt> views match;<br/>";
$text .= " if the <tt>item</tt> is left out, <tt>all</tt> items match.<br/>";
$text .= "</dd></dl>";
}
if ( $extraDiscoverInfo )
{
$text .= "<br/>";
$text .= "<strong>" . $extraDiscoverInfo . "</strong>";
}
$text .= "</p>";
return $text;
}
public static function Trim( $str )
{
return preg_replace( "/(^\s+)|(\s+$)/us", "", $string );
}
public static function Rtrim( $string )
{
return preg_replace( "/\s+$/us", "", $string );
}
/**
* Determines if @c $string ends with $lastPart.
* @param string $string The string to examine
* @param string $lastPart The substring to find at the end of @c $string
* @return true if @c $string ends with @c $lastPart, false otherwise
*/
private static function _EndsWith( $string, $lastPart )
{
if ( strlen( $string ) < strlen( $lastPart ) )
{
$endsWith = false;
}
else
{
if ( substr_compare( $string, $lastPart, -1 * strlen( $lastPart ) ) )
{
$endsWith = false;
}
else
{
$endsWith = true;
}
}
return $endsWith;
}
/**
* Determines the length of the given article text.
* @param string $htmltext The html text to consider.
* @param string $lengthUnit Unit of the length to determine. One of 'char',
* 'word', 'sentence', 'paragraph'.
* @param bool $end OUT If true, the last part of @c htmlText ends the
* ongoing length unit.
* @return the length expressed in the given unit.
* @pre it is assumed subsequent whitespace has already been removed.
*/
public static function DetermineLength( $htmltext, $lengthUnit, &$end )
{
switch ( $lengthUnit )
{
case 'sentence':
/* Calculation is done using a list of sentences.
* Exclude empty sentences, exclude sentences with only markup,
* exclude consecutive punctuation characters.
* The text is also trimmed to determine afterwards whether the last
* sentence has ended.
* Paragraph, row and list item demarcations also mark the end of a sentence.
*
* Ensure that all sentence endings can be treated alike.
*/
$search = array( '</p>', '</li>', '</dt>', '</dl>', '</tr>', '#', '.', '?', '!', '¿' );
$replace = array( '.', '.', '.', '.', '.', '_', '#', '#', '#', '#' );
$htmltext = JString::str_ireplace( $search, $replace, $htmltext );
$text = rtrim( strip_tags( $htmltext ) );
$length = 0;
foreach ( explode( '#', $text ) as $sentence )
{
if ( preg_split( '/\s+/', $sentence, 1, PREG_SPLIT_NO_EMPTY ) )
{
$length++;
}
}
$end = self::_EndsWith( $text, '#' );
break;
case 'paragraph':
/* Calculation is done using a list of non-empty paragraphs.
* Exclude empty paragraphs, exclude paragraps with only markup.
* The text is trimmed first to determine whether the last paragraph
* has ended afterwards.
* Table and list demarcations also mark the end of a sentence.
* Ensure that all paragraph endings can be treated alike.
*/
$search = array( '</ul>', '</ol>', '</dl>' );
$replace = array( '</p>', '</p>', '</p>' );
$htmltext = JString::str_ireplace( $search, $replace, $htmltext );
$htmltext = rtrim( $htmltext );
$length = 0;
foreach ( explode( '</p>', $htmltext ) as $paragraph )
{
$paragraph = strip_tags( $paragraph );
if ( preg_split( '/\s+/', $paragraph, 1, PREG_SPLIT_NO_EMPTY ) )
{
$length++;
}
}
$end = self::_EndsWith( $htmltext, '</p>' );
break;
case 'word':
/* Paragraph, row and list item demarcations also mark the end of a word.
* Ensure that all word endings can be treated alike.
*/
$search = array( '</p>', '</li>', '</dt>', '</dl>', '</tr>', '.', '?', '!', '¿' );
$replace = array( ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' );
$htmltext = JString::str_ireplace( $search, $replace, $htmltext );
/* Calculation only needs the plaintext. */
$text = strip_tags( $htmltext );
$length = count( preg_split( '/\s+/', $text, -1, PREG_SPLIT_NO_EMPTY ) );
$end = self::_EndsWith( $text, ' ' );
break;
case 'char':
default:
/* Calculation only needs the plaintext. */
$text = strip_tags( $htmltext );
/* Subsequent whitespace must be counted as one when $lengthUnit equals 'char' */
$text = preg_replace( '/\s+/mis', ' ', $text );
$length = JString::strlen( $text );
$end = true;
break;
}
return $length;
}
/**
* Returns the id of the article this plugin is being called upon.
* Works for com_content and com_eventlist items,
* and others (list?)
* @param JTableContent $article IN The item/article being prepared for display.
* @return A number.
*/
public static function GetArticleId( $article )
{
$id = 0;
foreach ( array( 'id', 'did', 'cid' ) as $field )
{
if ( isset( $article->$field ) )
{
$id = (int)$article->$field;
break;
}
}
return $id;
}
/**
* Returns the slug of the article this plugin is being called upon.
* Works for com_content and com_eventlist items,
* and others (list?)
* @param JTableContent $article IN The item/article being prepared for display.
* @return A string.
*/
public static function GetArticleSlug( $article )
{
$id = self::GetArticleId( $article );
if ( isset( $article->slug ) and $article->slug )
{
$slug = $article->slug;
}
else if ( isset( $article->alias ) and $article->alias )
{
$slug = $id . ':' . $article->alias;
}
else
{
if ( isset( $article->title ) and $article->title )
{
$slug = $id . ':' . JApplication::stringURLSafe( $article->title );
}
else if ( isset( $article->name ) and $article->name )
{
$slug = $id . ':' . JApplication::stringURLSafe( $article->name );
}
else
{
$slug = $id;
}
}
return $slug;
}
/**
* Returns the id of the category of the article this plugin is being called
* upon.
* Works for com_content and com_eventlist items,
* and others (list?)
* @param JTableContent $article The item/article being prepared for display.
* @return A number.
*/
public static function GetCategoryId( &$article )
{
$id = 0;
foreach ( array( 'catid', 'catsid' ) as $field )
{
if ( isset( $article->$field ) )
{
$id = (int)$article->$field;
break;
}
}
return $id;
}
/**
* Returns the slug of the category of the article this plugin is being called
* upon.
* Works for com_content and com_eventlist items,
* and others (list?)
* @param JTableContent $article The item/article being prepared for display.
* @return A string.
*/
public static function GetCategorySlug( &$article )
{
$id = self::GetCategoryId( $article );
if ( isset( $article->catslug ) and $article->catslug )
{
$slug = $article->catslug;
}
else if ( isset( $article->category_alias ) and $article->category_alias )
{
$slug = $id . ':' . $article->category_alias;
}
else
{
$slug = $id;
}
return $slug;
}
/**
*
* @param string $plainText
* @param integer $length
* @param string $lengthUnit Unit of the length. One of 'char',
* 'word', 'sentence', 'paragraph'.
* @param bool $retainWholeWords Only used when $lengthUnit equals 'char'
*/
public static function Substr( $plainText, $length, $lengthUnit, $retainWholeWords = false )
{
$substr = $plainText;
switch ( $lengthUnit )
{
case 'sentence':
$substrByteLength = 0;
$matches = preg_split( '/([.?!¿]+)/mis', $plainText, $length + 1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE );
/* Retain $length sentences and the line ending of each sentence. */
$i = min( count( $matches ), $length * 2 );
$substrByteLength = $matches[ $i ][1]; /* Start position of first match not to retain. */
$substr = substr( $plainText, 0, $substrByteLength);
break;
case 'paragraph':
/* Default value is correct. Nothing to do. */
break;
case 'word':
$words = preg_split( '/\s+/mis', $plainText, $length + 1, PREG_SPLIT_NO_EMPTY );
$words = array_slice( $words, 0, $length );
$substr = implode( ' ', $words );
break;
case 'char':
default:
$words = preg_split( '/(\s+)/mis', $plainText, $length + 1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );
$i = 0;
while ( $i < count( $words ) )
{
if ( ctype_space( $words[ $i ] ) )
{
$wordLength = 1; /* Count whitespace separating the words as one char. */
}
else
{
$wordLength = JString::strlen( $words[ $i ] );
}
if ( $length >= $wordLength )
{
$length -= $wordLength;
$i++;
}
else
{
break;
}
}
$lastChars = '';
if ( ( $length > 0 ) and ( $i < count( $words ) ) )
{
if ( $retainWholeWords )
{
/* The $i-th word is to be cut in half.
* Retain the whole word if it is the first to retain,
* Toss it completely away otherwise.
*/
$i = max( 1, $i );
}
else
{
$lastChars = JString::substr( $words[ $i ], 0, $length );
}
}
/* $i represents the number of words to retain fully,
* $lastChars contains a few characters of the last word that is retained only partially.
*/
$words = array_slice( $words, 0, $i );
$words[] = $lastChars;
$substr = implode( ' ', $words );
break;
}
return $substr;
}
}
?>