Николай Ланец
30 июля 2013 г., 16:35

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.

Добавить комментарий