В 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 и т.п.). Я класс переписал на мультибайтовые функции (надеюсь нигде ничего не пропустил). Теперь класс корректно подсчитывает кол-во символов и корректно режет все.
Остальные параметры не изучал. Так что если кому интересно, поиграйтесь, и если что будет полезное, отпишитесь.
{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, так как этот параметр не использовался ни как атрибут функции, ни как глобальная переменная.