Fi1osof 30 июля 2013 0 3
В 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
3 комментария
vgrish1
vgrish 30 июля 2013г в 23:44 #
мне вот интересно, а вы такие вещи достаточно быстро пишите? судя по всему как чайка попить)))
вообще он есть уже yandex.ru/yandsearch?text=modx%20revolution%20summary&clid=1987478&lr=213
Fi1osof1
Fi1osof 31 июля 2013г в 09:16 #
Ну, топик этот я наверно дольше писал :-)
Ссылка на поисковик — не очень юзабельно. Надо бы на конкретную страницу отправлять, а то кто знает, что там через несколько дней в результатах будет. Хотя я предполагаю, что имели ввиду класс от Агеля blog.agel-nash.ru/addon/summary.html
Я как-то на него наткнулся уже после того, как класс переписал. Но в целом там тоже самое почти, то есть тот же переписанный класс из Ditto. Но работает он только с конкретно указанной строковой переменной, а не с массивом ресурса (что в принципе совершенно не критично).
Fi1osof1
Fi1osof 22 августа 2013г в 22:38 #
Добавил параметр endTags.
Авторизуйтесь или зарегистрируйтесь (можно через соцсети ), чтобы оставлять комментарии.