Summary для MODX Revolution

В Ditto для MODX Evolution был классный плагин — summary. Если кто не в курсе — он позволял получать сокращенные анонсы из контента страницы. С тех пор, как я перешел на Рево, пожалуй, именно этой штуки мне всегда и не хватало (в его аналог getResources эта штука не попала). Кто-то не поверит, но за эти годы я так и не написал альтернативы этому, и не нашел альтернативный пакет (хотя может и плохо искал). А вот сегодня я взял, и написал замену :-) Точнее, я не с нуля это написал, а просто взял код из этого плагина для Ditto, и чуть-чуть его переписал в виде процессора для MODX-а. Под катом код процессора и некоторые особенности. В общем, этот функционал еще экспериментальный, и я его пока не планирую ни в какой пакет оформлять. Но вставил этот процессор в корневой процессор web/getdata. (про базовый процессор подробно писал здесь). После недолгой обкатки скорее всего включу его в сборку сайта. Так вот, теперь в вызов процессора достаточно передать логическое summary=>true, и в массиве конечных ресурсов будут элементы ['summary']. Вот код этих двух процессоров (оба они находятся в одном файле). <?php /* Базовый класс для выборки документов */

require_once MODX_CORE_PATH.'components/shopmodx/processors/web/getdata.class.php';

class modWebGetdataProcessor extends ShopmodxWebGetDataProcessor{

public function initialize(){
    
    $this->setDefaultProperties(array(
        'sort'              => "{$this->classKey}.menuindex",
        'dir'               => 'ASC',
        'showhidden'        => false,
        'showunpublished'   => false,
        'getPage'           => false,
        'limit'             => 10,
        'page'              => !empty($_REQUEST['page']) ? (int)$_REQUEST['page'] : 0,
        'summary'           => false,
    ));
    
    
    if($page = $this->getProperty('page') AND $page > 1 AND $limit = $this->getProperty('limit', 0)){
        $this->setProperty('start', ($page-1) * $limit);
    }
    
    return parent::initialize();
}

public function prepareQueryBeforeCount(xPDOQuery $c) {
    $c = parent::prepareQueryBeforeCount($c);
    
    $where = array(
        'deleted'   => false,
    );
    
    if(!$this->getProperty('showhidden', false)){
        $where['hidemenu'] = 0;
    }
    
    if(!$this->getProperty('showunpublished', false)){
        $where['published'] = 1;
    }
    
    $c->where($where);
    
    return $c;
}



public function afterIteration($list){
    $list = parent::afterIteration($list);
    
    if($this->getProperty('summary')){
        $properties = $this->getProperties();
        foreach($list as & $l){
            $l['summary'] = '';
            $trunc = new truncate($this->modx, array_merge($properties,array(
                'resource'  => $l, 
            )));
            if($response = $trunc->run() AND !$response->isError()){
                $l['summary'] = $response->getResponse();
            }
        }
    }
    
    return $list;
}     


public function outputArray(array $array, $count = false) {
    if($this->getProperty('getPage') AND $limit = $this->getProperty('limit')){
        $this->modx->setPlaceholder('total', $count);
        $this->modx->runSnippet('getPage@getPage', array(
            'limit' => $limit,
        ));
    }
    return parent::outputArray($array, $count);
}

}

class truncate extends modProcessor{

var $summaryType, $link, $output_charset;

public function initialize(){
    
    if(!$this->getProperty('resource')){
        return 'Не были получены данные ресурса';
    }
    
    $this->setDefaultProperties(array(
        'trunc'         => 1,
        'splitter'      => '<!-- splitter -->',
        'truncLen'      => 300,
        'truncOffset'   => 0,
        'truncsplit'    => '<!-- splitter -->',
        'truncChars'    => true,
        'output_charset'       => $this->modx->getOption('modx_charset'),
    ));
    
    $this->output_charset = $this->getProperty('output_charset');
    
    return parent::initialize();
}


function html_substr($posttext, $minimum_length = 200, $length_offset = 20, $truncChars=false) {

   // $minimum_length:
   // The approximate length you want the concatenated text to be


   // $length_offset:
   // The variation in how long the text can be in this example text
   // length will be between 200 and 200-20=180 characters and the
   // character where the last tag ends

   // Reset tag counter & quote checker
   $tag_counter = 0;
   $quotes_on = FALSE;
   // Check if the text is too long
   if (mb_strlen($posttext, $this->output_charset) > $minimum_length && $truncChars != 1) {

       // Reset the tag_counter and pass through (part of) the entire text
       $c = 0;
       for ($i = 0; $i < mb_strlen($posttext, $this->output_charset); $i++) {
           // Load the current character and the next one
           // if the string has not arrived at the last character
           $current_char = mb_substr($posttext,$i,1, $this->output_charset);
           if ($i < mb_strlen($posttext) - 1) {
               $next_char = mb_substr($posttext,$i + 1,1, $this->output_charset);
           }
           else {
               $next_char = "";
           }
           // First check if quotes are on
           if (!$quotes_on) {
               // Check if it's a tag
               // On a "<" add 3 if it's an opening tag (like <a href...)
               // or add only 1 if it's an ending tag (like </a>)
               if ($current_char == '<') {
                   if ($next_char == '/') {
                       $tag_counter += 1;
                   }
                   else {
                       $tag_counter += 3;
                   }
               }
               // Slash signifies an ending (like </a> or ... />)
               // substract 2
               if ($current_char == '/' && $tag_counter <> 0) $tag_counter -= 2;
               // On a ">" substract 1
               if ($current_char == '>') $tag_counter -= 1;
               // If quotes are encountered, start ignoring the tags
               // (for directory slashes)
               if ($current_char == '"') $quotes_on = TRUE;
           }
           else {
               // IF quotes are encountered again, turn it back off
               if ($current_char == '"') $quotes_on = FALSE;
           }

           // Count only the chars outside html tags
           if($tag_counter == 2 || $tag_counter == 0){
               $c++;
           }

           // Check if the counter has reached the minimum length yet,
           // then wait for the tag_counter to become 0, and chop the string there
           if ($c > $minimum_length - $length_offset && $tag_counter == 0) {
               $posttext = mb_substr($posttext,0,$i + 1, $this->output_charset);
               return $posttext;
           }
       }
   }  return $this->textTrunc($posttext, $minimum_length + $length_offset);
}

function textTrunc($string, $limit, $break=". ") {
// Original PHP code from The Art of Web: www.the-art-of-web.com

// return with no change if string is shorter than $limit
if(mb_strlen($string, $this->output_charset) <= $limit) return $string;

$string = mb_substr($string, 0, $limit, $this->output_charset);
if(false !== ($breakpoint = mb_strrpos($string, $break, 0, $this->output_charset))) {
  $string = mb_substr($string, 0, $breakpoint+1, $this->output_charset);
}

return $string;

}

function closeTags($text) {
	$debug = $this->getProperty('debug', false);
    
	$openPattern = "/<([^\/].*?)>/";
	$closePattern = "/<\/(.*?)>/";
	$endOpenPattern = "/<([^\/].*?)$/";
	$endClosePattern = "/<(\/.*?[^>])$/";
	$endTags = '';

	preg_match_all($openPattern, $text, $openTags);
	preg_match_all($closePattern, $text, $closeTags);

	if ($debug == 1) {
		print_r($openTags);
		print_r($closeTags);
	}

	$c = 0;
	$loopCounter = count($closeTags[1]); //used to prevent an infinite loop if the html is malformed
	while ($c < count($closeTags[1]) && $loopCounter) {
		$i = 0;
		while ($i < count($openTags[1])) {
			$tag = trim($openTags[1][$i]);

			if (mb_strstr($tag, ' ', false, $this->output_charset)) {
				$tag = mb_substr($tag, 0, mb_strpos($tag, ' ', 0, $this->output_charset), $this->output_charset);
			}
			if ($debug == 1) {
				echo $tag . '==' . $closeTags[1][$c] . "\n";
			}
			if ($tag == $closeTags[1][$c]) {
				$openTags[1][$i] = '';
				$c++;
				break;
			}
			$i++;
		}
		$loopCounter--;
	}

	$results = $openTags[1];

	if (is_array($results)) {
		$results = array_reverse($results);

		foreach ($results as $tag) {
			$tag = trim($tag);

			if (mb_strstr($tag, ' ', false, $this->output_charset)) {
				$tag = mb_substr($tag, 0, mb_strpos($tag, ' ',0 , $this->output_charset), $this->output_charset);
			}
			if (!mb_stristr($tag, 'br', false, $this->output_charset) && !mb_stristr($tag, 'img', false, $this->output_charset) && !empty ($tag)) {
				$endTags .= '</' . $tag . '>';
			}
		}
	}
	return $text . $endTags;
}

function process() {
    
    $resource = $this->getProperty('resource');
    $trunc = $this->getProperty('trunc');
    $splitter  = $this->getProperty('splitter');
    $truncLen = $this->getProperty('truncLen');
    $truncOffset =  $this->getProperty('truncOffset');
    $truncsplit = $this->getProperty('truncsplit');
    $truncChars = $this->getProperty('truncChars');
    
	$summary = '';
    
	$this->summaryType = "content";
	$this->link = false;
	$closeTags = true;
	// summary is turned off

	if ((strstr($resource['content'], $splitter)) && $truncsplit) {
		$summary = array ();

		// HTMLarea/XINHA encloses it in paragraph's
		$summary = explode('<p>' . $splitter . '</p>', $resource['content']);

		// For TinyMCE or if it isn't wrapped inside paragraph tags
		$summary = explode($splitter, $summary['0']);

		$summary = $summary['0'];
		$this->link = '[[~' . $resource['id'] . ']]';
		$this->summaryType = "content";

		// fall back to the summary text
	} else if (mb_strlen($resource['introtext'], $this->output_charset) > 0) {
			$summary = $resource['introtext'];
			$this->link = '[[~' . $resource['id'] . ']]';
			$this->summaryType = "introtext";
			$closeTags = false;
			// fall back to the summary text count of characters
	} else if (mb_strlen($resource['content'], $this->output_charset) > $truncLen && $trunc == 1) {
			$summary = $this->html_substr($resource['content'], $truncLen, $truncOffset, $truncChars);
			$this->link = '[[~' . $resource['id'] . ']]';
			$this->summaryType = "content";
			// and back to where we started if all else fails (short post)
	} else {
		$summary = $resource['content'];
		$this->summaryType = "content";
		$this->link = false;
	}

	// Post-processing to clean up summaries
	$summary = ($closeTags === true) ? $this->closeTags($summary) : $summary;
	return $summary;
}

}

return 'modWebGetdataProcessor'; Какие изменения появились в самом основном процессоре? 1. Новый параметр по умолчанию: 'summary' => false, То есть по умолчанию у нас truncate не выполняется. 2. Обработка массива объектов в цикле, если summary == true public function afterIteration($list){ $list = parent::afterIteration($list);

    if($this->getProperty('summary')){
        $properties = $this->getProperties();
        foreach($list as & $l){
            $l['summary'] = '';
            $trunc = new truncate($this->modx, array_merge($properties,array(
                'resource'  => $l, 
            )));
            if($response = $trunc->run() AND !$response->isError()){
                $l['summary'] = $response->getResponse();
            }
        }
    }
    
    return $list;

} При чем обратите внимание, что все параметры, переданные в основной процессор (или дефолтные), передаются и в процессор truncate. А теперь посмотрим, какие параметры принимает процессор truncate. public function initialize(){

if(!$this->getProperty('resource')){
    return 'Не были получены данные ресурса';
}

$this->setDefaultProperties(array(
    'trunc'         => 1,
    'splitter'      => '<!-- splitter -->',
    'truncLen'      => 300,
    'truncOffset'   => 0,
    'truncsplit'    => '<!-- splitter -->',
    'truncChars'    => true,
    'output_charset'  => $this->modx->getOption('modx_charset'),
));

$this->output_charset = $this->getProperty('output_charset');

return parent::initialize();

} 1. resource — массив данных ресурса. В нашем случае в основном процессоре уже данные у нас в массиве, но если вы захотите использовать этот процессор в отдельности, то не забывайте, что если у вас не массив данных, а объект документа, то передавать надо $resource->toArray(). 2. truncLen Вот это очень хитрая настройка, которую еще предстоит до конца изучить. Дело в том, что во-первых, ее поведение зависит от другой настройки — truncChars, то есть обрезать ли посимвольно. По умолчанию truncChars == true. Но если указать truncChars == false, то обрезать будет по словам. А второй момент — обрезается не просто так, до указанного символа, а обрезается до конца предложения (а иногда и до конца HTML-тега (это по-моему, когда truncChars == false)). В общем, это все надо очень досканально изучать. 3. output_charset. Это уже я добавил. Дело в том, что старый класс не был рассчитан на работу с мультибайтовыми кодировками (использовал простые strlen, substr и т.п.). Я класс переписал на мультибайтовые функции (надеюсь нигде ничего не пропустил). Теперь класс корректно подсчитывает кол-во символов и корректно режет все. Остальные параметры не изучал. Так что если кому интересно, поиграйтесь, и если что будет полезное, отпишитесь. Пример вызова в Smarty. {assign var=params value=[ "where" => [ "parent" => $modx->resource->id ] ,"limit" => 5 ,"getPage" => true ,"summary" => 1 ]}

{processor action="web/getdata" ns="unilos" params=$params assign=result} {foreach $result.object as $object} <div class="otzyv"><a href="{$object.uri}"><em>{$object.pagetitle}</em></a> <div> {$object.summary} </div> </div> {/foreach} P.S. В целом функционал сохранен. То есть если указан introtext, то берется из него. Если нет, то берется из content. UPD: Добавил параметр endTags. Переданный в него текст будет добавляться в конец обрезанного текста (к примеру многоточие). Почему правильней именно через этот параметр передавать концовку? Дело в том, что когда режется HTML, тогда окончание всегда закрывается открывающим HTML-тегом, и если там был какой-нибудь блочный тег (P, DIV и т.п.), то добавленный текст за пределами вызова процессора просто будет перенесен на новую строчку. А так он будет добавлен перед закрывающим тегом. Что интересно, судя по всему этот момент не использовался в MODX Evo, так как этот параметр не использовался ни как атрибут функции, ни как глобальная переменная. Актуальная версия скрипта: gist.github.com/Fi1osof/7ad19cf156303193da6d

мне вот интересно, а вы такие вещи достаточно быстро пишите? судя по всему как чайка попить))) вообще он есть уже yandex.ru/yandsearch?text=modx%20revolution%20summary&clid=1987478&lr=213

Ну, топик этот я наверно дольше писал :-) Ссылка на поисковик — не очень юзабельно. Надо бы на конкретную страницу отправлять, а то кто знает, что там через несколько дней в результатах будет. Хотя я предполагаю, что имели ввиду класс от Агеля blog.agel-nash.ru/addon/summary.html Я как-то на него наткнулся уже после того, как класс переписал. Но в целом там тоже самое почти, то есть тот же переписанный класс из Ditto. Но работает он только с конкретно указанной строковой переменной, а не с массивом ресурса (что в принципе совершенно не критично).

Добавил параметр endTags.